mirror of
https://github.com/actix/actix-web.git
synced 2024-11-30 05:21:09 +00:00
Merge branch 'master' into master
This commit is contained in:
commit
3074071d03
115 changed files with 11398 additions and 4106 deletions
|
@ -3,6 +3,15 @@ environment:
|
|||
PROJECT_NAME: actix
|
||||
matrix:
|
||||
# Stable channel
|
||||
- TARGET: i686-pc-windows-gnu
|
||||
CHANNEL: 1.20.0
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
CHANNEL: 1.20.0
|
||||
- TARGET: x86_64-pc-windows-gnu
|
||||
CHANNEL: 1.20.0
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
CHANNEL: 1.20.0
|
||||
# Stable channel
|
||||
- TARGET: i686-pc-windows-gnu
|
||||
CHANNEL: stable
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
|
@ -22,13 +31,13 @@ environment:
|
|||
CHANNEL: beta
|
||||
# Nightly channel
|
||||
- TARGET: i686-pc-windows-gnu
|
||||
CHANNEL: nightly
|
||||
CHANNEL: nightly-2017-12-21
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
CHANNEL: nightly
|
||||
CHANNEL: nightly-2017-12-21
|
||||
- TARGET: x86_64-pc-windows-gnu
|
||||
CHANNEL: nightly
|
||||
CHANNEL: nightly-2017-12-21
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
CHANNEL: nightly
|
||||
CHANNEL: nightly-2017-12-21
|
||||
|
||||
# Install Rust and Cargo
|
||||
# (Based on from https://github.com/rust-lang/libc/blob/master/appveyor.yml)
|
||||
|
|
43
.travis.yml
43
.travis.yml
|
@ -4,7 +4,7 @@ rust:
|
|||
- 1.20.0
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
- nightly-2018-01-03
|
||||
|
||||
sudo: required
|
||||
dist: trusty
|
||||
|
@ -28,7 +28,28 @@ before_script:
|
|||
- export PATH=$PATH:~/.cargo/bin
|
||||
|
||||
script:
|
||||
- USE_SKEPTIC=1 cargo test --features=alpn
|
||||
- |
|
||||
if [[ "$TRAVIS_RUST_VERSION" == "stable" ]]; then
|
||||
USE_SKEPTIC=1 cargo test --features=alpn
|
||||
else
|
||||
cargo test --features=alpn
|
||||
fi
|
||||
|
||||
- |
|
||||
if [[ "$TRAVIS_RUST_VERSION" == "1.20.0" ]]; then
|
||||
cd examples/basics && cargo check && cd ../..
|
||||
cd examples/hello-world && cargo check && cd ../..
|
||||
cd examples/multipart && cargo check && cd ../..
|
||||
cd examples/json && cargo check && cd ../..
|
||||
cd examples/template_tera && cargo check && cd ../..
|
||||
fi
|
||||
- |
|
||||
if [[ "$TRAVIS_RUST_VERSION" == "beta" ]]; then
|
||||
cd examples/diesel && cargo check && cd ../..
|
||||
cd examples/tls && cargo check && cd ../..
|
||||
cd examples/websocket-chat && cargo check && cd ../..
|
||||
cd examples/websocket && cargo check && cd ../..
|
||||
fi
|
||||
- |
|
||||
if [[ "$TRAVIS_RUST_VERSION" == "nightly" && $CLIPPY ]]; then
|
||||
cargo clippy
|
||||
|
@ -37,7 +58,7 @@ script:
|
|||
# Upload docs
|
||||
after_success:
|
||||
- |
|
||||
if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "nightly" ]]; then
|
||||
if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "nightly-2018-01-03" ]]; then
|
||||
cargo doc --features alpn --no-deps &&
|
||||
echo "<meta http-equiv=refresh content=0;url=os_balloon/index.html>" > target/doc/index.html &&
|
||||
cargo install mdbook &&
|
||||
|
@ -49,18 +70,8 @@ after_success:
|
|||
|
||||
- |
|
||||
if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then
|
||||
wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz &&
|
||||
tar xzf master.tar.gz &&
|
||||
cd kcov-master &&
|
||||
mkdir build &&
|
||||
cd build &&
|
||||
cmake .. &&
|
||||
make &&
|
||||
make install DESTDIR=../../kcov-build &&
|
||||
cd ../.. &&
|
||||
rm -rf kcov-master &&
|
||||
for file in target/debug/actix_web-*[^\.d]; do mkdir -p "target/cov/$(basename $file)"; ./kcov-build/usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file"; done &&
|
||||
for file in target/debug/test_*[^\.d]; do mkdir -p "target/cov/$(basename $file)"; ./kcov-build/usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file"; done &&
|
||||
bash <(curl -s https://codecov.io/bash) &&
|
||||
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)
|
||||
echo "Uploaded code coverage"
|
||||
fi
|
||||
|
|
|
@ -15,6 +15,10 @@
|
|||
|
||||
* Content compression/decompression (br, gzip, deflate)
|
||||
|
||||
* Server multi-threading
|
||||
|
||||
* Gracefull shutdown support
|
||||
|
||||
|
||||
## 0.2.1 (2017-11-03)
|
||||
|
||||
|
|
41
Cargo.toml
41
Cargo.toml
|
@ -4,13 +4,13 @@ version = "0.3.0"
|
|||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix web framework"
|
||||
readme = "README.md"
|
||||
keywords = ["http", "http2", "web", "async", "futures"]
|
||||
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"]
|
||||
license = "Apache-2.0"
|
||||
license = "MIT/Apache-2.0"
|
||||
exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"]
|
||||
build = "build.rs"
|
||||
|
||||
|
@ -42,7 +42,6 @@ httparse = "0.1"
|
|||
http-range = "0.1"
|
||||
mime = "0.3"
|
||||
mime_guess = "1.8"
|
||||
cookie = { version="0.10", features=["percent-encode", "secure"] }
|
||||
regex = "0.2"
|
||||
sha1 = "0.2"
|
||||
url = "1.5"
|
||||
|
@ -52,10 +51,17 @@ serde_json = "1.0"
|
|||
flate2 = "0.2"
|
||||
brotli2 = "^0.3.2"
|
||||
percent-encoding = "1.0"
|
||||
smallvec = "0.6"
|
||||
bitflags = "1.0"
|
||||
num_cpus = "1.0"
|
||||
|
||||
# redis-async = { git="https://github.com/benashford/redis-async-rs" }
|
||||
# temp solution
|
||||
# cookie = { version="0.10", features=["percent-encode", "secure"] }
|
||||
cookie = { git="https://github.com/alexcrichton/cookie-rs.git", features=["percent-encode", "secure"] }
|
||||
|
||||
# tokio
|
||||
# io
|
||||
mio = "0.6"
|
||||
net2 = "0.2"
|
||||
bytes = "0.4"
|
||||
futures = "0.1"
|
||||
tokio-io = "0.1"
|
||||
|
@ -71,11 +77,8 @@ tokio-tls = { version="0.1", optional = true }
|
|||
tokio-openssl = { version="0.1", optional = true }
|
||||
|
||||
[dependencies.actix]
|
||||
version = "^0.3.1"
|
||||
#path = "../actix"
|
||||
#git = "https://github.com/actix/actix.git"
|
||||
default-features = false
|
||||
features = []
|
||||
#version = "^0.4.2"
|
||||
git = "https://github.com/actix/actix.git"
|
||||
|
||||
[dependencies.openssl]
|
||||
version = "0.9"
|
||||
|
@ -89,10 +92,24 @@ serde_derive = "1.0"
|
|||
|
||||
[build-dependencies]
|
||||
skeptic = "0.13"
|
||||
serde_derive = "1.0"
|
||||
version_check = "0.1"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
opt-level = 3
|
||||
debug = true
|
||||
# debug = true
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"./",
|
||||
"examples/basics",
|
||||
"examples/diesel",
|
||||
"examples/json",
|
||||
"examples/hello-world",
|
||||
"examples/multipart",
|
||||
"examples/state",
|
||||
"examples/template_tera",
|
||||
"examples/tls",
|
||||
"examples/websocket",
|
||||
"examples/websocket-chat",
|
||||
]
|
||||
|
|
25
LICENSE-MIT
Normal file
25
LICENSE-MIT
Normal file
|
@ -0,0 +1,25 @@
|
|||
Copyright (c) 2017 Nikilay Kim
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
2
Makefile
2
Makefile
|
@ -1,6 +1,6 @@
|
|||
.PHONY: default build test doc book clean
|
||||
|
||||
CARGO_FLAGS := --features "$(FEATURES)"
|
||||
CARGO_FLAGS := --features "$(FEATURES) alpn"
|
||||
|
||||
default: test
|
||||
|
||||
|
|
149
README.md
149
README.md
|
@ -1,6 +1,25 @@
|
|||
# Actix web [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![Build status](https://ci.appveyor.com/api/projects/status/kkdb4yce7qhm5w85/branch/master?svg=true)](https://ci.appveyor.com/project/fafhrd91/actix-web-hdy9d/branch/master) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](http://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web)
|
||||
# Actix web [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![Build status](https://ci.appveyor.com/api/projects/status/kkdb4yce7qhm5w85/branch/master?svg=true)](https://ci.appveyor.com/project/fafhrd91/actix-web-hdy9d/branch/master) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](http://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
Actix web is a fast, down-to-earth, open source rust web framework.
|
||||
Actix web is a small, fast, down-to-earth, open source rust web framework.
|
||||
|
||||
```rust,ignore
|
||||
extern crate actix_web;
|
||||
use actix_web::*;
|
||||
|
||||
fn index(req: HttpRequest) -> String {
|
||||
format!("Hello {}!", &req.match_info()["name"])
|
||||
}
|
||||
|
||||
fn main() {
|
||||
HttpServer::new(
|
||||
|| Application::new()
|
||||
.resource("/{name}", |r| r.f(index)))
|
||||
.bind("127.0.0.1:8080").unwrap()
|
||||
.run();
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
* [User Guide](http://actix.github.io/actix-web/guide/)
|
||||
* [API Documentation (Development)](http://actix.github.io/actix-web/actix_web/)
|
||||
|
@ -8,111 +27,45 @@ Actix web is a fast, down-to-earth, open source rust web framework.
|
|||
* Cargo package: [actix-web](https://crates.io/crates/actix-web)
|
||||
* Minimum supported Rust version: 1.20 or later
|
||||
|
||||
---
|
||||
|
||||
Actix web is licensed under the [Apache-2.0 license](http://opensource.org/licenses/APACHE-2.0).
|
||||
|
||||
## Features
|
||||
|
||||
* Supported HTTP/1 and HTTP/2 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
|
||||
* Multipart streams
|
||||
* Middlewares (Logger, Session included)
|
||||
* 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))
|
||||
* Built on top of [Actix](https://github.com/actix/actix).
|
||||
|
||||
## Usage
|
||||
## Benchmarks
|
||||
|
||||
To use `actix-web`, add this to your `Cargo.toml`:
|
||||
Some basic benchmarks could be found in this [respository](https://github.com/fafhrd91/benchmarks).
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
actix-web = { git = "https://github.com/actix/actix-web" }
|
||||
```
|
||||
## Examples
|
||||
|
||||
## HTTP/2
|
||||
|
||||
Actix web automatically upgrades connection to `http/2` if possible.
|
||||
|
||||
### Negotiation
|
||||
|
||||
`HTTP/2` protocol over tls without prior knowlage requires
|
||||
[tls alpn](https://tools.ietf.org/html/rfc7301). At the moment only
|
||||
`rust-openssl` supports alpn.
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
actix-web = { git = "https://github.com/actix/actix-web", features=["alpn"] }
|
||||
```
|
||||
|
||||
Upgrade to `http/2` schema described in
|
||||
[rfc section 3.2](https://http2.github.io/http2-spec/#rfc.section.3.2) is not supported.
|
||||
Starting `http/2` with prior knowledge is supported for both clear text connection
|
||||
and tls connection. [rfc section 3.4](https://http2.github.io/http2-spec/#rfc.section.3.4)
|
||||
|
||||
[tls example](https://github.com/actix/actix-web/tree/master/examples/tls)
|
||||
|
||||
## Example
|
||||
|
||||
* [Basic](https://github.com/actix/actix-web/tree/master/examples/basic.rs)
|
||||
* [Stateful](https://github.com/actix/actix-web/tree/master/examples/state.rs)
|
||||
* [Mulitpart streams](https://github.com/actix/actix-web/tree/master/examples/multipart)
|
||||
* [Simple websocket session](https://github.com/actix/actix-web/tree/master/examples/websocket.rs)
|
||||
* [Tcp/Websocket chat](https://github.com/actix/actix-web/tree/master/examples/websocket-chat)
|
||||
* [Basic](https://github.com/actix/actix-web/tree/master/examples/basic/)
|
||||
* [Stateful](https://github.com/actix/actix-web/tree/master/examples/state/)
|
||||
* [Mulitpart streams](https://github.com/actix/actix-web/tree/master/examples/multipart/)
|
||||
* [Simple websocket session](https://github.com/actix/actix-web/tree/master/examples/websocket/)
|
||||
* [Tera templates](https://github.com/actix/actix-web/tree/master/examples/template_tera/)
|
||||
* [Diesel integration](https://github.com/actix/actix-web/tree/master/examples/diesel/)
|
||||
* [SSL / HTTP/2.0](https://github.com/actix/actix-web/tree/master/examples/tls/)
|
||||
* [Tcp/Websocket chat](https://github.com/actix/actix-web/tree/master/examples/websocket-chat/)
|
||||
* [SockJS Server](https://github.com/actix/actix-sockjs)
|
||||
* [Json](https://github.com/actix/actix-web/tree/master/examples/json/)
|
||||
|
||||
## License
|
||||
|
||||
```rust
|
||||
extern crate actix;
|
||||
extern crate actix_web;
|
||||
extern crate env_logger;
|
||||
This project is licensed under either of
|
||||
|
||||
use actix::*;
|
||||
use actix_web::*;
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
|
||||
|
||||
struct MyWebSocket;
|
||||
at your option.
|
||||
|
||||
/// Actor with http context
|
||||
impl Actor for MyWebSocket {
|
||||
type Context = HttpContext<Self>;
|
||||
}
|
||||
|
||||
/// Standard actix's stream handler for a stream of `ws::Message`
|
||||
impl StreamHandler<ws::Message> for MyWebSocket {}
|
||||
impl Handler<ws::Message> for MyWebSocket {
|
||||
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context)
|
||||
-> Response<Self, ws::Message>
|
||||
{
|
||||
// process websocket messages
|
||||
println!("WS: {:?}", msg);
|
||||
match msg {
|
||||
ws::Message::Ping(msg) => ws::WsWriter::pong(ctx, &msg),
|
||||
ws::Message::Text(text) => ws::WsWriter::text(ctx, &text),
|
||||
ws::Message::Binary(bin) => ws::WsWriter::binary(ctx, bin),
|
||||
ws::Message::Closed | ws::Message::Error => {
|
||||
ctx.stop();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
::std::env::set_var("RUST_LOG", "actix_web=info");
|
||||
let _ = env_logger::init();
|
||||
let sys = actix::System::new("ws-example");
|
||||
|
||||
HttpServer::new(
|
||||
Application::default("/")
|
||||
.middleware(middlewares::Logger::default()) // <- register logger middleware
|
||||
.resource("/ws/", |r| r.get(|req| ws::start(req, MyWebSocket))) // <- websocket route
|
||||
.route("/", StaticFiles::new("examples/static/", true))) // <- server static files
|
||||
.serve::<_, ()>("127.0.0.1:8080").unwrap();
|
||||
|
||||
Arbiter::system().send(msgs::SystemExit(0));
|
||||
let _ = sys.run();
|
||||
}
|
||||
```
|
||||
[![Analytics](https://ga-beacon.appspot.com/UA-110322332-2/actix-web/readme?flat&useReferer)](https://github.com/igrigorik/ga-beacon)
|
||||
|
|
11
build.rs
11
build.rs
|
@ -12,8 +12,19 @@ fn main() {
|
|||
// generates doc tests for `README.md`.
|
||||
skeptic::generate_doc_tests(
|
||||
&["README.md",
|
||||
"guide/src/qs_1.md",
|
||||
"guide/src/qs_2.md",
|
||||
"guide/src/qs_3.md",
|
||||
"guide/src/qs_3_5.md",
|
||||
"guide/src/qs_4.md",
|
||||
"guide/src/qs_4_5.md",
|
||||
"guide/src/qs_5.md",
|
||||
"guide/src/qs_7.md",
|
||||
"guide/src/qs_8.md",
|
||||
"guide/src/qs_9.md",
|
||||
"guide/src/qs_10.md",
|
||||
"guide/src/qs_12.md",
|
||||
"guide/src/qs_13.md",
|
||||
]);
|
||||
} else {
|
||||
let _ = fs::File::create(f);
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
#![allow(unused_variables)]
|
||||
#![cfg_attr(feature="cargo-clippy", allow(needless_pass_by_value))]
|
||||
|
||||
extern crate actix;
|
||||
extern crate actix_web;
|
||||
extern crate env_logger;
|
||||
extern crate futures;
|
||||
|
||||
use actix_web::*;
|
||||
use actix_web::middlewares::RequestSession;
|
||||
use futures::future::{FutureResult, result};
|
||||
|
||||
/// simple handler
|
||||
fn index(mut req: HttpRequest) -> Result<HttpResponse> {
|
||||
println!("{:?}", req);
|
||||
if let Ok(ch) = req.payload_mut().readany() {
|
||||
if let futures::Async::Ready(Some(d)) = ch {
|
||||
println!("{}", String::from_utf8_lossy(d.0.as_ref()));
|
||||
}
|
||||
}
|
||||
|
||||
// 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(HttpResponse::Ok().into())
|
||||
}
|
||||
|
||||
/// async handler
|
||||
fn index_async(req: HttpRequest) -> FutureResult<HttpResponse, Error>
|
||||
{
|
||||
println!("{:?}", req);
|
||||
|
||||
result(HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(format!("Hello {}!", req.match_info().get("name").unwrap()))
|
||||
.map_err(|e| e.into()))
|
||||
}
|
||||
|
||||
/// handler with path parameters like `/user/{name}/`
|
||||
fn with_param(req: HttpRequest) -> Result<HttpResponse>
|
||||
{
|
||||
println!("{:?}", req);
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("test/plain")
|
||||
.body(format!("Hello {}!", req.match_info().get("name").unwrap()))?)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
::std::env::set_var("RUST_LOG", "actix_web=info");
|
||||
let _ = env_logger::init();
|
||||
let sys = actix::System::new("ws-example");
|
||||
|
||||
HttpServer::new(
|
||||
Application::default("/")
|
||||
// enable logger
|
||||
.middleware(middlewares::Logger::default())
|
||||
// cookie session middleware
|
||||
.middleware(middlewares::SessionStorage::new(
|
||||
middlewares::CookieSessionBackend::build(&[0; 32])
|
||||
.secure(false)
|
||||
.finish()
|
||||
))
|
||||
// register simple handle r, handle all methods
|
||||
.handler("/index.html", index)
|
||||
// with path parameters
|
||||
.resource("/user/{name}/", |r| r.handler(Method::GET, with_param))
|
||||
// async handler
|
||||
.resource("/async/{name}", |r| r.async(Method::GET, index_async))
|
||||
// redirect
|
||||
.resource("/", |r| r.handler(Method::GET, |req| {
|
||||
println!("{:?}", req);
|
||||
|
||||
httpcodes::HTTPFound
|
||||
.build()
|
||||
.header("LOCATION", "/index.html")
|
||||
.body(Body::Empty)
|
||||
}))
|
||||
.handler("/test", |req| {
|
||||
match *req.method() {
|
||||
Method::GET => httpcodes::HTTPOk,
|
||||
Method::POST => httpcodes::HTTPMethodNotAllowed,
|
||||
_ => httpcodes::HTTPNotFound,
|
||||
}
|
||||
})
|
||||
// static files
|
||||
.route("/static", StaticFiles::new("examples/static/", true)))
|
||||
.serve::<_, ()>("127.0.0.1:8080").unwrap();
|
||||
|
||||
println!("Started http server: 127.0.0.1:8080");
|
||||
let _ = sys.run();
|
||||
}
|
11
examples/basics/Cargo.toml
Normal file
11
examples/basics/Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "basics"
|
||||
version = "0.1.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
workspace = "../.."
|
||||
|
||||
[dependencies]
|
||||
futures = "*"
|
||||
env_logger = "0.4"
|
||||
actix = "0.4"
|
||||
actix-web = { git = "https://github.com/actix/actix-web" }
|
19
examples/basics/README.md
Normal file
19
examples/basics/README.md
Normal file
|
@ -0,0 +1,19 @@
|
|||
# basics
|
||||
|
||||
## Usage
|
||||
|
||||
### server
|
||||
|
||||
```bash
|
||||
cd actix-web/examples/basics
|
||||
cargo run
|
||||
# Started http server: 127.0.0.1:8080
|
||||
```
|
||||
|
||||
### web client
|
||||
|
||||
- [http://localhost:8080/index.html](http://localhost:8080/index.html)
|
||||
- [http://localhost:8080/async/bob](http://localhost:8080/async/bob)
|
||||
- [http://localhost:8080/user/bob/](http://localhost:8080/user/bob/) plain/text download
|
||||
- [http://localhost:8080/test](http://localhost:8080/test) (return status switch GET or POST or other)
|
||||
- [http://localhost:8080/static/index.html](http://localhost:8080/static/index.html)
|
146
examples/basics/src/main.rs
Normal file
146
examples/basics/src/main.rs
Normal file
|
@ -0,0 +1,146 @@
|
|||
#![allow(unused_variables)]
|
||||
#![cfg_attr(feature="cargo-clippy", allow(needless_pass_by_value))]
|
||||
|
||||
extern crate actix;
|
||||
extern crate actix_web;
|
||||
extern crate env_logger;
|
||||
extern crate futures;
|
||||
use futures::Stream;
|
||||
|
||||
use actix_web::*;
|
||||
use actix_web::middleware::RequestSession;
|
||||
use futures::future::{FutureResult, result};
|
||||
|
||||
/// favicon handler
|
||||
fn favicon(req: HttpRequest) -> Result<fs::NamedFile> {
|
||||
Ok(fs::NamedFile::open("../static/favicon.ico")?)
|
||||
}
|
||||
|
||||
/// simple index handler
|
||||
fn index(mut req: HttpRequest) -> Result<HttpResponse> {
|
||||
println!("{:?}", req);
|
||||
|
||||
// example of ...
|
||||
if let Ok(ch) = req.payload_mut().readany().poll() {
|
||||
if let futures::Async::Ready(Some(d)) = ch {
|
||||
println!("{}", String::from_utf8_lossy(d.as_ref()));
|
||||
}
|
||||
}
|
||||
|
||||
// session
|
||||
let mut counter = 1;
|
||||
if let Some(count) = req.session().get::<i32>("counter")? {
|
||||
println!("SESSION value: {}", count);
|
||||
counter = count + 1;
|
||||
req.session().set("counter", counter)?;
|
||||
} else {
|
||||
req.session().set("counter", counter)?;
|
||||
}
|
||||
|
||||
// html
|
||||
let html = format!(r#"<!DOCTYPE html><html><head><title>actix - basics</title><link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" /></head>
|
||||
<body>
|
||||
<h1>Welcome <img width="30px" height="30px" src="/static/actixLogo.png" /></h1>
|
||||
session counter = {}
|
||||
</body>
|
||||
</html>"#, counter);
|
||||
|
||||
// response
|
||||
Ok(HttpResponse::build(StatusCode::OK)
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(&html).unwrap())
|
||||
|
||||
}
|
||||
|
||||
/// 404 handler
|
||||
fn p404(req: HttpRequest) -> Result<HttpResponse> {
|
||||
|
||||
// html
|
||||
let html = format!(r#"<!DOCTYPE html><html><head><title>actix - basics</title><link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" /></head>
|
||||
<body>
|
||||
<a href="index.html">back to home</a>
|
||||
<h1>404</h1>
|
||||
</body>
|
||||
</html>"#);
|
||||
|
||||
// response
|
||||
Ok(HttpResponse::build(StatusCode::NOT_FOUND)
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(&html).unwrap())
|
||||
}
|
||||
|
||||
|
||||
/// async handler
|
||||
fn index_async(req: HttpRequest) -> FutureResult<HttpResponse, Error>
|
||||
{
|
||||
println!("{:?}", req);
|
||||
|
||||
result(HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(format!("Hello {}!", req.match_info().get("name").unwrap()))
|
||||
.map_err(|e| e.into()))
|
||||
}
|
||||
|
||||
/// handler with path parameters like `/user/{name}/`
|
||||
fn with_param(req: HttpRequest) -> Result<HttpResponse>
|
||||
{
|
||||
println!("{:?}", req);
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("test/plain")
|
||||
.body(format!("Hello {}!", req.match_info().get("name").unwrap()))?)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
::std::env::set_var("RUST_LOG", "actix_web=info");
|
||||
let _ = env_logger::init();
|
||||
let sys = actix::System::new("basic-example");
|
||||
|
||||
let addr = HttpServer::new(
|
||||
|| Application::new()
|
||||
// enable logger
|
||||
.middleware(middleware::Logger::default())
|
||||
// cookie session middleware
|
||||
.middleware(middleware::SessionStorage::new(
|
||||
middleware::CookieSessionBackend::build(&[0; 32])
|
||||
.secure(false)
|
||||
.finish()
|
||||
))
|
||||
// register favicon
|
||||
.resource("/favicon.ico", |r| r.f(favicon))
|
||||
// register simple route, handle all methods
|
||||
.resource("/index.html", |r| r.f(index))
|
||||
// with path parameters
|
||||
.resource("/user/{name}/", |r| r.method(Method::GET).f(with_param))
|
||||
// async handler
|
||||
.resource("/async/{name}", |r| r.method(Method::GET).a(index_async))
|
||||
.resource("/test", |r| r.f(|req| {
|
||||
match *req.method() {
|
||||
Method::GET => httpcodes::HTTPOk,
|
||||
Method::POST => httpcodes::HTTPMethodNotAllowed,
|
||||
_ => httpcodes::HTTPNotFound,
|
||||
}
|
||||
}))
|
||||
// static files
|
||||
.handler("/static/", fs::StaticFiles::new("../static/", true))
|
||||
// redirect
|
||||
.resource("/", |r| r.method(Method::GET).f(|req| {
|
||||
println!("{:?}", req);
|
||||
|
||||
HttpResponse::Found()
|
||||
.header("LOCATION", "/index.html")
|
||||
.finish()
|
||||
}))
|
||||
// default
|
||||
.default_resource(|r| {
|
||||
r.method(Method::GET).f(p404);
|
||||
r.route().p(pred::Not(pred::Get())).f(|req| httpcodes::HTTPMethodNotAllowed);
|
||||
}))
|
||||
|
||||
.bind("127.0.0.1:8080").expect("Can not bind to 127.0.0.1:8080")
|
||||
.shutdown_timeout(0) // <- Set shutdown timeout to 0 seconds (default 60s)
|
||||
.start();
|
||||
|
||||
println!("Starting http server: 127.0.0.1:8080");
|
||||
let _ = sys.run();
|
||||
}
|
1
examples/diesel/.env
Normal file
1
examples/diesel/.env
Normal file
|
@ -0,0 +1 @@
|
|||
DATABASE_URL=file:test.db
|
19
examples/diesel/Cargo.toml
Normal file
19
examples/diesel/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "diesel-example"
|
||||
version = "0.1.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
workspace = "../.."
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.4"
|
||||
actix = "0.4"
|
||||
actix-web = { git = "https://github.com/actix/actix-web" }
|
||||
|
||||
futures = "0.1"
|
||||
uuid = { version = "0.5", features = ["serde", "v4"] }
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_derive = "1.0"
|
||||
|
||||
diesel = { version = "1.0.0-beta1", features = ["sqlite"] }
|
||||
dotenv = "0.10"
|
43
examples/diesel/README.md
Normal file
43
examples/diesel/README.md
Normal file
|
@ -0,0 +1,43 @@
|
|||
# diesel
|
||||
|
||||
Diesel's `Getting Started` guide using SQLite for Actix web
|
||||
|
||||
## Usage
|
||||
|
||||
### init database sqlite
|
||||
|
||||
```bash
|
||||
cargo install diesel_cli --no-default-features --features sqlite
|
||||
cd actix-web/examples/diesel
|
||||
echo "DATABASE_URL=file:test.db" > .env
|
||||
diesel migration run
|
||||
```
|
||||
|
||||
### server
|
||||
|
||||
```bash
|
||||
# if ubuntu : sudo apt-get install libsqlite3-dev
|
||||
# if fedora : sudo dnf install libsqlite3x-devel
|
||||
cd actix-web/examples/diesel
|
||||
cargo run (or ``cargo watch -x run``)
|
||||
# Started http server: 127.0.0.1:8080
|
||||
```
|
||||
|
||||
### web client
|
||||
|
||||
[http://127.0.0.1:8080/NAME](http://127.0.0.1:8080/NAME)
|
||||
|
||||
### sqlite client
|
||||
|
||||
```bash
|
||||
# if ubuntu : sudo apt-get install sqlite3
|
||||
# if fedora : sudo dnf install sqlite3x
|
||||
sqlite3 test.db
|
||||
sqlite> .tables
|
||||
sqlite> select * from users;
|
||||
```
|
||||
|
||||
|
||||
## Postgresql
|
||||
|
||||
You will also find another complete example of diesel+postgresql on [https://github.com/TechEmpower/FrameworkBenchmarks/tree/master/frameworks/Rust/actix](https://github.com/TechEmpower/FrameworkBenchmarks/tree/master/frameworks/Rust/actix)
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE users
|
|
@ -0,0 +1,4 @@
|
|||
CREATE TABLE users (
|
||||
id VARCHAR NOT NULL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL
|
||||
)
|
53
examples/diesel/src/db.rs
Normal file
53
examples/diesel/src/db.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
//! Db executor actor
|
||||
use uuid;
|
||||
use diesel;
|
||||
use actix_web::*;
|
||||
use actix::prelude::*;
|
||||
use diesel::prelude::*;
|
||||
|
||||
use models;
|
||||
use schema;
|
||||
|
||||
/// This is db executor actor. We are going to run 3 of them in parallele.
|
||||
pub struct DbExecutor(pub SqliteConnection);
|
||||
|
||||
/// 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 ResponseType for CreateUser {
|
||||
type Item = models::User;
|
||||
type Error = Error;
|
||||
}
|
||||
|
||||
impl Actor for DbExecutor {
|
||||
type Context = SyncContext<Self>;
|
||||
}
|
||||
|
||||
impl Handler<CreateUser> for DbExecutor {
|
||||
type Result = MessageResult<CreateUser>;
|
||||
|
||||
fn handle(&mut self, msg: CreateUser, _: &mut Self::Context) -> Self::Result {
|
||||
use self::schema::users::dsl::*;
|
||||
|
||||
let uuid = format!("{}", uuid::Uuid::new_v4());
|
||||
let new_user = models::NewUser {
|
||||
id: &uuid,
|
||||
name: &msg.name,
|
||||
};
|
||||
|
||||
diesel::insert_into(users)
|
||||
.values(&new_user)
|
||||
.execute(&self.0)
|
||||
.expect("Error inserting person");
|
||||
|
||||
let mut items = users
|
||||
.filter(id.eq(&uuid))
|
||||
.load::<models::User>(&self.0)
|
||||
.expect("Error loading person");
|
||||
|
||||
Ok(items.pop().unwrap())
|
||||
}
|
||||
}
|
73
examples/diesel/src/main.rs
Normal file
73
examples/diesel/src/main.rs
Normal file
|
@ -0,0 +1,73 @@
|
|||
//! Actix web diesel example
|
||||
//!
|
||||
//! Diesel does not support tokio, so we have to run it in separate threads.
|
||||
//! Actix supports sync actors by default, so we going to create sync actor that will
|
||||
//! use diesel. Technically sync actors are worker style actors, multiple of them
|
||||
//! can run in parallel and process messages from same queue.
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
extern crate uuid;
|
||||
extern crate futures;
|
||||
extern crate actix;
|
||||
extern crate actix_web;
|
||||
extern crate env_logger;
|
||||
|
||||
use actix::*;
|
||||
use actix_web::*;
|
||||
|
||||
use diesel::prelude::*;
|
||||
use futures::future::Future;
|
||||
|
||||
mod db;
|
||||
mod models;
|
||||
mod schema;
|
||||
|
||||
use db::{CreateUser, DbExecutor};
|
||||
|
||||
|
||||
/// State with DbExecutor address
|
||||
struct State {
|
||||
db: SyncAddress<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()})
|
||||
.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=info");
|
||||
let _ = env_logger::init();
|
||||
let sys = actix::System::new("diesel-example");
|
||||
|
||||
// Start db executor actors
|
||||
let addr = SyncArbiter::start(3, || {
|
||||
DbExecutor(SqliteConnection::establish("test.db").unwrap())
|
||||
});
|
||||
|
||||
// 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();
|
||||
|
||||
println!("Started http server: 127.0.0.1:8080");
|
||||
let _ = sys.run();
|
||||
}
|
14
examples/diesel/src/models.rs
Normal file
14
examples/diesel/src/models.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
use super::schema::users;
|
||||
|
||||
#[derive(Serialize, Queryable)]
|
||||
pub struct User {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[table_name = "users"]
|
||||
pub struct NewUser<'a> {
|
||||
pub id: &'a str,
|
||||
pub name: &'a str,
|
||||
}
|
6
examples/diesel/src/schema.rs
Normal file
6
examples/diesel/src/schema.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
table! {
|
||||
users (id) {
|
||||
id -> Text,
|
||||
name -> Text,
|
||||
}
|
||||
}
|
BIN
examples/diesel/test.db
Normal file
BIN
examples/diesel/test.db
Normal file
Binary file not shown.
10
examples/hello-world/Cargo.toml
Normal file
10
examples/hello-world/Cargo.toml
Normal file
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "hello-world"
|
||||
version = "0.1.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
workspace = "../.."
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.4"
|
||||
actix = "0.4"
|
||||
actix-web = { path = "../../" }
|
28
examples/hello-world/src/main.rs
Normal file
28
examples/hello-world/src/main.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
extern crate actix;
|
||||
extern crate actix_web;
|
||||
extern crate env_logger;
|
||||
|
||||
use actix_web::*;
|
||||
|
||||
|
||||
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("ws-example");
|
||||
|
||||
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)))
|
||||
.bind("127.0.0.1:8080").unwrap()
|
||||
.start();
|
||||
|
||||
println!("Started http server: 127.0.0.1:8080");
|
||||
let _ = sys.run();
|
||||
}
|
18
examples/json/Cargo.toml
Normal file
18
examples/json/Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "json-example"
|
||||
version = "0.1.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
workspace = "../.."
|
||||
|
||||
[dependencies]
|
||||
bytes = "0.4"
|
||||
futures = "0.1"
|
||||
env_logger = "*"
|
||||
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_derive = "1.0"
|
||||
json = "*"
|
||||
|
||||
actix = "0.4"
|
||||
actix-web = { git = "https://github.com/actix/actix-web" }
|
48
examples/json/README.md
Normal file
48
examples/json/README.md
Normal file
|
@ -0,0 +1,48 @@
|
|||
# json
|
||||
|
||||
Json's `Getting Started` guide using json (serde-json or json-rust) for Actix web
|
||||
|
||||
## Usage
|
||||
|
||||
### server
|
||||
|
||||
```bash
|
||||
cd actix-web/examples/json
|
||||
cargo run
|
||||
# Started http server: 127.0.0.1:8080
|
||||
```
|
||||
|
||||
### web client
|
||||
|
||||
With [Postman](https://www.getpostman.com/) or [Rested](moz-extension://60daeb1c-5b1b-4afd-9842-0579ed34dfcb/dist/index.html)
|
||||
|
||||
- POST / (embed serde-json):
|
||||
|
||||
- method : ``POST``
|
||||
- url : ``http://127.0.0.1:8080/``
|
||||
- header : ``Content-Type`` = ``application/json``
|
||||
- body (raw) : ``{"name": "Test user", "number": 100}``
|
||||
|
||||
- POST /manual (manual serde-json):
|
||||
|
||||
- method : ``POST``
|
||||
- url : ``http://127.0.0.1:8080/manual``
|
||||
- header : ``Content-Type`` = ``application/json``
|
||||
- body (raw) : ``{"name": "Test user", "number": 100}``
|
||||
|
||||
- POST /mjsonrust (manual json-rust):
|
||||
|
||||
- method : ``POST``
|
||||
- url : ``http://127.0.0.1:8080/mjsonrust``
|
||||
- header : ``Content-Type`` = ``application/json``
|
||||
- body (raw) : ``{"name": "Test user", "number": 100}`` (you can also test ``{notjson}``)
|
||||
|
||||
### python client
|
||||
|
||||
- ``pip install aiohttp``
|
||||
- ``python client.py``
|
||||
|
||||
if ubuntu :
|
||||
|
||||
- ``pip3 install aiohttp``
|
||||
- ``python3 client.py``
|
18
examples/json/client.py
Normal file
18
examples/json/client.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# This script could be used for actix-web multipart example test
|
||||
# just start server and run client.py
|
||||
|
||||
import json
|
||||
import asyncio
|
||||
import aiohttp
|
||||
|
||||
async def req():
|
||||
resp = await aiohttp.ClientSession().request(
|
||||
"post", 'http://localhost:8080/',
|
||||
data=json.dumps({"name": "Test user", "number": 100}),
|
||||
headers={"content-type": "application/json"})
|
||||
print(str(resp))
|
||||
print(await resp.text())
|
||||
assert 200 == resp.status
|
||||
|
||||
|
||||
asyncio.get_event_loop().run_until_complete(req())
|
99
examples/json/src/main.rs
Normal file
99
examples/json/src/main.rs
Normal file
|
@ -0,0 +1,99 @@
|
|||
extern crate actix;
|
||||
extern crate actix_web;
|
||||
extern crate bytes;
|
||||
extern crate futures;
|
||||
extern crate env_logger;
|
||||
extern crate serde_json;
|
||||
#[macro_use] extern crate serde_derive;
|
||||
#[macro_use] extern crate json;
|
||||
|
||||
use actix_web::*;
|
||||
|
||||
use bytes::BytesMut;
|
||||
use futures::{Future, Stream};
|
||||
use json::JsonValue;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct MyObj {
|
||||
name: String,
|
||||
number: i32,
|
||||
}
|
||||
|
||||
/// This handler uses `HttpRequest::json()` for loading serde json object.
|
||||
fn index(req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
req.json()
|
||||
.from_err() // convert all errors into `Error`
|
||||
.and_then(|val: MyObj| {
|
||||
println!("model: {:?}", val);
|
||||
Ok(httpcodes::HTTPOk.build().json(val)?) // <- send response
|
||||
})
|
||||
.responder()
|
||||
}
|
||||
|
||||
|
||||
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()
|
||||
// `Future::from_err` acts like `?` in that it coerces the error type from
|
||||
// the future into the final error type
|
||||
.from_err()
|
||||
|
||||
// `fold` will asynchronously read each chunk of the request body and
|
||||
// call supplied closure, then it resolves to result of closure
|
||||
.fold(BytesMut::new(), move |mut body, chunk| {
|
||||
// limit max size of in-memory payload
|
||||
if (body.len() + chunk.len()) > MAX_SIZE {
|
||||
Err(error::ErrorBadRequest("overflow"))
|
||||
} else {
|
||||
body.extend_from_slice(&chunk);
|
||||
Ok(body)
|
||||
}
|
||||
})
|
||||
// `Future::and_then` can be used to merge an asynchronous workflow with a
|
||||
// synchronous workflow
|
||||
.and_then(|body| {
|
||||
// body is loaded, now we can deserialize serde-json
|
||||
let obj = serde_json::from_slice::<MyObj>(&body)?;
|
||||
Ok(httpcodes::HTTPOk.build().json(obj)?) // <- send response
|
||||
})
|
||||
.responder()
|
||||
}
|
||||
|
||||
/// 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()
|
||||
.from_err()
|
||||
.and_then(|body| {
|
||||
// body is loaded, now we can deserialize json-rust
|
||||
let result = json::parse(std::str::from_utf8(&body).unwrap()); // return Result
|
||||
let injson: JsonValue = match result { Ok(v) => v, Err(e) => object!{"err" => e.to_string() } };
|
||||
Ok(HttpResponse::build(StatusCode::OK)
|
||||
.content_type("application/json")
|
||||
.body(injson.dump()).unwrap())
|
||||
|
||||
})
|
||||
.responder()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
::std::env::set_var("RUST_LOG", "actix_web=info");
|
||||
let _ = env_logger::init();
|
||||
let sys = actix::System::new("json-example");
|
||||
|
||||
let addr = HttpServer::new(|| {
|
||||
Application::new()
|
||||
// enable logger
|
||||
.middleware(middleware::Logger::default())
|
||||
.resource("/manual", |r| r.method(Method::POST).f(index_manual))
|
||||
.resource("/mjsonrust", |r| r.method(Method::POST).f(index_mjsonrust))
|
||||
.resource("/", |r| r.method(Method::POST).f(index))})
|
||||
.bind("127.0.0.1:8080").unwrap()
|
||||
.shutdown_timeout(1)
|
||||
.start();
|
||||
|
||||
println!("Started http server: 127.0.0.1:8080");
|
||||
let _ = sys.run();
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
name = "multipart-example"
|
||||
version = "0.1.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
workspace = "../.."
|
||||
|
||||
[[bin]]
|
||||
name = "multipart"
|
||||
|
@ -9,5 +10,6 @@ path = "src/main.rs"
|
|||
|
||||
[dependencies]
|
||||
env_logger = "*"
|
||||
actix = "^0.3.1"
|
||||
actix-web = { git = "https://github.com/actix/actix-web.git" }
|
||||
futures = "0.1"
|
||||
actix = "0.4"
|
||||
actix-web = { git = "https://github.com/actix/actix-web" }
|
||||
|
|
24
examples/multipart/README.md
Normal file
24
examples/multipart/README.md
Normal file
|
@ -0,0 +1,24 @@
|
|||
# multipart
|
||||
|
||||
Multipart's `Getting Started` guide for Actix web
|
||||
|
||||
## Usage
|
||||
|
||||
### server
|
||||
|
||||
```bash
|
||||
cd actix-web/examples/multipart
|
||||
cargo run (or ``cargo watch -x run``)
|
||||
# Started http server: 127.0.0.1:8080
|
||||
```
|
||||
|
||||
### client
|
||||
|
||||
- ``pip install aiohttp``
|
||||
- ``python client.py``
|
||||
- you must see in server console multipart fields
|
||||
|
||||
if ubuntu :
|
||||
|
||||
- ``pip3 install aiohttp``
|
||||
- ``python3 client.py``
|
|
@ -1,26 +1,28 @@
|
|||
# This script could be used for actix-web multipart example test
|
||||
# just start server and run client.py
|
||||
|
||||
import asyncio
|
||||
import aiohttp
|
||||
|
||||
|
||||
def req1():
|
||||
async def req1():
|
||||
with aiohttp.MultipartWriter() as writer:
|
||||
writer.append('test')
|
||||
writer.append_json({'passed': True})
|
||||
|
||||
resp = yield from aiohttp.request(
|
||||
resp = await aiohttp.ClientSession().request(
|
||||
"post", 'http://localhost:8080/multipart',
|
||||
data=writer, headers=writer.headers)
|
||||
print(resp)
|
||||
assert 200 == resp.status
|
||||
|
||||
|
||||
def req2():
|
||||
async def req2():
|
||||
with aiohttp.MultipartWriter() as writer:
|
||||
writer.append('test')
|
||||
writer.append_json({'passed': True})
|
||||
writer.append(open('src/main.rs'))
|
||||
|
||||
resp = yield from aiohttp.request(
|
||||
resp = await aiohttp.ClientSession().request(
|
||||
"post", 'http://localhost:8080/multipart',
|
||||
data=writer, headers=writer.headers)
|
||||
print(resp)
|
||||
|
|
|
@ -2,76 +2,58 @@
|
|||
extern crate actix;
|
||||
extern crate actix_web;
|
||||
extern crate env_logger;
|
||||
extern crate futures;
|
||||
|
||||
use actix::*;
|
||||
use actix_web::*;
|
||||
|
||||
struct MyRoute;
|
||||
use futures::{Future, Stream};
|
||||
use futures::future::{result, Either};
|
||||
|
||||
impl Actor for MyRoute {
|
||||
type Context = HttpContext<Self>;
|
||||
}
|
||||
|
||||
impl Route for MyRoute {
|
||||
type State = ();
|
||||
fn index(mut req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>>
|
||||
{
|
||||
println!("{:?}", req);
|
||||
|
||||
fn request(mut req: HttpRequest, ctx: &mut HttpContext<Self>) -> RouteResult<Self> {
|
||||
println!("{:?}", req);
|
||||
req.multipart() // <- get multipart stream for current request
|
||||
.from_err() // <- convert multipart errors
|
||||
.and_then(|item| { // <- iterate over multipart items
|
||||
match item {
|
||||
// Handle multipart Field
|
||||
multipart::MultipartItem::Field(field) => {
|
||||
println!("==== FIELD ==== {:?}", field);
|
||||
|
||||
let multipart = req.multipart()?;
|
||||
|
||||
// get Multipart stream
|
||||
WrapStream::<MyRoute>::actstream(multipart)
|
||||
.and_then(|item, act, ctx| {
|
||||
// Multipart stream is a stream of Fields and nested Multiparts
|
||||
match item {
|
||||
multipart::MultipartItem::Field(field) => {
|
||||
println!("==== FIELD ==== {:?}", field);
|
||||
|
||||
// Read field's stream
|
||||
fut::Either::A(
|
||||
field.actstream()
|
||||
.map(|chunk, act, ctx| {
|
||||
println!(
|
||||
"-- CHUNK: \n{}",
|
||||
std::str::from_utf8(&chunk.0).unwrap());
|
||||
})
|
||||
.finish())
|
||||
},
|
||||
multipart::MultipartItem::Nested(mp) => {
|
||||
// Do nothing for nested multipart stream
|
||||
fut::Either::B(fut::ok(()))
|
||||
}
|
||||
// Field in turn is stream of *Bytes* object
|
||||
Either::A(
|
||||
field.map_err(Error::from)
|
||||
.map(|chunk| {
|
||||
println!("-- CHUNK: \n{}",
|
||||
std::str::from_utf8(&chunk).unwrap());})
|
||||
.finish())
|
||||
},
|
||||
multipart::MultipartItem::Nested(mp) => {
|
||||
// Or item could be nested Multipart stream
|
||||
Either::B(result(Ok(())))
|
||||
}
|
||||
})
|
||||
// wait until stream finish
|
||||
.finish()
|
||||
.map_err(|e, act, ctx| {
|
||||
ctx.start(httpcodes::HTTPBadRequest);
|
||||
ctx.write_eof();
|
||||
})
|
||||
.map(|_, act, ctx| {
|
||||
ctx.start(httpcodes::HTTPOk);
|
||||
ctx.write_eof();
|
||||
})
|
||||
.spawn(ctx);
|
||||
|
||||
Reply::async(MyRoute)
|
||||
}
|
||||
}
|
||||
})
|
||||
.finish() // <- Stream::finish() combinator from actix
|
||||
.map(|_| httpcodes::HTTPOk.into())
|
||||
.responder()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
::std::env::set_var("RUST_LOG", "actix_web=info");
|
||||
let _ = env_logger::init();
|
||||
let sys = actix::System::new("multipart-example");
|
||||
|
||||
HttpServer::new(
|
||||
vec![
|
||||
Application::default("/")
|
||||
.resource("/multipart", |r| {
|
||||
r.post::<MyRoute>();
|
||||
}).finish()
|
||||
])
|
||||
.serve::<_, ()>("127.0.0.1:8080").unwrap();
|
||||
let addr = HttpServer::new(
|
||||
|| Application::new()
|
||||
.middleware(middleware::Logger::default()) // <- logger
|
||||
.resource("/multipart", |r| r.method(Method::POST).a(index)))
|
||||
.bind("127.0.0.1:8080").unwrap()
|
||||
.start();
|
||||
|
||||
println!("Starting http server: 127.0.0.1:8080");
|
||||
let _ = sys.run();
|
||||
}
|
||||
|
|
11
examples/state/Cargo.toml
Normal file
11
examples/state/Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "state"
|
||||
version = "0.1.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
workspace = "../.."
|
||||
|
||||
[dependencies]
|
||||
futures = "*"
|
||||
env_logger = "0.4"
|
||||
actix = "0.4"
|
||||
actix-web = { git = "https://github.com/actix/actix-web" }
|
15
examples/state/README.md
Normal file
15
examples/state/README.md
Normal file
|
@ -0,0 +1,15 @@
|
|||
# state
|
||||
|
||||
## Usage
|
||||
|
||||
### server
|
||||
|
||||
```bash
|
||||
cd actix-web/examples/state
|
||||
cargo run
|
||||
# Started http server: 127.0.0.1:8080
|
||||
```
|
||||
|
||||
### web client
|
||||
|
||||
- [http://localhost:8080/](http://localhost:8080/)
|
|
@ -7,10 +7,12 @@ extern crate actix;
|
|||
extern crate actix_web;
|
||||
extern crate env_logger;
|
||||
|
||||
use actix::*;
|
||||
use actix_web::*;
|
||||
use std::cell::Cell;
|
||||
|
||||
use actix::*;
|
||||
use actix_web::*;
|
||||
|
||||
/// Application state
|
||||
struct AppState {
|
||||
counter: Cell<usize>,
|
||||
}
|
||||
|
@ -34,11 +36,10 @@ impl Actor for MyWebSocket {
|
|||
type Context = HttpContext<Self, AppState>;
|
||||
}
|
||||
|
||||
impl StreamHandler<ws::Message> for MyWebSocket {}
|
||||
impl Handler<ws::Message> for MyWebSocket {
|
||||
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context)
|
||||
-> Response<Self, ws::Message>
|
||||
{
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) {
|
||||
self.counter += 1;
|
||||
println!("WS({}): {:?}", self.counter, msg);
|
||||
match msg {
|
||||
|
@ -50,7 +51,6 @@ impl Handler<ws::Message> for MyWebSocket {
|
|||
}
|
||||
_ => (),
|
||||
}
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,15 +59,18 @@ fn main() {
|
|||
let _ = env_logger::init();
|
||||
let sys = actix::System::new("ws-example");
|
||||
|
||||
HttpServer::new(
|
||||
Application::build("/", AppState{counter: Cell::new(0)})
|
||||
let addr = HttpServer::new(
|
||||
|| Application::with_state(AppState{counter: Cell::new(0)})
|
||||
// enable logger
|
||||
.middleware(middlewares::Logger::default())
|
||||
.middleware(middleware::Logger::default())
|
||||
// websocket route
|
||||
.resource("/ws/", |r| r.get(|r| ws::start(r, MyWebSocket{counter: 0})))
|
||||
.resource(
|
||||
"/ws/", |r|
|
||||
r.method(Method::GET).f(|req| ws::start(req, MyWebSocket{counter: 0})))
|
||||
// register simple handler, handle all methods
|
||||
.handler("/", index))
|
||||
.serve::<_, ()>("127.0.0.1:8080").unwrap();
|
||||
.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();
|
BIN
examples/static/actixLogo.png
Normal file
BIN
examples/static/actixLogo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
BIN
examples/static/favicon.ico
Normal file
BIN
examples/static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
11
examples/template_tera/Cargo.toml
Normal file
11
examples/template_tera/Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "template-tera"
|
||||
version = "0.1.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
workspace = "../.."
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.4"
|
||||
actix = "0.4"
|
||||
actix-web = { git = "https://github.com/actix/actix-web" }
|
||||
tera = "*"
|
17
examples/template_tera/README.md
Normal file
17
examples/template_tera/README.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
# template_tera
|
||||
|
||||
Minimal example of using the template [tera](https://github.com/Keats/tera) that displays a form.
|
||||
|
||||
## Usage
|
||||
|
||||
### server
|
||||
|
||||
```bash
|
||||
cd actix-web/examples/template_tera
|
||||
cargo run (or ``cargo watch -x run``)
|
||||
# Started http server: 127.0.0.1:8080
|
||||
```
|
||||
|
||||
### web client
|
||||
|
||||
- [http://localhost:8080](http://localhost:8080)
|
47
examples/template_tera/src/main.rs
Normal file
47
examples/template_tera/src/main.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
extern crate actix;
|
||||
extern crate actix_web;
|
||||
extern crate env_logger;
|
||||
#[macro_use]
|
||||
extern crate tera;
|
||||
|
||||
use actix_web::*;
|
||||
|
||||
|
||||
struct State {
|
||||
template: tera::Tera, // <- store tera template in application state
|
||||
}
|
||||
|
||||
fn index(req: HttpRequest<State>) -> Result<HttpResponse> {
|
||||
let s = if let Some(name) = req.query().get("name") { // <- submitted form
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.add("name", &name.to_owned());
|
||||
ctx.add("text", &"Welcome!".to_owned());
|
||||
req.state().template.render("user.html", &ctx)
|
||||
.map_err(|_| error::ErrorInternalServerError("Template error"))?
|
||||
} else {
|
||||
req.state().template.render("index.html", &tera::Context::new())
|
||||
.map_err(|_| error::ErrorInternalServerError("Template error"))?
|
||||
};
|
||||
Ok(httpcodes::HTTPOk.build()
|
||||
.content_type("text/html")
|
||||
.body(s)?)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
::std::env::set_var("RUST_LOG", "actix_web=info");
|
||||
let _ = env_logger::init();
|
||||
let sys = actix::System::new("tera-example");
|
||||
|
||||
let addr = HttpServer::new(|| {
|
||||
let tera = compile_templates!(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*"));
|
||||
|
||||
Application::with_state(State{template: tera})
|
||||
// enable logger
|
||||
.middleware(middleware::Logger::default())
|
||||
.resource("/", |r| r.method(Method::GET).f(index))})
|
||||
.bind("127.0.0.1:8080").unwrap()
|
||||
.start();
|
||||
|
||||
println!("Started http server: 127.0.0.1:8080");
|
||||
let _ = sys.run();
|
||||
}
|
17
examples/template_tera/templates/index.html
Normal file
17
examples/template_tera/templates/index.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Actix web</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Welcome!</h1>
|
||||
<p>
|
||||
<h3>What is your name?</h3>
|
||||
<form>
|
||||
<input type="text" name="name" /><br/>
|
||||
<p><input type="submit"></p>
|
||||
</form>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
13
examples/template_tera/templates/user.html
Normal file
13
examples/template_tera/templates/user.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Actix web</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hi, {{ name }}!</h1>
|
||||
<p>
|
||||
{{ text }}
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
|
@ -2,6 +2,7 @@
|
|||
name = "tls-example"
|
||||
version = "0.1.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
workspace = "../.."
|
||||
|
||||
[[bin]]
|
||||
name = "server"
|
||||
|
@ -9,5 +10,5 @@ path = "src/main.rs"
|
|||
|
||||
[dependencies]
|
||||
env_logger = "0.4"
|
||||
actix = "^0.3.1"
|
||||
actix-web = { git = "https://github.com/actix/actix-web.git", features=["alpn"] }
|
||||
actix = "0.4"
|
||||
actix-web = { git = "https://github.com/actix/actix-web", features=["alpn"] }
|
||||
|
|
|
@ -1,5 +1,16 @@
|
|||
# tls example
|
||||
|
||||
To start server use command: `cargo run`
|
||||
## Usage
|
||||
|
||||
Test command: `curl -v https://127.0.0.1:8080/index.html --compress -k`
|
||||
### server
|
||||
|
||||
```bash
|
||||
cd actix-web/examples/tls
|
||||
cargo run (or ``cargo watch -x run``)
|
||||
# Started http server: 127.0.0.1:8443
|
||||
```
|
||||
|
||||
### web client
|
||||
|
||||
- curl: ``curl -v https://127.0.0.1:8443/index.html --compress -k``
|
||||
- browser: [https://127.0.0.1:8443/index.html](https://127.0.0.1:8080/index.html)
|
||||
|
|
|
@ -8,6 +8,7 @@ use std::io::Read;
|
|||
|
||||
use actix_web::*;
|
||||
|
||||
|
||||
/// somple handle
|
||||
fn index(req: HttpRequest) -> Result<HttpResponse> {
|
||||
println!("{:?}", req);
|
||||
|
@ -29,21 +30,22 @@ fn main() {
|
|||
file.read_to_end(&mut pkcs12).unwrap();
|
||||
let pkcs12 = Pkcs12::from_der(&pkcs12).unwrap().parse("12345").unwrap();
|
||||
|
||||
HttpServer::new(
|
||||
Application::default("/")
|
||||
let addr = HttpServer::new(
|
||||
|| Application::new()
|
||||
// enable logger
|
||||
.middleware(middlewares::Logger::default())
|
||||
.middleware(middleware::Logger::default())
|
||||
// register simple handler, handle all methods
|
||||
.handler("/index.html", index)
|
||||
.resource("/index.html", |r| r.f(index))
|
||||
// with path parameters
|
||||
.resource("/", |r| r.handler(Method::GET, |req| {
|
||||
.resource("/", |r| r.method(Method::GET).f(|req| {
|
||||
httpcodes::HTTPFound
|
||||
.build()
|
||||
.header("LOCATION", "/index.html")
|
||||
.body(Body::Empty)
|
||||
})))
|
||||
.serve_tls::<_, ()>("127.0.0.1:8080", pkcs12).unwrap();
|
||||
.bind("127.0.0.1:8443").unwrap()
|
||||
.start_ssl(&pkcs12).unwrap();
|
||||
|
||||
println!("Started http server: 127.0.0.1:8080");
|
||||
println!("Started http server: 127.0.0.1:8443");
|
||||
let _ = sys.run();
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
name = "websocket-example"
|
||||
version = "0.1.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
workspace = "../.."
|
||||
|
||||
[[bin]]
|
||||
name = "server"
|
||||
|
@ -24,5 +25,6 @@ serde = "1.0"
|
|||
serde_json = "1.0"
|
||||
serde_derive = "1.0"
|
||||
|
||||
actix = "^0.3.1"
|
||||
actix-web = { git = "https://github.com/actix/actix-web.git" }
|
||||
#actix = "0.4"
|
||||
actix = { git = "https://github.com/actix/actix" }
|
||||
actix-web = { git = "https://github.com/actix/actix-web" }
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Websocket chat example
|
||||
|
||||
This is extension of the
|
||||
This is extension of the
|
||||
[actix chat example](https://github.com/actix/actix/tree/master/examples/chat)
|
||||
|
||||
Added features:
|
||||
|
@ -9,18 +9,16 @@ Added features:
|
|||
* Chat server runs in separate thread
|
||||
* Tcp listener runs in separate thread
|
||||
|
||||
|
||||
## Server
|
||||
|
||||
Chat server listens for incoming tcp connections. Server can access several types of message:
|
||||
|
||||
* `\list` - list all available rooms
|
||||
* `\join name` - join room, if room does not exist, create new one
|
||||
* `\name name` - set session name
|
||||
* `some message` - just string, send messsage to all peers in same room
|
||||
* client has to send heartbeat `Ping` messages, if server does not receive a heartbeat
|
||||
message for 10 seconds connection gets droppped
|
||||
|
||||
* `\list` - list all available rooms
|
||||
* `\join name` - join room, if room does not exist, create new one
|
||||
* `\name name` - set session name
|
||||
* `some message` - just string, send messsage to all peers in same room
|
||||
* client has to send heartbeat `Ping` messages, if server does not receive a heartbeat message for 10 seconds connection gets droppped
|
||||
|
||||
To start server use command: `cargo run --bin server`
|
||||
|
||||
## Client
|
||||
|
@ -29,7 +27,6 @@ Client connects to server. Reads input from stdin and sends to server.
|
|||
|
||||
To run client use command: `cargo run --bin client`
|
||||
|
||||
|
||||
## WebSocket Browser Client
|
||||
|
||||
Open url: http://localhost:8080/
|
||||
Open url: [http://localhost:8080/](http://localhost:8080/)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extern crate actix;
|
||||
#[macro_use] extern crate actix;
|
||||
extern crate bytes;
|
||||
extern crate byteorder;
|
||||
extern crate futures;
|
||||
|
@ -56,13 +56,9 @@ fn main() {
|
|||
|
||||
struct ChatClient;
|
||||
|
||||
#[derive(Message)]
|
||||
struct ClientCommand(String);
|
||||
|
||||
impl ResponseType for ClientCommand {
|
||||
type Item = ();
|
||||
type Error = ();
|
||||
}
|
||||
|
||||
impl Actor for ChatClient {
|
||||
type Context = FramedContext<Self>;
|
||||
|
||||
|
@ -70,6 +66,15 @@ impl Actor for ChatClient {
|
|||
// start heartbeats otherwise server will disconnect after 10 seconds
|
||||
self.hb(ctx)
|
||||
}
|
||||
|
||||
fn stopping(&mut self, _: &mut FramedContext<Self>) -> bool {
|
||||
println!("Disconnected");
|
||||
|
||||
// Stop application on disconnect
|
||||
Arbiter::system().send(actix::msgs::SystemExit(0));
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl ChatClient {
|
||||
|
@ -83,14 +88,13 @@ impl ChatClient {
|
|||
}
|
||||
|
||||
/// Handle stdin commands
|
||||
impl Handler<ClientCommand> for ChatClient
|
||||
{
|
||||
fn handle(&mut self, msg: ClientCommand, ctx: &mut FramedContext<Self>)
|
||||
-> Response<Self, ClientCommand>
|
||||
{
|
||||
impl Handler<ClientCommand> for ChatClient {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: ClientCommand, ctx: &mut FramedContext<Self>) {
|
||||
let m = msg.0.trim();
|
||||
if m.is_empty() {
|
||||
return Self::empty()
|
||||
return
|
||||
}
|
||||
|
||||
// we check for /sss type of messages
|
||||
|
@ -112,8 +116,6 @@ impl Handler<ClientCommand> for ChatClient
|
|||
} else {
|
||||
let _ = ctx.send(codec::ChatRequest::Message(m.to_owned()));
|
||||
}
|
||||
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,40 +124,26 @@ impl Handler<ClientCommand> for ChatClient
|
|||
impl FramedActor for ChatClient {
|
||||
type Io = TcpStream;
|
||||
type Codec = codec::ClientChatCodec;
|
||||
}
|
||||
|
||||
impl StreamHandler<codec::ChatResponse, io::Error> for ChatClient {
|
||||
|
||||
fn finished(&mut self, _: &mut FramedContext<Self>) {
|
||||
println!("Disconnected");
|
||||
|
||||
// Stop application on disconnect
|
||||
Arbiter::system().send(msgs::SystemExit(0));
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<codec::ChatResponse, io::Error> for ChatClient {
|
||||
|
||||
fn handle(&mut self, msg: codec::ChatResponse, _: &mut FramedContext<Self>)
|
||||
-> Response<Self, codec::ChatResponse>
|
||||
{
|
||||
fn handle(&mut self, msg: io::Result<codec::ChatResponse>, ctx: &mut FramedContext<Self>) {
|
||||
match msg {
|
||||
codec::ChatResponse::Message(ref msg) => {
|
||||
println!("message: {}", msg);
|
||||
}
|
||||
codec::ChatResponse::Joined(ref msg) => {
|
||||
println!("!!! joined: {}", msg);
|
||||
}
|
||||
codec::ChatResponse::Rooms(rooms) => {
|
||||
println!("\n!!! Available rooms:");
|
||||
for room in rooms {
|
||||
println!("{}", room);
|
||||
Err(_) => ctx.stop(),
|
||||
Ok(msg) => match msg {
|
||||
codec::ChatResponse::Message(ref msg) => {
|
||||
println!("message: {}", msg);
|
||||
}
|
||||
println!("");
|
||||
codec::ChatResponse::Joined(ref msg) => {
|
||||
println!("!!! joined: {}", msg);
|
||||
}
|
||||
codec::ChatResponse::Rooms(rooms) => {
|
||||
println!("\n!!! Available rooms:");
|
||||
for room in rooms {
|
||||
println!("{}", room);
|
||||
}
|
||||
println!("");
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ extern crate serde;
|
|||
extern crate serde_json;
|
||||
#[macro_use] extern crate serde_derive;
|
||||
|
||||
#[macro_use]
|
||||
extern crate actix;
|
||||
extern crate actix_web;
|
||||
|
||||
|
@ -29,7 +30,7 @@ struct WsChatSessionState {
|
|||
}
|
||||
|
||||
/// Entry point for our route
|
||||
fn chat_route(req: HttpRequest<WsChatSessionState>) -> Result<Reply> {
|
||||
fn chat_route(req: HttpRequest<WsChatSessionState>) -> Result<HttpResponse> {
|
||||
ws::start(
|
||||
req,
|
||||
WsChatSession {
|
||||
|
@ -52,23 +53,49 @@ struct WsChatSession {
|
|||
|
||||
impl Actor for WsChatSession {
|
||||
type Context = HttpContext<Self, WsChatSessionState>;
|
||||
|
||||
/// Method is called on actor start.
|
||||
/// We register ws session with ChatServer
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
// register self in chat server. `AsyncContext::wait` register
|
||||
// future within context, but context waits until this future resolves
|
||||
// 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| {
|
||||
match res {
|
||||
Ok(Ok(res)) => act.id = res,
|
||||
// something is wrong with chat server
|
||||
_ => ctx.stop(),
|
||||
}
|
||||
fut::ok(())
|
||||
}).wait(ctx);
|
||||
}
|
||||
|
||||
fn stopping(&mut self, ctx: &mut Self::Context) -> bool {
|
||||
// notify chat server
|
||||
ctx.state().addr.send(server::Disconnect{id: self.id});
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle messages from chat server, we simply send it to peer websocket
|
||||
impl Handler<session::Message> for WsChatSession {
|
||||
fn handle(&mut self, msg: session::Message, ctx: &mut Self::Context)
|
||||
-> Response<Self, session::Message>
|
||||
{
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: session::Message, ctx: &mut Self::Context) {
|
||||
ws::WsWriter::text(ctx, &msg.0);
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// WebSocket message handler
|
||||
impl Handler<ws::Message> for WsChatSession {
|
||||
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context)
|
||||
-> Response<Self, ws::Message>
|
||||
{
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) {
|
||||
println!("WEBSOCKET MESSAGE: {:?}", msg);
|
||||
match msg {
|
||||
ws::Message::Ping(msg) =>
|
||||
|
@ -140,42 +167,9 @@ impl Handler<ws::Message> for WsChatSession {
|
|||
}
|
||||
_ => (),
|
||||
}
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamHandler<ws::Message> for WsChatSession
|
||||
{
|
||||
/// Method is called when stream get polled first time.
|
||||
/// We register ws session with ChatServer
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
// register self in chat server. `AsyncContext::wait` register
|
||||
// future within context, but context waits until this future resolves
|
||||
// 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| {
|
||||
match res {
|
||||
Ok(Ok(res)) => act.id = res,
|
||||
// something is wrong with chat server
|
||||
_ => ctx.stop(),
|
||||
}
|
||||
fut::ok(())
|
||||
}).wait(ctx);
|
||||
}
|
||||
|
||||
/// Method is called when stream finishes, even if stream finishes with error.
|
||||
fn finished(&mut self, ctx: &mut Self::Context) {
|
||||
// notify chat server
|
||||
ctx.state().addr.send(server::Disconnect{id: self.id});
|
||||
ctx.stop()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn main() {
|
||||
let _ = env_logger::init();
|
||||
let sys = actix::System::new("websocket-example");
|
||||
|
@ -192,24 +186,28 @@ fn main() {
|
|||
Ok(())
|
||||
}));
|
||||
|
||||
// Websocket sessions state
|
||||
let state = WsChatSessionState { addr: server };
|
||||
|
||||
// Create Http server with websocket support
|
||||
HttpServer::new(
|
||||
Application::build("/", state)
|
||||
// redirect to websocket.html
|
||||
.resource("/", |r| r.handler(Method::GET, |req| {
|
||||
httpcodes::HTTPFound
|
||||
.build()
|
||||
.header("LOCATION", "/static/websocket.html")
|
||||
.body(Body::Empty)
|
||||
}))
|
||||
// websocket
|
||||
.resource("/ws/", |r| r.get(chat_route))
|
||||
// static resources
|
||||
.route("/static", StaticFiles::new("static/", true)))
|
||||
.serve::<_, ()>("127.0.0.1:8080").unwrap();
|
||||
let addr = HttpServer::new(
|
||||
move || {
|
||||
// Websocket sessions state
|
||||
let state = WsChatSessionState { addr: server.clone() };
|
||||
|
||||
Application::with_state(state)
|
||||
// redirect to websocket.html
|
||||
.resource("/", |r| r.method(Method::GET).f(|_| {
|
||||
httpcodes::HTTPFound
|
||||
.build()
|
||||
.header("LOCATION", "/static/websocket.html")
|
||||
.finish()
|
||||
}))
|
||||
// websocket
|
||||
.resource("/ws/", |r| r.route().f(chat_route))
|
||||
// static resources
|
||||
.handler("/static/", fs::StaticFiles::new("static/", true))
|
||||
})
|
||||
.bind("127.0.0.1:8080").unwrap()
|
||||
.start();
|
||||
|
||||
println!("Started http server: 127.0.0.1:8080");
|
||||
let _ = sys.run();
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ use session;
|
|||
|
||||
/// New chat session is created
|
||||
pub struct Connect {
|
||||
pub addr: Box<Subscriber<session::Message> + Send>,
|
||||
pub addr: Box<actix::Subscriber<session::Message> + Send>,
|
||||
}
|
||||
|
||||
/// Response type for Connect message
|
||||
|
@ -25,16 +25,13 @@ impl ResponseType for Connect {
|
|||
}
|
||||
|
||||
/// Session is disconnected
|
||||
#[derive(Message)]
|
||||
pub struct Disconnect {
|
||||
pub id: usize,
|
||||
}
|
||||
|
||||
impl ResponseType for Disconnect {
|
||||
type Item = ();
|
||||
type Error = ();
|
||||
}
|
||||
|
||||
/// Send message to specific room
|
||||
#[derive(Message)]
|
||||
pub struct Message {
|
||||
/// Id of the client session
|
||||
pub id: usize,
|
||||
|
@ -44,11 +41,6 @@ pub struct Message {
|
|||
pub room: String,
|
||||
}
|
||||
|
||||
impl ResponseType for Message {
|
||||
type Item = ();
|
||||
type Error = ();
|
||||
}
|
||||
|
||||
/// List of available rooms
|
||||
pub struct ListRooms;
|
||||
|
||||
|
@ -58,6 +50,7 @@ impl ResponseType for ListRooms {
|
|||
}
|
||||
|
||||
/// Join room, if room does not exists create new one.
|
||||
#[derive(Message)]
|
||||
pub struct Join {
|
||||
/// Client id
|
||||
pub id: usize,
|
||||
|
@ -65,15 +58,10 @@ pub struct Join {
|
|||
pub name: String,
|
||||
}
|
||||
|
||||
impl ResponseType for Join {
|
||||
type Item = ();
|
||||
type Error = ();
|
||||
}
|
||||
|
||||
/// `ChatServer` manages chat rooms and responsible for coordinating chat session.
|
||||
/// implementation is super primitive
|
||||
pub struct ChatServer {
|
||||
sessions: HashMap<usize, Box<Subscriber<session::Message> + Send>>,
|
||||
sessions: HashMap<usize, Box<actix::Subscriber<session::Message> + Send>>,
|
||||
rooms: HashMap<String, HashSet<usize>>,
|
||||
rng: RefCell<ThreadRng>,
|
||||
}
|
||||
|
@ -118,8 +106,9 @@ impl Actor for ChatServer {
|
|||
///
|
||||
/// Register new session and assign unique id to this session
|
||||
impl Handler<Connect> for ChatServer {
|
||||
type Result = MessageResult<Connect>;
|
||||
|
||||
fn handle(&mut self, msg: Connect, _: &mut Context<Self>) -> Response<Self, Connect> {
|
||||
fn handle(&mut self, msg: Connect, _: &mut Context<Self>) -> Self::Result {
|
||||
println!("Someone joined");
|
||||
|
||||
// notify all users in same room
|
||||
|
@ -133,14 +122,15 @@ impl Handler<Connect> for ChatServer {
|
|||
self.rooms.get_mut(&"Main".to_owned()).unwrap().insert(id);
|
||||
|
||||
// send id back
|
||||
Self::reply(id)
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for Disconnect message.
|
||||
impl Handler<Disconnect> for ChatServer {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: Disconnect, _: &mut Context<Self>) -> Response<Self, Disconnect> {
|
||||
fn handle(&mut self, msg: Disconnect, _: &mut Context<Self>) {
|
||||
println!("Someone disconnected");
|
||||
|
||||
let mut rooms: Vec<String> = Vec::new();
|
||||
|
@ -158,40 +148,39 @@ impl Handler<Disconnect> for ChatServer {
|
|||
for room in rooms {
|
||||
self.send_message(&room, "Someone disconnected", 0);
|
||||
}
|
||||
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for Message message.
|
||||
impl Handler<Message> for ChatServer {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: Message, _: &mut Context<Self>) -> Response<Self, Message> {
|
||||
fn handle(&mut self, msg: Message, _: &mut Context<Self>) {
|
||||
self.send_message(&msg.room, msg.msg.as_str(), msg.id);
|
||||
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for `ListRooms` message.
|
||||
impl Handler<ListRooms> for ChatServer {
|
||||
type Result = MessageResult<ListRooms>;
|
||||
|
||||
fn handle(&mut self, _: ListRooms, _: &mut Context<Self>) -> Response<Self, ListRooms> {
|
||||
fn handle(&mut self, _: ListRooms, _: &mut Context<Self>) -> Self::Result {
|
||||
let mut rooms = Vec::new();
|
||||
|
||||
for key in self.rooms.keys() {
|
||||
rooms.push(key.to_owned())
|
||||
}
|
||||
|
||||
Self::reply(rooms)
|
||||
Ok(rooms)
|
||||
}
|
||||
}
|
||||
|
||||
/// Join room, send disconnect message to old room
|
||||
/// send join message to new room
|
||||
impl Handler<Join> for ChatServer {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: Join, _: &mut Context<Self>) -> Response<Self, Join> {
|
||||
fn handle(&mut self, msg: Join, _: &mut Context<Self>) {
|
||||
let Join {id, name} = msg;
|
||||
let mut rooms = Vec::new();
|
||||
|
||||
|
@ -211,7 +200,5 @@ impl Handler<Join> for ChatServer {
|
|||
}
|
||||
self.send_message(&name, "Someone connected", id);
|
||||
self.rooms.get_mut(&name).unwrap().insert(id);
|
||||
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,20 +6,16 @@ use std::time::{Instant, Duration};
|
|||
use futures::Stream;
|
||||
use tokio_core::net::{TcpStream, TcpListener};
|
||||
|
||||
use actix::*;
|
||||
use actix::prelude::*;
|
||||
|
||||
use server::{self, ChatServer};
|
||||
use codec::{ChatRequest, ChatResponse, ChatCodec};
|
||||
|
||||
|
||||
/// Chat server sends this messages to session
|
||||
#[derive(Message)]
|
||||
pub struct Message(pub String);
|
||||
|
||||
impl ResponseType for Message {
|
||||
type Item = ();
|
||||
type Error = ();
|
||||
}
|
||||
|
||||
/// `ChatSession` actor is responsible for tcp peer communitions.
|
||||
pub struct ChatSession {
|
||||
/// unique session id
|
||||
|
@ -36,104 +32,87 @@ impl Actor for ChatSession {
|
|||
/// For tcp communication we are going to use `FramedContext`.
|
||||
/// It is convinient wrapper around `Framed` object from `tokio_io`
|
||||
type Context = FramedContext<Self>;
|
||||
}
|
||||
|
||||
/// To use `FramedContext` we have to define Io type and Codec
|
||||
impl FramedActor for ChatSession {
|
||||
type Io = TcpStream;
|
||||
type Codec= ChatCodec;
|
||||
}
|
||||
|
||||
/// Also `FramedContext` requires Actor which is able to handle stream
|
||||
/// of `<Codec as Decoder>::Item` items.
|
||||
impl StreamHandler<ChatRequest, io::Error> for ChatSession {
|
||||
|
||||
fn started(&mut self, ctx: &mut FramedContext<Self>) {
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
// we'll start heartbeat process on session start.
|
||||
self.hb(ctx);
|
||||
|
||||
// register self in chat server. `AsyncContext::wait` register
|
||||
// future within context, but context waits until this future resolves
|
||||
// before processing any other events.
|
||||
self.addr.call(self, server::Connect{addr: ctx.sync_subscriber()}).then(|res, act, ctx| {
|
||||
match res {
|
||||
Ok(Ok(res)) => act.id = res,
|
||||
// something is wrong with chat server
|
||||
_ => ctx.stop(),
|
||||
}
|
||||
fut::ok(())
|
||||
}).wait(ctx);
|
||||
let addr: SyncAddress<_> = ctx.address();
|
||||
self.addr.call(self, server::Connect{addr: addr.subscriber()})
|
||||
.then(|res, act, ctx| {
|
||||
match res {
|
||||
Ok(Ok(res)) => act.id = res,
|
||||
// something is wrong with chat server
|
||||
_ => ctx.stop(),
|
||||
}
|
||||
actix::fut::ok(())
|
||||
}).wait(ctx);
|
||||
}
|
||||
|
||||
fn finished(&mut self, ctx: &mut FramedContext<Self>) {
|
||||
fn stopping(&mut self, ctx: &mut Self::Context) -> bool {
|
||||
// notify chat server
|
||||
self.addr.send(server::Disconnect{id: self.id});
|
||||
|
||||
ctx.stop()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<ChatRequest, io::Error> for ChatSession {
|
||||
|
||||
/// We'll stop chat session actor on any error, high likely it is just
|
||||
/// termination of the tcp stream.
|
||||
fn error(&mut self, _: io::Error, ctx: &mut FramedContext<Self>) {
|
||||
ctx.stop()
|
||||
}
|
||||
/// To use `FramedContext` we have to define Io type and Codec
|
||||
impl FramedActor for ChatSession {
|
||||
type Io = TcpStream;
|
||||
type Codec= ChatCodec;
|
||||
|
||||
/// This is main event loop for client requests
|
||||
fn handle(&mut self, msg: ChatRequest, ctx: &mut FramedContext<Self>)
|
||||
-> Response<Self, ChatRequest>
|
||||
{
|
||||
fn handle(&mut self, msg: io::Result<ChatRequest>, ctx: &mut FramedContext<Self>) {
|
||||
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| {
|
||||
match res {
|
||||
Ok(Ok(rooms)) => {
|
||||
let _ = ctx.send(ChatResponse::Rooms(rooms));
|
||||
},
|
||||
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| {
|
||||
match res {
|
||||
Ok(Ok(rooms)) => {
|
||||
let _ = ctx.send(ChatResponse::Rooms(rooms));
|
||||
},
|
||||
_ => println!("Something is wrong"),
|
||||
}
|
||||
fut::ok(())
|
||||
}).wait(ctx)
|
||||
// .wait(ctx) pauses all events in context,
|
||||
// so actor wont receive any new messages until it get list of rooms back
|
||||
},
|
||||
ChatRequest::Join(name) => {
|
||||
println!("Join to room: {}", name);
|
||||
self.room = name.clone();
|
||||
self.addr.send(server::Join{id: self.id, name: name.clone()});
|
||||
let _ = ctx.send(ChatResponse::Joined(name));
|
||||
},
|
||||
ChatRequest::Message(message) => {
|
||||
// send message to chat server
|
||||
println!("Peer message: {}", message);
|
||||
self.addr.send(
|
||||
server::Message{id: self.id,
|
||||
msg: message, room:
|
||||
self.room.clone()})
|
||||
}
|
||||
actix::fut::ok(())
|
||||
}).wait(ctx)
|
||||
// .wait(ctx) pauses all events in context,
|
||||
// so actor wont receive any new messages until it get list of rooms back
|
||||
},
|
||||
ChatRequest::Join(name) => {
|
||||
println!("Join to room: {}", name);
|
||||
self.room = name.clone();
|
||||
self.addr.send(server::Join{id: self.id, name: name.clone()});
|
||||
let _ = ctx.send(ChatResponse::Joined(name));
|
||||
},
|
||||
ChatRequest::Message(message) => {
|
||||
// send message to chat server
|
||||
println!("Peer message: {}", message);
|
||||
self.addr.send(
|
||||
server::Message{id: self.id,
|
||||
msg: message, room:
|
||||
self.room.clone()})
|
||||
}
|
||||
// we update heartbeat time on ping from peer
|
||||
ChatRequest::Ping =>
|
||||
self.hb = Instant::now(),
|
||||
}
|
||||
// we update heartbeat time on ping from peer
|
||||
ChatRequest::Ping =>
|
||||
self.hb = Instant::now(),
|
||||
}
|
||||
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>)
|
||||
-> Response<Self, Message>
|
||||
{
|
||||
fn handle(&mut self, msg: Message, ctx: &mut FramedContext<Self>) {
|
||||
// send message to peer
|
||||
let _ = ctx.send(ChatResponse::Message(msg.0));
|
||||
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -188,7 +167,9 @@ impl TcpServer {
|
|||
// So to be able to handle this events `Server` actor has to implement
|
||||
// stream handler `StreamHandler<(TcpStream, net::SocketAddr), io::Error>`
|
||||
let _: () = TcpServer::create(|ctx| {
|
||||
ctx.add_stream(listener.incoming().map(|(t, a)| TcpConnect(t, a)));
|
||||
ctx.add_message_stream(listener.incoming()
|
||||
.map_err(|_| ())
|
||||
.map(|(t, a)| TcpConnect(t, a)));
|
||||
TcpServer{chat: chat}
|
||||
});
|
||||
}
|
||||
|
@ -200,27 +181,17 @@ impl Actor for TcpServer {
|
|||
type Context = Context<Self>;
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
struct TcpConnect(TcpStream, net::SocketAddr);
|
||||
|
||||
impl ResponseType for TcpConnect {
|
||||
type Item = ();
|
||||
type Error = ();
|
||||
}
|
||||
|
||||
/// Handle stream of TcpStream's
|
||||
impl StreamHandler<TcpConnect, io::Error> for TcpServer {}
|
||||
impl Handler<TcpConnect> for TcpServer {
|
||||
type Result = ();
|
||||
|
||||
impl Handler<TcpConnect, io::Error> for TcpServer {
|
||||
|
||||
fn handle(&mut self, msg: TcpConnect, _: &mut Context<Self>) -> Response<Self, TcpConnect>
|
||||
{
|
||||
fn handle(&mut self, msg: TcpConnect, _: &mut Context<Self>) {
|
||||
// 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);
|
||||
|
||||
// this is response for message, which is defined by `ResponseType` trait
|
||||
// in this case we just return unit.
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
|
15
examples/websocket/Cargo.toml
Normal file
15
examples/websocket/Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "websocket"
|
||||
version = "0.1.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
|
||||
[[bin]]
|
||||
name = "server"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
env_logger = "*"
|
||||
futures = "0.1"
|
||||
#actix = "0.4"
|
||||
actix = { git = "https://github.com/actix/actix.git" }
|
||||
actix-web = { git = "https://github.com/actix/actix-web.git" }
|
27
examples/websocket/README.md
Normal file
27
examples/websocket/README.md
Normal file
|
@ -0,0 +1,27 @@
|
|||
# websockect
|
||||
|
||||
Simple echo websocket server.
|
||||
|
||||
## Usage
|
||||
|
||||
### server
|
||||
|
||||
```bash
|
||||
cd actix-web/examples/websocket
|
||||
cargo run
|
||||
# Started http server: 127.0.0.1:8080
|
||||
```
|
||||
|
||||
### web client
|
||||
|
||||
- [http://localhost:8080/ws/index.html](http://localhost:8080/ws/index.html)
|
||||
|
||||
### python client
|
||||
|
||||
- ``pip install aiohttp``
|
||||
- ``python websocket-client.py``
|
||||
|
||||
if ubuntu :
|
||||
|
||||
- ``pip3 install aiohttp``
|
||||
- ``python3 websocket-client.py``
|
|
@ -11,10 +11,9 @@ extern crate env_logger;
|
|||
use actix::*;
|
||||
use actix_web::*;
|
||||
|
||||
|
||||
/// do websocket handshake and start `MyWebSocket` actor
|
||||
fn ws_index(r: HttpRequest) -> Reply {
|
||||
ws::start(r, MyWebSocket).into()
|
||||
fn ws_index(r: HttpRequest) -> Result<HttpResponse> {
|
||||
ws::start(r, MyWebSocket)
|
||||
}
|
||||
|
||||
/// websocket connection is long running connection, it easier
|
||||
|
@ -25,21 +24,11 @@ impl Actor for MyWebSocket {
|
|||
type Context = HttpContext<Self>;
|
||||
}
|
||||
|
||||
/// Standard actix's stream handler for a stream of `ws::Message`
|
||||
impl StreamHandler<ws::Message> for MyWebSocket {
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
println!("WebSocket session openned");
|
||||
}
|
||||
|
||||
fn finished(&mut self, ctx: &mut Self::Context) {
|
||||
println!("WebSocket session closed");
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for `ws::Message`
|
||||
impl Handler<ws::Message> for MyWebSocket {
|
||||
fn handle(&mut self, msg: ws::Message, ctx: &mut HttpContext<Self>)
|
||||
-> Response<Self, ws::Message>
|
||||
{
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: ws::Message, ctx: &mut HttpContext<Self>) {
|
||||
// process websocket messages
|
||||
println!("WS: {:?}", msg);
|
||||
match msg {
|
||||
|
@ -51,7 +40,6 @@ impl Handler<ws::Message> for MyWebSocket {
|
|||
}
|
||||
_ => (),
|
||||
}
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,16 +48,17 @@ fn main() {
|
|||
let _ = env_logger::init();
|
||||
let sys = actix::System::new("ws-example");
|
||||
|
||||
HttpServer::new(
|
||||
Application::default("/")
|
||||
let _addr = HttpServer::new(
|
||||
|| Application::new()
|
||||
// enable logger
|
||||
.middleware(middlewares::Logger::default())
|
||||
.middleware(middleware::Logger::default())
|
||||
// websocket route
|
||||
.resource("/ws/", |r| r.get(ws_index))
|
||||
.resource("/ws/", |r| r.method(Method::GET).f(ws_index))
|
||||
// static files
|
||||
.route("/", StaticFiles::new("examples/static/", true)))
|
||||
.handler("/", fs::StaticFiles::new("../static/", true)))
|
||||
// start http server on 127.0.0.1:8080
|
||||
.serve::<_, ()>("127.0.0.1:8080").unwrap();
|
||||
.bind("127.0.0.1:8080").unwrap()
|
||||
.start();
|
||||
|
||||
println!("Started http server: 127.0.0.1:8080");
|
||||
let _ = sys.run();
|
|
@ -3,13 +3,14 @@
|
|||
[Quickstart](./qs_1.md)
|
||||
- [Getting Started](./qs_2.md)
|
||||
- [Application](./qs_3.md)
|
||||
- [Server](./qs_3_5.md)
|
||||
- [Handler](./qs_4.md)
|
||||
- [Resources and Routes](./qs_5.md)
|
||||
- [Application state](./qs_6.md)
|
||||
- [Request](./qs_7.md)
|
||||
- [Response](./qs_8.md)
|
||||
- [WebSockets](./qs_9.md)
|
||||
- [User sessions](./qs_10.md)
|
||||
- [Logging](./qs_11.md)
|
||||
- [Errors](./qs_4_5.md)
|
||||
- [URL Dispatch](./qs_5.md)
|
||||
- [Request & Response](./qs_7.md)
|
||||
- [Testing](./qs_8.md)
|
||||
- [Middlewares](./qs_10.md)
|
||||
- [Static file handling](./qs_12.md)
|
||||
- [WebSockets](./qs_9.md)
|
||||
- [HTTP/2](./qs_13.md)
|
||||
- [Database integration](./qs_14.md)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Quickstart
|
||||
# Quick start
|
||||
|
||||
Before you can start writing a actix web application, you’ll need a version of Rust installed.
|
||||
Before you can start writing a actix web application, you’ll need a version of Rust installed.
|
||||
We recommend you use rustup to install or configure such a version.
|
||||
|
||||
## Install Rust
|
||||
|
@ -23,12 +23,12 @@ Actix web framework requies rust version 1.20 and up.
|
|||
|
||||
The fastest way to start experimenting with actix web is to clone the actix web repository
|
||||
and run the included examples in the examples/ directory. The following set of
|
||||
commands runs the `basic` example:
|
||||
commands runs the `basics` example:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/actix/actix-web
|
||||
cd actix-web
|
||||
cargo run --example basic
|
||||
cd actix-web/examples/basics
|
||||
cargo run
|
||||
```
|
||||
|
||||
Check `examples/` directory for more examples.
|
||||
Check [examples/](https://github.com/actix/actix-web/tree/master/examples) directory for more examples.
|
||||
|
|
|
@ -1 +1,206 @@
|
|||
# User sessions
|
||||
# Middlewares
|
||||
|
||||
Actix middlewares system allows to add additional behaviour to request/response processing.
|
||||
Middleware can hook into incomnig request process and modify request or halt request
|
||||
processing and return response early. Also it can hook into response processing.
|
||||
|
||||
Typically middlewares involves in following actions:
|
||||
|
||||
* Pre-process the Request
|
||||
* Post-process a Response
|
||||
* Modify application state
|
||||
* Access external services (redis, logging, sessions)
|
||||
|
||||
Middlewares are registered for each application and get executed in same order as
|
||||
registraton order. In general, *middleware* is a type that implements
|
||||
[*Middleware trait*](../actix_web/middlewares/trait.Middleware.html). Each method
|
||||
in this trait has default implementation. Each method can return result immidietly
|
||||
or *future* object.
|
||||
|
||||
Here is example of simple middleware that adds request and response headers:
|
||||
|
||||
```rust
|
||||
# extern crate http;
|
||||
# extern crate actix_web;
|
||||
use http::{header, HttpTryFrom};
|
||||
use actix_web::*;
|
||||
use actix_web::middleware::{Middleware, Started, Response};
|
||||
|
||||
struct Headers; // <- Our middleware
|
||||
|
||||
/// Middleware implementation, middlewares are generic over application state,
|
||||
/// so you can access state with `HttpRequest::state()` method.
|
||||
impl<S> Middleware<S> for Headers {
|
||||
|
||||
/// Method is called when request is ready. It may return
|
||||
/// future, which should resolve before next middleware get called.
|
||||
fn start(&self, req: &mut HttpRequest<S>) -> Started {
|
||||
req.headers_mut().insert(
|
||||
header::CONTENT_TYPE, header::HeaderValue::from_static("text/plain"));
|
||||
Started::Done
|
||||
}
|
||||
|
||||
/// Method is called when handler returns response,
|
||||
/// but before sending http message to peer.
|
||||
fn response(&self, req: &mut HttpRequest<S>, mut resp: HttpResponse) -> Response {
|
||||
resp.headers_mut().insert(
|
||||
header::HeaderName::try_from("X-VERSION").unwrap(),
|
||||
header::HeaderValue::from_static("0.2"));
|
||||
Response::Done(resp)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new()
|
||||
.middleware(Headers) // <- Register middleware, this method could be called multiple times
|
||||
.resource("/", |r| r.h(httpcodes::HTTPOk));
|
||||
}
|
||||
```
|
||||
|
||||
Active provides several useful middlewares, like *logging*, *user sessions*, etc.
|
||||
|
||||
|
||||
## Logging
|
||||
|
||||
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.
|
||||
|
||||
### Usage
|
||||
|
||||
Create `Logger` middleware with the specified `format`.
|
||||
Default `Logger` could be created with `default` method, it uses the default format:
|
||||
|
||||
```ignore
|
||||
%a %t "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T
|
||||
```
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
use actix_web::Application;
|
||||
use actix_web::middleware::Logger;
|
||||
|
||||
fn main() {
|
||||
Application::new()
|
||||
.middleware(Logger::default())
|
||||
.middleware(Logger::new("%a %{User-Agent}i"))
|
||||
.finish();
|
||||
}
|
||||
```
|
||||
|
||||
Here is example of default logging format:
|
||||
|
||||
```
|
||||
INFO:actix_web::middleware::logger: 127.0.0.1:59934 [02/Dec/2017:00:21:43 -0800] "GET / HTTP/1.1" 302 0 "-" "curl/7.54.0" 0.000397
|
||||
INFO:actix_web::middleware::logger: 127.0.0.1:59947 [02/Dec/2017:00:22:40 -0800] "GET /index.html HTTP/1.1" 200 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:57.0) Gecko/20100101 Firefox/57.0" 0.000646
|
||||
```
|
||||
|
||||
### Format
|
||||
|
||||
`%%` The percent sign
|
||||
|
||||
`%a` Remote IP-address (IP-address of proxy if using reverse proxy)
|
||||
|
||||
`%t` Time when the request was started to process
|
||||
|
||||
`%P` The process ID of the child that serviced the request
|
||||
|
||||
`%r` First line of request
|
||||
|
||||
`%s` Response status code
|
||||
|
||||
`%b` Size of response in bytes, including HTTP headers
|
||||
|
||||
`%T` Time taken to serve the request, in seconds with floating fraction in .06f format
|
||||
|
||||
`%D` Time taken to serve the request, in milliseconds
|
||||
|
||||
`%{FOO}i` request.headers['FOO']
|
||||
|
||||
`%{FOO}o` response.headers['FOO']
|
||||
|
||||
`%{FOO}e` os.environ['FOO']
|
||||
|
||||
|
||||
## Default headers
|
||||
|
||||
To set default response headers `DefaultHeaders` middleware could be used.
|
||||
*DefaultHeaders* middleware does not set header if response headers already contains
|
||||
specified header.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
use actix_web::*;
|
||||
|
||||
fn main() {
|
||||
let app = Application::new()
|
||||
.middleware(
|
||||
middleware::DefaultHeaders::build()
|
||||
.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);
|
||||
})
|
||||
.finish();
|
||||
}
|
||||
```
|
||||
|
||||
## User sessions
|
||||
|
||||
Actix provides general solution for session management.
|
||||
[*Session storage*](../actix_web/middleware/struct.SessionStorage.html) middleare can be
|
||||
use with different backend types to store session data in different backends.
|
||||
By default only cookie session backend is implemented. Other backend implementations
|
||||
could be added later.
|
||||
|
||||
[*Cookie session backend*](../actix_web/middleware/struct.CookieSessionBackend.html)
|
||||
uses signed cookies as session storage. *Cookie session backend* creates sessions which
|
||||
are limited to storing fewer than 4000 bytes of data (as the payload must fit into a
|
||||
single cookie). Internal server error get generated if session contains more than 4000 bytes.
|
||||
|
||||
You need to pass a random value to the constructor of *CookieSessionBackend*.
|
||||
This is private key for cookie session. When this value is changed, all session data is lost.
|
||||
Note that whatever you write into your session is visible by the user (but not modifiable).
|
||||
|
||||
In general case, you cretate
|
||||
[*Session storage*](../actix_web/middleware/struct.SessionStorage.html) middleware
|
||||
and initializes it with specific backend implementation, like *CookieSessionBackend*.
|
||||
To access session data
|
||||
[*HttpRequest::session()*](../actix_web/middleware/trait.RequestSession.html#tymethod.session)
|
||||
method has to be used. This method returns
|
||||
[*Session*](../actix_web/middleware/struct.Session.html) object, which allows to get or set
|
||||
session data.
|
||||
|
||||
```rust
|
||||
# extern crate actix;
|
||||
# extern crate actix_web;
|
||||
use actix_web::*;
|
||||
use actix_web::middleware::{RequestSession, SessionStorage, CookieSessionBackend};
|
||||
|
||||
fn index(mut req: HttpRequest) -> Result<&'static str> {
|
||||
// access session data
|
||||
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!")
|
||||
}
|
||||
|
||||
fn main() {
|
||||
# let sys = actix::System::new("basic-example");
|
||||
HttpServer::new(
|
||||
|| Application::new()
|
||||
.middleware(SessionStorage::new( // <- create session middleware
|
||||
CookieSessionBackend::build(&[0; 32]) // <- create cookie session backend
|
||||
.secure(false)
|
||||
.finish()
|
||||
)))
|
||||
.bind("127.0.0.1:59880").unwrap()
|
||||
.start();
|
||||
# actix::Arbiter::system().send(actix::msgs::SystemExit(0));
|
||||
# let _ = sys.run();
|
||||
}
|
||||
```
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
# Logging
|
||||
|
||||
Logging is implemented as middleware. Middlewares get executed in same order as registraton order.
|
||||
It is common to register logging middleware as first middleware for application.
|
||||
Logging middleware has to be registered for each application.
|
||||
|
||||
## Usage
|
||||
|
||||
Create `Logger` middlewares with the specified `format`.
|
||||
Default `Logger` could be created with `default` method, it uses the default format:
|
||||
|
||||
```ignore
|
||||
%a %t "%r" %s %b "%{Referrer}i" "%{User-Agent}i" %T
|
||||
```
|
||||
```rust
|
||||
extern crate actix_web;
|
||||
use actix_web::Application;
|
||||
use actix_web::middlewares::Logger;
|
||||
|
||||
fn main() {
|
||||
Application::default("/")
|
||||
.middleware(Logger::default())
|
||||
.middleware(Logger::new("%a %{User-Agent}i"))
|
||||
.finish();
|
||||
}
|
||||
```
|
||||
|
||||
Here is example of default logging format:
|
||||
|
||||
```
|
||||
INFO:actix_web::middlewares::logger: 127.0.0.1:59934 [02/Dec/2017:00:21:43 -0800] "GET / HTTP/1.1" 302 0 "-" "curl/7.54.0" 0.000397
|
||||
INFO:actix_web::middlewares::logger: 127.0.0.1:59947 [02/Dec/2017:00:22:40 -0800] "GET /index.html HTTP/1.1" 200 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:57.0) Gecko/20100101 Firefox/57.0" 0.000646
|
||||
```
|
||||
|
||||
## Format
|
||||
|
||||
`%%` The percent sign
|
||||
|
||||
`%a` Remote IP-address (IP-address of proxy if using reverse proxy)
|
||||
|
||||
`%t` Time when the request was started to process
|
||||
|
||||
`%P` The process ID of the child that serviced the request
|
||||
|
||||
`%r` First line of request
|
||||
|
||||
`%s` Response status code
|
||||
|
||||
`%b` Size of response in bytes, including HTTP headers
|
||||
|
||||
`%T` Time taken to serve the request, in seconds with floating fraction in .06f format
|
||||
|
||||
`%D` Time taken to serve the request, in milliseconds
|
||||
|
||||
`%{FOO}i` request.headers['FOO']
|
||||
|
||||
`%{FOO}o` response.headers['FOO']
|
||||
|
||||
`%{FOO}e` os.environ['FOO']
|
|
@ -1 +1,44 @@
|
|||
# Static file handling
|
||||
|
||||
## Individual file
|
||||
|
||||
It is possible to serve static files with custom path pattern and `NamedFile`. To
|
||||
match path tail we can use `[.*]` regex.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
use actix_web::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn index(req: HttpRequest) -> Result<fs::NamedFile> {
|
||||
let path: PathBuf = req.match_info().query("tail")?;
|
||||
Ok(fs::NamedFile::open(path)?)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new()
|
||||
.resource(r"/a/{tail:.*}", |r| r.method(Method::GET).f(index))
|
||||
.finish();
|
||||
}
|
||||
```
|
||||
|
||||
## Directory
|
||||
|
||||
To serve files from specific directory and sub-directories `StaticFiles` could be used.
|
||||
`StaticFiles` must be registered with `Application::handler()` method otherwise
|
||||
it won't be able to server sub-paths.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
use actix_web::*;
|
||||
|
||||
fn main() {
|
||||
Application::new()
|
||||
.handler("/static", fs::StaticFiles::new(".", true))
|
||||
.finish();
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
# HTTP/2
|
||||
# HTTP/2.0
|
||||
|
||||
Actix web automatically upgrades connection to `http/2` if possible.
|
||||
Actix web automatically upgrades connection to *HTTP/2.0* if possible.
|
||||
|
||||
## Negotiation
|
||||
|
||||
`HTTP/2` protocol over tls without prior knowlage requires
|
||||
*HTTP/2.0* protocol over tls without prior knowlage requires
|
||||
[tls alpn](https://tools.ietf.org/html/rfc7301). At the moment only
|
||||
`rust-openssl` has support. Turn on `alpn` feature to enable `alpn` negotiation.
|
||||
With enable `alpn` feature `HttpServer` provides
|
||||
|
@ -26,15 +26,16 @@ fn main() {
|
|||
let pkcs12 = Pkcs12::from_der(&pkcs12).unwrap().parse("12345").unwrap();
|
||||
|
||||
HttpServer::new(
|
||||
Application::default("/")
|
||||
.handler("/index.html", index)
|
||||
.serve_tls::<_, ()>("127.0.0.1:8080", pkcs12).unwrap();
|
||||
|| Application::new()
|
||||
.resource("/index.html", |r| r.f(index)))
|
||||
.bind("127.0.0.1:8080").unwrap();
|
||||
.serve_ssl(pkcs12).unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
Upgrade to `http/2` schema described in
|
||||
Upgrade to *HTTP/2.0* schema described in
|
||||
[rfc section 3.2](https://http2.github.io/http2-spec/#rfc.section.3.2) is not supported.
|
||||
Starting `http/2` with prior knowledge is supported for both clear text connection
|
||||
Starting *HTTP/2* with prior knowledge is supported for both clear text connection
|
||||
and tls connection. [rfc section 3.4](https://http2.github.io/http2-spec/#rfc.section.3.4)
|
||||
|
||||
Please check [example](https://github.com/actix/actix-web/tree/master/examples/tls)
|
||||
|
|
124
guide/src/qs_14.md
Normal file
124
guide/src/qs_14.md
Normal file
|
@ -0,0 +1,124 @@
|
|||
# Database integration
|
||||
|
||||
## Diesel
|
||||
|
||||
At the moment of 1.0 release Diesel does not support asynchronous operations.
|
||||
But it possible to use `actix` synchronous actor system as a db interface api.
|
||||
Technically sync actors are worker style actors, multiple of them
|
||||
can be run in parallel and process messages from same queue (sync actors work in mpmc mode).
|
||||
|
||||
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.
|
||||
|
||||
```rust,ignore
|
||||
use actix::prelude::*;*
|
||||
|
||||
struct DbExecutor(SqliteConnection);
|
||||
|
||||
impl Actor for DbExecutor {
|
||||
type Context = SyncContext<Self>;
|
||||
}
|
||||
```
|
||||
|
||||
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,
|
||||
}
|
||||
```
|
||||
|
||||
We can send `CreateUser` message to `DbExecutor` actor, and as result we get
|
||||
`User` model. Now we need to define actual handler implementation for this message.
|
||||
|
||||
```rust,ignore
|
||||
impl Handler<CreateUser> for DbExecutor {
|
||||
|
||||
fn handle(&mut self, msg: CreateUser, _: &mut Self::Context) -> Response<Self, CreateUser>
|
||||
{
|
||||
use self::schema::users::dsl::*;
|
||||
|
||||
// Create insertion model
|
||||
let uuid = format!("{}", uuid::Uuid::new_v4());
|
||||
let new_user = models::NewUser {
|
||||
id: &uuid,
|
||||
name: &msg.name,
|
||||
};
|
||||
|
||||
// normal diesl operations
|
||||
diesel::insert_into(users)
|
||||
.values(&new_user)
|
||||
.execute(&self.0)
|
||||
.expect("Error inserting person");
|
||||
|
||||
let mut items = users
|
||||
.filter(id.eq(&uuid))
|
||||
.load::<models::User>(&self.0)
|
||||
.expect("Error loading person");
|
||||
|
||||
Self::reply(items.pop().unwrap())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
That is it. Now we can use *DbExecutor* actor from any http handler or middleware.
|
||||
All we need is to start *DbExecutor* actors and store address in a state where http handler
|
||||
can access it.
|
||||
|
||||
```rust,ignore
|
||||
/// This is state where we will store *DbExecutor* address.
|
||||
struct State {
|
||||
db: SyncAddress<DbExecutor>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let sys = actix::System::new("diesel-example");
|
||||
|
||||
// Start 3 parallele db executors
|
||||
let addr = SyncArbiter::start(3, || {
|
||||
DbExecutor(SqliteConnection::establish("test.db").unwrap())
|
||||
});
|
||||
|
||||
// Start http server
|
||||
HttpServer::new(move || {
|
||||
Application::with_state(State{db: addr.clone()})
|
||||
.resource("/{name}", |r| r.method(Method::GET).a(index))})
|
||||
.bind("127.0.0.1:8080").unwrap()
|
||||
.start().unwrap();
|
||||
|
||||
println!("Started http server: 127.0.0.1:8080");
|
||||
let _ = sys.run();
|
||||
}
|
||||
```
|
||||
|
||||
And finally we can use address in a requst handler. We get message response
|
||||
asynchronously, so handler needs to return future object, also `Route::a()` needs to be
|
||||
used for async handler registration.
|
||||
|
||||
|
||||
```rust,ignore
|
||||
/// Async handler
|
||||
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()})
|
||||
.from_err()
|
||||
.and_then(|res| {
|
||||
match res {
|
||||
Ok(user) => Ok(httpcodes::HTTPOk.build().json(user)?),
|
||||
Err(_) => Ok(httpcodes::HTTPInternalServerError.into())
|
||||
}
|
||||
})
|
||||
.responder()
|
||||
}
|
||||
```
|
||||
|
||||
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).
|
|
@ -29,29 +29,39 @@ In order to implement a web server, first we need to create a request handler.
|
|||
A request handler is a function that accepts a `HttpRequest` instance as its only parameter
|
||||
and returns a type that can be converted into `HttpResponse`:
|
||||
|
||||
```rust,ignore
|
||||
extern crate actix_web;
|
||||
use actix_web::*;
|
||||
|
||||
fn index(req: HttpRequest) -> &'static str {
|
||||
"Hello world!"
|
||||
}
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
# use actix_web::*;
|
||||
fn index(req: HttpRequest) -> &'static str {
|
||||
"Hello world!"
|
||||
}
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
Next, create an `Application` instance and register the
|
||||
request handler with the application's `resource` on a particular *HTTP method* and *path*::
|
||||
|
||||
```rust,ignore
|
||||
let app = Application::default("/")
|
||||
.resource("/", |r| r.get(index))
|
||||
.finish()
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
# use actix_web::*;
|
||||
# fn index(req: HttpRequest) -> &'static str {
|
||||
# "Hello world!"
|
||||
# }
|
||||
# fn main() {
|
||||
Application::new()
|
||||
.resource("/", |r| r.f(index));
|
||||
# }
|
||||
```
|
||||
|
||||
After that, application instance can be used with `HttpServer` to listen for incoming
|
||||
connections:
|
||||
connections. Server accepts function that should return `HttpHandler` instance:
|
||||
|
||||
```rust,ignore
|
||||
HttpServer::new(app).serve::<_, ()>("127.0.0.1:8088");
|
||||
HttpServer::new(
|
||||
|| Application::new()
|
||||
.resource("/", |r| r.f(index)))
|
||||
.bind("127.0.0.1:8088")?
|
||||
.run();
|
||||
```
|
||||
|
||||
That's it. Now, compile and run the program with cargo run.
|
||||
|
@ -59,9 +69,8 @@ Head over to ``http://localhost:8088/`` to see the results.
|
|||
|
||||
Here is full source of main.rs file:
|
||||
|
||||
```rust
|
||||
extern crate actix;
|
||||
extern crate actix_web;
|
||||
```rust,ignore
|
||||
# extern crate actix_web;
|
||||
use actix_web::*;
|
||||
|
||||
fn index(req: HttpRequest) -> &'static str {
|
||||
|
@ -69,22 +78,15 @@ fn index(req: HttpRequest) -> &'static str {
|
|||
}
|
||||
|
||||
fn main() {
|
||||
let sys = actix::System::new("example");
|
||||
|
||||
HttpServer::new(
|
||||
Application::default("/")
|
||||
.resource("/", |r| r.get(index)))
|
||||
.serve::<_, ()>("127.0.0.1:8088").unwrap();
|
||||
|
||||
println!("Started http server: 127.0.0.1:8088");
|
||||
// do not copy this line
|
||||
actix::Arbiter::system().send(actix::msgs::SystemExit(0));
|
||||
|
||||
let _ = sys.run();
|
||||
|| Application::new()
|
||||
.resource("/", |r| r.f(index)))
|
||||
.bind("127.0.0.1:8088").expect("Can not bind to 127.0.0.1:8088")
|
||||
.run();
|
||||
}
|
||||
```
|
||||
|
||||
Note on `actix` crate. Actix web framework is built on top of actix actor library.
|
||||
`actix::System` initializes actor system, `HttpServer` is an actor and must run within
|
||||
proper configured actix system. For more information please check
|
||||
properly configured actix system. For more information please check
|
||||
[actix documentation](https://actix.github.io/actix/actix/)
|
||||
|
|
|
@ -5,44 +5,105 @@ It provides routing, middlewares, pre-processing of requests, and post-processin
|
|||
websocket protcol handling, multipart streams, etc.
|
||||
|
||||
All actix web server is built around `Application` instance.
|
||||
It is used for registering handlers for routes and resources, middlewares.
|
||||
Also it stores applicationspecific state that is shared accross all handlers
|
||||
It is used for registering routes for resources, middlewares.
|
||||
Also it stores application specific state that is shared across all handlers
|
||||
within same application.
|
||||
|
||||
Application acts as namespace for all routes, i.e all routes for specific application
|
||||
has same url path prefix:
|
||||
has same url path prefix. Application prefix always contains laading "/" slash.
|
||||
If supplied prefix does not contain leading slash, it get inserted.
|
||||
Prefix should consists of valud path segments. i.e for application with prefix `/app`
|
||||
any request with following paths `/app`, `/app/` or `/app/test` would match,
|
||||
but path `/application` would not match.
|
||||
|
||||
```rust,ignore
|
||||
let app = Application::default("/prefix")
|
||||
.resource("/index.html", |r| r.handler(Method::GET, index)
|
||||
# extern crate actix_web;
|
||||
# extern crate tokio_core;
|
||||
# use actix_web::*;
|
||||
# fn index(req: HttpRequest) -> &'static str {
|
||||
# "Hello world!"
|
||||
# }
|
||||
# fn main() {
|
||||
let app = Application::new()
|
||||
.prefix("/app")
|
||||
.resource("/index.html", |r| r.method(Method::GET).f(index))
|
||||
.finish()
|
||||
# }
|
||||
```
|
||||
|
||||
In this example application with `/prefix` prefix and `index.html` resource
|
||||
get created. This resource is available as on `/prefix/index.html` url.
|
||||
In this example application with `/app` prefix and `index.html` resource
|
||||
get created. This resource is available as on `/app/index.html` url.
|
||||
For more information check
|
||||
[*URL Matching*](./qs_5.html#using-a-application-prefix-to-compose-applications) section.
|
||||
|
||||
Multiple applications could be served with one server:
|
||||
|
||||
```rust
|
||||
extern crate actix_web;
|
||||
extern crate tokio_core;
|
||||
use std::net::SocketAddr;
|
||||
# extern crate actix_web;
|
||||
# extern crate tokio_core;
|
||||
# use tokio_core::net::TcpStream;
|
||||
# use std::net::SocketAddr;
|
||||
use actix_web::*;
|
||||
use tokio_core::net::TcpStream;
|
||||
|
||||
fn main() {
|
||||
HttpServer::<TcpStream, SocketAddr, _>::new(vec![
|
||||
Application::default("/app1")
|
||||
.resource("/", |r| r.get(|r| httpcodes::HTTPOk))
|
||||
.finish(),
|
||||
Application::default("/app2")
|
||||
.resource("/", |r| r.get(|r| httpcodes::HTTPOk))
|
||||
.finish(),
|
||||
Application::default("/")
|
||||
.resource("/", |r| r.get(|r| httpcodes::HTTPOk))
|
||||
.finish(),
|
||||
HttpServer::<TcpStream, SocketAddr, _, _>::new(|| vec![
|
||||
Application::new()
|
||||
.prefix("/app1")
|
||||
.resource("/", |r| r.f(|r| httpcodes::HTTPOk)),
|
||||
Application::new()
|
||||
.prefix("/app2")
|
||||
.resource("/", |r| r.f(|r| httpcodes::HTTPOk)),
|
||||
Application::new()
|
||||
.resource("/", |r| r.f(|r| httpcodes::HTTPOk)),
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
All `/app1` requests route to first application, `/app2` to second and then all other to third.
|
||||
Applications get matched based on registration order, if application with more general
|
||||
prefix is registered before less generic, that would effectively block less generic
|
||||
application to get matched. For example if *application* with prefix "/" get registered
|
||||
as first application, it would match all incoming requests.
|
||||
|
||||
## State
|
||||
|
||||
Application state is shared with all routes and resources within same application.
|
||||
State could be accessed with `HttpRequest::state()` method as a read-only item
|
||||
but interior mutability pattern with `RefCell` could be used to archive state mutability.
|
||||
State could be accessed with `HttpContext::state()` in case of http actor.
|
||||
State also available to route matching predicates and middlewares.
|
||||
|
||||
Let's write simple application that uses shared state. We are going to store requests count
|
||||
in the state:
|
||||
|
||||
```rust
|
||||
# extern crate actix;
|
||||
# extern crate actix_web;
|
||||
#
|
||||
use actix_web::*;
|
||||
use std::cell::Cell;
|
||||
|
||||
// This struct represents state
|
||||
struct AppState {
|
||||
counter: Cell<usize>,
|
||||
}
|
||||
|
||||
fn index(req: HttpRequest<AppState>) -> String {
|
||||
let count = req.state().counter.get() + 1; // <- get count
|
||||
req.state().counter.set(count); // <- store new count in state
|
||||
|
||||
format!("Request number: {}", count) // <- response with count
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::with_state(AppState{counter: Cell::new(0)})
|
||||
.resource("/", |r| r.method(Method::GET).f(index))
|
||||
.finish();
|
||||
}
|
||||
```
|
||||
|
||||
Note on application state, http server accepts application factory rather than application
|
||||
instance. Http server construct application instance for each thread, so application state
|
||||
must be constructed multiple times. If you want to share state between different thread
|
||||
shared object should be used, like `Arc`. Application state does not need to be `Send` and `Sync`
|
||||
but application factory must be `Send` + `Sync`.
|
||||
|
|
197
guide/src/qs_3_5.md
Normal file
197
guide/src/qs_3_5.md
Normal file
|
@ -0,0 +1,197 @@
|
|||
# Server
|
||||
|
||||
[*HttpServer*](../actix_web/struct.HttpServer.html) type is responsible for
|
||||
serving http requests. *HttpServer* accept application factory as a parameter,
|
||||
Application factory must have `Send` + `Sync` bounderies. More about that in
|
||||
*multi-threading* section. To bind to specific socket address `bind()` must be used.
|
||||
This method could be called multiple times. To start http server one of the *start*
|
||||
methods could be used. `start()` method start simple server, `start_tls()` or `start_ssl()`
|
||||
starts ssl server. *HttpServer* is an actix actor, it has to be initialized
|
||||
within properly configured actix system:
|
||||
|
||||
```rust
|
||||
# extern crate actix;
|
||||
# extern crate actix_web;
|
||||
use actix::*;
|
||||
use actix_web::*;
|
||||
|
||||
fn main() {
|
||||
let sys = actix::System::new("guide");
|
||||
|
||||
HttpServer::new(
|
||||
|| Application::new()
|
||||
.resource("/", |r| r.h(httpcodes::HTTPOk)))
|
||||
.bind("127.0.0.1:59080").unwrap()
|
||||
.start();
|
||||
|
||||
# actix::Arbiter::system().send(actix::msgs::SystemExit(0));
|
||||
let _ = sys.run();
|
||||
}
|
||||
```
|
||||
|
||||
It is possible to start server in separate thread with *spawn()* method. In that
|
||||
case server spawns new thread and create new actix system in it. To stop
|
||||
this server send `StopServer` message.
|
||||
|
||||
Http server is implemented as an actix actor. It is possible to communicate with server
|
||||
via messaging system. All start methods like `start()`, `start_ssl()`, etc returns
|
||||
address of the started http server. Actix http server accept several messages:
|
||||
|
||||
* `PauseServer` - Pause accepting incoming connections
|
||||
* `ResumeServer` - Resume accepting incoming connections
|
||||
* `StopServer` - Stop incoming connection processing, stop all workers and exit
|
||||
|
||||
```rust
|
||||
# extern crate futures;
|
||||
# extern crate actix;
|
||||
# extern crate actix_web;
|
||||
# use futures::Future;
|
||||
use actix_web::*;
|
||||
use std::thread;
|
||||
use std::sync::mpsc;
|
||||
|
||||
fn main() {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
thread::spawn(move || {
|
||||
let sys = actix::System::new("http-server");
|
||||
let addr = HttpServer::new(
|
||||
|| Application::new()
|
||||
.resource("/", |r| r.h(httpcodes::HTTPOk)))
|
||||
.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();
|
||||
let _ = tx.send(addr);
|
||||
let _ = sys.run();
|
||||
});
|
||||
|
||||
let addr = rx.recv().unwrap();
|
||||
let _ = addr.call_fut(
|
||||
dev::StopServer{graceful:true}).wait(); // <- Send `StopServer` message to server.
|
||||
}
|
||||
```
|
||||
|
||||
## Multi-threading
|
||||
|
||||
Http server automatically starts number of http workers, by default
|
||||
this number is equal to number of logical cpu in the system. This number
|
||||
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(
|
||||
|| Application::new()
|
||||
.resource("/", |r| r.h(httpcodes::HTTPOk)))
|
||||
.threads(4); // <- Start 4 workers
|
||||
}
|
||||
```
|
||||
|
||||
Server create separate application instance for each created worker. Application state
|
||||
is not shared between threads, to share state `Arc` could be used. Application state
|
||||
does not need to be `Send` and `Sync` but application factory must be `Send` + `Sync`.
|
||||
|
||||
## SSL
|
||||
|
||||
There are two `tls` and `alpn` features for ssl server. `tls` feature is for `native-tls`
|
||||
integration and `alpn` is for `openssl`.
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
actix-web = { git = "https://github.com/actix/actix-web", features=["alpn"] }
|
||||
```
|
||||
|
||||
```rust,ignore
|
||||
use std::fs::File;
|
||||
use actix_web::*;
|
||||
|
||||
fn main() {
|
||||
let mut file = File::open("identity.pfx").unwrap();
|
||||
let mut pkcs12 = vec![];
|
||||
file.read_to_end(&mut pkcs12).unwrap();
|
||||
let pkcs12 = Pkcs12::from_der(&pkcs12).unwrap().parse("12345").unwrap();
|
||||
|
||||
HttpServer::new(
|
||||
|| Application::new()
|
||||
.resource("/index.html", |r| r.f(index)))
|
||||
.bind("127.0.0.1:8080").unwrap()
|
||||
.serve_ssl(pkcs12).unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
Note on *HTTP/2.0* protocol over tls without prior knowledge, it requires
|
||||
[tls alpn](https://tools.ietf.org/html/rfc7301). At the moment only
|
||||
`openssl` has `alpn ` support.
|
||||
|
||||
Please check [example](https://github.com/actix/actix-web/tree/master/examples/tls)
|
||||
for full example.
|
||||
|
||||
## Keep-Alive
|
||||
|
||||
Actix can wait for requests on a keep-alive connection. *Keep alive*
|
||||
connection behavior is defined by server settings.
|
||||
|
||||
* `Some(75)` - enable 75 sec *keep alive* timer according request and response settings.
|
||||
* `Some(0)` - disable *keep alive*.
|
||||
* `None` - Use `SO_KEEPALIVE` socket option.
|
||||
|
||||
```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(||
|
||||
Application::new()
|
||||
.resource("/", |r| r.h(httpcodes::HTTPOk)))
|
||||
.keep_alive(None); // <- Use `SO_KEEPALIVE` socket option.
|
||||
}
|
||||
```
|
||||
|
||||
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".
|
||||
|
||||
*Connection type* could be change with `HttpResponseBuilder::connection_type()` method.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
# use actix_web::httpcodes::*;
|
||||
use actix_web::*;
|
||||
|
||||
fn index(req: HttpRequest) -> HttpResponse {
|
||||
HTTPOk.build()
|
||||
.connection_type(headers::ConnectionType::Close) // <- Close connection
|
||||
.force_close() // <- Alternative method
|
||||
.finish().unwrap()
|
||||
}
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
## Graceful shutdown
|
||||
|
||||
Actix http server support graceful shutdown. After receiving a stop signal, workers
|
||||
have specific amount of time to finish serving requests. Workers still alive after the
|
||||
timeout are force dropped. By default shutdown timeout sets to 30 seconds.
|
||||
You can change this parameter with `HttpServer::shutdown_timeout()` method.
|
||||
|
||||
You can send stop message to server with server address and specify if you what
|
||||
graceful shutdown or not. `start()` methods return address of the server.
|
||||
|
||||
Http server handles several OS signals. *CTRL-C* is available on all OSs,
|
||||
other signals are available on unix systems.
|
||||
|
||||
* *SIGINT* - Force shutdown workers
|
||||
* *SIGTERM* - Graceful shutdown workers
|
||||
* *SIGQUIT* - Force shutdown workers
|
||||
|
||||
It is possible to disable signals handling with `HttpServer::disable_signals()` method.
|
|
@ -1,21 +1,19 @@
|
|||
# Handler
|
||||
|
||||
A request handler can by any object that implements
|
||||
[`Handler` trait](../actix_web/struct.HttpResponse.html#implementations).
|
||||
A request handler can by any object that implements
|
||||
[*Handler trait*](../actix_web/dev/trait.Handler.html).
|
||||
Request handling happen in two stages. First handler object get called.
|
||||
Handle can return any object that implements
|
||||
[*Responder trait*](../actix_web/trait.Responder.html#foreign-impls).
|
||||
Then `respond_to()` get called on returned object. And finally
|
||||
result of the `respond_to()` call get converted to `Reply` object.
|
||||
|
||||
By default actix provdes several `Handler` implementations:
|
||||
|
||||
* Simple function that accepts `HttpRequest` and returns any object that
|
||||
can be converted to `HttpResponse`
|
||||
* Function that accepts `HttpRequest` and returns `Result<Reply, Into<Error>>` object.
|
||||
* Function that accepts `HttpRequest` and return actor that has `HttpContext<A>`as a context.
|
||||
|
||||
Actix provides response conversion into `HttpResponse` for some standard types,
|
||||
By default actix provides `Responder` implementations for some standard types,
|
||||
like `&'static str`, `String`, etc.
|
||||
For complete list of implementations check
|
||||
[HttpResponse documentation](../actix_web/struct.HttpResponse.html#implementations).
|
||||
[*Responder documentation*](../actix_web/trait.Responder.html#foreign-impls).
|
||||
|
||||
Examples:
|
||||
Examples of valid handlers:
|
||||
|
||||
```rust,ignore
|
||||
fn index(req: HttpRequest) -> &'static str {
|
||||
|
@ -41,13 +39,95 @@ fn index(req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
|||
}
|
||||
```
|
||||
|
||||
## Custom conversion
|
||||
Some notes on shared application state and handler state. If you noticed
|
||||
*Handler* trait is generic over *S*, which defines application state type. So
|
||||
application state is accessible from handler with `HttpRequest::state()` method.
|
||||
But state is accessible as a read-only reference, if you need mutable access to state
|
||||
you have to implement it yourself. On other hand handler can mutable access it's own state
|
||||
as `handle` method takes mutable reference to *self*. Beware, actix creates multiple copies
|
||||
of application state and handlers, unique for each thread, so if you run your
|
||||
application in several threads actix will create same amount as number of threads
|
||||
of application state objects and handler objects.
|
||||
|
||||
Here is example of handler that stores number of processed requests:
|
||||
|
||||
```rust
|
||||
# extern crate actix;
|
||||
# extern crate actix_web;
|
||||
use actix_web::*;
|
||||
use actix_web::dev::Handler;
|
||||
|
||||
struct MyHandler(usize);
|
||||
|
||||
impl<S> Handler<S> for MyHandler {
|
||||
type Result = HttpResponse;
|
||||
|
||||
/// Handle request
|
||||
fn handle(&mut self, req: HttpRequest<S>) -> Self::Result {
|
||||
self.0 += 1;
|
||||
httpcodes::HTTPOk.into()
|
||||
}
|
||||
}
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
This handler will work, but `self.0` value will be different depends on number of threads and
|
||||
number of requests processed per thread. Proper implementation would use `Arc` and `AtomicUsize`
|
||||
|
||||
```rust
|
||||
# extern crate actix;
|
||||
# extern crate actix_web;
|
||||
use actix_web::*;
|
||||
use actix_web::dev::Handler;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
struct MyHandler(Arc<AtomicUsize>);
|
||||
|
||||
impl<S> Handler<S> for MyHandler {
|
||||
type Result = HttpResponse;
|
||||
|
||||
/// 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()
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let sys = actix::System::new("example");
|
||||
|
||||
let inc = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
HttpServer::new(
|
||||
move || {
|
||||
let cloned = inc.clone();
|
||||
Application::new()
|
||||
.resource("/", move |r| r.h(MyHandler(cloned)))
|
||||
})
|
||||
.bind("127.0.0.1:8088").unwrap()
|
||||
.start();
|
||||
|
||||
println!("Started http server: 127.0.0.1:8088");
|
||||
# actix::Arbiter::system().send(actix::msgs::SystemExit(0));
|
||||
let _ = sys.run();
|
||||
}
|
||||
```
|
||||
|
||||
Be careful with synchronization primitives like *Mutex* or *RwLock*. Actix web framework
|
||||
handles request asynchronously, by blocking thread execution all concurrent
|
||||
request handling processes would block. If you need to share or update some state
|
||||
from multiple threads consider using [actix](https://actix.github.io/actix/actix/) actor system.
|
||||
|
||||
## Response with custom type
|
||||
|
||||
To return custom type directly from handler function, type needs to implement `Responder` trait.
|
||||
Let's create response for custom type that serializes to `application/json` response:
|
||||
|
||||
```rust
|
||||
extern crate actix;
|
||||
extern crate actix_web;
|
||||
# extern crate actix;
|
||||
# extern crate actix_web;
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
#[macro_use] extern crate serde_derive;
|
||||
|
@ -55,87 +135,101 @@ use actix_web::*;
|
|||
|
||||
#[derive(Serialize)]
|
||||
struct MyObj {
|
||||
name: String,
|
||||
name: &'static str,
|
||||
}
|
||||
|
||||
/// we have to convert Error into HttpResponse as well, but with
|
||||
/// specialization this could be handled genericly.
|
||||
impl Into<HttpResponse> for MyObj {
|
||||
fn into(self) -> HttpResponse {
|
||||
let body = match serde_json::to_string(&self) {
|
||||
Err(err) => return Error::from(err).into(),
|
||||
Ok(body) => body,
|
||||
};
|
||||
/// Responder
|
||||
impl Responder for MyObj {
|
||||
type Item = HttpResponse;
|
||||
type Error = Error;
|
||||
|
||||
fn respond_to(self, req: HttpRequest) -> Result<HttpResponse> {
|
||||
let body = serde_json::to_string(&self)?;
|
||||
|
||||
// Create response and set content type
|
||||
HttpResponse::Ok()
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body).unwrap()
|
||||
.body(body)?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Because `MyObj` implements `Responder`, it is possible to return it directly
|
||||
fn index(req: HttpRequest) -> MyObj {
|
||||
MyObj{name: "user"}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let sys = actix::System::new("example");
|
||||
|
||||
HttpServer::new(
|
||||
Application::default("/")
|
||||
.resource("/", |r| r.handler(
|
||||
Method::GET, |req| {MyObj{name: "user".to_owned()}})))
|
||||
.serve::<_, ()>("127.0.0.1:8088").unwrap();
|
||||
|| Application::new()
|
||||
.resource("/", |r| r.method(Method::GET).f(index)))
|
||||
.bind("127.0.0.1:8088").unwrap()
|
||||
.start();
|
||||
|
||||
println!("Started http server: 127.0.0.1:8088");
|
||||
actix::Arbiter::system().send(actix::msgs::SystemExit(0)); // <- remove this line, this code stops system during testing
|
||||
|
||||
# actix::Arbiter::system().send(actix::msgs::SystemExit(0));
|
||||
let _ = sys.run();
|
||||
}
|
||||
```
|
||||
|
||||
If `specialization` is enabled, conversion could be simplier:
|
||||
|
||||
```rust,ignore
|
||||
impl Into<Result<HttpResponse>> for MyObj {
|
||||
fn into(self) -> Result<HttpResponse> {
|
||||
let body = serde_json::to_string(&self)?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body)?)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Async handlers
|
||||
|
||||
There are two different types of async handlers.
|
||||
|
||||
Response object could be generated asynchronously. In this case handle must
|
||||
return `Future` object that resolves to `HttpResponse`, i.e:
|
||||
Response object could be generated asynchronously or more precisely, any type
|
||||
that implements [*Responder*](../actix_web/trait.Responder.html) trait. In this case handle must
|
||||
return `Future` object that resolves to *Responder* type, i.e:
|
||||
|
||||
```rust,ignore
|
||||
fn index(req: HttpRequest) -> Box<Future<HttpResponse, Error>> {
|
||||
...
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
# extern crate futures;
|
||||
# extern crate bytes;
|
||||
# use actix_web::*;
|
||||
# use bytes::Bytes;
|
||||
# use futures::stream::once;
|
||||
# use futures::future::{FutureResult, result};
|
||||
fn index(req: HttpRequest) -> FutureResult<HttpResponse, Error> {
|
||||
|
||||
result(HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(format!("Hello!"))
|
||||
.map_err(|e| e.into()))
|
||||
}
|
||||
|
||||
fn index2(req: HttpRequest) -> FutureResult<&'static str, Error> {
|
||||
result(Ok("Welcome!"))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new()
|
||||
.resource("/async", |r| r.route().a(index))
|
||||
.resource("/", |r| r.route().a(index2))
|
||||
.finish();
|
||||
}
|
||||
```
|
||||
|
||||
This handler can be registered with `ApplicationBuilder::async()` and
|
||||
`Resource::async()` methods.
|
||||
|
||||
Or response body can be generated asynchronously. In this case body
|
||||
must implement stream trait `Stream<Item=Bytes, Error=Error>`, i.e:
|
||||
|
||||
|
||||
```rust,ignore
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
# extern crate futures;
|
||||
# extern crate bytes;
|
||||
# use actix_web::*;
|
||||
# use bytes::Bytes;
|
||||
# use futures::stream::once;
|
||||
fn index(req: HttpRequest) -> HttpResponse {
|
||||
let body: Box<Stream<Item=Bytes, Error=Error>> = Box::new(SomeStream::new());
|
||||
let body = once(Ok(Bytes::from_static(b"test")));
|
||||
|
||||
HttpResponse::Ok().
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(Body::Streaming(body)).unwrap()
|
||||
.body(Body::Streaming(Box::new(body))).unwrap()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::default("/")
|
||||
.async("/async", index)
|
||||
Application::new()
|
||||
.resource("/async", |r| r.f(index))
|
||||
.finish();
|
||||
}
|
||||
```
|
||||
|
|
136
guide/src/qs_4_5.md
Normal file
136
guide/src/qs_4_5.md
Normal file
|
@ -0,0 +1,136 @@
|
|||
# Errors
|
||||
|
||||
Actix uses [`Error` type](../actix_web/error/struct.Error.html)
|
||||
and [`ResponseError` trait](../actix_web/error/trait.ResponseError.html)
|
||||
for handling handler's errors.
|
||||
Any error that implements `ResponseError` trait can be returned as error value.
|
||||
*Handler* can return *Result* object, actix by default provides
|
||||
`Responder` implemenation for compatible result object. Here is implementation
|
||||
definition:
|
||||
|
||||
```rust,ignore
|
||||
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
|
||||
by default.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
# use actix_web::*;
|
||||
use std::io;
|
||||
|
||||
fn index(req: HttpRequest) -> io::Result<fs::NamedFile> {
|
||||
Ok(fs::NamedFile::open("static/index.html")?)
|
||||
}
|
||||
#
|
||||
# fn main() {
|
||||
# Application::new()
|
||||
# .resource(r"/a/index.html", |r| r.f(index))
|
||||
# .finish();
|
||||
# }
|
||||
```
|
||||
|
||||
## Custom error response
|
||||
|
||||
To add support for custom errors, all we need to do is just implement `ResponseError` trait
|
||||
for custom error. `ResponseError` trait has default implementation
|
||||
for `error_response()` method, it generates *500* response.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
#[macro_use] extern crate failure;
|
||||
use actix_web::*;
|
||||
|
||||
#[derive(Fail, Debug)]
|
||||
#[fail(display="my error")]
|
||||
struct MyError {
|
||||
name: &'static str
|
||||
}
|
||||
|
||||
/// Use default implementation for `error_response()` method
|
||||
impl error::ResponseError for MyError {}
|
||||
|
||||
fn index(req: HttpRequest) -> Result<&'static str, MyError> {
|
||||
Err(MyError{name: "test"})
|
||||
}
|
||||
#
|
||||
# fn main() {
|
||||
# Application::new()
|
||||
# .resource(r"/a/index.html", |r| r.f(index))
|
||||
# .finish();
|
||||
# }
|
||||
```
|
||||
|
||||
In this example *index* handler will always return *500* response. But it is easy
|
||||
to return different responses for different type of errors.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
#[macro_use] extern crate failure;
|
||||
use actix_web::*;
|
||||
|
||||
#[derive(Fail, Debug)]
|
||||
enum MyError {
|
||||
#[fail(display="internal error")]
|
||||
InternalError,
|
||||
#[fail(display="bad request")]
|
||||
BadClientData,
|
||||
#[fail(display="timeout")]
|
||||
Timeout,
|
||||
}
|
||||
|
||||
impl error::ResponseError for MyError {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
match *self {
|
||||
MyError::InternalError => HttpResponse::new(
|
||||
StatusCode::INTERNAL_SERVER_ERROR, Body::Empty),
|
||||
MyError::BadClientData => HttpResponse::new(
|
||||
StatusCode::BAD_REQUEST, Body::Empty),
|
||||
MyError::Timeout => HttpResponse::new(
|
||||
StatusCode::GATEWAY_TIMEOUT, Body::Empty),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn index(req: HttpRequest) -> Result<&'static str, MyError> {
|
||||
Err(MyError::BadClientData)
|
||||
}
|
||||
#
|
||||
# fn main() {
|
||||
# Application::new()
|
||||
# .resource(r"/a/index.html", |r| r.f(index))
|
||||
# .finish();
|
||||
# }
|
||||
```
|
||||
|
||||
## Error helpers
|
||||
|
||||
Actix provides set of error helper types. It is possible to use them to generate
|
||||
specific error response. We can use helper types for first example with custom error.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
#[macro_use] extern crate failure;
|
||||
use actix_web::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MyError {
|
||||
name: &'static str
|
||||
}
|
||||
|
||||
fn index(req: HttpRequest) -> Result<&'static str> {
|
||||
let result: Result<&'static str, MyError> = Err(MyError{name: "test"});
|
||||
|
||||
Ok(result.map_err(error::ErrorBadRequest)?)
|
||||
}
|
||||
# fn main() {
|
||||
# Application::new()
|
||||
# .resource(r"/a/index.html", |r| r.f(index))
|
||||
# .finish();
|
||||
# }
|
||||
```
|
||||
|
||||
In this example *BAD REQUEST* response get generated for `MyError` error.
|
|
@ -1,107 +1,578 @@
|
|||
# Resources and Routes
|
||||
# URL Dispatch
|
||||
|
||||
All resources and routes register for specific application.
|
||||
Application routes incoming requests based on route criteria which is defined during
|
||||
resource registration or path prefix for simple handlers.
|
||||
Internally *router* is a list of *resources*. Resource is an entry in *route table*
|
||||
which corresponds to requested URL.
|
||||
URL dispatch provides a simple way to map URLs to `Handler` code using a simple pattern matching
|
||||
language. *Regex* crate and it's
|
||||
[*RegexSet*](https://doc.rust-lang.org/regex/regex/struct.RegexSet.html) is beeing used for
|
||||
pattern matching. If one of the patterns matches the path information associated with a request,
|
||||
a particular handler object is invoked. A handler is a specific object that implements
|
||||
`Handler` trait, defined in your application, that receives the request and returns
|
||||
a response object. More informatin is available in [handler section](../qs_4.html).
|
||||
|
||||
Prefix handler:
|
||||
## Resource configuration
|
||||
|
||||
```rust,ignore
|
||||
fn index(req: Httprequest) -> HttpResponse {
|
||||
...
|
||||
}
|
||||
Resource configuraiton is the act of adding a new resource to an application.
|
||||
A resource has a name, which acts as an identifier to be used for URL generation.
|
||||
The name also allows developers to add routes to existing resources.
|
||||
A resource also has a pattern, meant to match against the *PATH* portion of a *URL*,
|
||||
it does not match against *QUERY* portion (the portion following the scheme and
|
||||
port, e.g., */foo/bar* in the *URL* *http://localhost:8080/foo/bar?q=value*).
|
||||
|
||||
The [Application::resource](../actix_web/struct.Application.html#method.resource) methods
|
||||
add a single resource to application routing table. This method accepts *path pattern*
|
||||
and resource configuration funnction.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
# use actix_web::*;
|
||||
# use actix_web::httpcodes::*;
|
||||
#
|
||||
# fn index(req: HttpRequest) -> HttpResponse {
|
||||
# unimplemented!()
|
||||
# }
|
||||
#
|
||||
fn main() {
|
||||
Application::default("/")
|
||||
.handler("/prefix", |req| index)
|
||||
Application::new()
|
||||
.resource("/prefix", |r| r.f(index))
|
||||
.resource("/user/{name}",
|
||||
|r| r.method(Method::GET).f(|req| HTTPOk))
|
||||
.finish();
|
||||
}
|
||||
```
|
||||
|
||||
In this example `index` get called for any url which starts with `/prefix`.
|
||||
|
||||
Application prefix combines with handler prefix i.e
|
||||
*Configuraiton function* has following type:
|
||||
|
||||
```rust,ignore
|
||||
FnOnce(&mut Resource<_>) -> ()
|
||||
```
|
||||
|
||||
*Configration function* can set name and register specific routes.
|
||||
If resource does not contain any route or does not have any matching routes it
|
||||
returns *NOT FOUND* http resources.
|
||||
|
||||
## Configuring a Route
|
||||
|
||||
Resource contains set of routes. Each route in turn has set of predicates and handler.
|
||||
New route could be crearted with `Resource::route()` method which returns reference
|
||||
to new *Route* instance. By default *route* does not contain any predicates, so matches
|
||||
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
|
||||
the order that the routes were registered via `Resource::route()`. *Route* can contain
|
||||
any number of *predicates* but only one handler.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
# use actix_web::*;
|
||||
# use actix_web::httpcodes::*;
|
||||
|
||||
fn main() {
|
||||
Application::default("/app")
|
||||
.handler("/prefix", |req| index)
|
||||
Application::new()
|
||||
.resource("/path", |resource|
|
||||
resource.route()
|
||||
.p(pred::Get())
|
||||
.p(pred::Header("content-type", "text/plain"))
|
||||
.f(|req| HTTPOk)
|
||||
)
|
||||
.finish();
|
||||
}
|
||||
```
|
||||
|
||||
In this example `index` get called for any url which starts with`/app/prefix`.
|
||||
In this example `index` get called for *GET* request,
|
||||
if request contains `Content-Type` header and value of this header is *text/plain*
|
||||
and path equals to `/test`. Resource calls handle of the first matches route.
|
||||
If resource can not match any route "NOT FOUND" response get returned.
|
||||
|
||||
Resource contains set of route for same endpoint. Route corresponds to handling
|
||||
*HTTP method* by calling *web handler*. Resource select route based on *http method*,
|
||||
if no route could be matched default response `HTTPMethodNotAllowed` get resturned.
|
||||
[*Resource::route()*](../actix_web/struct.Resource.html#method.route) method returns
|
||||
[*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,
|
||||
any number of predicates could be registered for each route.
|
||||
|
||||
* [*Route::f()*](../actix_web/struct.Route.html#method.f) method registers handler function
|
||||
for this route. Only one handler could be registered. Usually handler registeration
|
||||
is the last config operation. Handler fanction could be function or closure and has type
|
||||
`Fn(HttpRequest<S>) -> R + 'static`
|
||||
|
||||
* [*Route::h()*](../actix_web/struct.Route.html#method.h) method registers handler object
|
||||
that implements `Handler` trait. This is similar to `f()` method, only one handler could
|
||||
be registered. Handler registeration is the last config operation.
|
||||
|
||||
* [*Route::a()*](../actix_web/struct.Route.html#method.a) method registers asynchandler
|
||||
function for this route. Only one handler could be registered. Handler registeration
|
||||
is the last config operation. Handler fanction could be function or closure and has type
|
||||
`Fn(HttpRequest<S>) -> Future<Item = HttpResponse, Error = Error> + 'static`
|
||||
|
||||
## Route matching
|
||||
|
||||
The main purpose of route configuration is to match (or not match) the request's `path`
|
||||
against a URL path pattern. `path` represents the path portion of the URL that was requested.
|
||||
|
||||
The way that *actix* does this is very simple. When a request enters the system,
|
||||
for each resource configuration registration present in the system, actix checks
|
||||
the request's path against the pattern declared. *Regex* crate and it's
|
||||
[*RegexSet*](https://doc.rust-lang.org/regex/regex/struct.RegexSet.html) is beeing used for
|
||||
pattern matching. If resource could not be found, *default resource* get used as matched
|
||||
resource.
|
||||
|
||||
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
|
||||
be used for a given request during a check. If any predicate in the set of route predicate
|
||||
arguments provided to a route configuration returns `false` during a check, that route is
|
||||
skipped and route matching continues through the ordered set of routes.
|
||||
|
||||
If any route matches, the route matching process stops and the handler associated with
|
||||
route get invoked.
|
||||
|
||||
If no route matches after all route patterns are exhausted, *NOT FOUND* response get returned.
|
||||
|
||||
## Resource pattern syntax
|
||||
|
||||
The syntax of the pattern matching language used by the actix in the pattern
|
||||
argument is straightforward.
|
||||
|
||||
The pattern used in route configuration may start with a slash character. If the pattern
|
||||
does not start with a slash character, an implicit slash will be prepended
|
||||
to it at matching time. For example, the following patterns are equivalent:
|
||||
|
||||
```
|
||||
{foo}/bar/baz
|
||||
```
|
||||
|
||||
and:
|
||||
|
||||
```
|
||||
/{foo}/bar/baz
|
||||
```
|
||||
|
||||
A *variable part* (replacement marker) is specified in the form *{identifier}*,
|
||||
where this means "accept any characters up to the next slash character and use this
|
||||
as the name in the `HttpRequest.match_info()` object".
|
||||
|
||||
A replacement marker in a pattern matches the regular expression `[^{}/]+`.
|
||||
|
||||
A match_info is the `Params` object representing the dynamic parts extracted from a
|
||||
*URL* based on the routing pattern. It is available as *request.match_info*. For example, the
|
||||
following pattern defines one literal segment (foo) and two replacement markers (baz, and bar):
|
||||
|
||||
```
|
||||
foo/{baz}/{bar}
|
||||
```
|
||||
|
||||
The above pattern will match these URLs, generating the following match information:
|
||||
|
||||
```
|
||||
foo/1/2 -> Params {'baz':'1', 'bar':'2'}
|
||||
foo/abc/def -> Params {'baz':'abc', 'bar':'def'}
|
||||
```
|
||||
|
||||
It will not match the following patterns however:
|
||||
|
||||
```
|
||||
foo/1/2/ -> No match (trailing slash)
|
||||
bar/abc/def -> First segment literal mismatch
|
||||
```
|
||||
|
||||
The match for a segment replacement marker in a segment will be done only up to
|
||||
the first non-alphanumeric character in the segment in the pattern. So, for instance,
|
||||
if this route pattern was used:
|
||||
|
||||
```
|
||||
foo/{name}.html
|
||||
```
|
||||
|
||||
The literal path */foo/biz.html* will match the above route pattern, and the match result
|
||||
will be `Params{'name': 'biz'}`. However, the literal path */foo/biz* will not match,
|
||||
because it does not contain a literal *.html* at the end of the segment represented
|
||||
by *{name}.html* (it only contains biz, not biz.html).
|
||||
|
||||
To capture both segments, two replacement markers can be used:
|
||||
|
||||
```
|
||||
foo/{name}.{ext}
|
||||
```
|
||||
|
||||
The literal path */foo/biz.html* will match the above route pattern, and the match
|
||||
result will be *Params{'name': 'biz', 'ext': 'html'}*. This occurs because there is a
|
||||
literal part of *.* (period) between the two replacement markers *{name}* and *{ext}*.
|
||||
|
||||
Replacement markers can optionally specify a regular expression which will be used to decide
|
||||
whether a path segment should match the marker. To specify that a replacement marker should
|
||||
match only a specific set of characters as defined by a regular expression, you must use a
|
||||
slightly extended form of replacement marker syntax. Within braces, the replacement marker
|
||||
name must be followed by a colon, then directly thereafter, the regular expression. The default
|
||||
regular expression associated with a replacement marker *[^/]+* matches one or more characters
|
||||
which are not a slash. For example, under the hood, the replacement marker *{foo}* can more
|
||||
verbosely be spelled as *{foo:[^/]+}*. You can change this to be an arbitrary regular expression
|
||||
to match an arbitrary sequence of characters, such as *{foo:\d+}* to match only digits.
|
||||
|
||||
Segments must contain at least one character in order to match a segment replacement marker.
|
||||
For example, for the URL */abc/*:
|
||||
|
||||
* */abc/{foo}* will not match.
|
||||
* */{foo}/* will match.
|
||||
|
||||
Note that path will be URL-unquoted and decoded into valid unicode string before
|
||||
matching pattern and values representing matched path segments will be URL-unquoted too.
|
||||
So for instance, the following pattern:
|
||||
|
||||
```
|
||||
foo/{bar}
|
||||
```
|
||||
|
||||
When matching the following URL:
|
||||
|
||||
```
|
||||
http://example.com/foo/La%20Pe%C3%B1a
|
||||
```
|
||||
|
||||
The matchdict will look like so (the value is URL-decoded):
|
||||
|
||||
```
|
||||
Params{'bar': 'La Pe\xf1a'}
|
||||
```
|
||||
|
||||
Literal strings in the path segment should represent the decoded value of the
|
||||
path provided to actix. You don't want to use a URL-encoded value in the pattern.
|
||||
For example, rather than this:
|
||||
|
||||
```
|
||||
/Foo%20Bar/{baz}
|
||||
```
|
||||
|
||||
You'll want to use something like this:
|
||||
|
||||
```
|
||||
/Foo Bar/{baz}
|
||||
```
|
||||
|
||||
It is possible to get "tail match". For this purpose custom regex has to be used.
|
||||
|
||||
```
|
||||
foo/{bar}/{tail:.*}
|
||||
```
|
||||
|
||||
The above pattern will match these URLs, generating the following match information:
|
||||
|
||||
```
|
||||
foo/1/2/ -> Params{'bar':'1', 'tail': '2/'}
|
||||
foo/abc/def/a/b/c -> Params{'bar':u'abc', 'tail': 'def/a/b/c'}
|
||||
```
|
||||
|
||||
## Match information
|
||||
|
||||
All values representing matched path segments are available in
|
||||
[`HttpRequest::match_info`](../actix_web/struct.HttpRequest.html#method.match_info).
|
||||
Specific value can be received with
|
||||
[`Params::get()`](../actix_web/dev/struct.Params.html#method.get) method.
|
||||
|
||||
Any matched parameter can be deserialized into specific type if this type
|
||||
implements `FromParam` trait. For example most of standard integer types
|
||||
implements `FromParam` trait. i.e.:
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
use actix_web::*;
|
||||
|
||||
fn index(req: HttpRequest) -> Result<String> {
|
||||
let v1: u8 = req.match_info().query("v1")?;
|
||||
let v2: u8 = req.match_info().query("v2")?;
|
||||
Ok(format!("Values {} {}", v1, v2))
|
||||
}
|
||||
|
||||
```rust,ignore
|
||||
fn main() {
|
||||
Application::default("/")
|
||||
.resource("/prefix", |r| {
|
||||
r.get(HTTPOk)
|
||||
r.post(HTTPForbidden)
|
||||
Application::new()
|
||||
.resource(r"/a/{v1}/{v2}/", |r| r.f(index))
|
||||
.finish();
|
||||
}
|
||||
```
|
||||
|
||||
For this example for path '/a/1/2/', values v1 and v2 will resolve to "1" and "2".
|
||||
|
||||
It is possible to create a `PathBuf` from a tail path parameter. The returned `PathBuf` is
|
||||
percent-decoded. If a segment is equal to "..", the previous segment (if
|
||||
any) is skipped.
|
||||
|
||||
For security purposes, if a segment meets any of the following conditions,
|
||||
an `Err` is returned indicating the condition met:
|
||||
|
||||
* Decoded segment starts with any of: `.` (except `..`), `*`
|
||||
* Decoded segment ends with any of: `:`, `>`, `<`
|
||||
* Decoded segment contains any of: `/`
|
||||
* On Windows, decoded segment contains any of: '\'
|
||||
* Percent-encoding results in invalid UTF8.
|
||||
|
||||
As a result of these conditions, a `PathBuf` parsed from request path parameter is
|
||||
safe to interpolate within, or use as a suffix of, a path without additional checks.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
use actix_web::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn index(req: HttpRequest) -> Result<String> {
|
||||
let path: PathBuf = req.match_info().query("tail")?;
|
||||
Ok(format!("Path {:?}", path))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new()
|
||||
.resource(r"/a/{tail:.*}", |r| r.method(Method::GET).f(index))
|
||||
.finish();
|
||||
}
|
||||
```
|
||||
|
||||
List of `FromParam` implementation could be found in
|
||||
[api docs](../actix_web/dev/trait.FromParam.html#foreign-impls)
|
||||
|
||||
## Generating resource URLs
|
||||
|
||||
Use the [HttpRequest.url_for()](../actix_web/struct.HttpRequest.html#method.url_for)
|
||||
method to generate URLs based on resource patterns. For example, if you've configured a
|
||||
resource with the name "foo" and the pattern "{a}/{b}/{c}", you might do this.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
# use actix_web::*;
|
||||
# use actix_web::httpcodes::*;
|
||||
#
|
||||
fn index(req: HttpRequest) -> HttpResponse {
|
||||
let url = req.url_for("foo", &["1", "2", "3"]); // <- generate url for "foo" resource
|
||||
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);
|
||||
})
|
||||
.finish();
|
||||
}
|
||||
```
|
||||
|
||||
[`ApplicationBuilder::resource()` method](../actix_web/dev/struct.ApplicationBuilder.html#method.resource)
|
||||
accepts configuration function, resource could be configured at once.
|
||||
Check [`Resource`](../actix-web/target/doc/actix_web/struct.Resource.html) documentation
|
||||
for more information.
|
||||
This would return something like the string *http://example.com/test/1/2/3* (at least if
|
||||
the current protocol and hostname implied http://example.com).
|
||||
`url_for()` method return [*Url object*](https://docs.rs/url/1.6.0/url/struct.Url.html) so you
|
||||
can modify this url (add query parameters, anchor, etc).
|
||||
`url_for()` could be called only for *named* resources otherwise error get returned.
|
||||
|
||||
## Variable resources
|
||||
|
||||
Resource may have *variable path*also. For instance, a resource with the
|
||||
path '/a/{name}/c' would match all incoming requests with paths such
|
||||
as '/a/b/c', '/a/1/c', and '/a/etc/c'.
|
||||
|
||||
A *variable part*is specified in the form {identifier}, where the identifier can be
|
||||
used later in a request handler to access the matched value for that part. This is
|
||||
done by looking up the identifier in the `HttpRequest.match_info` object:
|
||||
## External resources
|
||||
|
||||
Resources that are valid URLs, could be registered as external resources. They are useful
|
||||
for URL generation purposes only and are never considered for matching at request time.
|
||||
|
||||
```rust
|
||||
extern crate actix;
|
||||
# extern crate actix_web;
|
||||
use actix_web::*;
|
||||
|
||||
fn index(req: Httprequest) -> String {
|
||||
format!("Hello, {}", req.match_info.get('name').unwrap())
|
||||
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())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::default("/")
|
||||
.resource("/{name}", |r| r.get(index))
|
||||
let app = Application::new()
|
||||
.resource("/index.html", |r| r.f(index))
|
||||
.external_resource("youtube", "https://youtube.com/watch/{video_id}")
|
||||
.finish();
|
||||
}
|
||||
```
|
||||
|
||||
By default, each part matches the regular expression `[^{}/]+`.
|
||||
## Path normalization and redirecting to slash-appended routes
|
||||
|
||||
You can also specify a custom regex in the form `{identifier:regex}`:
|
||||
By normalizing it means:
|
||||
|
||||
- Add a trailing slash to the path.
|
||||
- Double slashes are replaced by one.
|
||||
|
||||
The handler returns as soon as it finds a path that resolves
|
||||
correctly. The order if all enable is 1) merge, 3) both merge and append
|
||||
and 3) append. If the path resolves with
|
||||
at least one of those conditions, it will redirect to the new path.
|
||||
|
||||
If *append* is *true* append slash when needed. If a resource is
|
||||
defined with trailing slash and the request comes without it, it will
|
||||
append it automatically.
|
||||
|
||||
If *merge* is *true*, merge multiple consecutive slashes in the path into one.
|
||||
|
||||
This handler designed to be use as a handler for application's *default resource*.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
# #[macro_use] extern crate serde_derive;
|
||||
# use actix_web::*;
|
||||
#
|
||||
# fn index(req: HttpRequest) -> httpcodes::StaticResponse {
|
||||
# httpcodes::HTTPOk
|
||||
# }
|
||||
fn main() {
|
||||
let app = Application::new()
|
||||
.resource("/resource/", |r| r.f(index))
|
||||
.default_resource(|r| r.h(NormalizePath::default()))
|
||||
.finish();
|
||||
}
|
||||
```
|
||||
|
||||
In this example `/resource`, `//resource///` will be redirected to `/resource/` url.
|
||||
|
||||
In this example path normalization handler get registered for all method,
|
||||
but you should not rely on this mechanism to redirect *POST* requests. The redirect of the
|
||||
slash-appending *Not Found* will turn a *POST* request into a GET, losing any
|
||||
*POST* data in the original request.
|
||||
|
||||
It is possible to register path normalization only for *GET* requests only
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
# #[macro_use] extern crate serde_derive;
|
||||
# use actix_web::*;
|
||||
#
|
||||
# fn index(req: HttpRequest) -> httpcodes::StaticResponse {
|
||||
# httpcodes::HTTPOk
|
||||
# }
|
||||
fn main() {
|
||||
let app = Application::new()
|
||||
.resource("/resource/", |r| r.f(index))
|
||||
.default_resource(|r| r.method(Method::GET).h(NormalizePath::default()))
|
||||
.finish();
|
||||
}
|
||||
```
|
||||
|
||||
## Using a Application Prefix to Compose Applications
|
||||
|
||||
The `Application::prefix()`" method allows to set specific application prefix.
|
||||
This prefix represents a resource prefix that will be prepended to all resource patterns added
|
||||
by the resource configuration. This can be used to help mount a set of routes at a different
|
||||
location than the included callable's author intended while still maintaining the same
|
||||
resource names.
|
||||
|
||||
For example:
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
# use actix_web::*;
|
||||
#
|
||||
fn show_users(req: HttpRequest) -> HttpResponse {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new()
|
||||
.prefix("/users")
|
||||
.resource("/show", |r| r.f(show_users))
|
||||
.finish();
|
||||
}
|
||||
```
|
||||
|
||||
In the above example, the *show_users* route will have an effective route pattern of
|
||||
*/users/show* instead of */show* because the application's prefix argument will be prepended
|
||||
to the pattern. The route will then only match if the URL path is */users/show*,
|
||||
and when the `HttpRequest.url_for()` function is called with the route name show_users,
|
||||
it will generate a URL with that same path.
|
||||
|
||||
## Custom route predicates
|
||||
|
||||
You can think of predicate as simple function that accept *request* object reference
|
||||
and returns *true* or *false*. Formally predicate is any object that implements
|
||||
[`Predicate`](../actix_web/pred/trait.Predicate.html) trait. Actix provides
|
||||
several predicates, you can check [functions section](../actix_web/pred/index.html#functions)
|
||||
of api docs.
|
||||
|
||||
Here is simple predicates that check that request contains specific *header*:
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
# extern crate http;
|
||||
# use actix_web::*;
|
||||
# use actix_web::httpcodes::*;
|
||||
use http::header::CONTENT_TYPE;
|
||||
use actix_web::pred::Predicate;
|
||||
|
||||
struct ContentTypeHeader;
|
||||
|
||||
impl<S: 'static> Predicate<S> for ContentTypeHeader {
|
||||
|
||||
fn check(&self, req: &mut HttpRequest<S>) -> bool {
|
||||
req.headers().contains_key(CONTENT_TYPE)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new()
|
||||
.resource("/index.html", |r|
|
||||
r.route()
|
||||
.p(ContentTypeHeader)
|
||||
.h(HTTPOk));
|
||||
}
|
||||
```
|
||||
|
||||
In this example *index* handler will be called only if request contains *CONTENT-TYPE* header.
|
||||
|
||||
Predicates can have access to application's state via `HttpRequest::state()` method.
|
||||
Also predicates can store extra information in
|
||||
[requests`s extensions](../actix_web/struct.HttpRequest.html#method.extensions).
|
||||
|
||||
### Modifing predicate values
|
||||
|
||||
You can invert the meaning of any predicate value by wrapping it in a `Not` predicate.
|
||||
For example if you want to return "METHOD NOT ALLOWED" response for all methods
|
||||
except "GET":
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
# extern crate http;
|
||||
# use actix_web::*;
|
||||
# use actix_web::httpcodes::*;
|
||||
use actix_web::pred;
|
||||
|
||||
fn main() {
|
||||
Application::new()
|
||||
.resource("/index.html", |r|
|
||||
r.route()
|
||||
.p(pred::Not(pred::Get()))
|
||||
.f(|req| HTTPMethodNotAllowed))
|
||||
.finish();
|
||||
}
|
||||
```
|
||||
|
||||
`Any` predicate accept list of predicates and matches if any of the supplied
|
||||
predicates match. i.e:
|
||||
|
||||
```rust,ignore
|
||||
fn main() {
|
||||
Application::default("/")
|
||||
.resource(r"{name:\d+}", |r| r.get(index))
|
||||
.finish();
|
||||
}
|
||||
pred::Any(pred::Get()).or(pred::Post())
|
||||
```
|
||||
|
||||
To match path tail, `{tail:*}` pattern could be used. Tail pattern has to be last
|
||||
segment in path otherwise it panics.
|
||||
`All` predicate accept list of predicates and matches if all of the supplied
|
||||
predicates match. i.e:
|
||||
|
||||
```rust,ignore
|
||||
fn main() {
|
||||
Application::default("/")
|
||||
.resource(r"/test/{tail:*}", |r| r.get(index))
|
||||
.finish();
|
||||
}
|
||||
pred::All(pred::Get()).and(pred::Header("content-type", "plain/text"))
|
||||
```
|
||||
|
||||
Above example would match all incoming requests with path such as
|
||||
'/test/b/c', '/test/index.html', and '/test/etc/test'.
|
||||
## Changing the default Not Found response
|
||||
|
||||
If path pattern can not be found in routing table or resource can not find matching
|
||||
route, default resource is used. Default response is *NOT FOUND* response.
|
||||
It is possible to override *NOT FOUND* response with `Application::default_resource()` method.
|
||||
This method accepts *configuration function* same as normal resource configuration
|
||||
with `Application::resource()` method.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
# extern crate http;
|
||||
use actix_web::*;
|
||||
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);
|
||||
})
|
||||
# .finish();
|
||||
}
|
||||
```
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
# Application state
|
||||
|
||||
Application state is shared with all routes within same application.
|
||||
State could be accessed with `HttpRequest::state()` method. It is read-only
|
||||
but interior mutability pattern with `RefCell` could be used to archive state mutability.
|
||||
State could be accessed with `HttpRequest::state()` method or
|
||||
`HttpContext::state()` in case of http actor.
|
||||
|
||||
Let's write simple application that uses shared state. We are going to store requests count
|
||||
in the state:
|
||||
|
||||
```rust
|
||||
extern crate actix;
|
||||
extern crate actix_web;
|
||||
|
||||
use std::cell::Cell;
|
||||
use actix_web::*;
|
||||
|
||||
// This struct represents state
|
||||
struct AppState {
|
||||
counter: Cell<usize>,
|
||||
}
|
||||
|
||||
fn index(req: HttpRequest<AppState>) -> String {
|
||||
let count = req.state().counter.get() + 1; // <- get count
|
||||
req.state().counter.set(count); // <- store new count in state
|
||||
|
||||
format!("Request number: {}", count) // <- response with count
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::build("/", AppState{counter: Cell::new(0)})
|
||||
.resource("/", |r| r.handler(Method::GET, index))
|
||||
.finish();
|
||||
}
|
||||
```
|
|
@ -1 +1,297 @@
|
|||
# Request
|
||||
# Request & Response
|
||||
|
||||
## Response
|
||||
|
||||
Builder-like patter is used to construct an instance of `HttpResponse`.
|
||||
`HttpResponse` provides several method that returns `HttpResponseBuilder` instance,
|
||||
which is implements various convinience methods that helps build response.
|
||||
Check [documentation](../actix_web/dev/struct.HttpResponseBuilder.html)
|
||||
for type description. Methods `.body`, `.finish`, `.json` finalizes response creation and
|
||||
returns constructed *HttpResponse* instance. if this methods get called for the same
|
||||
builder instance multiple times, builder will panic.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
use actix_web::*;
|
||||
use actix_web::headers::ContentEncoding;
|
||||
|
||||
fn index(req: HttpRequest) -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.content_encoding(ContentEncoding::Br)
|
||||
.content_type("plain/text")
|
||||
.header("X-Hdr", "sample")
|
||||
.body("data").unwrap()
|
||||
}
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
## Content encoding
|
||||
|
||||
Actix automatically *compress*/*decompress* payload. Following codecs are supported:
|
||||
|
||||
* Brotli
|
||||
* Gzip
|
||||
* Deflate
|
||||
* Identity
|
||||
|
||||
If request headers contains `Content-Encoding` header, request payload get decompressed
|
||||
according to header value. Multiple codecs are not supported, i.e: `Content-Encoding: br, gzip`.
|
||||
|
||||
Response payload get compressed based on *content_encoding* parameter.
|
||||
By default `ContentEncoding::Auto` is used. If `ContentEncoding::Auto` is selected
|
||||
then compression depends on request's `Accept-Encoding` header.
|
||||
`ContentEncoding::Identity` could be used to disable compression.
|
||||
If other content encoding is selected the compression is enforced for this codec. For example,
|
||||
to enable `brotli` response's body compression use `ContentEncoding::Br`:
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
use actix_web::*;
|
||||
use actix_web::headers::ContentEncoding;
|
||||
|
||||
fn index(req: HttpRequest) -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.content_encoding(ContentEncoding::Br)
|
||||
.body("data").unwrap()
|
||||
}
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
|
||||
## JSON Request
|
||||
|
||||
There are two options of json body deserialization.
|
||||
|
||||
First option is to use *HttpResponse::json()* method. This method returns
|
||||
[*JsonBody*](../actix_web/dev/struct.JsonBody.html) object which resolves into
|
||||
deserialized value.
|
||||
|
||||
```rust
|
||||
# extern crate actix;
|
||||
# extern crate actix_web;
|
||||
# extern crate futures;
|
||||
# extern crate serde_json;
|
||||
# #[macro_use] extern crate serde_derive;
|
||||
# use actix_web::*;
|
||||
# use futures::Future;
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct MyObj {
|
||||
name: String,
|
||||
number: i32,
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
.responder()
|
||||
}
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
Or you can manually load payload into memory and ther deserialize it.
|
||||
Here is simple example. We will deserialize *MyObj* struct. We need to load request
|
||||
body first and then deserialize json into object.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
# extern crate futures;
|
||||
# use actix_web::*;
|
||||
# #[macro_use] extern crate serde_derive;
|
||||
extern crate serde_json;
|
||||
use futures::{Future, Stream};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct MyObj {name: String, number: i32}
|
||||
|
||||
fn index(mut 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()
|
||||
// `Future::from_err` acts like `?` in that it coerces the error type from
|
||||
// the future into the final error type
|
||||
.from_err()
|
||||
// `Future::and_then` can be used to merge an asynchronous workflow with a
|
||||
// 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
|
||||
})
|
||||
.responder()
|
||||
}
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
Complete example for both options is available in
|
||||
[examples directory](https://github.com/actix/actix-web/tree/master/examples/json/).
|
||||
|
||||
|
||||
## JSON Response
|
||||
|
||||
The `Json` type allows you to respond with well-formed JSON data: simply return a value of
|
||||
type Json<T> where T is the type of a structure to serialize into *JSON*. The
|
||||
type `T` must implement the `Serialize` trait from *serde*.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
#[macro_use] extern crate serde_derive;
|
||||
use actix_web::*;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MyObj {
|
||||
name: String,
|
||||
}
|
||||
|
||||
fn index(req: HttpRequest) -> Result<Json<MyObj>> {
|
||||
Ok(Json(MyObj{name: req.match_info().query("name")?}))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new()
|
||||
.resource(r"/a/{name}", |r| r.method(Method::GET).f(index))
|
||||
.finish();
|
||||
}
|
||||
```
|
||||
|
||||
## Chunked transfer encoding
|
||||
|
||||
Actix automatically decode *chunked* encoding. `HttpRequest::payload()` already contains
|
||||
decoded bytes stream. If request payload compressed with one of supported
|
||||
compression codecs (br, gzip, deflate) bytes stream get decompressed.
|
||||
|
||||
Chunked encoding on response could be enabled with `HttpResponseBuilder::chunked()` method.
|
||||
But this takes effect only for `Body::Streaming(BodyStream)` or `Body::StreamingContext` bodies.
|
||||
Also if response payload compression is enabled and streaming body is used, chunked encoding
|
||||
get enabled automatically.
|
||||
|
||||
Enabling chunked encoding for *HTTP/2.0* responses is forbidden.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
use actix_web::*;
|
||||
|
||||
fn index(req: HttpRequest) -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.chunked()
|
||||
.body(Body::Streaming(payload::Payload::empty().stream())).unwrap()
|
||||
}
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
## Multipart body
|
||||
|
||||
Actix provides multipart stream support.
|
||||
[*Multipart*](../actix_web/multipart/struct.Multipart.html) is implemented as
|
||||
a stream of multipart items, each item could be
|
||||
[*Field*](../actix_web/multipart/struct.Field.html) or nested *Multipart* stream.
|
||||
`HttpResponse::multipart()` method returns *Multipart* stream for current request.
|
||||
|
||||
In simple form multipart stream handling could be implemented similar to this example
|
||||
|
||||
```rust,ignore
|
||||
# extern crate actix_web;
|
||||
use actix_web::*;
|
||||
|
||||
fn index(req: HttpRequest) -> Box<Future<...>> {
|
||||
req.multipart() // <- get multipart stream for current request
|
||||
.and_then(|item| { // <- iterate over multipart items
|
||||
match item {
|
||||
// Handle multipart Field
|
||||
multipart::MultipartItem::Field(field) => {
|
||||
println!("==== FIELD ==== {:?} {:?}", field.heders(), field.content_type());
|
||||
|
||||
Either::A(
|
||||
// Field in turn is a stream of *Bytes* objects
|
||||
field.map(|chunk| {
|
||||
println!("-- CHUNK: \n{}",
|
||||
std::str::from_utf8(&chunk).unwrap());})
|
||||
.fold((), |_, _| result(Ok(()))))
|
||||
},
|
||||
multipart::MultipartItem::Nested(mp) => {
|
||||
// Or item could be nested Multipart stream
|
||||
Either::B(result(Ok(())))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Full example is available in
|
||||
[examples directory](https://github.com/actix/actix-web/tree/master/examples/multipart/).
|
||||
|
||||
## Urlencoded body
|
||||
|
||||
Actix provides support for *application/x-www-form-urlencoded* encoded body.
|
||||
`HttpResponse::urlencoded()` method returns
|
||||
[*UrlEncoded*](../actix_web/dev/struct.UrlEncoded.html) future, it resolves
|
||||
into `HashMap<String, String>` which contains decoded parameters.
|
||||
*UrlEncoded* future can resolve into a error in several cases:
|
||||
|
||||
* content type is not `application/x-www-form-urlencoded`
|
||||
* transfer encoding is `chunked`.
|
||||
* content-length is greater than 256k
|
||||
* payload terminates with error.
|
||||
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
# extern crate futures;
|
||||
use actix_web::*;
|
||||
use futures::future::{Future, ok};
|
||||
|
||||
fn index(mut req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
req.urlencoded() // <- get UrlEncoded future
|
||||
.from_err()
|
||||
.and_then(|params| { // <- url encoded parameters
|
||||
println!("==== BODY ==== {:?}", params);
|
||||
ok(httpcodes::HTTPOk.into())
|
||||
})
|
||||
.responder()
|
||||
}
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
|
||||
## Streaming request
|
||||
|
||||
Actix uses [*Payload*](../actix_web/payload/struct.Payload.html) object as request payload stream.
|
||||
*HttpRequest* provides several methods, which can be used for payload access.
|
||||
At the same time *Payload* implements *Stream* trait, so it could be used with various
|
||||
stream combinators. Also *Payload* provides serveral convinience methods that return
|
||||
future object that resolve to Bytes object.
|
||||
|
||||
* *readany()* method returns *Stream* of *Bytes* objects.
|
||||
|
||||
* *readexactly()* method returns *Future* that resolves when specified number of bytes
|
||||
get received.
|
||||
|
||||
* *readline()* method returns *Future* that resolves when `\n` get received.
|
||||
|
||||
* *readuntil()* method returns *Future* that resolves when specified bytes string
|
||||
matches in input bytes stream
|
||||
|
||||
In this example handle reads request payload chunk by chunk and prints every chunk.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
# extern crate futures;
|
||||
# use futures::future::result;
|
||||
use actix_web::*;
|
||||
use futures::{Future, Stream};
|
||||
|
||||
|
||||
fn index(mut req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
req.payload_mut()
|
||||
.readany()
|
||||
.from_err()
|
||||
.fold((), |_, chunk| {
|
||||
println!("Chunk: {:?}", chunk);
|
||||
result::<_, error::PayloadError>(Ok(()))
|
||||
})
|
||||
.map(|_| HttpResponse::Ok().finish().unwrap())
|
||||
.responder()
|
||||
}
|
||||
# fn main() {}
|
||||
```
|
||||
|
|
|
@ -1 +1,97 @@
|
|||
# Response
|
||||
# Testing
|
||||
|
||||
Every application should be well tested and. Actix provides the tools to perform unit and
|
||||
integration tests.
|
||||
|
||||
## Unit tests
|
||||
|
||||
For unit testing actix provides request builder type and simple handler runner.
|
||||
[*TestRequest*](../actix_web/test/struct.TestRequest.html) implements builder-like pattern.
|
||||
You can generate `HttpRequest` instance with `finish()` method or you can
|
||||
run your handler with `run()` or `run_async()` methods.
|
||||
|
||||
```rust
|
||||
# extern crate http;
|
||||
# extern crate actix_web;
|
||||
use http::{header, StatusCode};
|
||||
use actix_web::*;
|
||||
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()
|
||||
}
|
||||
}
|
||||
httpcodes::HTTPBadRequest.into()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let resp = TestRequest::with_header("content-type", "text/plain")
|
||||
.run(index)
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let resp = TestRequest::default()
|
||||
.run(index)
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Integration tests
|
||||
|
||||
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 simple form *TestServer* could be configured to use handler. *TestServer::new* method
|
||||
accepts configuration function, only argument for this function is *test application*
|
||||
instance. You can check [api documentation](../actix_web/test/struct.TestApp.html)
|
||||
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()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
Other option is to use application factory. In this case you need to pass factory function
|
||||
same as you use for real http server configuration.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
extern crate reqwest;
|
||||
use actix_web::*;
|
||||
use actix_web::test::TestServer;
|
||||
|
||||
fn index(req: HttpRequest) -> HttpResponse {
|
||||
httpcodes::HTTPOk.into()
|
||||
}
|
||||
|
||||
/// This function get called by http server.
|
||||
fn create_app() -> Application {
|
||||
Application::new()
|
||||
.resource("/test", |r| r.h(index))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
```
|
||||
|
|
|
@ -1 +1,48 @@
|
|||
# WebSockets
|
||||
|
||||
Actix supports WebSockets out-of-the-box. It is possible to convert request's `Payload`
|
||||
to a stream of [*ws::Message*](../actix_web/ws/enum.Message.html) with
|
||||
a [*ws::WsStream*](../actix_web/ws/struct.WsStream.html) and then use stream
|
||||
combinators to handle actual messages. But it is simplier to handle websocket communications
|
||||
with http actor.
|
||||
|
||||
```rust
|
||||
extern crate actix;
|
||||
extern crate actix_web;
|
||||
|
||||
use actix::*;
|
||||
use actix_web::*;
|
||||
|
||||
/// Define http actor
|
||||
struct Ws;
|
||||
|
||||
impl Actor for Ws {
|
||||
type Context = HttpContext<Self>;
|
||||
}
|
||||
|
||||
/// Define Handler for ws::Message message
|
||||
impl Handler<ws::Message> for Ws {
|
||||
type Result=();
|
||||
|
||||
fn handle(&mut self, msg: ws::Message, ctx: &mut HttpContext<Self>) {
|
||||
match msg {
|
||||
ws::Message::Ping(msg) => ws::WsWriter::pong(ctx, &msg),
|
||||
ws::Message::Text(text) => ws::WsWriter::text(ctx, &text),
|
||||
ws::Message::Binary(bin) => ws::WsWriter::binary(ctx, bin),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new()
|
||||
.resource("/ws/", |r| r.f(|req| ws::start(req, Ws))) // <- register websocket route
|
||||
.finish();
|
||||
}
|
||||
```
|
||||
|
||||
Simple websocket echo server example is available in
|
||||
[examples directory](https://github.com/actix/actix-web/blob/master/examples/websocket.rs).
|
||||
|
||||
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/)
|
||||
|
|
|
@ -1,113 +1,195 @@
|
|||
use std::mem;
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use futures::Future;
|
||||
|
||||
use error::Error;
|
||||
use route::{RouteHandler, Reply, Handler, WrapHandler, AsyncHandler};
|
||||
use handler::Reply;
|
||||
use router::{Router, Pattern};
|
||||
use resource::Resource;
|
||||
use recognizer::{RouteRecognizer, check_pattern};
|
||||
use handler::{Handler, RouteHandler, WrapHandler};
|
||||
use httprequest::HttpRequest;
|
||||
use httpresponse::HttpResponse;
|
||||
use channel::HttpHandler;
|
||||
use pipeline::Pipeline;
|
||||
use middlewares::Middleware;
|
||||
|
||||
use channel::{HttpHandler, IntoHttpHandler, HttpHandlerTask};
|
||||
use pipeline::{Pipeline, PipelineHandler};
|
||||
use middleware::Middleware;
|
||||
use server::ServerSettings;
|
||||
|
||||
/// Application
|
||||
pub struct Application<S> {
|
||||
pub struct HttpApplication<S=()> {
|
||||
state: Rc<S>,
|
||||
prefix: String,
|
||||
default: Resource<S>,
|
||||
handlers: HashMap<String, Box<RouteHandler<S>>>,
|
||||
router: RouteRecognizer<Resource<S>>,
|
||||
middlewares: Rc<Vec<Box<Middleware>>>,
|
||||
router: Router,
|
||||
inner: Rc<RefCell<Inner<S>>>,
|
||||
middlewares: Rc<Vec<Box<Middleware<S>>>>,
|
||||
}
|
||||
|
||||
impl<S: 'static> Application<S> {
|
||||
pub(crate) struct Inner<S> {
|
||||
prefix: usize,
|
||||
default: Resource<S>,
|
||||
router: Router,
|
||||
resources: Vec<Resource<S>>,
|
||||
handlers: Vec<(String, Box<RouteHandler<S>>)>,
|
||||
}
|
||||
|
||||
fn run(&self, req: HttpRequest) -> Reply {
|
||||
let mut req = req.with_state(Rc::clone(&self.state));
|
||||
impl<S: 'static> PipelineHandler<S> for Inner<S> {
|
||||
|
||||
if let Some((params, h)) = self.router.recognize(req.path()) {
|
||||
if let Some(params) = params {
|
||||
req.set_match_info(params);
|
||||
}
|
||||
h.handle(req)
|
||||
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))
|
||||
} else {
|
||||
for (prefix, handler) in &self.handlers {
|
||||
if req.path().starts_with(prefix) {
|
||||
req.set_prefix(prefix.len());
|
||||
for &mut (ref prefix, ref mut handler) in &mut self.handlers {
|
||||
let m = {
|
||||
let path = &req.path()[self.prefix..];
|
||||
path.starts_with(prefix) && (path.len() == prefix.len() ||
|
||||
path.split_at(prefix.len()).1.starts_with('/'))
|
||||
};
|
||||
if m {
|
||||
let path: &'static str = unsafe{
|
||||
mem::transmute(&req.path()[self.prefix+prefix.len()..])};
|
||||
if path.is_empty() {
|
||||
req.match_info_mut().add("tail", "");
|
||||
} else {
|
||||
req.match_info_mut().add("tail", path.split_at(1).1);
|
||||
}
|
||||
return handler.handle(req)
|
||||
}
|
||||
}
|
||||
self.default.handle(req)
|
||||
self.default.handle(req, None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: 'static> HttpHandler for Application<S> {
|
||||
#[cfg(test)]
|
||||
impl<S: 'static> HttpApplication<S> {
|
||||
pub(crate) fn run(&mut self, req: HttpRequest<S>) -> Reply {
|
||||
self.inner.borrow_mut().handle(req)
|
||||
}
|
||||
pub(crate) fn prepare_request(&self, req: HttpRequest) -> HttpRequest<S> {
|
||||
req.with_state(Rc::clone(&self.state), self.router.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn handle(&self, req: HttpRequest) -> Result<Pipeline, HttpRequest> {
|
||||
if req.path().starts_with(&self.prefix) {
|
||||
Ok(Pipeline::new(req, Rc::clone(&self.middlewares),
|
||||
&|req: HttpRequest| self.run(req)))
|
||||
impl<S: 'static> HttpHandler for HttpApplication<S> {
|
||||
|
||||
fn handle(&mut self, req: HttpRequest) -> Result<Box<HttpHandlerTask>, HttpRequest> {
|
||||
let m = {
|
||||
let path = req.path();
|
||||
path.starts_with(&self.prefix) && (
|
||||
path.len() == self.prefix.len() ||
|
||||
path.split_at(self.prefix.len()).1.starts_with('/'))
|
||||
};
|
||||
if m {
|
||||
let inner = Rc::clone(&self.inner);
|
||||
let req = req.with_state(Rc::clone(&self.state), self.router.clone());
|
||||
|
||||
Ok(Box::new(Pipeline::new(req, Rc::clone(&self.middlewares), inner)))
|
||||
} else {
|
||||
Err(req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ApplicationParts<S> {
|
||||
state: S,
|
||||
prefix: String,
|
||||
settings: ServerSettings,
|
||||
default: Resource<S>,
|
||||
resources: HashMap<Pattern, Option<Resource<S>>>,
|
||||
handlers: Vec<(String, Box<RouteHandler<S>>)>,
|
||||
external: HashMap<String, Pattern>,
|
||||
middlewares: Vec<Box<Middleware<S>>>,
|
||||
}
|
||||
|
||||
/// Structure that follows the builder pattern for building `Application` structs.
|
||||
pub struct Application<S=()> {
|
||||
parts: Option<ApplicationParts<S>>,
|
||||
}
|
||||
|
||||
impl Application<()> {
|
||||
|
||||
/// Create default `ApplicationBuilder` with no state
|
||||
pub fn default<T: Into<String>>(prefix: T) -> ApplicationBuilder<()> {
|
||||
ApplicationBuilder {
|
||||
parts: Some(ApplicationBuilderParts {
|
||||
/// Create application with empty state. Application can
|
||||
/// be configured with builder-like pattern.
|
||||
pub fn new() -> Application<()> {
|
||||
Application {
|
||||
parts: Some(ApplicationParts {
|
||||
state: (),
|
||||
prefix: prefix.into(),
|
||||
prefix: "/".to_owned(),
|
||||
settings: ServerSettings::default(),
|
||||
default: Resource::default_not_found(),
|
||||
handlers: HashMap::new(),
|
||||
resources: HashMap::new(),
|
||||
handlers: Vec::new(),
|
||||
external: HashMap::new(),
|
||||
middlewares: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Application<()> {
|
||||
fn default() -> Self {
|
||||
Application::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Application<S> where S: 'static {
|
||||
|
||||
/// Create application builder with specific state. State is shared with all
|
||||
/// routes within same application and could be
|
||||
/// accessed with `HttpContext::state()` method.
|
||||
pub fn build<T: Into<String>>(prefix: T, state: S) -> ApplicationBuilder<S> {
|
||||
ApplicationBuilder {
|
||||
parts: Some(ApplicationBuilderParts {
|
||||
/// Create application with specific state. Application can be
|
||||
/// configured with builder-like pattern.
|
||||
///
|
||||
/// State is shared with all reousrces within same application and could be
|
||||
/// accessed with `HttpRequest::state()` method.
|
||||
pub fn with_state(state: S) -> Application<S> {
|
||||
Application {
|
||||
parts: Some(ApplicationParts {
|
||||
state: state,
|
||||
prefix: prefix.into(),
|
||||
prefix: "/".to_owned(),
|
||||
settings: ServerSettings::default(),
|
||||
default: Resource::default_not_found(),
|
||||
handlers: HashMap::new(),
|
||||
resources: HashMap::new(),
|
||||
handlers: Vec::new(),
|
||||
external: HashMap::new(),
|
||||
middlewares: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ApplicationBuilderParts<S> {
|
||||
state: S,
|
||||
prefix: String,
|
||||
default: Resource<S>,
|
||||
handlers: HashMap<String, Box<RouteHandler<S>>>,
|
||||
resources: HashMap<String, Resource<S>>,
|
||||
middlewares: Vec<Box<Middleware>>,
|
||||
}
|
||||
|
||||
/// Structure that follows the builder pattern for building `Application` structs.
|
||||
pub struct ApplicationBuilder<S=()> {
|
||||
parts: Option<ApplicationBuilderParts<S>>,
|
||||
}
|
||||
|
||||
impl<S> ApplicationBuilder<S> where S: 'static {
|
||||
/// Set application prefix
|
||||
///
|
||||
/// Only requests that matches application's prefix get processed by this application.
|
||||
/// Application prefix always contains laading "/" slash. If supplied prefix
|
||||
/// does not contain leading slash, it get inserted. Prefix should
|
||||
/// consists valid path segments. i.e for application with
|
||||
/// prefix `/app` any request with following paths `/app`, `/app/` or `/app/test`
|
||||
/// would match, but path `/application` would not match.
|
||||
///
|
||||
/// In the following example only requests with "/app/" path prefix
|
||||
/// get handled. Request with path "/app/test/" would be handled,
|
||||
/// but request with path "/application" or "/other/..." would return *NOT FOUND*
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// use actix_web::*;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let app = Application::new()
|
||||
/// .prefix("/app")
|
||||
/// .resource("/test", |r| {
|
||||
/// r.method(Method::GET).f(|_| httpcodes::HTTPOk);
|
||||
/// r.method(Method::HEAD).f(|_| httpcodes::HTTPMethodNotAllowed);
|
||||
/// })
|
||||
/// .finish();
|
||||
/// }
|
||||
/// ```
|
||||
pub fn prefix<P: Into<String>>(mut self, prefix: P) -> Application<S> {
|
||||
{
|
||||
let parts = self.parts.as_mut().expect("Use after finish");
|
||||
let mut prefix = prefix.into();
|
||||
if !prefix.starts_with('/') {
|
||||
prefix.insert(0, '/')
|
||||
}
|
||||
parts.prefix = prefix;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure resource for specific path.
|
||||
///
|
||||
|
@ -118,7 +200,7 @@ impl<S> ApplicationBuilder<S> where S: 'static {
|
|||
/// A variable part is specified in the form `{identifier}`, where
|
||||
/// the identifier can be used later in a request handler to access the matched
|
||||
/// value for that part. This is done by looking up the identifier
|
||||
/// in the `Params` object returned by `Request.match_info()` method.
|
||||
/// in the `Params` object returned by `HttpRequest.match_info()` method.
|
||||
///
|
||||
/// By default, each part matches the regular expression `[^{}/]+`.
|
||||
///
|
||||
|
@ -128,37 +210,39 @@ impl<S> ApplicationBuilder<S> where S: 'static {
|
|||
/// store userid and friend in the exposed Params object:
|
||||
///
|
||||
/// ```rust
|
||||
/// extern crate actix_web;
|
||||
/// # extern crate actix_web;
|
||||
/// use actix_web::*;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let app = Application::default("/")
|
||||
/// let app = Application::new()
|
||||
/// .resource("/test", |r| {
|
||||
/// r.get(|req| httpcodes::HTTPOk);
|
||||
/// r.handler(Method::HEAD, |req| httpcodes::HTTPMethodNotAllowed);
|
||||
/// })
|
||||
/// .finish();
|
||||
/// r.method(Method::GET).f(|_| httpcodes::HTTPOk);
|
||||
/// r.method(Method::HEAD).f(|_| httpcodes::HTTPMethodNotAllowed);
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
pub fn resource<F, P: Into<String>>(&mut self, path: P, f: F) -> &mut Self
|
||||
pub fn resource<F>(mut self, path: &str, f: F) -> Application<S>
|
||||
where F: FnOnce(&mut Resource<S>) + 'static
|
||||
{
|
||||
{
|
||||
let parts = self.parts.as_mut().expect("Use after finish");
|
||||
|
||||
// add resource
|
||||
let path = path.into();
|
||||
if !parts.resources.contains_key(&path) {
|
||||
check_pattern(&path);
|
||||
parts.resources.insert(path.clone(), Resource::default());
|
||||
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);
|
||||
}
|
||||
f(parts.resources.get_mut(&path).unwrap());
|
||||
|
||||
parts.resources.insert(pattern, Some(resource));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Default resource is used if no match route could be found.
|
||||
pub fn default_resource<F>(&mut self, f: F) -> &mut Self
|
||||
/// Default resource is used if no matched route could be found.
|
||||
pub fn default_resource<F>(mut self, f: F) -> Application<S>
|
||||
where F: FnOnce(&mut Resource<S>) + 'static
|
||||
{
|
||||
{
|
||||
|
@ -168,105 +252,142 @@ impl<S> ApplicationBuilder<S> where S: 'static {
|
|||
self
|
||||
}
|
||||
|
||||
/// This method register handler for specified path prefix.
|
||||
/// Any path that starts with this prefix matches handler.
|
||||
/// Register external resource.
|
||||
///
|
||||
/// External resources are useful for URL generation purposes only and
|
||||
/// are never considered for matching at request time.
|
||||
/// Call to `HttpRequest::url_for()` will work as expected.
|
||||
///
|
||||
/// ```rust
|
||||
/// extern crate actix_web;
|
||||
/// # extern crate actix_web;
|
||||
/// 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())
|
||||
/// }
|
||||
///
|
||||
/// fn main() {
|
||||
/// let app = Application::default("/")
|
||||
/// .handler("/test", |req| {
|
||||
/// match *req.method() {
|
||||
/// Method::GET => httpcodes::HTTPOk,
|
||||
/// Method::POST => httpcodes::HTTPMethodNotAllowed,
|
||||
/// _ => httpcodes::HTTPNotFound,
|
||||
/// }
|
||||
/// })
|
||||
/// let app = Application::new()
|
||||
/// .resource("/index.html", |r| r.f(index))
|
||||
/// .external_resource("youtube", "https://youtube.com/watch/{video_id}")
|
||||
/// .finish();
|
||||
/// }
|
||||
/// ```
|
||||
pub fn handler<P, F, R>(&mut self, path: P, handler: F) -> &mut Self
|
||||
where P: Into<String>,
|
||||
F: Fn(HttpRequest<S>) -> R + 'static,
|
||||
R: Into<Reply> + 'static
|
||||
pub fn external_resource<T, U>(mut self, name: T, url: U) -> Application<S>
|
||||
where T: AsRef<str>, U: AsRef<str>
|
||||
{
|
||||
self.parts.as_mut().expect("Use after finish")
|
||||
.handlers.insert(path.into(), Box::new(WrapHandler::new(handler)));
|
||||
{
|
||||
let parts = self.parts.as_mut().expect("Use after finish");
|
||||
|
||||
if parts.external.contains_key(name.as_ref()) {
|
||||
panic!("External resource {:?} is registered.", name.as_ref());
|
||||
}
|
||||
parts.external.insert(
|
||||
String::from(name.as_ref()), Pattern::new(name.as_ref(), url.as_ref(), "^/"));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// This method register handler for specified path prefix.
|
||||
/// Any path that starts with this prefix matches handler.
|
||||
pub fn route<P, H>(&mut self, path: P, handler: H) -> &mut Self
|
||||
where P: Into<String>, H: Handler<S>
|
||||
/// Configure handler for specific path prefix.
|
||||
///
|
||||
/// Path prefix consists valid path segments. i.e for prefix `/app`
|
||||
/// any request with following paths `/app`, `/app/` or `/app/test`
|
||||
/// would match, but path `/application` would not match.
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// use actix_web::*;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let app = Application::new()
|
||||
/// .handler("/app", |req: HttpRequest| {
|
||||
/// match *req.method() {
|
||||
/// Method::GET => httpcodes::HTTPOk,
|
||||
/// Method::POST => httpcodes::HTTPMethodNotAllowed,
|
||||
/// _ => httpcodes::HTTPNotFound,
|
||||
/// }});
|
||||
/// }
|
||||
/// ```
|
||||
pub fn handler<H: Handler<S>>(mut self, path: &str, handler: H) -> Application<S>
|
||||
{
|
||||
self.parts.as_mut().expect("Use after finish")
|
||||
.handlers.insert(path.into(), Box::new(WrapHandler::new(handler)));
|
||||
{
|
||||
let path = path.trim().trim_right_matches('/').to_owned();
|
||||
let parts = self.parts.as_mut().expect("Use after finish");
|
||||
parts.handlers.push((path, Box::new(WrapHandler::new(handler))));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// This method register async handler for specified path prefix.
|
||||
/// Any path that starts with this prefix matches handler.
|
||||
pub fn async<P, F, R>(&mut self, path: P, handler: F) -> &mut Self
|
||||
where F: Fn(HttpRequest<S>) -> R + 'static,
|
||||
R: Future<Item=HttpResponse, Error=Error> + 'static,
|
||||
P: Into<String>,
|
||||
{
|
||||
self.parts.as_mut().expect("Use after finish")
|
||||
.handlers.insert(path.into(), Box::new(AsyncHandler::new(handler)));
|
||||
self
|
||||
}
|
||||
|
||||
/// Construct application
|
||||
pub fn middleware<T>(&mut self, mw: T) -> &mut Self
|
||||
where T: Middleware + 'static
|
||||
/// Register a middleware
|
||||
pub fn middleware<T>(mut self, mw: T) -> Application<S>
|
||||
where T: Middleware<S> + 'static
|
||||
{
|
||||
self.parts.as_mut().expect("Use after finish")
|
||||
.middlewares.push(Box::new(mw));
|
||||
self
|
||||
}
|
||||
|
||||
/// Construct application
|
||||
pub fn finish(&mut self) -> Application<S> {
|
||||
/// Finish application configuration and create HttpHandler object
|
||||
pub fn finish(&mut self) -> HttpApplication<S> {
|
||||
let parts = self.parts.take().expect("Use after finish");
|
||||
let prefix = parts.prefix.trim().trim_right_matches('/');
|
||||
|
||||
let mut handlers = HashMap::new();
|
||||
let prefix = if parts.prefix.ends_with('/') {
|
||||
parts.prefix
|
||||
} else {
|
||||
parts.prefix + "/"
|
||||
};
|
||||
|
||||
let mut routes = Vec::new();
|
||||
for (path, handler) in parts.resources {
|
||||
routes.push((path, handler))
|
||||
let mut resources = parts.resources;
|
||||
for (_, pattern) in parts.external {
|
||||
resources.insert(pattern, None);
|
||||
}
|
||||
|
||||
for (path, mut handler) in parts.handlers {
|
||||
let path = prefix.clone() + path.trim_left_matches('/');
|
||||
handlers.insert(path, handler);
|
||||
}
|
||||
Application {
|
||||
let (router, resources) = Router::new(prefix, parts.settings, resources);
|
||||
|
||||
let inner = Rc::new(RefCell::new(
|
||||
Inner {
|
||||
prefix: prefix.len(),
|
||||
default: parts.default,
|
||||
router: router.clone(),
|
||||
resources: resources,
|
||||
handlers: parts.handlers,
|
||||
}
|
||||
));
|
||||
|
||||
HttpApplication {
|
||||
state: Rc::new(parts.state),
|
||||
prefix: prefix.clone(),
|
||||
default: parts.default,
|
||||
handlers: handlers,
|
||||
router: RouteRecognizer::new(prefix, routes),
|
||||
prefix: prefix.to_owned(),
|
||||
inner: inner,
|
||||
router: router.clone(),
|
||||
middlewares: Rc::new(parts.middlewares),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: 'static> From<ApplicationBuilder<S>> for Application<S> {
|
||||
fn from(mut builder: ApplicationBuilder<S>) -> Application<S> {
|
||||
builder.finish()
|
||||
impl<S: 'static> IntoHttpHandler for Application<S> {
|
||||
type Handler = HttpApplication<S>;
|
||||
|
||||
fn into_handler(mut self, settings: ServerSettings) -> HttpApplication<S> {
|
||||
{
|
||||
let parts = self.parts.as_mut().expect("Use after finish");
|
||||
parts.settings = settings;
|
||||
}
|
||||
self.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: 'static> Iterator for ApplicationBuilder<S> {
|
||||
type Item = Application<S>;
|
||||
impl<'a, S: 'static> IntoHttpHandler for &'a mut Application<S> {
|
||||
type Handler = HttpApplication<S>;
|
||||
|
||||
fn into_handler(self, settings: ServerSettings) -> HttpApplication<S> {
|
||||
{
|
||||
let parts = self.parts.as_mut().expect("Use after finish");
|
||||
parts.settings = settings;
|
||||
}
|
||||
self.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
impl<S: 'static> Iterator for Application<S> {
|
||||
type Item = HttpApplication<S>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.parts.is_some() {
|
||||
|
@ -276,3 +397,138 @@ impl<S: 'static> Iterator for ApplicationBuilder<S> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use http::StatusCode;
|
||||
use super::*;
|
||||
use test::TestRequest;
|
||||
use httprequest::HttpRequest;
|
||||
use httpcodes;
|
||||
|
||||
#[test]
|
||||
fn test_default_resource() {
|
||||
let mut app = Application::new()
|
||||
.resource("/test", |r| r.h(httpcodes::HTTPOk))
|
||||
.finish();
|
||||
|
||||
let req = TestRequest::with_uri("/test").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_response().unwrap().status(), StatusCode::OK);
|
||||
|
||||
let req = TestRequest::with_uri("/blah").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_response().unwrap().status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let mut app = Application::new()
|
||||
.default_resource(|r| r.h(httpcodes::HTTPMethodNotAllowed))
|
||||
.finish();
|
||||
let req = TestRequest::with_uri("/blah").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_response().unwrap().status(), StatusCode::METHOD_NOT_ALLOWED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unhandled_prefix() {
|
||||
let mut app = Application::new()
|
||||
.prefix("/test")
|
||||
.resource("/test", |r| r.h(httpcodes::HTTPOk))
|
||||
.finish();
|
||||
assert!(app.handle(HttpRequest::default()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_state() {
|
||||
let mut app = Application::with_state(10)
|
||||
.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);
|
||||
assert_eq!(resp.as_response().unwrap().status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prefix() {
|
||||
let mut app = Application::new()
|
||||
.prefix("/test")
|
||||
.resource("/blah", |r| r.h(httpcodes::HTTPOk))
|
||||
.finish();
|
||||
let req = TestRequest::with_uri("/test").finish();
|
||||
let resp = app.handle(req);
|
||||
assert!(resp.is_ok());
|
||||
|
||||
let req = TestRequest::with_uri("/test/").finish();
|
||||
let resp = app.handle(req);
|
||||
assert!(resp.is_ok());
|
||||
|
||||
let req = TestRequest::with_uri("/test/blah").finish();
|
||||
let resp = app.handle(req);
|
||||
assert!(resp.is_ok());
|
||||
|
||||
let req = TestRequest::with_uri("/testing").finish();
|
||||
let resp = app.handle(req);
|
||||
assert!(resp.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handler() {
|
||||
let mut app = Application::new()
|
||||
.handler("/test", httpcodes::HTTPOk)
|
||||
.finish();
|
||||
|
||||
let req = TestRequest::with_uri("/test").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_response().unwrap().status(), StatusCode::OK);
|
||||
|
||||
let req = TestRequest::with_uri("/test/").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_response().unwrap().status(), StatusCode::OK);
|
||||
|
||||
let req = TestRequest::with_uri("/test/app").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_response().unwrap().status(), StatusCode::OK);
|
||||
|
||||
let req = TestRequest::with_uri("/testapp").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_response().unwrap().status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let req = TestRequest::with_uri("/blah").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_response().unwrap().status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handler_prefix() {
|
||||
let mut app = Application::new()
|
||||
.prefix("/app")
|
||||
.handler("/test", httpcodes::HTTPOk)
|
||||
.finish();
|
||||
|
||||
let req = TestRequest::with_uri("/test").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_response().unwrap().status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let req = TestRequest::with_uri("/app/test").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_response().unwrap().status(), StatusCode::OK);
|
||||
|
||||
let req = TestRequest::with_uri("/app/test/").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_response().unwrap().status(), StatusCode::OK);
|
||||
|
||||
let req = TestRequest::with_uri("/app/test/app").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_response().unwrap().status(), StatusCode::OK);
|
||||
|
||||
let req = TestRequest::with_uri("/app/testapp").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_response().unwrap().status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let req = TestRequest::with_uri("/app/blah").finish();
|
||||
let resp = app.run(req);
|
||||
assert_eq!(resp.as_response().unwrap().status(), StatusCode::NOT_FOUND);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
40
src/body.rs
40
src/body.rs
|
@ -5,9 +5,10 @@ use bytes::{Bytes, BytesMut};
|
|||
use futures::Stream;
|
||||
|
||||
use error::Error;
|
||||
use context::ActorHttpContext;
|
||||
|
||||
pub(crate) type BodyStream = Box<Stream<Item=Bytes, Error=Error>>;
|
||||
|
||||
/// Type represent streaming body
|
||||
pub type BodyStream = Box<Stream<Item=Bytes, Error=Error>>;
|
||||
|
||||
/// Represents various types of http message body.
|
||||
pub enum Body {
|
||||
|
@ -18,12 +19,8 @@ pub enum Body {
|
|||
/// Unspecified streaming response. Developer is responsible for setting
|
||||
/// right `Content-Length` or `Transfer-Encoding` headers.
|
||||
Streaming(BodyStream),
|
||||
/// Upgrade connection.
|
||||
Upgrade(BodyStream),
|
||||
/// Special body type for actor streaming response.
|
||||
StreamingContext,
|
||||
/// Special body type for actor upgrade response.
|
||||
UpgradeContext,
|
||||
/// Special body type for actor response.
|
||||
Actor(Box<ActorHttpContext>),
|
||||
}
|
||||
|
||||
/// Represents various types of binary body.
|
||||
|
@ -48,15 +45,16 @@ pub enum Binary {
|
|||
|
||||
impl Body {
|
||||
/// Does this body streaming.
|
||||
#[inline]
|
||||
pub fn is_streaming(&self) -> bool {
|
||||
match *self {
|
||||
Body::Streaming(_) | Body::StreamingContext
|
||||
| Body::Upgrade(_) | Body::UpgradeContext => true,
|
||||
Body::Streaming(_) | Body::Actor(_) => true,
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
|
||||
/// Is this binary body.
|
||||
#[inline]
|
||||
pub fn is_binary(&self) -> bool {
|
||||
match *self {
|
||||
Body::Binary(_) => true,
|
||||
|
@ -81,15 +79,7 @@ impl PartialEq for Body {
|
|||
Body::Binary(ref b2) => b == b2,
|
||||
_ => false,
|
||||
},
|
||||
Body::StreamingContext => match *other {
|
||||
Body::StreamingContext => true,
|
||||
_ => false,
|
||||
},
|
||||
Body::UpgradeContext => match *other {
|
||||
Body::UpgradeContext => true,
|
||||
_ => false,
|
||||
},
|
||||
Body::Streaming(_) | Body::Upgrade(_) => false,
|
||||
Body::Streaming(_) | Body::Actor(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -100,9 +90,7 @@ impl fmt::Debug for Body {
|
|||
Body::Empty => write!(f, "Body::Empty"),
|
||||
Body::Binary(ref b) => write!(f, "Body::Binary({:?})", b),
|
||||
Body::Streaming(_) => write!(f, "Body::Streaming(_)"),
|
||||
Body::Upgrade(_) => write!(f, "Body::Upgrade(_)"),
|
||||
Body::StreamingContext => write!(f, "Body::StreamingContext"),
|
||||
Body::UpgradeContext => write!(f, "Body::UpgradeContext"),
|
||||
Body::Actor(_) => write!(f, "Body::Actor(_)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -113,11 +101,19 @@ impl<T> From<T> for Body where T: Into<Binary>{
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Box<ActorHttpContext>> for Body {
|
||||
fn from(ctx: Box<ActorHttpContext>) -> Body {
|
||||
Body::Actor(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Binary {
|
||||
#[inline]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn len(&self) -> usize {
|
||||
match *self {
|
||||
Binary::Bytes(ref bytes) => bytes.len(),
|
||||
|
|
357
src/channel.rs
357
src/channel.rs
|
@ -1,24 +1,54 @@
|
|||
use std::{ptr, mem, time, io};
|
||||
use std::rc::Rc;
|
||||
use std::net::SocketAddr;
|
||||
use std::net::{SocketAddr, Shutdown};
|
||||
|
||||
use actix::dev::*;
|
||||
use bytes::Bytes;
|
||||
use bytes::{Bytes, Buf, BufMut};
|
||||
use futures::{Future, Poll, Async};
|
||||
use tokio_io::{AsyncRead, AsyncWrite};
|
||||
use tokio_core::net::TcpStream;
|
||||
|
||||
use h1;
|
||||
use h2;
|
||||
use pipeline::Pipeline;
|
||||
use {h1, h2};
|
||||
use error::Error;
|
||||
use h1writer::Writer;
|
||||
use httprequest::HttpRequest;
|
||||
use server::ServerSettings;
|
||||
use worker::WorkerSettings;
|
||||
|
||||
/// Low level http request handler
|
||||
#[allow(unused_variables)]
|
||||
pub trait HttpHandler: 'static {
|
||||
|
||||
/// Handle request
|
||||
fn handle(&self, req: HttpRequest) -> Result<Pipeline, HttpRequest>;
|
||||
fn handle(&mut self, req: HttpRequest) -> Result<Box<HttpHandlerTask>, HttpRequest>;
|
||||
}
|
||||
|
||||
enum HttpProtocol<T, H>
|
||||
where T: AsyncRead + AsyncWrite + 'static, H: 'static
|
||||
pub trait HttpHandlerTask {
|
||||
|
||||
fn poll_io(&mut self, io: &mut Writer) -> Poll<bool, Error>;
|
||||
|
||||
fn poll(&mut self) -> Poll<(), Error>;
|
||||
|
||||
fn disconnected(&mut self);
|
||||
}
|
||||
|
||||
/// Conversion helper trait
|
||||
pub trait IntoHttpHandler {
|
||||
/// The associated type which is result of conversion.
|
||||
type Handler: HttpHandler;
|
||||
|
||||
/// Convert into `HttpHandler` object.
|
||||
fn into_handler(self, settings: ServerSettings) -> Self::Handler;
|
||||
}
|
||||
|
||||
impl<T: HttpHandler> IntoHttpHandler for T {
|
||||
type Handler = T;
|
||||
|
||||
fn into_handler(self, _: ServerSettings) -> Self::Handler {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
enum HttpProtocol<T: IoStream, H: 'static>
|
||||
{
|
||||
H1(h1::Http1<T, H>),
|
||||
H2(h2::Http2<T, H>),
|
||||
|
@ -26,61 +56,104 @@ enum HttpProtocol<T, H>
|
|||
|
||||
#[doc(hidden)]
|
||||
pub struct HttpChannel<T, H>
|
||||
where T: AsyncRead + AsyncWrite + 'static, H: 'static
|
||||
where T: IoStream, H: HttpHandler + 'static
|
||||
{
|
||||
proto: Option<HttpProtocol<T, H>>,
|
||||
node: Option<Node<HttpChannel<T, H>>>,
|
||||
}
|
||||
|
||||
impl<T, H> HttpChannel<T, H>
|
||||
where T: AsyncRead + AsyncWrite + 'static, H: HttpHandler + 'static
|
||||
where T: IoStream, H: HttpHandler + 'static
|
||||
{
|
||||
pub fn new(stream: T, addr: Option<SocketAddr>, router: Rc<Vec<H>>, http2: bool)
|
||||
-> HttpChannel<T, H> {
|
||||
pub(crate) fn new(h: Rc<WorkerSettings<H>>,
|
||||
io: T, peer: Option<SocketAddr>, http2: bool) -> HttpChannel<T, H>
|
||||
{
|
||||
h.add_channel();
|
||||
if http2 {
|
||||
HttpChannel {
|
||||
node: None,
|
||||
proto: Some(HttpProtocol::H2(
|
||||
h2::Http2::new(stream, addr, router, Bytes::new()))) }
|
||||
h2::Http2::new(h, io, peer, Bytes::new()))) }
|
||||
} else {
|
||||
HttpChannel {
|
||||
node: None,
|
||||
proto: Some(HttpProtocol::H1(
|
||||
h1::Http1::new(stream, addr, router))) }
|
||||
h1::Http1::new(h, io, peer))) }
|
||||
}
|
||||
}
|
||||
|
||||
fn shutdown(&mut self) {
|
||||
match self.proto {
|
||||
Some(HttpProtocol::H1(ref mut h1)) => {
|
||||
let io = h1.io();
|
||||
let _ = IoStream::set_linger(io, Some(time::Duration::new(0, 0)));
|
||||
let _ = IoStream::shutdown(io, Shutdown::Both);
|
||||
}
|
||||
Some(HttpProtocol::H2(ref mut h2)) => {
|
||||
h2.shutdown()
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*impl<T: 'static, A: 'static, H: 'static> Drop for HttpChannel<T, A, H> {
|
||||
/*impl<T, H> Drop for HttpChannel<T, H>
|
||||
where T: AsyncRead + AsyncWrite + 'static, H: HttpHandler + 'static
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
println!("Drop http channel");
|
||||
}
|
||||
}*/
|
||||
|
||||
impl<T, H> Actor for HttpChannel<T, H>
|
||||
where T: AsyncRead + AsyncWrite + 'static, H: HttpHandler + 'static
|
||||
{
|
||||
type Context = Context<Self>;
|
||||
}
|
||||
|
||||
impl<T, H> Future for HttpChannel<T, H>
|
||||
where T: AsyncRead + AsyncWrite + 'static, H: HttpHandler + 'static
|
||||
where T: IoStream, H: HttpHandler + 'static
|
||||
{
|
||||
type Item = ();
|
||||
type Error = ();
|
||||
|
||||
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
|
||||
if self.node.is_none() {
|
||||
self.node = Some(Node::new(self));
|
||||
match self.proto {
|
||||
Some(HttpProtocol::H1(ref mut h1)) => {
|
||||
h1.settings().head().insert(self.node.as_ref().unwrap());
|
||||
}
|
||||
Some(HttpProtocol::H2(ref mut h2)) => {
|
||||
h2.settings().head().insert(self.node.as_ref().unwrap());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
match self.proto {
|
||||
Some(HttpProtocol::H1(ref mut h1)) => {
|
||||
match h1.poll() {
|
||||
Ok(Async::Ready(h1::Http1Result::Done)) =>
|
||||
return Ok(Async::Ready(())),
|
||||
Ok(Async::Ready(h1::Http1Result::Done)) => {
|
||||
h1.settings().remove_channel();
|
||||
self.node.as_ref().unwrap().remove();
|
||||
return Ok(Async::Ready(()))
|
||||
}
|
||||
Ok(Async::Ready(h1::Http1Result::Switch)) => (),
|
||||
Ok(Async::NotReady) =>
|
||||
return Ok(Async::NotReady),
|
||||
Err(_) =>
|
||||
return Err(()),
|
||||
Err(_) => {
|
||||
h1.settings().remove_channel();
|
||||
self.node.as_ref().unwrap().remove();
|
||||
return Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(HttpProtocol::H2(ref mut h2)) =>
|
||||
return h2.poll(),
|
||||
Some(HttpProtocol::H2(ref mut h2)) => {
|
||||
let result = h2.poll();
|
||||
match result {
|
||||
Ok(Async::Ready(())) | Err(_) => {
|
||||
h2.settings().remove_channel();
|
||||
self.node.as_ref().unwrap().remove();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
return result
|
||||
}
|
||||
None => unreachable!(),
|
||||
}
|
||||
|
||||
|
@ -88,11 +161,233 @@ impl<T, H> Future for HttpChannel<T, H>
|
|||
let proto = self.proto.take().unwrap();
|
||||
match proto {
|
||||
HttpProtocol::H1(h1) => {
|
||||
let (stream, addr, router, buf) = h1.into_inner();
|
||||
self.proto = Some(HttpProtocol::H2(h2::Http2::new(stream, addr, router, buf)));
|
||||
let (h, io, addr, buf) = h1.into_inner();
|
||||
self.proto = Some(
|
||||
HttpProtocol::H2(h2::Http2::new(h, io, addr, buf)));
|
||||
self.poll()
|
||||
}
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Node<T>
|
||||
{
|
||||
next: Option<*mut Node<()>>,
|
||||
prev: Option<*mut Node<()>>,
|
||||
element: *mut T,
|
||||
}
|
||||
|
||||
impl<T> Node<T>
|
||||
{
|
||||
fn new(el: &mut T) -> Self {
|
||||
Node {
|
||||
next: None,
|
||||
prev: None,
|
||||
element: el as *mut _,
|
||||
}
|
||||
}
|
||||
|
||||
fn insert<I>(&self, next: &Node<I>) {
|
||||
#[allow(mutable_transmutes)]
|
||||
unsafe {
|
||||
if let Some(ref next2) = self.next {
|
||||
let n: &mut Node<()> = mem::transmute(next2.as_ref().unwrap());
|
||||
n.prev = Some(next as *const _ as *mut _);
|
||||
}
|
||||
let slf: &mut Node<T> = mem::transmute(self);
|
||||
slf.next = Some(next as *const _ as *mut _);
|
||||
|
||||
let next: &mut Node<T> = mem::transmute(next);
|
||||
next.prev = Some(slf as *const _ as *mut _);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(&self) {
|
||||
#[allow(mutable_transmutes)]
|
||||
unsafe {
|
||||
if let Some(ref prev) = self.prev {
|
||||
let p: &mut Node<()> = mem::transmute(prev.as_ref().unwrap());
|
||||
let slf: &mut Node<T> = mem::transmute(self);
|
||||
p.next = slf.next.take();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Node<()> {
|
||||
|
||||
pub(crate) fn head() -> Self {
|
||||
Node {
|
||||
next: None,
|
||||
prev: None,
|
||||
element: ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn traverse<T, H>(&self) where T: IoStream, H: HttpHandler + 'static {
|
||||
let mut next = self.next.as_ref();
|
||||
loop {
|
||||
if let Some(n) = next {
|
||||
unsafe {
|
||||
let n: &Node<()> = mem::transmute(n.as_ref().unwrap());
|
||||
next = n.next.as_ref();
|
||||
|
||||
if !n.element.is_null() {
|
||||
let ch: &mut HttpChannel<T, H> = mem::transmute(
|
||||
&mut *(n.element as *mut _));
|
||||
ch.shutdown();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Low-level io stream operations
|
||||
pub trait IoStream: AsyncRead + AsyncWrite + 'static {
|
||||
fn shutdown(&mut self, how: Shutdown) -> io::Result<()>;
|
||||
|
||||
fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()>;
|
||||
|
||||
fn set_linger(&mut self, dur: Option<time::Duration>) -> io::Result<()>;
|
||||
}
|
||||
|
||||
impl IoStream for TcpStream {
|
||||
#[inline]
|
||||
fn shutdown(&mut self, how: Shutdown) -> io::Result<()> {
|
||||
TcpStream::shutdown(self, how)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> {
|
||||
TcpStream::set_nodelay(self, nodelay)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_linger(&mut self, dur: Option<time::Duration>) -> io::Result<()> {
|
||||
TcpStream::set_linger(self, dur)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Wrapper for `AsyncRead + AsyncWrite` types
|
||||
pub(crate) struct WrapperStream<T> where T: AsyncRead + AsyncWrite + 'static {
|
||||
io: T,
|
||||
}
|
||||
|
||||
impl<T> WrapperStream<T> where T: AsyncRead + AsyncWrite + 'static
|
||||
{
|
||||
pub fn new(io: T) -> Self {
|
||||
WrapperStream{io: io}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IoStream for WrapperStream<T>
|
||||
where T: AsyncRead + AsyncWrite + 'static
|
||||
{
|
||||
#[inline]
|
||||
fn shutdown(&mut self, _: Shutdown) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_nodelay(&mut self, _: bool) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_linger(&mut self, _: Option<time::Duration>) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> io::Read for WrapperStream<T>
|
||||
where T: AsyncRead + AsyncWrite + 'static
|
||||
{
|
||||
#[inline]
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
self.io.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> io::Write for WrapperStream<T>
|
||||
where T: AsyncRead + AsyncWrite + 'static
|
||||
{
|
||||
#[inline]
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.io.write(buf)
|
||||
}
|
||||
#[inline]
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.io.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsyncRead for WrapperStream<T>
|
||||
where T: AsyncRead + AsyncWrite + 'static
|
||||
{
|
||||
fn read_buf<B: BufMut>(&mut self, buf: &mut B) -> Poll<usize, io::Error> {
|
||||
self.io.read_buf(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsyncWrite for WrapperStream<T>
|
||||
where T: AsyncRead + AsyncWrite + 'static
|
||||
{
|
||||
fn shutdown(&mut self) -> Poll<(), io::Error> {
|
||||
self.io.shutdown()
|
||||
}
|
||||
fn write_buf<B: Buf>(&mut self, buf: &mut B) -> Poll<usize, io::Error> {
|
||||
self.io.write_buf(buf)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(feature="alpn")]
|
||||
use tokio_openssl::SslStream;
|
||||
|
||||
#[cfg(feature="alpn")]
|
||||
impl IoStream for SslStream<TcpStream> {
|
||||
#[inline]
|
||||
fn shutdown(&mut self, _how: Shutdown) -> io::Result<()> {
|
||||
let _ = self.get_mut().shutdown();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> {
|
||||
self.get_mut().get_mut().set_nodelay(nodelay)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_linger(&mut self, dur: Option<time::Duration>) -> io::Result<()> {
|
||||
self.get_mut().get_mut().set_linger(dur)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature="tls")]
|
||||
use tokio_tls::TlsStream;
|
||||
|
||||
#[cfg(feature="tls")]
|
||||
impl IoStream for TlsStream<TcpStream> {
|
||||
#[inline]
|
||||
fn shutdown(&mut self, _how: Shutdown) -> io::Result<()> {
|
||||
let _ = self.get_mut().shutdown();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> {
|
||||
self.get_mut().get_mut().set_nodelay(nodelay)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_linger(&mut self, dur: Option<time::Duration>) -> io::Result<()> {
|
||||
self.get_mut().get_mut().set_linger(dur)
|
||||
}
|
||||
}
|
||||
|
|
247
src/context.rs
247
src/context.rs
|
@ -1,47 +1,38 @@
|
|||
use std;
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
use std::marker::PhantomData;
|
||||
use std::collections::VecDeque;
|
||||
use futures::{Async, Future, Poll};
|
||||
use futures::sync::oneshot::Sender;
|
||||
use futures::unsync::oneshot;
|
||||
|
||||
use actix::{Actor, ActorState, ActorContext, AsyncContext,
|
||||
Handler, Subscriber, ResponseType};
|
||||
Handler, Subscriber, ResponseType, SpawnHandle};
|
||||
use actix::fut::ActorFuture;
|
||||
use actix::dev::{AsyncContextApi, ActorAddressCell, ActorItemsCell, ActorWaitCell, SpawnHandle,
|
||||
Envelope, ToEnvelope, RemoteEnvelope};
|
||||
use actix::dev::{AsyncContextApi, ActorAddressCell,
|
||||
ContextImpl, Envelope, ToEnvelope, RemoteEnvelope};
|
||||
|
||||
use body::{Body, Binary};
|
||||
use error::Error;
|
||||
use error::{Error, Result, ErrorInternalServerError};
|
||||
use httprequest::HttpRequest;
|
||||
use httpresponse::HttpResponse;
|
||||
use pipeline::DrainFut;
|
||||
|
||||
pub(crate) trait IoContext: 'static {
|
||||
|
||||
pub trait ActorHttpContext: 'static {
|
||||
fn disconnected(&mut self);
|
||||
fn poll(&mut self) -> Poll<Option<Frame>, Error>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Frame {
|
||||
Message(HttpResponse),
|
||||
pub enum Frame {
|
||||
Payload(Option<Binary>),
|
||||
Drain(Rc<RefCell<DrainFut>>),
|
||||
Drain(oneshot::Sender<()>),
|
||||
}
|
||||
|
||||
/// Http actor execution context
|
||||
pub struct HttpContext<A, S=()> where A: Actor<Context=HttpContext<A, S>>,
|
||||
{
|
||||
act: A,
|
||||
state: ActorState,
|
||||
modified: bool,
|
||||
items: ActorItemsCell<A>,
|
||||
address: ActorAddressCell<A>,
|
||||
inner: ContextImpl<A>,
|
||||
stream: VecDeque<Frame>,
|
||||
wait: ActorWaitCell<A>,
|
||||
request: HttpRequest<S>,
|
||||
streaming: bool,
|
||||
disconnected: bool,
|
||||
}
|
||||
|
||||
|
@ -50,23 +41,17 @@ impl<A, S> ActorContext for HttpContext<A, S> where A: Actor<Context=Self>
|
|||
/// Stop actor execution
|
||||
fn stop(&mut self) {
|
||||
self.stream.push_back(Frame::Payload(None));
|
||||
self.items.stop();
|
||||
self.address.close();
|
||||
if self.state == ActorState::Running {
|
||||
self.state = ActorState::Stopping;
|
||||
}
|
||||
self.inner.stop();
|
||||
}
|
||||
|
||||
/// Terminate actor execution
|
||||
fn terminate(&mut self) {
|
||||
self.address.close();
|
||||
self.items.close();
|
||||
self.state = ActorState::Stopped;
|
||||
self.inner.terminate()
|
||||
}
|
||||
|
||||
/// Actor execution state
|
||||
fn state(&self) -> ActorState {
|
||||
self.state
|
||||
self.inner.state()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -75,51 +60,46 @@ impl<A, S> AsyncContext<A> for HttpContext<A, S> where A: Actor<Context=Self>
|
|||
fn spawn<F>(&mut self, fut: F) -> SpawnHandle
|
||||
where F: ActorFuture<Item=(), Error=(), Actor=A> + 'static
|
||||
{
|
||||
self.modified = true;
|
||||
self.items.spawn(fut)
|
||||
self.inner.spawn(fut)
|
||||
}
|
||||
|
||||
fn wait<F>(&mut self, fut: F)
|
||||
where F: ActorFuture<Item=(), Error=(), Actor=A> + 'static
|
||||
{
|
||||
self.modified = true;
|
||||
self.wait.add(fut);
|
||||
self.inner.wait(fut)
|
||||
}
|
||||
|
||||
fn cancel_future(&mut self, handle: SpawnHandle) -> bool {
|
||||
self.modified = true;
|
||||
self.items.cancel_future(handle)
|
||||
}
|
||||
|
||||
fn cancel_future_on_stop(&mut self, handle: SpawnHandle) {
|
||||
self.items.cancel_future_on_stop(handle)
|
||||
self.inner.cancel_future(handle)
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
impl<A, S> AsyncContextApi<A> for HttpContext<A, S> where A: Actor<Context=Self> {
|
||||
fn address_cell(&mut self) -> &mut ActorAddressCell<A> {
|
||||
&mut self.address
|
||||
self.inner.address_cell()
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, S> HttpContext<A, S> where A: Actor<Context=Self> {
|
||||
impl<A, S: 'static> HttpContext<A, S> where A: Actor<Context=Self> {
|
||||
|
||||
pub fn new(req: HttpRequest<S>, actor: A) -> HttpContext<A, S>
|
||||
{
|
||||
pub fn new(req: HttpRequest<S>, actor: A) -> HttpContext<A, S> {
|
||||
HttpContext::from_request(req).actor(actor)
|
||||
}
|
||||
|
||||
pub fn from_request(req: HttpRequest<S>) -> HttpContext<A, S> {
|
||||
HttpContext {
|
||||
act: actor,
|
||||
state: ActorState::Started,
|
||||
modified: false,
|
||||
items: ActorItemsCell::default(),
|
||||
address: ActorAddressCell::default(),
|
||||
wait: ActorWaitCell::default(),
|
||||
inner: ContextImpl::new(None),
|
||||
stream: VecDeque::new(),
|
||||
request: req,
|
||||
streaming: false,
|
||||
disconnected: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn actor(mut self, actor: A) -> HttpContext<A, S> {
|
||||
self.inner.set_actor(actor);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, S> HttpContext<A, S> where A: Actor<Context=Self> {
|
||||
|
@ -134,24 +114,12 @@ impl<A, S> HttpContext<A, S> where A: Actor<Context=Self> {
|
|||
&mut self.request
|
||||
}
|
||||
|
||||
/// Send response to peer
|
||||
pub fn start<R: Into<HttpResponse>>(&mut self, response: R) {
|
||||
let resp = response.into();
|
||||
match *resp.body() {
|
||||
Body::StreamingContext | Body::UpgradeContext => self.streaming = true,
|
||||
_ => (),
|
||||
}
|
||||
self.stream.push_back(Frame::Message(resp))
|
||||
}
|
||||
|
||||
/// Write payload
|
||||
pub fn write<B: Into<Binary>>(&mut self, data: B) {
|
||||
if self.streaming {
|
||||
if !self.disconnected {
|
||||
self.stream.push_back(Frame::Payload(Some(data.into())))
|
||||
}
|
||||
if !self.disconnected {
|
||||
self.stream.push_back(Frame::Payload(Some(data.into())));
|
||||
} else {
|
||||
warn!("Trying to write response body for non-streaming response");
|
||||
warn!("Trying to write to disconnected response");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -162,10 +130,10 @@ impl<A, S> HttpContext<A, S> where A: Actor<Context=Self> {
|
|||
|
||||
/// Returns drain future
|
||||
pub fn drain(&mut self) -> Drain<A> {
|
||||
let fut = Rc::new(RefCell::new(DrainFut::default()));
|
||||
self.stream.push_back(Frame::Drain(Rc::clone(&fut)));
|
||||
self.modified = true;
|
||||
Drain{ a: PhantomData, inner: fut }
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.inner.modify();
|
||||
self.stream.push_back(Frame::Drain(tx));
|
||||
Drain::new(rx)
|
||||
}
|
||||
|
||||
/// Check if connection still open
|
||||
|
@ -178,117 +146,43 @@ impl<A, S> HttpContext<A, S> where A: Actor<Context=Self> {
|
|||
|
||||
#[doc(hidden)]
|
||||
pub fn subscriber<M>(&mut self) -> Box<Subscriber<M>>
|
||||
where A: Handler<M>,
|
||||
M: ResponseType + 'static,
|
||||
where A: Handler<M>, M: ResponseType + 'static
|
||||
{
|
||||
Box::new(self.address.unsync_address())
|
||||
self.inner.subscriber()
|
||||
}
|
||||
|
||||
#[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,
|
||||
M: ResponseType + Send + 'static, M::Item: Send, M::Error: Send,
|
||||
{
|
||||
Box::new(self.address.sync_address())
|
||||
self.inner.sync_subscriber()
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, S> IoContext for HttpContext<A, S> where A: Actor<Context=Self>, S: 'static {
|
||||
impl<A, S> ActorHttpContext for HttpContext<A, S> where A: Actor<Context=Self>, S: 'static {
|
||||
|
||||
fn disconnected(&mut self) {
|
||||
self.items.stop();
|
||||
self.disconnected = true;
|
||||
if self.state == ActorState::Running {
|
||||
self.state = ActorState::Stopping;
|
||||
}
|
||||
self.stop();
|
||||
}
|
||||
|
||||
fn poll(&mut self) -> Poll<Option<Frame>, Error> {
|
||||
let act: &mut A = unsafe {
|
||||
std::mem::transmute(&mut self.act as &mut A)
|
||||
};
|
||||
let ctx: &mut HttpContext<A, S> = unsafe {
|
||||
std::mem::transmute(self as &mut HttpContext<A, S>)
|
||||
};
|
||||
|
||||
// update state
|
||||
match self.state {
|
||||
ActorState::Started => {
|
||||
Actor::started(act, ctx);
|
||||
self.state = ActorState::Running;
|
||||
},
|
||||
ActorState::Stopping => {
|
||||
Actor::stopping(act, ctx);
|
||||
}
|
||||
_ => ()
|
||||
}
|
||||
|
||||
let mut prep_stop = false;
|
||||
loop {
|
||||
self.modified = false;
|
||||
|
||||
// check wait futures
|
||||
if self.wait.poll(act, ctx) {
|
||||
match self.inner.poll(ctx) {
|
||||
Ok(Async::NotReady) => {
|
||||
// get frame
|
||||
if let Some(frame) = self.stream.pop_front() {
|
||||
return Ok(Async::Ready(Some(frame)))
|
||||
Ok(Async::Ready(Some(frame)))
|
||||
} else {
|
||||
Ok(Async::NotReady)
|
||||
}
|
||||
return Ok(Async::NotReady)
|
||||
}
|
||||
|
||||
// incoming messages
|
||||
self.address.poll(act, ctx);
|
||||
|
||||
// spawned futures and streams
|
||||
self.items.poll(act, ctx);
|
||||
|
||||
// are we done
|
||||
if self.modified {
|
||||
continue
|
||||
}
|
||||
|
||||
// get frame
|
||||
if let Some(frame) = self.stream.pop_front() {
|
||||
return Ok(Async::Ready(Some(frame)))
|
||||
}
|
||||
|
||||
// check state
|
||||
match self.state {
|
||||
ActorState::Stopped => {
|
||||
self.state = ActorState::Stopped;
|
||||
Actor::stopped(act, ctx);
|
||||
return Ok(Async::Ready(None))
|
||||
},
|
||||
ActorState::Stopping => {
|
||||
if prep_stop {
|
||||
if self.address.connected() || !self.items.is_empty() {
|
||||
self.state = ActorState::Running;
|
||||
continue
|
||||
} else {
|
||||
self.state = ActorState::Stopped;
|
||||
Actor::stopped(act, ctx);
|
||||
return Ok(Async::Ready(None))
|
||||
}
|
||||
} else {
|
||||
Actor::stopping(act, ctx);
|
||||
prep_stop = true;
|
||||
continue
|
||||
}
|
||||
},
|
||||
ActorState::Running => {
|
||||
if !self.address.connected() && self.items.is_empty() {
|
||||
self.state = ActorState::Stopping;
|
||||
Actor::stopping(act, ctx);
|
||||
prep_stop = true;
|
||||
continue
|
||||
}
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
|
||||
return Ok(Async::NotReady)
|
||||
Ok(Async::Ready(())) => Ok(Async::Ready(None)),
|
||||
Err(_) => Err(ErrorInternalServerError("error").into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -296,30 +190,49 @@ impl<A, S> IoContext for HttpContext<A, S> where A: Actor<Context=Self>, S: 'sta
|
|||
impl<A, S> ToEnvelope<A> for HttpContext<A, S>
|
||||
where A: Actor<Context=HttpContext<A, S>>,
|
||||
{
|
||||
fn pack<M>(msg: M, tx: Option<Sender<Result<M::Item, M::Error>>>) -> Envelope<A>
|
||||
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).into()
|
||||
RemoteEnvelope::new(msg, tx, channel_on_drop).into()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct Drain<A> {
|
||||
a: PhantomData<A>,
|
||||
inner: Rc<RefCell<DrainFut>>
|
||||
impl<A, S> From<HttpContext<A, S>> for Body
|
||||
where A: Actor<Context=HttpContext<A, S>>,
|
||||
S: 'static
|
||||
{
|
||||
fn from(ctx: HttpContext<A, S>) -> Body {
|
||||
Body::Actor(Box::new(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
impl<A> ActorFuture for Drain<A>
|
||||
where A: Actor
|
||||
{
|
||||
pub struct Drain<A> {
|
||||
fut: oneshot::Receiver<()>,
|
||||
_a: PhantomData<A>,
|
||||
}
|
||||
|
||||
impl<A> Drain<A> {
|
||||
fn new(fut: oneshot::Receiver<()>) -> Self {
|
||||
Drain {
|
||||
fut: fut,
|
||||
_a: PhantomData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Actor> ActorFuture for Drain<A> {
|
||||
type Item = ();
|
||||
type Error = ();
|
||||
type Actor = A;
|
||||
|
||||
fn poll(&mut self, _: &mut A, _: &mut <Self::Actor as Actor>::Context) -> Poll<(), ()> {
|
||||
self.inner.borrow_mut().poll()
|
||||
fn poll(&mut self,
|
||||
_: &mut A,
|
||||
_: &mut <Self::Actor as Actor>::Context) -> Poll<Self::Item, Self::Error>
|
||||
{
|
||||
self.fut.poll().map_err(|_| ())
|
||||
}
|
||||
}
|
||||
|
|
68
src/date.rs
68
src/date.rs
|
@ -1,68 +0,0 @@
|
|||
use std::cell::RefCell;
|
||||
use std::fmt::{self, Write};
|
||||
use std::str;
|
||||
use time::{self, Duration};
|
||||
|
||||
// "Sun, 06 Nov 1994 08:49:37 GMT".len()
|
||||
pub const DATE_VALUE_LENGTH: usize = 29;
|
||||
|
||||
pub fn extend(dst: &mut [u8]) {
|
||||
CACHED.with(|cache| {
|
||||
let mut cache = cache.borrow_mut();
|
||||
let now = time::get_time();
|
||||
if now > cache.next_update {
|
||||
cache.update(now);
|
||||
}
|
||||
|
||||
dst.copy_from_slice(cache.buffer());
|
||||
})
|
||||
}
|
||||
|
||||
struct CachedDate {
|
||||
bytes: [u8; DATE_VALUE_LENGTH],
|
||||
pos: usize,
|
||||
next_update: time::Timespec,
|
||||
}
|
||||
|
||||
thread_local!(static CACHED: RefCell<CachedDate> = RefCell::new(CachedDate {
|
||||
bytes: [0; DATE_VALUE_LENGTH],
|
||||
pos: 0,
|
||||
next_update: time::Timespec::new(0, 0),
|
||||
}));
|
||||
|
||||
impl CachedDate {
|
||||
fn buffer(&self) -> &[u8] {
|
||||
&self.bytes[..]
|
||||
}
|
||||
|
||||
fn update(&mut self, now: time::Timespec) {
|
||||
self.pos = 0;
|
||||
write!(self, "{}", time::at_utc(now).rfc822()).unwrap();
|
||||
assert_eq!(self.pos, DATE_VALUE_LENGTH);
|
||||
self.next_update = now + Duration::seconds(1);
|
||||
self.next_update.nsec = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Write for CachedDate {
|
||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||
let len = s.len();
|
||||
self.bytes[self.pos..self.pos + len].copy_from_slice(s.as_bytes());
|
||||
self.pos += len;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_date_len() {
|
||||
assert_eq!(DATE_VALUE_LENGTH, "Sun, 06 Nov 1994 08:49:37 GMT".len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_date() {
|
||||
let mut buf1 = [0u8; 29];
|
||||
extend(&mut buf1);
|
||||
let mut buf2 = [0u8; 29];
|
||||
extend(&mut buf2);
|
||||
assert_eq!(buf1, buf2);
|
||||
}
|
19
src/dev.rs
19
src/dev.rs
|
@ -1,19 +0,0 @@
|
|||
//! The `actix-web` prelude for library developers
|
||||
//!
|
||||
//! The purpose of this module is to alleviate imports of many common actix traits
|
||||
//! by adding a glob import to the top of actix heavy modules:
|
||||
//!
|
||||
//! ```
|
||||
//! # #![allow(unused_imports)]
|
||||
//! use actix_web::dev::*;
|
||||
//! ```
|
||||
|
||||
// dev specific
|
||||
pub use pipeline::Pipeline;
|
||||
pub use route::Handler;
|
||||
pub use recognizer::RouteRecognizer;
|
||||
pub use channel::{HttpChannel, HttpHandler};
|
||||
|
||||
pub use application::ApplicationBuilder;
|
||||
pub use httpresponse::HttpResponseBuilder;
|
||||
pub use cookie::CookieBuilder;
|
252
src/encoding.rs
252
src/encoding.rs
|
@ -15,7 +15,8 @@ use bytes::{Bytes, BytesMut, BufMut, Writer};
|
|||
|
||||
use body::{Body, Binary};
|
||||
use error::PayloadError;
|
||||
use httprequest::HttpRequest;
|
||||
use helpers::SharedBytes;
|
||||
use httprequest::HttpMessage;
|
||||
use httpresponse::HttpResponse;
|
||||
use payload::{PayloadSender, PayloadWriter};
|
||||
|
||||
|
@ -35,6 +36,14 @@ pub enum ContentEncoding {
|
|||
}
|
||||
|
||||
impl ContentEncoding {
|
||||
|
||||
fn is_compression(&self) -> bool {
|
||||
match *self {
|
||||
ContentEncoding::Identity | ContentEncoding::Auto => false,
|
||||
_ => true
|
||||
}
|
||||
}
|
||||
|
||||
fn as_str(&self) -> &'static str {
|
||||
match *self {
|
||||
ContentEncoding::Br => "br",
|
||||
|
@ -125,9 +134,9 @@ impl PayloadWriter for PayloadType {
|
|||
}
|
||||
|
||||
enum Decoder {
|
||||
Deflate(DeflateDecoder<Writer<BytesMut>>),
|
||||
Gzip(Option<GzDecoder<Wrapper>>),
|
||||
Br(BrotliDecoder<Writer<BytesMut>>),
|
||||
Deflate(Box<DeflateDecoder<Writer<BytesMut>>>),
|
||||
Gzip(Box<Option<GzDecoder<Wrapper>>>),
|
||||
Br(Box<BrotliDecoder<Writer<BytesMut>>>),
|
||||
Identity,
|
||||
}
|
||||
|
||||
|
@ -158,10 +167,10 @@ impl EncodedPayload {
|
|||
pub fn new(inner: PayloadSender, enc: ContentEncoding) -> EncodedPayload {
|
||||
let dec = match enc {
|
||||
ContentEncoding::Br => Decoder::Br(
|
||||
BrotliDecoder::new(BytesMut::with_capacity(8192).writer())),
|
||||
Box::new(BrotliDecoder::new(BytesMut::with_capacity(8192).writer()))),
|
||||
ContentEncoding::Deflate => Decoder::Deflate(
|
||||
DeflateDecoder::new(BytesMut::with_capacity(8192).writer())),
|
||||
ContentEncoding::Gzip => Decoder::Gzip(None),
|
||||
Box::new(DeflateDecoder::new(BytesMut::with_capacity(8192).writer()))),
|
||||
ContentEncoding::Gzip => Decoder::Gzip(Box::new(None)),
|
||||
_ => Decoder::Identity,
|
||||
};
|
||||
EncodedPayload {
|
||||
|
@ -204,13 +213,13 @@ impl PayloadWriter for EncodedPayload {
|
|||
}
|
||||
loop {
|
||||
let len = self.dst.get_ref().len();
|
||||
let len_buf = decoder.as_mut().unwrap().get_mut().buf.len();
|
||||
let len_buf = decoder.as_mut().as_mut().unwrap().get_mut().buf.len();
|
||||
|
||||
if len < len_buf * 2 {
|
||||
self.dst.get_mut().reserve(len_buf * 2 - len);
|
||||
unsafe{self.dst.get_mut().set_len(len_buf * 2)};
|
||||
}
|
||||
match decoder.as_mut().unwrap().read(&mut self.dst.get_mut()) {
|
||||
match decoder.as_mut().as_mut().unwrap().read(&mut self.dst.get_mut()) {
|
||||
Ok(n) => {
|
||||
if n == 0 {
|
||||
self.inner.feed_eof();
|
||||
|
@ -270,14 +279,14 @@ impl PayloadWriter for EncodedPayload {
|
|||
Decoder::Gzip(ref mut decoder) => {
|
||||
if decoder.is_none() {
|
||||
let mut buf = BytesMut::new();
|
||||
buf.extend(data);
|
||||
*decoder = Some(GzDecoder::new(Wrapper{buf: buf}).unwrap());
|
||||
buf.extend_from_slice(&data);
|
||||
*(decoder.as_mut()) = Some(GzDecoder::new(Wrapper{buf: buf}).unwrap());
|
||||
} else {
|
||||
decoder.as_mut().unwrap().get_mut().buf.extend(data);
|
||||
decoder.as_mut().as_mut().unwrap().get_mut().buf.extend_from_slice(&data);
|
||||
}
|
||||
|
||||
loop {
|
||||
let len_buf = decoder.as_mut().unwrap().get_mut().buf.len();
|
||||
let len_buf = decoder.as_mut().as_mut().unwrap().get_mut().buf.len();
|
||||
if len_buf == 0 {
|
||||
return
|
||||
}
|
||||
|
@ -287,7 +296,7 @@ impl PayloadWriter for EncodedPayload {
|
|||
self.dst.get_mut().reserve(len_buf * 2 - len);
|
||||
unsafe{self.dst.get_mut().set_len(len_buf * 2)};
|
||||
}
|
||||
match decoder.as_mut().unwrap().read(&mut self.dst.get_mut()) {
|
||||
match decoder.as_mut().as_mut().unwrap().read(&mut self.dst.get_mut()) {
|
||||
Ok(n) => {
|
||||
if n == 0 {
|
||||
return
|
||||
|
@ -328,16 +337,16 @@ impl PayloadWriter for EncodedPayload {
|
|||
|
||||
pub(crate) struct PayloadEncoder(ContentEncoder);
|
||||
|
||||
impl Default for PayloadEncoder {
|
||||
fn default() -> PayloadEncoder {
|
||||
PayloadEncoder(ContentEncoder::Identity(TransferEncoding::eof()))
|
||||
}
|
||||
}
|
||||
|
||||
impl PayloadEncoder {
|
||||
|
||||
pub fn new(req: &HttpRequest, resp: &mut HttpResponse) -> PayloadEncoder {
|
||||
let version = resp.version().unwrap_or_else(|| req.version());
|
||||
pub fn empty(bytes: SharedBytes) -> PayloadEncoder {
|
||||
PayloadEncoder(ContentEncoder::Identity(TransferEncoding::eof(bytes)))
|
||||
}
|
||||
|
||||
pub fn new(buf: SharedBytes, req: &HttpMessage, resp: &mut HttpResponse)
|
||||
-> PayloadEncoder
|
||||
{
|
||||
let version = resp.version().unwrap_or_else(|| req.version);
|
||||
let mut body = resp.replace_body(Body::Empty);
|
||||
let has_body = match body {
|
||||
Body::Empty => false,
|
||||
|
@ -346,11 +355,11 @@ impl PayloadEncoder {
|
|||
};
|
||||
|
||||
// Enable content encoding only if response does not contain Content-Encoding header
|
||||
let mut encoding = if has_body && !resp.headers.contains_key(CONTENT_ENCODING) {
|
||||
let mut encoding = if has_body && !resp.headers().contains_key(CONTENT_ENCODING) {
|
||||
let encoding = match *resp.content_encoding() {
|
||||
ContentEncoding::Auto => {
|
||||
// negotiate content-encoding
|
||||
if let Some(val) = req.headers().get(ACCEPT_ENCODING) {
|
||||
if let Some(val) = req.headers.get(ACCEPT_ENCODING) {
|
||||
if let Ok(enc) = val.to_str() {
|
||||
AcceptEncoding::parse(enc)
|
||||
} else {
|
||||
|
@ -362,7 +371,10 @@ impl PayloadEncoder {
|
|||
}
|
||||
encoding => encoding,
|
||||
};
|
||||
resp.headers.insert(CONTENT_ENCODING, HeaderValue::from_static(encoding.as_str()));
|
||||
if encoding.is_compression() {
|
||||
resp.headers_mut().insert(
|
||||
CONTENT_ENCODING, HeaderValue::from_static(encoding.as_str()));
|
||||
}
|
||||
encoding
|
||||
} else {
|
||||
ContentEncoding::Identity
|
||||
|
@ -377,13 +389,13 @@ impl PayloadEncoder {
|
|||
if resp.chunked() {
|
||||
error!("Chunked transfer is enabled but body is set to Empty");
|
||||
}
|
||||
resp.headers.insert(CONTENT_LENGTH, HeaderValue::from_static("0"));
|
||||
resp.headers.remove(TRANSFER_ENCODING);
|
||||
TransferEncoding::length(0)
|
||||
resp.headers_mut().remove(CONTENT_LENGTH);
|
||||
TransferEncoding::eof(buf)
|
||||
},
|
||||
Body::Binary(ref mut bytes) => {
|
||||
if compression {
|
||||
let transfer = TransferEncoding::eof();
|
||||
let buf = SharedBytes::default();
|
||||
let transfer = TransferEncoding::eof(buf.clone());
|
||||
let mut enc = match encoding {
|
||||
ContentEncoding::Deflate => ContentEncoder::Deflate(
|
||||
DeflateEncoder::new(transfer, Compression::Default)),
|
||||
|
@ -397,64 +409,55 @@ impl PayloadEncoder {
|
|||
// TODO return error!
|
||||
let _ = enc.write(bytes.as_ref());
|
||||
let _ = enc.write_eof();
|
||||
let b = enc.get_mut().take();
|
||||
|
||||
resp.headers.insert(
|
||||
CONTENT_LENGTH,
|
||||
HeaderValue::from_str(format!("{}", b.len()).as_str()).unwrap());
|
||||
*bytes = Binary::from(b);
|
||||
*bytes = Binary::from(buf.get_mut().take());
|
||||
encoding = ContentEncoding::Identity;
|
||||
TransferEncoding::eof()
|
||||
} else {
|
||||
resp.headers.insert(
|
||||
CONTENT_LENGTH,
|
||||
HeaderValue::from_str(format!("{}", bytes.len()).as_str()).unwrap());
|
||||
resp.headers.remove(TRANSFER_ENCODING);
|
||||
TransferEncoding::length(bytes.len() as u64)
|
||||
}
|
||||
resp.headers_mut().remove(CONTENT_LENGTH);
|
||||
TransferEncoding::eof(buf)
|
||||
}
|
||||
Body::Streaming(_) | Body::StreamingContext => {
|
||||
if resp.chunked() {
|
||||
resp.headers.remove(CONTENT_LENGTH);
|
||||
Body::Streaming(_) | Body::Actor(_) => {
|
||||
if resp.upgrade() {
|
||||
if version == Version::HTTP_2 {
|
||||
error!("Connection upgrade is forbidden for HTTP/2");
|
||||
} else {
|
||||
resp.headers_mut().insert(
|
||||
CONNECTION, HeaderValue::from_static("upgrade"));
|
||||
}
|
||||
if encoding != ContentEncoding::Identity {
|
||||
encoding = ContentEncoding::Identity;
|
||||
resp.headers_mut().remove(CONTENT_ENCODING);
|
||||
}
|
||||
TransferEncoding::eof(buf)
|
||||
} else if resp.chunked() {
|
||||
resp.headers_mut().remove(CONTENT_LENGTH);
|
||||
if version != Version::HTTP_11 {
|
||||
error!("Chunked transfer encoding is forbidden for {:?}", version);
|
||||
}
|
||||
if version == Version::HTTP_2 {
|
||||
resp.headers.remove(TRANSFER_ENCODING);
|
||||
TransferEncoding::eof()
|
||||
resp.headers_mut().remove(TRANSFER_ENCODING);
|
||||
TransferEncoding::eof(buf)
|
||||
} else {
|
||||
resp.headers.insert(
|
||||
resp.headers_mut().insert(
|
||||
TRANSFER_ENCODING, HeaderValue::from_static("chunked"));
|
||||
TransferEncoding::chunked()
|
||||
TransferEncoding::chunked(buf)
|
||||
}
|
||||
} else if let Some(len) = resp.headers().get(CONTENT_LENGTH) {
|
||||
// Content-Length
|
||||
if let Ok(s) = len.to_str() {
|
||||
if let Ok(len) = s.parse::<u64>() {
|
||||
TransferEncoding::length(len)
|
||||
TransferEncoding::length(len, buf)
|
||||
} else {
|
||||
debug!("illegal Content-Length: {:?}", len);
|
||||
TransferEncoding::eof()
|
||||
TransferEncoding::eof(buf)
|
||||
}
|
||||
} else {
|
||||
TransferEncoding::eof()
|
||||
TransferEncoding::eof(buf)
|
||||
}
|
||||
} else {
|
||||
TransferEncoding::eof()
|
||||
TransferEncoding::eof(buf)
|
||||
}
|
||||
}
|
||||
Body::Upgrade(_) | Body::UpgradeContext => {
|
||||
if version == Version::HTTP_2 {
|
||||
error!("Connection upgrade is forbidden for HTTP/2");
|
||||
} else {
|
||||
resp.headers.insert(CONNECTION, HeaderValue::from_static("upgrade"));
|
||||
}
|
||||
if encoding != ContentEncoding::Identity {
|
||||
encoding = ContentEncoding::Identity;
|
||||
resp.headers.remove(CONTENT_ENCODING);
|
||||
}
|
||||
TransferEncoding::eof()
|
||||
}
|
||||
};
|
||||
resp.replace_body(body);
|
||||
|
||||
|
@ -476,22 +479,29 @@ impl PayloadEncoder {
|
|||
|
||||
impl PayloadEncoder {
|
||||
|
||||
#[inline]
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.get_ref().len()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_mut(&mut self) -> &mut BytesMut {
|
||||
self.0.get_mut()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_eof(&self) -> bool {
|
||||
self.0.is_eof()
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(inline_always))]
|
||||
#[inline(always)]
|
||||
pub fn write(&mut self, payload: &[u8]) -> Result<(), io::Error> {
|
||||
self.0.write(payload)
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(inline_always))]
|
||||
#[inline(always)]
|
||||
pub fn write_eof(&mut self) -> Result<(), io::Error> {
|
||||
self.0.write_eof()
|
||||
}
|
||||
|
@ -506,6 +516,7 @@ enum ContentEncoder {
|
|||
|
||||
impl ContentEncoder {
|
||||
|
||||
#[inline]
|
||||
pub fn is_eof(&self) -> bool {
|
||||
match *self {
|
||||
ContentEncoder::Br(ref encoder) =>
|
||||
|
@ -519,34 +530,39 @@ impl ContentEncoder {
|
|||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_ref(&self) -> &BytesMut {
|
||||
match *self {
|
||||
ContentEncoder::Br(ref encoder) =>
|
||||
&encoder.get_ref().buffer,
|
||||
encoder.get_ref().buffer.get_ref(),
|
||||
ContentEncoder::Deflate(ref encoder) =>
|
||||
&encoder.get_ref().buffer,
|
||||
encoder.get_ref().buffer.get_ref(),
|
||||
ContentEncoder::Gzip(ref encoder) =>
|
||||
&encoder.get_ref().buffer,
|
||||
encoder.get_ref().buffer.get_ref(),
|
||||
ContentEncoder::Identity(ref encoder) =>
|
||||
&encoder.buffer,
|
||||
encoder.buffer.get_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_mut(&mut self) -> &mut BytesMut {
|
||||
match *self {
|
||||
ContentEncoder::Br(ref mut encoder) =>
|
||||
&mut encoder.get_mut().buffer,
|
||||
encoder.get_mut().buffer.get_mut(),
|
||||
ContentEncoder::Deflate(ref mut encoder) =>
|
||||
&mut encoder.get_mut().buffer,
|
||||
encoder.get_mut().buffer.get_mut(),
|
||||
ContentEncoder::Gzip(ref mut encoder) =>
|
||||
&mut encoder.get_mut().buffer,
|
||||
encoder.get_mut().buffer.get_mut(),
|
||||
ContentEncoder::Identity(ref mut encoder) =>
|
||||
&mut encoder.buffer,
|
||||
encoder.buffer.get_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(inline_always))]
|
||||
#[inline(always)]
|
||||
pub fn write_eof(&mut self) -> Result<(), io::Error> {
|
||||
let encoder = mem::replace(self, ContentEncoder::Identity(TransferEncoding::eof()));
|
||||
let encoder = mem::replace(
|
||||
self, ContentEncoder::Identity(TransferEncoding::eof(SharedBytes::empty())));
|
||||
|
||||
match encoder {
|
||||
ContentEncoder::Br(encoder) => {
|
||||
|
@ -587,13 +603,14 @@ impl ContentEncoder {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(inline_always))]
|
||||
#[inline(always)]
|
||||
pub fn write(&mut self, data: &[u8]) -> Result<(), io::Error> {
|
||||
match *self {
|
||||
ContentEncoder::Br(ref mut encoder) => {
|
||||
match encoder.write(data) {
|
||||
Ok(_) => {
|
||||
encoder.flush()
|
||||
},
|
||||
Ok(_) =>
|
||||
encoder.flush(),
|
||||
Err(err) => {
|
||||
trace!("Error decoding br encoding: {}", err);
|
||||
Err(err)
|
||||
|
@ -602,20 +619,18 @@ impl ContentEncoder {
|
|||
},
|
||||
ContentEncoder::Gzip(ref mut encoder) => {
|
||||
match encoder.write(data) {
|
||||
Ok(_) => {
|
||||
encoder.flush()
|
||||
},
|
||||
Ok(_) =>
|
||||
encoder.flush(),
|
||||
Err(err) => {
|
||||
trace!("Error decoding br encoding: {}", err);
|
||||
trace!("Error decoding gzip encoding: {}", err);
|
||||
Err(err)
|
||||
},
|
||||
}
|
||||
}
|
||||
ContentEncoder::Deflate(ref mut encoder) => {
|
||||
match encoder.write(data) {
|
||||
Ok(_) => {
|
||||
encoder.flush()
|
||||
},
|
||||
Ok(_) =>
|
||||
encoder.flush(),
|
||||
Err(err) => {
|
||||
trace!("Error decoding deflate encoding: {}", err);
|
||||
Err(err)
|
||||
|
@ -623,7 +638,7 @@ impl ContentEncoder {
|
|||
}
|
||||
}
|
||||
ContentEncoder::Identity(ref mut encoder) => {
|
||||
encoder.write_all(data)?;
|
||||
encoder.encode(data)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -634,7 +649,7 @@ impl ContentEncoder {
|
|||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct TransferEncoding {
|
||||
kind: TransferEncodingKind,
|
||||
buffer: BytesMut,
|
||||
buffer: SharedBytes,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
|
@ -653,27 +668,31 @@ enum TransferEncodingKind {
|
|||
|
||||
impl TransferEncoding {
|
||||
|
||||
pub fn eof() -> TransferEncoding {
|
||||
#[inline]
|
||||
pub fn eof(bytes: SharedBytes) -> TransferEncoding {
|
||||
TransferEncoding {
|
||||
kind: TransferEncodingKind::Eof,
|
||||
buffer: BytesMut::new(),
|
||||
buffer: bytes,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn chunked() -> TransferEncoding {
|
||||
#[inline]
|
||||
pub fn chunked(bytes: SharedBytes) -> TransferEncoding {
|
||||
TransferEncoding {
|
||||
kind: TransferEncodingKind::Chunked(false),
|
||||
buffer: BytesMut::new(),
|
||||
buffer: bytes,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn length(len: u64) -> TransferEncoding {
|
||||
#[inline]
|
||||
pub fn length(len: u64, bytes: SharedBytes) -> TransferEncoding {
|
||||
TransferEncoding {
|
||||
kind: TransferEncodingKind::Length(len),
|
||||
buffer: BytesMut::new(),
|
||||
buffer: bytes,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_eof(&self) -> bool {
|
||||
match self.kind {
|
||||
TransferEncodingKind::Eof => true,
|
||||
|
@ -685,50 +704,51 @@ impl TransferEncoding {
|
|||
}
|
||||
|
||||
/// Encode message. Return `EOF` state of encoder
|
||||
pub fn encode(&mut self, msg: &[u8]) -> bool {
|
||||
#[inline]
|
||||
pub fn encode(&mut self, msg: &[u8]) -> io::Result<bool> {
|
||||
match self.kind {
|
||||
TransferEncodingKind::Eof => {
|
||||
self.buffer.extend(msg);
|
||||
msg.is_empty()
|
||||
self.buffer.get_mut().extend_from_slice(msg);
|
||||
Ok(msg.is_empty())
|
||||
},
|
||||
TransferEncodingKind::Chunked(ref mut eof) => {
|
||||
if *eof {
|
||||
return true;
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if msg.is_empty() {
|
||||
*eof = true;
|
||||
self.buffer.extend(b"0\r\n\r\n");
|
||||
self.buffer.get_mut().extend_from_slice(b"0\r\n\r\n");
|
||||
} else {
|
||||
write!(self.buffer, "{:X}\r\n", msg.len()).unwrap();
|
||||
self.buffer.extend(msg);
|
||||
self.buffer.extend(b"\r\n");
|
||||
write!(self.buffer.get_mut(), "{:X}\r\n", msg.len())
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
self.buffer.get_mut().extend_from_slice(msg);
|
||||
self.buffer.get_mut().extend_from_slice(b"\r\n");
|
||||
}
|
||||
*eof
|
||||
Ok(*eof)
|
||||
},
|
||||
TransferEncodingKind::Length(ref mut remaining) => {
|
||||
if msg.is_empty() {
|
||||
return *remaining == 0
|
||||
return Ok(*remaining == 0)
|
||||
}
|
||||
let max = cmp::min(*remaining, msg.len() as u64);
|
||||
trace!("sized write = {}", max);
|
||||
self.buffer.extend(msg[..max as usize].as_ref());
|
||||
self.buffer.get_mut().extend_from_slice(msg[..max as usize].as_ref());
|
||||
|
||||
*remaining -= max as u64;
|
||||
trace!("encoded {} bytes, remaining = {}", max, remaining);
|
||||
*remaining == 0
|
||||
Ok(*remaining == 0)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode eof. Return `EOF` state of encoder
|
||||
#[inline]
|
||||
pub fn encode_eof(&mut self) {
|
||||
match self.kind {
|
||||
TransferEncodingKind::Eof | TransferEncodingKind::Length(_) => (),
|
||||
TransferEncodingKind::Chunked(ref mut eof) => {
|
||||
if !*eof {
|
||||
*eof = true;
|
||||
self.buffer.extend(b"0\r\n\r\n");
|
||||
self.buffer.get_mut().extend_from_slice(b"0\r\n\r\n");
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -737,11 +757,13 @@ impl TransferEncoding {
|
|||
|
||||
impl io::Write for TransferEncoding {
|
||||
|
||||
#[inline]
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.encode(buf);
|
||||
self.encode(buf)?;
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
@ -813,3 +835,19 @@ impl AcceptEncoding {
|
|||
ContentEncoding::Identity
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_chunked_te() {
|
||||
let bytes = SharedBytes::default();
|
||||
let mut enc = TransferEncoding::chunked(bytes.clone());
|
||||
assert!(!enc.encode(b"test").ok().unwrap());
|
||||
assert!(enc.encode(b"").ok().unwrap());
|
||||
assert_eq!(bytes.get_mut().take().freeze(),
|
||||
Bytes::from_static(b"4\r\ntest\r\n0\r\n\r\n"));
|
||||
}
|
||||
}
|
||||
|
|
273
src/error.rs
273
src/error.rs
|
@ -1,5 +1,5 @@
|
|||
//! Error and Result module
|
||||
use std::{fmt, result};
|
||||
use std::{io, fmt, result};
|
||||
use std::str::Utf8Error;
|
||||
use std::string::FromUtf8Error;
|
||||
use std::io::Error as IoError;
|
||||
|
@ -10,11 +10,13 @@ use std::error::Error as StdError;
|
|||
use cookie;
|
||||
use httparse;
|
||||
use failure::Fail;
|
||||
use futures::Canceled;
|
||||
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;
|
||||
|
||||
// re-exports
|
||||
pub use cookie::{ParseError as CookieParseError};
|
||||
|
@ -28,25 +30,25 @@ use httpcodes::{HTTPBadRequest, HTTPMethodNotAllowed, HTTPExpectationFailed};
|
|||
///
|
||||
/// This typedef is generally used to avoid writing out `actix_web::error::Error` directly and
|
||||
/// is otherwise a direct mapping to `Result`.
|
||||
pub type Result<T> = result::Result<T, Error>;
|
||||
pub type Result<T, E=Error> = result::Result<T, E>;
|
||||
|
||||
/// General purpose actix web error
|
||||
#[derive(Debug)]
|
||||
#[derive(Fail, Debug)]
|
||||
pub struct Error {
|
||||
cause: Box<ErrorResponse>,
|
||||
cause: Box<ResponseError>,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
|
||||
/// Returns a reference to the underlying cause of this Error.
|
||||
// this should return &Fail but needs this https://github.com/rust-lang/rust/issues/5665
|
||||
pub fn cause(&self) -> &ErrorResponse {
|
||||
pub fn cause(&self) -> &ResponseError {
|
||||
self.cause.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Error that can be converted to `HttpResponse`
|
||||
pub trait ErrorResponse: Fail {
|
||||
pub trait ResponseError: Fail {
|
||||
|
||||
/// Create response for error
|
||||
///
|
||||
|
@ -69,8 +71,8 @@ impl From<Error> for HttpResponse {
|
|||
}
|
||||
}
|
||||
|
||||
/// `Error` for any error that implements `ErrorResponse`
|
||||
impl<T: ErrorResponse> From<T> for Error {
|
||||
/// `Error` for any error that implements `ResponseError`
|
||||
impl<T: ResponseError> From<T> for Error {
|
||||
fn from(err: T) -> Error {
|
||||
Error { cause: Box::new(err) }
|
||||
}
|
||||
|
@ -78,31 +80,39 @@ impl<T: ErrorResponse> From<T> for Error {
|
|||
|
||||
/// Default error is `InternalServerError`
|
||||
#[cfg(actix_nightly)]
|
||||
default impl<T: StdError + Sync + Send + 'static> ErrorResponse for T {
|
||||
default impl<T: StdError + Sync + Send + 'static> ResponseError for T {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR, Body::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
/// `InternalServerError` for `JsonError`
|
||||
impl ErrorResponse for JsonError {}
|
||||
impl ResponseError for JsonError {}
|
||||
|
||||
/// Return `InternalServerError` for `HttpError`,
|
||||
/// Response generation can return `HttpError`, so it is internal error
|
||||
impl ErrorResponse for HttpError {}
|
||||
impl ResponseError for HttpError {}
|
||||
|
||||
/// Return `InternalServerError` for `io::Error`
|
||||
impl ErrorResponse for IoError {}
|
||||
impl ResponseError for io::Error {
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
match self.kind() {
|
||||
io::ErrorKind::NotFound =>
|
||||
HttpResponse::new(StatusCode::NOT_FOUND, Body::Empty),
|
||||
io::ErrorKind::PermissionDenied =>
|
||||
HttpResponse::new(StatusCode::FORBIDDEN, Body::Empty),
|
||||
_ =>
|
||||
HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR, Body::Empty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `InternalServerError` for `InvalidHeaderValue`
|
||||
impl ErrorResponse for header::InvalidHeaderValue {}
|
||||
impl ResponseError for header::InvalidHeaderValue {}
|
||||
|
||||
/// Internal error
|
||||
#[derive(Fail, Debug)]
|
||||
#[fail(display="Unexpected task frame")]
|
||||
pub struct UnexpectedTaskFrame;
|
||||
|
||||
impl ErrorResponse for UnexpectedTaskFrame {}
|
||||
/// `InternalServerError` for `futures::Canceled`
|
||||
impl ResponseError for Canceled {}
|
||||
|
||||
/// A set of errors that can occur during parsing HTTP streams
|
||||
#[derive(Fail, Debug)]
|
||||
|
@ -141,7 +151,7 @@ pub enum ParseError {
|
|||
}
|
||||
|
||||
/// Return `BadRequest` for `ParseError`
|
||||
impl ErrorResponse for ParseError {
|
||||
impl ResponseError for ParseError {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::new(StatusCode::BAD_REQUEST, Body::Empty)
|
||||
}
|
||||
|
@ -168,10 +178,8 @@ impl From<FromUtf8Error> for ParseError {
|
|||
impl From<httparse::Error> for ParseError {
|
||||
fn from(err: httparse::Error) -> ParseError {
|
||||
match err {
|
||||
httparse::Error::HeaderName |
|
||||
httparse::Error::HeaderValue |
|
||||
httparse::Error::NewLine |
|
||||
httparse::Error::Token => ParseError::Header,
|
||||
httparse::Error::HeaderName | httparse::Error::HeaderValue |
|
||||
httparse::Error::NewLine | httparse::Error::Token => ParseError::Header,
|
||||
httparse::Error::Status => ParseError::Status,
|
||||
httparse::Error::TooManyHeaders => ParseError::TooLarge,
|
||||
httparse::Error::Version => ParseError::Version,
|
||||
|
@ -188,6 +196,12 @@ pub enum PayloadError {
|
|||
/// Content encoding stream corruption
|
||||
#[fail(display="Can not decode content-encoding.")]
|
||||
EncodingCorrupted,
|
||||
/// A payload reached size limit.
|
||||
#[fail(display="A payload reached size limit.")]
|
||||
Overflow,
|
||||
/// A payload length is unknown.
|
||||
#[fail(display="A payload length is unknown.")]
|
||||
UnknownLength,
|
||||
/// Parse error
|
||||
#[fail(display="{}", _0)]
|
||||
ParseError(#[cause] IoError),
|
||||
|
@ -202,8 +216,11 @@ impl From<IoError> for PayloadError {
|
|||
}
|
||||
}
|
||||
|
||||
/// `InternalServerError` for `PayloadError`
|
||||
impl ResponseError for PayloadError {}
|
||||
|
||||
/// Return `BadRequest` for `cookie::ParseError`
|
||||
impl ErrorResponse for cookie::ParseError {
|
||||
impl ResponseError for cookie::ParseError {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::new(StatusCode::BAD_REQUEST, Body::Empty)
|
||||
}
|
||||
|
@ -217,13 +234,13 @@ pub enum HttpRangeError {
|
|||
InvalidRange,
|
||||
/// Returned if first-byte-pos of all of the byte-range-spec
|
||||
/// values is greater than the content size.
|
||||
/// See https://github.com/golang/go/commit/aa9b3d7
|
||||
/// See `https://github.com/golang/go/commit/aa9b3d7`
|
||||
#[fail(display="First-byte-pos of all of the byte-range-spec values is greater than the content size")]
|
||||
NoOverlap,
|
||||
}
|
||||
|
||||
/// Return `BadRequest` for `HttpRangeError`
|
||||
impl ErrorResponse for HttpRangeError {
|
||||
impl ResponseError for HttpRangeError {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::new(
|
||||
StatusCode::BAD_REQUEST, Body::from("Invalid Range header provided"))
|
||||
|
@ -272,7 +289,7 @@ impl From<PayloadError> for MultipartError {
|
|||
}
|
||||
|
||||
/// Return `BadRequest` for `MultipartError`
|
||||
impl ErrorResponse for MultipartError {
|
||||
impl ResponseError for MultipartError {
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::new(StatusCode::BAD_REQUEST, Body::Empty)
|
||||
|
@ -290,7 +307,7 @@ pub enum ExpectError {
|
|||
UnknownExpect,
|
||||
}
|
||||
|
||||
impl ErrorResponse for ExpectError {
|
||||
impl ResponseError for ExpectError {
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HTTPExpectationFailed.with_body("Unknown Expect")
|
||||
|
@ -320,7 +337,7 @@ pub enum WsHandshakeError {
|
|||
BadWebsocketKey,
|
||||
}
|
||||
|
||||
impl ErrorResponse for WsHandshakeError {
|
||||
impl ResponseError for WsHandshakeError {
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
match *self {
|
||||
|
@ -340,13 +357,13 @@ impl ErrorResponse for WsHandshakeError {
|
|||
WsHandshakeError::UnsupportedVersion =>
|
||||
HTTPBadRequest.with_reason("Unsupported version"),
|
||||
WsHandshakeError::BadWebsocketKey =>
|
||||
HTTPBadRequest.with_reason("Handshake error")
|
||||
HTTPBadRequest.with_reason("Handshake error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of errors that can occur during parsing urlencoded payloads
|
||||
#[derive(Fail, Debug, PartialEq)]
|
||||
#[derive(Fail, Debug)]
|
||||
pub enum UrlencodedError {
|
||||
/// Can not decode chunked transfer encoding
|
||||
#[fail(display="Can not decode chunked transfer encoding")]
|
||||
|
@ -360,16 +377,206 @@ pub enum UrlencodedError {
|
|||
/// Content type error
|
||||
#[fail(display="Content type error")]
|
||||
ContentType,
|
||||
/// Payload error
|
||||
#[fail(display="Error that occur during reading payload")]
|
||||
Payload(PayloadError),
|
||||
}
|
||||
|
||||
/// Return `BadRequest` for `UrlencodedError`
|
||||
impl ErrorResponse for UrlencodedError {
|
||||
impl ResponseError for UrlencodedError {
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::new(StatusCode::BAD_REQUEST, Body::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PayloadError> for UrlencodedError {
|
||||
fn from(err: PayloadError) -> UrlencodedError {
|
||||
UrlencodedError::Payload(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of errors that can occur during parsing json payloads
|
||||
#[derive(Fail, Debug)]
|
||||
pub enum JsonPayloadError {
|
||||
/// Payload size is bigger than 256k
|
||||
#[fail(display="Payload size is bigger than 256k")]
|
||||
Overflow,
|
||||
/// Content type error
|
||||
#[fail(display="Content type error")]
|
||||
ContentType,
|
||||
/// Deserialize error
|
||||
#[fail(display="Json deserialize error")]
|
||||
Deserialize(JsonError),
|
||||
/// Payload error
|
||||
#[fail(display="Error that occur during reading payload")]
|
||||
Payload(PayloadError),
|
||||
}
|
||||
|
||||
/// Return `BadRequest` for `UrlencodedError`
|
||||
impl ResponseError for JsonPayloadError {
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::new(StatusCode::BAD_REQUEST, Body::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PayloadError> for JsonPayloadError {
|
||||
fn from(err: PayloadError) -> JsonPayloadError {
|
||||
JsonPayloadError::Payload(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JsonError> for JsonPayloadError {
|
||||
fn from(err: JsonError) -> JsonPayloadError {
|
||||
JsonPayloadError::Deserialize(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors which can occur when attempting to interpret a segment string as a
|
||||
/// valid path segment.
|
||||
#[derive(Fail, Debug, PartialEq)]
|
||||
pub enum UriSegmentError {
|
||||
/// The segment started with the wrapped invalid character.
|
||||
#[fail(display="The segment started with the wrapped invalid character")]
|
||||
BadStart(char),
|
||||
/// The segment contained the wrapped invalid character.
|
||||
#[fail(display="The segment contained the wrapped invalid character")]
|
||||
BadChar(char),
|
||||
/// The segment ended with the wrapped invalid character.
|
||||
#[fail(display="The segment ended with the wrapped invalid character")]
|
||||
BadEnd(char),
|
||||
}
|
||||
|
||||
/// Return `BadRequest` for `UriSegmentError`
|
||||
impl ResponseError for UriSegmentError {
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::new(StatusCode::BAD_REQUEST, Body::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors which can occur when attempting to generate resource uri.
|
||||
#[derive(Fail, Debug, PartialEq)]
|
||||
pub enum UrlGenerationError {
|
||||
#[fail(display="Resource not found")]
|
||||
ResourceNotFound,
|
||||
#[fail(display="Not all path pattern covered")]
|
||||
NotEnoughElements,
|
||||
#[fail(display="Router is not available")]
|
||||
RouterNotAvailable,
|
||||
#[fail(display="{}", _0)]
|
||||
ParseError(#[cause] UrlParseError),
|
||||
}
|
||||
|
||||
/// `InternalServerError` for `UrlGeneratorError`
|
||||
impl ResponseError for UrlGenerationError {}
|
||||
|
||||
impl From<UrlParseError> for UrlGenerationError {
|
||||
fn from(err: UrlParseError) -> Self {
|
||||
UrlGenerationError::ParseError(err)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! ERROR_WRAP {
|
||||
($type:ty, $status:expr) => {
|
||||
unsafe impl<T> Sync for $type {}
|
||||
unsafe impl<T> Send for $type {}
|
||||
|
||||
impl<T> $type {
|
||||
pub fn cause(&self) -> &T {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: fmt::Debug + 'static> Fail for $type {}
|
||||
impl<T: fmt::Debug + 'static> fmt::Display for $type {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{:?}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ResponseError for $type
|
||||
where T: Send + Sync + fmt::Debug + 'static,
|
||||
{
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::new($status, Body::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper type that can wrap any error and generate *BAD REQUEST* response.
|
||||
///
|
||||
/// In following example any `io::Error` will be converted into "BAD REQUEST" response
|
||||
/// as oposite to *INNTERNAL SERVER ERROR* which is defined by default.
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// # use actix_web::*;
|
||||
/// use actix_web::fs::NamedFile;
|
||||
///
|
||||
/// fn index(req: HttpRequest) -> Result<fs::NamedFile> {
|
||||
/// let f = NamedFile::open("test.txt").map_err(error::ErrorBadRequest)?;
|
||||
/// Ok(f)
|
||||
/// }
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct ErrorBadRequest<T>(pub T);
|
||||
ERROR_WRAP!(ErrorBadRequest<T>, StatusCode::BAD_REQUEST);
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Helper type that can wrap any error and generate *UNAUTHORIZED* response.
|
||||
pub struct ErrorUnauthorized<T>(pub T);
|
||||
ERROR_WRAP!(ErrorUnauthorized<T>, StatusCode::UNAUTHORIZED);
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Helper type that can wrap any error and generate *FORBIDDEN* response.
|
||||
pub struct ErrorForbidden<T>(pub T);
|
||||
ERROR_WRAP!(ErrorForbidden<T>, StatusCode::FORBIDDEN);
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Helper type that can wrap any error and generate *NOT FOUND* response.
|
||||
pub struct ErrorNotFound<T>(pub T);
|
||||
ERROR_WRAP!(ErrorNotFound<T>, StatusCode::NOT_FOUND);
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Helper type that can wrap any error and generate *METHOD NOT ALLOWED* response.
|
||||
pub struct ErrorMethodNotAllowed<T>(pub T);
|
||||
ERROR_WRAP!(ErrorMethodNotAllowed<T>, StatusCode::METHOD_NOT_ALLOWED);
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Helper type that can wrap any error and generate *REQUEST TIMEOUT* response.
|
||||
pub struct ErrorRequestTimeout<T>(pub T);
|
||||
ERROR_WRAP!(ErrorRequestTimeout<T>, StatusCode::REQUEST_TIMEOUT);
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Helper type that can wrap any error and generate *CONFLICT* response.
|
||||
pub struct ErrorConflict<T>(pub T);
|
||||
ERROR_WRAP!(ErrorConflict<T>, StatusCode::CONFLICT);
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Helper type that can wrap any error and generate *GONE* response.
|
||||
pub struct ErrorGone<T>(pub T);
|
||||
ERROR_WRAP!(ErrorGone<T>, StatusCode::GONE);
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Helper type that can wrap any error and generate *PRECONDITION FAILED* response.
|
||||
pub struct ErrorPreconditionFailed<T>(pub T);
|
||||
ERROR_WRAP!(ErrorPreconditionFailed<T>, StatusCode::PRECONDITION_FAILED);
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Helper type that can wrap any error and generate *EXPECTATION FAILED* response.
|
||||
pub struct ErrorExpectationFailed<T>(pub T);
|
||||
ERROR_WRAP!(ErrorExpectationFailed<T>, StatusCode::EXPECTATION_FAILED);
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Helper type that can wrap any error and generate *INTERNAL SERVER ERROR* response.
|
||||
pub struct ErrorInternalServerError<T>(pub T);
|
||||
ERROR_WRAP!(ErrorInternalServerError<T>, StatusCode::INTERNAL_SERVER_ERROR);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::error::Error as StdError;
|
||||
|
|
318
src/fs.rs
Normal file
318
src/fs.rs
Normal file
|
@ -0,0 +1,318 @@
|
|||
//! Static files support.
|
||||
|
||||
// //! TODO: needs to re-implement actual files handling, current impl blocks
|
||||
use std::io;
|
||||
use std::io::Read;
|
||||
use std::fmt::Write;
|
||||
use std::fs::{File, DirEntry};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use mime_guess::get_mime_type;
|
||||
use param::FromParam;
|
||||
use handler::{Handler, Responder};
|
||||
use httprequest::HttpRequest;
|
||||
use httpresponse::HttpResponse;
|
||||
use httpcodes::HTTPOk;
|
||||
|
||||
/// A file with an associated name; responds with the Content-Type based on the
|
||||
/// file extension.
|
||||
#[derive(Debug)]
|
||||
pub struct NamedFile(PathBuf, File);
|
||||
|
||||
impl NamedFile {
|
||||
/// Attempts to open a file in read-only mode.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```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))
|
||||
}
|
||||
|
||||
/// Returns reference to the underlying `File` object.
|
||||
#[inline]
|
||||
pub fn file(&self) -> &File {
|
||||
&self.1
|
||||
}
|
||||
|
||||
/// Retrieve the path of this file.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # 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");
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn path(&self) -> &Path {
|
||||
self.0.as_path()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for NamedFile {
|
||||
type Target = File;
|
||||
|
||||
fn deref(&self) -> &File {
|
||||
&self.1
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for NamedFile {
|
||||
fn deref_mut(&mut self) -> &mut File {
|
||||
&mut self.1
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
if let Some(ext) = self.path().extension() {
|
||||
let mime = get_mime_type(&ext.to_string_lossy());
|
||||
resp.content_type(format!("{}", mime).as_str());
|
||||
}
|
||||
let mut data = Vec::new();
|
||||
let _ = self.1.read_to_end(&mut data);
|
||||
Ok(resp.body(data).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
/// A directory; responds with the generated directory listing.
|
||||
#[derive(Debug)]
|
||||
pub struct Directory{
|
||||
base: PathBuf,
|
||||
path: PathBuf
|
||||
}
|
||||
|
||||
impl Directory {
|
||||
pub fn new(base: PathBuf, path: PathBuf) -> Directory {
|
||||
Directory {
|
||||
base: base,
|
||||
path: path
|
||||
}
|
||||
}
|
||||
|
||||
fn can_list(&self, entry: &io::Result<DirEntry>) -> bool {
|
||||
if let Ok(ref entry) = *entry {
|
||||
if let Some(name) = entry.file_name().to_str() {
|
||||
if name.starts_with('.') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if let Ok(ref md) = entry.metadata() {
|
||||
let ft = md.file_type();
|
||||
return ft.is_dir() || ft.is_file() || ft.is_symlink()
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Responder for Directory {
|
||||
type Item = HttpResponse;
|
||||
type Error = io::Error;
|
||||
|
||||
fn respond_to(self, req: HttpRequest) -> Result<HttpResponse, io::Error> {
|
||||
let index_of = format!("Index of {}", req.path());
|
||||
let mut body = String::new();
|
||||
let base = Path::new(req.path());
|
||||
|
||||
for entry in self.path.read_dir()? {
|
||||
if self.can_list(&entry) {
|
||||
let entry = entry.unwrap();
|
||||
let p = match entry.path().strip_prefix(&self.base) {
|
||||
Ok(p) => base.join(p),
|
||||
Err(_) => continue
|
||||
};
|
||||
// show file url as relative to static path
|
||||
let file_url = format!("{}", p.to_string_lossy());
|
||||
|
||||
// if file is a directory, add '/' to the end of the name
|
||||
if let Ok(metadata) = entry.metadata() {
|
||||
if metadata.is_dir() {
|
||||
let _ = write!(body, "<li><a href=\"{}\">{}/</a></li>",
|
||||
file_url, entry.file_name().to_string_lossy());
|
||||
} else {
|
||||
let _ = write!(body, "<li><a href=\"{}\">{}</a></li>",
|
||||
file_url, entry.file_name().to_string_lossy());
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let html = format!("<html>\
|
||||
<head><title>{}</title></head>\
|
||||
<body><h1>{}</h1>\
|
||||
<ul>\
|
||||
{}\
|
||||
</ul></body>\n</html>", index_of, index_of, body);
|
||||
Ok(HTTPOk.build()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(html).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
/// This enum represents all filesystem elements.
|
||||
pub enum FilesystemElement {
|
||||
File(NamedFile),
|
||||
Directory(Directory),
|
||||
}
|
||||
|
||||
impl Responder for FilesystemElement {
|
||||
type Item = HttpResponse;
|
||||
type Error = io::Error;
|
||||
|
||||
fn respond_to(self, req: HttpRequest) -> Result<HttpResponse, io::Error> {
|
||||
match self {
|
||||
FilesystemElement::File(file) => file.respond_to(req),
|
||||
FilesystemElement::Directory(dir) => dir.respond_to(req),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Static files handling
|
||||
///
|
||||
/// `StaticFile` handler must be registered with `Application::handler()` method,
|
||||
/// because `StaticFile` handler requires access sub-path information.
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// use actix_web::{fs, Application};
|
||||
///
|
||||
/// fn main() {
|
||||
/// let app = Application::new()
|
||||
/// .handler("/static", fs::StaticFiles::new(".", true))
|
||||
/// .finish();
|
||||
/// }
|
||||
/// ```
|
||||
pub struct StaticFiles {
|
||||
directory: PathBuf,
|
||||
accessible: bool,
|
||||
show_index: bool,
|
||||
_chunk_size: usize,
|
||||
_follow_symlinks: bool,
|
||||
}
|
||||
|
||||
impl StaticFiles {
|
||||
/// Create new `StaticFiles` instance
|
||||
///
|
||||
/// `dir` - base directory
|
||||
///
|
||||
/// `index` - show index for directory
|
||||
pub fn new<D: Into<PathBuf>>(dir: D, index: bool) -> StaticFiles {
|
||||
let dir = dir.into();
|
||||
|
||||
let (dir, access) = match dir.canonicalize() {
|
||||
Ok(dir) => {
|
||||
if dir.is_dir() {
|
||||
(dir, true)
|
||||
} else {
|
||||
warn!("Is not directory `{:?}`", dir);
|
||||
(dir, false)
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
warn!("Static files directory `{:?}` error: {}", dir, err);
|
||||
(dir, false)
|
||||
}
|
||||
};
|
||||
|
||||
StaticFiles {
|
||||
directory: dir,
|
||||
accessible: access,
|
||||
show_index: index,
|
||||
_chunk_size: 0,
|
||||
_follow_symlinks: false,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl<S> Handler<S> for StaticFiles {
|
||||
type Result = Result<FilesystemElement, io::Error>;
|
||||
|
||||
fn handle(&mut self, req: HttpRequest<S>) -> Self::Result {
|
||||
if !self.accessible {
|
||||
Err(io::Error::new(io::ErrorKind::NotFound, "not found"))
|
||||
} else {
|
||||
let path = if let Some(path) = req.match_info().get("tail") {
|
||||
path
|
||||
} else {
|
||||
return Err(io::Error::new(io::ErrorKind::NotFound, "not found"))
|
||||
};
|
||||
|
||||
let relpath = PathBuf::from_param(path)
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::NotFound, "not found"))?;
|
||||
|
||||
// full filepath
|
||||
let path = self.directory.join(&relpath).canonicalize()?;
|
||||
|
||||
if path.is_dir() {
|
||||
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)?))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use http::header;
|
||||
|
||||
#[test]
|
||||
fn test_named_file() {
|
||||
assert!(NamedFile::open("test--").is_err());
|
||||
let mut file = NamedFile::open("Cargo.toml").unwrap();
|
||||
{ file.file();
|
||||
let _f: &File = &file; }
|
||||
{ let _f: &mut File = &mut file; }
|
||||
|
||||
let resp = file.respond_to(HttpRequest::default()).unwrap();
|
||||
assert_eq!(resp.headers().get(header::CONTENT_TYPE).unwrap(), "text/x-toml")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_static_files() {
|
||||
let mut st = StaticFiles::new(".", true);
|
||||
st.accessible = false;
|
||||
assert!(st.handle(HttpRequest::default()).is_err());
|
||||
|
||||
st.accessible = true;
|
||||
st.show_index = false;
|
||||
assert!(st.handle(HttpRequest::default()).is_err());
|
||||
|
||||
let mut req = HttpRequest::default();
|
||||
req.match_info_mut().add("tail", "");
|
||||
|
||||
st.show_index = true;
|
||||
let resp = st.handle(req).respond_to(HttpRequest::default()).unwrap();
|
||||
assert_eq!(resp.headers().get(header::CONTENT_TYPE).unwrap(), "text/html; charset=utf-8");
|
||||
assert!(resp.body().is_binary());
|
||||
assert!(format!("{:?}", resp.body()).contains("README.md"));
|
||||
}
|
||||
}
|
214
src/h1writer.rs
214
src/h1writer.rs
|
@ -1,14 +1,15 @@
|
|||
use std::io;
|
||||
use std::fmt::Write;
|
||||
use bytes::BufMut;
|
||||
use futures::{Async, Poll};
|
||||
use tokio_io::AsyncWrite;
|
||||
use http::{Version, StatusCode};
|
||||
use http::header::{HeaderValue, CONNECTION, CONTENT_TYPE, DATE};
|
||||
use http::Version;
|
||||
use http::header::{HeaderValue, CONNECTION, DATE};
|
||||
|
||||
use date;
|
||||
use helpers;
|
||||
use body::Body;
|
||||
use helpers::SharedBytes;
|
||||
use encoding::PayloadEncoder;
|
||||
use httprequest::HttpRequest;
|
||||
use httprequest::HttpMessage;
|
||||
use httpresponse::HttpResponse;
|
||||
|
||||
const AVERAGE_HEADER_SIZE: usize = 30; // totally scientific
|
||||
|
@ -16,58 +17,67 @@ const MAX_WRITE_BUFFER_SIZE: usize = 65_536; // max buffer size 64k
|
|||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum WriterState {
|
||||
pub enum WriterState {
|
||||
Done,
|
||||
Pause,
|
||||
}
|
||||
|
||||
/// Send stream
|
||||
pub(crate) trait Writer {
|
||||
pub trait Writer {
|
||||
fn written(&self) -> u64;
|
||||
|
||||
fn start(&mut self, req: &mut HttpRequest, resp: &mut HttpResponse)
|
||||
fn start(&mut self, req: &mut HttpMessage, resp: &mut HttpResponse)
|
||||
-> Result<WriterState, io::Error>;
|
||||
|
||||
fn write(&mut self, payload: &[u8]) -> Result<WriterState, io::Error>;
|
||||
|
||||
fn write_eof(&mut self) -> Result<WriterState, io::Error>;
|
||||
|
||||
fn poll_complete(&mut self) -> Poll<(), io::Error>;
|
||||
fn poll_completed(&mut self, shutdown: bool) -> Poll<(), io::Error>;
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
struct Flags: u8 {
|
||||
const STARTED = 0b0000_0001;
|
||||
const UPGRADE = 0b0000_0010;
|
||||
const KEEPALIVE = 0b0000_0100;
|
||||
const DISCONNECTED = 0b0000_1000;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct H1Writer<T: AsyncWrite> {
|
||||
stream: Option<T>,
|
||||
started: bool,
|
||||
flags: Flags,
|
||||
stream: T,
|
||||
encoder: PayloadEncoder,
|
||||
upgrade: bool,
|
||||
keepalive: bool,
|
||||
disconnected: bool,
|
||||
written: u64,
|
||||
headers_size: u64,
|
||||
headers_size: u32,
|
||||
buffer: SharedBytes,
|
||||
}
|
||||
|
||||
impl<T: AsyncWrite> H1Writer<T> {
|
||||
|
||||
pub fn new(stream: T) -> H1Writer<T> {
|
||||
pub fn new(stream: T, buf: SharedBytes) -> H1Writer<T> {
|
||||
H1Writer {
|
||||
stream: Some(stream),
|
||||
started: false,
|
||||
encoder: PayloadEncoder::default(),
|
||||
upgrade: false,
|
||||
keepalive: false,
|
||||
disconnected: false,
|
||||
flags: Flags::empty(),
|
||||
stream: stream,
|
||||
encoder: PayloadEncoder::empty(buf.clone()),
|
||||
written: 0,
|
||||
headers_size: 0,
|
||||
buffer: buf,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self) -> &mut T {
|
||||
self.stream.as_mut().unwrap()
|
||||
&mut self.stream
|
||||
}
|
||||
|
||||
pub fn unwrap(&mut self) -> T {
|
||||
self.stream.take().unwrap()
|
||||
pub fn reset(&mut self) {
|
||||
self.written = 0;
|
||||
self.flags = Flags::empty();
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> T {
|
||||
self.stream
|
||||
}
|
||||
|
||||
pub fn disconnected(&mut self) {
|
||||
|
@ -75,28 +85,25 @@ impl<T: AsyncWrite> H1Writer<T> {
|
|||
}
|
||||
|
||||
pub fn keepalive(&self) -> bool {
|
||||
self.keepalive && !self.upgrade
|
||||
self.flags.contains(Flags::KEEPALIVE) && !self.flags.contains(Flags::UPGRADE)
|
||||
}
|
||||
|
||||
fn write_to_stream(&mut self) -> Result<WriterState, io::Error> {
|
||||
let buffer = self.encoder.get_mut();
|
||||
|
||||
if let Some(ref mut stream) = self.stream {
|
||||
while !buffer.is_empty() {
|
||||
match stream.write(buffer.as_ref()) {
|
||||
Ok(n) => {
|
||||
buffer.split_to(n);
|
||||
self.written += n as u64;
|
||||
},
|
||||
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
|
||||
if buffer.len() > MAX_WRITE_BUFFER_SIZE {
|
||||
return Ok(WriterState::Pause)
|
||||
} else {
|
||||
return Ok(WriterState::Done)
|
||||
}
|
||||
while !buffer.is_empty() {
|
||||
match self.stream.write(buffer.as_ref()) {
|
||||
Ok(n) => {
|
||||
let _ = buffer.split_to(n);
|
||||
},
|
||||
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
|
||||
if buffer.len() > MAX_WRITE_BUFFER_SIZE {
|
||||
return Ok(WriterState::Pause)
|
||||
} else {
|
||||
return Ok(WriterState::Done)
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
Ok(WriterState::Done)
|
||||
|
@ -106,97 +113,93 @@ impl<T: AsyncWrite> H1Writer<T> {
|
|||
impl<T: AsyncWrite> Writer for H1Writer<T> {
|
||||
|
||||
fn written(&self) -> u64 {
|
||||
if self.written > self.headers_size {
|
||||
self.written - self.headers_size
|
||||
} else {
|
||||
0
|
||||
}
|
||||
self.written
|
||||
}
|
||||
|
||||
fn start(&mut self, req: &mut HttpRequest, msg: &mut HttpResponse)
|
||||
fn start(&mut self, req: &mut HttpMessage, msg: &mut HttpResponse)
|
||||
-> Result<WriterState, io::Error>
|
||||
{
|
||||
trace!("Prepare response with status: {:?}", msg.status);
|
||||
|
||||
// prepare task
|
||||
self.started = true;
|
||||
self.encoder = PayloadEncoder::new(req, msg);
|
||||
self.keepalive = msg.keep_alive().unwrap_or_else(|| req.keep_alive());
|
||||
self.flags.insert(Flags::STARTED);
|
||||
self.encoder = PayloadEncoder::new(self.buffer.clone(), req, msg);
|
||||
if msg.keep_alive().unwrap_or_else(|| req.keep_alive()) {
|
||||
self.flags.insert(Flags::KEEPALIVE);
|
||||
}
|
||||
|
||||
// Connection upgrade
|
||||
let version = msg.version().unwrap_or_else(|| req.version());
|
||||
let version = msg.version().unwrap_or_else(|| req.version);
|
||||
if msg.upgrade() {
|
||||
msg.headers.insert(CONNECTION, HeaderValue::from_static("upgrade"));
|
||||
msg.headers_mut().insert(CONNECTION, HeaderValue::from_static("upgrade"));
|
||||
}
|
||||
// keep-alive
|
||||
else if self.keepalive {
|
||||
else if self.flags.contains(Flags::KEEPALIVE) {
|
||||
if version < Version::HTTP_11 {
|
||||
msg.headers.insert(CONNECTION, HeaderValue::from_static("keep-alive"));
|
||||
msg.headers_mut().insert(CONNECTION, HeaderValue::from_static("keep-alive"));
|
||||
}
|
||||
} else if version >= Version::HTTP_11 {
|
||||
msg.headers.insert(CONNECTION, HeaderValue::from_static("close"));
|
||||
msg.headers_mut().insert(CONNECTION, HeaderValue::from_static("close"));
|
||||
}
|
||||
let body = msg.replace_body(Body::Empty);
|
||||
|
||||
// render message
|
||||
{
|
||||
let buffer = self.encoder.get_mut();
|
||||
if let Body::Binary(ref bytes) = *msg.body() {
|
||||
buffer.reserve(100 + msg.headers.len() * AVERAGE_HEADER_SIZE + bytes.len());
|
||||
let mut buffer = self.encoder.get_mut();
|
||||
if let Body::Binary(ref bytes) = body {
|
||||
buffer.reserve(256 + msg.headers().len() * AVERAGE_HEADER_SIZE + bytes.len());
|
||||
} else {
|
||||
buffer.reserve(100 + msg.headers.len() * AVERAGE_HEADER_SIZE);
|
||||
buffer.reserve(256 + msg.headers().len() * AVERAGE_HEADER_SIZE);
|
||||
}
|
||||
|
||||
if version == Version::HTTP_11 && msg.status == StatusCode::OK {
|
||||
buffer.extend(b"HTTP/1.1 200 OK\r\n");
|
||||
// status line
|
||||
helpers::write_status_line(version, msg.status().as_u16(), &mut buffer);
|
||||
buffer.extend_from_slice(msg.reason().as_bytes());
|
||||
|
||||
match body {
|
||||
Body::Empty =>
|
||||
buffer.extend_from_slice(b"\r\ncontent-length: 0\r\n"),
|
||||
Body::Binary(ref bytes) =>
|
||||
helpers::write_content_length(bytes.len(), &mut buffer),
|
||||
_ =>
|
||||
buffer.extend_from_slice(b"\r\n"),
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
// using helpers::date is quite a lot faster
|
||||
if !msg.headers().contains_key(DATE) {
|
||||
helpers::date(&mut buffer);
|
||||
} else {
|
||||
let _ = write!(buffer, "{:?} {}\r\n", version, msg.status);
|
||||
// msg eof
|
||||
buffer.extend_from_slice(b"\r\n");
|
||||
}
|
||||
for (key, value) in &msg.headers {
|
||||
let t: &[u8] = key.as_ref();
|
||||
buffer.extend(t);
|
||||
buffer.extend(b": ");
|
||||
buffer.extend(value.as_ref());
|
||||
buffer.extend(b"\r\n");
|
||||
}
|
||||
|
||||
// using http::h1::date is quite a lot faster than generating
|
||||
// a unique Date header each time like req/s goes up about 10%
|
||||
if !msg.headers.contains_key(DATE) {
|
||||
buffer.reserve(date::DATE_VALUE_LENGTH + 8);
|
||||
buffer.extend(b"Date: ");
|
||||
let mut bytes = [0u8; 29];
|
||||
date::extend(&mut bytes[..]);
|
||||
buffer.extend(&bytes);
|
||||
buffer.extend(b"\r\n");
|
||||
}
|
||||
|
||||
// default content-type
|
||||
if !msg.headers.contains_key(CONTENT_TYPE) {
|
||||
buffer.extend(b"ContentType: application/octet-stream\r\n".as_ref());
|
||||
}
|
||||
|
||||
// msg eof
|
||||
buffer.extend(b"\r\n");
|
||||
self.headers_size = buffer.len() as u64;
|
||||
self.headers_size = buffer.len() as u32;
|
||||
}
|
||||
|
||||
trace!("Response: {:?}", msg);
|
||||
|
||||
if msg.body().is_binary() {
|
||||
let body = msg.replace_body(Body::Empty);
|
||||
if let Body::Binary(bytes) = body {
|
||||
self.encoder.write(bytes.as_ref())?;
|
||||
return Ok(WriterState::Done)
|
||||
}
|
||||
if let Body::Binary(bytes) = body {
|
||||
self.written = bytes.len() as u64;
|
||||
self.encoder.write(bytes.as_ref())?;
|
||||
} else {
|
||||
msg.replace_body(body);
|
||||
}
|
||||
Ok(WriterState::Done)
|
||||
}
|
||||
|
||||
fn write(&mut self, payload: &[u8]) -> Result<WriterState, io::Error> {
|
||||
if !self.disconnected {
|
||||
if self.started {
|
||||
self.written += payload.len() as u64;
|
||||
if !self.flags.contains(Flags::DISCONNECTED) {
|
||||
if self.flags.contains(Flags::STARTED) {
|
||||
// TODO: add warning, write after EOF
|
||||
self.encoder.write(payload)?;
|
||||
return Ok(WriterState::Done)
|
||||
} else {
|
||||
// might be response to EXCEPT
|
||||
self.encoder.get_mut().extend_from_slice(payload)
|
||||
|
@ -214,7 +217,6 @@ impl<T: AsyncWrite> Writer for H1Writer<T> {
|
|||
self.encoder.write_eof()?;
|
||||
|
||||
if !self.encoder.is_eof() {
|
||||
//debug!("last payload item, but it is not EOF ");
|
||||
Err(io::Error::new(io::ErrorKind::Other,
|
||||
"Last payload item, but eof is not reached"))
|
||||
} else if self.encoder.len() > MAX_WRITE_BUFFER_SIZE {
|
||||
|
@ -224,9 +226,15 @@ impl<T: AsyncWrite> Writer for H1Writer<T> {
|
|||
}
|
||||
}
|
||||
|
||||
fn poll_complete(&mut self) -> Poll<(), io::Error> {
|
||||
fn poll_completed(&mut self, shutdown: bool) -> Poll<(), io::Error> {
|
||||
match self.write_to_stream() {
|
||||
Ok(WriterState::Done) => Ok(Async::Ready(())),
|
||||
Ok(WriterState::Done) => {
|
||||
if shutdown {
|
||||
self.stream.shutdown()
|
||||
} else {
|
||||
Ok(Async::Ready(()))
|
||||
}
|
||||
},
|
||||
Ok(WriterState::Pause) => Ok(Async::NotReady),
|
||||
Err(err) => Err(err)
|
||||
}
|
||||
|
|
142
src/h2.rs
142
src/h2.rs
|
@ -8,7 +8,7 @@ use std::collections::VecDeque;
|
|||
use actix::Arbiter;
|
||||
use http::request::Parts;
|
||||
use http2::{Reason, RecvStream};
|
||||
use http2::server::{Server, Handshake, Respond};
|
||||
use http2::server::{self, Connection, Handshake, SendResponse};
|
||||
use bytes::{Buf, Bytes};
|
||||
use futures::{Async, Poll, Future, Stream};
|
||||
use tokio_io::{AsyncRead, AsyncWrite};
|
||||
|
@ -16,29 +16,35 @@ use tokio_core::reactor::Timeout;
|
|||
|
||||
use pipeline::Pipeline;
|
||||
use h2writer::H2Writer;
|
||||
use channel::HttpHandler;
|
||||
use worker::WorkerSettings;
|
||||
use channel::{HttpHandler, HttpHandlerTask};
|
||||
use error::PayloadError;
|
||||
use encoding::PayloadType;
|
||||
use httpcodes::HTTPNotFound;
|
||||
use httprequest::HttpRequest;
|
||||
use payload::{Payload, PayloadWriter};
|
||||
|
||||
const KEEPALIVE_PERIOD: u64 = 15; // seconds
|
||||
bitflags! {
|
||||
struct Flags: u8 {
|
||||
const DISCONNECTED = 0b0000_0010;
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP/2 Transport
|
||||
pub(crate) struct Http2<T, H>
|
||||
where T: AsyncRead + AsyncWrite + 'static, H: 'static
|
||||
{
|
||||
router: Rc<Vec<H>>,
|
||||
flags: Flags,
|
||||
settings: Rc<WorkerSettings<H>>,
|
||||
addr: Option<SocketAddr>,
|
||||
state: State<IoWrapper<T>>,
|
||||
disconnected: bool,
|
||||
tasks: VecDeque<Entry>,
|
||||
keepalive_timer: Option<Timeout>,
|
||||
}
|
||||
|
||||
enum State<T: AsyncRead + AsyncWrite> {
|
||||
Handshake(Handshake<T, Bytes>),
|
||||
Server(Server<T, Bytes>),
|
||||
Server(Connection<T, Bytes>),
|
||||
Empty,
|
||||
}
|
||||
|
||||
|
@ -46,17 +52,28 @@ impl<T, H> Http2<T, H>
|
|||
where T: AsyncRead + AsyncWrite + 'static,
|
||||
H: HttpHandler + 'static
|
||||
{
|
||||
pub fn new(stream: T, addr: Option<SocketAddr>, router: Rc<Vec<H>>, buf: Bytes) -> Self {
|
||||
Http2{ router: router,
|
||||
pub fn new(h: Rc<WorkerSettings<H>>, io: T, addr: Option<SocketAddr>, buf: Bytes) -> Self
|
||||
{
|
||||
Http2{ flags: Flags::empty(),
|
||||
settings: h,
|
||||
addr: addr,
|
||||
disconnected: false,
|
||||
tasks: VecDeque::new(),
|
||||
state: State::Handshake(
|
||||
Server::handshake(IoWrapper{unread: Some(buf), inner: stream})),
|
||||
server::handshake(IoWrapper{unread: Some(buf), inner: io})),
|
||||
keepalive_timer: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn shutdown(&mut self) {
|
||||
self.state = State::Empty;
|
||||
self.tasks.clear();
|
||||
self.keepalive_timer.take();
|
||||
}
|
||||
|
||||
pub fn settings(&self) -> &WorkerSettings<H> {
|
||||
self.settings.as_ref()
|
||||
}
|
||||
|
||||
pub fn poll(&mut self) -> Poll<(), ()> {
|
||||
// server
|
||||
if let State::Server(ref mut server) = self.state {
|
||||
|
@ -80,33 +97,33 @@ impl<T, H> Http2<T, H>
|
|||
// read payload
|
||||
item.poll_payload();
|
||||
|
||||
if !item.eof {
|
||||
if !item.flags.contains(EntryFlags::EOF) {
|
||||
match item.task.poll_io(&mut item.stream) {
|
||||
Ok(Async::Ready(ready)) => {
|
||||
item.eof = true;
|
||||
item.flags.insert(EntryFlags::EOF);
|
||||
if ready {
|
||||
item.finished = true;
|
||||
item.flags.insert(EntryFlags::FINISHED);
|
||||
}
|
||||
not_ready = false;
|
||||
},
|
||||
Ok(Async::NotReady) => (),
|
||||
Err(err) => {
|
||||
error!("Unhandled error: {}", err);
|
||||
item.eof = true;
|
||||
item.error = true;
|
||||
item.flags.insert(EntryFlags::EOF);
|
||||
item.flags.insert(EntryFlags::ERROR);
|
||||
item.stream.reset(Reason::INTERNAL_ERROR);
|
||||
}
|
||||
}
|
||||
} else if !item.finished {
|
||||
} else if !item.flags.contains(EntryFlags::FINISHED) {
|
||||
match item.task.poll() {
|
||||
Ok(Async::NotReady) => (),
|
||||
Ok(Async::Ready(_)) => {
|
||||
not_ready = false;
|
||||
item.finished = true;
|
||||
item.flags.insert(EntryFlags::FINISHED);
|
||||
},
|
||||
Err(err) => {
|
||||
item.error = true;
|
||||
item.finished = true;
|
||||
item.flags.insert(EntryFlags::ERROR);
|
||||
item.flags.insert(EntryFlags::FINISHED);
|
||||
error!("Unhandled error: {}", err);
|
||||
}
|
||||
}
|
||||
|
@ -115,7 +132,10 @@ impl<T, H> Http2<T, H>
|
|||
|
||||
// cleanup finished tasks
|
||||
while !self.tasks.is_empty() {
|
||||
if self.tasks[0].eof && self.tasks[0].finished || self.tasks[0].error {
|
||||
if self.tasks[0].flags.contains(EntryFlags::EOF) &&
|
||||
self.tasks[0].flags.contains(EntryFlags::FINISHED) ||
|
||||
self.tasks[0].flags.contains(EntryFlags::ERROR)
|
||||
{
|
||||
self.tasks.pop_front();
|
||||
} else {
|
||||
break
|
||||
|
@ -123,11 +143,11 @@ impl<T, H> Http2<T, H>
|
|||
}
|
||||
|
||||
// get request
|
||||
if !self.disconnected {
|
||||
if !self.flags.contains(Flags::DISCONNECTED) {
|
||||
match server.poll() {
|
||||
Ok(Async::Ready(None)) => {
|
||||
not_ready = false;
|
||||
self.disconnected = true;
|
||||
self.flags.insert(Flags::DISCONNECTED);
|
||||
for entry in &mut self.tasks {
|
||||
entry.task.disconnected()
|
||||
}
|
||||
|
@ -140,23 +160,34 @@ impl<T, H> Http2<T, H>
|
|||
self.keepalive_timer.take();
|
||||
|
||||
self.tasks.push_back(
|
||||
Entry::new(parts, body, resp, self.addr, &self.router));
|
||||
Entry::new(parts, body, resp, self.addr, &self.settings));
|
||||
}
|
||||
Ok(Async::NotReady) => {
|
||||
// start keep-alive timer
|
||||
if self.tasks.is_empty() && self.keepalive_timer.is_none() {
|
||||
trace!("Start keep-alive timer");
|
||||
let mut timeout = Timeout::new(
|
||||
Duration::new(KEEPALIVE_PERIOD, 0),
|
||||
Arbiter::handle()).unwrap();
|
||||
// register timeout
|
||||
let _ = timeout.poll();
|
||||
self.keepalive_timer = Some(timeout);
|
||||
if self.tasks.is_empty() {
|
||||
if self.settings.keep_alive_enabled() {
|
||||
let keep_alive = self.settings.keep_alive();
|
||||
if keep_alive > 0 && self.keepalive_timer.is_none() {
|
||||
trace!("Start keep-alive timer");
|
||||
let mut timeout = Timeout::new(
|
||||
Duration::new(keep_alive, 0),
|
||||
Arbiter::handle()).unwrap();
|
||||
// register timeout
|
||||
let _ = timeout.poll();
|
||||
self.keepalive_timer = Some(timeout);
|
||||
}
|
||||
} else {
|
||||
// keep-alive disable, drop connection
|
||||
return Ok(Async::Ready(()))
|
||||
}
|
||||
} else {
|
||||
// keep-alive unset, rely on operating system
|
||||
return Ok(Async::NotReady)
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
trace!("Connection error: {}", err);
|
||||
self.disconnected = true;
|
||||
self.flags.insert(Flags::DISCONNECTED);
|
||||
for entry in &mut self.tasks {
|
||||
entry.task.disconnected()
|
||||
}
|
||||
|
@ -166,7 +197,7 @@ impl<T, H> Http2<T, H>
|
|||
}
|
||||
|
||||
if not_ready {
|
||||
if self.tasks.is_empty() && self.disconnected {
|
||||
if self.tasks.is_empty() && self.flags.contains(Flags::DISCONNECTED) {
|
||||
return Ok(Async::Ready(()))
|
||||
} else {
|
||||
return Ok(Async::NotReady)
|
||||
|
@ -196,41 +227,51 @@ impl<T, H> Http2<T, H>
|
|||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
struct EntryFlags: u8 {
|
||||
const EOF = 0b0000_0001;
|
||||
const REOF = 0b0000_0010;
|
||||
const ERROR = 0b0000_0100;
|
||||
const FINISHED = 0b0000_1000;
|
||||
}
|
||||
}
|
||||
|
||||
struct Entry {
|
||||
task: Pipeline,
|
||||
task: Box<HttpHandlerTask>,
|
||||
payload: PayloadType,
|
||||
recv: RecvStream,
|
||||
stream: H2Writer,
|
||||
eof: bool,
|
||||
error: bool,
|
||||
finished: bool,
|
||||
reof: bool,
|
||||
capacity: usize,
|
||||
flags: EntryFlags,
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
fn new<H>(parts: Parts,
|
||||
recv: RecvStream,
|
||||
resp: Respond<Bytes>,
|
||||
resp: SendResponse<Bytes>,
|
||||
addr: Option<SocketAddr>,
|
||||
router: &Rc<Vec<H>>) -> Entry
|
||||
settings: &Rc<WorkerSettings<H>>) -> Entry
|
||||
where H: HttpHandler + 'static
|
||||
{
|
||||
// Payload and Content-Encoding
|
||||
let (psender, payload) = Payload::new(false);
|
||||
|
||||
let mut req = HttpRequest::new(
|
||||
parts.method, parts.uri, parts.version, parts.headers, payload);
|
||||
let msg = settings.get_http_message();
|
||||
msg.get_mut().uri = parts.uri;
|
||||
msg.get_mut().method = parts.method;
|
||||
msg.get_mut().version = parts.version;
|
||||
msg.get_mut().headers = parts.headers;
|
||||
msg.get_mut().payload = Some(payload);
|
||||
msg.get_mut().addr = addr;
|
||||
|
||||
// set remote addr
|
||||
req.set_remove_addr(addr);
|
||||
let mut req = HttpRequest::from_message(msg);
|
||||
|
||||
// Payload sender
|
||||
let psender = PayloadType::new(req.headers(), psender);
|
||||
|
||||
// start request processing
|
||||
let mut task = None;
|
||||
for h in router.iter() {
|
||||
for h in settings.handlers().iter_mut() {
|
||||
req = match h.handle(req) {
|
||||
Ok(t) => {
|
||||
task = Some(t);
|
||||
|
@ -243,23 +284,20 @@ impl Entry {
|
|||
Entry {task: task.unwrap_or_else(|| Pipeline::error(HTTPNotFound)),
|
||||
payload: psender,
|
||||
recv: recv,
|
||||
stream: H2Writer::new(resp),
|
||||
eof: false,
|
||||
error: false,
|
||||
finished: false,
|
||||
reof: false,
|
||||
stream: H2Writer::new(resp, settings.get_shared_bytes()),
|
||||
flags: EntryFlags::empty(),
|
||||
capacity: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_payload(&mut self) {
|
||||
if !self.reof {
|
||||
if !self.flags.contains(EntryFlags::REOF) {
|
||||
match self.recv.poll() {
|
||||
Ok(Async::Ready(Some(chunk))) => {
|
||||
self.payload.feed_data(chunk);
|
||||
},
|
||||
Ok(Async::Ready(None)) => {
|
||||
self.reof = true;
|
||||
self.flags.insert(EntryFlags::REOF);
|
||||
},
|
||||
Ok(Async::NotReady) => (),
|
||||
Err(err) => {
|
||||
|
|
126
src/h2writer.rs
126
src/h2writer.rs
|
@ -1,43 +1,49 @@
|
|||
use std::{io, cmp};
|
||||
use bytes::Bytes;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures::{Async, Poll};
|
||||
use http2::{Reason, SendStream};
|
||||
use http2::server::Respond;
|
||||
use http2::server::SendResponse;
|
||||
use http::{Version, HttpTryFrom, Response};
|
||||
use http::header::{HeaderValue, CONNECTION, CONTENT_TYPE, TRANSFER_ENCODING, DATE};
|
||||
use http::header::{HeaderValue, CONNECTION, TRANSFER_ENCODING, DATE, CONTENT_LENGTH};
|
||||
|
||||
use date;
|
||||
use helpers;
|
||||
use body::Body;
|
||||
use helpers::SharedBytes;
|
||||
use encoding::PayloadEncoder;
|
||||
use httprequest::HttpRequest;
|
||||
use httprequest::HttpMessage;
|
||||
use httpresponse::HttpResponse;
|
||||
use h1writer::{Writer, WriterState};
|
||||
|
||||
const CHUNK_SIZE: usize = 16_384;
|
||||
const MAX_WRITE_BUFFER_SIZE: usize = 65_536; // max buffer size 64k
|
||||
|
||||
bitflags! {
|
||||
struct Flags: u8 {
|
||||
const STARTED = 0b0000_0001;
|
||||
const DISCONNECTED = 0b0000_0010;
|
||||
const EOF = 0b0000_0100;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct H2Writer {
|
||||
respond: Respond<Bytes>,
|
||||
respond: SendResponse<Bytes>,
|
||||
stream: Option<SendStream<Bytes>>,
|
||||
started: bool,
|
||||
encoder: PayloadEncoder,
|
||||
disconnected: bool,
|
||||
eof: bool,
|
||||
flags: Flags,
|
||||
written: u64,
|
||||
buffer: SharedBytes,
|
||||
}
|
||||
|
||||
impl H2Writer {
|
||||
|
||||
pub fn new(respond: Respond<Bytes>) -> H2Writer {
|
||||
pub fn new(respond: SendResponse<Bytes>, buf: SharedBytes) -> H2Writer {
|
||||
H2Writer {
|
||||
respond: respond,
|
||||
stream: None,
|
||||
started: false,
|
||||
encoder: PayloadEncoder::default(),
|
||||
disconnected: false,
|
||||
eof: true,
|
||||
encoder: PayloadEncoder::empty(buf.clone()),
|
||||
flags: Flags::empty(),
|
||||
written: 0,
|
||||
buffer: buf,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,7 +54,7 @@ impl H2Writer {
|
|||
}
|
||||
|
||||
fn write_to_stream(&mut self) -> Result<WriterState, io::Error> {
|
||||
if !self.started {
|
||||
if !self.flags.contains(Flags::STARTED) {
|
||||
return Ok(WriterState::Done)
|
||||
}
|
||||
|
||||
|
@ -56,7 +62,7 @@ impl H2Writer {
|
|||
let buffer = self.encoder.get_mut();
|
||||
|
||||
if buffer.is_empty() {
|
||||
if self.eof {
|
||||
if self.flags.contains(Flags::EOF) {
|
||||
let _ = stream.send_data(Bytes::new(), true);
|
||||
}
|
||||
return Ok(WriterState::Done)
|
||||
|
@ -77,7 +83,7 @@ impl H2Writer {
|
|||
Ok(Async::Ready(Some(cap))) => {
|
||||
let len = buffer.len();
|
||||
let bytes = buffer.split_to(cmp::min(cap, len));
|
||||
let eof = buffer.is_empty() && self.eof;
|
||||
let eof = buffer.is_empty() && self.flags.contains(Flags::EOF);
|
||||
self.written += bytes.len() as u64;
|
||||
|
||||
if let Err(err) = stream.send_data(bytes.freeze(), eof) {
|
||||
|
@ -86,7 +92,7 @@ impl H2Writer {
|
|||
let cap = cmp::min(buffer.len(), CHUNK_SIZE);
|
||||
stream.reserve_capacity(cap);
|
||||
} else {
|
||||
return Ok(WriterState::Done)
|
||||
return Ok(WriterState::Pause)
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
|
@ -105,42 +111,52 @@ impl Writer for H2Writer {
|
|||
self.written
|
||||
}
|
||||
|
||||
fn start(&mut self, req: &mut HttpRequest, msg: &mut HttpResponse)
|
||||
fn start(&mut self, req: &mut HttpMessage, msg: &mut HttpResponse)
|
||||
-> Result<WriterState, io::Error>
|
||||
{
|
||||
trace!("Prepare response with status: {:?}", msg.status);
|
||||
// trace!("Prepare response with status: {:?}", msg.status());
|
||||
|
||||
// prepare response
|
||||
self.started = true;
|
||||
self.encoder = PayloadEncoder::new(req, msg);
|
||||
self.eof = if let Body::Empty = *msg.body() { true } else { false };
|
||||
|
||||
// http2 specific
|
||||
msg.headers.remove(CONNECTION);
|
||||
msg.headers.remove(TRANSFER_ENCODING);
|
||||
|
||||
// using http::h1::date is quite a lot faster than generating
|
||||
// a unique Date header each time like req/s goes up about 10%
|
||||
if !msg.headers.contains_key(DATE) {
|
||||
let mut bytes = [0u8; 29];
|
||||
date::extend(&mut bytes[..]);
|
||||
msg.headers.insert(DATE, HeaderValue::try_from(&bytes[..]).unwrap());
|
||||
self.flags.insert(Flags::STARTED);
|
||||
self.encoder = PayloadEncoder::new(self.buffer.clone(), req, msg);
|
||||
if let Body::Empty = *msg.body() {
|
||||
self.flags.insert(Flags::EOF);
|
||||
}
|
||||
|
||||
// default content-type
|
||||
if !msg.headers.contains_key(CONTENT_TYPE) {
|
||||
msg.headers.insert(
|
||||
CONTENT_TYPE, HeaderValue::from_static("application/octet-stream"));
|
||||
// http2 specific
|
||||
msg.headers_mut().remove(CONNECTION);
|
||||
msg.headers_mut().remove(TRANSFER_ENCODING);
|
||||
|
||||
// using helpers::date is quite a lot faster
|
||||
if !msg.headers().contains_key(DATE) {
|
||||
let mut bytes = BytesMut::with_capacity(29);
|
||||
helpers::date_value(&mut bytes);
|
||||
msg.headers_mut().insert(DATE, HeaderValue::try_from(bytes.freeze()).unwrap());
|
||||
}
|
||||
|
||||
let body = msg.replace_body(Body::Empty);
|
||||
match body {
|
||||
Body::Binary(ref bytes) => {
|
||||
let mut val = BytesMut::new();
|
||||
helpers::convert_usize(bytes.len(), &mut val);
|
||||
let l = val.len();
|
||||
msg.headers_mut().insert(
|
||||
CONTENT_LENGTH, HeaderValue::try_from(val.split_to(l-2).freeze()).unwrap());
|
||||
}
|
||||
Body::Empty => {
|
||||
msg.headers_mut().insert(CONTENT_LENGTH, HeaderValue::from_static("0"));
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let mut resp = Response::new(());
|
||||
*resp.status_mut() = msg.status;
|
||||
*resp.status_mut() = msg.status();
|
||||
*resp.version_mut() = Version::HTTP_2;
|
||||
for (key, value) in msg.headers().iter() {
|
||||
resp.headers_mut().insert(key, value.clone());
|
||||
}
|
||||
|
||||
match self.respond.send_response(resp, self.eof) {
|
||||
match self.respond.send_response(resp, self.flags.contains(Flags::EOF)) {
|
||||
Ok(stream) =>
|
||||
self.stream = Some(stream),
|
||||
Err(_) =>
|
||||
|
@ -149,23 +165,25 @@ impl Writer for H2Writer {
|
|||
|
||||
trace!("Response: {:?}", msg);
|
||||
|
||||
if msg.body().is_binary() {
|
||||
if let Body::Binary(bytes) = msg.replace_body(Body::Empty) {
|
||||
self.eof = true;
|
||||
self.encoder.write(bytes.as_ref())?;
|
||||
if let Some(ref mut stream) = self.stream {
|
||||
stream.reserve_capacity(cmp::min(self.encoder.len(), CHUNK_SIZE));
|
||||
}
|
||||
return Ok(WriterState::Done)
|
||||
if let Body::Binary(bytes) = body {
|
||||
self.flags.insert(Flags::EOF);
|
||||
self.written = bytes.len() as u64;
|
||||
self.encoder.write(bytes.as_ref())?;
|
||||
if let Some(ref mut stream) = self.stream {
|
||||
stream.reserve_capacity(cmp::min(self.encoder.len(), CHUNK_SIZE));
|
||||
}
|
||||
Ok(WriterState::Pause)
|
||||
} else {
|
||||
msg.replace_body(body);
|
||||
Ok(WriterState::Done)
|
||||
}
|
||||
|
||||
Ok(WriterState::Done)
|
||||
}
|
||||
|
||||
fn write(&mut self, payload: &[u8]) -> Result<WriterState, io::Error> {
|
||||
if !self.disconnected {
|
||||
if self.started {
|
||||
self.written = payload.len() as u64;
|
||||
|
||||
if !self.flags.contains(Flags::DISCONNECTED) {
|
||||
if self.flags.contains(Flags::STARTED) {
|
||||
// TODO: add warning, write after EOF
|
||||
self.encoder.write(payload)?;
|
||||
} else {
|
||||
|
@ -184,7 +202,7 @@ impl Writer for H2Writer {
|
|||
fn write_eof(&mut self) -> Result<WriterState, io::Error> {
|
||||
self.encoder.write_eof()?;
|
||||
|
||||
self.eof = true;
|
||||
self.flags.insert(Flags::EOF);
|
||||
if !self.encoder.is_eof() {
|
||||
Err(io::Error::new(io::ErrorKind::Other,
|
||||
"Last payload item, but eof is not reached"))
|
||||
|
@ -195,7 +213,7 @@ impl Writer for H2Writer {
|
|||
}
|
||||
}
|
||||
|
||||
fn poll_complete(&mut self) -> Poll<(), io::Error> {
|
||||
fn poll_completed(&mut self, _shutdown: bool) -> Poll<(), io::Error> {
|
||||
match self.write_to_stream() {
|
||||
Ok(WriterState::Done) => Ok(Async::Ready(())),
|
||||
Ok(WriterState::Pause) => Ok(Async::NotReady),
|
||||
|
|
562
src/handler.rs
Normal file
562
src/handler.rs
Normal file
|
@ -0,0 +1,562 @@
|
|||
use std::marker::PhantomData;
|
||||
|
||||
use regex::Regex;
|
||||
use futures::future::{Future, ok, err};
|
||||
use http::{header, StatusCode, Error as HttpError};
|
||||
|
||||
use body::Body;
|
||||
use error::Error;
|
||||
use httprequest::HttpRequest;
|
||||
use httpresponse::HttpResponse;
|
||||
|
||||
/// Trait defines object that could be regestered as route handler
|
||||
#[allow(unused_variables)]
|
||||
pub trait Handler<S>: 'static {
|
||||
|
||||
/// The type of value that handler will return.
|
||||
type Result: Responder;
|
||||
|
||||
/// Handle request
|
||||
fn handle(&mut self, req: HttpRequest<S>) -> Self::Result;
|
||||
}
|
||||
|
||||
/// Trait implemented by types that generate responses for clients.
|
||||
///
|
||||
/// Types that implement this trait can be used as the return type of a handler.
|
||||
pub trait Responder {
|
||||
/// The associated item which can be returned.
|
||||
type Item: Into<Reply>;
|
||||
|
||||
/// The associated error which can be returned.
|
||||
type Error: Into<Error>;
|
||||
|
||||
/// Convert itself to `Reply` or `Error`.
|
||||
fn respond_to(self, req: HttpRequest) -> Result<Self::Item, Self::Error>;
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Convinience trait that convert `Future` object into `Boxed` future
|
||||
pub trait AsyncResponder<I, E>: Sized {
|
||||
fn responder(self) -> Box<Future<Item=I, Error=E>>;
|
||||
}
|
||||
|
||||
impl<F, I, E> AsyncResponder<I, E> for F
|
||||
where F: Future<Item=I, Error=E> + 'static,
|
||||
I: Responder + 'static,
|
||||
E: Into<Error> + 'static,
|
||||
{
|
||||
fn responder(self) -> Box<Future<Item=I, Error=E>> {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler<S> for Fn()
|
||||
impl<F, R, S> Handler<S> for F
|
||||
where F: Fn(HttpRequest<S>) -> R + 'static,
|
||||
R: Responder + 'static
|
||||
{
|
||||
type Result = R;
|
||||
|
||||
fn handle(&mut self, req: HttpRequest<S>) -> R {
|
||||
(self)(req)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents response process.
|
||||
pub struct Reply(ReplyItem);
|
||||
|
||||
pub(crate) enum ReplyItem {
|
||||
Message(HttpResponse),
|
||||
Future(Box<Future<Item=HttpResponse, Error=Error>>),
|
||||
}
|
||||
|
||||
impl Reply {
|
||||
|
||||
/// Create async response
|
||||
#[inline]
|
||||
pub fn async<F>(fut: F) -> Reply
|
||||
where F: Future<Item=HttpResponse, Error=Error> + 'static
|
||||
{
|
||||
Reply(ReplyItem::Future(Box::new(fut)))
|
||||
}
|
||||
|
||||
/// Send response
|
||||
#[inline]
|
||||
pub fn response<R: Into<HttpResponse>>(response: R) -> Reply {
|
||||
Reply(ReplyItem::Message(response.into()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn into(self) -> ReplyItem {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn as_response(&self) -> Option<&HttpResponse> {
|
||||
match self.0 {
|
||||
ReplyItem::Message(ref resp) => Some(resp),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Responder for Reply {
|
||||
type Item = Reply;
|
||||
type Error = Error;
|
||||
|
||||
fn respond_to(self, _: HttpRequest) -> Result<Reply, Error> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Responder for HttpResponse {
|
||||
type Item = Reply;
|
||||
type Error = Error;
|
||||
|
||||
#[inline]
|
||||
fn respond_to(self, _: HttpRequest) -> Result<Reply, Error> {
|
||||
Ok(Reply(ReplyItem::Message(self)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HttpResponse> for Reply {
|
||||
|
||||
#[inline]
|
||||
fn from(resp: HttpResponse) -> Reply {
|
||||
Reply(ReplyItem::Message(resp))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Responder, E: Into<Error>> Responder for Result<T, E>
|
||||
{
|
||||
type Item = <T as Responder>::Item;
|
||||
type Error = Error;
|
||||
|
||||
fn respond_to(self, req: HttpRequest) -> Result<Self::Item, Self::Error> {
|
||||
match self {
|
||||
Ok(val) => match val.respond_to(req) {
|
||||
Ok(val) => Ok(val),
|
||||
Err(err) => Err(err.into()),
|
||||
},
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Into<Error>> From<Result<Reply, E>> for Reply {
|
||||
#[inline]
|
||||
fn from(res: Result<Reply, E>) -> Self {
|
||||
match res {
|
||||
Ok(val) => val,
|
||||
Err(err) => Reply(ReplyItem::Message(err.into().into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Into<Error>> From<Result<HttpResponse, E>> for Reply {
|
||||
#[inline]
|
||||
fn from(res: Result<HttpResponse, E>) -> Self {
|
||||
match res {
|
||||
Ok(val) => Reply(ReplyItem::Message(val)),
|
||||
Err(err) => Reply(ReplyItem::Message(err.into().into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<Future<Item=HttpResponse, Error=Error>>> for Reply {
|
||||
#[inline]
|
||||
fn from(fut: Box<Future<Item=HttpResponse, Error=Error>>) -> Reply {
|
||||
Reply(ReplyItem::Future(fut))
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, E> Responder for Box<Future<Item=I, Error=E>>
|
||||
where I: Responder + 'static,
|
||||
E: Into<Error> + 'static
|
||||
{
|
||||
type Item = Reply;
|
||||
type Error = Error;
|
||||
|
||||
#[inline]
|
||||
fn respond_to(self, req: HttpRequest) -> Result<Reply, Error> {
|
||||
let fut = self.map_err(|e| e.into())
|
||||
.then(move |r| {
|
||||
match r.respond_to(req) {
|
||||
Ok(reply) => match reply.into().0 {
|
||||
ReplyItem::Message(resp) => ok(resp),
|
||||
_ => panic!("Nested async replies are not supported"),
|
||||
},
|
||||
Err(e) => err(e),
|
||||
}
|
||||
});
|
||||
Ok(Reply::async(fut))
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait defines object that could be regestered as resource route
|
||||
pub(crate) trait RouteHandler<S>: 'static {
|
||||
fn handle(&mut self, req: HttpRequest<S>) -> Reply;
|
||||
}
|
||||
|
||||
/// Route handler wrapper for Handler
|
||||
pub(crate)
|
||||
struct WrapHandler<S, H, R>
|
||||
where H: Handler<S, Result=R>,
|
||||
R: Responder,
|
||||
S: 'static,
|
||||
{
|
||||
h: H,
|
||||
s: PhantomData<S>,
|
||||
}
|
||||
|
||||
impl<S, H, R> WrapHandler<S, H, R>
|
||||
where H: Handler<S, Result=R>,
|
||||
R: Responder,
|
||||
S: 'static,
|
||||
{
|
||||
pub fn new(h: H) -> Self {
|
||||
WrapHandler{h: h, s: PhantomData}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, H, R> RouteHandler<S> for WrapHandler<S, H, R>
|
||||
where H: Handler<S, Result=R>,
|
||||
R: Responder + 'static,
|
||||
S: 'static,
|
||||
{
|
||||
fn handle(&mut self, req: HttpRequest<S>) -> Reply {
|
||||
let req2 = req.clone_without_state();
|
||||
match self.h.handle(req).respond_to(req2) {
|
||||
Ok(reply) => reply.into(),
|
||||
Err(err) => Reply::response(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Async route handler
|
||||
pub(crate)
|
||||
struct AsyncHandler<S, H, F, R, E>
|
||||
where H: Fn(HttpRequest<S>) -> F + 'static,
|
||||
F: Future<Item=R, Error=E> + 'static,
|
||||
R: Responder + 'static,
|
||||
E: Into<Error> + 'static,
|
||||
S: 'static,
|
||||
{
|
||||
h: Box<H>,
|
||||
s: PhantomData<S>,
|
||||
}
|
||||
|
||||
impl<S, H, F, R, E> AsyncHandler<S, H, F, R, E>
|
||||
where H: Fn(HttpRequest<S>) -> F + 'static,
|
||||
F: Future<Item=R, Error=E> + 'static,
|
||||
R: Responder + 'static,
|
||||
E: Into<Error> + 'static,
|
||||
S: 'static,
|
||||
{
|
||||
pub fn new(h: H) -> Self {
|
||||
AsyncHandler{h: Box::new(h), s: PhantomData}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, H, F, R, E> RouteHandler<S> for AsyncHandler<S, H, F, R, E>
|
||||
where H: Fn(HttpRequest<S>) -> F + 'static,
|
||||
F: Future<Item=R, Error=E> + 'static,
|
||||
R: Responder + 'static,
|
||||
E: Into<Error> + 'static,
|
||||
S: 'static,
|
||||
{
|
||||
fn handle(&mut self, req: HttpRequest<S>) -> Reply {
|
||||
let req2 = req.clone_without_state();
|
||||
let fut = (self.h)(req)
|
||||
.map_err(|e| e.into())
|
||||
.then(move |r| {
|
||||
match r.respond_to(req2) {
|
||||
Ok(reply) => match reply.into().0 {
|
||||
ReplyItem::Message(resp) => ok(resp),
|
||||
_ => panic!("Nested async replies are not supported"),
|
||||
},
|
||||
Err(e) => err(e),
|
||||
}
|
||||
});
|
||||
Reply::async(fut)
|
||||
}
|
||||
}
|
||||
|
||||
/// Path normalization helper
|
||||
///
|
||||
/// By normalizing it means:
|
||||
///
|
||||
/// - Add a trailing slash to the path.
|
||||
/// - Double slashes are replaced by one.
|
||||
///
|
||||
/// The handler returns as soon as it finds a path that resolves
|
||||
/// correctly. The order if all enable is 1) merge, 3) both merge and append
|
||||
/// and 3) append. If the path resolves with
|
||||
/// at least one of those conditions, it will redirect to the new path.
|
||||
///
|
||||
/// If *append* is *true* append slash when needed. If a resource is
|
||||
/// defined with trailing slash and the request comes without it, it will
|
||||
/// append it automatically.
|
||||
///
|
||||
/// If *merge* is *true*, merge multiple consecutive slashes in the path into one.
|
||||
///
|
||||
/// This handler designed to be use as a handler for application's *default resource*.
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// # #[macro_use] extern crate serde_derive;
|
||||
/// # use actix_web::*;
|
||||
/// #
|
||||
/// # fn index(req: HttpRequest) -> httpcodes::StaticResponse {
|
||||
/// # httpcodes::HTTPOk
|
||||
/// # }
|
||||
/// fn main() {
|
||||
/// let app = Application::new()
|
||||
/// .resource("/test/", |r| r.f(index))
|
||||
/// .default_resource(|r| r.h(NormalizePath::default()))
|
||||
/// .finish();
|
||||
/// }
|
||||
/// ```
|
||||
/// In this example `/test`, `/test///` will be redirected to `/test/` url.
|
||||
pub struct NormalizePath {
|
||||
append: bool,
|
||||
merge: bool,
|
||||
re_merge: Regex,
|
||||
redirect: StatusCode,
|
||||
not_found: StatusCode,
|
||||
}
|
||||
|
||||
impl Default for NormalizePath {
|
||||
/// Create default `NormalizePath` instance, *append* is set to *true*,
|
||||
/// *merge* is set to *true* and *redirect* is set to `StatusCode::MOVED_PERMANENTLY`
|
||||
fn default() -> NormalizePath {
|
||||
NormalizePath {
|
||||
append: true,
|
||||
merge: true,
|
||||
re_merge: Regex::new("//+").unwrap(),
|
||||
redirect: StatusCode::MOVED_PERMANENTLY,
|
||||
not_found: StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NormalizePath {
|
||||
/// Create new `NoramlizePath` instance
|
||||
pub fn new(append: bool, merge: bool, redirect: StatusCode) -> NormalizePath {
|
||||
NormalizePath {
|
||||
append: append,
|
||||
merge: merge,
|
||||
re_merge: Regex::new("//+").unwrap(),
|
||||
redirect: redirect,
|
||||
not_found: StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Handler<S> for NormalizePath {
|
||||
type Result = Result<HttpResponse, HttpError>;
|
||||
|
||||
fn handle(&mut self, req: HttpRequest<S>) -> Self::Result {
|
||||
if let Some(router) = req.router() {
|
||||
let query = req.query_string();
|
||||
if self.merge {
|
||||
// merge slashes
|
||||
let p = self.re_merge.replace_all(req.path(), "/");
|
||||
if p.len() != req.path().len() {
|
||||
if router.has_route(p.as_ref()) {
|
||||
let p = if !query.is_empty() { p + "?" + query } else { p };
|
||||
return HttpResponse::build(self.redirect)
|
||||
.header(header::LOCATION, p.as_ref())
|
||||
.body(Body::Empty);
|
||||
}
|
||||
// merge slashes and append trailing slash
|
||||
if self.append && !p.ends_with('/') {
|
||||
let p = p.as_ref().to_owned() + "/";
|
||||
if router.has_route(&p) {
|
||||
let p = if !query.is_empty() { p + "?" + query } else { p };
|
||||
return HttpResponse::build(self.redirect)
|
||||
.header(header::LOCATION, p.as_str())
|
||||
.body(Body::Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// append trailing slash
|
||||
if self.append && !req.path().ends_with('/') {
|
||||
let p = req.path().to_owned() + "/";
|
||||
if router.has_route(&p) {
|
||||
let p = if !query.is_empty() { p + "?" + query } else { p };
|
||||
return HttpResponse::build(self.redirect)
|
||||
.header(header::LOCATION, p.as_str())
|
||||
.body(Body::Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(HttpResponse::new(self.not_found, Body::Empty))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use http::{header, Method};
|
||||
use test::TestRequest;
|
||||
use application::Application;
|
||||
|
||||
fn index(_req: HttpRequest) -> HttpResponse {
|
||||
HttpResponse::new(StatusCode::OK, Body::Empty)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_path_trailing_slashes() {
|
||||
let mut app = Application::new()
|
||||
.resource("/resource1", |r| r.method(Method::GET).f(index))
|
||||
.resource("/resource2/", |r| r.method(Method::GET).f(index))
|
||||
.default_resource(|r| r.h(NormalizePath::default()))
|
||||
.finish();
|
||||
|
||||
// trailing slashes
|
||||
let params = vec![("/resource1", "", StatusCode::OK),
|
||||
("/resource1/", "", StatusCode::NOT_FOUND),
|
||||
("/resource2", "/resource2/", StatusCode::MOVED_PERMANENTLY),
|
||||
("/resource2/", "", StatusCode::OK),
|
||||
("/resource1?p1=1&p2=2", "", StatusCode::OK),
|
||||
("/resource1/?p1=1&p2=2", "", StatusCode::NOT_FOUND),
|
||||
("/resource2?p1=1&p2=2", "/resource2/?p1=1&p2=2",
|
||||
StatusCode::MOVED_PERMANENTLY),
|
||||
("/resource2/?p1=1&p2=2", "", StatusCode::OK)
|
||||
];
|
||||
for (path, target, code) in params {
|
||||
let req = app.prepare_request(TestRequest::with_uri(path).finish());
|
||||
let resp = app.run(req);
|
||||
let r = resp.as_response().unwrap();
|
||||
assert_eq!(r.status(), code);
|
||||
if !target.is_empty() {
|
||||
assert_eq!(
|
||||
target,
|
||||
r.headers().get(header::LOCATION).unwrap().to_str().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_path_trailing_slashes_disabled() {
|
||||
let mut app = Application::new()
|
||||
.resource("/resource1", |r| r.method(Method::GET).f(index))
|
||||
.resource("/resource2/", |r| r.method(Method::GET).f(index))
|
||||
.default_resource(|r| r.h(
|
||||
NormalizePath::new(false, true, StatusCode::MOVED_PERMANENTLY)))
|
||||
.finish();
|
||||
|
||||
// trailing slashes
|
||||
let params = vec![("/resource1", StatusCode::OK),
|
||||
("/resource1/", StatusCode::NOT_FOUND),
|
||||
("/resource2", StatusCode::NOT_FOUND),
|
||||
("/resource2/", StatusCode::OK),
|
||||
("/resource1?p1=1&p2=2", StatusCode::OK),
|
||||
("/resource1/?p1=1&p2=2", StatusCode::NOT_FOUND),
|
||||
("/resource2?p1=1&p2=2", StatusCode::NOT_FOUND),
|
||||
("/resource2/?p1=1&p2=2", StatusCode::OK)
|
||||
];
|
||||
for (path, code) in params {
|
||||
let req = app.prepare_request(TestRequest::with_uri(path).finish());
|
||||
let resp = app.run(req);
|
||||
let r = resp.as_response().unwrap();
|
||||
assert_eq!(r.status(), code);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_path_merge_slashes() {
|
||||
let mut app = Application::new()
|
||||
.resource("/resource1", |r| r.method(Method::GET).f(index))
|
||||
.resource("/resource1/a/b", |r| r.method(Method::GET).f(index))
|
||||
.default_resource(|r| r.h(NormalizePath::default()))
|
||||
.finish();
|
||||
|
||||
// trailing slashes
|
||||
let params = vec![
|
||||
("/resource1/a/b", "", StatusCode::OK),
|
||||
("//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/", "", StatusCode::NOT_FOUND),
|
||||
("/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", "", StatusCode::NOT_FOUND),
|
||||
];
|
||||
for (path, target, code) in params {
|
||||
let req = app.prepare_request(TestRequest::with_uri(path).finish());
|
||||
let resp = app.run(req);
|
||||
let r = resp.as_response().unwrap();
|
||||
assert_eq!(r.status(), code);
|
||||
if !target.is_empty() {
|
||||
assert_eq!(
|
||||
target,
|
||||
r.headers().get(header::LOCATION).unwrap().to_str().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_path_merge_and_append_slashes() {
|
||||
let mut app = Application::new()
|
||||
.resource("/resource1", |r| r.method(Method::GET).f(index))
|
||||
.resource("/resource2/", |r| r.method(Method::GET).f(index))
|
||||
.resource("/resource1/a/b", |r| r.method(Method::GET).f(index))
|
||||
.resource("/resource2/a/b/", |r| r.method(Method::GET).f(index))
|
||||
.default_resource(|r| r.h(NormalizePath::default()))
|
||||
.finish();
|
||||
|
||||
// trailing slashes
|
||||
let params = vec![
|
||||
("/resource1/a/b", "", StatusCode::OK),
|
||||
("/resource1/a/b/", "", StatusCode::NOT_FOUND),
|
||||
("//resource2//a//b", "/resource2/a/b/", StatusCode::MOVED_PERMANENTLY),
|
||||
("//resource2//a//b/", "/resource2/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/", "", StatusCode::NOT_FOUND),
|
||||
("/resource2/a/b", "/resource2/a/b/", StatusCode::MOVED_PERMANENTLY),
|
||||
("/resource2/a/b/", "", StatusCode::OK),
|
||||
("//resource2//a//b", "/resource2/a/b/", StatusCode::MOVED_PERMANENTLY),
|
||||
("//resource2//a//b/", "/resource2/a/b/", StatusCode::MOVED_PERMANENTLY),
|
||||
("///resource2//a//b", "/resource2/a/b/", StatusCode::MOVED_PERMANENTLY),
|
||||
("///resource2//a//b/", "/resource2/a/b/", StatusCode::MOVED_PERMANENTLY),
|
||||
("/////resource2/a///b", "/resource2/a/b/", StatusCode::MOVED_PERMANENTLY),
|
||||
("/////resource2/a///b/", "/resource2/a/b/", StatusCode::MOVED_PERMANENTLY),
|
||||
("/resource1/a/b?p=1", "", StatusCode::OK),
|
||||
("/resource1/a/b/?p=1", "", StatusCode::NOT_FOUND),
|
||||
("//resource2//a//b?p=1", "/resource2/a/b/?p=1", StatusCode::MOVED_PERMANENTLY),
|
||||
("//resource2//a//b/?p=1", "/resource2/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", "", StatusCode::NOT_FOUND),
|
||||
("/resource2/a/b?p=1", "/resource2/a/b/?p=1", StatusCode::MOVED_PERMANENTLY),
|
||||
("//resource2//a//b?p=1", "/resource2/a/b/?p=1", StatusCode::MOVED_PERMANENTLY),
|
||||
("//resource2//a//b/?p=1", "/resource2/a/b/?p=1", StatusCode::MOVED_PERMANENTLY),
|
||||
("///resource2//a//b?p=1", "/resource2/a/b/?p=1", StatusCode::MOVED_PERMANENTLY),
|
||||
("///resource2//a//b/?p=1", "/resource2/a/b/?p=1", StatusCode::MOVED_PERMANENTLY),
|
||||
("/////resource2/a///b?p=1", "/resource2/a/b/?p=1", StatusCode::MOVED_PERMANENTLY),
|
||||
("/////resource2/a///b/?p=1", "/resource2/a/b/?p=1", StatusCode::MOVED_PERMANENTLY),
|
||||
];
|
||||
for (path, target, code) in params {
|
||||
let req = app.prepare_request(TestRequest::with_uri(path).finish());
|
||||
let resp = app.run(req);
|
||||
let r = resp.as_response().unwrap();
|
||||
assert_eq!(r.status(), code);
|
||||
if !target.is_empty() {
|
||||
assert_eq!(
|
||||
target, r.headers().get(header::LOCATION).unwrap().to_str().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
416
src/helpers.rs
Normal file
416
src/helpers.rs
Normal file
|
@ -0,0 +1,416 @@
|
|||
use std::{str, mem, ptr, slice};
|
||||
use std::cell::RefCell;
|
||||
use std::fmt::{self, Write};
|
||||
use std::rc::Rc;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::collections::VecDeque;
|
||||
use time;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use http::Version;
|
||||
|
||||
use httprequest::HttpMessage;
|
||||
|
||||
// "Sun, 06 Nov 1994 08:49:37 GMT".len()
|
||||
pub(crate) const DATE_VALUE_LENGTH: usize = 29;
|
||||
|
||||
pub(crate) fn date(dst: &mut BytesMut) {
|
||||
CACHED.with(|cache| {
|
||||
let mut buf: [u8; 39] = unsafe { mem::uninitialized() };
|
||||
buf[..6].copy_from_slice(b"date: ");
|
||||
buf[6..35].copy_from_slice(cache.borrow().buffer());
|
||||
buf[35..].copy_from_slice(b"\r\n\r\n");
|
||||
dst.extend_from_slice(&buf);
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn date_value(dst: &mut BytesMut) {
|
||||
CACHED.with(|cache| {
|
||||
dst.extend_from_slice(cache.borrow().buffer());
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn update_date() {
|
||||
CACHED.with(|cache| {
|
||||
cache.borrow_mut().update();
|
||||
});
|
||||
}
|
||||
|
||||
struct CachedDate {
|
||||
bytes: [u8; DATE_VALUE_LENGTH],
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
thread_local!(static CACHED: RefCell<CachedDate> = RefCell::new(CachedDate {
|
||||
bytes: [0; DATE_VALUE_LENGTH],
|
||||
pos: 0,
|
||||
}));
|
||||
|
||||
impl CachedDate {
|
||||
fn buffer(&self) -> &[u8] {
|
||||
&self.bytes[..]
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
self.pos = 0;
|
||||
write!(self, "{}", time::at_utc(time::get_time()).rfc822()).unwrap();
|
||||
assert_eq!(self.pos, DATE_VALUE_LENGTH);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Write for CachedDate {
|
||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||
let len = s.len();
|
||||
self.bytes[self.pos..self.pos + len].copy_from_slice(s.as_bytes());
|
||||
self.pos += len;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal use only! unsafe
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SharedBytesPool(RefCell<VecDeque<Rc<BytesMut>>>);
|
||||
|
||||
impl SharedBytesPool {
|
||||
pub fn new() -> SharedBytesPool {
|
||||
SharedBytesPool(RefCell::new(VecDeque::with_capacity(128)))
|
||||
}
|
||||
|
||||
pub fn get_bytes(&self) -> Rc<BytesMut> {
|
||||
if let Some(bytes) = self.0.borrow_mut().pop_front() {
|
||||
bytes
|
||||
} else {
|
||||
Rc::new(BytesMut::new())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn release_bytes(&self, mut bytes: Rc<BytesMut>) {
|
||||
let v = &mut self.0.borrow_mut();
|
||||
if v.len() < 128 {
|
||||
Rc::get_mut(&mut bytes).unwrap().take();
|
||||
v.push_front(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SharedBytes(
|
||||
Option<Rc<BytesMut>>, Option<Rc<SharedBytesPool>>);
|
||||
|
||||
impl Drop for SharedBytes {
|
||||
fn drop(&mut self) {
|
||||
if let Some(ref pool) = self.1 {
|
||||
if let Some(bytes) = self.0.take() {
|
||||
if Rc::strong_count(&bytes) == 1 {
|
||||
pool.release_bytes(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SharedBytes {
|
||||
|
||||
pub fn empty() -> Self {
|
||||
SharedBytes(None, None)
|
||||
}
|
||||
|
||||
pub fn new(bytes: Rc<BytesMut>, pool: Rc<SharedBytesPool>) -> SharedBytes {
|
||||
SharedBytes(Some(bytes), Some(pool))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[allow(mutable_transmutes)]
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(mut_from_ref, inline_always))]
|
||||
pub fn get_mut(&self) -> &mut BytesMut {
|
||||
let r: &BytesMut = self.0.as_ref().unwrap().as_ref();
|
||||
unsafe{mem::transmute(r)}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_ref(&self) -> &BytesMut {
|
||||
self.0.as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SharedBytes {
|
||||
fn default() -> Self {
|
||||
SharedBytes(Some(Rc::new(BytesMut::new())), None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for SharedBytes {
|
||||
fn clone(&self) -> SharedBytes {
|
||||
SharedBytes(self.0.clone(), self.1.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal use only! unsafe
|
||||
pub(crate) struct SharedMessagePool(RefCell<VecDeque<Rc<HttpMessage>>>);
|
||||
|
||||
impl SharedMessagePool {
|
||||
pub fn new() -> SharedMessagePool {
|
||||
SharedMessagePool(RefCell::new(VecDeque::with_capacity(128)))
|
||||
}
|
||||
|
||||
pub fn get(&self) -> Rc<HttpMessage> {
|
||||
if let Some(msg) = self.0.borrow_mut().pop_front() {
|
||||
msg
|
||||
} else {
|
||||
Rc::new(HttpMessage::default())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn release(&self, mut msg: Rc<HttpMessage>) {
|
||||
let v = &mut self.0.borrow_mut();
|
||||
if v.len() < 128 {
|
||||
Rc::get_mut(&mut msg).unwrap().reset();
|
||||
v.push_front(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SharedHttpMessage(
|
||||
Option<Rc<HttpMessage>>, Option<Rc<SharedMessagePool>>);
|
||||
|
||||
impl Drop for SharedHttpMessage {
|
||||
fn drop(&mut self) {
|
||||
if let Some(ref pool) = self.1 {
|
||||
if let Some(msg) = self.0.take() {
|
||||
if Rc::strong_count(&msg) == 1 {
|
||||
pool.release(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SharedHttpMessage {
|
||||
type Target = HttpMessage;
|
||||
|
||||
fn deref(&self) -> &HttpMessage {
|
||||
self.get_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for SharedHttpMessage {
|
||||
|
||||
fn deref_mut(&mut self) -> &mut HttpMessage {
|
||||
self.get_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for SharedHttpMessage {
|
||||
|
||||
fn clone(&self) -> SharedHttpMessage {
|
||||
SharedHttpMessage(self.0.clone(), self.1.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SharedHttpMessage {
|
||||
|
||||
fn default() -> SharedHttpMessage {
|
||||
SharedHttpMessage(Some(Rc::new(HttpMessage::default())), None)
|
||||
}
|
||||
}
|
||||
|
||||
impl SharedHttpMessage {
|
||||
|
||||
pub fn from_message(msg: HttpMessage) -> SharedHttpMessage {
|
||||
SharedHttpMessage(Some(Rc::new(msg)), None)
|
||||
}
|
||||
|
||||
pub fn new(msg: Rc<HttpMessage>, pool: Rc<SharedMessagePool>) -> SharedHttpMessage {
|
||||
SharedHttpMessage(Some(msg), Some(pool))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[allow(mutable_transmutes)]
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(mut_from_ref, inline_always))]
|
||||
pub fn get_mut(&self) -> &mut HttpMessage {
|
||||
let r: &HttpMessage = self.0.as_ref().unwrap().as_ref();
|
||||
unsafe{mem::transmute(r)}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(inline_always))]
|
||||
pub fn get_ref(&self) -> &HttpMessage {
|
||||
self.0.as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
const DEC_DIGITS_LUT: &[u8] =
|
||||
b"0001020304050607080910111213141516171819\
|
||||
2021222324252627282930313233343536373839\
|
||||
4041424344454647484950515253545556575859\
|
||||
6061626364656667686970717273747576777879\
|
||||
8081828384858687888990919293949596979899";
|
||||
|
||||
pub(crate) fn write_status_line(version: Version, mut n: u16, bytes: &mut BytesMut) {
|
||||
let mut buf: [u8; 13] = [b'H', b'T', b'T', b'P', b'/', b'1', b'.', b'1',
|
||||
b' ', b' ', b' ', b' ', b' '];
|
||||
match version {
|
||||
Version::HTTP_2 => buf[5] = b'2',
|
||||
Version::HTTP_10 => buf[7] = b'0',
|
||||
Version::HTTP_09 => {buf[5] = b'0'; buf[7] = b'9';},
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let mut curr: isize = 12;
|
||||
let buf_ptr = buf.as_mut_ptr();
|
||||
let lut_ptr = DEC_DIGITS_LUT.as_ptr();
|
||||
let four = n > 999;
|
||||
|
||||
unsafe {
|
||||
// decode 2 more chars, if > 2 chars
|
||||
let d1 = (n % 100) << 1;
|
||||
n /= 100;
|
||||
curr -= 2;
|
||||
ptr::copy_nonoverlapping(lut_ptr.offset(d1 as isize), buf_ptr.offset(curr), 2);
|
||||
|
||||
// decode last 1 or 2 chars
|
||||
if n < 10 {
|
||||
curr -= 1;
|
||||
*buf_ptr.offset(curr) = (n as u8) + b'0';
|
||||
} else {
|
||||
let d1 = n << 1;
|
||||
curr -= 2;
|
||||
ptr::copy_nonoverlapping(lut_ptr.offset(d1 as isize), buf_ptr.offset(curr), 2);
|
||||
}
|
||||
}
|
||||
|
||||
bytes.extend_from_slice(&buf);
|
||||
if four {
|
||||
bytes.put(b' ');
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn write_content_length(mut n: usize, bytes: &mut BytesMut) {
|
||||
if n < 10 {
|
||||
let mut buf: [u8; 21] = [b'\r',b'\n',b'c',b'o',b'n',b't',b'e',
|
||||
b'n',b't',b'-',b'l',b'e',b'n',b'g',
|
||||
b't',b'h',b':',b' ',b'0',b'\r',b'\n'];
|
||||
buf[18] = (n as u8) + b'0';
|
||||
bytes.extend_from_slice(&buf);
|
||||
} else if n < 100 {
|
||||
let mut buf: [u8; 22] = [b'\r',b'\n',b'c',b'o',b'n',b't',b'e',
|
||||
b'n',b't',b'-',b'l',b'e',b'n',b'g',
|
||||
b't',b'h',b':',b' ',b'0',b'0',b'\r',b'\n'];
|
||||
let d1 = n << 1;
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(
|
||||
DEC_DIGITS_LUT.as_ptr().offset(d1 as isize), buf.as_mut_ptr().offset(18), 2);
|
||||
}
|
||||
bytes.extend_from_slice(&buf);
|
||||
} else if n < 1000 {
|
||||
let mut buf: [u8; 23] = [b'\r',b'\n',b'c',b'o',b'n',b't',b'e',
|
||||
b'n',b't',b'-',b'l',b'e',b'n',b'g',
|
||||
b't',b'h',b':',b' ',b'0',b'0',b'0',b'\r',b'\n'];
|
||||
// decode 2 more chars, if > 2 chars
|
||||
let d1 = (n % 100) << 1;
|
||||
n /= 100;
|
||||
unsafe {ptr::copy_nonoverlapping(
|
||||
DEC_DIGITS_LUT.as_ptr().offset(d1 as isize), buf.as_mut_ptr().offset(19), 2)};
|
||||
|
||||
// decode last 1
|
||||
buf[18] = (n as u8) + b'0';
|
||||
|
||||
bytes.extend_from_slice(&buf);
|
||||
} else {
|
||||
bytes.extend_from_slice(b"\r\ncontent-length: ");
|
||||
convert_usize(n, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn convert_usize(mut n: usize, bytes: &mut BytesMut) {
|
||||
let mut curr: isize = 39;
|
||||
let mut buf: [u8; 41] = unsafe { mem::uninitialized() };
|
||||
buf[39] = b'\r';
|
||||
buf[40] = b'\n';
|
||||
let buf_ptr = buf.as_mut_ptr();
|
||||
let lut_ptr = DEC_DIGITS_LUT.as_ptr();
|
||||
|
||||
unsafe {
|
||||
// eagerly decode 4 characters at a time
|
||||
while n >= 10_000 {
|
||||
let rem = (n % 10_000) as isize;
|
||||
n /= 10_000;
|
||||
|
||||
let d1 = (rem / 100) << 1;
|
||||
let d2 = (rem % 100) << 1;
|
||||
curr -= 4;
|
||||
ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2);
|
||||
ptr::copy_nonoverlapping(lut_ptr.offset(d2), buf_ptr.offset(curr + 2), 2);
|
||||
}
|
||||
|
||||
// if we reach here numbers are <= 9999, so at most 4 chars long
|
||||
let mut n = n as isize; // possibly reduce 64bit math
|
||||
|
||||
// decode 2 more chars, if > 2 chars
|
||||
if n >= 100 {
|
||||
let d1 = (n % 100) << 1;
|
||||
n /= 100;
|
||||
curr -= 2;
|
||||
ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2);
|
||||
}
|
||||
|
||||
// decode last 1 or 2 chars
|
||||
if n < 10 {
|
||||
curr -= 1;
|
||||
*buf_ptr.offset(curr) = (n as u8) + b'0';
|
||||
} else {
|
||||
let d1 = n << 1;
|
||||
curr -= 2;
|
||||
ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
bytes.extend_from_slice(
|
||||
slice::from_raw_parts(buf_ptr.offset(curr), 41 - curr as usize));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_date_len() {
|
||||
assert_eq!(DATE_VALUE_LENGTH, "Sun, 06 Nov 1994 08:49:37 GMT".len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_date() {
|
||||
let mut buf1 = BytesMut::new();
|
||||
date(&mut buf1);
|
||||
let mut buf2 = BytesMut::new();
|
||||
date(&mut buf2);
|
||||
assert_eq!(buf1, buf2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_content_length() {
|
||||
let mut bytes = BytesMut::new();
|
||||
write_content_length(0, &mut bytes);
|
||||
assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 0\r\n"[..]);
|
||||
write_content_length(9, &mut bytes);
|
||||
assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 9\r\n"[..]);
|
||||
write_content_length(10, &mut bytes);
|
||||
assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 10\r\n"[..]);
|
||||
write_content_length(99, &mut bytes);
|
||||
assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 99\r\n"[..]);
|
||||
write_content_length(100, &mut bytes);
|
||||
assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 100\r\n"[..]);
|
||||
write_content_length(101, &mut bytes);
|
||||
assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 101\r\n"[..]);
|
||||
write_content_length(998, &mut bytes);
|
||||
assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 998\r\n"[..]);
|
||||
write_content_length(1000, &mut bytes);
|
||||
assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 1000\r\n"[..]);
|
||||
write_content_length(1001, &mut bytes);
|
||||
assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 1001\r\n"[..]);
|
||||
write_content_length(5909, &mut bytes);
|
||||
assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 5909\r\n"[..]);
|
||||
}
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
//! Basic http responses
|
||||
#![allow(non_upper_case_globals)]
|
||||
use http::StatusCode;
|
||||
use http::{StatusCode, Error as HttpError};
|
||||
|
||||
use body::Body;
|
||||
use route::Reply;
|
||||
use route::RouteHandler;
|
||||
use handler::{Reply, Handler, RouteHandler, Responder};
|
||||
use httprequest::HttpRequest;
|
||||
use httpresponse::{HttpResponse, HttpResponseBuilder};
|
||||
|
||||
|
@ -55,9 +54,6 @@ impl StaticResponse {
|
|||
pub fn build(&self) -> HttpResponseBuilder {
|
||||
HttpResponse::build(self.0)
|
||||
}
|
||||
pub fn response(&self) -> HttpResponse {
|
||||
HttpResponse::new(self.0, Body::Empty)
|
||||
}
|
||||
pub fn with_reason(self, reason: &'static str) -> HttpResponse {
|
||||
let mut resp = HttpResponse::new(self.0, Body::Empty);
|
||||
resp.set_reason(reason);
|
||||
|
@ -68,15 +64,38 @@ impl StaticResponse {
|
|||
}
|
||||
}
|
||||
|
||||
impl<S> Handler<S> for StaticResponse {
|
||||
type Result = HttpResponse;
|
||||
|
||||
fn handle(&mut self, _: HttpRequest<S>) -> HttpResponse {
|
||||
HttpResponse::new(self.0, Body::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> RouteHandler<S> for StaticResponse {
|
||||
fn handle(&self, _: HttpRequest<S>) -> Reply {
|
||||
fn handle(&mut self, _: HttpRequest<S>) -> Reply {
|
||||
Reply::response(HttpResponse::new(self.0, Body::Empty))
|
||||
}
|
||||
}
|
||||
|
||||
impl Responder for StaticResponse {
|
||||
type Item = HttpResponse;
|
||||
type Error = HttpError;
|
||||
|
||||
fn respond_to(self, _: HttpRequest) -> Result<HttpResponse, HttpError> {
|
||||
self.build().body(Body::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StaticResponse> for HttpResponse {
|
||||
fn from(st: StaticResponse) -> Self {
|
||||
st.response()
|
||||
HttpResponse::new(st.0, Body::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StaticResponse> for Reply {
|
||||
fn from(st: StaticResponse) -> Self {
|
||||
HttpResponse::new(st.0, Body::Empty).into()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -137,7 +156,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_response() {
|
||||
let resp = HTTPOk.response();
|
||||
let resp: HttpResponse = HTTPOk.into();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
|
@ -149,8 +168,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_with_reason() {
|
||||
let resp = HTTPOk.response();
|
||||
assert_eq!(resp.reason(), "");
|
||||
let resp: HttpResponse = HTTPOk.into();
|
||||
assert_eq!(resp.reason(), "OK");
|
||||
|
||||
let resp = HTTPBadRequest.with_reason("test");
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,8 @@
|
|||
//! Pieces pertaining to the HTTP response.
|
||||
use std::{io, mem, str, fmt};
|
||||
use std::{mem, str, fmt};
|
||||
use std::cell::RefCell;
|
||||
use std::convert::Into;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use cookie::CookieJar;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
|
@ -8,11 +10,13 @@ use http::{StatusCode, Version, HeaderMap, HttpTryFrom, Error as HttpError};
|
|||
use http::header::{self, HeaderName, HeaderValue};
|
||||
use serde_json;
|
||||
use serde::Serialize;
|
||||
use cookie::Cookie;
|
||||
|
||||
use Cookie;
|
||||
use body::Body;
|
||||
use error::Error;
|
||||
use handler::Responder;
|
||||
use encoding::ContentEncoding;
|
||||
use httprequest::HttpRequest;
|
||||
|
||||
/// Represents various types of connection
|
||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||
|
@ -26,122 +30,122 @@ pub enum ConnectionType {
|
|||
}
|
||||
|
||||
/// An HTTP Response
|
||||
pub struct HttpResponse {
|
||||
pub version: Option<Version>,
|
||||
pub headers: HeaderMap,
|
||||
pub status: StatusCode,
|
||||
reason: Option<&'static str>,
|
||||
body: Body,
|
||||
chunked: bool,
|
||||
encoding: ContentEncoding,
|
||||
connection_type: Option<ConnectionType>,
|
||||
response_size: u64,
|
||||
error: Option<Error>,
|
||||
pub struct HttpResponse(Option<Box<InnerHttpResponse>>);
|
||||
|
||||
impl Drop for HttpResponse {
|
||||
fn drop(&mut self) {
|
||||
if let Some(inner) = self.0.take() {
|
||||
Pool::release(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpResponse {
|
||||
|
||||
#[inline(always)]
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(inline_always))]
|
||||
fn get_ref(&self) -> &InnerHttpResponse {
|
||||
self.0.as_ref().unwrap()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(inline_always))]
|
||||
fn get_mut(&mut self) -> &mut InnerHttpResponse {
|
||||
self.0.as_mut().unwrap()
|
||||
}
|
||||
|
||||
/// Create http response builder with specific status.
|
||||
#[inline]
|
||||
pub fn build(status: StatusCode) -> HttpResponseBuilder {
|
||||
HttpResponseBuilder {
|
||||
parts: Some(Parts::new(status)),
|
||||
response: Some(Pool::get(status)),
|
||||
err: None,
|
||||
cookies: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a response
|
||||
#[inline]
|
||||
pub fn new(status: StatusCode, body: Body) -> HttpResponse {
|
||||
HttpResponse {
|
||||
version: None,
|
||||
headers: Default::default(),
|
||||
status: status,
|
||||
reason: None,
|
||||
body: body,
|
||||
chunked: false,
|
||||
encoding: ContentEncoding::Auto,
|
||||
connection_type: None,
|
||||
response_size: 0,
|
||||
error: None,
|
||||
}
|
||||
HttpResponse(Some(Pool::with_body(status, body)))
|
||||
}
|
||||
|
||||
/// Constructs a error response
|
||||
#[inline]
|
||||
pub fn from_error(error: Error) -> HttpResponse {
|
||||
let mut resp = error.cause().error_response();
|
||||
resp.error = Some(error);
|
||||
resp.get_mut().error = Some(error);
|
||||
resp
|
||||
}
|
||||
|
||||
/// The source `error` for this response
|
||||
#[inline]
|
||||
pub fn error(&self) -> Option<&Error> {
|
||||
self.error.as_ref()
|
||||
self.get_ref().error.as_ref()
|
||||
}
|
||||
|
||||
/// Get the HTTP version of this response.
|
||||
#[inline]
|
||||
pub fn version(&self) -> Option<Version> {
|
||||
self.version
|
||||
self.get_ref().version
|
||||
}
|
||||
|
||||
/// Get the headers from the response.
|
||||
#[inline]
|
||||
pub fn headers(&self) -> &HeaderMap {
|
||||
&self.headers
|
||||
&self.get_ref().headers
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the headers.
|
||||
#[inline]
|
||||
pub fn headers_mut(&mut self) -> &mut HeaderMap {
|
||||
&mut self.headers
|
||||
&mut self.get_mut().headers
|
||||
}
|
||||
|
||||
/// Get the status from the server.
|
||||
#[inline]
|
||||
pub fn status(&self) -> StatusCode {
|
||||
self.status
|
||||
self.get_ref().status
|
||||
}
|
||||
|
||||
/// Set the `StatusCode` for this response.
|
||||
#[inline]
|
||||
pub fn status_mut(&mut self) -> &mut StatusCode {
|
||||
&mut self.status
|
||||
&mut self.get_mut().status
|
||||
}
|
||||
|
||||
/// Get custom reason for the response.
|
||||
#[inline]
|
||||
pub fn reason(&self) -> &str {
|
||||
if let Some(reason) = self.reason {
|
||||
if let Some(reason) = self.get_ref().reason {
|
||||
reason
|
||||
} else {
|
||||
""
|
||||
self.get_ref().status.canonical_reason().unwrap_or("<unknown status code>")
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the custom reason for the response.
|
||||
#[inline]
|
||||
pub fn set_reason(&mut self, reason: &'static str) -> &mut Self {
|
||||
self.reason = Some(reason);
|
||||
self.get_mut().reason = Some(reason);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set connection type
|
||||
pub fn set_connection_type(&mut self, conn: ConnectionType) -> &mut Self {
|
||||
self.connection_type = Some(conn);
|
||||
self.get_mut().connection_type = Some(conn);
|
||||
self
|
||||
}
|
||||
|
||||
/// Connection upgrade status
|
||||
#[inline]
|
||||
pub fn upgrade(&self) -> bool {
|
||||
self.connection_type == Some(ConnectionType::Upgrade)
|
||||
self.get_ref().connection_type == Some(ConnectionType::Upgrade)
|
||||
}
|
||||
|
||||
/// Keep-alive status for this connection
|
||||
pub fn keep_alive(&self) -> Option<bool> {
|
||||
if let Some(ct) = self.connection_type {
|
||||
if let Some(ct) = self.get_ref().connection_type {
|
||||
match ct {
|
||||
ConnectionType::KeepAlive => Some(true),
|
||||
ConnectionType::Close | ConnectionType::Upgrade => Some(false),
|
||||
|
@ -152,66 +156,59 @@ impl HttpResponse {
|
|||
}
|
||||
|
||||
/// is chunked encoding enabled
|
||||
#[inline]
|
||||
pub fn chunked(&self) -> bool {
|
||||
self.chunked
|
||||
}
|
||||
|
||||
/// Enables automatic chunked transfer encoding
|
||||
pub fn enable_chunked_encoding(&mut self) -> Result<(), io::Error> {
|
||||
if self.headers.contains_key(header::CONTENT_LENGTH) {
|
||||
Err(io::Error::new(io::ErrorKind::Other,
|
||||
"You can't enable chunked encoding when a content length is set"))
|
||||
} else {
|
||||
self.chunked = true;
|
||||
Ok(())
|
||||
}
|
||||
self.get_ref().chunked
|
||||
}
|
||||
|
||||
/// Content encoding
|
||||
#[inline]
|
||||
pub fn content_encoding(&self) -> &ContentEncoding {
|
||||
&self.encoding
|
||||
&self.get_ref().encoding
|
||||
}
|
||||
|
||||
/// Set content encoding
|
||||
pub fn set_content_encoding(&mut self, enc: ContentEncoding) -> &mut Self {
|
||||
self.encoding = enc;
|
||||
self.get_mut().encoding = enc;
|
||||
self
|
||||
}
|
||||
|
||||
/// Get body os this response
|
||||
#[inline]
|
||||
pub fn body(&self) -> &Body {
|
||||
&self.body
|
||||
&self.get_ref().body
|
||||
}
|
||||
|
||||
/// Set a body
|
||||
pub fn set_body<B: Into<Body>>(&mut self, body: B) {
|
||||
self.body = body.into();
|
||||
self.get_mut().body = body.into();
|
||||
}
|
||||
|
||||
/// Set a body and return previous body value
|
||||
pub fn replace_body<B: Into<Body>>(&mut self, body: B) -> Body {
|
||||
mem::replace(&mut self.body, body.into())
|
||||
mem::replace(&mut self.get_mut().body, body.into())
|
||||
}
|
||||
|
||||
/// Size of response in bytes, excluding HTTP headers
|
||||
pub fn response_size(&self) -> u64 {
|
||||
self.response_size
|
||||
self.get_ref().response_size
|
||||
}
|
||||
|
||||
/// Set content encoding
|
||||
pub(crate) fn set_response_size(&mut self, size: u64) {
|
||||
self.response_size = size;
|
||||
self.get_mut().response_size = size;
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for HttpResponse {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let res = write!(f, "\nHttpResponse {:?} {}{}\n",
|
||||
self.version, self.status, self.reason.unwrap_or(""));
|
||||
let _ = write!(f, " encoding: {:?}\n", self.encoding);
|
||||
self.get_ref().version, self.get_ref().status,
|
||||
self.get_ref().reason.unwrap_or(""));
|
||||
let _ = write!(f, " encoding: {:?}\n", self.get_ref().encoding);
|
||||
let _ = write!(f, " headers:\n");
|
||||
for key in self.headers.keys() {
|
||||
let vals: Vec<_> = self.headers.get_all(key).iter().collect();
|
||||
for key in self.get_ref().headers.keys() {
|
||||
let vals: Vec<_> = self.get_ref().headers.get_all(key).iter().collect();
|
||||
if vals.len() > 1 {
|
||||
let _ = write!(f, " {:?}: {:?}\n", key, vals);
|
||||
} else {
|
||||
|
@ -222,70 +219,53 @@ impl fmt::Debug for HttpResponse {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Parts {
|
||||
version: Option<Version>,
|
||||
headers: HeaderMap,
|
||||
status: StatusCode,
|
||||
reason: Option<&'static str>,
|
||||
chunked: bool,
|
||||
encoding: ContentEncoding,
|
||||
connection_type: Option<ConnectionType>,
|
||||
cookies: CookieJar,
|
||||
}
|
||||
|
||||
impl Parts {
|
||||
fn new(status: StatusCode) -> Self {
|
||||
Parts {
|
||||
version: None,
|
||||
headers: HeaderMap::new(),
|
||||
status: status,
|
||||
reason: None,
|
||||
chunked: false,
|
||||
encoding: ContentEncoding::Auto,
|
||||
connection_type: None,
|
||||
cookies: CookieJar::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// An HTTP response builder
|
||||
///
|
||||
/// This type can be used to construct an instance of `HttpResponse` through a
|
||||
/// builder-like pattern.
|
||||
#[derive(Debug)]
|
||||
pub struct HttpResponseBuilder {
|
||||
parts: Option<Parts>,
|
||||
response: Option<Box<InnerHttpResponse>>,
|
||||
err: Option<HttpError>,
|
||||
cookies: Option<CookieJar>,
|
||||
}
|
||||
|
||||
impl HttpResponseBuilder {
|
||||
/// Set the HTTP version of this response.
|
||||
/// Set HTTP version of this response.
|
||||
///
|
||||
/// By default response's http version depends on request's version.
|
||||
#[inline]
|
||||
pub fn version(&mut self, version: Version) -> &mut Self {
|
||||
if let Some(parts) = parts(&mut self.parts, &self.err) {
|
||||
if let Some(parts) = parts(&mut self.response, &self.err) {
|
||||
parts.version = Some(version);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the `StatusCode` for this response.
|
||||
#[inline]
|
||||
pub fn status(&mut self, status: StatusCode) -> &mut Self {
|
||||
if let Some(parts) = parts(&mut self.parts, &self.err) {
|
||||
parts.status = status;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a header.
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate http;
|
||||
/// # extern crate actix_web;
|
||||
/// # use actix_web::*;
|
||||
/// # use actix_web::httpcodes::*;
|
||||
/// #
|
||||
/// use http::header;
|
||||
///
|
||||
/// fn index(req: HttpRequest) -> Result<HttpResponse> {
|
||||
/// Ok(HTTPOk.build()
|
||||
/// .header("X-TEST", "value")
|
||||
/// .header(header::CONTENT_TYPE, "application/json")
|
||||
/// .finish()?)
|
||||
/// }
|
||||
/// fn main() {}
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn header<K, V>(&mut self, key: K, value: V) -> &mut Self
|
||||
where HeaderName: HttpTryFrom<K>,
|
||||
HeaderValue: HttpTryFrom<V>
|
||||
{
|
||||
if let Some(parts) = parts(&mut self.parts, &self.err) {
|
||||
if let Some(parts) = parts(&mut self.response, &self.err) {
|
||||
match HeaderName::try_from(key) {
|
||||
Ok(key) => {
|
||||
match HeaderValue::try_from(value) {
|
||||
|
@ -302,7 +282,7 @@ impl HttpResponseBuilder {
|
|||
/// Set the custom reason for the response.
|
||||
#[inline]
|
||||
pub fn reason(&mut self, reason: &'static str) -> &mut Self {
|
||||
if let Some(parts) = parts(&mut self.parts, &self.err) {
|
||||
if let Some(parts) = parts(&mut self.response, &self.err) {
|
||||
parts.reason = Some(reason);
|
||||
}
|
||||
self
|
||||
|
@ -313,44 +293,52 @@ impl HttpResponseBuilder {
|
|||
/// By default `ContentEncoding::Auto` is used, which automatically
|
||||
/// negotiates content encoding based on request's `Accept-Encoding` headers.
|
||||
/// To enforce specific encoding, use specific ContentEncoding` value.
|
||||
#[inline]
|
||||
pub fn content_encoding(&mut self, enc: ContentEncoding) -> &mut Self {
|
||||
if let Some(parts) = parts(&mut self.parts, &self.err) {
|
||||
if let Some(parts) = parts(&mut self.response, &self.err) {
|
||||
parts.encoding = enc;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set connection type
|
||||
#[inline]
|
||||
#[doc(hidden)]
|
||||
pub fn connection_type(&mut self, conn: ConnectionType) -> &mut Self {
|
||||
if let Some(parts) = parts(&mut self.parts, &self.err) {
|
||||
if let Some(parts) = parts(&mut self.response, &self.err) {
|
||||
parts.connection_type = Some(conn);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set connection type to Upgrade
|
||||
#[inline]
|
||||
#[doc(hidden)]
|
||||
pub fn upgrade(&mut self) -> &mut Self {
|
||||
self.connection_type(ConnectionType::Upgrade)
|
||||
}
|
||||
|
||||
/// Force close connection, even if it is marked as keep-alive
|
||||
#[inline]
|
||||
pub fn force_close(&mut self) -> &mut Self {
|
||||
self.connection_type(ConnectionType::Close)
|
||||
}
|
||||
|
||||
/// Enables automatic chunked transfer encoding
|
||||
pub fn enable_chunked(&mut self) -> &mut Self {
|
||||
if let Some(parts) = parts(&mut self.parts, &self.err) {
|
||||
#[inline]
|
||||
pub fn chunked(&mut self) -> &mut Self {
|
||||
if let Some(parts) = parts(&mut self.response, &self.err) {
|
||||
parts.chunked = true;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set response 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.parts, &self.err) {
|
||||
if let Some(parts) = parts(&mut self.response, &self.err) {
|
||||
match HeaderValue::try_from(value) {
|
||||
Ok(value) => { parts.headers.insert(header::CONTENT_TYPE, value); },
|
||||
Err(e) => self.err = Some(e.into()),
|
||||
|
@ -360,23 +348,53 @@ impl HttpResponseBuilder {
|
|||
}
|
||||
|
||||
/// Set a cookie
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// # use actix_web::*;
|
||||
/// # use actix_web::httpcodes::*;
|
||||
/// #
|
||||
/// use actix_web::headers::Cookie;
|
||||
///
|
||||
/// fn index(req: HttpRequest) -> Result<HttpResponse> {
|
||||
/// Ok(HTTPOk.build()
|
||||
/// .cookie(
|
||||
/// Cookie::build("name", "value")
|
||||
/// .domain("www.rust-lang.org")
|
||||
/// .path("/")
|
||||
/// .secure(true)
|
||||
/// .http_only(true)
|
||||
/// .finish())
|
||||
/// .finish()?)
|
||||
/// }
|
||||
/// fn main() {}
|
||||
/// ```
|
||||
pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self {
|
||||
if let Some(parts) = parts(&mut self.parts, &self.err) {
|
||||
parts.cookies.add(cookie.into_owned());
|
||||
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
|
||||
}
|
||||
|
||||
/// Remote cookie, cookie has to be cookie from `HttpRequest::cookies()` method.
|
||||
/// Remove cookie, cookie has to be cookie from `HttpRequest::cookies()` method.
|
||||
pub fn del_cookie<'a>(&mut self, cookie: &Cookie<'a>) -> &mut Self {
|
||||
if let Some(parts) = parts(&mut self.parts, &self.err) {
|
||||
{
|
||||
if self.cookies.is_none() {
|
||||
self.cookies = Some(CookieJar::new())
|
||||
}
|
||||
let jar = self.cookies.as_mut().unwrap();
|
||||
let cookie = cookie.clone().into_owned();
|
||||
parts.cookies.add_original(cookie.clone());
|
||||
parts.cookies.remove(cookie);
|
||||
jar.add_original(cookie.clone());
|
||||
jar.remove(cookie);
|
||||
}
|
||||
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: Fn(&mut HttpResponseBuilder) + 'static
|
||||
{
|
||||
|
@ -387,36 +405,31 @@ impl HttpResponseBuilder {
|
|||
}
|
||||
|
||||
/// Set a body and generate `HttpResponse`.
|
||||
///
|
||||
/// `HttpResponseBuilder` can not be used after this call.
|
||||
pub fn body<B: Into<Body>>(&mut self, body: B) -> Result<HttpResponse, HttpError> {
|
||||
let mut parts = self.parts.take().expect("cannot reuse response builder");
|
||||
if let Some(e) = self.err.take() {
|
||||
return Err(e)
|
||||
}
|
||||
for cookie in parts.cookies.delta() {
|
||||
parts.headers.append(
|
||||
header::SET_COOKIE,
|
||||
HeaderValue::from_str(&cookie.to_string())?);
|
||||
let mut response = self.response.take().expect("cannot reuse response builder");
|
||||
if let Some(ref jar) = self.cookies {
|
||||
for cookie in jar.delta() {
|
||||
response.headers.append(
|
||||
header::SET_COOKIE,
|
||||
HeaderValue::from_str(&cookie.to_string())?);
|
||||
}
|
||||
}
|
||||
Ok(HttpResponse {
|
||||
version: parts.version,
|
||||
headers: parts.headers,
|
||||
status: parts.status,
|
||||
reason: parts.reason,
|
||||
body: body.into(),
|
||||
chunked: parts.chunked,
|
||||
encoding: parts.encoding,
|
||||
connection_type: parts.connection_type,
|
||||
response_size: 0,
|
||||
error: None,
|
||||
})
|
||||
response.body = body.into();
|
||||
Ok(HttpResponse(Some(response)))
|
||||
}
|
||||
|
||||
/// Set a json body and generate `HttpResponse`
|
||||
///
|
||||
/// `HttpResponseBuilder` can not be used after this call.
|
||||
pub fn json<T: Serialize>(&mut self, value: T) -> Result<HttpResponse, Error> {
|
||||
let body = serde_json::to_string(&value)?;
|
||||
|
||||
let contains = if let Some(parts) = parts(&mut self.parts, &self.err) {
|
||||
let contains = if let Some(parts) = parts(&mut self.response, &self.err) {
|
||||
parts.headers.contains_key(header::CONTENT_TYPE)
|
||||
} else {
|
||||
true
|
||||
|
@ -429,12 +442,26 @@ impl HttpResponseBuilder {
|
|||
}
|
||||
|
||||
/// Set an empty body and generate `HttpResponse`
|
||||
///
|
||||
/// `HttpResponseBuilder` can not be used after this call.
|
||||
pub fn finish(&mut self) -> Result<HttpResponse, HttpError> {
|
||||
self.body(Body::Empty)
|
||||
}
|
||||
|
||||
/// This method construct new `HttpResponseBuilder`
|
||||
pub fn take(&mut self) -> HttpResponseBuilder {
|
||||
HttpResponseBuilder {
|
||||
response: self.response.take(),
|
||||
err: self.err.take(),
|
||||
cookies: self.cookies.take(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parts<'a>(parts: &'a mut Option<Parts>, err: &Option<HttpError>) -> Option<&'a mut Parts>
|
||||
#[inline]
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(borrowed_box))]
|
||||
fn parts<'a>(parts: &'a mut Option<Box<InnerHttpResponse>>, err: &Option<HttpError>)
|
||||
-> Option<&'a mut Box<InnerHttpResponse>>
|
||||
{
|
||||
if err.is_some() {
|
||||
return None
|
||||
|
@ -458,6 +485,16 @@ impl From<HttpResponseBuilder> for HttpResponse {
|
|||
}
|
||||
}
|
||||
|
||||
impl Responder for HttpResponseBuilder {
|
||||
type Item = HttpResponse;
|
||||
type Error = HttpError;
|
||||
|
||||
#[inline]
|
||||
fn respond_to(mut self, _: HttpRequest) -> Result<HttpResponse, HttpError> {
|
||||
self.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for HttpResponse {
|
||||
fn from(val: &'static str) -> Self {
|
||||
HttpResponse::build(StatusCode::OK)
|
||||
|
@ -467,6 +504,17 @@ impl From<&'static str> for HttpResponse {
|
|||
}
|
||||
}
|
||||
|
||||
impl Responder for &'static str {
|
||||
type Item = HttpResponse;
|
||||
type Error = HttpError;
|
||||
|
||||
fn respond_to(self, _: HttpRequest) -> Result<HttpResponse, HttpError> {
|
||||
HttpResponse::build(StatusCode::OK)
|
||||
.content_type("text/plain; charset=utf-8")
|
||||
.body(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static [u8]> for HttpResponse {
|
||||
fn from(val: &'static [u8]) -> Self {
|
||||
HttpResponse::build(StatusCode::OK)
|
||||
|
@ -476,6 +524,17 @@ impl From<&'static [u8]> for HttpResponse {
|
|||
}
|
||||
}
|
||||
|
||||
impl Responder for &'static [u8] {
|
||||
type Item = HttpResponse;
|
||||
type Error = HttpError;
|
||||
|
||||
fn respond_to(self, _: HttpRequest) -> Result<HttpResponse, HttpError> {
|
||||
HttpResponse::build(StatusCode::OK)
|
||||
.content_type("application/octet-stream")
|
||||
.body(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for HttpResponse {
|
||||
fn from(val: String) -> Self {
|
||||
HttpResponse::build(StatusCode::OK)
|
||||
|
@ -485,6 +544,17 @@ impl From<String> for HttpResponse {
|
|||
}
|
||||
}
|
||||
|
||||
impl Responder for String {
|
||||
type Item = HttpResponse;
|
||||
type Error = HttpError;
|
||||
|
||||
fn respond_to(self, _: HttpRequest) -> Result<HttpResponse, HttpError> {
|
||||
HttpResponse::build(StatusCode::OK)
|
||||
.content_type("text/plain; charset=utf-8")
|
||||
.body(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a String> for HttpResponse {
|
||||
fn from(val: &'a String) -> Self {
|
||||
HttpResponse::build(StatusCode::OK)
|
||||
|
@ -494,6 +564,17 @@ impl<'a> From<&'a String> for HttpResponse {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> Responder for &'a String {
|
||||
type Item = HttpResponse;
|
||||
type Error = HttpError;
|
||||
|
||||
fn respond_to(self, _: HttpRequest) -> Result<HttpResponse, HttpError> {
|
||||
HttpResponse::build(StatusCode::OK)
|
||||
.content_type("text/plain; charset=utf-8")
|
||||
.body(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Bytes> for HttpResponse {
|
||||
fn from(val: Bytes) -> Self {
|
||||
HttpResponse::build(StatusCode::OK)
|
||||
|
@ -503,6 +584,17 @@ impl From<Bytes> for HttpResponse {
|
|||
}
|
||||
}
|
||||
|
||||
impl Responder for Bytes {
|
||||
type Item = HttpResponse;
|
||||
type Error = HttpError;
|
||||
|
||||
fn respond_to(self, _: HttpRequest) -> Result<HttpResponse, HttpError> {
|
||||
HttpResponse::build(StatusCode::OK)
|
||||
.content_type("application/octet-stream")
|
||||
.body(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BytesMut> for HttpResponse {
|
||||
fn from(val: BytesMut) -> Self {
|
||||
HttpResponse::build(StatusCode::OK)
|
||||
|
@ -512,19 +604,160 @@ impl From<BytesMut> for HttpResponse {
|
|||
}
|
||||
}
|
||||
|
||||
impl Responder for BytesMut {
|
||||
type Item = HttpResponse;
|
||||
type Error = HttpError;
|
||||
|
||||
fn respond_to(self, _: HttpRequest) -> Result<HttpResponse, HttpError> {
|
||||
HttpResponse::build(StatusCode::OK)
|
||||
.content_type("application/octet-stream")
|
||||
.body(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InnerHttpResponse {
|
||||
version: Option<Version>,
|
||||
headers: HeaderMap,
|
||||
status: StatusCode,
|
||||
reason: Option<&'static str>,
|
||||
body: Body,
|
||||
chunked: bool,
|
||||
encoding: ContentEncoding,
|
||||
connection_type: Option<ConnectionType>,
|
||||
response_size: u64,
|
||||
error: Option<Error>,
|
||||
}
|
||||
|
||||
impl InnerHttpResponse {
|
||||
|
||||
#[inline]
|
||||
fn new(status: StatusCode, body: Body) -> InnerHttpResponse {
|
||||
InnerHttpResponse {
|
||||
version: None,
|
||||
headers: HeaderMap::with_capacity(16),
|
||||
status: status,
|
||||
reason: None,
|
||||
body: body,
|
||||
chunked: false,
|
||||
encoding: ContentEncoding::Auto,
|
||||
connection_type: None,
|
||||
response_size: 0,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal use only! unsafe
|
||||
struct Pool(VecDeque<Box<InnerHttpResponse>>);
|
||||
|
||||
thread_local!(static POOL: RefCell<Pool> =
|
||||
RefCell::new(Pool(VecDeque::with_capacity(128))));
|
||||
|
||||
impl Pool {
|
||||
|
||||
#[inline]
|
||||
fn get(status: StatusCode) -> Box<InnerHttpResponse> {
|
||||
POOL.with(|pool| {
|
||||
if let Some(mut resp) = pool.borrow_mut().0.pop_front() {
|
||||
resp.body = Body::Empty;
|
||||
resp.status = status;
|
||||
resp
|
||||
} else {
|
||||
Box::new(InnerHttpResponse::new(status, Body::Empty))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn with_body(status: StatusCode, body: Body) -> Box<InnerHttpResponse> {
|
||||
POOL.with(|pool| {
|
||||
if let Some(mut resp) = pool.borrow_mut().0.pop_front() {
|
||||
resp.status = status;
|
||||
resp.body = body;
|
||||
resp
|
||||
} else {
|
||||
Box::new(InnerHttpResponse::new(status, body))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(boxed_local, inline_always))]
|
||||
fn release(mut inner: Box<InnerHttpResponse>) {
|
||||
POOL.with(|pool| {
|
||||
let v = &mut pool.borrow_mut().0;
|
||||
if v.len() < 128 {
|
||||
inner.headers.clear();
|
||||
inner.version = None;
|
||||
inner.chunked = false;
|
||||
inner.reason = None;
|
||||
inner.encoding = ContentEncoding::Auto;
|
||||
inner.connection_type = None;
|
||||
inner.response_size = 0;
|
||||
inner.error = None;
|
||||
v.push_front(inner);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
use time::Duration;
|
||||
use http::{Method, Uri};
|
||||
use body::Binary;
|
||||
use {headers, httpcodes};
|
||||
|
||||
#[test]
|
||||
fn test_debug() {
|
||||
let resp = HttpResponse::Ok().finish().unwrap();
|
||||
let dbg = format!("{:?}", resp);
|
||||
assert!(dbg.contains("HttpResponse"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_cookies() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(header::COOKIE,
|
||||
header::HeaderValue::from_static("cookie1=value1; cookie2=value2"));
|
||||
|
||||
let req = HttpRequest::new(
|
||||
Method::GET, Uri::from_str("/").unwrap(), Version::HTTP_11, headers, None);
|
||||
let cookies = req.cookies().unwrap();
|
||||
|
||||
let resp = httpcodes::HTTPOk
|
||||
.build()
|
||||
.cookie(headers::Cookie::build("name", "value")
|
||||
.domain("www.rust-lang.org")
|
||||
.path("/test")
|
||||
.http_only(true)
|
||||
.max_age(Duration::days(1))
|
||||
.finish())
|
||||
.del_cookie(&cookies[0])
|
||||
.body(Body::Empty);
|
||||
|
||||
assert!(resp.is_ok());
|
||||
let resp = resp.unwrap();
|
||||
|
||||
let mut val: Vec<_> = resp.headers().get_all("Set-Cookie")
|
||||
.iter().map(|v| v.to_str().unwrap().to_owned()).collect();
|
||||
val.sort();
|
||||
assert!(val[0].starts_with("cookie1=; Max-Age=0;"));
|
||||
assert_eq!(
|
||||
val[1],"name=value; HttpOnly; Path=/test; Domain=www.rust-lang.org; Max-Age=86400");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_builder() {
|
||||
let resp = HttpResponse::Ok()
|
||||
.status(StatusCode::NO_CONTENT)
|
||||
.header("X-TEST", "value")
|
||||
.version(Version::HTTP_10)
|
||||
.finish().unwrap();
|
||||
assert_eq!(resp.version(), Some(Version::HTTP_10));
|
||||
assert_eq!(resp.status(), StatusCode::NO_CONTENT)
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -588,6 +821,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_into_response() {
|
||||
let req = HttpRequest::default();
|
||||
|
||||
let resp: HttpResponse = "test".into();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
|
@ -595,6 +830,13 @@ mod tests {
|
|||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.body().binary().unwrap(), &Binary::from("test"));
|
||||
|
||||
let resp: HttpResponse = "test".respond_to(req.clone()).ok().unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
header::HeaderValue::from_static("text/plain; charset=utf-8"));
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.body().binary().unwrap(), &Binary::from("test"));
|
||||
|
||||
let resp: HttpResponse = b"test".as_ref().into();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
|
@ -602,6 +844,13 @@ mod tests {
|
|||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.body().binary().unwrap(), &Binary::from(b"test".as_ref()));
|
||||
|
||||
let resp: HttpResponse = b"test".as_ref().respond_to(req.clone()).ok().unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
header::HeaderValue::from_static("application/octet-stream"));
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.body().binary().unwrap(), &Binary::from(b"test".as_ref()));
|
||||
|
||||
let resp: HttpResponse = "test".to_owned().into();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
|
@ -609,6 +858,13 @@ mod tests {
|
|||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.body().binary().unwrap(), &Binary::from("test".to_owned()));
|
||||
|
||||
let resp: HttpResponse = "test".to_owned().respond_to(req.clone()).ok().unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
header::HeaderValue::from_static("text/plain; charset=utf-8"));
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.body().binary().unwrap(), &Binary::from("test".to_owned()));
|
||||
|
||||
let resp: HttpResponse = (&"test".to_owned()).into();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
|
@ -616,6 +872,13 @@ mod tests {
|
|||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.body().binary().unwrap(), &Binary::from((&"test".to_owned())));
|
||||
|
||||
let resp: HttpResponse = (&"test".to_owned()).respond_to(req.clone()).ok().unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
header::HeaderValue::from_static("text/plain; charset=utf-8"));
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.body().binary().unwrap(), &Binary::from((&"test".to_owned())));
|
||||
|
||||
let b = Bytes::from_static(b"test");
|
||||
let resp: HttpResponse = b.into();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
@ -624,6 +887,14 @@ mod tests {
|
|||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.body().binary().unwrap(), &Binary::from(Bytes::from_static(b"test")));
|
||||
|
||||
let b = Bytes::from_static(b"test");
|
||||
let resp: HttpResponse = b.respond_to(req.clone()).ok().unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
header::HeaderValue::from_static("application/octet-stream"));
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.body().binary().unwrap(), &Binary::from(Bytes::from_static(b"test")));
|
||||
|
||||
let b = BytesMut::from("test");
|
||||
let resp: HttpResponse = b.into();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
@ -631,5 +902,13 @@ mod tests {
|
|||
header::HeaderValue::from_static("application/octet-stream"));
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.body().binary().unwrap(), &Binary::from(BytesMut::from("test")));
|
||||
|
||||
let b = BytesMut::from("test");
|
||||
let resp: HttpResponse = b.respond_to(req.clone()).ok().unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
header::HeaderValue::from_static("application/octet-stream"));
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.body().binary().unwrap(), &Binary::from(BytesMut::from("test")));
|
||||
}
|
||||
}
|
||||
|
|
213
src/info.rs
Normal file
213
src/info.rs
Normal file
|
@ -0,0 +1,213 @@
|
|||
use std::str::FromStr;
|
||||
use http::header::{self, HeaderName};
|
||||
use httprequest::HttpRequest;
|
||||
|
||||
const X_FORWARDED_FOR: &str = "X-FORWARDED-FOR";
|
||||
const X_FORWARDED_HOST: &str = "X-FORWARDED-HOST";
|
||||
const X_FORWARDED_PROTO: &str = "X-FORWARDED-PROTO";
|
||||
|
||||
|
||||
/// `HttpRequest` connection information
|
||||
pub struct ConnectionInfo<'a> {
|
||||
scheme: &'a str,
|
||||
host: &'a str,
|
||||
remote: Option<&'a str>,
|
||||
peer: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> ConnectionInfo<'a> {
|
||||
|
||||
/// Create *ConnectionInfo* instance for a request.
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(cyclomatic_complexity))]
|
||||
pub fn new<S>(req: &'a HttpRequest<S>) -> ConnectionInfo<'a> {
|
||||
let mut host = None;
|
||||
let mut scheme = None;
|
||||
let mut remote = None;
|
||||
let mut peer = None;
|
||||
|
||||
// load forwarded header
|
||||
for hdr in req.headers().get_all(header::FORWARDED) {
|
||||
if let Ok(val) = hdr.to_str() {
|
||||
for pair in val.split(';') {
|
||||
for el in pair.split(',') {
|
||||
let mut items = el.trim().splitn(2, '=');
|
||||
if let Some(name) = items.next() {
|
||||
if let Some(val) = items.next() {
|
||||
match &name.to_lowercase() as &str {
|
||||
"for" => if remote.is_none() {
|
||||
remote = Some(val.trim());
|
||||
},
|
||||
"proto" => if scheme.is_none() {
|
||||
scheme = Some(val.trim());
|
||||
},
|
||||
"host" => if host.is_none() {
|
||||
host = Some(val.trim());
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// scheme
|
||||
if scheme.is_none() {
|
||||
if let Some(h) = req.headers().get(
|
||||
HeaderName::from_str(X_FORWARDED_PROTO).unwrap()) {
|
||||
if let Ok(h) = h.to_str() {
|
||||
scheme = h.split(',').next().map(|v| v.trim());
|
||||
}
|
||||
}
|
||||
if scheme.is_none() {
|
||||
scheme = req.uri().scheme_part().map(|a| a.as_str());
|
||||
if scheme.is_none() {
|
||||
if let Some(router) = req.router() {
|
||||
if router.server_settings().secure() {
|
||||
scheme = Some("https")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// host
|
||||
if host.is_none() {
|
||||
if let Some(h) = req.headers().get(HeaderName::from_str(X_FORWARDED_HOST).unwrap()) {
|
||||
if let Ok(h) = h.to_str() {
|
||||
host = h.split(',').next().map(|v| v.trim());
|
||||
}
|
||||
}
|
||||
if host.is_none() {
|
||||
if let Some(h) = req.headers().get(header::HOST) {
|
||||
host = h.to_str().ok();
|
||||
}
|
||||
if host.is_none() {
|
||||
host = req.uri().authority_part().map(|a| a.as_str());
|
||||
if host.is_none() {
|
||||
if let Some(router) = req.router() {
|
||||
host = Some(router.server_settings().host());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remote addr
|
||||
if remote.is_none() {
|
||||
if let Some(h) = req.headers().get(
|
||||
HeaderName::from_str(X_FORWARDED_FOR).unwrap()) {
|
||||
if let Ok(h) = h.to_str() {
|
||||
remote = h.split(',').next().map(|v| v.trim());
|
||||
}
|
||||
}
|
||||
if remote.is_none() { // get peeraddr from socketaddr
|
||||
peer = req.peer_addr().map(|addr| format!("{}", addr));
|
||||
}
|
||||
}
|
||||
|
||||
ConnectionInfo {
|
||||
scheme: scheme.unwrap_or("http"),
|
||||
host: host.unwrap_or("localhost"),
|
||||
remote: remote,
|
||||
peer: peer,
|
||||
}
|
||||
}
|
||||
|
||||
/// Scheme of the request.
|
||||
///
|
||||
/// Scheme is resolved through the following headers, in this order:
|
||||
///
|
||||
/// - Forwarded
|
||||
/// - X-Forwarded-Proto
|
||||
/// - Uri
|
||||
#[inline]
|
||||
pub fn scheme(&self) -> &str {
|
||||
self.scheme
|
||||
}
|
||||
|
||||
/// Hostname of the request.
|
||||
///
|
||||
/// Hostname is resolved through the following headers, in this order:
|
||||
///
|
||||
/// - Forwarded
|
||||
/// - X-Forwarded-Host
|
||||
/// - Host
|
||||
/// - Uri
|
||||
/// - Server hostname
|
||||
pub fn host(&self) -> &str {
|
||||
self.host
|
||||
}
|
||||
|
||||
/// Remote IP of client initiated HTTP request.
|
||||
///
|
||||
/// The IP is resolved through the following headers, in this order:
|
||||
///
|
||||
/// - Forwarded
|
||||
/// - X-Forwarded-For
|
||||
/// - peername of opened socket
|
||||
#[inline]
|
||||
pub fn remote(&self) -> Option<&str> {
|
||||
if let Some(r) = self.remote {
|
||||
Some(r)
|
||||
} else if let Some(ref peer) = self.peer {
|
||||
Some(peer)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use http::header::HeaderValue;
|
||||
|
||||
#[test]
|
||||
fn test_forwarded() {
|
||||
let req = HttpRequest::default();
|
||||
let info = ConnectionInfo::new(&req);
|
||||
assert_eq!(info.scheme(), "http");
|
||||
assert_eq!(info.host(), "localhost");
|
||||
|
||||
let mut req = HttpRequest::default();
|
||||
req.headers_mut().insert(
|
||||
header::FORWARDED,
|
||||
HeaderValue::from_static(
|
||||
"for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org"));
|
||||
|
||||
let info = ConnectionInfo::new(&req);
|
||||
assert_eq!(info.scheme(), "https");
|
||||
assert_eq!(info.host(), "rust-lang.org");
|
||||
assert_eq!(info.remote(), Some("192.0.2.60"));
|
||||
|
||||
let mut req = HttpRequest::default();
|
||||
req.headers_mut().insert(
|
||||
header::HOST, HeaderValue::from_static("rust-lang.org"));
|
||||
|
||||
let info = ConnectionInfo::new(&req);
|
||||
assert_eq!(info.scheme(), "http");
|
||||
assert_eq!(info.host(), "rust-lang.org");
|
||||
assert_eq!(info.remote(), None);
|
||||
|
||||
let mut req = HttpRequest::default();
|
||||
req.headers_mut().insert(
|
||||
HeaderName::from_str(X_FORWARDED_FOR).unwrap(), HeaderValue::from_static("192.0.2.60"));
|
||||
let info = ConnectionInfo::new(&req);
|
||||
assert_eq!(info.remote(), Some("192.0.2.60"));
|
||||
|
||||
let mut req = HttpRequest::default();
|
||||
req.headers_mut().insert(
|
||||
HeaderName::from_str(X_FORWARDED_HOST).unwrap(), HeaderValue::from_static("192.0.2.60"));
|
||||
let info = ConnectionInfo::new(&req);
|
||||
assert_eq!(info.host(), "192.0.2.60");
|
||||
assert_eq!(info.remote(), None);
|
||||
|
||||
let mut req = HttpRequest::default();
|
||||
req.headers_mut().insert(
|
||||
HeaderName::from_str(X_FORWARDED_PROTO).unwrap(), HeaderValue::from_static("https"));
|
||||
let info = ConnectionInfo::new(&req);
|
||||
assert_eq!(info.scheme(), "https");
|
||||
}
|
||||
}
|
215
src/json.rs
Normal file
215
src/json.rs
Normal file
|
@ -0,0 +1,215 @@
|
|||
use bytes::BytesMut;
|
||||
use futures::{Poll, Future, Stream};
|
||||
use http::header::CONTENT_LENGTH;
|
||||
|
||||
use serde_json;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use error::{Error, JsonPayloadError};
|
||||
use handler::Responder;
|
||||
use httprequest::HttpRequest;
|
||||
use httpresponse::HttpResponse;
|
||||
|
||||
/// Json response helper
|
||||
///
|
||||
/// The `Json` type allows you to respond with well-formed JSON data: simply return a value of
|
||||
/// type Json<T> where T is the type of a structure to serialize into *JSON*. The
|
||||
/// type `T` must implement the `Serialize` trait from *serde*.
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// # #[macro_use] extern crate serde_derive;
|
||||
/// # use actix_web::*;
|
||||
/// #
|
||||
/// #[derive(Serialize)]
|
||||
/// struct MyObj {
|
||||
/// name: String,
|
||||
/// }
|
||||
///
|
||||
/// fn index(req: HttpRequest) -> Result<Json<MyObj>> {
|
||||
/// Ok(Json(MyObj{name: req.match_info().query("name")?}))
|
||||
/// }
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
pub struct Json<T: Serialize> (pub T);
|
||||
|
||||
impl<T: Serialize> Responder for Json<T> {
|
||||
type Item = HttpResponse;
|
||||
type Error = Error;
|
||||
|
||||
fn respond_to(self, _: HttpRequest) -> Result<HttpResponse, Error> {
|
||||
let body = serde_json::to_string(&self.0)?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(body)?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Request payload json parser that resolves to a deserialized `T` value.
|
||||
///
|
||||
/// Returns error:
|
||||
///
|
||||
/// * content type is not `application/json`
|
||||
/// * content length is greater than 256k
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// # extern crate futures;
|
||||
/// # #[macro_use] extern crate serde_derive;
|
||||
/// use actix_web::*;
|
||||
/// use futures::future::Future;
|
||||
///
|
||||
/// #[derive(Deserialize, Debug)]
|
||||
/// struct MyObj {
|
||||
/// name: String,
|
||||
/// }
|
||||
///
|
||||
/// fn index(mut req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
/// req.json() // <- get JsonBody future
|
||||
/// .from_err()
|
||||
/// .and_then(|val: MyObj| { // <- deserialized value
|
||||
/// println!("==== BODY ==== {:?}", val);
|
||||
/// Ok(httpcodes::HTTPOk.into())
|
||||
/// }).responder()
|
||||
/// }
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
pub struct JsonBody<S, T: DeserializeOwned>{
|
||||
limit: usize,
|
||||
ct: &'static str,
|
||||
req: Option<HttpRequest<S>>,
|
||||
fut: Option<Box<Future<Item=T, Error=JsonPayloadError>>>,
|
||||
}
|
||||
|
||||
impl<S, T: DeserializeOwned> JsonBody<S, T> {
|
||||
|
||||
/// Create `JsonBody` for request.
|
||||
pub fn from_request(req: &HttpRequest<S>) -> Self {
|
||||
JsonBody{
|
||||
limit: 262_144,
|
||||
req: Some(req.clone()),
|
||||
fut: None,
|
||||
ct: "application/json",
|
||||
}
|
||||
}
|
||||
|
||||
/// Change max size of payload. By default max size is 256Kb
|
||||
pub fn limit(mut self, limit: usize) -> Self {
|
||||
self.limit = limit;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set allowed content type.
|
||||
///
|
||||
/// By default *application/json* content type is used. Set content type
|
||||
/// to empty string if you want to disable content type check.
|
||||
pub fn content_type(mut self, ct: &'static str) -> Self {
|
||||
self.ct = ct;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T: DeserializeOwned + 'static> Future for JsonBody<S, T> {
|
||||
type Item = T;
|
||||
type Error = JsonPayloadError;
|
||||
|
||||
fn poll(&mut self) -> Poll<T, JsonPayloadError> {
|
||||
if let Some(req) = self.req.take() {
|
||||
if let Some(len) = req.headers().get(CONTENT_LENGTH) {
|
||||
if let Ok(s) = len.to_str() {
|
||||
if let Ok(len) = s.parse::<usize>() {
|
||||
if len > self.limit {
|
||||
return Err(JsonPayloadError::Overflow);
|
||||
}
|
||||
} else {
|
||||
return Err(JsonPayloadError::Overflow);
|
||||
}
|
||||
}
|
||||
}
|
||||
// check content-type
|
||||
if !self.ct.is_empty() && req.content_type() != self.ct {
|
||||
return Err(JsonPayloadError::ContentType)
|
||||
}
|
||||
|
||||
let limit = self.limit;
|
||||
let fut = req.payload().readany()
|
||||
.from_err()
|
||||
.fold(BytesMut::new(), move |mut body, chunk| {
|
||||
if (body.len() + chunk.len()) > limit {
|
||||
Err(JsonPayloadError::Overflow)
|
||||
} else {
|
||||
body.extend_from_slice(&chunk);
|
||||
Ok(body)
|
||||
}
|
||||
})
|
||||
.and_then(|body| Ok(serde_json::from_slice::<T>(&body)?));
|
||||
self.fut = Some(Box::new(fut));
|
||||
}
|
||||
|
||||
self.fut.as_mut().expect("JsonBody could not be used second time").poll()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bytes::Bytes;
|
||||
use http::header;
|
||||
use futures::Async;
|
||||
|
||||
impl PartialEq for JsonPayloadError {
|
||||
fn eq(&self, other: &JsonPayloadError) -> bool {
|
||||
match *self {
|
||||
JsonPayloadError::Overflow => match *other {
|
||||
JsonPayloadError::Overflow => true,
|
||||
_ => false,
|
||||
},
|
||||
JsonPayloadError::ContentType => match *other {
|
||||
JsonPayloadError::ContentType => true,
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||
struct MyObject {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json() {
|
||||
let json = Json(MyObject{name: "test".to_owned()});
|
||||
let resp = json.respond_to(HttpRequest::default()).unwrap();
|
||||
assert_eq!(resp.headers().get(header::CONTENT_TYPE).unwrap(), "application/json");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_body() {
|
||||
let mut req = HttpRequest::default();
|
||||
let mut json = req.json::<MyObject>();
|
||||
assert_eq!(json.poll().err().unwrap(), JsonPayloadError::ContentType);
|
||||
|
||||
let mut json = req.json::<MyObject>().content_type("text/json");
|
||||
req.headers_mut().insert(header::CONTENT_TYPE,
|
||||
header::HeaderValue::from_static("application/json"));
|
||||
assert_eq!(json.poll().err().unwrap(), JsonPayloadError::ContentType);
|
||||
|
||||
let mut json = req.json::<MyObject>().limit(100);
|
||||
req.headers_mut().insert(header::CONTENT_TYPE,
|
||||
header::HeaderValue::from_static("application/json"));
|
||||
req.headers_mut().insert(header::CONTENT_LENGTH,
|
||||
header::HeaderValue::from_static("10000"));
|
||||
assert_eq!(json.poll().err().unwrap(), JsonPayloadError::Overflow);
|
||||
|
||||
req.headers_mut().insert(header::CONTENT_LENGTH,
|
||||
header::HeaderValue::from_static("16"));
|
||||
req.payload_mut().unread_data(Bytes::from_static(b"{\"name\": \"test\"}"));
|
||||
let mut json = req.json::<MyObject>();
|
||||
assert_eq!(json.poll().ok().unwrap(), Async::Ready(MyObject{name: "test".to_owned()}));
|
||||
}
|
||||
|
||||
}
|
126
src/lib.rs
126
src/lib.rs
|
@ -1,4 +1,39 @@
|
|||
//! Web framework for [Actix](https://github.com/actix/actix)
|
||||
//! Actix web is a small, fast, down-to-earth, open source rust web framework.
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use actix_web::*;
|
||||
//!
|
||||
//! fn index(req: HttpRequest) -> String {
|
||||
//! format!("Hello {}!", &req.match_info()["name"])
|
||||
//! }
|
||||
//!
|
||||
//! fn main() {
|
||||
//! HttpServer::new(
|
||||
//! || Application::new()
|
||||
//! .resource("/{name}", |r| r.f(index)))
|
||||
//! .bind("127.0.0.1:8080")?
|
||||
//! .start()
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Documentation
|
||||
//!
|
||||
//! * [User Guide](http://actix.github.io/actix-web/guide/)
|
||||
//! * 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`
|
||||
//! * Transparent content compression/decompression (br, gzip, deflate)
|
||||
//! * Configurable request routing
|
||||
//! * Multipart streams
|
||||
//! * Middlewares (`Logger`, `Session`, `DefaultHeaders`)
|
||||
//! * Graceful server shutdown
|
||||
//! * Built on top of [Actix](https://github.com/actix/actix).
|
||||
|
||||
#![cfg_attr(actix_nightly, feature(
|
||||
specialization, // for impl ErrorResponse for std::error::Error
|
||||
|
@ -11,13 +46,15 @@ extern crate bytes;
|
|||
extern crate sha1;
|
||||
extern crate regex;
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
#[macro_use]
|
||||
extern crate failure;
|
||||
#[macro_use]
|
||||
extern crate futures;
|
||||
extern crate tokio_io;
|
||||
extern crate tokio_core;
|
||||
|
||||
extern crate failure;
|
||||
#[macro_use] extern crate failure_derive;
|
||||
|
||||
extern crate mio;
|
||||
extern crate net2;
|
||||
extern crate cookie;
|
||||
extern crate http;
|
||||
extern crate httparse;
|
||||
|
@ -31,10 +68,13 @@ extern crate serde_json;
|
|||
extern crate flate2;
|
||||
extern crate brotli2;
|
||||
extern crate percent_encoding;
|
||||
extern crate actix;
|
||||
extern crate smallvec;
|
||||
extern crate num_cpus;
|
||||
extern crate h2 as http2;
|
||||
#[macro_use] extern crate actix;
|
||||
|
||||
// extern crate redis_async;
|
||||
#[cfg(test)]
|
||||
#[macro_use] extern crate serde_derive;
|
||||
|
||||
#[cfg(feature="tls")]
|
||||
extern crate native_tls;
|
||||
|
@ -49,18 +89,20 @@ extern crate tokio_openssl;
|
|||
mod application;
|
||||
mod body;
|
||||
mod context;
|
||||
mod date;
|
||||
mod helpers;
|
||||
mod encoding;
|
||||
mod httprequest;
|
||||
mod httpresponse;
|
||||
mod payload;
|
||||
mod resource;
|
||||
mod recognizer;
|
||||
mod info;
|
||||
mod json;
|
||||
mod route;
|
||||
//mod task;
|
||||
mod router;
|
||||
mod param;
|
||||
mod resource;
|
||||
mod handler;
|
||||
mod pipeline;
|
||||
mod staticfiles;
|
||||
mod server;
|
||||
mod worker;
|
||||
mod channel;
|
||||
mod wsframe;
|
||||
mod wsproto;
|
||||
|
@ -69,33 +111,69 @@ mod h2;
|
|||
mod h1writer;
|
||||
mod h2writer;
|
||||
|
||||
pub mod fs;
|
||||
pub mod ws;
|
||||
pub mod dev;
|
||||
pub mod error;
|
||||
pub mod httpcodes;
|
||||
pub mod multipart;
|
||||
pub mod middlewares;
|
||||
pub use error::{Error, Result};
|
||||
pub use encoding::ContentEncoding;
|
||||
pub mod middleware;
|
||||
pub mod pred;
|
||||
pub mod test;
|
||||
pub mod payload;
|
||||
pub use error::{Error, Result, ResponseError};
|
||||
pub use body::{Body, Binary};
|
||||
pub use json::{Json};
|
||||
pub use application::Application;
|
||||
pub use httprequest::{HttpRequest, UrlEncoded};
|
||||
pub use httprequest::HttpRequest;
|
||||
pub use httpresponse::HttpResponse;
|
||||
pub use payload::{Payload, PayloadItem};
|
||||
pub use route::Reply;
|
||||
pub use handler::{Reply, Responder, NormalizePath, AsyncResponder};
|
||||
pub use route::Route;
|
||||
pub use resource::Resource;
|
||||
pub use recognizer::Params;
|
||||
pub use server::HttpServer;
|
||||
pub use context::HttpContext;
|
||||
pub use staticfiles::StaticFiles;
|
||||
|
||||
// re-exports
|
||||
pub use http::{Method, StatusCode, Version};
|
||||
pub use cookie::Cookie;
|
||||
pub use http_range::HttpRange;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature="tls")]
|
||||
pub use native_tls::Pkcs12;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature="openssl")]
|
||||
pub use openssl::pkcs12::Pkcs12;
|
||||
|
||||
pub mod headers {
|
||||
//! Headers implementation
|
||||
|
||||
pub use encoding::ContentEncoding;
|
||||
pub use httpresponse::ConnectionType;
|
||||
|
||||
pub use cookie::Cookie;
|
||||
pub use cookie::CookieBuilder;
|
||||
pub use http_range::HttpRange;
|
||||
}
|
||||
|
||||
pub mod dev {
|
||||
//! The `actix-web` prelude for library developers
|
||||
//!
|
||||
//! The purpose of this module is to alleviate imports of many common actix traits
|
||||
//! by adding a glob import to the top of actix heavy modules:
|
||||
//!
|
||||
//! ```
|
||||
//! # #![allow(unused_imports)]
|
||||
//! use actix_web::dev::*;
|
||||
//! ```
|
||||
|
||||
pub use body::BodyStream;
|
||||
pub use info::ConnectionInfo;
|
||||
pub use handler::Handler;
|
||||
pub use json::JsonBody;
|
||||
pub use router::{Router, Pattern};
|
||||
pub use channel::{HttpChannel, HttpHandler, IntoHttpHandler};
|
||||
pub use param::{FromParam, Params};
|
||||
pub use httprequest::{UrlEncoded, RequestBody};
|
||||
pub use httpresponse::HttpResponseBuilder;
|
||||
|
||||
pub use server::{ServerSettings, PauseServer, ResumeServer, StopServer};
|
||||
}
|
||||
|
|
128
src/middleware/defaultheaders.rs
Normal file
128
src/middleware/defaultheaders.rs
Normal file
|
@ -0,0 +1,128 @@
|
|||
//! Default response headers
|
||||
use http::{HeaderMap, HttpTryFrom};
|
||||
use http::header::{HeaderName, HeaderValue, CONTENT_TYPE};
|
||||
|
||||
use httprequest::HttpRequest;
|
||||
use httpresponse::HttpResponse;
|
||||
use middleware::{Response, Middleware};
|
||||
|
||||
/// `Middleware` for setting default response headers.
|
||||
///
|
||||
/// This middleware does not set header if response headers already contains it.
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// use actix_web::*;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let app = Application::new()
|
||||
/// .middleware(
|
||||
/// middleware::DefaultHeaders::build()
|
||||
/// .header("X-Version", "0.2")
|
||||
/// .finish())
|
||||
/// .resource("/test", |r| {
|
||||
/// r.method(Method::GET).f(|_| httpcodes::HTTPOk);
|
||||
/// r.method(Method::HEAD).f(|_| httpcodes::HTTPMethodNotAllowed);
|
||||
/// })
|
||||
/// .finish();
|
||||
/// }
|
||||
/// ```
|
||||
pub struct DefaultHeaders{
|
||||
ct: bool,
|
||||
headers: HeaderMap,
|
||||
}
|
||||
|
||||
impl DefaultHeaders {
|
||||
pub fn build() -> DefaultHeadersBuilder {
|
||||
DefaultHeadersBuilder{ct: false, headers: Some(HeaderMap::new())}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Middleware<S> for DefaultHeaders {
|
||||
|
||||
fn response(&self, _: &mut HttpRequest<S>, mut resp: HttpResponse) -> Response {
|
||||
for (key, value) in self.headers.iter() {
|
||||
if !resp.headers().contains_key(key) {
|
||||
resp.headers_mut().insert(key, value.clone());
|
||||
}
|
||||
}
|
||||
// default content-type
|
||||
if self.ct && !resp.headers().contains_key(CONTENT_TYPE) {
|
||||
resp.headers_mut().insert(
|
||||
CONTENT_TYPE, HeaderValue::from_static("application/octet-stream"));
|
||||
}
|
||||
Response::Done(resp)
|
||||
}
|
||||
}
|
||||
|
||||
/// Structure that follows the builder pattern for building `DefaultHeaders` middleware.
|
||||
#[derive(Debug)]
|
||||
pub struct DefaultHeadersBuilder {
|
||||
ct: bool,
|
||||
headers: Option<HeaderMap>,
|
||||
}
|
||||
|
||||
impl DefaultHeadersBuilder {
|
||||
|
||||
/// Set a header.
|
||||
#[inline]
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(match_wild_err_arm))]
|
||||
pub fn header<K, V>(&mut self, key: K, value: V) -> &mut Self
|
||||
where HeaderName: HttpTryFrom<K>,
|
||||
HeaderValue: HttpTryFrom<V>
|
||||
{
|
||||
if let Some(ref mut headers) = self.headers {
|
||||
match HeaderName::try_from(key) {
|
||||
Ok(key) => {
|
||||
match HeaderValue::try_from(value) {
|
||||
Ok(value) => { headers.append(key, value); }
|
||||
Err(_) => panic!("Can not create header value"),
|
||||
}
|
||||
},
|
||||
Err(_) => panic!("Can not create header name"),
|
||||
};
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set *CONTENT-TYPE* header if response does not contain this header.
|
||||
pub fn content_type(&mut self) -> &mut Self {
|
||||
self.ct = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Finishes building and returns the built `DefaultHeaders` middleware.
|
||||
pub fn finish(&mut self) -> DefaultHeaders {
|
||||
let headers = self.headers.take().expect("cannot reuse middleware builder");
|
||||
DefaultHeaders{ ct: self.ct, headers: headers }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use http::header::CONTENT_TYPE;
|
||||
|
||||
#[test]
|
||||
fn test_default_headers() {
|
||||
let mw = DefaultHeaders::build()
|
||||
.header(CONTENT_TYPE, "0001")
|
||||
.finish();
|
||||
|
||||
let mut req = HttpRequest::default();
|
||||
|
||||
let resp = HttpResponse::Ok().finish().unwrap();
|
||||
let resp = match mw.response(&mut req, resp) {
|
||||
Response::Done(resp) => resp,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001");
|
||||
|
||||
let resp = HttpResponse::Ok().header(CONTENT_TYPE, "0002").finish().unwrap();
|
||||
let resp = match mw.response(&mut req, resp) {
|
||||
Response::Done(resp) => resp,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0002");
|
||||
}
|
||||
}
|
|
@ -9,25 +9,25 @@ use regex::Regex;
|
|||
|
||||
use httprequest::HttpRequest;
|
||||
use httpresponse::HttpResponse;
|
||||
use middlewares::{Middleware, Started, Finished};
|
||||
use middleware::{Middleware, Started, Finished};
|
||||
|
||||
/// `Middleware` for logging request and response info to the terminal.
|
||||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// Create `Logger` middlewares with the specified `format`.
|
||||
/// Create `Logger` middleware with the specified `format`.
|
||||
/// Default `Logger` could be created with `default` method, it uses the default format:
|
||||
///
|
||||
/// ```ignore
|
||||
/// %a %t "%r" %s %b "%{Referrer}i" "%{User-Agent}i" %T
|
||||
/// %a %t "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T
|
||||
/// ```
|
||||
/// ```rust
|
||||
/// extern crate actix_web;
|
||||
/// # extern crate actix_web;
|
||||
/// use actix_web::Application;
|
||||
/// use actix_web::middlewares::Logger;
|
||||
/// use actix_web::middleware::Logger;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let app = Application::default("/")
|
||||
/// let app = Application::new()
|
||||
/// .middleware(Logger::default())
|
||||
/// .middleware(Logger::new("%a %{User-Agent}i"))
|
||||
/// .finish();
|
||||
|
@ -75,7 +75,7 @@ impl Default for Logger {
|
|||
/// Create `Logger` middleware with format:
|
||||
///
|
||||
/// ```ignore
|
||||
/// %a %t "%r" %s %b "%{Referrer}i" "%{User-Agent}i" %T
|
||||
/// %a %t "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T
|
||||
/// ```
|
||||
fn default() -> Logger {
|
||||
Logger { format: Format::default() }
|
||||
|
@ -86,7 +86,7 @@ struct StartTime(time::Tm);
|
|||
|
||||
impl Logger {
|
||||
|
||||
fn log(&self, req: &mut HttpRequest, resp: &HttpResponse) {
|
||||
fn log<S>(&self, req: &mut HttpRequest<S>, resp: &HttpResponse) {
|
||||
let entry_time = req.extensions().get::<StartTime>().unwrap().0;
|
||||
|
||||
let render = |fmt: &mut Formatter| {
|
||||
|
@ -99,20 +99,19 @@ impl Logger {
|
|||
}
|
||||
}
|
||||
|
||||
impl Middleware for Logger {
|
||||
impl<S> Middleware<S> for Logger {
|
||||
|
||||
fn start(&self, req: &mut HttpRequest) -> Started {
|
||||
fn start(&self, req: &mut HttpRequest<S>) -> Started {
|
||||
req.extensions().insert(StartTime(time::now()));
|
||||
Started::Done
|
||||
}
|
||||
|
||||
fn finish(&self, req: &mut HttpRequest, resp: &HttpResponse) -> Finished {
|
||||
fn finish(&self, req: &mut HttpRequest<S>, resp: &HttpResponse) -> Finished {
|
||||
self.log(req, resp);
|
||||
Finished::Done
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A formatting style for the `Logger`, consisting of multiple
|
||||
/// `FormatText`s concatenated into one line.
|
||||
#[derive(Clone)]
|
||||
|
@ -122,7 +121,7 @@ struct Format(Vec<FormatText>);
|
|||
impl Default for Format {
|
||||
/// Return the default formatting style for the `Logger`:
|
||||
fn default() -> Format {
|
||||
Format::new(r#"%a %t "%r" %s %b "%{Referrer}i" "%{User-Agent}i" %T"#)
|
||||
Format::new(r#"%a %t "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T"#)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -200,10 +199,10 @@ pub enum FormatText {
|
|||
|
||||
impl FormatText {
|
||||
|
||||
fn render(&self, fmt: &mut Formatter,
|
||||
req: &HttpRequest,
|
||||
resp: &HttpResponse,
|
||||
entry_time: time::Tm) -> Result<(), fmt::Error>
|
||||
fn render<S>(&self, fmt: &mut Formatter,
|
||||
req: &HttpRequest<S>,
|
||||
resp: &HttpResponse,
|
||||
entry_time: time::Tm) -> Result<(), fmt::Error>
|
||||
{
|
||||
match *self {
|
||||
FormatText::Str(ref string) => fmt.write_str(string),
|
||||
|
@ -224,7 +223,7 @@ impl FormatText {
|
|||
FormatText::Pid => unsafe{libc::getpid().fmt(fmt)},
|
||||
FormatText::Time => {
|
||||
let response_time = time::now() - entry_time;
|
||||
let response_time = (response_time.num_seconds() * 1000) as f64 +
|
||||
let response_time = response_time.num_seconds() as f64 +
|
||||
(response_time.num_nanoseconds().unwrap_or(0) as f64) / 1000000000.0;
|
||||
|
||||
fmt.write_fmt(format_args!("{:.6}", response_time))
|
||||
|
@ -237,8 +236,8 @@ impl FormatText {
|
|||
fmt.write_fmt(format_args!("{:.6}", response_time_ms))
|
||||
},
|
||||
FormatText::RemoteAddr => {
|
||||
if let Some(addr) = req.remote() {
|
||||
addr.fmt(fmt)
|
||||
if let Some(remote) = req.connection_info().remote() {
|
||||
return remote.fmt(fmt);
|
||||
} else {
|
||||
"-".fmt(fmt)
|
||||
}
|
||||
|
@ -292,7 +291,6 @@ mod tests {
|
|||
use time;
|
||||
use http::{Method, Version, StatusCode, Uri};
|
||||
use http::header::{self, HeaderMap};
|
||||
use payload::Payload;
|
||||
|
||||
#[test]
|
||||
fn test_logger() {
|
||||
|
@ -301,8 +299,7 @@ mod tests {
|
|||
let mut headers = HeaderMap::new();
|
||||
headers.insert(header::USER_AGENT, header::HeaderValue::from_static("ACTIX-WEB"));
|
||||
let mut req = HttpRequest::new(
|
||||
Method::GET, Uri::from_str("/").unwrap(),
|
||||
Version::HTTP_11, headers, Payload::empty());
|
||||
Method::GET, Uri::from_str("/").unwrap(), Version::HTTP_11, headers, None);
|
||||
let resp = HttpResponse::build(StatusCode::OK)
|
||||
.header("X-Test", "ttt")
|
||||
.force_close().body(Body::Empty).unwrap();
|
||||
|
@ -333,8 +330,7 @@ mod tests {
|
|||
let mut headers = HeaderMap::new();
|
||||
headers.insert(header::USER_AGENT, header::HeaderValue::from_static("ACTIX-WEB"));
|
||||
let req = HttpRequest::new(
|
||||
Method::GET, Uri::from_str("/").unwrap(),
|
||||
Version::HTTP_11, headers, Payload::empty());
|
||||
Method::GET, Uri::from_str("/").unwrap(), Version::HTTP_11, headers, None);
|
||||
let resp = HttpResponse::build(StatusCode::OK)
|
||||
.force_close().body(Body::Empty).unwrap();
|
||||
let entry_time = time::now();
|
||||
|
@ -352,7 +348,7 @@ mod tests {
|
|||
|
||||
let req = HttpRequest::new(
|
||||
Method::GET, Uri::from_str("/?test").unwrap(),
|
||||
Version::HTTP_11, HeaderMap::new(), Payload::empty());
|
||||
Version::HTTP_11, HeaderMap::new(), None);
|
||||
let resp = HttpResponse::build(StatusCode::OK)
|
||||
.force_close().body(Body::Empty).unwrap();
|
||||
let entry_time = time::now();
|
|
@ -7,7 +7,9 @@ use httpresponse::HttpResponse;
|
|||
|
||||
mod logger;
|
||||
mod session;
|
||||
mod defaultheaders;
|
||||
pub use self::logger::Logger;
|
||||
pub use self::defaultheaders::{DefaultHeaders, DefaultHeadersBuilder};
|
||||
pub use self::session::{RequestSession, Session, SessionImpl, SessionBackend, SessionStorage,
|
||||
CookieSessionError, CookieSessionBackend, CookieSessionBackendBuilder};
|
||||
|
||||
|
@ -44,22 +46,22 @@ pub enum Finished {
|
|||
|
||||
/// Middleware definition
|
||||
#[allow(unused_variables)]
|
||||
pub trait Middleware {
|
||||
pub trait Middleware<S> {
|
||||
|
||||
/// Method is called when request is ready. It may return
|
||||
/// future, which should resolve before next middleware get called.
|
||||
fn start(&self, req: &mut HttpRequest) -> Started {
|
||||
fn start(&self, req: &mut HttpRequest<S>) -> Started {
|
||||
Started::Done
|
||||
}
|
||||
|
||||
/// Method is called when handler returns response,
|
||||
/// but before sending http message to peer.
|
||||
fn response(&self, req: &mut HttpRequest, resp: HttpResponse) -> Response {
|
||||
fn response(&self, req: &mut HttpRequest<S>, resp: HttpResponse) -> Response {
|
||||
Response::Done(resp)
|
||||
}
|
||||
|
||||
/// Method is called after body stream get sent to peer.
|
||||
fn finish(&self, req: &mut HttpRequest, resp: &HttpResponse) -> Finished {
|
||||
fn finish(&self, req: &mut HttpRequest<S>, resp: &HttpResponse) -> Finished {
|
||||
Finished::Done
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
use std::any::Any;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::marker::PhantomData;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde_json;
|
||||
|
@ -13,17 +14,34 @@ use cookie::{CookieJar, Cookie, Key};
|
|||
use futures::Future;
|
||||
use futures::future::{FutureResult, ok as FutOk, err as FutErr};
|
||||
|
||||
use error::{Result, Error, ErrorResponse};
|
||||
use error::{Result, Error, ResponseError};
|
||||
use httprequest::HttpRequest;
|
||||
use httpresponse::HttpResponse;
|
||||
use middlewares::{Middleware, Started, Response};
|
||||
use middleware::{Middleware, Started, Response};
|
||||
|
||||
/// The helper trait to obtain your session data from a request.
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::*;
|
||||
/// use actix_web::middleware::RequestSession;
|
||||
///
|
||||
/// fn index(mut req: HttpRequest) -> Result<&'static str> {
|
||||
/// // access session data
|
||||
/// if let Some(count) = req.session().get::<i32>("counter")? {
|
||||
/// req.session().set("counter", count+1)?;
|
||||
/// } else {
|
||||
/// req.session().set("counter", 1)?;
|
||||
/// }
|
||||
///
|
||||
/// Ok("Welcome!")
|
||||
/// }
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
pub trait RequestSession {
|
||||
fn session(&mut self) -> Session;
|
||||
}
|
||||
|
||||
impl RequestSession for HttpRequest {
|
||||
impl<S> RequestSession for HttpRequest<S> {
|
||||
|
||||
fn session(&mut self) -> Session {
|
||||
if let Some(s_impl) = self.extensions().get_mut::<Arc<SessionImplBox>>() {
|
||||
|
@ -41,6 +59,23 @@ impl RequestSession for HttpRequest {
|
|||
/// Session object could be obtained with
|
||||
/// [`RequestSession::session`](trait.RequestSession.html#tymethod.session)
|
||||
/// method. `RequestSession` trait is implemented for `HttpRequest`.
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::*;
|
||||
/// use actix_web::middleware::RequestSession;
|
||||
///
|
||||
/// fn index(mut req: HttpRequest) -> Result<&'static str> {
|
||||
/// // access session data
|
||||
/// if let Some(count) = req.session().get::<i32>("counter")? {
|
||||
/// req.session().set("counter", count+1)?;
|
||||
/// } else {
|
||||
/// req.session().set("counter", 1)?;
|
||||
/// }
|
||||
///
|
||||
/// Ok("Welcome!")
|
||||
/// }
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
pub struct Session<'a>(&'a mut SessionImpl);
|
||||
|
||||
impl<'a> Session<'a> {
|
||||
|
@ -79,18 +114,34 @@ unsafe impl Send for SessionImplBox {}
|
|||
unsafe impl Sync for SessionImplBox {}
|
||||
|
||||
/// Session storage middleware
|
||||
pub struct SessionStorage<T>(T);
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix;
|
||||
/// # extern crate actix_web;
|
||||
/// # use actix_web::middleware::{SessionStorage, CookieSessionBackend};
|
||||
/// use actix_web::*;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let app = Application::new().middleware(
|
||||
/// SessionStorage::new( // <- create session middleware
|
||||
/// CookieSessionBackend::build(&[0; 32]) // <- create cookie session backend
|
||||
/// .secure(false)
|
||||
/// .finish())
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
pub struct SessionStorage<T, S>(T, PhantomData<S>);
|
||||
|
||||
impl<T: SessionBackend> SessionStorage<T> {
|
||||
impl<S, T: SessionBackend<S>> SessionStorage<T, S> {
|
||||
/// Create session storage
|
||||
pub fn new(backend: T) -> SessionStorage<T> {
|
||||
SessionStorage(backend)
|
||||
pub fn new(backend: T) -> SessionStorage<T, S> {
|
||||
SessionStorage(backend, PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionBackend> Middleware for SessionStorage<T> {
|
||||
impl<S: 'static, T: SessionBackend<S>> Middleware<S> for SessionStorage<T, S> {
|
||||
|
||||
fn start(&self, req: &mut HttpRequest) -> Started {
|
||||
fn start(&self, req: &mut HttpRequest<S>) -> Started {
|
||||
let mut req = req.clone();
|
||||
|
||||
let fut = self.0.from_request(&mut req)
|
||||
|
@ -106,7 +157,7 @@ impl<T: SessionBackend> Middleware for SessionStorage<T> {
|
|||
Started::Future(Box::new(fut))
|
||||
}
|
||||
|
||||
fn response(&self, req: &mut HttpRequest, resp: HttpResponse) -> Response {
|
||||
fn response(&self, req: &mut HttpRequest<S>, resp: HttpResponse) -> Response {
|
||||
if let Some(s_box) = req.extensions().remove::<Arc<SessionImplBox>>() {
|
||||
s_box.0.write(resp)
|
||||
} else {
|
||||
|
@ -133,12 +184,12 @@ pub trait SessionImpl: 'static {
|
|||
|
||||
/// Session's storage backend trait definition.
|
||||
#[doc(hidden)]
|
||||
pub trait SessionBackend: Sized + 'static {
|
||||
pub trait SessionBackend<S>: Sized + 'static {
|
||||
type Session: SessionImpl;
|
||||
type ReadFuture: Future<Item=Self::Session, Error=Error>;
|
||||
|
||||
/// Parse the session from request and load data from a storage backend.
|
||||
fn from_request(&self, request: &mut HttpRequest) -> Self::ReadFuture;
|
||||
fn from_request(&self, request: &mut HttpRequest<S>) -> Self::ReadFuture;
|
||||
}
|
||||
|
||||
/// Dummy session impl, does not do anything
|
||||
|
@ -177,7 +228,7 @@ pub enum CookieSessionError {
|
|||
Serialize(JsonError),
|
||||
}
|
||||
|
||||
impl ErrorResponse for CookieSessionError {}
|
||||
impl ResponseError for CookieSessionError {}
|
||||
|
||||
impl SessionImpl for CookieSession {
|
||||
|
||||
|
@ -225,7 +276,7 @@ impl CookieSessionInner {
|
|||
fn new(key: &[u8]) -> CookieSessionInner {
|
||||
CookieSessionInner {
|
||||
key: Key::from_master(key),
|
||||
name: "actix_session".to_owned(),
|
||||
name: "actix-session".to_owned(),
|
||||
path: "/".to_owned(),
|
||||
domain: None,
|
||||
secure: true }
|
||||
|
@ -258,8 +309,8 @@ impl CookieSessionInner {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn load(&self, req: &mut HttpRequest) -> HashMap<String, String> {
|
||||
if let Ok(cookies) = req.load_cookies() {
|
||||
fn load<S>(&self, req: &mut HttpRequest<S>) -> HashMap<String, String> {
|
||||
if let Ok(cookies) = req.cookies() {
|
||||
for cookie in cookies {
|
||||
if cookie.name() == self.name {
|
||||
let mut jar = CookieJar::new();
|
||||
|
@ -307,7 +358,7 @@ impl CookieSessionBackend {
|
|||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::middlewares::CookieSessionBackend;
|
||||
/// use actix_web::middleware::CookieSessionBackend;
|
||||
///
|
||||
/// let backend = CookieSessionBackend::build(&[0; 32]).finish();
|
||||
/// ```
|
||||
|
@ -316,12 +367,12 @@ impl CookieSessionBackend {
|
|||
}
|
||||
}
|
||||
|
||||
impl SessionBackend for CookieSessionBackend {
|
||||
impl<S> SessionBackend<S> for CookieSessionBackend {
|
||||
|
||||
type Session = CookieSession;
|
||||
type ReadFuture = FutureResult<CookieSession, Error>;
|
||||
|
||||
fn from_request(&self, req: &mut HttpRequest) -> Self::ReadFuture {
|
||||
fn from_request(&self, req: &mut HttpRequest<S>) -> Self::ReadFuture {
|
||||
let state = self.0.load(req);
|
||||
FutOk(
|
||||
CookieSession {
|
||||
|
@ -345,11 +396,12 @@ impl SessionBackend for CookieSessionBackend {
|
|||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
///
|
||||
/// use actix_web::middlewares::CookieSessionBackend;
|
||||
/// use actix_web::middleware::CookieSessionBackend;
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let backend: CookieSessionBackend = CookieSessionBackend::build(&[0; 32])
|
||||
/// .domain("www.rust-lang.org")
|
||||
/// .name("actix_session")
|
||||
/// .path("/")
|
||||
/// .secure(true)
|
||||
/// .finish();
|
||||
|
@ -369,6 +421,12 @@ impl CookieSessionBackendBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the `name` field in the session cookie being built.
|
||||
pub fn name<S: Into<String>>(mut self, value: S) -> CookieSessionBackendBuilder {
|
||||
self.0.name = value.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `domain` field in the session cookie being built.
|
||||
pub fn domain<S: Into<String>>(mut self, value: S) -> CookieSessionBackendBuilder {
|
||||
self.0.domain = Some(value.into());
|
|
@ -9,11 +9,12 @@ use httparse;
|
|||
use bytes::Bytes;
|
||||
use http::HttpTryFrom;
|
||||
use http::header::{self, HeaderMap, HeaderName, HeaderValue};
|
||||
use futures::{Async, Stream, Poll};
|
||||
use futures::{Async, Future, Stream, Poll};
|
||||
use futures::task::{Task, current as current_task};
|
||||
|
||||
use error::{ParseError, PayloadError, MultipartError};
|
||||
use payload::Payload;
|
||||
use httprequest::HttpRequest;
|
||||
|
||||
const MAX_HEADERS: usize = 32;
|
||||
|
||||
|
@ -26,7 +27,8 @@ const MAX_HEADERS: usize = 32;
|
|||
#[derive(Debug)]
|
||||
pub struct Multipart {
|
||||
safety: Safety,
|
||||
inner: Rc<RefCell<InnerMultipart>>,
|
||||
error: Option<MultipartError>,
|
||||
inner: Option<Rc<RefCell<InnerMultipart>>>,
|
||||
}
|
||||
|
||||
///
|
||||
|
@ -66,17 +68,32 @@ struct InnerMultipart {
|
|||
}
|
||||
|
||||
impl Multipart {
|
||||
|
||||
/// Create multipart instance for boundary.
|
||||
pub fn new(boundary: String, payload: Payload) -> Multipart {
|
||||
Multipart {
|
||||
error: None,
|
||||
safety: Safety::new(),
|
||||
inner: Rc::new(RefCell::new(
|
||||
inner: Some(Rc::new(RefCell::new(
|
||||
InnerMultipart {
|
||||
payload: PayloadRef::new(payload),
|
||||
boundary: boundary,
|
||||
state: InnerState::FirstBoundary,
|
||||
item: InnerMultipartItem::None,
|
||||
}))
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create multipart instance for request.
|
||||
pub fn from_request<S>(req: &mut HttpRequest<S>) -> Multipart {
|
||||
match Multipart::boundary(req.headers()) {
|
||||
Ok(boundary) => Multipart::new(boundary, req.payload().clone()),
|
||||
Err(err) =>
|
||||
Multipart {
|
||||
error: Some(err),
|
||||
safety: Safety::new(),
|
||||
inner: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,8 +124,10 @@ impl Stream for Multipart {
|
|||
type Error = MultipartError;
|
||||
|
||||
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
|
||||
if self.safety.current() {
|
||||
self.inner.borrow_mut().poll(&self.safety)
|
||||
if let Some(err) = self.error.take() {
|
||||
Err(err)
|
||||
} else if self.safety.current() {
|
||||
self.inner.as_mut().unwrap().borrow_mut().poll(&self.safety)
|
||||
} else {
|
||||
Ok(Async::NotReady)
|
||||
}
|
||||
|
@ -119,7 +138,7 @@ impl InnerMultipart {
|
|||
|
||||
fn read_headers(payload: &mut Payload) -> Poll<HeaderMap, MultipartError>
|
||||
{
|
||||
match payload.readuntil(b"\r\n\r\n")? {
|
||||
match payload.readuntil(b"\r\n\r\n").poll()? {
|
||||
Async::NotReady => Ok(Async::NotReady),
|
||||
Async::Ready(bytes) => {
|
||||
let mut hdrs = [httparse::EMPTY_HEADER; MAX_HEADERS];
|
||||
|
@ -150,7 +169,7 @@ impl InnerMultipart {
|
|||
fn read_boundary(payload: &mut Payload, boundary: &str) -> Poll<bool, MultipartError>
|
||||
{
|
||||
// TODO: need to read epilogue
|
||||
match payload.readline()? {
|
||||
match payload.readline().poll()? {
|
||||
Async::NotReady => Ok(Async::NotReady),
|
||||
Async::Ready(chunk) => {
|
||||
if chunk.len() == boundary.len() + 4 &&
|
||||
|
@ -175,7 +194,7 @@ impl InnerMultipart {
|
|||
{
|
||||
let mut eof = false;
|
||||
loop {
|
||||
if let Async::Ready(chunk) = payload.readline()? {
|
||||
if let Async::Ready(chunk) = payload.readline().poll()? {
|
||||
if chunk.is_empty() {
|
||||
//ValueError("Could not find starting boundary %r"
|
||||
//% (self._boundary))
|
||||
|
@ -327,7 +346,9 @@ impl InnerMultipart {
|
|||
|
||||
Ok(Async::Ready(Some(
|
||||
MultipartItem::Nested(
|
||||
Multipart{safety: safety.clone(), inner: inner}))))
|
||||
Multipart{safety: safety.clone(),
|
||||
error: None,
|
||||
inner: Some(inner)}))))
|
||||
} else {
|
||||
let field = Rc::new(RefCell::new(InnerField::new(
|
||||
self.payload.clone(), self.boundary.clone(), &headers)?));
|
||||
|
@ -356,10 +377,6 @@ pub struct Field {
|
|||
safety: Safety,
|
||||
}
|
||||
|
||||
/// A field's chunk
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct FieldChunk(pub Bytes);
|
||||
|
||||
impl Field {
|
||||
|
||||
fn new(safety: Safety, headers: HeaderMap,
|
||||
|
@ -382,7 +399,7 @@ impl Field {
|
|||
}
|
||||
|
||||
impl Stream for Field {
|
||||
type Item = FieldChunk;
|
||||
type Item = Bytes;
|
||||
type Error = MultipartError;
|
||||
|
||||
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
|
||||
|
@ -452,15 +469,15 @@ impl InnerField {
|
|||
if *size == 0 {
|
||||
Ok(Async::Ready(None))
|
||||
} else {
|
||||
match payload.readany() {
|
||||
match payload.readany().poll() {
|
||||
Ok(Async::NotReady) => Ok(Async::NotReady),
|
||||
Ok(Async::Ready(None)) => Ok(Async::Ready(None)),
|
||||
Ok(Async::Ready(Some(mut chunk))) => {
|
||||
let len = cmp::min(chunk.0.len() as u64, *size);
|
||||
let len = cmp::min(chunk.len() as u64, *size);
|
||||
*size -= len;
|
||||
let ch = chunk.0.split_to(len as usize);
|
||||
if !chunk.0.is_empty() {
|
||||
payload.unread_data(chunk.0);
|
||||
let ch = chunk.split_to(len as usize);
|
||||
if !chunk.is_empty() {
|
||||
payload.unread_data(chunk);
|
||||
}
|
||||
Ok(Async::Ready(Some(ch)))
|
||||
},
|
||||
|
@ -473,12 +490,12 @@ impl InnerField {
|
|||
/// The `Content-Length` header for body part is not necessary.
|
||||
fn read_stream(payload: &mut Payload, boundary: &str) -> Poll<Option<Bytes>, MultipartError>
|
||||
{
|
||||
match payload.readuntil(b"\r")? {
|
||||
match payload.readuntil(b"\r").poll()? {
|
||||
Async::NotReady => Ok(Async::NotReady),
|
||||
Async::Ready(mut chunk) => {
|
||||
if chunk.len() == 1 {
|
||||
payload.unread_data(chunk);
|
||||
match payload.readexactly(boundary.len() + 4)? {
|
||||
match payload.readexactly(boundary.len() + 4).poll()? {
|
||||
Async::NotReady => Ok(Async::NotReady),
|
||||
Async::Ready(chunk) => {
|
||||
if &chunk[..2] == b"\r\n" && &chunk[2..4] == b"--" &&
|
||||
|
@ -501,13 +518,13 @@ impl InnerField {
|
|||
}
|
||||
}
|
||||
|
||||
fn poll(&mut self, s: &Safety) -> Poll<Option<FieldChunk>, MultipartError> {
|
||||
fn poll(&mut self, s: &Safety) -> Poll<Option<Bytes>, MultipartError> {
|
||||
if self.payload.is_none() {
|
||||
return Ok(Async::Ready(None))
|
||||
}
|
||||
if self.eof {
|
||||
if let Some(payload) = self.payload.as_ref().unwrap().get_mut(s) {
|
||||
match payload.readline()? {
|
||||
match payload.readline().poll()? {
|
||||
Async::NotReady =>
|
||||
return Ok(Async::NotReady),
|
||||
Async::Ready(chunk) => {
|
||||
|
@ -533,10 +550,10 @@ impl InnerField {
|
|||
|
||||
match res {
|
||||
Async::NotReady => Async::NotReady,
|
||||
Async::Ready(Some(bytes)) => Async::Ready(Some(FieldChunk(bytes))),
|
||||
Async::Ready(Some(bytes)) => Async::Ready(Some(bytes)),
|
||||
Async::Ready(None) => {
|
||||
self.eof = true;
|
||||
match payload.readline()? {
|
||||
match payload.readline().poll()? {
|
||||
Async::NotReady => Async::NotReady,
|
||||
Async::Ready(chunk) => {
|
||||
assert_eq!(
|
||||
|
@ -713,7 +730,7 @@ mod tests {
|
|||
|
||||
match field.poll() {
|
||||
Ok(Async::Ready(Some(chunk))) =>
|
||||
assert_eq!(chunk.0, "test"),
|
||||
assert_eq!(chunk, "test"),
|
||||
_ => unreachable!()
|
||||
}
|
||||
match field.poll() {
|
||||
|
@ -736,7 +753,7 @@ mod tests {
|
|||
|
||||
match field.poll() {
|
||||
Ok(Async::Ready(Some(chunk))) =>
|
||||
assert_eq!(chunk.0, "data"),
|
||||
assert_eq!(chunk, "data"),
|
||||
_ => unreachable!()
|
||||
}
|
||||
match field.poll() {
|
||||
|
|
190
src/param.rs
Normal file
190
src/param.rs
Normal file
|
@ -0,0 +1,190 @@
|
|||
use std;
|
||||
use std::ops::Index;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::slice::Iter;
|
||||
use std::borrow::Cow;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use error::{ResponseError, UriSegmentError, ErrorBadRequest};
|
||||
|
||||
|
||||
/// A trait to abstract the idea of creating a new instance of a type from a path parameter.
|
||||
pub trait FromParam: Sized {
|
||||
/// The associated error which can be returned from parsing.
|
||||
type Err: ResponseError;
|
||||
|
||||
/// Parses a string `s` to return a value of this type.
|
||||
fn from_param(s: &str) -> Result<Self, Self::Err>;
|
||||
}
|
||||
|
||||
/// Route match information
|
||||
///
|
||||
/// If resource path contains variable patterns, `Params` stores this variables.
|
||||
#[derive(Debug)]
|
||||
pub struct Params<'a>(SmallVec<[(Cow<'a, str>, Cow<'a, str>); 3]>);
|
||||
|
||||
impl<'a> Params<'a> {
|
||||
|
||||
pub(crate) fn new() -> Params<'a> {
|
||||
Params(SmallVec::new())
|
||||
}
|
||||
|
||||
pub(crate) fn clear(&mut self) {
|
||||
self.0.clear();
|
||||
}
|
||||
|
||||
pub(crate) fn add<N, V>(&mut self, name: N, value: V)
|
||||
where N: Into<Cow<'a, str>>, V: Into<Cow<'a, str>>,
|
||||
{
|
||||
self.0.push((name.into(), value.into()));
|
||||
}
|
||||
|
||||
/// Check if there are any matched patterns
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// Get matched parameter by name without type conversion
|
||||
pub fn get(&'a self, key: &str) -> Option<&'a str> {
|
||||
for item in self.0.iter() {
|
||||
if key == item.0 {
|
||||
return Some(item.1.as_ref())
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Get matched `FromParam` compatible parameter by name.
|
||||
///
|
||||
/// If keyed parameter is not available empty string is used as default value.
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// # use actix_web::*;
|
||||
/// fn index(req: HttpRequest) -> Result<String> {
|
||||
/// let ivalue: isize = req.match_info().query("val")?;
|
||||
/// Ok(format!("isuze value: {:?}", ivalue))
|
||||
/// }
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
pub fn query<T: FromParam>(&'a self, key: &str) -> Result<T, <T as FromParam>::Err>
|
||||
{
|
||||
if let Some(s) = self.get(key) {
|
||||
T::from_param(s)
|
||||
} else {
|
||||
T::from_param("")
|
||||
}
|
||||
}
|
||||
|
||||
/// Return iterator to items in paramter container
|
||||
pub fn iter(&self) -> Iter<(Cow<'a, str>, Cow<'a, str>)> {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, 'c: 'a> Index<&'b str> for &'c Params<'a> {
|
||||
type Output = str;
|
||||
|
||||
fn index(&self, name: &'b str) -> &str {
|
||||
self.get(name).expect("Value for parameter is not available")
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `PathBuf` from a path parameter. The returned `PathBuf` is
|
||||
/// percent-decoded. If a segment is equal to "..", the previous segment (if
|
||||
/// any) is skipped.
|
||||
///
|
||||
/// For security purposes, if a segment meets any of the following conditions,
|
||||
/// an `Err` is returned indicating the condition met:
|
||||
///
|
||||
/// * Decoded segment starts with any of: `.` (except `..`), `*`
|
||||
/// * Decoded segment ends with any of: `:`, `>`, `<`
|
||||
/// * Decoded segment contains any of: `/`
|
||||
/// * On Windows, decoded segment contains any of: '\'
|
||||
/// * Percent-encoding results in invalid UTF8.
|
||||
///
|
||||
/// As a result of these conditions, a `PathBuf` parsed from request path parameter is
|
||||
/// safe to interpolate within, or use as a suffix of, a path without additional
|
||||
/// checks.
|
||||
impl FromParam for PathBuf {
|
||||
type Err = UriSegmentError;
|
||||
|
||||
fn from_param(val: &str) -> Result<PathBuf, UriSegmentError> {
|
||||
let mut buf = PathBuf::new();
|
||||
for segment in val.split('/') {
|
||||
if segment == ".." {
|
||||
buf.pop();
|
||||
} else if segment.starts_with('.') {
|
||||
return Err(UriSegmentError::BadStart('.'))
|
||||
} else if segment.starts_with('*') {
|
||||
return Err(UriSegmentError::BadStart('*'))
|
||||
} else if segment.ends_with(':') {
|
||||
return Err(UriSegmentError::BadEnd(':'))
|
||||
} else if segment.ends_with('>') {
|
||||
return Err(UriSegmentError::BadEnd('>'))
|
||||
} else if segment.ends_with('<') {
|
||||
return Err(UriSegmentError::BadEnd('<'))
|
||||
} else if segment.is_empty() {
|
||||
continue
|
||||
} else if cfg!(windows) && segment.contains('\\') {
|
||||
return Err(UriSegmentError::BadChar('\\'))
|
||||
} else {
|
||||
buf.push(segment)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! FROM_STR {
|
||||
($type:ty) => {
|
||||
impl FromParam for $type {
|
||||
type Err = ErrorBadRequest<<$type as FromStr>::Err>;
|
||||
|
||||
fn from_param(val: &str) -> Result<Self, Self::Err> {
|
||||
<$type as FromStr>::from_str(val).map_err(ErrorBadRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FROM_STR!(u8);
|
||||
FROM_STR!(u16);
|
||||
FROM_STR!(u32);
|
||||
FROM_STR!(u64);
|
||||
FROM_STR!(usize);
|
||||
FROM_STR!(i8);
|
||||
FROM_STR!(i16);
|
||||
FROM_STR!(i32);
|
||||
FROM_STR!(i64);
|
||||
FROM_STR!(isize);
|
||||
FROM_STR!(f32);
|
||||
FROM_STR!(f64);
|
||||
FROM_STR!(String);
|
||||
FROM_STR!(std::net::IpAddr);
|
||||
FROM_STR!(std::net::Ipv4Addr);
|
||||
FROM_STR!(std::net::Ipv6Addr);
|
||||
FROM_STR!(std::net::SocketAddr);
|
||||
FROM_STR!(std::net::SocketAddrV4);
|
||||
FROM_STR!(std::net::SocketAddrV6);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::iter::FromIterator;
|
||||
|
||||
#[test]
|
||||
fn test_path_buf() {
|
||||
assert_eq!(PathBuf::from_param("/test/.tt"), Err(UriSegmentError::BadStart('.')));
|
||||
assert_eq!(PathBuf::from_param("/test/*tt"), Err(UriSegmentError::BadStart('*')));
|
||||
assert_eq!(PathBuf::from_param("/test/tt:"), Err(UriSegmentError::BadEnd(':')));
|
||||
assert_eq!(PathBuf::from_param("/test/tt<"), Err(UriSegmentError::BadEnd('<')));
|
||||
assert_eq!(PathBuf::from_param("/test/tt>"), Err(UriSegmentError::BadEnd('>')));
|
||||
assert_eq!(PathBuf::from_param("/seg1/seg2/"),
|
||||
Ok(PathBuf::from_iter(vec!["seg1", "seg2"])));
|
||||
assert_eq!(PathBuf::from_param("/seg1/../seg2/"),
|
||||
Ok(PathBuf::from_iter(vec!["seg2"])));
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue