diff --git a/.travis.yml b/.travis.yml index b1b0769e1..2dea00c58 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,6 @@ cache: matrix: include: - - rust: 1.31.0 - rust: stable - rust: beta - rust: nightly-2019-04-02 @@ -43,7 +42,7 @@ script: after_success: - | if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then - cargo doc --all-features && + cargo doc --no-deps --all-features && echo "" > target/doc/index.html && git clone https://github.com/davisp/ghp-import.git && ./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://"$GH_TOKEN"@github.com/"$TRAVIS_REPO_SLUG.git" target/doc && diff --git a/CHANGES.md b/CHANGES.md index d6ff547d5..81be2a344 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,98 @@ # Changes +### Added + +* Extend `Responder` trait, allow to override status code and headers. + +* Add helper functions for reading response body `test::read_body()` + +* Added support for `remainder match` (i.e "/path/{tail}*") + + +### Changed + +* `.to_async()` handler can return `Responder` type #792 + + +### Fixed + +* Fix async web::Data factory handling + + +## [1.0.0-beta.1] - 2019-04-20 + +### Added + +* Add helper functions for reading test response body, + `test::read_response()` and test::read_response_json()` + +* Add `.peer_addr()` #744 + +* Add `NormalizePath` middleware + +### Changed + +* Rename `RouterConfig` to `ServiceConfig` + +* Rename `test::call_success` to `test::call_service` + +* Removed `ServiceRequest::from_parts()` as it is unsafe to create from parts. + +* `CookieIdentityPolicy::max_age()` accepts value in seconds + +### Fixed + +* Fixed `TestRequest::app_data()` + + +## [1.0.0-alpha.6] - 2019-04-14 + +### Changed + +* Allow to use any service as default service. + +* Remove generic type for request payload, always use default. + +* Removed `Decompress` middleware. Bytes, String, Json, Form extractors + automatically decompress payload. + +* Make extractor config type explicit. Add `FromRequest::Config` associated type. + + +## [1.0.0-alpha.5] - 2019-04-12 + +### Added + +* Added async io `TestBuffer` for testing. + +### Deleted + +* Removed native-tls support + + +## [1.0.0-alpha.4] - 2019-04-08 + +### Added + +* `App::configure()` allow to offload app configuration to different methods + +* Added `URLPath` option for logger + +* Added `ServiceRequest::app_data()`, returns `Data` + +* Added `ServiceFromRequest::app_data()`, returns `Data` + +### Changed + +* `FromRequest` trait refactoring + +* Move multipart support to actix-multipart crate + +### Fixed + +* Fix body propagation in Response::from_error. #760 + + ## [1.0.0-alpha.3] - 2019-04-02 ### Changed diff --git a/Cargo.toml b/Cargo.toml index 0e2fb32a9..f4720a1c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "1.0.0-alpha.3" +version = "1.0.0-beta.1" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" @@ -15,6 +15,9 @@ license = "MIT/Apache-2.0" exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] edition = "2018" +[package.metadata.docs.rs] +features = ["ssl", "brotli", "flate2-zlib", "secure-cookies", "client", "rust-tls"] + [badges] travis-ci = { repository = "actix/actix-web", branch = "master" } codecov = { repository = "actix/actix-web", branch = "master", service = "github" } @@ -29,15 +32,14 @@ members = [ "awc", "actix-http", "actix-files", + "actix-framed", "actix-session", + "actix-multipart", "actix-web-actors", "actix-web-codegen", "test-server", ] -[package.metadata.docs.rs] -features = ["ssl", "tls", "brotli", "flate2-zlib", "secure-cookies", "client", "rust-tls"] - [features] default = ["brotli", "flate2-zlib", "secure-cookies", "client"] @@ -56,9 +58,6 @@ flate2-rust = ["actix-http/flate2-rust"] # sessions feature, session require "ring" crate and c compiler secure-cookies = ["actix-http/secure-cookies"] -# tls -tls = ["native-tls", "actix-server/ssl"] - # openssl ssl = ["openssl", "actix-server/ssl", "awc/ssl"] @@ -66,24 +65,23 @@ ssl = ["openssl", "actix-server/ssl", "awc/ssl"] rust-tls = ["rustls", "actix-server/rust-tls"] [dependencies] -actix-codec = "0.1.1" -actix-service = "0.3.4" +actix-codec = "0.1.2" +actix-service = "0.3.6" actix-utils = "0.3.4" -actix-router = "0.1.0" +actix-router = "0.1.3" actix-rt = "0.2.2" -actix-web-codegen = "0.1.0-alpha.1" -actix-http = { version = "0.1.0-alpha.3", features=["fail"] } -actix-server = "0.4.2" -actix-server-config = "0.1.0" +actix-web-codegen = "0.1.0-beta.1" +actix-http = { version = "0.1.2", features=["fail"] } +actix-server = "0.4.3" +actix-server-config = "0.1.1" actix-threadpool = "0.1.0" -awc = { version = "0.1.0-alpha.3", optional = true } +awc = { version = "0.1.1", optional = true } bytes = "0.4" derive_more = "0.14" encoding = "0.2" futures = "0.1" -hashbrown = "0.1.8" -httparse = "1.3" +hashbrown = "0.2.2" log = "0.4" mime = "0.3" net2 = "0.2.33" @@ -91,18 +89,18 @@ parking_lot = "0.7" regex = "1.0" serde = { version = "1.0", features=["derive"] } serde_json = "1.0" -serde_urlencoded = "^0.5.3" -time = "0.1" +serde_urlencoded = "0.5.3" +time = "0.1.42" url = { version="1.7", features=["query_encoding"] } # ssl support -native-tls = { version="0.2", optional = true } openssl = { version="0.10", optional = true } rustls = { version = "^0.15", optional = true } [dev-dependencies] -actix-http = { version = "0.1.0-alpha.3", features=["ssl", "brotli", "flate2-zlib"] } -actix-http-test = { version = "0.1.0-alpha.3", features=["ssl"] } +actix-http = { version = "0.1.2", features=["ssl", "brotli", "flate2-zlib"] } +actix-http-test = { version = "0.1.1", features=["ssl"] } +actix-files = { version = "0.1.0-beta.1" } rand = "0.6" env_logger = "0.6" serde_derive = "1.0" @@ -116,7 +114,6 @@ opt-level = 3 codegen-units = 1 [patch.crates-io] -actix = { git = "https://github.com/actix/actix.git" } actix-web = { path = "." } actix-http = { path = "actix-http" } actix-http-test = { path = "test-server" } diff --git a/MIGRATION.md b/MIGRATION.md index 6b49e3e6a..9d433ed06 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,3 +1,213 @@ +## 1.0 + +* Resource registration. 1.0 version uses generalized resource + registration via `.service()` method. + + instead of + + ```rust + App.new().resource("/welcome", |r| r.f(welcome)) + ``` + + use App's or Scope's `.service()` method. `.service()` method accepts + object that implements `HttpServiceFactory` trait. By default + actix-web provides `Resource` and `Scope` services. + + ```rust + App.new().service( + web::resource("/welcome") + .route(web::get().to(welcome)) + .route(web::post().to(post_handler)) + ``` + +* Scope registration. + + instead of + + ```rust + let app = App::new().scope("/{project_id}", |scope| { + scope + .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) + .resource("/path2", |r| r.f(|_| HttpResponse::Ok())) + .resource("/path3", |r| r.f(|_| HttpResponse::MethodNotAllowed())) + }); + ``` + + use `.service()` for registration and `web::scope()` as scope object factory. + + ```rust + let app = App::new().service( + web::scope("/{project_id}") + .service(web::resource("/path1").to(|| HttpResponse::Ok())) + .service(web::resource("/path2").to(|| HttpResponse::Ok())) + .service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed())) + ); + ``` + +* `.with()`, `.with_async()` registration methods have been renamed to `.to()` and `.to_async()`. + + instead of + + ```rust + App.new().resource("/welcome", |r| r.with(welcome)) + ``` + + use `.to()` or `.to_async()` methods + + ```rust + App.new().service(web::resource("/welcome").to(welcome)) + ``` + +* Passing arguments to handler with extractors, multiple arguments are allowed + + instead of + + ```rust + fn welcome((body, req): (Bytes, HttpRequest)) -> ... { + ... + } + ``` + + use multiple arguments + + ```rust + fn welcome(body: Bytes, req: HttpRequest) -> ... { + ... + } + ``` + +* `.f()`, `.a()` and `.h()` handler registration methods have been removed. + Use `.to()` for handlers and `.to_async()` for async handlers. Handler function + must use extractors. + + instead of + + ```rust + App.new().resource("/welcome", |r| r.f(welcome)) + ``` + + use App's `to()` or `to_async()` methods + + ```rust + App.new().service(web::resource("/welcome").to(welcome)) + ``` + +* `State` is now `Data`. You register Data during the App initialization process + and then access it from handlers either using a Data extractor or using + HttpRequest's api. + + instead of + + ```rust + App.with_state(T) + ``` + + use App's `data` method + + ```rust + App.new() + .data(T) + ``` + + and either use the Data extractor within your handler + + ```rust + use actix_web::web::Data; + + fn endpoint_handler(Data)){ + ... + } + ``` + + .. or access your Data element from the HttpRequest + + ```rust + fn endpoint_handler(req: HttpRequest) { + let data: Option> = req.app_data::(); + } + ``` + + +* AsyncResponder is removed, use `.to_async()` registration method and `impl Future<>` as result type. + + instead of + + ```rust + use actix_web::AsyncResponder; + + fn endpoint_handler(...) -> impl Future{ + ... + .responder() + } + ``` + + .. simply omit AsyncResponder and the corresponding responder() finish method + + +* Middleware + + instead of + + ```rust + let app = App::new() + .middleware(middleware::Logger::default()) + ``` + + use `.wrap()` method + + ```rust + let app = App::new() + .wrap(middleware::Logger::default()) + .route("/index.html", web::get().to(index)); + ``` + +* `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()` + method have been removed. Use `Bytes`, `String`, `Form`, `Json`, `Multipart` extractors instead. + + instead if + + ```rust + fn index(req: &HttpRequest) -> Responder { + req.body() + .and_then(|body| { + ... + }) + } + + use + + ```rust + fn index(body: Bytes) -> Responder { + ... + } + ``` + +* `actix_web::server` module has been removed. To start http server use `actix_web::HttpServer` type + +* StaticFiles and NamedFile has been move to separate create. + + instead of `use actix_web::fs::StaticFile` + + use `use actix_files::Files` + + instead of `use actix_web::fs::Namedfile` + + use `use actix_files::NamedFile` + +* Multipart has been move to separate create. + + instead of `use actix_web::multipart::Multipart` + + use `use actix_multipart::Multipart` + +* Response compression is not enabled by default. + To enable, use `Compress` middleware, `App::new().wrap(Compress::default())`. + +* Session middleware moved to actix-session crate + +* Actors support have been moved to `actix-web-actors` crate + + ## 0.7.15 * The `' '` character is not percent decoded anymore before matching routes. If you need to use it in diff --git a/README.md b/README.md index 35ea0bf0e..fc8f78b86 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ Actix web is a simple, pragmatic and extremely fast web framework for Rust. ## Documentation & community resources * [User Guide](https://actix.rs/docs/) -* [API Documentation (Development)](https://actix.rs/actix-web/actix_web/) -* [API Documentation (0.7 Release)](https://docs.rs/actix-web/0.7.19/actix_web/) +* [API Documentation (1.0)](https://docs.rs/actix-web/) +* [API Documentation (0.7)](https://docs.rs/actix-web/0.7.19/actix_web/) * [Chat on gitter](https://gitter.im/actix/actix) * Cargo package: [actix-web](https://crates.io/crates/actix-web) * Minimum supported Rust version: 1.32 or later @@ -36,8 +36,7 @@ fn index(info: web::Path<(u32, String)>) -> impl Responder { fn main() -> std::io::Result<()> { HttpServer::new( || App::new().service( - web::resource("/{id}/{name}/index.html") - .route(web::get().to(index)))) + web::resource("/{id}/{name}/index.html").to(index))) .bind("127.0.0.1:8080")? .run() } diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 7c46b40f7..05a5e5805 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,10 +1,21 @@ # Changes +## [0.1.0-beta.1] - 2019-04-20 + +* Update actix-web to beta.1 + +## [0.1.0-alpha.6] - 2019-04-14 + +* Update actix-web to alpha6 + +## [0.1.0-alpha.4] - 2019-04-08 + +* Update actix-web to alpha4 + ## [0.1.0-alpha.2] - 2019-04-02 * Add default handler support - ## [0.1.0-alpha.1] - 2019-03-28 * Initial impl diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index a1044c6da..5e37fc090 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.1.0-alpha.2" +version = "0.1.0-beta.1" authors = ["Nikolay Kim "] description = "Static files support for actix web." readme = "README.md" @@ -18,7 +18,7 @@ name = "actix_files" path = "src/lib.rs" [dependencies] -actix-web = "1.0.0-alpha.3" +actix-web = "1.0.0-beta.1" actix-service = "0.3.4" bitflags = "1" bytes = "0.4" @@ -31,4 +31,4 @@ percent-encoding = "1.0" v_htmlescape = "0.4" [dev-dependencies] -actix-web = { version = "1.0.0-alpha.3", features=["ssl"] } +actix-web = { version = "1.0.0-beta.1", features=["ssl"] } diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 8404ab319..4038a5487 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -10,8 +10,8 @@ use std::{cmp, io}; use actix_service::boxed::{self, BoxedNewService, BoxedService}; use actix_service::{IntoNewService, NewService, Service}; use actix_web::dev::{ - HttpServiceFactory, Payload, ResourceDef, ServiceConfig, ServiceFromRequest, - ServiceRequest, ServiceResponse, + AppService, HttpServiceFactory, Payload, ResourceDef, ServiceRequest, + ServiceResponse, }; use actix_web::error::{BlockingError, Error, ErrorInternalServerError}; use actix_web::http::header::DispositionType; @@ -32,9 +32,8 @@ use self::error::{FilesError, UriSegmentError}; pub use crate::named::NamedFile; pub use crate::range::HttpRange; -type HttpService

= BoxedService, ServiceResponse, Error>; -type HttpNewService

= - BoxedNewService<(), ServiceRequest

, ServiceResponse, Error, ()>; +type HttpService = BoxedService; +type HttpNewService = BoxedNewService<(), ServiceRequest, ServiceResponse, Error, ()>; /// Return the MIME type associated with a filename extension (case-insensitive). /// If `ext` is empty or no associated type for the extension was found, returns @@ -225,18 +224,18 @@ type MimeOverride = Fn(&mime::Name) -> DispositionType; /// .service(fs::Files::new("/static", ".")); /// } /// ``` -pub struct Files { +pub struct Files { path: String, directory: PathBuf, index: Option, show_index: bool, - default: Rc>>>>, + default: Rc>>>, renderer: Rc, mime_override: Option>, file_flags: named::Flags, } -impl Clone for Files { +impl Clone for Files { fn clone(&self) -> Self { Self { directory: self.directory.clone(), @@ -251,13 +250,13 @@ impl Clone for Files { } } -impl Files { +impl Files { /// Create new `Files` instance for specified base directory. /// /// `File` uses `ThreadPool` for blocking filesystem operations. /// By default pool with 5x threads of available cpus is used. /// Pool size can be changed by setting ACTIX_CPU_POOL environment variable. - pub fn new>(path: &str, dir: T) -> Files { + pub fn new>(path: &str, dir: T) -> Files { let dir = dir.into().canonicalize().unwrap_or_else(|_| PathBuf::new()); if !dir.is_dir() { log::error!("Specified path is not a directory"); @@ -335,7 +334,7 @@ impl Files { where F: IntoNewService, U: NewService< - Request = ServiceRequest, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, > + 'static, @@ -349,11 +348,8 @@ impl Files { } } -impl

HttpServiceFactory

for Files

-where - P: 'static, -{ - fn register(self, config: &mut ServiceConfig

) { +impl HttpServiceFactory for Files { + fn register(self, config: &mut AppService) { if self.default.borrow().is_none() { *self.default.borrow_mut() = Some(config.default_service()); } @@ -366,11 +362,11 @@ where } } -impl NewService for Files

{ - type Request = ServiceRequest

; +impl NewService for Files { + type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; - type Service = FilesService

; + type Service = FilesService; type InitError = (); type Future = Box>; @@ -401,37 +397,36 @@ impl NewService for Files

{ } } -pub struct FilesService

{ +pub struct FilesService { directory: PathBuf, index: Option, show_index: bool, - default: Option>, + default: Option, renderer: Rc, mime_override: Option>, file_flags: named::Flags, } -impl

FilesService

{ +impl FilesService { fn handle_err( &mut self, e: io::Error, - req: HttpRequest, - payload: Payload

, + req: ServiceRequest, ) -> Either< FutureResult, Box>, > { log::debug!("Files: Failed to handle {}: {}", req.path(), e); if let Some(ref mut default) = self.default { - Either::B(default.call(ServiceRequest::from_parts(req, payload))) + default.call(req) } else { - Either::A(ok(ServiceResponse::from_err(e, req.clone()))) + Either::A(ok(req.error_response(e))) } } } -impl

Service for FilesService

{ - type Request = ServiceRequest

; +impl Service for FilesService { + type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; type Future = Either< @@ -443,18 +438,18 @@ impl

Service for FilesService

{ Ok(Async::Ready(())) } - fn call(&mut self, req: ServiceRequest

) -> Self::Future { - let (req, pl) = req.into_parts(); + fn call(&mut self, req: ServiceRequest) -> Self::Future { + // let (req, pl) = req.into_parts(); let real_path = match PathBufWrp::get_pathbuf(req.match_info().path()) { Ok(item) => item, - Err(e) => return Either::A(ok(ServiceResponse::from_err(e, req.clone()))), + Err(e) => return Either::A(ok(req.error_response(e))), }; // full filepath let path = match self.directory.join(&real_path.0).canonicalize() { Ok(path) => path, - Err(e) => return self.handle_err(e, req, pl), + Err(e) => return self.handle_err(e, req), }; if path.is_dir() { @@ -470,24 +465,26 @@ impl

Service for FilesService

{ } named_file.flags = self.file_flags; + let (req, _) = req.into_parts(); Either::A(ok(match named_file.respond_to(&req) { - Ok(item) => ServiceResponse::new(req.clone(), item), - Err(e) => ServiceResponse::from_err(e, req.clone()), + Ok(item) => ServiceResponse::new(req, item), + Err(e) => ServiceResponse::from_err(e, req), })) } - Err(e) => return self.handle_err(e, req, pl), + Err(e) => return self.handle_err(e, req), } } else if self.show_index { let dir = Directory::new(self.directory.clone(), path); + let (req, _) = req.into_parts(); let x = (self.renderer)(&dir, &req); match x { Ok(resp) => Either::A(ok(resp)), - Err(e) => return self.handle_err(e, req, pl), + Err(e) => return Either::A(ok(ServiceResponse::from_err(e, req))), } } else { Either::A(ok(ServiceResponse::from_err( FilesError::IsDirectory, - req.clone(), + req.into_parts().0, ))) } } else { @@ -500,16 +497,15 @@ impl

Service for FilesService

{ } named_file.flags = self.file_flags; + let (req, _) = req.into_parts(); match named_file.respond_to(&req) { Ok(item) => { Either::A(ok(ServiceResponse::new(req.clone(), item))) } - Err(e) => { - Either::A(ok(ServiceResponse::from_err(e, req.clone()))) - } + Err(e) => Either::A(ok(ServiceResponse::from_err(e, req))), } } - Err(e) => self.handle_err(e, req, pl), + Err(e) => self.handle_err(e, req), } } } @@ -547,12 +543,13 @@ impl PathBufWrp { } } -impl

FromRequest

for PathBufWrp { +impl FromRequest for PathBufWrp { type Error = UriSegmentError; type Future = Result; + type Config = (); - fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { - PathBufWrp::get_pathbuf(req.request().match_info().path()) + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + PathBufWrp::get_pathbuf(req.match_info().path()) } } @@ -570,6 +567,7 @@ mod tests { self, ContentDisposition, DispositionParam, DispositionType, }; use actix_web::http::{Method, StatusCode}; + use actix_web::middleware::Compress; use actix_web::test::{self, TestRequest}; use actix_web::App; @@ -777,7 +775,7 @@ mod tests { ); let request = TestRequest::get().uri("/").to_request(); - let response = test::call_success(&mut srv, request); + let response = test::call_service(&mut srv, request); assert_eq!(response.status(), StatusCode::OK); let content_disposition = response @@ -801,7 +799,7 @@ mod tests { .uri("/t%65st/Cargo.toml") .header(header::RANGE, "bytes=10-20") .to_request(); - let response = test::call_success(&mut srv, request); + let response = test::call_service(&mut srv, request); assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); // Invalid range header @@ -809,7 +807,7 @@ mod tests { .uri("/t%65st/Cargo.toml") .header(header::RANGE, "bytes=1-0") .to_request(); - let response = test::call_success(&mut srv, request); + let response = test::call_service(&mut srv, request); assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); } @@ -826,7 +824,7 @@ mod tests { .header(header::RANGE, "bytes=10-20") .to_request(); - let response = test::call_success(&mut srv, request); + let response = test::call_service(&mut srv, request); let contentrange = response .headers() .get(header::CONTENT_RANGE) @@ -841,7 +839,7 @@ mod tests { .uri("/t%65st/tests/test.binary") .header(header::RANGE, "bytes=10-5") .to_request(); - let response = test::call_success(&mut srv, request); + let response = test::call_service(&mut srv, request); let contentrange = response .headers() @@ -864,7 +862,7 @@ mod tests { .uri("/t%65st/tests/test.binary") .header(header::RANGE, "bytes=10-20") .to_request(); - let response = test::call_success(&mut srv, request); + let response = test::call_service(&mut srv, request); let contentlength = response .headers() @@ -880,7 +878,7 @@ mod tests { .uri("/t%65st/tests/test.binary") .header(header::RANGE, "bytes=10-8") .to_request(); - let response = test::call_success(&mut srv, request); + let response = test::call_service(&mut srv, request); assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); // Without range header @@ -888,7 +886,7 @@ mod tests { .uri("/t%65st/tests/test.binary") // .no_default_headers() .to_request(); - let response = test::call_success(&mut srv, request); + let response = test::call_service(&mut srv, request); let contentlength = response .headers() @@ -903,7 +901,7 @@ mod tests { let request = TestRequest::get() .uri("/t%65st/tests/test.binary") .to_request(); - let mut response = test::call_success(&mut srv, request); + let mut response = test::call_service(&mut srv, request); // with enabled compression // { @@ -934,7 +932,7 @@ mod tests { let request = TestRequest::get() .uri("/tests/test%20space.binary") .to_request(); - let mut response = test::call_success(&mut srv, request); + let mut response = test::call_service(&mut srv, request); assert_eq!(response.status(), StatusCode::OK); let bytes = @@ -965,7 +963,7 @@ mod tests { #[test] fn test_named_file_content_encoding() { - let mut srv = test::init_service(App::new().enable_encoding().service( + let mut srv = test::init_service(App::new().wrap(Compress::default()).service( web::resource("/").to(|| { NamedFile::open("Cargo.toml") .unwrap() @@ -977,16 +975,34 @@ mod tests { .uri("/") .header(header::ACCEPT_ENCODING, "gzip") .to_request(); - let res = test::call_success(&mut srv, request); + let res = test::call_service(&mut srv, request); assert_eq!(res.status(), StatusCode::OK); + assert!(!res.headers().contains_key(header::CONTENT_ENCODING)); + } + #[test] + fn test_named_file_content_encoding_gzip() { + let mut srv = test::init_service(App::new().wrap(Compress::default()).service( + web::resource("/").to(|| { + NamedFile::open("Cargo.toml") + .unwrap() + .set_content_encoding(header::ContentEncoding::Gzip) + }), + )); + + let request = TestRequest::get() + .uri("/") + .header(header::ACCEPT_ENCODING, "gzip") + .to_request(); + let res = test::call_service(&mut srv, request); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers() .get(header::CONTENT_ENCODING) .unwrap() .to_str() .unwrap(), - "identity" + "gzip" ); } @@ -1005,20 +1021,20 @@ mod tests { ); let req = TestRequest::with_uri("/missing").to_request(); - let resp = test::call_success(&mut srv, req); + let resp = test::call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let mut srv = test::init_service(App::new().service(Files::new("/", "."))); let req = TestRequest::default().to_request(); - let resp = test::call_success(&mut srv, req); + let resp = test::call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let mut srv = test::init_service( App::new().service(Files::new("/", ".").show_files_listing()), ); let req = TestRequest::with_uri("/tests").to_request(); - let mut resp = test::call_success(&mut srv, req); + let mut resp = test::call_service(&mut srv, req); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "text/html; charset=utf-8" @@ -1035,15 +1051,15 @@ mod tests { #[test] fn test_static_files_bad_directory() { - let _st: Files<()> = Files::new("/", "missing"); - let _st: Files<()> = Files::new("/", "Cargo.toml"); + let _st: Files = Files::new("/", "missing"); + let _st: Files = Files::new("/", "Cargo.toml"); } #[test] fn test_default_handler_file_missing() { let mut st = test::block_on( Files::new("/", ".") - .default_handler(|req: ServiceRequest<_>| { + .default_handler(|req: ServiceRequest| { Ok(req.into_response(HttpResponse::Ok().body("default content"))) }) .new_service(&()), @@ -1051,7 +1067,7 @@ mod tests { .unwrap(); let req = TestRequest::with_uri("/missing").to_srv_request(); - let mut resp = test::call_success(&mut st, req); + let mut resp = test::call_service(&mut st, req); assert_eq!(resp.status(), StatusCode::OK); let bytes = test::block_on(resp.take_body().fold(BytesMut::new(), |mut b, c| { diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 4ee1a3caa..c506c02f2 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -15,7 +15,7 @@ use actix_web::http::header::{ self, ContentDisposition, DispositionParam, DispositionType, }; use actix_web::http::{ContentEncoding, Method, StatusCode}; -use actix_web::middleware::encoding::BodyEncoding; +use actix_web::middleware::BodyEncoding; use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, Responder}; use crate::range::HttpRange; @@ -296,10 +296,9 @@ impl Responder for NamedFile { header::CONTENT_DISPOSITION, self.content_disposition.to_string(), ); - // TODO blocking by compressing - // if let Some(current_encoding) = self.encoding { - // resp.content_encoding(current_encoding); - // } + if let Some(current_encoding) = self.encoding { + resp.encoding(current_encoding); + } let reader = ChunkedReadFile { size: self.md.len(), offset: 0, @@ -345,7 +344,7 @@ impl Responder for NamedFile { // check last modified let not_modified = if !none_match(etag.as_ref(), req) { true - } else if req.headers().contains_key(header::IF_NONE_MATCH) { + } else if req.headers().contains_key(&header::IF_NONE_MATCH) { false } else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) = (last_modified, req.get_header()) @@ -379,7 +378,7 @@ impl Responder for NamedFile { let mut offset = 0; // check for range header - if let Some(ranges) = req.headers().get(header::RANGE) { + if let Some(ranges) = req.headers().get(&header::RANGE) { if let Ok(rangesheader) = ranges.to_str() { if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) { length = rangesvec[0].length; diff --git a/actix-framed/Cargo.toml b/actix-framed/Cargo.toml new file mode 100644 index 000000000..f0622fd90 --- /dev/null +++ b/actix-framed/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "actix-framed" +version = "0.1.0" +authors = ["Nikolay Kim "] +description = "Actix framed app server" +readme = "README.md" +keywords = ["http", "web", "framework", "async", "futures"] +homepage = "https://actix.rs" +repository = "https://github.com/actix/actix-web.git" +documentation = "https://docs.rs/actix-framed/" +categories = ["network-programming", "asynchronous", + "web-programming::http-server", + "web-programming::websocket"] +license = "MIT/Apache-2.0" +edition = "2018" +workspace =".." + +[lib] +name = "actix_framed" +path = "src/lib.rs" + +[dependencies] +actix-codec = "0.1.2" +actix-service = "0.3.6" +actix-utils = "0.3.4" +actix-router = "0.1.2" +actix-rt = "0.2.2" +actix-http = "0.1.0" + +bytes = "0.4" +futures = "0.1.25" +log = "0.4" + +[dev-dependencies] +actix-server = { version = "0.4.3", features=["ssl"] } +actix-connect = { version = "0.1.4", features=["ssl"] } +actix-http-test = { version = "0.1.0", features=["ssl"] } diff --git a/actix-framed/LICENSE-APACHE b/actix-framed/LICENSE-APACHE new file mode 100644 index 000000000..6cdf2d16c --- /dev/null +++ b/actix-framed/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017-NOW Nikolay Kim + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/actix-framed/LICENSE-MIT b/actix-framed/LICENSE-MIT new file mode 100644 index 000000000..0f80296ae --- /dev/null +++ b/actix-framed/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2017 Nikolay 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/actix-framed/README.md b/actix-framed/README.md new file mode 100644 index 000000000..f56ae145c --- /dev/null +++ b/actix-framed/README.md @@ -0,0 +1 @@ +# Framed app for actix web [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-framed)](https://crates.io/crates/actix-framed) [![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) diff --git a/actix-framed/changes.md b/actix-framed/changes.md new file mode 100644 index 000000000..9cef3c057 --- /dev/null +++ b/actix-framed/changes.md @@ -0,0 +1,10 @@ +# Changes + +## [0.1.0] - 2019-04-16 + +* Update tests + + +## [0.1.0-alpha.1] - 2019-04-12 + +* Initial release diff --git a/actix-framed/src/app.rs b/actix-framed/src/app.rs new file mode 100644 index 000000000..20bc2f771 --- /dev/null +++ b/actix-framed/src/app.rs @@ -0,0 +1,215 @@ +use std::rc::Rc; + +use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use actix_http::h1::{Codec, SendResponse}; +use actix_http::{Error, Request, Response}; +use actix_router::{Path, Router, Url}; +use actix_service::{IntoNewService, NewService, Service}; +use actix_utils::cloneable::CloneableService; +use futures::{Async, Future, Poll}; + +use crate::helpers::{BoxedHttpNewService, BoxedHttpService, HttpNewService}; +use crate::request::FramedRequest; +use crate::state::State; + +type BoxedResponse = Box>; + +pub trait HttpServiceFactory { + type Factory: NewService; + + fn path(&self) -> &str; + + fn create(self) -> Self::Factory; +} + +/// Application builder +pub struct FramedApp { + state: State, + services: Vec<(String, BoxedHttpNewService>)>, +} + +impl FramedApp { + pub fn new() -> Self { + FramedApp { + state: State::new(()), + services: Vec::new(), + } + } +} + +impl FramedApp { + pub fn with(state: S) -> FramedApp { + FramedApp { + services: Vec::new(), + state: State::new(state), + } + } + + pub fn service(mut self, factory: U) -> Self + where + U: HttpServiceFactory, + U::Factory: NewService< + Request = FramedRequest, + Response = (), + Error = Error, + InitError = (), + > + 'static, + ::Future: 'static, + ::Service: Service< + Request = FramedRequest, + Response = (), + Error = Error, + Future = Box>, + >, + { + let path = factory.path().to_string(); + self.services + .push((path, Box::new(HttpNewService::new(factory.create())))); + self + } +} + +impl IntoNewService> for FramedApp +where + T: AsyncRead + AsyncWrite + 'static, + S: 'static, +{ + fn into_new_service(self) -> FramedAppFactory { + FramedAppFactory { + state: self.state, + services: Rc::new(self.services), + } + } +} + +#[derive(Clone)] +pub struct FramedAppFactory { + state: State, + services: Rc>)>>, +} + +impl NewService for FramedAppFactory +where + T: AsyncRead + AsyncWrite + 'static, + S: 'static, +{ + type Request = (Request, Framed); + type Response = (); + type Error = Error; + type InitError = (); + type Service = CloneableService>; + type Future = CreateService; + + fn new_service(&self, _: &C) -> Self::Future { + CreateService { + fut: self + .services + .iter() + .map(|(path, service)| { + CreateServiceItem::Future( + Some(path.clone()), + service.new_service(&()), + ) + }) + .collect(), + state: self.state.clone(), + } + } +} + +#[doc(hidden)] +pub struct CreateService { + fut: Vec>, + state: State, +} + +enum CreateServiceItem { + Future( + Option, + Box>, Error = ()>>, + ), + Service(String, BoxedHttpService>), +} + +impl Future for CreateService +where + T: AsyncRead + AsyncWrite, +{ + type Item = CloneableService>; + type Error = (); + + fn poll(&mut self) -> Poll { + let mut done = true; + + // poll http services + for item in &mut self.fut { + let res = match item { + CreateServiceItem::Future(ref mut path, ref mut fut) => { + match fut.poll()? { + Async::Ready(service) => Some((path.take().unwrap(), service)), + Async::NotReady => { + done = false; + None + } + } + } + CreateServiceItem::Service(_, _) => continue, + }; + + if let Some((path, service)) = res { + *item = CreateServiceItem::Service(path, service); + } + } + + if done { + let router = self + .fut + .drain(..) + .fold(Router::build(), |mut router, item| { + match item { + CreateServiceItem::Service(path, service) => { + router.path(&path, service); + } + CreateServiceItem::Future(_, _) => unreachable!(), + } + router + }); + Ok(Async::Ready(CloneableService::new(FramedAppService { + router: router.finish(), + state: self.state.clone(), + }))) + } else { + Ok(Async::NotReady) + } + } +} + +pub struct FramedAppService { + state: State, + router: Router>>, +} + +impl Service for FramedAppService +where + T: AsyncRead + AsyncWrite, +{ + type Request = (Request, Framed); + type Response = (); + type Error = Error; + type Future = BoxedResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, (req, framed): (Request, Framed)) -> Self::Future { + let mut path = Path::new(Url::new(req.uri().clone())); + + if let Some((srv, _info)) = self.router.recognize_mut(&mut path) { + return srv.call(FramedRequest::new(req, framed, path, self.state.clone())); + } + Box::new( + SendResponse::new(framed, Response::NotFound().finish()).then(|_| Ok(())), + ) + } +} diff --git a/actix-framed/src/helpers.rs b/actix-framed/src/helpers.rs new file mode 100644 index 000000000..c2c7dbd8b --- /dev/null +++ b/actix-framed/src/helpers.rs @@ -0,0 +1,88 @@ +use actix_http::Error; +use actix_service::{NewService, Service}; +use futures::{Future, Poll}; + +pub(crate) type BoxedHttpService = Box< + Service< + Request = Req, + Response = (), + Error = Error, + Future = Box>, + >, +>; + +pub(crate) type BoxedHttpNewService = Box< + NewService< + Request = Req, + Response = (), + Error = Error, + InitError = (), + Service = BoxedHttpService, + Future = Box, Error = ()>>, + >, +>; + +pub(crate) struct HttpNewService(T); + +impl HttpNewService +where + T: NewService, + T::Response: 'static, + T::Future: 'static, + T::Service: Service>> + 'static, + ::Future: 'static, +{ + pub fn new(service: T) -> Self { + HttpNewService(service) + } +} + +impl NewService for HttpNewService +where + T: NewService, + T::Request: 'static, + T::Future: 'static, + T::Service: Service>> + 'static, + ::Future: 'static, +{ + type Request = T::Request; + type Response = (); + type Error = Error; + type InitError = (); + type Service = BoxedHttpService; + type Future = Box>; + + fn new_service(&self, _: &()) -> Self::Future { + Box::new(self.0.new_service(&()).map_err(|_| ()).and_then(|service| { + let service: BoxedHttpService<_> = Box::new(HttpServiceWrapper { service }); + Ok(service) + })) + } +} + +struct HttpServiceWrapper { + service: T, +} + +impl Service for HttpServiceWrapper +where + T: Service< + Response = (), + Future = Box>, + Error = Error, + >, + T::Request: 'static, +{ + type Request = T::Request; + type Response = (); + type Error = Error; + type Future = Box>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.service.poll_ready() + } + + fn call(&mut self, req: Self::Request) -> Self::Future { + self.service.call(req) + } +} diff --git a/actix-framed/src/lib.rs b/actix-framed/src/lib.rs new file mode 100644 index 000000000..c6405e20b --- /dev/null +++ b/actix-framed/src/lib.rs @@ -0,0 +1,16 @@ +mod app; +mod helpers; +mod request; +mod route; +mod service; +mod state; +pub mod test; + +// re-export for convinience +pub use actix_http::{http, Error, HttpMessage, Response, ResponseError}; + +pub use self::app::{FramedApp, FramedAppService}; +pub use self::request::FramedRequest; +pub use self::route::FramedRoute; +pub use self::service::{SendError, VerifyWebSockets}; +pub use self::state::State; diff --git a/actix-framed/src/request.rs b/actix-framed/src/request.rs new file mode 100644 index 000000000..bdcdd7026 --- /dev/null +++ b/actix-framed/src/request.rs @@ -0,0 +1,170 @@ +use std::cell::{Ref, RefMut}; + +use actix_codec::Framed; +use actix_http::http::{HeaderMap, Method, Uri, Version}; +use actix_http::{h1::Codec, Extensions, Request, RequestHead}; +use actix_router::{Path, Url}; + +use crate::state::State; + +pub struct FramedRequest { + req: Request, + framed: Framed, + state: State, + pub(crate) path: Path, +} + +impl FramedRequest { + pub fn new( + req: Request, + framed: Framed, + path: Path, + state: State, + ) -> Self { + Self { + req, + framed, + state, + path, + } + } +} + +impl FramedRequest { + /// Split request into a parts + pub fn into_parts(self) -> (Request, Framed, State) { + (self.req, self.framed, self.state) + } + + /// This method returns reference to the request head + #[inline] + pub fn head(&self) -> &RequestHead { + self.req.head() + } + + /// This method returns muttable reference to the request head. + /// panics if multiple references of http request exists. + #[inline] + pub fn head_mut(&mut self) -> &mut RequestHead { + self.req.head_mut() + } + + /// Shared application state + #[inline] + pub fn state(&self) -> &S { + self.state.get_ref() + } + + /// Request's uri. + #[inline] + pub fn uri(&self) -> &Uri { + &self.head().uri + } + + /// Read the Request method. + #[inline] + pub fn method(&self) -> &Method { + &self.head().method + } + + /// Read the Request Version. + #[inline] + pub fn version(&self) -> Version { + self.head().version + } + + #[inline] + /// Returns request's headers. + pub fn headers(&self) -> &HeaderMap { + &self.head().headers + } + + /// The target path of this Request. + #[inline] + pub fn path(&self) -> &str { + self.head().uri.path() + } + + /// The query string in the URL. + /// + /// E.g., id=10 + #[inline] + pub fn query_string(&self) -> &str { + if let Some(query) = self.uri().query().as_ref() { + query + } else { + "" + } + } + + /// Get a reference to the Path parameters. + /// + /// Params is a container for url parameters. + /// A variable segment is specified in the form `{identifier}`, + /// where the identifier can be used later in a request handler to + /// access the matched value for that segment. + #[inline] + pub fn match_info(&self) -> &Path { + &self.path + } + + /// Request extensions + #[inline] + pub fn extensions(&self) -> Ref { + self.head().extensions() + } + + /// Mutable reference to a the request's extensions + #[inline] + pub fn extensions_mut(&self) -> RefMut { + self.head().extensions_mut() + } +} + +#[cfg(test)] +mod tests { + use actix_http::http::{HeaderName, HeaderValue, HttpTryFrom}; + use actix_http::test::{TestBuffer, TestRequest}; + + use super::*; + + #[test] + fn test_reqest() { + let buf = TestBuffer::empty(); + let framed = Framed::new(buf, Codec::default()); + let req = TestRequest::with_uri("/index.html?q=1") + .header("content-type", "test") + .finish(); + let path = Path::new(Url::new(req.uri().clone())); + + let mut freq = FramedRequest::new(req, framed, path, State::new(10u8)); + assert_eq!(*freq.state(), 10); + assert_eq!(freq.version(), Version::HTTP_11); + assert_eq!(freq.method(), Method::GET); + assert_eq!(freq.path(), "/index.html"); + assert_eq!(freq.query_string(), "q=1"); + assert_eq!( + freq.headers() + .get("content-type") + .unwrap() + .to_str() + .unwrap(), + "test" + ); + + freq.head_mut().headers.insert( + HeaderName::try_from("x-hdr").unwrap(), + HeaderValue::from_static("test"), + ); + assert_eq!( + freq.headers().get("x-hdr").unwrap().to_str().unwrap(), + "test" + ); + + freq.extensions_mut().insert(100usize); + assert_eq!(*freq.extensions().get::().unwrap(), 100usize); + + let (_, _, state) = freq.into_parts(); + assert_eq!(*state, 10); + } +} diff --git a/actix-framed/src/route.rs b/actix-framed/src/route.rs new file mode 100644 index 000000000..c8d9d4326 --- /dev/null +++ b/actix-framed/src/route.rs @@ -0,0 +1,156 @@ +use std::fmt; +use std::marker::PhantomData; + +use actix_codec::{AsyncRead, AsyncWrite}; +use actix_http::{http::Method, Error}; +use actix_service::{NewService, Service}; +use futures::future::{ok, FutureResult}; +use futures::{Async, Future, IntoFuture, Poll}; +use log::error; + +use crate::app::HttpServiceFactory; +use crate::request::FramedRequest; + +/// 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 FramedRoute { + handler: F, + pattern: String, + methods: Vec, + state: PhantomData<(Io, S, R)>, +} + +impl FramedRoute { + pub fn new(pattern: &str) -> Self { + FramedRoute { + handler: (), + pattern: pattern.to_string(), + methods: Vec::new(), + state: PhantomData, + } + } + + pub fn get(path: &str) -> FramedRoute { + FramedRoute::new(path).method(Method::GET) + } + + pub fn post(path: &str) -> FramedRoute { + FramedRoute::new(path).method(Method::POST) + } + + pub fn put(path: &str) -> FramedRoute { + FramedRoute::new(path).method(Method::PUT) + } + + pub fn delete(path: &str) -> FramedRoute { + FramedRoute::new(path).method(Method::DELETE) + } + + pub fn method(mut self, method: Method) -> Self { + self.methods.push(method); + self + } + + pub fn to(self, handler: F) -> FramedRoute + where + F: FnMut(FramedRequest) -> R, + R: IntoFuture, + R::Future: 'static, + R::Error: fmt::Debug, + { + FramedRoute { + handler, + pattern: self.pattern, + methods: self.methods, + state: PhantomData, + } + } +} + +impl HttpServiceFactory for FramedRoute +where + Io: AsyncRead + AsyncWrite + 'static, + F: FnMut(FramedRequest) -> R + Clone, + R: IntoFuture, + R::Future: 'static, + R::Error: fmt::Display, +{ + type Factory = FramedRouteFactory; + + fn path(&self) -> &str { + &self.pattern + } + + fn create(self) -> Self::Factory { + FramedRouteFactory { + handler: self.handler, + methods: self.methods, + _t: PhantomData, + } + } +} + +pub struct FramedRouteFactory { + handler: F, + methods: Vec, + _t: PhantomData<(Io, S, R)>, +} + +impl NewService for FramedRouteFactory +where + Io: AsyncRead + AsyncWrite + 'static, + F: FnMut(FramedRequest) -> R + Clone, + R: IntoFuture, + R::Future: 'static, + R::Error: fmt::Display, +{ + type Request = FramedRequest; + type Response = (); + type Error = Error; + type InitError = (); + type Service = FramedRouteService; + type Future = FutureResult; + + fn new_service(&self, _: &()) -> Self::Future { + ok(FramedRouteService { + handler: self.handler.clone(), + methods: self.methods.clone(), + _t: PhantomData, + }) + } +} + +pub struct FramedRouteService { + handler: F, + methods: Vec, + _t: PhantomData<(Io, S, R)>, +} + +impl Service for FramedRouteService +where + Io: AsyncRead + AsyncWrite + 'static, + F: FnMut(FramedRequest) -> R + Clone, + R: IntoFuture, + R::Future: 'static, + R::Error: fmt::Display, +{ + type Request = FramedRequest; + type Response = (); + type Error = Error; + type Future = Box>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, req: FramedRequest) -> Self::Future { + Box::new((self.handler)(req).into_future().then(|res| { + if let Err(e) = res { + error!("Error in request handler: {}", e); + } + Ok(()) + })) + } +} diff --git a/actix-framed/src/service.rs b/actix-framed/src/service.rs new file mode 100644 index 000000000..6e5c7a543 --- /dev/null +++ b/actix-framed/src/service.rs @@ -0,0 +1,147 @@ +use std::marker::PhantomData; + +use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use actix_http::body::BodySize; +use actix_http::error::ResponseError; +use actix_http::h1::{Codec, Message}; +use actix_http::ws::{verify_handshake, HandshakeError}; +use actix_http::{Request, Response}; +use actix_service::{NewService, Service}; +use futures::future::{ok, Either, FutureResult}; +use futures::{Async, Future, IntoFuture, Poll, Sink}; + +/// Service that verifies incoming request if it is valid websocket +/// upgrade request. In case of error returns `HandshakeError` +pub struct VerifyWebSockets { + _t: PhantomData, +} + +impl Default for VerifyWebSockets { + fn default() -> Self { + VerifyWebSockets { _t: PhantomData } + } +} + +impl NewService for VerifyWebSockets { + type Request = (Request, Framed); + type Response = (Request, Framed); + type Error = (HandshakeError, Framed); + type InitError = (); + type Service = VerifyWebSockets; + type Future = FutureResult; + + fn new_service(&self, _: &C) -> Self::Future { + ok(VerifyWebSockets { _t: PhantomData }) + } +} + +impl Service for VerifyWebSockets { + type Request = (Request, Framed); + type Response = (Request, Framed); + type Error = (HandshakeError, Framed); + type Future = FutureResult; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, (req, framed): (Request, Framed)) -> Self::Future { + match verify_handshake(req.head()) { + Err(e) => Err((e, framed)).into_future(), + Ok(_) => Ok((req, framed)).into_future(), + } + } +} + +/// Send http/1 error response +pub struct SendError(PhantomData<(T, R, E)>); + +impl Default for SendError +where + T: AsyncRead + AsyncWrite, + E: ResponseError, +{ + fn default() -> Self { + SendError(PhantomData) + } +} + +impl NewService for SendError +where + T: AsyncRead + AsyncWrite + 'static, + R: 'static, + E: ResponseError + 'static, +{ + type Request = Result)>; + type Response = R; + type Error = (E, Framed); + type InitError = (); + type Service = SendError; + type Future = FutureResult; + + fn new_service(&self, _: &C) -> Self::Future { + ok(SendError(PhantomData)) + } +} + +impl Service for SendError +where + T: AsyncRead + AsyncWrite + 'static, + R: 'static, + E: ResponseError + 'static, +{ + type Request = Result)>; + type Response = R; + type Error = (E, Framed); + type Future = Either)>, SendErrorFut>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, req: Result)>) -> Self::Future { + match req { + Ok(r) => Either::A(ok(r)), + Err((e, framed)) => { + let res = e.error_response().drop_body(); + Either::B(SendErrorFut { + framed: Some(framed), + res: Some((res, BodySize::Empty).into()), + err: Some(e), + _t: PhantomData, + }) + } + } + } +} + +pub struct SendErrorFut { + res: Option, BodySize)>>, + framed: Option>, + err: Option, + _t: PhantomData, +} + +impl Future for SendErrorFut +where + E: ResponseError, + T: AsyncRead + AsyncWrite, +{ + type Item = R; + type Error = (E, Framed); + + fn poll(&mut self) -> Poll { + if let Some(res) = self.res.take() { + if self.framed.as_mut().unwrap().force_send(res).is_err() { + return Err((self.err.take().unwrap(), self.framed.take().unwrap())); + } + } + match self.framed.as_mut().unwrap().poll_complete() { + Ok(Async::Ready(_)) => { + Err((self.err.take().unwrap(), self.framed.take().unwrap())) + } + Ok(Async::NotReady) => Ok(Async::NotReady), + Err(_) => Err((self.err.take().unwrap(), self.framed.take().unwrap())), + } + } +} diff --git a/actix-framed/src/state.rs b/actix-framed/src/state.rs new file mode 100644 index 000000000..600a639ca --- /dev/null +++ b/actix-framed/src/state.rs @@ -0,0 +1,29 @@ +use std::ops::Deref; +use std::sync::Arc; + +/// Application state +pub struct State(Arc); + +impl State { + pub fn new(state: S) -> State { + State(Arc::new(state)) + } + + pub fn get_ref(&self) -> &S { + self.0.as_ref() + } +} + +impl Deref for State { + type Target = S; + + fn deref(&self) -> &S { + self.0.as_ref() + } +} + +impl Clone for State { + fn clone(&self) -> State { + State(self.0.clone()) + } +} diff --git a/actix-framed/src/test.rs b/actix-framed/src/test.rs new file mode 100644 index 000000000..3bc828df4 --- /dev/null +++ b/actix-framed/src/test.rs @@ -0,0 +1,153 @@ +//! Various helpers for Actix applications to use during testing. +use actix_codec::Framed; +use actix_http::h1::Codec; +use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; +use actix_http::http::{HttpTryFrom, Method, Uri, Version}; +use actix_http::test::{TestBuffer, TestRequest as HttpTestRequest}; +use actix_router::{Path, Url}; +use actix_rt::Runtime; +use futures::IntoFuture; + +use crate::{FramedRequest, State}; + +/// Test `Request` builder. +pub struct TestRequest { + req: HttpTestRequest, + path: Path, + state: State, +} + +impl Default for TestRequest<()> { + fn default() -> TestRequest { + TestRequest { + req: HttpTestRequest::default(), + path: Path::new(Url::new(Uri::default())), + state: State::new(()), + } + } +} + +impl TestRequest<()> { + /// Create TestRequest and set request uri + pub fn with_uri(path: &str) -> Self { + Self::get().uri(path) + } + + /// Create TestRequest and set header + pub fn with_hdr(hdr: H) -> Self { + Self::default().set(hdr) + } + + /// Create TestRequest and set header + pub fn with_header(key: K, value: V) -> Self + where + HeaderName: HttpTryFrom, + V: IntoHeaderValue, + { + Self::default().header(key, value) + } + + /// Create TestRequest and set method to `Method::GET` + pub fn get() -> Self { + Self::default().method(Method::GET) + } + + /// Create TestRequest and set method to `Method::POST` + pub fn post() -> Self { + Self::default().method(Method::POST) + } +} + +impl TestRequest { + /// Create TestRequest and set request uri + pub fn with_state(state: S) -> TestRequest { + let req = TestRequest::get(); + TestRequest { + state: State::new(state), + req: req.req, + path: req.path, + } + } + + /// Set HTTP version of this request + pub fn version(mut self, ver: Version) -> Self { + self.req.version(ver); + self + } + + /// Set HTTP method of this request + pub fn method(mut self, meth: Method) -> Self { + self.req.method(meth); + self + } + + /// Set HTTP Uri of this request + pub fn uri(mut self, path: &str) -> Self { + self.req.uri(path); + self + } + + /// Set a header + pub fn set(mut self, hdr: H) -> Self { + self.req.set(hdr); + self + } + + /// Set a header + pub fn header(mut self, key: K, value: V) -> Self + where + HeaderName: HttpTryFrom, + V: IntoHeaderValue, + { + self.req.header(key, value); + self + } + + /// Set request path pattern parameter + pub fn param(mut self, name: &'static str, value: &'static str) -> Self { + self.path.add_static(name, value); + self + } + + /// Complete request creation and generate `Request` instance + pub fn finish(mut self) -> FramedRequest { + let req = self.req.finish(); + self.path.get_mut().update(req.uri()); + let framed = Framed::new(TestBuffer::empty(), Codec::default()); + FramedRequest::new(req, framed, self.path, self.state) + } + + /// This method generates `FramedRequest` instance and executes async handler + pub fn run(self, f: F) -> Result + where + F: FnOnce(FramedRequest) -> R, + R: IntoFuture, + { + let mut rt = Runtime::new().unwrap(); + rt.block_on(f(self.finish()).into_future()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test() { + let req = TestRequest::with_uri("/index.html") + .header("x-test", "test") + .param("test", "123") + .finish(); + + assert_eq!(*req.state(), ()); + assert_eq!(req.version(), Version::HTTP_11); + assert_eq!(req.method(), Method::GET); + assert_eq!(req.path(), "/index.html"); + assert_eq!(req.query_string(), ""); + assert_eq!( + req.headers().get("x-test").unwrap().to_str().unwrap(), + "test" + ); + assert_eq!(&req.match_info()["test"], "123"); + } +} diff --git a/actix-framed/tests/test_server.rs b/actix-framed/tests/test_server.rs new file mode 100644 index 000000000..00f6a97d8 --- /dev/null +++ b/actix-framed/tests/test_server.rs @@ -0,0 +1,141 @@ +use actix_codec::{AsyncRead, AsyncWrite}; +use actix_http::{body, http::StatusCode, ws, Error, HttpService, Response}; +use actix_http_test::TestServer; +use actix_service::{IntoNewService, NewService}; +use actix_utils::framed::FramedTransport; +use bytes::{Bytes, BytesMut}; +use futures::future::{self, ok}; +use futures::{Future, Sink, Stream}; + +use actix_framed::{FramedApp, FramedRequest, FramedRoute, SendError, VerifyWebSockets}; + +fn ws_service( + req: FramedRequest, +) -> impl Future { + let (req, framed, _) = req.into_parts(); + let res = ws::handshake(req.head()).unwrap().message_body(()); + + framed + .send((res, body::BodySize::None).into()) + .map_err(|_| panic!()) + .and_then(|framed| { + FramedTransport::new(framed.into_framed(ws::Codec::new()), service) + .map_err(|_| panic!()) + }) +} + +fn service(msg: ws::Frame) -> impl Future { + let msg = match msg { + ws::Frame::Ping(msg) => ws::Message::Pong(msg), + ws::Frame::Text(text) => { + ws::Message::Text(String::from_utf8_lossy(&text.unwrap()).to_string()) + } + ws::Frame::Binary(bin) => ws::Message::Binary(bin.unwrap().freeze()), + ws::Frame::Close(reason) => ws::Message::Close(reason), + _ => panic!(), + }; + ok(msg) +} + +#[test] +fn test_simple() { + let mut srv = TestServer::new(|| { + HttpService::build() + .upgrade( + FramedApp::new().service(FramedRoute::get("/index.html").to(ws_service)), + ) + .finish(|_| future::ok::<_, Error>(Response::NotFound())) + }); + + assert!(srv.ws_at("/test").is_err()); + + // client service + let framed = srv.ws_at("/index.html").unwrap(); + let framed = srv + .block_on(framed.send(ws::Message::Text("text".to_string()))) + .unwrap(); + let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!(item, Some(ws::Frame::Text(Some(BytesMut::from("text"))))); + + let framed = srv + .block_on(framed.send(ws::Message::Binary("text".into()))) + .unwrap(); + let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!( + item, + Some(ws::Frame::Binary(Some(Bytes::from_static(b"text").into()))) + ); + + let framed = srv + .block_on(framed.send(ws::Message::Ping("text".into()))) + .unwrap(); + let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!(item, Some(ws::Frame::Pong("text".to_string().into()))); + + let framed = srv + .block_on(framed.send(ws::Message::Close(Some(ws::CloseCode::Normal.into())))) + .unwrap(); + + let (item, _framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!( + item, + Some(ws::Frame::Close(Some(ws::CloseCode::Normal.into()))) + ); +} + +#[test] +fn test_service() { + let mut srv = TestServer::new(|| { + actix_http::h1::OneRequest::new().map_err(|_| ()).and_then( + VerifyWebSockets::default() + .then(SendError::default()) + .map_err(|_| ()) + .and_then( + FramedApp::new() + .service(FramedRoute::get("/index.html").to(ws_service)) + .into_new_service() + .map_err(|_| ()), + ), + ) + }); + + // non ws request + let res = srv.block_on(srv.get("/index.html").send()).unwrap(); + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + + // not found + assert!(srv.ws_at("/test").is_err()); + + // client service + let framed = srv.ws_at("/index.html").unwrap(); + let framed = srv + .block_on(framed.send(ws::Message::Text("text".to_string()))) + .unwrap(); + let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!(item, Some(ws::Frame::Text(Some(BytesMut::from("text"))))); + + let framed = srv + .block_on(framed.send(ws::Message::Binary("text".into()))) + .unwrap(); + let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!( + item, + Some(ws::Frame::Binary(Some(Bytes::from_static(b"text").into()))) + ); + + let framed = srv + .block_on(framed.send(ws::Message::Ping("text".into()))) + .unwrap(); + let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!(item, Some(ws::Frame::Pong("text".to_string().into()))); + + let framed = srv + .block_on(framed.send(ws::Message::Close(Some(ws::CloseCode::Normal.into())))) + .unwrap(); + + let (item, _framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!( + item, + Some(ws::Frame::Close(Some(ws::CloseCode::Normal.into()))) + ); +} diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 79995b77f..37d0eec65 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,5 +1,81 @@ # Changes +## [0.1.3] - 2019-04-23 + +### Fixed + +* Fix http client pool management + +* Fix http client wait queue management #794 + + +## [0.1.2] - 2019-04-23 + +### Fixed + +* Fix BorrowMutError panic in client connector #793 + + +## [0.1.1] - 2019-04-19 + +### Changes + +* Cookie::max_age() accepts value in seconds + +* Cookie::max_age_time() accepts value in time::Duration + +* Allow to specify server address for client connector + + +## [0.1.0] - 2019-04-16 + +### Added + +* Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr` + +### Changed + +* `actix_http::encoding` always available + +* use trust-dns-resolver 0.11.0 + + +## [0.1.0-alpha.5] - 2019-04-12 + +### Added + +* Allow to use custom service for upgrade requests + +* Added `h1::SendResponse` future. + +### Changed + +* MessageBody::length() renamed to MessageBody::size() for consistency + +* ws handshake verification functions take RequestHead instead of Request + + +## [0.1.0-alpha.4] - 2019-04-08 + +### Added + +* Allow to use custom `Expect` handler + +* Add minimal `std::error::Error` impl for `Error` + +### Changed + +* Export IntoHeaderValue + +* Render error and return as response body + +* Use thread pool for response body comression + +### Deleted + +* Removed PayloadBuffer + + ## [0.1.0-alpha.3] - 2019-04-02 ### Added diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 9d4e15216..438754b3f 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "actix-http" -version = "0.1.0-alpha.3" +version = "0.1.3" authors = ["Nikolay Kim "] description = "Actix http primitives" readme = "README.md" keywords = ["actix", "http", "framework", "async", "futures"] homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-http.git" +repository = "https://github.com/actix/actix-web.git" documentation = "https://docs.rs/actix-http/" categories = ["network-programming", "asynchronous", "web-programming::http-server", @@ -18,10 +18,6 @@ workspace = ".." [package.metadata.docs.rs] features = ["ssl", "fail", "brotli", "flate2-zlib", "secure-cookies"] -[badges] -travis-ci = { repository = "actix/actix-web", branch = "master" } -codecov = { repository = "actix/actix-web", branch = "master", service = "github" } - [lib] name = "actix_http" path = "src/lib.rs" @@ -48,23 +44,25 @@ fail = ["failure"] secure-cookies = ["ring"] [dependencies] -actix-service = "0.3.4" +actix-service = "0.3.6" actix-codec = "0.1.2" -actix-connect = "0.1.0" -actix-utils = "0.3.4" -actix-server-config = "0.1.0" +actix-connect = "0.1.5" +actix-utils = "0.3.5" +actix-server-config = "0.1.1" actix-threadpool = "0.1.0" base64 = "0.10" bitflags = "1.0" bytes = "0.4" byteorder = "1.2" +copyless = "0.1.2" derive_more = "0.14" +either = "1.5.2" encoding = "0.2" futures = "0.1" -hashbrown = "0.1.8" +hashbrown = "0.2.2" h2 = "0.1.16" -http = "0.1.16" +http = "0.1.17" httparse = "1.3" indexmap = "1.0" lazy_static = "1.0" @@ -78,29 +76,30 @@ serde = "1.0" serde_json = "1.0" sha1 = "0.6" slab = "0.4" -serde_urlencoded = "0.5.3" -time = "0.1" +serde_urlencoded = "0.5.5" +time = "0.1.42" tokio-tcp = "0.1.3" -tokio-timer = "0.2" +tokio-timer = "0.2.8" tokio-current-thread = "0.1" -trust-dns-resolver = { version="0.11.0-alpha.2", default-features = false } +trust-dns-resolver = { version="0.11.0", default-features = false } # for secure cookie ring = { version = "0.14.6", optional = true } # compression -brotli2 = { version="^0.3.2", optional = true } -flate2 = { version="^1.0.2", optional = true, default-features = false } +brotli2 = { version="0.3.2", optional = true } +flate2 = { version="1.0.7", optional = true, default-features = false } # optional deps failure = { version = "0.1.5", optional = true } openssl = { version="0.10", optional = true } +chrono = "0.4.6" [dev-dependencies] actix-rt = "0.2.2" -actix-server = { version = "0.4.1", features=["ssl"] } -actix-connect = { version = "0.1.0", features=["ssl"] } -actix-http-test = { version = "0.1.0-alpha.3", features=["ssl"] } +actix-server = { version = "0.4.3", features=["ssl"] } +actix-connect = { version = "0.1.4", features=["ssl"] } +actix-http-test = { version = "0.1.0", features=["ssl"] } env_logger = "0.6" serde_derive = "1.0" openssl = { version="0.10" } diff --git a/actix-http/README.md b/actix-http/README.md index 467e67a9d..d75e822ba 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -1,4 +1,4 @@ -# Actix http [![Build Status](https://travis-ci.org/actix/actix-http.svg?branch=master)](https://travis-ci.org/actix/actix-http) [![codecov](https://codecov.io/gh/actix/actix-http/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-http) [![crates.io](https://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 http [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-http)](https://crates.io/crates/actix-http) [![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 http @@ -8,7 +8,7 @@ Actix http * [API Documentation](https://docs.rs/actix-http/) * [Chat on gitter](https://gitter.im/actix/actix) * Cargo package: [actix-http](https://crates.io/crates/actix-http) -* Minimum supported Rust version: 1.26 or later +* Minimum supported Rust version: 1.31 or later ## Example diff --git a/actix-http/examples/framed_hello.rs b/actix-http/examples/framed_hello.rs deleted file mode 100644 index 7d4c13d34..000000000 --- a/actix-http/examples/framed_hello.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::{env, io}; - -use actix_codec::Framed; -use actix_http::{h1, Response, SendResponse, ServiceConfig}; -use actix_server::{Io, Server}; -use actix_service::{fn_service, NewService}; -use actix_utils::framed::IntoFramed; -use actix_utils::stream::TakeItem; -use futures::Future; -use tokio_tcp::TcpStream; - -fn main() -> io::Result<()> { - env::set_var("RUST_LOG", "framed_hello=info"); - env_logger::init(); - - Server::build() - .bind("framed_hello", "127.0.0.1:8080", || { - fn_service(|io: Io| Ok(io.into_parts().0)) - .and_then(IntoFramed::new(|| h1::Codec::new(ServiceConfig::default()))) - .and_then(TakeItem::new().map_err(|_| ())) - .and_then(|(_req, _framed): (_, Framed<_, _>)| { - SendResponse::send(_framed, Response::Ok().body("Hello world!")) - .map_err(|_| ()) - .map(|_| ()) - }) - })? - .run() -} diff --git a/actix-http/src/body.rs b/actix-http/src/body.rs index 85717ba85..0652dd274 100644 --- a/actix-http/src/body.rs +++ b/actix-http/src/body.rs @@ -30,13 +30,13 @@ impl BodySize { /// Type that provides this trait can be streamed to a peer. pub trait MessageBody { - fn length(&self) -> BodySize; + fn size(&self) -> BodySize; fn poll_next(&mut self) -> Poll, Error>; } impl MessageBody for () { - fn length(&self) -> BodySize { + fn size(&self) -> BodySize { BodySize::Empty } @@ -46,8 +46,8 @@ impl MessageBody for () { } impl MessageBody for Box { - fn length(&self) -> BodySize { - self.as_ref().length() + fn size(&self) -> BodySize { + self.as_ref().size() } fn poll_next(&mut self) -> Poll, Error> { @@ -86,10 +86,10 @@ impl ResponseBody { } impl MessageBody for ResponseBody { - fn length(&self) -> BodySize { + fn size(&self) -> BodySize { match self { - ResponseBody::Body(ref body) => body.length(), - ResponseBody::Other(ref body) => body.length(), + ResponseBody::Body(ref body) => body.size(), + ResponseBody::Other(ref body) => body.size(), } } @@ -135,12 +135,12 @@ impl Body { } impl MessageBody for Body { - fn length(&self) -> BodySize { + fn size(&self) -> BodySize { match self { Body::None => BodySize::None, Body::Empty => BodySize::Empty, Body::Bytes(ref bin) => BodySize::Sized(bin.len()), - Body::Message(ref body) => body.length(), + Body::Message(ref body) => body.size(), } } @@ -153,7 +153,7 @@ impl MessageBody for Body { if len == 0 { Ok(Async::Ready(None)) } else { - Ok(Async::Ready(Some(bin.split_to(len)))) + Ok(Async::Ready(Some(mem::replace(bin, Bytes::new())))) } } Body::Message(ref mut body) => body.poll_next(), @@ -185,7 +185,7 @@ impl fmt::Debug for Body { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { Body::None => write!(f, "Body::None"), - Body::Empty => write!(f, "Body::Zero"), + Body::Empty => write!(f, "Body::Empty"), Body::Bytes(ref b) => write!(f, "Body::Bytes({:?})", b), Body::Message(_) => write!(f, "Body::Message(_)"), } @@ -235,7 +235,7 @@ impl From for Body { } impl MessageBody for Bytes { - fn length(&self) -> BodySize { + fn size(&self) -> BodySize { BodySize::Sized(self.len()) } @@ -249,7 +249,7 @@ impl MessageBody for Bytes { } impl MessageBody for BytesMut { - fn length(&self) -> BodySize { + fn size(&self) -> BodySize { BodySize::Sized(self.len()) } @@ -265,7 +265,7 @@ impl MessageBody for BytesMut { } impl MessageBody for &'static str { - fn length(&self) -> BodySize { + fn size(&self) -> BodySize { BodySize::Sized(self.len()) } @@ -281,7 +281,7 @@ impl MessageBody for &'static str { } impl MessageBody for &'static [u8] { - fn length(&self) -> BodySize { + fn size(&self) -> BodySize { BodySize::Sized(self.len()) } @@ -297,7 +297,7 @@ impl MessageBody for &'static [u8] { } impl MessageBody for Vec { - fn length(&self) -> BodySize { + fn size(&self) -> BodySize { BodySize::Sized(self.len()) } @@ -314,7 +314,7 @@ impl MessageBody for Vec { } impl MessageBody for String { - fn length(&self) -> BodySize { + fn size(&self) -> BodySize { BodySize::Sized(self.len()) } @@ -354,12 +354,12 @@ where S: Stream, E: Into, { - fn length(&self) -> BodySize { + fn size(&self) -> BodySize { BodySize::Stream } fn poll_next(&mut self) -> Poll, Error> { - self.stream.poll().map_err(|e| e.into()) + self.stream.poll().map_err(std::convert::Into::into) } } @@ -383,7 +383,7 @@ impl MessageBody for SizedStream where S: Stream, { - fn length(&self) -> BodySize { + fn size(&self) -> BodySize { BodySize::Sized(self.size) } @@ -416,47 +416,117 @@ mod tests { #[test] fn test_static_str() { - assert_eq!(Body::from("").length(), BodySize::Sized(0)); - assert_eq!(Body::from("test").length(), BodySize::Sized(4)); + assert_eq!(Body::from("").size(), BodySize::Sized(0)); + assert_eq!(Body::from("test").size(), BodySize::Sized(4)); assert_eq!(Body::from("test").get_ref(), b"test"); + + assert_eq!("test".size(), BodySize::Sized(4)); + assert_eq!( + "test".poll_next().unwrap(), + Async::Ready(Some(Bytes::from("test"))) + ); } #[test] fn test_static_bytes() { - assert_eq!(Body::from(b"test".as_ref()).length(), BodySize::Sized(4)); + assert_eq!(Body::from(b"test".as_ref()).size(), BodySize::Sized(4)); assert_eq!(Body::from(b"test".as_ref()).get_ref(), b"test"); assert_eq!( - Body::from_slice(b"test".as_ref()).length(), + Body::from_slice(b"test".as_ref()).size(), BodySize::Sized(4) ); assert_eq!(Body::from_slice(b"test".as_ref()).get_ref(), b"test"); + + assert_eq!((&b"test"[..]).size(), BodySize::Sized(4)); + assert_eq!( + (&b"test"[..]).poll_next().unwrap(), + Async::Ready(Some(Bytes::from("test"))) + ); } #[test] fn test_vec() { - assert_eq!(Body::from(Vec::from("test")).length(), BodySize::Sized(4)); + assert_eq!(Body::from(Vec::from("test")).size(), BodySize::Sized(4)); assert_eq!(Body::from(Vec::from("test")).get_ref(), b"test"); + + assert_eq!(Vec::from("test").size(), BodySize::Sized(4)); + assert_eq!( + Vec::from("test").poll_next().unwrap(), + Async::Ready(Some(Bytes::from("test"))) + ); } #[test] fn test_bytes() { - assert_eq!(Body::from(Bytes::from("test")).length(), BodySize::Sized(4)); - assert_eq!(Body::from(Bytes::from("test")).get_ref(), b"test"); - } - - #[test] - fn test_string() { - let b = "test".to_owned(); - assert_eq!(Body::from(b.clone()).length(), BodySize::Sized(4)); + let mut b = Bytes::from("test"); + assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4)); assert_eq!(Body::from(b.clone()).get_ref(), b"test"); - assert_eq!(Body::from(&b).length(), BodySize::Sized(4)); - assert_eq!(Body::from(&b).get_ref(), b"test"); + + assert_eq!(b.size(), BodySize::Sized(4)); + assert_eq!( + b.poll_next().unwrap(), + Async::Ready(Some(Bytes::from("test"))) + ); } #[test] fn test_bytes_mut() { - let b = BytesMut::from("test"); - assert_eq!(Body::from(b.clone()).length(), BodySize::Sized(4)); - assert_eq!(Body::from(b).get_ref(), b"test"); + let mut b = BytesMut::from("test"); + assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4)); + assert_eq!(Body::from(b.clone()).get_ref(), b"test"); + + assert_eq!(b.size(), BodySize::Sized(4)); + assert_eq!( + b.poll_next().unwrap(), + Async::Ready(Some(Bytes::from("test"))) + ); + } + + #[test] + fn test_string() { + let mut b = "test".to_owned(); + assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4)); + assert_eq!(Body::from(b.clone()).get_ref(), b"test"); + assert_eq!(Body::from(&b).size(), BodySize::Sized(4)); + assert_eq!(Body::from(&b).get_ref(), b"test"); + + assert_eq!(b.size(), BodySize::Sized(4)); + assert_eq!( + b.poll_next().unwrap(), + Async::Ready(Some(Bytes::from("test"))) + ); + } + + #[test] + fn test_unit() { + assert_eq!(().size(), BodySize::Empty); + assert_eq!(().poll_next().unwrap(), Async::Ready(None)); + } + + #[test] + fn test_box() { + let mut val = Box::new(()); + assert_eq!(val.size(), BodySize::Empty); + assert_eq!(val.poll_next().unwrap(), Async::Ready(None)); + } + + #[test] + fn test_body_eq() { + assert!(Body::None == Body::None); + assert!(Body::None != Body::Empty); + assert!(Body::Empty == Body::Empty); + assert!(Body::Empty != Body::None); + assert!( + Body::Bytes(Bytes::from_static(b"1")) + == Body::Bytes(Bytes::from_static(b"1")) + ); + assert!(Body::Bytes(Bytes::from_static(b"1")) != Body::None); + } + + #[test] + fn test_body_debug() { + assert!(format!("{:?}", Body::None).contains("Body::None")); + assert!(format!("{:?}", Body::Empty).contains("Body::Empty")); + assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains("1")); } } diff --git a/actix-http/src/builder.rs b/actix-http/src/builder.rs index 2f7466a90..56f144bd8 100644 --- a/actix-http/src/builder.rs +++ b/actix-http/src/builder.rs @@ -1,49 +1,67 @@ -use std::fmt::Debug; +use std::fmt; use std::marker::PhantomData; +use actix_codec::Framed; use actix_server_config::ServerConfig as SrvConfig; -use actix_service::{IntoNewService, NewService}; +use actix_service::{IntoNewService, NewService, Service}; use crate::body::MessageBody; use crate::config::{KeepAlive, ServiceConfig}; +use crate::error::Error; +use crate::h1::{Codec, ExpectHandler, H1Service, UpgradeHandler}; +use crate::h2::H2Service; use crate::request::Request; use crate::response::Response; - -use crate::h1::H1Service; -use crate::h2::H2Service; use crate::service::HttpService; /// A http service builder /// /// This type can be used to construct an instance of `http service` through a /// builder-like pattern. -pub struct HttpServiceBuilder { +pub struct HttpServiceBuilder> { keep_alive: KeepAlive, client_timeout: u64, client_disconnect: u64, + expect: X, + upgrade: Option, _t: PhantomData<(T, S)>, } -impl HttpServiceBuilder +impl HttpServiceBuilder> where S: NewService, - S::Error: Debug + 'static, - S::Service: 'static, + S::Error: Into, + S::InitError: fmt::Debug, { /// Create instance of `ServiceConfigBuilder` - pub fn new() -> HttpServiceBuilder { + pub fn new() -> Self { HttpServiceBuilder { keep_alive: KeepAlive::Timeout(5), client_timeout: 5000, client_disconnect: 0, + expect: ExpectHandler, + upgrade: None, _t: PhantomData, } } +} +impl HttpServiceBuilder +where + S: NewService, + S::Error: Into, + S::InitError: fmt::Debug, + X: NewService, + X::Error: Into, + X::InitError: fmt::Debug, + U: NewService), Response = ()>, + U::Error: fmt::Display, + U::InitError: fmt::Debug, +{ /// Set server keep-alive setting. /// /// By default keep alive is set to a 5 seconds. - pub fn keep_alive>(mut self, val: U) -> Self { + pub fn keep_alive>(mut self, val: W) -> Self { self.keep_alive = val.into(); self } @@ -75,30 +93,56 @@ where self } - // #[cfg(feature = "ssl")] - // /// Configure alpn protocols for SslAcceptorBuilder. - // pub fn configure_openssl( - // builder: &mut openssl::ssl::SslAcceptorBuilder, - // ) -> io::Result<()> { - // let protos: &[u8] = b"\x02h2"; - // builder.set_alpn_select_callback(|_, protos| { - // const H2: &[u8] = b"\x02h2"; - // if protos.windows(3).any(|window| window == H2) { - // Ok(b"h2") - // } else { - // Err(openssl::ssl::AlpnError::NOACK) - // } - // }); - // builder.set_alpn_protos(&protos)?; + /// Provide service for `EXPECT: 100-Continue` support. + /// + /// Service get called with request that contains `EXPECT` header. + /// Service must return request in case of success, in that case + /// request will be forwarded to main service. + pub fn expect(self, expect: F) -> HttpServiceBuilder + where + F: IntoNewService, + X1: NewService, + X1::Error: Into, + X1::InitError: fmt::Debug, + { + HttpServiceBuilder { + keep_alive: self.keep_alive, + client_timeout: self.client_timeout, + client_disconnect: self.client_disconnect, + expect: expect.into_new_service(), + upgrade: self.upgrade, + _t: PhantomData, + } + } - // Ok(()) - // } + /// Provide service for custom `Connection: UPGRADE` support. + /// + /// If service is provided then normal requests handling get halted + /// and this service get called with original request and framed object. + pub fn upgrade(self, upgrade: F) -> HttpServiceBuilder + where + F: IntoNewService, + U1: NewService), Response = ()>, + U1::Error: fmt::Display, + U1::InitError: fmt::Debug, + { + HttpServiceBuilder { + keep_alive: self.keep_alive, + client_timeout: self.client_timeout, + client_disconnect: self.client_disconnect, + expect: self.expect, + upgrade: Some(upgrade.into_new_service()), + _t: PhantomData, + } + } /// Finish service configuration and create *http service* for HTTP/1 protocol. - pub fn h1(self, service: F) -> H1Service + pub fn h1(self, service: F) -> H1Service where B: MessageBody + 'static, F: IntoNewService, + S::Error: Into, + S::InitError: fmt::Debug, S::Response: Into>, { let cfg = ServiceConfig::new( @@ -107,6 +151,8 @@ where self.client_disconnect, ); H1Service::with_config(cfg, service.into_new_service()) + .expect(self.expect) + .upgrade(self.upgrade) } /// Finish service configuration and create *http service* for HTTP/2 protocol. @@ -114,7 +160,10 @@ where where B: MessageBody + 'static, F: IntoNewService, + S::Error: Into, + S::InitError: fmt::Debug, S::Response: Into>, + ::Future: 'static, { let cfg = ServiceConfig::new( self.keep_alive, @@ -125,11 +174,14 @@ where } /// Finish service configuration and create `HttpService` instance. - pub fn finish(self, service: F) -> HttpService + pub fn finish(self, service: F) -> HttpService where B: MessageBody + 'static, F: IntoNewService, + S::Error: Into, + S::InitError: fmt::Debug, S::Response: Into>, + ::Future: 'static, { let cfg = ServiceConfig::new( self.keep_alive, @@ -137,5 +189,7 @@ where self.client_disconnect, ); HttpService::with_config(cfg, service.into_new_service()) + .expect(self.expect) + .upgrade(self.upgrade) } } diff --git a/actix-http/src/client/connection.rs b/actix-http/src/client/connection.rs index 4522dbbd3..9354fca4a 100644 --- a/actix-http/src/client/connection.rs +++ b/actix-http/src/client/connection.rs @@ -12,7 +12,7 @@ use crate::message::{RequestHead, ResponseHead}; use crate::payload::Payload; use super::error::SendRequestError; -use super::pool::Acquired; +use super::pool::{Acquired, Protocol}; use super::{h1proto, h2proto}; pub(crate) enum ConnectionType { @@ -24,6 +24,8 @@ pub trait Connection { type Io: AsyncRead + AsyncWrite; type Future: Future; + fn protocol(&self) -> Protocol; + /// Send request and body fn send_request( self, @@ -69,7 +71,7 @@ where } } -impl IoConnection { +impl IoConnection { pub(crate) fn new( io: ConnectionType, created: time::Instant, @@ -94,6 +96,14 @@ where type Io = T; type Future = Box>; + fn protocol(&self) -> Protocol { + match self.io { + Some(ConnectionType::H1(_)) => Protocol::Http1, + Some(ConnectionType::H2(_)) => Protocol::Http2, + None => Protocol::Http1, + } + } + fn send_request( mut self, head: RequestHead, @@ -161,6 +171,13 @@ where type Io = EitherIo; type Future = Box>; + fn protocol(&self) -> Protocol { + match self { + EitherConnection::A(con) => con.protocol(), + EitherConnection::B(con) => con.protocol(), + } + } + fn send_request( self, head: RequestHead, diff --git a/actix-http/src/client/connector.rs b/actix-http/src/client/connector.rs index 804756ced..0241e8472 100644 --- a/actix-http/src/client/connector.rs +++ b/actix-http/src/client/connector.rs @@ -14,6 +14,7 @@ use tokio_tcp::TcpStream; use super::connection::Connection; use super::error::ConnectError; use super::pool::{ConnectionPool, Protocol}; +use super::Connect; #[cfg(feature = "ssl")] use openssl::ssl::SslConnector; @@ -21,8 +22,18 @@ use openssl::ssl::SslConnector; #[cfg(not(feature = "ssl"))] type SslConnector = (); -/// Http client connector builde instance. -/// `Connector` type uses builder-like pattern for connector service construction. +/// Manages http client network connectivity +/// The `Connector` type uses a builder-like combinator pattern for service +/// construction that finishes by calling the `.finish()` method. +/// +/// ```rust,ignore +/// use std::time::Duration; +/// use actix_http::client::Connector; +/// +/// let connector = Connector::new() +/// .timeout(Duration::from_secs(5)) +/// .finish(); +/// ``` pub struct Connector { connector: T, timeout: Duration, @@ -47,8 +58,7 @@ impl Connector<(), ()> { let ssl = { #[cfg(feature = "ssl")] { - use log::error; - use openssl::ssl::{SslConnector, SslMethod}; + use openssl::ssl::SslMethod; let mut ssl = SslConnector::builder(SslMethod::tls()).unwrap(); let _ = ssl @@ -104,7 +114,8 @@ where Request = TcpConnect, Response = TcpConnection, Error = actix_connect::ConnectError, - > + Clone, + > + Clone + + 'static, { /// Connection timeout, i.e. max time to connect to remote host including dns name resolution. /// Set to 1 second by default. @@ -164,17 +175,21 @@ where } /// Finish configuration process and create connector service. - pub fn service( + /// The Connector builder always concludes by calling `finish()` last in + /// its combinator chain. + pub fn finish( self, - ) -> impl Service + Clone - { + ) -> impl Service + + Clone { #[cfg(not(feature = "ssl"))] { let connector = TimeoutService::new( self.timeout, - apply_fn(self.connector, |msg: Uri, srv| srv.call(msg.into())) - .map_err(ConnectError::from) - .map(|stream| (stream.into_parts().0, Protocol::Http1)), + apply_fn(self.connector, |msg: Connect, srv| { + srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr)) + }) + .map_err(ConnectError::from) + .map(|stream| (stream.into_parts().0, Protocol::Http1)), ) .map_err(|e| match e { TimeoutError::Service(e) => e, @@ -198,26 +213,28 @@ where let ssl_service = TimeoutService::new( self.timeout, - apply_fn(self.connector.clone(), |msg: Uri, srv| srv.call(msg.into())) - .map_err(ConnectError::from) - .and_then( - OpensslConnector::service(self.ssl) - .map_err(ConnectError::from) - .map(|stream| { - let sock = stream.into_parts().0; - let h2 = sock - .get_ref() - .ssl() - .selected_alpn_protocol() - .map(|protos| protos.windows(2).any(|w| w == H2)) - .unwrap_or(false); - if h2 { - (sock, Protocol::Http2) - } else { - (sock, Protocol::Http1) - } - }), - ), + apply_fn(self.connector.clone(), |msg: Connect, srv| { + srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr)) + }) + .map_err(ConnectError::from) + .and_then( + OpensslConnector::service(self.ssl) + .map_err(ConnectError::from) + .map(|stream| { + let sock = stream.into_parts().0; + let h2 = sock + .get_ref() + .ssl() + .selected_alpn_protocol() + .map(|protos| protos.windows(2).any(|w| w == H2)) + .unwrap_or(false); + if h2 { + (sock, Protocol::Http2) + } else { + (sock, Protocol::Http1) + } + }), + ), ) .map_err(|e| match e { TimeoutError::Service(e) => e, @@ -226,9 +243,11 @@ where let tcp_service = TimeoutService::new( self.timeout, - apply_fn(self.connector.clone(), |msg: Uri, srv| srv.call(msg.into())) - .map_err(ConnectError::from) - .map(|stream| (stream.into_parts().0, Protocol::Http1)), + apply_fn(self.connector.clone(), |msg: Connect, srv| { + srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr)) + }) + .map_err(ConnectError::from) + .map(|stream| (stream.into_parts().0, Protocol::Http1)), ) .map_err(|e| match e { TimeoutError::Service(e) => e, @@ -266,7 +285,9 @@ mod connect_impl { pub(crate) struct InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service + + Clone + + 'static, { pub(crate) tcp_pool: ConnectionPool, } @@ -274,8 +295,9 @@ mod connect_impl { impl Clone for InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service - + Clone, + T: Service + + Clone + + 'static, { fn clone(&self) -> Self { InnerConnector { @@ -287,9 +309,11 @@ mod connect_impl { impl Service for InnerConnector where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service + + Clone + + 'static, { - type Request = Uri; + type Request = Connect; type Response = IoConnection; type Error = ConnectError; type Future = Either< @@ -301,8 +325,8 @@ mod connect_impl { self.tcp_pool.poll_ready() } - fn call(&mut self, req: Uri) -> Self::Future { - match req.scheme_str() { + fn call(&mut self, req: Connect) -> Self::Future { + match req.uri.scheme_str() { Some("https") | Some("wss") => { Either::B(err(ConnectError::SslIsNotSupported)) } @@ -326,8 +350,8 @@ mod connect_impl { where Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, - T1: Service, - T2: Service, + T1: Service, + T2: Service, { pub(crate) tcp_pool: ConnectionPool, pub(crate) ssl_pool: ConnectionPool, @@ -337,10 +361,12 @@ mod connect_impl { where Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, - T1: Service - + Clone, - T2: Service - + Clone, + T1: Service + + Clone + + 'static, + T2: Service + + Clone + + 'static, { fn clone(&self) -> Self { InnerConnector { @@ -354,10 +380,14 @@ mod connect_impl { where Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, - T1: Service, - T2: Service, + T1: Service + + Clone + + 'static, + T2: Service + + Clone + + 'static, { - type Request = Uri; + type Request = Connect; type Response = EitherConnection; type Error = ConnectError; type Future = Either< @@ -372,8 +402,8 @@ mod connect_impl { self.tcp_pool.poll_ready() } - fn call(&mut self, req: Uri) -> Self::Future { - match req.scheme_str() { + fn call(&mut self, req: Connect) -> Self::Future { + match req.uri.scheme_str() { Some("https") | Some("wss") => { Either::B(Either::B(InnerConnectorResponseB { fut: self.ssl_pool.call(req), @@ -391,7 +421,9 @@ mod connect_impl { pub(crate) struct InnerConnectorResponseA where Io1: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service + + Clone + + 'static, { fut: as Service>::Future, _t: PhantomData, @@ -399,7 +431,9 @@ mod connect_impl { impl Future for InnerConnectorResponseA where - T: Service, + T: Service + + Clone + + 'static, Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, { @@ -417,7 +451,9 @@ mod connect_impl { pub(crate) struct InnerConnectorResponseB where Io2: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service + + Clone + + 'static, { fut: as Service>::Future, _t: PhantomData, @@ -425,7 +461,9 @@ mod connect_impl { impl Future for InnerConnectorResponseB where - T: Service, + T: Service + + Clone + + 'static, Io1: AsyncRead + AsyncWrite + 'static, Io2: AsyncRead + AsyncWrite + 'static, { diff --git a/actix-http/src/client/h1proto.rs b/actix-http/src/client/h1proto.rs index 5fec9c4f1..97ed3bbc7 100644 --- a/actix-http/src/client/h1proto.rs +++ b/actix-http/src/client/h1proto.rs @@ -1,12 +1,14 @@ +use std::io::Write; use std::{io, time}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; -use bytes::Bytes; +use bytes::{BufMut, Bytes, BytesMut}; use futures::future::{ok, Either}; use futures::{Async, Future, Poll, Sink, Stream}; use crate::error::PayloadError; use crate::h1; +use crate::http::header::{IntoHeaderValue, HOST}; use crate::message::{RequestHead, ResponseHead}; use crate::payload::{Payload, PayloadStream}; @@ -17,7 +19,7 @@ use crate::body::{BodySize, MessageBody}; pub(crate) fn send_request( io: T, - head: RequestHead, + mut head: RequestHead, body: B, created: time::Instant, pool: Option>, @@ -26,20 +28,41 @@ where T: AsyncRead + AsyncWrite + 'static, B: MessageBody, { + // set request host header + if !head.headers.contains_key(HOST) { + if let Some(host) = head.uri.host() { + let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); + + let _ = match head.uri.port_u16() { + None | Some(80) | Some(443) => write!(wrt, "{}", host), + Some(port) => write!(wrt, "{}:{}", host, port), + }; + + match wrt.get_mut().take().freeze().try_into() { + Ok(value) => { + head.headers.insert(HOST, value); + } + Err(e) => { + log::error!("Can not set HOST header {}", e); + } + } + } + } + let io = H1Connection { created, pool, io: Some(io), }; - let len = body.length(); + let len = body.size(); // create Framed and send reqest Framed::new(io, h1::ClientCodec::default()) .send((head, len).into()) .from_err() // send request body - .and_then(move |framed| match body.length() { + .and_then(move |framed| match body.size() { BodySize::None | BodySize::Empty | BodySize::Sized(0) => { Either::A(ok(framed)) } @@ -251,7 +274,7 @@ impl Stream for PlStream { Ok(Async::Ready(Some(chunk))) } else { let framed = self.framed.take().unwrap(); - let force_close = framed.get_codec().keepalive(); + let force_close = !framed.get_codec().keepalive(); release_connection(framed, force_close); Ok(Async::Ready(None)) } diff --git a/actix-http/src/client/h2proto.rs b/actix-http/src/client/h2proto.rs index d45716ab8..91240268e 100644 --- a/actix-http/src/client/h2proto.rs +++ b/actix-http/src/client/h2proto.rs @@ -27,9 +27,9 @@ where T: AsyncRead + AsyncWrite + 'static, B: MessageBody, { - trace!("Sending client request: {:?} {:?}", head, body.length()); + trace!("Sending client request: {:?} {:?}", head, body.size()); let head_req = head.method == Method::HEAD; - let length = body.length(); + let length = body.size(); let eof = match length { BodySize::None | BodySize::Empty | BodySize::Sized(0) => true, _ => false, @@ -104,11 +104,9 @@ where let (parts, body) = resp.into_parts(); let payload = if head_req { Payload::None } else { body.into() }; - let mut head = ResponseHead::default(); + let mut head = ResponseHead::new(parts.status); head.version = parts.version; - head.status = parts.status; - head.headers = parts.headers; - + head.headers = parts.headers.into(); Ok((head, payload)) }) .from_err() diff --git a/actix-http/src/client/mod.rs b/actix-http/src/client/mod.rs index 87c374742..1d10117cd 100644 --- a/actix-http/src/client/mod.rs +++ b/actix-http/src/client/mod.rs @@ -1,4 +1,6 @@ //! Http client api +use http::Uri; + mod connection; mod connector; mod error; @@ -9,3 +11,10 @@ mod pool; pub use self::connection::Connection; pub use self::connector::Connector; pub use self::error::{ConnectError, InvalidUrl, SendRequestError}; +pub use self::pool::Protocol; + +#[derive(Clone)] +pub struct Connect { + pub uri: Uri, + pub addr: Option, +} diff --git a/actix-http/src/client/pool.rs b/actix-http/src/client/pool.rs index aff11181b..8dedf72f5 100644 --- a/actix-http/src/client/pool.rs +++ b/actix-http/src/client/pool.rs @@ -13,15 +13,17 @@ use futures::unsync::oneshot; use futures::{Async, Future, Poll}; use h2::client::{handshake, Handshake}; use hashbrown::HashMap; -use http::uri::{Authority, Uri}; +use http::uri::Authority; use indexmap::IndexSet; use slab::Slab; use tokio_timer::{sleep, Delay}; use super::connection::{ConnectionType, IoConnection}; use super::error::ConnectError; +use super::Connect; #[derive(Clone, Copy, PartialEq)] +/// Protocol version pub enum Protocol { Http1, Http2, @@ -47,7 +49,9 @@ pub(crate) struct ConnectionPool( impl ConnectionPool where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service + + Clone + + 'static, { pub(crate) fn new( connector: T, @@ -67,7 +71,7 @@ where waiters: Slab::new(), waiters_queue: IndexSet::new(), available: HashMap::new(), - task: AtomicTask::new(), + task: None, })), ) } @@ -86,9 +90,11 @@ where impl Service for ConnectionPool where Io: AsyncRead + AsyncWrite + 'static, - T: Service, + T: Service + + Clone + + 'static, { - type Request = Uri; + type Request = Connect; type Response = IoConnection; type Error = ConnectError; type Future = Either< @@ -100,8 +106,8 @@ where self.0.poll_ready() } - fn call(&mut self, req: Uri) -> Self::Future { - let key = if let Some(authority) = req.authority_part() { + fn call(&mut self, req: Connect) -> Self::Future { + let key = if let Some(authority) = req.uri.authority_part() { authority.clone().into() } else { return Either::A(err(ConnectError::Unresolverd)); @@ -111,31 +117,41 @@ where match self.1.as_ref().borrow_mut().acquire(&key) { Acquire::Acquired(io, created) => { // use existing connection - Either::A(ok(IoConnection::new( + return Either::A(ok(IoConnection::new( io, created, Some(Acquired(key, Some(self.1.clone()))), - ))) - } - Acquire::NotAvailable => { - // connection is not available, wait - let (rx, token) = self.1.as_ref().borrow_mut().wait_for(req); - Either::B(Either::A(WaitForConnection { - rx, - key, - token, - inner: Some(self.1.clone()), - })) + ))); } Acquire::Available => { // open new connection - Either::B(Either::B(OpenConnection::new( + return Either::B(Either::B(OpenConnection::new( key, self.1.clone(), self.0.call(req), - ))) + ))); } + _ => (), } + + // connection is not available, wait + let (rx, token, support) = self.1.as_ref().borrow_mut().wait_for(req); + + // start support future + if !support { + self.1.as_ref().borrow_mut().task = Some(AtomicTask::new()); + tokio_current_thread::spawn(ConnectorPoolSupport { + connector: self.0.clone(), + inner: self.1.clone(), + }) + } + + Either::B(Either::A(WaitForConnection { + rx, + key, + token, + inner: Some(self.1.clone()), + })) } } @@ -243,7 +259,7 @@ where Ok(Async::Ready(IoConnection::new( ConnectionType::H2(snd), Instant::now(), - Some(Acquired(self.key.clone(), self.inner.clone())), + Some(Acquired(self.key.clone(), self.inner.take())), ))) } Ok(Async::NotReady) => Ok(Async::NotReady), @@ -254,12 +270,11 @@ where match self.fut.poll() { Err(err) => Err(err), Ok(Async::Ready((io, proto))) => { - let _ = self.inner.take(); if proto == Protocol::Http1 { Ok(Async::Ready(IoConnection::new( ConnectionType::H1(io), Instant::now(), - Some(Acquired(self.key.clone(), self.inner.clone())), + Some(Acquired(self.key.clone(), self.inner.take())), ))) } else { self.h2 = Some(handshake(io)); @@ -277,7 +292,6 @@ enum Acquire { NotAvailable, } -// #[derive(Debug)] struct AvailableConnection { io: ConnectionType, used: Instant, @@ -291,9 +305,12 @@ pub(crate) struct Inner { limit: usize, acquired: usize, available: HashMap>>, - waiters: Slab<(Uri, oneshot::Sender, ConnectError>>)>, + waiters: Slab<( + Connect, + oneshot::Sender, ConnectError>>, + )>, waiters_queue: IndexSet<(Key, usize)>, - task: AtomicTask, + task: Option, } impl Inner { @@ -309,18 +326,6 @@ impl Inner { self.waiters.remove(token); self.waiters_queue.remove(&(key.clone(), token)); } - - fn release_conn(&mut self, key: &Key, io: ConnectionType, created: Instant) { - self.acquired -= 1; - self.available - .entry(key.clone()) - .or_insert_with(VecDeque::new) - .push_back(AvailableConnection { - io, - created, - used: Instant::now(), - }); - } } impl Inner @@ -330,19 +335,21 @@ where /// connection is not available, wait fn wait_for( &mut self, - connect: Uri, + connect: Connect, ) -> ( oneshot::Receiver, ConnectError>>, usize, + bool, ) { let (tx, rx) = oneshot::channel(); - let key: Key = connect.authority_part().unwrap().clone().into(); + let key: Key = connect.uri.authority_part().unwrap().clone().into(); let entry = self.waiters.vacant_entry(); let token = entry.key(); entry.insert((connect, tx)); - assert!(!self.waiters_queue.insert((key, token))); - (rx, token) + assert!(self.waiters_queue.insert((key, token))); + + (rx, token, self.task.is_some()) } fn acquire(&mut self, key: &Key) -> Acquire { @@ -395,6 +402,19 @@ where Acquire::Available } + fn release_conn(&mut self, key: &Key, io: ConnectionType, created: Instant) { + self.acquired -= 1; + self.available + .entry(key.clone()) + .or_insert_with(VecDeque::new) + .push_back(AvailableConnection { + io, + created, + used: Instant::now(), + }); + self.check_availibility(); + } + fn release_close(&mut self, io: ConnectionType) { self.acquired -= 1; if let Some(timeout) = self.disconnect_timeout { @@ -402,11 +422,12 @@ where tokio_current_thread::spawn(CloseConnection::new(io, timeout)) } } + self.check_availibility(); } fn check_availibility(&self) { if !self.waiters_queue.is_empty() && self.acquired < self.limit { - self.task.notify() + self.task.as_ref().map(|t| t.notify()); } } } @@ -446,6 +467,147 @@ where } } +struct ConnectorPoolSupport +where + Io: AsyncRead + AsyncWrite + 'static, +{ + connector: T, + inner: Rc>>, +} + +impl Future for ConnectorPoolSupport +where + Io: AsyncRead + AsyncWrite + 'static, + T: Service, + T::Future: 'static, +{ + type Item = (); + type Error = (); + + fn poll(&mut self) -> Poll { + let mut inner = self.inner.as_ref().borrow_mut(); + inner.task.as_ref().unwrap().register(); + + // check waiters + loop { + let (key, token) = { + if let Some((key, token)) = inner.waiters_queue.get_index(0) { + (key.clone(), *token) + } else { + break; + } + }; + match inner.acquire(&key) { + Acquire::NotAvailable => break, + Acquire::Acquired(io, created) => { + let (_, tx) = inner.waiters.remove(token); + if let Err(conn) = tx.send(Ok(IoConnection::new( + io, + created, + Some(Acquired(key.clone(), Some(self.inner.clone()))), + ))) { + let (io, created) = conn.unwrap().into_inner(); + inner.release_conn(&key, io, created); + } + } + Acquire::Available => { + let (connect, tx) = inner.waiters.remove(token); + OpenWaitingConnection::spawn( + key.clone(), + tx, + self.inner.clone(), + self.connector.call(connect), + ); + } + } + let _ = inner.waiters_queue.swap_remove_index(0); + } + + Ok(Async::NotReady) + } +} + +struct OpenWaitingConnection +where + Io: AsyncRead + AsyncWrite + 'static, +{ + fut: F, + key: Key, + h2: Option>, + rx: Option, ConnectError>>>, + inner: Option>>>, +} + +impl OpenWaitingConnection +where + F: Future + 'static, + Io: AsyncRead + AsyncWrite + 'static, +{ + fn spawn( + key: Key, + rx: oneshot::Sender, ConnectError>>, + inner: Rc>>, + fut: F, + ) { + tokio_current_thread::spawn(OpenWaitingConnection { + key, + fut, + h2: None, + rx: Some(rx), + inner: Some(inner), + }) + } +} + +impl Drop for OpenWaitingConnection +where + Io: AsyncRead + AsyncWrite + 'static, +{ + fn drop(&mut self) { + if let Some(inner) = self.inner.take() { + let mut inner = inner.as_ref().borrow_mut(); + inner.release(); + inner.check_availibility(); + } + } +} + +impl Future for OpenWaitingConnection +where + F: Future, + Io: AsyncRead + AsyncWrite, +{ + type Item = (); + type Error = (); + + fn poll(&mut self) -> Poll { + match self.fut.poll() { + Err(err) => { + let _ = self.inner.take(); + if let Some(rx) = self.rx.take() { + let _ = rx.send(Err(err)); + } + Err(()) + } + Ok(Async::Ready((io, proto))) => { + if proto == Protocol::Http1 { + let rx = self.rx.take().unwrap(); + let _ = rx.send(Ok(IoConnection::new( + ConnectionType::H1(io), + Instant::now(), + Some(Acquired(self.key.clone(), self.inner.take())), + ))); + Ok(Async::Ready(())) + } else { + self.h2 = Some(handshake(io)); + self.poll() + } + } + Ok(Async::NotReady) => Ok(Async::NotReady), + } + } +} + pub(crate) struct Acquired(Key, Option>>>); impl Acquired diff --git a/actix-http/src/cookie/builder.rs b/actix-http/src/cookie/builder.rs index 2635572ab..efeddbb62 100644 --- a/actix-http/src/cookie/builder.rs +++ b/actix-http/src/cookie/builder.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; -use time::{Duration, Tm}; +use chrono::Duration; +use time::Tm; use super::{Cookie, SameSite}; @@ -16,7 +17,6 @@ use super::{Cookie, SameSite}; /// /// ```rust /// use actix_http::cookie::Cookie; -/// use time::Duration; /// /// # fn main() { /// let cookie: Cookie = Cookie::build("name", "value") @@ -24,7 +24,7 @@ use super::{Cookie, SameSite}; /// .path("/") /// .secure(true) /// .http_only(true) -/// .max_age(Duration::days(1)) +/// .max_age(84600) /// .finish(); /// # } /// ``` @@ -79,6 +79,26 @@ impl CookieBuilder { self } + /// Sets the `max_age` field in seconds in the cookie being built. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// # fn main() { + /// let c = Cookie::build("foo", "bar") + /// .max_age(1800) + /// .finish(); + /// + /// assert_eq!(c.max_age(), Some(time::Duration::seconds(30 * 60))); + /// # } + /// ``` + #[inline] + pub fn max_age(self, seconds: i64) -> CookieBuilder { + self.max_age_time(Duration::seconds(seconds)) + } + /// Sets the `max_age` field in the cookie being built. /// /// # Example @@ -88,14 +108,14 @@ impl CookieBuilder { /// /// # fn main() { /// let c = Cookie::build("foo", "bar") - /// .max_age(time::Duration::minutes(30)) + /// .max_age_time(time::Duration::minutes(30)) /// .finish(); /// /// assert_eq!(c.max_age(), Some(time::Duration::seconds(30 * 60))); /// # } /// ``` #[inline] - pub fn max_age(mut self, value: Duration) -> CookieBuilder { + pub fn max_age_time(mut self, value: Duration) -> CookieBuilder { self.cookie.set_max_age(value); self } @@ -200,7 +220,7 @@ impl CookieBuilder { /// /// ```rust /// use actix_http::cookie::Cookie; - /// use time::Duration; + /// use chrono::Duration; /// /// # fn main() { /// let c = Cookie::build("foo", "bar") diff --git a/actix-http/src/cookie/jar.rs b/actix-http/src/cookie/jar.rs index b60d73fe6..cc67536c6 100644 --- a/actix-http/src/cookie/jar.rs +++ b/actix-http/src/cookie/jar.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use std::mem::replace; -use time::{self, Duration}; +use chrono::Duration; use super::delta::DeltaCookie; use super::Cookie; @@ -188,7 +188,7 @@ impl CookieJar { /// /// ```rust /// use actix_http::cookie::{CookieJar, Cookie}; - /// use time::Duration; + /// use chrono::Duration; /// /// # fn main() { /// let mut jar = CookieJar::new(); @@ -241,7 +241,7 @@ impl CookieJar { /// /// ```rust /// use actix_http::cookie::{CookieJar, Cookie}; - /// use time::Duration; + /// use chrono::Duration; /// /// # fn main() { /// let mut jar = CookieJar::new(); @@ -537,8 +537,8 @@ mod test { #[test] #[cfg(feature = "secure-cookies")] fn delta() { + use chrono::Duration; use std::collections::HashMap; - use time::Duration; let mut c = CookieJar::new(); diff --git a/actix-http/src/cookie/mod.rs b/actix-http/src/cookie/mod.rs index 0f5f45488..ddcb12bbf 100644 --- a/actix-http/src/cookie/mod.rs +++ b/actix-http/src/cookie/mod.rs @@ -65,8 +65,9 @@ use std::borrow::Cow; use std::fmt; use std::str::FromStr; +use chrono::Duration; use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; -use time::{Duration, Tm}; +use time::Tm; pub use self::builder::CookieBuilder; pub use self::draft::*; @@ -624,7 +625,7 @@ impl<'c> Cookie<'c> { /// /// ```rust /// use actix_http::cookie::Cookie; - /// use time::Duration; + /// use chrono::Duration; /// /// # fn main() { /// let mut c = Cookie::new("name", "value"); @@ -703,7 +704,7 @@ impl<'c> Cookie<'c> { /// /// ```rust /// use actix_http::cookie::Cookie; - /// use time::Duration; + /// use chrono::Duration; /// /// # fn main() { /// let mut c = Cookie::new("foo", "bar"); @@ -977,7 +978,7 @@ impl<'a, 'b> PartialEq> for Cookie<'a> { #[cfg(test)] mod tests { use super::{Cookie, SameSite}; - use time::{strptime, Duration}; + use time::strptime; #[test] fn format() { @@ -987,9 +988,7 @@ mod tests { let cookie = Cookie::build("foo", "bar").http_only(true).finish(); assert_eq!(&cookie.to_string(), "foo=bar; HttpOnly"); - let cookie = Cookie::build("foo", "bar") - .max_age(Duration::seconds(10)) - .finish(); + let cookie = Cookie::build("foo", "bar").max_age(10).finish(); assert_eq!(&cookie.to_string(), "foo=bar; Max-Age=10"); let cookie = Cookie::build("foo", "bar").secure(true).finish(); diff --git a/actix-http/src/cookie/parse.rs b/actix-http/src/cookie/parse.rs index ebbd6ffc9..42a2c1fcf 100644 --- a/actix-http/src/cookie/parse.rs +++ b/actix-http/src/cookie/parse.rs @@ -5,8 +5,8 @@ use std::error::Error; use std::fmt; use std::str::Utf8Error; +use chrono::Duration; use percent_encoding::percent_decode; -use time::{self, Duration}; use super::{Cookie, CookieStr, SameSite}; @@ -220,7 +220,8 @@ where #[cfg(test)] mod tests { use super::{Cookie, SameSite}; - use time::{strptime, Duration}; + use chrono::Duration; + use time::strptime; macro_rules! assert_eq_parse { ($string:expr, $expected:expr) => { @@ -418,9 +419,7 @@ mod tests { #[test] fn do_not_panic_on_large_max_ages() { let max_seconds = Duration::max_value().num_seconds(); - let expected = Cookie::build("foo", "bar") - .max_age(Duration::seconds(max_seconds)) - .finish(); + let expected = Cookie::build("foo", "bar").max_age(max_seconds).finish(); assert_eq_parse!(format!(" foo=bar; Max-Age={:?}", max_seconds + 1), expected); } } diff --git a/actix-http/src/cookie/secure/key.rs b/actix-http/src/cookie/secure/key.rs index 3b8a0af78..4e74f6e78 100644 --- a/actix-http/src/cookie/secure/key.rs +++ b/actix-http/src/cookie/secure/key.rs @@ -68,8 +68,8 @@ impl Key { encryption_key.copy_from_slice(&both_keys[SIGNED_KEY_LEN..]); Key { - signing_key: signing_key, - encryption_key: encryption_key, + signing_key, + encryption_key, } } diff --git a/actix-http/src/cookie/secure/private.rs b/actix-http/src/cookie/secure/private.rs index 323687303..e59743767 100644 --- a/actix-http/src/cookie/secure/private.rs +++ b/actix-http/src/cookie/secure/private.rs @@ -37,7 +37,7 @@ impl<'a> PrivateJar<'a> { let mut key_array = [0u8; KEY_LEN]; key_array.copy_from_slice(key.encryption()); PrivateJar { - parent: parent, + parent, key: key_array, } } diff --git a/actix-http/src/cookie/secure/signed.rs b/actix-http/src/cookie/secure/signed.rs index 5a4ffb76c..1b1799cf4 100644 --- a/actix-http/src/cookie/secure/signed.rs +++ b/actix-http/src/cookie/secure/signed.rs @@ -31,7 +31,7 @@ impl<'a> SignedJar<'a> { #[doc(hidden)] pub fn new(parent: &'a mut CookieJar, key: &Key) -> SignedJar<'a> { SignedJar { - parent: parent, + parent, key: SigningKey::new(HMAC_DIGEST, key.signing()), } } diff --git a/actix-http/src/encoding/decoder.rs b/actix-http/src/encoding/decoder.rs index 16d15e905..4b56a1b62 100644 --- a/actix-http/src/encoding/decoder.rs +++ b/actix-http/src/encoding/decoder.rs @@ -55,7 +55,7 @@ where #[inline] pub fn from_headers(stream: S, headers: &HeaderMap) -> Decoder { // check content-encoding - let encoding = if let Some(enc) = headers.get(CONTENT_ENCODING) { + let encoding = if let Some(enc) = headers.get(&CONTENT_ENCODING) { if let Ok(enc) = enc.to_str() { ContentEncoding::from(enc) } else { diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index fcac3a427..aabce292a 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -1,13 +1,13 @@ //! Stream encoder use std::io::{self, Write}; -use bytes::Bytes; -use futures::{Async, Poll}; - +use actix_threadpool::{run, CpuFuture}; #[cfg(feature = "brotli")] use brotli2::write::BrotliEncoder; +use bytes::Bytes; #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] use flate2::write::{GzEncoder, ZlibEncoder}; +use futures::{Async, Future, Poll}; use crate::body::{Body, BodySize, MessageBody, ResponseBody}; use crate::http::header::{ContentEncoding, CONTENT_ENCODING}; @@ -16,9 +16,13 @@ use crate::{Error, ResponseHead}; use super::Writer; +const INPLACE: usize = 2049; + pub struct Encoder { + eof: bool, body: EncoderBody, encoder: Option, + fut: Option>, } impl Encoder { @@ -27,73 +31,60 @@ impl Encoder { head: &mut ResponseHead, body: ResponseBody, ) -> ResponseBody> { - let has_ce = head.headers().contains_key(CONTENT_ENCODING); - match body { - ResponseBody::Other(b) => match b { - Body::None => ResponseBody::Other(Body::None), - Body::Empty => ResponseBody::Other(Body::Empty), - Body::Bytes(buf) => { - if !(has_ce - || encoding == ContentEncoding::Identity - || encoding == ContentEncoding::Auto) - { - let mut enc = ContentEncoder::encoder(encoding).unwrap(); + let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING) + || head.status == StatusCode::SWITCHING_PROTOCOLS + || encoding == ContentEncoding::Identity + || encoding == ContentEncoding::Auto); - // TODO return error! - let _ = enc.write(buf.as_ref()); - let body = enc.finish().unwrap(); - update_head(encoding, head); - ResponseBody::Other(Body::Bytes(body)) + let body = match body { + ResponseBody::Other(b) => match b { + Body::None => return ResponseBody::Other(Body::None), + Body::Empty => return ResponseBody::Other(Body::Empty), + Body::Bytes(buf) => { + if can_encode { + EncoderBody::Bytes(buf) } else { - ResponseBody::Other(Body::Bytes(buf)) - } - } - Body::Message(stream) => { - if has_ce || head.status == StatusCode::SWITCHING_PROTOCOLS { - ResponseBody::Body(Encoder { - body: EncoderBody::Other(stream), - encoder: None, - }) - } else { - update_head(encoding, head); - head.no_chunking(false); - ResponseBody::Body(Encoder { - body: EncoderBody::Other(stream), - encoder: ContentEncoder::encoder(encoding), - }) + return ResponseBody::Other(Body::Bytes(buf)); } } + Body::Message(stream) => EncoderBody::BoxedStream(stream), }, - ResponseBody::Body(stream) => { - if has_ce || head.status == StatusCode::SWITCHING_PROTOCOLS { - ResponseBody::Body(Encoder { - body: EncoderBody::Body(stream), - encoder: None, - }) - } else { - update_head(encoding, head); - head.no_chunking(false); - ResponseBody::Body(Encoder { - body: EncoderBody::Body(stream), - encoder: ContentEncoder::encoder(encoding), - }) - } - } + ResponseBody::Body(stream) => EncoderBody::Stream(stream), + }; + + if can_encode { + update_head(encoding, head); + head.no_chunking(false); + ResponseBody::Body(Encoder { + body, + eof: false, + fut: None, + encoder: ContentEncoder::encoder(encoding), + }) + } else { + ResponseBody::Body(Encoder { + body, + eof: false, + fut: None, + encoder: None, + }) } } } enum EncoderBody { - Body(B), - Other(Box), + Bytes(Bytes), + Stream(B), + BoxedStream(Box), } impl MessageBody for Encoder { - fn length(&self) -> BodySize { + fn size(&self) -> BodySize { if self.encoder.is_none() { match self.body { - EncoderBody::Body(ref b) => b.length(), - EncoderBody::Other(ref b) => b.length(), + EncoderBody::Bytes(ref b) => b.size(), + EncoderBody::Stream(ref b) => b.size(), + EncoderBody::BoxedStream(ref b) => b.size(), } } else { BodySize::Stream @@ -102,16 +93,47 @@ impl MessageBody for Encoder { fn poll_next(&mut self) -> Poll, Error> { loop { + if self.eof { + return Ok(Async::Ready(None)); + } + + if let Some(ref mut fut) = self.fut { + let mut encoder = futures::try_ready!(fut.poll()); + let chunk = encoder.take(); + self.encoder = Some(encoder); + self.fut.take(); + if !chunk.is_empty() { + return Ok(Async::Ready(Some(chunk))); + } + } + let result = match self.body { - EncoderBody::Body(ref mut b) => b.poll_next()?, - EncoderBody::Other(ref mut b) => b.poll_next()?, + EncoderBody::Bytes(ref mut b) => { + if b.is_empty() { + Async::Ready(None) + } else { + Async::Ready(Some(std::mem::replace(b, Bytes::new()))) + } + } + EncoderBody::Stream(ref mut b) => b.poll_next()?, + EncoderBody::BoxedStream(ref mut b) => b.poll_next()?, }; match result { Async::NotReady => return Ok(Async::NotReady), Async::Ready(Some(chunk)) => { - if let Some(ref mut encoder) = self.encoder { - if encoder.write(&chunk)? { - return Ok(Async::Ready(Some(encoder.take()))); + if let Some(mut encoder) = self.encoder.take() { + if chunk.len() < INPLACE { + encoder.write(&chunk)?; + let chunk = encoder.take(); + self.encoder = Some(encoder); + if !chunk.is_empty() { + return Ok(Async::Ready(Some(chunk))); + } + } else { + self.fut = Some(run(move || { + encoder.write(&chunk)?; + Ok(encoder) + })); } } else { return Ok(Async::Ready(Some(chunk))); @@ -123,6 +145,7 @@ impl MessageBody for Encoder { if chunk.is_empty() { return Ok(Async::Ready(None)); } else { + self.eof = true; return Ok(Async::Ready(Some(chunk))); } } else { @@ -203,11 +226,11 @@ impl ContentEncoder { } } - fn write(&mut self, data: &[u8]) -> Result { + fn write(&mut self, data: &[u8]) -> Result<(), io::Error> { match *self { #[cfg(feature = "brotli")] ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) { - Ok(_) => Ok(!encoder.get_ref().buf.is_empty()), + Ok(_) => Ok(()), Err(err) => { trace!("Error decoding br encoding: {}", err); Err(err) @@ -215,7 +238,7 @@ impl ContentEncoder { }, #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) { - Ok(_) => Ok(!encoder.get_ref().buf.is_empty()), + Ok(_) => Ok(()), Err(err) => { trace!("Error decoding gzip encoding: {}", err); Err(err) @@ -223,7 +246,7 @@ impl ContentEncoder { }, #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) { - Ok(_) => Ok(!encoder.get_ref().buf.is_empty()), + Ok(_) => Ok(()), Err(err) => { trace!("Error decoding deflate encoding: {}", err); Err(err) diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index 45bf067dc..1768c9543 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -1,11 +1,13 @@ //! Error and Result module use std::cell::RefCell; +use std::io::Write; use std::str::Utf8Error; use std::string::FromUtf8Error; use std::{fmt, io, result}; pub use actix_threadpool::BlockingError; use actix_utils::timeout::TimeoutError; +use bytes::BytesMut; use derive_more::{Display, From}; use futures::Canceled; use http::uri::InvalidUri; @@ -17,9 +19,9 @@ use serde_urlencoded::ser::Error as FormError; use tokio_timer::Error as TimerError; // re-export for convinience -pub use crate::cookie::ParseError as CookieParseError; - use crate::body::Body; +pub use crate::cookie::ParseError as CookieParseError; +use crate::helpers::Writer; use crate::response::Response; /// A specialized [`Result`](https://doc.rust-lang.org/std/result/enum.Result.html) @@ -49,13 +51,6 @@ impl Error { pub fn as_response_error(&self) -> &ResponseError { self.cause.as_ref() } - - /// Converts error to a response instance and set error message as response body - pub fn response_with_message(self) -> Response { - let message = format!("{}", self); - let resp: Response = self.into(); - resp.set_body(Body::from(message)) - } } /// Error that can be converted to `Response` @@ -66,6 +61,18 @@ pub trait ResponseError: fmt::Debug + fmt::Display { fn error_response(&self) -> Response { Response::new(StatusCode::INTERNAL_SERVER_ERROR) } + + /// Constructs an error response + fn render_response(&self) -> Response { + let mut resp = self.error_response(); + let mut buf = BytesMut::new(); + let _ = write!(Writer(&mut buf), "{}", self); + resp.headers_mut().insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("text/plain"), + ); + resp.set_body(Body::from(buf)) + } } impl fmt::Display for Error { @@ -80,6 +87,26 @@ impl fmt::Debug for Error { } } +impl From<()> for Error { + fn from(_: ()) -> Self { + Error::from(UnitError) + } +} + +impl std::error::Error for Error { + fn description(&self) -> &str { + "actix-http::Error" + } + + fn cause(&self) -> Option<&dyn std::error::Error> { + None + } + + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + None + } +} + /// Convert `Error` to a `Response` instance impl From for Response { fn from(err: Error) -> Self { @@ -106,6 +133,13 @@ impl ResponseError for TimeoutError { } } +#[derive(Debug, Display)] +#[display(fmt = "UnknownError")] +struct UnitError; + +/// `InternalServerError` for `JsonError` +impl ResponseError for UnitError {} + /// `InternalServerError` for `JsonError` impl ResponseError for JsonError {} @@ -115,6 +149,14 @@ impl ResponseError for FormError {} /// `InternalServerError` for `TimerError` impl ResponseError for TimerError {} +#[cfg(feature = "ssl")] +/// `InternalServerError` for `SslError` +impl ResponseError for openssl::ssl::Error {} + +#[cfg(feature = "ssl")] +/// `InternalServerError` for `SslError` +impl ResponseError for openssl::ssl::HandshakeError {} + /// Return `BAD_REQUEST` for `de::value::Error` impl ResponseError for DeError { fn error_response(&self) -> Response { @@ -326,7 +368,10 @@ impl ResponseError for crate::cookie::ParseError { /// A set of errors that can occur during dispatching http requests pub enum DispatchError { /// Service error - Service, + Service(Error), + + /// Upgrade service error + Upgrade, /// An `io::Error` that occurred while trying to read or write to a network /// stream. @@ -452,7 +497,16 @@ where { fn error_response(&self) -> Response { match self.status { - InternalErrorType::Status(st) => Response::new(st), + InternalErrorType::Status(st) => { + let mut res = Response::new(st); + let mut buf = BytesMut::new(); + let _ = write!(Writer(&mut buf), "{}", self); + res.headers_mut().insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("text/plain"), + ); + res.set_body(Body::from(buf)) + } InternalErrorType::Response(ref resp) => { if let Some(resp) = resp.borrow_mut().take() { resp @@ -462,6 +516,11 @@ where } } } + + /// Constructs an error response + fn render_response(&self) -> Response { + self.error_response() + } } /// Convert Response to a Error diff --git a/actix-http/src/h1/codec.rs b/actix-http/src/h1/codec.rs index 6e891e7cd..1e1e1602f 100644 --- a/actix-http/src/h1/codec.rs +++ b/actix-http/src/h1/codec.rs @@ -1,6 +1,6 @@ #![allow(unused_imports, unused_variables, dead_code)] -use std::fmt; -use std::io::{self, Write}; +use std::io::Write; +use std::{fmt, io, net}; use actix_codec::{Decoder, Encoder}; use bitflags::bitflags; @@ -9,7 +9,7 @@ use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCOD use http::{Method, StatusCode, Version}; use super::decoder::{PayloadDecoder, PayloadItem, PayloadType}; -use super::{decoder, encoder, reserve_readbuf}; +use super::{decoder, encoder}; use super::{Message, MessageType}; use crate::body::BodySize; use crate::config::ServiceConfig; @@ -22,8 +22,8 @@ use crate::response::Response; bitflags! { struct Flags: u8 { const HEAD = 0b0000_0001; - const KEEPALIVE_ENABLED = 0b0000_1000; - const STREAM = 0b0001_0000; + const KEEPALIVE_ENABLED = 0b0000_0010; + const STREAM = 0b0000_0100; } } @@ -31,7 +31,7 @@ const AVERAGE_HEADER_SIZE: usize = 30; /// HTTP/1 Codec pub struct Codec { - config: ServiceConfig, + pub(crate) config: ServiceConfig, decoder: decoder::MessageDecoder, payload: Option, version: Version, @@ -39,7 +39,6 @@ pub struct Codec { // encoder part flags: Flags, - headers_size: u32, encoder: encoder::MessageEncoder>, } @@ -67,27 +66,34 @@ impl Codec { }; Codec { config, + flags, decoder: decoder::MessageDecoder::default(), payload: None, version: Version::HTTP_11, ctype: ConnectionType::Close, - - flags, - headers_size: 0, encoder: encoder::MessageEncoder::default(), } } + #[inline] /// Check if request is upgrade pub fn upgrade(&self) -> bool { self.ctype == ConnectionType::Upgrade } + #[inline] /// Check if last response is keep-alive pub fn keepalive(&self) -> bool { self.ctype == ConnectionType::KeepAlive } + #[inline] + /// Check if keep-alive enabled on server level + pub fn keepalive_enabled(&self) -> bool { + self.flags.contains(Flags::KEEPALIVE_ENABLED) + } + + #[inline] /// Check last request's message type pub fn message_type(&self) -> MessageType { if self.flags.contains(Flags::STREAM) { @@ -107,10 +113,7 @@ impl Decoder for Codec { fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { if self.payload.is_some() { Ok(match self.payload.as_mut().unwrap().decode(src)? { - Some(PayloadItem::Chunk(chunk)) => { - reserve_readbuf(src); - Some(Message::Chunk(Some(chunk))) - } + Some(PayloadItem::Chunk(chunk)) => Some(Message::Chunk(Some(chunk))), Some(PayloadItem::Eof) => { self.payload.take(); Some(Message::Chunk(None)) @@ -135,7 +138,6 @@ impl Decoder for Codec { self.flags.insert(Flags::STREAM); } } - reserve_readbuf(src); Ok(Some(Message::Item(req))) } else { Ok(None) @@ -180,7 +182,7 @@ impl Encoder for Codec { self.ctype, &self.config, )?; - self.headers_size = (dst.len() - len) as u32; + // self.headers_size = (dst.len() - len) as u32; } Message::Chunk(Some(bytes)) => { self.encoder.encode_chunk(bytes.as_ref(), dst)?; diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index dfd9fe25c..411649fc1 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -5,11 +5,12 @@ use actix_codec::Decoder; use bytes::{Bytes, BytesMut}; use futures::{Async, Poll}; use http::header::{HeaderName, HeaderValue}; -use http::{header, HeaderMap, HttpTryFrom, Method, StatusCode, Uri, Version}; +use http::{header, HttpTryFrom, Method, StatusCode, Uri, Version}; use httparse; use log::{debug, error, trace}; use crate::error::ParseError; +use crate::header::HeaderMap; use crate::message::{ConnectionType, ResponseHead}; use crate::request::Request; @@ -51,6 +52,8 @@ pub(crate) enum PayloadLength { pub(crate) trait MessageType: Sized { fn set_connection_type(&mut self, ctype: Option); + fn set_expect(&mut self); + fn headers_mut(&mut self) -> &mut HeaderMap; fn decode(src: &mut BytesMut) -> Result, ParseError>; @@ -62,6 +65,7 @@ pub(crate) trait MessageType: Sized { ) -> Result { let mut ka = None; let mut has_upgrade = false; + let mut expect = false; let mut chunked = false; let mut content_length = None; @@ -69,73 +73,81 @@ pub(crate) trait MessageType: Sized { let headers = self.headers_mut(); for idx in raw_headers.iter() { - if let Ok(name) = HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]) - { - // Unsafe: httparse check header value for valid utf-8 - let value = unsafe { - HeaderValue::from_shared_unchecked( - slice.slice(idx.value.0, idx.value.1), - ) - }; - match name { - header::CONTENT_LENGTH => { - if let Ok(s) = value.to_str() { - if let Ok(len) = s.parse::() { + let name = + HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]).unwrap(); + + // Unsafe: httparse check header value for valid utf-8 + let value = unsafe { + HeaderValue::from_shared_unchecked( + slice.slice(idx.value.0, idx.value.1), + ) + }; + match name { + header::CONTENT_LENGTH => { + if let Ok(s) = value.to_str() { + if let Ok(len) = s.parse::() { + if len != 0 { content_length = Some(len); - } else { - debug!("illegal Content-Length: {:?}", s); - return Err(ParseError::Header); } } else { - debug!("illegal Content-Length: {:?}", value); + debug!("illegal Content-Length: {:?}", s); return Err(ParseError::Header); } + } else { + debug!("illegal Content-Length: {:?}", value); + return Err(ParseError::Header); } - // transfer-encoding - header::TRANSFER_ENCODING => { - if let Ok(s) = value.to_str().map(|s| s.trim()) { - chunked = s.eq_ignore_ascii_case("chunked"); - } else { - return Err(ParseError::Header); - } + } + // transfer-encoding + header::TRANSFER_ENCODING => { + if let Ok(s) = value.to_str().map(|s| s.trim()) { + chunked = s.eq_ignore_ascii_case("chunked"); + } else { + return Err(ParseError::Header); } - // connection keep-alive state - header::CONNECTION => { - ka = if let Ok(conn) = value.to_str().map(|conn| conn.trim()) - { - if conn.eq_ignore_ascii_case("keep-alive") { - Some(ConnectionType::KeepAlive) - } else if conn.eq_ignore_ascii_case("close") { - Some(ConnectionType::Close) - } else if conn.eq_ignore_ascii_case("upgrade") { - Some(ConnectionType::Upgrade) - } else { - None - } + } + // connection keep-alive state + header::CONNECTION => { + ka = if let Ok(conn) = value.to_str().map(|conn| conn.trim()) { + if conn.eq_ignore_ascii_case("keep-alive") { + Some(ConnectionType::KeepAlive) + } else if conn.eq_ignore_ascii_case("close") { + Some(ConnectionType::Close) + } else if conn.eq_ignore_ascii_case("upgrade") { + Some(ConnectionType::Upgrade) } else { None - }; - } - header::UPGRADE => { - has_upgrade = true; - // check content-length, some clients (dart) - // sends "content-length: 0" with websocket upgrade - if let Ok(val) = value.to_str().map(|val| val.trim()) { - if val.eq_ignore_ascii_case("websocket") { - content_length = None; - } + } + } else { + None + }; + } + header::UPGRADE => { + has_upgrade = true; + // check content-length, some clients (dart) + // sends "content-length: 0" with websocket upgrade + if let Ok(val) = value.to_str().map(|val| val.trim()) { + if val.eq_ignore_ascii_case("websocket") { + content_length = None; } } - _ => (), } - - headers.append(name, value); - } else { - return Err(ParseError::Header); + header::EXPECT => { + let bytes = value.as_bytes(); + if bytes.len() >= 4 && &bytes[0..4] == b"100-" { + expect = true; + } + } + _ => (), } + + headers.append(name, value); } } self.set_connection_type(ka); + if expect { + self.set_expect() + } // https://tools.ietf.org/html/rfc7230#section-3.3.3 if chunked { @@ -163,6 +175,10 @@ impl MessageType for Request { } } + fn set_expect(&mut self) { + self.head_mut().set_expect(); + } + fn headers_mut(&mut self) -> &mut HeaderMap { &mut self.head_mut().headers } @@ -198,10 +214,10 @@ impl MessageType for Request { let mut msg = Request::new(); // convert headers - let len = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?; + let length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?; // payload decoder - let decoder = match len { + let decoder = match length { PayloadLength::Payload(pl) => pl, PayloadLength::Upgrade => { // upgrade(websocket) @@ -235,6 +251,8 @@ impl MessageType for ResponseHead { } } + fn set_expect(&mut self) {} + fn headers_mut(&mut self) -> &mut HeaderMap { &mut self.headers } @@ -266,13 +284,14 @@ impl MessageType for ResponseHead { } }; - let mut msg = ResponseHead::default(); + let mut msg = ResponseHead::new(status); + msg.version = ver; // convert headers - let len = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?; + let length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?; // message payload - let decoder = if let PayloadLength::Payload(pl) = len { + let decoder = if let PayloadLength::Payload(pl) = length { pl } else if status == StatusCode::SWITCHING_PROTOCOLS { // switching protocol or connect @@ -284,9 +303,6 @@ impl MessageType for ResponseHead { PayloadType::None }; - msg.status = status; - msg.version = ver; - Ok(Some((msg, decoder))) } } @@ -610,6 +626,7 @@ mod tests { use super::*; use crate::error::ParseError; + use crate::http::header::{HeaderName, SET_COOKIE}; use crate::httpmessage::HttpMessage; impl PayloadType { @@ -770,7 +787,13 @@ mod tests { 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_eq!( + req.headers() + .get(HeaderName::try_from("test").unwrap()) + .unwrap() + .as_bytes(), + b"value" + ); } #[test] @@ -785,12 +808,11 @@ mod tests { let val: Vec<_> = req .headers() - .get_all("Set-Cookie") - .iter() + .get_all(SET_COOKIE) .map(|v| v.to_str().unwrap().to_owned()) .collect(); - assert_eq!(val[0], "c1=cookie1"); - assert_eq!(val[1], "c2=cookie2"); + assert_eq!(val[1], "c1=cookie1"); + assert_eq!(val[0], "c2=cookie2"); } #[test] diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 96db08122..758466837 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -1,79 +1,127 @@ use std::collections::VecDeque; -use std::fmt::Debug; -use std::mem; use std::time::Instant; +use std::{fmt, io, net}; -use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use actix_codec::{Decoder, Encoder, Framed, FramedParts}; +use actix_server_config::IoStream; use actix_service::Service; use actix_utils::cloneable::CloneableService; use bitflags::bitflags; -use futures::{Async, Future, Poll, Sink, Stream}; -use log::{debug, error, trace}; +use bytes::{BufMut, BytesMut}; +use futures::{Async, Future, Poll}; +use log::{error, trace}; use tokio_timer::Delay; use crate::body::{Body, BodySize, MessageBody, ResponseBody}; use crate::config::ServiceConfig; -use crate::error::DispatchError; +use crate::error::{DispatchError, Error}; use crate::error::{ParseError, PayloadError}; use crate::request::Request; use crate::response::Response; use super::codec::Codec; -use super::payload::{Payload, PayloadSender, PayloadStatus, PayloadWriter}; +use super::payload::{Payload, PayloadSender, PayloadStatus}; use super::{Message, MessageType}; +const LW_BUFFER_SIZE: usize = 4096; +const HW_BUFFER_SIZE: usize = 32_768; const MAX_PIPELINED_MESSAGES: usize = 16; bitflags! { pub struct Flags: u8 { const STARTED = 0b0000_0001; - const KEEPALIVE_ENABLED = 0b0000_0010; - const KEEPALIVE = 0b0000_0100; - const POLLED = 0b0000_1000; - const SHUTDOWN = 0b0010_0000; - const DISCONNECTED = 0b0100_0000; - const DROPPING = 0b1000_0000; + const KEEPALIVE = 0b0000_0010; + const POLLED = 0b0000_0100; + const SHUTDOWN = 0b0000_1000; + const READ_DISCONNECT = 0b0001_0000; + const WRITE_DISCONNECT = 0b0010_0000; + const UPGRADE = 0b0100_0000; } } /// Dispatcher for HTTP/1.1 protocol -pub struct Dispatcher + 'static, B: MessageBody> +pub struct Dispatcher where - S::Error: Debug, + S: Service, + S::Error: Into, + B: MessageBody, + X: Service, + X::Error: Into, + U: Service), Response = ()>, + U::Error: fmt::Display, { - inner: Option>, + inner: DispatcherState, } -struct InnerDispatcher + 'static, B: MessageBody> +enum DispatcherState where - S::Error: Debug, + S: Service, + S::Error: Into, + B: MessageBody, + X: Service, + X::Error: Into, + U: Service), Response = ()>, + U::Error: fmt::Display, +{ + Normal(InnerDispatcher), + Upgrade(U::Future), + None, +} + +struct InnerDispatcher +where + S: Service, + S::Error: Into, + B: MessageBody, + X: Service, + X::Error: Into, + U: Service), Response = ()>, + U::Error: fmt::Display, { service: CloneableService, + expect: CloneableService, + upgrade: Option>, flags: Flags, - framed: Framed, + peer_addr: Option, error: Option, - config: ServiceConfig, - state: State, + state: State, payload: Option, messages: VecDeque, ka_expire: Instant, ka_timer: Option, + + io: T, + read_buf: BytesMut, + write_buf: BytesMut, + codec: Codec, } enum DispatcherMessage { Item(Request), + Upgrade(Request), Error(Response<()>), } -enum State, B: MessageBody> { +enum State +where + S: Service, + X: Service, + B: MessageBody, +{ None, + ExpectCall(X::Future), ServiceCall(S::Future), SendPayload(ResponseBody), } -impl, B: MessageBody> State { +impl State +where + S: Service, + X: Service, + B: MessageBody, +{ fn is_empty(&self) -> bool { if let State::None = self { true @@ -81,36 +129,84 @@ impl, B: MessageBody> State { false } } + + fn is_call(&self) -> bool { + if let State::ServiceCall(_) = self { + true + } else { + false + } + } } -impl Dispatcher +enum PollResponse { + Upgrade(Request), + DoNothing, + DrainWriteBuf, +} + +impl PartialEq for PollResponse { + fn eq(&self, other: &PollResponse) -> bool { + match self { + PollResponse::DrainWriteBuf => match other { + PollResponse::DrainWriteBuf => true, + _ => false, + }, + PollResponse::DoNothing => match other { + PollResponse::DoNothing => true, + _ => false, + }, + _ => false, + } + } +} + +impl Dispatcher where - T: AsyncRead + AsyncWrite, - S: Service + 'static, - S::Error: Debug, + T: IoStream, + S: Service, + S::Error: Into, S::Response: Into>, B: MessageBody, + X: Service, + X::Error: Into, + U: Service), Response = ()>, + U::Error: fmt::Display, { /// Create http/1 dispatcher. - pub fn new(stream: T, config: ServiceConfig, service: CloneableService) -> Self { + pub fn new( + stream: T, + config: ServiceConfig, + service: CloneableService, + expect: CloneableService, + upgrade: Option>, + ) -> Self { Dispatcher::with_timeout( - Framed::new(stream, Codec::new(config.clone())), + stream, + Codec::new(config.clone()), config, + BytesMut::with_capacity(HW_BUFFER_SIZE), None, service, + expect, + upgrade, ) } /// Create http/1 dispatcher with slow request timeout. pub fn with_timeout( - framed: Framed, + io: T, + codec: Codec, config: ServiceConfig, + read_buf: BytesMut, timeout: Option, service: CloneableService, + expect: CloneableService, + upgrade: Option>, ) -> Self { let keepalive = config.keep_alive_enabled(); let flags = if keepalive { - Flags::KEEPALIVE | Flags::KEEPALIVE_ENABLED + Flags::KEEPALIVE } else { Flags::empty() }; @@ -125,15 +221,20 @@ where }; Dispatcher { - inner: Some(InnerDispatcher { - framed, + inner: DispatcherState::Normal(InnerDispatcher { + write_buf: BytesMut::with_capacity(HW_BUFFER_SIZE), payload: None, state: State::None, error: None, + peer_addr: io.peer_addr(), messages: VecDeque::new(), + io, + codec, + read_buf, service, + expect, + upgrade, flags, - config, ka_expire, ka_timer, }), @@ -141,20 +242,25 @@ where } } -impl InnerDispatcher +impl InnerDispatcher where - T: AsyncRead + AsyncWrite, - S: Service + 'static, - S::Error: Debug, + T: IoStream, + S: Service, + S::Error: Into, S::Response: Into>, B: MessageBody, + X: Service, + X::Error: Into, + U: Service), Response = ()>, + U::Error: fmt::Display, { fn can_read(&self) -> bool { - if self.flags.contains(Flags::DISCONNECTED) { - return false; - } - - if let Some(ref info) = self.payload { + if self + .flags + .intersects(Flags::READ_DISCONNECT | Flags::UPGRADE) + { + false + } else if let Some(ref info) = self.payload { info.need_read() == PayloadStatus::Read } else { true @@ -163,41 +269,61 @@ where // if checked is set to true, delay disconnect until all tasks have finished. fn client_disconnected(&mut self) { - self.flags.insert(Flags::DISCONNECTED); + self.flags + .insert(Flags::READ_DISCONNECT | Flags::WRITE_DISCONNECT); if let Some(mut payload) = self.payload.take() { payload.set_error(PayloadError::Incomplete(None)); } } /// Flush stream - fn poll_flush(&mut self) -> Poll { - if !self.framed.is_write_buf_empty() { - match self.framed.poll_complete() { - Ok(Async::NotReady) => Ok(Async::NotReady), - Err(err) => { - debug!("Error sending data: {}", err); - Err(err.into()) - } - Ok(Async::Ready(_)) => { - // if payload is not consumed we can not use connection - if self.payload.is_some() && self.state.is_empty() { - return Err(DispatchError::PayloadIsNotConsumed); - } - Ok(Async::Ready(true)) - } - } - } else { - Ok(Async::Ready(false)) + /// + /// true - got whouldblock + /// false - didnt get whouldblock + fn poll_flush(&mut self) -> Result { + if self.write_buf.is_empty() { + return Ok(false); } + + let len = self.write_buf.len(); + let mut written = 0; + while written < len { + match self.io.write(&self.write_buf[written..]) { + Ok(0) => { + return Err(DispatchError::Io(io::Error::new( + io::ErrorKind::WriteZero, + "", + ))); + } + Ok(n) => { + written += n; + } + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { + if written > 0 { + let _ = self.write_buf.split_to(written); + } + return Ok(true); + } + Err(err) => return Err(DispatchError::Io(err)), + } + } + if written > 0 { + if written == self.write_buf.len() { + unsafe { self.write_buf.set_len(0) } + } else { + let _ = self.write_buf.split_to(written); + } + } + Ok(false) } fn send_response( &mut self, message: Response<()>, body: ResponseBody, - ) -> Result, DispatchError> { - self.framed - .force_send(Message::Item((message, body.length()))) + ) -> Result, DispatchError> { + self.codec + .encode(Message::Item((message, body.size())), &mut self.write_buf) .map_err(|err| { if let Some(mut payload) = self.payload.take() { payload.set_error(PayloadError::Incomplete(None)); @@ -205,91 +331,136 @@ where DispatchError::Io(err) })?; - self.flags - .set(Flags::KEEPALIVE, self.framed.get_codec().keepalive()); - match body.length() { + self.flags.set(Flags::KEEPALIVE, self.codec.keepalive()); + match body.size() { BodySize::None | BodySize::Empty => Ok(State::None), _ => Ok(State::SendPayload(body)), } } - fn poll_response(&mut self) -> Result<(), DispatchError> { - let mut retry = self.can_read(); + fn send_continue(&mut self) { + self.write_buf + .extend_from_slice(b"HTTP/1.1 100 Continue\r\n\r\n"); + } + + fn poll_response(&mut self) -> Result { loop { - let state = match mem::replace(&mut self.state, State::None) { + let state = match self.state { State::None => match self.messages.pop_front() { Some(DispatcherMessage::Item(req)) => { Some(self.handle_request(req)?) } Some(DispatcherMessage::Error(res)) => { - self.send_response(res, ResponseBody::Other(Body::Empty))?; - None + Some(self.send_response(res, ResponseBody::Other(Body::Empty))?) + } + Some(DispatcherMessage::Upgrade(req)) => { + return Ok(PollResponse::Upgrade(req)); } None => None, }, - State::ServiceCall(mut fut) => match fut.poll() { - Ok(Async::Ready(res)) => { - let (res, body) = res.into().replace_body(()); - Some(self.send_response(res, body)?) + State::ExpectCall(ref mut fut) => match fut.poll() { + Ok(Async::Ready(req)) => { + self.send_continue(); + self.state = State::ServiceCall(self.service.call(req)); + continue; } - Ok(Async::NotReady) => { - self.state = State::ServiceCall(fut); - None - } - Err(_e) => { - let res: Response = Response::InternalServerError().finish(); + Ok(Async::NotReady) => None, + Err(e) => { + let res: Response = e.into().into(); let (res, body) = res.replace_body(()); Some(self.send_response(res, body.into_body())?) } }, - State::SendPayload(mut stream) => { + State::ServiceCall(ref mut fut) => match fut.poll() { + Ok(Async::Ready(res)) => { + let (res, body) = res.into().replace_body(()); + self.state = self.send_response(res, body)?; + continue; + } + Ok(Async::NotReady) => None, + Err(e) => { + let res: Response = e.into().into(); + let (res, body) = res.replace_body(()); + Some(self.send_response(res, body.into_body())?) + } + }, + State::SendPayload(ref mut stream) => { loop { - if !self.framed.is_write_buf_full() { + if self.write_buf.len() < HW_BUFFER_SIZE { match stream .poll_next() .map_err(|_| DispatchError::Unknown)? { Async::Ready(Some(item)) => { - self.framed - .force_send(Message::Chunk(Some(item)))?; + self.codec.encode( + Message::Chunk(Some(item)), + &mut self.write_buf, + )?; continue; } Async::Ready(None) => { - self.framed.force_send(Message::Chunk(None))?; - } - Async::NotReady => { - self.state = State::SendPayload(stream); - return Ok(()); + self.codec.encode( + Message::Chunk(None), + &mut self.write_buf, + )?; + self.state = State::None; } + Async::NotReady => return Ok(PollResponse::DoNothing), } } else { - self.state = State::SendPayload(stream); - return Ok(()); + return Ok(PollResponse::DrainWriteBuf); } break; } - None + continue; } }; - match state { - Some(state) => self.state = state, - None => { - // if read-backpressure is enabled and we consumed some data. - // we may read more data and retry - if !retry && self.can_read() && self.poll_request()? { - retry = self.can_read(); + // set new state + if let Some(state) = state { + self.state = state; + if !self.state.is_empty() { + continue; + } + } else { + // if read-backpressure is enabled and we consumed some data. + // we may read more data and retry + if self.state.is_call() { + if self.poll_request()? { continue; } - break; + } else if !self.messages.is_empty() { + continue; } } + break; } - Ok(()) + Ok(PollResponse::DoNothing) } - fn handle_request(&mut self, req: Request) -> Result, DispatchError> { + fn handle_request(&mut self, req: Request) -> Result, DispatchError> { + // Handle `EXPECT: 100-Continue` header + let req = if req.head().expect() { + let mut task = self.expect.call(req); + match task.poll() { + Ok(Async::Ready(req)) => { + self.send_continue(); + req + } + Ok(Async::NotReady) => return Ok(State::ExpectCall(task)), + Err(e) => { + let e = e.into(); + let res: Response = e.into(); + let (res, body) = res.replace_body(()); + return self.send_response(res, body.into_body()); + } + } + } else { + req + }; + + // Call service let mut task = self.service.call(req); match task.poll() { Ok(Async::Ready(res)) => { @@ -297,8 +468,8 @@ where self.send_response(res, body) } Ok(Async::NotReady) => Ok(State::ServiceCall(task)), - Err(_e) => { - let res: Response = Response::InternalServerError().finish(); + Err(e) => { + let res: Response = e.into().into(); let (res, body) = res.replace_body(()); self.send_response(res, body.into_body()) } @@ -308,32 +479,32 @@ where /// Process one incoming requests pub(self) fn poll_request(&mut self) -> Result { // limit a mount of non processed requests - if self.messages.len() >= MAX_PIPELINED_MESSAGES { + if self.messages.len() >= MAX_PIPELINED_MESSAGES || !self.can_read() { return Ok(false); } let mut updated = false; loop { - match self.framed.poll() { - Ok(Async::Ready(Some(msg))) => { + match self.codec.decode(&mut self.read_buf) { + Ok(Some(msg)) => { updated = true; self.flags.insert(Flags::STARTED); match msg { Message::Item(mut req) => { - match self.framed.get_codec().message_type() { - MessageType::Payload | MessageType::Stream => { - let (ps, pl) = Payload::create(false); - let (req1, _) = - req.replace_payload(crate::Payload::H1(pl)); - req = req1; - self.payload = Some(ps); - } - //MessageType::Stream => { - // self.unhandled = Some(req); - // return Ok(updated); - //} - _ => (), + let pl = self.codec.message_type(); + req.head_mut().peer_addr = self.peer_addr; + + if pl == MessageType::Stream && self.upgrade.is_some() { + self.messages.push_back(DispatcherMessage::Upgrade(req)); + break; + } + if pl == MessageType::Payload || pl == MessageType::Stream { + let (ps, pl) = Payload::create(false); + let (req1, _) = + req.replace_payload(crate::Payload::H1(pl)); + req = req1; + self.payload = Some(ps); } // handle request early @@ -350,7 +521,7 @@ where error!( "Internal server error: unexpected payload chunk" ); - self.flags.insert(Flags::DISCONNECTED); + self.flags.insert(Flags::READ_DISCONNECT); self.messages.push_back(DispatcherMessage::Error( Response::InternalServerError().finish().drop_body(), )); @@ -363,7 +534,7 @@ where payload.feed_eof(); } else { error!("Internal server error: unexpected eof"); - self.flags.insert(Flags::DISCONNECTED); + self.flags.insert(Flags::READ_DISCONNECT); self.messages.push_back(DispatcherMessage::Error( Response::InternalServerError().finish().drop_body(), )); @@ -373,11 +544,7 @@ where } } } - Ok(Async::Ready(None)) => { - self.client_disconnected(); - break; - } - Ok(Async::NotReady) => break, + Ok(None) => break, Err(ParseError::Io(e)) => { self.client_disconnected(); self.error = Some(DispatchError::Io(e)); @@ -392,15 +559,15 @@ where self.messages.push_back(DispatcherMessage::Error( Response::BadRequest().finish().drop_body(), )); - self.flags.insert(Flags::DISCONNECTED); + self.flags.insert(Flags::READ_DISCONNECT); self.error = Some(e.into()); break; } } } - if self.ka_timer.is_some() && updated { - if let Some(expire) = self.config.keep_alive_expire() { + if updated && self.ka_timer.is_some() { + if let Some(expire) = self.codec.config.keep_alive_expire() { self.ka_expire = expire; } } @@ -412,10 +579,10 @@ where if self.ka_timer.is_none() { // shutdown timeout if self.flags.contains(Flags::SHUTDOWN) { - if let Some(interval) = self.config.client_disconnect_timer() { + if let Some(interval) = self.codec.config.client_disconnect_timer() { self.ka_timer = Some(Delay::new(interval)); } else { - self.flags.insert(Flags::DISCONNECTED); + self.flags.insert(Flags::READ_DISCONNECT); return Ok(()); } } else { @@ -433,13 +600,14 @@ where return Err(DispatchError::DisconnectTimeout); } else if self.ka_timer.as_mut().unwrap().deadline() >= self.ka_expire { // check for any outstanding tasks - if self.state.is_empty() && self.framed.is_write_buf_empty() { + if self.state.is_empty() && self.write_buf.is_empty() { if self.flags.contains(Flags::STARTED) { trace!("Keep-alive timeout, close connection"); self.flags.insert(Flags::SHUTDOWN); // start shutdown timer - if let Some(deadline) = self.config.client_disconnect_timer() + if let Some(deadline) = + self.codec.config.client_disconnect_timer() { if let Some(timer) = self.ka_timer.as_mut() { timer.reset(deadline); @@ -447,7 +615,7 @@ where } } else { // no shutdown timeout, drop socket - self.flags.insert(Flags::DISCONNECTED); + self.flags.insert(Flags::WRITE_DISCONNECT); return Ok(()); } } else { @@ -464,7 +632,8 @@ where self.flags.insert(Flags::STARTED | Flags::SHUTDOWN); self.state = State::None; } - } else if let Some(deadline) = self.config.keep_alive_expire() { + } else if let Some(deadline) = self.codec.config.keep_alive_expire() + { if let Some(timer) = self.ka_timer.as_mut() { timer.reset(deadline); let _ = timer.poll(); @@ -482,66 +651,167 @@ where } } -impl Future for Dispatcher +impl Future for Dispatcher where - T: AsyncRead + AsyncWrite, + T: IoStream, S: Service, - S::Error: Debug, + S::Error: Into, S::Response: Into>, B: MessageBody, + X: Service, + X::Error: Into, + U: Service), Response = ()>, + U::Error: fmt::Display, { type Item = (); type Error = DispatchError; #[inline] fn poll(&mut self) -> Poll { - let inner = self.inner.as_mut().unwrap(); + match self.inner { + DispatcherState::Normal(ref mut inner) => { + inner.poll_keepalive()?; - if inner.flags.contains(Flags::SHUTDOWN) { - inner.poll_keepalive()?; - if inner.flags.contains(Flags::DISCONNECTED) { - Ok(Async::Ready(())) - } else { - // try_ready!(inner.poll_flush()); - match inner.framed.get_mut().shutdown()? { - Async::Ready(_) => Ok(Async::Ready(())), - Async::NotReady => Ok(Async::NotReady), - } - } - } else { - inner.poll_keepalive()?; - inner.poll_request()?; - loop { - inner.poll_response()?; - if let Async::Ready(false) = inner.poll_flush()? { - break; - } - } - - if inner.flags.contains(Flags::DISCONNECTED) { - return Ok(Async::Ready(())); - } - - // keep-alive and stream errors - if inner.state.is_empty() && inner.framed.is_write_buf_empty() { - if let Some(err) = inner.error.take() { - Err(err) - } - // disconnect if keep-alive is not enabled - else if inner.flags.contains(Flags::STARTED) - && !inner.flags.intersects(Flags::KEEPALIVE) - { - inner.flags.insert(Flags::SHUTDOWN); - self.poll() - } - // disconnect if shutdown - else if inner.flags.contains(Flags::SHUTDOWN) { - self.poll() + if inner.flags.contains(Flags::SHUTDOWN) { + if inner.flags.contains(Flags::WRITE_DISCONNECT) { + Ok(Async::Ready(())) + } else { + // flush buffer + inner.poll_flush()?; + if !inner.write_buf.is_empty() { + Ok(Async::NotReady) + } else { + match inner.io.shutdown()? { + Async::Ready(_) => Ok(Async::Ready(())), + Async::NotReady => Ok(Async::NotReady), + } + } + } } else { - Ok(Async::NotReady) + // read socket into a buf + if !inner.flags.contains(Flags::READ_DISCONNECT) { + if let Some(true) = + read_available(&mut inner.io, &mut inner.read_buf)? + { + inner.flags.insert(Flags::READ_DISCONNECT) + } + } + + inner.poll_request()?; + loop { + if inner.write_buf.remaining_mut() < LW_BUFFER_SIZE { + inner.write_buf.reserve(HW_BUFFER_SIZE); + } + let result = inner.poll_response()?; + let drain = result == PollResponse::DrainWriteBuf; + + // switch to upgrade handler + if let PollResponse::Upgrade(req) = result { + if let DispatcherState::Normal(inner) = + std::mem::replace(&mut self.inner, DispatcherState::None) + { + let mut parts = FramedParts::with_read_buf( + inner.io, + inner.codec, + inner.read_buf, + ); + parts.write_buf = inner.write_buf; + let framed = Framed::from_parts(parts); + self.inner = DispatcherState::Upgrade( + inner.upgrade.unwrap().call((req, framed)), + ); + return self.poll(); + } else { + panic!() + } + } + + // we didnt get WouldBlock from write operation, + // so data get written to kernel completely (OSX) + // and we have to write again otherwise response can get stuck + if inner.poll_flush()? || !drain { + break; + } + } + + // client is gone + if inner.flags.contains(Flags::WRITE_DISCONNECT) { + return Ok(Async::Ready(())); + } + + let is_empty = inner.state.is_empty(); + + // read half is closed and we do not processing any responses + if inner.flags.contains(Flags::READ_DISCONNECT) && is_empty { + inner.flags.insert(Flags::SHUTDOWN); + } + + // keep-alive and stream errors + if is_empty && inner.write_buf.is_empty() { + if let Some(err) = inner.error.take() { + Err(err) + } + // disconnect if keep-alive is not enabled + else if inner.flags.contains(Flags::STARTED) + && !inner.flags.intersects(Flags::KEEPALIVE) + { + inner.flags.insert(Flags::SHUTDOWN); + self.poll() + } + // disconnect if shutdown + else if inner.flags.contains(Flags::SHUTDOWN) { + self.poll() + } else { + Ok(Async::NotReady) + } + } else { + Ok(Async::NotReady) + } } - } else { - Ok(Async::NotReady) + } + DispatcherState::Upgrade(ref mut fut) => fut.poll().map_err(|e| { + error!("Upgrade handler error: {}", e); + DispatchError::Upgrade + }), + DispatcherState::None => panic!(), + } + } +} + +fn read_available(io: &mut T, buf: &mut BytesMut) -> Result, io::Error> +where + T: io::Read, +{ + let mut read_some = false; + loop { + if buf.remaining_mut() < LW_BUFFER_SIZE { + buf.reserve(HW_BUFFER_SIZE); + } + + let read = unsafe { io.read(buf.bytes_mut()) }; + match read { + Ok(n) => { + if n == 0 { + return Ok(Some(true)); + } else { + read_some = true; + unsafe { + buf.advance_mut(n); + } + } + } + Err(e) => { + return if e.kind() == io::ErrorKind::WouldBlock { + if read_some { + Ok(Some(false)) + } else { + Ok(None) + } + } else if e.kind() == io::ErrorKind::ConnectionReset && read_some { + Ok(Some(true)) + } else { + Err(e) + }; } } } @@ -549,87 +819,35 @@ where #[cfg(test)] mod tests { - use std::{cmp, io}; - - use actix_codec::{AsyncRead, AsyncWrite}; use actix_service::IntoService; - use bytes::{Buf, Bytes}; use futures::future::{lazy, ok}; use super::*; use crate::error::Error; - - struct Buffer { - buf: Bytes, - err: Option, - } - - impl Buffer { - fn new(data: &'static str) -> Buffer { - Buffer { - buf: Bytes::from(data), - err: None, - } - } - } - - impl AsyncRead for Buffer {} - impl io::Read for Buffer { - fn read(&mut self, dst: &mut [u8]) -> Result { - if self.buf.is_empty() { - if self.err.is_some() { - Err(self.err.take().unwrap()) - } else { - Err(io::Error::new(io::ErrorKind::WouldBlock, "")) - } - } else { - let size = cmp::min(self.buf.len(), dst.len()); - let b = self.buf.split_to(size); - dst[..size].copy_from_slice(&b); - Ok(size) - } - } - } - - 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) - } - } + use crate::h1::{ExpectHandler, UpgradeHandler}; + use crate::test::TestBuffer; #[test] fn test_req_parse_err() { let mut sys = actix_rt::System::new("test"); let _ = sys.block_on(lazy(|| { - let buf = Buffer::new("GET /test HTTP/1\r\n\r\n"); + let buf = TestBuffer::new("GET /test HTTP/1\r\n\r\n"); - let mut h1 = Dispatcher::new( + let mut h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( buf, ServiceConfig::default(), CloneableService::new( (|_| ok::<_, Error>(Response::Ok().finish())).into_service(), ), + CloneableService::new(ExpectHandler), + None, ); - assert!(h1.poll().is_ok()); - assert!(h1.poll().is_ok()); - assert!(h1 - .inner - .as_ref() - .unwrap() - .flags - .contains(Flags::DISCONNECTED)); - // assert_eq!(h1.tasks.len(), 1); + assert!(h1.poll().is_err()); + + if let DispatcherState::Normal(ref inner) = h1.inner { + assert!(inner.flags.contains(Flags::READ_DISCONNECT)); + assert_eq!(&inner.io.write_buf[..26], b"HTTP/1.1 400 Bad Request\r\n"); + } ok::<_, ()>(()) })); } diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index 068237b0c..177661b5b 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -6,15 +6,15 @@ use std::str::FromStr; use std::{cmp, fmt, io, mem}; use bytes::{BufMut, Bytes, BytesMut}; -use http::header::{ - HeaderValue, ACCEPT_ENCODING, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, -}; -use http::{HeaderMap, Method, StatusCode, Version}; use crate::body::BodySize; use crate::config::ServiceConfig; -use crate::header::ContentEncoding; +use crate::header::{map, ContentEncoding}; use crate::helpers; +use crate::http::header::{ + HeaderValue, ACCEPT_ENCODING, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, +}; +use crate::http::{HeaderMap, Method, StatusCode, Version}; use crate::message::{ConnectionType, Head, RequestHead, ResponseHead}; use crate::request::Request; use crate::response::Response; @@ -41,8 +41,6 @@ impl Default for MessageEncoder { pub(crate) trait MessageType: Sized { fn status(&self) -> Option; - // fn connection_type(&self) -> Option; - fn headers(&self) -> &HeaderMap; fn upper_camel_case(&self) -> bool { @@ -80,32 +78,31 @@ pub(crate) trait MessageType: Sized { match length { BodySize::Stream => { if chunked { - dst.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") + dst.put_slice(b"\r\ntransfer-encoding: chunked\r\n") } else { skip_len = false; - dst.extend_from_slice(b"\r\n"); + dst.put_slice(b"\r\n"); } } BodySize::Empty => { - dst.extend_from_slice(b"\r\ncontent-length: 0\r\n"); + dst.put_slice(b"\r\ncontent-length: 0\r\n"); } BodySize::Sized(len) => helpers::write_content_length(len, dst), BodySize::Sized64(len) => { - dst.extend_from_slice(b"\r\ncontent-length: "); - write!(dst.writer(), "{}", len)?; - dst.extend_from_slice(b"\r\n"); + dst.put_slice(b"\r\ncontent-length: "); + write!(dst.writer(), "{}\r\n", len)?; } - BodySize::None => dst.extend_from_slice(b"\r\n"), + BodySize::None => dst.put_slice(b"\r\n"), } // Connection match ctype { - ConnectionType::Upgrade => dst.extend_from_slice(b"connection: upgrade\r\n"), + ConnectionType::Upgrade => dst.put_slice(b"connection: upgrade\r\n"), ConnectionType::KeepAlive if version < Version::HTTP_11 => { - dst.extend_from_slice(b"connection: keep-alive\r\n") + dst.put_slice(b"connection: keep-alive\r\n") } ConnectionType::Close if version >= Version::HTTP_11 => { - dst.extend_from_slice(b"connection: close\r\n") + dst.put_slice(b"connection: close\r\n") } _ => (), } @@ -115,7 +112,7 @@ pub(crate) trait MessageType: Sized { let mut has_date = false; let mut remaining = dst.remaining_mut(); let mut buf = unsafe { &mut *(dst.bytes_mut() as *mut [u8]) }; - for (key, value) in self.headers() { + for (key, value) in self.headers().inner.iter() { match *key { CONNECTION => continue, TRANSFER_ENCODING | CONTENT_LENGTH if skip_len => continue, @@ -124,38 +121,59 @@ pub(crate) trait MessageType: Sized { } _ => (), } - - let v = value.as_ref(); let k = key.as_str().as_bytes(); - let len = k.len() + v.len() + 4; - if len > remaining { - unsafe { - dst.advance_mut(pos); + match value { + map::Value::One(ref val) => { + let v = val.as_ref(); + let len = k.len() + v.len() + 4; + if len > remaining { + unsafe { + dst.advance_mut(pos); + } + pos = 0; + dst.reserve(len * 2); + remaining = dst.remaining_mut(); + unsafe { + buf = &mut *(dst.bytes_mut() as *mut _); + } + } + buf[pos..pos + k.len()].copy_from_slice(k); + pos += k.len(); + buf[pos..pos + 2].copy_from_slice(b": "); + pos += 2; + buf[pos..pos + v.len()].copy_from_slice(v); + pos += v.len(); + buf[pos..pos + 2].copy_from_slice(b"\r\n"); + pos += 2; + remaining -= len; } - pos = 0; - dst.reserve(len); - remaining = dst.remaining_mut(); - unsafe { - buf = &mut *(dst.bytes_mut() as *mut _); + map::Value::Multi(ref vec) => { + for val in vec { + let v = val.as_ref(); + let len = k.len() + v.len() + 4; + if len > remaining { + unsafe { + dst.advance_mut(pos); + } + pos = 0; + dst.reserve(len * 2); + remaining = dst.remaining_mut(); + unsafe { + buf = &mut *(dst.bytes_mut() as *mut _); + } + } + buf[pos..pos + k.len()].copy_from_slice(k); + pos += k.len(); + buf[pos..pos + 2].copy_from_slice(b": "); + pos += 2; + buf[pos..pos + v.len()].copy_from_slice(v); + pos += v.len(); + buf[pos..pos + 2].copy_from_slice(b"\r\n"); + pos += 2; + remaining -= len; + } } } - - buf[pos..pos + k.len()].copy_from_slice(k); - pos += k.len(); - buf[pos..pos + 2].copy_from_slice(b": "); - pos += 2; - - // use upper Camel-Case - if self.upper_camel_case() { - write_upper_camel_case(v, &mut buf[pos..pos + v.len()]); - } else { - buf[pos..pos + v.len()].copy_from_slice(v); - } - pos += v.len(); - - buf[pos..pos + 2].copy_from_slice(b"\r\n"); - pos += 2; - remaining -= len; } unsafe { dst.advance_mut(pos); @@ -182,10 +200,6 @@ impl MessageType for Response<()> { self.head().chunked() } - //fn connection_type(&self) -> Option { - // self.head().ctype - //} - fn headers(&self) -> &HeaderMap { &self.head().headers } @@ -197,7 +211,7 @@ impl MessageType for Response<()> { // status line helpers::write_status_line(head.version, head.status.as_u16(), dst); - dst.extend_from_slice(reason); + dst.put_slice(reason); Ok(()) } } @@ -220,6 +234,7 @@ impl MessageType for RequestHead { } fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()> { + dst.reserve(256 + self.headers.len() * AVERAGE_HEADER_SIZE); write!( Writer(dst), "{} {} {}", diff --git a/actix-http/src/h1/expect.rs b/actix-http/src/h1/expect.rs new file mode 100644 index 000000000..86fcb2cc3 --- /dev/null +++ b/actix-http/src/h1/expect.rs @@ -0,0 +1,36 @@ +use actix_service::{NewService, Service}; +use futures::future::{ok, FutureResult}; +use futures::{Async, Poll}; + +use crate::error::Error; +use crate::request::Request; + +pub struct ExpectHandler; + +impl NewService for ExpectHandler { + type Request = Request; + type Response = Request; + type Error = Error; + type Service = ExpectHandler; + type InitError = Error; + type Future = FutureResult; + + fn new_service(&self, _: &()) -> Self::Future { + ok(ExpectHandler) + } +} + +impl Service for ExpectHandler { + type Request = Request; + type Response = Request; + type Error = Error; + type Future = FutureResult; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, req: Request) -> Self::Future { + ok(req) + } +} diff --git a/actix-http/src/h1/mod.rs b/actix-http/src/h1/mod.rs index 472d73477..0c85f076a 100644 --- a/actix-http/src/h1/mod.rs +++ b/actix-http/src/h1/mod.rs @@ -6,14 +6,20 @@ mod codec; mod decoder; mod dispatcher; mod encoder; +mod expect; mod payload; mod service; +mod upgrade; +mod utils; pub use self::client::{ClientCodec, ClientPayloadCodec}; pub use self::codec::Codec; pub use self::dispatcher::Dispatcher; -pub use self::payload::{Payload, PayloadBuffer}; +pub use self::expect::ExpectHandler; +pub use self::payload::Payload; pub use self::service::{H1Service, H1ServiceHandler, OneRequest}; +pub use self::upgrade::UpgradeHandler; +pub use self::utils::SendResponse; #[derive(Debug)] /// Codec message diff --git a/actix-http/src/h1/payload.rs b/actix-http/src/h1/payload.rs index 73d05c4bb..28acb64bb 100644 --- a/actix-http/src/h1/payload.rs +++ b/actix-http/src/h1/payload.rs @@ -1,10 +1,9 @@ //! Payload stream use std::cell::RefCell; -use std::cmp; use std::collections::VecDeque; use std::rc::{Rc, Weak}; -use bytes::{Bytes, BytesMut}; +use bytes::Bytes; use futures::task::current as current_task; use futures::task::Task; use futures::{Async, Poll, Stream}; @@ -15,7 +14,7 @@ use crate::error::PayloadError; pub(crate) const MAX_BUFFER_SIZE: usize = 32_768; #[derive(Debug, PartialEq)] -pub(crate) enum PayloadStatus { +pub enum PayloadStatus { Read, Pause, Dropped, @@ -78,14 +77,6 @@ impl Payload { pub fn unread_data(&mut self, data: Bytes) { self.inner.borrow_mut().unread_data(data); } - - #[inline] - /// Set read buffer capacity - /// - /// Default buffer capacity is 32Kb. - pub fn set_read_buffer_capacity(&mut self, cap: usize) { - self.inner.borrow_mut().capacity = cap; - } } impl Stream for Payload { @@ -98,58 +89,35 @@ impl Stream for Payload { } } -impl Clone for Payload { - fn clone(&self) -> Payload { - Payload { - inner: Rc::clone(&self.inner), - } - } -} - -/// Payload writer interface. -pub(crate) 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); - - /// Need read data - fn need_read(&self) -> PayloadStatus; -} - /// Sender part of the payload stream pub struct PayloadSender { inner: Weak>, } -impl PayloadWriter for PayloadSender { +impl PayloadSender { #[inline] - fn set_error(&mut self, err: PayloadError) { + pub fn set_error(&mut self, err: PayloadError) { if let Some(shared) = self.inner.upgrade() { shared.borrow_mut().set_error(err) } } #[inline] - fn feed_eof(&mut self) { + pub fn feed_eof(&mut self) { if let Some(shared) = self.inner.upgrade() { shared.borrow_mut().feed_eof() } } #[inline] - fn feed_data(&mut self, data: Bytes) { + pub fn feed_data(&mut self, data: Bytes) { if let Some(shared) = self.inner.upgrade() { shared.borrow_mut().feed_data(data) } } #[inline] - fn need_read(&self) -> PayloadStatus { + pub fn need_read(&self) -> PayloadStatus { // we check need_read only if Payload (other side) is alive, // otherwise always return true (consume payload) if let Some(shared) = self.inner.upgrade() { @@ -177,7 +145,6 @@ struct Inner { err: Option, need_read: bool, items: VecDeque, - capacity: usize, task: Option, io_task: Option, } @@ -190,7 +157,6 @@ impl Inner { err: None, items: VecDeque::new(), need_read: true, - capacity: MAX_BUFFER_SIZE, task: None, io_task: None, } @@ -210,7 +176,7 @@ impl Inner { fn feed_data(&mut self, data: Bytes) { self.len += data.len(); self.items.push_back(data); - self.need_read = self.len < self.capacity; + self.need_read = self.len < MAX_BUFFER_SIZE; if let Some(task) = self.task.take() { task.notify() } @@ -224,7 +190,7 @@ impl Inner { fn readany(&mut self) -> Poll, PayloadError> { if let Some(data) = self.items.pop_front() { self.len -= data.len(); - self.need_read = self.len < self.capacity; + self.need_read = self.len < MAX_BUFFER_SIZE; if self.need_read && self.task.is_none() && !self.eof { self.task = Some(current_task()); @@ -258,407 +224,12 @@ impl Inner { } } -/// Payload buffer -pub struct PayloadBuffer { - len: usize, - items: VecDeque, - stream: S, -} - -impl PayloadBuffer -where - S: Stream, -{ - /// Create new `PayloadBuffer` instance - pub fn new(stream: S) -> Self { - PayloadBuffer { - len: 0, - items: VecDeque::new(), - stream, - } - } - - /// Get mutable reference to an inner stream. - pub fn get_mut(&mut self) -> &mut S { - &mut self.stream - } - - #[inline] - fn poll_stream(&mut self) -> Poll { - self.stream.poll().map(|res| match res { - Async::Ready(Some(data)) => { - self.len += data.len(); - self.items.push_back(data); - Async::Ready(true) - } - Async::Ready(None) => Async::Ready(false), - Async::NotReady => Async::NotReady, - }) - } - - /// Read first available chunk of bytes - #[inline] - pub fn readany(&mut self) -> Poll, PayloadError> { - if let Some(data) = self.items.pop_front() { - self.len -= data.len(); - Ok(Async::Ready(Some(data))) - } else { - match self.poll_stream()? { - Async::Ready(true) => self.readany(), - Async::Ready(false) => Ok(Async::Ready(None)), - Async::NotReady => Ok(Async::NotReady), - } - } - } - - /// Check if buffer contains enough bytes - #[inline] - pub fn can_read(&mut self, size: usize) -> Poll, PayloadError> { - if size <= self.len { - Ok(Async::Ready(Some(true))) - } else { - match self.poll_stream()? { - Async::Ready(true) => self.can_read(size), - Async::Ready(false) => Ok(Async::Ready(None)), - Async::NotReady => Ok(Async::NotReady), - } - } - } - - /// Return reference to the first chunk of data - #[inline] - pub fn get_chunk(&mut self) -> Poll, PayloadError> { - if self.items.is_empty() { - match self.poll_stream()? { - Async::Ready(true) => (), - Async::Ready(false) => return Ok(Async::Ready(None)), - Async::NotReady => return Ok(Async::NotReady), - } - } - match self.items.front().map(|c| c.as_ref()) { - Some(chunk) => Ok(Async::Ready(Some(chunk))), - None => Ok(Async::NotReady), - } - } - - /// Read exact number of bytes - #[inline] - pub fn read_exact(&mut self, size: usize) -> Poll, PayloadError> { - if size <= self.len { - self.len -= size; - let mut chunk = self.items.pop_front().unwrap(); - if size < chunk.len() { - let buf = chunk.split_to(size); - self.items.push_front(chunk); - Ok(Async::Ready(Some(buf))) - } else if size == chunk.len() { - Ok(Async::Ready(Some(chunk))) - } else { - let mut buf = BytesMut::with_capacity(size); - buf.extend_from_slice(&chunk); - - while buf.len() < size { - let mut chunk = self.items.pop_front().unwrap(); - let rem = cmp::min(size - buf.len(), chunk.len()); - buf.extend_from_slice(&chunk.split_to(rem)); - if !chunk.is_empty() { - self.items.push_front(chunk); - } - } - Ok(Async::Ready(Some(buf.freeze()))) - } - } else { - match self.poll_stream()? { - Async::Ready(true) => self.read_exact(size), - Async::Ready(false) => Ok(Async::Ready(None)), - Async::NotReady => Ok(Async::NotReady), - } - } - } - - /// Remove specified amount if bytes from buffer - #[inline] - pub fn drop_bytes(&mut self, size: usize) { - if size <= self.len { - self.len -= size; - - let mut len = 0; - while len < size { - let mut chunk = self.items.pop_front().unwrap(); - let rem = cmp::min(size - len, chunk.len()); - len += rem; - if rem < chunk.len() { - chunk.split_to(rem); - self.items.push_front(chunk); - } - } - } - } - - /// Copy buffered data - pub fn copy(&mut self, size: usize) -> Poll, PayloadError> { - if size <= self.len { - let mut buf = BytesMut::with_capacity(size); - for chunk in &self.items { - if buf.len() < size { - let rem = cmp::min(size - buf.len(), chunk.len()); - buf.extend_from_slice(&chunk[..rem]); - } - if buf.len() == size { - return Ok(Async::Ready(Some(buf))); - } - } - } - - match self.poll_stream()? { - Async::Ready(true) => self.copy(size), - Async::Ready(false) => Ok(Async::Ready(None)), - Async::NotReady => Ok(Async::NotReady), - } - } - - /// Read until specified ending - pub fn read_until(&mut self, line: &[u8]) -> Poll, PayloadError> { - let mut idx = 0; - let mut num = 0; - let mut offset = 0; - let mut found = false; - let mut length = 0; - - for no in 0..self.items.len() { - { - let chunk = &self.items[no]; - for (pos, ch) in chunk.iter().enumerate() { - if *ch == line[idx] { - idx += 1; - if idx == line.len() { - num = no; - offset = pos + 1; - length += pos + 1; - found = true; - break; - } - } else { - idx = 0 - } - } - if !found { - length += chunk.len() - } - } - - if found { - let mut buf = BytesMut::with_capacity(length); - if num > 0 { - for _ in 0..num { - buf.extend_from_slice(&self.items.pop_front().unwrap()); - } - } - if offset > 0 { - let mut chunk = self.items.pop_front().unwrap(); - buf.extend_from_slice(&chunk.split_to(offset)); - if !chunk.is_empty() { - self.items.push_front(chunk) - } - } - self.len -= length; - return Ok(Async::Ready(Some(buf.freeze()))); - } - } - - match self.poll_stream()? { - Async::Ready(true) => self.read_until(line), - Async::Ready(false) => Ok(Async::Ready(None)), - Async::NotReady => Ok(Async::NotReady), - } - } - - /// Read bytes until new line delimiter - pub fn readline(&mut self) -> Poll, PayloadError> { - self.read_until(b"\n") - } - - /// Put unprocessed data back to the buffer - pub fn unprocessed(&mut self, data: Bytes) { - self.len += data.len(); - self.items.push_front(data); - } - - /// Get remaining data from the buffer - pub fn remaining(&mut self) -> Bytes { - self.items - .iter_mut() - .fold(BytesMut::new(), |mut b, c| { - b.extend_from_slice(c); - b - }) - .freeze() - } -} - #[cfg(test)] mod tests { use super::*; use actix_rt::Runtime; use futures::future::{lazy, result}; - #[test] - fn test_basic() { - Runtime::new() - .unwrap() - .block_on(lazy(|| { - let (_, payload) = Payload::create(false); - let mut payload = PayloadBuffer::new(payload); - - assert_eq!(payload.len, 0); - assert_eq!(Async::NotReady, payload.readany().ok().unwrap()); - - let res: Result<(), ()> = Ok(()); - result(res) - })) - .unwrap(); - } - - #[test] - fn test_eof() { - Runtime::new() - .unwrap() - .block_on(lazy(|| { - let (mut sender, payload) = Payload::create(false); - let mut payload = PayloadBuffer::new(payload); - - assert_eq!(Async::NotReady, payload.readany().ok().unwrap()); - sender.feed_data(Bytes::from("data")); - sender.feed_eof(); - - assert_eq!( - Async::Ready(Some(Bytes::from("data"))), - payload.readany().ok().unwrap() - ); - assert_eq!(payload.len, 0); - assert_eq!(Async::Ready(None), payload.readany().ok().unwrap()); - - let res: Result<(), ()> = Ok(()); - result(res) - })) - .unwrap(); - } - - #[test] - fn test_err() { - Runtime::new() - .unwrap() - .block_on(lazy(|| { - let (mut sender, payload) = Payload::create(false); - let mut payload = PayloadBuffer::new(payload); - - assert_eq!(Async::NotReady, payload.readany().ok().unwrap()); - - sender.set_error(PayloadError::Incomplete(None)); - payload.readany().err().unwrap(); - let res: Result<(), ()> = Ok(()); - result(res) - })) - .unwrap(); - } - - #[test] - fn test_readany() { - Runtime::new() - .unwrap() - .block_on(lazy(|| { - let (mut sender, payload) = Payload::create(false); - let mut payload = PayloadBuffer::new(payload); - - sender.feed_data(Bytes::from("line1")); - sender.feed_data(Bytes::from("line2")); - - assert_eq!( - Async::Ready(Some(Bytes::from("line1"))), - payload.readany().ok().unwrap() - ); - assert_eq!(payload.len, 0); - - assert_eq!( - Async::Ready(Some(Bytes::from("line2"))), - payload.readany().ok().unwrap() - ); - assert_eq!(payload.len, 0); - - let res: Result<(), ()> = Ok(()); - result(res) - })) - .unwrap(); - } - - #[test] - fn test_readexactly() { - Runtime::new() - .unwrap() - .block_on(lazy(|| { - let (mut sender, payload) = Payload::create(false); - let mut payload = PayloadBuffer::new(payload); - - assert_eq!(Async::NotReady, payload.read_exact(2).ok().unwrap()); - - sender.feed_data(Bytes::from("line1")); - sender.feed_data(Bytes::from("line2")); - - assert_eq!( - Async::Ready(Some(Bytes::from_static(b"li"))), - payload.read_exact(2).ok().unwrap() - ); - assert_eq!(payload.len, 3); - - assert_eq!( - Async::Ready(Some(Bytes::from_static(b"ne1l"))), - payload.read_exact(4).ok().unwrap() - ); - assert_eq!(payload.len, 4); - - sender.set_error(PayloadError::Incomplete(None)); - payload.read_exact(10).err().unwrap(); - - let res: Result<(), ()> = Ok(()); - result(res) - })) - .unwrap(); - } - - #[test] - fn test_readuntil() { - Runtime::new() - .unwrap() - .block_on(lazy(|| { - let (mut sender, payload) = Payload::create(false); - let mut payload = PayloadBuffer::new(payload); - - assert_eq!(Async::NotReady, payload.read_until(b"ne").ok().unwrap()); - - sender.feed_data(Bytes::from("line1")); - sender.feed_data(Bytes::from("line2")); - - assert_eq!( - Async::Ready(Some(Bytes::from("line"))), - payload.read_until(b"ne").ok().unwrap() - ); - assert_eq!(payload.len, 1); - - assert_eq!( - Async::Ready(Some(Bytes::from("1line2"))), - payload.read_until(b"2").ok().unwrap() - ); - assert_eq!(payload.len, 0); - - sender.set_error(PayloadError::Incomplete(None)); - payload.read_until(b"b").err().unwrap(); - - let res: Result<(), ()> = Ok(()); - result(res) - })) - .unwrap(); - } - #[test] fn test_unread_data() { Runtime::new() diff --git a/actix-http/src/h1/service.rs b/actix-http/src/h1/service.rs index f3301b9b2..ecf6c8b93 100644 --- a/actix-http/src/h1/service.rs +++ b/actix-http/src/h1/service.rs @@ -1,8 +1,8 @@ -use std::fmt::Debug; +use std::fmt; use std::marker::PhantomData; -use actix_codec::{AsyncRead, AsyncWrite, Framed}; -use actix_server_config::{Io, ServerConfig as SrvConfig}; +use actix_codec::Framed; +use actix_server_config::{Io, IoStream, ServerConfig as SrvConfig}; use actix_service::{IntoNewService, NewService, Service}; use actix_utils::cloneable::CloneableService; use futures::future::{ok, FutureResult}; @@ -10,27 +10,29 @@ use futures::{try_ready, Async, Future, IntoFuture, Poll, Stream}; use crate::body::MessageBody; use crate::config::{KeepAlive, ServiceConfig}; -use crate::error::{DispatchError, ParseError}; +use crate::error::{DispatchError, Error, ParseError}; use crate::request::Request; use crate::response::Response; use super::codec::Codec; use super::dispatcher::Dispatcher; -use super::Message; +use super::{ExpectHandler, Message, UpgradeHandler}; /// `NewService` implementation for HTTP1 transport -pub struct H1Service { +pub struct H1Service> { srv: S, cfg: ServiceConfig, + expect: X, + upgrade: Option, _t: PhantomData<(T, P, B)>, } impl H1Service where S: NewService, - S::Error: Debug, + S::Error: Into, + S::InitError: fmt::Debug, S::Response: Into>, - S::Service: 'static, B: MessageBody, { /// Create new `HttpService` instance with default config. @@ -40,6 +42,8 @@ where H1Service { cfg, srv: service.into_new_service(), + expect: ExpectHandler, + upgrade: None, _t: PhantomData, } } @@ -52,30 +56,81 @@ where H1Service { cfg, srv: service.into_new_service(), + expect: ExpectHandler, + upgrade: None, _t: PhantomData, } } } -impl NewService for H1Service +impl H1Service where - T: AsyncRead + AsyncWrite, S: NewService, - S::Error: Debug, + S::Error: Into, S::Response: Into>, - S::Service: 'static, + S::InitError: fmt::Debug, B: MessageBody, +{ + pub fn expect(self, expect: X1) -> H1Service + where + X1: NewService, + X1::Error: Into, + X1::InitError: fmt::Debug, + { + H1Service { + expect, + cfg: self.cfg, + srv: self.srv, + upgrade: self.upgrade, + _t: PhantomData, + } + } + + pub fn upgrade(self, upgrade: Option) -> H1Service + where + U1: NewService), Response = ()>, + U1::Error: fmt::Display, + U1::InitError: fmt::Debug, + { + H1Service { + upgrade, + cfg: self.cfg, + srv: self.srv, + expect: self.expect, + _t: PhantomData, + } + } +} + +impl NewService for H1Service +where + T: IoStream, + S: NewService, + S::Error: Into, + S::Response: Into>, + S::InitError: fmt::Debug, + B: MessageBody, + X: NewService, + X::Error: Into, + X::InitError: fmt::Debug, + U: NewService), Response = ()>, + U::Error: fmt::Display, + U::InitError: fmt::Debug, { type Request = Io; type Response = (); type Error = DispatchError; - type InitError = S::InitError; - type Service = H1ServiceHandler; - type Future = H1ServiceResponse; + type InitError = (); + type Service = H1ServiceHandler; + type Future = H1ServiceResponse; fn new_service(&self, cfg: &SrvConfig) -> Self::Future { H1ServiceResponse { fut: self.srv.new_service(cfg).into_future(), + fut_ex: Some(self.expect.new_service(&())), + fut_upg: self.upgrade.as_ref().map(|f| f.new_service(&())), + expect: None, + upgrade: None, cfg: Some(self.cfg.clone()), _t: PhantomData, } @@ -83,78 +138,165 @@ where } #[doc(hidden)] -pub struct H1ServiceResponse, B> { - fut: ::Future, +pub struct H1ServiceResponse +where + S: NewService, + S::Error: Into, + S::InitError: fmt::Debug, + X: NewService, + X::Error: Into, + X::InitError: fmt::Debug, + U: NewService), Response = ()>, + U::Error: fmt::Display, + U::InitError: fmt::Debug, +{ + fut: S::Future, + fut_ex: Option, + fut_upg: Option, + expect: Option, + upgrade: Option, cfg: Option, _t: PhantomData<(T, P, B)>, } -impl Future for H1ServiceResponse +impl Future for H1ServiceResponse where - T: AsyncRead + AsyncWrite, + T: IoStream, S: NewService, - S::Service: 'static, - S::Error: Debug, + S::Error: Into, S::Response: Into>, + S::InitError: fmt::Debug, B: MessageBody, + X: NewService, + X::Error: Into, + X::InitError: fmt::Debug, + U: NewService), Response = ()>, + U::Error: fmt::Display, + U::InitError: fmt::Debug, { - type Item = H1ServiceHandler; - type Error = S::InitError; + type Item = H1ServiceHandler; + type Error = (); fn poll(&mut self) -> Poll { - let service = try_ready!(self.fut.poll()); + if let Some(ref mut fut) = self.fut_ex { + let expect = try_ready!(fut + .poll() + .map_err(|e| log::error!("Init http service error: {:?}", e))); + self.expect = Some(expect); + self.fut_ex.take(); + } + + if let Some(ref mut fut) = self.fut_upg { + let upgrade = try_ready!(fut + .poll() + .map_err(|e| log::error!("Init http service error: {:?}", e))); + self.upgrade = Some(upgrade); + self.fut_ex.take(); + } + + let service = try_ready!(self + .fut + .poll() + .map_err(|e| log::error!("Init http service error: {:?}", e))); Ok(Async::Ready(H1ServiceHandler::new( self.cfg.take().unwrap(), service, + self.expect.take().unwrap(), + self.upgrade.take(), ))) } } /// `Service` implementation for HTTP1 transport -pub struct H1ServiceHandler { +pub struct H1ServiceHandler { srv: CloneableService, + expect: CloneableService, + upgrade: Option>, cfg: ServiceConfig, _t: PhantomData<(T, P, B)>, } -impl H1ServiceHandler +impl H1ServiceHandler where S: Service, - S::Error: Debug, + S::Error: Into, S::Response: Into>, B: MessageBody, + X: Service, + X::Error: Into, + U: Service), Response = ()>, + U::Error: fmt::Display, { - fn new(cfg: ServiceConfig, srv: S) -> H1ServiceHandler { + fn new( + cfg: ServiceConfig, + srv: S, + expect: X, + upgrade: Option, + ) -> H1ServiceHandler { H1ServiceHandler { srv: CloneableService::new(srv), + expect: CloneableService::new(expect), + upgrade: upgrade.map(|s| CloneableService::new(s)), cfg, _t: PhantomData, } } } -impl Service for H1ServiceHandler +impl Service for H1ServiceHandler where - T: AsyncRead + AsyncWrite, + T: IoStream, S: Service, - S::Error: Debug, + S::Error: Into, S::Response: Into>, B: MessageBody, + X: Service, + X::Error: Into, + U: Service), Response = ()>, + U::Error: fmt::Display, { type Request = Io; type Response = (); type Error = DispatchError; - type Future = Dispatcher; + type Future = Dispatcher; fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.srv.poll_ready().map_err(|e| { - log::error!("Http service readiness error: {:?}", e); - DispatchError::Service - }) + let ready = self + .expect + .poll_ready() + .map_err(|e| { + let e = e.into(); + log::error!("Http service readiness error: {:?}", e); + DispatchError::Service(e) + })? + .is_ready(); + + let ready = self + .srv + .poll_ready() + .map_err(|e| { + let e = e.into(); + log::error!("Http service readiness error: {:?}", e); + DispatchError::Service(e) + })? + .is_ready() + && ready; + + if ready { + Ok(Async::Ready(())) + } else { + Ok(Async::NotReady) + } } fn call(&mut self, req: Self::Request) -> Self::Future { - Dispatcher::new(req.into_parts().0, self.cfg.clone(), self.srv.clone()) + Dispatcher::new( + req.into_parts().0, + self.cfg.clone(), + self.srv.clone(), + self.expect.clone(), + self.upgrade.clone(), + ) } } @@ -167,7 +309,7 @@ pub struct OneRequest { impl OneRequest where - T: AsyncRead + AsyncWrite, + T: IoStream, { /// Create new `H1SimpleService` instance. pub fn new() -> Self { @@ -180,7 +322,7 @@ where impl NewService for OneRequest where - T: AsyncRead + AsyncWrite, + T: IoStream, { type Request = Io; type Response = (Request, Framed); @@ -206,7 +348,7 @@ pub struct OneRequestService { impl Service for OneRequestService where - T: AsyncRead + AsyncWrite, + T: IoStream, { type Request = Io; type Response = (Request, Framed); @@ -230,14 +372,14 @@ where #[doc(hidden)] pub struct OneRequestServiceResponse where - T: AsyncRead + AsyncWrite, + T: IoStream, { framed: Option>, } impl Future for OneRequestServiceResponse where - T: AsyncRead + AsyncWrite, + T: IoStream, { type Item = (Request, Framed); type Error = ParseError; diff --git a/actix-http/src/h1/upgrade.rs b/actix-http/src/h1/upgrade.rs new file mode 100644 index 000000000..0d0164fe6 --- /dev/null +++ b/actix-http/src/h1/upgrade.rs @@ -0,0 +1,40 @@ +use std::marker::PhantomData; + +use actix_codec::Framed; +use actix_service::{NewService, Service}; +use futures::future::FutureResult; +use futures::{Async, Poll}; + +use crate::error::Error; +use crate::h1::Codec; +use crate::request::Request; + +pub struct UpgradeHandler(PhantomData); + +impl NewService for UpgradeHandler { + type Request = (Request, Framed); + type Response = (); + type Error = Error; + type Service = UpgradeHandler; + type InitError = Error; + type Future = FutureResult; + + fn new_service(&self, _: &()) -> Self::Future { + unimplemented!() + } +} + +impl Service for UpgradeHandler { + type Request = (Request, Framed); + type Response = (); + type Error = Error; + type Future = FutureResult; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, _: Self::Request) -> Self::Future { + unimplemented!() + } +} diff --git a/actix-http/src/h1/utils.rs b/actix-http/src/h1/utils.rs new file mode 100644 index 000000000..fdc4cf0bc --- /dev/null +++ b/actix-http/src/h1/utils.rs @@ -0,0 +1,92 @@ +use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use futures::{Async, Future, Poll, Sink}; + +use crate::body::{BodySize, MessageBody, ResponseBody}; +use crate::error::Error; +use crate::h1::{Codec, Message}; +use crate::response::Response; + +/// Send http/1 response +pub struct SendResponse { + res: Option, BodySize)>>, + body: Option>, + framed: Option>, +} + +impl SendResponse +where + B: MessageBody, +{ + pub fn new(framed: Framed, response: Response) -> Self { + let (res, body) = response.into_parts(); + + SendResponse { + res: Some((res, body.size()).into()), + body: Some(body), + framed: Some(framed), + } + } +} + +impl Future for SendResponse +where + T: AsyncRead + AsyncWrite, + B: MessageBody, +{ + type Item = Framed; + type Error = Error; + + fn poll(&mut self) -> Poll { + loop { + let mut body_ready = self.body.is_some(); + let framed = self.framed.as_mut().unwrap(); + + // send body + if self.res.is_none() && self.body.is_some() { + while body_ready && self.body.is_some() && !framed.is_write_buf_full() { + match self.body.as_mut().unwrap().poll_next()? { + Async::Ready(item) => { + // body is done + if item.is_none() { + let _ = self.body.take(); + } + framed.force_send(Message::Chunk(item))?; + } + Async::NotReady => body_ready = false, + } + } + } + + // flush write buffer + if !framed.is_write_buf_empty() { + match framed.poll_complete()? { + Async::Ready(_) => { + if body_ready { + continue; + } else { + return Ok(Async::NotReady); + } + } + Async::NotReady => return Ok(Async::NotReady), + } + } + + // send response + if let Some(res) = self.res.take() { + framed.force_send(res)?; + continue; + } + + if self.body.is_some() { + if body_ready { + continue; + } else { + return Ok(Async::NotReady); + } + } else { + break; + } + } + Ok(Async::Ready(self.framed.take().unwrap())) + } +} diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index 9b43be669..e66ff63c3 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -1,9 +1,10 @@ use std::collections::VecDeque; use std::marker::PhantomData; use std::time::Instant; -use std::{fmt, mem}; +use std::{fmt, mem, net}; use actix_codec::{AsyncRead, AsyncWrite}; +use actix_server_config::IoStream; use actix_service::Service; use actix_utils::cloneable::CloneableService; use bitflags::bitflags; @@ -29,14 +30,11 @@ use crate::response::Response; const CHUNK_SIZE: usize = 16_384; /// Dispatcher for HTTP/2 protocol -pub struct Dispatcher< - T: AsyncRead + AsyncWrite, - S: Service + 'static, - B: MessageBody, -> { +pub struct Dispatcher, B: MessageBody> { service: CloneableService, connection: Connection, config: ServiceConfig, + peer_addr: Option, ka_expire: Instant, ka_timer: Option, _t: PhantomData, @@ -44,9 +42,10 @@ pub struct Dispatcher< impl Dispatcher where - T: AsyncRead + AsyncWrite, - S: Service + 'static, - S::Error: fmt::Debug, + T: IoStream, + S: Service, + S::Error: Into, + S::Future: 'static, S::Response: Into>, B: MessageBody + 'static, { @@ -55,6 +54,7 @@ where connection: Connection, config: ServiceConfig, timeout: Option, + peer_addr: Option, ) -> Self { // let keepalive = config.keep_alive_enabled(); // let flags = if keepalive { @@ -75,9 +75,10 @@ where Dispatcher { service, config, + peer_addr, + connection, ka_expire, ka_timer, - connection, _t: PhantomData, } } @@ -85,9 +86,10 @@ where impl Future for Dispatcher where - T: AsyncRead + AsyncWrite, - S: Service + 'static, - S::Error: fmt::Debug, + T: IoStream, + S: Service, + S::Error: Into, + S::Future: 'static, S::Response: Into>, B: MessageBody + 'static, { @@ -114,8 +116,9 @@ where head.uri = parts.uri; head.method = parts.method; head.version = parts.version; - head.headers = parts.headers; - tokio_current_thread::spawn(ServiceResponse:: { + head.headers = parts.headers.into(); + head.peer_addr = self.peer_addr; + tokio_current_thread::spawn(ServiceResponse:: { state: ServiceResponseState::ServiceCall( self.service.call(req), Some(res), @@ -130,31 +133,31 @@ where } } -struct ServiceResponse { - state: ServiceResponseState, +struct ServiceResponse { + state: ServiceResponseState, config: ServiceConfig, buffer: Option, } -enum ServiceResponseState { - ServiceCall(S::Future, Option>), +enum ServiceResponseState { + ServiceCall(F, Option>), SendPayload(SendStream, ResponseBody), } -impl ServiceResponse +impl ServiceResponse where - S: Service + 'static, - S::Error: fmt::Debug, - S::Response: Into>, + F: Future, + F::Error: Into, + F::Item: Into>, B: MessageBody + 'static, { fn prepare_response( &self, head: &ResponseHead, - length: &mut BodySize, + size: &mut BodySize, ) -> http::Response<()> { let mut has_date = false; - let mut skip_len = length != &BodySize::Stream; + let mut skip_len = size != &BodySize::Stream; let mut res = http::Response::new(()); *res.status_mut() = head.status; @@ -164,14 +167,14 @@ where match head.status { http::StatusCode::NO_CONTENT | http::StatusCode::CONTINUE - | http::StatusCode::PROCESSING => *length = BodySize::None, + | http::StatusCode::PROCESSING => *size = BodySize::None, http::StatusCode::SWITCHING_PROTOCOLS => { skip_len = true; - *length = BodySize::Stream; + *size = BodySize::Stream; } _ => (), } - let _ = match length { + let _ = match size { BodySize::None | BodySize::Stream => None, BodySize::Empty => res .headers_mut() @@ -209,11 +212,11 @@ where } } -impl Future for ServiceResponse +impl Future for ServiceResponse where - S: Service + 'static, - S::Error: fmt::Debug, - S::Response: Into>, + F: Future, + F::Error: Into, + F::Item: Into>, B: MessageBody + 'static, { type Item = (); @@ -227,16 +230,15 @@ where let (res, body) = res.into().replace_body(()); let mut send = send.take().unwrap(); - let mut length = body.length(); - let h2_res = self.prepare_response(res.head(), &mut length); + let mut size = body.size(); + let h2_res = self.prepare_response(res.head(), &mut size); - let stream = send - .send_response(h2_res, length.is_eof()) - .map_err(|e| { + let stream = + send.send_response(h2_res, size.is_eof()).map_err(|e| { trace!("Error sending h2 response: {:?}", e); })?; - if length.is_eof() { + if size.is_eof() { Ok(Async::Ready(())) } else { self.state = ServiceResponseState::SendPayload(stream, body); @@ -249,16 +251,15 @@ where let (res, body) = res.replace_body(()); let mut send = send.take().unwrap(); - let mut length = body.length(); - let h2_res = self.prepare_response(res.head(), &mut length); + let mut size = body.size(); + let h2_res = self.prepare_response(res.head(), &mut size); - let stream = send - .send_response(h2_res, length.is_eof()) - .map_err(|e| { + let stream = + send.send_response(h2_res, size.is_eof()).map_err(|e| { trace!("Error sending h2 response: {:?}", e); })?; - if length.is_eof() { + if size.is_eof() { Ok(Async::Ready(())) } else { self.state = ServiceResponseState::SendPayload( diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index 9d9a19e24..42b8d8d82 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -3,7 +3,7 @@ use std::marker::PhantomData; use std::{io, net}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; -use actix_server_config::{Io, ServerConfig as SrvConfig}; +use actix_server_config::{Io, IoStream, ServerConfig as SrvConfig}; use actix_service::{IntoNewService, NewService, Service}; use actix_utils::cloneable::CloneableService; use bytes::Bytes; @@ -32,9 +32,9 @@ pub struct H2Service { impl H2Service where S: NewService, - S::Service: 'static, - S::Error: Debug + 'static, + S::Error: Into, S::Response: Into>, + ::Future: 'static, B: MessageBody + 'static, { /// Create new `HttpService` instance. @@ -63,11 +63,11 @@ where impl NewService for H2Service where - T: AsyncRead + AsyncWrite, + T: IoStream, S: NewService, - S::Service: 'static, - S::Error: Debug, + S::Error: Into, S::Response: Into>, + ::Future: 'static, B: MessageBody + 'static, { type Request = Io; @@ -95,11 +95,11 @@ pub struct H2ServiceResponse, impl Future for H2ServiceResponse where - T: AsyncRead + AsyncWrite, + T: IoStream, S: NewService, - S::Service: 'static, + S::Error: Into, S::Response: Into>, - S::Error: Debug, + ::Future: 'static, B: MessageBody + 'static, { type Item = H2ServiceHandler; @@ -115,7 +115,7 @@ where } /// `Service` implementation for http/2 transport -pub struct H2ServiceHandler { +pub struct H2ServiceHandler { srv: CloneableService, cfg: ServiceConfig, _t: PhantomData<(T, P, B)>, @@ -123,8 +123,9 @@ pub struct H2ServiceHandler { impl H2ServiceHandler where - S: Service + 'static, - S::Error: Debug, + S: Service, + S::Error: Into, + S::Future: 'static, S::Response: Into>, B: MessageBody + 'static, { @@ -139,9 +140,10 @@ where impl Service for H2ServiceHandler where - T: AsyncRead + AsyncWrite, - S: Service + 'static, - S::Error: Debug, + T: IoStream, + S: Service, + S::Error: Into, + S::Future: 'static, S::Response: Into>, B: MessageBody + 'static, { @@ -152,40 +154,45 @@ where fn poll_ready(&mut self) -> Poll<(), Self::Error> { self.srv.poll_ready().map_err(|e| { + let e = e.into(); error!("Service readiness error: {:?}", e); - DispatchError::Service + DispatchError::Service(e) }) } fn call(&mut self, req: Self::Request) -> Self::Future { + let io = req.into_parts().0; + let peer_addr = io.peer_addr(); H2ServiceHandlerResponse { state: State::Handshake( Some(self.srv.clone()), Some(self.cfg.clone()), - server::handshake(req.into_parts().0), + peer_addr, + server::handshake(io), ), } } } -enum State< - T: AsyncRead + AsyncWrite, - S: Service + 'static, - B: MessageBody, -> { +enum State, B: MessageBody> +where + S::Future: 'static, +{ Incoming(Dispatcher), Handshake( Option>, Option, + Option, Handshake, ), } pub struct H2ServiceHandlerResponse where - T: AsyncRead + AsyncWrite, - S: Service + 'static, - S::Error: Debug, + T: IoStream, + S: Service, + S::Error: Into, + S::Future: 'static, S::Response: Into>, B: MessageBody + 'static, { @@ -194,9 +201,10 @@ where impl Future for H2ServiceHandlerResponse where - T: AsyncRead + AsyncWrite, - S: Service + 'static, - S::Error: Debug, + T: IoStream, + S: Service, + S::Error: Into, + S::Future: 'static, S::Response: Into>, B: MessageBody, { @@ -206,24 +214,28 @@ where fn poll(&mut self) -> Poll { match self.state { State::Incoming(ref mut disp) => disp.poll(), - State::Handshake(ref mut srv, ref mut config, ref mut handshake) => { - match handshake.poll() { - Ok(Async::Ready(conn)) => { - self.state = State::Incoming(Dispatcher::new( - srv.take().unwrap(), - conn, - config.take().unwrap(), - None, - )); - self.poll() - } - Ok(Async::NotReady) => Ok(Async::NotReady), - Err(err) => { - trace!("H2 handshake error: {}", err); - Err(err.into()) - } + State::Handshake( + ref mut srv, + ref mut config, + ref peer_addr, + ref mut handshake, + ) => match handshake.poll() { + Ok(Async::Ready(conn)) => { + self.state = State::Incoming(Dispatcher::new( + srv.take().unwrap(), + conn, + config.take().unwrap(), + None, + peer_addr.clone(), + )); + self.poll() } - } + Ok(Async::NotReady) => Ok(Async::NotReady), + Err(err) => { + trace!("H2 handshake error: {}", err); + Err(err.into()) + } + }, } } } diff --git a/actix-http/src/header/common/cache_control.rs b/actix-http/src/header/common/cache_control.rs index 0b79ea7c0..55774619b 100644 --- a/actix-http/src/header/common/cache_control.rs +++ b/actix-http/src/header/common/cache_control.rs @@ -64,7 +64,7 @@ impl Header for CacheControl { where T: crate::HttpMessage, { - let directives = from_comma_delimited(msg.headers().get_all(Self::name()))?; + let directives = from_comma_delimited(msg.headers().get_all(&Self::name()))?; if !directives.is_empty() { Ok(CacheControl(directives)) } else { diff --git a/actix-http/src/header/common/content_disposition.rs b/actix-http/src/header/common/content_disposition.rs index 700400dad..badf307a0 100644 --- a/actix-http/src/header/common/content_disposition.rs +++ b/actix-http/src/header/common/content_disposition.rs @@ -444,7 +444,7 @@ impl Header for ContentDisposition { } fn parse(msg: &T) -> Result { - if let Some(h) = msg.headers().get(Self::name()) { + if let Some(h) = msg.headers().get(&Self::name()) { Self::from_raw(&h) } else { Err(crate::error::ParseError::Header) diff --git a/actix-http/src/header/common/if_range.rs b/actix-http/src/header/common/if_range.rs index 2140ccbb3..e910ebd96 100644 --- a/actix-http/src/header/common/if_range.rs +++ b/actix-http/src/header/common/if_range.rs @@ -73,12 +73,12 @@ impl Header for IfRange { T: HttpMessage, { let etag: Result = - from_one_raw_str(msg.headers().get(header::IF_RANGE)); + from_one_raw_str(msg.headers().get(&header::IF_RANGE)); if let Ok(etag) = etag { return Ok(IfRange::EntityTag(etag)); } let date: Result = - from_one_raw_str(msg.headers().get(header::IF_RANGE)); + from_one_raw_str(msg.headers().get(&header::IF_RANGE)); if let Ok(date) = date { return Ok(IfRange::Date(date)); } diff --git a/actix-http/src/header/map.rs b/actix-http/src/header/map.rs new file mode 100644 index 000000000..694aed02a --- /dev/null +++ b/actix-http/src/header/map.rs @@ -0,0 +1,384 @@ +use either::Either; +use hashbrown::hash_map::{self, Entry}; +use hashbrown::HashMap; +use http::header::{HeaderName, HeaderValue}; +use http::HttpTryFrom; + +/// A set of HTTP headers +/// +/// `HeaderMap` is an multimap of [`HeaderName`] to values. +/// +/// [`HeaderName`]: struct.HeaderName.html +#[derive(Debug)] +pub struct HeaderMap { + pub(crate) inner: HashMap, +} + +#[derive(Debug)] +pub(crate) enum Value { + One(HeaderValue), + Multi(Vec), +} + +impl Value { + fn get(&self) -> &HeaderValue { + match self { + Value::One(ref val) => val, + Value::Multi(ref val) => &val[0], + } + } + + fn get_mut(&mut self) -> &mut HeaderValue { + match self { + Value::One(ref mut val) => val, + Value::Multi(ref mut val) => &mut val[0], + } + } + + fn append(&mut self, val: HeaderValue) { + match self { + Value::One(_) => { + let data = std::mem::replace(self, Value::Multi(vec![val])); + match data { + Value::One(val) => self.append(val), + Value::Multi(_) => unreachable!(), + } + } + Value::Multi(ref mut vec) => vec.push(val), + } + } +} + +impl HeaderMap { + /// Create an empty `HeaderMap`. + /// + /// The map will be created without any capacity. This function will not + /// allocate. + pub fn new() -> Self { + HeaderMap { + inner: HashMap::new(), + } + } + + /// Create an empty `HeaderMap` with the specified capacity. + /// + /// The returned map will allocate internal storage in order to hold about + /// `capacity` elements without reallocating. However, this is a "best + /// effort" as there are usage patterns that could cause additional + /// allocations before `capacity` headers are stored in the map. + /// + /// More capacity than requested may be allocated. + pub fn with_capacity(capacity: usize) -> HeaderMap { + HeaderMap { + inner: HashMap::with_capacity(capacity), + } + } + + /// Returns the number of keys stored in the map. + /// + /// This number could be be less than or equal to actual headers stored in + /// the map. + pub fn len(&self) -> usize { + self.inner.len() + } + + /// Returns true if the map contains no elements. + pub fn is_empty(&self) -> bool { + self.inner.len() == 0 + } + + /// Clears the map, removing all key-value pairs. Keeps the allocated memory + /// for reuse. + pub fn clear(&mut self) { + self.inner.clear(); + } + + /// Returns the number of headers the map can hold without reallocating. + /// + /// This number is an approximation as certain usage patterns could cause + /// additional allocations before the returned capacity is filled. + pub fn capacity(&self) -> usize { + self.inner.capacity() + } + + /// Reserves capacity for at least `additional` more headers to be inserted + /// into the `HeaderMap`. + /// + /// The header map may reserve more space to avoid frequent reallocations. + /// Like with `with_capacity`, this will be a "best effort" to avoid + /// allocations until `additional` more headers are inserted. Certain usage + /// patterns could cause additional allocations before the number is + /// reached. + pub fn reserve(&mut self, additional: usize) { + self.inner.reserve(additional) + } + + /// Returns a reference to the value associated with the key. + /// + /// If there are multiple values associated with the key, then the first one + /// is returned. Use `get_all` to get all values associated with a given + /// key. Returns `None` if there are no values associated with the key. + pub fn get(&self, name: N) -> Option<&HeaderValue> { + self.get2(name).map(|v| v.get()) + } + + fn get2(&self, name: N) -> Option<&Value> { + match name.as_name() { + Either::Left(name) => self.inner.get(name), + Either::Right(s) => { + if let Ok(name) = HeaderName::try_from(s) { + self.inner.get(&name) + } else { + None + } + } + } + } + + /// Returns a view of all values associated with a key. + /// + /// The returned view does not incur any allocations and allows iterating + /// the values associated with the key. See [`GetAll`] for more details. + /// Returns `None` if there are no values associated with the key. + /// + /// [`GetAll`]: struct.GetAll.html + pub fn get_all(&self, name: N) -> GetAll { + GetAll { + idx: 0, + item: self.get2(name), + } + } + + /// Returns a mutable reference to the value associated with the key. + /// + /// If there are multiple values associated with the key, then the first one + /// is returned. Use `entry` to get all values associated with a given + /// key. Returns `None` if there are no values associated with the key. + pub fn get_mut(&mut self, name: N) -> Option<&mut HeaderValue> { + match name.as_name() { + Either::Left(name) => self.inner.get_mut(name).map(|v| v.get_mut()), + Either::Right(s) => { + if let Ok(name) = HeaderName::try_from(s) { + self.inner.get_mut(&name).map(|v| v.get_mut()) + } else { + None + } + } + } + } + + /// Returns true if the map contains a value for the specified key. + pub fn contains_key(&self, key: N) -> bool { + match key.as_name() { + Either::Left(name) => self.inner.contains_key(name), + Either::Right(s) => { + if let Ok(name) = HeaderName::try_from(s) { + self.inner.contains_key(&name) + } else { + false + } + } + } + } + + /// An iterator visiting all key-value pairs. + /// + /// The iteration order is arbitrary, but consistent across platforms for + /// the same crate version. Each key will be yielded once per associated + /// value. So, if a key has 3 associated values, it will be yielded 3 times. + pub fn iter(&self) -> Iter { + Iter::new(self.inner.iter()) + } + + /// An iterator visiting all keys. + /// + /// The iteration order is arbitrary, but consistent across platforms for + /// the same crate version. Each key will be yielded only once even if it + /// has multiple associated values. + pub fn keys(&self) -> Keys { + Keys(self.inner.keys()) + } + + /// Inserts a key-value pair into the map. + /// + /// If the map did not previously have this key present, then `None` is + /// returned. + /// + /// If the map did have this key present, the new value is associated with + /// the key and all previous values are removed. **Note** that only a single + /// one of the previous values is returned. If there are multiple values + /// that have been previously associated with the key, then the first one is + /// returned. See `insert_mult` on `OccupiedEntry` for an API that returns + /// all values. + /// + /// The key is not updated, though; this matters for types that can be `==` + /// without being identical. + pub fn insert(&mut self, key: HeaderName, val: HeaderValue) { + let _ = self.inner.insert(key, Value::One(val)); + } + + /// Inserts a key-value pair into the map. + /// + /// If the map did not previously have this key present, then `false` is + /// returned. + /// + /// If the map did have this key present, the new value is pushed to the end + /// of the list of values currently associated with the key. The key is not + /// updated, though; this matters for types that can be `==` without being + /// identical. + pub fn append(&mut self, key: HeaderName, value: HeaderValue) { + match self.inner.entry(key) { + Entry::Occupied(mut entry) => entry.get_mut().append(value), + Entry::Vacant(entry) => { + entry.insert(Value::One(value)); + } + } + } + + /// Removes all headers for a particular header name from the map. + pub fn remove(&mut self, key: N) { + match key.as_name() { + Either::Left(name) => { + let _ = self.inner.remove(name); + } + Either::Right(s) => { + if let Ok(name) = HeaderName::try_from(s) { + let _ = self.inner.remove(&name); + } + } + } + } +} + +#[doc(hidden)] +pub trait AsName { + fn as_name(&self) -> Either<&HeaderName, &str>; +} + +impl AsName for HeaderName { + fn as_name(&self) -> Either<&HeaderName, &str> { + Either::Left(self) + } +} + +impl<'a> AsName for &'a HeaderName { + fn as_name(&self) -> Either<&HeaderName, &str> { + Either::Left(self) + } +} + +impl<'a> AsName for &'a str { + fn as_name(&self) -> Either<&HeaderName, &str> { + Either::Right(self) + } +} + +impl AsName for String { + fn as_name(&self) -> Either<&HeaderName, &str> { + Either::Right(self.as_str()) + } +} + +impl<'a> AsName for &'a String { + fn as_name(&self) -> Either<&HeaderName, &str> { + Either::Right(self.as_str()) + } +} + +pub struct GetAll<'a> { + idx: usize, + item: Option<&'a Value>, +} + +impl<'a> Iterator for GetAll<'a> { + type Item = &'a HeaderValue; + + #[inline] + fn next(&mut self) -> Option<&'a HeaderValue> { + if let Some(ref val) = self.item { + match val { + Value::One(ref val) => { + self.item.take(); + Some(val) + } + Value::Multi(ref vec) => { + if self.idx < vec.len() { + let item = Some(&vec[self.idx]); + self.idx += 1; + item + } else { + self.item.take(); + None + } + } + } + } else { + None + } + } +} + +pub struct Keys<'a>(hash_map::Keys<'a, HeaderName, Value>); + +impl<'a> Iterator for Keys<'a> { + type Item = &'a HeaderName; + + #[inline] + fn next(&mut self) -> Option<&'a HeaderName> { + self.0.next() + } +} + +impl<'a> IntoIterator for &'a HeaderMap { + type Item = (&'a HeaderName, &'a HeaderValue); + type IntoIter = Iter<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +pub struct Iter<'a> { + idx: usize, + current: Option<(&'a HeaderName, &'a Vec)>, + iter: hash_map::Iter<'a, HeaderName, Value>, +} + +impl<'a> Iter<'a> { + fn new(iter: hash_map::Iter<'a, HeaderName, Value>) -> Self { + Self { + iter, + idx: 0, + current: None, + } + } +} + +impl<'a> Iterator for Iter<'a> { + type Item = (&'a HeaderName, &'a HeaderValue); + + #[inline] + fn next(&mut self) -> Option<(&'a HeaderName, &'a HeaderValue)> { + if let Some(ref mut item) = self.current { + if self.idx < item.1.len() { + let item = (item.0, &item.1[self.idx]); + self.idx += 1; + return Some(item); + } else { + self.idx = 0; + self.current.take(); + } + } + if let Some(item) = self.iter.next() { + match item.1 { + Value::One(ref value) => Some((item.0, value)), + Value::Multi(ref vec) => { + self.current = Some((item.0, vec)); + self.next() + } + } + } else { + None + } + } +} diff --git a/actix-http/src/header/mod.rs b/actix-http/src/header/mod.rs index 1ef1bd198..620183476 100644 --- a/actix-http/src/header/mod.rs +++ b/actix-http/src/header/mod.rs @@ -4,7 +4,6 @@ use std::{fmt, str::FromStr}; use bytes::{Bytes, BytesMut}; -use http::header::GetAll; use http::Error as HttpError; use mime::Mime; @@ -14,6 +13,7 @@ use crate::error::ParseError; use crate::httpmessage::HttpMessage; mod common; +pub(crate) mod map; mod shared; #[doc(hidden)] pub use self::common::*; @@ -21,6 +21,9 @@ pub use self::common::*; pub use self::shared::*; #[doc(hidden)] +pub use self::map::GetAll; +pub use self::map::HeaderMap; + /// A trait for any object that will represent a header field and value. pub trait Header where @@ -33,7 +36,6 @@ where fn parse(msg: &T) -> Result; } -#[doc(hidden)] /// A trait for any object that can be Converted to a `HeaderValue` pub trait IntoHeaderValue: Sized { /// The type returned in the event of a conversion error. @@ -97,6 +99,26 @@ impl IntoHeaderValue for String { } } +impl IntoHeaderValue for usize { + type Error = InvalidHeaderValueBytes; + + #[inline] + fn try_into(self) -> Result { + let s = format!("{}", self); + HeaderValue::from_shared(Bytes::from(s)) + } +} + +impl IntoHeaderValue for u64 { + type Error = InvalidHeaderValueBytes; + + #[inline] + fn try_into(self) -> Result { + let s = format!("{}", self); + HeaderValue::from_shared(Bytes::from(s)) + } +} + impl IntoHeaderValue for Mime { type Error = InvalidHeaderValueBytes; @@ -202,8 +224,8 @@ impl fmt::Write for Writer { #[inline] #[doc(hidden)] /// Reads a comma-delimited raw header into a Vec. -pub fn from_comma_delimited( - all: GetAll, +pub fn from_comma_delimited<'a, I: Iterator + 'a, T: FromStr>( + all: I, ) -> Result, ParseError> { let mut result = Vec::new(); for h in all { @@ -361,6 +383,17 @@ pub fn http_percent_encode(f: &mut fmt::Formatter, bytes: &[u8]) -> fmt::Result fmt::Display::fmt(&encoded, f) } +/// Convert http::HeaderMap to a HeaderMap +impl From for HeaderMap { + fn from(map: http::HeaderMap) -> HeaderMap { + let mut new_map = HeaderMap::with_capacity(map.capacity()); + for (h, v) in map.iter() { + new_map.append(h.clone(), v.clone()); + } + new_map + } +} + mod percent_encoding_http { use percent_encoding::{self, define_encode_set}; diff --git a/actix-http/src/helpers.rs b/actix-http/src/helpers.rs index e4ccd8aef..e8dbcd82a 100644 --- a/actix-http/src/helpers.rs +++ b/actix-http/src/helpers.rs @@ -1,6 +1,7 @@ +use std::{io, mem, ptr, slice}; + use bytes::{BufMut, BytesMut}; use http::Version; -use std::{mem, ptr, slice}; const DEC_DIGITS_LUT: &[u8] = b"0001020304050607080910111213141516171819\ 2021222324252627282930313233343536373839\ @@ -167,6 +168,18 @@ pub(crate) fn convert_usize(mut n: usize, bytes: &mut BytesMut) { } } +pub(crate) struct Writer<'a>(pub &'a mut BytesMut); + +impl<'a> io::Write for Writer<'a> { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.extend_from_slice(buf); + Ok(buf.len()) + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/actix-http/src/httpcodes.rs b/actix-http/src/httpcodes.rs index 5dfeefa9e..e7eda2da8 100644 --- a/actix-http/src/httpcodes.rs +++ b/actix-http/src/httpcodes.rs @@ -8,7 +8,7 @@ macro_rules! STATIC_RESP { ($name:ident, $status:expr) => { #[allow(non_snake_case, missing_docs)] pub fn $name() -> ResponseBuilder { - Response::build($status) + ResponseBuilder::new($status) } }; } @@ -60,6 +60,7 @@ impl Response { STATIC_RESP!(UnsupportedMediaType, StatusCode::UNSUPPORTED_MEDIA_TYPE); STATIC_RESP!(RangeNotSatisfiable, StatusCode::RANGE_NOT_SATISFIABLE); STATIC_RESP!(ExpectationFailed, StatusCode::EXPECTATION_FAILED); + STATIC_RESP!(UnprocessableEntity, StatusCode::UNPROCESSABLE_ENTITY); STATIC_RESP!(InternalServerError, StatusCode::INTERNAL_SERVER_ERROR); STATIC_RESP!(NotImplemented, StatusCode::NOT_IMPLEMENTED); diff --git a/actix-http/src/httpmessage.rs b/actix-http/src/httpmessage.rs index 7a2db52d6..1534973a8 100644 --- a/actix-http/src/httpmessage.rs +++ b/actix-http/src/httpmessage.rs @@ -4,13 +4,13 @@ use std::str; use encoding::all::UTF_8; use encoding::label::encoding_from_whatwg_label; use encoding::EncodingRef; -use http::{header, HeaderMap}; +use http::header; use mime::Mime; use crate::cookie::Cookie; use crate::error::{ContentTypeError, CookieParseError, ParseError}; use crate::extensions::Extensions; -use crate::header::Header; +use crate::header::{Header, HeaderMap}; use crate::payload::Payload; struct Cookies(Vec>); diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index 088125ae0..ac085eaea 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -1,5 +1,9 @@ //! Basic http primitives for actix-net framework. -#![allow(clippy::type_complexity, clippy::new_without_default)] +#![allow( + clippy::type_complexity, + clippy::new_without_default, + clippy::borrow_interior_mutable_const +)] #[macro_use] extern crate log; @@ -8,7 +12,6 @@ pub mod body; mod builder; pub mod client; mod config; -#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust", feature = "brotli"))] pub mod encoding; mod extensions; mod header; @@ -37,7 +40,7 @@ pub use self::message::{Message, RequestHead, ResponseHead}; pub use self::payload::{Payload, PayloadStream}; pub use self::request::Request; pub use self::response::{Response, ResponseBuilder}; -pub use self::service::{HttpService, SendError, SendResponse}; +pub use self::service::HttpService; pub mod http { //! Various HTTP related types @@ -45,10 +48,11 @@ pub mod http { // re-exports pub use http::header::{HeaderName, HeaderValue}; pub use http::uri::PathAndQuery; - pub use http::{uri, Error, HeaderMap, HttpTryFrom, Uri}; + pub use http::{uri, Error, HttpTryFrom, Uri}; pub use http::{Method, StatusCode, Version}; pub use crate::cookie::{Cookie, CookieBuilder}; + pub use crate::header::HeaderMap; /// Various http headers pub mod header { diff --git a/actix-http/src/message.rs b/actix-http/src/message.rs index 8761f89d4..f3129c758 100644 --- a/actix-http/src/message.rs +++ b/actix-http/src/message.rs @@ -1,11 +1,13 @@ use std::cell::{Ref, RefCell, RefMut}; -use std::collections::VecDeque; +use std::net; use std::rc::Rc; use bitflags::bitflags; +use copyless::BoxHelper; use crate::extensions::Extensions; -use crate::http::{header, HeaderMap, Method, StatusCode, Uri, Version}; +use crate::header::HeaderMap; +use crate::http::{header, Method, StatusCode, Uri, Version}; /// Represents various types of connection #[derive(Copy, Clone, PartialEq, Debug)] @@ -23,8 +25,9 @@ bitflags! { const CLOSE = 0b0000_0001; const KEEP_ALIVE = 0b0000_0010; const UPGRADE = 0b0000_0100; - const NO_CHUNKING = 0b0000_1000; - const CAMEL_CASE = 0b0001_0000; + const EXPECT = 0b0000_1000; + const NO_CHUNKING = 0b0001_0000; + const CAMEL_CASE = 0b0010_0000; } } @@ -42,6 +45,7 @@ pub struct RequestHead { pub version: Version, pub headers: HeaderMap, pub extensions: RefCell, + pub peer_addr: Option, flags: Flags, } @@ -53,6 +57,7 @@ impl Default for RequestHead { version: Version::HTTP_11, headers: HeaderMap::with_capacity(16), flags: Flags::empty(), + peer_addr: None, extensions: RefCell::new(Extensions::new()), } } @@ -163,6 +168,17 @@ impl RequestHead { self.flags.remove(Flags::NO_CHUNKING); } } + + #[inline] + /// Request contains `EXPECT` header + pub fn expect(&self) -> bool { + self.flags.contains(Flags::EXPECT) + } + + #[inline] + pub(crate) fn set_expect(&mut self) { + self.flags.insert(Flags::EXPECT); + } } #[derive(Debug)] @@ -175,32 +191,20 @@ pub struct ResponseHead { flags: Flags, } -impl Default for ResponseHead { - fn default() -> ResponseHead { +impl ResponseHead { + /// Create new instance of `ResponseHead` type + #[inline] + pub fn new(status: StatusCode) -> ResponseHead { ResponseHead { + status, version: Version::default(), - status: StatusCode::OK, - headers: HeaderMap::with_capacity(16), + headers: HeaderMap::with_capacity(12), reason: None, flags: Flags::empty(), extensions: RefCell::new(Extensions::new()), } } -} -impl Head for ResponseHead { - fn clear(&mut self) { - self.reason = None; - self.flags = Flags::empty(); - self.headers.clear(); - } - - fn pool() -> &'static MessagePool { - RESPONSE_POOL.with(|p| *p) - } -} - -impl ResponseHead { /// Message extensions #[inline] pub fn extensions(&self) -> Ref { @@ -306,7 +310,6 @@ impl ResponseHead { pub struct Message { head: Rc, - pool: &'static MessagePool, } impl Message { @@ -320,7 +323,6 @@ impl Clone for Message { fn clone(&self) -> Self { Message { head: self.head.clone(), - pool: self.pool, } } } @@ -342,39 +344,70 @@ impl std::ops::DerefMut for Message { impl Drop for Message { fn drop(&mut self) { if Rc::strong_count(&self.head) == 1 { - self.pool.release(self.head.clone()); + T::pool().release(self.head.clone()); } } } +pub(crate) struct BoxedResponseHead { + head: Option>, +} + +impl BoxedResponseHead { + /// Get new message from the pool of objects + pub fn new(status: StatusCode) -> Self { + RESPONSE_POOL.with(|p| p.get_message(status)) + } +} + +impl std::ops::Deref for BoxedResponseHead { + type Target = ResponseHead; + + fn deref(&self) -> &Self::Target { + self.head.as_ref().unwrap() + } +} + +impl std::ops::DerefMut for BoxedResponseHead { + fn deref_mut(&mut self) -> &mut Self::Target { + self.head.as_mut().unwrap() + } +} + +impl Drop for BoxedResponseHead { + fn drop(&mut self) { + RESPONSE_POOL.with(|p| p.release(self.head.take().unwrap())) + } +} + #[doc(hidden)] /// Request's objects pool -pub struct MessagePool(RefCell>>); +pub struct MessagePool(RefCell>>); + +#[doc(hidden)] +/// Request's objects pool +pub struct BoxedResponsePool(RefCell>>); thread_local!(static REQUEST_POOL: &'static MessagePool = MessagePool::::create()); -thread_local!(static RESPONSE_POOL: &'static MessagePool = MessagePool::::create()); +thread_local!(static RESPONSE_POOL: &'static BoxedResponsePool = BoxedResponsePool::create()); impl MessagePool { fn create() -> &'static MessagePool { - let pool = MessagePool(RefCell::new(VecDeque::with_capacity(128))); + let pool = MessagePool(RefCell::new(Vec::with_capacity(128))); Box::leak(Box::new(pool)) } /// Get message from the pool #[inline] fn get_message(&'static self) -> Message { - if let Some(mut msg) = self.0.borrow_mut().pop_front() { + if let Some(mut msg) = self.0.borrow_mut().pop() { if let Some(r) = Rc::get_mut(&mut msg) { r.clear(); } - Message { - head: msg, - pool: self, - } + Message { head: msg } } else { Message { head: Rc::new(T::default()), - pool: self, } } } @@ -384,7 +417,39 @@ impl MessagePool { fn release(&self, msg: Rc) { let v = &mut self.0.borrow_mut(); if v.len() < 128 { - v.push_front(msg); + v.push(msg); + } + } +} + +impl BoxedResponsePool { + fn create() -> &'static BoxedResponsePool { + let pool = BoxedResponsePool(RefCell::new(Vec::with_capacity(128))); + Box::leak(Box::new(pool)) + } + + /// Get message from the pool + #[inline] + fn get_message(&'static self, status: StatusCode) -> BoxedResponseHead { + if let Some(mut head) = self.0.borrow_mut().pop() { + head.reason = None; + head.status = status; + head.headers.clear(); + head.flags = Flags::empty(); + BoxedResponseHead { head: Some(head) } + } else { + BoxedResponseHead { + head: Some(Box::alloc().init(ResponseHead::new(status))), + } + } + } + + #[inline] + /// Release request instance + fn release(&self, msg: Box) { + let v = &mut self.0.borrow_mut(); + if v.len() < 128 { + v.push(msg); } } } diff --git a/actix-http/src/payload.rs b/actix-http/src/payload.rs index 91e6b5c95..0ce209705 100644 --- a/actix-http/src/payload.rs +++ b/actix-http/src/payload.rs @@ -53,6 +53,7 @@ where type Item = Bytes; type Error = PayloadError; + #[inline] fn poll(&mut self) -> Poll, Self::Error> { match self { Payload::None => Ok(Async::Ready(None)), diff --git a/actix-http/src/request.rs b/actix-http/src/request.rs index a645c7aeb..e9252a829 100644 --- a/actix-http/src/request.rs +++ b/actix-http/src/request.rs @@ -1,9 +1,10 @@ use std::cell::{Ref, RefMut}; -use std::fmt; +use std::{fmt, net}; -use http::{header, HeaderMap, Method, Uri, Version}; +use http::{header, Method, Uri, Version}; use crate::extensions::Extensions; +use crate::header::HeaderMap; use crate::httpmessage::HttpMessage; use crate::message::{Message, RequestHead}; use crate::payload::{Payload, PayloadStream}; @@ -138,6 +139,7 @@ impl

Request

{ } /// Check if request requires connection upgrade + #[inline] pub fn upgrade(&self) -> bool { if let Some(conn) = self.head().headers.get(header::CONNECTION) { if let Ok(s) = conn.to_str() { @@ -146,6 +148,15 @@ impl

Request

{ } self.head().method == Method::CONNECT } + + /// Peer socket address + /// + /// Peer address is actual socket address, if proxy is used in front of + /// actix http server, then peer address would be address of this proxy. + #[inline] + pub fn peer_addr(&self) -> Option { + self.head().peer_addr + } } impl

fmt::Debug for Request

{ @@ -161,9 +172,34 @@ impl

fmt::Debug for Request

{ writeln!(f, " query: ?{:?}", q)?; } writeln!(f, " headers:")?; - for (key, val) in self.headers().iter() { + for (key, val) in self.headers() { writeln!(f, " {:?}: {:?}", key, val)?; } Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use http::HttpTryFrom; + + #[test] + fn test_basics() { + let msg = Message::new(); + let mut req = Request::from(msg); + req.headers_mut().insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("text/plain"), + ); + assert!(req.headers().contains_key(header::CONTENT_TYPE)); + + *req.uri_mut() = Uri::try_from("/index.html?q=1").unwrap(); + assert_eq!(req.uri().path(), "/index.html"); + assert_eq!(req.uri().query(), Some("q=1")); + + let s = format!("{:?}", req); + println!("T: {:?}", s); + assert!(s.contains("Request HTTP/1.1 GET:/index.html")); + } +} diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs index 88eb7dccb..fd51e54c7 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/response.rs @@ -6,8 +6,6 @@ use std::{fmt, str}; use bytes::{BufMut, Bytes, BytesMut}; use futures::future::{ok, FutureResult, IntoFuture}; use futures::Stream; -use http::header::{self, HeaderName, HeaderValue}; -use http::{Error as HttpError, HeaderMap, HttpTryFrom, StatusCode}; use serde::Serialize; use serde_json; @@ -16,11 +14,13 @@ use crate::cookie::{Cookie, CookieJar}; use crate::error::Error; use crate::extensions::Extensions; use crate::header::{Header, IntoHeaderValue}; -use crate::message::{ConnectionType, Message, ResponseHead}; +use crate::http::header::{self, HeaderName, HeaderValue}; +use crate::http::{Error as HttpError, HeaderMap, HttpTryFrom, StatusCode}; +use crate::message::{BoxedResponseHead, ConnectionType, ResponseHead}; /// An HTTP Response pub struct Response { - head: Message, + head: BoxedResponseHead, body: ResponseBody, error: Option, } @@ -41,11 +41,8 @@ impl Response { /// Constructs a response #[inline] pub fn new(status: StatusCode) -> Response { - let mut head: Message = Message::new(); - head.status = status; - Response { - head, + head: BoxedResponseHead::new(status), body: ResponseBody::Body(Body::Empty), error: None, } @@ -54,7 +51,7 @@ impl Response { /// Constructs an error response #[inline] pub fn from_error(error: Error) -> Response { - let mut resp = error.as_response_error().error_response(); + let mut resp = error.as_response_error().render_response(); resp.error = Some(error); resp } @@ -74,6 +71,16 @@ impl Response { } impl Response { + /// Constructs a response with body + #[inline] + pub fn with_body(status: StatusCode, body: B) -> Response { + Response { + head: BoxedResponseHead::new(status), + body: ResponseBody::Body(body), + error: None, + } + } + #[inline] /// Http message part of the response pub fn head(&self) -> &ResponseHead { @@ -86,18 +93,6 @@ impl Response { &mut *self.head } - /// Constructs a response with body - #[inline] - pub fn with_body(status: StatusCode, body: B) -> Response { - let mut head: Message = Message::new(); - head.status = status; - Response { - head, - body: ResponseBody::Body(body), - error: None, - } - } - /// The source `error` for this response #[inline] pub fn error(&self) -> Option<&Error> { @@ -132,7 +127,7 @@ impl Response { #[inline] pub fn cookies(&self) -> CookieIter { CookieIter { - iter: self.head.headers.get_all(header::SET_COOKIE).iter(), + iter: self.head.headers.get_all(header::SET_COOKIE), } } @@ -154,7 +149,6 @@ impl Response { let h = &mut self.head.headers; let vals: Vec = h .get_all(header::SET_COOKIE) - .iter() .map(|v| v.to_owned()) .collect(); h.remove(header::SET_COOKIE); @@ -204,7 +198,7 @@ impl Response { } /// Set a body - pub(crate) fn set_body(self, body: B2) -> Response { + pub fn set_body(self, body: B2) -> Response { Response { head: self.head, body: ResponseBody::Body(body), @@ -212,8 +206,20 @@ impl Response { } } + /// Split response and body + pub fn into_parts(self) -> (Response<()>, ResponseBody) { + ( + Response { + head: self.head, + body: ResponseBody::Body(()), + error: self.error, + }, + self.body, + ) + } + /// Drop request's body - pub(crate) fn drop_body(self) -> Response<()> { + pub fn drop_body(self) -> Response<()> { Response { head: self.head, body: ResponseBody::Body(()), @@ -266,7 +272,7 @@ impl fmt::Debug for Response { for (key, val) in self.head.headers.iter() { let _ = writeln!(f, " {:?}: {:?}", key, val); } - let _ = writeln!(f, " body: {:?}", self.body.length()); + let _ = writeln!(f, " body: {:?}", self.body.size()); res } } @@ -282,7 +288,7 @@ impl IntoFuture for Response { } pub struct CookieIter<'a> { - iter: header::ValueIter<'a, HeaderValue>, + iter: header::GetAll<'a>, } impl<'a> Iterator for CookieIter<'a> { @@ -304,19 +310,17 @@ impl<'a> Iterator for CookieIter<'a> { /// This type can be used to construct an instance of `Response` through a /// builder-like pattern. pub struct ResponseBuilder { - head: Option>, + head: Option, err: Option, cookies: Option, } impl ResponseBuilder { + #[inline] /// Create response builder pub fn new(status: StatusCode) -> Self { - let mut head: Message = Message::new(); - head.status = status; - ResponseBuilder { - head: Some(head), + head: Some(BoxedResponseHead::new(status)), err: None, cookies: None, } @@ -540,15 +544,13 @@ impl ResponseBuilder { /// } /// ``` pub fn del_cookie<'a>(&mut self, cookie: &Cookie<'a>) -> &mut Self { - { - if self.cookies.is_none() { - self.cookies = Some(CookieJar::new()) - } - let jar = self.cookies.as_mut().unwrap(); - let cookie = cookie.clone().into_owned(); - jar.add_original(cookie.clone()); - jar.remove(cookie); + if self.cookies.is_none() { + self.cookies = Some(CookieJar::new()) } + let jar = self.cookies.as_mut().unwrap(); + let cookie = cookie.clone().into_owned(); + jar.add_original(cookie.clone()); + jar.remove(cookie); self } @@ -590,6 +592,7 @@ impl ResponseBuilder { head.extensions.borrow_mut() } + #[inline] /// Set a body and generate `Response`. /// /// `ResponseBuilder` can not be used after this call. @@ -610,9 +613,7 @@ impl ResponseBuilder { if let Some(ref jar) = self.cookies { for cookie in jar.delta() { match HeaderValue::from_str(&cookie.to_string()) { - Ok(val) => { - let _ = response.headers.append(header::SET_COOKIE, val); - } + Ok(val) => response.headers.append(header::SET_COOKIE, val), Err(e) => return Response::from(Error::from(e)).into_body(), }; } @@ -637,6 +638,7 @@ impl ResponseBuilder { self.body(Body::from_message(BodyStream::new(stream))) } + #[inline] /// Set a json body and generate `Response` /// /// `ResponseBuilder` can not be used after this call. @@ -685,13 +687,13 @@ impl ResponseBuilder { #[inline] fn parts<'a>( - parts: &'a mut Option>, + parts: &'a mut Option, err: &Option, -) -> Option<&'a mut Message> { +) -> Option<&'a mut ResponseHead> { if err.is_some() { return None; } - parts.as_mut() + parts.as_mut().map(|r| &mut **r) } /// Convert `Response` to a `ResponseBuilder`. Body get dropped. @@ -724,7 +726,7 @@ impl<'a> From<&'a ResponseHead> for ResponseBuilder { let mut jar: Option = None; let cookies = CookieIter { - iter: head.headers.get_all(header::SET_COOKIE).iter(), + iter: head.headers.get_all(header::SET_COOKIE), }; for c in cookies { if let Some(ref mut j) = jar { @@ -736,11 +738,12 @@ impl<'a> From<&'a ResponseHead> for ResponseBuilder { } } - let mut msg: Message = Message::new(); + let mut msg = BoxedResponseHead::new(head.status); msg.version = head.version; - msg.status = head.status; msg.reason = head.reason; - msg.headers = head.headers.clone(); + for (k, v) in &head.headers { + msg.headers.append(k.clone(), v.clone()); + } msg.no_chunking(!head.chunked()); ResponseBuilder { @@ -829,7 +832,7 @@ impl From for Response { mod tests { use super::*; use crate::body::Body; - use crate::http::header::{HeaderValue, CONTENT_TYPE, COOKIE}; + use crate::http::header::{HeaderValue, CONTENT_TYPE, COOKIE, SET_COOKIE}; #[test] fn test_debug() { @@ -857,16 +860,15 @@ mod tests { .domain("www.rust-lang.org") .path("/test") .http_only(true) - .max_age(time::Duration::days(1)) + .max_age_time(time::Duration::days(1)) .finish(), ) - .del_cookie(&cookies[0]) + .del_cookie(&cookies[1]) .finish(); let mut val: Vec<_> = resp .headers() - .get_all("Set-Cookie") - .iter() + .get_all(SET_COOKIE) .map(|v| v.to_str().unwrap().to_owned()) .collect(); val.sort(); @@ -895,9 +897,9 @@ mod tests { let mut iter = r.cookies(); let v = iter.next().unwrap(); - assert_eq!((v.name(), v.value()), ("original", "val100")); - let v = iter.next().unwrap(); assert_eq!((v.name(), v.value()), ("cookie3", "val300")); + let v = iter.next().unwrap(); + assert_eq!((v.name(), v.value()), ("original", "val100")); } #[test] @@ -1033,6 +1035,7 @@ mod tests { resp.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); + assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().get_ref(), b"test"); } diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs new file mode 100644 index 000000000..dd3af1db0 --- /dev/null +++ b/actix-http/src/service.rs @@ -0,0 +1,563 @@ +use std::marker::PhantomData; +use std::{fmt, io, net}; + +use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use actix_server_config::{ + Io as ServerIo, IoStream, Protocol, ServerConfig as SrvConfig, +}; +use actix_service::{IntoNewService, NewService, Service}; +use actix_utils::cloneable::CloneableService; +use bytes::{Buf, BufMut, Bytes, BytesMut}; +use futures::{try_ready, Async, Future, IntoFuture, Poll}; +use h2::server::{self, Handshake}; + +use crate::body::MessageBody; +use crate::builder::HttpServiceBuilder; +use crate::config::{KeepAlive, ServiceConfig}; +use crate::error::{DispatchError, Error}; +use crate::request::Request; +use crate::response::Response; +use crate::{h1, h2::Dispatcher}; + +/// `NewService` HTTP1.1/HTTP2 transport implementation +pub struct HttpService> { + srv: S, + cfg: ServiceConfig, + expect: X, + upgrade: Option, + _t: PhantomData<(T, P, B)>, +} + +impl HttpService +where + S: NewService, + S::Error: Into, + S::InitError: fmt::Debug, + S::Response: Into>, + ::Future: 'static, + B: MessageBody + 'static, +{ + /// Create builder for `HttpService` instance. + pub fn build() -> HttpServiceBuilder { + HttpServiceBuilder::new() + } +} + +impl HttpService +where + S: NewService, + S::Error: Into, + S::InitError: fmt::Debug, + S::Response: Into>, + ::Future: 'static, + B: MessageBody + 'static, +{ + /// Create new `HttpService` instance. + pub fn new>(service: F) -> Self { + let cfg = ServiceConfig::new(KeepAlive::Timeout(5), 5000, 0); + + HttpService { + cfg, + srv: service.into_new_service(), + expect: h1::ExpectHandler, + upgrade: None, + _t: PhantomData, + } + } + + /// Create new `HttpService` instance with config. + pub(crate) fn with_config>( + cfg: ServiceConfig, + service: F, + ) -> Self { + HttpService { + cfg, + srv: service.into_new_service(), + expect: h1::ExpectHandler, + upgrade: None, + _t: PhantomData, + } + } +} + +impl HttpService +where + S: NewService, + S::Error: Into, + S::InitError: fmt::Debug, + S::Response: Into>, + B: MessageBody, +{ + /// Provide service for `EXPECT: 100-Continue` support. + /// + /// Service get called with request that contains `EXPECT` header. + /// Service must return request in case of success, in that case + /// request will be forwarded to main service. + pub fn expect(self, expect: X1) -> HttpService + where + X1: NewService, + X1::Error: Into, + X1::InitError: fmt::Debug, + { + HttpService { + expect, + cfg: self.cfg, + srv: self.srv, + upgrade: self.upgrade, + _t: PhantomData, + } + } + + /// Provide service for custom `Connection: UPGRADE` support. + /// + /// If service is provided then normal requests handling get halted + /// and this service get called with original request and framed object. + pub fn upgrade(self, upgrade: Option) -> HttpService + where + U1: NewService), Response = ()>, + U1::Error: fmt::Display, + U1::InitError: fmt::Debug, + { + HttpService { + upgrade, + cfg: self.cfg, + srv: self.srv, + expect: self.expect, + _t: PhantomData, + } + } +} + +impl NewService for HttpService +where + T: IoStream, + S: NewService, + S::Error: Into, + S::InitError: fmt::Debug, + S::Response: Into>, + ::Future: 'static, + B: MessageBody + 'static, + X: NewService, + X::Error: Into, + X::InitError: fmt::Debug, + U: NewService), Response = ()>, + U::Error: fmt::Display, + U::InitError: fmt::Debug, +{ + type Request = ServerIo; + type Response = (); + type Error = DispatchError; + type InitError = (); + type Service = HttpServiceHandler; + type Future = HttpServiceResponse; + + fn new_service(&self, cfg: &SrvConfig) -> Self::Future { + HttpServiceResponse { + fut: self.srv.new_service(cfg).into_future(), + fut_ex: Some(self.expect.new_service(&())), + fut_upg: self.upgrade.as_ref().map(|f| f.new_service(&())), + expect: None, + upgrade: None, + cfg: Some(self.cfg.clone()), + _t: PhantomData, + } + } +} + +#[doc(hidden)] +pub struct HttpServiceResponse< + T, + P, + S: NewService, + B, + X: NewService, + U: NewService, +> { + fut: S::Future, + fut_ex: Option, + fut_upg: Option, + expect: Option, + upgrade: Option, + cfg: Option, + _t: PhantomData<(T, P, B)>, +} + +impl Future for HttpServiceResponse +where + T: IoStream, + S: NewService, + S::Error: Into, + S::InitError: fmt::Debug, + S::Response: Into>, + ::Future: 'static, + B: MessageBody + 'static, + X: NewService, + X::Error: Into, + X::InitError: fmt::Debug, + U: NewService), Response = ()>, + U::Error: fmt::Display, + U::InitError: fmt::Debug, +{ + type Item = HttpServiceHandler; + type Error = (); + + fn poll(&mut self) -> Poll { + if let Some(ref mut fut) = self.fut_ex { + let expect = try_ready!(fut + .poll() + .map_err(|e| log::error!("Init http service error: {:?}", e))); + self.expect = Some(expect); + self.fut_ex.take(); + } + + if let Some(ref mut fut) = self.fut_upg { + let upgrade = try_ready!(fut + .poll() + .map_err(|e| log::error!("Init http service error: {:?}", e))); + self.upgrade = Some(upgrade); + self.fut_ex.take(); + } + + let service = try_ready!(self + .fut + .poll() + .map_err(|e| log::error!("Init http service error: {:?}", e))); + Ok(Async::Ready(HttpServiceHandler::new( + self.cfg.take().unwrap(), + service, + self.expect.take().unwrap(), + self.upgrade.take(), + ))) + } +} + +/// `Service` implementation for http transport +pub struct HttpServiceHandler { + srv: CloneableService, + expect: CloneableService, + upgrade: Option>, + cfg: ServiceConfig, + _t: PhantomData<(T, P, B, X)>, +} + +impl HttpServiceHandler +where + S: Service, + S::Error: Into, + S::Future: 'static, + S::Response: Into>, + B: MessageBody + 'static, + X: Service, + X::Error: Into, + U: Service), Response = ()>, + U::Error: fmt::Display, +{ + fn new( + cfg: ServiceConfig, + srv: S, + expect: X, + upgrade: Option, + ) -> HttpServiceHandler { + HttpServiceHandler { + cfg, + srv: CloneableService::new(srv), + expect: CloneableService::new(expect), + upgrade: upgrade.map(|s| CloneableService::new(s)), + _t: PhantomData, + } + } +} + +impl Service for HttpServiceHandler +where + T: IoStream, + S: Service, + S::Error: Into, + S::Future: 'static, + S::Response: Into>, + B: MessageBody + 'static, + X: Service, + X::Error: Into, + U: Service), Response = ()>, + U::Error: fmt::Display, +{ + type Request = ServerIo; + type Response = (); + type Error = DispatchError; + type Future = HttpServiceHandlerResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + let ready = self + .expect + .poll_ready() + .map_err(|e| { + let e = e.into(); + log::error!("Http service readiness error: {:?}", e); + DispatchError::Service(e) + })? + .is_ready(); + + let ready = self + .srv + .poll_ready() + .map_err(|e| { + let e = e.into(); + log::error!("Http service readiness error: {:?}", e); + DispatchError::Service(e) + })? + .is_ready() + && ready; + + if ready { + Ok(Async::Ready(())) + } else { + Ok(Async::NotReady) + } + } + + fn call(&mut self, req: Self::Request) -> Self::Future { + let (io, _, proto) = req.into_parts(); + match proto { + Protocol::Http2 => { + let peer_addr = io.peer_addr(); + let io = Io { + inner: io, + unread: None, + }; + HttpServiceHandlerResponse { + state: State::Handshake(Some(( + server::handshake(io), + self.cfg.clone(), + self.srv.clone(), + peer_addr, + ))), + } + } + Protocol::Http10 | Protocol::Http11 => HttpServiceHandlerResponse { + state: State::H1(h1::Dispatcher::new( + io, + self.cfg.clone(), + self.srv.clone(), + self.expect.clone(), + self.upgrade.clone(), + )), + }, + _ => HttpServiceHandlerResponse { + state: State::Unknown(Some(( + io, + BytesMut::with_capacity(14), + self.cfg.clone(), + self.srv.clone(), + self.expect.clone(), + self.upgrade.clone(), + ))), + }, + } + } +} + +enum State +where + S: Service, + S::Future: 'static, + S::Error: Into, + T: IoStream, + B: MessageBody, + X: Service, + X::Error: Into, + U: Service), Response = ()>, + U::Error: fmt::Display, +{ + H1(h1::Dispatcher), + H2(Dispatcher, S, B>), + Unknown( + Option<( + T, + BytesMut, + ServiceConfig, + CloneableService, + CloneableService, + Option>, + )>, + ), + Handshake( + Option<( + Handshake, Bytes>, + ServiceConfig, + CloneableService, + Option, + )>, + ), +} + +pub struct HttpServiceHandlerResponse +where + T: IoStream, + S: Service, + S::Error: Into, + S::Future: 'static, + S::Response: Into>, + B: MessageBody + 'static, + X: Service, + X::Error: Into, + U: Service), Response = ()>, + U::Error: fmt::Display, +{ + state: State, +} + +const HTTP2_PREFACE: [u8; 14] = *b"PRI * HTTP/2.0"; + +impl Future for HttpServiceHandlerResponse +where + T: IoStream, + S: Service, + S::Error: Into, + S::Future: 'static, + S::Response: Into>, + B: MessageBody, + X: Service, + X::Error: Into, + U: Service), Response = ()>, + U::Error: fmt::Display, +{ + type Item = (); + type Error = DispatchError; + + fn poll(&mut self) -> Poll { + match self.state { + State::H1(ref mut disp) => disp.poll(), + State::H2(ref mut disp) => disp.poll(), + State::Unknown(ref mut data) => { + if let Some(ref mut item) = data { + loop { + unsafe { + let b = item.1.bytes_mut(); + let n = try_ready!(item.0.poll_read(b)); + if n == 0 { + return Ok(Async::Ready(())); + } + item.1.advance_mut(n); + if item.1.len() >= HTTP2_PREFACE.len() { + break; + } + } + } + } else { + panic!() + } + let (io, buf, cfg, srv, expect, upgrade) = data.take().unwrap(); + if buf[..14] == HTTP2_PREFACE[..] { + let peer_addr = io.peer_addr(); + let io = Io { + inner: io, + unread: Some(buf), + }; + self.state = State::Handshake(Some(( + server::handshake(io), + cfg, + srv, + peer_addr, + ))); + } else { + self.state = State::H1(h1::Dispatcher::with_timeout( + io, + h1::Codec::new(cfg.clone()), + cfg, + buf, + None, + srv, + expect, + upgrade, + )) + } + self.poll() + } + State::Handshake(ref mut data) => { + let conn = if let Some(ref mut item) = data { + match item.0.poll() { + Ok(Async::Ready(conn)) => conn, + Ok(Async::NotReady) => return Ok(Async::NotReady), + Err(err) => { + trace!("H2 handshake error: {}", err); + return Err(err.into()); + } + } + } else { + panic!() + }; + let (_, cfg, srv, peer_addr) = data.take().unwrap(); + self.state = State::H2(Dispatcher::new(srv, conn, cfg, None, peer_addr)); + self.poll() + } + } + } +} + +/// Wrapper for `AsyncRead + AsyncWrite` types +struct Io { + unread: Option, + inner: T, +} + +impl io::Read for Io { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + if let Some(mut bytes) = self.unread.take() { + let size = std::cmp::min(buf.len(), bytes.len()); + buf[..size].copy_from_slice(&bytes[..size]); + if bytes.len() > size { + bytes.split_to(size); + self.unread = Some(bytes); + } + Ok(size) + } else { + self.inner.read(buf) + } + } +} + +impl io::Write for Io { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.inner.write(buf) + } + fn flush(&mut self) -> io::Result<()> { + self.inner.flush() + } +} + +impl AsyncRead for Io { + unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [u8]) -> bool { + self.inner.prepare_uninitialized_buffer(buf) + } +} + +impl AsyncWrite for Io { + fn shutdown(&mut self) -> Poll<(), io::Error> { + self.inner.shutdown() + } + fn write_buf(&mut self, buf: &mut B) -> Poll { + self.inner.write_buf(buf) + } +} + +impl IoStream for Io { + #[inline] + fn peer_addr(&self) -> Option { + self.inner.peer_addr() + } + + #[inline] + fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { + self.inner.set_nodelay(nodelay) + } + + #[inline] + fn set_linger(&mut self, dur: Option) -> io::Result<()> { + self.inner.set_linger(dur) + } + + #[inline] + fn set_keepalive(&mut self, dur: Option) -> io::Result<()> { + self.inner.set_keepalive(dur) + } +} diff --git a/actix-http/src/service/mod.rs b/actix-http/src/service/mod.rs deleted file mode 100644 index 25e95bf60..000000000 --- a/actix-http/src/service/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod senderror; -mod service; - -pub use self::senderror::{SendError, SendResponse}; -pub use self::service::HttpService; diff --git a/actix-http/src/service/senderror.rs b/actix-http/src/service/senderror.rs deleted file mode 100644 index 03fe5976a..000000000 --- a/actix-http/src/service/senderror.rs +++ /dev/null @@ -1,241 +0,0 @@ -use std::marker::PhantomData; - -use actix_codec::{AsyncRead, AsyncWrite, Framed}; -use actix_service::{NewService, Service}; -use futures::future::{ok, Either, FutureResult}; -use futures::{Async, Future, Poll, Sink}; - -use crate::body::{BodySize, MessageBody, ResponseBody}; -use crate::error::{Error, ResponseError}; -use crate::h1::{Codec, Message}; -use crate::response::Response; - -pub struct SendError(PhantomData<(T, R, E)>); - -impl Default for SendError -where - T: AsyncRead + AsyncWrite, - E: ResponseError, -{ - fn default() -> Self { - SendError(PhantomData) - } -} - -impl NewService for SendError -where - T: AsyncRead + AsyncWrite, - E: ResponseError, -{ - type Request = Result)>; - type Response = R; - type Error = (E, Framed); - type InitError = (); - type Service = SendError; - type Future = FutureResult; - - fn new_service(&self, _: &()) -> Self::Future { - ok(SendError(PhantomData)) - } -} - -impl Service for SendError -where - T: AsyncRead + AsyncWrite, - E: ResponseError, -{ - type Request = Result)>; - type Response = R; - type Error = (E, Framed); - type Future = Either)>, SendErrorFut>; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, req: Result)>) -> Self::Future { - match req { - Ok(r) => Either::A(ok(r)), - Err((e, framed)) => { - let res = e.error_response().set_body(format!("{}", e)); - let (res, _body) = res.replace_body(()); - Either::B(SendErrorFut { - framed: Some(framed), - res: Some((res, BodySize::Empty).into()), - err: Some(e), - _t: PhantomData, - }) - } - } - } -} - -pub struct SendErrorFut { - res: Option, BodySize)>>, - framed: Option>, - err: Option, - _t: PhantomData, -} - -impl Future for SendErrorFut -where - E: ResponseError, - T: AsyncRead + AsyncWrite, -{ - type Item = R; - type Error = (E, Framed); - - fn poll(&mut self) -> Poll { - if let Some(res) = self.res.take() { - if self.framed.as_mut().unwrap().force_send(res).is_err() { - return Err((self.err.take().unwrap(), self.framed.take().unwrap())); - } - } - match self.framed.as_mut().unwrap().poll_complete() { - Ok(Async::Ready(_)) => { - Err((self.err.take().unwrap(), self.framed.take().unwrap())) - } - Ok(Async::NotReady) => Ok(Async::NotReady), - Err(_) => Err((self.err.take().unwrap(), self.framed.take().unwrap())), - } - } -} - -pub struct SendResponse(PhantomData<(T, B)>); - -impl Default for SendResponse { - fn default() -> Self { - SendResponse(PhantomData) - } -} - -impl SendResponse -where - T: AsyncRead + AsyncWrite, - B: MessageBody, -{ - pub fn send( - framed: Framed, - res: Response, - ) -> impl Future, Error = Error> { - // extract body from response - let (res, body) = res.replace_body(()); - - // write response - SendResponseFut { - res: Some(Message::Item((res, body.length()))), - body: Some(body), - framed: Some(framed), - } - } -} - -impl NewService for SendResponse -where - T: AsyncRead + AsyncWrite, - B: MessageBody, -{ - type Request = (Response, Framed); - type Response = Framed; - type Error = Error; - type InitError = (); - type Service = SendResponse; - type Future = FutureResult; - - fn new_service(&self, _: &()) -> Self::Future { - ok(SendResponse(PhantomData)) - } -} - -impl Service for SendResponse -where - T: AsyncRead + AsyncWrite, - B: MessageBody, -{ - type Request = (Response, Framed); - type Response = Framed; - type Error = Error; - type Future = SendResponseFut; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, (res, framed): (Response, Framed)) -> Self::Future { - let (res, body) = res.replace_body(()); - SendResponseFut { - res: Some(Message::Item((res, body.length()))), - body: Some(body), - framed: Some(framed), - } - } -} - -pub struct SendResponseFut { - res: Option, BodySize)>>, - body: Option>, - framed: Option>, -} - -impl Future for SendResponseFut -where - T: AsyncRead + AsyncWrite, - B: MessageBody, -{ - type Item = Framed; - type Error = Error; - - fn poll(&mut self) -> Poll { - loop { - let mut body_ready = self.body.is_some(); - let framed = self.framed.as_mut().unwrap(); - - // send body - if self.res.is_none() && self.body.is_some() { - while body_ready && self.body.is_some() && !framed.is_write_buf_full() { - match self.body.as_mut().unwrap().poll_next()? { - Async::Ready(item) => { - // body is done - if item.is_none() { - let _ = self.body.take(); - } - framed.force_send(Message::Chunk(item))?; - } - Async::NotReady => body_ready = false, - } - } - } - - // flush write buffer - if !framed.is_write_buf_empty() { - match framed.poll_complete()? { - Async::Ready(_) => { - if body_ready { - continue; - } else { - return Ok(Async::NotReady); - } - } - Async::NotReady => return Ok(Async::NotReady), - } - } - - // send response - if let Some(res) = self.res.take() { - framed.force_send(res)?; - continue; - } - - if self.body.is_some() { - if body_ready { - continue; - } else { - return Ok(Async::NotReady); - } - } else { - break; - } - } - Ok(Async::Ready(self.framed.take().unwrap())) - } -} diff --git a/actix-http/src/service/service.rs b/actix-http/src/service/service.rs deleted file mode 100644 index 50a1a6bdf..000000000 --- a/actix-http/src/service/service.rs +++ /dev/null @@ -1,347 +0,0 @@ -use std::fmt::Debug; -use std::marker::PhantomData; -use std::{fmt, io}; - -use actix_codec::{AsyncRead, AsyncWrite, Framed, FramedParts}; -use actix_server_config::{Io as ServerIo, Protocol, ServerConfig as SrvConfig}; -use actix_service::{IntoNewService, NewService, Service}; -use actix_utils::cloneable::CloneableService; -use bytes::{Buf, BufMut, Bytes, BytesMut}; -use futures::{try_ready, Async, Future, IntoFuture, Poll}; -use h2::server::{self, Handshake}; -use log::error; - -use crate::body::MessageBody; -use crate::builder::HttpServiceBuilder; -use crate::config::{KeepAlive, ServiceConfig}; -use crate::error::DispatchError; -use crate::request::Request; -use crate::response::Response; -use crate::{h1, h2::Dispatcher}; - -/// `NewService` HTTP1.1/HTTP2 transport implementation -pub struct HttpService { - srv: S, - cfg: ServiceConfig, - _t: PhantomData<(T, P, B)>, -} - -impl HttpService -where - S: NewService, - S::Service: 'static, - S::Error: Debug + 'static, - S::Response: Into>, - B: MessageBody + 'static, -{ - /// Create builder for `HttpService` instance. - pub fn build() -> HttpServiceBuilder { - HttpServiceBuilder::new() - } -} - -impl HttpService -where - S: NewService, - S::Service: 'static, - S::Error: Debug + 'static, - S::Response: Into>, - B: MessageBody + 'static, -{ - /// Create new `HttpService` instance. - pub fn new>(service: F) -> Self { - let cfg = ServiceConfig::new(KeepAlive::Timeout(5), 5000, 0); - - HttpService { - cfg, - srv: service.into_new_service(), - _t: PhantomData, - } - } - - /// Create new `HttpService` instance with config. - pub(crate) fn with_config>( - cfg: ServiceConfig, - service: F, - ) -> Self { - HttpService { - cfg, - srv: service.into_new_service(), - _t: PhantomData, - } - } -} - -impl NewService for HttpService -where - T: AsyncRead + AsyncWrite + 'static, - S: NewService, - S::Service: 'static, - S::Error: Debug, - S::Response: Into>, - B: MessageBody + 'static, -{ - type Request = ServerIo; - type Response = (); - type Error = DispatchError; - type InitError = S::InitError; - type Service = HttpServiceHandler; - type Future = HttpServiceResponse; - - fn new_service(&self, cfg: &SrvConfig) -> Self::Future { - HttpServiceResponse { - fut: self.srv.new_service(cfg).into_future(), - cfg: Some(self.cfg.clone()), - _t: PhantomData, - } - } -} - -#[doc(hidden)] -pub struct HttpServiceResponse, B> { - fut: ::Future, - cfg: Option, - _t: PhantomData<(T, P, B)>, -} - -impl Future for HttpServiceResponse -where - T: AsyncRead + AsyncWrite, - S: NewService, - S::Service: 'static, - S::Response: Into>, - S::Error: Debug, - B: MessageBody + 'static, -{ - type Item = HttpServiceHandler; - type Error = S::InitError; - - fn poll(&mut self) -> Poll { - let service = try_ready!(self.fut.poll()); - Ok(Async::Ready(HttpServiceHandler::new( - self.cfg.take().unwrap(), - service, - ))) - } -} - -/// `Service` implementation for http transport -pub struct HttpServiceHandler { - srv: CloneableService, - cfg: ServiceConfig, - _t: PhantomData<(T, P, B)>, -} - -impl HttpServiceHandler -where - S: Service + 'static, - S::Error: Debug, - S::Response: Into>, - B: MessageBody + 'static, -{ - fn new(cfg: ServiceConfig, srv: S) -> HttpServiceHandler { - HttpServiceHandler { - cfg, - srv: CloneableService::new(srv), - _t: PhantomData, - } - } -} - -impl Service for HttpServiceHandler -where - T: AsyncRead + AsyncWrite + 'static, - S: Service + 'static, - S::Error: Debug, - S::Response: Into>, - B: MessageBody + 'static, -{ - type Request = ServerIo; - type Response = (); - type Error = DispatchError; - type Future = HttpServiceHandlerResponse; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.srv.poll_ready().map_err(|e| { - error!("Service readiness error: {:?}", e); - DispatchError::Service - }) - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - let (io, _, proto) = req.into_parts(); - match proto { - Protocol::Http2 => { - let io = Io { - inner: io, - unread: None, - }; - HttpServiceHandlerResponse { - state: State::Handshake(Some(( - server::handshake(io), - self.cfg.clone(), - self.srv.clone(), - ))), - } - } - Protocol::Http10 | Protocol::Http11 => HttpServiceHandlerResponse { - state: State::H1(h1::Dispatcher::new( - io, - self.cfg.clone(), - self.srv.clone(), - )), - }, - _ => HttpServiceHandlerResponse { - state: State::Unknown(Some(( - io, - BytesMut::with_capacity(14), - self.cfg.clone(), - self.srv.clone(), - ))), - }, - } - } -} - -enum State + 'static, B: MessageBody> -where - S::Error: fmt::Debug, - T: AsyncRead + AsyncWrite + 'static, -{ - H1(h1::Dispatcher), - H2(Dispatcher, S, B>), - Unknown(Option<(T, BytesMut, ServiceConfig, CloneableService)>), - Handshake(Option<(Handshake, Bytes>, ServiceConfig, CloneableService)>), -} - -pub struct HttpServiceHandlerResponse -where - T: AsyncRead + AsyncWrite + 'static, - S: Service + 'static, - S::Error: Debug, - S::Response: Into>, - B: MessageBody + 'static, -{ - state: State, -} - -const HTTP2_PREFACE: [u8; 14] = *b"PRI * HTTP/2.0"; - -impl Future for HttpServiceHandlerResponse -where - T: AsyncRead + AsyncWrite, - S: Service + 'static, - S::Error: Debug, - S::Response: Into>, - B: MessageBody, -{ - type Item = (); - type Error = DispatchError; - - fn poll(&mut self) -> Poll { - match self.state { - State::H1(ref mut disp) => disp.poll(), - State::H2(ref mut disp) => disp.poll(), - State::Unknown(ref mut data) => { - if let Some(ref mut item) = data { - loop { - unsafe { - let b = item.1.bytes_mut(); - let n = try_ready!(item.0.poll_read(b)); - if n == 0 { - return Ok(Async::Ready(())); - } - item.1.advance_mut(n); - if item.1.len() >= HTTP2_PREFACE.len() { - break; - } - } - } - } else { - panic!() - } - let (io, buf, cfg, srv) = data.take().unwrap(); - if buf[..14] == HTTP2_PREFACE[..] { - let io = Io { - inner: io, - unread: Some(buf), - }; - self.state = - State::Handshake(Some((server::handshake(io), cfg, srv))); - } else { - let framed = Framed::from_parts(FramedParts::with_read_buf( - io, - h1::Codec::new(cfg.clone()), - buf, - )); - self.state = - State::H1(h1::Dispatcher::with_timeout(framed, cfg, None, srv)) - } - self.poll() - } - State::Handshake(ref mut data) => { - let conn = if let Some(ref mut item) = data { - match item.0.poll() { - Ok(Async::Ready(conn)) => conn, - Ok(Async::NotReady) => return Ok(Async::NotReady), - Err(err) => { - trace!("H2 handshake error: {}", err); - return Err(err.into()); - } - } - } else { - panic!() - }; - let (_, cfg, srv) = data.take().unwrap(); - self.state = State::H2(Dispatcher::new(srv, conn, cfg, None)); - self.poll() - } - } - } -} - -/// Wrapper for `AsyncRead + AsyncWrite` types -struct Io { - unread: Option, - inner: T, -} - -impl io::Read for Io { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - if let Some(mut bytes) = self.unread.take() { - let size = std::cmp::min(buf.len(), bytes.len()); - buf[..size].copy_from_slice(&bytes[..size]); - if bytes.len() > size { - bytes.split_to(size); - self.unread = Some(bytes); - } - Ok(size) - } else { - self.inner.read(buf) - } - } -} - -impl io::Write for Io { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.inner.write(buf) - } - fn flush(&mut self) -> io::Result<()> { - self.inner.flush() - } -} - -impl AsyncRead for Io { - unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [u8]) -> bool { - self.inner.prepare_uninitialized_buffer(buf) - } -} - -impl AsyncWrite for Io { - fn shutdown(&mut self) -> Poll<(), io::Error> { - self.inner.shutdown() - } - fn write_buf(&mut self, buf: &mut B) -> Poll { - self.inner.write_buf(buf) - } -} diff --git a/actix-http/src/test.rs b/actix-http/src/test.rs index 023161246..b4344a676 100644 --- a/actix-http/src/test.rs +++ b/actix-http/src/test.rs @@ -1,13 +1,18 @@ //! Test Various helpers for Actix applications to use during testing. use std::fmt::Write as FmtWrite; +use std::io; use std::str::FromStr; -use bytes::Bytes; +use actix_codec::{AsyncRead, AsyncWrite}; +use actix_server_config::IoStream; +use bytes::{Buf, Bytes, BytesMut}; +use futures::{Async, Poll}; use http::header::{self, HeaderName, HeaderValue}; -use http::{HeaderMap, HttpTryFrom, Method, Uri, Version}; +use http::{HttpTryFrom, Method, Uri, Version}; use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use crate::cookie::{Cookie, CookieJar}; +use crate::header::HeaderMap; use crate::header::{Header, IntoHeaderValue}; use crate::payload::Payload; use crate::Request; @@ -177,6 +182,89 @@ impl TestRequest { } #[inline] -fn parts<'a>(parts: &'a mut Option) -> &'a mut Inner { +fn parts(parts: &mut Option) -> &mut Inner { parts.as_mut().expect("cannot reuse test request builder") } + +/// Async io buffer +pub struct TestBuffer { + pub read_buf: BytesMut, + pub write_buf: BytesMut, + pub err: Option, +} + +impl TestBuffer { + /// Create new TestBuffer instance + pub fn new(data: T) -> TestBuffer + where + BytesMut: From, + { + TestBuffer { + read_buf: BytesMut::from(data), + write_buf: BytesMut::new(), + err: None, + } + } + + /// Create new empty TestBuffer instance + pub fn empty() -> TestBuffer { + TestBuffer::new("") + } + + /// Add extra data to read buffer. + pub fn extend_read_buf>(&mut self, data: T) { + self.read_buf.extend_from_slice(data.as_ref()) + } +} + +impl io::Read for TestBuffer { + fn read(&mut self, dst: &mut [u8]) -> Result { + if self.read_buf.is_empty() { + if self.err.is_some() { + Err(self.err.take().unwrap()) + } else { + Err(io::Error::new(io::ErrorKind::WouldBlock, "")) + } + } else { + let size = std::cmp::min(self.read_buf.len(), dst.len()); + let b = self.read_buf.split_to(size); + dst[..size].copy_from_slice(&b); + Ok(size) + } + } +} + +impl io::Write for TestBuffer { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.write_buf.extend(buf); + Ok(buf.len()) + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl AsyncRead for TestBuffer {} + +impl AsyncWrite for TestBuffer { + fn shutdown(&mut self) -> Poll<(), io::Error> { + Ok(Async::Ready(())) + } + fn write_buf(&mut self, _: &mut B) -> Poll { + Ok(Async::NotReady) + } +} + +impl IoStream for TestBuffer { + fn set_nodelay(&mut self, _nodelay: bool) -> io::Result<()> { + Ok(()) + } + + fn set_linger(&mut self, _dur: Option) -> io::Result<()> { + Ok(()) + } + + fn set_keepalive(&mut self, _dur: Option) -> io::Result<()> { + Ok(()) + } +} diff --git a/actix-http/src/ws/mod.rs b/actix-http/src/ws/mod.rs index 065c34d93..891d5110d 100644 --- a/actix-http/src/ws/mod.rs +++ b/actix-http/src/ws/mod.rs @@ -9,21 +9,18 @@ use derive_more::{Display, From}; use http::{header, Method, StatusCode}; use crate::error::ResponseError; -use crate::httpmessage::HttpMessage; -use crate::request::Request; +use crate::message::RequestHead; use crate::response::{Response, ResponseBuilder}; mod codec; mod frame; mod mask; mod proto; -mod service; mod transport; pub use self::codec::{Codec, Frame, Message}; pub use self::frame::Parser; pub use self::proto::{hash_key, CloseCode, CloseReason, OpCode}; -pub use self::service::VerifyWebSockets; pub use self::transport::Transport; /// Websocket protocol errors @@ -112,7 +109,7 @@ impl ResponseError for HandshakeError { // /// `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: &Request) -> Result { +pub fn handshake(req: &RequestHead) -> Result { verify_handshake(req)?; Ok(handshake_response(req)) } @@ -121,9 +118,9 @@ pub fn handshake(req: &Request) -> 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 verify_handshake(req: &Request) -> Result<(), HandshakeError> { +pub fn verify_handshake(req: &RequestHead) -> Result<(), HandshakeError> { // WebSocket accepts only GET - if *req.method() != Method::GET { + if req.method != Method::GET { return Err(HandshakeError::GetMethodRequired); } @@ -171,7 +168,7 @@ pub fn verify_handshake(req: &Request) -> Result<(), HandshakeError> { /// Create websocket's handshake response /// /// This function returns handshake `Response`, ready to send to peer. -pub fn handshake_response(req: &Request) -> ResponseBuilder { +pub fn handshake_response(req: &RequestHead) -> ResponseBuilder { let key = { let key = req.headers().get(header::SEC_WEBSOCKET_KEY).unwrap(); proto::hash_key(key.as_ref()) @@ -195,13 +192,13 @@ mod tests { let req = TestRequest::default().method(Method::POST).finish(); assert_eq!( HandshakeError::GetMethodRequired, - verify_handshake(&req).err().unwrap() + verify_handshake(req.head()).err().unwrap() ); let req = TestRequest::default().finish(); assert_eq!( HandshakeError::NoWebsocketUpgrade, - verify_handshake(&req).err().unwrap() + verify_handshake(req.head()).err().unwrap() ); let req = TestRequest::default() @@ -209,7 +206,7 @@ mod tests { .finish(); assert_eq!( HandshakeError::NoWebsocketUpgrade, - verify_handshake(&req).err().unwrap() + verify_handshake(req.head()).err().unwrap() ); let req = TestRequest::default() @@ -220,7 +217,7 @@ mod tests { .finish(); assert_eq!( HandshakeError::NoConnectionUpgrade, - verify_handshake(&req).err().unwrap() + verify_handshake(req.head()).err().unwrap() ); let req = TestRequest::default() @@ -235,7 +232,7 @@ mod tests { .finish(); assert_eq!( HandshakeError::NoVersionHeader, - verify_handshake(&req).err().unwrap() + verify_handshake(req.head()).err().unwrap() ); let req = TestRequest::default() @@ -254,7 +251,7 @@ mod tests { .finish(); assert_eq!( HandshakeError::UnsupportedVersion, - verify_handshake(&req).err().unwrap() + verify_handshake(req.head()).err().unwrap() ); let req = TestRequest::default() @@ -273,7 +270,7 @@ mod tests { .finish(); assert_eq!( HandshakeError::BadWebsocketKey, - verify_handshake(&req).err().unwrap() + verify_handshake(req.head()).err().unwrap() ); let req = TestRequest::default() @@ -296,7 +293,7 @@ mod tests { .finish(); assert_eq!( StatusCode::SWITCHING_PROTOCOLS, - handshake_response(&req).finish().status() + handshake_response(req.head()).finish().status() ); } diff --git a/actix-http/src/ws/service.rs b/actix-http/src/ws/service.rs deleted file mode 100644 index f3b066053..000000000 --- a/actix-http/src/ws/service.rs +++ /dev/null @@ -1,52 +0,0 @@ -use std::marker::PhantomData; - -use actix_codec::Framed; -use actix_service::{NewService, Service}; -use futures::future::{ok, FutureResult}; -use futures::{Async, IntoFuture, Poll}; - -use crate::h1::Codec; -use crate::request::Request; - -use super::{verify_handshake, HandshakeError}; - -pub struct VerifyWebSockets { - _t: PhantomData, -} - -impl Default for VerifyWebSockets { - fn default() -> Self { - VerifyWebSockets { _t: PhantomData } - } -} - -impl NewService for VerifyWebSockets { - type Request = (Request, Framed); - type Response = (Request, Framed); - type Error = (HandshakeError, Framed); - type InitError = (); - type Service = VerifyWebSockets; - type Future = FutureResult; - - fn new_service(&self, _: &()) -> Self::Future { - ok(VerifyWebSockets { _t: PhantomData }) - } -} - -impl Service for VerifyWebSockets { - type Request = (Request, Framed); - type Response = (Request, Framed); - type Error = (HandshakeError, Framed); - type Future = FutureResult; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, (req, framed): (Request, Framed)) -> Self::Future { - match verify_handshake(&req) { - Err(e) => Err((e, framed)).into_future(), - Ok(_) => Ok((req, framed)).into_future(), - } - } -} diff --git a/actix-http/tests/test_client.rs b/actix-http/tests/test_client.rs index 817164f81..6d382478f 100644 --- a/actix-http/tests/test_client.rs +++ b/actix-http/tests/test_client.rs @@ -31,9 +31,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ fn test_h1_v2() { env_logger::init(); let mut srv = TestServer::new(move || { - HttpService::build() - .finish(|_| future::ok::<_, ()>(Response::Ok().body(STR))) - .map(|_| ()) + HttpService::build().finish(|_| future::ok::<_, ()>(Response::Ok().body(STR))) }); let response = srv.block_on(srv.get("/").send()).unwrap(); assert!(response.status().is_success()); @@ -61,7 +59,9 @@ fn test_connection_close() { .finish(|_| ok::<_, ()>(Response::Ok().body(STR))) .map(|_| ()) }); + println!("REQ: {:?}", srv.get("/").force_close()); let response = srv.block_on(srv.get("/").force_close().send()).unwrap(); + println!("RES: {:?}", response); assert!(response.status().is_success()); } diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index 85cab929c..4b56e4b2c 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -5,10 +5,11 @@ use std::{net, thread}; use actix_codec::{AsyncRead, AsyncWrite}; use actix_http_test::TestServer; use actix_server_config::ServerConfig; -use actix_service::{fn_cfg_factory, NewService}; +use actix_service::{fn_cfg_factory, fn_service, NewService}; use bytes::{Bytes, BytesMut}; use futures::future::{self, ok, Future}; use futures::stream::{once, Stream}; +use tokio_timer::sleep; use actix_http::body::Body; use actix_http::error::PayloadError; @@ -34,7 +35,10 @@ fn test_h1() { .keep_alive(KeepAlive::Disabled) .client_timeout(1000) .client_disconnect(1000) - .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) + .h1(|req: Request| { + assert!(req.peer_addr().is_some()); + future::ok::<_, ()>(Response::Ok().finish()) + }) }); let response = srv.block_on(srv.get("/").send()).unwrap(); @@ -49,6 +53,7 @@ fn test_h1_2() { .client_timeout(1000) .client_disconnect(1000) .finish(|req: Request| { + assert!(req.peer_addr().is_some()); assert_eq!(req.version(), http::Version::HTTP_11); future::ok::<_, ()>(Response::Ok().finish()) }) @@ -114,6 +119,7 @@ fn test_h2_1() -> std::io::Result<()> { .and_then( HttpService::build() .finish(|req: Request| { + assert!(req.peer_addr().is_some()); assert_eq!(req.version(), http::Version::HTTP_2); future::ok::<_, Error>(Response::Ok().finish()) }) @@ -153,6 +159,62 @@ fn test_h2_body() -> std::io::Result<()> { Ok(()) } +#[test] +fn test_expect_continue() { + let srv = TestServer::new(|| { + HttpService::build() + .expect(fn_service(|req: Request| { + if req.head().uri.query() == Some("yes=") { + Ok(req) + } else { + Err(error::ErrorPreconditionFailed("error")) + } + })) + .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) + }); + + let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); + let _ = stream.write_all(b"GET /test HTTP/1.1\r\nexpect: 100-continue\r\n\r\n"); + let mut data = String::new(); + let _ = stream.read_to_string(&mut data); + assert!(data.starts_with("HTTP/1.1 412 Precondition Failed\r\ncontent-length")); + + let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); + let _ = stream.write_all(b"GET /test?yes= HTTP/1.1\r\nexpect: 100-continue\r\n\r\n"); + let mut data = String::new(); + let _ = stream.read_to_string(&mut data); + assert!(data.starts_with("HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\n")); +} + +#[test] +fn test_expect_continue_h1() { + let srv = TestServer::new(|| { + HttpService::build() + .expect(fn_service(|req: Request| { + sleep(Duration::from_millis(20)).then(move |_| { + if req.head().uri.query() == Some("yes=") { + Ok(req) + } else { + Err(error::ErrorPreconditionFailed("error")) + } + }) + })) + .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) + }); + + let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); + let _ = stream.write_all(b"GET /test HTTP/1.1\r\nexpect: 100-continue\r\n\r\n"); + let mut data = String::new(); + let _ = stream.read_to_string(&mut data); + assert!(data.starts_with("HTTP/1.1 412 Precondition Failed\r\ncontent-length")); + + let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); + let _ = stream.write_all(b"GET /test?yes= HTTP/1.1\r\nexpect: 100-continue\r\n\r\n"); + let mut data = String::new(); + let _ = stream.read_to_string(&mut data); + assert!(data.starts_with("HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\n")); +} + #[test] fn test_slow_request() { let srv = TestServer::new(|| { @@ -867,7 +929,7 @@ fn test_h1_response_http_error_handling() { // read response let bytes = srv.load_body(response).unwrap(); - assert!(bytes.is_empty()); + assert_eq!(bytes, Bytes::from_static(b"failed to parse header value")); } #[cfg(feature = "ssl")] @@ -900,7 +962,7 @@ fn test_h2_response_http_error_handling() { // read response let bytes = srv.load_body(response).unwrap(); - assert!(bytes.is_empty()); + assert_eq!(bytes, Bytes::from_static(b"failed to parse header value")); } #[test] @@ -911,11 +973,11 @@ fn test_h1_service_error() { }); let response = srv.block_on(srv.get("/").send()).unwrap(); - assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(response.status(), http::StatusCode::BAD_REQUEST); // read response let bytes = srv.load_body(response).unwrap(); - assert!(bytes.is_empty()); + assert_eq!(bytes, Bytes::from_static(b"error")); } #[cfg(feature = "ssl")] diff --git a/actix-http/tests/test_ws.rs b/actix-http/tests/test_ws.rs new file mode 100644 index 000000000..65a4d094d --- /dev/null +++ b/actix-http/tests/test_ws.rs @@ -0,0 +1,76 @@ +use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use actix_http::{body, h1, ws, Error, HttpService, Request, Response}; +use actix_http_test::TestServer; +use actix_utils::framed::FramedTransport; +use bytes::{Bytes, BytesMut}; +use futures::future::{self, ok}; +use futures::{Future, Sink, Stream}; + +fn ws_service( + (req, framed): (Request, Framed), +) -> impl Future { + let res = ws::handshake(req.head()).unwrap().message_body(()); + + framed + .send((res, body::BodySize::None).into()) + .map_err(|_| panic!()) + .and_then(|framed| { + FramedTransport::new(framed.into_framed(ws::Codec::new()), service) + .map_err(|_| panic!()) + }) +} + +fn service(msg: ws::Frame) -> impl Future { + let msg = match msg { + ws::Frame::Ping(msg) => ws::Message::Pong(msg), + ws::Frame::Text(text) => { + ws::Message::Text(String::from_utf8_lossy(&text.unwrap()).to_string()) + } + ws::Frame::Binary(bin) => ws::Message::Binary(bin.unwrap().freeze()), + ws::Frame::Close(reason) => ws::Message::Close(reason), + _ => panic!(), + }; + ok(msg) +} + +#[test] +fn test_simple() { + let mut srv = TestServer::new(|| { + HttpService::build() + .upgrade(ws_service) + .finish(|_| future::ok::<_, ()>(Response::NotFound())) + }); + + // client service + let framed = srv.ws().unwrap(); + let framed = srv + .block_on(framed.send(ws::Message::Text("text".to_string()))) + .unwrap(); + let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!(item, Some(ws::Frame::Text(Some(BytesMut::from("text"))))); + + let framed = srv + .block_on(framed.send(ws::Message::Binary("text".into()))) + .unwrap(); + let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!( + item, + Some(ws::Frame::Binary(Some(Bytes::from_static(b"text").into()))) + ); + + let framed = srv + .block_on(framed.send(ws::Message::Ping("text".into()))) + .unwrap(); + let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!(item, Some(ws::Frame::Pong("text".to_string().into()))); + + let framed = srv + .block_on(framed.send(ws::Message::Close(Some(ws::CloseCode::Normal.into())))) + .unwrap(); + + let (item, _framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!( + item, + Some(ws::Frame::Close(Some(ws::CloseCode::Normal.into()))) + ); +} diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md new file mode 100644 index 000000000..9f8fa052a --- /dev/null +++ b/actix-multipart/CHANGES.md @@ -0,0 +1,9 @@ +# Changes + +## [0.1.0-beta.1] - 2019-04-21 + +* Do not support nested multipart + +* Split multipart support to separate crate + +* Optimize multipart handling #634, #769 \ No newline at end of file diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml new file mode 100644 index 000000000..8e1714e7d --- /dev/null +++ b/actix-multipart/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "actix-multipart" +version = "0.1.0-beta.1" +authors = ["Nikolay Kim "] +description = "Multipart support for actix web framework." +readme = "README.md" +keywords = ["http", "web", "framework", "async", "futures"] +homepage = "https://actix.rs" +repository = "https://github.com/actix/actix-web.git" +documentation = "https://docs.rs/actix-multipart/" +license = "MIT/Apache-2.0" +exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] +workspace = ".." +edition = "2018" + +[lib] +name = "actix_multipart" +path = "src/lib.rs" + +[dependencies] +actix-web = "1.0.0-beta.1" +actix-service = "0.3.6" +bytes = "0.4" +derive_more = "0.14" +httparse = "1.3" +futures = "0.1.25" +log = "0.4" +mime = "0.3" +time = "0.1" +twoway = "0.2" + +[dev-dependencies] +actix-rt = "0.2.2" +actix-http = "0.1.1" \ No newline at end of file diff --git a/actix-multipart/README.md b/actix-multipart/README.md new file mode 100644 index 000000000..2739ff3d5 --- /dev/null +++ b/actix-multipart/README.md @@ -0,0 +1 @@ +# Multipart support for actix web framework [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-multipart)](https://crates.io/crates/actix-multipart) [![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) diff --git a/actix-multipart/src/error.rs b/actix-multipart/src/error.rs new file mode 100644 index 000000000..995585850 --- /dev/null +++ b/actix-multipart/src/error.rs @@ -0,0 +1,49 @@ +//! Error and Result module +use actix_web::error::{ParseError, PayloadError}; +use actix_web::http::StatusCode; +use actix_web::{HttpResponse, ResponseError}; +use derive_more::{Display, From}; + +/// A set of errors that can occur during parsing multipart streams +#[derive(Debug, Display, From)] +pub enum MultipartError { + /// Content-Type header is not found + #[display(fmt = "No Content-type header found")] + NoContentType, + /// Can not parse Content-Type header + #[display(fmt = "Can not parse Content-Type header")] + ParseContentType, + /// Multipart boundary is not found + #[display(fmt = "Multipart boundary is not found")] + Boundary, + /// Nested multipart is not supported + #[display(fmt = "Nested multipart is not supported")] + Nested, + /// Multipart stream is incomplete + #[display(fmt = "Multipart stream is incomplete")] + Incomplete, + /// Error during field parsing + #[display(fmt = "{}", _0)] + Parse(ParseError), + /// Payload error + #[display(fmt = "{}", _0)] + Payload(PayloadError), +} + +/// Return `BadRequest` for `MultipartError` +impl ResponseError for MultipartError { + fn error_response(&self) -> HttpResponse { + HttpResponse::new(StatusCode::BAD_REQUEST) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_multipart_error() { + let resp: HttpResponse = MultipartError::Boundary.error_response(); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } +} diff --git a/actix-multipart/src/extractor.rs b/actix-multipart/src/extractor.rs new file mode 100644 index 000000000..7274ed092 --- /dev/null +++ b/actix-multipart/src/extractor.rs @@ -0,0 +1,42 @@ +//! Multipart payload support +use actix_web::{dev::Payload, Error, FromRequest, HttpRequest}; + +use crate::server::Multipart; + +/// Get request's payload as multipart stream +/// +/// Content-type: multipart/form-data; +/// +/// ## Server example +/// +/// ```rust +/// # use futures::{Future, Stream}; +/// # use futures::future::{ok, result, Either}; +/// use actix_web::{web, HttpResponse, Error}; +/// use actix_multipart as mp; +/// +/// fn index(payload: mp::Multipart) -> impl Future { +/// payload.from_err() // <- get multipart stream for current request +/// .and_then(|field| { // <- iterate over multipart items +/// // Field in turn is stream of *Bytes* object +/// field.from_err() +/// .fold((), |_, chunk| { +/// println!("-- CHUNK: \n{:?}", std::str::from_utf8(&chunk)); +/// Ok::<_, Error>(()) +/// }) +/// }) +/// .fold((), |_, _| Ok::<_, Error>(())) +/// .map(|_| HttpResponse::Ok().into()) +/// } +/// # fn main() {} +/// ``` +impl FromRequest for Multipart { + type Error = Error; + type Future = Result; + type Config = (); + + #[inline] + fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { + Ok(Multipart::new(req.headers(), payload.take())) + } +} diff --git a/actix-multipart/src/lib.rs b/actix-multipart/src/lib.rs new file mode 100644 index 000000000..d8f365b2c --- /dev/null +++ b/actix-multipart/src/lib.rs @@ -0,0 +1,6 @@ +mod error; +mod extractor; +mod server; + +pub use self::error::MultipartError; +pub use self::server::{Field, Multipart}; diff --git a/src/types/multipart.rs b/actix-multipart/src/server.rs similarity index 54% rename from src/types/multipart.rs rename to actix-multipart/src/server.rs index 65a64d5e1..82b0b5ace 100644 --- a/src/types/multipart.rs +++ b/actix-multipart/src/server.rs @@ -4,26 +4,22 @@ use std::marker::PhantomData; use std::rc::Rc; use std::{cmp, fmt}; -use bytes::Bytes; +use bytes::{Bytes, BytesMut}; use futures::task::{current as current_task, Task}; use futures::{Async, Poll, Stream}; use httparse; use mime; -use crate::error::{Error, MultipartError, ParseError, PayloadError}; -use crate::extract::FromRequest; -use crate::http::header::{ +use actix_web::error::{ParseError, PayloadError}; +use actix_web::http::header::{ self, ContentDisposition, HeaderMap, HeaderName, HeaderValue, }; -use crate::http::HttpTryFrom; -use crate::service::ServiceFromRequest; -use crate::HttpMessage; +use actix_web::http::HttpTryFrom; + +use crate::error::MultipartError; const MAX_HEADERS: usize = 32; -type PayloadBuffer = - actix_http::h1::PayloadBuffer>>; - /// The server-side implementation of `multipart/form-data` requests. /// /// This will parse the incoming stream into `MultipartItem` instances via its @@ -36,64 +32,9 @@ pub struct Multipart { inner: Option>>, } -/// Multipart item -pub enum MultipartItem { - /// Multipart field - Field(MultipartField), - /// Nested multipart stream - Nested(Multipart), -} - -/// Get request's payload as multipart stream -/// -/// Content-type: multipart/form-data; -/// -/// ## Server example -/// -/// ```rust -/// # use futures::{Future, Stream}; -/// # use futures::future::{ok, result, Either}; -/// use actix_web::{web, HttpResponse, Error}; -/// -/// fn index(payload: web::Multipart) -> impl Future { -/// payload.from_err() // <- get multipart stream for current request -/// .and_then(|item| match item { // <- iterate over multipart items -/// web::MultipartItem::Field(field) => { -/// // Field in turn is stream of *Bytes* object -/// Either::A(field.from_err() -/// .fold((), |_, chunk| { -/// println!("-- CHUNK: \n{:?}", std::str::from_utf8(&chunk)); -/// Ok::<_, Error>(()) -/// })) -/// }, -/// web::MultipartItem::Nested(mp) => { -/// // Or item could be nested Multipart stream -/// Either::B(ok(())) -/// } -/// }) -/// .fold((), |_, _| Ok::<_, Error>(())) -/// .map(|_| HttpResponse::Ok().into()) -/// } -/// # fn main() {} -/// ``` -impl

FromRequest

for Multipart -where - P: Stream + 'static, -{ - type Error = Error; - type Future = Result; - - #[inline] - fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { - let pl = req.take_payload(); - Ok(Multipart::new(req.headers(), pl)) - } -} - enum InnerMultipartItem { None, Field(Rc>), - Multipart(Rc>), } #[derive(PartialEq, Debug)] @@ -142,7 +83,7 @@ impl Multipart { /// Extract boundary info from headers. fn boundary(headers: &HeaderMap) -> Result { - if let Some(content_type) = headers.get(header::CONTENT_TYPE) { + if let Some(content_type) = headers.get(&header::CONTENT_TYPE) { if let Ok(content_type) = content_type.to_str() { if let Ok(ct) = content_type.parse::() { if let Some(boundary) = ct.get_param(mime::BOUNDARY) { @@ -163,14 +104,18 @@ impl Multipart { } impl Stream for Multipart { - type Item = MultipartItem; + type Item = Field; type Error = MultipartError; fn poll(&mut self) -> Poll, Self::Error> { if let Some(err) = self.error.take() { Err(err) } else if self.safety.current() { - self.inner.as_mut().unwrap().borrow_mut().poll(&self.safety) + let mut inner = self.inner.as_mut().unwrap().borrow_mut(); + if let Some(payload) = inner.payload.get_mut(&self.safety) { + payload.poll_stream()?; + } + inner.poll(&self.safety) } else { Ok(Async::NotReady) } @@ -178,11 +123,18 @@ impl Stream for Multipart { } impl InnerMultipart { - fn read_headers(payload: &mut PayloadBuffer) -> Poll { - match payload.read_until(b"\r\n\r\n")? { - Async::NotReady => Ok(Async::NotReady), - Async::Ready(None) => Err(MultipartError::Incomplete), - Async::Ready(Some(bytes)) => { + fn read_headers( + payload: &mut PayloadBuffer, + ) -> Result, MultipartError> { + match payload.read_until(b"\r\n\r\n") { + None => { + if payload.eof { + Err(MultipartError::Incomplete) + } else { + Ok(None) + } + } + Some(bytes) => { let mut hdrs = [httparse::EMPTY_HEADER; MAX_HEADERS]; match httparse::parse_headers(&bytes, &mut hdrs) { Ok(httparse::Status::Complete((_, hdrs))) => { @@ -199,7 +151,7 @@ impl InnerMultipart { return Err(ParseError::Header.into()); } } - Ok(Async::Ready(headers)) + Ok(Some(headers)) } Ok(httparse::Status::Partial) => Err(ParseError::Header.into()), Err(err) => Err(ParseError::from(err).into()), @@ -211,23 +163,28 @@ impl InnerMultipart { fn read_boundary( payload: &mut PayloadBuffer, boundary: &str, - ) -> Poll { + ) -> Result, MultipartError> { // TODO: need to read epilogue - match payload.readline()? { - Async::NotReady => Ok(Async::NotReady), - Async::Ready(None) => Err(MultipartError::Incomplete), - Async::Ready(Some(chunk)) => { + match payload.readline() { + None => { + if payload.eof { + Ok(Some(true)) + } else { + Ok(None) + } + } + Some(chunk) => { if chunk.len() == boundary.len() + 4 && &chunk[..2] == b"--" && &chunk[2..boundary.len() + 2] == boundary.as_bytes() { - Ok(Async::Ready(false)) + Ok(Some(false)) } else if chunk.len() == boundary.len() + 6 && &chunk[..2] == b"--" && &chunk[2..boundary.len() + 2] == boundary.as_bytes() && &chunk[boundary.len() + 2..boundary.len() + 4] == b"--" { - Ok(Async::Ready(true)) + Ok(Some(true)) } else { Err(MultipartError::Boundary) } @@ -238,14 +195,13 @@ impl InnerMultipart { fn skip_until_boundary( payload: &mut PayloadBuffer, boundary: &str, - ) -> Poll { + ) -> Result, MultipartError> { let mut eof = false; loop { - match payload.readline()? { - Async::Ready(Some(chunk)) => { + match payload.readline() { + Some(chunk) => { if chunk.is_empty() { - //ValueError("Could not find starting boundary %r" - //% (self._boundary)) + return Err(MultipartError::Boundary); } if chunk.len() < boundary.len() { continue; @@ -267,14 +223,19 @@ impl InnerMultipart { } } } - Async::NotReady => return Ok(Async::NotReady), - Async::Ready(None) => return Err(MultipartError::Incomplete), + None => { + return if payload.eof { + Err(MultipartError::Incomplete) + } else { + Ok(None) + }; + } } } - Ok(Async::Ready(eof)) + Ok(Some(eof)) } - fn poll(&mut self, safety: &Safety) -> Poll, MultipartError> { + fn poll(&mut self, safety: &Safety) -> Poll, MultipartError> { if self.state == InnerState::Eof { Ok(Async::Ready(None)) } else { @@ -291,14 +252,7 @@ impl InnerMultipart { Async::Ready(None) => true, } } - InnerMultipartItem::Multipart(ref mut multipart) => { - match multipart.borrow_mut().poll(safety)? { - Async::NotReady => return Ok(Async::NotReady), - Async::Ready(Some(_)) => continue, - Async::Ready(None) => true, - } - } - _ => false, + InnerMultipartItem::None => false, }; if stop { self.item = InnerMultipartItem::None; @@ -317,7 +271,7 @@ impl InnerMultipart { payload, &self.boundary, )? { - Async::Ready(eof) => { + Some(eof) => { if eof { self.state = InnerState::Eof; return Ok(Async::Ready(None)); @@ -325,14 +279,14 @@ impl InnerMultipart { self.state = InnerState::Headers; } } - Async::NotReady => return Ok(Async::NotReady), + None => return Ok(Async::NotReady), } } // read boundary InnerState::Boundary => { match InnerMultipart::read_boundary(payload, &self.boundary)? { - Async::NotReady => return Ok(Async::NotReady), - Async::Ready(eof) => { + None => return Ok(Async::NotReady), + Some(eof) => { if eof { self.state = InnerState::Eof; return Ok(Async::Ready(None)); @@ -347,8 +301,7 @@ impl InnerMultipart { // read field headers for next field if self.state == InnerState::Headers { - if let Async::Ready(headers) = InnerMultipart::read_headers(payload)? - { + if let Some(headers) = InnerMultipart::read_headers(payload)? { self.state = InnerState::Boundary; headers } else { @@ -364,7 +317,7 @@ impl InnerMultipart { // content type let mut mt = mime::APPLICATION_OCTET_STREAM; - if let Some(content_type) = headers.get(header::CONTENT_TYPE) { + if let Some(content_type) = headers.get(&header::CONTENT_TYPE) { if let Ok(content_type) = content_type.to_str() { if let Ok(ct) = content_type.parse::() { mt = ct; @@ -376,24 +329,7 @@ impl InnerMultipart { // nested multipart stream if mt.type_() == mime::MULTIPART { - let inner = if let Some(boundary) = mt.get_param(mime::BOUNDARY) { - Rc::new(RefCell::new(InnerMultipart { - payload: self.payload.clone(), - boundary: boundary.as_str().to_owned(), - state: InnerState::FirstBoundary, - item: InnerMultipartItem::None, - })) - } else { - return Err(MultipartError::Boundary); - }; - - self.item = InnerMultipartItem::Multipart(Rc::clone(&inner)); - - Ok(Async::Ready(Some(MultipartItem::Nested(Multipart { - safety: safety.clone(), - error: None, - inner: Some(inner), - })))) + Err(MultipartError::Nested) } else { let field = Rc::new(RefCell::new(InnerField::new( self.payload.clone(), @@ -402,8 +338,11 @@ impl InnerMultipart { )?)); self.item = InnerMultipartItem::Field(Rc::clone(&field)); - Ok(Async::Ready(Some(MultipartItem::Field( - MultipartField::new(safety.clone(), headers, mt, field), + Ok(Async::Ready(Some(Field::new( + safety.clone(), + headers, + mt, + field, )))) } } @@ -418,21 +357,21 @@ impl Drop for InnerMultipart { } /// A single field in a multipart stream -pub struct MultipartField { +pub struct Field { ct: mime::Mime, headers: HeaderMap, inner: Rc>, safety: Safety, } -impl MultipartField { +impl Field { fn new( safety: Safety, headers: HeaderMap, ct: mime::Mime, inner: Rc>, ) -> Self { - MultipartField { + Field { ct, headers, inner, @@ -454,7 +393,7 @@ impl MultipartField { pub fn content_disposition(&self) -> Option { // RFC 7578: 'Each part MUST contain a Content-Disposition header field // where the disposition type is "form-data".' - if let Some(content_disposition) = self.headers.get(header::CONTENT_DISPOSITION) + if let Some(content_disposition) = self.headers.get(&header::CONTENT_DISPOSITION) { ContentDisposition::from_raw(content_disposition).ok() } else { @@ -463,22 +402,28 @@ impl MultipartField { } } -impl Stream for MultipartField { +impl Stream for Field { type Item = Bytes; type Error = MultipartError; fn poll(&mut self) -> Poll, Self::Error> { if self.safety.current() { - self.inner.borrow_mut().poll(&self.safety) + let mut inner = self.inner.borrow_mut(); + if let Some(payload) = inner.payload.as_ref().unwrap().get_mut(&self.safety) + { + payload.poll_stream()?; + } + + inner.poll(&self.safety) } else { Ok(Async::NotReady) } } } -impl fmt::Debug for MultipartField { +impl fmt::Debug for Field { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "\nMultipartField: {}", self.ct)?; + writeln!(f, "\nField: {}", self.ct)?; writeln!(f, " boundary: {}", self.inner.borrow().boundary)?; writeln!(f, " headers:")?; for (key, val) in self.headers.iter() { @@ -501,7 +446,7 @@ impl InnerField { boundary: String, headers: &HeaderMap, ) -> Result { - let len = if let Some(len) = headers.get(header::CONTENT_LENGTH) { + let len = if let Some(len) = headers.get(&header::CONTENT_LENGTH) { if let Ok(s) = len.to_str() { if let Ok(len) = s.parse::() { Some(len) @@ -532,10 +477,8 @@ impl InnerField { if *size == 0 { Ok(Async::Ready(None)) } else { - match payload.readany() { - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(None)) => Err(MultipartError::Incomplete), - Ok(Async::Ready(Some(mut chunk))) => { + match payload.read_max(*size) { + Some(mut chunk) => { let len = cmp::min(chunk.len() as u64, *size); *size -= len; let ch = chunk.split_to(len as usize); @@ -544,7 +487,13 @@ impl InnerField { } Ok(Async::Ready(Some(ch))) } - Err(err) => Err(err.into()), + None => { + if payload.eof && (*size != 0) { + Err(MultipartError::Incomplete) + } else { + Ok(Async::NotReady) + } + } } } } @@ -555,38 +504,73 @@ impl InnerField { payload: &mut PayloadBuffer, boundary: &str, ) -> Poll, MultipartError> { - match payload.read_until(b"\r")? { - Async::NotReady => Ok(Async::NotReady), - Async::Ready(None) => Err(MultipartError::Incomplete), - Async::Ready(Some(mut chunk)) => { - if chunk.len() == 1 { - payload.unprocessed(chunk); - match payload.read_exact(boundary.len() + 4)? { - Async::NotReady => Ok(Async::NotReady), - Async::Ready(None) => Err(MultipartError::Incomplete), - Async::Ready(Some(mut chunk)) => { - if &chunk[..2] == b"\r\n" - && &chunk[2..4] == b"--" - && &chunk[4..] == boundary.as_bytes() - { - payload.unprocessed(chunk); - Ok(Async::Ready(None)) - } else { - // \r might be part of data stream - let ch = chunk.split_to(1); - payload.unprocessed(chunk); - Ok(Async::Ready(Some(ch))) - } - } - } + let mut pos = 0; + + let len = payload.buf.len(); + if len == 0 { + return Ok(Async::NotReady); + } + + // check boundary + if len > 4 && payload.buf[0] == b'\r' { + let b_len = if &payload.buf[..2] == b"\r\n" && &payload.buf[2..4] == b"--" { + Some(4) + } else if &payload.buf[1..3] == b"--" { + Some(3) + } else { + None + }; + + if let Some(b_len) = b_len { + let b_size = boundary.len() + b_len; + if len < b_size { + return Ok(Async::NotReady); } else { - let to = chunk.len() - 1; - let ch = chunk.split_to(to); - payload.unprocessed(chunk); - Ok(Async::Ready(Some(ch))) + if &payload.buf[b_len..b_size] == boundary.as_bytes() { + // found boundary + return Ok(Async::Ready(None)); + } else { + pos = b_size; + } } } } + + loop { + return if let Some(idx) = twoway::find_bytes(&payload.buf[pos..], b"\r") { + let cur = pos + idx; + + // check if we have enough data for boundary detection + if cur + 4 > len { + if cur > 0 { + Ok(Async::Ready(Some(payload.buf.split_to(cur).freeze()))) + } else { + Ok(Async::NotReady) + } + } else { + // check boundary + if (&payload.buf[cur..cur + 2] == b"\r\n" + && &payload.buf[cur + 2..cur + 4] == b"--") + || (&payload.buf[cur..cur + 1] == b"\r" + && &payload.buf[cur + 1..cur + 3] == b"--") + { + if cur != 0 { + // return buffer + Ok(Async::Ready(Some(payload.buf.split_to(cur).freeze()))) + } else { + pos = cur + 1; + continue; + } + } else { + // not boundary + pos = cur + 1; + continue; + } + } + } else { + return Ok(Async::Ready(Some(payload.buf.take().freeze()))); + }; + } } fn poll(&mut self, s: &Safety) -> Poll, MultipartError> { @@ -595,27 +579,27 @@ impl InnerField { } let result = if let Some(payload) = self.payload.as_ref().unwrap().get_mut(s) { - let res = if let Some(ref mut len) = self.length { - InnerField::read_len(payload, len)? - } else { - InnerField::read_stream(payload, &self.boundary)? - }; + if !self.eof { + let res = if let Some(ref mut len) = self.length { + InnerField::read_len(payload, len)? + } else { + InnerField::read_stream(payload, &self.boundary)? + }; - match res { - Async::NotReady => Async::NotReady, - Async::Ready(Some(bytes)) => Async::Ready(Some(bytes)), - Async::Ready(None) => { - self.eof = true; - match payload.readline()? { - Async::NotReady => Async::NotReady, - Async::Ready(None) => Async::Ready(None), - Async::Ready(Some(line)) => { - if line.as_ref() != b"\r\n" { - log::warn!("multipart field did not read all the data or it is malformed"); - } - Async::Ready(None) - } + match res { + Async::NotReady => return Ok(Async::NotReady), + Async::Ready(Some(bytes)) => return Ok(Async::Ready(Some(bytes))), + Async::Ready(None) => self.eof = true, + } + } + + match payload.readline() { + None => Async::Ready(None), + Some(line) => { + if line.as_ref() != b"\r\n" { + log::warn!("multipart field did not read all the data or it is malformed"); } + Async::Ready(None) } } } else { @@ -711,14 +695,86 @@ impl Drop for Safety { } } +/// Payload buffer +struct PayloadBuffer { + eof: bool, + buf: BytesMut, + stream: Box>, +} + +impl PayloadBuffer { + /// Create new `PayloadBuffer` instance + fn new(stream: S) -> Self + where + S: Stream + 'static, + { + PayloadBuffer { + eof: false, + buf: BytesMut::new(), + stream: Box::new(stream), + } + } + + fn poll_stream(&mut self) -> Result<(), PayloadError> { + loop { + match self.stream.poll()? { + Async::Ready(Some(data)) => self.buf.extend_from_slice(&data), + Async::Ready(None) => { + self.eof = true; + return Ok(()); + } + Async::NotReady => return Ok(()), + } + } + } + + /// Read exact number of bytes + #[cfg(test)] + fn read_exact(&mut self, size: usize) -> Option { + if size <= self.buf.len() { + Some(self.buf.split_to(size).freeze()) + } else { + None + } + } + + fn read_max(&mut self, size: u64) -> Option { + if !self.buf.is_empty() { + let size = std::cmp::min(self.buf.len() as u64, size) as usize; + Some(self.buf.split_to(size).freeze()) + } else { + None + } + } + + /// Read until specified ending + pub fn read_until(&mut self, line: &[u8]) -> Option { + twoway::find_bytes(&self.buf, line) + .map(|idx| self.buf.split_to(idx + line.len()).freeze()) + } + + /// Read bytes until new line delimiter + pub fn readline(&mut self) -> Option { + self.read_until(b"\n") + } + + /// Put unprocessed data back to the buffer + pub fn unprocessed(&mut self, data: Bytes) { + let buf = BytesMut::from(data); + let buf = std::mem::replace(&mut self.buf, buf); + self.buf.extend_from_slice(&buf); + } +} + #[cfg(test)] mod tests { + use actix_http::h1::Payload; use bytes::Bytes; use futures::unsync::mpsc; use super::*; - use crate::http::header::{DispositionParam, DispositionType}; - use crate::test::run_on; + use actix_web::http::header::{DispositionParam, DispositionType}; + use actix_web::test::run_on; #[test] fn test_boundary() { @@ -799,58 +855,220 @@ mod tests { ); let mut multipart = Multipart::new(&headers, payload); - match multipart.poll() { - Ok(Async::Ready(Some(item))) => match item { - MultipartItem::Field(mut field) => { - { - let cd = field.content_disposition().unwrap(); - assert_eq!(cd.disposition, DispositionType::FormData); - assert_eq!( - cd.parameters[0], - DispositionParam::Name("file".into()) - ); - } - assert_eq!(field.content_type().type_(), mime::TEXT); - assert_eq!(field.content_type().subtype(), mime::PLAIN); + match multipart.poll().unwrap() { + Async::Ready(Some(mut field)) => { + let cd = field.content_disposition().unwrap(); + assert_eq!(cd.disposition, DispositionType::FormData); + assert_eq!(cd.parameters[0], DispositionParam::Name("file".into())); - match field.poll() { - Ok(Async::Ready(Some(chunk))) => assert_eq!(chunk, "test"), - _ => unreachable!(), - } - match field.poll() { - Ok(Async::Ready(None)) => (), - _ => unreachable!(), - } + assert_eq!(field.content_type().type_(), mime::TEXT); + assert_eq!(field.content_type().subtype(), mime::PLAIN); + + match field.poll().unwrap() { + Async::Ready(Some(chunk)) => assert_eq!(chunk, "test"), + _ => unreachable!(), } - _ => unreachable!(), - }, + match field.poll().unwrap() { + Async::Ready(None) => (), + _ => unreachable!(), + } + } _ => unreachable!(), } - match multipart.poll() { - Ok(Async::Ready(Some(item))) => match item { - MultipartItem::Field(mut field) => { - assert_eq!(field.content_type().type_(), mime::TEXT); - assert_eq!(field.content_type().subtype(), mime::PLAIN); + match multipart.poll().unwrap() { + Async::Ready(Some(mut field)) => { + assert_eq!(field.content_type().type_(), mime::TEXT); + assert_eq!(field.content_type().subtype(), mime::PLAIN); - match field.poll() { - Ok(Async::Ready(Some(chunk))) => assert_eq!(chunk, "data"), - _ => unreachable!(), - } - match field.poll() { - Ok(Async::Ready(None)) => (), - _ => unreachable!(), - } + match field.poll() { + Ok(Async::Ready(Some(chunk))) => assert_eq!(chunk, "data"), + _ => unreachable!(), } - _ => unreachable!(), - }, + match field.poll() { + Ok(Async::Ready(None)) => (), + _ => unreachable!(), + } + } _ => unreachable!(), } - match multipart.poll() { - Ok(Async::Ready(None)) => (), + match multipart.poll().unwrap() { + Async::Ready(None) => (), _ => unreachable!(), } }); } + + #[test] + fn test_stream() { + run_on(|| { + let (sender, payload) = create_stream(); + + let bytes = Bytes::from( + "testasdadsad\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ + Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\ + Content-Type: text/plain; charset=utf-8\r\n\r\n\ + test\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ + Content-Type: text/plain; charset=utf-8\r\n\r\n\ + data\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0--\r\n", + ); + sender.unbounded_send(Ok(bytes)).unwrap(); + + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static( + "multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", + ), + ); + + let mut multipart = Multipart::new(&headers, payload); + match multipart.poll().unwrap() { + Async::Ready(Some(mut field)) => { + let cd = field.content_disposition().unwrap(); + assert_eq!(cd.disposition, DispositionType::FormData); + assert_eq!(cd.parameters[0], DispositionParam::Name("file".into())); + + assert_eq!(field.content_type().type_(), mime::TEXT); + assert_eq!(field.content_type().subtype(), mime::PLAIN); + + match field.poll().unwrap() { + Async::Ready(Some(chunk)) => assert_eq!(chunk, "test"), + _ => unreachable!(), + } + match field.poll().unwrap() { + Async::Ready(None) => (), + _ => unreachable!(), + } + } + _ => unreachable!(), + } + + match multipart.poll().unwrap() { + Async::Ready(Some(mut field)) => { + assert_eq!(field.content_type().type_(), mime::TEXT); + assert_eq!(field.content_type().subtype(), mime::PLAIN); + + match field.poll() { + Ok(Async::Ready(Some(chunk))) => assert_eq!(chunk, "data"), + _ => unreachable!(), + } + match field.poll() { + Ok(Async::Ready(None)) => (), + _ => unreachable!(), + } + } + _ => unreachable!(), + } + + match multipart.poll().unwrap() { + Async::Ready(None) => (), + _ => unreachable!(), + } + }); + } + + #[test] + fn test_basic() { + run_on(|| { + let (_, payload) = Payload::create(false); + let mut payload = PayloadBuffer::new(payload); + + assert_eq!(payload.buf.len(), 0); + payload.poll_stream().unwrap(); + assert_eq!(None, payload.read_max(1)); + }) + } + + #[test] + fn test_eof() { + run_on(|| { + let (mut sender, payload) = Payload::create(false); + let mut payload = PayloadBuffer::new(payload); + + assert_eq!(None, payload.read_max(4)); + sender.feed_data(Bytes::from("data")); + sender.feed_eof(); + payload.poll_stream().unwrap(); + + assert_eq!(Some(Bytes::from("data")), payload.read_max(4)); + assert_eq!(payload.buf.len(), 0); + assert_eq!(None, payload.read_max(1)); + assert!(payload.eof); + }) + } + + #[test] + fn test_err() { + run_on(|| { + let (mut sender, payload) = Payload::create(false); + let mut payload = PayloadBuffer::new(payload); + assert_eq!(None, payload.read_max(1)); + sender.set_error(PayloadError::Incomplete(None)); + payload.poll_stream().err().unwrap(); + }) + } + + #[test] + fn test_readmax() { + run_on(|| { + let (mut sender, payload) = Payload::create(false); + let mut payload = PayloadBuffer::new(payload); + + sender.feed_data(Bytes::from("line1")); + sender.feed_data(Bytes::from("line2")); + payload.poll_stream().unwrap(); + assert_eq!(payload.buf.len(), 10); + + assert_eq!(Some(Bytes::from("line1")), payload.read_max(5)); + assert_eq!(payload.buf.len(), 5); + + assert_eq!(Some(Bytes::from("line2")), payload.read_max(5)); + assert_eq!(payload.buf.len(), 0); + }) + } + + #[test] + fn test_readexactly() { + run_on(|| { + let (mut sender, payload) = Payload::create(false); + let mut payload = PayloadBuffer::new(payload); + + assert_eq!(None, payload.read_exact(2)); + + sender.feed_data(Bytes::from("line1")); + sender.feed_data(Bytes::from("line2")); + payload.poll_stream().unwrap(); + + assert_eq!(Some(Bytes::from_static(b"li")), payload.read_exact(2)); + assert_eq!(payload.buf.len(), 8); + + assert_eq!(Some(Bytes::from_static(b"ne1l")), payload.read_exact(4)); + assert_eq!(payload.buf.len(), 4); + }) + } + + #[test] + fn test_readuntil() { + run_on(|| { + let (mut sender, payload) = Payload::create(false); + let mut payload = PayloadBuffer::new(payload); + + assert_eq!(None, payload.read_until(b"ne")); + + sender.feed_data(Bytes::from("line1")); + sender.feed_data(Bytes::from("line2")); + payload.poll_stream().unwrap(); + + assert_eq!(Some(Bytes::from("line")), payload.read_until(b"ne")); + assert_eq!(payload.buf.len(), 6); + + assert_eq!(Some(Bytes::from("1line2")), payload.read_until(b"2")); + assert_eq!(payload.buf.len(), 0); + }) + } } diff --git a/actix-session/CHANGES.md b/actix-session/CHANGES.md index 85e1123a9..a60d1e668 100644 --- a/actix-session/CHANGES.md +++ b/actix-session/CHANGES.md @@ -1,5 +1,19 @@ # Changes +## [0.1.0-beta.1] - 2019-04-20 + +* Update actix-web to beta.1 + +* `CookieSession::max_age()` accepts value in seconds + +## [0.1.0-alpha.6] - 2019-04-14 + +* Update actix-web alpha.6 + +## [0.1.0-alpha.4] - 2019-04-08 + +* Update actix-web + ## [0.1.0-alpha.3] - 2019-04-02 * Update actix-web diff --git a/actix-session/Cargo.toml b/actix-session/Cargo.toml index 956906fad..83f9807f7 100644 --- a/actix-session/Cargo.toml +++ b/actix-session/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-session" -version = "0.1.0-alpha.3" +version = "0.1.0-beta.1" authors = ["Nikolay Kim "] description = "Session for actix web framework." readme = "README.md" @@ -24,15 +24,15 @@ default = ["cookie-session"] cookie-session = ["actix-web/secure-cookies"] [dependencies] -actix-web = "1.0.0-alpha.3" +actix-web = "1.0.0-beta.1" actix-service = "0.3.4" bytes = "0.4" derive_more = "0.14" futures = "0.1.25" -hashbrown = "0.1.8" +hashbrown = "0.2.2" serde = "1.0" serde_json = "1.0" -time = "0.1" +time = "0.1.42" [dev-dependencies] actix-rt = "0.2.2" diff --git a/actix-session/src/cookie.rs b/actix-session/src/cookie.rs index f7b4ec03a..ac08d1146 100644 --- a/actix-session/src/cookie.rs +++ b/actix-session/src/cookie.rs @@ -27,7 +27,6 @@ use derive_more::{Display, From}; use futures::future::{ok, Future, FutureResult}; use futures::Poll; use serde_json::error::Error as JsonError; -use time::Duration; use crate::Session; @@ -57,7 +56,7 @@ struct CookieSessionInner { domain: Option, secure: bool, http_only: bool, - max_age: Option, + max_age: Option, same_site: Option, } @@ -120,7 +119,7 @@ impl CookieSessionInner { Ok(()) } - fn load

(&self, req: &ServiceRequest

) -> HashMap { + fn load(&self, req: &ServiceRequest) -> HashMap { if let Ok(cookies) = req.cookies() { for cookie in cookies.iter() { if cookie.name() == self.name { @@ -250,19 +249,24 @@ impl CookieSession { } /// Sets the `max-age` field in the session cookie being built. - pub fn max_age(mut self, value: Duration) -> CookieSession { + pub fn max_age(self, seconds: i64) -> CookieSession { + self.max_age_time(time::Duration::seconds(seconds)) + } + + /// Sets the `max-age` field in the session cookie being built. + pub fn max_age_time(mut self, value: time::Duration) -> CookieSession { Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); self } } -impl Transform for CookieSession +impl Transform for CookieSession where - S: Service, Response = ServiceResponse>, + S: Service>, S::Future: 'static, S::Error: 'static, { - type Request = ServiceRequest

; + type Request = ServiceRequest; type Response = ServiceResponse; type Error = S::Error; type InitError = (); @@ -283,23 +287,22 @@ pub struct CookieSessionMiddleware { inner: Rc, } -impl Service for CookieSessionMiddleware +impl Service for CookieSessionMiddleware where - S: Service, Response = ServiceResponse>, + S: Service>, S::Future: 'static, S::Error: 'static, { - type Request = ServiceRequest

; + type Request = ServiceRequest; type Response = ServiceResponse; type Error = S::Error; type Future = Box>; fn poll_ready(&mut self) -> Poll<(), Self::Error> { - //self.service.poll_ready().map_err(|e| e.into()) self.service.poll_ready() } - fn call(&mut self, mut req: ServiceRequest

) -> Self::Future { + fn call(&mut self, mut req: ServiceRequest) -> Self::Future { let inner = self.inner.clone(); let state = self.inner.load(&req); Session::set_session(state.into_iter(), &mut req); @@ -318,6 +321,7 @@ where mod tests { use super::*; use actix_web::{test, web, App}; + use bytes::Bytes; #[test] fn cookie_session() { @@ -339,6 +343,26 @@ mod tests { .is_some()); } + #[test] + fn private_cookie() { + let mut app = test::init_service( + App::new() + .wrap(CookieSession::private(&[0; 32]).secure(false)) + .service(web::resource("/").to(|ses: Session| { + let _ = ses.set("counter", 100); + "test" + })), + ); + + let request = test::TestRequest::get().to_request(); + let response = test::block_on(app.call(request)).unwrap(); + assert!(response + .response() + .cookies() + .find(|c| c.name() == "actix-session") + .is_some()); + } + #[test] fn cookie_session_extractor() { let mut app = test::init_service( @@ -358,4 +382,44 @@ mod tests { .find(|c| c.name() == "actix-session") .is_some()); } + + #[test] + fn basics() { + let mut app = test::init_service( + App::new() + .wrap( + CookieSession::signed(&[0; 32]) + .path("/test/") + .name("actix-test") + .domain("localhost") + .http_only(true) + .same_site(SameSite::Lax) + .max_age(100), + ) + .service(web::resource("/").to(|ses: Session| { + let _ = ses.set("counter", 100); + "test" + })) + .service(web::resource("/test/").to(|ses: Session| { + let val: usize = ses.get("counter").unwrap().unwrap(); + format!("counter: {}", val) + })), + ); + + let request = test::TestRequest::get().to_request(); + let response = test::block_on(app.call(request)).unwrap(); + let cookie = response + .response() + .cookies() + .find(|c| c.name() == "actix-test") + .unwrap() + .clone(); + assert_eq!(cookie.path().unwrap(), "/test/"); + + let request = test::TestRequest::with_uri("/test/") + .cookie(cookie) + .to_request(); + let body = test::read_response(&mut app, request); + assert_eq!(body, Bytes::from_static(b"counter: 100")); + } } diff --git a/actix-session/src/lib.rs b/actix-session/src/lib.rs index 0cd1b9ed8..b82029647 100644 --- a/actix-session/src/lib.rs +++ b/actix-session/src/lib.rs @@ -45,8 +45,8 @@ use std::cell::RefCell; use std::rc::Rc; -use actix_web::dev::{ServiceFromRequest, ServiceRequest, ServiceResponse}; -use actix_web::{Error, FromRequest, HttpMessage}; +use actix_web::dev::{Extensions, Payload, ServiceRequest, ServiceResponse}; +use actix_web::{Error, FromRequest, HttpMessage, HttpRequest}; use hashbrown::HashMap; use serde::de::DeserializeOwned; use serde::Serialize; @@ -119,11 +119,11 @@ impl Session { inner.state.clear() } - pub fn set_session

( + pub fn set_session( data: impl Iterator, - req: &mut ServiceRequest

, + req: &mut ServiceRequest, ) { - let session = Session::get_session(req); + let session = Session::get_session(&mut *req.extensions_mut()); let mut inner = session.0.borrow_mut(); inner.state.extend(data); } @@ -144,12 +144,12 @@ impl Session { } } - fn get_session(req: R) -> Session { - if let Some(s_impl) = req.extensions().get::>>() { + fn get_session(extensions: &mut Extensions) -> Session { + if let Some(s_impl) = extensions.get::>>() { return Session(Rc::clone(&s_impl)); } let inner = Rc::new(RefCell::new(SessionInner::default())); - req.extensions_mut().insert(inner.clone()); + extensions.insert(inner.clone()); Session(inner) } } @@ -172,13 +172,14 @@ impl Session { /// } /// # fn main() {} /// ``` -impl

FromRequest

for Session { +impl FromRequest for Session { type Error = Error; type Future = Result; + type Config = (); #[inline] - fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { - Ok(Session::get_session(req)) + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + Ok(Session::get_session(&mut *req.extensions_mut())) } } @@ -196,7 +197,7 @@ mod tests { vec![("key".to_string(), "\"value\"".to_string())].into_iter(), &mut req, ); - let session = Session::get_session(&mut req); + let session = Session::get_session(&mut *req.extensions_mut()); let res = session.get::("key").unwrap(); assert_eq!(res, Some("value".to_string())); diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 598d39459..22fdf613d 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -18,13 +18,13 @@ name = "actix_web_actors" path = "src/lib.rs" [dependencies] -actix = "0.8.0-alpha.2" -actix-web = "1.0.0-alpha.3" -actix-http = "0.1.0-alpha.3" +actix = "0.8.0" +actix-web = "1.0.0-beta.1" +actix-http = "0.1.1" actix-codec = "0.1.2" bytes = "0.4" futures = "0.1.25" [dev-dependencies] env_logger = "0.6" -actix-http-test = { version = "0.1.0-alpha.3", features=["ssl"] } +actix-http-test = { version = "0.1.0", features=["ssl"] } diff --git a/actix-web-actors/src/context.rs b/actix-web-actors/src/context.rs index da473ff3f..31b29500a 100644 --- a/actix-web-actors/src/context.rs +++ b/actix-web-actors/src/context.rs @@ -199,7 +199,7 @@ mod tests { use actix::Actor; use actix_web::http::StatusCode; - use actix_web::test::{block_on, call_success, init_service, TestRequest}; + use actix_web::test::{block_on, call_service, init_service, TestRequest}; use actix_web::{web, App, HttpResponse}; use bytes::{Bytes, BytesMut}; @@ -237,7 +237,7 @@ mod tests { }))); let req = TestRequest::with_uri("/test").to_request(); - let mut resp = call_success(&mut srv, req); + let mut resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::OK); let body = block_on(resp.take_body().fold( diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index 642222560..0ef3c9169 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -50,7 +50,7 @@ pub fn handshake(req: &HttpRequest) -> Result Result Result"] @@ -16,6 +16,7 @@ quote = "0.6" syn = { version = "0.15", features = ["full", "parsing"] } [dev-dependencies] -actix-web = { version = "1.0.0-alpha.2" } -actix-http = { version = "0.1.0-alpha.2", features=["ssl"] } -actix-http-test = { version = "0.1.0-alpha.2", features=["ssl"] } \ No newline at end of file +actix-web = { version = "1.0.0-alpha.6" } +actix-http = { version = "0.1.1", features=["ssl"] } +actix-http-test = { version = "0.1.0", features=["ssl"] } +futures = { version = "0.1" } diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 16123930a..70cde90e4 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -1,118 +1,94 @@ #![recursion_limit = "512"] +//! Actix-web codegen module +//! +//! Generators for routes and scopes +//! +//! ## Route +//! +//! Macros: +//! +//! - [get](attr.get.html) +//! - [post](attr.post.html) +//! - [put](attr.put.html) +//! - [delete](attr.delete.html) +//! +//! ### Attributes: +//! +//! - `"path"` - Raw literal string with path for which to register handle. Mandatory. +//! - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard` +//! +//! ## Notes +//! +//! Function name can be specified as any expression that is going to be accessible to the generate +//! code (e.g `my_guard` or `my_module::my_guard`) +//! +//! ## Example: +//! +//! ```rust +//! use actix_web::HttpResponse; +//! use actix_web_codegen::get; +//! use futures::{future, Future}; +//! +//! #[get("/test")] +//! fn async_test() -> impl Future { +//! future::ok(HttpResponse::Ok().finish()) +//! } +//! ``` extern crate proc_macro; +mod route; + use proc_macro::TokenStream; -use quote::quote; use syn::parse_macro_input; -/// #[get("path")] attribute +/// Creates route handler with `GET` method guard. +/// +/// Syntax: `#[get("path"[, attributes])]` +/// +/// ## Attributes: +/// +/// - `"path"` - Raw literal string with path for which to register handler. Mandatory. +/// - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard` #[proc_macro_attribute] pub fn get(args: TokenStream, input: TokenStream) -> TokenStream { let args = parse_macro_input!(args as syn::AttributeArgs); - if args.is_empty() { - panic!("invalid server definition, expected: #[get(\"some path\")]"); - } - - // path - let path = match args[0] { - syn::NestedMeta::Literal(syn::Lit::Str(ref fname)) => { - let fname = quote!(#fname).to_string(); - fname.as_str()[1..fname.len() - 1].to_owned() - } - _ => panic!("resource path"), - }; - - let ast: syn::ItemFn = syn::parse(input).unwrap(); - let name = ast.ident.clone(); - - (quote! { - #[allow(non_camel_case_types)] - struct #name; - - impl actix_web::dev::HttpServiceFactory

for #name { - fn register(self, config: &mut actix_web::dev::ServiceConfig

) { - #ast - actix_web::dev::HttpServiceFactory::register( - actix_web::Resource::new(#path) - .guard(actix_web::guard::Get()) - .to(#name), config); - } - } - }) - .into() + let gen = route::Args::new(&args, input, route::GuardType::Get); + gen.generate() } -/// #[post("path")] attribute +/// Creates route handler with `POST` method guard. +/// +/// Syntax: `#[post("path"[, attributes])]` +/// +/// Attributes are the same as in [get](attr.get.html) #[proc_macro_attribute] pub fn post(args: TokenStream, input: TokenStream) -> TokenStream { let args = parse_macro_input!(args as syn::AttributeArgs); - if args.is_empty() { - panic!("invalid server definition, expected: #[post(\"some path\")]"); - } - - // path - let path = match args[0] { - syn::NestedMeta::Literal(syn::Lit::Str(ref fname)) => { - let fname = quote!(#fname).to_string(); - fname.as_str()[1..fname.len() - 1].to_owned() - } - _ => panic!("resource path"), - }; - - let ast: syn::ItemFn = syn::parse(input).unwrap(); - let name = ast.ident.clone(); - - (quote! { - #[allow(non_camel_case_types)] - struct #name; - - impl actix_web::dev::HttpServiceFactory

for #name { - fn register(self, config: &mut actix_web::dev::ServiceConfig

) { - #ast - actix_web::dev::HttpServiceFactory::register( - actix_web::Resource::new(#path) - .guard(actix_web::guard::Post()) - .to(#name), config); - } - } - }) - .into() + let gen = route::Args::new(&args, input, route::GuardType::Post); + gen.generate() } -/// #[put("path")] attribute +/// Creates route handler with `PUT` method guard. +/// +/// Syntax: `#[put("path"[, attributes])]` +/// +/// Attributes are the same as in [get](attr.get.html) #[proc_macro_attribute] pub fn put(args: TokenStream, input: TokenStream) -> TokenStream { let args = parse_macro_input!(args as syn::AttributeArgs); - if args.is_empty() { - panic!("invalid server definition, expected: #[put(\"some path\")]"); - } - - // path - let path = match args[0] { - syn::NestedMeta::Literal(syn::Lit::Str(ref fname)) => { - let fname = quote!(#fname).to_string(); - fname.as_str()[1..fname.len() - 1].to_owned() - } - _ => panic!("resource path"), - }; - - let ast: syn::ItemFn = syn::parse(input).unwrap(); - let name = ast.ident.clone(); - - (quote! { - #[allow(non_camel_case_types)] - struct #name; - - impl actix_web::dev::HttpServiceFactory

for #name { - fn register(self, config: &mut actix_web::dev::ServiceConfig

) { - #ast - actix_web::dev::HttpServiceFactory::register( - actix_web::Resource::new(#path) - .guard(actix_web::guard::Put()) - .to(#name), config); - } - } - }) - .into() + let gen = route::Args::new(&args, input, route::GuardType::Put); + gen.generate() +} + +/// Creates route handler with `DELETE` method guard. +/// +/// Syntax: `#[delete("path"[, attributes])]` +/// +/// Attributes are the same as in [get](attr.get.html) +#[proc_macro_attribute] +pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream { + let args = parse_macro_input!(args as syn::AttributeArgs); + let gen = route::Args::new(&args, input, route::GuardType::Delete); + gen.generate() } diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs new file mode 100644 index 000000000..1a5f79298 --- /dev/null +++ b/actix-web-codegen/src/route.rs @@ -0,0 +1,184 @@ +extern crate proc_macro; + +use std::fmt; + +use proc_macro::TokenStream; +use quote::quote; + +enum ResourceType { + Async, + Sync, +} + +impl fmt::Display for ResourceType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + &ResourceType::Async => write!(f, "to_async"), + &ResourceType::Sync => write!(f, "to"), + } + } +} + +#[derive(PartialEq)] +pub enum GuardType { + Get, + Post, + Put, + Delete, +} + +impl fmt::Display for GuardType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + &GuardType::Get => write!(f, "Get"), + &GuardType::Post => write!(f, "Post"), + &GuardType::Put => write!(f, "Put"), + &GuardType::Delete => write!(f, "Delete"), + } + } +} + +pub struct Args { + name: syn::Ident, + path: String, + ast: syn::ItemFn, + resource_type: ResourceType, + pub guard: GuardType, + pub extra_guards: Vec, +} + +impl fmt::Display for Args { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let ast = &self.ast; + let guards = format!(".guard(actix_web::guard::{}())", self.guard); + let guards = self.extra_guards.iter().fold(guards, |acc, val| { + format!("{}.guard(actix_web::guard::fn_guard({}))", acc, val) + }); + + write!( + f, + " +#[allow(non_camel_case_types)] +pub struct {name}; + +impl actix_web::dev::HttpServiceFactory for {name} {{ + fn register(self, config: &mut actix_web::dev::AppService) {{ + {ast} + + let resource = actix_web::Resource::new(\"{path}\"){guards}.{to}({name}); + + actix_web::dev::HttpServiceFactory::register(resource, config) + }} +}}", + name = self.name, + ast = quote!(#ast), + path = self.path, + guards = guards, + to = self.resource_type + ) + } +} + +fn guess_resource_type(typ: &syn::Type) -> ResourceType { + let mut guess = ResourceType::Sync; + + match typ { + syn::Type::ImplTrait(typ) => { + for bound in typ.bounds.iter() { + match bound { + syn::TypeParamBound::Trait(bound) => { + for bound in bound.path.segments.iter() { + if bound.ident == "Future" { + guess = ResourceType::Async; + break; + } else if bound.ident == "Responder" { + guess = ResourceType::Sync; + break; + } + } + } + _ => (), + } + } + } + _ => (), + } + + guess +} + +impl Args { + pub fn new( + args: &Vec, + input: TokenStream, + guard: GuardType, + ) -> Self { + if args.is_empty() { + panic!( + "invalid server definition, expected: #[{}(\"some path\")]", + guard + ); + } + + let ast: syn::ItemFn = syn::parse(input).expect("Parse input as function"); + let name = ast.ident.clone(); + + let mut extra_guards = Vec::new(); + let mut path = None; + for arg in args { + match arg { + syn::NestedMeta::Literal(syn::Lit::Str(ref fname)) => { + if path.is_some() { + panic!("Multiple paths specified! Should be only one!") + } + let fname = quote!(#fname).to_string(); + path = Some(fname.as_str()[1..fname.len() - 1].to_owned()) + } + syn::NestedMeta::Meta(syn::Meta::NameValue(ident)) => { + match ident.ident.to_string().to_lowercase().as_str() { + "guard" => match ident.lit { + syn::Lit::Str(ref text) => extra_guards.push(text.value()), + _ => panic!("Attribute guard expects literal string!"), + }, + attr => panic!( + "Unknown attribute key is specified: {}. Allowed: guard", + attr + ), + } + } + attr => panic!("Unknown attribute{:?}", attr), + } + } + + let resource_type = if ast.asyncness.is_some() { + ResourceType::Async + } else { + match ast.decl.output { + syn::ReturnType::Default => { + panic!("Function {} has no return type. Cannot be used as handler") + } + syn::ReturnType::Type(_, ref typ) => guess_resource_type(typ.as_ref()), + } + }; + + let path = path.unwrap(); + + Self { + name, + path, + ast, + resource_type, + guard, + extra_guards, + } + } + + pub fn generate(&self) -> TokenStream { + let text = self.to_string(); + + match text.parse() { + Ok(res) => res, + Err(error) => panic!("Error: {:?}\nGenerated code: {}", error, text), + } + } +} diff --git a/actix-web-codegen/tests/test_macro.rs b/actix-web-codegen/tests/test_macro.rs index 8bf2c88be..dd105785d 100644 --- a/actix-web-codegen/tests/test_macro.rs +++ b/actix-web-codegen/tests/test_macro.rs @@ -1,15 +1,40 @@ use actix_http::HttpService; use actix_http_test::TestServer; -use actix_web::{get, http, App, HttpResponse, Responder}; +use actix_web::{http, App, HttpResponse, Responder}; +use actix_web_codegen::get; +use futures::{future, Future}; #[get("/test")] fn test() -> impl Responder { HttpResponse::Ok() } +#[get("/test")] +fn auto_async() -> impl Future { + future::ok(HttpResponse::Ok().finish()) +} + +#[get("/test")] +fn auto_sync() -> impl Future { + future::ok(HttpResponse::Ok().finish()) +} + #[test] fn test_body() { let mut srv = TestServer::new(|| HttpService::new(App::new().service(test))); + let request = srv.request(http::Method::GET, srv.url("/test")); + let response = srv.block_on(request.send()).unwrap(); + assert!(response.status().is_success()); + + let mut srv = TestServer::new(|| HttpService::new(App::new().service(auto_sync))); + let request = srv.request(http::Method::GET, srv.url("/test")); + let response = srv.block_on(request.send()).unwrap(); + assert!(response.status().is_success()); +} + +#[test] +fn test_auto_async() { + let mut srv = TestServer::new(|| HttpService::new(App::new().service(auto_async))); let request = srv.request(http::Method::GET, srv.url("/test")); let response = srv.block_on(request.send()).unwrap(); diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 0f0bd9f5b..30fd4a6d2 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,5 +1,46 @@ # Changes +## [0.1.1] - 2019-04-19 + +### Added + +* Allow to specify server address for http and ws requests. + +### Changed + +* `ClientRequest::if_true()` and `ClientRequest::if_some()` use instance instead of ref + + +## [0.1.0] - 2019-04-16 + +* No changes + + +## [0.1.0-alpha.6] - 2019-04-14 + +### Changed + +* Do not set default headers for websocket request + + +## [0.1.0-alpha.5] - 2019-04-12 + +### Changed + +* Do not set any default headers + +### Added + +* Add Debug impl for BoxedSocket + + +## [0.1.0-alpha.4] - 2019-04-08 + +### Changed + +* Update actix-http dependency + + ## [0.1.0-alpha.3] - 2019-04-02 ### Added diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 81d91e19f..e6018f44f 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "0.1.0-alpha.3" +version = "0.1.1" authors = ["Nikolay Kim "] description = "Actix http client." readme = "README.md" @@ -36,9 +36,9 @@ flate2-zlib = ["actix-http/flate2-zlib"] flate2-rust = ["actix-http/flate2-rust"] [dependencies] -actix-codec = "0.1.1" -actix-service = "0.3.4" -actix-http = "0.1.0-alpa.3" +actix-codec = "0.1.2" +actix-service = "0.3.6" +actix-http = "0.1.2" base64 = "0.10.1" bytes = "0.4" derive_more = "0.14" @@ -55,11 +55,11 @@ openssl = { version="0.10", optional = true } [dev-dependencies] actix-rt = "0.2.2" -actix-web = { version = "1.0.0-alpha.3", features=["ssl"] } -actix-http = { version = "0.1.0-alpa.3", features=["ssl"] } -actix-http-test = { version = "0.1.0-alpha.3", features=["ssl"] } +actix-web = { version = "1.0.0-beta.1", features=["ssl"] } +actix-http = { version = "0.1.2", features=["ssl"] } +actix-http-test = { version = "0.1.0", features=["ssl"] } actix-utils = "0.3.4" -actix-server = { version = "0.4.1", features=["ssl"] } +actix-server = { version = "0.4.3", features=["ssl"] } brotli2 = { version="0.3.2" } flate2 = { version="1.0.2" } env_logger = "0.6" diff --git a/awc/README.md b/awc/README.md index bb64559c1..3b0034d76 100644 --- a/awc/README.md +++ b/awc/README.md @@ -1 +1,33 @@ # Actix http client [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/awc)](https://crates.io/crates/awc) [![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) + +An HTTP Client + +## Documentation & community resources + +* [User Guide](https://actix.rs/docs/) +* [API Documentation](https://docs.rs/awc/) +* [Chat on gitter](https://gitter.im/actix/actix) +* Cargo package: [awc](https://crates.io/crates/awc) +* Minimum supported Rust version: 1.33 or later + +## Example + +```rust +use actix_rt::System; +use awc::Client; +use futures::future::{Future, lazy}; + +fn main() { + System::new("test").block_on(lazy(|| { + let mut client = Client::default(); + + client.get("http://www.rust-lang.org") // <- Create request builder + .header("User-Agent", "Actix-web") + .send() // <- Send http request + .and_then(|response| { // <- server http response + println!("Response: {:?}", response); + Ok(()) + }) + })); +} +``` diff --git a/awc/src/builder.rs b/awc/src/builder.rs index dcea55952..c460f1357 100644 --- a/awc/src/builder.rs +++ b/awc/src/builder.rs @@ -3,8 +3,8 @@ use std::fmt; use std::rc::Rc; use std::time::Duration; -use actix_http::client::{ConnectError, Connection, Connector}; -use actix_http::http::{header, HeaderMap, HeaderName, HttpTryFrom, Uri}; +use actix_http::client::{Connect, ConnectError, Connection, Connector}; +use actix_http::http::{header, HeaderMap, HeaderName, HttpTryFrom}; use actix_service::Service; use crate::connect::ConnectorWrapper; @@ -31,7 +31,7 @@ impl ClientBuilder { headers: HeaderMap::new(), timeout: Some(Duration::from_secs(5)), connector: RefCell::new(Box::new(ConnectorWrapper( - Connector::new().service(), + Connector::new().finish(), ))), }, } @@ -40,7 +40,7 @@ impl ClientBuilder { /// Use custom connector service. pub fn connector(mut self, connector: T) -> Self where - T: Service + 'static, + T: Service + 'static, T::Response: Connection, ::Future: 'static, T::Future: 'static, diff --git a/awc/src/connect.rs b/awc/src/connect.rs index 77cd1fbff..4b564d777 100644 --- a/awc/src/connect.rs +++ b/awc/src/connect.rs @@ -1,10 +1,12 @@ -use std::io; +use std::{fmt, io, net}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_http::body::Body; -use actix_http::client::{ConnectError, Connection, SendRequestError}; +use actix_http::client::{ + Connect as ClientConnect, ConnectError, Connection, SendRequestError, +}; use actix_http::h1::ClientCodec; -use actix_http::{http, RequestHead, ResponseHead}; +use actix_http::{RequestHead, ResponseHead}; use actix_service::Service; use futures::{Future, Poll}; @@ -17,12 +19,14 @@ pub(crate) trait Connect { &mut self, head: RequestHead, body: Body, + addr: Option, ) -> Box>; /// Send request, returns Response and Framed fn open_tunnel( &mut self, head: RequestHead, + addr: Option, ) -> Box< Future< Item = (ResponseHead, Framed), @@ -33,7 +37,7 @@ pub(crate) trait Connect { impl Connect for ConnectorWrapper where - T: Service, + T: Service, T::Response: Connection, ::Io: 'static, ::Future: 'static, @@ -44,11 +48,15 @@ where &mut self, head: RequestHead, body: Body, + addr: Option, ) -> Box> { Box::new( self.0 // connect to the host - .call(head.uri.clone()) + .call(ClientConnect { + uri: head.uri.clone(), + addr, + }) .from_err() // send request .and_then(move |connection| connection.send_request(head, body)) @@ -59,6 +67,7 @@ where fn open_tunnel( &mut self, head: RequestHead, + addr: Option, ) -> Box< Future< Item = (ResponseHead, Framed), @@ -68,7 +77,10 @@ where Box::new( self.0 // connect to the host - .call(head.uri.clone()) + .call(ClientConnect { + uri: head.uri.clone(), + addr, + }) .from_err() // send request .and_then(move |connection| connection.open_tunnel(head)) @@ -102,6 +114,12 @@ impl AsyncSocket for Socket { pub struct BoxedSocket(Box); +impl fmt::Debug for BoxedSocket { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "BoxedSocket") + } +} + impl io::Read for BoxedSocket { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.0.as_read_mut().read(buf) diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 5f9adb463..8c1bc80b6 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -78,7 +78,7 @@ impl Default for Client { fn default() -> Self { Client(Rc::new(ClientConfig { connector: RefCell::new(Box::new(ConnectorWrapper( - Connector::new().service(), + Connector::new().finish(), ))), headers: HeaderMap::new(), timeout: Some(Duration::from_secs(5)), @@ -104,7 +104,7 @@ impl Client { { let mut req = ClientRequest::new(method, url, self.0.clone()); - for (key, value) in &self.0.headers { + for (key, value) in self.0.headers.iter() { req = req.set_header_if_none(key.clone(), value.clone()); } req @@ -119,7 +119,7 @@ impl Client { Uri: HttpTryFrom, { let mut req = self.request(head.method.clone(), url); - for (key, value) in &head.headers { + for (key, value) in head.headers.iter() { req = req.set_header_if_none(key.clone(), value.clone()); } req @@ -187,7 +187,7 @@ impl Client { Uri: HttpTryFrom, { let mut req = ws::WebsocketsRequest::new(url, self.0.clone()); - for (key, value) in &self.0.headers { + for (key, value) in self.0.headers.iter() { req.head.headers.insert(key.clone(), value.clone()); } req diff --git a/awc/src/request.rs b/awc/src/request.rs index ad0a667ec..d99a9418e 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -1,8 +1,8 @@ -use std::fmt; use std::fmt::Write as FmtWrite; use std::io::Write; use std::rc::Rc; use std::time::Duration; +use std::{fmt, net}; use bytes::{BufMut, Bytes, BytesMut}; use futures::future::{err, Either}; @@ -60,8 +60,8 @@ const HTTPS_ENCODING: &str = "gzip, deflate"; pub struct ClientRequest { pub(crate) head: RequestHead, err: Option, + addr: Option, cookies: Option, - default_headers: bool, response_decompress: bool, timeout: Option, config: Rc, @@ -77,9 +77,9 @@ impl ClientRequest { config, head: RequestHead::default(), err: None, + addr: None, cookies: None, timeout: None, - default_headers: true, response_decompress: true, } .method(method) @@ -99,6 +99,15 @@ impl ClientRequest { self } + /// Set socket address of the server. + /// + /// This address is used for connection. If address is not + /// provided url's host name get resolved. + pub fn address(mut self, addr: net::SocketAddr) -> Self { + self.addr = Some(addr); + self + } + /// Set HTTP method of this request. #[inline] pub fn method(mut self, method: Method) -> Self { @@ -330,13 +339,6 @@ impl ClientRequest { self } - /// Do not add default request headers. - /// By default `Date` and `User-Agent` headers are set. - pub fn no_default_headers(mut self) -> Self { - self.default_headers = false; - self - } - /// Disable automatic decompress of response's body pub fn no_decompress(mut self) -> Self { self.response_decompress = false; @@ -354,26 +356,28 @@ impl ClientRequest { /// This method calls provided closure with builder reference if /// value is `true`. - pub fn if_true(mut self, value: bool, f: F) -> Self + pub fn if_true(self, value: bool, f: F) -> Self where - F: FnOnce(&mut ClientRequest), + F: FnOnce(ClientRequest) -> ClientRequest, { if value { - f(&mut self); + f(self) + } else { + self } - self } /// This method calls provided closure with builder reference if /// value is `Some`. - pub fn if_some(mut self, value: Option, f: F) -> Self + pub fn if_some(self, value: Option, f: F) -> Self where - F: FnOnce(T, &mut ClientRequest), + F: FnOnce(T, ClientRequest) -> ClientRequest, { if let Some(val) = value { - f(val, &mut self); + f(val, self) + } else { + self } - self } /// Complete request construction and send body. @@ -406,36 +410,6 @@ impl ClientRequest { return Either::A(err(InvalidUrl::UnknownScheme.into())); } - // set default headers - if self.default_headers { - // set request host header - if let Some(host) = self.head.uri.host() { - if !self.head.headers.contains_key(header::HOST) { - let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); - - let _ = match self.head.uri.port_u16() { - None | Some(80) | Some(443) => write!(wrt, "{}", host), - Some(port) => write!(wrt, "{}:{}", host, port), - }; - - match wrt.get_mut().take().freeze().try_into() { - Ok(value) => { - self.head.headers.insert(header::HOST, value); - } - Err(e) => return Either::A(err(HttpError::from(e).into())), - } - } - } - - // user agent - if !self.head.headers.contains_key(&header::USER_AGENT) { - self.head.headers.insert( - header::USER_AGENT, - HeaderValue::from_static(concat!("awc/", env!("CARGO_PKG_VERSION"))), - ); - } - } - // set cookies if let Some(ref mut jar) = self.cookies { let mut cookie = String::new(); @@ -450,7 +424,7 @@ impl ClientRequest { ); } - let slf = self; + let mut slf = self; // enable br only for https #[cfg(any( @@ -458,25 +432,26 @@ impl ClientRequest { feature = "flate2-zlib", feature = "flate2-rust" ))] - let slf = { - let https = slf - .head - .uri - .scheme_part() - .map(|s| s == &uri::Scheme::HTTPS) - .unwrap_or(true); + { + if slf.response_decompress { + let https = slf + .head + .uri + .scheme_part() + .map(|s| s == &uri::Scheme::HTTPS) + .unwrap_or(true); - if https { - slf.set_header_if_none(header::ACCEPT_ENCODING, HTTPS_ENCODING) - } else { - #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] - { - slf.set_header_if_none(header::ACCEPT_ENCODING, "gzip, deflate") - } - #[cfg(not(any(feature = "flate2-zlib", feature = "flate2-rust")))] - slf + if https { + slf = slf.set_header_if_none(header::ACCEPT_ENCODING, HTTPS_ENCODING) + } else { + #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] + { + slf = slf + .set_header_if_none(header::ACCEPT_ENCODING, "gzip, deflate") + } + }; } - }; + } let head = slf.head; let config = slf.config.as_ref(); @@ -485,7 +460,7 @@ impl ClientRequest { let fut = config .connector .borrow_mut() - .send_request(head, body.into()) + .send_request(head, body.into(), slf.addr) .map(move |res| { res.map_body(|head, payload| { if response_decompress { @@ -596,102 +571,121 @@ impl fmt::Debug for ClientRequest { #[cfg(test)] mod tests { + use std::time::SystemTime; + use super::*; - use crate::{test, Client}; + use crate::Client; #[test] fn test_debug() { - test::run_on(|| { - let request = Client::new().get("/").header("x-test", "111"); - let repr = format!("{:?}", request); - assert!(repr.contains("ClientRequest")); - assert!(repr.contains("x-test")); - }) + let request = Client::new().get("/").header("x-test", "111"); + let repr = format!("{:?}", request); + assert!(repr.contains("ClientRequest")); + assert!(repr.contains("x-test")); + } + + #[test] + fn test_basics() { + let mut req = Client::new() + .put("/") + .version(Version::HTTP_2) + .set(header::Date(SystemTime::now().into())) + .content_type("plain/text") + .if_true(true, |req| req.header(header::SERVER, "awc")) + .if_true(false, |req| req.header(header::EXPECT, "awc")) + .if_some(Some("server"), |val, req| { + req.header(header::USER_AGENT, val) + }) + .if_some(Option::<&str>::None, |_, req| { + req.header(header::ALLOW, "1") + }) + .content_length(100); + assert!(req.headers().contains_key(header::CONTENT_TYPE)); + assert!(req.headers().contains_key(header::DATE)); + assert!(req.headers().contains_key(header::SERVER)); + assert!(req.headers().contains_key(header::USER_AGENT)); + assert!(!req.headers().contains_key(header::ALLOW)); + assert!(!req.headers().contains_key(header::EXPECT)); + assert_eq!(req.head.version, Version::HTTP_2); + let _ = req.headers_mut(); + let _ = req.send_body(""); } #[test] fn test_client_header() { - test::run_on(|| { - let req = Client::build() - .header(header::CONTENT_TYPE, "111") - .finish() - .get("/"); + let req = Client::build() + .header(header::CONTENT_TYPE, "111") + .finish() + .get("/"); - assert_eq!( - req.head - .headers - .get(header::CONTENT_TYPE) - .unwrap() - .to_str() - .unwrap(), - "111" - ); - }) + assert_eq!( + req.head + .headers + .get(header::CONTENT_TYPE) + .unwrap() + .to_str() + .unwrap(), + "111" + ); } #[test] fn test_client_header_override() { - test::run_on(|| { - let req = Client::build() - .header(header::CONTENT_TYPE, "111") - .finish() - .get("/") - .set_header(header::CONTENT_TYPE, "222"); + let req = Client::build() + .header(header::CONTENT_TYPE, "111") + .finish() + .get("/") + .set_header(header::CONTENT_TYPE, "222"); - assert_eq!( - req.head - .headers - .get(header::CONTENT_TYPE) - .unwrap() - .to_str() - .unwrap(), - "222" - ); - }) + assert_eq!( + req.head + .headers + .get(header::CONTENT_TYPE) + .unwrap() + .to_str() + .unwrap(), + "222" + ); } #[test] fn client_basic_auth() { - test::run_on(|| { - let req = Client::new() - .get("/") - .basic_auth("username", Some("password")); - assert_eq!( - req.head - .headers - .get(header::AUTHORIZATION) - .unwrap() - .to_str() - .unwrap(), - "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" - ); + let req = Client::new() + .get("/") + .basic_auth("username", Some("password")); + assert_eq!( + req.head + .headers + .get(header::AUTHORIZATION) + .unwrap() + .to_str() + .unwrap(), + "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" + ); - let req = Client::new().get("/").basic_auth("username", None); - assert_eq!( - req.head - .headers - .get(header::AUTHORIZATION) - .unwrap() - .to_str() - .unwrap(), - "Basic dXNlcm5hbWU=" - ); - }); + let req = Client::new().get("/").basic_auth("username", None); + assert_eq!( + req.head + .headers + .get(header::AUTHORIZATION) + .unwrap() + .to_str() + .unwrap(), + "Basic dXNlcm5hbWU=" + ); } #[test] fn client_bearer_auth() { - test::run_on(|| { - let req = Client::new().get("/").bearer_auth("someS3cr3tAutht0k3n"); - assert_eq!( - req.head - .headers - .get(header::AUTHORIZATION) - .unwrap() - .to_str() - .unwrap(), - "Bearer someS3cr3tAutht0k3n" - ); - }) + let req = Client::new().get("/").bearer_auth("someS3cr3tAutht0k3n"); + assert_eq!( + req.head + .headers + .get(header::AUTHORIZATION) + .unwrap() + .to_str() + .unwrap(), + "Bearer someS3cr3tAutht0k3n" + ); } } diff --git a/awc/src/response.rs b/awc/src/response.rs index 73194d673..b9b007b87 100644 --- a/awc/src/response.rs +++ b/awc/src/response.rs @@ -46,7 +46,7 @@ impl HttpMessage for ClientResponse { if self.extensions().get::().is_none() { let mut cookies = Vec::new(); - for hdr in self.headers().get_all(SET_COOKIE) { + for hdr in self.headers().get_all(&SET_COOKIE) { let s = std::str::from_utf8(hdr.as_bytes()) .map_err(CookieParseError::from)?; cookies.push(Cookie::parse_encoded(s)?.into_owned()); @@ -160,7 +160,7 @@ where /// Create `MessageBody` for request. pub fn new(res: &mut ClientResponse) -> MessageBody { let mut len = None; - if let Some(l) = res.headers().get(CONTENT_LENGTH) { + if let Some(l) = res.headers().get(&CONTENT_LENGTH) { if let Ok(s) = l.to_str() { if let Ok(l) = s.parse::() { len = Some(l) @@ -254,7 +254,7 @@ where } let mut len = None; - if let Some(l) = req.headers().get(CONTENT_LENGTH) { + if let Some(l) = req.headers().get(&CONTENT_LENGTH) { if let Ok(s) = l.to_str() { if let Ok(l) = s.parse::() { len = Some(l) @@ -282,7 +282,7 @@ where impl Future for JsonBody where T: Stream, - U: DeserializeOwned + 'static, + U: DeserializeOwned, { type Item = U; type Error = JsonPayloadError; diff --git a/awc/src/test.rs b/awc/src/test.rs index 1c772905e..8df21e8f9 100644 --- a/awc/src/test.rs +++ b/awc/src/test.rs @@ -3,7 +3,7 @@ use std::fmt::Write as FmtWrite; use actix_http::cookie::{Cookie, CookieJar}; use actix_http::http::header::{self, Header, HeaderValue, IntoHeaderValue}; -use actix_http::http::{HeaderName, HttpTryFrom, Version}; +use actix_http::http::{HeaderName, HttpTryFrom, StatusCode, Version}; use actix_http::{h1, Payload, ResponseHead}; use bytes::Bytes; #[cfg(test)] @@ -49,7 +49,7 @@ pub struct TestResponse { impl Default for TestResponse { fn default() -> TestResponse { TestResponse { - head: ResponseHead::default(), + head: ResponseHead::new(StatusCode::OK), cookies: CookieJar::new(), payload: None, } @@ -134,3 +134,23 @@ impl TestResponse { } } } + +#[cfg(test)] +mod tests { + use std::time::SystemTime; + + use super::*; + use crate::{cookie, http::header}; + + #[test] + fn test_basics() { + let res = TestResponse::default() + .version(Version::HTTP_2) + .set(header::Date(SystemTime::now().into())) + .cookie(cookie::Cookie::build("name", "value").finish()) + .finish(); + assert!(res.headers().contains_key(header::SET_COOKIE)); + assert!(res.headers().contains_key(header::DATE)); + assert_eq!(res.version(), Version::HTTP_2); + } +} diff --git a/awc/src/ws.rs b/awc/src/ws.rs index bbeaa061b..94a90535b 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -1,13 +1,12 @@ //! Websockets client use std::fmt::Write as FmtWrite; -use std::io::Write; +use std::net::SocketAddr; use std::rc::Rc; use std::{fmt, str}; use actix_codec::Framed; use actix_http::cookie::{Cookie, CookieJar}; use actix_http::{ws, Payload, RequestHead}; -use bytes::{BufMut, BytesMut}; use futures::future::{err, Either, Future}; use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use tokio_timer::Timeout; @@ -31,9 +30,9 @@ pub struct WebsocketsRequest { err: Option, origin: Option, protocols: Option, + addr: Option, max_size: usize, server_mode: bool, - default_headers: bool, cookies: Option, config: Rc, } @@ -58,19 +57,28 @@ impl WebsocketsRequest { head, err, config, + addr: None, origin: None, protocols: None, max_size: 65_536, server_mode: false, cookies: None, - default_headers: true, } } + /// Set socket address of the server. + /// + /// This address is used for connection. If address is not + /// provided url's host name get resolved. + pub fn address(mut self, addr: SocketAddr) -> Self { + self.addr = Some(addr); + self + } + /// Set supported websocket protocols pub fn protocols(mut self, protos: U) -> Self where - U: IntoIterator + 'static, + U: IntoIterator, V: AsRef, { let mut protos = protos @@ -119,13 +127,6 @@ impl WebsocketsRequest { self } - /// Do not add default request headers. - /// By default `Date` and `User-Agent` headers are set. - pub fn no_default_headers(mut self) -> Self { - self.default_headers = false; - self - } - /// Append a header. /// /// Header gets appended to existing header. @@ -188,10 +189,9 @@ impl WebsocketsRequest { } /// Set HTTP basic authorization header - pub fn basic_auth(self, username: U, password: Option

) -> Self + pub fn basic_auth(self, username: U, password: Option<&str>) -> Self where U: fmt::Display, - P: fmt::Display, { let auth = match password { Some(password) => format!("{}:{}", username, password), @@ -232,67 +232,36 @@ impl WebsocketsRequest { return Either::A(err(InvalidUrl::UnknownScheme.into())); } - // set default headers - let mut slf = if self.default_headers { - // set request host header - if let Some(host) = self.head.uri.host() { - if !self.head.headers.contains_key(header::HOST) { - let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); - - let _ = match self.head.uri.port_u16() { - None | Some(80) | Some(443) => write!(wrt, "{}", host), - Some(port) => write!(wrt, "{}:{}", host, port), - }; - - match wrt.get_mut().take().freeze().try_into() { - Ok(value) => { - self.head.headers.insert(header::HOST, value); - } - Err(e) => return Either::A(err(HttpError::from(e).into())), - } - } - } - - // user agent - self.set_header_if_none( - header::USER_AGENT, - concat!("awc/", env!("CARGO_PKG_VERSION")), - ) - } else { - self - }; - - let mut head = slf.head; - // set cookies - if let Some(ref mut jar) = slf.cookies { + if let Some(ref mut jar) = self.cookies { let mut cookie = String::new(); for c in jar.delta() { let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); let _ = write!(&mut cookie, "; {}={}", name, value); } - head.headers.insert( + self.head.headers.insert( header::COOKIE, HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), ); } // origin - if let Some(origin) = slf.origin.take() { - head.headers.insert(header::ORIGIN, origin); + if let Some(origin) = self.origin.take() { + self.head.headers.insert(header::ORIGIN, origin); } - head.set_connection_type(ConnectionType::Upgrade); - head.headers + self.head.set_connection_type(ConnectionType::Upgrade); + self.head + .headers .insert(header::UPGRADE, HeaderValue::from_static("websocket")); - head.headers.insert( + self.head.headers.insert( header::SEC_WEBSOCKET_VERSION, HeaderValue::from_static("13"), ); - if let Some(protocols) = slf.protocols.take() { - head.headers.insert( + if let Some(protocols) = self.protocols.take() { + self.head.headers.insert( header::SEC_WEBSOCKET_PROTOCOL, HeaderValue::try_from(protocols.as_str()).unwrap(), ); @@ -304,19 +273,20 @@ impl WebsocketsRequest { let sec_key: [u8; 16] = rand::random(); let key = base64::encode(&sec_key); - head.headers.insert( + self.head.headers.insert( header::SEC_WEBSOCKET_KEY, HeaderValue::try_from(key.as_str()).unwrap(), ); - let max_size = slf.max_size; - let server_mode = slf.server_mode; + let head = self.head; + let max_size = self.max_size; + let server_mode = self.server_mode; - let fut = slf + let fut = self .config .connector .borrow_mut() - .open_tunnel(head) + .open_tunnel(head, self.addr) .from_err() .and_then(move |(head, framed)| { // verify response @@ -324,7 +294,7 @@ impl WebsocketsRequest { return Err(WsClientError::InvalidResponseStatus(head.status)); } // Check for "UPGRADE" to websocket header - let has_hdr = if let Some(hdr) = head.headers.get(header::UPGRADE) { + let has_hdr = if let Some(hdr) = head.headers.get(&header::UPGRADE) { if let Ok(s) = hdr.to_str() { s.to_ascii_lowercase().contains("websocket") } else { @@ -338,7 +308,7 @@ impl WebsocketsRequest { return Err(WsClientError::InvalidUpgradeHeader); } // Check for "CONNECTION" header - if let Some(conn) = head.headers.get(header::CONNECTION) { + if let Some(conn) = head.headers.get(&header::CONNECTION) { if let Ok(s) = conn.to_str() { if !s.to_ascii_lowercase().contains("upgrade") { log::trace!("Invalid connection header: {}", s); @@ -355,7 +325,7 @@ impl WebsocketsRequest { return Err(WsClientError::MissingConnectionHeader); } - if let Some(hdr_key) = head.headers.get(header::SEC_WEBSOCKET_ACCEPT) { + if let Some(hdr_key) = head.headers.get(&header::SEC_WEBSOCKET_ACCEPT) { let encoded = ws::hash_key(key.as_ref()); if hdr_key.as_bytes() != encoded.as_bytes() { log::trace!( @@ -387,7 +357,7 @@ impl WebsocketsRequest { }); // set request timeout - if let Some(timeout) = slf.config.timeout { + if let Some(timeout) = self.config.timeout { Either::B(Either::A(Timeout::new(fut, timeout).map_err(|e| { if let Some(e) = e.into_inner() { e @@ -400,3 +370,128 @@ impl WebsocketsRequest { } } } + +impl fmt::Debug for WebsocketsRequest { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!( + f, + "\nWebsocketsRequest {}:{}", + self.head.method, self.head.uri + )?; + writeln!(f, " headers:")?; + for (key, val) in self.head.headers.iter() { + writeln!(f, " {:?}: {:?}", key, val)?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Client; + + #[test] + fn test_debug() { + let request = Client::new().ws("/").header("x-test", "111"); + let repr = format!("{:?}", request); + assert!(repr.contains("WebsocketsRequest")); + assert!(repr.contains("x-test")); + } + + #[test] + fn test_header_override() { + let req = Client::build() + .header(header::CONTENT_TYPE, "111") + .finish() + .ws("/") + .set_header(header::CONTENT_TYPE, "222"); + + assert_eq!( + req.head + .headers + .get(header::CONTENT_TYPE) + .unwrap() + .to_str() + .unwrap(), + "222" + ); + } + + #[test] + fn basic_auth() { + let req = Client::new() + .ws("/") + .basic_auth("username", Some("password")); + assert_eq!( + req.head + .headers + .get(header::AUTHORIZATION) + .unwrap() + .to_str() + .unwrap(), + "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" + ); + + let req = Client::new().ws("/").basic_auth("username", None); + assert_eq!( + req.head + .headers + .get(header::AUTHORIZATION) + .unwrap() + .to_str() + .unwrap(), + "Basic dXNlcm5hbWU=" + ); + } + + #[test] + fn bearer_auth() { + let req = Client::new().ws("/").bearer_auth("someS3cr3tAutht0k3n"); + assert_eq!( + req.head + .headers + .get(header::AUTHORIZATION) + .unwrap() + .to_str() + .unwrap(), + "Bearer someS3cr3tAutht0k3n" + ); + let _ = req.connect(); + } + + #[test] + fn basics() { + actix_http_test::run_on(|| { + let req = Client::new() + .ws("http://localhost/") + .origin("test-origin") + .max_frame_size(100) + .server_mode() + .protocols(&["v1", "v2"]) + .set_header_if_none(header::CONTENT_TYPE, "json") + .set_header_if_none(header::CONTENT_TYPE, "text") + .cookie(Cookie::build("cookie1", "value1").finish()); + assert_eq!( + req.origin.as_ref().unwrap().to_str().unwrap(), + "test-origin" + ); + assert_eq!(req.max_size, 100); + assert_eq!(req.server_mode, true); + assert_eq!(req.protocols, Some("v1,v2".to_string())); + assert_eq!( + req.head.headers.get(header::CONTENT_TYPE).unwrap(), + header::HeaderValue::from_static("json") + ); + let _ = req.connect(); + }); + + assert!(Client::new().ws("/").connect().poll().is_err()); + assert!(Client::new().ws("http:///test").connect().poll().is_err()); + assert!(Client::new() + .ws("hmm://test.com/") + .connect() + .poll() + .is_err()); + } +} diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index a2882708a..7e2dc6ba4 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -1,15 +1,23 @@ -use std::io::Write; +use std::collections::HashMap; +use std::io::{Read, Write}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; use std::time::Duration; use brotli2::write::BrotliEncoder; use bytes::Bytes; +use flate2::read::GzDecoder; use flate2::write::GzEncoder; use flate2::Compression; -use futures::future::Future; +use futures::Future; use rand::Rng; +use actix_codec::{AsyncRead, AsyncWrite}; use actix_http::HttpService; use actix_http_test::TestServer; +use actix_service::{fn_service, NewService}; +use actix_web::http::{Cookie, Version}; +use actix_web::middleware::{BodyEncoding, Compress}; use actix_web::{http::header, web, App, Error, HttpMessage, HttpRequest, HttpResponse}; use awc::error::SendRequestError; @@ -35,6 +43,30 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World"; +#[cfg(feature = "ssl")] +fn ssl_acceptor( +) -> std::io::Result> { + use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; + // load ssl keys + let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); + builder + .set_private_key_file("../tests/key.pem", SslFiletype::PEM) + .unwrap(); + builder + .set_certificate_chain_file("../tests/cert.pem") + .unwrap(); + builder.set_alpn_select_callback(|_, protos| { + const H2: &[u8] = b"\x02h2"; + if protos.windows(3).any(|window| window == H2) { + Ok(b"h2") + } else { + Err(openssl::ssl::AlpnError::NOACK) + } + }); + builder.set_alpn_protos(b"\x02h2")?; + Ok(actix_server::ssl::OpensslAcceptor::new(builder.build())) +} + #[test] fn test_simple() { let mut srv = @@ -60,6 +92,38 @@ fn test_simple() { assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } +#[test] +fn test_json() { + let mut srv = TestServer::new(|| { + HttpService::new(App::new().service( + web::resource("/").route(web::to(|_: web::Json| HttpResponse::Ok())), + )) + }); + + let request = srv + .get("/") + .header("x-test", "111") + .send_json(&"TEST".to_string()); + let response = srv.block_on(request).unwrap(); + assert!(response.status().is_success()); +} + +#[test] +fn test_form() { + let mut srv = TestServer::new(|| { + HttpService::new(App::new().service(web::resource("/").route(web::to( + |_: web::Form>| HttpResponse::Ok(), + )))) + }); + + let mut data = HashMap::new(); + let _ = data.insert("key".to_string(), "TEST".to_string()); + + let request = srv.get("/").header("x-test", "111").send_form(&data); + let response = srv.block_on(request).unwrap(); + assert!(response.status().is_success()); +} + #[test] fn test_timeout() { let mut srv = TestServer::new(|| { @@ -94,11 +158,9 @@ fn test_timeout_override() { )))) }); - let client = srv.execute(|| { - awc::Client::build() - .timeout(Duration::from_millis(50000)) - .finish() - }); + let client = awc::Client::build() + .timeout(Duration::from_millis(50000)) + .finish(); let request = client .get(srv.url("/")) .timeout(Duration::from_millis(50)) @@ -109,58 +171,309 @@ fn test_timeout_override() { } } -// #[test] -// fn test_connection_close() { -// let mut srv = -// test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); +#[test] +fn test_connection_reuse() { + let num = Arc::new(AtomicUsize::new(0)); + let num2 = num.clone(); -// let request = srv.get("/").header("Connection", "close").finish().unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert!(response.status().is_success()); -// } + let mut srv = TestServer::new(move || { + let num2 = num2.clone(); + fn_service(move |io| { + num2.fetch_add(1, Ordering::Relaxed); + Ok(io) + }) + .and_then(HttpService::new( + App::new().service(web::resource("/").route(web::to(|| HttpResponse::Ok()))), + )) + }); -// #[test] -// fn test_with_query_parameter() { -// let mut srv = test::TestServer::new(|app| { -// app.handler(|req: &HttpRequest| match req.query().get("qp") { -// Some(_) => HttpResponse::Ok().finish(), -// None => HttpResponse::BadRequest().finish(), -// }) -// }); + let client = awc::Client::default(); -// let request = srv.get("/").uri(srv.url("/?qp=5").as_str()).finish().unwrap(); + // req 1 + let request = client.get(srv.url("/")).send(); + let response = srv.block_on(request).unwrap(); + assert!(response.status().is_success()); -// let response = srv.execute(request.send()).unwrap(); -// assert!(response.status().is_success()); -// } + // req 2 + let req = client.post(srv.url("/")); + let response = srv.block_on_fn(move || req.send()).unwrap(); + assert!(response.status().is_success()); -// #[test] -// fn test_no_decompress() { -// let mut srv = -// test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); + // one connection + assert_eq!(num.load(Ordering::Relaxed), 1); +} -// let request = srv.get("/").disable_decompress().finish().unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert!(response.status().is_success()); +#[cfg(feature = "ssl")] +#[test] +fn test_connection_reuse_h2() { + let openssl = ssl_acceptor().unwrap(); + let num = Arc::new(AtomicUsize::new(0)); + let num2 = num.clone(); -// // read response -// let bytes = srv.execute(response.body()).unwrap(); + let mut srv = TestServer::new(move || { + let num2 = num2.clone(); + fn_service(move |io| { + num2.fetch_add(1, Ordering::Relaxed); + Ok(io) + }) + .and_then( + openssl + .clone() + .map_err(|e| println!("Openssl error: {}", e)), + ) + .and_then( + HttpService::build() + .h2(App::new() + .service(web::resource("/").route(web::to(|| HttpResponse::Ok())))) + .map_err(|_| ()), + ) + }); -// let mut e = GzDecoder::new(&bytes[..]); -// let mut dec = Vec::new(); -// e.read_to_end(&mut dec).unwrap(); -// assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + // disable ssl verification + use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; -// // POST -// let request = srv.post().disable_decompress().finish().unwrap(); -// let response = srv.execute(request.send()).unwrap(); + let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); + builder.set_verify(SslVerifyMode::NONE); + let _ = builder + .set_alpn_protos(b"\x02h2\x08http/1.1") + .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); -// let bytes = srv.execute(response.body()).unwrap(); -// let mut e = GzDecoder::new(&bytes[..]); -// let mut dec = Vec::new(); -// e.read_to_end(&mut dec).unwrap(); -// assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); -// } + let client = awc::Client::build() + .connector(awc::Connector::new().ssl(builder.build()).finish()) + .finish(); + + // req 1 + let request = client.get(srv.surl("/")).send(); + let response = srv.block_on(request).unwrap(); + assert!(response.status().is_success()); + + // req 2 + let req = client.post(srv.surl("/")); + let response = srv.block_on_fn(move || req.send()).unwrap(); + assert!(response.status().is_success()); + assert_eq!(response.version(), Version::HTTP_2); + + // one connection + assert_eq!(num.load(Ordering::Relaxed), 1); +} + +#[test] +fn test_connection_force_close() { + let num = Arc::new(AtomicUsize::new(0)); + let num2 = num.clone(); + + let mut srv = TestServer::new(move || { + let num2 = num2.clone(); + fn_service(move |io| { + num2.fetch_add(1, Ordering::Relaxed); + Ok(io) + }) + .and_then(HttpService::new( + App::new().service(web::resource("/").route(web::to(|| HttpResponse::Ok()))), + )) + }); + + let client = awc::Client::default(); + + // req 1 + let request = client.get(srv.url("/")).force_close().send(); + let response = srv.block_on(request).unwrap(); + assert!(response.status().is_success()); + + // req 2 + let req = client.post(srv.url("/")).force_close(); + let response = srv.block_on_fn(move || req.send()).unwrap(); + assert!(response.status().is_success()); + + // two connection + assert_eq!(num.load(Ordering::Relaxed), 2); +} + +#[test] +fn test_connection_server_close() { + let num = Arc::new(AtomicUsize::new(0)); + let num2 = num.clone(); + + let mut srv = TestServer::new(move || { + let num2 = num2.clone(); + fn_service(move |io| { + num2.fetch_add(1, Ordering::Relaxed); + Ok(io) + }) + .and_then(HttpService::new( + App::new().service( + web::resource("/") + .route(web::to(|| HttpResponse::Ok().force_close().finish())), + ), + )) + }); + + let client = awc::Client::default(); + + // req 1 + let request = client.get(srv.url("/")).send(); + let response = srv.block_on(request).unwrap(); + assert!(response.status().is_success()); + + // req 2 + let req = client.post(srv.url("/")); + let response = srv.block_on_fn(move || req.send()).unwrap(); + assert!(response.status().is_success()); + + // two connection + assert_eq!(num.load(Ordering::Relaxed), 2); +} + +#[test] +fn test_connection_wait_queue() { + let num = Arc::new(AtomicUsize::new(0)); + let num2 = num.clone(); + + let mut srv = TestServer::new(move || { + let num2 = num2.clone(); + fn_service(move |io| { + num2.fetch_add(1, Ordering::Relaxed); + Ok(io) + }) + .and_then(HttpService::new(App::new().service( + web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR))), + ))) + }); + + let client = awc::Client::build() + .connector(awc::Connector::new().limit(1).finish()) + .finish(); + + // req 1 + let request = client.get(srv.url("/")).send(); + let mut response = srv.block_on(request).unwrap(); + assert!(response.status().is_success()); + + // req 2 + let req2 = client.post(srv.url("/")); + let req2_fut = srv.execute(move || { + let mut fut = req2.send(); + assert!(fut.poll().unwrap().is_not_ready()); + fut + }); + + // read response 1 + let bytes = srv.block_on(response.body()).unwrap(); + assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + // req 2 + let response = srv.block_on(req2_fut).unwrap(); + assert!(response.status().is_success()); + + // two connection + assert_eq!(num.load(Ordering::Relaxed), 1); +} + +#[test] +fn test_connection_wait_queue_force_close() { + let num = Arc::new(AtomicUsize::new(0)); + let num2 = num.clone(); + + let mut srv = TestServer::new(move || { + let num2 = num2.clone(); + fn_service(move |io| { + num2.fetch_add(1, Ordering::Relaxed); + Ok(io) + }) + .and_then(HttpService::new( + App::new().service( + web::resource("/") + .route(web::to(|| HttpResponse::Ok().force_close().body(STR))), + ), + )) + }); + + let client = awc::Client::build() + .connector(awc::Connector::new().limit(1).finish()) + .finish(); + + // req 1 + let request = client.get(srv.url("/")).send(); + let mut response = srv.block_on(request).unwrap(); + assert!(response.status().is_success()); + + // req 2 + let req2 = client.post(srv.url("/")); + let req2_fut = srv.execute(move || { + let mut fut = req2.send(); + assert!(fut.poll().unwrap().is_not_ready()); + fut + }); + + // read response 1 + let bytes = srv.block_on(response.body()).unwrap(); + assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + // req 2 + let response = srv.block_on(req2_fut).unwrap(); + assert!(response.status().is_success()); + + // two connection + assert_eq!(num.load(Ordering::Relaxed), 2); +} + +#[test] +fn test_with_query_parameter() { + let mut srv = TestServer::new(|| { + HttpService::new(App::new().service(web::resource("/").to( + |req: HttpRequest| { + if req.query_string().contains("qp") { + HttpResponse::Ok() + } else { + HttpResponse::BadRequest() + } + }, + ))) + }); + + let res = srv + .block_on(awc::Client::new().get(srv.url("/?qp=5")).send()) + .unwrap(); + assert!(res.status().is_success()); +} + +#[test] +fn test_no_decompress() { + let mut srv = TestServer::new(|| { + HttpService::new(App::new().wrap(Compress::default()).service( + web::resource("/").route(web::to(|| { + let mut res = HttpResponse::Ok().body(STR); + res.encoding(header::ContentEncoding::Gzip); + res + })), + )) + }); + + let mut res = srv + .block_on(awc::Client::new().get(srv.url("/")).no_decompress().send()) + .unwrap(); + assert!(res.status().is_success()); + + // read response + let bytes = srv.block_on(res.body()).unwrap(); + + let mut e = GzDecoder::new(&bytes[..]); + let mut dec = Vec::new(); + e.read_to_end(&mut dec).unwrap(); + assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + // POST + let mut res = srv + .block_on(awc::Client::new().post(srv.url("/")).no_decompress().send()) + .unwrap(); + assert!(res.status().is_success()); + + let bytes = srv.block_on(res.body()).unwrap(); + let mut e = GzDecoder::new(&bytes[..]); + let mut dec = Vec::new(); + e.read_to_end(&mut dec).unwrap(); + assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); +} #[test] fn test_client_gzip_encoding() { @@ -406,7 +719,6 @@ fn test_client_brotli_encoding() { #[test] fn test_client_cookie_handling() { - use actix_web::http::Cookie; fn err() -> Error { use std::io::{Error as IoError, ErrorKind}; // stub some generic error @@ -468,36 +780,6 @@ fn test_client_cookie_handling() { assert_eq!(c2, cookie2); } -// #[test] -// fn test_default_headers() { -// let srv = test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); - -// let request = srv.get("/").finish().unwrap(); -// let repr = format!("{:?}", request); -// assert!(repr.contains("\"accept-encoding\": \"gzip, deflate\"")); -// assert!(repr.contains(concat!( -// "\"user-agent\": \"actix-web/", -// env!("CARGO_PKG_VERSION"), -// "\"" -// ))); - -// let request_override = srv -// .get("/") -// .header("User-Agent", "test") -// .header("Accept-Encoding", "over_test") -// .finish() -// .unwrap(); -// let repr_override = format!("{:?}", request_override); -// assert!(repr_override.contains("\"user-agent\": \"test\"")); -// assert!(repr_override.contains("\"accept-encoding\": \"over_test\"")); -// assert!(!repr_override.contains("\"accept-encoding\": \"gzip, deflate\"")); -// assert!(!repr_override.contains(concat!( -// "\"user-agent\": \"Actix-web/", -// env!("CARGO_PKG_VERSION"), -// "\"" -// ))); -// } - // #[test] // fn client_read_until_eof() { // let addr = test::TestServer::unused_addr(); diff --git a/awc/tests/test_ws.rs b/awc/tests/test_ws.rs index d8942fb17..4fc9f4bdc 100644 --- a/awc/tests/test_ws.rs +++ b/awc/tests/test_ws.rs @@ -11,7 +11,7 @@ use futures::future::{ok, Either}; use futures::{Future, Sink, Stream}; use tokio_tcp::TcpStream; -use actix_http::{h1, ws, ResponseError, SendResponse, ServiceConfig}; +use actix_http::{body::BodySize, h1, ws, Request, ResponseError, ServiceConfig}; fn ws_service(req: ws::Frame) -> impl Future { match req { @@ -40,39 +40,49 @@ fn test_simple() { fn_service(|io: Io| Ok(io.into_parts().0)) .and_then(IntoFramed::new(|| h1::Codec::new(ServiceConfig::default()))) .and_then(TakeItem::new().map_err(|_| ())) - .and_then(|(req, framed): (_, Framed<_, _>)| { - // validate request - if let Some(h1::Message::Item(req)) = req { - match ws::verify_handshake(&req) { - Err(e) => { - // validation failed - Either::A( - SendResponse::send(framed, e.error_response()) - .map_err(|_| ()) - .map(|_| ()), - ) - } - Ok(_) => { - Either::B( - // send handshake response - SendResponse::send( - framed, - ws::handshake_response(&req).finish(), - ) - .map_err(|_| ()) - .and_then(|framed| { - // start websocket service - let framed = framed.into_framed(ws::Codec::new()); - ws::Transport::with(framed, ws_service) + .and_then( + |(req, framed): (Option>, Framed<_, _>)| { + // validate request + if let Some(h1::Message::Item(req)) = req { + match ws::verify_handshake(req.head()) { + Err(e) => { + // validation failed + let res = e.error_response(); + Either::A( + framed + .send(h1::Message::Item(( + res.drop_body(), + BodySize::Empty, + ))) .map_err(|_| ()) - }), - ) + .map(|_| ()), + ) + } + Ok(_) => { + let res = ws::handshake_response(req.head()).finish(); + Either::B( + // send handshake response + framed + .send(h1::Message::Item(( + res.drop_body(), + BodySize::None, + ))) + .map_err(|_| ()) + .and_then(|framed| { + // start websocket service + let framed = + framed.into_framed(ws::Codec::new()); + ws::Transport::with(framed, ws_service) + .map_err(|_| ()) + }), + ) + } } + } else { + panic!() } - } else { - panic!() - } - }) + }, + ) }); // client service diff --git a/examples/basic.rs b/examples/basic.rs index 1191b371c..46440d706 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -27,7 +27,7 @@ fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .wrap(middleware::DefaultHeaders::new().header("X-Version", "0.2")) - .wrap(middleware::encoding::Compress::default()) + .wrap(middleware::Compress::default()) .wrap(middleware::Logger::default()) .service(index) .service(no_params) @@ -36,9 +36,9 @@ fn main() -> std::io::Result<()> { .wrap( middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"), ) - .default_resource(|r| { - r.route(web::route().to(|| HttpResponse::MethodNotAllowed())) - }) + .default_service( + web::route().to(|| HttpResponse::MethodNotAllowed()), + ) .route(web::get().to_async(index_async)), ) .service(web::resource("/test1.html").to(|| "Test\r\n")) diff --git a/examples/client.rs b/examples/client.rs index c4df6f7d6..8a75fd306 100644 --- a/examples/client.rs +++ b/examples/client.rs @@ -1,7 +1,6 @@ use actix_http::Error; use actix_rt::System; -use bytes::BytesMut; -use futures::{future::lazy, Future, Stream}; +use futures::{future::lazy, Future}; fn main() -> Result<(), Error> { std::env::set_var("RUST_LOG", "actix_http=trace"); @@ -13,17 +12,14 @@ fn main() -> Result<(), Error> { .header("User-Agent", "Actix-web") .send() // <- Send http request .from_err() - .and_then(|response| { + .and_then(|mut response| { // <- server http response println!("Response: {:?}", response); // read response body response + .body() .from_err() - .fold(BytesMut::new(), move |mut acc, chunk| { - acc.extend_from_slice(&chunk); - Ok::<_, Error>(acc) - }) .map(|body| println!("Downloaded: {:?} bytes", body.len())) }) })) diff --git a/src/app.rs b/src/app.rs index fd91d0728..bb6d2aef8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,24 +1,21 @@ use std::cell::RefCell; +use std::fmt; use std::marker::PhantomData; use std::rc::Rc; use actix_http::body::{Body, MessageBody}; -#[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] -use actix_http::encoding::{Decoder, Encoder}; use actix_server_config::ServerConfig; use actix_service::boxed::{self, BoxedNewService}; use actix_service::{ apply_transform, IntoNewService, IntoTransform, NewService, Transform, }; -#[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] -use bytes::Bytes; -use futures::{IntoFuture, Stream}; +use futures::IntoFuture; -use crate::app_service::{AppChain, AppEntry, AppInit, AppRouting, AppRoutingFactory}; -use crate::config::{AppConfig, AppConfigInner}; +use crate::app_service::{AppEntry, AppInit, AppRoutingFactory}; +use crate::config::{AppConfig, AppConfigInner, ServiceConfig}; use crate::data::{Data, DataFactory}; -use crate::dev::{Payload, PayloadStream, ResourceDef}; -use crate::error::{Error, PayloadError}; +use crate::dev::ResourceDef; +use crate::error::Error; use crate::resource::Resource; use crate::route::Route; use crate::service::{ @@ -26,45 +23,49 @@ use crate::service::{ ServiceResponse, }; -type HttpNewService

= - BoxedNewService<(), ServiceRequest

, ServiceResponse, Error, ()>; +type HttpNewService = BoxedNewService<(), ServiceRequest, ServiceResponse, Error, ()>; /// Application builder - structure that follows the builder pattern /// for building application instances. -pub struct App -where - T: NewService, Response = ServiceRequest>, -{ - chain: T, +pub struct App { + endpoint: T, + services: Vec>, + default: Option>, + factory_ref: Rc>>, data: Vec>, config: AppConfigInner, - _t: PhantomData<(In, Out)>, + external: Vec, + _t: PhantomData<(B)>, } -impl App { +impl App { /// Create application builder. Application can be configured with a builder-like pattern. pub fn new() -> Self { + let fref = Rc::new(RefCell::new(None)); App { - chain: AppChain, + endpoint: AppEntry::new(fref.clone()), data: Vec::new(), + services: Vec::new(), + default: None, + factory_ref: fref, config: AppConfigInner::default(), + external: Vec::new(), _t: PhantomData, } } } -impl App +impl App where - In: 'static, - Out: 'static, + B: MessageBody, T: NewService< - Request = ServiceRequest, - Response = ServiceRequest, + Request = ServiceRequest, + Response = ServiceResponse, Error = Error, InitError = (), >, { - /// Set application data. Applicatin data could be accessed + /// Set application data. Application data could be accessed /// by using `Data` extractor where `T` is data type. /// /// **Note**: http server accepts an application factory rather than @@ -112,149 +113,42 @@ where self } - /// Registers middleware, in the form of a middleware component (type), - /// that runs during inbound and/or outbound processing in the request - /// lifecycle (request -> response), modifying request/response as - /// necessary, across all requests managed by the *Application*. + /// Run external configuration as part of the application building + /// process /// - /// Use middleware when you need to read or modify *every* request or response in some way. + /// This function is useful for moving parts of configuration to a + /// different module or even library. For example, + /// some of the resource's configuration could be moved to different module. /// /// ```rust - /// use actix_service::Service; - /// # use futures::Future; - /// use actix_web::{middleware, web, App}; - /// use actix_web::http::{header::CONTENT_TYPE, HeaderValue}; + /// # extern crate actix_web; + /// use actix_web::{web, middleware, App, HttpResponse}; /// - /// fn index() -> &'static str { - /// "Welcome!" + /// // this function could be located in different module + /// fn config(cfg: &mut web::ServiceConfig) { + /// cfg.service(web::resource("/test") + /// .route(web::get().to(|| HttpResponse::Ok())) + /// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) + /// ); /// } /// /// fn main() { /// let app = App::new() /// .wrap(middleware::Logger::default()) - /// .route("/index.html", web::get().to(index)); + /// .configure(config) // <- register resources + /// .route("/index.html", web::get().to(|| HttpResponse::Ok())); /// } /// ``` - pub fn wrap( - self, - mw: F, - ) -> AppRouter< - T, - Out, - B, - impl NewService< - Request = ServiceRequest, - Response = ServiceResponse, - Error = Error, - InitError = (), - >, - > + pub fn configure(mut self, f: F) -> Self where - M: Transform< - AppRouting, - Request = ServiceRequest, - Response = ServiceResponse, - Error = Error, - InitError = (), - >, - F: IntoTransform>, + F: Fn(&mut ServiceConfig), { - let fref = Rc::new(RefCell::new(None)); - let endpoint = apply_transform(mw, AppEntry::new(fref.clone())); - AppRouter { - endpoint, - chain: self.chain, - data: self.data, - services: Vec::new(), - default: None, - factory_ref: fref, - config: self.config, - external: Vec::new(), - _t: PhantomData, - } - } - - /// Registers middleware, in the form of a closure, that runs during inbound - /// and/or outbound processing in the request lifecycle (request -> response), - /// modifying request/response as necessary, across all requests managed by - /// the *Application*. - /// - /// Use middleware when you need to read or modify *every* request or response in some way. - /// - /// ```rust - /// use actix_service::Service; - /// # use futures::Future; - /// use actix_web::{web, App}; - /// use actix_web::http::{header::CONTENT_TYPE, HeaderValue}; - /// - /// fn index() -> &'static str { - /// "Welcome!" - /// } - /// - /// fn main() { - /// let app = App::new() - /// .wrap_fn(|req, srv| - /// srv.call(req).map(|mut res| { - /// res.headers_mut().insert( - /// CONTENT_TYPE, HeaderValue::from_static("text/plain"), - /// ); - /// res - /// })) - /// .route("/index.html", web::get().to(index)); - /// } - /// ``` - pub fn wrap_fn( - self, - mw: F, - ) -> AppRouter< - T, - Out, - B, - impl NewService< - Request = ServiceRequest, - Response = ServiceResponse, - Error = Error, - InitError = (), - >, - > - where - F: FnMut(ServiceRequest, &mut AppRouting) -> R + Clone, - R: IntoFuture, Error = Error>, - { - self.wrap(mw) - } - - /// Register a request modifier. It can modify any request parameters - /// including request payload type. - pub fn chain( - self, - chain: F, - ) -> App< - In, - P, - impl NewService< - Request = ServiceRequest, - Response = ServiceRequest

, - Error = Error, - InitError = (), - >, - > - where - C: NewService< - Request = ServiceRequest, - Response = ServiceRequest

, - Error = Error, - InitError = (), - >, - F: IntoNewService, - { - let chain = self.chain.and_then(chain.into_new_service()); - App { - chain, - data: self.data, - config: self.config, - _t: PhantomData, - } + let mut cfg = ServiceConfig::new(); + f(&mut cfg); + self.data.extend(cfg.data); + self.services.extend(cfg.services); + self.external.extend(cfg.external); + self } /// Configure route for a specific path. @@ -276,132 +170,7 @@ where /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())); /// } /// ``` - pub fn route( - self, - path: &str, - mut route: Route, - ) -> AppRouter> { - self.service( - Resource::new(path) - .add_guards(route.take_guards()) - .route(route), - ) - } - - /// Register http service. - /// - /// Http service is any type that implements `HttpServiceFactory` trait. - /// - /// Actix web provides several services implementations: - /// - /// * *Resource* is an entry in resource table which corresponds to requested URL. - /// * *Scope* is a set of resources with common root path. - /// * "StaticFiles" is a service for static files support - pub fn service(self, service: F) -> AppRouter> - where - F: HttpServiceFactory + 'static, - { - let fref = Rc::new(RefCell::new(None)); - - AppRouter { - chain: self.chain, - default: None, - endpoint: AppEntry::new(fref.clone()), - factory_ref: fref, - data: self.data, - config: self.config, - services: vec![Box::new(ServiceFactoryWrapper::new(service))], - external: Vec::new(), - _t: PhantomData, - } - } - - /// Set server host name. - /// - /// Host name is used by application router as a hostname for url - /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. - /// html#method.host) documentation for more information. - /// - /// By default host name is set to a "localhost" value. - pub fn hostname(mut self, val: &str) -> Self { - self.config.host = val.to_owned(); - self - } - - #[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] - /// Enable content compression and decompression. - pub fn enable_encoding( - self, - ) -> AppRouter< - impl NewService< - Request = ServiceRequest, - Response = ServiceRequest>>, - Error = Error, - InitError = (), - >, - Decoder>, - Encoder, - impl NewService< - Request = ServiceRequest>>, - Response = ServiceResponse>, - Error = Error, - InitError = (), - >, - > - where - Out: Stream, - { - use crate::middleware::encoding::{Compress, Decompress}; - - self.chain(Decompress::new()).wrap(Compress::default()) - } -} - -/// Application router builder - Structure that follows the builder pattern -/// for building application instances. -pub struct AppRouter { - chain: C, - endpoint: T, - services: Vec>>, - default: Option>>, - factory_ref: Rc>>>, - data: Vec>, - config: AppConfigInner, - external: Vec, - _t: PhantomData<(P, B)>, -} - -impl AppRouter -where - P: 'static, - B: MessageBody, - T: NewService< - Request = ServiceRequest

, - Response = ServiceResponse, - Error = Error, - InitError = (), - >, -{ - /// Configure route for a specific path. - /// - /// This is a simplified version of the `App::service()` method. - /// This method can not be could multiple times, in that case - /// multiple resources with one route would be registered for same resource path. - /// - /// ```rust - /// use actix_web::{web, App, HttpResponse}; - /// - /// fn index(data: web::Path<(String, String)>) -> &'static str { - /// "Welcome!" - /// } - /// - /// fn main() { - /// let app = App::new() - /// .route("/test1", web::get().to(index)) - /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())); - /// } - /// ``` - pub fn route(self, path: &str, mut route: Route

) -> Self { + pub fn route(self, path: &str, mut route: Route) -> Self { self.service( Resource::new(path) .add_guards(route.take_guards()) @@ -420,102 +189,75 @@ where /// * "StaticFiles" is a service for static files support pub fn service(mut self, factory: F) -> Self where - F: HttpServiceFactory

+ 'static, + F: HttpServiceFactory + 'static, { self.services .push(Box::new(ServiceFactoryWrapper::new(factory))); self } - /// Registers middleware, in the form of a middleware component (type), - /// that runs during inbound and/or outbound processing in the request - /// lifecycle (request -> response), modifying request/response as - /// necessary, across all requests managed by the *Route*. + /// Set server host name. /// - /// Use middleware when you need to read or modify *every* request or response in some way. + /// Host name is used by application router as a hostname for url + /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. + /// html#method.host) documentation for more information. /// - pub fn wrap( - self, - mw: F, - ) -> AppRouter< - C, - P, - B1, - impl NewService< - Request = ServiceRequest

, - Response = ServiceResponse, - Error = Error, - InitError = (), - >, - > - where - M: Transform< - T::Service, - Request = ServiceRequest

, - Response = ServiceResponse, - Error = Error, - InitError = (), - >, - B1: MessageBody, - F: IntoTransform, - { - let endpoint = apply_transform(mw, self.endpoint); - AppRouter { - endpoint, - chain: self.chain, - data: self.data, - services: self.services, - default: self.default, - factory_ref: self.factory_ref, - config: self.config, - external: self.external, - _t: PhantomData, - } + /// By default host name is set to a "localhost" value. + pub fn hostname(mut self, val: &str) -> Self { + self.config.host = val.to_owned(); + self } - /// Registers middleware, in the form of a closure, that runs during inbound - /// and/or outbound processing in the request lifecycle (request -> response), - /// modifying request/response as necessary, across all requests managed by - /// the *Route*. + /// Default service to be used if no matching resource could be found. /// - /// Use middleware when you need to read or modify *every* request or response in some way. + /// It is possible to use services like `Resource`, `Route`. /// - pub fn wrap_fn( - self, - mw: F, - ) -> AppRouter< - C, - P, - B1, - impl NewService< - Request = ServiceRequest

, - Response = ServiceResponse, - Error = Error, - InitError = (), - >, - > + /// ```rust + /// use actix_web::{web, App, HttpResponse}; + /// + /// fn index() -> &'static str { + /// "Welcome!" + /// } + /// + /// fn main() { + /// let app = App::new() + /// .service( + /// web::resource("/index.html").route(web::get().to(index))) + /// .default_service( + /// web::route().to(|| HttpResponse::NotFound())); + /// } + /// ``` + /// + /// It is also possible to use static files as default service. + /// + /// ```rust + /// use actix_files::Files; + /// use actix_web::{web, App, HttpResponse}; + /// + /// fn main() { + /// let app = App::new() + /// .service( + /// web::resource("/index.html").to(|| HttpResponse::Ok())) + /// .default_service( + /// Files::new("", "./static") + /// ); + /// } + /// ``` + pub fn default_service(mut self, f: F) -> Self where - B1: MessageBody, - F: FnMut(ServiceRequest

, &mut T::Service) -> R + Clone, - R: IntoFuture, Error = Error>, - { - self.wrap(mw) - } - - /// Default resource to be used if no matching resource could be found. - pub fn default_resource(mut self, f: F) -> Self - where - F: FnOnce(Resource

) -> Resource, + F: IntoNewService, U: NewService< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, - InitError = (), > + 'static, + U::InitError: fmt::Debug, { // create and configure default resource self.default = Some(Rc::new(boxed::new_service( - f(Resource::new("")).into_new_service().map_init_err(|_| ()), + f.into_new_service().map_init_err(|e| { + log::error!("Can not construct default service: {:?}", e) + }), ))); self @@ -553,27 +295,128 @@ where self.external.push(rdef); self } + + /// Registers middleware, in the form of a middleware component (type), + /// that runs during inbound and/or outbound processing in the request + /// lifecycle (request -> response), modifying request/response as + /// necessary, across all requests managed by the *Application*. + /// + /// Use middleware when you need to read or modify *every* request or response in some way. + /// + /// ```rust + /// use actix_service::Service; + /// # use futures::Future; + /// use actix_web::{middleware, web, App}; + /// use actix_web::http::{header::CONTENT_TYPE, HeaderValue}; + /// + /// fn index() -> &'static str { + /// "Welcome!" + /// } + /// + /// fn main() { + /// let app = App::new() + /// .wrap(middleware::Logger::default()) + /// .route("/index.html", web::get().to(index)); + /// } + /// ``` + pub fn wrap( + self, + mw: F, + ) -> App< + impl NewService< + Request = ServiceRequest, + Response = ServiceResponse, + Error = Error, + InitError = (), + >, + B1, + > + where + M: Transform< + T::Service, + Request = ServiceRequest, + Response = ServiceResponse, + Error = Error, + InitError = (), + >, + B1: MessageBody, + F: IntoTransform, + { + let endpoint = apply_transform(mw, self.endpoint); + App { + endpoint, + data: self.data, + services: self.services, + default: self.default, + factory_ref: self.factory_ref, + config: self.config, + external: self.external, + _t: PhantomData, + } + } + + /// Registers middleware, in the form of a closure, that runs during inbound + /// and/or outbound processing in the request lifecycle (request -> response), + /// modifying request/response as necessary, across all requests managed by + /// the *Application*. + /// + /// Use middleware when you need to read or modify *every* request or response in some way. + /// + /// ```rust + /// use actix_service::Service; + /// # use futures::Future; + /// use actix_web::{web, App}; + /// use actix_web::http::{header::CONTENT_TYPE, HeaderValue}; + /// + /// fn index() -> &'static str { + /// "Welcome!" + /// } + /// + /// fn main() { + /// let app = App::new() + /// .wrap_fn(|req, srv| + /// srv.call(req).map(|mut res| { + /// res.headers_mut().insert( + /// CONTENT_TYPE, HeaderValue::from_static("text/plain"), + /// ); + /// res + /// })) + /// .route("/index.html", web::get().to(index)); + /// } + /// ``` + pub fn wrap_fn( + self, + mw: F, + ) -> App< + impl NewService< + Request = ServiceRequest, + Response = ServiceResponse, + Error = Error, + InitError = (), + >, + B1, + > + where + B1: MessageBody, + F: FnMut(ServiceRequest, &mut T::Service) -> R + Clone, + R: IntoFuture, Error = Error>, + { + self.wrap(mw) + } } -impl IntoNewService, ServerConfig> - for AppRouter +impl IntoNewService, ServerConfig> for App where + B: MessageBody, T: NewService< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), >, - C: NewService< - Request = ServiceRequest, - Response = ServiceRequest

, - Error = Error, - InitError = (), - >, { - fn into_new_service(self) -> AppInit { + fn into_new_service(self) -> AppInit { AppInit { - chain: self.chain, data: self.data, endpoint: self.endpoint, services: RefCell::new(self.services), @@ -588,13 +431,14 @@ where #[cfg(test)] mod tests { use actix_service::Service; + use bytes::Bytes; use futures::{Future, IntoFuture}; use super::*; use crate::http::{header, HeaderValue, Method, StatusCode}; use crate::service::{ServiceRequest, ServiceResponse}; - use crate::test::{block_on, call_success, init_service, TestRequest}; - use crate::{web, Error, HttpResponse}; + use crate::test::{block_on, call_service, init_service, read_body, TestRequest}; + use crate::{web, Error, HttpRequest, HttpResponse}; #[test] fn test_default_resource() { @@ -614,10 +458,14 @@ mod tests { .service(web::resource("/test").to(|| HttpResponse::Ok())) .service( web::resource("/test2") - .default_resource(|r| r.to(|| HttpResponse::Created())) + .default_service(|r: ServiceRequest| { + r.into_response(HttpResponse::Created()) + }) .route(web::get().to(|| HttpResponse::Ok())), ) - .default_resource(|r| r.to(|| HttpResponse::MethodNotAllowed())), + .default_service(|r: ServiceRequest| { + r.into_response(HttpResponse::MethodNotAllowed()) + }), ); let req = TestRequest::with_uri("/blah").to_request(); @@ -654,13 +502,13 @@ mod tests { assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } - fn md( - req: ServiceRequest

, + fn md( + req: ServiceRequest, srv: &mut S, ) -> impl IntoFuture, Error = Error> where S: Service< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, >, @@ -680,7 +528,7 @@ mod tests { .route("/test", web::get().to(|| HttpResponse::Ok())), ); let req = TestRequest::with_uri("/test").to_request(); - let resp = call_success(&mut srv, req); + let resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), @@ -696,7 +544,7 @@ mod tests { .wrap(md), ); let req = TestRequest::with_uri("/test").to_request(); - let resp = call_success(&mut srv, req); + let resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), @@ -720,7 +568,7 @@ mod tests { .service(web::resource("/test").to(|| HttpResponse::Ok())), ); let req = TestRequest::with_uri("/test").to_request(); - let resp = call_success(&mut srv, req); + let resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), @@ -744,11 +592,33 @@ mod tests { }), ); let req = TestRequest::with_uri("/test").to_request(); - let resp = call_success(&mut srv, req); + let resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), HeaderValue::from_static("0001") ); } + + #[test] + fn test_external_resource() { + let mut srv = init_service( + App::new() + .external_resource("youtube", "https://youtube.com/watch/{video_id}") + .route( + "/test", + web::get().to(|req: HttpRequest| { + HttpResponse::Ok().body(format!( + "{}", + req.url_for("youtube", &["12345"]).unwrap() + )) + }), + ), + ); + let req = TestRequest::with_uri("/test").to_request(); + let resp = call_service(&mut srv, req); + assert_eq!(resp.status(), StatusCode::OK); + let body = read_body(resp); + assert_eq!(body, Bytes::from_static(b"https://youtube.com/watch/12345")); + } } diff --git a/src/app_service.rs b/src/app_service.rs index 236eed9f9..7229a2301 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -6,55 +6,50 @@ use actix_http::{Request, Response}; use actix_router::{Path, ResourceDef, ResourceInfo, Router, Url}; use actix_server_config::ServerConfig; use actix_service::boxed::{self, BoxedNewService, BoxedService}; -use actix_service::{fn_service, AndThen, NewService, Service, ServiceExt}; +use actix_service::{fn_service, NewService, Service}; use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, Poll}; -use crate::config::{AppConfig, ServiceConfig}; +use crate::config::{AppConfig, AppService}; use crate::data::{DataFactory, DataFactoryResult}; use crate::error::Error; use crate::guard::Guard; +use crate::request::{HttpRequest, HttpRequestPool}; use crate::rmap::ResourceMap; use crate::service::{ServiceFactory, ServiceRequest, ServiceResponse}; type Guards = Vec>; -type HttpService

= BoxedService, ServiceResponse, Error>; -type HttpNewService

= - BoxedNewService<(), ServiceRequest

, ServiceResponse, Error, ()>; -type BoxedResponse = Box>; +type HttpService = BoxedService; +type HttpNewService = BoxedNewService<(), ServiceRequest, ServiceResponse, Error, ()>; +type BoxedResponse = Either< + FutureResult, + Box>, +>; /// Service factory to convert `Request` to a `ServiceRequest`. /// It also executes data factories. -pub struct AppInit +pub struct AppInit where - C: NewService>, T: NewService< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), >, { - pub(crate) chain: C, pub(crate) endpoint: T, pub(crate) data: Vec>, pub(crate) config: RefCell, - pub(crate) services: RefCell>>>, - pub(crate) default: Option>>, - pub(crate) factory_ref: Rc>>>, + pub(crate) services: RefCell>>, + pub(crate) default: Option>, + pub(crate) factory_ref: Rc>>, pub(crate) external: RefCell>, } -impl NewService for AppInit +impl NewService for AppInit where - C: NewService< - Request = ServiceRequest, - Response = ServiceRequest

, - Error = Error, - InitError = (), - >, T: NewService< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), @@ -62,15 +57,15 @@ where { type Request = Request; type Response = ServiceResponse; - type Error = C::Error; - type InitError = C::InitError; - type Service = AndThen, T::Service>; - type Future = AppInitResult; + type Error = T::Error; + type InitError = T::InitError; + type Service = AppInitService; + type Future = AppInitResult; fn new_service(&self, cfg: &ServerConfig) -> Self::Future { // update resource default service let default = self.default.clone().unwrap_or_else(|| { - Rc::new(boxed::new_service(fn_service(|req: ServiceRequest

| { + Rc::new(boxed::new_service(fn_service(|req: ServiceRequest| { Ok(req.into_response(Response::NotFound().finish())) }))) }); @@ -82,8 +77,7 @@ where loc_cfg.addr = cfg.local_addr(); } - let mut config = - ServiceConfig::new(self.config.borrow().clone(), default.clone()); + let mut config = AppService::new(self.config.borrow().clone(), default.clone()); // register services std::mem::replace(&mut *self.services.borrow_mut(), Vec::new()) @@ -117,8 +111,6 @@ where rmap.finish(rmap.clone()); AppInitResult { - chain: None, - chain_fut: self.chain.new_service(&()), endpoint: None, endpoint_fut: self.endpoint.new_service(&()), data: self.data.iter().map(|s| s.construct()).collect(), @@ -129,38 +121,29 @@ where } } -pub struct AppInitResult +pub struct AppInitResult where - C: NewService, T: NewService, { - chain: Option, endpoint: Option, - chain_fut: C::Future, endpoint_fut: T::Future, rmap: Rc, data: Vec>, config: AppConfig, - _t: PhantomData<(P, B)>, + _t: PhantomData, } -impl Future for AppInitResult +impl Future for AppInitResult where - C: NewService< - Request = ServiceRequest, - Response = ServiceRequest

, - Error = Error, - InitError = (), - >, T: NewService< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), >, { - type Item = AndThen, T::Service>; - type Error = C::InitError; + type Item = AppInitService; + type Error = T::InitError; fn poll(&mut self) -> Poll { let mut idx = 0; @@ -173,27 +156,19 @@ where } } - if self.chain.is_none() { - if let Async::Ready(srv) = self.chain_fut.poll()? { - self.chain = Some(srv); - } - } - if self.endpoint.is_none() { if let Async::Ready(srv) = self.endpoint_fut.poll()? { self.endpoint = Some(srv); } } - if self.chain.is_some() && self.endpoint.is_some() { - Ok(Async::Ready( - AppInitService { - chain: self.chain.take().unwrap(), - rmap: self.rmap.clone(), - config: self.config.clone(), - } - .and_then(self.endpoint.take().unwrap()), - )) + if self.endpoint.is_some() && self.data.is_empty() { + Ok(Async::Ready(AppInitService { + service: self.endpoint.take().unwrap(), + rmap: self.rmap.clone(), + config: self.config.clone(), + pool: HttpRequestPool::create(), + })) } else { Ok(Async::NotReady) } @@ -201,51 +176,63 @@ where } /// Service to convert `Request` to a `ServiceRequest` -pub struct AppInitService +pub struct AppInitService where - C: Service, Error = Error>, + T: Service, Error = Error>, { - chain: C, + service: T, rmap: Rc, config: AppConfig, + pool: &'static HttpRequestPool, } -impl Service for AppInitService +impl Service for AppInitService where - C: Service, Error = Error>, + T: Service, Error = Error>, { type Request = Request; - type Response = ServiceRequest

; - type Error = C::Error; - type Future = C::Future; + type Response = ServiceResponse; + type Error = T::Error; + type Future = T::Future; fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.chain.poll_ready() + self.service.poll_ready() } fn call(&mut self, req: Request) -> Self::Future { - let req = ServiceRequest::new( - Path::new(Url::new(req.uri().clone())), - req, - self.rmap.clone(), - self.config.clone(), - ); - self.chain.call(req) + let (head, payload) = req.into_parts(); + + let req = if let Some(mut req) = self.pool.get_request() { + let inner = Rc::get_mut(&mut req.0).unwrap(); + inner.path.get_mut().update(&head.uri); + inner.path.reset(); + inner.head = head; + req + } else { + HttpRequest::new( + Path::new(Url::new(head.uri.clone())), + head, + self.rmap.clone(), + self.config.clone(), + self.pool, + ) + }; + self.service.call(ServiceRequest::from_parts(req, payload)) } } -pub struct AppRoutingFactory

{ - services: Rc, RefCell>)>>, - default: Rc>, +pub struct AppRoutingFactory { + services: Rc>)>>, + default: Rc, } -impl NewService for AppRoutingFactory

{ - type Request = ServiceRequest

; +impl NewService for AppRoutingFactory { + type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; type InitError = (); - type Service = AppRouting

; - type Future = AppRoutingFactoryResponse

; + type Service = AppRouting; + type Future = AppRoutingFactoryResponse; fn new_service(&self, _: &()) -> Self::Future { AppRoutingFactoryResponse { @@ -266,23 +253,23 @@ impl NewService for AppRoutingFactory

{ } } -type HttpServiceFut

= Box, Error = ()>>; +type HttpServiceFut = Box>; /// Create app service #[doc(hidden)] -pub struct AppRoutingFactoryResponse

{ - fut: Vec>, - default: Option>, - default_fut: Option, Error = ()>>>, +pub struct AppRoutingFactoryResponse { + fut: Vec, + default: Option, + default_fut: Option>>, } -enum CreateAppRoutingItem

{ - Future(Option, Option, HttpServiceFut

), - Service(ResourceDef, Option, HttpService

), +enum CreateAppRoutingItem { + Future(Option, Option, HttpServiceFut), + Service(ResourceDef, Option, HttpService), } -impl

Future for AppRoutingFactoryResponse

{ - type Item = AppRouting

; +impl Future for AppRoutingFactoryResponse { + type Item = AppRouting; type Error = (); fn poll(&mut self) -> Poll { @@ -343,17 +330,17 @@ impl

Future for AppRoutingFactoryResponse

{ } } -pub struct AppRouting

{ - router: Router, Guards>, - ready: Option<(ServiceRequest

, ResourceInfo)>, - default: Option>, +pub struct AppRouting { + router: Router, + ready: Option<(ServiceRequest, ResourceInfo)>, + default: Option, } -impl

Service for AppRouting

{ - type Request = ServiceRequest

; +impl Service for AppRouting { + type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; - type Future = Either>; + type Future = BoxedResponse; fn poll_ready(&mut self) -> Poll<(), Self::Error> { if self.ready.is_none() { @@ -363,7 +350,7 @@ impl

Service for AppRouting

{ } } - fn call(&mut self, mut req: ServiceRequest

) -> Self::Future { + fn call(&mut self, mut req: ServiceRequest) -> Self::Future { let res = self.router.recognize_mut_checked(&mut req, |req, guards| { if let Some(ref guards) = guards { for f in guards { @@ -376,69 +363,36 @@ impl

Service for AppRouting

{ }); if let Some((srv, _info)) = res { - Either::A(srv.call(req)) + srv.call(req) } else if let Some(ref mut default) = self.default { - Either::A(default.call(req)) + default.call(req) } else { let req = req.into_parts().0; - Either::B(ok(ServiceResponse::new(req, Response::NotFound().finish()))) + Either::A(ok(ServiceResponse::new(req, Response::NotFound().finish()))) } } } /// Wrapper service for routing -pub struct AppEntry

{ - factory: Rc>>>, +pub struct AppEntry { + factory: Rc>>, } -impl

AppEntry

{ - pub fn new(factory: Rc>>>) -> Self { +impl AppEntry { + pub fn new(factory: Rc>>) -> Self { AppEntry { factory } } } -impl NewService for AppEntry

{ - type Request = ServiceRequest

; +impl NewService for AppEntry { + type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; type InitError = (); - type Service = AppRouting

; - type Future = AppRoutingFactoryResponse

; + type Service = AppRouting; + type Future = AppRoutingFactoryResponse; fn new_service(&self, _: &()) -> Self::Future { self.factory.borrow_mut().as_mut().unwrap().new_service(&()) } } - -#[doc(hidden)] -pub struct AppChain; - -impl NewService for AppChain { - type Request = ServiceRequest; - type Response = ServiceRequest; - type Error = Error; - type InitError = (); - type Service = AppChain; - type Future = FutureResult; - - fn new_service(&self, _: &()) -> Self::Future { - ok(AppChain) - } -} - -impl Service for AppChain { - type Request = ServiceRequest; - type Response = ServiceRequest; - type Error = Error; - type Future = FutureResult; - - #[inline] - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - #[inline] - fn call(&mut self, req: ServiceRequest) -> Self::Future { - ok(req) - } -} diff --git a/src/config.rs b/src/config.rs index ceb58feb7..4c4bfa220 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,33 +5,40 @@ use std::rc::Rc; use actix_http::Extensions; use actix_router::ResourceDef; use actix_service::{boxed, IntoNewService, NewService}; +use futures::IntoFuture; +use crate::data::{Data, DataFactory}; use crate::error::Error; use crate::guard::Guard; +use crate::resource::Resource; use crate::rmap::ResourceMap; -use crate::service::{ServiceRequest, ServiceResponse}; +use crate::route::Route; +use crate::service::{ + HttpServiceFactory, ServiceFactory, ServiceFactoryWrapper, ServiceRequest, + ServiceResponse, +}; type Guards = Vec>; -type HttpNewService

= - boxed::BoxedNewService<(), ServiceRequest

, ServiceResponse, Error, ()>; +type HttpNewService = + boxed::BoxedNewService<(), ServiceRequest, ServiceResponse, Error, ()>; /// Application configuration -pub struct ServiceConfig

{ +pub struct AppService { config: AppConfig, root: bool, - default: Rc>, + default: Rc, services: Vec<( ResourceDef, - HttpNewService

, + HttpNewService, Option, Option>, )>, } -impl ServiceConfig

{ +impl AppService { /// Crate server settings instance - pub(crate) fn new(config: AppConfig, default: Rc>) -> Self { - ServiceConfig { + pub(crate) fn new(config: AppConfig, default: Rc) -> Self { + AppService { config, default, root: true, @@ -48,7 +55,7 @@ impl ServiceConfig

{ self, ) -> Vec<( ResourceDef, - HttpNewService

, + HttpNewService, Option, Option>, )> { @@ -56,7 +63,7 @@ impl ServiceConfig

{ } pub(crate) fn clone_config(&self) -> Self { - ServiceConfig { + AppService { config: self.config.clone(), default: self.default.clone(), services: Vec::new(), @@ -70,7 +77,7 @@ impl ServiceConfig

{ } /// Default resource - pub fn default_service(&self) -> Rc> { + pub fn default_service(&self) -> Rc { self.default.clone() } @@ -83,7 +90,7 @@ impl ServiceConfig

{ ) where F: IntoNewService, S: NewService< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), @@ -157,3 +164,197 @@ impl Default for AppConfigInner { } } } + +/// Service config is used for external configuration. +/// Part of application configuration could be offloaded +/// to set of external methods. This could help with +/// modularization of big application configuration. +pub struct ServiceConfig { + pub(crate) services: Vec>, + pub(crate) data: Vec>, + pub(crate) external: Vec, +} + +impl ServiceConfig { + pub(crate) fn new() -> Self { + Self { + services: Vec::new(), + data: Vec::new(), + external: Vec::new(), + } + } + + /// Set application data. Applicatin data could be accessed + /// by using `Data` extractor where `T` is data type. + /// + /// This is same as `App::data()` method. + pub fn data(&mut self, data: S) -> &mut Self { + self.data.push(Box::new(Data::new(data))); + self + } + + /// Set application data factory. This function is + /// similar to `.data()` but it accepts data factory. Data object get + /// constructed asynchronously during application initialization. + /// + /// This is same as `App::data_dactory()` method. + pub fn data_factory(&mut self, data: F) -> &mut Self + where + F: Fn() -> R + 'static, + R: IntoFuture + 'static, + R::Error: std::fmt::Debug, + { + self.data.push(Box::new(data)); + self + } + + /// Configure route for a specific path. + /// + /// This is same as `App::route()` method. + pub fn route(&mut self, path: &str, mut route: Route) -> &mut Self { + self.service( + Resource::new(path) + .add_guards(route.take_guards()) + .route(route), + ) + } + + /// Register http service. + /// + /// This is same as `App::service()` method. + pub fn service(&mut self, factory: F) -> &mut Self + where + F: HttpServiceFactory + 'static, + { + self.services + .push(Box::new(ServiceFactoryWrapper::new(factory))); + self + } + + /// Register an external resource. + /// + /// External resources are useful for URL generation purposes only + /// and are never considered for matching at request time. Calls to + /// `HttpRequest::url_for()` will work as expected. + /// + /// This is same as `App::external_service()` method. + pub fn external_resource(&mut self, name: N, url: U) -> &mut Self + where + N: AsRef, + U: AsRef, + { + let mut rdef = ResourceDef::new(url.as_ref()); + *rdef.name_mut() = name.as_ref().to_string(); + self.external.push(rdef); + self + } +} + +#[cfg(test)] +mod tests { + use actix_service::Service; + use bytes::Bytes; + use futures::Future; + use tokio_timer::sleep; + + use super::*; + use crate::http::{Method, StatusCode}; + use crate::test::{block_on, call_service, init_service, read_body, TestRequest}; + use crate::{web, App, HttpRequest, HttpResponse}; + + #[test] + fn test_data() { + let cfg = |cfg: &mut ServiceConfig| { + cfg.data(10usize); + }; + + let mut srv = + init_service(App::new().configure(cfg).service( + web::resource("/").to(|_: web::Data| HttpResponse::Ok()), + )); + let req = TestRequest::default().to_request(); + let resp = block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + } + + #[test] + fn test_data_factory() { + let cfg = |cfg: &mut ServiceConfig| { + cfg.data_factory(|| { + sleep(std::time::Duration::from_millis(50)).then(|_| { + println!("READY"); + Ok::<_, ()>(10usize) + }) + }); + }; + + let mut srv = + init_service(App::new().configure(cfg).service( + web::resource("/").to(|_: web::Data| HttpResponse::Ok()), + )); + let req = TestRequest::default().to_request(); + let resp = block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let cfg2 = |cfg: &mut ServiceConfig| { + cfg.data_factory(|| Ok::<_, ()>(10u32)); + }; + let mut srv = init_service( + App::new() + .service(web::resource("/").to(|_: web::Data| HttpResponse::Ok())) + .configure(cfg2), + ); + let req = TestRequest::default().to_request(); + let resp = block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); + } + + #[test] + fn test_external_resource() { + let mut srv = init_service( + App::new() + .configure(|cfg| { + cfg.external_resource( + "youtube", + "https://youtube.com/watch/{video_id}", + ); + }) + .route( + "/test", + web::get().to(|req: HttpRequest| { + HttpResponse::Ok().body(format!( + "{}", + req.url_for("youtube", &["12345"]).unwrap() + )) + }), + ), + ); + let req = TestRequest::with_uri("/test").to_request(); + let resp = call_service(&mut srv, req); + assert_eq!(resp.status(), StatusCode::OK); + let body = read_body(resp); + assert_eq!(body, Bytes::from_static(b"https://youtube.com/watch/12345")); + } + + #[test] + fn test_service() { + let mut srv = init_service(App::new().configure(|cfg| { + cfg.service( + web::resource("/test").route(web::get().to(|| HttpResponse::Created())), + ) + .route("/index.html", web::get().to(|| HttpResponse::Ok())); + })); + + let req = TestRequest::with_uri("/test") + .method(Method::GET) + .to_request(); + let resp = call_service(&mut srv, req); + assert_eq!(resp.status(), StatusCode::CREATED); + + let req = TestRequest::with_uri("/index.html") + .method(Method::GET) + .to_request(); + let resp = call_service(&mut srv, req); + assert_eq!(resp.status(), StatusCode::OK); + } +} diff --git a/src/data.rs b/src/data.rs index a79a303bc..0c896fcc2 100644 --- a/src/data.rs +++ b/src/data.rs @@ -5,8 +5,9 @@ use actix_http::error::{Error, ErrorInternalServerError}; use actix_http::Extensions; use futures::{Async, Future, IntoFuture, Poll}; +use crate::dev::Payload; use crate::extract::FromRequest; -use crate::service::ServiceFromRequest; +use crate::request::HttpRequest; /// Application data factory pub(crate) trait DataFactory { @@ -24,15 +25,16 @@ pub(crate) trait DataFactoryResult { /// during application configuration process /// with `App::data()` method. /// -/// Applicatin data could be accessed by using `Data` +/// Application data could be accessed by using `Data` /// extractor where `T` is data type. /// /// **Note**: http server accepts an application factory rather than /// an application instance. Http server constructs an application /// instance for each thread, thus application data must be constructed /// multiple times. If you want to share data between different -/// threads, a shared object should be used, e.g. `Arc`. Application -/// data does not need to be `Send` or `Sync`. +/// threads, a shareable object should be used, e.g. `Send + Sync`. Application +/// data does not need to be `Send` or `Sync`. Internally `Data` instance +/// uses `Arc`. /// /// If route data is not set for a handler, using `Data` extractor would /// cause *Internal Server Error* response. @@ -59,6 +61,7 @@ pub(crate) trait DataFactoryResult { /// web::get().to(index))); /// } /// ``` +#[derive(Debug)] pub struct Data(Arc); impl Data { @@ -86,15 +89,21 @@ impl Clone for Data { } } -impl FromRequest

for Data { +impl FromRequest for Data { + type Config = (); type Error = Error; type Future = Result; #[inline] - fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { - if let Some(st) = req.request().config().extensions().get::>() { + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + if let Some(st) = req.app_config().extensions().get::>() { Ok(st.clone()) } else { + log::debug!( + "Failed to construct App-level Data extractor. \ + Request path: {:?}", + req.path() + ); Err(ErrorInternalServerError( "App data is not configured, to configure use App::data()", )) @@ -225,15 +234,17 @@ impl Clone for RouteData { } } -impl FromRequest

for RouteData { +impl FromRequest for RouteData { + type Config = (); type Error = Error; type Future = Result; #[inline] - fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { if let Some(st) = req.route_data::() { Ok(st.clone()) } else { + log::debug!("Failed to construct Route-level Data extractor"); Err(ErrorInternalServerError( "Route data is not configured, to configure use Route::data()", )) diff --git a/src/error.rs b/src/error.rs index 78dc2fb6a..e9e225f22 100644 --- a/src/error.rs +++ b/src/error.rs @@ -31,7 +31,7 @@ pub enum UrlencodedError { #[display(fmt = "Can not decode chunked transfer encoding")] Chunked, /// Payload size is bigger than allowed. (default: 256kB) - #[display(fmt = "Urlencoded payload size is bigger than allowed. (default: 256kB)")] + #[display(fmt = "Urlencoded payload size is bigger than allowed (default: 256kB)")] Overflow, /// Payload size is now known #[display(fmt = "Payload size is now known")] @@ -66,7 +66,7 @@ impl ResponseError for UrlencodedError { #[derive(Debug, Display, From)] pub enum JsonPayloadError { /// Payload size is bigger than allowed. (default: 32kB) - #[display(fmt = "Json payload size is bigger than allowed.")] + #[display(fmt = "Json payload size is bigger than allowed")] Overflow, /// Content type error #[display(fmt = "Content type error")] @@ -121,36 +121,6 @@ impl ResponseError for ReadlinesError { } } -/// A set of errors that can occur during parsing multipart streams -#[derive(Debug, Display, From)] -pub enum MultipartError { - /// Content-Type header is not found - #[display(fmt = "No Content-type header found")] - NoContentType, - /// Can not parse Content-Type header - #[display(fmt = "Can not parse Content-Type header")] - ParseContentType, - /// Multipart boundary is not found - #[display(fmt = "Multipart boundary is not found")] - Boundary, - /// Multipart stream is incomplete - #[display(fmt = "Multipart stream is incomplete")] - Incomplete, - /// Error during field parsing - #[display(fmt = "{}", _0)] - Parse(ParseError), - /// Payload error - #[display(fmt = "{}", _0)] - Payload(PayloadError), -} - -/// Return `BadRequest` for `MultipartError` -impl ResponseError for MultipartError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - #[cfg(test)] mod tests { use super::*; @@ -180,10 +150,4 @@ mod tests { let resp: HttpResponse = ReadlinesError::EncodingError.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } - - #[test] - fn test_multipart_error() { - let resp: HttpResponse = MultipartError::Boundary.error_response(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - } } diff --git a/src/extract.rs b/src/extract.rs index 4cd04be2b..6d414fbcc 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -4,20 +4,39 @@ use actix_http::error::Error; use futures::future::ok; use futures::{future, Async, Future, IntoFuture, Poll}; -use crate::service::ServiceFromRequest; +use crate::dev::Payload; +use crate::request::HttpRequest; /// Trait implemented by types that can be extracted from request. /// /// Types that implement this trait can be used with `Route` handlers. -pub trait FromRequest

: Sized { +pub trait FromRequest: Sized { /// The associated error which can be returned. type Error: Into; /// Future that resolves to a Self type Future: IntoFuture; + /// Configuration for this extractor + type Config: Default + 'static; + /// Convert request to a Self - fn from_request(req: &mut ServiceFromRequest

) -> Self::Future; + fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future; + + /// Convert request to a Self + /// + /// This method uses `Payload::None` as payload stream. + fn extract(req: &HttpRequest) -> Self::Future { + Self::from_request(req, &mut Payload::None) + } + + /// Create and configure config instance. + fn configure(f: F) -> Self::Config + where + F: FnOnce(Self::Config) -> Self::Config, + { + f(Self::Config::default()) + } } /// Optionally extract a field from the request @@ -28,7 +47,7 @@ pub trait FromRequest

: Sized { /// /// ```rust /// # #[macro_use] extern crate serde_derive; -/// use actix_web::{web, dev, App, Error, FromRequest}; +/// use actix_web::{web, dev, App, Error, HttpRequest, FromRequest}; /// use actix_web::error::ErrorBadRequest; /// use rand; /// @@ -37,11 +56,12 @@ pub trait FromRequest

: Sized { /// name: String /// } /// -/// impl

FromRequest

for Thing { +/// impl FromRequest for Thing { /// type Error = Error; /// type Future = Result; +/// type Config = (); /// -/// fn from_request(req: &mut dev::ServiceFromRequest

) -> Self::Future { +/// fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { /// if rand::random() { /// Ok(Thing { name: "thingy".into() }) /// } else { @@ -67,23 +87,28 @@ pub trait FromRequest

: Sized { /// ); /// } /// ``` -impl FromRequest

for Option +impl FromRequest for Option where - T: FromRequest

, + T: FromRequest, T::Future: 'static, { + type Config = T::Config; type Error = Error; type Future = Box, Error = Error>>; #[inline] - fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { - Box::new(T::from_request(req).into_future().then(|r| match r { - Ok(v) => future::ok(Some(v)), - Err(e) => { - log::debug!("Error for Option extractor: {}", e.into()); - future::ok(None) - } - })) + fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { + Box::new( + T::from_request(req, payload) + .into_future() + .then(|r| match r { + Ok(v) => future::ok(Some(v)), + Err(e) => { + log::debug!("Error for Option extractor: {}", e.into()); + future::ok(None) + } + }), + ) } } @@ -95,7 +120,7 @@ where /// /// ```rust /// # #[macro_use] extern crate serde_derive; -/// use actix_web::{web, dev, App, Result, Error, FromRequest}; +/// use actix_web::{web, dev, App, Result, Error, HttpRequest, FromRequest}; /// use actix_web::error::ErrorBadRequest; /// use rand; /// @@ -104,11 +129,12 @@ where /// name: String /// } /// -/// impl

FromRequest

for Thing { +/// impl FromRequest for Thing { /// type Error = Error; /// type Future = Result; +/// type Config = (); /// -/// fn from_request(req: &mut dev::ServiceFromRequest

) -> Self::Future { +/// fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { /// if rand::random() { /// Ok(Thing { name: "thingy".into() }) /// } else { @@ -131,30 +157,36 @@ where /// ); /// } /// ``` -impl FromRequest

for Result +impl FromRequest for Result where - T: FromRequest

, + T: FromRequest, T::Future: 'static, T::Error: 'static, { + type Config = T::Config; type Error = Error; type Future = Box, Error = Error>>; #[inline] - fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { - Box::new(T::from_request(req).into_future().then(|res| match res { - Ok(v) => ok(Ok(v)), - Err(e) => ok(Err(e)), - })) + fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { + Box::new( + T::from_request(req, payload) + .into_future() + .then(|res| match res { + Ok(v) => ok(Ok(v)), + Err(e) => ok(Err(e)), + }), + ) } } #[doc(hidden)] -impl

FromRequest

for () { +impl FromRequest for () { + type Config = (); type Error = Error; type Future = Result<(), Error>; - fn from_request(_req: &mut ServiceFromRequest

) -> Self::Future { + fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future { Ok(()) } } @@ -163,26 +195,27 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { /// FromRequest implementation for tuple #[doc(hidden)] - impl + 'static),+> FromRequest

for ($($T,)+) + impl<$($T: FromRequest + 'static),+> FromRequest for ($($T,)+) { type Error = Error; - type Future = $fut_type; + type Future = $fut_type<$($T),+>; + type Config = ($($T::Config),+); - fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { + fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { $fut_type { items: <($(Option<$T>,)+)>::default(), - futs: ($($T::from_request(req).into_future(),)+), + futs: ($($T::from_request(req, payload).into_future(),)+), } } } #[doc(hidden)] - pub struct $fut_type),+> { + pub struct $fut_type<$($T: FromRequest),+> { items: ($(Option<$T>,)+), futs: ($(<$T::Future as futures::IntoFuture>::Future,)+), } - impl),+> Future for $fut_type + impl<$($T: FromRequest),+> Future for $fut_type<$($T),+> { type Item = ($($T,)+); type Error = Error; @@ -232,13 +265,12 @@ tuple_from_req!(TupleFromRequest10, (0, A), (1, B), (2, C), (3, D), (4, E), (5, #[cfg(test)] mod tests { use actix_http::http::header; - use actix_router::ResourceDef; use bytes::Bytes; use serde_derive::Deserialize; use super::*; use crate::test::{block_on, TestRequest}; - use crate::types::{Form, FormConfig, Path, Query}; + use crate::types::{Form, FormConfig}; #[derive(Deserialize, Debug, PartialEq)] struct Info { @@ -247,25 +279,25 @@ mod tests { #[test] fn test_option() { - let mut req = TestRequest::with_header( + let (req, mut pl) = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", ) .route_data(FormConfig::default().limit(4096)) - .to_from(); + .to_http_parts(); - let r = block_on(Option::>::from_request(&mut req)).unwrap(); + let r = block_on(Option::>::from_request(&req, &mut pl)).unwrap(); assert_eq!(r, None); - let mut req = TestRequest::with_header( + let (req, mut pl) = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", ) .header(header::CONTENT_LENGTH, "9") .set_payload(Bytes::from_static(b"hello=world")) - .to_from(); + .to_http_parts(); - let r = block_on(Option::>::from_request(&mut req)).unwrap(); + let r = block_on(Option::>::from_request(&req, &mut pl)).unwrap(); assert_eq!( r, Some(Form(Info { @@ -273,29 +305,29 @@ mod tests { })) ); - let mut req = TestRequest::with_header( + let (req, mut pl) = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", ) .header(header::CONTENT_LENGTH, "9") .set_payload(Bytes::from_static(b"bye=world")) - .to_from(); + .to_http_parts(); - let r = block_on(Option::>::from_request(&mut req)).unwrap(); + let r = block_on(Option::>::from_request(&req, &mut pl)).unwrap(); assert_eq!(r, None); } #[test] fn test_result() { - let mut req = TestRequest::with_header( + let (req, mut pl) = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", ) .header(header::CONTENT_LENGTH, "11") .set_payload(Bytes::from_static(b"hello=world")) - .to_from(); + .to_http_parts(); - let r = block_on(Result::, Error>::from_request(&mut req)) + let r = block_on(Result::, Error>::from_request(&req, &mut pl)) .unwrap() .unwrap(); assert_eq!( @@ -305,68 +337,16 @@ mod tests { }) ); - let mut req = TestRequest::with_header( + let (req, mut pl) = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", ) .header(header::CONTENT_LENGTH, "9") .set_payload(Bytes::from_static(b"bye=world")) - .to_from(); + .to_http_parts(); - let r = block_on(Result::, Error>::from_request(&mut req)).unwrap(); + let r = + block_on(Result::, Error>::from_request(&req, &mut pl)).unwrap(); assert!(r.is_err()); } - - #[derive(Deserialize)] - struct MyStruct { - key: String, - value: String, - } - - #[derive(Deserialize)] - struct Id { - id: String, - } - - #[derive(Deserialize)] - struct Test2 { - key: String, - value: u32, - } - - #[test] - fn test_request_extract() { - let mut req = TestRequest::with_uri("/name/user1/?id=test").to_from(); - - let resource = ResourceDef::new("/{key}/{value}/"); - resource.match_path(req.match_info_mut()); - - let s = Path::::from_request(&mut req).unwrap(); - assert_eq!(s.key, "name"); - assert_eq!(s.value, "user1"); - - let s = Path::<(String, String)>::from_request(&mut req).unwrap(); - assert_eq!(s.0, "name"); - assert_eq!(s.1, "user1"); - - let s = Query::::from_request(&mut req).unwrap(); - assert_eq!(s.id, "test"); - - let mut req = TestRequest::with_uri("/name/32/").to_from(); - let resource = ResourceDef::new("/{key}/{value}/"); - resource.match_path(req.match_info_mut()); - - let s = Path::::from_request(&mut req).unwrap(); - assert_eq!(s.as_ref().key, "name"); - assert_eq!(s.value, 32); - - let s = Path::<(String, u8)>::from_request(&mut req).unwrap(); - assert_eq!(s.0, "name"); - assert_eq!(s.1, 32); - - let res = Path::>::from_request(&mut req).unwrap(); - assert_eq!(res[0], "name".to_owned()); - assert_eq!(res[1], "32".to_owned()); - } - } diff --git a/src/handler.rs b/src/handler.rs index 4ff3193c8..850c0c92c 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -2,7 +2,7 @@ use std::cell::RefCell; use std::marker::PhantomData; use std::rc::Rc; -use actix_http::{Error, Extensions, Response}; +use actix_http::{Error, Extensions, Payload, Response}; use actix_service::{NewService, Service, Void}; use futures::future::{ok, FutureResult}; use futures::{try_ready, Async, Future, IntoFuture, Poll}; @@ -10,7 +10,7 @@ use futures::{try_ready, Async, Future, IntoFuture, Poll}; use crate::extract::FromRequest; use crate::request::HttpRequest; use crate::responder::Responder; -use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; +use crate::service::{ServiceRequest, ServiceResponse}; /// Handler converter factory pub trait Factory: Clone @@ -22,8 +22,8 @@ where impl Factory<(), R> for F where - F: Fn() -> R + Clone + 'static, - R: Responder + 'static, + F: Fn() -> R + Clone, + R: Responder, { fn call(&self, _: ()) -> R { (self)() @@ -52,40 +52,24 @@ where } } } -impl NewService for Handler + +impl Clone for Handler where F: Factory, - R: Responder + 'static, + R: Responder, { - type Request = (T, HttpRequest); - type Response = ServiceResponse; - type Error = Void; - type InitError = (); - type Service = HandlerService; - type Future = FutureResult; - - fn new_service(&self, _: &()) -> Self::Future { - ok(HandlerService { + fn clone(&self) -> Self { + Self { hnd: self.hnd.clone(), _t: PhantomData, - }) + } } } -#[doc(hidden)] -pub struct HandlerService +impl Service for Handler where F: Factory, - R: Responder + 'static, -{ - hnd: F, - _t: PhantomData<(T, R)>, -} - -impl Service for HandlerService -where - F: Factory, - R: Responder + 'static, + R: Responder, { type Request = (T, HttpRequest); type Response = ServiceResponse; @@ -140,7 +124,7 @@ where pub trait AsyncFactory: Clone + 'static where R: IntoFuture, - R::Item: Into, + R::Item: Responder, R::Error: Into, { fn call(&self, param: T) -> R; @@ -150,7 +134,7 @@ impl AsyncFactory<(), R> for F where F: Fn() -> R + Clone + 'static, R: IntoFuture, - R::Item: Into, + R::Item: Responder, R::Error: Into, { fn call(&self, _: ()) -> R { @@ -163,7 +147,7 @@ pub struct AsyncHandler where F: AsyncFactory, R: IntoFuture, - R::Item: Into, + R::Item: Responder, R::Error: Into, { hnd: F, @@ -174,7 +158,7 @@ impl AsyncHandler where F: AsyncFactory, R: IntoFuture, - R::Item: Into, + R::Item: Responder, R::Error: Into, { pub fn new(hnd: F) -> Self { @@ -184,50 +168,32 @@ where } } } -impl NewService for AsyncHandler + +impl Clone for AsyncHandler where F: AsyncFactory, R: IntoFuture, - R::Item: Into, + R::Item: Responder, R::Error: Into, { - type Request = (T, HttpRequest); - type Response = ServiceResponse; - type Error = Error; - type InitError = (); - type Service = AsyncHandlerService; - type Future = FutureResult; - - fn new_service(&self, _: &()) -> Self::Future { - ok(AsyncHandlerService { + fn clone(&self) -> Self { + AsyncHandler { hnd: self.hnd.clone(), _t: PhantomData, - }) + } } } -#[doc(hidden)] -pub struct AsyncHandlerService +impl Service for AsyncHandler where F: AsyncFactory, R: IntoFuture, - R::Item: Into, - R::Error: Into, -{ - hnd: F, - _t: PhantomData<(T, R)>, -} - -impl Service for AsyncHandlerService -where - F: AsyncFactory, - R: IntoFuture, - R::Item: Into, + R::Item: Responder, R::Error: Into, { type Request = (T, HttpRequest); type Response = ServiceResponse; - type Error = Error; + type Error = Void; type Future = AsyncHandlerServiceResponse; fn poll_ready(&mut self) -> Poll<(), Self::Error> { @@ -237,32 +203,56 @@ where fn call(&mut self, (param, req): (T, HttpRequest)) -> Self::Future { AsyncHandlerServiceResponse { fut: self.hnd.call(param).into_future(), + fut2: None, req: Some(req), } } } #[doc(hidden)] -pub struct AsyncHandlerServiceResponse { +pub struct AsyncHandlerServiceResponse +where + T: Future, + T::Item: Responder, +{ fut: T, + fut2: Option<<::Future as IntoFuture>::Future>, req: Option, } impl Future for AsyncHandlerServiceResponse where T: Future, - T::Item: Into, + T::Item: Responder, T::Error: Into, { type Item = ServiceResponse; - type Error = Error; + type Error = Void; fn poll(&mut self) -> Poll { + if let Some(ref mut fut) = self.fut2 { + return match fut.poll() { + Ok(Async::Ready(res)) => Ok(Async::Ready(ServiceResponse::new( + self.req.take().unwrap(), + res, + ))), + Ok(Async::NotReady) => Ok(Async::NotReady), + Err(e) => { + let res: Response = e.into().into(); + Ok(Async::Ready(ServiceResponse::new( + self.req.take().unwrap(), + res, + ))) + } + }; + } + match self.fut.poll() { - Ok(Async::Ready(res)) => Ok(Async::Ready(ServiceResponse::new( - self.req.take().unwrap(), - res.into(), - ))), + Ok(Async::Ready(res)) => { + self.fut2 = + Some(res.respond_to(self.req.as_ref().unwrap()).into_future()); + return self.poll(); + } Ok(Async::NotReady) => Ok(Async::NotReady), Err(e) => { let res: Response = e.into().into(); @@ -276,87 +266,112 @@ where } /// Extract arguments from request -pub struct Extract> { +pub struct Extract { config: Rc>>>, - _t: PhantomData<(P, T)>, + service: S, + _t: PhantomData, } -impl> Extract { - pub fn new(config: Rc>>>) -> Self { +impl Extract { + pub fn new(config: Rc>>>, service: S) -> Self { Extract { config, + service, _t: PhantomData, } } } -impl> NewService for Extract { - type Request = ServiceRequest

; - type Response = (T, HttpRequest); - type Error = (Error, ServiceFromRequest

); +impl NewService for Extract +where + S: Service + + Clone, +{ + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = (Error, ServiceRequest); type InitError = (); - type Service = ExtractService; + type Service = ExtractService; type Future = FutureResult; fn new_service(&self, _: &()) -> Self::Future { ok(ExtractService { _t: PhantomData, config: self.config.borrow().clone(), + service: self.service.clone(), }) } } -pub struct ExtractService> { +pub struct ExtractService { config: Option>, - _t: PhantomData<(P, T)>, + service: S, + _t: PhantomData, } -impl> Service for ExtractService { - type Request = ServiceRequest

; - type Response = (T, HttpRequest); - type Error = (Error, ServiceFromRequest

); - type Future = ExtractResponse; +impl Service for ExtractService +where + S: Service + + Clone, +{ + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = (Error, ServiceRequest); + type Future = ExtractResponse; fn poll_ready(&mut self) -> Poll<(), Self::Error> { Ok(Async::Ready(())) } - fn call(&mut self, req: ServiceRequest

) -> Self::Future { - let mut req = ServiceFromRequest::new(req, self.config.clone()); + fn call(&mut self, req: ServiceRequest) -> Self::Future { + let (mut req, mut payload) = req.into_parts(); + req.set_route_data(self.config.clone()); + let fut = T::from_request(&req, &mut payload).into_future(); + ExtractResponse { - fut: T::from_request(&mut req).into_future(), - req: Some(req), + fut, + fut_s: None, + req: Some((req, payload)), + service: self.service.clone(), } } } -pub struct ExtractResponse> { - req: Option>, +pub struct ExtractResponse { + req: Option<(HttpRequest, Payload)>, + service: S, fut: ::Future, + fut_s: Option, } -impl> Future for ExtractResponse { - type Item = (T, HttpRequest); - type Error = (Error, ServiceFromRequest

); +impl Future for ExtractResponse +where + S: Service, +{ + type Item = ServiceResponse; + type Error = (Error, ServiceRequest); fn poll(&mut self) -> Poll { - let item = try_ready!(self - .fut - .poll() - .map_err(|e| (e.into(), self.req.take().unwrap()))); + if let Some(ref mut fut) = self.fut_s { + return fut.poll().map_err(|_| panic!()); + } - let req = self.req.take().unwrap(); - let req = req.into_request(); + let item = try_ready!(self.fut.poll().map_err(|e| { + let (req, payload) = self.req.take().unwrap(); + let req = ServiceRequest::from_parts(req, payload); + (e.into(), req) + })); - Ok(Async::Ready((item, req))) + self.fut_s = Some(self.service.call((item, self.req.take().unwrap().0))); + self.poll() } } /// FromRequest trait impl for tuples macro_rules! factory_tuple ({ $(($n:tt, $T:ident)),+} => { impl Factory<($($T,)+), Res> for Func - where Func: Fn($($T,)+) -> Res + Clone + 'static, - Res: Responder + 'static, + where Func: Fn($($T,)+) -> Res + Clone, + Res: Responder, { fn call(&self, param: ($($T,)+)) -> Res { (self)($(param.$n,)+) @@ -365,8 +380,8 @@ macro_rules! factory_tuple ({ $(($n:tt, $T:ident)),+} => { impl AsyncFactory<($($T,)+), Res> for Func where Func: Fn($($T,)+) -> Res + Clone + 'static, - Res: IntoFuture + 'static, - Res::Item: Into, + Res: IntoFuture, + Res::Item: Responder, Res::Error: Into, { fn call(&self, param: ($($T,)+)) -> Res { diff --git a/src/info.rs b/src/info.rs index 9a97c3353..e9b375875 100644 --- a/src/info.rs +++ b/src/info.rs @@ -30,10 +30,10 @@ impl ConnectionInfo { let mut host = None; let mut scheme = None; let mut remote = None; - let peer = None; + let mut peer = None; // load forwarded header - for hdr in req.headers.get_all(header::FORWARDED) { + 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(',') { @@ -69,7 +69,7 @@ impl ConnectionInfo { if scheme.is_none() { if let Some(h) = req .headers - .get(HeaderName::from_lowercase(X_FORWARDED_PROTO).unwrap()) + .get(&HeaderName::from_lowercase(X_FORWARDED_PROTO).unwrap()) { if let Ok(h) = h.to_str() { scheme = h.split(',').next().map(|v| v.trim()); @@ -87,14 +87,14 @@ impl ConnectionInfo { if host.is_none() { if let Some(h) = req .headers - .get(HeaderName::from_lowercase(X_FORWARDED_HOST).unwrap()) + .get(&HeaderName::from_lowercase(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) { + if let Some(h) = req.headers.get(&header::HOST) { host = h.to_str().ok(); } if host.is_none() { @@ -110,16 +110,16 @@ impl ConnectionInfo { if remote.is_none() { if let Some(h) = req .headers - .get(HeaderName::from_lowercase(X_FORWARDED_FOR).unwrap()) + .get(&HeaderName::from_lowercase(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)); - // } + if remote.is_none() { + // get peeraddr from socketaddr + peer = req.peer_addr.map(|addr| format!("{}", addr)); + } } ConnectionInfo { diff --git a/src/lib.rs b/src/lib.rs index ca4968833..6abf37c1e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,7 +67,6 @@ //! ## Package feature //! //! * `client` - enables http client -//! * `tls` - enables ssl support via `native-tls` crate //! * `ssl` - enables ssl support via `openssl` crate, supports `http/2` //! * `rust-tls` - enables ssl support via `rustls` crate, supports `http/2` //! * `secure-cookies` - enables secure cookies support, includes `ring` crate as @@ -134,19 +133,16 @@ pub mod dev { //! use actix_web::dev::*; //! ``` - pub use crate::app::AppRouter; - pub use crate::config::{AppConfig, ServiceConfig}; + pub use crate::config::{AppConfig, AppService}; pub use crate::info::ConnectionInfo; pub use crate::rmap::ResourceMap; - pub use crate::service::{ - HttpServiceFactory, ServiceFromRequest, ServiceRequest, ServiceResponse, - }; + pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse}; pub use crate::types::form::UrlEncoded; pub use crate::types::json::JsonBody; - pub use crate::types::payload::HttpMessageBody; pub use crate::types::readlines::Readlines; pub use actix_http::body::{Body, BodySize, MessageBody, ResponseBody}; + pub use actix_http::encoding::Decoder as Decompress; pub use actix_http::ResponseBuilder as HttpResponseBuilder; pub use actix_http::{ Extensions, Payload, PayloadStream, RequestHead, ResponseHead, @@ -190,5 +186,7 @@ pub mod client { pub use awc::error::{ ConnectError, InvalidUrl, PayloadError, SendRequestError, WsClientError, }; - pub use awc::{test, Client, ClientBuilder, ClientRequest, ClientResponse}; + pub use awc::{ + test, Client, ClientBuilder, ClientRequest, ClientResponse, Connector, + }; } diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index f74754402..d5c4082ea 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -41,11 +41,11 @@ impl BodyEncoding for Response { /// To disable compression set encoding to `ContentEncoding::Identity` value. /// /// ```rust -/// use actix_web::{web, middleware::encoding, App, HttpResponse}; +/// use actix_web::{web, middleware, App, HttpResponse}; /// /// fn main() { /// let app = App::new() -/// .wrap(encoding::Compress::default()) +/// .wrap(middleware::Compress::default()) /// .service( /// web::resource("/test") /// .route(web::get().to(|| HttpResponse::Ok())) @@ -68,14 +68,12 @@ impl Default for Compress { } } -impl Transform for Compress +impl Transform for Compress where - P: 'static, B: MessageBody, - S: Service, Response = ServiceResponse>, - S::Future: 'static, + S: Service>, { - type Request = ServiceRequest

; + type Request = ServiceRequest; type Response = ServiceResponse>; type Error = S::Error; type InitError = (); @@ -95,25 +93,23 @@ pub struct CompressMiddleware { encoding: ContentEncoding, } -impl Service for CompressMiddleware +impl Service for CompressMiddleware where - P: 'static, B: MessageBody, - S: Service, Response = ServiceResponse>, - S::Future: 'static, + S: Service>, { - type Request = ServiceRequest

; + type Request = ServiceRequest; type Response = ServiceResponse>; type Error = S::Error; - type Future = CompressResponse; + type Future = CompressResponse; fn poll_ready(&mut self) -> Poll<(), Self::Error> { self.service.poll_ready() } - fn call(&mut self, req: ServiceRequest

) -> Self::Future { + fn call(&mut self, req: ServiceRequest) -> Self::Future { // negotiate content-encoding - let encoding = if let Some(val) = req.headers().get(ACCEPT_ENCODING) { + let encoding = if let Some(val) = req.headers().get(&ACCEPT_ENCODING) { if let Ok(enc) = val.to_str() { AcceptEncoding::parse(enc, self.encoding) } else { @@ -132,24 +128,20 @@ where } #[doc(hidden)] -pub struct CompressResponse +pub struct CompressResponse where - P: 'static, - B: MessageBody, S: Service, - S::Future: 'static, + B: MessageBody, { fut: S::Future, encoding: ContentEncoding, - _t: PhantomData<(P, B)>, + _t: PhantomData<(B)>, } -impl Future for CompressResponse +impl Future for CompressResponse where - P: 'static, B: MessageBody, - S: Service, Response = ServiceResponse>, - S::Future: 'static, + S: Service>, { type Item = ServiceResponse>; type Error = S::Error; diff --git a/src/middleware/cors.rs b/src/middleware/cors.rs index 920b480bb..12cd0b83a 100644 --- a/src/middleware/cors.rs +++ b/src/middleware/cors.rs @@ -475,10 +475,11 @@ fn cors<'a>( parts.as_mut() } -impl IntoTransform for Cors +impl IntoTransform for Cors where - S: Service, Response = ServiceResponse> + 'static, - P: 'static, + S: Service>, + S::Future: 'static, + S::Error: 'static, B: 'static, { fn into_transform(self) -> CorsFactory { @@ -536,15 +537,14 @@ pub struct CorsFactory { inner: Rc, } -impl Transform for CorsFactory +impl Transform for CorsFactory where - S: Service, Response = ServiceResponse>, + S: Service>, S::Future: 'static, S::Error: 'static, - P: 'static, B: 'static, { - type Request = ServiceRequest

; + type Request = ServiceRequest; type Response = ServiceResponse; type Error = S::Error; type InitError = (); @@ -584,7 +584,7 @@ struct Inner { impl Inner { fn validate_origin(&self, req: &RequestHead) -> Result<(), CorsError> { - if let Some(hdr) = req.headers().get(header::ORIGIN) { + if let Some(hdr) = req.headers().get(&header::ORIGIN) { if let Ok(origin) = hdr.to_str() { return match self.origins { AllOrSome::All => Ok(()), @@ -608,7 +608,7 @@ impl Inner { AllOrSome::All => { if self.send_wildcard { Some(HeaderValue::from_static("*")) - } else if let Some(origin) = req.headers().get(header::ORIGIN) { + } else if let Some(origin) = req.headers().get(&header::ORIGIN) { Some(origin.clone()) } else { None @@ -617,7 +617,7 @@ impl Inner { AllOrSome::Some(ref origins) => { if let Some(origin) = req.headers() - .get(header::ORIGIN) + .get(&header::ORIGIN) .filter(|o| match o.to_str() { Ok(os) => origins.contains(os), _ => false, @@ -632,7 +632,7 @@ impl Inner { } fn validate_allowed_method(&self, req: &RequestHead) -> Result<(), CorsError> { - if let Some(hdr) = req.headers().get(header::ACCESS_CONTROL_REQUEST_METHOD) { + if let Some(hdr) = req.headers().get(&header::ACCESS_CONTROL_REQUEST_METHOD) { if let Ok(meth) = hdr.to_str() { if let Ok(method) = Method::try_from(meth) { return self @@ -653,7 +653,7 @@ impl Inner { AllOrSome::All => Ok(()), AllOrSome::Some(ref allowed_headers) => { if let Some(hdr) = - req.headers().get(header::ACCESS_CONTROL_REQUEST_HEADERS) + req.headers().get(&header::ACCESS_CONTROL_REQUEST_HEADERS) { if let Ok(headers) = hdr.to_str() { let mut hdrs = HashSet::new(); @@ -678,15 +678,14 @@ impl Inner { } } -impl Service for CorsMiddleware +impl Service for CorsMiddleware where - S: Service, Response = ServiceResponse>, + S: Service>, S::Future: 'static, S::Error: 'static, - P: 'static, B: 'static, { - type Request = ServiceRequest

; + type Request = ServiceRequest; type Response = ServiceResponse; type Error = S::Error; type Future = Either< @@ -698,7 +697,7 @@ where self.service.poll_ready() } - fn call(&mut self, req: ServiceRequest

) -> Self::Future { + fn call(&mut self, req: ServiceRequest) -> Self::Future { if self.inner.preflight && Method::OPTIONS == *req.method() { if let Err(e) = self .inner @@ -721,7 +720,7 @@ where .unwrap(), ) } else if let Some(hdr) = - req.headers().get(header::ACCESS_CONTROL_REQUEST_HEADERS) + req.headers().get(&header::ACCESS_CONTROL_REQUEST_HEADERS) { Some(hdr.clone()) } else { @@ -760,7 +759,7 @@ where .into_body(); Either::A(ok(req.into_response(res))) - } else if req.headers().contains_key(header::ORIGIN) { + } else if req.headers().contains_key(&header::ORIGIN) { // Only check requests with a origin header. if let Err(e) = self.inner.validate_origin(req.head()) { return Either::A(ok(req.error_response(e))); @@ -791,7 +790,7 @@ where } if inner.vary_header { let value = - if let Some(hdr) = res.headers_mut().get(header::VARY) { + if let Some(hdr) = res.headers_mut().get(&header::VARY) { let mut val: Vec = Vec::with_capacity(hdr.as_bytes().len() + 8); val.extend(hdr.as_bytes()); @@ -816,17 +815,15 @@ mod tests { use actix_service::{FnService, Transform}; use super::*; - use crate::dev::PayloadStream; use crate::test::{self, block_on, TestRequest}; impl Cors { - fn finish(self, srv: S) -> CorsMiddleware + fn finish(self, srv: S) -> CorsMiddleware where - S: Service, Response = ServiceResponse> + S: Service> + 'static, S::Future: 'static, S::Error: 'static, - P: 'static, B: 'static, { block_on( @@ -851,7 +848,18 @@ mod tests { let req = TestRequest::with_header("Origin", "https://www.example.com") .to_srv_request(); - let resp = test::call_success(&mut cors, req); + let resp = test::call_service(&mut cors, req); + assert_eq!(resp.status(), StatusCode::OK); + } + + #[test] + fn default() { + let mut cors = + block_on(Cors::default().new_transform(test::ok_service())).unwrap(); + let req = TestRequest::with_header("Origin", "https://www.example.com") + .to_srv_request(); + + let resp = test::call_service(&mut cors, req); assert_eq!(resp.status(), StatusCode::OK); } @@ -871,7 +879,7 @@ mod tests { assert!(cors.inner.validate_allowed_method(req.head()).is_err()); assert!(cors.inner.validate_allowed_headers(req.head()).is_err()); - let resp = test::call_success(&mut cors, req); + let resp = test::call_service(&mut cors, req); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_header("Origin", "https://www.example.com") @@ -891,24 +899,24 @@ mod tests { .method(Method::OPTIONS) .to_srv_request(); - let resp = test::call_success(&mut cors, req); + let resp = test::call_service(&mut cors, req); assert_eq!( &b"*"[..], resp.headers() - .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) + .get(&header::ACCESS_CONTROL_ALLOW_ORIGIN) .unwrap() .as_bytes() ); assert_eq!( &b"3600"[..], resp.headers() - .get(header::ACCESS_CONTROL_MAX_AGE) + .get(&header::ACCESS_CONTROL_MAX_AGE) .unwrap() .as_bytes() ); let hdr = resp .headers() - .get(header::ACCESS_CONTROL_ALLOW_HEADERS) + .get(&header::ACCESS_CONTROL_ALLOW_HEADERS) .unwrap() .to_str() .unwrap(); @@ -937,7 +945,7 @@ mod tests { .method(Method::OPTIONS) .to_srv_request(); - let resp = test::call_success(&mut cors, req); + let resp = test::call_service(&mut cors, req); assert_eq!(resp.status(), StatusCode::OK); } @@ -976,7 +984,7 @@ mod tests { .method(Method::GET) .to_srv_request(); - let resp = test::call_success(&mut cors, req); + let resp = test::call_service(&mut cors, req); assert_eq!(resp.status(), StatusCode::OK); } @@ -985,7 +993,7 @@ mod tests { let mut cors = Cors::new().disable_preflight().finish(test::ok_service()); let req = TestRequest::default().method(Method::GET).to_srv_request(); - let resp = test::call_success(&mut cors, req); + let resp = test::call_service(&mut cors, req); assert!(resp .headers() .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) @@ -994,7 +1002,7 @@ mod tests { let req = TestRequest::with_header("Origin", "https://www.example.com") .method(Method::OPTIONS) .to_srv_request(); - let resp = test::call_success(&mut cors, req); + let resp = test::call_service(&mut cors, req); assert_eq!( &b"https://www.example.com"[..], resp.headers() @@ -1021,7 +1029,7 @@ mod tests { .method(Method::OPTIONS) .to_srv_request(); - let resp = test::call_success(&mut cors, req); + let resp = test::call_service(&mut cors, req); assert_eq!( &b"*"[..], resp.headers() @@ -1059,7 +1067,7 @@ mod tests { .allowed_headers(exposed_headers.clone()) .expose_headers(exposed_headers.clone()) .allowed_header(header::CONTENT_TYPE) - .finish(FnService::new(move |req: ServiceRequest| { + .finish(FnService::new(move |req: ServiceRequest| { req.into_response( HttpResponse::Ok().header(header::VARY, "Accept").finish(), ) @@ -1067,7 +1075,7 @@ mod tests { let req = TestRequest::with_header("Origin", "https://www.example.com") .method(Method::OPTIONS) .to_srv_request(); - let resp = test::call_success(&mut cors, req); + let resp = test::call_service(&mut cors, req); assert_eq!( &b"Accept, Origin"[..], resp.headers().get(header::VARY).unwrap().as_bytes() @@ -1083,7 +1091,7 @@ mod tests { .method(Method::OPTIONS) .header(header::ACCESS_CONTROL_REQUEST_METHOD, "POST") .to_srv_request(); - let resp = test::call_success(&mut cors, req); + let resp = test::call_service(&mut cors, req); let origins_str = resp .headers() @@ -1107,7 +1115,7 @@ mod tests { .method(Method::GET) .to_srv_request(); - let resp = test::call_success(&mut cors, req); + let resp = test::call_service(&mut cors, req); assert_eq!( &b"https://example.com"[..], resp.headers() @@ -1120,7 +1128,7 @@ mod tests { .method(Method::GET) .to_srv_request(); - let resp = test::call_success(&mut cors, req); + let resp = test::call_service(&mut cors, req); assert_eq!( &b"https://example.org"[..], resp.headers() @@ -1143,7 +1151,7 @@ mod tests { .method(Method::OPTIONS) .to_srv_request(); - let resp = test::call_success(&mut cors, req); + let resp = test::call_service(&mut cors, req); assert_eq!( &b"https://example.com"[..], resp.headers() @@ -1157,7 +1165,7 @@ mod tests { .method(Method::OPTIONS) .to_srv_request(); - let resp = test::call_success(&mut cors, req); + let resp = test::call_service(&mut cors, req); assert_eq!( &b"https://example.org"[..], resp.headers() diff --git a/src/middleware/decompress.rs b/src/middleware/decompress.rs deleted file mode 100644 index 13735143a..000000000 --- a/src/middleware/decompress.rs +++ /dev/null @@ -1,75 +0,0 @@ -//! Chain service for decompressing request payload. -use std::marker::PhantomData; - -use actix_http::encoding::Decoder; -use actix_service::{NewService, Service}; -use bytes::Bytes; -use futures::future::{ok, FutureResult}; -use futures::{Async, Poll, Stream}; - -use crate::dev::Payload; -use crate::error::{Error, PayloadError}; -use crate::service::ServiceRequest; - -/// `Middleware` for decompressing request's payload. -/// `Decompress` middleware must be added with `App::chain()` method. -/// -/// ```rust -/// use actix_web::{web, middleware::encoding, App, HttpResponse}; -/// -/// fn main() { -/// let app = App::new() -/// .chain(encoding::Decompress::new()) -/// .service( -/// web::resource("/test") -/// .route(web::get().to(|| HttpResponse::Ok())) -/// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) -/// ); -/// } -/// ``` -pub struct Decompress

(PhantomData

); - -impl

Decompress

-where - P: Stream, -{ - pub fn new() -> Self { - Decompress(PhantomData) - } -} - -impl

NewService for Decompress

-where - P: Stream, -{ - type Request = ServiceRequest

; - type Response = ServiceRequest>>; - type Error = Error; - type InitError = (); - type Service = Decompress

; - type Future = FutureResult; - - fn new_service(&self, _: &()) -> Self::Future { - ok(Decompress(PhantomData)) - } -} - -impl

Service for Decompress

-where - P: Stream, -{ - type Request = ServiceRequest

; - type Response = ServiceRequest>>; - type Error = Error; - type Future = FutureResult; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, req: ServiceRequest

) -> Self::Future { - let (req, payload) = req.into_parts(); - let payload = Decoder::from_headers(payload, req.headers()); - ok(ServiceRequest::from_parts(req, Payload::Stream(payload))) - } -} diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index 72e866dbd..c0e62e285 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -85,12 +85,12 @@ impl DefaultHeaders { } } -impl Transform for DefaultHeaders +impl Transform for DefaultHeaders where - S: Service, Response = ServiceResponse>, + S: Service>, S::Future: 'static, { - type Request = ServiceRequest

; + type Request = ServiceRequest; type Response = ServiceResponse; type Error = S::Error; type InitError = (); @@ -110,12 +110,12 @@ pub struct DefaultHeadersMiddleware { inner: Rc, } -impl Service for DefaultHeadersMiddleware +impl Service for DefaultHeadersMiddleware where - S: Service, Response = ServiceResponse>, + S: Service>, S::Future: 'static, { - type Request = ServiceRequest

; + type Request = ServiceRequest; type Response = ServiceResponse; type Error = S::Error; type Future = Box>; @@ -124,18 +124,18 @@ where self.service.poll_ready() } - fn call(&mut self, req: ServiceRequest

) -> Self::Future { + fn call(&mut self, req: ServiceRequest) -> Self::Future { let inner = self.inner.clone(); Box::new(self.service.call(req).map(move |mut res| { // set response headers for (key, value) in inner.headers.iter() { if !res.headers().contains_key(key) { - res.headers_mut().insert(key, value.clone()); + res.headers_mut().insert(key.clone(), value.clone()); } } // default content-type - if inner.ct && !res.headers().contains_key(CONTENT_TYPE) { + if inner.ct && !res.headers().contains_key(&CONTENT_TYPE) { res.headers_mut().insert( CONTENT_TYPE, HeaderValue::from_static("application/octet-stream"), @@ -171,7 +171,7 @@ mod tests { assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); let req = TestRequest::default().to_srv_request(); - let srv = FnService::new(|req: ServiceRequest<_>| { + let srv = FnService::new(|req: ServiceRequest| { req.into_response(HttpResponse::Ok().header(CONTENT_TYPE, "0002").finish()) }); let mut mw = block_on( @@ -186,7 +186,7 @@ mod tests { #[test] fn test_content_type() { - let srv = FnService::new(|req: ServiceRequest<_>| { + let srv = FnService::new(|req: ServiceRequest| { req.into_response(HttpResponse::Ok().finish()) }); let mut mw = diff --git a/src/middleware/errhandlers.rs b/src/middleware/errhandlers.rs index a69bdaf9f..aa36b6a4d 100644 --- a/src/middleware/errhandlers.rs +++ b/src/middleware/errhandlers.rs @@ -81,18 +81,14 @@ impl ErrorHandlers { } } -impl Transform for ErrorHandlers +impl Transform for ErrorHandlers where - S: Service< - Request = ServiceRequest

, - Response = ServiceResponse, - Error = Error, - >, + S: Service, Error = Error>, S::Future: 'static, S::Error: 'static, B: 'static, { - type Request = ServiceRequest

; + type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; type InitError = (); @@ -113,18 +109,14 @@ pub struct ErrorHandlersMiddleware { handlers: Rc>>>, } -impl Service for ErrorHandlersMiddleware +impl Service for ErrorHandlersMiddleware where - S: Service< - Request = ServiceRequest

, - Response = ServiceResponse, - Error = Error, - >, + S: Service, Error = Error>, S::Future: 'static, S::Error: 'static, B: 'static, { - type Request = ServiceRequest

; + type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; type Future = Box>; @@ -133,7 +125,7 @@ where self.service.poll_ready() } - fn call(&mut self, req: ServiceRequest

) -> Self::Future { + fn call(&mut self, req: ServiceRequest) -> Self::Future { let handlers = self.handlers.clone(); Box::new(self.service.call(req).and_then(move |res| { @@ -169,7 +161,7 @@ mod tests { #[test] fn test_handler() { - let srv = FnService::new(|req: ServiceRequest<_>| { + let srv = FnService::new(|req: ServiceRequest| { req.into_response(HttpResponse::InternalServerError().finish()) }); @@ -180,7 +172,7 @@ mod tests { ) .unwrap(); - let resp = test::call_success(&mut mw, TestRequest::default().to_srv_request()); + let resp = test::call_service(&mut mw, TestRequest::default().to_srv_request()); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); } @@ -195,7 +187,7 @@ mod tests { #[test] fn test_handler_async() { - let srv = FnService::new(|req: ServiceRequest<_>| { + let srv = FnService::new(|req: ServiceRequest| { req.into_response(HttpResponse::InternalServerError().finish()) }); @@ -206,7 +198,7 @@ mod tests { ) .unwrap(); - let resp = test::call_success(&mut mw, TestRequest::default().to_srv_request()); + let resp = test::call_service(&mut mw, TestRequest::default().to_srv_request()); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); } } diff --git a/src/middleware/identity.rs b/src/middleware/identity.rs index 7a2c9f376..ba03366fa 100644 --- a/src/middleware/identity.rs +++ b/src/middleware/identity.rs @@ -58,10 +58,8 @@ use time::Duration; use crate::cookie::{Cookie, CookieJar, Key, SameSite}; use crate::error::{Error, Result}; use crate::http::header::{self, HeaderValue}; -use crate::request::HttpRequest; -use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; -use crate::FromRequest; -use crate::HttpMessage; +use crate::service::{ServiceRequest, ServiceResponse}; +use crate::{dev::Payload, FromRequest, HttpMessage, HttpRequest}; /// The extractor type to obtain your identity from a request. /// @@ -142,13 +140,14 @@ struct IdentityItem { /// } /// # fn main() {} /// ``` -impl

FromRequest

for Identity { +impl FromRequest for Identity { + type Config = (); type Error = Error; type Future = Result; #[inline] - fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { - Ok(Identity(req.request().clone())) + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + Ok(Identity(req.clone())) } } @@ -161,7 +160,7 @@ pub trait IdentityPolicy: Sized + 'static { type ResponseFuture: IntoFuture; /// Parse the session from request and load data from a service identity. - fn from_request

(&self, request: &mut ServiceRequest

) -> Self::Future; + fn from_request(&self, request: &mut ServiceRequest) -> Self::Future; /// Write changes to response fn to_response( @@ -200,15 +199,15 @@ impl IdentityService { } } -impl Transform for IdentityService +impl Transform for IdentityService where - P: 'static, - S: Service, Response = ServiceResponse> + 'static, + S: Service> + 'static, S::Future: 'static, + S::Error: 'static, T: IdentityPolicy, B: 'static, { - type Request = ServiceRequest

; + type Request = ServiceRequest; type Response = ServiceResponse; type Error = S::Error; type InitError = (); @@ -229,15 +228,15 @@ pub struct IdentityServiceMiddleware { service: Rc>, } -impl Service for IdentityServiceMiddleware +impl Service for IdentityServiceMiddleware where - P: 'static, B: 'static, - S: Service, Response = ServiceResponse> + 'static, + S: Service> + 'static, S::Future: 'static, + S::Error: 'static, T: IdentityPolicy, { - type Request = ServiceRequest

; + type Request = ServiceRequest; type Response = ServiceResponse; type Error = S::Error; type Future = Box>; @@ -246,7 +245,7 @@ where self.service.borrow_mut().poll_ready() } - fn call(&mut self, mut req: ServiceRequest

) -> Self::Future { + fn call(&mut self, mut req: ServiceRequest) -> Self::Future { let srv = self.service.clone(); let backend = self.backend.clone(); @@ -348,7 +347,7 @@ impl CookieIdentityInner { Ok(()) } - fn load(&self, req: &ServiceRequest) -> Option { + fn load(&self, req: &ServiceRequest) -> Option { if let Ok(cookies) = req.cookies() { for cookie in cookies.iter() { if cookie.name() == self.name { @@ -428,8 +427,13 @@ impl CookieIdentityPolicy { self } - /// Sets the `max-age` field in the session cookie being built. - pub fn max_age(mut self, value: Duration) -> CookieIdentityPolicy { + /// Sets the `max-age` field in the session cookie being built with given number of seconds. + pub fn max_age(self, seconds: i64) -> CookieIdentityPolicy { + self.max_age_time(Duration::seconds(seconds)) + } + + /// Sets the `max-age` field in the session cookie being built with `chrono::Duration`. + pub fn max_age_time(mut self, value: Duration) -> CookieIdentityPolicy { Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); self } @@ -445,7 +449,7 @@ impl IdentityPolicy for CookieIdentityPolicy { type Future = Result, Error>; type ResponseFuture = Result<(), Error>; - fn from_request

(&self, req: &mut ServiceRequest

) -> Self::Future { + fn from_request(&self, req: &mut ServiceRequest) -> Self::Future { Ok(self.0.load(req)) } @@ -501,15 +505,15 @@ mod tests { })), ); let resp = - test::call_success(&mut srv, TestRequest::with_uri("/index").to_request()); + test::call_service(&mut srv, TestRequest::with_uri("/index").to_request()); assert_eq!(resp.status(), StatusCode::OK); let resp = - test::call_success(&mut srv, TestRequest::with_uri("/login").to_request()); + test::call_service(&mut srv, TestRequest::with_uri("/login").to_request()); assert_eq!(resp.status(), StatusCode::OK); let c = resp.response().cookies().next().unwrap().to_owned(); - let resp = test::call_success( + let resp = test::call_service( &mut srv, TestRequest::with_uri("/index") .cookie(c.clone()) @@ -517,7 +521,7 @@ mod tests { ); assert_eq!(resp.status(), StatusCode::CREATED); - let resp = test::call_success( + let resp = test::call_service( &mut srv, TestRequest::with_uri("/logout") .cookie(c.clone()) @@ -526,4 +530,56 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); assert!(resp.headers().contains_key(header::SET_COOKIE)) } + + #[test] + fn test_identity_max_age_time() { + let duration = Duration::days(1); + let mut srv = test::init_service( + App::new() + .wrap(IdentityService::new( + CookieIdentityPolicy::new(&[0; 32]) + .domain("www.rust-lang.org") + .name("actix_auth") + .path("/") + .max_age_time(duration) + .secure(true), + )) + .service(web::resource("/login").to(|id: Identity| { + id.remember("test".to_string()); + HttpResponse::Ok() + })), + ); + let resp = + test::call_service(&mut srv, TestRequest::with_uri("/login").to_request()); + assert_eq!(resp.status(), StatusCode::OK); + assert!(resp.headers().contains_key(header::SET_COOKIE)); + let c = resp.response().cookies().next().unwrap().to_owned(); + assert_eq!(duration, c.max_age().unwrap()); + } + + #[test] + fn test_identity_max_age() { + let seconds = 60; + let mut srv = test::init_service( + App::new() + .wrap(IdentityService::new( + CookieIdentityPolicy::new(&[0; 32]) + .domain("www.rust-lang.org") + .name("actix_auth") + .path("/") + .max_age(seconds) + .secure(true), + )) + .service(web::resource("/login").to(|id: Identity| { + id.remember("test".to_string()); + HttpResponse::Ok() + })), + ); + let resp = + test::call_service(&mut srv, TestRequest::with_uri("/login").to_request()); + assert_eq!(resp.status(), StatusCode::OK); + assert!(resp.headers().contains_key(header::SET_COOKIE)); + let c = resp.response().cookies().next().unwrap().to_owned(); + assert_eq!(Duration::seconds(seconds as i64), c.max_age().unwrap()); + } } diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index bdcc00f28..43893bc0f 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -14,6 +14,7 @@ use time; use crate::dev::{BodySize, MessageBody, ResponseBody}; use crate::error::{Error, Result}; +use crate::http::{HeaderName, HttpTryFrom}; use crate::service::{ServiceRequest, ServiceResponse}; use crate::HttpResponse; @@ -65,6 +66,8 @@ use crate::HttpResponse; /// /// `%D` Time taken to serve the request, in milliseconds /// +/// `%U` Request URL +/// /// `%{FOO}i` request.headers['FOO'] /// /// `%{FOO}o` response.headers['FOO'] @@ -111,12 +114,12 @@ impl Default for Logger { } } -impl Transform for Logger +impl Transform for Logger where - S: Service, Response = ServiceResponse>, + S: Service>, B: MessageBody, { - type Request = ServiceRequest

; + type Request = ServiceRequest; type Response = ServiceResponse>; type Error = S::Error; type InitError = (); @@ -137,21 +140,21 @@ pub struct LoggerMiddleware { service: S, } -impl Service for LoggerMiddleware +impl Service for LoggerMiddleware where - S: Service, Response = ServiceResponse>, + S: Service>, B: MessageBody, { - type Request = ServiceRequest

; + type Request = ServiceRequest; type Response = ServiceResponse>; type Error = S::Error; - type Future = LoggerResponse; + type Future = LoggerResponse; fn poll_ready(&mut self) -> Poll<(), Self::Error> { self.service.poll_ready() } - fn call(&mut self, req: ServiceRequest

) -> Self::Future { + fn call(&mut self, req: ServiceRequest) -> Self::Future { if self.inner.exclude.contains(req.path()) { LoggerResponse { fut: self.service.call(req), @@ -177,7 +180,7 @@ where } #[doc(hidden)] -pub struct LoggerResponse +pub struct LoggerResponse where B: MessageBody, S: Service, @@ -185,13 +188,13 @@ where fut: S::Future, time: time::Tm, format: Option, - _t: PhantomData<(P, B)>, + _t: PhantomData<(B,)>, } -impl Future for LoggerResponse +impl Future for LoggerResponse where B: MessageBody, - S: Service, Response = ServiceResponse>, + S: Service>, { type Item = ServiceResponse>; type Error = S::Error; @@ -238,8 +241,8 @@ impl Drop for StreamLog { } impl MessageBody for StreamLog { - fn length(&self) -> BodySize { - self.body.length() + fn size(&self) -> BodySize { + self.body.size() } fn poll_next(&mut self) -> Poll, Error> { @@ -272,7 +275,7 @@ impl Format { /// Returns `None` if the format string syntax is incorrect. pub fn new(s: &str) -> Format { log::trace!("Access log format: {}", s); - let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([ioe])|[atPrsbTD]?)").unwrap(); + let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([ioe])|[atPrUsbTD]?)").unwrap(); let mut idx = 0; let mut results = Vec::new(); @@ -286,8 +289,12 @@ impl Format { if let Some(key) = cap.get(2) { results.push(match cap.get(3).unwrap().as_str() { - "i" => FormatText::RequestHeader(key.as_str().to_owned()), - "o" => FormatText::ResponseHeader(key.as_str().to_owned()), + "i" => FormatText::RequestHeader( + HeaderName::try_from(key.as_str()).unwrap(), + ), + "o" => FormatText::ResponseHeader( + HeaderName::try_from(key.as_str()).unwrap(), + ), "e" => FormatText::EnvironHeader(key.as_str().to_owned()), _ => unreachable!(), }) @@ -300,6 +307,7 @@ impl Format { "r" => FormatText::RequestLine, "s" => FormatText::ResponseStatus, "b" => FormatText::ResponseSize, + "U" => FormatText::UrlPath, "T" => FormatText::Time, "D" => FormatText::TimeMillis, _ => FormatText::Str(m.as_str().to_owned()), @@ -328,8 +336,9 @@ pub enum FormatText { Time, TimeMillis, RemoteAddr, - RequestHeader(String), - ResponseHeader(String), + UrlPath, + RequestHeader(HeaderName), + ResponseHeader(HeaderName), EnvironHeader(String), } @@ -354,13 +363,6 @@ impl FormatText { let rt = (rt.num_nanoseconds().unwrap_or(0) as f64) / 1_000_000.0; fmt.write_fmt(format_args!("{:.6}", rt)) } - // FormatText::RemoteAddr => { - // if let Some(remote) = req.connection_info().remote() { - // return remote.fmt(fmt); - // } else { - // "-".fmt(fmt) - // } - // } FormatText::EnvironHeader(ref name) => { if let Ok(val) = env::var(name) { fmt.write_fmt(format_args!("{}", val)) @@ -393,7 +395,7 @@ impl FormatText { } } - fn render_request

(&mut self, now: time::Tm, req: &ServiceRequest

) { + fn render_request(&mut self, now: time::Tm, req: &ServiceRequest) { match *self { FormatText::RequestLine => { *self = if req.query_string().is_empty() { @@ -413,6 +415,7 @@ impl FormatText { )) }; } + FormatText::UrlPath => *self = FormatText::Str(format!("{}", req.path())), FormatText::RequestTime => { *self = FormatText::Str(format!( "{:?}", @@ -431,6 +434,14 @@ impl FormatText { }; *self = FormatText::Str(s.to_string()); } + FormatText::RemoteAddr => { + let s = if let Some(remote) = req.connection_info().remote() { + FormatText::Str(remote.to_string()) + } else { + FormatText::Str("-".to_string()) + }; + *self = s; + } _ => (), } } @@ -454,7 +465,7 @@ mod tests { #[test] fn test_logger() { - let srv = FnService::new(|req: ServiceRequest<_>| { + let srv = FnService::new(|req: ServiceRequest| { req.into_response( HttpResponse::build(StatusCode::OK) .header("X-Test", "ttt") @@ -473,6 +484,37 @@ mod tests { let _res = block_on(srv.call(req)); } + #[test] + fn test_url_path() { + let mut format = Format::new("%T %U"); + let req = TestRequest::with_header( + header::USER_AGENT, + header::HeaderValue::from_static("ACTIX-WEB"), + ) + .uri("/test/route/yeah") + .to_srv_request(); + + let now = time::now(); + for unit in &mut format.0 { + unit.render_request(now, &req); + } + + let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); + for unit in &mut format.0 { + unit.render_response(&resp); + } + + let render = |fmt: &mut Formatter| { + for unit in &format.0 { + unit.render(fmt, 1024, now)?; + } + Ok(()) + }; + let s = format!("{}", FormatDisplay(&render)); + println!("{}", s); + assert!(s.contains("/test/route/yeah")); + } + #[test] fn test_default_format() { let mut format = Format::default(); diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 6b6253fbc..5266f7c1a 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -1,22 +1,16 @@ //! Middlewares -#[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] mod compress; -#[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] -mod decompress; -#[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] -pub mod encoding { - //! Middlewares for compressing/decompressing payloads. - pub use super::compress::{BodyEncoding, Compress}; - pub use super::decompress::Decompress; -} +pub use self::compress::{BodyEncoding, Compress}; pub mod cors; mod defaultheaders; pub mod errhandlers; mod logger; +mod normalize; pub use self::defaultheaders::DefaultHeaders; pub use self::logger::Logger; +pub use self::normalize::NormalizePath; #[cfg(feature = "secure-cookies")] pub mod identity; diff --git a/src/middleware/normalize.rs b/src/middleware/normalize.rs new file mode 100644 index 000000000..060331a6b --- /dev/null +++ b/src/middleware/normalize.rs @@ -0,0 +1,108 @@ +//! `Middleware` to normalize request's URI + +use actix_service::{Service, Transform}; +use futures::future::{self, FutureResult}; +use regex::Regex; + +use crate::service::{ServiceRequest, ServiceResponse}; + +#[derive(Default, Clone, Copy)] +/// `Middleware` to normalize request's URI in place +/// +/// Performs following: +/// +/// - Merges multiple slashes into one. +pub struct NormalizePath; + +impl Transform for NormalizePath +where + S: Service, +{ + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = S::Error; + type InitError = (); + type Transform = NormalizePathNormalization; + type Future = FutureResult; + + fn new_transform(&self, service: S) -> Self::Future { + future::ok(NormalizePathNormalization { + service, + merge_slash: Regex::new("//+").unwrap(), + }) + } +} + +pub struct NormalizePathNormalization { + service: S, + merge_slash: Regex, +} + +impl Service for NormalizePathNormalization +where + S: Service, +{ + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = S::Error; + type Future = S::Future; + + fn poll_ready(&mut self) -> futures::Poll<(), Self::Error> { + self.service.poll_ready() + } + + fn call(&mut self, mut req: ServiceRequest) -> Self::Future { + let head = req.head_mut(); + + let path = head.uri.path(); + let original_len = path.len(); + let path = self.merge_slash.replace_all(path, "/"); + + if original_len != path.len() { + head.uri = path.parse().unwrap(); + } + + self.service.call(req) + } +} + +#[cfg(test)] +mod tests { + use actix_service::FnService; + + use super::*; + use crate::dev::ServiceRequest; + use crate::test::{block_on, TestRequest}; + use crate::HttpResponse; + + #[test] + fn test_in_place_normalization() { + let srv = FnService::new(|req: ServiceRequest| { + assert_eq!("/v1/something/", req.path()); + req.into_response(HttpResponse::Ok().finish()) + }); + + let mut normalize = block_on(NormalizePath.new_transform(srv)).unwrap(); + + let req = TestRequest::with_uri("/v1//something////").to_srv_request(); + let res = block_on(normalize.call(req)).unwrap(); + assert!(res.status().is_success()); + } + + #[test] + fn should_normalize_nothing() { + const URI: &str = "/v1/something/"; + + let srv = FnService::new(|req: ServiceRequest| { + assert_eq!(URI, req.path()); + req.into_response(HttpResponse::Ok().finish()) + }); + + let mut normalize = block_on(NormalizePath.new_transform(srv)).unwrap(); + + let req = TestRequest::with_uri(URI).to_srv_request(); + let res = block_on(normalize.call(req)).unwrap(); + assert!(res.status().is_success()); + } + +} diff --git a/src/request.rs b/src/request.rs index b5ba74122..ad5b2488f 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,42 +1,48 @@ -use std::cell::{Ref, RefMut}; -use std::fmt; +use std::cell::{Ref, RefCell, RefMut}; use std::rc::Rc; +use std::{fmt, net}; use actix_http::http::{HeaderMap, Method, Uri, Version}; use actix_http::{Error, Extensions, HttpMessage, Message, Payload, RequestHead}; use actix_router::{Path, Url}; use crate::config::AppConfig; -use crate::data::Data; +use crate::data::{Data, RouteData}; use crate::error::UrlGenerationError; use crate::extract::FromRequest; use crate::info::ConnectionInfo; use crate::rmap::ResourceMap; -use crate::service::ServiceFromRequest; #[derive(Clone)] /// An HTTP Request -pub struct HttpRequest { +pub struct HttpRequest(pub(crate) Rc); + +pub(crate) struct HttpRequestInner { pub(crate) head: Message, pub(crate) path: Path, rmap: Rc, config: AppConfig, + route_data: Option>, + pool: &'static HttpRequestPool, } impl HttpRequest { #[inline] pub(crate) fn new( - head: Message, path: Path, + head: Message, rmap: Rc, config: AppConfig, + pool: &'static HttpRequestPool, ) -> HttpRequest { - HttpRequest { + HttpRequest(Rc::new(HttpRequestInner { head, path, rmap, config, - } + pool, + route_data: None, + })) } } @@ -44,7 +50,14 @@ impl HttpRequest { /// This method returns reference to the request head #[inline] pub fn head(&self) -> &RequestHead { - &self.head + &self.0.head + } + + /// This method returns muttable reference to the request head. + /// panics if multiple references of http request exists. + #[inline] + pub(crate) fn head_mut(&mut self) -> &mut RequestHead { + &mut Rc::get_mut(&mut self.0).unwrap().head } /// Request's uri. @@ -97,23 +110,12 @@ impl HttpRequest { /// access the matched value for that segment. #[inline] pub fn match_info(&self) -> &Path { - &self.path + &self.0.path } - /// App config #[inline] - pub fn config(&self) -> &AppConfig { - &self.config - } - - /// Get an application data stored with `App::data()` method during - /// application configuration. - pub fn app_data(&self) -> Option> { - if let Some(st) = self.config.extensions().get::>() { - Some(st.clone()) - } else { - None - } + pub(crate) fn match_info_mut(&mut self) -> &mut Path { + &mut Rc::get_mut(&mut self.0).unwrap().path } /// Request extensions @@ -156,7 +158,7 @@ impl HttpRequest { U: IntoIterator, I: AsRef, { - self.rmap.url_for(&self, name, elements) + self.0.rmap.url_for(&self, name, elements) } /// Generate url for named resource @@ -168,10 +170,51 @@ impl HttpRequest { self.url_for(name, &NO_PARAMS) } + /// Peer socket address + /// + /// Peer address is actual 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()` should be used. + #[inline] + pub fn peer_addr(&self) -> Option { + self.head().peer_addr + } + /// Get *ConnectionInfo* for the current request. #[inline] pub fn connection_info(&self) -> Ref { - ConnectionInfo::get(self.head(), &*self.config()) + ConnectionInfo::get(self.head(), &*self.app_config()) + } + + /// App config + #[inline] + pub fn app_config(&self) -> &AppConfig { + &self.0.config + } + + /// Get an application data stored with `App::data()` method during + /// application configuration. + pub fn app_data(&self) -> Option> { + if let Some(st) = self.0.config.extensions().get::>() { + Some(st.clone()) + } else { + None + } + } + + /// Load route data. Route data could be set during + /// route configuration with `Route::data()` method. + pub fn route_data(&self) -> Option<&RouteData> { + if let Some(ref ext) = self.0.route_data { + ext.get::>() + } else { + None + } + } + + pub(crate) fn set_route_data(&mut self, data: Option>) { + Rc::get_mut(&mut self.0).unwrap().route_data = data; } } @@ -187,13 +230,13 @@ impl HttpMessage for HttpRequest { /// Request extensions #[inline] fn extensions(&self) -> Ref { - self.head.extensions() + self.0.head.extensions() } /// Mutable reference to a the request's extensions #[inline] fn extensions_mut(&self) -> RefMut { - self.head.extensions_mut() + self.0.head.extensions_mut() } #[inline] @@ -202,6 +245,17 @@ impl HttpMessage for HttpRequest { } } +impl Drop for HttpRequest { + fn drop(&mut self) { + if Rc::strong_count(&self.0) == 1 { + let v = &mut self.0.pool.0.borrow_mut(); + if v.len() < 128 { + v.push(self.0.clone()); + } + } + } +} + /// It is possible to get `HttpRequest` as an extractor handler parameter /// /// ## Example @@ -222,13 +276,14 @@ impl HttpMessage for HttpRequest { /// ); /// } /// ``` -impl

FromRequest

for HttpRequest { +impl FromRequest for HttpRequest { + type Config = (); type Error = Error; type Future = Result; #[inline] - fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { - Ok(req.request().clone()) + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + Ok(req.clone()) } } @@ -237,8 +292,8 @@ impl fmt::Debug for HttpRequest { writeln!( f, "\nHttpRequest {:?} {}:{}", - self.head.version, - self.head.method, + self.0.head.version, + self.0.head.method, self.path() )?; if !self.query_string().is_empty() { @@ -255,12 +310,32 @@ impl fmt::Debug for HttpRequest { } } +/// Request's objects pool +pub(crate) struct HttpRequestPool(RefCell>>); + +impl HttpRequestPool { + pub(crate) fn create() -> &'static HttpRequestPool { + let pool = HttpRequestPool(RefCell::new(Vec::with_capacity(128))); + Box::leak(Box::new(pool)) + } + + /// Get message from the pool + #[inline] + pub(crate) fn get_request(&self) -> Option { + if let Some(inner) = self.0.borrow_mut().pop() { + Some(HttpRequest(inner)) + } else { + None + } + } +} + #[cfg(test)] mod tests { use super::*; use crate::dev::{ResourceDef, ResourceMap}; use crate::http::{header, StatusCode}; - use crate::test::{call_success, init_service, TestRequest}; + use crate::test::{call_service, init_service, TestRequest}; use crate::{web, App, HttpResponse}; #[test] @@ -286,10 +361,10 @@ mod tests { { 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"); + assert_eq!(cookies[0].name(), "cookie2"); + assert_eq!(cookies[0].value(), "value2"); + assert_eq!(cookies[1].name(), "cookie1"); + assert_eq!(cookies[1].value(), "value1"); } let cookie = req.cookie("cookie1"); @@ -389,7 +464,7 @@ mod tests { )); let req = TestRequest::default().to_request(); - let resp = call_success(&mut srv, req); + let resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::OK); let mut srv = init_service(App::new().data(10u32).service( @@ -403,7 +478,7 @@ mod tests { )); let req = TestRequest::default().to_request(); - let resp = call_success(&mut srv, req); + let resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } } diff --git a/src/resource.rs b/src/resource.rs index 957795cd7..03c614a9d 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -1,4 +1,5 @@ use std::cell::RefCell; +use std::fmt; use std::rc::Rc; use actix_http::{Error, Response}; @@ -9,7 +10,7 @@ use actix_service::{ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; -use crate::dev::{insert_slash, HttpServiceFactory, ResourceDef, ServiceConfig}; +use crate::dev::{insert_slash, AppService, HttpServiceFactory, ResourceDef}; use crate::extract::FromRequest; use crate::guard::Guard; use crate::handler::{AsyncFactory, Factory}; @@ -17,9 +18,8 @@ use crate::responder::Responder; use crate::route::{CreateRouteService, Route, RouteService}; use crate::service::{ServiceRequest, ServiceResponse}; -type HttpService

= BoxedService, ServiceResponse, Error>; -type HttpNewService

= - BoxedNewService<(), ServiceRequest

, ServiceResponse, Error, ()>; +type HttpService = BoxedService; +type HttpNewService = BoxedNewService<(), ServiceRequest, ServiceResponse, Error, ()>; /// *Resource* is an entry in resources table which corresponds to requested URL. /// @@ -43,18 +43,18 @@ type HttpNewService

= /// /// If no matching route could be found, *405* response code get returned. /// Default behavior could be overriden with `default_resource()` method. -pub struct Resource> { +pub struct Resource { endpoint: T, rdef: String, name: Option, - routes: Vec>, + routes: Vec, guards: Vec>, - default: Rc>>>>, - factory_ref: Rc>>>, + default: Rc>>>, + factory_ref: Rc>>, } -impl

Resource

{ - pub fn new(path: &str) -> Resource

{ +impl Resource { + pub fn new(path: &str) -> Resource { let fref = Rc::new(RefCell::new(None)); Resource { @@ -69,11 +69,10 @@ impl

Resource

{ } } -impl Resource +impl Resource where - P: 'static, T: NewService< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), @@ -154,7 +153,7 @@ where /// # fn post_handler() {} /// # fn delete_handler() {} /// ``` - pub fn route(mut self, route: Route

) -> Self { + pub fn route(mut self, route: Route) -> Self { self.routes.push(route.finish()); self } @@ -182,7 +181,7 @@ where pub fn to(mut self, handler: F) -> Self where F: Factory + 'static, - I: FromRequest

+ 'static, + I: FromRequest + 'static, R: Responder + 'static, { self.routes.push(Route::new().to(handler)); @@ -216,9 +215,9 @@ where pub fn to_async(mut self, handler: F) -> Self where F: AsyncFactory, - I: FromRequest

+ 'static, + I: FromRequest + 'static, R: IntoFuture + 'static, - R::Item: Into, + R::Item: Responder, R::Error: Into, { self.routes.push(Route::new().to_async(handler)); @@ -236,9 +235,8 @@ where self, mw: F, ) -> Resource< - P, impl NewService< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), @@ -247,7 +245,7 @@ where where M: Transform< T::Service, - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), @@ -302,54 +300,54 @@ where self, mw: F, ) -> Resource< - P, impl NewService< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), >, > where - F: FnMut(ServiceRequest

, &mut T::Service) -> R + Clone, + F: FnMut(ServiceRequest, &mut T::Service) -> R + Clone, R: IntoFuture, { self.wrap(mw) } - /// Default resource to be used if no matching route could be found. + /// Default service to be used if no matching route could be found. /// By default *405* response get returned. Resource does not use /// default handler from `App` or `Scope`. - pub fn default_resource(mut self, f: F) -> Self + pub fn default_service(mut self, f: F) -> Self where - F: FnOnce(Resource

) -> R, - R: IntoNewService, + F: IntoNewService, U: NewService< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, > + 'static, + U::InitError: fmt::Debug, { // create and configure default resource self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::new_service( - f(Resource::new("")).into_new_service().map_init_err(|_| ()), + f.into_new_service().map_init_err(|e| { + log::error!("Can not construct default service: {:?}", e) + }), ))))); self } } -impl HttpServiceFactory

for Resource +impl HttpServiceFactory for Resource where - P: 'static, T: NewService< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), > + 'static, { - fn register(mut self, config: &mut ServiceConfig

) { + fn register(mut self, config: &mut AppService) { let guards = if self.guards.is_empty() { None } else { @@ -367,10 +365,10 @@ where } } -impl IntoNewService for Resource +impl IntoNewService for Resource where T: NewService< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), @@ -386,18 +384,18 @@ where } } -pub struct ResourceFactory

{ - routes: Vec>, - default: Rc>>>>, +pub struct ResourceFactory { + routes: Vec, + default: Rc>>>, } -impl NewService for ResourceFactory

{ - type Request = ServiceRequest

; +impl NewService for ResourceFactory { + type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; type InitError = (); - type Service = ResourceService

; - type Future = CreateResourceService

; + type Service = ResourceService; + type Future = CreateResourceService; fn new_service(&self, _: &()) -> Self::Future { let default_fut = if let Some(ref default) = *self.default.borrow() { @@ -418,19 +416,19 @@ impl NewService for ResourceFactory

{ } } -enum CreateRouteServiceItem

{ - Future(CreateRouteService

), - Service(RouteService

), +enum CreateRouteServiceItem { + Future(CreateRouteService), + Service(RouteService), } -pub struct CreateResourceService

{ - fut: Vec>, - default: Option>, - default_fut: Option, Error = ()>>>, +pub struct CreateResourceService { + fut: Vec, + default: Option, + default_fut: Option>>, } -impl

Future for CreateResourceService

{ - type Item = ResourceService

; +impl Future for CreateResourceService { + type Item = ResourceService; type Error = (); fn poll(&mut self) -> Poll { @@ -477,63 +475,60 @@ impl

Future for CreateResourceService

{ } } -pub struct ResourceService

{ - routes: Vec>, - default: Option>, +pub struct ResourceService { + routes: Vec, + default: Option, } -impl

Service for ResourceService

{ - type Request = ServiceRequest

; +impl Service for ResourceService { + type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; type Future = Either< + FutureResult, Box>, - Either< - Box>, - FutureResult, - >, >; fn poll_ready(&mut self) -> Poll<(), Self::Error> { Ok(Async::Ready(())) } - fn call(&mut self, mut req: ServiceRequest

) -> Self::Future { + fn call(&mut self, mut req: ServiceRequest) -> Self::Future { for route in self.routes.iter_mut() { if route.check(&mut req) { - return Either::A(route.call(req)); + return route.call(req); } } if let Some(ref mut default) = self.default { - Either::B(Either::A(default.call(req))) + default.call(req) } else { let req = req.into_parts().0; - Either::B(Either::B(ok(ServiceResponse::new( + Either::A(ok(ServiceResponse::new( req, Response::MethodNotAllowed().finish(), - )))) + ))) } } } #[doc(hidden)] -pub struct ResourceEndpoint

{ - factory: Rc>>>, +pub struct ResourceEndpoint { + factory: Rc>>, } -impl

ResourceEndpoint

{ - fn new(factory: Rc>>>) -> Self { +impl ResourceEndpoint { + fn new(factory: Rc>>) -> Self { ResourceEndpoint { factory } } } -impl NewService for ResourceEndpoint

{ - type Request = ServiceRequest

; +impl NewService for ResourceEndpoint { + type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; type InitError = (); - type Service = ResourceService

; - type Future = CreateResourceService

; + type Service = ResourceService; + type Future = CreateResourceService; fn new_service(&self, _: &()) -> Self::Future { self.factory.borrow_mut().as_mut().unwrap().new_service(&()) @@ -550,16 +545,16 @@ mod tests { use crate::http::{header, HeaderValue, Method, StatusCode}; use crate::service::{ServiceRequest, ServiceResponse}; - use crate::test::{call_success, init_service, TestRequest}; + use crate::test::{call_service, init_service, TestRequest}; use crate::{web, App, Error, HttpResponse}; - fn md( - req: ServiceRequest

, + fn md( + req: ServiceRequest, srv: &mut S, ) -> impl IntoFuture, Error = Error> where S: Service< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, >, @@ -582,7 +577,7 @@ mod tests { ), ); let req = TestRequest::with_uri("/test").to_request(); - let resp = call_success(&mut srv, req); + let resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), @@ -608,7 +603,7 @@ mod tests { ), ); let req = TestRequest::with_uri("/test").to_request(); - let resp = call_success(&mut srv, req); + let resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), @@ -623,7 +618,7 @@ mod tests { sleep(Duration::from_millis(100)).then(|_| HttpResponse::Ok()) }))); let req = TestRequest::with_uri("/test").to_request(); - let resp = call_success(&mut srv, req); + let resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::OK); } @@ -634,34 +629,38 @@ mod tests { .service( web::resource("/test").route(web::get().to(|| HttpResponse::Ok())), ) - .default_resource(|r| r.to(|| HttpResponse::BadRequest())), + .default_service(|r: ServiceRequest| { + r.into_response(HttpResponse::BadRequest()) + }), ); let req = TestRequest::with_uri("/test").to_request(); - let resp = call_success(&mut srv, req); + let resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/test") .method(Method::POST) .to_request(); - let resp = call_success(&mut srv, req); + let resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); let mut srv = init_service( App::new().service( web::resource("/test") .route(web::get().to(|| HttpResponse::Ok())) - .default_resource(|r| r.to(|| HttpResponse::BadRequest())), + .default_service(|r: ServiceRequest| { + r.into_response(HttpResponse::BadRequest()) + }), ), ); let req = TestRequest::with_uri("/test").to_request(); - let resp = call_success(&mut srv, req); + let resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/test") .method(Method::POST) .to_request(); - let resp = call_success(&mut srv, req); + let resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } } diff --git a/src/responder.rs b/src/responder.rs index 3e0676289..f7f2a8b3a 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -1,8 +1,12 @@ use actix_http::error::InternalError; -use actix_http::{http::StatusCode, Error, Response, ResponseBuilder}; +use actix_http::http::{ + header::IntoHeaderValue, Error as HttpError, HeaderMap, HeaderName, HttpTryFrom, + StatusCode, +}; +use actix_http::{Error, Response, ResponseBuilder}; use bytes::{Bytes, BytesMut}; use futures::future::{err, ok, Either as EitherFuture, FutureResult}; -use futures::{Future, IntoFuture, Poll}; +use futures::{try_ready, Async, Future, IntoFuture, Poll}; use crate::request::HttpRequest; @@ -18,6 +22,51 @@ pub trait Responder { /// Convert itself to `AsyncResult` or `Error`. fn respond_to(self, req: &HttpRequest) -> Self::Future; + + /// Override a status code for a responder. + /// + /// ```rust + /// use actix_web::{HttpRequest, Responder, http::StatusCode}; + /// + /// fn index(req: HttpRequest) -> impl Responder { + /// "Welcome!".with_status(StatusCode::OK) + /// } + /// # fn main() {} + /// ``` + fn with_status(self, status: StatusCode) -> CustomResponder + where + Self: Sized, + { + CustomResponder::new(self).with_status(status) + } + + /// Add extra header to the responder's response. + /// + /// ```rust + /// use actix_web::{web, HttpRequest, Responder}; + /// use serde::Serialize; + /// + /// #[derive(Serialize)] + /// struct MyObj { + /// name: String, + /// } + /// + /// fn index(req: HttpRequest) -> impl Responder { + /// web::Json( + /// MyObj{name: "Name".to_string()} + /// ) + /// .with_header("x-version", "1.2.3") + /// } + /// # fn main() {} + /// ``` + fn with_header(self, key: K, value: V) -> CustomResponder + where + Self: Sized, + HeaderName: HttpTryFrom, + V: IntoHeaderValue, + { + CustomResponder::new(self).with_header(key, value) + } } impl Responder for Response { @@ -154,6 +203,117 @@ impl Responder for BytesMut { } } +/// Allows to override status code and headers for a responder. +pub struct CustomResponder { + responder: T, + status: Option, + headers: Option, + error: Option, +} + +impl CustomResponder { + fn new(responder: T) -> Self { + CustomResponder { + responder, + status: None, + headers: None, + error: None, + } + } + + /// Override a status code for the responder's response. + /// + /// ```rust + /// use actix_web::{HttpRequest, Responder, http::StatusCode}; + /// + /// fn index(req: HttpRequest) -> impl Responder { + /// "Welcome!".with_status(StatusCode::OK) + /// } + /// # fn main() {} + /// ``` + pub fn with_status(mut self, status: StatusCode) -> Self { + self.status = Some(status); + self + } + + /// Add extra header to the responder's response. + /// + /// ```rust + /// use actix_web::{web, HttpRequest, Responder}; + /// use serde::Serialize; + /// + /// #[derive(Serialize)] + /// struct MyObj { + /// name: String, + /// } + /// + /// fn index(req: HttpRequest) -> impl Responder { + /// web::Json( + /// MyObj{name: "Name".to_string()} + /// ) + /// .with_header("x-version", "1.2.3") + /// } + /// # fn main() {} + /// ``` + pub fn with_header(mut self, key: K, value: V) -> Self + where + HeaderName: HttpTryFrom, + V: IntoHeaderValue, + { + if self.headers.is_none() { + self.headers = Some(HeaderMap::new()); + } + + match HeaderName::try_from(key) { + Ok(key) => match value.try_into() { + Ok(value) => { + self.headers.as_mut().unwrap().append(key, value); + } + Err(e) => self.error = Some(e.into()), + }, + Err(e) => self.error = Some(e.into()), + }; + self + } +} + +impl Responder for CustomResponder { + type Error = T::Error; + type Future = CustomResponderFut; + + fn respond_to(self, req: &HttpRequest) -> Self::Future { + CustomResponderFut { + fut: self.responder.respond_to(req).into_future(), + status: self.status, + headers: self.headers, + } + } +} + +pub struct CustomResponderFut { + fut: ::Future, + status: Option, + headers: Option, +} + +impl Future for CustomResponderFut { + type Item = Response; + type Error = T::Error; + + fn poll(&mut self) -> Poll { + let mut res = try_ready!(self.fut.poll()); + if let Some(status) = self.status { + *res.status_mut() = status; + } + if let Some(ref headers) = self.headers { + for (k, v) in headers { + res.headers_mut().insert(k.clone(), v.clone()); + } + } + Ok(Async::Ready(res)) + } +} + /// Combines two different responder types into a single type /// /// ```rust @@ -307,11 +467,11 @@ pub(crate) mod tests { ); let req = TestRequest::with_uri("/none").to_request(); - let resp = TestRequest::block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let req = TestRequest::with_uri("/some").to_request(); - let resp = TestRequest::block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); match resp.response().body() { ResponseBody::Body(Body::Bytes(ref b)) => { @@ -435,4 +595,33 @@ pub(crate) mod tests { ); assert!(res.is_err()); } + + #[test] + fn test_custom_responder() { + let req = TestRequest::default().to_http_request(); + let res = block_on( + "test" + .to_string() + .with_status(StatusCode::BAD_REQUEST) + .respond_to(&req), + ) + .unwrap(); + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + assert_eq!(res.body().bin_ref(), b"test"); + + let res = block_on( + "test" + .to_string() + .with_header("content-type", "json") + .respond_to(&req), + ) + .unwrap(); + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.body().bin_ref(), b"test"); + assert_eq!( + res.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("json") + ); + } } diff --git a/src/route.rs b/src/route.rs index 7f1cee3d4..8c97d7720 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,9 +1,9 @@ use std::cell::RefCell; -use std::marker::PhantomData; use std::rc::Rc; -use actix_http::{http::Method, Error, Extensions, Response}; +use actix_http::{http::Method, Error, Extensions}; use actix_service::{NewService, Service}; +use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; use crate::data::RouteData; @@ -11,7 +11,7 @@ use crate::extract::FromRequest; use crate::guard::{self, Guard}; use crate::handler::{AsyncFactory, AsyncHandler, Extract, Factory, Handler}; use crate::responder::Responder; -use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; +use crate::service::{ServiceRequest, ServiceResponse}; use crate::HttpResponse; type BoxedRouteService = Box< @@ -19,7 +19,10 @@ type BoxedRouteService = Box< Request = Req, Response = Res, Error = Error, - Future = Box>, + Future = Either< + FutureResult, + Box>, + >, >, >; @@ -38,23 +41,22 @@ type BoxedRouteNewService = Box< /// /// Route uses builder-like pattern for configuration. /// If handler is not explicitly set, default *404 Not Found* handler is used. -pub struct Route

{ - service: BoxedRouteNewService, ServiceResponse>, +pub struct Route { + service: BoxedRouteNewService, guards: Rc>>, data: Option, data_ref: Rc>>>, } -impl Route

{ +impl Route { /// Create new route which matches any request. - pub fn new() -> Route

{ + pub fn new() -> Route { let data_ref = Rc::new(RefCell::new(None)); Route { - service: Box::new(RouteNewService::new( - Extract::new(data_ref.clone()).and_then( - Handler::new(HttpResponse::NotFound).map_err(|_| panic!()), - ), - )), + service: Box::new(RouteNewService::new(Extract::new( + data_ref.clone(), + Handler::new(|| HttpResponse::NotFound()), + ))), guards: Rc::new(Vec::new()), data: None, data_ref, @@ -71,13 +73,13 @@ impl Route

{ } } -impl

NewService for Route

{ - type Request = ServiceRequest

; +impl NewService for Route { + type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; type InitError = (); - type Service = RouteService

; - type Future = CreateRouteService

; + type Service = RouteService; + type Future = CreateRouteService; fn new_service(&self, _: &()) -> Self::Future { CreateRouteService { @@ -87,17 +89,16 @@ impl

NewService for Route

{ } } -type RouteFuture

= Box< - Future, ServiceResponse>, Error = ()>, ->; +type RouteFuture = + Box, Error = ()>>; -pub struct CreateRouteService

{ - fut: RouteFuture

, +pub struct CreateRouteService { + fut: RouteFuture, guards: Rc>>, } -impl

Future for CreateRouteService

{ - type Item = RouteService

; +impl Future for CreateRouteService { + type Item = RouteService; type Error = (); fn poll(&mut self) -> Poll { @@ -111,13 +112,13 @@ impl

Future for CreateRouteService

{ } } -pub struct RouteService

{ - service: BoxedRouteService, ServiceResponse>, +pub struct RouteService { + service: BoxedRouteService, guards: Rc>>, } -impl

RouteService

{ - pub fn check(&self, req: &mut ServiceRequest

) -> bool { +impl RouteService { + pub fn check(&self, req: &mut ServiceRequest) -> bool { for f in self.guards.iter() { if !f.check(req.head()) { return false; @@ -127,22 +128,25 @@ impl

RouteService

{ } } -impl

Service for RouteService

{ - type Request = ServiceRequest

; +impl Service for RouteService { + type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; - type Future = Box>; + type Future = Either< + FutureResult, + Box>, + >; fn poll_ready(&mut self) -> Poll<(), Self::Error> { self.service.poll_ready() } - fn call(&mut self, req: ServiceRequest

) -> Self::Future { + fn call(&mut self, req: ServiceRequest) -> Self::Future { self.service.call(req) } } -impl Route

{ +impl Route { /// Add method guard to the route. /// /// ```rust @@ -229,16 +233,16 @@ impl Route

{ /// ); /// } /// ``` - pub fn to(mut self, handler: F) -> Route

+ pub fn to(mut self, handler: F) -> Route where F: Factory + 'static, - T: FromRequest

+ 'static, + T: FromRequest + 'static, R: Responder + 'static, { - self.service = Box::new(RouteNewService::new( - Extract::new(self.data_ref.clone()) - .and_then(Handler::new(handler).map_err(|_| panic!())), - )); + self.service = Box::new(RouteNewService::new(Extract::new( + self.data_ref.clone(), + Handler::new(handler), + ))); self } @@ -272,15 +276,15 @@ impl Route

{ pub fn to_async(mut self, handler: F) -> Self where F: AsyncFactory, - T: FromRequest

+ 'static, + T: FromRequest + 'static, R: IntoFuture + 'static, - R::Item: Into, + R::Item: Responder, R::Error: Into, { - self.service = Box::new(RouteNewService::new( - Extract::new(self.data_ref.clone()) - .and_then(AsyncHandler::new(handler).map_err(|_| panic!())), - )); + self.service = Box::new(RouteNewService::new(Extract::new( + self.data_ref.clone(), + AsyncHandler::new(handler), + ))); self } @@ -288,7 +292,7 @@ impl Route

{ /// configuration or specific state available via `RouteData` extractor. /// /// ```rust - /// use actix_web::{web, App}; + /// use actix_web::{web, App, FromRequest}; /// /// /// extract text data from request /// fn index(body: String) -> String { @@ -300,13 +304,15 @@ impl Route

{ /// web::resource("/index.html").route( /// web::get() /// // limit size of the payload - /// .data(web::PayloadConfig::new(4096)) + /// .data(String::configure(|cfg| { + /// cfg.limit(4096) + /// })) /// // register handler /// .to(index) /// )); /// } /// ``` - pub fn data(mut self, data: C) -> Self { + pub fn data(mut self, data: T) -> Self { if self.data.is_none() { self.data = Some(Extensions::new()); } @@ -315,49 +321,45 @@ impl Route

{ } } -struct RouteNewService +struct RouteNewService where - T: NewService, Error = (Error, ServiceFromRequest

)>, + T: NewService, { service: T, - _t: PhantomData

, } -impl RouteNewService +impl RouteNewService where T: NewService< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, - Error = (Error, ServiceFromRequest

), + Error = (Error, ServiceRequest), >, T::Future: 'static, T::Service: 'static, ::Future: 'static, { pub fn new(service: T) -> Self { - RouteNewService { - service, - _t: PhantomData, - } + RouteNewService { service } } } -impl NewService for RouteNewService +impl NewService for RouteNewService where T: NewService< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, - Error = (Error, ServiceFromRequest

), + Error = (Error, ServiceRequest), >, T::Future: 'static, T::Service: 'static, ::Future: 'static, { - type Request = ServiceRequest

; + type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; type InitError = (); - type Service = BoxedRouteService, Self::Response>; + type Service = BoxedRouteService; type Future = Box>; fn new_service(&self, _: &()) -> Self::Future { @@ -367,44 +369,48 @@ where .map_err(|_| ()) .and_then(|service| { let service: BoxedRouteService<_, _> = - Box::new(RouteServiceWrapper { - service, - _t: PhantomData, - }); + Box::new(RouteServiceWrapper { service }); Ok(service) }), ) } } -struct RouteServiceWrapper { +struct RouteServiceWrapper { service: T, - _t: PhantomData

, } -impl Service for RouteServiceWrapper +impl Service for RouteServiceWrapper where T::Future: 'static, T: Service< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, - Error = (Error, ServiceFromRequest

), + Error = (Error, ServiceRequest), >, { - type Request = ServiceRequest

; + type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; - type Future = Box>; + type Future = Either< + FutureResult, + Box>, + >; fn poll_ready(&mut self) -> Poll<(), Self::Error> { self.service.poll_ready().map_err(|(e, _)| e) } - fn call(&mut self, req: ServiceRequest

) -> Self::Future { - Box::new(self.service.call(req).then(|res| match res { - Ok(res) => Ok(res), - Err((err, req)) => Ok(req.error_response(err)), - })) + fn call(&mut self, req: ServiceRequest) -> Self::Future { + let mut fut = self.service.call(req); + match fut.poll() { + Ok(Async::Ready(res)) => Either::A(ok(res)), + Err((e, req)) => Either::A(ok(req.error_response(e))), + Ok(Async::NotReady) => Either::B(Box::new(fut.then(|res| match res { + Ok(res) => Ok(res), + Err((err, req)) => Ok(req.error_response(err)), + }))), + } } } @@ -412,18 +418,25 @@ where mod tests { use std::time::Duration; + use bytes::Bytes; use futures::Future; + use serde_derive::Serialize; use tokio_timer::sleep; use crate::http::{Method, StatusCode}; - use crate::test::{call_success, init_service, TestRequest}; + use crate::test::{call_service, init_service, read_body, TestRequest}; use crate::{error, web, App, HttpResponse}; + #[derive(Serialize, PartialEq, Debug)] + struct MyObject { + name: String, + } + #[test] fn test_route() { - let mut srv = - init_service( - App::new().service( + let mut srv = init_service( + App::new() + .service( web::resource("/test") .route(web::get().to(|| HttpResponse::Ok())) .route(web::put().to(|| { @@ -438,37 +451,51 @@ mod tests { Err::(error::ErrorBadRequest("err")) }) })), - ), - ); + ) + .service(web::resource("/json").route(web::get().to_async(|| { + sleep(Duration::from_millis(25)).then(|_| { + Ok::<_, crate::Error>(web::Json(MyObject { + name: "test".to_string(), + })) + }) + }))), + ); let req = TestRequest::with_uri("/test") .method(Method::GET) .to_request(); - let resp = call_success(&mut srv, req); + let resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/test") .method(Method::POST) .to_request(); - let resp = call_success(&mut srv, req); + let resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::CREATED); let req = TestRequest::with_uri("/test") .method(Method::PUT) .to_request(); - let resp = call_success(&mut srv, req); + let resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_uri("/test") .method(Method::DELETE) .to_request(); - let resp = call_success(&mut srv, req); + let resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_uri("/test") .method(Method::HEAD) .to_request(); - let resp = call_success(&mut srv, req); + let resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + + let req = TestRequest::with_uri("/json").to_request(); + let resp = call_service(&mut srv, req); + assert_eq!(resp.status(), StatusCode::OK); + + let body = read_body(resp); + assert_eq!(body, Bytes::from_static(b"{\"name\":\"test\"}")); } } diff --git a/src/scope.rs b/src/scope.rs index 7ad2d95eb..81bf84d23 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,4 +1,5 @@ use std::cell::RefCell; +use std::fmt; use std::rc::Rc; use actix_http::Response; @@ -10,7 +11,7 @@ use actix_service::{ use futures::future::{ok, Either, Future, FutureResult}; use futures::{Async, IntoFuture, Poll}; -use crate::dev::{HttpServiceFactory, ServiceConfig}; +use crate::dev::{AppService, HttpServiceFactory}; use crate::error::Error; use crate::guard::Guard; use crate::resource::Resource; @@ -21,10 +22,12 @@ use crate::service::{ }; type Guards = Vec>; -type HttpService

= BoxedService, ServiceResponse, Error>; -type HttpNewService

= - BoxedNewService<(), ServiceRequest

, ServiceResponse, Error, ()>; -type BoxedResponse = Box>; +type HttpService = BoxedService; +type HttpNewService = BoxedNewService<(), ServiceRequest, ServiceResponse, Error, ()>; +type BoxedResponse = Either< + FutureResult, + Box>, +>; /// Resources scope. /// @@ -55,18 +58,18 @@ type BoxedResponse = Box>; /// * /{project_id}/path2 - `GET` requests /// * /{project_id}/path3 - `HEAD` requests /// -pub struct Scope> { +pub struct Scope { endpoint: T, rdef: String, - services: Vec>>, + services: Vec>, guards: Vec>, - default: Rc>>>>, - factory_ref: Rc>>>, + default: Rc>>>, + factory_ref: Rc>>, } -impl Scope

{ +impl Scope { /// Create a new scope - pub fn new(path: &str) -> Scope

{ + pub fn new(path: &str) -> Scope { let fref = Rc::new(RefCell::new(None)); Scope { endpoint: ScopeEndpoint::new(fref.clone()), @@ -79,11 +82,10 @@ impl Scope

{ } } -impl Scope +impl Scope where - P: 'static, T: NewService< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), @@ -143,7 +145,7 @@ where /// ``` pub fn service(mut self, factory: F) -> Self where - F: HttpServiceFactory

+ 'static, + F: HttpServiceFactory + 'static, { self.services .push(Box::new(ServiceFactoryWrapper::new(factory))); @@ -171,7 +173,7 @@ where /// ); /// } /// ``` - pub fn route(self, path: &str, mut route: Route

) -> Self { + pub fn route(self, path: &str, mut route: Route) -> Self { self.service( Resource::new(path) .add_guards(route.take_guards()) @@ -179,22 +181,24 @@ where ) } - /// Default resource to be used if no matching route could be found. + /// Default service to be used if no matching route could be found. /// /// If default resource is not registered, app's default resource is being used. - pub fn default_resource(mut self, f: F) -> Self + pub fn default_service(mut self, f: F) -> Self where - F: FnOnce(Resource

) -> Resource, + F: IntoNewService, U: NewService< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, - InitError = (), > + 'static, + U::InitError: fmt::Debug, { // create and configure default resource self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::new_service( - f(Resource::new("")).into_new_service().map_init_err(|_| ()), + f.into_new_service().map_init_err(|e| { + log::error!("Can not construct default service: {:?}", e) + }), ))))); self @@ -213,9 +217,8 @@ where self, mw: F, ) -> Scope< - P, impl NewService< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), @@ -224,7 +227,7 @@ where where M: Transform< T::Service, - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), @@ -276,33 +279,31 @@ where self, mw: F, ) -> Scope< - P, impl NewService< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), >, > where - F: FnMut(ServiceRequest

, &mut T::Service) -> R + Clone, + F: FnMut(ServiceRequest, &mut T::Service) -> R + Clone, R: IntoFuture, { self.wrap(mw) } } -impl HttpServiceFactory

for Scope +impl HttpServiceFactory for Scope where - P: 'static, T: NewService< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, InitError = (), > + 'static, { - fn register(self, config: &mut ServiceConfig

) { + fn register(self, config: &mut AppService) { // update default resource if needed if self.default.borrow().is_none() { *self.default.borrow_mut() = Some(config.default_service()); @@ -347,18 +348,18 @@ where } } -pub struct ScopeFactory

{ - services: Rc, RefCell>)>>, - default: Rc>>>>, +pub struct ScopeFactory { + services: Rc>)>>, + default: Rc>>>, } -impl NewService for ScopeFactory

{ - type Request = ServiceRequest

; +impl NewService for ScopeFactory { + type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; type InitError = (); - type Service = ScopeService

; - type Future = ScopeFactoryResponse

; + type Service = ScopeService; + type Future = ScopeFactoryResponse; fn new_service(&self, _: &()) -> Self::Future { let default_fut = if let Some(ref default) = *self.default.borrow() { @@ -387,21 +388,21 @@ impl NewService for ScopeFactory

{ /// Create scope service #[doc(hidden)] -pub struct ScopeFactoryResponse

{ - fut: Vec>, - default: Option>, - default_fut: Option, Error = ()>>>, +pub struct ScopeFactoryResponse { + fut: Vec, + default: Option, + default_fut: Option>>, } -type HttpServiceFut

= Box, Error = ()>>; +type HttpServiceFut = Box>; -enum CreateScopeServiceItem

{ - Future(Option, Option, HttpServiceFut

), - Service(ResourceDef, Option, HttpService

), +enum CreateScopeServiceItem { + Future(Option, Option, HttpServiceFut), + Service(ResourceDef, Option, HttpService), } -impl

Future for ScopeFactoryResponse

{ - type Item = ScopeService

; +impl Future for ScopeFactoryResponse { + type Item = ScopeService; type Error = (); fn poll(&mut self) -> Poll { @@ -462,14 +463,14 @@ impl

Future for ScopeFactoryResponse

{ } } -pub struct ScopeService

{ - router: Router, Vec>>, - default: Option>, - _ready: Option<(ServiceRequest

, ResourceInfo)>, +pub struct ScopeService { + router: Router>>, + default: Option, + _ready: Option<(ServiceRequest, ResourceInfo)>, } -impl

Service for ScopeService

{ - type Request = ServiceRequest

; +impl Service for ScopeService { + type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; type Future = Either>; @@ -478,7 +479,7 @@ impl

Service for ScopeService

{ Ok(Async::Ready(())) } - fn call(&mut self, mut req: ServiceRequest

) -> Self::Future { + fn call(&mut self, mut req: ServiceRequest) -> Self::Future { let res = self.router.recognize_mut_checked(&mut req, |req, guards| { if let Some(ref guards) = guards { for f in guards { @@ -502,23 +503,23 @@ impl

Service for ScopeService

{ } #[doc(hidden)] -pub struct ScopeEndpoint

{ - factory: Rc>>>, +pub struct ScopeEndpoint { + factory: Rc>>, } -impl

ScopeEndpoint

{ - fn new(factory: Rc>>>) -> Self { +impl ScopeEndpoint { + fn new(factory: Rc>>) -> Self { ScopeEndpoint { factory } } } -impl NewService for ScopeEndpoint

{ - type Request = ServiceRequest

; +impl NewService for ScopeEndpoint { + type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; type InitError = (); - type Service = ScopeService

; - type Future = ScopeFactoryResponse

; + type Service = ScopeService; + type Future = ScopeFactoryResponse; fn new_service(&self, _: &()) -> Self::Future { self.factory.borrow_mut().as_mut().unwrap().new_service(&()) @@ -534,7 +535,7 @@ mod tests { use crate::dev::{Body, ResponseBody}; use crate::http::{header, HeaderValue, Method, StatusCode}; use crate::service::{ServiceRequest, ServiceResponse}; - use crate::test::{block_on, call_success, init_service, TestRequest}; + use crate::test::{block_on, call_service, init_service, TestRequest}; use crate::{guard, web, App, Error, HttpRequest, HttpResponse}; #[test] @@ -845,7 +846,9 @@ mod tests { App::new().service( web::scope("/app") .service(web::resource("/path1").to(|| HttpResponse::Ok())) - .default_resource(|r| r.to(|| HttpResponse::BadRequest())), + .default_service(|r: ServiceRequest| { + r.into_response(HttpResponse::BadRequest()) + }), ), ); @@ -862,12 +865,13 @@ mod tests { fn test_default_resource_propagation() { let mut srv = init_service( App::new() - .service( - web::scope("/app1") - .default_resource(|r| r.to(|| HttpResponse::BadRequest())), - ) + .service(web::scope("/app1").default_service( + web::resource("").to(|| HttpResponse::BadRequest()), + )) .service(web::scope("/app2")) - .default_resource(|r| r.to(|| HttpResponse::MethodNotAllowed())), + .default_service(|r: ServiceRequest| { + r.into_response(HttpResponse::MethodNotAllowed()) + }), ); let req = TestRequest::with_uri("/non-exist").to_request(); @@ -883,13 +887,13 @@ mod tests { assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); } - fn md( - req: ServiceRequest

, + fn md( + req: ServiceRequest, srv: &mut S, ) -> impl IntoFuture, Error = Error> where S: Service< - Request = ServiceRequest

, + Request = ServiceRequest, Response = ServiceResponse, Error = Error, >, @@ -908,7 +912,7 @@ mod tests { web::resource("/test").route(web::get().to(|| HttpResponse::Ok())), ))); let req = TestRequest::with_uri("/app/test").to_request(); - let resp = call_success(&mut srv, req); + let resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), @@ -934,7 +938,7 @@ mod tests { ), ); let req = TestRequest::with_uri("/app/test").to_request(); - let resp = call_success(&mut srv, req); + let resp = call_service(&mut srv, req); assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), diff --git a/src/server.rs b/src/server.rs index 2817f549c..efc70773f 100644 --- a/src/server.rs +++ b/src/server.rs @@ -2,7 +2,7 @@ use std::marker::PhantomData; use std::sync::Arc; use std::{fmt, io, net}; -use actix_http::{body::MessageBody, HttpService, KeepAlive, Request, Response}; +use actix_http::{body::MessageBody, Error, HttpService, KeepAlive, Request, Response}; use actix_rt::System; use actix_server::{Server, ServerBuilder}; use actix_server_config::ServerConfig; @@ -53,7 +53,8 @@ where F: Fn() -> I + Send + Clone + 'static, I: IntoNewService, S: NewService, - S::Error: fmt::Debug, + S::Error: Into, + S::InitError: fmt::Debug, S::Response: Into>, S::Service: 'static, B: MessageBody, @@ -72,7 +73,8 @@ where F: Fn() -> I + Send + Clone + 'static, I: IntoNewService, S: NewService, - S::Error: fmt::Debug + 'static, + S::Error: Into, + S::InitError: fmt::Debug, S::Response: Into>, S::Service: 'static, B: MessageBody + 'static, @@ -442,7 +444,8 @@ where F: Fn() -> I + Send + Clone + 'static, I: IntoNewService, S: NewService, - S::Error: fmt::Debug, + S::Error: Into, + S::InitError: fmt::Debug, S::Response: Into>, S::Service: 'static, B: MessageBody, diff --git a/src/service.rs b/src/service.rs index 13aae8692..396daab4b 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,88 +1,71 @@ use std::cell::{Ref, RefMut}; -use std::fmt; -use std::marker::PhantomData; -use std::rc::Rc; +use std::{fmt, net}; use actix_http::body::{Body, MessageBody, ResponseBody}; use actix_http::http::{HeaderMap, Method, StatusCode, Uri, Version}; use actix_http::{ - Error, Extensions, HttpMessage, Payload, PayloadStream, Request, RequestHead, - Response, ResponseHead, + Error, Extensions, HttpMessage, Payload, PayloadStream, RequestHead, Response, + ResponseHead, }; use actix_router::{Path, Resource, Url}; use futures::future::{ok, FutureResult, IntoFuture}; -use crate::config::{AppConfig, ServiceConfig}; -use crate::data::RouteData; +use crate::config::{AppConfig, AppService}; +use crate::data::Data; +use crate::info::ConnectionInfo; use crate::request::HttpRequest; -use crate::rmap::ResourceMap; -pub trait HttpServiceFactory

{ - fn register(self, config: &mut ServiceConfig

); +pub trait HttpServiceFactory { + fn register(self, config: &mut AppService); } -pub(crate) trait ServiceFactory

{ - fn register(&mut self, config: &mut ServiceConfig

); +pub(crate) trait ServiceFactory { + fn register(&mut self, config: &mut AppService); } -pub(crate) struct ServiceFactoryWrapper { +pub(crate) struct ServiceFactoryWrapper { factory: Option, - _t: PhantomData

, } -impl ServiceFactoryWrapper { +impl ServiceFactoryWrapper { pub fn new(factory: T) -> Self { Self { factory: Some(factory), - _t: PhantomData, } } } -impl ServiceFactory

for ServiceFactoryWrapper +impl ServiceFactory for ServiceFactoryWrapper where - T: HttpServiceFactory

, + T: HttpServiceFactory, { - fn register(&mut self, config: &mut ServiceConfig

) { + fn register(&mut self, config: &mut AppService) { if let Some(item) = self.factory.take() { item.register(config) } } } -pub struct ServiceRequest

{ +pub struct ServiceRequest { req: HttpRequest, - payload: Payload

, + payload: Payload, } -impl

ServiceRequest

{ - pub(crate) fn new( - path: Path, - request: Request

, - rmap: Rc, - config: AppConfig, - ) -> Self { - let (head, payload) = request.into_parts(); - ServiceRequest { - payload, - req: HttpRequest::new(head, path, rmap, config), - } - } - +impl ServiceRequest { /// Construct service request from parts - pub fn from_parts(req: HttpRequest, payload: Payload

) -> Self { + pub(crate) fn from_parts(req: HttpRequest, payload: Payload) -> Self { ServiceRequest { req, payload } } /// Deconstruct request into parts - pub fn into_parts(self) -> (HttpRequest, Payload

) { + pub fn into_parts(self) -> (HttpRequest, Payload) { (self.req, self.payload) } /// Create service response #[inline] - pub fn into_response(self, res: Response) -> ServiceResponse { - ServiceResponse::new(self.req, res) + pub fn into_response>>(self, res: R) -> ServiceResponse { + ServiceResponse::new(self.req, res.into()) } /// Create service response for error @@ -95,13 +78,13 @@ impl

ServiceRequest

{ /// This method returns reference to the request head #[inline] pub fn head(&self) -> &RequestHead { - &self.req.head + &self.req.head() } /// This method returns reference to the request head #[inline] pub fn head_mut(&mut self) -> &mut RequestHead { - &mut self.req.head + self.req.head_mut() } /// Request's uri. @@ -152,6 +135,23 @@ impl

ServiceRequest

{ } } + /// Peer socket address + /// + /// Peer address is actual 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 `ConnectionInfo` should be used. + #[inline] + pub fn peer_addr(&self) -> Option { + self.head().peer_addr + } + + /// Get *ConnectionInfo* for the current request. + #[inline] + pub fn connection_info(&self) -> Ref { + ConnectionInfo::get(self.head(), &*self.app_config()) + } + /// Get a reference to the Path parameters. /// /// Params is a container for url parameters. @@ -160,29 +160,39 @@ impl

ServiceRequest

{ /// access the matched value for that segment. #[inline] pub fn match_info(&self) -> &Path { - &self.req.path + self.req.match_info() } #[inline] pub fn match_info_mut(&mut self) -> &mut Path { - &mut self.req.path + self.req.match_info_mut() } /// Service configuration #[inline] pub fn app_config(&self) -> &AppConfig { - self.req.config() + self.req.app_config() + } + + /// Get an application data stored with `App::data()` method during + /// application configuration. + pub fn app_data(&self) -> Option> { + if let Some(st) = self.req.app_config().extensions().get::>() { + Some(st.clone()) + } else { + None + } } } -impl

Resource for ServiceRequest

{ +impl Resource for ServiceRequest { fn resource_path(&mut self) -> &mut Path { self.match_info_mut() } } -impl

HttpMessage for ServiceRequest

{ - type Stream = P; +impl HttpMessage for ServiceRequest { + type Stream = PayloadStream; #[inline] /// Returns Request's headers. @@ -193,13 +203,13 @@ impl

HttpMessage for ServiceRequest

{ /// Request extensions #[inline] fn extensions(&self) -> Ref { - self.req.head.extensions() + self.req.extensions() } /// Mutable reference to a the request's extensions #[inline] fn extensions_mut(&self) -> RefMut { - self.req.head.extensions_mut() + self.req.extensions_mut() } #[inline] @@ -208,7 +218,7 @@ impl

HttpMessage for ServiceRequest

{ } } -impl

fmt::Debug for ServiceRequest

{ +impl fmt::Debug for ServiceRequest { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!( f, @@ -231,82 +241,6 @@ impl

fmt::Debug for ServiceRequest

{ } } -pub struct ServiceFromRequest

{ - req: HttpRequest, - payload: Payload

, - data: Option>, -} - -impl

ServiceFromRequest

{ - pub(crate) fn new(req: ServiceRequest

, data: Option>) -> Self { - Self { - req: req.req, - payload: req.payload, - data, - } - } - - #[inline] - /// Get reference to inner HttpRequest - pub fn request(&self) -> &HttpRequest { - &self.req - } - - #[inline] - /// Convert this request into a HttpRequest - pub fn into_request(self) -> HttpRequest { - self.req - } - - #[inline] - /// Get match information for this request - pub fn match_info_mut(&mut self) -> &mut Path { - &mut self.req.path - } - - /// Create service response for error - #[inline] - pub fn error_response>(self, err: E) -> ServiceResponse { - ServiceResponse::new(self.req, err.into().into()) - } - - /// Load route data. Route data could be set during - /// route configuration with `Route::data()` method. - pub fn route_data(&self) -> Option<&RouteData> { - if let Some(ref ext) = self.data { - ext.get::>() - } else { - None - } - } -} - -impl

HttpMessage for ServiceFromRequest

{ - type Stream = P; - - #[inline] - fn headers(&self) -> &HeaderMap { - self.req.headers() - } - - /// Request extensions - #[inline] - fn extensions(&self) -> Ref { - self.req.head.extensions() - } - - /// Mutable reference to a the request's extensions - #[inline] - fn extensions_mut(&self) -> RefMut { - self.req.head.extensions_mut() - } - - #[inline] - fn take_payload(&mut self) -> Payload { - std::mem::replace(&mut self.payload, Payload::None) - } -} - pub struct ServiceResponse { request: HttpRequest, response: Response, @@ -441,7 +375,7 @@ impl fmt::Debug for ServiceResponse { for (key, val) in self.response.head().headers.iter() { let _ = writeln!(f, " {:?}: {:?}", key, val); } - let _ = writeln!(f, " body: {:?}", self.response.body().length()); + let _ = writeln!(f, " body: {:?}", self.response.body().size()); res } } diff --git a/src/test.rs b/src/test.rs index 209edac54..1f3a24271 100644 --- a/src/test.rs +++ b/src/test.rs @@ -4,21 +4,29 @@ use std::rc::Rc; use actix_http::cookie::Cookie; use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; -use actix_http::http::{HttpTryFrom, Method, StatusCode, Version}; +use actix_http::http::{HttpTryFrom, Method, StatusCode, Uri, Version}; use actix_http::test::TestRequest as HttpTestRequest; -use actix_http::{Extensions, PayloadStream, Request}; +use actix_http::{Extensions, Request}; use actix_router::{Path, ResourceDef, Url}; use actix_rt::Runtime; use actix_server_config::ServerConfig; use actix_service::{FnService, IntoNewService, NewService, Service}; -use bytes::Bytes; -use futures::future::{lazy, Future}; +use bytes::{Bytes, BytesMut}; +use futures::{ + future::{lazy, ok, Future}, + stream::Stream, +}; +use serde::de::DeserializeOwned; +use serde_json; + +pub use actix_http::test::TestBuffer; use crate::config::{AppConfig, AppConfigInner}; -use crate::data::RouteData; -use crate::dev::Body; +use crate::data::{Data, RouteData}; +use crate::dev::{Body, MessageBody, Payload}; +use crate::request::HttpRequestPool; use crate::rmap::ResourceMap; -use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; +use crate::service::{ServiceRequest, ServiceResponse}; use crate::{Error, HttpRequest, HttpResponse}; thread_local! { @@ -50,30 +58,25 @@ where /// This function panics on nested call. pub fn run_on(f: F) -> R where - F: Fn() -> R, + F: FnOnce() -> R, { RT.with(move |rt| rt.borrow_mut().block_on(lazy(|| Ok::<_, ()>(f())))) .unwrap() } /// Create service that always responds with `HttpResponse::Ok()` -pub fn ok_service() -> impl Service< - Request = ServiceRequest, - Response = ServiceResponse, - Error = Error, -> { +pub fn ok_service( +) -> impl Service, Error = Error> +{ default_service(StatusCode::OK) } /// Create service that responds with response with specified status code pub fn default_service( status_code: StatusCode, -) -> impl Service< - Request = ServiceRequest, - Response = ServiceResponse, - Error = Error, -> { - FnService::new(move |req: ServiceRequest| { +) -> impl Service, Error = Error> +{ + FnService::new(move |req: ServiceRequest| { req.into_response(HttpResponse::build(status_code).finish()) }) } @@ -81,11 +84,12 @@ pub fn default_service( /// This method accepts application builder instance, and constructs /// service. /// -/// ```rust,ignore +/// ```rust /// use actix_service::Service; /// use actix_web::{test, web, App, HttpResponse, http::StatusCode}; /// -/// fn main() { +/// #[test] +/// fn test_init_service() { /// let mut app = test::init_service( /// App::new() /// .service(web::resource("/test").to(|| HttpResponse::Ok())) @@ -113,16 +117,19 @@ where S::InitError: std::fmt::Debug, { let cfg = ServerConfig::new("127.0.0.1:8080".parse().unwrap()); - block_on(app.into_new_service().new_service(&cfg)).unwrap() + let srv = app.into_new_service(); + let fut = run_on(move || srv.new_service(&cfg)); + block_on(fut).unwrap() } /// Calls service and waits for response future completion. /// -/// ```rust,ignore +/// ```rust /// use actix_web::{test, App, HttpResponse, http::StatusCode}; /// use actix_service::Service; /// -/// fn main() { +/// #[test] +/// fn test_response() { /// let mut app = test::init_service( /// App::new() /// .service(web::resource("/test").to(|| HttpResponse::Ok())) @@ -132,16 +139,155 @@ where /// let req = test::TestRequest::with_uri("/test").to_request(); /// /// // Call application -/// let resp = test::call_succ_service(&mut app, req); +/// let resp = test::call_service(&mut app, req); /// assert_eq!(resp.status(), StatusCode::OK); /// } /// ``` -pub fn call_success(app: &mut S, req: R) -> S::Response +pub fn call_service(app: &mut S, req: R) -> S::Response where S: Service, Error = E>, E: std::fmt::Debug, { - block_on(app.call(req)).unwrap() + block_on(run_on(move || app.call(req))).unwrap() +} + +/// Helper function that returns a response body of a TestRequest +/// This function blocks the current thread until futures complete. +/// +/// ```rust +/// use actix_web::{test, web, App, HttpResponse, http::header}; +/// use bytes::Bytes; +/// +/// #[test] +/// fn test_index() { +/// let mut app = test::init_service( +/// App::new().service( +/// web::resource("/index.html") +/// .route(web::post().to( +/// || HttpResponse::Ok().body("welcome!"))))); +/// +/// let req = test::TestRequest::post() +/// .uri("/index.html") +/// .header(header::CONTENT_TYPE, "application/json") +/// .to_request(); +/// +/// let result = test::read_response(&mut app, req); +/// assert_eq!(result, Bytes::from_static(b"welcome!")); +/// } +/// ``` +pub fn read_response(app: &mut S, req: Request) -> Bytes +where + S: Service, Error = Error>, + B: MessageBody, +{ + block_on(run_on(move || { + app.call(req).and_then(|mut resp: ServiceResponse| { + resp.take_body() + .fold(BytesMut::new(), move |mut body, chunk| { + body.extend_from_slice(&chunk); + Ok::<_, Error>(body) + }) + .map(|body: BytesMut| body.freeze()) + }) + })) + .unwrap_or_else(|_| panic!("read_response failed at block_on unwrap")) +} + +/// Helper function that returns a response body of a ServiceResponse. +/// This function blocks the current thread until futures complete. +/// +/// ```rust +/// use actix_web::{test, web, App, HttpResponse, http::header}; +/// use bytes::Bytes; +/// +/// #[test] +/// fn test_index() { +/// let mut app = test::init_service( +/// App::new().service( +/// web::resource("/index.html") +/// .route(web::post().to( +/// || HttpResponse::Ok().body("welcome!"))))); +/// +/// let req = test::TestRequest::post() +/// .uri("/index.html") +/// .header(header::CONTENT_TYPE, "application/json") +/// .to_request(); +/// +/// let resp = call_service(&mut srv, req); +/// let result = test::read_body(resp); +/// assert_eq!(result, Bytes::from_static(b"welcome!")); +/// } +/// ``` +pub fn read_body(mut res: ServiceResponse) -> Bytes +where + B: MessageBody, +{ + block_on(run_on(move || { + res.take_body() + .fold(BytesMut::new(), move |mut body, chunk| { + body.extend_from_slice(&chunk); + Ok::<_, Error>(body) + }) + .map(|body: BytesMut| body.freeze()) + })) + .unwrap_or_else(|_| panic!("read_response failed at block_on unwrap")) +} + +/// Helper function that returns a deserialized response body of a TestRequest +/// This function blocks the current thread until futures complete. +/// +/// ```rust +/// use actix_web::{App, test, web, HttpResponse, http::header}; +/// use serde::{Serialize, Deserialize}; +/// +/// #[derive(Serialize, Deserialize)] +/// pub struct Person { +/// id: String, +/// name: String +/// } +/// +/// #[test] +/// fn test_add_person() { +/// let mut app = test::init_service( +/// App::new().service( +/// web::resource("/people") +/// .route(web::post().to(|person: web::Json| { +/// HttpResponse::Ok() +/// .json(person.into_inner())}) +/// ))); +/// +/// let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); +/// +/// let req = test::TestRequest::post() +/// .uri("/people") +/// .header(header::CONTENT_TYPE, "application/json") +/// .set_payload(payload) +/// .to_request(); +/// +/// let result: Person = test::read_response_json(&mut app, req); +/// } +/// ``` +pub fn read_response_json(app: &mut S, req: Request) -> T +where + S: Service, Error = Error>, + B: MessageBody, + T: DeserializeOwned, +{ + block_on(run_on(move || { + app.call(req).and_then(|mut resp: ServiceResponse| { + resp.take_body() + .fold(BytesMut::new(), move |mut body, chunk| { + body.extend_from_slice(&chunk); + Ok::<_, Error>(body) + }) + .and_then(|body: BytesMut| { + ok(serde_json::from_slice(&body).unwrap_or_else(|_| { + panic!("read_response_json failed during deserialization") + })) + }) + }) + })) + .unwrap_or_else(|_| panic!("read_response_json failed at block_on unwrap")) } /// Test `Request` builder. @@ -153,7 +299,7 @@ where /// * `TestRequest::to_from` creates `ServiceFromRequest` instance, which is used for testing extractors. /// * `TestRequest::to_http_request` creates `HttpRequest` instance, which is used for testing handlers. /// -/// ```rust,ignore +/// ```rust /// # use futures::IntoFuture; /// use actix_web::{test, HttpRequest, HttpResponse, HttpMessage}; /// use actix_web::http::{header, StatusCode}; @@ -166,7 +312,8 @@ where /// } /// } /// -/// fn main() { +/// #[test] +/// fn test_index() { /// let req = test::TestRequest::with_header("content-type", "text/plain") /// .to_http_request(); /// @@ -183,6 +330,7 @@ pub struct TestRequest { rmap: ResourceMap, config: AppConfigInner, route_data: Extensions, + path: Path, } impl Default for TestRequest { @@ -192,6 +340,7 @@ impl Default for TestRequest { rmap: ResourceMap::new(ResourceDef::new("")), config: AppConfigInner::default(), route_data: Extensions::new(), + path: Path::new(Url::new(Uri::default())), } } } @@ -200,22 +349,12 @@ impl Default for TestRequest { impl TestRequest { /// Create TestRequest and set request uri pub fn with_uri(path: &str) -> TestRequest { - TestRequest { - req: HttpTestRequest::default().uri(path).take(), - rmap: ResourceMap::new(ResourceDef::new("")), - config: AppConfigInner::default(), - route_data: Extensions::new(), - } + TestRequest::default().uri(path) } /// Create TestRequest and set header pub fn with_hdr(hdr: H) -> TestRequest { - TestRequest { - req: HttpTestRequest::default().set(hdr).take(), - config: AppConfigInner::default(), - rmap: ResourceMap::new(ResourceDef::new("")), - route_data: Extensions::new(), - } + TestRequest::default().set(hdr) } /// Create TestRequest and set header @@ -224,32 +363,27 @@ impl TestRequest { HeaderName: HttpTryFrom, V: IntoHeaderValue, { - TestRequest { - req: HttpTestRequest::default().header(key, value).take(), - config: AppConfigInner::default(), - rmap: ResourceMap::new(ResourceDef::new("")), - route_data: Extensions::new(), - } + TestRequest::default().header(key, value) } /// Create TestRequest and set method to `Method::GET` pub fn get() -> TestRequest { - TestRequest { - req: HttpTestRequest::default().method(Method::GET).take(), - config: AppConfigInner::default(), - rmap: ResourceMap::new(ResourceDef::new("")), - route_data: Extensions::new(), - } + TestRequest::default().method(Method::GET) } /// Create TestRequest and set method to `Method::POST` pub fn post() -> TestRequest { - TestRequest { - req: HttpTestRequest::default().method(Method::POST).take(), - config: AppConfigInner::default(), - rmap: ResourceMap::new(ResourceDef::new("")), - route_data: Extensions::new(), - } + TestRequest::default().method(Method::POST) + } + + /// Create TestRequest and set method to `Method::PUT` + pub fn put() -> TestRequest { + TestRequest::default().method(Method::PUT) + } + + /// Create TestRequest and set method to `Method::PATCH` + pub fn patch() -> TestRequest { + TestRequest::default().method(Method::PATCH) } /// Set HTTP version of this request @@ -292,6 +426,12 @@ impl TestRequest { self } + /// Set request path pattern parameter + pub fn param(mut self, name: &'static str, value: &'static str) -> Self { + self.path.add_static(name, value); + self + } + /// Set request payload pub fn set_payload>(mut self, data: B) -> Self { self.req.set_payload(data); @@ -301,7 +441,7 @@ impl TestRequest { /// Set application data. This is equivalent of `App::data()` method /// for testing purpose. pub fn app_data(self, data: T) -> Self { - self.config.extensions.borrow_mut().insert(data); + self.config.extensions.borrow_mut().insert(Data::new(data)); self } @@ -319,16 +459,25 @@ impl TestRequest { self } - /// Complete request creation and generate `ServiceRequest` instance - pub fn to_srv_request(mut self) -> ServiceRequest { - let req = self.req.finish(); + /// Complete request creation and generate `Request` instance + pub fn to_request(mut self) -> Request { + self.req.finish() + } - ServiceRequest::new( - Path::new(Url::new(req.uri().clone())), - req, + /// Complete request creation and generate `ServiceRequest` instance + pub fn to_srv_request(mut self) -> ServiceRequest { + let (head, payload) = self.req.finish().into_parts(); + self.path.get_mut().update(&head.uri); + + let req = HttpRequest::new( + self.path, + head, Rc::new(self.rmap), AppConfig::new(self.config), - ) + HttpRequestPool::create(), + ); + + ServiceRequest::from_parts(req, payload) } /// Complete request creation and generate `ServiceResponse` instance @@ -336,52 +485,132 @@ impl TestRequest { self.to_srv_request().into_response(res) } - /// Complete request creation and generate `Request` instance - pub fn to_request(mut self) -> Request { - self.req.finish() - } - /// Complete request creation and generate `HttpRequest` instance pub fn to_http_request(mut self) -> HttpRequest { - let req = self.req.finish(); + let (head, _) = self.req.finish().into_parts(); + self.path.get_mut().update(&head.uri); - ServiceRequest::new( - Path::new(Url::new(req.uri().clone())), - req, - Rc::new(self.rmap), - AppConfig::new(self.config), - ) - .into_parts() - .0 - } - - /// Complete request creation and generate `ServiceFromRequest` instance - pub fn to_from(mut self) -> ServiceFromRequest { - let req = self.req.finish(); - - let req = ServiceRequest::new( - Path::new(Url::new(req.uri().clone())), - req, + let mut req = HttpRequest::new( + self.path, + head, Rc::new(self.rmap), AppConfig::new(self.config), + HttpRequestPool::create(), ); - ServiceFromRequest::new(req, Some(Rc::new(self.route_data))) + req.set_route_data(Some(Rc::new(self.route_data))); + req } - /// Runs the provided future, blocking the current thread until the future - /// completes. - /// - /// This function can be used to synchronously block the current thread - /// until the provided `future` has resolved either successfully or with an - /// error. The result of the future is then returned from this function - /// call. - /// - /// Note that this function is intended to be used only for testing purpose. - /// This function panics on nested call. - pub fn block_on(f: F) -> Result - where - F: Future, - { - block_on(f) + /// Complete request creation and generate `HttpRequest` and `Payload` instances + pub fn to_http_parts(mut self) -> (HttpRequest, Payload) { + let (head, payload) = self.req.finish().into_parts(); + self.path.get_mut().update(&head.uri); + + let mut req = HttpRequest::new( + self.path, + head, + Rc::new(self.rmap), + AppConfig::new(self.config), + HttpRequestPool::create(), + ); + req.set_route_data(Some(Rc::new(self.route_data))); + (req, payload) + } +} + +#[cfg(test)] +mod tests { + use serde::{Deserialize, Serialize}; + use std::time::SystemTime; + + use super::*; + use crate::{http::header, web, App, HttpResponse}; + + #[test] + fn test_basics() { + let req = TestRequest::with_hdr(header::ContentType::json()) + .version(Version::HTTP_2) + .set(header::Date(SystemTime::now().into())) + .param("test", "123") + .app_data(10u32) + .to_http_request(); + assert!(req.headers().contains_key(header::CONTENT_TYPE)); + assert!(req.headers().contains_key(header::DATE)); + assert_eq!(&req.match_info()["test"], "123"); + assert_eq!(req.version(), Version::HTTP_2); + let data = req.app_data::().unwrap(); + assert_eq!(*data, 10); + assert_eq!(*data.get_ref(), 10); + } + + #[test] + fn test_request_methods() { + let mut app = init_service( + App::new().service( + web::resource("/index.html") + .route(web::put().to(|| HttpResponse::Ok().body("put!"))) + .route(web::patch().to(|| HttpResponse::Ok().body("patch!"))), + ), + ); + + let put_req = TestRequest::put() + .uri("/index.html") + .header(header::CONTENT_TYPE, "application/json") + .to_request(); + + let result = read_response(&mut app, put_req); + assert_eq!(result, Bytes::from_static(b"put!")); + + let patch_req = TestRequest::patch() + .uri("/index.html") + .header(header::CONTENT_TYPE, "application/json") + .to_request(); + + let result = read_response(&mut app, patch_req); + assert_eq!(result, Bytes::from_static(b"patch!")); + } + + #[test] + fn test_response() { + let mut app = init_service( + App::new().service( + web::resource("/index.html") + .route(web::post().to(|| HttpResponse::Ok().body("welcome!"))), + ), + ); + + let req = TestRequest::post() + .uri("/index.html") + .header(header::CONTENT_TYPE, "application/json") + .to_request(); + + let result = read_response(&mut app, req); + assert_eq!(result, Bytes::from_static(b"welcome!")); + } + + #[derive(Serialize, Deserialize)] + pub struct Person { + id: String, + name: String, + } + + #[test] + fn test_response_json() { + let mut app = init_service(App::new().service(web::resource("/people").route( + web::post().to(|person: web::Json| { + HttpResponse::Ok().json(person.into_inner()) + }), + ))); + + let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); + + let req = TestRequest::post() + .uri("/people") + .header(header::CONTENT_TYPE, "application/json") + .set_payload(payload) + .to_request(); + + let result: Person = read_response_json(&mut app, req); + assert_eq!(&result.id, "12345"); } } diff --git a/src/types/form.rs b/src/types/form.rs index 812a08e52..e8f78c496 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -3,20 +3,19 @@ use std::rc::Rc; use std::{fmt, ops}; -use actix_http::error::{Error, PayloadError}; -use actix_http::{HttpMessage, Payload}; -use bytes::{Bytes, BytesMut}; +use actix_http::{Error, HttpMessage, Payload}; +use bytes::BytesMut; use encoding::all::UTF_8; use encoding::types::{DecoderTrap, Encoding}; use encoding::EncodingRef; use futures::{Future, Poll, Stream}; use serde::de::DeserializeOwned; +use crate::dev::Decompress; use crate::error::UrlencodedError; use crate::extract::FromRequest; use crate::http::header::CONTENT_LENGTH; use crate::request::HttpRequest; -use crate::service::ServiceFromRequest; #[derive(PartialEq, Eq, PartialOrd, Ord)] /// Extract typed information from the request's body. @@ -70,24 +69,24 @@ impl ops::DerefMut for Form { } } -impl FromRequest

for Form +impl FromRequest for Form where T: DeserializeOwned + 'static, - P: Stream + 'static, { + type Config = FormConfig; type Error = Error; type Future = Box>; #[inline] - fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { - let req2 = req.request().clone(); + fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { + let req2 = req.clone(); let (limit, err) = req .route_data::() .map(|c| (c.limit, c.ehandler.clone())) .unwrap_or((16384, None)); Box::new( - UrlEncoded::new(req) + UrlEncoded::new(req, payload) .limit(limit) .map_err(move |e| { if let Some(err) = err { @@ -117,7 +116,7 @@ impl fmt::Display for Form { /// /// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App, Result}; +/// use actix_web::{web, App, FromRequest, Result}; /// /// #[derive(Deserialize)] /// struct FormData { @@ -135,7 +134,9 @@ impl fmt::Display for Form { /// web::resource("/index.html") /// .route(web::get() /// // change `Form` extractor configuration -/// .data(web::FormConfig::default().limit(4097)) +/// .data( +/// web::Form::::configure(|cfg| cfg.limit(4097)) +/// ) /// .to(index)) /// ); /// } @@ -183,8 +184,8 @@ impl Default for FormConfig { /// * content type is not `application/x-www-form-urlencoded` /// * content-length is greater than 32k /// -pub struct UrlEncoded { - stream: Payload, +pub struct UrlEncoded { + stream: Option>, limit: usize, length: Option, encoding: EncodingRef, @@ -192,13 +193,9 @@ pub struct UrlEncoded { fut: Option>>, } -impl UrlEncoded -where - T: HttpMessage, - T::Stream: Stream, -{ +impl UrlEncoded { /// Create a new future to URL encode a request - pub fn new(req: &mut T) -> UrlEncoded { + pub fn new(req: &HttpRequest, payload: &mut Payload) -> UrlEncoded { // check content type if req.content_type().to_lowercase() != "application/x-www-form-urlencoded" { return Self::err(UrlencodedError::ContentType); @@ -209,7 +206,7 @@ where }; let mut len = None; - if let Some(l) = req.headers().get(CONTENT_LENGTH) { + if let Some(l) = req.headers().get(&CONTENT_LENGTH) { if let Ok(s) = l.to_str() { if let Ok(l) = s.parse::() { len = Some(l) @@ -221,9 +218,10 @@ where } }; + let payload = Decompress::from_headers(payload.take(), req.headers()); UrlEncoded { encoding, - stream: req.take_payload(), + stream: Some(payload), limit: 32_768, length: len, fut: None, @@ -233,7 +231,7 @@ where fn err(e: UrlencodedError) -> Self { UrlEncoded { - stream: Payload::None, + stream: None, limit: 32_768, fut: None, err: Some(e), @@ -249,10 +247,8 @@ where } } -impl Future for UrlEncoded +impl Future for UrlEncoded where - T: HttpMessage, - T::Stream: Stream + 'static, U: DeserializeOwned + 'static, { type Item = U; @@ -277,7 +273,10 @@ where // future let encoding = self.encoding; - let fut = std::mem::replace(&mut self.stream, Payload::None) + let fut = self + .stream + .take() + .unwrap() .from_err() .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { if (body.len() + chunk.len()) > limit { @@ -320,22 +319,18 @@ mod tests { #[test] fn test_form() { - let mut req = + let (req, mut pl) = TestRequest::with_header(CONTENT_TYPE, "application/x-www-form-urlencoded") .header(CONTENT_LENGTH, "11") .set_payload(Bytes::from_static(b"hello=world")) - .to_from(); + .to_http_parts(); - let s = block_on(Form::::from_request(&mut req)).unwrap(); + let s = block_on(Form::::from_request(&req, &mut pl)).unwrap(); assert_eq!(s.hello, "world"); } fn eq(err: UrlencodedError, other: UrlencodedError) -> bool { match err { - UrlencodedError::Chunked => match other { - UrlencodedError::Chunked => true, - _ => false, - }, UrlencodedError::Overflow => match other { UrlencodedError::Overflow => true, _ => false, @@ -354,36 +349,36 @@ mod tests { #[test] fn test_urlencoded_error() { - let mut req = + let (req, mut pl) = TestRequest::with_header(CONTENT_TYPE, "application/x-www-form-urlencoded") .header(CONTENT_LENGTH, "xxxx") - .to_request(); - let info = block_on(UrlEncoded::<_, Info>::new(&mut req)); + .to_http_parts(); + let info = block_on(UrlEncoded::::new(&req, &mut pl)); assert!(eq(info.err().unwrap(), UrlencodedError::UnknownLength)); - let mut req = + let (req, mut pl) = TestRequest::with_header(CONTENT_TYPE, "application/x-www-form-urlencoded") .header(CONTENT_LENGTH, "1000000") - .to_request(); - let info = block_on(UrlEncoded::<_, Info>::new(&mut req)); + .to_http_parts(); + let info = block_on(UrlEncoded::::new(&req, &mut pl)); assert!(eq(info.err().unwrap(), UrlencodedError::Overflow)); - let mut req = TestRequest::with_header(CONTENT_TYPE, "text/plain") + let (req, mut pl) = TestRequest::with_header(CONTENT_TYPE, "text/plain") .header(CONTENT_LENGTH, "10") - .to_request(); - let info = block_on(UrlEncoded::<_, Info>::new(&mut req)); + .to_http_parts(); + let info = block_on(UrlEncoded::::new(&req, &mut pl)); assert!(eq(info.err().unwrap(), UrlencodedError::ContentType)); } #[test] fn test_urlencoded() { - let mut req = + let (req, mut pl) = TestRequest::with_header(CONTENT_TYPE, "application/x-www-form-urlencoded") .header(CONTENT_LENGTH, "11") .set_payload(Bytes::from_static(b"hello=world")) - .to_request(); + .to_http_parts(); - let info = block_on(UrlEncoded::<_, Info>::new(&mut req)).unwrap(); + let info = block_on(UrlEncoded::::new(&req, &mut pl)).unwrap(); assert_eq!( info, Info { @@ -391,15 +386,15 @@ mod tests { } ); - let mut req = TestRequest::with_header( + let (req, mut pl) = TestRequest::with_header( CONTENT_TYPE, "application/x-www-form-urlencoded; charset=utf-8", ) .header(CONTENT_LENGTH, "11") .set_payload(Bytes::from_static(b"hello=world")) - .to_request(); + .to_http_parts(); - let info = block_on(UrlEncoded::<_, Info>::new(&mut req)).unwrap(); + let info = block_on(UrlEncoded::::new(&req, &mut pl)).unwrap(); assert_eq!( info, Info { diff --git a/src/types/json.rs b/src/types/json.rs index c8ed5afd3..3543975ae 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -3,7 +3,7 @@ use std::rc::Rc; use std::{fmt, ops}; -use bytes::{Bytes, BytesMut}; +use bytes::BytesMut; use futures::{Future, Poll, Stream}; use serde::de::DeserializeOwned; use serde::Serialize; @@ -12,11 +12,11 @@ use serde_json; use actix_http::http::{header::CONTENT_LENGTH, StatusCode}; use actix_http::{HttpMessage, Payload, Response}; -use crate::error::{Error, JsonPayloadError, PayloadError}; +use crate::dev::Decompress; +use crate::error::{Error, JsonPayloadError}; use crate::extract::FromRequest; use crate::request::HttpRequest; use crate::responder::Responder; -use crate::service::ServiceFromRequest; /// Json helper /// @@ -164,26 +164,33 @@ impl Responder for Json { /// ); /// } /// ``` -impl FromRequest

for Json +impl FromRequest for Json where T: DeserializeOwned + 'static, - P: Stream + 'static, { + type Config = JsonConfig; type Error = Error; type Future = Box>; #[inline] - fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { - let req2 = req.request().clone(); + fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { + let req2 = req.clone(); let (limit, err) = req .route_data::() .map(|c| (c.limit, c.ehandler.clone())) .unwrap_or((32768, None)); + let path = req.path().to_string(); + Box::new( - JsonBody::new(req) + JsonBody::new(req, payload) .limit(limit) .map_err(move |e| { + log::debug!( + "Failed to deserialize Json from payload. \ + Request path: {:?}", + path + ); if let Some(err) = err { (*err)(e, &req2) } else { @@ -199,7 +206,7 @@ where /// /// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{error, web, App, HttpResponse}; +/// use actix_web::{error, web, App, FromRequest, HttpResponse}; /// /// #[derive(Deserialize)] /// struct Info { @@ -216,11 +223,13 @@ where /// web::resource("/index.html").route( /// web::post().data( /// // change json extractor configuration -/// web::JsonConfig::default().limit(4096) -/// .error_handler(|err, req| { // <- create custom error response -/// error::InternalError::from_response( -/// err, HttpResponse::Conflict().finish()).into() -/// })) +/// web::Json::::configure(|cfg| { +/// cfg.limit(4096) +/// .error_handler(|err, req| { // <- create custom error response +/// error::InternalError::from_response( +/// err, HttpResponse::Conflict().finish()).into() +/// }) +/// })) /// .to(index)) /// ); /// } @@ -264,22 +273,20 @@ impl Default for JsonConfig { /// /// * content type is not `application/json` /// * content length is greater than 256k -pub struct JsonBody { +pub struct JsonBody { limit: usize, length: Option, - stream: Payload, + stream: Option>, err: Option, fut: Option>>, } -impl JsonBody +impl JsonBody where - T: HttpMessage, - T::Stream: Stream + 'static, U: DeserializeOwned + 'static, { /// Create `JsonBody` for request. - pub fn new(req: &mut T) -> Self { + pub fn new(req: &HttpRequest, payload: &mut Payload) -> Self { // check content-type let json = if let Ok(Some(mime)) = req.mime_type() { mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) @@ -290,25 +297,26 @@ where return JsonBody { limit: 262_144, length: None, - stream: Payload::None, + stream: None, fut: None, err: Some(JsonPayloadError::ContentType), }; } let mut len = None; - if let Some(l) = req.headers().get(CONTENT_LENGTH) { + if let Some(l) = req.headers().get(&CONTENT_LENGTH) { if let Ok(s) = l.to_str() { if let Ok(l) = s.parse::() { len = Some(l) } } } + let payload = Decompress::from_headers(payload.take(), req.headers()); JsonBody { limit: 262_144, length: len, - stream: req.take_payload(), + stream: Some(payload), fut: None, err: None, } @@ -321,10 +329,8 @@ where } } -impl Future for JsonBody +impl Future for JsonBody where - T: HttpMessage, - T::Stream: Stream + 'static, U: DeserializeOwned + 'static, { type Item = U; @@ -346,7 +352,10 @@ where } } - let fut = std::mem::replace(&mut self.stream, Payload::None) + let fut = self + .stream + .take() + .unwrap() .from_err() .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { if (body.len() + chunk.len()) > limit { @@ -368,8 +377,10 @@ mod tests { use serde_derive::{Deserialize, Serialize}; use super::*; + use crate::error::InternalError; use crate::http::header; use crate::test::{block_on, TestRequest}; + use crate::HttpResponse; #[derive(Serialize, Deserialize, PartialEq, Debug)] struct MyObject { @@ -409,8 +420,8 @@ mod tests { } #[test] - fn test_extract() { - let mut req = TestRequest::default() + fn test_custom_error_responder() { + let (req, mut pl) = TestRequest::default() .header( header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"), @@ -420,9 +431,40 @@ mod tests { header::HeaderValue::from_static("16"), ) .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) - .to_from(); + .route_data(JsonConfig::default().limit(10).error_handler(|err, _| { + let msg = MyObject { + name: "invalid request".to_string(), + }; + let resp = HttpResponse::BadRequest() + .body(serde_json::to_string(&msg).unwrap()); + InternalError::from_response(err, resp).into() + })) + .to_http_parts(); - let s = block_on(Json::::from_request(&mut req)).unwrap(); + let s = block_on(Json::::from_request(&req, &mut pl)); + let mut resp = Response::from_error(s.err().unwrap().into()); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + + let body = block_on(resp.take_body().concat2()).unwrap(); + let msg: MyObject = serde_json::from_slice(&body).unwrap(); + assert_eq!(msg.name, "invalid request"); + } + + #[test] + fn test_extract() { + let (req, mut pl) = TestRequest::default() + .header( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + ) + .header( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("16"), + ) + .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) + .to_http_parts(); + + let s = block_on(Json::::from_request(&req, &mut pl)).unwrap(); assert_eq!(s.name, "test"); assert_eq!( s.into_inner(), @@ -431,7 +473,7 @@ mod tests { } ); - let mut req = TestRequest::default() + let (req, mut pl) = TestRequest::default() .header( header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"), @@ -442,12 +484,13 @@ mod tests { ) .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) .route_data(JsonConfig::default().limit(10)) - .to_from(); - let s = block_on(Json::::from_request(&mut req)); - assert!(format!("{}", s.err().unwrap()) - .contains("Json payload size is bigger than allowed.")); + .to_http_parts(); - let mut req = TestRequest::default() + let s = block_on(Json::::from_request(&req, &mut pl)); + assert!(format!("{}", s.err().unwrap()) + .contains("Json payload size is bigger than allowed")); + + let (req, mut pl) = TestRequest::default() .header( header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"), @@ -462,27 +505,27 @@ mod tests { .limit(10) .error_handler(|_, _| JsonPayloadError::ContentType.into()), ) - .to_from(); - let s = block_on(Json::::from_request(&mut req)); + .to_http_parts(); + let s = block_on(Json::::from_request(&req, &mut pl)); assert!(format!("{}", s.err().unwrap()).contains("Content type error")); } #[test] fn test_json_body() { - let mut req = TestRequest::default().to_request(); - let json = block_on(JsonBody::<_, MyObject>::new(&mut req)); + let (req, mut pl) = TestRequest::default().to_http_parts(); + let json = block_on(JsonBody::::new(&req, &mut pl)); assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); - let mut req = TestRequest::default() + let (req, mut pl) = TestRequest::default() .header( header::CONTENT_TYPE, header::HeaderValue::from_static("application/text"), ) - .to_request(); - let json = block_on(JsonBody::<_, MyObject>::new(&mut req)); + .to_http_parts(); + let json = block_on(JsonBody::::new(&req, &mut pl)); assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); - let mut req = TestRequest::default() + let (req, mut pl) = TestRequest::default() .header( header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"), @@ -491,12 +534,12 @@ mod tests { header::CONTENT_LENGTH, header::HeaderValue::from_static("10000"), ) - .to_request(); + .to_http_parts(); - let json = block_on(JsonBody::<_, MyObject>::new(&mut req).limit(100)); + let json = block_on(JsonBody::::new(&req, &mut pl).limit(100)); assert!(json_eq(json.err().unwrap(), JsonPayloadError::Overflow)); - let mut req = TestRequest::default() + let (req, mut pl) = TestRequest::default() .header( header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"), @@ -506,9 +549,9 @@ mod tests { header::HeaderValue::from_static("16"), ) .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) - .to_request(); + .to_http_parts(); - let json = block_on(JsonBody::<_, MyObject>::new(&mut req)); + let json = block_on(JsonBody::::new(&req, &mut pl)); assert_eq!( json.ok().unwrap(), MyObject { diff --git a/src/types/mod.rs b/src/types/mod.rs index 9a0a08801..30ee73091 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -2,7 +2,6 @@ pub(crate) mod form; pub(crate) mod json; -mod multipart; mod path; pub(crate) mod payload; mod query; @@ -10,7 +9,6 @@ pub(crate) mod readlines; pub use self::form::{Form, FormConfig}; pub use self::json::{Json, JsonConfig}; -pub use self::multipart::{Multipart, MultipartField, MultipartItem}; pub use self::path::Path; pub use self::payload::{Payload, PayloadConfig}; pub use self::query::Query; diff --git a/src/types/path.rs b/src/types/path.rs index fbd106630..5f0a05af9 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -6,8 +6,8 @@ use actix_http::error::{Error, ErrorNotFound}; use actix_router::PathDeserializer; use serde::de; +use crate::dev::Payload; use crate::request::HttpRequest; -use crate::service::ServiceFromRequest; use crate::FromRequest; #[derive(PartialEq, Eq, PartialOrd, Ord)] @@ -66,15 +66,6 @@ impl Path { pub fn into_inner(self) -> T { self.inner } - - /// Extract path information from a request - pub fn extract(req: &HttpRequest) -> Result, de::value::Error> - where - T: de::DeserializeOwned, - { - de::Deserialize::deserialize(PathDeserializer::new(req.match_info())) - .map(|inner| Path { inner }) - } } impl AsRef for Path { @@ -161,49 +152,73 @@ impl fmt::Display for Path { /// ); /// } /// ``` -impl FromRequest

for Path +impl FromRequest for Path where T: de::DeserializeOwned, { + type Config = (); type Error = Error; type Future = Result; #[inline] - fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { - Self::extract(req.request()).map_err(ErrorNotFound) + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + de::Deserialize::deserialize(PathDeserializer::new(req.match_info())) + .map(|inner| Path { inner }) + .map_err(ErrorNotFound) } } #[cfg(test)] mod tests { use actix_router::ResourceDef; + use derive_more::Display; + use serde_derive::Deserialize; use super::*; use crate::test::{block_on, TestRequest}; + #[derive(Deserialize, Debug, Display)] + #[display(fmt = "MyStruct({}, {})", key, value)] + struct MyStruct { + key: String, + value: String, + } + + #[derive(Deserialize)] + struct Test2 { + key: String, + value: u32, + } + #[test] fn test_extract_path_single() { let resource = ResourceDef::new("/{value}/"); - let mut req = TestRequest::with_uri("/32/").to_from(); + let mut req = TestRequest::with_uri("/32/").to_srv_request(); resource.match_path(req.match_info_mut()); - assert_eq!(*Path::::from_request(&mut req).unwrap(), 32); + let (req, mut pl) = req.into_parts(); + assert_eq!(*Path::::from_request(&req, &mut pl).unwrap(), 32); + assert!(Path::::from_request(&req, &mut pl).is_err()); } #[test] fn test_tuple_extract() { let resource = ResourceDef::new("/{key}/{value}/"); - let mut req = TestRequest::with_uri("/name/user1/?id=test").to_from(); + let mut req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request(); resource.match_path(req.match_info_mut()); - let res = block_on(<(Path<(String, String)>,)>::from_request(&mut req)).unwrap(); + let (req, mut pl) = req.into_parts(); + let res = + block_on(<(Path<(String, String)>,)>::from_request(&req, &mut pl)).unwrap(); assert_eq!((res.0).0, "name"); assert_eq!((res.0).1, "user1"); let res = block_on( - <(Path<(String, String)>, Path<(String, String)>)>::from_request(&mut req), + <(Path<(String, String)>, Path<(String, String)>)>::from_request( + &req, &mut pl, + ), ) .unwrap(); assert_eq!((res.0).0, "name"); @@ -211,7 +226,49 @@ mod tests { assert_eq!((res.1).0, "name"); assert_eq!((res.1).1, "user1"); - let () = <()>::from_request(&mut req).unwrap(); + let () = <()>::from_request(&req, &mut pl).unwrap(); + } + + #[test] + fn test_request_extract() { + let mut req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request(); + + let resource = ResourceDef::new("/{key}/{value}/"); + resource.match_path(req.match_info_mut()); + + let (req, mut pl) = req.into_parts(); + let mut s = Path::::from_request(&req, &mut pl).unwrap(); + assert_eq!(s.key, "name"); + assert_eq!(s.value, "user1"); + s.value = "user2".to_string(); + assert_eq!(s.value, "user2"); + assert_eq!( + format!("{}, {:?}", s, s), + "MyStruct(name, user2), MyStruct { key: \"name\", value: \"user2\" }" + ); + let s = s.into_inner(); + assert_eq!(s.value, "user2"); + + let s = Path::<(String, String)>::from_request(&req, &mut pl).unwrap(); + assert_eq!(s.0, "name"); + assert_eq!(s.1, "user1"); + + let mut req = TestRequest::with_uri("/name/32/").to_srv_request(); + let resource = ResourceDef::new("/{key}/{value}/"); + resource.match_path(req.match_info_mut()); + + let (req, mut pl) = req.into_parts(); + let s = Path::::from_request(&req, &mut pl).unwrap(); + assert_eq!(s.as_ref().key, "name"); + assert_eq!(s.value, 32); + + let s = Path::<(String, u8)>::from_request(&req, &mut pl).unwrap(); + assert_eq!(s.0, "name"); + assert_eq!(s.1, 32); + + let res = Path::>::from_request(&req, &mut pl).unwrap(); + assert_eq!(res[0], "name".to_owned()); + assert_eq!(res[1], "32".to_owned()); } } diff --git a/src/types/payload.rs b/src/types/payload.rs index 170b9c627..ca4b5de6b 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -10,9 +10,10 @@ use futures::future::{err, Either, FutureResult}; use futures::{Future, Poll, Stream}; use mime::Mime; +use crate::dev; use crate::extract::FromRequest; use crate::http::header; -use crate::service::ServiceFromRequest; +use crate::request::HttpRequest; /// Payload extractor returns request 's payload stream. /// @@ -43,7 +44,7 @@ use crate::service::ServiceFromRequest; /// ); /// } /// ``` -pub struct Payload(crate::dev::Payload>>); +pub struct Payload(crate::dev::Payload); impl Stream for Payload { type Item = Bytes; @@ -84,26 +85,14 @@ impl Stream for Payload { /// ); /// } /// ``` -impl

FromRequest

for Payload -where - P: Stream + 'static, -{ +impl FromRequest for Payload { + type Config = PayloadConfig; type Error = Error; type Future = Result; #[inline] - fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { - let pl = match req.take_payload() { - crate::dev::Payload::Stream(s) => { - let pl: Box> = - Box::new(s); - crate::dev::Payload::Stream(pl) - } - crate::dev::Payload::None => crate::dev::Payload::None, - crate::dev::Payload::H1(pl) => crate::dev::Payload::H1(pl), - crate::dev::Payload::H2(pl) => crate::dev::Payload::H2(pl), - }; - Ok(Payload(pl)) + fn from_request(_: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { + Ok(Payload(payload.take())) } } @@ -132,16 +121,14 @@ where /// ); /// } /// ``` -impl

FromRequest

for Bytes -where - P: Stream + 'static, -{ +impl FromRequest for Bytes { + type Config = PayloadConfig; type Error = Error; type Future = Either>, FutureResult>; #[inline] - fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { + fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { let mut tmp; let cfg = if let Some(cfg) = req.route_data::() { cfg @@ -155,7 +142,9 @@ where } let limit = cfg.limit; - Either::A(Box::new(HttpMessageBody::new(req).limit(limit).from_err())) + Either::A(Box::new( + HttpMessageBody::new(req, payload).limit(limit).from_err(), + )) } } @@ -169,7 +158,7 @@ where /// ## Example /// /// ```rust -/// use actix_web::{web, App}; +/// use actix_web::{web, App, FromRequest}; /// /// /// extract text data from request /// fn index(text: String) -> String { @@ -180,21 +169,21 @@ where /// let app = App::new().service( /// web::resource("/index.html").route( /// web::get() -/// .data(web::PayloadConfig::new(4096)) // <- limit size of the payload +/// .data(String::configure(|cfg| { // <- limit size of the payload +/// cfg.limit(4096) +/// })) /// .to(index)) // <- register handler with extractor params /// ); /// } /// ``` -impl

FromRequest

for String -where - P: Stream + 'static, -{ +impl FromRequest for String { + type Config = PayloadConfig; type Error = Error; type Future = Either>, FutureResult>; #[inline] - fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { + fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { let mut tmp; let cfg = if let Some(cfg) = req.route_data::() { cfg @@ -216,7 +205,7 @@ where let limit = cfg.limit; Either::A(Box::new( - HttpMessageBody::new(req) + HttpMessageBody::new(req, payload) .limit(limit) .from_err() .and_then(move |body| { @@ -244,7 +233,9 @@ pub struct PayloadConfig { impl PayloadConfig { /// Create `PayloadConfig` instance and set max size of payload. pub fn new(limit: usize) -> Self { - Self::default().limit(limit) + let mut cfg = Self::default(); + cfg.limit = limit; + cfg } /// Change max size of payload. By default max size is 256Kb @@ -260,7 +251,7 @@ impl PayloadConfig { self } - fn check_mimetype

(&self, req: &ServiceFromRequest

) -> Result<(), Error> { + fn check_mimetype(&self, req: &HttpRequest) -> Result<(), Error> { // check content-type if let Some(ref mt) = self.mimetype { match req.mime_type() { @@ -297,23 +288,19 @@ impl Default for PayloadConfig { /// By default only 256Kb payload reads to a memory, then /// `PayloadError::Overflow` get returned. Use `MessageBody::limit()` /// method to change upper limit. -pub struct HttpMessageBody { +pub struct HttpMessageBody { limit: usize, length: Option, - stream: actix_http::Payload, + stream: Option>, err: Option, fut: Option>>, } -impl HttpMessageBody -where - T: HttpMessage, - T::Stream: Stream, -{ +impl HttpMessageBody { /// Create `MessageBody` for request. - pub fn new(req: &mut T) -> HttpMessageBody { + pub fn new(req: &HttpRequest, payload: &mut dev::Payload) -> HttpMessageBody { let mut len = None; - if let Some(l) = req.headers().get(header::CONTENT_LENGTH) { + if let Some(l) = req.headers().get(&header::CONTENT_LENGTH) { if let Ok(s) = l.to_str() { if let Ok(l) = s.parse::() { len = Some(l) @@ -326,7 +313,7 @@ where } HttpMessageBody { - stream: req.take_payload(), + stream: Some(dev::Decompress::from_headers(payload.take(), req.headers())), limit: 262_144, length: len, fut: None, @@ -342,7 +329,7 @@ where fn err(e: PayloadError) -> Self { HttpMessageBody { - stream: actix_http::Payload::None, + stream: None, limit: 262_144, fut: None, err: Some(e), @@ -351,11 +338,7 @@ where } } -impl Future for HttpMessageBody -where - T: HttpMessage, - T::Stream: Stream + 'static, -{ +impl Future for HttpMessageBody { type Item = Bytes; type Error = PayloadError; @@ -377,7 +360,9 @@ where // future let limit = self.limit; self.fut = Some(Box::new( - std::mem::replace(&mut self.stream, actix_http::Payload::None) + self.stream + .take() + .unwrap() .from_err() .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { if (body.len() + chunk.len()) > limit { @@ -403,7 +388,7 @@ mod tests { #[test] fn test_payload_config() { - let req = TestRequest::default().to_from(); + let req = TestRequest::default().to_http_request(); let cfg = PayloadConfig::default().mimetype(mime::APPLICATION_JSON); assert!(cfg.check_mimetype(&req).is_err()); @@ -411,62 +396,64 @@ mod tests { header::CONTENT_TYPE, "application/x-www-form-urlencoded", ) - .to_from(); + .to_http_request(); assert!(cfg.check_mimetype(&req).is_err()); - let req = - TestRequest::with_header(header::CONTENT_TYPE, "application/json").to_from(); + let req = TestRequest::with_header(header::CONTENT_TYPE, "application/json") + .to_http_request(); assert!(cfg.check_mimetype(&req).is_ok()); } #[test] fn test_bytes() { - let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11") + let (req, mut pl) = TestRequest::with_header(header::CONTENT_LENGTH, "11") .set_payload(Bytes::from_static(b"hello=world")) - .to_from(); + .to_http_parts(); - let s = block_on(Bytes::from_request(&mut req)).unwrap(); + let s = block_on(Bytes::from_request(&req, &mut pl)).unwrap(); assert_eq!(s, Bytes::from_static(b"hello=world")); } #[test] fn test_string() { - let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11") + let (req, mut pl) = TestRequest::with_header(header::CONTENT_LENGTH, "11") .set_payload(Bytes::from_static(b"hello=world")) - .to_from(); + .to_http_parts(); - let s = block_on(String::from_request(&mut req)).unwrap(); + let s = block_on(String::from_request(&req, &mut pl)).unwrap(); assert_eq!(s, "hello=world"); } #[test] fn test_message_body() { - let mut req = - TestRequest::with_header(header::CONTENT_LENGTH, "xxxx").to_request(); - let res = block_on(HttpMessageBody::new(&mut req)); + let (req, mut pl) = TestRequest::with_header(header::CONTENT_LENGTH, "xxxx") + .to_srv_request() + .into_parts(); + let res = block_on(HttpMessageBody::new(&req, &mut pl)); match res.err().unwrap() { PayloadError::UnknownLength => (), _ => unreachable!("error"), } - let mut req = - TestRequest::with_header(header::CONTENT_LENGTH, "1000000").to_request(); - let res = block_on(HttpMessageBody::new(&mut req)); + let (req, mut pl) = TestRequest::with_header(header::CONTENT_LENGTH, "1000000") + .to_srv_request() + .into_parts(); + let res = block_on(HttpMessageBody::new(&req, &mut pl)); match res.err().unwrap() { PayloadError::Overflow => (), _ => unreachable!("error"), } - let mut req = TestRequest::default() + let (req, mut pl) = TestRequest::default() .set_payload(Bytes::from_static(b"test")) - .to_request(); - let res = block_on(HttpMessageBody::new(&mut req)); + .to_http_parts(); + let res = block_on(HttpMessageBody::new(&req, &mut pl)); assert_eq!(res.ok().unwrap(), Bytes::from_static(b"test")); - let mut req = TestRequest::default() + let (req, mut pl) = TestRequest::default() .set_payload(Bytes::from_static(b"11111111111111")) - .to_request(); - let res = block_on(HttpMessageBody::new(&mut req).limit(5)); + .to_http_parts(); + let res = block_on(HttpMessageBody::new(&req, &mut pl).limit(5)); match res.err().unwrap() { PayloadError::Overflow => (), _ => unreachable!("error"), diff --git a/src/types/query.rs b/src/types/query.rs index 85dab0610..f9f545d61 100644 --- a/src/types/query.rs +++ b/src/types/query.rs @@ -6,8 +6,9 @@ use actix_http::error::Error; use serde::de; use serde_urlencoded; +use crate::dev::Payload; use crate::extract::FromRequest; -use crate::service::ServiceFromRequest; +use crate::request::HttpRequest; #[derive(PartialEq, Eq, PartialOrd, Ord)] /// Extract typed information from from the request's query. @@ -110,17 +111,57 @@ impl fmt::Display for Query { /// .route(web::get().to(index))); // <- use `Query` extractor /// } /// ``` -impl FromRequest

for Query +impl FromRequest for Query where T: de::DeserializeOwned, { + type Config = (); type Error = Error; type Future = Result; #[inline] - fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { - serde_urlencoded::from_str::(req.request().query_string()) + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + serde_urlencoded::from_str::(req.query_string()) .map(|val| Ok(Query(val))) - .unwrap_or_else(|e| Err(e.into())) + .unwrap_or_else(|e| { + log::debug!( + "Failed during Query extractor deserialization. \ + Request path: {:?}", + req.path() + ); + Err(e.into()) + }) + } +} + +#[cfg(test)] +mod tests { + use derive_more::Display; + use serde_derive::Deserialize; + + use super::*; + use crate::test::TestRequest; + + #[derive(Deserialize, Debug, Display)] + struct Id { + id: String, + } + + #[test] + fn test_request_extract() { + let req = TestRequest::with_uri("/name/user1/").to_srv_request(); + let (req, mut pl) = req.into_parts(); + assert!(Query::::from_request(&req, &mut pl).is_err()); + + let req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request(); + let (req, mut pl) = req.into_parts(); + + let mut s = Query::::from_request(&req, &mut pl).unwrap(); + assert_eq!(s.id, "test"); + assert_eq!(format!("{}, {:?}", s, s), "test, Id { id: \"test\" }"); + + s.id = "test1".to_string(); + let s = s.into_inner(); + assert_eq!(s.id, "test1"); } } diff --git a/src/web.rs b/src/web.rs index 65b3cfc70..73314449c 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1,5 +1,5 @@ //! Essentials helper functions and types for application registration. -use actix_http::{http::Method, Response}; +use actix_http::http::Method; use futures::{Future, IntoFuture}; pub use actix_http::Response as HttpResponse; @@ -13,6 +13,7 @@ use crate::responder::Responder; use crate::route::Route; use crate::scope::Scope; +pub use crate::config::ServiceConfig; pub use crate::data::{Data, RouteData}; pub use crate::request::HttpRequest; pub use crate::types::*; @@ -49,7 +50,7 @@ pub use crate::types::*; /// ); /// } /// ``` -pub fn resource(path: &str) -> Resource

{ +pub fn resource(path: &str) -> Resource { Resource::new(path) } @@ -76,12 +77,12 @@ pub fn resource(path: &str) -> Resource

{ /// * /{project_id}/path2 /// * /{project_id}/path3 /// -pub fn scope(path: &str) -> Scope

{ +pub fn scope(path: &str) -> Scope { Scope::new(path) } /// Create *route* without configuration. -pub fn route() -> Route

{ +pub fn route() -> Route { Route::new() } @@ -101,8 +102,8 @@ pub fn route() -> Route

{ /// In the above example, one `GET` route get added: /// * /{project_id} /// -pub fn get() -> Route

{ - Route::new().method(Method::GET) +pub fn get() -> Route { + method(Method::GET) } /// Create *route* with `POST` method guard. @@ -121,8 +122,8 @@ pub fn get() -> Route

{ /// In the above example, one `POST` route get added: /// * /{project_id} /// -pub fn post() -> Route

{ - Route::new().method(Method::POST) +pub fn post() -> Route { + method(Method::POST) } /// Create *route* with `PUT` method guard. @@ -141,8 +142,8 @@ pub fn post() -> Route

{ /// In the above example, one `PUT` route get added: /// * /{project_id} /// -pub fn put() -> Route

{ - Route::new().method(Method::PUT) +pub fn put() -> Route { + method(Method::PUT) } /// Create *route* with `PATCH` method guard. @@ -161,8 +162,8 @@ pub fn put() -> Route

{ /// In the above example, one `PATCH` route get added: /// * /{project_id} /// -pub fn patch() -> Route

{ - Route::new().method(Method::PATCH) +pub fn patch() -> Route { + method(Method::PATCH) } /// Create *route* with `DELETE` method guard. @@ -181,8 +182,8 @@ pub fn patch() -> Route

{ /// In the above example, one `DELETE` route get added: /// * /{project_id} /// -pub fn delete() -> Route

{ - Route::new().method(Method::DELETE) +pub fn delete() -> Route { + method(Method::DELETE) } /// Create *route* with `HEAD` method guard. @@ -201,8 +202,8 @@ pub fn delete() -> Route

{ /// In the above example, one `HEAD` route get added: /// * /{project_id} /// -pub fn head() -> Route

{ - Route::new().method(Method::HEAD) +pub fn head() -> Route { + method(Method::HEAD) } /// Create *route* and add method guard. @@ -221,7 +222,7 @@ pub fn head() -> Route

{ /// In the above example, one `GET` route get added: /// * /{project_id} /// -pub fn method(method: Method) -> Route

{ +pub fn method(method: Method) -> Route { Route::new().method(method) } @@ -239,10 +240,10 @@ pub fn method(method: Method) -> Route

{ /// web::to(index)) /// ); /// ``` -pub fn to(handler: F) -> Route

+pub fn to(handler: F) -> Route where F: Factory + 'static, - I: FromRequest

+ 'static, + I: FromRequest + 'static, R: Responder + 'static, { Route::new().to(handler) @@ -262,12 +263,12 @@ where /// web::to_async(index)) /// ); /// ``` -pub fn to_async(handler: F) -> Route

+pub fn to_async(handler: F) -> Route where F: AsyncFactory, - I: FromRequest

+ 'static, + I: FromRequest + 'static, R: IntoFuture + 'static, - R::Item: Into, + R::Item: Responder, R::Error: Into, { Route::new().to_async(handler) diff --git a/test-server/CHANGES.md b/test-server/CHANGES.md index 14a8ce628..700b3aa1f 100644 --- a/test-server/CHANGES.md +++ b/test-server/CHANGES.md @@ -1,5 +1,15 @@ # Changes +## [0.1.1] - 2019-04-24 + +* Always make new connection for http client + + +## [0.1.0] - 2019-04-16 + +* No changes + + ## [0.1.0-alpha.3] - 2019-04-02 * Request functions accept path #743 diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index f85e2b156..906c9d389 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http-test" -version = "0.1.0-alpha.3" +version = "0.1.1" authors = ["Nikolay Kim "] description = "Actix http test server" readme = "README.md" @@ -30,12 +30,12 @@ default = [] ssl = ["openssl", "actix-server/ssl", "awc/ssl"] [dependencies] -actix-codec = "0.1.1" -actix-rt = "0.2.1" -actix-service = "0.3.4" -actix-server = "0.4.0" -actix-utils = "0.3.4" -awc = "0.1.0-alpha.3" +actix-codec = "0.1.2" +actix-rt = "0.2.2" +actix-service = "0.3.6" +actix-server = "0.4.3" +actix-utils = "0.3.5" +awc = "0.1.1" base64 = "0.10" bytes = "0.4" @@ -55,5 +55,5 @@ tokio-timer = "0.2" openssl = { version="0.10", optional = true } [dev-dependencies] -actix-web = "1.0.0-alpa.3" -actix-http = "0.1.0-alpa.3" +actix-web = "1.0.0-beta.1" +actix-http = "0.1.2" diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index 98bef99be..42d07549d 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -1,4 +1,5 @@ //! Various helpers for Actix applications to use during testing. +use std::cell::RefCell; use std::sync::mpsc; use std::{net, thread, time}; @@ -12,6 +13,41 @@ use futures::{Future, Stream}; use http::Method; use net2::TcpBuilder; +thread_local! { + static RT: RefCell = { + RefCell::new(Runtime::new().unwrap()) + }; +} + +/// Runs the provided future, blocking the current thread until the future +/// completes. +/// +/// This function can be used to synchronously block the current thread +/// until the provided `future` has resolved either successfully or with an +/// error. The result of the future is then returned from this function +/// call. +/// +/// Note that this function is intended to be used only for testing purpose. +/// This function panics on nested call. +pub fn block_on(f: F) -> Result +where + F: Future, +{ + RT.with(move |rt| rt.borrow_mut().block_on(f)) +} + +/// Runs the provided function, with runtime enabled. +/// +/// Note that this function is intended to be used only for testing purpose. +/// This function panics on nested call. +pub fn run_on(f: F) -> R +where + F: Fn() -> R, +{ + RT.with(move |rt| rt.borrow_mut().block_on(lazy(|| Ok::<_, ()>(f())))) + .unwrap() +} + /// The `TestServer` type. /// /// `TestServer` is very simple test server that simplify process of writing @@ -88,15 +124,17 @@ impl TestServer { |e| log::error!("Can not set alpn protocol: {:?}", e), ); Connector::new() + .conn_lifetime(time::Duration::from_secs(0)) .timeout(time::Duration::from_millis(500)) .ssl(builder.build()) - .service() + .finish() } #[cfg(not(feature = "ssl"))] { Connector::new() + .conn_lifetime(time::Duration::from_secs(0)) .timeout(time::Duration::from_millis(500)) - .service() + .finish() } }; @@ -107,7 +145,7 @@ impl TestServer { TestServerRuntime { addr, rt, client } } - /// Get firat available unused address + /// Get first 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(); @@ -127,6 +165,15 @@ impl TestServerRuntime { self.rt.block_on(fut) } + /// Execute future on current core + pub fn block_on_fn(&mut self, f: F) -> Result + where + F: FnOnce() -> R, + R: Future, + { + self.rt.block_on(lazy(|| f())) + } + /// Execute function on current core pub fn execute(&mut self, fut: F) -> R where diff --git a/tests/test_httpserver.rs b/tests/test_httpserver.rs index dca3377c9..c0d2e81c4 100644 --- a/tests/test_httpserver.rs +++ b/tests/test_httpserver.rs @@ -61,7 +61,7 @@ fn test_start() { .connector( client::Connector::new() .timeout(Duration::from_millis(100)) - .service(), + .finish(), ) .finish(), ) @@ -136,7 +136,7 @@ fn test_start_ssl() { awc::Connector::new() .ssl(builder.build()) .timeout(Duration::from_millis(100)) - .service(), + .finish(), ) .finish(), ) diff --git a/tests/test_server.rs b/tests/test_server.rs index 3c5d09066..33c18b001 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -16,12 +16,8 @@ use flate2::Compression; use futures::stream::once; use rand::{distributions::Alphanumeric, Rng}; -use actix_web::{dev::HttpMessageBody, http, test, web, App, HttpResponse, HttpServer}; - -#[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] -use actix_web::middleware::encoding; -#[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] -use actix_web::middleware::encoding::BodyEncoding; +use actix_web::middleware::{BodyEncoding, Compress}; +use actix_web::{dev, http, test, web, App, HttpResponse, HttpServer}; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ @@ -58,7 +54,7 @@ fn test_body() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } @@ -68,16 +64,56 @@ fn test_body_gzip() { let mut srv = TestServer::new(|| { h1::H1Service::new( App::new() - .wrap(encoding::Compress::new(ContentEncoding::Gzip)) + .wrap(Compress::new(ContentEncoding::Gzip)) .service(web::resource("/").route(web::to(|| Response::Ok().body(STR)))), ) }); - let mut response = srv.block_on(srv.get("/").no_decompress().send()).unwrap(); + let mut response = srv + .block_on( + srv.get("/") + .no_decompress() + .header(ACCEPT_ENCODING, "gzip") + .send(), + ) + .unwrap(); assert!(response.status().is_success()); // read response - let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); + + // decode + let mut e = GzDecoder::new(&bytes[..]); + let mut dec = Vec::new(); + e.read_to_end(&mut dec).unwrap(); + assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); +} + +#[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] +#[test] +fn test_body_gzip2() { + let mut srv = TestServer::new(|| { + h1::H1Service::new( + App::new() + .wrap(Compress::new(ContentEncoding::Gzip)) + .service(web::resource("/").route(web::to(|| { + Response::Ok().body(STR).into_body::() + }))), + ) + }); + + let mut response = srv + .block_on( + srv.get("/") + .no_decompress() + .header(ACCEPT_ENCODING, "gzip") + .send(), + ) + .unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = srv.block_on(response.body()).unwrap(); // decode let mut e = GzDecoder::new(&bytes[..]); @@ -92,13 +128,11 @@ fn test_body_encoding_override() { let mut srv = TestServer::new(|| { h1::H1Service::new( App::new() - .wrap(encoding::Compress::new(ContentEncoding::Gzip)) + .wrap(Compress::new(ContentEncoding::Gzip)) .service(web::resource("/").route(web::to(|| { - use actix_web::middleware::encoding::BodyEncoding; Response::Ok().encoding(ContentEncoding::Deflate).body(STR) }))) .service(web::resource("/raw").route(web::to(|| { - use actix_web::middleware::encoding::BodyEncoding; let body = actix_web::dev::Body::Bytes(STR.into()); let mut response = Response::with_body(actix_web::http::StatusCode::OK, body); @@ -111,11 +145,18 @@ fn test_body_encoding_override() { }); // Builder - let mut response = srv.block_on(srv.get("/").no_decompress().send()).unwrap(); + let mut response = srv + .block_on( + srv.get("/") + .no_decompress() + .header(ACCEPT_ENCODING, "deflate") + .send(), + ) + .unwrap(); assert!(response.status().is_success()); // read response - let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); // decode let mut e = ZlibDecoder::new(Vec::new()); @@ -128,13 +169,14 @@ fn test_body_encoding_override() { .block_on( srv.request(actix_web::http::Method::GET, srv.url("/raw")) .no_decompress() + .header(ACCEPT_ENCODING, "deflate") .send(), ) .unwrap(); assert!(response.status().is_success()); // read response - let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); // decode let mut e = ZlibDecoder::new(Vec::new()); @@ -153,7 +195,7 @@ fn test_body_gzip_large() { let data = srv_data.clone(); h1::H1Service::new( App::new() - .wrap(encoding::Compress::new(ContentEncoding::Gzip)) + .wrap(Compress::new(ContentEncoding::Gzip)) .service( web::resource("/") .route(web::to(move || Response::Ok().body(data.clone()))), @@ -161,11 +203,18 @@ fn test_body_gzip_large() { ) }); - let mut response = srv.block_on(srv.get("/").no_decompress().send()).unwrap(); + let mut response = srv + .block_on( + srv.get("/") + .no_decompress() + .header(ACCEPT_ENCODING, "gzip") + .send(), + ) + .unwrap(); assert!(response.status().is_success()); // read response - let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); // decode let mut e = GzDecoder::new(&bytes[..]); @@ -187,7 +236,7 @@ fn test_body_gzip_large_random() { let data = srv_data.clone(); h1::H1Service::new( App::new() - .wrap(encoding::Compress::new(ContentEncoding::Gzip)) + .wrap(Compress::new(ContentEncoding::Gzip)) .service( web::resource("/") .route(web::to(move || Response::Ok().body(data.clone()))), @@ -195,11 +244,18 @@ fn test_body_gzip_large_random() { ) }); - let mut response = srv.block_on(srv.get("/").no_decompress().send()).unwrap(); + let mut response = srv + .block_on( + srv.get("/") + .no_decompress() + .header(ACCEPT_ENCODING, "gzip") + .send(), + ) + .unwrap(); assert!(response.status().is_success()); // read response - let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); // decode let mut e = GzDecoder::new(&bytes[..]); @@ -215,7 +271,7 @@ fn test_body_chunked_implicit() { let mut srv = TestServer::new(move || { h1::H1Service::new( App::new() - .wrap(encoding::Compress::new(ContentEncoding::Gzip)) + .wrap(Compress::new(ContentEncoding::Gzip)) .service(web::resource("/").route(web::get().to(move || { Response::Ok().streaming(once(Ok::<_, Error>(Bytes::from_static( STR.as_ref(), @@ -224,7 +280,14 @@ fn test_body_chunked_implicit() { ) }); - let mut response = srv.block_on(srv.get("/").no_decompress().send()).unwrap(); + let mut response = srv + .block_on( + srv.get("/") + .no_decompress() + .header(ACCEPT_ENCODING, "gzip") + .send(), + ) + .unwrap(); assert!(response.status().is_success()); assert_eq!( response.headers().get(TRANSFER_ENCODING).unwrap(), @@ -232,7 +295,7 @@ fn test_body_chunked_implicit() { ); // read response - let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); // decode let mut e = GzDecoder::new(&bytes[..]); @@ -245,15 +308,12 @@ fn test_body_chunked_implicit() { #[cfg(feature = "brotli")] fn test_body_br_streaming() { let mut srv = TestServer::new(move || { - h1::H1Service::new( - App::new() - .wrap(encoding::Compress::new(ContentEncoding::Br)) - .service(web::resource("/").route(web::to(move || { - Response::Ok().streaming(once(Ok::<_, Error>(Bytes::from_static( - STR.as_ref(), - )))) - }))), - ) + h1::H1Service::new(App::new().wrap(Compress::new(ContentEncoding::Br)).service( + web::resource("/").route(web::to(move || { + Response::Ok() + .streaming(once(Ok::<_, Error>(Bytes::from_static(STR.as_ref())))) + })), + )) }); let mut response = srv @@ -267,7 +327,7 @@ fn test_body_br_streaming() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); // decode br let mut e = BrotliDecoder::new(Vec::with_capacity(2048)); @@ -293,7 +353,7 @@ fn test_head_binary() { } // read response - let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert!(bytes.is_empty()); } @@ -315,7 +375,7 @@ fn test_no_chunking() { assert!(!response.headers().contains_key(TRANSFER_ENCODING)); // read response - let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } @@ -325,7 +385,7 @@ fn test_body_deflate() { let mut srv = TestServer::new(move || { h1::H1Service::new( App::new() - .wrap(encoding::Compress::new(ContentEncoding::Deflate)) + .wrap(Compress::new(ContentEncoding::Deflate)) .service( web::resource("/").route(web::to(move || Response::Ok().body(STR))), ), @@ -333,13 +393,19 @@ fn test_body_deflate() { }); // client request - let mut response = srv.block_on(srv.get("/").no_decompress().send()).unwrap(); + let mut response = srv + .block_on( + srv.get("/") + .header(ACCEPT_ENCODING, "deflate") + .no_decompress() + .send(), + ) + .unwrap(); assert!(response.status().is_success()); // read response - let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); - // decode deflate let mut e = ZlibDecoder::new(Vec::new()); e.write_all(bytes.as_ref()).unwrap(); let dec = e.finish().unwrap(); @@ -350,13 +416,9 @@ fn test_body_deflate() { #[cfg(any(feature = "brotli"))] fn test_body_brotli() { let mut srv = TestServer::new(move || { - h1::H1Service::new( - App::new() - .wrap(encoding::Compress::new(ContentEncoding::Br)) - .service( - web::resource("/").route(web::to(move || Response::Ok().body(STR))), - ), - ) + h1::H1Service::new(App::new().wrap(Compress::new(ContentEncoding::Br)).service( + web::resource("/").route(web::to(move || Response::Ok().body(STR))), + )) }); // client request @@ -371,7 +433,7 @@ fn test_body_brotli() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); // decode brotli let mut e = BrotliDecoder::new(Vec::with_capacity(2048)); @@ -385,7 +447,7 @@ fn test_body_brotli() { fn test_encoding() { let mut srv = TestServer::new(move || { HttpService::new( - App::new().enable_encoding().service( + App::new().wrap(Compress::default()).service( web::resource("/") .route(web::to(move |body: Bytes| Response::Ok().body(body))), ), @@ -405,7 +467,7 @@ fn test_encoding() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } @@ -414,7 +476,7 @@ fn test_encoding() { fn test_gzip_encoding() { let mut srv = TestServer::new(move || { HttpService::new( - App::new().chain(encoding::Decompress::new()).service( + App::new().service( web::resource("/") .route(web::to(move |body: Bytes| Response::Ok().body(body))), ), @@ -434,7 +496,7 @@ fn test_gzip_encoding() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } @@ -444,7 +506,7 @@ fn test_gzip_encoding_large() { let data = STR.repeat(10); let mut srv = TestServer::new(move || { h1::H1Service::new( - App::new().chain(encoding::Decompress::new()).service( + App::new().service( web::resource("/") .route(web::to(move |body: Bytes| Response::Ok().body(body))), ), @@ -464,7 +526,7 @@ fn test_gzip_encoding_large() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from(data)); } @@ -478,7 +540,7 @@ fn test_reading_gzip_encoding_large_random() { let mut srv = TestServer::new(move || { HttpService::new( - App::new().chain(encoding::Decompress::new()).service( + App::new().service( web::resource("/") .route(web::to(move |body: Bytes| Response::Ok().body(body))), ), @@ -498,7 +560,7 @@ fn test_reading_gzip_encoding_large_random() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes.len(), data.len()); assert_eq!(bytes, Bytes::from(data)); } @@ -508,7 +570,7 @@ fn test_reading_gzip_encoding_large_random() { fn test_reading_deflate_encoding() { let mut srv = TestServer::new(move || { h1::H1Service::new( - App::new().chain(encoding::Decompress::new()).service( + App::new().service( web::resource("/") .route(web::to(move |body: Bytes| Response::Ok().body(body))), ), @@ -528,7 +590,7 @@ fn test_reading_deflate_encoding() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } @@ -538,7 +600,7 @@ fn test_reading_deflate_encoding_large() { let data = STR.repeat(10); let mut srv = TestServer::new(move || { h1::H1Service::new( - App::new().chain(encoding::Decompress::new()).service( + App::new().service( web::resource("/") .route(web::to(move |body: Bytes| Response::Ok().body(body))), ), @@ -558,7 +620,7 @@ fn test_reading_deflate_encoding_large() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from(data)); } @@ -572,7 +634,7 @@ fn test_reading_deflate_encoding_large_random() { let mut srv = TestServer::new(move || { h1::H1Service::new( - App::new().chain(encoding::Decompress::new()).service( + App::new().service( web::resource("/") .route(web::to(move |body: Bytes| Response::Ok().body(body))), ), @@ -592,7 +654,7 @@ fn test_reading_deflate_encoding_large_random() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes.len(), data.len()); assert_eq!(bytes, Bytes::from(data)); } @@ -602,7 +664,7 @@ fn test_reading_deflate_encoding_large_random() { fn test_brotli_encoding() { let mut srv = TestServer::new(move || { h1::H1Service::new( - App::new().chain(encoding::Decompress::new()).service( + App::new().service( web::resource("/") .route(web::to(move |body: Bytes| Response::Ok().body(body))), ), @@ -622,7 +684,7 @@ fn test_brotli_encoding() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } @@ -632,7 +694,7 @@ fn test_brotli_encoding_large() { let data = STR.repeat(10); let mut srv = TestServer::new(move || { h1::H1Service::new( - App::new().chain(encoding::Decompress::new()).service( + App::new().service( web::resource("/") .route(web::to(move |body: Bytes| Response::Ok().body(body))), ), @@ -652,7 +714,7 @@ fn test_brotli_encoding_large() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from(data)); } @@ -771,7 +833,7 @@ fn test_reading_deflate_encoding_large_random_ssl() { awc::Connector::new() .timeout(std::time::Duration::from_millis(500)) .ssl(builder.build()) - .service(), + .finish(), ) .finish() });