diff --git a/.appveyor.yml b/.appveyor.yml index 69165cf5c..e1fa120d4 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -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) diff --git a/.travis.yml b/.travis.yml index a1b3c431e..4642eb065 100644 --- a/.travis.yml +++ b/.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 "" > 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 diff --git a/CHANGES.md b/CHANGES.md index 836b16b3c..22b422667 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,10 @@ * Content compression/decompression (br, gzip, deflate) +* Server multi-threading + +* Gracefull shutdown support + ## 0.2.1 (2017-11-03) diff --git a/Cargo.toml b/Cargo.toml index bb4b7b932..5f9f27734 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,13 +4,13 @@ version = "0.3.0" authors = ["Nikolay Kim "] 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", +] diff --git a/LICENSE b/LICENSE-APACHE similarity index 100% rename from LICENSE rename to LICENSE-APACHE diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 000000000..410ce45a4 --- /dev/null +++ b/LICENSE-MIT @@ -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. diff --git a/Makefile b/Makefile index fc6071083..fdc3cbbc0 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: default build test doc book clean -CARGO_FLAGS := --features "$(FEATURES)" +CARGO_FLAGS := --features "$(FEATURES) alpn" default: test diff --git a/README.md b/README.md index ff19d5ede..124fb9e83 100644 --- a/README.md +++ b/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; -} - -/// Standard actix's stream handler for a stream of `ws::Message` -impl StreamHandler for MyWebSocket {} -impl Handler for MyWebSocket { - fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) - -> Response - { - // 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) diff --git a/build.rs b/build.rs index dae9e0626..2346ab169 100644 --- a/build.rs +++ b/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); diff --git a/examples/basic.rs b/examples/basic.rs deleted file mode 100644 index fc2a55355..000000000 --- a/examples/basic.rs +++ /dev/null @@ -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 { - 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::("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 -{ - 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 -{ - 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(); -} diff --git a/examples/basics/Cargo.toml b/examples/basics/Cargo.toml new file mode 100644 index 000000000..88b5f61e0 --- /dev/null +++ b/examples/basics/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "basics" +version = "0.1.0" +authors = ["Nikolay Kim "] +workspace = "../.." + +[dependencies] +futures = "*" +env_logger = "0.4" +actix = "0.4" +actix-web = { git = "https://github.com/actix/actix-web" } diff --git a/examples/basics/README.md b/examples/basics/README.md new file mode 100644 index 000000000..154fad9de --- /dev/null +++ b/examples/basics/README.md @@ -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) \ No newline at end of file diff --git a/examples/basics/src/main.rs b/examples/basics/src/main.rs new file mode 100644 index 000000000..68dc17f30 --- /dev/null +++ b/examples/basics/src/main.rs @@ -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 { + Ok(fs::NamedFile::open("../static/favicon.ico")?) +} + +/// simple index handler +fn index(mut req: HttpRequest) -> Result { + 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::("counter")? { + println!("SESSION value: {}", count); + counter = count + 1; + req.session().set("counter", counter)?; + } else { + req.session().set("counter", counter)?; + } + + // html + let html = format!(r#"actix - basics + +

Welcome

+ session counter = {} + +"#, counter); + + // response + Ok(HttpResponse::build(StatusCode::OK) + .content_type("text/html; charset=utf-8") + .body(&html).unwrap()) + +} + +/// 404 handler +fn p404(req: HttpRequest) -> Result { + + // html + let html = format!(r#"actix - basics + + back to home +

404

+ +"#); + + // 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 +{ + 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 +{ + 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(); +} diff --git a/examples/diesel/.env b/examples/diesel/.env new file mode 100644 index 000000000..1fbc5af72 --- /dev/null +++ b/examples/diesel/.env @@ -0,0 +1 @@ +DATABASE_URL=file:test.db diff --git a/examples/diesel/Cargo.toml b/examples/diesel/Cargo.toml new file mode 100644 index 000000000..eb6628375 --- /dev/null +++ b/examples/diesel/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "diesel-example" +version = "0.1.0" +authors = ["Nikolay Kim "] +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" diff --git a/examples/diesel/README.md b/examples/diesel/README.md new file mode 100644 index 000000000..922ba1e3b --- /dev/null +++ b/examples/diesel/README.md @@ -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) \ No newline at end of file diff --git a/examples/diesel/migrations/20170124012402_create_users/down.sql b/examples/diesel/migrations/20170124012402_create_users/down.sql new file mode 100644 index 000000000..9951735c4 --- /dev/null +++ b/examples/diesel/migrations/20170124012402_create_users/down.sql @@ -0,0 +1 @@ +DROP TABLE users diff --git a/examples/diesel/migrations/20170124012402_create_users/up.sql b/examples/diesel/migrations/20170124012402_create_users/up.sql new file mode 100644 index 000000000..d88d44fb7 --- /dev/null +++ b/examples/diesel/migrations/20170124012402_create_users/up.sql @@ -0,0 +1,4 @@ +CREATE TABLE users ( + id VARCHAR NOT NULL PRIMARY KEY, + name VARCHAR NOT NULL +) diff --git a/examples/diesel/src/db.rs b/examples/diesel/src/db.rs new file mode 100644 index 000000000..04daa6ed0 --- /dev/null +++ b/examples/diesel/src/db.rs @@ -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; +} + +impl Handler for DbExecutor { + type Result = MessageResult; + + 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::(&self.0) + .expect("Error loading person"); + + Ok(items.pop().unwrap()) + } +} diff --git a/examples/diesel/src/main.rs b/examples/diesel/src/main.rs new file mode 100644 index 000000000..4c4bc4cda --- /dev/null +++ b/examples/diesel/src/main.rs @@ -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, +} + +/// Async request handler +fn index(req: HttpRequest) -> Box> { + 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(); +} diff --git a/examples/diesel/src/models.rs b/examples/diesel/src/models.rs new file mode 100644 index 000000000..315d59f13 --- /dev/null +++ b/examples/diesel/src/models.rs @@ -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, +} diff --git a/examples/diesel/src/schema.rs b/examples/diesel/src/schema.rs new file mode 100644 index 000000000..51aa40b89 --- /dev/null +++ b/examples/diesel/src/schema.rs @@ -0,0 +1,6 @@ +table! { + users (id) { + id -> Text, + name -> Text, + } +} diff --git a/examples/diesel/test.db b/examples/diesel/test.db new file mode 100644 index 000000000..65e590a6e Binary files /dev/null and b/examples/diesel/test.db differ diff --git a/examples/hello-world/Cargo.toml b/examples/hello-world/Cargo.toml new file mode 100644 index 000000000..4cb1f70f7 --- /dev/null +++ b/examples/hello-world/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "hello-world" +version = "0.1.0" +authors = ["Nikolay Kim "] +workspace = "../.." + +[dependencies] +env_logger = "0.4" +actix = "0.4" +actix-web = { path = "../../" } diff --git a/examples/hello-world/src/main.rs b/examples/hello-world/src/main.rs new file mode 100644 index 000000000..5f1484bd2 --- /dev/null +++ b/examples/hello-world/src/main.rs @@ -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(); +} diff --git a/examples/json/Cargo.toml b/examples/json/Cargo.toml new file mode 100644 index 000000000..681b63450 --- /dev/null +++ b/examples/json/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "json-example" +version = "0.1.0" +authors = ["Nikolay Kim "] +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" } diff --git a/examples/json/README.md b/examples/json/README.md new file mode 100644 index 000000000..167c3909f --- /dev/null +++ b/examples/json/README.md @@ -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`` diff --git a/examples/json/client.py b/examples/json/client.py new file mode 100644 index 000000000..e89ffe096 --- /dev/null +++ b/examples/json/client.py @@ -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()) diff --git a/examples/json/src/main.rs b/examples/json/src/main.rs new file mode 100644 index 000000000..719d74853 --- /dev/null +++ b/examples/json/src/main.rs @@ -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> { + 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> { + // 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::(&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> { + 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(); +} diff --git a/examples/multipart/Cargo.toml b/examples/multipart/Cargo.toml index 5cb2031df..32edbea61 100644 --- a/examples/multipart/Cargo.toml +++ b/examples/multipart/Cargo.toml @@ -2,6 +2,7 @@ name = "multipart-example" version = "0.1.0" authors = ["Nikolay Kim "] +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" } diff --git a/examples/multipart/README.md b/examples/multipart/README.md new file mode 100644 index 000000000..348d28687 --- /dev/null +++ b/examples/multipart/README.md @@ -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`` diff --git a/examples/multipart/client.py b/examples/multipart/client.py index 35f97c1a6..afc07f17d 100644 --- a/examples/multipart/client.py +++ b/examples/multipart/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) diff --git a/examples/multipart/src/main.rs b/examples/multipart/src/main.rs index a1f31527d..7da6145a9 100644 --- a/examples/multipart/src/main.rs +++ b/examples/multipart/src/main.rs @@ -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; -} -impl Route for MyRoute { - type State = (); +fn index(mut req: HttpRequest) -> Box> +{ + println!("{:?}", req); - fn request(mut req: HttpRequest, ctx: &mut HttpContext) -> RouteResult { - 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::::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::(); - }).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(); } diff --git a/examples/state/Cargo.toml b/examples/state/Cargo.toml new file mode 100644 index 000000000..149e8128e --- /dev/null +++ b/examples/state/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "state" +version = "0.1.0" +authors = ["Nikolay Kim "] +workspace = "../.." + +[dependencies] +futures = "*" +env_logger = "0.4" +actix = "0.4" +actix-web = { git = "https://github.com/actix/actix-web" } \ No newline at end of file diff --git a/examples/state/README.md b/examples/state/README.md new file mode 100644 index 000000000..127ed2a0f --- /dev/null +++ b/examples/state/README.md @@ -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/) diff --git a/examples/state.rs b/examples/state/src/main.rs similarity index 80% rename from examples/state.rs rename to examples/state/src/main.rs index 52a2da799..395007aed 100644 --- a/examples/state.rs +++ b/examples/state/src/main.rs @@ -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, } @@ -34,11 +36,10 @@ impl Actor for MyWebSocket { type Context = HttpContext; } -impl StreamHandler for MyWebSocket {} impl Handler for MyWebSocket { - fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) - -> Response - { + 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 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(); diff --git a/examples/static/actixLogo.png b/examples/static/actixLogo.png new file mode 100644 index 000000000..142e4e8d5 Binary files /dev/null and b/examples/static/actixLogo.png differ diff --git a/examples/static/favicon.ico b/examples/static/favicon.ico new file mode 100644 index 000000000..03018db5b Binary files /dev/null and b/examples/static/favicon.ico differ diff --git a/examples/template_tera/Cargo.toml b/examples/template_tera/Cargo.toml new file mode 100644 index 000000000..3862fb803 --- /dev/null +++ b/examples/template_tera/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "template-tera" +version = "0.1.0" +authors = ["Nikolay Kim "] +workspace = "../.." + +[dependencies] +env_logger = "0.4" +actix = "0.4" +actix-web = { git = "https://github.com/actix/actix-web" } +tera = "*" diff --git a/examples/template_tera/README.md b/examples/template_tera/README.md new file mode 100644 index 000000000..35829599f --- /dev/null +++ b/examples/template_tera/README.md @@ -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) diff --git a/examples/template_tera/src/main.rs b/examples/template_tera/src/main.rs new file mode 100644 index 000000000..17a14c70c --- /dev/null +++ b/examples/template_tera/src/main.rs @@ -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) -> Result { + 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(); +} diff --git a/examples/template_tera/templates/index.html b/examples/template_tera/templates/index.html new file mode 100644 index 000000000..d8a47bc09 --- /dev/null +++ b/examples/template_tera/templates/index.html @@ -0,0 +1,17 @@ + + + + + Actix web + + +

Welcome!

+

+

What is your name?

+
+
+

+
+

+ + diff --git a/examples/template_tera/templates/user.html b/examples/template_tera/templates/user.html new file mode 100644 index 000000000..cb5328915 --- /dev/null +++ b/examples/template_tera/templates/user.html @@ -0,0 +1,13 @@ + + + + + Actix web + + +

Hi, {{ name }}!

+

+ {{ text }} +

+ + diff --git a/examples/tls/Cargo.toml b/examples/tls/Cargo.toml index fbb22fb77..e6d39742d 100644 --- a/examples/tls/Cargo.toml +++ b/examples/tls/Cargo.toml @@ -2,6 +2,7 @@ name = "tls-example" version = "0.1.0" authors = ["Nikolay Kim "] +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"] } diff --git a/examples/tls/README.md b/examples/tls/README.md index bd1f2400d..1bc9ba3b7 100644 --- a/examples/tls/README.md +++ b/examples/tls/README.md @@ -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) diff --git a/examples/tls/src/main.rs b/examples/tls/src/main.rs index 81574e5e5..15cfcc666 100644 --- a/examples/tls/src/main.rs +++ b/examples/tls/src/main.rs @@ -8,6 +8,7 @@ use std::io::Read; use actix_web::*; + /// somple handle fn index(req: HttpRequest) -> Result { 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(); } diff --git a/examples/websocket-chat/Cargo.toml b/examples/websocket-chat/Cargo.toml index 2a5c92925..a155e0e11 100644 --- a/examples/websocket-chat/Cargo.toml +++ b/examples/websocket-chat/Cargo.toml @@ -2,6 +2,7 @@ name = "websocket-example" version = "0.1.0" authors = ["Nikolay Kim "] +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" } diff --git a/examples/websocket-chat/README.md b/examples/websocket-chat/README.md index 2e75343b1..28d734dd3 100644 --- a/examples/websocket-chat/README.md +++ b/examples/websocket-chat/README.md @@ -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/) diff --git a/examples/websocket-chat/src/client.rs b/examples/websocket-chat/src/client.rs index c46a4875c..c57825fc9 100644 --- a/examples/websocket-chat/src/client.rs +++ b/examples/websocket-chat/src/client.rs @@ -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; @@ -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) -> 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 for ChatClient -{ - fn handle(&mut self, msg: ClientCommand, ctx: &mut FramedContext) - -> Response - { +impl Handler for ChatClient { + type Result = (); + + fn handle(&mut self, msg: ClientCommand, ctx: &mut FramedContext) { 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 for ChatClient } else { let _ = ctx.send(codec::ChatRequest::Message(m.to_owned())); } - - Self::empty() } } @@ -122,40 +124,26 @@ impl Handler for ChatClient impl FramedActor for ChatClient { type Io = TcpStream; type Codec = codec::ClientChatCodec; -} -impl StreamHandler for ChatClient { - - fn finished(&mut self, _: &mut FramedContext) { - println!("Disconnected"); - - // Stop application on disconnect - Arbiter::system().send(msgs::SystemExit(0)); - } -} - -impl Handler for ChatClient { - - fn handle(&mut self, msg: codec::ChatResponse, _: &mut FramedContext) - -> Response - { + fn handle(&mut self, msg: io::Result, ctx: &mut FramedContext) { 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() } } diff --git a/examples/websocket-chat/src/main.rs b/examples/websocket-chat/src/main.rs index ae123d5ee..aec05ec74 100644 --- a/examples/websocket-chat/src/main.rs +++ b/examples/websocket-chat/src/main.rs @@ -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) -> Result { +fn chat_route(req: HttpRequest) -> Result { ws::start( req, WsChatSession { @@ -52,23 +53,49 @@ struct WsChatSession { impl Actor for WsChatSession { type Context = HttpContext; + + /// 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 for WsChatSession { - fn handle(&mut self, msg: session::Message, ctx: &mut Self::Context) - -> Response - { + 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 for WsChatSession { - fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) - -> Response - { + 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 for WsChatSession { } _ => (), } - Self::empty() } } -impl StreamHandler 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(); } diff --git a/examples/websocket-chat/src/server.rs b/examples/websocket-chat/src/server.rs index c15644a6a..4eae86d00 100644 --- a/examples/websocket-chat/src/server.rs +++ b/examples/websocket-chat/src/server.rs @@ -13,7 +13,7 @@ use session; /// New chat session is created pub struct Connect { - pub addr: Box + Send>, + pub addr: Box + 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 + Send>>, + sessions: HashMap + Send>>, rooms: HashMap>, rng: RefCell, } @@ -118,8 +106,9 @@ impl Actor for ChatServer { /// /// Register new session and assign unique id to this session impl Handler for ChatServer { + type Result = MessageResult; - fn handle(&mut self, msg: Connect, _: &mut Context) -> Response { + fn handle(&mut self, msg: Connect, _: &mut Context) -> Self::Result { println!("Someone joined"); // notify all users in same room @@ -133,14 +122,15 @@ impl Handler 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 for ChatServer { + type Result = (); - fn handle(&mut self, msg: Disconnect, _: &mut Context) -> Response { + fn handle(&mut self, msg: Disconnect, _: &mut Context) { println!("Someone disconnected"); let mut rooms: Vec = Vec::new(); @@ -158,40 +148,39 @@ impl Handler for ChatServer { for room in rooms { self.send_message(&room, "Someone disconnected", 0); } - - Self::empty() } } /// Handler for Message message. impl Handler for ChatServer { + type Result = (); - fn handle(&mut self, msg: Message, _: &mut Context) -> Response { + fn handle(&mut self, msg: Message, _: &mut Context) { self.send_message(&msg.room, msg.msg.as_str(), msg.id); - - Self::empty() } } /// Handler for `ListRooms` message. impl Handler for ChatServer { + type Result = MessageResult; - fn handle(&mut self, _: ListRooms, _: &mut Context) -> Response { + fn handle(&mut self, _: ListRooms, _: &mut Context) -> 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 for ChatServer { + type Result = (); - fn handle(&mut self, msg: Join, _: &mut Context) -> Response { + fn handle(&mut self, msg: Join, _: &mut Context) { let Join {id, name} = msg; let mut rooms = Vec::new(); @@ -211,7 +200,5 @@ impl Handler for ChatServer { } self.send_message(&name, "Someone connected", id); self.rooms.get_mut(&name).unwrap().insert(id); - - Self::empty() } } diff --git a/examples/websocket-chat/src/session.rs b/examples/websocket-chat/src/session.rs index 961955a59..b0725fde4 100644 --- a/examples/websocket-chat/src/session.rs +++ b/examples/websocket-chat/src/session.rs @@ -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; -} -/// 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 `::Item` items. -impl StreamHandler for ChatSession { - - fn started(&mut self, ctx: &mut FramedContext) { + 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) { + 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 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) { - 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) - -> Response - { + fn handle(&mut self, msg: io::Result, ctx: &mut FramedContext) { 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 for ChatSession { + type Result = (); - fn handle(&mut self, msg: Message, ctx: &mut FramedContext) - -> Response - { + fn handle(&mut self, msg: Message, ctx: &mut FramedContext) { // 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; } +#[derive(Message)] struct TcpConnect(TcpStream, net::SocketAddr); -impl ResponseType for TcpConnect { - type Item = (); - type Error = (); -} - /// Handle stream of TcpStream's -impl StreamHandler for TcpServer {} +impl Handler for TcpServer { + type Result = (); -impl Handler for TcpServer { - - fn handle(&mut self, msg: TcpConnect, _: &mut Context) -> Response - { + fn handle(&mut self, msg: TcpConnect, _: &mut Context) { // 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() } } diff --git a/examples/websocket/Cargo.toml b/examples/websocket/Cargo.toml new file mode 100644 index 000000000..e75f5c390 --- /dev/null +++ b/examples/websocket/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "websocket" +version = "0.1.0" +authors = ["Nikolay Kim "] + +[[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" } diff --git a/examples/websocket/README.md b/examples/websocket/README.md new file mode 100644 index 000000000..374e939ac --- /dev/null +++ b/examples/websocket/README.md @@ -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`` diff --git a/examples/websocket.rs b/examples/websocket/src/main.rs similarity index 66% rename from examples/websocket.rs rename to examples/websocket/src/main.rs index 24a88d03b..fea8929de 100644 --- a/examples/websocket.rs +++ b/examples/websocket/src/main.rs @@ -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 { + ws::start(r, MyWebSocket) } /// websocket connection is long running connection, it easier @@ -25,21 +24,11 @@ impl Actor for MyWebSocket { type Context = HttpContext; } -/// Standard actix's stream handler for a stream of `ws::Message` -impl StreamHandler 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 for MyWebSocket { - fn handle(&mut self, msg: ws::Message, ctx: &mut HttpContext) - -> Response - { + type Result = (); + + fn handle(&mut self, msg: ws::Message, ctx: &mut HttpContext) { // process websocket messages println!("WS: {:?}", msg); match msg { @@ -51,7 +40,6 @@ impl Handler 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(); diff --git a/examples/websocket-client.py b/examples/websocket/websocket-client.py similarity index 100% rename from examples/websocket-client.py rename to examples/websocket/websocket-client.py diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 93a265a42..d76840f9c 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -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) diff --git a/guide/src/qs_1.md b/guide/src/qs_1.md index 09e83b586..8d2ee83c2 100644 --- a/guide/src/qs_1.md +++ b/guide/src/qs_1.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. diff --git a/guide/src/qs_10.md b/guide/src/qs_10.md index 0c28628eb..8974f241d 100644 --- a/guide/src/qs_10.md +++ b/guide/src/qs_10.md @@ -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 Middleware 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) -> 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, 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::("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(); +} +``` diff --git a/guide/src/qs_11.md b/guide/src/qs_11.md deleted file mode 100644 index 5aed847ce..000000000 --- a/guide/src/qs_11.md +++ /dev/null @@ -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'] diff --git a/guide/src/qs_12.md b/guide/src/qs_12.md index 8220368dd..286709353 100644 --- a/guide/src/qs_12.md +++ b/guide/src/qs_12.md @@ -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 { + 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. diff --git a/guide/src/qs_13.md b/guide/src/qs_13.md index 5a2472d4d..193b2e109 100644 --- a/guide/src/qs_13.md +++ b/guide/src/qs_13.md @@ -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) diff --git a/guide/src/qs_14.md b/guide/src/qs_14.md new file mode 100644 index 000000000..26fa4ecfb --- /dev/null +++ b/guide/src/qs_14.md @@ -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; +} +``` + +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 for DbExecutor { + + fn handle(&mut self, msg: CreateUser, _: &mut Self::Context) -> Response + { + 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::(&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, +} + +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) -> Box> { + 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). diff --git a/guide/src/qs_2.md b/guide/src/qs_2.md index 4fbcf67fa..3c347ce65 100644 --- a/guide/src/qs_2.md +++ b/guide/src/qs_2.md @@ -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/) diff --git a/guide/src/qs_3.md b/guide/src/qs_3.md index 66606206e..c970b3efe 100644 --- a/guide/src/qs_3.md +++ b/guide/src/qs_3.md @@ -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::::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::::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, +} + +fn index(req: HttpRequest) -> 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`. diff --git a/guide/src/qs_3_5.md b/guide/src/qs_3_5.md new file mode 100644 index 000000000..580f029d9 --- /dev/null +++ b/guide/src/qs_3_5.md @@ -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::::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::::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. diff --git a/guide/src/qs_4.md b/guide/src/qs_4.md index 969d01b8f..42afb9219 100644 --- a/guide/src/qs_4.md +++ b/guide/src/qs_4.md @@ -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>` object. -* Function that accepts `HttpRequest` and return actor that has `HttpContext`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> { } ``` -## 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 Handler for MyHandler { + type Result = HttpResponse; + + /// Handle request + fn handle(&mut self, req: HttpRequest) -> 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); + +impl Handler for MyHandler { + type Result = HttpResponse; + + /// Handle request + fn handle(&mut self, req: HttpRequest) -> 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 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 { + 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> for MyObj { - fn into(self) -> Result { - 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> { - ... +```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 { + + 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`, 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> = 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(); } ``` diff --git a/guide/src/qs_4_5.md b/guide/src/qs_4_5.md new file mode 100644 index 000000000..ef3a982ae --- /dev/null +++ b/guide/src/qs_4_5.md @@ -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> Responder for Result +``` + +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 { + 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. diff --git a/guide/src/qs_5.md b/guide/src/qs_5.md index 157b55c57..1aa7edeb0 100644 --- a/guide/src/qs_5.md +++ b/guide/src/qs_5.md @@ -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) -> 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) -> Future + '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 { + 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 { + 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 { + 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 Predicate for ContentTypeHeader { + + fn check(&self, req: &mut HttpRequest) -> 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(); +} +``` diff --git a/guide/src/qs_6.md b/guide/src/qs_6.md deleted file mode 100644 index 8060e455a..000000000 --- a/guide/src/qs_6.md +++ /dev/null @@ -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, -} - -fn index(req: HttpRequest) -> 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(); -} -``` diff --git a/guide/src/qs_7.md b/guide/src/qs_7.md index 0d92e9fdd..7cce5932b 100644 --- a/guide/src/qs_7.md +++ b/guide/src/qs_7.md @@ -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> { + 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> { + // `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::(&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 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> { + 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> { + 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` 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> { + 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> { + req.payload_mut() + .readany() + .from_err() + .fold((), |_, chunk| { + println!("Chunk: {:?}", chunk); + result::<_, error::PayloadError>(Ok(())) + }) + .map(|_| HttpResponse::Ok().finish().unwrap()) + .responder() +} +# fn main() {} +``` diff --git a/guide/src/qs_8.md b/guide/src/qs_8.md index 465fa3626..53713a205 100644 --- a/guide/src/qs_8.md +++ b/guide/src/qs_8.md @@ -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 +} +``` diff --git a/guide/src/qs_9.md b/guide/src/qs_9.md index 7627a1570..9c45fbd04 100644 --- a/guide/src/qs_9.md +++ b/guide/src/qs_9.md @@ -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; +} + +/// Define Handler for ws::Message message +impl Handler for Ws { + type Result=(); + + fn handle(&mut self, msg: ws::Message, ctx: &mut HttpContext) { + 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/) diff --git a/src/application.rs b/src/application.rs index 4fe5f1dd8..1e4d8273c 100644 --- a/src/application.rs +++ b/src/application.rs @@ -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 { +pub struct HttpApplication { state: Rc, prefix: String, - default: Resource, - handlers: HashMap>>, - router: RouteRecognizer>, - middlewares: Rc>>, + router: Router, + inner: Rc>>, + middlewares: Rc>>>, } -impl Application { +pub(crate) struct Inner { + prefix: usize, + default: Resource, + router: Router, + resources: Vec>, + handlers: Vec<(String, Box>)>, +} - fn run(&self, req: HttpRequest) -> Reply { - let mut req = req.with_state(Rc::clone(&self.state)); +impl PipelineHandler for Inner { - 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) -> 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 HttpHandler for Application { +#[cfg(test)] +impl HttpApplication { + pub(crate) fn run(&mut self, req: HttpRequest) -> Reply { + self.inner.borrow_mut().handle(req) + } + pub(crate) fn prepare_request(&self, req: HttpRequest) -> HttpRequest { + req.with_state(Rc::clone(&self.state), self.router.clone()) + } +} - fn handle(&self, req: HttpRequest) -> Result { - if req.path().starts_with(&self.prefix) { - Ok(Pipeline::new(req, Rc::clone(&self.middlewares), - &|req: HttpRequest| self.run(req))) +impl HttpHandler for HttpApplication { + + fn handle(&mut self, req: HttpRequest) -> Result, 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 { + state: S, + prefix: String, + settings: ServerSettings, + default: Resource, + resources: HashMap>>, + handlers: Vec<(String, Box>)>, + external: HashMap, + middlewares: Vec>>, +} + +/// Structure that follows the builder pattern for building `Application` structs. +pub struct Application { + parts: Option>, +} + impl Application<()> { - /// Create default `ApplicationBuilder` with no state - pub fn default>(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 Application 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>(prefix: T, state: S) -> ApplicationBuilder { - 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 { + 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 { - state: S, - prefix: String, - default: Resource, - handlers: HashMap>>, - resources: HashMap>, - middlewares: Vec>, -} - -/// Structure that follows the builder pattern for building `Application` structs. -pub struct ApplicationBuilder { - parts: Option>, -} - -impl ApplicationBuilder 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>(mut self, prefix: P) -> Application { + { + 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 ApplicationBuilder 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 ApplicationBuilder 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>(&mut self, path: P, f: F) -> &mut Self + pub fn resource(mut self, path: &str, f: F) -> Application where F: FnOnce(&mut Resource) + '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(&mut self, f: F) -> &mut Self + /// Default resource is used if no matched route could be found. + pub fn default_resource(mut self, f: F) -> Application where F: FnOnce(&mut Resource) + 'static { { @@ -168,105 +252,142 @@ impl ApplicationBuilder 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 { + /// 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(&mut self, path: P, handler: F) -> &mut Self - where P: Into, - F: Fn(HttpRequest) -> R + 'static, - R: Into + 'static + pub fn external_resource(mut self, name: T, url: U) -> Application + where T: AsRef, U: AsRef { - 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(&mut self, path: P, handler: H) -> &mut Self - where P: Into, H: Handler + /// 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>(mut self, path: &str, handler: H) -> Application { - 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(&mut self, path: P, handler: F) -> &mut Self - where F: Fn(HttpRequest) -> R + 'static, - R: Future + 'static, - P: Into, - { - self.parts.as_mut().expect("Use after finish") - .handlers.insert(path.into(), Box::new(AsyncHandler::new(handler))); - self - } - - /// Construct application - pub fn middleware(&mut self, mw: T) -> &mut Self - where T: Middleware + 'static + /// Register a middleware + pub fn middleware(mut self, mw: T) -> Application + where T: Middleware + 'static { self.parts.as_mut().expect("Use after finish") .middlewares.push(Box::new(mw)); self } - /// Construct application - pub fn finish(&mut self) -> Application { + /// Finish application configuration and create HttpHandler object + pub fn finish(&mut self) -> HttpApplication { 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 From> for Application { - fn from(mut builder: ApplicationBuilder) -> Application { - builder.finish() +impl IntoHttpHandler for Application { + type Handler = HttpApplication; + + fn into_handler(mut self, settings: ServerSettings) -> HttpApplication { + { + let parts = self.parts.as_mut().expect("Use after finish"); + parts.settings = settings; + } + self.finish() } } -impl Iterator for ApplicationBuilder { - type Item = Application; +impl<'a, S: 'static> IntoHttpHandler for &'a mut Application { + type Handler = HttpApplication; + + fn into_handler(self, settings: ServerSettings) -> HttpApplication { + { + let parts = self.parts.as_mut().expect("Use after finish"); + parts.settings = settings; + } + self.finish() + } +} + +#[doc(hidden)] +impl Iterator for Application { + type Item = HttpApplication; fn next(&mut self) -> Option { if self.parts.is_some() { @@ -276,3 +397,138 @@ impl Iterator for ApplicationBuilder { } } } + + +#[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); + + } + +} diff --git a/src/body.rs b/src/body.rs index 73bd8920c..53af6e40e 100644 --- a/src/body.rs +++ b/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>; - +/// Type represent streaming body +pub type BodyStream = Box>; /// 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), } /// 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 From for Body where T: Into{ } } +impl From> for Body { + fn from(ctx: Box) -> 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(), diff --git a/src/channel.rs b/src/channel.rs index a15591c9f..ef8afbd16 100644 --- a/src/channel.rs +++ b/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; + fn handle(&mut self, req: HttpRequest) -> Result, HttpRequest>; } -enum HttpProtocol - where T: AsyncRead + AsyncWrite + 'static, H: 'static +pub trait HttpHandlerTask { + + fn poll_io(&mut self, io: &mut Writer) -> Poll; + + 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 IntoHttpHandler for T { + type Handler = T; + + fn into_handler(self, _: ServerSettings) -> Self::Handler { + self + } +} + +enum HttpProtocol { H1(h1::Http1), H2(h2::Http2), @@ -26,61 +56,104 @@ enum HttpProtocol #[doc(hidden)] pub struct HttpChannel - where T: AsyncRead + AsyncWrite + 'static, H: 'static + where T: IoStream, H: HttpHandler + 'static { proto: Option>, + node: Option>>, } impl HttpChannel - where T: AsyncRead + AsyncWrite + 'static, H: HttpHandler + 'static + where T: IoStream, H: HttpHandler + 'static { - pub fn new(stream: T, addr: Option, router: Rc>, http2: bool) - -> HttpChannel { + pub(crate) fn new(h: Rc>, + io: T, peer: Option, http2: bool) -> HttpChannel + { + 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 Drop for HttpChannel { +/*impl Drop for HttpChannel + where T: AsyncRead + AsyncWrite + 'static, H: HttpHandler + 'static +{ fn drop(&mut self) { println!("Drop http channel"); } }*/ -impl Actor for HttpChannel - where T: AsyncRead + AsyncWrite + 'static, H: HttpHandler + 'static -{ - type Context = Context; -} - impl Future for HttpChannel - where T: AsyncRead + AsyncWrite + 'static, H: HttpHandler + 'static + where T: IoStream, H: HttpHandler + 'static { type Item = (); type Error = (); fn poll(&mut self) -> Poll { + 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 Future for HttpChannel 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 +{ + next: Option<*mut Node<()>>, + prev: Option<*mut Node<()>>, + element: *mut T, +} + +impl Node +{ + fn new(el: &mut T) -> Self { + Node { + next: None, + prev: None, + element: el as *mut _, + } + } + + fn insert(&self, next: &Node) { + #[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 = mem::transmute(self); + slf.next = Some(next as *const _ as *mut _); + + let next: &mut Node = 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 = 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(&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 = 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) -> 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) -> io::Result<()> { + TcpStream::set_linger(self, dur) + } +} + + +/// Wrapper for `AsyncRead + AsyncWrite` types +pub(crate) struct WrapperStream where T: AsyncRead + AsyncWrite + 'static { + io: T, +} + +impl WrapperStream where T: AsyncRead + AsyncWrite + 'static +{ + pub fn new(io: T) -> Self { + WrapperStream{io: io} + } +} + +impl IoStream for WrapperStream + 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) -> io::Result<()> { + Ok(()) + } +} + +impl io::Read for WrapperStream + where T: AsyncRead + AsyncWrite + 'static +{ + #[inline] + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.io.read(buf) + } +} + +impl io::Write for WrapperStream + where T: AsyncRead + AsyncWrite + 'static +{ + #[inline] + fn write(&mut self, buf: &[u8]) -> io::Result { + self.io.write(buf) + } + #[inline] + fn flush(&mut self) -> io::Result<()> { + self.io.flush() + } +} + +impl AsyncRead for WrapperStream + where T: AsyncRead + AsyncWrite + 'static +{ + fn read_buf(&mut self, buf: &mut B) -> Poll { + self.io.read_buf(buf) + } +} + +impl AsyncWrite for WrapperStream + where T: AsyncRead + AsyncWrite + 'static +{ + fn shutdown(&mut self) -> Poll<(), io::Error> { + self.io.shutdown() + } + fn write_buf(&mut self, buf: &mut B) -> Poll { + self.io.write_buf(buf) + } +} + + +#[cfg(feature="alpn")] +use tokio_openssl::SslStream; + +#[cfg(feature="alpn")] +impl IoStream for SslStream { + #[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) -> io::Result<()> { + self.get_mut().get_mut().set_linger(dur) + } +} + +#[cfg(feature="tls")] +use tokio_tls::TlsStream; + +#[cfg(feature="tls")] +impl IoStream for TlsStream { + #[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) -> io::Result<()> { + self.get_mut().get_mut().set_linger(dur) + } +} diff --git a/src/context.rs b/src/context.rs index c9f770147..30d03e8ae 100644 --- a/src/context.rs +++ b/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, Error>; } #[derive(Debug)] -pub(crate) enum Frame { - Message(HttpResponse), +pub enum Frame { Payload(Option), - Drain(Rc>), + Drain(oneshot::Sender<()>), } /// Http actor execution context pub struct HttpContext where A: Actor>, { - act: A, - state: ActorState, - modified: bool, - items: ActorItemsCell, - address: ActorAddressCell, + inner: ContextImpl, stream: VecDeque, - wait: ActorWaitCell, request: HttpRequest, - streaming: bool, disconnected: bool, } @@ -50,23 +41,17 @@ impl ActorContext for HttpContext where A: Actor /// 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 AsyncContext for HttpContext where A: Actor fn spawn(&mut self, fut: F) -> SpawnHandle where F: ActorFuture + 'static { - self.modified = true; - self.items.spawn(fut) + self.inner.spawn(fut) } fn wait(&mut self, fut: F) where F: ActorFuture + '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 AsyncContextApi for HttpContext where A: Actor { fn address_cell(&mut self) -> &mut ActorAddressCell { - &mut self.address + self.inner.address_cell() } } -impl HttpContext where A: Actor { +impl HttpContext where A: Actor { - pub fn new(req: HttpRequest, actor: A) -> HttpContext - { + pub fn new(req: HttpRequest, actor: A) -> HttpContext { + HttpContext::from_request(req).actor(actor) + } + + pub fn from_request(req: HttpRequest) -> HttpContext { 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 { + self.inner.set_actor(actor); + self + } } impl HttpContext where A: Actor { @@ -134,24 +114,12 @@ impl HttpContext where A: Actor { &mut self.request } - /// Send response to peer - pub fn start>(&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>(&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 HttpContext where A: Actor { /// Returns drain future pub fn drain(&mut self) -> Drain { - 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 HttpContext where A: Actor { #[doc(hidden)] pub fn subscriber(&mut self) -> Box> - where A: Handler, - M: ResponseType + 'static, + where A: Handler, M: ResponseType + 'static { - Box::new(self.address.unsync_address()) + self.inner.subscriber() } #[doc(hidden)] pub fn sync_subscriber(&mut self) -> Box + Send> where A: Handler, - 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 IoContext for HttpContext where A: Actor, S: 'static { +impl ActorHttpContext for HttpContext where A: Actor, 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, Error> { - let act: &mut A = unsafe { - std::mem::transmute(&mut self.act as &mut A) - }; let ctx: &mut HttpContext = unsafe { std::mem::transmute(self as &mut HttpContext) }; - // 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 IoContext for HttpContext where A: Actor, S: 'sta impl ToEnvelope for HttpContext where A: Actor>, { - fn pack(msg: M, tx: Option>>) -> Envelope + fn pack(msg: M, tx: Option>>, + channel_on_drop: bool) -> Envelope where A: Handler, 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: PhantomData, - inner: Rc> +impl From> for Body + where A: Actor>, + S: 'static +{ + fn from(ctx: HttpContext) -> Body { + Body::Actor(Box::new(ctx)) + } } -impl ActorFuture for Drain - where A: Actor -{ +pub struct Drain { + fut: oneshot::Receiver<()>, + _a: PhantomData, +} + +impl Drain { + fn new(fut: oneshot::Receiver<()>) -> Self { + Drain { + fut: fut, + _a: PhantomData + } + } +} + +impl ActorFuture for Drain { type Item = (); type Error = (); type Actor = A; - fn poll(&mut self, _: &mut A, _: &mut ::Context) -> Poll<(), ()> { - self.inner.borrow_mut().poll() + fn poll(&mut self, + _: &mut A, + _: &mut ::Context) -> Poll + { + self.fut.poll().map_err(|_| ()) } } diff --git a/src/date.rs b/src/date.rs deleted file mode 100644 index 27ae1db22..000000000 --- a/src/date.rs +++ /dev/null @@ -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 = 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); -} diff --git a/src/dev.rs b/src/dev.rs deleted file mode 100644 index cb409ccc0..000000000 --- a/src/dev.rs +++ /dev/null @@ -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; diff --git a/src/encoding.rs b/src/encoding.rs index 4774b4c7a..2475c006c 100644 --- a/src/encoding.rs +++ b/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>), - Gzip(Option>), - Br(BrotliDecoder>), + Deflate(Box>>), + Gzip(Box>>), + Br(Box>>), 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::() { - 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 { 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 { - 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")); + } +} diff --git a/src/error.rs b/src/error.rs index c6c4a7eb9..29a94d4c0 100644 --- a/src/error.rs +++ b/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 = result::Result; +pub type Result = result::Result; /// General purpose actix web error -#[derive(Debug)] +#[derive(Fail, Debug)] pub struct Error { - cause: Box, + cause: Box, } 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 for HttpResponse { } } -/// `Error` for any error that implements `ErrorResponse` -impl From for Error { +/// `Error` for any error that implements `ResponseError` +impl From for Error { fn from(err: T) -> Error { Error { cause: Box::new(err) } } @@ -78,31 +80,39 @@ impl From for Error { /// Default error is `InternalServerError` #[cfg(actix_nightly)] -default impl ErrorResponse for T { +default impl 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 for ParseError { impl From 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 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 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 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 for JsonPayloadError { + fn from(err: PayloadError) -> JsonPayloadError { + JsonPayloadError::Payload(err) + } +} + +impl From 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 for UrlGenerationError { + fn from(err: UrlParseError) -> Self { + UrlGenerationError::ParseError(err) + } +} + +macro_rules! ERROR_WRAP { + ($type:ty, $status:expr) => { + unsafe impl Sync for $type {} + unsafe impl Send for $type {} + + impl $type { + pub fn cause(&self) -> &T { + &self.0 + } + } + + impl Fail for $type {} + impl fmt::Display for $type { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self.0) + } + } + + impl 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 { +/// let f = NamedFile::open("test.txt").map_err(error::ErrorBadRequest)?; +/// Ok(f) +/// } +/// # fn main() {} +/// ``` +#[derive(Debug)] +pub struct ErrorBadRequest(pub T); +ERROR_WRAP!(ErrorBadRequest, StatusCode::BAD_REQUEST); + +#[derive(Debug)] +/// Helper type that can wrap any error and generate *UNAUTHORIZED* response. +pub struct ErrorUnauthorized(pub T); +ERROR_WRAP!(ErrorUnauthorized, StatusCode::UNAUTHORIZED); + +#[derive(Debug)] +/// Helper type that can wrap any error and generate *FORBIDDEN* response. +pub struct ErrorForbidden(pub T); +ERROR_WRAP!(ErrorForbidden, StatusCode::FORBIDDEN); + +#[derive(Debug)] +/// Helper type that can wrap any error and generate *NOT FOUND* response. +pub struct ErrorNotFound(pub T); +ERROR_WRAP!(ErrorNotFound, StatusCode::NOT_FOUND); + +#[derive(Debug)] +/// Helper type that can wrap any error and generate *METHOD NOT ALLOWED* response. +pub struct ErrorMethodNotAllowed(pub T); +ERROR_WRAP!(ErrorMethodNotAllowed, StatusCode::METHOD_NOT_ALLOWED); + +#[derive(Debug)] +/// Helper type that can wrap any error and generate *REQUEST TIMEOUT* response. +pub struct ErrorRequestTimeout(pub T); +ERROR_WRAP!(ErrorRequestTimeout, StatusCode::REQUEST_TIMEOUT); + +#[derive(Debug)] +/// Helper type that can wrap any error and generate *CONFLICT* response. +pub struct ErrorConflict(pub T); +ERROR_WRAP!(ErrorConflict, StatusCode::CONFLICT); + +#[derive(Debug)] +/// Helper type that can wrap any error and generate *GONE* response. +pub struct ErrorGone(pub T); +ERROR_WRAP!(ErrorGone, StatusCode::GONE); + +#[derive(Debug)] +/// Helper type that can wrap any error and generate *PRECONDITION FAILED* response. +pub struct ErrorPreconditionFailed(pub T); +ERROR_WRAP!(ErrorPreconditionFailed, StatusCode::PRECONDITION_FAILED); + +#[derive(Debug)] +/// Helper type that can wrap any error and generate *EXPECTATION FAILED* response. +pub struct ErrorExpectationFailed(pub T); +ERROR_WRAP!(ErrorExpectationFailed, StatusCode::EXPECTATION_FAILED); + +#[derive(Debug)] +/// Helper type that can wrap any error and generate *INTERNAL SERVER ERROR* response. +pub struct ErrorInternalServerError(pub T); +ERROR_WRAP!(ErrorInternalServerError, StatusCode::INTERNAL_SERVER_ERROR); + #[cfg(test)] mod tests { use std::error::Error as StdError; diff --git a/src/fs.rs b/src/fs.rs new file mode 100644 index 000000000..467e90192 --- /dev/null +++ b/src/fs.rs @@ -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>(path: P) -> io::Result { + 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 { + 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) -> 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 { + 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, "
  • {}/
  • ", + file_url, entry.file_name().to_string_lossy()); + } else { + let _ = write!(body, "
  • {}
  • ", + file_url, entry.file_name().to_string_lossy()); + } + } else { + continue + } + } + } + + let html = format!("\ + {}\ +

    {}

    \ +
      \ + {}\ +
    \n", 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 { + 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>(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 Handler for StaticFiles { + type Result = Result; + + fn handle(&mut self, req: HttpRequest) -> 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")); + } +} diff --git a/src/h1.rs b/src/h1.rs index ff358f154..c0a1c68db 100644 --- a/src/h1.rs +++ b/src/h1.rs @@ -1,4 +1,4 @@ -use std::{self, io, ptr}; +use std::{self, io}; use std::rc::Rc; use std::net::SocketAddr; use std::time::Duration; @@ -10,25 +10,41 @@ use http::{Uri, Method, Version, HttpTryFrom, HeaderMap}; use http::header::{self, HeaderName, HeaderValue}; use bytes::{Bytes, BytesMut, BufMut}; use futures::{Future, Poll, Async}; -use tokio_io::{AsyncRead, AsyncWrite}; use tokio_core::reactor::Timeout; use pipeline::Pipeline; use encoding::PayloadType; -use channel::HttpHandler; -use h1writer::H1Writer; +use channel::{HttpHandler, HttpHandlerTask, IoStream}; +use h1writer::{Writer, H1Writer}; +use worker::WorkerSettings; use httpcodes::HTTPNotFound; use httprequest::HttpRequest; -use error::{ParseError, PayloadError, ErrorResponse}; +use error::{ParseError, PayloadError, ResponseError}; use payload::{Payload, PayloadWriter, DEFAULT_BUFFER_SIZE}; -const KEEPALIVE_PERIOD: u64 = 15; // seconds -const INIT_BUFFER_SIZE: usize = 8192; +const LW_BUFFER_SIZE: usize = 4096; +const HW_BUFFER_SIZE: usize = 16_384; const MAX_BUFFER_SIZE: usize = 131_072; const MAX_HEADERS: usize = 100; const MAX_PIPELINED_MESSAGES: usize = 16; const HTTP2_PREFACE: [u8; 14] = *b"PRI * HTTP/2.0"; +bitflags! { + struct Flags: u8 { + const ERROR = 0b0000_0010; + const KEEPALIVE = 0b0000_0100; + const H2 = 0b0000_1000; + } +} + +bitflags! { + struct EntryFlags: u8 { + const EOF = 0b0000_0001; + const ERROR = 0b0000_0010; + const FINISHED = 0b0000_0100; + } +} + pub(crate) enum Http1Result { Done, Switch, @@ -40,51 +56,67 @@ enum Item { Http2, } -pub(crate) struct Http1 { - router: Rc>, +pub(crate) struct Http1 { + flags: Flags, + settings: Rc>, addr: Option, stream: H1Writer, reader: Reader, read_buf: BytesMut, - error: bool, tasks: VecDeque, - keepalive: bool, keepalive_timer: Option, - h2: bool, } struct Entry { - pipe: Pipeline, - eof: bool, - error: bool, - finished: bool, + pipe: Box, + flags: EntryFlags, } impl Http1 - where T: AsyncRead + AsyncWrite + 'static, - H: HttpHandler + 'static + where T: IoStream, H: HttpHandler + 'static { - pub fn new(stream: T, addr: Option, router: Rc>) -> Self { - Http1{ router: router, + pub fn new(h: Rc>, stream: T, addr: Option) -> Self { + let bytes = h.get_shared_bytes(); + Http1{ flags: Flags::KEEPALIVE, + settings: h, addr: addr, - stream: H1Writer::new(stream), + stream: H1Writer::new(stream, bytes), reader: Reader::new(), read_buf: BytesMut::new(), - error: false, tasks: VecDeque::new(), - keepalive: true, - keepalive_timer: None, - h2: false } + keepalive_timer: None } } - pub fn into_inner(mut self) -> (T, Option, Rc>, Bytes) { - (self.stream.unwrap(), self.addr, self.router, self.read_buf.freeze()) + pub fn settings(&self) -> &WorkerSettings { + self.settings.as_ref() } + pub fn into_inner(self) -> (Rc>, T, Option, Bytes) { + (self.settings, self.stream.into_inner(), self.addr, self.read_buf.freeze()) + } + + pub(crate) fn io(&mut self) -> &mut T { + self.stream.get_mut() + } + + fn poll_completed(&mut self, shutdown: bool) -> Result { + // check stream state + match self.stream.poll_completed(shutdown) { + Ok(Async::Ready(_)) => Ok(true), + Ok(Async::NotReady) => Ok(false), + Err(err) => { + debug!("Error sending data: {}", err); + Err(()) + } + } + } + + // TODO: refacrtor + #[cfg_attr(feature = "cargo-clippy", allow(cyclomatic_complexity))] pub fn poll(&mut self) -> Poll { // keep-alive timer - if let Some(ref mut timeout) = self.keepalive_timer { - match timeout.poll() { + if self.keepalive_timer.is_some() { + match self.keepalive_timer.as_mut().unwrap().poll() { Ok(Async::Ready(_)) => { trace!("Keep-alive timeout, close connection"); return Ok(Async::Ready(Http1Result::Done)) @@ -103,8 +135,12 @@ impl Http1 while idx < self.tasks.len() { let item = &mut self.tasks[idx]; - if !io && !item.eof { - if item.error { + if !io && !item.flags.contains(EntryFlags::EOF) { + if item.flags.contains(EntryFlags::ERROR) { + // check stream state + if let Ok(Async::NotReady) = self.stream.poll_completed(true) { + return Ok(Async::NotReady) + } return Err(()) } @@ -113,14 +149,16 @@ impl Http1 not_ready = false; // overide keep-alive state - if self.keepalive { - self.keepalive = self.stream.keepalive(); + if self.stream.keepalive() { + self.flags.insert(Flags::KEEPALIVE); + } else { + self.flags.remove(Flags::KEEPALIVE); } - self.stream = H1Writer::new(self.stream.unwrap()); + self.stream.reset(); - item.eof = true; + item.flags.insert(EntryFlags::EOF); if ready { - item.finished = true; + item.flags.insert(EntryFlags::FINISHED); } }, Ok(Async::NotReady) => { @@ -131,18 +169,24 @@ impl Http1 // it is not possible to recover from error // during pipe handling, so just drop connection error!("Unhandled error: {}", err); + item.flags.insert(EntryFlags::ERROR); + + // check stream state, we still can have valid data in buffer + if let Ok(Async::NotReady) = self.stream.poll_completed(true) { + return Ok(Async::NotReady) + } return Err(()) } } - } else if !item.finished { + } else if !item.flags.contains(EntryFlags::FINISHED) { match item.pipe.poll() { Ok(Async::NotReady) => (), Ok(Async::Ready(_)) => { not_ready = false; - item.finished = true; + item.flags.insert(EntryFlags::FINISHED); }, Err(err) => { - item.error = true; + item.flags.insert(EntryFlags::ERROR); error!("Unhandled error: {}", err); } } @@ -152,7 +196,9 @@ impl Http1 // cleanup finished tasks while !self.tasks.is_empty() { - if self.tasks[0].eof && self.tasks[0].finished { + if self.tasks[0].flags.contains(EntryFlags::EOF) && + self.tasks[0].flags.contains(EntryFlags::FINISHED) + { self.tasks.pop_front(); } else { break @@ -160,8 +206,15 @@ impl Http1 } // no keep-alive - if !self.keepalive && self.tasks.is_empty() { - if self.h2 { + if !self.flags.contains(Flags::KEEPALIVE) && self.tasks.is_empty() { + let h2 = self.flags.contains(Flags::H2); + + // check stream state + if !self.poll_completed(!h2)? { + return Ok(Async::NotReady) + } + + if h2 { return Ok(Async::Ready(Http1Result::Switch)) } else { return Ok(Async::Ready(Http1Result::Done)) @@ -169,20 +222,23 @@ impl Http1 } // read incoming data - while !self.error && !self.h2 && self.tasks.len() < MAX_PIPELINED_MESSAGES { - match self.reader.parse(self.stream.get_mut(), &mut self.read_buf) { + while !self.flags.contains(Flags::ERROR) && !self.flags.contains(Flags::H2) && + self.tasks.len() < MAX_PIPELINED_MESSAGES + { + match self.reader.parse(self.stream.get_mut(), + &mut self.read_buf, &self.settings) { Ok(Async::Ready(Item::Http1(mut req))) => { not_ready = false; // set remote addr - req.set_remove_addr(self.addr); + req.set_peer_addr(self.addr); // stop keepalive timer self.keepalive_timer.take(); // start request processing let mut pipe = None; - for h in self.router.iter() { + for h in self.settings.handlers().iter_mut() { req = match h.handle(req) { Ok(t) => { pipe = Some(t); @@ -194,16 +250,14 @@ impl Http1 self.tasks.push_back( Entry {pipe: pipe.unwrap_or_else(|| Pipeline::error(HTTPNotFound)), - eof: false, - error: false, - finished: false}); + flags: EntryFlags::empty()}); } Ok(Async::Ready(Item::Http2)) => { - self.h2 = true; + self.flags.insert(Flags::H2); } Err(ReaderError::Disconnect) => { not_ready = false; - self.error = true; + self.flags.insert(Flags::ERROR); self.stream.disconnected(); for entry in &mut self.tasks { entry.pipe.disconnected() @@ -218,37 +272,50 @@ impl Http1 } // kill keepalive - self.keepalive = false; + self.flags.remove(Flags::KEEPALIVE); self.keepalive_timer.take(); // on parse error, stop reading stream but tasks need to be completed - self.error = true; + self.flags.insert(Flags::ERROR); if self.tasks.is_empty() { if let ReaderError::Error(err) = err { self.tasks.push_back( Entry {pipe: Pipeline::error(err.error_response()), - eof: false, - error: false, - finished: false}); + flags: EntryFlags::empty()}); } } } Ok(Async::NotReady) => { - // start keep-alive timer, this is also slow request timeout + // start keep-alive timer, this also is slow request timeout if self.tasks.is_empty() { - if self.keepalive { - if 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.settings.keep_alive_enabled() { + let keep_alive = self.settings.keep_alive(); + if keep_alive > 0 && self.flags.contains(Flags::KEEPALIVE) { + if self.keepalive_timer.is_none() { + trace!("Start keep-alive timer"); + let mut to = Timeout::new( + Duration::new(keep_alive, 0), + Arbiter::handle()).unwrap(); + // register timeout + let _ = to.poll(); + self.keepalive_timer = Some(to); + } + } else { + // check stream state + if !self.poll_completed(true)? { + return Ok(Async::NotReady) + } + // keep-alive disable, drop connection + return Ok(Async::Ready(Http1Result::Done)) } + } else if !self.poll_completed(false)? || + self.flags.contains(Flags::KEEPALIVE) + { + // check stream state or + // if keep-alive unset, rely on operating system + return Ok(Async::NotReady) } else { - // keep-alive disable, drop connection return Ok(Async::Ready(Http1Result::Done)) } } @@ -259,15 +326,22 @@ impl Http1 // check for parse error if self.tasks.is_empty() { - if self.h2 { + let h2 = self.flags.contains(Flags::H2); + + // check stream state + if !self.poll_completed(!h2)? { + return Ok(Async::NotReady) + } + if h2 { return Ok(Async::Ready(Http1Result::Switch)) } - if self.error || self.keepalive_timer.is_none() { + if self.flags.contains(Flags::ERROR) || self.keepalive_timer.is_none() { return Ok(Async::Ready(Http1Result::Done)) } } if not_ready { + self.poll_completed(false)?; return Ok(Async::NotReady) } } @@ -338,80 +412,63 @@ impl Reader { } } - pub fn parse(&mut self, io: &mut T, buf: &mut BytesMut) -> Poll - where T: AsyncRead + pub fn parse(&mut self, io: &mut T, + buf: &mut BytesMut, + settings: &WorkerSettings) -> Poll + where T: IoStream { - loop { - match self.decode(buf)? { - Decoding::Paused => return Ok(Async::NotReady), - Decoding::Ready => { - self.payload = None; - break - }, - Decoding::NotReady => { - match self.read_from_io(io, buf) { - Ok(Async::Ready(0)) => { - if let Some(ref mut payload) = self.payload { - payload.tx.set_error(PayloadError::Incomplete); - } - // http channel should not deal with payload errors - return Err(ReaderError::Payload) - } - Ok(Async::Ready(_)) => { - continue - } - Ok(Async::NotReady) => break, - Err(err) => { - if let Some(ref mut payload) = self.payload { - payload.tx.set_error(err.into()); - } - // http channel should not deal with payload errors - return Err(ReaderError::Payload) - } + // read payload + if self.payload.is_some() { + match self.read_from_io(io, buf) { + Ok(Async::Ready(0)) => { + if let Some(ref mut payload) = self.payload { + payload.tx.set_error(PayloadError::Incomplete); } + // http channel should not deal with payload errors + return Err(ReaderError::Payload) + }, + Err(err) => { + if let Some(ref mut payload) = self.payload { + payload.tx.set_error(err.into()); + } + // http channel should not deal with payload errors + return Err(ReaderError::Payload) } + _ => (), + } + match self.decode(buf)? { + Decoding::Ready => self.payload = None, + Decoding::Paused | Decoding::NotReady => return Ok(Async::NotReady), } } + // if buf is empty parse_message will always return NotReady, let's avoid that + let read = if buf.is_empty() { + match self.read_from_io(io, buf) { + Ok(Async::Ready(0)) => { + // debug!("Ignored premature client disconnection"); + return Err(ReaderError::Disconnect); + }, + Ok(Async::Ready(_)) => (), + Ok(Async::NotReady) => + return Ok(Async::NotReady), + Err(err) => + return Err(ReaderError::Error(err.into())) + } + false + } else { + true + }; + loop { - match Reader::parse_message(buf).map_err(ReaderError::Error)? { + match Reader::parse_message(buf, settings).map_err(ReaderError::Error)? { Message::Http1(msg, decoder) => { + // process payload if let Some(payload) = decoder { self.payload = Some(payload); - - loop { - match self.decode(buf)? { - Decoding::Paused => - break, - Decoding::Ready => { - self.payload = None; - break - }, - Decoding::NotReady => { - match self.read_from_io(io, buf) { - Ok(Async::Ready(0)) => { - trace!("parse eof"); - if let Some(ref mut payload) = self.payload { - payload.tx.set_error( - PayloadError::Incomplete); - } - // http channel should deal with payload errors - return Err(ReaderError::Payload) - } - Ok(Async::Ready(_)) => { - continue - } - Ok(Async::NotReady) => break, - Err(err) => { - if let Some(ref mut payload) = self.payload { - payload.tx.set_error(err.into()); - } - // http channel should deal with payload errors - return Err(ReaderError::Payload) - } - } - } - } + match self.decode(buf)? { + Decoding::Paused | Decoding::NotReady => (), + Decoding::Ready => self.payload = None, } } self.h1 = true; @@ -425,52 +482,54 @@ impl Reader { }, Message::NotReady => { if buf.capacity() >= MAX_BUFFER_SIZE { - debug!("MAX_BUFFER_SIZE reached, closing"); + error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); return Err(ReaderError::Error(ParseError::TooLarge)); } + if read { + match self.read_from_io(io, buf) { + Ok(Async::Ready(0)) => { + debug!("Ignored premature client disconnection"); + return Err(ReaderError::Disconnect); + }, + Ok(Async::Ready(_)) => (), + Ok(Async::NotReady) => + return Ok(Async::NotReady), + Err(err) => + return Err(ReaderError::Error(err.into())) + } + } else { + return Ok(Async::NotReady) + } }, } - match self.read_from_io(io, buf) { - Ok(Async::Ready(0)) => { - debug!("Ignored premature client disconnection"); - return Err(ReaderError::Disconnect); - }, - Ok(Async::Ready(_)) => (), - Ok(Async::NotReady) => - return Ok(Async::NotReady), - Err(err) => - return Err(ReaderError::Error(err.into())) - } } } - fn read_from_io(&mut self, io: &mut T, buf: &mut BytesMut) - -> Poll + fn read_from_io(&mut self, io: &mut T, buf: &mut BytesMut) + -> Poll { - if buf.remaining_mut() < INIT_BUFFER_SIZE { - buf.reserve(INIT_BUFFER_SIZE); - unsafe { // Zero out unused memory - let b = buf.bytes_mut(); - let len = b.len(); - ptr::write_bytes(b.as_mut_ptr(), 0, len); - } - } unsafe { - let n = match io.read(buf.bytes_mut()) { - Ok(n) => n, + if buf.remaining_mut() < LW_BUFFER_SIZE { + buf.reserve(HW_BUFFER_SIZE); + } + match io.read(buf.bytes_mut()) { + Ok(n) => { + buf.advance_mut(n); + Ok(Async::Ready(n)) + }, Err(e) => { if e.kind() == io::ErrorKind::WouldBlock { - return Ok(Async::NotReady); + Ok(Async::NotReady) + } else { + Err(e) } - return Err(e) } - }; - buf.advance_mut(n); - Ok(Async::Ready(n)) + } } } - fn parse_message(buf: &mut BytesMut) -> Result + fn parse_message(buf: &mut BytesMut, settings: &WorkerSettings) + -> Result { if buf.is_empty() { return Ok(Message::NotReady); @@ -480,121 +539,107 @@ impl Reader { } // Parse http message - let mut headers_indices = [HeaderIndices { - name: (0, 0), - value: (0, 0) - }; MAX_HEADERS]; + let msg = { + let bytes_ptr = buf.as_ref().as_ptr() as usize; + let mut headers: [httparse::Header; MAX_HEADERS] = + unsafe{std::mem::uninitialized()}; - let (len, method, path, version, headers_len) = { - let mut headers = [httparse::EMPTY_HEADER; MAX_HEADERS]; - let mut req = httparse::Request::new(&mut headers); - match try!(req.parse(buf)) { - httparse::Status::Complete(len) => { - let method = Method::try_from(req.method.unwrap()) - .map_err(|_| ParseError::Method)?; - let path = req.path.unwrap(); - let bytes_ptr = buf.as_ref().as_ptr() as usize; - let path_start = path.as_ptr() as usize - bytes_ptr; - let path_end = path_start + path.len(); - let path = (path_start, path_end); + let (len, method, path, version, headers_len) = { + let b = unsafe{ let b: &[u8] = buf; std::mem::transmute(b) }; + let mut req = httparse::Request::new(&mut headers); + match req.parse(b)? { + httparse::Status::Complete(len) => { + let method = Method::try_from(req.method.unwrap()) + .map_err(|_| ParseError::Method)?; + let path = req.path.unwrap(); + let path_start = path.as_ptr() as usize - bytes_ptr; + let path_end = path_start + path.len(); + let path = (path_start, path_end); - let version = if req.version.unwrap() == 1 { - Version::HTTP_11 - } else { - Version::HTTP_10 - }; - - record_header_indices(buf.as_ref(), req.headers, &mut headers_indices); - let headers_len = req.headers.len(); - (len, method, path, version, headers_len) + let version = if req.version.unwrap() == 1 { + Version::HTTP_11 + } else { + Version::HTTP_10 + }; + (len, method, path, version, req.headers.len()) + } + httparse::Status::Partial => return Ok(Message::NotReady), } - httparse::Status::Partial => return Ok(Message::NotReady), - } - }; + }; - let slice = buf.split_to(len).freeze(); - let path = slice.slice(path.0, path.1); - // path was found to be utf8 by httparse - let uri = Uri::from_shared(path).map_err(ParseError::Uri)?; + let slice = buf.split_to(len).freeze(); - // convert headers - let mut headers = HeaderMap::with_capacity(headers_len); - for header in headers_indices[..headers_len].iter() { - if let Ok(name) = HeaderName::try_from(slice.slice(header.name.0, header.name.1)) { - if let Ok(value) = HeaderValue::try_from( - slice.slice(header.value.0, header.value.1)) - { - headers.append(name, value); + // convert headers + let msg = settings.get_http_message(); + for header in headers[..headers_len].iter() { + if let Ok(name) = HeaderName::try_from(header.name) { + let v_start = header.value.as_ptr() as usize - bytes_ptr; + let v_end = v_start + header.value.len(); + let value = unsafe { + HeaderValue::from_shared_unchecked(slice.slice(v_start, v_end)) }; + msg.get_mut().headers.append(name, value); } else { return Err(ParseError::Header) } - } else { - return Err(ParseError::Header) } - } - let (mut psender, payload) = Payload::new(false); - let msg = HttpRequest::new(method, uri, version, headers, payload); + let path = slice.slice(path.0, path.1); + let uri = Uri::from_shared(path).map_err(ParseError::Uri)?; - let decoder = if msg.upgrade() { - Decoder::eof() - } else { - let has_len = msg.headers().contains_key(header::CONTENT_LENGTH); + msg.get_mut().uri = uri; + msg.get_mut().method = method; + msg.get_mut().version = version; + msg + }; - // Chunked encoding - if msg.chunked()? { - if has_len { - return Err(ParseError::Header) - } - Decoder::chunked() - } else { - if !has_len { - psender.feed_eof(); - return Ok(Message::Http1(msg, None)) - } - - // Content-Length - let len = msg.headers().get(header::CONTENT_LENGTH).unwrap(); - if let Ok(s) = len.to_str() { - if let Ok(len) = s.parse::() { - Decoder::length(len) - } else { - debug!("illegal Content-Length: {:?}", len); - return Err(ParseError::Header) - } + let decoder = if let Some(len) = msg.get_ref().headers.get(header::CONTENT_LENGTH) { + // Content-Length + if let Ok(s) = len.to_str() { + if let Ok(len) = s.parse::() { + Some(Decoder::length(len)) } else { debug!("illegal Content-Length: {:?}", len); return Err(ParseError::Header) } + } else { + debug!("illegal Content-Length: {:?}", len); + return Err(ParseError::Header) } + } else if chunked(&msg.get_mut().headers)? { + // Chunked encoding + Some(Decoder::chunked()) + } else if msg.get_ref().headers.contains_key(header::UPGRADE) || + msg.get_ref().method == Method::CONNECT + { + Some(Decoder::eof()) + } else { + None }; - let payload = PayloadInfo { - tx: PayloadType::new(msg.headers(), psender), - decoder: decoder, - }; - Ok(Message::Http1(msg, Some(payload))) + if let Some(decoder) = decoder { + let (psender, payload) = Payload::new(false); + let info = PayloadInfo { + tx: PayloadType::new(&msg.get_mut().headers, psender), + decoder: decoder, + }; + msg.get_mut().payload = Some(payload); + Ok(Message::Http1(HttpRequest::from_message(msg), Some(info))) + } else { + Ok(Message::Http1(HttpRequest::from_message(msg), None)) + } } } -#[derive(Clone, Copy)] -struct HeaderIndices { - name: (usize, usize), - value: (usize, usize), -} - -fn record_header_indices(bytes: &[u8], - headers: &[httparse::Header], - indices: &mut [HeaderIndices]) -{ - let bytes_ptr = bytes.as_ptr() as usize; - for (header, indices) in headers.iter().zip(indices.iter_mut()) { - let name_start = header.name.as_ptr() as usize - bytes_ptr; - let name_end = name_start + header.name.len(); - indices.name = (name_start, name_end); - let value_start = header.value.as_ptr() as usize - bytes_ptr; - let value_end = value_start + header.value.len(); - indices.value = (value_start, value_end); +/// Check if request has chunked transfer encoding +fn chunked(headers: &HeaderMap) -> Result { + if let Some(encodings) = headers.get(header::TRANSFER_ENCODING) { + if let Ok(s) = encodings.to_str() { + Ok(s.to_lowercase().contains("chunked")) + } else { + Err(ParseError::Header) + } + } else { + Ok(false) } } @@ -658,23 +703,10 @@ enum ChunkedState { End, } -impl Decoder { - /*pub fn is_eof(&self) -> bool { - trace!("is_eof? {:?}", self); - match self.kind { - Kind::Length(0) | - Kind::Chunked(ChunkedState::End, _) | - Kind::Eof(true) => true, - _ => false, - } - }*/ -} - impl Decoder { pub fn decode(&mut self, body: &mut BytesMut) -> Poll, io::Error> { match self.kind { Kind::Length(ref mut remaining) => { - trace!("Sized read, remaining={:?}", remaining); if *remaining == 0 { Ok(Async::Ready(None)) } else { @@ -755,7 +787,6 @@ impl ChunkedState { } } fn read_size(rdr: &mut BytesMut, size: &mut u64) -> Poll { - trace!("Read chunk hex size"); let radix = 16; match byte!(rdr) { b @ b'0'...b'9' => { @@ -794,14 +825,12 @@ impl ChunkedState { } } fn read_extension(rdr: &mut BytesMut) -> Poll { - trace!("read_extension"); match byte!(rdr) { b'\r' => Ok(Async::Ready(ChunkedState::SizeLf)), _ => Ok(Async::Ready(ChunkedState::Extension)), // no supported extensions } } fn read_size_lf(rdr: &mut BytesMut, size: &mut u64) -> Poll { - trace!("Chunk size is {:?}", size); match byte!(rdr) { b'\n' if *size > 0 => Ok(Async::Ready(ChunkedState::Body)), b'\n' if *size == 0 => Ok(Async::Ready(ChunkedState::EndCr)), @@ -863,12 +892,17 @@ impl ChunkedState { #[cfg(test)] mod tests { - use std::{io, cmp}; - use bytes::{Bytes, BytesMut}; - use futures::{Async}; - use tokio_io::AsyncRead; + use std::{io, cmp, time}; + use std::net::Shutdown; + use bytes::{Bytes, BytesMut, Buf}; + use futures::Async; + use tokio_io::{AsyncRead, AsyncWrite}; use http::{Version, Method}; + use super::*; + use application::HttpApplication; + use worker::WorkerSettings; + use channel::IoStream; struct Buffer { buf: Bytes, @@ -907,6 +941,28 @@ mod tests { } } + impl IoStream for Buffer { + fn shutdown(&mut self, _: Shutdown) -> io::Result<()> { + Ok(()) + } + fn set_nodelay(&mut self, _: bool) -> io::Result<()> { + Ok(()) + } + fn set_linger(&mut self, _: Option) -> io::Result<()> { + Ok(()) + } + } + impl io::Write for Buffer { + fn write(&mut self, buf: &[u8]) -> io::Result {Ok(buf.len())} + fn flush(&mut self) -> io::Result<()> {Ok(())} + } + impl AsyncWrite for Buffer { + fn shutdown(&mut self) -> Poll<(), io::Error> { Ok(Async::Ready(())) } + fn write_buf(&mut self, _: &mut B) -> Poll { + Ok(Async::NotReady) + } + } + macro_rules! not_ready { ($e:expr) => (match $e { Ok(Async::NotReady) => (), @@ -916,13 +972,14 @@ mod tests { } macro_rules! parse_ready { - ($e:expr) => ( - match Reader::new().parse($e, &mut BytesMut::new()) { + ($e:expr) => ({ + let settings = WorkerSettings::::new(Vec::new(), None); + match Reader::new().parse($e, &mut BytesMut::new(), &settings) { Ok(Async::Ready(Item::Http1(req))) => req, Ok(_) => panic!("Eof during parsing http request"), Err(err) => panic!("Error during parsing http request: {:?}", err), } - ) + }) } macro_rules! reader_parse_ready { @@ -938,7 +995,9 @@ mod tests { macro_rules! expect_parse_err { ($e:expr) => ({ let mut buf = BytesMut::new(); - match Reader::new().parse($e, &mut buf) { + let settings = WorkerSettings::::new(Vec::new(), None); + + match Reader::new().parse($e, &mut buf, &settings) { Err(err) => match err { ReaderError::Error(_) => (), _ => panic!("Parse error expected"), @@ -954,14 +1013,14 @@ mod tests { fn test_parse() { let mut buf = Buffer::new("GET /test HTTP/1.1\r\n\r\n"); let mut readbuf = BytesMut::new(); + let settings = WorkerSettings::::new(Vec::new(), None); let mut reader = Reader::new(); - match reader.parse(&mut buf, &mut readbuf) { + match reader.parse(&mut buf, &mut readbuf, &settings) { Ok(Async::Ready(Item::Http1(req))) => { assert_eq!(req.version(), Version::HTTP_11); assert_eq!(*req.method(), Method::GET); assert_eq!(req.path(), "/test"); - assert!(req.payload().eof()); } Ok(_) | Err(_) => panic!("Error during parsing http request"), } @@ -971,20 +1030,20 @@ mod tests { fn test_parse_partial() { let mut buf = Buffer::new("PUT /test HTTP/1"); let mut readbuf = BytesMut::new(); + let settings = WorkerSettings::::new(Vec::new(), None); let mut reader = Reader::new(); - match reader.parse(&mut buf, &mut readbuf) { + match reader.parse(&mut buf, &mut readbuf, &settings) { Ok(Async::NotReady) => (), _ => panic!("Error"), } buf.feed_data(".1\r\n\r\n"); - match reader.parse(&mut buf, &mut readbuf) { + match reader.parse(&mut buf, &mut readbuf, &settings) { Ok(Async::Ready(Item::Http1(req))) => { assert_eq!(req.version(), Version::HTTP_11); assert_eq!(*req.method(), Method::PUT); assert_eq!(req.path(), "/test"); - assert!(req.payload().eof()); } Ok(_) | Err(_) => panic!("Error during parsing http request"), } @@ -994,14 +1053,14 @@ mod tests { fn test_parse_post() { let mut buf = Buffer::new("POST /test2 HTTP/1.0\r\n\r\n"); let mut readbuf = BytesMut::new(); + let settings = WorkerSettings::::new(Vec::new(), None); let mut reader = Reader::new(); - match reader.parse(&mut buf, &mut readbuf) { + match reader.parse(&mut buf, &mut readbuf, &settings) { Ok(Async::Ready(Item::Http1(req))) => { assert_eq!(req.version(), Version::HTTP_10); assert_eq!(*req.method(), Method::POST); assert_eq!(req.path(), "/test2"); - assert!(req.payload().eof()); } Ok(_) | Err(_) => panic!("Error during parsing http request"), } @@ -1011,9 +1070,10 @@ mod tests { fn test_parse_body() { let mut buf = Buffer::new("GET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody"); let mut readbuf = BytesMut::new(); + let settings = WorkerSettings::::new(Vec::new(), None); let mut reader = Reader::new(); - match reader.parse(&mut buf, &mut readbuf) { + match reader.parse(&mut buf, &mut readbuf, &settings) { Ok(Async::Ready(Item::Http1(mut req))) => { assert_eq!(req.version(), Version::HTTP_11); assert_eq!(*req.method(), Method::GET); @@ -1029,9 +1089,10 @@ mod tests { let mut buf = Buffer::new( "\r\nGET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody"); let mut readbuf = BytesMut::new(); + let settings = WorkerSettings::::new(Vec::new(), None); let mut reader = Reader::new(); - match reader.parse(&mut buf, &mut readbuf) { + match reader.parse(&mut buf, &mut readbuf, &settings) { Ok(Async::Ready(Item::Http1(mut req))) => { assert_eq!(req.version(), Version::HTTP_11); assert_eq!(*req.method(), Method::GET); @@ -1046,17 +1107,17 @@ mod tests { fn test_parse_partial_eof() { let mut buf = Buffer::new("GET /test HTTP/1.1\r\n"); let mut readbuf = BytesMut::new(); + let settings = WorkerSettings::::new(Vec::new(), None); let mut reader = Reader::new(); - not_ready!{ reader.parse(&mut buf, &mut readbuf) } + not_ready!{ reader.parse(&mut buf, &mut readbuf, &settings) } buf.feed_data("\r\n"); - match reader.parse(&mut buf, &mut readbuf) { + match reader.parse(&mut buf, &mut readbuf, &settings) { Ok(Async::Ready(Item::Http1(req))) => { assert_eq!(req.version(), Version::HTTP_11); assert_eq!(*req.method(), Method::GET); assert_eq!(req.path(), "/test"); - assert!(req.payload().eof()); } Ok(_) | Err(_) => panic!("Error during parsing http request"), } @@ -1066,24 +1127,24 @@ mod tests { fn test_headers_split_field() { let mut buf = Buffer::new("GET /test HTTP/1.1\r\n"); let mut readbuf = BytesMut::new(); + let settings = WorkerSettings::::new(Vec::new(), None); let mut reader = Reader::new(); - not_ready!{ reader.parse(&mut buf, &mut readbuf) } + not_ready!{ reader.parse(&mut buf, &mut readbuf, &settings) } buf.feed_data("t"); - not_ready!{ reader.parse(&mut buf, &mut readbuf) } + not_ready!{ reader.parse(&mut buf, &mut readbuf, &settings) } buf.feed_data("es"); - not_ready!{ reader.parse(&mut buf, &mut readbuf) } + not_ready!{ reader.parse(&mut buf, &mut readbuf, &settings) } buf.feed_data("t: value\r\n\r\n"); - match reader.parse(&mut buf, &mut readbuf) { + match reader.parse(&mut buf, &mut readbuf, &settings) { Ok(Async::Ready(Item::Http1(req))) => { assert_eq!(req.version(), Version::HTTP_11); assert_eq!(*req.method(), Method::GET); assert_eq!(req.path(), "/test"); assert_eq!(req.headers().get("test").unwrap().as_bytes(), b"value"); - assert!(req.payload().eof()); } Ok(_) | Err(_) => panic!("Error during parsing http request"), } @@ -1096,9 +1157,10 @@ mod tests { Set-Cookie: c1=cookie1\r\n\ Set-Cookie: c2=cookie2\r\n\r\n"); let mut readbuf = BytesMut::new(); + let settings = WorkerSettings::::new(Vec::new(), None); let mut reader = Reader::new(); - match reader.parse(&mut buf, &mut readbuf) { + match reader.parse(&mut buf, &mut readbuf, &settings) { Ok(Async::Ready(Item::Http1(req))) => { let val: Vec<_> = req.headers().get_all("Set-Cookie") .iter().map(|v| v.to_str().unwrap().to_owned()).collect(); @@ -1189,6 +1251,7 @@ mod tests { fn test_conn_upgrade() { let mut buf = Buffer::new( "GET /test HTTP/1.1\r\n\ + upgrade: websockets\r\n\ connection: upgrade\r\n\r\n"); let req = parse_ready!(&mut buf); @@ -1200,7 +1263,7 @@ mod tests { fn test_conn_upgrade_connect_method() { let mut buf = Buffer::new( "CONNECT /test HTTP/1.1\r\n\ - content-length: 0\r\n\r\n"); + content-type: text/plain\r\n\r\n"); let req = parse_ready!(&mut buf); assert!(req.upgrade()); @@ -1214,7 +1277,6 @@ mod tests { transfer-encoding: chunked\r\n\r\n"); let req = parse_ready!(&mut buf); - assert!(!req.payload().eof()); if let Ok(val) = req.chunked() { assert!(val); } else { @@ -1226,7 +1288,6 @@ mod tests { transfer-encoding: chnked\r\n\r\n"); let req = parse_ready!(&mut buf); - assert!(req.payload().eof()); if let Ok(val) = req.chunked() { assert!(!val); } else { @@ -1330,14 +1391,15 @@ mod tests { "GET /test HTTP/1.1\r\n\ transfer-encoding: chunked\r\n\r\n"); let mut readbuf = BytesMut::new(); + let settings = WorkerSettings::::new(Vec::new(), None); let mut reader = Reader::new(); - let mut req = reader_parse_ready!(reader.parse(&mut buf, &mut readbuf)); + let mut req = reader_parse_ready!(reader.parse(&mut buf, &mut readbuf, &settings)); assert!(req.chunked().unwrap()); assert!(!req.payload().eof()); buf.feed_data("4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); - not_ready!(reader.parse(&mut buf, &mut readbuf)); + not_ready!(reader.parse(&mut buf, &mut readbuf, &settings)); assert!(!req.payload().eof()); assert_eq!(req.payload_mut().readall().unwrap().as_ref(), b"dataline"); assert!(req.payload().eof()); @@ -1349,10 +1411,11 @@ mod tests { "GET /test HTTP/1.1\r\n\ transfer-encoding: chunked\r\n\r\n"); let mut readbuf = BytesMut::new(); + let settings = WorkerSettings::::new(Vec::new(), None); let mut reader = Reader::new(); - let mut req = reader_parse_ready!(reader.parse(&mut buf, &mut readbuf)); + let mut req = reader_parse_ready!(reader.parse(&mut buf, &mut readbuf, &settings)); assert!(req.chunked().unwrap()); assert!(!req.payload().eof()); @@ -1361,7 +1424,7 @@ mod tests { POST /test2 HTTP/1.1\r\n\ transfer-encoding: chunked\r\n\r\n"); - let req2 = reader_parse_ready!(reader.parse(&mut buf, &mut readbuf)); + let req2 = reader_parse_ready!(reader.parse(&mut buf, &mut readbuf, &settings)); assert_eq!(*req2.method(), Method::POST); assert!(req2.chunked().unwrap()); assert!(!req2.payload().eof()); @@ -1376,28 +1439,29 @@ mod tests { "GET /test HTTP/1.1\r\n\ transfer-encoding: chunked\r\n\r\n"); let mut readbuf = BytesMut::new(); + let settings = WorkerSettings::::new(Vec::new(), None); let mut reader = Reader::new(); - let mut req = reader_parse_ready!(reader.parse(&mut buf, &mut readbuf)); + let mut req = reader_parse_ready!(reader.parse(&mut buf, &mut readbuf, &settings)); assert!(req.chunked().unwrap()); assert!(!req.payload().eof()); buf.feed_data("4\r\ndata\r"); - not_ready!(reader.parse(&mut buf, &mut readbuf)); + not_ready!(reader.parse(&mut buf, &mut readbuf, &settings)); buf.feed_data("\n4"); - not_ready!(reader.parse(&mut buf, &mut readbuf)); + not_ready!(reader.parse(&mut buf, &mut readbuf, &settings)); buf.feed_data("\r"); - not_ready!(reader.parse(&mut buf, &mut readbuf)); + not_ready!(reader.parse(&mut buf, &mut readbuf, &settings)); buf.feed_data("\n"); - not_ready!(reader.parse(&mut buf, &mut readbuf)); + not_ready!(reader.parse(&mut buf, &mut readbuf, &settings)); buf.feed_data("li"); - not_ready!(reader.parse(&mut buf, &mut readbuf)); + not_ready!(reader.parse(&mut buf, &mut readbuf, &settings)); buf.feed_data("ne\r\n0\r\n"); - not_ready!(reader.parse(&mut buf, &mut readbuf)); + not_ready!(reader.parse(&mut buf, &mut readbuf, &settings)); //buf.feed_data("test: test\r\n"); //not_ready!(reader.parse(&mut buf, &mut readbuf)); @@ -1406,7 +1470,7 @@ mod tests { assert!(!req.payload().eof()); buf.feed_data("\r\n"); - not_ready!(reader.parse(&mut buf, &mut readbuf)); + not_ready!(reader.parse(&mut buf, &mut readbuf, &settings)); assert!(req.payload().eof()); } @@ -1416,14 +1480,15 @@ mod tests { "GET /test HTTP/1.1\r\n\ transfer-encoding: chunked\r\n\r\n"); let mut readbuf = BytesMut::new(); + let settings = WorkerSettings::::new(Vec::new(), None); let mut reader = Reader::new(); - let mut req = reader_parse_ready!(reader.parse(&mut buf, &mut readbuf)); + let mut req = reader_parse_ready!(reader.parse(&mut buf, &mut readbuf, &settings)); assert!(req.chunked().unwrap()); assert!(!req.payload().eof()); buf.feed_data("4;test\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); // test: test\r\n\r\n") - not_ready!(reader.parse(&mut buf, &mut readbuf)); + not_ready!(reader.parse(&mut buf, &mut readbuf, &settings)); assert!(!req.payload().eof()); assert_eq!(req.payload_mut().readall().unwrap().as_ref(), b"dataline"); assert!(req.payload().eof()); @@ -1450,9 +1515,10 @@ mod tests { fn test_http2_prefix() { let mut buf = Buffer::new("PRI * HTTP/2.0\r\n\r\n"); let mut readbuf = BytesMut::new(); + let settings = WorkerSettings::::new(Vec::new(), None); let mut reader = Reader::new(); - match reader.parse(&mut buf, &mut readbuf) { + match reader.parse(&mut buf, &mut readbuf, &settings) { Ok(Async::Ready(Item::Http2)) => (), Ok(_) | Err(_) => panic!("Error during parsing http request"), } diff --git a/src/h1writer.rs b/src/h1writer.rs index 63c98e876..200ff0529 100644 --- a/src/h1writer.rs +++ b/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; fn write(&mut self, payload: &[u8]) -> Result; fn write_eof(&mut self) -> Result; - 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 { - stream: Option, - 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 H1Writer { - pub fn new(stream: T) -> H1Writer { + pub fn new(stream: T, buf: SharedBytes) -> H1Writer { 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 H1Writer { } 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 { 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 H1Writer { impl Writer for H1Writer { 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 { - 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 { - 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 Writer for H1Writer { 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 Writer for H1Writer { } } - 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) } diff --git a/src/h2.rs b/src/h2.rs index 929fb9924..446219727 100644 --- a/src/h2.rs +++ b/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 where T: AsyncRead + AsyncWrite + 'static, H: 'static { - router: Rc>, + flags: Flags, + settings: Rc>, addr: Option, state: State>, - disconnected: bool, tasks: VecDeque, keepalive_timer: Option, } enum State { Handshake(Handshake), - Server(Server), + Server(Connection), Empty, } @@ -46,17 +52,28 @@ impl Http2 where T: AsyncRead + AsyncWrite + 'static, H: HttpHandler + 'static { - pub fn new(stream: T, addr: Option, router: Rc>, buf: Bytes) -> Self { - Http2{ router: router, + pub fn new(h: Rc>, io: T, addr: Option, 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 { + 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 Http2 // 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 Http2 // 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 Http2 } // 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 Http2 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 Http2 } 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 Http2 } } +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, payload: PayloadType, recv: RecvStream, stream: H2Writer, - eof: bool, - error: bool, - finished: bool, - reof: bool, capacity: usize, + flags: EntryFlags, } impl Entry { fn new(parts: Parts, recv: RecvStream, - resp: Respond, + resp: SendResponse, addr: Option, - router: &Rc>) -> Entry + settings: &Rc>) -> 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) => { diff --git a/src/h2writer.rs b/src/h2writer.rs index e3e04bd77..57c4bd357 100644 --- a/src/h2writer.rs +++ b/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, + respond: SendResponse, stream: Option>, - started: bool, encoder: PayloadEncoder, - disconnected: bool, - eof: bool, + flags: Flags, written: u64, + buffer: SharedBytes, } impl H2Writer { - pub fn new(respond: Respond) -> H2Writer { + pub fn new(respond: SendResponse, 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 { - 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 { - 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 { - 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 { 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), diff --git a/src/handler.rs b/src/handler.rs new file mode 100644 index 000000000..8f6861e5f --- /dev/null +++ b/src/handler.rs @@ -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: 'static { + + /// The type of value that handler will return. + type Result: Responder; + + /// Handle request + fn handle(&mut self, req: HttpRequest) -> 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; + + /// The associated error which can be returned. + type Error: Into; + + /// Convert itself to `Reply` or `Error`. + fn respond_to(self, req: HttpRequest) -> Result; +} + +#[doc(hidden)] +/// Convinience trait that convert `Future` object into `Boxed` future +pub trait AsyncResponder: Sized { + fn responder(self) -> Box>; +} + +impl AsyncResponder for F + where F: Future + 'static, + I: Responder + 'static, + E: Into + 'static, +{ + fn responder(self) -> Box> { + Box::new(self) + } +} + +/// Handler for Fn() +impl Handler for F + where F: Fn(HttpRequest) -> R + 'static, + R: Responder + 'static +{ + type Result = R; + + fn handle(&mut self, req: HttpRequest) -> R { + (self)(req) + } +} + +/// Represents response process. +pub struct Reply(ReplyItem); + +pub(crate) enum ReplyItem { + Message(HttpResponse), + Future(Box>), +} + +impl Reply { + + /// Create async response + #[inline] + pub fn async(fut: F) -> Reply + where F: Future + 'static + { + Reply(ReplyItem::Future(Box::new(fut))) + } + + /// Send response + #[inline] + pub fn response>(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 { + Ok(self) + } +} + +impl Responder for HttpResponse { + type Item = Reply; + type Error = Error; + + #[inline] + fn respond_to(self, _: HttpRequest) -> Result { + Ok(Reply(ReplyItem::Message(self))) + } +} + +impl From for Reply { + + #[inline] + fn from(resp: HttpResponse) -> Reply { + Reply(ReplyItem::Message(resp)) + } +} + +impl> Responder for Result +{ + type Item = ::Item; + type Error = Error; + + fn respond_to(self, req: HttpRequest) -> Result { + match self { + Ok(val) => match val.respond_to(req) { + Ok(val) => Ok(val), + Err(err) => Err(err.into()), + }, + Err(err) => Err(err.into()), + } + } +} + +impl> From> for Reply { + #[inline] + fn from(res: Result) -> Self { + match res { + Ok(val) => val, + Err(err) => Reply(ReplyItem::Message(err.into().into())), + } + } +} + +impl> From> for Reply { + #[inline] + fn from(res: Result) -> Self { + match res { + Ok(val) => Reply(ReplyItem::Message(val)), + Err(err) => Reply(ReplyItem::Message(err.into().into())), + } + } +} + +impl From>> for Reply { + #[inline] + fn from(fut: Box>) -> Reply { + Reply(ReplyItem::Future(fut)) + } +} + +impl Responder for Box> + where I: Responder + 'static, + E: Into + 'static +{ + type Item = Reply; + type Error = Error; + + #[inline] + fn respond_to(self, req: HttpRequest) -> Result { + 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: 'static { + fn handle(&mut self, req: HttpRequest) -> Reply; +} + +/// Route handler wrapper for Handler +pub(crate) +struct WrapHandler + where H: Handler, + R: Responder, + S: 'static, +{ + h: H, + s: PhantomData, +} + +impl WrapHandler + where H: Handler, + R: Responder, + S: 'static, +{ + pub fn new(h: H) -> Self { + WrapHandler{h: h, s: PhantomData} + } +} + +impl RouteHandler for WrapHandler + where H: Handler, + R: Responder + 'static, + S: 'static, +{ + fn handle(&mut self, req: HttpRequest) -> 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 + where H: Fn(HttpRequest) -> F + 'static, + F: Future + 'static, + R: Responder + 'static, + E: Into + 'static, + S: 'static, +{ + h: Box, + s: PhantomData, +} + +impl AsyncHandler + where H: Fn(HttpRequest) -> F + 'static, + F: Future + 'static, + R: Responder + 'static, + E: Into + 'static, + S: 'static, +{ + pub fn new(h: H) -> Self { + AsyncHandler{h: Box::new(h), s: PhantomData} + } +} + +impl RouteHandler for AsyncHandler + where H: Fn(HttpRequest) -> F + 'static, + F: Future + 'static, + R: Responder + 'static, + E: Into + 'static, + S: 'static, +{ + fn handle(&mut self, req: HttpRequest) -> 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 Handler for NormalizePath { + type Result = Result; + + fn handle(&mut self, req: HttpRequest) -> 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()); + } + } + } + + +} diff --git a/src/helpers.rs b/src/helpers.rs new file mode 100644 index 000000000..1b4bd0e11 --- /dev/null +++ b/src/helpers.rs @@ -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 = 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>>); + +impl SharedBytesPool { + pub fn new() -> SharedBytesPool { + SharedBytesPool(RefCell::new(VecDeque::with_capacity(128))) + } + + pub fn get_bytes(&self) -> Rc { + if let Some(bytes) = self.0.borrow_mut().pop_front() { + bytes + } else { + Rc::new(BytesMut::new()) + } + } + + pub fn release_bytes(&self, mut bytes: Rc) { + 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>, Option>); + +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, pool: Rc) -> 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>>); + +impl SharedMessagePool { + pub fn new() -> SharedMessagePool { + SharedMessagePool(RefCell::new(VecDeque::with_capacity(128))) + } + + pub fn get(&self) -> Rc { + if let Some(msg) = self.0.borrow_mut().pop_front() { + msg + } else { + Rc::new(HttpMessage::default()) + } + } + + pub fn release(&self, mut msg: Rc) { + 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>, Option>); + +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, pool: Rc) -> 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"[..]); + } +} diff --git a/src/httpcodes.rs b/src/httpcodes.rs index 44415f5f3..c39167dd7 100644 --- a/src/httpcodes.rs +++ b/src/httpcodes.rs @@ -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 Handler for StaticResponse { + type Result = HttpResponse; + + fn handle(&mut self, _: HttpRequest) -> HttpResponse { + HttpResponse::new(self.0, Body::Empty) + } +} + impl RouteHandler for StaticResponse { - fn handle(&self, _: HttpRequest) -> Reply { + fn handle(&mut self, _: HttpRequest) -> 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 { + self.build().body(Body::Empty) + } +} + impl From for HttpResponse { fn from(st: StaticResponse) -> Self { - st.response() + HttpResponse::new(st.0, Body::Empty) + } +} + +impl From 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); diff --git a/src/httprequest.rs b/src/httprequest.rs index 393330153..2498a9298 100644 --- a/src/httprequest.rs +++ b/src/httprequest.rs @@ -3,30 +3,38 @@ use std::{str, fmt, mem}; use std::rc::Rc; use std::net::SocketAddr; use std::collections::HashMap; -use bytes::BytesMut; +use bytes::{Bytes, BytesMut}; +use cookie::Cookie; use futures::{Async, Future, Stream, Poll}; -use url::form_urlencoded; +use http_range::HttpRange; +use serde::de::DeserializeOwned; +use url::{Url, form_urlencoded}; use http::{header, Uri, Method, Version, HeaderMap, Extensions}; -use {Cookie, HttpRange}; -use recognizer::Params; -use payload::Payload; +use info::ConnectionInfo; +use param::Params; +use router::Router; +use payload::{Payload, ReadAny}; +use json::JsonBody; use multipart::Multipart; -use error::{ParseError, PayloadError, - MultipartError, CookieParseError, HttpRangeError, UrlencodedError}; +use helpers::SharedHttpMessage; +use error::{ParseError, UrlGenerationError, + CookieParseError, HttpRangeError, PayloadError, UrlencodedError}; -struct HttpMessage { - version: Version, - method: Method, - uri: Uri, - prefix: usize, - headers: HeaderMap, - extensions: Extensions, - params: Params, - cookies: Vec>, - cookies_loaded: bool, - addr: Option, - payload: Payload, + +pub struct HttpMessage { + pub version: Version, + pub method: Method, + pub uri: Uri, + pub headers: HeaderMap, + pub extensions: Extensions, + pub params: Params<'static>, + pub cookies: Option>>, + pub query: Params<'static>, + pub query_loaded: bool, + pub addr: Option, + pub payload: Option, + pub info: Option>, } impl Default for HttpMessage { @@ -35,64 +43,140 @@ impl Default for HttpMessage { HttpMessage { method: Method::GET, uri: Uri::default(), - prefix: 0, version: Version::HTTP_11, - headers: HeaderMap::new(), - params: Params::empty(), - cookies: Vec::new(), - cookies_loaded: false, + headers: HeaderMap::with_capacity(16), + params: Params::new(), + query: Params::new(), + query_loaded: false, + cookies: None, addr: None, - payload: Payload::empty(), + payload: None, extensions: Extensions::new(), + info: None, } } } +impl HttpMessage { + + /// Checks if a connection should be kept alive. + #[inline] + pub fn keep_alive(&self) -> bool { + if let Some(conn) = self.headers.get(header::CONNECTION) { + if let Ok(conn) = conn.to_str() { + if self.version == Version::HTTP_10 && conn.contains("keep-alive") { + true + } else { + self.version == Version::HTTP_11 && + !(conn.contains("close") || conn.contains("upgrade")) + } + } else { + false + } + } else { + self.version != Version::HTTP_10 + } + } + + #[inline] + pub(crate) fn reset(&mut self) { + self.headers.clear(); + self.extensions.clear(); + self.params.clear(); + self.query.clear(); + self.query_loaded = false; + self.cookies = None; + self.addr = None; + self.info = None; + self.payload = None; + } +} + /// An HTTP Request -pub struct HttpRequest(Rc, Rc); +pub struct HttpRequest(SharedHttpMessage, Option>, Option); impl HttpRequest<()> { /// Construct a new Request. #[inline] pub fn new(method: Method, uri: Uri, - version: Version, headers: HeaderMap, payload: Payload) -> HttpRequest + version: Version, headers: HeaderMap, payload: Option) -> HttpRequest { HttpRequest( - Rc::new(HttpMessage { + SharedHttpMessage::from_message(HttpMessage { method: method, uri: uri, - prefix: 0, version: version, headers: headers, - params: Params::empty(), - cookies: Vec::new(), - cookies_loaded: false, + params: Params::new(), + query: Params::new(), + query_loaded: false, + cookies: None, addr: None, payload: payload, extensions: Extensions::new(), + info: None, }), - Rc::new(()) + None, + None, ) } + #[inline(always)] + #[cfg_attr(feature = "cargo-clippy", allow(inline_always))] + pub(crate) fn from_message(msg: SharedHttpMessage) -> HttpRequest { + HttpRequest(msg, None, None) + } + + #[inline] /// Construct new http request with state. - pub fn with_state(self, state: Rc) -> HttpRequest { - HttpRequest(self.0, state) + pub fn with_state(self, state: Rc, router: Router) -> HttpRequest { + HttpRequest(self.0, Some(state), Some(router)) + } + + #[cfg(test)] + /// Construct new http request with state. + pub(crate) fn with_state_no_router(self, state: Rc) -> HttpRequest { + HttpRequest(self.0, Some(state), None) } } impl HttpRequest { - /// get mutable reference for inner message - fn as_mut(&mut self) -> &mut HttpMessage { - let r: &HttpMessage = self.0.as_ref(); - #[allow(mutable_transmutes)] - unsafe{mem::transmute(r)} + #[inline] + /// Construct new http request with state. + pub fn change_state(&self, state: Rc) -> HttpRequest { + HttpRequest(self.0.clone(), Some(state), self.2.clone()) + } + + #[inline] + /// Construct new http request without state. + pub(crate) fn clone_without_state(&self) -> HttpRequest { + HttpRequest(self.0.clone(), None, None) + } + + // get mutable reference for inner message + // mutable reference should not be returned as result for request's method + #[inline(always)] + #[cfg_attr(feature = "cargo-clippy", allow(mut_from_ref, inline_always))] + pub(crate) fn as_mut(&self) -> &mut HttpMessage { + self.0.get_mut() + } + + #[inline(always)] + #[cfg_attr(feature = "cargo-clippy", allow(mut_from_ref, inline_always))] + fn as_ref(&self) -> &HttpMessage { + self.0.get_ref() + } + + #[inline] + pub(crate) fn get_inner(&mut self) -> &mut HttpMessage { + self.as_mut() } /// Shared application state + #[inline] pub fn state(&self) -> &S { - &self.1 + self.1.as_ref().unwrap() } /// Protocol extensions. @@ -101,67 +185,124 @@ impl HttpRequest { &mut self.as_mut().extensions } + #[doc(hidden)] + pub fn prefix_len(&self) -> usize { + if let Some(router) = self.router() { router.prefix().len() } else { 0 } + } + /// Read the Request Uri. #[inline] - pub fn uri(&self) -> &Uri { &self.0.uri } + pub fn uri(&self) -> &Uri { &self.as_ref().uri } /// Read the Request method. #[inline] - pub fn method(&self) -> &Method { &self.0.method } + pub fn method(&self) -> &Method { &self.as_ref().method } /// Read the Request Version. #[inline] pub fn version(&self) -> Version { - self.0.version + self.as_ref().version } /// Read the Request Headers. #[inline] pub fn headers(&self) -> &HeaderMap { - &self.0.headers + &self.as_ref().headers + } + + #[doc(hidden)] + #[inline] + pub fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.as_mut().headers } /// The target path of this Request. #[inline] pub fn path(&self) -> &str { - self.0.uri.path() + self.uri().path() } - pub(crate) fn set_prefix(&mut self, idx: usize) { - self.as_mut().prefix = idx; + /// Get *ConnectionInfo* for currect request. + pub fn connection_info(&self) -> &ConnectionInfo { + if self.as_ref().info.is_none() { + let info: ConnectionInfo<'static> = unsafe{ + mem::transmute(ConnectionInfo::new(self))}; + self.as_mut().info = Some(info); + } + self.as_ref().info.as_ref().unwrap() } - #[doc(hidden)] - pub fn prefix_len(&self) -> usize { - self.0.prefix - } - - /// Remote IP of client initiated HTTP request. + /// Generate url for named resource /// - /// The IP is resolved through the following headers, in this order: + /// ```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() + /// } /// - /// - Forwarded - /// - X-Forwarded-For - /// - peername of opened socket + /// fn main() { + /// let app = Application::new() + /// .resource("/test/{one}/{two}/{three}", |r| { + /// r.name("foo"); // <- set resource name, then it could be used in `url_for` + /// r.method(Method::GET).f(|_| httpcodes::HTTPOk); + /// }) + /// .finish(); + /// } + /// ``` + pub fn url_for(&self, name: &str, elements: U) -> Result + where U: IntoIterator, + I: AsRef, + { + if self.router().is_none() { + Err(UrlGenerationError::RouterNotAvailable) + } else { + let path = self.router().unwrap().resource_path(name, elements)?; + if path.starts_with('/') { + let conn = self.connection_info(); + Ok(Url::parse(&format!("{}://{}{}", conn.scheme(), conn.host(), path))?) + } else { + Ok(Url::parse(&path)?) + } + } + } + + /// This method returns reference to current `Router` object. #[inline] - pub fn remote(&self) -> Option<&SocketAddr> { - self.0.addr.as_ref() + pub fn router(&self) -> Option<&Router> { + self.2.as_ref() } - pub(crate) fn set_remove_addr(&mut self, addr: Option) { + /// Peer socket address + /// + /// Peer address is actuall socket address, if proxy is used in front of + /// actix http server, then peer address would be address of this proxy. + /// + /// To get client connection information `connection_info()` method should be used. + #[inline] + pub fn peer_addr(&self) -> Option<&SocketAddr> { + self.as_ref().addr.as_ref() + } + + #[inline] + pub(crate) fn set_peer_addr(&mut self, addr: Option) { self.as_mut().addr = addr } - /// Return a new iterator that yields pairs of `Cow` for query parameters - #[inline] - pub fn query(&self) -> HashMap { - let mut q: HashMap = HashMap::new(); - if let Some(query) = self.0.uri.query().as_ref() { - for (key, val) in form_urlencoded::parse(query.as_ref()) { - q.insert(key.to_string(), val.to_string()); + /// Get a reference to the Params object. + /// Params is a container for url query parameters. + pub fn query(&self) -> &Params { + if !self.as_ref().query_loaded { + let params: &mut Params = unsafe{ mem::transmute(&mut self.as_mut().query) }; + self.as_mut().query_loaded = true; + for (key, val) in form_urlencoded::parse(self.query_string().as_ref()) { + params.add(key, val); } } - q + unsafe{ mem::transmute(&self.as_ref().query) } } /// The query string in the URL. @@ -169,43 +310,40 @@ impl HttpRequest { /// E.g., id=10 #[inline] pub fn query_string(&self) -> &str { - if let Some(query) = self.0.uri.query().as_ref() { + if let Some(query) = self.uri().query().as_ref() { query } else { "" } } - /// Return request cookies. - pub fn cookies(&self) -> &Vec> { - &self.0.cookies - } - - /// Return request cookie. - pub fn cookie(&self, name: &str) -> Option<&Cookie> { - for cookie in &self.0.cookies { - if cookie.name() == name { - return Some(cookie) - } - } - None - } - - /// Load cookies - pub fn load_cookies(&mut self) -> Result<&Vec>, CookieParseError> - { - if !self.0.cookies_loaded { + /// Load request cookies. + pub fn cookies(&self) -> Result<&Vec>, CookieParseError> { + if self.as_ref().cookies.is_none() { let msg = self.as_mut(); - msg.cookies_loaded = true; + let mut cookies = Vec::new(); if let Some(val) = msg.headers.get(header::COOKIE) { let s = str::from_utf8(val.as_bytes()) .map_err(CookieParseError::from)?; for cookie in s.split("; ") { - msg.cookies.push(Cookie::parse_encoded(cookie)?.into_owned()); + cookies.push(Cookie::parse_encoded(cookie)?.into_owned()); + } + } + msg.cookies = Some(cookies) + } + Ok(self.as_ref().cookies.as_ref().unwrap()) + } + + /// Return request cookie. + pub fn cookie(&self, name: &str) -> Option<&Cookie> { + if let Ok(cookies) = self.cookies() { + for cookie in cookies { + if cookie.name() == name { + return Some(cookie) } } } - Ok(&self.0.cookies) + None } /// Get a reference to the Params object. @@ -213,34 +351,25 @@ impl HttpRequest { /// Route supports glob patterns: * for a single wildcard segment and :param /// for matching storing that segment of the request url in the Params object. #[inline] - pub fn match_info(&self) -> &Params { &self.0.params } + pub fn match_info(&self) -> &Params { + unsafe{ mem::transmute(&self.as_ref().params) } + } - /// Set request Params. - pub fn set_match_info(&mut self, params: Params) { - self.as_mut().params = params; + /// Get mutable reference to request's Params. + #[inline] + pub(crate) fn match_info_mut(&mut self) -> &mut Params { + unsafe{ mem::transmute(&mut self.as_mut().params) } } /// Checks if a connection should be kept alive. pub fn keep_alive(&self) -> bool { - if let Some(conn) = self.0.headers.get(header::CONNECTION) { - if let Ok(conn) = conn.to_str() { - if self.0.version == Version::HTTP_10 && conn.contains("keep-alive") { - true - } else { - self.0.version == Version::HTTP_11 && - !(conn.contains("close") || conn.contains("upgrade")) - } - } else { - false - } - } else { - self.0.version != Version::HTTP_10 - } + self.as_ref().keep_alive() } - /// Read the request content type + /// Read the request content type. If request does not contain + /// *Content-Type* header, empty str get returned. pub fn content_type(&self) -> &str { - if let Some(content_type) = self.0.headers.get(header::CONTENT_TYPE) { + if let Some(content_type) = self.headers().get(header::CONTENT_TYPE) { if let Ok(content_type) = content_type.to_str() { return content_type } @@ -250,17 +379,17 @@ impl HttpRequest { /// Check if request requires connection upgrade pub(crate) fn upgrade(&self) -> bool { - if let Some(conn) = self.0.headers.get(header::CONNECTION) { + if let Some(conn) = self.as_ref().headers.get(header::CONNECTION) { if let Ok(s) = conn.to_str() { return s.to_lowercase().contains("upgrade") } } - self.0.method == Method::CONNECT + self.as_ref().method == Method::CONNECT } /// Check if request has chunked transfer encoding pub fn chunked(&self) -> Result { - if let Some(encodings) = self.0.headers.get(header::TRANSFER_ENCODING) { + if let Some(encodings) = self.headers().get(header::TRANSFER_ENCODING) { if let Ok(s) = encodings.to_str() { Ok(s.to_lowercase().contains("chunked")) } else { @@ -274,7 +403,7 @@ impl HttpRequest { /// Parses Range HTTP header string as per RFC 2616. /// `size` is full size of response (file). pub fn range(&self, size: u64) -> Result, HttpRangeError> { - if let Some(range) = self.0.headers.get(header::RANGE) { + if let Some(range) = self.headers().get(header::RANGE) { HttpRange::parse(unsafe{str::from_utf8_unchecked(range.as_bytes())}, size) .map_err(|e| e.into()) } else { @@ -285,26 +414,89 @@ impl HttpRequest { /// Returns reference to the associated http payload. #[inline] pub fn payload(&self) -> &Payload { - &self.0.payload + let msg = self.as_mut(); + if msg.payload.is_none() { + msg.payload = Some(Payload::empty()); + } + msg.payload.as_ref().unwrap() } /// Returns mutable reference to the associated http payload. #[inline] pub fn payload_mut(&mut self) -> &mut Payload { - &mut self.as_mut().payload + let msg = self.as_mut(); + if msg.payload.is_none() { + msg.payload = Some(Payload::empty()); + } + msg.payload.as_mut().unwrap() } - /// Return payload - pub fn take_payload(&mut self) -> Payload { - mem::replace(&mut self.as_mut().payload, Payload::empty()) + /// Load request body. + /// + /// By default only 256Kb payload reads to a memory, then `BAD REQUEST` + /// http response get returns to a peer. Use `RequestBody::limit()` + /// method to change upper limit. + /// + /// ```rust + /// # extern crate bytes; + /// # extern crate actix_web; + /// # extern crate futures; + /// # #[macro_use] extern crate serde_derive; + /// use actix_web::*; + /// use bytes::Bytes; + /// use futures::future::Future; + /// + /// fn index(mut req: HttpRequest) -> Box> { + /// req.body() // <- get Body future + /// .limit(1024) // <- change max size of the body to a 1kb + /// .from_err() + /// .and_then(|bytes: Bytes| { // <- complete body + /// println!("==== BODY ==== {:?}", bytes); + /// Ok(httpcodes::HTTPOk.into()) + /// }).responder() + /// } + /// # fn main() {} + /// ``` + pub fn body(&self) -> RequestBody { + RequestBody::from_request(self) } - - /// Return stream to process BODY as multipart. + + /// Return stream to http payload processes as multipart. /// /// Content-type: multipart/form-data; - pub fn multipart(&mut self) -> Result { - let boundary = Multipart::boundary(&self.0.headers)?; - Ok(Multipart::new(boundary, self.take_payload())) + /// + /// ```rust + /// # extern crate actix; + /// # extern crate actix_web; + /// # extern crate env_logger; + /// # extern crate futures; + /// # use std::str; + /// # use actix::*; + /// # use actix_web::*; + /// # use futures::{Future, Stream}; + /// # use futures::future::{ok, result, Either}; + /// fn index(mut req: HttpRequest) -> Box> { + /// req.multipart().from_err() // <- get multipart stream for current request + /// .and_then(|item| match item { // <- iterate over multipart items + /// multipart::MultipartItem::Field(field) => { + /// // Field in turn is stream of *Bytes* object + /// Either::A(field.from_err() + /// .map(|c| println!("-- CHUNK: \n{:?}", str::from_utf8(&c))) + /// .finish()) + /// }, + /// multipart::MultipartItem::Nested(mp) => { + /// // Or item could be nested Multipart stream + /// Either::B(ok(())) + /// } + /// }) + /// .finish() // <- Stream::finish() combinator from actix + /// .map(|_| httpcodes::HTTPOk.into()) + /// .responder() + /// } + /// # fn main() {} + /// ``` + pub fn multipart(&mut self) -> Multipart { + Multipart::from_request(self) } /// Parse `application/x-www-form-urlencoded` encoded body. @@ -316,41 +508,60 @@ impl HttpRequest { /// * content type is not `application/x-www-form-urlencoded` /// * transfer encoding is `chunked`. /// * content-length is greater than 256k - pub fn urlencoded(&mut self) -> Result { - if let Ok(true) = self.chunked() { - return Err(UrlencodedError::Chunked) - } + /// + /// ```rust + /// # extern crate actix_web; + /// # extern crate futures; + /// use actix_web::*; + /// use futures::future::{Future, ok}; + /// + /// fn index(mut req: HttpRequest) -> Box> { + /// req.urlencoded() // <- get UrlEncoded future + /// .from_err() + /// .and_then(|params| { // <- url encoded parameters + /// println!("==== BODY ==== {:?}", params); + /// ok(httpcodes::HTTPOk.into()) + /// }) + /// .responder() + /// } + /// # fn main() {} + /// ``` + pub fn urlencoded(&self) -> UrlEncoded { + UrlEncoded::from_request(self) + } - if let Some(len) = self.headers().get(header::CONTENT_LENGTH) { - if let Ok(s) = len.to_str() { - if let Ok(len) = s.parse::() { - if len > 262_144 { - return Err(UrlencodedError::Overflow) - } - } else { - return Err(UrlencodedError::UnknownLength) - } - } else { - return Err(UrlencodedError::UnknownLength) - } - } - - // check content type - let t = if let Some(content_type) = self.0.headers.get(header::CONTENT_TYPE) { - if let Ok(content_type) = content_type.to_str() { - content_type.to_lowercase() == "application/x-www-form-urlencoded" - } else { - false - } - } else { - false - }; - - if t { - Ok(UrlEncoded{pl: self.take_payload(), body: BytesMut::new()}) - } else { - Err(UrlencodedError::ContentType) - } + /// Parse `application/json` encoded body. + /// Return `JsonBody` future. It resolves to a `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, ok}; + /// + /// #[derive(Deserialize, Debug)] + /// struct MyObj { + /// name: String, + /// } + /// + /// fn index(mut req: HttpRequest) -> Box> { + /// req.json() // <- get JsonBody future + /// .from_err() + /// .and_then(|val: MyObj| { // <- deserialized value + /// println!("==== BODY ==== {:?}", val); + /// Ok(httpcodes::HTTPOk.into()) + /// }).responder() + /// } + /// # fn main() {} + /// ``` + pub fn json(&self) -> JsonBody { + JsonBody::from_request(self) } } @@ -358,29 +569,29 @@ impl Default for HttpRequest<()> { /// Construct default request fn default() -> HttpRequest { - HttpRequest(Rc::new(HttpMessage::default()), Rc::new(())) + HttpRequest(SharedHttpMessage::default(), None, None) } } impl Clone for HttpRequest { fn clone(&self) -> HttpRequest { - HttpRequest(Rc::clone(&self.0), Rc::clone(&self.1)) + HttpRequest(self.0.clone(), self.1.clone(), None) } } impl fmt::Debug for HttpRequest { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let res = write!(f, "\nHttpRequest {:?} {}:{}\n", - self.0.version, self.0.method, self.0.uri); + self.as_ref().version, self.as_ref().method, self.as_ref().uri); if !self.query_string().is_empty() { let _ = write!(f, " query: ?{:?}\n", self.query_string()); } - if !self.0.params.is_empty() { - let _ = write!(f, " params: {:?}\n", self.0.params); + if !self.match_info().is_empty() { + let _ = write!(f, " params: {:?}\n", self.as_ref().params); } let _ = write!(f, " headers:\n"); - for key in self.0.headers.keys() { - let vals: Vec<_> = self.0.headers.get_all(key).iter().collect(); + for key in self.as_ref().headers.keys() { + let vals: Vec<_> = self.as_ref().headers.get_all(key).iter().collect(); if vals.len() > 1 { let _ = write!(f, " {:?}: {:?}\n", key, vals); } else { @@ -395,13 +606,59 @@ impl fmt::Debug for HttpRequest { pub struct UrlEncoded { pl: Payload, body: BytesMut, + error: Option, +} + +impl UrlEncoded { + pub fn from_request(req: &HttpRequest) -> UrlEncoded { + let mut encoded = UrlEncoded { + pl: req.payload().clone(), + body: BytesMut::new(), + error: None + }; + + if let Ok(true) = req.chunked() { + encoded.error = Some(UrlencodedError::Chunked); + } else if let Some(len) = req.headers().get(header::CONTENT_LENGTH) { + if let Ok(s) = len.to_str() { + if let Ok(len) = s.parse::() { + if len > 262_144 { + encoded.error = Some(UrlencodedError::Overflow); + } + } else { + encoded.error = Some(UrlencodedError::UnknownLength); + } + } else { + encoded.error = Some(UrlencodedError::UnknownLength); + } + } + + // check content type + if encoded.error.is_none() { + if let Some(content_type) = req.headers().get(header::CONTENT_TYPE) { + if let Ok(content_type) = content_type.to_str() { + if content_type.to_lowercase() == "application/x-www-form-urlencoded" { + return encoded + } + } + } + encoded.error = Some(UrlencodedError::ContentType); + return encoded + } + + encoded + } } impl Future for UrlEncoded { type Item = HashMap; - type Error = PayloadError; + type Error = UrlencodedError; fn poll(&mut self) -> Poll { + if let Some(err) = self.error.take() { + return Err(err) + } + loop { return match self.pl.poll() { Ok(Async::NotReady) => Ok(Async::NotReady), @@ -413,9 +670,81 @@ impl Future for UrlEncoded { Ok(Async::Ready(m)) }, Ok(Async::Ready(Some(item))) => { - self.body.extend(item.0); + self.body.extend_from_slice(&item); continue }, + Err(err) => Err(err.into()), + } + } + } +} + +/// Future that resolves to a complete request body. +pub struct RequestBody { + pl: ReadAny, + body: BytesMut, + limit: usize, + error: Option, +} + +impl RequestBody { + + /// Create `RequestBody` for request. + pub fn from_request(req: &HttpRequest) -> RequestBody { + let mut body = RequestBody { + pl: req.payload().readany(), + body: BytesMut::new(), + limit: 262_144, + error: None + }; + + if let Some(len) = req.headers().get(header::CONTENT_LENGTH) { + if let Ok(s) = len.to_str() { + if let Ok(len) = s.parse::() { + if len > 262_144 { + body.error = Some(PayloadError::Overflow); + } + } else { + body.error = Some(PayloadError::UnknownLength); + } + } else { + body.error = Some(PayloadError::UnknownLength); + } + } + + body + } + + /// Change max size of payload. By default max size is 256Kb + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } +} + +impl Future for RequestBody { + type Item = Bytes; + type Error = PayloadError; + + fn poll(&mut self) -> Poll { + if let Some(err) = self.error.take() { + return Err(err) + } + + loop { + return match self.pl.poll() { + Ok(Async::NotReady) => Ok(Async::NotReady), + Ok(Async::Ready(None)) => { + Ok(Async::Ready(self.body.take().freeze())) + }, + Ok(Async::Ready(Some(chunk))) => { + if (self.body.len() + chunk.len()) > self.limit { + Err(PayloadError::Overflow) + } else { + self.body.extend_from_slice(&chunk); + continue + } + }, Err(err) => Err(err), } } @@ -425,52 +754,239 @@ impl Future for UrlEncoded { #[cfg(test)] mod tests { use super::*; - use std::str::FromStr; - use payload::Payload; use http::Uri; + use std::str::FromStr; + use router::Pattern; + use resource::Resource; + use test::TestRequest; + use server::ServerSettings; + + #[test] + fn test_debug() { + let req = TestRequest::with_header("content-type", "text/plain").finish(); + let dbg = format!("{:?}", req); + assert!(dbg.contains("HttpRequest")); + } + + #[test] + fn test_no_request_cookies() { + let req = HttpRequest::default(); + assert!(req.cookies().unwrap().is_empty()); + } + + #[test] + fn test_request_cookies() { + let req = TestRequest::with_header( + header::COOKIE, "cookie1=value1; cookie2=value2").finish(); + { + let cookies = req.cookies().unwrap(); + assert_eq!(cookies.len(), 2); + assert_eq!(cookies[0].name(), "cookie1"); + assert_eq!(cookies[0].value(), "value1"); + assert_eq!(cookies[1].name(), "cookie2"); + assert_eq!(cookies[1].value(), "value2"); + } + + let cookie = req.cookie("cookie1"); + assert!(cookie.is_some()); + let cookie = cookie.unwrap(); + assert_eq!(cookie.name(), "cookie1"); + assert_eq!(cookie.value(), "value1"); + + let cookie = req.cookie("cookie-unknown"); + assert!(cookie.is_none()); + } + + #[test] + fn test_no_request_range_header() { + let req = HttpRequest::default(); + let ranges = req.range(100).unwrap(); + assert!(ranges.is_empty()); + } + + #[test] + fn test_request_range_header() { + let req = TestRequest::with_header(header::RANGE, "bytes=0-4").finish(); + let ranges = req.range(100).unwrap(); + assert_eq!(ranges.len(), 1); + assert_eq!(ranges[0].start, 0); + assert_eq!(ranges[0].length, 5); + } + + #[test] + fn test_request_query() { + let req = TestRequest::with_uri("/?id=test").finish(); + assert_eq!(req.query_string(), "id=test"); + let query = req.query(); + assert_eq!(&query["id"], "test"); + } + + #[test] + fn test_request_match_info() { + let mut req = TestRequest::with_uri("/value/?id=test").finish(); + + let mut resource = Resource::<()>::default(); + resource.name("index"); + let mut map = HashMap::new(); + map.insert(Pattern::new("index", "/{key}/", "^/"), Some(resource)); + let (router, _) = Router::new("", ServerSettings::default(), map); + assert!(router.recognize(&mut req).is_some()); + + assert_eq!(req.match_info().get("key"), Some("value")); + } + + #[test] + fn test_chunked() { + let req = HttpRequest::default(); + assert!(!req.chunked().unwrap()); + + let req = TestRequest::with_header(header::TRANSFER_ENCODING, "chunked").finish(); + assert!(req.chunked().unwrap()); + + let mut headers = HeaderMap::new(); + let s = unsafe{str::from_utf8_unchecked(b"some va\xadscc\xacas0xsdasdlue".as_ref())}; + + headers.insert(header::TRANSFER_ENCODING, + header::HeaderValue::from_str(s).unwrap()); + let req = HttpRequest::new( + Method::GET, Uri::from_str("/").unwrap(), + Version::HTTP_11, headers, None); + assert!(req.chunked().is_err()); + } + + impl PartialEq for UrlencodedError { + fn eq(&self, other: &UrlencodedError) -> bool { + match *self { + UrlencodedError::Chunked => match *other { + UrlencodedError::Chunked => true, + _ => false, + }, + UrlencodedError::Overflow => match *other { + UrlencodedError::Overflow => true, + _ => false, + }, + UrlencodedError::UnknownLength => match *other { + UrlencodedError::UnknownLength => true, + _ => false, + }, + UrlencodedError::ContentType => match *other { + UrlencodedError::ContentType => true, + _ => false, + }, + _ => false, + } + } + } #[test] fn test_urlencoded_error() { - let mut headers = HeaderMap::new(); - headers.insert(header::TRANSFER_ENCODING, - header::HeaderValue::from_static("chunked")); - let mut req = HttpRequest::new( - Method::GET, Uri::from_str("/").unwrap(), - Version::HTTP_11, headers, Payload::empty()); + let req = TestRequest::with_header(header::TRANSFER_ENCODING, "chunked").finish(); + assert_eq!(req.urlencoded().poll().err().unwrap(), UrlencodedError::Chunked); - assert_eq!(req.urlencoded().err().unwrap(), UrlencodedError::Chunked); + let req = TestRequest::with_header( + header::CONTENT_TYPE, "application/x-www-form-urlencoded") + .header(header::CONTENT_LENGTH, "xxxx") + .finish(); + assert_eq!(req.urlencoded().poll().err().unwrap(), UrlencodedError::UnknownLength); - let mut headers = HeaderMap::new(); - headers.insert(header::CONTENT_TYPE, - header::HeaderValue::from_static("application/x-www-form-urlencoded")); - headers.insert(header::CONTENT_LENGTH, - header::HeaderValue::from_static("xxxx")); - let mut req = HttpRequest::new( - Method::GET, Uri::from_str("/").unwrap(), Version::HTTP_11, - headers, Payload::empty()); + let req = TestRequest::with_header( + header::CONTENT_TYPE, "application/x-www-form-urlencoded") + .header(header::CONTENT_LENGTH, "1000000") + .finish(); + assert_eq!(req.urlencoded().poll().err().unwrap(), UrlencodedError::Overflow); - assert_eq!(req.urlencoded().err().unwrap(), UrlencodedError::UnknownLength); + let req = TestRequest::with_header( + header::CONTENT_TYPE, "text/plain") + .header(header::CONTENT_LENGTH, "10") + .finish(); + assert_eq!(req.urlencoded().poll().err().unwrap(), UrlencodedError::ContentType); + } - let mut headers = HeaderMap::new(); - headers.insert(header::CONTENT_TYPE, - header::HeaderValue::from_static("application/x-www-form-urlencoded")); - headers.insert(header::CONTENT_LENGTH, - header::HeaderValue::from_static("1000000")); - let mut req = HttpRequest::new( - Method::GET, Uri::from_str("/").unwrap(), - Version::HTTP_11, headers, Payload::empty()); + #[test] + fn test_request_body() { + let req = TestRequest::with_header(header::CONTENT_LENGTH, "xxxx").finish(); + match req.body().poll().err().unwrap() { + PayloadError::UnknownLength => (), + _ => panic!("error"), + } - assert_eq!(req.urlencoded().err().unwrap(), UrlencodedError::Overflow); + let req = TestRequest::with_header(header::CONTENT_LENGTH, "1000000").finish(); + match req.body().poll().err().unwrap() { + PayloadError::Overflow => (), + _ => panic!("error"), + } - let mut headers = HeaderMap::new(); - headers.insert(header::CONTENT_TYPE, - header::HeaderValue::from_static("text/plain")); - headers.insert(header::CONTENT_LENGTH, - header::HeaderValue::from_static("10")); - let mut req = HttpRequest::new( - Method::GET, Uri::from_str("/").unwrap(), - Version::HTTP_11, headers, Payload::empty()); + let mut req = HttpRequest::default(); + req.payload_mut().unread_data(Bytes::from_static(b"test")); + match req.body().poll().ok().unwrap() { + Async::Ready(bytes) => assert_eq!(bytes, Bytes::from_static(b"test")), + _ => panic!("error"), + } - assert_eq!(req.urlencoded().err().unwrap(), UrlencodedError::ContentType); + let mut req = HttpRequest::default(); + req.payload_mut().unread_data(Bytes::from_static(b"11111111111111")); + match req.body().limit(5).poll().err().unwrap() { + PayloadError::Overflow => (), + _ => panic!("error"), + } + } + + #[test] + fn test_url_for() { + let req = TestRequest::with_header(header::HOST, "www.rust-lang.org") + .finish_no_router(); + + let mut resource = Resource::<()>::default(); + resource.name("index"); + let mut map = HashMap::new(); + map.insert(Pattern::new("index", "/user/{name}.{ext}", "^/"), Some(resource)); + let (router, _) = Router::new("/", ServerSettings::default(), map); + assert!(router.has_route("/user/test.html")); + assert!(!router.has_route("/test/unknown")); + + assert_eq!(req.url_for("unknown", &["test"]), + Err(UrlGenerationError::RouterNotAvailable)); + + let req = req.with_state(Rc::new(()), router); + + assert_eq!(req.url_for("unknown", &["test"]), + Err(UrlGenerationError::ResourceNotFound)); + assert_eq!(req.url_for("index", &["test"]), + Err(UrlGenerationError::NotEnoughElements)); + let url = req.url_for("index", &["test", "html"]); + assert_eq!(url.ok().unwrap().as_str(), "http://www.rust-lang.org/user/test.html"); + } + + #[test] + fn test_url_for_with_prefix() { + let req = TestRequest::with_header(header::HOST, "www.rust-lang.org").finish(); + + let mut resource = Resource::<()>::default(); + resource.name("index"); + let mut map = HashMap::new(); + map.insert(Pattern::new("index", "/user/{name}.{ext}", "^/"), Some(resource)); + let (router, _) = Router::new("/prefix/", ServerSettings::default(), map); + assert!(router.has_route("/user/test.html")); + assert!(!router.has_route("/prefix/user/test.html")); + + let req = req.with_state(Rc::new(()), router); + let url = req.url_for("index", &["test", "html"]); + assert_eq!(url.ok().unwrap().as_str(), "http://www.rust-lang.org/prefix/user/test.html"); + } + + #[test] + fn test_url_for_external() { + let req = HttpRequest::default(); + + let mut resource = Resource::<()>::default(); + resource.name("index"); + let mut map = HashMap::new(); + map.insert(Pattern::new("youtube", "https://youtube.com/watch/{video_id}", "^/"), None); + let (router, _) = Router::new::<()>("", ServerSettings::default(), map); + assert!(!router.has_route("https://youtube.com/watch/unknown")); + + let req = req.with_state(Rc::new(()), router); + let url = req.url_for("youtube", &["oHg5SJYRHA0"]); + assert_eq!(url.ok().unwrap().as_str(), "https://youtube.com/watch/oHg5SJYRHA0"); } } diff --git a/src/httpresponse.rs b/src/httpresponse.rs index 75f45c844..f08553013 100644 --- a/src/httpresponse.rs +++ b/src/httpresponse.rs @@ -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, - pub headers: HeaderMap, - pub status: StatusCode, - reason: Option<&'static str>, - body: Body, - chunked: bool, - encoding: ContentEncoding, - connection_type: Option, - response_size: u64, - error: Option, +pub struct HttpResponse(Option>); + +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 { - 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("") } } /// 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 { - 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>(&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>(&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, - headers: HeaderMap, - status: StatusCode, - reason: Option<&'static str>, - chunked: bool, - encoding: ContentEncoding, - connection_type: Option, - 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, + response: Option>, err: Option, + cookies: Option, } 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 { + /// Ok(HTTPOk.build() + /// .header("X-TEST", "value") + /// .header(header::CONTENT_TYPE, "application/json") + /// .finish()?) + /// } + /// fn main() {} + /// ``` #[inline] pub fn header(&mut self, key: K, value: V) -> &mut Self where HeaderName: HttpTryFrom, HeaderValue: HttpTryFrom { - 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(&mut self, value: V) -> &mut Self where HeaderValue: HttpTryFrom { - 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 { + /// 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(&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>(&mut self, body: B) -> Result { - 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(&mut self, value: T) -> Result { 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 { 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, err: &Option) -> Option<&'a mut Parts> +#[inline] +#[cfg_attr(feature = "cargo-clippy", allow(borrowed_box))] +fn parts<'a>(parts: &'a mut Option>, err: &Option) + -> Option<&'a mut Box> { if err.is_some() { return None @@ -458,6 +485,16 @@ impl From for HttpResponse { } } +impl Responder for HttpResponseBuilder { + type Item = HttpResponse; + type Error = HttpError; + + #[inline] + fn respond_to(mut self, _: HttpRequest) -> Result { + 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::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::build(StatusCode::OK) + .content_type("application/octet-stream") + .body(self) + } +} + impl From for HttpResponse { fn from(val: String) -> Self { HttpResponse::build(StatusCode::OK) @@ -485,6 +544,17 @@ impl From for HttpResponse { } } +impl Responder for String { + type Item = HttpResponse; + type Error = HttpError; + + fn respond_to(self, _: HttpRequest) -> Result { + 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::build(StatusCode::OK) + .content_type("text/plain; charset=utf-8") + .body(self) + } +} + impl From for HttpResponse { fn from(val: Bytes) -> Self { HttpResponse::build(StatusCode::OK) @@ -503,6 +584,17 @@ impl From for HttpResponse { } } +impl Responder for Bytes { + type Item = HttpResponse; + type Error = HttpError; + + fn respond_to(self, _: HttpRequest) -> Result { + HttpResponse::build(StatusCode::OK) + .content_type("application/octet-stream") + .body(self) + } +} + impl From for HttpResponse { fn from(val: BytesMut) -> Self { HttpResponse::build(StatusCode::OK) @@ -512,19 +604,160 @@ impl From for HttpResponse { } } +impl Responder for BytesMut { + type Item = HttpResponse; + type Error = HttpError; + + fn respond_to(self, _: HttpRequest) -> Result { + HttpResponse::build(StatusCode::OK) + .content_type("application/octet-stream") + .body(self) + } +} + +#[derive(Debug)] +struct InnerHttpResponse { + version: Option, + headers: HeaderMap, + status: StatusCode, + reason: Option<&'static str>, + body: Body, + chunked: bool, + encoding: ContentEncoding, + connection_type: Option, + response_size: u64, + error: Option, +} + +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>); + +thread_local!(static POOL: RefCell = + RefCell::new(Pool(VecDeque::with_capacity(128)))); + +impl Pool { + + #[inline] + fn get(status: StatusCode) -> Box { + 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 { + 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) { + 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"))); } } diff --git a/src/info.rs b/src/info.rs new file mode 100644 index 000000000..92ffa4d6c --- /dev/null +++ b/src/info.rs @@ -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, +} + +impl<'a> ConnectionInfo<'a> { + + /// Create *ConnectionInfo* instance for a request. + #[cfg_attr(feature = "cargo-clippy", allow(cyclomatic_complexity))] + pub fn new(req: &'a HttpRequest) -> 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"); + } +} diff --git a/src/json.rs b/src/json.rs new file mode 100644 index 000000000..8bcda5c90 --- /dev/null +++ b/src/json.rs @@ -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 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> { +/// Ok(Json(MyObj{name: req.match_info().query("name")?})) +/// } +/// # fn main() {} +/// ``` +pub struct Json (pub T); + +impl Responder for Json { + type Item = HttpResponse; + type Error = Error; + + fn respond_to(self, _: HttpRequest) -> Result { + 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> { +/// 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{ + limit: usize, + ct: &'static str, + req: Option>, + fut: Option>>, +} + +impl JsonBody { + + /// Create `JsonBody` for request. + pub fn from_request(req: &HttpRequest) -> 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 Future for JsonBody { + type Item = T; + type Error = JsonPayloadError; + + fn poll(&mut self) -> Poll { + 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::() { + 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::(&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::(); + assert_eq!(json.poll().err().unwrap(), JsonPayloadError::ContentType); + + let mut json = req.json::().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::().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::(); + assert_eq!(json.poll().ok().unwrap(), Async::Ready(MyObject{name: "test".to_owned()})); + } + +} diff --git a/src/lib.rs b/src/lib.rs index fb2b1a3aa..5238a684e 100644 --- a/src/lib.rs +++ b/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}; +} diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs new file mode 100644 index 000000000..a013b7a57 --- /dev/null +++ b/src/middleware/defaultheaders.rs @@ -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 Middleware for DefaultHeaders { + + fn response(&self, _: &mut HttpRequest, 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, +} + +impl DefaultHeadersBuilder { + + /// Set a header. + #[inline] + #[cfg_attr(feature = "cargo-clippy", allow(match_wild_err_arm))] + pub fn header(&mut self, key: K, value: V) -> &mut Self + where HeaderName: HttpTryFrom, + HeaderValue: HttpTryFrom + { + 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"); + } +} diff --git a/src/middlewares/logger.rs b/src/middleware/logger.rs similarity index 88% rename from src/middlewares/logger.rs rename to src/middleware/logger.rs index 92117ff00..f7c008c1c 100644 --- a/src/middlewares/logger.rs +++ b/src/middleware/logger.rs @@ -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(&self, req: &mut HttpRequest, resp: &HttpResponse) { let entry_time = req.extensions().get::().unwrap().0; let render = |fmt: &mut Formatter| { @@ -99,20 +99,19 @@ impl Logger { } } -impl Middleware for Logger { +impl Middleware for Logger { - fn start(&self, req: &mut HttpRequest) -> Started { + fn start(&self, req: &mut HttpRequest) -> Started { req.extensions().insert(StartTime(time::now())); Started::Done } - fn finish(&self, req: &mut HttpRequest, resp: &HttpResponse) -> Finished { + fn finish(&self, req: &mut HttpRequest, 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); 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(&self, fmt: &mut Formatter, + req: &HttpRequest, + 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(); diff --git a/src/middlewares/mod.rs b/src/middleware/mod.rs similarity index 83% rename from src/middlewares/mod.rs rename to src/middleware/mod.rs index abb25800a..b9798c97b 100644 --- a/src/middlewares/mod.rs +++ b/src/middleware/mod.rs @@ -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 { /// 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) -> 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, 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, resp: &HttpResponse) -> Finished { Finished::Done } } diff --git a/src/middlewares/session.rs b/src/middleware/session.rs similarity index 78% rename from src/middlewares/session.rs rename to src/middleware/session.rs index 24ad9fefb..7f610e2d2 100644 --- a/src/middlewares/session.rs +++ b/src/middleware/session.rs @@ -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::("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 RequestSession for HttpRequest { fn session(&mut self) -> Session { if let Some(s_impl) = self.extensions().get_mut::>() { @@ -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::("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); +/// +/// ```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, PhantomData); -impl SessionStorage { +impl> SessionStorage { /// Create session storage - pub fn new(backend: T) -> SessionStorage { - SessionStorage(backend) + pub fn new(backend: T) -> SessionStorage { + SessionStorage(backend, PhantomData) } } -impl Middleware for SessionStorage { +impl> Middleware for SessionStorage { - fn start(&self, req: &mut HttpRequest) -> Started { + fn start(&self, req: &mut HttpRequest) -> Started { let mut req = req.clone(); let fut = self.0.from_request(&mut req) @@ -106,7 +157,7 @@ impl Middleware for SessionStorage { Started::Future(Box::new(fut)) } - fn response(&self, req: &mut HttpRequest, resp: HttpResponse) -> Response { + fn response(&self, req: &mut HttpRequest, resp: HttpResponse) -> Response { if let Some(s_box) = req.extensions().remove::>() { 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: Sized + 'static { type Session: SessionImpl; type ReadFuture: Future; /// 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) -> 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 { - if let Ok(cookies) = req.load_cookies() { + fn load(&self, req: &mut HttpRequest) -> HashMap { + 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 SessionBackend for CookieSessionBackend { type Session = CookieSession; type ReadFuture = FutureResult; - fn from_request(&self, req: &mut HttpRequest) -> Self::ReadFuture { + fn from_request(&self, req: &mut HttpRequest) -> 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>(mut self, value: S) -> CookieSessionBackendBuilder { + self.0.name = value.into(); + self + } + /// Sets the `domain` field in the session cookie being built. pub fn domain>(mut self, value: S) -> CookieSessionBackendBuilder { self.0.domain = Some(value.into()); diff --git a/src/multipart.rs b/src/multipart.rs index f09c135fd..aa3f72018 100644 --- a/src/multipart.rs +++ b/src/multipart.rs @@ -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>, + error: Option, + inner: Option>>, } /// @@ -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(req: &mut HttpRequest) -> 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, 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 { - 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 { // 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, 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, 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, MultipartError> { + fn poll(&mut self, s: &Safety) -> Poll, 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() { diff --git a/src/param.rs b/src/param.rs new file mode 100644 index 000000000..530e62089 --- /dev/null +++ b/src/param.rs @@ -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; +} + +/// 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(&mut self, name: N, value: V) + where N: Into>, V: Into>, + { + 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 { + /// let ivalue: isize = req.match_info().query("val")?; + /// Ok(format!("isuze value: {:?}", ivalue)) + /// } + /// # fn main() {} + /// ``` + pub fn query(&'a self, key: &str) -> Result::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 { + 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 { + <$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"]))); + } +} diff --git a/src/payload.rs b/src/payload.rs index a77bdcba0..df2e4f7fb 100644 --- a/src/payload.rs +++ b/src/payload.rs @@ -1,26 +1,22 @@ +//! Payload stream use std::{fmt, cmp}; use std::rc::{Rc, Weak}; use std::cell::RefCell; use std::collections::VecDeque; use std::ops::{Deref, DerefMut}; use bytes::{Bytes, BytesMut}; -use futures::{Async, Poll, Stream}; +use futures::{Future, Async, Poll, Stream}; use futures::task::{Task, current as current_task}; -use actix::ResponseType; +use body::BodyStream; use error::PayloadError; pub(crate) const DEFAULT_BUFFER_SIZE: usize = 65_536; // max buffer size 64k /// Just Bytes object -#[derive(PartialEq)] +#[derive(PartialEq, Message)] pub struct PayloadItem(pub Bytes); -impl ResponseType for PayloadItem { - type Item = (); - type Error = (); -} - impl Deref for PayloadItem { type Target = Bytes; @@ -41,9 +37,12 @@ impl fmt::Debug for PayloadItem { } } -/// Stream of byte chunks +/// Buffered stream of bytes chunks /// -/// Payload stores chunks in vector. First chunk can be received with `.readany()` method. +/// Payload stores chunks in a vector. First chunk can be received with `.readany()` method. +/// Payload stream is not thread safe. +/// +/// Payload stream can be used as `HttpResponse` body stream. #[derive(Debug)] pub struct Payload { inner: Rc>, @@ -51,7 +50,14 @@ pub struct Payload { impl Payload { - pub(crate) fn new(eof: bool) -> (PayloadSender, Payload) { + /// Create payload stream. + /// + /// This method construct two objects responsible for bytes stream generation. + /// + /// * `PayloadSender` - *Sender* side of the stream + /// + /// * `Payload` - *Receiver* side of the stream + pub fn new(eof: bool) -> (PayloadSender, Payload) { let shared = Rc::new(RefCell::new(Inner::new(eof))); (PayloadSender{inner: Rc::downgrade(&shared)}, Payload{inner: shared}) @@ -79,31 +85,27 @@ impl Payload { } /// Get first available chunk of data. - /// Returns Some(PayloadItem) as chunk, `None` indicates eof. - pub fn readany(&mut self) -> Poll, PayloadError> { - self.inner.borrow_mut().readany() + pub fn readany(&self) -> ReadAny { + ReadAny(Rc::clone(&self.inner)) } - /// Get exactly number of bytes - /// Returns Some(PayloadItem) as chunk, `None` indicates eof. - pub fn readexactly(&mut self, size: usize) -> Result, PayloadError> { - self.inner.borrow_mut().readexactly(size) + /// Get exact number of bytes + pub fn readexactly(&self, size: usize) -> ReadExactly { + ReadExactly(Rc::clone(&self.inner), size) } /// Read until `\n` - /// Returns Some(PayloadItem) as line, `None` indicates eof. - pub fn readline(&mut self) -> Result, PayloadError> { - self.inner.borrow_mut().readline() + pub fn readline(&self) -> ReadLine { + ReadLine(Rc::clone(&self.inner)) } /// Read until match line - /// Returns Some(PayloadItem) as line, `None` indicates eof. - pub fn readuntil(&mut self, line: &[u8]) -> Result, PayloadError> { - self.inner.borrow_mut().readuntil(line) + pub fn readuntil(&self, line: &[u8]) -> ReadUntil { + ReadUntil(Rc::clone(&self.inner), line.to_vec()) } #[doc(hidden)] - pub fn readall(&mut self) -> Option { + pub fn readall(&self) -> Option { self.inner.borrow_mut().readall() } @@ -121,6 +123,11 @@ impl Payload { pub fn set_buffer_size(&self, size: usize) { self.inner.borrow_mut().set_buffer_size(size) } + + /// Convert payload into compatible `HttpResponse` body stream + pub fn stream(self) -> BodyStream { + Box::new(self.map(|i| i.0).map_err(|e| e.into())) + } } impl Stream for Payload { @@ -128,21 +135,95 @@ impl Stream for Payload { type Error = PayloadError; fn poll(&mut self) -> Poll, PayloadError> { - self.readany() + self.inner.borrow_mut().readany() } } -pub(crate) trait PayloadWriter { +impl Clone for Payload { + fn clone(&self) -> Payload { + Payload{inner: Rc::clone(&self.inner)} + } +} + +/// Get first available chunk of data +pub struct ReadAny(Rc>); + +impl Stream for ReadAny { + type Item = Bytes; + type Error = PayloadError; + + fn poll(&mut self) -> Poll, Self::Error> { + match self.0.borrow_mut().readany()? { + Async::Ready(Some(item)) => Ok(Async::Ready(Some(item.0))), + Async::Ready(None) => Ok(Async::Ready(None)), + Async::NotReady => Ok(Async::NotReady), + } + } +} + +/// Get exact number of bytes +pub struct ReadExactly(Rc>, usize); + +impl Future for ReadExactly { + type Item = Bytes; + type Error = PayloadError; + + fn poll(&mut self) -> Poll { + match self.0.borrow_mut().readexactly(self.1)? { + Async::Ready(chunk) => Ok(Async::Ready(chunk)), + Async::NotReady => Ok(Async::NotReady), + } + } +} + +/// Read until `\n` +pub struct ReadLine(Rc>); + +impl Future for ReadLine { + type Item = Bytes; + type Error = PayloadError; + + fn poll(&mut self) -> Poll { + match self.0.borrow_mut().readline()? { + Async::Ready(chunk) => Ok(Async::Ready(chunk)), + Async::NotReady => Ok(Async::NotReady), + } + } +} + +/// Read until match line +pub struct ReadUntil(Rc>, Vec); + +impl Future for ReadUntil { + type Item = Bytes; + type Error = PayloadError; + + fn poll(&mut self) -> Poll { + match self.0.borrow_mut().readuntil(&self.1)? { + Async::Ready(chunk) => Ok(Async::Ready(chunk)), + Async::NotReady => Ok(Async::NotReady), + } + } +} + +/// Payload writer interface. +pub trait PayloadWriter { + + /// Set stream error. fn set_error(&mut self, err: PayloadError); + /// Write eof into a stream which closes reading side of a stream. fn feed_eof(&mut self); + /// Feed bytes into a payload stream fn feed_data(&mut self, data: Bytes); + /// Get estimated available capacity fn capacity(&self) -> usize; } -pub(crate) struct PayloadSender { +/// Sender part of the payload stream +pub struct PayloadSender { inner: Weak>, } @@ -250,7 +331,7 @@ impl Inner { let mut chunk = self.items.pop_front().unwrap(); let rem = cmp::min(size - buf.len(), chunk.len()); self.len -= rem; - buf.extend(&chunk.split_to(rem)); + buf.extend_from_slice(&chunk.split_to(rem)); if !chunk.is_empty() { self.items.push_front(chunk); return Ok(Async::Ready(buf.freeze())) @@ -299,12 +380,12 @@ impl Inner { let mut buf = BytesMut::with_capacity(length); if num > 0 { for _ in 0..num { - buf.extend(self.items.pop_front().unwrap()); + buf.extend_from_slice(&self.items.pop_front().unwrap()); } } if offset > 0 { let mut chunk = self.items.pop_front().unwrap(); - buf.extend(chunk.split_to(offset)); + buf.extend_from_slice(&chunk.split_to(offset)); if !chunk.is_empty() { self.items.push_front(chunk) } @@ -330,7 +411,7 @@ impl Inner { if len > 0 { let mut buf = BytesMut::with_capacity(len); for item in &self.items { - buf.extend(item); + buf.extend_from_slice(item); } self.items = VecDeque::new(); self.len = 0; @@ -342,7 +423,7 @@ impl Inner { fn unread_data(&mut self, data: Bytes) { self.len += data.len(); - self.items.push_front(data) + self.items.push_front(data); } fn capacity(&self) -> usize { @@ -383,12 +464,12 @@ mod tests { #[test] fn test_basic() { Core::new().unwrap().run(lazy(|| { - let (_, mut payload) = Payload::new(false); + let (_, payload) = Payload::new(false); assert!(!payload.eof()); assert!(payload.is_empty()); assert_eq!(payload.len(), 0); - assert_eq!(Async::NotReady, payload.readany().ok().unwrap()); + assert_eq!(Async::NotReady, payload.readany().poll().ok().unwrap()); let res: Result<(), ()> = Ok(()); result(res) @@ -398,9 +479,9 @@ mod tests { #[test] fn test_eof() { Core::new().unwrap().run(lazy(|| { - let (mut sender, mut payload) = Payload::new(false); + let (mut sender, payload) = Payload::new(false); - assert_eq!(Async::NotReady, payload.readany().ok().unwrap()); + assert_eq!(Async::NotReady, payload.readany().poll().ok().unwrap()); assert!(!payload.eof()); sender.feed_data(Bytes::from("data")); @@ -408,13 +489,13 @@ mod tests { assert!(!payload.eof()); - assert_eq!(Async::Ready(Some(PayloadItem(Bytes::from("data")))), - payload.readany().ok().unwrap()); + assert_eq!(Async::Ready(Some(Bytes::from("data"))), + payload.readany().poll().ok().unwrap()); assert!(payload.is_empty()); assert!(payload.eof()); assert_eq!(payload.len(), 0); - assert_eq!(Async::Ready(None), payload.readany().ok().unwrap()); + assert_eq!(Async::Ready(None), payload.readany().poll().ok().unwrap()); let res: Result<(), ()> = Ok(()); result(res) })).unwrap(); @@ -423,12 +504,12 @@ mod tests { #[test] fn test_err() { Core::new().unwrap().run(lazy(|| { - let (mut sender, mut payload) = Payload::new(false); + let (mut sender, payload) = Payload::new(false); - assert_eq!(Async::NotReady, payload.readany().ok().unwrap()); + assert_eq!(Async::NotReady, payload.readany().poll().ok().unwrap()); sender.set_error(PayloadError::Incomplete); - payload.readany().err().unwrap(); + payload.readany().poll().err().unwrap(); let res: Result<(), ()> = Ok(()); result(res) })).unwrap(); @@ -437,7 +518,7 @@ mod tests { #[test] fn test_readany() { Core::new().unwrap().run(lazy(|| { - let (mut sender, mut payload) = Payload::new(false); + let (mut sender, payload) = Payload::new(false); sender.feed_data(Bytes::from("line1")); @@ -448,8 +529,8 @@ mod tests { assert!(!payload.is_empty()); assert_eq!(payload.len(), 10); - assert_eq!(Async::Ready(Some(PayloadItem(Bytes::from("line1")))), - payload.readany().ok().unwrap()); + assert_eq!(Async::Ready(Some(Bytes::from("line1"))), + payload.readany().poll().ok().unwrap()); assert!(!payload.is_empty()); assert_eq!(payload.len(), 5); @@ -461,22 +542,24 @@ mod tests { #[test] fn test_readexactly() { Core::new().unwrap().run(lazy(|| { - let (mut sender, mut payload) = Payload::new(false); + let (mut sender, payload) = Payload::new(false); - assert_eq!(Async::NotReady, payload.readexactly(2).ok().unwrap()); + assert_eq!(Async::NotReady, payload.readexactly(2).poll().ok().unwrap()); sender.feed_data(Bytes::from("line1")); sender.feed_data(Bytes::from("line2")); assert_eq!(payload.len(), 10); - assert_eq!(Async::Ready(Bytes::from("li")), payload.readexactly(2).ok().unwrap()); + assert_eq!(Async::Ready(Bytes::from("li")), + payload.readexactly(2).poll().ok().unwrap()); assert_eq!(payload.len(), 8); - assert_eq!(Async::Ready(Bytes::from("ne1l")), payload.readexactly(4).ok().unwrap()); + assert_eq!(Async::Ready(Bytes::from("ne1l")), + payload.readexactly(4).poll().ok().unwrap()); assert_eq!(payload.len(), 4); sender.set_error(PayloadError::Incomplete); - payload.readexactly(10).err().unwrap(); + payload.readexactly(10).poll().err().unwrap(); let res: Result<(), ()> = Ok(()); result(res) @@ -486,24 +569,24 @@ mod tests { #[test] fn test_readuntil() { Core::new().unwrap().run(lazy(|| { - let (mut sender, mut payload) = Payload::new(false); + let (mut sender, payload) = Payload::new(false); - assert_eq!(Async::NotReady, payload.readuntil(b"ne").ok().unwrap()); + assert_eq!(Async::NotReady, payload.readuntil(b"ne").poll().ok().unwrap()); sender.feed_data(Bytes::from("line1")); sender.feed_data(Bytes::from("line2")); assert_eq!(payload.len(), 10); assert_eq!(Async::Ready(Bytes::from("line")), - payload.readuntil(b"ne").ok().unwrap()); + payload.readuntil(b"ne").poll().ok().unwrap()); assert_eq!(payload.len(), 6); assert_eq!(Async::Ready(Bytes::from("1line2")), - payload.readuntil(b"2").ok().unwrap()); + payload.readuntil(b"2").poll().ok().unwrap()); assert_eq!(payload.len(), 0); sender.set_error(PayloadError::Incomplete); - payload.readuntil(b"b").err().unwrap(); + payload.readuntil(b"b").poll().err().unwrap(); let res: Result<(), ()> = Ok(()); result(res) @@ -519,8 +602,8 @@ mod tests { assert!(!payload.is_empty()); assert_eq!(payload.len(), 4); - assert_eq!(Async::Ready(Some(PayloadItem(Bytes::from("data")))), - payload.readany().ok().unwrap()); + assert_eq!(Async::Ready(Some(Bytes::from("data"))), + payload.readany().poll().ok().unwrap()); let res: Result<(), ()> = Ok(()); result(res) diff --git a/src/pipeline.rs b/src/pipeline.rs index 85473fc74..44c503104 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -1,97 +1,62 @@ use std::{io, mem}; use std::rc::Rc; use std::cell::RefCell; +use std::marker::PhantomData; use futures::{Async, Poll, Future, Stream}; -use futures::task::{Task as FutureTask, current as current_task}; +use futures::unsync::oneshot; +use channel::HttpHandlerTask; use body::{Body, BodyStream}; -use context::{Frame, IoContext}; -use error::{Error, UnexpectedTaskFrame}; -use route::{Reply, ReplyItem}; +use context::{Frame, ActorHttpContext}; +use error::Error; +use handler::{Reply, ReplyItem}; use h1writer::{Writer, WriterState}; use httprequest::HttpRequest; use httpresponse::HttpResponse; -use middlewares::{Middleware, Finished, Started, Response}; +use middleware::{Middleware, Finished, Started, Response}; +use application::Inner; -type Handler = Fn(HttpRequest) -> Reply; -pub(crate) type PipelineHandler<'a> = &'a Fn(HttpRequest) -> Reply; +pub(crate) trait PipelineHandler { + fn handle(&mut self, req: HttpRequest) -> Reply; +} -pub struct Pipeline(PipelineState); +pub(crate) struct Pipeline(PipelineInfo, PipelineState); -enum PipelineState { +enum PipelineState { None, Error, - Starting(StartMiddlewares), - Handler(WaitingResponse), - RunMiddlewares(RunMiddlewares), - Response(ProcessResponse), - Finishing(FinishingMiddlewares), - Completed(Completed), + Starting(StartMiddlewares), + Handler(WaitingResponse), + RunMiddlewares(RunMiddlewares), + Response(ProcessResponse), + Finishing(FinishingMiddlewares), + Completed(Completed), } -impl PipelineState { - - fn is_done(&self) -> bool { - match *self { - PipelineState::None | PipelineState::Error - | PipelineState::Starting(_) | PipelineState::Handler(_) - | PipelineState::RunMiddlewares(_) | PipelineState::Response(_) => true, - PipelineState::Finishing(ref st) => st.info.context.is_none(), - PipelineState::Completed(_) => false, - } - } - - fn disconnect(&mut self) { - let info = match *self { - PipelineState::None | PipelineState::Error => return, - PipelineState::Starting(ref mut st) => &mut st.info, - PipelineState::Handler(ref mut st) => &mut st.info, - PipelineState::RunMiddlewares(ref mut st) => &mut st.info, - PipelineState::Response(ref mut st) => &mut st.info, - PipelineState::Finishing(ref mut st) => &mut st.info, - PipelineState::Completed(ref mut st) => &mut st.0, - }; - if let Some(ref mut context) = info.context { - context.disconnected(); - } - } - - fn error(&mut self) -> Option { - let info = match *self { - PipelineState::None | PipelineState::Error => return None, - PipelineState::Starting(ref mut st) => &mut st.info, - PipelineState::Handler(ref mut st) => &mut st.info, - PipelineState::RunMiddlewares(ref mut st) => &mut st.info, - PipelineState::Response(ref mut st) => &mut st.info, - PipelineState::Finishing(ref mut st) => &mut st.info, - PipelineState::Completed(ref mut st) => &mut st.0, - }; - info.error.take() - } -} - -struct PipelineInfo { - req: HttpRequest, +struct PipelineInfo { + req: HttpRequest, count: usize, - mws: Rc>>, - context: Option>, + mws: Rc>>>, + context: Option>, error: Option, + disconnected: Option, } -impl PipelineInfo { - fn new(req: HttpRequest) -> PipelineInfo { +impl PipelineInfo { + fn new(req: HttpRequest) -> PipelineInfo { PipelineInfo { req: req, count: 0, mws: Rc::new(Vec::new()), error: None, context: None, + disconnected: None, } } #[cfg_attr(feature = "cargo-clippy", allow(mut_from_ref))] - fn req_mut(&self) -> &mut HttpRequest { + fn req_mut(&self) -> &mut HttpRequest { #[allow(mutable_transmutes)] unsafe{mem::transmute(&self.req)} } @@ -109,145 +74,124 @@ impl PipelineInfo { } } -enum PipelineResponse { - None, - Context(Box), - Response(Box>), -} +impl> Pipeline { -/// Future that resolves when all buffered data get sent -#[doc(hidden)] -#[derive(Debug)] -pub struct DrainFut { - drained: bool, - task: Option, -} - -impl Default for DrainFut { - - fn default() -> DrainFut { - DrainFut { - drained: false, - task: None, - } - } -} - -impl DrainFut { - - fn set(&mut self) { - self.drained = true; - if let Some(task) = self.task.take() { - task.notify() - } - } -} - -impl Future for DrainFut { - type Item = (); - type Error = (); - - fn poll(&mut self) -> Poll<(), ()> { - if self.drained { - Ok(Async::Ready(())) - } else { - self.task = Some(current_task()); - Ok(Async::NotReady) - } - } -} - - -impl Pipeline { - - pub fn new(req: HttpRequest, - mw: Rc>>, - handler: PipelineHandler) -> Pipeline + pub fn new(req: HttpRequest, + mws: Rc>>>, + handler: Rc>) -> Pipeline { - Pipeline(StartMiddlewares::init(mw, req, handler)) + let mut info = PipelineInfo { + req: req, + count: 0, + mws: mws, + error: None, + context: None, + disconnected: None, + }; + let state = StartMiddlewares::init(&mut info, handler); + + Pipeline(info, state) + } +} + +impl Pipeline<(), Inner<()>> { + pub fn error>(err: R) -> Box { + Box::new(Pipeline::<(), Inner<()>>( + PipelineInfo::new(HttpRequest::default()), ProcessResponse::init(err.into()))) + } +} + +impl Pipeline { + + fn is_done(&self) -> bool { + match self.1 { + PipelineState::None | PipelineState::Error + | PipelineState::Starting(_) | PipelineState::Handler(_) + | PipelineState::RunMiddlewares(_) | PipelineState::Response(_) => true, + PipelineState::Finishing(_) => self.0.context.is_none(), + PipelineState::Completed(_) => false, + } + } +} + +impl> HttpHandlerTask for Pipeline { + + fn disconnected(&mut self) { + self.0.disconnected = Some(true); } - pub fn error>(err: R) -> Self { - Pipeline(ProcessResponse::init( - Box::new(PipelineInfo::new(HttpRequest::default())), err.into())) - } - - pub(crate) fn disconnected(&mut self) { - self.0.disconnect() - } - - pub(crate) fn poll_io(&mut self, io: &mut T) -> Poll { + fn poll_io(&mut self, io: &mut Writer) -> Poll { loop { - let state = mem::replace(&mut self.0, PipelineState::None); + let state = mem::replace(&mut self.1, PipelineState::None); match state { PipelineState::None => return Ok(Async::Ready(true)), PipelineState::Error => return Err(io::Error::new(io::ErrorKind::Other, "Internal error").into()), PipelineState::Starting(st) => { - match st.poll() { + match st.poll(&mut self.0) { Ok(state) => - self.0 = state, + self.1 = state, Err(state) => { - self.0 = state; + self.1 = state; return Ok(Async::NotReady) } } } PipelineState::Handler(st) => { - match st.poll() { + match st.poll(&mut self.0) { Ok(state) => - self.0 = state, + self.1 = state, Err(state) => { - self.0 = state; + self.1 = state; return Ok(Async::NotReady) } } } PipelineState::RunMiddlewares(st) => { - match st.poll() { + match st.poll(&mut self.0) { Ok(state) => - self.0 = state, + self.1 = state, Err(state) => { - self.0 = state; + self.1 = state; return Ok(Async::NotReady) } } } PipelineState::Response(st) => { - match st.poll_io(io) { + match st.poll_io(io, &mut self.0) { Ok(state) => { - self.0 = state; - if let Some(error) = self.0.error() { + self.1 = state; + if let Some(error) = self.0.error.take() { return Err(error) } else { - return Ok(Async::Ready(self.0.is_done())) + return Ok(Async::Ready(self.is_done())) } } Err(state) => { - self.0 = state; + self.1 = state; return Ok(Async::NotReady) } } } PipelineState::Finishing(st) => { - match st.poll() { + match st.poll(&mut self.0) { Ok(state) => - self.0 = state, + self.1 = state, Err(state) => { - self.0 = state; + self.1 = state; return Ok(Async::NotReady) } } } PipelineState::Completed(st) => { - match st.poll() { + match st.poll(&mut self.0) { Ok(state) => { - self.0 = state; + self.1 = state; return Ok(Async::Ready(true)); } Err(state) => { - self.0 = state; + self.1 = state; return Ok(Async::NotReady) } } @@ -256,65 +200,65 @@ impl Pipeline { } } - pub(crate) fn poll(&mut self) -> Poll<(), Error> { + fn poll(&mut self) -> Poll<(), Error> { loop { - let state = mem::replace(&mut self.0, PipelineState::None); + let state = mem::replace(&mut self.1, PipelineState::None); match state { PipelineState::None | PipelineState::Error => { return Ok(Async::Ready(())) } PipelineState::Starting(st) => { - match st.poll() { + match st.poll(&mut self.0) { Ok(state) => - self.0 = state, + self.1 = state, Err(state) => { - self.0 = state; + self.1 = state; return Ok(Async::NotReady) } } } PipelineState::Handler(st) => { - match st.poll() { + match st.poll(&mut self.0) { Ok(state) => - self.0 = state, + self.1 = state, Err(state) => { - self.0 = state; + self.1 = state; return Ok(Async::NotReady) } } } PipelineState::RunMiddlewares(st) => { - match st.poll() { + match st.poll(&mut self.0) { Ok(state) => - self.0 = state, + self.1 = state, Err(state) => { - self.0 = state; + self.1 = state; return Ok(Async::NotReady) } } } PipelineState::Response(_) => { - self.0 = state; + self.1 = state; return Ok(Async::NotReady); } PipelineState::Finishing(st) => { - match st.poll() { + match st.poll(&mut self.0) { Ok(state) => - self.0 = state, + self.1 = state, Err(state) => { - self.0 = state; + self.1 = state; return Ok(Async::NotReady) } } } PipelineState::Completed(st) => { - match st.poll() { + match st.poll(&mut self.0) { Ok(state) => { - self.0 = state; + self.1 = state; return Ok(Async::Ready(())); } Err(state) => { - self.0 = state; + self.1 = state; return Ok(Async::NotReady) } } @@ -327,185 +271,140 @@ impl Pipeline { type Fut = Box, Error=Error>>; /// Middlewares start executor -struct StartMiddlewares { - hnd: *mut Handler, +struct StartMiddlewares { + hnd: Rc>, fut: Option, - info: Box, + _s: PhantomData, } -impl StartMiddlewares { - - fn init(mws: Rc>>, - req: HttpRequest, handler: PipelineHandler) -> PipelineState { - let mut info = PipelineInfo { - req: req, - count: 0, - mws: mws, - error: None, - context: None, - }; +impl> StartMiddlewares { + fn init(info: &mut PipelineInfo, handler: Rc>) -> PipelineState + { // execute middlewares, we need this stage because middlewares could be non-async // and we can move to next state immidietly let len = info.mws.len(); loop { if info.count == len { - let reply = (&*handler)(info.req.clone()); - return WaitingResponse::init(Box::new(info), reply) + let reply = handler.borrow_mut().handle(info.req.clone()); + return WaitingResponse::init(info, reply) } else { match info.mws[info.count].start(&mut info.req) { Started::Done => info.count += 1, Started::Response(resp) => - return RunMiddlewares::init(Box::new(info), resp), + return RunMiddlewares::init(info, resp), Started::Future(mut fut) => match fut.poll() { Ok(Async::NotReady) => return PipelineState::Starting(StartMiddlewares { - hnd: handler as *const _ as *mut _, + hnd: handler, fut: Some(fut), - info: Box::new(info)}), + _s: PhantomData}), Ok(Async::Ready(resp)) => { if let Some(resp) = resp { - return RunMiddlewares::init(Box::new(info), resp); + return RunMiddlewares::init(info, resp); } info.count += 1; } Err(err) => - return ProcessResponse::init(Box::new(info), err.into()), + return ProcessResponse::init(err.into()), }, Started::Err(err) => - return ProcessResponse::init(Box::new(info), err.into()), + return ProcessResponse::init(err.into()), } } } } - fn poll(mut self) -> Result { - let len = self.info.mws.len(); + fn poll(mut self, info: &mut PipelineInfo) -> Result, PipelineState> + { + let len = info.mws.len(); 'outer: loop { match self.fut.as_mut().unwrap().poll() { Ok(Async::NotReady) => return Err(PipelineState::Starting(self)), Ok(Async::Ready(resp)) => { - self.info.count += 1; + info.count += 1; if let Some(resp) = resp { - return Ok(RunMiddlewares::init(self.info, resp)); + return Ok(RunMiddlewares::init(info, resp)); } - if self.info.count == len { - let reply = (unsafe{&*self.hnd})(self.info.req.clone()); - return Ok(WaitingResponse::init(self.info, reply)); + if info.count == len { + let reply = (*self.hnd.borrow_mut()).handle(info.req.clone()); + return Ok(WaitingResponse::init(info, reply)); } else { loop { - match self.info.mws[self.info.count].start(self.info.req_mut()) { + match info.mws[info.count].start(info.req_mut()) { Started::Done => - self.info.count += 1, + info.count += 1, Started::Response(resp) => { - return Ok(RunMiddlewares::init(self.info, resp)); + return Ok(RunMiddlewares::init(info, resp)); }, Started::Future(fut) => { self.fut = Some(fut); continue 'outer }, Started::Err(err) => - return Ok(ProcessResponse::init(self.info, err.into())) + return Ok(ProcessResponse::init(err.into())) } } } } Err(err) => - return Ok(ProcessResponse::init(self.info, err.into())) + return Ok(ProcessResponse::init(err.into())) } } } } // waiting for response -struct WaitingResponse { - info: Box, - stream: PipelineResponse, +struct WaitingResponse { + fut: Box>, + _s: PhantomData, + _h: PhantomData, } -impl WaitingResponse { +impl WaitingResponse { - fn init(info: Box, reply: Reply) -> PipelineState + #[inline] + fn init(info: &mut PipelineInfo, reply: Reply) -> PipelineState { - let stream = match reply.into() { + match reply.into() { ReplyItem::Message(resp) => - return RunMiddlewares::init(info, resp), - ReplyItem::Actor(ctx) => - PipelineResponse::Context(ctx), + RunMiddlewares::init(info, resp), ReplyItem::Future(fut) => - PipelineResponse::Response(fut), - }; - - PipelineState::Handler( - WaitingResponse { info: info, stream: stream }) + PipelineState::Handler( + WaitingResponse { fut: fut, _s: PhantomData, _h: PhantomData }), + } } - fn poll(mut self) -> Result { - let stream = mem::replace(&mut self.stream, PipelineResponse::None); - - match stream { - PipelineResponse::Context(mut context) => { - loop { - match context.poll() { - Ok(Async::Ready(Some(frame))) => { - match frame { - Frame::Message(resp) => { - self.info.context = Some(context); - return Ok(RunMiddlewares::init(self.info, resp)) - } - Frame::Payload(_) | Frame::Drain(_) => (), - } - }, - Ok(Async::Ready(None)) => { - error!("Unexpected eof"); - let err: Error = UnexpectedTaskFrame.into(); - return Ok(ProcessResponse::init(self.info, err.into())) - }, - Ok(Async::NotReady) => { - self.stream = PipelineResponse::Context(context); - return Err(PipelineState::Handler(self)) - }, - Err(err) => - return Ok(ProcessResponse::init(self.info, err.into())) - } - } - }, - PipelineResponse::Response(mut fut) => { - match fut.poll() { - Ok(Async::NotReady) => { - self.stream = PipelineResponse::Response(fut); - Err(PipelineState::Handler(self)) - } - Ok(Async::Ready(response)) => - Ok(RunMiddlewares::init(self.info, response)), - Err(err) => - Ok(ProcessResponse::init(self.info, err.into())), - } - } - PipelineResponse::None => { - unreachable!("Broken internal state") - } + fn poll(mut self, info: &mut PipelineInfo) -> Result, PipelineState> + { + match self.fut.poll() { + Ok(Async::NotReady) => + Err(PipelineState::Handler(self)), + Ok(Async::Ready(response)) => + Ok(RunMiddlewares::init(info, response)), + Err(err) => + Ok(ProcessResponse::init(err.into())), } - } } /// Middlewares response executor -pub(crate) struct RunMiddlewares { - info: Box, +struct RunMiddlewares { curr: usize, fut: Option>>, + _s: PhantomData, + _h: PhantomData, } -impl RunMiddlewares { +impl RunMiddlewares { - fn init(mut info: Box, mut resp: HttpResponse) -> PipelineState + fn init(info: &mut PipelineInfo, mut resp: HttpResponse) -> PipelineState { if info.count == 0 { - return ProcessResponse::init(info, resp); + return ProcessResponse::init(resp); } let mut curr = 0; let len = info.mws.len(); @@ -514,47 +413,50 @@ impl RunMiddlewares { resp = match info.mws[curr].response(info.req_mut(), resp) { Response::Err(err) => { info.count = curr + 1; - return ProcessResponse::init(info, err.into()) + return ProcessResponse::init(err.into()) } Response::Done(r) => { curr += 1; if curr == len { - return ProcessResponse::init(info, r) + return ProcessResponse::init(r) } else { r } }, Response::Future(fut) => { return PipelineState::RunMiddlewares( - RunMiddlewares { info: info, curr: curr, fut: Some(fut) }) + RunMiddlewares { curr: curr, fut: Some(fut), + _s: PhantomData, _h: PhantomData }) }, }; } } - fn poll(mut self) -> Result { - let len = self.info.mws.len(); + fn poll(mut self, info: &mut PipelineInfo) -> Result, PipelineState> + { + let len = info.mws.len(); loop { // poll latest fut let mut resp = match self.fut.as_mut().unwrap().poll() { - Ok(Async::NotReady) => - return Ok(PipelineState::RunMiddlewares(self)), + Ok(Async::NotReady) => { + return Err(PipelineState::RunMiddlewares(self)) + } Ok(Async::Ready(resp)) => { self.curr += 1; resp } Err(err) => - return Ok(ProcessResponse::init(self.info, err.into())), + return Ok(ProcessResponse::init(err.into())), }; loop { if self.curr == len { - return Ok(ProcessResponse::init(self.info, resp)); + return Ok(ProcessResponse::init(resp)); } else { - match self.info.mws[self.curr].response(self.info.req_mut(), resp) { + match info.mws[self.curr].response(info.req_mut(), resp) { Response::Err(err) => - return Ok(ProcessResponse::init(self.info, err.into())), + return Ok(ProcessResponse::init(err.into())), Response::Done(r) => { self.curr += 1; resp = r @@ -570,12 +472,13 @@ impl RunMiddlewares { } } -struct ProcessResponse { +struct ProcessResponse { resp: HttpResponse, iostate: IOState, running: RunningState, - drain: DrainVec, - info: Box, + drain: Option>, + _s: PhantomData, + _h: PhantomData, } #[derive(PartialEq)] @@ -603,60 +506,44 @@ impl RunningState { enum IOState { Response, Payload(BodyStream), - Context, + Actor(Box), Done, } -impl IOState { - fn is_done(&self) -> bool { - match *self { - IOState::Done => true, - _ => false - } - } -} +impl ProcessResponse { -struct DrainVec(Vec>>); -impl Drop for DrainVec { - fn drop(&mut self) { - for drain in &mut self.0 { - drain.borrow_mut().set() - } - } -} - -impl ProcessResponse { - - fn init(info: Box, resp: HttpResponse) -> PipelineState + #[inline] + fn init(resp: HttpResponse) -> PipelineState { PipelineState::Response( ProcessResponse{ resp: resp, iostate: IOState::Response, running: RunningState::Running, - drain: DrainVec(Vec::new()), - info: info}) + drain: None, + _s: PhantomData, _h: PhantomData}) } - fn poll_io(mut self, io: &mut T) -> Result { - if self.drain.0.is_empty() && self.running != RunningState::Paused { + fn poll_io(mut self, io: &mut Writer, info: &mut PipelineInfo) + -> Result, PipelineState> + { + if self.drain.is_none() && self.running != RunningState::Paused { // if task is paused, write buffer is probably full - loop { let result = match mem::replace(&mut self.iostate, IOState::Done) { IOState::Response => { - let result = match io.start(self.info.req_mut(), &mut self.resp) { + let result = match io.start(info.req_mut().get_inner(), &mut self.resp) { Ok(res) => res, Err(err) => { - self.info.error = Some(err.into()); - return Ok(FinishingMiddlewares::init(self.info, self.resp)) + info.error = Some(err.into()); + return Ok(FinishingMiddlewares::init(info, self.resp)) } }; match self.resp.replace_body(Body::Empty) { - Body::Streaming(stream) | Body::Upgrade(stream) => + Body::Streaming(stream) => self.iostate = IOState::Payload(stream), - Body::StreamingContext | Body::UpgradeContext => - self.iostate = IOState::Context, + Body::Actor(ctx) => + self.iostate = IOState::Actor(ctx), _ => (), } @@ -665,13 +552,13 @@ impl ProcessResponse { IOState::Payload(mut body) => { // always poll context if self.running == RunningState::Running { - match self.info.poll_context() { + match info.poll_context() { Ok(Async::NotReady) => (), Ok(Async::Ready(_)) => self.running = RunningState::Done, Err(err) => { - self.info.error = Some(err); - return Ok(FinishingMiddlewares::init(self.info, self.resp)) + info.error = Some(err); + return Ok(FinishingMiddlewares::init(info, self.resp)) } } } @@ -680,8 +567,8 @@ impl ProcessResponse { Ok(Async::Ready(None)) => { self.iostate = IOState::Done; if let Err(err) = io.write_eof() { - self.info.error = Some(err.into()); - return Ok(FinishingMiddlewares::init(self.info, self.resp)) + info.error = Some(err.into()); + return Ok(FinishingMiddlewares::init(info, self.resp)) } break }, @@ -689,9 +576,8 @@ impl ProcessResponse { self.iostate = IOState::Payload(body); match io.write(chunk.as_ref()) { Err(err) => { - self.info.error = Some(err.into()); - return Ok(FinishingMiddlewares::init( - self.info, self.resp)) + info.error = Some(err.into()); + return Ok(FinishingMiddlewares::init(info, self.resp)) }, Ok(result) => result } @@ -701,59 +587,57 @@ impl ProcessResponse { break }, Err(err) => { - self.info.error = Some(err); - return Ok(FinishingMiddlewares::init(self.info, self.resp)) + info.error = Some(err); + return Ok(FinishingMiddlewares::init(info, self.resp)) } } }, - IOState::Context => { - match self.info.context.as_mut().unwrap().poll() { + IOState::Actor(mut ctx) => { + if info.disconnected.take().is_some() { + ctx.disconnected(); + } + match ctx.poll() { Ok(Async::Ready(Some(frame))) => { match frame { - Frame::Message(msg) => { - error!("Unexpected message frame {:?}", msg); - self.info.error = Some(UnexpectedTaskFrame.into()); - return Ok( - FinishingMiddlewares::init(self.info, self.resp)) - }, Frame::Payload(None) => { + info.context = Some(ctx); self.iostate = IOState::Done; if let Err(err) = io.write_eof() { - self.info.error = Some(err.into()); + info.error = Some(err.into()); return Ok( - FinishingMiddlewares::init(self.info, self.resp)) + FinishingMiddlewares::init(info, self.resp)) } break }, Frame::Payload(Some(chunk)) => { - self.iostate = IOState::Context; + self.iostate = IOState::Actor(ctx); match io.write(chunk.as_ref()) { Err(err) => { - self.info.error = Some(err.into()); + info.error = Some(err.into()); return Ok(FinishingMiddlewares::init( - self.info, self.resp)) + info, self.resp)) }, Ok(result) => result } }, Frame::Drain(fut) => { - self.drain.0.push(fut); + self.drain = Some(fut); + self.iostate = IOState::Actor(ctx); break } } }, Ok(Async::Ready(None)) => { self.iostate = IOState::Done; - self.info.context.take(); break } Ok(Async::NotReady) => { - self.iostate = IOState::Context; + self.iostate = IOState::Actor(ctx); break } Err(err) => { - self.info.error = Some(err); - return Ok(FinishingMiddlewares::init(self.info, self.resp)) + info.error = Some(err); + return Ok(FinishingMiddlewares::init(info, self.resp)) } } } @@ -772,57 +656,71 @@ impl ProcessResponse { } } - // flush io - match io.poll_complete() { - Ok(Async::Ready(_)) => - self.running.resume(), - Ok(Async::NotReady) => - return Err(PipelineState::Response(self)), - Err(err) => { - debug!("Error sending data: {}", err); - self.info.error = Some(err.into()); - return Ok(FinishingMiddlewares::init(self.info, self.resp)) - } - } + // flush io but only if we need to + if self.running == RunningState::Paused || self.drain.is_some() { + match io.poll_completed(false) { + Ok(Async::Ready(_)) => { + self.running.resume(); - // drain futures - if !self.drain.0.is_empty() { - for fut in &mut self.drain.0 { - fut.borrow_mut().set() + // resolve drain futures + if let Some(tx) = self.drain.take() { + let _ = tx.send(()); + } + // restart io processing + return self.poll_io(io, info); + }, + Ok(Async::NotReady) => + return Err(PipelineState::Response(self)), + Err(err) => { + debug!("Error sending data: {}", err); + info.error = Some(err.into()); + return Ok(FinishingMiddlewares::init(info, self.resp)) + } } - self.drain.0.clear(); } // response is completed - if self.iostate.is_done() { - self.resp.set_response_size(io.written()); - Ok(FinishingMiddlewares::init(self.info, self.resp)) - } else { - Err(PipelineState::Response(self)) + match self.iostate { + IOState::Done => { + match io.write_eof() { + Ok(_) => (), + Err(err) => { + debug!("Error sending data: {}", err); + info.error = Some(err.into()); + return Ok(FinishingMiddlewares::init(info, self.resp)) + } + } + self.resp.set_response_size(io.written()); + Ok(FinishingMiddlewares::init(info, self.resp)) + } + _ => Err(PipelineState::Response(self)) } } } /// Middlewares start executor -struct FinishingMiddlewares { - info: Box, +struct FinishingMiddlewares { resp: HttpResponse, fut: Option>>, + _s: PhantomData, + _h: PhantomData, } -impl FinishingMiddlewares { +impl FinishingMiddlewares { - fn init(info: Box, resp: HttpResponse) -> PipelineState { + fn init(info: &mut PipelineInfo, resp: HttpResponse) -> PipelineState { if info.count == 0 { Completed::init(info) } else { - match (FinishingMiddlewares{info: info, resp: resp, fut: None}).poll() { + match (FinishingMiddlewares{resp: resp, fut: None, + _s: PhantomData, _h: PhantomData}).poll(info) { Ok(st) | Err(st) => st, } } } - fn poll(mut self) -> Result { + fn poll(mut self, info: &mut PipelineInfo) -> Result, PipelineState> + { loop { // poll latest fut let not_ready = if let Some(ref mut fut) = self.fut { @@ -845,12 +743,12 @@ impl FinishingMiddlewares { return Ok(PipelineState::Finishing(self)) } self.fut = None; - self.info.count -= 1; + info.count -= 1; - match self.info.mws[self.info.count].finish(self.info.req_mut(), &self.resp) { + match info.mws[info.count].finish(info.req_mut(), &self.resp) { Finished::Done => { - if self.info.count == 0 { - return Ok(Completed::init(self.info)) + if info.count == 0 { + return Ok(Completed::init(info)) } } Finished::Future(fut) => { @@ -861,22 +759,26 @@ impl FinishingMiddlewares { } } -struct Completed(Box); +struct Completed(PhantomData, PhantomData); -impl Completed { +impl Completed { - fn init(info: Box) -> PipelineState { + #[inline] + fn init(info: &mut PipelineInfo) -> PipelineState { if info.context.is_none() { PipelineState::None } else { - PipelineState::Completed(Completed(info)) + PipelineState::Completed(Completed(PhantomData, PhantomData)) } } - fn poll(mut self) -> Result { - match self.0.poll_context() { - Ok(Async::NotReady) => Ok(PipelineState::Completed(self)), - Ok(Async::Ready(())) => Ok(PipelineState::None), + #[inline] + fn poll(self, info: &mut PipelineInfo) -> Result, PipelineState> { + match info.poll_context() { + Ok(Async::NotReady) => + Ok(PipelineState::Completed(Completed(PhantomData, PhantomData))), + Ok(Async::Ready(())) => + Ok(PipelineState::None), Err(_) => Ok(PipelineState::Error), } } @@ -890,11 +792,11 @@ mod tests { use tokio_core::reactor::Core; use futures::future::{lazy, result}; - impl PipelineState { + impl PipelineState { fn is_none(&self) -> Option { if let PipelineState::None = *self { Some(true) } else { None } } - fn completed(self) -> Option { + fn completed(self) -> Option> { if let PipelineState::Completed(c) = self { Some(c) } else { None } } } @@ -907,23 +809,25 @@ mod tests { #[test] fn test_completed() { Core::new().unwrap().run(lazy(|| { - let info = Box::new(PipelineInfo::new(HttpRequest::default())); - Completed::init(info).is_none().unwrap(); + let mut info = PipelineInfo::new(HttpRequest::default()); + Completed::<(), Inner<()>>::init(&mut info).is_none().unwrap(); let req = HttpRequest::default(); let mut ctx = HttpContext::new(req.clone(), MyActor); let addr: Address<_> = ctx.address(); - let mut info = Box::new(PipelineInfo::new(req)); + let mut info = PipelineInfo::new(req); info.context = Some(Box::new(ctx)); - let mut state = Completed::init(info).completed().unwrap(); + let mut state = Completed::<(), Inner<()>>::init(&mut info).completed().unwrap(); - let st = state.poll().ok().unwrap(); - assert!(!st.is_done()); + let st = state.poll(&mut info).ok().unwrap(); + let pp = Pipeline(info, st); + assert!(!pp.is_done()); + let Pipeline(mut info, st) = pp; state = st.completed().unwrap(); drop(addr); - state.poll().ok().unwrap().is_none().unwrap(); + state.poll(&mut info).ok().unwrap().is_none().unwrap(); result(Ok::<_, ()>(())) })).unwrap() diff --git a/src/pred.rs b/src/pred.rs new file mode 100644 index 000000000..47d906fb0 --- /dev/null +++ b/src/pred.rs @@ -0,0 +1,296 @@ +//! Route match predicates +#![allow(non_snake_case)] +use std::marker::PhantomData; +use http; +use http::{header, HttpTryFrom}; +use httprequest::HttpRequest; + +/// Trait defines resource route predicate. +/// Predicate can modify request object. It is also possible to +/// to store extra attributes on request by using `.extensions()` method. +pub trait Predicate { + + /// Check if request matches predicate + fn check(&self, &mut HttpRequest) -> bool; + +} + +/// Return predicate that matches if any of supplied predicate matches. +/// +/// ```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::Any(pred::Get()).or(pred::Post())) +/// .h(HTTPMethodNotAllowed)); +/// } +/// ``` +pub fn Any + 'static>(pred: P) -> AnyPredicate +{ + AnyPredicate(vec![Box::new(pred)]) +} + +/// Matches if any of supplied predicate matches. +pub struct AnyPredicate(Vec>>); + +impl AnyPredicate { + /// Add new predicate to list of predicates to check + pub fn or + 'static>(mut self, pred: P) -> Self { + self.0.push(Box::new(pred)); + self + } +} + +impl Predicate for AnyPredicate { + fn check(&self, req: &mut HttpRequest) -> bool { + for p in &self.0 { + if p.check(req) { + return true + } + } + false + } +} + +/// Return predicate that matches if all of supplied predicate matches. +/// +/// ```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::All(pred::Get()) +/// .and(pred::Header("content-type", "plain/text"))) +/// .h(HTTPMethodNotAllowed)); +/// } +/// ``` +pub fn All + 'static>(pred: P) -> AllPredicate { + AllPredicate(vec![Box::new(pred)]) +} + +/// Matches if all of supplied predicate matches. +pub struct AllPredicate(Vec>>); + +impl AllPredicate { + /// Add new predicate to list of predicates to check + pub fn and + 'static>(mut self, pred: P) -> Self { + self.0.push(Box::new(pred)); + self + } +} + +impl Predicate for AllPredicate { + fn check(&self, req: &mut HttpRequest) -> bool { + for p in &self.0 { + if !p.check(req) { + return false + } + } + true + } +} + +/// Return predicate that matches if supplied predicate does not match. +pub fn Not + 'static>(pred: P) -> NotPredicate +{ + NotPredicate(Box::new(pred)) +} + +#[doc(hidden)] +pub struct NotPredicate(Box>); + +impl Predicate for NotPredicate { + fn check(&self, req: &mut HttpRequest) -> bool { + !self.0.check(req) + } +} + +/// Http method predicate +#[doc(hidden)] +pub struct MethodPredicate(http::Method, PhantomData); + +impl Predicate for MethodPredicate { + fn check(&self, req: &mut HttpRequest) -> bool { + *req.method() == self.0 + } +} + +/// Predicate to match *GET* http method +pub fn Get() -> MethodPredicate { + MethodPredicate(http::Method::GET, PhantomData) +} + +/// Predicate to match *POST* http method +pub fn Post() -> MethodPredicate { + MethodPredicate(http::Method::POST, PhantomData) +} + +/// Predicate to match *PUT* http method +pub fn Put() -> MethodPredicate { + MethodPredicate(http::Method::PUT, PhantomData) +} + +/// Predicate to match *DELETE* http method +pub fn Delete() -> MethodPredicate { + MethodPredicate(http::Method::DELETE, PhantomData) +} + +/// Predicate to match *HEAD* http method +pub fn Head() -> MethodPredicate { + MethodPredicate(http::Method::HEAD, PhantomData) +} + +/// Predicate to match *OPTIONS* http method +pub fn Options() -> MethodPredicate { + MethodPredicate(http::Method::OPTIONS, PhantomData) +} + +/// Predicate to match *CONNECT* http method +pub fn Connect() -> MethodPredicate { + MethodPredicate(http::Method::CONNECT, PhantomData) +} + +/// Predicate to match *PATCH* http method +pub fn Patch() -> MethodPredicate { + MethodPredicate(http::Method::PATCH, PhantomData) +} + +/// Predicate to match *TRACE* http method +pub fn Trace() -> MethodPredicate { + MethodPredicate(http::Method::TRACE, PhantomData) +} + +/// Predicate to match specified http method +pub fn Method(method: http::Method) -> MethodPredicate { + MethodPredicate(method, PhantomData) +} + +/// Return predicate that matches if request contains specified header and value. +pub fn Header(name: &'static str, value: &'static str) -> HeaderPredicate +{ + HeaderPredicate(header::HeaderName::try_from(name).unwrap(), + header::HeaderValue::from_static(value), + PhantomData) +} + +#[doc(hidden)] +pub struct HeaderPredicate(header::HeaderName, header::HeaderValue, PhantomData); + +impl Predicate for HeaderPredicate { + fn check(&self, req: &mut HttpRequest) -> bool { + if let Some(val) = req.headers().get(&self.0) { + return val == self.1 + } + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + use http::{Uri, Version, Method}; + use http::header::{self, HeaderMap}; + + #[test] + fn test_header() { + let mut headers = HeaderMap::new(); + headers.insert(header::TRANSFER_ENCODING, + header::HeaderValue::from_static("chunked")); + let mut req = HttpRequest::new( + Method::GET, Uri::from_str("/").unwrap(), Version::HTTP_11, headers, None); + + let pred = Header("transfer-encoding", "chunked"); + assert!(pred.check(&mut req)); + + let pred = Header("transfer-encoding", "other"); + assert!(!pred.check(&mut req)); + + let pred = Header("content-type", "other"); + assert!(!pred.check(&mut req)); + } + + #[test] + fn test_methods() { + let mut req = HttpRequest::new( + Method::GET, Uri::from_str("/").unwrap(), + Version::HTTP_11, HeaderMap::new(), None); + let mut req2 = HttpRequest::new( + Method::POST, Uri::from_str("/").unwrap(), + Version::HTTP_11, HeaderMap::new(), None); + + assert!(Get().check(&mut req)); + assert!(!Get().check(&mut req2)); + assert!(Post().check(&mut req2)); + assert!(!Post().check(&mut req)); + + let mut r = HttpRequest::new( + Method::PUT, Uri::from_str("/").unwrap(), + Version::HTTP_11, HeaderMap::new(), None); + assert!(Put().check(&mut r)); + assert!(!Put().check(&mut req)); + + let mut r = HttpRequest::new( + Method::DELETE, Uri::from_str("/").unwrap(), + Version::HTTP_11, HeaderMap::new(), None); + assert!(Delete().check(&mut r)); + assert!(!Delete().check(&mut req)); + + let mut r = HttpRequest::new( + Method::HEAD, Uri::from_str("/").unwrap(), + Version::HTTP_11, HeaderMap::new(), None); + assert!(Head().check(&mut r)); + assert!(!Head().check(&mut req)); + + let mut r = HttpRequest::new( + Method::OPTIONS, Uri::from_str("/").unwrap(), + Version::HTTP_11, HeaderMap::new(), None); + assert!(Options().check(&mut r)); + assert!(!Options().check(&mut req)); + + let mut r = HttpRequest::new( + Method::CONNECT, Uri::from_str("/").unwrap(), + Version::HTTP_11, HeaderMap::new(), None); + assert!(Connect().check(&mut r)); + assert!(!Connect().check(&mut req)); + + let mut r = HttpRequest::new( + Method::PATCH, Uri::from_str("/").unwrap(), + Version::HTTP_11, HeaderMap::new(), None); + assert!(Patch().check(&mut r)); + assert!(!Patch().check(&mut req)); + + let mut r = HttpRequest::new( + Method::TRACE, Uri::from_str("/").unwrap(), + Version::HTTP_11, HeaderMap::new(), None); + assert!(Trace().check(&mut r)); + assert!(!Trace().check(&mut req)); + } + + #[test] + fn test_preds() { + let mut r = HttpRequest::new( + Method::TRACE, Uri::from_str("/").unwrap(), + Version::HTTP_11, HeaderMap::new(), None); + + assert!(Not(Get()).check(&mut r)); + assert!(!Not(Trace()).check(&mut r)); + + assert!(All(Trace()).and(Trace()).check(&mut r)); + assert!(!All(Get()).and(Trace()).check(&mut r)); + + assert!(Any(Get()).or(Trace()).check(&mut r)); + assert!(!Any(Get()).or(Get()).check(&mut r)); + } +} diff --git a/src/recognizer.rs b/src/recognizer.rs deleted file mode 100644 index 17366bdb8..000000000 --- a/src/recognizer.rs +++ /dev/null @@ -1,351 +0,0 @@ -use std::rc::Rc; -use std::collections::HashMap; - -use regex::{Regex, RegexSet, Captures}; - - -#[doc(hidden)] -pub struct RouteRecognizer { - prefix: usize, - patterns: RegexSet, - routes: Vec<(Pattern, T)>, -} - -impl Default for RouteRecognizer { - - fn default() -> Self { - RouteRecognizer { - prefix: 0, - patterns: RegexSet::new([""].iter()).unwrap(), - routes: Vec::new(), - } - } -} - -impl RouteRecognizer { - - pub fn new, U>(prefix: P, routes: U) -> Self - where U: IntoIterator - { - let mut paths = Vec::new(); - let mut handlers = Vec::new(); - for item in routes { - let pat = parse(&item.0); - handlers.push((Pattern::new(&pat), item.1)); - paths.push(pat); - }; - let regset = RegexSet::new(&paths); - - RouteRecognizer { - prefix: prefix.into().len() - 1, - patterns: regset.unwrap(), - routes: handlers, - } - } - - pub fn set_routes(&mut self, routes: Vec<(&str, T)>) { - let mut paths = Vec::new(); - let mut handlers = Vec::new(); - for item in routes { - let pat = parse(item.0); - handlers.push((Pattern::new(&pat), item.1)); - paths.push(pat); - }; - self.patterns = RegexSet::new(&paths).unwrap(); - self.routes = handlers; - } - - pub fn set_prefix>(&mut self, prefix: P) { - let p = prefix.into(); - if p.ends_with('/') { - self.prefix = p.len() - 1; - } else { - self.prefix = p.len(); - } - } - - pub fn recognize(&self, path: &str) -> Option<(Option, &T)> { - let p = &path[self.prefix..]; - if p.is_empty() { - if let Some(idx) = self.patterns.matches("/").into_iter().next() { - let (ref pattern, ref route) = self.routes[idx]; - return Some((pattern.match_info(&path[self.prefix..]), route)) - } - } else if let Some(idx) = self.patterns.matches(p).into_iter().next() { - let (ref pattern, ref route) = self.routes[idx]; - return Some((pattern.match_info(&path[self.prefix..]), route)) - } - None - } -} - -struct Pattern { - re: Regex, - names: Rc>, -} - -impl Pattern { - fn new(pattern: &str) -> Self { - let re = Regex::new(pattern).unwrap(); - let names = re.capture_names() - .enumerate() - .filter_map(|(i, name)| name.map(|name| (name.to_owned(), i))) - .collect(); - - Pattern { - re, - names: Rc::new(names), - } - } - - fn match_info(&self, text: &str) -> Option { - let captures = match self.re.captures(text) { - Some(captures) => captures, - None => return None, - }; - - Some(Params::new(Rc::clone(&self.names), text, &captures)) - } -} - -pub(crate) fn check_pattern(path: &str) { - if let Err(err) = Regex::new(&parse(path)) { - panic!("Wrong path pattern: \"{}\" {}", path, err); - } -} - -fn parse(pattern: &str) -> String { - const DEFAULT_PATTERN: &str = "[^/]+"; - - let mut hard_stop = false; - let mut re = String::from("^/"); - let mut in_param = false; - let mut in_param_pattern = false; - let mut param_name = String::new(); - let mut param_pattern = String::from(DEFAULT_PATTERN); - - for (index, ch) in pattern.chars().enumerate() { - // All routes must have a leading slash so its optional to have one - if index == 0 && ch == '/' { - continue; - } - - if hard_stop { - panic!("Tail '*' section has to be last lection of pattern"); - } - - if in_param { - // In parameter segment: `{....}` - if ch == '}' { - if param_pattern == "*" { - hard_stop = true; - re.push_str( - &format!(r"(?P<{}>[%/[:word:][:punct:][:space:]]+)", ¶m_name)); - } else { - re.push_str(&format!(r"(?P<{}>{})", ¶m_name, ¶m_pattern)); - } - - param_name.clear(); - param_pattern = String::from(DEFAULT_PATTERN); - - in_param_pattern = false; - in_param = false; - } else if ch == ':' { - // The parameter name has been determined; custom pattern land - in_param_pattern = true; - param_pattern.clear(); - } else if in_param_pattern { - // Ignore leading whitespace for pattern - if !(ch == ' ' && param_pattern.is_empty()) { - param_pattern.push(ch); - } - } else { - param_name.push(ch); - } - } else if ch == '{' { - in_param = true; - } else { - re.push(ch); - } - } - - re.push('$'); - re -} - -/// Route match information -/// -/// If resource path contains variable patterns, `Params` stores this variables. -#[derive(Debug)] -pub struct Params { - text: String, - matches: Vec>, - names: Rc>, -} - -impl Params { - pub(crate) fn new(names: Rc>, - text: &str, - captures: &Captures) -> Self - { - Params { - names, - text: text.into(), - matches: captures - .iter() - .map(|capture| capture.map(|m| (m.start(), m.end()))) - .collect(), - } - } - - pub(crate) fn empty() -> Self - { - Params { - text: String::new(), - names: Rc::new(HashMap::new()), - matches: Vec::new(), - } - } - - pub fn is_empty(&self) -> bool { - self.names.is_empty() - } - - fn by_idx(&self, index: usize) -> Option<&str> { - self.matches - .get(index + 1) - .and_then(|m| m.map(|(start, end)| &self.text[start..end])) - } - - /// Get matched parameter by name - pub fn get(&self, key: &str) -> Option<&str> { - self.names.get(key).and_then(|&i| self.by_idx(i - 1)) - } -} - -#[cfg(test)] -mod tests { - use regex::Regex; - use super::*; - - #[test] - fn test_recognizer() { - let mut rec = RouteRecognizer::::default(); - - let routes = vec![ - ("/name", 1), - ("/name/{val}", 2), - ("/name/{val}/index.html", 3), - ("/v{val}/{val2}/index.html", 4), - ("/v/{tail:*}", 5), - ]; - rec.set_routes(routes); - - let (params, val) = rec.recognize("/name").unwrap(); - assert_eq!(*val, 1); - assert!(params.unwrap().is_empty()); - - let (params, val) = rec.recognize("/name/value").unwrap(); - assert_eq!(*val, 2); - assert!(!params.as_ref().unwrap().is_empty()); - assert_eq!(params.as_ref().unwrap().get("val").unwrap(), "value"); - - let (params, val) = rec.recognize("/name/value2/index.html").unwrap(); - assert_eq!(*val, 3); - assert!(!params.as_ref().unwrap().is_empty()); - assert_eq!(params.as_ref().unwrap().get("val").unwrap(), "value2"); - assert_eq!(params.as_ref().unwrap().by_idx(0).unwrap(), "value2"); - - let (params, val) = rec.recognize("/vtest/ttt/index.html").unwrap(); - assert_eq!(*val, 4); - assert!(!params.as_ref().unwrap().is_empty()); - assert_eq!(params.as_ref().unwrap().get("val").unwrap(), "test"); - assert_eq!(params.as_ref().unwrap().get("val2").unwrap(), "ttt"); - assert_eq!(params.as_ref().unwrap().by_idx(0).unwrap(), "test"); - assert_eq!(params.as_ref().unwrap().by_idx(1).unwrap(), "ttt"); - - let (params, val) = rec.recognize("/v/blah-blah/index.html").unwrap(); - assert_eq!(*val, 5); - assert!(!params.as_ref().unwrap().is_empty()); - assert_eq!(params.as_ref().unwrap().get("tail").unwrap(), "blah-blah/index.html"); - } - - fn assert_parse(pattern: &str, expected_re: &str) -> Regex { - let re_str = parse(pattern); - assert_eq!(&*re_str, expected_re); - Regex::new(&re_str).unwrap() - } - - #[test] - fn test_parse_static() { - let re = assert_parse("/", r"^/$"); - assert!(re.is_match("/")); - assert!(!re.is_match("/a")); - - let re = assert_parse("/name", r"^/name$"); - assert!(re.is_match("/name")); - assert!(!re.is_match("/name1")); - assert!(!re.is_match("/name/")); - assert!(!re.is_match("/name~")); - - let re = assert_parse("/name/", r"^/name/$"); - assert!(re.is_match("/name/")); - assert!(!re.is_match("/name")); - assert!(!re.is_match("/name/gs")); - - let re = assert_parse("/user/profile", r"^/user/profile$"); - assert!(re.is_match("/user/profile")); - assert!(!re.is_match("/user/profile/profile")); - } - - #[test] - fn test_parse_param() { - let re = assert_parse("/user/{id}", r"^/user/(?P[^/]+)$"); - assert!(re.is_match("/user/profile")); - assert!(re.is_match("/user/2345")); - assert!(!re.is_match("/user/2345/")); - assert!(!re.is_match("/user/2345/sdg")); - - let captures = re.captures("/user/profile").unwrap(); - assert_eq!(captures.get(1).unwrap().as_str(), "profile"); - assert_eq!(captures.name("id").unwrap().as_str(), "profile"); - - let captures = re.captures("/user/1245125").unwrap(); - assert_eq!(captures.get(1).unwrap().as_str(), "1245125"); - assert_eq!(captures.name("id").unwrap().as_str(), "1245125"); - - let re = assert_parse( - "/v{version}/resource/{id}", - r"^/v(?P[^/]+)/resource/(?P[^/]+)$", - ); - assert!(re.is_match("/v1/resource/320120")); - assert!(!re.is_match("/v/resource/1")); - assert!(!re.is_match("/resource")); - - let captures = re.captures("/v151/resource/adahg32").unwrap(); - assert_eq!(captures.get(1).unwrap().as_str(), "151"); - assert_eq!(captures.name("version").unwrap().as_str(), "151"); - assert_eq!(captures.name("id").unwrap().as_str(), "adahg32"); - } - - #[test] - fn test_tail_param() { - let re = assert_parse("/user/{tail:*}", - r"^/user/(?P[%/[:word:][:punct:][:space:]]+)$"); - assert!(re.is_match("/user/profile")); - assert!(re.is_match("/user/2345")); - assert!(re.is_match("/user/2345/")); - assert!(re.is_match("/user/2345/sdg")); - assert!(re.is_match("/user/2345/sd-_g/")); - assert!(re.is_match("/user/2345/sdg/asddsasd/index.html")); - - let re = assert_parse("/user/v{tail:*}", - r"^/user/v(?P[%/[:word:][:punct:][:space:]]+)$"); - assert!(!re.is_match("/user/2345/")); - assert!(re.is_match("/user/vprofile")); - assert!(re.is_match("/user/v_2345")); - assert!(re.is_match("/user/v2345/sdg")); - assert!(re.is_match("/user/v2345/sd-_g/test.html")); - assert!(re.is_match("/user/v/sdg/asddsasd/index.html")); - } -} diff --git a/src/resource.rs b/src/resource.rs index 6e82b60ae..ee6d682e5 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -1,35 +1,38 @@ use std::marker::PhantomData; -use std::collections::HashMap; -use http::Method; -use futures::Future; +use http::{Method, StatusCode}; -use error::Error; -use route::{Reply, Handler, RouteHandler, AsyncHandler, WrapHandler}; +use pred; +use body::Body; +use route::Route; +use handler::{Reply, Handler, Responder}; use httprequest::HttpRequest; use httpresponse::HttpResponse; -use httpcodes::{HTTPNotFound, HTTPMethodNotAllowed}; -/// Http resource -/// -/// `Resource` is an entry in route table which corresponds to requested URL. +/// *Resource* is an entry in route table which corresponds to requested URL. /// /// Resource in turn has at least one route. -/// Route corresponds to handling HTTP method by calling route handler. +/// Route consists of an object that implements `Handler` trait (handler) +/// and list of predicates (objects that implement `Predicate` trait). +/// Route uses builder-like pattern for configuration. +/// During request handling, resource object iterate through all routes +/// and check all predicates for specific route, if request matches all predicates route +/// route considired matched and route handler get called. /// /// ```rust -/// extern crate actix_web; +/// # extern crate actix_web; +/// use actix_web::*; /// /// fn main() { -/// let app = actix_web::Application::default("/") -/// .resource("/", |r| r.get(|_| actix_web::HttpResponse::Ok())) +/// let app = Application::new() +/// .resource( +/// "/", |r| r.method(Method::GET).f(|r| HttpResponse::Ok())) /// .finish(); /// } pub struct Resource { name: String, state: PhantomData, - routes: HashMap>>, - default: Box>, + routes: Vec>, } impl Default for Resource { @@ -37,85 +40,104 @@ impl Default for Resource { Resource { name: String::new(), state: PhantomData, - routes: HashMap::new(), - default: Box::new(HTTPMethodNotAllowed)} + routes: Vec::new() } } } -impl Resource where S: 'static { +impl Resource { pub(crate) fn default_not_found() -> Self { Resource { name: String::new(), state: PhantomData, - routes: HashMap::new(), - default: Box::new(HTTPNotFound)} + routes: Vec::new() } } /// Set resource name - pub fn set_name>(&mut self, name: T) { + pub fn name>(&mut self, name: T) { self.name = name.into(); } - /// Register handler for specified method. - pub fn handler(&mut self, method: Method, handler: F) - where F: Fn(HttpRequest) -> R + 'static, - R: Into + 'static, - { - self.routes.insert(method, Box::new(WrapHandler::new(handler))); - } - - /// Register async handler for specified method. - pub fn async(&mut self, method: Method, handler: F) - where F: Fn(HttpRequest) -> R + 'static, - R: Future + 'static, - { - self.routes.insert(method, Box::new(AsyncHandler::new(handler))); - } - - /// Default handler is used if no matched route found. - /// By default `HTTPMethodNotAllowed` is used. - pub fn default_handler(&mut self, handler: H) where H: Handler - { - self.default = Box::new(WrapHandler::new(handler)); - } - - /// Register handler for `GET` method. - pub fn get(&mut self, handler: F) - where F: Fn(HttpRequest) -> R + 'static, - R: Into + 'static, { - self.routes.insert(Method::GET, Box::new(WrapHandler::new(handler))); - } - - /// Register handler for `POST` method. - pub fn post(&mut self, handler: F) - where F: Fn(HttpRequest) -> R + 'static, - R: Into + 'static, { - self.routes.insert(Method::POST, Box::new(WrapHandler::new(handler))); - } - - /// Register handler for `PUT` method. - pub fn put(&mut self, handler: F) - where F: Fn(HttpRequest) -> R + 'static, - R: Into + 'static, { - self.routes.insert(Method::PUT, Box::new(WrapHandler::new(handler))); - } - - /// Register handler for `DELETE` method. - pub fn delete(&mut self, handler: F) - where F: Fn(HttpRequest) -> R + 'static, - R: Into + 'static, { - self.routes.insert(Method::DELETE, Box::new(WrapHandler::new(handler))); + pub(crate) fn get_name(&self) -> &str { + &self.name } } -impl RouteHandler for Resource { +impl Resource { - fn handle(&self, req: HttpRequest) -> Reply { - if let Some(handler) = self.routes.get(req.method()) { - handler.handle(req) + /// Register a new route and return mutable reference to *Route* object. + /// *Route* is used for route configuration, i.e. adding predicates, setting up handler. + /// + /// ```rust + /// # extern crate actix_web; + /// use actix_web::*; + /// + /// fn main() { + /// let app = Application::new() + /// .resource( + /// "/", |r| r.route() + /// .p(pred::Any(pred::Get()).or(pred::Put())) + /// .p(pred::Header("Content-Type", "text/plain")) + /// .f(|r| HttpResponse::Ok())) + /// .finish(); + /// } + /// ``` + pub fn route(&mut self) -> &mut Route { + self.routes.push(Route::default()); + self.routes.last_mut().unwrap() + } + + /// Register a new route and add method check to route. + /// + /// This is shortcut for: + /// + /// ```rust,ignore + /// Resource::resource("/", |r| r.route().p(pred::Get()).f(index) + /// ``` + pub fn method(&mut self, method: Method) -> &mut Route { + self.routes.push(Route::default()); + self.routes.last_mut().unwrap().p(pred::Method(method)) + } + + /// Register a new route and add handler object. + /// + /// This is shortcut for: + /// + /// ```rust,ignore + /// Resource::resource("/", |r| r.route().h(handler) + /// ``` + pub fn h>(&mut self, handler: H) { + self.routes.push(Route::default()); + self.routes.last_mut().unwrap().h(handler) + } + + /// Register a new route and add handler function. + /// + /// This is shortcut for: + /// + /// ```rust,ignore + /// Resource::resource("/", |r| r.route().f(index) + /// ``` + pub fn f(&mut self, handler: F) + where F: Fn(HttpRequest) -> R + 'static, + R: Responder + 'static, + { + self.routes.push(Route::default()); + self.routes.last_mut().unwrap().f(handler) + } + + pub(crate) fn handle(&mut self, mut req: HttpRequest, default: Option<&mut Resource>) + -> Reply + { + for route in &mut self.routes { + if route.check(&mut req) { + return route.handle(req) + } + } + if let Some(resource) = default { + resource.handle(req, None) } else { - self.default.handle(req) + Reply::response(HttpResponse::new(StatusCode::NOT_FOUND, Body::Empty)) } } } diff --git a/src/route.rs b/src/route.rs index 1550751c2..64b60603d 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,184 +1,89 @@ -use std::marker::PhantomData; -use std::result::Result as StdResult; - -use actix::Actor; use futures::Future; use error::Error; -use context::{HttpContext, IoContext}; +use pred::Predicate; +use handler::{Reply, Handler, Responder, RouteHandler, AsyncHandler, WrapHandler}; +use httpcodes::HTTPNotFound; use httprequest::HttpRequest; -use httpresponse::HttpResponse; -/// Trait defines object that could be regestered as route handler -#[allow(unused_variables)] -pub trait Handler: 'static { - - /// The type of value that handler will return. - type Result: Into; - - /// Handle request - fn handle(&self, req: HttpRequest) -> Self::Result; +/// Resource route definition +/// +/// Route uses builder-like pattern for configuration. +/// If handler is not explicitly set, default *404 Not Found* handler is used. +pub struct Route { + preds: Vec>>, + handler: Box>, } -/// Handler for Fn() -impl Handler for F - where F: Fn(HttpRequest) -> R + 'static, - R: Into + 'static -{ - type Result = R; +impl Default for Route { - fn handle(&self, req: HttpRequest) -> R { - (self)(req) - } -} - -/// Represents response process. -pub struct Reply(ReplyItem); - -pub(crate) enum ReplyItem { - Message(HttpResponse), - Actor(Box), - Future(Box>), -} - -impl Reply { - - /// Create actor response - pub fn actor(ctx: HttpContext) -> Reply - where A: Actor>, S: 'static - { - Reply(ReplyItem::Actor(Box::new(ctx))) - } - - /// Create async response - pub fn async(fut: F) -> Reply - where F: Future + 'static - { - Reply(ReplyItem::Future(Box::new(fut))) - } - - /// Send response - pub fn response>(response: R) -> Reply { - Reply(ReplyItem::Message(response.into())) - } - - pub(crate) fn into(self) -> ReplyItem { - self.0 - } -} - -#[cfg(not(actix_nightly))] -impl> From for Reply -{ - fn from(item: T) -> Self { - Reply(ReplyItem::Message(item.into())) - } -} - -#[cfg(actix_nightly)] -default impl> From for Reply -{ - fn from(item: T) -> Self { - Reply(ReplyItem::Message(item.into())) - } -} - -#[cfg(actix_nightly)] -default impl, E: Into> From> for Reply { - fn from(res: StdResult) -> Self { - match res { - Ok(val) => val.into().into(), - Err(err) => err.into().into(), + fn default() -> Route { + Route { + preds: Vec::new(), + handler: Box::new(WrapHandler::new(|_| HTTPNotFound)), } } } -impl> From> for Reply { - fn from(res: StdResult) -> Self { - match res { - Ok(val) => val, - Err(err) => err.into().into(), +impl Route { + + pub(crate) fn check(&self, req: &mut HttpRequest) -> bool { + for pred in &self.preds { + if !pred.check(req) { + return false + } } - } -} - -impl>, S: 'static> From> for Reply -{ - fn from(item: HttpContext) -> Self { - Reply(ReplyItem::Actor(Box::new(item))) - } -} - -impl From>> for Reply -{ - fn from(item: Box>) -> Self { - Reply(ReplyItem::Future(item)) - } -} - -/// Trait defines object that could be regestered as resource route -pub(crate) trait RouteHandler: 'static { - fn handle(&self, req: HttpRequest) -> Reply; -} - -/// Route handler wrapper for Handler -pub(crate) -struct WrapHandler - where H: Handler, - R: Into, - S: 'static, -{ - h: H, - s: PhantomData, -} - -impl WrapHandler - where H: Handler, - R: Into, - S: 'static, -{ - pub fn new(h: H) -> Self { - WrapHandler{h: h, s: PhantomData} - } -} - -impl RouteHandler for WrapHandler - where H: Handler, - R: Into + 'static, - S: 'static, -{ - fn handle(&self, req: HttpRequest) -> Reply { - self.h.handle(req).into() - } -} - -/// Async route handler -pub(crate) -struct AsyncHandler - where F: Fn(HttpRequest) -> R + 'static, - R: Future + 'static, - S: 'static, -{ - f: Box, - s: PhantomData, -} - -impl AsyncHandler - where F: Fn(HttpRequest) -> R + 'static, - R: Future + 'static, - S: 'static, -{ - pub fn new(f: F) -> Self { - AsyncHandler{f: Box::new(f), s: PhantomData} - } -} - -impl RouteHandler for AsyncHandler - where F: Fn(HttpRequest) -> R + 'static, - R: Future + 'static, - S: 'static, -{ - fn handle(&self, req: HttpRequest) -> Reply { - Reply::async((self.f)(req)) + true + } + + pub(crate) fn handle(&mut self, req: HttpRequest) -> Reply { + self.handler.handle(req) + } + + /// Add match predicate to route. + /// + /// ```rust + /// # extern crate actix_web; + /// # use actix_web::*; + /// # use actix_web::httpcodes::*; + /// # fn main() { + /// Application::new() + /// .resource("/path", |r| + /// r.route() + /// .p(pred::Get()) + /// .p(pred::Header("content-type", "text/plain")) + /// .f(|req| HTTPOk) + /// ) + /// # .finish(); + /// # } + /// ``` + pub fn p + 'static>(&mut self, p: T) -> &mut Self { + self.preds.push(Box::new(p)); + self + } + + /// Set handler object. Usually call to this method is last call + /// during route configuration, because it does not return reference to self. + pub fn h>(&mut self, handler: H) { + self.handler = Box::new(WrapHandler::new(handler)); + } + + /// Set handler function. Usually call to this method is last call + /// during route configuration, because it does not return reference to self. + pub fn f(&mut self, handler: F) + where F: Fn(HttpRequest) -> R + 'static, + R: Responder + 'static, + { + self.handler = Box::new(WrapHandler::new(handler)); + } + + /// Set async handler function. + pub fn a(&mut self, handler: H) + where H: Fn(HttpRequest) -> F + 'static, + F: Future + 'static, + R: Responder + 'static, + E: Into + 'static + { + self.handler = Box::new(AsyncHandler::new(handler)); } } diff --git a/src/router.rs b/src/router.rs new file mode 100644 index 000000000..560f7de79 --- /dev/null +++ b/src/router.rs @@ -0,0 +1,459 @@ +use std::mem; +use std::rc::Rc; +use std::hash::{Hash, Hasher}; +use std::collections::HashMap; + +use regex::{Regex, RegexSet}; + +use error::UrlGenerationError; +use resource::Resource; +use httprequest::HttpRequest; +use server::ServerSettings; + + +/// Interface for application router. +pub struct Router(Rc); + +struct Inner { + prefix: String, + prefix_len: usize, + regset: RegexSet, + named: HashMap, + patterns: Vec, + srv: ServerSettings, +} + +impl Router { + /// Create new router + pub fn new(prefix: &str, + settings: ServerSettings, + map: HashMap>>) -> (Router, Vec>) + { + let prefix = prefix.trim().trim_right_matches('/').to_owned(); + let mut named = HashMap::new(); + let mut patterns = Vec::new(); + let mut resources = Vec::new(); + let mut paths = Vec::new(); + + for (pattern, resource) in map { + if !pattern.name().is_empty() { + let name = pattern.name().into(); + named.insert(name, (pattern.clone(), resource.is_none())); + } + + if let Some(resource) = resource { + paths.push(pattern.pattern().to_owned()); + patterns.push(pattern); + resources.push(resource); + } + } + + let len = prefix.len(); + (Router(Rc::new( + Inner{ prefix: prefix, + prefix_len: len, + regset: RegexSet::new(&paths).unwrap(), + named: named, + patterns: patterns, + srv: settings })), resources) + } + + /// Router prefix + #[inline] + pub fn prefix(&self) -> &str { + &self.0.prefix + } + + /// Server settings + #[inline] + pub fn server_settings(&self) -> &ServerSettings { + &self.0.srv + } + + /// Query for matched resource + pub fn recognize(&self, req: &mut HttpRequest) -> Option { + let mut idx = None; + { + if self.0.prefix_len > req.path().len() { + return None + } + let path = &req.path()[self.0.prefix_len..]; + if path.is_empty() { + if let Some(i) = self.0.regset.matches("/").into_iter().next() { + idx = Some(i); + } + } else if let Some(i) = self.0.regset.matches(path).into_iter().next() { + idx = Some(i); + } + } + + if let Some(idx) = idx { + self.0.patterns[idx].update_match_info(req, self.0.prefix_len); + return Some(idx) + } else { + None + } + } + + /// Check if application contains matching route. + /// + /// This method does not take `prefix` into account. + /// For example if prefix is `/test` and router contains route `/name`, + /// following path would be recognizable `/test/name` but `has_route()` call + /// would return `false`. + pub fn has_route(&self, path: &str) -> bool { + if path.is_empty() { + if self.0.regset.matches("/").into_iter().next().is_some() { + return true + } + } else if self.0.regset.matches(path).into_iter().next().is_some() { + return true + } + false + } + + /// Build named resource path. + /// + /// Check [`HttpRequest::url_for()`](../struct.HttpRequest.html#method.url_for) + /// for detailed information. + pub fn resource_path(&self, name: &str, elements: U) + -> Result + where U: IntoIterator, + I: AsRef, + { + if let Some(pattern) = self.0.named.get(name) { + if pattern.1 { + pattern.0.path(None, elements) + } else { + pattern.0.path(Some(&self.0.prefix), elements) + } + } else { + Err(UrlGenerationError::ResourceNotFound) + } + } +} + +impl Clone for Router { + fn clone(&self) -> Router { + Router(Rc::clone(&self.0)) + } +} + +#[derive(Debug, Clone, PartialEq)] +enum PatternElement { + Str(String), + Var(String), +} + +#[derive(Clone)] +pub struct Pattern { + re: Regex, + name: String, + pattern: String, + names: Vec, + elements: Vec, +} + +impl Pattern { + /// Parse path pattern and create new `Pattern` instance. + /// + /// Panics if path pattern is wrong. + pub fn new(name: &str, path: &str, starts: &str) -> Self { + let (pattern, elements) = Pattern::parse(path, starts); + + let re = match Regex::new(&pattern) { + Ok(re) => re, + Err(err) => panic!("Wrong path pattern: \"{}\" {}", path, err) + }; + let names = re.capture_names() + .filter_map(|name| name.map(|name| name.to_owned())) + .collect(); + + Pattern { + re: re, + name: name.into(), + pattern: pattern, + names: names, + elements: elements, + } + } + + /// Returns name of the pattern + pub fn name(&self) -> &str { + &self.name + } + + /// Returns path of the pattern + pub fn pattern(&self) -> &str { + &self.pattern + } + + /// Extract pattern parameters from the text + // This method unsafe internally, assumption that Pattern instance lives + // longer than `req` + pub fn update_match_info(&self, req: &mut HttpRequest, prefix: usize) { + if !self.names.is_empty() { + let text: &str = unsafe{ mem::transmute(&req.path()[prefix..]) }; + if let Some(captures) = self.re.captures(text) { + let mut idx = 0; + for capture in captures.iter() { + if let Some(ref m) = capture { + if idx != 0 { + req.match_info_mut().add( + self.names[idx-1].as_str(), m.as_str()); + } + idx += 1; + } + } + }; + } + } + + /// Extract pattern parameters from the text + pub fn get_match_info<'a>(&self, text: &'a str) -> HashMap<&str, &'a str> { + let mut info = HashMap::new(); + if !self.names.is_empty() { + if let Some(captures) = self.re.captures(text) { + let mut idx = 0; + for capture in captures.iter() { + if let Some(ref m) = capture { + if idx != 0 { + info.insert(self.names[idx-1].as_str(), m.as_str()); + } + idx += 1; + } + } + }; + } + info + } + + /// Build pattern path. + pub fn path(&self, prefix: Option<&str>, elements: U) -> Result + where U: IntoIterator, + I: AsRef, + { + let mut iter = elements.into_iter(); + let mut path = if let Some(prefix) = prefix { + format!("{}/", prefix) + } else { + String::new() + }; + for el in &self.elements { + match *el { + PatternElement::Str(ref s) => path.push_str(s), + PatternElement::Var(_) => { + if let Some(val) = iter.next() { + path.push_str(val.as_ref()) + } else { + return Err(UrlGenerationError::NotEnoughElements) + } + } + } + } + Ok(path) + } + + fn parse(pattern: &str, starts: &str) -> (String, Vec) { + const DEFAULT_PATTERN: &str = "[^/]+"; + + let mut re = String::from(starts); + let mut el = String::new(); + let mut in_param = false; + let mut in_param_pattern = false; + let mut param_name = String::new(); + let mut param_pattern = String::from(DEFAULT_PATTERN); + let mut elems = Vec::new(); + + for (index, ch) in pattern.chars().enumerate() { + // All routes must have a leading slash so its optional to have one + if index == 0 && ch == '/' { + continue; + } + + if in_param { + // In parameter segment: `{....}` + if ch == '}' { + elems.push(PatternElement::Var(param_name.clone())); + re.push_str(&format!(r"(?P<{}>{})", ¶m_name, ¶m_pattern)); + + param_name.clear(); + param_pattern = String::from(DEFAULT_PATTERN); + + in_param_pattern = false; + in_param = false; + } else if ch == ':' { + // The parameter name has been determined; custom pattern land + in_param_pattern = true; + param_pattern.clear(); + } else if in_param_pattern { + // Ignore leading whitespace for pattern + if !(ch == ' ' && param_pattern.is_empty()) { + param_pattern.push(ch); + } + } else { + param_name.push(ch); + } + } else if ch == '{' { + in_param = true; + elems.push(PatternElement::Str(el.clone())); + el.clear(); + } else { + re.push(ch); + el.push(ch); + } + } + + re.push('$'); + (re, elems) + } +} + +impl PartialEq for Pattern { + fn eq(&self, other: &Pattern) -> bool { + self.pattern == other.pattern + } +} + +impl Eq for Pattern {} + +impl Hash for Pattern { + fn hash(&self, state: &mut H) { + self.pattern.hash(state); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use regex::Regex; + use test::TestRequest; + + #[test] + fn test_recognizer() { + let mut routes = HashMap::new(); + routes.insert(Pattern::new("", "/name", "^/"), Some(Resource::default())); + routes.insert(Pattern::new("", "/name/{val}", "^/"), Some(Resource::default())); + routes.insert(Pattern::new("", "/name/{val}/index.html", "^/"), + Some(Resource::default())); + routes.insert(Pattern::new("", "/v{val}/{val2}/index.html", "^/"), + Some(Resource::default())); + routes.insert(Pattern::new("", "/v/{tail:.*}", "^/"), Some(Resource::default())); + routes.insert(Pattern::new("", "{test}/index.html", "^/"), Some(Resource::default())); + let (rec, _) = Router::new::<()>("", ServerSettings::default(), routes); + + let mut req = TestRequest::with_uri("/name").finish(); + assert!(rec.recognize(&mut req).is_some()); + assert!(req.match_info().is_empty()); + + let mut req = TestRequest::with_uri("/name/value").finish(); + assert!(rec.recognize(&mut req).is_some()); + assert_eq!(req.match_info().get("val").unwrap(), "value"); + assert_eq!(&req.match_info()["val"], "value"); + + let mut req = TestRequest::with_uri("/name/value2/index.html").finish(); + assert!(rec.recognize(&mut req).is_some()); + assert_eq!(req.match_info().get("val").unwrap(), "value2"); + + let mut req = TestRequest::with_uri("/vtest/ttt/index.html").finish(); + assert!(rec.recognize(&mut req).is_some()); + assert_eq!(req.match_info().get("val").unwrap(), "test"); + assert_eq!(req.match_info().get("val2").unwrap(), "ttt"); + + let mut req = TestRequest::with_uri("/v/blah-blah/index.html").finish(); + assert!(rec.recognize(&mut req).is_some()); + assert_eq!(req.match_info().get("tail").unwrap(), "blah-blah/index.html"); + + let mut req = TestRequest::with_uri("/bbb/index.html").finish(); + assert!(rec.recognize(&mut req).is_some()); + assert_eq!(req.match_info().get("test").unwrap(), "bbb"); + } + + #[test] + fn test_recognizer_with_prefix() { + let mut routes = HashMap::new(); + routes.insert(Pattern::new("", "/name", "^/"), Some(Resource::default())); + routes.insert(Pattern::new("", "/name/{val}", "^/"), Some(Resource::default())); + let (rec, _) = Router::new::<()>("/test", ServerSettings::default(), routes); + + let mut req = TestRequest::with_uri("/name").finish(); + assert!(rec.recognize(&mut req).is_none()); + + let mut req = TestRequest::with_uri("/test/name").finish(); + assert!(rec.recognize(&mut req).is_some()); + + let mut req = TestRequest::with_uri("/test/name/value").finish(); + assert!(rec.recognize(&mut req).is_some()); + assert_eq!(req.match_info().get("val").unwrap(), "value"); + assert_eq!(&req.match_info()["val"], "value"); + + // same patterns + let mut routes = HashMap::new(); + routes.insert(Pattern::new("", "/name", "^/"), Some(Resource::default())); + routes.insert(Pattern::new("", "/name/{val}", "^/"), Some(Resource::default())); + let (rec, _) = Router::new::<()>("/test2", ServerSettings::default(), routes); + + let mut req = TestRequest::with_uri("/name").finish(); + assert!(rec.recognize(&mut req).is_none()); + let mut req = TestRequest::with_uri("/test2/name").finish(); + assert!(rec.recognize(&mut req).is_some()); + } + + fn assert_parse(pattern: &str, expected_re: &str) -> Regex { + let (re_str, _) = Pattern::parse(pattern, "^/"); + assert_eq!(&*re_str, expected_re); + Regex::new(&re_str).unwrap() + } + + #[test] + fn test_parse_static() { + let re = assert_parse("/", r"^/$"); + assert!(re.is_match("/")); + assert!(!re.is_match("/a")); + + let re = assert_parse("/name", r"^/name$"); + assert!(re.is_match("/name")); + assert!(!re.is_match("/name1")); + assert!(!re.is_match("/name/")); + assert!(!re.is_match("/name~")); + + let re = assert_parse("/name/", r"^/name/$"); + assert!(re.is_match("/name/")); + assert!(!re.is_match("/name")); + assert!(!re.is_match("/name/gs")); + + let re = assert_parse("/user/profile", r"^/user/profile$"); + assert!(re.is_match("/user/profile")); + assert!(!re.is_match("/user/profile/profile")); + } + + #[test] + fn test_parse_param() { + let re = assert_parse("/user/{id}", r"^/user/(?P[^/]+)$"); + assert!(re.is_match("/user/profile")); + assert!(re.is_match("/user/2345")); + assert!(!re.is_match("/user/2345/")); + assert!(!re.is_match("/user/2345/sdg")); + + let captures = re.captures("/user/profile").unwrap(); + assert_eq!(captures.get(1).unwrap().as_str(), "profile"); + assert_eq!(captures.name("id").unwrap().as_str(), "profile"); + + let captures = re.captures("/user/1245125").unwrap(); + assert_eq!(captures.get(1).unwrap().as_str(), "1245125"); + assert_eq!(captures.name("id").unwrap().as_str(), "1245125"); + + let re = assert_parse( + "/v{version}/resource/{id}", + r"^/v(?P[^/]+)/resource/(?P[^/]+)$", + ); + assert!(re.is_match("/v1/resource/320120")); + assert!(!re.is_match("/v/resource/1")); + assert!(!re.is_match("/resource")); + + let captures = re.captures("/v151/resource/adahg32").unwrap(); + assert_eq!(captures.get(1).unwrap().as_str(), "151"); + assert_eq!(captures.name("version").unwrap().as_str(), "151"); + assert_eq!(captures.name("id").unwrap().as_str(), "adahg32"); + } +} diff --git a/src/server.rs b/src/server.rs index d3fd36147..501445174 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,249 +1,810 @@ -use std::{io, net}; +use std::{io, net, thread}; use std::rc::Rc; -use std::net::SocketAddr; +use std::sync::{Arc, mpsc as sync_mpsc}; +use std::time::Duration; use std::marker::PhantomData; +use std::collections::HashMap; -use actix::dev::*; -use futures::Stream; +use actix::prelude::*; +use actix::actors::signal; +use futures::{Future, Sink, Stream}; +use futures::sync::mpsc; use tokio_io::{AsyncRead, AsyncWrite}; -use tokio_core::net::{TcpListener, TcpStream}; +use tokio_core::net::TcpStream; +use mio; +use num_cpus; +use net2::TcpBuilder; -#[cfg(feature="tls")] -use futures::Future; #[cfg(feature="tls")] use native_tls::TlsAcceptor; #[cfg(feature="tls")] -use tokio_tls::{TlsStream, TlsAcceptorExt}; +use tokio_tls::TlsStream; -#[cfg(feature="alpn")] -use futures::Future; #[cfg(feature="alpn")] use openssl::ssl::{SslMethod, SslAcceptorBuilder}; #[cfg(feature="alpn")] use openssl::pkcs12::ParsedPkcs12; #[cfg(feature="alpn")] -use tokio_openssl::{SslStream, SslAcceptorExt}; +use tokio_openssl::SslStream; -use channel::{HttpChannel, HttpHandler}; +use helpers; +use channel::{HttpChannel, HttpHandler, IntoHttpHandler, IoStream, WrapperStream}; +use worker::{Conn, Worker, WorkerSettings, StreamHandlerType, StopWorker}; +/// Various server settings +#[derive(Debug, Clone)] +pub struct ServerSettings { + addr: Option, + secure: bool, + host: String, +} + +impl Default for ServerSettings { + fn default() -> Self { + ServerSettings { + addr: None, + secure: false, + host: "localhost:8080".to_owned(), + } + } +} + +impl ServerSettings { + /// Crate server settings instance + fn new(addr: Option, host: &Option, secure: bool) -> Self { + let host = if let Some(ref host) = *host { + host.clone() + } else if let Some(ref addr) = addr { + format!("{}", addr) + } else { + "localhost".to_owned() + }; + ServerSettings { + addr: addr, + secure: secure, + host: host, + } + } + + /// Returns the socket address of the local half of this TCP connection + pub fn local_addr(&self) -> Option { + self.addr + } + + /// Returns true if connection is secure(https) + pub fn secure(&self) -> bool { + self.secure + } + + /// Returns host header value + pub fn host(&self) -> &str { + &self.host + } +} /// An HTTP Server /// -/// `T` - async stream, anything that implements `AsyncRead` + `AsyncWrite`. +/// `T` - async stream, anything that implements `AsyncRead` + `AsyncWrite`. /// /// `A` - peer address /// /// `H` - request handler -pub struct HttpServer { - h: Rc>, +pub struct HttpServer + where H: HttpHandler + 'static +{ + h: Option>>, io: PhantomData, addr: PhantomData, + threads: usize, + backlog: i32, + host: Option, + keep_alive: Option, + factory: Arc U + Send + Sync>, + workers: Vec>>, + sockets: HashMap, + accept: Vec<(mio::SetReadiness, sync_mpsc::Sender)>, + exit: bool, + shutdown_timeout: u16, + signals: Option>, + no_signals: bool, } -impl Actor for HttpServer { +unsafe impl Sync for HttpServer where H: HttpHandler + 'static {} +unsafe impl Send for HttpServer where H: HttpHandler + 'static {} + + +impl Actor for HttpServer { type Context = Context; -} -impl HttpServer where H: HttpHandler -{ - /// Create new http server with vec of http handlers - pub fn new>(handler: U) -> Self { - let apps: Vec<_> = handler.into_iter().collect(); - - HttpServer {h: Rc::new(apps), - io: PhantomData, - addr: PhantomData} + fn started(&mut self, ctx: &mut Self::Context) { + self.update_time(ctx); } } -impl HttpServer - where T: AsyncRead + AsyncWrite + 'static, - A: 'static, +impl HttpServer { + fn update_time(&self, ctx: &mut Context) { + helpers::update_date(); + ctx.run_later(Duration::new(1, 0), |slf, ctx| slf.update_time(ctx)); + } +} + +impl HttpServer + where A: 'static, + T: IoStream, H: HttpHandler, + U: IntoIterator + 'static, + V: IntoHttpHandler, { - /// Start listening for incomming connections from stream. - pub fn serve_incoming(self, stream: S) -> io::Result - where Self: ActorAddress, - S: Stream + 'static + /// Create new http server with application factory + pub fn new(factory: F) -> Self + where F: Sync + Send + 'static + Fn() -> U, { - Ok(HttpServer::create(move |ctx| { - ctx.add_stream(stream.map(|(t, _)| IoStream(t, None, false))); - self - })) + HttpServer{ h: None, + io: PhantomData, + addr: PhantomData, + threads: num_cpus::get(), + backlog: 2048, + host: None, + keep_alive: None, + factory: Arc::new(factory), + workers: Vec::new(), + sockets: HashMap::new(), + accept: Vec::new(), + exit: false, + shutdown_timeout: 30, + signals: None, + no_signals: false, + } } - fn bind(&self, addr: S) - -> io::Result> - { + /// Set number of workers to start. + /// + /// By default http server uses number of available logical cpu as threads count. + pub fn threads(mut self, num: usize) -> Self { + self.threads = num; + self + } + + /// Set the maximum number of pending connections. + /// + /// This refers to the number of clients that can be waiting to be served. + /// Exceeding this number results in the client getting an error when + /// attempting to connect. It should only affect servers under significant load. + /// + /// Generally set in the 64-2048 range. Default value is 2048. + /// + /// This method should be called before `bind()` method call. + pub fn backlog(mut self, num: i32) -> Self { + self.backlog = num; + self + } + + /// Set server keep-alive setting. + /// + /// By default keep alive is enabled. + /// + /// - `Some(75)` - enable + /// + /// - `Some(0)` - disable + /// + /// - `None` - use `SO_KEEPALIVE` socket option + pub fn keep_alive(mut self, val: Option) -> Self { + self.keep_alive = val; + self + } + + /// Set server host name. + /// + /// Host name is used by application router aa a hostname for url generation. + /// Check [ConnectionInfo](./dev/struct.ConnectionInfo.html#method.host) documentation + /// for more information. + pub fn server_hostname(mut self, val: String) -> Self { + self.host = Some(val); + self + } + + /// Send `SystemExit` message to actix system + /// + /// `SystemExit` message stops currently running system arbiter and all + /// nested arbiters. + pub fn system_exit(mut self) -> Self { + self.exit = true; + self + } + + /// Set alternative address for `ProcessSignals` actor. + pub fn signals(mut self, addr: SyncAddress) -> Self { + self.signals = Some(addr); + self + } + + /// Disable signal handling + pub fn disable_signals(mut self) -> Self { + self.no_signals = true; + self + } + + /// Timeout for graceful workers shutdown. + /// + /// After receiving a stop signal, workers have this much time to finish serving requests. + /// Workers still alive after the timeout are force dropped. + /// + /// By default shutdown timeout sets to 30 seconds. + pub fn shutdown_timeout(mut self, sec: u16) -> Self { + self.shutdown_timeout = sec; + self + } + + /// Get addresses of bound sockets. + pub fn addrs(&self) -> Vec { + self.sockets.keys().cloned().collect() + } + + /// The socket address to bind + /// + /// To mind multiple addresses this method can be call multiple times. + pub fn bind(mut self, addr: S) -> io::Result { let mut err = None; - let mut addrs = Vec::new(); - if let Ok(iter) = addr.to_socket_addrs() { - for addr in iter { - match TcpListener::bind(&addr, Arbiter::handle()) { - Ok(tcp) => addrs.push((addr, tcp)), - Err(e) => err = Some(e), - } + let mut succ = false; + for addr in addr.to_socket_addrs()? { + match create_tcp_listener(addr, self.backlog) { + Ok(lst) => { + succ = true; + self.sockets.insert(lst.local_addr().unwrap(), lst); + }, + Err(e) => err = Some(e), } } - if addrs.is_empty() { + + if !succ { if let Some(e) = err.take() { Err(e) } else { Err(io::Error::new(io::ErrorKind::Other, "Can not bind to address.")) } } else { - Ok(addrs) + Ok(self) + } + } + + fn start_workers(&mut self, settings: &ServerSettings, handler: &StreamHandlerType) + -> Vec>> + { + // start workers + let mut workers = Vec::new(); + for _ in 0..self.threads { + let s = settings.clone(); + let (tx, rx) = mpsc::unbounded::>(); + + let h = handler.clone(); + let ka = self.keep_alive; + let factory = Arc::clone(&self.factory); + let addr = Arbiter::start(move |ctx: &mut Context<_>| { + let apps: Vec<_> = (*factory)() + .into_iter() + .map(|h| h.into_handler(s.clone())).collect(); + ctx.add_message_stream(rx); + Worker::new(apps, h, ka) + }); + workers.push(tx); + self.workers.push(addr); + } + info!("Starting {} http workers", self.threads); + workers + } + + // subscribe to os signals + fn subscribe_to_signals(&self, addr: &SyncAddress>) { + if self.no_signals { + let msg = signal::Subscribe(addr.subscriber()); + if let Some(ref signals) = self.signals { + signals.send(msg); + } else { + Arbiter::system_registry().get::().send(msg); + } } } } -impl HttpServer { - +impl HttpServer + where U: IntoIterator + 'static, + V: IntoHttpHandler, +{ /// Start listening for incomming connections. /// - /// This methods converts address to list of `SocketAddr` - /// then binds to all available addresses. - pub fn serve(self, addr: S) -> io::Result - where Self: ActorAddress, - S: net::ToSocketAddrs, + /// This method starts number of http handler workers in seperate threads. + /// For each address this method starts separate thread which does `accept()` in a loop. + /// + /// This methods panics if no socket addresses get bound. + /// + /// This method requires to run within properly configured `Actix` system. + /// + /// ```rust + /// extern crate actix; + /// extern crate actix_web; + /// use actix_web::*; + /// + /// fn main() { + /// let sys = actix::System::new("example"); // <- create Actix system + /// + /// 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") + /// .start(); + /// # actix::Arbiter::system().send(actix::msgs::SystemExit(0)); + /// + /// let _ = sys.run(); // <- Run actix system, this method actually starts all async processes + /// } + /// ``` + pub fn start(mut self) -> SyncAddress { - let addrs = self.bind(addr)?; + if self.sockets.is_empty() { + panic!("HttpServer::bind() has to be called befor start()"); + } else { + let addrs: Vec<(net::SocketAddr, net::TcpListener)> = + self.sockets.drain().collect(); + let settings = ServerSettings::new(Some(addrs[0].0), &self.host, false); + let workers = self.start_workers(&settings, &StreamHandlerType::Normal); - Ok(HttpServer::create(move |ctx| { - for (addr, tcp) in addrs { + // start acceptors threads + for (addr, sock) in addrs { info!("Starting http server on {}", addr); - ctx.add_stream(tcp.incoming().map(|(t, a)| IoStream(t, Some(a), false))); + self.accept.push( + start_accept_thread(sock, addr, self.backlog, workers.clone())); } - self - })) + + // start http server actor + HttpServer::create(|ctx| { + self.subscribe_to_signals(&ctx.address()); + self + }) + } + } + + /// Spawn new thread and start listening for incomming connections. + /// + /// This method spawns new thread and starts new actix system. Other than that it is + /// similar to `start()` method. This method blocks. + /// + /// This methods panics if no socket addresses get bound. + /// + /// ```rust,ignore + /// # extern crate futures; + /// # extern crate actix; + /// # extern crate actix_web; + /// # use futures::Future; + /// use actix_web::*; + /// + /// fn main() { + /// 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") + /// .run(); + /// } + /// ``` + pub fn run(mut self) { + self.exit = true; + self.no_signals = false; + + let _ = thread::spawn(move || { + let sys = System::new("http-server"); + self.start(); + let _ = sys.run(); + }).join(); } } #[cfg(feature="tls")] -impl HttpServer, net::SocketAddr, H> { - +impl HttpServer, net::SocketAddr, H, U> + where U: IntoIterator + 'static, + V: IntoHttpHandler, +{ /// Start listening for incomming tls connections. - /// - /// This methods converts address to list of `SocketAddr` - /// then binds to all available addresses. - pub fn serve_tls(self, addr: S, pkcs12: ::Pkcs12) -> io::Result - where Self: ActorAddress, - S: net::ToSocketAddrs, - { - let addrs = self.bind(addr)?; - let acceptor = match TlsAcceptor::builder(pkcs12) { - Ok(builder) => { - match builder.build() { - Ok(acceptor) => Rc::new(acceptor), - Err(err) => return Err(io::Error::new(io::ErrorKind::Other, err)) + pub fn start_tls(mut self, pkcs12: ::Pkcs12) -> io::Result> { + if self.sockets.is_empty() { + Err(io::Error::new(io::ErrorKind::Other, "No socket addresses are bound")) + } else { + let addrs: Vec<(net::SocketAddr, net::TcpListener)> = self.sockets.drain().collect(); + let settings = ServerSettings::new(Some(addrs[0].0), &self.host, false); + let acceptor = match TlsAcceptor::builder(pkcs12) { + Ok(builder) => { + match builder.build() { + Ok(acceptor) => acceptor, + Err(err) => return Err(io::Error::new(io::ErrorKind::Other, err)) + } } - } - Err(err) => return Err(io::Error::new(io::ErrorKind::Other, err)) - }; + Err(err) => return Err(io::Error::new(io::ErrorKind::Other, err)) + }; + let workers = self.start_workers(&settings, &StreamHandlerType::Tls(acceptor)); - Ok(HttpServer::create(move |ctx| { - for (addr, tcp) in addrs { + // start acceptors threads + for (addr, sock) in addrs { info!("Starting tls http server on {}", addr); - - let acc = acceptor.clone(); - ctx.add_stream(tcp.incoming().and_then(move |(stream, addr)| { - TlsAcceptorExt::accept_async(acc.as_ref(), stream) - .map(move |t| { - IoStream(t, Some(addr), false) - }) - .map_err(|err| { - trace!("Error during handling tls connection: {}", err); - io::Error::new(io::ErrorKind::Other, err) - }) - })); + self.accept.push( + start_accept_thread(sock, addr, self.backlog, workers.clone())); } - self - })) + + // start http server actor + Ok(HttpServer::create(|ctx| { + self.subscribe_to_signals(&ctx.address()); + self + })) + } } } #[cfg(feature="alpn")] -impl HttpServer, net::SocketAddr, H> { - +impl HttpServer, net::SocketAddr, H, U> + where U: IntoIterator + 'static, + V: IntoHttpHandler, +{ /// Start listening for incomming tls connections. /// - /// This methods converts address to list of `SocketAddr` - /// then binds to all available addresses. - pub fn serve_tls(self, addr: S, identity: ParsedPkcs12) -> io::Result - where Self: ActorAddress, - S: net::ToSocketAddrs, - { - let addrs = self.bind(addr)?; - let acceptor = match SslAcceptorBuilder::mozilla_intermediate(SslMethod::tls(), - &identity.pkey, - &identity.cert, - &identity.chain) - { - Ok(mut builder) => { - match builder.builder_mut().set_alpn_protocols(&[b"h2", b"http/1.1"]) { - Ok(_) => builder.build(), - Err(err) => return Err(io::Error::new(io::ErrorKind::Other, err)), - } - }, - Err(err) => return Err(io::Error::new(io::ErrorKind::Other, err)) - }; + /// This method sets alpn protocols to "h2" and "http/1.1" + pub fn start_ssl(mut self, identity: &ParsedPkcs12) -> io::Result> { + if self.sockets.is_empty() { + Err(io::Error::new(io::ErrorKind::Other, "No socket addresses are bound")) + } else { + let addrs: Vec<(net::SocketAddr, net::TcpListener)> = self.sockets.drain().collect(); + let settings = ServerSettings::new(Some(addrs[0].0), &self.host, false); + let acceptor = match SslAcceptorBuilder::mozilla_intermediate( + SslMethod::tls(), &identity.pkey, &identity.cert, &identity.chain) + { + Ok(mut builder) => { + match builder.set_alpn_protocols(&[b"h2", b"http/1.1"]) { + Ok(_) => builder.build(), + Err(err) => return Err(io::Error::new(io::ErrorKind::Other, err)), + } + }, + Err(err) => return Err(io::Error::new(io::ErrorKind::Other, err)) + }; + let workers = self.start_workers(&settings, &StreamHandlerType::Alpn(acceptor)); - Ok(HttpServer::create(move |ctx| { - for (addr, tcp) in addrs { + // start acceptors threads + for (addr, sock) in addrs { info!("Starting tls http server on {}", addr); - - let acc = acceptor.clone(); - ctx.add_stream(tcp.incoming().and_then(move |(stream, addr)| { - SslAcceptorExt::accept_async(&acc, stream) - .map(move |stream| { - let http2 = if let Some(p) = - stream.get_ref().ssl().selected_alpn_protocol() - { - p.len() == 2 && &p == b"h2" - } else { - false - }; - IoStream(stream, Some(addr), http2) - }) - .map_err(|err| { - trace!("Error during handling tls connection: {}", err); - io::Error::new(io::ErrorKind::Other, err) - }) - })); + self.accept.push( + start_accept_thread(sock, addr, self.backlog, workers.clone())); } - self - })) + + // start http server actor + Ok(HttpServer::create(|ctx| { + self.subscribe_to_signals(&ctx.address()); + self + })) + } } } -struct IoStream(T, Option, bool); - -impl ResponseType for IoStream - where T: AsyncRead + AsyncWrite + 'static +impl HttpServer, A, H, U> + where A: 'static, + T: AsyncRead + AsyncWrite + 'static, + H: HttpHandler, + U: IntoIterator + 'static, + V: IntoHttpHandler, { - type Item = (); - type Error = (); + /// Start listening for incomming connections from a stream. + /// + /// This method uses only one thread for handling incoming connections. + pub fn start_incoming(mut self, stream: S, secure: bool) -> SyncAddress + where S: Stream + 'static + { + if !self.sockets.is_empty() { + let addrs: Vec<(net::SocketAddr, net::TcpListener)> = + self.sockets.drain().collect(); + let settings = ServerSettings::new(Some(addrs[0].0), &self.host, false); + let workers = self.start_workers(&settings, &StreamHandlerType::Normal); + + // start acceptors threads + for (addr, sock) in addrs { + info!("Starting http server on {}", addr); + self.accept.push( + start_accept_thread(sock, addr, self.backlog, workers.clone())); + } + } + + // set server settings + let addr: net::SocketAddr = "127.0.0.1:8080".parse().unwrap(); + let settings = ServerSettings::new(Some(addr), &self.host, secure); + let apps: Vec<_> = (*self.factory)() + .into_iter() + .map(|h| h.into_handler(settings.clone())).collect(); + self.h = Some(Rc::new(WorkerSettings::new(apps, self.keep_alive))); + + // start server + HttpServer::create(move |ctx| { + ctx.add_stream(stream.map( + move |(t, _)| Conn{io: WrapperStream::new(t), peer: None, http2: false})); + self.subscribe_to_signals(&ctx.address()); + self + }) + } } -impl StreamHandler, io::Error> for HttpServer - where T: AsyncRead + AsyncWrite + 'static, - H: HttpHandler + 'static, - A: 'static {} - -impl Handler, io::Error> for HttpServer - where T: AsyncRead + AsyncWrite + 'static, +/// Signals support +/// Handle `SIGINT`, `SIGTERM`, `SIGQUIT` signals and send `SystemExit(0)` +/// message to `System` actor. +impl Handler for HttpServer + where T: IoStream, H: HttpHandler + 'static, + U: 'static, A: 'static, { - fn error(&mut self, err: io::Error, _: &mut Context) { - debug!("Error handling request: {}", err) - } + type Result = (); - fn handle(&mut self, msg: IoStream, _: &mut Context) - -> Response> - { - Arbiter::handle().spawn( - HttpChannel::new(msg.0, msg.1, Rc::clone(&self.h), msg.2)); - Self::empty() + fn handle(&mut self, msg: signal::Signal, ctx: &mut Context) { + match msg.0 { + signal::SignalType::Int => { + info!("SIGINT received, exiting"); + self.exit = true; + Handler::::handle(self, StopServer{graceful: false}, ctx); + } + signal::SignalType::Term => { + info!("SIGTERM received, stopping"); + self.exit = true; + Handler::::handle(self, StopServer{graceful: true}, ctx); + } + signal::SignalType::Quit => { + info!("SIGQUIT received, exiting"); + self.exit = true; + Handler::::handle(self, StopServer{graceful: false}, ctx); + } + _ => (), + } } } + +impl Handler>> for HttpServer + where T: IoStream, + H: HttpHandler + 'static, + U: 'static, + A: 'static, +{ + type Result = (); + + fn handle(&mut self, msg: io::Result>, _: &mut Context) -> Self::Result { + match msg { + Ok(msg) => + Arbiter::handle().spawn( + HttpChannel::new( + Rc::clone(self.h.as_ref().unwrap()), msg.io, msg.peer, msg.http2)), + Err(err) => + debug!("Error handling request: {}", err), + } + } +} + +/// Pause accepting incoming connections +/// +/// If socket contains some pending connection, they might be dropped. +/// All opened connection remains active. +#[derive(Message)] +pub struct PauseServer; + +/// Resume accepting incoming connections +#[derive(Message)] +pub struct ResumeServer; + +/// Stop incoming connection processing, stop all workers and exit. +/// +/// If server starts with `spawn()` method, then spawned thread get terminated. +#[derive(Message)] +pub struct StopServer { + pub graceful: bool +} + +impl Handler for HttpServer + where T: IoStream, + H: HttpHandler + 'static, + U: 'static, + A: 'static, +{ + type Result = (); + + fn handle(&mut self, _: PauseServer, _: &mut Context) + { + for item in &self.accept { + let _ = item.1.send(Command::Pause); + let _ = item.0.set_readiness(mio::Ready::readable()); + } + } +} + +impl Handler for HttpServer + where T: IoStream, + H: HttpHandler + 'static, + U: 'static, + A: 'static, +{ + type Result = (); + + fn handle(&mut self, _: ResumeServer, _: &mut Context) { + for item in &self.accept { + let _ = item.1.send(Command::Resume); + let _ = item.0.set_readiness(mio::Ready::readable()); + } + } +} + +impl Handler for HttpServer + where T: IoStream, + H: HttpHandler + 'static, + U: 'static, + A: 'static, +{ + type Result = actix::Response; + + fn handle(&mut self, msg: StopServer, ctx: &mut Context) -> Self::Result { + // stop accept threads + for item in &self.accept { + let _ = item.1.send(Command::Stop); + let _ = item.0.set_readiness(mio::Ready::readable()); + } + + // stop workers + let (tx, rx) = mpsc::channel(1); + + let dur = if msg.graceful { + Some(Duration::new(u64::from(self.shutdown_timeout), 0)) + } else { + None + }; + for worker in &self.workers { + let tx2 = tx.clone(); + let fut = worker.call(self, StopWorker{graceful: dur}); + ActorFuture::then(fut, move |_, slf, _| { + slf.workers.pop(); + if slf.workers.is_empty() { + let _ = tx2.send(()); + + // we need to stop system if server was spawned + if slf.exit { + Arbiter::system().send(actix::msgs::SystemExit(0)) + } + } + actix::fut::ok(()) + }).spawn(ctx); + } + + if !self.workers.is_empty() { + Self::async_reply( + rx.into_future().map(|_| ()).map_err(|_| ()).actfuture()) + } else { + // we need to stop system if server was spawned + if self.exit { + Arbiter::system().send(actix::msgs::SystemExit(0)) + } + Self::reply(Ok(())) + } + } +} + +enum Command { + Pause, + Resume, + Stop, +} + +fn start_accept_thread(sock: net::TcpListener, addr: net::SocketAddr, backlog: i32, + workers: Vec>>) + -> (mio::SetReadiness, sync_mpsc::Sender) +{ + let (tx, rx) = sync_mpsc::channel(); + let (reg, readiness) = mio::Registration::new2(); + + // start accept thread + let _ = thread::Builder::new().name(format!("Accept on {}", addr)).spawn(move || { + const SRV: mio::Token = mio::Token(0); + const CMD: mio::Token = mio::Token(1); + + let mut server = Some( + mio::net::TcpListener::from_listener(sock, &addr) + .expect("Can not create mio::net::TcpListener")); + + // Create a poll instance + let poll = match mio::Poll::new() { + Ok(poll) => poll, + Err(err) => panic!("Can not create mio::Poll: {}", err), + }; + + // Start listening for incoming connections + if let Some(ref srv) = server { + if let Err(err) = poll.register( + srv, SRV, mio::Ready::readable(), mio::PollOpt::edge()) { + panic!("Can not register io: {}", err); + } + } + + // Start listening for incommin commands + if let Err(err) = poll.register(®, CMD, + mio::Ready::readable(), mio::PollOpt::edge()) { + panic!("Can not register Registration: {}", err); + } + + // Create storage for events + let mut events = mio::Events::with_capacity(128); + + let mut next = 0; + loop { + if let Err(err) = poll.poll(&mut events, None) { + panic!("Poll error: {}", err); + } + + for event in events.iter() { + match event.token() { + SRV => { + if let Some(ref server) = server { + loop { + match server.accept_std() { + Ok((sock, addr)) => { + let msg = Conn{ + io: sock, peer: Some(addr), http2: false}; + workers[next].unbounded_send(msg) + .expect("worker thread died"); + next = (next + 1) % workers.len(); + }, + Err(err) => if err.kind() == io::ErrorKind::WouldBlock { + break + } else { + error!("Error accepting connection: {:?}", err); + return + } + } + } + } + }, + CMD => match rx.try_recv() { + Ok(cmd) => match cmd { + Command::Pause => if let Some(server) = server.take() { + if let Err(err) = poll.deregister(&server) { + error!("Can not deregister server socket {}", err); + } else { + info!("Paused accepting connections on {}", addr); + } + }, + Command::Resume => { + let lst = create_tcp_listener(addr, backlog) + .expect("Can not create net::TcpListener"); + + server = Some( + mio::net::TcpListener::from_listener(lst, &addr) + .expect("Can not create mio::net::TcpListener")); + + if let Some(ref server) = server { + if let Err(err) = poll.register( + server, SRV, mio::Ready::readable(), mio::PollOpt::edge()) + { + error!("Can not resume socket accept process: {}", err); + } else { + info!("Accepting connections on {} has been resumed", + addr); + } + } + }, + Command::Stop => return, + }, + Err(err) => match err { + sync_mpsc::TryRecvError::Empty => (), + sync_mpsc::TryRecvError::Disconnected => return, + } + }, + _ => unreachable!(), + } + } + } + }); + + (readiness, tx) +} + +fn create_tcp_listener(addr: net::SocketAddr, backlog: i32) -> io::Result { + let builder = match addr { + net::SocketAddr::V4(_) => TcpBuilder::new_v4()?, + net::SocketAddr::V6(_) => TcpBuilder::new_v6()?, + }; + builder.bind(addr)?; + builder.reuse_address(true)?; + Ok(builder.listen(backlog)?) +} diff --git a/src/staticfiles.rs b/src/staticfiles.rs deleted file mode 100644 index 858ee5f6b..000000000 --- a/src/staticfiles.rs +++ /dev/null @@ -1,189 +0,0 @@ -//! 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::PathBuf; - -use mime_guess::get_mime_type; -use route::Handler; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use httpcodes::{HTTPOk, HTTPNotFound, HTTPForbidden}; - -/// Static files handling -/// -/// Can be registered with `Application::route_handler()`. -/// -/// ```rust -/// extern crate actix_web; -/// use actix_web::*; -/// -/// fn main() { -/// let app = Application::default("/") -/// .route("/static", 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>(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, - } - } - - fn index(&self, prefix: &str, relpath: &str, filename: &PathBuf) -> Result { - let index_of = format!("Index of {}{}", prefix, relpath); - let mut body = String::new(); - - for entry in filename.read_dir()? { - if self.can_list(&entry) { - let entry = entry.unwrap(); - // show file url as relative to static path - let file_url = format!( - "{}{}", prefix, - entry.path().strip_prefix(&self.directory).unwrap().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() { - //format!("", file_url, file_name)); - let _ = write!(body, "
  • {}/
  • ", - file_url, entry.file_name().to_string_lossy()); - } else { - // write!(body, "{}/", entry.file_name()) - let _ = write!(body, "
  • {}
  • ", - file_url, entry.file_name().to_string_lossy()); - } - } else { - continue - } - } - } - - let html = format!("\ - {}\ -

    {}

    \ -
      \ - {}\ -
    \n", index_of, index_of, body); - Ok( - HTTPOk.build() - .content_type("text/html; charset=utf-8") - .body(html).unwrap() - ) - } - - fn can_list(&self, entry: &io::Result) -> 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 Handler for StaticFiles { - type Result = Result; - - fn handle(&self, req: HttpRequest) -> Self::Result { - if !self.accessible { - Ok(HTTPNotFound.into()) - } else { - let mut hidden = false; - let filepath = req.path()[req.prefix_len()..] - .split('/').filter(|s| { - if s.starts_with('.') { - hidden = true; - } - !s.is_empty() - }) - .fold(String::new(), |s, i| {s + "/" + i}); - - // hidden file - if hidden { - return Ok(HTTPNotFound.into()) - } - - // full filepath - let idx = if filepath.starts_with('/') { 1 } else { 0 }; - let filename = match self.directory.join(&filepath[idx..]).canonicalize() { - Ok(fname) => fname, - Err(err) => return match err.kind() { - io::ErrorKind::NotFound => Ok(HTTPNotFound.into()), - io::ErrorKind::PermissionDenied => Ok(HTTPForbidden.into()), - _ => Err(err), - } - }; - - if filename.is_dir() { - match self.index( - &req.path()[..req.prefix_len()], &filepath[idx..], &filename) - { - Ok(resp) => Ok(resp), - Err(err) => match err.kind() { - io::ErrorKind::NotFound => Ok(HTTPNotFound.into()), - io::ErrorKind::PermissionDenied => Ok(HTTPForbidden.into()), - _ => Err(err), - } - } - } else { - let mut resp = HTTPOk.build(); - if let Some(ext) = filename.extension() { - let mime = get_mime_type(&ext.to_string_lossy()); - resp.content_type(format!("{}", mime).as_str()); - } - match File::open(filename) { - Ok(mut file) => { - let mut data = Vec::new(); - let _ = file.read_to_end(&mut data); - Ok(resp.body(data).unwrap()) - }, - Err(err) => Err(err), - } - } - } - } -} diff --git a/src/test.rs b/src/test.rs new file mode 100644 index 000000000..f92ed8e62 --- /dev/null +++ b/src/test.rs @@ -0,0 +1,412 @@ +//! Various helpers for Actix applications to use during testing. + +use std::{net, thread}; +use std::rc::Rc; +use std::sync::mpsc; +use std::str::FromStr; +use std::collections::HashMap; + +use actix::{Arbiter, SyncAddress, System, msgs}; +use cookie::Cookie; +use http::{Uri, Method, Version, HeaderMap, HttpTryFrom}; +use http::header::{HeaderName, HeaderValue}; +use futures::Future; +use tokio_core::net::TcpListener; +use tokio_core::reactor::Core; +use net2::TcpBuilder; + +use error::Error; +use server::{HttpServer, ServerSettings}; +use handler::{Handler, Responder, ReplyItem}; +use channel::{HttpHandler, IntoHttpHandler}; +use middleware::Middleware; +use application::{Application, HttpApplication}; +use param::Params; +use router::Router; +use payload::Payload; +use httprequest::HttpRequest; +use httpresponse::HttpResponse; + +/// The `TestServer` type. +/// +/// `TestServer` is very simple test server that simplify process of writing +/// integrational tests cases for actix web applications. +/// +/// # Examples +/// +/// ```rust +/// # extern crate actix; +/// # extern crate actix_web; +/// # use actix_web::*; +/// # extern crate reqwest; +/// # +/// # fn my_handler(req: HttpRequest) -> HttpResponse { +/// # httpcodes::HTTPOk.into() +/// # } +/// # +/// # fn main() { +/// use actix_web::test::TestServer; +/// +/// let srv = TestServer::new(|app| app.handler(my_handler)); +/// +/// assert!(reqwest::get(&srv.url("/")).unwrap().status().is_success()); +/// # } +/// ``` +pub struct TestServer { + addr: net::SocketAddr, + thread: Option>, + sys: SyncAddress, +} + +impl TestServer { + + /// Start new test server + /// + /// This methos accepts configuration method. You can add + /// middlewares or set handlers for test application. + pub fn new(config: F) -> Self + where F: Sync + Send + 'static + Fn(&mut TestApp<()>), + { + TestServer::with_state(||(), config) + } + + /// Start new test server with application factory + pub fn with_factory(factory: F) -> Self + where H: HttpHandler, + F: Sync + Send + 'static + Fn() -> U, + U: IntoIterator + 'static, + V: IntoHttpHandler, + { + let (tx, rx) = mpsc::channel(); + + // run server in separate thread + let join = thread::spawn(move || { + let sys = System::new("actix-test-server"); + let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); + let local_addr = tcp.local_addr().unwrap(); + let tcp = TcpListener::from_listener(tcp, &local_addr, Arbiter::handle()).unwrap(); + + HttpServer::new(factory).start_incoming(tcp.incoming(), false); + + tx.send((Arbiter::system(), local_addr)).unwrap(); + let _ = sys.run(); + }); + + let (sys, addr) = rx.recv().unwrap(); + TestServer { + addr: addr, + thread: Some(join), + sys: sys, + } + } + + /// Start new test server with custom application state + /// + /// This methos accepts state factory and configuration method. + pub fn with_state(state: FS, config: F) -> Self + where S: 'static, + FS: Sync + Send + 'static + Fn() -> S, + F: Sync + Send + 'static + Fn(&mut TestApp), + { + let (tx, rx) = mpsc::channel(); + + // run server in separate thread + let join = thread::spawn(move || { + let sys = System::new("actix-test-server"); + + let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); + let local_addr = tcp.local_addr().unwrap(); + let tcp = TcpListener::from_listener(tcp, &local_addr, Arbiter::handle()).unwrap(); + + HttpServer::new(move || { + let mut app = TestApp::new(state()); + config(&mut app); + app} + ).start_incoming(tcp.incoming(), false); + + tx.send((Arbiter::system(), local_addr)).unwrap(); + let _ = sys.run(); + }); + + let (sys, addr) = rx.recv().unwrap(); + TestServer { + addr: addr, + thread: Some(join), + sys: sys, + } + } + + /// Get firat available unused address + pub fn unused_addr() -> net::SocketAddr { + let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); + let socket = TcpBuilder::new_v4().unwrap(); + socket.bind(&addr).unwrap(); + socket.reuse_address(true).unwrap(); + let tcp = socket.to_tcp_listener().unwrap(); + tcp.local_addr().unwrap() + } + + /// Construct test server url + pub fn url(&self, uri: &str) -> String { + if uri.starts_with('/') { + format!("http://{}{}", self.addr, uri) + } else { + format!("http://{}/{}", self.addr, uri) + } + } + + /// Stop http server + fn stop(&mut self) { + if let Some(handle) = self.thread.take() { + self.sys.send(msgs::SystemExit(0)); + let _ = handle.join(); + } + } +} + +impl Drop for TestServer { + fn drop(&mut self) { + self.stop() + } +} + + +/// Test application helper for testing request handlers. +pub struct TestApp { + app: Option>, +} + +impl TestApp { + fn new(state: S) -> TestApp { + let app = Application::with_state(state); + TestApp{app: Some(app)} + } + + /// Register handler for "/" + pub fn handler>(&mut self, handler: H) { + self.app = Some(self.app.take().unwrap().resource("/", |r| r.h(handler))); + } + + /// Register middleware + pub fn middleware(&mut self, mw: T) -> &mut TestApp + where T: Middleware + 'static + { + self.app = Some(self.app.take().unwrap().middleware(mw)); + self + } +} + +impl IntoHttpHandler for TestApp { + type Handler = HttpApplication; + + fn into_handler(mut self, settings: ServerSettings) -> HttpApplication { + self.app.take().unwrap().into_handler(settings) + } +} + +#[doc(hidden)] +impl Iterator for TestApp { + type Item = HttpApplication; + + fn next(&mut self) -> Option { + if let Some(mut app) = self.app.take() { + Some(app.finish()) + } else { + None + } + } +} + +/// Test `HttpRequest` builder +/// +/// ```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) { +/// httpcodes::HTTPOk.into() +/// } else { +/// 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); +/// } +/// ``` +pub struct TestRequest { + state: S, + version: Version, + method: Method, + uri: Uri, + headers: HeaderMap, + params: Params<'static>, + cookies: Option>>, + payload: Option, +} + +impl Default for TestRequest<()> { + + fn default() -> TestRequest<()> { + TestRequest { + state: (), + method: Method::GET, + uri: Uri::from_str("/").unwrap(), + version: Version::HTTP_11, + headers: HeaderMap::new(), + params: Params::new(), + cookies: None, + payload: None, + } + } +} + +impl TestRequest<()> { + + /// Create TestReqeust and set request uri + pub fn with_uri(path: &str) -> TestRequest<()> { + TestRequest::default().uri(path) + } + + /// Create TestReqeust and set header + pub fn with_header(key: K, value: V) -> TestRequest<()> + where HeaderName: HttpTryFrom, + HeaderValue: HttpTryFrom + { + TestRequest::default().header(key, value) + } +} + +impl TestRequest { + + /// Start HttpRequest build process with application state + pub fn with_state(state: S) -> TestRequest { + TestRequest { + state: state, + method: Method::GET, + uri: Uri::from_str("/").unwrap(), + version: Version::HTTP_11, + headers: HeaderMap::new(), + params: Params::new(), + cookies: None, + payload: None, + } + } + + /// Set HTTP version of this request + pub fn version(mut self, ver: Version) -> Self { + self.version = ver; + self + } + + /// Set HTTP method of this request + pub fn method(mut self, meth: Method) -> Self { + self.method = meth; + self + } + + /// Set HTTP Uri of this request + pub fn uri(mut self, path: &str) -> Self { + self.uri = Uri::from_str(path).unwrap(); + self + } + + /// Set a header + pub fn header(mut self, key: K, value: V) -> Self + where HeaderName: HttpTryFrom, + HeaderValue: HttpTryFrom + { + if let Ok(key) = HeaderName::try_from(key) { + if let Ok(value) = HeaderValue::try_from(value) { + self.headers.append(key, value); + return self + } + } + panic!("Can not create header"); + } + + /// Set request path pattern parameter + pub fn param(mut self, name: &'static str, value: &'static str) -> Self { + self.params.add(name, value); + self + } + + /// Complete request creation and generate `HttpRequest` instance + pub fn finish(self) -> HttpRequest { + let TestRequest { state, method, uri, version, headers, params, cookies, payload } = self; + let req = HttpRequest::new(method, uri, version, headers, payload); + req.as_mut().cookies = cookies; + req.as_mut().params = params; + let (router, _) = Router::new::("/", ServerSettings::default(), HashMap::new()); + req.with_state(Rc::new(state), router) + } + + #[cfg(test)] + /// Complete request creation and generate `HttpRequest` instance + pub(crate) fn finish_no_router(self) -> HttpRequest { + let TestRequest { state, method, uri, version, headers, params, cookies, payload } = self; + let req = HttpRequest::new(method, uri, version, headers, payload); + req.as_mut().cookies = cookies; + req.as_mut().params = params; + req.with_state_no_router(Rc::new(state)) + } + + /// This method generates `HttpRequest` instance and runs handler + /// with generated request. + /// + /// This method panics is handler returns actor or async result. + pub fn run>(self, mut h: H) -> + Result>::Result as Responder>::Error> + { + let req = self.finish(); + let resp = h.handle(req.clone()); + + match resp.respond_to(req.clone_without_state()) { + Ok(resp) => { + match resp.into().into() { + ReplyItem::Message(resp) => Ok(resp), + ReplyItem::Future(_) => panic!("Async handler is not supported."), + } + }, + Err(err) => Err(err), + } + } + + /// This method generates `HttpRequest` instance and runs handler + /// with generated request. + /// + /// This method panics is handler returns actor. + pub fn run_async(self, h: H) -> Result + where H: Fn(HttpRequest) -> F + 'static, + F: Future + 'static, + R: Responder + 'static, + E: Into + 'static + { + let req = self.finish(); + let fut = h(req.clone()); + + let mut core = Core::new().unwrap(); + match core.run(fut) { + Ok(r) => { + match r.respond_to(req.clone_without_state()) { + Ok(reply) => match reply.into().into() { + ReplyItem::Message(resp) => Ok(resp), + _ => panic!("Nested async replies are not supported"), + }, + Err(e) => Err(e), + } + }, + Err(err) => Err(err), + } + } +} diff --git a/src/worker.rs b/src/worker.rs new file mode 100644 index 000000000..7b996a430 --- /dev/null +++ b/src/worker.rs @@ -0,0 +1,263 @@ +use std::{net, time}; +use std::rc::Rc; +use std::cell::{Cell, RefCell, RefMut}; +use futures::Future; +use futures::unsync::oneshot; +use tokio_core::net::TcpStream; +use tokio_core::reactor::Handle; +use net2::TcpStreamExt; + +#[cfg(feature="tls")] +use futures::future; +#[cfg(feature="tls")] +use native_tls::TlsAcceptor; +#[cfg(feature="tls")] +use tokio_tls::TlsAcceptorExt; + +#[cfg(feature="alpn")] +use futures::future; +#[cfg(feature="alpn")] +use openssl::ssl::SslAcceptor; +#[cfg(feature="alpn")] +use tokio_openssl::SslAcceptorExt; + +use actix::*; +use actix::msgs::StopArbiter; + +use helpers; +use channel::{HttpChannel, HttpHandler, Node}; + + +#[derive(Message)] +pub(crate) struct Conn { + pub io: T, + pub peer: Option, + pub http2: bool, +} + +/// Stop worker message. Returns `true` on successful shutdown +/// and `false` if some connections still alive. +#[derive(Message)] +#[rtype(bool)] +pub(crate) struct StopWorker { + pub graceful: Option, +} + +pub(crate) struct WorkerSettings { + h: RefCell>, + enabled: bool, + keep_alive: u64, + bytes: Rc, + messages: Rc, + channels: Cell, + node: Node<()>, +} + +impl WorkerSettings { + pub(crate) fn new(h: Vec, keep_alive: Option) -> WorkerSettings { + WorkerSettings { + h: RefCell::new(h), + enabled: if let Some(ka) = keep_alive { ka > 0 } else { false }, + keep_alive: keep_alive.unwrap_or(0), + bytes: Rc::new(helpers::SharedBytesPool::new()), + messages: Rc::new(helpers::SharedMessagePool::new()), + channels: Cell::new(0), + node: Node::head(), + } + } + + pub fn head(&self) -> &Node<()> { + &self.node + } + pub fn handlers(&self) -> RefMut> { + self.h.borrow_mut() + } + pub fn keep_alive(&self) -> u64 { + self.keep_alive + } + pub fn keep_alive_enabled(&self) -> bool { + self.enabled + } + pub fn get_shared_bytes(&self) -> helpers::SharedBytes { + helpers::SharedBytes::new(self.bytes.get_bytes(), Rc::clone(&self.bytes)) + } + pub fn get_http_message(&self) -> helpers::SharedHttpMessage { + helpers::SharedHttpMessage::new(self.messages.get(), Rc::clone(&self.messages)) + } + pub fn add_channel(&self) { + self.channels.set(self.channels.get()+1); + } + pub fn remove_channel(&self) { + let num = self.channels.get(); + if num > 0 { + self.channels.set(num-1); + } else { + error!("Number of removed channels is bigger than added channel. Bug in actix-web"); + } + } +} + +/// Http worker +/// +/// Worker accepts Socket objects via unbounded channel and start requests processing. +pub(crate) struct Worker where H: HttpHandler + 'static { + settings: Rc>, + hnd: Handle, + handler: StreamHandlerType, +} + +impl Worker { + + pub(crate) fn new(h: Vec, handler: StreamHandlerType, keep_alive: Option) + -> Worker + { + Worker { + settings: Rc::new(WorkerSettings::new(h, keep_alive)), + hnd: Arbiter::handle().clone(), + handler: handler, + } + } + + fn update_time(&self, ctx: &mut Context) { + helpers::update_date(); + ctx.run_later(time::Duration::new(1, 0), |slf, ctx| slf.update_time(ctx)); + } + + fn shutdown_timeout(&self, ctx: &mut Context, + tx: oneshot::Sender, dur: time::Duration) { + // sleep for 1 second and then check again + ctx.run_later(time::Duration::new(1, 0), move |slf, ctx| { + let num = slf.settings.channels.get(); + if num == 0 { + let _ = tx.send(true); + Arbiter::arbiter().send(StopArbiter(0)); + } else if let Some(d) = dur.checked_sub(time::Duration::new(1, 0)) { + slf.shutdown_timeout(ctx, tx, d); + } else { + info!("Force shutdown http worker, {} connections", num); + slf.settings.head().traverse::(); + let _ = tx.send(false); + Arbiter::arbiter().send(StopArbiter(0)); + } + }); + } +} + +impl Actor for Worker where H: HttpHandler + 'static { + type Context = Context; + + fn started(&mut self, ctx: &mut Self::Context) { + self.update_time(ctx); + } +} + +impl Handler> for Worker + where H: HttpHandler + 'static, +{ + type Result = (); + + fn handle(&mut self, msg: Conn, _: &mut Context) + { + if !self.settings.keep_alive_enabled() && + msg.io.set_keepalive(Some(time::Duration::new(75, 0))).is_err() + { + error!("Can not set socket keep-alive option"); + } + self.handler.handle(Rc::clone(&self.settings), &self.hnd, msg); + } +} + +/// `StopWorker` message handler +impl Handler for Worker + where H: HttpHandler + 'static, +{ + type Result = Response; + + fn handle(&mut self, msg: StopWorker, ctx: &mut Context) -> Self::Result { + let num = self.settings.channels.get(); + if num == 0 { + info!("Shutting down http worker, 0 connections"); + Self::reply(Ok(true)) + } else if let Some(dur) = msg.graceful { + info!("Graceful http worker shutdown, {} connections", num); + let (tx, rx) = oneshot::channel(); + self.shutdown_timeout(ctx, tx, dur); + Self::async_reply(rx.map_err(|_| ()).actfuture()) + } else { + info!("Force shutdown http worker, {} connections", num); + self.settings.head().traverse::(); + Self::reply(Ok(false)) + } + } +} + +#[derive(Clone)] +pub(crate) enum StreamHandlerType { + Normal, + #[cfg(feature="tls")] + Tls(TlsAcceptor), + #[cfg(feature="alpn")] + Alpn(SslAcceptor), +} + +impl StreamHandlerType { + + fn handle(&mut self, + h: Rc>, + hnd: &Handle, msg: Conn) { + match *self { + StreamHandlerType::Normal => { + let _ = msg.io.set_nodelay(true); + let io = TcpStream::from_stream(msg.io, hnd) + .expect("failed to associate TCP stream"); + + hnd.spawn(HttpChannel::new(h, io, msg.peer, msg.http2)); + } + #[cfg(feature="tls")] + StreamHandlerType::Tls(ref acceptor) => { + let Conn { io, peer, http2 } = msg; + let _ = io.set_nodelay(true); + let io = TcpStream::from_stream(io, hnd) + .expect("failed to associate TCP stream"); + + hnd.spawn( + TlsAcceptorExt::accept_async(acceptor, io).then(move |res| { + match res { + Ok(io) => Arbiter::handle().spawn( + HttpChannel::new(h, io, peer, http2)), + Err(err) => + trace!("Error during handling tls connection: {}", err), + }; + future::result(Ok(())) + }) + ); + } + #[cfg(feature="alpn")] + StreamHandlerType::Alpn(ref acceptor) => { + let Conn { io, peer, .. } = msg; + let _ = io.set_nodelay(true); + let io = TcpStream::from_stream(io, hnd) + .expect("failed to associate TCP stream"); + + hnd.spawn( + SslAcceptorExt::accept_async(acceptor, io).then(move |res| { + match res { + Ok(io) => { + let http2 = if let Some(p) = io.get_ref().ssl().selected_alpn_protocol() + { + p.len() == 2 && &p == b"h2" + } else { + false + }; + Arbiter::handle().spawn(HttpChannel::new(h, io, peer, http2)); + }, + Err(err) => + trace!("Error during handling tls connection: {}", err), + }; + future::result(Ok(())) + }) + ); + } + } + } +} diff --git a/src/ws.rs b/src/ws.rs index cd9cf0059..c49cb7d4e 100644 --- a/src/ws.rs +++ b/src/ws.rs @@ -6,73 +6,62 @@ //! ## Example //! //! ```rust -//! extern crate actix; -//! extern crate actix_web; -//! +//! # extern crate actix; +//! # extern crate actix_web; //! use actix::*; //! use actix_web::*; //! //! // do websocket handshake and start actor -//! fn ws_index(req: HttpRequest) -> Result { -//! ws::start(req, WsRoute) +//! fn ws_index(req: HttpRequest) -> Result { +//! ws::start(req, Ws) //! } //! -//! // WebSocket Route -//! struct WsRoute; +//! struct Ws; //! -//! impl Actor for WsRoute { +//! impl Actor for Ws { //! type Context = HttpContext; //! } //! //! // Define Handler for ws::Message message -//! impl StreamHandler for WsRoute {} +//! impl Handler for Ws { +//! type Result = (); //! -//! impl Handler for WsRoute { -//! fn handle(&mut self, msg: ws::Message, ctx: &mut HttpContext) -//! -> Response -//! { +//! fn handle(&mut self, msg: ws::Message, ctx: &mut HttpContext) { //! 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), //! _ => (), //! } -//! Self::empty() //! } //! } -//! -//! fn main() { -//! Application::default("/") -//! .resource("/ws/", |r| r.get(ws_index)) // <- register websocket route -//! .finish(); -//! } +//! # +//! # fn main() { +//! # Application::new() +//! # .resource("/ws/", |r| r.f(ws_index)) // <- register websocket route +//! # .finish(); +//! # } //! ``` use std::vec::Vec; use http::{Method, StatusCode, header}; use bytes::BytesMut; use futures::{Async, Poll, Stream}; -use actix::{Actor, AsyncContext, ResponseType, StreamHandler}; +use actix::{Actor, AsyncContext, ResponseType, Handler}; -use body::Body; -use context::HttpContext; -use route::Reply; -use payload::Payload; +use payload::ReadAny; use error::{Error, WsHandshakeError}; +use context::HttpContext; use httprequest::HttpRequest; -use httpresponse::{ConnectionType, HttpResponse}; +use httpresponse::{ConnectionType, HttpResponse, HttpResponseBuilder}; use wsframe; use wsproto::*; pub use wsproto::CloseCode; -#[doc(hidden)] const SEC_WEBSOCKET_ACCEPT: &str = "SEC-WEBSOCKET-ACCEPT"; -#[doc(hidden)] const SEC_WEBSOCKET_KEY: &str = "SEC-WEBSOCKET-KEY"; -#[doc(hidden)] const SEC_WEBSOCKET_VERSION: &str = "SEC-WEBSOCKET-VERSION"; -// #[doc(hidden)] // const SEC_WEBSOCKET_PROTOCOL: &'static str = "SEC-WEBSOCKET-PROTOCOL"; @@ -94,17 +83,17 @@ impl ResponseType for Message { } /// Do websocket handshake and start actor -pub fn start(mut req: HttpRequest, actor: A) -> Result - where A: Actor> + StreamHandler, +pub fn start(mut req: HttpRequest, actor: A) -> Result + where A: Actor> + Handler, S: 'static { - let resp = handshake(&req)?; + let mut resp = handshake(&req)?; + let stream = WsStream::new(req.payload_mut().readany()); - let stream = WsStream::new(&mut req); let mut ctx = HttpContext::new(req, actor); - ctx.start(resp); - ctx.add_stream(stream); - Ok(ctx.into()) + ctx.add_message_stream(stream); + + Ok(resp.body(ctx)?) } /// Prepare `WebSocket` handshake response. @@ -115,7 +104,7 @@ pub fn start(mut req: HttpRequest, actor: A) -> Result // /// `protocols` is a sequence of known protocols. On successful handshake, // /// the returned response headers contain the first protocol in this list // /// which the server also knows. -pub fn handshake(req: &HttpRequest) -> Result { +pub fn handshake(req: &HttpRequest) -> Result { // WebSocket accepts only GET if *req.method() != Method::GET { return Err(WsHandshakeError::GetMethodRequired) @@ -169,21 +158,23 @@ pub fn handshake(req: &HttpRequest) -> Result(req: &mut HttpRequest) -> WsStream { - WsStream { rx: req.take_payload(), buf: BytesMut::new(), closed: false, error_sent: false } + pub fn new(payload: ReadAny) -> WsStream { + WsStream { rx: payload, + buf: BytesMut::new(), + closed: false, + error_sent: false } } } @@ -196,9 +187,9 @@ impl Stream for WsStream { if !self.closed { loop { - match self.rx.readany() { + match self.rx.poll() { Ok(Async::Ready(Some(chunk))) => { - self.buf.extend(chunk.0) + self.buf.extend_from_slice(&chunk) } Ok(Async::Ready(None)) => { done = true; @@ -217,7 +208,7 @@ impl Stream for WsStream { loop { match wsframe::Frame::parse(&mut self.buf) { Ok(Some(frame)) => { - trace!("WsFrame {}", frame); + // trace!("WsFrame {}", frame); let (_finished, opcode, payload) = frame.unpack(); match opcode { @@ -336,31 +327,30 @@ impl WsWriter { mod tests { use super::*; use std::str::FromStr; - use payload::Payload; use http::{Method, HeaderMap, Version, Uri, header}; #[test] fn test_handshake() { let req = HttpRequest::new(Method::POST, Uri::from_str("/").unwrap(), - Version::HTTP_11, HeaderMap::new(), Payload::empty()); + Version::HTTP_11, HeaderMap::new(), None); assert_eq!(WsHandshakeError::GetMethodRequired, handshake(&req).err().unwrap()); let req = HttpRequest::new(Method::GET, Uri::from_str("/").unwrap(), - Version::HTTP_11, HeaderMap::new(), Payload::empty()); + Version::HTTP_11, HeaderMap::new(), None); assert_eq!(WsHandshakeError::NoWebsocketUpgrade, handshake(&req).err().unwrap()); let mut headers = HeaderMap::new(); headers.insert(header::UPGRADE, header::HeaderValue::from_static("test")); let req = HttpRequest::new(Method::GET, Uri::from_str("/").unwrap(), - Version::HTTP_11, headers, Payload::empty()); + Version::HTTP_11, headers, None); assert_eq!(WsHandshakeError::NoWebsocketUpgrade, handshake(&req).err().unwrap()); let mut headers = HeaderMap::new(); headers.insert(header::UPGRADE, header::HeaderValue::from_static("websocket")); let req = HttpRequest::new(Method::GET, Uri::from_str("/").unwrap(), - Version::HTTP_11, headers, Payload::empty()); + Version::HTTP_11, headers, None); assert_eq!(WsHandshakeError::NoConnectionUpgrade, handshake(&req).err().unwrap()); let mut headers = HeaderMap::new(); @@ -369,7 +359,7 @@ mod tests { headers.insert(header::CONNECTION, header::HeaderValue::from_static("upgrade")); let req = HttpRequest::new(Method::GET, Uri::from_str("/").unwrap(), - Version::HTTP_11, headers, Payload::empty()); + Version::HTTP_11, headers, None); assert_eq!(WsHandshakeError::NoVersionHeader, handshake(&req).err().unwrap()); let mut headers = HeaderMap::new(); @@ -380,7 +370,7 @@ mod tests { headers.insert(SEC_WEBSOCKET_VERSION, header::HeaderValue::from_static("5")); let req = HttpRequest::new(Method::GET, Uri::from_str("/").unwrap(), - Version::HTTP_11, headers, Payload::empty()); + Version::HTTP_11, headers, None); assert_eq!(WsHandshakeError::UnsupportedVersion, handshake(&req).err().unwrap()); let mut headers = HeaderMap::new(); @@ -391,7 +381,7 @@ mod tests { headers.insert(SEC_WEBSOCKET_VERSION, header::HeaderValue::from_static("13")); let req = HttpRequest::new(Method::GET, Uri::from_str("/").unwrap(), - Version::HTTP_11, headers, Payload::empty()); + Version::HTTP_11, headers, None); assert_eq!(WsHandshakeError::BadWebsocketKey, handshake(&req).err().unwrap()); let mut headers = HeaderMap::new(); @@ -404,7 +394,8 @@ mod tests { headers.insert(SEC_WEBSOCKET_KEY, header::HeaderValue::from_static("13")); let req = HttpRequest::new(Method::GET, Uri::from_str("/").unwrap(), - Version::HTTP_11, headers, Payload::empty()); - assert_eq!(StatusCode::SWITCHING_PROTOCOLS, handshake(&req).unwrap().status()); + Version::HTTP_11, headers, None); + assert_eq!(StatusCode::SWITCHING_PROTOCOLS, + handshake(&req).unwrap().finish().unwrap().status()); } } diff --git a/tests/test_httprequest.rs b/tests/test_httprequest.rs deleted file mode 100644 index e69ee9249..000000000 --- a/tests/test_httprequest.rs +++ /dev/null @@ -1,127 +0,0 @@ -extern crate actix_web; -extern crate http; -extern crate time; - -use std::str; -use std::str::FromStr; -use actix_web::*; -use actix_web::dev::*; -use http::{header, Method, Version, HeaderMap, Uri}; - - -#[test] -fn test_debug() { - let req = HttpRequest::new( - Method::GET, Uri::from_str("/").unwrap(), Version::HTTP_11, - HeaderMap::new(), Payload::empty()); - let _ = format!("{:?}", req); -} - -#[test] -fn test_no_request_cookies() { - let mut req = HttpRequest::new( - Method::GET, Uri::from_str("/").unwrap(), - Version::HTTP_11, HeaderMap::new(), Payload::empty()); - assert!(req.cookies().is_empty()); - let _ = req.load_cookies(); - assert!(req.cookies().is_empty()); -} - -#[test] -fn test_request_cookies() { - let mut headers = HeaderMap::new(); - headers.insert(header::COOKIE, - header::HeaderValue::from_static("cookie1=value1; cookie2=value2")); - - let mut req = HttpRequest::new( - Method::GET, Uri::from_str("/").unwrap(), - Version::HTTP_11, headers, Payload::empty()); - assert!(req.cookies().is_empty()); - { - let cookies = req.load_cookies().unwrap(); - assert_eq!(cookies.len(), 2); - assert_eq!(cookies[0].name(), "cookie1"); - assert_eq!(cookies[0].value(), "value1"); - assert_eq!(cookies[1].name(), "cookie2"); - assert_eq!(cookies[1].value(), "value2"); - } - - let cookie = req.cookie("cookie1"); - assert!(cookie.is_some()); - let cookie = cookie.unwrap(); - assert_eq!(cookie.name(), "cookie1"); - assert_eq!(cookie.value(), "value1"); - - let cookie = req.cookie("cookie-unknown"); - assert!(cookie.is_none()); -} - -#[test] -fn test_no_request_range_header() { - let req = HttpRequest::new(Method::GET, Uri::from_str("/").unwrap(), - Version::HTTP_11, HeaderMap::new(), Payload::empty()); - let ranges = req.range(100).unwrap(); - assert!(ranges.is_empty()); -} - -#[test] -fn test_request_range_header() { - let mut headers = HeaderMap::new(); - headers.insert(header::RANGE, - header::HeaderValue::from_static("bytes=0-4")); - - let req = HttpRequest::new(Method::GET, Uri::from_str("/").unwrap(), - Version::HTTP_11, headers, Payload::empty()); - let ranges = req.range(100).unwrap(); - assert_eq!(ranges.len(), 1); - assert_eq!(ranges[0].start, 0); - assert_eq!(ranges[0].length, 5); -} - -#[test] -fn test_request_query() { - let req = HttpRequest::new(Method::GET, Uri::from_str("/?id=test").unwrap(), - Version::HTTP_11, HeaderMap::new(), Payload::empty()); - assert_eq!(req.query_string(), "id=test"); - let query = req.query(); - assert_eq!(&query["id"], "test"); -} - -#[test] -fn test_request_match_info() { - let mut req = HttpRequest::new(Method::GET, Uri::from_str("/value/?id=test").unwrap(), - Version::HTTP_11, HeaderMap::new(), Payload::empty()); - - let rec = RouteRecognizer::new("/".to_owned(), vec![("/{key}/".to_owned(), 1)]); - let (params, _) = rec.recognize(req.path()).unwrap(); - let params = params.unwrap(); - - req.set_match_info(params); - assert_eq!(req.match_info().get("key"), Some("value")); -} - -#[test] -fn test_chunked() { - let req = HttpRequest::new( - Method::GET, Uri::from_str("/").unwrap(), - Version::HTTP_11, HeaderMap::new(), Payload::empty()); - assert!(!req.chunked().unwrap()); - - let mut headers = HeaderMap::new(); - headers.insert(header::TRANSFER_ENCODING, - header::HeaderValue::from_static("chunked")); - let req = HttpRequest::new( - Method::GET, Uri::from_str("/").unwrap(), Version::HTTP_11, - headers, Payload::empty()); - assert!(req.chunked().unwrap()); - - let mut headers = HeaderMap::new(); - let s = unsafe{str::from_utf8_unchecked(b"some va\xadscc\xacas0xsdasdlue".as_ref())}; - - headers.insert(header::TRANSFER_ENCODING, - header::HeaderValue::from_str(s).unwrap()); - let req = HttpRequest::new( - Method::GET, Uri::from_str("/").unwrap(), - Version::HTTP_11, headers, Payload::empty()); - assert!(req.chunked().is_err()); -} diff --git a/tests/test_httpresponse.rs b/tests/test_httpresponse.rs deleted file mode 100644 index 8a239ae70..000000000 --- a/tests/test_httpresponse.rs +++ /dev/null @@ -1,41 +0,0 @@ -extern crate actix_web; -extern crate http; -extern crate time; - -use actix_web::*; -use std::str::FromStr; -use time::Duration; -use http::{header, Method, Version, HeaderMap, Uri}; - - -#[test] -fn test_response_cookies() { - let mut headers = HeaderMap::new(); - headers.insert(header::COOKIE, - header::HeaderValue::from_static("cookie1=value1; cookie2=value2")); - - let mut req = HttpRequest::new( - Method::GET, Uri::from_str("/").unwrap(), Version::HTTP_11, headers, Payload::empty()); - let cookies = req.load_cookies().unwrap(); - - let resp = httpcodes::HTTPOk - .build() - .cookie(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"); -} diff --git a/tests/test_server.rs b/tests/test_server.rs index 445536dd4..032c750f4 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -2,54 +2,57 @@ extern crate actix; extern crate actix_web; extern crate tokio_core; extern crate reqwest; +extern crate futures; -use std::{net, thread}; -use std::sync::Arc; +use std::{net, thread, time}; +use std::sync::{Arc, mpsc}; use std::sync::atomic::{AtomicUsize, Ordering}; -use tokio_core::net::TcpListener; +use futures::Future; -use actix::*; use actix_web::*; - - -fn create_server() -> HttpServer> { - HttpServer::new( - vec![Application::default("/") - .resource("/", |r| - r.handler(Method::GET, |_| httpcodes::HTTPOk)) - .finish()]) -} +use actix::System; #[test] -fn test_serve() { - thread::spawn(|| { - let sys = System::new("test"); - let srv = create_server(); - srv.serve::<_, ()>("127.0.0.1:58902").unwrap(); - sys.run(); - }); - assert!(reqwest::get("http://localhost:58902/").unwrap().status().is_success()); -} - -#[test] -fn test_serve_incoming() { - let loopback = net::Ipv4Addr::new(127, 0, 0, 1); - let socket = net::SocketAddrV4::new(loopback, 0); - let tcp = net::TcpListener::bind(socket).unwrap(); - let addr1 = tcp.local_addr().unwrap(); - let addr2 = tcp.local_addr().unwrap(); +fn test_start() { + let _ = test::TestServer::unused_addr(); + let (tx, rx) = mpsc::channel(); thread::spawn(move || { let sys = System::new("test"); + let srv = HttpServer::new( + || vec![Application::new() + .resource("/", |r| r.method(Method::GET).h(httpcodes::HTTPOk))]); - let srv = create_server(); - let tcp = TcpListener::from_listener(tcp, &addr2, Arbiter::handle()).unwrap(); - srv.serve_incoming::<_, ()>(tcp.incoming()).unwrap(); + let srv = srv.bind("127.0.0.1:0").unwrap(); + let addr = srv.addrs()[0]; + let srv_addr = srv.start(); + let _ = tx.send((addr, srv_addr)); sys.run(); }); + let (addr, srv_addr) = rx.recv().unwrap(); + assert!(reqwest::get(&format!("http://{}/", addr)).unwrap().status().is_success()); - assert!(reqwest::get(&format!("http://{}/", addr1)) - .unwrap().status().is_success()); + // pause + let _ = srv_addr.call_fut(dev::PauseServer).wait(); + thread::sleep(time::Duration::from_millis(100)); + assert!(net::TcpStream::connect(addr).is_err()); + + // resume + let _ = srv_addr.call_fut(dev::ResumeServer).wait(); + assert!(reqwest::get(&format!("http://{}/", addr)).unwrap().status().is_success()); +} + +#[test] +fn test_simple() { + let srv = test::TestServer::new(|app| app.handler(httpcodes::HTTPOk)); + assert!(reqwest::get(&srv.url("/")).unwrap().status().is_success()); +} + +#[test] +fn test_application() { + let srv = test::TestServer::with_factory( + || Application::new().resource("/", |r| r.h(httpcodes::HTTPOk))); + assert!(reqwest::get(&srv.url("/")).unwrap().status().is_success()); } struct MiddlewareTest { @@ -58,20 +61,20 @@ struct MiddlewareTest { finish: Arc, } -impl middlewares::Middleware for MiddlewareTest { - fn start(&self, _: &mut HttpRequest) -> middlewares::Started { +impl middleware::Middleware for MiddlewareTest { + fn start(&self, _: &mut HttpRequest) -> middleware::Started { self.start.store(self.start.load(Ordering::Relaxed) + 1, Ordering::Relaxed); - middlewares::Started::Done + middleware::Started::Done } - fn response(&self, _: &mut HttpRequest, resp: HttpResponse) -> middlewares::Response { + fn response(&self, _: &mut HttpRequest, resp: HttpResponse) -> middleware::Response { self.response.store(self.response.load(Ordering::Relaxed) + 1, Ordering::Relaxed); - middlewares::Response::Done(resp) + middleware::Response::Done(resp) } - fn finish(&self, _: &mut HttpRequest, _: &HttpResponse) -> middlewares::Finished { + fn finish(&self, _: &mut HttpRequest, _: &HttpResponse) -> middleware::Finished { self.finish.store(self.finish.load(Ordering::Relaxed) + 1, Ordering::Relaxed); - middlewares::Finished::Done + middleware::Finished::Done } } @@ -85,23 +88,14 @@ fn test_middlewares() { let act_num2 = Arc::clone(&num2); let act_num3 = Arc::clone(&num3); - thread::spawn(move || { - let sys = System::new("test"); - - HttpServer::new( - vec![Application::default("/") - .middleware(MiddlewareTest{start: act_num1, - response: act_num2, - finish: act_num3}) - .resource("/", |r| - r.handler(Method::GET, |_| httpcodes::HTTPOk)) - .finish()]) - .serve::<_, ()>("127.0.0.1:58904").unwrap(); - sys.run(); - }); - - assert!(reqwest::get("http://localhost:58904/").unwrap().status().is_success()); - + let srv = test::TestServer::new( + move |app| app.middleware(MiddlewareTest{start: Arc::clone(&act_num1), + response: Arc::clone(&act_num2), + finish: Arc::clone(&act_num3)}) + .handler(httpcodes::HTTPOk) + ); + + assert!(reqwest::get(&srv.url("/")).unwrap().status().is_success()); assert_eq!(num1.load(Ordering::Relaxed), 1); assert_eq!(num2.load(Ordering::Relaxed), 1); assert_eq!(num3.load(Ordering::Relaxed), 1);
  • {}