From 2d7293aaf8fa7e81cec3efc2a50ffbd79f9b1de9 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 1 Mar 2019 22:51:32 -0800 Subject: [PATCH 001/109] copy actix-web2 --- CHANGES.md | 832 +------- Cargo.toml | 122 +- build.rs | 16 - examples/basic.rs | 54 + rustfmt.toml | 3 - src/app.rs | 648 ++++++ src/application.rs | 416 ++-- src/blocking.rs | 74 + src/body.rs | 391 ---- src/client/connector.rs | 1340 ------------ src/client/mod.rs | 120 -- src/client/parser.rs | 238 --- src/client/pipeline.rs | 553 ----- src/client/request.rs | 783 ------- src/client/response.rs | 124 -- src/client/writer.rs | 412 ---- src/context.rs | 294 --- src/de.rs | 455 ----- src/error.rs | 1426 ------------- src/extensions.rs | 114 -- src/extractor.rs | 1280 ++++++------ src/filter.rs | 327 +++ src/framed_app.rs | 240 +++ src/framed_handler.rs | 379 ++++ src/framed_route.rs | 448 ++++ src/fs.rs | 2354 +++++++++++----------- src/handler.rs | 872 ++++---- src/header/common/accept.rs | 159 -- src/header/common/accept_charset.rs | 69 - src/header/common/accept_encoding.rs | 72 - src/header/common/accept_language.rs | 75 - src/header/common/allow.rs | 85 - src/header/common/cache_control.rs | 254 --- src/header/common/content_disposition.rs | 914 --------- src/header/common/content_language.rs | 65 - src/header/common/content_range.rs | 210 -- src/header/common/content_type.rs | 122 -- src/header/common/date.rs | 42 - src/header/common/etag.rs | 96 - src/header/common/expires.rs | 39 - src/header/common/if_match.rs | 70 - src/header/common/if_modified_since.rs | 39 - src/header/common/if_none_match.rs | 92 - src/header/common/if_range.rs | 115 -- src/header/common/if_unmodified_since.rs | 40 - src/header/common/last_modified.rs | 38 - src/header/common/mod.rs | 350 ---- src/header/common/range.rs | 434 ---- src/header/mod.rs | 471 ----- src/header/shared/charset.rs | 152 -- src/header/shared/encoding.rs | 59 - src/header/shared/entity.rs | 266 --- src/header/shared/httpdate.rs | 119 -- src/header/shared/mod.rs | 14 - src/header/shared/quality_item.rs | 294 --- src/helpers.rs | 709 ++----- src/httpcodes.rs | 84 - src/httpmessage.rs | 855 -------- src/httprequest.rs | 545 ----- src/httpresponse.rs | 1458 -------------- src/info.rs | 78 +- src/json.rs | 519 ----- src/lib.rs | 309 +-- src/middleware/compress.rs | 443 ++++ src/middleware/cors.rs | 1227 ----------- src/middleware/csrf.rs | 275 --- src/middleware/defaultheaders.rs | 147 +- src/middleware/errhandlers.rs | 141 -- src/middleware/identity.rs | 399 ---- src/middleware/logger.rs | 384 ---- src/middleware/mod.rs | 113 +- src/middleware/session.rs | 618 ------ src/multipart.rs | 815 -------- src/param.rs | 334 --- src/payload.rs | 715 ------- src/pipeline.rs | 869 -------- src/pred.rs | 328 --- src/request.rs | 174 ++ src/resource.rs | 513 +++-- src/responder.rs | 259 +++ src/route.rs | 896 ++++---- src/router.rs | 1247 ------------ src/scope.rs | 1236 ------------ src/server/acceptor.rs | 383 ---- src/server/builder.rs | 134 -- src/server/channel.rs | 300 --- src/server/error.rs | 108 - src/server/h1.rs | 1353 ------------- src/server/h1decoder.rs | 541 ----- src/server/h1writer.rs | 364 ---- src/server/h2.rs | 472 ----- src/server/h2writer.rs | 268 --- src/server/handler.rs | 208 -- src/server/helpers.rs | 208 -- src/server/http.rs | 579 ------ src/server/incoming.rs | 69 - src/server/input.rs | 288 --- src/server/message.rs | 284 --- src/server/mod.rs | 370 ---- src/server/output.rs | 760 ------- src/server/service.rs | 272 --- src/server/settings.rs | 503 ----- src/server/ssl/mod.rs | 12 - src/server/ssl/nativetls.rs | 34 - src/server/ssl/openssl.rs | 87 - src/server/ssl/rustls.rs | 87 - src/service.rs | 155 ++ src/state.rs | 120 ++ src/test.rs | 668 +----- src/uri.rs | 177 -- src/with.rs | 383 ---- src/ws/client.rs | 602 ------ src/ws/context.rs | 341 ---- src/ws/frame.rs | 538 ----- src/ws/mask.rs | 152 -- src/ws/mod.rs | 477 ----- src/ws/proto.rs | 318 --- tests/identity.pfx | Bin 5549 -> 0 bytes tests/test space.binary | 1 - tests/test.binary | 1 - tests/test.png | Bin 168 -> 0 bytes tests/test_client.rs | 508 ----- tests/test_custom_pipeline.rs | 81 - tests/test_handlers.rs | 677 ------- tests/test_middleware.rs | 1055 ---------- tests/test_server.rs | 1942 +++++++----------- tests/test_ws.rs | 395 ---- 127 files changed, 7554 insertions(+), 43481 deletions(-) delete mode 100644 build.rs create mode 100644 examples/basic.rs create mode 100644 src/app.rs create mode 100644 src/blocking.rs delete mode 100644 src/body.rs delete mode 100644 src/client/connector.rs delete mode 100644 src/client/mod.rs delete mode 100644 src/client/parser.rs delete mode 100644 src/client/pipeline.rs delete mode 100644 src/client/request.rs delete mode 100644 src/client/response.rs delete mode 100644 src/client/writer.rs delete mode 100644 src/context.rs delete mode 100644 src/de.rs delete mode 100644 src/error.rs delete mode 100644 src/extensions.rs create mode 100644 src/filter.rs create mode 100644 src/framed_app.rs create mode 100644 src/framed_handler.rs create mode 100644 src/framed_route.rs delete mode 100644 src/header/common/accept.rs delete mode 100644 src/header/common/accept_charset.rs delete mode 100644 src/header/common/accept_encoding.rs delete mode 100644 src/header/common/accept_language.rs delete mode 100644 src/header/common/allow.rs delete mode 100644 src/header/common/cache_control.rs delete mode 100644 src/header/common/content_disposition.rs delete mode 100644 src/header/common/content_language.rs delete mode 100644 src/header/common/content_range.rs delete mode 100644 src/header/common/content_type.rs delete mode 100644 src/header/common/date.rs delete mode 100644 src/header/common/etag.rs delete mode 100644 src/header/common/expires.rs delete mode 100644 src/header/common/if_match.rs delete mode 100644 src/header/common/if_modified_since.rs delete mode 100644 src/header/common/if_none_match.rs delete mode 100644 src/header/common/if_range.rs delete mode 100644 src/header/common/if_unmodified_since.rs delete mode 100644 src/header/common/last_modified.rs delete mode 100644 src/header/common/mod.rs delete mode 100644 src/header/common/range.rs delete mode 100644 src/header/mod.rs delete mode 100644 src/header/shared/charset.rs delete mode 100644 src/header/shared/encoding.rs delete mode 100644 src/header/shared/entity.rs delete mode 100644 src/header/shared/httpdate.rs delete mode 100644 src/header/shared/mod.rs delete mode 100644 src/header/shared/quality_item.rs delete mode 100644 src/httpcodes.rs delete mode 100644 src/httpmessage.rs delete mode 100644 src/httprequest.rs delete mode 100644 src/httpresponse.rs delete mode 100644 src/json.rs create mode 100644 src/middleware/compress.rs delete mode 100644 src/middleware/cors.rs delete mode 100644 src/middleware/csrf.rs delete mode 100644 src/middleware/errhandlers.rs delete mode 100644 src/middleware/identity.rs delete mode 100644 src/middleware/logger.rs delete mode 100644 src/middleware/session.rs delete mode 100644 src/multipart.rs delete mode 100644 src/param.rs delete mode 100644 src/payload.rs delete mode 100644 src/pipeline.rs delete mode 100644 src/pred.rs create mode 100644 src/request.rs create mode 100644 src/responder.rs delete mode 100644 src/router.rs delete mode 100644 src/scope.rs delete mode 100644 src/server/acceptor.rs delete mode 100644 src/server/builder.rs delete mode 100644 src/server/channel.rs delete mode 100644 src/server/error.rs delete mode 100644 src/server/h1.rs delete mode 100644 src/server/h1decoder.rs delete mode 100644 src/server/h1writer.rs delete mode 100644 src/server/h2.rs delete mode 100644 src/server/h2writer.rs delete mode 100644 src/server/handler.rs delete mode 100644 src/server/helpers.rs delete mode 100644 src/server/http.rs delete mode 100644 src/server/incoming.rs delete mode 100644 src/server/input.rs delete mode 100644 src/server/message.rs delete mode 100644 src/server/mod.rs delete mode 100644 src/server/output.rs delete mode 100644 src/server/service.rs delete mode 100644 src/server/settings.rs delete mode 100644 src/server/ssl/mod.rs delete mode 100644 src/server/ssl/nativetls.rs delete mode 100644 src/server/ssl/openssl.rs delete mode 100644 src/server/ssl/rustls.rs create mode 100644 src/service.rs create mode 100644 src/state.rs delete mode 100644 src/uri.rs delete mode 100644 src/with.rs delete mode 100644 src/ws/client.rs delete mode 100644 src/ws/context.rs delete mode 100644 src/ws/frame.rs delete mode 100644 src/ws/mask.rs delete mode 100644 src/ws/mod.rs delete mode 100644 src/ws/proto.rs delete mode 100644 tests/identity.pfx delete mode 100644 tests/test space.binary delete mode 100644 tests/test.binary delete mode 100644 tests/test.png delete mode 100644 tests/test_client.rs delete mode 100644 tests/test_custom_pipeline.rs delete mode 100644 tests/test_handlers.rs delete mode 100644 tests/test_middleware.rs delete mode 100644 tests/test_ws.rs diff --git a/CHANGES.md b/CHANGES.md index d1d838f4..b93e282a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,833 +1,5 @@ # Changes -## [x.x.xx] - xxxx-xx-xx +## [0.1.0] - 2018-10-x -### Added - -* Add `from_file` and `from_file_with_config` to `NamedFile` to allow sending files without a known path. #670 - -* Add `insert` and `remove` methods to `HttpResponseBuilder` - -### Fixed - -* Ignored the `If-Modified-Since` if `If-None-Match` is specified. #680 - -## [0.7.18] - 2019-01-10 - -### Added - -* Add `with_cookie` for `TestRequest` to allow users to customize request cookie. #647 - -* Add `cookie` method for `TestRequest` to allow users to add cookie dynamically. - -### Fixed - -* StaticFiles decode special characters in request's path - -* Fix test server listener leak #654 - -## [0.7.17] - 2018-12-25 - -### Added - -* Support for custom content types in `JsonConfig`. #637 - -* Send `HTTP/1.1 100 Continue` if request contains `expect: continue` header #634 - -### Fixed - -* HTTP1 decoder should perform case-insentive comparison for client requests (e.g. `Keep-Alive`). #631 - -* Access-Control-Allow-Origin header should only a return a single, matching origin. #603 - -## [0.7.16] - 2018-12-11 - -### Added - -* Implement `FromRequest` extractor for `Either` - -* Implement `ResponseError` for `SendError` - - -## [0.7.15] - 2018-12-05 - -### Changed - -* `ClientConnector::resolver` now accepts `Into` instead of `Addr`. It enables user to implement own resolver. - -* `QueryConfig` and `PathConfig` are made public. - -* `AsyncResult::async` is changed to `AsyncResult::future` as `async` is reserved keyword in 2018 edition. - -### Added - -* By default, `Path` extractor now percent decode all characters. This behaviour can be disabled - with `PathConfig::default().disable_decoding()` - - -## [0.7.14] - 2018-11-14 - -### Added - -* Add method to configure custom error handler to `Query` and `Path` extractors. - -* Add method to configure `SameSite` option in `CookieIdentityPolicy`. - -* By default, `Path` extractor now percent decode all characters. This behaviour can be disabled - with `PathConfig::default().disable_decoding()` - - -### Fixed - -* Fix websockets connection drop if request contains "content-length" header #567 - -* Fix keep-alive timer reset - -* HttpServer now treats streaming bodies the same for HTTP/1.x protocols. #549 - -* Set nodelay for socket #560 - - -## [0.7.13] - 2018-10-14 - -### Fixed - -* Fixed rustls support - -* HttpServer not sending streamed request body on HTTP/2 requests #544 - - -## [0.7.12] - 2018-10-10 - -### Changed - -* Set min version for actix - -* Set min version for actix-net - - -## [0.7.11] - 2018-10-09 - -### Fixed - -* Fixed 204 responses for http/2 - - -## [0.7.10] - 2018-10-09 - -### Fixed - -* Fixed panic during graceful shutdown - - -## [0.7.9] - 2018-10-09 - -### Added - -* Added client shutdown timeout setting - -* Added slow request timeout setting - -* Respond with 408 response on slow request timeout #523 - - -### Fixed - -* HTTP1 decoding errors are reported to the client. #512 - -* Correctly compose multiple allowed origins in CORS. #517 - -* Websocket server finished() isn't called if client disconnects #511 - -* Responses with the following codes: 100, 101, 102, 204 -- are sent without Content-Length header. #521 - -* Correct usage of `no_http2` flag in `bind_*` methods. #519 - - -## [0.7.8] - 2018-09-17 - -### Added - -* Use server `Keep-Alive` setting as slow request timeout #439 - -### Changed - -* Use 5 seconds keep-alive timer by default. - -### Fixed - -* Fixed wrong error message for i16 type #510 - - -## [0.7.7] - 2018-09-11 - -### Fixed - -* Fix linked list of HttpChannels #504 - -* Fix requests to TestServer fail #508 - - -## [0.7.6] - 2018-09-07 - -### Fixed - -* Fix system_exit in HttpServer #501 - -* Fix parsing of route param containin regexes with repetition #500 - -### Changes - -* Unhide `SessionBackend` and `SessionImpl` traits #455 - - -## [0.7.5] - 2018-09-04 - -### Added - -* Added the ability to pass a custom `TlsConnector`. - -* Allow to register handlers on scope level #465 - - -### Fixed - -* Handle socket read disconnect - -* Handling scoped paths without leading slashes #460 - - -### Changed - -* Read client response until eof if connection header set to close #464 - - -## [0.7.4] - 2018-08-23 - -### Added - -* Added `HttpServer::maxconn()` and `HttpServer::maxconnrate()`, - accept backpressure #250 - -* Allow to customize connection handshake process via `HttpServer::listen_with()` - and `HttpServer::bind_with()` methods - -* Support making client connections via `tokio-uds`'s `UnixStream` when "uds" feature is enabled #472 - -### Changed - -* It is allowed to use function with up to 10 parameters for handler with `extractor parameters`. - `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple - even for handler with one parameter. - -* native-tls - 0.2 - -* `Content-Disposition` is re-worked. Its parser is now more robust and handles quoted content better. See #461 - -### Fixed - -* Use zlib instead of raw deflate for decoding and encoding payloads with - `Content-Encoding: deflate`. - -* Fixed headers formating for CORS Middleware Access-Control-Expose-Headers #436 - -* Fix adding multiple response headers #446 - -* Client includes port in HOST header when it is not default(e.g. not 80 and 443). #448 - -* Panic during access without routing being set #452 - -* Fixed http/2 error handling - -### Deprecated - -* `HttpServer::no_http2()` is deprecated, use `OpensslAcceptor::with_flags()` or - `RustlsAcceptor::with_flags()` instead - -* `HttpServer::listen_tls()`, `HttpServer::listen_ssl()`, `HttpServer::listen_rustls()` have been - deprecated in favor of `HttpServer::listen_with()` with specific `acceptor`. - -* `HttpServer::bind_tls()`, `HttpServer::bind_ssl()`, `HttpServer::bind_rustls()` have been - deprecated in favor of `HttpServer::bind_with()` with specific `acceptor`. - - -## [0.7.3] - 2018-08-01 - -### Added - -* Support HTTP/2 with rustls #36 - -* Allow TestServer to open a websocket on any URL (TestServer::ws_at()) #433 - -### Fixed - -* Fixed failure 0.1.2 compatibility - -* Do not override HOST header for client request #428 - -* Gz streaming, use `flate2::write::GzDecoder` #228 - -* HttpRequest::url_for is not working with scopes #429 - -* Fixed headers' formating for CORS Middleware `Access-Control-Expose-Headers` header value to HTTP/1.1 & HTTP/2 spec-compliant format #436 - - -## [0.7.2] - 2018-07-26 - -### Added - -* Add implementation of `FromRequest` for `Option` and `Result` - -* Allow to handle application prefix, i.e. allow to handle `/app` path - for application with `/app` prefix. - Check [`App::prefix()`](https://actix.rs/actix-web/actix_web/struct.App.html#method.prefix) - api doc. - -* Add `CookieSessionBackend::http_only` method to set `HttpOnly` directive of cookies - -### Changed - -* Upgrade to cookie 0.11 - -* Removed the timestamp from the default logger middleware - -### Fixed - -* Missing response header "content-encoding" #421 - -* Fix stream draining for http/2 connections #290 - - -## [0.7.1] - 2018-07-21 - -### Fixed - -* Fixed default_resource 'not yet implemented' panic #410 - - -## [0.7.0] - 2018-07-21 - -### Added - -* Add `fs::StaticFileConfig` to provide means of customizing static - file services. It allows to map `mime` to `Content-Disposition`, - specify whether to use `ETag` and `Last-Modified` and allowed methods. - -* Add `.has_prefixed_resource()` method to `router::ResourceInfo` - for route matching with prefix awareness - -* Add `HttpMessage::readlines()` for reading line by line. - -* Add `ClientRequestBuilder::form()` for sending `application/x-www-form-urlencoded` requests. - -* Add method to configure custom error handler to `Form` extractor. - -* Add methods to `HttpResponse` to retrieve, add, and delete cookies - -* Add `.set_content_type()` and `.set_content_disposition()` methods - to `fs::NamedFile` to allow overriding the values inferred by default - -* Add `fs::file_extension_to_mime()` helper function to get the MIME - type for a file extension - -* Add `.content_disposition()` method to parse Content-Disposition of - multipart fields - -* Re-export `actix::prelude::*` as `actix_web::actix` module. - -* `HttpRequest::url_for_static()` for a named route with no variables segments - -* Propagation of the application's default resource to scopes that haven't set a default resource. - - -### Changed - -* Min rustc version is 1.26 - -* Use tokio instead of tokio-core - -* `CookieSessionBackend` sets percent encoded cookies for outgoing HTTP messages. - -* Became possible to use enums with query extractor. - Issue [#371](https://github.com/actix/actix-web/issues/371). - [Example](https://github.com/actix/actix-web/blob/master/tests/test_handlers.rs#L94-L134) - -* `HttpResponse::into_builder()` now moves cookies into the builder - instead of dropping them - -* For safety and performance reasons `Handler::handle()` uses `&self` instead of `&mut self` - -* `Handler::handle()` uses `&HttpRequest` instead of `HttpRequest` - -* Added header `User-Agent: Actix-web/` to default headers when building a request - -* port `Extensions` type from http create, we don't need `Send + Sync` - -* `HttpRequest::query()` returns `Ref>` - -* `HttpRequest::cookies()` returns `Ref>>` - -* `StaticFiles::new()` returns `Result, Error>` instead of `StaticFiles` - -* `StaticFiles` uses the default handler if the file does not exist - - -### Removed - -* Remove `Route::with2()` and `Route::with3()` use tuple of extractors instead. - -* Remove `HttpMessage::range()` - - -## [0.6.15] - 2018-07-11 - -### Fixed - -* Fix h2 compatibility #352 - -* Fix duplicate tail of StaticFiles with index_file. #344 - - -## [0.6.14] - 2018-06-21 - -### Added - -* Allow to disable masking for websockets client - -### Fixed - -* SendRequest execution fails with the "internal error: entered unreachable code" #329 - - -## [0.6.13] - 2018-06-11 - -* http/2 end-of-frame is not set if body is empty bytes #307 - -* InternalError can trigger memory unsafety #301 - - -## [0.6.12] - 2018-06-08 - -### Added - -* Add `Host` filter #287 - -* Allow to filter applications - -* Improved failure interoperability with downcasting #285 - -* Allow to use custom resolver for `ClientConnector` - - -## [0.6.11] - 2018-06-05 - -* Support chunked encoding for UrlEncoded body #262 - -* `HttpRequest::url_for()` for a named route with no variables segments #265 - -* `Middleware::response()` is not invoked if error result was returned by another `Middleware::start()` #255 - -* CORS: Do not validate Origin header on non-OPTION requests #271 - -* Fix multipart upload "Incomplete" error #282 - - -## [0.6.10] - 2018-05-24 - -### Added - -* Allow to use path without trailing slashes for scope registration #241 - -* Allow to set encoding for exact NamedFile #239 - -### Fixed - -* `TestServer::post()` actually sends `GET` request #240 - - -## 0.6.9 (2018-05-22) - -* Drop connection if request's payload is not fully consumed #236 - -* Fix streaming response with body compression - - -## 0.6.8 (2018-05-20) - -* Fix scope resource path extractor #234 - -* Re-use tcp listener on pause/resume - - -## 0.6.7 (2018-05-17) - -* Fix compilation with --no-default-features - - -## 0.6.6 (2018-05-17) - -* Panic during middleware execution #226 - -* Add support for listen_tls/listen_ssl #224 - -* Implement extractor for `Session` - -* Ranges header support for NamedFile #60 - - -## 0.6.5 (2018-05-15) - -* Fix error handling during request decoding #222 - - -## 0.6.4 (2018-05-11) - -* Fix segfault in ServerSettings::get_response_builder() - - -## 0.6.3 (2018-05-10) - -* Add `Router::with_async()` method for async handler registration. - -* Added error response functions for 501,502,503,504 - -* Fix client request timeout handling - - -## 0.6.2 (2018-05-09) - -* WsWriter trait is optional. - - -## 0.6.1 (2018-05-08) - -* Fix http/2 payload streaming #215 - -* Fix connector's default `keep-alive` and `lifetime` settings #212 - -* Send `ErrorNotFound` instead of `ErrorBadRequest` when path extractor fails #214 - -* Allow to exclude certain endpoints from logging #211 - - -## 0.6.0 (2018-05-08) - -* Add route scopes #202 - -* Allow to use ssl and non-ssl connections at the same time #206 - -* Websocket CloseCode Empty/Status is ambiguous #193 - -* Add Content-Disposition to NamedFile #204 - -* Allow to access Error's backtrace object - -* Allow to override files listing renderer for `StaticFiles` #203 - -* Various extractor usability improvements #207 - - -## 0.5.6 (2018-04-24) - -* Make flate2 crate optional #200 - - -## 0.5.5 (2018-04-24) - -* Fix panic when Websocket is closed with no error code #191 - -* Allow to use rust backend for flate2 crate #199 - -## 0.5.4 (2018-04-19) - -* Add identity service middleware - -* Middleware response() is not invoked if there was an error in async handler #187 - -* Use Display formatting for InternalError Display implementation #188 - - -## 0.5.3 (2018-04-18) - -* Impossible to quote slashes in path parameters #182 - - -## 0.5.2 (2018-04-16) - -* Allow to configure StaticFiles's CpuPool, via static method or env variable - -* Add support for custom handling of Json extractor errors #181 - -* Fix StaticFiles does not support percent encoded paths #177 - -* Fix Client Request with custom Body Stream halting on certain size requests #176 - - -## 0.5.1 (2018-04-12) - -* Client connector provides stats, `ClientConnector::stats()` - -* Fix end-of-stream handling in parse_payload #173 - -* Fix StaticFiles generate a lot of threads #174 - - -## 0.5.0 (2018-04-10) - -* Type-safe path/query/form parameter handling, using serde #70 - -* HttpResponse builder's methods `.body()`, `.finish()`, `.json()` - return `HttpResponse` instead of `Result` - -* Use more ergonomic `actix_web::Error` instead of `http::Error` for `ClientRequestBuilder::body()` - -* Added `signed` and `private` `CookieSessionBackend`s - -* Added `HttpRequest::resource()`, returns current matched resource - -* Added `ErrorHandlers` middleware - -* Fix router cannot parse Non-ASCII characters in URL #137 - -* Fix client connection pooling - -* Fix long client urls #129 - -* Fix panic on invalid URL characters #130 - -* Fix logger request duration calculation #152 - -* Fix prefix and static file serving #168 - - -## 0.4.10 (2018-03-20) - -* Use `Error` instead of `InternalError` for `error::ErrorXXXX` methods - -* Allow to set client request timeout - -* Allow to set client websocket handshake timeout - -* Refactor `TestServer` configuration - -* Fix server websockets big payloads support - -* Fix http/2 date header generation - - -## 0.4.9 (2018-03-16) - -* Allow to disable http/2 support - -* Wake payload reading task when data is available - -* Fix server keep-alive handling - -* Send Query Parameters in client requests #120 - -* Move brotli encoding to a feature - -* Add option of default handler for `StaticFiles` handler #57 - -* Add basic client connection pooling - - -## 0.4.8 (2018-03-12) - -* Allow to set read buffer capacity for server request - -* Handle WouldBlock error for socket accept call - - -## 0.4.7 (2018-03-11) - -* Fix panic on unknown content encoding - -* Fix connection get closed too early - -* Fix streaming response handling for http/2 - -* Better sleep on error support - - -## 0.4.6 (2018-03-10) - -* Fix client cookie handling - -* Fix json content type detection - -* Fix CORS middleware #117 - -* Optimize websockets stream support - - -## 0.4.5 (2018-03-07) - -* Fix compression #103 and #104 - -* Fix client cookie handling #111 - -* Non-blocking processing of a `NamedFile` - -* Enable compression support for `NamedFile` - -* Better support for `NamedFile` type - -* Add `ResponseError` impl for `SendRequestError`. This improves ergonomics of the client. - -* Add native-tls support for client - -* Allow client connection timeout to be set #108 - -* Allow to use std::net::TcpListener for HttpServer - -* Handle panics in worker threads - - -## 0.4.4 (2018-03-04) - -* Allow to use Arc> as response/request body - -* Fix handling of requests with an encoded body with a length > 8192 #93 - -## 0.4.3 (2018-03-03) - -* Fix request body read bug - -* Fix segmentation fault #79 - -* Set reuse address before bind #90 - - -## 0.4.2 (2018-03-02) - -* Better naming for websockets implementation - -* Add `Pattern::with_prefix()`, make it more usable outside of actix - -* Add csrf middleware for filter for cross-site request forgery #89 - -* Fix disconnect on idle connections - - -## 0.4.1 (2018-03-01) - -* Rename `Route::p()` to `Route::filter()` - -* Better naming for http codes - -* Fix payload parse in situation when socket data is not ready. - -* Fix Session mutable borrow lifetime #87 - - -## 0.4.0 (2018-02-28) - -* Actix 0.5 compatibility - -* Fix request json/urlencoded loaders - -* Simplify HttpServer type definition - -* Added HttpRequest::encoding() method - -* Added HttpRequest::mime_type() method - -* Added HttpRequest::uri_mut(), allows to modify request uri - -* Added StaticFiles::index_file() - -* Added http client - -* Added websocket client - -* Added TestServer::ws(), test websockets client - -* Added TestServer http client support - -* Allow to override content encoding on application level - - -## 0.3.3 (2018-01-25) - -* Stop processing any events after context stop - -* Re-enable write back-pressure for h1 connections - -* Refactor HttpServer::start_ssl() method - -* Upgrade openssl to 0.10 - - -## 0.3.2 (2018-01-21) - -* Fix HEAD requests handling - -* Log request processing errors - -* Always enable content encoding if encoding explicitly selected - -* Allow multiple Applications on a single server with different state #49 - -* CORS middleware: allowed_headers is defaulting to None #50 - - -## 0.3.1 (2018-01-13) - -* Fix directory entry path #47 - -* Do not enable chunked encoding for HTTP/1.0 - -* Allow explicitly disable chunked encoding - - -## 0.3.0 (2018-01-12) - -* HTTP/2 Support - -* Refactor streaming responses - -* Refactor error handling - -* Asynchronous middlewares - -* Refactor logger middleware - -* Content compression/decompression (br, gzip, deflate) - -* Server multi-threading - -* Graceful shutdown support - - -## 0.2.1 (2017-11-03) - -* Allow to start tls server with `HttpServer::serve_tls` - -* Export `Frame` enum - -* Add conversion impl from `HttpResponse` and `BinaryBody` to a `Frame` - - -## 0.2.0 (2017-10-30) - -* Do not use `http::Uri` as it can not parse some valid paths - -* Refactor response `Body` - -* Refactor `RouteRecognizer` usability - -* Refactor `HttpContext::write` - -* Refactor `Payload` stream - -* Re-use `BinaryBody` for `Frame::Payload` - -* Stop http actor on `write_eof` - -* Fix disconnection handling. - - -## 0.1.0 (2017-10-23) - -* First release +* Initial impl diff --git a/Cargo.toml b/Cargo.toml index bd3cb306..6a61c780 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "0.7.18" +version = "0.1.0" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" @@ -10,44 +10,21 @@ repository = "https://github.com/actix/actix-web.git" documentation = "https://actix.rs/api/actix-web/stable/actix_web/" categories = ["network-programming", "asynchronous", "web-programming::http-server", - "web-programming::http-client", "web-programming::websocket"] license = "MIT/Apache-2.0" exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] -build = "build.rs" - -[package.metadata.docs.rs] -features = ["tls", "ssl", "rust-tls", "session", "brotli", "flate2-c"] +edition = "2018" [badges] -travis-ci = { repository = "actix/actix-web", branch = "master" } -appveyor = { repository = "fafhrd91/actix-web-hdy9d" } -codecov = { repository = "actix/actix-web", branch = "master", service = "github" } +travis-ci = { repository = "actix/actix-web2", branch = "master" } +codecov = { repository = "actix/actix-web2", branch = "master", service = "github" } [lib] name = "actix_web" path = "src/lib.rs" [features] -default = ["session", "brotli", "flate2-c", "cell"] - -# tls -tls = ["native-tls", "tokio-tls", "actix-net/tls"] - -# openssl -ssl = ["openssl", "tokio-openssl", "actix-net/ssl"] - -# deprecated, use "ssl" -alpn = ["openssl", "tokio-openssl", "actix-net/ssl"] - -# rustls -rust-tls = ["rustls", "tokio-rustls", "webpki", "webpki-roots", "actix-net/rust-tls"] - -# unix sockets -uds = ["tokio-uds"] - -# sessions feature, session require "ring" crate and c compiler -session = ["cookie/secure"] +default = ["brotli", "flate2-c"] # brotli encoding, requires c compiler brotli = ["brotli2"] @@ -58,81 +35,54 @@ flate2-c = ["flate2/miniz-sys"] # rust backend for flate2 crate flate2-rust = ["flate2/rust_backend"] -cell = ["actix-net/cell"] - [dependencies] -actix = "0.7.9" -actix-net = "0.2.6" +actix-codec = "0.1.0" +#actix-service = "0.2.1" +#actix-server = "0.2.1" +#actix-utils = "0.2.1" +actix-service = { git = "https://github.com/actix/actix-net.git" } +actix-server = { git = "https://github.com/actix/actix-net.git" } +actix-utils = { git = "https://github.com/actix/actix-net.git" } -v_htmlescape = "0.4" -base64 = "0.10" -bitflags = "1.0" -failure = "^0.1.2" -h2 = "0.1" -http = "^0.1.14" -httparse = "1.3" +actix-rt = "0.1.0" +actix-http = { git = "https://github.com/actix/actix-http.git" } +actix-router = { git = "https://github.com/actix/actix-net.git" } + +bytes = "0.4" +futures = "0.1" +derive_more = "0.14" log = "0.4" +lazy_static = "1.2" mime = "0.3" mime_guess = "2.0.0-alpha" -num_cpus = "1.0" +num_cpus = "1.10" percent-encoding = "1.0" -rand = "0.6" -regex = "1.0" +cookie = { version="0.11", features=["percent-encode"] } +v_htmlescape = "0.4" serde = "1.0" serde_json = "1.0" -sha1 = "0.6" -smallvec = "0.6" -time = "0.1" encoding = "0.2" -language-tags = "0.2" -lazy_static = "1.0" -lazycell = "1.0.0" -parking_lot = "0.7" serde_urlencoded = "^0.5.3" -url = { version="1.7", features=["query_encoding"] } -cookie = { version="0.11", features=["percent-encode"] } +parking_lot = "0.7" +hashbrown = "0.1" +regex = "1" +time = "0.1" +threadpool = "1.7" + +# compression brotli2 = { version="^0.3.2", optional = true } flate2 = { version="^1.0.2", optional = true, default-features = false } -# io -mio = "^0.6.13" -net2 = "0.2" -bytes = "0.4" -byteorder = "1.2" -futures = "0.1" -futures-cpupool = "0.1" -slab = "0.4" -tokio = "0.1" -tokio-io = "0.1" -tokio-tcp = "0.1" -tokio-timer = "0.2.8" -tokio-reactor = "0.1" -tokio-current-thread = "0.1" - -# native-tls -native-tls = { version="0.2", optional = true } -tokio-tls = { version="0.2", optional = true } - -# openssl -openssl = { version="0.10", optional = true } -tokio-openssl = { version="0.2", optional = true } - -#rustls -rustls = { version = "0.14", optional = true } -tokio-rustls = { version = "0.8", optional = true } -webpki = { version = "0.18", optional = true } -webpki-roots = { version = "0.15", optional = true } - -# unix sockets -tokio-uds = { version="0.2", optional = true } - [dev-dependencies] +actix-rt = "0.1.0" +#actix-server = { version="0.2", features=["ssl"] } +actix-server = { git = "https://github.com/actix/actix-net.git", features=["ssl"] } +actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } +actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } +rand = "0.6" env_logger = "0.6" serde_derive = "1.0" -[build-dependencies] -version_check = "0.1" - [profile.release] lto = true opt-level = 3 diff --git a/build.rs b/build.rs deleted file mode 100644 index c8457944..00000000 --- a/build.rs +++ /dev/null @@ -1,16 +0,0 @@ -extern crate version_check; - -fn main() { - match version_check::is_min_version("1.26.0") { - Some((true, _)) => println!("cargo:rustc-cfg=actix_impl_trait"), - _ => (), - }; - match version_check::is_nightly() { - Some(true) => { - println!("cargo:rustc-cfg=actix_nightly"); - println!("cargo:rustc-cfg=actix_impl_trait"); - } - Some(false) => (), - None => (), - }; -} diff --git a/examples/basic.rs b/examples/basic.rs new file mode 100644 index 00000000..9f4701eb --- /dev/null +++ b/examples/basic.rs @@ -0,0 +1,54 @@ +use futures::IntoFuture; + +use actix_http::{h1, http::Method, Response}; +use actix_server::Server; +use actix_web2::{middleware, App, Error, HttpRequest, Resource}; + +fn index(req: HttpRequest) -> &'static str { + println!("REQ: {:?}", req); + "Hello world!\r\n" +} + +fn index_async(req: HttpRequest) -> impl IntoFuture { + println!("REQ: {:?}", req); + Ok("Hello world!\r\n") +} + +fn no_params() -> &'static str { + "Hello world!\r\n" +} + +fn main() { + ::std::env::set_var("RUST_LOG", "actix_server=info,actix_web2=info"); + env_logger::init(); + let sys = actix_rt::System::new("hello-world"); + + Server::build() + .bind("test", "127.0.0.1:8080", || { + h1::H1Service::new( + App::new() + .middleware( + middleware::DefaultHeaders::new().header("X-Version", "0.2"), + ) + .middleware(middleware::Compress::default()) + .resource("/resource1/index.html", |r| r.get(index)) + .service( + "/resource2/index.html", + Resource::new() + .middleware( + middleware::DefaultHeaders::new() + .header("X-Version-R2", "0.3"), + ) + .default_resource(|r| r.to(|| Response::MethodNotAllowed())) + .method(Method::GET, |r| r.to_async(index_async)), + ) + .service("/test1.html", Resource::new().to(|| "Test\r\n")) + .service("/", Resource::new().to(no_params)), + ) + }) + .unwrap() + .workers(1) + .start(); + + let _ = sys.run(); +} diff --git a/rustfmt.toml b/rustfmt.toml index 4fff285e..94bd11d5 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,5 +1,2 @@ max_width = 89 reorder_imports = true -#wrap_comments = true -fn_args_density = "Compressed" -#use_small_heuristics = false diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 00000000..6ed9b144 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,648 @@ +use std::cell::RefCell; +use std::marker::PhantomData; +use std::rc::Rc; + +use actix_http::body::{Body, MessageBody}; +use actix_http::{Extensions, PayloadStream, Request, Response}; +use actix_router::{Path, ResourceDef, ResourceInfo, Router, Url}; +use actix_service::{ + AndThenNewService, ApplyNewService, IntoNewService, IntoNewTransform, NewService, + NewTransform, Service, +}; +use futures::future::{ok, Either, FutureResult}; +use futures::{Async, Future, IntoFuture, Poll}; + +use crate::helpers::{ + BoxedHttpNewService, BoxedHttpService, DefaultNewService, HttpDefaultNewService, +}; +use crate::resource::Resource; +use crate::service::{ServiceRequest, ServiceResponse}; +use crate::state::{State, StateFactory, StateFactoryResult}; + +type BoxedResponse = Box>; + +pub trait HttpServiceFactory { + type Factory: NewService; + + fn rdef(&self) -> &ResourceDef; + + fn create(self) -> Self::Factory; +} + +/// Application builder +pub struct App { + services: Vec<( + ResourceDef, + BoxedHttpNewService, ServiceResponse>, + )>, + default: Option, ServiceResponse>>>, + defaults: Vec< + Rc< + RefCell< + Option, ServiceResponse>>>, + >, + >, + >, + endpoint: T, + factory_ref: Rc>>>, + extensions: Extensions, + state: Vec>, + _t: PhantomData<(P, B)>, +} + +impl App> { + /// Create application with empty state. Application can + /// be configured with a builder-like pattern. + pub fn new() -> Self { + App::create() + } +} + +impl Default for App> { + fn default() -> Self { + App::new() + } +} + +impl App> { + /// Create application with specified state. Application can be + /// configured with a builder-like pattern. + /// + /// State is shared with all resources within same application and + /// could be accessed with `HttpRequest::state()` method. + /// + /// **Note**: http server accepts an application factory rather than + /// an application instance. Http server constructs an application + /// instance for each thread, thus application state must be constructed + /// multiple times. If you want to share state between different + /// threads, a shared object should be used, e.g. `Arc`. Application + /// state does not need to be `Send` or `Sync`. + pub fn state(mut self, state: S) -> Self { + self.state.push(Box::new(State::new(state))); + self + } + + /// Set application state. This function is + /// similar to `.state()` but it accepts state factory. State get + /// constructed asynchronously during application initialization. + pub fn state_factory(mut self, state: F) -> Self + where + F: Fn() -> Out + 'static, + Out: IntoFuture + 'static, + Out::Error: std::fmt::Debug, + { + self.state.push(Box::new(State::new(state))); + self + } + + fn create() -> Self { + let fref = Rc::new(RefCell::new(None)); + App { + services: Vec::new(), + default: None, + defaults: Vec::new(), + endpoint: AppEntry::new(fref.clone()), + factory_ref: fref, + extensions: Extensions::new(), + state: Vec::new(), + _t: PhantomData, + } + } +} + +// /// Application router builder +// pub struct AppRouter { +// services: Vec<( +// ResourceDef, +// BoxedHttpNewService, Response>, +// )>, +// default: Option, Response>>>, +// defaults: +// Vec, Response>>>>>>, +// state: AppState, +// endpoint: T, +// factory_ref: Rc>>>, +// extensions: Extensions, +// _t: PhantomData

, +// } + +impl App +where + P: 'static, + B: MessageBody, + T: NewService< + Request = ServiceRequest

, + Response = ServiceResponse, + Error = (), + InitError = (), + >, +{ + /// Configure resource for a specific path. + /// + /// Resources may have variable path segments. For example, a + /// resource with the path `/a/{name}/c` would match all incoming + /// requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`. + /// + /// 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. This is done by + /// looking up the identifier in the `Params` object returned by + /// `HttpRequest.match_info()` method. + /// + /// By default, each segment matches the regular expression `[^{}/]+`. + /// + /// You can also specify a custom regex in the form `{identifier:regex}`: + /// + /// For instance, to route `GET`-requests on any route matching + /// `/users/{userid}/{friend}` and store `userid` and `friend` in + /// the exposed `Params` object: + /// + /// ```rust,ignore + /// # extern crate actix_web; + /// use actix_web::{http, App, HttpResponse}; + /// + /// fn main() { + /// let app = App::new().resource("/users/{userid}/{friend}", |r| { + /// r.get(|r| r.to(|_| HttpResponse::Ok())); + /// r.head(|r| r.to(|_| HttpResponse::MethodNotAllowed())) + /// }); + /// } + /// ``` + pub fn resource(mut self, path: &str, f: F) -> Self + where + F: FnOnce(Resource

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

, + Response = ServiceResponse, + Error = (), + InitError = (), + > + 'static, + { + let rdef = ResourceDef::new(path); + let resource = f(Resource::new()); + self.defaults.push(resource.get_default()); + self.services.push(( + rdef, + Box::new(HttpNewService::new(resource.into_new_service())), + )); + self + } + + /// Default resource to be used if no matching route could be found. + /// + /// Default resource works with resources only and does not work with + /// custom services. + pub fn default_resource(mut self, f: F) -> Self + where + F: FnOnce(Resource

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

, + Response = ServiceResponse, + Error = (), + > + 'static, + { + // create and configure default resource + self.default = Some(Rc::new(Box::new(DefaultNewService::new( + f(Resource::new()).into_new_service(), + )))); + + self + } + + /// Register resource handler service. + pub fn service(mut self, rdef: R, factory: F) -> Self + where + R: Into, + F: IntoNewService, + U: NewService< + Request = ServiceRequest

, + Response = ServiceResponse, + Error = (), + > + 'static, + { + self.services.push(( + rdef.into(), + Box::new(HttpNewService::new(factory.into_new_service())), + )); + self + } + + /// Register a middleware. + pub fn middleware( + self, + mw: F, + ) -> App< + P, + B1, + impl NewService< + Request = ServiceRequest

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

, + Response = ServiceResponse, + Error = (), + InitError = (), + >, + B1: MessageBody, + F: IntoNewTransform, + { + let endpoint = ApplyNewService::new(mw, self.endpoint); + App { + endpoint, + state: self.state, + services: self.services, + default: self.default, + defaults: Vec::new(), + factory_ref: self.factory_ref, + extensions: Extensions::new(), + _t: PhantomData, + } + } + + /// 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. + /// + /// ```rust,ignore + /// # extern crate actix_web; + /// use actix_web::{App, HttpRequest, HttpResponse, Result}; + /// + /// fn index(req: &HttpRequest) -> Result { + /// let url = req.url_for("youtube", &["oHg5SJYRHA0"])?; + /// assert_eq!(url.as_str(), "https://youtube.com/watch/oHg5SJYRHA0"); + /// Ok(HttpResponse::Ok().into()) + /// } + /// + /// fn main() { + /// let app = App::new() + /// .resource("/index.html", |r| r.get().f(index)) + /// .external_resource("youtube", "https://youtube.com/watch/{video_id}") + /// .finish(); + /// } + /// ``` + pub fn external_resource(self, _name: N, _url: U) -> Self + where + N: AsRef, + U: AsRef, + { + // self.parts + // .as_mut() + // .expect("Use after finish") + // .router + // .register_external(name.as_ref(), ResourceDef::external(url.as_ref())); + self + } +} + +impl + IntoNewService, T, ()>> for App +where + T: NewService< + Request = ServiceRequest

, + Response = ServiceResponse, + Error = (), + InitError = (), + >, +{ + fn into_new_service(self) -> AndThenNewService, T, ()> { + // update resource default service + if self.default.is_some() { + for default in &self.defaults { + if default.borrow_mut().is_none() { + *default.borrow_mut() = self.default.clone(); + } + } + } + + // set factory + *self.factory_ref.borrow_mut() = Some(AppFactory { + services: Rc::new(self.services), + }); + + AppStateFactory { + state: self.state, + extensions: Rc::new(RefCell::new(Rc::new(self.extensions))), + _t: PhantomData, + } + .and_then(self.endpoint) + } +} + +/// Service factory to convert `Request` to a `ServiceRequest` +pub struct AppStateFactory

{ + state: Vec>, + extensions: Rc>>, + _t: PhantomData

, +} + +impl NewService for AppStateFactory

{ + type Request = Request

; + type Response = ServiceRequest

; + type Error = (); + type InitError = (); + type Service = AppStateService

; + type Future = AppStateFactoryResult

; + + fn new_service(&self, _: &()) -> Self::Future { + AppStateFactoryResult { + state: self.state.iter().map(|s| s.construct()).collect(), + extensions: self.extensions.clone(), + _t: PhantomData, + } + } +} + +#[doc(hidden)] +pub struct AppStateFactoryResult

{ + state: Vec>, + extensions: Rc>>, + _t: PhantomData

, +} + +impl

Future for AppStateFactoryResult

{ + type Item = AppStateService

; + type Error = (); + + fn poll(&mut self) -> Poll { + if let Some(extensions) = Rc::get_mut(&mut *self.extensions.borrow_mut()) { + let mut idx = 0; + while idx < self.state.len() { + if let Async::Ready(_) = self.state[idx].poll_result(extensions)? { + self.state.remove(idx); + } else { + idx += 1; + } + } + if !self.state.is_empty() { + return Ok(Async::NotReady); + } + } else { + log::warn!("Multiple copies of app extensions exists"); + } + + Ok(Async::Ready(AppStateService { + extensions: self.extensions.borrow().clone(), + _t: PhantomData, + })) + } +} + +/// Service to convert `Request` to a `ServiceRequest` +pub struct AppStateService

{ + extensions: Rc, + _t: PhantomData

, +} + +impl

Service for AppStateService

{ + type Request = Request

; + type Response = ServiceRequest

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

) -> Self::Future { + ok(ServiceRequest::new( + Path::new(Url::new(req.uri().clone())), + req, + self.extensions.clone(), + )) + } +} + +pub struct AppFactory

{ + services: Rc< + Vec<( + ResourceDef, + BoxedHttpNewService, ServiceResponse>, + )>, + >, +} + +impl

NewService for AppFactory

{ + type Request = ServiceRequest

; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = AppService

; + type Future = CreateAppService

; + + fn new_service(&self, _: &()) -> Self::Future { + CreateAppService { + fut: self + .services + .iter() + .map(|(path, service)| { + CreateAppServiceItem::Future( + Some(path.clone()), + service.new_service(&()), + ) + }) + .collect(), + } + } +} + +type HttpServiceFut

= + Box, ServiceResponse>, Error = ()>>; + +/// Create app service +#[doc(hidden)] +pub struct CreateAppService

{ + fut: Vec>, +} + +enum CreateAppServiceItem

{ + Future(Option, HttpServiceFut

), + Service( + ResourceDef, + BoxedHttpService, ServiceResponse>, + ), +} + +impl

Future for CreateAppService

{ + type Item = AppService

; + type Error = (); + + fn poll(&mut self) -> Poll { + let mut done = true; + + // poll http services + for item in &mut self.fut { + let res = match item { + CreateAppServiceItem::Future(ref mut path, ref mut fut) => { + match fut.poll()? { + Async::Ready(service) => Some((path.take().unwrap(), service)), + Async::NotReady => { + done = false; + None + } + } + } + CreateAppServiceItem::Service(_, _) => continue, + }; + + if let Some((path, service)) = res { + *item = CreateAppServiceItem::Service(path, service); + } + } + + if done { + let router = self + .fut + .drain(..) + .fold(Router::build(), |mut router, item| { + match item { + CreateAppServiceItem::Service(path, service) => { + router.rdef(path, service) + } + CreateAppServiceItem::Future(_, _) => unreachable!(), + } + router + }); + Ok(Async::Ready(AppService { + router: router.finish(), + ready: None, + })) + } else { + Ok(Async::NotReady) + } + } +} + +pub struct AppService

{ + router: Router, ServiceResponse>>, + ready: Option<(ServiceRequest

, ResourceInfo)>, +} + +impl

Service for AppService

{ + type Request = ServiceRequest

; + type Response = ServiceResponse; + type Error = (); + type Future = Either>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + if self.ready.is_none() { + Ok(Async::Ready(())) + } else { + Ok(Async::NotReady) + } + } + + fn call(&mut self, mut req: ServiceRequest

) -> Self::Future { + if let Some((srv, _info)) = self.router.recognize_mut(req.match_info_mut()) { + Either::A(srv.call(req)) + } else { + let req = req.into_request(); + Either::B(ok(ServiceResponse::new(req, Response::NotFound().finish()))) + } + } +} + +pub struct AppServiceResponse(Box>); + +impl Future for AppServiceResponse { + type Item = ServiceResponse; + type Error = (); + + fn poll(&mut self) -> Poll { + self.0.poll().map_err(|_| panic!()) + } +} + +struct HttpNewService>>(T); + +impl HttpNewService +where + T: NewService, Response = ServiceResponse, Error = ()>, + T::Future: 'static, + ::Future: 'static, +{ + pub fn new(service: T) -> Self { + HttpNewService(service) + } +} + +impl NewService for HttpNewService +where + T: NewService, Response = ServiceResponse, Error = ()>, + T::Future: 'static, + T::Service: 'static, + ::Future: 'static, +{ + type Request = ServiceRequest

; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = BoxedHttpService, Self::Response>; + 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, + _t: PhantomData, + }); + Ok(service) + })) + } +} + +struct HttpServiceWrapper { + service: T, + _t: PhantomData<(P,)>, +} + +impl Service for HttpServiceWrapper +where + T::Future: 'static, + T: Service, Response = ServiceResponse, Error = ()>, +{ + type Request = ServiceRequest

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

) -> Self::Future { + Box::new(self.service.call(req)) + } +} + +#[doc(hidden)] +pub struct AppEntry

{ + factory: Rc>>>, +} + +impl

AppEntry

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

NewService for AppEntry

{ + type Request = ServiceRequest

; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = AppService

; + type Future = CreateAppService

; + + fn new_service(&self, _: &()) -> Self::Future { + self.factory.borrow_mut().as_mut().unwrap().new_service(&()) + } +} diff --git a/src/application.rs b/src/application.rs index d8a6cbe7..6ca4ce28 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,17 +1,21 @@ use std::rc::Rc; +use actix_http::http::ContentEncoding; +use actix_http::{Error, Request, Response}; +use actix_service::Service; +use futures::{Async, Future, Poll}; + use handler::{AsyncResult, FromRequest, Handler, Responder, WrapHandler}; -use header::ContentEncoding; use http::Method; use httprequest::HttpRequest; use httpresponse::HttpResponse; -use middleware::Middleware; -use pipeline::{Pipeline, PipelineHandler}; +// use middleware::Middleware; +// use pipeline::{Pipeline, PipelineHandler}; use pred::Predicate; use resource::Resource; use router::{ResourceDef, Router}; -use scope::Scope; -use server::{HttpHandler, HttpHandlerTask, IntoHttpHandler, Request}; +// use scope::Scope; +// use server::{HttpHandler, HttpHandlerTask, IntoHttpHandler, Request}; use with::WithFactory; /// Application @@ -21,7 +25,7 @@ pub struct HttpApplication { prefix_len: usize, inner: Rc>, filters: Option>>>, - middlewares: Rc>>>, + // middlewares: Rc>>>, } #[doc(hidden)] @@ -30,16 +34,16 @@ pub struct Inner { encoding: ContentEncoding, } -impl PipelineHandler for Inner { - #[inline] - fn encoding(&self) -> ContentEncoding { - self.encoding - } +// impl PipelineHandler for Inner { +// #[inline] +// fn encoding(&self) -> ContentEncoding { +// self.encoding +// } - fn handle(&self, req: &HttpRequest) -> AsyncResult { - self.router.handle(req) - } -} +// fn handle(&self, req: &HttpRequest) -> AsyncResult { +// self.router.handle(req) +// } +// } impl HttpApplication { #[cfg(test)] @@ -54,10 +58,18 @@ impl HttpApplication { } } -impl HttpHandler for HttpApplication { - type Task = Pipeline>; +impl Service for HttpApplication { + // type Task = Pipeline>; + type Request = actix_http::Request; + type Response = actix_http::Response; + type Error = Error; + type Future = Box>; - fn handle(&self, msg: Request) -> Result>, Request> { + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, msg: actix_http::Request) -> Self::Future { let m = { if self.prefix_len == 0 { true @@ -70,11 +82,12 @@ impl HttpHandler for HttpApplication { }; if m { if let Some(ref filters) = self.filters { - for filter in filters { - if !filter.check(&msg, &self.state) { - return Err(msg); - } - } + //for filter in filters { + // if !filter.check(&msg, &self.state) { + //return Err(msg); + unimplemented!() + // } + //} } let info = self @@ -83,10 +96,12 @@ impl HttpHandler for HttpApplication { .recognize(&msg, &self.state, self.prefix_len); let inner = Rc::clone(&self.inner); - let req = HttpRequest::new(msg, Rc::clone(&self.state), info); - Ok(Pipeline::new(req, Rc::clone(&self.middlewares), inner)) + // let req = HttpRequest::new(msg, Rc::clone(&self.state), info); + // Ok(Pipeline::new(req, inner)) + unimplemented!() } else { - Err(msg) + // Err(msg) + unimplemented!() } } } @@ -96,7 +111,7 @@ struct ApplicationParts { prefix: String, router: Router, encoding: ContentEncoding, - middlewares: Vec>>, + // middlewares: Vec>>, filters: Vec>>, } @@ -106,55 +121,10 @@ pub struct App { parts: Option>, } -impl App<()> { - /// Create application with empty state. Application can - /// be configured with a builder-like pattern. - pub fn new() -> App<()> { - App::with_state(()) - } -} - -impl Default for App<()> { - fn default() -> Self { - App::new() - } -} - impl App where S: 'static, { - /// Create application with specified state. Application can be - /// configured with a builder-like pattern. - /// - /// State is shared with all resources within same application and - /// could be accessed with `HttpRequest::state()` method. - /// - /// **Note**: http server accepts an application factory rather than - /// an application instance. Http server constructs an application - /// instance for each thread, thus application state must be constructed - /// multiple times. If you want to share state between different - /// threads, a shared object should be used, e.g. `Arc`. Application - /// state does not need to be `Send` or `Sync`. - pub fn with_state(state: S) -> App { - App { - parts: Some(ApplicationParts { - state, - prefix: "".to_owned(), - router: Router::new(ResourceDef::prefix("")), - middlewares: Vec::new(), - filters: Vec::new(), - encoding: ContentEncoding::Auto, - }), - } - } - - /// Get reference to the application state - pub fn state(&self) -> &S { - let parts = self.parts.as_ref().expect("Use after finish"); - &parts.state - } - /// Set application prefix. /// /// Only requests that match the application's prefix get @@ -205,26 +175,6 @@ where self } - /// Add match predicate to application. - /// - /// ```rust - /// # extern crate actix_web; - /// # use actix_web::*; - /// # fn main() { - /// App::new() - /// .filter(pred::Host("www.rust-lang.org")) - /// .resource("/path", |r| r.f(|_| HttpResponse::Ok())) - /// # .finish(); - /// # } - /// ``` - pub fn filter + 'static>(mut self, p: T) -> App { - { - let parts = self.parts.as_mut().expect("Use after finish"); - parts.filters.push(Box::new(p)); - } - self - } - /// Configure route for a specific path. /// /// This is a simplified version of the `App::resource()` method. @@ -263,42 +213,42 @@ where self } - /// Configure scope for common root path. - /// - /// Scopes collect multiple paths under a common path prefix. - /// Scope path can contain variable path segments as resources. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpRequest, HttpResponse}; - /// - /// fn main() { - /// 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())) - /// }); - /// } - /// ``` - /// - /// In the above example, three routes get added: - /// * /{project_id}/path1 - /// * /{project_id}/path2 - /// * /{project_id}/path3 - /// - pub fn scope(mut self, path: &str, f: F) -> App - where - F: FnOnce(Scope) -> Scope, - { - let scope = f(Scope::new(path)); - self.parts - .as_mut() - .expect("Use after finish") - .router - .register_scope(scope); - self - } + // /// Configure scope for common root path. + // /// + // /// Scopes collect multiple paths under a common path prefix. + // /// Scope path can contain variable path segments as resources. + // /// + // /// ```rust + // /// # extern crate actix_web; + // /// use actix_web::{http, App, HttpRequest, HttpResponse}; + // /// + // /// fn main() { + // /// 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())) + // /// }); + // /// } + // /// ``` + // /// + // /// In the above example, three routes get added: + // /// * /{project_id}/path1 + // /// * /{project_id}/path2 + // /// * /{project_id}/path3 + // /// + // pub fn scope(mut self, path: &str, f: F) -> App + // where + // F: FnOnce(Scope) -> Scope, + // { + // let scope = f(Scope::new(path)); + // self.parts + // .as_mut() + // .expect("Use after finish") + // .router + // .register_scope(scope); + // self + // } /// Configure resource for a specific path. /// @@ -377,51 +327,6 @@ where self } - /// Set default content encoding. `ContentEncoding::Auto` is set by default. - pub fn default_encoding(mut self, encoding: ContentEncoding) -> App { - { - let parts = self.parts.as_mut().expect("Use after finish"); - parts.encoding = encoding; - } - 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. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{App, HttpRequest, HttpResponse, Result}; - /// - /// fn index(req: &HttpRequest) -> Result { - /// let url = req.url_for("youtube", &["oHg5SJYRHA0"])?; - /// assert_eq!(url.as_str(), "https://youtube.com/watch/oHg5SJYRHA0"); - /// Ok(HttpResponse::Ok().into()) - /// } - /// - /// fn main() { - /// let app = App::new() - /// .resource("/index.html", |r| r.get().f(index)) - /// .external_resource("youtube", "https://youtube.com/watch/{video_id}") - /// .finish(); - /// } - /// ``` - pub fn external_resource(mut self, name: T, url: U) -> App - where - T: AsRef, - U: AsRef, - { - self.parts - .as_mut() - .expect("Use after finish") - .router - .register_external(name.as_ref(), ResourceDef::external(url.as_ref())); - self - } - /// Configure handler for specific path prefix. /// /// A path prefix consists of valid path segments, i.e for the @@ -458,15 +363,15 @@ where self } - /// Register a middleware. - pub fn middleware>(mut self, mw: M) -> App { - self.parts - .as_mut() - .expect("Use after finish") - .middlewares - .push(Box::new(mw)); - self - } + // /// Register a middleware. + // pub fn middleware>(mut self, mw: M) -> App { + // self.parts + // .as_mut() + // .expect("Use after finish") + // .middlewares + // .push(Box::new(mw)); + // self + // } /// Run external configuration as part of the application building /// process @@ -521,93 +426,93 @@ where inner, filters, state: Rc::new(parts.state), - middlewares: Rc::new(parts.middlewares), + // middlewares: Rc::new(parts.middlewares), prefix: prefix.to_owned(), prefix_len: prefix.len(), } } - /// Convenience method for creating `Box` instances. - /// - /// This method is useful if you need to register multiple - /// application instances with different state. - /// - /// ```rust - /// # use std::thread; - /// # extern crate actix_web; - /// use actix_web::{server, App, HttpResponse}; - /// - /// struct State1; - /// - /// struct State2; - /// - /// fn main() { - /// # thread::spawn(|| { - /// server::new(|| { - /// vec![ - /// App::with_state(State1) - /// .prefix("/app1") - /// .resource("/", |r| r.f(|r| HttpResponse::Ok())) - /// .boxed(), - /// App::with_state(State2) - /// .prefix("/app2") - /// .resource("/", |r| r.f(|r| HttpResponse::Ok())) - /// .boxed(), - /// ] - /// }).bind("127.0.0.1:8080") - /// .unwrap() - /// .run() - /// # }); - /// } - /// ``` - pub fn boxed(mut self) -> Box>> { - Box::new(BoxedApplication { app: self.finish() }) - } + // /// Convenience method for creating `Box` instances. + // /// + // /// This method is useful if you need to register multiple + // /// application instances with different state. + // /// + // /// ```rust + // /// # use std::thread; + // /// # extern crate actix_web; + // /// use actix_web::{server, App, HttpResponse}; + // /// + // /// struct State1; + // /// + // /// struct State2; + // /// + // /// fn main() { + // /// # thread::spawn(|| { + // /// server::new(|| { + // /// vec![ + // /// App::with_state(State1) + // /// .prefix("/app1") + // /// .resource("/", |r| r.f(|r| HttpResponse::Ok())) + // /// .boxed(), + // /// App::with_state(State2) + // /// .prefix("/app2") + // /// .resource("/", |r| r.f(|r| HttpResponse::Ok())) + // /// .boxed(), + // /// ] + // /// }).bind("127.0.0.1:8080") + // /// .unwrap() + // /// .run() + // /// # }); + // /// } + // /// ``` + // pub fn boxed(mut self) -> Box>> { + // Box::new(BoxedApplication { app: self.finish() }) + // } } -struct BoxedApplication { - app: HttpApplication, -} +// struct BoxedApplication { +// app: HttpApplication, +// } -impl HttpHandler for BoxedApplication { - type Task = Box; +// impl HttpHandler for BoxedApplication { +// type Task = Box; - fn handle(&self, req: Request) -> Result { - self.app.handle(req).map(|t| { - let task: Self::Task = Box::new(t); - task - }) - } -} +// fn handle(&self, req: Request) -> Result { +// self.app.handle(req).map(|t| { +// let task: Self::Task = Box::new(t); +// task +// }) +// } +// } -impl IntoHttpHandler for App { - type Handler = HttpApplication; +// impl IntoHttpHandler for App { +// type Handler = HttpApplication; - fn into_handler(mut self) -> HttpApplication { - self.finish() - } -} +// fn into_handler(mut self) -> HttpApplication { +// self.finish() +// } +// } -impl<'a, S: 'static> IntoHttpHandler for &'a mut App { - type Handler = HttpApplication; +// impl<'a, S: 'static> IntoHttpHandler for &'a mut App { +// type Handler = HttpApplication; - fn into_handler(self) -> HttpApplication { - self.finish() - } -} +// fn into_handler(self) -> HttpApplication { +// self.finish() +// } +// } -#[doc(hidden)] -impl Iterator for App { - type Item = HttpApplication; +// #[doc(hidden)] +// impl Iterator for App { +// type Item = HttpApplication; - fn next(&mut self) -> Option { - if self.parts.is_some() { - Some(self.finish()) - } else { - None - } - } -} +// fn next(&mut self) -> Option { +// if self.parts.is_some() { +// Some(self.finish()) +// } else { +// None +// } +// } +// } #[cfg(test)] mod tests { @@ -773,7 +678,8 @@ mod tests { .route("/test", Method::GET, |_: HttpRequest| HttpResponse::Ok()) .route("/test", Method::POST, |_: HttpRequest| { HttpResponse::Created() - }).finish(); + }) + .finish(); let req = TestRequest::with_uri("/test").method(Method::GET).request(); let resp = app.run(req); diff --git a/src/blocking.rs b/src/blocking.rs new file mode 100644 index 00000000..fc2624f6 --- /dev/null +++ b/src/blocking.rs @@ -0,0 +1,74 @@ +//! Thread pool for blocking operations + +use futures::sync::oneshot; +use futures::{Async, Future, Poll}; +use parking_lot::Mutex; +use threadpool::ThreadPool; + +/// Env variable for default cpu pool size +const ENV_CPU_POOL_VAR: &str = "ACTIX_CPU_POOL"; + +lazy_static::lazy_static! { + pub(crate) static ref DEFAULT_POOL: Mutex = { + let default = match std::env::var(ENV_CPU_POOL_VAR) { + Ok(val) => { + if let Ok(val) = val.parse() { + val + } else { + log::error!("Can not parse ACTIX_CPU_POOL value"); + num_cpus::get() * 5 + } + } + Err(_) => num_cpus::get() * 5, + }; + Mutex::new( + threadpool::Builder::new() + .thread_name("actix-web".to_owned()) + .num_threads(8) + .build(), + ) + }; +} + +thread_local! { + static POOL: ThreadPool = { + DEFAULT_POOL.lock().clone() + }; +} + +pub enum BlockingError { + Error(E), + Canceled, +} + +/// Execute blocking function on a thread pool, returns future that resolves +/// to result of the function execution. +pub fn run(f: F) -> CpuFuture +where + F: FnOnce() -> Result, +{ + let (tx, rx) = oneshot::channel(); + POOL.with(move |pool| { + let _ = tx.send(f()); + }); + + CpuFuture { rx } +} + +pub struct CpuFuture { + rx: oneshot::Receiver>, +} + +impl Future for CpuFuture { + type Item = I; + type Error = BlockingError; + + fn poll(&mut self) -> Poll { + let res = + futures::try_ready!(self.rx.poll().map_err(|_| BlockingError::Canceled)); + match res { + Ok(val) => Ok(Async::Ready(val)), + Err(err) => Err(BlockingError::Error(err)), + } + } +} diff --git a/src/body.rs b/src/body.rs deleted file mode 100644 index 5487dbba..00000000 --- a/src/body.rs +++ /dev/null @@ -1,391 +0,0 @@ -use bytes::{Bytes, BytesMut}; -use futures::Stream; -use std::borrow::Cow; -use std::sync::Arc; -use std::{fmt, mem}; - -use context::ActorHttpContext; -use error::Error; -use handler::Responder; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; - -/// Type represent streaming body -pub type BodyStream = Box>; - -/// Represents various types of http message body. -pub enum Body { - /// Empty response. `Content-Length` header is set to `0` - Empty, - /// Specific response body. - Binary(Binary), - /// Unspecified streaming response. Developer is responsible for setting - /// right `Content-Length` or `Transfer-Encoding` headers. - Streaming(BodyStream), - /// Special body type for actor response. - Actor(Box), -} - -/// Represents various types of binary body. -/// `Content-Length` header is set to length of the body. -#[derive(Debug, PartialEq)] -pub enum Binary { - /// Bytes body - Bytes(Bytes), - /// Static slice - Slice(&'static [u8]), - /// Shared string body - #[doc(hidden)] - SharedString(Arc), - /// Shared vec body - SharedVec(Arc>), -} - -impl Body { - /// Does this body streaming. - #[inline] - pub fn is_streaming(&self) -> bool { - match *self { - Body::Streaming(_) | Body::Actor(_) => true, - _ => false, - } - } - - /// Is this binary body. - #[inline] - pub fn is_binary(&self) -> bool { - match *self { - Body::Binary(_) => true, - _ => false, - } - } - - /// Is this binary empy. - #[inline] - pub fn is_empty(&self) -> bool { - match *self { - Body::Empty => true, - _ => false, - } - } - - /// Create body from slice (copy) - pub fn from_slice(s: &[u8]) -> Body { - Body::Binary(Binary::Bytes(Bytes::from(s))) - } - - /// Is this binary body. - #[inline] - pub(crate) fn binary(self) -> Binary { - match self { - Body::Binary(b) => b, - _ => panic!(), - } - } -} - -impl PartialEq for Body { - fn eq(&self, other: &Body) -> bool { - match *self { - Body::Empty => match *other { - Body::Empty => true, - _ => false, - }, - Body::Binary(ref b) => match *other { - Body::Binary(ref b2) => b == b2, - _ => false, - }, - Body::Streaming(_) | Body::Actor(_) => false, - } - } -} - -impl fmt::Debug for Body { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Body::Empty => write!(f, "Body::Empty"), - Body::Binary(ref b) => write!(f, "Body::Binary({:?})", b), - Body::Streaming(_) => write!(f, "Body::Streaming(_)"), - Body::Actor(_) => write!(f, "Body::Actor(_)"), - } - } -} - -impl From for Body -where - T: Into, -{ - fn from(b: T) -> Body { - Body::Binary(b.into()) - } -} - -impl From> for Body { - fn from(ctx: Box) -> Body { - Body::Actor(ctx) - } -} - -impl Binary { - #[inline] - /// Returns `true` if body is empty - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - #[inline] - /// Length of body in bytes - pub fn len(&self) -> usize { - match *self { - Binary::Bytes(ref bytes) => bytes.len(), - Binary::Slice(slice) => slice.len(), - Binary::SharedString(ref s) => s.len(), - Binary::SharedVec(ref s) => s.len(), - } - } - - /// Create binary body from slice - pub fn from_slice(s: &[u8]) -> Binary { - Binary::Bytes(Bytes::from(s)) - } - - /// Convert Binary to a Bytes instance - pub fn take(&mut self) -> Bytes { - mem::replace(self, Binary::Slice(b"")).into() - } -} - -impl Clone for Binary { - fn clone(&self) -> Binary { - match *self { - Binary::Bytes(ref bytes) => Binary::Bytes(bytes.clone()), - Binary::Slice(slice) => Binary::Bytes(Bytes::from(slice)), - Binary::SharedString(ref s) => Binary::SharedString(s.clone()), - Binary::SharedVec(ref s) => Binary::SharedVec(s.clone()), - } - } -} - -impl Into for Binary { - fn into(self) -> Bytes { - match self { - Binary::Bytes(bytes) => bytes, - Binary::Slice(slice) => Bytes::from(slice), - Binary::SharedString(s) => Bytes::from(s.as_str()), - Binary::SharedVec(s) => Bytes::from(AsRef::<[u8]>::as_ref(s.as_ref())), - } - } -} - -impl From<&'static str> for Binary { - fn from(s: &'static str) -> Binary { - Binary::Slice(s.as_ref()) - } -} - -impl From<&'static [u8]> for Binary { - fn from(s: &'static [u8]) -> Binary { - Binary::Slice(s) - } -} - -impl From> for Binary { - fn from(vec: Vec) -> Binary { - Binary::Bytes(Bytes::from(vec)) - } -} - -impl From> for Binary { - fn from(b: Cow<'static, [u8]>) -> Binary { - match b { - Cow::Borrowed(s) => Binary::Slice(s), - Cow::Owned(vec) => Binary::Bytes(Bytes::from(vec)), - } - } -} - -impl From for Binary { - fn from(s: String) -> Binary { - Binary::Bytes(Bytes::from(s)) - } -} - -impl From> for Binary { - fn from(s: Cow<'static, str>) -> Binary { - match s { - Cow::Borrowed(s) => Binary::Slice(s.as_ref()), - Cow::Owned(s) => Binary::Bytes(Bytes::from(s)), - } - } -} - -impl<'a> From<&'a String> for Binary { - fn from(s: &'a String) -> Binary { - Binary::Bytes(Bytes::from(AsRef::<[u8]>::as_ref(&s))) - } -} - -impl From for Binary { - fn from(s: Bytes) -> Binary { - Binary::Bytes(s) - } -} - -impl From for Binary { - fn from(s: BytesMut) -> Binary { - Binary::Bytes(s.freeze()) - } -} - -impl From> for Binary { - fn from(body: Arc) -> Binary { - Binary::SharedString(body) - } -} - -impl<'a> From<&'a Arc> for Binary { - fn from(body: &'a Arc) -> Binary { - Binary::SharedString(Arc::clone(body)) - } -} - -impl From>> for Binary { - fn from(body: Arc>) -> Binary { - Binary::SharedVec(body) - } -} - -impl<'a> From<&'a Arc>> for Binary { - fn from(body: &'a Arc>) -> Binary { - Binary::SharedVec(Arc::clone(body)) - } -} - -impl AsRef<[u8]> for Binary { - #[inline] - fn as_ref(&self) -> &[u8] { - match *self { - Binary::Bytes(ref bytes) => bytes.as_ref(), - Binary::Slice(slice) => slice, - Binary::SharedString(ref s) => s.as_bytes(), - Binary::SharedVec(ref s) => s.as_ref().as_ref(), - } - } -} - -impl Responder for Binary { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - Ok(HttpResponse::build_from(req) - .content_type("application/octet-stream") - .body(self)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_body_is_streaming() { - assert_eq!(Body::Empty.is_streaming(), false); - assert_eq!(Body::Binary(Binary::from("")).is_streaming(), false); - } - - #[test] - fn test_is_empty() { - assert_eq!(Binary::from("").is_empty(), true); - assert_eq!(Binary::from("test").is_empty(), false); - } - - #[test] - fn test_static_str() { - assert_eq!(Binary::from("test").len(), 4); - assert_eq!(Binary::from("test").as_ref(), b"test"); - } - - #[test] - fn test_cow_str() { - let cow: Cow<'static, str> = Cow::Borrowed("test"); - assert_eq!(Binary::from(cow.clone()).len(), 4); - assert_eq!(Binary::from(cow.clone()).as_ref(), b"test"); - let cow: Cow<'static, str> = Cow::Owned("test".to_owned()); - assert_eq!(Binary::from(cow.clone()).len(), 4); - assert_eq!(Binary::from(cow.clone()).as_ref(), b"test"); - } - - #[test] - fn test_static_bytes() { - assert_eq!(Binary::from(b"test".as_ref()).len(), 4); - assert_eq!(Binary::from(b"test".as_ref()).as_ref(), b"test"); - assert_eq!(Binary::from_slice(b"test".as_ref()).len(), 4); - assert_eq!(Binary::from_slice(b"test".as_ref()).as_ref(), b"test"); - } - - #[test] - fn test_vec() { - assert_eq!(Binary::from(Vec::from("test")).len(), 4); - assert_eq!(Binary::from(Vec::from("test")).as_ref(), b"test"); - } - - #[test] - fn test_bytes() { - assert_eq!(Binary::from(Bytes::from("test")).len(), 4); - assert_eq!(Binary::from(Bytes::from("test")).as_ref(), b"test"); - } - - #[test] - fn test_cow_bytes() { - let cow: Cow<'static, [u8]> = Cow::Borrowed(b"test"); - assert_eq!(Binary::from(cow.clone()).len(), 4); - assert_eq!(Binary::from(cow.clone()).as_ref(), b"test"); - let cow: Cow<'static, [u8]> = Cow::Owned(Vec::from("test")); - assert_eq!(Binary::from(cow.clone()).len(), 4); - assert_eq!(Binary::from(cow.clone()).as_ref(), b"test"); - } - - #[test] - fn test_arc_string() { - let b = Arc::new("test".to_owned()); - assert_eq!(Binary::from(b.clone()).len(), 4); - assert_eq!(Binary::from(b.clone()).as_ref(), b"test"); - assert_eq!(Binary::from(&b).len(), 4); - assert_eq!(Binary::from(&b).as_ref(), b"test"); - } - - #[test] - fn test_string() { - let b = "test".to_owned(); - assert_eq!(Binary::from(b.clone()).len(), 4); - assert_eq!(Binary::from(b.clone()).as_ref(), b"test"); - assert_eq!(Binary::from(&b).len(), 4); - assert_eq!(Binary::from(&b).as_ref(), b"test"); - } - - #[test] - fn test_shared_vec() { - let b = Arc::new(Vec::from(&b"test"[..])); - assert_eq!(Binary::from(b.clone()).len(), 4); - assert_eq!(Binary::from(b.clone()).as_ref(), &b"test"[..]); - assert_eq!(Binary::from(&b).len(), 4); - assert_eq!(Binary::from(&b).as_ref(), &b"test"[..]); - } - - #[test] - fn test_bytes_mut() { - let b = BytesMut::from("test"); - assert_eq!(Binary::from(b.clone()).len(), 4); - assert_eq!(Binary::from(b).as_ref(), b"test"); - } - - #[test] - fn test_binary_into() { - let bytes = Bytes::from_static(b"test"); - let b: Bytes = Binary::from("test").into(); - assert_eq!(b, bytes); - let b: Bytes = Binary::from(bytes.clone()).into(); - assert_eq!(b, bytes); - } -} diff --git a/src/client/connector.rs b/src/client/connector.rs deleted file mode 100644 index 1d062302..00000000 --- a/src/client/connector.rs +++ /dev/null @@ -1,1340 +0,0 @@ -use std::collections::{HashMap, VecDeque}; -use std::net::Shutdown; -use std::time::{Duration, Instant}; -use std::{fmt, io, mem, time}; - -use actix_inner::actors::resolver::{Connect as ResolveConnect, Resolver, ResolverError}; -use actix_inner::{ - fut, Actor, ActorFuture, ActorResponse, AsyncContext, Context, - ContextFutureSpawner, Handler, Message, Recipient, StreamHandler, Supervised, - SystemService, WrapFuture, -}; - -use futures::sync::{mpsc, oneshot}; -use futures::{Async, Future, Poll}; -use http::{Error as HttpError, HttpTryFrom, Uri}; -use tokio_io::{AsyncRead, AsyncWrite}; -use tokio_timer::Delay; - -#[cfg(any(feature = "alpn", feature = "ssl"))] -use { - openssl::ssl::{Error as SslError, SslConnector, SslMethod}, - tokio_openssl::SslConnectorExt, -}; - -#[cfg(all( - feature = "tls", - not(any(feature = "alpn", feature = "ssl", feature = "rust-tls")) -))] -use { - native_tls::{Error as SslError, TlsConnector as NativeTlsConnector}, - tokio_tls::TlsConnector as SslConnector, -}; - -#[cfg(all( - feature = "rust-tls", - not(any(feature = "alpn", feature = "tls", feature = "ssl")) -))] -use { - rustls::ClientConfig, std::io::Error as SslError, std::sync::Arc, - tokio_rustls::TlsConnector as SslConnector, webpki::DNSNameRef, webpki_roots, -}; - -#[cfg(not(any( - feature = "alpn", - feature = "ssl", - feature = "tls", - feature = "rust-tls" -)))] -type SslConnector = (); - -use server::IoStream; -use {HAS_OPENSSL, HAS_RUSTLS, HAS_TLS}; - -/// Client connector usage stats -#[derive(Default, Message)] -pub struct ClientConnectorStats { - /// Number of waited-on connections - pub waits: usize, - /// Size of the wait queue - pub wait_queue: usize, - /// Number of reused connections - pub reused: usize, - /// Number of opened connections - pub opened: usize, - /// Number of closed connections - pub closed: usize, - /// Number of connections with errors - pub errors: usize, - /// Number of connection timeouts - pub timeouts: usize, -} - -#[derive(Debug)] -/// `Connect` type represents a message that can be sent to -/// `ClientConnector` with a connection request. -pub struct Connect { - pub(crate) uri: Uri, - pub(crate) wait_timeout: Duration, - pub(crate) conn_timeout: Duration, -} - -impl Connect { - /// Create `Connect` message for specified `Uri` - pub fn new(uri: U) -> Result - where - Uri: HttpTryFrom, - { - Ok(Connect { - uri: Uri::try_from(uri).map_err(|e| e.into())?, - wait_timeout: Duration::from_secs(5), - conn_timeout: Duration::from_secs(1), - }) - } - - /// Connection timeout, i.e. max time to connect to remote host. - /// Set to 1 second by default. - pub fn conn_timeout(mut self, timeout: Duration) -> Self { - self.conn_timeout = timeout; - self - } - - /// If connection pool limits are enabled, wait time indicates - /// max time to wait for a connection to become available. - /// Set to 5 seconds by default. - pub fn wait_timeout(mut self, timeout: Duration) -> Self { - self.wait_timeout = timeout; - self - } -} - -impl Message for Connect { - type Result = Result; -} - -/// Pause connection process for `ClientConnector` -/// -/// All connect requests enter wait state during connector pause. -pub struct Pause { - time: Option, -} - -impl Pause { - /// Create message with pause duration parameter - pub fn new(time: Duration) -> Pause { - Pause { time: Some(time) } - } -} - -impl Default for Pause { - fn default() -> Pause { - Pause { time: None } - } -} - -impl Message for Pause { - type Result = (); -} - -/// Resume connection process for `ClientConnector` -#[derive(Message)] -pub struct Resume; - -/// A set of errors that can occur while connecting to an HTTP host -#[derive(Fail, Debug)] -pub enum ClientConnectorError { - /// Invalid URL - #[fail(display = "Invalid URL")] - InvalidUrl, - - /// SSL feature is not enabled - #[fail(display = "SSL is not supported")] - SslIsNotSupported, - - /// SSL error - #[cfg(any( - feature = "tls", - feature = "alpn", - feature = "ssl", - feature = "rust-tls", - ))] - #[fail(display = "{}", _0)] - SslError(#[cause] SslError), - - /// Resolver error - #[fail(display = "{}", _0)] - Resolver(#[cause] ResolverError), - - /// Connection took too long - #[fail(display = "Timeout while establishing connection")] - Timeout, - - /// Connector has been disconnected - #[fail(display = "Internal error: connector has been disconnected")] - Disconnected, - - /// Connection IO error - #[fail(display = "{}", _0)] - IoError(#[cause] io::Error), -} - -impl From for ClientConnectorError { - fn from(err: ResolverError) -> ClientConnectorError { - match err { - ResolverError::Timeout => ClientConnectorError::Timeout, - _ => ClientConnectorError::Resolver(err), - } - } -} - -struct Waiter { - tx: oneshot::Sender>, - wait: Instant, - conn_timeout: Duration, -} - -enum Paused { - No, - Yes, - Timeout(Instant, Delay), -} - -impl Paused { - fn is_paused(&self) -> bool { - match *self { - Paused::No => false, - _ => true, - } - } -} - -/// `ClientConnector` type is responsible for transport layer of a -/// client connection. -pub struct ClientConnector { - #[allow(dead_code)] - connector: SslConnector, - - stats: ClientConnectorStats, - subscriber: Option>, - - acq_tx: mpsc::UnboundedSender, - acq_rx: Option>, - - resolver: Option>, - conn_lifetime: Duration, - conn_keep_alive: Duration, - limit: usize, - limit_per_host: usize, - acquired: usize, - acquired_per_host: HashMap, - available: HashMap>, - to_close: Vec, - waiters: Option>>, - wait_timeout: Option<(Instant, Delay)>, - paused: Paused, -} - -impl Actor for ClientConnector { - type Context = Context; - - fn started(&mut self, ctx: &mut Self::Context) { - if self.resolver.is_none() { - self.resolver = Some(Resolver::from_registry().recipient()) - } - self.collect_periodic(ctx); - ctx.add_stream(self.acq_rx.take().unwrap()); - ctx.spawn(Maintenance); - } -} - -impl Supervised for ClientConnector {} - -impl SystemService for ClientConnector {} - -impl Default for ClientConnector { - fn default() -> ClientConnector { - let connector = { - #[cfg(all(any(feature = "alpn", feature = "ssl")))] - { - SslConnector::builder(SslMethod::tls()).unwrap().build() - } - - #[cfg(all( - feature = "tls", - not(any(feature = "alpn", feature = "ssl", feature = "rust-tls")) - ))] - { - NativeTlsConnector::builder().build().unwrap().into() - } - - #[cfg(all( - feature = "rust-tls", - not(any(feature = "alpn", feature = "tls", feature = "ssl")) - ))] - { - let mut config = ClientConfig::new(); - config - .root_store - .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); - SslConnector::from(Arc::new(config)) - } - - #[cfg_attr(rustfmt, rustfmt_skip)] - #[cfg(not(any( - feature = "alpn", feature = "ssl", feature = "tls", feature = "rust-tls")))] - { - () - } - }; - - #[cfg_attr(feature = "cargo-clippy", allow(let_unit_value))] - ClientConnector::with_connector_impl(connector) - } -} - -impl ClientConnector { - #[cfg(any(feature = "alpn", feature = "ssl"))] - /// Create `ClientConnector` actor with custom `SslConnector` instance. - /// - /// By default `ClientConnector` uses very a simple SSL configuration. - /// With `with_connector` method it is possible to use a custom - /// `SslConnector` object. - /// - /// ```rust,ignore - /// # #![cfg(feature="alpn")] - /// # extern crate actix_web; - /// # extern crate futures; - /// # use futures::{future, Future}; - /// # use std::io::Write; - /// # use actix_web::actix::Actor; - /// extern crate openssl; - /// use actix_web::{actix, client::ClientConnector, client::Connect}; - /// - /// use openssl::ssl::{SslConnector, SslMethod}; - /// - /// fn main() { - /// actix::run(|| { - /// // Start `ClientConnector` with custom `SslConnector` - /// let ssl_conn = SslConnector::builder(SslMethod::tls()).unwrap().build(); - /// let conn = ClientConnector::with_connector(ssl_conn).start(); - /// - /// conn.send( - /// Connect::new("https://www.rust-lang.org").unwrap()) // <- connect to host - /// .map_err(|_| ()) - /// .and_then(|res| { - /// if let Ok(mut stream) = res { - /// stream.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap(); - /// } - /// # actix::System::current().stop(); - /// Ok(()) - /// }) - /// }); - /// } - /// ``` - pub fn with_connector(connector: SslConnector) -> ClientConnector { - // keep level of indirection for docstrings matching featureflags - Self::with_connector_impl(connector) - } - - #[cfg(all( - feature = "rust-tls", - not(any(feature = "alpn", feature = "ssl", feature = "tls")) - ))] - /// Create `ClientConnector` actor with custom `SslConnector` instance. - /// - /// By default `ClientConnector` uses very a simple SSL configuration. - /// With `with_connector` method it is possible to use a custom - /// `SslConnector` object. - /// - /// ```rust - /// # #![cfg(feature = "rust-tls")] - /// # extern crate actix_web; - /// # extern crate futures; - /// # use futures::{future, Future}; - /// # use std::io::Write; - /// # use actix_web::actix::Actor; - /// extern crate rustls; - /// extern crate webpki_roots; - /// use actix_web::{actix, client::ClientConnector, client::Connect}; - /// - /// use rustls::ClientConfig; - /// use std::sync::Arc; - /// - /// fn main() { - /// actix::run(|| { - /// // Start `ClientConnector` with custom `ClientConfig` - /// let mut config = ClientConfig::new(); - /// config - /// .root_store - /// .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); - /// let conn = ClientConnector::with_connector(config).start(); - /// - /// conn.send( - /// Connect::new("https://www.rust-lang.org").unwrap()) // <- connect to host - /// .map_err(|_| ()) - /// .and_then(|res| { - /// if let Ok(mut stream) = res { - /// stream.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap(); - /// } - /// # actix::System::current().stop(); - /// Ok(()) - /// }) - /// }); - /// } - /// ``` - pub fn with_connector(connector: ClientConfig) -> ClientConnector { - // keep level of indirection for docstrings matching featureflags - Self::with_connector_impl(SslConnector::from(Arc::new(connector))) - } - - #[cfg(all( - feature = "tls", - not(any(feature = "ssl", feature = "alpn", feature = "rust-tls")) - ))] - /// Create `ClientConnector` actor with custom `SslConnector` instance. - /// - /// By default `ClientConnector` uses very a simple SSL configuration. - /// With `with_connector` method it is possible to use a custom - /// `SslConnector` object. - /// - /// ```rust - /// # #![cfg(feature = "tls")] - /// # extern crate actix_web; - /// # extern crate futures; - /// # use futures::{future, Future}; - /// # use std::io::Write; - /// # use actix_web::actix::Actor; - /// extern crate native_tls; - /// extern crate webpki_roots; - /// use native_tls::TlsConnector; - /// use actix_web::{actix, client::ClientConnector, client::Connect}; - /// - /// fn main() { - /// actix::run(|| { - /// let connector = TlsConnector::new().unwrap(); - /// let conn = ClientConnector::with_connector(connector.into()).start(); - /// - /// conn.send( - /// Connect::new("https://www.rust-lang.org").unwrap()) // <- connect to host - /// .map_err(|_| ()) - /// .and_then(|res| { - /// if let Ok(mut stream) = res { - /// stream.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap(); - /// } - /// # actix::System::current().stop(); - /// Ok(()) - /// }) - /// }); - /// } - /// ``` - pub fn with_connector(connector: SslConnector) -> ClientConnector { - // keep level of indirection for docstrings matching featureflags - Self::with_connector_impl(connector) - } - - #[inline] - fn with_connector_impl(connector: SslConnector) -> ClientConnector { - let (tx, rx) = mpsc::unbounded(); - - ClientConnector { - connector, - stats: ClientConnectorStats::default(), - subscriber: None, - acq_tx: tx, - acq_rx: Some(rx), - resolver: None, - conn_lifetime: Duration::from_secs(75), - conn_keep_alive: Duration::from_secs(15), - limit: 100, - limit_per_host: 0, - acquired: 0, - acquired_per_host: HashMap::new(), - available: HashMap::new(), - to_close: Vec::new(), - waiters: Some(HashMap::new()), - wait_timeout: None, - paused: Paused::No, - } - } - - /// Set total number of simultaneous connections. - /// - /// If limit is 0, the connector has no limit. - /// The default limit size is 100. - pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; - self - } - - /// Set total number of simultaneous connections to the same endpoint. - /// - /// Endpoints are the same if they have equal (host, port, ssl) triplets. - /// If limit is 0, the connector has no limit. The default limit size is 0. - pub fn limit_per_host(mut self, limit: usize) -> Self { - self.limit_per_host = limit; - self - } - - /// Set keep-alive period for opened connection. - /// - /// Keep-alive period is the period between connection usage. If - /// the delay between repeated usages of the same connection - /// exceeds this period, the connection is closed. - /// Default keep-alive period is 15 seconds. - pub fn conn_keep_alive(mut self, dur: Duration) -> Self { - self.conn_keep_alive = dur; - self - } - - /// Set max lifetime period for connection. - /// - /// Connection lifetime is max lifetime of any opened connection - /// until it is closed regardless of keep-alive period. - /// Default lifetime period is 75 seconds. - pub fn conn_lifetime(mut self, dur: Duration) -> Self { - self.conn_lifetime = dur; - self - } - - /// Subscribe for connector stats. Only one subscriber is supported. - pub fn stats(mut self, subs: Recipient) -> Self { - self.subscriber = Some(subs); - self - } - - /// Use custom resolver actor - /// - /// By default actix's Resolver is used. - pub fn resolver>>(mut self, addr: A) -> Self { - self.resolver = Some(addr.into()); - self - } - - fn acquire(&mut self, key: &Key) -> Acquire { - // check limits - if self.limit > 0 { - if self.acquired >= self.limit { - return Acquire::NotAvailable; - } - if self.limit_per_host > 0 { - if let Some(per_host) = self.acquired_per_host.get(key) { - if *per_host >= self.limit_per_host { - return Acquire::NotAvailable; - } - } - } - } else if self.limit_per_host > 0 { - if let Some(per_host) = self.acquired_per_host.get(key) { - if *per_host >= self.limit_per_host { - return Acquire::NotAvailable; - } - } - } - - self.reserve(key); - - // check if open connection is available - // cleanup stale connections at the same time - if let Some(ref mut connections) = self.available.get_mut(key) { - let now = Instant::now(); - while let Some(conn) = connections.pop_back() { - // check if it still usable - if (now - conn.0) > self.conn_keep_alive - || (now - conn.1.ts) > self.conn_lifetime - { - self.stats.closed += 1; - self.to_close.push(conn.1); - } else { - let mut conn = conn.1; - let mut buf = [0; 2]; - match conn.stream().read(&mut buf) { - Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => (), - Ok(n) if n > 0 => { - self.stats.closed += 1; - self.to_close.push(conn); - continue; - } - Ok(_) | Err(_) => continue, - } - return Acquire::Acquired(conn); - } - } - } - Acquire::Available - } - - fn reserve(&mut self, key: &Key) { - self.acquired += 1; - let per_host = if let Some(per_host) = self.acquired_per_host.get(key) { - *per_host - } else { - 0 - }; - self.acquired_per_host.insert(key.clone(), per_host + 1); - } - - fn release_key(&mut self, key: &Key) { - if self.acquired > 0 { - self.acquired -= 1; - } - let per_host = if let Some(per_host) = self.acquired_per_host.get(key) { - *per_host - } else { - return; - }; - if per_host > 1 { - self.acquired_per_host.insert(key.clone(), per_host - 1); - } else { - self.acquired_per_host.remove(key); - } - } - - fn collect_periodic(&mut self, ctx: &mut Context) { - // check connections for shutdown - let mut idx = 0; - while idx < self.to_close.len() { - match AsyncWrite::shutdown(&mut self.to_close[idx]) { - Ok(Async::NotReady) => idx += 1, - _ => { - self.to_close.swap_remove(idx); - } - } - } - - // re-schedule next collect period - ctx.run_later(Duration::from_secs(1), |act, ctx| act.collect_periodic(ctx)); - - // send stats - let mut stats = mem::replace(&mut self.stats, ClientConnectorStats::default()); - if let Some(ref mut subscr) = self.subscriber { - if let Some(ref waiters) = self.waiters { - for w in waiters.values() { - stats.wait_queue += w.len(); - } - } - let _ = subscr.do_send(stats); - } - } - - // TODO: waiters should be sorted by deadline. maybe timewheel? - fn collect_waiters(&mut self) { - let now = Instant::now(); - let mut next = None; - - for waiters in self.waiters.as_mut().unwrap().values_mut() { - let mut idx = 0; - while idx < waiters.len() { - let wait = waiters[idx].wait; - if wait <= now { - self.stats.timeouts += 1; - let waiter = waiters.swap_remove_back(idx).unwrap(); - let _ = waiter.tx.send(Err(ClientConnectorError::Timeout)); - } else { - if let Some(n) = next { - if wait < n { - next = Some(wait); - } - } else { - next = Some(wait); - } - idx += 1; - } - } - } - - if next.is_some() { - self.install_wait_timeout(next.unwrap()); - } - } - - fn install_wait_timeout(&mut self, time: Instant) { - if let Some(ref mut wait) = self.wait_timeout { - if wait.0 < time { - return; - } - } - - let mut timeout = Delay::new(time); - let _ = timeout.poll(); - self.wait_timeout = Some((time, timeout)); - } - - fn wait_for( - &mut self, key: Key, wait: Duration, conn_timeout: Duration, - ) -> oneshot::Receiver> { - // connection is not available, wait - let (tx, rx) = oneshot::channel(); - - let wait = Instant::now() + wait; - self.install_wait_timeout(wait); - - let waiter = Waiter { - tx, - wait, - conn_timeout, - }; - self.waiters - .as_mut() - .unwrap() - .entry(key) - .or_insert_with(VecDeque::new) - .push_back(waiter); - rx - } - - fn check_availibility(&mut self, ctx: &mut Context) { - // check waiters - let mut act_waiters = self.waiters.take().unwrap(); - - for (key, ref mut waiters) in &mut act_waiters { - while let Some(waiter) = waiters.pop_front() { - if waiter.tx.is_canceled() { - continue; - } - - match self.acquire(key) { - Acquire::Acquired(mut conn) => { - // use existing connection - self.stats.reused += 1; - conn.pool = - Some(AcquiredConn(key.clone(), Some(self.acq_tx.clone()))); - let _ = waiter.tx.send(Ok(conn)); - } - Acquire::NotAvailable => { - waiters.push_front(waiter); - break; - } - Acquire::Available => { - // create new connection - self.connect_waiter(&key, waiter, ctx); - } - } - } - } - - self.waiters = Some(act_waiters); - } - - fn connect_waiter(&mut self, key: &Key, waiter: Waiter, ctx: &mut Context) { - let key = key.clone(); - let conn = AcquiredConn(key.clone(), Some(self.acq_tx.clone())); - - let key2 = key.clone(); - fut::WrapFuture::::actfuture( - self.resolver.as_ref().unwrap().send( - ResolveConnect::host_and_port(&conn.0.host, conn.0.port) - .timeout(waiter.conn_timeout), - ), - ).map_err(move |_, act, _| { - act.release_key(&key2); - () - }).and_then(move |res, act, _| { - #[cfg(any(feature = "alpn", feature = "ssl"))] - match res { - Err(err) => { - let _ = waiter.tx.send(Err(err.into())); - fut::Either::B(fut::err(())) - } - Ok(stream) => { - act.stats.opened += 1; - if conn.0.ssl { - fut::Either::A( - act.connector - .connect_async(&key.host, stream) - .into_actor(act) - .then(move |res, _, _| { - match res { - Err(e) => { - let _ = waiter.tx.send(Err( - ClientConnectorError::SslError(e), - )); - } - Ok(stream) => { - let _ = waiter.tx.send(Ok(Connection::new( - conn.0.clone(), - Some(conn), - Box::new(stream), - ))); - } - } - fut::ok(()) - }), - ) - } else { - let _ = waiter.tx.send(Ok(Connection::new( - conn.0.clone(), - Some(conn), - Box::new(stream), - ))); - fut::Either::B(fut::ok(())) - } - } - } - - #[cfg(all(feature = "tls", not(any(feature = "alpn", feature = "ssl"))))] - match res { - Err(err) => { - let _ = waiter.tx.send(Err(err.into())); - fut::Either::B(fut::err(())) - } - Ok(stream) => { - act.stats.opened += 1; - if conn.0.ssl { - fut::Either::A( - act.connector - .connect(&conn.0.host, stream) - .into_actor(act) - .then(move |res, _, _| { - match res { - Err(e) => { - let _ = waiter.tx.send(Err( - ClientConnectorError::SslError(e), - )); - } - Ok(stream) => { - let _ = waiter.tx.send(Ok(Connection::new( - conn.0.clone(), - Some(conn), - Box::new(stream), - ))); - } - } - fut::ok(()) - }), - ) - } else { - let _ = waiter.tx.send(Ok(Connection::new( - conn.0.clone(), - Some(conn), - Box::new(stream), - ))); - fut::Either::B(fut::ok(())) - } - } - } - - #[cfg(all( - feature = "rust-tls", - not(any(feature = "alpn", feature = "ssl", feature = "tls")) - ))] - match res { - Err(err) => { - let _ = waiter.tx.send(Err(err.into())); - fut::Either::B(fut::err(())) - } - Ok(stream) => { - act.stats.opened += 1; - if conn.0.ssl { - let host = DNSNameRef::try_from_ascii_str(&key.host).unwrap(); - fut::Either::A( - act.connector - .connect(host, stream) - .into_actor(act) - .then(move |res, _, _| { - match res { - Err(e) => { - let _ = waiter.tx.send(Err( - ClientConnectorError::SslError(e), - )); - } - Ok(stream) => { - let _ = waiter.tx.send(Ok(Connection::new( - conn.0.clone(), - Some(conn), - Box::new(stream), - ))); - } - } - fut::ok(()) - }), - ) - } else { - let _ = waiter.tx.send(Ok(Connection::new( - conn.0.clone(), - Some(conn), - Box::new(stream), - ))); - fut::Either::B(fut::ok(())) - } - } - } - - #[cfg(not(any( - feature = "alpn", - feature = "ssl", - feature = "tls", - feature = "rust-tls" - )))] - match res { - Err(err) => { - let _ = waiter.tx.send(Err(err.into())); - fut::err(()) - } - Ok(stream) => { - act.stats.opened += 1; - if conn.0.ssl { - let _ = - waiter.tx.send(Err(ClientConnectorError::SslIsNotSupported)); - } else { - let _ = waiter.tx.send(Ok(Connection::new( - conn.0.clone(), - Some(conn), - Box::new(stream), - ))); - }; - fut::ok(()) - } - } - }).spawn(ctx); - } -} - -impl Handler for ClientConnector { - type Result = (); - - fn handle(&mut self, msg: Pause, _: &mut Self::Context) { - if let Some(time) = msg.time { - let when = Instant::now() + time; - let mut timeout = Delay::new(when); - let _ = timeout.poll(); - self.paused = Paused::Timeout(when, timeout); - } else { - self.paused = Paused::Yes; - } - } -} - -impl Handler for ClientConnector { - type Result = (); - - fn handle(&mut self, _: Resume, _: &mut Self::Context) { - self.paused = Paused::No; - } -} - -impl Handler for ClientConnector { - type Result = ActorResponse; - - fn handle(&mut self, msg: Connect, ctx: &mut Self::Context) -> Self::Result { - let uri = &msg.uri; - let wait_timeout = msg.wait_timeout; - let conn_timeout = msg.conn_timeout; - - // host name is required - if uri.host().is_none() { - return ActorResponse::reply(Err(ClientConnectorError::InvalidUrl)); - } - - // supported protocols - let proto = match uri.scheme_part() { - Some(scheme) => match Protocol::from(scheme.as_str()) { - Some(proto) => proto, - None => { - return ActorResponse::reply(Err(ClientConnectorError::InvalidUrl)) - } - }, - None => return ActorResponse::reply(Err(ClientConnectorError::InvalidUrl)), - }; - - // check ssl availability - if proto.is_secure() && !HAS_OPENSSL && !HAS_TLS && !HAS_RUSTLS { - return ActorResponse::reply(Err(ClientConnectorError::SslIsNotSupported)); - } - - let host = uri.host().unwrap().to_owned(); - let port = uri.port_part().map(|port| port.as_u16()).unwrap_or_else(|| proto.port()); - let key = Key { - host, - port, - ssl: proto.is_secure(), - }; - - // check pause state - if self.paused.is_paused() { - let rx = self.wait_for(key.clone(), wait_timeout, conn_timeout); - self.stats.waits += 1; - return ActorResponse::async( - rx.map_err(|_| ClientConnectorError::Disconnected) - .into_actor(self) - .and_then(move |res, act, ctx| match res { - Ok(conn) => fut::ok(conn), - Err(err) => { - match err { - ClientConnectorError::Timeout => (), - _ => { - act.release_key(&key); - } - } - act.stats.errors += 1; - act.check_availibility(ctx); - fut::err(err) - } - }), - ); - } - - // do not re-use websockets connection - if !proto.is_http() { - let (tx, rx) = oneshot::channel(); - let wait = Instant::now() + wait_timeout; - let waiter = Waiter { - tx, - wait, - conn_timeout, - }; - self.connect_waiter(&key, waiter, ctx); - - return ActorResponse::async( - rx.map_err(|_| ClientConnectorError::Disconnected) - .into_actor(self) - .and_then(move |res, act, ctx| match res { - Ok(conn) => fut::ok(conn), - Err(err) => { - act.stats.errors += 1; - act.release_key(&key); - act.check_availibility(ctx); - fut::err(err) - } - }), - ); - } - - // acquire connection - match self.acquire(&key) { - Acquire::Acquired(mut conn) => { - // use existing connection - conn.pool = Some(AcquiredConn(key, Some(self.acq_tx.clone()))); - self.stats.reused += 1; - ActorResponse::async(fut::ok(conn)) - } - Acquire::NotAvailable => { - // connection is not available, wait - let rx = self.wait_for(key.clone(), wait_timeout, conn_timeout); - self.stats.waits += 1; - - ActorResponse::async( - rx.map_err(|_| ClientConnectorError::Disconnected) - .into_actor(self) - .and_then(move |res, act, ctx| match res { - Ok(conn) => fut::ok(conn), - Err(err) => { - match err { - ClientConnectorError::Timeout => (), - _ => { - act.release_key(&key); - } - } - act.stats.errors += 1; - act.check_availibility(ctx); - fut::err(err) - } - }), - ) - } - Acquire::Available => { - let (tx, rx) = oneshot::channel(); - let wait = Instant::now() + wait_timeout; - let waiter = Waiter { - tx, - wait, - conn_timeout, - }; - self.connect_waiter(&key, waiter, ctx); - - ActorResponse::async( - rx.map_err(|_| ClientConnectorError::Disconnected) - .into_actor(self) - .and_then(move |res, act, ctx| match res { - Ok(conn) => fut::ok(conn), - Err(err) => { - act.stats.errors += 1; - act.release_key(&key); - act.check_availibility(ctx); - fut::err(err) - } - }), - ) - } - } - } -} - -impl StreamHandler for ClientConnector { - fn handle(&mut self, msg: AcquiredConnOperation, ctx: &mut Context) { - match msg { - AcquiredConnOperation::Close(conn) => { - self.release_key(&conn.key); - self.to_close.push(conn); - self.stats.closed += 1; - } - AcquiredConnOperation::Release(conn) => { - self.release_key(&conn.key); - if (Instant::now() - conn.ts) < self.conn_lifetime { - self.available - .entry(conn.key.clone()) - .or_insert_with(VecDeque::new) - .push_back(Conn(Instant::now(), conn)); - } else { - self.to_close.push(conn); - self.stats.closed += 1; - } - } - AcquiredConnOperation::ReleaseKey(key) => { - // closed - self.stats.closed += 1; - self.release_key(&key); - } - } - - self.check_availibility(ctx); - } -} - -struct Maintenance; - -impl fut::ActorFuture for Maintenance { - type Item = (); - type Error = (); - type Actor = ClientConnector; - - fn poll( - &mut self, act: &mut ClientConnector, ctx: &mut Context, - ) -> Poll { - // check pause duration - if let Paused::Timeout(inst, _) = act.paused { - if inst <= Instant::now() { - act.paused = Paused::No; - } - } - - // collect wait timers - act.collect_waiters(); - - // check waiters - act.check_availibility(ctx); - - Ok(Async::NotReady) - } -} - -#[derive(PartialEq, Hash, Debug, Clone, Copy)] -enum Protocol { - Http, - Https, - Ws, - Wss, -} - -impl Protocol { - fn from(s: &str) -> Option { - match s { - "http" => Some(Protocol::Http), - "https" => Some(Protocol::Https), - "ws" => Some(Protocol::Ws), - "wss" => Some(Protocol::Wss), - _ => None, - } - } - - fn is_http(self) -> bool { - match self { - Protocol::Https | Protocol::Http => true, - _ => false, - } - } - - fn is_secure(self) -> bool { - match self { - Protocol::Https | Protocol::Wss => true, - _ => false, - } - } - - fn port(self) -> u16 { - match self { - Protocol::Http | Protocol::Ws => 80, - Protocol::Https | Protocol::Wss => 443, - } - } -} - -#[derive(Hash, Eq, PartialEq, Clone, Debug)] -struct Key { - host: String, - port: u16, - ssl: bool, -} - -impl Key { - fn empty() -> Key { - Key { - host: String::new(), - port: 0, - ssl: false, - } - } -} - -#[derive(Debug)] -struct Conn(Instant, Connection); - -enum Acquire { - Acquired(Connection), - Available, - NotAvailable, -} - -enum AcquiredConnOperation { - Close(Connection), - Release(Connection), - ReleaseKey(Key), -} - -struct AcquiredConn(Key, Option>); - -impl AcquiredConn { - fn close(&mut self, conn: Connection) { - if let Some(tx) = self.1.take() { - let _ = tx.unbounded_send(AcquiredConnOperation::Close(conn)); - } - } - fn release(&mut self, conn: Connection) { - if let Some(tx) = self.1.take() { - let _ = tx.unbounded_send(AcquiredConnOperation::Release(conn)); - } - } -} - -impl Drop for AcquiredConn { - fn drop(&mut self) { - if let Some(tx) = self.1.take() { - let _ = tx.unbounded_send(AcquiredConnOperation::ReleaseKey(self.0.clone())); - } - } -} - -/// HTTP client connection -pub struct Connection { - key: Key, - stream: Box, - pool: Option, - ts: Instant, -} - -impl fmt::Debug for Connection { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Connection {}:{}", self.key.host, self.key.port) - } -} - -impl Connection { - fn new(key: Key, pool: Option, stream: Box) -> Self { - Connection { - key, - stream, - pool, - ts: Instant::now(), - } - } - - /// Raw IO stream - pub fn stream(&mut self) -> &mut IoStream { - &mut *self.stream - } - - /// Create a new connection from an IO Stream - /// - /// The stream can be a `UnixStream` if the Unix-only "uds" feature is enabled. - /// - /// See also `ClientRequestBuilder::with_connection()`. - pub fn from_stream(io: T) -> Connection { - Connection::new(Key::empty(), None, Box::new(io)) - } - - /// Close connection - pub fn close(mut self) { - if let Some(mut pool) = self.pool.take() { - pool.close(self) - } - } - - /// Release this connection to the connection pool - pub fn release(mut self) { - if let Some(mut pool) = self.pool.take() { - pool.release(self) - } - } -} - -impl IoStream for Connection { - fn shutdown(&mut self, how: Shutdown) -> io::Result<()> { - IoStream::shutdown(&mut *self.stream, how) - } - - #[inline] - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { - IoStream::set_nodelay(&mut *self.stream, nodelay) - } - - #[inline] - fn set_linger(&mut self, dur: Option) -> io::Result<()> { - IoStream::set_linger(&mut *self.stream, dur) - } - - #[inline] - fn set_keepalive(&mut self, dur: Option) -> io::Result<()> { - IoStream::set_keepalive(&mut *self.stream, dur) - } -} - -impl io::Read for Connection { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.stream.read(buf) - } -} - -impl AsyncRead for Connection {} - -impl io::Write for Connection { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.stream.write(buf) - } - - fn flush(&mut self) -> io::Result<()> { - self.stream.flush() - } -} - -impl AsyncWrite for Connection { - fn shutdown(&mut self) -> Poll<(), io::Error> { - self.stream.shutdown() - } -} - -#[cfg(feature = "tls")] -use tokio_tls::TlsStream; - -#[cfg(feature = "tls")] -/// This is temp solution untile actix-net migration -impl IoStream for TlsStream { - #[inline] - fn shutdown(&mut self, _how: Shutdown) -> io::Result<()> { - let _ = self.get_mut().shutdown(); - Ok(()) - } - - #[inline] - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { - self.get_mut().get_mut().set_nodelay(nodelay) - } - - #[inline] - fn set_linger(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().get_mut().set_linger(dur) - } - - #[inline] - fn set_keepalive(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().get_mut().set_keepalive(dur) - } -} diff --git a/src/client/mod.rs b/src/client/mod.rs deleted file mode 100644 index 5321e4b0..00000000 --- a/src/client/mod.rs +++ /dev/null @@ -1,120 +0,0 @@ -//! Http client api -//! -//! ```rust -//! # extern crate actix_web; -//! # extern crate actix; -//! # extern crate futures; -//! # extern crate tokio; -//! # use std::process; -//! use actix_web::client; -//! use futures::Future; -//! -//! fn main() { -//! actix::run( -//! || client::get("http://www.rust-lang.org") // <- Create request builder -//! .header("User-Agent", "Actix-web") -//! .finish().unwrap() -//! .send() // <- Send http request -//! .map_err(|_| ()) -//! .and_then(|response| { // <- server http response -//! println!("Response: {:?}", response); -//! # actix::System::current().stop(); -//! Ok(()) -//! }) -//! ); -//! } -//! ``` -mod connector; -mod parser; -mod pipeline; -mod request; -mod response; -mod writer; - -pub use self::connector::{ - ClientConnector, ClientConnectorError, ClientConnectorStats, Connect, Connection, - Pause, Resume, -}; -pub(crate) use self::parser::{HttpResponseParser, HttpResponseParserError}; -pub(crate) use self::pipeline::Pipeline; -pub use self::pipeline::{SendRequest, SendRequestError}; -pub use self::request::{ClientRequest, ClientRequestBuilder}; -pub use self::response::ClientResponse; -pub(crate) use self::writer::HttpClientWriter; - -use error::ResponseError; -use http::Method; -use httpresponse::HttpResponse; - -/// Convert `SendRequestError` to a `HttpResponse` -impl ResponseError for SendRequestError { - fn error_response(&self) -> HttpResponse { - match *self { - SendRequestError::Timeout => HttpResponse::GatewayTimeout(), - SendRequestError::Connector(_) => HttpResponse::BadGateway(), - _ => HttpResponse::InternalServerError(), - }.into() - } -} - -/// Create request builder for `GET` requests -/// -/// -/// ```rust -/// # extern crate actix_web; -/// # extern crate actix; -/// # extern crate futures; -/// # extern crate tokio; -/// # extern crate env_logger; -/// # use std::process; -/// use actix_web::client; -/// use futures::Future; -/// -/// fn main() { -/// actix::run( -/// || client::get("http://www.rust-lang.org") // <- Create request builder -/// .header("User-Agent", "Actix-web") -/// .finish().unwrap() -/// .send() // <- Send http request -/// .map_err(|_| ()) -/// .and_then(|response| { // <- server http response -/// println!("Response: {:?}", response); -/// # actix::System::current().stop(); -/// Ok(()) -/// }), -/// ); -/// } -/// ``` -pub fn get>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::GET).uri(uri); - builder -} - -/// Create request builder for `HEAD` requests -pub fn head>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::HEAD).uri(uri); - builder -} - -/// Create request builder for `POST` requests -pub fn post>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::POST).uri(uri); - builder -} - -/// Create request builder for `PUT` requests -pub fn put>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::PUT).uri(uri); - builder -} - -/// Create request builder for `DELETE` requests -pub fn delete>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::DELETE).uri(uri); - builder -} diff --git a/src/client/parser.rs b/src/client/parser.rs deleted file mode 100644 index 92a7abe1..00000000 --- a/src/client/parser.rs +++ /dev/null @@ -1,238 +0,0 @@ -use std::mem; - -use bytes::{Bytes, BytesMut}; -use futures::{Async, Poll}; -use http::header::{self, HeaderName, HeaderValue}; -use http::{HeaderMap, StatusCode, Version}; -use httparse; - -use error::{ParseError, PayloadError}; - -use server::h1decoder::{EncodingDecoder, HeaderIndex}; -use server::IoStream; - -use super::response::ClientMessage; -use super::ClientResponse; - -const MAX_BUFFER_SIZE: usize = 131_072; -const MAX_HEADERS: usize = 96; - -#[derive(Default)] -pub struct HttpResponseParser { - decoder: Option, - eof: bool, // indicate that we read payload until stream eof -} - -#[derive(Debug, Fail)] -pub enum HttpResponseParserError { - /// Server disconnected - #[fail(display = "Server disconnected")] - Disconnect, - #[fail(display = "{}", _0)] - Error(#[cause] ParseError), -} - -impl HttpResponseParser { - pub fn parse( - &mut self, io: &mut T, buf: &mut BytesMut, - ) -> Poll - where - T: IoStream, - { - loop { - // Don't call parser until we have data to parse. - if !buf.is_empty() { - match HttpResponseParser::parse_message(buf) - .map_err(HttpResponseParserError::Error)? - { - Async::Ready((msg, info)) => { - if let Some((decoder, eof)) = info { - self.eof = eof; - self.decoder = Some(decoder); - } else { - self.eof = false; - self.decoder = None; - } - return Ok(Async::Ready(msg)); - } - Async::NotReady => { - if buf.len() >= MAX_BUFFER_SIZE { - return Err(HttpResponseParserError::Error( - ParseError::TooLarge, - )); - } - // Parser needs more data. - } - } - } - // Read some more data into the buffer for the parser. - match io.read_available(buf) { - Ok(Async::Ready((false, true))) => { - return Err(HttpResponseParserError::Disconnect) - } - Ok(Async::Ready(_)) => (), - Ok(Async::NotReady) => return Ok(Async::NotReady), - Err(err) => return Err(HttpResponseParserError::Error(err.into())), - } - } - } - - pub fn parse_payload( - &mut self, io: &mut T, buf: &mut BytesMut, - ) -> Poll, PayloadError> - where - T: IoStream, - { - if self.decoder.is_some() { - loop { - // read payload - let (not_ready, stream_finished) = match io.read_available(buf) { - Ok(Async::Ready((_, true))) => (false, true), - Ok(Async::Ready((_, false))) => (false, false), - Ok(Async::NotReady) => (true, false), - Err(err) => return Err(err.into()), - }; - - match self.decoder.as_mut().unwrap().decode(buf) { - Ok(Async::Ready(Some(b))) => return Ok(Async::Ready(Some(b))), - Ok(Async::Ready(None)) => { - self.decoder.take(); - return Ok(Async::Ready(None)); - } - Ok(Async::NotReady) => { - if not_ready { - return Ok(Async::NotReady); - } - if stream_finished { - // read untile eof? - if self.eof { - return Ok(Async::Ready(None)); - } else { - return Err(PayloadError::Incomplete); - } - } - } - Err(err) => return Err(err.into()), - } - } - } else { - Ok(Async::Ready(None)) - } - } - - fn parse_message( - buf: &mut BytesMut, - ) -> Poll<(ClientResponse, Option<(EncodingDecoder, bool)>), ParseError> { - // Unsafe: we read only this data only after httparse parses headers into. - // performance bump for pipeline benchmarks. - let mut headers: [HeaderIndex; MAX_HEADERS] = unsafe { mem::uninitialized() }; - - let (len, version, status, headers_len) = { - let mut parsed: [httparse::Header; MAX_HEADERS] = - unsafe { mem::uninitialized() }; - - let mut resp = httparse::Response::new(&mut parsed); - match resp.parse(buf)? { - httparse::Status::Complete(len) => { - let version = if resp.version.unwrap_or(1) == 1 { - Version::HTTP_11 - } else { - Version::HTTP_10 - }; - HeaderIndex::record(buf, resp.headers, &mut headers); - let status = StatusCode::from_u16(resp.code.unwrap()) - .map_err(|_| ParseError::Status)?; - - (len, version, status, resp.headers.len()) - } - httparse::Status::Partial => return Ok(Async::NotReady), - } - }; - - let slice = buf.split_to(len).freeze(); - - // convert headers - let mut hdrs = HeaderMap::new(); - for idx in headers[..headers_len].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), - ) - }; - hdrs.append(name, value); - } else { - return Err(ParseError::Header); - } - } - - let decoder = if status == StatusCode::SWITCHING_PROTOCOLS { - Some((EncodingDecoder::eof(), true)) - } else if let Some(len) = hdrs.get(header::CONTENT_LENGTH) { - // Content-Length - if let Ok(s) = len.to_str() { - if let Ok(len) = s.parse::() { - Some((EncodingDecoder::length(len), false)) - } else { - debug!("illegal Content-Length: {:?}", len); - return Err(ParseError::Header); - } - } else { - debug!("illegal Content-Length: {:?}", len); - return Err(ParseError::Header); - } - } else if chunked(&hdrs)? { - // Chunked encoding - Some((EncodingDecoder::chunked(), false)) - } else if let Some(value) = hdrs.get(header::CONNECTION) { - let close = if let Ok(s) = value.to_str() { - s == "close" - } else { - false - }; - if close { - Some((EncodingDecoder::eof(), true)) - } else { - None - } - } else { - None - }; - - if let Some(decoder) = decoder { - Ok(Async::Ready(( - ClientResponse::new(ClientMessage { - status, - version, - headers: hdrs, - cookies: None, - }), - Some(decoder), - ))) - } else { - Ok(Async::Ready(( - ClientResponse::new(ClientMessage { - status, - version, - headers: hdrs, - cookies: None, - }), - None, - ))) - } - } -} - -/// Check if request has chunked transfer encoding -pub fn chunked(headers: &HeaderMap) -> Result { - if let Some(encodings) = headers.get(header::TRANSFER_ENCODING) { - if let Ok(s) = encodings.to_str() { - Ok(s.to_lowercase().contains("chunked")) - } else { - Err(ParseError::Header) - } - } else { - Ok(false) - } -} diff --git a/src/client/pipeline.rs b/src/client/pipeline.rs deleted file mode 100644 index 1dbd2e17..00000000 --- a/src/client/pipeline.rs +++ /dev/null @@ -1,553 +0,0 @@ -use bytes::{Bytes, BytesMut}; -use futures::sync::oneshot; -use futures::{Async, Future, Poll, Stream}; -use http::header::CONTENT_ENCODING; -use std::time::{Duration, Instant}; -use std::{io, mem}; -use tokio_timer::Delay; - -use actix_inner::dev::Request; -use actix::{Addr, SystemService}; - -use super::{ - ClientConnector, ClientConnectorError, ClientRequest, ClientResponse, Connect, - Connection, HttpClientWriter, HttpResponseParser, HttpResponseParserError, -}; -use body::{Body, BodyStream}; -use context::{ActorHttpContext, Frame}; -use error::Error; -use error::PayloadError; -use header::ContentEncoding; -use http::{Method, Uri}; -use httpmessage::HttpMessage; -use server::input::PayloadStream; -use server::WriterState; - -/// A set of errors that can occur during request sending and response reading -#[derive(Fail, Debug)] -pub enum SendRequestError { - /// Response took too long - #[fail(display = "Timeout while waiting for response")] - Timeout, - /// Failed to connect to host - #[fail(display = "Failed to connect to host: {}", _0)] - Connector(#[cause] ClientConnectorError), - /// Error parsing response - #[fail(display = "{}", _0)] - ParseError(#[cause] HttpResponseParserError), - /// Error reading response payload - #[fail(display = "Error reading response payload: {}", _0)] - Io(#[cause] io::Error), -} - -impl From for SendRequestError { - fn from(err: io::Error) -> SendRequestError { - SendRequestError::Io(err) - } -} - -impl From for SendRequestError { - fn from(err: ClientConnectorError) -> SendRequestError { - match err { - ClientConnectorError::Timeout => SendRequestError::Timeout, - _ => SendRequestError::Connector(err), - } - } -} - -enum State { - New, - Connect(Request), - Connection(Connection), - Send(Box), - None, -} - -/// `SendRequest` is a `Future` which represents an asynchronous -/// request sending process. -#[must_use = "SendRequest does nothing unless polled"] -pub struct SendRequest { - req: ClientRequest, - state: State, - conn: Option>, - conn_timeout: Duration, - wait_timeout: Duration, - timeout: Option, -} - -impl SendRequest { - pub(crate) fn new(req: ClientRequest) -> SendRequest { - SendRequest { - req, - conn: None, - state: State::New, - timeout: None, - wait_timeout: Duration::from_secs(5), - conn_timeout: Duration::from_secs(1), - } - } - - pub(crate) fn with_connector( - req: ClientRequest, conn: Addr, - ) -> SendRequest { - SendRequest { - req, - conn: Some(conn), - state: State::New, - timeout: None, - wait_timeout: Duration::from_secs(5), - conn_timeout: Duration::from_secs(1), - } - } - - pub(crate) fn with_connection(req: ClientRequest, conn: Connection) -> SendRequest { - SendRequest { - req, - state: State::Connection(conn), - conn: None, - timeout: None, - wait_timeout: Duration::from_secs(5), - conn_timeout: Duration::from_secs(1), - } - } - - /// Set request timeout - /// - /// Request timeout is the total time before a response must be received. - /// Default value is 5 seconds. - pub fn timeout(mut self, timeout: Duration) -> Self { - self.timeout = Some(timeout); - self - } - - /// Set connection timeout - /// - /// Connection timeout includes resolving hostname and actual connection to - /// the host. - /// Default value is 1 second. - pub fn conn_timeout(mut self, timeout: Duration) -> Self { - self.conn_timeout = timeout; - self - } - - /// Set wait timeout - /// - /// If connections pool limits are enabled, wait time indicates max time - /// to wait for available connection. Default value is 5 seconds. - pub fn wait_timeout(mut self, timeout: Duration) -> Self { - self.wait_timeout = timeout; - self - } -} - -impl Future for SendRequest { - type Item = ClientResponse; - type Error = SendRequestError; - - fn poll(&mut self) -> Poll { - loop { - let state = mem::replace(&mut self.state, State::None); - - match state { - State::New => { - let conn = if let Some(conn) = self.conn.take() { - conn - } else { - ClientConnector::from_registry() - }; - self.state = State::Connect(conn.send(Connect { - uri: self.req.uri().clone(), - wait_timeout: self.wait_timeout, - conn_timeout: self.conn_timeout, - })) - } - State::Connect(mut conn) => match conn.poll() { - Ok(Async::NotReady) => { - self.state = State::Connect(conn); - return Ok(Async::NotReady); - } - Ok(Async::Ready(result)) => match result { - Ok(stream) => self.state = State::Connection(stream), - Err(err) => return Err(err.into()), - }, - Err(_) => { - return Err(SendRequestError::Connector( - ClientConnectorError::Disconnected, - )); - } - }, - State::Connection(conn) => { - let mut writer = HttpClientWriter::new(); - writer.start(&mut self.req)?; - - let body = match self.req.replace_body(Body::Empty) { - Body::Streaming(stream) => IoBody::Payload(stream), - Body::Actor(ctx) => IoBody::Actor(ctx), - _ => IoBody::Done, - }; - - let timeout = self - .timeout - .take() - .unwrap_or_else(|| Duration::from_secs(5)); - - let pl = Box::new(Pipeline { - body, - writer, - conn: Some(conn), - parser: Some(HttpResponseParser::default()), - parser_buf: BytesMut::new(), - disconnected: false, - body_completed: false, - drain: None, - decompress: None, - should_decompress: self.req.response_decompress(), - write_state: RunningState::Running, - timeout: Some(Delay::new(Instant::now() + timeout)), - meth: self.req.method().clone(), - path: self.req.uri().clone(), - }); - self.state = State::Send(pl); - } - State::Send(mut pl) => { - pl.poll_timeout()?; - pl.poll_write().map_err(|e| { - io::Error::new(io::ErrorKind::Other, format!("{}", e).as_str()) - })?; - - match pl.parse() { - Ok(Async::Ready(mut resp)) => { - if self.req.method() == Method::HEAD { - pl.parser.take(); - } - resp.set_pipeline(pl); - return Ok(Async::Ready(resp)); - } - Ok(Async::NotReady) => { - self.state = State::Send(pl); - return Ok(Async::NotReady); - } - Err(err) => { - return Err(SendRequestError::ParseError(err)); - } - } - } - State::None => unreachable!(), - } - } - } -} - -pub struct Pipeline { - body: IoBody, - body_completed: bool, - conn: Option, - writer: HttpClientWriter, - parser: Option, - parser_buf: BytesMut, - disconnected: bool, - drain: Option>, - decompress: Option, - should_decompress: bool, - write_state: RunningState, - timeout: Option, - meth: Method, - path: Uri, -} - -enum IoBody { - Payload(BodyStream), - Actor(Box), - Done, -} - -#[derive(Debug, PartialEq)] -enum RunningState { - Running, - Paused, - Done, -} - -impl RunningState { - #[inline] - fn pause(&mut self) { - if *self != RunningState::Done { - *self = RunningState::Paused - } - } - #[inline] - fn resume(&mut self) { - if *self != RunningState::Done { - *self = RunningState::Running - } - } -} - -impl Pipeline { - fn release_conn(&mut self) { - if let Some(conn) = self.conn.take() { - if self.meth == Method::HEAD { - conn.close() - } else { - conn.release() - } - } - } - - #[inline] - fn parse(&mut self) -> Poll { - if let Some(ref mut conn) = self.conn { - match self - .parser - .as_mut() - .unwrap() - .parse(conn, &mut self.parser_buf) - { - Ok(Async::Ready(resp)) => { - // check content-encoding - if self.should_decompress { - if let Some(enc) = resp.headers().get(CONTENT_ENCODING) { - if let Ok(enc) = enc.to_str() { - match ContentEncoding::from(enc) { - ContentEncoding::Auto - | ContentEncoding::Identity => (), - enc => { - self.decompress = Some(PayloadStream::new(enc)) - } - } - } - } - } - - Ok(Async::Ready(resp)) - } - val => val, - } - } else { - Ok(Async::NotReady) - } - } - - #[inline] - pub(crate) fn poll(&mut self) -> Poll, PayloadError> { - if self.conn.is_none() { - return Ok(Async::Ready(None)); - } - let mut need_run = false; - - // need write? - match self - .poll_write() - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{}", e)))? - { - Async::NotReady => need_run = true, - Async::Ready(_) => { - self.poll_timeout().map_err(|e| { - io::Error::new(io::ErrorKind::Other, format!("{}", e)) - })?; - } - } - - // need read? - if self.parser.is_some() { - let conn: &mut Connection = self.conn.as_mut().unwrap(); - - loop { - match self - .parser - .as_mut() - .unwrap() - .parse_payload(conn, &mut self.parser_buf)? - { - Async::Ready(Some(b)) => { - if let Some(ref mut decompress) = self.decompress { - match decompress.feed_data(b) { - Ok(Some(b)) => return Ok(Async::Ready(Some(b))), - Ok(None) => return Ok(Async::NotReady), - Err(ref err) - if err.kind() == io::ErrorKind::WouldBlock => - { - continue - } - Err(err) => return Err(err.into()), - } - } else { - return Ok(Async::Ready(Some(b))); - } - } - Async::Ready(None) => { - let _ = self.parser.take(); - break; - } - Async::NotReady => return Ok(Async::NotReady), - } - } - } - - // eof - if let Some(mut decompress) = self.decompress.take() { - let res = decompress.feed_eof(); - if let Some(b) = res? { - self.release_conn(); - return Ok(Async::Ready(Some(b))); - } - } - - if need_run { - Ok(Async::NotReady) - } else { - self.release_conn(); - Ok(Async::Ready(None)) - } - } - - fn poll_timeout(&mut self) -> Result<(), SendRequestError> { - if self.timeout.is_some() { - match self.timeout.as_mut().unwrap().poll() { - Ok(Async::Ready(())) => return Err(SendRequestError::Timeout), - Ok(Async::NotReady) => (), - Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e).into()), - } - } - Ok(()) - } - - #[inline] - fn poll_write(&mut self) -> Poll<(), Error> { - if self.write_state == RunningState::Done || self.conn.is_none() { - return Ok(Async::Ready(())); - } - - let mut done = false; - if self.drain.is_none() && self.write_state != RunningState::Paused { - 'outter: loop { - let result = match mem::replace(&mut self.body, IoBody::Done) { - IoBody::Payload(mut body) => match body.poll()? { - Async::Ready(None) => { - self.writer.write_eof()?; - self.body_completed = true; - break; - } - Async::Ready(Some(chunk)) => { - self.body = IoBody::Payload(body); - self.writer.write(chunk.as_ref())? - } - Async::NotReady => { - done = true; - self.body = IoBody::Payload(body); - break; - } - }, - IoBody::Actor(mut ctx) => { - if self.disconnected { - ctx.disconnected(); - } - match ctx.poll()? { - Async::Ready(Some(vec)) => { - if vec.is_empty() { - self.body = IoBody::Actor(ctx); - break; - } - let mut res = None; - for frame in vec { - match frame { - Frame::Chunk(None) => { - self.body_completed = true; - self.writer.write_eof()?; - break 'outter; - } - Frame::Chunk(Some(chunk)) => { - res = - Some(self.writer.write(chunk.as_ref())?) - } - Frame::Drain(fut) => self.drain = Some(fut), - } - } - self.body = IoBody::Actor(ctx); - if self.drain.is_some() { - self.write_state.resume(); - break; - } - res.unwrap() - } - Async::Ready(None) => { - done = true; - break; - } - Async::NotReady => { - done = true; - self.body = IoBody::Actor(ctx); - break; - } - } - } - IoBody::Done => { - self.body_completed = true; - done = true; - break; - } - }; - - match result { - WriterState::Pause => { - self.write_state.pause(); - break; - } - WriterState::Done => self.write_state.resume(), - } - } - } - - // flush io but only if we need to - match self - .writer - .poll_completed(self.conn.as_mut().unwrap(), false) - { - Ok(Async::Ready(_)) => { - if self.disconnected - || (self.body_completed && self.writer.is_completed()) - { - self.write_state = RunningState::Done; - } else { - self.write_state.resume(); - } - - // resolve drain futures - if let Some(tx) = self.drain.take() { - let _ = tx.send(()); - } - // restart io processing - if !done || self.write_state == RunningState::Done { - self.poll_write() - } else { - Ok(Async::NotReady) - } - } - Ok(Async::NotReady) => Ok(Async::NotReady), - Err(err) => Err(err.into()), - } - } -} - -impl Drop for Pipeline { - fn drop(&mut self) { - if let Some(conn) = self.conn.take() { - debug!( - "Client http transaction is not completed, dropping connection: {:?} {:?}", - self.meth, - self.path, - ); - conn.close() - } - } -} - -/// Future that resolves to a complete request body. -impl Stream for Box { - type Item = Bytes; - type Error = PayloadError; - - fn poll(&mut self) -> Poll, Self::Error> { - Pipeline::poll(self) - } -} diff --git a/src/client/request.rs b/src/client/request.rs deleted file mode 100644 index ad08ad13..00000000 --- a/src/client/request.rs +++ /dev/null @@ -1,783 +0,0 @@ -use std::fmt::Write as FmtWrite; -use std::io::Write; -use std::time::Duration; -use std::{fmt, mem}; - -use actix::Addr; -use bytes::{BufMut, Bytes, BytesMut}; -use cookie::{Cookie, CookieJar}; -use futures::Stream; -use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; -use serde::Serialize; -use serde_json; -use serde_urlencoded; -use url::Url; - -use super::connector::{ClientConnector, Connection}; -use super::pipeline::SendRequest; -use body::Body; -use error::Error; -use header::{ContentEncoding, Header, IntoHeaderValue}; -use http::header::{self, HeaderName, HeaderValue}; -use http::{uri, Error as HttpError, HeaderMap, HttpTryFrom, Method, Uri, Version}; -use httpmessage::HttpMessage; -use httprequest::HttpRequest; - -/// An HTTP Client Request -/// -/// ```rust -/// # extern crate actix_web; -/// # extern crate actix; -/// # extern crate futures; -/// # extern crate tokio; -/// # use futures::Future; -/// # use std::process; -/// use actix_web::client; -/// -/// fn main() { -/// actix::run( -/// || client::ClientRequest::get("http://www.rust-lang.org") // <- Create request builder -/// .header("User-Agent", "Actix-web") -/// .finish().unwrap() -/// .send() // <- Send http request -/// .map_err(|_| ()) -/// .and_then(|response| { // <- server http response -/// println!("Response: {:?}", response); -/// # actix::System::current().stop(); -/// Ok(()) -/// }), -/// ); -/// } -/// ``` -pub struct ClientRequest { - uri: Uri, - method: Method, - version: Version, - headers: HeaderMap, - body: Body, - chunked: bool, - upgrade: bool, - timeout: Option, - encoding: ContentEncoding, - response_decompress: bool, - buffer_capacity: usize, - conn: ConnectionType, -} - -enum ConnectionType { - Default, - Connector(Addr), - Connection(Connection), -} - -impl Default for ClientRequest { - fn default() -> ClientRequest { - ClientRequest { - uri: Uri::default(), - method: Method::default(), - version: Version::HTTP_11, - headers: HeaderMap::with_capacity(16), - body: Body::Empty, - chunked: false, - upgrade: false, - timeout: None, - encoding: ContentEncoding::Auto, - response_decompress: true, - buffer_capacity: 32_768, - conn: ConnectionType::Default, - } - } -} - -impl ClientRequest { - /// Create request builder for `GET` request - pub fn get>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::GET).uri(uri); - builder - } - - /// Create request builder for `HEAD` request - pub fn head>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::HEAD).uri(uri); - builder - } - - /// Create request builder for `POST` request - pub fn post>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::POST).uri(uri); - builder - } - - /// Create request builder for `PUT` request - pub fn put>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::PUT).uri(uri); - builder - } - - /// Create request builder for `DELETE` request - pub fn delete>(uri: U) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - builder.method(Method::DELETE).uri(uri); - builder - } -} - -impl ClientRequest { - /// Create client request builder - pub fn build() -> ClientRequestBuilder { - ClientRequestBuilder { - request: Some(ClientRequest::default()), - err: None, - cookies: None, - default_headers: true, - } - } - - /// Create client request builder - pub fn build_from>(source: T) -> ClientRequestBuilder { - source.into() - } - - /// Get the request URI - #[inline] - pub fn uri(&self) -> &Uri { - &self.uri - } - - /// Set client request URI - #[inline] - pub fn set_uri(&mut self, uri: Uri) { - self.uri = uri - } - - /// Get the request method - #[inline] - pub fn method(&self) -> &Method { - &self.method - } - - /// Set HTTP `Method` for the request - #[inline] - pub fn set_method(&mut self, method: Method) { - self.method = method - } - - /// Get HTTP version for the request - #[inline] - pub fn version(&self) -> Version { - self.version - } - - /// Set http `Version` for the request - #[inline] - pub fn set_version(&mut self, version: Version) { - self.version = version - } - - /// Get the headers from the request - #[inline] - pub fn headers(&self) -> &HeaderMap { - &self.headers - } - - /// Get a mutable reference to the headers - #[inline] - pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.headers - } - - /// is chunked encoding enabled - #[inline] - pub fn chunked(&self) -> bool { - self.chunked - } - - /// is upgrade request - #[inline] - pub fn upgrade(&self) -> bool { - self.upgrade - } - - /// Content encoding - #[inline] - pub fn content_encoding(&self) -> ContentEncoding { - self.encoding - } - - /// Decompress response payload - #[inline] - pub fn response_decompress(&self) -> bool { - self.response_decompress - } - - /// Requested write buffer capacity - pub fn write_buffer_capacity(&self) -> usize { - self.buffer_capacity - } - - /// Get body of this response - #[inline] - pub fn body(&self) -> &Body { - &self.body - } - - /// Set a body - pub fn set_body>(&mut self, body: B) { - self.body = body.into(); - } - - /// Extract body, replace it with `Empty` - pub(crate) fn replace_body(&mut self, body: Body) -> Body { - mem::replace(&mut self.body, body) - } - - /// Send request - /// - /// This method returns a future that resolves to a ClientResponse - pub fn send(mut self) -> SendRequest { - let timeout = self.timeout.take(); - let send = match mem::replace(&mut self.conn, ConnectionType::Default) { - ConnectionType::Default => SendRequest::new(self), - ConnectionType::Connector(conn) => SendRequest::with_connector(self, conn), - ConnectionType::Connection(conn) => SendRequest::with_connection(self, conn), - }; - if let Some(timeout) = timeout { - send.timeout(timeout) - } else { - send - } - } -} - -impl fmt::Debug for ClientRequest { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!( - f, - "\nClientRequest {:?} {}:{}", - self.version, self.method, self.uri - )?; - writeln!(f, " headers:")?; - for (key, val) in self.headers.iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } -} - -/// An HTTP Client request builder -/// -/// This type can be used to construct an instance of `ClientRequest` through a -/// builder-like pattern. -pub struct ClientRequestBuilder { - request: Option, - err: Option, - cookies: Option, - default_headers: bool, -} - -impl ClientRequestBuilder { - /// Set HTTP URI of request. - #[inline] - pub fn uri>(&mut self, uri: U) -> &mut Self { - match Url::parse(uri.as_ref()) { - Ok(url) => self._uri(url.as_str()), - Err(_) => self._uri(uri.as_ref()), - } - } - - fn _uri(&mut self, url: &str) -> &mut Self { - match Uri::try_from(url) { - Ok(uri) => { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.uri = uri; - } - } - Err(e) => self.err = Some(e.into()), - } - self - } - - /// Set HTTP method of this request. - #[inline] - pub fn method(&mut self, method: Method) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.method = method; - } - self - } - - /// Set HTTP method of this request. - #[inline] - pub fn get_method(&mut self) -> &Method { - let parts = self.request.as_ref().expect("cannot reuse request builder"); - &parts.method - } - - /// Set HTTP version of this request. - /// - /// By default requests's HTTP version depends on network stream - #[inline] - pub fn version(&mut self, version: Version) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.version = version; - } - self - } - - /// Set a header. - /// - /// ```rust - /// # extern crate mime; - /// # extern crate actix_web; - /// # use actix_web::client::*; - /// # - /// use actix_web::{client, http}; - /// - /// fn main() { - /// let req = client::ClientRequest::build() - /// .set(http::header::Date::now()) - /// .set(http::header::ContentType(mime::TEXT_HTML)) - /// .finish() - /// .unwrap(); - /// } - /// ``` - #[doc(hidden)] - pub fn set(&mut self, hdr: H) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - match hdr.try_into() { - Ok(value) => { - parts.headers.insert(H::name(), value); - } - Err(e) => self.err = Some(e.into()), - } - } - self - } - - /// Append a header. - /// - /// Header gets appended to existing header. - /// To override header use `set_header()` method. - /// - /// ```rust - /// # extern crate http; - /// # extern crate actix_web; - /// # use actix_web::client::*; - /// # - /// use http::header; - /// - /// fn main() { - /// let req = ClientRequest::build() - /// .header("X-TEST", "value") - /// .header(header::CONTENT_TYPE, "application/json") - /// .finish() - /// .unwrap(); - /// } - /// ``` - pub fn header(&mut self, key: K, value: V) -> &mut Self - where - HeaderName: HttpTryFrom, - V: IntoHeaderValue, - { - if let Some(parts) = parts(&mut self.request, &self.err) { - match HeaderName::try_from(key) { - Ok(key) => match value.try_into() { - Ok(value) => { - parts.headers.append(key, value); - } - Err(e) => self.err = Some(e.into()), - }, - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Set a header. - pub fn set_header(&mut self, key: K, value: V) -> &mut Self - where - HeaderName: HttpTryFrom, - V: IntoHeaderValue, - { - if let Some(parts) = parts(&mut self.request, &self.err) { - match HeaderName::try_from(key) { - Ok(key) => match value.try_into() { - Ok(value) => { - parts.headers.insert(key, value); - } - Err(e) => self.err = Some(e.into()), - }, - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Set a header only if it is not yet set. - pub fn set_header_if_none(&mut self, key: K, value: V) -> &mut Self - where - HeaderName: HttpTryFrom, - V: IntoHeaderValue, - { - if let Some(parts) = parts(&mut self.request, &self.err) { - match HeaderName::try_from(key) { - Ok(key) => if !parts.headers.contains_key(&key) { - match value.try_into() { - Ok(value) => { - parts.headers.insert(key, value); - } - Err(e) => self.err = Some(e.into()), - } - }, - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Set content encoding. - /// - /// By default `ContentEncoding::Identity` is used. - #[inline] - pub fn content_encoding(&mut self, enc: ContentEncoding) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.encoding = enc; - } - self - } - - /// Enables automatic chunked transfer encoding - #[inline] - pub fn chunked(&mut self) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.chunked = true; - } - self - } - - /// Enable connection upgrade - #[inline] - pub fn upgrade(&mut self) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.upgrade = true; - } - self - } - - /// Set request's content type - #[inline] - pub fn content_type(&mut self, value: V) -> &mut Self - where - HeaderValue: HttpTryFrom, - { - if let Some(parts) = parts(&mut self.request, &self.err) { - match HeaderValue::try_from(value) { - Ok(value) => { - parts.headers.insert(header::CONTENT_TYPE, value); - } - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Set content length - #[inline] - pub fn content_length(&mut self, len: u64) -> &mut Self { - let mut wrt = BytesMut::new().writer(); - let _ = write!(wrt, "{}", len); - self.header(header::CONTENT_LENGTH, wrt.get_mut().take().freeze()) - } - - /// Set a cookie - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{client, http}; - /// - /// fn main() { - /// let req = client::ClientRequest::build() - /// .cookie( - /// http::Cookie::build("name", "value") - /// .domain("www.rust-lang.org") - /// .path("/") - /// .secure(true) - /// .http_only(true) - /// .finish(), - /// ) - /// .finish() - /// .unwrap(); - /// } - /// ``` - pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self { - if self.cookies.is_none() { - let mut jar = CookieJar::new(); - jar.add(cookie.into_owned()); - self.cookies = Some(jar) - } else { - self.cookies.as_mut().unwrap().add(cookie.into_owned()); - } - self - } - - /// Do not add default request headers. - /// By default `Accept-Encoding` and `User-Agent` headers are set. - pub fn no_default_headers(&mut self) -> &mut Self { - self.default_headers = false; - self - } - - /// Disable automatic decompress response body - pub fn disable_decompress(&mut self) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.response_decompress = false; - } - self - } - - /// Set write buffer capacity - /// - /// Default buffer capacity is 32kb - pub fn write_buffer_capacity(&mut self, cap: usize) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.buffer_capacity = cap; - } - self - } - - /// Set request timeout - /// - /// Request timeout is a total time before response should be received. - /// Default value is 5 seconds. - pub fn timeout(&mut self, timeout: Duration) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.timeout = Some(timeout); - } - self - } - - /// Send request using custom connector - pub fn with_connector(&mut self, conn: Addr) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.conn = ConnectionType::Connector(conn); - } - self - } - - /// Send request using existing `Connection` - pub fn with_connection(&mut self, conn: Connection) -> &mut Self { - if let Some(parts) = parts(&mut self.request, &self.err) { - parts.conn = ConnectionType::Connection(conn); - } - self - } - - /// This method calls provided closure with builder reference if - /// value is `true`. - pub fn if_true(&mut self, value: bool, f: F) -> &mut Self - where - F: FnOnce(&mut ClientRequestBuilder), - { - if value { - f(self); - } - self - } - - /// This method calls provided closure with builder reference if - /// value is `Some`. - pub fn if_some(&mut self, value: Option, f: F) -> &mut Self - where - F: FnOnce(T, &mut ClientRequestBuilder), - { - if let Some(val) = value { - f(val, self); - } - self - } - - /// Set a body and generate `ClientRequest`. - /// - /// `ClientRequestBuilder` can not be used after this call. - pub fn body>(&mut self, body: B) -> Result { - if let Some(e) = self.err.take() { - return Err(e.into()); - } - - if self.default_headers { - // enable br only for https - let https = if let Some(parts) = parts(&mut self.request, &self.err) { - parts - .uri - .scheme_part() - .map(|s| s == &uri::Scheme::HTTPS) - .unwrap_or(true) - } else { - true - }; - - if https { - self.set_header_if_none(header::ACCEPT_ENCODING, "br, gzip, deflate"); - } else { - self.set_header_if_none(header::ACCEPT_ENCODING, "gzip, deflate"); - } - - // set request host header - if let Some(parts) = parts(&mut self.request, &self.err) { - if let Some(host) = parts.uri.host() { - if !parts.headers.contains_key(header::HOST) { - let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); - - let _ = match parts.uri.port_part().map(|port| port.as_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) => { - parts.headers.insert(header::HOST, value); - } - Err(e) => self.err = Some(e.into()), - } - } - } - } - - // user agent - self.set_header_if_none( - header::USER_AGENT, - concat!("actix-web/", env!("CARGO_PKG_VERSION")), - ); - } - - let mut request = self.request.take().expect("cannot reuse request builder"); - - // set cookies - if let Some(ref mut jar) = self.cookies { - let mut cookie = String::new(); - for c in jar.delta() { - let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); - let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); - let _ = write!(&mut cookie, "; {}={}", name, value); - } - request.headers.insert( - header::COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), - ); - } - request.body = body.into(); - Ok(request) - } - - /// Set a JSON body and generate `ClientRequest` - /// - /// `ClientRequestBuilder` can not be used after this call. - pub fn json(&mut self, value: T) -> Result { - let body = serde_json::to_string(&value)?; - - let contains = if let Some(parts) = parts(&mut self.request, &self.err) { - parts.headers.contains_key(header::CONTENT_TYPE) - } else { - true - }; - if !contains { - self.header(header::CONTENT_TYPE, "application/json"); - } - - self.body(body) - } - - /// Set a urlencoded body and generate `ClientRequest` - /// - /// `ClientRequestBuilder` can not be used after this call. - pub fn form(&mut self, value: T) -> Result { - let body = serde_urlencoded::to_string(&value)?; - - let contains = if let Some(parts) = parts(&mut self.request, &self.err) { - parts.headers.contains_key(header::CONTENT_TYPE) - } else { - true - }; - if !contains { - self.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded"); - } - - self.body(body) - } - - /// Set a streaming body and generate `ClientRequest`. - /// - /// `ClientRequestBuilder` can not be used after this call. - pub fn streaming(&mut self, stream: S) -> Result - where - S: Stream + 'static, - E: Into, - { - self.body(Body::Streaming(Box::new(stream.map_err(|e| e.into())))) - } - - /// Set an empty body and generate `ClientRequest` - /// - /// `ClientRequestBuilder` can not be used after this call. - pub fn finish(&mut self) -> Result { - self.body(Body::Empty) - } - - /// This method construct new `ClientRequestBuilder` - pub fn take(&mut self) -> ClientRequestBuilder { - ClientRequestBuilder { - request: self.request.take(), - err: self.err.take(), - cookies: self.cookies.take(), - default_headers: self.default_headers, - } - } -} - -#[inline] -fn parts<'a>( - parts: &'a mut Option, err: &Option, -) -> Option<&'a mut ClientRequest> { - if err.is_some() { - return None; - } - parts.as_mut() -} - -impl fmt::Debug for ClientRequestBuilder { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if let Some(ref parts) = self.request { - writeln!( - f, - "\nClientRequestBuilder {:?} {}:{}", - parts.version, parts.method, parts.uri - )?; - writeln!(f, " headers:")?; - for (key, val) in parts.headers.iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } else { - write!(f, "ClientRequestBuilder(Consumed)") - } - } -} - -/// Create `ClientRequestBuilder` from `HttpRequest` -/// -/// It is useful for proxy requests. This implementation -/// copies all request headers and the method. -impl<'a, S: 'static> From<&'a HttpRequest> for ClientRequestBuilder { - fn from(req: &'a HttpRequest) -> ClientRequestBuilder { - let mut builder = ClientRequest::build(); - for (key, value) in req.headers() { - builder.header(key.clone(), value.clone()); - } - builder.method(req.method().clone()); - builder - } -} diff --git a/src/client/response.rs b/src/client/response.rs deleted file mode 100644 index 5f1f4264..00000000 --- a/src/client/response.rs +++ /dev/null @@ -1,124 +0,0 @@ -use std::cell::RefCell; -use std::{fmt, str}; - -use cookie::Cookie; -use http::header::{self, HeaderValue}; -use http::{HeaderMap, StatusCode, Version}; - -use error::CookieParseError; -use httpmessage::HttpMessage; - -use super::pipeline::Pipeline; - -pub(crate) struct ClientMessage { - pub status: StatusCode, - pub version: Version, - pub headers: HeaderMap, - pub cookies: Option>>, -} - -impl Default for ClientMessage { - fn default() -> ClientMessage { - ClientMessage { - status: StatusCode::OK, - version: Version::HTTP_11, - headers: HeaderMap::with_capacity(16), - cookies: None, - } - } -} - -/// An HTTP Client response -pub struct ClientResponse(ClientMessage, RefCell>>); - -impl HttpMessage for ClientResponse { - type Stream = Box; - - /// Get the headers from the response. - #[inline] - fn headers(&self) -> &HeaderMap { - &self.0.headers - } - - #[inline] - fn payload(&self) -> Box { - self.1 - .borrow_mut() - .take() - .expect("Payload is already consumed.") - } -} - -impl ClientResponse { - pub(crate) fn new(msg: ClientMessage) -> ClientResponse { - ClientResponse(msg, RefCell::new(None)) - } - - pub(crate) fn set_pipeline(&mut self, pl: Box) { - *self.1.borrow_mut() = Some(pl); - } - - /// Get the HTTP version of this response. - #[inline] - pub fn version(&self) -> Version { - self.0.version - } - - /// Get the status from the server. - #[inline] - pub fn status(&self) -> StatusCode { - self.0.status - } - - /// Load response cookies. - pub fn cookies(&self) -> Result>, CookieParseError> { - let mut cookies = Vec::new(); - for val in self.0.headers.get_all(header::SET_COOKIE).iter() { - let s = str::from_utf8(val.as_bytes()).map_err(CookieParseError::from)?; - cookies.push(Cookie::parse_encoded(s)?.into_owned()); - } - Ok(cookies) - } - - /// Return request cookie. - pub fn cookie(&self, name: &str) -> Option { - if let Ok(cookies) = self.cookies() { - for cookie in cookies { - if cookie.name() == name { - return Some(cookie); - } - } - } - None - } -} - -impl fmt::Debug for ClientResponse { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "\nClientResponse {:?} {}", self.version(), self.status())?; - writeln!(f, " headers:")?; - for (key, val) in self.headers().iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_debug() { - let mut resp = ClientResponse::new(ClientMessage::default()); - resp.0 - .headers - .insert(header::COOKIE, HeaderValue::from_static("cookie1=value1")); - resp.0 - .headers - .insert(header::COOKIE, HeaderValue::from_static("cookie2=value2")); - - let dbg = format!("{:?}", resp); - assert!(dbg.contains("ClientResponse")); - } -} diff --git a/src/client/writer.rs b/src/client/writer.rs deleted file mode 100644 index 321753bb..00000000 --- a/src/client/writer.rs +++ /dev/null @@ -1,412 +0,0 @@ -#![cfg_attr( - feature = "cargo-clippy", - allow(redundant_field_names) -)] - -use std::cell::RefCell; -use std::fmt::Write as FmtWrite; -use std::io::{self, Write}; - -#[cfg(feature = "brotli")] -use brotli2::write::BrotliEncoder; -use bytes::{BufMut, BytesMut}; -#[cfg(feature = "flate2")] -use flate2::write::{GzEncoder, ZlibEncoder}; -#[cfg(feature = "flate2")] -use flate2::Compression; -use futures::{Async, Poll}; -use http::header::{ - HeaderValue, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, -}; -use http::{HttpTryFrom, Version}; -use time::{self, Duration}; -use tokio_io::AsyncWrite; - -use body::{Binary, Body}; -use header::ContentEncoding; -use server::output::{ContentEncoder, Output, TransferEncoding}; -use server::WriterState; - -use client::ClientRequest; - -const AVERAGE_HEADER_SIZE: usize = 30; - -bitflags! { - struct Flags: u8 { - const STARTED = 0b0000_0001; - const UPGRADE = 0b0000_0010; - const KEEPALIVE = 0b0000_0100; - const DISCONNECTED = 0b0000_1000; - } -} - -pub(crate) struct HttpClientWriter { - flags: Flags, - written: u64, - headers_size: u32, - buffer: Output, - buffer_capacity: usize, -} - -impl HttpClientWriter { - pub fn new() -> HttpClientWriter { - HttpClientWriter { - flags: Flags::empty(), - written: 0, - headers_size: 0, - buffer_capacity: 0, - buffer: Output::Buffer(BytesMut::new()), - } - } - - pub fn disconnected(&mut self) { - self.buffer.take(); - } - - pub fn is_completed(&self) -> bool { - self.buffer.is_empty() - } - - // pub fn keepalive(&self) -> bool { - // self.flags.contains(Flags::KEEPALIVE) && - // !self.flags.contains(Flags::UPGRADE) } - - fn write_to_stream( - &mut self, stream: &mut T, - ) -> io::Result { - while !self.buffer.is_empty() { - match stream.write(self.buffer.as_ref().as_ref()) { - Ok(0) => { - self.disconnected(); - return Ok(WriterState::Done); - } - Ok(n) => { - let _ = self.buffer.split_to(n); - } - Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { - if self.buffer.len() > self.buffer_capacity { - return Ok(WriterState::Pause); - } else { - return Ok(WriterState::Done); - } - } - Err(err) => return Err(err), - } - } - Ok(WriterState::Done) - } -} - -pub 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(()) - } -} - -impl HttpClientWriter { - pub fn start(&mut self, msg: &mut ClientRequest) -> io::Result<()> { - // prepare task - self.buffer = content_encoder(self.buffer.take(), msg); - self.flags.insert(Flags::STARTED); - if msg.upgrade() { - self.flags.insert(Flags::UPGRADE); - } - - // render message - { - // output buffer - let buffer = self.buffer.as_mut(); - - // status line - writeln!( - Writer(buffer), - "{} {} {:?}\r", - msg.method(), - msg.uri() - .path_and_query() - .map(|u| u.as_str()) - .unwrap_or("/"), - msg.version() - ).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - - // write headers - if let Body::Binary(ref bytes) = *msg.body() { - buffer.reserve(msg.headers().len() * AVERAGE_HEADER_SIZE + bytes.len()); - } else { - buffer.reserve(msg.headers().len() * AVERAGE_HEADER_SIZE); - } - - for (key, value) in msg.headers() { - let v = value.as_ref(); - let k = key.as_str().as_bytes(); - buffer.reserve(k.len() + v.len() + 4); - buffer.put_slice(k); - buffer.put_slice(b": "); - buffer.put_slice(v); - buffer.put_slice(b"\r\n"); - } - - // set date header - if !msg.headers().contains_key(DATE) { - buffer.extend_from_slice(b"date: "); - set_date(buffer); - buffer.extend_from_slice(b"\r\n\r\n"); - } else { - buffer.extend_from_slice(b"\r\n"); - } - } - self.headers_size = self.buffer.len() as u32; - - if msg.body().is_binary() { - if let Body::Binary(bytes) = msg.replace_body(Body::Empty) { - self.written += bytes.len() as u64; - self.buffer.write(bytes.as_ref())?; - } - } else { - self.buffer_capacity = msg.write_buffer_capacity(); - } - Ok(()) - } - - pub fn write(&mut self, payload: &[u8]) -> io::Result { - self.written += payload.len() as u64; - if !self.flags.contains(Flags::DISCONNECTED) { - self.buffer.write(payload)?; - } - - if self.buffer.len() > self.buffer_capacity { - Ok(WriterState::Pause) - } else { - Ok(WriterState::Done) - } - } - - pub fn write_eof(&mut self) -> io::Result<()> { - if self.buffer.write_eof()? { - Ok(()) - } else { - Err(io::Error::new( - io::ErrorKind::Other, - "Last payload item, but eof is not reached", - )) - } - } - - #[inline] - pub fn poll_completed( - &mut self, stream: &mut T, shutdown: bool, - ) -> Poll<(), io::Error> { - match self.write_to_stream(stream) { - Ok(WriterState::Done) => { - if shutdown { - stream.shutdown() - } else { - Ok(Async::Ready(())) - } - } - Ok(WriterState::Pause) => Ok(Async::NotReady), - Err(err) => Err(err), - } - } -} - -fn content_encoder(buf: BytesMut, req: &mut ClientRequest) -> Output { - let version = req.version(); - let mut body = req.replace_body(Body::Empty); - let mut encoding = req.content_encoding(); - - let transfer = match body { - Body::Empty => { - req.headers_mut().remove(CONTENT_LENGTH); - return Output::Empty(buf); - } - Body::Binary(ref mut bytes) => { - #[cfg(any(feature = "flate2", feature = "brotli"))] - { - if encoding.is_compression() { - let mut tmp = BytesMut::new(); - let mut transfer = TransferEncoding::eof(tmp); - let mut enc = match encoding { - #[cfg(feature = "flate2")] - ContentEncoding::Deflate => ContentEncoder::Deflate( - ZlibEncoder::new(transfer, Compression::default()), - ), - #[cfg(feature = "flate2")] - ContentEncoding::Gzip => ContentEncoder::Gzip(GzEncoder::new( - transfer, - Compression::default(), - )), - #[cfg(feature = "brotli")] - ContentEncoding::Br => { - ContentEncoder::Br(BrotliEncoder::new(transfer, 5)) - } - ContentEncoding::Auto | ContentEncoding::Identity => { - unreachable!() - } - }; - // TODO return error! - let _ = enc.write(bytes.as_ref()); - let _ = enc.write_eof(); - *bytes = Binary::from(enc.buf_mut().take()); - - req.headers_mut().insert( - CONTENT_ENCODING, - HeaderValue::from_static(encoding.as_str()), - ); - encoding = ContentEncoding::Identity; - } - let mut b = BytesMut::new(); - let _ = write!(b, "{}", bytes.len()); - req.headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::try_from(b.freeze()).unwrap()); - TransferEncoding::eof(buf) - } - #[cfg(not(any(feature = "flate2", feature = "brotli")))] - { - let mut b = BytesMut::new(); - let _ = write!(b, "{}", bytes.len()); - req.headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::try_from(b.freeze()).unwrap()); - TransferEncoding::eof(buf) - } - } - Body::Streaming(_) | Body::Actor(_) => { - if req.upgrade() { - if version == Version::HTTP_2 { - error!("Connection upgrade is forbidden for HTTP/2"); - } else { - req.headers_mut() - .insert(CONNECTION, HeaderValue::from_static("upgrade")); - } - if encoding != ContentEncoding::Identity { - encoding = ContentEncoding::Identity; - req.headers_mut().remove(CONTENT_ENCODING); - } - TransferEncoding::eof(buf) - } else { - streaming_encoding(buf, version, req) - } - } - }; - - if encoding.is_compression() { - req.headers_mut().insert( - CONTENT_ENCODING, - HeaderValue::from_static(encoding.as_str()), - ); - } - - req.replace_body(body); - let enc = match encoding { - #[cfg(feature = "flate2")] - ContentEncoding::Deflate => { - ContentEncoder::Deflate(ZlibEncoder::new(transfer, Compression::default())) - } - #[cfg(feature = "flate2")] - ContentEncoding::Gzip => { - ContentEncoder::Gzip(GzEncoder::new(transfer, Compression::default())) - } - #[cfg(feature = "brotli")] - ContentEncoding::Br => ContentEncoder::Br(BrotliEncoder::new(transfer, 5)), - ContentEncoding::Identity | ContentEncoding::Auto => return Output::TE(transfer), - }; - Output::Encoder(enc) -} - -fn streaming_encoding( - buf: BytesMut, version: Version, req: &mut ClientRequest, -) -> TransferEncoding { - if req.chunked() { - // Enable transfer encoding - req.headers_mut().remove(CONTENT_LENGTH); - if version == Version::HTTP_2 { - req.headers_mut().remove(TRANSFER_ENCODING); - TransferEncoding::eof(buf) - } else { - req.headers_mut() - .insert(TRANSFER_ENCODING, HeaderValue::from_static("chunked")); - TransferEncoding::chunked(buf) - } - } else { - // if Content-Length is specified, then use it as length hint - let (len, chunked) = if let Some(len) = req.headers().get(CONTENT_LENGTH) { - // Content-Length - if let Ok(s) = len.to_str() { - if let Ok(len) = s.parse::() { - (Some(len), false) - } else { - error!("illegal Content-Length: {:?}", len); - (None, false) - } - } else { - error!("illegal Content-Length: {:?}", len); - (None, false) - } - } else { - (None, true) - }; - - if !chunked { - if let Some(len) = len { - TransferEncoding::length(len, buf) - } else { - TransferEncoding::eof(buf) - } - } else { - // Enable transfer encoding - match version { - Version::HTTP_11 => { - req.headers_mut() - .insert(TRANSFER_ENCODING, HeaderValue::from_static("chunked")); - TransferEncoding::chunked(buf) - } - _ => { - req.headers_mut().remove(TRANSFER_ENCODING); - TransferEncoding::eof(buf) - } - } - } - } -} - -// "Sun, 06 Nov 1994 08:49:37 GMT".len() -pub const DATE_VALUE_LENGTH: usize = 29; - -fn set_date(dst: &mut BytesMut) { - CACHED.with(|cache| { - let mut cache = cache.borrow_mut(); - let now = time::get_time(); - if now > cache.next_update { - cache.update(now); - } - dst.extend_from_slice(cache.buffer()); - }) -} - -struct CachedDate { - bytes: [u8; DATE_VALUE_LENGTH], - next_update: time::Timespec, -} - -thread_local!(static CACHED: RefCell = RefCell::new(CachedDate { - bytes: [0; DATE_VALUE_LENGTH], - next_update: time::Timespec::new(0, 0), -})); - -impl CachedDate { - fn buffer(&self) -> &[u8] { - &self.bytes[..] - } - - fn update(&mut self, now: time::Timespec) { - write!(&mut self.bytes[..], "{}", time::at_utc(now).rfc822()).unwrap(); - self.next_update = now + Duration::seconds(1); - self.next_update.nsec = 0; - } -} diff --git a/src/context.rs b/src/context.rs deleted file mode 100644 index 71a5af2d..00000000 --- a/src/context.rs +++ /dev/null @@ -1,294 +0,0 @@ -extern crate actix; - -use futures::sync::oneshot; -use futures::sync::oneshot::Sender; -use futures::{Async, Future, Poll}; -use smallvec::SmallVec; -use std::marker::PhantomData; - -use self::actix::dev::{ - AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, ToEnvelope, -}; -use self::actix::fut::ActorFuture; -use self::actix::{ - Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message, SpawnHandle, -}; - -use body::{Binary, Body}; -use error::{Error, ErrorInternalServerError}; -use httprequest::HttpRequest; - -pub trait ActorHttpContext: 'static { - fn disconnected(&mut self); - fn poll(&mut self) -> Poll>, Error>; -} - -#[derive(Debug)] -pub enum Frame { - Chunk(Option), - Drain(oneshot::Sender<()>), -} - -impl Frame { - pub fn len(&self) -> usize { - match *self { - Frame::Chunk(Some(ref bin)) => bin.len(), - _ => 0, - } - } -} - -/// Execution context for http actors -pub struct HttpContext -where - A: Actor>, -{ - inner: ContextParts, - stream: Option>, - request: HttpRequest, - disconnected: bool, -} - -impl ActorContext for HttpContext -where - A: Actor, -{ - fn stop(&mut self) { - self.inner.stop(); - } - fn terminate(&mut self) { - self.inner.terminate() - } - fn state(&self) -> ActorState { - self.inner.state() - } -} - -impl AsyncContext for HttpContext -where - A: Actor, -{ - #[inline] - fn spawn(&mut self, fut: F) -> SpawnHandle - where - F: ActorFuture + 'static, - { - self.inner.spawn(fut) - } - #[inline] - fn wait(&mut self, fut: F) - where - F: ActorFuture + 'static, - { - self.inner.wait(fut) - } - #[doc(hidden)] - #[inline] - fn waiting(&self) -> bool { - self.inner.waiting() - || self.inner.state() == ActorState::Stopping - || self.inner.state() == ActorState::Stopped - } - #[inline] - fn cancel_future(&mut self, handle: SpawnHandle) -> bool { - self.inner.cancel_future(handle) - } - #[inline] - fn address(&self) -> Addr { - self.inner.address() - } -} - -impl HttpContext -where - A: Actor, -{ - #[inline] - /// Create a new HTTP Context from a request and an actor - pub fn create(req: HttpRequest, actor: A) -> Body { - let mb = Mailbox::default(); - let ctx = HttpContext { - inner: ContextParts::new(mb.sender_producer()), - stream: None, - request: req, - disconnected: false, - }; - Body::Actor(Box::new(HttpContextFut::new(ctx, actor, mb))) - } - - /// Create a new HTTP Context - pub fn with_factory(req: HttpRequest, f: F) -> Body - where - F: FnOnce(&mut Self) -> A + 'static, - { - let mb = Mailbox::default(); - let mut ctx = HttpContext { - inner: ContextParts::new(mb.sender_producer()), - stream: None, - request: req, - disconnected: false, - }; - - let act = f(&mut ctx); - Body::Actor(Box::new(HttpContextFut::new(ctx, act, mb))) - } -} - -impl HttpContext -where - A: Actor, -{ - /// Shared application state - #[inline] - pub fn state(&self) -> &S { - self.request.state() - } - - /// Incoming request - #[inline] - pub fn request(&mut self) -> &mut HttpRequest { - &mut self.request - } - - /// Write payload - #[inline] - pub fn write>(&mut self, data: B) { - if !self.disconnected { - self.add_frame(Frame::Chunk(Some(data.into()))); - } else { - warn!("Trying to write to disconnected response"); - } - } - - /// Indicate end of streaming payload. Also this method calls `Self::close`. - #[inline] - pub fn write_eof(&mut self) { - self.add_frame(Frame::Chunk(None)); - } - - /// Returns drain future - pub fn drain(&mut self) -> Drain { - let (tx, rx) = oneshot::channel(); - self.add_frame(Frame::Drain(tx)); - Drain::new(rx) - } - - /// Check if connection still open - #[inline] - pub fn connected(&self) -> bool { - !self.disconnected - } - - #[inline] - fn add_frame(&mut self, frame: Frame) { - if self.stream.is_none() { - self.stream = Some(SmallVec::new()); - } - if let Some(s) = self.stream.as_mut() { - s.push(frame) - } - } - - /// Handle of the running future - /// - /// SpawnHandle is the handle returned by `AsyncContext::spawn()` method. - pub fn handle(&self) -> SpawnHandle { - self.inner.curr_handle() - } -} - -impl AsyncContextParts for HttpContext -where - A: Actor, -{ - fn parts(&mut self) -> &mut ContextParts { - &mut self.inner - } -} - -struct HttpContextFut -where - A: Actor>, -{ - fut: ContextFut>, -} - -impl HttpContextFut -where - A: Actor>, -{ - fn new(ctx: HttpContext, act: A, mailbox: Mailbox) -> Self { - let fut = ContextFut::new(ctx, act, mailbox); - HttpContextFut { fut } - } -} - -impl ActorHttpContext for HttpContextFut -where - A: Actor>, - S: 'static, -{ - #[inline] - fn disconnected(&mut self) { - self.fut.ctx().disconnected = true; - self.fut.ctx().stop(); - } - - fn poll(&mut self) -> Poll>, Error> { - if self.fut.alive() { - match self.fut.poll() { - Ok(Async::NotReady) | Ok(Async::Ready(())) => (), - Err(_) => return Err(ErrorInternalServerError("error")), - } - } - - // frames - if let Some(data) = self.fut.ctx().stream.take() { - Ok(Async::Ready(Some(data))) - } else if self.fut.alive() { - Ok(Async::NotReady) - } else { - Ok(Async::Ready(None)) - } - } -} - -impl ToEnvelope for HttpContext -where - A: Actor> + Handler, - M: Message + Send + 'static, - M::Result: Send, -{ - fn pack(msg: M, tx: Option>) -> Envelope { - Envelope::new(msg, tx) - } -} - -/// Consume a future -pub struct Drain { - fut: oneshot::Receiver<()>, - _a: PhantomData, -} - -impl Drain { - /// Create a drain from a future - pub fn new(fut: oneshot::Receiver<()>) -> Self { - Drain { - fut, - _a: PhantomData, - } - } -} - -impl ActorFuture for Drain { - type Item = (); - type Error = (); - type Actor = A; - - #[inline] - fn poll( - &mut self, _: &mut A, _: &mut ::Context, - ) -> Poll { - self.fut.poll().map_err(|_| ()) - } -} diff --git a/src/de.rs b/src/de.rs deleted file mode 100644 index 05f8914f..00000000 --- a/src/de.rs +++ /dev/null @@ -1,455 +0,0 @@ -use std::rc::Rc; - -use serde::de::{self, Deserializer, Error as DeError, Visitor}; - -use httprequest::HttpRequest; -use param::ParamsIter; -use uri::RESERVED_QUOTER; - -macro_rules! unsupported_type { - ($trait_fn:ident, $name:expr) => { - fn $trait_fn(self, _: V) -> Result - where V: Visitor<'de> - { - Err(de::value::Error::custom(concat!("unsupported type: ", $name))) - } - }; -} - -macro_rules! percent_decode_if_needed { - ($value:expr, $decode:expr) => { - if $decode { - if let Some(ref mut value) = RESERVED_QUOTER.requote($value.as_bytes()) { - Rc::make_mut(value).parse() - } else { - $value.parse() - } - } else { - $value.parse() - } - } -} - -macro_rules! parse_single_value { - ($trait_fn:ident, $visit_fn:ident, $tp:tt) => { - fn $trait_fn(self, visitor: V) -> Result - where V: Visitor<'de> - { - if self.req.match_info().len() != 1 { - Err(de::value::Error::custom( - format!("wrong number of parameters: {} expected 1", - self.req.match_info().len()).as_str())) - } else { - let v_parsed = percent_decode_if_needed!(&self.req.match_info()[0], self.decode) - .map_err(|_| de::value::Error::custom( - format!("can not parse {:?} to a {}", &self.req.match_info()[0], $tp) - ))?; - visitor.$visit_fn(v_parsed) - } - } - } -} - -pub struct PathDeserializer<'de, S: 'de> { - req: &'de HttpRequest, - decode: bool, -} - -impl<'de, S: 'de> PathDeserializer<'de, S> { - pub fn new(req: &'de HttpRequest, decode: bool) -> Self { - PathDeserializer { req, decode } - } -} - -impl<'de, S: 'de> Deserializer<'de> for PathDeserializer<'de, S> { - type Error = de::value::Error; - - fn deserialize_map(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_map(ParamsDeserializer { - params: self.req.match_info().iter(), - current: None, - decode: self.decode, - }) - } - - fn deserialize_struct( - self, _: &'static str, _: &'static [&'static str], visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - self.deserialize_map(visitor) - } - - fn deserialize_unit(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_unit() - } - - fn deserialize_unit_struct( - self, _: &'static str, visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - self.deserialize_unit(visitor) - } - - fn deserialize_newtype_struct( - self, _: &'static str, visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - visitor.visit_newtype_struct(self) - } - - fn deserialize_tuple( - self, len: usize, visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - if self.req.match_info().len() < len { - Err(de::value::Error::custom( - format!( - "wrong number of parameters: {} expected {}", - self.req.match_info().len(), - len - ).as_str(), - )) - } else { - visitor.visit_seq(ParamsSeq { - params: self.req.match_info().iter(), - decode: self.decode, - }) - } - } - - fn deserialize_tuple_struct( - self, _: &'static str, len: usize, visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - if self.req.match_info().len() < len { - Err(de::value::Error::custom( - format!( - "wrong number of parameters: {} expected {}", - self.req.match_info().len(), - len - ).as_str(), - )) - } else { - visitor.visit_seq(ParamsSeq { - params: self.req.match_info().iter(), - decode: self.decode, - }) - } - } - - fn deserialize_enum( - self, _: &'static str, _: &'static [&'static str], _: V, - ) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("unsupported type: enum")) - } - - fn deserialize_seq(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_seq(ParamsSeq { - params: self.req.match_info().iter(), - decode: self.decode, - }) - } - - unsupported_type!(deserialize_any, "'any'"); - unsupported_type!(deserialize_bytes, "bytes"); - unsupported_type!(deserialize_option, "Option"); - unsupported_type!(deserialize_identifier, "identifier"); - unsupported_type!(deserialize_ignored_any, "ignored_any"); - - parse_single_value!(deserialize_bool, visit_bool, "bool"); - parse_single_value!(deserialize_i8, visit_i8, "i8"); - parse_single_value!(deserialize_i16, visit_i16, "i16"); - parse_single_value!(deserialize_i32, visit_i32, "i32"); - parse_single_value!(deserialize_i64, visit_i64, "i64"); - parse_single_value!(deserialize_u8, visit_u8, "u8"); - parse_single_value!(deserialize_u16, visit_u16, "u16"); - parse_single_value!(deserialize_u32, visit_u32, "u32"); - parse_single_value!(deserialize_u64, visit_u64, "u64"); - parse_single_value!(deserialize_f32, visit_f32, "f32"); - parse_single_value!(deserialize_f64, visit_f64, "f64"); - parse_single_value!(deserialize_string, visit_string, "String"); - parse_single_value!(deserialize_str, visit_string, "String"); - parse_single_value!(deserialize_byte_buf, visit_string, "String"); - parse_single_value!(deserialize_char, visit_char, "char"); - -} - -struct ParamsDeserializer<'de> { - params: ParamsIter<'de>, - current: Option<(&'de str, &'de str)>, - decode: bool, -} - -impl<'de> de::MapAccess<'de> for ParamsDeserializer<'de> { - type Error = de::value::Error; - - fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> - where - K: de::DeserializeSeed<'de>, - { - self.current = self.params.next().map(|ref item| (item.0, item.1)); - match self.current { - Some((key, _)) => Ok(Some(seed.deserialize(Key { key })?)), - None => Ok(None), - } - } - - fn next_value_seed(&mut self, seed: V) -> Result - where - V: de::DeserializeSeed<'de>, - { - if let Some((_, value)) = self.current.take() { - seed.deserialize(Value { value, decode: self.decode }) - } else { - Err(de::value::Error::custom("unexpected item")) - } - } -} - -struct Key<'de> { - key: &'de str, -} - -impl<'de> Deserializer<'de> for Key<'de> { - type Error = de::value::Error; - - fn deserialize_identifier(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_str(self.key) - } - - fn deserialize_any(self, _visitor: V) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("Unexpected")) - } - - forward_to_deserialize_any! { - bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str string bytes - byte_buf option unit unit_struct newtype_struct seq tuple - tuple_struct map struct enum ignored_any - } -} - -macro_rules! parse_value { - ($trait_fn:ident, $visit_fn:ident, $tp:tt) => { - fn $trait_fn(self, visitor: V) -> Result - where V: Visitor<'de> - { - let v_parsed = percent_decode_if_needed!(&self.value, self.decode) - .map_err(|_| de::value::Error::custom( - format!("can not parse {:?} to a {}", &self.value, $tp) - ))?; - visitor.$visit_fn(v_parsed) - } - } -} - -struct Value<'de> { - value: &'de str, - decode: bool, -} - -impl<'de> Deserializer<'de> for Value<'de> { - type Error = de::value::Error; - - parse_value!(deserialize_bool, visit_bool, "bool"); - parse_value!(deserialize_i8, visit_i8, "i8"); - parse_value!(deserialize_i16, visit_i16, "i16"); - parse_value!(deserialize_i32, visit_i32, "i16"); - parse_value!(deserialize_i64, visit_i64, "i64"); - parse_value!(deserialize_u8, visit_u8, "u8"); - parse_value!(deserialize_u16, visit_u16, "u16"); - parse_value!(deserialize_u32, visit_u32, "u32"); - parse_value!(deserialize_u64, visit_u64, "u64"); - parse_value!(deserialize_f32, visit_f32, "f32"); - parse_value!(deserialize_f64, visit_f64, "f64"); - parse_value!(deserialize_string, visit_string, "String"); - parse_value!(deserialize_byte_buf, visit_string, "String"); - parse_value!(deserialize_char, visit_char, "char"); - - fn deserialize_ignored_any(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_unit() - } - - fn deserialize_unit(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_unit() - } - - fn deserialize_unit_struct( - self, _: &'static str, visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - visitor.visit_unit() - } - - fn deserialize_bytes(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_borrowed_bytes(self.value.as_bytes()) - } - - fn deserialize_str(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_borrowed_str(self.value) - } - - fn deserialize_option(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_some(self) - } - - fn deserialize_enum( - self, _: &'static str, _: &'static [&'static str], visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - visitor.visit_enum(ValueEnum { value: self.value }) - } - - fn deserialize_newtype_struct( - self, _: &'static str, visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - visitor.visit_newtype_struct(self) - } - - fn deserialize_tuple(self, _: usize, _: V) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("unsupported type: tuple")) - } - - fn deserialize_struct( - self, _: &'static str, _: &'static [&'static str], _: V, - ) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("unsupported type: struct")) - } - - fn deserialize_tuple_struct( - self, _: &'static str, _: usize, _: V, - ) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("unsupported type: tuple struct")) - } - - unsupported_type!(deserialize_any, "any"); - unsupported_type!(deserialize_seq, "seq"); - unsupported_type!(deserialize_map, "map"); - unsupported_type!(deserialize_identifier, "identifier"); -} - -struct ParamsSeq<'de> { - params: ParamsIter<'de>, - decode: bool, -} - -impl<'de> de::SeqAccess<'de> for ParamsSeq<'de> { - type Error = de::value::Error; - - fn next_element_seed(&mut self, seed: T) -> Result, Self::Error> - where - T: de::DeserializeSeed<'de>, - { - match self.params.next() { - Some(item) => Ok(Some(seed.deserialize(Value { value: item.1, decode: self.decode })?)), - None => Ok(None), - } - } -} - -struct ValueEnum<'de> { - value: &'de str, -} - -impl<'de> de::EnumAccess<'de> for ValueEnum<'de> { - type Error = de::value::Error; - type Variant = UnitVariant; - - fn variant_seed(self, seed: V) -> Result<(V::Value, Self::Variant), Self::Error> - where - V: de::DeserializeSeed<'de>, - { - Ok((seed.deserialize(Key { key: self.value })?, UnitVariant)) - } -} - -struct UnitVariant; - -impl<'de> de::VariantAccess<'de> for UnitVariant { - type Error = de::value::Error; - - fn unit_variant(self) -> Result<(), Self::Error> { - Ok(()) - } - - fn newtype_variant_seed(self, _seed: T) -> Result - where - T: de::DeserializeSeed<'de>, - { - Err(de::value::Error::custom("not supported")) - } - - fn tuple_variant(self, _len: usize, _visitor: V) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("not supported")) - } - - fn struct_variant( - self, _: &'static [&'static str], _: V, - ) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("not supported")) - } -} diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index f4ea981c..00000000 --- a/src/error.rs +++ /dev/null @@ -1,1426 +0,0 @@ -//! Error and Result module -use std::io::Error as IoError; -use std::str::Utf8Error; -use std::string::FromUtf8Error; -use std::sync::Mutex; -use std::{fmt, io, result}; - -use actix::{MailboxError, SendError}; -use cookie; -use failure::{self, Backtrace, Fail}; -use futures::Canceled; -use http::uri::InvalidUri; -use http::{header, Error as HttpError, StatusCode}; -use http2::Error as Http2Error; -use httparse; -use serde::de::value::Error as DeError; -use serde_json::error::Error as JsonError; -use serde_urlencoded::ser::Error as FormError; -use tokio_timer::Error as TimerError; -pub use url::ParseError as UrlParseError; - -// re-exports -pub use cookie::ParseError as CookieParseError; - -use handler::Responder; -use httprequest::HttpRequest; -use httpresponse::{HttpResponse, HttpResponseParts}; - -/// A specialized [`Result`](https://doc.rust-lang.org/std/result/enum.Result.html) -/// for actix web operations -/// -/// This typedef is generally used to avoid writing out -/// `actix_web::error::Error` directly and is otherwise a direct mapping to -/// `Result`. -pub type Result = result::Result; - -/// General purpose actix web error. -/// -/// An actix web error is used to carry errors from `failure` or `std::error` -/// through actix in a convenient way. It can be created through -/// converting errors with `into()`. -/// -/// Whenever it is created from an external object a response error is created -/// for it that can be used to create an http response from it this means that -/// if you have access to an actix `Error` you can always get a -/// `ResponseError` reference from it. -pub struct Error { - cause: Box, - backtrace: Option, -} - -impl Error { - /// Deprecated way to reference the underlying response error. - #[deprecated( - since = "0.6.0", - note = "please use `Error::as_response_error()` instead" - )] - pub fn cause(&self) -> &ResponseError { - self.cause.as_ref() - } - - /// Returns a reference to the underlying cause of this `Error` as `Fail` - pub fn as_fail(&self) -> &Fail { - self.cause.as_fail() - } - - /// Returns the reference to the underlying `ResponseError`. - pub fn as_response_error(&self) -> &ResponseError { - self.cause.as_ref() - } - - /// Returns a reference to the Backtrace carried by this error, if it - /// carries one. - /// - /// This uses the same `Backtrace` type that `failure` uses. - pub fn backtrace(&self) -> &Backtrace { - if let Some(bt) = self.cause.backtrace() { - bt - } else { - self.backtrace.as_ref().unwrap() - } - } - - /// Attempts to downcast this `Error` to a particular `Fail` type by - /// reference. - /// - /// If the underlying error is not of type `T`, this will return `None`. - pub fn downcast_ref(&self) -> Option<&T> { - // in the most trivial way the cause is directly of the requested type. - if let Some(rv) = Fail::downcast_ref(self.cause.as_fail()) { - return Some(rv); - } - - // in the more complex case the error has been constructed from a failure - // error. This happens because we implement From by - // calling compat() and then storing it here. In failure this is - // represented by a failure::Error being wrapped in a failure::Compat. - // - // So we first downcast into that compat, to then further downcast through - // the failure's Error downcasting system into the original failure. - let compat: Option<&failure::Compat> = - Fail::downcast_ref(self.cause.as_fail()); - compat.and_then(|e| e.get_ref().downcast_ref()) - } -} - -/// Helper trait to downcast a response error into a fail. -/// -/// This is currently not exposed because it's unclear if this is the best way -/// to achieve the downcasting on `Error` for which this is needed. -#[doc(hidden)] -pub trait InternalResponseErrorAsFail { - #[doc(hidden)] - fn as_fail(&self) -> &Fail; - #[doc(hidden)] - fn as_mut_fail(&mut self) -> &mut Fail; -} - -#[doc(hidden)] -impl InternalResponseErrorAsFail for T { - fn as_fail(&self) -> &Fail { - self - } - fn as_mut_fail(&mut self) -> &mut Fail { - self - } -} - -/// Error that can be converted to `HttpResponse` -pub trait ResponseError: Fail + InternalResponseErrorAsFail { - /// Create response for error - /// - /// Internal server error is generated by default. - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR) - } -} - -impl ResponseError for SendError -where T: Send + Sync + 'static { -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.cause, f) - } -} - -impl fmt::Debug for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if let Some(bt) = self.cause.backtrace() { - write!(f, "{:?}\n\n{:?}", &self.cause, bt) - } else { - write!( - f, - "{:?}\n\n{:?}", - &self.cause, - self.backtrace.as_ref().unwrap() - ) - } - } -} - -/// Convert `Error` to a `HttpResponse` instance -impl From for HttpResponse { - fn from(err: Error) -> Self { - HttpResponse::from_error(err) - } -} - -/// `Error` for any error that implements `ResponseError` -impl From for Error { - fn from(err: T) -> Error { - let backtrace = if err.backtrace().is_none() { - Some(Backtrace::new()) - } else { - None - }; - Error { - cause: Box::new(err), - backtrace, - } - } -} - -/// Compatibility for `failure::Error` -impl ResponseError for failure::Compat where - T: fmt::Display + fmt::Debug + Sync + Send + 'static -{} - -impl From for Error { - fn from(err: failure::Error) -> Error { - err.compat().into() - } -} - -/// `InternalServerError` for `JsonError` -impl ResponseError for JsonError {} - -/// `InternalServerError` for `FormError` -impl ResponseError for FormError {} - -/// `InternalServerError` for `TimerError` -impl ResponseError for TimerError {} - -/// `InternalServerError` for `UrlParseError` -impl ResponseError for UrlParseError {} - -/// Return `BAD_REQUEST` for `de::value::Error` -impl ResponseError for DeError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - -/// Return `BAD_REQUEST` for `Utf8Error` -impl ResponseError for Utf8Error { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - -/// Return `InternalServerError` for `HttpError`, -/// Response generation can return `HttpError`, so it is internal error -impl ResponseError for HttpError {} - -/// Return `InternalServerError` for `io::Error` -impl ResponseError for io::Error { - fn error_response(&self) -> HttpResponse { - match self.kind() { - io::ErrorKind::NotFound => HttpResponse::new(StatusCode::NOT_FOUND), - io::ErrorKind::PermissionDenied => HttpResponse::new(StatusCode::FORBIDDEN), - _ => HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR), - } - } -} - -/// `BadRequest` for `InvalidHeaderValue` -impl ResponseError for header::InvalidHeaderValue { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - -/// `BadRequest` for `InvalidHeaderValue` -impl ResponseError for header::InvalidHeaderValueBytes { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - -/// `InternalServerError` for `futures::Canceled` -impl ResponseError for Canceled {} - -/// `InternalServerError` for `actix::MailboxError` -impl ResponseError for MailboxError {} - -/// A set of errors that can occur during parsing HTTP streams -#[derive(Fail, Debug)] -pub enum ParseError { - /// An invalid `Method`, such as `GE.T`. - #[fail(display = "Invalid Method specified")] - Method, - /// An invalid `Uri`, such as `exam ple.domain`. - #[fail(display = "Uri error: {}", _0)] - Uri(InvalidUri), - /// An invalid `HttpVersion`, such as `HTP/1.1` - #[fail(display = "Invalid HTTP version specified")] - Version, - /// An invalid `Header`. - #[fail(display = "Invalid Header provided")] - Header, - /// A message head is too large to be reasonable. - #[fail(display = "Message head is too large")] - TooLarge, - /// A message reached EOF, but is not complete. - #[fail(display = "Message is incomplete")] - Incomplete, - /// An invalid `Status`, such as `1337 ELITE`. - #[fail(display = "Invalid Status provided")] - Status, - /// A timeout occurred waiting for an IO event. - #[allow(dead_code)] - #[fail(display = "Timeout")] - Timeout, - /// An `io::Error` that occurred while trying to read or write to a network - /// stream. - #[fail(display = "IO error: {}", _0)] - Io(#[cause] IoError), - /// Parsing a field as string failed - #[fail(display = "UTF8 error: {}", _0)] - Utf8(#[cause] Utf8Error), -} - -/// Return `BadRequest` for `ParseError` -impl ResponseError for ParseError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - -impl From for ParseError { - fn from(err: IoError) -> ParseError { - ParseError::Io(err) - } -} - -impl From for ParseError { - fn from(err: InvalidUri) -> ParseError { - ParseError::Uri(err) - } -} - -impl From for ParseError { - fn from(err: Utf8Error) -> ParseError { - ParseError::Utf8(err) - } -} - -impl From for ParseError { - fn from(err: FromUtf8Error) -> ParseError { - ParseError::Utf8(err.utf8_error()) - } -} - -impl From for ParseError { - fn from(err: httparse::Error) -> ParseError { - match err { - httparse::Error::HeaderName - | httparse::Error::HeaderValue - | httparse::Error::NewLine - | httparse::Error::Token => ParseError::Header, - httparse::Error::Status => ParseError::Status, - httparse::Error::TooManyHeaders => ParseError::TooLarge, - httparse::Error::Version => ParseError::Version, - } - } -} - -#[derive(Fail, Debug)] -/// A set of errors that can occur during payload parsing -pub enum PayloadError { - /// A payload reached EOF, but is not complete. - #[fail(display = "A payload reached EOF, but is not complete.")] - Incomplete, - /// Content encoding stream corruption - #[fail(display = "Can not decode content-encoding.")] - EncodingCorrupted, - /// A payload reached size limit. - #[fail(display = "A payload reached size limit.")] - Overflow, - /// A payload length is unknown. - #[fail(display = "A payload length is unknown.")] - UnknownLength, - /// Io error - #[fail(display = "{}", _0)] - Io(#[cause] IoError), - /// Http2 error - #[fail(display = "{}", _0)] - Http2(#[cause] Http2Error), -} - -impl From for PayloadError { - fn from(err: IoError) -> PayloadError { - PayloadError::Io(err) - } -} - -/// `PayloadError` returns two possible results: -/// -/// - `Overflow` returns `PayloadTooLarge` -/// - Other errors returns `BadRequest` -impl ResponseError for PayloadError { - fn error_response(&self) -> HttpResponse { - match *self { - PayloadError::Overflow => HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE), - _ => HttpResponse::new(StatusCode::BAD_REQUEST), - } - } -} - -/// Return `BadRequest` for `cookie::ParseError` -impl ResponseError for cookie::ParseError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - -/// A set of errors that can occur during parsing multipart streams -#[derive(Fail, Debug)] -pub enum MultipartError { - /// Content-Type header is not found - #[fail(display = "No Content-type header found")] - NoContentType, - /// Can not parse Content-Type header - #[fail(display = "Can not parse Content-Type header")] - ParseContentType, - /// Multipart boundary is not found - #[fail(display = "Multipart boundary is not found")] - Boundary, - /// Multipart stream is incomplete - #[fail(display = "Multipart stream is incomplete")] - Incomplete, - /// Error during field parsing - #[fail(display = "{}", _0)] - Parse(#[cause] ParseError), - /// Payload error - #[fail(display = "{}", _0)] - Payload(#[cause] PayloadError), -} - -impl From for MultipartError { - fn from(err: ParseError) -> MultipartError { - MultipartError::Parse(err) - } -} - -impl From for MultipartError { - fn from(err: PayloadError) -> MultipartError { - MultipartError::Payload(err) - } -} - -/// Return `BadRequest` for `MultipartError` -impl ResponseError for MultipartError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - -/// Error during handling `Expect` header -#[derive(Fail, PartialEq, Debug)] -pub enum ExpectError { - /// Expect header value can not be converted to utf8 - #[fail(display = "Expect header value can not be converted to utf8")] - Encoding, - /// Unknown expect value - #[fail(display = "Unknown expect value")] - UnknownExpect, -} - -impl ResponseError for ExpectError { - fn error_response(&self) -> HttpResponse { - HttpResponse::with_body(StatusCode::EXPECTATION_FAILED, "Unknown Expect") - } -} - -/// A set of error that can occure during parsing content type -#[derive(Fail, PartialEq, Debug)] -pub enum ContentTypeError { - /// Can not parse content type - #[fail(display = "Can not parse content type")] - ParseError, - /// Unknown content encoding - #[fail(display = "Unknown content encoding")] - UnknownEncoding, -} - -/// Return `BadRequest` for `ContentTypeError` -impl ResponseError for ContentTypeError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - -/// A set of errors that can occur during parsing urlencoded payloads -#[derive(Fail, Debug)] -pub enum UrlencodedError { - /// Can not decode chunked transfer encoding - #[fail(display = "Can not decode chunked transfer encoding")] - Chunked, - /// Payload size is bigger than allowed. (default: 256kB) - #[fail( - display = "Urlencoded payload size is bigger than allowed. (default: 256kB)" - )] - Overflow, - /// Payload size is now known - #[fail(display = "Payload size is now known")] - UnknownLength, - /// Content type error - #[fail(display = "Content type error")] - ContentType, - /// Parse error - #[fail(display = "Parse error")] - Parse, - /// Payload error - #[fail(display = "Error that occur during reading payload: {}", _0)] - Payload(#[cause] PayloadError), -} - -/// Return `BadRequest` for `UrlencodedError` -impl ResponseError for UrlencodedError { - fn error_response(&self) -> HttpResponse { - match *self { - UrlencodedError::Overflow => { - HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE) - } - UrlencodedError::UnknownLength => { - HttpResponse::new(StatusCode::LENGTH_REQUIRED) - } - _ => HttpResponse::new(StatusCode::BAD_REQUEST), - } - } -} - -impl From for UrlencodedError { - fn from(err: PayloadError) -> UrlencodedError { - UrlencodedError::Payload(err) - } -} - -/// A set of errors that can occur during parsing json payloads -#[derive(Fail, Debug)] -pub enum JsonPayloadError { - /// Payload size is bigger than allowed. (default: 256kB) - #[fail(display = "Json payload size is bigger than allowed. (default: 256kB)")] - Overflow, - /// Content type error - #[fail(display = "Content type error")] - ContentType, - /// Deserialize error - #[fail(display = "Json deserialize error: {}", _0)] - Deserialize(#[cause] JsonError), - /// Payload error - #[fail(display = "Error that occur during reading payload: {}", _0)] - Payload(#[cause] PayloadError), -} - -/// Return `BadRequest` for `UrlencodedError` -impl ResponseError for JsonPayloadError { - fn error_response(&self) -> HttpResponse { - match *self { - JsonPayloadError::Overflow => { - HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE) - } - _ => HttpResponse::new(StatusCode::BAD_REQUEST), - } - } -} - -impl From for JsonPayloadError { - fn from(err: PayloadError) -> JsonPayloadError { - JsonPayloadError::Payload(err) - } -} - -impl From for JsonPayloadError { - fn from(err: JsonError) -> JsonPayloadError { - JsonPayloadError::Deserialize(err) - } -} - -/// Error type returned when reading body as lines. -pub enum ReadlinesError { - /// Error when decoding a line. - EncodingError, - /// Payload error. - PayloadError(PayloadError), - /// Line limit exceeded. - LimitOverflow, - /// ContentType error. - ContentTypeError(ContentTypeError), -} - -impl From for ReadlinesError { - fn from(err: PayloadError) -> Self { - ReadlinesError::PayloadError(err) - } -} - -impl From for ReadlinesError { - fn from(err: ContentTypeError) -> Self { - ReadlinesError::ContentTypeError(err) - } -} - -/// Errors which can occur when attempting to interpret a segment string as a -/// valid path segment. -#[derive(Fail, Debug, PartialEq)] -pub enum UriSegmentError { - /// The segment started with the wrapped invalid character. - #[fail(display = "The segment started with the wrapped invalid character")] - BadStart(char), - /// The segment contained the wrapped invalid character. - #[fail(display = "The segment contained the wrapped invalid character")] - BadChar(char), - /// The segment ended with the wrapped invalid character. - #[fail(display = "The segment ended with the wrapped invalid character")] - BadEnd(char), -} - -/// Return `BadRequest` for `UriSegmentError` -impl ResponseError for UriSegmentError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::BAD_REQUEST) - } -} - -/// Errors which can occur when attempting to generate resource uri. -#[derive(Fail, Debug, PartialEq)] -pub enum UrlGenerationError { - /// Resource not found - #[fail(display = "Resource not found")] - ResourceNotFound, - /// Not all path pattern covered - #[fail(display = "Not all path pattern covered")] - NotEnoughElements, - /// URL parse error - #[fail(display = "{}", _0)] - ParseError(#[cause] UrlParseError), -} - -/// `InternalServerError` for `UrlGeneratorError` -impl ResponseError for UrlGenerationError {} - -impl From for UrlGenerationError { - fn from(err: UrlParseError) -> Self { - UrlGenerationError::ParseError(err) - } -} - -/// Errors which can occur when serving static files. -#[derive(Fail, Debug, PartialEq)] -pub enum StaticFileError { - /// Path is not a directory - #[fail(display = "Path is not a directory. Unable to serve static files")] - IsNotDirectory, - /// Cannot render directory - #[fail(display = "Unable to render directory without index file")] - IsDirectory, -} - -/// Return `NotFound` for `StaticFileError` -impl ResponseError for StaticFileError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::NOT_FOUND) - } -} - -/// Helper type that can wrap any error and generate custom response. -/// -/// In following example any `io::Error` will be converted into "BAD REQUEST" -/// response as opposite to *INTERNAL SERVER ERROR* which is defined by -/// default. -/// -/// ```rust -/// # extern crate actix_web; -/// # use actix_web::*; -/// use actix_web::fs::NamedFile; -/// -/// fn index(req: HttpRequest) -> Result { -/// let f = NamedFile::open("test.txt").map_err(error::ErrorBadRequest)?; -/// Ok(f) -/// } -/// # fn main() {} -/// ``` -pub struct InternalError { - cause: T, - status: InternalErrorType, - backtrace: Backtrace, -} - -enum InternalErrorType { - Status(StatusCode), - Response(Box>>), -} - -impl InternalError { - /// Create `InternalError` instance - pub fn new(cause: T, status: StatusCode) -> Self { - InternalError { - cause, - status: InternalErrorType::Status(status), - backtrace: Backtrace::new(), - } - } - - /// Create `InternalError` with predefined `HttpResponse`. - pub fn from_response(cause: T, response: HttpResponse) -> Self { - let resp = response.into_parts(); - InternalError { - cause, - status: InternalErrorType::Response(Box::new(Mutex::new(Some(resp)))), - backtrace: Backtrace::new(), - } - } -} - -impl Fail for InternalError -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - fn backtrace(&self) -> Option<&Backtrace> { - Some(&self.backtrace) - } -} - -impl fmt::Debug for InternalError -where - T: Send + Sync + fmt::Debug + 'static, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Debug::fmt(&self.cause, f) - } -} - -impl fmt::Display for InternalError -where - T: Send + Sync + fmt::Display + 'static, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.cause, f) - } -} - -impl ResponseError for InternalError -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - fn error_response(&self) -> HttpResponse { - match self.status { - InternalErrorType::Status(st) => HttpResponse::new(st), - InternalErrorType::Response(ref resp) => { - if let Some(resp) = resp.lock().unwrap().take() { - HttpResponse::from_parts(resp) - } else { - HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR) - } - } - } - } -} - -impl Responder for InternalError -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, _: &HttpRequest) -> Result { - Err(self.into()) - } -} - -/// Helper function that creates wrapper of any error and generate *BAD -/// REQUEST* response. -#[allow(non_snake_case)] -pub fn ErrorBadRequest(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::BAD_REQUEST).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *UNAUTHORIZED* response. -#[allow(non_snake_case)] -pub fn ErrorUnauthorized(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::UNAUTHORIZED).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *PAYMENT_REQUIRED* response. -#[allow(non_snake_case)] -pub fn ErrorPaymentRequired(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::PAYMENT_REQUIRED).into() -} - -/// Helper function that creates wrapper of any error and generate *FORBIDDEN* -/// response. -#[allow(non_snake_case)] -pub fn ErrorForbidden(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::FORBIDDEN).into() -} - -/// Helper function that creates wrapper of any error and generate *NOT FOUND* -/// response. -#[allow(non_snake_case)] -pub fn ErrorNotFound(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::NOT_FOUND).into() -} - -/// Helper function that creates wrapper of any error and generate *METHOD NOT -/// ALLOWED* response. -#[allow(non_snake_case)] -pub fn ErrorMethodNotAllowed(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::METHOD_NOT_ALLOWED).into() -} - -/// Helper function that creates wrapper of any error and generate *NOT -/// ACCEPTABLE* response. -#[allow(non_snake_case)] -pub fn ErrorNotAcceptable(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::NOT_ACCEPTABLE).into() -} - -/// Helper function that creates wrapper of any error and generate *PROXY -/// AUTHENTICATION REQUIRED* response. -#[allow(non_snake_case)] -pub fn ErrorProxyAuthenticationRequired(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::PROXY_AUTHENTICATION_REQUIRED).into() -} - -/// Helper function that creates wrapper of any error and generate *REQUEST -/// TIMEOUT* response. -#[allow(non_snake_case)] -pub fn ErrorRequestTimeout(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::REQUEST_TIMEOUT).into() -} - -/// Helper function that creates wrapper of any error and generate *CONFLICT* -/// response. -#[allow(non_snake_case)] -pub fn ErrorConflict(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::CONFLICT).into() -} - -/// Helper function that creates wrapper of any error and generate *GONE* -/// response. -#[allow(non_snake_case)] -pub fn ErrorGone(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::GONE).into() -} - -/// Helper function that creates wrapper of any error and generate *LENGTH -/// REQUIRED* response. -#[allow(non_snake_case)] -pub fn ErrorLengthRequired(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::LENGTH_REQUIRED).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *PRECONDITION FAILED* response. -#[allow(non_snake_case)] -pub fn ErrorPreconditionFailed(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::PRECONDITION_FAILED).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *PAYLOAD TOO LARGE* response. -#[allow(non_snake_case)] -pub fn ErrorPayloadTooLarge(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::PAYLOAD_TOO_LARGE).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *URI TOO LONG* response. -#[allow(non_snake_case)] -pub fn ErrorUriTooLong(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::URI_TOO_LONG).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *UNSUPPORTED MEDIA TYPE* response. -#[allow(non_snake_case)] -pub fn ErrorUnsupportedMediaType(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::UNSUPPORTED_MEDIA_TYPE).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *RANGE NOT SATISFIABLE* response. -#[allow(non_snake_case)] -pub fn ErrorRangeNotSatisfiable(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::RANGE_NOT_SATISFIABLE).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *EXPECTATION FAILED* response. -#[allow(non_snake_case)] -pub fn ErrorExpectationFailed(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::EXPECTATION_FAILED).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *IM A TEAPOT* response. -#[allow(non_snake_case)] -pub fn ErrorImATeapot(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::IM_A_TEAPOT).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *MISDIRECTED REQUEST* response. -#[allow(non_snake_case)] -pub fn ErrorMisdirectedRequest(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::MISDIRECTED_REQUEST).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *UNPROCESSABLE ENTITY* response. -#[allow(non_snake_case)] -pub fn ErrorUnprocessableEntity(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::UNPROCESSABLE_ENTITY).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *LOCKED* response. -#[allow(non_snake_case)] -pub fn ErrorLocked(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::LOCKED).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *FAILED DEPENDENCY* response. -#[allow(non_snake_case)] -pub fn ErrorFailedDependency(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::FAILED_DEPENDENCY).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *UPGRADE REQUIRED* response. -#[allow(non_snake_case)] -pub fn ErrorUpgradeRequired(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::UPGRADE_REQUIRED).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *PRECONDITION REQUIRED* response. -#[allow(non_snake_case)] -pub fn ErrorPreconditionRequired(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::PRECONDITION_REQUIRED).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *TOO MANY REQUESTS* response. -#[allow(non_snake_case)] -pub fn ErrorTooManyRequests(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::TOO_MANY_REQUESTS).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *REQUEST HEADER FIELDS TOO LARGE* response. -#[allow(non_snake_case)] -pub fn ErrorRequestHeaderFieldsTooLarge(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *UNAVAILABLE FOR LEGAL REASONS* response. -#[allow(non_snake_case)] -pub fn ErrorUnavailableForLegalReasons(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *INTERNAL SERVER ERROR* response. -#[allow(non_snake_case)] -pub fn ErrorInternalServerError(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::INTERNAL_SERVER_ERROR).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *NOT IMPLEMENTED* response. -#[allow(non_snake_case)] -pub fn ErrorNotImplemented(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::NOT_IMPLEMENTED).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *BAD GATEWAY* response. -#[allow(non_snake_case)] -pub fn ErrorBadGateway(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::BAD_GATEWAY).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *SERVICE UNAVAILABLE* response. -#[allow(non_snake_case)] -pub fn ErrorServiceUnavailable(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::SERVICE_UNAVAILABLE).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *GATEWAY TIMEOUT* response. -#[allow(non_snake_case)] -pub fn ErrorGatewayTimeout(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::GATEWAY_TIMEOUT).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *HTTP VERSION NOT SUPPORTED* response. -#[allow(non_snake_case)] -pub fn ErrorHttpVersionNotSupported(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::HTTP_VERSION_NOT_SUPPORTED).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *VARIANT ALSO NEGOTIATES* response. -#[allow(non_snake_case)] -pub fn ErrorVariantAlsoNegotiates(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::VARIANT_ALSO_NEGOTIATES).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *INSUFFICIENT STORAGE* response. -#[allow(non_snake_case)] -pub fn ErrorInsufficientStorage(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::INSUFFICIENT_STORAGE).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *LOOP DETECTED* response. -#[allow(non_snake_case)] -pub fn ErrorLoopDetected(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::LOOP_DETECTED).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *NOT EXTENDED* response. -#[allow(non_snake_case)] -pub fn ErrorNotExtended(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::NOT_EXTENDED).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *NETWORK AUTHENTICATION REQUIRED* response. -#[allow(non_snake_case)] -pub fn ErrorNetworkAuthenticationRequired(err: T) -> Error -where - T: Send + Sync + fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::NETWORK_AUTHENTICATION_REQUIRED).into() -} - -#[cfg(test)] -mod tests { - use super::*; - use cookie::ParseError as CookieParseError; - use failure; - use http::{Error as HttpError, StatusCode}; - use httparse; - use std::env; - use std::error::Error as StdError; - use std::io; - - #[test] - #[cfg(actix_nightly)] - fn test_nightly() { - let resp: HttpResponse = - IoError::new(io::ErrorKind::Other, "test").error_response(); - assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); - } - - #[test] - fn test_into_response() { - let resp: HttpResponse = ParseError::Incomplete.error_response(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - - let resp: HttpResponse = CookieParseError::EmptyName.error_response(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - - let resp: HttpResponse = MultipartError::Boundary.error_response(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - - let err: HttpError = StatusCode::from_u16(10000).err().unwrap().into(); - let resp: HttpResponse = err.error_response(); - assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); - } - - #[test] - fn test_as_fail() { - let orig = io::Error::new(io::ErrorKind::Other, "other"); - let desc = orig.description().to_owned(); - let e = ParseError::Io(orig); - assert_eq!(format!("{}", e.cause().unwrap()), desc); - } - - #[test] - fn test_backtrace() { - let e = ErrorBadRequest("err"); - let _ = e.backtrace(); - } - - #[test] - fn test_error_cause() { - let orig = io::Error::new(io::ErrorKind::Other, "other"); - let desc = orig.description().to_owned(); - let e = Error::from(orig); - assert_eq!(format!("{}", e.as_fail()), desc); - } - - #[test] - fn test_error_display() { - let orig = io::Error::new(io::ErrorKind::Other, "other"); - let desc = orig.description().to_owned(); - let e = Error::from(orig); - assert_eq!(format!("{}", e), desc); - } - - #[test] - fn test_error_http_response() { - let orig = io::Error::new(io::ErrorKind::Other, "other"); - let e = Error::from(orig); - let resp: HttpResponse = e.into(); - assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); - } - - #[test] - fn test_expect_error() { - let resp: HttpResponse = ExpectError::Encoding.error_response(); - assert_eq!(resp.status(), StatusCode::EXPECTATION_FAILED); - let resp: HttpResponse = ExpectError::UnknownExpect.error_response(); - assert_eq!(resp.status(), StatusCode::EXPECTATION_FAILED); - } - - macro_rules! from { - ($from:expr => $error:pat) => { - match ParseError::from($from) { - e @ $error => { - assert!(format!("{}", e).len() >= 5); - } - e => unreachable!("{:?}", e), - } - }; - } - - macro_rules! from_and_cause { - ($from:expr => $error:pat) => { - match ParseError::from($from) { - e @ $error => { - let desc = format!("{}", e.cause().unwrap()); - assert_eq!(desc, $from.description().to_owned()); - } - _ => unreachable!("{:?}", $from), - } - }; - } - - #[test] - fn test_from() { - from_and_cause!(io::Error::new(io::ErrorKind::Other, "other") => ParseError::Io(..)); - - from!(httparse::Error::HeaderName => ParseError::Header); - from!(httparse::Error::HeaderName => ParseError::Header); - from!(httparse::Error::HeaderValue => ParseError::Header); - from!(httparse::Error::NewLine => ParseError::Header); - from!(httparse::Error::Status => ParseError::Status); - from!(httparse::Error::Token => ParseError::Header); - from!(httparse::Error::TooManyHeaders => ParseError::TooLarge); - from!(httparse::Error::Version => ParseError::Version); - } - - #[test] - fn failure_error() { - const NAME: &str = "RUST_BACKTRACE"; - let old_tb = env::var(NAME); - env::set_var(NAME, "0"); - let error = failure::err_msg("Hello!"); - let resp: Error = error.into(); - assert_eq!( - format!("{:?}", resp), - "Compat { error: ErrorMessage { msg: \"Hello!\" } }\n\n" - ); - match old_tb { - Ok(x) => env::set_var(NAME, x), - _ => env::remove_var(NAME), - } - } - - #[test] - fn test_internal_error() { - let err = InternalError::from_response( - ExpectError::Encoding, - HttpResponse::Ok().into(), - ); - let resp: HttpResponse = err.error_response(); - assert_eq!(resp.status(), StatusCode::OK); - } - - #[test] - fn test_error_downcasting_direct() { - #[derive(Debug, Fail)] - #[fail(display = "demo error")] - struct DemoError; - - impl ResponseError for DemoError {} - - let err: Error = DemoError.into(); - let err_ref: &DemoError = err.downcast_ref().unwrap(); - assert_eq!(err_ref.to_string(), "demo error"); - } - - #[test] - fn test_error_downcasting_compat() { - #[derive(Debug, Fail)] - #[fail(display = "demo error")] - struct DemoError; - - impl ResponseError for DemoError {} - - let err: Error = failure::Error::from(DemoError).into(); - let err_ref: &DemoError = err.downcast_ref().unwrap(); - assert_eq!(err_ref.to_string(), "demo error"); - } - - #[test] - fn test_error_helpers() { - let r: HttpResponse = ErrorBadRequest("err").into(); - assert_eq!(r.status(), StatusCode::BAD_REQUEST); - - let r: HttpResponse = ErrorUnauthorized("err").into(); - assert_eq!(r.status(), StatusCode::UNAUTHORIZED); - - let r: HttpResponse = ErrorPaymentRequired("err").into(); - assert_eq!(r.status(), StatusCode::PAYMENT_REQUIRED); - - let r: HttpResponse = ErrorForbidden("err").into(); - assert_eq!(r.status(), StatusCode::FORBIDDEN); - - let r: HttpResponse = ErrorNotFound("err").into(); - assert_eq!(r.status(), StatusCode::NOT_FOUND); - - let r: HttpResponse = ErrorMethodNotAllowed("err").into(); - assert_eq!(r.status(), StatusCode::METHOD_NOT_ALLOWED); - - let r: HttpResponse = ErrorNotAcceptable("err").into(); - assert_eq!(r.status(), StatusCode::NOT_ACCEPTABLE); - - let r: HttpResponse = ErrorProxyAuthenticationRequired("err").into(); - assert_eq!(r.status(), StatusCode::PROXY_AUTHENTICATION_REQUIRED); - - let r: HttpResponse = ErrorRequestTimeout("err").into(); - assert_eq!(r.status(), StatusCode::REQUEST_TIMEOUT); - - let r: HttpResponse = ErrorConflict("err").into(); - assert_eq!(r.status(), StatusCode::CONFLICT); - - let r: HttpResponse = ErrorGone("err").into(); - assert_eq!(r.status(), StatusCode::GONE); - - let r: HttpResponse = ErrorLengthRequired("err").into(); - assert_eq!(r.status(), StatusCode::LENGTH_REQUIRED); - - let r: HttpResponse = ErrorPreconditionFailed("err").into(); - assert_eq!(r.status(), StatusCode::PRECONDITION_FAILED); - - let r: HttpResponse = ErrorPayloadTooLarge("err").into(); - assert_eq!(r.status(), StatusCode::PAYLOAD_TOO_LARGE); - - let r: HttpResponse = ErrorUriTooLong("err").into(); - assert_eq!(r.status(), StatusCode::URI_TOO_LONG); - - let r: HttpResponse = ErrorUnsupportedMediaType("err").into(); - assert_eq!(r.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE); - - let r: HttpResponse = ErrorRangeNotSatisfiable("err").into(); - assert_eq!(r.status(), StatusCode::RANGE_NOT_SATISFIABLE); - - let r: HttpResponse = ErrorExpectationFailed("err").into(); - assert_eq!(r.status(), StatusCode::EXPECTATION_FAILED); - - let r: HttpResponse = ErrorImATeapot("err").into(); - assert_eq!(r.status(), StatusCode::IM_A_TEAPOT); - - let r: HttpResponse = ErrorMisdirectedRequest("err").into(); - assert_eq!(r.status(), StatusCode::MISDIRECTED_REQUEST); - - let r: HttpResponse = ErrorUnprocessableEntity("err").into(); - assert_eq!(r.status(), StatusCode::UNPROCESSABLE_ENTITY); - - let r: HttpResponse = ErrorLocked("err").into(); - assert_eq!(r.status(), StatusCode::LOCKED); - - let r: HttpResponse = ErrorFailedDependency("err").into(); - assert_eq!(r.status(), StatusCode::FAILED_DEPENDENCY); - - let r: HttpResponse = ErrorUpgradeRequired("err").into(); - assert_eq!(r.status(), StatusCode::UPGRADE_REQUIRED); - - let r: HttpResponse = ErrorPreconditionRequired("err").into(); - assert_eq!(r.status(), StatusCode::PRECONDITION_REQUIRED); - - let r: HttpResponse = ErrorTooManyRequests("err").into(); - assert_eq!(r.status(), StatusCode::TOO_MANY_REQUESTS); - - let r: HttpResponse = ErrorRequestHeaderFieldsTooLarge("err").into(); - assert_eq!(r.status(), StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE); - - let r: HttpResponse = ErrorUnavailableForLegalReasons("err").into(); - assert_eq!(r.status(), StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS); - - let r: HttpResponse = ErrorInternalServerError("err").into(); - assert_eq!(r.status(), StatusCode::INTERNAL_SERVER_ERROR); - - let r: HttpResponse = ErrorNotImplemented("err").into(); - assert_eq!(r.status(), StatusCode::NOT_IMPLEMENTED); - - let r: HttpResponse = ErrorBadGateway("err").into(); - assert_eq!(r.status(), StatusCode::BAD_GATEWAY); - - let r: HttpResponse = ErrorServiceUnavailable("err").into(); - assert_eq!(r.status(), StatusCode::SERVICE_UNAVAILABLE); - - let r: HttpResponse = ErrorGatewayTimeout("err").into(); - assert_eq!(r.status(), StatusCode::GATEWAY_TIMEOUT); - - let r: HttpResponse = ErrorHttpVersionNotSupported("err").into(); - assert_eq!(r.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED); - - let r: HttpResponse = ErrorVariantAlsoNegotiates("err").into(); - assert_eq!(r.status(), StatusCode::VARIANT_ALSO_NEGOTIATES); - - let r: HttpResponse = ErrorInsufficientStorage("err").into(); - assert_eq!(r.status(), StatusCode::INSUFFICIENT_STORAGE); - - let r: HttpResponse = ErrorLoopDetected("err").into(); - assert_eq!(r.status(), StatusCode::LOOP_DETECTED); - - let r: HttpResponse = ErrorNotExtended("err").into(); - assert_eq!(r.status(), StatusCode::NOT_EXTENDED); - - let r: HttpResponse = ErrorNetworkAuthenticationRequired("err").into(); - assert_eq!(r.status(), StatusCode::NETWORK_AUTHENTICATION_REQUIRED); - } -} diff --git a/src/extensions.rs b/src/extensions.rs deleted file mode 100644 index 430b87bd..00000000 --- a/src/extensions.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::any::{Any, TypeId}; -use std::collections::HashMap; -use std::fmt; -use std::hash::{BuildHasherDefault, Hasher}; - -struct IdHasher { - id: u64, -} - -impl Default for IdHasher { - fn default() -> IdHasher { - IdHasher { id: 0 } - } -} - -impl Hasher for IdHasher { - fn write(&mut self, bytes: &[u8]) { - for &x in bytes { - self.id.wrapping_add(u64::from(x)); - } - } - - fn write_u64(&mut self, u: u64) { - self.id = u; - } - - fn finish(&self) -> u64 { - self.id - } -} - -type AnyMap = HashMap, BuildHasherDefault>; - -#[derive(Default)] -/// A type map of request extensions. -pub struct Extensions { - map: AnyMap, -} - -impl Extensions { - /// Create an empty `Extensions`. - #[inline] - pub fn new() -> Extensions { - Extensions { - map: HashMap::default(), - } - } - - /// Insert a type into this `Extensions`. - /// - /// If a extension of this type already existed, it will - /// be returned. - pub fn insert(&mut self, val: T) { - self.map.insert(TypeId::of::(), Box::new(val)); - } - - /// Get a reference to a type previously inserted on this `Extensions`. - pub fn get(&self) -> Option<&T> { - self.map - .get(&TypeId::of::()) - .and_then(|boxed| (&**boxed as &(Any + 'static)).downcast_ref()) - } - - /// Get a mutable reference to a type previously inserted on this `Extensions`. - pub fn get_mut(&mut self) -> Option<&mut T> { - self.map - .get_mut(&TypeId::of::()) - .and_then(|boxed| (&mut **boxed as &mut (Any + 'static)).downcast_mut()) - } - - /// Remove a type from this `Extensions`. - /// - /// If a extension of this type existed, it will be returned. - pub fn remove(&mut self) -> Option { - self.map.remove(&TypeId::of::()).and_then(|boxed| { - (boxed as Box) - .downcast() - .ok() - .map(|boxed| *boxed) - }) - } - - /// Clear the `Extensions` of all inserted extensions. - #[inline] - pub fn clear(&mut self) { - self.map.clear(); - } -} - -impl fmt::Debug for Extensions { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Extensions").finish() - } -} - -#[test] -fn test_extensions() { - #[derive(Debug, PartialEq)] - struct MyType(i32); - - let mut extensions = Extensions::new(); - - extensions.insert(5i32); - extensions.insert(MyType(10)); - - assert_eq!(extensions.get(), Some(&5i32)); - assert_eq!(extensions.get_mut(), Some(&mut 5i32)); - - assert_eq!(extensions.remove::(), Some(5i32)); - assert!(extensions.get::().is_none()); - - assert_eq!(extensions.get::(), None); - assert_eq!(extensions.get(), Some(&MyType(10))); -} diff --git a/src/extractor.rs b/src/extractor.rs index 33705723..53209ad0 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -1,4 +1,3 @@ -use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; use std::rc::Rc; use std::{fmt, str}; @@ -6,25 +5,34 @@ use std::{fmt, str}; use bytes::Bytes; use encoding::all::UTF_8; use encoding::types::{DecoderTrap, Encoding}; -use futures::{future, Async, Future, Poll}; +use futures::future::{err, ok, Either, FutureResult}; +use futures::{future, Async, Future, IntoFuture, Poll, Stream}; use mime::Mime; use serde::de::{self, DeserializeOwned}; +use serde::Serialize; +use serde_json; use serde_urlencoded; -use de::PathDeserializer; -use error::{Error, ErrorBadRequest, ErrorNotFound, UrlencodedError, ErrorConflict}; -use handler::{AsyncResult, FromRequest}; -use httpmessage::{HttpMessage, MessageBody, UrlEncoded}; -use httprequest::HttpRequest; -use Either; +use actix_http::dev::{JsonBody, MessageBody, UrlEncoded}; +use actix_http::error::{ + Error, ErrorBadRequest, ErrorNotFound, JsonPayloadError, PayloadError, + UrlencodedError, +}; +use actix_http::http::StatusCode; +use actix_http::{HttpMessage, Response}; +use actix_router::PathDeserializer; + +use crate::handler::FromRequest; +use crate::request::HttpRequest; +use crate::responder::Responder; +use crate::service::ServiceRequest; #[derive(PartialEq, Eq, PartialOrd, Ord)] -/// Extract typed information from the request's path. Information from the path is -/// URL decoded. Decoding of special characters can be disabled through `PathConfig`. +/// Extract typed information from the request's path. /// /// ## Example /// -/// ```rust +/// ```rust,ignore /// # extern crate bytes; /// # extern crate actix_web; /// # extern crate futures; @@ -48,7 +56,7 @@ use Either; /// It is possible to extract path information to a specific type that /// implements `Deserialize` trait from *serde*. /// -/// ```rust +/// ```rust,ignore /// # extern crate bytes; /// # extern crate actix_web; /// # extern crate futures; @@ -101,6 +109,15 @@ impl Path { pub fn into_inner(self) -> T { self.inner } + + /// Extract path information from a request + pub fn extract

(req: &ServiceRequest

) -> Result, de::value::Error> + where + T: DeserializeOwned, + { + de::Deserialize::deserialize(PathDeserializer::new(req.match_info())) + .map(|inner| Path { inner }) + } } impl From for Path { @@ -109,74 +126,16 @@ impl From for Path { } } -impl FromRequest for Path +impl FromRequest

for Path where T: DeserializeOwned, { - type Config = PathConfig; - type Result = Result; + type Error = Error; + type Future = FutureResult; #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - let req = req.clone(); - let req2 = req.clone(); - let err = Rc::clone(&cfg.ehandler); - de::Deserialize::deserialize(PathDeserializer::new(&req, cfg.decode)) - .map_err(move |e| (*err)(e, &req2)) - .map(|inner| Path { inner }) - } -} - -/// Path extractor configuration -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::{error, http, App, HttpResponse, Path, Result}; -/// -/// /// deserialize `Info` from request's body, max payload size is 4kb -/// fn index(info: Path<(u32, String)>) -> Result { -/// Ok(format!("Welcome {}!", info.1)) -/// } -/// -/// fn main() { -/// let app = App::new().resource("/index.html/{id}/{name}", |r| { -/// r.method(http::Method::GET).with_config(index, |cfg| { -/// cfg.0.error_handler(|err, req| { -/// // <- create custom error response -/// error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into() -/// }); -/// }) -/// }); -/// } -/// ``` -pub struct PathConfig { - ehandler: Rc) -> Error>, - decode: bool, -} -impl PathConfig { - /// Set custom error handler - pub fn error_handler(&mut self, f: F) -> &mut Self - where - F: Fn(serde_urlencoded::de::Error, &HttpRequest) -> Error + 'static, - { - self.ehandler = Rc::new(f); - self - } - - /// Disable decoding of URL encoded special charaters from the path - pub fn disable_decoding(&mut self) -> &mut Self - { - self.decode = false; - self - } -} - -impl Default for PathConfig { - fn default() -> Self { - PathConfig { - ehandler: Rc::new(|e, _| ErrorNotFound(e)), - decode: true, - } + fn from_request(req: &mut ServiceRequest

) -> Self::Future { + Self::extract(req).map_err(ErrorNotFound).into_future() } } @@ -193,11 +152,11 @@ impl fmt::Display for Path { } #[derive(PartialEq, Eq, PartialOrd, Ord)] -/// Extract typed information from the request's query. +/// Extract typed information from from the request's query. /// /// ## Example /// -/// ```rust +/// ```rust,ignore /// # extern crate bytes; /// # extern crate actix_web; /// # extern crate futures; @@ -253,70 +212,18 @@ impl Query { } } -impl FromRequest for Query +impl FromRequest

for Query where T: de::DeserializeOwned, { - type Config = QueryConfig; - type Result = Result; + type Error = Error; + type Future = FutureResult; #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - let req2 = req.clone(); - let err = Rc::clone(&cfg.ehandler); + fn from_request(req: &mut ServiceRequest

) -> Self::Future { serde_urlencoded::from_str::(req.query_string()) - .map_err(move |e| (*err)(e, &req2)) - .map(Query) - } -} - -/// Query extractor configuration -/// -/// ```rust -/// # extern crate actix_web; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{error, http, App, HttpResponse, Query, Result}; -/// -/// #[derive(Deserialize)] -/// struct Info { -/// username: String, -/// } -/// -/// /// deserialize `Info` from request's body, max payload size is 4kb -/// fn index(info: Query) -> Result { -/// Ok(format!("Welcome {}!", info.username)) -/// } -/// -/// fn main() { -/// let app = App::new().resource("/index.html", |r| { -/// r.method(http::Method::GET).with_config(index, |cfg| { -/// cfg.0.error_handler(|err, req| { -/// // <- create custom error response -/// error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into() -/// }); -/// }) -/// }); -/// } -/// ``` -pub struct QueryConfig { - ehandler: Rc) -> Error>, -} -impl QueryConfig { - /// Set custom error handler - pub fn error_handler(&mut self, f: F) -> &mut Self - where - F: Fn(serde_urlencoded::de::Error, &HttpRequest) -> Error + 'static, - { - self.ehandler = Rc::new(f); - self - } -} - -impl Default for QueryConfig { - fn default() -> Self { - QueryConfig { - ehandler: Rc::new(|e, _| e.into()), - } + .map(|val| ok(Query(val))) + .unwrap_or_else(|e| err(e.into())) } } @@ -343,7 +250,7 @@ impl fmt::Display for Query { /// /// ## Example /// -/// ```rust +/// ```rust,ignore /// # extern crate actix_web; /// #[macro_use] extern crate serde_derive; /// use actix_web::{App, Form, Result}; @@ -384,16 +291,18 @@ impl DerefMut for Form { } } -impl FromRequest for Form +impl FromRequest

for Form where T: DeserializeOwned + 'static, - S: 'static, + P: Stream + 'static, { - type Config = FormConfig; - type Result = Box>; + type Error = Error; + type Future = Box>; #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { + fn from_request(req: &mut ServiceRequest

) -> Self::Future { + let cfg = FormConfig::default(); + let req2 = req.clone(); let err = Rc::clone(&cfg.ehandler); Box::new( @@ -419,7 +328,7 @@ impl fmt::Display for Form { /// Form extractor configuration /// -/// ```rust +/// ```rust,ignore /// # extern crate actix_web; /// #[macro_use] extern crate serde_derive; /// use actix_web::{http, App, Form, Result}; @@ -446,12 +355,12 @@ impl fmt::Display for Form { /// ); /// } /// ``` -pub struct FormConfig { +pub struct FormConfig { limit: usize, - ehandler: Rc) -> Error>, + ehandler: Rc Error>, } -impl FormConfig { +impl FormConfig { /// Change max size of payload. By default max size is 256Kb pub fn limit(&mut self, limit: usize) -> &mut Self { self.limit = limit; @@ -461,14 +370,14 @@ impl FormConfig { /// Set custom error handler pub fn error_handler(&mut self, f: F) -> &mut Self where - F: Fn(UrlencodedError, &HttpRequest) -> Error + 'static, + F: Fn(UrlencodedError, &HttpRequest) -> Error + 'static, { self.ehandler = Rc::new(f); self } } -impl Default for FormConfig { +impl Default for FormConfig { fn default() -> Self { FormConfig { limit: 262_144, @@ -477,6 +386,205 @@ impl Default for FormConfig { } } +/// Json helper +/// +/// Json can be used for two different purpose. First is for json response +/// generation and second is for extracting typed information from request's +/// payload. +/// +/// To extract typed information from request's body, the type `T` must +/// implement the `Deserialize` trait from *serde*. +/// +/// [**JsonConfig**](dev/struct.JsonConfig.html) allows to configure extraction +/// process. +/// +/// ## Example +/// +/// ```rust,ignore +/// # extern crate actix_web; +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{App, Json, Result, http}; +/// +/// #[derive(Deserialize)] +/// struct Info { +/// username: String, +/// } +/// +/// /// deserialize `Info` from request's body +/// fn index(info: Json) -> Result { +/// Ok(format!("Welcome {}!", info.username)) +/// } +/// +/// fn main() { +/// let app = App::new().resource( +/// "/index.html", +/// |r| r.method(http::Method::POST).with(index)); // <- use `with` extractor +/// } +/// ``` +/// +/// The `Json` type allows you to respond with well-formed JSON data: simply +/// return a value of type Json where T is the type of a structure +/// to serialize into *JSON*. The type `T` must implement the `Serialize` +/// trait from *serde*. +/// +/// ```rust,ignore +/// # extern crate actix_web; +/// # #[macro_use] extern crate serde_derive; +/// # use actix_web::*; +/// # +/// #[derive(Serialize)] +/// struct MyObj { +/// name: String, +/// } +/// +/// fn index(req: HttpRequest) -> Result> { +/// Ok(Json(MyObj { +/// name: req.match_info().query("name")?, +/// })) +/// } +/// # fn main() {} +/// ``` +pub struct Json(pub T); + +impl Json { + /// Deconstruct to an inner value + pub fn into_inner(self) -> T { + self.0 + } +} + +impl Deref for Json { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +impl DerefMut for Json { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl fmt::Debug for Json +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Json: {:?}", self.0) + } +} + +impl fmt::Display for Json +where + T: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl Responder for Json { + type Error = Error; + type Future = FutureResult; + + fn respond_to(self, _: &HttpRequest) -> Self::Future { + let body = match serde_json::to_string(&self.0) { + Ok(body) => body, + Err(e) => return err(e.into()), + }; + + ok(Response::build(StatusCode::OK) + .content_type("application/json") + .body(body)) + } +} + +impl FromRequest

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

) -> Self::Future { + let cfg = JsonConfig::default(); + + let req2 = req.clone(); + let err = Rc::clone(&cfg.ehandler); + Box::new( + JsonBody::new(req) + .limit(cfg.limit) + .map_err(move |e| (*err)(e, &req2)) + .map(Json), + ) + } +} + +/// Json extractor configuration +/// +/// ```rust,ignore +/// # extern crate actix_web; +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{error, http, App, HttpResponse, Json, Result}; +/// +/// #[derive(Deserialize)] +/// struct Info { +/// username: String, +/// } +/// +/// /// deserialize `Info` from request's body, max payload size is 4kb +/// fn index(info: Json) -> Result { +/// Ok(format!("Welcome {}!", info.username)) +/// } +/// +/// fn main() { +/// let app = App::new().resource("/index.html", |r| { +/// r.method(http::Method::POST) +/// .with_config(index, |cfg| { +/// cfg.0.limit(4096) // <- change json extractor configuration +/// .error_handler(|err, req| { // <- create custom error response +/// error::InternalError::from_response( +/// err, HttpResponse::Conflict().finish()).into() +/// }); +/// }) +/// }); +/// } +/// ``` +pub struct JsonConfig { + limit: usize, + ehandler: Rc Error>, +} + +impl JsonConfig { + /// Change max size of payload. By default max size is 256Kb + pub fn limit(&mut self, limit: usize) -> &mut Self { + self.limit = limit; + self + } + + /// Set custom error handler + pub fn error_handler(&mut self, f: F) -> &mut Self + where + F: Fn(JsonPayloadError, &HttpRequest) -> Error + 'static, + { + self.ehandler = Rc::new(f); + self + } +} + +impl Default for JsonConfig { + fn default() -> Self { + JsonConfig { + limit: 262_144, + ehandler: Rc::new(|e, _| e.into()), + } + } +} + /// Request payload extractor. /// /// Loads request's payload and construct Bytes instance. @@ -486,7 +594,7 @@ impl Default for FormConfig { /// /// ## Example /// -/// ```rust +/// ```rust,ignore /// extern crate bytes; /// # extern crate actix_web; /// use actix_web::{http, App, Result}; @@ -501,16 +609,23 @@ impl Default for FormConfig { /// .resource("/index.html", |r| r.method(http::Method::GET).with(index)); /// } /// ``` -impl FromRequest for Bytes { - type Config = PayloadConfig; - type Result = Result>, Error>; +impl

FromRequest

for Bytes +where + P: Stream + 'static, +{ + type Error = Error; + type Future = + Either>, FutureResult>; #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - // check content-type - cfg.check_mimetype(req)?; + fn from_request(req: &mut ServiceRequest

) -> Self::Future { + let cfg = PayloadConfig::default(); - Ok(Box::new(MessageBody::new(req).limit(cfg.limit).from_err())) + if let Err(e) = cfg.check_mimetype(req) { + return Either::B(err(e)); + } + + Either::A(Box::new(MessageBody::new(req).limit(cfg.limit).from_err())) } } @@ -523,7 +638,7 @@ impl FromRequest for Bytes { /// /// ## Example /// -/// ```rust +/// ```rust,ignore /// # extern crate actix_web; /// use actix_web::{http, App, Result}; /// @@ -541,19 +656,30 @@ impl FromRequest for Bytes { /// }); /// } /// ``` -impl FromRequest for String { - type Config = PayloadConfig; - type Result = Result>, Error>; +impl

FromRequest

for String +where + P: Stream + 'static, +{ + type Error = Error; + type Future = + Either>, FutureResult>; #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { + fn from_request(req: &mut ServiceRequest

) -> Self::Future { + let cfg = PayloadConfig::default(); + // check content-type - cfg.check_mimetype(req)?; + if let Err(e) = cfg.check_mimetype(req) { + return Either::B(err(e)); + } // check charset - let encoding = req.encoding()?; + let encoding = match req.encoding() { + Ok(enc) => enc, + Err(e) => return Either::B(err(e.into())), + }; - Ok(Box::new( + Either::A(Box::new( MessageBody::new(req) .limit(cfg.limit) .from_err() @@ -579,7 +705,7 @@ impl FromRequest for String { /// /// ## Example /// -/// ```rust +/// ```rust,ignore /// # extern crate actix_web; /// extern crate rand; /// #[macro_use] extern crate serde_derive; @@ -619,176 +745,30 @@ impl FromRequest for String { /// }); /// } /// ``` -impl FromRequest for Option +impl FromRequest

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

, + T::Future: 'static, { - type Config = T::Config; - type Result = Box, Error = Error>>; + type Error = Error; + type Future = Box, Error = Error>>; #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - Box::new(T::from_request(req, cfg).into().then(|r| match r { + fn from_request(req: &mut ServiceRequest

) -> Self::Future { + Box::new(T::from_request(req).then(|r| match r { Ok(v) => future::ok(Some(v)), Err(_) => future::ok(None), })) } } -/// Extract either one of two fields from the request. -/// -/// If both or none of the fields can be extracted, the default behaviour is to prefer the first -/// successful, last that failed. The behaviour can be changed by setting the appropriate -/// ```EitherCollisionStrategy```. -/// -/// CAVEAT: Most of the time both extractors will be run. Make sure that the extractors you specify -/// can be run one after another (or in parallel). This will always fail for extractors that modify -/// the request state (such as the `Form` extractors that read in the body stream). -/// So Either, Form> will not work correctly - it will only succeed if it matches the first -/// option, but will always fail to match the second (since the body stream will be at the end, and -/// appear to be empty). -/// -/// ## Example -/// -/// ```rust -/// # extern crate actix_web; -/// extern crate rand; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{http, App, Result, HttpRequest, Error, FromRequest}; -/// use actix_web::error::ErrorBadRequest; -/// use actix_web::Either; -/// -/// #[derive(Debug, Deserialize)] -/// struct Thing { name: String } -/// -/// #[derive(Debug, Deserialize)] -/// struct OtherThing { id: String } -/// -/// impl FromRequest for Thing { -/// type Config = (); -/// type Result = Result; -/// -/// #[inline] -/// fn from_request(req: &HttpRequest, _cfg: &Self::Config) -> Self::Result { -/// if rand::random() { -/// Ok(Thing { name: "thingy".into() }) -/// } else { -/// Err(ErrorBadRequest("no luck")) -/// } -/// } -/// } -/// -/// impl FromRequest for OtherThing { -/// type Config = (); -/// type Result = Result; -/// -/// #[inline] -/// fn from_request(req: &HttpRequest, _cfg: &Self::Config) -> Self::Result { -/// if rand::random() { -/// Ok(OtherThing { id: "otherthingy".into() }) -/// } else { -/// Err(ErrorBadRequest("no luck")) -/// } -/// } -/// } -/// -/// /// extract text data from request -/// fn index(supplied_thing: Either) -> Result { -/// match supplied_thing { -/// Either::A(thing) => Ok(format!("Got something: {:?}", thing)), -/// Either::B(other_thing) => Ok(format!("Got anotherthing: {:?}", other_thing)) -/// } -/// } -/// -/// fn main() { -/// let app = App::new().resource("/users/:first", |r| { -/// r.method(http::Method::POST).with(index) -/// }); -/// } -/// ``` -impl FromRequest for Either where A: FromRequest, B: FromRequest { - type Config = EitherConfig; - type Result = AsyncResult>; - - #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - let a = A::from_request(&req.clone(), &cfg.a).into().map(|a| Either::A(a)); - let b = B::from_request(req, &cfg.b).into().map(|b| Either::B(b)); - - match &cfg.collision_strategy { - EitherCollisionStrategy::PreferA => AsyncResult::future(Box::new(a.or_else(|_| b))), - EitherCollisionStrategy::PreferB => AsyncResult::future(Box::new(b.or_else(|_| a))), - EitherCollisionStrategy::FastestSuccessful => AsyncResult::future(Box::new(a.select2(b).then( |r| match r { - Ok(future::Either::A((ares, _b))) => AsyncResult::ok(ares), - Ok(future::Either::B((bres, _a))) => AsyncResult::ok(bres), - Err(future::Either::A((_aerr, b))) => AsyncResult::future(Box::new(b)), - Err(future::Either::B((_berr, a))) => AsyncResult::future(Box::new(a)) - }))), - EitherCollisionStrategy::ErrorA => AsyncResult::future(Box::new(b.then(|r| match r { - Err(_berr) => AsyncResult::future(Box::new(a)), - Ok(b) => AsyncResult::future(Box::new(a.then( |r| match r { - Ok(_a) => Err(ErrorConflict("Both wings of either extractor completed")), - Err(_arr) => Ok(b) - }))) - }))), - EitherCollisionStrategy::ErrorB => AsyncResult::future(Box::new(a.then(|r| match r { - Err(_aerr) => AsyncResult::future(Box::new(b)), - Ok(a) => AsyncResult::future(Box::new(b.then( |r| match r { - Ok(_b) => Err(ErrorConflict("Both wings of either extractor completed")), - Err(_berr) => Ok(a) - }))) - }))), - } - } -} - -/// Defines the result if neither or both of the extractors supplied to an Either extractor succeed. -#[derive(Debug)] -pub enum EitherCollisionStrategy { - /// If both are successful, return A, if both fail, return error of B - PreferA, - /// If both are successful, return B, if both fail, return error of A - PreferB, - /// Return result of the faster, error of the slower if both fail - FastestSuccessful, - /// Return error if both succeed, return error of A if both fail - ErrorA, - /// Return error if both succeed, return error of B if both fail - ErrorB -} - -impl Default for EitherCollisionStrategy { - fn default() -> Self { - EitherCollisionStrategy::FastestSuccessful - } -} - -///Determines Either extractor configuration -/// -///By default `EitherCollisionStrategy::FastestSuccessful` is used. -pub struct EitherConfig where A: FromRequest, B: FromRequest { - a: A::Config, - b: B::Config, - collision_strategy: EitherCollisionStrategy -} - -impl Default for EitherConfig where A: FromRequest, B: FromRequest { - fn default() -> Self { - EitherConfig { - a: A::Config::default(), - b: B::Config::default(), - collision_strategy: EitherCollisionStrategy::default() - } - } -} - /// Optionally extract a field from the request or extract the Error if unsuccessful /// /// If the FromRequest for T fails, inject Err into handler rather than returning an error response /// /// ## Example /// -/// ```rust +/// ```rust,ignore /// # extern crate actix_web; /// extern crate rand; /// #[macro_use] extern crate serde_derive; @@ -798,12 +778,12 @@ impl Default for EitherConfig where A: FromRequest, B: FromRequ /// #[derive(Debug, Deserialize)] /// struct Thing { name: String } /// -/// impl FromRequest for Thing { +/// impl FromRequest for Thing { /// type Config = (); /// type Result = Result; /// /// #[inline] -/// fn from_request(req: &HttpRequest, _cfg: &Self::Config) -> Self::Result { +/// fn from_request(req: &Request, _cfg: &Self::Config) -> Self::Result { /// if rand::random() { /// Ok(Thing { name: "thingy".into() }) /// } else { @@ -827,16 +807,21 @@ impl Default for EitherConfig where A: FromRequest, B: FromRequ /// }); /// } /// ``` -impl FromRequest for Result +impl FromRequest

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

, + T::Future: 'static, + T::Error: 'static, { - type Config = T::Config; - type Result = Box, Error = Error>>; + type Error = Error; + type Future = Box, Error = Error>>; #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - Box::new(T::from_request(req, cfg).into().then(future::ok)) + fn from_request(req: &mut ServiceRequest

) -> Self::Future { + Box::new(T::from_request(req).then(|res| match res { + Ok(v) => ok(Ok(v)), + Err(e) => ok(Err(e)), + })) } } @@ -860,7 +845,7 @@ impl PayloadConfig { self } - fn check_mimetype(&self, req: &HttpRequest) -> Result<(), Error> { + fn check_mimetype

(&self, req: &ServiceRequest

) -> Result<(), Error> { // check content-type if let Some(ref mt) = self.mimetype { match req.mime_type() { @@ -893,34 +878,26 @@ impl Default for PayloadConfig { macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { /// FromRequest implementation for tuple - impl + 'static),+> FromRequest for ($($T,)+) - where - S: 'static, + impl + 'static),+> FromRequest

for ($($T,)+) { - type Config = ($($T::Config,)+); - type Result = Box>; + type Error = Error; + type Future = $fut_type; - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - Box::new($fut_type { - s: PhantomData, + fn from_request(req: &mut ServiceRequest

) -> Self::Future { + $fut_type { items: <($(Option<$T>,)+)>::default(), - futs: ($(Some($T::from_request(req, &cfg.$n).into()),)+), - }) + futs: ($($T::from_request(req),)+), + } } } - struct $fut_type),+> - where - S: 'static, - { - s: PhantomData, + #[doc(hidden)] + pub struct $fut_type),+> { items: ($(Option<$T>,)+), - futs: ($(Option>,)+), + futs: ($($T::Future,)+), } - impl),+> Future for $fut_type - where - S: 'static, + impl),+> Future for $fut_type { type Item = ($($T,)+); type Error = Error; @@ -929,14 +906,13 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { let mut ready = true; $( - if self.futs.$n.is_some() { - match self.futs.$n.as_mut().unwrap().poll() { + if self.items.$n.is_none() { + match self.futs.$n.poll() { Ok(Async::Ready(item)) => { self.items.$n = Some(item); - self.futs.$n.take(); } Ok(Async::NotReady) => ready = false, - Err(e) => return Err(e), + Err(e) => return Err(e.into()), } } )+ @@ -952,10 +928,13 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { } }); -impl FromRequest for () { - type Config = (); - type Result = Self; - fn from_request(_req: &HttpRequest, _cfg: &Self::Config) -> Self::Result {} +impl

FromRequest

for () { + type Error = Error; + type Future = FutureResult<(), Error>; + + fn from_request(_req: &mut ServiceRequest

) -> Self::Future { + ok(()) + } } tuple_from_req!(TupleFromRequest1, (0, A)); @@ -1005,385 +984,298 @@ tuple_from_req!( (7, H), (8, I) ); +tuple_from_req!( + TupleFromRequest10, + (0, A), + (1, B), + (2, C), + (3, D), + (4, E), + (5, F), + (6, G), + (7, H), + (8, I), + (9, J) +); -#[cfg(test)] -mod tests { - use super::*; - use bytes::Bytes; - use futures::{Async, Future}; - use http::header; - use mime; - use resource::Resource; - use router::{ResourceDef, Router}; - use test::TestRequest; +// #[cfg(test)] +// mod tests { +// use super::*; +// use actix_http::http::header; +// use actix_http::test::TestRequest; +// use bytes::Bytes; +// use futures::{Async, Future}; +// use mime; +// use serde::{Deserialize, Serialize}; - #[derive(Deserialize, Debug, PartialEq)] - struct Info { - hello: String, - } +// use crate::resource::Resource; +// // use crate::router::{ResourceDef, Router}; - #[derive(Deserialize, Debug, PartialEq)] - struct OtherInfo { - bye: String, - } +// #[derive(Deserialize, Debug, PartialEq)] +// struct Info { +// hello: String, +// } - #[test] - fn test_bytes() { - let cfg = PayloadConfig::default(); - let req = TestRequest::with_header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .finish(); +// #[test] +// fn test_bytes() { +// let cfg = PayloadConfig::default(); +// let req = TestRequest::with_header(header::CONTENT_LENGTH, "11") +// .set_payload(Bytes::from_static(b"hello=world")) +// .finish(); - match Bytes::from_request(&req, &cfg).unwrap().poll().unwrap() { - Async::Ready(s) => { - assert_eq!(s, Bytes::from_static(b"hello=world")); - } - _ => unreachable!(), - } - } +// match Bytes::from_request(&req, &cfg).unwrap().poll().unwrap() { +// Async::Ready(s) => { +// assert_eq!(s, Bytes::from_static(b"hello=world")); +// } +// _ => unreachable!(), +// } +// } - #[test] - fn test_string() { - let cfg = PayloadConfig::default(); - let req = TestRequest::with_header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .finish(); +// #[test] +// fn test_string() { +// let cfg = PayloadConfig::default(); +// let req = TestRequest::with_header(header::CONTENT_LENGTH, "11") +// .set_payload(Bytes::from_static(b"hello=world")) +// .finish(); - match String::from_request(&req, &cfg).unwrap().poll().unwrap() { - Async::Ready(s) => { - assert_eq!(s, "hello=world"); - } - _ => unreachable!(), - } - } +// match String::from_request(&req, &cfg).unwrap().poll().unwrap() { +// Async::Ready(s) => { +// assert_eq!(s, "hello=world"); +// } +// _ => unreachable!(), +// } +// } - #[test] - fn test_form() { - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .finish(); +// #[test] +// fn test_form() { +// let req = TestRequest::with_header( +// header::CONTENT_TYPE, +// "application/x-www-form-urlencoded", +// ) +// .header(header::CONTENT_LENGTH, "11") +// .set_payload(Bytes::from_static(b"hello=world")) +// .finish(); - let mut cfg = FormConfig::default(); - cfg.limit(4096); - match Form::::from_request(&req, &cfg).poll().unwrap() { - Async::Ready(s) => { - assert_eq!(s.hello, "world"); - } - _ => unreachable!(), - } - } +// let mut cfg = FormConfig::default(); +// cfg.limit(4096); +// match Form::::from_request(&req, &cfg).poll().unwrap() { +// Async::Ready(s) => { +// assert_eq!(s.hello, "world"); +// } +// _ => unreachable!(), +// } +// } - #[test] - fn test_option() { - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).finish(); +// #[test] +// fn test_option() { +// let req = TestRequest::with_header( +// header::CONTENT_TYPE, +// "application/x-www-form-urlencoded", +// ) +// .finish(); - let mut cfg = FormConfig::default(); - cfg.limit(4096); +// let mut cfg = FormConfig::default(); +// cfg.limit(4096); - match Option::>::from_request(&req, &cfg) - .poll() - .unwrap() - { - Async::Ready(r) => assert_eq!(r, None), - _ => unreachable!(), - } +// match Option::>::from_request(&req, &cfg) +// .poll() +// .unwrap() +// { +// Async::Ready(r) => assert_eq!(r, None), +// _ => unreachable!(), +// } - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "9") - .set_payload(Bytes::from_static(b"hello=world")) - .finish(); +// let req = TestRequest::with_header( +// header::CONTENT_TYPE, +// "application/x-www-form-urlencoded", +// ) +// .header(header::CONTENT_LENGTH, "9") +// .set_payload(Bytes::from_static(b"hello=world")) +// .finish(); - match Option::>::from_request(&req, &cfg) - .poll() - .unwrap() - { - Async::Ready(r) => assert_eq!( - r, - Some(Form(Info { - hello: "world".into() - })) - ), - _ => unreachable!(), - } +// match Option::>::from_request(&req, &cfg) +// .poll() +// .unwrap() +// { +// Async::Ready(r) => assert_eq!( +// r, +// Some(Form(Info { +// hello: "world".into() +// })) +// ), +// _ => unreachable!(), +// } - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "9") - .set_payload(Bytes::from_static(b"bye=world")) - .finish(); +// let req = TestRequest::with_header( +// header::CONTENT_TYPE, +// "application/x-www-form-urlencoded", +// ) +// .header(header::CONTENT_LENGTH, "9") +// .set_payload(Bytes::from_static(b"bye=world")) +// .finish(); - match Option::>::from_request(&req, &cfg) - .poll() - .unwrap() - { - Async::Ready(r) => assert_eq!(r, None), - _ => unreachable!(), - } - } +// match Option::>::from_request(&req, &cfg) +// .poll() +// .unwrap() +// { +// Async::Ready(r) => assert_eq!(r, None), +// _ => unreachable!(), +// } +// } - #[test] - fn test_either() { - let req = TestRequest::default().finish(); - let mut cfg: EitherConfig, Query, _> = EitherConfig::default(); +// #[test] +// fn test_result() { +// let req = TestRequest::with_header( +// header::CONTENT_TYPE, +// "application/x-www-form-urlencoded", +// ) +// .header(header::CONTENT_LENGTH, "11") +// .set_payload(Bytes::from_static(b"hello=world")) +// .finish(); - assert!(Either::, Query>::from_request(&req, &cfg).poll().is_err()); +// match Result::, Error>::from_request(&req, &FormConfig::default()) +// .poll() +// .unwrap() +// { +// Async::Ready(Ok(r)) => assert_eq!( +// r, +// Form(Info { +// hello: "world".into() +// }) +// ), +// _ => unreachable!(), +// } - let req = TestRequest::default().uri("/index?hello=world").finish(); +// let req = TestRequest::with_header( +// header::CONTENT_TYPE, +// "application/x-www-form-urlencoded", +// ) +// .header(header::CONTENT_LENGTH, "9") +// .set_payload(Bytes::from_static(b"bye=world")) +// .finish(); - match Either::, Query>::from_request(&req, &cfg).poll().unwrap() { - Async::Ready(r) => assert_eq!(r, Either::A(Query(Info { hello: "world".into() }))), - _ => unreachable!(), - } +// match Result::, Error>::from_request(&req, &FormConfig::default()) +// .poll() +// .unwrap() +// { +// Async::Ready(r) => assert!(r.is_err()), +// _ => unreachable!(), +// } +// } - let req = TestRequest::default().uri("/index?bye=world").finish(); - match Either::, Query>::from_request(&req, &cfg).poll().unwrap() { - Async::Ready(r) => assert_eq!(r, Either::B(Query(OtherInfo { bye: "world".into() }))), - _ => unreachable!(), - } +// #[test] +// fn test_payload_config() { +// let req = TestRequest::default().finish(); +// let mut cfg = PayloadConfig::default(); +// cfg.mimetype(mime::APPLICATION_JSON); +// assert!(cfg.check_mimetype(&req).is_err()); - let req = TestRequest::default().uri("/index?hello=world&bye=world").finish(); - cfg.collision_strategy = EitherCollisionStrategy::PreferA; +// let req = TestRequest::with_header( +// header::CONTENT_TYPE, +// "application/x-www-form-urlencoded", +// ) +// .finish(); +// assert!(cfg.check_mimetype(&req).is_err()); - match Either::, Query>::from_request(&req, &cfg).poll().unwrap() { - Async::Ready(r) => assert_eq!(r, Either::A(Query(Info { hello: "world".into() }))), - _ => unreachable!(), - } +// let req = +// TestRequest::with_header(header::CONTENT_TYPE, "application/json").finish(); +// assert!(cfg.check_mimetype(&req).is_ok()); +// } - cfg.collision_strategy = EitherCollisionStrategy::PreferB; +// #[derive(Deserialize)] +// struct MyStruct { +// key: String, +// value: String, +// } - match Either::, Query>::from_request(&req, &cfg).poll().unwrap() { - Async::Ready(r) => assert_eq!(r, Either::B(Query(OtherInfo { bye: "world".into() }))), - _ => unreachable!(), - } +// #[derive(Deserialize)] +// struct Id { +// id: String, +// } - cfg.collision_strategy = EitherCollisionStrategy::ErrorA; - assert!(Either::, Query>::from_request(&req, &cfg).poll().is_err()); +// #[derive(Deserialize)] +// struct Test2 { +// key: String, +// value: u32, +// } - cfg.collision_strategy = EitherCollisionStrategy::FastestSuccessful; - assert!(Either::, Query>::from_request(&req, &cfg).poll().is_ok()); - } +// #[test] +// fn test_request_extract() { +// let req = TestRequest::with_uri("/name/user1/?id=test").finish(); - #[test] - fn test_result() { - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .finish(); +// let mut router = Router::<()>::default(); +// router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); +// let info = router.recognize(&req, &(), 0); +// let req = req.with_route_info(info); - match Result::, Error>::from_request(&req, &FormConfig::default()) - .poll() - .unwrap() - { - Async::Ready(Ok(r)) => assert_eq!( - r, - Form(Info { - hello: "world".into() - }) - ), - _ => unreachable!(), - } +// let s = Path::::from_request(&req, &()).unwrap(); +// assert_eq!(s.key, "name"); +// assert_eq!(s.value, "user1"); - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "9") - .set_payload(Bytes::from_static(b"bye=world")) - .finish(); +// let s = Path::<(String, String)>::from_request(&req, &()).unwrap(); +// assert_eq!(s.0, "name"); +// assert_eq!(s.1, "user1"); - match Result::, Error>::from_request(&req, &FormConfig::default()) - .poll() - .unwrap() - { - Async::Ready(r) => assert!(r.is_err()), - _ => unreachable!(), - } - } +// let s = Query::::from_request(&req, &()).unwrap(); +// assert_eq!(s.id, "test"); - #[test] - fn test_payload_config() { - let req = TestRequest::default().finish(); - let mut cfg = PayloadConfig::default(); - cfg.mimetype(mime::APPLICATION_JSON); - assert!(cfg.check_mimetype(&req).is_err()); +// let mut router = Router::<()>::default(); +// router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); +// let req = TestRequest::with_uri("/name/32/").finish(); +// let info = router.recognize(&req, &(), 0); +// let req = req.with_route_info(info); - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).finish(); - assert!(cfg.check_mimetype(&req).is_err()); +// let s = Path::::from_request(&req, &()).unwrap(); +// assert_eq!(s.as_ref().key, "name"); +// assert_eq!(s.value, 32); - let req = - TestRequest::with_header(header::CONTENT_TYPE, "application/json").finish(); - assert!(cfg.check_mimetype(&req).is_ok()); - } +// let s = Path::<(String, u8)>::from_request(&req, &()).unwrap(); +// assert_eq!(s.0, "name"); +// assert_eq!(s.1, 32); - #[derive(Deserialize)] - struct MyStruct { - key: String, - value: String, - } +// let res = Path::>::extract(&req).unwrap(); +// assert_eq!(res[0], "name".to_owned()); +// assert_eq!(res[1], "32".to_owned()); +// } - #[derive(Deserialize)] - struct Id { - id: String, - } +// #[test] +// fn test_extract_path_single() { +// let mut router = Router::<()>::default(); +// router.register_resource(Resource::new(ResourceDef::new("/{value}/"))); - #[derive(Deserialize)] - struct Test2 { - key: String, - value: u32, - } +// let req = TestRequest::with_uri("/32/").finish(); +// let info = router.recognize(&req, &(), 0); +// let req = req.with_route_info(info); +// assert_eq!(*Path::::from_request(&req, &()).unwrap(), 32); +// } - #[test] - fn test_request_extract() { - let req = TestRequest::with_uri("/name/user1/?id=test").finish(); +// #[test] +// fn test_tuple_extract() { +// let mut router = Router::<()>::default(); +// router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); - let info = router.recognize(&req, &(), 0); - let req = req.with_route_info(info); +// let req = TestRequest::with_uri("/name/user1/?id=test").finish(); +// let info = router.recognize(&req, &(), 0); +// let req = req.with_route_info(info); - let s = Path::::from_request(&req, &PathConfig::default()).unwrap(); - assert_eq!(s.key, "name"); - assert_eq!(s.value, "user1"); +// let res = match <(Path<(String, String)>,)>::extract(&req).poll() { +// Ok(Async::Ready(res)) => res, +// _ => panic!("error"), +// }; +// assert_eq!((res.0).0, "name"); +// assert_eq!((res.0).1, "user1"); - let s = Path::<(String, String)>::from_request(&req, &PathConfig::default()).unwrap(); - assert_eq!(s.0, "name"); - assert_eq!(s.1, "user1"); +// let res = match <(Path<(String, String)>, Path<(String, String)>)>::extract(&req) +// .poll() +// { +// Ok(Async::Ready(res)) => res, +// _ => panic!("error"), +// }; +// assert_eq!((res.0).0, "name"); +// assert_eq!((res.0).1, "user1"); +// assert_eq!((res.1).0, "name"); +// assert_eq!((res.1).1, "user1"); - let s = Query::::from_request(&req, &QueryConfig::default()).unwrap(); - assert_eq!(s.id, "test"); - - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); - let req = TestRequest::with_uri("/name/32/").finish(); - let info = router.recognize(&req, &(), 0); - let req = req.with_route_info(info); - - let s = Path::::from_request(&req, &PathConfig::default()).unwrap(); - assert_eq!(s.as_ref().key, "name"); - assert_eq!(s.value, 32); - - let s = Path::<(String, u8)>::from_request(&req, &PathConfig::default()).unwrap(); - assert_eq!(s.0, "name"); - assert_eq!(s.1, 32); - - let res = Path::>::extract(&req).unwrap(); - assert_eq!(res[0], "name".to_owned()); - assert_eq!(res[1], "32".to_owned()); - } - - #[test] - fn test_extract_path_single() { - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/{value}/"))); - - let req = TestRequest::with_uri("/32/").finish(); - let info = router.recognize(&req, &(), 0); - let req = req.with_route_info(info); - assert_eq!(*Path::::from_request(&req, &&PathConfig::default()).unwrap(), 32); - } - - #[test] - fn test_extract_path_decode() { - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/{value}/"))); - - macro_rules! test_single_value { - ($value:expr, $expected:expr) => { - { - let req = TestRequest::with_uri($value).finish(); - let info = router.recognize(&req, &(), 0); - let req = req.with_route_info(info); - assert_eq!(*Path::::from_request(&req, &PathConfig::default()).unwrap(), $expected); - } - } - } - - test_single_value!("/%25/", "%"); - test_single_value!("/%40%C2%A3%24%25%5E%26%2B%3D/", "@£$%^&+="); - test_single_value!("/%2B/", "+"); - test_single_value!("/%252B/", "%2B"); - test_single_value!("/%2F/", "/"); - test_single_value!("/%252F/", "%2F"); - test_single_value!("/http%3A%2F%2Flocalhost%3A80%2Ffoo/", "http://localhost:80/foo"); - test_single_value!("/%2Fvar%2Flog%2Fsyslog/", "/var/log/syslog"); - test_single_value!( - "/http%3A%2F%2Flocalhost%3A80%2Ffile%2F%252Fvar%252Flog%252Fsyslog/", - "http://localhost:80/file/%2Fvar%2Flog%2Fsyslog" - ); - - let req = TestRequest::with_uri("/%25/7/?id=test").finish(); - - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); - let info = router.recognize(&req, &(), 0); - let req = req.with_route_info(info); - - let s = Path::::from_request(&req, &PathConfig::default()).unwrap(); - assert_eq!(s.key, "%"); - assert_eq!(s.value, 7); - - let s = Path::<(String, String)>::from_request(&req, &PathConfig::default()).unwrap(); - assert_eq!(s.0, "%"); - assert_eq!(s.1, "7"); - } - - #[test] - fn test_extract_path_no_decode() { - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/{value}/"))); - - let req = TestRequest::with_uri("/%25/").finish(); - let info = router.recognize(&req, &(), 0); - let req = req.with_route_info(info); - assert_eq!( - *Path::::from_request( - &req, - &&PathConfig::default().disable_decoding() - ).unwrap(), - "%25" - ); - } - - #[test] - fn test_tuple_extract() { - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); - - let req = TestRequest::with_uri("/name/user1/?id=test").finish(); - let info = router.recognize(&req, &(), 0); - let req = req.with_route_info(info); - - let res = match <(Path<(String, String)>,)>::extract(&req).poll() { - Ok(Async::Ready(res)) => res, - _ => panic!("error"), - }; - assert_eq!((res.0).0, "name"); - assert_eq!((res.0).1, "user1"); - - let res = match <(Path<(String, String)>, Path<(String, String)>)>::extract(&req) - .poll() - { - Ok(Async::Ready(res)) => res, - _ => panic!("error"), - }; - assert_eq!((res.0).0, "name"); - assert_eq!((res.0).1, "user1"); - assert_eq!((res.1).0, "name"); - assert_eq!((res.1).1, "user1"); - - let () = <()>::extract(&req); - } -} +// let () = <()>::extract(&req); +// } +// } diff --git a/src/filter.rs b/src/filter.rs new file mode 100644 index 00000000..a0566092 --- /dev/null +++ b/src/filter.rs @@ -0,0 +1,327 @@ +//! Route match predicates +#![allow(non_snake_case)] +use actix_http::http::{self, header, HttpTryFrom}; + +use crate::request::HttpRequest; + +/// Trait defines resource predicate. +/// Predicate can modify request object. It is also possible to +/// to store extra attributes on request by using `Extensions` container, +/// Extensions container available via `HttpRequest::extensions()` method. +pub trait Filter { + /// Check if request matches predicate + fn check(&self, request: &HttpRequest) -> bool; +} + +/// Return filter that matches if any of supplied filters. +/// +/// ```rust,ignore +/// # extern crate actix_web; +/// use actix_web2::{filter, App, HttpResponse}; +/// +/// fn main() { +/// App::new().resource("/index.html", |r| { +/// r.route() +/// .filter(pred::Any(pred::Get()).or(pred::Post())) +/// .f(|r| HttpResponse::MethodNotAllowed()) +/// }); +/// } +/// ``` +pub fn Any(filter: F) -> AnyFilter { + AnyFilter(vec![Box::new(filter)]) +} + +/// Matches if any of supplied filters matche. +pub struct AnyFilter(Vec>); + +impl AnyFilter { + /// Add filter to a list of filters to check + pub fn or(mut self, filter: F) -> Self { + self.0.push(Box::new(filter)); + self + } +} + +impl Filter for AnyFilter { + fn check(&self, req: &HttpRequest) -> bool { + for p in &self.0 { + if p.check(req) { + return true; + } + } + false + } +} + +/// Return filter that matches if all of supplied filters match. +/// +/// ```rust,ignore +/// # extern crate actix_web; +/// use actix_web::{pred, App, HttpResponse}; +/// +/// fn main() { +/// App::new().resource("/index.html", |r| { +/// r.route() +/// .filter( +/// pred::All(pred::Get()) +/// .and(pred::Header("content-type", "text/plain")), +/// ) +/// .f(|_| HttpResponse::MethodNotAllowed()) +/// }); +/// } +/// ``` +pub fn All(filter: F) -> AllFilter { + AllFilter(vec![Box::new(filter)]) +} + +/// Matches if all of supplied filters matche. +pub struct AllFilter(Vec>); + +impl AllFilter { + /// Add new predicate to list of predicates to check + pub fn and(mut self, filter: F) -> Self { + self.0.push(Box::new(filter)); + self + } +} + +impl Filter for AllFilter { + fn check(&self, request: &HttpRequest) -> bool { + for p in &self.0 { + if !p.check(request) { + return false; + } + } + true + } +} + +/// Return predicate that matches if supplied predicate does not match. +pub fn Not(filter: F) -> NotFilter { + NotFilter(Box::new(filter)) +} + +#[doc(hidden)] +pub struct NotFilter(Box); + +impl Filter for NotFilter { + fn check(&self, request: &HttpRequest) -> bool { + !self.0.check(request) + } +} + +/// Http method predicate +#[doc(hidden)] +pub struct MethodFilter(http::Method); + +impl Filter for MethodFilter { + fn check(&self, request: &HttpRequest) -> bool { + request.method() == self.0 + } +} + +/// Predicate to match *GET* http method +pub fn Get() -> MethodFilter { + MethodFilter(http::Method::GET) +} + +/// Predicate to match *POST* http method +pub fn Post() -> MethodFilter { + MethodFilter(http::Method::POST) +} + +/// Predicate to match *PUT* http method +pub fn Put() -> MethodFilter { + MethodFilter(http::Method::PUT) +} + +/// Predicate to match *DELETE* http method +pub fn Delete() -> MethodFilter { + MethodFilter(http::Method::DELETE) +} + +/// Predicate to match *HEAD* http method +pub fn Head() -> MethodFilter { + MethodFilter(http::Method::HEAD) +} + +/// Predicate to match *OPTIONS* http method +pub fn Options() -> MethodFilter { + MethodFilter(http::Method::OPTIONS) +} + +/// Predicate to match *CONNECT* http method +pub fn Connect() -> MethodFilter { + MethodFilter(http::Method::CONNECT) +} + +/// Predicate to match *PATCH* http method +pub fn Patch() -> MethodFilter { + MethodFilter(http::Method::PATCH) +} + +/// Predicate to match *TRACE* http method +pub fn Trace() -> MethodFilter { + MethodFilter(http::Method::TRACE) +} + +/// Predicate to match specified http method +pub fn Method(method: http::Method) -> MethodFilter { + MethodFilter(method) +} + +/// Return predicate that matches if request contains specified header and +/// value. +pub fn Header(name: &'static str, value: &'static str) -> HeaderFilter { + HeaderFilter( + header::HeaderName::try_from(name).unwrap(), + header::HeaderValue::from_static(value), + ) +} + +#[doc(hidden)] +pub struct HeaderFilter(header::HeaderName, header::HeaderValue); + +impl Filter for HeaderFilter { + fn check(&self, req: &HttpRequest) -> bool { + if let Some(val) = req.headers().get(&self.0) { + return val == self.1; + } + false + } +} + +/// Return predicate that matches if request contains specified Host name. +/// +/// ```rust,ignore +/// # extern crate actix_web; +/// use actix_web::{pred, App, HttpResponse}; +/// +/// fn main() { +/// App::new().resource("/index.html", |r| { +/// r.route() +/// .filter(pred::Host("www.rust-lang.org")) +/// .f(|_| HttpResponse::MethodNotAllowed()) +/// }); +/// } +/// ``` +pub fn Host>(host: H) -> HostFilter { + HostFilter(host.as_ref().to_string(), None) +} + +#[doc(hidden)] +pub struct HostFilter(String, Option); + +impl HostFilter { + /// Set reuest scheme to match + pub fn scheme>(&mut self, scheme: H) { + self.1 = Some(scheme.as_ref().to_string()) + } +} + +impl Filter for HostFilter { + fn check(&self, _req: &HttpRequest) -> bool { + // let info = req.connection_info(); + // if let Some(ref scheme) = self.1 { + // self.0 == info.host() && scheme == info.scheme() + // } else { + // self.0 == info.host() + // } + false + } +} + +// #[cfg(test)] +// mod tests { +// use actix_http::http::{header, Method}; +// use actix_http::test::TestRequest; + +// use super::*; + +// #[test] +// fn test_header() { +// let req = TestRequest::with_header( +// header::TRANSFER_ENCODING, +// header::HeaderValue::from_static("chunked"), +// ) +// .finish(); + +// let pred = Header("transfer-encoding", "chunked"); +// assert!(pred.check(&req, req.state())); + +// let pred = Header("transfer-encoding", "other"); +// assert!(!pred.check(&req, req.state())); + +// let pred = Header("content-type", "other"); +// assert!(!pred.check(&req, req.state())); +// } + +// #[test] +// fn test_host() { +// let req = TestRequest::default() +// .header( +// header::HOST, +// header::HeaderValue::from_static("www.rust-lang.org"), +// ) +// .finish(); + +// let pred = Host("www.rust-lang.org"); +// assert!(pred.check(&req, req.state())); + +// let pred = Host("localhost"); +// assert!(!pred.check(&req, req.state())); +// } + +// #[test] +// fn test_methods() { +// let req = TestRequest::default().finish(); +// let req2 = TestRequest::default().method(Method::POST).finish(); + +// assert!(Get().check(&req, req.state())); +// assert!(!Get().check(&req2, req2.state())); +// assert!(Post().check(&req2, req2.state())); +// assert!(!Post().check(&req, req.state())); + +// let r = TestRequest::default().method(Method::PUT).finish(); +// assert!(Put().check(&r, r.state())); +// assert!(!Put().check(&req, req.state())); + +// let r = TestRequest::default().method(Method::DELETE).finish(); +// assert!(Delete().check(&r, r.state())); +// assert!(!Delete().check(&req, req.state())); + +// let r = TestRequest::default().method(Method::HEAD).finish(); +// assert!(Head().check(&r, r.state())); +// assert!(!Head().check(&req, req.state())); + +// let r = TestRequest::default().method(Method::OPTIONS).finish(); +// assert!(Options().check(&r, r.state())); +// assert!(!Options().check(&req, req.state())); + +// let r = TestRequest::default().method(Method::CONNECT).finish(); +// assert!(Connect().check(&r, r.state())); +// assert!(!Connect().check(&req, req.state())); + +// let r = TestRequest::default().method(Method::PATCH).finish(); +// assert!(Patch().check(&r, r.state())); +// assert!(!Patch().check(&req, req.state())); + +// let r = TestRequest::default().method(Method::TRACE).finish(); +// assert!(Trace().check(&r, r.state())); +// assert!(!Trace().check(&req, req.state())); +// } + +// #[test] +// fn test_preds() { +// let r = TestRequest::default().method(Method::TRACE).finish(); + +// assert!(Not(Get()).check(&r, r.state())); +// assert!(!Not(Trace()).check(&r, r.state())); + +// assert!(All(Trace()).and(Trace()).check(&r, r.state())); +// assert!(!All(Get()).and(Trace()).check(&r, r.state())); + +// assert!(Any(Get()).or(Trace()).check(&r, r.state())); +// assert!(!Any(Get()).or(Get()).check(&r, r.state())); +// } +// } diff --git a/src/framed_app.rs b/src/framed_app.rs new file mode 100644 index 00000000..ba925414 --- /dev/null +++ b/src/framed_app.rs @@ -0,0 +1,240 @@ +use std::marker::PhantomData; +use std::rc::Rc; + +use actix_codec::Framed; +use actix_http::h1::Codec; +use actix_http::{Request, Response, SendResponse}; +use actix_router::{Path, Router, Url}; +use actix_service::{IntoNewService, NewService, Service}; +use actix_utils::cloneable::CloneableService; +use futures::{Async, Future, Poll}; +use tokio_io::{AsyncRead, AsyncWrite}; + +use crate::app::{HttpServiceFactory, State}; +use crate::framed_handler::FramedRequest; +use crate::helpers::{BoxedHttpNewService, BoxedHttpService, HttpNewService}; +use crate::request::Request as WebRequest; + +pub type FRequest = (Request, Framed); +type BoxedResponse = Box>; + +/// Application builder +pub struct FramedApp { + services: Vec<(String, BoxedHttpNewService, ()>)>, + state: State, +} + +impl FramedApp { + pub fn new() -> Self { + FramedApp { + services: Vec::new(), + state: State::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, Response = ()> + 'static, + ::Future: 'static, + ::Service: Service>, + <::Service as Service>::Future: 'static, + { + let path = factory.path().to_string(); + self.services.push(( + path, + Box::new(HttpNewService::new(factory.create(self.state.clone()))), + )); + self + } + + pub fn register_service(&mut self, factory: U) + where + U: HttpServiceFactory, + U::Factory: NewService, Response = ()> + 'static, + ::Future: 'static, + ::Service: Service>, + <::Service as Service>::Future: 'static, + { + let path = factory.path().to_string(); + self.services.push(( + path, + Box::new(HttpNewService::new(factory.create(self.state.clone()))), + )); + } +} + +impl IntoNewService> for FramedApp +where + T: AsyncRead + AsyncWrite, +{ + fn into_new_service(self) -> FramedAppFactory { + FramedAppFactory { + state: self.state, + services: Rc::new(self.services), + _t: PhantomData, + } + } +} + +#[derive(Clone)] +pub struct FramedAppFactory { + state: State, + services: Rc, ()>)>>, + _t: PhantomData, +} + +impl NewService for FramedAppFactory +where + T: AsyncRead + AsyncWrite, +{ + type Request = FRequest; + type Response = (); + type Error = (); + type InitError = (); + type Service = CloneableService>; + type Future = CreateService; + + fn new_service(&self) -> 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(), + // default: self.default.take().expect("something is wrong"), + }))) + } else { + Ok(Async::NotReady) + } + } +} + +pub struct FramedAppService { + state: State, + router: Router, ()>>, +} + +impl Service for FramedAppService +where + T: AsyncRead + AsyncWrite, +{ + type Request = FRequest; + type Response = (); + type Error = (); + type Future = BoxedResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + // let mut ready = true; + // for service in &mut self.services { + // if let Async::NotReady = service.poll_ready()? { + // ready = false; + // } + // } + // if ready { + // Ok(Async::Ready(())) + // } else { + // Ok(Async::NotReady) + // } + 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( + WebRequest::new(self.state.clone(), req, path), + framed, + )); + } + // for item in &mut self.services { + // req = match item.handle(req) { + // Ok(fut) => return fut, + // Err(req) => req, + // }; + // } + // self.default.call(req) + Box::new( + SendResponse::send(framed, Response::NotFound().finish().into()) + .map(|_| ()) + .map_err(|_| ()), + ) + } +} diff --git a/src/framed_handler.rs b/src/framed_handler.rs new file mode 100644 index 00000000..109b5f0a --- /dev/null +++ b/src/framed_handler.rs @@ -0,0 +1,379 @@ +use std::marker::PhantomData; +use std::rc::Rc; + +use actix_codec::Framed; +use actix_http::{h1::Codec, Error}; +use actix_service::{NewService, Service}; +use futures::future::{ok, FutureResult}; +use futures::{Async, Future, IntoFuture, Poll}; +use log::error; + +use crate::handler::FromRequest; +use crate::request::Request; + +pub struct FramedError { + pub err: Error, + pub framed: Framed, +} + +pub struct FramedRequest { + req: Request, + framed: Framed, + param: Ex, +} + +impl FramedRequest { + pub fn new(req: Request, framed: Framed) -> Self { + Self { + req, + framed, + param: (), + } + } +} + +impl FramedRequest { + pub fn request(&self) -> &Request { + &self.req + } + + pub fn request_mut(&mut self) -> &mut Request { + &mut self.req + } + + pub fn into_parts(self) -> (Request, Framed, Ex) { + (self.req, self.framed, self.param) + } + + pub fn map(self, op: F) -> FramedRequest + where + F: FnOnce(Ex) -> Ex2, + { + FramedRequest { + req: self.req, + framed: self.framed, + param: op(self.param), + } + } +} + +/// T handler converter factory +pub trait FramedFactory: Clone + 'static +where + R: IntoFuture, + E: Into, +{ + fn call(&self, framed: Framed, param: T, extra: Ex) -> R; +} + +#[doc(hidden)] +pub struct FramedHandle +where + F: FramedFactory, + R: IntoFuture, + E: Into, +{ + hnd: F, + _t: PhantomData<(S, Io, Ex, T, R, E)>, +} + +impl FramedHandle +where + F: FramedFactory, + R: IntoFuture, + E: Into, +{ + pub fn new(hnd: F) -> Self { + FramedHandle { + hnd, + _t: PhantomData, + } + } +} +impl NewService for FramedHandle +where + F: FramedFactory, + R: IntoFuture, + E: Into, +{ + type Request = (T, FramedRequest); + type Response = (); + type Error = FramedError; + type InitError = (); + type Service = FramedHandleService; + type Future = FutureResult; + + fn new_service(&self) -> Self::Future { + ok(FramedHandleService { + hnd: self.hnd.clone(), + _t: PhantomData, + }) + } +} + +#[doc(hidden)] +pub struct FramedHandleService +where + F: FramedFactory, + R: IntoFuture, + E: Into, +{ + hnd: F, + _t: PhantomData<(S, Io, Ex, T, R, E)>, +} + +impl Service for FramedHandleService +where + F: FramedFactory, + R: IntoFuture, + E: Into, +{ + type Request = (T, FramedRequest); + type Response = (); + type Error = FramedError; + type Future = FramedHandleServiceResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, (param, framed): (T, FramedRequest)) -> Self::Future { + let (_, framed, ex) = framed.into_parts(); + FramedHandleServiceResponse { + fut: self.hnd.call(framed, param, ex).into_future(), + _t: PhantomData, + } + } +} + +#[doc(hidden)] +pub struct FramedHandleServiceResponse { + fut: F, + _t: PhantomData, +} + +impl Future for FramedHandleServiceResponse +where + F: Future, + F::Error: Into, +{ + type Item = (); + type Error = FramedError; + + fn poll(&mut self) -> Poll { + match self.fut.poll() { + Ok(Async::NotReady) => Ok(Async::NotReady), + Ok(Async::Ready(res)) => Ok(Async::Ready(res.into())), + Err(e) => { + let e: Error = e.into(); + error!("Error in handler: {:?}", e); + Ok(Async::Ready(())) + } + } + } +} + +pub struct FramedExtract +where + T: FromRequest, +{ + cfg: Rc, + _t: PhantomData<(Io, Ex)>, +} + +impl FramedExtract +where + T: FromRequest + 'static, +{ + pub fn new(cfg: T::Config) -> FramedExtract { + FramedExtract { + cfg: Rc::new(cfg), + _t: PhantomData, + } + } +} +impl NewService for FramedExtract +where + T: FromRequest + 'static, +{ + type Request = FramedRequest; + type Response = (T, FramedRequest); + type Error = FramedError; + type InitError = (); + type Service = FramedExtractService; + type Future = FutureResult; + + fn new_service(&self) -> Self::Future { + ok(FramedExtractService { + cfg: self.cfg.clone(), + _t: PhantomData, + }) + } +} + +pub struct FramedExtractService +where + T: FromRequest, +{ + cfg: Rc, + _t: PhantomData<(Io, Ex)>, +} + +impl Service for FramedExtractService +where + T: FromRequest + 'static, +{ + type Request = FramedRequest; + type Response = (T, FramedRequest); + type Error = FramedError; + type Future = FramedExtractResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, req: FramedRequest) -> Self::Future { + FramedExtractResponse { + fut: T::from_request(&req.request(), self.cfg.as_ref()), + req: Some(req), + } + } +} + +pub struct FramedExtractResponse +where + T: FromRequest + 'static, +{ + req: Option>, + fut: T::Future, +} + +impl Future for FramedExtractResponse +where + T: FromRequest + 'static, +{ + type Item = (T, FramedRequest); + type Error = FramedError; + + fn poll(&mut self) -> Poll { + match self.fut.poll() { + Ok(Async::NotReady) => Ok(Async::NotReady), + Ok(Async::Ready(item)) => Ok(Async::Ready((item, self.req.take().unwrap()))), + Err(err) => Err(FramedError { + err: err.into(), + framed: self.req.take().unwrap().into_parts().1, + }), + } + } +} + +macro_rules! factory_tuple ({ ($(($nex:tt, $Ex:ident)),+), $(($n:tt, $T:ident)),+} => { + impl FramedFactory for Func + where Func: Fn(Framed, $($Ex,)+ $($T,)+) -> Res + Clone + 'static, + $($T: FromRequest + 'static,)+ + Res: IntoFuture + 'static, + Err: Into, + { + fn call(&self, framed: Framed, param: ($($T,)+), extra: ($($Ex,)+)) -> Res { + (self)(framed, $(extra.$nex,)+ $(param.$n,)+) + } + } +}); + +macro_rules! factory_tuple_unit ({$(($n:tt, $T:ident)),+} => { + impl FramedFactory for Func + where Func: Fn(Framed, $($T,)+) -> Res + Clone + 'static, + $($T: FromRequest + 'static,)+ + Res: IntoFuture + 'static, + Err: Into, + { + fn call(&self, framed: Framed, param: ($($T,)+), _extra: () ) -> Res { + (self)(framed, $(param.$n,)+) + } + } +}); + +#[cfg_attr(rustfmt, rustfmt_skip)] +mod m { + use super::*; + +factory_tuple_unit!((0, A)); +factory_tuple!(((0, Aex)), (0, A)); +factory_tuple!(((0, Aex), (1, Bex)), (0, A)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A)); + +factory_tuple_unit!((0, A), (1, B)); +factory_tuple!(((0, Aex)), (0, A), (1, B)); +factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B)); + +factory_tuple_unit!((0, A), (1, B), (2, C)); +factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C)); +factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C)); + +factory_tuple_unit!((0, A), (1, B), (2, C), (3, D)); +factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D)); +factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D)); + +factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E)); +factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E)); +factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E)); + +factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); +factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); +factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); + +factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); +factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); +factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); + +factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); +factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); +factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); + +factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); +factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); +factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); + +factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); +factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); +factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); +factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); +} diff --git a/src/framed_route.rs b/src/framed_route.rs new file mode 100644 index 00000000..90555a9c --- /dev/null +++ b/src/framed_route.rs @@ -0,0 +1,448 @@ +use std::marker::PhantomData; + +use actix_http::http::{HeaderName, HeaderValue, Method}; +use actix_http::Error; +use actix_service::{IntoNewService, NewService, NewServiceExt, Service}; +use futures::{try_ready, Async, Future, IntoFuture, Poll}; +use log::{debug, error}; +use tokio_io::{AsyncRead, AsyncWrite}; + +use crate::app::{HttpServiceFactory, State}; +use crate::framed_handler::{ + FramedError, FramedExtract, FramedFactory, FramedHandle, FramedRequest, +}; +use crate::handler::FromRequest; + +/// 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 { + service: T, + pattern: String, + methods: Vec, + headers: Vec<(HeaderName, HeaderValue)>, + state: PhantomData<(S, Io)>, +} + +impl FramedRoute { + pub fn build(path: &str) -> FramedRoutePatternBuilder { + FramedRoutePatternBuilder::new(path) + } + + pub fn get(path: &str) -> FramedRoutePatternBuilder { + FramedRoutePatternBuilder::new(path).method(Method::GET) + } + + pub fn post(path: &str) -> FramedRoutePatternBuilder { + FramedRoutePatternBuilder::new(path).method(Method::POST) + } + + pub fn put(path: &str) -> FramedRoutePatternBuilder { + FramedRoutePatternBuilder::new(path).method(Method::PUT) + } + + pub fn delete(path: &str) -> FramedRoutePatternBuilder { + FramedRoutePatternBuilder::new(path).method(Method::DELETE) + } +} + +impl FramedRoute +where + T: NewService< + Request = FramedRequest, + Response = (), + Error = FramedError, + > + 'static, +{ + pub fn new>(pattern: &str, factory: F) -> Self { + FramedRoute { + pattern: pattern.to_string(), + service: factory.into_new_service(), + headers: Vec::new(), + methods: Vec::new(), + state: PhantomData, + } + } + + pub fn method(mut self, method: Method) -> Self { + self.methods.push(method); + self + } + + pub fn header(mut self, name: HeaderName, value: HeaderValue) -> Self { + self.headers.push((name, value)); + self + } +} + +impl HttpServiceFactory for FramedRoute +where + Io: AsyncRead + AsyncWrite + 'static, + T: NewService< + Request = FramedRequest, + Response = (), + Error = FramedError, + > + 'static, + T::Service: 'static, +{ + type Factory = FramedRouteFactory; + + fn path(&self) -> &str { + &self.pattern + } + + fn create(self, state: State) -> Self::Factory { + FramedRouteFactory { + state, + service: self.service, + pattern: self.pattern, + methods: self.methods, + headers: self.headers, + _t: PhantomData, + } + } +} + +pub struct FramedRouteFactory { + service: T, + pattern: String, + methods: Vec, + headers: Vec<(HeaderName, HeaderValue)>, + state: State, + _t: PhantomData, +} + +impl NewService for FramedRouteFactory +where + Io: AsyncRead + AsyncWrite + 'static, + T: NewService< + Request = FramedRequest, + Response = (), + Error = FramedError, + > + 'static, + T::Service: 'static, +{ + type Request = FramedRequest; + type Response = T::Response; + type Error = (); + type InitError = T::InitError; + type Service = FramedRouteService; + type Future = CreateRouteService; + + fn new_service(&self) -> Self::Future { + CreateRouteService { + fut: self.service.new_service(), + pattern: self.pattern.clone(), + methods: self.methods.clone(), + headers: self.headers.clone(), + state: self.state.clone(), + _t: PhantomData, + } + } +} + +pub struct CreateRouteService { + fut: T::Future, + pattern: String, + methods: Vec, + headers: Vec<(HeaderName, HeaderValue)>, + state: State, + _t: PhantomData, +} + +impl Future for CreateRouteService +where + T: NewService< + Request = FramedRequest, + Response = (), + Error = FramedError, + >, +{ + type Item = FramedRouteService; + type Error = T::InitError; + + fn poll(&mut self) -> Poll { + let service = try_ready!(self.fut.poll()); + + Ok(Async::Ready(FramedRouteService { + service, + state: self.state.clone(), + pattern: self.pattern.clone(), + methods: self.methods.clone(), + headers: self.headers.clone(), + _t: PhantomData, + })) + } +} + +pub struct FramedRouteService { + service: T, + pattern: String, + methods: Vec, + headers: Vec<(HeaderName, HeaderValue)>, + state: State, + _t: PhantomData, +} + +impl Service for FramedRouteService +where + Io: AsyncRead + AsyncWrite + 'static, + T: Service, Response = (), Error = FramedError> + + 'static, +{ + type Request = FramedRequest; + type Response = (); + type Error = (); + type Future = FramedRouteServiceResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.service.poll_ready().map_err(|e| { + debug!("Service not available: {}", e.err); + () + }) + } + + fn call(&mut self, req: FramedRequest) -> Self::Future { + FramedRouteServiceResponse { + fut: self.service.call(req), + send: None, + _t: PhantomData, + } + } +} + +// impl HttpService<(Request, Framed)> for FramedRouteService +// where +// Io: AsyncRead + AsyncWrite + 'static, +// S: 'static, +// T: Service, Response = (), Error = FramedError> + 'static, +// { +// fn handle( +// &mut self, +// (req, framed): (Request, Framed), +// ) -> Result)> { +// if self.methods.is_empty() +// || !self.methods.is_empty() && self.methods.contains(req.method()) +// { +// if let Some(params) = self.pattern.match_with_params(&req, 0) { +// return Ok(FramedRouteServiceResponse { +// fut: self.service.call(FramedRequest::new( +// WebRequest::new(self.state.clone(), req, params), +// framed, +// )), +// send: None, +// _t: PhantomData, +// }); +// } +// } +// Err((req, framed)) +// } +// } + +#[doc(hidden)] +pub struct FramedRouteServiceResponse { + fut: F, + send: Option>>, + _t: PhantomData, +} + +impl Future for FramedRouteServiceResponse +where + F: Future>, + Io: AsyncRead + AsyncWrite + 'static, +{ + type Item = (); + type Error = (); + + fn poll(&mut self) -> Poll { + if let Some(ref mut fut) = self.send { + return match fut.poll() { + Ok(Async::NotReady) => Ok(Async::NotReady), + Ok(Async::Ready(_)) => Ok(Async::Ready(())), + Err(e) => { + debug!("Error during error response send: {}", e); + Err(()) + } + }; + }; + + match self.fut.poll() { + Ok(Async::NotReady) => Ok(Async::NotReady), + Ok(Async::Ready(_)) => Ok(Async::Ready(())), + Err(e) => { + error!("Error occurred during request handling: {}", e.err); + Err(()) + } + } + } +} + +pub struct FramedRoutePatternBuilder { + pattern: String, + methods: Vec, + headers: Vec<(HeaderName, HeaderValue)>, + state: PhantomData<(Io, S)>, +} + +impl FramedRoutePatternBuilder { + fn new(path: &str) -> FramedRoutePatternBuilder { + FramedRoutePatternBuilder { + pattern: path.to_string(), + methods: Vec::new(), + headers: Vec::new(), + state: PhantomData, + } + } + + pub fn method(mut self, method: Method) -> Self { + self.methods.push(method); + self + } + + pub fn map>( + self, + md: F, + ) -> FramedRouteBuilder + where + T: NewService< + Request = FramedRequest, + Response = FramedRequest, + Error = FramedError, + InitError = (), + >, + { + FramedRouteBuilder { + service: md.into_new_service(), + pattern: self.pattern, + methods: self.methods, + headers: self.headers, + state: PhantomData, + } + } + + pub fn with( + self, + handler: F, + ) -> FramedRoute< + Io, + impl NewService< + Request = FramedRequest, + Response = (), + Error = FramedError, + InitError = (), + >, + S, + > + where + F: FramedFactory, + P: FromRequest + 'static, + R: IntoFuture, + E: Into, + { + FramedRoute { + service: FramedExtract::new(P::Config::default()) + .and_then(FramedHandle::new(handler)), + pattern: self.pattern, + methods: self.methods, + headers: self.headers, + state: PhantomData, + } + } +} + +pub struct FramedRouteBuilder { + service: T, + pattern: String, + methods: Vec, + headers: Vec<(HeaderName, HeaderValue)>, + state: PhantomData<(Io, S, U1, U2)>, +} + +impl FramedRouteBuilder +where + T: NewService< + Request = FramedRequest, + Response = FramedRequest, + Error = FramedError, + InitError = (), + >, +{ + pub fn new>(path: &str, factory: F) -> Self { + FramedRouteBuilder { + service: factory.into_new_service(), + pattern: path.to_string(), + methods: Vec::new(), + headers: Vec::new(), + state: PhantomData, + } + } + + pub fn method(mut self, method: Method) -> Self { + self.methods.push(method); + self + } + + pub fn map>( + self, + md: F, + ) -> FramedRouteBuilder< + Io, + S, + impl NewService< + Request = FramedRequest, + Response = FramedRequest, + Error = FramedError, + InitError = (), + >, + U1, + U3, + > + where + K: NewService< + Request = FramedRequest, + Response = FramedRequest, + Error = FramedError, + InitError = (), + >, + { + FramedRouteBuilder { + service: self.service.from_err().and_then(md.into_new_service()), + pattern: self.pattern, + methods: self.methods, + headers: self.headers, + state: PhantomData, + } + } + + pub fn with( + self, + handler: F, + ) -> FramedRoute< + Io, + impl NewService< + Request = FramedRequest, + Response = (), + Error = FramedError, + InitError = (), + >, + S, + > + where + F: FramedFactory, + P: FromRequest + 'static, + R: IntoFuture, + E: Into, + { + FramedRoute { + service: self + .service + .and_then(FramedExtract::new(P::Config::default())) + .and_then(FramedHandle::new(handler)), + pattern: self.pattern, + methods: self.methods, + headers: self.headers, + state: PhantomData, + } + } +} diff --git a/src/fs.rs b/src/fs.rs index 604ac550..3c83af6e 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1,34 +1,41 @@ //! Static files support +use std::cell::RefCell; use std::fmt::Write; use std::fs::{DirEntry, File, Metadata}; use std::io::{Read, Seek}; use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; +use std::rc::Rc; use std::time::{SystemTime, UNIX_EPOCH}; use std::{cmp, io}; #[cfg(unix)] use std::os::unix::fs::MetadataExt; -use v_htmlescape::escape as escape_html_entity; use bytes::Bytes; +use derive_more::Display; use futures::{Async, Future, Poll, Stream}; -use futures_cpupool::{CpuFuture, CpuPool}; use mime; use mime_guess::{get_mime_type, guess_mime_type}; use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; +use v_htmlescape::escape as escape_html_entity; -use error::{Error, StaticFileError}; -use handler::{AsyncResult, Handler, Responder, RouteHandler, WrapHandler}; -use header; -use header::{ContentDisposition, DispositionParam, DispositionType}; -use http::{ContentEncoding, Method, StatusCode}; -use httpmessage::HttpMessage; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use param::FromParam; -use server::settings::DEFAULT_CPUPOOL; +use actix_http::error::{Error, ErrorInternalServerError, ResponseError}; +use actix_http::http::header::{ + self, ContentDisposition, DispositionParam, DispositionType, +}; +use actix_http::http::{ContentEncoding, Method, StatusCode}; +use actix_http::{HttpMessage, Response}; +use actix_service::{NewService, Service}; +use futures::future::{err, ok, FutureResult}; + +use crate::blocking; +use crate::handler::FromRequest; +use crate::helpers::HttpDefaultNewService; +use crate::request::HttpRequest; +use crate::responder::Responder; +use crate::service::{ServiceRequest, ServiceResponse}; ///Describes `StaticFiles` configiration /// @@ -39,7 +46,7 @@ use server::settings::DEFAULT_CPUPOOL; /// ///## Example /// -///```rust +///```rust,ignore /// extern crate mime; /// extern crate actix_web; /// use actix_web::http::header::DispositionType; @@ -113,7 +120,6 @@ pub struct NamedFile { content_disposition: header::ContentDisposition, md: Metadata, modified: Option, - cpu_pool: Option, encoding: Option, status_code: StatusCode, _cd_map: PhantomData, @@ -127,7 +133,7 @@ impl NamedFile { /// /// # Examples /// - /// ```no_run + /// ```rust,ignore /// extern crate actix_web; /// /// use actix_web::fs::NamedFile; @@ -150,7 +156,7 @@ impl NamedFile { /// /// # Examples /// - /// ```rust + /// ```rust,ignore /// use actix_web::fs::NamedFile; /// /// let file = NamedFile::open("foo.txt"); @@ -168,7 +174,7 @@ impl NamedFile { /// /// # Examples /// - /// ```no_run + /// ```rust,ignore /// extern crate actix_web; /// /// use actix_web::fs::{DefaultConfig, NamedFile}; @@ -183,7 +189,11 @@ impl NamedFile { /// Ok(()) /// } /// ``` - pub fn from_file_with_config>(file: File, path: P, _: C) -> io::Result> { + pub fn from_file_with_config>( + file: File, + path: P, + _: C, + ) -> io::Result> { let path = path.as_ref().to_path_buf(); // Get the name of the file and use it to construct default Content-Type @@ -195,7 +205,7 @@ impl NamedFile { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Provided path has no filename", - )) + )); } }; @@ -210,7 +220,6 @@ impl NamedFile { let md = file.metadata()?; let modified = md.modified().ok(); - let cpu_pool = None; let encoding = None; Ok(NamedFile { path, @@ -219,7 +228,6 @@ impl NamedFile { content_disposition, md, modified, - cpu_pool, encoding, status_code: StatusCode::OK, _cd_map: PhantomData, @@ -230,12 +238,15 @@ impl NamedFile { /// /// # Examples /// - /// ```rust + /// ```rust,ignore /// use actix_web::fs::{DefaultConfig, NamedFile}; /// /// let file = NamedFile::open_with_config("foo.txt", DefaultConfig); /// ``` - pub fn open_with_config>(path: P, config: C) -> io::Result> { + pub fn open_with_config>( + path: P, + config: C, + ) -> io::Result> { Self::from_file_with_config(File::open(&path)?, path, config) } @@ -249,7 +260,7 @@ impl NamedFile { /// /// # Examples /// - /// ```rust + /// ```rust,ignore /// # use std::io; /// use actix_web::fs::NamedFile; /// @@ -264,13 +275,6 @@ impl NamedFile { self.path.as_path() } - /// Set `CpuPool` to use - #[inline] - pub fn set_cpu_pool(mut self, cpu_pool: CpuPool) -> Self { - self.cpu_pool = Some(cpu_pool); - self - } - /// Set response **Status Code** pub fn set_status_code(mut self, status: StatusCode) -> Self { self.status_code = status; @@ -352,7 +356,7 @@ impl DerefMut for NamedFile { } /// Returns true if `req` has no `If-Match` header or one which matches `etag`. -fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { +fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { match req.get_header::() { None | Some(header::IfMatch::Any) => true, Some(header::IfMatch::Items(ref items)) => { @@ -369,7 +373,7 @@ fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool } /// Returns true if `req` doesn't have an `If-None-Match` header matching `req`. -fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { +fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { match req.get_header::() { Some(header::IfNoneMatch::Any) => false, Some(header::IfNoneMatch::Items(ref items)) => { @@ -387,34 +391,33 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool } impl Responder for NamedFile { - type Item = HttpResponse; - type Error = io::Error; + type Error = Error; + type Future = FutureResult; - fn respond_to(self, req: &HttpRequest) -> Result { + fn respond_to(self, req: &HttpRequest) -> Self::Future { if self.status_code != StatusCode::OK { - let mut resp = HttpResponse::build(self.status_code); + let mut resp = Response::build(self.status_code); resp.set(header::ContentType(self.content_type.clone())) .header( header::CONTENT_DISPOSITION, self.content_disposition.to_string(), ); - - if let Some(current_encoding) = self.encoding { - resp.content_encoding(current_encoding); - } + // TODO blocking by compressing + // if let Some(current_encoding) = self.encoding { + // resp.content_encoding(current_encoding); + // } let reader = ChunkedReadFile { size: self.md.len(), offset: 0, - cpu_pool: self.cpu_pool.unwrap_or_else(|| req.cpu_pool().clone()), file: Some(self.file), fut: None, counter: 0, }; - return Ok(resp.streaming(reader)); + return ok(resp.streaming(reader)); } if !C::is_method_allowed(req.method()) { - return Ok(HttpResponse::MethodNotAllowed() + return ok(Response::MethodNotAllowed() .header(header::CONTENT_TYPE, "text/plain") .header(header::ALLOW, "GET, HEAD") .body("This resource only supports GET and HEAD.")); @@ -451,20 +454,21 @@ impl Responder for NamedFile { false }; - let mut resp = HttpResponse::build(self.status_code); + let mut resp = Response::build(self.status_code); resp.set(header::ContentType(self.content_type.clone())) .header( header::CONTENT_DISPOSITION, self.content_disposition.to_string(), ); - - if let Some(current_encoding) = self.encoding { - resp.content_encoding(current_encoding); - } + // TODO blocking by compressing + // if let Some(current_encoding) = self.encoding { + // resp.content_encoding(current_encoding); + // } resp.if_some(last_modified, |lm, resp| { resp.set(header::LastModified(lm)); - }).if_some(etag, |etag, resp| { + }) + .if_some(etag, |etag, resp| { resp.set(header::ETag(etag)); }); @@ -479,7 +483,8 @@ impl Responder for NamedFile { if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) { length = rangesvec[0].length; offset = rangesvec[0].start; - resp.content_encoding(ContentEncoding::Identity); + // TODO blocking by compressing + // resp.content_encoding(ContentEncoding::Identity); resp.header( header::CONTENT_RANGE, format!( @@ -491,59 +496,66 @@ impl Responder for NamedFile { ); } else { resp.header(header::CONTENT_RANGE, format!("bytes */{}", length)); - return Ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish()); + return ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish()); }; } else { - return Ok(resp.status(StatusCode::BAD_REQUEST).finish()); + return ok(resp.status(StatusCode::BAD_REQUEST).finish()); }; }; resp.header(header::CONTENT_LENGTH, format!("{}", length)); if precondition_failed { - return Ok(resp.status(StatusCode::PRECONDITION_FAILED).finish()); + return ok(resp.status(StatusCode::PRECONDITION_FAILED).finish()); } else if not_modified { - return Ok(resp.status(StatusCode::NOT_MODIFIED).finish()); + return ok(resp.status(StatusCode::NOT_MODIFIED).finish()); } if *req.method() == Method::HEAD { - Ok(resp.finish()) + ok(resp.finish()) } else { let reader = ChunkedReadFile { offset, size: length, - cpu_pool: self.cpu_pool.unwrap_or_else(|| req.cpu_pool().clone()), file: Some(self.file), fut: None, counter: 0, }; if offset != 0 || length != self.md.len() { - return Ok(resp.status(StatusCode::PARTIAL_CONTENT).streaming(reader)); + return ok(resp.status(StatusCode::PARTIAL_CONTENT).streaming(reader)); }; - Ok(resp.streaming(reader)) + ok(resp.streaming(reader)) } } } #[doc(hidden)] /// A helper created from a `std::fs::File` which reads the file -/// chunk-by-chunk on a `CpuPool`. +/// chunk-by-chunk on a `ThreadPool`. pub struct ChunkedReadFile { size: u64, offset: u64, - cpu_pool: CpuPool, file: Option, - fut: Option>, + fut: Option>, counter: u64, } +fn handle_error(err: blocking::BlockingError) -> Error { + match err { + blocking::BlockingError::Error(err) => err.into(), + blocking::BlockingError::Canceled => { + ErrorInternalServerError("Unexpected error").into() + } + } +} + impl Stream for ChunkedReadFile { type Item = Bytes; type Error = Error; fn poll(&mut self) -> Poll, Error> { if self.fut.is_some() { - return match self.fut.as_mut().unwrap().poll()? { + return match self.fut.as_mut().unwrap().poll().map_err(handle_error)? { Async::Ready((file, bytes)) => { self.fut.take(); self.file = Some(file); @@ -563,7 +575,7 @@ impl Stream for ChunkedReadFile { Ok(Async::Ready(None)) } else { let mut file = self.file.take().expect("Use after completion"); - self.fut = Some(self.cpu_pool.spawn_fn(move || { + self.fut = Some(blocking::run(move || { let max_bytes: usize; max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; let mut buf = Vec::with_capacity(max_bytes); @@ -580,8 +592,8 @@ impl Stream for ChunkedReadFile { } } -type DirectoryRenderer = - Fn(&Directory, &HttpRequest) -> Result; +type DirectoryRenderer = + Fn(&Directory, &HttpRequest) -> Result; /// A directory; responds with the generated directory listing. #[derive(Debug)] @@ -629,10 +641,10 @@ macro_rules! encode_file_name { }; } -fn directory_listing( +fn directory_listing( dir: &Directory, - req: &HttpRequest, -) -> Result { + req: &HttpRequest, +) -> Result { let index_of = format!("Index of {}", req.path()); let mut body = String::new(); let base = Path::new(req.path()); @@ -677,9 +689,12 @@ fn directory_listing( \n", index_of, index_of, body ); - Ok(HttpResponse::Ok() - .content_type("text/html; charset=utf-8") - .body(html)) + Ok(ServiceResponse::new( + req.clone(), + Response::Ok() + .content_type("text/html; charset=utf-8") + .body(html), + )) } /// Static files handling @@ -687,7 +702,7 @@ fn directory_listing( /// `StaticFile` handler must be registered with `App::handler()` method, /// because `StaticFile` handler requires access sub-path information. /// -/// ```rust +/// ```rust,ignore /// # extern crate actix_web; /// use actix_web::{fs, App}; /// @@ -701,9 +716,10 @@ pub struct StaticFiles { directory: PathBuf, index: Option, show_index: bool, - cpu_pool: CpuPool, - default: Box>, - renderer: Box>, + default: Rc< + RefCell, ServiceResponse>>>>, + >, + renderer: Rc, _chunk_size: usize, _follow_symlinks: bool, _cd_map: PhantomData, @@ -712,21 +728,12 @@ pub struct StaticFiles { impl StaticFiles { /// Create new `StaticFiles` instance for specified base directory. /// - /// `StaticFile` uses `CpuPool` for blocking filesystem operations. - /// By default pool with 20 threads is used. + /// `StaticFile` 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>(dir: T) -> Result, Error> { Self::with_config(dir, DefaultConfig) } - - /// Create new `StaticFiles` instance for specified base directory and - /// `CpuPool`. - pub fn with_pool>( - dir: T, - pool: CpuPool, - ) -> Result, Error> { - Self::with_config_pool(dir, pool, DefaultConfig) - } } impl StaticFiles { @@ -735,36 +742,20 @@ impl StaticFiles { /// Identical with `new` but allows to specify configiration to use. pub fn with_config>( dir: T, - config: C, - ) -> Result, Error> { - // use default CpuPool - let pool = { DEFAULT_CPUPOOL.lock().clone() }; - - StaticFiles::with_config_pool(dir, pool, config) - } - - /// Create new `StaticFiles` instance for specified base directory with config and - /// `CpuPool`. - pub fn with_config_pool>( - dir: T, - pool: CpuPool, _: C, ) -> Result, Error> { let dir = dir.into().canonicalize()?; if !dir.is_dir() { - return Err(StaticFileError::IsNotDirectory.into()); + return Err(StaticFilesError::IsNotDirectory.into()); } Ok(StaticFiles { directory: dir, index: None, show_index: false, - cpu_pool: pool, - default: Box::new(WrapHandler::new(|_: &_| { - HttpResponse::new(StatusCode::NOT_FOUND) - })), - renderer: Box::new(directory_listing), + default: Rc::new(RefCell::new(None)), + renderer: Rc::new(directory_listing), _chunk_size: 0, _follow_symlinks: false, _cd_map: PhantomData, @@ -782,11 +773,11 @@ impl StaticFiles { /// Set custom directory renderer pub fn files_listing_renderer(mut self, f: F) -> Self where - for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) - -> Result - + 'static, + for<'r, 's> F: + Fn(&'r Directory, &'s HttpRequest) -> Result + + 'static, { - self.renderer = Box::new(f); + self.renderer = Rc::new(f); self } @@ -798,54 +789,174 @@ impl StaticFiles { self.index = Some(index.into()); self } +} - /// Sets default handler which is used when no matched file could be found. - pub fn default_handler>(mut self, handler: H) -> StaticFiles { - self.default = Box::new(WrapHandler::new(handler)); - self +impl NewService for StaticFiles { + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = Error; + type Service = StaticFilesService; + type InitError = Error; + type Future = FutureResult; + + fn new_service(&self, _: &()) -> Self::Future { + ok(StaticFilesService { + directory: self.directory.clone(), + index: self.index.clone(), + show_index: self.show_index, + default: self.default.clone(), + renderer: self.renderer.clone(), + _chunk_size: self._chunk_size, + _follow_symlinks: self._follow_symlinks, + _cd_map: self._cd_map, + }) + } +} + +pub struct StaticFilesService { + directory: PathBuf, + index: Option, + show_index: bool, + default: Rc< + RefCell, ServiceResponse>>>>, + >, + renderer: Rc, + _chunk_size: usize, + _follow_symlinks: bool, + _cd_map: PhantomData, +} + +impl Service for StaticFilesService { + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = Error; + type Future = FutureResult; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) } - fn try_handle( - &self, - req: &HttpRequest, - ) -> Result, Error> { - let tail: String = req.match_info().get_decoded("tail").unwrap_or_else(|| "".to_string()); - let relpath = PathBuf::from_param(tail.trim_left_matches('/'))?; - + fn call(&mut self, req: Self::Request) -> Self::Future { + let mut req = req; + let real_path = match PathBuf::from_request(&mut req).poll() { + Ok(Async::Ready(item)) => item, + Ok(Async::NotReady) => unreachable!(), + Err(e) => return err(Error::from(e)), + }; // full filepath - let path = self.directory.join(&relpath).canonicalize()?; + let path = match self.directory.join(&real_path).canonicalize() { + Ok(path) => path, + Err(e) => return err(Error::from(e)), + }; if path.is_dir() { if let Some(ref redir_index) = self.index { let path = path.join(redir_index); - NamedFile::open_with_config(path, C::default())? - .set_cpu_pool(self.cpu_pool.clone()) - .respond_to(&req)? - .respond_to(&req) + match NamedFile::open_with_config(path, C::default()) { + Ok(named_file) => match named_file.respond_to(&req).poll() { + Ok(Async::Ready(item)) => { + ok(ServiceResponse::new(req.clone(), item)) + } + Ok(Async::NotReady) => unreachable!(), + Err(e) => err(Error::from(e)), + }, + Err(e) => err(Error::from(e)), + } } else if self.show_index { let dir = Directory::new(self.directory.clone(), path); - Ok((*self.renderer)(&dir, &req)?.into()) + let x = (self.renderer)(&dir, &req); + match x { + Ok(resp) => ok(resp), + Err(e) => err(Error::from(e)), + } } else { - Err(StaticFileError::IsDirectory.into()) + err(StaticFilesError::IsDirectory.into()) } } else { - NamedFile::open_with_config(path, C::default())? - .set_cpu_pool(self.cpu_pool.clone()) - .respond_to(&req)? - .respond_to(&req) + match NamedFile::open_with_config(path, C::default()) { + Ok(named_file) => match named_file.respond_to(&req).poll() { + Ok(Async::Ready(item)) => { + ok(ServiceResponse::new(req.clone(), item)) + } + Ok(Async::NotReady) => unreachable!(), + Err(e) => err(Error::from(e)), + }, + Err(e) => err(Error::from(e)), + } } } } -impl Handler for StaticFiles { - type Result = Result, Error>; +impl

FromRequest

for PathBuf { + type Error = UriSegmentError; + type Future = FutureResult; - fn handle(&self, req: &HttpRequest) -> Self::Result { - self.try_handle(req).or_else(|e| { - debug!("StaticFiles: Failed to handle {}: {}", req.path(), e); - Ok(self.default.handle(req)) - }) + fn from_request(req: &mut ServiceRequest

) -> Self::Future { + let path_str = req.match_info().path(); + let mut buf = PathBuf::new(); + for segment in path_str.split('/') { + if segment == ".." { + buf.pop(); + } else if segment.starts_with('.') { + return err(UriSegmentError::BadStart('.')); + } else if segment.starts_with('*') { + return err(UriSegmentError::BadStart('*')); + } else if segment.ends_with(':') { + return err(UriSegmentError::BadEnd(':')); + } else if segment.ends_with('>') { + return err(UriSegmentError::BadEnd('>')); + } else if segment.ends_with('<') { + return err(UriSegmentError::BadEnd('<')); + } else if segment.is_empty() { + continue; + } else if cfg!(windows) && segment.contains('\\') { + return err(UriSegmentError::BadChar('\\')); + } else { + buf.push(segment) + } + } + + ok(buf) + } +} + +/// Errors which can occur when serving static files. +#[derive(Display, Debug, PartialEq)] +enum StaticFilesError { + /// Path is not a directory + #[display(fmt = "Path is not a directory. Unable to serve static files")] + IsNotDirectory, + + /// Cannot render directory + #[display(fmt = "Unable to render directory without index file")] + IsDirectory, +} + +/// Return `NotFound` for `StaticFilesError` +impl ResponseError for StaticFilesError { + fn error_response(&self) -> Response { + Response::new(StatusCode::NOT_FOUND) + } +} + +#[derive(Display, Debug, PartialEq)] +pub enum UriSegmentError { + /// The segment started with the wrapped invalid character. + #[display(fmt = "The segment started with the wrapped invalid character")] + BadStart(char), + /// The segment contained the wrapped invalid character. + #[display(fmt = "The segment contained the wrapped invalid character")] + BadChar(char), + /// The segment ended with the wrapped invalid character. + #[display(fmt = "The segment ended with the wrapped invalid character")] + BadEnd(char), +} + +/// Return `BadRequest` for `UriSegmentError` +impl ResponseError for UriSegmentError { + fn error_response(&self) -> Response { + Response::new(StatusCode::BAD_REQUEST) } } @@ -888,7 +999,7 @@ impl HttpRange { if start_str.is_empty() { // If no start is specified, end specifies the // range start relative to the end of the file. - let mut length: i64 = try!(end_str.parse().map_err(|_| ())); + let mut length: i64 = end_str.parse().map_err(|_| ())?; if length > size_sig { length = size_sig; @@ -931,7 +1042,8 @@ impl HttpRange { length: length as u64, })) } - }).collect::>()?; + }) + .collect::>()?; let ranges: Vec = all_ranges.into_iter().filter_map(|x| x).collect(); @@ -943,985 +1055,981 @@ impl HttpRange { } } -#[cfg(test)] -mod tests { - use std::fs; - use std::time::Duration; - use std::ops::Add; - - use super::*; - use application::App; - use body::{Binary, Body}; - use http::{header, Method, StatusCode}; - use test::{self, TestRequest}; - - #[test] - fn test_file_extension_to_mime() { - let m = file_extension_to_mime("jpg"); - assert_eq!(m, mime::IMAGE_JPEG); - - let m = file_extension_to_mime("invalid extension!!"); - assert_eq!(m, mime::APPLICATION_OCTET_STREAM); - - let m = file_extension_to_mime(""); - assert_eq!(m, mime::APPLICATION_OCTET_STREAM); - } - - #[test] - fn test_if_modified_since_without_if_none_match() { - let mut file = NamedFile::open("Cargo.toml") - .unwrap() - .set_cpu_pool(CpuPool::new(1)); - let since = header::HttpDate::from( - SystemTime::now().add(Duration::from_secs(60))); - - let req = TestRequest::default() - .header(header::IF_MODIFIED_SINCE, since) - .finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.status(), - StatusCode::NOT_MODIFIED - ); - } - - #[test] - fn test_if_modified_since_with_if_none_match() { - let mut file = NamedFile::open("Cargo.toml") - .unwrap() - .set_cpu_pool(CpuPool::new(1)); - let since = header::HttpDate::from( - SystemTime::now().add(Duration::from_secs(60))); - - let req = TestRequest::default() - .header(header::IF_NONE_MATCH, "miss_etag") - .header(header::IF_MODIFIED_SINCE, since) - .finish(); - let resp = file.respond_to(&req).unwrap(); - assert_ne!( - resp.status(), - StatusCode::NOT_MODIFIED - ); - } - - #[test] - fn test_named_file_text() { - assert!(NamedFile::open("test--").is_err()); - let mut file = NamedFile::open("Cargo.toml") - .unwrap() - .set_cpu_pool(CpuPool::new(1)); - { - file.file(); - let _f: &File = &file; - } - { - let _f: &mut File = &mut file; - } - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "text/x-toml" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "inline; filename=\"Cargo.toml\"" - ); - } - - #[test] - fn test_named_file_set_content_type() { - let mut file = NamedFile::open("Cargo.toml") - .unwrap() - .set_content_type(mime::TEXT_XML) - .set_cpu_pool(CpuPool::new(1)); - { - file.file(); - let _f: &File = &file; - } - { - let _f: &mut File = &mut file; - } - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "text/xml" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "inline; filename=\"Cargo.toml\"" - ); - } - - #[test] - fn test_named_file_image() { - let mut file = NamedFile::open("tests/test.png") - .unwrap() - .set_cpu_pool(CpuPool::new(1)); - { - file.file(); - let _f: &File = &file; - } - { - let _f: &mut File = &mut file; - } - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "image/png" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "inline; filename=\"test.png\"" - ); - } - - #[test] - fn test_named_file_image_attachment() { - use header::{ContentDisposition, DispositionParam, DispositionType}; - let cd = ContentDisposition { - disposition: DispositionType::Attachment, - parameters: vec![DispositionParam::Filename(String::from("test.png"))], - }; - let mut file = NamedFile::open("tests/test.png") - .unwrap() - .set_content_disposition(cd) - .set_cpu_pool(CpuPool::new(1)); - { - file.file(); - let _f: &File = &file; - } - { - let _f: &mut File = &mut file; - } - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "image/png" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "attachment; filename=\"test.png\"" - ); - } - - #[derive(Default)] - pub struct AllAttachmentConfig; - impl StaticFileConfig for AllAttachmentConfig { - fn content_disposition_map(_typ: mime::Name) -> DispositionType { - DispositionType::Attachment - } - } - - #[derive(Default)] - pub struct AllInlineConfig; - impl StaticFileConfig for AllInlineConfig { - fn content_disposition_map(_typ: mime::Name) -> DispositionType { - DispositionType::Inline - } - } - - #[test] - fn test_named_file_image_attachment_and_custom_config() { - let file = NamedFile::open_with_config("tests/test.png", AllAttachmentConfig) - .unwrap() - .set_cpu_pool(CpuPool::new(1)); - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "image/png" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "attachment; filename=\"test.png\"" - ); - - let file = NamedFile::open_with_config("tests/test.png", AllInlineConfig) - .unwrap() - .set_cpu_pool(CpuPool::new(1)); - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "image/png" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "inline; filename=\"test.png\"" - ); - } - - #[test] - fn test_named_file_binary() { - let mut file = NamedFile::open("tests/test.binary") - .unwrap() - .set_cpu_pool(CpuPool::new(1)); - { - file.file(); - let _f: &File = &file; - } - { - let _f: &mut File = &mut file; - } - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "application/octet-stream" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "attachment; filename=\"test.binary\"" - ); - } - - #[test] - fn test_named_file_status_code_text() { - let mut file = NamedFile::open("Cargo.toml") - .unwrap() - .set_status_code(StatusCode::NOT_FOUND) - .set_cpu_pool(CpuPool::new(1)); - { - file.file(); - let _f: &File = &file; - } - { - let _f: &mut File = &mut file; - } - - let req = TestRequest::default().finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "text/x-toml" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "inline; filename=\"Cargo.toml\"" - ); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_named_file_ranges_status_code() { - let mut srv = test::TestServer::with_factory(|| { - App::new().handler( - "test", - StaticFiles::new(".").unwrap().index_file("Cargo.toml"), - ) - }); - - // Valid range header - let request = srv - .get() - .uri(srv.url("/t%65st/Cargo.toml")) - .header(header::RANGE, "bytes=10-20") - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); - - // Invalid range header - let request = srv - .get() - .uri(srv.url("/t%65st/Cargo.toml")) - .header(header::RANGE, "bytes=1-0") - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - - assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); - } - - #[test] - fn test_named_file_content_range_headers() { - let mut srv = test::TestServer::with_factory(|| { - App::new().handler( - "test", - StaticFiles::new(".") - .unwrap() - .index_file("tests/test.binary"), - ) - }); - - // Valid range header - let request = srv - .get() - .uri(srv.url("/t%65st/tests/test.binary")) - .header(header::RANGE, "bytes=10-20") - .finish() - .unwrap(); - - let response = srv.execute(request.send()).unwrap(); - - let contentrange = response - .headers() - .get(header::CONTENT_RANGE) - .unwrap() - .to_str() - .unwrap(); - - assert_eq!(contentrange, "bytes 10-20/100"); - - // Invalid range header - let request = srv - .get() - .uri(srv.url("/t%65st/tests/test.binary")) - .header(header::RANGE, "bytes=10-5") - .finish() - .unwrap(); - - let response = srv.execute(request.send()).unwrap(); - - let contentrange = response - .headers() - .get(header::CONTENT_RANGE) - .unwrap() - .to_str() - .unwrap(); - - assert_eq!(contentrange, "bytes */100"); - } - - #[test] - fn test_named_file_content_length_headers() { - let mut srv = test::TestServer::with_factory(|| { - App::new().handler( - "test", - StaticFiles::new(".") - .unwrap() - .index_file("tests/test.binary"), - ) - }); - - // Valid range header - let request = srv - .get() - .uri(srv.url("/t%65st/tests/test.binary")) - .header(header::RANGE, "bytes=10-20") - .finish() - .unwrap(); - - let response = srv.execute(request.send()).unwrap(); - - let contentlength = response - .headers() - .get(header::CONTENT_LENGTH) - .unwrap() - .to_str() - .unwrap(); - - assert_eq!(contentlength, "11"); - - // Invalid range header - let request = srv - .get() - .uri(srv.url("/t%65st/tests/test.binary")) - .header(header::RANGE, "bytes=10-8") - .finish() - .unwrap(); - - let response = srv.execute(request.send()).unwrap(); - - let contentlength = response - .headers() - .get(header::CONTENT_LENGTH) - .unwrap() - .to_str() - .unwrap(); - - assert_eq!(contentlength, "0"); - - // Without range header - let request = srv - .get() - .uri(srv.url("/t%65st/tests/test.binary")) - .no_default_headers() - .finish() - .unwrap(); - - let response = srv.execute(request.send()).unwrap(); - - let contentlength = response - .headers() - .get(header::CONTENT_LENGTH) - .unwrap() - .to_str() - .unwrap(); - - assert_eq!(contentlength, "100"); - - // chunked - let request = srv - .get() - .uri(srv.url("/t%65st/tests/test.binary")) - .finish() - .unwrap(); - - let response = srv.execute(request.send()).unwrap(); - { - let te = response - .headers() - .get(header::TRANSFER_ENCODING) - .unwrap() - .to_str() - .unwrap(); - assert_eq!(te, "chunked"); - } - let bytes = srv.execute(response.body()).unwrap(); - let data = Bytes::from(fs::read("tests/test.binary").unwrap()); - assert_eq!(bytes, data); - } - - #[test] - fn test_static_files_with_spaces() { - let mut srv = test::TestServer::with_factory(|| { - App::new().handler( - "/", - StaticFiles::new(".").unwrap().index_file("Cargo.toml"), - ) - }); - let request = srv - .get() - .uri(srv.url("/tests/test%20space.binary")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - - let bytes = srv.execute(response.body()).unwrap(); - let data = Bytes::from(fs::read("tests/test space.binary").unwrap()); - assert_eq!(bytes, data); - } - - #[derive(Default)] - pub struct OnlyMethodHeadConfig; - impl StaticFileConfig for OnlyMethodHeadConfig { - fn is_method_allowed(method: &Method) -> bool { - match *method { - Method::HEAD => true, - _ => false, - } - } - } - - #[test] - fn test_named_file_not_allowed() { - let file = - NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); - let req = TestRequest::default().method(Method::POST).finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - - let file = - NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); - let req = TestRequest::default().method(Method::PUT).finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - - let file = - NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); - let req = TestRequest::default().method(Method::GET).finish(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - } - - #[test] - fn test_named_file_content_encoding() { - let req = TestRequest::default().method(Method::GET).finish(); - let file = NamedFile::open("Cargo.toml").unwrap(); - - assert!(file.encoding.is_none()); - let resp = file - .set_content_encoding(ContentEncoding::Identity) - .respond_to(&req) - .unwrap(); - - assert!(resp.content_encoding().is_some()); - assert_eq!(resp.content_encoding().unwrap().as_str(), "identity"); - } - - #[test] - fn test_named_file_any_method() { - let req = TestRequest::default().method(Method::POST).finish(); - let file = NamedFile::open("Cargo.toml").unwrap(); - let resp = file.respond_to(&req).unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - } - - #[test] - fn test_static_files() { - let mut st = StaticFiles::new(".").unwrap().show_files_listing(); - let req = TestRequest::with_uri("/missing") - .param("tail", "missing") - .finish(); - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - - st.show_index = false; - let req = TestRequest::default().finish(); - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - - let req = TestRequest::default().param("tail", "").finish(); - - st.show_index = true; - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "text/html; charset=utf-8" - ); - assert!(resp.body().is_binary()); - assert!(format!("{:?}", resp.body()).contains("README.md")); - } - - #[test] - fn test_static_files_bad_directory() { - let st: Result, Error> = StaticFiles::new("missing"); - assert!(st.is_err()); - - let st: Result, Error> = StaticFiles::new("Cargo.toml"); - assert!(st.is_err()); - } - - #[test] - fn test_default_handler_file_missing() { - let st = StaticFiles::new(".") - .unwrap() - .default_handler(|_: &_| "default content"); - let req = TestRequest::with_uri("/missing") - .param("tail", "missing") - .finish(); - - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.body(), - &Body::Binary(Binary::Slice(b"default content")) - ); - } - - #[test] - fn test_serve_index() { - let st = StaticFiles::new(".").unwrap().index_file("test.binary"); - let req = TestRequest::default().uri("/tests").finish(); - - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).expect("content type"), - "application/octet-stream" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).expect("content disposition"), - "attachment; filename=\"test.binary\"" - ); - - let req = TestRequest::default().uri("/tests/").finish(); - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "application/octet-stream" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "attachment; filename=\"test.binary\"" - ); - - // nonexistent index file - let req = TestRequest::default().uri("/tests/unknown").finish(); - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - - let req = TestRequest::default().uri("/tests/unknown/").finish(); - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_serve_index_nested() { - let st = StaticFiles::new(".").unwrap().index_file("mod.rs"); - let req = TestRequest::default().uri("/src/client").finish(); - let resp = st.handle(&req).respond_to(&req).unwrap(); - let resp = resp.as_msg(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "text/x-rust" - ); - assert_eq!( - resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - "inline; filename=\"mod.rs\"" - ); - } - - #[test] - fn integration_serve_index_with_prefix() { - let mut srv = test::TestServer::with_factory(|| { - App::new() - .prefix("public") - .handler("/", StaticFiles::new(".").unwrap().index_file("Cargo.toml")) - }); - - let request = srv.get().uri(srv.url("/public")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - let bytes = srv.execute(response.body()).unwrap(); - let data = Bytes::from(fs::read("Cargo.toml").unwrap()); - assert_eq!(bytes, data); - - let request = srv.get().uri(srv.url("/public/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - let bytes = srv.execute(response.body()).unwrap(); - let data = Bytes::from(fs::read("Cargo.toml").unwrap()); - assert_eq!(bytes, data); - } - - #[test] - fn integration_serve_index() { - let mut srv = test::TestServer::with_factory(|| { - App::new().handler( - "test", - StaticFiles::new(".").unwrap().index_file("Cargo.toml"), - ) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - let bytes = srv.execute(response.body()).unwrap(); - let data = Bytes::from(fs::read("Cargo.toml").unwrap()); - assert_eq!(bytes, data); - - let request = srv.get().uri(srv.url("/test/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - let bytes = srv.execute(response.body()).unwrap(); - let data = Bytes::from(fs::read("Cargo.toml").unwrap()); - assert_eq!(bytes, data); - - // nonexistent index file - let request = srv.get().uri(srv.url("/test/unknown")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::NOT_FOUND); - - let request = srv.get().uri(srv.url("/test/unknown/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::NOT_FOUND); - } - - #[test] - fn integration_percent_encoded() { - let mut srv = test::TestServer::with_factory(|| { - App::new().handler( - "test", - StaticFiles::new(".").unwrap().index_file("Cargo.toml"), - ) - }); - - let request = srv - .get() - .uri(srv.url("/test/%43argo.toml")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - } - - struct T(&'static str, u64, Vec); - - #[test] - fn test_parse() { - let tests = vec![ - T("", 0, vec![]), - T("", 1000, vec![]), - T("foo", 0, vec![]), - T("bytes=", 0, vec![]), - T("bytes=7", 10, vec![]), - T("bytes= 7 ", 10, vec![]), - T("bytes=1-", 0, vec![]), - T("bytes=5-4", 10, vec![]), - T("bytes=0-2,5-4", 10, vec![]), - T("bytes=2-5,4-3", 10, vec![]), - T("bytes=--5,4--3", 10, vec![]), - T("bytes=A-", 10, vec![]), - T("bytes=A- ", 10, vec![]), - T("bytes=A-Z", 10, vec![]), - T("bytes= -Z", 10, vec![]), - T("bytes=5-Z", 10, vec![]), - T("bytes=Ran-dom, garbage", 10, vec![]), - T("bytes=0x01-0x02", 10, vec![]), - T("bytes= ", 10, vec![]), - T("bytes= , , , ", 10, vec![]), - T( - "bytes=0-9", - 10, - vec![HttpRange { - start: 0, - length: 10, - }], - ), - T( - "bytes=0-", - 10, - vec![HttpRange { - start: 0, - length: 10, - }], - ), - T( - "bytes=5-", - 10, - vec![HttpRange { - start: 5, - length: 5, - }], - ), - T( - "bytes=0-20", - 10, - vec![HttpRange { - start: 0, - length: 10, - }], - ), - T( - "bytes=15-,0-5", - 10, - vec![HttpRange { - start: 0, - length: 6, - }], - ), - T( - "bytes=1-2,5-", - 10, - vec![ - HttpRange { - start: 1, - length: 2, - }, - HttpRange { - start: 5, - length: 5, - }, - ], - ), - T( - "bytes=-2 , 7-", - 11, - vec![ - HttpRange { - start: 9, - length: 2, - }, - HttpRange { - start: 7, - length: 4, - }, - ], - ), - T( - "bytes=0-0 ,2-2, 7-", - 11, - vec![ - HttpRange { - start: 0, - length: 1, - }, - HttpRange { - start: 2, - length: 1, - }, - HttpRange { - start: 7, - length: 4, - }, - ], - ), - T( - "bytes=-5", - 10, - vec![HttpRange { - start: 5, - length: 5, - }], - ), - T( - "bytes=-15", - 10, - vec![HttpRange { - start: 0, - length: 10, - }], - ), - T( - "bytes=0-499", - 10000, - vec![HttpRange { - start: 0, - length: 500, - }], - ), - T( - "bytes=500-999", - 10000, - vec![HttpRange { - start: 500, - length: 500, - }], - ), - T( - "bytes=-500", - 10000, - vec![HttpRange { - start: 9500, - length: 500, - }], - ), - T( - "bytes=9500-", - 10000, - vec![HttpRange { - start: 9500, - length: 500, - }], - ), - T( - "bytes=0-0,-1", - 10000, - vec![ - HttpRange { - start: 0, - length: 1, - }, - HttpRange { - start: 9999, - length: 1, - }, - ], - ), - T( - "bytes=500-600,601-999", - 10000, - vec![ - HttpRange { - start: 500, - length: 101, - }, - HttpRange { - start: 601, - length: 399, - }, - ], - ), - T( - "bytes=500-700,601-999", - 10000, - vec![ - HttpRange { - start: 500, - length: 201, - }, - HttpRange { - start: 601, - length: 399, - }, - ], - ), - // Match Apache laxity: - T( - "bytes= 1 -2 , 4- 5, 7 - 8 , ,,", - 11, - vec![ - HttpRange { - start: 1, - length: 2, - }, - HttpRange { - start: 4, - length: 2, - }, - HttpRange { - start: 7, - length: 2, - }, - ], - ), - ]; - - for t in tests { - let header = t.0; - let size = t.1; - let expected = t.2; - - let res = HttpRange::parse(header, size); - - if res.is_err() { - if expected.is_empty() { - continue; - } else { - assert!( - false, - "parse({}, {}) returned error {:?}", - header, - size, - res.unwrap_err() - ); - } - } - - let got = res.unwrap(); - - if got.len() != expected.len() { - assert!( - false, - "len(parseRange({}, {})) = {}, want {}", - header, - size, - got.len(), - expected.len() - ); - continue; - } - - for i in 0..expected.len() { - if got[i].start != expected[i].start { - assert!( - false, - "parseRange({}, {})[{}].start = {}, want {}", - header, size, i, got[i].start, expected[i].start - ) - } - if got[i].length != expected[i].length { - assert!( - false, - "parseRange({}, {})[{}].length = {}, want {}", - header, size, i, got[i].length, expected[i].length - ) - } - } - } - } -} +// #[cfg(test)] +// mod tests { +// use std::fs; +// use std::ops::Add; +// use std::time::Duration; + +// use super::*; +// use application::App; +// use body::{Binary, Body}; +// use http::{header, Method, StatusCode}; +// use test::{self, TestRequest}; + +// #[test] +// fn test_file_extension_to_mime() { +// let m = file_extension_to_mime("jpg"); +// assert_eq!(m, mime::IMAGE_JPEG); + +// let m = file_extension_to_mime("invalid extension!!"); +// assert_eq!(m, mime::APPLICATION_OCTET_STREAM); + +// let m = file_extension_to_mime(""); +// assert_eq!(m, mime::APPLICATION_OCTET_STREAM); +// } + +// #[test] +// fn test_if_modified_since_without_if_none_match() { +// let mut file = NamedFile::open("Cargo.toml") +// .unwrap() +// .set_cpu_pool(CpuPool::new(1)); +// let since = +// header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); + +// let req = TestRequest::default() +// .header(header::IF_MODIFIED_SINCE, since) +// .finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!(resp.status(), StatusCode::NOT_MODIFIED); +// } + +// #[test] +// fn test_if_modified_since_with_if_none_match() { +// let mut file = NamedFile::open("Cargo.toml") +// .unwrap() +// .set_cpu_pool(CpuPool::new(1)); +// let since = +// header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); + +// let req = TestRequest::default() +// .header(header::IF_NONE_MATCH, "miss_etag") +// .header(header::IF_MODIFIED_SINCE, since) +// .finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_ne!(resp.status(), StatusCode::NOT_MODIFIED); +// } + +// #[test] +// fn test_named_file_text() { +// assert!(NamedFile::open("test--").is_err()); +// let mut file = NamedFile::open("Cargo.toml") +// .unwrap() +// .set_cpu_pool(CpuPool::new(1)); +// { +// file.file(); +// let _f: &File = &file; +// } +// { +// let _f: &mut File = &mut file; +// } + +// let req = TestRequest::default().finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!( +// resp.headers().get(header::CONTENT_TYPE).unwrap(), +// "text/x-toml" +// ); +// assert_eq!( +// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), +// "inline; filename=\"Cargo.toml\"" +// ); +// } + +// #[test] +// fn test_named_file_set_content_type() { +// let mut file = NamedFile::open("Cargo.toml") +// .unwrap() +// .set_content_type(mime::TEXT_XML) +// .set_cpu_pool(CpuPool::new(1)); +// { +// file.file(); +// let _f: &File = &file; +// } +// { +// let _f: &mut File = &mut file; +// } + +// let req = TestRequest::default().finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!( +// resp.headers().get(header::CONTENT_TYPE).unwrap(), +// "text/xml" +// ); +// assert_eq!( +// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), +// "inline; filename=\"Cargo.toml\"" +// ); +// } + +// #[test] +// fn test_named_file_image() { +// let mut file = NamedFile::open("tests/test.png") +// .unwrap() +// .set_cpu_pool(CpuPool::new(1)); +// { +// file.file(); +// let _f: &File = &file; +// } +// { +// let _f: &mut File = &mut file; +// } + +// let req = TestRequest::default().finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!( +// resp.headers().get(header::CONTENT_TYPE).unwrap(), +// "image/png" +// ); +// assert_eq!( +// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), +// "inline; filename=\"test.png\"" +// ); +// } + +// #[test] +// fn test_named_file_image_attachment() { +// use header::{ContentDisposition, DispositionParam, DispositionType}; +// let cd = ContentDisposition { +// disposition: DispositionType::Attachment, +// parameters: vec![DispositionParam::Filename(String::from("test.png"))], +// }; +// let mut file = NamedFile::open("tests/test.png") +// .unwrap() +// .set_content_disposition(cd) +// .set_cpu_pool(CpuPool::new(1)); +// { +// file.file(); +// let _f: &File = &file; +// } +// { +// let _f: &mut File = &mut file; +// } + +// let req = TestRequest::default().finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!( +// resp.headers().get(header::CONTENT_TYPE).unwrap(), +// "image/png" +// ); +// assert_eq!( +// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), +// "attachment; filename=\"test.png\"" +// ); +// } + +// #[derive(Default)] +// pub struct AllAttachmentConfig; +// impl StaticFileConfig for AllAttachmentConfig { +// fn content_disposition_map(_typ: mime::Name) -> DispositionType { +// DispositionType::Attachment +// } +// } + +// #[derive(Default)] +// pub struct AllInlineConfig; +// impl StaticFileConfig for AllInlineConfig { +// fn content_disposition_map(_typ: mime::Name) -> DispositionType { +// DispositionType::Inline +// } +// } + +// #[test] +// fn test_named_file_image_attachment_and_custom_config() { +// let file = NamedFile::open_with_config("tests/test.png", AllAttachmentConfig) +// .unwrap() +// .set_cpu_pool(CpuPool::new(1)); + +// let req = TestRequest::default().finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!( +// resp.headers().get(header::CONTENT_TYPE).unwrap(), +// "image/png" +// ); +// assert_eq!( +// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), +// "attachment; filename=\"test.png\"" +// ); + +// let file = NamedFile::open_with_config("tests/test.png", AllInlineConfig) +// .unwrap() +// .set_cpu_pool(CpuPool::new(1)); + +// let req = TestRequest::default().finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!( +// resp.headers().get(header::CONTENT_TYPE).unwrap(), +// "image/png" +// ); +// assert_eq!( +// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), +// "inline; filename=\"test.png\"" +// ); +// } + +// #[test] +// fn test_named_file_binary() { +// let mut file = NamedFile::open("tests/test.binary") +// .unwrap() +// .set_cpu_pool(CpuPool::new(1)); +// { +// file.file(); +// let _f: &File = &file; +// } +// { +// let _f: &mut File = &mut file; +// } + +// let req = TestRequest::default().finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!( +// resp.headers().get(header::CONTENT_TYPE).unwrap(), +// "application/octet-stream" +// ); +// assert_eq!( +// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), +// "attachment; filename=\"test.binary\"" +// ); +// } + +// #[test] +// fn test_named_file_status_code_text() { +// let mut file = NamedFile::open("Cargo.toml") +// .unwrap() +// .set_status_code(StatusCode::NOT_FOUND) +// .set_cpu_pool(CpuPool::new(1)); +// { +// file.file(); +// let _f: &File = &file; +// } +// { +// let _f: &mut File = &mut file; +// } + +// let req = TestRequest::default().finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!( +// resp.headers().get(header::CONTENT_TYPE).unwrap(), +// "text/x-toml" +// ); +// assert_eq!( +// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), +// "inline; filename=\"Cargo.toml\"" +// ); +// assert_eq!(resp.status(), StatusCode::NOT_FOUND); +// } + +// #[test] +// fn test_named_file_ranges_status_code() { +// let mut srv = test::TestServer::with_factory(|| { +// App::new().handler( +// "test", +// StaticFiles::new(".").unwrap().index_file("Cargo.toml"), +// ) +// }); + +// // Valid range header +// let request = srv +// .get() +// .uri(srv.url("/t%65st/Cargo.toml")) +// .header(header::RANGE, "bytes=10-20") +// .finish() +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); + +// // Invalid range header +// let request = srv +// .get() +// .uri(srv.url("/t%65st/Cargo.toml")) +// .header(header::RANGE, "bytes=1-0") +// .finish() +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); + +// assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); +// } + +// #[test] +// fn test_named_file_content_range_headers() { +// let mut srv = test::TestServer::with_factory(|| { +// App::new().handler( +// "test", +// StaticFiles::new(".") +// .unwrap() +// .index_file("tests/test.binary"), +// ) +// }); + +// // Valid range header +// let request = srv +// .get() +// .uri(srv.url("/t%65st/tests/test.binary")) +// .header(header::RANGE, "bytes=10-20") +// .finish() +// .unwrap(); + +// let response = srv.execute(request.send()).unwrap(); + +// let contentrange = response +// .headers() +// .get(header::CONTENT_RANGE) +// .unwrap() +// .to_str() +// .unwrap(); + +// assert_eq!(contentrange, "bytes 10-20/100"); + +// // Invalid range header +// let request = srv +// .get() +// .uri(srv.url("/t%65st/tests/test.binary")) +// .header(header::RANGE, "bytes=10-5") +// .finish() +// .unwrap(); + +// let response = srv.execute(request.send()).unwrap(); + +// let contentrange = response +// .headers() +// .get(header::CONTENT_RANGE) +// .unwrap() +// .to_str() +// .unwrap(); + +// assert_eq!(contentrange, "bytes */100"); +// } + +// #[test] +// fn test_named_file_content_length_headers() { +// let mut srv = test::TestServer::with_factory(|| { +// App::new().handler( +// "test", +// StaticFiles::new(".") +// .unwrap() +// .index_file("tests/test.binary"), +// ) +// }); + +// // Valid range header +// let request = srv +// .get() +// .uri(srv.url("/t%65st/tests/test.binary")) +// .header(header::RANGE, "bytes=10-20") +// .finish() +// .unwrap(); + +// let response = srv.execute(request.send()).unwrap(); + +// let contentlength = response +// .headers() +// .get(header::CONTENT_LENGTH) +// .unwrap() +// .to_str() +// .unwrap(); + +// assert_eq!(contentlength, "11"); + +// // Invalid range header +// let request = srv +// .get() +// .uri(srv.url("/t%65st/tests/test.binary")) +// .header(header::RANGE, "bytes=10-8") +// .finish() +// .unwrap(); + +// let response = srv.execute(request.send()).unwrap(); + +// let contentlength = response +// .headers() +// .get(header::CONTENT_LENGTH) +// .unwrap() +// .to_str() +// .unwrap(); + +// assert_eq!(contentlength, "0"); + +// // Without range header +// let request = srv +// .get() +// .uri(srv.url("/t%65st/tests/test.binary")) +// .no_default_headers() +// .finish() +// .unwrap(); + +// let response = srv.execute(request.send()).unwrap(); + +// let contentlength = response +// .headers() +// .get(header::CONTENT_LENGTH) +// .unwrap() +// .to_str() +// .unwrap(); + +// assert_eq!(contentlength, "100"); + +// // chunked +// let request = srv +// .get() +// .uri(srv.url("/t%65st/tests/test.binary")) +// .finish() +// .unwrap(); + +// let response = srv.execute(request.send()).unwrap(); +// { +// let te = response +// .headers() +// .get(header::TRANSFER_ENCODING) +// .unwrap() +// .to_str() +// .unwrap(); +// assert_eq!(te, "chunked"); +// } +// let bytes = srv.execute(response.body()).unwrap(); +// let data = Bytes::from(fs::read("tests/test.binary").unwrap()); +// assert_eq!(bytes, data); +// } + +// #[test] +// fn test_static_files_with_spaces() { +// let mut srv = test::TestServer::with_factory(|| { +// App::new() +// .handler("/", StaticFiles::new(".").unwrap().index_file("Cargo.toml")) +// }); +// let request = srv +// .get() +// .uri(srv.url("/tests/test%20space.binary")) +// .finish() +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert_eq!(response.status(), StatusCode::OK); + +// let bytes = srv.execute(response.body()).unwrap(); +// let data = Bytes::from(fs::read("tests/test space.binary").unwrap()); +// assert_eq!(bytes, data); +// } + +// #[derive(Default)] +// pub struct OnlyMethodHeadConfig; +// impl StaticFileConfig for OnlyMethodHeadConfig { +// fn is_method_allowed(method: &Method) -> bool { +// match *method { +// Method::HEAD => true, +// _ => false, +// } +// } +// } + +// #[test] +// fn test_named_file_not_allowed() { +// let file = +// NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); +// let req = TestRequest::default().method(Method::POST).finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + +// let file = +// NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); +// let req = TestRequest::default().method(Method::PUT).finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + +// let file = +// NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); +// let req = TestRequest::default().method(Method::GET).finish(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); +// } + +// #[test] +// fn test_named_file_content_encoding() { +// let req = TestRequest::default().method(Method::GET).finish(); +// let file = NamedFile::open("Cargo.toml").unwrap(); + +// assert!(file.encoding.is_none()); +// let resp = file +// .set_content_encoding(ContentEncoding::Identity) +// .respond_to(&req) +// .unwrap(); + +// assert!(resp.content_encoding().is_some()); +// assert_eq!(resp.content_encoding().unwrap().as_str(), "identity"); +// } + +// #[test] +// fn test_named_file_any_method() { +// let req = TestRequest::default().method(Method::POST).finish(); +// let file = NamedFile::open("Cargo.toml").unwrap(); +// let resp = file.respond_to(&req).unwrap(); +// assert_eq!(resp.status(), StatusCode::OK); +// } + +// #[test] +// fn test_static_files() { +// let mut st = StaticFiles::new(".").unwrap().show_files_listing(); +// let req = TestRequest::with_uri("/missing") +// .param("tail", "missing") +// .finish(); +// let resp = st.handle(&req).respond_to(&req).unwrap(); +// let resp = resp.as_msg(); +// assert_eq!(resp.status(), StatusCode::NOT_FOUND); + +// st.show_index = false; +// let req = TestRequest::default().finish(); +// let resp = st.handle(&req).respond_to(&req).unwrap(); +// let resp = resp.as_msg(); +// assert_eq!(resp.status(), StatusCode::NOT_FOUND); + +// let req = TestRequest::default().param("tail", "").finish(); + +// st.show_index = true; +// let resp = st.handle(&req).respond_to(&req).unwrap(); +// let resp = resp.as_msg(); +// assert_eq!( +// resp.headers().get(header::CONTENT_TYPE).unwrap(), +// "text/html; charset=utf-8" +// ); +// assert!(resp.body().is_binary()); +// assert!(format!("{:?}", resp.body()).contains("README.md")); +// } + +// #[test] +// fn test_static_files_bad_directory() { +// let st: Result, Error> = StaticFiles::new("missing"); +// assert!(st.is_err()); + +// let st: Result, Error> = StaticFiles::new("Cargo.toml"); +// assert!(st.is_err()); +// } + +// #[test] +// fn test_default_handler_file_missing() { +// let st = StaticFiles::new(".") +// .unwrap() +// .default_handler(|_: &_| "default content"); +// let req = TestRequest::with_uri("/missing") +// .param("tail", "missing") +// .finish(); + +// let resp = st.handle(&req).respond_to(&req).unwrap(); +// let resp = resp.as_msg(); +// assert_eq!(resp.status(), StatusCode::OK); +// assert_eq!( +// resp.body(), +// &Body::Binary(Binary::Slice(b"default content")) +// ); +// } + +// #[test] +// fn test_serve_index() { +// let st = StaticFiles::new(".").unwrap().index_file("test.binary"); +// let req = TestRequest::default().uri("/tests").finish(); + +// let resp = st.handle(&req).respond_to(&req).unwrap(); +// let resp = resp.as_msg(); +// assert_eq!(resp.status(), StatusCode::OK); +// assert_eq!( +// resp.headers() +// .get(header::CONTENT_TYPE) +// .expect("content type"), +// "application/octet-stream" +// ); +// assert_eq!( +// resp.headers() +// .get(header::CONTENT_DISPOSITION) +// .expect("content disposition"), +// "attachment; filename=\"test.binary\"" +// ); + +// let req = TestRequest::default().uri("/tests/").finish(); +// let resp = st.handle(&req).respond_to(&req).unwrap(); +// let resp = resp.as_msg(); +// assert_eq!(resp.status(), StatusCode::OK); +// assert_eq!( +// resp.headers().get(header::CONTENT_TYPE).unwrap(), +// "application/octet-stream" +// ); +// assert_eq!( +// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), +// "attachment; filename=\"test.binary\"" +// ); + +// // nonexistent index file +// let req = TestRequest::default().uri("/tests/unknown").finish(); +// let resp = st.handle(&req).respond_to(&req).unwrap(); +// let resp = resp.as_msg(); +// assert_eq!(resp.status(), StatusCode::NOT_FOUND); + +// let req = TestRequest::default().uri("/tests/unknown/").finish(); +// let resp = st.handle(&req).respond_to(&req).unwrap(); +// let resp = resp.as_msg(); +// assert_eq!(resp.status(), StatusCode::NOT_FOUND); +// } + +// #[test] +// fn test_serve_index_nested() { +// let st = StaticFiles::new(".").unwrap().index_file("mod.rs"); +// let req = TestRequest::default().uri("/src/client").finish(); +// let resp = st.handle(&req).respond_to(&req).unwrap(); +// let resp = resp.as_msg(); +// assert_eq!(resp.status(), StatusCode::OK); +// assert_eq!( +// resp.headers().get(header::CONTENT_TYPE).unwrap(), +// "text/x-rust" +// ); +// assert_eq!( +// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), +// "inline; filename=\"mod.rs\"" +// ); +// } + +// #[test] +// fn integration_serve_index_with_prefix() { +// let mut srv = test::TestServer::with_factory(|| { +// App::new() +// .prefix("public") +// .handler("/", StaticFiles::new(".").unwrap().index_file("Cargo.toml")) +// }); + +// let request = srv.get().uri(srv.url("/public")).finish().unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert_eq!(response.status(), StatusCode::OK); +// let bytes = srv.execute(response.body()).unwrap(); +// let data = Bytes::from(fs::read("Cargo.toml").unwrap()); +// assert_eq!(bytes, data); + +// let request = srv.get().uri(srv.url("/public/")).finish().unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert_eq!(response.status(), StatusCode::OK); +// let bytes = srv.execute(response.body()).unwrap(); +// let data = Bytes::from(fs::read("Cargo.toml").unwrap()); +// assert_eq!(bytes, data); +// } + +// #[test] +// fn integration_serve_index() { +// let mut srv = test::TestServer::with_factory(|| { +// App::new().handler( +// "test", +// StaticFiles::new(".").unwrap().index_file("Cargo.toml"), +// ) +// }); + +// let request = srv.get().uri(srv.url("/test")).finish().unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert_eq!(response.status(), StatusCode::OK); +// let bytes = srv.execute(response.body()).unwrap(); +// let data = Bytes::from(fs::read("Cargo.toml").unwrap()); +// assert_eq!(bytes, data); + +// let request = srv.get().uri(srv.url("/test/")).finish().unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert_eq!(response.status(), StatusCode::OK); +// let bytes = srv.execute(response.body()).unwrap(); +// let data = Bytes::from(fs::read("Cargo.toml").unwrap()); +// assert_eq!(bytes, data); + +// // nonexistent index file +// let request = srv.get().uri(srv.url("/test/unknown")).finish().unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert_eq!(response.status(), StatusCode::NOT_FOUND); + +// let request = srv.get().uri(srv.url("/test/unknown/")).finish().unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert_eq!(response.status(), StatusCode::NOT_FOUND); +// } + +// #[test] +// fn integration_percent_encoded() { +// let mut srv = test::TestServer::with_factory(|| { +// App::new().handler( +// "test", +// StaticFiles::new(".").unwrap().index_file("Cargo.toml"), +// ) +// }); + +// let request = srv +// .get() +// .uri(srv.url("/test/%43argo.toml")) +// .finish() +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert_eq!(response.status(), StatusCode::OK); +// } + +// struct T(&'static str, u64, Vec); + +// #[test] +// fn test_parse() { +// let tests = vec![ +// T("", 0, vec![]), +// T("", 1000, vec![]), +// T("foo", 0, vec![]), +// T("bytes=", 0, vec![]), +// T("bytes=7", 10, vec![]), +// T("bytes= 7 ", 10, vec![]), +// T("bytes=1-", 0, vec![]), +// T("bytes=5-4", 10, vec![]), +// T("bytes=0-2,5-4", 10, vec![]), +// T("bytes=2-5,4-3", 10, vec![]), +// T("bytes=--5,4--3", 10, vec![]), +// T("bytes=A-", 10, vec![]), +// T("bytes=A- ", 10, vec![]), +// T("bytes=A-Z", 10, vec![]), +// T("bytes= -Z", 10, vec![]), +// T("bytes=5-Z", 10, vec![]), +// T("bytes=Ran-dom, garbage", 10, vec![]), +// T("bytes=0x01-0x02", 10, vec![]), +// T("bytes= ", 10, vec![]), +// T("bytes= , , , ", 10, vec![]), +// T( +// "bytes=0-9", +// 10, +// vec![HttpRange { +// start: 0, +// length: 10, +// }], +// ), +// T( +// "bytes=0-", +// 10, +// vec![HttpRange { +// start: 0, +// length: 10, +// }], +// ), +// T( +// "bytes=5-", +// 10, +// vec![HttpRange { +// start: 5, +// length: 5, +// }], +// ), +// T( +// "bytes=0-20", +// 10, +// vec![HttpRange { +// start: 0, +// length: 10, +// }], +// ), +// T( +// "bytes=15-,0-5", +// 10, +// vec![HttpRange { +// start: 0, +// length: 6, +// }], +// ), +// T( +// "bytes=1-2,5-", +// 10, +// vec![ +// HttpRange { +// start: 1, +// length: 2, +// }, +// HttpRange { +// start: 5, +// length: 5, +// }, +// ], +// ), +// T( +// "bytes=-2 , 7-", +// 11, +// vec![ +// HttpRange { +// start: 9, +// length: 2, +// }, +// HttpRange { +// start: 7, +// length: 4, +// }, +// ], +// ), +// T( +// "bytes=0-0 ,2-2, 7-", +// 11, +// vec![ +// HttpRange { +// start: 0, +// length: 1, +// }, +// HttpRange { +// start: 2, +// length: 1, +// }, +// HttpRange { +// start: 7, +// length: 4, +// }, +// ], +// ), +// T( +// "bytes=-5", +// 10, +// vec![HttpRange { +// start: 5, +// length: 5, +// }], +// ), +// T( +// "bytes=-15", +// 10, +// vec![HttpRange { +// start: 0, +// length: 10, +// }], +// ), +// T( +// "bytes=0-499", +// 10000, +// vec![HttpRange { +// start: 0, +// length: 500, +// }], +// ), +// T( +// "bytes=500-999", +// 10000, +// vec![HttpRange { +// start: 500, +// length: 500, +// }], +// ), +// T( +// "bytes=-500", +// 10000, +// vec![HttpRange { +// start: 9500, +// length: 500, +// }], +// ), +// T( +// "bytes=9500-", +// 10000, +// vec![HttpRange { +// start: 9500, +// length: 500, +// }], +// ), +// T( +// "bytes=0-0,-1", +// 10000, +// vec![ +// HttpRange { +// start: 0, +// length: 1, +// }, +// HttpRange { +// start: 9999, +// length: 1, +// }, +// ], +// ), +// T( +// "bytes=500-600,601-999", +// 10000, +// vec![ +// HttpRange { +// start: 500, +// length: 101, +// }, +// HttpRange { +// start: 601, +// length: 399, +// }, +// ], +// ), +// T( +// "bytes=500-700,601-999", +// 10000, +// vec![ +// HttpRange { +// start: 500, +// length: 201, +// }, +// HttpRange { +// start: 601, +// length: 399, +// }, +// ], +// ), +// // Match Apache laxity: +// T( +// "bytes= 1 -2 , 4- 5, 7 - 8 , ,,", +// 11, +// vec![ +// HttpRange { +// start: 1, +// length: 2, +// }, +// HttpRange { +// start: 4, +// length: 2, +// }, +// HttpRange { +// start: 7, +// length: 2, +// }, +// ], +// ), +// ]; + +// for t in tests { +// let header = t.0; +// let size = t.1; +// let expected = t.2; + +// let res = HttpRange::parse(header, size); + +// if res.is_err() { +// if expected.is_empty() { +// continue; +// } else { +// assert!( +// false, +// "parse({}, {}) returned error {:?}", +// header, +// size, +// res.unwrap_err() +// ); +// } +// } + +// let got = res.unwrap(); + +// if got.len() != expected.len() { +// assert!( +// false, +// "len(parseRange({}, {})) = {}, want {}", +// header, +// size, +// got.len(), +// expected.len() +// ); +// continue; +// } + +// for i in 0..expected.len() { +// if got[i].start != expected[i].start { +// assert!( +// false, +// "parseRange({}, {})[{}].start = {}, want {}", +// header, size, i, got[i].start, expected[i].start +// ) +// } +// if got[i].length != expected[i].length { +// assert!( +// false, +// "parseRange({}, {})[{}].length = {}, want {}", +// header, size, i, got[i].length, expected[i].length +// ) +// } +// } +// } +// } +// } diff --git a/src/handler.rs b/src/handler.rs index c6880818..e957d15e 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,562 +1,402 @@ use std::marker::PhantomData; -use std::ops::Deref; -use futures::future::{err, ok, Future}; -use futures::{Async, Poll}; +use actix_http::{Error, Response}; +use actix_service::{NewService, Service}; +use actix_utils::Never; +use futures::future::{ok, FutureResult}; +use futures::{try_ready, Async, Future, IntoFuture, Poll}; -use error::Error; -use http::StatusCode; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use resource::DefaultResource; - -/// Trait defines object that could be registered as route handler -#[allow(unused_variables)] -pub trait Handler: 'static { - /// The type of value that handler will return. - type Result: Responder; - - /// Handle request - fn handle(&self, req: &HttpRequest) -> Self::Result; -} - -/// Trait implemented by types that generate responses for clients. -/// -/// Types that implement this trait can be used as the return type of a handler. -pub trait Responder { - /// The associated item which can be returned. - type Item: Into>; - - /// The associated error which can be returned. - type Error: Into; - - /// Convert itself to `AsyncResult` or `Error`. - fn respond_to( - self, req: &HttpRequest, - ) -> Result; -} +use crate::request::HttpRequest; +use crate::responder::Responder; +use crate::service::{ServiceRequest, ServiceResponse}; /// Trait implemented by types that can be extracted from request. /// -/// Types that implement this trait can be used with `Route::with()` method. -pub trait FromRequest: Sized { - /// Configuration for conversion process - type Config: Default; +/// Types that implement this trait can be used with `Route` handlers. +pub trait FromRequest

: Sized { + /// The associated error which can be returned. + type Error: Into; /// Future that resolves to a Self - type Result: Into>; + type Future: Future; /// Convert request to a Self - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result; - - /// Convert request to a Self - /// - /// This method uses default extractor configuration - fn extract(req: &HttpRequest) -> Self::Result { - Self::from_request(req, &Self::Config::default()) - } + fn from_request(req: &mut ServiceRequest

) -> Self::Future; } -/// Combines two different responder types into a single type -/// -/// ```rust -/// # extern crate actix_web; -/// # extern crate futures; -/// # use futures::future::Future; -/// use actix_web::{AsyncResponder, Either, Error, HttpRequest, HttpResponse}; -/// use futures::future::result; -/// -/// type RegisterResult = -/// Either>>; -/// -/// fn index(req: HttpRequest) -> RegisterResult { -/// if is_a_variant() { -/// // <- choose variant A -/// Either::A(HttpResponse::BadRequest().body("Bad data")) -/// } else { -/// Either::B( -/// // <- variant B -/// result(Ok(HttpResponse::Ok() -/// .content_type("text/html") -/// .body("Hello!"))) -/// .responder(), -/// ) -/// } -/// } -/// # fn is_a_variant() -> bool { true } -/// # fn main() {} -/// ``` -#[derive(Debug, PartialEq)] -pub enum Either { - /// First branch of the type - A(A), - /// Second branch of the type - B(B), -} - -impl Responder for Either +/// Handler converter factory +pub trait Factory: Clone where - A: Responder, - B: Responder, -{ - type Item = AsyncResult; - type Error = Error; - - fn respond_to( - self, req: &HttpRequest, - ) -> Result, Error> { - match self { - Either::A(a) => match a.respond_to(req) { - Ok(val) => Ok(val.into()), - Err(err) => Err(err.into()), - }, - Either::B(b) => match b.respond_to(req) { - Ok(val) => Ok(val.into()), - Err(err) => Err(err.into()), - }, - } - } -} - -impl Future for Either -where - A: Future, - B: Future, -{ - type Item = I; - type Error = E; - - fn poll(&mut self) -> Poll { - match *self { - Either::A(ref mut fut) => fut.poll(), - Either::B(ref mut fut) => fut.poll(), - } - } -} - -impl Responder for Option -where - T: Responder, -{ - type Item = AsyncResult; - type Error = Error; - - fn respond_to( - self, req: &HttpRequest, - ) -> Result, Error> { - match self { - Some(t) => match t.respond_to(req) { - Ok(val) => Ok(val.into()), - Err(err) => Err(err.into()), - }, - None => Ok(req.build_response(StatusCode::NOT_FOUND).finish().into()), - } - } -} - -/// Convenience trait that converts `Future` object to a `Boxed` future -/// -/// For example loading json from request's body is async operation. -/// -/// ```rust -/// # extern crate actix_web; -/// # extern crate futures; -/// # #[macro_use] extern crate serde_derive; -/// use actix_web::{ -/// App, AsyncResponder, Error, HttpMessage, HttpRequest, HttpResponse, -/// }; -/// use futures::future::Future; -/// -/// #[derive(Deserialize, Debug)] -/// struct MyObj { -/// name: String, -/// } -/// -/// fn index(mut req: HttpRequest) -> Box> { -/// req.json() // <- get JsonBody future -/// .from_err() -/// .and_then(|val: MyObj| { // <- deserialized value -/// Ok(HttpResponse::Ok().into()) -/// }) -/// // Construct boxed future by using `AsyncResponder::responder()` method -/// .responder() -/// } -/// # fn main() {} -/// ``` -pub trait AsyncResponder: Sized { - /// Convert to a boxed future - fn responder(self) -> Box>; -} - -impl AsyncResponder for F -where - F: Future + 'static, - I: Responder + 'static, - E: Into + 'static, -{ - fn responder(self) -> Box> { - Box::new(self) - } -} - -/// Handler for Fn() -impl Handler for F -where - F: Fn(&HttpRequest) -> R + 'static, - R: Responder + 'static, -{ - type Result = R; - - fn handle(&self, req: &HttpRequest) -> R { - (self)(req) - } -} - -/// Represents async result -/// -/// Result could be in tree different forms. -/// * Ok(T) - ready item -/// * Err(E) - error happen during reply process -/// * Future - reply process completes in the future -pub struct AsyncResult(Option>); - -impl Future for AsyncResult { - type Item = I; - type Error = E; - - fn poll(&mut self) -> Poll { - let res = self.0.take().expect("use after resolve"); - match res { - AsyncResultItem::Ok(msg) => Ok(Async::Ready(msg)), - AsyncResultItem::Err(err) => Err(err), - AsyncResultItem::Future(mut fut) => match fut.poll() { - Ok(Async::NotReady) => { - self.0 = Some(AsyncResultItem::Future(fut)); - Ok(Async::NotReady) - } - Ok(Async::Ready(msg)) => Ok(Async::Ready(msg)), - Err(err) => Err(err), - }, - } - } -} - -pub(crate) enum AsyncResultItem { - Ok(I), - Err(E), - Future(Box>), -} - -impl AsyncResult { - /// Create async response - #[inline] - pub fn future(fut: Box>) -> AsyncResult { - AsyncResult(Some(AsyncResultItem::Future(fut))) - } - - /// Send response - #[inline] - pub fn ok>(ok: R) -> AsyncResult { - AsyncResult(Some(AsyncResultItem::Ok(ok.into()))) - } - - /// Send error - #[inline] - pub fn err>(err: R) -> AsyncResult { - AsyncResult(Some(AsyncResultItem::Err(err.into()))) - } - - #[inline] - pub(crate) fn into(self) -> AsyncResultItem { - self.0.expect("use after resolve") - } - - #[cfg(test)] - pub(crate) fn as_msg(&self) -> &I { - match self.0.as_ref().unwrap() { - &AsyncResultItem::Ok(ref resp) => resp, - _ => panic!(), - } - } - - #[cfg(test)] - pub(crate) fn as_err(&self) -> Option<&E> { - match self.0.as_ref().unwrap() { - &AsyncResultItem::Err(ref err) => Some(err), - _ => None, - } - } -} - -impl Responder for AsyncResult { - type Item = AsyncResult; - type Error = Error; - - fn respond_to( - self, _: &HttpRequest, - ) -> Result, Error> { - Ok(self) - } -} - -impl Responder for HttpResponse { - type Item = AsyncResult; - type Error = Error; - - #[inline] - fn respond_to( - self, _: &HttpRequest, - ) -> Result, Error> { - Ok(AsyncResult(Some(AsyncResultItem::Ok(self)))) - } -} - -impl From for AsyncResult { - #[inline] - fn from(resp: T) -> AsyncResult { - AsyncResult(Some(AsyncResultItem::Ok(resp))) - } -} - -impl> Responder for Result { - type Item = ::Item; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - match self { - Ok(val) => match val.respond_to(req) { - Ok(val) => Ok(val), - Err(err) => Err(err.into()), - }, - Err(err) => Err(err.into()), - } - } -} - -impl> From, E>> for AsyncResult { - #[inline] - fn from(res: Result, E>) -> Self { - match res { - Ok(val) => val, - Err(err) => AsyncResult(Some(AsyncResultItem::Err(err.into()))), - } - } -} - -impl> From> for AsyncResult { - #[inline] - fn from(res: Result) -> Self { - match res { - Ok(val) => AsyncResult(Some(AsyncResultItem::Ok(val))), - Err(err) => AsyncResult(Some(AsyncResultItem::Err(err.into()))), - } - } -} - -impl From>, E>> for AsyncResult -where - T: 'static, - E: Into + 'static, -{ - #[inline] - fn from(res: Result>, E>) -> Self { - match res { - Ok(fut) => AsyncResult(Some(AsyncResultItem::Future(Box::new( - fut.map_err(|e| e.into()), - )))), - Err(err) => AsyncResult(Some(AsyncResultItem::Err(err.into()))), - } - } -} - -impl From>> for AsyncResult { - #[inline] - fn from(fut: Box>) -> AsyncResult { - AsyncResult(Some(AsyncResultItem::Future(fut))) - } -} - -/// Convenience type alias -pub type FutureResponse = Box>; - -impl Responder for Box> -where - I: Responder + 'static, - E: Into + 'static, -{ - type Item = AsyncResult; - type Error = Error; - - #[inline] - fn respond_to( - self, req: &HttpRequest, - ) -> Result, Error> { - let req = req.clone(); - let fut = self - .map_err(|e| e.into()) - .then(move |r| match r.respond_to(&req) { - Ok(reply) => match reply.into().into() { - AsyncResultItem::Ok(resp) => ok(resp), - _ => panic!("Nested async replies are not supported"), - }, - Err(e) => err(e), - }); - Ok(AsyncResult::future(Box::new(fut))) - } -} - -pub(crate) trait RouteHandler: 'static { - fn handle(&self, &HttpRequest) -> AsyncResult; - - fn has_default_resource(&self) -> bool { - false - } - - fn default_resource(&mut self, _: DefaultResource) {} - - fn finish(&mut self) {} -} - -/// Route handler wrapper for Handler -pub(crate) struct WrapHandler -where - H: Handler, R: Responder, - S: 'static, { - h: H, - s: PhantomData, + fn call(&self, param: T) -> R; } -impl WrapHandler +impl Factory<(), R> for F where - H: Handler, + F: Fn() -> R + Clone + 'static, + R: Responder + 'static, +{ + fn call(&self, _: ()) -> R { + (self)() + } +} + +#[doc(hidden)] +pub struct Handle +where + F: Factory, R: Responder, - S: 'static, { - pub fn new(h: H) -> Self { - WrapHandler { h, s: PhantomData } + hnd: F, + _t: PhantomData<(T, R)>, +} + +impl Handle +where + F: Factory, + R: Responder, +{ + pub fn new(hnd: F) -> Self { + Handle { + hnd, + _t: PhantomData, + } + } +} +impl NewService for Handle +where + F: Factory, + R: Responder + 'static, +{ + type Request = (T, HttpRequest); + type Response = ServiceResponse; + type Error = Never; + type InitError = (); + type Service = HandleService; + type Future = FutureResult; + + fn new_service(&self, _: &()) -> Self::Future { + ok(HandleService { + hnd: self.hnd.clone(), + _t: PhantomData, + }) } } -impl RouteHandler for WrapHandler +#[doc(hidden)] +pub struct HandleService where - H: Handler, + F: Factory, R: Responder + 'static, - S: 'static, { - fn handle(&self, req: &HttpRequest) -> AsyncResult { - match self.h.handle(req).respond_to(req) { - Ok(reply) => reply.into(), - Err(err) => AsyncResult::err(err.into()), + hnd: F, + _t: PhantomData<(T, R)>, +} + +impl Service for HandleService +where + F: Factory, + R: Responder + 'static, +{ + type Request = (T, HttpRequest); + type Response = ServiceResponse; + type Error = Never; + type Future = HandleServiceResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, (param, req): (T, HttpRequest)) -> Self::Future { + let fut = self.hnd.call(param).respond_to(&req); + HandleServiceResponse { + fut, + req: Some(req), } } } -/// Async route handler -pub(crate) struct AsyncHandler -where - H: Fn(&HttpRequest) -> F + 'static, - F: Future + 'static, - R: Responder + 'static, - E: Into + 'static, - S: 'static, -{ - h: Box, - s: PhantomData, +pub struct HandleServiceResponse { + fut: T, + req: Option, } -impl AsyncHandler +impl Future for HandleServiceResponse where - H: Fn(&HttpRequest) -> F + 'static, - F: Future + 'static, - R: Responder + 'static, - E: Into + 'static, - S: 'static, + T: Future, + T::Error: Into, { - pub fn new(h: H) -> Self { - AsyncHandler { - h: Box::new(h), - s: PhantomData, - } - } -} + type Item = ServiceResponse; + type Error = Never; -impl RouteHandler for AsyncHandler -where - H: Fn(&HttpRequest) -> F + 'static, - F: Future + 'static, - R: Responder + 'static, - E: Into + 'static, - S: 'static, -{ - fn handle(&self, req: &HttpRequest) -> AsyncResult { - let req = req.clone(); - let fut = (self.h)(&req).map_err(|e| e.into()).then(move |r| { - match r.respond_to(&req) { - Ok(reply) => match reply.into().into() { - AsyncResultItem::Ok(resp) => Either::A(ok(resp)), - AsyncResultItem::Err(e) => Either::A(err(e)), - AsyncResultItem::Future(fut) => Either::B(fut), - }, - Err(e) => Either::A(err(e)), + fn poll(&mut self) -> Poll { + match self.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, + ))) } - }); - AsyncResult::future(Box::new(fut)) + } } } -/// Access an application state -/// -/// `S` - application state type -/// -/// ## Example -/// -/// ```rust -/// # extern crate bytes; -/// # extern crate actix_web; -/// # extern crate futures; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{http, App, Path, State}; -/// -/// /// Application state -/// struct MyApp { -/// msg: &'static str, -/// } -/// -/// #[derive(Deserialize)] -/// struct Info { -/// username: String, -/// } -/// -/// /// extract path info using serde -/// fn index(state: State, path: Path) -> String { -/// format!("{} {}!", state.msg, path.username) -/// } -/// -/// fn main() { -/// let app = App::with_state(MyApp { msg: "Welcome" }).resource( -/// "/{username}/index.html", // <- define path parameters -/// |r| r.method(http::Method::GET).with(index), -/// ); // <- use `with` extractor -/// } -/// ``` -pub struct State(HttpRequest); +/// Async handler converter factory +pub trait AsyncFactory: Clone + 'static +where + R: IntoFuture, + R::Item: Into, + R::Error: Into, +{ + fn call(&self, param: T) -> R; +} -impl Deref for State { - type Target = S; - - fn deref(&self) -> &S { - self.0.state() +impl AsyncFactory<(), R> for F +where + F: Fn() -> R + Clone + 'static, + R: IntoFuture, + R::Item: Into, + R::Error: Into, +{ + fn call(&self, _: ()) -> R { + (self)() } } -impl FromRequest for State { - type Config = (); - type Result = State; +#[doc(hidden)] +pub struct AsyncHandle +where + F: AsyncFactory, + R: IntoFuture, + R::Item: Into, + R::Error: Into, +{ + hnd: F, + _t: PhantomData<(T, R)>, +} - #[inline] - fn from_request(req: &HttpRequest, _: &Self::Config) -> Self::Result { - State(req.clone()) +impl AsyncHandle +where + F: AsyncFactory, + R: IntoFuture, + R::Item: Into, + R::Error: Into, +{ + pub fn new(hnd: F) -> Self { + AsyncHandle { + hnd, + _t: PhantomData, + } } } +impl NewService for AsyncHandle +where + F: AsyncFactory, + R: IntoFuture, + R::Item: Into, + R::Error: Into, +{ + type Request = (T, HttpRequest); + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = AsyncHandleService; + type Future = FutureResult; + + fn new_service(&self, _: &()) -> Self::Future { + ok(AsyncHandleService { + hnd: self.hnd.clone(), + _t: PhantomData, + }) + } +} + +#[doc(hidden)] +pub struct AsyncHandleService +where + F: AsyncFactory, + R: IntoFuture, + R::Item: Into, + R::Error: Into, +{ + hnd: F, + _t: PhantomData<(T, R)>, +} + +impl Service for AsyncHandleService +where + F: AsyncFactory, + R: IntoFuture, + R::Item: Into, + R::Error: Into, +{ + type Request = (T, HttpRequest); + type Response = ServiceResponse; + type Error = (); + type Future = AsyncHandleServiceResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, (param, req): (T, HttpRequest)) -> Self::Future { + AsyncHandleServiceResponse { + fut: self.hnd.call(param).into_future(), + req: Some(req), + } + } +} + +#[doc(hidden)] +pub struct AsyncHandleServiceResponse { + fut: T, + req: Option, +} + +impl Future for AsyncHandleServiceResponse +where + T: Future, + T::Item: Into, + T::Error: Into, +{ + type Item = ServiceResponse; + type Error = (); + + fn poll(&mut self) -> Poll { + match self.fut.poll() { + Ok(Async::Ready(res)) => Ok(Async::Ready(ServiceResponse::new( + self.req.take().unwrap(), + res.into(), + ))), + Ok(Async::NotReady) => Ok(Async::NotReady), + Err(e) => { + let res: Response = e.into().into(); + Ok(Async::Ready(ServiceResponse::new( + self.req.take().unwrap(), + res, + ))) + } + } + } +} + +/// Extract arguments from request +pub struct Extract> { + _t: PhantomData<(P, T)>, +} + +impl> Extract { + pub fn new() -> Self { + Extract { _t: PhantomData } + } +} + +impl> Default for Extract { + fn default() -> Self { + Self::new() + } +} + +impl> NewService for Extract { + type Request = ServiceRequest

; + type Response = (T, HttpRequest); + type Error = (Error, ServiceRequest

); + type InitError = (); + type Service = ExtractService; + type Future = FutureResult; + + fn new_service(&self, _: &()) -> Self::Future { + ok(ExtractService { _t: PhantomData }) + } +} + +pub struct ExtractService> { + _t: PhantomData<(P, T)>, +} + +impl> Service for ExtractService { + type Request = ServiceRequest

; + type Response = (T, HttpRequest); + type Error = (Error, ServiceRequest

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

) -> Self::Future { + ExtractResponse { + fut: T::from_request(&mut req), + req: Some(req), + } + } +} + +pub struct ExtractResponse> { + req: Option>, + fut: T::Future, +} + +impl> Future for ExtractResponse { + type Item = (T, HttpRequest); + 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()))); + + let req = self.req.take().unwrap(); + let req = req.into_request(); + + Ok(Async::Ready((item, req))) + } +} + +/// 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, + //$($T,)+ + Res: Responder + 'static, + { + fn call(&self, param: ($($T,)+)) -> Res { + (self)($(param.$n,)+) + } + } + + impl AsyncFactory<($($T,)+), Res> for Func + where Func: Fn($($T,)+) -> Res + Clone + 'static, + Res: IntoFuture + 'static, + Res::Item: Into, + Res::Error: Into, + { + fn call(&self, param: ($($T,)+)) -> Res { + (self)($(param.$n,)+) + } + } +}); + +#[rustfmt::skip] +mod m { + use super::*; + +factory_tuple!((0, A)); +factory_tuple!((0, A), (1, B)); +factory_tuple!((0, A), (1, B), (2, C)); +factory_tuple!((0, A), (1, B), (2, C), (3, D)); +factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E)); +factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); +factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); +factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); +factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); +factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); +} diff --git a/src/header/common/accept.rs b/src/header/common/accept.rs deleted file mode 100644 index d736e53a..00000000 --- a/src/header/common/accept.rs +++ /dev/null @@ -1,159 +0,0 @@ -use header::{qitem, QualityItem}; -use http::header as http; -use mime::{self, Mime}; - -header! { - /// `Accept` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.2) - /// - /// The `Accept` header field can be used by user agents to specify - /// response media types that are acceptable. Accept header fields can - /// be used to indicate that the request is specifically limited to a - /// small set of desired types, as in the case of a request for an - /// in-line image - /// - /// # ABNF - /// - /// ```text - /// Accept = #( media-range [ accept-params ] ) - /// - /// media-range = ( "*/*" - /// / ( type "/" "*" ) - /// / ( type "/" subtype ) - /// ) *( OWS ";" OWS parameter ) - /// accept-params = weight *( accept-ext ) - /// accept-ext = OWS ";" OWS token [ "=" ( token / quoted-string ) ] - /// ``` - /// - /// # Example values - /// * `audio/*; q=0.2, audio/basic` - /// * `text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c` - /// - /// # Examples - /// ```rust - /// # extern crate actix_web; - /// extern crate mime; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{Accept, qitem}; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// - /// builder.set( - /// Accept(vec![ - /// qitem(mime::TEXT_HTML), - /// ]) - /// ); - /// # } - /// ``` - /// - /// ```rust - /// # extern crate actix_web; - /// extern crate mime; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{Accept, qitem}; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// - /// builder.set( - /// Accept(vec![ - /// qitem(mime::APPLICATION_JSON), - /// ]) - /// ); - /// # } - /// ``` - /// - /// ```rust - /// # extern crate actix_web; - /// extern crate mime; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{Accept, QualityItem, q, qitem}; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// - /// builder.set( - /// Accept(vec![ - /// qitem(mime::TEXT_HTML), - /// qitem("application/xhtml+xml".parse().unwrap()), - /// QualityItem::new( - /// mime::TEXT_XML, - /// q(900) - /// ), - /// qitem("image/webp".parse().unwrap()), - /// QualityItem::new( - /// mime::STAR_STAR, - /// q(800) - /// ), - /// ]) - /// ); - /// # } - /// ``` - (Accept, http::ACCEPT) => (QualityItem)+ - - test_accept { - // Tests from the RFC - test_header!( - test1, - vec![b"audio/*; q=0.2, audio/basic"], - Some(HeaderField(vec![ - QualityItem::new("audio/*".parse().unwrap(), q(200)), - qitem("audio/basic".parse().unwrap()), - ]))); - test_header!( - test2, - vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"], - Some(HeaderField(vec![ - QualityItem::new(TEXT_PLAIN, q(500)), - qitem(TEXT_HTML), - QualityItem::new( - "text/x-dvi".parse().unwrap(), - q(800)), - qitem("text/x-c".parse().unwrap()), - ]))); - // Custom tests - test_header!( - test3, - vec![b"text/plain; charset=utf-8"], - Some(Accept(vec![ - qitem(TEXT_PLAIN_UTF_8), - ]))); - test_header!( - test4, - vec![b"text/plain; charset=utf-8; q=0.5"], - Some(Accept(vec![ - QualityItem::new(TEXT_PLAIN_UTF_8, - q(500)), - ]))); - - #[test] - fn test_fuzzing1() { - use test::TestRequest; - let req = TestRequest::with_header(super::http::ACCEPT, "chunk#;e").finish(); - let header = Accept::parse(&req); - assert!(header.is_ok()); - } - } -} - -impl Accept { - /// A constructor to easily create `Accept: */*`. - pub fn star() -> Accept { - Accept(vec![qitem(mime::STAR_STAR)]) - } - - /// A constructor to easily create `Accept: application/json`. - pub fn json() -> Accept { - Accept(vec![qitem(mime::APPLICATION_JSON)]) - } - - /// A constructor to easily create `Accept: text/*`. - pub fn text() -> Accept { - Accept(vec![qitem(mime::TEXT_STAR)]) - } - - /// A constructor to easily create `Accept: image/*`. - pub fn image() -> Accept { - Accept(vec![qitem(mime::IMAGE_STAR)]) - } -} diff --git a/src/header/common/accept_charset.rs b/src/header/common/accept_charset.rs deleted file mode 100644 index 674415fb..00000000 --- a/src/header/common/accept_charset.rs +++ /dev/null @@ -1,69 +0,0 @@ -use header::{Charset, QualityItem, ACCEPT_CHARSET}; - -header! { - /// `Accept-Charset` header, defined in - /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.3) - /// - /// The `Accept-Charset` header field can be sent by a user agent to - /// indicate what charsets are acceptable in textual response content. - /// This field allows user agents capable of understanding more - /// comprehensive or special-purpose charsets to signal that capability - /// to an origin server that is capable of representing information in - /// those charsets. - /// - /// # ABNF - /// - /// ```text - /// Accept-Charset = 1#( ( charset / "*" ) [ weight ] ) - /// ``` - /// - /// # Example values - /// * `iso-8859-5, unicode-1-1;q=0.8` - /// - /// # Examples - /// ```rust - /// # extern crate actix_web; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptCharset, Charset, qitem}; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// AcceptCharset(vec![qitem(Charset::Us_Ascii)]) - /// ); - /// # } - /// ``` - /// ```rust - /// # extern crate actix_web; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptCharset, Charset, q, QualityItem}; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// AcceptCharset(vec![ - /// QualityItem::new(Charset::Us_Ascii, q(900)), - /// QualityItem::new(Charset::Iso_8859_10, q(200)), - /// ]) - /// ); - /// # } - /// ``` - /// ```rust - /// # extern crate actix_web; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptCharset, Charset, qitem}; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// AcceptCharset(vec![qitem(Charset::Ext("utf-8".to_owned()))]) - /// ); - /// # } - /// ``` - (AcceptCharset, ACCEPT_CHARSET) => (QualityItem)+ - - test_accept_charset { - /// Test case from RFC - test_header!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]); - } -} diff --git a/src/header/common/accept_encoding.rs b/src/header/common/accept_encoding.rs deleted file mode 100644 index c90f529b..00000000 --- a/src/header/common/accept_encoding.rs +++ /dev/null @@ -1,72 +0,0 @@ -use header::{Encoding, QualityItem}; - -header! { - /// `Accept-Encoding` header, defined in - /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.4) - /// - /// The `Accept-Encoding` header field can be used by user agents to - /// indicate what response content-codings are - /// acceptable in the response. An `identity` token is used as a synonym - /// for "no encoding" in order to communicate when no encoding is - /// preferred. - /// - /// # ABNF - /// - /// ```text - /// Accept-Encoding = #( codings [ weight ] ) - /// codings = content-coding / "identity" / "*" - /// ``` - /// - /// # Example values - /// * `compress, gzip` - /// * `` - /// * `*` - /// * `compress;q=0.5, gzip;q=1` - /// * `gzip;q=1.0, identity; q=0.5, *;q=0` - /// - /// # Examples - /// ``` - /// use hyper::header::{Headers, AcceptEncoding, Encoding, qitem}; - /// - /// let mut headers = Headers::new(); - /// headers.set( - /// AcceptEncoding(vec![qitem(Encoding::Chunked)]) - /// ); - /// ``` - /// ``` - /// use hyper::header::{Headers, AcceptEncoding, Encoding, qitem}; - /// - /// let mut headers = Headers::new(); - /// headers.set( - /// AcceptEncoding(vec![ - /// qitem(Encoding::Chunked), - /// qitem(Encoding::Gzip), - /// qitem(Encoding::Deflate), - /// ]) - /// ); - /// ``` - /// ``` - /// use hyper::header::{Headers, AcceptEncoding, Encoding, QualityItem, q, qitem}; - /// - /// let mut headers = Headers::new(); - /// headers.set( - /// AcceptEncoding(vec![ - /// qitem(Encoding::Chunked), - /// QualityItem::new(Encoding::Gzip, q(600)), - /// QualityItem::new(Encoding::EncodingExt("*".to_owned()), q(0)), - /// ]) - /// ); - /// ``` - (AcceptEncoding, "Accept-Encoding") => (QualityItem)* - - test_accept_encoding { - // From the RFC - test_header!(test1, vec![b"compress, gzip"]); - test_header!(test2, vec![b""], Some(AcceptEncoding(vec![]))); - test_header!(test3, vec![b"*"]); - // Note: Removed quality 1 from gzip - test_header!(test4, vec![b"compress;q=0.5, gzip"]); - // Note: Removed quality 1 from gzip - test_header!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]); - } -} diff --git a/src/header/common/accept_language.rs b/src/header/common/accept_language.rs deleted file mode 100644 index 12593e1a..00000000 --- a/src/header/common/accept_language.rs +++ /dev/null @@ -1,75 +0,0 @@ -use header::{QualityItem, ACCEPT_LANGUAGE}; -use language_tags::LanguageTag; - -header! { - /// `Accept-Language` header, defined in - /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.5) - /// - /// The `Accept-Language` header field can be used by user agents to - /// indicate the set of natural languages that are preferred in the - /// response. - /// - /// # ABNF - /// - /// ```text - /// Accept-Language = 1#( language-range [ weight ] ) - /// language-range = - /// ``` - /// - /// # Example values - /// * `da, en-gb;q=0.8, en;q=0.7` - /// * `en-us;q=1.0, en;q=0.5, fr` - /// - /// # Examples - /// - /// ```rust - /// # extern crate actix_web; - /// # extern crate language_tags; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptLanguage, LanguageTag, qitem}; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// let mut langtag: LanguageTag = Default::default(); - /// langtag.language = Some("en".to_owned()); - /// langtag.region = Some("US".to_owned()); - /// builder.set( - /// AcceptLanguage(vec![ - /// qitem(langtag), - /// ]) - /// ); - /// # } - /// ``` - /// - /// ```rust - /// # extern crate actix_web; - /// # #[macro_use] extern crate language_tags; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptLanguage, QualityItem, q, qitem}; - /// # - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// AcceptLanguage(vec![ - /// qitem(langtag!(da)), - /// QualityItem::new(langtag!(en;;;GB), q(800)), - /// QualityItem::new(langtag!(en), q(700)), - /// ]) - /// ); - /// # } - /// ``` - (AcceptLanguage, ACCEPT_LANGUAGE) => (QualityItem)+ - - test_accept_language { - // From the RFC - test_header!(test1, vec![b"da, en-gb;q=0.8, en;q=0.7"]); - // Own test - test_header!( - test2, vec![b"en-US, en; q=0.5, fr"], - Some(AcceptLanguage(vec![ - qitem("en-US".parse().unwrap()), - QualityItem::new("en".parse().unwrap(), q(500)), - qitem("fr".parse().unwrap()), - ]))); - } -} diff --git a/src/header/common/allow.rs b/src/header/common/allow.rs deleted file mode 100644 index 5046290d..00000000 --- a/src/header/common/allow.rs +++ /dev/null @@ -1,85 +0,0 @@ -use http::Method; -use http::header; - -header! { - /// `Allow` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.4.1) - /// - /// The `Allow` header field lists the set of methods advertised as - /// supported by the target resource. The purpose of this field is - /// strictly to inform the recipient of valid request methods associated - /// with the resource. - /// - /// # ABNF - /// - /// ```text - /// Allow = #method - /// ``` - /// - /// # Example values - /// * `GET, HEAD, PUT` - /// * `OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH, fOObAr` - /// * `` - /// - /// # Examples - /// - /// ```rust - /// # extern crate http; - /// # extern crate actix_web; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::Allow; - /// use http::Method; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// Allow(vec![Method::GET]) - /// ); - /// # } - /// ``` - /// - /// ```rust - /// # extern crate http; - /// # extern crate actix_web; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::Allow; - /// use http::Method; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// Allow(vec![ - /// Method::GET, - /// Method::POST, - /// Method::PATCH, - /// ]) - /// ); - /// # } - /// ``` - (Allow, header::ALLOW) => (Method)* - - test_allow { - // From the RFC - test_header!( - test1, - vec![b"GET, HEAD, PUT"], - Some(HeaderField(vec![Method::GET, Method::HEAD, Method::PUT]))); - // Own tests - test_header!( - test2, - vec![b"OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH"], - Some(HeaderField(vec![ - Method::OPTIONS, - Method::GET, - Method::PUT, - Method::POST, - Method::DELETE, - Method::HEAD, - Method::TRACE, - Method::CONNECT, - Method::PATCH]))); - test_header!( - test3, - vec![b""], - Some(HeaderField(Vec::::new()))); - } -} diff --git a/src/header/common/cache_control.rs b/src/header/common/cache_control.rs deleted file mode 100644 index adc60e4a..00000000 --- a/src/header/common/cache_control.rs +++ /dev/null @@ -1,254 +0,0 @@ -use header::{Header, IntoHeaderValue, Writer}; -use header::{fmt_comma_delimited, from_comma_delimited}; -use http::header; -use std::fmt::{self, Write}; -use std::str::FromStr; - -/// `Cache-Control` header, defined in [RFC7234](https://tools.ietf.org/html/rfc7234#section-5.2) -/// -/// The `Cache-Control` header field is used to specify directives for -/// caches along the request/response chain. Such cache directives are -/// unidirectional in that the presence of a directive in a request does -/// not imply that the same directive is to be given in the response. -/// -/// # ABNF -/// -/// ```text -/// Cache-Control = 1#cache-directive -/// cache-directive = token [ "=" ( token / quoted-string ) ] -/// ``` -/// -/// # Example values -/// -/// * `no-cache` -/// * `private, community="UCI"` -/// * `max-age=30` -/// -/// # Examples -/// ```rust -/// use actix_web::HttpResponse; -/// use actix_web::http::header::{CacheControl, CacheDirective}; -/// -/// let mut builder = HttpResponse::Ok(); -/// builder.set(CacheControl(vec![CacheDirective::MaxAge(86400u32)])); -/// ``` -/// -/// ```rust -/// use actix_web::HttpResponse; -/// use actix_web::http::header::{CacheControl, CacheDirective}; -/// -/// let mut builder = HttpResponse::Ok(); -/// builder.set(CacheControl(vec![ -/// CacheDirective::NoCache, -/// CacheDirective::Private, -/// CacheDirective::MaxAge(360u32), -/// CacheDirective::Extension("foo".to_owned(), Some("bar".to_owned())), -/// ])); -/// ``` -#[derive(PartialEq, Clone, Debug)] -pub struct CacheControl(pub Vec); - -__hyper__deref!(CacheControl => Vec); - -//TODO: this could just be the header! macro -impl Header for CacheControl { - fn name() -> header::HeaderName { - header::CACHE_CONTROL - } - - #[inline] - fn parse(msg: &T) -> Result - where - T: ::HttpMessage, - { - let directives = from_comma_delimited(msg.headers().get_all(Self::name()))?; - if !directives.is_empty() { - Ok(CacheControl(directives)) - } else { - Err(::error::ParseError::Header) - } - } -} - -impl fmt::Display for CacheControl { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt_comma_delimited(f, &self[..]) - } -} - -impl IntoHeaderValue for CacheControl { - type Error = header::InvalidHeaderValueBytes; - - fn try_into(self) -> Result { - let mut writer = Writer::new(); - let _ = write!(&mut writer, "{}", self); - header::HeaderValue::from_shared(writer.take()) - } -} - -/// `CacheControl` contains a list of these directives. -#[derive(PartialEq, Clone, Debug)] -pub enum CacheDirective { - /// "no-cache" - NoCache, - /// "no-store" - NoStore, - /// "no-transform" - NoTransform, - /// "only-if-cached" - OnlyIfCached, - - // request directives - /// "max-age=delta" - MaxAge(u32), - /// "max-stale=delta" - MaxStale(u32), - /// "min-fresh=delta" - MinFresh(u32), - - // response directives - /// "must-revalidate" - MustRevalidate, - /// "public" - Public, - /// "private" - Private, - /// "proxy-revalidate" - ProxyRevalidate, - /// "s-maxage=delta" - SMaxAge(u32), - - /// Extension directives. Optionally include an argument. - Extension(String, Option), -} - -impl fmt::Display for CacheDirective { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::CacheDirective::*; - fmt::Display::fmt( - match *self { - NoCache => "no-cache", - NoStore => "no-store", - NoTransform => "no-transform", - OnlyIfCached => "only-if-cached", - - MaxAge(secs) => return write!(f, "max-age={}", secs), - MaxStale(secs) => return write!(f, "max-stale={}", secs), - MinFresh(secs) => return write!(f, "min-fresh={}", secs), - - MustRevalidate => "must-revalidate", - Public => "public", - Private => "private", - ProxyRevalidate => "proxy-revalidate", - SMaxAge(secs) => return write!(f, "s-maxage={}", secs), - - Extension(ref name, None) => &name[..], - Extension(ref name, Some(ref arg)) => { - return write!(f, "{}={}", name, arg) - } - }, - f, - ) - } -} - -impl FromStr for CacheDirective { - type Err = Option<::Err>; - fn from_str(s: &str) -> Result::Err>> { - use self::CacheDirective::*; - match s { - "no-cache" => Ok(NoCache), - "no-store" => Ok(NoStore), - "no-transform" => Ok(NoTransform), - "only-if-cached" => Ok(OnlyIfCached), - "must-revalidate" => Ok(MustRevalidate), - "public" => Ok(Public), - "private" => Ok(Private), - "proxy-revalidate" => Ok(ProxyRevalidate), - "" => Err(None), - _ => match s.find('=') { - Some(idx) if idx + 1 < s.len() => { - match (&s[..idx], (&s[idx + 1..]).trim_matches('"')) { - ("max-age", secs) => secs.parse().map(MaxAge).map_err(Some), - ("max-stale", secs) => secs.parse().map(MaxStale).map_err(Some), - ("min-fresh", secs) => secs.parse().map(MinFresh).map_err(Some), - ("s-maxage", secs) => secs.parse().map(SMaxAge).map_err(Some), - (left, right) => { - Ok(Extension(left.to_owned(), Some(right.to_owned()))) - } - } - } - Some(_) => Err(None), - None => Ok(Extension(s.to_owned(), None)), - }, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use header::Header; - use test::TestRequest; - - #[test] - fn test_parse_multiple_headers() { - let req = TestRequest::with_header(header::CACHE_CONTROL, "no-cache, private") - .finish(); - let cache = Header::parse(&req); - assert_eq!( - cache.ok(), - Some(CacheControl(vec![ - CacheDirective::NoCache, - CacheDirective::Private, - ])) - ) - } - - #[test] - fn test_parse_argument() { - let req = - TestRequest::with_header(header::CACHE_CONTROL, "max-age=100, private") - .finish(); - let cache = Header::parse(&req); - assert_eq!( - cache.ok(), - Some(CacheControl(vec![ - CacheDirective::MaxAge(100), - CacheDirective::Private, - ])) - ) - } - - #[test] - fn test_parse_quote_form() { - let req = - TestRequest::with_header(header::CACHE_CONTROL, "max-age=\"200\"").finish(); - let cache = Header::parse(&req); - assert_eq!( - cache.ok(), - Some(CacheControl(vec![CacheDirective::MaxAge(200)])) - ) - } - - #[test] - fn test_parse_extension() { - let req = - TestRequest::with_header(header::CACHE_CONTROL, "foo, bar=baz").finish(); - let cache = Header::parse(&req); - assert_eq!( - cache.ok(), - Some(CacheControl(vec![ - CacheDirective::Extension("foo".to_owned(), None), - CacheDirective::Extension("bar".to_owned(), Some("baz".to_owned())), - ])) - ) - } - - #[test] - fn test_parse_bad_syntax() { - let req = TestRequest::with_header(header::CACHE_CONTROL, "foo=").finish(); - let cache: Result = Header::parse(&req); - assert_eq!(cache.ok(), None) - } -} diff --git a/src/header/common/content_disposition.rs b/src/header/common/content_disposition.rs deleted file mode 100644 index 5e8cbd67..00000000 --- a/src/header/common/content_disposition.rs +++ /dev/null @@ -1,914 +0,0 @@ -// # References -// -// "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt -// "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt -// "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc7578.txt -// Browser conformance tests at: http://greenbytes.de/tech/tc2231/ -// IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml - -use header; -use header::ExtendedValue; -use header::{Header, IntoHeaderValue, Writer}; -use regex::Regex; - -use std::fmt::{self, Write}; - -/// Split at the index of the first `needle` if it exists or at the end. -fn split_once(haystack: &str, needle: char) -> (&str, &str) { - haystack.find(needle).map_or_else( - || (haystack, ""), - |sc| { - let (first, last) = haystack.split_at(sc); - (first, last.split_at(1).1) - }, - ) -} - -/// Split at the index of the first `needle` if it exists or at the end, trim the right of the -/// first part and the left of the last part. -fn split_once_and_trim(haystack: &str, needle: char) -> (&str, &str) { - let (first, last) = split_once(haystack, needle); - (first.trim_right(), last.trim_left()) -} - -/// The implied disposition of the content of the HTTP body. -#[derive(Clone, Debug, PartialEq)] -pub enum DispositionType { - /// Inline implies default processing - Inline, - /// Attachment implies that the recipient should prompt the user to save the response locally, - /// rather than process it normally (as per its media type). - Attachment, - /// Used in *multipart/form-data* as defined in - /// [RFC7578](https://tools.ietf.org/html/rfc7578) to carry the field name and the file name. - FormData, - /// Extension type. Should be handled by recipients the same way as Attachment - Ext(String), -} - -impl<'a> From<&'a str> for DispositionType { - fn from(origin: &'a str) -> DispositionType { - if origin.eq_ignore_ascii_case("inline") { - DispositionType::Inline - } else if origin.eq_ignore_ascii_case("attachment") { - DispositionType::Attachment - } else if origin.eq_ignore_ascii_case("form-data") { - DispositionType::FormData - } else { - DispositionType::Ext(origin.to_owned()) - } - } -} - -/// Parameter in [`ContentDisposition`]. -/// -/// # Examples -/// ``` -/// use actix_web::http::header::DispositionParam; -/// -/// let param = DispositionParam::Filename(String::from("sample.txt")); -/// assert!(param.is_filename()); -/// assert_eq!(param.as_filename().unwrap(), "sample.txt"); -/// ``` -#[derive(Clone, Debug, PartialEq)] -pub enum DispositionParam { - /// For [`DispositionType::FormData`] (i.e. *multipart/form-data*), the name of an field from - /// the form. - Name(String), - /// A plain file name. - Filename(String), - /// An extended file name. It must not exist for `ContentType::Formdata` according to - /// [RFC7578 Section 4.2](https://tools.ietf.org/html/rfc7578#section-4.2). - FilenameExt(ExtendedValue), - /// An unrecognized regular parameter as defined in - /// [RFC5987](https://tools.ietf.org/html/rfc5987) as *reg-parameter*, in - /// [RFC6266](https://tools.ietf.org/html/rfc6266) as *token "=" value*. Recipients should - /// ignore unrecognizable parameters. - Unknown(String, String), - /// An unrecognized extended paramater as defined in - /// [RFC5987](https://tools.ietf.org/html/rfc5987) as *ext-parameter*, in - /// [RFC6266](https://tools.ietf.org/html/rfc6266) as *ext-token "=" ext-value*. The single - /// trailling asterisk is not included. Recipients should ignore unrecognizable parameters. - UnknownExt(String, ExtendedValue), -} - -impl DispositionParam { - /// Returns `true` if the paramater is [`Name`](DispositionParam::Name). - #[inline] - pub fn is_name(&self) -> bool { - self.as_name().is_some() - } - - /// Returns `true` if the paramater is [`Filename`](DispositionParam::Filename). - #[inline] - pub fn is_filename(&self) -> bool { - self.as_filename().is_some() - } - - /// Returns `true` if the paramater is [`FilenameExt`](DispositionParam::FilenameExt). - #[inline] - pub fn is_filename_ext(&self) -> bool { - self.as_filename_ext().is_some() - } - - /// Returns `true` if the paramater is [`Unknown`](DispositionParam::Unknown) and the `name` - #[inline] - /// matches. - pub fn is_unknown>(&self, name: T) -> bool { - self.as_unknown(name).is_some() - } - - /// Returns `true` if the paramater is [`UnknownExt`](DispositionParam::UnknownExt) and the - /// `name` matches. - #[inline] - pub fn is_unknown_ext>(&self, name: T) -> bool { - self.as_unknown_ext(name).is_some() - } - - /// Returns the name if applicable. - #[inline] - pub fn as_name(&self) -> Option<&str> { - match self { - DispositionParam::Name(ref name) => Some(name.as_str()), - _ => None, - } - } - - /// Returns the filename if applicable. - #[inline] - pub fn as_filename(&self) -> Option<&str> { - match self { - DispositionParam::Filename(ref filename) => Some(filename.as_str()), - _ => None, - } - } - - /// Returns the filename* if applicable. - #[inline] - pub fn as_filename_ext(&self) -> Option<&ExtendedValue> { - match self { - DispositionParam::FilenameExt(ref value) => Some(value), - _ => None, - } - } - - /// Returns the value of the unrecognized regular parameter if it is - /// [`Unknown`](DispositionParam::Unknown) and the `name` matches. - #[inline] - pub fn as_unknown>(&self, name: T) -> Option<&str> { - match self { - DispositionParam::Unknown(ref ext_name, ref value) - if ext_name.eq_ignore_ascii_case(name.as_ref()) => - { - Some(value.as_str()) - } - _ => None, - } - } - - /// Returns the value of the unrecognized extended parameter if it is - /// [`Unknown`](DispositionParam::Unknown) and the `name` matches. - #[inline] - pub fn as_unknown_ext>(&self, name: T) -> Option<&ExtendedValue> { - match self { - DispositionParam::UnknownExt(ref ext_name, ref value) - if ext_name.eq_ignore_ascii_case(name.as_ref()) => - { - Some(value) - } - _ => None, - } - } -} - -/// A *Content-Disposition* header. It is compatible to be used either as -/// [a response header for the main body](https://mdn.io/Content-Disposition#As_a_response_header_for_the_main_body) -/// as (re)defined in [RFC6266](https://tools.ietf.org/html/rfc6266), or as -/// [a header for a multipart body](https://mdn.io/Content-Disposition#As_a_header_for_a_multipart_body) -/// as (re)defined in [RFC7587](https://tools.ietf.org/html/rfc7578). -/// -/// In a regular HTTP response, the *Content-Disposition* response header is a header indicating if -/// the content is expected to be displayed *inline* in the browser, that is, as a Web page or as -/// part of a Web page, or as an attachment, that is downloaded and saved locally, and also can be -/// used to attach additional metadata, such as the filename to use when saving the response payload -/// locally. -/// -/// In a *multipart/form-data* body, the HTTP *Content-Disposition* general header is a header that -/// can be used on the subpart of a multipart body to give information about the field it applies to. -/// The subpart is delimited by the boundary defined in the *Content-Type* header. Used on the body -/// itself, *Content-Disposition* has no effect. -/// -/// # ABNF - -/// ```text -/// content-disposition = "Content-Disposition" ":" -/// disposition-type *( ";" disposition-parm ) -/// -/// disposition-type = "inline" | "attachment" | disp-ext-type -/// ; case-insensitive -/// -/// disp-ext-type = token -/// -/// disposition-parm = filename-parm | disp-ext-parm -/// -/// filename-parm = "filename" "=" value -/// | "filename*" "=" ext-value -/// -/// disp-ext-parm = token "=" value -/// | ext-token "=" ext-value -/// -/// ext-token = -/// ``` -/// -/// **Note**: filename* [must not](https://tools.ietf.org/html/rfc7578#section-4.2) be used within -/// *multipart/form-data*. -/// -/// # Example -/// -/// ``` -/// use actix_web::http::header::{ -/// Charset, ContentDisposition, DispositionParam, DispositionType, -/// ExtendedValue, -/// }; -/// -/// let cd1 = ContentDisposition { -/// disposition: DispositionType::Attachment, -/// parameters: vec![DispositionParam::FilenameExt(ExtendedValue { -/// charset: Charset::Iso_8859_1, // The character set for the bytes of the filename -/// language_tag: None, // The optional language tag (see `language-tag` crate) -/// value: b"\xa9 Copyright 1989.txt".to_vec(), // the actual bytes of the filename -/// })], -/// }; -/// assert!(cd1.is_attachment()); -/// assert!(cd1.get_filename_ext().is_some()); -/// -/// let cd2 = ContentDisposition { -/// disposition: DispositionType::FormData, -/// parameters: vec![ -/// DispositionParam::Name(String::from("file")), -/// DispositionParam::Filename(String::from("bill.odt")), -/// ], -/// }; -/// assert_eq!(cd2.get_name(), Some("file")); // field name -/// assert_eq!(cd2.get_filename(), Some("bill.odt")); -/// ``` -/// -/// # WARN -/// If "filename" parameter is supplied, do not use the file name blindly, check and possibly -/// change to match local file system conventions if applicable, and do not use directory path -/// information that may be present. See [RFC2183](https://tools.ietf.org/html/rfc2183#section-2.3) -/// . -#[derive(Clone, Debug, PartialEq)] -pub struct ContentDisposition { - /// The disposition type - pub disposition: DispositionType, - /// Disposition parameters - pub parameters: Vec, -} - -impl ContentDisposition { - /// Parse a raw Content-Disposition header value. - pub fn from_raw(hv: &header::HeaderValue) -> Result { - // `header::from_one_raw_str` invokes `hv.to_str` which assumes `hv` contains only visible - // ASCII characters. So `hv.as_bytes` is necessary here. - let hv = String::from_utf8(hv.as_bytes().to_vec()) - .map_err(|_| ::error::ParseError::Header)?; - let (disp_type, mut left) = split_once_and_trim(hv.as_str().trim(), ';'); - if disp_type.is_empty() { - return Err(::error::ParseError::Header); - } - let mut cd = ContentDisposition { - disposition: disp_type.into(), - parameters: Vec::new(), - }; - - while !left.is_empty() { - let (param_name, new_left) = split_once_and_trim(left, '='); - if param_name.is_empty() || param_name == "*" || new_left.is_empty() { - return Err(::error::ParseError::Header); - } - left = new_left; - if param_name.ends_with('*') { - // extended parameters - let param_name = ¶m_name[..param_name.len() - 1]; // trim asterisk - let (ext_value, new_left) = split_once_and_trim(left, ';'); - left = new_left; - let ext_value = header::parse_extended_value(ext_value)?; - - let param = if param_name.eq_ignore_ascii_case("filename") { - DispositionParam::FilenameExt(ext_value) - } else { - DispositionParam::UnknownExt(param_name.to_owned(), ext_value) - }; - cd.parameters.push(param); - } else { - // regular parameters - let value = if left.starts_with('\"') { - // quoted-string: defined in RFC6266 -> RFC2616 Section 3.6 - let mut escaping = false; - let mut quoted_string = vec![]; - let mut end = None; - // search for closing quote - for (i, &c) in left.as_bytes().iter().skip(1).enumerate() { - if escaping { - escaping = false; - quoted_string.push(c); - } else if c == 0x5c { - // backslash - escaping = true; - } else if c == 0x22 { - // double quote - end = Some(i + 1); // cuz skipped 1 for the leading quote - break; - } else { - quoted_string.push(c); - } - } - left = &left[end.ok_or(::error::ParseError::Header)? + 1..]; - left = split_once(left, ';').1.trim_left(); - // In fact, it should not be Err if the above code is correct. - String::from_utf8(quoted_string).map_err(|_| ::error::ParseError::Header)? - } else { - // token: won't contains semicolon according to RFC 2616 Section 2.2 - let (token, new_left) = split_once_and_trim(left, ';'); - left = new_left; - token.to_owned() - }; - if value.is_empty() { - return Err(::error::ParseError::Header); - } - - let param = if param_name.eq_ignore_ascii_case("name") { - DispositionParam::Name(value) - } else if param_name.eq_ignore_ascii_case("filename") { - DispositionParam::Filename(value) - } else { - DispositionParam::Unknown(param_name.to_owned(), value) - }; - cd.parameters.push(param); - } - } - - Ok(cd) - } - - /// Returns `true` if it is [`Inline`](DispositionType::Inline). - pub fn is_inline(&self) -> bool { - match self.disposition { - DispositionType::Inline => true, - _ => false, - } - } - - /// Returns `true` if it is [`Attachment`](DispositionType::Attachment). - pub fn is_attachment(&self) -> bool { - match self.disposition { - DispositionType::Attachment => true, - _ => false, - } - } - - /// Returns `true` if it is [`FormData`](DispositionType::FormData). - pub fn is_form_data(&self) -> bool { - match self.disposition { - DispositionType::FormData => true, - _ => false, - } - } - - /// Returns `true` if it is [`Ext`](DispositionType::Ext) and the `disp_type` matches. - pub fn is_ext>(&self, disp_type: T) -> bool { - match self.disposition { - DispositionType::Ext(ref t) - if t.eq_ignore_ascii_case(disp_type.as_ref()) => - { - true - } - _ => false, - } - } - - /// Return the value of *name* if exists. - pub fn get_name(&self) -> Option<&str> { - self.parameters.iter().filter_map(|p| p.as_name()).nth(0) - } - - /// Return the value of *filename* if exists. - pub fn get_filename(&self) -> Option<&str> { - self.parameters - .iter() - .filter_map(|p| p.as_filename()) - .nth(0) - } - - /// Return the value of *filename\** if exists. - pub fn get_filename_ext(&self) -> Option<&ExtendedValue> { - self.parameters - .iter() - .filter_map(|p| p.as_filename_ext()) - .nth(0) - } - - /// Return the value of the parameter which the `name` matches. - pub fn get_unknown>(&self, name: T) -> Option<&str> { - let name = name.as_ref(); - self.parameters - .iter() - .filter_map(|p| p.as_unknown(name)) - .nth(0) - } - - /// Return the value of the extended parameter which the `name` matches. - pub fn get_unknown_ext>(&self, name: T) -> Option<&ExtendedValue> { - let name = name.as_ref(); - self.parameters - .iter() - .filter_map(|p| p.as_unknown_ext(name)) - .nth(0) - } -} - -impl IntoHeaderValue for ContentDisposition { - type Error = header::InvalidHeaderValueBytes; - - fn try_into(self) -> Result { - let mut writer = Writer::new(); - let _ = write!(&mut writer, "{}", self); - header::HeaderValue::from_shared(writer.take()) - } -} - -impl Header for ContentDisposition { - fn name() -> header::HeaderName { - header::CONTENT_DISPOSITION - } - - fn parse(msg: &T) -> Result { - if let Some(h) = msg.headers().get(Self::name()) { - Self::from_raw(&h) - } else { - Err(::error::ParseError::Header) - } - } -} - -impl fmt::Display for DispositionType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - DispositionType::Inline => write!(f, "inline"), - DispositionType::Attachment => write!(f, "attachment"), - DispositionType::FormData => write!(f, "form-data"), - DispositionType::Ext(ref s) => write!(f, "{}", s), - } - } -} - -impl fmt::Display for DispositionParam { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // All ASCII control charaters (0-30, 127) excepting horizontal tab, double quote, and - // backslash should be escaped in quoted-string (i.e. "foobar"). - // Ref: RFC6266 S4.1 -> RFC2616 S2.2; RFC 7578 S4.2 -> RFC2183 S2 -> ... . - lazy_static! { - static ref RE: Regex = Regex::new("[\x01-\x08\x10\x1F\x7F\"\\\\]").unwrap(); - } - match self { - DispositionParam::Name(ref value) => write!(f, "name={}", value), - DispositionParam::Filename(ref value) => { - write!(f, "filename=\"{}\"", RE.replace_all(value, "\\$0").as_ref()) - } - DispositionParam::Unknown(ref name, ref value) => write!( - f, - "{}=\"{}\"", - name, - &RE.replace_all(value, "\\$0").as_ref() - ), - DispositionParam::FilenameExt(ref ext_value) => { - write!(f, "filename*={}", ext_value) - } - DispositionParam::UnknownExt(ref name, ref ext_value) => { - write!(f, "{}*={}", name, ext_value) - } - } - } -} - -impl fmt::Display for ContentDisposition { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.disposition)?; - self.parameters - .iter() - .map(|param| write!(f, "; {}", param)) - .collect() - } -} - -#[cfg(test)] -mod tests { - use super::{ContentDisposition, DispositionParam, DispositionType}; - use header::shared::Charset; - use header::{ExtendedValue, HeaderValue}; - #[test] - fn test_from_raw_basic() { - assert!(ContentDisposition::from_raw(&HeaderValue::from_static("")).is_err()); - - let a = HeaderValue::from_static( - "form-data; dummy=3; name=upload; filename=\"sample.png\"", - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()), - DispositionParam::Name("upload".to_owned()), - DispositionParam::Filename("sample.png".to_owned()), - ], - }; - assert_eq!(a, b); - - let a = HeaderValue::from_static("attachment; filename=\"image.jpg\""); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Attachment, - parameters: vec![DispositionParam::Filename("image.jpg".to_owned())], - }; - assert_eq!(a, b); - - let a = HeaderValue::from_static("inline; filename=image.jpg"); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Inline, - parameters: vec![DispositionParam::Filename("image.jpg".to_owned())], - }; - assert_eq!(a, b); - - let a = HeaderValue::from_static( - "attachment; creation-date=\"Wed, 12 Feb 1997 16:29:51 -0500\"", - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Attachment, - parameters: vec![DispositionParam::Unknown( - String::from("creation-date"), - "Wed, 12 Feb 1997 16:29:51 -0500".to_owned(), - )], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_extended() { - let a = HeaderValue::from_static( - "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates", - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Attachment, - parameters: vec![DispositionParam::FilenameExt(ExtendedValue { - charset: Charset::Ext(String::from("UTF-8")), - language_tag: None, - value: vec![ - 0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, - b'r', b'a', b't', b'e', b's', - ], - })], - }; - assert_eq!(a, b); - - let a = HeaderValue::from_static( - "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates", - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Attachment, - parameters: vec![DispositionParam::FilenameExt(ExtendedValue { - charset: Charset::Ext(String::from("UTF-8")), - language_tag: None, - value: vec![ - 0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, - b'r', b'a', b't', b'e', b's', - ], - })], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_extra_whitespace() { - let a = HeaderValue::from_static( - "form-data ; du-mmy= 3 ; name =upload ; filename = \"sample.png\" ; ", - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Unknown("du-mmy".to_owned(), "3".to_owned()), - DispositionParam::Name("upload".to_owned()), - DispositionParam::Filename("sample.png".to_owned()), - ], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_unordered() { - let a = HeaderValue::from_static( - "form-data; dummy=3; filename=\"sample.png\" ; name=upload;", - // Actually, a trailling semolocon is not compliant. But it is fine to accept. - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()), - DispositionParam::Filename("sample.png".to_owned()), - DispositionParam::Name("upload".to_owned()), - ], - }; - assert_eq!(a, b); - - let a = HeaderValue::from_str( - "attachment; filename*=iso-8859-1''foo-%E4.html; filename=\"foo-ä.html\"", - ).unwrap(); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Attachment, - parameters: vec![ - DispositionParam::FilenameExt(ExtendedValue { - charset: Charset::Iso_8859_1, - language_tag: None, - value: b"foo-\xe4.html".to_vec(), - }), - DispositionParam::Filename("foo-ä.html".to_owned()), - ], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_only_disp() { - let a = ContentDisposition::from_raw(&HeaderValue::from_static("attachment")) - .unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Attachment, - parameters: vec![], - }; - assert_eq!(a, b); - - let a = - ContentDisposition::from_raw(&HeaderValue::from_static("inline ;")).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Inline, - parameters: vec![], - }; - assert_eq!(a, b); - - let a = ContentDisposition::from_raw(&HeaderValue::from_static( - "unknown-disp-param", - )).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Ext(String::from("unknown-disp-param")), - parameters: vec![], - }; - assert_eq!(a, b); - } - - #[test] - fn from_raw_with_mixed_case() { - let a = HeaderValue::from_str( - "InLInE; fIlenAME*=iso-8859-1''foo-%E4.html; filEName=\"foo-ä.html\"", - ).unwrap(); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::Inline, - parameters: vec![ - DispositionParam::FilenameExt(ExtendedValue { - charset: Charset::Iso_8859_1, - language_tag: None, - value: b"foo-\xe4.html".to_vec(), - }), - DispositionParam::Filename("foo-ä.html".to_owned()), - ], - }; - assert_eq!(a, b); - } - - #[test] - fn from_raw_with_unicode() { - /* RFC7578 Section 4.2: - Some commonly deployed systems use multipart/form-data with file names directly encoded - including octets outside the US-ASCII range. The encoding used for the file names is - typically UTF-8, although HTML forms will use the charset associated with the form. - - Mainstream browsers like Firefox (gecko) and Chrome use UTF-8 directly as above. - (And now, only UTF-8 is handled by this implementation.) - */ - let a = - HeaderValue::from_str("form-data; name=upload; filename=\"文件.webp\"") - .unwrap(); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Name(String::from("upload")), - DispositionParam::Filename(String::from("文件.webp")), - ], - }; - assert_eq!(a, b); - - let a = - HeaderValue::from_str("form-data; name=upload; filename=\"余固知謇謇之為患兮,å¿è€Œä¸èƒ½èˆä¹Ÿ.pptx\"").unwrap(); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Name(String::from("upload")), - DispositionParam::Filename(String::from( - "余固知謇謇之為患兮,å¿è€Œä¸èƒ½èˆä¹Ÿ.pptx", - )), - ], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_escape() { - let a = HeaderValue::from_static( - "form-data; dummy=3; name=upload; filename=\"s\\amp\\\"le.png\"", - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()), - DispositionParam::Name("upload".to_owned()), - DispositionParam::Filename( - ['s', 'a', 'm', 'p', '\"', 'l', 'e', '.', 'p', 'n', 'g'] - .iter() - .collect(), - ), - ], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_semicolon() { - let a = - HeaderValue::from_static("form-data; filename=\"A semicolon here;.pdf\""); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![DispositionParam::Filename(String::from( - "A semicolon here;.pdf", - ))], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_uncessary_percent_decode() { - let a = HeaderValue::from_static( - "form-data; name=photo; filename=\"%74%65%73%74%2e%70%6e%67\"", // Should not be decoded! - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Name("photo".to_owned()), - DispositionParam::Filename(String::from("%74%65%73%74%2e%70%6e%67")), - ], - }; - assert_eq!(a, b); - - let a = HeaderValue::from_static( - "form-data; name=photo; filename=\"%74%65%73%74.png\"", - ); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let b = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Name("photo".to_owned()), - DispositionParam::Filename(String::from("%74%65%73%74.png")), - ], - }; - assert_eq!(a, b); - } - - #[test] - fn test_from_raw_param_value_missing() { - let a = HeaderValue::from_static("form-data; name=upload ; filename="); - assert!(ContentDisposition::from_raw(&a).is_err()); - - let a = HeaderValue::from_static("attachment; dummy=; filename=invoice.pdf"); - assert!(ContentDisposition::from_raw(&a).is_err()); - - let a = HeaderValue::from_static("inline; filename= "); - assert!(ContentDisposition::from_raw(&a).is_err()); - } - - #[test] - fn test_from_raw_param_name_missing() { - let a = HeaderValue::from_static("inline; =\"test.txt\""); - assert!(ContentDisposition::from_raw(&a).is_err()); - - let a = HeaderValue::from_static("inline; =diary.odt"); - assert!(ContentDisposition::from_raw(&a).is_err()); - - let a = HeaderValue::from_static("inline; ="); - assert!(ContentDisposition::from_raw(&a).is_err()); - } - - #[test] - fn test_display_extended() { - let as_string = - "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates"; - let a = HeaderValue::from_static(as_string); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let display_rendered = format!("{}", a); - assert_eq!(as_string, display_rendered); - - let a = HeaderValue::from_static("attachment; filename=colourful.csv"); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let display_rendered = format!("{}", a); - assert_eq!( - "attachment; filename=\"colourful.csv\"".to_owned(), - display_rendered - ); - } - - #[test] - fn test_display_quote() { - let as_string = "form-data; name=upload; filename=\"Quote\\\"here.png\""; - as_string - .find(['\\', '\"'].iter().collect::().as_str()) - .unwrap(); // ensure `\"` is there - let a = HeaderValue::from_static(as_string); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let display_rendered = format!("{}", a); - assert_eq!(as_string, display_rendered); - } - - #[test] - fn test_display_space_tab() { - let as_string = "form-data; name=upload; filename=\"Space here.png\""; - let a = HeaderValue::from_static(as_string); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let display_rendered = format!("{}", a); - assert_eq!(as_string, display_rendered); - - let a: ContentDisposition = ContentDisposition { - disposition: DispositionType::Inline, - parameters: vec![DispositionParam::Filename(String::from("Tab\there.png"))], - }; - let display_rendered = format!("{}", a); - assert_eq!("inline; filename=\"Tab\x09here.png\"", display_rendered); - } - - #[test] - fn test_display_control_characters() { - /* let a = "attachment; filename=\"carriage\rreturn.png\""; - let a = HeaderValue::from_static(a); - let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); - let display_rendered = format!("{}", a); - assert_eq!( - "attachment; filename=\"carriage\\\rreturn.png\"", - display_rendered - );*/ - // No way to create a HeaderValue containing a carriage return. - - let a: ContentDisposition = ContentDisposition { - disposition: DispositionType::Inline, - parameters: vec![DispositionParam::Filename(String::from("bell\x07.png"))], - }; - let display_rendered = format!("{}", a); - assert_eq!("inline; filename=\"bell\\\x07.png\"", display_rendered); - } - - #[test] - fn test_param_methods() { - let param = DispositionParam::Filename(String::from("sample.txt")); - assert!(param.is_filename()); - assert_eq!(param.as_filename().unwrap(), "sample.txt"); - - let param = DispositionParam::Unknown(String::from("foo"), String::from("bar")); - assert!(param.is_unknown("foo")); - assert_eq!(param.as_unknown("fOo"), Some("bar")); - } - - #[test] - fn test_disposition_methods() { - let cd = ContentDisposition { - disposition: DispositionType::FormData, - parameters: vec![ - DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()), - DispositionParam::Name("upload".to_owned()), - DispositionParam::Filename("sample.png".to_owned()), - ], - }; - assert_eq!(cd.get_name(), Some("upload")); - assert_eq!(cd.get_unknown("dummy"), Some("3")); - assert_eq!(cd.get_filename(), Some("sample.png")); - assert_eq!(cd.get_unknown_ext("dummy"), None); - assert_eq!(cd.get_unknown("duMMy"), Some("3")); - } -} diff --git a/src/header/common/content_language.rs b/src/header/common/content_language.rs deleted file mode 100644 index e12d34d0..00000000 --- a/src/header/common/content_language.rs +++ /dev/null @@ -1,65 +0,0 @@ -use header::{QualityItem, CONTENT_LANGUAGE}; -use language_tags::LanguageTag; - -header! { - /// `Content-Language` header, defined in - /// [RFC7231](https://tools.ietf.org/html/rfc7231#section-3.1.3.2) - /// - /// The `Content-Language` header field describes the natural language(s) - /// of the intended audience for the representation. Note that this - /// might not be equivalent to all the languages used within the - /// representation. - /// - /// # ABNF - /// - /// ```text - /// Content-Language = 1#language-tag - /// ``` - /// - /// # Example values - /// - /// * `da` - /// * `mi, en` - /// - /// # Examples - /// - /// ```rust - /// # extern crate actix_web; - /// # #[macro_use] extern crate language_tags; - /// use actix_web::HttpResponse; - /// # use actix_web::http::header::{ContentLanguage, qitem}; - /// # - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// ContentLanguage(vec![ - /// qitem(langtag!(en)), - /// ]) - /// ); - /// # } - /// ``` - /// - /// ```rust - /// # extern crate actix_web; - /// # #[macro_use] extern crate language_tags; - /// use actix_web::HttpResponse; - /// # use actix_web::http::header::{ContentLanguage, qitem}; - /// # - /// # fn main() { - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// ContentLanguage(vec![ - /// qitem(langtag!(da)), - /// qitem(langtag!(en;;;GB)), - /// ]) - /// ); - /// # } - /// ``` - (ContentLanguage, CONTENT_LANGUAGE) => (QualityItem)+ - - test_content_language { - test_header!(test1, vec![b"da"]); - test_header!(test2, vec![b"mi, en"]); - } -} diff --git a/src/header/common/content_range.rs b/src/header/common/content_range.rs deleted file mode 100644 index 999307e2..00000000 --- a/src/header/common/content_range.rs +++ /dev/null @@ -1,210 +0,0 @@ -use error::ParseError; -use header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, Writer, - CONTENT_RANGE}; -use std::fmt::{self, Display, Write}; -use std::str::FromStr; - -header! { - /// `Content-Range` header, defined in - /// [RFC7233](http://tools.ietf.org/html/rfc7233#section-4.2) - (ContentRange, CONTENT_RANGE) => [ContentRangeSpec] - - test_content_range { - test_header!(test_bytes, - vec![b"bytes 0-499/500"], - Some(ContentRange(ContentRangeSpec::Bytes { - range: Some((0, 499)), - instance_length: Some(500) - }))); - - test_header!(test_bytes_unknown_len, - vec![b"bytes 0-499/*"], - Some(ContentRange(ContentRangeSpec::Bytes { - range: Some((0, 499)), - instance_length: None - }))); - - test_header!(test_bytes_unknown_range, - vec![b"bytes */500"], - Some(ContentRange(ContentRangeSpec::Bytes { - range: None, - instance_length: Some(500) - }))); - - test_header!(test_unregistered, - vec![b"seconds 1-2"], - Some(ContentRange(ContentRangeSpec::Unregistered { - unit: "seconds".to_owned(), - resp: "1-2".to_owned() - }))); - - test_header!(test_no_len, - vec![b"bytes 0-499"], - None::); - - test_header!(test_only_unit, - vec![b"bytes"], - None::); - - test_header!(test_end_less_than_start, - vec![b"bytes 499-0/500"], - None::); - - test_header!(test_blank, - vec![b""], - None::); - - test_header!(test_bytes_many_spaces, - vec![b"bytes 1-2/500 3"], - None::); - - test_header!(test_bytes_many_slashes, - vec![b"bytes 1-2/500/600"], - None::); - - test_header!(test_bytes_many_dashes, - vec![b"bytes 1-2-3/500"], - None::); - - } -} - -/// Content-Range, described in [RFC7233](https://tools.ietf.org/html/rfc7233#section-4.2) -/// -/// # ABNF -/// -/// ```text -/// Content-Range = byte-content-range -/// / other-content-range -/// -/// byte-content-range = bytes-unit SP -/// ( byte-range-resp / unsatisfied-range ) -/// -/// byte-range-resp = byte-range "/" ( complete-length / "*" ) -/// byte-range = first-byte-pos "-" last-byte-pos -/// unsatisfied-range = "*/" complete-length -/// -/// complete-length = 1*DIGIT -/// -/// other-content-range = other-range-unit SP other-range-resp -/// other-range-resp = *CHAR -/// ``` -#[derive(PartialEq, Clone, Debug)] -pub enum ContentRangeSpec { - /// Byte range - Bytes { - /// First and last bytes of the range, omitted if request could not be - /// satisfied - range: Option<(u64, u64)>, - - /// Total length of the instance, can be omitted if unknown - instance_length: Option, - }, - - /// Custom range, with unit not registered at IANA - Unregistered { - /// other-range-unit - unit: String, - - /// other-range-resp - resp: String, - }, -} - -fn split_in_two(s: &str, separator: char) -> Option<(&str, &str)> { - let mut iter = s.splitn(2, separator); - match (iter.next(), iter.next()) { - (Some(a), Some(b)) => Some((a, b)), - _ => None, - } -} - -impl FromStr for ContentRangeSpec { - type Err = ParseError; - - fn from_str(s: &str) -> Result { - let res = match split_in_two(s, ' ') { - Some(("bytes", resp)) => { - let (range, instance_length) = - split_in_two(resp, '/').ok_or(ParseError::Header)?; - - let instance_length = if instance_length == "*" { - None - } else { - Some(instance_length - .parse() - .map_err(|_| ParseError::Header)?) - }; - - let range = if range == "*" { - None - } else { - let (first_byte, last_byte) = - split_in_two(range, '-').ok_or(ParseError::Header)?; - let first_byte = first_byte.parse().map_err(|_| ParseError::Header)?; - let last_byte = last_byte.parse().map_err(|_| ParseError::Header)?; - if last_byte < first_byte { - return Err(ParseError::Header); - } - Some((first_byte, last_byte)) - }; - - ContentRangeSpec::Bytes { - range, - instance_length, - } - } - Some((unit, resp)) => ContentRangeSpec::Unregistered { - unit: unit.to_owned(), - resp: resp.to_owned(), - }, - _ => return Err(ParseError::Header), - }; - Ok(res) - } -} - -impl Display for ContentRangeSpec { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - ContentRangeSpec::Bytes { - range, - instance_length, - } => { - f.write_str("bytes ")?; - match range { - Some((first_byte, last_byte)) => { - write!(f, "{}-{}", first_byte, last_byte)?; - } - None => { - f.write_str("*")?; - } - }; - f.write_str("/")?; - if let Some(v) = instance_length { - write!(f, "{}", v) - } else { - f.write_str("*") - } - } - ContentRangeSpec::Unregistered { - ref unit, - ref resp, - } => { - f.write_str(unit)?; - f.write_str(" ")?; - f.write_str(resp) - } - } - } -} - -impl IntoHeaderValue for ContentRangeSpec { - type Error = InvalidHeaderValueBytes; - - fn try_into(self) -> Result { - let mut writer = Writer::new(); - let _ = write!(&mut writer, "{}", self); - HeaderValue::from_shared(writer.take()) - } -} diff --git a/src/header/common/content_type.rs b/src/header/common/content_type.rs deleted file mode 100644 index 08900e1c..00000000 --- a/src/header/common/content_type.rs +++ /dev/null @@ -1,122 +0,0 @@ -use header::CONTENT_TYPE; -use mime::{self, Mime}; - -header! { - /// `Content-Type` header, defined in - /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-3.1.1.5) - /// - /// The `Content-Type` header field indicates the media type of the - /// associated representation: either the representation enclosed in the - /// message payload or the selected representation, as determined by the - /// message semantics. The indicated media type defines both the data - /// format and how that data is intended to be processed by a recipient, - /// within the scope of the received message semantics, after any content - /// codings indicated by Content-Encoding are decoded. - /// - /// Although the `mime` crate allows the mime options to be any slice, this crate - /// forces the use of Vec. This is to make sure the same header can't have more than 1 type. If - /// this is an issue, it's possible to implement `Header` on a custom struct. - /// - /// # ABNF - /// - /// ```text - /// Content-Type = media-type - /// ``` - /// - /// # Example values - /// - /// * `text/html; charset=utf-8` - /// * `application/json` - /// - /// # Examples - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::ContentType; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// ContentType::json() - /// ); - /// # } - /// ``` - /// - /// ```rust - /// # extern crate mime; - /// # extern crate actix_web; - /// use mime::TEXT_HTML; - /// use actix_web::HttpResponse; - /// use actix_web::http::header::ContentType; - /// - /// # fn main() { - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// ContentType(TEXT_HTML) - /// ); - /// # } - /// ``` - (ContentType, CONTENT_TYPE) => [Mime] - - test_content_type { - test_header!( - test1, - vec![b"text/html"], - Some(HeaderField(TEXT_HTML))); - } -} - -impl ContentType { - /// A constructor to easily create a `Content-Type: application/json` - /// header. - #[inline] - pub fn json() -> ContentType { - ContentType(mime::APPLICATION_JSON) - } - - /// A constructor to easily create a `Content-Type: text/plain; - /// charset=utf-8` header. - #[inline] - pub fn plaintext() -> ContentType { - ContentType(mime::TEXT_PLAIN_UTF_8) - } - - /// A constructor to easily create a `Content-Type: text/html` header. - #[inline] - pub fn html() -> ContentType { - ContentType(mime::TEXT_HTML) - } - - /// A constructor to easily create a `Content-Type: text/xml` header. - #[inline] - pub fn xml() -> ContentType { - ContentType(mime::TEXT_XML) - } - - /// A constructor to easily create a `Content-Type: - /// application/www-form-url-encoded` header. - #[inline] - pub fn form_url_encoded() -> ContentType { - ContentType(mime::APPLICATION_WWW_FORM_URLENCODED) - } - /// A constructor to easily create a `Content-Type: image/jpeg` header. - #[inline] - pub fn jpeg() -> ContentType { - ContentType(mime::IMAGE_JPEG) - } - - /// A constructor to easily create a `Content-Type: image/png` header. - #[inline] - pub fn png() -> ContentType { - ContentType(mime::IMAGE_PNG) - } - - /// A constructor to easily create a `Content-Type: - /// application/octet-stream` header. - #[inline] - pub fn octet_stream() -> ContentType { - ContentType(mime::APPLICATION_OCTET_STREAM) - } -} - -impl Eq for ContentType {} diff --git a/src/header/common/date.rs b/src/header/common/date.rs deleted file mode 100644 index 88a47bc3..00000000 --- a/src/header/common/date.rs +++ /dev/null @@ -1,42 +0,0 @@ -use header::{HttpDate, DATE}; -use std::time::SystemTime; - -header! { - /// `Date` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.1.1.2) - /// - /// The `Date` header field represents the date and time at which the - /// message was originated. - /// - /// # ABNF - /// - /// ```text - /// Date = HTTP-date - /// ``` - /// - /// # Example values - /// - /// * `Tue, 15 Nov 1994 08:12:31 GMT` - /// - /// # Example - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::Date; - /// use std::time::SystemTime; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set(Date(SystemTime::now().into())); - /// ``` - (Date, DATE) => [HttpDate] - - test_date { - test_header!(test1, vec![b"Tue, 15 Nov 1994 08:12:31 GMT"]); - } -} - -impl Date { - /// Create a date instance set to the current system time - pub fn now() -> Date { - Date(SystemTime::now().into()) - } -} diff --git a/src/header/common/etag.rs b/src/header/common/etag.rs deleted file mode 100644 index 39dd908c..00000000 --- a/src/header/common/etag.rs +++ /dev/null @@ -1,96 +0,0 @@ -use header::{EntityTag, ETAG}; - -header! { - /// `ETag` header, defined in [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.3) - /// - /// The `ETag` header field in a response provides the current entity-tag - /// for the selected representation, as determined at the conclusion of - /// handling the request. An entity-tag is an opaque validator for - /// differentiating between multiple representations of the same - /// resource, regardless of whether those multiple representations are - /// due to resource state changes over time, content negotiation - /// resulting in multiple representations being valid at the same time, - /// or both. An entity-tag consists of an opaque quoted string, possibly - /// prefixed by a weakness indicator. - /// - /// # ABNF - /// - /// ```text - /// ETag = entity-tag - /// ``` - /// - /// # Example values - /// - /// * `"xyzzy"` - /// * `W/"xyzzy"` - /// * `""` - /// - /// # Examples - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{ETag, EntityTag}; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set(ETag(EntityTag::new(false, "xyzzy".to_owned()))); - /// ``` - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{ETag, EntityTag}; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set(ETag(EntityTag::new(true, "xyzzy".to_owned()))); - /// ``` - (ETag, ETAG) => [EntityTag] - - test_etag { - // From the RFC - test_header!(test1, - vec![b"\"xyzzy\""], - Some(ETag(EntityTag::new(false, "xyzzy".to_owned())))); - test_header!(test2, - vec![b"W/\"xyzzy\""], - Some(ETag(EntityTag::new(true, "xyzzy".to_owned())))); - test_header!(test3, - vec![b"\"\""], - Some(ETag(EntityTag::new(false, "".to_owned())))); - // Own tests - test_header!(test4, - vec![b"\"foobar\""], - Some(ETag(EntityTag::new(false, "foobar".to_owned())))); - test_header!(test5, - vec![b"\"\""], - Some(ETag(EntityTag::new(false, "".to_owned())))); - test_header!(test6, - vec![b"W/\"weak-etag\""], - Some(ETag(EntityTag::new(true, "weak-etag".to_owned())))); - test_header!(test7, - vec![b"W/\"\x65\x62\""], - Some(ETag(EntityTag::new(true, "\u{0065}\u{0062}".to_owned())))); - test_header!(test8, - vec![b"W/\"\""], - Some(ETag(EntityTag::new(true, "".to_owned())))); - test_header!(test9, - vec![b"no-dquotes"], - None::); - test_header!(test10, - vec![b"w/\"the-first-w-is-case-sensitive\""], - None::); - test_header!(test11, - vec![b""], - None::); - test_header!(test12, - vec![b"\"unmatched-dquotes1"], - None::); - test_header!(test13, - vec![b"unmatched-dquotes2\""], - None::); - test_header!(test14, - vec![b"matched-\"dquotes\""], - None::); - test_header!(test15, - vec![b"\""], - None::); - } -} diff --git a/src/header/common/expires.rs b/src/header/common/expires.rs deleted file mode 100644 index 4ec66b88..00000000 --- a/src/header/common/expires.rs +++ /dev/null @@ -1,39 +0,0 @@ -use header::{HttpDate, EXPIRES}; - -header! { - /// `Expires` header, defined in [RFC7234](http://tools.ietf.org/html/rfc7234#section-5.3) - /// - /// The `Expires` header field gives the date/time after which the - /// response is considered stale. - /// - /// The presence of an Expires field does not imply that the original - /// resource will change or cease to exist at, before, or after that - /// time. - /// - /// # ABNF - /// - /// ```text - /// Expires = HTTP-date - /// ``` - /// - /// # Example values - /// * `Thu, 01 Dec 1994 16:00:00 GMT` - /// - /// # Example - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::Expires; - /// use std::time::{SystemTime, Duration}; - /// - /// let mut builder = HttpResponse::Ok(); - /// let expiration = SystemTime::now() + Duration::from_secs(60 * 60 * 24); - /// builder.set(Expires(expiration.into())); - /// ``` - (Expires, EXPIRES) => [HttpDate] - - test_expires { - // Test case from RFC - test_header!(test1, vec![b"Thu, 01 Dec 1994 16:00:00 GMT"]); - } -} diff --git a/src/header/common/if_match.rs b/src/header/common/if_match.rs deleted file mode 100644 index 20a2b1e6..00000000 --- a/src/header/common/if_match.rs +++ /dev/null @@ -1,70 +0,0 @@ -use header::{EntityTag, IF_MATCH}; - -header! { - /// `If-Match` header, defined in - /// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.1) - /// - /// The `If-Match` header field makes the request method conditional on - /// the recipient origin server either having at least one current - /// representation of the target resource, when the field-value is "*", - /// or having a current representation of the target resource that has an - /// entity-tag matching a member of the list of entity-tags provided in - /// the field-value. - /// - /// An origin server MUST use the strong comparison function when - /// comparing entity-tags for `If-Match`, since the client - /// intends this precondition to prevent the method from being applied if - /// there have been any changes to the representation data. - /// - /// # ABNF - /// - /// ```text - /// If-Match = "*" / 1#entity-tag - /// ``` - /// - /// # Example values - /// - /// * `"xyzzy"` - /// * "xyzzy", "r2d2xxxx", "c3piozzzz" - /// - /// # Examples - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::IfMatch; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set(IfMatch::Any); - /// ``` - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{IfMatch, EntityTag}; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// IfMatch::Items(vec![ - /// EntityTag::new(false, "xyzzy".to_owned()), - /// EntityTag::new(false, "foobar".to_owned()), - /// EntityTag::new(false, "bazquux".to_owned()), - /// ]) - /// ); - /// ``` - (IfMatch, IF_MATCH) => {Any / (EntityTag)+} - - test_if_match { - test_header!( - test1, - vec![b"\"xyzzy\""], - Some(HeaderField::Items( - vec![EntityTag::new(false, "xyzzy".to_owned())]))); - test_header!( - test2, - vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""], - Some(HeaderField::Items( - vec![EntityTag::new(false, "xyzzy".to_owned()), - EntityTag::new(false, "r2d2xxxx".to_owned()), - EntityTag::new(false, "c3piozzzz".to_owned())]))); - test_header!(test3, vec![b"*"], Some(IfMatch::Any)); - } -} diff --git a/src/header/common/if_modified_since.rs b/src/header/common/if_modified_since.rs deleted file mode 100644 index 1914d34d..00000000 --- a/src/header/common/if_modified_since.rs +++ /dev/null @@ -1,39 +0,0 @@ -use header::{HttpDate, IF_MODIFIED_SINCE}; - -header! { - /// `If-Modified-Since` header, defined in - /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.3) - /// - /// The `If-Modified-Since` header field makes a GET or HEAD request - /// method conditional on the selected representation's modification date - /// being more recent than the date provided in the field-value. - /// Transfer of the selected representation's data is avoided if that - /// data has not changed. - /// - /// # ABNF - /// - /// ```text - /// If-Unmodified-Since = HTTP-date - /// ``` - /// - /// # Example values - /// * `Sat, 29 Oct 1994 19:43:31 GMT` - /// - /// # Example - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::IfModifiedSince; - /// use std::time::{SystemTime, Duration}; - /// - /// let mut builder = HttpResponse::Ok(); - /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); - /// builder.set(IfModifiedSince(modified.into())); - /// ``` - (IfModifiedSince, IF_MODIFIED_SINCE) => [HttpDate] - - test_if_modified_since { - // Test case from RFC - test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); - } -} diff --git a/src/header/common/if_none_match.rs b/src/header/common/if_none_match.rs deleted file mode 100644 index 124f4b8e..00000000 --- a/src/header/common/if_none_match.rs +++ /dev/null @@ -1,92 +0,0 @@ -use header::{EntityTag, IF_NONE_MATCH}; - -header! { - /// `If-None-Match` header, defined in - /// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.2) - /// - /// The `If-None-Match` header field makes the request method conditional - /// on a recipient cache or origin server either not having any current - /// representation of the target resource, when the field-value is "*", - /// or having a selected representation with an entity-tag that does not - /// match any of those listed in the field-value. - /// - /// A recipient MUST use the weak comparison function when comparing - /// entity-tags for If-None-Match (Section 2.3.2), since weak entity-tags - /// can be used for cache validation even if there have been changes to - /// the representation data. - /// - /// # ABNF - /// - /// ```text - /// If-None-Match = "*" / 1#entity-tag - /// ``` - /// - /// # Example values - /// - /// * `"xyzzy"` - /// * `W/"xyzzy"` - /// * `"xyzzy", "r2d2xxxx", "c3piozzzz"` - /// * `W/"xyzzy", W/"r2d2xxxx", W/"c3piozzzz"` - /// * `*` - /// - /// # Examples - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::IfNoneMatch; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set(IfNoneMatch::Any); - /// ``` - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{IfNoneMatch, EntityTag}; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.set( - /// IfNoneMatch::Items(vec![ - /// EntityTag::new(false, "xyzzy".to_owned()), - /// EntityTag::new(false, "foobar".to_owned()), - /// EntityTag::new(false, "bazquux".to_owned()), - /// ]) - /// ); - /// ``` - (IfNoneMatch, IF_NONE_MATCH) => {Any / (EntityTag)+} - - test_if_none_match { - test_header!(test1, vec![b"\"xyzzy\""]); - test_header!(test2, vec![b"W/\"xyzzy\""]); - test_header!(test3, vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""]); - test_header!(test4, vec![b"W/\"xyzzy\", W/\"r2d2xxxx\", W/\"c3piozzzz\""]); - test_header!(test5, vec![b"*"]); - } -} - -#[cfg(test)] -mod tests { - use super::IfNoneMatch; - use header::{EntityTag, Header, IF_NONE_MATCH}; - use test::TestRequest; - - #[test] - fn test_if_none_match() { - let mut if_none_match: Result; - - let req = TestRequest::with_header(IF_NONE_MATCH, "*").finish(); - if_none_match = Header::parse(&req); - assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Any)); - - let req = - TestRequest::with_header(IF_NONE_MATCH, &b"\"foobar\", W/\"weak-etag\""[..]) - .finish(); - - if_none_match = Header::parse(&req); - let mut entities: Vec = Vec::new(); - let foobar_etag = EntityTag::new(false, "foobar".to_owned()); - let weak_etag = EntityTag::new(true, "weak-etag".to_owned()); - entities.push(foobar_etag); - entities.push(weak_etag); - assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Items(entities))); - } -} diff --git a/src/header/common/if_range.rs b/src/header/common/if_range.rs deleted file mode 100644 index dd95b7ba..00000000 --- a/src/header/common/if_range.rs +++ /dev/null @@ -1,115 +0,0 @@ -use error::ParseError; -use header::from_one_raw_str; -use header::{EntityTag, Header, HeaderName, HeaderValue, HttpDate, IntoHeaderValue, - InvalidHeaderValueBytes, Writer}; -use http::header; -use httpmessage::HttpMessage; -use std::fmt::{self, Display, Write}; - -/// `If-Range` header, defined in [RFC7233](http://tools.ietf.org/html/rfc7233#section-3.2) -/// -/// If a client has a partial copy of a representation and wishes to have -/// an up-to-date copy of the entire representation, it could use the -/// Range header field with a conditional GET (using either or both of -/// If-Unmodified-Since and If-Match.) However, if the precondition -/// fails because the representation has been modified, the client would -/// then have to make a second request to obtain the entire current -/// representation. -/// -/// The `If-Range` header field allows a client to \"short-circuit\" the -/// second request. Informally, its meaning is as follows: if the -/// representation is unchanged, send me the part(s) that I am requesting -/// in Range; otherwise, send me the entire representation. -/// -/// # ABNF -/// -/// ```text -/// If-Range = entity-tag / HTTP-date -/// ``` -/// -/// # Example values -/// -/// * `Sat, 29 Oct 1994 19:43:31 GMT` -/// * `\"xyzzy\"` -/// -/// # Examples -/// -/// ```rust -/// use actix_web::HttpResponse; -/// use actix_web::http::header::{EntityTag, IfRange}; -/// -/// let mut builder = HttpResponse::Ok(); -/// builder.set(IfRange::EntityTag(EntityTag::new( -/// false, -/// "xyzzy".to_owned(), -/// ))); -/// ``` -/// -/// ```rust -/// use actix_web::HttpResponse; -/// use actix_web::http::header::IfRange; -/// use std::time::{Duration, SystemTime}; -/// -/// let mut builder = HttpResponse::Ok(); -/// let fetched = SystemTime::now() - Duration::from_secs(60 * 60 * 24); -/// builder.set(IfRange::Date(fetched.into())); -/// ``` -#[derive(Clone, Debug, PartialEq)] -pub enum IfRange { - /// The entity-tag the client has of the resource - EntityTag(EntityTag), - /// The date when the client retrieved the resource - Date(HttpDate), -} - -impl Header for IfRange { - fn name() -> HeaderName { - header::IF_RANGE - } - #[inline] - fn parse(msg: &T) -> Result - where - T: HttpMessage, - { - let etag: Result = - 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)); - if let Ok(date) = date { - return Ok(IfRange::Date(date)); - } - Err(ParseError::Header) - } -} - -impl Display for IfRange { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - IfRange::EntityTag(ref x) => Display::fmt(x, f), - IfRange::Date(ref x) => Display::fmt(x, f), - } - } -} - -impl IntoHeaderValue for IfRange { - type Error = InvalidHeaderValueBytes; - - fn try_into(self) -> Result { - let mut writer = Writer::new(); - let _ = write!(&mut writer, "{}", self); - HeaderValue::from_shared(writer.take()) - } -} - -#[cfg(test)] -mod test_if_range { - use super::IfRange as HeaderField; - use header::*; - use std::str; - test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); - test_header!(test2, vec![b"\"xyzzy\""]); - test_header!(test3, vec![b"this-is-invalid"], None::); -} diff --git a/src/header/common/if_unmodified_since.rs b/src/header/common/if_unmodified_since.rs deleted file mode 100644 index f87e760c..00000000 --- a/src/header/common/if_unmodified_since.rs +++ /dev/null @@ -1,40 +0,0 @@ -use header::{HttpDate, IF_UNMODIFIED_SINCE}; - -header! { - /// `If-Unmodified-Since` header, defined in - /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.4) - /// - /// The `If-Unmodified-Since` header field makes the request method - /// conditional on the selected representation's last modification date - /// being earlier than or equal to the date provided in the field-value. - /// This field accomplishes the same purpose as If-Match for cases where - /// the user agent does not have an entity-tag for the representation. - /// - /// # ABNF - /// - /// ```text - /// If-Unmodified-Since = HTTP-date - /// ``` - /// - /// # Example values - /// - /// * `Sat, 29 Oct 1994 19:43:31 GMT` - /// - /// # Example - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::IfUnmodifiedSince; - /// use std::time::{SystemTime, Duration}; - /// - /// let mut builder = HttpResponse::Ok(); - /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); - /// builder.set(IfUnmodifiedSince(modified.into())); - /// ``` - (IfUnmodifiedSince, IF_UNMODIFIED_SINCE) => [HttpDate] - - test_if_unmodified_since { - // Test case from RFC - test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); - } -} diff --git a/src/header/common/last_modified.rs b/src/header/common/last_modified.rs deleted file mode 100644 index aba82888..00000000 --- a/src/header/common/last_modified.rs +++ /dev/null @@ -1,38 +0,0 @@ -use header::{HttpDate, LAST_MODIFIED}; - -header! { - /// `Last-Modified` header, defined in - /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.2) - /// - /// The `Last-Modified` header field in a response provides a timestamp - /// indicating the date and time at which the origin server believes the - /// selected representation was last modified, as determined at the - /// conclusion of handling the request. - /// - /// # ABNF - /// - /// ```text - /// Expires = HTTP-date - /// ``` - /// - /// # Example values - /// - /// * `Sat, 29 Oct 1994 19:43:31 GMT` - /// - /// # Example - /// - /// ```rust - /// use actix_web::HttpResponse; - /// use actix_web::http::header::LastModified; - /// use std::time::{SystemTime, Duration}; - /// - /// let mut builder = HttpResponse::Ok(); - /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); - /// builder.set(LastModified(modified.into())); - /// ``` - (LastModified, LAST_MODIFIED) => [HttpDate] - - test_last_modified { - // Test case from RFC - test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);} -} diff --git a/src/header/common/mod.rs b/src/header/common/mod.rs deleted file mode 100644 index e6185b5a..00000000 --- a/src/header/common/mod.rs +++ /dev/null @@ -1,350 +0,0 @@ -//! A Collection of Header implementations for common HTTP Headers. -//! -//! ## Mime -//! -//! Several header fields use MIME values for their contents. Keeping with the -//! strongly-typed theme, the [mime](https://docs.rs/mime) crate -//! is used, such as `ContentType(pub Mime)`. -#![cfg_attr(rustfmt, rustfmt_skip)] - -pub use self::accept_charset::AcceptCharset; -//pub use self::accept_encoding::AcceptEncoding; -pub use self::accept_language::AcceptLanguage; -pub use self::accept::Accept; -pub use self::allow::Allow; -pub use self::cache_control::{CacheControl, CacheDirective}; -pub use self::content_disposition::{ContentDisposition, DispositionType, DispositionParam}; -pub use self::content_language::ContentLanguage; -pub use self::content_range::{ContentRange, ContentRangeSpec}; -pub use self::content_type::ContentType; -pub use self::date::Date; -pub use self::etag::ETag; -pub use self::expires::Expires; -pub use self::if_match::IfMatch; -pub use self::if_modified_since::IfModifiedSince; -pub use self::if_none_match::IfNoneMatch; -pub use self::if_range::IfRange; -pub use self::if_unmodified_since::IfUnmodifiedSince; -pub use self::last_modified::LastModified; -//pub use self::range::{Range, ByteRangeSpec}; - -#[doc(hidden)] -#[macro_export] -macro_rules! __hyper__deref { - ($from:ty => $to:ty) => { - impl ::std::ops::Deref for $from { - type Target = $to; - - #[inline] - fn deref(&self) -> &$to { - &self.0 - } - } - - impl ::std::ops::DerefMut for $from { - #[inline] - fn deref_mut(&mut self) -> &mut $to { - &mut self.0 - } - } - } -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __hyper__tm { - ($id:ident, $tm:ident{$($tf:item)*}) => { - #[allow(unused_imports)] - #[cfg(test)] - mod $tm{ - use std::str; - use http::Method; - use $crate::header::*; - use $crate::mime::*; - use super::$id as HeaderField; - $($tf)* - } - - } -} - -#[doc(hidden)] -#[macro_export] -macro_rules! test_header { - ($id:ident, $raw:expr) => { - #[test] - fn $id() { - use test; - let raw = $raw; - let a: Vec> = raw.iter().map(|x| x.to_vec()).collect(); - let mut req = test::TestRequest::default(); - for item in a { - req = req.header(HeaderField::name(), item); - } - let req = req.finish(); - let value = HeaderField::parse(&req); - let result = format!("{}", value.unwrap()); - let expected = String::from_utf8(raw[0].to_vec()).unwrap(); - let result_cmp: Vec = result - .to_ascii_lowercase() - .split(' ') - .map(|x| x.to_owned()) - .collect(); - let expected_cmp: Vec = expected - .to_ascii_lowercase() - .split(' ') - .map(|x| x.to_owned()) - .collect(); - assert_eq!(result_cmp.concat(), expected_cmp.concat()); - } - }; - ($id:ident, $raw:expr, $typed:expr) => { - #[test] - fn $id() { - use $crate::test; - let a: Vec> = $raw.iter().map(|x| x.to_vec()).collect(); - let mut req = test::TestRequest::default(); - for item in a { - req = req.header(HeaderField::name(), item); - } - let req = req.finish(); - let val = HeaderField::parse(&req); - let typed: Option = $typed; - // Test parsing - assert_eq!(val.ok(), typed); - // Test formatting - if typed.is_some() { - let raw = &($raw)[..]; - let mut iter = raw.iter().map(|b|str::from_utf8(&b[..]).unwrap()); - let mut joined = String::new(); - joined.push_str(iter.next().unwrap()); - for s in iter { - joined.push_str(", "); - joined.push_str(s); - } - assert_eq!(format!("{}", typed.unwrap()), joined); - } - } - } -} - -#[macro_export] -macro_rules! header { - // $a:meta: Attributes associated with the header item (usually docs) - // $id:ident: Identifier of the header - // $n:expr: Lowercase name of the header - // $nn:expr: Nice name of the header - - // List header, zero or more items - ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)*) => { - $(#[$a])* - #[derive(Clone, Debug, PartialEq)] - pub struct $id(pub Vec<$item>); - __hyper__deref!($id => Vec<$item>); - impl $crate::header::Header for $id { - #[inline] - fn name() -> $crate::header::HeaderName { - $name - } - #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { - $crate::header::from_comma_delimited( - msg.headers().get_all(Self::name())).map($id) - } - } - impl ::std::fmt::Display for $id { - #[inline] - fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - $crate::header::fmt_comma_delimited(f, &self.0[..]) - } - } - impl $crate::header::IntoHeaderValue for $id { - type Error = $crate::header::InvalidHeaderValueBytes; - - fn try_into(self) -> Result<$crate::header::HeaderValue, Self::Error> { - use std::fmt::Write; - let mut writer = $crate::header::Writer::new(); - let _ = write!(&mut writer, "{}", self); - $crate::header::HeaderValue::from_shared(writer.take()) - } - } - }; - // List header, one or more items - ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)+) => { - $(#[$a])* - #[derive(Clone, Debug, PartialEq)] - pub struct $id(pub Vec<$item>); - __hyper__deref!($id => Vec<$item>); - impl $crate::header::Header for $id { - #[inline] - fn name() -> $crate::header::HeaderName { - $name - } - #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { - $crate::header::from_comma_delimited( - msg.headers().get_all(Self::name())).map($id) - } - } - impl ::std::fmt::Display for $id { - #[inline] - fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - $crate::header::fmt_comma_delimited(f, &self.0[..]) - } - } - impl $crate::header::IntoHeaderValue for $id { - type Error = $crate::header::InvalidHeaderValueBytes; - - fn try_into(self) -> Result<$crate::header::HeaderValue, Self::Error> { - use std::fmt::Write; - let mut writer = $crate::header::Writer::new(); - let _ = write!(&mut writer, "{}", self); - $crate::header::HeaderValue::from_shared(writer.take()) - } - } - }; - // Single value header - ($(#[$a:meta])*($id:ident, $name:expr) => [$value:ty]) => { - $(#[$a])* - #[derive(Clone, Debug, PartialEq)] - pub struct $id(pub $value); - __hyper__deref!($id => $value); - impl $crate::header::Header for $id { - #[inline] - fn name() -> $crate::header::HeaderName { - $name - } - #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { - $crate::header::from_one_raw_str( - msg.headers().get(Self::name())).map($id) - } - } - impl ::std::fmt::Display for $id { - #[inline] - fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - ::std::fmt::Display::fmt(&self.0, f) - } - } - impl $crate::header::IntoHeaderValue for $id { - type Error = $crate::header::InvalidHeaderValueBytes; - - fn try_into(self) -> Result<$crate::header::HeaderValue, Self::Error> { - self.0.try_into() - } - } - }; - // List header, one or more items with "*" option - ($(#[$a:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+}) => { - $(#[$a])* - #[derive(Clone, Debug, PartialEq)] - pub enum $id { - /// Any value is a match - Any, - /// Only the listed items are a match - Items(Vec<$item>), - } - impl $crate::header::Header for $id { - #[inline] - fn name() -> $crate::header::HeaderName { - $name - } - #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { - let any = msg.headers().get(Self::name()).and_then(|hdr| { - hdr.to_str().ok().and_then(|hdr| Some(hdr.trim() == "*"))}); - - if let Some(true) = any { - Ok($id::Any) - } else { - Ok($id::Items( - $crate::header::from_comma_delimited( - msg.headers().get_all(Self::name()))?)) - } - } - } - impl ::std::fmt::Display for $id { - #[inline] - fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - match *self { - $id::Any => f.write_str("*"), - $id::Items(ref fields) => $crate::header::fmt_comma_delimited( - f, &fields[..]) - } - } - } - impl $crate::header::IntoHeaderValue for $id { - type Error = $crate::header::InvalidHeaderValueBytes; - - fn try_into(self) -> Result<$crate::header::HeaderValue, Self::Error> { - use std::fmt::Write; - let mut writer = $crate::header::Writer::new(); - let _ = write!(&mut writer, "{}", self); - $crate::header::HeaderValue::from_shared(writer.take()) - } - } - }; - - // optional test module - ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)* $tm:ident{$($tf:item)*}) => { - header! { - $(#[$a])* - ($id, $name) => ($item)* - } - - __hyper__tm! { $id, $tm { $($tf)* }} - }; - ($(#[$a:meta])*($id:ident, $n:expr) => ($item:ty)+ $tm:ident{$($tf:item)*}) => { - header! { - $(#[$a])* - ($id, $n) => ($item)+ - } - - __hyper__tm! { $id, $tm { $($tf)* }} - }; - ($(#[$a:meta])*($id:ident, $name:expr) => [$item:ty] $tm:ident{$($tf:item)*}) => { - header! { - $(#[$a])* ($id, $name) => [$item] - } - - __hyper__tm! { $id, $tm { $($tf)* }} - }; - ($(#[$a:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+} $tm:ident{$($tf:item)*}) => { - header! { - $(#[$a])* - ($id, $name) => {Any / ($item)+} - } - - __hyper__tm! { $id, $tm { $($tf)* }} - }; -} - - -mod accept_charset; -//mod accept_encoding; -mod accept_language; -mod accept; -mod allow; -mod cache_control; -mod content_disposition; -mod content_language; -mod content_range; -mod content_type; -mod date; -mod etag; -mod expires; -mod if_match; -mod if_modified_since; -mod if_none_match; -mod if_range; -mod if_unmodified_since; -mod last_modified; -//mod range; diff --git a/src/header/common/range.rs b/src/header/common/range.rs deleted file mode 100644 index 71718fc7..00000000 --- a/src/header/common/range.rs +++ /dev/null @@ -1,434 +0,0 @@ -use std::fmt::{self, Display}; -use std::str::FromStr; - -use header::parsing::from_one_raw_str; -use header::{Header, Raw}; - -/// `Range` header, defined in [RFC7233](https://tools.ietf.org/html/rfc7233#section-3.1) -/// -/// The "Range" header field on a GET request modifies the method -/// semantics to request transfer of only one or more subranges of the -/// selected representation data, rather than the entire selected -/// representation data. -/// -/// # ABNF -/// -/// ```text -/// Range = byte-ranges-specifier / other-ranges-specifier -/// other-ranges-specifier = other-range-unit "=" other-range-set -/// other-range-set = 1*VCHAR -/// -/// bytes-unit = "bytes" -/// -/// byte-ranges-specifier = bytes-unit "=" byte-range-set -/// byte-range-set = 1#(byte-range-spec / suffix-byte-range-spec) -/// byte-range-spec = first-byte-pos "-" [last-byte-pos] -/// first-byte-pos = 1*DIGIT -/// last-byte-pos = 1*DIGIT -/// ``` -/// -/// # Example values -/// -/// * `bytes=1000-` -/// * `bytes=-2000` -/// * `bytes=0-1,30-40` -/// * `bytes=0-10,20-90,-100` -/// * `custom_unit=0-123` -/// * `custom_unit=xxx-yyy` -/// -/// # Examples -/// -/// ``` -/// use hyper::header::{Headers, Range, ByteRangeSpec}; -/// -/// let mut headers = Headers::new(); -/// headers.set(Range::Bytes( -/// vec![ByteRangeSpec::FromTo(1, 100), ByteRangeSpec::AllFrom(200)] -/// )); -/// -/// headers.clear(); -/// headers.set(Range::Unregistered("letters".to_owned(), "a-f".to_owned())); -/// ``` -/// -/// ``` -/// use hyper::header::{Headers, Range}; -/// -/// let mut headers = Headers::new(); -/// headers.set(Range::bytes(1, 100)); -/// -/// headers.clear(); -/// headers.set(Range::bytes_multi(vec![(1, 100), (200, 300)])); -/// ``` -#[derive(PartialEq, Clone, Debug)] -pub enum Range { - /// Byte range - Bytes(Vec), - /// Custom range, with unit not registered at IANA - /// (`other-range-unit`: String , `other-range-set`: String) - Unregistered(String, String), -} - -/// Each `Range::Bytes` header can contain one or more `ByteRangeSpecs`. -/// Each `ByteRangeSpec` defines a range of bytes to fetch -#[derive(PartialEq, Clone, Debug)] -pub enum ByteRangeSpec { - /// Get all bytes between x and y ("x-y") - FromTo(u64, u64), - /// Get all bytes starting from x ("x-") - AllFrom(u64), - /// Get last x bytes ("-x") - Last(u64), -} - -impl ByteRangeSpec { - /// Given the full length of the entity, attempt to normalize the byte range - /// into an satisfiable end-inclusive (from, to) range. - /// - /// The resulting range is guaranteed to be a satisfiable range within the - /// bounds of `0 <= from <= to < full_length`. - /// - /// If the byte range is deemed unsatisfiable, `None` is returned. - /// An unsatisfiable range is generally cause for a server to either reject - /// the client request with a `416 Range Not Satisfiable` status code, or to - /// simply ignore the range header and serve the full entity using a `200 - /// OK` status code. - /// - /// This function closely follows [RFC 7233][1] section 2.1. - /// As such, it considers ranges to be satisfiable if they meet the - /// following conditions: - /// - /// > If a valid byte-range-set includes at least one byte-range-spec with - /// a first-byte-pos that is less than the current length of the - /// representation, or at least one suffix-byte-range-spec with a - /// non-zero suffix-length, then the byte-range-set is satisfiable. - /// Otherwise, the byte-range-set is unsatisfiable. - /// - /// The function also computes remainder ranges based on the RFC: - /// - /// > If the last-byte-pos value is - /// absent, or if the value is greater than or equal to the current - /// length of the representation data, the byte range is interpreted as - /// the remainder of the representation (i.e., the server replaces the - /// value of last-byte-pos with a value that is one less than the current - /// length of the selected representation). - /// - /// [1]: https://tools.ietf.org/html/rfc7233 - pub fn to_satisfiable_range(&self, full_length: u64) -> Option<(u64, u64)> { - // If the full length is zero, there is no satisfiable end-inclusive range. - if full_length == 0 { - return None; - } - match self { - &ByteRangeSpec::FromTo(from, to) => { - if from < full_length && from <= to { - Some((from, ::std::cmp::min(to, full_length - 1))) - } else { - None - } - } - &ByteRangeSpec::AllFrom(from) => { - if from < full_length { - Some((from, full_length - 1)) - } else { - None - } - } - &ByteRangeSpec::Last(last) => { - if last > 0 { - // From the RFC: If the selected representation is shorter - // than the specified suffix-length, - // the entire representation is used. - if last > full_length { - Some((0, full_length - 1)) - } else { - Some((full_length - last, full_length - 1)) - } - } else { - None - } - } - } - } -} - -impl Range { - /// Get the most common byte range header ("bytes=from-to") - pub fn bytes(from: u64, to: u64) -> Range { - Range::Bytes(vec![ByteRangeSpec::FromTo(from, to)]) - } - - /// Get byte range header with multiple subranges - /// ("bytes=from1-to1,from2-to2,fromX-toX") - pub fn bytes_multi(ranges: Vec<(u64, u64)>) -> Range { - Range::Bytes( - ranges - .iter() - .map(|r| ByteRangeSpec::FromTo(r.0, r.1)) - .collect(), - ) - } -} - -impl fmt::Display for ByteRangeSpec { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - ByteRangeSpec::FromTo(from, to) => write!(f, "{}-{}", from, to), - ByteRangeSpec::Last(pos) => write!(f, "-{}", pos), - ByteRangeSpec::AllFrom(pos) => write!(f, "{}-", pos), - } - } -} - -impl fmt::Display for Range { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Range::Bytes(ref ranges) => { - try!(write!(f, "bytes=")); - - for (i, range) in ranges.iter().enumerate() { - if i != 0 { - try!(f.write_str(",")); - } - try!(Display::fmt(range, f)); - } - Ok(()) - } - Range::Unregistered(ref unit, ref range_str) => { - write!(f, "{}={}", unit, range_str) - } - } - } -} - -impl FromStr for Range { - type Err = ::Error; - - fn from_str(s: &str) -> ::Result { - let mut iter = s.splitn(2, '='); - - match (iter.next(), iter.next()) { - (Some("bytes"), Some(ranges)) => { - let ranges = from_comma_delimited(ranges); - if ranges.is_empty() { - return Err(::Error::Header); - } - Ok(Range::Bytes(ranges)) - } - (Some(unit), Some(range_str)) if unit != "" && range_str != "" => Ok( - Range::Unregistered(unit.to_owned(), range_str.to_owned()), - ), - _ => Err(::Error::Header), - } - } -} - -impl FromStr for ByteRangeSpec { - type Err = ::Error; - - fn from_str(s: &str) -> ::Result { - let mut parts = s.splitn(2, '-'); - - match (parts.next(), parts.next()) { - (Some(""), Some(end)) => end.parse() - .or(Err(::Error::Header)) - .map(ByteRangeSpec::Last), - (Some(start), Some("")) => start - .parse() - .or(Err(::Error::Header)) - .map(ByteRangeSpec::AllFrom), - (Some(start), Some(end)) => match (start.parse(), end.parse()) { - (Ok(start), Ok(end)) if start <= end => { - Ok(ByteRangeSpec::FromTo(start, end)) - } - _ => Err(::Error::Header), - }, - _ => Err(::Error::Header), - } - } -} - -fn from_comma_delimited(s: &str) -> Vec { - s.split(',') - .filter_map(|x| match x.trim() { - "" => None, - y => Some(y), - }) - .filter_map(|x| x.parse().ok()) - .collect() -} - -impl Header for Range { - fn header_name() -> &'static str { - static NAME: &'static str = "Range"; - NAME - } - - fn parse_header(raw: &Raw) -> ::Result { - from_one_raw_str(raw) - } - - fn fmt_header(&self, f: &mut ::header::Formatter) -> fmt::Result { - f.fmt_line(self) - } -} - -#[test] -fn test_parse_bytes_range_valid() { - let r: Range = Header::parse_header(&"bytes=1-100".into()).unwrap(); - let r2: Range = Header::parse_header(&"bytes=1-100,-".into()).unwrap(); - let r3 = Range::bytes(1, 100); - assert_eq!(r, r2); - assert_eq!(r2, r3); - - let r: Range = Header::parse_header(&"bytes=1-100,200-".into()).unwrap(); - let r2: Range = - Header::parse_header(&"bytes= 1-100 , 101-xxx, 200- ".into()).unwrap(); - let r3 = Range::Bytes(vec![ - ByteRangeSpec::FromTo(1, 100), - ByteRangeSpec::AllFrom(200), - ]); - assert_eq!(r, r2); - assert_eq!(r2, r3); - - let r: Range = Header::parse_header(&"bytes=1-100,-100".into()).unwrap(); - let r2: Range = Header::parse_header(&"bytes=1-100, ,,-100".into()).unwrap(); - let r3 = Range::Bytes(vec![ - ByteRangeSpec::FromTo(1, 100), - ByteRangeSpec::Last(100), - ]); - assert_eq!(r, r2); - assert_eq!(r2, r3); - - let r: Range = Header::parse_header(&"custom=1-100,-100".into()).unwrap(); - let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned()); - assert_eq!(r, r2); -} - -#[test] -fn test_parse_unregistered_range_valid() { - let r: Range = Header::parse_header(&"custom=1-100,-100".into()).unwrap(); - let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned()); - assert_eq!(r, r2); - - let r: Range = Header::parse_header(&"custom=abcd".into()).unwrap(); - let r2 = Range::Unregistered("custom".to_owned(), "abcd".to_owned()); - assert_eq!(r, r2); - - let r: Range = Header::parse_header(&"custom=xxx-yyy".into()).unwrap(); - let r2 = Range::Unregistered("custom".to_owned(), "xxx-yyy".to_owned()); - assert_eq!(r, r2); -} - -#[test] -fn test_parse_invalid() { - let r: ::Result = Header::parse_header(&"bytes=1-a,-".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"bytes=1-2-3".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"abc".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"bytes=1-100=".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"bytes=".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"custom=".into()); - assert_eq!(r.ok(), None); - - let r: ::Result = Header::parse_header(&"=1-100".into()); - assert_eq!(r.ok(), None); -} - -#[test] -fn test_fmt() { - use header::Headers; - - let mut headers = Headers::new(); - - headers.set(Range::Bytes(vec![ - ByteRangeSpec::FromTo(0, 1000), - ByteRangeSpec::AllFrom(2000), - ])); - assert_eq!(&headers.to_string(), "Range: bytes=0-1000,2000-\r\n"); - - headers.clear(); - headers.set(Range::Bytes(vec![])); - - assert_eq!(&headers.to_string(), "Range: bytes=\r\n"); - - headers.clear(); - headers.set(Range::Unregistered( - "custom".to_owned(), - "1-xxx".to_owned(), - )); - - assert_eq!(&headers.to_string(), "Range: custom=1-xxx\r\n"); -} - -#[test] -fn test_byte_range_spec_to_satisfiable_range() { - assert_eq!( - Some((0, 0)), - ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(3) - ); - assert_eq!( - Some((1, 2)), - ByteRangeSpec::FromTo(1, 2).to_satisfiable_range(3) - ); - assert_eq!( - Some((1, 2)), - ByteRangeSpec::FromTo(1, 5).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::FromTo(3, 3).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::FromTo(2, 1).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(0) - ); - - assert_eq!( - Some((0, 2)), - ByteRangeSpec::AllFrom(0).to_satisfiable_range(3) - ); - assert_eq!( - Some((2, 2)), - ByteRangeSpec::AllFrom(2).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::AllFrom(3).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::AllFrom(5).to_satisfiable_range(3) - ); - assert_eq!( - None, - ByteRangeSpec::AllFrom(0).to_satisfiable_range(0) - ); - - assert_eq!( - Some((1, 2)), - ByteRangeSpec::Last(2).to_satisfiable_range(3) - ); - assert_eq!( - Some((2, 2)), - ByteRangeSpec::Last(1).to_satisfiable_range(3) - ); - assert_eq!( - Some((0, 2)), - ByteRangeSpec::Last(5).to_satisfiable_range(3) - ); - assert_eq!(None, ByteRangeSpec::Last(0).to_satisfiable_range(3)); - assert_eq!(None, ByteRangeSpec::Last(2).to_satisfiable_range(0)); -} diff --git a/src/header/mod.rs b/src/header/mod.rs deleted file mode 100644 index 74e4b03e..00000000 --- a/src/header/mod.rs +++ /dev/null @@ -1,471 +0,0 @@ -//! Various http headers -// This is mostly copy of [hyper](https://github.com/hyperium/hyper/tree/master/src/header) - -use std::fmt; -use std::str::FromStr; - -use bytes::{Bytes, BytesMut}; -use mime::Mime; -use modhttp::header::GetAll; -use modhttp::Error as HttpError; -use percent_encoding; - -pub use modhttp::header::*; - -use error::ParseError; -use httpmessage::HttpMessage; - -mod common; -mod shared; -#[doc(hidden)] -pub use self::common::*; -#[doc(hidden)] -pub use self::shared::*; - -#[doc(hidden)] -/// A trait for any object that will represent a header field and value. -pub trait Header -where - Self: IntoHeaderValue, -{ - /// Returns the name of the header field - fn name() -> HeaderName; - - /// Parse a header - 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. - type Error: Into; - - /// Try to convert value to a Header value. - fn try_into(self) -> Result; -} - -impl IntoHeaderValue for HeaderValue { - type Error = InvalidHeaderValue; - - #[inline] - fn try_into(self) -> Result { - Ok(self) - } -} - -impl<'a> IntoHeaderValue for &'a str { - type Error = InvalidHeaderValue; - - #[inline] - fn try_into(self) -> Result { - self.parse() - } -} - -impl<'a> IntoHeaderValue for &'a [u8] { - type Error = InvalidHeaderValue; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::from_bytes(self) - } -} - -impl IntoHeaderValue for Bytes { - type Error = InvalidHeaderValueBytes; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::from_shared(self) - } -} - -impl IntoHeaderValue for Vec { - type Error = InvalidHeaderValueBytes; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::from_shared(Bytes::from(self)) - } -} - -impl IntoHeaderValue for String { - type Error = InvalidHeaderValueBytes; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::from_shared(Bytes::from(self)) - } -} - -impl IntoHeaderValue for Mime { - type Error = InvalidHeaderValueBytes; - - #[inline] - fn try_into(self) -> Result { - HeaderValue::from_shared(Bytes::from(format!("{}", self))) - } -} - -/// Represents supported types of content encodings -#[derive(Copy, Clone, PartialEq, Debug)] -pub enum ContentEncoding { - /// Automatically select encoding based on encoding negotiation - Auto, - /// A format using the Brotli algorithm - #[cfg(feature = "brotli")] - Br, - /// A format using the zlib structure with deflate algorithm - #[cfg(feature = "flate2")] - Deflate, - /// Gzip algorithm - #[cfg(feature = "flate2")] - Gzip, - /// Indicates the identity function (i.e. no compression, nor modification) - Identity, -} - -impl ContentEncoding { - #[inline] - /// Is the content compressed? - pub fn is_compression(self) -> bool { - match self { - ContentEncoding::Identity | ContentEncoding::Auto => false, - _ => true, - } - } - - #[inline] - /// Convert content encoding to string - pub fn as_str(self) -> &'static str { - match self { - #[cfg(feature = "brotli")] - ContentEncoding::Br => "br", - #[cfg(feature = "flate2")] - ContentEncoding::Gzip => "gzip", - #[cfg(feature = "flate2")] - ContentEncoding::Deflate => "deflate", - ContentEncoding::Identity | ContentEncoding::Auto => "identity", - } - } - - #[inline] - /// default quality value - pub fn quality(self) -> f64 { - match self { - #[cfg(feature = "brotli")] - ContentEncoding::Br => 1.1, - #[cfg(feature = "flate2")] - ContentEncoding::Gzip => 1.0, - #[cfg(feature = "flate2")] - ContentEncoding::Deflate => 0.9, - ContentEncoding::Identity | ContentEncoding::Auto => 0.1, - } - } -} - -// TODO: remove memory allocation -impl<'a> From<&'a str> for ContentEncoding { - fn from(s: &'a str) -> ContentEncoding { - match AsRef::::as_ref(&s.trim().to_lowercase()) { - #[cfg(feature = "brotli")] - "br" => ContentEncoding::Br, - #[cfg(feature = "flate2")] - "gzip" => ContentEncoding::Gzip, - #[cfg(feature = "flate2")] - "deflate" => ContentEncoding::Deflate, - _ => ContentEncoding::Identity, - } - } -} - -#[doc(hidden)] -pub(crate) struct Writer { - buf: BytesMut, -} - -impl Writer { - fn new() -> Writer { - Writer { - buf: BytesMut::new(), - } - } - fn take(&mut self) -> Bytes { - self.buf.take().freeze() - } -} - -impl fmt::Write for Writer { - #[inline] - fn write_str(&mut self, s: &str) -> fmt::Result { - self.buf.extend_from_slice(s.as_bytes()); - Ok(()) - } - - #[inline] - fn write_fmt(&mut self, args: fmt::Arguments) -> fmt::Result { - fmt::write(self, args) - } -} - -#[inline] -#[doc(hidden)] -/// Reads a comma-delimited raw header into a Vec. -pub fn from_comma_delimited( - all: GetAll, -) -> Result, ParseError> { - let mut result = Vec::new(); - for h in all { - let s = h.to_str().map_err(|_| ParseError::Header)?; - result.extend( - s.split(',') - .filter_map(|x| match x.trim() { - "" => None, - y => Some(y), - }).filter_map(|x| x.trim().parse().ok()), - ) - } - Ok(result) -} - -#[inline] -#[doc(hidden)] -/// Reads a single string when parsing a header. -pub fn from_one_raw_str(val: Option<&HeaderValue>) -> Result { - if let Some(line) = val { - let line = line.to_str().map_err(|_| ParseError::Header)?; - if !line.is_empty() { - return T::from_str(line).or(Err(ParseError::Header)); - } - } - Err(ParseError::Header) -} - -#[inline] -#[doc(hidden)] -/// Format an array into a comma-delimited string. -pub fn fmt_comma_delimited(f: &mut fmt::Formatter, parts: &[T]) -> fmt::Result -where - T: fmt::Display, -{ - let mut iter = parts.iter(); - if let Some(part) = iter.next() { - fmt::Display::fmt(part, f)?; - } - for part in iter { - f.write_str(", ")?; - fmt::Display::fmt(part, f)?; - } - Ok(()) -} - -// From hyper v0.11.27 src/header/parsing.rs - -/// The value part of an extended parameter consisting of three parts: -/// the REQUIRED character set name (`charset`), the OPTIONAL language information (`language_tag`), -/// and a character sequence representing the actual value (`value`), separated by single quote -/// characters. It is defined in [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2). -#[derive(Clone, Debug, PartialEq)] -pub struct ExtendedValue { - /// The character set that is used to encode the `value` to a string. - pub charset: Charset, - /// The human language details of the `value`, if available. - pub language_tag: Option, - /// The parameter value, as expressed in octets. - pub value: Vec, -} - -/// Parses extended header parameter values (`ext-value`), as defined in -/// [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2). -/// -/// Extended values are denoted by parameter names that end with `*`. -/// -/// ## ABNF -/// -/// ```text -/// ext-value = charset "'" [ language ] "'" value-chars -/// ; like RFC 2231's -/// ; (see [RFC2231], Section 7) -/// -/// charset = "UTF-8" / "ISO-8859-1" / mime-charset -/// -/// mime-charset = 1*mime-charsetc -/// mime-charsetc = ALPHA / DIGIT -/// / "!" / "#" / "$" / "%" / "&" -/// / "+" / "-" / "^" / "_" / "`" -/// / "{" / "}" / "~" -/// ; as in Section 2.3 of [RFC2978] -/// ; except that the single quote is not included -/// ; SHOULD be registered in the IANA charset registry -/// -/// language = -/// -/// value-chars = *( pct-encoded / attr-char ) -/// -/// pct-encoded = "%" HEXDIG HEXDIG -/// ; see [RFC3986], Section 2.1 -/// -/// attr-char = ALPHA / DIGIT -/// / "!" / "#" / "$" / "&" / "+" / "-" / "." -/// / "^" / "_" / "`" / "|" / "~" -/// ; token except ( "*" / "'" / "%" ) -/// ``` -pub fn parse_extended_value(val: &str) -> Result { - // Break into three pieces separated by the single-quote character - let mut parts = val.splitn(3, '\''); - - // Interpret the first piece as a Charset - let charset: Charset = match parts.next() { - None => return Err(::error::ParseError::Header), - Some(n) => FromStr::from_str(n).map_err(|_| ::error::ParseError::Header)?, - }; - - // Interpret the second piece as a language tag - let language_tag: Option = match parts.next() { - None => return Err(::error::ParseError::Header), - Some("") => None, - Some(s) => match s.parse() { - Ok(lt) => Some(lt), - Err(_) => return Err(::error::ParseError::Header), - }, - }; - - // Interpret the third piece as a sequence of value characters - let value: Vec = match parts.next() { - None => return Err(::error::ParseError::Header), - Some(v) => percent_encoding::percent_decode(v.as_bytes()).collect(), - }; - - Ok(ExtendedValue { - value, - charset, - language_tag, - }) -} - -impl fmt::Display for ExtendedValue { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let encoded_value = percent_encoding::percent_encode( - &self.value[..], - self::percent_encoding_http::HTTP_VALUE, - ); - if let Some(ref lang) = self.language_tag { - write!(f, "{}'{}'{}", self.charset, lang, encoded_value) - } else { - write!(f, "{}''{}", self.charset, encoded_value) - } - } -} - -/// Percent encode a sequence of bytes with a character set defined in -/// [https://tools.ietf.org/html/rfc5987#section-3.2][url] -/// -/// [url]: https://tools.ietf.org/html/rfc5987#section-3.2 -pub fn http_percent_encode(f: &mut fmt::Formatter, bytes: &[u8]) -> fmt::Result { - let encoded = - percent_encoding::percent_encode(bytes, self::percent_encoding_http::HTTP_VALUE); - fmt::Display::fmt(&encoded, f) -} -mod percent_encoding_http { - use percent_encoding; - - // internal module because macro is hard-coded to make a public item - // but we don't want to public export this item - define_encode_set! { - // This encode set is used for HTTP header values and is defined at - // https://tools.ietf.org/html/rfc5987#section-3.2 - pub HTTP_VALUE = [percent_encoding::SIMPLE_ENCODE_SET] | { - ' ', '"', '%', '\'', '(', ')', '*', ',', '/', ':', ';', '<', '-', '>', '?', - '[', '\\', ']', '{', '}' - } - } -} - -#[cfg(test)] -mod tests { - use super::{parse_extended_value, ExtendedValue}; - use header::shared::Charset; - use language_tags::LanguageTag; - - #[test] - fn test_parse_extended_value_with_encoding_and_language_tag() { - let expected_language_tag = "en".parse::().unwrap(); - // RFC 5987, Section 3.2.2 - // Extended notation, using the Unicode character U+00A3 (POUND SIGN) - let result = parse_extended_value("iso-8859-1'en'%A3%20rates"); - assert!(result.is_ok()); - let extended_value = result.unwrap(); - assert_eq!(Charset::Iso_8859_1, extended_value.charset); - assert!(extended_value.language_tag.is_some()); - assert_eq!(expected_language_tag, extended_value.language_tag.unwrap()); - assert_eq!( - vec![163, b' ', b'r', b'a', b't', b'e', b's'], - extended_value.value - ); - } - - #[test] - fn test_parse_extended_value_with_encoding() { - // RFC 5987, Section 3.2.2 - // Extended notation, using the Unicode characters U+00A3 (POUND SIGN) - // and U+20AC (EURO SIGN) - let result = parse_extended_value("UTF-8''%c2%a3%20and%20%e2%82%ac%20rates"); - assert!(result.is_ok()); - let extended_value = result.unwrap(); - assert_eq!(Charset::Ext("UTF-8".to_string()), extended_value.charset); - assert!(extended_value.language_tag.is_none()); - assert_eq!( - vec![ - 194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a', - b't', b'e', b's', - ], - extended_value.value - ); - } - - #[test] - fn test_parse_extended_value_missing_language_tag_and_encoding() { - // From: https://greenbytes.de/tech/tc2231/#attwithfn2231quot2 - let result = parse_extended_value("foo%20bar.html"); - assert!(result.is_err()); - } - - #[test] - fn test_parse_extended_value_partially_formatted() { - let result = parse_extended_value("UTF-8'missing third part"); - assert!(result.is_err()); - } - - #[test] - fn test_parse_extended_value_partially_formatted_blank() { - let result = parse_extended_value("blank second part'"); - assert!(result.is_err()); - } - - #[test] - fn test_fmt_extended_value_with_encoding_and_language_tag() { - let extended_value = ExtendedValue { - charset: Charset::Iso_8859_1, - language_tag: Some("en".parse().expect("Could not parse language tag")), - value: vec![163, b' ', b'r', b'a', b't', b'e', b's'], - }; - assert_eq!("ISO-8859-1'en'%A3%20rates", format!("{}", extended_value)); - } - - #[test] - fn test_fmt_extended_value_with_encoding() { - let extended_value = ExtendedValue { - charset: Charset::Ext("UTF-8".to_string()), - language_tag: None, - value: vec![ - 194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a', - b't', b'e', b's', - ], - }; - assert_eq!( - "UTF-8''%C2%A3%20and%20%E2%82%AC%20rates", - format!("{}", extended_value) - ); - } -} diff --git a/src/header/shared/charset.rs b/src/header/shared/charset.rs deleted file mode 100644 index b679971b..00000000 --- a/src/header/shared/charset.rs +++ /dev/null @@ -1,152 +0,0 @@ -use std::fmt::{self, Display}; -use std::str::FromStr; - -use self::Charset::*; - -/// A Mime charset. -/// -/// The string representation is normalized to upper case. -/// -/// See [http://www.iana.org/assignments/character-sets/character-sets.xhtml][url]. -/// -/// [url]: http://www.iana.org/assignments/character-sets/character-sets.xhtml -#[derive(Clone, Debug, PartialEq)] -#[allow(non_camel_case_types)] -pub enum Charset { - /// US ASCII - Us_Ascii, - /// ISO-8859-1 - Iso_8859_1, - /// ISO-8859-2 - Iso_8859_2, - /// ISO-8859-3 - Iso_8859_3, - /// ISO-8859-4 - Iso_8859_4, - /// ISO-8859-5 - Iso_8859_5, - /// ISO-8859-6 - Iso_8859_6, - /// ISO-8859-7 - Iso_8859_7, - /// ISO-8859-8 - Iso_8859_8, - /// ISO-8859-9 - Iso_8859_9, - /// ISO-8859-10 - Iso_8859_10, - /// Shift_JIS - Shift_Jis, - /// EUC-JP - Euc_Jp, - /// ISO-2022-KR - Iso_2022_Kr, - /// EUC-KR - Euc_Kr, - /// ISO-2022-JP - Iso_2022_Jp, - /// ISO-2022-JP-2 - Iso_2022_Jp_2, - /// ISO-8859-6-E - Iso_8859_6_E, - /// ISO-8859-6-I - Iso_8859_6_I, - /// ISO-8859-8-E - Iso_8859_8_E, - /// ISO-8859-8-I - Iso_8859_8_I, - /// GB2312 - Gb2312, - /// Big5 - Big5, - /// KOI8-R - Koi8_R, - /// An arbitrary charset specified as a string - Ext(String), -} - -impl Charset { - fn label(&self) -> &str { - match *self { - Us_Ascii => "US-ASCII", - Iso_8859_1 => "ISO-8859-1", - Iso_8859_2 => "ISO-8859-2", - Iso_8859_3 => "ISO-8859-3", - Iso_8859_4 => "ISO-8859-4", - Iso_8859_5 => "ISO-8859-5", - Iso_8859_6 => "ISO-8859-6", - Iso_8859_7 => "ISO-8859-7", - Iso_8859_8 => "ISO-8859-8", - Iso_8859_9 => "ISO-8859-9", - Iso_8859_10 => "ISO-8859-10", - Shift_Jis => "Shift-JIS", - Euc_Jp => "EUC-JP", - Iso_2022_Kr => "ISO-2022-KR", - Euc_Kr => "EUC-KR", - Iso_2022_Jp => "ISO-2022-JP", - Iso_2022_Jp_2 => "ISO-2022-JP-2", - Iso_8859_6_E => "ISO-8859-6-E", - Iso_8859_6_I => "ISO-8859-6-I", - Iso_8859_8_E => "ISO-8859-8-E", - Iso_8859_8_I => "ISO-8859-8-I", - Gb2312 => "GB2312", - Big5 => "big5", - Koi8_R => "KOI8-R", - Ext(ref s) => s, - } - } -} - -impl Display for Charset { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(self.label()) - } -} - -impl FromStr for Charset { - type Err = ::Error; - fn from_str(s: &str) -> ::Result { - Ok(match s.to_ascii_uppercase().as_ref() { - "US-ASCII" => Us_Ascii, - "ISO-8859-1" => Iso_8859_1, - "ISO-8859-2" => Iso_8859_2, - "ISO-8859-3" => Iso_8859_3, - "ISO-8859-4" => Iso_8859_4, - "ISO-8859-5" => Iso_8859_5, - "ISO-8859-6" => Iso_8859_6, - "ISO-8859-7" => Iso_8859_7, - "ISO-8859-8" => Iso_8859_8, - "ISO-8859-9" => Iso_8859_9, - "ISO-8859-10" => Iso_8859_10, - "SHIFT-JIS" => Shift_Jis, - "EUC-JP" => Euc_Jp, - "ISO-2022-KR" => Iso_2022_Kr, - "EUC-KR" => Euc_Kr, - "ISO-2022-JP" => Iso_2022_Jp, - "ISO-2022-JP-2" => Iso_2022_Jp_2, - "ISO-8859-6-E" => Iso_8859_6_E, - "ISO-8859-6-I" => Iso_8859_6_I, - "ISO-8859-8-E" => Iso_8859_8_E, - "ISO-8859-8-I" => Iso_8859_8_I, - "GB2312" => Gb2312, - "big5" => Big5, - "KOI8-R" => Koi8_R, - s => Ext(s.to_owned()), - }) - } -} - -#[test] -fn test_parse() { - assert_eq!(Us_Ascii, "us-ascii".parse().unwrap()); - assert_eq!(Us_Ascii, "US-Ascii".parse().unwrap()); - assert_eq!(Us_Ascii, "US-ASCII".parse().unwrap()); - assert_eq!(Shift_Jis, "Shift-JIS".parse().unwrap()); - assert_eq!(Ext("ABCD".to_owned()), "abcd".parse().unwrap()); -} - -#[test] -fn test_display() { - assert_eq!("US-ASCII", format!("{}", Us_Ascii)); - assert_eq!("ABCD", format!("{}", Ext("ABCD".to_owned()))); -} diff --git a/src/header/shared/encoding.rs b/src/header/shared/encoding.rs deleted file mode 100644 index 64027d8a..00000000 --- a/src/header/shared/encoding.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::fmt; -use std::str; - -pub use self::Encoding::{ - Brotli, Chunked, Compress, Deflate, EncodingExt, Gzip, Identity, Trailers, -}; - -/// A value to represent an encoding used in `Transfer-Encoding` -/// or `Accept-Encoding` header. -#[derive(Clone, PartialEq, Debug)] -pub enum Encoding { - /// The `chunked` encoding. - Chunked, - /// The `br` encoding. - Brotli, - /// The `gzip` encoding. - Gzip, - /// The `deflate` encoding. - Deflate, - /// The `compress` encoding. - Compress, - /// The `identity` encoding. - Identity, - /// The `trailers` encoding. - Trailers, - /// Some other encoding that is less common, can be any String. - EncodingExt(String), -} - -impl fmt::Display for Encoding { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(match *self { - Chunked => "chunked", - Brotli => "br", - Gzip => "gzip", - Deflate => "deflate", - Compress => "compress", - Identity => "identity", - Trailers => "trailers", - EncodingExt(ref s) => s.as_ref(), - }) - } -} - -impl str::FromStr for Encoding { - type Err = ::error::ParseError; - fn from_str(s: &str) -> Result { - match s { - "chunked" => Ok(Chunked), - "br" => Ok(Brotli), - "deflate" => Ok(Deflate), - "gzip" => Ok(Gzip), - "compress" => Ok(Compress), - "identity" => Ok(Identity), - "trailers" => Ok(Trailers), - _ => Ok(EncodingExt(s.to_owned())), - } - } -} diff --git a/src/header/shared/entity.rs b/src/header/shared/entity.rs deleted file mode 100644 index 0d3b0a4e..00000000 --- a/src/header/shared/entity.rs +++ /dev/null @@ -1,266 +0,0 @@ -use header::{HeaderValue, IntoHeaderValue, InvalidHeaderValueBytes, Writer}; -use std::fmt::{self, Display, Write}; -use std::str::FromStr; - -/// check that each char in the slice is either: -/// 1. `%x21`, or -/// 2. in the range `%x23` to `%x7E`, or -/// 3. above `%x80` -fn check_slice_validity(slice: &str) -> bool { - slice - .bytes() - .all(|c| c == b'\x21' || (c >= b'\x23' && c <= b'\x7e') | (c >= b'\x80')) -} - -/// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3) -/// -/// An entity tag consists of a string enclosed by two literal double quotes. -/// Preceding the first double quote is an optional weakness indicator, -/// which always looks like `W/`. Examples for valid tags are `"xyzzy"` and -/// `W/"xyzzy"`. -/// -/// # ABNF -/// -/// ```text -/// entity-tag = [ weak ] opaque-tag -/// weak = %x57.2F ; "W/", case-sensitive -/// opaque-tag = DQUOTE *etagc DQUOTE -/// etagc = %x21 / %x23-7E / obs-text -/// ; VCHAR except double quotes, plus obs-text -/// ``` -/// -/// # Comparison -/// To check if two entity tags are equivalent in an application always use the -/// `strong_eq` or `weak_eq` methods based on the context of the Tag. Only use -/// `==` to check if two tags are identical. -/// -/// The example below shows the results for a set of entity-tag pairs and -/// both the weak and strong comparison function results: -/// -/// | `ETag 1`| `ETag 2`| Strong Comparison | Weak Comparison | -/// |---------|---------|-------------------|-----------------| -/// | `W/"1"` | `W/"1"` | no match | match | -/// | `W/"1"` | `W/"2"` | no match | no match | -/// | `W/"1"` | `"1"` | no match | match | -/// | `"1"` | `"1"` | match | match | -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct EntityTag { - /// Weakness indicator for the tag - pub weak: bool, - /// The opaque string in between the DQUOTEs - tag: String, -} - -impl EntityTag { - /// Constructs a new EntityTag. - /// # Panics - /// If the tag contains invalid characters. - pub fn new(weak: bool, tag: String) -> EntityTag { - assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag); - EntityTag { weak, tag } - } - - /// Constructs a new weak EntityTag. - /// # Panics - /// If the tag contains invalid characters. - pub fn weak(tag: String) -> EntityTag { - EntityTag::new(true, tag) - } - - /// Constructs a new strong EntityTag. - /// # Panics - /// If the tag contains invalid characters. - pub fn strong(tag: String) -> EntityTag { - EntityTag::new(false, tag) - } - - /// Get the tag. - pub fn tag(&self) -> &str { - self.tag.as_ref() - } - - /// Set the tag. - /// # Panics - /// If the tag contains invalid characters. - pub fn set_tag(&mut self, tag: String) { - assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag); - self.tag = tag - } - - /// For strong comparison two entity-tags are equivalent if both are not - /// weak and their opaque-tags match character-by-character. - pub fn strong_eq(&self, other: &EntityTag) -> bool { - !self.weak && !other.weak && self.tag == other.tag - } - - /// For weak comparison two entity-tags are equivalent if their - /// opaque-tags match character-by-character, regardless of either or - /// both being tagged as "weak". - pub fn weak_eq(&self, other: &EntityTag) -> bool { - self.tag == other.tag - } - - /// The inverse of `EntityTag.strong_eq()`. - pub fn strong_ne(&self, other: &EntityTag) -> bool { - !self.strong_eq(other) - } - - /// The inverse of `EntityTag.weak_eq()`. - pub fn weak_ne(&self, other: &EntityTag) -> bool { - !self.weak_eq(other) - } -} - -impl Display for EntityTag { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.weak { - write!(f, "W/\"{}\"", self.tag) - } else { - write!(f, "\"{}\"", self.tag) - } - } -} - -impl FromStr for EntityTag { - type Err = ::error::ParseError; - - fn from_str(s: &str) -> Result { - let length: usize = s.len(); - let slice = &s[..]; - // Early exits if it doesn't terminate in a DQUOTE. - if !slice.ends_with('"') || slice.len() < 2 { - return Err(::error::ParseError::Header); - } - // The etag is weak if its first char is not a DQUOTE. - if slice.len() >= 2 - && slice.starts_with('"') - && check_slice_validity(&slice[1..length - 1]) - { - // No need to check if the last char is a DQUOTE, - // we already did that above. - return Ok(EntityTag { - weak: false, - tag: slice[1..length - 1].to_owned(), - }); - } else if slice.len() >= 4 - && slice.starts_with("W/\"") - && check_slice_validity(&slice[3..length - 1]) - { - return Ok(EntityTag { - weak: true, - tag: slice[3..length - 1].to_owned(), - }); - } - Err(::error::ParseError::Header) - } -} - -impl IntoHeaderValue for EntityTag { - type Error = InvalidHeaderValueBytes; - - fn try_into(self) -> Result { - let mut wrt = Writer::new(); - write!(wrt, "{}", self).unwrap(); - HeaderValue::from_shared(wrt.take()) - } -} - -#[cfg(test)] -mod tests { - use super::EntityTag; - - #[test] - fn test_etag_parse_success() { - // Expected success - assert_eq!( - "\"foobar\"".parse::().unwrap(), - EntityTag::strong("foobar".to_owned()) - ); - assert_eq!( - "\"\"".parse::().unwrap(), - EntityTag::strong("".to_owned()) - ); - assert_eq!( - "W/\"weaktag\"".parse::().unwrap(), - EntityTag::weak("weaktag".to_owned()) - ); - assert_eq!( - "W/\"\x65\x62\"".parse::().unwrap(), - EntityTag::weak("\x65\x62".to_owned()) - ); - assert_eq!( - "W/\"\"".parse::().unwrap(), - EntityTag::weak("".to_owned()) - ); - } - - #[test] - fn test_etag_parse_failures() { - // Expected failures - assert!("no-dquotes".parse::().is_err()); - assert!( - "w/\"the-first-w-is-case-sensitive\"" - .parse::() - .is_err() - ); - assert!("".parse::().is_err()); - assert!("\"unmatched-dquotes1".parse::().is_err()); - assert!("unmatched-dquotes2\"".parse::().is_err()); - assert!("matched-\"dquotes\"".parse::().is_err()); - } - - #[test] - fn test_etag_fmt() { - assert_eq!( - format!("{}", EntityTag::strong("foobar".to_owned())), - "\"foobar\"" - ); - assert_eq!(format!("{}", EntityTag::strong("".to_owned())), "\"\""); - assert_eq!( - format!("{}", EntityTag::weak("weak-etag".to_owned())), - "W/\"weak-etag\"" - ); - assert_eq!( - format!("{}", EntityTag::weak("\u{0065}".to_owned())), - "W/\"\x65\"" - ); - assert_eq!(format!("{}", EntityTag::weak("".to_owned())), "W/\"\""); - } - - #[test] - fn test_cmp() { - // | ETag 1 | ETag 2 | Strong Comparison | Weak Comparison | - // |---------|---------|-------------------|-----------------| - // | `W/"1"` | `W/"1"` | no match | match | - // | `W/"1"` | `W/"2"` | no match | no match | - // | `W/"1"` | `"1"` | no match | match | - // | `"1"` | `"1"` | match | match | - let mut etag1 = EntityTag::weak("1".to_owned()); - let mut etag2 = EntityTag::weak("1".to_owned()); - assert!(!etag1.strong_eq(&etag2)); - assert!(etag1.weak_eq(&etag2)); - assert!(etag1.strong_ne(&etag2)); - assert!(!etag1.weak_ne(&etag2)); - - etag1 = EntityTag::weak("1".to_owned()); - etag2 = EntityTag::weak("2".to_owned()); - assert!(!etag1.strong_eq(&etag2)); - assert!(!etag1.weak_eq(&etag2)); - assert!(etag1.strong_ne(&etag2)); - assert!(etag1.weak_ne(&etag2)); - - etag1 = EntityTag::weak("1".to_owned()); - etag2 = EntityTag::strong("1".to_owned()); - assert!(!etag1.strong_eq(&etag2)); - assert!(etag1.weak_eq(&etag2)); - assert!(etag1.strong_ne(&etag2)); - assert!(!etag1.weak_ne(&etag2)); - - etag1 = EntityTag::strong("1".to_owned()); - etag2 = EntityTag::strong("1".to_owned()); - assert!(etag1.strong_eq(&etag2)); - assert!(etag1.weak_eq(&etag2)); - assert!(!etag1.strong_ne(&etag2)); - assert!(!etag1.weak_ne(&etag2)); - } -} diff --git a/src/header/shared/httpdate.rs b/src/header/shared/httpdate.rs deleted file mode 100644 index 7fd26b12..00000000 --- a/src/header/shared/httpdate.rs +++ /dev/null @@ -1,119 +0,0 @@ -use std::fmt::{self, Display}; -use std::io::Write; -use std::str::FromStr; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - -use bytes::{BufMut, BytesMut}; -use http::header::{HeaderValue, InvalidHeaderValueBytes}; -use time; - -use error::ParseError; -use header::IntoHeaderValue; - -/// A timestamp with HTTP formatting and parsing -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct HttpDate(time::Tm); - -impl FromStr for HttpDate { - type Err = ParseError; - - fn from_str(s: &str) -> Result { - match time::strptime(s, "%a, %d %b %Y %T %Z") - .or_else(|_| time::strptime(s, "%A, %d-%b-%y %T %Z")) - .or_else(|_| time::strptime(s, "%c")) - { - Ok(t) => Ok(HttpDate(t)), - Err(_) => Err(ParseError::Header), - } - } -} - -impl Display for HttpDate { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.0.to_utc().rfc822(), f) - } -} - -impl From for HttpDate { - fn from(tm: time::Tm) -> HttpDate { - HttpDate(tm) - } -} - -impl From for HttpDate { - fn from(sys: SystemTime) -> HttpDate { - let tmspec = match sys.duration_since(UNIX_EPOCH) { - Ok(dur) => { - time::Timespec::new(dur.as_secs() as i64, dur.subsec_nanos() as i32) - } - Err(err) => { - let neg = err.duration(); - time::Timespec::new( - -(neg.as_secs() as i64), - -(neg.subsec_nanos() as i32), - ) - } - }; - HttpDate(time::at_utc(tmspec)) - } -} - -impl IntoHeaderValue for HttpDate { - type Error = InvalidHeaderValueBytes; - - fn try_into(self) -> Result { - let mut wrt = BytesMut::with_capacity(29).writer(); - write!(wrt, "{}", self.0.rfc822()).unwrap(); - HeaderValue::from_shared(wrt.get_mut().take().freeze()) - } -} - -impl From for SystemTime { - fn from(date: HttpDate) -> SystemTime { - let spec = date.0.to_timespec(); - if spec.sec >= 0 { - UNIX_EPOCH + Duration::new(spec.sec as u64, spec.nsec as u32) - } else { - UNIX_EPOCH - Duration::new(spec.sec as u64, spec.nsec as u32) - } - } -} - -#[cfg(test)] -mod tests { - use super::HttpDate; - use time::Tm; - - const NOV_07: HttpDate = HttpDate(Tm { - tm_nsec: 0, - tm_sec: 37, - tm_min: 48, - tm_hour: 8, - tm_mday: 7, - tm_mon: 10, - tm_year: 94, - tm_wday: 0, - tm_isdst: 0, - tm_yday: 0, - tm_utcoff: 0, - }); - - #[test] - fn test_date() { - assert_eq!( - "Sun, 07 Nov 1994 08:48:37 GMT".parse::().unwrap(), - NOV_07 - ); - assert_eq!( - "Sunday, 07-Nov-94 08:48:37 GMT" - .parse::() - .unwrap(), - NOV_07 - ); - assert_eq!( - "Sun Nov 7 08:48:37 1994".parse::().unwrap(), - NOV_07 - ); - assert!("this-is-no-date".parse::().is_err()); - } -} diff --git a/src/header/shared/mod.rs b/src/header/shared/mod.rs deleted file mode 100644 index f2bc9163..00000000 --- a/src/header/shared/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Copied for `hyper::header::shared`; - -pub use self::charset::Charset; -pub use self::encoding::Encoding; -pub use self::entity::EntityTag; -pub use self::httpdate::HttpDate; -pub use self::quality_item::{q, qitem, Quality, QualityItem}; -pub use language_tags::LanguageTag; - -mod charset; -mod encoding; -mod entity; -mod httpdate; -mod quality_item; diff --git a/src/header/shared/quality_item.rs b/src/header/shared/quality_item.rs deleted file mode 100644 index 80bd7e1c..00000000 --- a/src/header/shared/quality_item.rs +++ /dev/null @@ -1,294 +0,0 @@ -use std::cmp; -use std::default::Default; -use std::fmt; -use std::str; - -use self::internal::IntoQuality; - -/// Represents a quality used in quality values. -/// -/// Can be created with the `q` function. -/// -/// # Implementation notes -/// -/// The quality value is defined as a number between 0 and 1 with three decimal -/// places. This means there are 1001 possible values. Since floating point -/// numbers are not exact and the smallest floating point data type (`f32`) -/// consumes four bytes, hyper uses an `u16` value to store the -/// quality internally. For performance reasons you may set quality directly to -/// a value between 0 and 1000 e.g. `Quality(532)` matches the quality -/// `q=0.532`. -/// -/// [RFC7231 Section 5.3.1](https://tools.ietf.org/html/rfc7231#section-5.3.1) -/// gives more information on quality values in HTTP header fields. -#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] -pub struct Quality(u16); - -impl Default for Quality { - fn default() -> Quality { - Quality(1000) - } -} - -/// Represents an item with a quality value as defined in -/// [RFC7231](https://tools.ietf.org/html/rfc7231#section-5.3.1). -#[derive(Clone, PartialEq, Debug)] -pub struct QualityItem { - /// The actual contents of the field. - pub item: T, - /// The quality (client or server preference) for the value. - pub quality: Quality, -} - -impl QualityItem { - /// Creates a new `QualityItem` from an item and a quality. - /// The item can be of any type. - /// The quality should be a value in the range [0, 1]. - pub fn new(item: T, quality: Quality) -> QualityItem { - QualityItem { item, quality } - } -} - -impl cmp::PartialOrd for QualityItem { - fn partial_cmp(&self, other: &QualityItem) -> Option { - self.quality.partial_cmp(&other.quality) - } -} - -impl fmt::Display for QualityItem { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.item, f)?; - match self.quality.0 { - 1000 => Ok(()), - 0 => f.write_str("; q=0"), - x => write!(f, "; q=0.{}", format!("{:03}", x).trim_right_matches('0')), - } - } -} - -impl str::FromStr for QualityItem { - type Err = ::error::ParseError; - - fn from_str(s: &str) -> Result, ::error::ParseError> { - if !s.is_ascii() { - return Err(::error::ParseError::Header); - } - // Set defaults used if parsing fails. - let mut raw_item = s; - let mut quality = 1f32; - - let parts: Vec<&str> = s.rsplitn(2, ';').map(|x| x.trim()).collect(); - if parts.len() == 2 { - if parts[0].len() < 2 { - return Err(::error::ParseError::Header); - } - let start = &parts[0][0..2]; - if start == "q=" || start == "Q=" { - let q_part = &parts[0][2..parts[0].len()]; - if q_part.len() > 5 { - return Err(::error::ParseError::Header); - } - match q_part.parse::() { - Ok(q_value) => { - if 0f32 <= q_value && q_value <= 1f32 { - quality = q_value; - raw_item = parts[1]; - } else { - return Err(::error::ParseError::Header); - } - } - Err(_) => return Err(::error::ParseError::Header), - } - } - } - match raw_item.parse::() { - // we already checked above that the quality is within range - Ok(item) => Ok(QualityItem::new(item, from_f32(quality))), - Err(_) => Err(::error::ParseError::Header), - } - } -} - -#[inline] -fn from_f32(f: f32) -> Quality { - // this function is only used internally. A check that `f` is within range - // should be done before calling this method. Just in case, this - // debug_assert should catch if we were forgetful - debug_assert!( - f >= 0f32 && f <= 1f32, - "q value must be between 0.0 and 1.0" - ); - Quality((f * 1000f32) as u16) -} - -/// Convenience function to wrap a value in a `QualityItem` -/// Sets `q` to the default 1.0 -pub fn qitem(item: T) -> QualityItem { - QualityItem::new(item, Default::default()) -} - -/// Convenience function to create a `Quality` from a float or integer. -/// -/// Implemented for `u16` and `f32`. Panics if value is out of range. -pub fn q(val: T) -> Quality { - val.into_quality() -} - -mod internal { - use super::Quality; - - // TryFrom is probably better, but it's not stable. For now, we want to - // keep the functionality of the `q` function, while allowing it to be - // generic over `f32` and `u16`. - // - // `q` would panic before, so keep that behavior. `TryFrom` can be - // introduced later for a non-panicking conversion. - - pub trait IntoQuality: Sealed + Sized { - fn into_quality(self) -> Quality; - } - - impl IntoQuality for f32 { - fn into_quality(self) -> Quality { - assert!( - self >= 0f32 && self <= 1f32, - "float must be between 0.0 and 1.0" - ); - super::from_f32(self) - } - } - - impl IntoQuality for u16 { - fn into_quality(self) -> Quality { - assert!(self <= 1000, "u16 must be between 0 and 1000"); - Quality(self) - } - } - - pub trait Sealed {} - impl Sealed for u16 {} - impl Sealed for f32 {} -} - -#[cfg(test)] -mod tests { - use super::super::encoding::*; - use super::*; - - #[test] - fn test_quality_item_fmt_q_1() { - let x = qitem(Chunked); - assert_eq!(format!("{}", x), "chunked"); - } - #[test] - fn test_quality_item_fmt_q_0001() { - let x = QualityItem::new(Chunked, Quality(1)); - assert_eq!(format!("{}", x), "chunked; q=0.001"); - } - #[test] - fn test_quality_item_fmt_q_05() { - // Custom value - let x = QualityItem { - item: EncodingExt("identity".to_owned()), - quality: Quality(500), - }; - assert_eq!(format!("{}", x), "identity; q=0.5"); - } - - #[test] - fn test_quality_item_fmt_q_0() { - // Custom value - let x = QualityItem { - item: EncodingExt("identity".to_owned()), - quality: Quality(0), - }; - assert_eq!(x.to_string(), "identity; q=0"); - } - - #[test] - fn test_quality_item_from_str1() { - let x: Result, _> = "chunked".parse(); - assert_eq!( - x.unwrap(), - QualityItem { - item: Chunked, - quality: Quality(1000), - } - ); - } - #[test] - fn test_quality_item_from_str2() { - let x: Result, _> = "chunked; q=1".parse(); - assert_eq!( - x.unwrap(), - QualityItem { - item: Chunked, - quality: Quality(1000), - } - ); - } - #[test] - fn test_quality_item_from_str3() { - let x: Result, _> = "gzip; q=0.5".parse(); - assert_eq!( - x.unwrap(), - QualityItem { - item: Gzip, - quality: Quality(500), - } - ); - } - #[test] - fn test_quality_item_from_str4() { - let x: Result, _> = "gzip; q=0.273".parse(); - assert_eq!( - x.unwrap(), - QualityItem { - item: Gzip, - quality: Quality(273), - } - ); - } - #[test] - fn test_quality_item_from_str5() { - let x: Result, _> = "gzip; q=0.2739999".parse(); - assert!(x.is_err()); - } - #[test] - fn test_quality_item_from_str6() { - let x: Result, _> = "gzip; q=2".parse(); - assert!(x.is_err()); - } - #[test] - fn test_quality_item_ordering() { - let x: QualityItem = "gzip; q=0.5".parse().ok().unwrap(); - let y: QualityItem = "gzip; q=0.273".parse().ok().unwrap(); - let comparision_result: bool = x.gt(&y); - assert!(comparision_result) - } - - #[test] - fn test_quality() { - assert_eq!(q(0.5), Quality(500)); - } - - #[test] - #[should_panic] // FIXME - 32-bit msvc unwinding broken - #[cfg_attr(all(target_arch = "x86", target_env = "msvc"), ignore)] - fn test_quality_invalid() { - q(-1.0); - } - - #[test] - #[should_panic] // FIXME - 32-bit msvc unwinding broken - #[cfg_attr(all(target_arch = "x86", target_env = "msvc"), ignore)] - fn test_quality_invalid2() { - q(2.0); - } - - #[test] - fn test_fuzzing_bugs() { - assert!("99999;".parse::>().is_err()); - assert!("\x0d;;;=\u{d6aa}==".parse::>().is_err()) - } -} diff --git a/src/helpers.rs b/src/helpers.rs index e82d6161..860a02a4 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,571 +1,180 @@ -//! Various helpers +use actix_http::Response; +use actix_service::{NewService, Service}; +use futures::future::{ok, FutureResult}; +use futures::{Future, Poll}; -use http::{header, StatusCode}; -use regex::Regex; +pub(crate) type BoxedHttpService = Box< + Service< + Request = Req, + Response = Res, + Error = (), + Future = Box>, + >, +>; -use handler::Handler; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; +pub(crate) type BoxedHttpNewService = Box< + NewService< + Request = Req, + Response = Res, + Error = (), + InitError = (), + Service = BoxedHttpService, + Future = Box, Error = ()>>, + >, +>; -/// Path normalization helper -/// -/// By normalizing it means: -/// -/// - Add a trailing slash to the path. -/// - Remove a trailing slash from the path. -/// - Double slashes are replaced by one. -/// -/// The handler returns as soon as it finds a path that resolves -/// correctly. The order if all enable is 1) merge, 3) both merge and append -/// and 3) append. If the path resolves with -/// at least one of those conditions, it will redirect to the new path. -/// -/// If *append* is *true* append slash when needed. If a resource is -/// defined with trailing slash and the request comes without it, it will -/// append it automatically. -/// -/// If *merge* is *true*, merge multiple consecutive slashes in the path into -/// one. -/// -/// This handler designed to be use as a handler for application's *default -/// resource*. -/// -/// ```rust -/// # extern crate actix_web; -/// # #[macro_use] extern crate serde_derive; -/// # use actix_web::*; -/// use actix_web::http::NormalizePath; -/// -/// # fn index(req: &HttpRequest) -> HttpResponse { -/// # HttpResponse::Ok().into() -/// # } -/// fn main() { -/// let app = App::new() -/// .resource("/test/", |r| r.f(index)) -/// .default_resource(|r| r.h(NormalizePath::default())) -/// .finish(); -/// } -/// ``` -/// In this example `/test`, `/test///` will be redirected to `/test/` url. -pub struct NormalizePath { - append: bool, - merge: bool, - re_merge: Regex, - redirect: StatusCode, - not_found: StatusCode, -} +pub(crate) struct HttpNewService(T); -impl Default for NormalizePath { - /// Create default `NormalizePath` instance, *append* is set to *true*, - /// *merge* is set to *true* and *redirect* is set to - /// `StatusCode::MOVED_PERMANENTLY` - fn default() -> NormalizePath { - NormalizePath { - append: true, - merge: true, - re_merge: Regex::new("//+").unwrap(), - redirect: StatusCode::MOVED_PERMANENTLY, - not_found: StatusCode::NOT_FOUND, - } +impl HttpNewService +where + T: NewService, + T::Response: 'static, + T::Future: 'static, + T::Service: Service, + ::Future: 'static, +{ + pub fn new(service: T) -> Self { + HttpNewService(service) } } -impl NormalizePath { - /// Create new `NormalizePath` instance - pub fn new(append: bool, merge: bool, redirect: StatusCode) -> NormalizePath { - NormalizePath { - append, - merge, - redirect, - re_merge: Regex::new("//+").unwrap(), - not_found: StatusCode::NOT_FOUND, - } +impl NewService for HttpNewService +where + T: NewService, + T::Request: 'static, + T::Response: 'static, + T::Future: 'static, + T::Service: Service + 'static, + ::Future: 'static, +{ + type Request = T::Request; + type Response = T::Response; + type 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) + })) } } -impl Handler for NormalizePath { - type Result = HttpResponse; +struct HttpServiceWrapper { + service: T, +} - fn handle(&self, req: &HttpRequest) -> Self::Result { - let query = req.query_string(); - if self.merge { - // merge slashes - let p = self.re_merge.replace_all(req.path(), "/"); - if p.len() != req.path().len() { - if req.resource().has_prefixed_resource(p.as_ref()) { - let p = if !query.is_empty() { - p + "?" + query - } else { - p - }; - return HttpResponse::build(self.redirect) - .header(header::LOCATION, p.as_ref()) - .finish(); - } - // merge slashes and append trailing slash - if self.append && !p.ends_with('/') { - let p = p.as_ref().to_owned() + "/"; - if req.resource().has_prefixed_resource(&p) { - let p = if !query.is_empty() { - p + "?" + query - } else { - p - }; - return HttpResponse::build(self.redirect) - .header(header::LOCATION, p.as_str()) - .finish(); - } - } +impl Service for HttpServiceWrapper +where + T: Service, + T::Request: 'static, + T::Response: 'static, + T::Future: 'static, +{ + type Request = T::Request; + type Response = T::Response; + type Error = (); + type Future = Box>; - // try to remove trailing slash - if p.ends_with('/') { - let p = p.as_ref().trim_right_matches('/'); - if req.resource().has_prefixed_resource(p) { - let mut req = HttpResponse::build(self.redirect); - return if !query.is_empty() { - req.header( - header::LOCATION, - (p.to_owned() + "?" + query).as_str(), - ) - } else { - req.header(header::LOCATION, p) - }.finish(); - } - } - } else if p.ends_with('/') { - // try to remove trailing slash - let p = p.as_ref().trim_right_matches('/'); - if req.resource().has_prefixed_resource(p) { - let mut req = HttpResponse::build(self.redirect); - return if !query.is_empty() { - req.header( - header::LOCATION, - (p.to_owned() + "?" + query).as_str(), - ) - } else { - req.header(header::LOCATION, p) - }.finish(); - } - } - } - // append trailing slash - if self.append && !req.path().ends_with('/') { - let p = req.path().to_owned() + "/"; - if req.resource().has_prefixed_resource(&p) { - let p = if !query.is_empty() { - p + "?" + query - } else { - p - }; - return HttpResponse::build(self.redirect) - .header(header::LOCATION, p.as_str()) - .finish(); - } - } - HttpResponse::new(self.not_found) + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.service.poll_ready().map_err(|_| ()) + } + + fn call(&mut self, req: Self::Request) -> Self::Future { + Box::new(self.service.call(req).map_err(|_| ())) } } -#[cfg(test)] -mod tests { - use super::*; - use application::App; - use http::{header, Method}; - use test::TestRequest; +pub(crate) fn not_found(_: Req) -> FutureResult { + ok(Response::NotFound().finish()) +} - fn index(_req: &HttpRequest) -> HttpResponse { - HttpResponse::new(StatusCode::OK) - } +pub(crate) type HttpDefaultService = Box< + Service< + Request = Req, + Response = Res, + Error = (), + Future = Box>, + >, +>; - #[test] - fn test_normalize_path_trailing_slashes() { - let app = App::new() - .resource("/resource1", |r| r.method(Method::GET).f(index)) - .resource("/resource2/", |r| r.method(Method::GET).f(index)) - .default_resource(|r| r.h(NormalizePath::default())) - .finish(); +pub(crate) type HttpDefaultNewService = Box< + NewService< + Request = Req, + Response = Res, + Error = (), + InitError = (), + Service = HttpDefaultService, + Future = Box, Error = ()>>, + >, +>; - // trailing slashes - let params = vec![ - ("/resource1", "", StatusCode::OK), - ("/resource1/", "/resource1", StatusCode::MOVED_PERMANENTLY), - ("/resource2", "/resource2/", StatusCode::MOVED_PERMANENTLY), - ("/resource2/", "", StatusCode::OK), - ("/resource1?p1=1&p2=2", "", StatusCode::OK), - ( - "/resource1/?p1=1&p2=2", - "/resource1?p1=1&p2=2", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/resource2?p1=1&p2=2", - "/resource2/?p1=1&p2=2", - StatusCode::MOVED_PERMANENTLY, - ), - ("/resource2/?p1=1&p2=2", "", StatusCode::OK), - ]; - for (path, target, code) in params { - let req = TestRequest::with_uri(path).request(); - let resp = app.run(req); - let r = &resp.as_msg(); - assert_eq!(r.status(), code); - if !target.is_empty() { - assert_eq!( - target, - r.headers().get(header::LOCATION).unwrap().to_str().unwrap() - ); - } - } - } +pub(crate) struct DefaultNewService { + service: T, +} - #[test] - fn test_prefixed_normalize_path_trailing_slashes() { - let app = App::new() - .prefix("/test") - .resource("/resource1", |r| r.method(Method::GET).f(index)) - .resource("/resource2/", |r| r.method(Method::GET).f(index)) - .default_resource(|r| r.h(NormalizePath::default())) - .finish(); - - // trailing slashes - let params = vec![ - ("/test/resource1", "", StatusCode::OK), - ( - "/test/resource1/", - "/test/resource1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/test/resource2", - "/test/resource2/", - StatusCode::MOVED_PERMANENTLY, - ), - ("/test/resource2/", "", StatusCode::OK), - ("/test/resource1?p1=1&p2=2", "", StatusCode::OK), - ( - "/test/resource1/?p1=1&p2=2", - "/test/resource1?p1=1&p2=2", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/test/resource2?p1=1&p2=2", - "/test/resource2/?p1=1&p2=2", - StatusCode::MOVED_PERMANENTLY, - ), - ("/test/resource2/?p1=1&p2=2", "", StatusCode::OK), - ]; - for (path, target, code) in params { - let req = TestRequest::with_uri(path).request(); - let resp = app.run(req); - let r = &resp.as_msg(); - assert_eq!(r.status(), code); - if !target.is_empty() { - assert_eq!( - target, - r.headers().get(header::LOCATION).unwrap().to_str().unwrap() - ); - } - } - } - - #[test] - fn test_normalize_path_trailing_slashes_disabled() { - let app = App::new() - .resource("/resource1", |r| r.method(Method::GET).f(index)) - .resource("/resource2/", |r| r.method(Method::GET).f(index)) - .default_resource(|r| { - r.h(NormalizePath::new( - false, - true, - StatusCode::MOVED_PERMANENTLY, - )) - }).finish(); - - // trailing slashes - let params = vec![ - ("/resource1", StatusCode::OK), - ("/resource1/", StatusCode::MOVED_PERMANENTLY), - ("/resource2", StatusCode::NOT_FOUND), - ("/resource2/", StatusCode::OK), - ("/resource1?p1=1&p2=2", StatusCode::OK), - ("/resource1/?p1=1&p2=2", StatusCode::MOVED_PERMANENTLY), - ("/resource2?p1=1&p2=2", StatusCode::NOT_FOUND), - ("/resource2/?p1=1&p2=2", StatusCode::OK), - ]; - for (path, code) in params { - let req = TestRequest::with_uri(path).request(); - let resp = app.run(req); - let r = &resp.as_msg(); - assert_eq!(r.status(), code); - } - } - - #[test] - fn test_normalize_path_merge_slashes() { - let app = App::new() - .resource("/resource1", |r| r.method(Method::GET).f(index)) - .resource("/resource1/a/b", |r| r.method(Method::GET).f(index)) - .default_resource(|r| r.h(NormalizePath::default())) - .finish(); - - // trailing slashes - let params = vec![ - ("/resource1/a/b", "", StatusCode::OK), - ("/resource1/", "/resource1", StatusCode::MOVED_PERMANENTLY), - ("/resource1//", "/resource1", StatusCode::MOVED_PERMANENTLY), - ( - "//resource1//a//b", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource1//a//b/", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource1//a//b//", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource1//a//b", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a///b", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a//b/", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ("/resource1/a/b?p=1", "", StatusCode::OK), - ( - "//resource1//a//b?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource1//a//b/?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource1//a//b?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a///b?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a//b/?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a//b//?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ]; - for (path, target, code) in params { - let req = TestRequest::with_uri(path).request(); - let resp = app.run(req); - let r = &resp.as_msg(); - assert_eq!(r.status(), code); - if !target.is_empty() { - assert_eq!( - target, - r.headers().get(header::LOCATION).unwrap().to_str().unwrap() - ); - } - } - } - - #[test] - fn test_normalize_path_merge_and_append_slashes() { - let app = App::new() - .resource("/resource1", |r| r.method(Method::GET).f(index)) - .resource("/resource2/", |r| r.method(Method::GET).f(index)) - .resource("/resource1/a/b", |r| r.method(Method::GET).f(index)) - .resource("/resource2/a/b/", |r| r.method(Method::GET).f(index)) - .default_resource(|r| r.h(NormalizePath::default())) - .finish(); - - // trailing slashes - let params = vec![ - ("/resource1/a/b", "", StatusCode::OK), - ( - "/resource1/a/b/", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b/", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b//", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource1//a//b", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource1//a//b/", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a///b", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a///b/", - "/resource1/a/b", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/resource2/a/b", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ("/resource2/a/b/", "", StatusCode::OK), - ( - "//resource2//a//b", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b/", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource2//a//b", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource2//a//b/", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource2/a///b", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource2/a///b/", - "/resource2/a/b/", - StatusCode::MOVED_PERMANENTLY, - ), - ("/resource1/a/b?p=1", "", StatusCode::OK), - ( - "/resource1/a/b/?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b/?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource1//a//b?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource1//a//b/?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a///b?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a///b/?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource1/a///b//?p=1", - "/resource1/a/b?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/resource2/a/b?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "//resource2//a//b/?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource2//a//b?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "///resource2//a//b/?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource2/a///b?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ( - "/////resource2/a///b/?p=1", - "/resource2/a/b/?p=1", - StatusCode::MOVED_PERMANENTLY, - ), - ]; - for (path, target, code) in params { - let req = TestRequest::with_uri(path).request(); - let resp = app.run(req); - let r = &resp.as_msg(); - assert_eq!(r.status(), code); - if !target.is_empty() { - assert_eq!( - target, - r.headers().get(header::LOCATION).unwrap().to_str().unwrap() - ); - } - } +impl DefaultNewService +where + T: NewService + 'static, + T::Future: 'static, + ::Future: 'static, +{ + pub fn new(service: T) -> Self { + DefaultNewService { service } + } +} + +impl NewService for DefaultNewService +where + T: NewService + 'static, + T::Request: 'static, + T::Future: 'static, + T::Service: 'static, + ::Future: 'static, +{ + type Request = T::Request; + type Response = T::Response; + type Error = (); + type InitError = (); + type Service = HttpDefaultService; + type Future = Box>; + + fn new_service(&self, _: &()) -> Self::Future { + Box::new( + self.service + .new_service(&()) + .map_err(|_| ()) + .and_then(|service| { + let service: HttpDefaultService<_, _> = + Box::new(DefaultServiceWrapper { service }); + Ok(service) + }), + ) + } +} + +struct DefaultServiceWrapper { + service: T, +} + +impl Service for DefaultServiceWrapper +where + T: Service + 'static, + T::Future: 'static, +{ + type Request = T::Request; + type Response = T::Response; + type Error = (); + type Future = Box>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.service.poll_ready().map_err(|_| ()) + } + + fn call(&mut self, req: T::Request) -> Self::Future { + Box::new(self.service.call(req).map_err(|_| ())) } } diff --git a/src/httpcodes.rs b/src/httpcodes.rs deleted file mode 100644 index 41e57d1e..00000000 --- a/src/httpcodes.rs +++ /dev/null @@ -1,84 +0,0 @@ -//! Basic http responses -#![allow(non_upper_case_globals)] -use http::StatusCode; -use httpresponse::{HttpResponse, HttpResponseBuilder}; - -macro_rules! STATIC_RESP { - ($name:ident, $status:expr) => { - #[allow(non_snake_case, missing_docs)] - pub fn $name() -> HttpResponseBuilder { - HttpResponse::build($status) - } - }; -} - -impl HttpResponse { - STATIC_RESP!(Ok, StatusCode::OK); - STATIC_RESP!(Created, StatusCode::CREATED); - STATIC_RESP!(Accepted, StatusCode::ACCEPTED); - STATIC_RESP!( - NonAuthoritativeInformation, - StatusCode::NON_AUTHORITATIVE_INFORMATION - ); - - STATIC_RESP!(NoContent, StatusCode::NO_CONTENT); - STATIC_RESP!(ResetContent, StatusCode::RESET_CONTENT); - STATIC_RESP!(PartialContent, StatusCode::PARTIAL_CONTENT); - STATIC_RESP!(MultiStatus, StatusCode::MULTI_STATUS); - STATIC_RESP!(AlreadyReported, StatusCode::ALREADY_REPORTED); - - STATIC_RESP!(MultipleChoices, StatusCode::MULTIPLE_CHOICES); - STATIC_RESP!(MovedPermanenty, StatusCode::MOVED_PERMANENTLY); - STATIC_RESP!(MovedPermanently, StatusCode::MOVED_PERMANENTLY); - STATIC_RESP!(Found, StatusCode::FOUND); - STATIC_RESP!(SeeOther, StatusCode::SEE_OTHER); - STATIC_RESP!(NotModified, StatusCode::NOT_MODIFIED); - STATIC_RESP!(UseProxy, StatusCode::USE_PROXY); - STATIC_RESP!(TemporaryRedirect, StatusCode::TEMPORARY_REDIRECT); - STATIC_RESP!(PermanentRedirect, StatusCode::PERMANENT_REDIRECT); - - STATIC_RESP!(BadRequest, StatusCode::BAD_REQUEST); - STATIC_RESP!(NotFound, StatusCode::NOT_FOUND); - STATIC_RESP!(Unauthorized, StatusCode::UNAUTHORIZED); - STATIC_RESP!(PaymentRequired, StatusCode::PAYMENT_REQUIRED); - STATIC_RESP!(Forbidden, StatusCode::FORBIDDEN); - STATIC_RESP!(MethodNotAllowed, StatusCode::METHOD_NOT_ALLOWED); - STATIC_RESP!(NotAcceptable, StatusCode::NOT_ACCEPTABLE); - STATIC_RESP!( - ProxyAuthenticationRequired, - StatusCode::PROXY_AUTHENTICATION_REQUIRED - ); - STATIC_RESP!(RequestTimeout, StatusCode::REQUEST_TIMEOUT); - STATIC_RESP!(Conflict, StatusCode::CONFLICT); - STATIC_RESP!(Gone, StatusCode::GONE); - STATIC_RESP!(LengthRequired, StatusCode::LENGTH_REQUIRED); - STATIC_RESP!(PreconditionFailed, StatusCode::PRECONDITION_FAILED); - STATIC_RESP!(PayloadTooLarge, StatusCode::PAYLOAD_TOO_LARGE); - STATIC_RESP!(UriTooLong, StatusCode::URI_TOO_LONG); - STATIC_RESP!(UnsupportedMediaType, StatusCode::UNSUPPORTED_MEDIA_TYPE); - STATIC_RESP!(RangeNotSatisfiable, StatusCode::RANGE_NOT_SATISFIABLE); - STATIC_RESP!(ExpectationFailed, StatusCode::EXPECTATION_FAILED); - - STATIC_RESP!(InternalServerError, StatusCode::INTERNAL_SERVER_ERROR); - STATIC_RESP!(NotImplemented, StatusCode::NOT_IMPLEMENTED); - STATIC_RESP!(BadGateway, StatusCode::BAD_GATEWAY); - STATIC_RESP!(ServiceUnavailable, StatusCode::SERVICE_UNAVAILABLE); - STATIC_RESP!(GatewayTimeout, StatusCode::GATEWAY_TIMEOUT); - STATIC_RESP!(VersionNotSupported, StatusCode::HTTP_VERSION_NOT_SUPPORTED); - STATIC_RESP!(VariantAlsoNegotiates, StatusCode::VARIANT_ALSO_NEGOTIATES); - STATIC_RESP!(InsufficientStorage, StatusCode::INSUFFICIENT_STORAGE); - STATIC_RESP!(LoopDetected, StatusCode::LOOP_DETECTED); -} - -#[cfg(test)] -mod tests { - use body::Body; - use http::StatusCode; - use httpresponse::HttpResponse; - - #[test] - fn test_build() { - let resp = HttpResponse::Ok().body(Body::Empty); - assert_eq!(resp.status(), StatusCode::OK); - } -} diff --git a/src/httpmessage.rs b/src/httpmessage.rs deleted file mode 100644 index ea5e4d86..00000000 --- a/src/httpmessage.rs +++ /dev/null @@ -1,855 +0,0 @@ -use bytes::{Bytes, BytesMut}; -use encoding::all::UTF_8; -use encoding::label::encoding_from_whatwg_label; -use encoding::types::{DecoderTrap, Encoding}; -use encoding::EncodingRef; -use futures::{Async, Future, Poll, Stream}; -use http::{header, HeaderMap}; -use mime::Mime; -use serde::de::DeserializeOwned; -use serde_urlencoded; -use std::str; - -use error::{ - ContentTypeError, ParseError, PayloadError, ReadlinesError, UrlencodedError, -}; -use header::Header; -use json::JsonBody; -use multipart::Multipart; - -/// Trait that implements general purpose operations on http messages -pub trait HttpMessage: Sized { - /// Type of message payload stream - type Stream: Stream + Sized; - - /// Read the message headers. - fn headers(&self) -> &HeaderMap; - - /// Message payload stream - fn payload(&self) -> Self::Stream; - - #[doc(hidden)] - /// Get a header - fn get_header(&self) -> Option - where - Self: Sized, - { - if self.headers().contains_key(H::name()) { - H::parse(self).ok() - } else { - None - } - } - - /// Read the request content type. If request does not contain - /// *Content-Type* header, empty str get returned. - fn content_type(&self) -> &str { - if let Some(content_type) = self.headers().get(header::CONTENT_TYPE) { - if let Ok(content_type) = content_type.to_str() { - return content_type.split(';').next().unwrap().trim(); - } - } - "" - } - - /// Get content type encoding - /// - /// UTF-8 is used by default, If request charset is not set. - fn encoding(&self) -> Result { - if let Some(mime_type) = self.mime_type()? { - if let Some(charset) = mime_type.get_param("charset") { - if let Some(enc) = encoding_from_whatwg_label(charset.as_str()) { - Ok(enc) - } else { - Err(ContentTypeError::UnknownEncoding) - } - } else { - Ok(UTF_8) - } - } else { - Ok(UTF_8) - } - } - - /// Convert the request content type to a known mime type. - fn mime_type(&self) -> Result, ContentTypeError> { - if let Some(content_type) = self.headers().get(header::CONTENT_TYPE) { - if let Ok(content_type) = content_type.to_str() { - return match content_type.parse() { - Ok(mt) => Ok(Some(mt)), - Err(_) => Err(ContentTypeError::ParseError), - }; - } else { - return Err(ContentTypeError::ParseError); - } - } - Ok(None) - } - - /// Check if request has chunked transfer encoding - fn chunked(&self) -> Result { - if let Some(encodings) = self.headers().get(header::TRANSFER_ENCODING) { - if let Ok(s) = encodings.to_str() { - Ok(s.to_lowercase().contains("chunked")) - } else { - Err(ParseError::Header) - } - } else { - Ok(false) - } - } - - /// Load http message body. - /// - /// By default only 256Kb payload reads to a memory, then - /// `PayloadError::Overflow` get returned. Use `MessageBody::limit()` - /// method to change upper limit. - /// - /// ## Server example - /// - /// ```rust - /// # extern crate bytes; - /// # extern crate actix_web; - /// # extern crate futures; - /// # #[macro_use] extern crate serde_derive; - /// use actix_web::{ - /// AsyncResponder, FutureResponse, HttpMessage, HttpRequest, HttpResponse, - /// }; - /// use bytes::Bytes; - /// use futures::future::Future; - /// - /// fn index(mut req: HttpRequest) -> FutureResponse { - /// req.body() // <- get Body future - /// .limit(1024) // <- change max size of the body to a 1kb - /// .from_err() - /// .and_then(|bytes: Bytes| { // <- complete body - /// println!("==== BODY ==== {:?}", bytes); - /// Ok(HttpResponse::Ok().into()) - /// }).responder() - /// } - /// # fn main() {} - /// ``` - fn body(&self) -> MessageBody { - MessageBody::new(self) - } - - /// Parse `application/x-www-form-urlencoded` encoded request's body. - /// Return `UrlEncoded` future. Form can be deserialized to any type that - /// implements `Deserialize` trait from *serde*. - /// - /// Returns error: - /// - /// * content type is not `application/x-www-form-urlencoded` - /// * content-length is greater than 256k - /// - /// ## Server example - /// - /// ```rust - /// # extern crate actix_web; - /// # extern crate futures; - /// # use futures::Future; - /// # use std::collections::HashMap; - /// use actix_web::{FutureResponse, HttpMessage, HttpRequest, HttpResponse}; - /// - /// fn index(mut req: HttpRequest) -> FutureResponse { - /// Box::new( - /// req.urlencoded::>() // <- get UrlEncoded future - /// .from_err() - /// .and_then(|params| { // <- url encoded parameters - /// println!("==== BODY ==== {:?}", params); - /// Ok(HttpResponse::Ok().into()) - /// }), - /// ) - /// } - /// # fn main() {} - /// ``` - fn urlencoded(&self) -> UrlEncoded { - UrlEncoded::new(self) - } - - /// Parse `application/json` encoded body. - /// Return `JsonBody` future. It resolves to a `T` value. - /// - /// Returns error: - /// - /// * content type is not `application/json` - /// * content length is greater than 256k - /// - /// ## Server example - /// - /// ```rust - /// # extern crate actix_web; - /// # extern crate futures; - /// # #[macro_use] extern crate serde_derive; - /// use actix_web::*; - /// use futures::future::{ok, Future}; - /// - /// #[derive(Deserialize, Debug)] - /// struct MyObj { - /// name: String, - /// } - /// - /// fn index(mut req: HttpRequest) -> Box> { - /// req.json() // <- get JsonBody future - /// .from_err() - /// .and_then(|val: MyObj| { // <- deserialized value - /// println!("==== BODY ==== {:?}", val); - /// Ok(HttpResponse::Ok().into()) - /// }).responder() - /// } - /// # fn main() {} - /// ``` - fn json(&self) -> JsonBody { - JsonBody::new::<()>(self, None) - } - - /// Return stream to http payload processes as multipart. - /// - /// Content-type: multipart/form-data; - /// - /// ## Server example - /// - /// ```rust - /// # extern crate actix_web; - /// # extern crate env_logger; - /// # extern crate futures; - /// # extern crate actix; - /// # use std::str; - /// # use actix_web::*; - /// # use actix::FinishStream; - /// # use futures::{Future, Stream}; - /// # use futures::future::{ok, result, Either}; - /// fn index(mut req: HttpRequest) -> Box> { - /// req.multipart().from_err() // <- get multipart stream for current request - /// .and_then(|item| match item { // <- iterate over multipart items - /// multipart::MultipartItem::Field(field) => { - /// // Field in turn is stream of *Bytes* object - /// Either::A(field.from_err() - /// .map(|c| println!("-- CHUNK: \n{:?}", str::from_utf8(&c))) - /// .finish()) - /// }, - /// multipart::MultipartItem::Nested(mp) => { - /// // Or item could be nested Multipart stream - /// Either::B(ok(())) - /// } - /// }) - /// .finish() // <- Stream::finish() combinator from actix - /// .map(|_| HttpResponse::Ok().into()) - /// .responder() - /// } - /// # fn main() {} - /// ``` - fn multipart(&self) -> Multipart { - let boundary = Multipart::boundary(self.headers()); - Multipart::new(boundary, self.payload()) - } - - /// Return stream of lines. - fn readlines(&self) -> Readlines { - Readlines::new(self) - } -} - -/// Stream to read request line by line. -pub struct Readlines { - stream: T::Stream, - buff: BytesMut, - limit: usize, - checked_buff: bool, - encoding: EncodingRef, - err: Option, -} - -impl Readlines { - /// Create a new stream to read request line by line. - fn new(req: &T) -> Self { - let encoding = match req.encoding() { - Ok(enc) => enc, - Err(err) => return Self::err(req, err.into()), - }; - - Readlines { - stream: req.payload(), - buff: BytesMut::with_capacity(262_144), - limit: 262_144, - checked_buff: true, - err: None, - encoding, - } - } - - /// Change max line size. By default max size is 256Kb - pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; - self - } - - fn err(req: &T, err: ReadlinesError) -> Self { - Readlines { - stream: req.payload(), - buff: BytesMut::new(), - limit: 262_144, - checked_buff: true, - encoding: UTF_8, - err: Some(err), - } - } -} - -impl Stream for Readlines { - type Item = String; - type Error = ReadlinesError; - - fn poll(&mut self) -> Poll, Self::Error> { - if let Some(err) = self.err.take() { - return Err(err); - } - - // check if there is a newline in the buffer - if !self.checked_buff { - let mut found: Option = None; - for (ind, b) in self.buff.iter().enumerate() { - if *b == b'\n' { - found = Some(ind); - break; - } - } - if let Some(ind) = found { - // check if line is longer than limit - if ind + 1 > self.limit { - return Err(ReadlinesError::LimitOverflow); - } - let enc: *const Encoding = self.encoding as *const Encoding; - let line = if enc == UTF_8 { - str::from_utf8(&self.buff.split_to(ind + 1)) - .map_err(|_| ReadlinesError::EncodingError)? - .to_owned() - } else { - self.encoding - .decode(&self.buff.split_to(ind + 1), DecoderTrap::Strict) - .map_err(|_| ReadlinesError::EncodingError)? - }; - return Ok(Async::Ready(Some(line))); - } - self.checked_buff = true; - } - // poll req for more bytes - match self.stream.poll() { - Ok(Async::Ready(Some(mut bytes))) => { - // check if there is a newline in bytes - let mut found: Option = None; - for (ind, b) in bytes.iter().enumerate() { - if *b == b'\n' { - found = Some(ind); - break; - } - } - if let Some(ind) = found { - // check if line is longer than limit - if ind + 1 > self.limit { - return Err(ReadlinesError::LimitOverflow); - } - let enc: *const Encoding = self.encoding as *const Encoding; - let line = if enc == UTF_8 { - str::from_utf8(&bytes.split_to(ind + 1)) - .map_err(|_| ReadlinesError::EncodingError)? - .to_owned() - } else { - self.encoding - .decode(&bytes.split_to(ind + 1), DecoderTrap::Strict) - .map_err(|_| ReadlinesError::EncodingError)? - }; - // extend buffer with rest of the bytes; - self.buff.extend_from_slice(&bytes); - self.checked_buff = false; - return Ok(Async::Ready(Some(line))); - } - self.buff.extend_from_slice(&bytes); - Ok(Async::NotReady) - } - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(None)) => { - if self.buff.is_empty() { - return Ok(Async::Ready(None)); - } - if self.buff.len() > self.limit { - return Err(ReadlinesError::LimitOverflow); - } - let enc: *const Encoding = self.encoding as *const Encoding; - let line = if enc == UTF_8 { - str::from_utf8(&self.buff) - .map_err(|_| ReadlinesError::EncodingError)? - .to_owned() - } else { - self.encoding - .decode(&self.buff, DecoderTrap::Strict) - .map_err(|_| ReadlinesError::EncodingError)? - }; - self.buff.clear(); - Ok(Async::Ready(Some(line))) - } - Err(e) => Err(ReadlinesError::from(e)), - } - } -} - -/// Future that resolves to a complete http message body. -pub struct MessageBody { - limit: usize, - length: Option, - stream: Option, - err: Option, - fut: Option>>, -} - -impl MessageBody { - /// Create `MessageBody` for request. - pub fn new(req: &T) -> MessageBody { - let mut len = None; - 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) - } else { - return Self::err(PayloadError::UnknownLength); - } - } else { - return Self::err(PayloadError::UnknownLength); - } - } - - MessageBody { - limit: 262_144, - length: len, - stream: Some(req.payload()), - fut: None, - err: None, - } - } - - /// Change max size of payload. By default max size is 256Kb - pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; - self - } - - fn err(e: PayloadError) -> Self { - MessageBody { - stream: None, - limit: 262_144, - fut: None, - err: Some(e), - length: None, - } - } -} - -impl Future for MessageBody -where - T: HttpMessage + 'static, -{ - type Item = Bytes; - type Error = PayloadError; - - fn poll(&mut self) -> Poll { - if let Some(ref mut fut) = self.fut { - return fut.poll(); - } - - if let Some(err) = self.err.take() { - return Err(err); - } - - if let Some(len) = self.length.take() { - if len > self.limit { - return Err(PayloadError::Overflow); - } - } - - // future - let limit = self.limit; - self.fut = Some(Box::new( - self.stream - .take() - .expect("Can not be used second time") - .from_err() - .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { - if (body.len() + chunk.len()) > limit { - Err(PayloadError::Overflow) - } else { - body.extend_from_slice(&chunk); - Ok(body) - } - }).map(|body| body.freeze()), - )); - self.poll() - } -} - -/// Future that resolves to a parsed urlencoded values. -pub struct UrlEncoded { - stream: Option, - limit: usize, - length: Option, - encoding: EncodingRef, - err: Option, - fut: Option>>, -} - -impl UrlEncoded { - /// Create a new future to URL encode a request - pub fn new(req: &T) -> UrlEncoded { - // check content type - if req.content_type().to_lowercase() != "application/x-www-form-urlencoded" { - return Self::err(UrlencodedError::ContentType); - } - let encoding = match req.encoding() { - Ok(enc) => enc, - Err(_) => return Self::err(UrlencodedError::ContentType), - }; - - let mut len = None; - 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) - } else { - return Self::err(UrlencodedError::UnknownLength); - } - } else { - return Self::err(UrlencodedError::UnknownLength); - } - }; - - UrlEncoded { - encoding, - stream: Some(req.payload()), - limit: 262_144, - length: len, - fut: None, - err: None, - } - } - - fn err(e: UrlencodedError) -> Self { - UrlEncoded { - stream: None, - limit: 262_144, - fut: None, - err: Some(e), - length: None, - encoding: UTF_8, - } - } - - /// Change max size of payload. By default max size is 256Kb - pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; - self - } -} - -impl Future for UrlEncoded -where - T: HttpMessage + 'static, - U: DeserializeOwned + 'static, -{ - type Item = U; - type Error = UrlencodedError; - - fn poll(&mut self) -> Poll { - if let Some(ref mut fut) = self.fut { - return fut.poll(); - } - - if let Some(err) = self.err.take() { - return Err(err); - } - - // payload size - let limit = self.limit; - if let Some(len) = self.length.take() { - if len > limit { - return Err(UrlencodedError::Overflow); - } - } - - // future - let encoding = self.encoding; - let fut = self - .stream - .take() - .expect("UrlEncoded could not be used second time") - .from_err() - .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { - if (body.len() + chunk.len()) > limit { - Err(UrlencodedError::Overflow) - } else { - body.extend_from_slice(&chunk); - Ok(body) - } - }).and_then(move |body| { - if (encoding as *const Encoding) == UTF_8 { - serde_urlencoded::from_bytes::(&body) - .map_err(|_| UrlencodedError::Parse) - } else { - let body = encoding - .decode(&body, DecoderTrap::Strict) - .map_err(|_| UrlencodedError::Parse)?; - serde_urlencoded::from_str::(&body) - .map_err(|_| UrlencodedError::Parse) - } - }); - self.fut = Some(Box::new(fut)); - self.poll() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use encoding::all::ISO_8859_2; - use encoding::Encoding; - use futures::Async; - use mime; - use test::TestRequest; - - #[test] - fn test_content_type() { - let req = TestRequest::with_header("content-type", "text/plain").finish(); - assert_eq!(req.content_type(), "text/plain"); - let req = - TestRequest::with_header("content-type", "application/json; charset=utf=8") - .finish(); - assert_eq!(req.content_type(), "application/json"); - let req = TestRequest::default().finish(); - assert_eq!(req.content_type(), ""); - } - - #[test] - fn test_mime_type() { - let req = TestRequest::with_header("content-type", "application/json").finish(); - assert_eq!(req.mime_type().unwrap(), Some(mime::APPLICATION_JSON)); - let req = TestRequest::default().finish(); - assert_eq!(req.mime_type().unwrap(), None); - let req = - TestRequest::with_header("content-type", "application/json; charset=utf-8") - .finish(); - let mt = req.mime_type().unwrap().unwrap(); - assert_eq!(mt.get_param(mime::CHARSET), Some(mime::UTF_8)); - assert_eq!(mt.type_(), mime::APPLICATION); - assert_eq!(mt.subtype(), mime::JSON); - } - - #[test] - fn test_mime_type_error() { - let req = TestRequest::with_header( - "content-type", - "applicationadfadsfasdflknadsfklnadsfjson", - ).finish(); - assert_eq!(Err(ContentTypeError::ParseError), req.mime_type()); - } - - #[test] - fn test_encoding() { - let req = TestRequest::default().finish(); - assert_eq!(UTF_8.name(), req.encoding().unwrap().name()); - - let req = TestRequest::with_header("content-type", "application/json").finish(); - assert_eq!(UTF_8.name(), req.encoding().unwrap().name()); - - let req = TestRequest::with_header( - "content-type", - "application/json; charset=ISO-8859-2", - ).finish(); - assert_eq!(ISO_8859_2.name(), req.encoding().unwrap().name()); - } - - #[test] - fn test_encoding_error() { - let req = TestRequest::with_header("content-type", "applicatjson").finish(); - assert_eq!(Some(ContentTypeError::ParseError), req.encoding().err()); - - let req = TestRequest::with_header( - "content-type", - "application/json; charset=kkkttktk", - ).finish(); - assert_eq!( - Some(ContentTypeError::UnknownEncoding), - req.encoding().err() - ); - } - - #[test] - fn test_chunked() { - let req = TestRequest::default().finish(); - assert!(!req.chunked().unwrap()); - - let req = - TestRequest::with_header(header::TRANSFER_ENCODING, "chunked").finish(); - assert!(req.chunked().unwrap()); - - let req = TestRequest::default() - .header( - header::TRANSFER_ENCODING, - Bytes::from_static(b"some va\xadscc\xacas0xsdasdlue"), - ).finish(); - assert!(req.chunked().is_err()); - } - - impl PartialEq for UrlencodedError { - fn eq(&self, other: &UrlencodedError) -> bool { - match *self { - UrlencodedError::Chunked => match *other { - UrlencodedError::Chunked => true, - _ => false, - }, - UrlencodedError::Overflow => match *other { - UrlencodedError::Overflow => true, - _ => false, - }, - UrlencodedError::UnknownLength => match *other { - UrlencodedError::UnknownLength => true, - _ => false, - }, - UrlencodedError::ContentType => match *other { - UrlencodedError::ContentType => true, - _ => false, - }, - _ => false, - } - } - } - - #[derive(Deserialize, Debug, PartialEq)] - struct Info { - hello: String, - } - - #[test] - fn test_urlencoded_error() { - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "xxxx") - .finish(); - assert_eq!( - req.urlencoded::().poll().err().unwrap(), - UrlencodedError::UnknownLength - ); - - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "1000000") - .finish(); - assert_eq!( - req.urlencoded::().poll().err().unwrap(), - UrlencodedError::Overflow - ); - - let req = TestRequest::with_header(header::CONTENT_TYPE, "text/plain") - .header(header::CONTENT_LENGTH, "10") - .finish(); - assert_eq!( - req.urlencoded::().poll().err().unwrap(), - UrlencodedError::ContentType - ); - } - - #[test] - fn test_urlencoded() { - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ).header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .finish(); - - let result = req.urlencoded::().poll().ok().unwrap(); - assert_eq!( - result, - Async::Ready(Info { - hello: "world".to_owned() - }) - ); - - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded; charset=utf-8", - ).header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .finish(); - - let result = req.urlencoded().poll().ok().unwrap(); - assert_eq!( - result, - Async::Ready(Info { - hello: "world".to_owned() - }) - ); - } - - #[test] - fn test_message_body() { - let req = TestRequest::with_header(header::CONTENT_LENGTH, "xxxx").finish(); - match req.body().poll().err().unwrap() { - PayloadError::UnknownLength => (), - _ => unreachable!("error"), - } - - let req = TestRequest::with_header(header::CONTENT_LENGTH, "1000000").finish(); - match req.body().poll().err().unwrap() { - PayloadError::Overflow => (), - _ => unreachable!("error"), - } - - let req = TestRequest::default() - .set_payload(Bytes::from_static(b"test")) - .finish(); - match req.body().poll().ok().unwrap() { - Async::Ready(bytes) => assert_eq!(bytes, Bytes::from_static(b"test")), - _ => unreachable!("error"), - } - - let req = TestRequest::default() - .set_payload(Bytes::from_static(b"11111111111111")) - .finish(); - match req.body().limit(5).poll().err().unwrap() { - PayloadError::Overflow => (), - _ => unreachable!("error"), - } - } - - #[test] - fn test_readlines() { - let req = TestRequest::default() - .set_payload(Bytes::from_static( - b"Lorem Ipsum is simply dummy text of the printing and typesetting\n\ - industry. Lorem Ipsum has been the industry's standard dummy\n\ - Contrary to popular belief, Lorem Ipsum is not simply random text.", - )).finish(); - let mut r = Readlines::new(&req); - match r.poll().ok().unwrap() { - Async::Ready(Some(s)) => assert_eq!( - s, - "Lorem Ipsum is simply dummy text of the printing and typesetting\n" - ), - _ => unreachable!("error"), - } - match r.poll().ok().unwrap() { - Async::Ready(Some(s)) => assert_eq!( - s, - "industry. Lorem Ipsum has been the industry's standard dummy\n" - ), - _ => unreachable!("error"), - } - match r.poll().ok().unwrap() { - Async::Ready(Some(s)) => assert_eq!( - s, - "Contrary to popular belief, Lorem Ipsum is not simply random text." - ), - _ => unreachable!("error"), - } - } -} diff --git a/src/httprequest.rs b/src/httprequest.rs deleted file mode 100644 index 0e4f74e5..00000000 --- a/src/httprequest.rs +++ /dev/null @@ -1,545 +0,0 @@ -//! HTTP Request message related code. -use std::cell::{Ref, RefMut}; -use std::collections::HashMap; -use std::net::SocketAddr; -use std::ops::Deref; -use std::rc::Rc; -use std::{fmt, str}; - -use cookie::Cookie; -use futures_cpupool::CpuPool; -use http::{header, HeaderMap, Method, StatusCode, Uri, Version}; -use url::{form_urlencoded, Url}; - -use body::Body; -use error::{CookieParseError, UrlGenerationError}; -use extensions::Extensions; -use handler::FromRequest; -use httpmessage::HttpMessage; -use httpresponse::{HttpResponse, HttpResponseBuilder}; -use info::ConnectionInfo; -use param::Params; -use payload::Payload; -use router::ResourceInfo; -use server::Request; - -struct Query(HashMap); -struct Cookies(Vec>); - -/// An HTTP Request -pub struct HttpRequest { - req: Option, - state: Rc, - resource: ResourceInfo, -} - -impl HttpMessage for HttpRequest { - type Stream = Payload; - - #[inline] - fn headers(&self) -> &HeaderMap { - self.request().headers() - } - - #[inline] - fn payload(&self) -> Payload { - if let Some(payload) = self.request().inner.payload.borrow_mut().take() { - payload - } else { - Payload::empty() - } - } -} - -impl Deref for HttpRequest { - type Target = Request; - - fn deref(&self) -> &Request { - self.request() - } -} - -impl HttpRequest { - #[inline] - pub(crate) fn new( - req: Request, state: Rc, resource: ResourceInfo, - ) -> HttpRequest { - HttpRequest { - state, - resource, - req: Some(req), - } - } - - #[inline] - /// Construct new http request with state. - pub(crate) fn with_state(&self, state: Rc) -> HttpRequest { - HttpRequest { - state, - req: self.req.as_ref().map(|r| r.clone()), - resource: self.resource.clone(), - } - } - - /// Construct new http request with empty state. - pub fn drop_state(&self) -> HttpRequest { - HttpRequest { - state: Rc::new(()), - req: self.req.as_ref().map(|r| r.clone()), - resource: self.resource.clone(), - } - } - - #[inline] - /// Construct new http request with new RouteInfo. - pub(crate) fn with_route_info(&self, mut resource: ResourceInfo) -> HttpRequest { - resource.merge(&self.resource); - - HttpRequest { - resource, - req: self.req.as_ref().map(|r| r.clone()), - state: self.state.clone(), - } - } - - /// Shared application state - #[inline] - pub fn state(&self) -> &S { - &self.state - } - - #[inline] - /// Server request - pub fn request(&self) -> &Request { - self.req.as_ref().unwrap() - } - - /// Request extensions - #[inline] - pub fn extensions(&self) -> Ref { - self.request().extensions() - } - - /// Mutable reference to a the request's extensions - #[inline] - pub fn extensions_mut(&self) -> RefMut { - self.request().extensions_mut() - } - - /// Default `CpuPool` - #[inline] - #[doc(hidden)] - pub fn cpu_pool(&self) -> &CpuPool { - self.request().server_settings().cpu_pool() - } - - #[inline] - /// Create http response - pub fn response(&self, status: StatusCode, body: Body) -> HttpResponse { - self.request().server_settings().get_response(status, body) - } - - #[inline] - /// Create http response builder - pub fn build_response(&self, status: StatusCode) -> HttpResponseBuilder { - self.request() - .server_settings() - .get_response_builder(status) - } - - /// Read the Request Uri. - #[inline] - pub fn uri(&self) -> &Uri { - self.request().inner.url.uri() - } - - /// Read the Request method. - #[inline] - pub fn method(&self) -> &Method { - &self.request().inner.method - } - - /// Read the Request Version. - #[inline] - pub fn version(&self) -> Version { - self.request().inner.version - } - - /// The target path of this Request. - #[inline] - pub fn path(&self) -> &str { - self.request().inner.url.path() - } - - /// Get *ConnectionInfo* for the correct request. - #[inline] - pub fn connection_info(&self) -> Ref { - self.request().connection_info() - } - - /// Generate url for named resource - /// - /// ```rust - /// # extern crate actix_web; - /// # use actix_web::{App, HttpRequest, HttpResponse, http}; - /// # - /// fn index(req: HttpRequest) -> HttpResponse { - /// let url = req.url_for("foo", &["1", "2", "3"]); // <- generate url for "foo" resource - /// HttpResponse::Ok().into() - /// } - /// - /// fn main() { - /// let app = App::new() - /// .resource("/test/{one}/{two}/{three}", |r| { - /// r.name("foo"); // <- set resource name, then it could be used in `url_for` - /// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); - /// }) - /// .finish(); - /// } - /// ``` - pub fn url_for( - &self, name: &str, elements: U, - ) -> Result - where - U: IntoIterator, - I: AsRef, - { - self.resource.url_for(&self, name, elements) - } - - /// Generate url for named resource - /// - /// This method is similar to `HttpRequest::url_for()` but it can be used - /// for urls that do not contain variable parts. - pub fn url_for_static(&self, name: &str) -> Result { - const NO_PARAMS: [&str; 0] = []; - self.url_for(name, &NO_PARAMS) - } - - /// This method returns reference to current `ResourceInfo` object. - #[inline] - pub fn resource(&self) -> &ResourceInfo { - &self.resource - } - - /// 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()` method should - /// be used. - #[inline] - pub fn peer_addr(&self) -> Option { - self.request().inner.addr - } - - /// url query parameters. - pub fn query(&self) -> Ref> { - if self.extensions().get::().is_none() { - let mut query = HashMap::new(); - for (key, val) in form_urlencoded::parse(self.query_string().as_ref()) { - query.insert(key.as_ref().to_string(), val.to_string()); - } - self.extensions_mut().insert(Query(query)); - } - Ref::map(self.extensions(), |ext| &ext.get::().unwrap().0) - } - - /// 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 { - "" - } - } - - /// Load request cookies. - #[inline] - pub fn cookies(&self) -> Result>>, CookieParseError> { - if self.extensions().get::().is_none() { - let mut cookies = Vec::new(); - for hdr in self.request().inner.headers.get_all(header::COOKIE) { - let s = - str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?; - for cookie_str in s.split(';').map(|s| s.trim()) { - if !cookie_str.is_empty() { - cookies.push(Cookie::parse_encoded(cookie_str)?.into_owned()); - } - } - } - self.extensions_mut().insert(Cookies(cookies)); - } - Ok(Ref::map(self.extensions(), |ext| { - &ext.get::().unwrap().0 - })) - } - - /// Return request cookie. - #[inline] - pub fn cookie(&self, name: &str) -> Option> { - if let Ok(cookies) = self.cookies() { - for cookie in cookies.iter() { - if cookie.name() == name { - return Some(cookie.to_owned()); - } - } - } - None - } - - pub(crate) fn set_cookies(&mut self, cookies: Option>>) { - if let Some(cookies) = cookies { - self.extensions_mut().insert(Cookies(cookies)); - } - } - - /// Get a reference to the Params object. - /// - /// 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) -> &Params { - &self.resource.match_info() - } - - /// Check if request requires connection upgrade - pub(crate) fn upgrade(&self) -> bool { - self.request().upgrade() - } - - /// Set read buffer capacity - /// - /// Default buffer capacity is 32Kb. - pub fn set_read_buffer_capacity(&mut self, cap: usize) { - if let Some(payload) = self.request().inner.payload.borrow_mut().as_mut() { - payload.set_read_buffer_capacity(cap) - } - } -} - -impl Drop for HttpRequest { - fn drop(&mut self) { - if let Some(req) = self.req.take() { - req.release(); - } - } -} - -impl Clone for HttpRequest { - fn clone(&self) -> HttpRequest { - HttpRequest { - req: self.req.as_ref().map(|r| r.clone()), - state: self.state.clone(), - resource: self.resource.clone(), - } - } -} - -impl FromRequest for HttpRequest { - type Config = (); - type Result = Self; - - #[inline] - fn from_request(req: &HttpRequest, _: &Self::Config) -> Self::Result { - req.clone() - } -} - -impl fmt::Debug for HttpRequest { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!( - f, - "\nHttpRequest {:?} {}:{}", - self.version(), - self.method(), - self.path() - )?; - if !self.query_string().is_empty() { - writeln!(f, " query: ?{:?}", self.query_string())?; - } - if !self.match_info().is_empty() { - writeln!(f, " params: {:?}", self.match_info())?; - } - writeln!(f, " headers:")?; - for (key, val) in self.headers().iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use resource::Resource; - use router::{ResourceDef, Router}; - use test::TestRequest; - - #[test] - fn test_debug() { - let req = TestRequest::with_header("content-type", "text/plain").finish(); - let dbg = format!("{:?}", req); - assert!(dbg.contains("HttpRequest")); - } - - #[test] - fn test_no_request_cookies() { - let req = TestRequest::default().finish(); - assert!(req.cookies().unwrap().is_empty()); - } - - #[test] - fn test_request_cookies() { - let req = TestRequest::default() - .header(header::COOKIE, "cookie1=value1") - .header(header::COOKIE, "cookie2=value2") - .finish(); - { - let cookies = req.cookies().unwrap(); - assert_eq!(cookies.len(), 2); - assert_eq!(cookies[0].name(), "cookie1"); - assert_eq!(cookies[0].value(), "value1"); - assert_eq!(cookies[1].name(), "cookie2"); - assert_eq!(cookies[1].value(), "value2"); - } - - let cookie = req.cookie("cookie1"); - assert!(cookie.is_some()); - let cookie = cookie.unwrap(); - assert_eq!(cookie.name(), "cookie1"); - assert_eq!(cookie.value(), "value1"); - - let cookie = req.cookie("cookie-unknown"); - assert!(cookie.is_none()); - } - - #[test] - fn test_request_query() { - let req = TestRequest::with_uri("/?id=test").finish(); - assert_eq!(req.query_string(), "id=test"); - let query = req.query(); - assert_eq!(&query["id"], "test"); - } - - #[test] - fn test_request_match_info() { - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/{key}/"))); - - let req = TestRequest::with_uri("/value/?id=test").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.match_info().get("key"), Some("value")); - } - - #[test] - fn test_url_for() { - let mut router = Router::<()>::default(); - let mut resource = Resource::new(ResourceDef::new("/user/{name}.{ext}")); - resource.name("index"); - router.register_resource(resource); - - let info = router.default_route_info(); - assert!(!info.has_prefixed_resource("/use/")); - assert!(info.has_resource("/user/test.html")); - assert!(info.has_prefixed_resource("/user/test.html")); - assert!(!info.has_resource("/test/unknown")); - assert!(!info.has_prefixed_resource("/test/unknown")); - - let req = TestRequest::with_header(header::HOST, "www.rust-lang.org") - .finish_with_router(router); - - assert_eq!( - req.url_for("unknown", &["test"]), - Err(UrlGenerationError::ResourceNotFound) - ); - assert_eq!( - req.url_for("index", &["test"]), - Err(UrlGenerationError::NotEnoughElements) - ); - let url = req.url_for("index", &["test", "html"]); - assert_eq!( - url.ok().unwrap().as_str(), - "http://www.rust-lang.org/user/test.html" - ); - } - - #[test] - fn test_url_for_with_prefix() { - let mut resource = Resource::new(ResourceDef::new("/user/{name}.html")); - resource.name("index"); - let mut router = Router::<()>::default(); - router.set_prefix("/prefix"); - router.register_resource(resource); - - let mut info = router.default_route_info(); - info.set_prefix(7); - assert!(!info.has_prefixed_resource("/use/")); - assert!(info.has_resource("/user/test.html")); - assert!(!info.has_prefixed_resource("/user/test.html")); - assert!(!info.has_resource("/prefix/user/test.html")); - assert!(info.has_prefixed_resource("/prefix/user/test.html")); - - let req = TestRequest::with_uri("/prefix/test") - .prefix(7) - .header(header::HOST, "www.rust-lang.org") - .finish_with_router(router); - let url = req.url_for("index", &["test"]); - assert_eq!( - url.ok().unwrap().as_str(), - "http://www.rust-lang.org/prefix/user/test.html" - ); - } - - #[test] - fn test_url_for_static() { - let mut resource = Resource::new(ResourceDef::new("/index.html")); - resource.name("index"); - let mut router = Router::<()>::default(); - router.set_prefix("/prefix"); - router.register_resource(resource); - - let mut info = router.default_route_info(); - info.set_prefix(7); - assert!(info.has_resource("/index.html")); - assert!(!info.has_prefixed_resource("/index.html")); - assert!(!info.has_resource("/prefix/index.html")); - assert!(info.has_prefixed_resource("/prefix/index.html")); - - let req = TestRequest::with_uri("/prefix/test") - .prefix(7) - .header(header::HOST, "www.rust-lang.org") - .finish_with_router(router); - let url = req.url_for_static("index"); - assert_eq!( - url.ok().unwrap().as_str(), - "http://www.rust-lang.org/prefix/index.html" - ); - } - - #[test] - fn test_url_for_external() { - let mut router = Router::<()>::default(); - router.register_external( - "youtube", - ResourceDef::external("https://youtube.com/watch/{video_id}"), - ); - - let info = router.default_route_info(); - assert!(!info.has_resource("https://youtube.com/watch/unknown")); - assert!(!info.has_prefixed_resource("https://youtube.com/watch/unknown")); - - let req = TestRequest::default().finish_with_router(router); - let url = req.url_for("youtube", &["oHg5SJYRHA0"]); - assert_eq!( - url.ok().unwrap().as_str(), - "https://youtube.com/watch/oHg5SJYRHA0" - ); - } -} diff --git a/src/httpresponse.rs b/src/httpresponse.rs deleted file mode 100644 index 226c847f..00000000 --- a/src/httpresponse.rs +++ /dev/null @@ -1,1458 +0,0 @@ -//! Http response -use std::cell::RefCell; -use std::collections::VecDeque; -use std::io::Write; -use std::{fmt, mem, str}; - -use bytes::{BufMut, Bytes, BytesMut}; -use cookie::{Cookie, CookieJar}; -use futures::Stream; -use http::header::{self, HeaderName, HeaderValue}; -use http::{Error as HttpError, HeaderMap, HttpTryFrom, StatusCode, Version}; -use serde::Serialize; -use serde_json; - -use body::Body; -use client::ClientResponse; -use error::Error; -use handler::Responder; -use header::{ContentEncoding, Header, IntoHeaderValue}; -use httpmessage::HttpMessage; -use httprequest::HttpRequest; - -/// max write buffer size 64k -pub(crate) const MAX_WRITE_BUFFER_SIZE: usize = 65_536; - -/// Represents various types of connection -#[derive(Copy, Clone, PartialEq, Debug)] -pub enum ConnectionType { - /// Close connection after response - Close, - /// Keep connection alive after response - KeepAlive, - /// Connection is upgraded to different type - Upgrade, -} - -/// An HTTP Response -pub struct HttpResponse(Box, &'static HttpResponsePool); - -impl HttpResponse { - #[inline] - fn get_ref(&self) -> &InnerHttpResponse { - self.0.as_ref() - } - - #[inline] - fn get_mut(&mut self) -> &mut InnerHttpResponse { - self.0.as_mut() - } - - /// Create http response builder with specific status. - #[inline] - pub fn build(status: StatusCode) -> HttpResponseBuilder { - HttpResponsePool::get(status) - } - - /// Create http response builder - #[inline] - pub fn build_from>(source: T) -> HttpResponseBuilder { - source.into() - } - - /// Constructs a response - #[inline] - pub fn new(status: StatusCode) -> HttpResponse { - HttpResponsePool::with_body(status, Body::Empty) - } - - /// Constructs a response with body - #[inline] - pub fn with_body>(status: StatusCode, body: B) -> HttpResponse { - HttpResponsePool::with_body(status, body.into()) - } - - /// Constructs an error response - #[inline] - pub fn from_error(error: Error) -> HttpResponse { - let mut resp = error.as_response_error().error_response(); - resp.get_mut().error = Some(error); - resp - } - - /// Convert `HttpResponse` to a `HttpResponseBuilder` - #[inline] - pub fn into_builder(self) -> HttpResponseBuilder { - // If this response has cookies, load them into a jar - let mut jar: Option = None; - for c in self.cookies() { - if let Some(ref mut j) = jar { - j.add_original(c.into_owned()); - } else { - let mut j = CookieJar::new(); - j.add_original(c.into_owned()); - jar = Some(j); - } - } - - HttpResponseBuilder { - pool: self.1, - response: Some(self.0), - err: None, - cookies: jar, - } - } - - /// The source `error` for this response - #[inline] - pub fn error(&self) -> Option<&Error> { - self.get_ref().error.as_ref() - } - - /// Get the HTTP version of this response - #[inline] - pub fn version(&self) -> Option { - self.get_ref().version - } - - /// Get the headers from the response - #[inline] - pub fn headers(&self) -> &HeaderMap { - &self.get_ref().headers - } - - /// Get a mutable reference to the headers - #[inline] - pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.get_mut().headers - } - - /// Get an iterator for the cookies set by this response - #[inline] - pub fn cookies(&self) -> CookieIter { - CookieIter { - iter: self.get_ref().headers.get_all(header::SET_COOKIE).iter(), - } - } - - /// Add a cookie to this response - #[inline] - pub fn add_cookie(&mut self, cookie: &Cookie) -> Result<(), HttpError> { - let h = &mut self.get_mut().headers; - HeaderValue::from_str(&cookie.to_string()) - .map(|c| { - h.append(header::SET_COOKIE, c); - }).map_err(|e| e.into()) - } - - /// Remove all cookies with the given name from this response. Returns - /// the number of cookies removed. - #[inline] - pub fn del_cookie(&mut self, name: &str) -> usize { - let h = &mut self.get_mut().headers; - let vals: Vec = h - .get_all(header::SET_COOKIE) - .iter() - .map(|v| v.to_owned()) - .collect(); - h.remove(header::SET_COOKIE); - - let mut count: usize = 0; - for v in vals { - if let Ok(s) = v.to_str() { - if let Ok(c) = Cookie::parse_encoded(s) { - if c.name() == name { - count += 1; - continue; - } - } - } - h.append(header::SET_COOKIE, v); - } - count - } - - /// Get the response status code - #[inline] - pub fn status(&self) -> StatusCode { - self.get_ref().status - } - - /// Set the `StatusCode` for this response - #[inline] - pub fn status_mut(&mut self) -> &mut StatusCode { - &mut self.get_mut().status - } - - /// Get custom reason for the response - #[inline] - pub fn reason(&self) -> &str { - if let Some(reason) = self.get_ref().reason { - reason - } else { - self.get_ref() - .status - .canonical_reason() - .unwrap_or("") - } - } - - /// Set the custom reason for the response - #[inline] - pub fn set_reason(&mut self, reason: &'static str) -> &mut Self { - self.get_mut().reason = Some(reason); - self - } - - /// Set connection type - pub fn set_connection_type(&mut self, conn: ConnectionType) -> &mut Self { - self.get_mut().connection_type = Some(conn); - self - } - - /// Connection upgrade status - #[inline] - pub fn upgrade(&self) -> bool { - self.get_ref().connection_type == Some(ConnectionType::Upgrade) - } - - /// Keep-alive status for this connection - pub fn keep_alive(&self) -> Option { - if let Some(ct) = self.get_ref().connection_type { - match ct { - ConnectionType::KeepAlive => Some(true), - ConnectionType::Close | ConnectionType::Upgrade => Some(false), - } - } else { - None - } - } - - /// is chunked encoding enabled - #[inline] - pub fn chunked(&self) -> Option { - self.get_ref().chunked - } - - /// Content encoding - #[inline] - pub fn content_encoding(&self) -> Option { - self.get_ref().encoding - } - - /// Set content encoding - pub fn set_content_encoding(&mut self, enc: ContentEncoding) -> &mut Self { - self.get_mut().encoding = Some(enc); - self - } - - /// Get body of this response - #[inline] - pub fn body(&self) -> &Body { - &self.get_ref().body - } - - /// Set a body - pub fn set_body>(&mut self, body: B) { - self.get_mut().body = body.into(); - } - - /// Set a body and return previous body value - pub fn replace_body>(&mut self, body: B) -> Body { - mem::replace(&mut self.get_mut().body, body.into()) - } - - /// Size of response in bytes, excluding HTTP headers - pub fn response_size(&self) -> u64 { - self.get_ref().response_size - } - - /// Set content encoding - pub(crate) fn set_response_size(&mut self, size: u64) { - self.get_mut().response_size = size; - } - - /// Get write buffer capacity - pub fn write_buffer_capacity(&self) -> usize { - self.get_ref().write_capacity - } - - /// Set write buffer capacity - pub fn set_write_buffer_capacity(&mut self, cap: usize) { - self.get_mut().write_capacity = cap; - } - - pub(crate) fn release(self) { - self.1.release(self.0); - } - - pub(crate) fn into_parts(self) -> HttpResponseParts { - self.0.into_parts() - } - - pub(crate) fn from_parts(parts: HttpResponseParts) -> HttpResponse { - HttpResponse( - Box::new(InnerHttpResponse::from_parts(parts)), - HttpResponsePool::get_pool(), - ) - } -} - -impl fmt::Debug for HttpResponse { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let res = writeln!( - f, - "\nHttpResponse {:?} {}{}", - self.get_ref().version, - self.get_ref().status, - self.get_ref().reason.unwrap_or("") - ); - let _ = writeln!(f, " encoding: {:?}", self.get_ref().encoding); - let _ = writeln!(f, " headers:"); - for (key, val) in self.get_ref().headers.iter() { - let _ = writeln!(f, " {:?}: {:?}", key, val); - } - res - } -} - -pub struct CookieIter<'a> { - iter: header::ValueIter<'a, HeaderValue>, -} - -impl<'a> Iterator for CookieIter<'a> { - type Item = Cookie<'a>; - - #[inline] - fn next(&mut self) -> Option> { - for v in self.iter.by_ref() { - if let Ok(c) = Cookie::parse_encoded(v.to_str().ok()?) { - return Some(c); - } - } - None - } -} - -/// An HTTP response builder -/// -/// This type can be used to construct an instance of `HttpResponse` through a -/// builder-like pattern. -pub struct HttpResponseBuilder { - pool: &'static HttpResponsePool, - response: Option>, - err: Option, - cookies: Option, -} - -impl HttpResponseBuilder { - /// Set HTTP status code of this response. - #[inline] - pub fn status(&mut self, status: StatusCode) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.status = status; - } - self - } - - /// Set HTTP version of this response. - /// - /// By default response's http version depends on request's version. - #[inline] - pub fn version(&mut self, version: Version) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.version = Some(version); - } - self - } - - /// Append a header. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, HttpRequest, HttpResponse, Result}; - /// - /// fn index(req: HttpRequest) -> Result { - /// Ok(HttpResponse::Ok() - /// .set(http::header::IfModifiedSince( - /// "Sun, 07 Nov 1994 08:48:37 GMT".parse()?, - /// )) - /// .finish()) - /// } - /// fn main() {} - /// ``` - #[doc(hidden)] - pub fn set(&mut self, hdr: H) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - match hdr.try_into() { - Ok(value) => { - parts.headers.append(H::name(), value); - } - Err(e) => self.err = Some(e.into()), - } - } - self - } - - /// Append a header. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, HttpRequest, HttpResponse}; - /// - /// fn index(req: HttpRequest) -> HttpResponse { - /// HttpResponse::Ok() - /// .header("X-TEST", "value") - /// .header(http::header::CONTENT_TYPE, "application/json") - /// .finish() - /// } - /// fn main() {} - /// ``` - pub fn header(&mut self, key: K, value: V) -> &mut Self - where - HeaderName: HttpTryFrom, - V: IntoHeaderValue, - { - if let Some(parts) = parts(&mut self.response, &self.err) { - match HeaderName::try_from(key) { - Ok(key) => match value.try_into() { - Ok(value) => { - parts.headers.append(key, value); - } - Err(e) => self.err = Some(e.into()), - }, - Err(e) => self.err = Some(e.into()), - }; - } - self - } - /// Set or replace a header with a single value. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, HttpRequest, HttpResponse}; - /// - /// fn index(req: HttpRequest) -> HttpResponse { - /// HttpResponse::Ok() - /// .insert("X-TEST", "value") - /// .insert(http::header::CONTENT_TYPE, "application/json") - /// .finish() - /// } - /// fn main() {} - /// ``` - pub fn insert(&mut self, key: K, value: V) -> &mut Self - where - HeaderName: HttpTryFrom, - V: IntoHeaderValue, - { - if let Some(parts) = parts(&mut self.response, &self.err) { - match HeaderName::try_from(key) { - Ok(key) => match value.try_into() { - Ok(value) => { - parts.headers.insert(key, value); - } - Err(e) => self.err = Some(e.into()), - }, - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Remove all instances of a header already set on this `HttpResponseBuilder`. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, HttpRequest, HttpResponse}; - /// - /// fn index(req: HttpRequest) -> HttpResponse { - /// HttpResponse::Ok() - /// .header(http::header::CONTENT_TYPE, "nevermind") // won't be used - /// .remove(http::header::CONTENT_TYPE) - /// .finish() - /// } - /// ``` - pub fn remove(&mut self, key: K) -> &mut Self - where HeaderName: HttpTryFrom - { - if let Some(parts) = parts(&mut self.response, &self.err) { - match HeaderName::try_from(key) { - Ok(key) => { - parts.headers.remove(key); - }, - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Set the custom reason for the response. - #[inline] - pub fn reason(&mut self, reason: &'static str) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.reason = Some(reason); - } - self - } - - /// Set content encoding. - /// - /// By default `ContentEncoding::Auto` is used, which automatically - /// negotiates content encoding based on request's `Accept-Encoding` - /// headers. To enforce specific encoding, use specific - /// ContentEncoding` value. - #[inline] - pub fn content_encoding(&mut self, enc: ContentEncoding) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.encoding = Some(enc); - } - self - } - - /// Set connection type - #[inline] - #[doc(hidden)] - pub fn connection_type(&mut self, conn: ConnectionType) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.connection_type = Some(conn); - } - self - } - - /// Set connection type to Upgrade - #[inline] - #[doc(hidden)] - pub fn upgrade(&mut self) -> &mut Self { - self.connection_type(ConnectionType::Upgrade) - } - - /// Force close connection, even if it is marked as keep-alive - #[inline] - pub fn force_close(&mut self) -> &mut Self { - self.connection_type(ConnectionType::Close) - } - - /// Enables automatic chunked transfer encoding - #[inline] - pub fn chunked(&mut self) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.chunked = Some(true); - } - self - } - - /// Force disable chunked encoding - #[inline] - pub fn no_chunking(&mut self) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.chunked = Some(false); - } - self - } - - /// Set response content type - #[inline] - pub fn content_type(&mut self, value: V) -> &mut Self - where - HeaderValue: HttpTryFrom, - { - if let Some(parts) = parts(&mut self.response, &self.err) { - match HeaderValue::try_from(value) { - Ok(value) => { - parts.headers.insert(header::CONTENT_TYPE, value); - } - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Set content length - #[inline] - pub fn content_length(&mut self, len: u64) -> &mut Self { - let mut wrt = BytesMut::new().writer(); - let _ = write!(wrt, "{}", len); - self.header(header::CONTENT_LENGTH, wrt.get_mut().take().freeze()) - } - - /// Set a cookie - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, HttpRequest, HttpResponse, Result}; - /// - /// fn index(req: HttpRequest) -> HttpResponse { - /// HttpResponse::Ok() - /// .cookie( - /// http::Cookie::build("name", "value") - /// .domain("www.rust-lang.org") - /// .path("/") - /// .secure(true) - /// .http_only(true) - /// .finish(), - /// ) - /// .finish() - /// } - /// ``` - pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self { - if self.cookies.is_none() { - let mut jar = CookieJar::new(); - jar.add(cookie.into_owned()); - self.cookies = Some(jar) - } else { - self.cookies.as_mut().unwrap().add(cookie.into_owned()); - } - self - } - - /// Remove cookie - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, HttpRequest, HttpResponse, Result}; - /// - /// fn index(req: &HttpRequest) -> HttpResponse { - /// let mut builder = HttpResponse::Ok(); - /// - /// if let Some(ref cookie) = req.cookie("name") { - /// builder.del_cookie(cookie); - /// } - /// - /// builder.finish() - /// } - /// ``` - 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); - } - self - } - - /// This method calls provided closure with builder reference if value is - /// true. - pub fn if_true(&mut self, value: bool, f: F) -> &mut Self - where - F: FnOnce(&mut HttpResponseBuilder), - { - if value { - f(self); - } - self - } - - /// This method calls provided closure with builder reference if value is - /// Some. - pub fn if_some(&mut self, value: Option, f: F) -> &mut Self - where - F: FnOnce(T, &mut HttpResponseBuilder), - { - if let Some(val) = value { - f(val, self); - } - self - } - - /// Set write buffer capacity - /// - /// This parameter makes sense only for streaming response - /// or actor. If write buffer reaches specified capacity, stream or actor - /// get paused. - /// - /// Default write buffer capacity is 64kb - pub fn write_buffer_capacity(&mut self, cap: usize) -> &mut Self { - if let Some(parts) = parts(&mut self.response, &self.err) { - parts.write_capacity = cap; - } - self - } - - /// Set a body and generate `HttpResponse`. - /// - /// `HttpResponseBuilder` can not be used after this call. - pub fn body>(&mut self, body: B) -> HttpResponse { - if let Some(e) = self.err.take() { - return Error::from(e).into(); - } - let mut response = self.response.take().expect("cannot reuse response builder"); - if let Some(ref jar) = self.cookies { - for cookie in jar.delta() { - match HeaderValue::from_str(&cookie.to_string()) { - Ok(val) => response.headers.append(header::SET_COOKIE, val), - Err(e) => return Error::from(e).into(), - }; - } - } - response.body = body.into(); - HttpResponse(response, self.pool) - } - - #[inline] - /// Set a streaming body and generate `HttpResponse`. - /// - /// `HttpResponseBuilder` can not be used after this call. - pub fn streaming(&mut self, stream: S) -> HttpResponse - where - S: Stream + 'static, - E: Into, - { - self.body(Body::Streaming(Box::new(stream.map_err(|e| e.into())))) - } - - /// Set a json body and generate `HttpResponse` - /// - /// `HttpResponseBuilder` can not be used after this call. - pub fn json(&mut self, value: T) -> HttpResponse { - self.json2(&value) - } - - /// Set a json body and generate `HttpResponse` - /// - /// `HttpResponseBuilder` can not be used after this call. - pub fn json2(&mut self, value: &T) -> HttpResponse { - match serde_json::to_string(value) { - Ok(body) => { - let contains = if let Some(parts) = parts(&mut self.response, &self.err) - { - parts.headers.contains_key(header::CONTENT_TYPE) - } else { - true - }; - if !contains { - self.header(header::CONTENT_TYPE, "application/json"); - } - - self.body(body) - } - Err(e) => Error::from(e).into(), - } - } - - #[inline] - /// Set an empty body and generate `HttpResponse` - /// - /// `HttpResponseBuilder` can not be used after this call. - pub fn finish(&mut self) -> HttpResponse { - self.body(Body::Empty) - } - - /// This method construct new `HttpResponseBuilder` - pub fn take(&mut self) -> HttpResponseBuilder { - HttpResponseBuilder { - pool: self.pool, - response: self.response.take(), - err: self.err.take(), - cookies: self.cookies.take(), - } - } -} - -#[inline] -#[cfg_attr(feature = "cargo-clippy", allow(borrowed_box))] -fn parts<'a>( - parts: &'a mut Option>, err: &Option, -) -> Option<&'a mut Box> { - if err.is_some() { - return None; - } - parts.as_mut() -} - -/// Helper converters -impl, E: Into> From> for HttpResponse { - fn from(res: Result) -> Self { - match res { - Ok(val) => val.into(), - Err(err) => err.into().into(), - } - } -} - -impl From for HttpResponse { - fn from(mut builder: HttpResponseBuilder) -> Self { - builder.finish() - } -} - -impl Responder for HttpResponseBuilder { - type Item = HttpResponse; - type Error = Error; - - #[inline] - fn respond_to(mut self, _: &HttpRequest) -> Result { - Ok(self.finish()) - } -} - -impl From<&'static str> for HttpResponse { - fn from(val: &'static str) -> Self { - HttpResponse::Ok() - .content_type("text/plain; charset=utf-8") - .body(val) - } -} - -impl Responder for &'static str { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - Ok(req - .build_response(StatusCode::OK) - .content_type("text/plain; charset=utf-8") - .body(self)) - } -} - -impl From<&'static [u8]> for HttpResponse { - fn from(val: &'static [u8]) -> Self { - HttpResponse::Ok() - .content_type("application/octet-stream") - .body(val) - } -} - -impl Responder for &'static [u8] { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - Ok(req - .build_response(StatusCode::OK) - .content_type("application/octet-stream") - .body(self)) - } -} - -impl From for HttpResponse { - fn from(val: String) -> Self { - HttpResponse::Ok() - .content_type("text/plain; charset=utf-8") - .body(val) - } -} - -impl Responder for String { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - Ok(req - .build_response(StatusCode::OK) - .content_type("text/plain; charset=utf-8") - .body(self)) - } -} - -impl<'a> From<&'a String> for HttpResponse { - fn from(val: &'a String) -> Self { - HttpResponse::build(StatusCode::OK) - .content_type("text/plain; charset=utf-8") - .body(val) - } -} - -impl<'a> Responder for &'a String { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - Ok(req - .build_response(StatusCode::OK) - .content_type("text/plain; charset=utf-8") - .body(self)) - } -} - -impl From for HttpResponse { - fn from(val: Bytes) -> Self { - HttpResponse::Ok() - .content_type("application/octet-stream") - .body(val) - } -} - -impl Responder for Bytes { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - Ok(req - .build_response(StatusCode::OK) - .content_type("application/octet-stream") - .body(self)) - } -} - -impl From for HttpResponse { - fn from(val: BytesMut) -> Self { - HttpResponse::Ok() - .content_type("application/octet-stream") - .body(val) - } -} - -impl Responder for BytesMut { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - Ok(req - .build_response(StatusCode::OK) - .content_type("application/octet-stream") - .body(self)) - } -} - -/// Create `HttpResponseBuilder` from `ClientResponse` -/// -/// It is useful for proxy response. This implementation -/// copies all responses's headers and status. -impl<'a> From<&'a ClientResponse> for HttpResponseBuilder { - fn from(resp: &'a ClientResponse) -> HttpResponseBuilder { - let mut builder = HttpResponse::build(resp.status()); - for (key, value) in resp.headers() { - builder.header(key.clone(), value.clone()); - } - builder - } -} - -impl<'a, S> From<&'a HttpRequest> for HttpResponseBuilder { - fn from(req: &'a HttpRequest) -> HttpResponseBuilder { - req.request() - .server_settings() - .get_response_builder(StatusCode::OK) - } -} - -#[derive(Debug)] -struct InnerHttpResponse { - version: Option, - headers: HeaderMap, - status: StatusCode, - reason: Option<&'static str>, - body: Body, - chunked: Option, - encoding: Option, - connection_type: Option, - write_capacity: usize, - response_size: u64, - error: Option, -} - -pub(crate) struct HttpResponseParts { - version: Option, - headers: HeaderMap, - status: StatusCode, - reason: Option<&'static str>, - body: Option, - encoding: Option, - connection_type: Option, - error: Option, -} - -impl InnerHttpResponse { - #[inline] - fn new(status: StatusCode, body: Body) -> InnerHttpResponse { - InnerHttpResponse { - status, - body, - version: None, - headers: HeaderMap::with_capacity(16), - reason: None, - chunked: None, - encoding: None, - connection_type: None, - response_size: 0, - write_capacity: MAX_WRITE_BUFFER_SIZE, - error: None, - } - } - - /// This is for failure, we can not have Send + Sync on Streaming and Actor response - fn into_parts(mut self) -> HttpResponseParts { - let body = match mem::replace(&mut self.body, Body::Empty) { - Body::Empty => None, - Body::Binary(mut bin) => Some(bin.take()), - Body::Streaming(_) | Body::Actor(_) => { - error!("Streaming or Actor body is not support by error response"); - None - } - }; - - HttpResponseParts { - body, - version: self.version, - headers: self.headers, - status: self.status, - reason: self.reason, - encoding: self.encoding, - connection_type: self.connection_type, - error: self.error, - } - } - - fn from_parts(parts: HttpResponseParts) -> InnerHttpResponse { - let body = if let Some(ref body) = parts.body { - Body::Binary(body.clone().into()) - } else { - Body::Empty - }; - - InnerHttpResponse { - body, - status: parts.status, - version: parts.version, - headers: parts.headers, - reason: parts.reason, - chunked: None, - encoding: parts.encoding, - connection_type: parts.connection_type, - response_size: 0, - write_capacity: MAX_WRITE_BUFFER_SIZE, - error: parts.error, - } - } -} - -/// Internal use only! -pub(crate) struct HttpResponsePool(RefCell>>); - -thread_local!(static POOL: &'static HttpResponsePool = HttpResponsePool::pool()); - -impl HttpResponsePool { - fn pool() -> &'static HttpResponsePool { - let pool = HttpResponsePool(RefCell::new(VecDeque::with_capacity(128))); - Box::leak(Box::new(pool)) - } - - pub fn get_pool() -> &'static HttpResponsePool { - POOL.with(|p| *p) - } - - #[inline] - pub fn get_builder( - pool: &'static HttpResponsePool, status: StatusCode, - ) -> HttpResponseBuilder { - if let Some(mut msg) = pool.0.borrow_mut().pop_front() { - msg.status = status; - HttpResponseBuilder { - pool, - response: Some(msg), - err: None, - cookies: None, - } - } else { - let msg = Box::new(InnerHttpResponse::new(status, Body::Empty)); - HttpResponseBuilder { - pool, - response: Some(msg), - err: None, - cookies: None, - } - } - } - - #[inline] - pub fn get_response( - pool: &'static HttpResponsePool, status: StatusCode, body: Body, - ) -> HttpResponse { - if let Some(mut msg) = pool.0.borrow_mut().pop_front() { - msg.status = status; - msg.body = body; - HttpResponse(msg, pool) - } else { - let msg = Box::new(InnerHttpResponse::new(status, body)); - HttpResponse(msg, pool) - } - } - - #[inline] - fn get(status: StatusCode) -> HttpResponseBuilder { - POOL.with(|pool| HttpResponsePool::get_builder(pool, status)) - } - - #[inline] - fn with_body(status: StatusCode, body: Body) -> HttpResponse { - POOL.with(|pool| HttpResponsePool::get_response(pool, status, body)) - } - - #[inline] - fn release(&self, mut inner: Box) { - let mut p = self.0.borrow_mut(); - if p.len() < 128 { - inner.headers.clear(); - inner.version = None; - inner.chunked = None; - inner.reason = None; - inner.encoding = None; - inner.connection_type = None; - inner.response_size = 0; - inner.error = None; - inner.write_capacity = MAX_WRITE_BUFFER_SIZE; - p.push_front(inner); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use body::Binary; - use http; - use http::header::{HeaderValue, CONTENT_TYPE, COOKIE}; - use time::Duration; - - use test::TestRequest; - - #[test] - fn test_debug() { - let resp = HttpResponse::Ok() - .header(COOKIE, HeaderValue::from_static("cookie1=value1; ")) - .header(COOKIE, HeaderValue::from_static("cookie2=value2; ")) - .finish(); - let dbg = format!("{:?}", resp); - assert!(dbg.contains("HttpResponse")); - } - - #[test] - fn test_response_cookies() { - let req = TestRequest::default() - .header(COOKIE, "cookie1=value1") - .header(COOKIE, "cookie2=value2") - .finish(); - let cookies = req.cookies().unwrap(); - - let resp = HttpResponse::Ok() - .cookie( - http::Cookie::build("name", "value") - .domain("www.rust-lang.org") - .path("/test") - .http_only(true) - .max_age(Duration::days(1)) - .finish(), - ).del_cookie(&cookies[0]) - .finish(); - - let mut val: Vec<_> = resp - .headers() - .get_all("Set-Cookie") - .iter() - .map(|v| v.to_str().unwrap().to_owned()) - .collect(); - val.sort(); - assert!(val[0].starts_with("cookie1=; Max-Age=0;")); - assert_eq!( - val[1], - "name=value; HttpOnly; Path=/test; Domain=www.rust-lang.org; Max-Age=86400" - ); - } - - #[test] - fn test_update_response_cookies() { - let mut r = HttpResponse::Ok() - .cookie(http::Cookie::new("original", "val100")) - .finish(); - - r.add_cookie(&http::Cookie::new("cookie2", "val200")) - .unwrap(); - r.add_cookie(&http::Cookie::new("cookie2", "val250")) - .unwrap(); - r.add_cookie(&http::Cookie::new("cookie3", "val300")) - .unwrap(); - - assert_eq!(r.cookies().count(), 4); - r.del_cookie("cookie2"); - - 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")); - } - - #[test] - fn test_basic_builder() { - let resp = HttpResponse::Ok() - .header("X-TEST", "value") - .version(Version::HTTP_10) - .finish(); - assert_eq!(resp.version(), Some(Version::HTTP_10)); - assert_eq!(resp.status(), StatusCode::OK); - } - - #[test] - fn test_insert() { - let resp = HttpResponse::Ok() - .insert("deleteme", "old value") - .insert("deleteme", "new value") - .finish(); - assert_eq!("new value", resp.headers().get("deleteme").expect("new value")); - } - - #[test] - fn test_remove() { - let resp = HttpResponse::Ok() - .header("deleteme", "value") - .remove("deleteme") - .finish(); - assert!(resp.headers().get("deleteme").is_none()) - } - - #[test] - fn test_remove_replace() { - let resp = HttpResponse::Ok() - .header("some-header", "old_value1") - .header("some-header", "old_value2") - .remove("some-header") - .header("some-header", "new_value1") - .header("some-header", "new_value2") - .remove("unrelated-header") - .finish(); - let mut v = resp.headers().get_all("some-header").into_iter(); - assert_eq!("new_value1", v.next().unwrap()); - assert_eq!("new_value2", v.next().unwrap()); - assert_eq!(None, v.next()); - } - - #[test] - fn test_upgrade() { - let resp = HttpResponse::build(StatusCode::OK).upgrade().finish(); - assert!(resp.upgrade()) - } - - #[test] - fn test_force_close() { - let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); - assert!(!resp.keep_alive().unwrap()) - } - - #[test] - fn test_content_type() { - let resp = HttpResponse::build(StatusCode::OK) - .content_type("text/plain") - .body(Body::Empty); - assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") - } - - #[test] - fn test_content_encoding() { - let resp = HttpResponse::build(StatusCode::OK).finish(); - assert_eq!(resp.content_encoding(), None); - - #[cfg(feature = "brotli")] - { - let resp = HttpResponse::build(StatusCode::OK) - .content_encoding(ContentEncoding::Br) - .finish(); - assert_eq!(resp.content_encoding(), Some(ContentEncoding::Br)); - } - - let resp = HttpResponse::build(StatusCode::OK) - .content_encoding(ContentEncoding::Gzip) - .finish(); - assert_eq!(resp.content_encoding(), Some(ContentEncoding::Gzip)); - } - - #[test] - fn test_json() { - let resp = HttpResponse::build(StatusCode::OK).json(vec!["v1", "v2", "v3"]); - let ct = resp.headers().get(CONTENT_TYPE).unwrap(); - assert_eq!(ct, HeaderValue::from_static("application/json")); - assert_eq!( - *resp.body(), - Body::from(Bytes::from_static(b"[\"v1\",\"v2\",\"v3\"]")) - ); - } - - #[test] - fn test_json_ct() { - let resp = HttpResponse::build(StatusCode::OK) - .header(CONTENT_TYPE, "text/json") - .json(vec!["v1", "v2", "v3"]); - let ct = resp.headers().get(CONTENT_TYPE).unwrap(); - assert_eq!(ct, HeaderValue::from_static("text/json")); - assert_eq!( - *resp.body(), - Body::from(Bytes::from_static(b"[\"v1\",\"v2\",\"v3\"]")) - ); - } - - #[test] - fn test_json2() { - let resp = HttpResponse::build(StatusCode::OK).json2(&vec!["v1", "v2", "v3"]); - let ct = resp.headers().get(CONTENT_TYPE).unwrap(); - assert_eq!(ct, HeaderValue::from_static("application/json")); - assert_eq!( - *resp.body(), - Body::from(Bytes::from_static(b"[\"v1\",\"v2\",\"v3\"]")) - ); - } - - #[test] - fn test_json2_ct() { - let resp = HttpResponse::build(StatusCode::OK) - .header(CONTENT_TYPE, "text/json") - .json2(&vec!["v1", "v2", "v3"]); - let ct = resp.headers().get(CONTENT_TYPE).unwrap(); - assert_eq!(ct, HeaderValue::from_static("text/json")); - assert_eq!( - *resp.body(), - Body::from(Bytes::from_static(b"[\"v1\",\"v2\",\"v3\"]")) - ); - } - - impl Body { - pub(crate) fn bin_ref(&self) -> &Binary { - match *self { - Body::Binary(ref bin) => bin, - _ => panic!(), - } - } - } - - #[test] - fn test_into_response() { - let req = TestRequest::default().finish(); - - let resp: HttpResponse = "test".into(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("text/plain; charset=utf-8") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from("test")); - - let resp: HttpResponse = "test".respond_to(&req).ok().unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("text/plain; charset=utf-8") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from("test")); - - let resp: HttpResponse = b"test".as_ref().into(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("application/octet-stream") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from(b"test".as_ref())); - - let resp: HttpResponse = b"test".as_ref().respond_to(&req).ok().unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("application/octet-stream") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from(b"test".as_ref())); - - let resp: HttpResponse = "test".to_owned().into(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("text/plain; charset=utf-8") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from("test".to_owned())); - - let resp: HttpResponse = "test".to_owned().respond_to(&req).ok().unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("text/plain; charset=utf-8") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from("test".to_owned())); - - let resp: HttpResponse = (&"test".to_owned()).into(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("text/plain; charset=utf-8") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from(&"test".to_owned())); - - let resp: HttpResponse = (&"test".to_owned()).respond_to(&req).ok().unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("text/plain; charset=utf-8") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from(&"test".to_owned())); - - let b = Bytes::from_static(b"test"); - let resp: HttpResponse = b.into(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("application/octet-stream") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.body().bin_ref(), - &Binary::from(Bytes::from_static(b"test")) - ); - - let b = Bytes::from_static(b"test"); - let resp: HttpResponse = b.respond_to(&req).ok().unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("application/octet-stream") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.body().bin_ref(), - &Binary::from(Bytes::from_static(b"test")) - ); - - let b = BytesMut::from("test"); - let resp: HttpResponse = b.into(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("application/octet-stream") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from(BytesMut::from("test"))); - - let b = BytesMut::from("test"); - let resp: HttpResponse = b.respond_to(&req).ok().unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("application/octet-stream") - ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), &Binary::from(BytesMut::from("test"))); - } - - #[test] - fn test_into_builder() { - let mut resp: HttpResponse = "test".into(); - assert_eq!(resp.status(), StatusCode::OK); - - resp.add_cookie(&http::Cookie::new("cookie1", "val100")) - .unwrap(); - - let mut builder = resp.into_builder(); - let resp = builder.status(StatusCode::BAD_REQUEST).finish(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - - let cookie = resp.cookies().next().unwrap(); - assert_eq!((cookie.name(), cookie.value()), ("cookie1", "val100")); - } -} diff --git a/src/info.rs b/src/info.rs index 43c22123..3b51215f 100644 --- a/src/info.rs +++ b/src/info.rs @@ -1,10 +1,17 @@ -use http::header::{self, HeaderName}; -use server::Request; +use std::cell::Ref; + +use actix_http::http::header::{self, HeaderName}; +use actix_http::RequestHead; const X_FORWARDED_FOR: &[u8] = b"x-forwarded-for"; const X_FORWARDED_HOST: &[u8] = b"x-forwarded-host"; const X_FORWARDED_PROTO: &[u8] = b"x-forwarded-proto"; +pub enum ConnectionInfoError { + UnknownHost, + UnknownScheme, +} + /// `HttpRequest` connection information #[derive(Clone, Default)] pub struct ConnectionInfo { @@ -16,18 +23,22 @@ pub struct ConnectionInfo { impl ConnectionInfo { /// Create *ConnectionInfo* instance for a request. - #[cfg_attr( - feature = "cargo-clippy", - allow(cyclomatic_complexity) - )] - pub fn update(&mut self, req: &Request) { + pub fn get(req: &RequestHead) -> Ref { + if !req.extensions().contains::() { + req.extensions_mut().insert(ConnectionInfo::new(req)); + } + Ref::map(req.extensions(), |e| e.get().unwrap()) + } + + #[cfg_attr(feature = "cargo-clippy", allow(cyclomatic_complexity))] + fn new(req: &RequestHead) -> ConnectionInfo { let mut host = None; let mut scheme = None; let mut remote = None; let mut peer = None; // load forwarded header - for hdr in req.headers().get_all(header::FORWARDED) { + 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(',') { @@ -35,15 +46,21 @@ impl ConnectionInfo { if let Some(name) = items.next() { if let Some(val) = items.next() { match &name.to_lowercase() as &str { - "for" => if remote.is_none() { - remote = Some(val.trim()); - }, - "proto" => if scheme.is_none() { - scheme = Some(val.trim()); - }, - "host" => if host.is_none() { - host = Some(val.trim()); - }, + "for" => { + if remote.is_none() { + remote = Some(val.trim()); + } + } + "proto" => { + if scheme.is_none() { + scheme = Some(val.trim()); + } + } + "host" => { + if host.is_none() { + host = Some(val.trim()); + } + } _ => (), } } @@ -56,7 +73,7 @@ impl ConnectionInfo { // scheme if scheme.is_none() { if let Some(h) = req - .headers() + .headers .get(HeaderName::from_lowercase(X_FORWARDED_PROTO).unwrap()) { if let Ok(h) = h.to_str() { @@ -64,7 +81,7 @@ impl ConnectionInfo { } } if scheme.is_none() { - scheme = req.uri().scheme_part().map(|a| a.as_str()); + scheme = req.uri.scheme_part().map(|a| a.as_str()); if scheme.is_none() && req.server_settings().secure() { scheme = Some("https") } @@ -74,7 +91,7 @@ impl ConnectionInfo { // host if host.is_none() { if let Some(h) = req - .headers() + .headers .get(HeaderName::from_lowercase(X_FORWARDED_HOST).unwrap()) { if let Ok(h) = h.to_str() { @@ -82,11 +99,11 @@ impl ConnectionInfo { } } 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() { - host = req.uri().authority_part().map(|a| a.as_str()); + host = req.uri.authority_part().map(|a| a.as_str()); if host.is_none() { host = Some(req.server_settings().host()); } @@ -97,7 +114,7 @@ impl ConnectionInfo { // remote addr if remote.is_none() { if let Some(h) = req - .headers() + .headers .get(HeaderName::from_lowercase(X_FORWARDED_FOR).unwrap()) { if let Ok(h) = h.to_str() { @@ -110,10 +127,12 @@ impl ConnectionInfo { } } - self.scheme = scheme.unwrap_or("http").to_owned(); - self.host = host.unwrap_or("localhost").to_owned(); - self.remote = remote.map(|s| s.to_owned()); - self.peer = peer; + ConnectionInfo { + scheme: scheme.unwrap_or("http").to_owned(), + host: host.unwrap_or("localhost").to_owned(), + remote: remote.map(|s| s.to_owned()), + peer: peer, + } } /// Scheme of the request. @@ -163,7 +182,7 @@ impl ConnectionInfo { #[cfg(test)] mod tests { use super::*; - use test::TestRequest; + use crate::test::TestRequest; #[test] fn test_forwarded() { @@ -177,7 +196,8 @@ mod tests { .header( header::FORWARDED, "for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org", - ).request(); + ) + .request(); let mut info = ConnectionInfo::default(); info.update(&req); diff --git a/src/json.rs b/src/json.rs deleted file mode 100644 index b04cad2f..00000000 --- a/src/json.rs +++ /dev/null @@ -1,519 +0,0 @@ -use bytes::BytesMut; -use futures::{Future, Poll, Stream}; -use http::header::CONTENT_LENGTH; -use std::fmt; -use std::ops::{Deref, DerefMut}; -use std::rc::Rc; - -use mime; -use serde::de::DeserializeOwned; -use serde::Serialize; -use serde_json; - -use error::{Error, JsonPayloadError}; -use handler::{FromRequest, Responder}; -use http::StatusCode; -use httpmessage::HttpMessage; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; - -/// Json helper -/// -/// Json can be used for two different purpose. First is for json response -/// generation and second is for extracting typed information from request's -/// payload. -/// -/// To extract typed information from request's body, the type `T` must -/// implement the `Deserialize` trait from *serde*. -/// -/// [**JsonConfig**](dev/struct.JsonConfig.html) allows to configure extraction -/// process. -/// -/// ## Example -/// -/// ```rust -/// # extern crate actix_web; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{App, Json, Result, http}; -/// -/// #[derive(Deserialize)] -/// struct Info { -/// username: String, -/// } -/// -/// /// deserialize `Info` from request's body -/// fn index(info: Json) -> Result { -/// Ok(format!("Welcome {}!", info.username)) -/// } -/// -/// fn main() { -/// let app = App::new().resource( -/// "/index.html", -/// |r| r.method(http::Method::POST).with(index)); // <- use `with` extractor -/// } -/// ``` -/// -/// The `Json` type allows you to respond with well-formed JSON data: simply -/// return a value of type Json where T is the type of a structure -/// to serialize into *JSON*. The type `T` must implement the `Serialize` -/// trait from *serde*. -/// -/// ```rust -/// # extern crate actix_web; -/// # #[macro_use] extern crate serde_derive; -/// # use actix_web::*; -/// # -/// #[derive(Serialize)] -/// struct MyObj { -/// name: String, -/// } -/// -/// fn index(req: HttpRequest) -> Result> { -/// Ok(Json(MyObj { -/// name: req.match_info().query("name")?, -/// })) -/// } -/// # fn main() {} -/// ``` -pub struct Json(pub T); - -impl Json { - /// Deconstruct to an inner value - pub fn into_inner(self) -> T { - self.0 - } -} - -impl Deref for Json { - type Target = T; - - fn deref(&self) -> &T { - &self.0 - } -} - -impl DerefMut for Json { - fn deref_mut(&mut self) -> &mut T { - &mut self.0 - } -} - -impl fmt::Debug for Json -where - T: fmt::Debug, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Json: {:?}", self.0) - } -} - -impl fmt::Display for Json -where - T: fmt::Display, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.0, f) - } -} - -impl Responder for Json { - type Item = HttpResponse; - type Error = Error; - - fn respond_to(self, req: &HttpRequest) -> Result { - let body = serde_json::to_string(&self.0)?; - - Ok(req - .build_response(StatusCode::OK) - .content_type("application/json") - .body(body)) - } -} - -impl FromRequest for Json -where - T: DeserializeOwned + 'static, - S: 'static, -{ - type Config = JsonConfig; - type Result = Box>; - - #[inline] - fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { - let req2 = req.clone(); - let err = Rc::clone(&cfg.ehandler); - Box::new( - JsonBody::new(req, Some(cfg)) - .limit(cfg.limit) - .map_err(move |e| (*err)(e, &req2)) - .map(Json), - ) - } -} - -/// Json extractor configuration -/// -/// ```rust -/// # extern crate actix_web; -/// extern crate mime; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{error, http, App, HttpResponse, Json, Result}; -/// -/// #[derive(Deserialize)] -/// struct Info { -/// username: String, -/// } -/// -/// /// deserialize `Info` from request's body, max payload size is 4kb -/// fn index(info: Json) -> Result { -/// Ok(format!("Welcome {}!", info.username)) -/// } -/// -/// fn main() { -/// let app = App::new().resource("/index.html", |r| { -/// r.method(http::Method::POST) -/// .with_config(index, |cfg| { -/// cfg.0.limit(4096) // <- change json extractor configuration -/// .content_type(|mime| { // <- accept text/plain content type -/// mime.type_() == mime::TEXT && mime.subtype() == mime::PLAIN -/// }) -/// .error_handler(|err, req| { // <- create custom error response -/// error::InternalError::from_response( -/// err, HttpResponse::Conflict().finish()).into() -/// }); -/// }) -/// }); -/// } -/// ``` -pub struct JsonConfig { - limit: usize, - ehandler: Rc) -> Error>, - content_type: Option bool>>, -} - -impl JsonConfig { - /// Change max size of payload. By default max size is 256Kb - pub fn limit(&mut self, limit: usize) -> &mut Self { - self.limit = limit; - self - } - - /// Set custom error handler - pub fn error_handler(&mut self, f: F) -> &mut Self - where - F: Fn(JsonPayloadError, &HttpRequest) -> Error + 'static, - { - self.ehandler = Rc::new(f); - self - } - - /// Set predicate for allowed content types - pub fn content_type(&mut self, predicate: F) -> &mut Self - where - F: Fn(mime::Mime) -> bool + 'static, - { - self.content_type = Some(Box::new(predicate)); - self - } -} - -impl Default for JsonConfig { - fn default() -> Self { - JsonConfig { - limit: 262_144, - ehandler: Rc::new(|e, _| e.into()), - content_type: None, - } - } -} - -/// Request payload json parser that resolves to a deserialized `T` value. -/// -/// Returns error: -/// -/// * content type is not `application/json` -/// (unless specified in [`JsonConfig`](struct.JsonConfig.html)) -/// * content length is greater than 256k -/// -/// # Server example -/// -/// ```rust -/// # extern crate actix_web; -/// # extern crate futures; -/// # #[macro_use] extern crate serde_derive; -/// use actix_web::{AsyncResponder, Error, HttpMessage, HttpRequest, HttpResponse}; -/// use futures::future::Future; -/// -/// #[derive(Deserialize, Debug)] -/// struct MyObj { -/// name: String, -/// } -/// -/// fn index(mut req: HttpRequest) -> Box> { -/// req.json() // <- get JsonBody future -/// .from_err() -/// .and_then(|val: MyObj| { // <- deserialized value -/// println!("==== BODY ==== {:?}", val); -/// Ok(HttpResponse::Ok().into()) -/// }).responder() -/// } -/// # fn main() {} -/// ``` -pub struct JsonBody { - limit: usize, - length: Option, - stream: Option, - err: Option, - fut: Option>>, -} - -impl JsonBody { - /// Create `JsonBody` for request. - pub fn new(req: &T, cfg: Option<&JsonConfig>) -> Self { - // check content-type - let json = if let Ok(Some(mime)) = req.mime_type() { - mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) || - cfg.map_or(false, |cfg| { - cfg.content_type.as_ref().map_or(false, |predicate| predicate(mime)) - }) - } else { - false - }; - if !json { - return JsonBody { - limit: 262_144, - length: None, - stream: None, - fut: None, - err: Some(JsonPayloadError::ContentType), - }; - } - - let mut len = None; - 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) - } - } - } - - JsonBody { - limit: 262_144, - length: len, - stream: Some(req.payload()), - fut: None, - err: None, - } - } - - /// Change max size of payload. By default max size is 256Kb - pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; - self - } -} - -impl Future for JsonBody { - type Item = U; - type Error = JsonPayloadError; - - fn poll(&mut self) -> Poll { - if let Some(ref mut fut) = self.fut { - return fut.poll(); - } - - if let Some(err) = self.err.take() { - return Err(err); - } - - let limit = self.limit; - if let Some(len) = self.length.take() { - if len > limit { - return Err(JsonPayloadError::Overflow); - } - } - - let fut = self - .stream - .take() - .expect("JsonBody could not be used second time") - .from_err() - .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { - if (body.len() + chunk.len()) > limit { - Err(JsonPayloadError::Overflow) - } else { - body.extend_from_slice(&chunk); - Ok(body) - } - }).and_then(|body| Ok(serde_json::from_slice::(&body)?)); - self.fut = Some(Box::new(fut)); - self.poll() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use bytes::Bytes; - use futures::Async; - use http::header; - - use handler::Handler; - use test::TestRequest; - use with::With; - - impl PartialEq for JsonPayloadError { - fn eq(&self, other: &JsonPayloadError) -> bool { - match *self { - JsonPayloadError::Overflow => match *other { - JsonPayloadError::Overflow => true, - _ => false, - }, - JsonPayloadError::ContentType => match *other { - JsonPayloadError::ContentType => true, - _ => false, - }, - _ => false, - } - } - } - - #[derive(Serialize, Deserialize, PartialEq, Debug)] - struct MyObject { - name: String, - } - - #[test] - fn test_json() { - let json = Json(MyObject { - name: "test".to_owned(), - }); - let resp = json.respond_to(&TestRequest::default().finish()).unwrap(); - assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), - "application/json" - ); - } - - #[test] - fn test_json_body() { - let req = TestRequest::default().finish(); - let mut json = req.json::(); - assert_eq!(json.poll().err().unwrap(), JsonPayloadError::ContentType); - - let req = TestRequest::default() - .header( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/text"), - ).finish(); - let mut json = req.json::(); - assert_eq!(json.poll().err().unwrap(), JsonPayloadError::ContentType); - - let req = TestRequest::default() - .header( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/json"), - ).header( - header::CONTENT_LENGTH, - header::HeaderValue::from_static("10000"), - ).finish(); - let mut json = req.json::().limit(100); - assert_eq!(json.poll().err().unwrap(), JsonPayloadError::Overflow); - - let req = 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\"}")) - .finish(); - - let mut json = req.json::(); - assert_eq!( - json.poll().ok().unwrap(), - Async::Ready(MyObject { - name: "test".to_owned() - }) - ); - } - - #[test] - fn test_with_json() { - let mut cfg = JsonConfig::default(); - cfg.limit(4096); - let handler = With::new(|data: Json| data, cfg); - - let req = TestRequest::default().finish(); - assert!(handler.handle(&req).as_err().is_some()); - - let req = TestRequest::with_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\"}")) - .finish(); - assert!(handler.handle(&req).as_err().is_none()) - } - - #[test] - fn test_with_json_and_bad_content_type() { - let mut cfg = JsonConfig::default(); - cfg.limit(4096); - let handler = With::new(|data: Json| data, cfg); - - let req = TestRequest::with_header( - header::CONTENT_TYPE, - header::HeaderValue::from_static("text/plain"), - ).header( - header::CONTENT_LENGTH, - header::HeaderValue::from_static("16"), - ).set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) - .finish(); - assert!(handler.handle(&req).as_err().is_some()) - } - - #[test] - fn test_with_json_and_good_custom_content_type() { - let mut cfg = JsonConfig::default(); - cfg.limit(4096); - cfg.content_type(|mime: mime::Mime| { - mime.type_() == mime::TEXT && mime.subtype() == mime::PLAIN - }); - let handler = With::new(|data: Json| data, cfg); - - let req = TestRequest::with_header( - header::CONTENT_TYPE, - header::HeaderValue::from_static("text/plain"), - ).header( - header::CONTENT_LENGTH, - header::HeaderValue::from_static("16"), - ).set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) - .finish(); - assert!(handler.handle(&req).as_err().is_none()) - } - - #[test] - fn test_with_json_and_bad_custom_content_type() { - let mut cfg = JsonConfig::default(); - cfg.limit(4096); - cfg.content_type(|mime: mime::Mime| { - mime.type_() == mime::TEXT && mime.subtype() == mime::PLAIN - }); - let handler = With::new(|data: Json| data, cfg); - - let req = TestRequest::with_header( - header::CONTENT_TYPE, - header::HeaderValue::from_static("text/html"), - ).header( - header::CONTENT_LENGTH, - header::HeaderValue::from_static("16"), - ).set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) - .finish(); - assert!(handler.handle(&req).as_err().is_some()) - } -} diff --git a/src/lib.rs b/src/lib.rs index 3b00cda1..f09c11ce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,292 +1,37 @@ -//! Actix web is a small, pragmatic, and extremely fast web framework -//! for Rust. -//! -//! ```rust -//! use actix_web::{server, App, Path, Responder}; -//! # use std::thread; -//! -//! fn index(info: Path<(String, u32)>) -> impl Responder { -//! format!("Hello {}! id:{}", info.0, info.1) -//! } -//! -//! fn main() { -//! # thread::spawn(|| { -//! server::new(|| { -//! App::new().resource("/{name}/{id}/index.html", |r| r.with(index)) -//! }).bind("127.0.0.1:8080") -//! .unwrap() -//! .run(); -//! # }); -//! } -//! ``` -//! -//! ## Documentation & community resources -//! -//! Besides the API documentation (which you are currently looking -//! at!), several other resources are available: -//! -//! * [User Guide](https://actix.rs/docs/) -//! * [Chat on gitter](https://gitter.im/actix/actix) -//! * [GitHub repository](https://github.com/actix/actix-web) -//! * [Cargo package](https://crates.io/crates/actix-web) -//! -//! To get started navigating the API documentation you may want to -//! consider looking at the following pages: -//! -//! * [App](struct.App.html): This struct represents an actix-web -//! application and is used to configure routes and other common -//! settings. -//! -//! * [HttpServer](server/struct.HttpServer.html): This struct -//! represents an HTTP server instance and is used to instantiate and -//! configure servers. -//! -//! * [HttpRequest](struct.HttpRequest.html) and -//! [HttpResponse](struct.HttpResponse.html): These structs -//! represent HTTP requests and responses and expose various methods -//! for inspecting, creating and otherwise utilizing them. -//! -//! ## Features -//! -//! * Supported *HTTP/1.x* and *HTTP/2.0* protocols -//! * Streaming and pipelining -//! * Keep-alive and slow requests handling -//! * `WebSockets` server/client -//! * Transparent content compression/decompression (br, gzip, deflate) -//! * Configurable request routing -//! * Graceful server shutdown -//! * Multipart streams -//! * SSL support with OpenSSL or `native-tls` -//! * Middlewares (`Logger`, `Session`, `CORS`, `CSRF`, `DefaultHeaders`) -//! * Built on top of [Actix actor framework](https://github.com/actix/actix) -//! * Supported Rust version: 1.26 or later -//! -//! ## Package feature -//! -//! * `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` -//! * `uds` - enables support for making client requests via Unix Domain Sockets. -//! Unix only. Not necessary for *serving* requests. -//! * `session` - enables session support, includes `ring` crate as -//! dependency -//! * `brotli` - enables `brotli` compression support, requires `c` -//! compiler -//! * `flate2-c` - enables `gzip`, `deflate` compression support, requires -//! `c` compiler -//! * `flate2-rust` - experimental rust based implementation for -//! `gzip`, `deflate` compression. -//! -#![cfg_attr(actix_nightly, feature( - specialization, // for impl ErrorResponse for std::error::Error - extern_prelude, - tool_lints, -))] -#![warn(missing_docs)] +#![allow(clippy::type_complexity, dead_code, unused_variables)] -#[macro_use] -extern crate log; -extern crate base64; -extern crate byteorder; -extern crate bytes; -extern crate regex; -extern crate sha1; -extern crate time; -#[macro_use] -extern crate bitflags; -#[macro_use] -extern crate failure; -#[macro_use] -extern crate lazy_static; -#[macro_use] -extern crate futures; -extern crate cookie; -extern crate futures_cpupool; -extern crate http as modhttp; -extern crate httparse; -extern crate language_tags; -extern crate lazycell; -extern crate mime; -extern crate mime_guess; -extern crate mio; -extern crate net2; -extern crate parking_lot; -extern crate rand; -extern crate slab; -extern crate tokio; -extern crate tokio_current_thread; -extern crate tokio_io; -extern crate tokio_reactor; -extern crate tokio_tcp; -extern crate tokio_timer; -#[cfg(all(unix, feature = "uds"))] -extern crate tokio_uds; -extern crate url; -#[macro_use] -extern crate serde; -#[cfg(feature = "brotli")] -extern crate brotli2; -extern crate encoding; -#[cfg(feature = "flate2")] -extern crate flate2; -extern crate h2 as http2; -extern crate num_cpus; -extern crate serde_urlencoded; -#[macro_use] -extern crate percent_encoding; -extern crate serde_json; -extern crate smallvec; -extern crate v_htmlescape; - -extern crate actix_net; -#[macro_use] -extern crate actix as actix_inner; - -#[cfg(test)] -#[macro_use] -extern crate serde_derive; - -#[cfg(feature = "tls")] -extern crate native_tls; -#[cfg(feature = "tls")] -extern crate tokio_tls; - -#[cfg(feature = "openssl")] -extern crate openssl; -#[cfg(feature = "openssl")] -extern crate tokio_openssl; - -#[cfg(feature = "rust-tls")] -extern crate rustls; -#[cfg(feature = "rust-tls")] -extern crate tokio_rustls; -#[cfg(feature = "rust-tls")] -extern crate webpki; -#[cfg(feature = "rust-tls")] -extern crate webpki_roots; - -mod application; -mod body; -mod context; -mod de; -mod extensions; +mod app; mod extractor; -mod handler; -mod header; +pub mod handler; mod helpers; -mod httpcodes; -mod httpmessage; -mod httprequest; -mod httpresponse; -mod info; -mod json; -mod param; -mod payload; -mod pipeline; -mod resource; -mod route; -mod router; -mod scope; -mod uri; -mod with; - -pub mod client; -pub mod error; +// mod info; +pub mod blocking; +pub mod filter; pub mod fs; pub mod middleware; -pub mod multipart; -pub mod pred; -pub mod server; -pub mod test; -pub mod ws; -pub use application::App; -pub use body::{Binary, Body}; -pub use context::HttpContext; -pub use error::{Error, ResponseError, Result}; -pub use extensions::Extensions; -pub use extractor::{Form, Path, Query}; -pub use handler::{ - AsyncResponder, Either, FromRequest, FutureResponse, Responder, State, -}; -pub use httpmessage::HttpMessage; -pub use httprequest::HttpRequest; -pub use httpresponse::HttpResponse; -pub use json::Json; -pub use scope::Scope; -pub use server::Request; +mod request; +mod resource; +mod responder; +mod route; +mod service; +mod state; -pub mod actix { - //! Re-exports [actix's](https://docs.rs/actix/) prelude - pub use super::actix_inner::actors::resolver; - pub use super::actix_inner::actors::signal; - pub use super::actix_inner::fut; - pub use super::actix_inner::msgs; - pub use super::actix_inner::prelude::*; - pub use super::actix_inner::{run, spawn}; -} +// re-export for convenience +pub use actix_http::Response as HttpResponse; +pub use actix_http::{http, Error, HttpMessage, ResponseError}; -#[cfg(feature = "openssl")] -pub(crate) const HAS_OPENSSL: bool = true; -#[cfg(not(feature = "openssl"))] -pub(crate) const HAS_OPENSSL: bool = false; - -#[cfg(feature = "tls")] -pub(crate) const HAS_TLS: bool = true; -#[cfg(not(feature = "tls"))] -pub(crate) const HAS_TLS: bool = false; - -#[cfg(feature = "rust-tls")] -pub(crate) const HAS_RUSTLS: bool = true; -#[cfg(not(feature = "rust-tls"))] -pub(crate) const HAS_RUSTLS: bool = false; +pub use crate::app::App; +pub use crate::extractor::{Form, Json, Path, Query}; +pub use crate::handler::FromRequest; +pub use crate::request::HttpRequest; +pub use crate::resource::Resource; +pub use crate::responder::{Either, Responder}; +pub use crate::service::{ServiceRequest, ServiceResponse}; +pub use crate::state::State; pub mod dev { - //! The `actix-web` prelude for library developers - //! - //! The purpose of this module is to alleviate imports of many common actix - //! traits by adding a glob import to the top of actix heavy modules: - //! - //! ``` - //! # #![allow(unused_imports)] - //! use actix_web::dev::*; - //! ``` - - pub use body::BodyStream; - pub use context::Drain; - pub use extractor::{FormConfig, PayloadConfig, QueryConfig, PathConfig, EitherConfig, EitherCollisionStrategy}; - pub use handler::{AsyncResult, Handler}; - pub use httpmessage::{MessageBody, Readlines, UrlEncoded}; - pub use httpresponse::HttpResponseBuilder; - pub use info::ConnectionInfo; - pub use json::{JsonBody, JsonConfig}; - pub use param::{FromParam, Params}; - pub use payload::{Payload, PayloadBuffer}; - pub use pipeline::Pipeline; - pub use resource::Resource; - pub use route::Route; - pub use router::{ResourceDef, ResourceInfo, ResourceType, Router}; -} - -pub mod http { - //! Various HTTP related types - - // re-exports - pub use modhttp::{Method, StatusCode, Version}; - - #[doc(hidden)] - pub use modhttp::{uri, Error, Extensions, HeaderMap, HttpTryFrom, Uri}; - - pub use cookie::{Cookie, CookieBuilder}; - - pub use helpers::NormalizePath; - - /// Various http headers - pub mod header { - pub use header::*; - pub use header::{ - Charset, ContentDisposition, DispositionParam, DispositionType, LanguageTag, - }; - } - pub use header::ContentEncoding; - pub use httpresponse::ConnectionType; + pub use crate::app::AppService; + pub use crate::handler::{AsyncFactory, Extract, Factory, Handle}; + pub use crate::route::{Route, RouteBuilder}; + // pub use crate::info::ConnectionInfo; } diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs new file mode 100644 index 00000000..fee17ce1 --- /dev/null +++ b/src/middleware/compress.rs @@ -0,0 +1,443 @@ +use std::io::Write; +use std::str::FromStr; +use std::{cmp, fmt, io}; + +use actix_http::body::{Body, BodyLength, MessageBody, ResponseBody}; +use actix_http::http::header::{ + ContentEncoding, HeaderValue, ACCEPT_ENCODING, CONTENT_ENCODING, +}; +use actix_http::http::{HttpTryFrom, StatusCode}; +use actix_http::{Error, Head, ResponseHead}; +use actix_service::{IntoNewTransform, Service, Transform}; +use bytes::{Bytes, BytesMut}; +use futures::{Async, Future, Poll}; +use log::trace; + +#[cfg(feature = "brotli")] +use brotli2::write::BrotliEncoder; +#[cfg(feature = "flate2")] +use flate2::write::{GzEncoder, ZlibEncoder}; + +use crate::middleware::MiddlewareFactory; +use crate::service::{ServiceRequest, ServiceResponse}; + +#[derive(Debug, Clone)] +pub struct Compress(ContentEncoding); + +impl Compress { + pub fn new(encoding: ContentEncoding) -> Self { + Compress(encoding) + } +} + +impl Default for Compress { + fn default() -> Self { + Compress::new(ContentEncoding::Auto) + } +} + +impl Transform for Compress +where + P: 'static, + B: MessageBody, + S: Service, Response = ServiceResponse>, + S::Future: 'static, +{ + type Request = ServiceRequest

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

, srv: &mut S) -> Self::Future { + // negotiate content-encoding + let encoding = if let Some(val) = req.headers.get(ACCEPT_ENCODING) { + if let Ok(enc) = val.to_str() { + AcceptEncoding::parse(enc, self.0) + } else { + ContentEncoding::Identity + } + } else { + ContentEncoding::Identity + }; + + CompressResponse { + encoding, + fut: srv.call(req), + } + } +} + +#[doc(hidden)] +pub struct CompressResponse +where + P: 'static, + B: MessageBody, + S: Service, Response = ServiceResponse>, + S::Future: 'static, +{ + fut: S::Future, + encoding: ContentEncoding, +} + +impl Future for CompressResponse +where + P: 'static, + B: MessageBody, + S: Service, Response = ServiceResponse>, + S::Future: 'static, +{ + type Item = ServiceResponse>; + type Error = S::Error; + + fn poll(&mut self) -> Poll { + let resp = futures::try_ready!(self.fut.poll()); + + Ok(Async::Ready(resp.map_body(move |head, body| { + Encoder::body(self.encoding, head, body) + }))) + } +} + +impl IntoNewTransform, S> for Compress +where + P: 'static, + B: MessageBody, + S: Service, Response = ServiceResponse>, + S::Future: 'static, +{ + fn into_new_transform(self) -> MiddlewareFactory { + MiddlewareFactory::new(self) + } +} + +enum EncoderBody { + Body(B), + Other(Box), + None, +} + +pub struct Encoder { + body: EncoderBody, + encoder: Option, +} + +impl MessageBody for Encoder { + fn length(&self) -> BodyLength { + if self.encoder.is_none() { + match self.body { + EncoderBody::Body(ref b) => b.length(), + EncoderBody::Other(ref b) => b.length(), + EncoderBody::None => BodyLength::Empty, + } + } else { + BodyLength::Stream + } + } + + fn poll_next(&mut self) -> Poll, Error> { + loop { + let result = match self.body { + EncoderBody::Body(ref mut b) => b.poll_next()?, + EncoderBody::Other(ref mut b) => b.poll_next()?, + EncoderBody::None => return Ok(Async::Ready(None)), + }; + 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()))); + } + } else { + return Ok(Async::Ready(Some(chunk))); + } + } + Async::Ready(None) => { + if let Some(encoder) = self.encoder.take() { + let chunk = encoder.finish()?; + if chunk.is_empty() { + return Ok(Async::Ready(None)); + } else { + return Ok(Async::Ready(Some(chunk))); + } + } else { + return Ok(Async::Ready(None)); + } + } + } + } + } +} + +fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) { + head.headers_mut().insert( + CONTENT_ENCODING, + HeaderValue::try_from(Bytes::from_static(encoding.as_str().as_bytes())).unwrap(), + ); +} + +impl Encoder { + fn body( + encoding: ContentEncoding, + 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(); + + // TODO return error! + let _ = enc.write(buf.as_ref()); + let body = enc.finish().unwrap(); + update_head(encoding, head); + ResponseBody::Other(Body::Bytes(body)) + } 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), + }) + } + } + }, + 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), + }) + } + } + } + } +} + +pub(crate) struct Writer { + buf: BytesMut, +} + +impl Writer { + fn new() -> Writer { + Writer { + buf: BytesMut::with_capacity(8192), + } + } + fn take(&mut self) -> Bytes { + self.buf.take().freeze() + } +} + +impl io::Write for Writer { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.buf.extend_from_slice(buf); + Ok(buf.len()) + } + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +pub(crate) enum ContentEncoder { + #[cfg(feature = "flate2")] + Deflate(ZlibEncoder), + #[cfg(feature = "flate2")] + Gzip(GzEncoder), + #[cfg(feature = "brotli")] + Br(BrotliEncoder), +} + +impl fmt::Debug for ContentEncoder { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + #[cfg(feature = "brotli")] + ContentEncoder::Br(_) => writeln!(f, "ContentEncoder(Brotli)"), + #[cfg(feature = "flate2")] + ContentEncoder::Deflate(_) => writeln!(f, "ContentEncoder(Deflate)"), + #[cfg(feature = "flate2")] + ContentEncoder::Gzip(_) => writeln!(f, "ContentEncoder(Gzip)"), + } + } +} + +impl ContentEncoder { + fn encoder(encoding: ContentEncoding) -> Option { + match encoding { + #[cfg(feature = "flate2")] + ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new( + Writer::new(), + flate2::Compression::fast(), + ))), + #[cfg(feature = "flate2")] + ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new( + Writer::new(), + flate2::Compression::fast(), + ))), + #[cfg(feature = "brotli")] + ContentEncoding::Br => { + Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3))) + } + _ => None, + } + } + + #[inline] + pub(crate) fn take(&mut self) -> Bytes { + match *self { + #[cfg(feature = "brotli")] + ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(), + #[cfg(feature = "flate2")] + ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(), + #[cfg(feature = "flate2")] + ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(), + } + } + + fn finish(self) -> Result { + match self { + #[cfg(feature = "brotli")] + ContentEncoder::Br(encoder) => match encoder.finish() { + Ok(writer) => Ok(writer.buf.freeze()), + Err(err) => Err(err), + }, + #[cfg(feature = "flate2")] + ContentEncoder::Gzip(encoder) => match encoder.finish() { + Ok(writer) => Ok(writer.buf.freeze()), + Err(err) => Err(err), + }, + #[cfg(feature = "flate2")] + ContentEncoder::Deflate(encoder) => match encoder.finish() { + Ok(writer) => Ok(writer.buf.freeze()), + Err(err) => Err(err), + }, + } + } + + fn write(&mut self, data: &[u8]) -> Result { + match *self { + #[cfg(feature = "brotli")] + ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) { + Ok(_) => Ok(!encoder.get_ref().buf.is_empty()), + Err(err) => { + trace!("Error decoding br encoding: {}", err); + Err(err) + } + }, + #[cfg(feature = "flate2")] + ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) { + Ok(_) => Ok(!encoder.get_ref().buf.is_empty()), + Err(err) => { + trace!("Error decoding gzip encoding: {}", err); + Err(err) + } + }, + #[cfg(feature = "flate2")] + ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) { + Ok(_) => Ok(!encoder.get_ref().buf.is_empty()), + Err(err) => { + trace!("Error decoding deflate encoding: {}", err); + Err(err) + } + }, + } + } +} + +struct AcceptEncoding { + encoding: ContentEncoding, + quality: f64, +} + +impl Eq for AcceptEncoding {} + +impl Ord for AcceptEncoding { + fn cmp(&self, other: &AcceptEncoding) -> cmp::Ordering { + if self.quality > other.quality { + cmp::Ordering::Less + } else if self.quality < other.quality { + cmp::Ordering::Greater + } else { + cmp::Ordering::Equal + } + } +} + +impl PartialOrd for AcceptEncoding { + fn partial_cmp(&self, other: &AcceptEncoding) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for AcceptEncoding { + fn eq(&self, other: &AcceptEncoding) -> bool { + self.quality == other.quality + } +} + +impl AcceptEncoding { + fn new(tag: &str) -> Option { + let parts: Vec<&str> = tag.split(';').collect(); + let encoding = match parts.len() { + 0 => return None, + _ => ContentEncoding::from(parts[0]), + }; + let quality = match parts.len() { + 1 => encoding.quality(), + _ => match f64::from_str(parts[1]) { + Ok(q) => q, + Err(_) => 0.0, + }, + }; + Some(AcceptEncoding { encoding, quality }) + } + + /// Parse a raw Accept-Encoding header value into an ordered list. + pub fn parse(raw: &str, encoding: ContentEncoding) -> ContentEncoding { + let mut encodings: Vec<_> = raw + .replace(' ', "") + .split(',') + .map(|l| AcceptEncoding::new(l)) + .collect(); + encodings.sort(); + + for enc in encodings { + if let Some(enc) = enc { + if encoding == ContentEncoding::Auto { + return enc.encoding; + } else if encoding == enc.encoding { + return encoding; + } + } + } + ContentEncoding::Identity + } +} diff --git a/src/middleware/cors.rs b/src/middleware/cors.rs deleted file mode 100644 index 386d0007..00000000 --- a/src/middleware/cors.rs +++ /dev/null @@ -1,1227 +0,0 @@ -//! Cross-origin resource sharing (CORS) for Actix applications -//! -//! CORS middleware could be used with application and with resource. -//! First you need to construct CORS middleware instance. -//! -//! To construct a cors: -//! -//! 1. Call [`Cors::build`](struct.Cors.html#method.build) to start building. -//! 2. Use any of the builder methods to set fields in the backend. -//! 3. Call [finish](struct.Cors.html#method.finish) to retrieve the -//! constructed backend. -//! -//! Cors middleware could be used as parameter for `App::middleware()` or -//! `Resource::middleware()` methods. But you have to use -//! `Cors::for_app()` method to support *preflight* OPTIONS request. -//! -//! -//! # Example -//! -//! ```rust -//! # extern crate actix_web; -//! use actix_web::middleware::cors::Cors; -//! use actix_web::{http, App, HttpRequest, HttpResponse}; -//! -//! fn index(mut req: HttpRequest) -> &'static str { -//! "Hello world" -//! } -//! -//! fn main() { -//! let app = App::new().configure(|app| { -//! Cors::for_app(app) // <- Construct CORS middleware builder -//! .allowed_origin("https://www.rust-lang.org/") -//! .allowed_methods(vec!["GET", "POST"]) -//! .allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT]) -//! .allowed_header(http::header::CONTENT_TYPE) -//! .max_age(3600) -//! .resource("/index.html", |r| { -//! r.method(http::Method::GET).f(|_| HttpResponse::Ok()); -//! r.method(http::Method::HEAD).f(|_| HttpResponse::MethodNotAllowed()); -//! }) -//! .register() -//! }); -//! } -//! ``` -//! In this example custom *CORS* middleware get registered for "/index.html" -//! endpoint. -//! -//! Cors middleware automatically handle *OPTIONS* preflight request. -use std::collections::HashSet; -use std::iter::FromIterator; -use std::rc::Rc; - -use http::header::{self, HeaderName, HeaderValue}; -use http::{self, HttpTryFrom, Method, StatusCode, Uri}; - -use application::App; -use error::{ResponseError, Result}; -use httpmessage::HttpMessage; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response, Started}; -use resource::Resource; -use router::ResourceDef; -use server::Request; - -/// A set of errors that can occur during processing CORS -#[derive(Debug, Fail)] -pub enum CorsError { - /// The HTTP request header `Origin` is required but was not provided - #[fail( - display = "The HTTP request header `Origin` is required but was not provided" - )] - MissingOrigin, - /// The HTTP request header `Origin` could not be parsed correctly. - #[fail(display = "The HTTP request header `Origin` could not be parsed correctly.")] - BadOrigin, - /// The request header `Access-Control-Request-Method` is required but is - /// missing - #[fail( - display = "The request header `Access-Control-Request-Method` is required but is missing" - )] - MissingRequestMethod, - /// The request header `Access-Control-Request-Method` has an invalid value - #[fail( - display = "The request header `Access-Control-Request-Method` has an invalid value" - )] - BadRequestMethod, - /// The request header `Access-Control-Request-Headers` has an invalid - /// value - #[fail( - display = "The request header `Access-Control-Request-Headers` has an invalid value" - )] - BadRequestHeaders, - /// The request header `Access-Control-Request-Headers` is required but is - /// missing. - #[fail( - display = "The request header `Access-Control-Request-Headers` is required but is - missing" - )] - MissingRequestHeaders, - /// Origin is not allowed to make this request - #[fail(display = "Origin is not allowed to make this request")] - OriginNotAllowed, - /// Requested method is not allowed - #[fail(display = "Requested method is not allowed")] - MethodNotAllowed, - /// One or more headers requested are not allowed - #[fail(display = "One or more headers requested are not allowed")] - HeadersNotAllowed, -} - -impl ResponseError for CorsError { - fn error_response(&self) -> HttpResponse { - HttpResponse::with_body(StatusCode::BAD_REQUEST, format!("{}", self)) - } -} - -/// An enum signifying that some of type T is allowed, or `All` (everything is -/// allowed). -/// -/// `Default` is implemented for this enum and is `All`. -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum AllOrSome { - /// Everything is allowed. Usually equivalent to the "*" value. - All, - /// Only some of `T` is allowed - Some(T), -} - -impl Default for AllOrSome { - fn default() -> Self { - AllOrSome::All - } -} - -impl AllOrSome { - /// Returns whether this is an `All` variant - pub fn is_all(&self) -> bool { - match *self { - AllOrSome::All => true, - AllOrSome::Some(_) => false, - } - } - - /// Returns whether this is a `Some` variant - pub fn is_some(&self) -> bool { - !self.is_all() - } - - /// Returns &T - pub fn as_ref(&self) -> Option<&T> { - match *self { - AllOrSome::All => None, - AllOrSome::Some(ref t) => Some(t), - } - } -} - -/// `Middleware` for Cross-origin resource sharing support -/// -/// The Cors struct contains the settings for CORS requests to be validated and -/// for responses to be generated. -#[derive(Clone)] -pub struct Cors { - inner: Rc, -} - -struct Inner { - methods: HashSet, - origins: AllOrSome>, - origins_str: Option, - headers: AllOrSome>, - expose_hdrs: Option, - max_age: Option, - preflight: bool, - send_wildcard: bool, - supports_credentials: bool, - vary_header: bool, -} - -impl Default for Cors { - fn default() -> Cors { - let inner = Inner { - origins: AllOrSome::default(), - origins_str: None, - methods: HashSet::from_iter( - vec![ - Method::GET, - Method::HEAD, - Method::POST, - Method::OPTIONS, - Method::PUT, - Method::PATCH, - Method::DELETE, - ].into_iter(), - ), - headers: AllOrSome::All, - expose_hdrs: None, - max_age: None, - preflight: true, - send_wildcard: false, - supports_credentials: false, - vary_header: true, - }; - Cors { - inner: Rc::new(inner), - } - } -} - -impl Cors { - /// Build a new CORS middleware instance - pub fn build() -> CorsBuilder<()> { - CorsBuilder { - cors: Some(Inner { - origins: AllOrSome::All, - origins_str: None, - methods: HashSet::new(), - headers: AllOrSome::All, - expose_hdrs: None, - max_age: None, - preflight: true, - send_wildcard: false, - supports_credentials: false, - vary_header: true, - }), - methods: false, - error: None, - expose_hdrs: HashSet::new(), - resources: Vec::new(), - app: None, - } - } - - /// Create CorsBuilder for a specified application. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::middleware::cors::Cors; - /// use actix_web::{http, App, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().configure( - /// |app| { - /// Cors::for_app(app) // <- Construct CORS builder - /// .allowed_origin("https://www.rust-lang.org/") - /// .resource("/resource", |r| { // register resource - /// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); - /// }) - /// .register() - /// }, // construct CORS and return application instance - /// ); - /// } - /// ``` - pub fn for_app(app: App) -> CorsBuilder { - CorsBuilder { - cors: Some(Inner { - origins: AllOrSome::All, - origins_str: None, - methods: HashSet::new(), - headers: AllOrSome::All, - expose_hdrs: None, - max_age: None, - preflight: true, - send_wildcard: false, - supports_credentials: false, - vary_header: true, - }), - methods: false, - error: None, - expose_hdrs: HashSet::new(), - resources: Vec::new(), - app: Some(app), - } - } - - /// This method register cors middleware with resource and - /// adds route for *OPTIONS* preflight requests. - /// - /// It is possible to register *Cors* middleware with - /// `Resource::middleware()` method, but in that case *Cors* - /// middleware wont be able to handle *OPTIONS* requests. - pub fn register(self, resource: &mut Resource) { - resource - .method(Method::OPTIONS) - .h(|_: &_| HttpResponse::Ok()); - resource.middleware(self); - } - - fn validate_origin(&self, req: &Request) -> Result<(), CorsError> { - if let Some(hdr) = req.headers().get(header::ORIGIN) { - if let Ok(origin) = hdr.to_str() { - return match self.inner.origins { - AllOrSome::All => Ok(()), - AllOrSome::Some(ref allowed_origins) => allowed_origins - .get(origin) - .and_then(|_| Some(())) - .ok_or_else(|| CorsError::OriginNotAllowed), - }; - } - Err(CorsError::BadOrigin) - } else { - return match self.inner.origins { - AllOrSome::All => Ok(()), - _ => Err(CorsError::MissingOrigin), - }; - } - } - - fn validate_allowed_method(&self, req: &Request) -> Result<(), CorsError> { - 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 - .inner - .methods - .get(&method) - .and_then(|_| Some(())) - .ok_or_else(|| CorsError::MethodNotAllowed); - } - } - Err(CorsError::BadRequestMethod) - } else { - Err(CorsError::MissingRequestMethod) - } - } - - fn validate_allowed_headers(&self, req: &Request) -> Result<(), CorsError> { - match self.inner.headers { - AllOrSome::All => Ok(()), - AllOrSome::Some(ref allowed_headers) => { - if let Some(hdr) = - req.headers().get(header::ACCESS_CONTROL_REQUEST_HEADERS) - { - if let Ok(headers) = hdr.to_str() { - let mut hdrs = HashSet::new(); - for hdr in headers.split(',') { - match HeaderName::try_from(hdr.trim()) { - Ok(hdr) => hdrs.insert(hdr), - Err(_) => return Err(CorsError::BadRequestHeaders), - }; - } - - if !hdrs.is_empty() && !hdrs.is_subset(allowed_headers) { - return Err(CorsError::HeadersNotAllowed); - } - return Ok(()); - } - Err(CorsError::BadRequestHeaders) - } else { - Err(CorsError::MissingRequestHeaders) - } - } - } - } -} - -impl Middleware for Cors { - fn start(&self, req: &HttpRequest) -> Result { - if self.inner.preflight && Method::OPTIONS == *req.method() { - self.validate_origin(req)?; - self.validate_allowed_method(&req)?; - self.validate_allowed_headers(&req)?; - - // allowed headers - let headers = if let Some(headers) = self.inner.headers.as_ref() { - Some( - HeaderValue::try_from( - &headers - .iter() - .fold(String::new(), |s, v| s + "," + v.as_str()) - .as_str()[1..], - ).unwrap(), - ) - } else if let Some(hdr) = - req.headers().get(header::ACCESS_CONTROL_REQUEST_HEADERS) - { - Some(hdr.clone()) - } else { - None - }; - - Ok(Started::Response( - HttpResponse::Ok() - .if_some(self.inner.max_age.as_ref(), |max_age, resp| { - let _ = resp.header( - header::ACCESS_CONTROL_MAX_AGE, - format!("{}", max_age).as_str(), - ); - }).if_some(headers, |headers, resp| { - let _ = - resp.header(header::ACCESS_CONTROL_ALLOW_HEADERS, headers); - }).if_true(self.inner.origins.is_all(), |resp| { - if self.inner.send_wildcard { - resp.header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - } else { - let origin = req.headers().get(header::ORIGIN).unwrap(); - resp.header( - header::ACCESS_CONTROL_ALLOW_ORIGIN, - origin.clone(), - ); - } - }).if_true(self.inner.origins.is_some(), |resp| { - resp.header( - header::ACCESS_CONTROL_ALLOW_ORIGIN, - self.inner.origins_str.as_ref().unwrap().clone(), - ); - }).if_true(self.inner.supports_credentials, |resp| { - resp.header(header::ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - }).header( - header::ACCESS_CONTROL_ALLOW_METHODS, - &self - .inner - .methods - .iter() - .fold(String::new(), |s, v| s + "," + v.as_str()) - .as_str()[1..], - ).finish(), - )) - } else { - // Only check requests with a origin header. - if req.headers().contains_key(header::ORIGIN) { - self.validate_origin(req)?; - } - - Ok(Started::Done) - } - } - - fn response( - &self, req: &HttpRequest, mut resp: HttpResponse, - ) -> Result { - match self.inner.origins { - AllOrSome::All => { - if self.inner.send_wildcard { - resp.headers_mut().insert( - header::ACCESS_CONTROL_ALLOW_ORIGIN, - HeaderValue::from_static("*"), - ); - } else if let Some(origin) = req.headers().get(header::ORIGIN) { - resp.headers_mut() - .insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.clone()); - } - } - AllOrSome::Some(ref origins) => { - if let Some(origin) = req.headers().get(header::ORIGIN).filter(|o| { - match o.to_str() { - Ok(os) => origins.contains(os), - _ => false - } - }) { - resp.headers_mut().insert( - header::ACCESS_CONTROL_ALLOW_ORIGIN, - origin.clone(), - ); - } else { - resp.headers_mut().insert( - header::ACCESS_CONTROL_ALLOW_ORIGIN, - self.inner.origins_str.as_ref().unwrap().clone() - ); - }; - } - } - - if let Some(ref expose) = self.inner.expose_hdrs { - resp.headers_mut().insert( - header::ACCESS_CONTROL_EXPOSE_HEADERS, - HeaderValue::try_from(expose.as_str()).unwrap(), - ); - } - if self.inner.supports_credentials { - resp.headers_mut().insert( - header::ACCESS_CONTROL_ALLOW_CREDENTIALS, - HeaderValue::from_static("true"), - ); - } - if self.inner.vary_header { - let value = if let Some(hdr) = resp.headers_mut().get(header::VARY) { - let mut val: Vec = Vec::with_capacity(hdr.as_bytes().len() + 8); - val.extend(hdr.as_bytes()); - val.extend(b", Origin"); - HeaderValue::try_from(&val[..]).unwrap() - } else { - HeaderValue::from_static("Origin") - }; - resp.headers_mut().insert(header::VARY, value); - } - Ok(Response::Done(resp)) - } -} - -/// Structure that follows the builder pattern for building `Cors` middleware -/// structs. -/// -/// To construct a cors: -/// -/// 1. Call [`Cors::build`](struct.Cors.html#method.build) to start building. -/// 2. Use any of the builder methods to set fields in the backend. -/// 3. Call [finish](struct.Cors.html#method.finish) to retrieve the -/// constructed backend. -/// -/// # Example -/// -/// ```rust -/// # extern crate http; -/// # extern crate actix_web; -/// use actix_web::middleware::cors; -/// use http::header; -/// -/// # fn main() { -/// let cors = cors::Cors::build() -/// .allowed_origin("https://www.rust-lang.org/") -/// .allowed_methods(vec!["GET", "POST"]) -/// .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT]) -/// .allowed_header(header::CONTENT_TYPE) -/// .max_age(3600) -/// .finish(); -/// # } -/// ``` -pub struct CorsBuilder { - cors: Option, - methods: bool, - error: Option, - expose_hdrs: HashSet, - resources: Vec>, - app: Option>, -} - -fn cors<'a>( - parts: &'a mut Option, err: &Option, -) -> Option<&'a mut Inner> { - if err.is_some() { - return None; - } - parts.as_mut() -} - -impl CorsBuilder { - /// Add an origin that are allowed to make requests. - /// Will be verified against the `Origin` request header. - /// - /// When `All` is set, and `send_wildcard` is set, "*" will be sent in - /// the `Access-Control-Allow-Origin` response header. Otherwise, the - /// client's `Origin` request header will be echoed back in the - /// `Access-Control-Allow-Origin` response header. - /// - /// When `Some` is set, the client's `Origin` request header will be - /// checked in a case-sensitive manner. - /// - /// This is the `list of origins` in the - /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model). - /// - /// Defaults to `All`. - /// - /// Builder panics if supplied origin is not valid uri. - pub fn allowed_origin(&mut self, origin: &str) -> &mut CorsBuilder { - if let Some(cors) = cors(&mut self.cors, &self.error) { - match Uri::try_from(origin) { - Ok(_) => { - if cors.origins.is_all() { - cors.origins = AllOrSome::Some(HashSet::new()); - } - if let AllOrSome::Some(ref mut origins) = cors.origins { - origins.insert(origin.to_owned()); - } - } - Err(e) => { - self.error = Some(e.into()); - } - } - } - self - } - - /// Set a list of methods which the allowed origins are allowed to access - /// for requests. - /// - /// This is the `list of methods` in the - /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model). - /// - /// Defaults to `[GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE]` - pub fn allowed_methods(&mut self, methods: U) -> &mut CorsBuilder - where - U: IntoIterator, - Method: HttpTryFrom, - { - self.methods = true; - if let Some(cors) = cors(&mut self.cors, &self.error) { - for m in methods { - match Method::try_from(m) { - Ok(method) => { - cors.methods.insert(method); - } - Err(e) => { - self.error = Some(e.into()); - break; - } - } - } - } - self - } - - /// Set an allowed header - pub fn allowed_header(&mut self, header: H) -> &mut CorsBuilder - where - HeaderName: HttpTryFrom, - { - if let Some(cors) = cors(&mut self.cors, &self.error) { - match HeaderName::try_from(header) { - Ok(method) => { - if cors.headers.is_all() { - cors.headers = AllOrSome::Some(HashSet::new()); - } - if let AllOrSome::Some(ref mut headers) = cors.headers { - headers.insert(method); - } - } - Err(e) => self.error = Some(e.into()), - } - } - self - } - - /// Set a list of header field names which can be used when - /// this resource is accessed by allowed origins. - /// - /// If `All` is set, whatever is requested by the client in - /// `Access-Control-Request-Headers` will be echoed back in the - /// `Access-Control-Allow-Headers` header. - /// - /// This is the `list of headers` in the - /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model). - /// - /// Defaults to `All`. - pub fn allowed_headers(&mut self, headers: U) -> &mut CorsBuilder - where - U: IntoIterator, - HeaderName: HttpTryFrom, - { - if let Some(cors) = cors(&mut self.cors, &self.error) { - for h in headers { - match HeaderName::try_from(h) { - Ok(method) => { - if cors.headers.is_all() { - cors.headers = AllOrSome::Some(HashSet::new()); - } - if let AllOrSome::Some(ref mut headers) = cors.headers { - headers.insert(method); - } - } - Err(e) => { - self.error = Some(e.into()); - break; - } - } - } - } - self - } - - /// Set a list of headers which are safe to expose to the API of a CORS API - /// specification. This corresponds to the - /// `Access-Control-Expose-Headers` response header. - /// - /// This is the `list of exposed headers` in the - /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model). - /// - /// This defaults to an empty set. - pub fn expose_headers(&mut self, headers: U) -> &mut CorsBuilder - where - U: IntoIterator, - HeaderName: HttpTryFrom, - { - for h in headers { - match HeaderName::try_from(h) { - Ok(method) => { - self.expose_hdrs.insert(method); - } - Err(e) => { - self.error = Some(e.into()); - break; - } - } - } - self - } - - /// Set a maximum time for which this CORS request maybe cached. - /// This value is set as the `Access-Control-Max-Age` header. - /// - /// This defaults to `None` (unset). - pub fn max_age(&mut self, max_age: usize) -> &mut CorsBuilder { - if let Some(cors) = cors(&mut self.cors, &self.error) { - cors.max_age = Some(max_age) - } - self - } - - /// Set a wildcard origins - /// - /// If send wildcard is set and the `allowed_origins` parameter is `All`, a - /// wildcard `Access-Control-Allow-Origin` response header is sent, - /// rather than the request’s `Origin` header. - /// - /// This is the `supports credentials flag` in the - /// [Resource Processing Model](https://www.w3.org/TR/cors/#resource-processing-model). - /// - /// This **CANNOT** be used in conjunction with `allowed_origins` set to - /// `All` and `allow_credentials` set to `true`. Depending on the mode - /// of usage, this will either result in an `Error:: - /// CredentialsWithWildcardOrigin` error during actix launch or runtime. - /// - /// Defaults to `false`. - pub fn send_wildcard(&mut self) -> &mut CorsBuilder { - if let Some(cors) = cors(&mut self.cors, &self.error) { - cors.send_wildcard = true - } - self - } - - /// Allows users to make authenticated requests - /// - /// If true, injects the `Access-Control-Allow-Credentials` header in - /// responses. This allows cookies and credentials to be submitted - /// across domains. - /// - /// This option cannot be used in conjunction with an `allowed_origin` set - /// to `All` and `send_wildcards` set to `true`. - /// - /// Defaults to `false`. - /// - /// Builder panics if credentials are allowed, but the Origin is set to "*". - /// This is not allowed by W3C - pub fn supports_credentials(&mut self) -> &mut CorsBuilder { - if let Some(cors) = cors(&mut self.cors, &self.error) { - cors.supports_credentials = true - } - self - } - - /// Disable `Vary` header support. - /// - /// When enabled the header `Vary: Origin` will be returned as per the W3 - /// implementation guidelines. - /// - /// Setting this header when the `Access-Control-Allow-Origin` is - /// dynamically generated (e.g. when there is more than one allowed - /// origin, and an Origin than '*' is returned) informs CDNs and other - /// caches that the CORS headers are dynamic, and cannot be cached. - /// - /// By default `vary` header support is enabled. - pub fn disable_vary_header(&mut self) -> &mut CorsBuilder { - if let Some(cors) = cors(&mut self.cors, &self.error) { - cors.vary_header = false - } - self - } - - /// Disable *preflight* request support. - /// - /// When enabled cors middleware automatically handles *OPTIONS* request. - /// This is useful application level middleware. - /// - /// By default *preflight* support is enabled. - pub fn disable_preflight(&mut self) -> &mut CorsBuilder { - if let Some(cors) = cors(&mut self.cors, &self.error) { - cors.preflight = false - } - self - } - - /// Configure resource for a specific path. - /// - /// This is similar to a `App::resource()` method. Except, cors middleware - /// get registered for the resource. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::middleware::cors::Cors; - /// use actix_web::{http, App, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().configure( - /// |app| { - /// Cors::for_app(app) // <- Construct CORS builder - /// .allowed_origin("https://www.rust-lang.org/") - /// .allowed_methods(vec!["GET", "POST"]) - /// .allowed_header(http::header::CONTENT_TYPE) - /// .max_age(3600) - /// .resource("/resource1", |r| { // register resource - /// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); - /// }) - /// .resource("/resource2", |r| { // register another resource - /// r.method(http::Method::HEAD) - /// .f(|_| HttpResponse::MethodNotAllowed()); - /// }) - /// .register() - /// }, // construct CORS and return application instance - /// ); - /// } - /// ``` - pub fn resource(&mut self, path: &str, f: F) -> &mut CorsBuilder - where - F: FnOnce(&mut Resource) -> R + 'static, - { - // add resource handler - let mut resource = Resource::new(ResourceDef::new(path)); - f(&mut resource); - - self.resources.push(resource); - self - } - - fn construct(&mut self) -> Cors { - if !self.methods { - self.allowed_methods(vec![ - Method::GET, - Method::HEAD, - Method::POST, - Method::OPTIONS, - Method::PUT, - Method::PATCH, - Method::DELETE, - ]); - } - - if let Some(e) = self.error.take() { - panic!("{}", e); - } - - let mut cors = self.cors.take().expect("cannot reuse CorsBuilder"); - - if cors.supports_credentials && cors.send_wildcard && cors.origins.is_all() { - panic!("Credentials are allowed, but the Origin is set to \"*\""); - } - - if let AllOrSome::Some(ref origins) = cors.origins { - let s = origins - .iter() - .fold(String::new(), |s, v| format!("{}, {}", s, v)); - cors.origins_str = Some(HeaderValue::try_from(&s[2..]).unwrap()); - } - - if !self.expose_hdrs.is_empty() { - cors.expose_hdrs = Some( - self.expose_hdrs - .iter() - .fold(String::new(), |s, v| format!("{}, {}", s, v.as_str()))[2..] - .to_owned(), - ); - } - Cors { - inner: Rc::new(cors), - } - } - - /// Finishes building and returns the built `Cors` instance. - /// - /// This method panics in case of any configuration error. - pub fn finish(&mut self) -> Cors { - if !self.resources.is_empty() { - panic!( - "CorsBuilder::resource() was used, - to construct CORS `.register(app)` method should be used" - ); - } - self.construct() - } - - /// Finishes building Cors middleware and register middleware for - /// application - /// - /// This method panics in case of any configuration error or if non of - /// resources are registered. - pub fn register(&mut self) -> App { - if self.resources.is_empty() { - panic!("No resources are registered."); - } - - let cors = self.construct(); - let mut app = self - .app - .take() - .expect("CorsBuilder has to be constructed with Cors::for_app(app)"); - - // register resources - for mut resource in self.resources.drain(..) { - cors.clone().register(&mut resource); - app.register_resource(resource); - } - - app - } -} - -#[cfg(test)] -mod tests { - use super::*; - use test::{self, TestRequest}; - - impl Started { - fn is_done(&self) -> bool { - match *self { - Started::Done => true, - _ => false, - } - } - fn response(self) -> HttpResponse { - match self { - Started::Response(resp) => resp, - _ => panic!(), - } - } - } - impl Response { - fn response(self) -> HttpResponse { - match self { - Response::Done(resp) => resp, - _ => panic!(), - } - } - } - - #[test] - #[should_panic(expected = "Credentials are allowed, but the Origin is set to")] - fn cors_validates_illegal_allow_credentials() { - Cors::build() - .supports_credentials() - .send_wildcard() - .finish(); - } - - #[test] - #[should_panic(expected = "No resources are registered")] - fn no_resource() { - Cors::build() - .supports_credentials() - .send_wildcard() - .register(); - } - - #[test] - #[should_panic(expected = "Cors::for_app(app)")] - fn no_resource2() { - Cors::build() - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - .register(); - } - - #[test] - fn validate_origin_allows_all_origins() { - let cors = Cors::default(); - let req = TestRequest::with_header("Origin", "https://www.example.com").finish(); - - assert!(cors.start(&req).ok().unwrap().is_done()) - } - - #[test] - fn test_preflight() { - let mut cors = Cors::build() - .send_wildcard() - .max_age(3600) - .allowed_methods(vec![Method::GET, Method::OPTIONS, Method::POST]) - .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT]) - .allowed_header(header::CONTENT_TYPE) - .finish(); - - let req = TestRequest::with_header("Origin", "https://www.example.com") - .method(Method::OPTIONS) - .finish(); - - assert!(cors.start(&req).is_err()); - - let req = TestRequest::with_header("Origin", "https://www.example.com") - .header(header::ACCESS_CONTROL_REQUEST_METHOD, "put") - .method(Method::OPTIONS) - .finish(); - - assert!(cors.start(&req).is_err()); - - let req = TestRequest::with_header("Origin", "https://www.example.com") - .header(header::ACCESS_CONTROL_REQUEST_METHOD, "POST") - .header( - header::ACCESS_CONTROL_REQUEST_HEADERS, - "AUTHORIZATION,ACCEPT", - ).method(Method::OPTIONS) - .finish(); - - let resp = cors.start(&req).unwrap().response(); - assert_eq!( - &b"*"[..], - resp.headers() - .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) - .unwrap() - .as_bytes() - ); - assert_eq!( - &b"3600"[..], - resp.headers() - .get(header::ACCESS_CONTROL_MAX_AGE) - .unwrap() - .as_bytes() - ); - //assert_eq!( - // &b"authorization,accept,content-type"[..], - // resp.headers().get(header::ACCESS_CONTROL_ALLOW_HEADERS).unwrap(). - // as_bytes()); assert_eq!( - // &b"POST,GET,OPTIONS"[..], - // resp.headers().get(header::ACCESS_CONTROL_ALLOW_METHODS).unwrap(). - // as_bytes()); - - Rc::get_mut(&mut cors.inner).unwrap().preflight = false; - assert!(cors.start(&req).unwrap().is_done()); - } - - // #[test] - // #[should_panic(expected = "MissingOrigin")] - // fn test_validate_missing_origin() { - // let cors = Cors::build() - // .allowed_origin("https://www.example.com") - // .finish(); - // let mut req = HttpRequest::default(); - // cors.start(&req).unwrap(); - // } - - #[test] - #[should_panic(expected = "OriginNotAllowed")] - fn test_validate_not_allowed_origin() { - let cors = Cors::build() - .allowed_origin("https://www.example.com") - .finish(); - - let req = TestRequest::with_header("Origin", "https://www.unknown.com") - .method(Method::GET) - .finish(); - cors.start(&req).unwrap(); - } - - #[test] - fn test_validate_origin() { - let cors = Cors::build() - .allowed_origin("https://www.example.com") - .finish(); - - let req = TestRequest::with_header("Origin", "https://www.example.com") - .method(Method::GET) - .finish(); - - assert!(cors.start(&req).unwrap().is_done()); - } - - #[test] - fn test_no_origin_response() { - let cors = Cors::build().finish(); - - let req = TestRequest::default().method(Method::GET).finish(); - let resp: HttpResponse = HttpResponse::Ok().into(); - let resp = cors.response(&req, resp).unwrap().response(); - assert!( - resp.headers() - .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) - .is_none() - ); - - let req = TestRequest::with_header("Origin", "https://www.example.com") - .method(Method::OPTIONS) - .finish(); - let resp = cors.response(&req, resp).unwrap().response(); - assert_eq!( - &b"https://www.example.com"[..], - resp.headers() - .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) - .unwrap() - .as_bytes() - ); - } - - #[test] - fn test_response() { - let exposed_headers = vec![header::AUTHORIZATION, header::ACCEPT]; - let cors = Cors::build() - .send_wildcard() - .disable_preflight() - .max_age(3600) - .allowed_methods(vec![Method::GET, Method::OPTIONS, Method::POST]) - .allowed_headers(exposed_headers.clone()) - .expose_headers(exposed_headers.clone()) - .allowed_header(header::CONTENT_TYPE) - .finish(); - - let req = TestRequest::with_header("Origin", "https://www.example.com") - .method(Method::OPTIONS) - .finish(); - - let resp: HttpResponse = HttpResponse::Ok().into(); - let resp = cors.response(&req, resp).unwrap().response(); - assert_eq!( - &b"*"[..], - resp.headers() - .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) - .unwrap() - .as_bytes() - ); - assert_eq!( - &b"Origin"[..], - resp.headers().get(header::VARY).unwrap().as_bytes() - ); - - { - let headers = resp - .headers() - .get(header::ACCESS_CONTROL_EXPOSE_HEADERS) - .unwrap() - .to_str() - .unwrap() - .split(',') - .map(|s| s.trim()) - .collect::>(); - - for h in exposed_headers { - assert!(headers.contains(&h.as_str())); - } - } - - let resp: HttpResponse = - HttpResponse::Ok().header(header::VARY, "Accept").finish(); - let resp = cors.response(&req, resp).unwrap().response(); - assert_eq!( - &b"Accept, Origin"[..], - resp.headers().get(header::VARY).unwrap().as_bytes() - ); - - let cors = Cors::build() - .disable_vary_header() - .allowed_origin("https://www.example.com") - .allowed_origin("https://www.google.com") - .finish(); - let resp: HttpResponse = HttpResponse::Ok().into(); - let resp = cors.response(&req, resp).unwrap().response(); - - let origins_str = resp - .headers() - .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) - .unwrap() - .to_str() - .unwrap(); - - assert_eq!( - "https://www.example.com", - origins_str - ); - } - - #[test] - fn cors_resource() { - let mut srv = test::TestServer::with_factory(|| { - App::new().configure(|app| { - Cors::for_app(app) - .allowed_origin("https://www.example.com") - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - .register() - }) - }); - - let request = srv - .get() - .uri(srv.url("/test")) - .header("ORIGIN", "https://www.example2.com") - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - - let request = srv - .get() - .uri(srv.url("/test")) - .header("ORIGIN", "https://www.example.com") - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - } - - #[test] - fn test_multiple_origins() { - let cors = Cors::build() - .allowed_origin("https://example.com") - .allowed_origin("https://example.org") - .allowed_methods(vec![Method::GET]) - .finish(); - - - let req = TestRequest::with_header("Origin", "https://example.com") - .method(Method::GET) - .finish(); - let resp: HttpResponse = HttpResponse::Ok().into(); - - let resp = cors.response(&req, resp).unwrap().response(); - print!("{:?}", resp); - assert_eq!( - &b"https://example.com"[..], - resp.headers() - .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) - .unwrap() - .as_bytes() - ); - - let req = TestRequest::with_header("Origin", "https://example.org") - .method(Method::GET) - .finish(); - let resp: HttpResponse = HttpResponse::Ok().into(); - - let resp = cors.response(&req, resp).unwrap().response(); - assert_eq!( - &b"https://example.org"[..], - resp.headers() - .get(header::ACCESS_CONTROL_ALLOW_ORIGIN) - .unwrap() - .as_bytes() - ); - } -} diff --git a/src/middleware/csrf.rs b/src/middleware/csrf.rs deleted file mode 100644 index cacfc8d5..00000000 --- a/src/middleware/csrf.rs +++ /dev/null @@ -1,275 +0,0 @@ -//! A filter for cross-site request forgery (CSRF). -//! -//! This middleware is stateless and [based on request -//! headers](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Verifying_Same_Origin_with_Standard_Headers). -//! -//! By default requests are allowed only if one of these is true: -//! -//! * The request method is safe (`GET`, `HEAD`, `OPTIONS`). It is the -//! applications responsibility to ensure these methods cannot be used to -//! execute unwanted actions. Note that upgrade requests for websockets are -//! also considered safe. -//! * The `Origin` header (added automatically by the browser) matches one -//! of the allowed origins. -//! * There is no `Origin` header but the `Referer` header matches one of -//! the allowed origins. -//! -//! Use [`CsrfFilter::allow_xhr()`](struct.CsrfFilter.html#method.allow_xhr) -//! if you want to allow requests with unprotected methods via -//! [CORS](../cors/struct.Cors.html). -//! -//! # Example -//! -//! ``` -//! # extern crate actix_web; -//! use actix_web::middleware::csrf; -//! use actix_web::{http, App, HttpRequest, HttpResponse}; -//! -//! fn handle_post(_: &HttpRequest) -> &'static str { -//! "This action should only be triggered with requests from the same site" -//! } -//! -//! fn main() { -//! let app = App::new() -//! .middleware( -//! csrf::CsrfFilter::new().allowed_origin("https://www.example.com"), -//! ) -//! .resource("/", |r| { -//! r.method(http::Method::GET).f(|_| HttpResponse::Ok()); -//! r.method(http::Method::POST).f(handle_post); -//! }) -//! .finish(); -//! } -//! ``` -//! -//! In this example the entire application is protected from CSRF. - -use std::borrow::Cow; -use std::collections::HashSet; - -use bytes::Bytes; -use error::{ResponseError, Result}; -use http::{header, HeaderMap, HttpTryFrom, Uri}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Started}; -use server::Request; - -/// Potential cross-site request forgery detected. -#[derive(Debug, Fail)] -pub enum CsrfError { - /// The HTTP request header `Origin` was required but not provided. - #[fail(display = "Origin header required")] - MissingOrigin, - /// The HTTP request header `Origin` could not be parsed correctly. - #[fail(display = "Could not parse Origin header")] - BadOrigin, - /// The cross-site request was denied. - #[fail(display = "Cross-site request denied")] - CsrDenied, -} - -impl ResponseError for CsrfError { - fn error_response(&self) -> HttpResponse { - HttpResponse::Forbidden().body(self.to_string()) - } -} - -fn uri_origin(uri: &Uri) -> Option { - match (uri.scheme_part(), uri.host(), uri.port_part().map(|port| port.as_u16())) { - (Some(scheme), Some(host), Some(port)) => { - Some(format!("{}://{}:{}", scheme, host, port)) - } - (Some(scheme), Some(host), None) => Some(format!("{}://{}", scheme, host)), - _ => None, - } -} - -fn origin(headers: &HeaderMap) -> Option, CsrfError>> { - headers - .get(header::ORIGIN) - .map(|origin| { - origin - .to_str() - .map_err(|_| CsrfError::BadOrigin) - .map(|o| o.into()) - }).or_else(|| { - headers.get(header::REFERER).map(|referer| { - Uri::try_from(Bytes::from(referer.as_bytes())) - .ok() - .as_ref() - .and_then(uri_origin) - .ok_or(CsrfError::BadOrigin) - .map(|o| o.into()) - }) - }) -} - -/// A middleware that filters cross-site requests. -/// -/// To construct a CSRF filter: -/// -/// 1. Call [`CsrfFilter::build`](struct.CsrfFilter.html#method.build) to -/// start building. -/// 2. [Add](struct.CsrfFilterBuilder.html#method.allowed_origin) allowed -/// origins. -/// 3. Call [finish](struct.CsrfFilterBuilder.html#method.finish) to retrieve -/// the constructed filter. -/// -/// # Example -/// -/// ``` -/// use actix_web::middleware::csrf; -/// use actix_web::App; -/// -/// # fn main() { -/// let app = App::new() -/// .middleware(csrf::CsrfFilter::new().allowed_origin("https://www.example.com")); -/// # } -/// ``` -#[derive(Default)] -pub struct CsrfFilter { - origins: HashSet, - allow_xhr: bool, - allow_missing_origin: bool, - allow_upgrade: bool, -} - -impl CsrfFilter { - /// Start building a `CsrfFilter`. - pub fn new() -> CsrfFilter { - CsrfFilter { - origins: HashSet::new(), - allow_xhr: false, - allow_missing_origin: false, - allow_upgrade: false, - } - } - - /// Add an origin that is allowed to make requests. Will be verified - /// against the `Origin` request header. - pub fn allowed_origin>(mut self, origin: T) -> CsrfFilter { - self.origins.insert(origin.into()); - self - } - - /// Allow all requests with an `X-Requested-With` header. - /// - /// A cross-site attacker should not be able to send requests with custom - /// headers unless a CORS policy whitelists them. Therefore it should be - /// safe to allow requests with an `X-Requested-With` header (added - /// automatically by many JavaScript libraries). - /// - /// This is disabled by default, because in Safari it is possible to - /// circumvent this using redirects and Flash. - /// - /// Use this method to enable more lax filtering. - pub fn allow_xhr(mut self) -> CsrfFilter { - self.allow_xhr = true; - self - } - - /// Allow requests if the expected `Origin` header is missing (and - /// there is no `Referer` to fall back on). - /// - /// The filter is conservative by default, but it should be safe to allow - /// missing `Origin` headers because a cross-site attacker cannot prevent - /// the browser from sending `Origin` on unprotected requests. - pub fn allow_missing_origin(mut self) -> CsrfFilter { - self.allow_missing_origin = true; - self - } - - /// Allow cross-site upgrade requests (for example to open a WebSocket). - pub fn allow_upgrade(mut self) -> CsrfFilter { - self.allow_upgrade = true; - self - } - - fn validate(&self, req: &Request) -> Result<(), CsrfError> { - let is_upgrade = req.headers().contains_key(header::UPGRADE); - let is_safe = req.method().is_safe() && (self.allow_upgrade || !is_upgrade); - - if is_safe || (self.allow_xhr && req.headers().contains_key("x-requested-with")) - { - Ok(()) - } else if let Some(header) = origin(req.headers()) { - match header { - Ok(ref origin) if self.origins.contains(origin.as_ref()) => Ok(()), - Ok(_) => Err(CsrfError::CsrDenied), - Err(err) => Err(err), - } - } else if self.allow_missing_origin { - Ok(()) - } else { - Err(CsrfError::MissingOrigin) - } - } -} - -impl Middleware for CsrfFilter { - fn start(&self, req: &HttpRequest) -> Result { - self.validate(req)?; - Ok(Started::Done) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use http::Method; - use test::TestRequest; - - #[test] - fn test_safe() { - let csrf = CsrfFilter::new().allowed_origin("https://www.example.com"); - - let req = TestRequest::with_header("Origin", "https://www.w3.org") - .method(Method::HEAD) - .finish(); - - assert!(csrf.start(&req).is_ok()); - } - - #[test] - fn test_csrf() { - let csrf = CsrfFilter::new().allowed_origin("https://www.example.com"); - - let req = TestRequest::with_header("Origin", "https://www.w3.org") - .method(Method::POST) - .finish(); - - assert!(csrf.start(&req).is_err()); - } - - #[test] - fn test_referer() { - let csrf = CsrfFilter::new().allowed_origin("https://www.example.com"); - - let req = TestRequest::with_header( - "Referer", - "https://www.example.com/some/path?query=param", - ).method(Method::POST) - .finish(); - - assert!(csrf.start(&req).is_ok()); - } - - #[test] - fn test_upgrade() { - let strict_csrf = CsrfFilter::new().allowed_origin("https://www.example.com"); - - let lax_csrf = CsrfFilter::new() - .allowed_origin("https://www.example.com") - .allow_upgrade(); - - let req = TestRequest::with_header("Origin", "https://cswsh.com") - .header("Connection", "Upgrade") - .header("Upgrade", "websocket") - .method(Method::GET) - .finish(); - - assert!(strict_csrf.start(&req).is_err()); - assert!(lax_csrf.start(&req).is_ok()); - } -} diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index a33fa6a3..1ace34fe 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -1,17 +1,19 @@ -//! Default response headers -use http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; -use http::{HeaderMap, HttpTryFrom}; +//! Middleware for setting default response headers +use std::rc::Rc; -use error::Result; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response}; +use actix_http::http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; +use actix_http::http::{HeaderMap, HttpTryFrom}; +use actix_service::{IntoNewTransform, Service, Transform}; +use futures::{Async, Future, Poll}; + +use crate::middleware::MiddlewareFactory; +use crate::service::{ServiceRequest, ServiceResponse}; /// `Middleware` for setting default response headers. /// /// This middleware does not set header if response headers already contains it. /// -/// ```rust +/// ```rust,ignore /// # extern crate actix_web; /// use actix_web::{http, middleware, App, HttpResponse}; /// @@ -22,11 +24,15 @@ use middleware::{Middleware, Response}; /// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); /// r.method(http::Method::HEAD) /// .f(|_| HttpResponse::MethodNotAllowed()); -/// }) -/// .finish(); +/// }); /// } /// ``` +#[derive(Clone)] pub struct DefaultHeaders { + inner: Rc, +} + +struct Inner { ct: bool, headers: HeaderMap, } @@ -34,8 +40,10 @@ pub struct DefaultHeaders { impl Default for DefaultHeaders { fn default() -> Self { DefaultHeaders { - ct: false, - headers: HeaderMap::new(), + inner: Rc::new(Inner { + ct: false, + headers: HeaderMap::new(), + }), } } } @@ -48,16 +56,19 @@ impl DefaultHeaders { /// Set a header. #[inline] - #[cfg_attr(feature = "cargo-clippy", allow(match_wild_err_arm))] pub fn header(mut self, key: K, value: V) -> Self where HeaderName: HttpTryFrom, HeaderValue: HttpTryFrom, { + #[allow(clippy::match_wild_err_arm)] match HeaderName::try_from(key) { Ok(key) => match HeaderValue::try_from(value) { Ok(value) => { - self.headers.append(key, value); + Rc::get_mut(&mut self.inner) + .expect("Multiple copies exist") + .headers + .append(key, value); } Err(_) => panic!("Can not create header value"), }, @@ -68,53 +79,85 @@ impl DefaultHeaders { /// Set *CONTENT-TYPE* header if response does not contain this header. pub fn content_type(mut self) -> Self { - self.ct = true; + Rc::get_mut(&mut self.inner) + .expect("Multiple copies exist") + .ct = true; self } } -impl Middleware for DefaultHeaders { - fn response(&self, _: &HttpRequest, mut resp: HttpResponse) -> Result { - for (key, value) in self.headers.iter() { - if !resp.headers().contains_key(key) { - resp.headers_mut().insert(key, value.clone()); +impl IntoNewTransform, S> + for DefaultHeaders +where + S: Service, Response = ServiceResponse>, + S::Future: 'static, +{ + fn into_new_transform(self) -> MiddlewareFactory { + MiddlewareFactory::new(self) + } +} + +impl Transform for DefaultHeaders +where + S: Service, Response = ServiceResponse>, + S::Future: 'static, +{ + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = S::Error; + type Future = Box>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, req: ServiceRequest, srv: &mut S) -> Self::Future { + let inner = self.inner.clone(); + + Box::new(srv.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()); + } } - } - // default content-type - if self.ct && !resp.headers().contains_key(CONTENT_TYPE) { - resp.headers_mut().insert( - CONTENT_TYPE, - HeaderValue::from_static("application/octet-stream"), - ); - } - Ok(Response::Done(resp)) + // default content-type + if inner.ct && !res.headers().contains_key(CONTENT_TYPE) { + res.headers_mut().insert( + CONTENT_TYPE, + HeaderValue::from_static("application/octet-stream"), + ); + } + + res + })) } } -#[cfg(test)] -mod tests { - use super::*; - use http::header::CONTENT_TYPE; - use test::TestRequest; +// #[cfg(test)] +// mod tests { +// use super::*; +// use actix_http::http::header::CONTENT_TYPE; +// use actix_http::test::TestRequest; - #[test] - fn test_default_headers() { - let mw = DefaultHeaders::new().header(CONTENT_TYPE, "0001"); +// #[test] +// fn test_default_headers() { +// let mw = DefaultHeaders::new().header(CONTENT_TYPE, "0001"); - let req = TestRequest::default().finish(); +// let req = TestRequest::default().finish(); - let resp = HttpResponse::Ok().finish(); - let resp = match mw.response(&req, resp) { - Ok(Response::Done(resp)) => resp, - _ => panic!(), - }; - assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); +// let resp = Response::Ok().finish(); +// let resp = match mw.response(&req, resp) { +// Ok(Response::Done(resp)) => resp, +// _ => panic!(), +// }; +// assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); - let resp = HttpResponse::Ok().header(CONTENT_TYPE, "0002").finish(); - let resp = match mw.response(&req, resp) { - Ok(Response::Done(resp)) => resp, - _ => panic!(), - }; - assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0002"); - } -} +// let resp = Response::Ok().header(CONTENT_TYPE, "0002").finish(); +// let resp = match mw.response(&req, resp) { +// Ok(Response::Done(resp)) => resp, +// _ => panic!(), +// }; +// assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0002"); +// } +// } diff --git a/src/middleware/errhandlers.rs b/src/middleware/errhandlers.rs deleted file mode 100644 index c7d19d33..00000000 --- a/src/middleware/errhandlers.rs +++ /dev/null @@ -1,141 +0,0 @@ -use std::collections::HashMap; - -use error::Result; -use http::StatusCode; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response}; - -type ErrorHandler = Fn(&HttpRequest, HttpResponse) -> Result; - -/// `Middleware` for allowing custom handlers for responses. -/// -/// You can use `ErrorHandlers::handler()` method to register a custom error -/// handler for specific status code. You can modify existing response or -/// create completely new one. -/// -/// ## Example -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::{ErrorHandlers, Response}; -/// use actix_web::{http, App, HttpRequest, HttpResponse, Result}; -/// -/// fn render_500(_: &HttpRequest, resp: HttpResponse) -> Result { -/// let mut builder = resp.into_builder(); -/// builder.header(http::header::CONTENT_TYPE, "application/json"); -/// Ok(Response::Done(builder.into())) -/// } -/// -/// fn main() { -/// let app = App::new() -/// .middleware( -/// ErrorHandlers::new() -/// .handler(http::StatusCode::INTERNAL_SERVER_ERROR, render_500), -/// ) -/// .resource("/test", |r| { -/// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); -/// r.method(http::Method::HEAD) -/// .f(|_| HttpResponse::MethodNotAllowed()); -/// }) -/// .finish(); -/// } -/// ``` -pub struct ErrorHandlers { - handlers: HashMap>>, -} - -impl Default for ErrorHandlers { - fn default() -> Self { - ErrorHandlers { - handlers: HashMap::new(), - } - } -} - -impl ErrorHandlers { - /// Construct new `ErrorHandlers` instance - pub fn new() -> Self { - ErrorHandlers::default() - } - - /// Register error handler for specified status code - pub fn handler(mut self, status: StatusCode, handler: F) -> Self - where - F: Fn(&HttpRequest, HttpResponse) -> Result + 'static, - { - self.handlers.insert(status, Box::new(handler)); - self - } -} - -impl Middleware for ErrorHandlers { - fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { - if let Some(handler) = self.handlers.get(&resp.status()) { - handler(req, resp) - } else { - Ok(Response::Done(resp)) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use error::{Error, ErrorInternalServerError}; - use http::header::CONTENT_TYPE; - use http::StatusCode; - use httpmessage::HttpMessage; - use middleware::Started; - use test::{self, TestRequest}; - - fn render_500(_: &HttpRequest, resp: HttpResponse) -> Result { - let mut builder = resp.into_builder(); - builder.header(CONTENT_TYPE, "0001"); - Ok(Response::Done(builder.into())) - } - - #[test] - fn test_handler() { - let mw = - ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); - - let mut req = TestRequest::default().finish(); - let resp = HttpResponse::InternalServerError().finish(); - let resp = match mw.response(&mut req, resp) { - Ok(Response::Done(resp)) => resp, - _ => panic!(), - }; - assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); - - let resp = HttpResponse::Ok().finish(); - let resp = match mw.response(&mut req, resp) { - Ok(Response::Done(resp)) => resp, - _ => panic!(), - }; - assert!(!resp.headers().contains_key(CONTENT_TYPE)); - } - - struct MiddlewareOne; - - impl Middleware for MiddlewareOne { - fn start(&self, _: &HttpRequest) -> Result { - Err(ErrorInternalServerError("middleware error")) - } - } - - #[test] - fn test_middleware_start_error() { - let mut srv = test::TestServer::new(move |app| { - app.middleware( - ErrorHandlers::new() - .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500), - ).middleware(MiddlewareOne) - .handler(|_| HttpResponse::Ok()) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.headers().get(CONTENT_TYPE).unwrap(), "0001"); - } -} diff --git a/src/middleware/identity.rs b/src/middleware/identity.rs deleted file mode 100644 index a664ba1f..00000000 --- a/src/middleware/identity.rs +++ /dev/null @@ -1,399 +0,0 @@ -//! Request identity service for Actix applications. -//! -//! [**IdentityService**](struct.IdentityService.html) middleware can be -//! used with different policies types to store identity information. -//! -//! By default, only cookie identity policy is implemented. Other backend -//! implementations can be added separately. -//! -//! [**CookieIdentityPolicy**](struct.CookieIdentityPolicy.html) -//! uses cookies as identity storage. -//! -//! To access current request identity -//! [**RequestIdentity**](trait.RequestIdentity.html) should be used. -//! *HttpRequest* implements *RequestIdentity* trait. -//! -//! ```rust -//! use actix_web::middleware::identity::RequestIdentity; -//! use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; -//! use actix_web::*; -//! -//! fn index(req: HttpRequest) -> Result { -//! // access request identity -//! if let Some(id) = req.identity() { -//! Ok(format!("Welcome! {}", id)) -//! } else { -//! Ok("Welcome Anonymous!".to_owned()) -//! } -//! } -//! -//! fn login(mut req: HttpRequest) -> HttpResponse { -//! req.remember("User1".to_owned()); // <- remember identity -//! HttpResponse::Ok().finish() -//! } -//! -//! fn logout(mut req: HttpRequest) -> HttpResponse { -//! req.forget(); // <- remove identity -//! HttpResponse::Ok().finish() -//! } -//! -//! fn main() { -//! let app = App::new().middleware(IdentityService::new( -//! // <- create identity middleware -//! CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend -//! .name("auth-cookie") -//! .secure(false), -//! )); -//! } -//! ``` -use std::rc::Rc; - -use cookie::{Cookie, CookieJar, Key, SameSite}; -use futures::future::{err as FutErr, ok as FutOk, FutureResult}; -use futures::Future; -use time::Duration; - -use error::{Error, Result}; -use http::header::{self, HeaderValue}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response, Started}; - -/// The helper trait to obtain your identity from a request. -/// -/// ```rust -/// use actix_web::middleware::identity::RequestIdentity; -/// use actix_web::*; -/// -/// fn index(req: HttpRequest) -> Result { -/// // access request identity -/// if let Some(id) = req.identity() { -/// Ok(format!("Welcome! {}", id)) -/// } else { -/// Ok("Welcome Anonymous!".to_owned()) -/// } -/// } -/// -/// fn login(mut req: HttpRequest) -> HttpResponse { -/// req.remember("User1".to_owned()); // <- remember identity -/// HttpResponse::Ok().finish() -/// } -/// -/// fn logout(mut req: HttpRequest) -> HttpResponse { -/// req.forget(); // <- remove identity -/// HttpResponse::Ok().finish() -/// } -/// # fn main() {} -/// ``` -pub trait RequestIdentity { - /// Return the claimed identity of the user associated request or - /// ``None`` if no identity can be found associated with the request. - fn identity(&self) -> Option; - - /// Remember identity. - fn remember(&self, identity: String); - - /// This method is used to 'forget' the current identity on subsequent - /// requests. - fn forget(&self); -} - -impl RequestIdentity for HttpRequest { - fn identity(&self) -> Option { - if let Some(id) = self.extensions().get::() { - return id.0.identity().map(|s| s.to_owned()); - } - None - } - - fn remember(&self, identity: String) { - if let Some(id) = self.extensions_mut().get_mut::() { - return id.0.as_mut().remember(identity); - } - } - - fn forget(&self) { - if let Some(id) = self.extensions_mut().get_mut::() { - return id.0.forget(); - } - } -} - -/// An identity -pub trait Identity: 'static { - /// Return the claimed identity of the user associated request or - /// ``None`` if no identity can be found associated with the request. - fn identity(&self) -> Option<&str>; - - /// Remember identity. - fn remember(&mut self, key: String); - - /// This method is used to 'forget' the current identity on subsequent - /// requests. - fn forget(&mut self); - - /// Write session to storage backend. - fn write(&mut self, resp: HttpResponse) -> Result; -} - -/// Identity policy definition. -pub trait IdentityPolicy: Sized + 'static { - /// The associated identity - type Identity: Identity; - - /// The return type of the middleware - type Future: Future; - - /// Parse the session from request and load data from a service identity. - fn from_request(&self, request: &HttpRequest) -> Self::Future; -} - -/// Request identity middleware -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; -/// use actix_web::App; -/// -/// fn main() { -/// let app = App::new().middleware(IdentityService::new( -/// // <- create identity middleware -/// CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend -/// .name("auth-cookie") -/// .secure(false), -/// )); -/// } -/// ``` -pub struct IdentityService { - backend: T, -} - -impl IdentityService { - /// Create new identity service with specified backend. - pub fn new(backend: T) -> Self { - IdentityService { backend } - } -} - -struct IdentityBox(Box); - -impl> Middleware for IdentityService { - fn start(&self, req: &HttpRequest) -> Result { - let req = req.clone(); - let fut = self.backend.from_request(&req).then(move |res| match res { - Ok(id) => { - req.extensions_mut().insert(IdentityBox(Box::new(id))); - FutOk(None) - } - Err(err) => FutErr(err), - }); - Ok(Started::Future(Box::new(fut))) - } - - fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { - if let Some(ref mut id) = req.extensions_mut().get_mut::() { - id.0.as_mut().write(resp) - } else { - Ok(Response::Done(resp)) - } - } -} - -#[doc(hidden)] -/// Identity that uses private cookies as identity storage. -pub struct CookieIdentity { - changed: bool, - identity: Option, - inner: Rc, -} - -impl Identity for CookieIdentity { - fn identity(&self) -> Option<&str> { - self.identity.as_ref().map(|s| s.as_ref()) - } - - fn remember(&mut self, value: String) { - self.changed = true; - self.identity = Some(value); - } - - fn forget(&mut self) { - self.changed = true; - self.identity = None; - } - - fn write(&mut self, mut resp: HttpResponse) -> Result { - if self.changed { - let _ = self.inner.set_cookie(&mut resp, self.identity.take()); - } - Ok(Response::Done(resp)) - } -} - -struct CookieIdentityInner { - key: Key, - name: String, - path: String, - domain: Option, - secure: bool, - max_age: Option, - same_site: Option, -} - -impl CookieIdentityInner { - fn new(key: &[u8]) -> CookieIdentityInner { - CookieIdentityInner { - key: Key::from_master(key), - name: "actix-identity".to_owned(), - path: "/".to_owned(), - domain: None, - secure: true, - max_age: None, - same_site: None, - } - } - - fn set_cookie(&self, resp: &mut HttpResponse, id: Option) -> Result<()> { - let some = id.is_some(); - { - let id = id.unwrap_or_else(String::new); - let mut cookie = Cookie::new(self.name.clone(), id); - cookie.set_path(self.path.clone()); - cookie.set_secure(self.secure); - cookie.set_http_only(true); - - if let Some(ref domain) = self.domain { - cookie.set_domain(domain.clone()); - } - - if let Some(max_age) = self.max_age { - cookie.set_max_age(max_age); - } - - if let Some(same_site) = self.same_site { - cookie.set_same_site(same_site); - } - - let mut jar = CookieJar::new(); - if some { - jar.private(&self.key).add(cookie); - } else { - jar.add_original(cookie.clone()); - jar.private(&self.key).remove(cookie); - } - - for cookie in jar.delta() { - let val = HeaderValue::from_str(&cookie.to_string())?; - resp.headers_mut().append(header::SET_COOKIE, val); - } - } - - Ok(()) - } - - fn load(&self, req: &HttpRequest) -> Option { - if let Ok(cookies) = req.cookies() { - for cookie in cookies.iter() { - if cookie.name() == self.name { - let mut jar = CookieJar::new(); - jar.add_original(cookie.clone()); - - let cookie_opt = jar.private(&self.key).get(&self.name); - if let Some(cookie) = cookie_opt { - return Some(cookie.value().into()); - } - } - } - } - None - } -} - -/// Use cookies for request identity storage. -/// -/// The constructors take a key as an argument. -/// This is the private key for cookie - when this value is changed, -/// all identities are lost. The constructors will panic if the key is less -/// than 32 bytes in length. -/// -/// # Example -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; -/// use actix_web::App; -/// -/// fn main() { -/// let app = App::new().middleware(IdentityService::new( -/// // <- create identity middleware -/// CookieIdentityPolicy::new(&[0; 32]) // <- construct cookie policy -/// .domain("www.rust-lang.org") -/// .name("actix_auth") -/// .path("/") -/// .secure(true), -/// )); -/// } -/// ``` -pub struct CookieIdentityPolicy(Rc); - -impl CookieIdentityPolicy { - /// Construct new `CookieIdentityPolicy` instance. - /// - /// Panics if key length is less than 32 bytes. - pub fn new(key: &[u8]) -> CookieIdentityPolicy { - CookieIdentityPolicy(Rc::new(CookieIdentityInner::new(key))) - } - - /// Sets the `path` field in the session cookie being built. - pub fn path>(mut self, value: S) -> CookieIdentityPolicy { - Rc::get_mut(&mut self.0).unwrap().path = value.into(); - self - } - - /// Sets the `name` field in the session cookie being built. - pub fn name>(mut self, value: S) -> CookieIdentityPolicy { - Rc::get_mut(&mut self.0).unwrap().name = value.into(); - self - } - - /// Sets the `domain` field in the session cookie being built. - pub fn domain>(mut self, value: S) -> CookieIdentityPolicy { - Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into()); - self - } - - /// Sets the `secure` field in the session cookie being built. - /// - /// If the `secure` field is set, a cookie will only be transmitted when the - /// connection is secure - i.e. `https` - pub fn secure(mut self, value: bool) -> CookieIdentityPolicy { - Rc::get_mut(&mut self.0).unwrap().secure = value; - self - } - - /// Sets the `max-age` field in the session cookie being built. - pub fn max_age(mut self, value: Duration) -> CookieIdentityPolicy { - Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); - self - } - - /// Sets the `same_site` field in the session cookie being built. - pub fn same_site(mut self, same_site: SameSite) -> Self { - Rc::get_mut(&mut self.0).unwrap().same_site = Some(same_site); - self - } -} - -impl IdentityPolicy for CookieIdentityPolicy { - type Identity = CookieIdentity; - type Future = FutureResult; - - fn from_request(&self, req: &HttpRequest) -> Self::Future { - let identity = self.0.load(req); - FutOk(CookieIdentity { - identity, - changed: false, - inner: Rc::clone(&self.0), - }) - } -} diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs deleted file mode 100644 index b7bb1bb8..00000000 --- a/src/middleware/logger.rs +++ /dev/null @@ -1,384 +0,0 @@ -//! Request logging middleware -use std::collections::HashSet; -use std::env; -use std::fmt::{self, Display, Formatter}; - -use regex::Regex; -use time; - -use error::Result; -use httpmessage::HttpMessage; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Finished, Middleware, Started}; - -/// `Middleware` for logging request and response info to the terminal. -/// -/// `Logger` middleware uses standard log crate to log information. You should -/// enable logger for `actix_web` package to see access log. -/// ([`env_logger`](https://docs.rs/env_logger/*/env_logger/) or similar) -/// -/// ## Usage -/// -/// Create `Logger` middleware with the specified `format`. -/// Default `Logger` could be created with `default` method, it uses the -/// default format: -/// -/// ```ignore -/// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T -/// ``` -/// ```rust -/// # extern crate actix_web; -/// extern crate env_logger; -/// use actix_web::middleware::Logger; -/// use actix_web::App; -/// -/// fn main() { -/// std::env::set_var("RUST_LOG", "actix_web=info"); -/// env_logger::init(); -/// -/// let app = App::new() -/// .middleware(Logger::default()) -/// .middleware(Logger::new("%a %{User-Agent}i")) -/// .finish(); -/// } -/// ``` -/// -/// ## Format -/// -/// `%%` The percent sign -/// -/// `%a` Remote IP-address (IP-address of proxy if using reverse proxy) -/// -/// `%t` Time when the request was started to process -/// -/// `%r` First line of request -/// -/// `%s` Response status code -/// -/// `%b` Size of response in bytes, including HTTP headers -/// -/// `%T` Time taken to serve the request, in seconds with floating fraction in -/// .06f format -/// -/// `%D` Time taken to serve the request, in milliseconds -/// -/// `%{FOO}i` request.headers['FOO'] -/// -/// `%{FOO}o` response.headers['FOO'] -/// -/// `%{FOO}e` os.environ['FOO'] -/// -pub struct Logger { - format: Format, - exclude: HashSet, -} - -impl Logger { - /// Create `Logger` middleware with the specified `format`. - pub fn new(format: &str) -> Logger { - Logger { - format: Format::new(format), - exclude: HashSet::new(), - } - } - - /// Ignore and do not log access info for specified path. - pub fn exclude>(mut self, path: T) -> Self { - self.exclude.insert(path.into()); - self - } -} - -impl Default for Logger { - /// Create `Logger` middleware with format: - /// - /// ```ignore - /// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T - /// ``` - fn default() -> Logger { - Logger { - format: Format::default(), - exclude: HashSet::new(), - } - } -} - -struct StartTime(time::Tm); - -impl Logger { - fn log(&self, req: &HttpRequest, resp: &HttpResponse) { - if let Some(entry_time) = req.extensions().get::() { - let render = |fmt: &mut Formatter| { - for unit in &self.format.0 { - unit.render(fmt, req, resp, entry_time.0)?; - } - Ok(()) - }; - info!("{}", FormatDisplay(&render)); - } - } -} - -impl Middleware for Logger { - fn start(&self, req: &HttpRequest) -> Result { - if !self.exclude.contains(req.path()) { - req.extensions_mut().insert(StartTime(time::now())); - } - Ok(Started::Done) - } - - fn finish(&self, req: &HttpRequest, resp: &HttpResponse) -> Finished { - self.log(req, resp); - Finished::Done - } -} - -/// A formatting style for the `Logger`, consisting of multiple -/// `FormatText`s concatenated into one line. -#[derive(Clone)] -#[doc(hidden)] -struct Format(Vec); - -impl Default for Format { - /// Return the default formatting style for the `Logger`: - fn default() -> Format { - Format::new(r#"%a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T"#) - } -} - -impl Format { - /// Create a `Format` from a format string. - /// - /// Returns `None` if the format string syntax is incorrect. - pub fn new(s: &str) -> Format { - trace!("Access log format: {}", s); - let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([ioe])|[atPrsbTD]?)").unwrap(); - - let mut idx = 0; - let mut results = Vec::new(); - for cap in fmt.captures_iter(s) { - let m = cap.get(0).unwrap(); - let pos = m.start(); - if idx != pos { - results.push(FormatText::Str(s[idx..pos].to_owned())); - } - idx = m.end(); - - 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()), - "e" => FormatText::EnvironHeader(key.as_str().to_owned()), - _ => unreachable!(), - }) - } else { - let m = cap.get(1).unwrap(); - results.push(match m.as_str() { - "%" => FormatText::Percent, - "a" => FormatText::RemoteAddr, - "t" => FormatText::RequestTime, - "r" => FormatText::RequestLine, - "s" => FormatText::ResponseStatus, - "b" => FormatText::ResponseSize, - "T" => FormatText::Time, - "D" => FormatText::TimeMillis, - _ => FormatText::Str(m.as_str().to_owned()), - }); - } - } - if idx != s.len() { - results.push(FormatText::Str(s[idx..].to_owned())); - } - - Format(results) - } -} - -/// A string of text to be logged. This is either one of the data -/// fields supported by the `Logger`, or a custom `String`. -#[doc(hidden)] -#[derive(Debug, Clone)] -pub enum FormatText { - Str(String), - Percent, - RequestLine, - RequestTime, - ResponseStatus, - ResponseSize, - Time, - TimeMillis, - RemoteAddr, - RequestHeader(String), - ResponseHeader(String), - EnvironHeader(String), -} - -impl FormatText { - fn render( - &self, fmt: &mut Formatter, req: &HttpRequest, resp: &HttpResponse, - entry_time: time::Tm, - ) -> Result<(), fmt::Error> { - match *self { - FormatText::Str(ref string) => fmt.write_str(string), - FormatText::Percent => "%".fmt(fmt), - FormatText::RequestLine => { - if req.query_string().is_empty() { - fmt.write_fmt(format_args!( - "{} {} {:?}", - req.method(), - req.path(), - req.version() - )) - } else { - fmt.write_fmt(format_args!( - "{} {}?{} {:?}", - req.method(), - req.path(), - req.query_string(), - req.version() - )) - } - } - FormatText::ResponseStatus => resp.status().as_u16().fmt(fmt), - FormatText::ResponseSize => resp.response_size().fmt(fmt), - FormatText::Time => { - let rt = time::now() - entry_time; - let rt = (rt.num_nanoseconds().unwrap_or(0) as f64) / 1_000_000_000.0; - fmt.write_fmt(format_args!("{:.6}", rt)) - } - FormatText::TimeMillis => { - let rt = time::now() - entry_time; - 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::RequestTime => entry_time - .strftime("[%d/%b/%Y:%H:%M:%S %z]") - .unwrap() - .fmt(fmt), - FormatText::RequestHeader(ref name) => { - let s = if let Some(val) = req.headers().get(name) { - if let Ok(s) = val.to_str() { - s - } else { - "-" - } - } else { - "-" - }; - fmt.write_fmt(format_args!("{}", s)) - } - FormatText::ResponseHeader(ref name) => { - let s = if let Some(val) = resp.headers().get(name) { - if let Ok(s) = val.to_str() { - s - } else { - "-" - } - } else { - "-" - }; - fmt.write_fmt(format_args!("{}", s)) - } - FormatText::EnvironHeader(ref name) => { - if let Ok(val) = env::var(name) { - fmt.write_fmt(format_args!("{}", val)) - } else { - "-".fmt(fmt) - } - } - } - } -} - -pub(crate) struct FormatDisplay<'a>(&'a Fn(&mut Formatter) -> Result<(), fmt::Error>); - -impl<'a> fmt::Display for FormatDisplay<'a> { - fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> { - (self.0)(fmt) - } -} - -#[cfg(test)] -mod tests { - use time; - - use super::*; - use http::{header, StatusCode}; - use test::TestRequest; - - #[test] - fn test_logger() { - let logger = Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test"); - - let req = TestRequest::with_header( - header::USER_AGENT, - header::HeaderValue::from_static("ACTIX-WEB"), - ).finish(); - let resp = HttpResponse::build(StatusCode::OK) - .header("X-Test", "ttt") - .force_close() - .finish(); - - match logger.start(&req) { - Ok(Started::Done) => (), - _ => panic!(), - }; - match logger.finish(&req, &resp) { - Finished::Done => (), - _ => panic!(), - } - let entry_time = time::now(); - let render = |fmt: &mut Formatter| { - for unit in &logger.format.0 { - unit.render(fmt, &req, &resp, entry_time)?; - } - Ok(()) - }; - let s = format!("{}", FormatDisplay(&render)); - assert!(s.contains("ACTIX-WEB ttt")); - } - - #[test] - fn test_default_format() { - let format = Format::default(); - - let req = TestRequest::with_header( - header::USER_AGENT, - header::HeaderValue::from_static("ACTIX-WEB"), - ).finish(); - let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); - let entry_time = time::now(); - - let render = |fmt: &mut Formatter| { - for unit in &format.0 { - unit.render(fmt, &req, &resp, entry_time)?; - } - Ok(()) - }; - let s = format!("{}", FormatDisplay(&render)); - assert!(s.contains("GET / HTTP/1.1")); - assert!(s.contains("200 0")); - assert!(s.contains("ACTIX-WEB")); - - let req = TestRequest::with_uri("/?test").finish(); - let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); - let entry_time = time::now(); - - let render = |fmt: &mut Formatter| { - for unit in &format.0 { - unit.render(fmt, &req, &resp, entry_time)?; - } - Ok(()) - }; - let s = format!("{}", FormatDisplay(&render)); - assert!(s.contains("GET /?test HTTP/1.1")); - } -} diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index c69dbb3e..a8b4b3c6 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -1,68 +1,65 @@ -//! Middlewares -use futures::Future; +use std::marker::PhantomData; -use error::{Error, Result}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; +use actix_service::{NewTransform, Service, Transform}; +use futures::future::{ok, FutureResult}; -mod logger; +#[cfg(any(feature = "brotli", feature = "flate2"))] +mod compress; +#[cfg(any(feature = "brotli", feature = "flate2"))] +pub use self::compress::Compress; -pub mod cors; -pub mod csrf; mod defaultheaders; -mod errhandlers; -#[cfg(feature = "session")] -pub mod identity; -#[cfg(feature = "session")] -pub mod session; pub use self::defaultheaders::DefaultHeaders; -pub use self::errhandlers::ErrorHandlers; -pub use self::logger::Logger; -/// Middleware start result -pub enum Started { - /// Middleware is completed, continue to next middleware - Done, - /// New http response got generated. If middleware generates response - /// handler execution halts. - Response(HttpResponse), - /// Execution completed, runs future to completion. - Future(Box, Error = Error>>), +/// Helper for middleware service factory +pub struct MiddlewareFactory +where + T: Transform + Clone, + S: Service, +{ + tr: T, + _t: PhantomData, } -/// Middleware execution result -pub enum Response { - /// New http response got generated - Done(HttpResponse), - /// Result is a future that resolves to a new http response - Future(Box>), -} - -/// Middleware finish result -pub enum Finished { - /// Execution completed - Done, - /// Execution completed, but run future to completion - Future(Box>), -} - -/// Middleware definition -#[allow(unused_variables)] -pub trait Middleware: 'static { - /// Method is called when request is ready. It may return - /// future, which should resolve before next middleware get called. - fn start(&self, req: &HttpRequest) -> Result { - Ok(Started::Done) - } - - /// Method is called when handler returns response, - /// but before sending http message to peer. - fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { - Ok(Response::Done(resp)) - } - - /// Method is called after body stream get sent to peer. - fn finish(&self, req: &HttpRequest, resp: &HttpResponse) -> Finished { - Finished::Done +impl MiddlewareFactory +where + T: Transform + Clone, + S: Service, +{ + pub fn new(tr: T) -> Self { + MiddlewareFactory { + tr, + _t: PhantomData, + } + } +} + +impl Clone for MiddlewareFactory +where + T: Transform + Clone, + S: Service, +{ + fn clone(&self) -> Self { + Self { + tr: self.tr.clone(), + _t: PhantomData, + } + } +} + +impl NewTransform for MiddlewareFactory +where + T: Transform + Clone, + S: Service, +{ + type Request = T::Request; + type Response = T::Response; + type Error = T::Error; + type Transform = T; + type InitError = (); + type Future = FutureResult; + + fn new_transform(&self) -> Self::Future { + ok(self.tr.clone()) } } diff --git a/src/middleware/session.rs b/src/middleware/session.rs deleted file mode 100644 index 0271a13f..00000000 --- a/src/middleware/session.rs +++ /dev/null @@ -1,618 +0,0 @@ -//! User sessions. -//! -//! Actix provides a general solution for session management. The -//! [**SessionStorage**](struct.SessionStorage.html) -//! middleware can be used with different backend types to store session -//! data in different backends. -//! -//! By default, only cookie session backend is implemented. Other -//! backend implementations can be added. -//! -//! [**CookieSessionBackend**](struct.CookieSessionBackend.html) -//! uses cookies as session storage. `CookieSessionBackend` creates sessions -//! which are limited to storing fewer than 4000 bytes of data, as the payload -//! must fit into a single cookie. An internal server error is generated if a -//! session contains more than 4000 bytes. -//! -//! A cookie may have a security policy of *signed* or *private*. Each has -//! a respective `CookieSessionBackend` constructor. -//! -//! A *signed* cookie may be viewed but not modified by the client. A *private* -//! cookie may neither be viewed nor modified by the client. -//! -//! The constructors take a key as an argument. This is the private key -//! for cookie session - when this value is changed, all session data is lost. -//! -//! In general, you create a `SessionStorage` middleware and initialize it -//! with specific backend implementation, such as a `CookieSessionBackend`. -//! To access session data, -//! [*HttpRequest::session()*](trait.RequestSession.html#tymethod.session) -//! must be used. This method returns a -//! [*Session*](struct.Session.html) object, which allows us to get or set -//! session data. -//! -//! ```rust -//! # extern crate actix_web; -//! # extern crate actix; -//! use actix_web::{server, App, HttpRequest, Result}; -//! use actix_web::middleware::session::{RequestSession, SessionStorage, CookieSessionBackend}; -//! -//! fn index(req: HttpRequest) -> Result<&'static str> { -//! // access session data -//! if let Some(count) = req.session().get::("counter")? { -//! println!("SESSION value: {}", count); -//! req.session().set("counter", count+1)?; -//! } else { -//! req.session().set("counter", 1)?; -//! } -//! -//! Ok("Welcome!") -//! } -//! -//! fn main() { -//! actix::System::run(|| { -//! server::new( -//! || App::new().middleware( -//! SessionStorage::new( // <- create session middleware -//! CookieSessionBackend::signed(&[0; 32]) // <- create signed cookie session backend -//! .secure(false) -//! ))) -//! .bind("127.0.0.1:59880").unwrap() -//! .start(); -//! # actix::System::current().stop(); -//! }); -//! } -//! ``` -use std::cell::RefCell; -use std::collections::HashMap; -use std::marker::PhantomData; -use std::rc::Rc; -use std::sync::Arc; - -use cookie::{Cookie, CookieJar, Key, SameSite}; -use futures::future::{err as FutErr, ok as FutOk, FutureResult}; -use futures::Future; -use http::header::{self, HeaderValue}; -use serde::de::DeserializeOwned; -use serde::Serialize; -use serde_json; -use serde_json::error::Error as JsonError; -use time::Duration; - -use error::{Error, ResponseError, Result}; -use handler::FromRequest; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response, Started}; - -/// The helper trait to obtain your session data from a request. -/// -/// ```rust -/// use actix_web::middleware::session::RequestSession; -/// use actix_web::*; -/// -/// fn index(mut req: HttpRequest) -> Result<&'static str> { -/// // access session data -/// if let Some(count) = req.session().get::("counter")? { -/// req.session().set("counter", count + 1)?; -/// } else { -/// req.session().set("counter", 1)?; -/// } -/// -/// Ok("Welcome!") -/// } -/// # fn main() {} -/// ``` -pub trait RequestSession { - /// Get the session from the request - fn session(&self) -> Session; -} - -impl RequestSession for HttpRequest { - fn session(&self) -> Session { - if let Some(s_impl) = self.extensions().get::>() { - return Session(SessionInner::Session(Arc::clone(&s_impl))); - } - Session(SessionInner::None) - } -} - -/// The high-level interface you use to modify session data. -/// -/// Session object could be obtained with -/// [`RequestSession::session`](trait.RequestSession.html#tymethod.session) -/// method. `RequestSession` trait is implemented for `HttpRequest`. -/// -/// ```rust -/// use actix_web::middleware::session::RequestSession; -/// use actix_web::*; -/// -/// fn index(mut req: HttpRequest) -> Result<&'static str> { -/// // access session data -/// if let Some(count) = req.session().get::("counter")? { -/// req.session().set("counter", count + 1)?; -/// } else { -/// req.session().set("counter", 1)?; -/// } -/// -/// Ok("Welcome!") -/// } -/// # fn main() {} -/// ``` -pub struct Session(SessionInner); - -enum SessionInner { - Session(Arc), - None, -} - -impl Session { - /// Get a `value` from the session. - pub fn get(&self, key: &str) -> Result> { - match self.0 { - SessionInner::Session(ref sess) => { - if let Some(s) = sess.as_ref().0.borrow().get(key) { - Ok(Some(serde_json::from_str(s)?)) - } else { - Ok(None) - } - } - SessionInner::None => Ok(None), - } - } - - /// Set a `value` from the session. - pub fn set(&self, key: &str, value: T) -> Result<()> { - match self.0 { - SessionInner::Session(ref sess) => { - sess.as_ref() - .0 - .borrow_mut() - .set(key, serde_json::to_string(&value)?); - Ok(()) - } - SessionInner::None => Ok(()), - } - } - - /// Remove value from the session. - pub fn remove(&self, key: &str) { - match self.0 { - SessionInner::Session(ref sess) => sess.as_ref().0.borrow_mut().remove(key), - SessionInner::None => (), - } - } - - /// Clear the session. - pub fn clear(&self) { - match self.0 { - SessionInner::Session(ref sess) => sess.as_ref().0.borrow_mut().clear(), - SessionInner::None => (), - } - } -} - -/// Extractor implementation for Session type. -/// -/// ```rust -/// # use actix_web::*; -/// use actix_web::middleware::session::Session; -/// -/// fn index(session: Session) -> Result<&'static str> { -/// // access session data -/// if let Some(count) = session.get::("counter")? { -/// session.set("counter", count + 1)?; -/// } else { -/// session.set("counter", 1)?; -/// } -/// -/// Ok("Welcome!") -/// } -/// # fn main() {} -/// ``` -impl FromRequest for Session { - type Config = (); - type Result = Session; - - #[inline] - fn from_request(req: &HttpRequest, _: &Self::Config) -> Self::Result { - req.session() - } -} - -struct SessionImplCell(RefCell>); - -/// Session storage middleware -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::session::{CookieSessionBackend, SessionStorage}; -/// use actix_web::App; -/// -/// fn main() { -/// let app = App::new().middleware(SessionStorage::new( -/// // <- create session middleware -/// CookieSessionBackend::signed(&[0; 32]) // <- create cookie session backend -/// .secure(false), -/// )); -/// } -/// ``` -pub struct SessionStorage(T, PhantomData); - -impl> SessionStorage { - /// Create session storage - pub fn new(backend: T) -> SessionStorage { - SessionStorage(backend, PhantomData) - } -} - -impl> Middleware for SessionStorage { - fn start(&self, req: &HttpRequest) -> Result { - let mut req = req.clone(); - - let fut = self.0.from_request(&mut req).then(move |res| match res { - Ok(sess) => { - req.extensions_mut() - .insert(Arc::new(SessionImplCell(RefCell::new(Box::new(sess))))); - FutOk(None) - } - Err(err) => FutErr(err), - }); - Ok(Started::Future(Box::new(fut))) - } - - fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { - if let Some(s_box) = req.extensions().get::>() { - s_box.0.borrow_mut().write(resp) - } else { - Ok(Response::Done(resp)) - } - } -} - -/// A simple key-value storage interface that is internally used by `Session`. -pub trait SessionImpl: 'static { - /// Get session value by key - fn get(&self, key: &str) -> Option<&str>; - - /// Set session value - fn set(&mut self, key: &str, value: String); - - /// Remove specific key from session - fn remove(&mut self, key: &str); - - /// Remove all values from session - fn clear(&mut self); - - /// Write session to storage backend. - fn write(&self, resp: HttpResponse) -> Result; -} - -/// Session's storage backend trait definition. -pub trait SessionBackend: Sized + 'static { - /// Session item - type Session: SessionImpl; - /// Future that reads session - type ReadFuture: Future; - - /// Parse the session from request and load data from a storage backend. - fn from_request(&self, request: &mut HttpRequest) -> Self::ReadFuture; -} - -/// Session that uses signed cookies as session storage -pub struct CookieSession { - changed: bool, - state: HashMap, - inner: Rc, -} - -/// Errors that can occur during handling cookie session -#[derive(Fail, Debug)] -pub enum CookieSessionError { - /// Size of the serialized session is greater than 4000 bytes. - #[fail(display = "Size of the serialized session is greater than 4000 bytes.")] - Overflow, - /// Fail to serialize session. - #[fail(display = "Fail to serialize session")] - Serialize(JsonError), -} - -impl ResponseError for CookieSessionError {} - -impl SessionImpl for CookieSession { - fn get(&self, key: &str) -> Option<&str> { - if let Some(s) = self.state.get(key) { - Some(s) - } else { - None - } - } - - fn set(&mut self, key: &str, value: String) { - self.changed = true; - self.state.insert(key.to_owned(), value); - } - - fn remove(&mut self, key: &str) { - self.changed = true; - self.state.remove(key); - } - - fn clear(&mut self) { - self.changed = true; - self.state.clear() - } - - fn write(&self, mut resp: HttpResponse) -> Result { - if self.changed { - let _ = self.inner.set_cookie(&mut resp, &self.state); - } - Ok(Response::Done(resp)) - } -} - -enum CookieSecurity { - Signed, - Private, -} - -struct CookieSessionInner { - key: Key, - security: CookieSecurity, - name: String, - path: String, - domain: Option, - secure: bool, - http_only: bool, - max_age: Option, - same_site: Option, -} - -impl CookieSessionInner { - fn new(key: &[u8], security: CookieSecurity) -> CookieSessionInner { - CookieSessionInner { - security, - key: Key::from_master(key), - name: "actix-session".to_owned(), - path: "/".to_owned(), - domain: None, - secure: true, - http_only: true, - max_age: None, - same_site: None, - } - } - - fn set_cookie( - &self, resp: &mut HttpResponse, state: &HashMap, - ) -> Result<()> { - let value = - serde_json::to_string(&state).map_err(CookieSessionError::Serialize)?; - if value.len() > 4064 { - return Err(CookieSessionError::Overflow.into()); - } - - let mut cookie = Cookie::new(self.name.clone(), value); - cookie.set_path(self.path.clone()); - cookie.set_secure(self.secure); - cookie.set_http_only(self.http_only); - - if let Some(ref domain) = self.domain { - cookie.set_domain(domain.clone()); - } - - if let Some(max_age) = self.max_age { - cookie.set_max_age(max_age); - } - - if let Some(same_site) = self.same_site { - cookie.set_same_site(same_site); - } - - let mut jar = CookieJar::new(); - - match self.security { - CookieSecurity::Signed => jar.signed(&self.key).add(cookie), - CookieSecurity::Private => jar.private(&self.key).add(cookie), - } - - for cookie in jar.delta() { - let val = HeaderValue::from_str(&cookie.encoded().to_string())?; - resp.headers_mut().append(header::SET_COOKIE, val); - } - - Ok(()) - } - - fn load(&self, req: &mut HttpRequest) -> HashMap { - if let Ok(cookies) = req.cookies() { - for cookie in cookies.iter() { - if cookie.name() == self.name { - let mut jar = CookieJar::new(); - jar.add_original(cookie.clone()); - - let cookie_opt = match self.security { - CookieSecurity::Signed => jar.signed(&self.key).get(&self.name), - CookieSecurity::Private => { - jar.private(&self.key).get(&self.name) - } - }; - if let Some(cookie) = cookie_opt { - if let Ok(val) = serde_json::from_str(cookie.value()) { - return val; - } - } - } - } - } - HashMap::new() - } -} - -/// Use cookies for session storage. -/// -/// `CookieSessionBackend` creates sessions which are limited to storing -/// fewer than 4000 bytes of data (as the payload must fit into a single -/// cookie). An Internal Server Error is generated if the session contains more -/// than 4000 bytes. -/// -/// A cookie may have a security policy of *signed* or *private*. Each has a -/// respective `CookieSessionBackend` constructor. -/// -/// A *signed* cookie is stored on the client as plaintext alongside -/// a signature such that the cookie may be viewed but not modified by the -/// client. -/// -/// A *private* cookie is stored on the client as encrypted text -/// such that it may neither be viewed nor modified by the client. -/// -/// The constructors take a key as an argument. -/// This is the private key for cookie session - when this value is changed, -/// all session data is lost. The constructors will panic if the key is less -/// than 32 bytes in length. -/// -/// The backend relies on `cookie` crate to create and read cookies. -/// By default all cookies are percent encoded, but certain symbols may -/// cause troubles when reading cookie, if they are not properly percent encoded. -/// -/// # Example -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::session::CookieSessionBackend; -/// -/// # fn main() { -/// let backend: CookieSessionBackend = CookieSessionBackend::signed(&[0; 32]) -/// .domain("www.rust-lang.org") -/// .name("actix_session") -/// .path("/") -/// .secure(true); -/// # } -/// ``` -pub struct CookieSessionBackend(Rc); - -impl CookieSessionBackend { - /// Construct new *signed* `CookieSessionBackend` instance. - /// - /// Panics if key length is less than 32 bytes. - pub fn signed(key: &[u8]) -> CookieSessionBackend { - CookieSessionBackend(Rc::new(CookieSessionInner::new( - key, - CookieSecurity::Signed, - ))) - } - - /// Construct new *private* `CookieSessionBackend` instance. - /// - /// Panics if key length is less than 32 bytes. - pub fn private(key: &[u8]) -> CookieSessionBackend { - CookieSessionBackend(Rc::new(CookieSessionInner::new( - key, - CookieSecurity::Private, - ))) - } - - /// Sets the `path` field in the session cookie being built. - pub fn path>(mut self, value: S) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().path = value.into(); - self - } - - /// Sets the `name` field in the session cookie being built. - pub fn name>(mut self, value: S) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().name = value.into(); - self - } - - /// Sets the `domain` field in the session cookie being built. - pub fn domain>(mut self, value: S) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into()); - self - } - - /// Sets the `secure` field in the session cookie being built. - /// - /// If the `secure` field is set, a cookie will only be transmitted when the - /// connection is secure - i.e. `https` - pub fn secure(mut self, value: bool) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().secure = value; - self - } - - /// Sets the `http_only` field in the session cookie being built. - pub fn http_only(mut self, value: bool) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().http_only = value; - self - } - - /// Sets the `same_site` field in the session cookie being built. - pub fn same_site(mut self, value: SameSite) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().same_site = Some(value); - self - } - - /// Sets the `max-age` field in the session cookie being built. - pub fn max_age(mut self, value: Duration) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); - self - } -} - -impl SessionBackend for CookieSessionBackend { - type Session = CookieSession; - type ReadFuture = FutureResult; - - fn from_request(&self, req: &mut HttpRequest) -> Self::ReadFuture { - let state = self.0.load(req); - FutOk(CookieSession { - changed: false, - inner: Rc::clone(&self.0), - state, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use application::App; - use test; - - #[test] - fn cookie_session() { - let mut srv = test::TestServer::with_factory(|| { - App::new() - .middleware(SessionStorage::new( - CookieSessionBackend::signed(&[0; 32]).secure(false), - )).resource("/", |r| { - r.f(|req| { - let _ = req.session().set("counter", 100); - "test" - }) - }) - }); - - let request = srv.get().uri(srv.url("/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.cookie("actix-session").is_some()); - } - - #[test] - fn cookie_session_extractor() { - let mut srv = test::TestServer::with_factory(|| { - App::new() - .middleware(SessionStorage::new( - CookieSessionBackend::signed(&[0; 32]).secure(false), - )).resource("/", |r| { - r.with(|ses: Session| { - let _ = ses.set("counter", 100); - "test" - }) - }) - }); - - let request = srv.get().uri(srv.url("/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.cookie("actix-session").is_some()); - } -} diff --git a/src/multipart.rs b/src/multipart.rs deleted file mode 100644 index 862f60ec..00000000 --- a/src/multipart.rs +++ /dev/null @@ -1,815 +0,0 @@ -//! Multipart requests support -use std::cell::{RefCell, UnsafeCell}; -use std::marker::PhantomData; -use std::rc::Rc; -use std::{cmp, fmt}; - -use bytes::Bytes; -use futures::task::{current as current_task, Task}; -use futures::{Async, Poll, Stream}; -use http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue}; -use http::HttpTryFrom; -use httparse; -use mime; - -use error::{MultipartError, ParseError, PayloadError}; -use payload::PayloadBuffer; - -const MAX_HEADERS: usize = 32; - -/// The server-side implementation of `multipart/form-data` requests. -/// -/// This will parse the incoming stream into `MultipartItem` instances via its -/// Stream implementation. -/// `MultipartItem::Field` contains multipart field. `MultipartItem::Multipart` -/// is used for nested multipart streams. -pub struct Multipart { - safety: Safety, - error: Option, - inner: Option>>>, -} - -/// -pub enum MultipartItem { - /// Multipart field - Field(Field), - /// Nested multipart stream - Nested(Multipart), -} - -enum InnerMultipartItem { - None, - Field(Rc>>), - Multipart(Rc>>), -} - -#[derive(PartialEq, Debug)] -enum InnerState { - /// Stream eof - Eof, - /// Skip data until first boundary - FirstBoundary, - /// Reading boundary - Boundary, - /// Reading Headers, - Headers, -} - -struct InnerMultipart { - payload: PayloadRef, - boundary: String, - state: InnerState, - item: InnerMultipartItem, -} - -impl Multipart<()> { - /// Extract boundary info from headers. - pub fn boundary(headers: &HeaderMap) -> Result { - 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) { - Ok(boundary.as_str().to_owned()) - } else { - Err(MultipartError::Boundary) - } - } else { - Err(MultipartError::ParseContentType) - } - } else { - Err(MultipartError::ParseContentType) - } - } else { - Err(MultipartError::NoContentType) - } - } -} - -impl Multipart -where - S: Stream, -{ - /// Create multipart instance for boundary. - pub fn new(boundary: Result, stream: S) -> Multipart { - match boundary { - Ok(boundary) => Multipart { - error: None, - safety: Safety::new(), - inner: Some(Rc::new(RefCell::new(InnerMultipart { - boundary, - payload: PayloadRef::new(PayloadBuffer::new(stream)), - state: InnerState::FirstBoundary, - item: InnerMultipartItem::None, - }))), - }, - Err(err) => Multipart { - error: Some(err), - safety: Safety::new(), - inner: None, - }, - } - } -} - -impl Stream for Multipart -where - S: Stream, -{ - type Item = MultipartItem; - 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) - } else { - Ok(Async::NotReady) - } - } -} - -impl InnerMultipart -where - S: Stream, -{ - 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)) => { - let mut hdrs = [httparse::EMPTY_HEADER; MAX_HEADERS]; - match httparse::parse_headers(&bytes, &mut hdrs) { - Ok(httparse::Status::Complete((_, hdrs))) => { - // convert headers - let mut headers = HeaderMap::with_capacity(hdrs.len()); - for h in hdrs { - if let Ok(name) = HeaderName::try_from(h.name) { - if let Ok(value) = HeaderValue::try_from(h.value) { - headers.append(name, value); - } else { - return Err(ParseError::Header.into()); - } - } else { - return Err(ParseError::Header.into()); - } - } - Ok(Async::Ready(headers)) - } - Ok(httparse::Status::Partial) => Err(ParseError::Header.into()), - Err(err) => Err(ParseError::from(err).into()), - } - } - } - } - - fn read_boundary( - payload: &mut PayloadBuffer, boundary: &str, - ) -> Poll { - // TODO: need to read epilogue - match payload.readline()? { - Async::NotReady => Ok(Async::NotReady), - Async::Ready(None) => Err(MultipartError::Incomplete), - Async::Ready(Some(chunk)) => { - if chunk.len() == boundary.len() + 4 - && &chunk[..2] == b"--" - && &chunk[2..boundary.len() + 2] == boundary.as_bytes() - { - Ok(Async::Ready(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)) - } else { - Err(MultipartError::Boundary) - } - } - } - } - - fn skip_until_boundary( - payload: &mut PayloadBuffer, boundary: &str, - ) -> Poll { - let mut eof = false; - loop { - match payload.readline()? { - Async::Ready(Some(chunk)) => { - if chunk.is_empty() { - //ValueError("Could not find starting boundary %r" - //% (self._boundary)) - } - if chunk.len() < boundary.len() { - continue; - } - if &chunk[..2] == b"--" - && &chunk[2..chunk.len() - 2] == boundary.as_bytes() - { - break; - } else { - if chunk.len() < boundary.len() + 2 { - continue; - } - let b: &[u8] = boundary.as_ref(); - if &chunk[..boundary.len()] == b - && &chunk[boundary.len()..boundary.len() + 2] == b"--" - { - eof = true; - break; - } - } - } - Async::NotReady => return Ok(Async::NotReady), - Async::Ready(None) => return Err(MultipartError::Incomplete), - } - } - Ok(Async::Ready(eof)) - } - - fn poll( - &mut self, safety: &Safety, - ) -> Poll>, MultipartError> { - if self.state == InnerState::Eof { - Ok(Async::Ready(None)) - } else { - // release field - loop { - // Nested multipart streams of fields has to be consumed - // before switching to next - if safety.current() { - let stop = match self.item { - InnerMultipartItem::Field(ref mut field) => { - match field.borrow_mut().poll(safety)? { - Async::NotReady => return Ok(Async::NotReady), - Async::Ready(Some(_)) => continue, - 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, - }; - if stop { - self.item = InnerMultipartItem::None; - } - if let InnerMultipartItem::None = self.item { - break; - } - } - } - - let headers = if let Some(payload) = self.payload.get_mut(safety) { - match self.state { - // read until first boundary - InnerState::FirstBoundary => { - match InnerMultipart::skip_until_boundary( - payload, - &self.boundary, - )? { - Async::Ready(eof) => { - if eof { - self.state = InnerState::Eof; - return Ok(Async::Ready(None)); - } else { - self.state = InnerState::Headers; - } - } - Async::NotReady => return Ok(Async::NotReady), - } - } - // read boundary - InnerState::Boundary => { - match InnerMultipart::read_boundary(payload, &self.boundary)? { - Async::NotReady => return Ok(Async::NotReady), - Async::Ready(eof) => { - if eof { - self.state = InnerState::Eof; - return Ok(Async::Ready(None)); - } else { - self.state = InnerState::Headers; - } - } - } - } - _ => (), - } - - // read field headers for next field - if self.state == InnerState::Headers { - if let Async::Ready(headers) = InnerMultipart::read_headers(payload)? - { - self.state = InnerState::Boundary; - headers - } else { - return Ok(Async::NotReady); - } - } else { - unreachable!() - } - } else { - debug!("NotReady: field is in flight"); - return Ok(Async::NotReady); - }; - - // content type - let mut mt = mime::APPLICATION_OCTET_STREAM; - 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; - } - } - } - - self.state = InnerState::Boundary; - - // 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), - })))) - } else { - let field = Rc::new(RefCell::new(InnerField::new( - self.payload.clone(), - self.boundary.clone(), - &headers, - )?)); - self.item = InnerMultipartItem::Field(Rc::clone(&field)); - - Ok(Async::Ready(Some(MultipartItem::Field(Field::new( - safety.clone(), - headers, - mt, - field, - ))))) - } - } - } -} - -impl Drop for InnerMultipart { - fn drop(&mut self) { - // InnerMultipartItem::Field has to be dropped first because of Safety. - self.item = InnerMultipartItem::None; - } -} - -/// A single field in a multipart stream -pub struct Field { - ct: mime::Mime, - headers: HeaderMap, - inner: Rc>>, - safety: Safety, -} - -impl Field -where - S: Stream, -{ - fn new( - safety: Safety, headers: HeaderMap, ct: mime::Mime, - inner: Rc>>, - ) -> Self { - Field { - ct, - headers, - inner, - safety, - } - } - - /// Get a map of headers - pub fn headers(&self) -> &HeaderMap { - &self.headers - } - - /// Get the content type of the field - pub fn content_type(&self) -> &mime::Mime { - &self.ct - } - - /// Get the content disposition of the field, if it exists - 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(::http::header::CONTENT_DISPOSITION) - { - ContentDisposition::from_raw(content_disposition).ok() - } else { - None - } - } -} - -impl Stream for Field -where - S: Stream, -{ - type Item = Bytes; - type Error = MultipartError; - - fn poll(&mut self) -> Poll, Self::Error> { - if self.safety.current() { - self.inner.borrow_mut().poll(&self.safety) - } else { - Ok(Async::NotReady) - } - } -} - -impl fmt::Debug for Field { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "\nMultipartField: {}", self.ct)?; - writeln!(f, " boundary: {}", self.inner.borrow().boundary)?; - writeln!(f, " headers:")?; - for (key, val) in self.headers.iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } -} - -struct InnerField { - payload: Option>, - boundary: String, - eof: bool, - length: Option, -} - -impl InnerField -where - S: Stream, -{ - fn new( - payload: PayloadRef, boundary: String, headers: &HeaderMap, - ) -> Result, PayloadError> { - 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) - } else { - return Err(PayloadError::Incomplete); - } - } else { - return Err(PayloadError::Incomplete); - } - } else { - None - }; - - Ok(InnerField { - boundary, - payload: Some(payload), - eof: false, - length: len, - }) - } - - /// Reads body part content chunk of the specified size. - /// The body part must has `Content-Length` header with proper value. - fn read_len( - payload: &mut PayloadBuffer, size: &mut u64, - ) -> Poll, MultipartError> { - 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))) => { - let len = cmp::min(chunk.len() as u64, *size); - *size -= len; - let ch = chunk.split_to(len as usize); - if !chunk.is_empty() { - payload.unprocessed(chunk); - } - Ok(Async::Ready(Some(ch))) - } - Err(err) => Err(err.into()), - } - } - } - - /// Reads content chunk of body part with unknown length. - /// The `Content-Length` header for body part is not necessary. - fn read_stream( - 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))) - } - } - } - } else { - let to = chunk.len() - 1; - let ch = chunk.split_to(to); - payload.unprocessed(chunk); - Ok(Async::Ready(Some(ch))) - } - } - } - } - - fn poll(&mut self, s: &Safety) -> Poll, MultipartError> { - if self.payload.is_none() { - return Ok(Async::Ready(None)); - } - - 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)? - }; - - 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" { - warn!("multipart field did not read all the data or it is malformed"); - } - Async::Ready(None) - } - } - } - } - } else { - Async::NotReady - }; - - if Async::Ready(None) == result { - self.payload.take(); - } - Ok(result) - } -} - -struct PayloadRef { - payload: Rc>>, -} - -impl PayloadRef -where - S: Stream, -{ - fn new(payload: PayloadBuffer) -> PayloadRef { - PayloadRef { - payload: Rc::new(payload.into()), - } - } - - fn get_mut<'a, 'b>(&'a self, s: &'b Safety) -> Option<&'a mut PayloadBuffer> - where - 'a: 'b, - { - // Unsafe: Invariant is inforced by Safety Safety is used as ref counter, - // only top most ref can have mutable access to payload. - if s.current() { - let payload: &mut PayloadBuffer = unsafe { &mut *self.payload.get() }; - Some(payload) - } else { - None - } - } -} - -impl Clone for PayloadRef { - fn clone(&self) -> PayloadRef { - PayloadRef { - payload: Rc::clone(&self.payload), - } - } -} - -/// Counter. It tracks of number of clones of payloads and give access to -/// payload only to top most task panics if Safety get destroyed and it not top -/// most task. -#[derive(Debug)] -struct Safety { - task: Option, - level: usize, - payload: Rc>, -} - -impl Safety { - fn new() -> Safety { - let payload = Rc::new(PhantomData); - Safety { - task: None, - level: Rc::strong_count(&payload), - payload, - } - } - - fn current(&self) -> bool { - Rc::strong_count(&self.payload) == self.level - } -} - -impl Clone for Safety { - fn clone(&self) -> Safety { - let payload = Rc::clone(&self.payload); - Safety { - task: Some(current_task()), - level: Rc::strong_count(&payload), - payload, - } - } -} - -impl Drop for Safety { - fn drop(&mut self) { - // parent task is dead - if Rc::strong_count(&self.payload) != self.level { - panic!("Safety get dropped but it is not from top-most task"); - } - if let Some(task) = self.task.take() { - task.notify() - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use bytes::Bytes; - use futures::future::{lazy, result}; - use payload::{Payload, PayloadWriter}; - use tokio::runtime::current_thread::Runtime; - - #[test] - fn test_boundary() { - let headers = HeaderMap::new(); - match Multipart::boundary(&headers) { - Err(MultipartError::NoContentType) => (), - _ => unreachable!("should not happen"), - } - - let mut headers = HeaderMap::new(); - headers.insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static("test"), - ); - - match Multipart::boundary(&headers) { - Err(MultipartError::ParseContentType) => (), - _ => unreachable!("should not happen"), - } - - let mut headers = HeaderMap::new(); - headers.insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static("multipart/mixed"), - ); - match Multipart::boundary(&headers) { - Err(MultipartError::Boundary) => (), - _ => unreachable!("should not happen"), - } - - let mut headers = HeaderMap::new(); - headers.insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static( - "multipart/mixed; boundary=\"5c02368e880e436dab70ed54e1c58209\"", - ), - ); - - assert_eq!( - Multipart::boundary(&headers).unwrap(), - "5c02368e880e436dab70ed54e1c58209" - ); - } - - #[test] - fn test_multipart() { - Runtime::new() - .unwrap() - .block_on(lazy(|| { - let (mut sender, payload) = Payload::new(false); - - 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\nContent-Length: 4\r\n\r\n\ - test\r\n\ - --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ - Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ - data\r\n\ - --abbc761f78ff4d7cb7573b5a23f96ef0--\r\n"); - sender.feed_data(bytes); - - let mut multipart = Multipart::new( - Ok("abbc761f78ff4d7cb7573b5a23f96ef0".to_owned()), - payload, - ); - match multipart.poll() { - Ok(Async::Ready(Some(item))) => match item { - MultipartItem::Field(mut field) => { - { - use http::header::{DispositionParam, DispositionType}; - 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() { - Ok(Async::Ready(Some(chunk))) => { - assert_eq!(chunk, "test") - } - _ => unreachable!(), - } - match field.poll() { - Ok(Async::Ready(None)) => (), - _ => unreachable!(), - } - } - _ => 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 field.poll() { - Ok(Async::Ready(Some(chunk))) => { - assert_eq!(chunk, "data") - } - _ => unreachable!(), - } - match field.poll() { - Ok(Async::Ready(None)) => (), - _ => unreachable!(), - } - } - _ => unreachable!(), - }, - _ => unreachable!(), - } - - match multipart.poll() { - Ok(Async::Ready(None)) => (), - _ => unreachable!(), - } - - let res: Result<(), ()> = Ok(()); - result(res) - })).unwrap(); - } -} diff --git a/src/param.rs b/src/param.rs deleted file mode 100644 index a3f60259..00000000 --- a/src/param.rs +++ /dev/null @@ -1,334 +0,0 @@ -use std; -use std::ops::Index; -use std::path::PathBuf; -use std::rc::Rc; -use std::str::FromStr; - -use http::StatusCode; -use smallvec::SmallVec; - -use error::{InternalError, ResponseError, UriSegmentError}; -use uri::{Url, RESERVED_QUOTER}; - -/// A trait to abstract the idea of creating a new instance of a type from a -/// path parameter. -pub trait FromParam: Sized { - /// The associated error which can be returned from parsing. - type Err: ResponseError; - - /// Parses a string `s` to return a value of this type. - fn from_param(s: &str) -> Result; -} - -#[derive(Debug, Clone)] -pub(crate) enum ParamItem { - Static(&'static str), - UrlSegment(u16, u16), -} - -/// Route match information -/// -/// If resource path contains variable patterns, `Params` stores this variables. -#[derive(Debug, Clone)] -pub struct Params { - url: Url, - pub(crate) tail: u16, - pub(crate) segments: SmallVec<[(Rc, ParamItem); 3]>, -} - -impl Params { - pub(crate) fn new() -> Params { - Params { - url: Url::default(), - tail: 0, - segments: SmallVec::new(), - } - } - - pub(crate) fn with_url(url: &Url) -> Params { - Params { - url: url.clone(), - tail: 0, - segments: SmallVec::new(), - } - } - - pub(crate) fn clear(&mut self) { - self.segments.clear(); - } - - pub(crate) fn set_tail(&mut self, tail: u16) { - self.tail = tail; - } - - pub(crate) fn set_url(&mut self, url: Url) { - self.url = url; - } - - pub(crate) fn add(&mut self, name: Rc, value: ParamItem) { - self.segments.push((name, value)); - } - - pub(crate) fn add_static(&mut self, name: &str, value: &'static str) { - self.segments - .push((Rc::new(name.to_string()), ParamItem::Static(value))); - } - - /// Check if there are any matched patterns - pub fn is_empty(&self) -> bool { - self.segments.is_empty() - } - - /// Check number of extracted parameters - pub fn len(&self) -> usize { - self.segments.len() - } - - /// Get matched parameter by name without type conversion - pub fn get(&self, key: &str) -> Option<&str> { - for item in self.segments.iter() { - if key == item.0.as_str() { - return match item.1 { - ParamItem::Static(ref s) => Some(&s), - ParamItem::UrlSegment(s, e) => { - Some(&self.url.path()[(s as usize)..(e as usize)]) - } - }; - } - } - if key == "tail" { - Some(&self.url.path()[(self.tail as usize)..]) - } else { - None - } - } - - /// Get URL-decoded matched parameter by name without type conversion - pub fn get_decoded(&self, key: &str) -> Option { - self.get(key).map(|value| { - if let Some(ref mut value) = RESERVED_QUOTER.requote(value.as_bytes()) { - Rc::make_mut(value).to_string() - } else { - value.to_string() - } - }) - } - - /// Get unprocessed part of path - pub fn unprocessed(&self) -> &str { - &self.url.path()[(self.tail as usize)..] - } - - /// Get matched `FromParam` compatible parameter by name. - /// - /// If keyed parameter is not available empty string is used as default - /// value. - /// - /// ```rust - /// # extern crate actix_web; - /// # use actix_web::*; - /// fn index(req: HttpRequest) -> Result { - /// let ivalue: isize = req.match_info().query("val")?; - /// Ok(format!("isuze value: {:?}", ivalue)) - /// } - /// # fn main() {} - /// ``` - pub fn query(&self, key: &str) -> Result::Err> { - if let Some(s) = self.get(key) { - T::from_param(s) - } else { - T::from_param("") - } - } - - /// Return iterator to items in parameter container - pub fn iter(&self) -> ParamsIter { - ParamsIter { - idx: 0, - params: self, - } - } -} - -#[derive(Debug)] -pub struct ParamsIter<'a> { - idx: usize, - params: &'a Params, -} - -impl<'a> Iterator for ParamsIter<'a> { - type Item = (&'a str, &'a str); - - #[inline] - fn next(&mut self) -> Option<(&'a str, &'a str)> { - if self.idx < self.params.len() { - let idx = self.idx; - let res = match self.params.segments[idx].1 { - ParamItem::Static(ref s) => &s, - ParamItem::UrlSegment(s, e) => { - &self.params.url.path()[(s as usize)..(e as usize)] - } - }; - self.idx += 1; - return Some((&self.params.segments[idx].0, res)); - } - None - } -} - -impl<'a> Index<&'a str> for Params { - type Output = str; - - fn index(&self, name: &'a str) -> &str { - self.get(name) - .expect("Value for parameter is not available") - } -} - -impl Index for Params { - type Output = str; - - fn index(&self, idx: usize) -> &str { - match self.segments[idx].1 { - ParamItem::Static(ref s) => &s, - ParamItem::UrlSegment(s, e) => &self.url.path()[(s as usize)..(e as usize)], - } - } -} - -/// Creates a `PathBuf` from a path parameter. The returned `PathBuf` is -/// percent-decoded. If a segment is equal to "..", the previous segment (if -/// any) is skipped. -/// -/// For security purposes, if a segment meets any of the following conditions, -/// an `Err` is returned indicating the condition met: -/// -/// * Decoded segment starts with any of: `.` (except `..`), `*` -/// * Decoded segment ends with any of: `:`, `>`, `<` -/// * Decoded segment contains any of: `/` -/// * On Windows, decoded segment contains any of: '\' -/// * Percent-encoding results in invalid UTF8. -/// -/// As a result of these conditions, a `PathBuf` parsed from request path -/// parameter is safe to interpolate within, or use as a suffix of, a path -/// without additional checks. -impl FromParam for PathBuf { - type Err = UriSegmentError; - - fn from_param(val: &str) -> Result { - let mut buf = PathBuf::new(); - for segment in val.split('/') { - if segment == ".." { - buf.pop(); - } else if segment.starts_with('.') { - return Err(UriSegmentError::BadStart('.')); - } else if segment.starts_with('*') { - return Err(UriSegmentError::BadStart('*')); - } else if segment.ends_with(':') { - return Err(UriSegmentError::BadEnd(':')); - } else if segment.ends_with('>') { - return Err(UriSegmentError::BadEnd('>')); - } else if segment.ends_with('<') { - return Err(UriSegmentError::BadEnd('<')); - } else if segment.is_empty() { - continue; - } else if cfg!(windows) && segment.contains('\\') { - return Err(UriSegmentError::BadChar('\\')); - } else { - buf.push(segment) - } - } - - Ok(buf) - } -} - -macro_rules! FROM_STR { - ($type:ty) => { - impl FromParam for $type { - type Err = InternalError<<$type as FromStr>::Err>; - fn from_param(val: &str) -> Result { - <$type as FromStr>::from_str(val) - .map_err(|e| InternalError::new(e, StatusCode::BAD_REQUEST)) - } - } - }; -} - -FROM_STR!(u8); -FROM_STR!(u16); -FROM_STR!(u32); -FROM_STR!(u64); -FROM_STR!(usize); -FROM_STR!(i8); -FROM_STR!(i16); -FROM_STR!(i32); -FROM_STR!(i64); -FROM_STR!(isize); -FROM_STR!(f32); -FROM_STR!(f64); -FROM_STR!(String); -FROM_STR!(std::net::IpAddr); -FROM_STR!(std::net::Ipv4Addr); -FROM_STR!(std::net::Ipv6Addr); -FROM_STR!(std::net::SocketAddr); -FROM_STR!(std::net::SocketAddrV4); -FROM_STR!(std::net::SocketAddrV6); - -#[cfg(test)] -mod tests { - use super::*; - use std::iter::FromIterator; - - #[test] - fn test_path_buf() { - assert_eq!( - PathBuf::from_param("/test/.tt"), - Err(UriSegmentError::BadStart('.')) - ); - assert_eq!( - PathBuf::from_param("/test/*tt"), - Err(UriSegmentError::BadStart('*')) - ); - assert_eq!( - PathBuf::from_param("/test/tt:"), - Err(UriSegmentError::BadEnd(':')) - ); - assert_eq!( - PathBuf::from_param("/test/tt<"), - Err(UriSegmentError::BadEnd('<')) - ); - assert_eq!( - PathBuf::from_param("/test/tt>"), - Err(UriSegmentError::BadEnd('>')) - ); - assert_eq!( - PathBuf::from_param("/seg1/seg2/"), - Ok(PathBuf::from_iter(vec!["seg1", "seg2"])) - ); - assert_eq!( - PathBuf::from_param("/seg1/../seg2/"), - Ok(PathBuf::from_iter(vec!["seg2"])) - ); - } - - #[test] - fn test_get_param_by_name() { - let mut params = Params::new(); - params.add_static("item1", "path"); - params.add_static("item2", "http%3A%2F%2Flocalhost%3A80%2Ffoo"); - - assert_eq!(params.get("item0"), None); - assert_eq!(params.get_decoded("item0"), None); - assert_eq!(params.get("item1"), Some("path")); - assert_eq!(params.get_decoded("item1"), Some("path".to_string())); - assert_eq!( - params.get("item2"), - Some("http%3A%2F%2Flocalhost%3A80%2Ffoo") - ); - assert_eq!( - params.get_decoded("item2"), - Some("http://localhost:80/foo".to_string()) - ); - } -} diff --git a/src/payload.rs b/src/payload.rs deleted file mode 100644 index 2131e3c3..00000000 --- a/src/payload.rs +++ /dev/null @@ -1,715 +0,0 @@ -//! Payload stream -use bytes::{Bytes, BytesMut}; -#[cfg(not(test))] -use futures::task::current as current_task; -use futures::task::Task; -use futures::{Async, Poll, Stream}; -use std::cell::RefCell; -use std::cmp; -use std::collections::VecDeque; -use std::rc::{Rc, Weak}; - -use error::PayloadError; - -/// max buffer size 32k -pub(crate) const MAX_BUFFER_SIZE: usize = 32_768; - -#[derive(Debug, PartialEq)] -pub(crate) enum PayloadStatus { - Read, - Pause, - Dropped, -} - -/// Buffered stream of bytes chunks -/// -/// Payload stores chunks in a vector. First chunk can be received with -/// `.readany()` method. Payload stream is not thread safe. Payload does not -/// notify current task when new data is available. -/// -/// Payload stream can be used as `HttpResponse` body stream. -#[derive(Debug)] -pub struct Payload { - inner: Rc>, -} - -impl Payload { - /// Create payload stream. - /// - /// This method construct two objects responsible for bytes stream - /// generation. - /// - /// * `PayloadSender` - *Sender* side of the stream - /// - /// * `Payload` - *Receiver* side of the stream - pub fn new(eof: bool) -> (PayloadSender, Payload) { - let shared = Rc::new(RefCell::new(Inner::new(eof))); - - ( - PayloadSender { - inner: Rc::downgrade(&shared), - }, - Payload { inner: shared }, - ) - } - - /// Create empty payload - #[doc(hidden)] - pub fn empty() -> Payload { - Payload { - inner: Rc::new(RefCell::new(Inner::new(true))), - } - } - - /// Length of the data in this payload - #[cfg(test)] - pub fn len(&self) -> usize { - self.inner.borrow().len() - } - - /// Is payload empty - #[cfg(test)] - pub fn is_empty(&self) -> bool { - self.inner.borrow().len() == 0 - } - - /// Put unused data back to payload - #[inline] - pub fn unread_data(&mut self, data: Bytes) { - self.inner.borrow_mut().unread_data(data); - } - - #[cfg(test)] - pub(crate) fn readall(&self) -> Option { - self.inner.borrow_mut().readall() - } - - #[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 { - type Item = Bytes; - type Error = PayloadError; - - #[inline] - fn poll(&mut self) -> Poll, PayloadError> { - self.inner.borrow_mut().readany() - } -} - -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 { - #[inline] - 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) { - if let Some(shared) = self.inner.upgrade() { - shared.borrow_mut().feed_eof() - } - } - - #[inline] - 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 { - // we check need_read only if Payload (other side) is alive, - // otherwise always return true (consume payload) - if let Some(shared) = self.inner.upgrade() { - if shared.borrow().need_read { - PayloadStatus::Read - } else { - #[cfg(not(test))] - { - if shared.borrow_mut().io_task.is_none() { - shared.borrow_mut().io_task = Some(current_task()); - } - } - PayloadStatus::Pause - } - } else { - PayloadStatus::Dropped - } - } -} - -#[derive(Debug)] -struct Inner { - len: usize, - eof: bool, - err: Option, - need_read: bool, - items: VecDeque, - capacity: usize, - task: Option, - io_task: Option, -} - -impl Inner { - fn new(eof: bool) -> Self { - Inner { - eof, - len: 0, - err: None, - items: VecDeque::new(), - need_read: true, - capacity: MAX_BUFFER_SIZE, - task: None, - io_task: None, - } - } - - #[inline] - fn set_error(&mut self, err: PayloadError) { - self.err = Some(err); - } - - #[inline] - fn feed_eof(&mut self) { - self.eof = true; - } - - #[inline] - fn feed_data(&mut self, data: Bytes) { - self.len += data.len(); - self.items.push_back(data); - self.need_read = self.len < self.capacity; - if let Some(task) = self.task.take() { - task.notify() - } - } - - #[cfg(test)] - fn len(&self) -> usize { - self.len - } - - #[cfg(test)] - pub(crate) fn readall(&mut self) -> Option { - let len = self.items.iter().map(|b| b.len()).sum(); - if len > 0 { - let mut buf = BytesMut::with_capacity(len); - for item in &self.items { - buf.extend_from_slice(item); - } - self.items = VecDeque::new(); - self.len = 0; - Some(buf.take().freeze()) - } else { - self.need_read = true; - None - } - } - - 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; - #[cfg(not(test))] - { - if self.need_read && self.task.is_none() { - self.task = Some(current_task()); - } - if let Some(task) = self.io_task.take() { - task.notify() - } - } - Ok(Async::Ready(Some(data))) - } else if let Some(err) = self.err.take() { - Err(err) - } else if self.eof { - Ok(Async::Ready(None)) - } else { - self.need_read = true; - #[cfg(not(test))] - { - if self.task.is_none() { - self.task = Some(current_task()); - } - if let Some(task) = self.io_task.take() { - task.notify() - } - } - Ok(Async::NotReady) - } - } - - fn unread_data(&mut self, data: Bytes) { - self.len += data.len(); - self.items.push_front(data); - } -} - -/// 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 failure::Fail; - use futures::future::{lazy, result}; - use std::io; - use tokio::runtime::current_thread::Runtime; - - #[test] - fn test_error() { - let err: PayloadError = - io::Error::new(io::ErrorKind::Other, "ParseError").into(); - assert_eq!(format!("{}", err), "ParseError"); - assert_eq!(format!("{}", err.cause().unwrap()), "ParseError"); - - let err = PayloadError::Incomplete; - assert_eq!( - format!("{}", err), - "A payload reached EOF, but is not complete." - ); - } - - #[test] - fn test_basic() { - Runtime::new() - .unwrap() - .block_on(lazy(|| { - let (_, payload) = Payload::new(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::new(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::new(false); - let mut payload = PayloadBuffer::new(payload); - - assert_eq!(Async::NotReady, payload.readany().ok().unwrap()); - - sender.set_error(PayloadError::Incomplete); - 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::new(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::new(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); - 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::new(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); - payload.read_until(b"b").err().unwrap(); - - let res: Result<(), ()> = Ok(()); - result(res) - })).unwrap(); - } - - #[test] - fn test_unread_data() { - Runtime::new() - .unwrap() - .block_on(lazy(|| { - let (_, mut payload) = Payload::new(false); - - payload.unread_data(Bytes::from("data")); - assert!(!payload.is_empty()); - assert_eq!(payload.len(), 4); - - assert_eq!( - Async::Ready(Some(Bytes::from("data"))), - payload.poll().ok().unwrap() - ); - - let res: Result<(), ()> = Ok(()); - result(res) - })).unwrap(); - } -} diff --git a/src/pipeline.rs b/src/pipeline.rs deleted file mode 100644 index a938f2eb..00000000 --- a/src/pipeline.rs +++ /dev/null @@ -1,869 +0,0 @@ -use std::marker::PhantomData; -use std::rc::Rc; -use std::{io, mem}; - -use futures::sync::oneshot; -use futures::{Async, Future, Poll, Stream}; -use log::Level::Debug; - -use body::{Body, BodyStream}; -use context::{ActorHttpContext, Frame}; -use error::Error; -use handler::{AsyncResult, AsyncResultItem}; -use header::ContentEncoding; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Finished, Middleware, Response, Started}; -use server::{HttpHandlerTask, Writer, WriterState}; - -#[doc(hidden)] -pub trait PipelineHandler { - fn encoding(&self) -> ContentEncoding; - - fn handle(&self, &HttpRequest) -> AsyncResult; -} - -#[doc(hidden)] -pub struct Pipeline( - PipelineInfo, - PipelineState, - Rc>>>, -); - -enum PipelineState { - None, - Error, - Starting(StartMiddlewares), - Handler(WaitingResponse), - RunMiddlewares(RunMiddlewares), - Response(ProcessResponse), - Finishing(FinishingMiddlewares), - Completed(Completed), -} - -impl> PipelineState { - fn poll( - &mut self, info: &mut PipelineInfo, mws: &[Box>], - ) -> Option> { - match *self { - PipelineState::Starting(ref mut state) => state.poll(info, mws), - PipelineState::Handler(ref mut state) => state.poll(info, mws), - PipelineState::RunMiddlewares(ref mut state) => state.poll(info, mws), - PipelineState::Finishing(ref mut state) => state.poll(info, mws), - PipelineState::Completed(ref mut state) => state.poll(info), - PipelineState::Response(ref mut state) => state.poll(info, mws), - PipelineState::None | PipelineState::Error => None, - } - } -} - -struct PipelineInfo { - req: HttpRequest, - count: u16, - context: Option>, - error: Option, - disconnected: Option, - encoding: ContentEncoding, -} - -impl PipelineInfo { - fn poll_context(&mut self) -> Poll<(), Error> { - if let Some(ref mut context) = self.context { - match context.poll() { - Err(err) => Err(err), - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(_)) => Ok(Async::Ready(())), - } - } else { - Ok(Async::Ready(())) - } - } -} - -impl> Pipeline { - pub(crate) fn new( - req: HttpRequest, mws: Rc>>>, handler: Rc, - ) -> Pipeline { - let mut info = PipelineInfo { - req, - count: 0, - error: None, - context: None, - disconnected: None, - encoding: handler.encoding(), - }; - let state = StartMiddlewares::init(&mut info, &mws, handler); - - Pipeline(info, state, mws) - } -} - -impl Pipeline { - #[inline] - fn is_done(&self) -> bool { - match self.1 { - PipelineState::None - | PipelineState::Error - | PipelineState::Starting(_) - | PipelineState::Handler(_) - | PipelineState::RunMiddlewares(_) - | PipelineState::Response(_) => true, - PipelineState::Finishing(_) | PipelineState::Completed(_) => false, - } - } -} - -impl> HttpHandlerTask for Pipeline { - fn disconnected(&mut self) { - self.0.disconnected = Some(true); - } - - fn poll_io(&mut self, io: &mut Writer) -> Poll { - let mut state = mem::replace(&mut self.1, PipelineState::None); - - loop { - if let PipelineState::Response(st) = state { - match st.poll_io(io, &mut self.0, &self.2) { - Ok(state) => { - self.1 = state; - if let Some(error) = self.0.error.take() { - return Err(error); - } else { - return Ok(Async::Ready(self.is_done())); - } - } - Err(state) => { - self.1 = state; - return Ok(Async::NotReady); - } - } - } - match state { - PipelineState::None => return Ok(Async::Ready(true)), - PipelineState::Error => { - return Err( - io::Error::new(io::ErrorKind::Other, "Internal error").into() - ) - } - _ => (), - } - - match state.poll(&mut self.0, &self.2) { - Some(st) => state = st, - None => { - return { - self.1 = state; - Ok(Async::NotReady) - } - } - } - } - } - - fn poll_completed(&mut self) -> Poll<(), Error> { - let mut state = mem::replace(&mut self.1, PipelineState::None); - loop { - match state { - PipelineState::None | PipelineState::Error => { - return Ok(Async::Ready(())) - } - _ => (), - } - - if let Some(st) = state.poll(&mut self.0, &self.2) { - state = st; - } else { - self.1 = state; - return Ok(Async::NotReady); - } - } - } -} - -type Fut = Box, Error = Error>>; - -/// Middlewares start executor -struct StartMiddlewares { - hnd: Rc, - fut: Option, - _s: PhantomData, -} - -impl> StartMiddlewares { - fn init( - info: &mut PipelineInfo, mws: &[Box>], hnd: Rc, - ) -> PipelineState { - // execute middlewares, we need this stage because middlewares could be - // non-async and we can move to next state immediately - let len = mws.len() as u16; - - loop { - if info.count == len { - let reply = hnd.handle(&info.req); - return WaitingResponse::init(info, mws, reply); - } else { - match mws[info.count as usize].start(&info.req) { - Ok(Started::Done) => info.count += 1, - Ok(Started::Response(resp)) => { - return RunMiddlewares::init(info, mws, resp); - } - Ok(Started::Future(fut)) => { - return PipelineState::Starting(StartMiddlewares { - hnd, - fut: Some(fut), - _s: PhantomData, - }) - } - Err(err) => { - return RunMiddlewares::init(info, mws, err.into()); - } - } - } - } - } - - fn poll( - &mut self, info: &mut PipelineInfo, mws: &[Box>], - ) -> Option> { - let len = mws.len() as u16; - - 'outer: loop { - match self.fut.as_mut().unwrap().poll() { - Ok(Async::NotReady) => { - return None; - } - Ok(Async::Ready(resp)) => { - info.count += 1; - if let Some(resp) = resp { - return Some(RunMiddlewares::init(info, mws, resp)); - } - loop { - if info.count == len { - let reply = self.hnd.handle(&info.req); - return Some(WaitingResponse::init(info, mws, reply)); - } else { - let res = mws[info.count as usize].start(&info.req); - match res { - Ok(Started::Done) => info.count += 1, - Ok(Started::Response(resp)) => { - return Some(RunMiddlewares::init(info, mws, resp)); - } - Ok(Started::Future(fut)) => { - self.fut = Some(fut); - continue 'outer; - } - Err(err) => { - return Some(RunMiddlewares::init( - info, - mws, - err.into(), - )); - } - } - } - } - } - Err(err) => { - return Some(RunMiddlewares::init(info, mws, err.into())); - } - } - } - } -} - -// waiting for response -struct WaitingResponse { - fut: Box>, - _s: PhantomData, - _h: PhantomData, -} - -impl WaitingResponse { - #[inline] - fn init( - info: &mut PipelineInfo, mws: &[Box>], - reply: AsyncResult, - ) -> PipelineState { - match reply.into() { - AsyncResultItem::Ok(resp) => RunMiddlewares::init(info, mws, resp), - AsyncResultItem::Err(err) => RunMiddlewares::init(info, mws, err.into()), - AsyncResultItem::Future(fut) => PipelineState::Handler(WaitingResponse { - fut, - _s: PhantomData, - _h: PhantomData, - }), - } - } - - fn poll( - &mut self, info: &mut PipelineInfo, mws: &[Box>], - ) -> Option> { - match self.fut.poll() { - Ok(Async::NotReady) => None, - Ok(Async::Ready(resp)) => Some(RunMiddlewares::init(info, mws, resp)), - Err(err) => Some(RunMiddlewares::init(info, mws, err.into())), - } - } -} - -/// Middlewares response executor -struct RunMiddlewares { - curr: usize, - fut: Option>>, - _s: PhantomData, - _h: PhantomData, -} - -impl RunMiddlewares { - #[inline] - fn init( - info: &mut PipelineInfo, mws: &[Box>], mut resp: HttpResponse, - ) -> PipelineState { - if info.count == 0 { - return ProcessResponse::init(resp); - } - let mut curr = 0; - let len = mws.len(); - - loop { - let state = mws[curr].response(&info.req, resp); - resp = match state { - Err(err) => { - info.count = (curr + 1) as u16; - return ProcessResponse::init(err.into()); - } - Ok(Response::Done(r)) => { - curr += 1; - if curr == len { - return ProcessResponse::init(r); - } else { - r - } - } - Ok(Response::Future(fut)) => { - return PipelineState::RunMiddlewares(RunMiddlewares { - curr, - fut: Some(fut), - _s: PhantomData, - _h: PhantomData, - }); - } - }; - } - } - - fn poll( - &mut self, info: &mut PipelineInfo, mws: &[Box>], - ) -> Option> { - let len = mws.len(); - - loop { - // poll latest fut - let mut resp = match self.fut.as_mut().unwrap().poll() { - Ok(Async::NotReady) => return None, - Ok(Async::Ready(resp)) => { - self.curr += 1; - resp - } - Err(err) => return Some(ProcessResponse::init(err.into())), - }; - - loop { - if self.curr == len { - return Some(ProcessResponse::init(resp)); - } else { - let state = mws[self.curr].response(&info.req, resp); - match state { - Err(err) => return Some(ProcessResponse::init(err.into())), - Ok(Response::Done(r)) => { - self.curr += 1; - resp = r - } - Ok(Response::Future(fut)) => { - self.fut = Some(fut); - break; - } - } - } - } - } - } -} - -struct ProcessResponse { - resp: Option, - iostate: IOState, - running: RunningState, - drain: Option>, - _s: PhantomData, - _h: PhantomData, -} - -#[derive(PartialEq, Debug)] -enum RunningState { - Running, - Paused, - Done, -} - -impl RunningState { - #[inline] - fn pause(&mut self) { - if *self != RunningState::Done { - *self = RunningState::Paused - } - } - #[inline] - fn resume(&mut self) { - if *self != RunningState::Done { - *self = RunningState::Running - } - } -} - -enum IOState { - Response, - Payload(BodyStream), - Actor(Box), - Done, -} - -impl ProcessResponse { - #[inline] - fn init(resp: HttpResponse) -> PipelineState { - PipelineState::Response(ProcessResponse { - resp: Some(resp), - iostate: IOState::Response, - running: RunningState::Running, - drain: None, - _s: PhantomData, - _h: PhantomData, - }) - } - - fn poll( - &mut self, info: &mut PipelineInfo, mws: &[Box>], - ) -> Option> { - // connection is dead at this point - match mem::replace(&mut self.iostate, IOState::Done) { - IOState::Response => Some(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )), - IOState::Payload(_) => Some(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )), - IOState::Actor(mut ctx) => { - if info.disconnected.take().is_some() { - ctx.disconnected(); - } - loop { - match ctx.poll() { - Ok(Async::Ready(Some(vec))) => { - if vec.is_empty() { - continue; - } - for frame in vec { - match frame { - Frame::Chunk(None) => { - info.context = Some(ctx); - return Some(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - Frame::Chunk(Some(_)) => (), - Frame::Drain(fut) => { - let _ = fut.send(()); - } - } - } - } - Ok(Async::Ready(None)) => { - return Some(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )) - } - Ok(Async::NotReady) => { - self.iostate = IOState::Actor(ctx); - return None; - } - Err(err) => { - info.context = Some(ctx); - info.error = Some(err); - return Some(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - } - } - } - IOState::Done => Some(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )), - } - } - - fn poll_io( - mut self, io: &mut Writer, info: &mut PipelineInfo, - mws: &[Box>], - ) -> Result, PipelineState> { - loop { - if self.drain.is_none() && self.running != RunningState::Paused { - // if task is paused, write buffer is probably full - 'inner: loop { - let result = match mem::replace(&mut self.iostate, IOState::Done) { - IOState::Response => { - let encoding = self - .resp - .as_ref() - .unwrap() - .content_encoding() - .unwrap_or(info.encoding); - - let result = match io.start( - &info.req, - self.resp.as_mut().unwrap(), - encoding, - ) { - Ok(res) => res, - Err(err) => { - info.error = Some(err.into()); - return Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - }; - - if let Some(err) = self.resp.as_ref().unwrap().error() { - if self.resp.as_ref().unwrap().status().is_server_error() - { - error!( - "Error occurred during request handling, status: {} {}", - self.resp.as_ref().unwrap().status(), err - ); - } else { - warn!( - "Error occurred during request handling: {}", - err - ); - } - if log_enabled!(Debug) { - debug!("{:?}", err); - } - } - - // always poll stream or actor for the first time - match self.resp.as_mut().unwrap().replace_body(Body::Empty) { - Body::Streaming(stream) => { - self.iostate = IOState::Payload(stream); - continue 'inner; - } - Body::Actor(ctx) => { - self.iostate = IOState::Actor(ctx); - continue 'inner; - } - _ => (), - } - - result - } - IOState::Payload(mut body) => match body.poll() { - Ok(Async::Ready(None)) => { - if let Err(err) = io.write_eof() { - info.error = Some(err.into()); - return Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - break; - } - Ok(Async::Ready(Some(chunk))) => { - self.iostate = IOState::Payload(body); - match io.write(&chunk.into()) { - Err(err) => { - info.error = Some(err.into()); - return Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - Ok(result) => result, - } - } - Ok(Async::NotReady) => { - self.iostate = IOState::Payload(body); - break; - } - Err(err) => { - info.error = Some(err); - return Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - }, - IOState::Actor(mut ctx) => { - if info.disconnected.take().is_some() { - ctx.disconnected(); - } - match ctx.poll() { - Ok(Async::Ready(Some(vec))) => { - if vec.is_empty() { - self.iostate = IOState::Actor(ctx); - break; - } - let mut res = None; - for frame in vec { - match frame { - Frame::Chunk(None) => { - info.context = Some(ctx); - if let Err(err) = io.write_eof() { - info.error = Some(err.into()); - return Ok( - FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - ), - ); - } - break 'inner; - } - Frame::Chunk(Some(chunk)) => match io - .write(&chunk) - { - Err(err) => { - info.context = Some(ctx); - info.error = Some(err.into()); - return Ok( - FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - ), - ); - } - Ok(result) => res = Some(result), - }, - Frame::Drain(fut) => self.drain = Some(fut), - } - } - self.iostate = IOState::Actor(ctx); - if self.drain.is_some() { - self.running.resume(); - break 'inner; - } - res.unwrap() - } - Ok(Async::Ready(None)) => break, - Ok(Async::NotReady) => { - self.iostate = IOState::Actor(ctx); - break; - } - Err(err) => { - info.context = Some(ctx); - info.error = Some(err); - return Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - } - } - IOState::Done => break, - }; - - match result { - WriterState::Pause => { - self.running.pause(); - break; - } - WriterState::Done => self.running.resume(), - } - } - } - - // flush io but only if we need to - if self.running == RunningState::Paused || self.drain.is_some() { - match io.poll_completed(false) { - Ok(Async::Ready(_)) => { - self.running.resume(); - - // resolve drain futures - if let Some(tx) = self.drain.take() { - let _ = tx.send(()); - } - // restart io processing - continue; - } - Ok(Async::NotReady) => return Err(PipelineState::Response(self)), - Err(err) => { - if let IOState::Actor(mut ctx) = - mem::replace(&mut self.iostate, IOState::Done) - { - ctx.disconnected(); - info.context = Some(ctx); - } - info.error = Some(err.into()); - return Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - } - } - break; - } - - // response is completed - match self.iostate { - IOState::Done => { - match io.write_eof() { - Ok(_) => (), - Err(err) => { - info.error = Some(err.into()); - return Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )); - } - } - self.resp.as_mut().unwrap().set_response_size(io.written()); - Ok(FinishingMiddlewares::init( - info, - mws, - self.resp.take().unwrap(), - )) - } - _ => Err(PipelineState::Response(self)), - } - } -} - -/// Middlewares start executor -struct FinishingMiddlewares { - resp: Option, - fut: Option>>, - _s: PhantomData, - _h: PhantomData, -} - -impl FinishingMiddlewares { - #[inline] - fn init( - info: &mut PipelineInfo, mws: &[Box>], resp: HttpResponse, - ) -> PipelineState { - if info.count == 0 { - resp.release(); - Completed::init(info) - } else { - let mut state = FinishingMiddlewares { - resp: Some(resp), - fut: None, - _s: PhantomData, - _h: PhantomData, - }; - if let Some(st) = state.poll(info, mws) { - st - } else { - PipelineState::Finishing(state) - } - } - } - - fn poll( - &mut self, info: &mut PipelineInfo, mws: &[Box>], - ) -> Option> { - loop { - // poll latest fut - let not_ready = if let Some(ref mut fut) = self.fut { - match fut.poll() { - Ok(Async::NotReady) => true, - Ok(Async::Ready(())) => false, - Err(err) => { - error!("Middleware finish error: {}", err); - false - } - } - } else { - false - }; - if not_ready { - return None; - } - self.fut = None; - if info.count == 0 { - self.resp.take().unwrap().release(); - return Some(Completed::init(info)); - } - - info.count -= 1; - let state = - mws[info.count as usize].finish(&info.req, self.resp.as_ref().unwrap()); - match state { - Finished::Done => { - if info.count == 0 { - self.resp.take().unwrap().release(); - return Some(Completed::init(info)); - } - } - Finished::Future(fut) => { - self.fut = Some(fut); - } - } - } - } -} - -#[derive(Debug)] -struct Completed(PhantomData, PhantomData); - -impl Completed { - #[inline] - fn init(info: &mut PipelineInfo) -> PipelineState { - if let Some(ref err) = info.error { - error!("Error occurred during request handling: {}", err); - } - - if info.context.is_none() { - PipelineState::None - } else { - match info.poll_context() { - Ok(Async::NotReady) => { - PipelineState::Completed(Completed(PhantomData, PhantomData)) - } - Ok(Async::Ready(())) => PipelineState::None, - Err(_) => PipelineState::Error, - } - } - } - - #[inline] - fn poll(&mut self, info: &mut PipelineInfo) -> Option> { - match info.poll_context() { - Ok(Async::NotReady) => None, - Ok(Async::Ready(())) => Some(PipelineState::None), - Err(_) => Some(PipelineState::Error), - } - } -} diff --git a/src/pred.rs b/src/pred.rs deleted file mode 100644 index 99d6e608..00000000 --- a/src/pred.rs +++ /dev/null @@ -1,328 +0,0 @@ -//! Route match predicates -#![allow(non_snake_case)] -use std::marker::PhantomData; - -use http; -use http::{header, HttpTryFrom}; -use server::message::Request; - -/// Trait defines resource route predicate. -/// Predicate can modify request object. It is also possible to -/// to store extra attributes on request by using `Extensions` container, -/// Extensions container available via `HttpRequest::extensions()` method. -pub trait Predicate { - /// Check if request matches predicate - fn check(&self, &Request, &S) -> bool; -} - -/// Return predicate that matches if any of supplied predicate matches. -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::{pred, App, HttpResponse}; -/// -/// fn main() { -/// App::new().resource("/index.html", |r| { -/// r.route() -/// .filter(pred::Any(pred::Get()).or(pred::Post())) -/// .f(|r| HttpResponse::MethodNotAllowed()) -/// }); -/// } -/// ``` -pub fn Any + 'static>(pred: P) -> AnyPredicate { - AnyPredicate(vec![Box::new(pred)]) -} - -/// Matches if any of supplied predicate matches. -pub struct AnyPredicate(Vec>>); - -impl AnyPredicate { - /// Add new predicate to list of predicates to check - pub fn or + 'static>(mut self, pred: P) -> Self { - self.0.push(Box::new(pred)); - self - } -} - -impl Predicate for AnyPredicate { - fn check(&self, req: &Request, state: &S) -> bool { - for p in &self.0 { - if p.check(req, state) { - return true; - } - } - false - } -} - -/// Return predicate that matches if all of supplied predicate matches. -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::{pred, App, HttpResponse}; -/// -/// fn main() { -/// App::new().resource("/index.html", |r| { -/// r.route() -/// .filter( -/// pred::All(pred::Get()) -/// .and(pred::Header("content-type", "text/plain")), -/// ) -/// .f(|_| HttpResponse::MethodNotAllowed()) -/// }); -/// } -/// ``` -pub fn All + 'static>(pred: P) -> AllPredicate { - AllPredicate(vec![Box::new(pred)]) -} - -/// Matches if all of supplied predicate matches. -pub struct AllPredicate(Vec>>); - -impl AllPredicate { - /// Add new predicate to list of predicates to check - pub fn and + 'static>(mut self, pred: P) -> Self { - self.0.push(Box::new(pred)); - self - } -} - -impl Predicate for AllPredicate { - fn check(&self, req: &Request, state: &S) -> bool { - for p in &self.0 { - if !p.check(req, state) { - return false; - } - } - true - } -} - -/// Return predicate that matches if supplied predicate does not match. -pub fn Not + 'static>(pred: P) -> NotPredicate { - NotPredicate(Box::new(pred)) -} - -#[doc(hidden)] -pub struct NotPredicate(Box>); - -impl Predicate for NotPredicate { - fn check(&self, req: &Request, state: &S) -> bool { - !self.0.check(req, state) - } -} - -/// Http method predicate -#[doc(hidden)] -pub struct MethodPredicate(http::Method, PhantomData); - -impl Predicate for MethodPredicate { - fn check(&self, req: &Request, _: &S) -> bool { - *req.method() == self.0 - } -} - -/// Predicate to match *GET* http method -pub fn Get() -> MethodPredicate { - MethodPredicate(http::Method::GET, PhantomData) -} - -/// Predicate to match *POST* http method -pub fn Post() -> MethodPredicate { - MethodPredicate(http::Method::POST, PhantomData) -} - -/// Predicate to match *PUT* http method -pub fn Put() -> MethodPredicate { - MethodPredicate(http::Method::PUT, PhantomData) -} - -/// Predicate to match *DELETE* http method -pub fn Delete() -> MethodPredicate { - MethodPredicate(http::Method::DELETE, PhantomData) -} - -/// Predicate to match *HEAD* http method -pub fn Head() -> MethodPredicate { - MethodPredicate(http::Method::HEAD, PhantomData) -} - -/// Predicate to match *OPTIONS* http method -pub fn Options() -> MethodPredicate { - MethodPredicate(http::Method::OPTIONS, PhantomData) -} - -/// Predicate to match *CONNECT* http method -pub fn Connect() -> MethodPredicate { - MethodPredicate(http::Method::CONNECT, PhantomData) -} - -/// Predicate to match *PATCH* http method -pub fn Patch() -> MethodPredicate { - MethodPredicate(http::Method::PATCH, PhantomData) -} - -/// Predicate to match *TRACE* http method -pub fn Trace() -> MethodPredicate { - MethodPredicate(http::Method::TRACE, PhantomData) -} - -/// Predicate to match specified http method -pub fn Method(method: http::Method) -> MethodPredicate { - MethodPredicate(method, PhantomData) -} - -/// Return predicate that matches if request contains specified header and -/// value. -pub fn Header( - name: &'static str, value: &'static str, -) -> HeaderPredicate { - HeaderPredicate( - header::HeaderName::try_from(name).unwrap(), - header::HeaderValue::from_static(value), - PhantomData, - ) -} - -#[doc(hidden)] -pub struct HeaderPredicate(header::HeaderName, header::HeaderValue, PhantomData); - -impl Predicate for HeaderPredicate { - fn check(&self, req: &Request, _: &S) -> bool { - if let Some(val) = req.headers().get(&self.0) { - return val == self.1; - } - false - } -} - -/// Return predicate that matches if request contains specified Host name. -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::{pred, App, HttpResponse}; -/// -/// fn main() { -/// App::new().resource("/index.html", |r| { -/// r.route() -/// .filter(pred::Host("www.rust-lang.org")) -/// .f(|_| HttpResponse::MethodNotAllowed()) -/// }); -/// } -/// ``` -pub fn Host>(host: H) -> HostPredicate { - HostPredicate(host.as_ref().to_string(), None, PhantomData) -} - -#[doc(hidden)] -pub struct HostPredicate(String, Option, PhantomData); - -impl HostPredicate { - /// Set reuest scheme to match - pub fn scheme>(&mut self, scheme: H) { - self.1 = Some(scheme.as_ref().to_string()) - } -} - -impl Predicate for HostPredicate { - fn check(&self, req: &Request, _: &S) -> bool { - let info = req.connection_info(); - if let Some(ref scheme) = self.1 { - self.0 == info.host() && scheme == info.scheme() - } else { - self.0 == info.host() - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use http::{header, Method}; - use test::TestRequest; - - #[test] - fn test_header() { - let req = TestRequest::with_header( - header::TRANSFER_ENCODING, - header::HeaderValue::from_static("chunked"), - ).finish(); - - let pred = Header("transfer-encoding", "chunked"); - assert!(pred.check(&req, req.state())); - - let pred = Header("transfer-encoding", "other"); - assert!(!pred.check(&req, req.state())); - - let pred = Header("content-type", "other"); - assert!(!pred.check(&req, req.state())); - } - - #[test] - fn test_host() { - let req = TestRequest::default() - .header( - header::HOST, - header::HeaderValue::from_static("www.rust-lang.org"), - ).finish(); - - let pred = Host("www.rust-lang.org"); - assert!(pred.check(&req, req.state())); - - let pred = Host("localhost"); - assert!(!pred.check(&req, req.state())); - } - - #[test] - fn test_methods() { - let req = TestRequest::default().finish(); - let req2 = TestRequest::default().method(Method::POST).finish(); - - assert!(Get().check(&req, req.state())); - assert!(!Get().check(&req2, req2.state())); - assert!(Post().check(&req2, req2.state())); - assert!(!Post().check(&req, req.state())); - - let r = TestRequest::default().method(Method::PUT).finish(); - assert!(Put().check(&r, r.state())); - assert!(!Put().check(&req, req.state())); - - let r = TestRequest::default().method(Method::DELETE).finish(); - assert!(Delete().check(&r, r.state())); - assert!(!Delete().check(&req, req.state())); - - let r = TestRequest::default().method(Method::HEAD).finish(); - assert!(Head().check(&r, r.state())); - assert!(!Head().check(&req, req.state())); - - let r = TestRequest::default().method(Method::OPTIONS).finish(); - assert!(Options().check(&r, r.state())); - assert!(!Options().check(&req, req.state())); - - let r = TestRequest::default().method(Method::CONNECT).finish(); - assert!(Connect().check(&r, r.state())); - assert!(!Connect().check(&req, req.state())); - - let r = TestRequest::default().method(Method::PATCH).finish(); - assert!(Patch().check(&r, r.state())); - assert!(!Patch().check(&req, req.state())); - - let r = TestRequest::default().method(Method::TRACE).finish(); - assert!(Trace().check(&r, r.state())); - assert!(!Trace().check(&req, req.state())); - } - - #[test] - fn test_preds() { - let r = TestRequest::default().method(Method::TRACE).finish(); - - assert!(Not(Get()).check(&r, r.state())); - assert!(!Not(Trace()).check(&r, r.state())); - - assert!(All(Trace()).and(Trace()).check(&r, r.state())); - assert!(!All(Get()).and(Trace()).check(&r, r.state())); - - assert!(Any(Get()).or(Trace()).check(&r, r.state())); - assert!(!Any(Get()).or(Get()).check(&r, r.state())); - } -} diff --git a/src/request.rs b/src/request.rs new file mode 100644 index 00000000..571431cc --- /dev/null +++ b/src/request.rs @@ -0,0 +1,174 @@ +use std::cell::{Ref, RefMut}; +use std::fmt; +use std::ops::Deref; +use std::rc::Rc; + +use actix_http::http::{HeaderMap, Method, Uri, Version}; +use actix_http::{Error, Extensions, HttpMessage, Message, Payload, RequestHead}; +use actix_router::{Path, Url}; +use futures::future::{ok, FutureResult}; + +use crate::handler::FromRequest; +use crate::service::ServiceRequest; + +#[derive(Clone)] +pub struct HttpRequest { + head: Message, + pub(crate) path: Path, + extensions: Rc, +} + +impl HttpRequest { + #[inline] + pub fn new( + head: Message, + path: Path, + extensions: Rc, + ) -> HttpRequest { + HttpRequest { + head, + path, + extensions, + } + } +} + +impl HttpRequest { + /// This method returns reference to the request head + #[inline] + pub fn head(&self) -> &RequestHead { + &self.head + } + + /// 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 + } + + /// The target path of this Request. + #[inline] + pub fn path(&self) -> &str { + self.head().uri.path() + } + + #[inline] + /// Returns Request's headers. + pub fn headers(&self) -> &HeaderMap { + &self.head().headers + } + + /// 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() + } + + /// Application extensions + #[inline] + pub fn app_extensions(&self) -> &Extensions { + &self.extensions + } + + // /// Get *ConnectionInfo* for the correct request. + // #[inline] + // pub fn connection_info(&self) -> Ref { + // ConnectionInfo::get(&*self) + // } +} + +impl Deref for HttpRequest { + type Target = RequestHead; + + fn deref(&self) -> &RequestHead { + self.head() + } +} + +impl HttpMessage for HttpRequest { + type Stream = (); + + #[inline] + fn headers(&self) -> &HeaderMap { + self.headers() + } + + #[inline] + fn take_payload(&mut self) -> Payload { + Payload::None + } +} + +impl

FromRequest

for HttpRequest { + type Error = Error; + type Future = FutureResult; + + #[inline] + fn from_request(req: &mut ServiceRequest

) -> Self::Future { + ok(req.clone()) + } +} + +impl fmt::Debug for HttpRequest { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!( + f, + "\nHttpRequest {:?} {}:{}", + self.head.version, + self.head.method, + self.path() + )?; + if !self.query_string().is_empty() { + writeln!(f, " query: ?{:?}", self.query_string())?; + } + if !self.match_info().is_empty() { + writeln!(f, " params: {:?}", self.match_info())?; + } + writeln!(f, " headers:")?; + for (key, val) in self.headers().iter() { + writeln!(f, " {:?}: {:?}", key, val)?; + } + Ok(()) + } +} diff --git a/src/resource.rs b/src/resource.rs index d884dd44..88f7ae5a 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -1,82 +1,65 @@ -use std::ops::Deref; +use std::cell::RefCell; use std::rc::Rc; -use futures::Future; -use http::Method; -use smallvec::SmallVec; +use actix_http::{http::Method, Error, Response}; +use actix_service::{ + ApplyNewService, IntoNewService, IntoNewTransform, NewService, NewTransform, Service, +}; +use futures::future::{ok, Either, FutureResult}; +use futures::{Async, Future, IntoFuture, Poll}; -use error::Error; -use handler::{AsyncResult, FromRequest, Handler, Responder}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::Middleware; -use pred; -use route::Route; -use router::ResourceDef; -use with::WithFactory; +use crate::handler::{AsyncFactory, Factory, FromRequest}; +use crate::helpers::{DefaultNewService, HttpDefaultNewService, HttpDefaultService}; +use crate::responder::Responder; +use crate::route::{CreateRouteService, Route, RouteBuilder, RouteService}; +use crate::service::{ServiceRequest, ServiceResponse}; -#[derive(Copy, Clone)] -pub(crate) struct RouteId(usize); - -/// *Resource* is an entry in route table which corresponds to requested URL. +/// Resource route definition /// -/// Resource in turn has at least one route. -/// Route consists of an object that implements `Handler` trait (handler) -/// and list of predicates (objects that implement `Predicate` trait). /// Route uses builder-like pattern for configuration. -/// During request handling, resource object iterate through all routes -/// and check all predicates for specific route, if request matches all -/// predicates route route considered matched and route handler get called. -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::{App, HttpResponse, http}; -/// -/// fn main() { -/// let app = App::new() -/// .resource( -/// "/", |r| r.method(http::Method::GET).f(|r| HttpResponse::Ok())) -/// .finish(); -/// } -pub struct Resource { - rdef: ResourceDef, - routes: SmallVec<[Route; 3]>, - middlewares: Rc>>>, +/// If handler is not explicitly set, default *404 Not Found* handler is used. +pub struct Resource> { + routes: Vec>, + endpoint: T, + default: Rc< + RefCell, ServiceResponse>>>>, + >, + factory_ref: Rc>>>, } -impl Resource { - /// Create new resource with specified resource definition - pub fn new(rdef: ResourceDef) -> Self { +impl

Resource

{ + pub fn new() -> Resource

{ + let fref = Rc::new(RefCell::new(None)); + Resource { - rdef, - routes: SmallVec::new(), - middlewares: Rc::new(Vec::new()), + routes: Vec::new(), + endpoint: ResourceEndpoint::new(fref.clone()), + factory_ref: fref, + default: Rc::new(RefCell::new(None)), } } +} - /// Name of the resource - pub(crate) fn get_name(&self) -> &str { - self.rdef.name() - } - - /// Set resource name - pub fn name(&mut self, name: &str) { - self.rdef.set_name(name); - } - - /// Resource definition - pub fn rdef(&self) -> &ResourceDef { - &self.rdef +impl

Default for Resource

{ + fn default() -> Self { + Self::new() } } -impl Resource { +impl Resource +where + T: NewService< + Request = ServiceRequest

, + Response = ServiceResponse, + Error = (), + InitError = (), + >, +{ /// Register a new route and return mutable reference to *Route* object. /// *Route* is used for route configuration, i.e. adding predicates, /// setting up handler. /// - /// ```rust - /// # extern crate actix_web; + /// ```rust,ignore /// use actix_web::*; /// /// fn main() { @@ -90,44 +73,72 @@ impl Resource { /// .finish(); /// } /// ``` - pub fn route(&mut self) -> &mut Route { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap() + pub fn route(mut self, f: F) -> Self + where + F: FnOnce(RouteBuilder

) -> Route

, + { + self.routes.push(f(Route::build())); + self } /// Register a new `GET` route. - pub fn get(&mut self) -> &mut Route { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().filter(pred::Get()) + pub fn get(mut self, f: F) -> Self + where + F: Factory + 'static, + I: FromRequest

+ 'static, + R: Responder + 'static, + { + self.routes.push(Route::get().to(f)); + self } /// Register a new `POST` route. - pub fn post(&mut self) -> &mut Route { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().filter(pred::Post()) + pub fn post(mut self, f: F) -> Self + where + F: Factory + 'static, + I: FromRequest

+ 'static, + R: Responder + 'static, + { + self.routes.push(Route::post().to(f)); + self } /// Register a new `PUT` route. - pub fn put(&mut self) -> &mut Route { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().filter(pred::Put()) + pub fn put(mut self, f: F) -> Self + where + F: Factory + 'static, + I: FromRequest

+ 'static, + R: Responder + 'static, + { + self.routes.push(Route::put().to(f)); + self } /// Register a new `DELETE` route. - pub fn delete(&mut self) -> &mut Route { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().filter(pred::Delete()) + pub fn delete(mut self, f: F) -> Self + where + F: Factory + 'static, + I: FromRequest

+ 'static, + R: Responder + 'static, + { + self.routes.push(Route::delete().to(f)); + self } /// Register a new `HEAD` route. - pub fn head(&mut self) -> &mut Route { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().filter(pred::Head()) + pub fn head(mut self, f: F) -> Self + where + F: Factory + 'static, + I: FromRequest

+ 'static, + R: Responder + 'static, + { + self.routes.push(Route::build().method(Method::HEAD).to(f)); + self } /// Register a new route and add method check to route. /// - /// ```rust + /// ```rust,ignore /// # extern crate actix_web; /// use actix_web::*; /// fn index(req: &HttpRequest) -> HttpResponse { unimplemented!() } @@ -137,70 +148,23 @@ impl Resource { /// /// This is shortcut for: /// - /// ```rust + /// ```rust,ignore /// # extern crate actix_web; /// # use actix_web::*; /// # fn index(req: &HttpRequest) -> HttpResponse { unimplemented!() } /// App::new().resource("/", |r| r.route().filter(pred::Get()).f(index)); /// ``` - pub fn method(&mut self, method: Method) -> &mut Route { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().filter(pred::Method(method)) - } - - /// Register a new route and add handler object. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::*; - /// fn handler(req: &HttpRequest) -> HttpResponse { unimplemented!() } - /// - /// App::new().resource("/", |r| r.h(handler)); - /// ``` - /// - /// This is shortcut for: - /// - /// ```rust - /// # extern crate actix_web; - /// # use actix_web::*; - /// # fn handler(req: &HttpRequest) -> HttpResponse { unimplemented!() } - /// App::new().resource("/", |r| r.route().h(handler)); - /// ``` - pub fn h>(&mut self, handler: H) { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().h(handler) - } - - /// Register a new route and add handler function. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::*; - /// fn index(req: &HttpRequest) -> HttpResponse { unimplemented!() } - /// - /// App::new().resource("/", |r| r.f(index)); - /// ``` - /// - /// This is shortcut for: - /// - /// ```rust - /// # extern crate actix_web; - /// # use actix_web::*; - /// # fn index(req: &HttpRequest) -> HttpResponse { unimplemented!() } - /// App::new().resource("/", |r| r.route().f(index)); - /// ``` - pub fn f(&mut self, handler: F) + pub fn method(mut self, method: Method, f: F) -> Self where - F: Fn(&HttpRequest) -> R + 'static, - R: Responder + 'static, + F: FnOnce(RouteBuilder

) -> Route

, { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().f(handler) + self.routes.push(f(Route::build().method(method))); + self } /// Register a new route and add handler. /// - /// ```rust + /// ```rust,ignore /// # extern crate actix_web; /// use actix_web::*; /// fn index(req: HttpRequest) -> HttpResponse { unimplemented!() } @@ -210,25 +174,25 @@ impl Resource { /// /// This is shortcut for: /// - /// ```rust + /// ```rust,ignore /// # extern crate actix_web; /// # use actix_web::*; /// # fn index(req: HttpRequest) -> HttpResponse { unimplemented!() } /// App::new().resource("/", |r| r.route().with(index)); /// ``` - pub fn with(&mut self, handler: F) + pub fn to(mut self, handler: F) -> Self where - F: WithFactory, + F: Factory + 'static, + I: FromRequest

+ 'static, R: Responder + 'static, - T: FromRequest + 'static, { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().with(handler); + self.routes.push(Route::build().to(handler)); + self } /// Register a new route and add async handler. /// - /// ```rust + /// ```rust,ignore /// # extern crate actix_web; /// # extern crate futures; /// use actix_web::*; @@ -243,7 +207,7 @@ impl Resource { /// /// This is shortcut for: /// - /// ```rust + /// ```rust,ignore /// # extern crate actix_web; /// # extern crate futures; /// # use actix_web::*; @@ -253,72 +217,259 @@ impl Resource { /// # } /// App::new().resource("/", |r| r.route().with_async(index)); /// ``` - pub fn with_async(&mut self, handler: F) + #[allow(clippy::wrong_self_convention)] + pub fn to_async(mut self, handler: F) -> Self where - F: Fn(T) -> R + 'static, - R: Future + 'static, - I: Responder + 'static, - E: Into + 'static, - T: FromRequest + 'static, + F: AsyncFactory, + I: FromRequest

+ 'static, + R: IntoFuture + 'static, + R::Item: Into, + R::Error: Into, { - self.routes.push(Route::default()); - self.routes.last_mut().unwrap().with_async(handler); + self.routes.push(Route::build().to_async(handler)); + self } /// Register a resource middleware /// /// This is similar to `App's` middlewares, but /// middlewares get invoked on resource level. - /// - /// *Note* `Middleware::finish()` fires right after response get - /// prepared. It does not wait until body get sent to peer. - pub fn middleware>(&mut self, mw: M) { - Rc::get_mut(&mut self.middlewares) - .unwrap() - .push(Box::new(mw)); + pub fn middleware( + self, + mw: F, + ) -> Resource< + P, + impl NewService< + Request = ServiceRequest

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

, + Response = ServiceResponse, + Error = (), + InitError = (), + >, + F: IntoNewTransform, + { + let endpoint = ApplyNewService::new(mw, self.endpoint); + Resource { + endpoint, + routes: self.routes, + default: self.default, + factory_ref: self.factory_ref, + } } - #[inline] - pub(crate) fn get_route_id(&self, req: &HttpRequest) -> Option { - for idx in 0..self.routes.len() { - if (&self.routes[idx]).check(req) { - return Some(RouteId(idx)); + /// Default resource to be used if no matching route could be found. + pub fn default_resource(mut self, f: F) -> Self + where + F: FnOnce(Resource

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

, + Response = ServiceResponse, + Error = (), + > + 'static, + { + // create and configure default resource + self.default = Rc::new(RefCell::new(Some(Rc::new(Box::new( + DefaultNewService::new(f(Resource::new()).into_new_service()), + ))))); + + self + } + + pub(crate) fn get_default( + &self, + ) -> Rc, ServiceResponse>>>>> + { + self.default.clone() + } +} + +impl IntoNewService for Resource +where + T: NewService< + Request = ServiceRequest

, + Response = ServiceResponse, + Error = (), + InitError = (), + >, +{ + fn into_new_service(self) -> T { + *self.factory_ref.borrow_mut() = Some(ResourceFactory { + routes: self.routes, + default: self.default, + }); + + self.endpoint + } +} + +pub struct ResourceFactory

{ + routes: Vec>, + default: Rc< + RefCell, ServiceResponse>>>>, + >, +} + +impl

NewService for ResourceFactory

{ + type Request = ServiceRequest

; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = ResourceService

; + type Future = CreateResourceService

; + + fn new_service(&self, _: &()) -> Self::Future { + let default_fut = if let Some(ref default) = *self.default.borrow() { + Some(default.new_service(&())) + } else { + None + }; + + CreateResourceService { + fut: self + .routes + .iter() + .map(|route| CreateRouteServiceItem::Future(route.new_service(&()))) + .collect(), + default: None, + default_fut, + } + } +} + +enum CreateRouteServiceItem

{ + Future(CreateRouteService

), + Service(RouteService

), +} + +pub struct CreateResourceService

{ + fut: Vec>, + default: Option, ServiceResponse>>, + default_fut: Option< + Box< + Future< + Item = HttpDefaultService, ServiceResponse>, + Error = (), + >, + >, + >, +} + +impl

Future for CreateResourceService

{ + type Item = ResourceService

; + type Error = (); + + fn poll(&mut self) -> Poll { + let mut done = true; + + if let Some(ref mut fut) = self.default_fut { + match fut.poll()? { + Async::Ready(default) => self.default = Some(default), + Async::NotReady => done = false, } } - None - } - #[inline] - pub(crate) fn handle( - &self, id: RouteId, req: &HttpRequest, - ) -> AsyncResult { - if self.middlewares.is_empty() { - (&self.routes[id.0]).handle(req) + // poll http services + for item in &mut self.fut { + match item { + CreateRouteServiceItem::Future(ref mut fut) => match fut.poll()? { + Async::Ready(route) => { + *item = CreateRouteServiceItem::Service(route) + } + Async::NotReady => { + done = false; + } + }, + CreateRouteServiceItem::Service(_) => continue, + }; + } + + if done { + let routes = self + .fut + .drain(..) + .map(|item| match item { + CreateRouteServiceItem::Service(service) => service, + CreateRouteServiceItem::Future(_) => unreachable!(), + }) + .collect(); + Ok(Async::Ready(ResourceService { + routes, + default: self.default.take(), + })) } else { - (&self.routes[id.0]).compose(req.clone(), Rc::clone(&self.middlewares)) + Ok(Async::NotReady) } } } -/// Default resource -pub struct DefaultResource(Rc>); +pub struct ResourceService

{ + routes: Vec>, + default: Option, ServiceResponse>>, +} -impl Deref for DefaultResource { - type Target = Resource; +impl

Service for ResourceService

{ + type Request = ServiceRequest

; + type Response = ServiceResponse; + type Error = (); + type Future = Either< + Box>, + Either< + Box>, + FutureResult, + >, + >; - fn deref(&self) -> &Resource { - self.0.as_ref() + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + 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)); + } + } + if let Some(ref mut default) = self.default { + Either::B(Either::A(default.call(req))) + } else { + let req = req.into_request(); + Either::B(Either::B(ok(ServiceResponse::new( + req, + Response::NotFound().finish(), + )))) + } } } -impl Clone for DefaultResource { - fn clone(&self) -> Self { - DefaultResource(self.0.clone()) +#[doc(hidden)] +pub struct ResourceEndpoint

{ + factory: Rc>>>, +} + +impl

ResourceEndpoint

{ + fn new(factory: Rc>>>) -> Self { + ResourceEndpoint { factory } } } -impl From> for DefaultResource { - fn from(res: Resource) -> Self { - DefaultResource(Rc::new(res)) +impl

NewService for ResourceEndpoint

{ + type Request = ServiceRequest

; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = ResourceService

; + type Future = CreateResourceService

; + + fn new_service(&self, _: &()) -> Self::Future { + self.factory.borrow_mut().as_mut().unwrap().new_service(&()) } } diff --git a/src/responder.rs b/src/responder.rs new file mode 100644 index 00000000..5520c610 --- /dev/null +++ b/src/responder.rs @@ -0,0 +1,259 @@ +use actix_http::dev::ResponseBuilder; +use actix_http::http::StatusCode; +use actix_http::{Error, Response}; +use bytes::{Bytes, BytesMut}; +use futures::future::{err, ok, Either as EitherFuture, FutureResult}; +use futures::{Future, Poll}; + +use crate::request::HttpRequest; + +/// Trait implemented by types that generate http responses. +/// +/// Types that implement this trait can be used as the return type of a handler. +pub trait Responder { + /// The associated error which can be returned. + type Error: Into; + + /// The future response value. + type Future: Future; + + /// Convert itself to `AsyncResult` or `Error`. + fn respond_to(self, req: &HttpRequest) -> Self::Future; +} + +impl Responder for Response { + type Error = Error; + type Future = FutureResult; + + #[inline] + fn respond_to(self, _: &HttpRequest) -> Self::Future { + ok(self) + } +} + +impl Responder for Option +where + T: Responder, +{ + type Error = T::Error; + type Future = EitherFuture>; + + fn respond_to(self, req: &HttpRequest) -> Self::Future { + match self { + Some(t) => EitherFuture::A(t.respond_to(req)), + None => EitherFuture::B(ok(Response::build(StatusCode::NOT_FOUND).finish())), + } + } +} + +impl Responder for Result +where + T: Responder, + E: Into, +{ + type Error = Error; + type Future = EitherFuture, FutureResult>; + + fn respond_to(self, req: &HttpRequest) -> Self::Future { + match self { + Ok(val) => EitherFuture::A(ResponseFuture::new(val.respond_to(req))), + Err(e) => EitherFuture::B(err(e.into())), + } + } +} + +impl Responder for ResponseBuilder { + type Error = Error; + type Future = FutureResult; + + #[inline] + fn respond_to(mut self, _: &HttpRequest) -> Self::Future { + ok(self.finish()) + } +} + +impl Responder for &'static str { + type Error = Error; + type Future = FutureResult; + + fn respond_to(self, _: &HttpRequest) -> Self::Future { + ok(Response::build(StatusCode::OK) + .content_type("text/plain; charset=utf-8") + .body(self)) + } +} + +impl Responder for &'static [u8] { + type Error = Error; + type Future = FutureResult; + + fn respond_to(self, _: &HttpRequest) -> Self::Future { + ok(Response::build(StatusCode::OK) + .content_type("application/octet-stream") + .body(self)) + } +} + +impl Responder for String { + type Error = Error; + type Future = FutureResult; + + fn respond_to(self, _: &HttpRequest) -> Self::Future { + ok(Response::build(StatusCode::OK) + .content_type("text/plain; charset=utf-8") + .body(self)) + } +} + +impl<'a> Responder for &'a String { + type Error = Error; + type Future = FutureResult; + + fn respond_to(self, _: &HttpRequest) -> Self::Future { + ok(Response::build(StatusCode::OK) + .content_type("text/plain; charset=utf-8") + .body(self)) + } +} + +impl Responder for Bytes { + type Error = Error; + type Future = FutureResult; + + fn respond_to(self, _: &HttpRequest) -> Self::Future { + ok(Response::build(StatusCode::OK) + .content_type("application/octet-stream") + .body(self)) + } +} + +impl Responder for BytesMut { + type Error = Error; + type Future = FutureResult; + + fn respond_to(self, _: &HttpRequest) -> Self::Future { + ok(Response::build(StatusCode::OK) + .content_type("application/octet-stream") + .body(self)) + } +} + +/// Combines two different responder types into a single type +/// +/// ```rust,ignore +/// # extern crate actix_web; +/// # extern crate futures; +/// # use futures::future::Future; +/// use actix_web::{AsyncResponder, Either, Error, Request, Response}; +/// use futures::future::result; +/// +/// type RegisterResult = +/// Either>>; +/// +/// fn index(req: Request) -> RegisterResult { +/// if is_a_variant() { +/// // <- choose variant A +/// Either::A(Response::BadRequest().body("Bad data")) +/// } else { +/// Either::B( +/// // <- variant B +/// result(Ok(Response::Ok() +/// .content_type("text/html") +/// .body("Hello!"))) +/// .responder(), +/// ) +/// } +/// } +/// # fn is_a_variant() -> bool { true } +/// # fn main() {} +/// ``` +pub enum Either { + /// First branch of the type + A(A), + /// Second branch of the type + B(B), +} + +impl Responder for Either +where + A: Responder, + B: Responder, +{ + type Error = Error; + type Future = EitherResponder; + + fn respond_to(self, req: &HttpRequest) -> Self::Future { + match self { + Either::A(a) => EitherResponder::A(a.respond_to(req)), + Either::B(b) => EitherResponder::B(b.respond_to(req)), + } + } +} + +pub enum EitherResponder +where + A: Future, + A::Error: Into, + B: Future, + B::Error: Into, +{ + A(A), + B(B), +} + +impl Future for EitherResponder +where + A: Future, + A::Error: Into, + B: Future, + B::Error: Into, +{ + type Item = Response; + type Error = Error; + + fn poll(&mut self) -> Poll { + match self { + EitherResponder::A(ref mut fut) => Ok(fut.poll().map_err(|e| e.into())?), + EitherResponder::B(ref mut fut) => Ok(fut.poll().map_err(|e| e.into())?), + } + } +} + +impl Responder for Box> +where + I: Responder + 'static, + E: Into + 'static, +{ + type Error = Error; + type Future = Box>; + + #[inline] + fn respond_to(self, req: &HttpRequest) -> Self::Future { + let req = req.clone(); + Box::new( + self.map_err(|e| e.into()) + .and_then(move |r| ResponseFuture(r.respond_to(&req))), + ) + } +} + +pub struct ResponseFuture(T); + +impl ResponseFuture { + pub fn new(fut: T) -> Self { + ResponseFuture(fut) + } +} + +impl Future for ResponseFuture +where + T: Future, + T::Error: Into, +{ + type Item = Response; + type Error = Error; + + fn poll(&mut self) -> Poll { + Ok(self.0.poll().map_err(|e| e.into())?) + } +} diff --git a/src/route.rs b/src/route.rs index 884a367e..574e8e34 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,68 +1,153 @@ use std::marker::PhantomData; use std::rc::Rc; -use futures::{Async, Future, Poll}; +use actix_http::{http::Method, Error, Response}; +use actix_service::{NewService, Service}; +use futures::{Async, Future, IntoFuture, Poll}; -use error::Error; -use handler::{ - AsyncHandler, AsyncResult, AsyncResultItem, FromRequest, Handler, Responder, - RouteHandler, WrapHandler, -}; -use http::StatusCode; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{ - Finished as MiddlewareFinished, Middleware, Response as MiddlewareResponse, - Started as MiddlewareStarted, -}; -use pred::Predicate; -use with::{WithAsyncFactory, WithFactory}; +use crate::filter::{self, Filter}; +use crate::handler::{AsyncFactory, AsyncHandle, Extract, Factory, FromRequest, Handle}; +use crate::responder::Responder; +use crate::service::{ServiceRequest, ServiceResponse}; + +type BoxedRouteService = Box< + Service< + Request = Req, + Response = Res, + Error = (), + Future = Box>, + >, +>; + +type BoxedRouteNewService = Box< + NewService< + Request = Req, + Response = Res, + Error = (), + InitError = (), + Service = BoxedRouteService, + Future = Box, Error = ()>>, + >, +>; /// Resource route definition /// /// Route uses builder-like pattern for configuration. /// If handler is not explicitly set, default *404 Not Found* handler is used. -pub struct Route { - preds: Vec>>, - handler: InnerHandler, +pub struct Route

{ + service: BoxedRouteNewService, ServiceResponse>, + filters: Rc>>, } -impl Default for Route { - fn default() -> Route { - Route { - preds: Vec::new(), - handler: InnerHandler::new(|_: &_| HttpResponse::new(StatusCode::NOT_FOUND)), +impl Route

{ + pub fn build() -> RouteBuilder

{ + RouteBuilder::new() + } + + pub fn get() -> RouteBuilder

{ + RouteBuilder::new().method(Method::GET) + } + + pub fn post() -> RouteBuilder

{ + RouteBuilder::new().method(Method::POST) + } + + pub fn put() -> RouteBuilder

{ + RouteBuilder::new().method(Method::PUT) + } + + pub fn delete() -> RouteBuilder

{ + RouteBuilder::new().method(Method::DELETE) + } +} + +impl

NewService for Route

{ + type Request = ServiceRequest

; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = RouteService

; + type Future = CreateRouteService

; + + fn new_service(&self, _: &()) -> Self::Future { + CreateRouteService { + fut: self.service.new_service(&()), + filters: self.filters.clone(), } } } -impl Route { - #[inline] - pub(crate) fn check(&self, req: &HttpRequest) -> bool { - let state = req.state(); - for pred in &self.preds { - if !pred.check(req, state) { +type RouteFuture

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

{ + fut: RouteFuture

, + filters: Rc>>, +} + +impl

Future for CreateRouteService

{ + type Item = RouteService

; + type Error = (); + + fn poll(&mut self) -> Poll { + match self.fut.poll()? { + Async::Ready(service) => Ok(Async::Ready(RouteService { + service, + filters: self.filters.clone(), + })), + Async::NotReady => Ok(Async::NotReady), + } + } +} + +pub struct RouteService

{ + service: BoxedRouteService, ServiceResponse>, + filters: Rc>>, +} + +impl

RouteService

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

) -> bool { + for f in self.filters.iter() { + if !f.check(req.request()) { return false; } } true } +} - #[inline] - pub(crate) fn handle(&self, req: &HttpRequest) -> AsyncResult { - self.handler.handle(req) +impl

Service for RouteService

{ + type Request = ServiceRequest

; + type Response = ServiceResponse; + type Error = (); + type Future = Box>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.service.poll_ready() } - #[inline] - pub(crate) fn compose( - &self, req: HttpRequest, mws: Rc>>>, - ) -> AsyncResult { - AsyncResult::future(Box::new(Compose::new(req, mws, self.handler.clone()))) + fn call(&mut self, req: Self::Request) -> Self::Future { + self.service.call(req) + } +} + +pub struct RouteBuilder

{ + filters: Vec>, + _t: PhantomData

, +} + +impl RouteBuilder

{ + fn new() -> RouteBuilder

{ + RouteBuilder { + filters: Vec::new(), + _t: PhantomData, + } } - /// Add match predicate to route. + /// Add method match filter to the route. /// - /// ```rust + /// ```rust,ignore /// # extern crate actix_web; /// # use actix_web::*; /// # fn main() { @@ -75,41 +160,52 @@ impl Route { /// # .finish(); /// # } /// ``` - pub fn filter + 'static>(&mut self, p: T) -> &mut Self { - self.preds.push(Box::new(p)); + pub fn method(mut self, method: Method) -> Self { + self.filters.push(Box::new(filter::Method(method))); self } - /// Set handler object. Usually call to this method is last call - /// during route configuration, so it does not return reference to self. - pub fn h>(&mut self, handler: H) { - self.handler = InnerHandler::new(handler); + /// Add filter to the route. + /// + /// ```rust,ignore + /// # extern crate actix_web; + /// # use actix_web::*; + /// # fn main() { + /// App::new().resource("/path", |r| { + /// r.route() + /// .filter(pred::Get()) + /// .filter(pred::Header("content-type", "text/plain")) + /// .f(|req| HttpResponse::Ok()) + /// }) + /// # .finish(); + /// # } + /// ``` + pub fn filter(&mut self, f: F) -> &mut Self { + self.filters.push(Box::new(f)); + self } - /// Set handler function. Usually call to this method is last call - /// during route configuration, so it does not return reference to self. - pub fn f(&mut self, handler: F) - where - F: Fn(&HttpRequest) -> R + 'static, - R: Responder + 'static, - { - self.handler = InnerHandler::new(handler); - } - - /// Set async handler function. - pub fn a(&mut self, handler: H) - where - H: Fn(&HttpRequest) -> F + 'static, - F: Future + 'static, - R: Responder + 'static, - E: Into + 'static, - { - self.handler = InnerHandler::async(handler); - } + // pub fn map>( + // self, + // md: F, + // ) -> RouteServiceBuilder + // where + // T: NewService< + // Request = HandlerRequest, + // Response = HandlerRequest, + // InitError = (), + // >, + // { + // RouteServiceBuilder { + // service: md.into_new_service(), + // filters: self.filters, + // _t: PhantomData, + // } + // } /// Set handler function, use request extractor for parameters. /// - /// ```rust + /// ```rust,ignore /// # extern crate bytes; /// # extern crate actix_web; /// # extern crate futures; @@ -136,7 +232,7 @@ impl Route { /// /// It is possible to use multiple extractors for one handler function. /// - /// ```rust + /// ```rust,ignore /// # extern crate bytes; /// # extern crate actix_web; /// # extern crate futures; @@ -163,56 +259,25 @@ impl Route { /// ); // <- use `with` extractor /// } /// ``` - pub fn with(&mut self, handler: F) + pub fn to(self, handler: F) -> Route

where - F: WithFactory + 'static, + F: Factory + 'static, + T: FromRequest

+ 'static, R: Responder + 'static, - T: FromRequest + 'static, { - self.h(handler.create()); - } - - /// Set handler function. Same as `.with()` but it allows to configure - /// extractor. Configuration closure accepts config objects as tuple. - /// - /// ```rust - /// # extern crate bytes; - /// # extern crate actix_web; - /// # extern crate futures; - /// #[macro_use] extern crate serde_derive; - /// use actix_web::{http, App, Path, Result}; - /// - /// /// extract text data from request - /// fn index(body: String) -> Result { - /// Ok(format!("Body {}!", body)) - /// } - /// - /// fn main() { - /// let app = App::new().resource("/index.html", |r| { - /// r.method(http::Method::GET) - /// .with_config(index, |cfg| { // <- register handler - /// cfg.0.limit(4096); // <- limit size of the payload - /// }) - /// }); - /// } - /// ``` - pub fn with_config(&mut self, handler: F, cfg_f: C) - where - F: WithFactory, - R: Responder + 'static, - T: FromRequest + 'static, - C: FnOnce(&mut T::Config), - { - let mut cfg = ::default(); - cfg_f(&mut cfg); - self.h(handler.create_with_config(cfg)); + Route { + service: Box::new(RouteNewService::new( + Extract::new().and_then(Handle::new(handler).map_err(|_| panic!())), + )), + filters: Rc::new(self.filters), + } } /// Set async handler function, use request extractor for parameters. /// Also this method needs to be used if your handler function returns /// `impl Future<>` /// - /// ```rust + /// ```rust,ignore /// # extern crate bytes; /// # extern crate actix_web; /// # extern crate futures; @@ -237,430 +302,233 @@ impl Route { /// ); // <- use `with` extractor /// } /// ``` - pub fn with_async(&mut self, handler: F) + #[allow(clippy::wrong_self_convention)] + pub fn to_async(self, handler: F) -> Route

where - F: WithAsyncFactory, - R: Future + 'static, - I: Responder + 'static, - E: Into + 'static, - T: FromRequest + 'static, + F: AsyncFactory, + T: FromRequest

+ 'static, + R: IntoFuture + 'static, + R::Item: Into, + R::Error: Into, { - self.h(handler.create()); - } - - /// Set async handler function, use request extractor for parameters. - /// This method allows to configure extractor. Configuration closure - /// accepts config objects as tuple. - /// - /// ```rust - /// # extern crate bytes; - /// # extern crate actix_web; - /// # extern crate futures; - /// #[macro_use] extern crate serde_derive; - /// use actix_web::{http, App, Error, Form}; - /// use futures::Future; - /// - /// #[derive(Deserialize)] - /// struct Info { - /// username: String, - /// } - /// - /// /// extract path info using serde - /// fn index(info: Form) -> Box> { - /// unimplemented!() - /// } - /// - /// fn main() { - /// let app = App::new().resource( - /// "/{username}/index.html", // <- define path parameters - /// |r| r.method(http::Method::GET) - /// .with_async_config(index, |cfg| { - /// cfg.0.limit(4096); - /// }), - /// ); // <- use `with` extractor - /// } - /// ``` - pub fn with_async_config(&mut self, handler: F, cfg: C) - where - F: WithAsyncFactory, - R: Future + 'static, - I: Responder + 'static, - E: Into + 'static, - T: FromRequest + 'static, - C: FnOnce(&mut T::Config), - { - let mut extractor_cfg = ::default(); - cfg(&mut extractor_cfg); - self.h(handler.create_with_config(extractor_cfg)); - } -} - -/// `RouteHandler` wrapper. This struct is required because it needs to be -/// shared for resource level middlewares. -struct InnerHandler(Rc>>); - -impl InnerHandler { - #[inline] - fn new>(h: H) -> Self { - InnerHandler(Rc::new(Box::new(WrapHandler::new(h)))) - } - - #[inline] - fn async(h: H) -> Self - where - H: Fn(&HttpRequest) -> F + 'static, - F: Future + 'static, - R: Responder + 'static, - E: Into + 'static, - { - InnerHandler(Rc::new(Box::new(AsyncHandler::new(h)))) - } - - #[inline] - pub fn handle(&self, req: &HttpRequest) -> AsyncResult { - self.0.handle(req) - } -} - -impl Clone for InnerHandler { - #[inline] - fn clone(&self) -> Self { - InnerHandler(Rc::clone(&self.0)) - } -} - -/// Compose resource level middlewares with route handler. -struct Compose { - info: ComposeInfo, - state: ComposeState, -} - -struct ComposeInfo { - count: usize, - req: HttpRequest, - mws: Rc>>>, - handler: InnerHandler, -} - -enum ComposeState { - Starting(StartMiddlewares), - Handler(WaitingResponse), - RunMiddlewares(RunMiddlewares), - Finishing(FinishingMiddlewares), - Completed(Response), -} - -impl ComposeState { - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - match *self { - ComposeState::Starting(ref mut state) => state.poll(info), - ComposeState::Handler(ref mut state) => state.poll(info), - ComposeState::RunMiddlewares(ref mut state) => state.poll(info), - ComposeState::Finishing(ref mut state) => state.poll(info), - ComposeState::Completed(_) => None, + Route { + service: Box::new(RouteNewService::new( + Extract::new().and_then(AsyncHandle::new(handler).map_err(|_| panic!())), + )), + filters: Rc::new(self.filters), } } } -impl Compose { - fn new( - req: HttpRequest, mws: Rc>>>, handler: InnerHandler, - ) -> Self { - let mut info = ComposeInfo { - count: 0, - req, - mws, - handler, - }; - let state = StartMiddlewares::init(&mut info); +pub struct RouteServiceBuilder { + service: T, + filters: Vec>, + _t: PhantomData<(P, U1, U2)>, +} - Compose { state, info } +// impl RouteServiceBuilder +// where +// T: NewService< +// Request = HandlerRequest, +// Response = HandlerRequest, +// Error = Error, +// InitError = (), +// >, +// { +// pub fn new>(factory: F) -> Self { +// RouteServiceBuilder { +// service: factory.into_new_service(), +// filters: Vec::new(), +// _t: PhantomData, +// } +// } + +// /// Add method match filter to the route. +// /// +// /// ```rust +// /// # extern crate actix_web; +// /// # use actix_web::*; +// /// # fn main() { +// /// App::new().resource("/path", |r| { +// /// r.route() +// /// .filter(pred::Get()) +// /// .filter(pred::Header("content-type", "text/plain")) +// /// .f(|req| HttpResponse::Ok()) +// /// }) +// /// # .finish(); +// /// # } +// /// ``` +// pub fn method(mut self, method: Method) -> Self { +// self.filters.push(Box::new(filter::Method(method))); +// self +// } + +// /// Add filter to the route. +// /// +// /// ```rust +// /// # extern crate actix_web; +// /// # use actix_web::*; +// /// # fn main() { +// /// App::new().resource("/path", |r| { +// /// r.route() +// /// .filter(pred::Get()) +// /// .filter(pred::Header("content-type", "text/plain")) +// /// .f(|req| HttpResponse::Ok()) +// /// }) +// /// # .finish(); +// /// # } +// /// ``` +// pub fn filter + 'static>(&mut self, f: F) -> &mut Self { +// self.filters.push(Box::new(f)); +// self +// } + +// pub fn map>( +// self, +// md: F, +// ) -> RouteServiceBuilder< +// impl NewService< +// Request = HandlerRequest, +// Response = HandlerRequest, +// Error = Error, +// InitError = (), +// >, +// S, +// U1, +// U2, +// > +// where +// T1: NewService< +// Request = HandlerRequest, +// Response = HandlerRequest, +// InitError = (), +// >, +// T1::Error: Into, +// { +// RouteServiceBuilder { +// service: self +// .service +// .and_then(md.into_new_service().map_err(|e| e.into())), +// filters: self.filters, +// _t: PhantomData, +// } +// } + +// pub fn to_async(self, handler: F) -> Route +// where +// F: AsyncFactory, +// P: FromRequest + 'static, +// R: IntoFuture, +// R::Item: Into, +// R::Error: Into, +// { +// Route { +// service: self +// .service +// .and_then(Extract::new(P::Config::default())) +// .then(AsyncHandle::new(handler)), +// filters: Rc::new(self.filters), +// } +// } + +// pub fn to(self, handler: F) -> Route +// where +// F: Factory + 'static, +// P: FromRequest + 'static, +// R: Responder + 'static, +// { +// Route { +// service: Box::new(RouteNewService::new( +// self.service +// .and_then(Extract::new(P::Config::default())) +// .and_then(Handle::new(handler)), +// )), +// filters: Rc::new(self.filters), +// } +// } +// } + +struct RouteNewService +where + T: NewService, Error = (Error, ServiceRequest

)>, +{ + service: T, +} + +impl RouteNewService +where + T: NewService< + Request = ServiceRequest

, + Response = ServiceResponse, + Error = (Error, ServiceRequest

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

, + Response = ServiceResponse, + Error = (Error, ServiceRequest

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

; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = BoxedRouteService; + type Future = Box>; - fn poll(&mut self) -> Poll { - loop { - if let ComposeState::Completed(ref mut resp) = self.state { - let resp = resp.resp.take().unwrap(); - return Ok(Async::Ready(resp)); - } - if let Some(state) = self.state.poll(&mut self.info) { - self.state = state; - } else { - return Ok(Async::NotReady); - } - } + fn new_service(&self, _: &()) -> Self::Future { + Box::new( + self.service + .new_service(&()) + .map_err(|_| ()) + .and_then(|service| { + let service: BoxedRouteService<_, _> = + Box::new(RouteServiceWrapper { service }); + Ok(service) + }), + ) } } -/// Middlewares start executor -struct StartMiddlewares { - fut: Option, - _s: PhantomData, +struct RouteServiceWrapper>> { + service: T, } -type Fut = Box, Error = Error>>; +impl Service for RouteServiceWrapper +where + T::Future: 'static, + T: Service< + Request = ServiceRequest

, + Response = ServiceResponse, + Error = (Error, ServiceRequest

), + >, +{ + type Request = ServiceRequest

; + type Response = ServiceResponse; + type Error = (); + type Future = Box>; -impl StartMiddlewares { - fn init(info: &mut ComposeInfo) -> ComposeState { - let len = info.mws.len(); - - loop { - if info.count == len { - let reply = info.handler.handle(&info.req); - return WaitingResponse::init(info, reply); - } else { - let result = info.mws[info.count].start(&info.req); - match result { - Ok(MiddlewareStarted::Done) => info.count += 1, - Ok(MiddlewareStarted::Response(resp)) => { - return RunMiddlewares::init(info, resp); - } - Ok(MiddlewareStarted::Future(fut)) => { - return ComposeState::Starting(StartMiddlewares { - fut: Some(fut), - _s: PhantomData, - }); - } - Err(err) => { - return RunMiddlewares::init(info, err.into()); - } - } - } - } + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.service.poll_ready().map_err(|_| ()) } - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - let len = info.mws.len(); - - 'outer: loop { - match self.fut.as_mut().unwrap().poll() { - Ok(Async::NotReady) => { - return None; - } - Ok(Async::Ready(resp)) => { - info.count += 1; - if let Some(resp) = resp { - return Some(RunMiddlewares::init(info, resp)); - } - loop { - if info.count == len { - let reply = info.handler.handle(&info.req); - return Some(WaitingResponse::init(info, reply)); - } else { - let result = info.mws[info.count].start(&info.req); - match result { - Ok(MiddlewareStarted::Done) => info.count += 1, - Ok(MiddlewareStarted::Response(resp)) => { - return Some(RunMiddlewares::init(info, resp)); - } - Ok(MiddlewareStarted::Future(fut)) => { - self.fut = Some(fut); - continue 'outer; - } - Err(err) => { - return Some(RunMiddlewares::init(info, err.into())); - } - } - } - } - } - Err(err) => { - return Some(RunMiddlewares::init(info, err.into())); - } - } - } - } -} - -type HandlerFuture = Future; - -// waiting for response -struct WaitingResponse { - fut: Box, - _s: PhantomData, -} - -impl WaitingResponse { - #[inline] - fn init( - info: &mut ComposeInfo, reply: AsyncResult, - ) -> ComposeState { - match reply.into() { - AsyncResultItem::Ok(resp) => RunMiddlewares::init(info, resp), - AsyncResultItem::Err(err) => RunMiddlewares::init(info, err.into()), - AsyncResultItem::Future(fut) => ComposeState::Handler(WaitingResponse { - fut, - _s: PhantomData, - }), - } - } - - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - match self.fut.poll() { - Ok(Async::NotReady) => None, - Ok(Async::Ready(resp)) => Some(RunMiddlewares::init(info, resp)), - Err(err) => Some(RunMiddlewares::init(info, err.into())), - } - } -} - -/// Middlewares response executor -struct RunMiddlewares { - curr: usize, - fut: Option>>, - _s: PhantomData, -} - -impl RunMiddlewares { - fn init(info: &mut ComposeInfo, mut resp: HttpResponse) -> ComposeState { - let mut curr = 0; - let len = info.mws.len(); - - loop { - let state = info.mws[curr].response(&info.req, resp); - resp = match state { - Err(err) => { - info.count = curr + 1; - return FinishingMiddlewares::init(info, err.into()); - } - Ok(MiddlewareResponse::Done(r)) => { - curr += 1; - if curr == len { - return FinishingMiddlewares::init(info, r); - } else { - r - } - } - Ok(MiddlewareResponse::Future(fut)) => { - return ComposeState::RunMiddlewares(RunMiddlewares { - curr, - fut: Some(fut), - _s: PhantomData, - }); - } - }; - } - } - - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - let len = info.mws.len(); - - loop { - // poll latest fut - let mut resp = match self.fut.as_mut().unwrap().poll() { - Ok(Async::NotReady) => return None, - Ok(Async::Ready(resp)) => { - self.curr += 1; - resp - } - Err(err) => return Some(FinishingMiddlewares::init(info, err.into())), - }; - - loop { - if self.curr == len { - return Some(FinishingMiddlewares::init(info, resp)); - } else { - let state = info.mws[self.curr].response(&info.req, resp); - match state { - Err(err) => { - return Some(FinishingMiddlewares::init(info, err.into())) - } - Ok(MiddlewareResponse::Done(r)) => { - self.curr += 1; - resp = r - } - Ok(MiddlewareResponse::Future(fut)) => { - self.fut = Some(fut); - break; - } - } - } - } - } - } -} - -/// Middlewares start executor -struct FinishingMiddlewares { - resp: Option, - fut: Option>>, - _s: PhantomData, -} - -impl FinishingMiddlewares { - fn init(info: &mut ComposeInfo, resp: HttpResponse) -> ComposeState { - if info.count == 0 { - Response::init(resp) - } else { - let mut state = FinishingMiddlewares { - resp: Some(resp), - fut: None, - _s: PhantomData, - }; - if let Some(st) = state.poll(info) { - st - } else { - ComposeState::Finishing(state) - } - } - } - - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - loop { - // poll latest fut - let not_ready = if let Some(ref mut fut) = self.fut { - match fut.poll() { - Ok(Async::NotReady) => true, - Ok(Async::Ready(())) => false, - Err(err) => { - error!("Middleware finish error: {}", err); - false - } - } - } else { - false - }; - if not_ready { - return None; - } - self.fut = None; - if info.count == 0 { - return Some(Response::init(self.resp.take().unwrap())); - } - - info.count -= 1; - - let state = info.mws[info.count as usize] - .finish(&info.req, self.resp.as_ref().unwrap()); - match state { - MiddlewareFinished::Done => { - if info.count == 0 { - return Some(Response::init(self.resp.take().unwrap())); - } - } - MiddlewareFinished::Future(fut) => { - self.fut = Some(fut); - } - } - } - } -} - -struct Response { - resp: Option, - _s: PhantomData, -} - -impl Response { - fn init(resp: HttpResponse) -> ComposeState { - ComposeState::Completed(Response { - resp: Some(resp), - _s: PhantomData, - }) + 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)), + })) } } diff --git a/src/router.rs b/src/router.rs deleted file mode 100644 index aa15e46d..00000000 --- a/src/router.rs +++ /dev/null @@ -1,1247 +0,0 @@ -use std::cell::RefCell; -use std::cmp::min; -use std::collections::HashMap; -use std::hash::{Hash, Hasher}; -use std::rc::Rc; - -use regex::{escape, Regex}; -use url::Url; - -use error::UrlGenerationError; -use handler::{AsyncResult, FromRequest, Responder, RouteHandler}; -use http::{Method, StatusCode}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use param::{ParamItem, Params}; -use pred::Predicate; -use resource::{DefaultResource, Resource}; -use scope::Scope; -use server::Request; -use with::WithFactory; - -#[derive(Debug, Copy, Clone, PartialEq)] -pub(crate) enum ResourceId { - Default, - Normal(u16), -} - -enum ResourcePattern { - Resource(ResourceDef), - Handler(ResourceDef, Option>>>), - Scope(ResourceDef, Vec>>), -} - -enum ResourceItem { - Resource(Resource), - Handler(Box>), - Scope(Scope), -} - -/// Interface for application router. -pub struct Router { - rmap: Rc, - patterns: Vec>, - resources: Vec>, - default: Option>, -} - -/// Information about current resource -#[derive(Clone)] -pub struct ResourceInfo { - rmap: Rc, - resource: ResourceId, - params: Params, - prefix: u16, -} - -impl ResourceInfo { - /// Name os the resource - #[inline] - pub fn name(&self) -> &str { - if let ResourceId::Normal(idx) = self.resource { - self.rmap.patterns[idx as usize].0.name() - } else { - "" - } - } - - /// This method returns reference to matched `ResourceDef` object. - #[inline] - pub fn rdef(&self) -> Option<&ResourceDef> { - if let ResourceId::Normal(idx) = self.resource { - Some(&self.rmap.patterns[idx as usize].0) - } else { - None - } - } - - pub(crate) fn set_prefix(&mut self, prefix: u16) { - self.prefix = prefix; - } - - /// Get a reference to the Params object. - /// - /// 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) -> &Params { - &self.params - } - - #[inline] - pub(crate) fn merge(&mut self, info: &ResourceInfo) { - let mut p = info.params.clone(); - p.set_tail(self.params.tail); - for item in &self.params.segments { - p.add(item.0.clone(), item.1.clone()); - } - - self.prefix = info.params.tail; - self.params = p; - } - - /// Generate url for named resource - /// - /// Check [`HttpRequest::url_for()`](../struct.HttpRequest.html#method. - /// url_for) for detailed information. - pub fn url_for( - &self, req: &Request, name: &str, elements: U, - ) -> Result - where - U: IntoIterator, - I: AsRef, - { - let mut path = String::new(); - let mut elements = elements.into_iter(); - - if self - .rmap - .patterns_for(name, &mut path, &mut elements)? - .is_some() - { - if path.starts_with('/') { - let conn = req.connection_info(); - Ok(Url::parse(&format!( - "{}://{}{}", - conn.scheme(), - conn.host(), - path - ))?) - } else { - Ok(Url::parse(&path)?) - } - } else { - Err(UrlGenerationError::ResourceNotFound) - } - } - - /// Check if application contains matching resource. - /// - /// This method does not take `prefix` into account. - /// For example if prefix is `/test` and router contains route `/name`, - /// following path would be recognizable `/test/name` but `has_resource()` call - /// would return `false`. - pub fn has_resource(&self, path: &str) -> bool { - self.rmap.has_resource(path) - } - - /// Check if application contains matching resource. - /// - /// This method does take `prefix` into account - /// but behaves like `has_route` in case `prefix` is not set in the router. - /// - /// For example if prefix is `/test` and router contains route `/name`, the - /// following path would be recognizable `/test/name` and `has_prefixed_route()` call - /// would return `true`. - /// It will not match against prefix in case it's not given. For example for `/name` - /// with a `/test` prefix would return `false` - pub fn has_prefixed_resource(&self, path: &str) -> bool { - let prefix = self.prefix as usize; - if prefix >= path.len() { - return false; - } - self.rmap.has_resource(&path[prefix..]) - } -} - -pub(crate) struct ResourceMap { - root: ResourceDef, - parent: RefCell>>, - named: HashMap, - patterns: Vec<(ResourceDef, Option>)>, - nested: Vec>, -} - -impl ResourceMap { - fn has_resource(&self, path: &str) -> bool { - let path = if path.is_empty() { "/" } else { path }; - - for (pattern, rmap) in &self.patterns { - if let Some(ref rmap) = rmap { - if let Some(plen) = pattern.is_prefix_match(path) { - return rmap.has_resource(&path[plen..]); - } - } else if pattern.is_match(path) { - return true; - } - } - false - } - - fn patterns_for( - &self, name: &str, path: &mut String, elements: &mut U, - ) -> Result, UrlGenerationError> - where - U: Iterator, - I: AsRef, - { - if self.pattern_for(name, path, elements)?.is_some() { - Ok(Some(())) - } else { - self.parent_pattern_for(name, path, elements) - } - } - - fn pattern_for( - &self, name: &str, path: &mut String, elements: &mut U, - ) -> Result, UrlGenerationError> - where - U: Iterator, - I: AsRef, - { - if let Some(pattern) = self.named.get(name) { - self.fill_root(path, elements)?; - pattern.resource_path(path, elements)?; - Ok(Some(())) - } else { - for rmap in &self.nested { - if rmap.pattern_for(name, path, elements)?.is_some() { - return Ok(Some(())); - } - } - Ok(None) - } - } - - fn fill_root( - &self, path: &mut String, elements: &mut U, - ) -> Result<(), UrlGenerationError> - where - U: Iterator, - I: AsRef, - { - if let Some(ref parent) = *self.parent.borrow() { - parent.fill_root(path, elements)?; - } - self.root.resource_path(path, elements) - } - - fn parent_pattern_for( - &self, name: &str, path: &mut String, elements: &mut U, - ) -> Result, UrlGenerationError> - where - U: Iterator, - I: AsRef, - { - if let Some(ref parent) = *self.parent.borrow() { - if let Some(pattern) = parent.named.get(name) { - self.fill_root(path, elements)?; - pattern.resource_path(path, elements)?; - Ok(Some(())) - } else { - parent.parent_pattern_for(name, path, elements) - } - } else { - Ok(None) - } - } -} - -impl Default for Router { - fn default() -> Self { - Router::new(ResourceDef::new("")) - } -} - -impl Router { - pub(crate) fn new(root: ResourceDef) -> Self { - Router { - rmap: Rc::new(ResourceMap { - root, - parent: RefCell::new(None), - named: HashMap::new(), - patterns: Vec::new(), - nested: Vec::new(), - }), - resources: Vec::new(), - patterns: Vec::new(), - default: None, - } - } - - #[inline] - pub(crate) fn route_info_params(&self, idx: u16, params: Params) -> ResourceInfo { - ResourceInfo { - params, - prefix: 0, - rmap: self.rmap.clone(), - resource: ResourceId::Normal(idx), - } - } - - #[cfg(test)] - pub(crate) fn default_route_info(&self) -> ResourceInfo { - ResourceInfo { - params: Params::new(), - rmap: self.rmap.clone(), - resource: ResourceId::Default, - prefix: 0, - } - } - - pub(crate) fn set_prefix(&mut self, path: &str) { - Rc::get_mut(&mut self.rmap).unwrap().root = ResourceDef::new(path); - } - - pub(crate) fn register_resource(&mut self, resource: Resource) { - { - let rmap = Rc::get_mut(&mut self.rmap).unwrap(); - - let name = resource.get_name(); - if !name.is_empty() { - assert!( - !rmap.named.contains_key(name), - "Named resource {:?} is registered.", - name - ); - rmap.named.insert(name.to_owned(), resource.rdef().clone()); - } - rmap.patterns.push((resource.rdef().clone(), None)); - } - self.patterns - .push(ResourcePattern::Resource(resource.rdef().clone())); - self.resources.push(ResourceItem::Resource(resource)); - } - - pub(crate) fn register_scope(&mut self, mut scope: Scope) { - Rc::get_mut(&mut self.rmap) - .unwrap() - .patterns - .push((scope.rdef().clone(), Some(scope.router().rmap.clone()))); - Rc::get_mut(&mut self.rmap) - .unwrap() - .nested - .push(scope.router().rmap.clone()); - - let filters = scope.take_filters(); - self.patterns - .push(ResourcePattern::Scope(scope.rdef().clone(), filters)); - self.resources.push(ResourceItem::Scope(scope)); - } - - pub(crate) fn register_handler( - &mut self, path: &str, hnd: Box>, - filters: Option>>>, - ) { - let rdef = ResourceDef::prefix(path); - Rc::get_mut(&mut self.rmap) - .unwrap() - .patterns - .push((rdef.clone(), None)); - self.resources.push(ResourceItem::Handler(hnd)); - self.patterns.push(ResourcePattern::Handler(rdef, filters)); - } - - pub(crate) fn has_default_resource(&self) -> bool { - self.default.is_some() - } - - pub(crate) fn register_default_resource(&mut self, resource: DefaultResource) { - self.default = Some(resource); - } - - pub(crate) fn finish(&mut self) { - for resource in &mut self.resources { - match resource { - ResourceItem::Resource(_) => (), - ResourceItem::Scope(scope) => { - if !scope.has_default_resource() { - if let Some(ref default) = self.default { - scope.default_resource(default.clone()); - } - } - *scope.router().rmap.parent.borrow_mut() = Some(self.rmap.clone()); - scope.finish(); - } - ResourceItem::Handler(hnd) => { - if !hnd.has_default_resource() { - if let Some(ref default) = self.default { - hnd.default_resource(default.clone()); - } - } - hnd.finish() - } - } - } - } - - pub(crate) fn register_external(&mut self, name: &str, rdef: ResourceDef) { - let rmap = Rc::get_mut(&mut self.rmap).unwrap(); - assert!( - !rmap.named.contains_key(name), - "Named resource {:?} is registered.", - name - ); - rmap.named.insert(name.to_owned(), rdef); - } - - pub(crate) fn register_route(&mut self, path: &str, method: Method, f: F) - where - F: WithFactory, - R: Responder + 'static, - T: FromRequest + 'static, - { - let out = { - // get resource handler - let mut iterator = self.resources.iter_mut(); - - loop { - if let Some(ref mut resource) = iterator.next() { - if let ResourceItem::Resource(ref mut resource) = resource { - if resource.rdef().pattern() == path { - resource.method(method).with(f); - break None; - } - } - } else { - let mut resource = Resource::new(ResourceDef::new(path)); - resource.method(method).with(f); - break Some(resource); - } - } - }; - if let Some(out) = out { - self.register_resource(out); - } - } - - /// Handle request - pub fn handle(&self, req: &HttpRequest) -> AsyncResult { - let resource = match req.resource().resource { - ResourceId::Normal(idx) => &self.resources[idx as usize], - ResourceId::Default => { - if let Some(ref default) = self.default { - if let Some(id) = default.get_route_id(req) { - return default.handle(id, req); - } - } - return AsyncResult::ok(HttpResponse::new(StatusCode::NOT_FOUND)); - } - }; - match resource { - ResourceItem::Resource(ref resource) => { - if let Some(id) = resource.get_route_id(req) { - return resource.handle(id, req); - } - - if let Some(ref default) = self.default { - if let Some(id) = default.get_route_id(req) { - return default.handle(id, req); - } - } - } - ResourceItem::Handler(hnd) => return hnd.handle(req), - ResourceItem::Scope(hnd) => return hnd.handle(req), - } - AsyncResult::ok(HttpResponse::new(StatusCode::NOT_FOUND)) - } - - /// Query for matched resource - pub fn recognize(&self, req: &Request, state: &S, tail: usize) -> ResourceInfo { - if tail <= req.path().len() { - 'outer: for (idx, resource) in self.patterns.iter().enumerate() { - match resource { - ResourcePattern::Resource(rdef) => { - if let Some(params) = rdef.match_with_params(req, tail) { - return self.route_info_params(idx as u16, params); - } - } - ResourcePattern::Handler(rdef, filters) => { - if let Some(params) = rdef.match_prefix_with_params(req, tail) { - if let Some(ref filters) = filters { - for filter in filters { - if !filter.check(req, state) { - continue 'outer; - } - } - } - return self.route_info_params(idx as u16, params); - } - } - ResourcePattern::Scope(rdef, filters) => { - if let Some(params) = rdef.match_prefix_with_params(req, tail) { - for filter in filters { - if !filter.check(req, state) { - continue 'outer; - } - } - return self.route_info_params(idx as u16, params); - } - } - } - } - } - ResourceInfo { - prefix: tail as u16, - params: Params::new(), - rmap: self.rmap.clone(), - resource: ResourceId::Default, - } - } -} - -#[derive(Debug, Clone, PartialEq)] -enum PatternElement { - Str(String), - Var(String), -} - -#[derive(Clone, Debug)] -enum PatternType { - Static(String), - Prefix(String), - Dynamic(Regex, Vec>, usize), -} - -#[derive(Debug, Copy, Clone, PartialEq)] -/// Resource type -pub enum ResourceType { - /// Normal resource - Normal, - /// Resource for application default handler - Default, - /// External resource - External, - /// Unknown resource type - Unset, -} - -/// Resource type describes an entry in resources table -#[derive(Clone, Debug)] -pub struct ResourceDef { - tp: PatternType, - rtp: ResourceType, - name: String, - pattern: String, - elements: Vec, -} - -impl ResourceDef { - /// Parse path pattern and create new `ResourceDef` instance. - /// - /// Panics if path pattern is wrong. - pub fn new(path: &str) -> Self { - ResourceDef::with_prefix(path, false, !path.is_empty()) - } - - /// Parse path pattern and create new `ResourceDef` instance. - /// - /// Use `prefix` type instead of `static`. - /// - /// Panics if path regex pattern is wrong. - pub fn prefix(path: &str) -> Self { - ResourceDef::with_prefix(path, true, !path.is_empty()) - } - - /// Construct external resource def - /// - /// Panics if path pattern is wrong. - pub fn external(path: &str) -> Self { - let mut resource = ResourceDef::with_prefix(path, false, false); - resource.rtp = ResourceType::External; - resource - } - - /// Parse path pattern and create new `ResourceDef` instance with custom prefix - pub fn with_prefix(path: &str, for_prefix: bool, slash: bool) -> Self { - let mut path = path.to_owned(); - if slash && !path.starts_with('/') { - path.insert(0, '/'); - } - let (pattern, elements, is_dynamic, len) = ResourceDef::parse(&path, for_prefix); - - let tp = if is_dynamic { - let re = match Regex::new(&pattern) { - Ok(re) => re, - Err(err) => panic!("Wrong path pattern: \"{}\" {}", path, err), - }; - // actix creates one router per thread - let names = re - .capture_names() - .filter_map(|name| name.map(|name| Rc::new(name.to_owned()))) - .collect(); - PatternType::Dynamic(re, names, len) - } else if for_prefix { - PatternType::Prefix(pattern.clone()) - } else { - PatternType::Static(pattern.clone()) - }; - - ResourceDef { - tp, - elements, - name: "".to_string(), - rtp: ResourceType::Normal, - pattern: path.to_owned(), - } - } - - /// Resource type - pub fn rtype(&self) -> ResourceType { - self.rtp - } - - /// Resource name - pub fn name(&self) -> &str { - &self.name - } - - /// Resource name - pub(crate) fn set_name(&mut self, name: &str) { - self.name = name.to_owned(); - } - - /// Path pattern of the resource - pub fn pattern(&self) -> &str { - &self.pattern - } - - /// Is this path a match against this resource? - pub fn is_match(&self, path: &str) -> bool { - match self.tp { - PatternType::Static(ref s) => s == path, - PatternType::Dynamic(ref re, _, _) => re.is_match(path), - PatternType::Prefix(ref s) => path.starts_with(s), - } - } - - fn is_prefix_match(&self, path: &str) -> Option { - let plen = path.len(); - let path = if path.is_empty() { "/" } else { path }; - - match self.tp { - PatternType::Static(ref s) => if s == path { - Some(plen) - } else { - None - }, - PatternType::Dynamic(ref re, _, len) => { - if let Some(captures) = re.captures(path) { - let mut pos = 0; - let mut passed = false; - for capture in captures.iter() { - if let Some(ref m) = capture { - if !passed { - passed = true; - continue; - } - - pos = m.end(); - } - } - Some(plen + pos + len) - } else { - None - } - } - PatternType::Prefix(ref s) => { - let len = if path == s { - s.len() - } else if path.starts_with(s) - && (s.ends_with('/') || path.split_at(s.len()).1.starts_with('/')) - { - if s.ends_with('/') { - s.len() - 1 - } else { - s.len() - } - } else { - return None; - }; - Some(min(plen, len)) - } - } - } - - /// Are the given path and parameters a match against this resource? - pub fn match_with_params(&self, req: &Request, plen: usize) -> Option { - let path = &req.path()[plen..]; - - match self.tp { - PatternType::Static(ref s) => if s != path { - None - } else { - Some(Params::with_url(req.url())) - }, - PatternType::Dynamic(ref re, ref names, _) => { - if let Some(captures) = re.captures(path) { - let mut params = Params::with_url(req.url()); - let mut idx = 0; - let mut passed = false; - for capture in captures.iter() { - if let Some(ref m) = capture { - if !passed { - passed = true; - continue; - } - params.add( - names[idx].clone(), - ParamItem::UrlSegment( - (plen + m.start()) as u16, - (plen + m.end()) as u16, - ), - ); - idx += 1; - } - } - params.set_tail(req.path().len() as u16); - Some(params) - } else { - None - } - } - PatternType::Prefix(ref s) => if !path.starts_with(s) { - None - } else { - Some(Params::with_url(req.url())) - }, - } - } - - /// Is the given path a prefix match and do the parameters match against this resource? - pub fn match_prefix_with_params( - &self, req: &Request, plen: usize, - ) -> Option { - let path = &req.path()[plen..]; - let path = if path.is_empty() { "/" } else { path }; - - match self.tp { - PatternType::Static(ref s) => if s == path { - let mut params = Params::with_url(req.url()); - params.set_tail(req.path().len() as u16); - Some(params) - } else { - None - }, - PatternType::Dynamic(ref re, ref names, len) => { - if let Some(captures) = re.captures(path) { - let mut params = Params::with_url(req.url()); - let mut pos = 0; - let mut passed = false; - let mut idx = 0; - for capture in captures.iter() { - if let Some(ref m) = capture { - if !passed { - passed = true; - continue; - } - - params.add( - names[idx].clone(), - ParamItem::UrlSegment( - (plen + m.start()) as u16, - (plen + m.end()) as u16, - ), - ); - idx += 1; - pos = m.end(); - } - } - params.set_tail((plen + pos + len) as u16); - Some(params) - } else { - None - } - } - PatternType::Prefix(ref s) => { - let len = if path == s { - s.len() - } else if path.starts_with(s) - && (s.ends_with('/') || path.split_at(s.len()).1.starts_with('/')) - { - if s.ends_with('/') { - s.len() - 1 - } else { - s.len() - } - } else { - return None; - }; - let mut params = Params::with_url(req.url()); - params.set_tail(min(req.path().len(), plen + len) as u16); - Some(params) - } - } - } - - /// Build resource path. - pub fn resource_path( - &self, path: &mut String, elements: &mut U, - ) -> Result<(), UrlGenerationError> - where - U: Iterator, - I: AsRef, - { - match self.tp { - PatternType::Prefix(ref p) => path.push_str(p), - PatternType::Static(ref p) => path.push_str(p), - PatternType::Dynamic(..) => { - for el in &self.elements { - match *el { - PatternElement::Str(ref s) => path.push_str(s), - PatternElement::Var(_) => { - if let Some(val) = elements.next() { - path.push_str(val.as_ref()) - } else { - return Err(UrlGenerationError::NotEnoughElements); - } - } - } - } - } - }; - Ok(()) - } - - fn parse_param(pattern: &str) -> (PatternElement, String, &str) { - const DEFAULT_PATTERN: &str = "[^/]+"; - let mut params_nesting = 0usize; - let close_idx = pattern - .find(|c| match c { - '{' => { - params_nesting += 1; - false - } - '}' => { - params_nesting -= 1; - params_nesting == 0 - } - _ => false, - }).expect("malformed param"); - let (mut param, rem) = pattern.split_at(close_idx + 1); - param = ¶m[1..param.len() - 1]; // Remove outer brackets - let (name, pattern) = match param.find(':') { - Some(idx) => { - let (name, pattern) = param.split_at(idx); - (name, &pattern[1..]) - } - None => (param, DEFAULT_PATTERN), - }; - ( - PatternElement::Var(name.to_string()), - format!(r"(?P<{}>{})", &name, &pattern), - rem, - ) - } - - fn parse( - mut pattern: &str, for_prefix: bool, - ) -> (String, Vec, bool, usize) { - if pattern.find('{').is_none() { - return ( - String::from(pattern), - vec![PatternElement::Str(String::from(pattern))], - false, - pattern.chars().count(), - ); - }; - - let mut elems = Vec::new(); - let mut re = String::from("^"); - - while let Some(idx) = pattern.find('{') { - let (prefix, rem) = pattern.split_at(idx); - elems.push(PatternElement::Str(String::from(prefix))); - re.push_str(&escape(prefix)); - let (param_pattern, re_part, rem) = Self::parse_param(rem); - elems.push(param_pattern); - re.push_str(&re_part); - pattern = rem; - } - - elems.push(PatternElement::Str(String::from(pattern))); - re.push_str(&escape(pattern)); - - if !for_prefix { - re.push_str("$"); - } - - (re, elems, true, pattern.chars().count()) - } -} - -impl PartialEq for ResourceDef { - fn eq(&self, other: &ResourceDef) -> bool { - self.pattern == other.pattern - } -} - -impl Eq for ResourceDef {} - -impl Hash for ResourceDef { - fn hash(&self, state: &mut H) { - self.pattern.hash(state); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use test::TestRequest; - - #[test] - fn test_recognizer10() { - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/name"))); - router.register_resource(Resource::new(ResourceDef::new("/name/{val}"))); - router.register_resource(Resource::new(ResourceDef::new( - "/name/{val}/index.html", - ))); - router.register_resource(Resource::new(ResourceDef::new("/file/{file}.{ext}"))); - router.register_resource(Resource::new(ResourceDef::new( - "/v{val}/{val2}/index.html", - ))); - router.register_resource(Resource::new(ResourceDef::new("/v/{tail:.*}"))); - router.register_resource(Resource::new(ResourceDef::new("/test2/{test}.html"))); - router.register_resource(Resource::new(ResourceDef::new("/{test}/index.html"))); - - let req = TestRequest::with_uri("/name").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(0)); - assert!(info.match_info().is_empty()); - - let req = TestRequest::with_uri("/name/value").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(1)); - assert_eq!(info.match_info().get("val").unwrap(), "value"); - assert_eq!(&info.match_info()["val"], "value"); - - let req = TestRequest::with_uri("/name/value2/index.html").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(2)); - assert_eq!(info.match_info().get("val").unwrap(), "value2"); - - let req = TestRequest::with_uri("/file/file.gz").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(3)); - assert_eq!(info.match_info().get("file").unwrap(), "file"); - assert_eq!(info.match_info().get("ext").unwrap(), "gz"); - - let req = TestRequest::with_uri("/vtest/ttt/index.html").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(4)); - assert_eq!(info.match_info().get("val").unwrap(), "test"); - assert_eq!(info.match_info().get("val2").unwrap(), "ttt"); - - let req = TestRequest::with_uri("/v/blah-blah/index.html").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(5)); - assert_eq!( - info.match_info().get("tail").unwrap(), - "blah-blah/index.html" - ); - - let req = TestRequest::with_uri("/test2/index.html").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(6)); - assert_eq!(info.match_info().get("test").unwrap(), "index"); - - let req = TestRequest::with_uri("/bbb/index.html").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(7)); - assert_eq!(info.match_info().get("test").unwrap(), "bbb"); - } - - #[test] - fn test_recognizer_2() { - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/index.json"))); - router.register_resource(Resource::new(ResourceDef::new("/{source}.json"))); - - let req = TestRequest::with_uri("/index.json").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(0)); - - let req = TestRequest::with_uri("/test.json").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(1)); - } - - #[test] - fn test_recognizer_with_prefix() { - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/name"))); - router.register_resource(Resource::new(ResourceDef::new("/name/{val}"))); - - let req = TestRequest::with_uri("/name").finish(); - let info = router.recognize(&req, &(), 5); - assert_eq!(info.resource, ResourceId::Default); - - let req = TestRequest::with_uri("/test/name").finish(); - let info = router.recognize(&req, &(), 5); - assert_eq!(info.resource, ResourceId::Normal(0)); - - let req = TestRequest::with_uri("/test/name/value").finish(); - let info = router.recognize(&req, &(), 5); - assert_eq!(info.resource, ResourceId::Normal(1)); - assert_eq!(info.match_info().get("val").unwrap(), "value"); - assert_eq!(&info.match_info()["val"], "value"); - - // same patterns - let mut router = Router::<()>::default(); - router.register_resource(Resource::new(ResourceDef::new("/name"))); - router.register_resource(Resource::new(ResourceDef::new("/name/{val}"))); - - let req = TestRequest::with_uri("/name").finish(); - let info = router.recognize(&req, &(), 6); - assert_eq!(info.resource, ResourceId::Default); - - let req = TestRequest::with_uri("/test2/name").finish(); - let info = router.recognize(&req, &(), 6); - assert_eq!(info.resource, ResourceId::Normal(0)); - - let req = TestRequest::with_uri("/test2/name-test").finish(); - let info = router.recognize(&req, &(), 6); - assert_eq!(info.resource, ResourceId::Default); - - let req = TestRequest::with_uri("/test2/name/ttt").finish(); - let info = router.recognize(&req, &(), 6); - assert_eq!(info.resource, ResourceId::Normal(1)); - assert_eq!(&info.match_info()["val"], "ttt"); - } - - #[test] - fn test_parse_static() { - let re = ResourceDef::new("/"); - assert!(re.is_match("/")); - assert!(!re.is_match("/a")); - - let re = ResourceDef::new("/name"); - assert!(re.is_match("/name")); - assert!(!re.is_match("/name1")); - assert!(!re.is_match("/name/")); - assert!(!re.is_match("/name~")); - - let re = ResourceDef::new("/name/"); - assert!(re.is_match("/name/")); - assert!(!re.is_match("/name")); - assert!(!re.is_match("/name/gs")); - - let re = ResourceDef::new("/user/profile"); - assert!(re.is_match("/user/profile")); - assert!(!re.is_match("/user/profile/profile")); - } - - #[test] - fn test_parse_param() { - let re = ResourceDef::new("/user/{id}"); - assert!(re.is_match("/user/profile")); - assert!(re.is_match("/user/2345")); - assert!(!re.is_match("/user/2345/")); - assert!(!re.is_match("/user/2345/sdg")); - - let req = TestRequest::with_uri("/user/profile").finish(); - let info = re.match_with_params(&req, 0).unwrap(); - assert_eq!(info.get("id").unwrap(), "profile"); - - let req = TestRequest::with_uri("/user/1245125").finish(); - let info = re.match_with_params(&req, 0).unwrap(); - assert_eq!(info.get("id").unwrap(), "1245125"); - - let re = ResourceDef::new("/v{version}/resource/{id}"); - assert!(re.is_match("/v1/resource/320120")); - assert!(!re.is_match("/v/resource/1")); - assert!(!re.is_match("/resource")); - - let req = TestRequest::with_uri("/v151/resource/adahg32").finish(); - let info = re.match_with_params(&req, 0).unwrap(); - assert_eq!(info.get("version").unwrap(), "151"); - assert_eq!(info.get("id").unwrap(), "adahg32"); - - let re = ResourceDef::new("/{id:[[:digit:]]{6}}"); - assert!(re.is_match("/012345")); - assert!(!re.is_match("/012")); - assert!(!re.is_match("/01234567")); - assert!(!re.is_match("/XXXXXX")); - - let req = TestRequest::with_uri("/012345").finish(); - let info = re.match_with_params(&req, 0).unwrap(); - assert_eq!(info.get("id").unwrap(), "012345"); - } - - #[test] - fn test_resource_prefix() { - let re = ResourceDef::prefix("/name"); - assert!(re.is_match("/name")); - assert!(re.is_match("/name/")); - assert!(re.is_match("/name/test/test")); - assert!(re.is_match("/name1")); - assert!(re.is_match("/name~")); - - let re = ResourceDef::prefix("/name/"); - assert!(re.is_match("/name/")); - assert!(re.is_match("/name/gs")); - assert!(!re.is_match("/name")); - } - - #[test] - fn test_reousrce_prefix_dynamic() { - let re = ResourceDef::prefix("/{name}/"); - assert!(re.is_match("/name/")); - assert!(re.is_match("/name/gs")); - assert!(!re.is_match("/name")); - - let req = TestRequest::with_uri("/test2/").finish(); - let info = re.match_with_params(&req, 0).unwrap(); - assert_eq!(&info["name"], "test2"); - assert_eq!(&info[0], "test2"); - - let req = TestRequest::with_uri("/test2/subpath1/subpath2/index.html").finish(); - let info = re.match_with_params(&req, 0).unwrap(); - assert_eq!(&info["name"], "test2"); - assert_eq!(&info[0], "test2"); - } - - #[test] - fn test_request_resource() { - let mut router = Router::<()>::default(); - let mut resource = Resource::new(ResourceDef::new("/index.json")); - resource.name("r1"); - router.register_resource(resource); - let mut resource = Resource::new(ResourceDef::new("/test.json")); - resource.name("r2"); - router.register_resource(resource); - - let req = TestRequest::with_uri("/index.json").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(0)); - - assert_eq!(info.name(), "r1"); - - let req = TestRequest::with_uri("/test.json").finish(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(1)); - assert_eq!(info.name(), "r2"); - } - - #[test] - fn test_has_resource() { - let mut router = Router::<()>::default(); - let scope = Scope::new("/test").resource("/name", |_| "done"); - router.register_scope(scope); - - { - let info = router.default_route_info(); - assert!(!info.has_resource("/test")); - assert!(info.has_resource("/test/name")); - } - - let scope = - Scope::new("/test2").nested("/test10", |s| s.resource("/name", |_| "done")); - router.register_scope(scope); - - let info = router.default_route_info(); - assert!(info.has_resource("/test2/test10/name")); - } - - #[test] - fn test_url_for() { - let mut router = Router::<()>::new(ResourceDef::prefix("")); - - let mut resource = Resource::new(ResourceDef::new("/tttt")); - resource.name("r0"); - router.register_resource(resource); - - let scope = Scope::new("/test").resource("/name", |r| { - r.name("r1"); - }); - router.register_scope(scope); - - let scope = Scope::new("/test2") - .nested("/test10", |s| s.resource("/name", |r| r.name("r2"))); - router.register_scope(scope); - router.finish(); - - let req = TestRequest::with_uri("/test").request(); - { - let info = router.default_route_info(); - - let res = info - .url_for(&req, "r0", Vec::<&'static str>::new()) - .unwrap(); - assert_eq!(res.as_str(), "http://localhost:8080/tttt"); - - let res = info - .url_for(&req, "r1", Vec::<&'static str>::new()) - .unwrap(); - assert_eq!(res.as_str(), "http://localhost:8080/test/name"); - - let res = info - .url_for(&req, "r2", Vec::<&'static str>::new()) - .unwrap(); - assert_eq!(res.as_str(), "http://localhost:8080/test2/test10/name"); - } - - let req = TestRequest::with_uri("/test/name").request(); - let info = router.recognize(&req, &(), 0); - assert_eq!(info.resource, ResourceId::Normal(1)); - - let res = info - .url_for(&req, "r0", Vec::<&'static str>::new()) - .unwrap(); - assert_eq!(res.as_str(), "http://localhost:8080/tttt"); - - let res = info - .url_for(&req, "r1", Vec::<&'static str>::new()) - .unwrap(); - assert_eq!(res.as_str(), "http://localhost:8080/test/name"); - - let res = info - .url_for(&req, "r2", Vec::<&'static str>::new()) - .unwrap(); - assert_eq!(res.as_str(), "http://localhost:8080/test2/test10/name"); - } - - #[test] - fn test_url_for_dynamic() { - let mut router = Router::<()>::new(ResourceDef::prefix("")); - - let mut resource = Resource::new(ResourceDef::new("/{name}/test/index.{ext}")); - resource.name("r0"); - router.register_resource(resource); - - let scope = Scope::new("/{name1}").nested("/{name2}", |s| { - s.resource("/{name3}/test/index.{ext}", |r| r.name("r2")) - }); - router.register_scope(scope); - router.finish(); - - let req = TestRequest::with_uri("/test").request(); - { - let info = router.default_route_info(); - - let res = info.url_for(&req, "r0", vec!["sec1", "html"]).unwrap(); - assert_eq!(res.as_str(), "http://localhost:8080/sec1/test/index.html"); - - let res = info - .url_for(&req, "r2", vec!["sec1", "sec2", "sec3", "html"]) - .unwrap(); - assert_eq!( - res.as_str(), - "http://localhost:8080/sec1/sec2/sec3/test/index.html" - ); - } - } -} diff --git a/src/scope.rs b/src/scope.rs deleted file mode 100644 index fb9e7514..00000000 --- a/src/scope.rs +++ /dev/null @@ -1,1236 +0,0 @@ -use std::marker::PhantomData; -use std::mem; -use std::rc::Rc; - -use futures::{Async, Future, Poll}; - -use error::Error; -use handler::{ - AsyncResult, AsyncResultItem, FromRequest, Handler, Responder, RouteHandler, - WrapHandler, -}; -use http::Method; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{ - Finished as MiddlewareFinished, Middleware, Response as MiddlewareResponse, - Started as MiddlewareStarted, -}; -use pred::Predicate; -use resource::{DefaultResource, Resource}; -use router::{ResourceDef, Router}; -use server::Request; -use with::WithFactory; - -/// Resources scope -/// -/// Scope is a set of resources with common root path. -/// Scopes collect multiple paths under a common path prefix. -/// Scope path can contain variable path segments as resources. -/// Scope prefix is always complete path segment, i.e `/app` would -/// be converted to a `/app/` and it would not match `/app` path. -/// -/// You can get variable path segments from `HttpRequest::match_info()`. -/// `Path` extractor also is able to extract scope level variable segments. -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::{http, App, HttpRequest, HttpResponse}; -/// -/// fn main() { -/// 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())) -/// }); -/// } -/// ``` -/// -/// In the above example three routes get registered: -/// * /{project_id}/path1 - reponds to all http method -/// * /{project_id}/path2 - `GET` requests -/// * /{project_id}/path3 - `HEAD` requests -/// -pub struct Scope { - rdef: ResourceDef, - router: Rc>, - filters: Vec>>, - middlewares: Rc>>>, -} - -#[cfg_attr( - feature = "cargo-clippy", - allow(new_without_default_derive) -)] -impl Scope { - /// Create a new scope - pub fn new(path: &str) -> Scope { - let rdef = ResourceDef::prefix(path); - Scope { - rdef: rdef.clone(), - router: Rc::new(Router::new(rdef)), - filters: Vec::new(), - middlewares: Rc::new(Vec::new()), - } - } - - #[inline] - pub(crate) fn rdef(&self) -> &ResourceDef { - &self.rdef - } - - pub(crate) fn router(&self) -> &Router { - self.router.as_ref() - } - - #[inline] - pub(crate) fn take_filters(&mut self) -> Vec>> { - mem::replace(&mut self.filters, Vec::new()) - } - - /// Add match predicate to scope. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, pred, App, HttpRequest, HttpResponse, Path}; - /// - /// fn index(data: Path<(String, String)>) -> &'static str { - /// "Welcome!" - /// } - /// - /// fn main() { - /// let app = App::new().scope("/app", |scope| { - /// scope - /// .filter(pred::Header("content-type", "text/plain")) - /// .route("/test1", http::Method::GET, index) - /// .route("/test2", http::Method::POST, |_: HttpRequest| { - /// HttpResponse::MethodNotAllowed() - /// }) - /// }); - /// } - /// ``` - pub fn filter + 'static>(mut self, p: T) -> Self { - self.filters.push(Box::new(p)); - self - } - - /// Create nested scope with new state. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{App, HttpRequest}; - /// - /// struct AppState; - /// - /// fn index(req: &HttpRequest) -> &'static str { - /// "Welcome!" - /// } - /// - /// fn main() { - /// let app = App::new().scope("/app", |scope| { - /// scope.with_state("/state2", AppState, |scope| { - /// scope.resource("/test1", |r| r.f(index)) - /// }) - /// }); - /// } - /// ``` - pub fn with_state(mut self, path: &str, state: T, f: F) -> Scope - where - F: FnOnce(Scope) -> Scope, - { - let rdef = ResourceDef::prefix(path); - let scope = Scope { - rdef: rdef.clone(), - filters: Vec::new(), - router: Rc::new(Router::new(rdef)), - middlewares: Rc::new(Vec::new()), - }; - let mut scope = f(scope); - - let state = Rc::new(state); - let filters: Vec>> = vec![Box::new(FiltersWrapper { - state: Rc::clone(&state), - filters: scope.take_filters(), - })]; - let handler = Box::new(Wrapper { scope, state }); - - Rc::get_mut(&mut self.router).unwrap().register_handler( - path, - handler, - Some(filters), - ); - - self - } - - /// Create nested scope. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{App, HttpRequest}; - /// - /// struct AppState; - /// - /// fn index(req: &HttpRequest) -> &'static str { - /// "Welcome!" - /// } - /// - /// fn main() { - /// let app = App::with_state(AppState).scope("/app", |scope| { - /// scope.nested("/v1", |scope| scope.resource("/test1", |r| r.f(index))) - /// }); - /// } - /// ``` - pub fn nested(mut self, path: &str, f: F) -> Scope - where - F: FnOnce(Scope) -> Scope, - { - let rdef = ResourceDef::prefix(&insert_slash(path)); - let scope = Scope { - rdef: rdef.clone(), - filters: Vec::new(), - router: Rc::new(Router::new(rdef)), - middlewares: Rc::new(Vec::new()), - }; - Rc::get_mut(&mut self.router) - .unwrap() - .register_scope(f(scope)); - - self - } - - /// Configure route for a specific path. - /// - /// This is a simplified version of the `Scope::resource()` method. - /// Handler functions need to accept one request extractor - /// argument. - /// - /// This method could be called multiple times, in that case - /// multiple routes would be registered for same resource path. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpRequest, HttpResponse, Path}; - /// - /// fn index(data: Path<(String, String)>) -> &'static str { - /// "Welcome!" - /// } - /// - /// fn main() { - /// let app = App::new().scope("/app", |scope| { - /// scope.route("/test1", http::Method::GET, index).route( - /// "/test2", - /// http::Method::POST, - /// |_: HttpRequest| HttpResponse::MethodNotAllowed(), - /// ) - /// }); - /// } - /// ``` - pub fn route(mut self, path: &str, method: Method, f: F) -> Scope - where - F: WithFactory, - R: Responder + 'static, - T: FromRequest + 'static, - { - Rc::get_mut(&mut self.router).unwrap().register_route( - &insert_slash(path), - method, - f, - ); - self - } - - /// Configure resource for a specific path. - /// - /// This method is similar to an `App::resource()` method. - /// Resources may have variable path segments. Resource path uses scope - /// path as a path prefix. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::*; - /// - /// fn main() { - /// let app = App::new().scope("/api", |scope| { - /// scope.resource("/users/{userid}/{friend}", |r| { - /// r.get().f(|_| HttpResponse::Ok()); - /// r.head().f(|_| HttpResponse::MethodNotAllowed()); - /// r.route() - /// .filter(pred::Any(pred::Get()).or(pred::Put())) - /// .filter(pred::Header("Content-Type", "text/plain")) - /// .f(|_| HttpResponse::Ok()) - /// }) - /// }); - /// } - /// ``` - pub fn resource(mut self, path: &str, f: F) -> Scope - where - F: FnOnce(&mut Resource) -> R + 'static, - { - // add resource - let mut resource = Resource::new(ResourceDef::new(&insert_slash(path))); - f(&mut resource); - - Rc::get_mut(&mut self.router) - .unwrap() - .register_resource(resource); - self - } - - /// Default resource to be used if no matching route could be found. - pub fn default_resource(mut self, f: F) -> Scope - where - F: FnOnce(&mut Resource) -> R + 'static, - { - // create and configure default resource - let mut resource = Resource::new(ResourceDef::new("")); - f(&mut resource); - - Rc::get_mut(&mut self.router) - .expect("Multiple copies of scope router") - .register_default_resource(resource.into()); - - self - } - - /// Configure handler for specific path prefix. - /// - /// A path prefix consists of valid path segments, i.e for the - /// prefix `/app` any request with the paths `/app`, `/app/` or - /// `/app/test` would match, but the path `/application` would - /// not. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpRequest, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().scope("/scope-prefix", |scope| { - /// scope.handler("/app", |req: &HttpRequest| match *req.method() { - /// http::Method::GET => HttpResponse::Ok(), - /// http::Method::POST => HttpResponse::MethodNotAllowed(), - /// _ => HttpResponse::NotFound(), - /// }) - /// }); - /// } - /// ``` - pub fn handler>(mut self, path: &str, handler: H) -> Scope { - let path = insert_slash(path.trim().trim_right_matches('/')); - Rc::get_mut(&mut self.router) - .expect("Multiple copies of scope router") - .register_handler(&path, Box::new(WrapHandler::new(handler)), None); - self - } - - /// Register a scope middleware - /// - /// This is similar to `App's` middlewares, but - /// middlewares get invoked on scope level. - /// - /// *Note* `Middleware::finish()` fires right after response get - /// prepared. It does not wait until body get sent to the peer. - pub fn middleware>(mut self, mw: M) -> Scope { - Rc::get_mut(&mut self.middlewares) - .expect("Can not use after configuration") - .push(Box::new(mw)); - self - } -} - -fn insert_slash(path: &str) -> String { - let mut path = path.to_owned(); - if !path.is_empty() && !path.starts_with('/') { - path.insert(0, '/'); - }; - path -} - -impl RouteHandler for Scope { - fn handle(&self, req: &HttpRequest) -> AsyncResult { - let tail = req.match_info().tail as usize; - - // recognize resources - let info = self.router.recognize(req, req.state(), tail); - let req2 = req.with_route_info(info); - if self.middlewares.is_empty() { - self.router.handle(&req2) - } else { - AsyncResult::future(Box::new(Compose::new( - req2, - Rc::clone(&self.router), - Rc::clone(&self.middlewares), - ))) - } - } - - fn has_default_resource(&self) -> bool { - self.router.has_default_resource() - } - - fn default_resource(&mut self, default: DefaultResource) { - Rc::get_mut(&mut self.router) - .expect("Can not use after configuration") - .register_default_resource(default); - } - - fn finish(&mut self) { - Rc::get_mut(&mut self.router) - .expect("Can not use after configuration") - .finish(); - } -} - -struct Wrapper { - state: Rc, - scope: Scope, -} - -impl RouteHandler for Wrapper { - fn handle(&self, req: &HttpRequest) -> AsyncResult { - let req = req.with_state(Rc::clone(&self.state)); - self.scope.handle(&req) - } -} - -struct FiltersWrapper { - state: Rc, - filters: Vec>>, -} - -impl Predicate for FiltersWrapper { - fn check(&self, req: &Request, _: &S2) -> bool { - for filter in &self.filters { - if !filter.check(&req, &self.state) { - return false; - } - } - true - } -} - -/// Compose resource level middlewares with route handler. -struct Compose { - info: ComposeInfo, - state: ComposeState, -} - -struct ComposeInfo { - count: usize, - req: HttpRequest, - router: Rc>, - mws: Rc>>>, -} - -enum ComposeState { - Starting(StartMiddlewares), - Handler(WaitingResponse), - RunMiddlewares(RunMiddlewares), - Finishing(FinishingMiddlewares), - Completed(Response), -} - -impl ComposeState { - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - match *self { - ComposeState::Starting(ref mut state) => state.poll(info), - ComposeState::Handler(ref mut state) => state.poll(info), - ComposeState::RunMiddlewares(ref mut state) => state.poll(info), - ComposeState::Finishing(ref mut state) => state.poll(info), - ComposeState::Completed(_) => None, - } - } -} - -impl Compose { - fn new( - req: HttpRequest, router: Rc>, mws: Rc>>>, - ) -> Self { - let mut info = ComposeInfo { - mws, - req, - router, - count: 0, - }; - let state = StartMiddlewares::init(&mut info); - - Compose { state, info } - } -} - -impl Future for Compose { - type Item = HttpResponse; - type Error = Error; - - fn poll(&mut self) -> Poll { - loop { - if let ComposeState::Completed(ref mut resp) = self.state { - let resp = resp.resp.take().unwrap(); - return Ok(Async::Ready(resp)); - } - if let Some(state) = self.state.poll(&mut self.info) { - self.state = state; - } else { - return Ok(Async::NotReady); - } - } - } -} - -/// Middlewares start executor -struct StartMiddlewares { - fut: Option, - _s: PhantomData, -} - -type Fut = Box, Error = Error>>; - -impl StartMiddlewares { - fn init(info: &mut ComposeInfo) -> ComposeState { - let len = info.mws.len(); - - loop { - if info.count == len { - let reply = info.router.handle(&info.req); - return WaitingResponse::init(info, reply); - } else { - let result = info.mws[info.count].start(&info.req); - match result { - Ok(MiddlewareStarted::Done) => info.count += 1, - Ok(MiddlewareStarted::Response(resp)) => { - return RunMiddlewares::init(info, resp); - } - Ok(MiddlewareStarted::Future(fut)) => { - return ComposeState::Starting(StartMiddlewares { - fut: Some(fut), - _s: PhantomData, - }); - } - Err(err) => { - return RunMiddlewares::init(info, err.into()); - } - } - } - } - } - - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - let len = info.mws.len(); - - 'outer: loop { - match self.fut.as_mut().unwrap().poll() { - Ok(Async::NotReady) => { - return None; - } - Ok(Async::Ready(resp)) => { - info.count += 1; - - if let Some(resp) = resp { - return Some(RunMiddlewares::init(info, resp)); - } - loop { - if info.count == len { - let reply = info.router.handle(&info.req); - return Some(WaitingResponse::init(info, reply)); - } else { - let result = info.mws[info.count].start(&info.req); - match result { - Ok(MiddlewareStarted::Done) => info.count += 1, - Ok(MiddlewareStarted::Response(resp)) => { - return Some(RunMiddlewares::init(info, resp)); - } - Ok(MiddlewareStarted::Future(fut)) => { - self.fut = Some(fut); - continue 'outer; - } - Err(err) => { - return Some(RunMiddlewares::init(info, err.into())); - } - } - } - } - } - Err(err) => { - return Some(RunMiddlewares::init(info, err.into())); - } - } - } - } -} - -// waiting for response -struct WaitingResponse { - fut: Box>, - _s: PhantomData, -} - -impl WaitingResponse { - #[inline] - fn init( - info: &mut ComposeInfo, reply: AsyncResult, - ) -> ComposeState { - match reply.into() { - AsyncResultItem::Ok(resp) => RunMiddlewares::init(info, resp), - AsyncResultItem::Err(err) => RunMiddlewares::init(info, err.into()), - AsyncResultItem::Future(fut) => ComposeState::Handler(WaitingResponse { - fut, - _s: PhantomData, - }), - } - } - - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - match self.fut.poll() { - Ok(Async::NotReady) => None, - Ok(Async::Ready(resp)) => Some(RunMiddlewares::init(info, resp)), - Err(err) => Some(RunMiddlewares::init(info, err.into())), - } - } -} - -/// Middlewares response executor -struct RunMiddlewares { - curr: usize, - fut: Option>>, - _s: PhantomData, -} - -impl RunMiddlewares { - fn init(info: &mut ComposeInfo, mut resp: HttpResponse) -> ComposeState { - let mut curr = 0; - let len = info.mws.len(); - - loop { - let state = info.mws[curr].response(&info.req, resp); - resp = match state { - Err(err) => { - info.count = curr + 1; - return FinishingMiddlewares::init(info, err.into()); - } - Ok(MiddlewareResponse::Done(r)) => { - curr += 1; - if curr == len { - return FinishingMiddlewares::init(info, r); - } else { - r - } - } - Ok(MiddlewareResponse::Future(fut)) => { - return ComposeState::RunMiddlewares(RunMiddlewares { - curr, - fut: Some(fut), - _s: PhantomData, - }); - } - }; - } - } - - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - let len = info.mws.len(); - - loop { - // poll latest fut - let mut resp = match self.fut.as_mut().unwrap().poll() { - Ok(Async::NotReady) => return None, - Ok(Async::Ready(resp)) => { - self.curr += 1; - resp - } - Err(err) => return Some(FinishingMiddlewares::init(info, err.into())), - }; - - loop { - if self.curr == len { - return Some(FinishingMiddlewares::init(info, resp)); - } else { - let state = info.mws[self.curr].response(&info.req, resp); - match state { - Err(err) => { - return Some(FinishingMiddlewares::init(info, err.into())) - } - Ok(MiddlewareResponse::Done(r)) => { - self.curr += 1; - resp = r - } - Ok(MiddlewareResponse::Future(fut)) => { - self.fut = Some(fut); - break; - } - } - } - } - } - } -} - -/// Middlewares start executor -struct FinishingMiddlewares { - resp: Option, - fut: Option>>, - _s: PhantomData, -} - -impl FinishingMiddlewares { - fn init(info: &mut ComposeInfo, resp: HttpResponse) -> ComposeState { - if info.count == 0 { - Response::init(resp) - } else { - let mut state = FinishingMiddlewares { - resp: Some(resp), - fut: None, - _s: PhantomData, - }; - if let Some(st) = state.poll(info) { - st - } else { - ComposeState::Finishing(state) - } - } - } - - fn poll(&mut self, info: &mut ComposeInfo) -> Option> { - loop { - // poll latest fut - let not_ready = if let Some(ref mut fut) = self.fut { - match fut.poll() { - Ok(Async::NotReady) => true, - Ok(Async::Ready(())) => false, - Err(err) => { - error!("Middleware finish error: {}", err); - false - } - } - } else { - false - }; - if not_ready { - return None; - } - self.fut = None; - if info.count == 0 { - return Some(Response::init(self.resp.take().unwrap())); - } - - info.count -= 1; - let state = info.mws[info.count as usize] - .finish(&info.req, self.resp.as_ref().unwrap()); - match state { - MiddlewareFinished::Done => { - if info.count == 0 { - return Some(Response::init(self.resp.take().unwrap())); - } - } - MiddlewareFinished::Future(fut) => { - self.fut = Some(fut); - } - } - } - } -} - -struct Response { - resp: Option, - _s: PhantomData, -} - -impl Response { - fn init(resp: HttpResponse) -> ComposeState { - ComposeState::Completed(Response { - resp: Some(resp), - _s: PhantomData, - }) - } -} - -#[cfg(test)] -mod tests { - use bytes::Bytes; - - use application::App; - use body::Body; - use http::{Method, StatusCode}; - use httprequest::HttpRequest; - use httpresponse::HttpResponse; - use pred; - use test::TestRequest; - - #[test] - fn test_scope() { - let app = App::new() - .scope("/app", |scope| { - scope.resource("/path1", |r| r.f(|_| HttpResponse::Ok())) - }).finish(); - - let req = TestRequest::with_uri("/app/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - } - - #[test] - fn test_scope_root() { - let app = App::new() - .scope("/app", |scope| { - scope - .resource("", |r| r.f(|_| HttpResponse::Ok())) - .resource("/", |r| r.f(|_| HttpResponse::Created())) - }).finish(); - - let req = TestRequest::with_uri("/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - } - - #[test] - fn test_scope_root2() { - let app = App::new() - .scope("/app/", |scope| { - scope.resource("", |r| r.f(|_| HttpResponse::Ok())) - }).finish(); - - let req = TestRequest::with_uri("/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - } - - #[test] - fn test_scope_root3() { - let app = App::new() - .scope("/app/", |scope| { - scope.resource("/", |r| r.f(|_| HttpResponse::Ok())) - }).finish(); - - let req = TestRequest::with_uri("/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_scope_route() { - let app = App::new() - .scope("app", |scope| { - scope - .route("/path1", Method::GET, |_: HttpRequest<_>| { - HttpResponse::Ok() - }).route("/path1", Method::DELETE, |_: HttpRequest<_>| { - HttpResponse::Ok() - }) - }).finish(); - - let req = TestRequest::with_uri("/app/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/path1") - .method(Method::DELETE) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/path1") - .method(Method::POST) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_scope_route_without_leading_slash() { - let app = App::new() - .scope("app", |scope| { - scope - .route("path1", Method::GET, |_: HttpRequest<_>| HttpResponse::Ok()) - .route("path1", Method::DELETE, |_: HttpRequest<_>| { - HttpResponse::Ok() - }) - }).finish(); - - let req = TestRequest::with_uri("/app/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/path1") - .method(Method::DELETE) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/path1") - .method(Method::POST) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_scope_filter() { - let app = App::new() - .scope("/app", |scope| { - scope - .filter(pred::Get()) - .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) - }).finish(); - - let req = TestRequest::with_uri("/app/path1") - .method(Method::POST) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/path1") - .method(Method::GET) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - } - - #[test] - fn test_scope_variable_segment() { - let app = App::new() - .scope("/ab-{project}", |scope| { - scope.resource("/path1", |r| { - r.f(|r| { - HttpResponse::Ok() - .body(format!("project: {}", &r.match_info()["project"])) - }) - }) - }).finish(); - - let req = TestRequest::with_uri("/ab-project1/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - match resp.as_msg().body() { - &Body::Binary(ref b) => { - let bytes: Bytes = b.clone().into(); - assert_eq!(bytes, Bytes::from_static(b"project: project1")); - } - _ => panic!(), - } - - let req = TestRequest::with_uri("/aa-project1/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_scope_with_state() { - struct State; - - let app = App::new() - .scope("/app", |scope| { - scope.with_state("/t1", State, |scope| { - scope.resource("/path1", |r| r.f(|_| HttpResponse::Created())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - } - - #[test] - fn test_scope_with_state_root() { - struct State; - - let app = App::new() - .scope("/app", |scope| { - scope.with_state("/t1", State, |scope| { - scope - .resource("", |r| r.f(|_| HttpResponse::Ok())) - .resource("/", |r| r.f(|_| HttpResponse::Created())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/t1/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - } - - #[test] - fn test_scope_with_state_root2() { - struct State; - - let app = App::new() - .scope("/app", |scope| { - scope.with_state("/t1/", State, |scope| { - scope.resource("", |r| r.f(|_| HttpResponse::Ok())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/t1/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - } - - #[test] - fn test_scope_with_state_root3() { - struct State; - - let app = App::new() - .scope("/app", |scope| { - scope.with_state("/t1/", State, |scope| { - scope.resource("/", |r| r.f(|_| HttpResponse::Ok())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/t1/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_scope_with_state_filter() { - struct State; - - let app = App::new() - .scope("/app", |scope| { - scope.with_state("/t1", State, |scope| { - scope - .filter(pred::Get()) - .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1/path1") - .method(Method::POST) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/t1/path1") - .method(Method::GET) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - } - - #[test] - fn test_nested_scope() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/t1", |scope| { - scope.resource("/path1", |r| r.f(|_| HttpResponse::Created())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - } - - #[test] - fn test_nested_scope_no_slash() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("t1", |scope| { - scope.resource("/path1", |r| r.f(|_| HttpResponse::Created())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - } - - #[test] - fn test_nested_scope_root() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/t1", |scope| { - scope - .resource("", |r| r.f(|_| HttpResponse::Ok())) - .resource("/", |r| r.f(|_| HttpResponse::Created())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/t1/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - } - - #[test] - fn test_nested_scope_filter() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/t1", |scope| { - scope - .filter(pred::Get()) - .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/t1/path1") - .method(Method::POST) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/t1/path1") - .method(Method::GET) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - } - - #[test] - fn test_nested_scope_with_variable_segment() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/{project_id}", |scope| { - scope.resource("/path1", |r| { - r.f(|r| { - HttpResponse::Created().body(format!( - "project: {}", - &r.match_info()["project_id"] - )) - }) - }) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/project_1/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - - match resp.as_msg().body() { - &Body::Binary(ref b) => { - let bytes: Bytes = b.clone().into(); - assert_eq!(bytes, Bytes::from_static(b"project: project_1")); - } - _ => panic!(), - } - } - - #[test] - fn test_nested2_scope_with_variable_segment() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/{project}", |scope| { - scope.nested("/{id}", |scope| { - scope.resource("/path1", |r| { - r.f(|r| { - HttpResponse::Created().body(format!( - "project: {} - {}", - &r.match_info()["project"], - &r.match_info()["id"], - )) - }) - }) - }) - }) - }).finish(); - - let req = TestRequest::with_uri("/app/test/1/path1").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - - match resp.as_msg().body() { - &Body::Binary(ref b) => { - let bytes: Bytes = b.clone().into(); - assert_eq!(bytes, Bytes::from_static(b"project: test - 1")); - } - _ => panic!(), - } - - let req = TestRequest::with_uri("/app/test/1/path2").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_default_resource() { - let app = App::new() - .scope("/app", |scope| { - scope - .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) - .default_resource(|r| r.f(|_| HttpResponse::BadRequest())) - }).finish(); - - let req = TestRequest::with_uri("/app/path2").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::BAD_REQUEST); - - let req = TestRequest::with_uri("/path2").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_default_resource_propagation() { - let app = App::new() - .scope("/app1", |scope| { - scope.default_resource(|r| r.f(|_| HttpResponse::BadRequest())) - }).scope("/app2", |scope| scope) - .default_resource(|r| r.f(|_| HttpResponse::MethodNotAllowed())) - .finish(); - - let req = TestRequest::with_uri("/non-exist").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::METHOD_NOT_ALLOWED); - - let req = TestRequest::with_uri("/app1/non-exist").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::BAD_REQUEST); - - let req = TestRequest::with_uri("/app2/non-exist").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::METHOD_NOT_ALLOWED); - } - - #[test] - fn test_handler() { - let app = App::new() - .scope("/scope", |scope| { - scope.handler("/test", |_: &_| HttpResponse::Ok()) - }).finish(); - - let req = TestRequest::with_uri("/scope/test").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/scope/test/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/scope/test/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/scope/testapp").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/scope/blah").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } -} diff --git a/src/server/acceptor.rs b/src/server/acceptor.rs deleted file mode 100644 index 994b4b7b..00000000 --- a/src/server/acceptor.rs +++ /dev/null @@ -1,383 +0,0 @@ -use std::time::Duration; -use std::{fmt, net}; - -use actix_net::server::ServerMessage; -use actix_net::service::{NewService, Service}; -use futures::future::{err, ok, Either, FutureResult}; -use futures::{Async, Future, Poll}; -use tokio_reactor::Handle; -use tokio_tcp::TcpStream; -use tokio_timer::{sleep, Delay}; - -use super::error::AcceptorError; -use super::IoStream; - -/// This trait indicates types that can create acceptor service for http server. -pub trait AcceptorServiceFactory: Send + Clone + 'static { - type Io: IoStream + Send; - type NewService: NewService; - - fn create(&self) -> Self::NewService; -} - -impl AcceptorServiceFactory for F -where - F: Fn() -> T + Send + Clone + 'static, - T::Response: IoStream + Send, - T: NewService, - T::InitError: fmt::Debug, -{ - type Io = T::Response; - type NewService = T; - - fn create(&self) -> T { - (self)() - } -} - -#[derive(Clone)] -/// Default acceptor service convert `TcpStream` to a `tokio_tcp::TcpStream` -pub(crate) struct DefaultAcceptor; - -impl AcceptorServiceFactory for DefaultAcceptor { - type Io = TcpStream; - type NewService = DefaultAcceptor; - - fn create(&self) -> Self::NewService { - DefaultAcceptor - } -} - -impl NewService for DefaultAcceptor { - type Request = TcpStream; - type Response = TcpStream; - type Error = (); - type InitError = (); - type Service = DefaultAcceptor; - type Future = FutureResult; - - fn new_service(&self) -> Self::Future { - ok(DefaultAcceptor) - } -} - -impl Service for DefaultAcceptor { - type Request = TcpStream; - type Response = TcpStream; - type Error = (); - type Future = FutureResult; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - ok(req) - } -} - -pub(crate) struct TcpAcceptor { - inner: T, -} - -impl TcpAcceptor -where - T: NewService>, - T::InitError: fmt::Debug, -{ - pub(crate) fn new(inner: T) -> Self { - TcpAcceptor { inner } - } -} - -impl NewService for TcpAcceptor -where - T: NewService>, - T::InitError: fmt::Debug, -{ - type Request = net::TcpStream; - type Response = T::Response; - type Error = AcceptorError; - type InitError = T::InitError; - type Service = TcpAcceptorService; - type Future = TcpAcceptorResponse; - - fn new_service(&self) -> Self::Future { - TcpAcceptorResponse { - fut: self.inner.new_service(), - } - } -} - -pub(crate) struct TcpAcceptorResponse -where - T: NewService, - T::InitError: fmt::Debug, -{ - fut: T::Future, -} - -impl Future for TcpAcceptorResponse -where - T: NewService, - T::InitError: fmt::Debug, -{ - type Item = TcpAcceptorService; - type Error = T::InitError; - - fn poll(&mut self) -> Poll { - match self.fut.poll() { - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(service)) => { - Ok(Async::Ready(TcpAcceptorService { inner: service })) - } - Err(e) => { - error!("Can not create accetor service: {:?}", e); - Err(e) - } - } - } -} - -pub(crate) struct TcpAcceptorService { - inner: T, -} - -impl Service for TcpAcceptorService -where - T: Service>, -{ - type Request = net::TcpStream; - type Response = T::Response; - type Error = AcceptorError; - type Future = Either>; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.inner.poll_ready() - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - let stream = TcpStream::from_std(req, &Handle::default()).map_err(|e| { - error!("Can not convert to an async tcp stream: {}", e); - AcceptorError::Io(e) - }); - - match stream { - Ok(stream) => Either::A(self.inner.call(stream)), - Err(e) => Either::B(err(e)), - } - } -} - -#[doc(hidden)] -/// Acceptor timeout middleware -/// -/// Applies timeout to request prcoessing. -pub struct AcceptorTimeout { - inner: T, - timeout: Duration, -} - -impl AcceptorTimeout { - /// Create new `AcceptorTimeout` instance. timeout is in milliseconds. - pub fn new(timeout: u64, inner: T) -> Self { - Self { - inner, - timeout: Duration::from_millis(timeout), - } - } -} - -impl NewService for AcceptorTimeout { - type Request = T::Request; - type Response = T::Response; - type Error = AcceptorError; - type InitError = T::InitError; - type Service = AcceptorTimeoutService; - type Future = AcceptorTimeoutFut; - - fn new_service(&self) -> Self::Future { - AcceptorTimeoutFut { - fut: self.inner.new_service(), - timeout: self.timeout, - } - } -} - -#[doc(hidden)] -pub struct AcceptorTimeoutFut { - fut: T::Future, - timeout: Duration, -} - -impl Future for AcceptorTimeoutFut { - type Item = AcceptorTimeoutService; - type Error = T::InitError; - - fn poll(&mut self) -> Poll { - let inner = try_ready!(self.fut.poll()); - Ok(Async::Ready(AcceptorTimeoutService { - inner, - timeout: self.timeout, - })) - } -} - -#[doc(hidden)] -/// Acceptor timeout service -/// -/// Applies timeout to request prcoessing. -pub struct AcceptorTimeoutService { - inner: T, - timeout: Duration, -} - -impl Service for AcceptorTimeoutService { - type Request = T::Request; - type Response = T::Response; - type Error = AcceptorError; - type Future = AcceptorTimeoutResponse; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.inner.poll_ready().map_err(AcceptorError::Service) - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - AcceptorTimeoutResponse { - fut: self.inner.call(req), - sleep: sleep(self.timeout), - } - } -} - -#[doc(hidden)] -pub struct AcceptorTimeoutResponse { - fut: T::Future, - sleep: Delay, -} - -impl Future for AcceptorTimeoutResponse { - type Item = T::Response; - type Error = AcceptorError; - - fn poll(&mut self) -> Poll { - match self.fut.poll().map_err(AcceptorError::Service)? { - Async::NotReady => match self.sleep.poll() { - Err(_) => Err(AcceptorError::Timeout), - Ok(Async::Ready(_)) => Err(AcceptorError::Timeout), - Ok(Async::NotReady) => Ok(Async::NotReady), - }, - Async::Ready(resp) => Ok(Async::Ready(resp)), - } - } -} - -pub(crate) struct ServerMessageAcceptor { - inner: T, -} - -impl ServerMessageAcceptor -where - T: NewService, -{ - pub(crate) fn new(inner: T) -> Self { - ServerMessageAcceptor { inner } - } -} - -impl NewService for ServerMessageAcceptor -where - T: NewService, -{ - type Request = ServerMessage; - type Response = (); - type Error = T::Error; - type InitError = T::InitError; - type Service = ServerMessageAcceptorService; - type Future = ServerMessageAcceptorResponse; - - fn new_service(&self) -> Self::Future { - ServerMessageAcceptorResponse { - fut: self.inner.new_service(), - } - } -} - -pub(crate) struct ServerMessageAcceptorResponse -where - T: NewService, -{ - fut: T::Future, -} - -impl Future for ServerMessageAcceptorResponse -where - T: NewService, -{ - type Item = ServerMessageAcceptorService; - type Error = T::InitError; - - fn poll(&mut self) -> Poll { - match self.fut.poll()? { - Async::NotReady => Ok(Async::NotReady), - Async::Ready(service) => Ok(Async::Ready(ServerMessageAcceptorService { - inner: service, - })), - } - } -} - -pub(crate) struct ServerMessageAcceptorService { - inner: T, -} - -impl Service for ServerMessageAcceptorService -where - T: Service, -{ - type Request = ServerMessage; - type Response = (); - type Error = T::Error; - type Future = - Either, FutureResult<(), Self::Error>>; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.inner.poll_ready() - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - match req { - ServerMessage::Connect(stream) => { - Either::A(ServerMessageAcceptorServiceFut { - fut: self.inner.call(stream), - }) - } - ServerMessage::Shutdown(_) => Either::B(ok(())), - ServerMessage::ForceShutdown => { - // self.settings - // .head() - // .traverse(|proto: &mut HttpProtocol| proto.shutdown()); - Either::B(ok(())) - } - } - } -} - -pub(crate) struct ServerMessageAcceptorServiceFut { - fut: T::Future, -} - -impl Future for ServerMessageAcceptorServiceFut -where - T: Service, -{ - type Item = (); - type Error = T::Error; - - fn poll(&mut self) -> Poll { - match self.fut.poll()? { - Async::NotReady => Ok(Async::NotReady), - Async::Ready(_) => Ok(Async::Ready(())), - } - } -} diff --git a/src/server/builder.rs b/src/server/builder.rs deleted file mode 100644 index ea3638f1..00000000 --- a/src/server/builder.rs +++ /dev/null @@ -1,134 +0,0 @@ -use std::{fmt, net}; - -use actix_net::either::Either; -use actix_net::server::{Server, ServiceFactory}; -use actix_net::service::{NewService, NewServiceExt}; - -use super::acceptor::{ - AcceptorServiceFactory, AcceptorTimeout, ServerMessageAcceptor, TcpAcceptor, -}; -use super::error::AcceptorError; -use super::handler::IntoHttpHandler; -use super::service::{HttpService, StreamConfiguration}; -use super::settings::{ServerSettings, ServiceConfig}; -use super::KeepAlive; - -pub(crate) trait ServiceProvider { - fn register( - &self, - server: Server, - lst: net::TcpListener, - host: String, - addr: net::SocketAddr, - keep_alive: KeepAlive, - secure: bool, - client_timeout: u64, - client_shutdown: u64, - ) -> Server; -} - -/// Utility type that builds complete http pipeline -pub(crate) struct HttpServiceBuilder -where - F: Fn() -> H + Send + Clone, -{ - factory: F, - acceptor: A, -} - -impl HttpServiceBuilder -where - F: Fn() -> H + Send + Clone + 'static, - H: IntoHttpHandler, - A: AcceptorServiceFactory, - ::InitError: fmt::Debug, -{ - /// Create http service builder - pub fn new(factory: F, acceptor: A) -> Self { - Self { factory, acceptor } - } - - fn finish( - &self, - host: String, - addr: net::SocketAddr, - keep_alive: KeepAlive, - secure: bool, - client_timeout: u64, - client_shutdown: u64, - ) -> impl ServiceFactory { - let factory = self.factory.clone(); - let acceptor = self.acceptor.clone(); - move || { - let app = (factory)().into_handler(); - let settings = ServiceConfig::new( - app, - keep_alive, - client_timeout, - client_shutdown, - ServerSettings::new(addr, &host, false), - ); - - if secure { - Either::B(ServerMessageAcceptor::new( - TcpAcceptor::new(AcceptorTimeout::new( - client_timeout, - acceptor.create(), - )).map_err(|_| ()) - .map_init_err(|_| ()) - .and_then(StreamConfiguration::new().nodelay(true)) - .and_then( - HttpService::new(settings) - .map_init_err(|_| ()) - .map_err(|_| ()), - ), - )) - } else { - Either::A(ServerMessageAcceptor::new( - TcpAcceptor::new(acceptor.create().map_err(AcceptorError::Service)) - .map_err(|_| ()) - .map_init_err(|_| ()) - .and_then(StreamConfiguration::new().nodelay(true)) - .and_then( - HttpService::new(settings) - .map_init_err(|_| ()) - .map_err(|_| ()), - ), - )) - } - } - } -} - -impl ServiceProvider for HttpServiceBuilder -where - F: Fn() -> H + Send + Clone + 'static, - A: AcceptorServiceFactory, - ::InitError: fmt::Debug, - H: IntoHttpHandler, -{ - fn register( - &self, - server: Server, - lst: net::TcpListener, - host: String, - addr: net::SocketAddr, - keep_alive: KeepAlive, - secure: bool, - client_timeout: u64, - client_shutdown: u64, - ) -> Server { - server.listen2( - "actix-web", - lst, - self.finish( - host, - addr, - keep_alive, - secure, - client_timeout, - client_shutdown, - ), - ) - } -} diff --git a/src/server/channel.rs b/src/server/channel.rs deleted file mode 100644 index d65b05e8..00000000 --- a/src/server/channel.rs +++ /dev/null @@ -1,300 +0,0 @@ -use std::net::Shutdown; -use std::{io, mem, time}; - -use bytes::{Buf, BufMut, BytesMut}; -use futures::{Async, Future, Poll}; -use tokio_io::{AsyncRead, AsyncWrite}; -use tokio_timer::Delay; - -use super::error::HttpDispatchError; -use super::settings::ServiceConfig; -use super::{h1, h2, HttpHandler, IoStream}; -use http::StatusCode; - -const HTTP2_PREFACE: [u8; 14] = *b"PRI * HTTP/2.0"; - -pub(crate) enum HttpProtocol { - H1(h1::Http1Dispatcher), - H2(h2::Http2), - Unknown(ServiceConfig, T, BytesMut), - None, -} - -// impl HttpProtocol { -// fn shutdown_(&mut self) { -// match self { -// HttpProtocol::H1(ref mut h1) => { -// let io = h1.io(); -// let _ = IoStream::set_linger(io, Some(time::Duration::new(0, 0))); -// let _ = IoStream::shutdown(io, Shutdown::Both); -// } -// HttpProtocol::H2(ref mut h2) => h2.shutdown(), -// HttpProtocol::Unknown(_, io, _) => { -// let _ = IoStream::set_linger(io, Some(time::Duration::new(0, 0))); -// let _ = IoStream::shutdown(io, Shutdown::Both); -// } -// HttpProtocol::None => (), -// } -// } -// } - -enum ProtocolKind { - Http1, - Http2, -} - -#[doc(hidden)] -pub struct HttpChannel -where - T: IoStream, - H: HttpHandler + 'static, -{ - proto: HttpProtocol, - ka_timeout: Option, -} - -impl HttpChannel -where - T: IoStream, - H: HttpHandler + 'static, -{ - pub(crate) fn new(settings: ServiceConfig, io: T) -> HttpChannel { - let ka_timeout = settings.client_timer(); - - HttpChannel { - ka_timeout, - proto: HttpProtocol::Unknown(settings, io, BytesMut::with_capacity(8192)), - } - } -} - -impl Future for HttpChannel -where - T: IoStream, - H: HttpHandler + 'static, -{ - type Item = (); - type Error = HttpDispatchError; - - fn poll(&mut self) -> Poll { - // keep-alive timer - if self.ka_timeout.is_some() { - match self.ka_timeout.as_mut().unwrap().poll() { - Ok(Async::Ready(_)) => { - trace!("Slow request timed out, close connection"); - let proto = mem::replace(&mut self.proto, HttpProtocol::None); - if let HttpProtocol::Unknown(settings, io, buf) = proto { - self.proto = HttpProtocol::H1(h1::Http1Dispatcher::for_error( - settings, - io, - StatusCode::REQUEST_TIMEOUT, - self.ka_timeout.take(), - buf, - )); - return self.poll(); - } - return Ok(Async::Ready(())); - } - Ok(Async::NotReady) => (), - Err(_) => panic!("Something is really wrong"), - } - } - - let mut is_eof = false; - let kind = match self.proto { - HttpProtocol::H1(ref mut h1) => return h1.poll(), - HttpProtocol::H2(ref mut h2) => return h2.poll(), - HttpProtocol::Unknown(_, ref mut io, ref mut buf) => { - let mut err = None; - let mut disconnect = false; - match io.read_available(buf) { - Ok(Async::Ready((read_some, stream_closed))) => { - is_eof = stream_closed; - // Only disconnect if no data was read. - if is_eof && !read_some { - disconnect = true; - } - } - Err(e) => { - err = Some(e.into()); - } - _ => (), - } - if disconnect { - debug!("Ignored premature client disconnection"); - return Ok(Async::Ready(())); - } else if let Some(e) = err { - return Err(e); - } - - if buf.len() >= 14 { - if buf[..14] == HTTP2_PREFACE[..] { - ProtocolKind::Http2 - } else { - ProtocolKind::Http1 - } - } else { - return Ok(Async::NotReady); - } - } - HttpProtocol::None => unreachable!(), - }; - - // upgrade to specific http protocol - let proto = mem::replace(&mut self.proto, HttpProtocol::None); - if let HttpProtocol::Unknown(settings, io, buf) = proto { - match kind { - ProtocolKind::Http1 => { - self.proto = HttpProtocol::H1(h1::Http1Dispatcher::new( - settings, - io, - buf, - is_eof, - self.ka_timeout.take(), - )); - return self.poll(); - } - ProtocolKind::Http2 => { - self.proto = HttpProtocol::H2(h2::Http2::new( - settings, - io, - buf.freeze(), - self.ka_timeout.take(), - )); - return self.poll(); - } - } - } - unreachable!() - } -} - -#[doc(hidden)] -pub struct H1Channel -where - T: IoStream, - H: HttpHandler + 'static, -{ - proto: HttpProtocol, -} - -impl H1Channel -where - T: IoStream, - H: HttpHandler + 'static, -{ - pub(crate) fn new(settings: ServiceConfig, io: T) -> H1Channel { - H1Channel { - proto: HttpProtocol::H1(h1::Http1Dispatcher::new( - settings, - io, - BytesMut::with_capacity(8192), - false, - None, - )), - } - } -} - -impl Future for H1Channel -where - T: IoStream, - H: HttpHandler + 'static, -{ - type Item = (); - type Error = HttpDispatchError; - - fn poll(&mut self) -> Poll { - match self.proto { - HttpProtocol::H1(ref mut h1) => h1.poll(), - _ => unreachable!(), - } - } -} - -/// Wrapper for `AsyncRead + AsyncWrite` types -pub(crate) struct WrapperStream -where - T: AsyncRead + AsyncWrite + 'static, -{ - io: T, -} - -impl WrapperStream -where - T: AsyncRead + AsyncWrite + 'static, -{ - pub fn new(io: T) -> Self { - WrapperStream { io } - } -} - -impl IoStream for WrapperStream -where - T: AsyncRead + AsyncWrite + 'static, -{ - #[inline] - fn shutdown(&mut self, _: Shutdown) -> io::Result<()> { - Ok(()) - } - #[inline] - fn set_nodelay(&mut self, _: bool) -> io::Result<()> { - Ok(()) - } - #[inline] - fn set_linger(&mut self, _: Option) -> io::Result<()> { - Ok(()) - } - #[inline] - fn set_keepalive(&mut self, _: Option) -> io::Result<()> { - Ok(()) - } -} - -impl io::Read for WrapperStream -where - T: AsyncRead + AsyncWrite + 'static, -{ - #[inline] - fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.io.read(buf) - } -} - -impl io::Write for WrapperStream -where - T: AsyncRead + AsyncWrite + 'static, -{ - #[inline] - fn write(&mut self, buf: &[u8]) -> io::Result { - self.io.write(buf) - } - #[inline] - fn flush(&mut self) -> io::Result<()> { - self.io.flush() - } -} - -impl AsyncRead for WrapperStream -where - T: AsyncRead + AsyncWrite + 'static, -{ - #[inline] - fn read_buf(&mut self, buf: &mut B) -> Poll { - self.io.read_buf(buf) - } -} - -impl AsyncWrite for WrapperStream -where - T: AsyncRead + AsyncWrite + 'static, -{ - #[inline] - fn shutdown(&mut self) -> Poll<(), io::Error> { - self.io.shutdown() - } - #[inline] - fn write_buf(&mut self, buf: &mut B) -> Poll { - self.io.write_buf(buf) - } -} diff --git a/src/server/error.rs b/src/server/error.rs deleted file mode 100644 index 70f10099..00000000 --- a/src/server/error.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::io; - -use futures::{Async, Poll}; -use http2; - -use super::{helpers, HttpHandlerTask, Writer}; -use http::{StatusCode, Version}; -use Error; - -/// Errors produced by `AcceptorError` service. -#[derive(Debug)] -pub enum AcceptorError { - /// The inner service error - Service(T), - - /// Io specific error - Io(io::Error), - - /// The request did not complete within the specified timeout. - Timeout, -} - -#[derive(Fail, Debug)] -/// A set of errors that can occur during dispatching http requests -pub enum HttpDispatchError { - /// Application error - #[fail(display = "Application specific error: {}", _0)] - App(Error), - - /// An `io::Error` that occurred while trying to read or write to a network - /// stream. - #[fail(display = "IO error: {}", _0)] - Io(io::Error), - - /// The first request did not complete within the specified timeout. - #[fail(display = "The first request did not complete within the specified timeout")] - SlowRequestTimeout, - - /// Shutdown timeout - #[fail(display = "Connection shutdown timeout")] - ShutdownTimeout, - - /// HTTP2 error - #[fail(display = "HTTP2 error: {}", _0)] - Http2(http2::Error), - - /// Payload is not consumed - #[fail(display = "Task is completed but request's payload is not consumed")] - PayloadIsNotConsumed, - - /// Malformed request - #[fail(display = "Malformed request")] - MalformedRequest, - - /// Internal error - #[fail(display = "Internal error")] - InternalError, - - /// Unknown error - #[fail(display = "Unknown error")] - Unknown, -} - -impl From for HttpDispatchError { - fn from(err: Error) -> Self { - HttpDispatchError::App(err) - } -} - -impl From for HttpDispatchError { - fn from(err: io::Error) -> Self { - HttpDispatchError::Io(err) - } -} - -impl From for HttpDispatchError { - fn from(err: http2::Error) -> Self { - HttpDispatchError::Http2(err) - } -} - -pub(crate) struct ServerError(Version, StatusCode); - -impl ServerError { - pub fn err(ver: Version, status: StatusCode) -> Box { - Box::new(ServerError(ver, status)) - } -} - -impl HttpHandlerTask for ServerError { - fn poll_io(&mut self, io: &mut Writer) -> Poll { - { - let bytes = io.buffer(); - // Buffer should have sufficient capacity for status line - // and extra space - bytes.reserve(helpers::STATUS_LINE_BUF_SIZE + 1); - helpers::write_status_line(self.0, self.1.as_u16(), bytes); - } - // Convert Status Code to Reason. - let reason = self.1.canonical_reason().unwrap_or(""); - io.buffer().extend_from_slice(reason.as_bytes()); - // No response body. - io.buffer().extend_from_slice(b"\r\ncontent-length: 0\r\n"); - // date header - io.set_date(); - Ok(Async::Ready(true)) - } -} diff --git a/src/server/h1.rs b/src/server/h1.rs deleted file mode 100644 index fa7d2fda..00000000 --- a/src/server/h1.rs +++ /dev/null @@ -1,1353 +0,0 @@ -use std::collections::VecDeque; -use std::net::{Shutdown, SocketAddr}; -use std::time::{Duration, Instant}; - -use bytes::BytesMut; -use futures::{Async, Future, Poll}; -use tokio_current_thread::spawn; -use tokio_timer::Delay; - -use body::Binary; -use error::{Error, PayloadError}; -use http::{StatusCode, Version}; -use payload::{Payload, PayloadStatus, PayloadWriter}; - -use super::error::{HttpDispatchError, ServerError}; -use super::h1decoder::{DecoderError, H1Decoder, Message}; -use super::h1writer::H1Writer; -use super::handler::{HttpHandler, HttpHandlerTask, HttpHandlerTaskFut}; -use super::input::PayloadType; -use super::settings::ServiceConfig; -use super::{IoStream, Writer}; - -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 SHUTDOWN = 0b0000_1000; - const READ_DISCONNECTED = 0b0001_0000; - const WRITE_DISCONNECTED = 0b0010_0000; - const POLLED = 0b0100_0000; - const FLUSHED = 0b1000_0000; - } -} - -/// Dispatcher for HTTP/1.1 protocol -pub struct Http1Dispatcher { - flags: Flags, - settings: ServiceConfig, - addr: Option, - stream: H1Writer, - decoder: H1Decoder, - payload: Option, - buf: BytesMut, - tasks: VecDeque>, - error: Option, - ka_expire: Instant, - ka_timer: Option, -} - -enum Entry { - Task(H::Task, Option<()>), - Error(Box), -} - -impl Entry { - fn into_task(self) -> H::Task { - match self { - Entry::Task(task, _) => task, - Entry::Error(_) => panic!(), - } - } - fn disconnected(&mut self) { - match *self { - Entry::Task(ref mut task, _) => task.disconnected(), - Entry::Error(ref mut task) => task.disconnected(), - } - } - fn poll_io(&mut self, io: &mut Writer) -> Poll { - match *self { - Entry::Task(ref mut task, ref mut except) => { - match except { - Some(_) => { - let _ = io.write(&Binary::from("HTTP/1.1 100 Continue\r\n\r\n")); - } - _ => (), - }; - task.poll_io(io) - } - Entry::Error(ref mut task) => task.poll_io(io), - } - } - fn poll_completed(&mut self) -> Poll<(), Error> { - match *self { - Entry::Task(ref mut task, _) => task.poll_completed(), - Entry::Error(ref mut task) => task.poll_completed(), - } - } -} - -impl Http1Dispatcher -where - T: IoStream, - H: HttpHandler + 'static, -{ - pub fn new( - settings: ServiceConfig, - stream: T, - buf: BytesMut, - is_eof: bool, - keepalive_timer: Option, - ) -> Self { - let addr = stream.peer_addr(); - let (ka_expire, ka_timer) = if let Some(delay) = keepalive_timer { - (delay.deadline(), Some(delay)) - } else if let Some(delay) = settings.keep_alive_timer() { - (delay.deadline(), Some(delay)) - } else { - (settings.now(), None) - }; - - let flags = if is_eof { - Flags::READ_DISCONNECTED | Flags::FLUSHED - } else if settings.keep_alive_enabled() { - Flags::KEEPALIVE | Flags::KEEPALIVE_ENABLED | Flags::FLUSHED - } else { - Flags::empty() - }; - - Http1Dispatcher { - stream: H1Writer::new(stream, settings.clone()), - decoder: H1Decoder::new(), - payload: None, - tasks: VecDeque::new(), - error: None, - flags, - addr, - buf, - settings, - ka_timer, - ka_expire, - } - } - - pub(crate) fn for_error( - settings: ServiceConfig, - stream: T, - status: StatusCode, - mut keepalive_timer: Option, - buf: BytesMut, - ) -> Self { - if let Some(deadline) = settings.client_timer_expire() { - let _ = keepalive_timer.as_mut().map(|delay| delay.reset(deadline)); - } - - let mut disp = Http1Dispatcher { - flags: Flags::STARTED | Flags::READ_DISCONNECTED | Flags::FLUSHED, - stream: H1Writer::new(stream, settings.clone()), - decoder: H1Decoder::new(), - payload: None, - tasks: VecDeque::new(), - error: None, - addr: None, - ka_timer: keepalive_timer, - ka_expire: settings.now(), - buf, - settings, - }; - disp.push_response_entry(status); - disp - } - - #[inline] - fn can_read(&self) -> bool { - if self.flags.contains(Flags::READ_DISCONNECTED) { - return false; - } - - if let Some(ref info) = self.payload { - info.need_read() == PayloadStatus::Read - } else { - true - } - } - - // if checked is set to true, delay disconnect until all tasks have finished. - fn client_disconnected(&mut self, checked: bool) { - self.flags.insert(Flags::READ_DISCONNECTED); - if let Some(mut payload) = self.payload.take() { - payload.set_error(PayloadError::Incomplete); - } - - if !checked || self.tasks.is_empty() { - self.flags - .insert(Flags::WRITE_DISCONNECTED | Flags::FLUSHED); - self.stream.disconnected(); - - // notify all tasks - for mut task in self.tasks.drain(..) { - task.disconnected(); - match task.poll_completed() { - Ok(Async::NotReady) => { - // spawn not completed task, it does not require access to io - // at this point - spawn(HttpHandlerTaskFut::new(task.into_task())); - } - Ok(Async::Ready(_)) => (), - Err(err) => { - error!("Unhandled application error: {}", err); - } - } - } - } - } - - #[inline] - pub fn poll(&mut self) -> Poll<(), HttpDispatchError> { - // check connection keep-alive - self.poll_keepalive()?; - - // shutdown - if self.flags.contains(Flags::SHUTDOWN) { - if self.flags.contains(Flags::WRITE_DISCONNECTED) { - return Ok(Async::Ready(())); - } - return self.poll_flush(true); - } - - // process incoming requests - if !self.flags.contains(Flags::WRITE_DISCONNECTED) { - self.poll_handler()?; - - // flush stream - self.poll_flush(false)?; - - // deal with keep-alive and stream eof (client-side write shutdown) - if self.tasks.is_empty() && self.flags.contains(Flags::FLUSHED) { - // handle stream eof - if self - .flags - .intersects(Flags::READ_DISCONNECTED | Flags::WRITE_DISCONNECTED) - { - return Ok(Async::Ready(())); - } - // no keep-alive - if self.flags.contains(Flags::STARTED) - && (!self.flags.contains(Flags::KEEPALIVE_ENABLED) - || !self.flags.contains(Flags::KEEPALIVE)) - { - self.flags.insert(Flags::SHUTDOWN); - return self.poll(); - } - } - Ok(Async::NotReady) - } else if let Some(err) = self.error.take() { - Err(err) - } else { - Ok(Async::Ready(())) - } - } - - /// Flush stream - fn poll_flush(&mut self, shutdown: bool) -> Poll<(), HttpDispatchError> { - if shutdown || self.flags.contains(Flags::STARTED) { - match self.stream.poll_completed(shutdown) { - Ok(Async::NotReady) => { - // mark stream - if !self.stream.flushed() { - self.flags.remove(Flags::FLUSHED); - } - Ok(Async::NotReady) - } - Err(err) => { - debug!("Error sending data: {}", err); - self.client_disconnected(false); - Err(err.into()) - } - Ok(Async::Ready(_)) => { - // if payload is not consumed we can not use connection - if self.payload.is_some() && self.tasks.is_empty() { - return Err(HttpDispatchError::PayloadIsNotConsumed); - } - self.flags.insert(Flags::FLUSHED); - Ok(Async::Ready(())) - } - } - } else { - Ok(Async::Ready(())) - } - } - - /// keep-alive timer. returns `true` is keep-alive, otherwise drop - fn poll_keepalive(&mut self) -> Result<(), HttpDispatchError> { - if let Some(ref mut timer) = self.ka_timer { - match timer.poll() { - Ok(Async::Ready(_)) => { - // if we get timer during shutdown, just drop connection - if self.flags.contains(Flags::SHUTDOWN) { - let io = self.stream.get_mut(); - let _ = IoStream::set_linger(io, Some(Duration::from_secs(0))); - let _ = IoStream::shutdown(io, Shutdown::Both); - return Err(HttpDispatchError::ShutdownTimeout); - } - if timer.deadline() >= self.ka_expire { - // check for any outstanding request handling - if self.tasks.is_empty() && self.flags.contains(Flags::FLUSHED) { - if !self.flags.contains(Flags::STARTED) { - // timeout on first request (slow request) return 408 - trace!("Slow request timeout"); - self.flags - .insert(Flags::STARTED | Flags::READ_DISCONNECTED); - self.tasks.push_back(Entry::Error(ServerError::err( - Version::HTTP_11, - StatusCode::REQUEST_TIMEOUT, - ))); - } else { - trace!("Keep-alive timeout, close connection"); - self.flags.insert(Flags::SHUTDOWN); - - // start shutdown timer - if let Some(deadline) = - self.settings.client_shutdown_timer() - { - timer.reset(deadline); - let _ = timer.poll(); - } else { - return Ok(()); - } - } - } else if let Some(dl) = self.settings.keep_alive_expire() { - timer.reset(dl); - let _ = timer.poll(); - } - } else { - timer.reset(self.ka_expire); - let _ = timer.poll(); - } - } - Ok(Async::NotReady) => (), - Err(e) => { - error!("Timer error {:?}", e); - return Err(HttpDispatchError::Unknown); - } - } - } - - Ok(()) - } - - #[inline] - /// read data from the stream - pub(self) fn poll_io(&mut self) -> Result { - if !self.flags.contains(Flags::POLLED) { - self.flags.insert(Flags::POLLED); - if !self.buf.is_empty() { - let updated = self.parse()?; - return Ok(updated); - } - } - - // read io from socket - let mut updated = false; - if self.can_read() && self.tasks.len() < MAX_PIPELINED_MESSAGES { - match self.stream.get_mut().read_available(&mut self.buf) { - Ok(Async::Ready((read_some, disconnected))) => { - if read_some && self.parse()? { - updated = true; - } - if disconnected { - self.client_disconnected(true); - } - } - Ok(Async::NotReady) => (), - Err(err) => { - self.client_disconnected(false); - return Err(err.into()); - } - } - } - Ok(updated) - } - - pub(self) fn poll_handler(&mut self) -> Result<(), HttpDispatchError> { - self.poll_io()?; - let mut retry = self.can_read(); - - // process first pipelined response, only first task can do io operation in http/1 - while !self.tasks.is_empty() { - match self.tasks[0].poll_io(&mut self.stream) { - Ok(Async::Ready(ready)) => { - // override keep-alive state - if self.stream.keepalive() { - self.flags.insert(Flags::KEEPALIVE); - } else { - self.flags.remove(Flags::KEEPALIVE); - } - // prepare stream for next response - self.stream.reset(); - - let task = self.tasks.pop_front().unwrap(); - if !ready { - // task is done with io operations but still needs to do more work - spawn(HttpHandlerTaskFut::new(task.into_task())); - } - } - Ok(Async::NotReady) => { - // check if we need timer - if self.ka_timer.is_some() && self.stream.upgrade() { - self.ka_timer.take(); - } - - // if read-backpressure is enabled and we consumed some data. - // we may read more dataand retry - if !retry && self.can_read() && self.poll_io()? { - retry = self.can_read(); - continue; - } - break; - } - Err(err) => { - error!("Unhandled error1: {}", err); - // it is not possible to recover from error - // during pipe handling, so just drop connection - self.client_disconnected(false); - return Err(err.into()); - } - } - } - - // check in-flight messages. all tasks must be alive, - // they need to produce response. if app returned error - // and we can not continue processing incoming requests. - let mut idx = 1; - while idx < self.tasks.len() { - let stop = match self.tasks[idx].poll_completed() { - Ok(Async::NotReady) => false, - Ok(Async::Ready(_)) => true, - Err(err) => { - self.error = Some(err.into()); - true - } - }; - if stop { - // error in task handling or task is completed, - // so no response for this task which means we can not read more requests - // because pipeline sequence is broken. - // but we can safely complete existing tasks - self.flags.insert(Flags::READ_DISCONNECTED); - - for mut task in self.tasks.drain(idx..) { - task.disconnected(); - match task.poll_completed() { - Ok(Async::NotReady) => { - // spawn not completed task, it does not require access to io - // at this point - spawn(HttpHandlerTaskFut::new(task.into_task())); - } - Ok(Async::Ready(_)) => (), - Err(err) => { - error!("Unhandled application error: {}", err); - } - } - } - break; - } else { - idx += 1; - } - } - - Ok(()) - } - - fn push_response_entry(&mut self, status: StatusCode) { - self.tasks - .push_back(Entry::Error(ServerError::err(Version::HTTP_11, status))); - } - - pub(self) fn parse(&mut self) -> Result { - let mut updated = false; - - 'outer: loop { - match self.decoder.decode(&mut self.buf, &self.settings) { - Ok(Some(Message::Message { - mut msg, - mut expect, - payload, - })) => { - updated = true; - self.flags.insert(Flags::STARTED); - - if payload { - let (ps, pl) = Payload::new(false); - *msg.inner.payload.borrow_mut() = Some(pl); - self.payload = Some(PayloadType::new(&msg.inner.headers, ps)); - } - - // stream extensions - msg.inner_mut().stream_extensions = - self.stream.get_mut().extensions(); - - // set remote addr - msg.inner_mut().addr = self.addr; - - // search handler for request - match self.settings.handler().handle(msg) { - Ok(mut task) => { - if self.tasks.is_empty() { - if expect { - expect = false; - let _ = self.stream.write(&Binary::from( - "HTTP/1.1 100 Continue\r\n\r\n", - )); - } - match task.poll_io(&mut self.stream) { - Ok(Async::Ready(ready)) => { - // override keep-alive state - if self.stream.keepalive() { - self.flags.insert(Flags::KEEPALIVE); - } else { - self.flags.remove(Flags::KEEPALIVE); - } - // prepare stream for next response - self.stream.reset(); - - if !ready { - // task is done with io operations - // but still needs to do more work - spawn(HttpHandlerTaskFut::new(task)); - } - continue 'outer; - } - Ok(Async::NotReady) => (), - Err(err) => { - error!("Unhandled error: {}", err); - self.client_disconnected(false); - return Err(err.into()); - } - } - } - self.tasks.push_back(Entry::Task( - task, - if expect { Some(()) } else { None }, - )); - continue 'outer; - } - Err(_) => { - // handler is not found - self.push_response_entry(StatusCode::NOT_FOUND); - } - } - } - Ok(Some(Message::Chunk(chunk))) => { - updated = true; - if let Some(ref mut payload) = self.payload { - payload.feed_data(chunk); - } else { - error!("Internal server error: unexpected payload chunk"); - self.flags.insert(Flags::READ_DISCONNECTED | Flags::STARTED); - self.push_response_entry(StatusCode::INTERNAL_SERVER_ERROR); - self.error = Some(HttpDispatchError::InternalError); - break; - } - } - Ok(Some(Message::Eof)) => { - updated = true; - if let Some(mut payload) = self.payload.take() { - payload.feed_eof(); - } else { - error!("Internal server error: unexpected eof"); - self.flags.insert(Flags::READ_DISCONNECTED | Flags::STARTED); - self.push_response_entry(StatusCode::INTERNAL_SERVER_ERROR); - self.error = Some(HttpDispatchError::InternalError); - break; - } - } - Ok(None) => { - if self.flags.contains(Flags::READ_DISCONNECTED) { - self.client_disconnected(true); - } - break; - } - Err(e) => { - if let Some(mut payload) = self.payload.take() { - let e = match e { - DecoderError::Io(e) => PayloadError::Io(e), - DecoderError::Error(_) => PayloadError::EncodingCorrupted, - }; - payload.set_error(e); - } - - // Malformed requests should be responded with 400 - self.push_response_entry(StatusCode::BAD_REQUEST); - self.flags.insert(Flags::READ_DISCONNECTED | Flags::STARTED); - self.error = Some(HttpDispatchError::MalformedRequest); - break; - } - } - } - - if self.ka_timer.is_some() && updated { - if let Some(expire) = self.settings.keep_alive_expire() { - self.ka_expire = expire; - } - } - Ok(updated) - } -} - -#[cfg(test)] -mod tests { - use std::net::Shutdown; - use std::{cmp, io, time}; - - use actix::System; - use bytes::{Buf, Bytes, BytesMut}; - use futures::future; - use http::{Method, Version}; - use tokio_io::{AsyncRead, AsyncWrite}; - - use super::*; - use application::{App, HttpApplication}; - use httpmessage::HttpMessage; - use server::h1decoder::Message; - use server::handler::IntoHttpHandler; - use server::settings::{ServerSettings, ServiceConfig}; - use server::{KeepAlive, Request}; - - fn wrk_settings() -> ServiceConfig { - ServiceConfig::::new( - App::new().into_handler(), - KeepAlive::Os, - 5000, - 2000, - ServerSettings::default(), - ) - } - - impl Message { - fn message(self) -> Request { - match self { - Message::Message { msg, .. } => msg, - _ => panic!("error"), - } - } - fn is_payload(&self) -> bool { - match *self { - Message::Message { payload, .. } => payload, - _ => panic!("error"), - } - } - fn chunk(self) -> Bytes { - match self { - Message::Chunk(chunk) => chunk, - _ => panic!("error"), - } - } - fn eof(&self) -> bool { - match *self { - Message::Eof => true, - _ => false, - } - } - } - - macro_rules! parse_ready { - ($e:expr) => {{ - let settings = wrk_settings(); - match H1Decoder::new().decode($e, &settings) { - Ok(Some(msg)) => msg.message(), - Ok(_) => unreachable!("Eof during parsing http request"), - Err(err) => unreachable!("Error during parsing http request: {:?}", err), - } - }}; - } - - macro_rules! expect_parse_err { - ($e:expr) => {{ - let settings = wrk_settings(); - - match H1Decoder::new().decode($e, &settings) { - Err(err) => match err { - DecoderError::Error(_) => (), - _ => unreachable!("Parse error expected"), - }, - _ => unreachable!("Error expected"), - } - }}; - } - - 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 IoStream for Buffer { - fn shutdown(&mut self, _: Shutdown) -> io::Result<()> { - Ok(()) - } - fn set_nodelay(&mut self, _: bool) -> io::Result<()> { - Ok(()) - } - fn set_linger(&mut self, _: Option) -> io::Result<()> { - Ok(()) - } - fn set_keepalive(&mut self, _: Option) -> io::Result<()> { - Ok(()) - } - } - impl io::Write for Buffer { - fn write(&mut self, buf: &[u8]) -> io::Result { - Ok(buf.len()) - } - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } - } - impl AsyncWrite for Buffer { - fn shutdown(&mut self) -> Poll<(), io::Error> { - Ok(Async::Ready(())) - } - fn write_buf(&mut self, _: &mut B) -> Poll { - Ok(Async::NotReady) - } - } - - #[test] - fn test_req_parse_err() { - let mut sys = System::new("test"); - let _ = sys.block_on(future::lazy(|| { - let buf = Buffer::new("GET /test HTTP/1\r\n\r\n"); - let readbuf = BytesMut::new(); - let settings = wrk_settings(); - - let mut h1 = - Http1Dispatcher::new(settings.clone(), buf, readbuf, false, None); - assert!(h1.poll_io().is_ok()); - assert!(h1.poll_io().is_ok()); - assert!(h1.flags.contains(Flags::READ_DISCONNECTED)); - assert_eq!(h1.tasks.len(), 1); - future::ok::<_, ()>(()) - })); - } - - #[test] - fn test_parse() { - let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n\r\n"); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(); - match reader.decode(&mut buf, &settings) { - Ok(Some(msg)) => { - let req = msg.message(); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(*req.method(), Method::GET); - assert_eq!(req.path(), "/test"); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } - } - - #[test] - fn test_parse_partial() { - let mut buf = BytesMut::from("PUT /test HTTP/1"); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(); - match reader.decode(&mut buf, &settings) { - Ok(None) => (), - _ => unreachable!("Error"), - } - - buf.extend(b".1\r\n\r\n"); - match reader.decode(&mut buf, &settings) { - Ok(Some(msg)) => { - let mut req = msg.message(); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(*req.method(), Method::PUT); - assert_eq!(req.path(), "/test"); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } - } - - #[test] - fn test_parse_post() { - let mut buf = BytesMut::from("POST /test2 HTTP/1.0\r\n\r\n"); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(); - match reader.decode(&mut buf, &settings) { - Ok(Some(msg)) => { - let mut req = msg.message(); - assert_eq!(req.version(), Version::HTTP_10); - assert_eq!(*req.method(), Method::POST); - assert_eq!(req.path(), "/test2"); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } - } - - #[test] - fn test_parse_body() { - let mut buf = - BytesMut::from("GET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody"); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(); - match reader.decode(&mut buf, &settings) { - Ok(Some(msg)) => { - let mut req = msg.message(); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(*req.method(), Method::GET); - assert_eq!(req.path(), "/test"); - assert_eq!( - reader - .decode(&mut buf, &settings) - .unwrap() - .unwrap() - .chunk() - .as_ref(), - b"body" - ); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } - } - - #[test] - fn test_parse_body_crlf() { - let mut buf = - BytesMut::from("\r\nGET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody"); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(); - match reader.decode(&mut buf, &settings) { - Ok(Some(msg)) => { - let mut req = msg.message(); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(*req.method(), Method::GET); - assert_eq!(req.path(), "/test"); - assert_eq!( - reader - .decode(&mut buf, &settings) - .unwrap() - .unwrap() - .chunk() - .as_ref(), - b"body" - ); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } - } - - #[test] - fn test_parse_partial_eof() { - let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n"); - let settings = wrk_settings(); - let mut reader = H1Decoder::new(); - assert!(reader.decode(&mut buf, &settings).unwrap().is_none()); - - buf.extend(b"\r\n"); - match reader.decode(&mut buf, &settings) { - Ok(Some(msg)) => { - let req = msg.message(); - assert_eq!(req.version(), Version::HTTP_11); - assert_eq!(*req.method(), Method::GET); - assert_eq!(req.path(), "/test"); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } - } - - #[test] - fn test_headers_split_field() { - let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n"); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(); - assert! { reader.decode(&mut buf, &settings).unwrap().is_none() } - - buf.extend(b"t"); - assert! { reader.decode(&mut buf, &settings).unwrap().is_none() } - - buf.extend(b"es"); - assert! { reader.decode(&mut buf, &settings).unwrap().is_none() } - - buf.extend(b"t: value\r\n\r\n"); - match reader.decode(&mut buf, &settings) { - Ok(Some(msg)) => { - let req = msg.message(); - 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"); - } - Ok(_) | Err(_) => unreachable!("Error during parsing http request"), - } - } - - #[test] - fn test_headers_multi_value() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - Set-Cookie: c1=cookie1\r\n\ - Set-Cookie: c2=cookie2\r\n\r\n", - ); - let settings = wrk_settings(); - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - let req = msg.message(); - - let val: Vec<_> = req - .headers() - .get_all("Set-Cookie") - .iter() - .map(|v| v.to_str().unwrap().to_owned()) - .collect(); - assert_eq!(val[0], "c1=cookie1"); - assert_eq!(val[1], "c2=cookie2"); - } - - #[test] - fn test_conn_default_1_0() { - let mut buf = BytesMut::from("GET /test HTTP/1.0\r\n\r\n"); - let req = parse_ready!(&mut buf); - - assert!(!req.keep_alive()); - } - - #[test] - fn test_conn_default_1_1() { - let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n\r\n"); - let req = parse_ready!(&mut buf); - - assert!(req.keep_alive()); - } - - #[test] - fn test_conn_close() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - connection: close\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(!req.keep_alive()); - - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - connection: Close\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(!req.keep_alive()); - } - - #[test] - fn test_conn_close_1_0() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.0\r\n\ - connection: close\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(!req.keep_alive()); - - let mut buf = BytesMut::from( - "GET /test HTTP/1.0\r\n\ - connection: Close\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(!req.keep_alive()); - } - - #[test] - fn test_conn_keep_alive_1_0() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.0\r\n\ - connection: Keep-Alive\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(req.keep_alive()); - - let mut buf = BytesMut::from( - "GET /test HTTP/1.0\r\n\ - connection: keep-alive\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(req.keep_alive()); - } - - #[test] - fn test_conn_keep_alive_1_1() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - connection: keep-alive\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(req.keep_alive()); - } - - #[test] - fn test_conn_other_1_0() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.0\r\n\ - connection: other\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(!req.keep_alive()); - } - - #[test] - fn test_conn_other_1_1() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - connection: other\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(req.keep_alive()); - } - - #[test] - fn test_conn_upgrade() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - upgrade: websockets\r\n\ - connection: upgrade\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(req.upgrade()); - - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - upgrade: Websockets\r\n\ - connection: Upgrade\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(req.upgrade()); - } - - #[test] - fn test_conn_upgrade_connect_method() { - let mut buf = BytesMut::from( - "CONNECT /test HTTP/1.1\r\n\ - content-type: text/plain\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert!(req.upgrade()); - } - - #[test] - fn test_request_chunked() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - if let Ok(val) = req.chunked() { - assert!(val); - } else { - unreachable!("Error"); - } - - // type in chunked - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chnked\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - if let Ok(val) = req.chunked() { - assert!(!val); - } else { - unreachable!("Error"); - } - } - - #[test] - fn test_headers_content_length_err_1() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - content-length: line\r\n\r\n", - ); - - expect_parse_err!(&mut buf) - } - - #[test] - fn test_headers_content_length_err_2() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - content-length: -1\r\n\r\n", - ); - - expect_parse_err!(&mut buf); - } - - #[test] - fn test_invalid_header() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - test line\r\n\r\n", - ); - - expect_parse_err!(&mut buf); - } - - #[test] - fn test_invalid_name() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - test[]: line\r\n\r\n", - ); - - expect_parse_err!(&mut buf); - } - - #[test] - fn test_http_request_bad_status_line() { - let mut buf = BytesMut::from("getpath \r\n\r\n"); - expect_parse_err!(&mut buf); - } - - #[test] - fn test_http_request_upgrade() { - let settings = wrk_settings(); - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - connection: upgrade\r\n\ - upgrade: websocket\r\n\r\n\ - some raw data", - ); - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert!(msg.is_payload()); - let req = msg.message(); - assert!(!req.keep_alive()); - assert!(req.upgrade()); - assert_eq!( - reader - .decode(&mut buf, &settings) - .unwrap() - .unwrap() - .chunk() - .as_ref(), - b"some raw data" - ); - } - - #[test] - fn test_http_request_parser_utf8() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - x-test: теÑÑ‚\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - assert_eq!( - req.headers().get("x-test").unwrap().as_bytes(), - "теÑÑ‚".as_bytes() - ); - } - - #[test] - fn test_http_request_parser_two_slashes() { - let mut buf = BytesMut::from("GET //path HTTP/1.1\r\n\r\n"); - let req = parse_ready!(&mut buf); - - assert_eq!(req.path(), "//path"); - } - - #[test] - fn test_http_request_parser_bad_method() { - let mut buf = BytesMut::from("!12%()+=~$ /get HTTP/1.1\r\n\r\n"); - - expect_parse_err!(&mut buf); - } - - #[test] - fn test_http_request_parser_bad_version() { - let mut buf = BytesMut::from("GET //get HT/11\r\n\r\n"); - - expect_parse_err!(&mut buf); - } - - #[test] - fn test_http_request_chunked_payload() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - let settings = wrk_settings(); - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert!(msg.is_payload()); - let req = msg.message(); - assert!(req.chunked().unwrap()); - - buf.extend(b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); - assert_eq!( - reader - .decode(&mut buf, &settings) - .unwrap() - .unwrap() - .chunk() - .as_ref(), - b"data" - ); - assert_eq!( - reader - .decode(&mut buf, &settings) - .unwrap() - .unwrap() - .chunk() - .as_ref(), - b"line" - ); - assert!(reader.decode(&mut buf, &settings).unwrap().unwrap().eof()); - } - - #[test] - fn test_http_request_chunked_payload_and_next_message() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - let settings = wrk_settings(); - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert!(msg.is_payload()); - let req = msg.message(); - assert!(req.chunked().unwrap()); - - buf.extend( - b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n\ - POST /test2 HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n" - .iter(), - ); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"data"); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"line"); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert!(msg.eof()); - - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert!(msg.is_payload()); - let req2 = msg.message(); - assert!(req2.chunked().unwrap()); - assert_eq!(*req2.method(), Method::POST); - assert!(req2.chunked().unwrap()); - } - - #[test] - fn test_http_request_chunked_payload_chunks() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert!(msg.is_payload()); - let req = msg.message(); - assert!(req.chunked().unwrap()); - - buf.extend(b"4\r\n1111\r\n"); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"1111"); - - buf.extend(b"4\r\ndata\r"); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"data"); - - buf.extend(b"\n4"); - assert!(reader.decode(&mut buf, &settings).unwrap().is_none()); - - buf.extend(b"\r"); - assert!(reader.decode(&mut buf, &settings).unwrap().is_none()); - buf.extend(b"\n"); - assert!(reader.decode(&mut buf, &settings).unwrap().is_none()); - - buf.extend(b"li"); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"li"); - - //trailers - //buf.feed_data("test: test\r\n"); - //not_ready!(reader.parse(&mut buf, &mut readbuf)); - - buf.extend(b"ne\r\n0\r\n"); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"ne"); - assert!(reader.decode(&mut buf, &settings).unwrap().is_none()); - - buf.extend(b"\r\n"); - assert!(reader.decode(&mut buf, &settings).unwrap().unwrap().eof()); - } - - #[test] - fn test_parse_chunked_payload_chunk_extension() { - let mut buf = BytesMut::from( - &"GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n"[..], - ); - let settings = wrk_settings(); - - let mut reader = H1Decoder::new(); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert!(msg.is_payload()); - assert!(msg.message().chunked().unwrap()); - - buf.extend(b"4;test\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); // test: test\r\n\r\n") - let chunk = reader.decode(&mut buf, &settings).unwrap().unwrap().chunk(); - assert_eq!(chunk, Bytes::from_static(b"data")); - let chunk = reader.decode(&mut buf, &settings).unwrap().unwrap().chunk(); - assert_eq!(chunk, Bytes::from_static(b"line")); - let msg = reader.decode(&mut buf, &settings).unwrap().unwrap(); - assert!(msg.eof()); - } -} diff --git a/src/server/h1decoder.rs b/src/server/h1decoder.rs deleted file mode 100644 index ece6b3cc..00000000 --- a/src/server/h1decoder.rs +++ /dev/null @@ -1,541 +0,0 @@ -use std::{io, mem}; - -use bytes::{Bytes, BytesMut}; -use futures::{Async, Poll}; -use httparse; - -use super::message::{MessageFlags, Request}; -use super::settings::ServiceConfig; -use error::ParseError; -use http::header::{HeaderName, HeaderValue}; -use http::{header, HttpTryFrom, Method, Uri, Version}; -use uri::Url; - -const MAX_BUFFER_SIZE: usize = 131_072; -const MAX_HEADERS: usize = 96; - -pub(crate) struct H1Decoder { - decoder: Option, -} - -#[derive(Debug)] -pub(crate) enum Message { - Message { - msg: Request, - payload: bool, - expect: bool, - }, - Chunk(Bytes), - Eof, -} - -#[derive(Debug)] -pub(crate) enum DecoderError { - Io(io::Error), - Error(ParseError), -} - -impl From for DecoderError { - fn from(err: io::Error) -> DecoderError { - DecoderError::Io(err) - } -} - -impl H1Decoder { - pub fn new() -> H1Decoder { - H1Decoder { decoder: None } - } - - pub fn decode( - &mut self, - src: &mut BytesMut, - settings: &ServiceConfig, - ) -> Result, DecoderError> { - // read payload - if self.decoder.is_some() { - match self.decoder.as_mut().unwrap().decode(src)? { - Async::Ready(Some(bytes)) => return Ok(Some(Message::Chunk(bytes))), - Async::Ready(None) => { - self.decoder.take(); - return Ok(Some(Message::Eof)); - } - Async::NotReady => return Ok(None), - } - } - - match self - .parse_message(src, settings) - .map_err(DecoderError::Error)? - { - Async::Ready((msg, expect, decoder)) => { - self.decoder = decoder; - Ok(Some(Message::Message { - msg, - expect, - payload: self.decoder.is_some(), - })) - } - Async::NotReady => { - if src.len() >= MAX_BUFFER_SIZE { - error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); - Err(DecoderError::Error(ParseError::TooLarge)) - } else { - Ok(None) - } - } - } - } - - fn parse_message( - &self, - buf: &mut BytesMut, - settings: &ServiceConfig, - ) -> Poll<(Request, bool, Option), ParseError> { - // Parse http message - let mut has_upgrade = false; - let mut chunked = false; - let mut content_length = None; - let mut expect_continue = false; - - let msg = { - // Unsafe: we read only this data only after httparse parses headers into. - // performance bump for pipeline benchmarks. - let mut headers: [HeaderIndex; MAX_HEADERS] = - unsafe { mem::uninitialized() }; - - let (len, method, path, version, headers_len) = { - let mut parsed: [httparse::Header; MAX_HEADERS] = - unsafe { mem::uninitialized() }; - - let mut req = httparse::Request::new(&mut parsed); - match req.parse(buf)? { - httparse::Status::Complete(len) => { - let method = Method::from_bytes(req.method.unwrap().as_bytes()) - .map_err(|_| ParseError::Method)?; - let path = Url::new(Uri::try_from(req.path.unwrap())?); - let version = if req.version.unwrap() == 1 { - Version::HTTP_11 - } else { - Version::HTTP_10 - }; - HeaderIndex::record(buf, req.headers, &mut headers); - - (len, method, path, version, req.headers.len()) - } - httparse::Status::Partial => return Ok(Async::NotReady), - } - }; - - let slice = buf.split_to(len).freeze(); - - // convert headers - let mut msg = settings.get_request(); - { - let inner = msg.inner_mut(); - inner - .flags - .get_mut() - .set(MessageFlags::KEEPALIVE, version != Version::HTTP_10); - - for idx in headers[..headers_len].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::() { - content_length = Some(len); - } else { - debug!("illegal Content-Length: {:?}", len); - return Err(ParseError::Header); - } - } else { - debug!("illegal Content-Length: {:?}", len); - 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 => { - let ka = if let Ok(conn) = - value.to_str().map(|conn| conn.trim()) - { - if version == Version::HTTP_10 - && conn.eq_ignore_ascii_case("keep-alive") - { - true - } else { - version == Version::HTTP_11 - && !(conn.eq_ignore_ascii_case("close") - || conn.eq_ignore_ascii_case("upgrade")) - } - } else { - false - }; - inner.flags.get_mut().set(MessageFlags::KEEPALIVE, ka); - } - 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; - } - } - } - header::EXPECT => { - if value == "100-continue" { - expect_continue = true - } - } - _ => (), - } - - inner.headers.append(name, value); - } else { - return Err(ParseError::Header); - } - } - - inner.url = path; - inner.method = method; - inner.version = version; - } - msg - }; - - // https://tools.ietf.org/html/rfc7230#section-3.3.3 - let decoder = if chunked { - // Chunked encoding - Some(EncodingDecoder::chunked()) - } else if let Some(len) = content_length { - // Content-Length - Some(EncodingDecoder::length(len)) - } else if has_upgrade || msg.inner.method == Method::CONNECT { - // upgrade(websocket) or connect - Some(EncodingDecoder::eof()) - } else { - None - }; - - Ok(Async::Ready((msg, expect_continue, decoder))) - } -} - -#[derive(Clone, Copy)] -pub(crate) struct HeaderIndex { - pub(crate) name: (usize, usize), - pub(crate) value: (usize, usize), -} - -impl HeaderIndex { - pub(crate) fn record( - bytes: &[u8], - headers: &[httparse::Header], - indices: &mut [HeaderIndex], - ) { - let bytes_ptr = bytes.as_ptr() as usize; - for (header, indices) in headers.iter().zip(indices.iter_mut()) { - let name_start = header.name.as_ptr() as usize - bytes_ptr; - let name_end = name_start + header.name.len(); - indices.name = (name_start, name_end); - let value_start = header.value.as_ptr() as usize - bytes_ptr; - let value_end = value_start + header.value.len(); - indices.value = (value_start, value_end); - } - } -} - -/// Decoders to handle different Transfer-Encodings. -/// -/// If a message body does not include a Transfer-Encoding, it *should* -/// include a Content-Length header. -#[derive(Debug, Clone, PartialEq)] -pub struct EncodingDecoder { - kind: Kind, -} - -impl EncodingDecoder { - pub fn length(x: u64) -> EncodingDecoder { - EncodingDecoder { - kind: Kind::Length(x), - } - } - - pub fn chunked() -> EncodingDecoder { - EncodingDecoder { - kind: Kind::Chunked(ChunkedState::Size, 0), - } - } - - pub fn eof() -> EncodingDecoder { - EncodingDecoder { - kind: Kind::Eof(false), - } - } -} - -#[derive(Debug, Clone, PartialEq)] -enum Kind { - /// A Reader used when a Content-Length header is passed with a positive - /// integer. - Length(u64), - /// A Reader used when Transfer-Encoding is `chunked`. - Chunked(ChunkedState, u64), - /// A Reader used for responses that don't indicate a length or chunked. - /// - /// Note: This should only used for `Response`s. It is illegal for a - /// `Request` to be made with both `Content-Length` and - /// `Transfer-Encoding: chunked` missing, as explained from the spec: - /// - /// > If a Transfer-Encoding header field is present in a response and - /// > the chunked transfer coding is not the final encoding, the - /// > message body length is determined by reading the connection until - /// > it is closed by the server. If a Transfer-Encoding header field - /// > is present in a request and the chunked transfer coding is not - /// > the final encoding, the message body length cannot be determined - /// > reliably; the server MUST respond with the 400 (Bad Request) - /// > status code and then close the connection. - Eof(bool), -} - -#[derive(Debug, PartialEq, Clone)] -enum ChunkedState { - Size, - SizeLws, - Extension, - SizeLf, - Body, - BodyCr, - BodyLf, - EndCr, - EndLf, - End, -} - -impl EncodingDecoder { - pub fn decode(&mut self, body: &mut BytesMut) -> Poll, io::Error> { - match self.kind { - Kind::Length(ref mut remaining) => { - if *remaining == 0 { - Ok(Async::Ready(None)) - } else { - if body.is_empty() { - return Ok(Async::NotReady); - } - let len = body.len() as u64; - let buf; - if *remaining > len { - buf = body.take().freeze(); - *remaining -= len; - } else { - buf = body.split_to(*remaining as usize).freeze(); - *remaining = 0; - } - trace!("Length read: {}", buf.len()); - Ok(Async::Ready(Some(buf))) - } - } - Kind::Chunked(ref mut state, ref mut size) => { - loop { - let mut buf = None; - // advances the chunked state - *state = try_ready!(state.step(body, size, &mut buf)); - if *state == ChunkedState::End { - trace!("End of chunked stream"); - return Ok(Async::Ready(None)); - } - if let Some(buf) = buf { - return Ok(Async::Ready(Some(buf))); - } - if body.is_empty() { - return Ok(Async::NotReady); - } - } - } - Kind::Eof(ref mut is_eof) => { - if *is_eof { - Ok(Async::Ready(None)) - } else if !body.is_empty() { - Ok(Async::Ready(Some(body.take().freeze()))) - } else { - Ok(Async::NotReady) - } - } - } - } -} - -macro_rules! byte ( - ($rdr:ident) => ({ - if $rdr.len() > 0 { - let b = $rdr[0]; - $rdr.split_to(1); - b - } else { - return Ok(Async::NotReady) - } - }) -); - -impl ChunkedState { - fn step( - &self, - body: &mut BytesMut, - size: &mut u64, - buf: &mut Option, - ) -> Poll { - use self::ChunkedState::*; - match *self { - Size => ChunkedState::read_size(body, size), - SizeLws => ChunkedState::read_size_lws(body), - Extension => ChunkedState::read_extension(body), - SizeLf => ChunkedState::read_size_lf(body, size), - Body => ChunkedState::read_body(body, size, buf), - BodyCr => ChunkedState::read_body_cr(body), - BodyLf => ChunkedState::read_body_lf(body), - EndCr => ChunkedState::read_end_cr(body), - EndLf => ChunkedState::read_end_lf(body), - End => Ok(Async::Ready(ChunkedState::End)), - } - } - fn read_size(rdr: &mut BytesMut, size: &mut u64) -> Poll { - let radix = 16; - match byte!(rdr) { - b @ b'0'...b'9' => { - *size *= radix; - *size += u64::from(b - b'0'); - } - b @ b'a'...b'f' => { - *size *= radix; - *size += u64::from(b + 10 - b'a'); - } - b @ b'A'...b'F' => { - *size *= radix; - *size += u64::from(b + 10 - b'A'); - } - b'\t' | b' ' => return Ok(Async::Ready(ChunkedState::SizeLws)), - b';' => return Ok(Async::Ready(ChunkedState::Extension)), - b'\r' => return Ok(Async::Ready(ChunkedState::SizeLf)), - _ => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk size line: Invalid Size", - )); - } - } - Ok(Async::Ready(ChunkedState::Size)) - } - fn read_size_lws(rdr: &mut BytesMut) -> Poll { - trace!("read_size_lws"); - match byte!(rdr) { - // LWS can follow the chunk size, but no more digits can come - b'\t' | b' ' => Ok(Async::Ready(ChunkedState::SizeLws)), - b';' => Ok(Async::Ready(ChunkedState::Extension)), - b'\r' => Ok(Async::Ready(ChunkedState::SizeLf)), - _ => Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk size linear white space", - )), - } - } - fn read_extension(rdr: &mut BytesMut) -> Poll { - match byte!(rdr) { - b'\r' => Ok(Async::Ready(ChunkedState::SizeLf)), - _ => Ok(Async::Ready(ChunkedState::Extension)), // no supported extensions - } - } - fn read_size_lf( - rdr: &mut BytesMut, - size: &mut u64, - ) -> Poll { - match byte!(rdr) { - b'\n' if *size > 0 => Ok(Async::Ready(ChunkedState::Body)), - b'\n' if *size == 0 => Ok(Async::Ready(ChunkedState::EndCr)), - _ => Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk size LF", - )), - } - } - - fn read_body( - rdr: &mut BytesMut, - rem: &mut u64, - buf: &mut Option, - ) -> Poll { - trace!("Chunked read, remaining={:?}", rem); - - let len = rdr.len() as u64; - if len == 0 { - Ok(Async::Ready(ChunkedState::Body)) - } else { - let slice; - if *rem > len { - slice = rdr.take().freeze(); - *rem -= len; - } else { - slice = rdr.split_to(*rem as usize).freeze(); - *rem = 0; - } - *buf = Some(slice); - if *rem > 0 { - Ok(Async::Ready(ChunkedState::Body)) - } else { - Ok(Async::Ready(ChunkedState::BodyCr)) - } - } - } - - fn read_body_cr(rdr: &mut BytesMut) -> Poll { - match byte!(rdr) { - b'\r' => Ok(Async::Ready(ChunkedState::BodyLf)), - _ => Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk body CR", - )), - } - } - fn read_body_lf(rdr: &mut BytesMut) -> Poll { - match byte!(rdr) { - b'\n' => Ok(Async::Ready(ChunkedState::Size)), - _ => Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk body LF", - )), - } - } - fn read_end_cr(rdr: &mut BytesMut) -> Poll { - match byte!(rdr) { - b'\r' => Ok(Async::Ready(ChunkedState::EndLf)), - _ => Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk end CR", - )), - } - } - fn read_end_lf(rdr: &mut BytesMut) -> Poll { - match byte!(rdr) { - b'\n' => Ok(Async::Ready(ChunkedState::End)), - _ => Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk end LF", - )), - } - } -} diff --git a/src/server/h1writer.rs b/src/server/h1writer.rs deleted file mode 100644 index 97ce6dff..00000000 --- a/src/server/h1writer.rs +++ /dev/null @@ -1,364 +0,0 @@ -// #![cfg_attr(feature = "cargo-clippy", allow(redundant_field_names))] - -use std::io::{self, Write}; - -use bytes::{BufMut, BytesMut}; -use futures::{Async, Poll}; -use tokio_io::AsyncWrite; - -use super::helpers; -use super::output::{Output, ResponseInfo, ResponseLength}; -use super::settings::ServiceConfig; -use super::Request; -use super::{Writer, WriterState, MAX_WRITE_BUFFER_SIZE}; -use body::{Binary, Body}; -use header::ContentEncoding; -use http::header::{ - HeaderValue, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, -}; -use http::{Method, Version}; -use httpresponse::HttpResponse; - -const AVERAGE_HEADER_SIZE: usize = 30; // totally scientific - -bitflags! { - struct Flags: u8 { - const STARTED = 0b0000_0001; - const UPGRADE = 0b0000_0010; - const KEEPALIVE = 0b0000_0100; - const DISCONNECTED = 0b0000_1000; - } -} - -pub(crate) struct H1Writer { - flags: Flags, - stream: T, - written: u64, - headers_size: u32, - buffer: Output, - buffer_capacity: usize, - settings: ServiceConfig, -} - -impl H1Writer { - pub fn new(stream: T, settings: ServiceConfig) -> H1Writer { - H1Writer { - flags: Flags::KEEPALIVE, - written: 0, - headers_size: 0, - buffer: Output::Buffer(settings.get_bytes()), - buffer_capacity: 0, - stream, - settings, - } - } - - pub fn get_mut(&mut self) -> &mut T { - &mut self.stream - } - - pub fn reset(&mut self) { - self.written = 0; - self.flags = Flags::KEEPALIVE; - } - - pub fn flushed(&mut self) -> bool { - self.buffer.is_empty() - } - - pub fn disconnected(&mut self) { - self.flags.insert(Flags::DISCONNECTED); - } - - pub fn upgrade(&self) -> bool { - self.flags.contains(Flags::UPGRADE) - } - - pub fn keepalive(&self) -> bool { - self.flags.contains(Flags::KEEPALIVE) && !self.flags.contains(Flags::UPGRADE) - } - - fn write_data(stream: &mut T, data: &[u8]) -> io::Result { - let mut written = 0; - while written < data.len() { - match stream.write(&data[written..]) { - Ok(0) => { - return Err(io::Error::new(io::ErrorKind::WriteZero, "")); - } - Ok(n) => { - written += n; - } - Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { - return Ok(written) - } - Err(err) => return Err(err), - } - } - Ok(written) - } -} - -impl Drop for H1Writer { - fn drop(&mut self) { - if let Some(bytes) = self.buffer.take_option() { - self.settings.release_bytes(bytes); - } - } -} - -impl Writer for H1Writer { - #[inline] - fn written(&self) -> u64 { - self.written - } - - #[inline] - fn set_date(&mut self) { - self.settings.set_date(self.buffer.as_mut(), true) - } - - #[inline] - fn buffer(&mut self) -> &mut BytesMut { - self.buffer.as_mut() - } - - fn start( - &mut self, req: &Request, msg: &mut HttpResponse, encoding: ContentEncoding, - ) -> io::Result { - // prepare task - let mut info = ResponseInfo::new(req.inner.method == Method::HEAD); - self.buffer.for_server(&mut info, &req.inner, msg, encoding); - if msg.keep_alive().unwrap_or_else(|| req.keep_alive()) { - self.flags = Flags::STARTED | Flags::KEEPALIVE; - } else { - self.flags = Flags::STARTED; - } - - // Connection upgrade - let version = msg.version().unwrap_or_else(|| req.inner.version); - if msg.upgrade() { - self.flags.insert(Flags::UPGRADE); - msg.headers_mut() - .insert(CONNECTION, HeaderValue::from_static("upgrade")); - } - // keep-alive - else if self.flags.contains(Flags::KEEPALIVE) { - if version < Version::HTTP_11 { - msg.headers_mut() - .insert(CONNECTION, HeaderValue::from_static("keep-alive")); - } - } else if version >= Version::HTTP_11 { - msg.headers_mut() - .insert(CONNECTION, HeaderValue::from_static("close")); - } - let body = msg.replace_body(Body::Empty); - - // render message - { - // output buffer - let mut buffer = self.buffer.as_mut(); - - let reason = msg.reason().as_bytes(); - if let Body::Binary(ref bytes) = body { - buffer.reserve( - 256 + msg.headers().len() * AVERAGE_HEADER_SIZE - + bytes.len() - + reason.len(), - ); - } else { - buffer.reserve( - 256 + msg.headers().len() * AVERAGE_HEADER_SIZE + reason.len(), - ); - } - - // status line - helpers::write_status_line(version, msg.status().as_u16(), &mut buffer); - buffer.extend_from_slice(reason); - - // content length - let mut len_is_set = true; - match info.length { - ResponseLength::Chunked => { - buffer.extend_from_slice(b"\r\ntransfer-encoding: chunked\r\n") - } - ResponseLength::Length(len) => { - helpers::write_content_length(len, &mut buffer) - } - ResponseLength::Length64(len) => { - buffer.extend_from_slice(b"\r\ncontent-length: "); - write!(buffer.writer(), "{}", len)?; - buffer.extend_from_slice(b"\r\n"); - } - ResponseLength::Zero => { - len_is_set = false; - buffer.extend_from_slice(b"\r\n"); - } - ResponseLength::None => buffer.extend_from_slice(b"\r\n"), - } - if let Some(ce) = info.content_encoding { - buffer.extend_from_slice(b"content-encoding: "); - buffer.extend_from_slice(ce.as_ref()); - buffer.extend_from_slice(b"\r\n"); - } - - // write headers - let mut pos = 0; - let mut has_date = false; - let mut remaining = buffer.remaining_mut(); - let mut buf = unsafe { &mut *(buffer.bytes_mut() as *mut [u8]) }; - for (key, value) in msg.headers() { - match *key { - TRANSFER_ENCODING => continue, - CONTENT_ENCODING => if encoding != ContentEncoding::Identity { - continue; - }, - CONTENT_LENGTH => match info.length { - ResponseLength::None => (), - ResponseLength::Zero => { - len_is_set = true; - } - _ => continue, - }, - DATE => { - has_date = true; - } - _ => (), - } - - let v = value.as_ref(); - let k = key.as_str().as_bytes(); - let len = k.len() + v.len() + 4; - if len > remaining { - unsafe { - buffer.advance_mut(pos); - } - pos = 0; - buffer.reserve(len); - remaining = buffer.remaining_mut(); - unsafe { - buf = &mut *(buffer.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; - } - unsafe { - buffer.advance_mut(pos); - } - if !len_is_set { - buffer.extend_from_slice(b"content-length: 0\r\n") - } - - // optimized date header, set_date writes \r\n - if !has_date { - self.settings.set_date(&mut buffer, true); - } else { - // msg eof - buffer.extend_from_slice(b"\r\n"); - } - self.headers_size = buffer.len() as u32; - } - - if let Body::Binary(bytes) = body { - self.written = bytes.len() as u64; - self.buffer.write(bytes.as_ref())?; - } else { - // capacity, makes sense only for streaming or actor - self.buffer_capacity = msg.write_buffer_capacity(); - - msg.replace_body(body); - } - Ok(WriterState::Done) - } - - fn write(&mut self, payload: &Binary) -> io::Result { - self.written += payload.len() as u64; - if !self.flags.contains(Flags::DISCONNECTED) { - if self.flags.contains(Flags::STARTED) { - // shortcut for upgraded connection - if self.flags.contains(Flags::UPGRADE) { - if self.buffer.is_empty() { - let pl: &[u8] = payload.as_ref(); - let n = match Self::write_data(&mut self.stream, pl) { - Err(err) => { - self.disconnected(); - return Err(err); - } - Ok(val) => val, - }; - if n < pl.len() { - self.buffer.write(&pl[n..])?; - return Ok(WriterState::Done); - } - } else { - self.buffer.write(payload.as_ref())?; - } - } else { - // TODO: add warning, write after EOF - self.buffer.write(payload.as_ref())?; - } - } else { - // could be response to EXCEPT header - self.buffer.write(payload.as_ref())?; - } - } - - if self.buffer.len() > self.buffer_capacity { - Ok(WriterState::Pause) - } else { - Ok(WriterState::Done) - } - } - - fn write_eof(&mut self) -> io::Result { - if !self.buffer.write_eof()? { - Err(io::Error::new( - io::ErrorKind::Other, - "Last payload item, but eof is not reached", - )) - } else if self.buffer.len() > MAX_WRITE_BUFFER_SIZE { - Ok(WriterState::Pause) - } else { - Ok(WriterState::Done) - } - } - - #[inline] - fn poll_completed(&mut self, shutdown: bool) -> Poll<(), io::Error> { - if self.flags.contains(Flags::DISCONNECTED) { - return Err(io::Error::new(io::ErrorKind::Other, "disconnected")); - } - - if !self.buffer.is_empty() { - let written = { - match Self::write_data(&mut self.stream, self.buffer.as_ref().as_ref()) { - Err(err) => { - self.disconnected(); - return Err(err); - } - Ok(val) => val, - } - }; - let _ = self.buffer.split_to(written); - if shutdown && !self.buffer.is_empty() - || (self.buffer.len() > self.buffer_capacity) - { - return Ok(Async::NotReady); - } - } - if shutdown { - self.stream.poll_flush()?; - self.stream.shutdown() - } else { - Ok(self.stream.poll_flush()?) - } - } -} diff --git a/src/server/h2.rs b/src/server/h2.rs deleted file mode 100644 index c9e968a3..00000000 --- a/src/server/h2.rs +++ /dev/null @@ -1,472 +0,0 @@ -use std::collections::VecDeque; -use std::io::{Read, Write}; -use std::net::SocketAddr; -use std::rc::Rc; -use std::time::Instant; -use std::{cmp, io, mem}; - -use bytes::{Buf, Bytes}; -use futures::{Async, Future, Poll, Stream}; -use http2::server::{self, Connection, Handshake, SendResponse}; -use http2::{Reason, RecvStream}; -use modhttp::request::Parts; -use tokio_io::{AsyncRead, AsyncWrite}; -use tokio_timer::Delay; - -use error::{Error, PayloadError}; -use extensions::Extensions; -use http::{StatusCode, Version}; -use payload::{Payload, PayloadStatus, PayloadWriter}; -use uri::Url; - -use super::error::{HttpDispatchError, ServerError}; -use super::h2writer::H2Writer; -use super::input::PayloadType; -use super::settings::ServiceConfig; -use super::{HttpHandler, HttpHandlerTask, IoStream, Writer}; - -bitflags! { - struct Flags: u8 { - const DISCONNECTED = 0b0000_0001; - const SHUTDOWN = 0b0000_0010; - } -} - -/// HTTP/2 Transport -pub(crate) struct Http2 -where - T: AsyncRead + AsyncWrite + 'static, - H: HttpHandler + 'static, -{ - flags: Flags, - settings: ServiceConfig, - addr: Option, - state: State>, - tasks: VecDeque>, - extensions: Option>, - ka_expire: Instant, - ka_timer: Option, -} - -enum State { - Handshake(Handshake), - Connection(Connection), - Empty, -} - -impl Http2 -where - T: IoStream + 'static, - H: HttpHandler + 'static, -{ - pub fn new( - settings: ServiceConfig, - io: T, - buf: Bytes, - keepalive_timer: Option, - ) -> Self { - let addr = io.peer_addr(); - let extensions = io.extensions(); - - // keep-alive timeout - let (ka_expire, ka_timer) = if let Some(delay) = keepalive_timer { - (delay.deadline(), Some(delay)) - } else if let Some(delay) = settings.keep_alive_timer() { - (delay.deadline(), Some(delay)) - } else { - (settings.now(), None) - }; - - Http2 { - flags: Flags::empty(), - tasks: VecDeque::new(), - state: State::Handshake(server::handshake(IoWrapper { - unread: if buf.is_empty() { None } else { Some(buf) }, - inner: io, - })), - addr, - settings, - extensions, - ka_expire, - ka_timer, - } - } - - pub fn poll(&mut self) -> Poll<(), HttpDispatchError> { - self.poll_keepalive()?; - - // server - if let State::Connection(ref mut conn) = self.state { - loop { - // shutdown connection - if self.flags.contains(Flags::SHUTDOWN) { - return conn.poll_close().map_err(|e| e.into()); - } - - let mut not_ready = true; - let disconnected = self.flags.contains(Flags::DISCONNECTED); - - // check in-flight connections - for item in &mut self.tasks { - // read payload - if !disconnected { - item.poll_payload(); - } - - if !item.flags.contains(EntryFlags::EOF) { - if disconnected { - item.flags.insert(EntryFlags::EOF); - } else { - let retry = item.payload.need_read() == PayloadStatus::Read; - loop { - match item.task.poll_io(&mut item.stream) { - Ok(Async::Ready(ready)) => { - if ready { - item.flags.insert( - EntryFlags::EOF | EntryFlags::FINISHED, - ); - } else { - item.flags.insert(EntryFlags::EOF); - } - not_ready = false; - } - Ok(Async::NotReady) => { - if item.payload.need_read() - == PayloadStatus::Read - && !retry - { - continue; - } - } - Err(err) => { - error!("Unhandled error: {}", err); - item.flags.insert( - EntryFlags::EOF - | EntryFlags::ERROR - | EntryFlags::WRITE_DONE, - ); - item.stream.reset(Reason::INTERNAL_ERROR); - } - } - break; - } - } - } - - if item.flags.contains(EntryFlags::EOF) - && !item.flags.contains(EntryFlags::FINISHED) - { - match item.task.poll_completed() { - Ok(Async::NotReady) => (), - Ok(Async::Ready(_)) => { - item.flags.insert( - EntryFlags::FINISHED | EntryFlags::WRITE_DONE, - ); - } - Err(err) => { - item.flags.insert( - EntryFlags::ERROR - | EntryFlags::WRITE_DONE - | EntryFlags::FINISHED, - ); - error!("Unhandled error: {}", err); - } - } - } - - if item.flags.contains(EntryFlags::FINISHED) - && !item.flags.contains(EntryFlags::WRITE_DONE) - && !disconnected - { - match item.stream.poll_completed(false) { - Ok(Async::NotReady) => (), - Ok(Async::Ready(_)) => { - not_ready = false; - item.flags.insert(EntryFlags::WRITE_DONE); - } - Err(_) => { - item.flags.insert(EntryFlags::ERROR); - } - } - } - } - - // cleanup finished tasks - while !self.tasks.is_empty() { - if self.tasks[0].flags.contains(EntryFlags::FINISHED) - && self.tasks[0].flags.contains(EntryFlags::WRITE_DONE) - || self.tasks[0].flags.contains(EntryFlags::ERROR) - { - self.tasks.pop_front(); - } else { - break; - } - } - - // get request - if !self.flags.contains(Flags::DISCONNECTED) { - match conn.poll() { - Ok(Async::Ready(None)) => { - not_ready = false; - self.flags.insert(Flags::DISCONNECTED); - for entry in &mut self.tasks { - entry.task.disconnected() - } - } - Ok(Async::Ready(Some((req, resp)))) => { - not_ready = false; - let (parts, body) = req.into_parts(); - - // update keep-alive expire - if self.ka_timer.is_some() { - if let Some(expire) = self.settings.keep_alive_expire() { - self.ka_expire = expire; - } - } - - self.tasks.push_back(Entry::new( - parts, - body, - resp, - self.addr, - self.settings.clone(), - self.extensions.clone(), - )); - } - Ok(Async::NotReady) => return Ok(Async::NotReady), - Err(err) => { - trace!("Connection error: {}", err); - self.flags.insert(Flags::SHUTDOWN); - for entry in &mut self.tasks { - entry.task.disconnected() - } - continue; - } - } - } - - if not_ready { - if self.tasks.is_empty() && self.flags.contains(Flags::DISCONNECTED) - { - return conn.poll_close().map_err(|e| e.into()); - } else { - return Ok(Async::NotReady); - } - } - } - } - - // handshake - self.state = if let State::Handshake(ref mut handshake) = self.state { - match handshake.poll() { - Ok(Async::Ready(conn)) => State::Connection(conn), - Ok(Async::NotReady) => return Ok(Async::NotReady), - Err(err) => { - trace!("Error handling connection: {}", err); - return Err(err.into()); - } - } - } else { - mem::replace(&mut self.state, State::Empty) - }; - - self.poll() - } - - /// keep-alive timer. returns `true` is keep-alive, otherwise drop - fn poll_keepalive(&mut self) -> Result<(), HttpDispatchError> { - if let Some(ref mut timer) = self.ka_timer { - match timer.poll() { - Ok(Async::Ready(_)) => { - // if we get timer during shutdown, just drop connection - if self.flags.contains(Flags::SHUTDOWN) { - return Err(HttpDispatchError::ShutdownTimeout); - } - if timer.deadline() >= self.ka_expire { - // check for any outstanding request handling - if self.tasks.is_empty() { - return Err(HttpDispatchError::ShutdownTimeout); - } else if let Some(dl) = self.settings.keep_alive_expire() { - timer.reset(dl); - let _ = timer.poll(); - } - } else { - timer.reset(self.ka_expire); - let _ = timer.poll(); - } - } - Ok(Async::NotReady) => (), - Err(e) => { - error!("Timer error {:?}", e); - return Err(HttpDispatchError::Unknown); - } - } - } - - Ok(()) - } -} - -bitflags! { - struct EntryFlags: u8 { - const EOF = 0b0000_0001; - const REOF = 0b0000_0010; - const ERROR = 0b0000_0100; - const FINISHED = 0b0000_1000; - const WRITE_DONE = 0b0001_0000; - } -} - -enum EntryPipe { - Task(H::Task), - Error(Box), -} - -impl EntryPipe { - fn disconnected(&mut self) { - match *self { - EntryPipe::Task(ref mut task) => task.disconnected(), - EntryPipe::Error(ref mut task) => task.disconnected(), - } - } - fn poll_io(&mut self, io: &mut Writer) -> Poll { - match *self { - EntryPipe::Task(ref mut task) => task.poll_io(io), - EntryPipe::Error(ref mut task) => task.poll_io(io), - } - } - fn poll_completed(&mut self) -> Poll<(), Error> { - match *self { - EntryPipe::Task(ref mut task) => task.poll_completed(), - EntryPipe::Error(ref mut task) => task.poll_completed(), - } - } -} - -struct Entry { - task: EntryPipe, - payload: PayloadType, - recv: RecvStream, - stream: H2Writer, - flags: EntryFlags, -} - -impl Entry { - fn new( - parts: Parts, - recv: RecvStream, - resp: SendResponse, - addr: Option, - settings: ServiceConfig, - extensions: Option>, - ) -> Entry - where - H: HttpHandler + 'static, - { - // Payload and Content-Encoding - let (psender, payload) = Payload::new(false); - - let mut msg = settings.get_request(); - { - let inner = msg.inner_mut(); - inner.url = Url::new(parts.uri); - inner.method = parts.method; - inner.version = parts.version; - inner.headers = parts.headers; - inner.stream_extensions = extensions; - *inner.payload.borrow_mut() = Some(payload); - inner.addr = addr; - } - - // Payload sender - let psender = PayloadType::new(msg.headers(), psender); - - // start request processing - let task = match settings.handler().handle(msg) { - Ok(task) => EntryPipe::Task(task), - Err(_) => EntryPipe::Error(ServerError::err( - Version::HTTP_2, - StatusCode::NOT_FOUND, - )), - }; - - Entry { - task, - recv, - payload: psender, - stream: H2Writer::new(resp, settings), - flags: EntryFlags::empty(), - } - } - - fn poll_payload(&mut self) { - while !self.flags.contains(EntryFlags::REOF) - && self.payload.need_read() == PayloadStatus::Read - { - match self.recv.poll() { - Ok(Async::Ready(Some(chunk))) => { - let l = chunk.len(); - self.payload.feed_data(chunk); - if let Err(err) = self.recv.release_capacity().release_capacity(l) { - self.payload.set_error(PayloadError::Http2(err)); - break; - } - } - Ok(Async::Ready(None)) => { - self.flags.insert(EntryFlags::REOF); - self.payload.feed_eof(); - } - Ok(Async::NotReady) => break, - Err(err) => { - self.payload.set_error(PayloadError::Http2(err)); - break; - } - } - } - } -} - -struct IoWrapper { - unread: Option, - inner: T, -} - -impl Read for IoWrapper { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - if let Some(mut bytes) = self.unread.take() { - let size = 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 Write for IoWrapper { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.inner.write(buf) - } - fn flush(&mut self) -> io::Result<()> { - self.inner.flush() - } -} - -impl AsyncRead for IoWrapper { - unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [u8]) -> bool { - self.inner.prepare_uninitialized_buffer(buf) - } -} - -impl AsyncWrite for IoWrapper { - 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/src/server/h2writer.rs b/src/server/h2writer.rs deleted file mode 100644 index fef6f889..00000000 --- a/src/server/h2writer.rs +++ /dev/null @@ -1,268 +0,0 @@ -#![cfg_attr( - feature = "cargo-clippy", - allow(redundant_field_names) -)] - -use std::{cmp, io}; - -use bytes::{Bytes, BytesMut}; -use futures::{Async, Poll}; -use http2::server::SendResponse; -use http2::{Reason, SendStream}; -use modhttp::Response; - -use super::helpers; -use super::message::Request; -use super::output::{Output, ResponseInfo, ResponseLength}; -use super::settings::ServiceConfig; -use super::{Writer, WriterState, MAX_WRITE_BUFFER_SIZE}; -use body::{Binary, Body}; -use header::ContentEncoding; -use http::header::{ - HeaderValue, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, -}; -use http::{HttpTryFrom, Method, Version}; -use httpresponse::HttpResponse; - -const CHUNK_SIZE: usize = 16_384; - -bitflags! { - struct Flags: u8 { - const STARTED = 0b0000_0001; - const DISCONNECTED = 0b0000_0010; - const EOF = 0b0000_0100; - const RESERVED = 0b0000_1000; - } -} - -pub(crate) struct H2Writer { - respond: SendResponse, - stream: Option>, - flags: Flags, - written: u64, - buffer: Output, - buffer_capacity: usize, - settings: ServiceConfig, -} - -impl H2Writer { - pub fn new(respond: SendResponse, settings: ServiceConfig) -> H2Writer { - H2Writer { - stream: None, - flags: Flags::empty(), - written: 0, - buffer: Output::Buffer(settings.get_bytes()), - buffer_capacity: 0, - respond, - settings, - } - } - - pub fn reset(&mut self, reason: Reason) { - if let Some(mut stream) = self.stream.take() { - stream.send_reset(reason) - } - } -} - -impl Drop for H2Writer { - fn drop(&mut self) { - self.settings.release_bytes(self.buffer.take()); - } -} - -impl Writer for H2Writer { - fn written(&self) -> u64 { - self.written - } - - #[inline] - fn set_date(&mut self) { - self.settings.set_date(self.buffer.as_mut(), true) - } - - #[inline] - fn buffer(&mut self) -> &mut BytesMut { - self.buffer.as_mut() - } - - fn start( - &mut self, req: &Request, msg: &mut HttpResponse, encoding: ContentEncoding, - ) -> io::Result { - // prepare response - self.flags.insert(Flags::STARTED); - let mut info = ResponseInfo::new(req.inner.method == Method::HEAD); - self.buffer.for_server(&mut info, &req.inner, msg, encoding); - - let mut has_date = false; - let mut resp = Response::new(()); - let mut len_is_set = false; - *resp.status_mut() = msg.status(); - *resp.version_mut() = Version::HTTP_2; - for (key, value) in msg.headers().iter() { - match *key { - // http2 specific - CONNECTION | TRANSFER_ENCODING => continue, - CONTENT_ENCODING => if encoding != ContentEncoding::Identity { - continue; - }, - CONTENT_LENGTH => match info.length { - ResponseLength::None => (), - ResponseLength::Zero => { - len_is_set = true; - } - _ => continue, - }, - DATE => has_date = true, - _ => (), - } - resp.headers_mut().append(key, value.clone()); - } - - // set date header - if !has_date { - let mut bytes = BytesMut::with_capacity(29); - self.settings.set_date(&mut bytes, false); - resp.headers_mut() - .insert(DATE, HeaderValue::try_from(bytes.freeze()).unwrap()); - } - - // content length - match info.length { - ResponseLength::Zero => { - if !len_is_set { - resp.headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::from_static("0")); - } - self.flags.insert(Flags::EOF); - } - ResponseLength::Length(len) => { - let mut val = BytesMut::new(); - helpers::convert_usize(len, &mut val); - let l = val.len(); - resp.headers_mut().insert( - CONTENT_LENGTH, - HeaderValue::try_from(val.split_to(l - 2).freeze()).unwrap(), - ); - } - ResponseLength::Length64(len) => { - let l = format!("{}", len); - resp.headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::try_from(l.as_str()).unwrap()); - } - ResponseLength::None => { - self.flags.insert(Flags::EOF); - } - _ => (), - } - if let Some(ce) = info.content_encoding { - resp.headers_mut() - .insert(CONTENT_ENCODING, HeaderValue::try_from(ce).unwrap()); - } - - trace!("Response: {:?}", resp); - - match self - .respond - .send_response(resp, self.flags.contains(Flags::EOF)) - { - Ok(stream) => self.stream = Some(stream), - Err(_) => return Err(io::Error::new(io::ErrorKind::Other, "err")), - } - - let body = msg.replace_body(Body::Empty); - if let Body::Binary(bytes) = body { - if bytes.is_empty() { - Ok(WriterState::Done) - } else { - self.flags.insert(Flags::EOF); - self.buffer.write(bytes.as_ref())?; - if let Some(ref mut stream) = self.stream { - self.flags.insert(Flags::RESERVED); - stream.reserve_capacity(cmp::min(self.buffer.len(), CHUNK_SIZE)); - } - Ok(WriterState::Pause) - } - } else { - msg.replace_body(body); - self.buffer_capacity = msg.write_buffer_capacity(); - Ok(WriterState::Done) - } - } - - fn write(&mut self, payload: &Binary) -> io::Result { - if !self.flags.contains(Flags::DISCONNECTED) { - if self.flags.contains(Flags::STARTED) { - // TODO: add warning, write after EOF - self.buffer.write(payload.as_ref())?; - } else { - // might be response for EXCEPT - error!("Not supported"); - } - } - - if self.buffer.len() > self.buffer_capacity { - Ok(WriterState::Pause) - } else { - Ok(WriterState::Done) - } - } - - fn write_eof(&mut self) -> io::Result { - self.flags.insert(Flags::EOF); - if !self.buffer.write_eof()? { - Err(io::Error::new( - io::ErrorKind::Other, - "Last payload item, but eof is not reached", - )) - } else if self.buffer.len() > MAX_WRITE_BUFFER_SIZE { - Ok(WriterState::Pause) - } else { - Ok(WriterState::Done) - } - } - - fn poll_completed(&mut self, _shutdown: bool) -> Poll<(), io::Error> { - if !self.flags.contains(Flags::STARTED) { - return Ok(Async::NotReady); - } - - if let Some(ref mut stream) = self.stream { - // reserve capacity - if !self.flags.contains(Flags::RESERVED) && !self.buffer.is_empty() { - self.flags.insert(Flags::RESERVED); - stream.reserve_capacity(cmp::min(self.buffer.len(), CHUNK_SIZE)); - } - - loop { - match stream.poll_capacity() { - Ok(Async::NotReady) => return Ok(Async::NotReady), - Ok(Async::Ready(None)) => return Ok(Async::Ready(())), - Ok(Async::Ready(Some(cap))) => { - let len = self.buffer.len(); - let bytes = self.buffer.split_to(cmp::min(cap, len)); - let eof = - self.buffer.is_empty() && self.flags.contains(Flags::EOF); - self.written += bytes.len() as u64; - - if let Err(e) = stream.send_data(bytes.freeze(), eof) { - return Err(io::Error::new(io::ErrorKind::Other, e)); - } else if !self.buffer.is_empty() { - let cap = cmp::min(self.buffer.len(), CHUNK_SIZE); - stream.reserve_capacity(cap); - } else { - if eof { - stream.reserve_capacity(0); - continue; - } - self.flags.remove(Flags::RESERVED); - return Ok(Async::Ready(())); - } - } - Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e)), - } - } - } - Ok(Async::Ready(())) - } -} diff --git a/src/server/handler.rs b/src/server/handler.rs deleted file mode 100644 index 33e50ac3..00000000 --- a/src/server/handler.rs +++ /dev/null @@ -1,208 +0,0 @@ -use futures::{Async, Future, Poll}; - -use super::message::Request; -use super::Writer; -use error::Error; - -/// Low level http request handler -#[allow(unused_variables)] -pub trait HttpHandler: 'static { - /// Request handling task - type Task: HttpHandlerTask; - - /// Handle request - fn handle(&self, req: Request) -> Result; -} - -impl HttpHandler for Box>> { - type Task = Box; - - fn handle(&self, req: Request) -> Result, Request> { - self.as_ref().handle(req) - } -} - -/// Low level http request handler -pub trait HttpHandlerTask { - /// Poll task, this method is used before or after *io* object is available - fn poll_completed(&mut self) -> Poll<(), Error> { - Ok(Async::Ready(())) - } - - /// Poll task when *io* object is available - fn poll_io(&mut self, io: &mut Writer) -> Poll; - - /// Connection is disconnected - fn disconnected(&mut self) {} -} - -impl HttpHandlerTask for Box { - fn poll_io(&mut self, io: &mut Writer) -> Poll { - self.as_mut().poll_io(io) - } -} - -pub(super) struct HttpHandlerTaskFut { - task: T, -} - -impl HttpHandlerTaskFut { - pub(crate) fn new(task: T) -> Self { - Self { task } - } -} - -impl Future for HttpHandlerTaskFut { - type Item = (); - type Error = (); - - fn poll(&mut self) -> Poll<(), ()> { - self.task.poll_completed().map_err(|_| ()) - } -} - -/// Conversion helper trait -pub trait IntoHttpHandler { - /// The associated type which is result of conversion. - type Handler: HttpHandler; - - /// Convert into `HttpHandler` object. - fn into_handler(self) -> Self::Handler; -} - -impl IntoHttpHandler for T { - type Handler = T; - - fn into_handler(self) -> Self::Handler { - self - } -} - -impl IntoHttpHandler for Vec { - type Handler = VecHttpHandler; - - fn into_handler(self) -> Self::Handler { - VecHttpHandler(self.into_iter().map(|item| item.into_handler()).collect()) - } -} - -#[doc(hidden)] -pub struct VecHttpHandler(Vec); - -impl HttpHandler for VecHttpHandler { - type Task = H::Task; - - fn handle(&self, mut req: Request) -> Result { - for h in &self.0 { - req = match h.handle(req) { - Ok(task) => return Ok(task), - Err(e) => e, - }; - } - Err(req) - } -} - -macro_rules! http_handler ({$EN:ident, $(($n:tt, $T:ident)),+} => { - impl<$($T: HttpHandler,)+> HttpHandler for ($($T,)+) { - type Task = $EN<$($T,)+>; - - fn handle(&self, mut req: Request) -> Result { - $( - req = match self.$n.handle(req) { - Ok(task) => return Ok($EN::$T(task)), - Err(e) => e, - }; - )+ - Err(req) - } - } - - #[doc(hidden)] - pub enum $EN<$($T: HttpHandler,)+> { - $($T ($T::Task),)+ - } - - impl<$($T: HttpHandler,)+> HttpHandlerTask for $EN<$($T,)+> - { - fn poll_completed(&mut self) -> Poll<(), Error> { - match self { - $($EN :: $T(ref mut task) => task.poll_completed(),)+ - } - } - - fn poll_io(&mut self, io: &mut Writer) -> Poll { - match self { - $($EN::$T(ref mut task) => task.poll_io(io),)+ - } - } - - /// Connection is disconnected - fn disconnected(&mut self) { - match self { - $($EN::$T(ref mut task) => task.disconnected(),)+ - } - } - } -}); - -http_handler!(HttpHandlerTask1, (0, A)); -http_handler!(HttpHandlerTask2, (0, A), (1, B)); -http_handler!(HttpHandlerTask3, (0, A), (1, B), (2, C)); -http_handler!(HttpHandlerTask4, (0, A), (1, B), (2, C), (3, D)); -http_handler!(HttpHandlerTask5, (0, A), (1, B), (2, C), (3, D), (4, E)); -http_handler!( - HttpHandlerTask6, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F) -); -http_handler!( - HttpHandlerTask7, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G) -); -http_handler!( - HttpHandlerTask8, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G), - (7, H) -); -http_handler!( - HttpHandlerTask9, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G), - (7, H), - (8, I) -); -http_handler!( - HttpHandlerTask10, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G), - (7, H), - (8, I), - (9, J) -); diff --git a/src/server/helpers.rs b/src/server/helpers.rs deleted file mode 100644 index e4ccd8ae..00000000 --- a/src/server/helpers.rs +++ /dev/null @@ -1,208 +0,0 @@ -use bytes::{BufMut, BytesMut}; -use http::Version; -use std::{mem, ptr, slice}; - -const DEC_DIGITS_LUT: &[u8] = b"0001020304050607080910111213141516171819\ - 2021222324252627282930313233343536373839\ - 4041424344454647484950515253545556575859\ - 6061626364656667686970717273747576777879\ - 8081828384858687888990919293949596979899"; - -pub(crate) const STATUS_LINE_BUF_SIZE: usize = 13; - -pub(crate) fn write_status_line(version: Version, mut n: u16, bytes: &mut BytesMut) { - let mut buf: [u8; STATUS_LINE_BUF_SIZE] = [ - b'H', b'T', b'T', b'P', b'/', b'1', b'.', b'1', b' ', b' ', b' ', b' ', b' ', - ]; - match version { - Version::HTTP_2 => buf[5] = b'2', - Version::HTTP_10 => buf[7] = b'0', - Version::HTTP_09 => { - buf[5] = b'0'; - buf[7] = b'9'; - } - _ => (), - } - - let mut curr: isize = 12; - let buf_ptr = buf.as_mut_ptr(); - let lut_ptr = DEC_DIGITS_LUT.as_ptr(); - let four = n > 999; - - // decode 2 more chars, if > 2 chars - let d1 = (n % 100) << 1; - n /= 100; - curr -= 2; - unsafe { - ptr::copy_nonoverlapping(lut_ptr.offset(d1 as isize), buf_ptr.offset(curr), 2); - } - - // decode last 1 or 2 chars - if n < 10 { - curr -= 1; - unsafe { - *buf_ptr.offset(curr) = (n as u8) + b'0'; - } - } else { - let d1 = n << 1; - curr -= 2; - unsafe { - ptr::copy_nonoverlapping( - lut_ptr.offset(d1 as isize), - buf_ptr.offset(curr), - 2, - ); - } - } - - bytes.put_slice(&buf); - if four { - bytes.put(b' '); - } -} - -/// NOTE: bytes object has to contain enough space -pub fn write_content_length(mut n: usize, bytes: &mut BytesMut) { - if n < 10 { - let mut buf: [u8; 21] = [ - b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l', b'e', - b'n', b'g', b't', b'h', b':', b' ', b'0', b'\r', b'\n', - ]; - buf[18] = (n as u8) + b'0'; - bytes.put_slice(&buf); - } else if n < 100 { - let mut buf: [u8; 22] = [ - b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l', b'e', - b'n', b'g', b't', b'h', b':', b' ', b'0', b'0', b'\r', b'\n', - ]; - let d1 = n << 1; - unsafe { - ptr::copy_nonoverlapping( - DEC_DIGITS_LUT.as_ptr().add(d1), - buf.as_mut_ptr().offset(18), - 2, - ); - } - bytes.put_slice(&buf); - } else if n < 1000 { - let mut buf: [u8; 23] = [ - b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l', b'e', - b'n', b'g', b't', b'h', b':', b' ', b'0', b'0', b'0', b'\r', b'\n', - ]; - // decode 2 more chars, if > 2 chars - let d1 = (n % 100) << 1; - n /= 100; - unsafe { - ptr::copy_nonoverlapping( - DEC_DIGITS_LUT.as_ptr().add(d1), - buf.as_mut_ptr().offset(19), - 2, - ) - }; - - // decode last 1 - buf[18] = (n as u8) + b'0'; - - bytes.put_slice(&buf); - } else { - bytes.put_slice(b"\r\ncontent-length: "); - convert_usize(n, bytes); - } -} - -pub(crate) fn convert_usize(mut n: usize, bytes: &mut BytesMut) { - let mut curr: isize = 39; - let mut buf: [u8; 41] = unsafe { mem::uninitialized() }; - buf[39] = b'\r'; - buf[40] = b'\n'; - let buf_ptr = buf.as_mut_ptr(); - let lut_ptr = DEC_DIGITS_LUT.as_ptr(); - - // eagerly decode 4 characters at a time - while n >= 10_000 { - let rem = (n % 10_000) as isize; - n /= 10_000; - - let d1 = (rem / 100) << 1; - let d2 = (rem % 100) << 1; - curr -= 4; - unsafe { - ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2); - ptr::copy_nonoverlapping(lut_ptr.offset(d2), buf_ptr.offset(curr + 2), 2); - } - } - - // if we reach here numbers are <= 9999, so at most 4 chars long - let mut n = n as isize; // possibly reduce 64bit math - - // decode 2 more chars, if > 2 chars - if n >= 100 { - let d1 = (n % 100) << 1; - n /= 100; - curr -= 2; - unsafe { - ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2); - } - } - - // decode last 1 or 2 chars - if n < 10 { - curr -= 1; - unsafe { - *buf_ptr.offset(curr) = (n as u8) + b'0'; - } - } else { - let d1 = n << 1; - curr -= 2; - unsafe { - ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2); - } - } - - unsafe { - bytes.extend_from_slice(slice::from_raw_parts( - buf_ptr.offset(curr), - 41 - curr as usize, - )); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_write_content_length() { - let mut bytes = BytesMut::new(); - bytes.reserve(50); - write_content_length(0, &mut bytes); - assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 0\r\n"[..]); - bytes.reserve(50); - write_content_length(9, &mut bytes); - assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 9\r\n"[..]); - bytes.reserve(50); - write_content_length(10, &mut bytes); - assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 10\r\n"[..]); - bytes.reserve(50); - write_content_length(99, &mut bytes); - assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 99\r\n"[..]); - bytes.reserve(50); - write_content_length(100, &mut bytes); - assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 100\r\n"[..]); - bytes.reserve(50); - write_content_length(101, &mut bytes); - assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 101\r\n"[..]); - bytes.reserve(50); - write_content_length(998, &mut bytes); - assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 998\r\n"[..]); - bytes.reserve(50); - write_content_length(1000, &mut bytes); - assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 1000\r\n"[..]); - bytes.reserve(50); - write_content_length(1001, &mut bytes); - assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 1001\r\n"[..]); - bytes.reserve(50); - write_content_length(5909, &mut bytes); - assert_eq!(bytes.take().freeze(), b"\r\ncontent-length: 5909\r\n"[..]); - } -} diff --git a/src/server/http.rs b/src/server/http.rs deleted file mode 100644 index 5ff621af..00000000 --- a/src/server/http.rs +++ /dev/null @@ -1,579 +0,0 @@ -use std::{fmt, io, mem, net}; - -use actix::{Addr, System}; -use actix_net::server::Server; -use actix_net::service::NewService; -use actix_net::ssl; - -use net2::TcpBuilder; -use num_cpus; - -#[cfg(feature = "tls")] -use native_tls::TlsAcceptor; - -#[cfg(any(feature = "alpn", feature = "ssl"))] -use openssl::ssl::SslAcceptorBuilder; - -#[cfg(feature = "rust-tls")] -use rustls::ServerConfig; - -use super::acceptor::{AcceptorServiceFactory, DefaultAcceptor}; -use super::builder::{HttpServiceBuilder, ServiceProvider}; -use super::{IntoHttpHandler, KeepAlive}; - -struct Socket { - scheme: &'static str, - lst: net::TcpListener, - addr: net::SocketAddr, - handler: Box, -} - -/// An HTTP Server -/// -/// By default it serves HTTP2 when HTTPs is enabled, -/// in order to change it, use `ServerFlags` that can be provided -/// to acceptor service. -pub struct HttpServer -where - H: IntoHttpHandler + 'static, - F: Fn() -> H + Send + Clone, -{ - pub(super) factory: F, - pub(super) host: Option, - pub(super) keep_alive: KeepAlive, - pub(super) client_timeout: u64, - pub(super) client_shutdown: u64, - backlog: i32, - threads: usize, - exit: bool, - shutdown_timeout: u16, - no_http2: bool, - no_signals: bool, - maxconn: usize, - maxconnrate: usize, - sockets: Vec, -} - -impl HttpServer -where - H: IntoHttpHandler + 'static, - F: Fn() -> H + Send + Clone + 'static, -{ - /// Create new http server with application factory - pub fn new(factory: F) -> HttpServer { - HttpServer { - factory, - threads: num_cpus::get(), - host: None, - backlog: 2048, - keep_alive: KeepAlive::Timeout(5), - shutdown_timeout: 30, - exit: false, - no_http2: false, - no_signals: false, - maxconn: 25_600, - maxconnrate: 256, - client_timeout: 5000, - client_shutdown: 5000, - sockets: Vec::new(), - } - } - - /// Set number of workers to start. - /// - /// By default http server uses number of available logical cpu as threads - /// count. - pub fn workers(mut self, num: usize) -> Self { - self.threads = num; - self - } - - /// Set the maximum number of pending connections. - /// - /// This refers to the number of clients that can be waiting to be served. - /// Exceeding this number results in the client getting an error when - /// attempting to connect. It should only affect servers under significant - /// load. - /// - /// Generally set in the 64-2048 range. Default value is 2048. - /// - /// This method should be called before `bind()` method call. - pub fn backlog(mut self, num: i32) -> Self { - self.backlog = num; - self - } - - /// Sets the maximum per-worker number of concurrent connections. - /// - /// All socket listeners will stop accepting connections when this limit is reached - /// for each worker. - /// - /// By default max connections is set to a 25k. - pub fn maxconn(mut self, num: usize) -> Self { - self.maxconn = num; - self - } - - /// Sets the maximum per-worker concurrent connection establish process. - /// - /// All listeners will stop accepting connections when this limit is reached. It - /// can be used to limit the global SSL CPU usage. - /// - /// By default max connections is set to a 256. - pub fn maxconnrate(mut self, num: usize) -> Self { - self.maxconnrate = num; - self - } - - /// Set server keep-alive setting. - /// - /// By default keep alive is set to a 5 seconds. - pub fn keep_alive>(mut self, val: T) -> Self { - self.keep_alive = val.into(); - self - } - - /// Set server client timeout in milliseconds for first request. - /// - /// Defines a timeout for reading client request header. If a client does not transmit - /// the entire set headers within this time, the request is terminated with - /// the 408 (Request Time-out) error. - /// - /// To disable timeout set value to 0. - /// - /// By default client timeout is set to 5000 milliseconds. - pub fn client_timeout(mut self, val: u64) -> Self { - self.client_timeout = val; - self - } - - /// Set server connection shutdown timeout in milliseconds. - /// - /// Defines a timeout for shutdown connection. If a shutdown procedure does not complete - /// within this time, the request is dropped. - /// - /// To disable timeout set value to 0. - /// - /// By default client timeout is set to 5000 milliseconds. - pub fn client_shutdown(mut self, val: u64) -> Self { - self.client_shutdown = val; - self - } - - /// Set server host name. - /// - /// Host name is used by application router aa a hostname for url - /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. - /// html#method.host) documentation for more information. - pub fn server_hostname(mut self, val: String) -> Self { - self.host = Some(val); - self - } - - /// Stop actix system. - /// - /// `SystemExit` message stops currently running system. - pub fn system_exit(mut self) -> Self { - self.exit = true; - self - } - - /// Disable signal handling - pub fn disable_signals(mut self) -> Self { - self.no_signals = true; - self - } - - /// Timeout for graceful workers shutdown. - /// - /// After receiving a stop signal, workers have this much time to finish - /// serving requests. Workers still alive after the timeout are force - /// dropped. - /// - /// By default shutdown timeout sets to 30 seconds. - pub fn shutdown_timeout(mut self, sec: u16) -> Self { - self.shutdown_timeout = sec; - self - } - - /// Disable `HTTP/2` support - pub fn no_http2(mut self) -> Self { - self.no_http2 = true; - self - } - - /// Get addresses of bound sockets. - pub fn addrs(&self) -> Vec { - self.sockets.iter().map(|s| s.addr).collect() - } - - /// Get addresses of bound sockets and the scheme for it. - /// - /// This is useful when the server is bound from different sources - /// with some sockets listening on http and some listening on https - /// and the user should be presented with an enumeration of which - /// socket requires which protocol. - pub fn addrs_with_scheme(&self) -> Vec<(net::SocketAddr, &str)> { - self.sockets.iter().map(|s| (s.addr, s.scheme)).collect() - } - - /// Use listener for accepting incoming connection requests - /// - /// HttpServer does not change any configuration for TcpListener, - /// it needs to be configured before passing it to listen() method. - pub fn listen(mut self, lst: net::TcpListener) -> Self { - let addr = lst.local_addr().unwrap(); - self.sockets.push(Socket { - lst, - addr, - scheme: "http", - handler: Box::new(HttpServiceBuilder::new( - self.factory.clone(), - DefaultAcceptor, - )), - }); - - self - } - - #[doc(hidden)] - /// Use listener for accepting incoming connection requests - pub fn listen_with(mut self, lst: net::TcpListener, acceptor: A) -> Self - where - A: AcceptorServiceFactory, - ::InitError: fmt::Debug, - { - let addr = lst.local_addr().unwrap(); - self.sockets.push(Socket { - lst, - addr, - scheme: "https", - handler: Box::new(HttpServiceBuilder::new(self.factory.clone(), acceptor)), - }); - - self - } - - #[cfg(feature = "tls")] - /// Use listener for accepting incoming tls connection requests - /// - /// HttpServer does not change any configuration for TcpListener, - /// it needs to be configured before passing it to listen() method. - pub fn listen_tls(self, lst: net::TcpListener, acceptor: TlsAcceptor) -> Self { - use actix_net::service::NewServiceExt; - - self.listen_with(lst, move || { - ssl::NativeTlsAcceptor::new(acceptor.clone()).map_err(|_| ()) - }) - } - - #[cfg(any(feature = "alpn", feature = "ssl"))] - /// Use listener for accepting incoming tls connection requests - /// - /// This method sets alpn protocols to "h2" and "http/1.1" - pub fn listen_ssl( - self, lst: net::TcpListener, builder: SslAcceptorBuilder, - ) -> io::Result { - use super::{openssl_acceptor_with_flags, ServerFlags}; - use actix_net::service::NewServiceExt; - - let flags = if self.no_http2 { - ServerFlags::HTTP1 - } else { - ServerFlags::HTTP1 | ServerFlags::HTTP2 - }; - - let acceptor = openssl_acceptor_with_flags(builder, flags)?; - Ok(self.listen_with(lst, move || { - ssl::OpensslAcceptor::new(acceptor.clone()).map_err(|_| ()) - })) - } - - #[cfg(feature = "rust-tls")] - /// Use listener for accepting incoming tls connection requests - /// - /// This method sets alpn protocols to "h2" and "http/1.1" - pub fn listen_rustls(self, lst: net::TcpListener, config: ServerConfig) -> Self { - use super::{RustlsAcceptor, ServerFlags}; - use actix_net::service::NewServiceExt; - - // alpn support - let flags = if self.no_http2 { - ServerFlags::HTTP1 - } else { - ServerFlags::HTTP1 | ServerFlags::HTTP2 - }; - - self.listen_with(lst, move || { - RustlsAcceptor::with_flags(config.clone(), flags).map_err(|_| ()) - }) - } - - /// The socket address to bind - /// - /// To bind multiple addresses this method can be called multiple times. - pub fn bind(mut self, addr: S) -> io::Result { - let sockets = self.bind2(addr)?; - - for lst in sockets { - self = self.listen(lst); - } - - Ok(self) - } - - /// Start listening for incoming connections with supplied acceptor. - #[doc(hidden)] - #[cfg_attr( - feature = "cargo-clippy", - allow(needless_pass_by_value) - )] - pub fn bind_with(mut self, addr: S, acceptor: A) -> io::Result - where - S: net::ToSocketAddrs, - A: AcceptorServiceFactory, - ::InitError: fmt::Debug, - { - let sockets = self.bind2(addr)?; - - for lst in sockets { - let addr = lst.local_addr().unwrap(); - self.sockets.push(Socket { - lst, - addr, - scheme: "https", - handler: Box::new(HttpServiceBuilder::new( - self.factory.clone(), - acceptor.clone(), - )), - }); - } - - Ok(self) - } - - fn bind2( - &self, addr: S, - ) -> io::Result> { - let mut err = None; - let mut succ = false; - let mut sockets = Vec::new(); - for addr in addr.to_socket_addrs()? { - match create_tcp_listener(addr, self.backlog) { - Ok(lst) => { - succ = true; - sockets.push(lst); - } - Err(e) => err = Some(e), - } - } - - if !succ { - if let Some(e) = err.take() { - Err(e) - } else { - Err(io::Error::new( - io::ErrorKind::Other, - "Can not bind to address.", - )) - } - } else { - Ok(sockets) - } - } - - #[cfg(feature = "tls")] - /// The ssl socket address to bind - /// - /// To bind multiple addresses this method can be called multiple times. - pub fn bind_tls( - self, addr: S, acceptor: TlsAcceptor, - ) -> io::Result { - use actix_net::service::NewServiceExt; - use actix_net::ssl::NativeTlsAcceptor; - - self.bind_with(addr, move || { - NativeTlsAcceptor::new(acceptor.clone()).map_err(|_| ()) - }) - } - - #[cfg(any(feature = "alpn", feature = "ssl"))] - /// Start listening for incoming tls connections. - /// - /// This method sets alpn protocols to "h2" and "http/1.1" - pub fn bind_ssl(self, addr: S, builder: SslAcceptorBuilder) -> io::Result - where - S: net::ToSocketAddrs, - { - use super::{openssl_acceptor_with_flags, ServerFlags}; - use actix_net::service::NewServiceExt; - - // alpn support - let flags = if self.no_http2 { - ServerFlags::HTTP1 - } else { - ServerFlags::HTTP1 | ServerFlags::HTTP2 - }; - - let acceptor = openssl_acceptor_with_flags(builder, flags)?; - self.bind_with(addr, move || { - ssl::OpensslAcceptor::new(acceptor.clone()).map_err(|_| ()) - }) - } - - #[cfg(feature = "rust-tls")] - /// Start listening for incoming tls connections. - /// - /// This method sets alpn protocols to "h2" and "http/1.1" - pub fn bind_rustls( - self, addr: S, builder: ServerConfig, - ) -> io::Result { - use super::{RustlsAcceptor, ServerFlags}; - use actix_net::service::NewServiceExt; - - // alpn support - let flags = if self.no_http2 { - ServerFlags::HTTP1 - } else { - ServerFlags::HTTP1 | ServerFlags::HTTP2 - }; - - self.bind_with(addr, move || { - RustlsAcceptor::with_flags(builder.clone(), flags).map_err(|_| ()) - }) - } -} - -impl H + Send + Clone> HttpServer { - /// Start listening for incoming connections. - /// - /// This method starts number of http workers in separate threads. - /// For each address this method starts separate thread which does - /// `accept()` in a loop. - /// - /// This methods panics if no socket address can be bound or an `Actix` system is not yet - /// configured. - /// - /// ```rust - /// extern crate actix_web; - /// extern crate actix; - /// use actix_web::{server, App, HttpResponse}; - /// - /// fn main() { - /// let sys = actix::System::new("example"); // <- create Actix system - /// - /// server::new(|| App::new().resource("/", |r| r.h(|_: &_| HttpResponse::Ok()))) - /// .bind("127.0.0.1:0") - /// .expect("Can not bind to 127.0.0.1:0") - /// .start(); - /// # actix::System::current().stop(); - /// sys.run(); // <- Run actix system, this method starts all async processes - /// } - /// ``` - pub fn start(mut self) -> Addr { - ssl::max_concurrent_ssl_connect(self.maxconnrate); - - let mut srv = Server::new() - .workers(self.threads) - .maxconn(self.maxconn) - .shutdown_timeout(self.shutdown_timeout); - - srv = if self.exit { srv.system_exit() } else { srv }; - srv = if self.no_signals { - srv.disable_signals() - } else { - srv - }; - - let sockets = mem::replace(&mut self.sockets, Vec::new()); - - for socket in sockets { - let host = self - .host - .as_ref() - .map(|h| h.to_owned()) - .unwrap_or_else(|| format!("{}", socket.addr)); - let (secure, client_shutdown) = if socket.scheme == "https" { - (true, self.client_shutdown) - } else { - (false, 0) - }; - srv = socket.handler.register( - srv, - socket.lst, - host, - socket.addr, - self.keep_alive, - secure, - self.client_timeout, - client_shutdown, - ); - } - srv.start() - } - - /// Spawn new thread and start listening for incoming connections. - /// - /// This method spawns new thread and starts new actix system. Other than - /// that it is similar to `start()` method. This method blocks. - /// - /// This methods panics if no socket addresses get bound. - /// - /// ```rust,ignore - /// # extern crate futures; - /// # extern crate actix_web; - /// # use futures::Future; - /// use actix_web::*; - /// - /// fn main() { - /// HttpServer::new(|| App::new().resource("/", |r| r.h(|_| HttpResponse::Ok()))) - /// .bind("127.0.0.1:0") - /// .expect("Can not bind to 127.0.0.1:0") - /// .run(); - /// } - /// ``` - pub fn run(self) { - let sys = System::new("http-server"); - self.start(); - sys.run(); - } - - /// Register current http server as actix-net's server service - pub fn register(self, mut srv: Server) -> Server { - for socket in self.sockets { - let host = self - .host - .as_ref() - .map(|h| h.to_owned()) - .unwrap_or_else(|| format!("{}", socket.addr)); - let (secure, client_shutdown) = if socket.scheme == "https" { - (true, self.client_shutdown) - } else { - (false, 0) - }; - srv = socket.handler.register( - srv, - socket.lst, - host, - socket.addr, - self.keep_alive, - secure, - self.client_timeout, - client_shutdown, - ); - } - srv - } -} - -fn create_tcp_listener( - addr: net::SocketAddr, backlog: i32, -) -> io::Result { - let builder = match addr { - net::SocketAddr::V4(_) => TcpBuilder::new_v4()?, - net::SocketAddr::V6(_) => TcpBuilder::new_v6()?, - }; - builder.reuse_address(true)?; - builder.bind(addr)?; - Ok(builder.listen(backlog)?) -} diff --git a/src/server/incoming.rs b/src/server/incoming.rs deleted file mode 100644 index b13bba2a..00000000 --- a/src/server/incoming.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! Support for `Stream`, deprecated! -use std::{io, net}; - -use actix::{Actor, Arbiter, AsyncContext, Context, Handler, Message}; -use futures::{Future, Stream}; -use tokio_io::{AsyncRead, AsyncWrite}; - -use super::channel::{HttpChannel, WrapperStream}; -use super::handler::{HttpHandler, IntoHttpHandler}; -use super::http::HttpServer; -use super::settings::{ServerSettings, ServiceConfig}; - -impl Message for WrapperStream { - type Result = (); -} - -impl HttpServer -where - H: IntoHttpHandler, - F: Fn() -> H + Send + Clone, -{ - #[doc(hidden)] - #[deprecated(since = "0.7.8")] - /// Start listening for incoming connections from a stream. - /// - /// This method uses only one thread for handling incoming connections. - pub fn start_incoming(self, stream: S, secure: bool) - where - S: Stream + 'static, - T: AsyncRead + AsyncWrite + 'static, - { - // set server settings - let addr: net::SocketAddr = "127.0.0.1:8080".parse().unwrap(); - let apps = (self.factory)().into_handler(); - let settings = ServiceConfig::new( - apps, - self.keep_alive, - self.client_timeout, - self.client_shutdown, - ServerSettings::new(addr, "127.0.0.1:8080", secure), - ); - - // start server - HttpIncoming::create(move |ctx| { - ctx.add_message_stream(stream.map_err(|_| ()).map(WrapperStream::new)); - HttpIncoming { settings } - }); - } -} - -struct HttpIncoming { - settings: ServiceConfig, -} - -impl Actor for HttpIncoming { - type Context = Context; -} - -impl Handler> for HttpIncoming -where - T: AsyncRead + AsyncWrite, - H: HttpHandler, -{ - type Result = (); - - fn handle(&mut self, msg: WrapperStream, _: &mut Context) -> Self::Result { - Arbiter::spawn(HttpChannel::new(self.settings.clone(), msg).map_err(|_| ())); - } -} diff --git a/src/server/input.rs b/src/server/input.rs deleted file mode 100644 index d23d1e99..00000000 --- a/src/server/input.rs +++ /dev/null @@ -1,288 +0,0 @@ -use std::io::{self, Write}; - -#[cfg(feature = "brotli")] -use brotli2::write::BrotliDecoder; -use bytes::{Bytes, BytesMut}; -use error::PayloadError; -#[cfg(feature = "flate2")] -use flate2::write::{GzDecoder, ZlibDecoder}; -use header::ContentEncoding; -use http::header::{HeaderMap, CONTENT_ENCODING}; -use payload::{PayloadSender, PayloadStatus, PayloadWriter}; - -pub(crate) enum PayloadType { - Sender(PayloadSender), - Encoding(Box), -} - -impl PayloadType { - #[cfg(any(feature = "brotli", feature = "flate2"))] - pub fn new(headers: &HeaderMap, sender: PayloadSender) -> PayloadType { - // check content-encoding - let enc = if let Some(enc) = headers.get(CONTENT_ENCODING) { - if let Ok(enc) = enc.to_str() { - ContentEncoding::from(enc) - } else { - ContentEncoding::Auto - } - } else { - ContentEncoding::Auto - }; - - match enc { - ContentEncoding::Auto | ContentEncoding::Identity => { - PayloadType::Sender(sender) - } - _ => PayloadType::Encoding(Box::new(EncodedPayload::new(sender, enc))), - } - } - - #[cfg(not(any(feature = "brotli", feature = "flate2")))] - pub fn new(headers: &HeaderMap, sender: PayloadSender) -> PayloadType { - PayloadType::Sender(sender) - } -} - -impl PayloadWriter for PayloadType { - #[inline] - fn set_error(&mut self, err: PayloadError) { - match *self { - PayloadType::Sender(ref mut sender) => sender.set_error(err), - PayloadType::Encoding(ref mut enc) => enc.set_error(err), - } - } - - #[inline] - fn feed_eof(&mut self) { - match *self { - PayloadType::Sender(ref mut sender) => sender.feed_eof(), - PayloadType::Encoding(ref mut enc) => enc.feed_eof(), - } - } - - #[inline] - fn feed_data(&mut self, data: Bytes) { - match *self { - PayloadType::Sender(ref mut sender) => sender.feed_data(data), - PayloadType::Encoding(ref mut enc) => enc.feed_data(data), - } - } - - #[inline] - fn need_read(&self) -> PayloadStatus { - match *self { - PayloadType::Sender(ref sender) => sender.need_read(), - PayloadType::Encoding(ref enc) => enc.need_read(), - } - } -} - -/// Payload wrapper with content decompression support -pub(crate) struct EncodedPayload { - inner: PayloadSender, - error: bool, - payload: PayloadStream, -} - -impl EncodedPayload { - pub fn new(inner: PayloadSender, enc: ContentEncoding) -> EncodedPayload { - EncodedPayload { - inner, - error: false, - payload: PayloadStream::new(enc), - } - } -} - -impl PayloadWriter for EncodedPayload { - fn set_error(&mut self, err: PayloadError) { - self.inner.set_error(err) - } - - fn feed_eof(&mut self) { - if !self.error { - match self.payload.feed_eof() { - Err(err) => { - self.error = true; - self.set_error(PayloadError::Io(err)); - } - Ok(value) => { - if let Some(b) = value { - self.inner.feed_data(b); - } - self.inner.feed_eof(); - } - } - } - } - - fn feed_data(&mut self, data: Bytes) { - if self.error { - return; - } - - match self.payload.feed_data(data) { - Ok(Some(b)) => self.inner.feed_data(b), - Ok(None) => (), - Err(e) => { - self.error = true; - self.set_error(e.into()); - } - } - } - - #[inline] - fn need_read(&self) -> PayloadStatus { - self.inner.need_read() - } -} - -pub(crate) enum Decoder { - #[cfg(feature = "flate2")] - Deflate(Box>), - #[cfg(feature = "flate2")] - Gzip(Box>), - #[cfg(feature = "brotli")] - Br(Box>), - Identity, -} - -pub(crate) struct Writer { - buf: BytesMut, -} - -impl Writer { - fn new() -> Writer { - Writer { - buf: BytesMut::with_capacity(8192), - } - } - fn take(&mut self) -> Bytes { - self.buf.take().freeze() - } -} - -impl io::Write for Writer { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.buf.extend_from_slice(buf); - Ok(buf.len()) - } - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } -} - -/// Payload stream with decompression support -pub(crate) struct PayloadStream { - decoder: Decoder, -} - -impl PayloadStream { - pub fn new(enc: ContentEncoding) -> PayloadStream { - let decoder = match enc { - #[cfg(feature = "brotli")] - ContentEncoding::Br => { - Decoder::Br(Box::new(BrotliDecoder::new(Writer::new()))) - } - #[cfg(feature = "flate2")] - ContentEncoding::Deflate => { - Decoder::Deflate(Box::new(ZlibDecoder::new(Writer::new()))) - } - #[cfg(feature = "flate2")] - ContentEncoding::Gzip => { - Decoder::Gzip(Box::new(GzDecoder::new(Writer::new()))) - } - _ => Decoder::Identity, - }; - PayloadStream { decoder } - } -} - -impl PayloadStream { - pub fn feed_eof(&mut self) -> io::Result> { - match self.decoder { - #[cfg(feature = "brotli")] - Decoder::Br(ref mut decoder) => match decoder.finish() { - Ok(mut writer) => { - let b = writer.take(); - if !b.is_empty() { - Ok(Some(b)) - } else { - Ok(None) - } - } - Err(e) => Err(e), - }, - #[cfg(feature = "flate2")] - Decoder::Gzip(ref mut decoder) => match decoder.try_finish() { - Ok(_) => { - let b = decoder.get_mut().take(); - if !b.is_empty() { - Ok(Some(b)) - } else { - Ok(None) - } - } - Err(e) => Err(e), - }, - #[cfg(feature = "flate2")] - Decoder::Deflate(ref mut decoder) => match decoder.try_finish() { - Ok(_) => { - let b = decoder.get_mut().take(); - if !b.is_empty() { - Ok(Some(b)) - } else { - Ok(None) - } - } - Err(e) => Err(e), - }, - Decoder::Identity => Ok(None), - } - } - - pub fn feed_data(&mut self, data: Bytes) -> io::Result> { - match self.decoder { - #[cfg(feature = "brotli")] - Decoder::Br(ref mut decoder) => match decoder.write_all(&data) { - Ok(_) => { - decoder.flush()?; - let b = decoder.get_mut().take(); - if !b.is_empty() { - Ok(Some(b)) - } else { - Ok(None) - } - } - Err(e) => Err(e), - }, - #[cfg(feature = "flate2")] - Decoder::Gzip(ref mut decoder) => match decoder.write_all(&data) { - Ok(_) => { - decoder.flush()?; - let b = decoder.get_mut().take(); - if !b.is_empty() { - Ok(Some(b)) - } else { - Ok(None) - } - } - Err(e) => Err(e), - }, - #[cfg(feature = "flate2")] - Decoder::Deflate(ref mut decoder) => match decoder.write_all(&data) { - Ok(_) => { - decoder.flush()?; - let b = decoder.get_mut().take(); - if !b.is_empty() { - Ok(Some(b)) - } else { - Ok(None) - } - } - Err(e) => Err(e), - }, - Decoder::Identity => Ok(Some(data)), - } - } -} diff --git a/src/server/message.rs b/src/server/message.rs deleted file mode 100644 index 9c4bc1ec..00000000 --- a/src/server/message.rs +++ /dev/null @@ -1,284 +0,0 @@ -use std::cell::{Cell, Ref, RefCell, RefMut}; -use std::collections::VecDeque; -use std::fmt; -use std::net::SocketAddr; -use std::rc::Rc; - -use http::{header, HeaderMap, Method, Uri, Version}; - -use extensions::Extensions; -use httpmessage::HttpMessage; -use info::ConnectionInfo; -use payload::Payload; -use server::ServerSettings; -use uri::Url as InnerUrl; - -bitflags! { - pub(crate) struct MessageFlags: u8 { - const KEEPALIVE = 0b0000_0001; - const CONN_INFO = 0b0000_0010; - } -} - -/// Request's context -pub struct Request { - pub(crate) inner: Rc, -} - -pub(crate) struct InnerRequest { - pub(crate) version: Version, - pub(crate) method: Method, - pub(crate) url: InnerUrl, - pub(crate) flags: Cell, - pub(crate) headers: HeaderMap, - pub(crate) extensions: RefCell, - pub(crate) addr: Option, - pub(crate) info: RefCell, - pub(crate) payload: RefCell>, - pub(crate) settings: ServerSettings, - pub(crate) stream_extensions: Option>, - pool: &'static RequestPool, -} - -impl InnerRequest { - #[inline] - /// Reset request instance - pub fn reset(&mut self) { - self.headers.clear(); - self.extensions.borrow_mut().clear(); - self.flags.set(MessageFlags::empty()); - *self.payload.borrow_mut() = None; - } -} - -impl HttpMessage for Request { - type Stream = Payload; - - fn headers(&self) -> &HeaderMap { - &self.inner.headers - } - - #[inline] - fn payload(&self) -> Payload { - if let Some(payload) = self.inner.payload.borrow_mut().take() { - payload - } else { - Payload::empty() - } - } -} - -impl Request { - /// Create new RequestContext instance - pub(crate) fn new(pool: &'static RequestPool, settings: ServerSettings) -> Request { - Request { - inner: Rc::new(InnerRequest { - pool, - settings, - method: Method::GET, - url: InnerUrl::default(), - version: Version::HTTP_11, - headers: HeaderMap::with_capacity(16), - flags: Cell::new(MessageFlags::empty()), - addr: None, - info: RefCell::new(ConnectionInfo::default()), - payload: RefCell::new(None), - extensions: RefCell::new(Extensions::new()), - stream_extensions: None, - }), - } - } - - #[inline] - pub(crate) fn inner(&self) -> &InnerRequest { - self.inner.as_ref() - } - - #[inline] - pub(crate) fn inner_mut(&mut self) -> &mut InnerRequest { - Rc::get_mut(&mut self.inner).expect("Multiple copies exist") - } - - #[inline] - pub(crate) fn url(&self) -> &InnerUrl { - &self.inner().url - } - - /// Read the Request Uri. - #[inline] - pub fn uri(&self) -> &Uri { - self.inner().url.uri() - } - - /// Read the Request method. - #[inline] - pub fn method(&self) -> &Method { - &self.inner().method - } - - /// Read the Request Version. - #[inline] - pub fn version(&self) -> Version { - self.inner().version - } - - /// The target path of this Request. - #[inline] - pub fn path(&self) -> &str { - self.inner().url.path() - } - - #[inline] - /// Returns Request's headers. - pub fn headers(&self) -> &HeaderMap { - &self.inner().headers - } - - #[inline] - /// Returns mutable Request's headers. - pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.inner_mut().headers - } - - /// 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()` method should - /// be used. - pub fn peer_addr(&self) -> Option { - self.inner().addr - } - - /// Checks if a connection should be kept alive. - #[inline] - pub fn keep_alive(&self) -> bool { - self.inner().flags.get().contains(MessageFlags::KEEPALIVE) - } - - /// Request extensions - #[inline] - pub fn extensions(&self) -> Ref { - self.inner().extensions.borrow() - } - - /// Mutable reference to a the request's extensions - #[inline] - pub fn extensions_mut(&self) -> RefMut { - self.inner().extensions.borrow_mut() - } - - /// Check if request requires connection upgrade - pub fn upgrade(&self) -> bool { - if let Some(conn) = self.inner().headers.get(header::CONNECTION) { - if let Ok(s) = conn.to_str() { - return s.to_lowercase().contains("upgrade"); - } - } - self.inner().method == Method::CONNECT - } - - /// Get *ConnectionInfo* for the correct request. - pub fn connection_info(&self) -> Ref { - if self.inner().flags.get().contains(MessageFlags::CONN_INFO) { - self.inner().info.borrow() - } else { - let mut flags = self.inner().flags.get(); - flags.insert(MessageFlags::CONN_INFO); - self.inner().flags.set(flags); - self.inner().info.borrow_mut().update(self); - self.inner().info.borrow() - } - } - - /// Io stream extensions - #[inline] - pub fn stream_extensions(&self) -> Option<&Extensions> { - self.inner().stream_extensions.as_ref().map(|e| e.as_ref()) - } - - /// Server settings - #[inline] - pub fn server_settings(&self) -> &ServerSettings { - &self.inner().settings - } - - pub(crate) fn clone(&self) -> Self { - Request { - inner: self.inner.clone(), - } - } - - pub(crate) fn release(self) { - let mut inner = self.inner; - if let Some(r) = Rc::get_mut(&mut inner) { - r.reset(); - } else { - return; - } - inner.pool.release(inner); - } -} - -impl fmt::Debug for Request { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!( - f, - "\nRequest {:?} {}:{}", - self.version(), - self.method(), - self.path() - )?; - if let Some(q) = self.uri().query().as_ref() { - writeln!(f, " query: ?{:?}", q)?; - } - writeln!(f, " headers:")?; - for (key, val) in self.headers().iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } -} - -pub(crate) struct RequestPool( - RefCell>>, - RefCell, -); - -thread_local!(static POOL: &'static RequestPool = RequestPool::create()); - -impl RequestPool { - fn create() -> &'static RequestPool { - let pool = RequestPool( - RefCell::new(VecDeque::with_capacity(128)), - RefCell::new(ServerSettings::default()), - ); - Box::leak(Box::new(pool)) - } - - pub fn pool(settings: ServerSettings) -> &'static RequestPool { - POOL.with(|p| { - *p.1.borrow_mut() = settings; - *p - }) - } - - #[inline] - pub fn get(pool: &'static RequestPool) -> Request { - if let Some(msg) = pool.0.borrow_mut().pop_front() { - Request { inner: msg } - } else { - Request::new(pool, pool.1.borrow().clone()) - } - } - - #[inline] - /// Release request instance - pub fn release(&self, msg: Rc) { - let v = &mut self.0.borrow_mut(); - if v.len() < 128 { - v.push_front(msg); - } - } -} diff --git a/src/server/mod.rs b/src/server/mod.rs deleted file mode 100644 index 64129854..00000000 --- a/src/server/mod.rs +++ /dev/null @@ -1,370 +0,0 @@ -//! Http server module -//! -//! The module contains everything necessary to setup -//! HTTP server. -//! -//! In order to start HTTP server, first you need to create and configure it -//! using factory that can be supplied to [new](fn.new.html). -//! -//! ## Factory -//! -//! Factory is a function that returns Application, describing how -//! to serve incoming HTTP requests. -//! -//! As the server uses worker pool, the factory function is restricted to trait bounds -//! `Send + Clone + 'static` so that each worker would be able to accept Application -//! without a need for synchronization. -//! -//! If you wish to share part of state among all workers you should -//! wrap it in `Arc` and potentially synchronization primitive like -//! [RwLock](https://doc.rust-lang.org/std/sync/struct.RwLock.html) -//! If the wrapped type is not thread safe. -//! -//! Note though that locking is not advisable for asynchronous programming -//! and you should minimize all locks in your request handlers -//! -//! ## HTTPS Support -//! -//! Actix-web provides support for major crates that provides TLS. -//! Each TLS implementation is provided with [AcceptorService](trait.AcceptorService.html) -//! that describes how HTTP Server accepts connections. -//! -//! For `bind` and `listen` there are corresponding `bind_ssl|tls|rustls` and `listen_ssl|tls|rustls` that accepts -//! these services. -//! -//! **NOTE:** `native-tls` doesn't support `HTTP2` yet -//! -//! ## Signal handling and shutdown -//! -//! By default HTTP Server listens for system signals -//! and, gracefully shuts down at most after 30 seconds. -//! -//! Both signal handling and shutdown timeout can be controlled -//! using corresponding methods. -//! -//! If worker, for some reason, unable to shut down within timeout -//! it is forcibly dropped. -//! -//! ## Example -//! -//! ```rust,ignore -//!extern crate actix; -//!extern crate actix_web; -//!extern crate rustls; -//! -//!use actix_web::{http, middleware, server, App, Error, HttpRequest, HttpResponse, Responder}; -//!use std::io::BufReader; -//!use rustls::internal::pemfile::{certs, rsa_private_keys}; -//!use rustls::{NoClientAuth, ServerConfig}; -//! -//!fn index(req: &HttpRequest) -> Result { -//! Ok(HttpResponse::Ok().content_type("text/plain").body("Welcome!")) -//!} -//! -//!fn load_ssl() -> ServerConfig { -//! use std::io::BufReader; -//! -//! const CERT: &'static [u8] = include_bytes!("../cert.pem"); -//! const KEY: &'static [u8] = include_bytes!("../key.pem"); -//! -//! let mut cert = BufReader::new(CERT); -//! let mut key = BufReader::new(KEY); -//! -//! let mut config = ServerConfig::new(NoClientAuth::new()); -//! let cert_chain = certs(&mut cert).unwrap(); -//! let mut keys = rsa_private_keys(&mut key).unwrap(); -//! config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); -//! -//! config -//!} -//! -//!fn main() { -//! let sys = actix::System::new("http-server"); -//! // load ssl keys -//! let config = load_ssl(); -//! -//! // create and start server at once -//! server::new(|| { -//! App::new() -//! // register simple handler, handle all methods -//! .resource("/index.html", |r| r.f(index)) -//! })) -//! }).bind_rustls("127.0.0.1:8443", config) -//! .unwrap() -//! .start(); -//! -//! println!("Started http server: 127.0.0.1:8080"); -//! //Run system so that server would start accepting connections -//! let _ = sys.run(); -//!} -//! ``` -use std::net::{Shutdown, SocketAddr}; -use std::rc::Rc; -use std::{io, time}; - -use bytes::{BufMut, BytesMut}; -use futures::{Async, Poll}; -use tokio_io::{AsyncRead, AsyncWrite}; -use tokio_tcp::TcpStream; - -pub use actix_net::server::{PauseServer, ResumeServer, StopServer}; - -pub(crate) mod acceptor; -pub(crate) mod builder; -mod channel; -mod error; -pub(crate) mod h1; -pub(crate) mod h1decoder; -mod h1writer; -mod h2; -mod h2writer; -mod handler; -pub(crate) mod helpers; -mod http; -pub(crate) mod incoming; -pub(crate) mod input; -pub(crate) mod message; -pub(crate) mod output; -pub(crate) mod service; -pub(crate) mod settings; -mod ssl; - -pub use self::handler::*; -pub use self::http::HttpServer; -pub use self::message::Request; -pub use self::ssl::*; - -pub use self::error::{AcceptorError, HttpDispatchError}; -pub use self::settings::ServerSettings; - -#[doc(hidden)] -pub use self::acceptor::AcceptorTimeout; - -#[doc(hidden)] -pub use self::settings::{ServiceConfig, ServiceConfigBuilder}; - -#[doc(hidden)] -pub use self::service::{H1Service, HttpService, StreamConfiguration}; - -#[doc(hidden)] -pub use self::helpers::write_content_length; - -use body::Binary; -use extensions::Extensions; -use header::ContentEncoding; -use httpresponse::HttpResponse; - -/// max buffer size 64k -pub(crate) const MAX_WRITE_BUFFER_SIZE: usize = 65_536; - -const LW_BUFFER_SIZE: usize = 4096; -const HW_BUFFER_SIZE: usize = 32_768; - -/// Create new http server with application factory. -/// -/// This is shortcut for `server::HttpServer::new()` method. -/// -/// ```rust -/// # extern crate actix_web; -/// # extern crate actix; -/// use actix_web::{server, App, HttpResponse}; -/// -/// fn main() { -/// let sys = actix::System::new("example"); // <- create Actix system -/// -/// server::new( -/// || App::new() -/// .resource("/", |r| r.f(|_| HttpResponse::Ok()))) -/// .bind("127.0.0.1:59090").unwrap() -/// .start(); -/// -/// # actix::System::current().stop(); -/// sys.run(); -/// } -/// ``` -pub fn new(factory: F) -> HttpServer -where - F: Fn() -> H + Send + Clone + 'static, - H: IntoHttpHandler + 'static, -{ - HttpServer::new(factory) -} - -#[doc(hidden)] -bitflags! { - ///Flags that can be used to configure HTTP Server. - pub struct ServerFlags: u8 { - ///Use HTTP1 protocol - const HTTP1 = 0b0000_0001; - ///Use HTTP2 protocol - const HTTP2 = 0b0000_0010; - } -} - -#[derive(Debug, PartialEq, Clone, Copy)] -/// Server keep-alive setting -pub enum KeepAlive { - /// Keep alive in seconds - Timeout(usize), - /// Use `SO_KEEPALIVE` socket option, value in seconds - Tcp(usize), - /// Relay on OS to shutdown tcp connection - Os, - /// Disabled - Disabled, -} - -impl From for KeepAlive { - fn from(keepalive: usize) -> Self { - KeepAlive::Timeout(keepalive) - } -} - -impl From> for KeepAlive { - fn from(keepalive: Option) -> Self { - if let Some(keepalive) = keepalive { - KeepAlive::Timeout(keepalive) - } else { - KeepAlive::Disabled - } - } -} - -#[doc(hidden)] -#[derive(Debug)] -pub enum WriterState { - Done, - Pause, -} - -#[doc(hidden)] -/// Stream writer -pub trait Writer { - /// number of bytes written to the stream - fn written(&self) -> u64; - - #[doc(hidden)] - fn set_date(&mut self); - - #[doc(hidden)] - fn buffer(&mut self) -> &mut BytesMut; - - fn start( - &mut self, req: &Request, resp: &mut HttpResponse, encoding: ContentEncoding, - ) -> io::Result; - - fn write(&mut self, payload: &Binary) -> io::Result; - - fn write_eof(&mut self) -> io::Result; - - fn poll_completed(&mut self, shutdown: bool) -> Poll<(), io::Error>; -} - -#[doc(hidden)] -/// Low-level io stream operations -pub trait IoStream: AsyncRead + AsyncWrite + 'static { - fn shutdown(&mut self, how: Shutdown) -> io::Result<()>; - - /// Returns the socket address of the remote peer of this TCP connection. - fn peer_addr(&self) -> Option { - None - } - - /// Sets the value of the TCP_NODELAY option on this socket. - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()>; - - fn set_linger(&mut self, dur: Option) -> io::Result<()>; - - fn set_keepalive(&mut self, dur: Option) -> io::Result<()>; - - fn read_available(&mut self, buf: &mut BytesMut) -> Poll<(bool, bool), io::Error> { - let mut read_some = false; - loop { - if buf.remaining_mut() < LW_BUFFER_SIZE { - buf.reserve(HW_BUFFER_SIZE); - } - - let read = unsafe { self.read(buf.bytes_mut()) }; - match read { - Ok(n) => { - if n == 0 { - return Ok(Async::Ready((read_some, true))); - } else { - read_some = true; - unsafe { - buf.advance_mut(n); - } - } - } - Err(e) => { - return if e.kind() == io::ErrorKind::WouldBlock { - if read_some { - Ok(Async::Ready((read_some, false))) - } else { - Ok(Async::NotReady) - } - } else if e.kind() == io::ErrorKind::ConnectionReset && read_some { - Ok(Async::Ready((read_some, true))) - } else { - Err(e) - }; - } - } - } - } - - /// Extra io stream extensions - fn extensions(&self) -> Option> { - None - } -} - -#[cfg(all(unix, feature = "uds"))] -impl IoStream for ::tokio_uds::UnixStream { - #[inline] - fn shutdown(&mut self, how: Shutdown) -> io::Result<()> { - ::tokio_uds::UnixStream::shutdown(self, how) - } - - #[inline] - fn set_nodelay(&mut self, _nodelay: bool) -> io::Result<()> { - Ok(()) - } - - #[inline] - fn set_linger(&mut self, _dur: Option) -> io::Result<()> { - Ok(()) - } - - #[inline] - fn set_keepalive(&mut self, _dur: Option) -> io::Result<()> { - Ok(()) - } -} - -impl IoStream for TcpStream { - #[inline] - fn shutdown(&mut self, how: Shutdown) -> io::Result<()> { - TcpStream::shutdown(self, how) - } - - #[inline] - fn peer_addr(&self) -> Option { - TcpStream::peer_addr(self).ok() - } - - #[inline] - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { - TcpStream::set_nodelay(self, nodelay) - } - - #[inline] - fn set_linger(&mut self, dur: Option) -> io::Result<()> { - TcpStream::set_linger(self, dur) - } - - #[inline] - fn set_keepalive(&mut self, dur: Option) -> io::Result<()> { - TcpStream::set_keepalive(self, dur) - } -} diff --git a/src/server/output.rs b/src/server/output.rs deleted file mode 100644 index 4a86ffbb..00000000 --- a/src/server/output.rs +++ /dev/null @@ -1,760 +0,0 @@ -use std::fmt::Write as FmtWrite; -use std::io::Write; -use std::str::FromStr; -use std::{cmp, fmt, io, mem}; - -#[cfg(feature = "brotli")] -use brotli2::write::BrotliEncoder; -use bytes::BytesMut; -#[cfg(feature = "flate2")] -use flate2::write::{GzEncoder, ZlibEncoder}; -#[cfg(feature = "flate2")] -use flate2::Compression; -use http::header::{ACCEPT_ENCODING, CONTENT_LENGTH}; -use http::{StatusCode, Version}; - -use super::message::InnerRequest; -use body::{Binary, Body}; -use header::ContentEncoding; -use httpresponse::HttpResponse; - -#[derive(Debug)] -pub(crate) enum ResponseLength { - Chunked, - Zero, - Length(usize), - Length64(u64), - None, -} - -#[derive(Debug)] -pub(crate) struct ResponseInfo { - head: bool, - pub length: ResponseLength, - pub content_encoding: Option<&'static str>, -} - -impl ResponseInfo { - pub fn new(head: bool) -> Self { - ResponseInfo { - head, - length: ResponseLength::None, - content_encoding: None, - } - } -} - -#[derive(Debug)] -pub(crate) enum Output { - Empty(BytesMut), - Buffer(BytesMut), - Encoder(ContentEncoder), - TE(TransferEncoding), - Done, -} - -impl Output { - pub fn take(&mut self) -> BytesMut { - match mem::replace(self, Output::Done) { - Output::Empty(bytes) => bytes, - Output::Buffer(bytes) => bytes, - Output::Encoder(mut enc) => enc.take_buf(), - Output::TE(mut te) => te.take(), - Output::Done => panic!(), - } - } - - pub fn take_option(&mut self) -> Option { - match mem::replace(self, Output::Done) { - Output::Empty(bytes) => Some(bytes), - Output::Buffer(bytes) => Some(bytes), - Output::Encoder(mut enc) => Some(enc.take_buf()), - Output::TE(mut te) => Some(te.take()), - Output::Done => None, - } - } - - pub fn as_ref(&mut self) -> &BytesMut { - match self { - Output::Empty(ref mut bytes) => bytes, - Output::Buffer(ref mut bytes) => bytes, - Output::Encoder(ref mut enc) => enc.buf_ref(), - Output::TE(ref mut te) => te.buf_ref(), - Output::Done => panic!(), - } - } - pub fn as_mut(&mut self) -> &mut BytesMut { - match self { - Output::Empty(ref mut bytes) => bytes, - Output::Buffer(ref mut bytes) => bytes, - Output::Encoder(ref mut enc) => enc.buf_mut(), - Output::TE(ref mut te) => te.buf_mut(), - Output::Done => panic!(), - } - } - pub fn split_to(&mut self, cap: usize) -> BytesMut { - match self { - Output::Empty(ref mut bytes) => bytes.split_to(cap), - Output::Buffer(ref mut bytes) => bytes.split_to(cap), - Output::Encoder(ref mut enc) => enc.buf_mut().split_to(cap), - Output::TE(ref mut te) => te.buf_mut().split_to(cap), - Output::Done => BytesMut::new(), - } - } - - pub fn len(&self) -> usize { - match self { - Output::Empty(ref bytes) => bytes.len(), - Output::Buffer(ref bytes) => bytes.len(), - Output::Encoder(ref enc) => enc.len(), - Output::TE(ref te) => te.len(), - Output::Done => 0, - } - } - - pub fn is_empty(&self) -> bool { - match self { - Output::Empty(ref bytes) => bytes.is_empty(), - Output::Buffer(ref bytes) => bytes.is_empty(), - Output::Encoder(ref enc) => enc.is_empty(), - Output::TE(ref te) => te.is_empty(), - Output::Done => true, - } - } - - pub fn write(&mut self, data: &[u8]) -> Result<(), io::Error> { - match self { - Output::Buffer(ref mut bytes) => { - bytes.extend_from_slice(data); - Ok(()) - } - Output::Encoder(ref mut enc) => enc.write(data), - Output::TE(ref mut te) => te.encode(data).map(|_| ()), - Output::Empty(_) | Output::Done => Ok(()), - } - } - - pub fn write_eof(&mut self) -> Result { - match self { - Output::Buffer(_) => Ok(true), - Output::Encoder(ref mut enc) => enc.write_eof(), - Output::TE(ref mut te) => Ok(te.encode_eof()), - Output::Empty(_) | Output::Done => Ok(true), - } - } - - pub(crate) fn for_server( - &mut self, info: &mut ResponseInfo, req: &InnerRequest, resp: &mut HttpResponse, - response_encoding: ContentEncoding, - ) { - let buf = self.take(); - let version = resp.version().unwrap_or_else(|| req.version); - let mut len = 0; - - let has_body = match resp.body() { - Body::Empty => false, - Body::Binary(ref bin) => { - len = bin.len(); - !(response_encoding == ContentEncoding::Auto && len < 96) - } - _ => true, - }; - - // Enable content encoding only if response does not contain Content-Encoding - // header - #[cfg(any(feature = "brotli", feature = "flate2"))] - let mut encoding = if has_body { - let encoding = match response_encoding { - ContentEncoding::Auto => { - // negotiate content-encoding - if let Some(val) = req.headers.get(ACCEPT_ENCODING) { - if let Ok(enc) = val.to_str() { - AcceptEncoding::parse(enc) - } else { - ContentEncoding::Identity - } - } else { - ContentEncoding::Identity - } - } - encoding => encoding, - }; - if encoding.is_compression() { - info.content_encoding = Some(encoding.as_str()); - } - encoding - } else { - ContentEncoding::Identity - }; - #[cfg(not(any(feature = "brotli", feature = "flate2")))] - let mut encoding = ContentEncoding::Identity; - - let transfer = match resp.body() { - Body::Empty => { - info.length = match resp.status() { - StatusCode::NO_CONTENT - | StatusCode::CONTINUE - | StatusCode::SWITCHING_PROTOCOLS - | StatusCode::PROCESSING => ResponseLength::None, - _ => ResponseLength::Zero, - }; - *self = Output::Empty(buf); - return; - } - Body::Binary(_) => { - #[cfg(any(feature = "brotli", feature = "flate2"))] - { - if !(encoding == ContentEncoding::Identity - || encoding == ContentEncoding::Auto) - { - let mut tmp = BytesMut::new(); - let mut transfer = TransferEncoding::eof(tmp); - let mut enc = match encoding { - #[cfg(feature = "flate2")] - ContentEncoding::Deflate => ContentEncoder::Deflate( - ZlibEncoder::new(transfer, Compression::fast()), - ), - #[cfg(feature = "flate2")] - ContentEncoding::Gzip => ContentEncoder::Gzip( - GzEncoder::new(transfer, Compression::fast()), - ), - #[cfg(feature = "brotli")] - ContentEncoding::Br => { - ContentEncoder::Br(BrotliEncoder::new(transfer, 3)) - } - ContentEncoding::Identity | ContentEncoding::Auto => { - unreachable!() - } - }; - - let bin = resp.replace_body(Body::Empty).binary(); - - // TODO return error! - let _ = enc.write(bin.as_ref()); - let _ = enc.write_eof(); - let body = enc.buf_mut().take(); - len = body.len(); - resp.replace_body(Binary::from(body)); - } - } - - info.length = ResponseLength::Length(len); - if info.head { - *self = Output::Empty(buf); - } else { - *self = Output::Buffer(buf); - } - return; - } - Body::Streaming(_) | Body::Actor(_) => { - if resp.upgrade() { - if version == Version::HTTP_2 { - error!("Connection upgrade is forbidden for HTTP/2"); - } - if encoding != ContentEncoding::Identity { - encoding = ContentEncoding::Identity; - info.content_encoding.take(); - } - TransferEncoding::eof(buf) - } else { - if !(encoding == ContentEncoding::Identity - || encoding == ContentEncoding::Auto) - { - resp.headers_mut().remove(CONTENT_LENGTH); - } - Output::streaming_encoding(info, buf, version, resp) - } - } - }; - // check for head response - if info.head { - resp.set_body(Body::Empty); - *self = Output::Empty(transfer.buf.unwrap()); - return; - } - - let enc = match encoding { - #[cfg(feature = "flate2")] - ContentEncoding::Deflate => { - ContentEncoder::Deflate(ZlibEncoder::new(transfer, Compression::fast())) - } - #[cfg(feature = "flate2")] - ContentEncoding::Gzip => { - ContentEncoder::Gzip(GzEncoder::new(transfer, Compression::fast())) - } - #[cfg(feature = "brotli")] - ContentEncoding::Br => ContentEncoder::Br(BrotliEncoder::new(transfer, 3)), - ContentEncoding::Identity | ContentEncoding::Auto => { - *self = Output::TE(transfer); - return; - } - }; - *self = Output::Encoder(enc); - } - - fn streaming_encoding( - info: &mut ResponseInfo, buf: BytesMut, version: Version, - resp: &mut HttpResponse, - ) -> TransferEncoding { - match resp.chunked() { - Some(true) => { - // Enable transfer encoding - info.length = ResponseLength::Chunked; - if version == Version::HTTP_2 { - TransferEncoding::eof(buf) - } else { - TransferEncoding::chunked(buf) - } - } - Some(false) => TransferEncoding::eof(buf), - None => { - // if Content-Length is specified, then use it as length hint - let (len, chunked) = - if let Some(len) = resp.headers().get(CONTENT_LENGTH) { - // Content-Length - if let Ok(s) = len.to_str() { - if let Ok(len) = s.parse::() { - (Some(len), false) - } else { - error!("illegal Content-Length: {:?}", len); - (None, false) - } - } else { - error!("illegal Content-Length: {:?}", len); - (None, false) - } - } else { - (None, true) - }; - - if !chunked { - if let Some(len) = len { - info.length = ResponseLength::Length64(len); - TransferEncoding::length(len, buf) - } else { - TransferEncoding::eof(buf) - } - } else { - // Enable transfer encoding - info.length = ResponseLength::Chunked; - if version == Version::HTTP_2 { - TransferEncoding::eof(buf) - } else { - TransferEncoding::chunked(buf) - } - } - } - } - } -} - -pub(crate) enum ContentEncoder { - #[cfg(feature = "flate2")] - Deflate(ZlibEncoder), - #[cfg(feature = "flate2")] - Gzip(GzEncoder), - #[cfg(feature = "brotli")] - Br(BrotliEncoder), - Identity(TransferEncoding), -} - -impl fmt::Debug for ContentEncoder { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(_) => writeln!(f, "ContentEncoder(Brotli)"), - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(_) => writeln!(f, "ContentEncoder(Deflate)"), - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(_) => writeln!(f, "ContentEncoder(Gzip)"), - ContentEncoder::Identity(_) => writeln!(f, "ContentEncoder(Identity)"), - } - } -} - -impl ContentEncoder { - #[inline] - pub fn len(&self) -> usize { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(ref encoder) => encoder.get_ref().len(), - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(ref encoder) => encoder.get_ref().len(), - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(ref encoder) => encoder.get_ref().len(), - ContentEncoder::Identity(ref encoder) => encoder.len(), - } - } - - #[inline] - pub fn is_empty(&self) -> bool { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(ref encoder) => encoder.get_ref().is_empty(), - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(ref encoder) => encoder.get_ref().is_empty(), - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(ref encoder) => encoder.get_ref().is_empty(), - ContentEncoder::Identity(ref encoder) => encoder.is_empty(), - } - } - - #[inline] - pub(crate) fn take_buf(&mut self) -> BytesMut { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(), - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(), - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(), - ContentEncoder::Identity(ref mut encoder) => encoder.take(), - } - } - - #[inline] - pub(crate) fn buf_mut(&mut self) -> &mut BytesMut { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(ref mut encoder) => encoder.get_mut().buf_mut(), - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().buf_mut(), - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().buf_mut(), - ContentEncoder::Identity(ref mut encoder) => encoder.buf_mut(), - } - } - - #[inline] - pub(crate) fn buf_ref(&mut self) -> &BytesMut { - match *self { - #[cfg(feature = "brotli")] - ContentEncoder::Br(ref mut encoder) => encoder.get_mut().buf_ref(), - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().buf_ref(), - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().buf_ref(), - ContentEncoder::Identity(ref mut encoder) => encoder.buf_ref(), - } - } - - #[cfg_attr(feature = "cargo-clippy", allow(inline_always))] - #[inline(always)] - pub fn write_eof(&mut self) -> Result { - let encoder = - mem::replace(self, ContentEncoder::Identity(TransferEncoding::empty())); - - match encoder { - #[cfg(feature = "brotli")] - ContentEncoder::Br(encoder) => match encoder.finish() { - Ok(mut writer) => { - writer.encode_eof(); - *self = ContentEncoder::Identity(writer); - Ok(true) - } - Err(err) => Err(err), - }, - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(encoder) => match encoder.finish() { - Ok(mut writer) => { - writer.encode_eof(); - *self = ContentEncoder::Identity(writer); - Ok(true) - } - Err(err) => Err(err), - }, - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(encoder) => match encoder.finish() { - Ok(mut writer) => { - writer.encode_eof(); - *self = ContentEncoder::Identity(writer); - Ok(true) - } - Err(err) => Err(err), - }, - ContentEncoder::Identity(mut writer) => { - let res = writer.encode_eof(); - *self = ContentEncoder::Identity(writer); - Ok(res) - } - } - } - - #[cfg_attr(feature = "cargo-clippy", allow(inline_always))] - #[inline(always)] - pub 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(()), - Err(err) => { - trace!("Error decoding br encoding: {}", err); - Err(err) - } - }, - #[cfg(feature = "flate2")] - ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) { - Ok(_) => Ok(()), - Err(err) => { - trace!("Error decoding gzip encoding: {}", err); - Err(err) - } - }, - #[cfg(feature = "flate2")] - ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) { - Ok(_) => Ok(()), - Err(err) => { - trace!("Error decoding deflate encoding: {}", err); - Err(err) - } - }, - ContentEncoder::Identity(ref mut encoder) => { - encoder.encode(data)?; - Ok(()) - } - } - } -} - -/// Encoders to handle different Transfer-Encodings. -#[derive(Debug)] -pub(crate) struct TransferEncoding { - buf: Option, - kind: TransferEncodingKind, -} - -#[derive(Debug, PartialEq, Clone)] -enum TransferEncodingKind { - /// An Encoder for when Transfer-Encoding includes `chunked`. - Chunked(bool), - /// An Encoder for when Content-Length is set. - /// - /// Enforces that the body is not longer than the Content-Length header. - Length(u64), - /// An Encoder for when Content-Length is not known. - /// - /// Application decides when to stop writing. - Eof, -} - -impl TransferEncoding { - fn take(&mut self) -> BytesMut { - self.buf.take().unwrap() - } - - fn buf_ref(&mut self) -> &BytesMut { - self.buf.as_ref().unwrap() - } - - fn len(&self) -> usize { - self.buf.as_ref().unwrap().len() - } - - fn is_empty(&self) -> bool { - self.buf.as_ref().unwrap().is_empty() - } - - fn buf_mut(&mut self) -> &mut BytesMut { - self.buf.as_mut().unwrap() - } - - #[inline] - pub fn empty() -> TransferEncoding { - TransferEncoding { - buf: None, - kind: TransferEncodingKind::Eof, - } - } - - #[inline] - pub fn eof(buf: BytesMut) -> TransferEncoding { - TransferEncoding { - buf: Some(buf), - kind: TransferEncodingKind::Eof, - } - } - - #[inline] - pub fn chunked(buf: BytesMut) -> TransferEncoding { - TransferEncoding { - buf: Some(buf), - kind: TransferEncodingKind::Chunked(false), - } - } - - #[inline] - pub fn length(len: u64, buf: BytesMut) -> TransferEncoding { - TransferEncoding { - buf: Some(buf), - kind: TransferEncodingKind::Length(len), - } - } - - /// Encode message. Return `EOF` state of encoder - #[inline] - pub fn encode(&mut self, msg: &[u8]) -> io::Result { - match self.kind { - TransferEncodingKind::Eof => { - let eof = msg.is_empty(); - self.buf.as_mut().unwrap().extend_from_slice(msg); - Ok(eof) - } - TransferEncodingKind::Chunked(ref mut eof) => { - if *eof { - return Ok(true); - } - - if msg.is_empty() { - *eof = true; - self.buf.as_mut().unwrap().extend_from_slice(b"0\r\n\r\n"); - } else { - let mut buf = BytesMut::new(); - writeln!(&mut buf, "{:X}\r", msg.len()) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - - let b = self.buf.as_mut().unwrap(); - b.reserve(buf.len() + msg.len() + 2); - b.extend_from_slice(buf.as_ref()); - b.extend_from_slice(msg); - b.extend_from_slice(b"\r\n"); - } - Ok(*eof) - } - TransferEncodingKind::Length(ref mut remaining) => { - if *remaining > 0 { - if msg.is_empty() { - return Ok(*remaining == 0); - } - let len = cmp::min(*remaining, msg.len() as u64); - - self.buf - .as_mut() - .unwrap() - .extend_from_slice(&msg[..len as usize]); - - *remaining -= len as u64; - Ok(*remaining == 0) - } else { - Ok(true) - } - } - } - } - - /// Encode eof. Return `EOF` state of encoder - #[inline] - pub fn encode_eof(&mut self) -> bool { - match self.kind { - TransferEncodingKind::Eof => true, - TransferEncodingKind::Length(rem) => rem == 0, - TransferEncodingKind::Chunked(ref mut eof) => { - if !*eof { - *eof = true; - self.buf.as_mut().unwrap().extend_from_slice(b"0\r\n\r\n"); - } - true - } - } - } -} - -impl io::Write for TransferEncoding { - #[inline] - fn write(&mut self, buf: &[u8]) -> io::Result { - if self.buf.is_some() { - self.encode(buf)?; - } - Ok(buf.len()) - } - - #[inline] - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } -} - -struct AcceptEncoding { - encoding: ContentEncoding, - quality: f64, -} - -impl Eq for AcceptEncoding {} - -impl Ord for AcceptEncoding { - fn cmp(&self, other: &AcceptEncoding) -> cmp::Ordering { - if self.quality > other.quality { - cmp::Ordering::Less - } else if self.quality < other.quality { - cmp::Ordering::Greater - } else { - cmp::Ordering::Equal - } - } -} - -impl PartialOrd for AcceptEncoding { - fn partial_cmp(&self, other: &AcceptEncoding) -> Option { - Some(self.cmp(other)) - } -} - -impl PartialEq for AcceptEncoding { - fn eq(&self, other: &AcceptEncoding) -> bool { - self.quality == other.quality - } -} - -impl AcceptEncoding { - fn new(tag: &str) -> Option { - let parts: Vec<&str> = tag.split(';').collect(); - let encoding = match parts.len() { - 0 => return None, - _ => ContentEncoding::from(parts[0]), - }; - let quality = match parts.len() { - 1 => encoding.quality(), - _ => match f64::from_str(parts[1]) { - Ok(q) => q, - Err(_) => 0.0, - }, - }; - Some(AcceptEncoding { encoding, quality }) - } - - /// Parse a raw Accept-Encoding header value into an ordered list. - pub fn parse(raw: &str) -> ContentEncoding { - let mut encodings: Vec<_> = raw - .replace(' ', "") - .split(',') - .map(|l| AcceptEncoding::new(l)) - .collect(); - encodings.sort(); - - for enc in encodings { - if let Some(enc) = enc { - return enc.encoding; - } - } - ContentEncoding::Identity - } -} - -#[cfg(test)] -mod tests { - use super::*; - use bytes::Bytes; - - #[test] - fn test_chunked_te() { - let bytes = BytesMut::new(); - let mut enc = TransferEncoding::chunked(bytes); - { - assert!(!enc.encode(b"test").ok().unwrap()); - assert!(enc.encode(b"").ok().unwrap()); - } - assert_eq!( - enc.buf_mut().take().freeze(), - Bytes::from_static(b"4\r\ntest\r\n0\r\n\r\n") - ); - } -} diff --git a/src/server/service.rs b/src/server/service.rs deleted file mode 100644 index e3402e30..00000000 --- a/src/server/service.rs +++ /dev/null @@ -1,272 +0,0 @@ -use std::marker::PhantomData; -use std::time::Duration; - -use actix_net::service::{NewService, Service}; -use futures::future::{ok, FutureResult}; -use futures::{Async, Poll}; - -use super::channel::{H1Channel, HttpChannel}; -use super::error::HttpDispatchError; -use super::handler::HttpHandler; -use super::settings::ServiceConfig; -use super::IoStream; - -/// `NewService` implementation for HTTP1/HTTP2 transports -pub struct HttpService -where - H: HttpHandler, - Io: IoStream, -{ - settings: ServiceConfig, - _t: PhantomData, -} - -impl HttpService -where - H: HttpHandler, - Io: IoStream, -{ - /// Create new `HttpService` instance. - pub fn new(settings: ServiceConfig) -> Self { - HttpService { - settings, - _t: PhantomData, - } - } -} - -impl NewService for HttpService -where - H: HttpHandler, - Io: IoStream, -{ - type Request = Io; - type Response = (); - type Error = HttpDispatchError; - type InitError = (); - type Service = HttpServiceHandler; - type Future = FutureResult; - - fn new_service(&self) -> Self::Future { - ok(HttpServiceHandler::new(self.settings.clone())) - } -} - -pub struct HttpServiceHandler -where - H: HttpHandler, - Io: IoStream, -{ - settings: ServiceConfig, - _t: PhantomData, -} - -impl HttpServiceHandler -where - H: HttpHandler, - Io: IoStream, -{ - fn new(settings: ServiceConfig) -> HttpServiceHandler { - HttpServiceHandler { - settings, - _t: PhantomData, - } - } -} - -impl Service for HttpServiceHandler -where - H: HttpHandler, - Io: IoStream, -{ - type Request = Io; - type Response = (); - type Error = HttpDispatchError; - type Future = HttpChannel; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - HttpChannel::new(self.settings.clone(), req) - } -} - -/// `NewService` implementation for HTTP1 transport -pub struct H1Service -where - H: HttpHandler, - Io: IoStream, -{ - settings: ServiceConfig, - _t: PhantomData, -} - -impl H1Service -where - H: HttpHandler, - Io: IoStream, -{ - /// Create new `HttpService` instance. - pub fn new(settings: ServiceConfig) -> Self { - H1Service { - settings, - _t: PhantomData, - } - } -} - -impl NewService for H1Service -where - H: HttpHandler, - Io: IoStream, -{ - type Request = Io; - type Response = (); - type Error = HttpDispatchError; - type InitError = (); - type Service = H1ServiceHandler; - type Future = FutureResult; - - fn new_service(&self) -> Self::Future { - ok(H1ServiceHandler::new(self.settings.clone())) - } -} - -/// `Service` implementation for HTTP1 transport -pub struct H1ServiceHandler -where - H: HttpHandler, - Io: IoStream, -{ - settings: ServiceConfig, - _t: PhantomData, -} - -impl H1ServiceHandler -where - H: HttpHandler, - Io: IoStream, -{ - fn new(settings: ServiceConfig) -> H1ServiceHandler { - H1ServiceHandler { - settings, - _t: PhantomData, - } - } -} - -impl Service for H1ServiceHandler -where - H: HttpHandler, - Io: IoStream, -{ - type Request = Io; - type Response = (); - type Error = HttpDispatchError; - type Future = H1Channel; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - H1Channel::new(self.settings.clone(), req) - } -} - -/// `NewService` implementation for stream configuration service -/// -/// Stream configuration service allows to change some socket level -/// parameters. for example `tcp nodelay` or `tcp keep-alive`. -pub struct StreamConfiguration { - no_delay: Option, - tcp_ka: Option>, - _t: PhantomData<(T, E)>, -} - -impl Default for StreamConfiguration { - fn default() -> Self { - Self::new() - } -} - -impl StreamConfiguration { - /// Create new `StreamConfigurationService` instance. - pub fn new() -> Self { - Self { - no_delay: None, - tcp_ka: None, - _t: PhantomData, - } - } - - /// Sets the value of the `TCP_NODELAY` option on this socket. - pub fn nodelay(mut self, nodelay: bool) -> Self { - self.no_delay = Some(nodelay); - self - } - - /// Sets whether keepalive messages are enabled to be sent on this socket. - pub fn tcp_keepalive(mut self, keepalive: Option) -> Self { - self.tcp_ka = Some(keepalive); - self - } -} - -impl NewService for StreamConfiguration { - type Request = T; - type Response = T; - type Error = E; - type InitError = (); - type Service = StreamConfigurationService; - type Future = FutureResult; - - fn new_service(&self) -> Self::Future { - ok(StreamConfigurationService { - no_delay: self.no_delay, - tcp_ka: self.tcp_ka, - _t: PhantomData, - }) - } -} - -/// Stream configuration service -/// -/// Stream configuration service allows to change some socket level -/// parameters. for example `tcp nodelay` or `tcp keep-alive`. -pub struct StreamConfigurationService { - no_delay: Option, - tcp_ka: Option>, - _t: PhantomData<(T, E)>, -} - -impl Service for StreamConfigurationService -where - T: IoStream, -{ - type Request = T; - type Response = T; - type Error = E; - type Future = FutureResult; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, mut req: Self::Request) -> Self::Future { - if let Some(no_delay) = self.no_delay { - if req.set_nodelay(no_delay).is_err() { - error!("Can not set socket no-delay option"); - } - } - if let Some(keepalive) = self.tcp_ka { - if req.set_keepalive(keepalive).is_err() { - error!("Can not set socket keep-alive option"); - } - } - - ok(req) - } -} diff --git a/src/server/settings.rs b/src/server/settings.rs deleted file mode 100644 index 66a4eed8..00000000 --- a/src/server/settings.rs +++ /dev/null @@ -1,503 +0,0 @@ -use std::cell::{Cell, RefCell}; -use std::collections::VecDeque; -use std::fmt::Write; -use std::rc::Rc; -use std::time::{Duration, Instant}; -use std::{env, fmt, net}; - -use bytes::BytesMut; -use futures::{future, Future}; -use futures_cpupool::CpuPool; -use http::StatusCode; -use lazycell::LazyCell; -use parking_lot::Mutex; -use time; -use tokio_current_thread::spawn; -use tokio_timer::{sleep, Delay}; - -use super::message::{Request, RequestPool}; -use super::KeepAlive; -use body::Body; -use httpresponse::{HttpResponse, HttpResponseBuilder, HttpResponsePool}; - -/// Env variable for default cpu pool size -const ENV_CPU_POOL_VAR: &str = "ACTIX_CPU_POOL"; - -lazy_static! { - pub(crate) static ref DEFAULT_CPUPOOL: Mutex = { - let default = match env::var(ENV_CPU_POOL_VAR) { - Ok(val) => { - if let Ok(val) = val.parse() { - val - } else { - error!("Can not parse ACTIX_CPU_POOL value"); - 20 - } - } - Err(_) => 20, - }; - Mutex::new(CpuPool::new(default)) - }; -} - -/// Various server settings -pub struct ServerSettings { - addr: net::SocketAddr, - secure: bool, - host: String, - cpu_pool: LazyCell, - responses: &'static HttpResponsePool, -} - -impl Clone for ServerSettings { - fn clone(&self) -> Self { - ServerSettings { - addr: self.addr, - secure: self.secure, - host: self.host.clone(), - cpu_pool: LazyCell::new(), - responses: HttpResponsePool::get_pool(), - } - } -} - -impl Default for ServerSettings { - fn default() -> Self { - ServerSettings { - addr: "127.0.0.1:8080".parse().unwrap(), - secure: false, - host: "localhost:8080".to_owned(), - responses: HttpResponsePool::get_pool(), - cpu_pool: LazyCell::new(), - } - } -} - -impl ServerSettings { - /// Crate server settings instance - pub(crate) fn new( - addr: net::SocketAddr, host: &str, secure: bool, - ) -> ServerSettings { - let host = host.to_owned(); - let cpu_pool = LazyCell::new(); - let responses = HttpResponsePool::get_pool(); - ServerSettings { - addr, - secure, - host, - cpu_pool, - responses, - } - } - - /// Returns the socket address of the local half of this TCP connection - pub fn local_addr(&self) -> net::SocketAddr { - self.addr - } - - /// Returns true if connection is secure(https) - pub fn secure(&self) -> bool { - self.secure - } - - /// Returns host header value - pub fn host(&self) -> &str { - &self.host - } - - /// Returns default `CpuPool` for server - pub fn cpu_pool(&self) -> &CpuPool { - self.cpu_pool.borrow_with(|| DEFAULT_CPUPOOL.lock().clone()) - } - - #[inline] - pub(crate) fn get_response(&self, status: StatusCode, body: Body) -> HttpResponse { - HttpResponsePool::get_response(&self.responses, status, body) - } - - #[inline] - pub(crate) fn get_response_builder( - &self, status: StatusCode, - ) -> HttpResponseBuilder { - HttpResponsePool::get_builder(&self.responses, status) - } -} - -// "Sun, 06 Nov 1994 08:49:37 GMT".len() -const DATE_VALUE_LENGTH: usize = 29; - -/// Http service configuration -pub struct ServiceConfig(Rc>); - -struct Inner { - handler: H, - keep_alive: Option, - client_timeout: u64, - client_shutdown: u64, - ka_enabled: bool, - bytes: Rc, - messages: &'static RequestPool, - date: Cell>, -} - -impl Clone for ServiceConfig { - fn clone(&self) -> Self { - ServiceConfig(self.0.clone()) - } -} - -impl ServiceConfig { - /// Create instance of `ServiceConfig` - pub(crate) fn new( - handler: H, keep_alive: KeepAlive, client_timeout: u64, client_shutdown: u64, - settings: ServerSettings, - ) -> ServiceConfig { - let (keep_alive, ka_enabled) = match keep_alive { - KeepAlive::Timeout(val) => (val as u64, true), - KeepAlive::Os | KeepAlive::Tcp(_) => (0, true), - KeepAlive::Disabled => (0, false), - }; - let keep_alive = if ka_enabled && keep_alive > 0 { - Some(Duration::from_secs(keep_alive)) - } else { - None - }; - - ServiceConfig(Rc::new(Inner { - handler, - keep_alive, - ka_enabled, - client_timeout, - client_shutdown, - bytes: Rc::new(SharedBytesPool::new()), - messages: RequestPool::pool(settings), - date: Cell::new(None), - })) - } - - /// Create worker settings builder. - pub fn build(handler: H) -> ServiceConfigBuilder { - ServiceConfigBuilder::new(handler) - } - - pub(crate) fn handler(&self) -> &H { - &self.0.handler - } - - #[inline] - /// Keep alive duration if configured. - pub fn keep_alive(&self) -> Option { - self.0.keep_alive - } - - #[inline] - /// Return state of connection keep-alive funcitonality - pub fn keep_alive_enabled(&self) -> bool { - self.0.ka_enabled - } - - pub(crate) fn get_bytes(&self) -> BytesMut { - self.0.bytes.get_bytes() - } - - pub(crate) fn release_bytes(&self, bytes: BytesMut) { - self.0.bytes.release_bytes(bytes) - } - - pub(crate) fn get_request(&self) -> Request { - RequestPool::get(self.0.messages) - } -} - -impl ServiceConfig { - #[inline] - /// Client timeout for first request. - pub fn client_timer(&self) -> Option { - let delay = self.0.client_timeout; - if delay != 0 { - Some(Delay::new(self.now() + Duration::from_millis(delay))) - } else { - None - } - } - - /// Client timeout for first request. - pub fn client_timer_expire(&self) -> Option { - let delay = self.0.client_timeout; - if delay != 0 { - Some(self.now() + Duration::from_millis(delay)) - } else { - None - } - } - - /// Client shutdown timer - pub fn client_shutdown_timer(&self) -> Option { - let delay = self.0.client_shutdown; - if delay != 0 { - Some(self.now() + Duration::from_millis(delay)) - } else { - None - } - } - - #[inline] - /// Return keep-alive timer delay is configured. - pub fn keep_alive_timer(&self) -> Option { - if let Some(ka) = self.0.keep_alive { - Some(Delay::new(self.now() + ka)) - } else { - None - } - } - - /// Keep-alive expire time - pub fn keep_alive_expire(&self) -> Option { - if let Some(ka) = self.0.keep_alive { - Some(self.now() + ka) - } else { - None - } - } - - fn check_date(&self) { - if unsafe { &*self.0.date.as_ptr() }.is_none() { - self.0.date.set(Some(Date::new())); - - // periodic date update - let s = self.clone(); - spawn(sleep(Duration::from_millis(500)).then(move |_| { - s.0.date.set(None); - future::ok(()) - })); - } - } - - pub(crate) fn set_date(&self, dst: &mut BytesMut, full: bool) { - self.check_date(); - - let date = &unsafe { &*self.0.date.as_ptr() }.as_ref().unwrap().bytes; - - if full { - let mut buf: [u8; 39] = [0; 39]; - buf[..6].copy_from_slice(b"date: "); - buf[6..35].copy_from_slice(date); - buf[35..].copy_from_slice(b"\r\n\r\n"); - dst.extend_from_slice(&buf); - } else { - dst.extend_from_slice(date); - } - } - - #[inline] - pub(crate) fn now(&self) -> Instant { - self.check_date(); - unsafe { &*self.0.date.as_ptr() }.as_ref().unwrap().current - } -} - -/// A service config builder -/// -/// This type can be used to construct an instance of `ServiceConfig` through a -/// builder-like pattern. -pub struct ServiceConfigBuilder { - handler: H, - keep_alive: KeepAlive, - client_timeout: u64, - client_shutdown: u64, - host: String, - addr: net::SocketAddr, - secure: bool, -} - -impl ServiceConfigBuilder { - /// Create instance of `ServiceConfigBuilder` - pub fn new(handler: H) -> ServiceConfigBuilder { - ServiceConfigBuilder { - handler, - keep_alive: KeepAlive::Timeout(5), - client_timeout: 5000, - client_shutdown: 5000, - secure: false, - host: "localhost".to_owned(), - addr: "127.0.0.1:8080".parse().unwrap(), - } - } - - /// Enable secure flag for current server. - /// - /// By default this flag is set to false. - pub fn secure(mut self) -> Self { - self.secure = true; - self - } - - /// Set server keep-alive setting. - /// - /// By default keep alive is set to a 5 seconds. - pub fn keep_alive>(mut self, val: T) -> Self { - self.keep_alive = val.into(); - self - } - - /// Set server client timeout in milliseconds for first request. - /// - /// Defines a timeout for reading client request header. If a client does not transmit - /// the entire set headers within this time, the request is terminated with - /// the 408 (Request Time-out) error. - /// - /// To disable timeout set value to 0. - /// - /// By default client timeout is set to 5000 milliseconds. - pub fn client_timeout(mut self, val: u64) -> Self { - self.client_timeout = val; - self - } - - /// Set server connection shutdown timeout in milliseconds. - /// - /// Defines a timeout for shutdown connection. If a shutdown procedure does not complete - /// within this time, the request is dropped. This timeout affects only secure connections. - /// - /// To disable timeout set value to 0. - /// - /// By default client timeout is set to 5000 milliseconds. - pub fn client_shutdown(mut self, val: u64) -> Self { - self.client_shutdown = val; - self - } - - /// Set server host name. - /// - /// Host name is used by application router aa a hostname for url - /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. - /// html#method.host) documentation for more information. - /// - /// By default host name is set to a "localhost" value. - pub fn server_hostname(mut self, val: &str) -> Self { - self.host = val.to_owned(); - self - } - - /// Set server ip address. - /// - /// Host name is used by application router aa a hostname for url - /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. - /// html#method.host) documentation for more information. - /// - /// By default server address is set to a "127.0.0.1:8080" - pub fn server_address(mut self, addr: S) -> Self { - match addr.to_socket_addrs() { - Err(err) => error!("Can not convert to SocketAddr: {}", err), - Ok(mut addrs) => if let Some(addr) = addrs.next() { - self.addr = addr; - }, - } - self - } - - /// Finish service configuration and create `ServiceConfig` object. - pub fn finish(self) -> ServiceConfig { - let settings = ServerSettings::new(self.addr, &self.host, self.secure); - let client_shutdown = if self.secure { self.client_shutdown } else { 0 }; - - ServiceConfig::new( - self.handler, - self.keep_alive, - self.client_timeout, - client_shutdown, - settings, - ) - } -} - -#[derive(Copy, Clone)] -struct Date { - current: Instant, - bytes: [u8; DATE_VALUE_LENGTH], - pos: usize, -} - -impl Date { - fn new() -> Date { - let mut date = Date { - current: Instant::now(), - bytes: [0; DATE_VALUE_LENGTH], - pos: 0, - }; - date.update(); - date - } - fn update(&mut self) { - self.pos = 0; - self.current = Instant::now(); - write!(self, "{}", time::at_utc(time::get_time()).rfc822()).unwrap(); - } -} - -impl fmt::Write for Date { - fn write_str(&mut self, s: &str) -> fmt::Result { - let len = s.len(); - self.bytes[self.pos..self.pos + len].copy_from_slice(s.as_bytes()); - self.pos += len; - Ok(()) - } -} - -#[derive(Debug)] -pub(crate) struct SharedBytesPool(RefCell>); - -impl SharedBytesPool { - pub fn new() -> SharedBytesPool { - SharedBytesPool(RefCell::new(VecDeque::with_capacity(128))) - } - - pub fn get_bytes(&self) -> BytesMut { - if let Some(bytes) = self.0.borrow_mut().pop_front() { - bytes - } else { - BytesMut::new() - } - } - - pub fn release_bytes(&self, mut bytes: BytesMut) { - let v = &mut self.0.borrow_mut(); - if v.len() < 128 { - bytes.clear(); - v.push_front(bytes); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use futures::future; - use tokio::runtime::current_thread; - - #[test] - fn test_date_len() { - assert_eq!(DATE_VALUE_LENGTH, "Sun, 06 Nov 1994 08:49:37 GMT".len()); - } - - #[test] - fn test_date() { - let mut rt = current_thread::Runtime::new().unwrap(); - - let _ = rt.block_on(future::lazy(|| { - let settings = ServiceConfig::<()>::new( - (), - KeepAlive::Os, - 0, - 0, - ServerSettings::default(), - ); - let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10); - settings.set_date(&mut buf1, true); - let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10); - settings.set_date(&mut buf2, true); - assert_eq!(buf1, buf2); - future::ok::<_, ()>(()) - })); - } -} diff --git a/src/server/ssl/mod.rs b/src/server/ssl/mod.rs deleted file mode 100644 index c09573fe..00000000 --- a/src/server/ssl/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[cfg(any(feature = "alpn", feature = "ssl"))] -mod openssl; -#[cfg(any(feature = "alpn", feature = "ssl"))] -pub use self::openssl::{openssl_acceptor_with_flags, OpensslAcceptor}; - -#[cfg(feature = "tls")] -mod nativetls; - -#[cfg(feature = "rust-tls")] -mod rustls; -#[cfg(feature = "rust-tls")] -pub use self::rustls::RustlsAcceptor; diff --git a/src/server/ssl/nativetls.rs b/src/server/ssl/nativetls.rs deleted file mode 100644 index a9797ffb..00000000 --- a/src/server/ssl/nativetls.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::net::{Shutdown, SocketAddr}; -use std::{io, time}; - -use actix_net::ssl::TlsStream; - -use server::IoStream; - -impl IoStream for TlsStream { - #[inline] - fn shutdown(&mut self, _how: Shutdown) -> io::Result<()> { - let _ = self.get_mut().shutdown(); - Ok(()) - } - - #[inline] - fn peer_addr(&self) -> Option { - self.get_ref().get_ref().peer_addr() - } - - #[inline] - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { - self.get_mut().get_mut().set_nodelay(nodelay) - } - - #[inline] - fn set_linger(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().get_mut().set_linger(dur) - } - - #[inline] - fn set_keepalive(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().get_mut().set_keepalive(dur) - } -} diff --git a/src/server/ssl/openssl.rs b/src/server/ssl/openssl.rs deleted file mode 100644 index 9d370f8b..00000000 --- a/src/server/ssl/openssl.rs +++ /dev/null @@ -1,87 +0,0 @@ -use std::net::{Shutdown, SocketAddr}; -use std::{io, time}; - -use actix_net::ssl; -use openssl::ssl::{AlpnError, SslAcceptor, SslAcceptorBuilder}; -use tokio_io::{AsyncRead, AsyncWrite}; -use tokio_openssl::SslStream; - -use server::{IoStream, ServerFlags}; - -/// Support `SSL` connections via openssl package -/// -/// `ssl` feature enables `OpensslAcceptor` type -pub struct OpensslAcceptor { - _t: ssl::OpensslAcceptor, -} - -impl OpensslAcceptor { - /// Create `OpensslAcceptor` with enabled `HTTP/2` and `HTTP1.1` support. - pub fn new(builder: SslAcceptorBuilder) -> io::Result> { - OpensslAcceptor::with_flags(builder, ServerFlags::HTTP1 | ServerFlags::HTTP2) - } - - /// Create `OpensslAcceptor` with custom server flags. - pub fn with_flags( - builder: SslAcceptorBuilder, flags: ServerFlags, - ) -> io::Result> { - let acceptor = openssl_acceptor_with_flags(builder, flags)?; - - Ok(ssl::OpensslAcceptor::new(acceptor)) - } -} - -/// Configure `SslAcceptorBuilder` with custom server flags. -pub fn openssl_acceptor_with_flags( - mut builder: SslAcceptorBuilder, flags: ServerFlags, -) -> io::Result { - let mut protos = Vec::new(); - if flags.contains(ServerFlags::HTTP1) { - protos.extend(b"\x08http/1.1"); - } - if flags.contains(ServerFlags::HTTP2) { - protos.extend(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(AlpnError::NOACK) - } - }); - } - - if !protos.is_empty() { - builder.set_alpn_protos(&protos)?; - } - - Ok(builder.build()) -} - -impl IoStream for SslStream { - #[inline] - fn shutdown(&mut self, _how: Shutdown) -> io::Result<()> { - let _ = self.get_mut().shutdown(); - Ok(()) - } - - #[inline] - fn peer_addr(&self) -> Option { - self.get_ref().get_ref().peer_addr() - } - - #[inline] - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { - self.get_mut().get_mut().set_nodelay(nodelay) - } - - #[inline] - fn set_linger(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().get_mut().set_linger(dur) - } - - #[inline] - fn set_keepalive(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().get_mut().set_keepalive(dur) - } -} diff --git a/src/server/ssl/rustls.rs b/src/server/ssl/rustls.rs deleted file mode 100644 index a53a53a9..00000000 --- a/src/server/ssl/rustls.rs +++ /dev/null @@ -1,87 +0,0 @@ -use std::net::{Shutdown, SocketAddr}; -use std::{io, time}; - -use actix_net::ssl; //::RustlsAcceptor; -use rustls::{ClientSession, ServerConfig, ServerSession}; -use tokio_io::{AsyncRead, AsyncWrite}; -use tokio_rustls::TlsStream; - -use server::{IoStream, ServerFlags}; - -/// Support `SSL` connections via rustls package -/// -/// `rust-tls` feature enables `RustlsAcceptor` type -pub struct RustlsAcceptor { - _t: ssl::RustlsAcceptor, -} - -impl RustlsAcceptor { - /// Create `RustlsAcceptor` with custom server flags. - pub fn with_flags( - mut config: ServerConfig, flags: ServerFlags, - ) -> ssl::RustlsAcceptor { - let mut protos = Vec::new(); - if flags.contains(ServerFlags::HTTP2) { - protos.push("h2".to_string()); - } - if flags.contains(ServerFlags::HTTP1) { - protos.push("http/1.1".to_string()); - } - if !protos.is_empty() { - config.set_protocols(&protos); - } - - ssl::RustlsAcceptor::new(config) - } -} - -impl IoStream for TlsStream { - #[inline] - fn shutdown(&mut self, _how: Shutdown) -> io::Result<()> { - let _ = ::shutdown(self); - Ok(()) - } - - #[inline] - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { - self.get_mut().0.set_nodelay(nodelay) - } - - #[inline] - fn set_linger(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().0.set_linger(dur) - } - - #[inline] - fn set_keepalive(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().0.set_keepalive(dur) - } -} - -impl IoStream for TlsStream { - #[inline] - fn shutdown(&mut self, _how: Shutdown) -> io::Result<()> { - let _ = ::shutdown(self); - Ok(()) - } - - #[inline] - fn peer_addr(&self) -> Option { - self.get_ref().0.peer_addr() - } - - #[inline] - fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> { - self.get_mut().0.set_nodelay(nodelay) - } - - #[inline] - fn set_linger(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().0.set_linger(dur) - } - - #[inline] - fn set_keepalive(&mut self, dur: Option) -> io::Result<()> { - self.get_mut().0.set_keepalive(dur) - } -} diff --git a/src/service.rs b/src/service.rs new file mode 100644 index 00000000..775bb611 --- /dev/null +++ b/src/service.rs @@ -0,0 +1,155 @@ +use std::rc::Rc; + +use actix_http::body::{Body, MessageBody, ResponseBody}; +use actix_http::http::HeaderMap; +use actix_http::{ + Error, Extensions, HttpMessage, Payload, Request, Response, ResponseHead, +}; +use actix_router::{Path, Url}; + +use crate::request::HttpRequest; + +pub struct ServiceRequest

{ + req: HttpRequest, + payload: Payload

, +} + +impl

ServiceRequest

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

, + extensions: Rc, + ) -> Self { + let (head, payload) = request.into_parts(); + ServiceRequest { + payload, + req: HttpRequest::new(head, path, extensions), + } + } + + #[inline] + pub fn request(&self) -> &HttpRequest { + &self.req + } + + #[inline] + pub fn into_request(self) -> HttpRequest { + self.req + } + + /// Create service response + #[inline] + pub fn into_response(self, res: Response) -> ServiceResponse { + ServiceResponse::new(self.req, res) + } + + /// Create service response for error + #[inline] + pub fn error_response>(self, err: E) -> ServiceResponse { + ServiceResponse::new(self.req, err.into().into()) + } + + #[inline] + pub fn match_info_mut(&mut self) -> &mut Path { + &mut self.req.path + } +} + +impl

HttpMessage for ServiceRequest

{ + type Stream = P; + + #[inline] + fn headers(&self) -> &HeaderMap { + self.req.headers() + } + + #[inline] + fn take_payload(&mut self) -> Payload { + std::mem::replace(&mut self.payload, Payload::None) + } +} + +impl

std::ops::Deref for ServiceRequest

{ + type Target = HttpRequest; + + fn deref(&self) -> &HttpRequest { + self.request() + } +} + +pub struct ServiceResponse { + request: HttpRequest, + response: Response, +} + +impl ServiceResponse { + /// Create service response instance + pub fn new(request: HttpRequest, response: Response) -> Self { + ServiceResponse { request, response } + } + + /// Get reference to original request + #[inline] + pub fn request(&self) -> &HttpRequest { + &self.request + } + + /// Get reference to response + #[inline] + pub fn response(&self) -> &Response { + &self.response + } + + /// Get mutable reference to response + #[inline] + pub fn response_mut(&mut self) -> &mut Response { + &mut self.response + } + + /// Get the headers from the response + #[inline] + pub fn headers(&self) -> &HeaderMap { + self.response.headers() + } + + /// Get a mutable reference to the headers + #[inline] + pub fn headers_mut(&mut self) -> &mut HeaderMap { + self.response.headers_mut() + } +} + +impl ServiceResponse { + /// Set a new body + pub fn map_body(self, f: F) -> ServiceResponse + where + F: FnOnce(&mut ResponseHead, ResponseBody) -> ResponseBody, + { + let response = self.response.map_body(f); + + ServiceResponse { + response, + request: self.request, + } + } +} + +impl std::ops::Deref for ServiceResponse { + type Target = Response; + + fn deref(&self) -> &Response { + self.response() + } +} + +impl std::ops::DerefMut for ServiceResponse { + fn deref_mut(&mut self) -> &mut Response { + self.response_mut() + } +} + +impl Into> for ServiceResponse { + fn into(self) -> Response { + self.response + } +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 00000000..82aecc6e --- /dev/null +++ b/src/state.rs @@ -0,0 +1,120 @@ +use std::ops::Deref; +use std::rc::Rc; + +use actix_http::error::{Error, ErrorInternalServerError}; +use actix_http::Extensions; +use futures::future::{err, ok, FutureResult}; +use futures::{Async, Future, IntoFuture, Poll}; + +use crate::handler::FromRequest; +use crate::service::ServiceRequest; + +/// Application state factory +pub(crate) trait StateFactory { + fn construct(&self) -> Box; +} + +pub(crate) trait StateFactoryResult { + fn poll_result(&mut self, extensions: &mut Extensions) -> Poll<(), ()>; +} + +/// Application state +pub struct State(Rc); + +impl State { + pub fn new(state: S) -> State { + State(Rc::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()) + } +} + +impl FromRequest

for State { + type Error = Error; + type Future = FutureResult; + + #[inline] + fn from_request(req: &mut ServiceRequest

) -> Self::Future { + if let Some(st) = req.app_extensions().get::>() { + ok(st.clone()) + } else { + err(ErrorInternalServerError( + "State is not configured, use App::state()", + )) + } + } +} + +impl StateFactory for State { + fn construct(&self) -> Box { + Box::new(StateFut { st: self.clone() }) + } +} + +struct StateFut { + st: State, +} + +impl StateFactoryResult for StateFut { + fn poll_result(&mut self, extensions: &mut Extensions) -> Poll<(), ()> { + extensions.insert(self.st.clone()); + Ok(Async::Ready(())) + } +} + +impl StateFactory for F +where + F: Fn() -> Out + 'static, + Out: IntoFuture + 'static, + Out::Error: std::fmt::Debug, +{ + fn construct(&self) -> Box { + Box::new(StateFactoryFut { + fut: (*self)().into_future(), + }) + } +} + +struct StateFactoryFut +where + F: Future, + F::Error: std::fmt::Debug, +{ + fut: F, +} + +impl StateFactoryResult for StateFactoryFut +where + F: Future, + F::Error: std::fmt::Debug, +{ + fn poll_result(&mut self, extensions: &mut Extensions) -> Poll<(), ()> { + match self.fut.poll() { + Ok(Async::Ready(s)) => { + extensions.insert(State::new(s)); + Ok(Async::Ready(())) + } + Ok(Async::NotReady) => Ok(Async::NotReady), + Err(e) => { + log::error!("Can not construct application state: {:?}", e); + Err(()) + } + } + } +} diff --git a/src/test.rs b/src/test.rs index 1d86db9f..e13dcd16 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,449 +1,23 @@ //! Various helpers for Actix applications to use during testing. -use std::rc::Rc; use std::str::FromStr; -use std::sync::mpsc; -use std::{net, thread}; -use actix::{Actor, Addr, System}; -use actix::actors::signal; +use bytes::Bytes; +use futures::IntoFuture; +use tokio_current_thread::Runtime; -use actix_net::server::Server; -use cookie::Cookie; -use futures::Future; -use http::header::HeaderName; -use http::{HeaderMap, HttpTryFrom, Method, Uri, Version}; -use net2::TcpBuilder; -use tokio::runtime::current_thread::Runtime; +use actix_http::dev::Payload; +use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; +use actix_http::http::{HeaderMap, HttpTryFrom, Method, Uri, Version}; +use actix_http::Request as HttpRequest; +use actix_router::{Path, Url}; -#[cfg(any(feature = "alpn", feature = "ssl"))] -use openssl::ssl::SslAcceptorBuilder; -#[cfg(feature = "rust-tls")] -use rustls::ServerConfig; +use crate::app::State; +use crate::request::Request; +use crate::service::ServiceRequest; -use application::{App, HttpApplication}; -use body::Binary; -use client::{ClientConnector, ClientRequest, ClientRequestBuilder}; -use error::Error; -use handler::{AsyncResult, AsyncResultItem, Handler, Responder}; -use header::{Header, IntoHeaderValue}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::Middleware; -use param::Params; -use payload::Payload; -use resource::Resource; -use router::Router; -use server::message::{Request, RequestPool}; -use server::{HttpServer, IntoHttpHandler, ServerSettings}; -use uri::Url as InnerUrl; -use ws; - -/// The `TestServer` type. +/// Test `Request` builder /// -/// `TestServer` is very simple test server that simplify process of writing -/// integration tests cases for actix web applications. -/// -/// # Examples -/// -/// ```rust -/// # extern crate actix_web; -/// # use actix_web::*; -/// # -/// # fn my_handler(req: &HttpRequest) -> HttpResponse { -/// # HttpResponse::Ok().into() -/// # } -/// # -/// # fn main() { -/// use actix_web::test::TestServer; -/// -/// let mut srv = TestServer::new(|app| app.handler(my_handler)); -/// -/// let req = srv.get().finish().unwrap(); -/// let response = srv.execute(req.send()).unwrap(); -/// assert!(response.status().is_success()); -/// # } -/// ``` -pub struct TestServer { - addr: net::SocketAddr, - ssl: bool, - conn: Addr, - rt: Runtime, - backend: Addr, -} - -impl TestServer { - /// Start new test server - /// - /// This method accepts configuration method. You can add - /// middlewares or set handlers for test application. - pub fn new(config: F) -> Self - where - F: Clone + Send + 'static + Fn(&mut TestApp<()>), - { - TestServerBuilder::new(|| ()).start(config) - } - - /// Create test server builder - pub fn build() -> TestServerBuilder<(), impl Fn() -> () + Clone + Send + 'static> { - TestServerBuilder::new(|| ()) - } - - /// Create test server builder with specific state factory - /// - /// This method can be used for constructing application state. - /// Also it can be used for external dependency initialization, - /// like creating sync actors for diesel integration. - pub fn build_with_state(state: F) -> TestServerBuilder - where - F: Fn() -> S + Clone + Send + 'static, - S: 'static, - { - TestServerBuilder::new(state) - } - - /// Start new test server with application factory - pub fn with_factory(factory: F) -> Self - where - F: Fn() -> H + Send + Clone + 'static, - H: IntoHttpHandler + 'static, - { - let (tx, rx) = mpsc::channel(); - - // run server in separate thread - thread::spawn(move || { - let sys = System::new("actix-test-server"); - let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); - let local_addr = tcp.local_addr().unwrap(); - - let srv = HttpServer::new(factory) - .disable_signals() - .listen(tcp) - .keep_alive(5) - .start(); - - tx.send((System::current(), local_addr, TestServer::get_conn(), srv)) - .unwrap(); - sys.run(); - }); - - let (system, addr, conn, backend) = rx.recv().unwrap(); - System::set_current(system); - TestServer { - addr, - conn, - ssl: false, - rt: Runtime::new().unwrap(), - backend, - } - } - - fn get_conn() -> Addr { - #[cfg(any(feature = "alpn", feature = "ssl"))] - { - use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; - - let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); - builder.set_verify(SslVerifyMode::NONE); - ClientConnector::with_connector(builder.build()).start() - } - #[cfg(all( - feature = "rust-tls", - not(any(feature = "alpn", feature = "ssl")) - ))] - { - use rustls::ClientConfig; - use std::fs::File; - use std::io::BufReader; - let mut config = ClientConfig::new(); - let pem_file = &mut BufReader::new(File::open("tests/cert.pem").unwrap()); - config.root_store.add_pem_file(pem_file).unwrap(); - ClientConnector::with_connector(config).start() - } - #[cfg(not(any(feature = "alpn", feature = "ssl", feature = "rust-tls")))] - { - ClientConnector::default().start() - } - } - - /// Get firat available unused address - pub fn unused_addr() -> net::SocketAddr { - let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); - let socket = TcpBuilder::new_v4().unwrap(); - socket.bind(&addr).unwrap(); - socket.reuse_address(true).unwrap(); - let tcp = socket.to_tcp_listener().unwrap(); - tcp.local_addr().unwrap() - } - - /// Construct test server url - pub fn addr(&self) -> net::SocketAddr { - self.addr - } - - /// Construct test server url - pub fn url(&self, uri: &str) -> String { - if uri.starts_with('/') { - format!( - "{}://localhost:{}{}", - if self.ssl { "https" } else { "http" }, - self.addr.port(), - uri - ) - } else { - format!( - "{}://localhost:{}/{}", - if self.ssl { "https" } else { "http" }, - self.addr.port(), - uri - ) - } - } - - /// Stop http server - fn stop(&mut self) { - let _ = self.backend.send(signal::Signal(signal::SignalType::Term)).wait(); - System::current().stop(); - } - - /// Execute future on current core - pub fn execute(&mut self, fut: F) -> Result - where - F: Future, - { - self.rt.block_on(fut) - } - - /// Connect to websocket server at a given path - pub fn ws_at( - &mut self, path: &str, - ) -> Result<(ws::ClientReader, ws::ClientWriter), ws::ClientError> { - let url = self.url(path); - self.rt - .block_on(ws::Client::with_connector(url, self.conn.clone()).connect()) - } - - /// Connect to a websocket server - pub fn ws( - &mut self, - ) -> Result<(ws::ClientReader, ws::ClientWriter), ws::ClientError> { - self.ws_at("/") - } - - /// Create `GET` request - pub fn get(&self) -> ClientRequestBuilder { - ClientRequest::get(self.url("/").as_str()) - } - - /// Create `POST` request - pub fn post(&self) -> ClientRequestBuilder { - ClientRequest::post(self.url("/").as_str()) - } - - /// Create `HEAD` request - pub fn head(&self) -> ClientRequestBuilder { - ClientRequest::head(self.url("/").as_str()) - } - - /// Connect to test http server - pub fn client(&self, meth: Method, path: &str) -> ClientRequestBuilder { - ClientRequest::build() - .method(meth) - .uri(self.url(path).as_str()) - .with_connector(self.conn.clone()) - .take() - } -} - -impl Drop for TestServer { - fn drop(&mut self) { - self.stop() - } -} - -/// An `TestServer` builder -/// -/// This type can be used to construct an instance of `TestServer` through a -/// builder-like pattern. -pub struct TestServerBuilder -where - F: Fn() -> S + Send + Clone + 'static, -{ - state: F, - #[cfg(any(feature = "alpn", feature = "ssl"))] - ssl: Option, - #[cfg(feature = "rust-tls")] - rust_ssl: Option, -} - -impl TestServerBuilder -where - F: Fn() -> S + Send + Clone + 'static, -{ - /// Create a new test server - pub fn new(state: F) -> TestServerBuilder { - TestServerBuilder { - state, - #[cfg(any(feature = "alpn", feature = "ssl"))] - ssl: None, - #[cfg(feature = "rust-tls")] - rust_ssl: None, - } - } - - #[cfg(any(feature = "alpn", feature = "ssl"))] - /// Create ssl server - pub fn ssl(mut self, ssl: SslAcceptorBuilder) -> Self { - self.ssl = Some(ssl); - self - } - - #[cfg(feature = "rust-tls")] - /// Create rust tls server - pub fn rustls(mut self, ssl: ServerConfig) -> Self { - self.rust_ssl = Some(ssl); - self - } - - #[allow(unused_mut)] - /// Configure test application and run test server - pub fn start(mut self, config: C) -> TestServer - where - C: Fn(&mut TestApp) + Clone + Send + 'static, - { - let (tx, rx) = mpsc::channel(); - - let mut has_ssl = false; - - #[cfg(any(feature = "alpn", feature = "ssl"))] - { - has_ssl = has_ssl || self.ssl.is_some(); - } - - #[cfg(feature = "rust-tls")] - { - has_ssl = has_ssl || self.rust_ssl.is_some(); - } - - // run server in separate thread - thread::spawn(move || { - let addr = TestServer::unused_addr(); - - let sys = System::new("actix-test-server"); - let state = self.state; - let mut srv = HttpServer::new(move || { - let mut app = TestApp::new(state()); - config(&mut app); - app - }).workers(1) - .keep_alive(5) - .disable_signals(); - - - - #[cfg(any(feature = "alpn", feature = "ssl"))] - { - let ssl = self.ssl.take(); - if let Some(ssl) = ssl { - let tcp = net::TcpListener::bind(addr).unwrap(); - srv = srv.listen_ssl(tcp, ssl).unwrap(); - } - } - #[cfg(feature = "rust-tls")] - { - let ssl = self.rust_ssl.take(); - if let Some(ssl) = ssl { - let tcp = net::TcpListener::bind(addr).unwrap(); - srv = srv.listen_rustls(tcp, ssl); - } - } - if !has_ssl { - let tcp = net::TcpListener::bind(addr).unwrap(); - srv = srv.listen(tcp); - } - let backend = srv.start(); - - tx.send((System::current(), addr, TestServer::get_conn(), backend)) - .unwrap(); - - sys.run(); - }); - - let (system, addr, conn, backend) = rx.recv().unwrap(); - System::set_current(system); - TestServer { - addr, - conn, - ssl: has_ssl, - rt: Runtime::new().unwrap(), - backend, - } - } -} - -/// Test application helper for testing request handlers. -pub struct TestApp { - app: Option>, -} - -impl TestApp { - fn new(state: S) -> TestApp { - let app = App::with_state(state); - TestApp { app: Some(app) } - } - - /// Register handler for "/" - pub fn handler(&mut self, handler: F) - where - F: Fn(&HttpRequest) -> R + 'static, - R: Responder + 'static, - { - self.app = Some(self.app.take().unwrap().resource("/", |r| r.f(handler))); - } - - /// Register middleware - pub fn middleware(&mut self, mw: T) -> &mut TestApp - where - T: Middleware + 'static, - { - self.app = Some(self.app.take().unwrap().middleware(mw)); - self - } - - /// Register resource. This method is similar - /// to `App::resource()` method. - pub fn resource(&mut self, path: &str, f: F) -> &mut TestApp - where - F: FnOnce(&mut Resource) -> R + 'static, - { - self.app = Some(self.app.take().unwrap().resource(path, f)); - self - } -} - -impl IntoHttpHandler for TestApp { - type Handler = HttpApplication; - - fn into_handler(mut self) -> HttpApplication { - self.app.take().unwrap().into_handler() - } -} - -#[doc(hidden)] -impl Iterator for TestApp { - type Item = HttpApplication; - - fn next(&mut self) -> Option { - if let Some(mut app) = self.app.take() { - Some(app.finish()) - } else { - None - } - } -} - -/// Test `HttpRequest` builder -/// -/// ```rust +/// ```rust,ignore /// # extern crate http; /// # extern crate actix_web; /// # use http::{header, StatusCode}; @@ -474,10 +48,8 @@ pub struct TestRequest { method: Method, uri: Uri, headers: HeaderMap, - params: Params, - cookies: Option>>, + params: Path, payload: Option, - prefix: u16, } impl Default for TestRequest<()> { @@ -488,10 +60,8 @@ impl Default for TestRequest<()> { uri: Uri::from_str("/").unwrap(), version: Version::HTTP_11, headers: HeaderMap::new(), - params: Params::new(), - cookies: None, + params: Path::new(Url::default()), payload: None, - prefix: 0, } } } @@ -515,11 +85,6 @@ impl TestRequest<()> { { TestRequest::default().header(key, value) } - - /// Create TestRequest and set request cookie - pub fn with_cookie(cookie: Cookie<'static>) -> TestRequest<()> { - TestRequest::default().cookie(cookie) - } } impl TestRequest { @@ -531,10 +96,8 @@ impl TestRequest { uri: Uri::from_str("/").unwrap(), version: Version::HTTP_11, headers: HeaderMap::new(), - params: Params::new(), - cookies: None, + params: Path::new(Url::default()), payload: None, - prefix: 0, } } @@ -556,25 +119,6 @@ impl TestRequest { self } - /// set cookie of this request - pub fn cookie(mut self, cookie: Cookie<'static>) -> Self { - if self.cookies.is_some() { - let mut should_insert = true; - let old_cookies = self.cookies.as_mut().unwrap(); - for old_cookie in old_cookies.iter() { - if old_cookie == &cookie { - should_insert = false - }; - }; - if should_insert { - old_cookies.push(cookie); - }; - } else { - self.cookies = Some(vec![cookie]); - }; - self - } - /// Set a header pub fn set(mut self, hdr: H) -> Self { if let Ok(value) = hdr.try_into() { @@ -606,22 +150,15 @@ impl TestRequest { } /// Set request payload - pub fn set_payload>(mut self, data: B) -> Self { - let mut data = data.into(); + pub fn set_payload>(mut self, data: B) -> Self { let mut payload = Payload::empty(); - payload.unread_data(data.take()); + payload.unread_data(data.into()); self.payload = Some(payload); self } - /// Set request's prefix - pub fn prefix(mut self, prefix: u16) -> Self { - self.prefix = prefix; - self - } - /// Complete request creation and generate `HttpRequest` instance - pub fn finish(self) -> HttpRequest { + pub fn finish(self) -> ServiceRequest { let TestRequest { state, method, @@ -629,172 +166,31 @@ impl TestRequest { version, headers, mut params, - cookies, payload, - prefix, - } = self; - let router = Router::<()>::default(); - - let pool = RequestPool::pool(ServerSettings::default()); - let mut req = RequestPool::get(pool); - { - let inner = req.inner_mut(); - inner.method = method; - inner.url = InnerUrl::new(uri); - inner.version = version; - inner.headers = headers; - *inner.payload.borrow_mut() = payload; - } - params.set_url(req.url().clone()); - let mut info = router.route_info_params(0, params); - info.set_prefix(prefix); - - let mut req = HttpRequest::new(req, Rc::new(state), info); - req.set_cookies(cookies); - req - } - - #[cfg(test)] - /// Complete request creation and generate `HttpRequest` instance - pub(crate) fn finish_with_router(self, router: Router) -> HttpRequest { - let TestRequest { - state, - method, - uri, - version, - headers, - mut params, - cookies, - payload, - prefix, } = self; - let pool = RequestPool::pool(ServerSettings::default()); - let mut req = RequestPool::get(pool); + params.get_mut().update(&uri); + + let mut req = HttpRequest::new(); { let inner = req.inner_mut(); - inner.method = method; - inner.url = InnerUrl::new(uri); - inner.version = version; - inner.headers = headers; + inner.head.uri = uri; + inner.head.method = method; + inner.head.version = version; + inner.head.headers = headers; *inner.payload.borrow_mut() = payload; } - params.set_url(req.url().clone()); - let mut info = router.route_info_params(0, params); - info.set_prefix(prefix); - let mut req = HttpRequest::new(req, Rc::new(state), info); - req.set_cookies(cookies); - req - } - /// Complete request creation and generate server `Request` instance - pub fn request(self) -> Request { - let TestRequest { - method, - uri, - version, - headers, - payload, - .. - } = self; - - let pool = RequestPool::pool(ServerSettings::default()); - let mut req = RequestPool::get(pool); - { - let inner = req.inner_mut(); - inner.method = method; - inner.url = InnerUrl::new(uri); - inner.version = version; - inner.headers = headers; - *inner.payload.borrow_mut() = payload; - } - req - } - - /// This method generates `HttpRequest` instance and runs handler - /// with generated request. - pub fn run>(self, h: &H) -> Result { - let req = self.finish(); - let resp = h.handle(&req); - - match resp.respond_to(&req) { - Ok(resp) => match resp.into().into() { - AsyncResultItem::Ok(resp) => Ok(resp), - AsyncResultItem::Err(err) => Err(err), - AsyncResultItem::Future(fut) => { - let mut sys = System::new("test"); - sys.block_on(fut) - } - }, - Err(err) => Err(err.into()), - } - } - - /// This method generates `HttpRequest` instance and runs handler - /// with generated request. - /// - /// This method panics is handler returns actor. - pub fn run_async(self, h: H) -> Result - where - H: Fn(HttpRequest) -> F + 'static, - F: Future + 'static, - R: Responder + 'static, - E: Into + 'static, - { - let req = self.finish(); - let fut = h(req.clone()); - - let mut sys = System::new("test"); - match sys.block_on(fut) { - Ok(r) => match r.respond_to(&req) { - Ok(reply) => match reply.into().into() { - AsyncResultItem::Ok(resp) => Ok(resp), - _ => panic!("Nested async replies are not supported"), - }, - Err(e) => Err(e), - }, - Err(err) => Err(err), - } + Request::new(State::new(state), req, params) } /// This method generates `HttpRequest` instance and executes handler - pub fn run_async_result(self, f: F) -> Result + pub fn run_async(self, f: F) -> Result where - F: FnOnce(&HttpRequest) -> R, - R: Into>, + F: FnOnce(&Request) -> R, + R: IntoFuture, { - let req = self.finish(); - let res = f(&req); - - match res.into().into() { - AsyncResultItem::Ok(resp) => Ok(resp), - AsyncResultItem::Err(err) => Err(err), - AsyncResultItem::Future(fut) => { - let mut sys = System::new("test"); - sys.block_on(fut) - } - } - } - - /// This method generates `HttpRequest` instance and executes handler - pub fn execute(self, f: F) -> Result - where - F: FnOnce(&HttpRequest) -> R, - R: Responder + 'static, - { - let req = self.finish(); - let resp = f(&req); - - match resp.respond_to(&req) { - Ok(resp) => match resp.into().into() { - AsyncResultItem::Ok(resp) => Ok(resp), - AsyncResultItem::Err(err) => Err(err), - AsyncResultItem::Future(fut) => { - let mut sys = System::new("test"); - sys.block_on(fut) - } - }, - Err(err) => Err(err.into()), - } + let mut rt = Runtime::new().unwrap(); + rt.block_on(f(&self.finish()).into_future()) } } diff --git a/src/uri.rs b/src/uri.rs deleted file mode 100644 index c87cb3d5..00000000 --- a/src/uri.rs +++ /dev/null @@ -1,177 +0,0 @@ -use http::Uri; -use std::rc::Rc; - -// https://tools.ietf.org/html/rfc3986#section-2.2 -const RESERVED_PLUS_EXTRA: &[u8] = b":/?#[]@!$&'()*,+?;=%^ <>\"\\`{}|"; - -// https://tools.ietf.org/html/rfc3986#section-2.3 -const UNRESERVED: &[u8] = - b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-._~"; - -#[inline] -fn bit_at(array: &[u8], ch: u8) -> bool { - array[(ch >> 3) as usize] & (1 << (ch & 7)) != 0 -} - -#[inline] -fn set_bit(array: &mut [u8], ch: u8) { - array[(ch >> 3) as usize] |= 1 << (ch & 7) -} - -lazy_static! { - static ref UNRESERVED_QUOTER: Quoter = { Quoter::new(UNRESERVED) }; - pub(crate) static ref RESERVED_QUOTER: Quoter = { Quoter::new(RESERVED_PLUS_EXTRA) }; -} - -#[derive(Default, Clone, Debug)] -pub(crate) struct Url { - uri: Uri, - path: Option>, -} - -impl Url { - pub fn new(uri: Uri) -> Url { - let path = UNRESERVED_QUOTER.requote(uri.path().as_bytes()); - - Url { uri, path } - } - - pub fn uri(&self) -> &Uri { - &self.uri - } - - pub fn path(&self) -> &str { - if let Some(ref s) = self.path { - s - } else { - self.uri.path() - } - } -} - -pub(crate) struct Quoter { - safe_table: [u8; 16], -} - -impl Quoter { - pub fn new(safe: &[u8]) -> Quoter { - let mut q = Quoter { - safe_table: [0; 16], - }; - - // prepare safe table - for ch in safe { - set_bit(&mut q.safe_table, *ch) - } - - q - } - - pub fn requote(&self, val: &[u8]) -> Option> { - let mut has_pct = 0; - let mut pct = [b'%', 0, 0]; - let mut idx = 0; - let mut cloned: Option> = None; - - let len = val.len(); - while idx < len { - let ch = val[idx]; - - if has_pct != 0 { - pct[has_pct] = val[idx]; - has_pct += 1; - if has_pct == 3 { - has_pct = 0; - let buf = cloned.as_mut().unwrap(); - - if let Some(ch) = restore_ch(pct[1], pct[2]) { - if ch < 128 { - if bit_at(&self.safe_table, ch) { - buf.push(ch); - idx += 1; - continue; - } - - buf.extend_from_slice(&pct); - } else { - // Not ASCII, decode it - buf.push(ch); - } - } else { - buf.extend_from_slice(&pct[..]); - } - } - } else if ch == b'%' { - has_pct = 1; - if cloned.is_none() { - let mut c = Vec::with_capacity(len); - c.extend_from_slice(&val[..idx]); - cloned = Some(c); - } - } else if let Some(ref mut cloned) = cloned { - cloned.push(ch) - } - idx += 1; - } - - if let Some(data) = cloned { - // Unsafe: we get data from http::Uri, which does utf-8 checks already - // this code only decodes valid pct encoded values - Some(Rc::new(unsafe { String::from_utf8_unchecked(data) })) - } else { - None - } - } -} - -#[inline] -fn from_hex(v: u8) -> Option { - if v >= b'0' && v <= b'9' { - Some(v - 0x30) // ord('0') == 0x30 - } else if v >= b'A' && v <= b'F' { - Some(v - 0x41 + 10) // ord('A') == 0x41 - } else if v > b'a' && v <= b'f' { - Some(v - 0x61 + 10) // ord('a') == 0x61 - } else { - None - } -} - -#[inline] -fn restore_ch(d1: u8, d2: u8) -> Option { - from_hex(d1).and_then(|d1| from_hex(d2).and_then(move |d2| Some(d1 << 4 | d2))) -} - - -#[cfg(test)] -mod tests { - use std::rc::Rc; - - use super::*; - - #[test] - fn decode_path() { - assert_eq!(UNRESERVED_QUOTER.requote(b"https://localhost:80/foo"), None); - - assert_eq!( - Rc::try_unwrap(UNRESERVED_QUOTER.requote( - b"https://localhost:80/foo%25" - ).unwrap()).unwrap(), - "https://localhost:80/foo%25".to_string() - ); - - assert_eq!( - Rc::try_unwrap(UNRESERVED_QUOTER.requote( - b"http://cache-service/http%3A%2F%2Flocalhost%3A80%2Ffoo" - ).unwrap()).unwrap(), - "http://cache-service/http%3A%2F%2Flocalhost%3A80%2Ffoo".to_string() - ); - - assert_eq!( - Rc::try_unwrap(UNRESERVED_QUOTER.requote( - b"http://cache/http%3A%2F%2Flocal%3A80%2Ffile%2F%252Fvar%252Flog%0A" - ).unwrap()).unwrap(), - "http://cache/http%3A%2F%2Flocal%3A80%2Ffile%2F%252Fvar%252Flog%0A".to_string() - ); - } -} \ No newline at end of file diff --git a/src/with.rs b/src/with.rs deleted file mode 100644 index 140e086e..00000000 --- a/src/with.rs +++ /dev/null @@ -1,383 +0,0 @@ -use futures::{Async, Future, Poll}; -use std::marker::PhantomData; -use std::rc::Rc; - -use error::Error; -use handler::{AsyncResult, AsyncResultItem, FromRequest, Handler, Responder}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; - -trait FnWith: 'static { - fn call_with(self: &Self, T) -> R; -} - -impl R + 'static> FnWith for F { - fn call_with(self: &Self, arg: T) -> R { - (*self)(arg) - } -} - -#[doc(hidden)] -pub trait WithFactory: 'static -where - T: FromRequest, - R: Responder, -{ - fn create(self) -> With; - - fn create_with_config(self, T::Config) -> With; -} - -#[doc(hidden)] -pub trait WithAsyncFactory: 'static -where - T: FromRequest, - R: Future, - I: Responder, - E: Into, -{ - fn create(self) -> WithAsync; - - fn create_with_config(self, T::Config) -> WithAsync; -} - -#[doc(hidden)] -pub struct With -where - T: FromRequest, - S: 'static, -{ - hnd: Rc>, - cfg: Rc, - _s: PhantomData, -} - -impl With -where - T: FromRequest, - S: 'static, -{ - pub fn new R + 'static>(f: F, cfg: T::Config) -> Self { - With { - cfg: Rc::new(cfg), - hnd: Rc::new(f), - _s: PhantomData, - } - } -} - -impl Handler for With -where - R: Responder + 'static, - T: FromRequest + 'static, - S: 'static, -{ - type Result = AsyncResult; - - fn handle(&self, req: &HttpRequest) -> Self::Result { - let mut fut = WithHandlerFut { - req: req.clone(), - started: false, - hnd: Rc::clone(&self.hnd), - cfg: self.cfg.clone(), - fut1: None, - fut2: None, - }; - - match fut.poll() { - Ok(Async::Ready(resp)) => AsyncResult::ok(resp), - Ok(Async::NotReady) => AsyncResult::future(Box::new(fut)), - Err(e) => AsyncResult::err(e), - } - } -} - -struct WithHandlerFut -where - R: Responder, - T: FromRequest + 'static, - S: 'static, -{ - started: bool, - hnd: Rc>, - cfg: Rc, - req: HttpRequest, - fut1: Option>>, - fut2: Option>>, -} - -impl Future for WithHandlerFut -where - R: Responder + 'static, - T: FromRequest + 'static, - S: 'static, -{ - type Item = HttpResponse; - type Error = Error; - - fn poll(&mut self) -> Poll { - if let Some(ref mut fut) = self.fut2 { - return fut.poll(); - } - - let item = if !self.started { - self.started = true; - let reply = T::from_request(&self.req, self.cfg.as_ref()).into(); - match reply.into() { - AsyncResultItem::Err(err) => return Err(err), - AsyncResultItem::Ok(msg) => msg, - AsyncResultItem::Future(fut) => { - self.fut1 = Some(fut); - return self.poll(); - } - } - } else { - match self.fut1.as_mut().unwrap().poll()? { - Async::Ready(item) => item, - Async::NotReady => return Ok(Async::NotReady), - } - }; - - let item = match self.hnd.as_ref().call_with(item).respond_to(&self.req) { - Ok(item) => item.into(), - Err(e) => return Err(e.into()), - }; - - match item.into() { - AsyncResultItem::Err(err) => Err(err), - AsyncResultItem::Ok(resp) => Ok(Async::Ready(resp)), - AsyncResultItem::Future(fut) => { - self.fut2 = Some(fut); - self.poll() - } - } - } -} - -#[doc(hidden)] -pub struct WithAsync -where - R: Future, - I: Responder, - E: Into, - T: FromRequest, - S: 'static, -{ - hnd: Rc>, - cfg: Rc, - _s: PhantomData, -} - -impl WithAsync -where - R: Future, - I: Responder, - E: Into, - T: FromRequest, - S: 'static, -{ - pub fn new R + 'static>(f: F, cfg: T::Config) -> Self { - WithAsync { - cfg: Rc::new(cfg), - hnd: Rc::new(f), - _s: PhantomData, - } - } -} - -impl Handler for WithAsync -where - R: Future + 'static, - I: Responder + 'static, - E: Into + 'static, - T: FromRequest + 'static, - S: 'static, -{ - type Result = AsyncResult; - - fn handle(&self, req: &HttpRequest) -> Self::Result { - let mut fut = WithAsyncHandlerFut { - req: req.clone(), - started: false, - hnd: Rc::clone(&self.hnd), - cfg: Rc::clone(&self.cfg), - fut1: None, - fut2: None, - fut3: None, - }; - - match fut.poll() { - Ok(Async::Ready(resp)) => AsyncResult::ok(resp), - Ok(Async::NotReady) => AsyncResult::future(Box::new(fut)), - Err(e) => AsyncResult::err(e), - } - } -} - -struct WithAsyncHandlerFut -where - R: Future + 'static, - I: Responder + 'static, - E: Into + 'static, - T: FromRequest + 'static, - S: 'static, -{ - started: bool, - hnd: Rc>, - cfg: Rc, - req: HttpRequest, - fut1: Option>>, - fut2: Option, - fut3: Option>>, -} - -impl Future for WithAsyncHandlerFut -where - R: Future + 'static, - I: Responder + 'static, - E: Into + 'static, - T: FromRequest + 'static, - S: 'static, -{ - type Item = HttpResponse; - type Error = Error; - - fn poll(&mut self) -> Poll { - if let Some(ref mut fut) = self.fut3 { - return fut.poll(); - } - - if self.fut2.is_some() { - return match self.fut2.as_mut().unwrap().poll() { - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(r)) => match r.respond_to(&self.req) { - Ok(r) => match r.into().into() { - AsyncResultItem::Err(err) => Err(err), - AsyncResultItem::Ok(resp) => Ok(Async::Ready(resp)), - AsyncResultItem::Future(fut) => { - self.fut3 = Some(fut); - self.poll() - } - }, - Err(e) => Err(e.into()), - }, - Err(e) => Err(e.into()), - }; - } - - let item = if !self.started { - self.started = true; - let reply = T::from_request(&self.req, self.cfg.as_ref()).into(); - match reply.into() { - AsyncResultItem::Err(err) => return Err(err), - AsyncResultItem::Ok(msg) => msg, - AsyncResultItem::Future(fut) => { - self.fut1 = Some(fut); - return self.poll(); - } - } - } else { - match self.fut1.as_mut().unwrap().poll()? { - Async::Ready(item) => item, - Async::NotReady => return Ok(Async::NotReady), - } - }; - - self.fut2 = Some(self.hnd.as_ref().call_with(item)); - self.poll() - } -} - -macro_rules! with_factory_tuple ({$(($n:tt, $T:ident)),+} => { - impl<$($T,)+ State, Func, Res> WithFactory<($($T,)+), State, Res> for Func - where Func: Fn($($T,)+) -> Res + 'static, - $($T: FromRequest + 'static,)+ - Res: Responder + 'static, - State: 'static, - { - fn create(self) -> With<($($T,)+), State, Res> { - With::new(move |($($n,)+)| (self)($($n,)+), ($($T::Config::default(),)+)) - } - - fn create_with_config(self, cfg: ($($T::Config,)+)) -> With<($($T,)+), State, Res> { - With::new(move |($($n,)+)| (self)($($n,)+), cfg) - } - } -}); - -macro_rules! with_async_factory_tuple ({$(($n:tt, $T:ident)),+} => { - impl<$($T,)+ State, Func, Res, Item, Err> WithAsyncFactory<($($T,)+), State, Res, Item, Err> for Func - where Func: Fn($($T,)+) -> Res + 'static, - $($T: FromRequest + 'static,)+ - Res: Future, - Item: Responder + 'static, - Err: Into, - State: 'static, - { - fn create(self) -> WithAsync<($($T,)+), State, Res, Item, Err> { - WithAsync::new(move |($($n,)+)| (self)($($n,)+), ($($T::Config::default(),)+)) - } - - fn create_with_config(self, cfg: ($($T::Config,)+)) -> WithAsync<($($T,)+), State, Res, Item, Err> { - WithAsync::new(move |($($n,)+)| (self)($($n,)+), cfg) - } - } -}); - -with_factory_tuple!((a, A)); -with_factory_tuple!((a, A), (b, B)); -with_factory_tuple!((a, A), (b, B), (c, C)); -with_factory_tuple!((a, A), (b, B), (c, C), (d, D)); -with_factory_tuple!((a, A), (b, B), (c, C), (d, D), (e, E)); -with_factory_tuple!((a, A), (b, B), (c, C), (d, D), (e, E), (f, F)); -with_factory_tuple!((a, A), (b, B), (c, C), (d, D), (e, E), (f, F), (g, G)); -with_factory_tuple!( - (a, A), - (b, B), - (c, C), - (d, D), - (e, E), - (f, F), - (g, G), - (h, H) -); -with_factory_tuple!( - (a, A), - (b, B), - (c, C), - (d, D), - (e, E), - (f, F), - (g, G), - (h, H), - (i, I) -); - -with_async_factory_tuple!((a, A)); -with_async_factory_tuple!((a, A), (b, B)); -with_async_factory_tuple!((a, A), (b, B), (c, C)); -with_async_factory_tuple!((a, A), (b, B), (c, C), (d, D)); -with_async_factory_tuple!((a, A), (b, B), (c, C), (d, D), (e, E)); -with_async_factory_tuple!((a, A), (b, B), (c, C), (d, D), (e, E), (f, F)); -with_async_factory_tuple!((a, A), (b, B), (c, C), (d, D), (e, E), (f, F), (g, G)); -with_async_factory_tuple!( - (a, A), - (b, B), - (c, C), - (d, D), - (e, E), - (f, F), - (g, G), - (h, H) -); -with_async_factory_tuple!( - (a, A), - (b, B), - (c, C), - (d, D), - (e, E), - (f, F), - (g, G), - (h, H), - (i, I) -); diff --git a/src/ws/client.rs b/src/ws/client.rs deleted file mode 100644 index 18789fef..00000000 --- a/src/ws/client.rs +++ /dev/null @@ -1,602 +0,0 @@ -//! Http client request -use std::cell::RefCell; -use std::rc::Rc; -use std::time::Duration; -use std::{fmt, io, str}; - -use base64; -use bytes::Bytes; -use cookie::Cookie; -use futures::sync::mpsc::{unbounded, UnboundedSender}; -use futures::{Async, Future, Poll, Stream}; -use http::header::{self, HeaderName, HeaderValue}; -use http::{Error as HttpError, HttpTryFrom, StatusCode}; -use rand; -use sha1::Sha1; - -use actix::{Addr, SystemService}; - -use body::{Binary, Body}; -use error::{Error, UrlParseError}; -use header::IntoHeaderValue; -use httpmessage::HttpMessage; -use payload::PayloadBuffer; - -use client::{ - ClientConnector, ClientRequest, ClientRequestBuilder, HttpResponseParserError, - Pipeline, SendRequest, SendRequestError, -}; - -use super::frame::{Frame, FramedMessage}; -use super::proto::{CloseReason, OpCode}; -use super::{Message, ProtocolError, WsWriter}; - -/// Websocket client error -#[derive(Fail, Debug)] -pub enum ClientError { - /// Invalid url - #[fail(display = "Invalid url")] - InvalidUrl, - /// Invalid response status - #[fail(display = "Invalid response status")] - InvalidResponseStatus(StatusCode), - /// Invalid upgrade header - #[fail(display = "Invalid upgrade header")] - InvalidUpgradeHeader, - /// Invalid connection header - #[fail(display = "Invalid connection header")] - InvalidConnectionHeader(HeaderValue), - /// Missing CONNECTION header - #[fail(display = "Missing CONNECTION header")] - MissingConnectionHeader, - /// Missing SEC-WEBSOCKET-ACCEPT header - #[fail(display = "Missing SEC-WEBSOCKET-ACCEPT header")] - MissingWebSocketAcceptHeader, - /// Invalid challenge response - #[fail(display = "Invalid challenge response")] - InvalidChallengeResponse(String, HeaderValue), - /// Http parsing error - #[fail(display = "Http parsing error")] - Http(Error), - /// Url parsing error - #[fail(display = "Url parsing error")] - Url(UrlParseError), - /// Response parsing error - #[fail(display = "Response parsing error")] - ResponseParseError(HttpResponseParserError), - /// Send request error - #[fail(display = "{}", _0)] - SendRequest(SendRequestError), - /// Protocol error - #[fail(display = "{}", _0)] - Protocol(#[cause] ProtocolError), - /// IO Error - #[fail(display = "{}", _0)] - Io(io::Error), - /// "Disconnected" - #[fail(display = "Disconnected")] - Disconnected, -} - -impl From for ClientError { - fn from(err: Error) -> ClientError { - ClientError::Http(err) - } -} - -impl From for ClientError { - fn from(err: UrlParseError) -> ClientError { - ClientError::Url(err) - } -} - -impl From for ClientError { - fn from(err: SendRequestError) -> ClientError { - ClientError::SendRequest(err) - } -} - -impl From for ClientError { - fn from(err: ProtocolError) -> ClientError { - ClientError::Protocol(err) - } -} - -impl From for ClientError { - fn from(err: io::Error) -> ClientError { - ClientError::Io(err) - } -} - -impl From for ClientError { - fn from(err: HttpResponseParserError) -> ClientError { - ClientError::ResponseParseError(err) - } -} - -/// `WebSocket` client -/// -/// Example of `WebSocket` client usage is available in -/// [websocket example]( -/// https://github.com/actix/examples/blob/master/websocket/src/client.rs#L24) -pub struct Client { - request: ClientRequestBuilder, - err: Option, - http_err: Option, - origin: Option, - protocols: Option, - conn: Addr, - max_size: usize, - no_masking: bool, -} - -impl Client { - /// Create new websocket connection - pub fn new>(uri: S) -> Client { - Client::with_connector(uri, ClientConnector::from_registry()) - } - - /// Create new websocket connection with custom `ClientConnector` - pub fn with_connector>(uri: S, conn: Addr) -> Client { - let mut cl = Client { - request: ClientRequest::build(), - err: None, - http_err: None, - origin: None, - protocols: None, - max_size: 65_536, - no_masking: false, - conn, - }; - cl.request.uri(uri.as_ref()); - cl - } - - /// Set supported websocket protocols - pub fn protocols(mut self, protos: U) -> Self - where - U: IntoIterator + 'static, - V: AsRef, - { - let mut protos = protos - .into_iter() - .fold(String::new(), |acc, s| acc + s.as_ref() + ","); - protos.pop(); - self.protocols = Some(protos); - self - } - - /// Set cookie for handshake request - pub fn cookie(mut self, cookie: Cookie) -> Self { - self.request.cookie(cookie); - self - } - - /// Set request Origin - pub fn origin(mut self, origin: V) -> Self - where - HeaderValue: HttpTryFrom, - { - match HeaderValue::try_from(origin) { - Ok(value) => self.origin = Some(value), - Err(e) => self.http_err = Some(e.into()), - } - self - } - - /// Set max frame size - /// - /// By default max size is set to 64kb - pub fn max_frame_size(mut self, size: usize) -> Self { - self.max_size = size; - self - } - - /// Set write buffer capacity - /// - /// Default buffer capacity is 32kb - pub fn write_buffer_capacity(mut self, cap: usize) -> Self { - self.request.write_buffer_capacity(cap); - self - } - - /// Disable payload masking. By default ws client masks frame payload. - pub fn no_masking(mut self) -> Self { - self.no_masking = true; - self - } - - /// Set request header - pub fn header(mut self, key: K, value: V) -> Self - where - HeaderName: HttpTryFrom, - V: IntoHeaderValue, - { - self.request.header(key, value); - self - } - - /// Set websocket handshake timeout - /// - /// Handshake timeout is a total time for successful handshake. - /// Default value is 5 seconds. - pub fn timeout(mut self, timeout: Duration) -> Self { - self.request.timeout(timeout); - self - } - - /// Connect to websocket server and do ws handshake - pub fn connect(&mut self) -> ClientHandshake { - if let Some(e) = self.err.take() { - ClientHandshake::error(e) - } else if let Some(e) = self.http_err.take() { - ClientHandshake::error(Error::from(e).into()) - } else { - // origin - if let Some(origin) = self.origin.take() { - self.request.set_header(header::ORIGIN, origin); - } - - self.request.upgrade(); - self.request.set_header(header::UPGRADE, "websocket"); - self.request.set_header(header::CONNECTION, "upgrade"); - self.request.set_header(header::SEC_WEBSOCKET_VERSION, "13"); - self.request.with_connector(self.conn.clone()); - - if let Some(protocols) = self.protocols.take() { - self.request - .set_header(header::SEC_WEBSOCKET_PROTOCOL, protocols.as_str()); - } - let request = match self.request.finish() { - Ok(req) => req, - Err(err) => return ClientHandshake::error(err.into()), - }; - - if request.uri().host().is_none() { - return ClientHandshake::error(ClientError::InvalidUrl); - } - if let Some(scheme) = request.uri().scheme_part() { - if scheme != "http" - && scheme != "https" - && scheme != "ws" - && scheme != "wss" - { - return ClientHandshake::error(ClientError::InvalidUrl); - } - } else { - return ClientHandshake::error(ClientError::InvalidUrl); - } - - // start handshake - ClientHandshake::new(request, self.max_size, self.no_masking) - } - } -} - -struct Inner { - tx: UnboundedSender, - rx: PayloadBuffer>, - closed: bool, -} - -/// Future that implementes client websocket handshake process. -/// -/// It resolves to a pair of `ClientReader` and `ClientWriter` that -/// can be used for reading and writing websocket frames. -pub struct ClientHandshake { - request: Option, - tx: Option>, - key: String, - error: Option, - max_size: usize, - no_masking: bool, -} - -impl ClientHandshake { - fn new( - mut request: ClientRequest, max_size: usize, no_masking: bool, - ) -> ClientHandshake { - // Generate a random key for the `Sec-WebSocket-Key` header. - // a base64-encoded (see Section 4 of [RFC4648]) value that, - // when decoded, is 16 bytes in length (RFC 6455) - let sec_key: [u8; 16] = rand::random(); - let key = base64::encode(&sec_key); - - request.headers_mut().insert( - header::SEC_WEBSOCKET_KEY, - HeaderValue::try_from(key.as_str()).unwrap(), - ); - - let (tx, rx) = unbounded(); - request.set_body(Body::Streaming(Box::new(rx.map_err(|_| { - io::Error::new(io::ErrorKind::Other, "disconnected").into() - })))); - - ClientHandshake { - key, - max_size, - no_masking, - request: Some(request.send()), - tx: Some(tx), - error: None, - } - } - - fn error(err: ClientError) -> ClientHandshake { - ClientHandshake { - key: String::new(), - request: None, - tx: None, - error: Some(err), - max_size: 0, - no_masking: false, - } - } - - /// Set handshake timeout - /// - /// Handshake timeout is a total time before handshake should be completed. - /// Default value is 5 seconds. - pub fn timeout(mut self, timeout: Duration) -> Self { - if let Some(request) = self.request.take() { - self.request = Some(request.timeout(timeout)); - } - self - } - - /// Set connection timeout - /// - /// Connection timeout includes resolving hostname and actual connection to - /// the host. - /// Default value is 1 second. - pub fn conn_timeout(mut self, timeout: Duration) -> Self { - if let Some(request) = self.request.take() { - self.request = Some(request.conn_timeout(timeout)); - } - self - } -} - -impl Future for ClientHandshake { - type Item = (ClientReader, ClientWriter); - type Error = ClientError; - - fn poll(&mut self) -> Poll { - if let Some(err) = self.error.take() { - return Err(err); - } - - let resp = match self.request.as_mut().unwrap().poll()? { - Async::Ready(response) => { - self.request.take(); - response - } - Async::NotReady => return Ok(Async::NotReady), - }; - - // verify response - if resp.status() != StatusCode::SWITCHING_PROTOCOLS { - return Err(ClientError::InvalidResponseStatus(resp.status())); - } - // Check for "UPGRADE" to websocket header - let has_hdr = if let Some(hdr) = resp.headers().get(header::UPGRADE) { - if let Ok(s) = hdr.to_str() { - s.to_lowercase().contains("websocket") - } else { - false - } - } else { - false - }; - if !has_hdr { - trace!("Invalid upgrade header"); - return Err(ClientError::InvalidUpgradeHeader); - } - // Check for "CONNECTION" header - if let Some(conn) = resp.headers().get(header::CONNECTION) { - if let Ok(s) = conn.to_str() { - if !s.to_lowercase().contains("upgrade") { - trace!("Invalid connection header: {}", s); - return Err(ClientError::InvalidConnectionHeader(conn.clone())); - } - } else { - trace!("Invalid connection header: {:?}", conn); - return Err(ClientError::InvalidConnectionHeader(conn.clone())); - } - } else { - trace!("Missing connection header"); - return Err(ClientError::MissingConnectionHeader); - } - - if let Some(key) = resp.headers().get(header::SEC_WEBSOCKET_ACCEPT) { - // field is constructed by concatenating /key/ - // with the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" (RFC 6455) - const WS_GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - let mut sha1 = Sha1::new(); - sha1.update(self.key.as_ref()); - sha1.update(WS_GUID); - let encoded = base64::encode(&sha1.digest().bytes()); - if key.as_bytes() != encoded.as_bytes() { - trace!( - "Invalid challenge response: expected: {} received: {:?}", - encoded, - key - ); - return Err(ClientError::InvalidChallengeResponse(encoded, key.clone())); - } - } else { - trace!("Missing SEC-WEBSOCKET-ACCEPT header"); - return Err(ClientError::MissingWebSocketAcceptHeader); - }; - - let inner = Inner { - tx: self.tx.take().unwrap(), - rx: PayloadBuffer::new(resp.payload()), - closed: false, - }; - - let inner = Rc::new(RefCell::new(inner)); - Ok(Async::Ready(( - ClientReader { - inner: Rc::clone(&inner), - max_size: self.max_size, - no_masking: self.no_masking, - }, - ClientWriter { inner }, - ))) - } -} - -/// Websocket reader client -pub struct ClientReader { - inner: Rc>, - max_size: usize, - no_masking: bool, -} - -impl fmt::Debug for ClientReader { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "ws::ClientReader()") - } -} - -impl Stream for ClientReader { - type Item = Message; - type Error = ProtocolError; - - fn poll(&mut self) -> Poll, Self::Error> { - let max_size = self.max_size; - let no_masking = self.no_masking; - let mut inner = self.inner.borrow_mut(); - if inner.closed { - return Ok(Async::Ready(None)); - } - - // read - match Frame::parse(&mut inner.rx, no_masking, max_size) { - Ok(Async::Ready(Some(frame))) => { - let (_finished, opcode, payload) = frame.unpack(); - - match opcode { - // continuation is not supported - OpCode::Continue => { - inner.closed = true; - Err(ProtocolError::NoContinuation) - } - OpCode::Bad => { - inner.closed = true; - Err(ProtocolError::BadOpCode) - } - OpCode::Close => { - inner.closed = true; - let close_reason = Frame::parse_close_payload(&payload); - Ok(Async::Ready(Some(Message::Close(close_reason)))) - } - OpCode::Ping => Ok(Async::Ready(Some(Message::Ping( - String::from_utf8_lossy(payload.as_ref()).into(), - )))), - OpCode::Pong => Ok(Async::Ready(Some(Message::Pong( - String::from_utf8_lossy(payload.as_ref()).into(), - )))), - OpCode::Binary => Ok(Async::Ready(Some(Message::Binary(payload)))), - OpCode::Text => { - let tmp = Vec::from(payload.as_ref()); - match String::from_utf8(tmp) { - Ok(s) => Ok(Async::Ready(Some(Message::Text(s)))), - Err(_) => { - inner.closed = true; - Err(ProtocolError::BadEncoding) - } - } - } - } - } - Ok(Async::Ready(None)) => Ok(Async::Ready(None)), - Ok(Async::NotReady) => Ok(Async::NotReady), - Err(e) => { - inner.closed = true; - Err(e) - } - } - } -} - -/// Websocket writer client -pub struct ClientWriter { - inner: Rc>, -} - -impl ClientWriter { - /// Write payload - #[inline] - fn write(&mut self, mut data: FramedMessage) { - let inner = self.inner.borrow_mut(); - if !inner.closed { - let _ = inner.tx.unbounded_send(data.0.take()); - } else { - warn!("Trying to write to disconnected response"); - } - } - - /// Send text frame - #[inline] - pub fn text>(&mut self, text: T) { - self.write(Frame::message(text.into(), OpCode::Text, true, true)); - } - - /// Send binary frame - #[inline] - pub fn binary>(&mut self, data: B) { - self.write(Frame::message(data, OpCode::Binary, true, true)); - } - - /// Send ping frame - #[inline] - pub fn ping(&mut self, message: &str) { - self.write(Frame::message(Vec::from(message), OpCode::Ping, true, true)); - } - - /// Send pong frame - #[inline] - pub fn pong(&mut self, message: &str) { - self.write(Frame::message(Vec::from(message), OpCode::Pong, true, true)); - } - - /// Send close frame - #[inline] - pub fn close(&mut self, reason: Option) { - self.write(Frame::close(reason, true)); - } -} - -impl WsWriter for ClientWriter { - /// Send text frame - #[inline] - fn send_text>(&mut self, text: T) { - self.text(text) - } - - /// Send binary frame - #[inline] - fn send_binary>(&mut self, data: B) { - self.binary(data) - } - - /// Send ping frame - #[inline] - fn send_ping(&mut self, message: &str) { - self.ping(message) - } - - /// Send pong frame - #[inline] - fn send_pong(&mut self, message: &str) { - self.pong(message) - } - - /// Send close frame - #[inline] - fn send_close(&mut self, reason: Option) { - self.close(reason); - } -} diff --git a/src/ws/context.rs b/src/ws/context.rs deleted file mode 100644 index 5e207d43..00000000 --- a/src/ws/context.rs +++ /dev/null @@ -1,341 +0,0 @@ -extern crate actix; - -use bytes::Bytes; -use futures::sync::oneshot::{self, Sender}; -use futures::{Async, Future, Poll, Stream}; -use smallvec::SmallVec; - -use self::actix::dev::{ - AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, StreamHandler, - ToEnvelope, -}; -use self::actix::fut::ActorFuture; -use self::actix::{ - Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, - Message as ActixMessage, SpawnHandle, -}; - -use body::{Binary, Body}; -use context::{ActorHttpContext, Drain, Frame as ContextFrame}; -use error::{Error, ErrorInternalServerError, PayloadError}; -use httprequest::HttpRequest; - -use ws::frame::{Frame, FramedMessage}; -use ws::proto::{CloseReason, OpCode}; -use ws::{Message, ProtocolError, WsStream, WsWriter}; - -/// Execution context for `WebSockets` actors -pub struct WebsocketContext -where - A: Actor>, -{ - inner: ContextParts, - stream: Option>, - request: HttpRequest, - disconnected: bool, -} - -impl ActorContext for WebsocketContext -where - A: Actor, -{ - fn stop(&mut self) { - self.inner.stop(); - } - fn terminate(&mut self) { - self.inner.terminate() - } - fn state(&self) -> ActorState { - self.inner.state() - } -} - -impl AsyncContext for WebsocketContext -where - A: Actor, -{ - fn spawn(&mut self, fut: F) -> SpawnHandle - where - F: ActorFuture + 'static, - { - self.inner.spawn(fut) - } - - fn wait(&mut self, fut: F) - where - F: ActorFuture + 'static, - { - self.inner.wait(fut) - } - - #[doc(hidden)] - #[inline] - fn waiting(&self) -> bool { - self.inner.waiting() - || self.inner.state() == ActorState::Stopping - || self.inner.state() == ActorState::Stopped - } - - fn cancel_future(&mut self, handle: SpawnHandle) -> bool { - self.inner.cancel_future(handle) - } - - #[inline] - fn address(&self) -> Addr { - self.inner.address() - } -} - -impl WebsocketContext -where - A: Actor, -{ - #[inline] - /// Create a new Websocket context from a request and an actor - pub fn create

(req: HttpRequest, actor: A, stream: WsStream

) -> Body - where - A: StreamHandler, - P: Stream + 'static, - { - let mb = Mailbox::default(); - let mut ctx = WebsocketContext { - inner: ContextParts::new(mb.sender_producer()), - stream: None, - request: req, - disconnected: false, - }; - ctx.add_stream(stream); - - Body::Actor(Box::new(WebsocketContextFut::new(ctx, actor, mb))) - } - - /// Create a new Websocket context - pub fn with_factory(req: HttpRequest, f: F) -> Body - where - F: FnOnce(&mut Self) -> A + 'static, - { - let mb = Mailbox::default(); - let mut ctx = WebsocketContext { - inner: ContextParts::new(mb.sender_producer()), - stream: None, - request: req, - disconnected: false, - }; - - let act = f(&mut ctx); - Body::Actor(Box::new(WebsocketContextFut::new(ctx, act, mb))) - } -} - -impl WebsocketContext -where - A: Actor, -{ - /// Write payload - /// - /// This is a low-level function that accepts framed messages that should - /// be created using `Frame::message()`. If you want to send text or binary - /// data you should prefer the `text()` or `binary()` convenience functions - /// that handle the framing for you. - #[inline] - pub fn write_raw(&mut self, data: FramedMessage) { - if !self.disconnected { - if self.stream.is_none() { - self.stream = Some(SmallVec::new()); - } - let stream = self.stream.as_mut().unwrap(); - stream.push(ContextFrame::Chunk(Some(data.0))); - } else { - warn!("Trying to write to disconnected response"); - } - } - - /// Shared application state - #[inline] - pub fn state(&self) -> &S { - self.request.state() - } - - /// Incoming request - #[inline] - pub fn request(&mut self) -> &mut HttpRequest { - &mut self.request - } - - /// Returns drain future - pub fn drain(&mut self) -> Drain { - let (tx, rx) = oneshot::channel(); - self.add_frame(ContextFrame::Drain(tx)); - Drain::new(rx) - } - - /// Send text frame - #[inline] - pub fn text>(&mut self, text: T) { - self.write_raw(Frame::message(text.into(), OpCode::Text, true, false)); - } - - /// Send binary frame - #[inline] - pub fn binary>(&mut self, data: B) { - self.write_raw(Frame::message(data, OpCode::Binary, true, false)); - } - - /// Send ping frame - #[inline] - pub fn ping(&mut self, message: &str) { - self.write_raw(Frame::message( - Vec::from(message), - OpCode::Ping, - true, - false, - )); - } - - /// Send pong frame - #[inline] - pub fn pong(&mut self, message: &str) { - self.write_raw(Frame::message( - Vec::from(message), - OpCode::Pong, - true, - false, - )); - } - - /// Send close frame - #[inline] - pub fn close(&mut self, reason: Option) { - self.write_raw(Frame::close(reason, false)); - } - - /// Check if connection still open - #[inline] - pub fn connected(&self) -> bool { - !self.disconnected - } - - #[inline] - fn add_frame(&mut self, frame: ContextFrame) { - if self.stream.is_none() { - self.stream = Some(SmallVec::new()); - } - if let Some(s) = self.stream.as_mut() { - s.push(frame) - } - } - - /// Handle of the running future - /// - /// SpawnHandle is the handle returned by `AsyncContext::spawn()` method. - pub fn handle(&self) -> SpawnHandle { - self.inner.curr_handle() - } - - /// Set mailbox capacity - /// - /// By default mailbox capacity is 16 messages. - pub fn set_mailbox_capacity(&mut self, cap: usize) { - self.inner.set_mailbox_capacity(cap) - } -} - -impl WsWriter for WebsocketContext -where - A: Actor, - S: 'static, -{ - /// Send text frame - #[inline] - fn send_text>(&mut self, text: T) { - self.text(text) - } - - /// Send binary frame - #[inline] - fn send_binary>(&mut self, data: B) { - self.binary(data) - } - - /// Send ping frame - #[inline] - fn send_ping(&mut self, message: &str) { - self.ping(message) - } - - /// Send pong frame - #[inline] - fn send_pong(&mut self, message: &str) { - self.pong(message) - } - - /// Send close frame - #[inline] - fn send_close(&mut self, reason: Option) { - self.close(reason) - } -} - -impl AsyncContextParts for WebsocketContext -where - A: Actor, -{ - fn parts(&mut self) -> &mut ContextParts { - &mut self.inner - } -} - -struct WebsocketContextFut -where - A: Actor>, -{ - fut: ContextFut>, -} - -impl WebsocketContextFut -where - A: Actor>, -{ - fn new(ctx: WebsocketContext, act: A, mailbox: Mailbox) -> Self { - let fut = ContextFut::new(ctx, act, mailbox); - WebsocketContextFut { fut } - } -} - -impl ActorHttpContext for WebsocketContextFut -where - A: Actor>, - S: 'static, -{ - #[inline] - fn disconnected(&mut self) { - self.fut.ctx().disconnected = true; - self.fut.ctx().stop(); - } - - fn poll(&mut self) -> Poll>, Error> { - if self.fut.alive() && self.fut.poll().is_err() { - return Err(ErrorInternalServerError("error")); - } - - // frames - if let Some(data) = self.fut.ctx().stream.take() { - Ok(Async::Ready(Some(data))) - } else if self.fut.alive() { - Ok(Async::NotReady) - } else { - Ok(Async::Ready(None)) - } - } -} - -impl ToEnvelope for WebsocketContext -where - A: Actor> + Handler, - M: ActixMessage + Send + 'static, - M::Result: Send, -{ - fn pack(msg: M, tx: Option>) -> Envelope { - Envelope::new(msg, tx) - } -} diff --git a/src/ws/frame.rs b/src/ws/frame.rs deleted file mode 100644 index 5e4fd829..00000000 --- a/src/ws/frame.rs +++ /dev/null @@ -1,538 +0,0 @@ -use byteorder::{ByteOrder, LittleEndian, NetworkEndian}; -use bytes::{BufMut, Bytes, BytesMut}; -use futures::{Async, Poll, Stream}; -use rand; -use std::fmt; - -use body::Binary; -use error::PayloadError; -use payload::PayloadBuffer; - -use ws::mask::apply_mask; -use ws::proto::{CloseCode, CloseReason, OpCode}; -use ws::ProtocolError; - -/// A struct representing a `WebSocket` frame. -#[derive(Debug)] -pub struct Frame { - finished: bool, - opcode: OpCode, - payload: Binary, -} - -impl Frame { - /// Destruct frame - pub fn unpack(self) -> (bool, OpCode, Binary) { - (self.finished, self.opcode, self.payload) - } - - /// Create a new Close control frame. - #[inline] - pub fn close(reason: Option, genmask: bool) -> FramedMessage { - let payload = match reason { - None => Vec::new(), - Some(reason) => { - let mut code_bytes = [0; 2]; - NetworkEndian::write_u16(&mut code_bytes, reason.code.into()); - - let mut payload = Vec::from(&code_bytes[..]); - if let Some(description) = reason.description { - payload.extend(description.as_bytes()); - } - payload - } - }; - - Frame::message(payload, OpCode::Close, true, genmask) - } - - #[cfg_attr(feature = "cargo-clippy", allow(type_complexity))] - fn read_copy_md( - pl: &mut PayloadBuffer, server: bool, max_size: usize, - ) -> Poll)>, ProtocolError> - where - S: Stream, - { - let mut idx = 2; - let buf = match pl.copy(2)? { - Async::Ready(Some(buf)) => buf, - Async::Ready(None) => return Ok(Async::Ready(None)), - Async::NotReady => return Ok(Async::NotReady), - }; - let first = buf[0]; - let second = buf[1]; - let finished = first & 0x80 != 0; - - // check masking - let masked = second & 0x80 != 0; - if !masked && server { - return Err(ProtocolError::UnmaskedFrame); - } else if masked && !server { - return Err(ProtocolError::MaskedFrame); - } - - // Op code - let opcode = OpCode::from(first & 0x0F); - - if let OpCode::Bad = opcode { - return Err(ProtocolError::InvalidOpcode(first & 0x0F)); - } - - let len = second & 0x7F; - let length = if len == 126 { - let buf = match pl.copy(4)? { - Async::Ready(Some(buf)) => buf, - Async::Ready(None) => return Ok(Async::Ready(None)), - Async::NotReady => return Ok(Async::NotReady), - }; - let len = NetworkEndian::read_uint(&buf[idx..], 2) as usize; - idx += 2; - len - } else if len == 127 { - let buf = match pl.copy(10)? { - Async::Ready(Some(buf)) => buf, - Async::Ready(None) => return Ok(Async::Ready(None)), - Async::NotReady => return Ok(Async::NotReady), - }; - let len = NetworkEndian::read_uint(&buf[idx..], 8); - if len > max_size as u64 { - return Err(ProtocolError::Overflow); - } - idx += 8; - len as usize - } else { - len as usize - }; - - // check for max allowed size - if length > max_size { - return Err(ProtocolError::Overflow); - } - - let mask = if server { - let buf = match pl.copy(idx + 4)? { - Async::Ready(Some(buf)) => buf, - Async::Ready(None) => return Ok(Async::Ready(None)), - Async::NotReady => return Ok(Async::NotReady), - }; - - let mask: &[u8] = &buf[idx..idx + 4]; - let mask_u32 = LittleEndian::read_u32(mask); - idx += 4; - Some(mask_u32) - } else { - None - }; - - Ok(Async::Ready(Some((idx, finished, opcode, length, mask)))) - } - - fn read_chunk_md( - chunk: &[u8], server: bool, max_size: usize, - ) -> Poll<(usize, bool, OpCode, usize, Option), ProtocolError> { - let chunk_len = chunk.len(); - - let mut idx = 2; - if chunk_len < 2 { - return Ok(Async::NotReady); - } - - let first = chunk[0]; - let second = chunk[1]; - let finished = first & 0x80 != 0; - - // check masking - let masked = second & 0x80 != 0; - if !masked && server { - return Err(ProtocolError::UnmaskedFrame); - } else if masked && !server { - return Err(ProtocolError::MaskedFrame); - } - - // Op code - let opcode = OpCode::from(first & 0x0F); - - if let OpCode::Bad = opcode { - return Err(ProtocolError::InvalidOpcode(first & 0x0F)); - } - - let len = second & 0x7F; - let length = if len == 126 { - if chunk_len < 4 { - return Ok(Async::NotReady); - } - let len = NetworkEndian::read_uint(&chunk[idx..], 2) as usize; - idx += 2; - len - } else if len == 127 { - if chunk_len < 10 { - return Ok(Async::NotReady); - } - let len = NetworkEndian::read_uint(&chunk[idx..], 8); - if len > max_size as u64 { - return Err(ProtocolError::Overflow); - } - idx += 8; - len as usize - } else { - len as usize - }; - - // check for max allowed size - if length > max_size { - return Err(ProtocolError::Overflow); - } - - let mask = if server { - if chunk_len < idx + 4 { - return Ok(Async::NotReady); - } - - let mask: &[u8] = &chunk[idx..idx + 4]; - let mask_u32 = LittleEndian::read_u32(mask); - idx += 4; - Some(mask_u32) - } else { - None - }; - - Ok(Async::Ready((idx, finished, opcode, length, mask))) - } - - /// Parse the input stream into a frame. - pub fn parse( - pl: &mut PayloadBuffer, server: bool, max_size: usize, - ) -> Poll, ProtocolError> - where - S: Stream, - { - // try to parse ws frame md from one chunk - let result = match pl.get_chunk()? { - Async::NotReady => return Ok(Async::NotReady), - Async::Ready(None) => return Ok(Async::Ready(None)), - Async::Ready(Some(chunk)) => Frame::read_chunk_md(chunk, server, max_size)?, - }; - - let (idx, finished, opcode, length, mask) = match result { - // we may need to join several chunks - Async::NotReady => match Frame::read_copy_md(pl, server, max_size)? { - Async::Ready(Some(item)) => item, - Async::NotReady => return Ok(Async::NotReady), - Async::Ready(None) => return Ok(Async::Ready(None)), - }, - Async::Ready(item) => item, - }; - - match pl.can_read(idx + length)? { - Async::Ready(Some(true)) => (), - Async::Ready(None) => return Ok(Async::Ready(None)), - Async::Ready(Some(false)) | Async::NotReady => return Ok(Async::NotReady), - } - - // remove prefix - pl.drop_bytes(idx); - - // no need for body - if length == 0 { - return Ok(Async::Ready(Some(Frame { - finished, - opcode, - payload: Binary::from(""), - }))); - } - - let data = match pl.read_exact(length)? { - Async::Ready(Some(buf)) => buf, - Async::Ready(None) => return Ok(Async::Ready(None)), - Async::NotReady => panic!(), - }; - - // control frames must have length <= 125 - match opcode { - OpCode::Ping | OpCode::Pong if length > 125 => { - return Err(ProtocolError::InvalidLength(length)) - } - OpCode::Close if length > 125 => { - debug!("Received close frame with payload length exceeding 125. Morphing to protocol close frame."); - return Ok(Async::Ready(Some(Frame::default()))); - } - _ => (), - } - - // unmask - let data = if let Some(mask) = mask { - let mut buf = BytesMut::new(); - buf.extend_from_slice(&data); - apply_mask(&mut buf, mask); - buf.freeze() - } else { - data - }; - - Ok(Async::Ready(Some(Frame { - finished, - opcode, - payload: data.into(), - }))) - } - - /// Parse the payload of a close frame. - pub fn parse_close_payload(payload: &Binary) -> Option { - if payload.len() >= 2 { - let raw_code = NetworkEndian::read_u16(payload.as_ref()); - let code = CloseCode::from(raw_code); - let description = if payload.len() > 2 { - Some(String::from_utf8_lossy(&payload.as_ref()[2..]).into()) - } else { - None - }; - Some(CloseReason { code, description }) - } else { - None - } - } - - /// Generate binary representation - pub fn message>( - data: B, code: OpCode, finished: bool, genmask: bool, - ) -> FramedMessage { - let payload = data.into(); - let one: u8 = if finished { - 0x80 | Into::::into(code) - } else { - code.into() - }; - let payload_len = payload.len(); - let (two, p_len) = if genmask { - (0x80, payload_len + 4) - } else { - (0, payload_len) - }; - - let mut buf = if payload_len < 126 { - let mut buf = BytesMut::with_capacity(p_len + 2); - buf.put_slice(&[one, two | payload_len as u8]); - buf - } else if payload_len <= 65_535 { - let mut buf = BytesMut::with_capacity(p_len + 4); - buf.put_slice(&[one, two | 126]); - buf.put_u16_be(payload_len as u16); - buf - } else { - let mut buf = BytesMut::with_capacity(p_len + 10); - buf.put_slice(&[one, two | 127]); - buf.put_u64_be(payload_len as u64); - buf - }; - - let binary = if genmask { - let mask = rand::random::(); - buf.put_u32_le(mask); - buf.extend_from_slice(payload.as_ref()); - let pos = buf.len() - payload_len; - apply_mask(&mut buf[pos..], mask); - buf.into() - } else { - buf.put_slice(payload.as_ref()); - buf.into() - }; - - FramedMessage(binary) - } -} - -impl Default for Frame { - fn default() -> Frame { - Frame { - finished: true, - opcode: OpCode::Close, - payload: Binary::from(&b""[..]), - } - } -} - -impl fmt::Display for Frame { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - " - - final: {} - opcode: {} - payload length: {} - payload: 0x{} -", - self.finished, - self.opcode, - self.payload.len(), - self.payload - .as_ref() - .iter() - .map(|byte| format!("{:x}", byte)) - .collect::() - ) - } -} - -/// `WebSocket` message with framing. -#[derive(Debug)] -pub struct FramedMessage(pub(crate) Binary); - -#[cfg(test)] -mod tests { - use super::*; - use futures::stream::once; - - fn is_none(frm: &Poll, ProtocolError>) -> bool { - match *frm { - Ok(Async::Ready(None)) => true, - _ => false, - } - } - - fn extract(frm: Poll, ProtocolError>) -> Frame { - match frm { - Ok(Async::Ready(Some(frame))) => frame, - _ => unreachable!("error"), - } - } - - #[test] - fn test_parse() { - let mut buf = PayloadBuffer::new(once(Ok(BytesMut::from( - &[0b0000_0001u8, 0b0000_0001u8][..], - ).freeze()))); - assert!(is_none(&Frame::parse(&mut buf, false, 1024))); - - let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0001u8][..]); - buf.extend(b"1"); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); - - let frame = extract(Frame::parse(&mut buf, false, 1024)); - assert!(!frame.finished); - assert_eq!(frame.opcode, OpCode::Text); - assert_eq!(frame.payload.as_ref(), &b"1"[..]); - } - - #[test] - fn test_parse_length0() { - let buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0000u8][..]); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); - - let frame = extract(Frame::parse(&mut buf, false, 1024)); - assert!(!frame.finished); - assert_eq!(frame.opcode, OpCode::Text); - assert!(frame.payload.is_empty()); - } - - #[test] - fn test_parse_length2() { - let buf = BytesMut::from(&[0b0000_0001u8, 126u8][..]); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); - assert!(is_none(&Frame::parse(&mut buf, false, 1024))); - - let mut buf = BytesMut::from(&[0b0000_0001u8, 126u8][..]); - buf.extend(&[0u8, 4u8][..]); - buf.extend(b"1234"); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); - - let frame = extract(Frame::parse(&mut buf, false, 1024)); - assert!(!frame.finished); - assert_eq!(frame.opcode, OpCode::Text); - assert_eq!(frame.payload.as_ref(), &b"1234"[..]); - } - - #[test] - fn test_parse_length4() { - let buf = BytesMut::from(&[0b0000_0001u8, 127u8][..]); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); - assert!(is_none(&Frame::parse(&mut buf, false, 1024))); - - let mut buf = BytesMut::from(&[0b0000_0001u8, 127u8][..]); - buf.extend(&[0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 4u8][..]); - buf.extend(b"1234"); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); - - let frame = extract(Frame::parse(&mut buf, false, 1024)); - assert!(!frame.finished); - assert_eq!(frame.opcode, OpCode::Text); - assert_eq!(frame.payload.as_ref(), &b"1234"[..]); - } - - #[test] - fn test_parse_frame_mask() { - let mut buf = BytesMut::from(&[0b0000_0001u8, 0b1000_0001u8][..]); - buf.extend(b"0001"); - buf.extend(b"1"); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); - - assert!(Frame::parse(&mut buf, false, 1024).is_err()); - - let frame = extract(Frame::parse(&mut buf, true, 1024)); - assert!(!frame.finished); - assert_eq!(frame.opcode, OpCode::Text); - assert_eq!(frame.payload, vec![1u8].into()); - } - - #[test] - fn test_parse_frame_no_mask() { - let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0001u8][..]); - buf.extend(&[1u8]); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); - - assert!(Frame::parse(&mut buf, true, 1024).is_err()); - - let frame = extract(Frame::parse(&mut buf, false, 1024)); - assert!(!frame.finished); - assert_eq!(frame.opcode, OpCode::Text); - assert_eq!(frame.payload, vec![1u8].into()); - } - - #[test] - fn test_parse_frame_max_size() { - let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0010u8][..]); - buf.extend(&[1u8, 1u8]); - let mut buf = PayloadBuffer::new(once(Ok(buf.freeze()))); - - assert!(Frame::parse(&mut buf, true, 1).is_err()); - - if let Err(ProtocolError::Overflow) = Frame::parse(&mut buf, false, 0) { - } else { - unreachable!("error"); - } - } - - #[test] - fn test_ping_frame() { - let frame = Frame::message(Vec::from("data"), OpCode::Ping, true, false); - - let mut v = vec![137u8, 4u8]; - v.extend(b"data"); - assert_eq!(frame.0, v.into()); - } - - #[test] - fn test_pong_frame() { - let frame = Frame::message(Vec::from("data"), OpCode::Pong, true, false); - - let mut v = vec![138u8, 4u8]; - v.extend(b"data"); - assert_eq!(frame.0, v.into()); - } - - #[test] - fn test_close_frame() { - let reason = (CloseCode::Normal, "data"); - let frame = Frame::close(Some(reason.into()), false); - - let mut v = vec![136u8, 6u8, 3u8, 232u8]; - v.extend(b"data"); - assert_eq!(frame.0, v.into()); - } - - #[test] - fn test_empty_close_frame() { - let frame = Frame::close(None, false); - assert_eq!(frame.0, vec![0x88, 0x00].into()); - } -} diff --git a/src/ws/mask.rs b/src/ws/mask.rs deleted file mode 100644 index 18ce57bb..00000000 --- a/src/ws/mask.rs +++ /dev/null @@ -1,152 +0,0 @@ -//! This is code from [Tungstenite project](https://github.com/snapview/tungstenite-rs) -#![cfg_attr(feature = "cargo-clippy", allow(cast_ptr_alignment))] -use std::ptr::copy_nonoverlapping; -use std::slice; - -// Holds a slice guaranteed to be shorter than 8 bytes -struct ShortSlice<'a>(&'a mut [u8]); - -impl<'a> ShortSlice<'a> { - unsafe fn new(slice: &'a mut [u8]) -> Self { - // Sanity check for debug builds - debug_assert!(slice.len() < 8); - ShortSlice(slice) - } - fn len(&self) -> usize { - self.0.len() - } -} - -/// Faster version of `apply_mask()` which operates on 8-byte blocks. -#[inline] -#[cfg_attr(feature = "cargo-clippy", allow(cast_lossless))] -pub(crate) fn apply_mask(buf: &mut [u8], mask_u32: u32) { - // Extend the mask to 64 bits - let mut mask_u64 = ((mask_u32 as u64) << 32) | (mask_u32 as u64); - // Split the buffer into three segments - let (head, mid, tail) = align_buf(buf); - - // Initial unaligned segment - let head_len = head.len(); - if head_len > 0 { - xor_short(head, mask_u64); - if cfg!(target_endian = "big") { - mask_u64 = mask_u64.rotate_left(8 * head_len as u32); - } else { - mask_u64 = mask_u64.rotate_right(8 * head_len as u32); - } - } - // Aligned segment - for v in mid { - *v ^= mask_u64; - } - // Final unaligned segment - if tail.len() > 0 { - xor_short(tail, mask_u64); - } -} - -#[inline] -// TODO: copy_nonoverlapping here compiles to call memcpy. While it is not so -// inefficient, it could be done better. The compiler does not understand that -// a `ShortSlice` must be smaller than a u64. -#[cfg_attr( - feature = "cargo-clippy", - allow(needless_pass_by_value) -)] -fn xor_short(buf: ShortSlice, mask: u64) { - // Unsafe: we know that a `ShortSlice` fits in a u64 - unsafe { - let (ptr, len) = (buf.0.as_mut_ptr(), buf.0.len()); - let mut b: u64 = 0; - #[allow(trivial_casts)] - copy_nonoverlapping(ptr, &mut b as *mut _ as *mut u8, len); - b ^= mask; - #[allow(trivial_casts)] - copy_nonoverlapping(&b as *const _ as *const u8, ptr, len); - } -} - -#[inline] -// Unsafe: caller must ensure the buffer has the correct size and alignment -unsafe fn cast_slice(buf: &mut [u8]) -> &mut [u64] { - // Assert correct size and alignment in debug builds - debug_assert!(buf.len().trailing_zeros() >= 3); - debug_assert!((buf.as_ptr() as usize).trailing_zeros() >= 3); - - slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut u64, buf.len() >> 3) -} - -#[inline] -// Splits a slice into three parts: an unaligned short head and tail, plus an aligned -// u64 mid section. -fn align_buf(buf: &mut [u8]) -> (ShortSlice, &mut [u64], ShortSlice) { - let start_ptr = buf.as_ptr() as usize; - let end_ptr = start_ptr + buf.len(); - - // Round *up* to next aligned boundary for start - let start_aligned = (start_ptr + 7) & !0x7; - // Round *down* to last aligned boundary for end - let end_aligned = end_ptr & !0x7; - - if end_aligned >= start_aligned { - // We have our three segments (head, mid, tail) - let (tmp, tail) = buf.split_at_mut(end_aligned - start_ptr); - let (head, mid) = tmp.split_at_mut(start_aligned - start_ptr); - - // Unsafe: we know the middle section is correctly aligned, and the outer - // sections are smaller than 8 bytes - unsafe { (ShortSlice::new(head), cast_slice(mid), ShortSlice(tail)) } - } else { - // We didn't cross even one aligned boundary! - - // Unsafe: The outer sections are smaller than 8 bytes - unsafe { (ShortSlice::new(buf), &mut [], ShortSlice::new(&mut [])) } - } -} - -#[cfg(test)] -mod tests { - use super::apply_mask; - use byteorder::{ByteOrder, LittleEndian}; - - /// A safe unoptimized mask application. - fn apply_mask_fallback(buf: &mut [u8], mask: &[u8; 4]) { - for (i, byte) in buf.iter_mut().enumerate() { - *byte ^= mask[i & 3]; - } - } - - #[test] - fn test_apply_mask() { - let mask = [0x6d, 0xb6, 0xb2, 0x80]; - let mask_u32: u32 = LittleEndian::read_u32(&mask); - - let unmasked = vec![ - 0xf3, 0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0x82, 0xff, 0xfe, 0x00, 0x17, - 0x74, 0xf9, 0x12, 0x03, - ]; - - // Check masking with proper alignment. - { - let mut masked = unmasked.clone(); - apply_mask_fallback(&mut masked, &mask); - - let mut masked_fast = unmasked.clone(); - apply_mask(&mut masked_fast, mask_u32); - - assert_eq!(masked, masked_fast); - } - - // Check masking without alignment. - { - let mut masked = unmasked.clone(); - apply_mask_fallback(&mut masked[1..], &mask); - - let mut masked_fast = unmasked.clone(); - apply_mask(&mut masked_fast[1..], mask_u32); - - assert_eq!(masked, masked_fast); - } - } -} diff --git a/src/ws/mod.rs b/src/ws/mod.rs deleted file mode 100644 index b0942c0d..00000000 --- a/src/ws/mod.rs +++ /dev/null @@ -1,477 +0,0 @@ -//! `WebSocket` support for Actix -//! -//! To setup a `WebSocket`, first do web socket handshake then on success -//! convert `Payload` into a `WsStream` stream and then use `WsWriter` to -//! communicate with the peer. -//! -//! ## Example -//! -//! ```rust -//! # extern crate actix_web; -//! # extern crate actix; -//! # use actix::prelude::*; -//! # use actix_web::*; -//! use actix_web::{ws, HttpRequest, HttpResponse}; -//! -//! // do websocket handshake and start actor -//! fn ws_index(req: &HttpRequest) -> Result { -//! ws::start(req, Ws) -//! } -//! -//! struct Ws; -//! -//! impl Actor for Ws { -//! type Context = ws::WebsocketContext; -//! } -//! -//! // Handler for ws::Message messages -//! impl StreamHandler for Ws { -//! fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) { -//! match msg { -//! ws::Message::Ping(msg) => ctx.pong(&msg), -//! ws::Message::Text(text) => ctx.text(text), -//! ws::Message::Binary(bin) => ctx.binary(bin), -//! _ => (), -//! } -//! } -//! } -//! # -//! # fn main() { -//! # App::new() -//! # .resource("/ws/", |r| r.f(ws_index)) // <- register websocket route -//! # .finish(); -//! # } -//! ``` -use bytes::Bytes; -use futures::{Async, Poll, Stream}; -use http::{header, Method, StatusCode}; - -use super::actix::{Actor, StreamHandler}; - -use body::Binary; -use error::{Error, PayloadError, ResponseError}; -use httpmessage::HttpMessage; -use httprequest::HttpRequest; -use httpresponse::{ConnectionType, HttpResponse, HttpResponseBuilder}; -use payload::PayloadBuffer; - -mod client; -mod context; -mod frame; -mod mask; -mod proto; - -pub use self::client::{ - Client, ClientError, ClientHandshake, ClientReader, ClientWriter, -}; -pub use self::context::WebsocketContext; -pub use self::frame::{Frame, FramedMessage}; -pub use self::proto::{CloseCode, CloseReason, OpCode}; - -/// Websocket protocol errors -#[derive(Fail, Debug)] -pub enum ProtocolError { - /// Received an unmasked frame from client - #[fail(display = "Received an unmasked frame from client")] - UnmaskedFrame, - /// Received a masked frame from server - #[fail(display = "Received a masked frame from server")] - MaskedFrame, - /// Encountered invalid opcode - #[fail(display = "Invalid opcode: {}", _0)] - InvalidOpcode(u8), - /// Invalid control frame length - #[fail(display = "Invalid control frame length: {}", _0)] - InvalidLength(usize), - /// Bad web socket op code - #[fail(display = "Bad web socket op code")] - BadOpCode, - /// A payload reached size limit. - #[fail(display = "A payload reached size limit.")] - Overflow, - /// Continuation is not supported - #[fail(display = "Continuation is not supported.")] - NoContinuation, - /// Bad utf-8 encoding - #[fail(display = "Bad utf-8 encoding.")] - BadEncoding, - /// Payload error - #[fail(display = "Payload error: {}", _0)] - Payload(#[cause] PayloadError), -} - -impl ResponseError for ProtocolError {} - -impl From for ProtocolError { - fn from(err: PayloadError) -> ProtocolError { - ProtocolError::Payload(err) - } -} - -/// Websocket handshake errors -#[derive(Fail, PartialEq, Debug)] -pub enum HandshakeError { - /// Only get method is allowed - #[fail(display = "Method not allowed")] - GetMethodRequired, - /// Upgrade header if not set to websocket - #[fail(display = "Websocket upgrade is expected")] - NoWebsocketUpgrade, - /// Connection header is not set to upgrade - #[fail(display = "Connection upgrade is expected")] - NoConnectionUpgrade, - /// Websocket version header is not set - #[fail(display = "Websocket version header is required")] - NoVersionHeader, - /// Unsupported websocket version - #[fail(display = "Unsupported version")] - UnsupportedVersion, - /// Websocket key is not set or wrong - #[fail(display = "Unknown websocket key")] - BadWebsocketKey, -} - -impl ResponseError for HandshakeError { - fn error_response(&self) -> HttpResponse { - match *self { - HandshakeError::GetMethodRequired => HttpResponse::MethodNotAllowed() - .header(header::ALLOW, "GET") - .finish(), - HandshakeError::NoWebsocketUpgrade => HttpResponse::BadRequest() - .reason("No WebSocket UPGRADE header found") - .finish(), - HandshakeError::NoConnectionUpgrade => HttpResponse::BadRequest() - .reason("No CONNECTION upgrade") - .finish(), - HandshakeError::NoVersionHeader => HttpResponse::BadRequest() - .reason("Websocket version header is required") - .finish(), - HandshakeError::UnsupportedVersion => HttpResponse::BadRequest() - .reason("Unsupported version") - .finish(), - HandshakeError::BadWebsocketKey => HttpResponse::BadRequest() - .reason("Handshake error") - .finish(), - } - } -} - -/// `WebSocket` Message -#[derive(Debug, PartialEq, Message)] -pub enum Message { - /// Text message - Text(String), - /// Binary message - Binary(Binary), - /// Ping message - Ping(String), - /// Pong message - Pong(String), - /// Close message with optional reason - Close(Option), -} - -/// Do websocket handshake and start actor -pub fn start(req: &HttpRequest, actor: A) -> Result -where - A: Actor> + StreamHandler, - S: 'static, -{ - let mut resp = handshake(req)?; - let stream = WsStream::new(req.payload()); - - let body = WebsocketContext::create(req.clone(), actor, stream); - Ok(resp.body(body)) -} - -/// Prepare `WebSocket` handshake response. -/// -/// This function returns handshake `HttpResponse`, ready to send to peer. -/// It does not perform any IO. -/// -// /// `protocols` is a sequence of known protocols. On successful handshake, -// /// the returned response headers contain the first protocol in this list -// /// which the server also knows. -pub fn handshake( - req: &HttpRequest, -) -> Result { - // WebSocket accepts only GET - if *req.method() != Method::GET { - return Err(HandshakeError::GetMethodRequired); - } - - // Check for "UPGRADE" to websocket header - let has_hdr = if let Some(hdr) = req.headers().get(header::UPGRADE) { - if let Ok(s) = hdr.to_str() { - s.to_lowercase().contains("websocket") - } else { - false - } - } else { - false - }; - if !has_hdr { - return Err(HandshakeError::NoWebsocketUpgrade); - } - - // Upgrade connection - if !req.upgrade() { - return Err(HandshakeError::NoConnectionUpgrade); - } - - // check supported version - if !req.headers().contains_key(header::SEC_WEBSOCKET_VERSION) { - return Err(HandshakeError::NoVersionHeader); - } - let supported_ver = { - if let Some(hdr) = req.headers().get(header::SEC_WEBSOCKET_VERSION) { - hdr == "13" || hdr == "8" || hdr == "7" - } else { - false - } - }; - if !supported_ver { - return Err(HandshakeError::UnsupportedVersion); - } - - // check client handshake for validity - if !req.headers().contains_key(header::SEC_WEBSOCKET_KEY) { - return Err(HandshakeError::BadWebsocketKey); - } - let key = { - let key = req.headers().get(header::SEC_WEBSOCKET_KEY).unwrap(); - proto::hash_key(key.as_ref()) - }; - - Ok(HttpResponse::build(StatusCode::SWITCHING_PROTOCOLS) - .connection_type(ConnectionType::Upgrade) - .header(header::UPGRADE, "websocket") - .header(header::TRANSFER_ENCODING, "chunked") - .header(header::SEC_WEBSOCKET_ACCEPT, key.as_str()) - .take()) -} - -/// Maps `Payload` stream into stream of `ws::Message` items -pub struct WsStream { - rx: PayloadBuffer, - closed: bool, - max_size: usize, -} - -impl WsStream -where - S: Stream, -{ - /// Create new websocket frames stream - pub fn new(stream: S) -> WsStream { - WsStream { - rx: PayloadBuffer::new(stream), - closed: false, - max_size: 65_536, - } - } - - /// Set max frame size - /// - /// By default max size is set to 64kb - pub fn max_size(mut self, size: usize) -> Self { - self.max_size = size; - self - } -} - -impl Stream for WsStream -where - S: Stream, -{ - type Item = Message; - type Error = ProtocolError; - - fn poll(&mut self) -> Poll, Self::Error> { - if self.closed { - return Ok(Async::Ready(None)); - } - - match Frame::parse(&mut self.rx, true, self.max_size) { - Ok(Async::Ready(Some(frame))) => { - let (finished, opcode, payload) = frame.unpack(); - - // continuation is not supported - if !finished { - self.closed = true; - return Err(ProtocolError::NoContinuation); - } - - match opcode { - OpCode::Continue => Err(ProtocolError::NoContinuation), - OpCode::Bad => { - self.closed = true; - Err(ProtocolError::BadOpCode) - } - OpCode::Close => { - self.closed = true; - let close_reason = Frame::parse_close_payload(&payload); - Ok(Async::Ready(Some(Message::Close(close_reason)))) - } - OpCode::Ping => Ok(Async::Ready(Some(Message::Ping( - String::from_utf8_lossy(payload.as_ref()).into(), - )))), - OpCode::Pong => Ok(Async::Ready(Some(Message::Pong( - String::from_utf8_lossy(payload.as_ref()).into(), - )))), - OpCode::Binary => Ok(Async::Ready(Some(Message::Binary(payload)))), - OpCode::Text => { - let tmp = Vec::from(payload.as_ref()); - match String::from_utf8(tmp) { - Ok(s) => Ok(Async::Ready(Some(Message::Text(s)))), - Err(_) => { - self.closed = true; - Err(ProtocolError::BadEncoding) - } - } - } - } - } - Ok(Async::Ready(None)) => Ok(Async::Ready(None)), - Ok(Async::NotReady) => Ok(Async::NotReady), - Err(e) => { - self.closed = true; - Err(e) - } - } - } -} - -/// Common writing methods for a websocket. -pub trait WsWriter { - /// Send a text - fn send_text>(&mut self, text: T); - /// Send a binary - fn send_binary>(&mut self, data: B); - /// Send a ping message - fn send_ping(&mut self, message: &str); - /// Send a pong message - fn send_pong(&mut self, message: &str); - /// Close the connection - fn send_close(&mut self, reason: Option); -} - -#[cfg(test)] -mod tests { - use super::*; - use http::{header, Method}; - use test::TestRequest; - - #[test] - fn test_handshake() { - let req = TestRequest::default().method(Method::POST).finish(); - assert_eq!( - HandshakeError::GetMethodRequired, - handshake(&req).err().unwrap() - ); - - let req = TestRequest::default().finish(); - assert_eq!( - HandshakeError::NoWebsocketUpgrade, - handshake(&req).err().unwrap() - ); - - let req = TestRequest::default() - .header(header::UPGRADE, header::HeaderValue::from_static("test")) - .finish(); - assert_eq!( - HandshakeError::NoWebsocketUpgrade, - handshake(&req).err().unwrap() - ); - - let req = TestRequest::default() - .header( - header::UPGRADE, - header::HeaderValue::from_static("websocket"), - ).finish(); - assert_eq!( - HandshakeError::NoConnectionUpgrade, - handshake(&req).err().unwrap() - ); - - let req = TestRequest::default() - .header( - header::UPGRADE, - header::HeaderValue::from_static("websocket"), - ).header( - header::CONNECTION, - header::HeaderValue::from_static("upgrade"), - ).finish(); - assert_eq!( - HandshakeError::NoVersionHeader, - handshake(&req).err().unwrap() - ); - - let req = TestRequest::default() - .header( - header::UPGRADE, - header::HeaderValue::from_static("websocket"), - ).header( - header::CONNECTION, - header::HeaderValue::from_static("upgrade"), - ).header( - header::SEC_WEBSOCKET_VERSION, - header::HeaderValue::from_static("5"), - ).finish(); - assert_eq!( - HandshakeError::UnsupportedVersion, - handshake(&req).err().unwrap() - ); - - let req = TestRequest::default() - .header( - header::UPGRADE, - header::HeaderValue::from_static("websocket"), - ).header( - header::CONNECTION, - header::HeaderValue::from_static("upgrade"), - ).header( - header::SEC_WEBSOCKET_VERSION, - header::HeaderValue::from_static("13"), - ).finish(); - assert_eq!( - HandshakeError::BadWebsocketKey, - handshake(&req).err().unwrap() - ); - - let req = TestRequest::default() - .header( - header::UPGRADE, - header::HeaderValue::from_static("websocket"), - ).header( - header::CONNECTION, - header::HeaderValue::from_static("upgrade"), - ).header( - header::SEC_WEBSOCKET_VERSION, - header::HeaderValue::from_static("13"), - ).header( - header::SEC_WEBSOCKET_KEY, - header::HeaderValue::from_static("13"), - ).finish(); - assert_eq!( - StatusCode::SWITCHING_PROTOCOLS, - handshake(&req).unwrap().finish().status() - ); - } - - #[test] - fn test_wserror_http_response() { - let resp: HttpResponse = HandshakeError::GetMethodRequired.error_response(); - assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - let resp: HttpResponse = HandshakeError::NoWebsocketUpgrade.error_response(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: HttpResponse = HandshakeError::NoConnectionUpgrade.error_response(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: HttpResponse = HandshakeError::NoVersionHeader.error_response(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: HttpResponse = HandshakeError::UnsupportedVersion.error_response(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: HttpResponse = HandshakeError::BadWebsocketKey.error_response(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - } -} diff --git a/src/ws/proto.rs b/src/ws/proto.rs deleted file mode 100644 index 35fbbe3e..00000000 --- a/src/ws/proto.rs +++ /dev/null @@ -1,318 +0,0 @@ -use base64; -use sha1; -use std::convert::{From, Into}; -use std::fmt; - -use self::OpCode::*; -/// Operation codes as part of rfc6455. -#[derive(Debug, Eq, PartialEq, Clone, Copy)] -pub enum OpCode { - /// Indicates a continuation frame of a fragmented message. - Continue, - /// Indicates a text data frame. - Text, - /// Indicates a binary data frame. - Binary, - /// Indicates a close control frame. - Close, - /// Indicates a ping control frame. - Ping, - /// Indicates a pong control frame. - Pong, - /// Indicates an invalid opcode was received. - Bad, -} - -impl fmt::Display for OpCode { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Continue => write!(f, "CONTINUE"), - Text => write!(f, "TEXT"), - Binary => write!(f, "BINARY"), - Close => write!(f, "CLOSE"), - Ping => write!(f, "PING"), - Pong => write!(f, "PONG"), - Bad => write!(f, "BAD"), - } - } -} - -impl Into for OpCode { - fn into(self) -> u8 { - match self { - Continue => 0, - Text => 1, - Binary => 2, - Close => 8, - Ping => 9, - Pong => 10, - Bad => { - debug_assert!( - false, - "Attempted to convert invalid opcode to u8. This is a bug." - ); - 8 // if this somehow happens, a close frame will help us tear down quickly - } - } - } -} - -impl From for OpCode { - fn from(byte: u8) -> OpCode { - match byte { - 0 => Continue, - 1 => Text, - 2 => Binary, - 8 => Close, - 9 => Ping, - 10 => Pong, - _ => Bad, - } - } -} - -use self::CloseCode::*; -/// Status code used to indicate why an endpoint is closing the `WebSocket` -/// connection. -#[derive(Debug, Eq, PartialEq, Clone, Copy)] -pub enum CloseCode { - /// Indicates a normal closure, meaning that the purpose for - /// which the connection was established has been fulfilled. - Normal, - /// Indicates that an endpoint is "going away", such as a server - /// going down or a browser having navigated away from a page. - Away, - /// Indicates that an endpoint is terminating the connection due - /// to a protocol error. - Protocol, - /// Indicates that an endpoint is terminating the connection - /// because it has received a type of data it cannot accept (e.g., an - /// endpoint that understands only text data MAY send this if it - /// receives a binary message). - Unsupported, - /// Indicates an abnormal closure. If the abnormal closure was due to an - /// error, this close code will not be used. Instead, the `on_error` method - /// of the handler will be called with the error. However, if the connection - /// is simply dropped, without an error, this close code will be sent to the - /// handler. - Abnormal, - /// Indicates that an endpoint is terminating the connection - /// because it has received data within a message that was not - /// consistent with the type of the message (e.g., non-UTF-8 [RFC3629] - /// data within a text message). - Invalid, - /// Indicates that an endpoint is terminating the connection - /// because it has received a message that violates its policy. This - /// is a generic status code that can be returned when there is no - /// other more suitable status code (e.g., Unsupported or Size) or if there - /// is a need to hide specific details about the policy. - Policy, - /// Indicates that an endpoint is terminating the connection - /// because it has received a message that is too big for it to - /// process. - Size, - /// Indicates that an endpoint (client) is terminating the - /// connection because it has expected the server to negotiate one or - /// more extension, but the server didn't return them in the response - /// message of the WebSocket handshake. The list of extensions that - /// are needed should be given as the reason for closing. - /// Note that this status code is not used by the server, because it - /// can fail the WebSocket handshake instead. - Extension, - /// Indicates that a server is terminating the connection because - /// it encountered an unexpected condition that prevented it from - /// fulfilling the request. - Error, - /// Indicates that the server is restarting. A client may choose to - /// reconnect, and if it does, it should use a randomized delay of 5-30 - /// seconds between attempts. - Restart, - /// Indicates that the server is overloaded and the client should either - /// connect to a different IP (when multiple targets exist), or - /// reconnect to the same IP when a user has performed an action. - Again, - #[doc(hidden)] - Tls, - #[doc(hidden)] - Other(u16), -} - -impl Into for CloseCode { - fn into(self) -> u16 { - match self { - Normal => 1000, - Away => 1001, - Protocol => 1002, - Unsupported => 1003, - Abnormal => 1006, - Invalid => 1007, - Policy => 1008, - Size => 1009, - Extension => 1010, - Error => 1011, - Restart => 1012, - Again => 1013, - Tls => 1015, - Other(code) => code, - } - } -} - -impl From for CloseCode { - fn from(code: u16) -> CloseCode { - match code { - 1000 => Normal, - 1001 => Away, - 1002 => Protocol, - 1003 => Unsupported, - 1006 => Abnormal, - 1007 => Invalid, - 1008 => Policy, - 1009 => Size, - 1010 => Extension, - 1011 => Error, - 1012 => Restart, - 1013 => Again, - 1015 => Tls, - _ => Other(code), - } - } -} - -#[derive(Debug, Eq, PartialEq, Clone)] -/// Reason for closing the connection -pub struct CloseReason { - /// Exit code - pub code: CloseCode, - /// Optional description of the exit code - pub description: Option, -} - -impl From for CloseReason { - fn from(code: CloseCode) -> Self { - CloseReason { - code, - description: None, - } - } -} - -impl> From<(CloseCode, T)> for CloseReason { - fn from(info: (CloseCode, T)) -> Self { - CloseReason { - code: info.0, - description: Some(info.1.into()), - } - } -} - -static WS_GUID: &'static str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - -// TODO: hash is always same size, we dont need String -pub(crate) fn hash_key(key: &[u8]) -> String { - let mut hasher = sha1::Sha1::new(); - - hasher.update(key); - hasher.update(WS_GUID.as_bytes()); - - base64::encode(&hasher.digest().bytes()) -} - -#[cfg(test)] -mod test { - #![allow(unused_imports, unused_variables, dead_code)] - use super::*; - - macro_rules! opcode_into { - ($from:expr => $opcode:pat) => { - match OpCode::from($from) { - e @ $opcode => (), - e => unreachable!("{:?}", e), - } - }; - } - - macro_rules! opcode_from { - ($from:expr => $opcode:pat) => { - let res: u8 = $from.into(); - match res { - e @ $opcode => (), - e => unreachable!("{:?}", e), - } - }; - } - - #[test] - fn test_to_opcode() { - opcode_into!(0 => OpCode::Continue); - opcode_into!(1 => OpCode::Text); - opcode_into!(2 => OpCode::Binary); - opcode_into!(8 => OpCode::Close); - opcode_into!(9 => OpCode::Ping); - opcode_into!(10 => OpCode::Pong); - opcode_into!(99 => OpCode::Bad); - } - - #[test] - fn test_from_opcode() { - opcode_from!(OpCode::Continue => 0); - opcode_from!(OpCode::Text => 1); - opcode_from!(OpCode::Binary => 2); - opcode_from!(OpCode::Close => 8); - opcode_from!(OpCode::Ping => 9); - opcode_from!(OpCode::Pong => 10); - } - - #[test] - #[should_panic] - fn test_from_opcode_debug() { - opcode_from!(OpCode::Bad => 99); - } - - #[test] - fn test_from_opcode_display() { - assert_eq!(format!("{}", OpCode::Continue), "CONTINUE"); - assert_eq!(format!("{}", OpCode::Text), "TEXT"); - assert_eq!(format!("{}", OpCode::Binary), "BINARY"); - assert_eq!(format!("{}", OpCode::Close), "CLOSE"); - assert_eq!(format!("{}", OpCode::Ping), "PING"); - assert_eq!(format!("{}", OpCode::Pong), "PONG"); - assert_eq!(format!("{}", OpCode::Bad), "BAD"); - } - - #[test] - fn closecode_from_u16() { - assert_eq!(CloseCode::from(1000u16), CloseCode::Normal); - assert_eq!(CloseCode::from(1001u16), CloseCode::Away); - assert_eq!(CloseCode::from(1002u16), CloseCode::Protocol); - assert_eq!(CloseCode::from(1003u16), CloseCode::Unsupported); - assert_eq!(CloseCode::from(1006u16), CloseCode::Abnormal); - assert_eq!(CloseCode::from(1007u16), CloseCode::Invalid); - assert_eq!(CloseCode::from(1008u16), CloseCode::Policy); - assert_eq!(CloseCode::from(1009u16), CloseCode::Size); - assert_eq!(CloseCode::from(1010u16), CloseCode::Extension); - assert_eq!(CloseCode::from(1011u16), CloseCode::Error); - assert_eq!(CloseCode::from(1012u16), CloseCode::Restart); - assert_eq!(CloseCode::from(1013u16), CloseCode::Again); - assert_eq!(CloseCode::from(1015u16), CloseCode::Tls); - assert_eq!(CloseCode::from(2000u16), CloseCode::Other(2000)); - } - - #[test] - fn closecode_into_u16() { - assert_eq!(1000u16, Into::::into(CloseCode::Normal)); - assert_eq!(1001u16, Into::::into(CloseCode::Away)); - assert_eq!(1002u16, Into::::into(CloseCode::Protocol)); - assert_eq!(1003u16, Into::::into(CloseCode::Unsupported)); - assert_eq!(1006u16, Into::::into(CloseCode::Abnormal)); - assert_eq!(1007u16, Into::::into(CloseCode::Invalid)); - assert_eq!(1008u16, Into::::into(CloseCode::Policy)); - assert_eq!(1009u16, Into::::into(CloseCode::Size)); - assert_eq!(1010u16, Into::::into(CloseCode::Extension)); - assert_eq!(1011u16, Into::::into(CloseCode::Error)); - assert_eq!(1012u16, Into::::into(CloseCode::Restart)); - assert_eq!(1013u16, Into::::into(CloseCode::Again)); - assert_eq!(1015u16, Into::::into(CloseCode::Tls)); - assert_eq!(2000u16, Into::::into(CloseCode::Other(2000))); - } -} diff --git a/tests/identity.pfx b/tests/identity.pfx deleted file mode 100644 index 946e3b8b8ae10e19a11e7ac6eead66b12fff0014..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5549 zcmY+GRZtv&vTbn*Fa-DD5S(Fv;O=h0gF_fx!{8nycyM=jcXvr}cXtMNxH(n#z4P8j zS68jQyT2EE0A2|kEIfMvo;?yO<4>8N_ZYCqu-O54MhF3T`v0&tdjMMWe%lL7KUFAFV5{u;owkU`~uKqm}O*fBSo5RVYIG` zPAKF6ePO1*(FhW)-QpFAvtO?cVWGD0hs0WO4>qy>9T48E19Q`LuD0r#V<$Vj_c2yp z)4}gHs(AF<^Bo{R_`YId+=%k!c;why`_I0GXxv)FEmD)RG7Vs?g`z@xC^k*0+`Ps8 zZQH$QkvVVeOE(P8=g*0kvj|2!A! zCaUuJ^XO0NPlE$=oQRK9ti`Ic7JJnq;osnZ4OA`G7m{`HKP{cH&`YLS z&7cXaq1TKaOG&uTJqqY25!-%6M82yrFSuJv{`JhJUOE4ZMT1J%h8u3pSuQ$qle0}C z2}0Lk)3OpsFm1yeJ1|XW9gt0oX1PIiJ+HG_ccQbIH}C6hb|aPpRO(@Rt|u~DJIX%l zC+^x;>&_>s!^v1hwDp-biu2rspT|oc!3;MUJ>ui2i(Lamzq&B9OCcN`j&N`^wTcd1 z`DFH$T83h&1Ws&{N(khR##{-N3RSG@_ZPyM3SQ_TAXRRqyV#LUkE0&kEvE8Y61{fKJ+Y= z$eQ*1oOaeF^Z9A#-r@E7LmI)arBxsyT>V+(Ruq)&tBqzOi1sjBwNjPe_jraq(=2r^ zB|%@2bxrAnZr&d!&MNXZn&!iHPAs)aINufHy*7>&8Ap}7vY+Ji590{9Gq+KBpjkN0 zeCWDYHbCrNc%062ljm(+!6eM>ku}@T8$$TYjIT%~Y3z>YH2FxhEJ6lUL_#W3!=?ks zFo969*bgk}a!GRqXMSjjgd&?6Y%hBVuhL~74jyLlXNnJzvi?lWEo_HD7ZkXXB+vYf z**da2rqVV>3tDz^j`grt{G5Ye8`Q@KgQelPxGF%zLkZ=m7N>eor9pQmK6B$F&s9TaFeP0w?b5Q2p`A;be~m;x!mwj;I4ye%g!Op2Z^tb4S}i z#o}zOP0;F#mw3YEF_5DmxuBJQ{MZN!`^i6PwkgfA(n$bs9PQx6aZB5p$2&d$+`VjO zsH)tO=SExUR825&T#?}~JqRDb1{y6YsTMn zYINH#Ru0n1kxJc|=rVjd@`uh*nzIv3n@lt8^$P6GC5xIW1?7XLu5&GtX$Ho%rp5@a z<;*{8D{Rd@ocNX}STxWU(2moCw*4dI^{&HccPIwaXF^%V^P@gTFF*ow(`z_5$lNR7B+?<1E> zULlb}pDx~mAAb5{pg@(IH|PW09Pz6r7!}<9>(fAn8=3nRL|EV^f5o-!1D@`uY_E0= zXWciaPcKF60&?K+MAnU0gz}SuZb>wVAeRY>Lz9(&Zu_rB_JiKSV8HOqO3;vSD41G~ zIYpO4JcJ5K0+;O6x0VlNvNY%;v*XCt2V>-|%pxixP0jfH+xE^8^D3ZXs zS4AQ;$|oz)t4F{7(j7F7l{R}lJhwq)ekCh$|7`g4CXbIghAm6OSZ1L&K5-Y1w$mQa z_MBdP-vu45dbf@y=ls^2$D{4YS(f+0N}W49&$hLSRWl_WZB;uN`i7GQSU2{> z%tc5FX16D4Hc~M^2Q-}r|Th>@2!HT<`AbzLpW6XWl;-{}i)bscjNo9~Z zt>K*BFR$SlC#sBVi?$v)Y?HUyl)E2s`8W6ZXD9n+OhlM3@csT_+o|7G5+{-kChNBE zGoihxT`ld2`0D_n8Kx;B37>D}D7${tb4|NGV4*rXJY< zrP!d-L?ATkL~#^D#VB&5mZjsJsQ(E>4kqY74x%)7DdB0tEho2OB+t;4_6jhwgA|cO znS?z?-;5Y{h$UXY8kALQOM~}sKX{KB4C#6j4aK&Qk9w{6xr6GoWSrQBZ-V^FUe|IuEu|!Ho zP~_-_Q3K`aH^E3h7qd#rjmRUmA*ZPMaLSD!4zIf>se1cE%=z*bSCvx#B3|QUR$0#m9GAZ3L&y-$F1DIC& zT8gc<4SckFy#eY%&9U^Kz5>cD`L{z)t6x+lxMS@K!T}DzX*QtVExm3-I%(wjkMP#> z1(Cx)JgRslsAW|p`13(`X}#T<>eipuNW4PA4R2-@N+Vc&(y87pk_BSeS+d6)P$^TP z`?$x%WPFPWh9eb*vW#gb#gty{p~gAkXo)>T5Uf}c#r*~bw?jGhA9M7i8I5Xoby}f^ z^p4Qzi_ghn7*)HR6CmO5t+f(DYnCA3GX_!BFzYWFViu9Jn~Rh{W4lS>Ien~yruGbu z>(;O4jayyFSMEP1yQ9A{eQ^Hh(X@0+auu!ON=}-efn0d?U5LMh$zI#vo9nqvQ$058 z0%`dSn#OpIe|FctVo1fv^b^C(=MqC(usOYfc9zoBNAJEY_F!S;aKK8MEaXZ2?J0*q z%9PBkwW53|md4e|UjFDs!BwW$KTztoZy9^)QA!(U)itkV!h$GWP*eYJRt+x@x5V%i z1J7`#SEWSE_@>C3i32lz2g3U@e}8X+mAz+L`pS5%*YB7Dxr&GE4t*>KB6I zE-@2=x9^2U&Ux`Z5<+-#U+{a@#CzMU?Leqv9AhmP6iaWp>Oq!NsxHbS=EW1!zJLz{ zMUGYEB7n6q3Er#o<|Z`u0MwrUN4&EGP-_taP%Ho8(tlHkg!X?l`~xi9ztHXDeK*m&V~nDC|pLVujbH(b=fk1EfnQhXPjX1C48B*(7^BO>cFO~ zAH*SPpf#PD###N!E9oD!h87(U5h5>lHJLd?LymUDW$S z^HqD#EbaAcgn)aq3MVH(f*KmTMjHO5dW|_+katLspc=S!v^H3wli*n6ojf^qf3K=y zKr%Yay%^*Xvh0;}J@-TW@bcRzax<}Q{(@FjI+hfy@)|L*hscS(TttTvz_%Nv+(z8GGr?Ic>si}%oD zjS`B@EE+;f!(VuH>B7DBvpXijW%<{YjhQlo9fhwvuO{nH%@)BNiq<1`YY&2FCcm1v z)V&H!Kcj^3Bt0y%<I%;IH=P zJ41oky-lS{I9?XOY~Id^5UX<7RGwt!+GLyLENxHkj-snrrD9wg(faIi6I{jBh1go{ zM*ViN9ui7wip-=5;2)#r3!%>8z?BacE<1_@ALhkFNVIX%-ABNFWSr=jiLQ6%-{KRS zWwq<$wd1t!5~>t;g)EsTUt{vq;Fq-VE-~ml6ZXxgTNfUz_@@T7 zjsLv05k4~6I(7sOoX;Me9Mx*!2n`*&G62*4lL#eRXMj)}# zKr96@p5g@T(q@%oV#NRfPt?05WUo1wQk&aO{Pmwj7rdpbgcv6`Z78Sujv^Cw!}t9r`^Lc8t$WvLza7iBfkmc zDDY9Tit~6VzqS0L=Ayqpm%buj8Ag(=8<}4__o+jk*7sRDS~t>ecLxvu9W`08b;!5* zVykt!s+BvMXq)=q=y6Z4d=}r*-a;}j-*Qyy?4M4cOLy-!>N4}f4H%^$+ju=$d;iJ7 zPw8!Js)`{-TTglxv%Sm^K45gm#!5u2ni5W%dv9_+kEJG*<)*>LnTIO3 zP6HOD&&FrUjtF+byLCKHD-ACb%_vi{l0xPj|E3=MNnc)wn90{jb#I)vL$jsxtHlPu zCN(WOd)y*If;(Pc2D_T*H8TGC6#FjyA&L=1eB$U3^IK?5o$tR7-J`4e7hlpb7w~C+Agyessj{qzudSzQHf)p zE3bG;_o5JE@|R;^XOzYSMRqL`ey& z7cxd^&=J@~sxSdNJ~4`WKG3Pf5(-q%1&@Bp@h^T2p}7IVJL}{Ir#}ZYrY(`qOdx+V z!4-keXG9fPwZ72Z>0InsQ!U51)YwT!#5(o7Q73y)O7Vj^xant+Mw#45BH$9?B1vV- zK-S5Bmx_T9S^3JTc)fx@Q&R72RSU{u74roB{&3hlzknbj{dy%m?DxD5GH*40Tk$q; zDxcEj{-~XXePdPnzs!=S2lkV$IweaW8lBM@z7iZlAu5N%pYus=+D*DQyFs=GYfXl#{yYa^Ur z^wdzws}mB6Y~FeN1FIrbx)Pe=LH~}x^wgbl7Mnn)HlS|~MKWejJ*=#vF+_%p7!)FC z*>GliQit=X=~)<^UO-kl>$hKAhy@PbN%T6E8AFDRIPqP?3nKbu9SC1*x-_q%oa>un z)jigLjSg;Ie=Sf4&{X0(O6asc*}f}KP)&dmbcEmjIu<#h|51e3-$TXpM$ACpy4wx^ z7~&b%^6mSNlP@DXnZzWuf4O5+N;qfkl$8#UJw?Z-lL=nl=k*Qj5K!X_jNe5*ceN|1 zJO(G;HL??jza#IvW7CL&O%M}9wD`Q<7M?twO`H?+^WCB~JG8Y5`NHaesh58pW(2{G0QEPc8si#*pBZg zbmb#%Lx#-6A2aE}_`5AxI8xhZ#FL@NXPYMwK!#-*abrgDL>W=V)NBx_S5!i=ATh~B zoT!rujGkse$2dK9!A$E8ILE})=TNqjus$Y5VZgwNlvHx9Q@B&7X378MKMHcNc9XmD z{1dA1N8T7+{i%3V@mO!?q$Lgg=zTVwG9vk)dN9s4cwe diff --git a/tests/test space.binary b/tests/test space.binary deleted file mode 100644 index ef8ff024..00000000 --- a/tests/test space.binary +++ /dev/null @@ -1 +0,0 @@ -ÂTÇ‘É‚Vù2þvI ª–\ÇRË™–ˆæeÞvDØ:è—½¬RVÖYpíÿ;ÍÏGñùp!2÷CŒ.– û®õpA !ûߦÙx j+Uc÷±©X”c%Û;ï"yì­AI \ No newline at end of file diff --git a/tests/test.binary b/tests/test.binary deleted file mode 100644 index ef8ff024..00000000 --- a/tests/test.binary +++ /dev/null @@ -1 +0,0 @@ -ÂTÇ‘É‚Vù2þvI ª–\ÇRË™–ˆæeÞvDØ:è—½¬RVÖYpíÿ;ÍÏGñùp!2÷CŒ.– û®õpA !ûߦÙx j+Uc÷±©X”c%Û;ï"yì­AI \ No newline at end of file diff --git a/tests/test.png b/tests/test.png deleted file mode 100644 index 6b7cdc0b8fb5a439d3bd19f210e89a37a2354e61..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 168 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1SFYWcSQjy&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=h((rL&%EBHDibIqn;8;O;+&tGo0?Yw HttpResponse::Ok().finish(), - None => HttpResponse::BadRequest().finish(), - }) - }); - - let request = srv.get().uri(srv.url("/?qp=5").as_str()).finish().unwrap(); - - let response = srv.execute(request.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))); - - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - 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())); - - // POST - let request = srv.post().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - - 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())); -} - -#[test] -fn test_client_gzip_encoding() { - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Deflate) - .body(bytes)) - }).responder() - }) - }); - - // client request - let request = srv - .post() - .content_encoding(http::ContentEncoding::Gzip) - .body(STR) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_client_gzip_encoding_large() { - let data = STR.repeat(10); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Deflate) - .body(bytes)) - }).responder() - }) - }); - - // client request - let request = srv - .post() - .content_encoding(http::ContentEncoding::Gzip) - .body(data.clone()) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - -#[test] -fn test_client_gzip_encoding_large_random() { - let data = rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(100_000) - .collect::(); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Deflate) - .body(bytes)) - }).responder() - }) - }); - - // client request - let request = srv - .post() - .content_encoding(http::ContentEncoding::Gzip) - .body(data.clone()) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - -#[cfg(all(unix, feature = "uds"))] -#[test] -fn test_compatible_with_unix_socket_stream() { - let (stream, _) = tokio_uds::UnixStream::pair().unwrap(); - let _ = client::Connection::from_stream(stream); -} - -#[cfg(feature = "brotli")] -#[test] -fn test_client_brotli_encoding() { - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Gzip) - .body(bytes)) - }).responder() - }) - }); - - // client request - let request = srv - .client(http::Method::POST, "/") - .content_encoding(http::ContentEncoding::Br) - .body(STR) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[cfg(feature = "brotli")] -#[test] -fn test_client_brotli_encoding_large_random() { - let data = rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(70_000) - .collect::(); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(move |bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Gzip) - .body(bytes)) - }).responder() - }) - }); - - // client request - let request = srv - .client(http::Method::POST, "/") - .content_encoding(http::ContentEncoding::Br) - .body(data.clone()) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes.len(), data.len()); - assert_eq!(bytes, Bytes::from(data)); -} - -#[cfg(feature = "brotli")] -#[test] -fn test_client_deflate_encoding() { - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Br) - .body(bytes)) - }).responder() - }) - }); - - // client request - let request = srv - .post() - .content_encoding(http::ContentEncoding::Deflate) - .body(STR) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[cfg(feature = "brotli")] -#[test] -fn test_client_deflate_encoding_large_random() { - let data = rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(70_000) - .collect::(); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Br) - .body(bytes)) - }).responder() - }) - }); - - // client request - let request = srv - .post() - .content_encoding(http::ContentEncoding::Deflate) - .body(data.clone()) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - -#[test] -fn test_client_streaming_explicit() { - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .map_err(Error::from) - .and_then(|body| { - Ok(HttpResponse::Ok() - .chunked() - .content_encoding(http::ContentEncoding::Identity) - .body(body)) - }).responder() - }) - }); - - let body = once(Ok(Bytes::from_static(STR.as_ref()))); - - let request = srv.get().body(Body::Streaming(Box::new(body))).unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_body_streaming_implicit() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - let body = once(Ok(Bytes::from_static(STR.as_ref()))); - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Gzip) - .body(Body::Streaming(Box::new(body))) - }) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[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 - Error::from(IoError::from(ErrorKind::NotFound)) - } - let cookie1 = Cookie::build("cookie1", "value1").finish(); - let cookie2 = Cookie::build("cookie2", "value2") - .domain("www.example.org") - .path("/") - .secure(true) - .http_only(true) - .finish(); - // Q: are all these clones really necessary? A: Yes, possibly - let cookie1b = cookie1.clone(); - let cookie2b = cookie2.clone(); - let mut srv = test::TestServer::new(move |app| { - let cookie1 = cookie1b.clone(); - let cookie2 = cookie2b.clone(); - app.handler(move |req: &HttpRequest| { - // Check cookies were sent correctly - req.cookie("cookie1") - .ok_or_else(err) - .and_then(|c1| { - if c1.value() == "value1" { - Ok(()) - } else { - Err(err()) - } - }).and_then(|()| req.cookie("cookie2").ok_or_else(err)) - .and_then(|c2| { - if c2.value() == "value2" { - Ok(()) - } else { - Err(err()) - } - }) - // Send some cookies back - .map(|_| { - HttpResponse::Ok() - .cookie(cookie1.clone()) - .cookie(cookie2.clone()) - .finish() - }) - }) - }); - - let request = srv - .get() - .cookie(cookie1.clone()) - .cookie(cookie2.clone()) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - let c1 = response.cookie("cookie1").expect("Missing cookie1"); - assert_eq!(c1, cookie1); - let c2 = response.cookie("cookie2").expect("Missing cookie2"); - 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(); - - thread::spawn(move || { - let lst = net::TcpListener::bind(addr).unwrap(); - - for stream in lst.incoming() { - let mut stream = stream.unwrap(); - let mut b = [0; 1000]; - let _ = stream.read(&mut b).unwrap(); - let _ = stream - .write_all(b"HTTP/1.1 200 OK\r\nconnection: close\r\n\r\nwelcome!"); - } - }); - - let mut sys = actix::System::new("test"); - - // client request - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = sys.block_on(req.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = sys.block_on(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"welcome!")); -} diff --git a/tests/test_custom_pipeline.rs b/tests/test_custom_pipeline.rs deleted file mode 100644 index 6b5df00e..00000000 --- a/tests/test_custom_pipeline.rs +++ /dev/null @@ -1,81 +0,0 @@ -extern crate actix; -extern crate actix_net; -extern crate actix_web; - -use std::{thread, time}; - -use actix::System; -use actix_net::server::Server; -use actix_net::service::NewServiceExt; -use actix_web::server::{HttpService, KeepAlive, ServiceConfig, StreamConfiguration}; -use actix_web::{client, http, test, App, HttpRequest}; - -#[test] -fn test_custom_pipeline() { - let addr = test::TestServer::unused_addr(); - - thread::spawn(move || { - Server::new() - .bind("test", addr, move || { - let app = App::new() - .route("/", http::Method::GET, |_: HttpRequest| "OK") - .finish(); - let settings = ServiceConfig::build(app) - .keep_alive(KeepAlive::Disabled) - .client_timeout(1000) - .client_shutdown(1000) - .server_hostname("localhost") - .server_address(addr) - .finish(); - - StreamConfiguration::new() - .nodelay(true) - .tcp_keepalive(Some(time::Duration::from_secs(10))) - .and_then(HttpService::new(settings)) - }).unwrap() - .run(); - }); - - let mut sys = System::new("test"); - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = sys.block_on(req.send()).unwrap(); - assert!(response.status().is_success()); - } -} - -#[test] -fn test_h1() { - use actix_web::server::H1Service; - - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - Server::new() - .bind("test", addr, move || { - let app = App::new() - .route("/", http::Method::GET, |_: HttpRequest| "OK") - .finish(); - let settings = ServiceConfig::build(app) - .keep_alive(KeepAlive::Disabled) - .client_timeout(1000) - .client_shutdown(1000) - .server_hostname("localhost") - .server_address(addr) - .finish(); - - H1Service::new(settings) - }).unwrap() - .run(); - }); - - let mut sys = System::new("test"); - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = sys.block_on(req.send()).unwrap(); - assert!(response.status().is_success()); - } -} diff --git a/tests/test_handlers.rs b/tests/test_handlers.rs deleted file mode 100644 index debc1626..00000000 --- a/tests/test_handlers.rs +++ /dev/null @@ -1,677 +0,0 @@ -extern crate actix; -extern crate actix_web; -extern crate bytes; -extern crate futures; -extern crate h2; -extern crate http; -extern crate tokio_timer; -#[macro_use] -extern crate serde_derive; -extern crate serde_json; - -use std::io; -use std::time::{Duration, Instant}; - -use actix_web::*; -use bytes::Bytes; -use futures::Future; -use http::StatusCode; -use serde_json::Value; -use tokio_timer::Delay; - -#[derive(Deserialize)] -struct PParam { - username: String, -} - -#[test] -fn test_path_extractor() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.with(|p: Path| format!("Welcome {}!", p.username)) - }); - }); - - // client request - let request = srv.get().uri(srv.url("/test/index.html")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test!")); -} - -#[test] -fn test_async_handler() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with(|p: Path| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| Ok(format!("Welcome {}!", p.username))) - .responder() - }) - }); - }); - - // client request - let request = srv.get().uri(srv.url("/test/index.html")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test!")); -} - -#[test] -fn test_query_extractor() { - let mut srv = test::TestServer::new(|app| { - app.resource("/index.html", |r| { - r.with(|p: Query| format!("Welcome {}!", p.username)) - }); - }); - - // client request - let request = srv - .get() - .uri(srv.url("/index.html?username=test")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test!")); - - // client request - let request = srv.get().uri(srv.url("/index.html")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[derive(Deserialize, Debug)] -pub enum ResponseType { - Token, - Code, -} - -#[derive(Debug, Deserialize)] -pub struct AuthRequest { - id: u64, - response_type: ResponseType, -} - -#[test] -fn test_query_enum_extractor() { - let mut srv = test::TestServer::new(|app| { - app.resource("/index.html", |r| { - r.with(|p: Query| format!("{:?}", p.into_inner())) - }); - }); - - // client request - let request = srv - .get() - .uri(srv.url("/index.html?id=64&response_type=Code")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!( - bytes, - Bytes::from_static(b"AuthRequest { id: 64, response_type: Code }") - ); - - let request = srv - .get() - .uri(srv.url("/index.html?id=64&response_type=Co")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); - - let request = srv - .get() - .uri(srv.url("/index.html?response_type=Code")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[test] -fn test_async_extractor_async() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with(|data: Json| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| Ok(format!("{}", data.0))) - .responder() - }) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"{\"test\":1}")); -} - -#[derive(Deserialize, Serialize)] -struct FormData { - username: String, -} - -#[test] -fn test_form_extractor() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route() - .with(|form: Form| format!("{}", form.username)) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html")) - .form(FormData { - username: "test".to_string(), - }).unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"test")); -} - -#[test] -fn test_form_extractor2() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with_config( - |form: Form| format!("{}", form.username), - |cfg| { - cfg.0.error_handler(|err, _| { - error::InternalError::from_response( - err, - HttpResponse::Conflict().finish(), - ).into() - }); - }, - ); - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html")) - .header("content-type", "application/x-www-form-urlencoded") - .body("918237129hdk:D:D:D:D:D:DjASHDKJhaswkjeq") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_client_error()); -} - -#[test] -fn test_path_and_query_extractor() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with(|(p, q): (Path, Query)| { - format!("Welcome {} - {}!", p.username, q.username) - }) - }); - }); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html?username=test2")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test1 - test2!")); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[test] -fn test_path_and_query_extractor2() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route() - .with(|(_r, p, q): (HttpRequest, Path, Query)| { - format!("Welcome {} - {}!", p.username, q.username) - }) - }); - }); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html?username=test2")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test1 - test2!")); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[test] -fn test_path_and_query_extractor2_async() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with( - |(p, _q, data): (Path, Query, Json)| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| { - Ok(format!("Welcome {} - {}!", p.username, data.0)) - }).responder() - }, - ) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html?username=test2")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test1 - {\"test\":1}!")); -} - -#[test] -fn test_path_and_query_extractor3_async() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with(|(p, data): (Path, Json)| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| { - Ok(format!("Welcome {} - {}!", p.username, data.0)) - }).responder() - }) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); -} - -#[test] -fn test_path_and_query_extractor4_async() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with(|(data, p): (Json, Path)| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| { - Ok(format!("Welcome {} - {}!", p.username, data.0)) - }).responder() - }) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); -} - -#[test] -fn test_path_and_query_extractor2_async2() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with( - |(p, data, _q): (Path, Json, Query)| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| { - Ok(format!("Welcome {} - {}!", p.username, data.0)) - }).responder() - }, - ) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html?username=test2")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test1 - {\"test\":1}!")); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[test] -fn test_path_and_query_extractor2_async3() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route() - .with(|data: Json, p: Path, _: Query| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| { - Ok(format!("Welcome {} - {}!", p.username, data.0)) - }).responder() - }) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html?username=test2")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test1 - {\"test\":1}!")); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[test] -fn test_path_and_query_extractor2_async4() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route() - .with(|data: (Json, Path, Query)| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(move |_| { - Ok(format!("Welcome {} - {}!", data.1.username, (data.0).0)) - }).responder() - }) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html?username=test2")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test1 - {\"test\":1}!")); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[test] -fn test_scope_and_path_extractor() { - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/sc", |scope| { - scope.resource("/{num}/index.html", |r| { - r.route() - .with(|p: Path<(usize,)>| format!("Welcome {}!", p.0)) - }) - }) - }); - - // client request - let request = srv - .get() - .uri(srv.url("/sc/10/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome 10!")); - - // client request - let request = srv - .get() - .uri(srv.url("/sc/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::NOT_FOUND); -} - -#[test] -fn test_nested_scope_and_path_extractor() { - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/sc", |scope| { - scope.nested("/{num}", |scope| { - scope.resource("/{num}/index.html", |r| { - r.route().with(|p: Path<(usize, usize)>| { - format!("Welcome {} {}!", p.0, p.1) - }) - }) - }) - }) - }); - - // client request - let request = srv - .get() - .uri(srv.url("/sc/10/12/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome 10 12!")); - - // client request - let request = srv - .get() - .uri(srv.url("/sc/10/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::NOT_FOUND); -} - -#[cfg(actix_impl_trait)] -fn test_impl_trait( - data: (Json, Path, Query), -) -> impl Future { - Delay::new(Instant::now() + Duration::from_millis(10)) - .map_err(|_| io::Error::new(io::ErrorKind::Other, "timeout")) - .and_then(move |_| Ok(format!("Welcome {} - {}!", data.1.username, (data.0).0))) -} - -#[cfg(actix_impl_trait)] -fn test_impl_trait_err( - _data: (Json, Path, Query), -) -> impl Future { - Delay::new(Instant::now() + Duration::from_millis(10)) - .map_err(|_| io::Error::new(io::ErrorKind::Other, "timeout")) - .and_then(move |_| Err(io::Error::new(io::ErrorKind::Other, "other"))) -} - -#[cfg(actix_impl_trait)] -#[test] -fn test_path_and_query_extractor2_async4_impl_trait() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with_async(test_impl_trait) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html?username=test2")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"Welcome test1 - {\"test\":1}!")); - - // client request - let request = srv - .get() - .uri(srv.url("/test1/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); -} - -#[cfg(actix_impl_trait)] -#[test] -fn test_path_and_query_extractor2_async4_impl_trait_err() { - let mut srv = test::TestServer::new(|app| { - app.resource("/{username}/index.html", |r| { - r.route().with_async(test_impl_trait_err) - }); - }); - - // client request - let request = srv - .post() - .uri(srv.url("/test1/index.html?username=test2")) - .header("content-type", "application/json") - .body("{\"test\": 1}") - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); -} - -#[test] -fn test_non_ascii_route() { - let mut srv = test::TestServer::new(|app| { - app.resource("/中文/index.html", |r| r.f(|_| "success")); - }); - - // client request - let request = srv - .get() - .uri(srv.url("/中文/index.html")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(b"success")); -} - -#[test] -fn test_unsafe_path_route() { - let mut srv = test::TestServer::new(|app| { - app.resource("/test/{url}", |r| { - r.f(|r| format!("success: {}", &r.match_info()["url"])) - }); - }); - - // client request - let request = srv - .get() - .uri(srv.url("/test/http%3A%2F%2Fexample.com")) - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!( - bytes, - Bytes::from_static(b"success: http%3A%2F%2Fexample.com") - ); -} diff --git a/tests/test_middleware.rs b/tests/test_middleware.rs deleted file mode 100644 index 6cb6ee36..00000000 --- a/tests/test_middleware.rs +++ /dev/null @@ -1,1055 +0,0 @@ -extern crate actix; -extern crate actix_web; -extern crate futures; -extern crate tokio_timer; - -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; -use std::thread; -use std::time::{Duration, Instant}; - -use actix_web::error::{Error, ErrorInternalServerError}; -use actix_web::*; -use futures::{future, Future}; -use tokio_timer::Delay; - -struct MiddlewareTest { - start: Arc, - response: Arc, - finish: Arc, -} - -impl middleware::Middleware for MiddlewareTest { - fn start(&self, _: &HttpRequest) -> Result { - self.start - .store(self.start.load(Ordering::Relaxed) + 1, Ordering::Relaxed); - Ok(middleware::Started::Done) - } - - fn response( - &self, _: &HttpRequest, resp: HttpResponse, - ) -> Result { - self.response - .store(self.response.load(Ordering::Relaxed) + 1, Ordering::Relaxed); - Ok(middleware::Response::Done(resp)) - } - - fn finish(&self, _: &HttpRequest, _: &HttpResponse) -> middleware::Finished { - self.finish - .store(self.finish.load(Ordering::Relaxed) + 1, Ordering::Relaxed); - middleware::Finished::Done - } -} - -#[test] -fn test_middleware() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::new(move |app| { - app.middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).handler(|_| HttpResponse::Ok()) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::new(move |app| { - app.middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).handler(|_| HttpResponse::Ok()) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_resource_middleware() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::new(move |app| { - app.middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).handler(|_| HttpResponse::Ok()) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_resource_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::new(move |app| { - app.middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).handler(|_| HttpResponse::Ok()) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_scope_middleware() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/scope", |scope| { - scope - .middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_scope_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/scope", |scope| { - scope - .middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_middleware_async_handler() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new() - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/", |r| { - r.route().a(|_| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(|_| Ok(HttpResponse::Ok())) - }) - }) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - thread::sleep(Duration::from_millis(20)); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_resource_middleware_async_handler() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw = MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().resource("/test", |r| { - r.middleware(mw); - r.route().a(|_| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(|_| Ok(HttpResponse::Ok())) - }) - }) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_scope_middleware_async_handler() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/scope", |scope| { - scope - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| { - r.route().a(|_| { - Delay::new(Instant::now() + Duration::from_millis(10)) - .and_then(|_| Ok(HttpResponse::Ok())) - }) - }) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -fn index_test_middleware_async_error(_: &HttpRequest) -> FutureResponse { - future::result(Err(error::ErrorBadRequest("TEST"))).responder() -} - -#[test] -fn test_middleware_async_error() { - let req = Arc::new(AtomicUsize::new(0)); - let resp = Arc::new(AtomicUsize::new(0)); - let fin = Arc::new(AtomicUsize::new(0)); - - let act_req = Arc::clone(&req); - let act_resp = Arc::clone(&resp); - let act_fin = Arc::clone(&fin); - - let mut srv = test::TestServer::new(move |app| { - app.middleware(MiddlewareTest { - start: Arc::clone(&act_req), - response: Arc::clone(&act_resp), - finish: Arc::clone(&act_fin), - }).handler(index_test_middleware_async_error) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), http::StatusCode::BAD_REQUEST); - - assert_eq!(req.load(Ordering::Relaxed), 1); - assert_eq!(resp.load(Ordering::Relaxed), 1); - assert_eq!(fin.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_scope_middleware_async_error() { - let req = Arc::new(AtomicUsize::new(0)); - let resp = Arc::new(AtomicUsize::new(0)); - let fin = Arc::new(AtomicUsize::new(0)); - - let act_req = Arc::clone(&req); - let act_resp = Arc::clone(&resp); - let act_fin = Arc::clone(&fin); - - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/scope", |scope| { - scope - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_req), - response: Arc::clone(&act_resp), - finish: Arc::clone(&act_fin), - }).resource("/test", |r| r.f(index_test_middleware_async_error)) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), http::StatusCode::BAD_REQUEST); - - assert_eq!(req.load(Ordering::Relaxed), 1); - assert_eq!(resp.load(Ordering::Relaxed), 1); - assert_eq!(fin.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_resource_middleware_async_error() { - let req = Arc::new(AtomicUsize::new(0)); - let resp = Arc::new(AtomicUsize::new(0)); - let fin = Arc::new(AtomicUsize::new(0)); - - let act_req = Arc::clone(&req); - let act_resp = Arc::clone(&resp); - let act_fin = Arc::clone(&fin); - - let mut srv = test::TestServer::with_factory(move || { - let mw = MiddlewareAsyncTest { - start: Arc::clone(&act_req), - response: Arc::clone(&act_resp), - finish: Arc::clone(&act_fin), - }; - - App::new().resource("/test", move |r| { - r.middleware(mw); - r.f(index_test_middleware_async_error); - }) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), http::StatusCode::BAD_REQUEST); - - assert_eq!(req.load(Ordering::Relaxed), 1); - assert_eq!(resp.load(Ordering::Relaxed), 1); - assert_eq!(fin.load(Ordering::Relaxed), 1); -} - -struct MiddlewareAsyncTest { - start: Arc, - response: Arc, - finish: Arc, -} - -impl middleware::Middleware for MiddlewareAsyncTest { - fn start(&self, _: &HttpRequest) -> Result { - let to = Delay::new(Instant::now() + Duration::from_millis(10)); - - let start = Arc::clone(&self.start); - Ok(middleware::Started::Future(Box::new( - to.from_err().and_then(move |_| { - start.fetch_add(1, Ordering::Relaxed); - Ok(None) - }), - ))) - } - - fn response( - &self, _: &HttpRequest, resp: HttpResponse, - ) -> Result { - let to = Delay::new(Instant::now() + Duration::from_millis(10)); - - let response = Arc::clone(&self.response); - Ok(middleware::Response::Future(Box::new( - to.from_err().and_then(move |_| { - response.fetch_add(1, Ordering::Relaxed); - Ok(resp) - }), - ))) - } - - fn finish(&self, _: &HttpRequest, _: &HttpResponse) -> middleware::Finished { - let to = Delay::new(Instant::now() + Duration::from_millis(10)); - - let finish = Arc::clone(&self.finish); - middleware::Finished::Future(Box::new(to.from_err().and_then(move |_| { - finish.fetch_add(1, Ordering::Relaxed); - Ok(()) - }))) - } -} - -#[test] -fn test_async_middleware() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::new(move |app| { - app.middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).handler(|_| HttpResponse::Ok()) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - - thread::sleep(Duration::from_millis(20)); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_async_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new() - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - - thread::sleep(Duration::from_millis(50)); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_async_sync_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new() - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - - thread::sleep(Duration::from_millis(50)); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_async_scope_middleware() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/scope", |scope| { - scope - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - - thread::sleep(Duration::from_millis(20)); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_async_scope_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/scope", |scope| { - scope - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - - thread::sleep(Duration::from_millis(20)); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_async_async_scope_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - App::new().scope("/scope", |scope| { - scope - .middleware(MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).middleware(MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }).resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - - thread::sleep(Duration::from_millis(20)); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_async_resource_middleware() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw = MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().resource("/test", move |r| { - r.middleware(mw); - r.f(|_| HttpResponse::Ok()); - }) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - - thread::sleep(Duration::from_millis(40)); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_async_resource_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - let mw2 = MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().resource("/test", move |r| { - r.middleware(mw1); - r.middleware(mw2); - r.f(|_| HttpResponse::Ok()); - }) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - - thread::sleep(Duration::from_millis(40)); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -#[test] -fn test_async_sync_resource_middleware_multiple() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareAsyncTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - let mw2 = MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().resource("/test", move |r| { - r.middleware(mw1); - r.middleware(mw2); - r.f(|_| HttpResponse::Ok()); - }) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - assert_eq!(num1.load(Ordering::Relaxed), 2); - assert_eq!(num2.load(Ordering::Relaxed), 2); - - thread::sleep(Duration::from_millis(40)); - assert_eq!(num3.load(Ordering::Relaxed), 2); -} - -struct MiddlewareWithErr; - -impl middleware::Middleware for MiddlewareWithErr { - fn start(&self, _: &HttpRequest) -> Result { - Err(ErrorInternalServerError("middleware error")) - } -} - -struct MiddlewareAsyncWithErr; - -impl middleware::Middleware for MiddlewareAsyncWithErr { - fn start(&self, _: &HttpRequest) -> Result { - Ok(middleware::Started::Future(Box::new(future::err( - ErrorInternalServerError("middleware error"), - )))) - } -} - -#[test] -fn test_middleware_chain_with_error() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new() - .middleware(mw1) - .middleware(MiddlewareWithErr) - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - srv.execute(request.send()).unwrap(); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_middleware_async_chain_with_error() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new() - .middleware(mw1) - .middleware(MiddlewareAsyncWithErr) - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - srv.execute(request.send()).unwrap(); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_scope_middleware_chain_with_error() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().scope("/scope", |scope| { - scope - .middleware(mw1) - .middleware(MiddlewareWithErr) - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - srv.execute(request.send()).unwrap(); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_scope_middleware_async_chain_with_error() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().scope("/scope", |scope| { - scope - .middleware(mw1) - .middleware(MiddlewareAsyncWithErr) - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - }) - }); - - let request = srv.get().uri(srv.url("/scope/test")).finish().unwrap(); - srv.execute(request.send()).unwrap(); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_resource_middleware_chain_with_error() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().resource("/test", move |r| { - r.middleware(mw1); - r.middleware(MiddlewareWithErr); - r.f(|_| HttpResponse::Ok()); - }) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - srv.execute(request.send()).unwrap(); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_resource_middleware_async_chain_with_error() { - let num1 = Arc::new(AtomicUsize::new(0)); - let num2 = Arc::new(AtomicUsize::new(0)); - let num3 = Arc::new(AtomicUsize::new(0)); - - let act_num1 = Arc::clone(&num1); - let act_num2 = Arc::clone(&num2); - let act_num3 = Arc::clone(&num3); - - let mut srv = test::TestServer::with_factory(move || { - let mw1 = MiddlewareTest { - start: Arc::clone(&act_num1), - response: Arc::clone(&act_num2), - finish: Arc::clone(&act_num3), - }; - App::new().resource("/test", move |r| { - r.middleware(mw1); - r.middleware(MiddlewareAsyncWithErr); - r.f(|_| HttpResponse::Ok()); - }) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - srv.execute(request.send()).unwrap(); - - assert_eq!(num1.load(Ordering::Relaxed), 1); - assert_eq!(num2.load(Ordering::Relaxed), 1); - assert_eq!(num3.load(Ordering::Relaxed), 1); -} - -#[cfg(feature = "session")] -#[test] -fn test_session_storage_middleware() { - use actix_web::middleware::session::{ - CookieSessionBackend, RequestSession, SessionStorage, - }; - - const SIMPLE_NAME: &'static str = "simple"; - const SIMPLE_PAYLOAD: &'static str = "kantan"; - const COMPLEX_NAME: &'static str = "test"; - const COMPLEX_PAYLOAD: &'static str = "url=https://test.com&generate_204"; - //TODO: investigate how to handle below input - //const COMPLEX_PAYLOAD: &'static str = "FJc%26continue_url%3Dhttp%253A%252F%252Fconnectivitycheck.gstatic.com%252Fgenerate_204"; - - let mut srv = test::TestServer::with_factory(move || { - App::new() - .middleware(SessionStorage::new( - CookieSessionBackend::signed(&[0; 32]).secure(false), - )).resource("/index", move |r| { - r.f(|req| { - let res = req.session().set(COMPLEX_NAME, COMPLEX_PAYLOAD); - assert!(res.is_ok()); - let value = req.session().get::(COMPLEX_NAME); - assert!(value.is_ok()); - let value = value.unwrap(); - assert!(value.is_some()); - assert_eq!(value.unwrap(), COMPLEX_PAYLOAD); - - let res = req.session().set(SIMPLE_NAME, SIMPLE_PAYLOAD); - assert!(res.is_ok()); - let value = req.session().get::(SIMPLE_NAME); - assert!(value.is_ok()); - let value = value.unwrap(); - assert!(value.is_some()); - assert_eq!(value.unwrap(), SIMPLE_PAYLOAD); - - HttpResponse::Ok() - }) - }).resource("/expect_cookie", move |r| { - r.f(|req| { - let _cookies = req.cookies().expect("To get cookies"); - - let value = req.session().get::(SIMPLE_NAME); - assert!(value.is_ok()); - let value = value.unwrap(); - assert!(value.is_some()); - assert_eq!(value.unwrap(), SIMPLE_PAYLOAD); - - let value = req.session().get::(COMPLEX_NAME); - assert!(value.is_ok()); - let value = value.unwrap(); - assert!(value.is_some()); - assert_eq!(value.unwrap(), COMPLEX_PAYLOAD); - - HttpResponse::Ok() - }) - }) - }); - - let request = srv.get().uri(srv.url("/index")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - - assert!(response.headers().contains_key("set-cookie")); - let set_cookie = response.headers().get("set-cookie"); - assert!(set_cookie.is_some()); - let set_cookie = set_cookie.unwrap().to_str().expect("Convert to str"); - - let request = srv - .get() - .uri(srv.url("/expect_cookie")) - .header("cookie", set_cookie.split(';').next().unwrap()) - .finish() - .unwrap(); - - srv.execute(request.send()).unwrap(); -} diff --git a/tests/test_server.rs b/tests/test_server.rs index f3c9bf9d..2d01d270 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -1,48 +1,18 @@ -extern crate actix; -extern crate actix_net; -extern crate actix_web; -#[cfg(feature = "brotli")] -extern crate brotli2; -extern crate bytes; -extern crate flate2; -extern crate futures; -extern crate h2; -extern crate http as modhttp; -extern crate rand; -extern crate tokio; -extern crate tokio_current_thread; -extern crate tokio_current_thread as current_thread; -extern crate tokio_reactor; -extern crate tokio_tcp; - -#[cfg(feature = "tls")] -extern crate native_tls; -#[cfg(feature = "ssl")] -extern crate openssl; -#[cfg(feature = "rust-tls")] -extern crate rustls; - use std::io::{Read, Write}; -use std::sync::Arc; -use std::{thread, time}; -#[cfg(feature = "brotli")] -use brotli2::write::{BrotliDecoder, BrotliEncoder}; -use bytes::{Bytes, BytesMut}; +use actix_http::http::header::{ + ContentEncoding, ACCEPT_ENCODING, CONTENT_LENGTH, TRANSFER_ENCODING, +}; +use actix_http::{h1, Error, HttpMessage, Response}; +use actix_http_test::TestServer; +use brotli2::write::BrotliDecoder; +use bytes::Bytes; use flate2::read::GzDecoder; -use flate2::write::{GzEncoder, ZlibDecoder, ZlibEncoder}; -use flate2::Compression; -use futures::stream::once; -use futures::{Future, Stream}; -use h2::client as h2client; -use modhttp::Request; -use rand::distributions::Alphanumeric; -use rand::Rng; -use tokio::runtime::current_thread::Runtime; -use tokio_current_thread::spawn; -use tokio_tcp::TcpStream; +use flate2::write::ZlibDecoder; +use futures::stream::once; //Future, Stream +use rand::{distributions::Alphanumeric, Rng}; -use actix_web::*; +use actix_web2::{middleware, App}; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ @@ -66,240 +36,35 @@ 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"; -#[test] -#[cfg(unix)] -fn test_start() { - use actix::System; - use std::sync::mpsc; - - let _ = test::TestServer::unused_addr(); - let (tx, rx) = mpsc::channel(); - - thread::spawn(|| { - System::run(move || { - let srv = server::new(|| { - vec![App::new().resource("/", |r| { - r.method(http::Method::GET).f(|_| HttpResponse::Ok()) - })] - }); - - let srv = srv.bind("127.0.0.1:0").unwrap(); - let addr = srv.addrs()[0]; - let srv_addr = srv.start(); - let _ = tx.send((addr, srv_addr, System::current())); - }); - }); - let (addr, srv_addr, sys) = rx.recv().unwrap(); - System::set_current(sys.clone()); - - let mut rt = Runtime::new().unwrap(); - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = rt.block_on(req.send()).unwrap(); - assert!(response.status().is_success()); - } - - // pause - let _ = srv_addr.send(server::PauseServer).wait(); - thread::sleep(time::Duration::from_millis(200)); - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .timeout(time::Duration::from_millis(200)) - .finish() - .unwrap(); - assert!(rt.block_on(req.send()).is_err()); - } - - // resume - let _ = srv_addr.send(server::ResumeServer).wait(); - thread::sleep(time::Duration::from_millis(200)); - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = rt.block_on(req.send()).unwrap(); - assert!(response.status().is_success()); - } - - let _ = sys.stop(); -} - -#[test] -#[cfg(unix)] -fn test_shutdown() { - use actix::System; - use std::net; - use std::sync::mpsc; - - let _ = test::TestServer::unused_addr(); - let (tx, rx) = mpsc::channel(); - - thread::spawn(|| { - System::run(move || { - let srv = server::new(|| { - vec![App::new().resource("/", |r| { - r.method(http::Method::GET).f(|_| HttpResponse::Ok()) - })] - }); - - let srv = srv.bind("127.0.0.1:0").unwrap(); - let addr = srv.addrs()[0]; - let srv_addr = srv.shutdown_timeout(1).start(); - let _ = tx.send((addr, srv_addr, System::current())); - }); - }); - let (addr, srv_addr, sys) = rx.recv().unwrap(); - System::set_current(sys.clone()); - - let mut rt = Runtime::new().unwrap(); - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = rt.block_on(req.send()).unwrap(); - srv_addr.do_send(server::StopServer { graceful: true }); - assert!(response.status().is_success()); - } - - thread::sleep(time::Duration::from_millis(1000)); - assert!(net::TcpStream::connect(addr).is_err()); - - let _ = sys.stop(); -} - -#[test] -#[cfg(unix)] -fn test_panic() { - use actix::System; - use std::sync::mpsc; - - let _ = test::TestServer::unused_addr(); - let (tx, rx) = mpsc::channel(); - - thread::spawn(|| { - System::run(move || { - let srv = server::new(|| { - App::new() - .resource("/panic", |r| { - r.method(http::Method::GET).f(|_| -> &'static str { - panic!("error"); - }); - }).resource("/", |r| { - r.method(http::Method::GET).f(|_| HttpResponse::Ok()) - }) - }).workers(1); - - let srv = srv.bind("127.0.0.1:0").unwrap(); - let addr = srv.addrs()[0]; - srv.start(); - let _ = tx.send((addr, System::current())); - }); - }); - let (addr, sys) = rx.recv().unwrap(); - System::set_current(sys.clone()); - - let mut rt = Runtime::new().unwrap(); - { - let req = client::ClientRequest::get(format!("http://{}/panic", addr).as_str()) - .finish() - .unwrap(); - let response = rt.block_on(req.send()); - assert!(response.is_err()); - } - - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = rt.block_on(req.send()); - assert!(response.is_err()); - } - { - let req = client::ClientRequest::get(format!("http://{}/", addr).as_str()) - .finish() - .unwrap(); - let response = rt.block_on(req.send()).unwrap(); - assert!(response.status().is_success()); - } - - let _ = sys.stop(); -} - -#[test] -fn test_simple() { - let mut srv = test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok())); - let req = srv.get().finish().unwrap(); - let response = srv.execute(req.send()).unwrap(); - assert!(response.status().is_success()); -} - -#[test] -fn test_headers() { - let data = STR.repeat(10); - let srv_data = Arc::new(data.clone()); - let mut srv = test::TestServer::new(move |app| { - let data = srv_data.clone(); - app.handler(move |_| { - let mut builder = HttpResponse::Ok(); - for idx in 0..90 { - builder.header( - format!("X-TEST-{}", idx).as_str(), - "TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST \ - TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", - ); - } - builder.body(data.as_ref()) - }) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - #[test] fn test_body() { - let mut srv = - test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); + let mut srv = TestServer::new(|| { + h1::H1Service::new( + App::new().resource("/", |r| r.get(|| Response::Ok().body(STR))), + ) + }); let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); + let mut response = srv.send_request(request).unwrap(); assert!(response.status().is_success()); // read response - let bytes = srv.execute(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } #[test] fn test_body_gzip() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Gzip) - .body(STR) - }) + let mut srv = TestServer::new(|| { + h1::H1Service::new( + App::new() + .middleware(middleware::Compress::new(ContentEncoding::Gzip)) + .resource("/", |r| r.get(|| Response::Ok().body(STR))), + ) }); - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); + let request = srv.get().finish().unwrap(); + let mut response = srv.send_request(request).unwrap(); assert!(response.status().is_success()); // read response @@ -315,19 +80,19 @@ fn test_body_gzip() { #[test] fn test_body_gzip_large() { let data = STR.repeat(10); - let srv_data = Arc::new(data.clone()); + let srv_data = data.clone(); - let mut srv = test::TestServer::new(move |app| { + let mut srv = TestServer::new(move || { let data = srv_data.clone(); - app.handler(move |_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Gzip) - .body(data.as_ref()) - }) + h1::H1Service::new( + App::new() + .middleware(middleware::Compress::new(ContentEncoding::Gzip)) + .resource("/", |r| r.get(move || Response::Ok().body(data.clone()))), + ) }); - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); + let request = srv.get().finish().unwrap(); + let mut response = srv.send_request(request).unwrap(); assert!(response.status().is_success()); // read response @@ -346,19 +111,19 @@ fn test_body_gzip_large_random() { .sample_iter(&Alphanumeric) .take(70_000) .collect::(); - let srv_data = Arc::new(data.clone()); + let srv_data = data.clone(); - let mut srv = test::TestServer::new(move |app| { + let mut srv = TestServer::new(move || { let data = srv_data.clone(); - app.handler(move |_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Gzip) - .body(data.as_ref()) - }) + h1::H1Service::new( + App::new() + .middleware(middleware::Compress::new(ContentEncoding::Gzip)) + .resource("/", |r| r.get(move || Response::Ok().body(data.clone()))), + ) }); - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); + let request = srv.get().finish().unwrap(); + let mut response = srv.send_request(request).unwrap(); assert!(response.status().is_success()); // read response @@ -374,18 +139,27 @@ fn test_body_gzip_large_random() { #[test] fn test_body_chunked_implicit() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - let body = once(Ok(Bytes::from_static(STR.as_ref()))); - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Gzip) - .body(Body::Streaming(Box::new(body))) - }) + let mut srv = TestServer::new(move || { + h1::H1Service::new( + App::new() + .middleware(middleware::Compress::new(ContentEncoding::Gzip)) + .resource("/", |r| { + r.get(move || { + Response::Ok().streaming(once(Ok::<_, Error>( + Bytes::from_static(STR.as_ref()), + ))) + }) + }), + ) }); - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); + let request = srv.get().finish().unwrap(); + let mut response = srv.send_request(request).unwrap(); assert!(response.status().is_success()); + assert_eq!( + response.headers().get(TRANSFER_ENCODING).unwrap(), + &b"chunked"[..] + ); // read response let bytes = srv.execute(response.body()).unwrap(); @@ -397,20 +171,24 @@ fn test_body_chunked_implicit() { assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); } -#[cfg(feature = "brotli")] #[test] fn test_body_br_streaming() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - let body = once(Ok(Bytes::from_static(STR.as_ref()))); - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Br) - .body(Body::Streaming(Box::new(body))) - }) + let mut srv = TestServer::new(move || { + h1::H1Service::new( + App::new() + .middleware(middleware::Compress::new(ContentEncoding::Br)) + .resource("/", |r| { + r.get(move || { + Response::Ok().streaming(once(Ok::<_, Error>( + Bytes::from_static(STR.as_ref()), + ))) + }) + }), + ) }); - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); + let request = srv.get().header(ACCEPT_ENCODING, "br").finish().unwrap(); + let mut response = srv.send_request(request).unwrap(); assert!(response.status().is_success()); // read response @@ -423,49 +201,20 @@ fn test_body_br_streaming() { assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); } -#[test] -fn test_head_empty() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| HttpResponse::Ok().content_length(STR.len() as u64).finish()) - }); - - let request = srv.head().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - { - let len = response - .headers() - .get(http::header::CONTENT_LENGTH) - .unwrap(); - assert_eq!(format!("{}", STR.len()), len.to_str().unwrap()); - } - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert!(bytes.is_empty()); -} - #[test] fn test_head_binary() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .content_length(100) - .body(STR) - }) + let mut srv = TestServer::new(move || { + h1::H1Service::new(App::new().resource("/", |r| { + r.head(move || Response::Ok().content_length(100).body(STR)) + })) }); let request = srv.head().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); + let mut response = srv.send_request(request).unwrap(); assert!(response.status().is_success()); { - let len = response - .headers() - .get(http::header::CONTENT_LENGTH) - .unwrap(); + let len = response.headers().get(CONTENT_LENGTH).unwrap(); assert_eq!(format!("{}", STR.len()), len.to_str().unwrap()); } @@ -475,121 +224,41 @@ fn test_head_binary() { } #[test] -fn test_head_binary2() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(STR) - }) - }); - - let request = srv.head().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - { - let len = response - .headers() - .get(http::header::CONTENT_LENGTH) - .unwrap(); - assert_eq!(format!("{}", STR.len()), len.to_str().unwrap()); - } -} - -#[test] -fn test_body_length() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - let body = once(Ok(Bytes::from_static(STR.as_ref()))); - HttpResponse::Ok() - .content_length(STR.len() as u64) - .content_encoding(http::ContentEncoding::Identity) - .body(Body::Streaming(Box::new(body))) - }) +fn test_no_chunking() { + let mut srv = TestServer::new(move || { + h1::H1Service::new(App::new().resource("/", |r| { + r.get(move || { + Response::Ok() + .no_chunking() + .content_length(STR.len() as u64) + .streaming(once(Ok::<_, Error>(Bytes::from_static(STR.as_ref())))) + }) + })) }); let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); + let mut response = srv.send_request(request).unwrap(); assert!(response.status().is_success()); + assert!(!response.headers().contains_key(TRANSFER_ENCODING)); // read response let bytes = srv.execute(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } -#[test] -fn test_body_chunked_explicit() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - let body = once(Ok(Bytes::from_static(STR.as_ref()))); - HttpResponse::Ok() - .chunked() - .content_encoding(http::ContentEncoding::Gzip) - .body(Body::Streaming(Box::new(body))) - }) - }); - - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(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())); -} - -#[test] -fn test_body_identity() { - let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); - e.write_all(STR.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - let enc2 = enc.clone(); - - let mut srv = test::TestServer::new(move |app| { - let enc3 = enc2.clone(); - app.handler(move |_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .header(http::header::CONTENT_ENCODING, "deflate") - .body(enc3.clone()) - }) - }); - - // client request - let request = srv - .get() - .header("accept-encoding", "deflate") - .finish() - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - - // decode deflate - assert_eq!(bytes, Bytes::from(STR)); -} - #[test] fn test_body_deflate() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Deflate) - .body(STR) - }) + let mut srv = TestServer::new(move || { + h1::H1Service::new( + App::new() + .middleware(middleware::Compress::new(ContentEncoding::Deflate)) + .resource("/", |r| r.get(move || Response::Ok().body(STR))), + ) }); // client request - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); + let request = srv.get().finish().unwrap(); + let mut response = srv.send_request(request).unwrap(); assert!(response.status().is_success()); // read response @@ -602,20 +271,19 @@ fn test_body_deflate() { assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); } -#[cfg(feature = "brotli")] #[test] fn test_body_brotli() { - let mut srv = test::TestServer::new(|app| { - app.handler(|_| { - HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Br) - .body(STR) - }) + let mut srv = TestServer::new(move || { + h1::H1Service::new( + App::new() + .middleware(middleware::Compress::new(ContentEncoding::Br)) + .resource("/", |r| r.get(move || Response::Ok().body(STR))), + ) }); // client request - let request = srv.get().disable_decompress().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); + let request = srv.get().header(ACCEPT_ENCODING, "br").finish().unwrap(); + let mut response = srv.send_request(request).unwrap(); assert!(response.status().is_success()); // read response @@ -628,773 +296,623 @@ fn test_body_brotli() { assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); } -#[test] -fn test_gzip_encoding() { - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - // client request - let mut e = GzEncoder::new(Vec::new(), Compression::default()); - e.write_all(STR.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "gzip") - .body(enc.clone()) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_gzip_encoding_large() { - let data = STR.repeat(10); - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - // client request - let mut e = GzEncoder::new(Vec::new(), Compression::default()); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "gzip") - .body(enc.clone()) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - -#[test] -fn test_reading_gzip_encoding_large_random() { - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(60_000) - .collect::(); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - // client request - let mut e = GzEncoder::new(Vec::new(), Compression::default()); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "gzip") - .body(enc.clone()) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes.len(), data.len()); - assert_eq!(bytes, Bytes::from(data)); -} - -#[test] -fn test_reading_deflate_encoding() { - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); - e.write_all(STR.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "deflate") - .body(enc) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_reading_deflate_encoding_large() { - let data = STR.repeat(10); - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "deflate") - .body(enc) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - -#[test] -fn test_reading_deflate_encoding_large_random() { - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(160_000) - .collect::(); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "deflate") - .body(enc) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes.len(), data.len()); - assert_eq!(bytes, Bytes::from(data)); -} - -#[cfg(feature = "brotli")] -#[test] -fn test_brotli_encoding() { - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - let mut e = BrotliEncoder::new(Vec::new(), 5); - e.write_all(STR.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "br") - .body(enc) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); -} - -#[cfg(feature = "brotli")] -#[test] -fn test_brotli_encoding_large() { - let data = STR.repeat(10); - let mut srv = test::TestServer::new(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - let mut e = BrotliEncoder::new(Vec::new(), 5); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = srv - .post() - .header(http::header::CONTENT_ENCODING, "br") - .body(enc) - .unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = srv.execute(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - -#[cfg(all(feature = "brotli", feature = "ssl"))] -#[test] -fn test_brotli_encoding_large_ssl() { - use actix::{Actor, System}; - use openssl::ssl::{ - SslAcceptor, SslConnector, SslFiletype, SslMethod, SslVerifyMode, - }; - // 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(); - - let data = STR.repeat(10); - let srv = test::TestServer::build().ssl(builder).start(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - let mut rt = System::new("test"); - - // client connector - let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); - builder.set_verify(SslVerifyMode::NONE); - let conn = client::ClientConnector::with_connector(builder.build()).start(); - - // body - let mut e = BrotliEncoder::new(Vec::new(), 5); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = client::ClientRequest::build() - .uri(srv.url("/")) - .method(http::Method::POST) - .header(http::header::CONTENT_ENCODING, "br") - .with_connector(conn) - .body(enc) - .unwrap(); - let response = rt.block_on(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = rt.block_on(response.body()).unwrap(); - assert_eq!(bytes, Bytes::from(data)); -} - -#[cfg(all(feature = "rust-tls", feature = "ssl"))] -#[test] -fn test_reading_deflate_encoding_large_random_ssl() { - use actix::{Actor, System}; - use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; - use rustls::internal::pemfile::{certs, rsa_private_keys}; - use rustls::{NoClientAuth, ServerConfig}; - use std::fs::File; - use std::io::BufReader; - - // load ssl keys - let mut config = ServerConfig::new(NoClientAuth::new()); - let cert_file = &mut BufReader::new(File::open("tests/cert.pem").unwrap()); - let key_file = &mut BufReader::new(File::open("tests/key.pem").unwrap()); - let cert_chain = certs(cert_file).unwrap(); - let mut keys = rsa_private_keys(key_file).unwrap(); - config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); - - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(160_000) - .collect::(); - - let srv = test::TestServer::build().rustls(config).start(|app| { - app.handler(|req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }); - - let mut rt = System::new("test"); - - // client connector - let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); - builder.set_verify(SslVerifyMode::NONE); - let conn = client::ClientConnector::with_connector(builder.build()).start(); - - // encode data - let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = client::ClientRequest::build() - .uri(srv.url("/")) - .method(http::Method::POST) - .header(http::header::CONTENT_ENCODING, "deflate") - .with_connector(conn) - .body(enc) - .unwrap(); - let response = rt.block_on(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = rt.block_on(response.body()).unwrap(); - assert_eq!(bytes.len(), data.len()); - assert_eq!(bytes, Bytes::from(data)); -} - -#[cfg(all(feature = "tls", feature = "ssl"))] -#[test] -fn test_reading_deflate_encoding_large_random_tls() { - use native_tls::{Identity, TlsAcceptor}; - use openssl::ssl::{ - SslAcceptor, SslConnector, SslFiletype, SslMethod, SslVerifyMode, - }; - use std::fs::File; - use std::sync::mpsc; - - use actix::{Actor, System}; - let (tx, rx) = mpsc::channel(); - - // load ssl keys - let mut file = File::open("tests/identity.pfx").unwrap(); - let mut identity = vec![]; - file.read_to_end(&mut identity).unwrap(); - let identity = Identity::from_pkcs12(&identity, "1").unwrap(); - let acceptor = TlsAcceptor::new(identity).unwrap(); - - // 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(); - - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(160_000) - .collect::(); - - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - System::run(move || { - server::new(|| { - App::new().handler("/", |req: &HttpRequest| { - req.body() - .and_then(|bytes: Bytes| { - Ok(HttpResponse::Ok() - .content_encoding(http::ContentEncoding::Identity) - .body(bytes)) - }).responder() - }) - }).bind_tls(addr, acceptor) - .unwrap() - .start(); - let _ = tx.send(System::current()); - }); - }); - let sys = rx.recv().unwrap(); - - let mut rt = System::new("test"); - - // client connector - let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); - builder.set_verify(SslVerifyMode::NONE); - let conn = client::ClientConnector::with_connector(builder.build()).start(); - - // encode data - let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); - - // client request - let request = client::ClientRequest::build() - .uri(format!("https://{}/", addr)) - .method(http::Method::POST) - .header(http::header::CONTENT_ENCODING, "deflate") - .with_connector(conn) - .body(enc) - .unwrap(); - let response = rt.block_on(request.send()).unwrap(); - assert!(response.status().is_success()); - - // read response - let bytes = rt.block_on(response.body()).unwrap(); - assert_eq!(bytes.len(), data.len()); - assert_eq!(bytes, Bytes::from(data)); - - let _ = sys.stop(); -} - -#[test] -fn test_h2() { - let srv = test::TestServer::new(|app| app.handler(|_| HttpResponse::Ok().body(STR))); - let addr = srv.addr(); - thread::sleep(time::Duration::from_millis(500)); - - let mut core = Runtime::new().unwrap(); - let tcp = TcpStream::connect(&addr); - - let tcp = tcp - .then(|res| h2client::handshake(res.unwrap())) - .then(move |res| { - let (mut client, h2) = res.unwrap(); - - let request = Request::builder() - .uri(format!("https://{}/", addr).as_str()) - .body(()) - .unwrap(); - let (response, _) = client.send_request(request, false).unwrap(); - - // Spawn a task to run the conn... - spawn(h2.map_err(|e| println!("GOT ERR={:?}", e))); - - response.and_then(|response| { - assert_eq!(response.status(), http::StatusCode::OK); - - let (_, body) = response.into_parts(); - - body.fold(BytesMut::new(), |mut b, c| -> Result<_, h2::Error> { - b.extend(c); - Ok(b) - }) - }) - }); - let _res = core.block_on(tcp); - // assert_eq!(_res.unwrap(), Bytes::from_static(STR.as_ref())); -} - -#[test] -fn test_application() { - let mut srv = test::TestServer::with_factory(|| { - App::new().resource("/", |r| r.f(|_| HttpResponse::Ok())) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); -} - -#[test] -fn test_default_404_handler_response() { - let mut srv = test::TestServer::with_factory(|| { - App::new() - .prefix("/app") - .resource("", |r| r.f(|_| HttpResponse::Ok())) - .resource("/", |r| r.f(|_| HttpResponse::Ok())) - }); - let addr = srv.addr(); - - let mut buf = [0; 24]; - let request = TcpStream::connect(&addr) - .and_then(|sock| { - tokio::io::write_all(sock, "HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n") - .and_then(|(sock, _)| tokio::io::read_exact(sock, &mut buf)) - .and_then(|(_, buf)| Ok(buf)) - }).map_err(|e| panic!("{:?}", e)); - let response = srv.execute(request).unwrap(); - let rep = String::from_utf8_lossy(&response[..]); - assert!(rep.contains("HTTP/1.1 404 Not Found")); -} - -#[test] -fn test_server_cookies() { - use actix_web::http; - - let mut srv = test::TestServer::with_factory(|| { - App::new().resource("/", |r| { - r.f(|_| { - HttpResponse::Ok() - .cookie( - http::CookieBuilder::new("first", "first_value") - .http_only(true) - .finish(), - ).cookie(http::Cookie::new("second", "first_value")) - .cookie(http::Cookie::new("second", "second_value")) - .finish() - }) - }) - }); - - let first_cookie = http::CookieBuilder::new("first", "first_value") - .http_only(true) - .finish(); - let second_cookie = http::Cookie::new("second", "second_value"); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - let cookies = response.cookies().expect("To have cookies"); - assert_eq!(cookies.len(), 2); - if cookies[0] == first_cookie { - assert_eq!(cookies[1], second_cookie); - } else { - assert_eq!(cookies[0], second_cookie); - assert_eq!(cookies[1], first_cookie); - } - - let first_cookie = first_cookie.to_string(); - let second_cookie = second_cookie.to_string(); - //Check that we have exactly two instances of raw cookie headers - let cookies = response - .headers() - .get_all(http::header::SET_COOKIE) - .iter() - .map(|header| header.to_str().expect("To str").to_string()) - .collect::>(); - assert_eq!(cookies.len(), 2); - if cookies[0] == first_cookie { - assert_eq!(cookies[1], second_cookie); - } else { - assert_eq!(cookies[0], second_cookie); - assert_eq!(cookies[1], first_cookie); - } -} - -#[test] -fn test_slow_request() { - use actix::System; - use std::net; - use std::sync::mpsc; - let (tx, rx) = mpsc::channel(); - - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - System::run(move || { - let srv = server::new(|| { - vec![App::new().resource("/", |r| { - r.method(http::Method::GET).f(|_| HttpResponse::Ok()) - })] - }); - - let srv = srv.bind(addr).unwrap(); - srv.client_timeout(200).start(); - let _ = tx.send(System::current()); - }); - }); - let sys = rx.recv().unwrap(); - - thread::sleep(time::Duration::from_millis(200)); - - let mut stream = net::TcpStream::connect(addr).unwrap(); - let mut data = String::new(); - let _ = stream.read_to_string(&mut data); - assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); - - let mut stream = net::TcpStream::connect(addr).unwrap(); - let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\n"); - let mut data = String::new(); - let _ = stream.read_to_string(&mut data); - assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); - - sys.stop(); -} - -#[test] -fn test_malformed_request() { - use actix::System; - use std::net; - use std::sync::mpsc; - let (tx, rx) = mpsc::channel(); - - let addr = test::TestServer::unused_addr(); - thread::spawn(move || { - System::run(move || { - let srv = server::new(|| { - App::new().resource("/", |r| { - r.method(http::Method::GET).f(|_| HttpResponse::Ok()) - }) - }); - - let _ = srv.bind(addr).unwrap().start(); - let _ = tx.send(System::current()); - }); - }); - let sys = rx.recv().unwrap(); - thread::sleep(time::Duration::from_millis(200)); - - let mut stream = net::TcpStream::connect(addr).unwrap(); - let _ = stream.write_all(b"GET /test/tests/test HTTP1.1\r\n"); - let mut data = String::new(); - let _ = stream.read_to_string(&mut data); - assert!(data.starts_with("HTTP/1.1 400 Bad Request")); - - sys.stop(); -} - -#[test] -fn test_app_404() { - let mut srv = test::TestServer::with_factory(|| { - App::new().prefix("/prefix").resource("/", |r| { - r.method(http::Method::GET).f(|_| HttpResponse::Ok()) - }) - }); - - let request = srv.client(http::Method::GET, "/prefix/").finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.status().is_success()); - - let request = srv.client(http::Method::GET, "/").finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), http::StatusCode::NOT_FOUND); -} - -#[test] -#[cfg(feature = "ssl")] -fn test_ssl_handshake_timeout() { - use actix::System; - use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; - use std::net; - use std::sync::mpsc; - - let (tx, rx) = mpsc::channel(); - let addr = test::TestServer::unused_addr(); - - // 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(); - - thread::spawn(move || { - System::run(move || { - let srv = server::new(|| { - App::new().resource("/", |r| { - r.method(http::Method::GET).f(|_| HttpResponse::Ok()) - }) - }); - - srv.bind_ssl(addr, builder) - .unwrap() - .workers(1) - .client_timeout(200) - .start(); - let _ = tx.send(System::current()); - }); - }); - let sys = rx.recv().unwrap(); - - let mut stream = net::TcpStream::connect(addr).unwrap(); - let mut data = String::new(); - let _ = stream.read_to_string(&mut data); - assert!(data.is_empty()); - - let _ = sys.stop(); -} - -#[test] -fn test_content_length() { - use actix_web::http::header::{HeaderName, HeaderValue}; - use http::StatusCode; - - let mut srv = test::TestServer::new(move |app| { - app.resource("/{status}", |r| { - r.f(|req: &HttpRequest| { - let indx: usize = - req.match_info().get("status").unwrap().parse().unwrap(); - let statuses = [ - StatusCode::NO_CONTENT, - StatusCode::CONTINUE, - StatusCode::SWITCHING_PROTOCOLS, - StatusCode::PROCESSING, - StatusCode::OK, - StatusCode::NOT_FOUND, - ]; - HttpResponse::new(statuses[indx]) - }) - }); - }); - - let addr = srv.addr(); - let mut get_resp = |i| { - let url = format!("http://{}/{}", addr, i); - let req = srv.get().uri(url).finish().unwrap(); - srv.execute(req.send()).unwrap() - }; - - let header = HeaderName::from_static("content-length"); - let value = HeaderValue::from_static("0"); - - for i in 0..4 { - let response = get_resp(i); - assert_eq!(response.headers().get(&header), None); - } - for i in 4..6 { - let response = get_resp(i); - assert_eq!(response.headers().get(&header), Some(&value)); - } -} +// #[test] +// fn test_gzip_encoding() { +// let mut srv = test::TestServer::new(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Identity) +// .body(bytes)) +// }) +// .responder() +// }) +// }); + +// // client request +// let mut e = GzEncoder::new(Vec::new(), Compression::default()); +// e.write_all(STR.as_ref()).unwrap(); +// let enc = e.finish().unwrap(); + +// let request = srv +// .post() +// .header(http::header::CONTENT_ENCODING, "gzip") +// .body(enc.clone()) +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = srv.execute(response.body()).unwrap(); +// assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +// } + +// #[test] +// fn test_gzip_encoding_large() { +// let data = STR.repeat(10); +// let mut srv = test::TestServer::new(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Identity) +// .body(bytes)) +// }) +// .responder() +// }) +// }); + +// // client request +// let mut e = GzEncoder::new(Vec::new(), Compression::default()); +// e.write_all(data.as_ref()).unwrap(); +// let enc = e.finish().unwrap(); + +// let request = srv +// .post() +// .header(http::header::CONTENT_ENCODING, "gzip") +// .body(enc.clone()) +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = srv.execute(response.body()).unwrap(); +// assert_eq!(bytes, Bytes::from(data)); +// } + +// #[test] +// fn test_reading_gzip_encoding_large_random() { +// let data = rand::thread_rng() +// .sample_iter(&Alphanumeric) +// .take(60_000) +// .collect::(); + +// let mut srv = test::TestServer::new(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Identity) +// .body(bytes)) +// }) +// .responder() +// }) +// }); + +// // client request +// let mut e = GzEncoder::new(Vec::new(), Compression::default()); +// e.write_all(data.as_ref()).unwrap(); +// let enc = e.finish().unwrap(); + +// let request = srv +// .post() +// .header(http::header::CONTENT_ENCODING, "gzip") +// .body(enc.clone()) +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = srv.execute(response.body()).unwrap(); +// assert_eq!(bytes.len(), data.len()); +// assert_eq!(bytes, Bytes::from(data)); +// } + +// #[test] +// fn test_reading_deflate_encoding() { +// let mut srv = test::TestServer::new(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Identity) +// .body(bytes)) +// }) +// .responder() +// }) +// }); + +// let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); +// e.write_all(STR.as_ref()).unwrap(); +// let enc = e.finish().unwrap(); + +// // client request +// let request = srv +// .post() +// .header(http::header::CONTENT_ENCODING, "deflate") +// .body(enc) +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = srv.execute(response.body()).unwrap(); +// assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +// } + +// #[test] +// fn test_reading_deflate_encoding_large() { +// let data = STR.repeat(10); +// let mut srv = test::TestServer::new(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Identity) +// .body(bytes)) +// }) +// .responder() +// }) +// }); + +// let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); +// e.write_all(data.as_ref()).unwrap(); +// let enc = e.finish().unwrap(); + +// // client request +// let request = srv +// .post() +// .header(http::header::CONTENT_ENCODING, "deflate") +// .body(enc) +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = srv.execute(response.body()).unwrap(); +// assert_eq!(bytes, Bytes::from(data)); +// } + +// #[test] +// fn test_reading_deflate_encoding_large_random() { +// let data = rand::thread_rng() +// .sample_iter(&Alphanumeric) +// .take(160_000) +// .collect::(); + +// let mut srv = test::TestServer::new(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Identity) +// .body(bytes)) +// }) +// .responder() +// }) +// }); + +// let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); +// e.write_all(data.as_ref()).unwrap(); +// let enc = e.finish().unwrap(); + +// // client request +// let request = srv +// .post() +// .header(http::header::CONTENT_ENCODING, "deflate") +// .body(enc) +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = srv.execute(response.body()).unwrap(); +// assert_eq!(bytes.len(), data.len()); +// assert_eq!(bytes, Bytes::from(data)); +// } + +// #[cfg(feature = "brotli")] +// #[test] +// fn test_brotli_encoding() { +// let mut srv = test::TestServer::new(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Identity) +// .body(bytes)) +// }) +// .responder() +// }) +// }); + +// let mut e = BrotliEncoder::new(Vec::new(), 5); +// e.write_all(STR.as_ref()).unwrap(); +// let enc = e.finish().unwrap(); + +// // client request +// let request = srv +// .post() +// .header(http::header::CONTENT_ENCODING, "br") +// .body(enc) +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = srv.execute(response.body()).unwrap(); +// assert_eq!(bytes, Bytes::from_static(STR.as_ref())); +// } + +// #[cfg(feature = "brotli")] +// #[test] +// fn test_brotli_encoding_large() { +// let data = STR.repeat(10); +// let mut srv = test::TestServer::new(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Identity) +// .body(bytes)) +// }) +// .responder() +// }) +// }); + +// let mut e = BrotliEncoder::new(Vec::new(), 5); +// e.write_all(data.as_ref()).unwrap(); +// let enc = e.finish().unwrap(); + +// // client request +// let request = srv +// .post() +// .header(http::header::CONTENT_ENCODING, "br") +// .body(enc) +// .unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = srv.execute(response.body()).unwrap(); +// assert_eq!(bytes, Bytes::from(data)); +// } + +// #[cfg(all(feature = "brotli", feature = "ssl"))] +// #[test] +// fn test_brotli_encoding_large_ssl() { +// use actix::{Actor, System}; +// use openssl::ssl::{ +// SslAcceptor, SslConnector, SslFiletype, SslMethod, SslVerifyMode, +// }; +// // 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(); + +// let data = STR.repeat(10); +// let srv = test::TestServer::build().ssl(builder).start(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Identity) +// .body(bytes)) +// }) +// .responder() +// }) +// }); +// let mut rt = System::new("test"); + +// // client connector +// let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); +// builder.set_verify(SslVerifyMode::NONE); +// let conn = client::ClientConnector::with_connector(builder.build()).start(); + +// // body +// let mut e = BrotliEncoder::new(Vec::new(), 5); +// e.write_all(data.as_ref()).unwrap(); +// let enc = e.finish().unwrap(); + +// // client request +// let request = client::ClientRequest::build() +// .uri(srv.url("/")) +// .method(http::Method::POST) +// .header(http::header::CONTENT_ENCODING, "br") +// .with_connector(conn) +// .body(enc) +// .unwrap(); +// let response = rt.block_on(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = rt.block_on(response.body()).unwrap(); +// assert_eq!(bytes, Bytes::from(data)); +// } + +// #[cfg(all(feature = "rust-tls", feature = "ssl"))] +// #[test] +// fn test_reading_deflate_encoding_large_random_ssl() { +// use actix::{Actor, System}; +// use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; +// use rustls::internal::pemfile::{certs, rsa_private_keys}; +// use rustls::{NoClientAuth, ServerConfig}; +// use std::fs::File; +// use std::io::BufReader; + +// // load ssl keys +// let mut config = ServerConfig::new(NoClientAuth::new()); +// let cert_file = &mut BufReader::new(File::open("tests/cert.pem").unwrap()); +// let key_file = &mut BufReader::new(File::open("tests/key.pem").unwrap()); +// let cert_chain = certs(cert_file).unwrap(); +// let mut keys = rsa_private_keys(key_file).unwrap(); +// config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); + +// let data = rand::thread_rng() +// .sample_iter(&Alphanumeric) +// .take(160_000) +// .collect::(); + +// let srv = test::TestServer::build().rustls(config).start(|app| { +// app.handler(|req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Identity) +// .body(bytes)) +// }) +// .responder() +// }) +// }); + +// let mut rt = System::new("test"); + +// // client connector +// let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); +// builder.set_verify(SslVerifyMode::NONE); +// let conn = client::ClientConnector::with_connector(builder.build()).start(); + +// // encode data +// let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); +// e.write_all(data.as_ref()).unwrap(); +// let enc = e.finish().unwrap(); + +// // client request +// let request = client::ClientRequest::build() +// .uri(srv.url("/")) +// .method(http::Method::POST) +// .header(http::header::CONTENT_ENCODING, "deflate") +// .with_connector(conn) +// .body(enc) +// .unwrap(); +// let response = rt.block_on(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = rt.block_on(response.body()).unwrap(); +// assert_eq!(bytes.len(), data.len()); +// assert_eq!(bytes, Bytes::from(data)); +// } + +// #[cfg(all(feature = "tls", feature = "ssl"))] +// #[test] +// fn test_reading_deflate_encoding_large_random_tls() { +// use native_tls::{Identity, TlsAcceptor}; +// use openssl::ssl::{ +// SslAcceptor, SslConnector, SslFiletype, SslMethod, SslVerifyMode, +// }; +// use std::fs::File; +// use std::sync::mpsc; + +// use actix::{Actor, System}; +// let (tx, rx) = mpsc::channel(); + +// // load ssl keys +// let mut file = File::open("tests/identity.pfx").unwrap(); +// let mut identity = vec![]; +// file.read_to_end(&mut identity).unwrap(); +// let identity = Identity::from_pkcs12(&identity, "1").unwrap(); +// let acceptor = TlsAcceptor::new(identity).unwrap(); + +// // 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(); + +// let data = rand::thread_rng() +// .sample_iter(&Alphanumeric) +// .take(160_000) +// .collect::(); + +// let addr = test::TestServer::unused_addr(); +// thread::spawn(move || { +// System::run(move || { +// server::new(|| { +// App::new().handler("/", |req: &HttpRequest| { +// req.body() +// .and_then(|bytes: Bytes| { +// Ok(HttpResponse::Ok() +// .content_encoding(http::ContentEncoding::Identity) +// .body(bytes)) +// }) +// .responder() +// }) +// }) +// .bind_tls(addr, acceptor) +// .unwrap() +// .start(); +// let _ = tx.send(System::current()); +// }); +// }); +// let sys = rx.recv().unwrap(); + +// let mut rt = System::new("test"); + +// // client connector +// let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); +// builder.set_verify(SslVerifyMode::NONE); +// let conn = client::ClientConnector::with_connector(builder.build()).start(); + +// // encode data +// let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); +// e.write_all(data.as_ref()).unwrap(); +// let enc = e.finish().unwrap(); + +// // client request +// let request = client::ClientRequest::build() +// .uri(format!("https://{}/", addr)) +// .method(http::Method::POST) +// .header(http::header::CONTENT_ENCODING, "deflate") +// .with_connector(conn) +// .body(enc) +// .unwrap(); +// let response = rt.block_on(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// // read response +// let bytes = rt.block_on(response.body()).unwrap(); +// assert_eq!(bytes.len(), data.len()); +// assert_eq!(bytes, Bytes::from(data)); + +// let _ = sys.stop(); +// } + +// #[test] +// fn test_server_cookies() { +// use actix_web::http; + +// let mut srv = test::TestServer::with_factory(|| { +// App::new().resource("/", |r| { +// r.f(|_| { +// HttpResponse::Ok() +// .cookie( +// http::CookieBuilder::new("first", "first_value") +// .http_only(true) +// .finish(), +// ) +// .cookie(http::Cookie::new("second", "first_value")) +// .cookie(http::Cookie::new("second", "second_value")) +// .finish() +// }) +// }) +// }); + +// let first_cookie = http::CookieBuilder::new("first", "first_value") +// .http_only(true) +// .finish(); +// let second_cookie = http::Cookie::new("second", "second_value"); + +// let request = srv.get().finish().unwrap(); +// let response = srv.execute(request.send()).unwrap(); +// assert!(response.status().is_success()); + +// let cookies = response.cookies().expect("To have cookies"); +// assert_eq!(cookies.len(), 2); +// if cookies[0] == first_cookie { +// assert_eq!(cookies[1], second_cookie); +// } else { +// assert_eq!(cookies[0], second_cookie); +// assert_eq!(cookies[1], first_cookie); +// } + +// let first_cookie = first_cookie.to_string(); +// let second_cookie = second_cookie.to_string(); +// //Check that we have exactly two instances of raw cookie headers +// let cookies = response +// .headers() +// .get_all(http::header::SET_COOKIE) +// .iter() +// .map(|header| header.to_str().expect("To str").to_string()) +// .collect::>(); +// assert_eq!(cookies.len(), 2); +// if cookies[0] == first_cookie { +// assert_eq!(cookies[1], second_cookie); +// } else { +// assert_eq!(cookies[0], second_cookie); +// assert_eq!(cookies[1], first_cookie); +// } +// } + +// #[test] +// fn test_slow_request() { +// use actix::System; +// use std::net; +// use std::sync::mpsc; +// let (tx, rx) = mpsc::channel(); + +// let addr = test::TestServer::unused_addr(); +// thread::spawn(move || { +// System::run(move || { +// let srv = server::new(|| { +// vec![App::new().resource("/", |r| { +// r.method(http::Method::GET).f(|_| HttpResponse::Ok()) +// })] +// }); + +// let srv = srv.bind(addr).unwrap(); +// srv.client_timeout(200).start(); +// let _ = tx.send(System::current()); +// }); +// }); +// let sys = rx.recv().unwrap(); + +// thread::sleep(time::Duration::from_millis(200)); + +// let mut stream = net::TcpStream::connect(addr).unwrap(); +// let mut data = String::new(); +// let _ = stream.read_to_string(&mut data); +// assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); + +// let mut stream = net::TcpStream::connect(addr).unwrap(); +// let _ = stream.write_all(b"GET /test/tests/test HTTP/1.1\r\n"); +// let mut data = String::new(); +// let _ = stream.read_to_string(&mut data); +// assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); + +// sys.stop(); +// } + +// #[test] +// #[cfg(feature = "ssl")] +// fn test_ssl_handshake_timeout() { +// use actix::System; +// use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; +// use std::net; +// use std::sync::mpsc; + +// let (tx, rx) = mpsc::channel(); +// let addr = test::TestServer::unused_addr(); + +// // 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(); + +// thread::spawn(move || { +// System::run(move || { +// let srv = server::new(|| { +// App::new().resource("/", |r| { +// r.method(http::Method::GET).f(|_| HttpResponse::Ok()) +// }) +// }); + +// srv.bind_ssl(addr, builder) +// .unwrap() +// .workers(1) +// .client_timeout(200) +// .start(); +// let _ = tx.send(System::current()); +// }); +// }); +// let sys = rx.recv().unwrap(); + +// let mut stream = net::TcpStream::connect(addr).unwrap(); +// let mut data = String::new(); +// let _ = stream.read_to_string(&mut data); +// assert!(data.is_empty()); + +// let _ = sys.stop(); +// } diff --git a/tests/test_ws.rs b/tests/test_ws.rs deleted file mode 100644 index cb46bc7e..00000000 --- a/tests/test_ws.rs +++ /dev/null @@ -1,395 +0,0 @@ -extern crate actix; -extern crate actix_web; -extern crate bytes; -extern crate futures; -extern crate http; -extern crate rand; - -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; -use std::{thread, time}; - -use bytes::Bytes; -use futures::Stream; -use rand::distributions::Alphanumeric; -use rand::Rng; - -#[cfg(feature = "ssl")] -extern crate openssl; -#[cfg(feature = "rust-tls")] -extern crate rustls; - -use actix::prelude::*; -use actix_web::*; - -struct Ws; - -impl Actor for Ws { - type Context = ws::WebsocketContext; -} - -impl StreamHandler for Ws { - fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) { - match msg { - ws::Message::Ping(msg) => ctx.pong(&msg), - ws::Message::Text(text) => ctx.text(text), - ws::Message::Binary(bin) => ctx.binary(bin), - ws::Message::Close(reason) => ctx.close(reason), - _ => (), - } - } -} - -#[test] -fn test_simple() { - let mut srv = test::TestServer::new(|app| app.handler(|req| ws::start(req, Ws))); - let (reader, mut writer) = srv.ws().unwrap(); - - writer.text("text"); - let (item, reader) = srv.execute(reader.into_future()).unwrap(); - assert_eq!(item, Some(ws::Message::Text("text".to_owned()))); - - writer.binary(b"text".as_ref()); - let (item, reader) = srv.execute(reader.into_future()).unwrap(); - assert_eq!( - item, - Some(ws::Message::Binary(Bytes::from_static(b"text").into())) - ); - - writer.ping("ping"); - let (item, reader) = srv.execute(reader.into_future()).unwrap(); - assert_eq!(item, Some(ws::Message::Pong("ping".to_owned()))); - - writer.close(Some(ws::CloseCode::Normal.into())); - let (item, _) = srv.execute(reader.into_future()).unwrap(); - assert_eq!( - item, - Some(ws::Message::Close(Some(ws::CloseCode::Normal.into()))) - ); -} - -// websocket resource helper function -fn start_ws_resource(req: &HttpRequest) -> Result { - ws::start(req, Ws) -} - -#[test] -fn test_simple_path() { - const PATH: &str = "/v1/ws/"; - - // Create a websocket at a specific path. - let mut srv = test::TestServer::new(|app| { - app.resource(PATH, |r| r.route().f(start_ws_resource)); - }); - // fetch the sockets for the resource at a given path. - let (reader, mut writer) = srv.ws_at(PATH).unwrap(); - - writer.text("text"); - let (item, reader) = srv.execute(reader.into_future()).unwrap(); - assert_eq!(item, Some(ws::Message::Text("text".to_owned()))); - - writer.binary(b"text".as_ref()); - let (item, reader) = srv.execute(reader.into_future()).unwrap(); - assert_eq!( - item, - Some(ws::Message::Binary(Bytes::from_static(b"text").into())) - ); - - writer.ping("ping"); - let (item, reader) = srv.execute(reader.into_future()).unwrap(); - assert_eq!(item, Some(ws::Message::Pong("ping".to_owned()))); - - writer.close(Some(ws::CloseCode::Normal.into())); - let (item, _) = srv.execute(reader.into_future()).unwrap(); - assert_eq!( - item, - Some(ws::Message::Close(Some(ws::CloseCode::Normal.into()))) - ); -} - -#[test] -fn test_empty_close_code() { - let mut srv = test::TestServer::new(|app| app.handler(|req| ws::start(req, Ws))); - let (reader, mut writer) = srv.ws().unwrap(); - - writer.close(None); - let (item, _) = srv.execute(reader.into_future()).unwrap(); - assert_eq!(item, Some(ws::Message::Close(None))); -} - -#[test] -fn test_close_description() { - let mut srv = test::TestServer::new(|app| app.handler(|req| ws::start(req, Ws))); - let (reader, mut writer) = srv.ws().unwrap(); - - let close_reason: ws::CloseReason = - (ws::CloseCode::Normal, "close description").into(); - writer.close(Some(close_reason.clone())); - let (item, _) = srv.execute(reader.into_future()).unwrap(); - assert_eq!(item, Some(ws::Message::Close(Some(close_reason)))); -} - -#[test] -fn test_large_text() { - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(65_536) - .collect::(); - - let mut srv = test::TestServer::new(|app| app.handler(|req| ws::start(req, Ws))); - let (mut reader, mut writer) = srv.ws().unwrap(); - - for _ in 0..100 { - writer.text(data.clone()); - let (item, r) = srv.execute(reader.into_future()).unwrap(); - reader = r; - assert_eq!(item, Some(ws::Message::Text(data.clone()))); - } -} - -#[test] -fn test_large_bin() { - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(65_536) - .collect::(); - - let mut srv = test::TestServer::new(|app| app.handler(|req| ws::start(req, Ws))); - let (mut reader, mut writer) = srv.ws().unwrap(); - - for _ in 0..100 { - writer.binary(data.clone()); - let (item, r) = srv.execute(reader.into_future()).unwrap(); - reader = r; - assert_eq!(item, Some(ws::Message::Binary(Binary::from(data.clone())))); - } -} - -#[test] -fn test_client_frame_size() { - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(131_072) - .collect::(); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req| -> Result { - let mut resp = ws::handshake(req)?; - let stream = ws::WsStream::new(req.payload()).max_size(131_072); - - let body = ws::WebsocketContext::create(req.clone(), Ws, stream); - Ok(resp.body(body)) - }) - }); - let (reader, mut writer) = srv.ws().unwrap(); - - writer.binary(data.clone()); - match srv.execute(reader.into_future()).err().unwrap().0 { - ws::ProtocolError::Overflow => (), - _ => panic!(), - } -} - -struct Ws2 { - count: usize, - bin: bool, -} - -impl Actor for Ws2 { - type Context = ws::WebsocketContext; - - fn started(&mut self, ctx: &mut Self::Context) { - self.send(ctx); - } -} - -impl Ws2 { - fn send(&mut self, ctx: &mut ws::WebsocketContext) { - if self.bin { - ctx.binary(Vec::from("0".repeat(65_536))); - } else { - ctx.text("0".repeat(65_536)); - } - ctx.drain() - .and_then(|_, act, ctx| { - act.count += 1; - if act.count != 10_000 { - act.send(ctx); - } - actix::fut::ok(()) - }).wait(ctx); - } -} - -impl StreamHandler for Ws2 { - fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) { - match msg { - ws::Message::Ping(msg) => ctx.pong(&msg), - ws::Message::Text(text) => ctx.text(text), - ws::Message::Binary(bin) => ctx.binary(bin), - ws::Message::Close(reason) => ctx.close(reason), - _ => (), - } - } -} - -#[test] -fn test_server_send_text() { - let data = Some(ws::Message::Text("0".repeat(65_536))); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req| { - ws::start( - req, - Ws2 { - count: 0, - bin: false, - }, - ) - }) - }); - let (mut reader, _writer) = srv.ws().unwrap(); - - for _ in 0..10_000 { - let (item, r) = srv.execute(reader.into_future()).unwrap(); - reader = r; - assert_eq!(item, data); - } -} - -#[test] -fn test_server_send_bin() { - let data = Some(ws::Message::Binary(Binary::from("0".repeat(65_536)))); - - let mut srv = test::TestServer::new(|app| { - app.handler(|req| { - ws::start( - req, - Ws2 { - count: 0, - bin: true, - }, - ) - }) - }); - let (mut reader, _writer) = srv.ws().unwrap(); - - for _ in 0..10_000 { - let (item, r) = srv.execute(reader.into_future()).unwrap(); - reader = r; - assert_eq!(item, data); - } -} - -#[test] -#[cfg(feature = "ssl")] -fn test_ws_server_ssl() { - 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(); - - let mut srv = test::TestServer::build().ssl(builder).start(|app| { - app.handler(|req| { - ws::start( - req, - Ws2 { - count: 0, - bin: false, - }, - ) - }) - }); - let (mut reader, _writer) = srv.ws().unwrap(); - - let data = Some(ws::Message::Text("0".repeat(65_536))); - for _ in 0..10_000 { - let (item, r) = srv.execute(reader.into_future()).unwrap(); - reader = r; - assert_eq!(item, data); - } -} - -#[test] -#[cfg(feature = "rust-tls")] -fn test_ws_server_rust_tls() { - use rustls::internal::pemfile::{certs, rsa_private_keys}; - use rustls::{NoClientAuth, ServerConfig}; - use std::fs::File; - use std::io::BufReader; - - // load ssl keys - let mut config = ServerConfig::new(NoClientAuth::new()); - let cert_file = &mut BufReader::new(File::open("tests/cert.pem").unwrap()); - let key_file = &mut BufReader::new(File::open("tests/key.pem").unwrap()); - let cert_chain = certs(cert_file).unwrap(); - let mut keys = rsa_private_keys(key_file).unwrap(); - config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); - - let mut srv = test::TestServer::build().rustls(config).start(|app| { - app.handler(|req| { - ws::start( - req, - Ws2 { - count: 0, - bin: false, - }, - ) - }) - }); - - let (mut reader, _writer) = srv.ws().unwrap(); - - let data = Some(ws::Message::Text("0".repeat(65_536))); - for _ in 0..10_000 { - let (item, r) = srv.execute(reader.into_future()).unwrap(); - reader = r; - assert_eq!(item, data); - } -} - -struct WsStopped(Arc); - -impl Actor for WsStopped { - type Context = ws::WebsocketContext; - - fn stopped(&mut self, _: &mut Self::Context) { - self.0.fetch_add(1, Ordering::Relaxed); - } -} - -impl StreamHandler for WsStopped { - fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) { - match msg { - ws::Message::Text(text) => ctx.text(text), - _ => (), - } - } -} - -#[test] -fn test_ws_stopped() { - let num = Arc::new(AtomicUsize::new(0)); - let num2 = num.clone(); - - let mut srv = test::TestServer::new(move |app| { - let num3 = num2.clone(); - app.handler(move |req| ws::start(req, WsStopped(num3.clone()))) - }); - { - let (reader, mut writer) = srv.ws().unwrap(); - writer.text("text"); - writer.close(None); - let (item, _) = srv.execute(reader.into_future()).unwrap(); - assert_eq!(item, Some(ws::Message::Text("text".to_owned()))); - } - thread::sleep(time::Duration::from_millis(100)); - - assert_eq!(num.load(Ordering::Relaxed), 1); -} From e6d04d24cce00d4f0d08da518325f0ecdf195b5e Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Fri, 1 Mar 2019 23:59:44 -0800 Subject: [PATCH 002/109] move fs to separate crate --- Cargo.toml | 6 + Makefile | 14 - actix-web-fs/CHANGES.md | 5 + actix-web-fs/Cargo.toml | 42 +++ actix-web-fs/README.md | 82 +++++ src/fs.rs => actix-web-fs/src/lib.rs | 30 +- examples/basic.rs | 2 +- src/app.rs | 118 ++----- src/framed_app.rs | 240 -------------- src/framed_handler.rs | 379 ---------------------- src/framed_route.rs | 448 --------------------------- src/helpers.rs | 180 ----------- src/lib.rs | 3 +- src/resource.rs | 39 +-- tests/test_server.rs | 2 +- 15 files changed, 184 insertions(+), 1406 deletions(-) delete mode 100644 Makefile create mode 100644 actix-web-fs/CHANGES.md create mode 100644 actix-web-fs/Cargo.toml create mode 100644 actix-web-fs/README.md rename src/fs.rs => actix-web-fs/src/lib.rs (99%) delete mode 100644 src/framed_app.rs delete mode 100644 src/framed_handler.rs delete mode 100644 src/framed_route.rs delete mode 100644 src/helpers.rs diff --git a/Cargo.toml b/Cargo.toml index 6a61c780..e9c298fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,12 @@ codecov = { repository = "actix/actix-web2", branch = "master", service = "githu name = "actix_web" path = "src/lib.rs" +[workspace] +members = [ + ".", + "actix-web-fs", +] + [features] default = ["brotli", "flate2-c"] diff --git a/Makefile b/Makefile deleted file mode 100644 index e3b8b2cf..00000000 --- a/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -.PHONY: default build test doc book clean - -CARGO_FLAGS := --features "$(FEATURES) alpn tls" - -default: test - -build: - cargo build $(CARGO_FLAGS) - -test: build clippy - cargo test $(CARGO_FLAGS) - -doc: build - cargo doc --no-deps $(CARGO_FLAGS) diff --git a/actix-web-fs/CHANGES.md b/actix-web-fs/CHANGES.md new file mode 100644 index 00000000..b93e282a --- /dev/null +++ b/actix-web-fs/CHANGES.md @@ -0,0 +1,5 @@ +# Changes + +## [0.1.0] - 2018-10-x + +* Initial impl diff --git a/actix-web-fs/Cargo.toml b/actix-web-fs/Cargo.toml new file mode 100644 index 00000000..5900a936 --- /dev/null +++ b/actix-web-fs/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "actix-web-fs" +version = "0.1.0" +authors = ["Nikolay Kim "] +description = "Static files support for Actix web." +readme = "README.md" +keywords = ["actix", "http", "async", "futures"] +homepage = "https://actix.rs" +repository = "https://github.com/actix/actix-web.git" +documentation = "https://actix.rs/api/actix-web/stable/actix_web/" +categories = ["asynchronous", "web-programming::http-server"] +license = "MIT/Apache-2.0" +edition = "2018" +workspace = ".." + +[lib] +name = "actix_web_fs" +path = "src/lib.rs" + +[dependencies] +actix-web = { path=".." } +actix-http = { git = "https://github.com/actix/actix-http.git" } +actix-service = { git = "https://github.com/actix/actix-net.git" } + +bytes = "0.4" +futures = "0.1" +derive_more = "0.14" +log = "0.4" +mime = "0.3" +mime_guess = "2.0.0-alpha" +percent-encoding = "1.0" +v_htmlescape = "0.4" + +[dev-dependencies] +actix-rt = "0.1.0" +#actix-server = { version="0.2", features=["ssl"] } +actix-server = { git = "https://github.com/actix/actix-net.git", features=["ssl"] } +actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } +actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } +rand = "0.6" +env_logger = "0.6" +serde_derive = "1.0" diff --git a/actix-web-fs/README.md b/actix-web-fs/README.md new file mode 100644 index 00000000..c7e195de --- /dev/null +++ b/actix-web-fs/README.md @@ -0,0 +1,82 @@ +# Actix web [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![Build status](https://ci.appveyor.com/api/projects/status/kkdb4yce7qhm5w85/branch/master?svg=true)](https://ci.appveyor.com/project/fafhrd91/actix-web-hdy9d/branch/master) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](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 web is a simple, pragmatic and extremely fast web framework for Rust. + +* Supported *HTTP/1.x* and [*HTTP/2.0*](https://actix.rs/docs/http2/) protocols +* Streaming and pipelining +* Keep-alive and slow requests handling +* Client/server [WebSockets](https://actix.rs/docs/websockets/) support +* Transparent content compression/decompression (br, gzip, deflate) +* Configurable [request routing](https://actix.rs/docs/url-dispatch/) +* Multipart streams +* Static assets +* SSL support with OpenSSL or `native-tls` +* Middlewares ([Logger, Session, CORS, CSRF, etc](https://actix.rs/docs/middleware/)) +* Includes an asynchronous [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html) +* Built on top of [Actix actor framework](https://github.com/actix/actix) +* Experimental [Async/Await](https://github.com/mehcode/actix-web-async-await) support. + +## Documentation & community resources + +* [User Guide](https://actix.rs/docs/) +* [API Documentation (Development)](https://actix.rs/actix-web/actix_web/) +* [API Documentation (Releases)](https://actix.rs/api/actix-web/stable/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.31 or later + +## Example + +```rust +extern crate actix_web; +use actix_web::{http, server, App, Path, Responder}; + +fn index(info: Path<(u32, String)>) -> impl Responder { + format!("Hello {}! id:{}", info.1, info.0) +} + +fn main() { + server::new( + || App::new() + .route("/{id}/{name}/index.html", http::Method::GET, index)) + .bind("127.0.0.1:8080").unwrap() + .run(); +} +``` + +### More examples + +* [Basics](https://github.com/actix/examples/tree/master/basics/) +* [Stateful](https://github.com/actix/examples/tree/master/state/) +* [Protobuf support](https://github.com/actix/examples/tree/master/protobuf/) +* [Multipart streams](https://github.com/actix/examples/tree/master/multipart/) +* [Simple websocket](https://github.com/actix/examples/tree/master/websocket/) +* [Tera](https://github.com/actix/examples/tree/master/template_tera/) / + [Askama](https://github.com/actix/examples/tree/master/template_askama/) templates +* [Diesel integration](https://github.com/actix/examples/tree/master/diesel/) +* [r2d2](https://github.com/actix/examples/tree/master/r2d2/) +* [SSL / HTTP/2.0](https://github.com/actix/examples/tree/master/tls/) +* [Tcp/Websocket chat](https://github.com/actix/examples/tree/master/websocket-chat/) +* [Json](https://github.com/actix/examples/tree/master/json/) + +You may consider checking out +[this directory](https://github.com/actix/examples/tree/master/) for more examples. + +## Benchmarks + +* [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r16&hw=ph&test=plaintext) + +## License + +This project is licensed under either of + +* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)) +* MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT)) + +at your option. + +## Code of Conduct + +Contribution to the actix-web crate is organized under the terms of the +Contributor Covenant, the maintainer of actix-web, @fafhrd91, promises to +intervene to uphold that code of conduct. diff --git a/src/fs.rs b/actix-web-fs/src/lib.rs similarity index 99% rename from src/fs.rs rename to actix-web-fs/src/lib.rs index 3c83af6e..c2ac5f3a 100644 --- a/src/fs.rs +++ b/actix-web-fs/src/lib.rs @@ -27,15 +27,14 @@ use actix_http::http::header::{ }; use actix_http::http::{ContentEncoding, Method, StatusCode}; use actix_http::{HttpMessage, Response}; +use actix_service::boxed::BoxedNewService; use actix_service::{NewService, Service}; +use actix_web::{ + blocking, FromRequest, HttpRequest, Responder, ServiceRequest, ServiceResponse, +}; use futures::future::{err, ok, FutureResult}; -use crate::blocking; -use crate::handler::FromRequest; -use crate::helpers::HttpDefaultNewService; -use crate::request::HttpRequest; -use crate::responder::Responder; -use crate::service::{ServiceRequest, ServiceResponse}; +type HttpNewService

= BoxedNewService<(), ServiceRequest

, ServiceResponse, (), ()>; ///Describes `StaticFiles` configiration /// @@ -101,6 +100,7 @@ pub trait StaticFileConfig: Default { ///[StaticFileConfig](trait.StaticFileConfig.html) #[derive(Default)] pub struct DefaultConfig; + impl StaticFileConfig for DefaultConfig {} /// Return the MIME type associated with a filename extension (case-insensitive). @@ -716,9 +716,7 @@ pub struct StaticFiles { directory: PathBuf, index: Option, show_index: bool, - default: Rc< - RefCell, ServiceResponse>>>>, - >, + default: Rc>>>>, renderer: Rc, _chunk_size: usize, _follow_symlinks: bool, @@ -817,9 +815,7 @@ pub struct StaticFilesService { directory: PathBuf, index: Option, show_index: bool, - default: Rc< - RefCell, ServiceResponse>>>>, - >, + default: Rc>>>>, renderer: Rc, _chunk_size: usize, _follow_symlinks: bool, @@ -838,8 +834,8 @@ impl Service for StaticFilesService { fn call(&mut self, req: Self::Request) -> Self::Future { let mut req = req; - let real_path = match PathBuf::from_request(&mut req).poll() { - Ok(Async::Ready(item)) => item, + let real_path = match PathBufWrp::from_request(&mut req).poll() { + Ok(Async::Ready(item)) => item.0, Ok(Async::NotReady) => unreachable!(), Err(e) => return err(Error::from(e)), }; @@ -888,7 +884,9 @@ impl Service for StaticFilesService { } } -impl

FromRequest

for PathBuf { +struct PathBufWrp(PathBuf); + +impl

FromRequest

for PathBufWrp { type Error = UriSegmentError; type Future = FutureResult; @@ -917,7 +915,7 @@ impl

FromRequest

for PathBuf { } } - ok(buf) + ok(PathBufWrp(buf)) } } diff --git a/examples/basic.rs b/examples/basic.rs index 9f4701eb..a18581f9 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -2,7 +2,7 @@ use futures::IntoFuture; use actix_http::{h1, http::Method, Response}; use actix_server::Server; -use actix_web2::{middleware, App, Error, HttpRequest, Resource}; +use actix_web::{middleware, App, Error, HttpRequest, Resource}; fn index(req: HttpRequest) -> &'static str { println!("REQ: {:?}", req); diff --git a/src/app.rs b/src/app.rs index 6ed9b144..7c077706 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,6 +5,7 @@ use std::rc::Rc; use actix_http::body::{Body, MessageBody}; use actix_http::{Extensions, PayloadStream, Request, Response}; use actix_router::{Path, ResourceDef, ResourceInfo, Router, Url}; +use actix_service::boxed::{self, BoxedNewService, BoxedService}; use actix_service::{ AndThenNewService, ApplyNewService, IntoNewService, IntoNewTransform, NewService, NewTransform, Service, @@ -12,13 +13,12 @@ use actix_service::{ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; -use crate::helpers::{ - BoxedHttpNewService, BoxedHttpService, DefaultNewService, HttpDefaultNewService, -}; use crate::resource::Resource; use crate::service::{ServiceRequest, ServiceResponse}; use crate::state::{State, StateFactory, StateFactoryResult}; +type HttpService

= BoxedService, ServiceResponse, ()>; +type HttpNewService

= BoxedNewService<(), ServiceRequest

, ServiceResponse, (), ()>; type BoxedResponse = Box>; pub trait HttpServiceFactory { @@ -31,18 +31,9 @@ pub trait HttpServiceFactory { /// Application builder pub struct App { - services: Vec<( - ResourceDef, - BoxedHttpNewService, ServiceResponse>, - )>, - default: Option, ServiceResponse>>>, - defaults: Vec< - Rc< - RefCell< - Option, ServiceResponse>>>, - >, - >, - >, + services: Vec<(ResourceDef, HttpNewService

)>, + default: Option>>, + defaults: Vec>>>>>, endpoint: T, factory_ref: Rc>>>, extensions: Extensions, @@ -181,10 +172,8 @@ where let rdef = ResourceDef::new(path); let resource = f(Resource::new()); self.defaults.push(resource.get_default()); - self.services.push(( - rdef, - Box::new(HttpNewService::new(resource.into_new_service())), - )); + self.services + .push((rdef, boxed::new_service(resource.into_new_service()))); self } @@ -203,9 +192,9 @@ where > + 'static, { // create and configure default resource - self.default = Some(Rc::new(Box::new(DefaultNewService::new( - f(Resource::new()).into_new_service(), - )))); + self.default = Some(Rc::new(boxed::new_service( + f(Resource::new()).into_new_service().map_init_err(|_| ()), + ))); self } @@ -223,7 +212,7 @@ where { self.services.push(( rdef.into(), - Box::new(HttpNewService::new(factory.into_new_service())), + boxed::new_service(factory.into_new_service().map_init_err(|_| ())), )); self } @@ -422,15 +411,10 @@ impl

Service for AppStateService

{ } pub struct AppFactory

{ - services: Rc< - Vec<( - ResourceDef, - BoxedHttpNewService, ServiceResponse>, - )>, - >, + services: Rc)>>, } -impl

NewService for AppFactory

{ +impl NewService for AppFactory

{ type Request = ServiceRequest

; type Response = ServiceResponse; type Error = (); @@ -454,8 +438,7 @@ impl

NewService for AppFactory

{ } } -type HttpServiceFut

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

= Box, Error = ()>>; /// Create app service #[doc(hidden)] @@ -465,10 +448,7 @@ pub struct CreateAppService

{ enum CreateAppServiceItem

{ Future(Option, HttpServiceFut

), - Service( - ResourceDef, - BoxedHttpService, ServiceResponse>, - ), + Service(ResourceDef, HttpService

), } impl

Future for CreateAppService

{ @@ -522,7 +502,7 @@ impl

Future for CreateAppService

{ } pub struct AppService

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

, ResourceInfo)>, } @@ -561,68 +541,6 @@ impl Future for AppServiceResponse { } } -struct HttpNewService>>(T); - -impl HttpNewService -where - T: NewService, Response = ServiceResponse, Error = ()>, - T::Future: 'static, - ::Future: 'static, -{ - pub fn new(service: T) -> Self { - HttpNewService(service) - } -} - -impl NewService for HttpNewService -where - T: NewService, Response = ServiceResponse, Error = ()>, - T::Future: 'static, - T::Service: 'static, - ::Future: 'static, -{ - type Request = ServiceRequest

; - type Response = ServiceResponse; - type Error = (); - type InitError = (); - type Service = BoxedHttpService, Self::Response>; - 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, - _t: PhantomData, - }); - Ok(service) - })) - } -} - -struct HttpServiceWrapper { - service: T, - _t: PhantomData<(P,)>, -} - -impl Service for HttpServiceWrapper -where - T::Future: 'static, - T: Service, Response = ServiceResponse, Error = ()>, -{ - type Request = ServiceRequest

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

) -> Self::Future { - Box::new(self.service.call(req)) - } -} - #[doc(hidden)] pub struct AppEntry

{ factory: Rc>>>, @@ -634,7 +552,7 @@ impl

AppEntry

{ } } -impl

NewService for AppEntry

{ +impl NewService for AppEntry

{ type Request = ServiceRequest

; type Response = ServiceResponse; type Error = (); diff --git a/src/framed_app.rs b/src/framed_app.rs deleted file mode 100644 index ba925414..00000000 --- a/src/framed_app.rs +++ /dev/null @@ -1,240 +0,0 @@ -use std::marker::PhantomData; -use std::rc::Rc; - -use actix_codec::Framed; -use actix_http::h1::Codec; -use actix_http::{Request, Response, SendResponse}; -use actix_router::{Path, Router, Url}; -use actix_service::{IntoNewService, NewService, Service}; -use actix_utils::cloneable::CloneableService; -use futures::{Async, Future, Poll}; -use tokio_io::{AsyncRead, AsyncWrite}; - -use crate::app::{HttpServiceFactory, State}; -use crate::framed_handler::FramedRequest; -use crate::helpers::{BoxedHttpNewService, BoxedHttpService, HttpNewService}; -use crate::request::Request as WebRequest; - -pub type FRequest = (Request, Framed); -type BoxedResponse = Box>; - -/// Application builder -pub struct FramedApp { - services: Vec<(String, BoxedHttpNewService, ()>)>, - state: State, -} - -impl FramedApp { - pub fn new() -> Self { - FramedApp { - services: Vec::new(), - state: State::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, Response = ()> + 'static, - ::Future: 'static, - ::Service: Service>, - <::Service as Service>::Future: 'static, - { - let path = factory.path().to_string(); - self.services.push(( - path, - Box::new(HttpNewService::new(factory.create(self.state.clone()))), - )); - self - } - - pub fn register_service(&mut self, factory: U) - where - U: HttpServiceFactory, - U::Factory: NewService, Response = ()> + 'static, - ::Future: 'static, - ::Service: Service>, - <::Service as Service>::Future: 'static, - { - let path = factory.path().to_string(); - self.services.push(( - path, - Box::new(HttpNewService::new(factory.create(self.state.clone()))), - )); - } -} - -impl IntoNewService> for FramedApp -where - T: AsyncRead + AsyncWrite, -{ - fn into_new_service(self) -> FramedAppFactory { - FramedAppFactory { - state: self.state, - services: Rc::new(self.services), - _t: PhantomData, - } - } -} - -#[derive(Clone)] -pub struct FramedAppFactory { - state: State, - services: Rc, ()>)>>, - _t: PhantomData, -} - -impl NewService for FramedAppFactory -where - T: AsyncRead + AsyncWrite, -{ - type Request = FRequest; - type Response = (); - type Error = (); - type InitError = (); - type Service = CloneableService>; - type Future = CreateService; - - fn new_service(&self) -> 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(), - // default: self.default.take().expect("something is wrong"), - }))) - } else { - Ok(Async::NotReady) - } - } -} - -pub struct FramedAppService { - state: State, - router: Router, ()>>, -} - -impl Service for FramedAppService -where - T: AsyncRead + AsyncWrite, -{ - type Request = FRequest; - type Response = (); - type Error = (); - type Future = BoxedResponse; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - // let mut ready = true; - // for service in &mut self.services { - // if let Async::NotReady = service.poll_ready()? { - // ready = false; - // } - // } - // if ready { - // Ok(Async::Ready(())) - // } else { - // Ok(Async::NotReady) - // } - 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( - WebRequest::new(self.state.clone(), req, path), - framed, - )); - } - // for item in &mut self.services { - // req = match item.handle(req) { - // Ok(fut) => return fut, - // Err(req) => req, - // }; - // } - // self.default.call(req) - Box::new( - SendResponse::send(framed, Response::NotFound().finish().into()) - .map(|_| ()) - .map_err(|_| ()), - ) - } -} diff --git a/src/framed_handler.rs b/src/framed_handler.rs deleted file mode 100644 index 109b5f0a..00000000 --- a/src/framed_handler.rs +++ /dev/null @@ -1,379 +0,0 @@ -use std::marker::PhantomData; -use std::rc::Rc; - -use actix_codec::Framed; -use actix_http::{h1::Codec, Error}; -use actix_service::{NewService, Service}; -use futures::future::{ok, FutureResult}; -use futures::{Async, Future, IntoFuture, Poll}; -use log::error; - -use crate::handler::FromRequest; -use crate::request::Request; - -pub struct FramedError { - pub err: Error, - pub framed: Framed, -} - -pub struct FramedRequest { - req: Request, - framed: Framed, - param: Ex, -} - -impl FramedRequest { - pub fn new(req: Request, framed: Framed) -> Self { - Self { - req, - framed, - param: (), - } - } -} - -impl FramedRequest { - pub fn request(&self) -> &Request { - &self.req - } - - pub fn request_mut(&mut self) -> &mut Request { - &mut self.req - } - - pub fn into_parts(self) -> (Request, Framed, Ex) { - (self.req, self.framed, self.param) - } - - pub fn map(self, op: F) -> FramedRequest - where - F: FnOnce(Ex) -> Ex2, - { - FramedRequest { - req: self.req, - framed: self.framed, - param: op(self.param), - } - } -} - -/// T handler converter factory -pub trait FramedFactory: Clone + 'static -where - R: IntoFuture, - E: Into, -{ - fn call(&self, framed: Framed, param: T, extra: Ex) -> R; -} - -#[doc(hidden)] -pub struct FramedHandle -where - F: FramedFactory, - R: IntoFuture, - E: Into, -{ - hnd: F, - _t: PhantomData<(S, Io, Ex, T, R, E)>, -} - -impl FramedHandle -where - F: FramedFactory, - R: IntoFuture, - E: Into, -{ - pub fn new(hnd: F) -> Self { - FramedHandle { - hnd, - _t: PhantomData, - } - } -} -impl NewService for FramedHandle -where - F: FramedFactory, - R: IntoFuture, - E: Into, -{ - type Request = (T, FramedRequest); - type Response = (); - type Error = FramedError; - type InitError = (); - type Service = FramedHandleService; - type Future = FutureResult; - - fn new_service(&self) -> Self::Future { - ok(FramedHandleService { - hnd: self.hnd.clone(), - _t: PhantomData, - }) - } -} - -#[doc(hidden)] -pub struct FramedHandleService -where - F: FramedFactory, - R: IntoFuture, - E: Into, -{ - hnd: F, - _t: PhantomData<(S, Io, Ex, T, R, E)>, -} - -impl Service for FramedHandleService -where - F: FramedFactory, - R: IntoFuture, - E: Into, -{ - type Request = (T, FramedRequest); - type Response = (); - type Error = FramedError; - type Future = FramedHandleServiceResponse; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, (param, framed): (T, FramedRequest)) -> Self::Future { - let (_, framed, ex) = framed.into_parts(); - FramedHandleServiceResponse { - fut: self.hnd.call(framed, param, ex).into_future(), - _t: PhantomData, - } - } -} - -#[doc(hidden)] -pub struct FramedHandleServiceResponse { - fut: F, - _t: PhantomData, -} - -impl Future for FramedHandleServiceResponse -where - F: Future, - F::Error: Into, -{ - type Item = (); - type Error = FramedError; - - fn poll(&mut self) -> Poll { - match self.fut.poll() { - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(res)) => Ok(Async::Ready(res.into())), - Err(e) => { - let e: Error = e.into(); - error!("Error in handler: {:?}", e); - Ok(Async::Ready(())) - } - } - } -} - -pub struct FramedExtract -where - T: FromRequest, -{ - cfg: Rc, - _t: PhantomData<(Io, Ex)>, -} - -impl FramedExtract -where - T: FromRequest + 'static, -{ - pub fn new(cfg: T::Config) -> FramedExtract { - FramedExtract { - cfg: Rc::new(cfg), - _t: PhantomData, - } - } -} -impl NewService for FramedExtract -where - T: FromRequest + 'static, -{ - type Request = FramedRequest; - type Response = (T, FramedRequest); - type Error = FramedError; - type InitError = (); - type Service = FramedExtractService; - type Future = FutureResult; - - fn new_service(&self) -> Self::Future { - ok(FramedExtractService { - cfg: self.cfg.clone(), - _t: PhantomData, - }) - } -} - -pub struct FramedExtractService -where - T: FromRequest, -{ - cfg: Rc, - _t: PhantomData<(Io, Ex)>, -} - -impl Service for FramedExtractService -where - T: FromRequest + 'static, -{ - type Request = FramedRequest; - type Response = (T, FramedRequest); - type Error = FramedError; - type Future = FramedExtractResponse; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, req: FramedRequest) -> Self::Future { - FramedExtractResponse { - fut: T::from_request(&req.request(), self.cfg.as_ref()), - req: Some(req), - } - } -} - -pub struct FramedExtractResponse -where - T: FromRequest + 'static, -{ - req: Option>, - fut: T::Future, -} - -impl Future for FramedExtractResponse -where - T: FromRequest + 'static, -{ - type Item = (T, FramedRequest); - type Error = FramedError; - - fn poll(&mut self) -> Poll { - match self.fut.poll() { - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(item)) => Ok(Async::Ready((item, self.req.take().unwrap()))), - Err(err) => Err(FramedError { - err: err.into(), - framed: self.req.take().unwrap().into_parts().1, - }), - } - } -} - -macro_rules! factory_tuple ({ ($(($nex:tt, $Ex:ident)),+), $(($n:tt, $T:ident)),+} => { - impl FramedFactory for Func - where Func: Fn(Framed, $($Ex,)+ $($T,)+) -> Res + Clone + 'static, - $($T: FromRequest + 'static,)+ - Res: IntoFuture + 'static, - Err: Into, - { - fn call(&self, framed: Framed, param: ($($T,)+), extra: ($($Ex,)+)) -> Res { - (self)(framed, $(extra.$nex,)+ $(param.$n,)+) - } - } -}); - -macro_rules! factory_tuple_unit ({$(($n:tt, $T:ident)),+} => { - impl FramedFactory for Func - where Func: Fn(Framed, $($T,)+) -> Res + Clone + 'static, - $($T: FromRequest + 'static,)+ - Res: IntoFuture + 'static, - Err: Into, - { - fn call(&self, framed: Framed, param: ($($T,)+), _extra: () ) -> Res { - (self)(framed, $(param.$n,)+) - } - } -}); - -#[cfg_attr(rustfmt, rustfmt_skip)] -mod m { - use super::*; - -factory_tuple_unit!((0, A)); -factory_tuple!(((0, Aex)), (0, A)); -factory_tuple!(((0, Aex), (1, Bex)), (0, A)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A)); - -factory_tuple_unit!((0, A), (1, B)); -factory_tuple!(((0, Aex)), (0, A), (1, B)); -factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B)); - -factory_tuple_unit!((0, A), (1, B), (2, C)); -factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C)); -factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C)); - -factory_tuple_unit!((0, A), (1, B), (2, C), (3, D)); -factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D)); -factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D)); - -factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E)); -factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E)); -factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E)); - -factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); -factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); -factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); - -factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); -factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); -factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); - -factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); -factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); -factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); - -factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); -factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); -factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); - -factory_tuple_unit!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); -factory_tuple!(((0, Aex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); -factory_tuple!(((0, Aex), (1, Bex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); -factory_tuple!(((0, Aex), (1, Bex), (2, Cex), (3, Dex), (4, Eex), (5, Fex)), (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); -} diff --git a/src/framed_route.rs b/src/framed_route.rs deleted file mode 100644 index 90555a9c..00000000 --- a/src/framed_route.rs +++ /dev/null @@ -1,448 +0,0 @@ -use std::marker::PhantomData; - -use actix_http::http::{HeaderName, HeaderValue, Method}; -use actix_http::Error; -use actix_service::{IntoNewService, NewService, NewServiceExt, Service}; -use futures::{try_ready, Async, Future, IntoFuture, Poll}; -use log::{debug, error}; -use tokio_io::{AsyncRead, AsyncWrite}; - -use crate::app::{HttpServiceFactory, State}; -use crate::framed_handler::{ - FramedError, FramedExtract, FramedFactory, FramedHandle, FramedRequest, -}; -use crate::handler::FromRequest; - -/// 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 { - service: T, - pattern: String, - methods: Vec, - headers: Vec<(HeaderName, HeaderValue)>, - state: PhantomData<(S, Io)>, -} - -impl FramedRoute { - pub fn build(path: &str) -> FramedRoutePatternBuilder { - FramedRoutePatternBuilder::new(path) - } - - pub fn get(path: &str) -> FramedRoutePatternBuilder { - FramedRoutePatternBuilder::new(path).method(Method::GET) - } - - pub fn post(path: &str) -> FramedRoutePatternBuilder { - FramedRoutePatternBuilder::new(path).method(Method::POST) - } - - pub fn put(path: &str) -> FramedRoutePatternBuilder { - FramedRoutePatternBuilder::new(path).method(Method::PUT) - } - - pub fn delete(path: &str) -> FramedRoutePatternBuilder { - FramedRoutePatternBuilder::new(path).method(Method::DELETE) - } -} - -impl FramedRoute -where - T: NewService< - Request = FramedRequest, - Response = (), - Error = FramedError, - > + 'static, -{ - pub fn new>(pattern: &str, factory: F) -> Self { - FramedRoute { - pattern: pattern.to_string(), - service: factory.into_new_service(), - headers: Vec::new(), - methods: Vec::new(), - state: PhantomData, - } - } - - pub fn method(mut self, method: Method) -> Self { - self.methods.push(method); - self - } - - pub fn header(mut self, name: HeaderName, value: HeaderValue) -> Self { - self.headers.push((name, value)); - self - } -} - -impl HttpServiceFactory for FramedRoute -where - Io: AsyncRead + AsyncWrite + 'static, - T: NewService< - Request = FramedRequest, - Response = (), - Error = FramedError, - > + 'static, - T::Service: 'static, -{ - type Factory = FramedRouteFactory; - - fn path(&self) -> &str { - &self.pattern - } - - fn create(self, state: State) -> Self::Factory { - FramedRouteFactory { - state, - service: self.service, - pattern: self.pattern, - methods: self.methods, - headers: self.headers, - _t: PhantomData, - } - } -} - -pub struct FramedRouteFactory { - service: T, - pattern: String, - methods: Vec, - headers: Vec<(HeaderName, HeaderValue)>, - state: State, - _t: PhantomData, -} - -impl NewService for FramedRouteFactory -where - Io: AsyncRead + AsyncWrite + 'static, - T: NewService< - Request = FramedRequest, - Response = (), - Error = FramedError, - > + 'static, - T::Service: 'static, -{ - type Request = FramedRequest; - type Response = T::Response; - type Error = (); - type InitError = T::InitError; - type Service = FramedRouteService; - type Future = CreateRouteService; - - fn new_service(&self) -> Self::Future { - CreateRouteService { - fut: self.service.new_service(), - pattern: self.pattern.clone(), - methods: self.methods.clone(), - headers: self.headers.clone(), - state: self.state.clone(), - _t: PhantomData, - } - } -} - -pub struct CreateRouteService { - fut: T::Future, - pattern: String, - methods: Vec, - headers: Vec<(HeaderName, HeaderValue)>, - state: State, - _t: PhantomData, -} - -impl Future for CreateRouteService -where - T: NewService< - Request = FramedRequest, - Response = (), - Error = FramedError, - >, -{ - type Item = FramedRouteService; - type Error = T::InitError; - - fn poll(&mut self) -> Poll { - let service = try_ready!(self.fut.poll()); - - Ok(Async::Ready(FramedRouteService { - service, - state: self.state.clone(), - pattern: self.pattern.clone(), - methods: self.methods.clone(), - headers: self.headers.clone(), - _t: PhantomData, - })) - } -} - -pub struct FramedRouteService { - service: T, - pattern: String, - methods: Vec, - headers: Vec<(HeaderName, HeaderValue)>, - state: State, - _t: PhantomData, -} - -impl Service for FramedRouteService -where - Io: AsyncRead + AsyncWrite + 'static, - T: Service, Response = (), Error = FramedError> - + 'static, -{ - type Request = FramedRequest; - type Response = (); - type Error = (); - type Future = FramedRouteServiceResponse; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.service.poll_ready().map_err(|e| { - debug!("Service not available: {}", e.err); - () - }) - } - - fn call(&mut self, req: FramedRequest) -> Self::Future { - FramedRouteServiceResponse { - fut: self.service.call(req), - send: None, - _t: PhantomData, - } - } -} - -// impl HttpService<(Request, Framed)> for FramedRouteService -// where -// Io: AsyncRead + AsyncWrite + 'static, -// S: 'static, -// T: Service, Response = (), Error = FramedError> + 'static, -// { -// fn handle( -// &mut self, -// (req, framed): (Request, Framed), -// ) -> Result)> { -// if self.methods.is_empty() -// || !self.methods.is_empty() && self.methods.contains(req.method()) -// { -// if let Some(params) = self.pattern.match_with_params(&req, 0) { -// return Ok(FramedRouteServiceResponse { -// fut: self.service.call(FramedRequest::new( -// WebRequest::new(self.state.clone(), req, params), -// framed, -// )), -// send: None, -// _t: PhantomData, -// }); -// } -// } -// Err((req, framed)) -// } -// } - -#[doc(hidden)] -pub struct FramedRouteServiceResponse { - fut: F, - send: Option>>, - _t: PhantomData, -} - -impl Future for FramedRouteServiceResponse -where - F: Future>, - Io: AsyncRead + AsyncWrite + 'static, -{ - type Item = (); - type Error = (); - - fn poll(&mut self) -> Poll { - if let Some(ref mut fut) = self.send { - return match fut.poll() { - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(_)) => Ok(Async::Ready(())), - Err(e) => { - debug!("Error during error response send: {}", e); - Err(()) - } - }; - }; - - match self.fut.poll() { - Ok(Async::NotReady) => Ok(Async::NotReady), - Ok(Async::Ready(_)) => Ok(Async::Ready(())), - Err(e) => { - error!("Error occurred during request handling: {}", e.err); - Err(()) - } - } - } -} - -pub struct FramedRoutePatternBuilder { - pattern: String, - methods: Vec, - headers: Vec<(HeaderName, HeaderValue)>, - state: PhantomData<(Io, S)>, -} - -impl FramedRoutePatternBuilder { - fn new(path: &str) -> FramedRoutePatternBuilder { - FramedRoutePatternBuilder { - pattern: path.to_string(), - methods: Vec::new(), - headers: Vec::new(), - state: PhantomData, - } - } - - pub fn method(mut self, method: Method) -> Self { - self.methods.push(method); - self - } - - pub fn map>( - self, - md: F, - ) -> FramedRouteBuilder - where - T: NewService< - Request = FramedRequest, - Response = FramedRequest, - Error = FramedError, - InitError = (), - >, - { - FramedRouteBuilder { - service: md.into_new_service(), - pattern: self.pattern, - methods: self.methods, - headers: self.headers, - state: PhantomData, - } - } - - pub fn with( - self, - handler: F, - ) -> FramedRoute< - Io, - impl NewService< - Request = FramedRequest, - Response = (), - Error = FramedError, - InitError = (), - >, - S, - > - where - F: FramedFactory, - P: FromRequest + 'static, - R: IntoFuture, - E: Into, - { - FramedRoute { - service: FramedExtract::new(P::Config::default()) - .and_then(FramedHandle::new(handler)), - pattern: self.pattern, - methods: self.methods, - headers: self.headers, - state: PhantomData, - } - } -} - -pub struct FramedRouteBuilder { - service: T, - pattern: String, - methods: Vec, - headers: Vec<(HeaderName, HeaderValue)>, - state: PhantomData<(Io, S, U1, U2)>, -} - -impl FramedRouteBuilder -where - T: NewService< - Request = FramedRequest, - Response = FramedRequest, - Error = FramedError, - InitError = (), - >, -{ - pub fn new>(path: &str, factory: F) -> Self { - FramedRouteBuilder { - service: factory.into_new_service(), - pattern: path.to_string(), - methods: Vec::new(), - headers: Vec::new(), - state: PhantomData, - } - } - - pub fn method(mut self, method: Method) -> Self { - self.methods.push(method); - self - } - - pub fn map>( - self, - md: F, - ) -> FramedRouteBuilder< - Io, - S, - impl NewService< - Request = FramedRequest, - Response = FramedRequest, - Error = FramedError, - InitError = (), - >, - U1, - U3, - > - where - K: NewService< - Request = FramedRequest, - Response = FramedRequest, - Error = FramedError, - InitError = (), - >, - { - FramedRouteBuilder { - service: self.service.from_err().and_then(md.into_new_service()), - pattern: self.pattern, - methods: self.methods, - headers: self.headers, - state: PhantomData, - } - } - - pub fn with( - self, - handler: F, - ) -> FramedRoute< - Io, - impl NewService< - Request = FramedRequest, - Response = (), - Error = FramedError, - InitError = (), - >, - S, - > - where - F: FramedFactory, - P: FromRequest + 'static, - R: IntoFuture, - E: Into, - { - FramedRoute { - service: self - .service - .and_then(FramedExtract::new(P::Config::default())) - .and_then(FramedHandle::new(handler)), - pattern: self.pattern, - methods: self.methods, - headers: self.headers, - state: PhantomData, - } - } -} diff --git a/src/helpers.rs b/src/helpers.rs deleted file mode 100644 index 860a02a4..00000000 --- a/src/helpers.rs +++ /dev/null @@ -1,180 +0,0 @@ -use actix_http::Response; -use actix_service::{NewService, Service}; -use futures::future::{ok, FutureResult}; -use futures::{Future, Poll}; - -pub(crate) type BoxedHttpService = Box< - Service< - Request = Req, - Response = Res, - Error = (), - Future = Box>, - >, ->; - -pub(crate) type BoxedHttpNewService = Box< - NewService< - Request = Req, - Response = Res, - 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, - ::Future: 'static, -{ - pub fn new(service: T) -> Self { - HttpNewService(service) - } -} - -impl NewService for HttpNewService -where - T: NewService, - T::Request: 'static, - T::Response: 'static, - T::Future: 'static, - T::Service: Service + 'static, - ::Future: 'static, -{ - type Request = T::Request; - type Response = T::Response; - type 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, - T::Request: 'static, - T::Response: 'static, - T::Future: 'static, -{ - type Request = T::Request; - type Response = T::Response; - type Error = (); - type Future = Box>; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.service.poll_ready().map_err(|_| ()) - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - Box::new(self.service.call(req).map_err(|_| ())) - } -} - -pub(crate) fn not_found(_: Req) -> FutureResult { - ok(Response::NotFound().finish()) -} - -pub(crate) type HttpDefaultService = Box< - Service< - Request = Req, - Response = Res, - Error = (), - Future = Box>, - >, ->; - -pub(crate) type HttpDefaultNewService = Box< - NewService< - Request = Req, - Response = Res, - Error = (), - InitError = (), - Service = HttpDefaultService, - Future = Box, Error = ()>>, - >, ->; - -pub(crate) struct DefaultNewService { - service: T, -} - -impl DefaultNewService -where - T: NewService + 'static, - T::Future: 'static, - ::Future: 'static, -{ - pub fn new(service: T) -> Self { - DefaultNewService { service } - } -} - -impl NewService for DefaultNewService -where - T: NewService + 'static, - T::Request: 'static, - T::Future: 'static, - T::Service: 'static, - ::Future: 'static, -{ - type Request = T::Request; - type Response = T::Response; - type Error = (); - type InitError = (); - type Service = HttpDefaultService; - type Future = Box>; - - fn new_service(&self, _: &()) -> Self::Future { - Box::new( - self.service - .new_service(&()) - .map_err(|_| ()) - .and_then(|service| { - let service: HttpDefaultService<_, _> = - Box::new(DefaultServiceWrapper { service }); - Ok(service) - }), - ) - } -} - -struct DefaultServiceWrapper { - service: T, -} - -impl Service for DefaultServiceWrapper -where - T: Service + 'static, - T::Future: 'static, -{ - type Request = T::Request; - type Response = T::Response; - type Error = (); - type Future = Box>; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.service.poll_ready().map_err(|_| ()) - } - - fn call(&mut self, req: T::Request) -> Self::Future { - Box::new(self.service.call(req).map_err(|_| ())) - } -} diff --git a/src/lib.rs b/src/lib.rs index f09c11ce..b4b12eb1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,11 +3,10 @@ mod app; mod extractor; pub mod handler; -mod helpers; +// mod helpers; // mod info; pub mod blocking; pub mod filter; -pub mod fs; pub mod middleware; mod request; mod resource; diff --git a/src/resource.rs b/src/resource.rs index 88f7ae5a..80ac8d83 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -2,6 +2,7 @@ use std::cell::RefCell; use std::rc::Rc; use actix_http::{http::Method, Error, Response}; +use actix_service::boxed::{self, BoxedNewService, BoxedService}; use actix_service::{ ApplyNewService, IntoNewService, IntoNewTransform, NewService, NewTransform, Service, }; @@ -9,11 +10,13 @@ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; use crate::handler::{AsyncFactory, Factory, FromRequest}; -use crate::helpers::{DefaultNewService, HttpDefaultNewService, HttpDefaultService}; use crate::responder::Responder; use crate::route::{CreateRouteService, Route, RouteBuilder, RouteService}; use crate::service::{ServiceRequest, ServiceResponse}; +type HttpService

= BoxedService, ServiceResponse, ()>; +type HttpNewService

= BoxedNewService<(), ServiceRequest

, ServiceResponse, (), ()>; + /// Resource route definition /// /// Route uses builder-like pattern for configuration. @@ -21,9 +24,7 @@ use crate::service::{ServiceRequest, ServiceResponse}; pub struct Resource> { routes: Vec>, endpoint: T, - default: Rc< - RefCell, ServiceResponse>>>>, - >, + default: Rc>>>>, factory_ref: Rc>>>, } @@ -277,17 +278,14 @@ where > + 'static, { // create and configure default resource - self.default = Rc::new(RefCell::new(Some(Rc::new(Box::new( - DefaultNewService::new(f(Resource::new()).into_new_service()), + self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::new_service( + f(Resource::new()).into_new_service().map_init_err(|_| ()), ))))); self } - pub(crate) fn get_default( - &self, - ) -> Rc, ServiceResponse>>>>> - { + pub(crate) fn get_default(&self) -> Rc>>>> { self.default.clone() } } @@ -313,12 +311,10 @@ where pub struct ResourceFactory

{ routes: Vec>, - default: Rc< - RefCell, ServiceResponse>>>>, - >, + default: Rc>>>>, } -impl

NewService for ResourceFactory

{ +impl NewService for ResourceFactory

{ type Request = ServiceRequest

; type Response = ServiceResponse; type Error = (); @@ -352,15 +348,8 @@ enum CreateRouteServiceItem

{ pub struct CreateResourceService

{ fut: Vec>, - default: Option, ServiceResponse>>, - default_fut: Option< - Box< - Future< - Item = HttpDefaultService, ServiceResponse>, - Error = (), - >, - >, - >, + default: Option>, + default_fut: Option, Error = ()>>>, } impl

Future for CreateResourceService

{ @@ -413,7 +402,7 @@ impl

Future for CreateResourceService

{ pub struct ResourceService

{ routes: Vec>, - default: Option, ServiceResponse>>, + default: Option>, } impl

Service for ResourceService

{ @@ -461,7 +450,7 @@ impl

ResourceEndpoint

{ } } -impl

NewService for ResourceEndpoint

{ +impl NewService for ResourceEndpoint

{ type Request = ServiceRequest

; type Response = ServiceResponse; type Error = (); diff --git a/tests/test_server.rs b/tests/test_server.rs index 2d01d270..590cc0e7 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -12,7 +12,7 @@ use flate2::write::ZlibDecoder; use futures::stream::once; //Future, Stream use rand::{distributions::Alphanumeric, Rng}; -use actix_web2::{middleware, App}; +use actix_web::{middleware, App}; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ From bc3c29c39868bf940f19de1960e0f876b15c5636 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 00:04:39 -0800 Subject: [PATCH 003/109] update version --- .travis.yml | 10 ++++------ Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9b1bcff5..077989d2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,15 +32,13 @@ script: - | if [[ "$TRAVIS_RUST_VERSION" != "nightly" ]]; then cargo clean - cargo check --features rust-tls - cargo check --features ssl - cargo check --features tls - cargo test --features="ssl,tls,rust-tls,uds" -- --nocapture + cargo check + cargo test -- --nocapture fi - | if [[ "$TRAVIS_RUST_VERSION" == "nightly" ]]; then RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install -f cargo-tarpaulin - RUST_BACKTRACE=1 cargo tarpaulin --features="ssl,tls,rust-tls" --out Xml + RUST_BACKTRACE=1 cargo tarpaulin --out Xml bash <(curl -s https://codecov.io/bash) echo "Uploaded code coverage" fi @@ -49,7 +47,7 @@ script: after_success: - | if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then - cargo doc --features "ssl,tls,rust-tls,session" --no-deps && + cargo doc --no-deps && 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/Cargo.toml b/Cargo.toml index e9c298fa..3cc42d80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "0.1.0" +version = "1.0.0-alpha.1" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" From fdf30118378f0b84004d39624daa7ba36ac67535 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 09:05:07 -0800 Subject: [PATCH 004/109] add responder for unit type --- Cargo.toml | 1 + src/application.rs | 785 --------------------------------------------- src/lib.rs | 1 - src/responder.rs | 26 +- 4 files changed, 15 insertions(+), 798 deletions(-) delete mode 100644 src/application.rs diff --git a/Cargo.toml b/Cargo.toml index 3cc42d80..bfd06101 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ actix-router = { git = "https://github.com/actix/actix-net.git" } bytes = "0.4" futures = "0.1" derive_more = "0.14" +either = "1.5.1" log = "0.4" lazy_static = "1.2" mime = "0.3" diff --git a/src/application.rs b/src/application.rs deleted file mode 100644 index 6ca4ce28..00000000 --- a/src/application.rs +++ /dev/null @@ -1,785 +0,0 @@ -use std::rc::Rc; - -use actix_http::http::ContentEncoding; -use actix_http::{Error, Request, Response}; -use actix_service::Service; -use futures::{Async, Future, Poll}; - -use handler::{AsyncResult, FromRequest, Handler, Responder, WrapHandler}; -use http::Method; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -// use middleware::Middleware; -// use pipeline::{Pipeline, PipelineHandler}; -use pred::Predicate; -use resource::Resource; -use router::{ResourceDef, Router}; -// use scope::Scope; -// use server::{HttpHandler, HttpHandlerTask, IntoHttpHandler, Request}; -use with::WithFactory; - -/// Application -pub struct HttpApplication { - state: Rc, - prefix: String, - prefix_len: usize, - inner: Rc>, - filters: Option>>>, - // middlewares: Rc>>>, -} - -#[doc(hidden)] -pub struct Inner { - router: Router, - encoding: ContentEncoding, -} - -// impl PipelineHandler for Inner { -// #[inline] -// fn encoding(&self) -> ContentEncoding { -// self.encoding -// } - -// fn handle(&self, req: &HttpRequest) -> AsyncResult { -// self.router.handle(req) -// } -// } - -impl HttpApplication { - #[cfg(test)] - pub(crate) fn run(&self, req: Request) -> AsyncResult { - let info = self - .inner - .router - .recognize(&req, &self.state, self.prefix_len); - let req = HttpRequest::new(req, Rc::clone(&self.state), info); - - self.inner.handle(&req) - } -} - -impl Service for HttpApplication { - // type Task = Pipeline>; - type Request = actix_http::Request; - type Response = actix_http::Response; - type Error = Error; - type Future = Box>; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, msg: actix_http::Request) -> Self::Future { - let m = { - if self.prefix_len == 0 { - true - } else { - let path = msg.path(); - path.starts_with(&self.prefix) - && (path.len() == self.prefix_len - || path.split_at(self.prefix_len).1.starts_with('/')) - } - }; - if m { - if let Some(ref filters) = self.filters { - //for filter in filters { - // if !filter.check(&msg, &self.state) { - //return Err(msg); - unimplemented!() - // } - //} - } - - let info = self - .inner - .router - .recognize(&msg, &self.state, self.prefix_len); - - let inner = Rc::clone(&self.inner); - // let req = HttpRequest::new(msg, Rc::clone(&self.state), info); - // Ok(Pipeline::new(req, inner)) - unimplemented!() - } else { - // Err(msg) - unimplemented!() - } - } -} - -struct ApplicationParts { - state: S, - prefix: String, - router: Router, - encoding: ContentEncoding, - // middlewares: Vec>>, - filters: Vec>>, -} - -/// Structure that follows the builder pattern for building application -/// instances. -pub struct App { - parts: Option>, -} - -impl App -where - S: 'static, -{ - /// Set application prefix. - /// - /// Only requests that match the application's prefix get - /// processed by this application. - /// - /// The application prefix always contains a leading slash (`/`). - /// If the supplied prefix does not contain leading slash, it is - /// inserted. - /// - /// Prefix should consist of valid path segments. i.e for an - /// application with the prefix `/app` any request with the paths - /// `/app`, `/app/` or `/app/test` would match, but the path - /// `/application` would not. - /// - /// In the following example only requests with an `/app/` path - /// prefix get handled. Requests with path `/app/test/` would be - /// handled, while requests with the paths `/application` or - /// `/other/...` would return `NOT FOUND`. It is also possible to - /// handle `/app` path, to do this you can register resource for - /// empty string `""` - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpResponse}; - /// - /// fn main() { - /// let app = App::new() - /// .prefix("/app") - /// .resource("", |r| r.f(|_| HttpResponse::Ok())) // <- handle `/app` path - /// .resource("/", |r| r.f(|_| HttpResponse::Ok())) // <- handle `/app/` path - /// .resource("/test", |r| { - /// r.get().f(|_| HttpResponse::Ok()); - /// r.head().f(|_| HttpResponse::MethodNotAllowed()); - /// }) - /// .finish(); - /// } - /// ``` - pub fn prefix>(mut self, prefix: P) -> App { - { - let parts = self.parts.as_mut().expect("Use after finish"); - let mut prefix = prefix.into(); - if !prefix.starts_with('/') { - prefix.insert(0, '/') - } - parts.router.set_prefix(&prefix); - parts.prefix = prefix; - } - self - } - - /// Configure route for a specific path. - /// - /// This is a simplified version of the `App::resource()` method. - /// Handler functions need to accept one request extractor - /// argument. - /// - /// This method could be called multiple times, in that case - /// multiple routes would be registered for same resource path. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpRequest, HttpResponse}; - /// - /// fn main() { - /// let app = App::new() - /// .route("/test", http::Method::GET, |_: HttpRequest| { - /// HttpResponse::Ok() - /// }) - /// .route("/test", http::Method::POST, |_: HttpRequest| { - /// HttpResponse::MethodNotAllowed() - /// }); - /// } - /// ``` - pub fn route(mut self, path: &str, method: Method, f: F) -> App - where - F: WithFactory, - R: Responder + 'static, - T: FromRequest + 'static, - { - self.parts - .as_mut() - .expect("Use after finish") - .router - .register_route(path, method, f); - - self - } - - // /// Configure scope for common root path. - // /// - // /// Scopes collect multiple paths under a common path prefix. - // /// Scope path can contain variable path segments as resources. - // /// - // /// ```rust - // /// # extern crate actix_web; - // /// use actix_web::{http, App, HttpRequest, HttpResponse}; - // /// - // /// fn main() { - // /// 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())) - // /// }); - // /// } - // /// ``` - // /// - // /// In the above example, three routes get added: - // /// * /{project_id}/path1 - // /// * /{project_id}/path2 - // /// * /{project_id}/path3 - // /// - // pub fn scope(mut self, path: &str, f: F) -> App - // where - // F: FnOnce(Scope) -> Scope, - // { - // let scope = f(Scope::new(path)); - // self.parts - // .as_mut() - // .expect("Use after finish") - // .router - // .register_scope(scope); - // self - // } - - /// Configure resource for a specific path. - /// - /// Resources may have variable path segments. For example, a - /// resource with the path `/a/{name}/c` would match all incoming - /// requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`. - /// - /// 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. This is done by - /// looking up the identifier in the `Params` object returned by - /// `HttpRequest.match_info()` method. - /// - /// By default, each segment matches the regular expression `[^{}/]+`. - /// - /// You can also specify a custom regex in the form `{identifier:regex}`: - /// - /// For instance, to route `GET`-requests on any route matching - /// `/users/{userid}/{friend}` and store `userid` and `friend` in - /// the exposed `Params` object: - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().resource("/users/{userid}/{friend}", |r| { - /// r.get().f(|_| HttpResponse::Ok()); - /// r.head().f(|_| HttpResponse::MethodNotAllowed()); - /// }); - /// } - /// ``` - pub fn resource(mut self, path: &str, f: F) -> App - where - F: FnOnce(&mut Resource) -> R + 'static, - { - { - let parts = self.parts.as_mut().expect("Use after finish"); - - // create resource - let mut resource = Resource::new(ResourceDef::new(path)); - - // configure - f(&mut resource); - - parts.router.register_resource(resource); - } - self - } - - /// Configure resource for a specific path. - #[doc(hidden)] - pub fn register_resource(&mut self, resource: Resource) { - self.parts - .as_mut() - .expect("Use after finish") - .router - .register_resource(resource); - } - - /// Default resource to be used if no matching route could be found. - pub fn default_resource(mut self, f: F) -> App - where - F: FnOnce(&mut Resource) -> R + 'static, - { - // create and configure default resource - let mut resource = Resource::new(ResourceDef::new("")); - f(&mut resource); - - self.parts - .as_mut() - .expect("Use after finish") - .router - .register_default_resource(resource.into()); - - self - } - - /// Configure handler for specific path prefix. - /// - /// A path prefix consists of valid path segments, i.e for the - /// prefix `/app` any request with the paths `/app`, `/app/` or - /// `/app/test` would match, but the path `/application` would - /// not. - /// - /// Path tail is available as `tail` parameter in request's match_dict. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpRequest, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().handler("/app", |req: &HttpRequest| match *req.method() { - /// http::Method::GET => HttpResponse::Ok(), - /// http::Method::POST => HttpResponse::MethodNotAllowed(), - /// _ => HttpResponse::NotFound(), - /// }); - /// } - /// ``` - pub fn handler>(mut self, path: &str, handler: H) -> App { - { - let mut path = path.trim().trim_right_matches('/').to_owned(); - if !path.is_empty() && !path.starts_with('/') { - path.insert(0, '/'); - }; - self.parts - .as_mut() - .expect("Use after finish") - .router - .register_handler(&path, Box::new(WrapHandler::new(handler)), None); - } - self - } - - // /// Register a middleware. - // pub fn middleware>(mut self, mw: M) -> App { - // self.parts - // .as_mut() - // .expect("Use after finish") - // .middlewares - // .push(Box::new(mw)); - // self - // } - - /// Run external configuration as part of the application building - /// process - /// - /// This function is useful for moving parts of configuration to a - /// different module or event library. For example we can move - /// some of the resources' configuration to different module. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{fs, middleware, App, HttpResponse}; - /// - /// // this function could be located in different module - /// fn config(app: App) -> App { - /// app.resource("/test", |r| { - /// r.get().f(|_| HttpResponse::Ok()); - /// r.head().f(|_| HttpResponse::MethodNotAllowed()); - /// }) - /// } - /// - /// fn main() { - /// let app = App::new() - /// .middleware(middleware::Logger::default()) - /// .configure(config) // <- register resources - /// .handler("/static", fs::StaticFiles::new(".").unwrap()); - /// } - /// ``` - pub fn configure(self, cfg: F) -> App - where - F: Fn(App) -> App, - { - cfg(self) - } - - /// Finish application configuration and create `HttpHandler` object. - pub fn finish(&mut self) -> HttpApplication { - let mut parts = self.parts.take().expect("Use after finish"); - let prefix = parts.prefix.trim().trim_right_matches('/'); - parts.router.finish(); - - let inner = Rc::new(Inner { - router: parts.router, - encoding: parts.encoding, - }); - let filters = if parts.filters.is_empty() { - None - } else { - Some(parts.filters) - }; - - HttpApplication { - inner, - filters, - state: Rc::new(parts.state), - // middlewares: Rc::new(parts.middlewares), - prefix: prefix.to_owned(), - prefix_len: prefix.len(), - } - } - - // /// Convenience method for creating `Box` instances. - // /// - // /// This method is useful if you need to register multiple - // /// application instances with different state. - // /// - // /// ```rust - // /// # use std::thread; - // /// # extern crate actix_web; - // /// use actix_web::{server, App, HttpResponse}; - // /// - // /// struct State1; - // /// - // /// struct State2; - // /// - // /// fn main() { - // /// # thread::spawn(|| { - // /// server::new(|| { - // /// vec![ - // /// App::with_state(State1) - // /// .prefix("/app1") - // /// .resource("/", |r| r.f(|r| HttpResponse::Ok())) - // /// .boxed(), - // /// App::with_state(State2) - // /// .prefix("/app2") - // /// .resource("/", |r| r.f(|r| HttpResponse::Ok())) - // /// .boxed(), - // /// ] - // /// }).bind("127.0.0.1:8080") - // /// .unwrap() - // /// .run() - // /// # }); - // /// } - // /// ``` - // pub fn boxed(mut self) -> Box>> { - // Box::new(BoxedApplication { app: self.finish() }) - // } -} - -// struct BoxedApplication { -// app: HttpApplication, -// } - -// impl HttpHandler for BoxedApplication { -// type Task = Box; - -// fn handle(&self, req: Request) -> Result { -// self.app.handle(req).map(|t| { -// let task: Self::Task = Box::new(t); -// task -// }) -// } -// } - -// impl IntoHttpHandler for App { -// type Handler = HttpApplication; - -// fn into_handler(mut self) -> HttpApplication { -// self.finish() -// } -// } - -// impl<'a, S: 'static> IntoHttpHandler for &'a mut App { -// type Handler = HttpApplication; - -// fn into_handler(self) -> HttpApplication { -// self.finish() -// } -// } - -// #[doc(hidden)] -// impl Iterator for App { -// type Item = HttpApplication; - -// fn next(&mut self) -> Option { -// if self.parts.is_some() { -// Some(self.finish()) -// } else { -// None -// } -// } -// } - -#[cfg(test)] -mod tests { - use super::*; - use body::{Binary, Body}; - use http::StatusCode; - use httprequest::HttpRequest; - use httpresponse::HttpResponse; - use pred; - use test::{TestRequest, TestServer}; - - #[test] - fn test_default_resource() { - let app = App::new() - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - .finish(); - - let req = TestRequest::with_uri("/test").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/blah").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let app = App::new() - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - .default_resource(|r| r.f(|_| HttpResponse::MethodNotAllowed())) - .finish(); - let req = TestRequest::with_uri("/blah").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::METHOD_NOT_ALLOWED); - } - - #[test] - fn test_unhandled_prefix() { - let app = App::new() - .prefix("/test") - .resource("/test", |r| r.f(|_| HttpResponse::Ok())) - .finish(); - let ctx = TestRequest::default().request(); - assert!(app.handle(ctx).is_err()); - } - - #[test] - fn test_state() { - let app = App::with_state(10) - .resource("/", |r| r.f(|_| HttpResponse::Ok())) - .finish(); - let req = TestRequest::with_state(10).request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - } - - #[test] - fn test_prefix() { - let app = App::new() - .prefix("/test") - .resource("/blah", |r| r.f(|_| HttpResponse::Ok())) - .finish(); - let req = TestRequest::with_uri("/test").request(); - let resp = app.handle(req); - assert!(resp.is_ok()); - - let req = TestRequest::with_uri("/test/").request(); - let resp = app.handle(req); - assert!(resp.is_ok()); - - let req = TestRequest::with_uri("/test/blah").request(); - let resp = app.handle(req); - assert!(resp.is_ok()); - - let req = TestRequest::with_uri("/testing").request(); - let resp = app.handle(req); - assert!(resp.is_err()); - } - - #[test] - fn test_handler() { - let app = App::new() - .handler("/test", |_: &_| HttpResponse::Ok()) - .finish(); - - let req = TestRequest::with_uri("/test").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/test/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/test/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/testapp").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/blah").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_handler2() { - let app = App::new() - .handler("test", |_: &_| HttpResponse::Ok()) - .finish(); - - let req = TestRequest::with_uri("/test").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/test/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/test/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/testapp").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/blah").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_handler_with_prefix() { - let app = App::new() - .prefix("prefix") - .handler("/test", |_: &_| HttpResponse::Ok()) - .finish(); - - let req = TestRequest::with_uri("/prefix/test").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/prefix/test/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/prefix/test/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/prefix/testapp").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/prefix/blah").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_route() { - let app = App::new() - .route("/test", Method::GET, |_: HttpRequest| HttpResponse::Ok()) - .route("/test", Method::POST, |_: HttpRequest| { - HttpResponse::Created() - }) - .finish(); - - let req = TestRequest::with_uri("/test").method(Method::GET).request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/test") - .method(Method::POST) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - - let req = TestRequest::with_uri("/test") - .method(Method::HEAD) - .request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_handler_prefix() { - let app = App::new() - .prefix("/app") - .handler("/test", |_: &_| HttpResponse::Ok()) - .finish(); - - let req = TestRequest::with_uri("/test").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/test").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/test/").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/test/app").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - - let req = TestRequest::with_uri("/app/testapp").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/app/blah").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_option_responder() { - let app = App::new() - .resource("/none", |r| r.f(|_| -> Option<&'static str> { None })) - .resource("/some", |r| r.f(|_| Some("some"))) - .finish(); - - let req = TestRequest::with_uri("/none").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - let req = TestRequest::with_uri("/some").request(); - let resp = app.run(req); - assert_eq!(resp.as_msg().status(), StatusCode::OK); - assert_eq!(resp.as_msg().body(), &Body::Binary(Binary::Slice(b"some"))); - } - - #[test] - fn test_filter() { - let mut srv = TestServer::with_factory(|| { - App::new() - .filter(pred::Get()) - .handler("/test", |_: &_| HttpResponse::Ok()) - }); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - - let request = srv.post().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::NOT_FOUND); - } - - #[test] - fn test_prefix_root() { - let mut srv = TestServer::with_factory(|| { - App::new() - .prefix("/test") - .resource("/", |r| r.f(|_| HttpResponse::Ok())) - .resource("", |r| r.f(|_| HttpResponse::Created())) - }); - - let request = srv.get().uri(srv.url("/test/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - - let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::CREATED); - } - -} diff --git a/src/lib.rs b/src/lib.rs index b4b12eb1..74fa0a94 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,6 @@ mod app; mod extractor; pub mod handler; -// mod helpers; // mod info; pub mod blocking; pub mod filter; diff --git a/src/responder.rs b/src/responder.rs index 5520c610..b3ec7ec7 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -1,13 +1,11 @@ -use actix_http::dev::ResponseBuilder; -use actix_http::http::StatusCode; -use actix_http::{Error, Response}; +use actix_http::{dev::ResponseBuilder, http::StatusCode, Error, Response}; use bytes::{Bytes, BytesMut}; use futures::future::{err, ok, Either as EitherFuture, FutureResult}; use futures::{Future, Poll}; use crate::request::HttpRequest; -/// Trait implemented by types that generate http responses. +/// Trait implemented by types that can be converted to a http response. /// /// Types that implement this trait can be used as the return type of a handler. pub trait Responder { @@ -72,6 +70,15 @@ impl Responder for ResponseBuilder { } } +impl Responder for () { + type Error = Error; + type Future = FutureResult; + + fn respond_to(self, _: &HttpRequest) -> Self::Future { + ok(Response::build(StatusCode::OK).finish()) + } +} + impl Responder for &'static str { type Error = Error; type Future = FutureResult; @@ -167,12 +174,7 @@ impl Responder for BytesMut { /// # fn is_a_variant() -> bool { true } /// # fn main() {} /// ``` -pub enum Either { - /// First branch of the type - A(A), - /// Second branch of the type - B(B), -} +pub type Either = either::Either; impl Responder for Either where @@ -184,8 +186,8 @@ where fn respond_to(self, req: &HttpRequest) -> Self::Future { match self { - Either::A(a) => EitherResponder::A(a.respond_to(req)), - Either::B(b) => EitherResponder::B(b.respond_to(req)), + either::Either::Left(a) => EitherResponder::A(a.respond_to(req)), + either::Either::Right(b) => EitherResponder::B(b.respond_to(req)), } } } From cc20fee62884d70990fbf0d0b4013172d8e2759d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 11:53:05 -0800 Subject: [PATCH 005/109] add request chain services --- src/app.rs | 480 ++++++++++++++++++++++++++----------- src/blocking.rs | 12 +- src/extractor.rs | 1 + src/lib.rs | 4 +- src/middleware/compress.rs | 3 - src/route.rs | 10 +- 6 files changed, 360 insertions(+), 150 deletions(-) diff --git a/src/app.rs b/src/app.rs index 7c077706..78f718db 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,7 +2,7 @@ use std::cell::RefCell; use std::marker::PhantomData; use std::rc::Rc; -use actix_http::body::{Body, MessageBody}; +use actix_http::body::MessageBody; use actix_http::{Extensions, PayloadStream, Request, Response}; use actix_router::{Path, ResourceDef, ResourceInfo, Router, Url}; use actix_service::boxed::{self, BoxedNewService, BoxedService}; @@ -30,32 +30,45 @@ pub trait HttpServiceFactory { } /// Application builder -pub struct App { - services: Vec<(ResourceDef, HttpNewService

)>, - default: Option>>, - defaults: Vec>>>>>, - endpoint: T, - factory_ref: Rc>>>, +pub struct App +where + T: NewService, Response = ServiceRequest

>, +{ + chain: T, extensions: Extensions, state: Vec>, - _t: PhantomData<(P, B)>, + _t: PhantomData<(P,)>, } -impl App> { - /// Create application with empty state. Application can +impl App { + /// Create application builder with empty state. Application can /// be configured with a builder-like pattern. pub fn new() -> Self { - App::create() + App { + chain: AppChain, + extensions: Extensions::new(), + state: Vec::new(), + _t: PhantomData, + } } } -impl Default for App> { +impl Default for App { fn default() -> Self { App::new() } } -impl App> { +impl App +where + P: 'static, + T: NewService< + Request = ServiceRequest, + Response = ServiceRequest

, + Error = (), + InitError = (), + >, +{ /// Create application with specified state. Application can be /// configured with a builder-like pattern. /// @@ -86,38 +99,172 @@ impl App> { self } - fn create() -> Self { + /// Configure resource for a specific path. + /// + /// Resources may have variable path segments. For example, a + /// resource with the path `/a/{name}/c` would match all incoming + /// requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`. + /// + /// 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. This is done by + /// looking up the identifier in the `Params` object returned by + /// `HttpRequest.match_info()` method. + /// + /// By default, each segment matches the regular expression `[^{}/]+`. + /// + /// You can also specify a custom regex in the form `{identifier:regex}`: + /// + /// For instance, to route `GET`-requests on any route matching + /// `/users/{userid}/{friend}` and store `userid` and `friend` in + /// the exposed `Params` object: + /// + /// ```rust,ignore + /// # extern crate actix_web; + /// use actix_web::{http, App, HttpResponse}; + /// + /// fn main() { + /// let app = App::new().resource("/users/{userid}/{friend}", |r| { + /// r.get(|r| r.to(|_| HttpResponse::Ok())); + /// r.head(|r| r.to(|_| HttpResponse::MethodNotAllowed())) + /// }); + /// } + /// ``` + pub fn resource(self, path: &str, f: F) -> AppRouter> + where + F: FnOnce(Resource

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

, + Response = ServiceResponse, + Error = (), + InitError = (), + > + 'static, + { + let rdef = ResourceDef::new(path); + let resource = f(Resource::new()); + let default = resource.get_default(); + let fref = Rc::new(RefCell::new(None)); + AppRouter { + chain: self.chain, + services: vec![(rdef, boxed::new_service(resource.into_new_service()))], + default: None, + defaults: vec![default], + endpoint: AppEntry::new(fref.clone()), + factory_ref: fref, + extensions: self.extensions, + state: self.state, + _t: PhantomData, + } + } + + /// Register a middleware. + pub fn middleware( + self, + mw: F, + ) -> AppRouter< + T, + P, + B, + impl NewService< + Request = ServiceRequest

, + Response = ServiceResponse, + Error = (), + InitError = (), + >, + > + where + M: NewTransform< + AppService

, + Request = ServiceRequest

, + Response = ServiceResponse, + Error = (), + InitError = (), + >, + B: MessageBody, + F: IntoNewTransform>, + { + let fref = Rc::new(RefCell::new(None)); + let endpoint = ApplyNewService::new(mw, AppEntry::new(fref.clone())); + AppRouter { + endpoint, + chain: self.chain, + state: self.state, + services: Vec::new(), + default: None, + defaults: Vec::new(), + factory_ref: fref, + extensions: self.extensions, + _t: PhantomData, + } + } + + /// Register a request modifier. It can modify any request parameters + /// including payload stream. + pub fn chain( + self, + chain: C, + ) -> App< + P1, + impl NewService< + Request = ServiceRequest, + Response = ServiceRequest, + Error = (), + InitError = (), + >, + > + where + C: NewService< + (), + Request = ServiceRequest

, + Response = ServiceRequest, + Error = (), + InitError = (), + >, + F: IntoNewService, + { + let chain = self.chain.and_then(chain.into_new_service()); App { + chain, + state: self.state, + extensions: self.extensions, + _t: PhantomData, + } + } + + /// Complete applicatin chain configuration and start resource + /// configuration. + pub fn router(self) -> AppRouter> { + let fref = Rc::new(RefCell::new(None)); + AppRouter { + chain: self.chain, services: Vec::new(), default: None, defaults: Vec::new(), endpoint: AppEntry::new(fref.clone()), factory_ref: fref, - extensions: Extensions::new(), - state: Vec::new(), + extensions: self.extensions, + state: self.state, _t: PhantomData, } } } -// /// Application router builder -// pub struct AppRouter { -// services: Vec<( -// ResourceDef, -// BoxedHttpNewService, Response>, -// )>, -// default: Option, Response>>>, -// defaults: -// Vec, Response>>>>>>, -// state: AppState, -// endpoint: T, -// factory_ref: Rc>>>, -// extensions: Extensions, -// _t: PhantomData

, -// } +/// Structure that follows the builder pattern for building application +/// instances. +pub struct AppRouter { + chain: C, + services: Vec<(ResourceDef, HttpNewService

)>, + default: Option>>, + defaults: Vec>>>>>, + endpoint: T, + factory_ref: Rc>>>, + extensions: Extensions, + state: Vec>, + _t: PhantomData<(P, B)>, +} -impl App +impl AppRouter where P: 'static, B: MessageBody, @@ -221,7 +368,8 @@ where pub fn middleware( self, mw: F, - ) -> App< + ) -> AppRouter< + C, P, B1, impl NewService< @@ -243,14 +391,15 @@ where F: IntoNewTransform, { let endpoint = ApplyNewService::new(mw, self.endpoint); - App { + AppRouter { endpoint, + chain: self.chain, state: self.state, services: self.services, default: self.default, - defaults: Vec::new(), + defaults: self.defaults, factory_ref: self.factory_ref, - extensions: Extensions::new(), + extensions: self.extensions, _t: PhantomData, } } @@ -292,8 +441,8 @@ where } } -impl - IntoNewService, T, ()>> for App +impl + IntoNewService, T, ()>> for AppRouter where T: NewService< Request = ServiceRequest

, @@ -301,8 +450,14 @@ where Error = (), InitError = (), >, + C: NewService< + Request = ServiceRequest, + Response = ServiceRequest

, + Error = (), + InitError = (), + >, { - fn into_new_service(self) -> AndThenNewService, T, ()> { + fn into_new_service(self) -> AndThenNewService, T, ()> { // update resource default service if self.default.is_some() { for default in &self.defaults { @@ -317,99 +472,15 @@ where services: Rc::new(self.services), }); - AppStateFactory { + AppInit { + chain: self.chain, state: self.state, extensions: Rc::new(RefCell::new(Rc::new(self.extensions))), - _t: PhantomData, } .and_then(self.endpoint) } } -/// Service factory to convert `Request` to a `ServiceRequest` -pub struct AppStateFactory

{ - state: Vec>, - extensions: Rc>>, - _t: PhantomData

, -} - -impl NewService for AppStateFactory

{ - type Request = Request

; - type Response = ServiceRequest

; - type Error = (); - type InitError = (); - type Service = AppStateService

; - type Future = AppStateFactoryResult

; - - fn new_service(&self, _: &()) -> Self::Future { - AppStateFactoryResult { - state: self.state.iter().map(|s| s.construct()).collect(), - extensions: self.extensions.clone(), - _t: PhantomData, - } - } -} - -#[doc(hidden)] -pub struct AppStateFactoryResult

{ - state: Vec>, - extensions: Rc>>, - _t: PhantomData

, -} - -impl

Future for AppStateFactoryResult

{ - type Item = AppStateService

; - type Error = (); - - fn poll(&mut self) -> Poll { - if let Some(extensions) = Rc::get_mut(&mut *self.extensions.borrow_mut()) { - let mut idx = 0; - while idx < self.state.len() { - if let Async::Ready(_) = self.state[idx].poll_result(extensions)? { - self.state.remove(idx); - } else { - idx += 1; - } - } - if !self.state.is_empty() { - return Ok(Async::NotReady); - } - } else { - log::warn!("Multiple copies of app extensions exists"); - } - - Ok(Async::Ready(AppStateService { - extensions: self.extensions.borrow().clone(), - _t: PhantomData, - })) - } -} - -/// Service to convert `Request` to a `ServiceRequest` -pub struct AppStateService

{ - extensions: Rc, - _t: PhantomData

, -} - -impl

Service for AppStateService

{ - type Request = Request

; - type Response = ServiceRequest

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

) -> Self::Future { - ok(ServiceRequest::new( - Path::new(Url::new(req.uri().clone())), - req, - self.extensions.clone(), - )) - } -} - pub struct AppFactory

{ services: Rc)>>, } @@ -530,17 +601,6 @@ impl

Service for AppService

{ } } -pub struct AppServiceResponse(Box>); - -impl Future for AppServiceResponse { - type Item = ServiceResponse; - type Error = (); - - fn poll(&mut self) -> Poll { - self.0.poll().map_err(|_| panic!()) - } -} - #[doc(hidden)] pub struct AppEntry

{ factory: Rc>>>, @@ -564,3 +624,151 @@ impl NewService for AppEntry

{ 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 = (); + 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 = (); + type Future = FutureResult; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + Ok(Async::Ready(())) + } + + fn call(&mut self, req: Self::Request) -> Self::Future { + ok(req) + } +} + +/// Service factory to convert `Request` to a `ServiceRequest` +pub struct AppInit +where + C: NewService, Response = ServiceRequest

>, +{ + chain: C, + state: Vec>, + extensions: Rc>>, +} + +impl NewService for AppInit +where + C: NewService< + Request = ServiceRequest, + Response = ServiceRequest

, + InitError = (), + >, +{ + type Request = Request; + type Response = ServiceRequest

; + type Error = C::Error; + type InitError = C::InitError; + type Service = AppInitService; + type Future = AppInitResult; + + fn new_service(&self, _: &()) -> Self::Future { + AppInitResult { + chain: self.chain.new_service(&()), + state: self.state.iter().map(|s| s.construct()).collect(), + extensions: self.extensions.clone(), + } + } +} + +#[doc(hidden)] +pub struct AppInitResult +where + C: NewService< + Request = ServiceRequest, + Response = ServiceRequest

, + InitError = (), + >, +{ + chain: C::Future, + state: Vec>, + extensions: Rc>>, +} + +impl Future for AppInitResult +where + C: NewService< + Request = ServiceRequest, + Response = ServiceRequest

, + InitError = (), + >, +{ + type Item = AppInitService; + type Error = C::InitError; + + fn poll(&mut self) -> Poll { + if let Some(extensions) = Rc::get_mut(&mut *self.extensions.borrow_mut()) { + let mut idx = 0; + while idx < self.state.len() { + if let Async::Ready(_) = self.state[idx].poll_result(extensions)? { + self.state.remove(idx); + } else { + idx += 1; + } + } + if !self.state.is_empty() { + return Ok(Async::NotReady); + } + } else { + log::warn!("Multiple copies of app extensions exists"); + } + + let chain = futures::try_ready!(self.chain.poll()); + + Ok(Async::Ready(AppInitService { + chain, + extensions: self.extensions.borrow().clone(), + })) + } +} + +/// Service to convert `Request` to a `ServiceRequest` +pub struct AppInitService +where + C: Service, Response = ServiceRequest

>, +{ + chain: C, + extensions: Rc, +} + +impl Service for AppInitService +where + C: Service, Response = ServiceRequest

>, +{ + type Request = Request; + type Response = ServiceRequest

; + type Error = C::Error; + type Future = C::Future; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.chain.poll_ready() + } + + fn call(&mut self, req: Request) -> Self::Future { + let req = ServiceRequest::new( + Path::new(Url::new(req.uri().clone())), + req, + self.extensions.clone(), + ); + self.chain.call(req) + } +} diff --git a/src/blocking.rs b/src/blocking.rs index fc2624f6..abf4282c 100644 --- a/src/blocking.rs +++ b/src/blocking.rs @@ -24,7 +24,7 @@ lazy_static::lazy_static! { Mutex::new( threadpool::Builder::new() .thread_name("actix-web".to_owned()) - .num_threads(8) + .num_threads(default) .build(), ) }; @@ -45,11 +45,15 @@ pub enum BlockingError { /// to result of the function execution. pub fn run(f: F) -> CpuFuture where - F: FnOnce() -> Result, + F: FnOnce() -> Result + Send + 'static, + I: Send + 'static, + E: Send + 'static, { let (tx, rx) = oneshot::channel(); - POOL.with(move |pool| { - let _ = tx.send(f()); + POOL.with(|pool| { + pool.execute(move || { + let _ = tx.send(f()); + }) }); CpuFuture { rx } diff --git a/src/extractor.rs b/src/extractor.rs index 53209ad0..48c70b86 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] use std::ops::{Deref, DerefMut}; use std::rc::Rc; use std::{fmt, str}; diff --git a/src/lib.rs b/src/lib.rs index 74fa0a94..c70f47e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(clippy::type_complexity, dead_code, unused_variables)] +#![allow(clippy::type_complexity)] mod app; mod extractor; @@ -28,7 +28,7 @@ pub use crate::service::{ServiceRequest, ServiceResponse}; pub use crate::state::State; pub mod dev { - pub use crate::app::AppService; + pub use crate::app::AppRouter; pub use crate::handler::{AsyncFactory, Extract, Factory, Handle}; pub use crate::route::{Route, RouteBuilder}; // pub use crate::info::ConnectionInfo; diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index fee17ce1..5d5586cf 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -117,7 +117,6 @@ where enum EncoderBody { Body(B), Other(Box), - None, } pub struct Encoder { @@ -131,7 +130,6 @@ impl MessageBody for Encoder { match self.body { EncoderBody::Body(ref b) => b.length(), EncoderBody::Other(ref b) => b.length(), - EncoderBody::None => BodyLength::Empty, } } else { BodyLength::Stream @@ -143,7 +141,6 @@ impl MessageBody for Encoder { let result = match self.body { EncoderBody::Body(ref mut b) => b.poll_next()?, EncoderBody::Other(ref mut b) => b.poll_next()?, - EncoderBody::None => return Ok(Async::Ready(None)), }; match result { Async::NotReady => return Ok(Async::NotReady), diff --git a/src/route.rs b/src/route.rs index 574e8e34..4abb2f1d 100644 --- a/src/route.rs +++ b/src/route.rs @@ -320,11 +320,11 @@ impl RouteBuilder

{ } } -pub struct RouteServiceBuilder { - service: T, - filters: Vec>, - _t: PhantomData<(P, U1, U2)>, -} +// pub struct RouteServiceBuilder { +// service: T, +// filters: Vec>, +// _t: PhantomData<(P, U1, U2)>, +// } // impl RouteServiceBuilder // where From 75fbb9748051db5fdb70f8cbc0cb5d1e111ded15 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 13:57:00 -0800 Subject: [PATCH 006/109] update new transform trait --- src/handler.rs | 9 ++++----- src/middleware/mod.rs | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index e957d15e..8076d4d4 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,8 +1,7 @@ use std::marker::PhantomData; use actix_http::{Error, Response}; -use actix_service::{NewService, Service}; -use actix_utils::Never; +use actix_service::{NewService, Service, Void}; use futures::future::{ok, FutureResult}; use futures::{try_ready, Async, Future, IntoFuture, Poll}; @@ -71,7 +70,7 @@ where { type Request = (T, HttpRequest); type Response = ServiceResponse; - type Error = Never; + type Error = Void; type InitError = (); type Service = HandleService; type Future = FutureResult; @@ -101,7 +100,7 @@ where { type Request = (T, HttpRequest); type Response = ServiceResponse; - type Error = Never; + type Error = Void; type Future = HandleServiceResponse; fn poll_ready(&mut self) -> Poll<(), Self::Error> { @@ -128,7 +127,7 @@ where T::Error: Into, { type Item = ServiceResponse; - type Error = Never; + type Error = Void; fn poll(&mut self) -> Poll { match self.fut.poll() { diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index a8b4b3c6..8ef316b4 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -47,7 +47,7 @@ where } } -impl NewTransform for MiddlewareFactory +impl NewTransform for MiddlewareFactory where T: Transform + Clone, S: Service, @@ -59,7 +59,7 @@ where type InitError = (); type Future = FutureResult; - fn new_transform(&self) -> Self::Future { + fn new_transform(&self, _: &C) -> Self::Future { ok(self.tr.clone()) } } From 3454812b687d2d22e4f1148d624574cf829aef32 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 13:59:12 -0800 Subject: [PATCH 007/109] rename actix-web-fs crate --- Cargo.toml | 2 +- {actix-web-fs => staticfiles}/CHANGES.md | 0 {actix-web-fs => staticfiles}/Cargo.toml | 4 ++-- {actix-web-fs => staticfiles}/README.md | 0 {actix-web-fs => staticfiles}/src/lib.rs | 0 5 files changed, 3 insertions(+), 3 deletions(-) rename {actix-web-fs => staticfiles}/CHANGES.md (100%) rename {actix-web-fs => staticfiles}/Cargo.toml (95%) rename {actix-web-fs => staticfiles}/README.md (100%) rename {actix-web-fs => staticfiles}/src/lib.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index bfd06101..fa654686 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ path = "src/lib.rs" [workspace] members = [ ".", - "actix-web-fs", + "staticfiles", ] [features] diff --git a/actix-web-fs/CHANGES.md b/staticfiles/CHANGES.md similarity index 100% rename from actix-web-fs/CHANGES.md rename to staticfiles/CHANGES.md diff --git a/actix-web-fs/Cargo.toml b/staticfiles/Cargo.toml similarity index 95% rename from actix-web-fs/Cargo.toml rename to staticfiles/Cargo.toml index 5900a936..c2516302 100644 --- a/actix-web-fs/Cargo.toml +++ b/staticfiles/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "actix-web-fs" +name = "actix-staticfiles" version = "0.1.0" authors = ["Nikolay Kim "] description = "Static files support for Actix web." @@ -14,7 +14,7 @@ edition = "2018" workspace = ".." [lib] -name = "actix_web_fs" +name = "actix_staticfiles" path = "src/lib.rs" [dependencies] diff --git a/actix-web-fs/README.md b/staticfiles/README.md similarity index 100% rename from actix-web-fs/README.md rename to staticfiles/README.md diff --git a/actix-web-fs/src/lib.rs b/staticfiles/src/lib.rs similarity index 100% rename from actix-web-fs/src/lib.rs rename to staticfiles/src/lib.rs From 9394a4e2a5513de678d17fd2669d5810254bcd71 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 14:07:21 -0800 Subject: [PATCH 008/109] cleanup dependencies --- Cargo.toml | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fa654686..d5b2fa3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ readme = "README.md" keywords = ["http", "web", "framework", "async", "futures"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" -documentation = "https://actix.rs/api/actix-web/stable/actix_web/" +documentation = "https://docs.rs/actix-web/" categories = ["network-programming", "asynchronous", "web-programming::http-server", "web-programming::websocket"] @@ -16,8 +16,9 @@ exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] edition = "2018" [badges] -travis-ci = { repository = "actix/actix-web2", branch = "master" } -codecov = { repository = "actix/actix-web2", branch = "master", service = "github" } +travis-ci = { repository = "actix/actix-web", branch = "master" } +appveyor = { repository = "fafhrd91/actix-web-hdy9d" } +codecov = { repository = "actix/actix-web", branch = "master", service = "github" } [lib] name = "actix_web" @@ -46,34 +47,24 @@ actix-codec = "0.1.0" #actix-service = "0.2.1" #actix-server = "0.2.1" #actix-utils = "0.2.1" -actix-service = { git = "https://github.com/actix/actix-net.git" } -actix-server = { git = "https://github.com/actix/actix-net.git" } actix-utils = { git = "https://github.com/actix/actix-net.git" } - -actix-rt = "0.1.0" actix-http = { git = "https://github.com/actix/actix-http.git" } actix-router = { git = "https://github.com/actix/actix-net.git" } +actix-service = { git = "https://github.com/actix/actix-net.git" } bytes = "0.4" -futures = "0.1" derive_more = "0.14" either = "1.5.1" +encoding = "0.2" +futures = "0.1" log = "0.4" lazy_static = "1.2" mime = "0.3" -mime_guess = "2.0.0-alpha" num_cpus = "1.10" -percent-encoding = "1.0" -cookie = { version="0.11", features=["percent-encode"] } -v_htmlescape = "0.4" +parking_lot = "0.7" serde = "1.0" serde_json = "1.0" -encoding = "0.2" serde_urlencoded = "^0.5.3" -parking_lot = "0.7" -hashbrown = "0.1" -regex = "1" -time = "0.1" threadpool = "1.7" # compression From e4198a037a9d42d1546791c62f3605dec5909dab Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 16:24:14 -0800 Subject: [PATCH 009/109] add TestServiceRequest builder --- Cargo.toml | 11 +- src/app.rs | 13 ++- src/filter.rs | 288 ++++++++++++++++++++++++++------------------------ src/lib.rs | 1 + src/route.rs | 2 +- src/test.rs | 173 ++++++++++++------------------ 6 files changed, 229 insertions(+), 259 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d5b2fa3f..30d23d02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,13 +44,11 @@ flate2-rust = ["flate2/rust_backend"] [dependencies] actix-codec = "0.1.0" -#actix-service = "0.2.1" -#actix-server = "0.2.1" -#actix-utils = "0.2.1" -actix-utils = { git = "https://github.com/actix/actix-net.git" } +actix-service = "0.3.0" +actix-utils = "0.3.0" + actix-http = { git = "https://github.com/actix/actix-http.git" } actix-router = { git = "https://github.com/actix/actix-net.git" } -actix-service = { git = "https://github.com/actix/actix-net.git" } bytes = "0.4" derive_more = "0.14" @@ -73,8 +71,7 @@ flate2 = { version="^1.0.2", optional = true, default-features = false } [dev-dependencies] actix-rt = "0.1.0" -#actix-server = { version="0.2", features=["ssl"] } -actix-server = { git = "https://github.com/actix/actix-net.git", features=["ssl"] } +actix-server = { version="0.3.0", features=["ssl"] } actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } rand = "0.6" diff --git a/src/app.rs b/src/app.rs index 78f718db..2e45f2d3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,7 +2,7 @@ use std::cell::RefCell; use std::marker::PhantomData; use std::rc::Rc; -use actix_http::body::MessageBody; +use actix_http::body::{Body, MessageBody}; use actix_http::{Extensions, PayloadStream, Request, Response}; use actix_router::{Path, ResourceDef, ResourceInfo, Router, Url}; use actix_service::boxed::{self, BoxedNewService, BoxedService}; @@ -130,7 +130,7 @@ where /// }); /// } /// ``` - pub fn resource(self, path: &str, f: F) -> AppRouter> + pub fn resource(self, path: &str, f: F) -> AppRouter> where F: FnOnce(Resource

) -> Resource, U: NewService< @@ -159,16 +159,16 @@ where } /// Register a middleware. - pub fn middleware( + pub fn middleware( self, mw: F, ) -> AppRouter< T, P, - B, + Body, impl NewService< Request = ServiceRequest

, - Response = ServiceResponse, + Response = ServiceResponse, Error = (), InitError = (), >, @@ -177,11 +177,10 @@ where M: NewTransform< AppService

, Request = ServiceRequest

, - Response = ServiceResponse, + Response = ServiceResponse, Error = (), InitError = (), >, - B: MessageBody, F: IntoNewTransform>, { let fref = Rc::new(RefCell::new(None)); diff --git a/src/filter.rs b/src/filter.rs index a0566092..37c23d73 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -16,14 +16,13 @@ pub trait Filter { /// Return filter that matches if any of supplied filters. /// /// ```rust,ignore -/// # extern crate actix_web; -/// use actix_web2::{filter, App, HttpResponse}; +/// use actix_web::{filter, App, HttpResponse}; /// /// fn main() { /// App::new().resource("/index.html", |r| { /// r.route() /// .filter(pred::Any(pred::Get()).or(pred::Post())) -/// .f(|r| HttpResponse::MethodNotAllowed()) +/// .to(|| HttpResponse::MethodNotAllowed()) /// }); /// } /// ``` @@ -55,18 +54,18 @@ impl Filter for AnyFilter { /// Return filter that matches if all of supplied filters match. /// -/// ```rust,ignore +/// ```rust /// # extern crate actix_web; -/// use actix_web::{pred, App, HttpResponse}; +/// use actix_web::{filter, App, HttpResponse}; /// /// fn main() { /// App::new().resource("/index.html", |r| { -/// r.route() -/// .filter( -/// pred::All(pred::Get()) -/// .and(pred::Header("content-type", "text/plain")), +/// r.route( +/// |r| r.filter( +/// filter::All(filter::Get()) +/// .and(filter::Header("content-type", "text/plain")), /// ) -/// .f(|_| HttpResponse::MethodNotAllowed()) +/// .to(|| HttpResponse::MethodNotAllowed())) /// }); /// } /// ``` @@ -191,137 +190,146 @@ impl Filter for HeaderFilter { } } -/// Return predicate that matches if request contains specified Host name. -/// -/// ```rust,ignore -/// # extern crate actix_web; -/// use actix_web::{pred, App, HttpResponse}; -/// -/// fn main() { -/// App::new().resource("/index.html", |r| { -/// r.route() -/// .filter(pred::Host("www.rust-lang.org")) -/// .f(|_| HttpResponse::MethodNotAllowed()) -/// }); -/// } -/// ``` -pub fn Host>(host: H) -> HostFilter { - HostFilter(host.as_ref().to_string(), None) -} +// /// Return predicate that matches if request contains specified Host name. +// /// +// /// ```rust,ignore +// /// # extern crate actix_web; +// /// use actix_web::{pred, App, HttpResponse}; +// /// +// /// fn main() { +// /// App::new().resource("/index.html", |r| { +// /// r.route() +// /// .filter(pred::Host("www.rust-lang.org")) +// /// .f(|_| HttpResponse::MethodNotAllowed()) +// /// }); +// /// } +// /// ``` +// pub fn Host>(host: H) -> HostFilter { +// HostFilter(host.as_ref().to_string(), None) +// } -#[doc(hidden)] -pub struct HostFilter(String, Option); +// #[doc(hidden)] +// pub struct HostFilter(String, Option); -impl HostFilter { - /// Set reuest scheme to match - pub fn scheme>(&mut self, scheme: H) { - self.1 = Some(scheme.as_ref().to_string()) - } -} - -impl Filter for HostFilter { - fn check(&self, _req: &HttpRequest) -> bool { - // let info = req.connection_info(); - // if let Some(ref scheme) = self.1 { - // self.0 == info.host() && scheme == info.scheme() - // } else { - // self.0 == info.host() - // } - false - } -} - -// #[cfg(test)] -// mod tests { -// use actix_http::http::{header, Method}; -// use actix_http::test::TestRequest; - -// use super::*; - -// #[test] -// fn test_header() { -// let req = TestRequest::with_header( -// header::TRANSFER_ENCODING, -// header::HeaderValue::from_static("chunked"), -// ) -// .finish(); - -// let pred = Header("transfer-encoding", "chunked"); -// assert!(pred.check(&req, req.state())); - -// let pred = Header("transfer-encoding", "other"); -// assert!(!pred.check(&req, req.state())); - -// let pred = Header("content-type", "other"); -// assert!(!pred.check(&req, req.state())); -// } - -// #[test] -// fn test_host() { -// let req = TestRequest::default() -// .header( -// header::HOST, -// header::HeaderValue::from_static("www.rust-lang.org"), -// ) -// .finish(); - -// let pred = Host("www.rust-lang.org"); -// assert!(pred.check(&req, req.state())); - -// let pred = Host("localhost"); -// assert!(!pred.check(&req, req.state())); -// } - -// #[test] -// fn test_methods() { -// let req = TestRequest::default().finish(); -// let req2 = TestRequest::default().method(Method::POST).finish(); - -// assert!(Get().check(&req, req.state())); -// assert!(!Get().check(&req2, req2.state())); -// assert!(Post().check(&req2, req2.state())); -// assert!(!Post().check(&req, req.state())); - -// let r = TestRequest::default().method(Method::PUT).finish(); -// assert!(Put().check(&r, r.state())); -// assert!(!Put().check(&req, req.state())); - -// let r = TestRequest::default().method(Method::DELETE).finish(); -// assert!(Delete().check(&r, r.state())); -// assert!(!Delete().check(&req, req.state())); - -// let r = TestRequest::default().method(Method::HEAD).finish(); -// assert!(Head().check(&r, r.state())); -// assert!(!Head().check(&req, req.state())); - -// let r = TestRequest::default().method(Method::OPTIONS).finish(); -// assert!(Options().check(&r, r.state())); -// assert!(!Options().check(&req, req.state())); - -// let r = TestRequest::default().method(Method::CONNECT).finish(); -// assert!(Connect().check(&r, r.state())); -// assert!(!Connect().check(&req, req.state())); - -// let r = TestRequest::default().method(Method::PATCH).finish(); -// assert!(Patch().check(&r, r.state())); -// assert!(!Patch().check(&req, req.state())); - -// let r = TestRequest::default().method(Method::TRACE).finish(); -// assert!(Trace().check(&r, r.state())); -// assert!(!Trace().check(&req, req.state())); -// } - -// #[test] -// fn test_preds() { -// let r = TestRequest::default().method(Method::TRACE).finish(); - -// assert!(Not(Get()).check(&r, r.state())); -// assert!(!Not(Trace()).check(&r, r.state())); - -// assert!(All(Trace()).and(Trace()).check(&r, r.state())); -// assert!(!All(Get()).and(Trace()).check(&r, r.state())); - -// assert!(Any(Get()).or(Trace()).check(&r, r.state())); -// assert!(!Any(Get()).or(Get()).check(&r, r.state())); +// impl HostFilter { +// /// Set reuest scheme to match +// pub fn scheme>(&mut self, scheme: H) { +// self.1 = Some(scheme.as_ref().to_string()) // } // } + +// impl Filter for HostFilter { +// fn check(&self, _req: &HttpRequest) -> bool { +// // let info = req.connection_info(); +// // if let Some(ref scheme) = self.1 { +// // self.0 == info.host() && scheme == info.scheme() +// // } else { +// // self.0 == info.host() +// // } +// false +// } +// } + +#[cfg(test)] +mod tests { + use crate::test::TestServiceRequest; + use actix_http::http::{header, Method}; + + use super::*; + + #[test] + fn test_header() { + let req = TestServiceRequest::with_header(header::TRANSFER_ENCODING, "chunked") + .finish() + .into_request(); + + let pred = Header("transfer-encoding", "chunked"); + assert!(pred.check(&req)); + + let pred = Header("transfer-encoding", "other"); + assert!(!pred.check(&req)); + + let pred = Header("content-type", "other"); + assert!(!pred.check(&req)); + } + + // #[test] + // fn test_host() { + // let req = TestServiceRequest::default() + // .header( + // header::HOST, + // header::HeaderValue::from_static("www.rust-lang.org"), + // ) + // .request(); + + // let pred = Host("www.rust-lang.org"); + // assert!(pred.check(&req)); + + // let pred = Host("localhost"); + // assert!(!pred.check(&req)); + // } + + #[test] + fn test_methods() { + let req = TestServiceRequest::default().finish().into_request(); + let req2 = TestServiceRequest::default() + .method(Method::POST) + .finish() + .into_request(); + + assert!(Get().check(&req)); + assert!(!Get().check(&req2)); + assert!(Post().check(&req2)); + assert!(!Post().check(&req)); + + let r = TestServiceRequest::default().method(Method::PUT).finish(); + assert!(Put().check(&r,)); + assert!(!Put().check(&req,)); + + let r = TestServiceRequest::default() + .method(Method::DELETE) + .finish(); + assert!(Delete().check(&r,)); + assert!(!Delete().check(&req,)); + + let r = TestServiceRequest::default().method(Method::HEAD).finish(); + assert!(Head().check(&r,)); + assert!(!Head().check(&req,)); + + let r = TestServiceRequest::default() + .method(Method::OPTIONS) + .finish(); + assert!(Options().check(&r,)); + assert!(!Options().check(&req,)); + + let r = TestServiceRequest::default() + .method(Method::CONNECT) + .finish(); + assert!(Connect().check(&r,)); + assert!(!Connect().check(&req,)); + + let r = TestServiceRequest::default().method(Method::PATCH).finish(); + assert!(Patch().check(&r,)); + assert!(!Patch().check(&req,)); + + let r = TestServiceRequest::default().method(Method::TRACE).finish(); + assert!(Trace().check(&r,)); + assert!(!Trace().check(&req,)); + } + + #[test] + fn test_preds() { + let r = TestServiceRequest::default() + .method(Method::TRACE) + .request(); + + assert!(Not(Get()).check(&r,)); + assert!(!Not(Trace()).check(&r,)); + + assert!(All(Trace()).and(Trace()).check(&r,)); + assert!(!All(Get()).and(Trace()).check(&r,)); + + assert!(Any(Get()).or(Trace()).check(&r,)); + assert!(!Any(Get()).or(Get()).check(&r,)); + } +} diff --git a/src/lib.rs b/src/lib.rs index c70f47e8..70ce9607 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ mod responder; mod route; mod service; mod state; +pub mod test; // re-export for convenience pub use actix_http::Response as HttpResponse; diff --git a/src/route.rs b/src/route.rs index 4abb2f1d..d5e11424 100644 --- a/src/route.rs +++ b/src/route.rs @@ -180,7 +180,7 @@ impl RouteBuilder

{ /// # .finish(); /// # } /// ``` - pub fn filter(&mut self, f: F) -> &mut Self { + pub fn filter(mut self, f: F) -> Self { self.filters.push(Box::new(f)); self } diff --git a/src/test.rs b/src/test.rs index e13dcd16..d67696b1 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,18 +1,15 @@ //! Various helpers for Actix applications to use during testing. -use std::str::FromStr; +use std::ops::{Deref, DerefMut}; +use std::rc::Rc; -use bytes::Bytes; -use futures::IntoFuture; -use tokio_current_thread::Runtime; - -use actix_http::dev::Payload; use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; -use actix_http::http::{HeaderMap, HttpTryFrom, Method, Uri, Version}; -use actix_http::Request as HttpRequest; +use actix_http::http::{HttpTryFrom, Method, Version}; +use actix_http::test::TestRequest; +use actix_http::{Extensions, PayloadStream}; use actix_router::{Path, Url}; +use bytes::Bytes; -use crate::app::State; -use crate::request::Request; +use crate::request::HttpRequest; use crate::service::ServiceRequest; /// Test `Request` builder @@ -42,90 +39,71 @@ use crate::service::ServiceRequest; /// assert_eq!(resp.status(), StatusCode::BAD_REQUEST); /// } /// ``` -pub struct TestRequest { - state: S, - version: Version, - method: Method, - uri: Uri, - headers: HeaderMap, - params: Path, - payload: Option, +pub struct TestServiceRequest { + req: TestRequest, + extensions: Extensions, } -impl Default for TestRequest<()> { - fn default() -> TestRequest<()> { - TestRequest { - state: (), - method: Method::GET, - uri: Uri::from_str("/").unwrap(), - version: Version::HTTP_11, - headers: HeaderMap::new(), - params: Path::new(Url::default()), - payload: None, +impl Default for TestServiceRequest { + fn default() -> TestServiceRequest { + TestServiceRequest { + req: TestRequest::default(), + extensions: Extensions::new(), } } } -impl TestRequest<()> { +impl TestServiceRequest { /// Create TestRequest and set request uri - pub fn with_uri(path: &str) -> TestRequest<()> { - TestRequest::default().uri(path) + pub fn with_uri(path: &str) -> TestServiceRequest { + TestServiceRequest { + req: TestRequest::default().uri(path).take(), + extensions: Extensions::new(), + } } /// Create TestRequest and set header - pub fn with_hdr(hdr: H) -> TestRequest<()> { - TestRequest::default().set(hdr) + pub fn with_hdr(hdr: H) -> TestServiceRequest { + TestServiceRequest { + req: TestRequest::default().set(hdr).take(), + extensions: Extensions::new(), + } } /// Create TestRequest and set header - pub fn with_header(key: K, value: V) -> TestRequest<()> + pub fn with_header(key: K, value: V) -> TestServiceRequest where HeaderName: HttpTryFrom, V: IntoHeaderValue, { - TestRequest::default().header(key, value) - } -} - -impl TestRequest { - /// Start HttpRequest build process with application state - pub fn with_state(state: S) -> TestRequest { - TestRequest { - state, - method: Method::GET, - uri: Uri::from_str("/").unwrap(), - version: Version::HTTP_11, - headers: HeaderMap::new(), - params: Path::new(Url::default()), - payload: None, + TestServiceRequest { + req: TestRequest::default().header(key, value).take(), + extensions: Extensions::new(), } } /// Set HTTP version of this request pub fn version(mut self, ver: Version) -> Self { - self.version = ver; + self.req.version(ver); self } /// Set HTTP method of this request pub fn method(mut self, meth: Method) -> Self { - self.method = meth; + self.req.method(meth); self } /// Set HTTP Uri of this request pub fn uri(mut self, path: &str) -> Self { - self.uri = Uri::from_str(path).unwrap(); + self.req.uri(path); self } /// Set a header pub fn set(mut self, hdr: H) -> Self { - if let Ok(value) = hdr.try_into() { - self.headers.append(H::name(), value); - return self; - } - panic!("Can not set header"); + self.req.set(hdr); + self } /// Set a header @@ -134,63 +112,50 @@ impl TestRequest { HeaderName: HttpTryFrom, V: IntoHeaderValue, { - if let Ok(key) = HeaderName::try_from(key) { - if let Ok(value) = value.try_into() { - self.headers.append(key, value); - return self; - } - } - panic!("Can not create header"); - } - - /// Set request path pattern parameter - pub fn param(mut self, name: &'static str, value: &'static str) -> Self { - self.params.add_static(name, value); + self.req.header(key, value); self } /// Set request payload pub fn set_payload>(mut self, data: B) -> Self { - let mut payload = Payload::empty(); - payload.unread_data(data.into()); - self.payload = Some(payload); + self.req.set_payload(data); self } - /// Complete request creation and generate `HttpRequest` instance - pub fn finish(self) -> ServiceRequest { - let TestRequest { - state, - method, - uri, - version, - headers, - mut params, - payload, - } = self; + /// Complete request creation and generate `ServiceRequest` instance + pub fn finish(mut self) -> ServiceRequest { + let req = self.req.finish(); - params.get_mut().update(&uri); - - let mut req = HttpRequest::new(); - { - let inner = req.inner_mut(); - inner.head.uri = uri; - inner.head.method = method; - inner.head.version = version; - inner.head.headers = headers; - *inner.payload.borrow_mut() = payload; - } - - Request::new(State::new(state), req, params) + ServiceRequest::new( + Path::new(Url::new(req.uri().clone())), + req, + Rc::new(self.extensions), + ) } - /// This method generates `HttpRequest` instance and executes handler - pub fn run_async(self, f: F) -> Result - where - F: FnOnce(&Request) -> R, - R: IntoFuture, - { - let mut rt = Runtime::new().unwrap(); - rt.block_on(f(&self.finish()).into_future()) + /// Complete request creation and generate `HttpRequest` instance + pub fn request(mut self) -> HttpRequest { + let req = self.req.finish(); + + ServiceRequest::new( + Path::new(Url::new(req.uri().clone())), + req, + Rc::new(self.extensions), + ) + .into_request() + } +} + +impl Deref for TestServiceRequest { + type Target = TestRequest; + + fn deref(&self) -> &TestRequest { + &self.req + } +} + +impl DerefMut for TestServiceRequest { + fn deref_mut(&mut self) -> &mut TestRequest { + &mut self.req } } From 8103d3327068d6ce4de977abbe007b0f104c65de Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 19:19:56 -0800 Subject: [PATCH 010/109] use custom request for FromRequest trait --- examples/basic.rs | 10 +-- src/app.rs | 8 +-- src/extractor.rs | 26 ++++---- src/filter.rs | 46 +++++++------ src/handler.rs | 15 +++-- src/lib.rs | 81 ++++++++++++++++++++++- src/request.rs | 6 +- src/resource.rs | 96 ++------------------------- src/route.rs | 83 +++++++++++------------- src/service.rs | 151 ++++++++++++++++++++++++++++++++++++++++--- src/state.rs | 4 +- tests/test_server.rs | 32 +++++---- 12 files changed, 342 insertions(+), 216 deletions(-) diff --git a/examples/basic.rs b/examples/basic.rs index a18581f9..886efb7b 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -2,7 +2,7 @@ use futures::IntoFuture; use actix_http::{h1, http::Method, Response}; use actix_server::Server; -use actix_web::{middleware, App, Error, HttpRequest, Resource}; +use actix_web::{middleware, web, App, Error, HttpRequest, Resource, Route}; fn index(req: HttpRequest) -> &'static str { println!("REQ: {:?}", req); @@ -31,7 +31,7 @@ fn main() { middleware::DefaultHeaders::new().header("X-Version", "0.2"), ) .middleware(middleware::Compress::default()) - .resource("/resource1/index.html", |r| r.get(index)) + .resource("/resource1/index.html", |r| r.route(web::get().to(index))) .service( "/resource2/index.html", Resource::new() @@ -39,8 +39,10 @@ fn main() { middleware::DefaultHeaders::new() .header("X-Version-R2", "0.3"), ) - .default_resource(|r| r.to(|| Response::MethodNotAllowed())) - .method(Method::GET, |r| r.to_async(index_async)), + .default_resource(|r| { + r.route(Route::new().to(|| Response::MethodNotAllowed())) + }) + .route(web::method(Method::GET).to_async(index_async)), ) .service("/test1.html", Resource::new().to(|| "Test\r\n")) .service("/", Resource::new().to(no_params)), diff --git a/src/app.rs b/src/app.rs index 2e45f2d3..d9219091 100644 --- a/src/app.rs +++ b/src/app.rs @@ -159,16 +159,16 @@ where } /// Register a middleware. - pub fn middleware( + pub fn middleware( self, mw: F, ) -> AppRouter< T, P, - Body, + B, impl NewService< Request = ServiceRequest

, - Response = ServiceResponse, + Response = ServiceResponse, Error = (), InitError = (), >, @@ -177,7 +177,7 @@ where M: NewTransform< AppService

, Request = ServiceRequest

, - Response = ServiceResponse, + Response = ServiceResponse, Error = (), InitError = (), >, diff --git a/src/extractor.rs b/src/extractor.rs index 48c70b86..522ce721 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -26,7 +26,7 @@ use actix_router::PathDeserializer; use crate::handler::FromRequest; use crate::request::HttpRequest; use crate::responder::Responder; -use crate::service::ServiceRequest; +use crate::service::ServiceFromRequest; #[derive(PartialEq, Eq, PartialOrd, Ord)] /// Extract typed information from the request's path. @@ -112,7 +112,7 @@ impl Path { } /// Extract path information from a request - pub fn extract

(req: &ServiceRequest

) -> Result, de::value::Error> + pub fn extract

(req: &ServiceFromRequest

) -> Result, de::value::Error> where T: DeserializeOwned, { @@ -135,7 +135,7 @@ where type Future = FutureResult; #[inline] - fn from_request(req: &mut ServiceRequest

) -> Self::Future { + fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { Self::extract(req).map_err(ErrorNotFound).into_future() } } @@ -221,7 +221,7 @@ where type Future = FutureResult; #[inline] - fn from_request(req: &mut ServiceRequest

) -> Self::Future { + fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { serde_urlencoded::from_str::(req.query_string()) .map(|val| ok(Query(val))) .unwrap_or_else(|e| err(e.into())) @@ -301,7 +301,7 @@ where type Future = Box>; #[inline] - fn from_request(req: &mut ServiceRequest

) -> Self::Future { + fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { let cfg = FormConfig::default(); let req2 = req.clone(); @@ -511,7 +511,7 @@ where type Future = Box>; #[inline] - fn from_request(req: &mut ServiceRequest

) -> Self::Future { + fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { let cfg = JsonConfig::default(); let req2 = req.clone(); @@ -619,7 +619,7 @@ where Either>, FutureResult>; #[inline] - fn from_request(req: &mut ServiceRequest

) -> Self::Future { + fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { let cfg = PayloadConfig::default(); if let Err(e) = cfg.check_mimetype(req) { @@ -666,7 +666,7 @@ where Either>, FutureResult>; #[inline] - fn from_request(req: &mut ServiceRequest

) -> Self::Future { + fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { let cfg = PayloadConfig::default(); // check content-type @@ -755,7 +755,7 @@ where type Future = Box, Error = Error>>; #[inline] - fn from_request(req: &mut ServiceRequest

) -> Self::Future { + fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { Box::new(T::from_request(req).then(|r| match r { Ok(v) => future::ok(Some(v)), Err(_) => future::ok(None), @@ -818,7 +818,7 @@ where type Future = Box, Error = Error>>; #[inline] - fn from_request(req: &mut ServiceRequest

) -> Self::Future { + fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { Box::new(T::from_request(req).then(|res| match res { Ok(v) => ok(Ok(v)), Err(e) => ok(Err(e)), @@ -846,7 +846,7 @@ impl PayloadConfig { self } - fn check_mimetype

(&self, req: &ServiceRequest

) -> Result<(), Error> { + fn check_mimetype

(&self, req: &ServiceFromRequest

) -> Result<(), Error> { // check content-type if let Some(ref mt) = self.mimetype { match req.mime_type() { @@ -884,7 +884,7 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { type Error = Error; type Future = $fut_type; - fn from_request(req: &mut ServiceRequest

) -> Self::Future { + fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { $fut_type { items: <($(Option<$T>,)+)>::default(), futs: ($($T::from_request(req),)+), @@ -933,7 +933,7 @@ impl

FromRequest

for () { type Error = Error; type Future = FutureResult<(), Error>; - fn from_request(_req: &mut ServiceRequest

) -> Self::Future { + fn from_request(_req: &mut ServiceFromRequest

) -> Self::Future { ok(()) } } diff --git a/src/filter.rs b/src/filter.rs index 37c23d73..e3d87b76 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -1,8 +1,7 @@ //! Route match predicates #![allow(non_snake_case)] use actix_http::http::{self, header, HttpTryFrom}; - -use crate::request::HttpRequest; +use actix_http::RequestHead; /// Trait defines resource predicate. /// Predicate can modify request object. It is also possible to @@ -10,20 +9,21 @@ use crate::request::HttpRequest; /// Extensions container available via `HttpRequest::extensions()` method. pub trait Filter { /// Check if request matches predicate - fn check(&self, request: &HttpRequest) -> bool; + fn check(&self, request: &RequestHead) -> bool; } /// Return filter that matches if any of supplied filters. /// -/// ```rust,ignore -/// use actix_web::{filter, App, HttpResponse}; +/// ```rust +/// use actix_web::{web, filter, App, HttpResponse}; /// /// fn main() { -/// App::new().resource("/index.html", |r| { -/// r.route() -/// .filter(pred::Any(pred::Get()).or(pred::Post())) -/// .to(|| HttpResponse::MethodNotAllowed()) -/// }); +/// App::new().resource("/index.html", |r| +/// r.route( +/// web::route() +/// .filter(filter::Any(filter::Get()).or(filter::Post())) +/// .to(|| HttpResponse::MethodNotAllowed())) +/// ); /// } /// ``` pub fn Any(filter: F) -> AnyFilter { @@ -42,7 +42,7 @@ impl AnyFilter { } impl Filter for AnyFilter { - fn check(&self, req: &HttpRequest) -> bool { + fn check(&self, req: &RequestHead) -> bool { for p in &self.0 { if p.check(req) { return true; @@ -56,15 +56,13 @@ impl Filter for AnyFilter { /// /// ```rust /// # extern crate actix_web; -/// use actix_web::{filter, App, HttpResponse}; +/// use actix_web::{filter, web, App, HttpResponse}; /// /// fn main() { /// App::new().resource("/index.html", |r| { -/// r.route( -/// |r| r.filter( -/// filter::All(filter::Get()) -/// .and(filter::Header("content-type", "text/plain")), -/// ) +/// r.route(web::route() +/// .filter( +/// filter::All(filter::Get()).and(filter::Header("content-type", "text/plain"))) /// .to(|| HttpResponse::MethodNotAllowed())) /// }); /// } @@ -85,7 +83,7 @@ impl AllFilter { } impl Filter for AllFilter { - fn check(&self, request: &HttpRequest) -> bool { + fn check(&self, request: &RequestHead) -> bool { for p in &self.0 { if !p.check(request) { return false; @@ -104,7 +102,7 @@ pub fn Not(filter: F) -> NotFilter { pub struct NotFilter(Box); impl Filter for NotFilter { - fn check(&self, request: &HttpRequest) -> bool { + fn check(&self, request: &RequestHead) -> bool { !self.0.check(request) } } @@ -114,8 +112,8 @@ impl Filter for NotFilter { pub struct MethodFilter(http::Method); impl Filter for MethodFilter { - fn check(&self, request: &HttpRequest) -> bool { - request.method() == self.0 + fn check(&self, request: &RequestHead) -> bool { + request.method == self.0 } } @@ -182,8 +180,8 @@ pub fn Header(name: &'static str, value: &'static str) -> HeaderFilter { pub struct HeaderFilter(header::HeaderName, header::HeaderValue); impl Filter for HeaderFilter { - fn check(&self, req: &HttpRequest) -> bool { - if let Some(val) = req.headers().get(&self.0) { + fn check(&self, req: &RequestHead) -> bool { + if let Some(val) = req.headers.get(&self.0) { return val == self.1; } false @@ -219,7 +217,7 @@ impl Filter for HeaderFilter { // } // impl Filter for HostFilter { -// fn check(&self, _req: &HttpRequest) -> bool { +// fn check(&self, _req: &RequestHead) -> bool { // // let info = req.connection_info(); // // if let Some(ref scheme) = self.1 { // // self.0 == info.host() && scheme == info.scheme() diff --git a/src/handler.rs b/src/handler.rs index 8076d4d4..31ae796b 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -7,7 +7,7 @@ use futures::{try_ready, Async, Future, IntoFuture, Poll}; use crate::request::HttpRequest; use crate::responder::Responder; -use crate::service::{ServiceRequest, ServiceResponse}; +use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; /// Trait implemented by types that can be extracted from request. /// @@ -20,7 +20,7 @@ pub trait FromRequest

: Sized { type Future: Future; /// Convert request to a Self - fn from_request(req: &mut ServiceRequest

) -> Self::Future; + fn from_request(req: &mut ServiceFromRequest

) -> Self::Future; } /// Handler converter factory @@ -306,7 +306,7 @@ impl> Default for Extract { impl> NewService for Extract { type Request = ServiceRequest

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

); + type Error = (Error, ServiceFromRequest

); type InitError = (); type Service = ExtractService; type Future = FutureResult; @@ -323,14 +323,15 @@ pub struct ExtractService> { impl> Service for ExtractService { type Request = ServiceRequest

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

); + type Error = (Error, ServiceFromRequest

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

) -> Self::Future { + fn call(&mut self, req: ServiceRequest

) -> Self::Future { + let mut req = req.into(); ExtractResponse { fut: T::from_request(&mut req), req: Some(req), @@ -339,13 +340,13 @@ impl> Service for ExtractService { } pub struct ExtractResponse> { - req: Option>, + req: Option>, fut: T::Future, } impl> Future for ExtractResponse { type Item = (T, HttpRequest); - type Error = (Error, ServiceRequest

); + type Error = (Error, ServiceFromRequest

); fn poll(&mut self) -> Poll { let item = try_ready!(self diff --git a/src/lib.rs b/src/lib.rs index 70ce9607..0e81b65a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,12 +25,91 @@ pub use crate::handler::FromRequest; pub use crate::request::HttpRequest; pub use crate::resource::Resource; pub use crate::responder::{Either, Responder}; +pub use crate::route::Route; pub use crate::service::{ServiceRequest, ServiceResponse}; pub use crate::state::State; +pub mod web { + use actix_http::{http::Method, Error, Response}; + use futures::IntoFuture; + + use crate::handler::{AsyncFactory, Factory, FromRequest}; + use crate::responder::Responder; + use crate::Route; + + pub fn route() -> Route

{ + Route::new() + } + + pub fn get() -> Route

{ + Route::get() + } + + pub fn post() -> Route

{ + Route::post() + } + + pub fn put() -> Route

{ + Route::put() + } + + pub fn delete() -> Route

{ + Route::delete() + } + + pub fn head() -> Route

{ + Route::new().method(Method::HEAD) + } + + pub fn method(method: Method) -> Route

{ + Route::new().method(method) + } + + /// Create a new route and add handler. + /// + /// ```rust + /// use actix_web::{web, App, HttpResponse}; + /// + /// fn index() -> HttpResponse { + /// unimplemented!() + /// } + /// + /// App::new().resource("/", |r| r.route(web::to(index))); + /// ``` + pub fn to(handler: F) -> Route

+ where + F: Factory + 'static, + I: FromRequest

+ 'static, + R: Responder + 'static, + { + Route::new().to(handler) + } + + /// Create a new route and add async handler. + /// + /// ```rust + /// use actix_web::{web, App, HttpResponse, Error}; + /// + /// fn index() -> impl futures::Future { + /// futures::future::ok(HttpResponse::Ok().finish()) + /// } + /// + /// App::new().resource("/", |r| r.route(web::to_async(index))); + /// ``` + pub fn to_async(handler: F) -> Route

+ where + F: AsyncFactory, + I: FromRequest

+ 'static, + R: IntoFuture + 'static, + R::Item: Into, + R::Error: Into, + { + Route::new().to_async(handler) + } +} + pub mod dev { pub use crate::app::AppRouter; pub use crate::handler::{AsyncFactory, Extract, Factory, Handle}; - pub use crate::route::{Route, RouteBuilder}; // pub use crate::info::ConnectionInfo; } diff --git a/src/request.rs b/src/request.rs index 571431cc..538064f1 100644 --- a/src/request.rs +++ b/src/request.rs @@ -9,11 +9,11 @@ use actix_router::{Path, Url}; use futures::future::{ok, FutureResult}; use crate::handler::FromRequest; -use crate::service::ServiceRequest; +use crate::service::ServiceFromRequest; #[derive(Clone)] pub struct HttpRequest { - head: Message, + pub(crate) head: Message, pub(crate) path: Path, extensions: Rc, } @@ -145,7 +145,7 @@ impl

FromRequest

for HttpRequest { type Future = FutureResult; #[inline] - fn from_request(req: &mut ServiceRequest

) -> Self::Future { + fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { ok(req.clone()) } } diff --git a/src/resource.rs b/src/resource.rs index 80ac8d83..ab6096c5 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -1,7 +1,7 @@ use std::cell::RefCell; use std::rc::Rc; -use actix_http::{http::Method, Error, Response}; +use actix_http::{Error, Response}; use actix_service::boxed::{self, BoxedNewService, BoxedService}; use actix_service::{ ApplyNewService, IntoNewService, IntoNewTransform, NewService, NewTransform, Service, @@ -11,7 +11,7 @@ use futures::{Async, Future, IntoFuture, Poll}; use crate::handler::{AsyncFactory, Factory, FromRequest}; use crate::responder::Responder; -use crate::route::{CreateRouteService, Route, RouteBuilder, RouteService}; +use crate::route::{CreateRouteService, Route, RouteService}; use crate::service::{ServiceRequest, ServiceResponse}; type HttpService

= BoxedService, ServiceResponse, ()>; @@ -74,92 +74,8 @@ where /// .finish(); /// } /// ``` - pub fn route(mut self, f: F) -> Self - where - F: FnOnce(RouteBuilder

) -> Route

, - { - self.routes.push(f(Route::build())); - self - } - - /// Register a new `GET` route. - pub fn get(mut self, f: F) -> Self - where - F: Factory + 'static, - I: FromRequest

+ 'static, - R: Responder + 'static, - { - self.routes.push(Route::get().to(f)); - self - } - - /// Register a new `POST` route. - pub fn post(mut self, f: F) -> Self - where - F: Factory + 'static, - I: FromRequest

+ 'static, - R: Responder + 'static, - { - self.routes.push(Route::post().to(f)); - self - } - - /// Register a new `PUT` route. - pub fn put(mut self, f: F) -> Self - where - F: Factory + 'static, - I: FromRequest

+ 'static, - R: Responder + 'static, - { - self.routes.push(Route::put().to(f)); - self - } - - /// Register a new `DELETE` route. - pub fn delete(mut self, f: F) -> Self - where - F: Factory + 'static, - I: FromRequest

+ 'static, - R: Responder + 'static, - { - self.routes.push(Route::delete().to(f)); - self - } - - /// Register a new `HEAD` route. - pub fn head(mut self, f: F) -> Self - where - F: Factory + 'static, - I: FromRequest

+ 'static, - R: Responder + 'static, - { - self.routes.push(Route::build().method(Method::HEAD).to(f)); - self - } - - /// Register a new route and add method check to route. - /// - /// ```rust,ignore - /// # extern crate actix_web; - /// use actix_web::*; - /// fn index(req: &HttpRequest) -> HttpResponse { unimplemented!() } - /// - /// App::new().resource("/", |r| r.method(http::Method::GET).f(index)); - /// ``` - /// - /// This is shortcut for: - /// - /// ```rust,ignore - /// # extern crate actix_web; - /// # use actix_web::*; - /// # fn index(req: &HttpRequest) -> HttpResponse { unimplemented!() } - /// App::new().resource("/", |r| r.route().filter(pred::Get()).f(index)); - /// ``` - pub fn method(mut self, method: Method, f: F) -> Self - where - F: FnOnce(RouteBuilder

) -> Route

, - { - self.routes.push(f(Route::build().method(method))); + pub fn route(mut self, route: Route

) -> Self { + self.routes.push(route); self } @@ -187,7 +103,7 @@ where I: FromRequest

+ 'static, R: Responder + 'static, { - self.routes.push(Route::build().to(handler)); + self.routes.push(Route::new().to(handler)); self } @@ -227,7 +143,7 @@ where R::Item: Into, R::Error: Into, { - self.routes.push(Route::build().to_async(handler)); + self.routes.push(Route::new().to_async(handler)); self } diff --git a/src/route.rs b/src/route.rs index d5e11424..99117afe 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,4 +1,3 @@ -use std::marker::PhantomData; use std::rc::Rc; use actix_http::{http::Method, Error, Response}; @@ -8,7 +7,8 @@ use futures::{Async, Future, IntoFuture, Poll}; use crate::filter::{self, Filter}; use crate::handler::{AsyncFactory, AsyncHandle, Extract, Factory, FromRequest, Handle}; use crate::responder::Responder; -use crate::service::{ServiceRequest, ServiceResponse}; +use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; +use crate::HttpResponse; type BoxedRouteService = Box< Service< @@ -40,24 +40,29 @@ pub struct Route

{ } impl Route

{ - pub fn build() -> RouteBuilder

{ - RouteBuilder::new() + pub fn new() -> Route

{ + Route { + service: Box::new(RouteNewService::new(Extract::new().and_then( + Handle::new(|| HttpResponse::NotFound()).map_err(|_| panic!()), + ))), + filters: Rc::new(Vec::new()), + } } - pub fn get() -> RouteBuilder

{ - RouteBuilder::new().method(Method::GET) + pub fn get() -> Route

{ + Route::new().method(Method::GET) } - pub fn post() -> RouteBuilder

{ - RouteBuilder::new().method(Method::POST) + pub fn post() -> Route

{ + Route::new().method(Method::POST) } - pub fn put() -> RouteBuilder

{ - RouteBuilder::new().method(Method::PUT) + pub fn put() -> Route

{ + Route::new().method(Method::PUT) } - pub fn delete() -> RouteBuilder

{ - RouteBuilder::new().method(Method::DELETE) + pub fn delete() -> Route

{ + Route::new().method(Method::DELETE) } } @@ -109,7 +114,7 @@ pub struct RouteService

{ impl

RouteService

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

) -> bool { for f in self.filters.iter() { - if !f.check(req.request()) { + if !f.check(req.head()) { return false; } } @@ -132,19 +137,7 @@ impl

Service for RouteService

{ } } -pub struct RouteBuilder

{ - filters: Vec>, - _t: PhantomData

, -} - -impl RouteBuilder

{ - fn new() -> RouteBuilder

{ - RouteBuilder { - filters: Vec::new(), - _t: PhantomData, - } - } - +impl Route

{ /// Add method match filter to the route. /// /// ```rust,ignore @@ -161,7 +154,9 @@ impl RouteBuilder

{ /// # } /// ``` pub fn method(mut self, method: Method) -> Self { - self.filters.push(Box::new(filter::Method(method))); + Rc::get_mut(&mut self.filters) + .unwrap() + .push(Box::new(filter::Method(method))); self } @@ -181,7 +176,7 @@ impl RouteBuilder

{ /// # } /// ``` pub fn filter(mut self, f: F) -> Self { - self.filters.push(Box::new(f)); + Rc::get_mut(&mut self.filters).unwrap().push(Box::new(f)); self } @@ -259,18 +254,16 @@ impl RouteBuilder

{ /// ); // <- use `with` extractor /// } /// ``` - pub fn to(self, handler: F) -> Route

+ pub fn to(mut self, handler: F) -> Route

where F: Factory + 'static, T: FromRequest

+ 'static, R: Responder + 'static, { - Route { - service: Box::new(RouteNewService::new( - Extract::new().and_then(Handle::new(handler).map_err(|_| panic!())), - )), - filters: Rc::new(self.filters), - } + self.service = Box::new(RouteNewService::new( + Extract::new().and_then(Handle::new(handler).map_err(|_| panic!())), + )); + self } /// Set async handler function, use request extractor for parameters. @@ -303,7 +296,7 @@ impl RouteBuilder

{ /// } /// ``` #[allow(clippy::wrong_self_convention)] - pub fn to_async(self, handler: F) -> Route

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

+ 'static, @@ -311,12 +304,10 @@ impl RouteBuilder

{ R::Item: Into, R::Error: Into, { - Route { - service: Box::new(RouteNewService::new( - Extract::new().and_then(AsyncHandle::new(handler).map_err(|_| panic!())), - )), - filters: Rc::new(self.filters), - } + self.service = Box::new(RouteNewService::new( + Extract::new().and_then(AsyncHandle::new(handler).map_err(|_| panic!())), + )); + self } } @@ -450,7 +441,7 @@ impl RouteBuilder

{ struct RouteNewService where - T: NewService, Error = (Error, ServiceRequest

)>, + T: NewService, Error = (Error, ServiceFromRequest

)>, { service: T, } @@ -460,7 +451,7 @@ where T: NewService< Request = ServiceRequest

, Response = ServiceResponse, - Error = (Error, ServiceRequest

), + Error = (Error, ServiceFromRequest

), >, T::Future: 'static, T::Service: 'static, @@ -476,7 +467,7 @@ where T: NewService< Request = ServiceRequest

, Response = ServiceResponse, - Error = (Error, ServiceRequest

), + Error = (Error, ServiceFromRequest

), >, T::Future: 'static, T::Service: 'static, @@ -513,7 +504,7 @@ where T: Service< Request = ServiceRequest

, Response = ServiceResponse, - Error = (Error, ServiceRequest

), + Error = (Error, ServiceFromRequest

), >, { type Request = ServiceRequest

; diff --git a/src/service.rs b/src/service.rs index 775bb611..078cb223 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,9 +1,11 @@ +use std::cell::{Ref, RefMut}; use std::rc::Rc; use actix_http::body::{Body, MessageBody, ResponseBody}; -use actix_http::http::HeaderMap; +use actix_http::http::{HeaderMap, Method, Uri, Version}; use actix_http::{ - Error, Extensions, HttpMessage, Payload, Request, Response, ResponseHead, + Error, Extensions, HttpMessage, Payload, Request, RequestHead, Response, + ResponseHead, }; use actix_router::{Path, Url}; @@ -27,11 +29,6 @@ impl

ServiceRequest

{ } } - #[inline] - pub fn request(&self) -> &HttpRequest { - &self.req - } - #[inline] pub fn into_request(self) -> HttpRequest { self.req @@ -49,10 +46,93 @@ impl

ServiceRequest

{ ServiceResponse::new(self.req, err.into().into()) } + /// This method returns reference to the request head + #[inline] + pub fn head(&self) -> &RequestHead { + &self.req.head + } + + /// This method returns reference to the request head + #[inline] + pub fn head_mut(&mut self) -> &mut RequestHead { + &mut self.req.head + } + + /// 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 + } + + /// The target path of this Request. + #[inline] + pub fn path(&self) -> &str { + self.head().uri.path() + } + + #[inline] + /// Returns Request's headers. + pub fn headers(&self) -> &HeaderMap { + &self.head().headers + } + + /// 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.req.path + } + #[inline] pub fn match_info_mut(&mut self) -> &mut Path { &mut self.req.path } + + /// Request extensions + #[inline] + pub fn extensions(&self) -> Ref { + self.req.head.extensions() + } + + /// Mutable reference to a the request's extensions + #[inline] + pub fn extensions_mut(&self) -> RefMut { + self.req.head.extensions_mut() + } + + /// Application extensions + #[inline] + pub fn app_extensions(&self) -> &Extensions { + self.req.app_extensions() + } } impl

HttpMessage for ServiceRequest

{ @@ -70,10 +150,65 @@ impl

HttpMessage for ServiceRequest

{ } impl

std::ops::Deref for ServiceRequest

{ + type Target = RequestHead; + + fn deref(&self) -> &RequestHead { + self.req.head() + } +} + +impl

std::ops::DerefMut for ServiceRequest

{ + fn deref_mut(&mut self) -> &mut RequestHead { + self.head_mut() + } +} + +pub struct ServiceFromRequest

{ + req: HttpRequest, + payload: Payload

, +} + +impl

ServiceFromRequest

{ + #[inline] + pub fn into_request(self) -> HttpRequest { + self.req + } + + /// Create service response for error + #[inline] + pub fn error_response>(self, err: E) -> ServiceResponse { + ServiceResponse::new(self.req, err.into().into()) + } +} + +impl

std::ops::Deref for ServiceFromRequest

{ type Target = HttpRequest; fn deref(&self) -> &HttpRequest { - self.request() + &self.req + } +} + +impl

HttpMessage for ServiceFromRequest

{ + type Stream = P; + + #[inline] + fn headers(&self) -> &HeaderMap { + self.req.headers() + } + + #[inline] + fn take_payload(&mut self) -> Payload { + std::mem::replace(&mut self.payload, Payload::None) + } +} + +impl

From> for ServiceFromRequest

{ + fn from(req: ServiceRequest

) -> Self { + Self { + req: req.req, + payload: req.payload, + } } } diff --git a/src/state.rs b/src/state.rs index 82aecc6e..db263777 100644 --- a/src/state.rs +++ b/src/state.rs @@ -7,7 +7,7 @@ use futures::future::{err, ok, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; use crate::handler::FromRequest; -use crate::service::ServiceRequest; +use crate::service::ServiceFromRequest; /// Application state factory pub(crate) trait StateFactory { @@ -50,7 +50,7 @@ impl FromRequest

for State { type Future = FutureResult; #[inline] - fn from_request(req: &mut ServiceRequest

) -> Self::Future { + fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { if let Some(st) = req.app_extensions().get::>() { ok(st.clone()) } else { diff --git a/tests/test_server.rs b/tests/test_server.rs index 590cc0e7..d28f207c 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -12,7 +12,7 @@ use flate2::write::ZlibDecoder; use futures::stream::once; //Future, Stream use rand::{distributions::Alphanumeric, Rng}; -use actix_web::{middleware, App}; +use actix_web::{middleware, web, App}; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ @@ -40,7 +40,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ fn test_body() { let mut srv = TestServer::new(|| { h1::H1Service::new( - App::new().resource("/", |r| r.get(|| Response::Ok().body(STR))), + App::new().resource("/", |r| r.route(web::to(|| Response::Ok().body(STR)))), ) }); @@ -59,7 +59,7 @@ fn test_body_gzip() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Gzip)) - .resource("/", |r| r.get(|| Response::Ok().body(STR))), + .resource("/", |r| r.route(web::to(|| Response::Ok().body(STR)))), ) }); @@ -87,7 +87,9 @@ fn test_body_gzip_large() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Gzip)) - .resource("/", |r| r.get(move || Response::Ok().body(data.clone()))), + .resource("/", |r| { + r.route(web::to(move || Response::Ok().body(data.clone()))) + }), ) }); @@ -118,7 +120,9 @@ fn test_body_gzip_large_random() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Gzip)) - .resource("/", |r| r.get(move || Response::Ok().body(data.clone()))), + .resource("/", |r| { + r.route(web::to(move || Response::Ok().body(data.clone()))) + }), ) }); @@ -144,11 +148,11 @@ fn test_body_chunked_implicit() { App::new() .middleware(middleware::Compress::new(ContentEncoding::Gzip)) .resource("/", |r| { - r.get(move || { + r.route(web::get().to(move || { Response::Ok().streaming(once(Ok::<_, Error>( Bytes::from_static(STR.as_ref()), ))) - }) + })) }), ) }); @@ -178,11 +182,11 @@ fn test_body_br_streaming() { App::new() .middleware(middleware::Compress::new(ContentEncoding::Br)) .resource("/", |r| { - r.get(move || { + r.route(web::to(move || { Response::Ok().streaming(once(Ok::<_, Error>( Bytes::from_static(STR.as_ref()), ))) - }) + })) }), ) }); @@ -205,7 +209,7 @@ fn test_body_br_streaming() { fn test_head_binary() { let mut srv = TestServer::new(move || { h1::H1Service::new(App::new().resource("/", |r| { - r.head(move || Response::Ok().content_length(100).body(STR)) + r.route(web::head().to(move || Response::Ok().content_length(100).body(STR))) })) }); @@ -227,12 +231,12 @@ fn test_head_binary() { fn test_no_chunking() { let mut srv = TestServer::new(move || { h1::H1Service::new(App::new().resource("/", |r| { - r.get(move || { + r.route(web::to(move || { Response::Ok() .no_chunking() .content_length(STR.len() as u64) .streaming(once(Ok::<_, Error>(Bytes::from_static(STR.as_ref())))) - }) + })) })) }); @@ -252,7 +256,7 @@ fn test_body_deflate() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Deflate)) - .resource("/", |r| r.get(move || Response::Ok().body(STR))), + .resource("/", |r| r.route(web::to(move || Response::Ok().body(STR)))), ) }); @@ -277,7 +281,7 @@ fn test_body_brotli() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Br)) - .resource("/", |r| r.get(move || Response::Ok().body(STR))), + .resource("/", |r| r.route(web::to(move || Response::Ok().body(STR)))), ) }); From 352e7b7a754cb48e09922b8cd3a7c883369d2128 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 21:35:31 -0800 Subject: [PATCH 011/109] update tests for defaultheaders middleware --- src/middleware/defaultheaders.rs | 74 +++++++++++++++++++------------- src/service.rs | 11 +++++ 2 files changed, 56 insertions(+), 29 deletions(-) diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index 1ace34fe..83bb94c6 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -13,17 +13,15 @@ use crate::service::{ServiceRequest, ServiceResponse}; /// /// This middleware does not set header if response headers already contains it. /// -/// ```rust,ignore -/// # extern crate actix_web; -/// use actix_web::{http, middleware, App, HttpResponse}; +/// ```rust +/// use actix_web::{web, http, middleware, App, HttpResponse}; /// /// fn main() { /// let app = App::new() /// .middleware(middleware::DefaultHeaders::new().header("X-Version", "0.2")) /// .resource("/test", |r| { -/// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); -/// r.method(http::Method::HEAD) -/// .f(|_| HttpResponse::MethodNotAllowed()); +/// r.route(web::get().to(|| HttpResponse::Ok())) +/// .route(web::method(http::Method::HEAD).to(|| HttpResponse::MethodNotAllowed())) /// }); /// } /// ``` @@ -134,30 +132,48 @@ where } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use actix_http::http::header::CONTENT_TYPE; -// use actix_http::test::TestRequest; +#[cfg(test)] +mod tests { + use actix_http::http::header::CONTENT_TYPE; + use actix_service::FnService; -// #[test] -// fn test_default_headers() { -// let mw = DefaultHeaders::new().header(CONTENT_TYPE, "0001"); + use super::*; + use crate::test::TestServiceRequest; + use crate::{HttpResponse, ServiceRequest}; -// let req = TestRequest::default().finish(); + #[test] + fn test_default_headers() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let mut mw = DefaultHeaders::new().header(CONTENT_TYPE, "0001"); + let mut srv = FnService::new(|req: ServiceRequest<_>| { + req.into_response(HttpResponse::Ok().finish()) + }); -// let resp = Response::Ok().finish(); -// let resp = match mw.response(&req, resp) { -// Ok(Response::Done(resp)) => resp, -// _ => panic!(), -// }; -// assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); + let req = TestServiceRequest::default().finish(); + let resp = rt.block_on(mw.call(req, &mut srv)).unwrap(); + assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); -// let resp = Response::Ok().header(CONTENT_TYPE, "0002").finish(); -// let resp = match mw.response(&req, resp) { -// Ok(Response::Done(resp)) => resp, -// _ => panic!(), -// }; -// assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0002"); -// } -// } + let req = TestServiceRequest::default().finish(); + let mut srv = FnService::new(|req: ServiceRequest<_>| { + req.into_response(HttpResponse::Ok().header(CONTENT_TYPE, "0002").finish()) + }); + let resp = rt.block_on(mw.call(req, &mut srv)).unwrap(); + assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0002"); + } + + #[test] + fn test_content_type() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let mut mw = DefaultHeaders::new().content_type(); + let mut srv = FnService::new(|req: ServiceRequest<_>| { + req.into_response(HttpResponse::Ok().finish()) + }); + + let req = TestServiceRequest::default().finish(); + let resp = rt.block_on(mw.call(req, &mut srv)).unwrap(); + assert_eq!( + resp.headers().get(CONTENT_TYPE).unwrap(), + "application/octet-stream" + ); + } +} diff --git a/src/service.rs b/src/service.rs index 078cb223..99af73c1 100644 --- a/src/service.rs +++ b/src/service.rs @@ -8,6 +8,7 @@ use actix_http::{ ResponseHead, }; use actix_router::{Path, Url}; +use futures::future::{ok, FutureResult, IntoFuture}; use crate::request::HttpRequest; @@ -288,3 +289,13 @@ impl Into> for ServiceResponse { self.response } } + +impl IntoFuture for ServiceResponse { + type Item = ServiceResponse; + type Error = Error; + type Future = FutureResult, Error>; + + fn into_future(self) -> Self::Future { + ok(self) + } +} From d5c54a18675f2ed7159307ea616ebbcce85d3444 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 22:03:45 -0800 Subject: [PATCH 012/109] update extractor tests --- src/app.rs | 36 +++++------ src/extractor.rs | 105 ++++++++++++++----------------- src/filter.rs | 32 ++++------ src/middleware/defaultheaders.rs | 8 +-- src/middleware/mod.rs | 13 ---- src/test.rs | 49 +++++---------- 6 files changed, 97 insertions(+), 146 deletions(-) diff --git a/src/app.rs b/src/app.rs index d9219091..ae510621 100644 --- a/src/app.rs +++ b/src/app.rs @@ -29,7 +29,8 @@ pub trait HttpServiceFactory { fn create(self) -> Self::Factory; } -/// Application builder +/// Application builder - structure that follows the builder pattern +/// for building application instances. pub struct App where T: NewService, Response = ServiceRequest

>, @@ -69,11 +70,8 @@ where InitError = (), >, { - /// Create application with specified state. Application can be - /// configured with a builder-like pattern. - /// - /// State is shared with all resources within same application and - /// could be accessed with `HttpRequest::state()` method. + /// Set application state. Applicatin state could be accessed + /// by using `State` extractor where `T` is state type. /// /// **Note**: http server accepts an application factory rather than /// an application instance. Http server constructs an application @@ -86,7 +84,7 @@ where self } - /// Set application state. This function is + /// Set application state factory. This function is /// similar to `.state()` but it accepts state factory. State get /// constructed asynchronously during application initialization. pub fn state_factory(mut self, state: F) -> Self @@ -119,14 +117,14 @@ where /// `/users/{userid}/{friend}` and store `userid` and `friend` in /// the exposed `Params` object: /// - /// ```rust,ignore + /// ```rust /// # extern crate actix_web; - /// use actix_web::{http, App, HttpResponse}; + /// use actix_web::{web, http, App, HttpResponse}; /// /// fn main() { /// let app = App::new().resource("/users/{userid}/{friend}", |r| { - /// r.get(|r| r.to(|_| HttpResponse::Ok())); - /// r.head(|r| r.to(|_| HttpResponse::MethodNotAllowed())) + /// r.route(web::get().to(|| HttpResponse::Ok())) + /// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) /// }); /// } /// ``` @@ -294,15 +292,17 @@ where /// `/users/{userid}/{friend}` and store `userid` and `friend` in /// the exposed `Params` object: /// - /// ```rust,ignore - /// # extern crate actix_web; - /// use actix_web::{http, App, HttpResponse}; + /// ```rust + /// use actix_web::{web, http, App, HttpResponse}; /// /// fn main() { - /// let app = App::new().resource("/users/{userid}/{friend}", |r| { - /// r.get(|r| r.to(|_| HttpResponse::Ok())); - /// r.head(|r| r.to(|_| HttpResponse::MethodNotAllowed())) - /// }); + /// let app = App::new() + /// .resource("/users/{userid}/{friend}", |r| { + /// r.route(web::to(|| HttpResponse::Ok())) + /// }) + /// .resource("/index.html", |r| { + /// r.route(web::head().to(|| HttpResponse::MethodNotAllowed())) + /// }); /// } /// ``` pub fn resource(mut self, path: &str, f: F) -> Self diff --git a/src/extractor.rs b/src/extractor.rs index 522ce721..04dfb81a 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -999,73 +999,60 @@ tuple_from_req!( (9, J) ); -// #[cfg(test)] -// mod tests { -// use super::*; -// use actix_http::http::header; -// use actix_http::test::TestRequest; -// use bytes::Bytes; -// use futures::{Async, Future}; -// use mime; -// use serde::{Deserialize, Serialize}; +#[cfg(test)] +mod tests { + use actix_http::http::header; + use bytes::Bytes; + use serde_derive::Deserialize; -// use crate::resource::Resource; -// // use crate::router::{ResourceDef, Router}; + use super::*; + use crate::test::TestRequest; -// #[derive(Deserialize, Debug, PartialEq)] -// struct Info { -// hello: String, -// } + #[derive(Deserialize, Debug, PartialEq)] + struct Info { + hello: String, + } -// #[test] -// fn test_bytes() { -// let cfg = PayloadConfig::default(); -// let req = TestRequest::with_header(header::CONTENT_LENGTH, "11") -// .set_payload(Bytes::from_static(b"hello=world")) -// .finish(); + #[test] + fn test_bytes() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11") + .set_payload(Bytes::from_static(b"hello=world")) + .finish() + .into(); -// match Bytes::from_request(&req, &cfg).unwrap().poll().unwrap() { -// Async::Ready(s) => { -// assert_eq!(s, Bytes::from_static(b"hello=world")); -// } -// _ => unreachable!(), -// } -// } + let s = rt.block_on(Bytes::from_request(&mut req)).unwrap(); + assert_eq!(s, Bytes::from_static(b"hello=world")); + } -// #[test] -// fn test_string() { -// let cfg = PayloadConfig::default(); -// let req = TestRequest::with_header(header::CONTENT_LENGTH, "11") -// .set_payload(Bytes::from_static(b"hello=world")) -// .finish(); + #[test] + fn test_string() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11") + .set_payload(Bytes::from_static(b"hello=world")) + .finish() + .into(); -// match String::from_request(&req, &cfg).unwrap().poll().unwrap() { -// Async::Ready(s) => { -// assert_eq!(s, "hello=world"); -// } -// _ => unreachable!(), -// } -// } + let s = rt.block_on(String::from_request(&mut req)).unwrap(); + assert_eq!(s, "hello=world"); + } -// #[test] -// fn test_form() { -// let req = TestRequest::with_header( -// header::CONTENT_TYPE, -// "application/x-www-form-urlencoded", -// ) -// .header(header::CONTENT_LENGTH, "11") -// .set_payload(Bytes::from_static(b"hello=world")) -// .finish(); + #[test] + fn test_form() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let mut req = TestRequest::with_header( + header::CONTENT_TYPE, + "application/x-www-form-urlencoded", + ) + .header(header::CONTENT_LENGTH, "11") + .set_payload(Bytes::from_static(b"hello=world")) + .finish() + .into(); -// let mut cfg = FormConfig::default(); -// cfg.limit(4096); -// match Form::::from_request(&req, &cfg).poll().unwrap() { -// Async::Ready(s) => { -// assert_eq!(s.hello, "world"); -// } -// _ => unreachable!(), -// } -// } + let s = rt.block_on(Form::::from_request(&mut req)).unwrap(); + assert_eq!(s.hello, "world"); + } +} // #[test] // fn test_option() { diff --git a/src/filter.rs b/src/filter.rs index e3d87b76..9b49c9dd 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -230,14 +230,14 @@ impl Filter for HeaderFilter { #[cfg(test)] mod tests { - use crate::test::TestServiceRequest; use actix_http::http::{header, Method}; use super::*; + use crate::test::TestRequest; #[test] fn test_header() { - let req = TestServiceRequest::with_header(header::TRANSFER_ENCODING, "chunked") + let req = TestRequest::with_header(header::TRANSFER_ENCODING, "chunked") .finish() .into_request(); @@ -269,8 +269,8 @@ mod tests { #[test] fn test_methods() { - let req = TestServiceRequest::default().finish().into_request(); - let req2 = TestServiceRequest::default() + let req = TestRequest::default().finish().into_request(); + let req2 = TestRequest::default() .method(Method::POST) .finish() .into_request(); @@ -280,46 +280,38 @@ mod tests { assert!(Post().check(&req2)); assert!(!Post().check(&req)); - let r = TestServiceRequest::default().method(Method::PUT).finish(); + let r = TestRequest::default().method(Method::PUT).finish(); assert!(Put().check(&r,)); assert!(!Put().check(&req,)); - let r = TestServiceRequest::default() - .method(Method::DELETE) - .finish(); + let r = TestRequest::default().method(Method::DELETE).finish(); assert!(Delete().check(&r,)); assert!(!Delete().check(&req,)); - let r = TestServiceRequest::default().method(Method::HEAD).finish(); + let r = TestRequest::default().method(Method::HEAD).finish(); assert!(Head().check(&r,)); assert!(!Head().check(&req,)); - let r = TestServiceRequest::default() - .method(Method::OPTIONS) - .finish(); + let r = TestRequest::default().method(Method::OPTIONS).finish(); assert!(Options().check(&r,)); assert!(!Options().check(&req,)); - let r = TestServiceRequest::default() - .method(Method::CONNECT) - .finish(); + let r = TestRequest::default().method(Method::CONNECT).finish(); assert!(Connect().check(&r,)); assert!(!Connect().check(&req,)); - let r = TestServiceRequest::default().method(Method::PATCH).finish(); + let r = TestRequest::default().method(Method::PATCH).finish(); assert!(Patch().check(&r,)); assert!(!Patch().check(&req,)); - let r = TestServiceRequest::default().method(Method::TRACE).finish(); + let r = TestRequest::default().method(Method::TRACE).finish(); assert!(Trace().check(&r,)); assert!(!Trace().check(&req,)); } #[test] fn test_preds() { - let r = TestServiceRequest::default() - .method(Method::TRACE) - .request(); + let r = TestRequest::default().method(Method::TRACE).request(); assert!(Not(Get()).check(&r,)); assert!(!Not(Trace()).check(&r,)); diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index 83bb94c6..fa287b28 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -138,7 +138,7 @@ mod tests { use actix_service::FnService; use super::*; - use crate::test::TestServiceRequest; + use crate::test::TestRequest; use crate::{HttpResponse, ServiceRequest}; #[test] @@ -149,11 +149,11 @@ mod tests { req.into_response(HttpResponse::Ok().finish()) }); - let req = TestServiceRequest::default().finish(); + let req = TestRequest::default().finish(); let resp = rt.block_on(mw.call(req, &mut srv)).unwrap(); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); - let req = TestServiceRequest::default().finish(); + let req = TestRequest::default().finish(); let mut srv = FnService::new(|req: ServiceRequest<_>| { req.into_response(HttpResponse::Ok().header(CONTENT_TYPE, "0002").finish()) }); @@ -169,7 +169,7 @@ mod tests { req.into_response(HttpResponse::Ok().finish()) }); - let req = TestServiceRequest::default().finish(); + let req = TestRequest::default().finish(); let resp = rt.block_on(mw.call(req, &mut srv)).unwrap(); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 8ef316b4..85127ee2 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -34,19 +34,6 @@ where } } -impl Clone for MiddlewareFactory -where - T: Transform + Clone, - S: Service, -{ - fn clone(&self) -> Self { - Self { - tr: self.tr.clone(), - _t: PhantomData, - } - } -} - impl NewTransform for MiddlewareFactory where T: Transform + Clone, diff --git a/src/test.rs b/src/test.rs index d67696b1..d6caf897 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,10 +1,9 @@ //! Various helpers for Actix applications to use during testing. -use std::ops::{Deref, DerefMut}; use std::rc::Rc; use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; use actix_http::http::{HttpTryFrom, Method, Version}; -use actix_http::test::TestRequest; +use actix_http::test::TestRequest as HttpTestRequest; use actix_http::{Extensions, PayloadStream}; use actix_router::{Path, Url}; use bytes::Bytes; @@ -39,45 +38,45 @@ use crate::service::ServiceRequest; /// assert_eq!(resp.status(), StatusCode::BAD_REQUEST); /// } /// ``` -pub struct TestServiceRequest { - req: TestRequest, +pub struct TestRequest { + req: HttpTestRequest, extensions: Extensions, } -impl Default for TestServiceRequest { - fn default() -> TestServiceRequest { - TestServiceRequest { - req: TestRequest::default(), +impl Default for TestRequest { + fn default() -> TestRequest { + TestRequest { + req: HttpTestRequest::default(), extensions: Extensions::new(), } } } -impl TestServiceRequest { +impl TestRequest { /// Create TestRequest and set request uri - pub fn with_uri(path: &str) -> TestServiceRequest { - TestServiceRequest { - req: TestRequest::default().uri(path).take(), + pub fn with_uri(path: &str) -> TestRequest { + TestRequest { + req: HttpTestRequest::default().uri(path).take(), extensions: Extensions::new(), } } /// Create TestRequest and set header - pub fn with_hdr(hdr: H) -> TestServiceRequest { - TestServiceRequest { - req: TestRequest::default().set(hdr).take(), + pub fn with_hdr(hdr: H) -> TestRequest { + TestRequest { + req: HttpTestRequest::default().set(hdr).take(), extensions: Extensions::new(), } } /// Create TestRequest and set header - pub fn with_header(key: K, value: V) -> TestServiceRequest + pub fn with_header(key: K, value: V) -> TestRequest where HeaderName: HttpTryFrom, V: IntoHeaderValue, { - TestServiceRequest { - req: TestRequest::default().header(key, value).take(), + TestRequest { + req: HttpTestRequest::default().header(key, value).take(), extensions: Extensions::new(), } } @@ -145,17 +144,3 @@ impl TestServiceRequest { .into_request() } } - -impl Deref for TestServiceRequest { - type Target = TestRequest; - - fn deref(&self) -> &TestRequest { - &self.req - } -} - -impl DerefMut for TestServiceRequest { - fn deref_mut(&mut self) -> &mut TestRequest { - &mut self.req - } -} From 115b30d9ccf52c9f19609bf13862dfdd58b99d0b Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 22:11:24 -0800 Subject: [PATCH 013/109] add state example --- src/app.rs | 21 ++++++++++++++++ src/extractor.rs | 65 ++++++++---------------------------------------- 2 files changed, 31 insertions(+), 55 deletions(-) diff --git a/src/app.rs b/src/app.rs index ae510621..e9b10081 100644 --- a/src/app.rs +++ b/src/app.rs @@ -79,6 +79,27 @@ where /// multiple times. If you want to share state between different /// threads, a shared object should be used, e.g. `Arc`. Application /// state does not need to be `Send` or `Sync`. + /// + /// ```rust + /// use std::cell::Cell; + /// use actix_web::{web, State, App}; + /// + /// struct MyState { + /// counter: Cell, + /// } + /// + /// fn index(state: State) { + /// state.counter.set(state.counter.get() + 1); + /// } + /// + /// fn main() { + /// let app = App::new() + /// .state(MyState{ counter: Cell::new(0) }) + /// .resource( + /// "/index.html", + /// |r| r.route(web::get().to(index))); + /// } + /// ``` pub fn state(mut self, state: S) -> Self { self.state.push(Box::new(State::new(state))); self diff --git a/src/extractor.rs b/src/extractor.rs index 04dfb81a..ea7681b0 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -938,66 +938,21 @@ impl

FromRequest

for () { } } +#[rustfmt::skip] +mod m { + use super::*; + tuple_from_req!(TupleFromRequest1, (0, A)); tuple_from_req!(TupleFromRequest2, (0, A), (1, B)); tuple_from_req!(TupleFromRequest3, (0, A), (1, B), (2, C)); tuple_from_req!(TupleFromRequest4, (0, A), (1, B), (2, C), (3, D)); tuple_from_req!(TupleFromRequest5, (0, A), (1, B), (2, C), (3, D), (4, E)); -tuple_from_req!( - TupleFromRequest6, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F) -); -tuple_from_req!( - TupleFromRequest7, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G) -); -tuple_from_req!( - TupleFromRequest8, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G), - (7, H) -); -tuple_from_req!( - TupleFromRequest9, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G), - (7, H), - (8, I) -); -tuple_from_req!( - TupleFromRequest10, - (0, A), - (1, B), - (2, C), - (3, D), - (4, E), - (5, F), - (6, G), - (7, H), - (8, I), - (9, J) -); +tuple_from_req!(TupleFromRequest6, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); +tuple_from_req!(TupleFromRequest7, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); +tuple_from_req!(TupleFromRequest8, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); +tuple_from_req!(TupleFromRequest9, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); +tuple_from_req!(TupleFromRequest10, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); +} #[cfg(test)] mod tests { From b320dc127a3aa9a64b6b59808138b56edbb1ea56 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 22:22:45 -0800 Subject: [PATCH 014/109] remove unused code --- .travis.yml | 7 ++++++- src/app.rs | 23 ----------------------- 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/.travis.yml b/.travis.yml index 077989d2..f0dc9e90 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ matrix: include: - rust: stable - rust: beta - - rust: nightly + - rust: nightly-2019-03-02 allow_failures: - rust: nightly @@ -24,6 +24,11 @@ before_install: - sudo apt-get update -qq - sudo apt-get install -y openssl libssl-dev libelf-dev libdw-dev cmake gcc binutils-dev libiberty-dev +before_cache: | + if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then + RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install cargo-tarpaulin -f + fi + # Add clippy before_script: - export PATH=$PATH:~/.cargo/bin diff --git a/src/app.rs b/src/app.rs index e9b10081..2a2380b2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -54,12 +54,6 @@ impl App { } } -impl Default for App { - fn default() -> Self { - App::new() - } -} - impl App where P: 'static, @@ -249,23 +243,6 @@ where _t: PhantomData, } } - - /// Complete applicatin chain configuration and start resource - /// configuration. - pub fn router(self) -> AppRouter> { - let fref = Rc::new(RefCell::new(None)); - AppRouter { - chain: self.chain, - services: Vec::new(), - default: None, - defaults: Vec::new(), - endpoint: AppEntry::new(fref.clone()), - factory_ref: fref, - extensions: self.extensions, - state: self.state, - _t: PhantomData, - } - } } /// Structure that follows the builder pattern for building application From 08fcb6891e5f29cd615c7c839aaa004dee652959 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 2 Mar 2019 22:33:46 -0800 Subject: [PATCH 015/109] use specific nightly version for travis --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index f0dc9e90..15a0b04e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,8 +25,8 @@ before_install: - sudo apt-get install -y openssl libssl-dev libelf-dev libdw-dev cmake gcc binutils-dev libiberty-dev before_cache: | - if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then - RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install cargo-tarpaulin -f + if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then + RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install cargo-tarpaulin fi # Add clippy @@ -35,13 +35,13 @@ before_script: script: - | - if [[ "$TRAVIS_RUST_VERSION" != "nightly" ]]; then + if [[ "$TRAVIS_RUST_VERSION" != "nightly-2019-03-02" ]]; then cargo clean cargo check cargo test -- --nocapture fi - | - if [[ "$TRAVIS_RUST_VERSION" == "nightly" ]]; then + if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install -f cargo-tarpaulin RUST_BACKTRACE=1 cargo tarpaulin --out Xml bash <(curl -s https://codecov.io/bash) From 6df85e32dfa2ef7c2c3d587c65c8a3602406e772 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 00:57:48 -0800 Subject: [PATCH 016/109] added extractor configuration system --- src/extractor.rs | 83 ++++++++++++++++++++++++++++++++++++++---------- src/handler.rs | 58 ++++++++++++++++++++++++++------- src/lib.rs | 4 +-- src/request.rs | 1 + src/resource.rs | 2 +- src/route.rs | 66 ++++++++++++++++++++++++++++++++++---- src/service.rs | 29 +++++++++++------ src/state.rs | 1 + src/test.rs | 16 ++++++++-- 9 files changed, 210 insertions(+), 50 deletions(-) diff --git a/src/extractor.rs b/src/extractor.rs index ea7681b0..24e4c8af 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -23,7 +23,7 @@ use actix_http::http::StatusCode; use actix_http::{HttpMessage, Response}; use actix_router::PathDeserializer; -use crate::handler::FromRequest; +use crate::handler::{ConfigStorage, ExtractorConfig, FromRequest}; use crate::request::HttpRequest; use crate::responder::Responder; use crate::service::ServiceFromRequest; @@ -133,6 +133,7 @@ where { type Error = Error; type Future = FutureResult; + type Config = (); #[inline] fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { @@ -219,6 +220,7 @@ where { type Error = Error; type Future = FutureResult; + type Config = (); #[inline] fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { @@ -299,16 +301,18 @@ where { type Error = Error; type Future = Box>; + type Config = FormConfig; #[inline] fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { - let cfg = FormConfig::default(); - let req2 = req.clone(); + let cfg = req.load_config::(); + + let limit = cfg.limit; let err = Rc::clone(&cfg.ehandler); Box::new( UrlEncoded::new(req) - .limit(cfg.limit) + .limit(limit) .map_err(move |e| (*err)(e, &req2)) .map(Form), ) @@ -356,6 +360,7 @@ impl fmt::Display for Form { /// ); /// } /// ``` +#[derive(Clone)] pub struct FormConfig { limit: usize, ehandler: Rc Error>, @@ -363,13 +368,13 @@ pub struct FormConfig { impl FormConfig { /// Change max size of payload. By default max size is 256Kb - pub fn limit(&mut self, limit: usize) -> &mut Self { + pub fn limit(mut self, limit: usize) -> Self { self.limit = limit; self } /// Set custom error handler - pub fn error_handler(&mut self, f: F) -> &mut Self + pub fn error_handler(mut self, f: F) -> Self where F: Fn(UrlencodedError, &HttpRequest) -> Error + 'static, { @@ -378,6 +383,8 @@ impl FormConfig { } } +impl ExtractorConfig for FormConfig {} + impl Default for FormConfig { fn default() -> Self { FormConfig { @@ -509,16 +516,18 @@ where { type Error = Error; type Future = Box>; + type Config = JsonConfig; #[inline] fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { - let cfg = JsonConfig::default(); - let req2 = req.clone(); + let cfg = req.load_config::(); + + let limit = cfg.limit; let err = Rc::clone(&cfg.ehandler); Box::new( JsonBody::new(req) - .limit(cfg.limit) + .limit(limit) .map_err(move |e| (*err)(e, &req2)) .map(Json), ) @@ -555,6 +564,7 @@ where /// }); /// } /// ``` +#[derive(Clone)] pub struct JsonConfig { limit: usize, ehandler: Rc Error>, @@ -562,13 +572,13 @@ pub struct JsonConfig { impl JsonConfig { /// Change max size of payload. By default max size is 256Kb - pub fn limit(&mut self, limit: usize) -> &mut Self { + pub fn limit(mut self, limit: usize) -> Self { self.limit = limit; self } /// Set custom error handler - pub fn error_handler(&mut self, f: F) -> &mut Self + pub fn error_handler(mut self, f: F) -> Self where F: Fn(JsonPayloadError, &HttpRequest) -> Error + 'static, { @@ -577,6 +587,8 @@ impl JsonConfig { } } +impl ExtractorConfig for JsonConfig {} + impl Default for JsonConfig { fn default() -> Self { JsonConfig { @@ -617,16 +629,18 @@ where type Error = Error; type Future = Either>, FutureResult>; + type Config = PayloadConfig; #[inline] fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { - let cfg = PayloadConfig::default(); + let cfg = req.load_config::(); if let Err(e) = cfg.check_mimetype(req) { return Either::B(err(e)); } - Either::A(Box::new(MessageBody::new(req).limit(cfg.limit).from_err())) + let limit = cfg.limit; + Either::A(Box::new(MessageBody::new(req).limit(limit).from_err())) } } @@ -664,10 +678,11 @@ where type Error = Error; type Future = Either>, FutureResult>; + type Config = PayloadConfig; #[inline] fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { - let cfg = PayloadConfig::default(); + let cfg = req.load_config::(); // check content-type if let Err(e) = cfg.check_mimetype(req) { @@ -679,10 +694,11 @@ where Ok(enc) => enc, Err(e) => return Either::B(err(e.into())), }; + let limit = cfg.limit; Either::A(Box::new( MessageBody::new(req) - .limit(cfg.limit) + .limit(limit) .from_err() .and_then(move |body| { let enc: *const Encoding = encoding as *const Encoding; @@ -753,6 +769,7 @@ where { type Error = Error; type Future = Box, Error = Error>>; + type Config = T::Config; #[inline] fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { @@ -816,6 +833,7 @@ where { type Error = Error; type Future = Box, Error = Error>>; + type Config = T::Config; #[inline] fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { @@ -827,21 +845,27 @@ where } /// Payload configuration for request's payload. +#[derive(Clone)] pub struct PayloadConfig { limit: usize, mimetype: Option, } impl PayloadConfig { + /// Create `PayloadConfig` instance and set max size of payload. + pub fn new(limit: usize) -> Self { + Self::default().limit(limit) + } + /// Change max size of payload. By default max size is 256Kb - pub fn limit(&mut self, limit: usize) -> &mut Self { + pub fn limit(mut self, limit: usize) -> Self { self.limit = limit; self } /// Set required mime-type of the request. By default mime type is not /// enforced. - pub fn mimetype(&mut self, mt: Mime) -> &mut Self { + pub fn mimetype(mut self, mt: Mime) -> Self { self.mimetype = Some(mt); self } @@ -867,6 +891,8 @@ impl PayloadConfig { } } +impl ExtractorConfig for PayloadConfig {} + impl Default for PayloadConfig { fn default() -> Self { PayloadConfig { @@ -876,6 +902,16 @@ impl Default for PayloadConfig { } } +macro_rules! tuple_config ({ $($T:ident),+} => { + impl<$($T,)+> ExtractorConfig for ($($T,)+) + where $($T: ExtractorConfig + Clone,)+ + { + fn store_default(ext: &mut ConfigStorage) { + $($T::store_default(ext);)+ + } + } +}); + macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { /// FromRequest implementation for tuple @@ -883,6 +919,7 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { { type Error = Error; type Future = $fut_type; + type Config = ($($T::Config,)+); fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { $fut_type { @@ -932,6 +969,7 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { impl

FromRequest

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

) -> Self::Future { ok(()) @@ -942,6 +980,17 @@ impl

FromRequest

for () { mod m { use super::*; +tuple_config!(A); +tuple_config!(A, B); +tuple_config!(A, B, C); +tuple_config!(A, B, C, D); +tuple_config!(A, B, C, D, E); +tuple_config!(A, B, C, D, E, F); +tuple_config!(A, B, C, D, E, F, G); +tuple_config!(A, B, C, D, E, F, G, H); +tuple_config!(A, B, C, D, E, F, G, H, I); +tuple_config!(A, B, C, D, E, F, G, H, I, J); + tuple_from_req!(TupleFromRequest1, (0, A)); tuple_from_req!(TupleFromRequest2, (0, A), (1, B)); tuple_from_req!(TupleFromRequest3, (0, A), (1, B), (2, C)); diff --git a/src/handler.rs b/src/handler.rs index 31ae796b..313422ed 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,6 +1,8 @@ +use std::cell::RefCell; use std::marker::PhantomData; +use std::rc::Rc; -use actix_http::{Error, Response}; +use actix_http::{Error, Extensions, Response}; use actix_service::{NewService, Service, Void}; use futures::future::{ok, FutureResult}; use futures::{try_ready, Async, Future, IntoFuture, Poll}; @@ -19,10 +21,41 @@ pub trait FromRequest

: Sized { /// Future that resolves to a Self type Future: Future; + /// Configuration for the extractor + type Config: ExtractorConfig; + /// Convert request to a Self fn from_request(req: &mut ServiceFromRequest

) -> Self::Future; } +/// Storage for extractor configs +#[derive(Default)] +pub struct ConfigStorage { + pub(crate) storage: Option>, +} + +impl ConfigStorage { + pub fn store(&mut self, config: C) { + if self.storage.is_none() { + self.storage = Some(Rc::new(Extensions::new())); + } + if let Some(ref mut ext) = self.storage { + Rc::get_mut(ext).unwrap().insert(config); + } + } +} + +pub trait ExtractorConfig: Default + Clone + 'static { + /// Set default configuration to config storage + fn store_default(ext: &mut ConfigStorage) { + ext.store(Self::default()) + } +} + +impl ExtractorConfig for () { + fn store_default(_: &mut ConfigStorage) {} +} + /// Handler converter factory pub trait Factory: Clone where @@ -288,18 +321,16 @@ where /// Extract arguments from request pub struct Extract> { + config: Rc>>>, _t: PhantomData<(P, T)>, } impl> Extract { - pub fn new() -> Self { - Extract { _t: PhantomData } - } -} - -impl> Default for Extract { - fn default() -> Self { - Self::new() + pub fn new(config: Rc>>>) -> Self { + Extract { + config, + _t: PhantomData, + } } } @@ -312,11 +343,15 @@ impl> NewService for Extract { type Future = FutureResult; fn new_service(&self, _: &()) -> Self::Future { - ok(ExtractService { _t: PhantomData }) + ok(ExtractService { + _t: PhantomData, + config: self.config.borrow().clone(), + }) } } pub struct ExtractService> { + config: Option>, _t: PhantomData<(P, T)>, } @@ -331,7 +366,7 @@ impl> Service for ExtractService { } fn call(&mut self, req: ServiceRequest

) -> Self::Future { - let mut req = req.into(); + let mut req = ServiceFromRequest::new(req, self.config.clone()); ExtractResponse { fut: T::from_request(&mut req), req: Some(req), @@ -365,7 +400,6 @@ impl> Future for ExtractResponse { macro_rules! factory_tuple ({ $(($n:tt, $T:ident)),+} => { impl Factory<($($T,)+), Res> for Func where Func: Fn($($T,)+) -> Res + Clone + 'static, - //$($T,)+ Res: Responder + 'static, { fn call(&self, param: ($($T,)+)) -> Res { diff --git a/src/lib.rs b/src/lib.rs index 0e81b65a..8ad689aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ #![allow(clippy::type_complexity)] mod app; -mod extractor; +pub mod extractor; pub mod handler; // mod info; pub mod blocking; @@ -20,7 +20,7 @@ pub use actix_http::Response as HttpResponse; pub use actix_http::{http, Error, HttpMessage, ResponseError}; pub use crate::app::App; -pub use crate::extractor::{Form, Json, Path, Query}; +pub use crate::extractor::{Form, Json, Path, PayloadConfig, Query}; pub use crate::handler::FromRequest; pub use crate::request::HttpRequest; pub use crate::resource::Resource; diff --git a/src/request.rs b/src/request.rs index 538064f1..a7c84b53 100644 --- a/src/request.rs +++ b/src/request.rs @@ -143,6 +143,7 @@ impl HttpMessage for HttpRequest { impl

FromRequest

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

) -> Self::Future { diff --git a/src/resource.rs b/src/resource.rs index ab6096c5..0bee0ecd 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -75,7 +75,7 @@ where /// } /// ``` pub fn route(mut self, route: Route

) -> Self { - self.routes.push(route); + self.routes.push(route.finish()); self } diff --git a/src/route.rs b/src/route.rs index 99117afe..578ba79e 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,11 +1,15 @@ +use std::cell::RefCell; use std::rc::Rc; -use actix_http::{http::Method, Error, Response}; +use actix_http::{http::Method, Error, Extensions, Response}; use actix_service::{NewService, Service}; use futures::{Async, Future, IntoFuture, Poll}; use crate::filter::{self, Filter}; -use crate::handler::{AsyncFactory, AsyncHandle, Extract, Factory, FromRequest, Handle}; +use crate::handler::{ + AsyncFactory, AsyncHandle, ConfigStorage, Extract, ExtractorConfig, Factory, + FromRequest, Handle, +}; use crate::responder::Responder; use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; use crate::HttpResponse; @@ -37,33 +41,50 @@ type BoxedRouteNewService = Box< pub struct Route

{ service: BoxedRouteNewService, ServiceResponse>, filters: Rc>>, + config: ConfigStorage, + config_ref: Rc>>>, } impl Route

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

{ + let config_ref = Rc::new(RefCell::new(None)); Route { - service: Box::new(RouteNewService::new(Extract::new().and_then( - Handle::new(|| HttpResponse::NotFound()).map_err(|_| panic!()), - ))), + service: Box::new(RouteNewService::new( + Extract::new(config_ref.clone()).and_then( + Handle::new(|| HttpResponse::NotFound()).map_err(|_| panic!()), + ), + )), filters: Rc::new(Vec::new()), + config: ConfigStorage::default(), + config_ref, } } + /// Create new `GET` route. pub fn get() -> Route

{ Route::new().method(Method::GET) } + /// Create new `POST` route. pub fn post() -> Route

{ Route::new().method(Method::POST) } + /// Create new `PUT` route. pub fn put() -> Route

{ Route::new().method(Method::PUT) } + /// Create new `DELETE` route. pub fn delete() -> Route

{ Route::new().method(Method::DELETE) } + + pub(crate) fn finish(self) -> Self { + *self.config_ref.borrow_mut() = self.config.storage.clone(); + self + } } impl

NewService for Route

{ @@ -260,8 +281,10 @@ impl Route

{ T: FromRequest

+ 'static, R: Responder + 'static, { + T::Config::store_default(&mut self.config); self.service = Box::new(RouteNewService::new( - Extract::new().and_then(Handle::new(handler).map_err(|_| panic!())), + Extract::new(self.config_ref.clone()) + .and_then(Handle::new(handler).map_err(|_| panic!())), )); self } @@ -305,10 +328,39 @@ impl Route

{ R::Error: Into, { self.service = Box::new(RouteNewService::new( - Extract::new().and_then(AsyncHandle::new(handler).map_err(|_| panic!())), + Extract::new(self.config_ref.clone()) + .and_then(AsyncHandle::new(handler).map_err(|_| panic!())), )); self } + + /// This method allows to add extractor configuration + /// for specific route. + /// + /// ```rust + /// use actix_web::{web, extractor, App}; + /// + /// /// extract text data from request + /// fn index(body: String) -> String { + /// format!("Body {}!", body) + /// } + /// + /// fn main() { + /// let app = App::new().resource("/index.html", |r| { + /// r.route( + /// web::get() + /// // limit size of the payload + /// .config(extractor::PayloadConfig::new(4096)) + /// // register handler + /// .to(index) + /// ) + /// }); + /// } + /// ``` + pub fn config(mut self, config: C) -> Self { + self.config.store(config); + self + } } // pub struct RouteServiceBuilder { diff --git a/src/service.rs b/src/service.rs index 99af73c1..5602a613 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::cell::{Ref, RefMut}; use std::rc::Rc; @@ -167,9 +168,18 @@ impl

std::ops::DerefMut for ServiceRequest

{ pub struct ServiceFromRequest

{ req: HttpRequest, payload: Payload

, + config: Option>, } impl

ServiceFromRequest

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

, config: Option>) -> Self { + Self { + req: req.req, + payload: req.payload, + config, + } + } + #[inline] pub fn into_request(self) -> HttpRequest { self.req @@ -180,6 +190,16 @@ impl

ServiceFromRequest

{ pub fn error_response>(self, err: E) -> ServiceResponse { ServiceResponse::new(self.req, err.into().into()) } + + /// Load extractor configuration + pub fn load_config(&self) -> Cow { + if let Some(ref ext) = self.config { + if let Some(cfg) = ext.get::() { + return Cow::Borrowed(cfg); + } + } + Cow::Owned(T::default()) + } } impl

std::ops::Deref for ServiceFromRequest

{ @@ -204,15 +224,6 @@ impl

HttpMessage for ServiceFromRequest

{ } } -impl

From> for ServiceFromRequest

{ - fn from(req: ServiceRequest

) -> Self { - Self { - req: req.req, - payload: req.payload, - } - } -} - pub struct ServiceResponse { request: HttpRequest, response: Response, diff --git a/src/state.rs b/src/state.rs index db263777..4a450245 100644 --- a/src/state.rs +++ b/src/state.rs @@ -48,6 +48,7 @@ impl Clone for State { impl FromRequest

for State { type Error = Error; type Future = FutureResult; + type Config = (); #[inline] fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { diff --git a/src/test.rs b/src/test.rs index d6caf897..4899cfe4 100644 --- a/src/test.rs +++ b/src/test.rs @@ -9,7 +9,7 @@ use actix_router::{Path, Url}; use bytes::Bytes; use crate::request::HttpRequest; -use crate::service::ServiceRequest; +use crate::service::{ServiceFromRequest, ServiceRequest}; /// Test `Request` builder /// @@ -133,7 +133,7 @@ impl TestRequest { } /// Complete request creation and generate `HttpRequest` instance - pub fn request(mut self) -> HttpRequest { + pub fn to_request(mut self) -> HttpRequest { let req = self.req.finish(); ServiceRequest::new( @@ -143,4 +143,16 @@ impl TestRequest { ) .into_request() } + + /// 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, + Rc::new(self.extensions), + ); + ServiceFromRequest::new(req, None) + } } From a8f3dec527dd3ce3d0aef1139e23a367bec5f7f4 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 01:03:28 -0800 Subject: [PATCH 017/109] use tarpaulin from cache --- .travis.yml | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 15a0b04e..da8f33be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,19 +34,9 @@ before_script: - export PATH=$PATH:~/.cargo/bin script: - - | - if [[ "$TRAVIS_RUST_VERSION" != "nightly-2019-03-02" ]]; then - cargo clean - cargo check - cargo test -- --nocapture - fi - - | - if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then - RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install -f cargo-tarpaulin - RUST_BACKTRACE=1 cargo tarpaulin --out Xml - bash <(curl -s https://codecov.io/bash) - echo "Uploaded code coverage" - fi + - cargo clean + - cargo check + - cargo test -- --nocapture # Upload docs after_success: @@ -58,3 +48,9 @@ after_success: ./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://"$GH_TOKEN"@github.com/"$TRAVIS_REPO_SLUG.git" target/doc && echo "Uploaded documentation" fi + - | + if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then + cargo tarpaulin --features="ssl" --out Xml + bash <(curl -s https://codecov.io/bash) + echo "Uploaded code coverage" + fi From f90ca868ca76f8e3d63475f3f661558f1e96330f Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 01:12:06 -0800 Subject: [PATCH 018/109] update tests --- src/extractor.rs | 9 +++------ src/filter.rs | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/extractor.rs b/src/extractor.rs index 24e4c8af..5fa9af61 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -1022,8 +1022,7 @@ mod tests { let mut rt = actix_rt::Runtime::new().unwrap(); let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11") .set_payload(Bytes::from_static(b"hello=world")) - .finish() - .into(); + .to_from(); let s = rt.block_on(Bytes::from_request(&mut req)).unwrap(); assert_eq!(s, Bytes::from_static(b"hello=world")); @@ -1034,8 +1033,7 @@ mod tests { let mut rt = actix_rt::Runtime::new().unwrap(); let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11") .set_payload(Bytes::from_static(b"hello=world")) - .finish() - .into(); + .to_from(); let s = rt.block_on(String::from_request(&mut req)).unwrap(); assert_eq!(s, "hello=world"); @@ -1050,8 +1048,7 @@ mod tests { ) .header(header::CONTENT_LENGTH, "11") .set_payload(Bytes::from_static(b"hello=world")) - .finish() - .into(); + .to_from(); let s = rt.block_on(Form::::from_request(&mut req)).unwrap(); assert_eq!(s.hello, "world"); diff --git a/src/filter.rs b/src/filter.rs index 9b49c9dd..501c60d8 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -311,7 +311,7 @@ mod tests { #[test] fn test_preds() { - let r = TestRequest::default().method(Method::TRACE).request(); + let r = TestRequest::default().method(Method::TRACE).to_request(); assert!(Not(Get()).check(&r,)); assert!(!Not(Trace()).check(&r,)); From 015364edf841d37a4a4ddd977934aa87fd77ede8 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 08:00:12 -0800 Subject: [PATCH 019/109] fix travis config --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index da8f33be..d994a80d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -50,7 +50,7 @@ after_success: fi - | if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then - cargo tarpaulin --features="ssl" --out Xml + cargo tarpaulin --out Xml bash <(curl -s https://codecov.io/bash) echo "Uploaded code coverage" fi From b81ae899f6472d58eb525715308ed9ad3f59b241 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 08:24:09 -0800 Subject: [PATCH 020/109] better naming --- .travis.yml | 1 - src/app.rs | 60 ++++++++++++++++++++++++++++------------------------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/.travis.yml b/.travis.yml index d994a80d..1d3c227a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,7 +35,6 @@ before_script: script: - cargo clean - - cargo check - cargo test -- --nocapture # Upload docs diff --git a/src/app.rs b/src/app.rs index 2a2380b2..c9c23d9c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -188,13 +188,13 @@ where > where M: NewTransform< - AppService

, + AppRouting

, Request = ServiceRequest

, Response = ServiceResponse, Error = (), InitError = (), >, - F: IntoNewTransform>, + F: IntoNewTransform>, { let fref = Rc::new(RefCell::new(None)); let endpoint = ApplyNewService::new(mw, AppEntry::new(fref.clone())); @@ -253,7 +253,7 @@ pub struct AppRouter { default: Option>>, defaults: Vec>>>>>, endpoint: T, - factory_ref: Rc>>>, + factory_ref: Rc>>>, extensions: Extensions, state: Vec>, _t: PhantomData<(P, B)>, @@ -465,7 +465,7 @@ where } // set factory - *self.factory_ref.borrow_mut() = Some(AppFactory { + *self.factory_ref.borrow_mut() = Some(AppRoutingFactory { services: Rc::new(self.services), }); @@ -478,25 +478,25 @@ where } } -pub struct AppFactory

{ +pub struct AppRoutingFactory

{ services: Rc)>>, } -impl NewService for AppFactory

{ +impl NewService for AppRoutingFactory

{ type Request = ServiceRequest

; type Response = ServiceResponse; type Error = (); type InitError = (); - type Service = AppService

; - type Future = CreateAppService

; + type Service = AppRouting

; + type Future = AppRoutingFactoryResponse

; fn new_service(&self, _: &()) -> Self::Future { - CreateAppService { + AppRoutingFactoryResponse { fut: self .services .iter() .map(|(path, service)| { - CreateAppServiceItem::Future( + CreateAppRoutingItem::Future( Some(path.clone()), service.new_service(&()), ) @@ -510,17 +510,17 @@ type HttpServiceFut

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

{ - fut: Vec>, +pub struct AppRoutingFactoryResponse

{ + fut: Vec>, } -enum CreateAppServiceItem

{ +enum CreateAppRoutingItem

{ Future(Option, HttpServiceFut

), Service(ResourceDef, HttpService

), } -impl

Future for CreateAppService

{ - type Item = AppService

; +impl

Future for AppRoutingFactoryResponse

{ + type Item = AppRouting

; type Error = (); fn poll(&mut self) -> Poll { @@ -529,7 +529,7 @@ impl

Future for CreateAppService

{ // poll http services for item in &mut self.fut { let res = match item { - CreateAppServiceItem::Future(ref mut path, ref mut fut) => { + CreateAppRoutingItem::Future(ref mut path, ref mut fut) => { match fut.poll()? { Async::Ready(service) => Some((path.take().unwrap(), service)), Async::NotReady => { @@ -538,11 +538,11 @@ impl

Future for CreateAppService

{ } } } - CreateAppServiceItem::Service(_, _) => continue, + CreateAppRoutingItem::Service(_, _) => continue, }; if let Some((path, service)) = res { - *item = CreateAppServiceItem::Service(path, service); + *item = CreateAppRoutingItem::Service(path, service); } } @@ -552,14 +552,14 @@ impl

Future for CreateAppService

{ .drain(..) .fold(Router::build(), |mut router, item| { match item { - CreateAppServiceItem::Service(path, service) => { + CreateAppRoutingItem::Service(path, service) => { router.rdef(path, service) } - CreateAppServiceItem::Future(_, _) => unreachable!(), + CreateAppRoutingItem::Future(_, _) => unreachable!(), } router }); - Ok(Async::Ready(AppService { + Ok(Async::Ready(AppRouting { router: router.finish(), ready: None, })) @@ -569,12 +569,12 @@ impl

Future for CreateAppService

{ } } -pub struct AppService

{ +pub struct AppRouting

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

, ResourceInfo)>, } -impl

Service for AppService

{ +impl

Service for AppRouting

{ type Request = ServiceRequest

; type Response = ServiceResponse; type Error = (); @@ -599,12 +599,13 @@ impl

Service for AppService

{ } #[doc(hidden)] +/// Wrapper service for routing pub struct AppEntry

{ - factory: Rc>>>, + factory: Rc>>>, } impl

AppEntry

{ - fn new(factory: Rc>>>) -> Self { + fn new(factory: Rc>>>) -> Self { AppEntry { factory } } } @@ -614,8 +615,8 @@ impl NewService for AppEntry

{ type Response = ServiceResponse; type Error = (); type InitError = (); - type Service = AppService

; - type Future = CreateAppService

; + type Service = AppRouting

; + type Future = AppRoutingFactoryResponse

; fn new_service(&self, _: &()) -> Self::Future { self.factory.borrow_mut().as_mut().unwrap().new_service(&()) @@ -644,16 +645,19 @@ impl Service for AppChain { type Error = (); type Future = FutureResult; + #[inline] fn poll_ready(&mut self) -> Poll<(), Self::Error> { Ok(Async::Ready(())) } + #[inline] fn call(&mut self, req: Self::Request) -> Self::Future { ok(req) } } -/// Service factory to convert `Request` to a `ServiceRequest` +/// Service factory to convert `Request` to a `ServiceRequest`. +/// It also executes state factories. pub struct AppInit where C: NewService, Response = ServiceRequest

>, From 237677be15d00b6074b18eb214790ccbd21eb349 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 12:09:38 -0800 Subject: [PATCH 021/109] rename filter to guard --- src/{filter.rs => guard.rs} | 139 +++++++++++---------- src/lib.rs | 2 +- src/resource.rs | 51 ++++---- src/route.rs | 243 ++++++++---------------------------- 4 files changed, 145 insertions(+), 290 deletions(-) rename src/{filter.rs => guard.rs} (67%) diff --git a/src/filter.rs b/src/guard.rs similarity index 67% rename from src/filter.rs rename to src/guard.rs index 501c60d8..10a56921 100644 --- a/src/filter.rs +++ b/src/guard.rs @@ -1,47 +1,48 @@ -//! Route match predicates +//! Route match guards. #![allow(non_snake_case)] use actix_http::http::{self, header, HttpTryFrom}; use actix_http::RequestHead; -/// Trait defines resource predicate. -/// Predicate can modify request object. It is also possible to +/// Trait defines resource guards. Guards are used for routes selection. +/// +/// Guard can not modify request object. But it is possible to /// to store extra attributes on request by using `Extensions` container, -/// Extensions container available via `HttpRequest::extensions()` method. -pub trait Filter { +/// Extensions container available via `RequestHead::extensions()` method. +pub trait Guard { /// Check if request matches predicate fn check(&self, request: &RequestHead) -> bool; } -/// Return filter that matches if any of supplied filters. +/// Return guard that matches if any of supplied guards. /// /// ```rust -/// use actix_web::{web, filter, App, HttpResponse}; +/// use actix_web::{web, guard, App, HttpResponse}; /// /// fn main() { /// App::new().resource("/index.html", |r| /// r.route( /// web::route() -/// .filter(filter::Any(filter::Get()).or(filter::Post())) +/// .guard(guard::Any(guard::Get()).or(guard::Post())) /// .to(|| HttpResponse::MethodNotAllowed())) /// ); /// } /// ``` -pub fn Any(filter: F) -> AnyFilter { - AnyFilter(vec![Box::new(filter)]) +pub fn Any(guard: F) -> AnyGuard { + AnyGuard(vec![Box::new(guard)]) } -/// Matches if any of supplied filters matche. -pub struct AnyFilter(Vec>); +/// Matches if any of supplied guards matche. +pub struct AnyGuard(Vec>); -impl AnyFilter { - /// Add filter to a list of filters to check - pub fn or(mut self, filter: F) -> Self { - self.0.push(Box::new(filter)); +impl AnyGuard { + /// Add guard to a list of guards to check + pub fn or(mut self, guard: F) -> Self { + self.0.push(Box::new(guard)); self } } -impl Filter for AnyFilter { +impl Guard for AnyGuard { fn check(&self, req: &RequestHead) -> bool { for p in &self.0 { if p.check(req) { @@ -52,37 +53,37 @@ impl Filter for AnyFilter { } } -/// Return filter that matches if all of supplied filters match. +/// Return guard that matches if all of the supplied guards. /// /// ```rust /// # extern crate actix_web; -/// use actix_web::{filter, web, App, HttpResponse}; +/// use actix_web::{guard, web, App, HttpResponse}; /// /// fn main() { /// App::new().resource("/index.html", |r| { /// r.route(web::route() -/// .filter( -/// filter::All(filter::Get()).and(filter::Header("content-type", "text/plain"))) +/// .guard( +/// guard::All(guard::Get()).and(guard::Header("content-type", "text/plain"))) /// .to(|| HttpResponse::MethodNotAllowed())) /// }); /// } /// ``` -pub fn All(filter: F) -> AllFilter { - AllFilter(vec![Box::new(filter)]) +pub fn All(guard: F) -> AllGuard { + AllGuard(vec![Box::new(guard)]) } -/// Matches if all of supplied filters matche. -pub struct AllFilter(Vec>); +/// Matches if all of supplied guards. +pub struct AllGuard(Vec>); -impl AllFilter { - /// Add new predicate to list of predicates to check - pub fn and(mut self, filter: F) -> Self { - self.0.push(Box::new(filter)); +impl AllGuard { + /// Add new guard to the list of guards to check + pub fn and(mut self, guard: F) -> Self { + self.0.push(Box::new(guard)); self } } -impl Filter for AllFilter { +impl Guard for AllGuard { fn check(&self, request: &RequestHead) -> bool { for p in &self.0 { if !p.check(request) { @@ -93,93 +94,93 @@ impl Filter for AllFilter { } } -/// Return predicate that matches if supplied predicate does not match. -pub fn Not(filter: F) -> NotFilter { - NotFilter(Box::new(filter)) +/// Return guard that matches if supplied guard does not match. +pub fn Not(guard: F) -> NotGuard { + NotGuard(Box::new(guard)) } #[doc(hidden)] -pub struct NotFilter(Box); +pub struct NotGuard(Box); -impl Filter for NotFilter { +impl Guard for NotGuard { fn check(&self, request: &RequestHead) -> bool { !self.0.check(request) } } -/// Http method predicate +/// Http method guard #[doc(hidden)] -pub struct MethodFilter(http::Method); +pub struct MethodGuard(http::Method); -impl Filter for MethodFilter { +impl Guard for MethodGuard { fn check(&self, request: &RequestHead) -> bool { request.method == self.0 } } -/// Predicate to match *GET* http method -pub fn Get() -> MethodFilter { - MethodFilter(http::Method::GET) +/// Guard to match *GET* http method +pub fn Get() -> MethodGuard { + MethodGuard(http::Method::GET) } /// Predicate to match *POST* http method -pub fn Post() -> MethodFilter { - MethodFilter(http::Method::POST) +pub fn Post() -> MethodGuard { + MethodGuard(http::Method::POST) } /// Predicate to match *PUT* http method -pub fn Put() -> MethodFilter { - MethodFilter(http::Method::PUT) +pub fn Put() -> MethodGuard { + MethodGuard(http::Method::PUT) } /// Predicate to match *DELETE* http method -pub fn Delete() -> MethodFilter { - MethodFilter(http::Method::DELETE) +pub fn Delete() -> MethodGuard { + MethodGuard(http::Method::DELETE) } /// Predicate to match *HEAD* http method -pub fn Head() -> MethodFilter { - MethodFilter(http::Method::HEAD) +pub fn Head() -> MethodGuard { + MethodGuard(http::Method::HEAD) } /// Predicate to match *OPTIONS* http method -pub fn Options() -> MethodFilter { - MethodFilter(http::Method::OPTIONS) +pub fn Options() -> MethodGuard { + MethodGuard(http::Method::OPTIONS) } /// Predicate to match *CONNECT* http method -pub fn Connect() -> MethodFilter { - MethodFilter(http::Method::CONNECT) +pub fn Connect() -> MethodGuard { + MethodGuard(http::Method::CONNECT) } /// Predicate to match *PATCH* http method -pub fn Patch() -> MethodFilter { - MethodFilter(http::Method::PATCH) +pub fn Patch() -> MethodGuard { + MethodGuard(http::Method::PATCH) } /// Predicate to match *TRACE* http method -pub fn Trace() -> MethodFilter { - MethodFilter(http::Method::TRACE) +pub fn Trace() -> MethodGuard { + MethodGuard(http::Method::TRACE) } /// Predicate to match specified http method -pub fn Method(method: http::Method) -> MethodFilter { - MethodFilter(method) +pub fn Method(method: http::Method) -> MethodGuard { + MethodGuard(method) } /// Return predicate that matches if request contains specified header and /// value. -pub fn Header(name: &'static str, value: &'static str) -> HeaderFilter { - HeaderFilter( +pub fn Header(name: &'static str, value: &'static str) -> HeaderGuard { + HeaderGuard( header::HeaderName::try_from(name).unwrap(), header::HeaderValue::from_static(value), ) } #[doc(hidden)] -pub struct HeaderFilter(header::HeaderName, header::HeaderValue); +pub struct HeaderGuard(header::HeaderName, header::HeaderValue); -impl Filter for HeaderFilter { +impl Guard for HeaderGuard { fn check(&self, req: &RequestHead) -> bool { if let Some(val) = req.headers.get(&self.0) { return val == self.1; @@ -197,26 +198,26 @@ impl Filter for HeaderFilter { // /// fn main() { // /// App::new().resource("/index.html", |r| { // /// r.route() -// /// .filter(pred::Host("www.rust-lang.org")) +// /// .guard(pred::Host("www.rust-lang.org")) // /// .f(|_| HttpResponse::MethodNotAllowed()) // /// }); // /// } // /// ``` -// pub fn Host>(host: H) -> HostFilter { -// HostFilter(host.as_ref().to_string(), None) +// pub fn Host>(host: H) -> HostGuard { +// HostGuard(host.as_ref().to_string(), None) // } // #[doc(hidden)] -// pub struct HostFilter(String, Option); +// pub struct HostGuard(String, Option); -// impl HostFilter { +// impl HostGuard { // /// Set reuest scheme to match // pub fn scheme>(&mut self, scheme: H) { // self.1 = Some(scheme.as_ref().to_string()) // } // } -// impl Filter for HostFilter { +// impl Guard for HostGuard { // fn check(&self, _req: &RequestHead) -> bool { // // let info = req.connection_info(); // // if let Some(ref scheme) = self.1 { diff --git a/src/lib.rs b/src/lib.rs index 8ad689aa..e876a7ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ pub mod extractor; pub mod handler; // mod info; pub mod blocking; -pub mod filter; +pub mod guard; pub mod middleware; mod request; mod resource; diff --git a/src/resource.rs b/src/resource.rs index 0bee0ecd..98c2dc11 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -56,22 +56,21 @@ where InitError = (), >, { - /// Register a new route and return mutable reference to *Route* object. - /// *Route* is used for route configuration, i.e. adding predicates, + /// Register a new route. + /// *Route* is used for route configuration, i.e. adding guards, /// setting up handler. /// - /// ```rust,ignore - /// use actix_web::*; + /// ```rust + /// use actix_web::{web, guard, App, HttpResponse}; /// /// fn main() { /// let app = App::new() /// .resource("/", |r| { - /// r.route() - /// .filter(pred::Any(pred::Get()).or(pred::Put())) - /// .filter(pred::Header("Content-Type", "text/plain")) - /// .f(|r| HttpResponse::Ok()) - /// }) - /// .finish(); + /// r.route(web::route() + /// .guard(guard::Any(guard::Get()).or(guard::Put())) + /// .guard(guard::Header("Content-Type", "text/plain")) + /// .to(|| HttpResponse::Ok())) + /// }); /// } /// ``` pub fn route(mut self, route: Route

) -> Self { @@ -81,21 +80,23 @@ where /// Register a new route and add handler. /// - /// ```rust,ignore - /// # extern crate actix_web; + /// ```rust /// use actix_web::*; - /// fn index(req: HttpRequest) -> HttpResponse { unimplemented!() } /// - /// App::new().resource("/", |r| r.with(index)); + /// fn index(req: HttpRequest) -> HttpResponse { + /// unimplemented!() + /// } + /// + /// App::new().resource("/", |r| r.to(index)); /// ``` /// /// This is shortcut for: /// - /// ```rust,ignore + /// ```rust /// # extern crate actix_web; /// # use actix_web::*; /// # fn index(req: HttpRequest) -> HttpResponse { unimplemented!() } - /// App::new().resource("/", |r| r.route().with(index)); + /// App::new().resource("/", |r| r.route(web::route().to(index))); /// ``` pub fn to(mut self, handler: F) -> Self where @@ -109,30 +110,26 @@ where /// Register a new route and add async handler. /// - /// ```rust,ignore - /// # extern crate actix_web; - /// # extern crate futures; + /// ```rust /// use actix_web::*; - /// use futures::future::Future; + /// use futures::future::{ok, Future}; /// - /// fn index(req: HttpRequest) -> Box> { - /// unimplemented!() + /// fn index(req: HttpRequest) -> impl Future { + /// ok(HttpResponse::Ok().finish()) /// } /// - /// App::new().resource("/", |r| r.with_async(index)); + /// App::new().resource("/", |r| r.to_async(index)); /// ``` /// /// This is shortcut for: /// - /// ```rust,ignore - /// # extern crate actix_web; - /// # extern crate futures; + /// ```rust /// # use actix_web::*; /// # use futures::future::Future; /// # fn index(req: HttpRequest) -> Box> { /// # unimplemented!() /// # } - /// App::new().resource("/", |r| r.route().with_async(index)); + /// App::new().resource("/", |r| r.route(web::route().to_async(index))); /// ``` #[allow(clippy::wrong_self_convention)] pub fn to_async(mut self, handler: F) -> Self diff --git a/src/route.rs b/src/route.rs index 578ba79e..16a4fc5b 100644 --- a/src/route.rs +++ b/src/route.rs @@ -5,7 +5,7 @@ use actix_http::{http::Method, Error, Extensions, Response}; use actix_service::{NewService, Service}; use futures::{Async, Future, IntoFuture, Poll}; -use crate::filter::{self, Filter}; +use crate::guard::{self, Guard}; use crate::handler::{ AsyncFactory, AsyncHandle, ConfigStorage, Extract, ExtractorConfig, Factory, FromRequest, Handle, @@ -40,7 +40,7 @@ type BoxedRouteNewService = Box< /// If handler is not explicitly set, default *404 Not Found* handler is used. pub struct Route

{ service: BoxedRouteNewService, ServiceResponse>, - filters: Rc>>, + guards: Rc>>, config: ConfigStorage, config_ref: Rc>>>, } @@ -55,7 +55,7 @@ impl Route

{ Handle::new(|| HttpResponse::NotFound()).map_err(|_| panic!()), ), )), - filters: Rc::new(Vec::new()), + guards: Rc::new(Vec::new()), config: ConfigStorage::default(), config_ref, } @@ -98,7 +98,7 @@ impl

NewService for Route

{ fn new_service(&self, _: &()) -> Self::Future { CreateRouteService { fut: self.service.new_service(&()), - filters: self.filters.clone(), + guards: self.guards.clone(), } } } @@ -109,7 +109,7 @@ type RouteFuture

= Box< pub struct CreateRouteService

{ fut: RouteFuture

, - filters: Rc>>, + guards: Rc>>, } impl

Future for CreateRouteService

{ @@ -120,7 +120,7 @@ impl

Future for CreateRouteService

{ match self.fut.poll()? { Async::Ready(service) => Ok(Async::Ready(RouteService { service, - filters: self.filters.clone(), + guards: self.guards.clone(), })), Async::NotReady => Ok(Async::NotReady), } @@ -129,12 +129,12 @@ impl

Future for CreateRouteService

{ pub struct RouteService

{ service: BoxedRouteService, ServiceResponse>, - filters: Rc>>, + guards: Rc>>, } impl

RouteService

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

) -> bool { - for f in self.filters.iter() { + for f in self.guards.iter() { if !f.check(req.head()) { return false; } @@ -159,45 +159,41 @@ impl

Service for RouteService

{ } impl Route

{ - /// Add method match filter to the route. + /// Add method guard to the route. /// - /// ```rust,ignore - /// # extern crate actix_web; + /// ```rust /// # use actix_web::*; /// # fn main() { /// App::new().resource("/path", |r| { - /// r.route() - /// .filter(pred::Get()) - /// .filter(pred::Header("content-type", "text/plain")) - /// .f(|req| HttpResponse::Ok()) - /// }) - /// # .finish(); + /// r.route(web::get() + /// .guard(guard::Get()) + /// .guard(guard::Header("content-type", "text/plain")) + /// .to(|req: HttpRequest| HttpResponse::Ok())) + /// }); /// # } /// ``` pub fn method(mut self, method: Method) -> Self { - Rc::get_mut(&mut self.filters) + Rc::get_mut(&mut self.guards) .unwrap() - .push(Box::new(filter::Method(method))); + .push(Box::new(guard::Method(method))); self } - /// Add filter to the route. + /// Add guard to the route. /// - /// ```rust,ignore - /// # extern crate actix_web; + /// ```rust /// # use actix_web::*; /// # fn main() { /// App::new().resource("/path", |r| { - /// r.route() - /// .filter(pred::Get()) - /// .filter(pred::Header("content-type", "text/plain")) - /// .f(|req| HttpResponse::Ok()) - /// }) - /// # .finish(); + /// r.route(web::route() + /// .guard(guard::Get()) + /// .guard(guard::Header("content-type", "text/plain")) + /// .to(|req: HttpRequest| HttpResponse::Ok())) + /// }); /// # } /// ``` - pub fn filter(mut self, f: F) -> Self { - Rc::get_mut(&mut self.filters).unwrap().push(Box::new(f)); + pub fn guard(mut self, f: F) -> Self { + Rc::get_mut(&mut self.guards).unwrap().push(Box::new(f)); self } @@ -214,19 +210,16 @@ impl Route

{ // { // RouteServiceBuilder { // service: md.into_new_service(), - // filters: self.filters, + // guards: self.guards, // _t: PhantomData, // } // } - /// Set handler function, use request extractor for parameters. + /// Set handler function, use request extractors for parameters. /// - /// ```rust,ignore - /// # extern crate bytes; - /// # extern crate actix_web; - /// # extern crate futures; + /// ```rust /// #[macro_use] extern crate serde_derive; - /// use actix_web::{http, App, Path, Result}; + /// use actix_web::{web, http, App, Path}; /// /// #[derive(Deserialize)] /// struct Info { @@ -234,27 +227,24 @@ impl Route

{ /// } /// /// /// extract path info using serde - /// fn index(info: Path) -> Result { - /// Ok(format!("Welcome {}!", info.username)) + /// fn index(info: Path) -> String { + /// format!("Welcome {}!", info.username) /// } /// /// fn main() { /// let app = App::new().resource( /// "/{username}/index.html", // <- define path parameters - /// |r| r.method(http::Method::GET).with(index), - /// ); // <- use `with` extractor + /// |r| r.route(web::get().to(index)), // <- register handler + /// ); /// } /// ``` /// /// It is possible to use multiple extractors for one handler function. /// - /// ```rust,ignore - /// # extern crate bytes; - /// # extern crate actix_web; - /// # extern crate futures; - /// #[macro_use] extern crate serde_derive; + /// ```rust /// # use std::collections::HashMap; - /// use actix_web::{http, App, Json, Path, Query, Result}; + /// # use serde_derive::Deserialize; + /// use actix_web::{web, http, App, Json, Path, Query}; /// /// #[derive(Deserialize)] /// struct Info { @@ -262,17 +252,15 @@ impl Route

{ /// } /// /// /// extract path info using serde - /// fn index( - /// path: Path, query: Query>, body: Json, - /// ) -> Result { - /// Ok(format!("Welcome {}!", path.username)) + /// fn index(path: Path, query: Query>, body: Json) -> String { + /// format!("Welcome {}!", path.username) /// } /// /// fn main() { /// let app = App::new().resource( /// "/{username}/index.html", // <- define path parameters - /// |r| r.method(http::Method::GET).with(index), - /// ); // <- use `with` extractor + /// |r| r.route(web::method(http::Method::GET).to(index)), + /// ); /// } /// ``` pub fn to(mut self, handler: F) -> Route

@@ -289,16 +277,13 @@ impl Route

{ self } - /// Set async handler function, use request extractor for parameters. - /// Also this method needs to be used if your handler function returns - /// `impl Future<>` + /// Set async handler function, use request extractors for parameters. + /// This method has to be used if your handler function returns `impl Future<>` /// - /// ```rust,ignore - /// # extern crate bytes; - /// # extern crate actix_web; - /// # extern crate futures; + /// ```rust + /// # use futures::future::ok; /// #[macro_use] extern crate serde_derive; - /// use actix_web::{http, App, Error, Path}; + /// use actix_web::{web, http, App, Error, Path}; /// use futures::Future; /// /// #[derive(Deserialize)] @@ -307,15 +292,15 @@ impl Route

{ /// } /// /// /// extract path info using serde - /// fn index(info: Path) -> Box> { - /// unimplemented!() + /// fn index(info: Path) -> impl Future { + /// ok("Hello World!") /// } /// /// fn main() { /// let app = App::new().resource( /// "/{username}/index.html", // <- define path parameters - /// |r| r.method(http::Method::GET).with_async(index), - /// ); // <- use `with` extractor + /// |r| r.route(web::get().to_async(index)), // <- register async handler + /// ); /// } /// ``` #[allow(clippy::wrong_self_convention)] @@ -363,134 +348,6 @@ impl Route

{ } } -// pub struct RouteServiceBuilder { -// service: T, -// filters: Vec>, -// _t: PhantomData<(P, U1, U2)>, -// } - -// impl RouteServiceBuilder -// where -// T: NewService< -// Request = HandlerRequest, -// Response = HandlerRequest, -// Error = Error, -// InitError = (), -// >, -// { -// pub fn new>(factory: F) -> Self { -// RouteServiceBuilder { -// service: factory.into_new_service(), -// filters: Vec::new(), -// _t: PhantomData, -// } -// } - -// /// Add method match filter to the route. -// /// -// /// ```rust -// /// # extern crate actix_web; -// /// # use actix_web::*; -// /// # fn main() { -// /// App::new().resource("/path", |r| { -// /// r.route() -// /// .filter(pred::Get()) -// /// .filter(pred::Header("content-type", "text/plain")) -// /// .f(|req| HttpResponse::Ok()) -// /// }) -// /// # .finish(); -// /// # } -// /// ``` -// pub fn method(mut self, method: Method) -> Self { -// self.filters.push(Box::new(filter::Method(method))); -// self -// } - -// /// Add filter to the route. -// /// -// /// ```rust -// /// # extern crate actix_web; -// /// # use actix_web::*; -// /// # fn main() { -// /// App::new().resource("/path", |r| { -// /// r.route() -// /// .filter(pred::Get()) -// /// .filter(pred::Header("content-type", "text/plain")) -// /// .f(|req| HttpResponse::Ok()) -// /// }) -// /// # .finish(); -// /// # } -// /// ``` -// pub fn filter + 'static>(&mut self, f: F) -> &mut Self { -// self.filters.push(Box::new(f)); -// self -// } - -// pub fn map>( -// self, -// md: F, -// ) -> RouteServiceBuilder< -// impl NewService< -// Request = HandlerRequest, -// Response = HandlerRequest, -// Error = Error, -// InitError = (), -// >, -// S, -// U1, -// U2, -// > -// where -// T1: NewService< -// Request = HandlerRequest, -// Response = HandlerRequest, -// InitError = (), -// >, -// T1::Error: Into, -// { -// RouteServiceBuilder { -// service: self -// .service -// .and_then(md.into_new_service().map_err(|e| e.into())), -// filters: self.filters, -// _t: PhantomData, -// } -// } - -// pub fn to_async(self, handler: F) -> Route -// where -// F: AsyncFactory, -// P: FromRequest + 'static, -// R: IntoFuture, -// R::Item: Into, -// R::Error: Into, -// { -// Route { -// service: self -// .service -// .and_then(Extract::new(P::Config::default())) -// .then(AsyncHandle::new(handler)), -// filters: Rc::new(self.filters), -// } -// } - -// pub fn to(self, handler: F) -> Route -// where -// F: Factory + 'static, -// P: FromRequest + 'static, -// R: Responder + 'static, -// { -// Route { -// service: Box::new(RouteNewService::new( -// self.service -// .and_then(Extract::new(P::Config::default())) -// .and_then(Handle::new(handler)), -// )), -// filters: Rc::new(self.filters), -// } -// } -// } - struct RouteNewService where T: NewService, Error = (Error, ServiceFromRequest

)>, From e50d4c5e0e7bcaaf50031ff1aaebc222910e3517 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 13:53:31 -0800 Subject: [PATCH 022/109] rename extractor module to extract, re-enable doc tests --- Cargo.toml | 1 - src/{extractor.rs => extract.rs} | 279 +++++++++++++++++-------------- src/handler.rs | 54 +----- src/lib.rs | 18 +- src/request.rs | 2 +- src/resource.rs | 3 +- src/responder.rs | 61 ++++--- src/route.rs | 18 +- src/state.rs | 2 +- src/test.rs | 5 +- 10 files changed, 215 insertions(+), 228 deletions(-) rename src/{extractor.rs => extract.rs} (83%) diff --git a/Cargo.toml b/Cargo.toml index 30d23d02..03b1794f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,6 @@ actix-router = { git = "https://github.com/actix/actix-net.git" } bytes = "0.4" derive_more = "0.14" -either = "1.5.1" encoding = "0.2" futures = "0.1" log = "0.4" diff --git a/src/extractor.rs b/src/extract.rs similarity index 83% rename from src/extractor.rs rename to src/extract.rs index 5fa9af61..d6e53327 100644 --- a/src/extractor.rs +++ b/src/extract.rs @@ -20,65 +20,103 @@ use actix_http::error::{ UrlencodedError, }; use actix_http::http::StatusCode; -use actix_http::{HttpMessage, Response}; +use actix_http::{Extensions, HttpMessage, Response}; use actix_router::PathDeserializer; -use crate::handler::{ConfigStorage, ExtractorConfig, FromRequest}; use crate::request::HttpRequest; use crate::responder::Responder; use crate::service::ServiceFromRequest; +/// 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 { + /// The associated error which can be returned. + type Error: Into; + + /// Future that resolves to a Self + type Future: IntoFuture; + + /// Configuration for the extractor + type Config: ExtractorConfig; + + /// Convert request to a Self + fn from_request(req: &mut ServiceFromRequest

) -> Self::Future; +} + +/// Storage for extractor configs +#[derive(Default)] +pub struct ConfigStorage { + pub(crate) storage: Option>, +} + +impl ConfigStorage { + pub fn store(&mut self, config: C) { + if self.storage.is_none() { + self.storage = Some(Rc::new(Extensions::new())); + } + if let Some(ref mut ext) = self.storage { + Rc::get_mut(ext).unwrap().insert(config); + } + } +} + +pub trait ExtractorConfig: Default + Clone + 'static { + /// Set default configuration to config storage + fn store_default(ext: &mut ConfigStorage) { + ext.store(Self::default()) + } +} + +impl ExtractorConfig for () { + fn store_default(_: &mut ConfigStorage) {} +} + #[derive(PartialEq, Eq, PartialOrd, Ord)] /// Extract typed information from the request's path. /// /// ## Example /// -/// ```rust,ignore -/// # extern crate bytes; -/// # extern crate actix_web; -/// # extern crate futures; -/// use actix_web::{http, App, Path, Result}; +/// ```rust +/// use actix_web::{web, http, App, extract::Path}; /// /// /// extract path info from "/{username}/{count}/index.html" url /// /// {username} - deserializes to a String /// /// {count} - - deserializes to a u32 -/// fn index(info: Path<(String, u32)>) -> Result { -/// Ok(format!("Welcome {}! {}", info.0, info.1)) +/// fn index(info: Path<(String, u32)>) -> String { +/// format!("Welcome {}! {}", info.0, info.1) /// } /// /// fn main() { /// let app = App::new().resource( /// "/{username}/{count}/index.html", // <- define path parameters -/// |r| r.method(http::Method::GET).with(index), -/// ); // <- use `with` extractor +/// |r| r.route(web::get().to(index)) // <- register handler with `Path` extractor +/// ); /// } /// ``` /// /// It is possible to extract path information to a specific type that /// implements `Deserialize` trait from *serde*. /// -/// ```rust,ignore -/// # extern crate bytes; -/// # extern crate actix_web; -/// # extern crate futures; +/// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{http, App, Path, Result}; +/// use actix_web::{web, App, extract::Path, Error}; /// /// #[derive(Deserialize)] /// struct Info { /// username: String, /// } /// -/// /// extract path info using serde -/// fn index(info: Path) -> Result { +/// /// extract `Info` from a path using serde +/// fn index(info: Path) -> Result { /// Ok(format!("Welcome {}!", info.username)) /// } /// /// fn main() { /// let app = App::new().resource( /// "/{username}/index.html", // <- define path parameters -/// |r| r.method(http::Method::GET).with(index), -/// ); // <- use `with` extractor +/// |r| r.route(web::get().to(index)) // <- use handler with Path` extractor +/// ); /// } /// ``` pub struct Path { @@ -112,7 +150,7 @@ impl Path { } /// Extract path information from a request - pub fn extract

(req: &ServiceFromRequest

) -> Result, de::value::Error> + pub fn extract(req: &HttpRequest) -> Result, de::value::Error> where T: DeserializeOwned, { @@ -158,13 +196,9 @@ impl fmt::Display for Path { /// /// ## Example /// -/// ```rust,ignore -/// # extern crate bytes; -/// # extern crate actix_web; -/// # extern crate futures; +/// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{App, Query, http}; -/// +/// use actix_web::{web, extract, App}; /// ///#[derive(Debug, Deserialize)] ///pub enum ResponseType { @@ -178,17 +212,17 @@ impl fmt::Display for Path { /// response_type: ResponseType, ///} /// -/// // use `with` extractor for query info -/// // this handler get called only if request's query contains `username` field +/// // Use `Query` extractor for query information. +/// // This handler get called only if request's query contains `username` field /// // The correct request for this handler would be `/index.html?id=64&response_type=Code"` -/// fn index(info: Query) -> String { +/// fn index(info: extract::Query) -> String { /// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) /// } /// /// fn main() { /// let app = App::new().resource( /// "/index.html", -/// |r| r.method(http::Method::GET).with(index)); // <- use `with` extractor +/// |r| r.route(web::get().to(index))); // <- use `Query` extractor /// } /// ``` pub struct Query(T); @@ -253,21 +287,21 @@ impl fmt::Display for Query { /// /// ## Example /// -/// ```rust,ignore +/// ```rust /// # extern crate actix_web; /// #[macro_use] extern crate serde_derive; -/// use actix_web::{App, Form, Result}; +/// use actix_web::{web, App, extract::Form}; /// /// #[derive(Deserialize)] /// struct FormData { /// username: String, /// } /// -/// /// extract form data using serde -/// /// this handler get called only if content type is *x-www-form-urlencoded* +/// /// Extract form data using serde. +/// /// This handler get called only if content type is *x-www-form-urlencoded* /// /// and content of the request could be deserialized to a `FormData` struct -/// fn index(form: Form) -> Result { -/// Ok(format!("Welcome {}!", form.username)) +/// fn index(form: Form) -> String { +/// format!("Welcome {}!", form.username) /// } /// # fn main() {} /// ``` @@ -333,19 +367,18 @@ impl fmt::Display for Form { /// Form extractor configuration /// -/// ```rust,ignore -/// # extern crate actix_web; +/// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{http, App, Form, Result}; +/// use actix_web::{web, extract, App, Result}; /// /// #[derive(Deserialize)] /// struct FormData { /// username: String, /// } /// -/// /// extract form data using serde. -/// /// custom configuration is used for this handler, max payload size is 4k -/// fn index(form: Form) -> Result { +/// /// Extract form data using serde. +/// /// Custom configuration is used for this handler, max payload size is 4k +/// fn index(form: extract::Form) -> Result { /// Ok(format!("Welcome {}!", form.username)) /// } /// @@ -353,11 +386,11 @@ impl fmt::Display for Form { /// let app = App::new().resource( /// "/index.html", /// |r| { -/// r.method(http::Method::GET) -/// // register form handler and change form extractor configuration -/// .with_config(index, |cfg| {cfg.0.limit(4096);}) -/// }, -/// ); +/// r.route(web::get() +/// // change `Form` extractor configuration +/// .config(extract::FormConfig::default().limit(4097)) +/// .to(index)) +/// }); /// } /// ``` #[derive(Clone)] @@ -408,10 +441,9 @@ impl Default for FormConfig { /// /// ## Example /// -/// ```rust,ignore -/// # extern crate actix_web; +/// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{App, Json, Result, http}; +/// use actix_web::{web, extract, App}; /// /// #[derive(Deserialize)] /// struct Info { @@ -419,14 +451,14 @@ impl Default for FormConfig { /// } /// /// /// deserialize `Info` from request's body -/// fn index(info: Json) -> Result { -/// Ok(format!("Welcome {}!", info.username)) +/// fn index(info: extract::Json) -> String { +/// format!("Welcome {}!", info.username) /// } /// /// fn main() { /// let app = App::new().resource( /// "/index.html", -/// |r| r.method(http::Method::POST).with(index)); // <- use `with` extractor +/// |r| r.route(web::post().to(index))); /// } /// ``` /// @@ -435,8 +467,7 @@ impl Default for FormConfig { /// to serialize into *JSON*. The type `T` must implement the `Serialize` /// trait from *serde*. /// -/// ```rust,ignore -/// # extern crate actix_web; +/// ```rust /// # #[macro_use] extern crate serde_derive; /// # use actix_web::*; /// # @@ -447,7 +478,7 @@ impl Default for FormConfig { /// /// fn index(req: HttpRequest) -> Result> { /// Ok(Json(MyObj { -/// name: req.match_info().query("name")?, +/// name: req.match_info().get("name").unwrap().to_string(), /// })) /// } /// # fn main() {} @@ -536,10 +567,9 @@ where /// Json extractor configuration /// -/// ```rust,ignore -/// # extern crate actix_web; +/// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{error, http, App, HttpResponse, Json, Result}; +/// use actix_web::{error, extract, web, App, HttpResponse, Json}; /// /// #[derive(Deserialize)] /// struct Info { @@ -547,20 +577,20 @@ where /// } /// /// /// deserialize `Info` from request's body, max payload size is 4kb -/// fn index(info: Json) -> Result { -/// Ok(format!("Welcome {}!", info.username)) +/// fn index(info: Json) -> String { +/// format!("Welcome {}!", info.username) /// } /// /// fn main() { /// let app = App::new().resource("/index.html", |r| { -/// r.method(http::Method::POST) -/// .with_config(index, |cfg| { -/// cfg.0.limit(4096) // <- change json extractor configuration -/// .error_handler(|err, req| { // <- create custom error response -/// error::InternalError::from_response( -/// err, HttpResponse::Conflict().finish()).into() -/// }); -/// }) +/// r.route(web::post().config( +/// // change json extractor configuration +/// extract::JsonConfig::default().limit(4096) +/// .error_handler(|err, req| { // <- create custom error response +/// error::InternalError::from_response( +/// err, HttpResponse::Conflict().finish()).into() +/// })) +/// .to(index)) /// }); /// } /// ``` @@ -598,7 +628,7 @@ impl Default for JsonConfig { } } -/// Request payload extractor. +/// Request binary data from a request's payload. /// /// Loads request's payload and construct Bytes instance. /// @@ -607,19 +637,18 @@ impl Default for JsonConfig { /// /// ## Example /// -/// ```rust,ignore -/// extern crate bytes; -/// # extern crate actix_web; -/// use actix_web::{http, App, Result}; +/// ```rust +/// use bytes::Bytes; +/// use actix_web::{web, App}; /// -/// /// extract text data from request -/// fn index(body: bytes::Bytes) -> Result { -/// Ok(format!("Body {:?}!", body)) +/// /// extract binary data from request +/// fn index(body: Bytes) -> String { +/// format!("Body {:?}!", body) /// } /// /// fn main() { /// let app = App::new() -/// .resource("/index.html", |r| r.method(http::Method::GET).with(index)); +/// .resource("/index.html", |r| r.route(web::get().to(index))); /// } /// ``` impl

FromRequest

for Bytes @@ -644,7 +673,7 @@ where } } -/// Extract text information from the request's body. +/// Extract text information from a request's body. /// /// Text extractor automatically decode body according to the request's charset. /// @@ -653,21 +682,20 @@ where /// /// ## Example /// -/// ```rust,ignore -/// # extern crate actix_web; -/// use actix_web::{http, App, Result}; +/// ```rust +/// use actix_web::{web, extract, App}; /// /// /// extract text data from request -/// fn index(body: String) -> Result { -/// Ok(format!("Body {}!", body)) +/// fn index(text: String) -> String { +/// format!("Body {}!", text) /// } /// /// fn main() { /// let app = App::new().resource("/index.html", |r| { -/// r.method(http::Method::GET) -/// .with_config(index, |cfg| { // <- register handler with extractor params -/// cfg.0.limit(4096); // <- limit size of the payload -/// }) +/// r.route( +/// web::get() +/// .config(extract::PayloadConfig::new(4096)) // <- limit size of the payload +/// .to(index)) // <- register handler with extractor params /// }); /// } /// ``` @@ -722,22 +750,23 @@ where /// /// ## Example /// -/// ```rust,ignore -/// # extern crate actix_web; -/// extern crate rand; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{http, App, Result, HttpRequest, Error, FromRequest}; +/// ```rust +/// # #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App, Error, FromRequest, ServiceFromRequest}; /// use actix_web::error::ErrorBadRequest; +/// use rand; /// /// #[derive(Debug, Deserialize)] -/// struct Thing { name: String } +/// struct Thing { +/// name: String +/// } /// -/// impl FromRequest for Thing { +/// impl

FromRequest

for Thing { +/// type Error = Error; +/// type Future = Result; /// type Config = (); -/// type Result = Result; /// -/// #[inline] -/// fn from_request(req: &HttpRequest, _cfg: &Self::Config) -> Self::Result { +/// fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { /// if rand::random() { /// Ok(Thing { name: "thingy".into() }) /// } else { @@ -747,18 +776,18 @@ where /// } /// } /// -/// /// extract text data from request -/// fn index(supplied_thing: Option) -> Result { +/// /// extract `Thing` from request +/// fn index(supplied_thing: Option) -> String { /// match supplied_thing { /// // Puns not intended -/// Some(thing) => Ok(format!("Got something: {:?}", thing)), -/// None => Ok(format!("No thing!")) +/// Some(thing) => format!("Got something: {:?}", thing), +/// None => format!("No thing!") /// } /// } /// /// fn main() { /// let app = App::new().resource("/users/:first", |r| { -/// r.method(http::Method::POST).with(index) +/// r.route(web::post().to(index)) /// }); /// } /// ``` @@ -773,7 +802,7 @@ where #[inline] fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { - Box::new(T::from_request(req).then(|r| match r { + Box::new(T::from_request(req).into_future().then(|r| match r { Ok(v) => future::ok(Some(v)), Err(_) => future::ok(None), })) @@ -782,46 +811,46 @@ where /// Optionally extract a field from the request or extract the Error if unsuccessful /// -/// If the FromRequest for T fails, inject Err into handler rather than returning an error response +/// If the `FromRequest` for T fails, inject Err into handler rather than returning an error response /// /// ## Example /// -/// ```rust,ignore -/// # extern crate actix_web; -/// extern crate rand; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{http, App, Result, HttpRequest, Error, FromRequest}; +/// ```rust +/// # #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App, Result, Error, FromRequest, ServiceFromRequest}; /// use actix_web::error::ErrorBadRequest; +/// use rand; /// /// #[derive(Debug, Deserialize)] -/// struct Thing { name: String } +/// struct Thing { +/// name: String +/// } /// -/// impl FromRequest for Thing { +/// impl

FromRequest

for Thing { +/// type Error = Error; +/// type Future = Result; /// type Config = (); -/// type Result = Result; /// -/// #[inline] -/// fn from_request(req: &Request, _cfg: &Self::Config) -> Self::Result { +/// fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { /// if rand::random() { /// Ok(Thing { name: "thingy".into() }) /// } else { /// Err(ErrorBadRequest("no luck")) /// } -/// /// } /// } /// -/// /// extract text data from request -/// fn index(supplied_thing: Result) -> Result { +/// /// extract `Thing` from request +/// fn index(supplied_thing: Result) -> String { /// match supplied_thing { -/// Ok(thing) => Ok(format!("Got thing: {:?}", thing)), -/// Err(e) => Ok(format!("Error extracting thing: {}", e)) +/// Ok(thing) => format!("Got thing: {:?}", thing), +/// Err(e) => format!("Error extracting thing: {}", e) /// } /// } /// /// fn main() { /// let app = App::new().resource("/users/:first", |r| { -/// r.method(http::Method::POST).with(index) +/// r.route(web::post().to(index)) /// }); /// } /// ``` @@ -837,7 +866,7 @@ where #[inline] fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { - Box::new(T::from_request(req).then(|res| match res { + Box::new(T::from_request(req).into_future().then(|res| match res { Ok(v) => ok(Ok(v)), Err(e) => ok(Err(e)), })) @@ -924,7 +953,7 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { $fut_type { items: <($(Option<$T>,)+)>::default(), - futs: ($($T::from_request(req),)+), + futs: ($($T::from_request(req).into_future(),)+), } } } @@ -932,7 +961,7 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { #[doc(hidden)] pub struct $fut_type),+> { items: ($(Option<$T>,)+), - futs: ($($T::Future,)+), + futs: ($(<$T::Future as futures::IntoFuture>::Future,)+), } impl),+> Future for $fut_type diff --git a/src/handler.rs b/src/handler.rs index 313422ed..98a36c56 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -7,55 +7,11 @@ use actix_service::{NewService, Service, Void}; use futures::future::{ok, FutureResult}; 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}; -/// 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 { - /// The associated error which can be returned. - type Error: Into; - - /// Future that resolves to a Self - type Future: Future; - - /// Configuration for the extractor - type Config: ExtractorConfig; - - /// Convert request to a Self - fn from_request(req: &mut ServiceFromRequest

) -> Self::Future; -} - -/// Storage for extractor configs -#[derive(Default)] -pub struct ConfigStorage { - pub(crate) storage: Option>, -} - -impl ConfigStorage { - pub fn store(&mut self, config: C) { - if self.storage.is_none() { - self.storage = Some(Rc::new(Extensions::new())); - } - if let Some(ref mut ext) = self.storage { - Rc::get_mut(ext).unwrap().insert(config); - } - } -} - -pub trait ExtractorConfig: Default + Clone + 'static { - /// Set default configuration to config storage - fn store_default(ext: &mut ConfigStorage) { - ext.store(Self::default()) - } -} - -impl ExtractorConfig for () { - fn store_default(_: &mut ConfigStorage) {} -} - /// Handler converter factory pub trait Factory: Clone where @@ -134,14 +90,14 @@ where type Request = (T, HttpRequest); type Response = ServiceResponse; type Error = Void; - type Future = HandleServiceResponse; + type Future = HandleServiceResponse<::Future>; fn poll_ready(&mut self) -> Poll<(), Self::Error> { Ok(Async::Ready(())) } fn call(&mut self, (param, req): (T, HttpRequest)) -> Self::Future { - let fut = self.hnd.call(param).respond_to(&req); + let fut = self.hnd.call(param).respond_to(&req).into_future(); HandleServiceResponse { fut, req: Some(req), @@ -368,7 +324,7 @@ impl> Service for ExtractService { fn call(&mut self, req: ServiceRequest

) -> Self::Future { let mut req = ServiceFromRequest::new(req, self.config.clone()); ExtractResponse { - fut: T::from_request(&mut req), + fut: T::from_request(&mut req).into_future(), req: Some(req), } } @@ -376,7 +332,7 @@ impl> Service for ExtractService { pub struct ExtractResponse> { req: Option>, - fut: T::Future, + fut: ::Future, } impl> Future for ExtractResponse { diff --git a/src/lib.rs b/src/lib.rs index e876a7ea..f61bf0ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ #![allow(clippy::type_complexity)] mod app; -pub mod extractor; +pub mod extract; pub mod handler; // mod info; pub mod blocking; @@ -17,23 +17,23 @@ pub mod test; // re-export for convenience pub use actix_http::Response as HttpResponse; -pub use actix_http::{http, Error, HttpMessage, ResponseError}; +pub use actix_http::{error, http, Error, HttpMessage, ResponseError, Result}; pub use crate::app::App; -pub use crate::extractor::{Form, Json, Path, PayloadConfig, Query}; -pub use crate::handler::FromRequest; +pub use crate::extract::{FromRequest, Json}; pub use crate::request::HttpRequest; pub use crate::resource::Resource; pub use crate::responder::{Either, Responder}; pub use crate::route::Route; -pub use crate::service::{ServiceRequest, ServiceResponse}; +pub use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; pub use crate::state::State; pub mod web { use actix_http::{http::Method, Error, Response}; use futures::IntoFuture; - use crate::handler::{AsyncFactory, Factory, FromRequest}; + use crate::extract::FromRequest; + use crate::handler::{AsyncFactory, Factory}; use crate::responder::Responder; use crate::Route; @@ -107,9 +107,3 @@ pub mod web { Route::new().to_async(handler) } } - -pub mod dev { - pub use crate::app::AppRouter; - pub use crate::handler::{AsyncFactory, Extract, Factory, Handle}; - // pub use crate::info::ConnectionInfo; -} diff --git a/src/request.rs b/src/request.rs index a7c84b53..48a2dd83 100644 --- a/src/request.rs +++ b/src/request.rs @@ -8,7 +8,7 @@ use actix_http::{Error, Extensions, HttpMessage, Message, Payload, RequestHead}; use actix_router::{Path, Url}; use futures::future::{ok, FutureResult}; -use crate::handler::FromRequest; +use crate::extract::FromRequest; use crate::service::ServiceFromRequest; #[derive(Clone)] diff --git a/src/resource.rs b/src/resource.rs index 98c2dc11..f05e998f 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -9,7 +9,8 @@ use actix_service::{ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; -use crate::handler::{AsyncFactory, Factory, FromRequest}; +use crate::extract::FromRequest; +use crate::handler::{AsyncFactory, Factory}; use crate::responder::Responder; use crate::route::{CreateRouteService, Route, RouteService}; use crate::service::{ServiceRequest, ServiceResponse}; diff --git a/src/responder.rs b/src/responder.rs index b3ec7ec7..22588a9c 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -1,7 +1,7 @@ use actix_http::{dev::ResponseBuilder, http::StatusCode, Error, Response}; use bytes::{Bytes, BytesMut}; use futures::future::{err, ok, Either as EitherFuture, FutureResult}; -use futures::{Future, Poll}; +use futures::{Future, IntoFuture, Poll}; use crate::request::HttpRequest; @@ -13,7 +13,7 @@ pub trait Responder { type Error: Into; /// The future response value. - type Future: Future; + type Future: IntoFuture; /// Convert itself to `AsyncResult` or `Error`. fn respond_to(self, req: &HttpRequest) -> Self::Future; @@ -34,11 +34,14 @@ where T: Responder, { type Error = T::Error; - type Future = EitherFuture>; + type Future = EitherFuture< + ::Future, + FutureResult, + >; fn respond_to(self, req: &HttpRequest) -> Self::Future { match self { - Some(t) => EitherFuture::A(t.respond_to(req)), + Some(t) => EitherFuture::A(t.respond_to(req).into_future()), None => EitherFuture::B(ok(Response::build(StatusCode::NOT_FOUND).finish())), } } @@ -50,11 +53,16 @@ where E: Into, { type Error = Error; - type Future = EitherFuture, FutureResult>; + type Future = EitherFuture< + ResponseFuture<::Future>, + FutureResult, + >; fn respond_to(self, req: &HttpRequest) -> Self::Future { match self { - Ok(val) => EitherFuture::A(ResponseFuture::new(val.respond_to(req))), + Ok(val) => { + EitherFuture::A(ResponseFuture::new(val.respond_to(req).into_future())) + } Err(e) => EitherFuture::B(err(e.into())), } } @@ -147,34 +155,36 @@ impl Responder for BytesMut { /// Combines two different responder types into a single type /// -/// ```rust,ignore -/// # extern crate actix_web; -/// # extern crate futures; -/// # use futures::future::Future; -/// use actix_web::{AsyncResponder, Either, Error, Request, Response}; -/// use futures::future::result; +/// ```rust +/// # use futures::future::{ok, Future}; +/// use actix_web::{Either, Error, HttpResponse}; /// /// type RegisterResult = -/// Either>>; +/// Either>>; /// -/// fn index(req: Request) -> RegisterResult { +/// fn index() -> RegisterResult { /// if is_a_variant() { -/// // <- choose variant A -/// Either::A(Response::BadRequest().body("Bad data")) +/// // <- choose left variant +/// Either::A(HttpResponse::BadRequest().body("Bad data")) /// } else { /// Either::B( -/// // <- variant B -/// result(Ok(Response::Ok() +/// // <- Right variant +/// Box::new(ok(HttpResponse::Ok() /// .content_type("text/html") /// .body("Hello!"))) -/// .responder(), /// ) /// } /// } /// # fn is_a_variant() -> bool { true } /// # fn main() {} /// ``` -pub type Either = either::Either; +#[derive(Debug, PartialEq)] +pub enum Either { + /// First branch of the type + A(A), + /// Second branch of the type + B(B), +} impl Responder for Either where @@ -182,12 +192,15 @@ where B: Responder, { type Error = Error; - type Future = EitherResponder; + type Future = EitherResponder< + ::Future, + ::Future, + >; fn respond_to(self, req: &HttpRequest) -> Self::Future { match self { - either::Either::Left(a) => EitherResponder::A(a.respond_to(req)), - either::Either::Right(b) => EitherResponder::B(b.respond_to(req)), + Either::A(a) => EitherResponder::A(a.respond_to(req).into_future()), + Either::B(b) => EitherResponder::B(b.respond_to(req).into_future()), } } } @@ -234,7 +247,7 @@ where let req = req.clone(); Box::new( self.map_err(|e| e.into()) - .and_then(move |r| ResponseFuture(r.respond_to(&req))), + .and_then(move |r| ResponseFuture(r.respond_to(&req).into_future())), ) } } diff --git a/src/route.rs b/src/route.rs index 16a4fc5b..72abeb32 100644 --- a/src/route.rs +++ b/src/route.rs @@ -5,11 +5,9 @@ use actix_http::{http::Method, Error, Extensions, Response}; use actix_service::{NewService, Service}; use futures::{Async, Future, IntoFuture, Poll}; +use crate::extract::{ConfigStorage, ExtractorConfig, FromRequest}; use crate::guard::{self, Guard}; -use crate::handler::{ - AsyncFactory, AsyncHandle, ConfigStorage, Extract, ExtractorConfig, Factory, - FromRequest, Handle, -}; +use crate::handler::{AsyncFactory, AsyncHandle, Extract, Factory, Handle}; use crate::responder::Responder; use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; use crate::HttpResponse; @@ -219,7 +217,7 @@ impl Route

{ /// /// ```rust /// #[macro_use] extern crate serde_derive; - /// use actix_web::{web, http, App, Path}; + /// use actix_web::{web, http, App, extract::Path}; /// /// #[derive(Deserialize)] /// struct Info { @@ -244,7 +242,7 @@ impl Route

{ /// ```rust /// # use std::collections::HashMap; /// # use serde_derive::Deserialize; - /// use actix_web::{web, http, App, Json, Path, Query}; + /// use actix_web::{web, App, Json, extract::Path, extract::Query}; /// /// #[derive(Deserialize)] /// struct Info { @@ -259,7 +257,7 @@ impl Route

{ /// fn main() { /// let app = App::new().resource( /// "/{username}/index.html", // <- define path parameters - /// |r| r.route(web::method(http::Method::GET).to(index)), + /// |r| r.route(web::get().to(index)), /// ); /// } /// ``` @@ -283,7 +281,7 @@ impl Route

{ /// ```rust /// # use futures::future::ok; /// #[macro_use] extern crate serde_derive; - /// use actix_web::{web, http, App, Error, Path}; + /// use actix_web::{web, App, Error, extract::Path}; /// use futures::Future; /// /// #[derive(Deserialize)] @@ -323,7 +321,7 @@ impl Route

{ /// for specific route. /// /// ```rust - /// use actix_web::{web, extractor, App}; + /// use actix_web::{web, extract, App}; /// /// /// extract text data from request /// fn index(body: String) -> String { @@ -335,7 +333,7 @@ impl Route

{ /// r.route( /// web::get() /// // limit size of the payload - /// .config(extractor::PayloadConfig::new(4096)) + /// .config(extract::PayloadConfig::new(4096)) /// // register handler /// .to(index) /// ) diff --git a/src/state.rs b/src/state.rs index 4a450245..168a8c89 100644 --- a/src/state.rs +++ b/src/state.rs @@ -6,7 +6,7 @@ use actix_http::Extensions; use futures::future::{err, ok, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; -use crate::handler::FromRequest; +use crate::extract::FromRequest; use crate::service::ServiceFromRequest; /// Application state factory diff --git a/src/test.rs b/src/test.rs index 4899cfe4..b4447f8b 100644 --- a/src/test.rs +++ b/src/test.rs @@ -14,13 +14,10 @@ use crate::service::{ServiceFromRequest, ServiceRequest}; /// Test `Request` builder /// /// ```rust,ignore -/// # extern crate http; -/// # extern crate actix_web; -/// # use http::{header, StatusCode}; /// # use actix_web::*; /// use actix_web::test::TestRequest; /// -/// fn index(req: &HttpRequest) -> HttpResponse { +/// fn index(req: HttpRequest) -> HttpResponse { /// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) { /// HttpResponse::Ok().into() /// } else { From 360082f99ffed06d19388905bf3023b81a6c80aa Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 14:45:56 -0800 Subject: [PATCH 023/109] update api docs --- src/app.rs | 4 +- src/extract.rs | 168 ++++++++++++++++++++++++++++++++++++++++-------- src/lib.rs | 11 +++- src/request.rs | 27 ++++++-- src/resource.rs | 46 +++++++++++-- src/state.rs | 48 +++++++------- 6 files changed, 237 insertions(+), 67 deletions(-) diff --git a/src/app.rs b/src/app.rs index c9c23d9c..34a663ae 100644 --- a/src/app.rs +++ b/src/app.rs @@ -245,8 +245,8 @@ where } } -/// Structure that follows the builder pattern for building application -/// instances. +/// Application router builder - Structure that follows the builder pattern +/// for building application instances. pub struct AppRouter { chain: C, services: Vec<(ResourceDef, HttpNewService

)>, diff --git a/src/extract.rs b/src/extract.rs index d6e53327..c0af6016 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -165,17 +165,63 @@ impl From for Path { } } +/// Extract typed information from the request's path. +/// +/// ## Example +/// +/// ```rust +/// use actix_web::{web, http, App, extract::Path}; +/// +/// /// extract path info from "/{username}/{count}/index.html" url +/// /// {username} - deserializes to a String +/// /// {count} - - deserializes to a u32 +/// fn index(info: Path<(String, u32)>) -> String { +/// format!("Welcome {}! {}", info.0, info.1) +/// } +/// +/// fn main() { +/// let app = App::new().resource( +/// "/{username}/{count}/index.html", // <- define path parameters +/// |r| r.route(web::get().to(index)) // <- register handler with `Path` extractor +/// ); +/// } +/// ``` +/// +/// It is possible to extract path information to a specific type that +/// implements `Deserialize` trait from *serde*. +/// +/// ```rust +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App, extract::Path, Error}; +/// +/// #[derive(Deserialize)] +/// struct Info { +/// username: String, +/// } +/// +/// /// extract `Info` from a path using serde +/// fn index(info: Path) -> Result { +/// Ok(format!("Welcome {}!", info.username)) +/// } +/// +/// fn main() { +/// let app = App::new().resource( +/// "/{username}/index.html", // <- define path parameters +/// |r| r.route(web::get().to(index)) // <- use handler with Path` extractor +/// ); +/// } +/// ``` impl FromRequest

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

) -> Self::Future { - Self::extract(req).map_err(ErrorNotFound).into_future() + Self::extract(req).map_err(ErrorNotFound) } } @@ -200,17 +246,17 @@ impl fmt::Display for Path { /// #[macro_use] extern crate serde_derive; /// use actix_web::{web, extract, App}; /// -///#[derive(Debug, Deserialize)] -///pub enum ResponseType { +/// #[derive(Debug, Deserialize)] +/// pub enum ResponseType { /// Token, /// Code -///} +/// } /// -///#[derive(Deserialize)] -///pub struct AuthRequest { +/// #[derive(Deserialize)] +/// pub struct AuthRequest { /// id: u64, /// response_type: ResponseType, -///} +/// } /// /// // Use `Query` extractor for query information. /// // This handler get called only if request's query contains `username` field @@ -248,19 +294,52 @@ impl Query { } } +/// Extract typed information from from the request's query. +/// +/// ## Example +/// +/// ```rust +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{web, extract, App}; +/// +/// #[derive(Debug, Deserialize)] +/// pub enum ResponseType { +/// Token, +/// Code +/// } +/// +/// #[derive(Deserialize)] +/// pub struct AuthRequest { +/// id: u64, +/// response_type: ResponseType, +/// } +/// +/// // Use `Query` extractor for query information. +/// // This handler get called only if request's query contains `username` field +/// // The correct request for this handler would be `/index.html?id=64&response_type=Code"` +/// fn index(info: extract::Query) -> String { +/// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) +/// } +/// +/// fn main() { +/// let app = App::new().resource( +/// "/index.html", +/// |r| r.route(web::get().to(index))); // <- use `Query` extractor +/// } +/// ``` impl FromRequest

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

) -> Self::Future { serde_urlencoded::from_str::(req.query_string()) - .map(|val| ok(Query(val))) - .unwrap_or_else(|e| err(e.into())) + .map(|val| Ok(Query(val))) + .unwrap_or_else(|e| Err(e.into())) } } @@ -282,7 +361,7 @@ impl fmt::Display for Query { /// To extract typed information from request's body, the type `T` must /// implement the `Deserialize` trait from *serde*. /// -/// [**FormConfig**](dev/struct.FormConfig.html) allows to configure extraction +/// [**FormConfig**](struct.FormConfig.html) allows to configure extraction /// process. /// /// ## Example @@ -436,7 +515,7 @@ impl Default for FormConfig { /// To extract typed information from request's body, the type `T` must /// implement the `Deserialize` trait from *serde*. /// -/// [**JsonConfig**](dev/struct.JsonConfig.html) allows to configure extraction +/// [**JsonConfig**](struct.JsonConfig.html) allows to configure extraction /// process. /// /// ## Example @@ -526,20 +605,51 @@ where impl Responder for Json { type Error = Error; - type Future = FutureResult; + type Future = Result; fn respond_to(self, _: &HttpRequest) -> Self::Future { let body = match serde_json::to_string(&self.0) { Ok(body) => body, - Err(e) => return err(e.into()), + Err(e) => return Err(e.into()), }; - ok(Response::build(StatusCode::OK) + Ok(Response::build(StatusCode::OK) .content_type("application/json") .body(body)) } } +/// Json extractor. Allow to extract typed information from request's +/// payload. +/// +/// To extract typed information from request's body, the type `T` must +/// implement the `Deserialize` trait from *serde*. +/// +/// [**JsonConfig**](struct.JsonConfig.html) allows to configure extraction +/// process. +/// +/// ## Example +/// +/// ```rust +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{web, extract, App}; +/// +/// #[derive(Deserialize)] +/// struct Info { +/// username: String, +/// } +/// +/// /// deserialize `Info` from request's body +/// fn index(info: extract::Json) -> String { +/// format!("Welcome {}!", info.username) +/// } +/// +/// fn main() { +/// let app = App::new().resource( +/// "/index.html", +/// |r| r.route(web::post().to(index))); +/// } +/// ``` impl FromRequest

for Json where T: DeserializeOwned + 'static, @@ -632,7 +742,7 @@ impl Default for JsonConfig { /// /// Loads request's payload and construct Bytes instance. /// -/// [**PayloadConfig**](dev/struct.PayloadConfig.html) allows to configure +/// [**PayloadConfig**](struct.PayloadConfig.html) allows to configure /// extraction process. /// /// ## Example @@ -677,7 +787,7 @@ where /// /// Text extractor automatically decode body according to the request's charset. /// -/// [**PayloadConfig**](dev/struct.PayloadConfig.html) allows to configure +/// [**PayloadConfig**](struct.PayloadConfig.html) allows to configure /// extraction process. /// /// ## Example @@ -931,6 +1041,17 @@ impl Default for PayloadConfig { } } +#[doc(hidden)] +impl

FromRequest

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

) -> Self::Future { + ok(()) + } +} + macro_rules! tuple_config ({ $($T:ident),+} => { impl<$($T,)+> ExtractorConfig for ($($T,)+) where $($T: ExtractorConfig + Clone,)+ @@ -944,6 +1065,7 @@ macro_rules! tuple_config ({ $($T:ident),+} => { macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { /// FromRequest implementation for tuple + #[doc(hidden)] impl + 'static),+> FromRequest

for ($($T,)+) { type Error = Error; @@ -995,16 +1117,6 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { } }); -impl

FromRequest

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

) -> Self::Future { - ok(()) - } -} - #[rustfmt::skip] mod m { use super::*; diff --git a/src/lib.rs b/src/lib.rs index f61bf0ac..37fd7591 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ mod app; pub mod extract; -pub mod handler; +mod handler; // mod info; pub mod blocking; pub mod guard; @@ -19,7 +19,7 @@ pub mod test; pub use actix_http::Response as HttpResponse; pub use actix_http::{error, http, Error, HttpMessage, ResponseError, Result}; -pub use crate::app::App; +pub use crate::app::{App, AppRouter}; pub use crate::extract::{FromRequest, Json}; pub use crate::request::HttpRequest; pub use crate::resource::Resource; @@ -37,30 +37,37 @@ pub mod web { use crate::responder::Responder; use crate::Route; + /// Create **route** without configuration. pub fn route() -> Route

{ Route::new() } + /// Create **route** with `GET` method guard. pub fn get() -> Route

{ Route::get() } + /// Create **route** with `POST` method guard. pub fn post() -> Route

{ Route::post() } + /// Create **route** with `PUT` method guard. pub fn put() -> Route

{ Route::put() } + /// Create **route** with `DELETE` method guard. pub fn delete() -> Route

{ Route::delete() } + /// Create **route** with `HEAD` method guard. pub fn head() -> Route

{ Route::new().method(Method::HEAD) } + /// Create **route** and add method guard. pub fn method(method: Method) -> Route

{ Route::new().method(method) } diff --git a/src/request.rs b/src/request.rs index 48a2dd83..d90627f5 100644 --- a/src/request.rs +++ b/src/request.rs @@ -6,12 +6,12 @@ use std::rc::Rc; use actix_http::http::{HeaderMap, Method, Uri, Version}; use actix_http::{Error, Extensions, HttpMessage, Message, Payload, RequestHead}; use actix_router::{Path, Url}; -use futures::future::{ok, FutureResult}; use crate::extract::FromRequest; use crate::service::ServiceFromRequest; #[derive(Clone)] +/// An HTTP Request pub struct HttpRequest { pub(crate) head: Message, pub(crate) path: Path, @@ -20,7 +20,7 @@ pub struct HttpRequest { impl HttpRequest { #[inline] - pub fn new( + pub(crate) fn new( head: Message, path: Path, extensions: Rc, @@ -140,14 +140,33 @@ impl HttpMessage for HttpRequest { } } +/// It is possible to get `HttpRequest` as an extractor handler parameter +/// +/// ## Example +/// +/// ```rust +/// # #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App, HttpRequest}; +/// +/// /// extract `Thing` from request +/// fn index(req: HttpRequest) -> String { +/// format!("Got thing: {:?}", req) +/// } +/// +/// fn main() { +/// let app = App::new().resource("/users/:first", |r| { +/// r.route(web::get().to(index)) +/// }); +/// } +/// ``` impl

FromRequest

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

) -> Self::Future { - ok(req.clone()) + Ok(req.clone()) } } diff --git a/src/resource.rs b/src/resource.rs index f05e998f..342d801d 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -18,10 +18,24 @@ use crate::service::{ServiceRequest, ServiceResponse}; type HttpService

= BoxedService, ServiceResponse, ()>; type HttpNewService

= BoxedNewService<(), ServiceRequest

, ServiceResponse, (), ()>; -/// Resource route definition +/// *Resource* is an entry in route table which corresponds to requested URL. /// -/// Route uses builder-like pattern for configuration. -/// If handler is not explicitly set, default *404 Not Found* handler is used. +/// Resource in turn has at least one route. +/// Route consists of an handlers objects and list of guards +/// (objects that implement `Guard` trait). +/// Resources and rouets uses builder-like pattern for configuration. +/// During request handling, resource object iterate through all routes +/// and check guards for specific route, if request matches all +/// guards, route considered matched and route handler get called. +/// +/// ```rust +/// use actix_web::{web, App, HttpResponse}; +/// +/// fn main() { +/// let app = App::new() +/// .resource( +/// "/", |r| r.route(web::get().to(|| HttpResponse::Ok()))); +/// } pub struct Resource> { routes: Vec>, endpoint: T, @@ -58,8 +72,6 @@ where >, { /// Register a new route. - /// *Route* is used for route configuration, i.e. adding guards, - /// setting up handler. /// /// ```rust /// use actix_web::{web, guard, App, HttpResponse}; @@ -74,12 +86,31 @@ where /// }); /// } /// ``` + /// + /// Multiple routes could be added to a resource. + /// + /// ```rust + /// use actix_web::{web, guard, App, HttpResponse}; + /// + /// fn main() { + /// let app = App::new() + /// .resource("/container/", |r| { + /// r.route(web::get().to(get_handler)) + /// .route(web::post().to(post_handler)) + /// .route(web::delete().to(delete_handler)) + /// }); + /// } + /// # fn get_handler() {} + /// # fn post_handler() {} + /// # fn delete_handler() {} + /// ``` pub fn route(mut self, route: Route

) -> Self { self.routes.push(route.finish()); self } - /// Register a new route and add handler. + /// Register a new route and add handler. This route get called for all + /// requests. /// /// ```rust /// use actix_web::*; @@ -148,7 +179,8 @@ where /// Register a resource middleware /// /// This is similar to `App's` middlewares, but - /// middlewares get invoked on resource level. + /// middleware is not allowed to change response type (i.e modify response's body). + /// Middleware get invoked on resource level. pub fn middleware( self, mw: F, diff --git a/src/state.rs b/src/state.rs index 168a8c89..d4e4c894 100644 --- a/src/state.rs +++ b/src/state.rs @@ -3,7 +3,6 @@ use std::rc::Rc; use actix_http::error::{Error, ErrorInternalServerError}; use actix_http::Extensions; -use futures::future::{err, ok, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; use crate::extract::FromRequest; @@ -19,60 +18,61 @@ pub(crate) trait StateFactoryResult { } /// Application state -pub struct State(Rc); +pub struct State(Rc); -impl State { - pub fn new(state: S) -> State { +impl State { + pub(crate) fn new(state: T) -> State { State(Rc::new(state)) } - pub fn get_ref(&self) -> &S { + /// Get referecnce to inner state type. + pub fn get_ref(&self) -> &T { self.0.as_ref() } } -impl Deref for State { - type Target = S; +impl Deref for State { + type Target = T; - fn deref(&self) -> &S { + fn deref(&self) -> &T { self.0.as_ref() } } -impl Clone for State { - fn clone(&self) -> State { +impl Clone for State { + fn clone(&self) -> State { State(self.0.clone()) } } -impl FromRequest

for State { +impl FromRequest

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

) -> Self::Future { - if let Some(st) = req.app_extensions().get::>() { - ok(st.clone()) + if let Some(st) = req.app_extensions().get::>() { + Ok(st.clone()) } else { - err(ErrorInternalServerError( - "State is not configured, use App::state()", + Err(ErrorInternalServerError( + "State is not configured, to configure use App::state()", )) } } } -impl StateFactory for State { +impl StateFactory for State { fn construct(&self) -> Box { Box::new(StateFut { st: self.clone() }) } } -struct StateFut { - st: State, +struct StateFut { + st: State, } -impl StateFactoryResult for StateFut { +impl StateFactoryResult for StateFut { fn poll_result(&mut self, extensions: &mut Extensions) -> Poll<(), ()> { extensions.insert(self.st.clone()); Ok(Async::Ready(())) @@ -92,17 +92,17 @@ where } } -struct StateFactoryFut +struct StateFactoryFut where - F: Future, + F: Future, F::Error: std::fmt::Debug, { fut: F, } -impl StateFactoryResult for StateFactoryFut +impl StateFactoryResult for StateFactoryFut where - F: Future, + F: Future, F::Error: std::fmt::Debug, { fn poll_result(&mut self, extensions: &mut Extensions) -> Poll<(), ()> { From 8502c32a3c576463e2b24d47244d543210c86d74 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 15:32:47 -0800 Subject: [PATCH 024/109] re-enable extractor tests --- src/extract.rs | 411 +++++++++++++++++++++++-------------------------- src/service.rs | 5 + src/test.rs | 6 + 3 files changed, 206 insertions(+), 216 deletions(-) diff --git a/src/extract.rs b/src/extract.rs index c0af6016..3b5c7e74 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -1044,11 +1044,11 @@ impl Default for PayloadConfig { #[doc(hidden)] impl

FromRequest

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

) -> Self::Future { - ok(()) + Ok(()) } } @@ -1147,6 +1147,7 @@ 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; @@ -1194,218 +1195,196 @@ mod tests { let s = rt.block_on(Form::::from_request(&mut req)).unwrap(); assert_eq!(s.hello, "world"); } + + #[test] + fn test_option() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let mut req = TestRequest::with_header( + header::CONTENT_TYPE, + "application/x-www-form-urlencoded", + ) + .config(FormConfig::default().limit(4096)) + .to_from(); + + let r = rt + .block_on(Option::>::from_request(&mut req)) + .unwrap(); + assert_eq!(r, None); + + let mut req = 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(); + + let r = rt + .block_on(Option::>::from_request(&mut req)) + .unwrap(); + assert_eq!( + r, + Some(Form(Info { + hello: "world".into() + })) + ); + + let mut req = 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(); + + let r = rt + .block_on(Option::>::from_request(&mut req)) + .unwrap(); + assert_eq!(r, None); + } + + #[test] + fn test_result() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let mut req = 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(); + + let r = rt + .block_on(Result::, Error>::from_request(&mut req)) + .unwrap() + .unwrap(); + assert_eq!( + r, + Form(Info { + hello: "world".into() + }) + ); + + let mut req = 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(); + + let r = rt + .block_on(Result::, Error>::from_request(&mut req)) + .unwrap(); + assert!(r.is_err()); + } + + #[test] + fn test_payload_config() { + let req = TestRequest::default().to_from(); + let cfg = PayloadConfig::default().mimetype(mime::APPLICATION_JSON); + assert!(cfg.check_mimetype(&req).is_err()); + + let req = TestRequest::with_header( + header::CONTENT_TYPE, + "application/x-www-form-urlencoded", + ) + .to_from(); + assert!(cfg.check_mimetype(&req).is_err()); + + let req = + TestRequest::with_header(header::CONTENT_TYPE, "application/json").to_from(); + assert!(cfg.check_mimetype(&req).is_ok()); + } + + #[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()); + } + + #[test] + fn test_extract_path_single() { + let resource = ResourceDef::new("/{value}/"); + + let mut req = TestRequest::with_uri("/32/").to_from(); + resource.match_path(req.match_info_mut()); + + assert_eq!(*Path::::from_request(&mut req).unwrap(), 32); + } + + #[test] + fn test_tuple_extract() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let resource = ResourceDef::new("/{key}/{value}/"); + + let mut req = TestRequest::with_uri("/name/user1/?id=test").to_from(); + resource.match_path(req.match_info_mut()); + + let res = rt + .block_on(<(Path<(String, String)>,)>::from_request(&mut req)) + .unwrap(); + assert_eq!((res.0).0, "name"); + assert_eq!((res.0).1, "user1"); + + let res = rt + .block_on( + <(Path<(String, String)>, Path<(String, String)>)>::from_request( + &mut req, + ), + ) + .unwrap(); + assert_eq!((res.0).0, "name"); + assert_eq!((res.0).1, "user1"); + assert_eq!((res.1).0, "name"); + assert_eq!((res.1).1, "user1"); + + let () = <()>::from_request(&mut req).unwrap(); + } } - -// #[test] -// fn test_option() { -// let req = TestRequest::with_header( -// header::CONTENT_TYPE, -// "application/x-www-form-urlencoded", -// ) -// .finish(); - -// let mut cfg = FormConfig::default(); -// cfg.limit(4096); - -// match Option::>::from_request(&req, &cfg) -// .poll() -// .unwrap() -// { -// Async::Ready(r) => assert_eq!(r, None), -// _ => unreachable!(), -// } - -// let req = TestRequest::with_header( -// header::CONTENT_TYPE, -// "application/x-www-form-urlencoded", -// ) -// .header(header::CONTENT_LENGTH, "9") -// .set_payload(Bytes::from_static(b"hello=world")) -// .finish(); - -// match Option::>::from_request(&req, &cfg) -// .poll() -// .unwrap() -// { -// Async::Ready(r) => assert_eq!( -// r, -// Some(Form(Info { -// hello: "world".into() -// })) -// ), -// _ => unreachable!(), -// } - -// let req = TestRequest::with_header( -// header::CONTENT_TYPE, -// "application/x-www-form-urlencoded", -// ) -// .header(header::CONTENT_LENGTH, "9") -// .set_payload(Bytes::from_static(b"bye=world")) -// .finish(); - -// match Option::>::from_request(&req, &cfg) -// .poll() -// .unwrap() -// { -// Async::Ready(r) => assert_eq!(r, None), -// _ => unreachable!(), -// } -// } - -// #[test] -// fn test_result() { -// let req = TestRequest::with_header( -// header::CONTENT_TYPE, -// "application/x-www-form-urlencoded", -// ) -// .header(header::CONTENT_LENGTH, "11") -// .set_payload(Bytes::from_static(b"hello=world")) -// .finish(); - -// match Result::, Error>::from_request(&req, &FormConfig::default()) -// .poll() -// .unwrap() -// { -// Async::Ready(Ok(r)) => assert_eq!( -// r, -// Form(Info { -// hello: "world".into() -// }) -// ), -// _ => unreachable!(), -// } - -// let req = TestRequest::with_header( -// header::CONTENT_TYPE, -// "application/x-www-form-urlencoded", -// ) -// .header(header::CONTENT_LENGTH, "9") -// .set_payload(Bytes::from_static(b"bye=world")) -// .finish(); - -// match Result::, Error>::from_request(&req, &FormConfig::default()) -// .poll() -// .unwrap() -// { -// Async::Ready(r) => assert!(r.is_err()), -// _ => unreachable!(), -// } -// } - -// #[test] -// fn test_payload_config() { -// let req = TestRequest::default().finish(); -// let mut cfg = PayloadConfig::default(); -// cfg.mimetype(mime::APPLICATION_JSON); -// assert!(cfg.check_mimetype(&req).is_err()); - -// let req = TestRequest::with_header( -// header::CONTENT_TYPE, -// "application/x-www-form-urlencoded", -// ) -// .finish(); -// assert!(cfg.check_mimetype(&req).is_err()); - -// let req = -// TestRequest::with_header(header::CONTENT_TYPE, "application/json").finish(); -// assert!(cfg.check_mimetype(&req).is_ok()); -// } - -// #[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 req = TestRequest::with_uri("/name/user1/?id=test").finish(); - -// let mut router = Router::<()>::default(); -// router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); -// let info = router.recognize(&req, &(), 0); -// let req = req.with_route_info(info); - -// let s = Path::::from_request(&req, &()).unwrap(); -// assert_eq!(s.key, "name"); -// assert_eq!(s.value, "user1"); - -// let s = Path::<(String, String)>::from_request(&req, &()).unwrap(); -// assert_eq!(s.0, "name"); -// assert_eq!(s.1, "user1"); - -// let s = Query::::from_request(&req, &()).unwrap(); -// assert_eq!(s.id, "test"); - -// let mut router = Router::<()>::default(); -// router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); -// let req = TestRequest::with_uri("/name/32/").finish(); -// let info = router.recognize(&req, &(), 0); -// let req = req.with_route_info(info); - -// let s = Path::::from_request(&req, &()).unwrap(); -// assert_eq!(s.as_ref().key, "name"); -// assert_eq!(s.value, 32); - -// let s = Path::<(String, u8)>::from_request(&req, &()).unwrap(); -// assert_eq!(s.0, "name"); -// assert_eq!(s.1, 32); - -// let res = Path::>::extract(&req).unwrap(); -// assert_eq!(res[0], "name".to_owned()); -// assert_eq!(res[1], "32".to_owned()); -// } - -// #[test] -// fn test_extract_path_single() { -// let mut router = Router::<()>::default(); -// router.register_resource(Resource::new(ResourceDef::new("/{value}/"))); - -// let req = TestRequest::with_uri("/32/").finish(); -// let info = router.recognize(&req, &(), 0); -// let req = req.with_route_info(info); -// assert_eq!(*Path::::from_request(&req, &()).unwrap(), 32); -// } - -// #[test] -// fn test_tuple_extract() { -// let mut router = Router::<()>::default(); -// router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); - -// let req = TestRequest::with_uri("/name/user1/?id=test").finish(); -// let info = router.recognize(&req, &(), 0); -// let req = req.with_route_info(info); - -// let res = match <(Path<(String, String)>,)>::extract(&req).poll() { -// Ok(Async::Ready(res)) => res, -// _ => panic!("error"), -// }; -// assert_eq!((res.0).0, "name"); -// assert_eq!((res.0).1, "user1"); - -// let res = match <(Path<(String, String)>, Path<(String, String)>)>::extract(&req) -// .poll() -// { -// Ok(Async::Ready(res)) => res, -// _ => panic!("error"), -// }; -// assert_eq!((res.0).0, "name"); -// assert_eq!((res.0).1, "user1"); -// assert_eq!((res.1).0, "name"); -// assert_eq!((res.1).1, "user1"); - -// let () = <()>::extract(&req); -// } -// } diff --git a/src/service.rs b/src/service.rs index 5602a613..a515300a 100644 --- a/src/service.rs +++ b/src/service.rs @@ -185,6 +185,11 @@ impl

ServiceFromRequest

{ self.req } + #[inline] + 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 { diff --git a/src/test.rs b/src/test.rs index b4447f8b..46fd45d5 100644 --- a/src/test.rs +++ b/src/test.rs @@ -118,6 +118,12 @@ impl TestRequest { self } + /// Set request config + pub fn config(mut self, data: T) -> Self { + self.extensions.insert(data); + self + } + /// Complete request creation and generate `ServiceRequest` instance pub fn finish(mut self) -> ServiceRequest { let req = self.req.finish(); From 34171fa7f570a8188491445fec86a2d427e48bb0 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 21:02:01 -0800 Subject: [PATCH 025/109] add scopes --- Cargo.toml | 1 + src/app.rs | 120 ++++++- src/guard.rs | 4 +- src/lib.rs | 2 + src/scope.rs | 872 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/test.rs | 9 +- 6 files changed, 1004 insertions(+), 4 deletions(-) create mode 100644 src/scope.rs diff --git a/Cargo.toml b/Cargo.toml index 03b1794f..370089e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ actix-utils = "0.3.0" actix-http = { git = "https://github.com/actix/actix-http.git" } actix-router = { git = "https://github.com/actix/actix-net.git" } +#actix-router = { path = "../actix-net/router" } bytes = "0.4" derive_more = "0.14" diff --git a/src/app.rs b/src/app.rs index 34a663ae..27658011 100644 --- a/src/app.rs +++ b/src/app.rs @@ -14,6 +14,7 @@ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; use crate::resource::Resource; +use crate::scope::Scope; use crate::service::{ServiceRequest, ServiceResponse}; use crate::state::{State, StateFactory, StateFactoryResult}; @@ -112,6 +113,52 @@ where self } + /// Configure scope for common root path. + /// + /// Scopes collect multiple paths under a common path prefix. + /// Scope path can contain variable path segments as resources. + /// + /// ```rust + /// # extern crate actix_web; + /// use actix_web::{App, HttpRequest, HttpResponse}; + /// + /// fn main() { + /// let app = App::new().scope("/{project_id}", |scope| { + /// scope + /// .resource("/path1", |r| r.to(|| HttpResponse::Ok())) + /// .resource("/path2", |r| r.to(|| HttpResponse::Ok())) + /// .resource("/path3", |r| r.to(|| HttpResponse::MethodNotAllowed())) + /// }); + /// } + /// ``` + /// + /// In the above example, three routes get added: + /// * /{project_id}/path1 + /// * /{project_id}/path2 + /// * /{project_id}/path3 + /// + pub fn scope(self, path: &str, f: F) -> AppRouter> + where + F: FnOnce(Scope

) -> Scope

, + { + let scope = f(Scope::new(path)); + let rdef = scope.rdef().clone(); + let default = scope.get_default(); + + let fref = Rc::new(RefCell::new(None)); + AppRouter { + chain: self.chain, + services: vec![(rdef, boxed::new_service(scope.into_new_service()))], + default: None, + defaults: vec![default], + endpoint: AppEntry::new(fref.clone()), + factory_ref: fref, + extensions: self.extensions, + state: self.state, + _t: PhantomData, + } + } + /// Configure resource for a specific path. /// /// Resources may have variable path segments. For example, a @@ -243,6 +290,18 @@ where _t: PhantomData, } } + + /// Set server host name. + /// + /// Host name is used by application router aa a hostname for url + /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. + /// html#method.host) documentation for more information. + /// + /// By default host name is set to a "localhost" value. + pub fn hostname(self, _val: &str) -> Self { + // self.host = val.to_owned(); + self + } } /// Application router builder - Structure that follows the builder pattern @@ -270,6 +329,42 @@ where InitError = (), >, { + /// Configure scope for common root path. + /// + /// Scopes collect multiple paths under a common path prefix. + /// Scope path can contain variable path segments as resources. + /// + /// ```rust + /// # extern crate actix_web; + /// use actix_web::{App, HttpRequest, HttpResponse}; + /// + /// fn main() { + /// let app = App::new().scope("/{project_id}", |scope| { + /// scope + /// .resource("/path1", |r| r.to(|| HttpResponse::Ok())) + /// .resource("/path2", |r| r.to(|| HttpResponse::Ok())) + /// .resource("/path3", |r| r.to(|| HttpResponse::MethodNotAllowed())) + /// }); + /// } + /// ``` + /// + /// In the above example, three routes get added: + /// * /{project_id}/path1 + /// * /{project_id}/path2 + /// * /{project_id}/path3 + /// + pub fn scope(mut self, path: &str, f: F) -> Self + where + F: FnOnce(Scope

) -> Scope

, + { + let scope = f(Scope::new(path)); + let rdef = scope.rdef().clone(); + self.defaults.push(scope.get_default()); + self.services + .push((rdef, boxed::new_service(scope.into_new_service()))); + self + } + /// Configure resource for a specific path. /// /// Resources may have variable path segments. For example, a @@ -466,6 +561,7 @@ where // set factory *self.factory_ref.borrow_mut() = Some(AppRoutingFactory { + default: self.default.clone(), services: Rc::new(self.services), }); @@ -480,6 +576,7 @@ where pub struct AppRoutingFactory

{ services: Rc)>>, + default: Option>>, } impl NewService for AppRoutingFactory

{ @@ -491,6 +588,12 @@ impl NewService for AppRoutingFactory

{ type Future = AppRoutingFactoryResponse

; fn new_service(&self, _: &()) -> Self::Future { + let default_fut = if let Some(ref default) = self.default { + Some(default.new_service(&())) + } else { + None + }; + AppRoutingFactoryResponse { fut: self .services @@ -502,6 +605,8 @@ impl NewService for AppRoutingFactory

{ ) }) .collect(), + default: None, + default_fut, } } } @@ -512,6 +617,8 @@ type HttpServiceFut

= Box, Error = ()>>; #[doc(hidden)] pub struct AppRoutingFactoryResponse

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

{ @@ -526,6 +633,13 @@ impl

Future for AppRoutingFactoryResponse

{ fn poll(&mut self) -> Poll { let mut done = true; + if let Some(ref mut fut) = self.default_fut { + match fut.poll()? { + Async::Ready(default) => self.default = Some(default), + Async::NotReady => done = false, + } + } + // poll http services for item in &mut self.fut { let res = match item { @@ -560,8 +674,9 @@ impl

Future for AppRoutingFactoryResponse

{ router }); Ok(Async::Ready(AppRouting { - router: router.finish(), ready: None, + router: router.finish(), + default: self.default.take(), })) } else { Ok(Async::NotReady) @@ -572,6 +687,7 @@ impl

Future for AppRoutingFactoryResponse

{ pub struct AppRouting

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

, ResourceInfo)>, + default: Option>, } impl

Service for AppRouting

{ @@ -591,6 +707,8 @@ impl

Service for AppRouting

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

) -> Self::Future { if let Some((srv, _info)) = self.router.recognize_mut(req.match_info_mut()) { Either::A(srv.call(req)) + } else if let Some(ref mut default) = self.default { + Either::A(default.call(req)) } else { let req = req.into_request(); Either::B(ok(ServiceResponse::new(req, Response::NotFound().finish()))) diff --git a/src/guard.rs b/src/guard.rs index 10a56921..c8948b3b 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -312,7 +312,9 @@ mod tests { #[test] fn test_preds() { - let r = TestRequest::default().method(Method::TRACE).to_request(); + let r = TestRequest::default() + .method(Method::TRACE) + .to_http_request(); assert!(Not(Get()).check(&r,)); assert!(!Not(Trace()).check(&r,)); diff --git a/src/lib.rs b/src/lib.rs index 37fd7591..18a6de06 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ mod request; mod resource; mod responder; mod route; +mod scope; mod service; mod state; pub mod test; @@ -25,6 +26,7 @@ pub use crate::request::HttpRequest; pub use crate::resource::Resource; pub use crate::responder::{Either, Responder}; pub use crate::route::Route; +pub use crate::scope::Scope; pub use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; pub use crate::state::State; diff --git a/src/scope.rs b/src/scope.rs new file mode 100644 index 00000000..5327f953 --- /dev/null +++ b/src/scope.rs @@ -0,0 +1,872 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use actix_http::Response; +use actix_router::{ResourceDef, ResourceInfo, Router}; +use actix_service::boxed::{self, BoxedNewService, BoxedService}; +use actix_service::{ + ApplyNewService, IntoNewService, IntoNewTransform, NewService, NewTransform, Service, +}; +use futures::future::{ok, Either, Future, FutureResult}; +use futures::{Async, Poll}; + +use crate::guard::Guard; +use crate::resource::Resource; +use crate::route::Route; +use crate::service::{ServiceRequest, ServiceResponse}; + +type HttpService

= BoxedService, ServiceResponse, ()>; +type HttpNewService

= BoxedNewService<(), ServiceRequest

, ServiceResponse, (), ()>; +type BoxedResponse = Box>; + +/// Resources scope +/// +/// Scope is a set of resources with common root path. +/// Scopes collect multiple paths under a common path prefix. +/// Scope path can contain variable path segments as resources. +/// Scope prefix is always complete path segment, i.e `/app` would +/// be converted to a `/app/` and it would not match `/app` path. +/// +/// You can get variable path segments from `HttpRequest::match_info()`. +/// `Path` extractor also is able to extract scope level variable segments. +/// +/// ```rust +/// use actix_web::{App, HttpResponse}; +/// +/// fn main() { +/// let app = App::new().scope("/{project_id}/", |scope| { +/// scope +/// .resource("/path1", |r| r.to(|| HttpResponse::Ok())) +/// .resource("/path2", |r| r.to(|| HttpResponse::Ok())) +/// .resource("/path3", |r| r.to(|| HttpResponse::MethodNotAllowed())) +/// }); +/// } +/// ``` +/// +/// In the above example three routes get registered: +/// * /{project_id}/path1 - reponds to all http method +/// * /{project_id}/path2 - `GET` requests +/// * /{project_id}/path3 - `HEAD` requests +/// +pub struct Scope> { + endpoint: T, + rdef: ResourceDef, + services: Vec<(ResourceDef, HttpNewService

)>, + guards: Rc>>, + default: Rc>>>>, + defaults: Vec>>>>>, + factory_ref: Rc>>>, +} + +impl Scope

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

{ + let fref = Rc::new(RefCell::new(None)); + let rdef = ResourceDef::prefix(&insert_slash(path)); + Scope { + endpoint: ScopeEndpoint::new(fref.clone()), + rdef: rdef.clone(), + guards: Rc::new(Vec::new()), + services: Vec::new(), + default: Rc::new(RefCell::new(None)), + defaults: Vec::new(), + factory_ref: fref, + } + } +} + +impl Scope +where + T: NewService< + Request = ServiceRequest

, + Response = ServiceResponse, + Error = (), + InitError = (), + >, +{ + #[inline] + pub(crate) fn rdef(&self) -> &ResourceDef { + &self.rdef + } + + /// Add guard to a scope. + /// + /// ```rust + /// use actix_web::{web, guard, App, HttpRequest, HttpResponse, extract::Path}; + /// + /// fn index(data: Path<(String, String)>) -> &'static str { + /// "Welcome!" + /// } + /// + /// fn main() { + /// let app = App::new().scope("/app", |scope| { + /// scope + /// .guard(guard::Header("content-type", "text/plain")) + /// .route("/test1",web::get().to(index)) + /// .route("/test2", web::post().to(|r: HttpRequest| { + /// HttpResponse::MethodNotAllowed() + /// })) + /// }); + /// } + /// ``` + pub fn guard(mut self, guard: G) -> Self { + Rc::get_mut(&mut self.guards).unwrap().push(Box::new(guard)); + self + } + + /// Create nested scope. + /// + /// ```rust + /// use actix_web::{App, HttpRequest}; + /// + /// struct AppState; + /// + /// fn index(req: HttpRequest) -> &'static str { + /// "Welcome!" + /// } + /// + /// fn main() { + /// let app = App::new().scope("/app", |scope| { + /// scope.nested("/v1", |scope| scope.resource("/test1", |r| r.to(index))) + /// }); + /// } + /// ``` + pub fn nested(mut self, path: &str, f: F) -> Self + where + F: FnOnce(Scope

) -> Scope

, + { + let scope = f(Scope::new(path)); + let rdef = scope.rdef().clone(); + self.defaults.push(scope.get_default()); + self.services + .push((rdef, boxed::new_service(scope.into_new_service()))); + + self + } + + /// Configure route for a specific path. + /// + /// This is a simplified version of the `Scope::resource()` 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, extract::Path}; + /// + /// fn index(data: Path<(String, String)>) -> &'static str { + /// "Welcome!" + /// } + /// + /// fn main() { + /// let app = App::new().scope("/app", |scope| { + /// scope.route("/test1", web::get().to(index)) + /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())) + /// }); + /// } + /// ``` + pub fn route(self, path: &str, route: Route

) -> Self { + self.resource(path, move |r| r.route(route)) + } + + /// configure resource for a specific path. + /// + /// This method is similar to an `App::resource()` method. + /// Resources may have variable path segments. Resource path uses scope + /// path as a path prefix. + /// + /// ```rust + /// use actix_web::*; + /// + /// fn main() { + /// let app = App::new().scope("/api", |scope| { + /// scope.resource("/users/{userid}/{friend}", |r| { + /// r.route(web::get().to(|| HttpResponse::Ok())) + /// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) + /// .route(web::route() + /// .guard(guard::Any(guard::Get()).or(guard::Put())) + /// .guard(guard::Header("Content-Type", "text/plain")) + /// .to(|| HttpResponse::Ok())) + /// }) + /// }); + /// } + /// ``` + pub fn resource(mut self, path: &str, f: F) -> Self + where + F: FnOnce(Resource

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

, + Response = ServiceResponse, + Error = (), + InitError = (), + > + 'static, + { + // add resource + let rdef = ResourceDef::new(&insert_slash(path)); + let resource = f(Resource::new()); + self.defaults.push(resource.get_default()); + self.services + .push((rdef, boxed::new_service(resource.into_new_service()))); + self + } + + /// Default resource to be used if no matching route could be found. + pub fn default_resource(mut self, f: F) -> Self + where + F: FnOnce(Resource

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

, + Response = ServiceResponse, + Error = (), + InitError = (), + > + 'static, + { + // 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(|_| ()), + ))))); + + self + } + + /// Register a scope middleware + /// + /// This is similar to `App's` middlewares, but + /// middleware is not allowed to change response type (i.e modify response's body). + /// Middleware get invoked on scope level. + pub fn middleware( + self, + mw: F, + ) -> Scope< + P, + impl NewService< + Request = ServiceRequest

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

, + Response = ServiceResponse, + Error = (), + InitError = (), + >, + F: IntoNewTransform, + { + let endpoint = ApplyNewService::new(mw, self.endpoint); + Scope { + endpoint, + rdef: self.rdef, + guards: self.guards, + services: self.services, + default: self.default, + defaults: self.defaults, + factory_ref: self.factory_ref, + } + } + + pub(crate) fn get_default(&self) -> Rc>>>> { + self.default.clone() + } +} + +fn insert_slash(path: &str) -> String { + let mut path = path.to_owned(); + if !path.is_empty() && !path.starts_with('/') { + path.insert(0, '/'); + }; + path +} + +impl IntoNewService for Scope +where + T: NewService< + Request = ServiceRequest

, + Response = ServiceResponse, + Error = (), + InitError = (), + >, +{ + fn into_new_service(self) -> T { + // update resource default service + if let Some(ref d) = *self.default.as_ref().borrow() { + for default in &self.defaults { + if default.borrow_mut().is_none() { + *default.borrow_mut() = Some(d.clone()); + } + } + } + + *self.factory_ref.borrow_mut() = Some(ScopeFactory { + default: self.default.clone(), + services: Rc::new(self.services), + }); + + self.endpoint + } +} + +pub struct ScopeFactory

{ + services: Rc)>>, + default: Rc>>>>, +} + +impl NewService for ScopeFactory

{ + type Request = ServiceRequest

; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = ScopeService

; + type Future = ScopeFactoryResponse

; + + fn new_service(&self, _: &()) -> Self::Future { + let default_fut = if let Some(ref default) = *self.default.borrow() { + Some(default.new_service(&())) + } else { + None + }; + + ScopeFactoryResponse { + fut: self + .services + .iter() + .map(|(path, service)| { + CreateScopeServiceItem::Future( + Some(path.clone()), + service.new_service(&()), + ) + }) + .collect(), + default: None, + default_fut, + } + } +} + +/// Create app service +#[doc(hidden)] +pub struct ScopeFactoryResponse

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

= Box, Error = ()>>; + +enum CreateScopeServiceItem

{ + Future(Option, HttpServiceFut

), + Service(ResourceDef, HttpService

), +} + +impl

Future for ScopeFactoryResponse

{ + type Item = ScopeService

; + type Error = (); + + fn poll(&mut self) -> Poll { + let mut done = true; + + if let Some(ref mut fut) = self.default_fut { + match fut.poll()? { + Async::Ready(default) => self.default = Some(default), + Async::NotReady => done = false, + } + } + + // poll http services + for item in &mut self.fut { + let res = match item { + CreateScopeServiceItem::Future(ref mut path, ref mut fut) => { + match fut.poll()? { + Async::Ready(service) => Some((path.take().unwrap(), service)), + Async::NotReady => { + done = false; + None + } + } + } + CreateScopeServiceItem::Service(_, _) => continue, + }; + + if let Some((path, service)) = res { + *item = CreateScopeServiceItem::Service(path, service); + } + } + + if done { + let router = self + .fut + .drain(..) + .fold(Router::build(), |mut router, item| { + match item { + CreateScopeServiceItem::Service(path, service) => { + router.rdef(path, service) + } + CreateScopeServiceItem::Future(_, _) => unreachable!(), + } + router + }); + Ok(Async::Ready(ScopeService { + router: router.finish(), + default: self.default.take(), + _ready: None, + })) + } else { + Ok(Async::NotReady) + } + } +} + +pub struct ScopeService

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

, ResourceInfo)>, +} + +impl

Service for ScopeService

{ + type Request = ServiceRequest

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

) -> Self::Future { + if let Some((srv, _info)) = self.router.recognize_mut(req.match_info_mut()) { + Either::A(srv.call(req)) + } else if let Some(ref mut default) = self.default { + Either::A(default.call(req)) + } else { + let req = req.into_request(); + Either::B(ok(ServiceResponse::new(req, Response::NotFound().finish()))) + } + } +} + +#[doc(hidden)] +pub struct ScopeEndpoint

{ + factory: Rc>>>, +} + +impl

ScopeEndpoint

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

{ + type Request = ServiceRequest

; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = ScopeService

; + type Future = ScopeFactoryResponse

; + + fn new_service(&self, _: &()) -> Self::Future { + self.factory.borrow_mut().as_mut().unwrap().new_service(&()) + } +} + +#[cfg(test)] +mod tests { + use actix_http::body::{Body, ResponseBody}; + use actix_http::http::{Method, StatusCode}; + use actix_service::{IntoNewService, NewService, Service}; + use bytes::Bytes; + + use crate::test::TestRequest; + use crate::{web, App, HttpRequest, HttpResponse}; + + #[test] + fn test_scope() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope.resource("/path1", |r| r.to(|| HttpResponse::Ok())) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + } + + #[test] + fn test_scope_root() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope + .resource("", |r| r.to(|| HttpResponse::Ok())) + .resource("/", |r| r.to(|| HttpResponse::Created())) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/app/").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::CREATED); + } + + #[test] + fn test_scope_root2() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app/", |scope| { + scope.resource("", |r| r.to(|| HttpResponse::Ok())) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let req = TestRequest::with_uri("/app/").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + } + + #[test] + fn test_scope_root3() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app/", |scope| { + scope.resource("/", |r| r.to(|| HttpResponse::Ok())) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let req = TestRequest::with_uri("/app/").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[test] + fn test_scope_route() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("app", |scope| { + scope.resource("/path1", |r| { + r.route(web::get().to(|| HttpResponse::Ok())) + .route(web::delete().to(|| HttpResponse::Ok())) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/app/path1") + .method(Method::DELETE) + .to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/app/path1") + .method(Method::POST) + .to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[test] + fn test_scope_route_without_leading_slash() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("app", |scope| { + scope.resource("path1", |r| { + r.route(web::get().to(|| HttpResponse::Ok())) + .route(web::delete().to(|| HttpResponse::Ok())) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/app/path1") + .method(Method::DELETE) + .to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/app/path1") + .method(Method::POST) + .to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + // #[test] + // fn test_scope_guard() { + // let mut rt = actix_rt::Runtime::new().unwrap(); + // let app = App::new() + // .scope("/app", |scope| { + // scope + // .guard(guard::Get()) + // .resource("/path1", |r| r.to(|| HttpResponse::Ok())) + // }) + // .into_new_service(); + // let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + // let req = TestRequest::with_uri("/app/path1") + // .method(Method::POST) + // .to_request(); + // let resp = rt.block_on(srv.call(req)).unwrap(); + // assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + // let req = TestRequest::with_uri("/app/path1") + // .method(Method::GET) + // .to_request(); + // let resp = rt.block_on(srv.call(req)).unwrap(); + // assert_eq!(resp.status(), StatusCode::OK); + // } + + #[test] + fn test_scope_variable_segment() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/ab-{project}", |scope| { + scope.resource("/path1", |r| { + r.to(|r: HttpRequest| { + HttpResponse::Ok() + .body(format!("project: {}", &r.match_info()["project"])) + }) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/ab-project1/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + match resp.body() { + ResponseBody::Body(Body::Bytes(ref b)) => { + let bytes: Bytes = b.clone().into(); + assert_eq!(bytes, Bytes::from_static(b"project: project1")); + } + _ => panic!(), + } + + let req = TestRequest::with_uri("/aa-project1/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[test] + fn test_nested_scope() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope.nested("/t1", |scope| { + scope.resource("/path1", |r| r.to(|| HttpResponse::Created())) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/t1/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::CREATED); + } + + #[test] + fn test_nested_scope_no_slash() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope.nested("t1", |scope| { + scope.resource("/path1", |r| r.to(|| HttpResponse::Created())) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/t1/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::CREATED); + } + + #[test] + fn test_nested_scope_root() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope.nested("/t1", |scope| { + scope + .resource("", |r| r.to(|| HttpResponse::Ok())) + .resource("/", |r| r.to(|| HttpResponse::Created())) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/t1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/app/t1/").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::CREATED); + } + + // #[test] + // fn test_nested_scope_filter() { + // let app = App::new() + // .scope("/app", |scope| { + // scope.nested("/t1", |scope| { + // scope + // .filter(pred::Get()) + // .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) + // }) + // }) + // .finish(); + + // let req = TestRequest::with_uri("/app/t1/path1") + // .method(Method::POST) + // .request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); + + // let req = TestRequest::with_uri("/app/t1/path1") + // .method(Method::GET) + // .request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::OK); + // } + + #[test] + fn test_nested_scope_with_variable_segment() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope.nested("/{project_id}", |scope| { + scope.resource("/path1", |r| { + r.to(|r: HttpRequest| { + HttpResponse::Created().body(format!( + "project: {}", + &r.match_info()["project_id"] + )) + }) + }) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/project_1/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::CREATED); + + match resp.body() { + ResponseBody::Body(Body::Bytes(ref b)) => { + let bytes: Bytes = b.clone().into(); + assert_eq!(bytes, Bytes::from_static(b"project: project_1")); + } + _ => panic!(), + } + } + + #[test] + fn test_nested2_scope_with_variable_segment() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope.nested("/{project}", |scope| { + scope.nested("/{id}", |scope| { + scope.resource("/path1", |r| { + r.to(|r: HttpRequest| { + HttpResponse::Created().body(format!( + "project: {} - {}", + &r.match_info()["project"], + &r.match_info()["id"], + )) + }) + }) + }) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/test/1/path1").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::CREATED); + + match resp.body() { + ResponseBody::Body(Body::Bytes(ref b)) => { + let bytes: Bytes = b.clone().into(); + assert_eq!(bytes, Bytes::from_static(b"project: test - 1")); + } + _ => panic!(), + } + + let req = TestRequest::with_uri("/app/test/1/path2").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[test] + fn test_default_resource() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope + .resource("/path1", |r| r.to(|| HttpResponse::Ok())) + .default_resource(|r| r.to(|| HttpResponse::BadRequest())) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/app/path2").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + + let req = TestRequest::with_uri("/path2").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[test] + fn test_default_resource_propagation() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app1", |scope| { + scope.default_resource(|r| r.to(|| HttpResponse::BadRequest())) + }) + .scope("/app2", |scope| scope) + .default_resource(|r| r.to(|| HttpResponse::MethodNotAllowed())) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/non-exist").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + + let req = TestRequest::with_uri("/app1/non-exist").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + + let req = TestRequest::with_uri("/app2/non-exist").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + } +} diff --git a/src/test.rs b/src/test.rs index 46fd45d5..684817ec 100644 --- a/src/test.rs +++ b/src/test.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; use actix_http::http::{HttpTryFrom, Method, Version}; use actix_http::test::TestRequest as HttpTestRequest; -use actix_http::{Extensions, PayloadStream}; +use actix_http::{Extensions, PayloadStream, Request}; use actix_router::{Path, Url}; use bytes::Bytes; @@ -135,8 +135,13 @@ impl TestRequest { ) } + /// 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_request(mut self) -> HttpRequest { + pub fn to_http_request(mut self) -> HttpRequest { let req = self.req.finish(); ServiceRequest::new( From 5c61321565d02f8c08b73d31a90676a48ab08694 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 3 Mar 2019 21:40:03 -0800 Subject: [PATCH 026/109] fix state factory support, tests for state and state factory --- src/app.rs | 175 +++++++++++++++++++++++++++++++++++++++++++++-- src/responder.rs | 37 ++++++++++ src/scope.rs | 2 +- 3 files changed, 208 insertions(+), 6 deletions(-) diff --git a/src/app.rs b/src/app.rs index 27658011..3940b9fc 100644 --- a/src/app.rs +++ b/src/app.rs @@ -14,7 +14,7 @@ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; use crate::resource::Resource; -use crate::scope::Scope; +use crate::scope::{insert_slash, Scope}; use crate::service::{ServiceRequest, ServiceResponse}; use crate::state::{State, StateFactory, StateFactoryResult}; @@ -103,13 +103,13 @@ where /// Set application state factory. This function is /// similar to `.state()` but it accepts state factory. State get /// constructed asynchronously during application initialization. - pub fn state_factory(mut self, state: F) -> Self + pub fn state_factory(mut self, state: F) -> Self where F: Fn() -> Out + 'static, Out: IntoFuture + 'static, Out::Error: std::fmt::Debug, { - self.state.push(Box::new(State::new(state))); + self.state.push(Box::new(state)); self } @@ -200,7 +200,7 @@ where InitError = (), > + 'static, { - let rdef = ResourceDef::new(path); + let rdef = ResourceDef::new(&insert_slash(path)); let resource = f(Resource::new()); let default = resource.get_default(); @@ -408,7 +408,7 @@ where InitError = (), > + 'static, { - let rdef = ResourceDef::new(path); + let rdef = ResourceDef::new(&insert_slash(path)); let resource = f(Resource::new()); self.defaults.push(resource.get_default()); self.services @@ -891,3 +891,168 @@ where self.chain.call(req) } } + +#[cfg(test)] +mod tests { + use actix_http::http::StatusCode; + + use super::*; + use crate::test::TestRequest; + use crate::{HttpResponse, State}; + + #[test] + fn test_default_resource() { + let mut rt = actix_rt::Runtime::new().unwrap(); + + let app = App::new() + .resource("/test", |r| r.to(|| HttpResponse::Ok())) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/test").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/blah").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let app = App::new() + .resource("/test", |r| r.to(|| HttpResponse::Ok())) + .default_resource(|r| r.to(|| HttpResponse::MethodNotAllowed())) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/blah").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + } + + #[test] + fn test_state() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .state(10usize) + .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::default().to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let app = App::new() + .state(10u32) + .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::default().to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); + } + + #[test] + fn test_state_factory() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .state_factory(|| Ok::<_, ()>(10usize)) + .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::default().to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let app = App::new() + .state_factory(|| Ok::<_, ()>(10u32)) + .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::default().to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); + } + + // #[test] + // fn test_handler() { + // let app = App::new() + // .handler("/test", |_: &_| HttpResponse::Ok()) + // .finish(); + + // let req = TestRequest::with_uri("/test").request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::OK); + + // let req = TestRequest::with_uri("/test/").request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::OK); + + // let req = TestRequest::with_uri("/test/app").request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::OK); + + // let req = TestRequest::with_uri("/testapp").request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); + + // let req = TestRequest::with_uri("/blah").request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); + // } + + // #[test] + // fn test_handler2() { + // let app = App::new() + // .handler("test", |_: &_| HttpResponse::Ok()) + // .finish(); + + // let req = TestRequest::with_uri("/test").request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::OK); + + // let req = TestRequest::with_uri("/test/").request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::OK); + + // let req = TestRequest::with_uri("/test/app").request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::OK); + + // let req = TestRequest::with_uri("/testapp").request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); + + // let req = TestRequest::with_uri("/blah").request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); + // } + + // #[test] + // fn test_route() { + // let app = App::new() + // .route("/test", Method::GET, |_: HttpRequest| HttpResponse::Ok()) + // .route("/test", Method::POST, |_: HttpRequest| { + // HttpResponse::Created() + // }) + // .finish(); + + // let req = TestRequest::with_uri("/test").method(Method::GET).request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::OK); + + // let req = TestRequest::with_uri("/test") + // .method(Method::POST) + // .request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::CREATED); + + // let req = TestRequest::with_uri("/test") + // .method(Method::HEAD) + // .request(); + // let resp = app.run(req); + // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); + // } +} diff --git a/src/responder.rs b/src/responder.rs index 22588a9c..8e7f66b4 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -272,3 +272,40 @@ where Ok(self.0.poll().map_err(|e| e.into())?) } } + +#[cfg(test)] +mod tests { + // use actix_http::body::Body; + use actix_http::body::{Body, ResponseBody}; + use actix_http::http::StatusCode; + use actix_service::{IntoNewService, NewService, Service}; + use bytes::Bytes; + + use crate::test::TestRequest; + use crate::App; + + #[test] + fn test_option_responder() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .resource("/none", |r| r.to(|| -> Option<&'static str> { None })) + .resource("/some", |r| r.to(|| Some("some"))) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); + + let req = TestRequest::with_uri("/none").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let req = TestRequest::with_uri("/some").to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + match resp.body() { + ResponseBody::Body(Body::Bytes(ref b)) => { + let bytes: Bytes = b.clone().into(); + assert_eq!(bytes, Bytes::from_static(b"some")); + } + _ => panic!(), + } + } +} diff --git a/src/scope.rs b/src/scope.rs index 5327f953..ec6bc035 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -272,7 +272,7 @@ where } } -fn insert_slash(path: &str) -> String { +pub(crate) fn insert_slash(path: &str) -> String { let mut path = path.to_owned(); if !path.is_empty() && !path.starts_with('/') { path.insert(0, '/'); From e442ddb1671ad9fb1644c3e7f150d6f834cccf78 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 4 Mar 2019 11:47:53 -0800 Subject: [PATCH 027/109] allow scope level guards --- Cargo.toml | 1 - src/app.rs | 92 +++++++++++++++--------- src/scope.rs | 185 +++++++++++++++++++++++++++++-------------------- src/service.rs | 8 ++- 4 files changed, 179 insertions(+), 107 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 370089e7..03b1794f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,6 @@ actix-utils = "0.3.0" actix-http = { git = "https://github.com/actix/actix-http.git" } actix-router = { git = "https://github.com/actix/actix-net.git" } -#actix-router = { path = "../actix-net/router" } bytes = "0.4" derive_more = "0.14" diff --git a/src/app.rs b/src/app.rs index 3940b9fc..119f1a21 100644 --- a/src/app.rs +++ b/src/app.rs @@ -13,11 +13,13 @@ use actix_service::{ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; +use crate::guard::Guard; use crate::resource::Resource; use crate::scope::{insert_slash, Scope}; use crate::service::{ServiceRequest, ServiceResponse}; use crate::state::{State, StateFactory, StateFactoryResult}; +type Guards = Vec>; type HttpService

= BoxedService, ServiceResponse, ()>; type HttpNewService

= BoxedNewService<(), ServiceRequest

, ServiceResponse, (), ()>; type BoxedResponse = Box>; @@ -141,14 +143,15 @@ where where F: FnOnce(Scope

) -> Scope

, { - let scope = f(Scope::new(path)); + let mut scope = f(Scope::new(path)); let rdef = scope.rdef().clone(); let default = scope.get_default(); + let guards = scope.take_guards(); let fref = Rc::new(RefCell::new(None)); AppRouter { chain: self.chain, - services: vec![(rdef, boxed::new_service(scope.into_new_service()))], + services: vec![(rdef, boxed::new_service(scope.into_new_service()), guards)], default: None, defaults: vec![default], endpoint: AppEntry::new(fref.clone()), @@ -201,13 +204,13 @@ where > + 'static, { let rdef = ResourceDef::new(&insert_slash(path)); - let resource = f(Resource::new()); - let default = resource.get_default(); + let res = f(Resource::new()); + let default = res.get_default(); let fref = Rc::new(RefCell::new(None)); AppRouter { chain: self.chain, - services: vec![(rdef, boxed::new_service(resource.into_new_service()))], + services: vec![(rdef, boxed::new_service(res.into_new_service()), None)], default: None, defaults: vec![default], endpoint: AppEntry::new(fref.clone()), @@ -308,7 +311,7 @@ where /// for building application instances. pub struct AppRouter { chain: C, - services: Vec<(ResourceDef, HttpNewService

)>, + services: Vec<(ResourceDef, HttpNewService

, Option)>, default: Option>>, defaults: Vec>>>>>, endpoint: T, @@ -357,11 +360,12 @@ where where F: FnOnce(Scope

) -> Scope

, { - let scope = f(Scope::new(path)); + let mut scope = f(Scope::new(path)); let rdef = scope.rdef().clone(); + let guards = scope.take_guards(); self.defaults.push(scope.get_default()); self.services - .push((rdef, boxed::new_service(scope.into_new_service()))); + .push((rdef, boxed::new_service(scope.into_new_service()), guards)); self } @@ -411,8 +415,11 @@ where let rdef = ResourceDef::new(&insert_slash(path)); let resource = f(Resource::new()); self.defaults.push(resource.get_default()); - self.services - .push((rdef, boxed::new_service(resource.into_new_service()))); + self.services.push(( + rdef, + boxed::new_service(resource.into_new_service()), + None, + )); self } @@ -452,6 +459,7 @@ where self.services.push(( rdef.into(), boxed::new_service(factory.into_new_service().map_init_err(|_| ())), + None, )); self } @@ -562,7 +570,12 @@ where // set factory *self.factory_ref.borrow_mut() = Some(AppRoutingFactory { default: self.default.clone(), - services: Rc::new(self.services), + services: Rc::new( + self.services + .into_iter() + .map(|(rdef, srv, guards)| (rdef, srv, RefCell::new(guards))) + .collect(), + ), }); AppInit { @@ -575,7 +588,7 @@ where } pub struct AppRoutingFactory

{ - services: Rc)>>, + services: Rc, RefCell>)>>, default: Option>>, } @@ -598,9 +611,10 @@ impl NewService for AppRoutingFactory

{ fut: self .services .iter() - .map(|(path, service)| { + .map(|(path, service, guards)| { CreateAppRoutingItem::Future( Some(path.clone()), + guards.borrow_mut().take(), service.new_service(&()), ) }) @@ -622,8 +636,8 @@ pub struct AppRoutingFactoryResponse

{ } enum CreateAppRoutingItem

{ - Future(Option, HttpServiceFut

), - Service(ResourceDef, HttpService

), + Future(Option, Option, HttpServiceFut

), + Service(ResourceDef, Option, HttpService

), } impl

Future for AppRoutingFactoryResponse

{ @@ -643,20 +657,24 @@ impl

Future for AppRoutingFactoryResponse

{ // poll http services for item in &mut self.fut { let res = match item { - CreateAppRoutingItem::Future(ref mut path, ref mut fut) => { - match fut.poll()? { - Async::Ready(service) => Some((path.take().unwrap(), service)), - Async::NotReady => { - done = false; - None - } + CreateAppRoutingItem::Future( + ref mut path, + ref mut guards, + ref mut fut, + ) => match fut.poll()? { + Async::Ready(service) => { + Some((path.take().unwrap(), guards.take(), service)) } - } - CreateAppRoutingItem::Service(_, _) => continue, + Async::NotReady => { + done = false; + None + } + }, + CreateAppRoutingItem::Service(_, _, _) => continue, }; - if let Some((path, service)) = res { - *item = CreateAppRoutingItem::Service(path, service); + if let Some((path, guards, service)) = res { + *item = CreateAppRoutingItem::Service(path, guards, service); } } @@ -666,10 +684,11 @@ impl

Future for AppRoutingFactoryResponse

{ .drain(..) .fold(Router::build(), |mut router, item| { match item { - CreateAppRoutingItem::Service(path, service) => { - router.rdef(path, service) + CreateAppRoutingItem::Service(path, guards, service) => { + router.rdef(path, service); + router.set_user_data(guards); } - CreateAppRoutingItem::Future(_, _) => unreachable!(), + CreateAppRoutingItem::Future(_, _, _) => unreachable!(), } router }); @@ -685,7 +704,7 @@ impl

Future for AppRoutingFactoryResponse

{ } pub struct AppRouting

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

, ResourceInfo)>, default: Option>, } @@ -705,7 +724,18 @@ impl

Service for AppRouting

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

) -> Self::Future { - if let Some((srv, _info)) = self.router.recognize_mut(req.match_info_mut()) { + let res = self.router.recognize_mut_checked(&mut req, |req, guards| { + if let Some(ref guards) = guards { + for f in guards { + if !f.check(req.head()) { + return false; + } + } + } + true + }); + + if let Some((srv, _info)) = res { Either::A(srv.call(req)) } else if let Some(ref mut default) = self.default { Either::A(default.call(req)) diff --git a/src/scope.rs b/src/scope.rs index ec6bc035..2ed18a42 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -15,6 +15,7 @@ use crate::resource::Resource; use crate::route::Route; use crate::service::{ServiceRequest, ServiceResponse}; +type Guards = Vec>; type HttpService

= BoxedService, ServiceResponse, ()>; type HttpNewService

= BoxedNewService<(), ServiceRequest

, ServiceResponse, (), ()>; type BoxedResponse = Box>; @@ -51,8 +52,8 @@ type BoxedResponse = Box>; pub struct Scope> { endpoint: T, rdef: ResourceDef, - services: Vec<(ResourceDef, HttpNewService

)>, - guards: Rc>>, + services: Vec<(ResourceDef, HttpNewService

, Option)>, + guards: Vec>, default: Rc>>>>, defaults: Vec>>>>>, factory_ref: Rc>>>, @@ -66,7 +67,7 @@ impl Scope

{ Scope { endpoint: ScopeEndpoint::new(fref.clone()), rdef: rdef.clone(), - guards: Rc::new(Vec::new()), + guards: Vec::new(), services: Vec::new(), default: Rc::new(RefCell::new(None)), defaults: Vec::new(), @@ -110,7 +111,7 @@ where /// } /// ``` pub fn guard(mut self, guard: G) -> Self { - Rc::get_mut(&mut self.guards).unwrap().push(Box::new(guard)); + self.guards.push(Box::new(guard)); self } @@ -135,11 +136,12 @@ where where F: FnOnce(Scope

) -> Scope

, { - let scope = f(Scope::new(path)); + let mut scope = f(Scope::new(path)); let rdef = scope.rdef().clone(); + let guards = scope.take_guards(); self.defaults.push(scope.get_default()); self.services - .push((rdef, boxed::new_service(scope.into_new_service()))); + .push((rdef, boxed::new_service(scope.into_new_service()), guards)); self } @@ -204,8 +206,11 @@ where let rdef = ResourceDef::new(&insert_slash(path)); let resource = f(Resource::new()); self.defaults.push(resource.get_default()); - self.services - .push((rdef, boxed::new_service(resource.into_new_service()))); + self.services.push(( + rdef, + boxed::new_service(resource.into_new_service()), + None, + )); self } @@ -270,6 +275,14 @@ where pub(crate) fn get_default(&self) -> Rc>>>> { self.default.clone() } + + pub(crate) fn take_guards(&mut self) -> Option>> { + if self.guards.is_empty() { + None + } else { + Some(std::mem::replace(&mut self.guards, Vec::new())) + } + } } pub(crate) fn insert_slash(path: &str) -> String { @@ -301,7 +314,12 @@ where *self.factory_ref.borrow_mut() = Some(ScopeFactory { default: self.default.clone(), - services: Rc::new(self.services), + services: Rc::new( + self.services + .into_iter() + .map(|(rdef, srv, guards)| (rdef, srv, RefCell::new(guards))) + .collect(), + ), }); self.endpoint @@ -309,7 +327,7 @@ where } pub struct ScopeFactory

{ - services: Rc)>>, + services: Rc, RefCell>)>>, default: Rc>>>>, } @@ -332,9 +350,10 @@ impl NewService for ScopeFactory

{ fut: self .services .iter() - .map(|(path, service)| { + .map(|(path, service, guards)| { CreateScopeServiceItem::Future( Some(path.clone()), + guards.borrow_mut().take(), service.new_service(&()), ) }) @@ -356,8 +375,8 @@ pub struct ScopeFactoryResponse

{ type HttpServiceFut

= Box, Error = ()>>; enum CreateScopeServiceItem

{ - Future(Option, HttpServiceFut

), - Service(ResourceDef, HttpService

), + Future(Option, Option, HttpServiceFut

), + Service(ResourceDef, Option, HttpService

), } impl

Future for ScopeFactoryResponse

{ @@ -377,20 +396,24 @@ impl

Future for ScopeFactoryResponse

{ // poll http services for item in &mut self.fut { let res = match item { - CreateScopeServiceItem::Future(ref mut path, ref mut fut) => { - match fut.poll()? { - Async::Ready(service) => Some((path.take().unwrap(), service)), - Async::NotReady => { - done = false; - None - } + CreateScopeServiceItem::Future( + ref mut path, + ref mut guards, + ref mut fut, + ) => match fut.poll()? { + Async::Ready(service) => { + Some((path.take().unwrap(), guards.take(), service)) } - } - CreateScopeServiceItem::Service(_, _) => continue, + Async::NotReady => { + done = false; + None + } + }, + CreateScopeServiceItem::Service(_, _, _) => continue, }; - if let Some((path, service)) = res { - *item = CreateScopeServiceItem::Service(path, service); + if let Some((path, guards, service)) = res { + *item = CreateScopeServiceItem::Service(path, guards, service); } } @@ -400,10 +423,11 @@ impl

Future for ScopeFactoryResponse

{ .drain(..) .fold(Router::build(), |mut router, item| { match item { - CreateScopeServiceItem::Service(path, service) => { - router.rdef(path, service) + CreateScopeServiceItem::Service(path, guards, service) => { + router.rdef(path, service); + router.set_user_data(guards); } - CreateScopeServiceItem::Future(_, _) => unreachable!(), + CreateScopeServiceItem::Future(_, _, _) => unreachable!(), } router }); @@ -419,7 +443,7 @@ impl

Future for ScopeFactoryResponse

{ } pub struct ScopeService

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

, ResourceInfo)>, } @@ -435,7 +459,18 @@ impl

Service for ScopeService

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

) -> Self::Future { - if let Some((srv, _info)) = self.router.recognize_mut(req.match_info_mut()) { + let res = self.router.recognize_mut_checked(&mut req, |req, guards| { + if let Some(ref guards) = guards { + for f in guards { + if !f.check(req.head()) { + return false; + } + } + } + true + }); + + if let Some((srv, _info)) = res { Either::A(srv.call(req)) } else if let Some(ref mut default) = self.default { Either::A(default.call(req)) @@ -478,7 +513,7 @@ mod tests { use bytes::Bytes; use crate::test::TestRequest; - use crate::{web, App, HttpRequest, HttpResponse}; + use crate::{guard, web, App, HttpRequest, HttpResponse}; #[test] fn test_scope() { @@ -614,30 +649,30 @@ mod tests { assert_eq!(resp.status(), StatusCode::NOT_FOUND); } - // #[test] - // fn test_scope_guard() { - // let mut rt = actix_rt::Runtime::new().unwrap(); - // let app = App::new() - // .scope("/app", |scope| { - // scope - // .guard(guard::Get()) - // .resource("/path1", |r| r.to(|| HttpResponse::Ok())) - // }) - // .into_new_service(); - // let mut srv = rt.block_on(app.new_service(&())).unwrap(); + #[test] + fn test_scope_guard() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope + .guard(guard::Get()) + .resource("/path1", |r| r.to(|| HttpResponse::Ok())) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); - // let req = TestRequest::with_uri("/app/path1") - // .method(Method::POST) - // .to_request(); - // let resp = rt.block_on(srv.call(req)).unwrap(); - // assert_eq!(resp.status(), StatusCode::NOT_FOUND); + let req = TestRequest::with_uri("/app/path1") + .method(Method::POST) + .to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); - // let req = TestRequest::with_uri("/app/path1") - // .method(Method::GET) - // .to_request(); - // let resp = rt.block_on(srv.call(req)).unwrap(); - // assert_eq!(resp.status(), StatusCode::OK); - // } + let req = TestRequest::with_uri("/app/path1") + .method(Method::GET) + .to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + } #[test] fn test_scope_variable_segment() { @@ -728,30 +763,32 @@ mod tests { assert_eq!(resp.status(), StatusCode::CREATED); } - // #[test] - // fn test_nested_scope_filter() { - // let app = App::new() - // .scope("/app", |scope| { - // scope.nested("/t1", |scope| { - // scope - // .filter(pred::Get()) - // .resource("/path1", |r| r.f(|_| HttpResponse::Ok())) - // }) - // }) - // .finish(); + #[test] + fn test_nested_scope_filter() { + let mut rt = actix_rt::Runtime::new().unwrap(); + let app = App::new() + .scope("/app", |scope| { + scope.nested("/t1", |scope| { + scope + .guard(guard::Get()) + .resource("/path1", |r| r.to(|| HttpResponse::Ok())) + }) + }) + .into_new_service(); + let mut srv = rt.block_on(app.new_service(&())).unwrap(); - // let req = TestRequest::with_uri("/app/t1/path1") - // .method(Method::POST) - // .request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); + let req = TestRequest::with_uri("/app/t1/path1") + .method(Method::POST) + .to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); - // let req = TestRequest::with_uri("/app/t1/path1") - // .method(Method::GET) - // .request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::OK); - // } + let req = TestRequest::with_uri("/app/t1/path1") + .method(Method::GET) + .to_request(); + let resp = rt.block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + } #[test] fn test_nested_scope_with_variable_segment() { diff --git a/src/service.rs b/src/service.rs index a515300a..637a8668 100644 --- a/src/service.rs +++ b/src/service.rs @@ -8,7 +8,7 @@ use actix_http::{ Error, Extensions, HttpMessage, Payload, Request, RequestHead, Response, ResponseHead, }; -use actix_router::{Path, Url}; +use actix_router::{Path, Resource, Url}; use futures::future::{ok, FutureResult, IntoFuture}; use crate::request::HttpRequest; @@ -137,6 +137,12 @@ impl

ServiceRequest

{ } } +impl

Resource for ServiceRequest

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

HttpMessage for ServiceRequest

{ type Stream = P; From bd4124587a1fae9e14a31d5ecaf050f7b454d186 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 4 Mar 2019 13:25:35 -0800 Subject: [PATCH 028/109] provide block_on function for testing purpose --- Cargo.toml | 2 +- src/app.rs | 50 +++++++------ src/extract.rs | 48 ++++--------- src/guard.rs | 74 ++++++++++--------- src/middleware/defaultheaders.rs | 16 ++--- src/responder.rs | 7 +- src/scope.rs | 118 +++++++++++++------------------ src/test.rs | 67 +++++++++++++++--- 8 files changed, 207 insertions(+), 175 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 03b1794f..3b88c300 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ flate2-rust = ["flate2/rust_backend"] actix-codec = "0.1.0" actix-service = "0.3.0" actix-utils = "0.3.0" +actix-rt = "0.1.0" actix-http = { git = "https://github.com/actix/actix-http.git" } actix-router = { git = "https://github.com/actix/actix-net.git" } @@ -69,7 +70,6 @@ brotli2 = { version="^0.3.2", optional = true } flate2 = { version="^1.0.2", optional = true, default-features = false } [dev-dependencies] -actix-rt = "0.1.0" actix-server = { version="0.3.0", features=["ssl"] } actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } diff --git a/src/app.rs b/src/app.rs index 119f1a21..e1479080 100644 --- a/src/app.rs +++ b/src/app.rs @@ -924,85 +924,95 @@ where #[cfg(test)] mod tests { - use actix_http::http::StatusCode; + use actix_http::http::{Method, StatusCode}; use super::*; - use crate::test::TestRequest; - use crate::{HttpResponse, State}; + use crate::test::{block_on, TestRequest}; + use crate::{web, HttpResponse, State}; #[test] fn test_default_resource() { - let mut rt = actix_rt::Runtime::new().unwrap(); - let app = App::new() .resource("/test", |r| r.to(|| HttpResponse::Ok())) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/test").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/blah").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let app = App::new() .resource("/test", |r| r.to(|| HttpResponse::Ok())) + .resource("/test2", |r| { + r.default_resource(|r| r.to(|| HttpResponse::Created())) + .route(web::get().to(|| HttpResponse::Ok())) + }) .default_resource(|r| r.to(|| HttpResponse::MethodNotAllowed())) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/blah").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + + let req = TestRequest::with_uri("/test2").to_request(); + let resp = block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/test2") + .method(Method::POST) + .to_request(); + let resp = block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::CREATED); } #[test] fn test_state() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .state(10usize) .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::default().to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); let app = App::new() .state(10u32) .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::default().to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } #[test] fn test_state_factory() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .state_factory(|| Ok::<_, ()>(10usize)) .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::default().to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); let app = App::new() .state_factory(|| Ok::<_, ()>(10u32)) .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::default().to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } diff --git a/src/extract.rs b/src/extract.rs index 3b5c7e74..7350d7d9 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -1152,7 +1152,7 @@ mod tests { use serde_derive::Deserialize; use super::*; - use crate::test::TestRequest; + use crate::test::{block_on, TestRequest}; #[derive(Deserialize, Debug, PartialEq)] struct Info { @@ -1161,29 +1161,26 @@ mod tests { #[test] fn test_bytes() { - let mut rt = actix_rt::Runtime::new().unwrap(); let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11") .set_payload(Bytes::from_static(b"hello=world")) .to_from(); - let s = rt.block_on(Bytes::from_request(&mut req)).unwrap(); + let s = block_on(Bytes::from_request(&mut req)).unwrap(); assert_eq!(s, Bytes::from_static(b"hello=world")); } #[test] fn test_string() { - let mut rt = actix_rt::Runtime::new().unwrap(); let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11") .set_payload(Bytes::from_static(b"hello=world")) .to_from(); - let s = rt.block_on(String::from_request(&mut req)).unwrap(); + let s = block_on(String::from_request(&mut req)).unwrap(); assert_eq!(s, "hello=world"); } #[test] fn test_form() { - let mut rt = actix_rt::Runtime::new().unwrap(); let mut req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", @@ -1192,13 +1189,12 @@ mod tests { .set_payload(Bytes::from_static(b"hello=world")) .to_from(); - let s = rt.block_on(Form::::from_request(&mut req)).unwrap(); + let s = block_on(Form::::from_request(&mut req)).unwrap(); assert_eq!(s.hello, "world"); } #[test] fn test_option() { - let mut rt = actix_rt::Runtime::new().unwrap(); let mut req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", @@ -1206,9 +1202,7 @@ mod tests { .config(FormConfig::default().limit(4096)) .to_from(); - let r = rt - .block_on(Option::>::from_request(&mut req)) - .unwrap(); + let r = block_on(Option::>::from_request(&mut req)).unwrap(); assert_eq!(r, None); let mut req = TestRequest::with_header( @@ -1219,9 +1213,7 @@ mod tests { .set_payload(Bytes::from_static(b"hello=world")) .to_from(); - let r = rt - .block_on(Option::>::from_request(&mut req)) - .unwrap(); + let r = block_on(Option::>::from_request(&mut req)).unwrap(); assert_eq!( r, Some(Form(Info { @@ -1237,15 +1229,12 @@ mod tests { .set_payload(Bytes::from_static(b"bye=world")) .to_from(); - let r = rt - .block_on(Option::>::from_request(&mut req)) - .unwrap(); + let r = block_on(Option::>::from_request(&mut req)).unwrap(); assert_eq!(r, None); } #[test] fn test_result() { - let mut rt = actix_rt::Runtime::new().unwrap(); let mut req = TestRequest::with_header( header::CONTENT_TYPE, "application/x-www-form-urlencoded", @@ -1254,8 +1243,7 @@ mod tests { .set_payload(Bytes::from_static(b"hello=world")) .to_from(); - let r = rt - .block_on(Result::, Error>::from_request(&mut req)) + let r = block_on(Result::, Error>::from_request(&mut req)) .unwrap() .unwrap(); assert_eq!( @@ -1273,9 +1261,7 @@ mod tests { .set_payload(Bytes::from_static(b"bye=world")) .to_from(); - let r = rt - .block_on(Result::, Error>::from_request(&mut req)) - .unwrap(); + let r = block_on(Result::, Error>::from_request(&mut req)).unwrap(); assert!(r.is_err()); } @@ -1361,25 +1347,19 @@ mod tests { #[test] fn test_tuple_extract() { - let mut rt = actix_rt::Runtime::new().unwrap(); let resource = ResourceDef::new("/{key}/{value}/"); let mut req = TestRequest::with_uri("/name/user1/?id=test").to_from(); resource.match_path(req.match_info_mut()); - let res = rt - .block_on(<(Path<(String, String)>,)>::from_request(&mut req)) - .unwrap(); + let res = block_on(<(Path<(String, String)>,)>::from_request(&mut req)).unwrap(); assert_eq!((res.0).0, "name"); assert_eq!((res.0).1, "user1"); - let res = rt - .block_on( - <(Path<(String, String)>, Path<(String, String)>)>::from_request( - &mut req, - ), - ) - .unwrap(); + let res = block_on( + <(Path<(String, String)>, Path<(String, String)>)>::from_request(&mut req), + ) + .unwrap(); assert_eq!((res.0).0, "name"); assert_eq!((res.0).1, "user1"); assert_eq!((res.1).0, "name"); diff --git a/src/guard.rs b/src/guard.rs index c8948b3b..93b6e132 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -239,8 +239,7 @@ mod tests { #[test] fn test_header() { let req = TestRequest::with_header(header::TRANSFER_ENCODING, "chunked") - .finish() - .into_request(); + .to_http_request(); let pred = Header("transfer-encoding", "chunked"); assert!(pred.check(&req)); @@ -270,44 +269,55 @@ mod tests { #[test] fn test_methods() { - let req = TestRequest::default().finish().into_request(); + let req = TestRequest::default().to_http_request(); let req2 = TestRequest::default() .method(Method::POST) - .finish() - .into_request(); + .to_http_request(); assert!(Get().check(&req)); assert!(!Get().check(&req2)); assert!(Post().check(&req2)); assert!(!Post().check(&req)); - let r = TestRequest::default().method(Method::PUT).finish(); - assert!(Put().check(&r,)); - assert!(!Put().check(&req,)); + let r = TestRequest::default().method(Method::PUT).to_http_request(); + assert!(Put().check(&r)); + assert!(!Put().check(&req)); - let r = TestRequest::default().method(Method::DELETE).finish(); - assert!(Delete().check(&r,)); - assert!(!Delete().check(&req,)); + let r = TestRequest::default() + .method(Method::DELETE) + .to_http_request(); + assert!(Delete().check(&r)); + assert!(!Delete().check(&req)); - let r = TestRequest::default().method(Method::HEAD).finish(); - assert!(Head().check(&r,)); - assert!(!Head().check(&req,)); + let r = TestRequest::default() + .method(Method::HEAD) + .to_http_request(); + assert!(Head().check(&r)); + assert!(!Head().check(&req)); - let r = TestRequest::default().method(Method::OPTIONS).finish(); - assert!(Options().check(&r,)); - assert!(!Options().check(&req,)); + let r = TestRequest::default() + .method(Method::OPTIONS) + .to_http_request(); + assert!(Options().check(&r)); + assert!(!Options().check(&req)); - let r = TestRequest::default().method(Method::CONNECT).finish(); - assert!(Connect().check(&r,)); - assert!(!Connect().check(&req,)); + let r = TestRequest::default() + .method(Method::CONNECT) + .to_http_request(); + assert!(Connect().check(&r)); + assert!(!Connect().check(&req)); - let r = TestRequest::default().method(Method::PATCH).finish(); - assert!(Patch().check(&r,)); - assert!(!Patch().check(&req,)); + let r = TestRequest::default() + .method(Method::PATCH) + .to_http_request(); + assert!(Patch().check(&r)); + assert!(!Patch().check(&req)); - let r = TestRequest::default().method(Method::TRACE).finish(); - assert!(Trace().check(&r,)); - assert!(!Trace().check(&req,)); + let r = TestRequest::default() + .method(Method::TRACE) + .to_http_request(); + assert!(Trace().check(&r)); + assert!(!Trace().check(&req)); } #[test] @@ -316,13 +326,13 @@ mod tests { .method(Method::TRACE) .to_http_request(); - assert!(Not(Get()).check(&r,)); - assert!(!Not(Trace()).check(&r,)); + assert!(Not(Get()).check(&r)); + assert!(!Not(Trace()).check(&r)); - assert!(All(Trace()).and(Trace()).check(&r,)); - assert!(!All(Get()).and(Trace()).check(&r,)); + assert!(All(Trace()).and(Trace()).check(&r)); + assert!(!All(Get()).and(Trace()).check(&r)); - assert!(Any(Get()).or(Trace()).check(&r,)); - assert!(!Any(Get()).or(Get()).check(&r,)); + assert!(Any(Get()).or(Trace()).check(&r)); + assert!(!Any(Get()).or(Get()).check(&r)); } } diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index fa287b28..40bf9f1c 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -138,39 +138,37 @@ mod tests { use actix_service::FnService; use super::*; - use crate::test::TestRequest; + use crate::test::{block_on, TestRequest}; use crate::{HttpResponse, ServiceRequest}; #[test] fn test_default_headers() { - let mut rt = actix_rt::Runtime::new().unwrap(); let mut mw = DefaultHeaders::new().header(CONTENT_TYPE, "0001"); let mut srv = FnService::new(|req: ServiceRequest<_>| { req.into_response(HttpResponse::Ok().finish()) }); - let req = TestRequest::default().finish(); - let resp = rt.block_on(mw.call(req, &mut srv)).unwrap(); + let req = TestRequest::default().to_service(); + let resp = block_on(mw.call(req, &mut srv)).unwrap(); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); - let req = TestRequest::default().finish(); + let req = TestRequest::default().to_service(); let mut srv = FnService::new(|req: ServiceRequest<_>| { req.into_response(HttpResponse::Ok().header(CONTENT_TYPE, "0002").finish()) }); - let resp = rt.block_on(mw.call(req, &mut srv)).unwrap(); + let resp = block_on(mw.call(req, &mut srv)).unwrap(); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0002"); } #[test] fn test_content_type() { - let mut rt = actix_rt::Runtime::new().unwrap(); let mut mw = DefaultHeaders::new().content_type(); let mut srv = FnService::new(|req: ServiceRequest<_>| { req.into_response(HttpResponse::Ok().finish()) }); - let req = TestRequest::default().finish(); - let resp = rt.block_on(mw.call(req, &mut srv)).unwrap(); + let req = TestRequest::default().to_service(); + let resp = block_on(mw.call(req, &mut srv)).unwrap(); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), "application/octet-stream" diff --git a/src/responder.rs b/src/responder.rs index 8e7f66b4..b2fd848f 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -286,19 +286,18 @@ mod tests { #[test] fn test_option_responder() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .resource("/none", |r| r.to(|| -> Option<&'static str> { None })) .resource("/some", |r| r.to(|| Some("some"))) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = TestRequest::block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/none").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = TestRequest::block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let req = TestRequest::with_uri("/some").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = TestRequest::block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); match resp.body() { ResponseBody::Body(Body::Bytes(ref b)) => { diff --git a/src/scope.rs b/src/scope.rs index 2ed18a42..7aeb5041 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -32,14 +32,14 @@ type BoxedResponse = Box>; /// `Path` extractor also is able to extract scope level variable segments. /// /// ```rust -/// use actix_web::{App, HttpResponse}; +/// use actix_web::{web, App, HttpResponse}; /// /// fn main() { /// let app = App::new().scope("/{project_id}/", |scope| { /// scope /// .resource("/path1", |r| r.to(|| HttpResponse::Ok())) -/// .resource("/path2", |r| r.to(|| HttpResponse::Ok())) -/// .resource("/path3", |r| r.to(|| HttpResponse::MethodNotAllowed())) +/// .resource("/path2", |r| r.route(web::get().to(|| HttpResponse::Ok()))) +/// .resource("/path3", |r| r.route(web::head().to(|| HttpResponse::MethodNotAllowed()))) /// }); /// } /// ``` @@ -512,27 +512,25 @@ mod tests { use actix_service::{IntoNewService, NewService, Service}; use bytes::Bytes; - use crate::test::TestRequest; + use crate::test::{block_on, TestRequest}; use crate::{guard, web, App, HttpRequest, HttpResponse}; #[test] fn test_scope() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app", |scope| { scope.resource("/path1", |r| r.to(|| HttpResponse::Ok())) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app/path1").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[test] fn test_scope_root() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app", |scope| { scope @@ -540,58 +538,55 @@ mod tests { .resource("/", |r| r.to(|| HttpResponse::Created())) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/app/").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); } #[test] fn test_scope_root2() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app/", |scope| { scope.resource("", |r| r.to(|| HttpResponse::Ok())) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app").to_request(); - let resp = rt.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("/app/").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[test] fn test_scope_root3() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app/", |scope| { scope.resource("/", |r| r.to(|| HttpResponse::Ok())) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app").to_request(); - let resp = rt.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("/app/").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[test] fn test_scope_route() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("app", |scope| { scope.resource("/path1", |r| { @@ -600,28 +595,27 @@ mod tests { }) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app/path1").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/app/path1") .method(Method::DELETE) .to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/app/path1") .method(Method::POST) .to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[test] fn test_scope_route_without_leading_slash() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("app", |scope| { scope.resource("path1", |r| { @@ -630,28 +624,27 @@ mod tests { }) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app/path1").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/app/path1") .method(Method::DELETE) .to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/app/path1") .method(Method::POST) .to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[test] fn test_scope_guard() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app", |scope| { scope @@ -659,24 +652,23 @@ mod tests { .resource("/path1", |r| r.to(|| HttpResponse::Ok())) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app/path1") .method(Method::POST) .to_request(); - let resp = rt.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("/app/path1") .method(Method::GET) .to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[test] fn test_scope_variable_segment() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/ab-{project}", |scope| { scope.resource("/path1", |r| { @@ -687,10 +679,10 @@ mod tests { }) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/ab-project1/path1").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); match resp.body() { @@ -702,13 +694,12 @@ mod tests { } let req = TestRequest::with_uri("/aa-project1/path1").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[test] fn test_nested_scope() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app", |scope| { scope.nested("/t1", |scope| { @@ -716,16 +707,15 @@ mod tests { }) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app/t1/path1").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); } #[test] fn test_nested_scope_no_slash() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app", |scope| { scope.nested("t1", |scope| { @@ -733,16 +723,15 @@ mod tests { }) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app/t1/path1").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); } #[test] fn test_nested_scope_root() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app", |scope| { scope.nested("/t1", |scope| { @@ -752,20 +741,19 @@ mod tests { }) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app/t1").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/app/t1/").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); } #[test] fn test_nested_scope_filter() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app", |scope| { scope.nested("/t1", |scope| { @@ -775,24 +763,23 @@ mod tests { }) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app/t1/path1") .method(Method::POST) .to_request(); - let resp = rt.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("/app/t1/path1") .method(Method::GET) .to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[test] fn test_nested_scope_with_variable_segment() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app", |scope| { scope.nested("/{project_id}", |scope| { @@ -807,10 +794,10 @@ mod tests { }) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app/project_1/path1").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); match resp.body() { @@ -824,7 +811,6 @@ mod tests { #[test] fn test_nested2_scope_with_variable_segment() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app", |scope| { scope.nested("/{project}", |scope| { @@ -842,10 +828,10 @@ mod tests { }) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app/test/1/path1").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); match resp.body() { @@ -857,13 +843,12 @@ mod tests { } let req = TestRequest::with_uri("/app/test/1/path2").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[test] fn test_default_resource() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app", |scope| { scope @@ -871,20 +856,19 @@ mod tests { .default_resource(|r| r.to(|| HttpResponse::BadRequest())) }) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/app/path2").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_uri("/path2").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[test] fn test_default_resource_propagation() { - let mut rt = actix_rt::Runtime::new().unwrap(); let app = App::new() .scope("/app1", |scope| { scope.default_resource(|r| r.to(|| HttpResponse::BadRequest())) @@ -892,18 +876,18 @@ mod tests { .scope("/app2", |scope| scope) .default_resource(|r| r.to(|| HttpResponse::MethodNotAllowed())) .into_new_service(); - let mut srv = rt.block_on(app.new_service(&())).unwrap(); + let mut srv = block_on(app.new_service(&())).unwrap(); let req = TestRequest::with_uri("/non-exist").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); let req = TestRequest::with_uri("/app1/non-exist").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_uri("/app2/non-exist").to_request(); - let resp = rt.block_on(srv.call(req)).unwrap(); + let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); } } diff --git a/src/test.rs b/src/test.rs index 684817ec..7ceedacc 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,4 +1,5 @@ //! Various helpers for Actix applications to use during testing. +use std::cell::RefCell; use std::rc::Rc; use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; @@ -6,16 +7,47 @@ use actix_http::http::{HttpTryFrom, Method, Version}; use actix_http::test::TestRequest as HttpTestRequest; use actix_http::{Extensions, PayloadStream, Request}; use actix_router::{Path, Url}; +use actix_rt::Runtime; use bytes::Bytes; +use futures::Future; use crate::request::HttpRequest; use crate::service::{ServiceFromRequest, ServiceRequest}; -/// Test `Request` builder +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)) +} + +/// Test `Request` builder. +/// +/// For unit testing, actix provides a request builder type and a simple handler runner. TestRequest implements a builder-like pattern. +/// You can generate various types of request via TestRequest's methods: +/// * `TestRequest::to_request` creates `actix_http::Request` instance. +/// * `TestRequest::to_service` creates `ServiceRequest` instance, which is used for testing middlewares and chain adapters. +/// * `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 -/// # use actix_web::*; -/// use actix_web::test::TestRequest; +/// use actix_web::test; /// /// fn index(req: HttpRequest) -> HttpResponse { /// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) { @@ -26,12 +58,14 @@ use crate::service::{ServiceFromRequest, ServiceRequest}; /// } /// /// fn main() { -/// let resp = TestRequest::with_header("content-type", "text/plain") -/// .run(&index) -/// .unwrap(); +/// let req = test::TestRequest::with_header("content-type", "text/plain") +/// .to_http_request(); +/// +/// let resp = test::block_on(index(req)); /// assert_eq!(resp.status(), StatusCode::OK); /// -/// let resp = TestRequest::default().run(&index).unwrap(); +/// let req = test::TestRequest::default().to_http_request(); +/// let resp = test::block_on(index(req)); /// assert_eq!(resp.status(), StatusCode::BAD_REQUEST); /// } /// ``` @@ -125,7 +159,7 @@ impl TestRequest { } /// Complete request creation and generate `ServiceRequest` instance - pub fn finish(mut self) -> ServiceRequest { + pub fn to_service(mut self) -> ServiceRequest { let req = self.req.finish(); ServiceRequest::new( @@ -163,4 +197,21 @@ impl TestRequest { ); ServiceFromRequest::new(req, None) } + + /// 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) + } } From 2e79562c9d2d16a376e321ff42e662cdc905d0c4 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 4 Mar 2019 16:29:03 -0800 Subject: [PATCH 029/109] add HttpServer type --- Cargo.toml | 20 +- examples/basic.rs | 54 ++--- src/lib.rs | 2 + src/server.rs | 521 +++++++++++++++++++++++++++++++++++++++++ staticfiles/Cargo.toml | 3 +- 5 files changed, 569 insertions(+), 31 deletions(-) create mode 100644 src/server.rs diff --git a/Cargo.toml b/Cargo.toml index 3b88c300..1bc3af3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,9 @@ members = [ "staticfiles", ] +[package.metadata.docs.rs] +features = ["ssl", "tls", "rust-tls"] + [features] default = ["brotli", "flate2-c"] @@ -42,6 +45,15 @@ flate2-c = ["flate2/miniz-sys"] # rust backend for flate2 crate flate2-rust = ["flate2/rust_backend"] +# tls +tls = ["native-tls", "actix-server/ssl"] + +# openssl +ssl = ["openssl", "actix-server/ssl"] + +# rustls +# rust-tls = ["rustls", "actix-server/rustls"] + [dependencies] actix-codec = "0.1.0" actix-service = "0.3.0" @@ -50,6 +62,7 @@ actix-rt = "0.1.0" actix-http = { git = "https://github.com/actix/actix-http.git" } actix-router = { git = "https://github.com/actix/actix-net.git" } +actix-server = { git = "https://github.com/actix/actix-net.git" } bytes = "0.4" derive_more = "0.14" @@ -58,6 +71,7 @@ futures = "0.1" log = "0.4" lazy_static = "1.2" mime = "0.3" +net2 = "0.2.33" num_cpus = "1.10" parking_lot = "0.7" serde = "1.0" @@ -69,8 +83,12 @@ threadpool = "1.7" brotli2 = { version="^0.3.2", optional = true } flate2 = { version="^1.0.2", optional = true, default-features = false } +# ssl support +native-tls = { version="0.2", optional = true } +openssl = { version="0.10", optional = true } +# rustls = { version = "^0.15", optional = true } + [dev-dependencies] -actix-server = { version="0.3.0", features=["ssl"] } actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } rand = "0.6" diff --git a/examples/basic.rs b/examples/basic.rs index 886efb7b..3cb07959 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,8 +1,9 @@ use futures::IntoFuture; -use actix_http::{h1, http::Method, Response}; -use actix_server::Server; -use actix_web::{middleware, web, App, Error, HttpRequest, Resource, Route}; +use actix_web::{ + http::Method, middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer, + Resource, Route, +}; fn index(req: HttpRequest) -> &'static str { println!("REQ: {:?}", req); @@ -18,39 +19,34 @@ fn no_params() -> &'static str { "Hello world!\r\n" } -fn main() { +fn main() -> std::io::Result<()> { ::std::env::set_var("RUST_LOG", "actix_server=info,actix_web2=info"); env_logger::init(); let sys = actix_rt::System::new("hello-world"); - Server::build() - .bind("test", "127.0.0.1:8080", || { - h1::H1Service::new( - App::new() + HttpServer::new(|| { + App::new() + .middleware(middleware::DefaultHeaders::new().header("X-Version", "0.2")) + .middleware(middleware::Compress::default()) + .resource("/resource1/index.html", |r| r.route(web::get().to(index))) + .service( + "/resource2/index.html", + Resource::new() .middleware( - middleware::DefaultHeaders::new().header("X-Version", "0.2"), + middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"), ) - .middleware(middleware::Compress::default()) - .resource("/resource1/index.html", |r| r.route(web::get().to(index))) - .service( - "/resource2/index.html", - Resource::new() - .middleware( - middleware::DefaultHeaders::new() - .header("X-Version-R2", "0.3"), - ) - .default_resource(|r| { - r.route(Route::new().to(|| Response::MethodNotAllowed())) - }) - .route(web::method(Method::GET).to_async(index_async)), - ) - .service("/test1.html", Resource::new().to(|| "Test\r\n")) - .service("/", Resource::new().to(no_params)), + .default_resource(|r| { + r.route(Route::new().to(|| HttpResponse::MethodNotAllowed())) + }) + .route(web::method(Method::GET).to_async(index_async)), ) - }) - .unwrap() - .workers(1) - .start(); + .service("/test1.html", Resource::new().to(|| "Test\r\n")) + .service("/", Resource::new().to(no_params)) + }) + .bind("127.0.0.1:8080")? + .workers(1) + .start(); let _ = sys.run(); + Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 18a6de06..f21c5e43 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ mod resource; mod responder; mod route; mod scope; +mod server; mod service; mod state; pub mod test; @@ -27,6 +28,7 @@ pub use crate::resource::Resource; pub use crate::responder::{Either, Responder}; pub use crate::route::Route; pub use crate::scope::Scope; +pub use crate::server::HttpServer; pub use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; pub use crate::state::State; diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 00000000..95cab2b0 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,521 @@ +use std::marker::PhantomData; +use std::sync::Arc; +use std::{fmt, io, net}; + +use actix_http::{body::MessageBody, h1, KeepAlive, Request, Response, ServiceConfig}; +use actix_rt::System; +use actix_server::{Server, ServerBuilder}; +use actix_service::{IntoNewService, NewService}; +use parking_lot::Mutex; + +use net2::TcpBuilder; + +#[cfg(feature = "tls")] +use native_tls::TlsAcceptor; + +#[cfg(feature = "ssl")] +use openssl::ssl::{SslAcceptor, SslAcceptorBuilder}; + +struct Socket { + scheme: &'static str, + addr: net::SocketAddr, +} + +struct Config { + keep_alive: KeepAlive, + client_timeout: u64, + client_shutdown: u64, +} + +/// An HTTP Server. +/// +/// Create new http server with application factory. +/// +/// ```rust +/// use std::io; +/// use actix_web::{App, HttpResponse, HttpServer}; +/// +/// fn main() -> io::Result<()> { +/// let sys = actix_rt::System::new("example"); // <- create Actix runtime +/// +/// HttpServer::new( +/// || App::new() +/// .resource("/", |r| r.to(|| HttpResponse::Ok()))) +/// .bind("127.0.0.1:59090")? +/// .start(); +/// +/// # actix_rt::System::current().stop(); +/// sys.run(); +/// Ok(()) +/// } +/// ``` +pub struct HttpServer +where + F: Fn() -> I + Send + Clone + 'static, + I: IntoNewService, + S: NewService, + S::Error: fmt::Debug, + S::Response: Into>, + S::Service: 'static, + B: MessageBody, +{ + pub(super) factory: F, + pub(super) host: Option, + config: Arc>, + backlog: i32, + sockets: Vec, + builder: Option, + _t: PhantomData<(S, B)>, +} + +impl HttpServer +where + F: Fn() -> I + Send + Clone + 'static, + I: IntoNewService, + S: NewService, + S::Error: fmt::Debug, + S::Response: Into>, + S::Service: 'static, + B: MessageBody, +{ + /// Create new http server with application factory + pub fn new(factory: F) -> Self { + HttpServer { + factory, + host: None, + backlog: 2048, + config: Arc::new(Mutex::new(Config { + keep_alive: KeepAlive::Timeout(5), + client_timeout: 5000, + client_shutdown: 5000, + })), + sockets: Vec::new(), + builder: Some(ServerBuilder::default()), + _t: PhantomData, + } + } + + /// Set number of workers to start. + /// + /// By default http server uses number of available logical cpu as threads + /// count. + pub fn workers(mut self, num: usize) -> Self { + self.builder = Some(self.builder.take().unwrap().workers(num)); + self + } + + /// Set the maximum number of pending connections. + /// + /// This refers to the number of clients that can be waiting to be served. + /// Exceeding this number results in the client getting an error when + /// attempting to connect. It should only affect servers under significant + /// load. + /// + /// Generally set in the 64-2048 range. Default value is 2048. + /// + /// This method should be called before `bind()` method call. + pub fn backlog(mut self, num: i32) -> Self { + self.backlog = num; + self + } + + /// Sets the maximum per-worker number of concurrent connections. + /// + /// All socket listeners will stop accepting connections when this limit is reached + /// for each worker. + /// + /// By default max connections is set to a 25k. + pub fn maxconn(mut self, num: usize) -> Self { + self.builder = Some(self.builder.take().unwrap().maxconn(num)); + self + } + + /// Sets the maximum per-worker concurrent connection establish process. + /// + /// All listeners will stop accepting connections when this limit is reached. It + /// can be used to limit the global SSL CPU usage. + /// + /// By default max connections is set to a 256. + pub fn maxconnrate(mut self, num: usize) -> Self { + self.builder = Some(self.builder.take().unwrap().maxconnrate(num)); + self + } + + /// Set server keep-alive setting. + /// + /// By default keep alive is set to a 5 seconds. + pub fn keep_alive>(self, val: T) -> Self { + self.config.lock().keep_alive = val.into(); + self + } + + /// Set server client timeout in milliseconds for first request. + /// + /// Defines a timeout for reading client request header. If a client does not transmit + /// the entire set headers within this time, the request is terminated with + /// the 408 (Request Time-out) error. + /// + /// To disable timeout set value to 0. + /// + /// By default client timeout is set to 5000 milliseconds. + pub fn client_timeout(self, val: u64) -> Self { + self.config.lock().client_timeout = val; + self + } + + /// Set server connection shutdown timeout in milliseconds. + /// + /// Defines a timeout for shutdown connection. If a shutdown procedure does not complete + /// within this time, the request is dropped. + /// + /// To disable timeout set value to 0. + /// + /// By default client timeout is set to 5000 milliseconds. + pub fn client_shutdown(self, val: u64) -> Self { + self.config.lock().client_shutdown = val; + self + } + + /// Set server host name. + /// + /// Host name is used by application router aa a hostname for url + /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. + /// html#method.host) documentation for more information. + pub fn server_hostname(mut self, val: String) -> Self { + self.host = Some(val); + self + } + + /// Stop actix system. + pub fn system_exit(mut self) -> Self { + self.builder = Some(self.builder.take().unwrap().system_exit()); + self + } + + /// Disable signal handling + pub fn disable_signals(mut self) -> Self { + self.builder = Some(self.builder.take().unwrap().disable_signals()); + self + } + + /// Timeout for graceful workers shutdown. + /// + /// After receiving a stop signal, workers have this much time to finish + /// serving requests. Workers still alive after the timeout are force + /// dropped. + /// + /// By default shutdown timeout sets to 30 seconds. + pub fn shutdown_timeout(mut self, sec: u16) -> Self { + self.builder = Some(self.builder.take().unwrap().shutdown_timeout(sec)); + self + } + + /// Get addresses of bound sockets. + pub fn addrs(&self) -> Vec { + self.sockets.iter().map(|s| s.addr).collect() + } + + /// Get addresses of bound sockets and the scheme for it. + /// + /// This is useful when the server is bound from different sources + /// with some sockets listening on http and some listening on https + /// and the user should be presented with an enumeration of which + /// socket requires which protocol. + pub fn addrs_with_scheme(&self) -> Vec<(net::SocketAddr, &str)> { + self.sockets.iter().map(|s| (s.addr, s.scheme)).collect() + } + + /// Use listener for accepting incoming connection requests + /// + /// HttpServer does not change any configuration for TcpListener, + /// it needs to be configured before passing it to listen() method. + pub fn listen(mut self, lst: net::TcpListener) -> Self { + let cfg = self.config.clone(); + let factory = self.factory.clone(); + let addr = lst.local_addr().unwrap(); + self.sockets.push(Socket { + addr, + scheme: "http", + }); + + self.builder = Some(self.builder.take().unwrap().listen( + format!("actix-web-service-{}", addr), + lst, + move || { + let c = cfg.lock(); + let service_config = + ServiceConfig::new(c.keep_alive, c.client_timeout, 0); + h1::H1Service::with_config(service_config, factory()) + }, + )); + + self + } + + #[cfg(feature = "tls")] + /// Use listener for accepting incoming tls connection requests + /// + /// HttpServer does not change any configuration for TcpListener, + /// it needs to be configured before passing it to listen() method. + pub fn listen_tls(self, lst: net::TcpListener, acceptor: TlsAcceptor) -> Self { + use actix_net::service::NewServiceExt; + + self.listen_with(lst, move || { + ssl::NativeTlsAcceptor::new(acceptor.clone()).map_err(|_| ()) + }) + } + + #[cfg(feature = "ssl")] + /// Use listener for accepting incoming tls connection requests + /// + /// This method sets alpn protocols to "h2" and "http/1.1" + pub fn listen_ssl( + mut self, + lst: net::TcpListener, + builder: SslAcceptorBuilder, + ) -> io::Result { + self.listen_ssl_inner(lst, openssl_acceptor(builder)?); + Ok(self) + } + + #[cfg(feature = "ssl")] + fn listen_ssl_inner(&mut self, lst: net::TcpListener, acceptor: SslAcceptor) { + use actix_server::ssl::{OpensslAcceptor, SslError}; + + let acceptor = OpensslAcceptor::new(acceptor); + let factory = self.factory.clone(); + let cfg = self.config.clone(); + let addr = lst.local_addr().unwrap(); + self.sockets.push(Socket { + addr, + scheme: "http", + }); + + self.builder = Some(self.builder.take().unwrap().listen( + format!("actix-web-service-{}", addr), + lst, + move || { + let c = cfg.lock(); + let service_config = + ServiceConfig::new(c.keep_alive, c.client_timeout, c.client_timeout); + acceptor.clone().map_err(|e| SslError::Ssl(e)).and_then( + h1::H1Service::with_config(service_config, factory()) + .map_err(|e| SslError::Service(e)) + .map_init_err(|_| ()), + ) + }, + )); + } + + #[cfg(feature = "rust-tls")] + /// Use listener for accepting incoming tls connection requests + /// + /// This method sets alpn protocols to "h2" and "http/1.1" + pub fn listen_rustls(self, lst: net::TcpListener, config: ServerConfig) -> Self { + use super::{RustlsAcceptor, ServerFlags}; + use actix_net::service::NewServiceExt; + + self.listen_with(lst, move || { + RustlsAcceptor::with_flags(config.clone(), flags).map_err(|_| ()) + }) + } + + /// The socket address to bind + /// + /// To bind multiple addresses this method can be called multiple times. + pub fn bind(mut self, addr: A) -> io::Result { + let sockets = self.bind2(addr)?; + + for lst in sockets { + self = self.listen(lst); + } + + Ok(self) + } + + fn bind2( + &self, + addr: A, + ) -> io::Result> { + let mut err = None; + let mut succ = false; + let mut sockets = Vec::new(); + for addr in addr.to_socket_addrs()? { + match create_tcp_listener(addr, self.backlog) { + Ok(lst) => { + succ = true; + sockets.push(lst); + } + Err(e) => err = Some(e), + } + } + + if !succ { + if let Some(e) = err.take() { + Err(e) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + "Can not bind to address.", + )) + } + } else { + Ok(sockets) + } + } + + #[cfg(feature = "tls")] + /// The ssl socket address to bind + /// + /// To bind multiple addresses this method can be called multiple times. + pub fn bind_tls( + self, + addr: A, + acceptor: TlsAcceptor, + ) -> io::Result { + use actix_net::service::NewServiceExt; + use actix_net::ssl::NativeTlsAcceptor; + + self.bind_with(addr, move || { + NativeTlsAcceptor::new(acceptor.clone()).map_err(|_| ()) + }) + } + + #[cfg(feature = "ssl")] + /// Start listening for incoming tls connections. + /// + /// This method sets alpn protocols to "h2" and "http/1.1" + pub fn bind_ssl( + mut self, + addr: A, + builder: SslAcceptorBuilder, + ) -> io::Result + where + A: net::ToSocketAddrs, + { + let sockets = self.bind2(addr)?; + let acceptor = openssl_acceptor(builder)?; + + for lst in sockets { + self.listen_ssl_inner(lst, acceptor.clone()); + } + + Ok(self) + } + + #[cfg(feature = "rust-tls")] + /// Start listening for incoming tls connections. + /// + /// This method sets alpn protocols to "h2" and "http/1.1" + pub fn bind_rustls( + self, + addr: A, + builder: ServerConfig, + ) -> io::Result { + use super::{RustlsAcceptor, ServerFlags}; + use actix_net::service::NewServiceExt; + + // alpn support + let flags = if self.no_http2 { + ServerFlags::HTTP1 + } else { + ServerFlags::HTTP1 | ServerFlags::HTTP2 + }; + + self.bind_with(addr, move || { + RustlsAcceptor::with_flags(builder.clone(), flags).map_err(|_| ()) + }) + } +} + +impl HttpServer +where + F: Fn() -> I + Send + Clone + 'static, + I: IntoNewService, + S: NewService, + S::Error: fmt::Debug, + S::Response: Into>, + S::Service: 'static, + B: MessageBody, +{ + /// Start listening for incoming connections. + /// + /// This method starts number of http workers in separate threads. + /// For each address this method starts separate thread which does + /// `accept()` in a loop. + /// + /// This methods panics if no socket address can be bound or an `Actix` system is not yet + /// configured. + /// + /// ```rust + /// use actix_web::{App, HttpResponse, HttpServer}; + /// + /// fn main() { + /// let sys = actix_rt::System::new("example"); // <- create Actix system + /// + /// HttpServer::new(|| App::new().resource("/", |r| r.to(|| HttpResponse::Ok()))) + /// .bind("127.0.0.1:0") + /// .expect("Can not bind to 127.0.0.1:0") + /// .start(); + /// # actix_rt::System::current().stop(); + /// sys.run(); // <- Run actix system, this method starts all async processes + /// } + /// ``` + pub fn start(mut self) -> Server { + self.builder.take().unwrap().start() + } + + /// Spawn new thread and start listening for incoming connections. + /// + /// This method spawns new thread and starts new actix system. Other than + /// that it is similar to `start()` method. This method blocks. + /// + /// This methods panics if no socket addresses get bound. + /// + /// ```rust,ignore + /// use actix_web::{App, HttpResponse, HttpServer}; + /// + /// fn main() { + /// HttpServer::new(|| App::new().resource("/", |r| r.to(|| HttpResponse::Ok()))) + /// .bind("127.0.0.1:0") + /// .expect("Can not bind to 127.0.0.1:0") + /// .run(); + /// } + /// ``` + pub fn run(self) { + let sys = System::new("http-server"); + self.start(); + sys.run(); + } +} + +fn create_tcp_listener( + addr: net::SocketAddr, + backlog: i32, +) -> io::Result { + let builder = match addr { + net::SocketAddr::V4(_) => TcpBuilder::new_v4()?, + net::SocketAddr::V6(_) => TcpBuilder::new_v6()?, + }; + builder.reuse_address(true)?; + builder.bind(addr)?; + Ok(builder.listen(backlog)?) +} + +#[cfg(feature = "ssl")] +/// Configure `SslAcceptorBuilder` with custom server flags. +fn openssl_acceptor(mut builder: SslAcceptorBuilder) -> io::Result { + use openssl::ssl::AlpnError; + + builder.set_alpn_select_callback(|_, protos| { + const H2: &[u8] = b"\x02h2"; + if protos.windows(3).any(|window| window == H2) { + Ok(b"h2") + } else { + Err(AlpnError::NOACK) + } + }); + builder.set_alpn_protos(b"\x08http/1.1\x02h2")?; + + Ok(builder.build()) +} diff --git a/staticfiles/Cargo.toml b/staticfiles/Cargo.toml index c2516302..0aa58970 100644 --- a/staticfiles/Cargo.toml +++ b/staticfiles/Cargo.toml @@ -20,7 +20,7 @@ path = "src/lib.rs" [dependencies] actix-web = { path=".." } actix-http = { git = "https://github.com/actix/actix-http.git" } -actix-service = { git = "https://github.com/actix/actix-net.git" } +actix-service = "0.3.0" bytes = "0.4" futures = "0.1" @@ -34,6 +34,7 @@ v_htmlescape = "0.4" [dev-dependencies] actix-rt = "0.1.0" #actix-server = { version="0.2", features=["ssl"] } +actix-web = { path="..", features=["ssl"] } actix-server = { git = "https://github.com/actix/actix-net.git", features=["ssl"] } actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } From b6fe1dacf2c8f547138cbef91c3b8fc4330429c6 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 4 Mar 2019 21:37:57 -0800 Subject: [PATCH 030/109] update middleware impl --- Cargo.toml | 7 +++- src/app.rs | 16 ++++---- src/middleware/compress.rs | 51 +++++++++++++++-------- src/middleware/defaultheaders.rs | 70 ++++++++++++++++++++++---------- src/middleware/mod.rs | 45 -------------------- src/resource.rs | 8 ++-- src/scope.rs | 8 ++-- 7 files changed, 102 insertions(+), 103 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1bc3af3b..0b4ad38a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,8 +56,8 @@ ssl = ["openssl", "actix-server/ssl"] [dependencies] actix-codec = "0.1.0" -actix-service = "0.3.0" -actix-utils = "0.3.0" +actix-service = "0.3.2" +actix-utils = "0.3.1" actix-rt = "0.1.0" actix-http = { git = "https://github.com/actix/actix-http.git" } @@ -99,3 +99,6 @@ serde_derive = "1.0" lto = true opt-level = 3 codegen-units = 1 + +[patch.crates-io] +actix-service = { git = "https://github.com/actix/actix-net.git" } diff --git a/src/app.rs b/src/app.rs index e1479080..8336fcca 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,8 +7,8 @@ use actix_http::{Extensions, PayloadStream, Request, Response}; use actix_router::{Path, ResourceDef, ResourceInfo, Router, Url}; use actix_service::boxed::{self, BoxedNewService, BoxedService}; use actix_service::{ - AndThenNewService, ApplyNewService, IntoNewService, IntoNewTransform, NewService, - NewTransform, Service, + AndThenNewService, ApplyTransform, IntoNewService, IntoTransform, NewService, + Service, Transform, }; use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; @@ -237,17 +237,17 @@ where >, > where - M: NewTransform< + M: Transform< AppRouting

, Request = ServiceRequest

, Response = ServiceResponse, Error = (), InitError = (), >, - F: IntoNewTransform>, + F: IntoTransform>, { let fref = Rc::new(RefCell::new(None)); - let endpoint = ApplyNewService::new(mw, AppEntry::new(fref.clone())); + let endpoint = ApplyTransform::new(mw, AppEntry::new(fref.clone())); AppRouter { endpoint, chain: self.chain, @@ -480,7 +480,7 @@ where >, > where - M: NewTransform< + M: Transform< T::Service, Request = ServiceRequest

, Response = ServiceResponse, @@ -488,9 +488,9 @@ where InitError = (), >, B1: MessageBody, - F: IntoNewTransform, + F: IntoTransform, { - let endpoint = ApplyNewService::new(mw, self.endpoint); + let endpoint = ApplyTransform::new(mw, self.endpoint); AppRouter { endpoint, chain: self.chain, diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 5d5586cf..b95553cb 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -8,8 +8,9 @@ use actix_http::http::header::{ }; use actix_http::http::{HttpTryFrom, StatusCode}; use actix_http::{Error, Head, ResponseHead}; -use actix_service::{IntoNewTransform, Service, Transform}; +use actix_service::{Service, Transform}; use bytes::{Bytes, BytesMut}; +use futures::future::{ok, FutureResult}; use futures::{Async, Future, Poll}; use log::trace; @@ -18,7 +19,6 @@ use brotli2::write::BrotliEncoder; #[cfg(feature = "flate2")] use flate2::write::{GzEncoder, ZlibEncoder}; -use crate::middleware::MiddlewareFactory; use crate::service::{ServiceRequest, ServiceResponse}; #[derive(Debug, Clone)] @@ -37,6 +37,33 @@ impl Default for Compress { } impl Transform for Compress +where + P: 'static, + B: MessageBody, + S: Service, Response = ServiceResponse>, + S::Future: 'static, +{ + type Request = ServiceRequest

; + type Response = ServiceResponse>; + type Error = S::Error; + type InitError = (); + type Transform = CompressMiddleware; + type Future = FutureResult; + + fn new_transform(&self, service: S) -> Self::Future { + ok(CompressMiddleware { + service, + encoding: self.0, + }) + } +} + +pub struct CompressMiddleware { + service: S, + encoding: ContentEncoding, +} + +impl Service for CompressMiddleware where P: 'static, B: MessageBody, @@ -49,14 +76,14 @@ where type Future = CompressResponse; fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) + self.service.poll_ready() } - fn call(&mut self, req: ServiceRequest

, srv: &mut S) -> Self::Future { + fn call(&mut self, req: ServiceRequest

) -> Self::Future { // negotiate content-encoding let encoding = if let Some(val) = req.headers.get(ACCEPT_ENCODING) { if let Ok(enc) = val.to_str() { - AcceptEncoding::parse(enc, self.0) + AcceptEncoding::parse(enc, self.encoding) } else { ContentEncoding::Identity } @@ -66,7 +93,7 @@ where CompressResponse { encoding, - fut: srv.call(req), + fut: self.service.call(req), } } } @@ -102,18 +129,6 @@ where } } -impl IntoNewTransform, S> for Compress -where - P: 'static, - B: MessageBody, - S: Service, Response = ServiceResponse>, - S::Future: 'static, -{ - fn into_new_transform(self) -> MiddlewareFactory { - MiddlewareFactory::new(self) - } -} - enum EncoderBody { Body(B), Other(Box), diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index 40bf9f1c..2bd1d5d4 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -3,10 +3,10 @@ use std::rc::Rc; use actix_http::http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; use actix_http::http::{HeaderMap, HttpTryFrom}; -use actix_service::{IntoNewTransform, Service, Transform}; -use futures::{Async, Future, Poll}; +use actix_service::{Service, Transform}; +use futures::future::{ok, FutureResult}; +use futures::{Future, Poll}; -use crate::middleware::MiddlewareFactory; use crate::service::{ServiceRequest, ServiceResponse}; /// `Middleware` for setting default response headers. @@ -84,35 +84,49 @@ impl DefaultHeaders { } } -impl IntoNewTransform, S> - for DefaultHeaders +impl Transform for DefaultHeaders where - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, { - fn into_new_transform(self) -> MiddlewareFactory { - MiddlewareFactory::new(self) + type Request = ServiceRequest

; + type Response = ServiceResponse; + type Error = S::Error; + type InitError = (); + type Transform = DefaultHeadersMiddleware; + type Future = FutureResult; + + fn new_transform(&self, service: S) -> Self::Future { + ok(DefaultHeadersMiddleware { + service, + inner: self.inner.clone(), + }) } } -impl Transform for DefaultHeaders +pub struct DefaultHeadersMiddleware { + service: S, + inner: Rc, +} + +impl Service for DefaultHeadersMiddleware where - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: '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> { - Ok(Async::Ready(())) + self.service.poll_ready() } - fn call(&mut self, req: ServiceRequest, srv: &mut S) -> Self::Future { + fn call(&mut self, req: ServiceRequest

) -> Self::Future { let inner = self.inner.clone(); - Box::new(srv.call(req).map(move |mut res| { + 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) { @@ -143,32 +157,44 @@ mod tests { #[test] fn test_default_headers() { - let mut mw = DefaultHeaders::new().header(CONTENT_TYPE, "0001"); - let mut srv = FnService::new(|req: ServiceRequest<_>| { + let srv = FnService::new(|req: ServiceRequest<_>| { req.into_response(HttpResponse::Ok().finish()) }); + let mut mw = block_on( + DefaultHeaders::new() + .header(CONTENT_TYPE, "0001") + .new_transform(srv), + ) + .unwrap(); let req = TestRequest::default().to_service(); - let resp = block_on(mw.call(req, &mut srv)).unwrap(); + let resp = block_on(mw.call(req)).unwrap(); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); let req = TestRequest::default().to_service(); - let mut srv = FnService::new(|req: ServiceRequest<_>| { + let srv = FnService::new(|req: ServiceRequest<_>| { req.into_response(HttpResponse::Ok().header(CONTENT_TYPE, "0002").finish()) }); - let resp = block_on(mw.call(req, &mut srv)).unwrap(); + let mut mw = block_on( + DefaultHeaders::new() + .header(CONTENT_TYPE, "0001") + .new_transform(srv), + ) + .unwrap(); + let resp = block_on(mw.call(req)).unwrap(); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0002"); } #[test] fn test_content_type() { - let mut mw = DefaultHeaders::new().content_type(); - let mut srv = FnService::new(|req: ServiceRequest<_>| { + let srv = FnService::new(|req: ServiceRequest<_>| { req.into_response(HttpResponse::Ok().finish()) }); + let mut mw = + block_on(DefaultHeaders::new().content_type().new_transform(srv)).unwrap(); let req = TestRequest::default().to_service(); - let resp = block_on(mw.call(req, &mut srv)).unwrap(); + let resp = block_on(mw.call(req)).unwrap(); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), "application/octet-stream" diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 85127ee2..fc992302 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -1,8 +1,3 @@ -use std::marker::PhantomData; - -use actix_service::{NewTransform, Service, Transform}; -use futures::future::{ok, FutureResult}; - #[cfg(any(feature = "brotli", feature = "flate2"))] mod compress; #[cfg(any(feature = "brotli", feature = "flate2"))] @@ -10,43 +5,3 @@ pub use self::compress::Compress; mod defaultheaders; pub use self::defaultheaders::DefaultHeaders; - -/// Helper for middleware service factory -pub struct MiddlewareFactory -where - T: Transform + Clone, - S: Service, -{ - tr: T, - _t: PhantomData, -} - -impl MiddlewareFactory -where - T: Transform + Clone, - S: Service, -{ - pub fn new(tr: T) -> Self { - MiddlewareFactory { - tr, - _t: PhantomData, - } - } -} - -impl NewTransform for MiddlewareFactory -where - T: Transform + Clone, - S: Service, -{ - type Request = T::Request; - type Response = T::Response; - type Error = T::Error; - type Transform = T; - type InitError = (); - type Future = FutureResult; - - fn new_transform(&self, _: &C) -> Self::Future { - ok(self.tr.clone()) - } -} diff --git a/src/resource.rs b/src/resource.rs index 342d801d..755b8d07 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use actix_http::{Error, Response}; use actix_service::boxed::{self, BoxedNewService, BoxedService}; use actix_service::{ - ApplyNewService, IntoNewService, IntoNewTransform, NewService, NewTransform, Service, + ApplyTransform, IntoNewService, IntoTransform, NewService, Service, Transform, }; use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; @@ -194,16 +194,16 @@ where >, > where - M: NewTransform< + M: Transform< T::Service, Request = ServiceRequest

, Response = ServiceResponse, Error = (), InitError = (), >, - F: IntoNewTransform, + F: IntoTransform, { - let endpoint = ApplyNewService::new(mw, self.endpoint); + let endpoint = ApplyTransform::new(mw, self.endpoint); Resource { endpoint, routes: self.routes, diff --git a/src/scope.rs b/src/scope.rs index 7aeb5041..b255ac14 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -5,7 +5,7 @@ use actix_http::Response; use actix_router::{ResourceDef, ResourceInfo, Router}; use actix_service::boxed::{self, BoxedNewService, BoxedService}; use actix_service::{ - ApplyNewService, IntoNewService, IntoNewTransform, NewService, NewTransform, Service, + ApplyTransform, IntoNewService, IntoTransform, NewService, Service, Transform, }; use futures::future::{ok, Either, Future, FutureResult}; use futures::{Async, Poll}; @@ -251,16 +251,16 @@ where >, > where - M: NewTransform< + M: Transform< T::Service, Request = ServiceRequest

, Response = ServiceResponse, Error = (), InitError = (), >, - F: IntoNewTransform, + F: IntoTransform, { - let endpoint = ApplyNewService::new(mw, self.endpoint); + let endpoint = ApplyTransform::new(mw, self.endpoint); Scope { endpoint, rdef: self.rdef, From 03248028a9a7f140a168fdc9a2797ee81210cec7 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 10:08:08 -0800 Subject: [PATCH 031/109] update actix-service --- Cargo.toml | 15 ++++- src/app.rs | 111 ++++++++++++------------------- src/handler.rs | 18 ++--- src/middleware/compress.rs | 14 ++-- src/middleware/defaultheaders.rs | 10 ++- src/middleware/mod.rs | 3 + src/resource.rs | 30 ++++----- src/route.rs | 47 +++++++------ src/scope.rs | 25 +++---- src/server.rs | 12 ++-- src/service.rs | 6 +- 11 files changed, 132 insertions(+), 159 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0b4ad38a..f473ac55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ members = [ ] [package.metadata.docs.rs] -features = ["ssl", "tls", "rust-tls"] +features = ["ssl", "tls", "rust-tls"] #, "session"] [features] default = ["brotli", "flate2-c"] @@ -45,6 +45,9 @@ flate2-c = ["flate2/miniz-sys"] # rust backend for flate2 crate flate2-rust = ["flate2/rust_backend"] +# sessions feature, session require "ring" crate and c compiler +# session = ["actix-session"] + # tls tls = ["native-tls", "actix-server/ssl"] @@ -56,10 +59,13 @@ ssl = ["openssl", "actix-server/ssl"] [dependencies] actix-codec = "0.1.0" -actix-service = "0.3.2" -actix-utils = "0.3.1" +#actix-service = "0.3.2" +#actix-utils = "0.3.1" actix-rt = "0.1.0" +actix-service = { git = "https://github.com/actix/actix-net.git" } +actix-utils = { git = "https://github.com/actix/actix-net.git" } + actix-http = { git = "https://github.com/actix/actix-http.git" } actix-router = { git = "https://github.com/actix/actix-net.git" } actix-server = { git = "https://github.com/actix/actix-net.git" } @@ -79,6 +85,9 @@ serde_json = "1.0" serde_urlencoded = "^0.5.3" threadpool = "1.7" +# middlewares +# actix-session = { path="session", optional = true } + # compression brotli2 = { version="^0.3.2", optional = true } flate2 = { version="^1.0.2", optional = true, default-features = false } diff --git a/src/app.rs b/src/app.rs index 8336fcca..b2164501 100644 --- a/src/app.rs +++ b/src/app.rs @@ -25,7 +25,7 @@ type HttpNewService

= BoxedNewService<(), ServiceRequest

, ServiceResponse, type BoxedResponse = Box>; pub trait HttpServiceFactory { - type Factory: NewService; + type Factory: NewService; fn rdef(&self) -> &ResourceDef; @@ -36,7 +36,7 @@ pub trait HttpServiceFactory { /// for building application instances. pub struct App where - T: NewService, Response = ServiceRequest

>, + T: NewService>, { chain: T, extensions: Extensions, @@ -61,7 +61,7 @@ impl App where P: 'static, T: NewService< - Request = ServiceRequest, + ServiceRequest, Response = ServiceRequest

, Error = (), InitError = (), @@ -197,7 +197,7 @@ where where F: FnOnce(Resource

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

, + ServiceRequest

, Response = ServiceResponse, Error = (), InitError = (), @@ -230,7 +230,7 @@ where P, B, impl NewService< - Request = ServiceRequest

, + ServiceRequest

, Response = ServiceResponse, Error = (), InitError = (), @@ -239,12 +239,12 @@ where where M: Transform< AppRouting

, - Request = ServiceRequest

, + ServiceRequest

, Response = ServiceResponse, Error = (), InitError = (), >, - F: IntoTransform>, + F: IntoTransform, ServiceRequest

>, { let fref = Rc::new(RefCell::new(None)); let endpoint = ApplyTransform::new(mw, AppEntry::new(fref.clone())); @@ -269,7 +269,7 @@ where ) -> App< P1, impl NewService< - Request = ServiceRequest, + ServiceRequest, Response = ServiceRequest, Error = (), InitError = (), @@ -277,13 +277,12 @@ where > where C: NewService< - (), - Request = ServiceRequest

, + ServiceRequest

, Response = ServiceRequest, Error = (), InitError = (), >, - F: IntoNewService, + F: IntoNewService>, { let chain = self.chain.and_then(chain.into_new_service()); App { @@ -326,7 +325,7 @@ where P: 'static, B: MessageBody, T: NewService< - Request = ServiceRequest

, + ServiceRequest

, Response = ServiceResponse, Error = (), InitError = (), @@ -406,7 +405,7 @@ where where F: FnOnce(Resource

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

, + ServiceRequest

, Response = ServiceResponse, Error = (), InitError = (), @@ -430,12 +429,9 @@ where pub fn default_resource(mut self, f: F) -> Self where F: FnOnce(Resource

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

, - Response = ServiceResponse, - Error = (), - > + 'static, + R: IntoNewService>, + U: NewService, Response = ServiceResponse, Error = ()> + + 'static, { // create and configure default resource self.default = Some(Rc::new(boxed::new_service( @@ -449,12 +445,9 @@ where pub fn service(mut self, rdef: R, factory: F) -> Self where R: Into, - F: IntoNewService, - U: NewService< - Request = ServiceRequest

, - Response = ServiceResponse, - Error = (), - > + 'static, + F: IntoNewService>, + U: NewService, Response = ServiceResponse, Error = ()> + + 'static, { self.services.push(( rdef.into(), @@ -473,7 +466,7 @@ where P, B1, impl NewService< - Request = ServiceRequest

, + ServiceRequest

, Response = ServiceResponse, Error = (), InitError = (), @@ -482,13 +475,13 @@ where where M: Transform< T::Service, - Request = ServiceRequest

, + ServiceRequest

, Response = ServiceResponse, Error = (), InitError = (), >, B1: MessageBody, - F: IntoTransform, + F: IntoTransform>, { let endpoint = ApplyTransform::new(mw, self.endpoint); AppRouter { @@ -542,22 +535,23 @@ where } impl - IntoNewService, T, ()>> for AppRouter + IntoNewService, T>, Request> + for AppRouter where T: NewService< - Request = ServiceRequest

, + ServiceRequest

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

, Error = (), InitError = (), >, { - fn into_new_service(self) -> AndThenNewService, T, ()> { + fn into_new_service(self) -> AndThenNewService, T> { // update resource default service if self.default.is_some() { for default in &self.defaults { @@ -592,8 +586,7 @@ pub struct AppRoutingFactory

{ default: Option>>, } -impl NewService for AppRoutingFactory

{ - type Request = ServiceRequest

; +impl NewService> for AppRoutingFactory

{ type Response = ServiceResponse; type Error = (); type InitError = (); @@ -709,8 +702,7 @@ pub struct AppRouting

{ default: Option>, } -impl

Service for AppRouting

{ - type Request = ServiceRequest

; +impl

Service> for AppRouting

{ type Response = ServiceResponse; type Error = (); type Future = Either>; @@ -758,8 +750,7 @@ impl

AppEntry

{ } } -impl NewService for AppEntry

{ - type Request = ServiceRequest

; +impl NewService> for AppEntry

{ type Response = ServiceResponse; type Error = (); type InitError = (); @@ -774,9 +765,8 @@ impl NewService for AppEntry

{ #[doc(hidden)] pub struct AppChain; -impl NewService<()> for AppChain { - type Request = ServiceRequest; - type Response = ServiceRequest; +impl NewService for AppChain { + type Response = ServiceRequest; type Error = (); type InitError = (); type Service = AppChain; @@ -787,9 +777,8 @@ impl NewService<()> for AppChain { } } -impl Service for AppChain { - type Request = ServiceRequest; - type Response = ServiceRequest; +impl Service for AppChain { + type Response = ServiceRequest; type Error = (); type Future = FutureResult; @@ -799,7 +788,7 @@ impl Service for AppChain { } #[inline] - fn call(&mut self, req: Self::Request) -> Self::Future { + fn call(&mut self, req: ServiceRequest) -> Self::Future { ok(req) } } @@ -808,22 +797,17 @@ impl Service for AppChain { /// It also executes state factories. pub struct AppInit where - C: NewService, Response = ServiceRequest

>, + C: NewService>, { chain: C, state: Vec>, extensions: Rc>>, } -impl NewService for AppInit +impl NewService for AppInit where - C: NewService< - Request = ServiceRequest, - Response = ServiceRequest

, - InitError = (), - >, + C: NewService, InitError = ()>, { - type Request = Request; type Response = ServiceRequest

; type Error = C::Error; type InitError = C::InitError; @@ -842,11 +826,7 @@ where #[doc(hidden)] pub struct AppInitResult where - C: NewService< - Request = ServiceRequest, - Response = ServiceRequest

, - InitError = (), - >, + C: NewService, InitError = ()>, { chain: C::Future, state: Vec>, @@ -855,11 +835,7 @@ where impl Future for AppInitResult where - C: NewService< - Request = ServiceRequest, - Response = ServiceRequest

, - InitError = (), - >, + C: NewService, InitError = ()>, { type Item = AppInitService; type Error = C::InitError; @@ -893,17 +869,16 @@ where /// Service to convert `Request` to a `ServiceRequest` pub struct AppInitService where - C: Service, Response = ServiceRequest

>, + C: Service>, { chain: C, extensions: Rc, } -impl Service for AppInitService +impl Service for AppInitService where - C: Service, Response = ServiceRequest

>, + C: Service>, { - type Request = Request; type Response = ServiceRequest

; type Error = C::Error; type Future = C::Future; @@ -912,7 +887,7 @@ where self.chain.poll_ready() } - fn call(&mut self, req: Request) -> Self::Future { + fn call(&mut self, req: Request) -> Self::Future { let req = ServiceRequest::new( Path::new(Url::new(req.uri().clone())), req, diff --git a/src/handler.rs b/src/handler.rs index 98a36c56..442dc60d 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -52,12 +52,11 @@ where } } } -impl NewService for Handle +impl NewService<(T, HttpRequest)> for Handle where F: Factory, R: Responder + 'static, { - type Request = (T, HttpRequest); type Response = ServiceResponse; type Error = Void; type InitError = (); @@ -82,12 +81,11 @@ where _t: PhantomData<(T, R)>, } -impl Service for HandleService +impl Service<(T, HttpRequest)> for HandleService where F: Factory, R: Responder + 'static, { - type Request = (T, HttpRequest); type Response = ServiceResponse; type Error = Void; type Future = HandleServiceResponse<::Future>; @@ -184,14 +182,13 @@ where } } } -impl NewService for AsyncHandle +impl NewService<(T, HttpRequest)> for AsyncHandle where F: AsyncFactory, R: IntoFuture, R::Item: Into, R::Error: Into, { - type Request = (T, HttpRequest); type Response = ServiceResponse; type Error = (); type InitError = (); @@ -218,14 +215,13 @@ where _t: PhantomData<(T, R)>, } -impl Service for AsyncHandleService +impl Service<(T, HttpRequest)> for AsyncHandleService where F: AsyncFactory, R: IntoFuture, R::Item: Into, R::Error: Into, { - type Request = (T, HttpRequest); type Response = ServiceResponse; type Error = (); type Future = AsyncHandleServiceResponse; @@ -290,8 +286,7 @@ impl> Extract { } } -impl> NewService for Extract { - type Request = ServiceRequest

; +impl> NewService> for Extract { type Response = (T, HttpRequest); type Error = (Error, ServiceFromRequest

); type InitError = (); @@ -311,8 +306,7 @@ pub struct ExtractService> { _t: PhantomData<(P, T)>, } -impl> Service for ExtractService { - type Request = ServiceRequest

; +impl> Service> for ExtractService { type Response = (T, HttpRequest); type Error = (Error, ServiceFromRequest

); type Future = ExtractResponse; diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index b95553cb..c6f090a6 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -36,14 +36,13 @@ impl Default for Compress { } } -impl Transform for Compress +impl Transform> for Compress where P: 'static, B: MessageBody, - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, { - type Request = ServiceRequest

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

; type Response = ServiceResponse>; type Error = S::Error; type Future = CompressResponse; @@ -103,7 +101,7 @@ pub struct CompressResponse where P: 'static, B: MessageBody, - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, { fut: S::Future, @@ -114,7 +112,7 @@ impl Future for CompressResponse where P: 'static, B: MessageBody, - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, { type Item = ServiceResponse>; diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index 2bd1d5d4..5fd51919 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -84,12 +84,11 @@ impl DefaultHeaders { } } -impl Transform for DefaultHeaders +impl Transform> for DefaultHeaders where - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, { - type Request = ServiceRequest

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

; type Response = ServiceResponse; type Error = S::Error; type Future = Box>; diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index fc992302..8c4cd754 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -5,3 +5,6 @@ pub use self::compress::Compress; mod defaultheaders; pub use self::defaultheaders::DefaultHeaders; + +#[cfg(feature = "session")] +pub use actix_session as session; diff --git a/src/resource.rs b/src/resource.rs index 755b8d07..8d81ead0 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -65,7 +65,7 @@ impl

Default for Resource

{ impl Resource where T: NewService< - Request = ServiceRequest

, + ServiceRequest

, Response = ServiceResponse, Error = (), InitError = (), @@ -187,7 +187,7 @@ where ) -> Resource< P, impl NewService< - Request = ServiceRequest

, + ServiceRequest

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

, + ServiceRequest

, Response = ServiceResponse, Error = (), InitError = (), >, - F: IntoTransform, + F: IntoTransform>, { let endpoint = ApplyTransform::new(mw, self.endpoint); Resource { @@ -216,12 +216,9 @@ where pub fn default_resource(mut self, f: F) -> Self where F: FnOnce(Resource

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

, - Response = ServiceResponse, - Error = (), - > + 'static, + R: IntoNewService>, + U: NewService, Response = ServiceResponse, Error = ()> + + 'static, { // create and configure default resource self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::new_service( @@ -236,10 +233,10 @@ where } } -impl IntoNewService for Resource +impl IntoNewService> for Resource where T: NewService< - Request = ServiceRequest

, + ServiceRequest

, Response = ServiceResponse, Error = (), InitError = (), @@ -260,8 +257,7 @@ pub struct ResourceFactory

{ default: Rc>>>>, } -impl NewService for ResourceFactory

{ - type Request = ServiceRequest

; +impl NewService> for ResourceFactory

{ type Response = ServiceResponse; type Error = (); type InitError = (); @@ -351,8 +347,7 @@ pub struct ResourceService

{ default: Option>, } -impl

Service for ResourceService

{ - type Request = ServiceRequest

; +impl

Service> for ResourceService

{ type Response = ServiceResponse; type Error = (); type Future = Either< @@ -396,8 +391,7 @@ impl

ResourceEndpoint

{ } } -impl NewService for ResourceEndpoint

{ - type Request = ServiceRequest

; +impl NewService> for ResourceEndpoint

{ type Response = ServiceResponse; type Error = (); type InitError = (); diff --git a/src/route.rs b/src/route.rs index 72abeb32..42e78488 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,4 +1,5 @@ use std::cell::RefCell; +use std::marker::PhantomData; use std::rc::Rc; use actix_http::{http::Method, Error, Extensions, Response}; @@ -14,7 +15,7 @@ use crate::HttpResponse; type BoxedRouteService = Box< Service< - Request = Req, + Req, Response = Res, Error = (), Future = Box>, @@ -23,7 +24,7 @@ type BoxedRouteService = Box< type BoxedRouteNewService = Box< NewService< - Request = Req, + Req, Response = Res, Error = (), InitError = (), @@ -85,8 +86,7 @@ impl Route

{ } } -impl

NewService for Route

{ - type Request = ServiceRequest

; +impl

NewService> for Route

{ type Response = ServiceResponse; type Error = (); type InitError = (); @@ -141,8 +141,7 @@ impl

RouteService

{ } } -impl

Service for RouteService

{ - type Request = ServiceRequest

; +impl

Service> for RouteService

{ type Response = ServiceResponse; type Error = (); type Future = Box>; @@ -151,7 +150,7 @@ impl

Service for RouteService

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

) -> Self::Future { self.service.call(req) } } @@ -348,43 +347,46 @@ impl Route

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

)>, + T: NewService, Error = (Error, ServiceFromRequest

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

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

, + ServiceRequest

, Response = ServiceResponse, Error = (Error, ServiceFromRequest

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

, + ServiceRequest

, Response = ServiceResponse, Error = (Error, ServiceFromRequest

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

; type Response = ServiceResponse; type Error = (); type InitError = (); - type Service = BoxedRouteService; + type Service = BoxedRouteService, Self::Response>; type Future = Box>; fn new_service(&self, _: &()) -> Self::Future { @@ -394,27 +396,30 @@ where .map_err(|_| ()) .and_then(|service| { let service: BoxedRouteService<_, _> = - Box::new(RouteServiceWrapper { service }); + Box::new(RouteServiceWrapper { + service, + _t: PhantomData, + }); 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

, + ServiceRequest

, Response = ServiceResponse, Error = (Error, ServiceFromRequest

), >, { - type Request = ServiceRequest

; type Response = ServiceResponse; type Error = (); type Future = Box>; diff --git a/src/scope.rs b/src/scope.rs index b255ac14..fa7392b4 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -79,7 +79,7 @@ impl Scope

{ impl Scope where T: NewService< - Request = ServiceRequest

, + ServiceRequest

, Response = ServiceResponse, Error = (), InitError = (), @@ -196,7 +196,7 @@ where where F: FnOnce(Resource

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

, + ServiceRequest

, Response = ServiceResponse, Error = (), InitError = (), @@ -219,7 +219,7 @@ where where F: FnOnce(Resource

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

, + ServiceRequest

, Response = ServiceResponse, Error = (), InitError = (), @@ -244,7 +244,7 @@ where ) -> Scope< P, impl NewService< - Request = ServiceRequest

, + ServiceRequest

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

, + ServiceRequest

, Response = ServiceResponse, Error = (), InitError = (), >, - F: IntoTransform, + F: IntoTransform>, { let endpoint = ApplyTransform::new(mw, self.endpoint); Scope { @@ -293,10 +293,10 @@ pub(crate) fn insert_slash(path: &str) -> String { path } -impl IntoNewService for Scope +impl IntoNewService> for Scope where T: NewService< - Request = ServiceRequest

, + ServiceRequest

, Response = ServiceResponse, Error = (), InitError = (), @@ -331,8 +331,7 @@ pub struct ScopeFactory

{ default: Rc>>>>, } -impl NewService for ScopeFactory

{ - type Request = ServiceRequest

; +impl NewService> for ScopeFactory

{ type Response = ServiceResponse; type Error = (); type InitError = (); @@ -448,8 +447,7 @@ pub struct ScopeService

{ _ready: Option<(ServiceRequest

, ResourceInfo)>, } -impl

Service for ScopeService

{ - type Request = ServiceRequest

; +impl

Service> for ScopeService

{ type Response = ServiceResponse; type Error = (); type Future = Either>; @@ -492,8 +490,7 @@ impl

ScopeEndpoint

{ } } -impl NewService for ScopeEndpoint

{ - type Request = ServiceRequest

; +impl NewService> for ScopeEndpoint

{ type Response = ServiceResponse; type Error = (); type InitError = (); diff --git a/src/server.rs b/src/server.rs index 95cab2b0..78ba692e 100644 --- a/src/server.rs +++ b/src/server.rs @@ -52,8 +52,8 @@ struct Config { pub struct HttpServer where F: Fn() -> I + Send + Clone + 'static, - I: IntoNewService, - S: NewService, + I: IntoNewService, + S: NewService, S::Error: fmt::Debug, S::Response: Into>, S::Service: 'static, @@ -71,8 +71,8 @@ where impl HttpServer where F: Fn() -> I + Send + Clone + 'static, - I: IntoNewService, - S: NewService, + I: IntoNewService, + S: NewService, S::Error: fmt::Debug, S::Response: Into>, S::Service: 'static, @@ -431,8 +431,8 @@ where impl HttpServer where F: Fn() -> I + Send + Clone + 'static, - I: IntoNewService, - S: NewService, + I: IntoNewService, + S: NewService, S::Error: fmt::Debug, S::Response: Into>, S::Service: 'static, diff --git a/src/service.rs b/src/service.rs index 637a8668..50b2924a 100644 --- a/src/service.rs +++ b/src/service.rs @@ -5,15 +5,15 @@ use std::rc::Rc; use actix_http::body::{Body, MessageBody, ResponseBody}; use actix_http::http::{HeaderMap, Method, Uri, Version}; use actix_http::{ - Error, Extensions, HttpMessage, Payload, Request, RequestHead, Response, - ResponseHead, + Error, Extensions, HttpMessage, Payload, PayloadStream, Request, RequestHead, + Response, ResponseHead, }; use actix_router::{Path, Resource, Url}; use futures::future::{ok, FutureResult, IntoFuture}; use crate::request::HttpRequest; -pub struct ServiceRequest

{ +pub struct ServiceRequest

{ req: HttpRequest, payload: Payload

, } From 6457996cf1b404245e23cfff0c0836a8051ce37d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 10:12:49 -0800 Subject: [PATCH 032/109] move session to separate crate --- session/src/lib.rs | 618 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 618 insertions(+) create mode 100644 session/src/lib.rs diff --git a/session/src/lib.rs b/session/src/lib.rs new file mode 100644 index 00000000..0271a13f --- /dev/null +++ b/session/src/lib.rs @@ -0,0 +1,618 @@ +//! User sessions. +//! +//! Actix provides a general solution for session management. The +//! [**SessionStorage**](struct.SessionStorage.html) +//! middleware can be used with different backend types to store session +//! data in different backends. +//! +//! By default, only cookie session backend is implemented. Other +//! backend implementations can be added. +//! +//! [**CookieSessionBackend**](struct.CookieSessionBackend.html) +//! uses cookies as session storage. `CookieSessionBackend` creates sessions +//! which are limited to storing fewer than 4000 bytes of data, as the payload +//! must fit into a single cookie. An internal server error is generated if a +//! session contains more than 4000 bytes. +//! +//! A cookie may have a security policy of *signed* or *private*. Each has +//! a respective `CookieSessionBackend` constructor. +//! +//! A *signed* cookie may be viewed but not modified by the client. A *private* +//! cookie may neither be viewed nor modified by the client. +//! +//! The constructors take a key as an argument. This is the private key +//! for cookie session - when this value is changed, all session data is lost. +//! +//! In general, you create a `SessionStorage` middleware and initialize it +//! with specific backend implementation, such as a `CookieSessionBackend`. +//! To access session data, +//! [*HttpRequest::session()*](trait.RequestSession.html#tymethod.session) +//! must be used. This method returns a +//! [*Session*](struct.Session.html) object, which allows us to get or set +//! session data. +//! +//! ```rust +//! # extern crate actix_web; +//! # extern crate actix; +//! use actix_web::{server, App, HttpRequest, Result}; +//! use actix_web::middleware::session::{RequestSession, SessionStorage, CookieSessionBackend}; +//! +//! fn index(req: HttpRequest) -> Result<&'static str> { +//! // access session data +//! if let Some(count) = req.session().get::("counter")? { +//! println!("SESSION value: {}", count); +//! req.session().set("counter", count+1)?; +//! } else { +//! req.session().set("counter", 1)?; +//! } +//! +//! Ok("Welcome!") +//! } +//! +//! fn main() { +//! actix::System::run(|| { +//! server::new( +//! || App::new().middleware( +//! SessionStorage::new( // <- create session middleware +//! CookieSessionBackend::signed(&[0; 32]) // <- create signed cookie session backend +//! .secure(false) +//! ))) +//! .bind("127.0.0.1:59880").unwrap() +//! .start(); +//! # actix::System::current().stop(); +//! }); +//! } +//! ``` +use std::cell::RefCell; +use std::collections::HashMap; +use std::marker::PhantomData; +use std::rc::Rc; +use std::sync::Arc; + +use cookie::{Cookie, CookieJar, Key, SameSite}; +use futures::future::{err as FutErr, ok as FutOk, FutureResult}; +use futures::Future; +use http::header::{self, HeaderValue}; +use serde::de::DeserializeOwned; +use serde::Serialize; +use serde_json; +use serde_json::error::Error as JsonError; +use time::Duration; + +use error::{Error, ResponseError, Result}; +use handler::FromRequest; +use httprequest::HttpRequest; +use httpresponse::HttpResponse; +use middleware::{Middleware, Response, Started}; + +/// The helper trait to obtain your session data from a request. +/// +/// ```rust +/// use actix_web::middleware::session::RequestSession; +/// use actix_web::*; +/// +/// fn index(mut req: HttpRequest) -> Result<&'static str> { +/// // access session data +/// if let Some(count) = req.session().get::("counter")? { +/// req.session().set("counter", count + 1)?; +/// } else { +/// req.session().set("counter", 1)?; +/// } +/// +/// Ok("Welcome!") +/// } +/// # fn main() {} +/// ``` +pub trait RequestSession { + /// Get the session from the request + fn session(&self) -> Session; +} + +impl RequestSession for HttpRequest { + fn session(&self) -> Session { + if let Some(s_impl) = self.extensions().get::>() { + return Session(SessionInner::Session(Arc::clone(&s_impl))); + } + Session(SessionInner::None) + } +} + +/// The high-level interface you use to modify session data. +/// +/// Session object could be obtained with +/// [`RequestSession::session`](trait.RequestSession.html#tymethod.session) +/// method. `RequestSession` trait is implemented for `HttpRequest`. +/// +/// ```rust +/// use actix_web::middleware::session::RequestSession; +/// use actix_web::*; +/// +/// fn index(mut req: HttpRequest) -> Result<&'static str> { +/// // access session data +/// if let Some(count) = req.session().get::("counter")? { +/// req.session().set("counter", count + 1)?; +/// } else { +/// req.session().set("counter", 1)?; +/// } +/// +/// Ok("Welcome!") +/// } +/// # fn main() {} +/// ``` +pub struct Session(SessionInner); + +enum SessionInner { + Session(Arc), + None, +} + +impl Session { + /// Get a `value` from the session. + pub fn get(&self, key: &str) -> Result> { + match self.0 { + SessionInner::Session(ref sess) => { + if let Some(s) = sess.as_ref().0.borrow().get(key) { + Ok(Some(serde_json::from_str(s)?)) + } else { + Ok(None) + } + } + SessionInner::None => Ok(None), + } + } + + /// Set a `value` from the session. + pub fn set(&self, key: &str, value: T) -> Result<()> { + match self.0 { + SessionInner::Session(ref sess) => { + sess.as_ref() + .0 + .borrow_mut() + .set(key, serde_json::to_string(&value)?); + Ok(()) + } + SessionInner::None => Ok(()), + } + } + + /// Remove value from the session. + pub fn remove(&self, key: &str) { + match self.0 { + SessionInner::Session(ref sess) => sess.as_ref().0.borrow_mut().remove(key), + SessionInner::None => (), + } + } + + /// Clear the session. + pub fn clear(&self) { + match self.0 { + SessionInner::Session(ref sess) => sess.as_ref().0.borrow_mut().clear(), + SessionInner::None => (), + } + } +} + +/// Extractor implementation for Session type. +/// +/// ```rust +/// # use actix_web::*; +/// use actix_web::middleware::session::Session; +/// +/// fn index(session: Session) -> Result<&'static str> { +/// // access session data +/// if let Some(count) = session.get::("counter")? { +/// session.set("counter", count + 1)?; +/// } else { +/// session.set("counter", 1)?; +/// } +/// +/// Ok("Welcome!") +/// } +/// # fn main() {} +/// ``` +impl FromRequest for Session { + type Config = (); + type Result = Session; + + #[inline] + fn from_request(req: &HttpRequest, _: &Self::Config) -> Self::Result { + req.session() + } +} + +struct SessionImplCell(RefCell>); + +/// Session storage middleware +/// +/// ```rust +/// # extern crate actix_web; +/// use actix_web::middleware::session::{CookieSessionBackend, SessionStorage}; +/// use actix_web::App; +/// +/// fn main() { +/// let app = App::new().middleware(SessionStorage::new( +/// // <- create session middleware +/// CookieSessionBackend::signed(&[0; 32]) // <- create cookie session backend +/// .secure(false), +/// )); +/// } +/// ``` +pub struct SessionStorage(T, PhantomData); + +impl> SessionStorage { + /// Create session storage + pub fn new(backend: T) -> SessionStorage { + SessionStorage(backend, PhantomData) + } +} + +impl> Middleware for SessionStorage { + fn start(&self, req: &HttpRequest) -> Result { + let mut req = req.clone(); + + let fut = self.0.from_request(&mut req).then(move |res| match res { + Ok(sess) => { + req.extensions_mut() + .insert(Arc::new(SessionImplCell(RefCell::new(Box::new(sess))))); + FutOk(None) + } + Err(err) => FutErr(err), + }); + Ok(Started::Future(Box::new(fut))) + } + + fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { + if let Some(s_box) = req.extensions().get::>() { + s_box.0.borrow_mut().write(resp) + } else { + Ok(Response::Done(resp)) + } + } +} + +/// A simple key-value storage interface that is internally used by `Session`. +pub trait SessionImpl: 'static { + /// Get session value by key + fn get(&self, key: &str) -> Option<&str>; + + /// Set session value + fn set(&mut self, key: &str, value: String); + + /// Remove specific key from session + fn remove(&mut self, key: &str); + + /// Remove all values from session + fn clear(&mut self); + + /// Write session to storage backend. + fn write(&self, resp: HttpResponse) -> Result; +} + +/// Session's storage backend trait definition. +pub trait SessionBackend: Sized + 'static { + /// Session item + type Session: SessionImpl; + /// Future that reads session + type ReadFuture: Future; + + /// Parse the session from request and load data from a storage backend. + fn from_request(&self, request: &mut HttpRequest) -> Self::ReadFuture; +} + +/// Session that uses signed cookies as session storage +pub struct CookieSession { + changed: bool, + state: HashMap, + inner: Rc, +} + +/// Errors that can occur during handling cookie session +#[derive(Fail, Debug)] +pub enum CookieSessionError { + /// Size of the serialized session is greater than 4000 bytes. + #[fail(display = "Size of the serialized session is greater than 4000 bytes.")] + Overflow, + /// Fail to serialize session. + #[fail(display = "Fail to serialize session")] + Serialize(JsonError), +} + +impl ResponseError for CookieSessionError {} + +impl SessionImpl for CookieSession { + fn get(&self, key: &str) -> Option<&str> { + if let Some(s) = self.state.get(key) { + Some(s) + } else { + None + } + } + + fn set(&mut self, key: &str, value: String) { + self.changed = true; + self.state.insert(key.to_owned(), value); + } + + fn remove(&mut self, key: &str) { + self.changed = true; + self.state.remove(key); + } + + fn clear(&mut self) { + self.changed = true; + self.state.clear() + } + + fn write(&self, mut resp: HttpResponse) -> Result { + if self.changed { + let _ = self.inner.set_cookie(&mut resp, &self.state); + } + Ok(Response::Done(resp)) + } +} + +enum CookieSecurity { + Signed, + Private, +} + +struct CookieSessionInner { + key: Key, + security: CookieSecurity, + name: String, + path: String, + domain: Option, + secure: bool, + http_only: bool, + max_age: Option, + same_site: Option, +} + +impl CookieSessionInner { + fn new(key: &[u8], security: CookieSecurity) -> CookieSessionInner { + CookieSessionInner { + security, + key: Key::from_master(key), + name: "actix-session".to_owned(), + path: "/".to_owned(), + domain: None, + secure: true, + http_only: true, + max_age: None, + same_site: None, + } + } + + fn set_cookie( + &self, resp: &mut HttpResponse, state: &HashMap, + ) -> Result<()> { + let value = + serde_json::to_string(&state).map_err(CookieSessionError::Serialize)?; + if value.len() > 4064 { + return Err(CookieSessionError::Overflow.into()); + } + + let mut cookie = Cookie::new(self.name.clone(), value); + cookie.set_path(self.path.clone()); + cookie.set_secure(self.secure); + cookie.set_http_only(self.http_only); + + if let Some(ref domain) = self.domain { + cookie.set_domain(domain.clone()); + } + + if let Some(max_age) = self.max_age { + cookie.set_max_age(max_age); + } + + if let Some(same_site) = self.same_site { + cookie.set_same_site(same_site); + } + + let mut jar = CookieJar::new(); + + match self.security { + CookieSecurity::Signed => jar.signed(&self.key).add(cookie), + CookieSecurity::Private => jar.private(&self.key).add(cookie), + } + + for cookie in jar.delta() { + let val = HeaderValue::from_str(&cookie.encoded().to_string())?; + resp.headers_mut().append(header::SET_COOKIE, val); + } + + Ok(()) + } + + fn load(&self, req: &mut HttpRequest) -> HashMap { + if let Ok(cookies) = req.cookies() { + for cookie in cookies.iter() { + if cookie.name() == self.name { + let mut jar = CookieJar::new(); + jar.add_original(cookie.clone()); + + let cookie_opt = match self.security { + CookieSecurity::Signed => jar.signed(&self.key).get(&self.name), + CookieSecurity::Private => { + jar.private(&self.key).get(&self.name) + } + }; + if let Some(cookie) = cookie_opt { + if let Ok(val) = serde_json::from_str(cookie.value()) { + return val; + } + } + } + } + } + HashMap::new() + } +} + +/// Use cookies for session storage. +/// +/// `CookieSessionBackend` creates sessions which are limited to storing +/// fewer than 4000 bytes of data (as the payload must fit into a single +/// cookie). An Internal Server Error is generated if the session contains more +/// than 4000 bytes. +/// +/// A cookie may have a security policy of *signed* or *private*. Each has a +/// respective `CookieSessionBackend` constructor. +/// +/// A *signed* cookie is stored on the client as plaintext alongside +/// a signature such that the cookie may be viewed but not modified by the +/// client. +/// +/// A *private* cookie is stored on the client as encrypted text +/// such that it may neither be viewed nor modified by the client. +/// +/// The constructors take a key as an argument. +/// This is the private key for cookie session - when this value is changed, +/// all session data is lost. The constructors will panic if the key is less +/// than 32 bytes in length. +/// +/// The backend relies on `cookie` crate to create and read cookies. +/// By default all cookies are percent encoded, but certain symbols may +/// cause troubles when reading cookie, if they are not properly percent encoded. +/// +/// # Example +/// +/// ```rust +/// # extern crate actix_web; +/// use actix_web::middleware::session::CookieSessionBackend; +/// +/// # fn main() { +/// let backend: CookieSessionBackend = CookieSessionBackend::signed(&[0; 32]) +/// .domain("www.rust-lang.org") +/// .name("actix_session") +/// .path("/") +/// .secure(true); +/// # } +/// ``` +pub struct CookieSessionBackend(Rc); + +impl CookieSessionBackend { + /// Construct new *signed* `CookieSessionBackend` instance. + /// + /// Panics if key length is less than 32 bytes. + pub fn signed(key: &[u8]) -> CookieSessionBackend { + CookieSessionBackend(Rc::new(CookieSessionInner::new( + key, + CookieSecurity::Signed, + ))) + } + + /// Construct new *private* `CookieSessionBackend` instance. + /// + /// Panics if key length is less than 32 bytes. + pub fn private(key: &[u8]) -> CookieSessionBackend { + CookieSessionBackend(Rc::new(CookieSessionInner::new( + key, + CookieSecurity::Private, + ))) + } + + /// Sets the `path` field in the session cookie being built. + pub fn path>(mut self, value: S) -> CookieSessionBackend { + Rc::get_mut(&mut self.0).unwrap().path = value.into(); + self + } + + /// Sets the `name` field in the session cookie being built. + pub fn name>(mut self, value: S) -> CookieSessionBackend { + Rc::get_mut(&mut self.0).unwrap().name = value.into(); + self + } + + /// Sets the `domain` field in the session cookie being built. + pub fn domain>(mut self, value: S) -> CookieSessionBackend { + Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into()); + self + } + + /// Sets the `secure` field in the session cookie being built. + /// + /// If the `secure` field is set, a cookie will only be transmitted when the + /// connection is secure - i.e. `https` + pub fn secure(mut self, value: bool) -> CookieSessionBackend { + Rc::get_mut(&mut self.0).unwrap().secure = value; + self + } + + /// Sets the `http_only` field in the session cookie being built. + pub fn http_only(mut self, value: bool) -> CookieSessionBackend { + Rc::get_mut(&mut self.0).unwrap().http_only = value; + self + } + + /// Sets the `same_site` field in the session cookie being built. + pub fn same_site(mut self, value: SameSite) -> CookieSessionBackend { + Rc::get_mut(&mut self.0).unwrap().same_site = Some(value); + self + } + + /// Sets the `max-age` field in the session cookie being built. + pub fn max_age(mut self, value: Duration) -> CookieSessionBackend { + Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); + self + } +} + +impl SessionBackend for CookieSessionBackend { + type Session = CookieSession; + type ReadFuture = FutureResult; + + fn from_request(&self, req: &mut HttpRequest) -> Self::ReadFuture { + let state = self.0.load(req); + FutOk(CookieSession { + changed: false, + inner: Rc::clone(&self.0), + state, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use application::App; + use test; + + #[test] + fn cookie_session() { + let mut srv = test::TestServer::with_factory(|| { + App::new() + .middleware(SessionStorage::new( + CookieSessionBackend::signed(&[0; 32]).secure(false), + )).resource("/", |r| { + r.f(|req| { + let _ = req.session().set("counter", 100); + "test" + }) + }) + }); + + let request = srv.get().uri(srv.url("/")).finish().unwrap(); + let response = srv.execute(request.send()).unwrap(); + assert!(response.cookie("actix-session").is_some()); + } + + #[test] + fn cookie_session_extractor() { + let mut srv = test::TestServer::with_factory(|| { + App::new() + .middleware(SessionStorage::new( + CookieSessionBackend::signed(&[0; 32]).secure(false), + )).resource("/", |r| { + r.with(|ses: Session| { + let _ = ses.set("counter", 100); + "test" + }) + }) + }); + + let request = srv.get().uri(srv.url("/")).finish().unwrap(); + let response = srv.execute(request.send()).unwrap(); + assert!(response.cookie("actix-session").is_some()); + } +} From 143ef87b666559dd6c25ddce04db494040eef809 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 18:47:18 -0800 Subject: [PATCH 033/109] add session and cookie session backend --- Cargo.toml | 1 + session/Cargo.toml | 51 ++++ session/src/cookie.rs | 360 +++++++++++++++++++++++ session/src/lib.rs | 652 +++++++----------------------------------- src/request.rs | 38 +-- src/service.rs | 85 ++++-- src/test.rs | 47 ++- 7 files changed, 646 insertions(+), 588 deletions(-) create mode 100644 session/Cargo.toml create mode 100644 session/src/cookie.rs diff --git a/Cargo.toml b/Cargo.toml index f473ac55..2f69c7ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ path = "src/lib.rs" [workspace] members = [ ".", + "session", "staticfiles", ] diff --git a/session/Cargo.toml b/session/Cargo.toml new file mode 100644 index 00000000..3bbeb4f8 --- /dev/null +++ b/session/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "actix-session" +version = "0.1.0" +authors = ["Nikolay Kim "] +description = "Session 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-web/" +license = "MIT/Apache-2.0" +exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] +workspace = ".." +edition = "2018" + +[lib] +name = "actix_session" +path = "src/lib.rs" + +[features] +default = ["cookie-session"] + +# sessions feature, session require "ring" crate and c compiler +cookie-session = ["cookie/secure"] + +[dependencies] +actix-web = { path=".." } +actix-codec = "0.1.0" + +#actix-service = "0.3.2" +#actix-utils = "0.3.1" +actix-service = { git = "https://github.com/actix/actix-net.git" } +actix-utils = { git = "https://github.com/actix/actix-net.git" } + +actix-http = { git = "https://github.com/actix/actix-http.git" } +actix-router = { git = "https://github.com/actix/actix-net.git" } +actix-server = { git = "https://github.com/actix/actix-net.git" } + +bytes = "0.4" +cookie = { version="0.11", features=["percent-encode"], optional=true } +derive_more = "0.14" +encoding = "0.2" +futures = "0.1" +hashbrown = "0.1.8" +log = "0.4" +serde = "1.0" +serde_json = "1.0" +time = "0.1" + +[dev-dependencies] +actix-rt = "0.1.0" diff --git a/session/src/cookie.rs b/session/src/cookie.rs new file mode 100644 index 00000000..9cde02e0 --- /dev/null +++ b/session/src/cookie.rs @@ -0,0 +1,360 @@ +//! Cookie session. +//! +//! [**CookieSession**](struct.CookieSession.html) +//! uses cookies as session storage. `CookieSession` creates sessions +//! which are limited to storing fewer than 4000 bytes of data, as the payload +//! must fit into a single cookie. An internal server error is generated if a +//! session contains more than 4000 bytes. +//! +//! A cookie may have a security policy of *signed* or *private*. Each has +//! a respective `CookieSession` constructor. +//! +//! A *signed* cookie may be viewed but not modified by the client. A *private* +//! cookie may neither be viewed nor modified by the client. +//! +//! The constructors take a key as an argument. This is the private key +//! for cookie session - when this value is changed, all session data is lost. + +use std::collections::HashMap; +use std::rc::Rc; + +use actix_service::{Service, Transform}; +use actix_web::http::{header::SET_COOKIE, HeaderValue}; +use actix_web::{Error, HttpMessage, ResponseError, ServiceRequest, ServiceResponse}; +use cookie::{Cookie, CookieJar, Key, SameSite}; +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; + +/// Errors that can occur during handling cookie session +#[derive(Debug, From, Display)] +pub enum CookieSessionError { + /// Size of the serialized session is greater than 4000 bytes. + #[display(fmt = "Size of the serialized session is greater than 4000 bytes.")] + Overflow, + /// Fail to serialize session. + #[display(fmt = "Fail to serialize session")] + Serialize(JsonError), +} + +impl ResponseError for CookieSessionError {} + +enum CookieSecurity { + Signed, + Private, +} + +struct CookieSessionInner { + key: Key, + security: CookieSecurity, + name: String, + path: String, + domain: Option, + secure: bool, + http_only: bool, + max_age: Option, + same_site: Option, +} + +impl CookieSessionInner { + fn new(key: &[u8], security: CookieSecurity) -> CookieSessionInner { + CookieSessionInner { + security, + key: Key::from_master(key), + name: "actix-session".to_owned(), + path: "/".to_owned(), + domain: None, + secure: true, + http_only: true, + max_age: None, + same_site: None, + } + } + + fn set_cookie( + &self, + res: &mut ServiceResponse, + state: impl Iterator, + ) -> Result<(), Error> { + let state: HashMap = state.collect(); + let value = + serde_json::to_string(&state).map_err(CookieSessionError::Serialize)?; + if value.len() > 4064 { + return Err(CookieSessionError::Overflow.into()); + } + + let mut cookie = Cookie::new(self.name.clone(), value); + cookie.set_path(self.path.clone()); + cookie.set_secure(self.secure); + cookie.set_http_only(self.http_only); + + if let Some(ref domain) = self.domain { + cookie.set_domain(domain.clone()); + } + + if let Some(max_age) = self.max_age { + cookie.set_max_age(max_age); + } + + if let Some(same_site) = self.same_site { + cookie.set_same_site(same_site); + } + + let mut jar = CookieJar::new(); + + match self.security { + CookieSecurity::Signed => jar.signed(&self.key).add(cookie), + CookieSecurity::Private => jar.private(&self.key).add(cookie), + } + + for cookie in jar.delta() { + let val = HeaderValue::from_str(&cookie.encoded().to_string())?; + res.headers_mut().append(SET_COOKIE, val); + } + + Ok(()) + } + + fn load

(&self, req: &ServiceRequest

) -> HashMap { + if let Ok(cookies) = req.cookies() { + for cookie in cookies.iter() { + if cookie.name() == self.name { + let mut jar = CookieJar::new(); + jar.add_original(cookie.clone()); + + let cookie_opt = match self.security { + CookieSecurity::Signed => jar.signed(&self.key).get(&self.name), + CookieSecurity::Private => { + jar.private(&self.key).get(&self.name) + } + }; + if let Some(cookie) = cookie_opt { + if let Ok(val) = serde_json::from_str(cookie.value()) { + return val; + } + } + } + } + } + HashMap::new() + } +} + +/// Use cookies for session storage. +/// +/// `CookieSession` creates sessions which are limited to storing +/// fewer than 4000 bytes of data (as the payload must fit into a single +/// cookie). An Internal Server Error is generated if the session contains more +/// than 4000 bytes. +/// +/// A cookie may have a security policy of *signed* or *private*. Each has a +/// respective `CookieSessionBackend` constructor. +/// +/// A *signed* cookie is stored on the client as plaintext alongside +/// a signature such that the cookie may be viewed but not modified by the +/// client. +/// +/// A *private* cookie is stored on the client as encrypted text +/// such that it may neither be viewed nor modified by the client. +/// +/// The constructors take a key as an argument. +/// This is the private key for cookie session - when this value is changed, +/// all session data is lost. The constructors will panic if the key is less +/// than 32 bytes in length. +/// +/// The backend relies on `cookie` crate to create and read cookies. +/// By default all cookies are percent encoded, but certain symbols may +/// cause troubles when reading cookie, if they are not properly percent encoded. +/// +/// # Example +/// +/// ```rust +/// use actix_session::CookieSession; +/// use actix_web::{App, HttpResponse, HttpServer}; +/// +/// fn main() { +/// let app = App::new().middleware( +/// CookieSession::signed(&[0; 32]) +/// .domain("www.rust-lang.org") +/// .name("actix_session") +/// .path("/") +/// .secure(true)) +/// .resource("/", |r| r.to(|| HttpResponse::Ok())); +/// } +/// ``` +pub struct CookieSession(Rc); + +impl CookieSession { + /// Construct new *signed* `CookieSessionBackend` instance. + /// + /// Panics if key length is less than 32 bytes. + pub fn signed(key: &[u8]) -> CookieSession { + CookieSession(Rc::new(CookieSessionInner::new( + key, + CookieSecurity::Signed, + ))) + } + + /// Construct new *private* `CookieSessionBackend` instance. + /// + /// Panics if key length is less than 32 bytes. + pub fn private(key: &[u8]) -> CookieSession { + CookieSession(Rc::new(CookieSessionInner::new( + key, + CookieSecurity::Private, + ))) + } + + /// Sets the `path` field in the session cookie being built. + pub fn path>(mut self, value: S) -> CookieSession { + Rc::get_mut(&mut self.0).unwrap().path = value.into(); + self + } + + /// Sets the `name` field in the session cookie being built. + pub fn name>(mut self, value: S) -> CookieSession { + Rc::get_mut(&mut self.0).unwrap().name = value.into(); + self + } + + /// Sets the `domain` field in the session cookie being built. + pub fn domain>(mut self, value: S) -> CookieSession { + Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into()); + self + } + + /// Sets the `secure` field in the session cookie being built. + /// + /// If the `secure` field is set, a cookie will only be transmitted when the + /// connection is secure - i.e. `https` + pub fn secure(mut self, value: bool) -> CookieSession { + Rc::get_mut(&mut self.0).unwrap().secure = value; + self + } + + /// Sets the `http_only` field in the session cookie being built. + pub fn http_only(mut self, value: bool) -> CookieSession { + Rc::get_mut(&mut self.0).unwrap().http_only = value; + self + } + + /// Sets the `same_site` field in the session cookie being built. + pub fn same_site(mut self, value: SameSite) -> CookieSession { + Rc::get_mut(&mut self.0).unwrap().same_site = Some(value); + self + } + + /// Sets the `max-age` field in the session cookie being built. + pub fn max_age(mut self, value: Duration) -> CookieSession { + Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); + self + } +} + +impl Transform> for CookieSession +where + S: Service, Response = ServiceResponse>, + S::Future: 'static, + S::Error: 'static, +{ + type Response = ServiceResponse; + type Error = S::Error; + type InitError = (); + type Transform = CookieSessionMiddleware; + type Future = FutureResult; + + fn new_transform(&self, service: S) -> Self::Future { + ok(CookieSessionMiddleware { + service, + inner: self.0.clone(), + }) + } +} + +/// Cookie session middleware +pub struct CookieSessionMiddleware { + service: S, + inner: Rc, +} + +impl Service> for CookieSessionMiddleware +where + S: Service, Response = ServiceResponse>, + S::Future: 'static, + S::Error: 'static, +{ + 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 { + let inner = self.inner.clone(); + let state = self.inner.load(&req); + Session::set_session(state.into_iter(), &mut req); + + Box::new(self.service.call(req).map(move |mut res| { + if let Some(state) = Session::get_changes(&mut res) { + res.checked_expr(|res| inner.set_cookie(res, state)) + } else { + res + } + })) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use actix_web::{test, App}; + + #[test] + fn cookie_session() { + let mut app = test::init_service( + App::new() + .middleware(CookieSession::signed(&[0; 32]).secure(false)) + .resource("/", |r| { + r.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 + .cookies() + .find(|c| c.name() == "actix-session") + .is_some()); + } + + #[test] + fn cookie_session_extractor() { + let mut app = test::init_service( + App::new() + .middleware(CookieSession::signed(&[0; 32]).secure(false)) + .resource("/", |r| { + r.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 + .cookies() + .find(|c| c.name() == "actix-session") + .is_some()); + } +} diff --git a/session/src/lib.rs b/session/src/lib.rs index 0271a13f..f57e11f2 100644 --- a/session/src/lib.rs +++ b/session/src/lib.rs @@ -1,121 +1,61 @@ //! User sessions. //! -//! Actix provides a general solution for session management. The -//! [**SessionStorage**](struct.SessionStorage.html) -//! middleware can be used with different backend types to store session -//! data in different backends. +//! Actix provides a general solution for session management. Session +//! middlewares could provide different implementations which could +//! be accessed via general session api. //! //! By default, only cookie session backend is implemented. Other //! backend implementations can be added. //! -//! [**CookieSessionBackend**](struct.CookieSessionBackend.html) -//! uses cookies as session storage. `CookieSessionBackend` creates sessions -//! which are limited to storing fewer than 4000 bytes of data, as the payload -//! must fit into a single cookie. An internal server error is generated if a -//! session contains more than 4000 bytes. -//! -//! A cookie may have a security policy of *signed* or *private*. Each has -//! a respective `CookieSessionBackend` constructor. -//! -//! A *signed* cookie may be viewed but not modified by the client. A *private* -//! cookie may neither be viewed nor modified by the client. -//! -//! The constructors take a key as an argument. This is the private key -//! for cookie session - when this value is changed, all session data is lost. -//! -//! In general, you create a `SessionStorage` middleware and initialize it -//! with specific backend implementation, such as a `CookieSessionBackend`. -//! To access session data, -//! [*HttpRequest::session()*](trait.RequestSession.html#tymethod.session) -//! must be used. This method returns a -//! [*Session*](struct.Session.html) object, which allows us to get or set -//! session data. +//! In general, you insert a *session* middleware and initialize it +//! , such as a `CookieSessionBackend`. To access session data, +//! [*Session*](struct.Session.html) extractor must be used. Session +//! extractor allows us to get or set session data. //! //! ```rust -//! # extern crate actix_web; -//! # extern crate actix; -//! use actix_web::{server, App, HttpRequest, Result}; -//! use actix_web::middleware::session::{RequestSession, SessionStorage, CookieSessionBackend}; +//! use actix_web::{App, HttpServer, HttpResponse, Error}; +//! use actix_session::{Session, CookieSession}; //! -//! fn index(req: HttpRequest) -> Result<&'static str> { +//! fn index(session: Session) -> Result<&'static str, Error> { //! // access session data -//! if let Some(count) = req.session().get::("counter")? { +//! if let Some(count) = session.get::("counter")? { //! println!("SESSION value: {}", count); -//! req.session().set("counter", count+1)?; +//! session.set("counter", count+1)?; //! } else { -//! req.session().set("counter", 1)?; +//! session.set("counter", 1)?; //! } //! //! Ok("Welcome!") //! } //! -//! fn main() { -//! actix::System::run(|| { -//! server::new( -//! || App::new().middleware( -//! SessionStorage::new( // <- create session middleware -//! CookieSessionBackend::signed(&[0; 32]) // <- create signed cookie session backend +//! fn main() -> std::io::Result<()> { +//! let sys = actix_rt::System::new("example"); // <- create Actix runtime +//! +//! HttpServer::new( +//! || App::new().middleware( +//! CookieSession::signed(&[0; 32]) // <- create cookie based session middleware //! .secure(false) -//! ))) -//! .bind("127.0.0.1:59880").unwrap() -//! .start(); -//! # actix::System::current().stop(); -//! }); +//! ) +//! .resource("/", |r| r.to(|| HttpResponse::Ok()))) +//! .bind("127.0.0.1:59880")? +//! .start(); +//! # actix_rt::System::current().stop(); +//! sys.run(); +//! Ok(()) //! } //! ``` use std::cell::RefCell; -use std::collections::HashMap; -use std::marker::PhantomData; use std::rc::Rc; -use std::sync::Arc; -use cookie::{Cookie, CookieJar, Key, SameSite}; -use futures::future::{err as FutErr, ok as FutOk, FutureResult}; -use futures::Future; -use http::header::{self, HeaderValue}; +use actix_web::{Error, FromRequest, HttpMessage}; +use actix_web::{ServiceFromRequest, ServiceRequest, ServiceResponse}; +use hashbrown::HashMap; use serde::de::DeserializeOwned; use serde::Serialize; use serde_json; -use serde_json::error::Error as JsonError; -use time::Duration; -use error::{Error, ResponseError, Result}; -use handler::FromRequest; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response, Started}; - -/// The helper trait to obtain your session data from a request. -/// -/// ```rust -/// use actix_web::middleware::session::RequestSession; -/// use actix_web::*; -/// -/// fn index(mut req: HttpRequest) -> Result<&'static str> { -/// // access session data -/// if let Some(count) = req.session().get::("counter")? { -/// req.session().set("counter", count + 1)?; -/// } else { -/// req.session().set("counter", 1)?; -/// } -/// -/// Ok("Welcome!") -/// } -/// # fn main() {} -/// ``` -pub trait RequestSession { - /// Get the session from the request - fn session(&self) -> Session; -} - -impl RequestSession for HttpRequest { - fn session(&self) -> Session { - if let Some(s_impl) = self.extensions().get::>() { - return Session(SessionInner::Session(Arc::clone(&s_impl))); - } - Session(SessionInner::None) - } -} +mod cookie; +pub use crate::cookie::CookieSession; /// The high-level interface you use to modify session data. /// @@ -124,80 +64,9 @@ impl RequestSession for HttpRequest { /// method. `RequestSession` trait is implemented for `HttpRequest`. /// /// ```rust -/// use actix_web::middleware::session::RequestSession; +/// use actix_session::Session; /// use actix_web::*; /// -/// fn index(mut req: HttpRequest) -> Result<&'static str> { -/// // access session data -/// if let Some(count) = req.session().get::("counter")? { -/// req.session().set("counter", count + 1)?; -/// } else { -/// req.session().set("counter", 1)?; -/// } -/// -/// Ok("Welcome!") -/// } -/// # fn main() {} -/// ``` -pub struct Session(SessionInner); - -enum SessionInner { - Session(Arc), - None, -} - -impl Session { - /// Get a `value` from the session. - pub fn get(&self, key: &str) -> Result> { - match self.0 { - SessionInner::Session(ref sess) => { - if let Some(s) = sess.as_ref().0.borrow().get(key) { - Ok(Some(serde_json::from_str(s)?)) - } else { - Ok(None) - } - } - SessionInner::None => Ok(None), - } - } - - /// Set a `value` from the session. - pub fn set(&self, key: &str, value: T) -> Result<()> { - match self.0 { - SessionInner::Session(ref sess) => { - sess.as_ref() - .0 - .borrow_mut() - .set(key, serde_json::to_string(&value)?); - Ok(()) - } - SessionInner::None => Ok(()), - } - } - - /// Remove value from the session. - pub fn remove(&self, key: &str) { - match self.0 { - SessionInner::Session(ref sess) => sess.as_ref().0.borrow_mut().remove(key), - SessionInner::None => (), - } - } - - /// Clear the session. - pub fn clear(&self) { - match self.0 { - SessionInner::Session(ref sess) => sess.as_ref().0.borrow_mut().clear(), - SessionInner::None => (), - } - } -} - -/// Extractor implementation for Session type. -/// -/// ```rust -/// # use actix_web::*; -/// use actix_web::middleware::session::Session; -/// /// fn index(session: Session) -> Result<&'static str> { /// // access session data /// if let Some(count) = session.get::("counter")? { @@ -210,409 +79,108 @@ impl Session { /// } /// # fn main() {} /// ``` -impl FromRequest for Session { - type Config = (); - type Result = Session; +pub struct Session(Rc>); - #[inline] - fn from_request(req: &HttpRequest, _: &Self::Config) -> Self::Result { - req.session() - } +#[derive(Default)] +struct SessionInner { + state: HashMap, + changed: bool, } -struct SessionImplCell(RefCell>); - -/// Session storage middleware -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::session::{CookieSessionBackend, SessionStorage}; -/// use actix_web::App; -/// -/// fn main() { -/// let app = App::new().middleware(SessionStorage::new( -/// // <- create session middleware -/// CookieSessionBackend::signed(&[0; 32]) // <- create cookie session backend -/// .secure(false), -/// )); -/// } -/// ``` -pub struct SessionStorage(T, PhantomData); - -impl> SessionStorage { - /// Create session storage - pub fn new(backend: T) -> SessionStorage { - SessionStorage(backend, PhantomData) - } -} - -impl> Middleware for SessionStorage { - fn start(&self, req: &HttpRequest) -> Result { - let mut req = req.clone(); - - let fut = self.0.from_request(&mut req).then(move |res| match res { - Ok(sess) => { - req.extensions_mut() - .insert(Arc::new(SessionImplCell(RefCell::new(Box::new(sess))))); - FutOk(None) - } - Err(err) => FutErr(err), - }); - Ok(Started::Future(Box::new(fut))) - } - - fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { - if let Some(s_box) = req.extensions().get::>() { - s_box.0.borrow_mut().write(resp) +impl Session { + /// Get a `value` from the session. + pub fn get(&self, key: &str) -> Result, Error> { + if let Some(s) = self.0.borrow().state.get(key) { + Ok(Some(serde_json::from_str(s)?)) } else { - Ok(Response::Done(resp)) + Ok(None) } } -} -/// A simple key-value storage interface that is internally used by `Session`. -pub trait SessionImpl: 'static { - /// Get session value by key - fn get(&self, key: &str) -> Option<&str>; + /// Set a `value` from the session. + pub fn set(&self, key: &str, value: T) -> Result<(), Error> { + let mut inner = self.0.borrow_mut(); + inner.changed = true; + inner + .state + .insert(key.to_owned(), serde_json::to_string(&value)?); + Ok(()) + } - /// Set session value - fn set(&mut self, key: &str, value: String); + /// Remove value from the session. + pub fn remove(&self, key: &str) { + let mut inner = self.0.borrow_mut(); + inner.changed = true; + inner.state.remove(key); + } - /// Remove specific key from session - fn remove(&mut self, key: &str); + /// Clear the session. + pub fn clear(&self) { + let mut inner = self.0.borrow_mut(); + inner.changed = true; + inner.state.clear() + } - /// Remove all values from session - fn clear(&mut self); + pub fn set_session

( + data: impl Iterator, + req: &mut ServiceRequest

, + ) { + let session = Session::get_session(req); + let mut inner = session.0.borrow_mut(); + inner.state.extend(data); + } - /// Write session to storage backend. - fn write(&self, resp: HttpResponse) -> Result; -} - -/// Session's storage backend trait definition. -pub trait SessionBackend: Sized + 'static { - /// Session item - type Session: SessionImpl; - /// Future that reads session - type ReadFuture: Future; - - /// Parse the session from request and load data from a storage backend. - fn from_request(&self, request: &mut HttpRequest) -> Self::ReadFuture; -} - -/// Session that uses signed cookies as session storage -pub struct CookieSession { - changed: bool, - state: HashMap, - inner: Rc, -} - -/// Errors that can occur during handling cookie session -#[derive(Fail, Debug)] -pub enum CookieSessionError { - /// Size of the serialized session is greater than 4000 bytes. - #[fail(display = "Size of the serialized session is greater than 4000 bytes.")] - Overflow, - /// Fail to serialize session. - #[fail(display = "Fail to serialize session")] - Serialize(JsonError), -} - -impl ResponseError for CookieSessionError {} - -impl SessionImpl for CookieSession { - fn get(&self, key: &str) -> Option<&str> { - if let Some(s) = self.state.get(key) { - Some(s) + pub fn get_changes( + res: &mut ServiceResponse, + ) -> Option> { + if let Some(s_impl) = res + .request() + .extensions() + .get::>>() + { + let state = + std::mem::replace(&mut s_impl.borrow_mut().state, HashMap::new()); + Some(state.into_iter()) } else { None } } - fn set(&mut self, key: &str, value: String) { - self.changed = true; - self.state.insert(key.to_owned(), value); - } - - fn remove(&mut self, key: &str) { - self.changed = true; - self.state.remove(key); - } - - fn clear(&mut self) { - self.changed = true; - self.state.clear() - } - - fn write(&self, mut resp: HttpResponse) -> Result { - if self.changed { - let _ = self.inner.set_cookie(&mut resp, &self.state); + fn get_session(req: R) -> Session { + if let Some(s_impl) = req.extensions().get::>>() { + return Session(Rc::clone(&s_impl)); } - Ok(Response::Done(resp)) + let inner = Rc::new(RefCell::new(SessionInner::default())); + req.extensions_mut().insert(inner.clone()); + Session(inner) } } -enum CookieSecurity { - Signed, - Private, -} - -struct CookieSessionInner { - key: Key, - security: CookieSecurity, - name: String, - path: String, - domain: Option, - secure: bool, - http_only: bool, - max_age: Option, - same_site: Option, -} - -impl CookieSessionInner { - fn new(key: &[u8], security: CookieSecurity) -> CookieSessionInner { - CookieSessionInner { - security, - key: Key::from_master(key), - name: "actix-session".to_owned(), - path: "/".to_owned(), - domain: None, - secure: true, - http_only: true, - max_age: None, - same_site: None, - } - } - - fn set_cookie( - &self, resp: &mut HttpResponse, state: &HashMap, - ) -> Result<()> { - let value = - serde_json::to_string(&state).map_err(CookieSessionError::Serialize)?; - if value.len() > 4064 { - return Err(CookieSessionError::Overflow.into()); - } - - let mut cookie = Cookie::new(self.name.clone(), value); - cookie.set_path(self.path.clone()); - cookie.set_secure(self.secure); - cookie.set_http_only(self.http_only); - - if let Some(ref domain) = self.domain { - cookie.set_domain(domain.clone()); - } - - if let Some(max_age) = self.max_age { - cookie.set_max_age(max_age); - } - - if let Some(same_site) = self.same_site { - cookie.set_same_site(same_site); - } - - let mut jar = CookieJar::new(); - - match self.security { - CookieSecurity::Signed => jar.signed(&self.key).add(cookie), - CookieSecurity::Private => jar.private(&self.key).add(cookie), - } - - for cookie in jar.delta() { - let val = HeaderValue::from_str(&cookie.encoded().to_string())?; - resp.headers_mut().append(header::SET_COOKIE, val); - } - - Ok(()) - } - - fn load(&self, req: &mut HttpRequest) -> HashMap { - if let Ok(cookies) = req.cookies() { - for cookie in cookies.iter() { - if cookie.name() == self.name { - let mut jar = CookieJar::new(); - jar.add_original(cookie.clone()); - - let cookie_opt = match self.security { - CookieSecurity::Signed => jar.signed(&self.key).get(&self.name), - CookieSecurity::Private => { - jar.private(&self.key).get(&self.name) - } - }; - if let Some(cookie) = cookie_opt { - if let Ok(val) = serde_json::from_str(cookie.value()) { - return val; - } - } - } - } - } - HashMap::new() - } -} - -/// Use cookies for session storage. -/// -/// `CookieSessionBackend` creates sessions which are limited to storing -/// fewer than 4000 bytes of data (as the payload must fit into a single -/// cookie). An Internal Server Error is generated if the session contains more -/// than 4000 bytes. -/// -/// A cookie may have a security policy of *signed* or *private*. Each has a -/// respective `CookieSessionBackend` constructor. -/// -/// A *signed* cookie is stored on the client as plaintext alongside -/// a signature such that the cookie may be viewed but not modified by the -/// client. -/// -/// A *private* cookie is stored on the client as encrypted text -/// such that it may neither be viewed nor modified by the client. -/// -/// The constructors take a key as an argument. -/// This is the private key for cookie session - when this value is changed, -/// all session data is lost. The constructors will panic if the key is less -/// than 32 bytes in length. -/// -/// The backend relies on `cookie` crate to create and read cookies. -/// By default all cookies are percent encoded, but certain symbols may -/// cause troubles when reading cookie, if they are not properly percent encoded. -/// -/// # Example +/// Extractor implementation for Session type. /// /// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::session::CookieSessionBackend; +/// # use actix_web::*; +/// use actix_session::Session; /// -/// # fn main() { -/// let backend: CookieSessionBackend = CookieSessionBackend::signed(&[0; 32]) -/// .domain("www.rust-lang.org") -/// .name("actix_session") -/// .path("/") -/// .secure(true); -/// # } +/// fn index(session: Session) -> Result<&'static str> { +/// // access session data +/// if let Some(count) = session.get::("counter")? { +/// session.set("counter", count + 1)?; +/// } else { +/// session.set("counter", 1)?; +/// } +/// +/// Ok("Welcome!") +/// } +/// # fn main() {} /// ``` -pub struct CookieSessionBackend(Rc); +impl

FromRequest

for Session { + type Error = Error; + type Future = Result; + type Config = (); -impl CookieSessionBackend { - /// Construct new *signed* `CookieSessionBackend` instance. - /// - /// Panics if key length is less than 32 bytes. - pub fn signed(key: &[u8]) -> CookieSessionBackend { - CookieSessionBackend(Rc::new(CookieSessionInner::new( - key, - CookieSecurity::Signed, - ))) - } - - /// Construct new *private* `CookieSessionBackend` instance. - /// - /// Panics if key length is less than 32 bytes. - pub fn private(key: &[u8]) -> CookieSessionBackend { - CookieSessionBackend(Rc::new(CookieSessionInner::new( - key, - CookieSecurity::Private, - ))) - } - - /// Sets the `path` field in the session cookie being built. - pub fn path>(mut self, value: S) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().path = value.into(); - self - } - - /// Sets the `name` field in the session cookie being built. - pub fn name>(mut self, value: S) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().name = value.into(); - self - } - - /// Sets the `domain` field in the session cookie being built. - pub fn domain>(mut self, value: S) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into()); - self - } - - /// Sets the `secure` field in the session cookie being built. - /// - /// If the `secure` field is set, a cookie will only be transmitted when the - /// connection is secure - i.e. `https` - pub fn secure(mut self, value: bool) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().secure = value; - self - } - - /// Sets the `http_only` field in the session cookie being built. - pub fn http_only(mut self, value: bool) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().http_only = value; - self - } - - /// Sets the `same_site` field in the session cookie being built. - pub fn same_site(mut self, value: SameSite) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().same_site = Some(value); - self - } - - /// Sets the `max-age` field in the session cookie being built. - pub fn max_age(mut self, value: Duration) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); - self - } -} - -impl SessionBackend for CookieSessionBackend { - type Session = CookieSession; - type ReadFuture = FutureResult; - - fn from_request(&self, req: &mut HttpRequest) -> Self::ReadFuture { - let state = self.0.load(req); - FutOk(CookieSession { - changed: false, - inner: Rc::clone(&self.0), - state, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use application::App; - use test; - - #[test] - fn cookie_session() { - let mut srv = test::TestServer::with_factory(|| { - App::new() - .middleware(SessionStorage::new( - CookieSessionBackend::signed(&[0; 32]).secure(false), - )).resource("/", |r| { - r.f(|req| { - let _ = req.session().set("counter", 100); - "test" - }) - }) - }); - - let request = srv.get().uri(srv.url("/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.cookie("actix-session").is_some()); - } - - #[test] - fn cookie_session_extractor() { - let mut srv = test::TestServer::with_factory(|| { - App::new() - .middleware(SessionStorage::new( - CookieSessionBackend::signed(&[0; 32]).secure(false), - )).resource("/", |r| { - r.with(|ses: Session| { - let _ = ses.set("counter", 100); - "test" - }) - }) - }); - - let request = srv.get().uri(srv.url("/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.cookie("actix-session").is_some()); + #[inline] + fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { + Ok(Session::get_session(req)) } } diff --git a/src/request.rs b/src/request.rs index d90627f5..75daf59d 100644 --- a/src/request.rs +++ b/src/request.rs @@ -64,12 +64,6 @@ impl HttpRequest { self.head().uri.path() } - #[inline] - /// Returns Request's headers. - pub fn headers(&self) -> &HeaderMap { - &self.head().headers - } - /// The query string in the URL. /// /// E.g., id=10 @@ -93,18 +87,6 @@ impl HttpRequest { &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() - } - /// Application extensions #[inline] pub fn app_extensions(&self) -> &Extensions { @@ -130,8 +112,26 @@ impl HttpMessage for HttpRequest { type Stream = (); #[inline] + /// Returns Request's headers. fn headers(&self) -> &HeaderMap { - self.headers() + &self.head().headers + } + + #[inline] + fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.head.headers + } + + /// Request extensions + #[inline] + fn extensions(&self) -> Ref { + self.head.extensions() + } + + /// Mutable reference to a the request's extensions + #[inline] + fn extensions_mut(&self) -> RefMut { + self.head.extensions_mut() } #[inline] diff --git a/src/service.rs b/src/service.rs index 50b2924a..0da66439 100644 --- a/src/service.rs +++ b/src/service.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use std::cell::{Ref, RefMut}; use std::rc::Rc; -use actix_http::body::{Body, MessageBody, ResponseBody}; +use actix_http::body::{Body, ResponseBody}; use actix_http::http::{HeaderMap, Method, Uri, Version}; use actix_http::{ Error, Extensions, HttpMessage, Payload, PayloadStream, Request, RequestHead, @@ -84,12 +84,6 @@ impl

ServiceRequest

{ self.head().uri.path() } - #[inline] - /// Returns Request's headers. - pub fn headers(&self) -> &HeaderMap { - &self.head().headers - } - /// The query string in the URL. /// /// E.g., id=10 @@ -118,18 +112,6 @@ impl

ServiceRequest

{ &mut self.req.path } - /// Request extensions - #[inline] - pub fn extensions(&self) -> Ref { - self.req.head.extensions() - } - - /// Mutable reference to a the request's extensions - #[inline] - pub fn extensions_mut(&self) -> RefMut { - self.req.head.extensions_mut() - } - /// Application extensions #[inline] pub fn app_extensions(&self) -> &Extensions { @@ -147,8 +129,27 @@ impl

HttpMessage for ServiceRequest

{ type Stream = P; #[inline] + /// Returns Request's headers. fn headers(&self) -> &HeaderMap { - self.req.headers() + &self.head().headers + } + + #[inline] + /// Mutable reference to the request's headers. + fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.head_mut().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] @@ -229,6 +230,23 @@ impl

HttpMessage for ServiceFromRequest

{ self.req.headers() } + #[inline] + fn headers_mut(&mut self) -> &mut HeaderMap { + self.req.headers_mut() + } + + /// 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) @@ -275,11 +293,26 @@ impl ServiceResponse { pub fn headers_mut(&mut self) -> &mut HeaderMap { self.response.headers_mut() } + + /// Execute closure and in case of error convert it to response. + pub fn checked_expr(mut self, f: F) -> Self + where + F: FnOnce(&mut Self) -> Result<(), E>, + E: Into, + { + match f(&mut self) { + Ok(_) => self, + Err(err) => { + let res: Response = err.into().into(); + ServiceResponse::new(self.request, res.into_body()) + } + } + } } -impl ServiceResponse { +impl ServiceResponse { /// Set a new body - pub fn map_body(self, f: F) -> ServiceResponse + pub fn map_body(self, f: F) -> ServiceResponse where F: FnOnce(&mut ResponseHead, ResponseBody) -> ResponseBody, { @@ -292,7 +325,7 @@ impl ServiceResponse { } } -impl std::ops::Deref for ServiceResponse { +impl std::ops::Deref for ServiceResponse { type Target = Response; fn deref(&self) -> &Response { @@ -300,19 +333,19 @@ impl std::ops::Deref for ServiceResponse { } } -impl std::ops::DerefMut for ServiceResponse { +impl std::ops::DerefMut for ServiceResponse { fn deref_mut(&mut self) -> &mut Response { self.response_mut() } } -impl Into> for ServiceResponse { +impl Into> for ServiceResponse { fn into(self) -> Response { self.response } } -impl IntoFuture for ServiceResponse { +impl IntoFuture for ServiceResponse { type Item = ServiceResponse; type Error = Error; type Future = FutureResult, Error>; diff --git a/src/test.rs b/src/test.rs index 7ceedacc..8b6667a4 100644 --- a/src/test.rs +++ b/src/test.rs @@ -8,11 +8,12 @@ use actix_http::test::TestRequest as HttpTestRequest; use actix_http::{Extensions, PayloadStream, Request}; use actix_router::{Path, Url}; use actix_rt::Runtime; +use actix_service::{IntoNewService, NewService, Service}; use bytes::Bytes; use futures::Future; use crate::request::HttpRequest; -use crate::service::{ServiceFromRequest, ServiceRequest}; +use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; thread_local! { static RT: RefCell = { @@ -37,6 +38,34 @@ where RT.with(move |rt| rt.borrow_mut().block_on(f)) } +/// This method accepts application builder instance, and constructs +/// service. +/// +/// ```rust +/// use actix_http::http::{test, App, HttpResponse}; +/// +/// fn main() { +/// let app = test::init_service( +/// App::new() +/// .resource("/test", |r| r.to(|| HttpResponse::Ok())) +/// ) +/// +/// let req = TestRequest::with_uri("/test").to_request(); +/// let resp = block_on(srv.call(req)).unwrap(); +/// assert_eq!(resp.status(), StatusCode::OK); +/// } +/// ``` +pub fn init_service( + app: R, +) -> impl Service, Error = E> +where + R: IntoNewService, + S: NewService, Error = E>, + S::InitError: std::fmt::Debug, +{ + block_on(app.into_new_service().new_service(&())).unwrap() +} + /// Test `Request` builder. /// /// For unit testing, actix provides a request builder type and a simple handler runner. TestRequest implements a builder-like pattern. @@ -112,6 +141,22 @@ impl TestRequest { } } + /// Create TestRequest and set method to `Method::GET` + pub fn get() -> TestRequest { + TestRequest { + req: HttpTestRequest::default().method(Method::GET).take(), + extensions: Extensions::new(), + } + } + + /// Create TestRequest and set method to `Method::POST` + pub fn post() -> TestRequest { + TestRequest { + req: HttpTestRequest::default().method(Method::POST).take(), + extensions: Extensions::new(), + } + } + /// Set HTTP version of this request pub fn version(mut self, ver: Version) -> Self { self.req.version(ver); From 0cf73f1a04d23995bffd8db21f00107713baf209 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 18:52:29 -0800 Subject: [PATCH 034/109] move session to different folder --- Cargo.toml | 2 +- {session => actix-session}/Cargo.toml | 0 {session => actix-session}/src/cookie.rs | 0 {session => actix-session}/src/lib.rs | 0 src/test.rs | 14 +++++++++----- 5 files changed, 10 insertions(+), 6 deletions(-) rename {session => actix-session}/Cargo.toml (100%) rename {session => actix-session}/src/cookie.rs (100%) rename {session => actix-session}/src/lib.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 2f69c7ef..2f50b210 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ path = "src/lib.rs" [workspace] members = [ ".", - "session", + "actix-session", "staticfiles", ] diff --git a/session/Cargo.toml b/actix-session/Cargo.toml similarity index 100% rename from session/Cargo.toml rename to actix-session/Cargo.toml diff --git a/session/src/cookie.rs b/actix-session/src/cookie.rs similarity index 100% rename from session/src/cookie.rs rename to actix-session/src/cookie.rs diff --git a/session/src/lib.rs b/actix-session/src/lib.rs similarity index 100% rename from session/src/lib.rs rename to actix-session/src/lib.rs diff --git a/src/test.rs b/src/test.rs index 8b6667a4..8495ea89 100644 --- a/src/test.rs +++ b/src/test.rs @@ -42,16 +42,20 @@ where /// service. /// /// ```rust -/// use actix_http::http::{test, App, HttpResponse}; +/// use actix_web::{test, App, HttpResponse, http::StatusCode}; +/// use actix_service::Service; /// /// fn main() { -/// let app = test::init_service( +/// let mut app = test::init_service( /// App::new() /// .resource("/test", |r| r.to(|| HttpResponse::Ok())) -/// ) +/// ); /// -/// let req = TestRequest::with_uri("/test").to_request(); -/// let resp = block_on(srv.call(req)).unwrap(); +/// // Create request object +/// let req = test::TestRequest::with_uri("/test").to_request(); +/// +/// // Execute application +/// let resp = test::block_on(app.call(req)).unwrap(); /// assert_eq!(resp.status(), StatusCode::OK); /// } /// ``` From 81273f71ef521fe9ec131d13ab4a57e19bb13c99 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 19:03:59 -0800 Subject: [PATCH 035/109] update tests --- src/app.rs | 68 +++++++++++++++++++++++++--------------------------- src/scope.rs | 33 ++++++++++--------------- src/test.rs | 10 ++++---- 3 files changed, 51 insertions(+), 60 deletions(-) diff --git a/src/app.rs b/src/app.rs index b2164501..e38180c4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -902,16 +902,14 @@ mod tests { use actix_http::http::{Method, StatusCode}; use super::*; - use crate::test::{block_on, TestRequest}; + use crate::test::{self, block_on, TestRequest}; use crate::{web, HttpResponse, State}; #[test] fn test_default_resource() { - let app = App::new() - .resource("/test", |r| r.to(|| HttpResponse::Ok())) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); - + let mut srv = test::init_service( + App::new().resource("/test", |r| r.to(|| HttpResponse::Ok())), + ); let req = TestRequest::with_uri("/test").to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); @@ -920,15 +918,15 @@ mod tests { let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); - let app = App::new() - .resource("/test", |r| r.to(|| HttpResponse::Ok())) - .resource("/test2", |r| { - r.default_resource(|r| r.to(|| HttpResponse::Created())) - .route(web::get().to(|| HttpResponse::Ok())) - }) - .default_resource(|r| r.to(|| HttpResponse::MethodNotAllowed())) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = test::init_service( + App::new() + .resource("/test", |r| r.to(|| HttpResponse::Ok())) + .resource("/test2", |r| { + r.default_resource(|r| r.to(|| HttpResponse::Created())) + .route(web::get().to(|| HttpResponse::Ok())) + }) + .default_resource(|r| r.to(|| HttpResponse::MethodNotAllowed())), + ); let req = TestRequest::with_uri("/blah").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -947,21 +945,21 @@ mod tests { #[test] fn test_state() { - let app = App::new() - .state(10usize) - .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = test::init_service( + App::new() + .state(10usize) + .resource("/", |r| r.to(|_: State| HttpResponse::Ok())), + ); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); - let app = App::new() - .state(10u32) - .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = test::init_service( + App::new() + .state(10u32) + .resource("/", |r| r.to(|_: State| HttpResponse::Ok())), + ); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -970,21 +968,21 @@ mod tests { #[test] fn test_state_factory() { - let app = App::new() - .state_factory(|| Ok::<_, ()>(10usize)) - .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = test::init_service( + App::new() + .state_factory(|| Ok::<_, ()>(10usize)) + .resource("/", |r| r.to(|_: State| HttpResponse::Ok())), + ); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); - let app = App::new() - .state_factory(|| Ok::<_, ()>(10u32)) - .resource("/", |r| r.to(|_: State| HttpResponse::Ok())) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = test::init_service( + App::new() + .state_factory(|| Ok::<_, ()>(10u32)) + .resource("/", |r| r.to(|_: State| HttpResponse::Ok())), + ); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); diff --git a/src/scope.rs b/src/scope.rs index fa7392b4..dc88388f 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -509,17 +509,14 @@ mod tests { use actix_service::{IntoNewService, NewService, Service}; use bytes::Bytes; - use crate::test::{block_on, TestRequest}; + use crate::test::{self, block_on, TestRequest}; use crate::{guard, web, App, HttpRequest, HttpResponse}; #[test] fn test_scope() { - let app = App::new() - .scope("/app", |scope| { - scope.resource("/path1", |r| r.to(|| HttpResponse::Ok())) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = test::init_service(App::new().scope("/app", |scope| { + scope.resource("/path1", |r| r.to(|| HttpResponse::Ok())) + })); let req = TestRequest::with_uri("/app/path1").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -528,14 +525,11 @@ mod tests { #[test] fn test_scope_root() { - let app = App::new() - .scope("/app", |scope| { - scope - .resource("", |r| r.to(|| HttpResponse::Ok())) - .resource("/", |r| r.to(|| HttpResponse::Created())) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = test::init_service(App::new().scope("/app", |scope| { + scope + .resource("", |r| r.to(|| HttpResponse::Ok())) + .resource("/", |r| r.to(|| HttpResponse::Created())) + })); let req = TestRequest::with_uri("/app").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -548,12 +542,9 @@ mod tests { #[test] fn test_scope_root2() { - let app = App::new() - .scope("/app/", |scope| { - scope.resource("", |r| r.to(|| HttpResponse::Ok())) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = test::init_service(App::new().scope("/app/", |scope| { + scope.resource("", |r| r.to(|| HttpResponse::Ok())) + })); let req = TestRequest::with_uri("/app").to_request(); let resp = block_on(srv.call(req)).unwrap(); diff --git a/src/test.rs b/src/test.rs index 8495ea89..22bfe0c3 100644 --- a/src/test.rs +++ b/src/test.rs @@ -41,7 +41,7 @@ where /// This method accepts application builder instance, and constructs /// service. /// -/// ```rust +/// ```rust,ignore /// use actix_web::{test, App, HttpResponse, http::StatusCode}; /// use actix_service::Service; /// @@ -80,7 +80,9 @@ where /// * `TestRequest::to_http_request` creates `HttpRequest` instance, which is used for testing handlers. /// /// ```rust,ignore -/// use actix_web::test; +/// # use futures::IntoFuture; +/// use actix_web::{test, HttpRequest, HttpResponse, HttpMessage}; +/// use actix_web::http::{header, StatusCode}; /// /// fn index(req: HttpRequest) -> HttpResponse { /// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) { @@ -94,11 +96,11 @@ where /// let req = test::TestRequest::with_header("content-type", "text/plain") /// .to_http_request(); /// -/// let resp = test::block_on(index(req)); +/// let resp = test::block_on(index(req).into_future()).unwrap(); /// assert_eq!(resp.status(), StatusCode::OK); /// /// let req = test::TestRequest::default().to_http_request(); -/// let resp = test::block_on(index(req)); +/// let resp = test::block_on(index(req).into_future()).unwrap(); /// assert_eq!(resp.status(), StatusCode::BAD_REQUEST); /// } /// ``` From f71354783e3d7d75fe9210fa612745be0a1588b3 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 19:10:45 -0800 Subject: [PATCH 036/109] update HttpMessage impls --- src/request.rs | 5 ----- src/service.rs | 17 ++++++----------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/request.rs b/src/request.rs index 75daf59d..211f60b8 100644 --- a/src/request.rs +++ b/src/request.rs @@ -117,11 +117,6 @@ impl HttpMessage for HttpRequest { &self.head().headers } - #[inline] - fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.head.headers - } - /// Request extensions #[inline] fn extensions(&self) -> Ref { diff --git a/src/service.rs b/src/service.rs index 0da66439..7d17527a 100644 --- a/src/service.rs +++ b/src/service.rs @@ -78,6 +78,12 @@ impl

ServiceRequest

{ self.head().version } + #[inline] + /// Returns mutable Request's headers. + pub fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.head_mut().headers + } + /// The target path of this Request. #[inline] pub fn path(&self) -> &str { @@ -134,12 +140,6 @@ impl

HttpMessage for ServiceRequest

{ &self.head().headers } - #[inline] - /// Mutable reference to the request's headers. - fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.head_mut().headers - } - /// Request extensions #[inline] fn extensions(&self) -> Ref { @@ -230,11 +230,6 @@ impl

HttpMessage for ServiceFromRequest

{ self.req.headers() } - #[inline] - fn headers_mut(&mut self) -> &mut HeaderMap { - self.req.headers_mut() - } - /// Request extensions #[inline] fn extensions(&self) -> Ref { From 0de47211b2065d48a6ac7d341870b05ab5db8f79 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 19:30:44 -0800 Subject: [PATCH 037/109] tune App::default_resource signature --- src/app.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/app.rs b/src/app.rs index e38180c4..27ca5c95 100644 --- a/src/app.rs +++ b/src/app.rs @@ -426,12 +426,15 @@ where /// /// Default resource works with resources only and does not work with /// custom services. - pub fn default_resource(mut self, f: F) -> Self + pub fn default_resource(mut self, f: F) -> Self where - F: FnOnce(Resource

) -> R, - R: IntoNewService>, - U: NewService, Response = ServiceResponse, Error = ()> - + 'static, + F: FnOnce(Resource

) -> Resource, + U: NewService< + ServiceRequest

, + Response = ServiceResponse, + Error = (), + InitError = (), + > + 'static, { // create and configure default resource self.default = Some(Rc::new(boxed::new_service( From 1a80b70868a79cc5fc03f04b105adac7bae36769 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 19:41:50 -0800 Subject: [PATCH 038/109] add Responder impl for InternalError --- src/responder.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/responder.rs b/src/responder.rs index b2fd848f..dedfa1b4 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -1,3 +1,4 @@ +use actix_http::error::InternalError; use actix_http::{dev::ResponseBuilder, http::StatusCode, Error, Response}; use bytes::{Bytes, BytesMut}; use futures::future::{err, ok, Either as EitherFuture, FutureResult}; @@ -252,6 +253,18 @@ where } } +impl Responder for InternalError +where + T: std::fmt::Debug + std::fmt::Display + 'static, +{ + type Error = Error; + type Future = Result; + + fn respond_to(self, _: &HttpRequest) -> Self::Future { + Err(self.into()) + } +} + pub struct ResponseFuture(T); impl ResponseFuture { From 6efc3438b83497577b8d791b77c6600e95660f35 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 5 Mar 2019 22:10:08 -0800 Subject: [PATCH 039/109] refactor and enable some tests for staticfiles --- .travis.yml | 4 +- Cargo.toml | 2 +- {staticfiles => actix-staticfiles}/CHANGES.md | 0 {staticfiles => actix-staticfiles}/Cargo.toml | 3 +- {staticfiles => actix-staticfiles}/README.md | 0 actix-staticfiles/src/config.rs | 70 + actix-staticfiles/src/error.rs | 41 + actix-staticfiles/src/lib.rs | 1482 ++++++++++++ actix-staticfiles/src/named.rs | 438 ++++ actix-staticfiles/tests/test space.binary | 1 + actix-staticfiles/tests/test.binary | 1 + actix-staticfiles/tests/test.png | Bin 0 -> 168 bytes examples/basic.rs | 25 +- src/app.rs | 45 +- src/lib.rs | 24 +- src/service.rs | 64 +- src/test.rs | 28 + staticfiles/src/lib.rs | 2033 ----------------- 18 files changed, 2198 insertions(+), 2063 deletions(-) rename {staticfiles => actix-staticfiles}/CHANGES.md (100%) rename {staticfiles => actix-staticfiles}/Cargo.toml (93%) rename {staticfiles => actix-staticfiles}/README.md (100%) create mode 100644 actix-staticfiles/src/config.rs create mode 100644 actix-staticfiles/src/error.rs create mode 100644 actix-staticfiles/src/lib.rs create mode 100644 actix-staticfiles/src/named.rs create mode 100644 actix-staticfiles/tests/test space.binary create mode 100644 actix-staticfiles/tests/test.binary create mode 100644 actix-staticfiles/tests/test.png delete mode 100644 staticfiles/src/lib.rs diff --git a/.travis.yml b/.travis.yml index 1d3c227a..55a03ec8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,7 +35,7 @@ before_script: script: - cargo clean - - cargo test -- --nocapture + - cargo test --all -- --nocapture # Upload docs after_success: @@ -49,7 +49,7 @@ after_success: fi - | if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then - cargo tarpaulin --out Xml + cargo tarpaulin --out Xml --all bash <(curl -s https://codecov.io/bash) echo "Uploaded code coverage" fi diff --git a/Cargo.toml b/Cargo.toml index 2f50b210..acacb2f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ path = "src/lib.rs" members = [ ".", "actix-session", - "staticfiles", + "actix-staticfiles", ] [package.metadata.docs.rs] diff --git a/staticfiles/CHANGES.md b/actix-staticfiles/CHANGES.md similarity index 100% rename from staticfiles/CHANGES.md rename to actix-staticfiles/CHANGES.md diff --git a/staticfiles/Cargo.toml b/actix-staticfiles/Cargo.toml similarity index 93% rename from staticfiles/Cargo.toml rename to actix-staticfiles/Cargo.toml index 0aa58970..0a551792 100644 --- a/staticfiles/Cargo.toml +++ b/actix-staticfiles/Cargo.toml @@ -20,7 +20,8 @@ path = "src/lib.rs" [dependencies] actix-web = { path=".." } actix-http = { git = "https://github.com/actix/actix-http.git" } -actix-service = "0.3.0" +actix-service = { git = "https://github.com/actix/actix-net.git" } +#actix-service = "0.3.0" bytes = "0.4" futures = "0.1" diff --git a/staticfiles/README.md b/actix-staticfiles/README.md similarity index 100% rename from staticfiles/README.md rename to actix-staticfiles/README.md diff --git a/actix-staticfiles/src/config.rs b/actix-staticfiles/src/config.rs new file mode 100644 index 00000000..da72da20 --- /dev/null +++ b/actix-staticfiles/src/config.rs @@ -0,0 +1,70 @@ +use actix_http::http::header::DispositionType; +use actix_web::http::Method; +use mime; + +/// Describes `StaticFiles` configiration +/// +/// To configure actix's static resources you need +/// to define own configiration type and implement any method +/// you wish to customize. +/// As trait implements reasonable defaults for Actix. +/// +/// ## Example +/// +/// ```rust,ignore +/// extern crate mime; +/// extern crate actix_web; +/// use actix_web::http::header::DispositionType; +/// use actix_web::fs::{StaticFileConfig, NamedFile}; +/// +/// #[derive(Default)] +/// struct MyConfig; +/// +/// impl StaticFileConfig for MyConfig { +/// fn content_disposition_map(typ: mime::Name) -> DispositionType { +/// DispositionType::Attachment +/// } +/// } +/// +/// let file = NamedFile::open_with_config("foo.txt", MyConfig); +/// ``` +pub trait StaticFileConfig: Default { + ///Describes mapping for mime type to content disposition header + /// + ///By default `IMAGE`, `TEXT` and `VIDEO` are mapped to Inline. + ///Others are mapped to Attachment + fn content_disposition_map(typ: mime::Name) -> DispositionType { + match typ { + mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, + _ => DispositionType::Attachment, + } + } + + ///Describes whether Actix should attempt to calculate `ETag` + /// + ///Defaults to `true` + fn is_use_etag() -> bool { + true + } + + ///Describes whether Actix should use last modified date of file. + /// + ///Defaults to `true` + fn is_use_last_modifier() -> bool { + true + } + + ///Describes allowed methods to access static resources. + /// + ///By default all methods are allowed + fn is_method_allowed(_method: &Method) -> bool { + true + } +} + +///Default content disposition as described in +///[StaticFileConfig](trait.StaticFileConfig.html) +#[derive(Default)] +pub struct DefaultConfig; + +impl StaticFileConfig for DefaultConfig {} diff --git a/actix-staticfiles/src/error.rs b/actix-staticfiles/src/error.rs new file mode 100644 index 00000000..f165a618 --- /dev/null +++ b/actix-staticfiles/src/error.rs @@ -0,0 +1,41 @@ +use actix_web::{http::StatusCode, HttpResponse, ResponseError}; +use derive_more::Display; + +/// Errors which can occur when serving static files. +#[derive(Display, Debug, PartialEq)] +pub enum StaticFilesError { + /// Path is not a directory + #[display(fmt = "Path is not a directory. Unable to serve static files")] + IsNotDirectory, + + /// Cannot render directory + #[display(fmt = "Unable to render directory without index file")] + IsDirectory, +} + +/// Return `NotFound` for `StaticFilesError` +impl ResponseError for StaticFilesError { + fn error_response(&self) -> HttpResponse { + HttpResponse::new(StatusCode::NOT_FOUND) + } +} + +#[derive(Display, Debug, PartialEq)] +pub enum UriSegmentError { + /// The segment started with the wrapped invalid character. + #[display(fmt = "The segment started with the wrapped invalid character")] + BadStart(char), + /// The segment contained the wrapped invalid character. + #[display(fmt = "The segment contained the wrapped invalid character")] + BadChar(char), + /// The segment ended with the wrapped invalid character. + #[display(fmt = "The segment ended with the wrapped invalid character")] + BadEnd(char), +} + +/// Return `BadRequest` for `UriSegmentError` +impl ResponseError for UriSegmentError { + fn error_response(&self) -> HttpResponse { + HttpResponse::new(StatusCode::BAD_REQUEST) + } +} diff --git a/actix-staticfiles/src/lib.rs b/actix-staticfiles/src/lib.rs new file mode 100644 index 00000000..01306d4b --- /dev/null +++ b/actix-staticfiles/src/lib.rs @@ -0,0 +1,1482 @@ +//! Static files support +use std::cell::RefCell; +use std::fmt::Write; +use std::fs::{DirEntry, File}; +use std::io::{Read, Seek}; +use std::marker::PhantomData; +use std::path::{Path, PathBuf}; +use std::rc::Rc; +use std::{cmp, io}; + +use bytes::Bytes; +use futures::{Async, Future, Poll, Stream}; +use mime; +use mime_guess::get_mime_type; +use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; +use v_htmlescape::escape as escape_html_entity; + +use actix_http::error::{Error, ErrorInternalServerError}; +use actix_service::{boxed::BoxedNewService, NewService, Service}; +use actix_web::dev::{self, HttpServiceFactory, ResourceDef, Url}; +use actix_web::{ + blocking, FromRequest, HttpRequest, HttpResponse, Responder, ServiceFromRequest, + ServiceRequest, ServiceResponse, +}; +use futures::future::{ok, FutureResult}; + +mod config; +mod error; +mod named; + +use self::error::{StaticFilesError, UriSegmentError}; +pub use crate::config::{DefaultConfig, StaticFileConfig}; +pub use crate::named::NamedFile; + +type HttpNewService

= BoxedNewService<(), ServiceRequest

, ServiceResponse, (), ()>; + +/// 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 +/// the type `application/octet-stream`. +#[inline] +pub fn file_extension_to_mime(ext: &str) -> mime::Mime { + get_mime_type(ext) +} + +#[doc(hidden)] +/// A helper created from a `std::fs::File` which reads the file +/// chunk-by-chunk on a `ThreadPool`. +pub struct ChunkedReadFile { + size: u64, + offset: u64, + file: Option, + fut: Option>, + counter: u64, +} + +fn handle_error(err: blocking::BlockingError) -> Error { + match err { + blocking::BlockingError::Error(err) => err.into(), + blocking::BlockingError::Canceled => { + ErrorInternalServerError("Unexpected error").into() + } + } +} + +impl Stream for ChunkedReadFile { + type Item = Bytes; + type Error = Error; + + fn poll(&mut self) -> Poll, Error> { + if self.fut.is_some() { + return match self.fut.as_mut().unwrap().poll().map_err(handle_error)? { + Async::Ready((file, bytes)) => { + self.fut.take(); + self.file = Some(file); + self.offset += bytes.len() as u64; + self.counter += bytes.len() as u64; + Ok(Async::Ready(Some(bytes))) + } + Async::NotReady => Ok(Async::NotReady), + }; + } + + let size = self.size; + let offset = self.offset; + let counter = self.counter; + + if size == counter { + Ok(Async::Ready(None)) + } else { + let mut file = self.file.take().expect("Use after completion"); + self.fut = Some(blocking::run(move || { + let max_bytes: usize; + max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; + let mut buf = Vec::with_capacity(max_bytes); + file.seek(io::SeekFrom::Start(offset))?; + let nbytes = + file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?; + if nbytes == 0 { + return Err(io::ErrorKind::UnexpectedEof.into()); + } + Ok((file, Bytes::from(buf))) + })); + self.poll() + } + } +} + +type DirectoryRenderer = + Fn(&Directory, &HttpRequest) -> Result; + +/// A directory; responds with the generated directory listing. +#[derive(Debug)] +pub struct Directory { + /// Base directory + pub base: PathBuf, + /// Path of subdirectory to generate listing for + pub path: PathBuf, +} + +impl Directory { + /// Create a new directory + pub fn new(base: PathBuf, path: PathBuf) -> Directory { + Directory { base, path } + } + + /// Is this entry visible from this directory? + pub fn is_visible(&self, entry: &io::Result) -> bool { + if let Ok(ref entry) = *entry { + if let Some(name) = entry.file_name().to_str() { + if name.starts_with('.') { + return false; + } + } + if let Ok(ref md) = entry.metadata() { + let ft = md.file_type(); + return ft.is_dir() || ft.is_file() || ft.is_symlink(); + } + } + false + } +} + +// show file url as relative to static path +macro_rules! encode_file_url { + ($path:ident) => { + utf8_percent_encode(&$path.to_string_lossy(), DEFAULT_ENCODE_SET) + }; +} + +// " -- " & -- & ' -- ' < -- < > -- > / -- / +macro_rules! encode_file_name { + ($entry:ident) => { + escape_html_entity(&$entry.file_name().to_string_lossy()) + }; +} + +fn directory_listing( + dir: &Directory, + req: &HttpRequest, +) -> Result { + let index_of = format!("Index of {}", req.path()); + let mut body = String::new(); + let base = Path::new(req.path()); + + for entry in dir.path.read_dir()? { + if dir.is_visible(&entry) { + let entry = entry.unwrap(); + let p = match entry.path().strip_prefix(&dir.path) { + Ok(p) => base.join(p), + Err(_) => continue, + }; + + // if file is a directory, add '/' to the end of the name + if let Ok(metadata) = entry.metadata() { + if metadata.is_dir() { + let _ = write!( + body, + "

  • {}/
  • ", + encode_file_url!(p), + encode_file_name!(entry), + ); + } else { + let _ = write!( + body, + "
  • {}
  • ", + encode_file_url!(p), + encode_file_name!(entry), + ); + } + } else { + continue; + } + } + } + + let html = format!( + "\ + {}\ +

    {}

    \ +
      \ + {}\ +
    \n", + index_of, index_of, body + ); + Ok(ServiceResponse::new( + req.clone(), + HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(html), + )) +} + +/// Static files handling +/// +/// `StaticFile` handler must be registered with `App::handler()` method, +/// because `StaticFile` handler requires access sub-path information. +/// +/// ```rust,ignore +/// # extern crate actix_web; +/// use actix_web::{fs, App}; +/// +/// fn main() { +/// let app = App::new() +/// .handler("/static", fs::StaticFiles::new(".").unwrap()) +/// .finish(); +/// } +/// ``` +pub struct StaticFiles { + path: ResourceDef, + directory: PathBuf, + index: Option, + show_index: bool, + default: Rc>>>>, + renderer: Rc, + _chunk_size: usize, + _follow_symlinks: bool, + _cd_map: PhantomData, +} + +impl StaticFiles { + /// Create new `StaticFiles` instance for specified base directory. + /// + /// `StaticFile` 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) -> Result, Error> { + Self::with_config(path, dir, DefaultConfig) + } +} + +impl StaticFiles { + /// Create new `StaticFiles` instance for specified base directory. + /// + /// Identical with `new` but allows to specify configiration to use. + pub fn with_config>( + path: &str, + dir: T, + _: C, + ) -> Result, Error> { + let dir = dir.into().canonicalize()?; + + if !dir.is_dir() { + return Err(StaticFilesError::IsNotDirectory.into()); + } + + Ok(StaticFiles { + path: ResourceDef::root_prefix(path), + directory: dir, + index: None, + show_index: false, + default: Rc::new(RefCell::new(None)), + renderer: Rc::new(directory_listing), + _chunk_size: 0, + _follow_symlinks: false, + _cd_map: PhantomData, + }) + } + + /// Show files listing for directories. + /// + /// By default show files listing is disabled. + pub fn show_files_listing(mut self) -> Self { + self.show_index = true; + self + } + + /// Set custom directory renderer + pub fn files_listing_renderer(mut self, f: F) -> Self + where + for<'r, 's> F: + Fn(&'r Directory, &'s HttpRequest) -> Result + + 'static, + { + self.renderer = Rc::new(f); + self + } + + /// Set index file + /// + /// Shows specific index file for directory "/" instead of + /// showing files listing. + pub fn index_file>(mut self, index: T) -> StaticFiles { + self.index = Some(index.into()); + self + } +} + +impl HttpServiceFactory

    for StaticFiles { + type Factory = Self; + + fn rdef(&self) -> &ResourceDef { + &self.path + } + + fn create(self) -> Self { + self + } +} + +impl NewService> + for StaticFiles +{ + type Response = ServiceResponse; + type Error = (); + type Service = StaticFilesService; + type InitError = (); + type Future = FutureResult; + + fn new_service(&self, _: &()) -> Self::Future { + ok(StaticFilesService { + directory: self.directory.clone(), + index: self.index.clone(), + show_index: self.show_index, + default: self.default.clone(), + renderer: self.renderer.clone(), + _chunk_size: self._chunk_size, + _follow_symlinks: self._follow_symlinks, + _cd_map: self._cd_map, + }) + } +} + +pub struct StaticFilesService { + directory: PathBuf, + index: Option, + show_index: bool, + default: Rc>>>>, + renderer: Rc, + _chunk_size: usize, + _follow_symlinks: bool, + _cd_map: PhantomData, +} + +impl Service> for StaticFilesService { + type Response = ServiceResponse; + type 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, _) = req.into_parts(); + + let real_path = match PathBufWrp::get_pathbuf(req.match_info()) { + Ok(item) => item, + Err(e) => return ok(ServiceResponse::from_err(e, req.clone())), + }; + + // full filepath + let path = match self.directory.join(&real_path.0).canonicalize() { + Ok(path) => path, + Err(e) => return ok(ServiceResponse::from_err(e, req.clone())), + }; + + if path.is_dir() { + if let Some(ref redir_index) = self.index { + let path = path.join(redir_index); + + match NamedFile::open_with_config(path, C::default()) { + Ok(named_file) => match named_file.respond_to(&req) { + Ok(item) => ok(ServiceResponse::new(req.clone(), item)), + Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + }, + Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + } + } else if self.show_index { + let dir = Directory::new(self.directory.clone(), path); + let x = (self.renderer)(&dir, &req); + match x { + Ok(resp) => ok(resp), + Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + } + } else { + ok(ServiceResponse::from_err( + StaticFilesError::IsDirectory, + req.clone(), + )) + } + } else { + match NamedFile::open_with_config(path, C::default()) { + Ok(named_file) => match named_file.respond_to(&req) { + Ok(item) => ok(ServiceResponse::new(req.clone(), item)), + Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + }, + Err(e) => ok(ServiceResponse::from_err(e, req.clone())), + } + } + } +} + +struct PathBufWrp(PathBuf); + +impl PathBufWrp { + fn get_pathbuf(path: &dev::Path) -> Result { + let path_str = path.path(); + let mut buf = PathBuf::new(); + for segment in path_str.split('/') { + if segment == ".." { + buf.pop(); + } else if segment.starts_with('.') { + return Err(UriSegmentError::BadStart('.')); + } else if segment.starts_with('*') { + return Err(UriSegmentError::BadStart('*')); + } else if segment.ends_with(':') { + return Err(UriSegmentError::BadEnd(':')); + } else if segment.ends_with('>') { + return Err(UriSegmentError::BadEnd('>')); + } else if segment.ends_with('<') { + return Err(UriSegmentError::BadEnd('<')); + } else if segment.is_empty() { + continue; + } else if cfg!(windows) && segment.contains('\\') { + return Err(UriSegmentError::BadChar('\\')); + } else { + buf.push(segment) + } + } + + Ok(PathBufWrp(buf)) + } +} + +impl

    FromRequest

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

    ) -> Self::Future { + PathBufWrp::get_pathbuf(req.match_info()) + } +} + +/// HTTP Range header representation. +#[derive(Debug, Clone, Copy)] +struct HttpRange { + pub start: u64, + pub length: u64, +} + +static PREFIX: &'static str = "bytes="; +const PREFIX_LEN: usize = 6; + +impl HttpRange { + /// Parses Range HTTP header string as per RFC 2616. + /// + /// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`). + /// `size` is full size of response (file). + fn parse(header: &str, size: u64) -> Result, ()> { + if header.is_empty() { + return Ok(Vec::new()); + } + if !header.starts_with(PREFIX) { + return Err(()); + } + + let size_sig = size as i64; + let mut no_overlap = false; + + let all_ranges: Vec> = header[PREFIX_LEN..] + .split(',') + .map(|x| x.trim()) + .filter(|x| !x.is_empty()) + .map(|ra| { + let mut start_end_iter = ra.split('-'); + + let start_str = start_end_iter.next().ok_or(())?.trim(); + let end_str = start_end_iter.next().ok_or(())?.trim(); + + if start_str.is_empty() { + // If no start is specified, end specifies the + // range start relative to the end of the file. + let mut length: i64 = end_str.parse().map_err(|_| ())?; + + if length > size_sig { + length = size_sig; + } + + Ok(Some(HttpRange { + start: (size_sig - length) as u64, + length: length as u64, + })) + } else { + let start: i64 = start_str.parse().map_err(|_| ())?; + + if start < 0 { + return Err(()); + } + if start >= size_sig { + no_overlap = true; + return Ok(None); + } + + let length = if end_str.is_empty() { + // If no end is specified, range extends to end of the file. + size_sig - start + } else { + let mut end: i64 = end_str.parse().map_err(|_| ())?; + + if start > end { + return Err(()); + } + + if end >= size_sig { + end = size_sig - 1; + } + + end - start + 1 + }; + + Ok(Some(HttpRange { + start: start as u64, + length: length as u64, + })) + } + }) + .collect::>()?; + + let ranges: Vec = all_ranges.into_iter().filter_map(|x| x).collect(); + + if no_overlap && ranges.is_empty() { + return Err(()); + } + + Ok(ranges) + } +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::ops::Add; + use std::time::{Duration, SystemTime}; + + use bytes::BytesMut; + + use super::*; + use actix_web::http::{header, header::DispositionType, Method, StatusCode}; + use actix_web::test::{self, TestRequest}; + use actix_web::App; + + #[test] + fn test_file_extension_to_mime() { + let m = file_extension_to_mime("jpg"); + assert_eq!(m, mime::IMAGE_JPEG); + + let m = file_extension_to_mime("invalid extension!!"); + assert_eq!(m, mime::APPLICATION_OCTET_STREAM); + + let m = file_extension_to_mime(""); + assert_eq!(m, mime::APPLICATION_OCTET_STREAM); + } + + #[test] + fn test_if_modified_since_without_if_none_match() { + let file = NamedFile::open("Cargo.toml").unwrap(); + let since = + header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); + + let req = TestRequest::default() + .header(header::IF_MODIFIED_SINCE, since) + .to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!(resp.status(), StatusCode::NOT_MODIFIED); + } + + #[test] + fn test_if_modified_since_with_if_none_match() { + let file = NamedFile::open("Cargo.toml").unwrap(); + let since = + header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); + + let req = TestRequest::default() + .header(header::IF_NONE_MATCH, "miss_etag") + .header(header::IF_MODIFIED_SINCE, since) + .to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_ne!(resp.status(), StatusCode::NOT_MODIFIED); + } + + #[test] + fn test_named_file_text() { + assert!(NamedFile::open("test--").is_err()); + let mut file = NamedFile::open("Cargo.toml").unwrap(); + { + file.file(); + let _f: &File = &file; + } + { + let _f: &mut File = &mut file; + } + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "text/x-toml" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "inline; filename=\"Cargo.toml\"" + ); + } + + #[test] + fn test_named_file_set_content_type() { + let mut file = NamedFile::open("Cargo.toml") + .unwrap() + .set_content_type(mime::TEXT_XML); + { + file.file(); + let _f: &File = &file; + } + { + let _f: &mut File = &mut file; + } + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "text/xml" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "inline; filename=\"Cargo.toml\"" + ); + } + + #[test] + fn test_named_file_image() { + let mut file = NamedFile::open("tests/test.png").unwrap(); + { + file.file(); + let _f: &File = &file; + } + { + let _f: &mut File = &mut file; + } + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "image/png" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "inline; filename=\"test.png\"" + ); + } + + #[test] + fn test_named_file_image_attachment() { + use header::{ContentDisposition, DispositionParam, DispositionType}; + let cd = ContentDisposition { + disposition: DispositionType::Attachment, + parameters: vec![DispositionParam::Filename(String::from("test.png"))], + }; + let mut file = NamedFile::open("tests/test.png") + .unwrap() + .set_content_disposition(cd); + { + file.file(); + let _f: &File = &file; + } + { + let _f: &mut File = &mut file; + } + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "image/png" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "attachment; filename=\"test.png\"" + ); + } + + #[derive(Default)] + pub struct AllAttachmentConfig; + impl StaticFileConfig for AllAttachmentConfig { + fn content_disposition_map(_typ: mime::Name) -> DispositionType { + DispositionType::Attachment + } + } + + #[derive(Default)] + pub struct AllInlineConfig; + impl StaticFileConfig for AllInlineConfig { + fn content_disposition_map(_typ: mime::Name) -> DispositionType { + DispositionType::Inline + } + } + + #[test] + fn test_named_file_image_attachment_and_custom_config() { + let file = + NamedFile::open_with_config("tests/test.png", AllAttachmentConfig).unwrap(); + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "image/png" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "attachment; filename=\"test.png\"" + ); + + let file = + NamedFile::open_with_config("tests/test.png", AllInlineConfig).unwrap(); + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "image/png" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "inline; filename=\"test.png\"" + ); + } + + #[test] + fn test_named_file_binary() { + let mut file = NamedFile::open("tests/test.binary").unwrap(); + { + file.file(); + let _f: &File = &file; + } + { + let _f: &mut File = &mut file; + } + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "application/octet-stream" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "attachment; filename=\"test.binary\"" + ); + } + + #[test] + fn test_named_file_status_code_text() { + let mut file = NamedFile::open("Cargo.toml") + .unwrap() + .set_status_code(StatusCode::NOT_FOUND); + { + file.file(); + let _f: &File = &file; + } + { + let _f: &mut File = &mut file; + } + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "text/x-toml" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "inline; filename=\"Cargo.toml\"" + ); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[test] + fn test_named_file_ranges_status_code() { + let mut srv = test::init_service( + App::new().service( + StaticFiles::new("/test", ".") + .unwrap() + .index_file("Cargo.toml"), + ), + ); + + // Valid range header + let request = TestRequest::get() + .uri("/t%65st/Cargo.toml") + .header(header::RANGE, "bytes=10-20") + .to_request(); + let response = test::call_success(&mut srv, request); + assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); + + // Invalid range header + let request = TestRequest::get() + .uri("/t%65st/Cargo.toml") + .header(header::RANGE, "bytes=1-0") + .to_request(); + let response = test::call_success(&mut srv, request); + + assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); + } + + #[test] + fn test_named_file_content_range_headers() { + let mut srv = test::init_service( + App::new().service( + StaticFiles::new("/test", ".") + .unwrap() + .index_file("tests/test.binary"), + ), + ); + + // Valid range header + let request = TestRequest::get() + .uri("/t%65st/tests/test.binary") + .header(header::RANGE, "bytes=10-20") + .to_request(); + + let response = test::call_success(&mut srv, request); + let contentrange = response + .headers() + .get(header::CONTENT_RANGE) + .unwrap() + .to_str() + .unwrap(); + + assert_eq!(contentrange, "bytes 10-20/100"); + + // Invalid range header + let request = TestRequest::get() + .uri("/t%65st/tests/test.binary") + .header(header::RANGE, "bytes=10-5") + .to_request(); + let response = test::call_success(&mut srv, request); + + let contentrange = response + .headers() + .get(header::CONTENT_RANGE) + .unwrap() + .to_str() + .unwrap(); + + assert_eq!(contentrange, "bytes */100"); + } + + #[test] + fn test_named_file_content_length_headers() { + let mut srv = test::init_service( + App::new().service( + StaticFiles::new("test", ".") + .unwrap() + .index_file("tests/test.binary"), + ), + ); + + // Valid range header + let request = TestRequest::get() + .uri("/t%65st/tests/test.binary") + .header(header::RANGE, "bytes=10-20") + .to_request(); + let response = test::call_success(&mut srv, request); + + let contentlength = response + .headers() + .get(header::CONTENT_LENGTH) + .unwrap() + .to_str() + .unwrap(); + + assert_eq!(contentlength, "11"); + + // Invalid range header + let request = TestRequest::get() + .uri("/t%65st/tests/test.binary") + .header(header::RANGE, "bytes=10-8") + .to_request(); + let response = test::call_success(&mut srv, request); + assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); + + // Without range header + let request = TestRequest::get() + .uri("/t%65st/tests/test.binary") + // .no_default_headers() + .to_request(); + let response = test::call_success(&mut srv, request); + + let contentlength = response + .headers() + .get(header::CONTENT_LENGTH) + .unwrap() + .to_str() + .unwrap(); + + assert_eq!(contentlength, "100"); + + // chunked + let request = TestRequest::get() + .uri("/t%65st/tests/test.binary") + .to_request(); + let mut response = test::call_success(&mut srv, request); + + // with enabled compression + // { + // let te = response + // .headers() + // .get(header::TRANSFER_ENCODING) + // .unwrap() + // .to_str() + // .unwrap(); + // assert_eq!(te, "chunked"); + // } + + let bytes = + test::block_on(response.take_body().fold(BytesMut::new(), |mut b, c| { + b.extend(c); + Ok::<_, Error>(b) + })) + .unwrap(); + let data = Bytes::from(fs::read("tests/test.binary").unwrap()); + assert_eq!(bytes.freeze(), data); + } + + #[test] + fn test_static_files_with_spaces() { + let mut srv = test::init_service( + App::new() + .service(StaticFiles::new("/", ".").unwrap().index_file("Cargo.toml")), + ); + let request = TestRequest::get() + .uri("/tests/test%20space.binary") + .to_request(); + let mut response = test::call_success(&mut srv, request); + assert_eq!(response.status(), StatusCode::OK); + + let bytes = + test::block_on(response.take_body().fold(BytesMut::new(), |mut b, c| { + b.extend(c); + Ok::<_, Error>(b) + })) + .unwrap(); + + let data = Bytes::from(fs::read("tests/test space.binary").unwrap()); + assert_eq!(bytes.freeze(), data); + } + + #[derive(Default)] + pub struct OnlyMethodHeadConfig; + impl StaticFileConfig for OnlyMethodHeadConfig { + fn is_method_allowed(method: &Method) -> bool { + match *method { + Method::HEAD => true, + _ => false, + } + } + } + + #[test] + fn test_named_file_not_allowed() { + let file = + NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); + let req = TestRequest::default() + .method(Method::POST) + .to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + + let file = + NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); + let req = TestRequest::default().method(Method::PUT).to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + + let file = + NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); + let req = TestRequest::default().method(Method::GET).to_http_request(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + } + + // #[test] + // fn test_named_file_content_encoding() { + // let req = TestRequest::default().method(Method::GET).finish(); + // let file = NamedFile::open("Cargo.toml").unwrap(); + + // assert!(file.encoding.is_none()); + // let resp = file + // .set_content_encoding(ContentEncoding::Identity) + // .respond_to(&req) + // .unwrap(); + + // assert!(resp.content_encoding().is_some()); + // assert_eq!(resp.content_encoding().unwrap().as_str(), "identity"); + // } + + #[test] + fn test_named_file_any_method() { + let req = TestRequest::default() + .method(Method::POST) + .to_http_request(); + let file = NamedFile::open("Cargo.toml").unwrap(); + let resp = file.respond_to(&req).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + } + + #[test] + fn test_static_files() { + let mut srv = test::init_service( + App::new().service(StaticFiles::new("/", ".").unwrap().show_files_listing()), + ); + let req = TestRequest::with_uri("/missing").to_request(); + + let resp = test::call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let mut srv = + test::init_service(App::new().service(StaticFiles::new("/", ".").unwrap())); + + let req = TestRequest::default().to_request(); + let resp = test::call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let mut srv = test::init_service( + App::new().service(StaticFiles::new("/", ".").unwrap().show_files_listing()), + ); + let req = TestRequest::with_uri("/tests").to_request(); + let mut resp = test::call_success(&mut srv, req); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "text/html; charset=utf-8" + ); + + let bytes = + test::block_on(resp.take_body().fold(BytesMut::new(), |mut b, c| { + b.extend(c); + Ok::<_, Error>(b) + })) + .unwrap(); + assert!(format!("{:?}", bytes).contains("/tests/test.png")); + } + + #[test] + fn test_static_files_bad_directory() { + let st: Result, Error> = StaticFiles::new("/", "missing"); + assert!(st.is_err()); + + let st: Result, Error> = StaticFiles::new("/", "Cargo.toml"); + assert!(st.is_err()); + } + + // #[test] + // fn test_default_handler_file_missing() { + // let st = StaticFiles::new(".") + // .unwrap() + // .default_handler(|_: &_| "default content"); + // let req = TestRequest::with_uri("/missing") + // .param("tail", "missing") + // .finish(); + + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // assert_eq!(resp.status(), StatusCode::OK); + // assert_eq!( + // resp.body(), + // &Body::Binary(Binary::Slice(b"default content")) + // ); + // } + + // #[test] + // fn test_serve_index() { + // let st = StaticFiles::new(".").unwrap().index_file("test.binary"); + // let req = TestRequest::default().uri("/tests").finish(); + + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // assert_eq!(resp.status(), StatusCode::OK); + // assert_eq!( + // resp.headers() + // .get(header::CONTENT_TYPE) + // .expect("content type"), + // "application/octet-stream" + // ); + // assert_eq!( + // resp.headers() + // .get(header::CONTENT_DISPOSITION) + // .expect("content disposition"), + // "attachment; filename=\"test.binary\"" + // ); + + // let req = TestRequest::default().uri("/tests/").finish(); + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // assert_eq!(resp.status(), StatusCode::OK); + // assert_eq!( + // resp.headers().get(header::CONTENT_TYPE).unwrap(), + // "application/octet-stream" + // ); + // assert_eq!( + // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + // "attachment; filename=\"test.binary\"" + // ); + + // // nonexistent index file + // let req = TestRequest::default().uri("/tests/unknown").finish(); + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + // let req = TestRequest::default().uri("/tests/unknown/").finish(); + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // assert_eq!(resp.status(), StatusCode::NOT_FOUND); + // } + + // #[test] + // fn test_serve_index_nested() { + // let st = StaticFiles::new(".").unwrap().index_file("mod.rs"); + // let req = TestRequest::default().uri("/src/client").finish(); + // let resp = st.handle(&req).respond_to(&req).unwrap(); + // let resp = resp.as_msg(); + // assert_eq!(resp.status(), StatusCode::OK); + // assert_eq!( + // resp.headers().get(header::CONTENT_TYPE).unwrap(), + // "text/x-rust" + // ); + // assert_eq!( + // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + // "inline; filename=\"mod.rs\"" + // ); + // } + + // #[test] + // fn integration_serve_index() { + // let mut srv = test::TestServer::with_factory(|| { + // App::new().handler( + // "test", + // StaticFiles::new(".").unwrap().index_file("Cargo.toml"), + // ) + // }); + + // let request = srv.get().uri(srv.url("/test")).finish().unwrap(); + // let response = srv.execute(request.send()).unwrap(); + // assert_eq!(response.status(), StatusCode::OK); + // let bytes = srv.execute(response.body()).unwrap(); + // let data = Bytes::from(fs::read("Cargo.toml").unwrap()); + // assert_eq!(bytes, data); + + // let request = srv.get().uri(srv.url("/test/")).finish().unwrap(); + // let response = srv.execute(request.send()).unwrap(); + // assert_eq!(response.status(), StatusCode::OK); + // let bytes = srv.execute(response.body()).unwrap(); + // let data = Bytes::from(fs::read("Cargo.toml").unwrap()); + // assert_eq!(bytes, data); + + // // nonexistent index file + // let request = srv.get().uri(srv.url("/test/unknown")).finish().unwrap(); + // let response = srv.execute(request.send()).unwrap(); + // assert_eq!(response.status(), StatusCode::NOT_FOUND); + + // let request = srv.get().uri(srv.url("/test/unknown/")).finish().unwrap(); + // let response = srv.execute(request.send()).unwrap(); + // assert_eq!(response.status(), StatusCode::NOT_FOUND); + // } + + // #[test] + // fn integration_percent_encoded() { + // let mut srv = test::TestServer::with_factory(|| { + // App::new().handler( + // "test", + // StaticFiles::new(".").unwrap().index_file("Cargo.toml"), + // ) + // }); + + // let request = srv + // .get() + // .uri(srv.url("/test/%43argo.toml")) + // .finish() + // .unwrap(); + // let response = srv.execute(request.send()).unwrap(); + // assert_eq!(response.status(), StatusCode::OK); + // } + + struct T(&'static str, u64, Vec); + + #[test] + fn test_parse() { + let tests = vec![ + T("", 0, vec![]), + T("", 1000, vec![]), + T("foo", 0, vec![]), + T("bytes=", 0, vec![]), + T("bytes=7", 10, vec![]), + T("bytes= 7 ", 10, vec![]), + T("bytes=1-", 0, vec![]), + T("bytes=5-4", 10, vec![]), + T("bytes=0-2,5-4", 10, vec![]), + T("bytes=2-5,4-3", 10, vec![]), + T("bytes=--5,4--3", 10, vec![]), + T("bytes=A-", 10, vec![]), + T("bytes=A- ", 10, vec![]), + T("bytes=A-Z", 10, vec![]), + T("bytes= -Z", 10, vec![]), + T("bytes=5-Z", 10, vec![]), + T("bytes=Ran-dom, garbage", 10, vec![]), + T("bytes=0x01-0x02", 10, vec![]), + T("bytes= ", 10, vec![]), + T("bytes= , , , ", 10, vec![]), + T( + "bytes=0-9", + 10, + vec![HttpRange { + start: 0, + length: 10, + }], + ), + T( + "bytes=0-", + 10, + vec![HttpRange { + start: 0, + length: 10, + }], + ), + T( + "bytes=5-", + 10, + vec![HttpRange { + start: 5, + length: 5, + }], + ), + T( + "bytes=0-20", + 10, + vec![HttpRange { + start: 0, + length: 10, + }], + ), + T( + "bytes=15-,0-5", + 10, + vec![HttpRange { + start: 0, + length: 6, + }], + ), + T( + "bytes=1-2,5-", + 10, + vec![ + HttpRange { + start: 1, + length: 2, + }, + HttpRange { + start: 5, + length: 5, + }, + ], + ), + T( + "bytes=-2 , 7-", + 11, + vec![ + HttpRange { + start: 9, + length: 2, + }, + HttpRange { + start: 7, + length: 4, + }, + ], + ), + T( + "bytes=0-0 ,2-2, 7-", + 11, + vec![ + HttpRange { + start: 0, + length: 1, + }, + HttpRange { + start: 2, + length: 1, + }, + HttpRange { + start: 7, + length: 4, + }, + ], + ), + T( + "bytes=-5", + 10, + vec![HttpRange { + start: 5, + length: 5, + }], + ), + T( + "bytes=-15", + 10, + vec![HttpRange { + start: 0, + length: 10, + }], + ), + T( + "bytes=0-499", + 10000, + vec![HttpRange { + start: 0, + length: 500, + }], + ), + T( + "bytes=500-999", + 10000, + vec![HttpRange { + start: 500, + length: 500, + }], + ), + T( + "bytes=-500", + 10000, + vec![HttpRange { + start: 9500, + length: 500, + }], + ), + T( + "bytes=9500-", + 10000, + vec![HttpRange { + start: 9500, + length: 500, + }], + ), + T( + "bytes=0-0,-1", + 10000, + vec![ + HttpRange { + start: 0, + length: 1, + }, + HttpRange { + start: 9999, + length: 1, + }, + ], + ), + T( + "bytes=500-600,601-999", + 10000, + vec![ + HttpRange { + start: 500, + length: 101, + }, + HttpRange { + start: 601, + length: 399, + }, + ], + ), + T( + "bytes=500-700,601-999", + 10000, + vec![ + HttpRange { + start: 500, + length: 201, + }, + HttpRange { + start: 601, + length: 399, + }, + ], + ), + // Match Apache laxity: + T( + "bytes= 1 -2 , 4- 5, 7 - 8 , ,,", + 11, + vec![ + HttpRange { + start: 1, + length: 2, + }, + HttpRange { + start: 4, + length: 2, + }, + HttpRange { + start: 7, + length: 2, + }, + ], + ), + ]; + + for t in tests { + let header = t.0; + let size = t.1; + let expected = t.2; + + let res = HttpRange::parse(header, size); + + if res.is_err() { + if expected.is_empty() { + continue; + } else { + assert!( + false, + "parse({}, {}) returned error {:?}", + header, + size, + res.unwrap_err() + ); + } + } + + let got = res.unwrap(); + + if got.len() != expected.len() { + assert!( + false, + "len(parseRange({}, {})) = {}, want {}", + header, + size, + got.len(), + expected.len() + ); + continue; + } + + for i in 0..expected.len() { + if got[i].start != expected[i].start { + assert!( + false, + "parseRange({}, {})[{}].start = {}, want {}", + header, size, i, got[i].start, expected[i].start + ) + } + if got[i].length != expected[i].length { + assert!( + false, + "parseRange({}, {})[{}].length = {}, want {}", + header, size, i, got[i].length, expected[i].length + ) + } + } + } + } +} diff --git a/actix-staticfiles/src/named.rs b/actix-staticfiles/src/named.rs new file mode 100644 index 00000000..5fba0483 --- /dev/null +++ b/actix-staticfiles/src/named.rs @@ -0,0 +1,438 @@ +use std::fs::{File, Metadata}; +use std::io; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; + +#[cfg(unix)] +use std::os::unix::fs::MetadataExt; + +use mime; +use mime_guess::guess_mime_type; + +use actix_http::error::Error; +use actix_http::http::header::{self, ContentDisposition, DispositionParam}; +use actix_web::http::{ContentEncoding, Method, StatusCode}; +use actix_web::{HttpMessage, HttpRequest, HttpResponse, Responder}; + +use crate::config::{DefaultConfig, StaticFileConfig}; +use crate::{ChunkedReadFile, HttpRange}; + +/// A file with an associated name. +#[derive(Debug)] +pub struct NamedFile { + path: PathBuf, + file: File, + pub(crate) content_type: mime::Mime, + pub(crate) content_disposition: header::ContentDisposition, + pub(crate) md: Metadata, + modified: Option, + encoding: Option, + pub(crate) status_code: StatusCode, + _cd_map: PhantomData, +} + +impl NamedFile { + /// Creates an instance from a previously opened file. + /// + /// The given `path` need not exist and is only used to determine the `ContentType` and + /// `ContentDisposition` headers. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate actix_web; + /// + /// use actix_web::fs::NamedFile; + /// use std::io::{self, Write}; + /// use std::env; + /// use std::fs::File; + /// + /// fn main() -> io::Result<()> { + /// let mut file = File::create("foo.txt")?; + /// file.write_all(b"Hello, world!")?; + /// let named_file = NamedFile::from_file(file, "bar.txt")?; + /// Ok(()) + /// } + /// ``` + pub fn from_file>(file: File, path: P) -> io::Result { + Self::from_file_with_config(file, path, DefaultConfig) + } + + /// Attempts to open a file in read-only mode. + /// + /// # Examples + /// + /// ```rust,ignore + /// use actix_web::fs::NamedFile; + /// + /// let file = NamedFile::open("foo.txt"); + /// ``` + pub fn open>(path: P) -> io::Result { + Self::open_with_config(path, DefaultConfig) + } +} + +impl NamedFile { + /// Creates an instance from a previously opened file using the provided configuration. + /// + /// The given `path` need not exist and is only used to determine the `ContentType` and + /// `ContentDisposition` headers. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate actix_web; + /// + /// use actix_web::fs::{DefaultConfig, NamedFile}; + /// use std::io::{self, Write}; + /// use std::env; + /// use std::fs::File; + /// + /// fn main() -> io::Result<()> { + /// let mut file = File::create("foo.txt")?; + /// file.write_all(b"Hello, world!")?; + /// let named_file = NamedFile::from_file_with_config(file, "bar.txt", DefaultConfig)?; + /// Ok(()) + /// } + /// ``` + pub fn from_file_with_config>( + file: File, + path: P, + _: C, + ) -> io::Result> { + let path = path.as_ref().to_path_buf(); + + // Get the name of the file and use it to construct default Content-Type + // and Content-Disposition values + let (content_type, content_disposition) = { + let filename = match path.file_name() { + Some(name) => name.to_string_lossy(), + None => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Provided path has no filename", + )); + } + }; + + let ct = guess_mime_type(&path); + let disposition_type = C::content_disposition_map(ct.type_()); + let cd = ContentDisposition { + disposition: disposition_type, + parameters: vec![DispositionParam::Filename(filename.into_owned())], + }; + (ct, cd) + }; + + let md = file.metadata()?; + let modified = md.modified().ok(); + let encoding = None; + Ok(NamedFile { + path, + file, + content_type, + content_disposition, + md, + modified, + encoding, + status_code: StatusCode::OK, + _cd_map: PhantomData, + }) + } + + /// Attempts to open a file in read-only mode using provided configuration. + /// + /// # Examples + /// + /// ```rust,ignore + /// use actix_web::fs::{DefaultConfig, NamedFile}; + /// + /// let file = NamedFile::open_with_config("foo.txt", DefaultConfig); + /// ``` + pub fn open_with_config>( + path: P, + config: C, + ) -> io::Result> { + Self::from_file_with_config(File::open(&path)?, path, config) + } + + /// Returns reference to the underlying `File` object. + #[inline] + pub fn file(&self) -> &File { + &self.file + } + + /// Retrieve the path of this file. + /// + /// # Examples + /// + /// ```rust,ignore + /// # use std::io; + /// use actix_web::fs::NamedFile; + /// + /// # fn path() -> io::Result<()> { + /// let file = NamedFile::open("test.txt")?; + /// assert_eq!(file.path().as_os_str(), "foo.txt"); + /// # Ok(()) + /// # } + /// ``` + #[inline] + pub fn path(&self) -> &Path { + self.path.as_path() + } + + /// Set response **Status Code** + pub fn set_status_code(mut self, status: StatusCode) -> Self { + self.status_code = status; + self + } + + /// Set the MIME Content-Type for serving this file. By default + /// the Content-Type is inferred from the filename extension. + #[inline] + pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self { + self.content_type = mime_type; + self + } + + /// Set the Content-Disposition for serving this file. This allows + /// changing the inline/attachment disposition as well as the filename + /// sent to the peer. By default the disposition is `inline` for text, + /// image, and video content types, and `attachment` otherwise, and + /// the filename is taken from the path provided in the `open` method + /// after converting it to UTF-8 using + /// [to_string_lossy](https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_string_lossy). + #[inline] + pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self { + self.content_disposition = cd; + self + } + + /// Set content encoding for serving this file + #[inline] + pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self { + self.encoding = Some(enc); + self + } + + pub(crate) fn etag(&self) -> Option { + // This etag format is similar to Apache's. + self.modified.as_ref().map(|mtime| { + let ino = { + #[cfg(unix)] + { + self.md.ino() + } + #[cfg(not(unix))] + { + 0 + } + }; + + let dur = mtime + .duration_since(UNIX_EPOCH) + .expect("modification time must be after epoch"); + header::EntityTag::strong(format!( + "{:x}:{:x}:{:x}:{:x}", + ino, + self.md.len(), + dur.as_secs(), + dur.subsec_nanos() + )) + }) + } + + pub(crate) fn last_modified(&self) -> Option { + self.modified.map(|mtime| mtime.into()) + } +} + +impl Deref for NamedFile { + type Target = File; + + fn deref(&self) -> &File { + &self.file + } +} + +impl DerefMut for NamedFile { + fn deref_mut(&mut self) -> &mut File { + &mut self.file + } +} + +/// Returns true if `req` has no `If-Match` header or one which matches `etag`. +fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { + match req.get_header::() { + None | Some(header::IfMatch::Any) => true, + Some(header::IfMatch::Items(ref items)) => { + if let Some(some_etag) = etag { + for item in items { + if item.strong_eq(some_etag) { + return true; + } + } + } + false + } + } +} + +/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`. +fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { + match req.get_header::() { + Some(header::IfNoneMatch::Any) => false, + Some(header::IfNoneMatch::Items(ref items)) => { + if let Some(some_etag) = etag { + for item in items { + if item.weak_eq(some_etag) { + return false; + } + } + } + true + } + None => true, + } +} + +impl Responder for NamedFile { + type Error = Error; + type Future = Result; + + fn respond_to(self, req: &HttpRequest) -> Self::Future { + if self.status_code != StatusCode::OK { + let mut resp = HttpResponse::build(self.status_code); + resp.set(header::ContentType(self.content_type.clone())) + .header( + header::CONTENT_DISPOSITION, + self.content_disposition.to_string(), + ); + // TODO blocking by compressing + // if let Some(current_encoding) = self.encoding { + // resp.content_encoding(current_encoding); + // } + let reader = ChunkedReadFile { + size: self.md.len(), + offset: 0, + file: Some(self.file), + fut: None, + counter: 0, + }; + return Ok(resp.streaming(reader)); + } + + if !C::is_method_allowed(req.method()) { + return Ok(HttpResponse::MethodNotAllowed() + .header(header::CONTENT_TYPE, "text/plain") + .header(header::ALLOW, "GET, HEAD") + .body("This resource only supports GET and HEAD.")); + } + + let etag = if C::is_use_etag() { self.etag() } else { None }; + let last_modified = if C::is_use_last_modifier() { + self.last_modified() + } else { + None + }; + + // check preconditions + let precondition_failed = if !any_match(etag.as_ref(), req) { + true + } else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) = + (last_modified, req.get_header()) + { + m > since + } else { + false + }; + + // check last modified + let not_modified = if !none_match(etag.as_ref(), req) { + true + } else if 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()) + { + m <= since + } else { + false + }; + + let mut resp = HttpResponse::build(self.status_code); + resp.set(header::ContentType(self.content_type.clone())) + .header( + header::CONTENT_DISPOSITION, + self.content_disposition.to_string(), + ); + // TODO blocking by compressing + // if let Some(current_encoding) = self.encoding { + // resp.content_encoding(current_encoding); + // } + + resp.if_some(last_modified, |lm, resp| { + resp.set(header::LastModified(lm)); + }) + .if_some(etag, |etag, resp| { + resp.set(header::ETag(etag)); + }); + + resp.header(header::ACCEPT_RANGES, "bytes"); + + let mut length = self.md.len(); + let mut offset = 0; + + // check for range header + 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; + offset = rangesvec[0].start; + // TODO blocking by compressing + // resp.content_encoding(ContentEncoding::Identity); + resp.header( + header::CONTENT_RANGE, + format!( + "bytes {}-{}/{}", + offset, + offset + length - 1, + self.md.len() + ), + ); + } else { + resp.header(header::CONTENT_RANGE, format!("bytes */{}", length)); + return Ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish()); + }; + } else { + return Ok(resp.status(StatusCode::BAD_REQUEST).finish()); + }; + }; + + resp.header(header::CONTENT_LENGTH, format!("{}", length)); + + if precondition_failed { + return Ok(resp.status(StatusCode::PRECONDITION_FAILED).finish()); + } else if not_modified { + return Ok(resp.status(StatusCode::NOT_MODIFIED).finish()); + } + + if *req.method() == Method::HEAD { + Ok(resp.finish()) + } else { + let reader = ChunkedReadFile { + offset, + size: length, + file: Some(self.file), + fut: None, + counter: 0, + }; + if offset != 0 || length != self.md.len() { + return Ok(resp.status(StatusCode::PARTIAL_CONTENT).streaming(reader)); + }; + Ok(resp.streaming(reader)) + } + } +} diff --git a/actix-staticfiles/tests/test space.binary b/actix-staticfiles/tests/test space.binary new file mode 100644 index 00000000..ef8ff024 --- /dev/null +++ b/actix-staticfiles/tests/test space.binary @@ -0,0 +1 @@ +ÂTÇ‘É‚Vù2þvI ª–\ÇRË™–ˆæeÞvDØ:è—½¬RVÖYpíÿ;ÍÏGñùp!2÷CŒ.– û®õpA !ûߦÙx j+Uc÷±©X”c%Û;ï"yì­AI \ No newline at end of file diff --git a/actix-staticfiles/tests/test.binary b/actix-staticfiles/tests/test.binary new file mode 100644 index 00000000..ef8ff024 --- /dev/null +++ b/actix-staticfiles/tests/test.binary @@ -0,0 +1 @@ +ÂTÇ‘É‚Vù2þvI ª–\ÇRË™–ˆæeÞvDØ:è—½¬RVÖYpíÿ;ÍÏGñùp!2÷CŒ.– û®õpA !ûߦÙx j+Uc÷±©X”c%Û;ï"yì­AI \ No newline at end of file diff --git a/actix-staticfiles/tests/test.png b/actix-staticfiles/tests/test.png new file mode 100644 index 0000000000000000000000000000000000000000..6b7cdc0b8fb5a439d3bd19f210e89a37a2354e61 GIT binary patch literal 168 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1SFYWcSQjy&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=h((rL&%EBHDibIqn;8;O;+&tGo0?Yw &'static str { @@ -29,19 +28,17 @@ fn main() -> std::io::Result<()> { .middleware(middleware::DefaultHeaders::new().header("X-Version", "0.2")) .middleware(middleware::Compress::default()) .resource("/resource1/index.html", |r| r.route(web::get().to(index))) - .service( - "/resource2/index.html", - Resource::new() - .middleware( - middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"), - ) - .default_resource(|r| { - r.route(Route::new().to(|| HttpResponse::MethodNotAllowed())) - }) - .route(web::method(Method::GET).to_async(index_async)), - ) - .service("/test1.html", Resource::new().to(|| "Test\r\n")) - .service("/", Resource::new().to(no_params)) + .resource("/resource2/index.html", |r| { + r.middleware( + middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"), + ) + .default_resource(|r| { + r.route(web::route().to(|| HttpResponse::MethodNotAllowed())) + }) + .route(web::method(Method::GET).to_async(index_async)) + }) + .resource("/test1.html", |r| r.to(|| "Test\r\n")) + .resource("/", |r| r.to(no_params)) }) .bind("127.0.0.1:8080")? .workers(1) diff --git a/src/app.rs b/src/app.rs index 27ca5c95..d503d8dd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -24,8 +24,13 @@ type HttpService

    = BoxedService, ServiceResponse, ()>; type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; type BoxedResponse = Box>; -pub trait HttpServiceFactory { - type Factory: NewService; +pub trait HttpServiceFactory

    { + type Factory: NewService< + ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + >; fn rdef(&self) -> &ResourceDef; @@ -293,6 +298,29 @@ where } } + /// Register resource handler service. + pub fn service(self, service: F) -> AppRouter> + where + F: HttpServiceFactory

    + 'static, + { + let fref = Rc::new(RefCell::new(None)); + AppRouter { + chain: self.chain, + services: vec![( + service.rdef().clone(), + boxed::new_service(service.create().map_init_err(|_| ())), + None, + )], + default: None, + defaults: vec![], + endpoint: AppEntry::new(fref.clone()), + factory_ref: fref, + extensions: self.extensions, + state: self.state, + _t: PhantomData, + } + } + /// Set server host name. /// /// Host name is used by application router aa a hostname for url @@ -445,16 +473,15 @@ where } /// Register resource handler service. - pub fn service(mut self, rdef: R, factory: F) -> Self + pub fn service(mut self, factory: F) -> Self where - R: Into, - F: IntoNewService>, - U: NewService, Response = ServiceResponse, Error = ()> - + 'static, + F: HttpServiceFactory

    + 'static, { + let rdef = factory.rdef().clone(); + self.services.push(( - rdef.into(), - boxed::new_service(factory.into_new_service().map_init_err(|_| ())), + rdef, + boxed::new_service(factory.create().map_init_err(|_| ())), None, )); self diff --git a/src/lib.rs b/src/lib.rs index f21c5e43..44dcde35 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,9 +19,9 @@ pub mod test; // re-export for convenience pub use actix_http::Response as HttpResponse; -pub use actix_http::{error, http, Error, HttpMessage, ResponseError, Result}; +pub use actix_http::{body, error, http, Error, HttpMessage, ResponseError, Result}; -pub use crate::app::{App, AppRouter}; +pub use crate::app::App; pub use crate::extract::{FromRequest, Json}; pub use crate::request::HttpRequest; pub use crate::resource::Resource; @@ -32,6 +32,26 @@ pub use crate::server::HttpServer; pub use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; pub use crate::state::State; +pub mod dev { + //! The `actix-web` prelude for library developers + //! + //! The purpose of this module is to alleviate imports of many common actix + //! traits by adding a glob import to the top of actix heavy modules: + //! + //! ``` + //! # #![allow(unused_imports)] + //! use actix_web::dev::*; + //! ``` + + pub use crate::app::{AppRouter, HttpServiceFactory}; + pub use actix_http::body::{Body, MessageBody, ResponseBody}; + pub use actix_http::dev::ResponseBuilder as HttpResponseBuilder; + pub use actix_http::{ + Extensions, Payload, PayloadStream, RequestHead, ResponseHead, + }; + pub use actix_router::{Path, ResourceDef, Url}; +} + pub mod web { use actix_http::{http::Method, Error, Response}; use futures::IntoFuture; diff --git a/src/service.rs b/src/service.rs index 7d17527a..c9666e31 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,8 +1,9 @@ use std::borrow::Cow; use std::cell::{Ref, RefMut}; +use std::fmt; use std::rc::Rc; -use actix_http::body::{Body, ResponseBody}; +use actix_http::body::{Body, MessageBody, ResponseBody}; use actix_http::http::{HeaderMap, Method, Uri, Version}; use actix_http::{ Error, Extensions, HttpMessage, Payload, PayloadStream, Request, RequestHead, @@ -123,6 +124,11 @@ impl

    ServiceRequest

    { pub fn app_extensions(&self) -> &Extensions { self.req.app_extensions() } + + /// Deconstruct request into parts + pub fn into_parts(self) -> (HttpRequest, Payload

    ) { + (self.req, self.payload) + } } impl

    Resource for ServiceRequest

    { @@ -172,6 +178,29 @@ impl

    std::ops::DerefMut for ServiceRequest

    { } } +impl

    fmt::Debug for ServiceRequest

    { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!( + f, + "\nServiceRequest {:?} {}:{}", + self.head().version, + self.head().method, + self.path() + )?; + if !self.query_string().is_empty() { + writeln!(f, " query: ?{:?}", self.query_string())?; + } + if !self.match_info().is_empty() { + writeln!(f, " params: {:?}", self.match_info())?; + } + writeln!(f, " headers:")?; + for (key, val) in self.headers().iter() { + writeln!(f, " {:?}: {:?}", key, val)?; + } + Ok(()) + } +} + pub struct ServiceFromRequest

    { req: HttpRequest, payload: Payload

    , @@ -259,6 +288,16 @@ impl ServiceResponse { ServiceResponse { request, response } } + /// Create service response from the error + pub fn from_err>(err: E, request: HttpRequest) -> Self { + let e: Error = err.into(); + let res: Response = e.into(); + ServiceResponse { + request, + response: res.into_body(), + } + } + /// Get reference to original request #[inline] pub fn request(&self) -> &HttpRequest { @@ -303,6 +342,11 @@ impl ServiceResponse { } } } + + /// Extract response body + pub fn take_body(&mut self) -> ResponseBody { + self.response.take_body() + } } impl ServiceResponse { @@ -349,3 +393,21 @@ impl IntoFuture for ServiceResponse { ok(self) } } + +impl fmt::Debug for ServiceResponse { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let res = writeln!( + f, + "\nServiceResponse {:?} {}{}", + self.response.head().version, + self.response.head().status, + self.response.head().reason.unwrap_or(""), + ); + let _ = writeln!(f, " headers:"); + for (key, val) in self.response.head().headers.iter() { + let _ = writeln!(f, " {:?}: {:?}", key, val); + } + let _ = writeln!(f, " body: {:?}", self.response.body().length()); + res + } +} diff --git a/src/test.rs b/src/test.rs index 22bfe0c3..ccc4b38e 100644 --- a/src/test.rs +++ b/src/test.rs @@ -70,6 +70,34 @@ where block_on(app.into_new_service().new_service(&())).unwrap() } +/// Calls service and waits for response future completion. +/// +/// ```rust,ignore +/// use actix_web::{test, App, HttpResponse, http::StatusCode}; +/// use actix_service::Service; +/// +/// fn main() { +/// let mut app = test::init_service( +/// App::new() +/// .resource("/test", |r| r.to(|| HttpResponse::Ok())) +/// ); +/// +/// // Create request object +/// let req = test::TestRequest::with_uri("/test").to_request(); +/// +/// // Call application +/// let resp = test::call_succ_service(&mut app, req); +/// assert_eq!(resp.status(), StatusCode::OK); +/// } +/// ``` +pub fn call_success(app: &mut S, req: R) -> S::Response +where + S: Service, Error = E>, + E: std::fmt::Debug, +{ + block_on(app.call(req)).unwrap() +} + /// Test `Request` builder. /// /// For unit testing, actix provides a request builder type and a simple handler runner. TestRequest implements a builder-like pattern. diff --git a/staticfiles/src/lib.rs b/staticfiles/src/lib.rs deleted file mode 100644 index c2ac5f3a..00000000 --- a/staticfiles/src/lib.rs +++ /dev/null @@ -1,2033 +0,0 @@ -//! Static files support -use std::cell::RefCell; -use std::fmt::Write; -use std::fs::{DirEntry, File, Metadata}; -use std::io::{Read, Seek}; -use std::marker::PhantomData; -use std::ops::{Deref, DerefMut}; -use std::path::{Path, PathBuf}; -use std::rc::Rc; -use std::time::{SystemTime, UNIX_EPOCH}; -use std::{cmp, io}; - -#[cfg(unix)] -use std::os::unix::fs::MetadataExt; - -use bytes::Bytes; -use derive_more::Display; -use futures::{Async, Future, Poll, Stream}; -use mime; -use mime_guess::{get_mime_type, guess_mime_type}; -use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; -use v_htmlescape::escape as escape_html_entity; - -use actix_http::error::{Error, ErrorInternalServerError, ResponseError}; -use actix_http::http::header::{ - self, ContentDisposition, DispositionParam, DispositionType, -}; -use actix_http::http::{ContentEncoding, Method, StatusCode}; -use actix_http::{HttpMessage, Response}; -use actix_service::boxed::BoxedNewService; -use actix_service::{NewService, Service}; -use actix_web::{ - blocking, FromRequest, HttpRequest, Responder, ServiceRequest, ServiceResponse, -}; -use futures::future::{err, ok, FutureResult}; - -type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; - -///Describes `StaticFiles` configiration -/// -///To configure actix's static resources you need -///to define own configiration type and implement any method -///you wish to customize. -///As trait implements reasonable defaults for Actix. -/// -///## Example -/// -///```rust,ignore -/// extern crate mime; -/// extern crate actix_web; -/// use actix_web::http::header::DispositionType; -/// use actix_web::fs::{StaticFileConfig, NamedFile}; -/// -/// #[derive(Default)] -/// struct MyConfig; -/// -/// impl StaticFileConfig for MyConfig { -/// fn content_disposition_map(typ: mime::Name) -> DispositionType { -/// DispositionType::Attachment -/// } -/// } -/// -/// let file = NamedFile::open_with_config("foo.txt", MyConfig); -///``` -pub trait StaticFileConfig: Default { - ///Describes mapping for mime type to content disposition header - /// - ///By default `IMAGE`, `TEXT` and `VIDEO` are mapped to Inline. - ///Others are mapped to Attachment - fn content_disposition_map(typ: mime::Name) -> DispositionType { - match typ { - mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, - _ => DispositionType::Attachment, - } - } - - ///Describes whether Actix should attempt to calculate `ETag` - /// - ///Defaults to `true` - fn is_use_etag() -> bool { - true - } - - ///Describes whether Actix should use last modified date of file. - /// - ///Defaults to `true` - fn is_use_last_modifier() -> bool { - true - } - - ///Describes allowed methods to access static resources. - /// - ///By default all methods are allowed - fn is_method_allowed(_method: &Method) -> bool { - true - } -} - -///Default content disposition as described in -///[StaticFileConfig](trait.StaticFileConfig.html) -#[derive(Default)] -pub struct DefaultConfig; - -impl StaticFileConfig for DefaultConfig {} - -/// 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 -/// the type `application/octet-stream`. -#[inline] -pub fn file_extension_to_mime(ext: &str) -> mime::Mime { - get_mime_type(ext) -} - -/// A file with an associated name. -#[derive(Debug)] -pub struct NamedFile { - path: PathBuf, - file: File, - content_type: mime::Mime, - content_disposition: header::ContentDisposition, - md: Metadata, - modified: Option, - encoding: Option, - status_code: StatusCode, - _cd_map: PhantomData, -} - -impl NamedFile { - /// Creates an instance from a previously opened file. - /// - /// The given `path` need not exist and is only used to determine the `ContentType` and - /// `ContentDisposition` headers. - /// - /// # Examples - /// - /// ```rust,ignore - /// extern crate actix_web; - /// - /// use actix_web::fs::NamedFile; - /// use std::io::{self, Write}; - /// use std::env; - /// use std::fs::File; - /// - /// fn main() -> io::Result<()> { - /// let mut file = File::create("foo.txt")?; - /// file.write_all(b"Hello, world!")?; - /// let named_file = NamedFile::from_file(file, "bar.txt")?; - /// Ok(()) - /// } - /// ``` - pub fn from_file>(file: File, path: P) -> io::Result { - Self::from_file_with_config(file, path, DefaultConfig) - } - - /// Attempts to open a file in read-only mode. - /// - /// # Examples - /// - /// ```rust,ignore - /// use actix_web::fs::NamedFile; - /// - /// let file = NamedFile::open("foo.txt"); - /// ``` - pub fn open>(path: P) -> io::Result { - Self::open_with_config(path, DefaultConfig) - } -} - -impl NamedFile { - /// Creates an instance from a previously opened file using the provided configuration. - /// - /// The given `path` need not exist and is only used to determine the `ContentType` and - /// `ContentDisposition` headers. - /// - /// # Examples - /// - /// ```rust,ignore - /// extern crate actix_web; - /// - /// use actix_web::fs::{DefaultConfig, NamedFile}; - /// use std::io::{self, Write}; - /// use std::env; - /// use std::fs::File; - /// - /// fn main() -> io::Result<()> { - /// let mut file = File::create("foo.txt")?; - /// file.write_all(b"Hello, world!")?; - /// let named_file = NamedFile::from_file_with_config(file, "bar.txt", DefaultConfig)?; - /// Ok(()) - /// } - /// ``` - pub fn from_file_with_config>( - file: File, - path: P, - _: C, - ) -> io::Result> { - let path = path.as_ref().to_path_buf(); - - // Get the name of the file and use it to construct default Content-Type - // and Content-Disposition values - let (content_type, content_disposition) = { - let filename = match path.file_name() { - Some(name) => name.to_string_lossy(), - None => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Provided path has no filename", - )); - } - }; - - let ct = guess_mime_type(&path); - let disposition_type = C::content_disposition_map(ct.type_()); - let cd = ContentDisposition { - disposition: disposition_type, - parameters: vec![DispositionParam::Filename(filename.into_owned())], - }; - (ct, cd) - }; - - let md = file.metadata()?; - let modified = md.modified().ok(); - let encoding = None; - Ok(NamedFile { - path, - file, - content_type, - content_disposition, - md, - modified, - encoding, - status_code: StatusCode::OK, - _cd_map: PhantomData, - }) - } - - /// Attempts to open a file in read-only mode using provided configuration. - /// - /// # Examples - /// - /// ```rust,ignore - /// use actix_web::fs::{DefaultConfig, NamedFile}; - /// - /// let file = NamedFile::open_with_config("foo.txt", DefaultConfig); - /// ``` - pub fn open_with_config>( - path: P, - config: C, - ) -> io::Result> { - Self::from_file_with_config(File::open(&path)?, path, config) - } - - /// Returns reference to the underlying `File` object. - #[inline] - pub fn file(&self) -> &File { - &self.file - } - - /// Retrieve the path of this file. - /// - /// # Examples - /// - /// ```rust,ignore - /// # use std::io; - /// use actix_web::fs::NamedFile; - /// - /// # fn path() -> io::Result<()> { - /// let file = NamedFile::open("test.txt")?; - /// assert_eq!(file.path().as_os_str(), "foo.txt"); - /// # Ok(()) - /// # } - /// ``` - #[inline] - pub fn path(&self) -> &Path { - self.path.as_path() - } - - /// Set response **Status Code** - pub fn set_status_code(mut self, status: StatusCode) -> Self { - self.status_code = status; - self - } - - /// Set the MIME Content-Type for serving this file. By default - /// the Content-Type is inferred from the filename extension. - #[inline] - pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self { - self.content_type = mime_type; - self - } - - /// Set the Content-Disposition for serving this file. This allows - /// changing the inline/attachment disposition as well as the filename - /// sent to the peer. By default the disposition is `inline` for text, - /// image, and video content types, and `attachment` otherwise, and - /// the filename is taken from the path provided in the `open` method - /// after converting it to UTF-8 using - /// [to_string_lossy](https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_string_lossy). - #[inline] - pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self { - self.content_disposition = cd; - self - } - - /// Set content encoding for serving this file - #[inline] - pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self { - self.encoding = Some(enc); - self - } - - fn etag(&self) -> Option { - // This etag format is similar to Apache's. - self.modified.as_ref().map(|mtime| { - let ino = { - #[cfg(unix)] - { - self.md.ino() - } - #[cfg(not(unix))] - { - 0 - } - }; - - let dur = mtime - .duration_since(UNIX_EPOCH) - .expect("modification time must be after epoch"); - header::EntityTag::strong(format!( - "{:x}:{:x}:{:x}:{:x}", - ino, - self.md.len(), - dur.as_secs(), - dur.subsec_nanos() - )) - }) - } - - fn last_modified(&self) -> Option { - self.modified.map(|mtime| mtime.into()) - } -} - -impl Deref for NamedFile { - type Target = File; - - fn deref(&self) -> &File { - &self.file - } -} - -impl DerefMut for NamedFile { - fn deref_mut(&mut self) -> &mut File { - &mut self.file - } -} - -/// Returns true if `req` has no `If-Match` header or one which matches `etag`. -fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { - match req.get_header::() { - None | Some(header::IfMatch::Any) => true, - Some(header::IfMatch::Items(ref items)) => { - if let Some(some_etag) = etag { - for item in items { - if item.strong_eq(some_etag) { - return true; - } - } - } - false - } - } -} - -/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`. -fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { - match req.get_header::() { - Some(header::IfNoneMatch::Any) => false, - Some(header::IfNoneMatch::Items(ref items)) => { - if let Some(some_etag) = etag { - for item in items { - if item.weak_eq(some_etag) { - return false; - } - } - } - true - } - None => true, - } -} - -impl Responder for NamedFile { - type Error = Error; - type Future = FutureResult; - - fn respond_to(self, req: &HttpRequest) -> Self::Future { - if self.status_code != StatusCode::OK { - let mut resp = Response::build(self.status_code); - resp.set(header::ContentType(self.content_type.clone())) - .header( - header::CONTENT_DISPOSITION, - self.content_disposition.to_string(), - ); - // TODO blocking by compressing - // if let Some(current_encoding) = self.encoding { - // resp.content_encoding(current_encoding); - // } - let reader = ChunkedReadFile { - size: self.md.len(), - offset: 0, - file: Some(self.file), - fut: None, - counter: 0, - }; - return ok(resp.streaming(reader)); - } - - if !C::is_method_allowed(req.method()) { - return ok(Response::MethodNotAllowed() - .header(header::CONTENT_TYPE, "text/plain") - .header(header::ALLOW, "GET, HEAD") - .body("This resource only supports GET and HEAD.")); - } - - let etag = if C::is_use_etag() { self.etag() } else { None }; - let last_modified = if C::is_use_last_modifier() { - self.last_modified() - } else { - None - }; - - // check preconditions - let precondition_failed = if !any_match(etag.as_ref(), req) { - true - } else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) = - (last_modified, req.get_header()) - { - m > since - } else { - false - }; - - // check last modified - let not_modified = if !none_match(etag.as_ref(), req) { - true - } else if 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()) - { - m <= since - } else { - false - }; - - let mut resp = Response::build(self.status_code); - resp.set(header::ContentType(self.content_type.clone())) - .header( - header::CONTENT_DISPOSITION, - self.content_disposition.to_string(), - ); - // TODO blocking by compressing - // if let Some(current_encoding) = self.encoding { - // resp.content_encoding(current_encoding); - // } - - resp.if_some(last_modified, |lm, resp| { - resp.set(header::LastModified(lm)); - }) - .if_some(etag, |etag, resp| { - resp.set(header::ETag(etag)); - }); - - resp.header(header::ACCEPT_RANGES, "bytes"); - - let mut length = self.md.len(); - let mut offset = 0; - - // check for range header - 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; - offset = rangesvec[0].start; - // TODO blocking by compressing - // resp.content_encoding(ContentEncoding::Identity); - resp.header( - header::CONTENT_RANGE, - format!( - "bytes {}-{}/{}", - offset, - offset + length - 1, - self.md.len() - ), - ); - } else { - resp.header(header::CONTENT_RANGE, format!("bytes */{}", length)); - return ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish()); - }; - } else { - return ok(resp.status(StatusCode::BAD_REQUEST).finish()); - }; - }; - - resp.header(header::CONTENT_LENGTH, format!("{}", length)); - - if precondition_failed { - return ok(resp.status(StatusCode::PRECONDITION_FAILED).finish()); - } else if not_modified { - return ok(resp.status(StatusCode::NOT_MODIFIED).finish()); - } - - if *req.method() == Method::HEAD { - ok(resp.finish()) - } else { - let reader = ChunkedReadFile { - offset, - size: length, - file: Some(self.file), - fut: None, - counter: 0, - }; - if offset != 0 || length != self.md.len() { - return ok(resp.status(StatusCode::PARTIAL_CONTENT).streaming(reader)); - }; - ok(resp.streaming(reader)) - } - } -} - -#[doc(hidden)] -/// A helper created from a `std::fs::File` which reads the file -/// chunk-by-chunk on a `ThreadPool`. -pub struct ChunkedReadFile { - size: u64, - offset: u64, - file: Option, - fut: Option>, - counter: u64, -} - -fn handle_error(err: blocking::BlockingError) -> Error { - match err { - blocking::BlockingError::Error(err) => err.into(), - blocking::BlockingError::Canceled => { - ErrorInternalServerError("Unexpected error").into() - } - } -} - -impl Stream for ChunkedReadFile { - type Item = Bytes; - type Error = Error; - - fn poll(&mut self) -> Poll, Error> { - if self.fut.is_some() { - return match self.fut.as_mut().unwrap().poll().map_err(handle_error)? { - Async::Ready((file, bytes)) => { - self.fut.take(); - self.file = Some(file); - self.offset += bytes.len() as u64; - self.counter += bytes.len() as u64; - Ok(Async::Ready(Some(bytes))) - } - Async::NotReady => Ok(Async::NotReady), - }; - } - - let size = self.size; - let offset = self.offset; - let counter = self.counter; - - if size == counter { - Ok(Async::Ready(None)) - } else { - let mut file = self.file.take().expect("Use after completion"); - self.fut = Some(blocking::run(move || { - let max_bytes: usize; - max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; - let mut buf = Vec::with_capacity(max_bytes); - file.seek(io::SeekFrom::Start(offset))?; - let nbytes = - file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?; - if nbytes == 0 { - return Err(io::ErrorKind::UnexpectedEof.into()); - } - Ok((file, Bytes::from(buf))) - })); - self.poll() - } - } -} - -type DirectoryRenderer = - Fn(&Directory, &HttpRequest) -> Result; - -/// A directory; responds with the generated directory listing. -#[derive(Debug)] -pub struct Directory { - /// Base directory - pub base: PathBuf, - /// Path of subdirectory to generate listing for - pub path: PathBuf, -} - -impl Directory { - /// Create a new directory - pub fn new(base: PathBuf, path: PathBuf) -> Directory { - Directory { base, path } - } - - /// Is this entry visible from this directory? - pub fn is_visible(&self, entry: &io::Result) -> bool { - if let Ok(ref entry) = *entry { - if let Some(name) = entry.file_name().to_str() { - if name.starts_with('.') { - return false; - } - } - if let Ok(ref md) = entry.metadata() { - let ft = md.file_type(); - return ft.is_dir() || ft.is_file() || ft.is_symlink(); - } - } - false - } -} - -// show file url as relative to static path -macro_rules! encode_file_url { - ($path:ident) => { - utf8_percent_encode(&$path.to_string_lossy(), DEFAULT_ENCODE_SET) - }; -} - -// " -- " & -- & ' -- ' < -- < > -- > / -- / -macro_rules! encode_file_name { - ($entry:ident) => { - escape_html_entity(&$entry.file_name().to_string_lossy()) - }; -} - -fn directory_listing( - dir: &Directory, - req: &HttpRequest, -) -> Result { - let index_of = format!("Index of {}", req.path()); - let mut body = String::new(); - let base = Path::new(req.path()); - - for entry in dir.path.read_dir()? { - if dir.is_visible(&entry) { - let entry = entry.unwrap(); - let p = match entry.path().strip_prefix(&dir.path) { - Ok(p) => base.join(p), - Err(_) => continue, - }; - - // if file is a directory, add '/' to the end of the name - if let Ok(metadata) = entry.metadata() { - if metadata.is_dir() { - let _ = write!( - body, - "

  • {}/
  • ", - encode_file_url!(p), - encode_file_name!(entry), - ); - } else { - let _ = write!( - body, - "
  • {}
  • ", - encode_file_url!(p), - encode_file_name!(entry), - ); - } - } else { - continue; - } - } - } - - let html = format!( - "\ - {}\ -

    {}

    \ -
      \ - {}\ -
    \n", - index_of, index_of, body - ); - Ok(ServiceResponse::new( - req.clone(), - Response::Ok() - .content_type("text/html; charset=utf-8") - .body(html), - )) -} - -/// Static files handling -/// -/// `StaticFile` handler must be registered with `App::handler()` method, -/// because `StaticFile` handler requires access sub-path information. -/// -/// ```rust,ignore -/// # extern crate actix_web; -/// use actix_web::{fs, App}; -/// -/// fn main() { -/// let app = App::new() -/// .handler("/static", fs::StaticFiles::new(".").unwrap()) -/// .finish(); -/// } -/// ``` -pub struct StaticFiles { - directory: PathBuf, - index: Option, - show_index: bool, - default: Rc>>>>, - renderer: Rc, - _chunk_size: usize, - _follow_symlinks: bool, - _cd_map: PhantomData, -} - -impl StaticFiles { - /// Create new `StaticFiles` instance for specified base directory. - /// - /// `StaticFile` 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>(dir: T) -> Result, Error> { - Self::with_config(dir, DefaultConfig) - } -} - -impl StaticFiles { - /// Create new `StaticFiles` instance for specified base directory. - /// - /// Identical with `new` but allows to specify configiration to use. - pub fn with_config>( - dir: T, - _: C, - ) -> Result, Error> { - let dir = dir.into().canonicalize()?; - - if !dir.is_dir() { - return Err(StaticFilesError::IsNotDirectory.into()); - } - - Ok(StaticFiles { - directory: dir, - index: None, - show_index: false, - default: Rc::new(RefCell::new(None)), - renderer: Rc::new(directory_listing), - _chunk_size: 0, - _follow_symlinks: false, - _cd_map: PhantomData, - }) - } - - /// Show files listing for directories. - /// - /// By default show files listing is disabled. - pub fn show_files_listing(mut self) -> Self { - self.show_index = true; - self - } - - /// Set custom directory renderer - pub fn files_listing_renderer(mut self, f: F) -> Self - where - for<'r, 's> F: - Fn(&'r Directory, &'s HttpRequest) -> Result - + 'static, - { - self.renderer = Rc::new(f); - self - } - - /// Set index file - /// - /// Shows specific index file for directory "/" instead of - /// showing files listing. - pub fn index_file>(mut self, index: T) -> StaticFiles { - self.index = Some(index.into()); - self - } -} - -impl NewService for StaticFiles { - type Request = ServiceRequest; - type Response = ServiceResponse; - type Error = Error; - type Service = StaticFilesService; - type InitError = Error; - type Future = FutureResult; - - fn new_service(&self, _: &()) -> Self::Future { - ok(StaticFilesService { - directory: self.directory.clone(), - index: self.index.clone(), - show_index: self.show_index, - default: self.default.clone(), - renderer: self.renderer.clone(), - _chunk_size: self._chunk_size, - _follow_symlinks: self._follow_symlinks, - _cd_map: self._cd_map, - }) - } -} - -pub struct StaticFilesService { - directory: PathBuf, - index: Option, - show_index: bool, - default: Rc>>>>, - renderer: Rc, - _chunk_size: usize, - _follow_symlinks: bool, - _cd_map: PhantomData, -} - -impl Service for StaticFilesService { - type Request = ServiceRequest; - type Response = ServiceResponse; - type Error = Error; - type Future = FutureResult; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - Ok(Async::Ready(())) - } - - fn call(&mut self, req: Self::Request) -> Self::Future { - let mut req = req; - let real_path = match PathBufWrp::from_request(&mut req).poll() { - Ok(Async::Ready(item)) => item.0, - Ok(Async::NotReady) => unreachable!(), - Err(e) => return err(Error::from(e)), - }; - // full filepath - let path = match self.directory.join(&real_path).canonicalize() { - Ok(path) => path, - Err(e) => return err(Error::from(e)), - }; - - if path.is_dir() { - if let Some(ref redir_index) = self.index { - let path = path.join(redir_index); - - match NamedFile::open_with_config(path, C::default()) { - Ok(named_file) => match named_file.respond_to(&req).poll() { - Ok(Async::Ready(item)) => { - ok(ServiceResponse::new(req.clone(), item)) - } - Ok(Async::NotReady) => unreachable!(), - Err(e) => err(Error::from(e)), - }, - Err(e) => err(Error::from(e)), - } - } else if self.show_index { - let dir = Directory::new(self.directory.clone(), path); - let x = (self.renderer)(&dir, &req); - match x { - Ok(resp) => ok(resp), - Err(e) => err(Error::from(e)), - } - } else { - err(StaticFilesError::IsDirectory.into()) - } - } else { - match NamedFile::open_with_config(path, C::default()) { - Ok(named_file) => match named_file.respond_to(&req).poll() { - Ok(Async::Ready(item)) => { - ok(ServiceResponse::new(req.clone(), item)) - } - Ok(Async::NotReady) => unreachable!(), - Err(e) => err(Error::from(e)), - }, - Err(e) => err(Error::from(e)), - } - } - } -} - -struct PathBufWrp(PathBuf); - -impl

    FromRequest

    for PathBufWrp { - type Error = UriSegmentError; - type Future = FutureResult; - - fn from_request(req: &mut ServiceRequest

    ) -> Self::Future { - let path_str = req.match_info().path(); - let mut buf = PathBuf::new(); - for segment in path_str.split('/') { - if segment == ".." { - buf.pop(); - } else if segment.starts_with('.') { - return err(UriSegmentError::BadStart('.')); - } else if segment.starts_with('*') { - return err(UriSegmentError::BadStart('*')); - } else if segment.ends_with(':') { - return err(UriSegmentError::BadEnd(':')); - } else if segment.ends_with('>') { - return err(UriSegmentError::BadEnd('>')); - } else if segment.ends_with('<') { - return err(UriSegmentError::BadEnd('<')); - } else if segment.is_empty() { - continue; - } else if cfg!(windows) && segment.contains('\\') { - return err(UriSegmentError::BadChar('\\')); - } else { - buf.push(segment) - } - } - - ok(PathBufWrp(buf)) - } -} - -/// Errors which can occur when serving static files. -#[derive(Display, Debug, PartialEq)] -enum StaticFilesError { - /// Path is not a directory - #[display(fmt = "Path is not a directory. Unable to serve static files")] - IsNotDirectory, - - /// Cannot render directory - #[display(fmt = "Unable to render directory without index file")] - IsDirectory, -} - -/// Return `NotFound` for `StaticFilesError` -impl ResponseError for StaticFilesError { - fn error_response(&self) -> Response { - Response::new(StatusCode::NOT_FOUND) - } -} - -#[derive(Display, Debug, PartialEq)] -pub enum UriSegmentError { - /// The segment started with the wrapped invalid character. - #[display(fmt = "The segment started with the wrapped invalid character")] - BadStart(char), - /// The segment contained the wrapped invalid character. - #[display(fmt = "The segment contained the wrapped invalid character")] - BadChar(char), - /// The segment ended with the wrapped invalid character. - #[display(fmt = "The segment ended with the wrapped invalid character")] - BadEnd(char), -} - -/// Return `BadRequest` for `UriSegmentError` -impl ResponseError for UriSegmentError { - fn error_response(&self) -> Response { - Response::new(StatusCode::BAD_REQUEST) - } -} - -/// HTTP Range header representation. -#[derive(Debug, Clone, Copy)] -struct HttpRange { - pub start: u64, - pub length: u64, -} - -static PREFIX: &'static str = "bytes="; -const PREFIX_LEN: usize = 6; - -impl HttpRange { - /// Parses Range HTTP header string as per RFC 2616. - /// - /// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`). - /// `size` is full size of response (file). - fn parse(header: &str, size: u64) -> Result, ()> { - if header.is_empty() { - return Ok(Vec::new()); - } - if !header.starts_with(PREFIX) { - return Err(()); - } - - let size_sig = size as i64; - let mut no_overlap = false; - - let all_ranges: Vec> = header[PREFIX_LEN..] - .split(',') - .map(|x| x.trim()) - .filter(|x| !x.is_empty()) - .map(|ra| { - let mut start_end_iter = ra.split('-'); - - let start_str = start_end_iter.next().ok_or(())?.trim(); - let end_str = start_end_iter.next().ok_or(())?.trim(); - - if start_str.is_empty() { - // If no start is specified, end specifies the - // range start relative to the end of the file. - let mut length: i64 = end_str.parse().map_err(|_| ())?; - - if length > size_sig { - length = size_sig; - } - - Ok(Some(HttpRange { - start: (size_sig - length) as u64, - length: length as u64, - })) - } else { - let start: i64 = start_str.parse().map_err(|_| ())?; - - if start < 0 { - return Err(()); - } - if start >= size_sig { - no_overlap = true; - return Ok(None); - } - - let length = if end_str.is_empty() { - // If no end is specified, range extends to end of the file. - size_sig - start - } else { - let mut end: i64 = end_str.parse().map_err(|_| ())?; - - if start > end { - return Err(()); - } - - if end >= size_sig { - end = size_sig - 1; - } - - end - start + 1 - }; - - Ok(Some(HttpRange { - start: start as u64, - length: length as u64, - })) - } - }) - .collect::>()?; - - let ranges: Vec = all_ranges.into_iter().filter_map(|x| x).collect(); - - if no_overlap && ranges.is_empty() { - return Err(()); - } - - Ok(ranges) - } -} - -// #[cfg(test)] -// mod tests { -// use std::fs; -// use std::ops::Add; -// use std::time::Duration; - -// use super::*; -// use application::App; -// use body::{Binary, Body}; -// use http::{header, Method, StatusCode}; -// use test::{self, TestRequest}; - -// #[test] -// fn test_file_extension_to_mime() { -// let m = file_extension_to_mime("jpg"); -// assert_eq!(m, mime::IMAGE_JPEG); - -// let m = file_extension_to_mime("invalid extension!!"); -// assert_eq!(m, mime::APPLICATION_OCTET_STREAM); - -// let m = file_extension_to_mime(""); -// assert_eq!(m, mime::APPLICATION_OCTET_STREAM); -// } - -// #[test] -// fn test_if_modified_since_without_if_none_match() { -// let mut file = NamedFile::open("Cargo.toml") -// .unwrap() -// .set_cpu_pool(CpuPool::new(1)); -// let since = -// header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); - -// let req = TestRequest::default() -// .header(header::IF_MODIFIED_SINCE, since) -// .finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!(resp.status(), StatusCode::NOT_MODIFIED); -// } - -// #[test] -// fn test_if_modified_since_with_if_none_match() { -// let mut file = NamedFile::open("Cargo.toml") -// .unwrap() -// .set_cpu_pool(CpuPool::new(1)); -// let since = -// header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); - -// let req = TestRequest::default() -// .header(header::IF_NONE_MATCH, "miss_etag") -// .header(header::IF_MODIFIED_SINCE, since) -// .finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_ne!(resp.status(), StatusCode::NOT_MODIFIED); -// } - -// #[test] -// fn test_named_file_text() { -// assert!(NamedFile::open("test--").is_err()); -// let mut file = NamedFile::open("Cargo.toml") -// .unwrap() -// .set_cpu_pool(CpuPool::new(1)); -// { -// file.file(); -// let _f: &File = &file; -// } -// { -// let _f: &mut File = &mut file; -// } - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "text/x-toml" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "inline; filename=\"Cargo.toml\"" -// ); -// } - -// #[test] -// fn test_named_file_set_content_type() { -// let mut file = NamedFile::open("Cargo.toml") -// .unwrap() -// .set_content_type(mime::TEXT_XML) -// .set_cpu_pool(CpuPool::new(1)); -// { -// file.file(); -// let _f: &File = &file; -// } -// { -// let _f: &mut File = &mut file; -// } - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "text/xml" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "inline; filename=\"Cargo.toml\"" -// ); -// } - -// #[test] -// fn test_named_file_image() { -// let mut file = NamedFile::open("tests/test.png") -// .unwrap() -// .set_cpu_pool(CpuPool::new(1)); -// { -// file.file(); -// let _f: &File = &file; -// } -// { -// let _f: &mut File = &mut file; -// } - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "image/png" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "inline; filename=\"test.png\"" -// ); -// } - -// #[test] -// fn test_named_file_image_attachment() { -// use header::{ContentDisposition, DispositionParam, DispositionType}; -// let cd = ContentDisposition { -// disposition: DispositionType::Attachment, -// parameters: vec![DispositionParam::Filename(String::from("test.png"))], -// }; -// let mut file = NamedFile::open("tests/test.png") -// .unwrap() -// .set_content_disposition(cd) -// .set_cpu_pool(CpuPool::new(1)); -// { -// file.file(); -// let _f: &File = &file; -// } -// { -// let _f: &mut File = &mut file; -// } - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "image/png" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "attachment; filename=\"test.png\"" -// ); -// } - -// #[derive(Default)] -// pub struct AllAttachmentConfig; -// impl StaticFileConfig for AllAttachmentConfig { -// fn content_disposition_map(_typ: mime::Name) -> DispositionType { -// DispositionType::Attachment -// } -// } - -// #[derive(Default)] -// pub struct AllInlineConfig; -// impl StaticFileConfig for AllInlineConfig { -// fn content_disposition_map(_typ: mime::Name) -> DispositionType { -// DispositionType::Inline -// } -// } - -// #[test] -// fn test_named_file_image_attachment_and_custom_config() { -// let file = NamedFile::open_with_config("tests/test.png", AllAttachmentConfig) -// .unwrap() -// .set_cpu_pool(CpuPool::new(1)); - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "image/png" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "attachment; filename=\"test.png\"" -// ); - -// let file = NamedFile::open_with_config("tests/test.png", AllInlineConfig) -// .unwrap() -// .set_cpu_pool(CpuPool::new(1)); - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "image/png" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "inline; filename=\"test.png\"" -// ); -// } - -// #[test] -// fn test_named_file_binary() { -// let mut file = NamedFile::open("tests/test.binary") -// .unwrap() -// .set_cpu_pool(CpuPool::new(1)); -// { -// file.file(); -// let _f: &File = &file; -// } -// { -// let _f: &mut File = &mut file; -// } - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "application/octet-stream" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "attachment; filename=\"test.binary\"" -// ); -// } - -// #[test] -// fn test_named_file_status_code_text() { -// let mut file = NamedFile::open("Cargo.toml") -// .unwrap() -// .set_status_code(StatusCode::NOT_FOUND) -// .set_cpu_pool(CpuPool::new(1)); -// { -// file.file(); -// let _f: &File = &file; -// } -// { -// let _f: &mut File = &mut file; -// } - -// let req = TestRequest::default().finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "text/x-toml" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "inline; filename=\"Cargo.toml\"" -// ); -// assert_eq!(resp.status(), StatusCode::NOT_FOUND); -// } - -// #[test] -// fn test_named_file_ranges_status_code() { -// let mut srv = test::TestServer::with_factory(|| { -// App::new().handler( -// "test", -// StaticFiles::new(".").unwrap().index_file("Cargo.toml"), -// ) -// }); - -// // Valid range header -// let request = srv -// .get() -// .uri(srv.url("/t%65st/Cargo.toml")) -// .header(header::RANGE, "bytes=10-20") -// .finish() -// .unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); - -// // Invalid range header -// let request = srv -// .get() -// .uri(srv.url("/t%65st/Cargo.toml")) -// .header(header::RANGE, "bytes=1-0") -// .finish() -// .unwrap(); -// let response = srv.execute(request.send()).unwrap(); - -// assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); -// } - -// #[test] -// fn test_named_file_content_range_headers() { -// let mut srv = test::TestServer::with_factory(|| { -// App::new().handler( -// "test", -// StaticFiles::new(".") -// .unwrap() -// .index_file("tests/test.binary"), -// ) -// }); - -// // Valid range header -// let request = srv -// .get() -// .uri(srv.url("/t%65st/tests/test.binary")) -// .header(header::RANGE, "bytes=10-20") -// .finish() -// .unwrap(); - -// let response = srv.execute(request.send()).unwrap(); - -// let contentrange = response -// .headers() -// .get(header::CONTENT_RANGE) -// .unwrap() -// .to_str() -// .unwrap(); - -// assert_eq!(contentrange, "bytes 10-20/100"); - -// // Invalid range header -// let request = srv -// .get() -// .uri(srv.url("/t%65st/tests/test.binary")) -// .header(header::RANGE, "bytes=10-5") -// .finish() -// .unwrap(); - -// let response = srv.execute(request.send()).unwrap(); - -// let contentrange = response -// .headers() -// .get(header::CONTENT_RANGE) -// .unwrap() -// .to_str() -// .unwrap(); - -// assert_eq!(contentrange, "bytes */100"); -// } - -// #[test] -// fn test_named_file_content_length_headers() { -// let mut srv = test::TestServer::with_factory(|| { -// App::new().handler( -// "test", -// StaticFiles::new(".") -// .unwrap() -// .index_file("tests/test.binary"), -// ) -// }); - -// // Valid range header -// let request = srv -// .get() -// .uri(srv.url("/t%65st/tests/test.binary")) -// .header(header::RANGE, "bytes=10-20") -// .finish() -// .unwrap(); - -// let response = srv.execute(request.send()).unwrap(); - -// let contentlength = response -// .headers() -// .get(header::CONTENT_LENGTH) -// .unwrap() -// .to_str() -// .unwrap(); - -// assert_eq!(contentlength, "11"); - -// // Invalid range header -// let request = srv -// .get() -// .uri(srv.url("/t%65st/tests/test.binary")) -// .header(header::RANGE, "bytes=10-8") -// .finish() -// .unwrap(); - -// let response = srv.execute(request.send()).unwrap(); - -// let contentlength = response -// .headers() -// .get(header::CONTENT_LENGTH) -// .unwrap() -// .to_str() -// .unwrap(); - -// assert_eq!(contentlength, "0"); - -// // Without range header -// let request = srv -// .get() -// .uri(srv.url("/t%65st/tests/test.binary")) -// .no_default_headers() -// .finish() -// .unwrap(); - -// let response = srv.execute(request.send()).unwrap(); - -// let contentlength = response -// .headers() -// .get(header::CONTENT_LENGTH) -// .unwrap() -// .to_str() -// .unwrap(); - -// assert_eq!(contentlength, "100"); - -// // chunked -// let request = srv -// .get() -// .uri(srv.url("/t%65st/tests/test.binary")) -// .finish() -// .unwrap(); - -// let response = srv.execute(request.send()).unwrap(); -// { -// let te = response -// .headers() -// .get(header::TRANSFER_ENCODING) -// .unwrap() -// .to_str() -// .unwrap(); -// assert_eq!(te, "chunked"); -// } -// let bytes = srv.execute(response.body()).unwrap(); -// let data = Bytes::from(fs::read("tests/test.binary").unwrap()); -// assert_eq!(bytes, data); -// } - -// #[test] -// fn test_static_files_with_spaces() { -// let mut srv = test::TestServer::with_factory(|| { -// App::new() -// .handler("/", StaticFiles::new(".").unwrap().index_file("Cargo.toml")) -// }); -// let request = srv -// .get() -// .uri(srv.url("/tests/test%20space.binary")) -// .finish() -// .unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::OK); - -// let bytes = srv.execute(response.body()).unwrap(); -// let data = Bytes::from(fs::read("tests/test space.binary").unwrap()); -// assert_eq!(bytes, data); -// } - -// #[derive(Default)] -// pub struct OnlyMethodHeadConfig; -// impl StaticFileConfig for OnlyMethodHeadConfig { -// fn is_method_allowed(method: &Method) -> bool { -// match *method { -// Method::HEAD => true, -// _ => false, -// } -// } -// } - -// #[test] -// fn test_named_file_not_allowed() { -// let file = -// NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); -// let req = TestRequest::default().method(Method::POST).finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - -// let file = -// NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); -// let req = TestRequest::default().method(Method::PUT).finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - -// let file = -// NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap(); -// let req = TestRequest::default().method(Method::GET).finish(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); -// } - -// #[test] -// fn test_named_file_content_encoding() { -// let req = TestRequest::default().method(Method::GET).finish(); -// let file = NamedFile::open("Cargo.toml").unwrap(); - -// assert!(file.encoding.is_none()); -// let resp = file -// .set_content_encoding(ContentEncoding::Identity) -// .respond_to(&req) -// .unwrap(); - -// assert!(resp.content_encoding().is_some()); -// assert_eq!(resp.content_encoding().unwrap().as_str(), "identity"); -// } - -// #[test] -// fn test_named_file_any_method() { -// let req = TestRequest::default().method(Method::POST).finish(); -// let file = NamedFile::open("Cargo.toml").unwrap(); -// let resp = file.respond_to(&req).unwrap(); -// assert_eq!(resp.status(), StatusCode::OK); -// } - -// #[test] -// fn test_static_files() { -// let mut st = StaticFiles::new(".").unwrap().show_files_listing(); -// let req = TestRequest::with_uri("/missing") -// .param("tail", "missing") -// .finish(); -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::NOT_FOUND); - -// st.show_index = false; -// let req = TestRequest::default().finish(); -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::NOT_FOUND); - -// let req = TestRequest::default().param("tail", "").finish(); - -// st.show_index = true; -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "text/html; charset=utf-8" -// ); -// assert!(resp.body().is_binary()); -// assert!(format!("{:?}", resp.body()).contains("README.md")); -// } - -// #[test] -// fn test_static_files_bad_directory() { -// let st: Result, Error> = StaticFiles::new("missing"); -// assert!(st.is_err()); - -// let st: Result, Error> = StaticFiles::new("Cargo.toml"); -// assert!(st.is_err()); -// } - -// #[test] -// fn test_default_handler_file_missing() { -// let st = StaticFiles::new(".") -// .unwrap() -// .default_handler(|_: &_| "default content"); -// let req = TestRequest::with_uri("/missing") -// .param("tail", "missing") -// .finish(); - -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::OK); -// assert_eq!( -// resp.body(), -// &Body::Binary(Binary::Slice(b"default content")) -// ); -// } - -// #[test] -// fn test_serve_index() { -// let st = StaticFiles::new(".").unwrap().index_file("test.binary"); -// let req = TestRequest::default().uri("/tests").finish(); - -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::OK); -// assert_eq!( -// resp.headers() -// .get(header::CONTENT_TYPE) -// .expect("content type"), -// "application/octet-stream" -// ); -// assert_eq!( -// resp.headers() -// .get(header::CONTENT_DISPOSITION) -// .expect("content disposition"), -// "attachment; filename=\"test.binary\"" -// ); - -// let req = TestRequest::default().uri("/tests/").finish(); -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::OK); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "application/octet-stream" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "attachment; filename=\"test.binary\"" -// ); - -// // nonexistent index file -// let req = TestRequest::default().uri("/tests/unknown").finish(); -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::NOT_FOUND); - -// let req = TestRequest::default().uri("/tests/unknown/").finish(); -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::NOT_FOUND); -// } - -// #[test] -// fn test_serve_index_nested() { -// let st = StaticFiles::new(".").unwrap().index_file("mod.rs"); -// let req = TestRequest::default().uri("/src/client").finish(); -// let resp = st.handle(&req).respond_to(&req).unwrap(); -// let resp = resp.as_msg(); -// assert_eq!(resp.status(), StatusCode::OK); -// assert_eq!( -// resp.headers().get(header::CONTENT_TYPE).unwrap(), -// "text/x-rust" -// ); -// assert_eq!( -// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), -// "inline; filename=\"mod.rs\"" -// ); -// } - -// #[test] -// fn integration_serve_index_with_prefix() { -// let mut srv = test::TestServer::with_factory(|| { -// App::new() -// .prefix("public") -// .handler("/", StaticFiles::new(".").unwrap().index_file("Cargo.toml")) -// }); - -// let request = srv.get().uri(srv.url("/public")).finish().unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::OK); -// let bytes = srv.execute(response.body()).unwrap(); -// let data = Bytes::from(fs::read("Cargo.toml").unwrap()); -// assert_eq!(bytes, data); - -// let request = srv.get().uri(srv.url("/public/")).finish().unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::OK); -// let bytes = srv.execute(response.body()).unwrap(); -// let data = Bytes::from(fs::read("Cargo.toml").unwrap()); -// assert_eq!(bytes, data); -// } - -// #[test] -// fn integration_serve_index() { -// let mut srv = test::TestServer::with_factory(|| { -// App::new().handler( -// "test", -// StaticFiles::new(".").unwrap().index_file("Cargo.toml"), -// ) -// }); - -// let request = srv.get().uri(srv.url("/test")).finish().unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::OK); -// let bytes = srv.execute(response.body()).unwrap(); -// let data = Bytes::from(fs::read("Cargo.toml").unwrap()); -// assert_eq!(bytes, data); - -// let request = srv.get().uri(srv.url("/test/")).finish().unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::OK); -// let bytes = srv.execute(response.body()).unwrap(); -// let data = Bytes::from(fs::read("Cargo.toml").unwrap()); -// assert_eq!(bytes, data); - -// // nonexistent index file -// let request = srv.get().uri(srv.url("/test/unknown")).finish().unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::NOT_FOUND); - -// let request = srv.get().uri(srv.url("/test/unknown/")).finish().unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::NOT_FOUND); -// } - -// #[test] -// fn integration_percent_encoded() { -// let mut srv = test::TestServer::with_factory(|| { -// App::new().handler( -// "test", -// StaticFiles::new(".").unwrap().index_file("Cargo.toml"), -// ) -// }); - -// let request = srv -// .get() -// .uri(srv.url("/test/%43argo.toml")) -// .finish() -// .unwrap(); -// let response = srv.execute(request.send()).unwrap(); -// assert_eq!(response.status(), StatusCode::OK); -// } - -// struct T(&'static str, u64, Vec); - -// #[test] -// fn test_parse() { -// let tests = vec![ -// T("", 0, vec![]), -// T("", 1000, vec![]), -// T("foo", 0, vec![]), -// T("bytes=", 0, vec![]), -// T("bytes=7", 10, vec![]), -// T("bytes= 7 ", 10, vec![]), -// T("bytes=1-", 0, vec![]), -// T("bytes=5-4", 10, vec![]), -// T("bytes=0-2,5-4", 10, vec![]), -// T("bytes=2-5,4-3", 10, vec![]), -// T("bytes=--5,4--3", 10, vec![]), -// T("bytes=A-", 10, vec![]), -// T("bytes=A- ", 10, vec![]), -// T("bytes=A-Z", 10, vec![]), -// T("bytes= -Z", 10, vec![]), -// T("bytes=5-Z", 10, vec![]), -// T("bytes=Ran-dom, garbage", 10, vec![]), -// T("bytes=0x01-0x02", 10, vec![]), -// T("bytes= ", 10, vec![]), -// T("bytes= , , , ", 10, vec![]), -// T( -// "bytes=0-9", -// 10, -// vec![HttpRange { -// start: 0, -// length: 10, -// }], -// ), -// T( -// "bytes=0-", -// 10, -// vec![HttpRange { -// start: 0, -// length: 10, -// }], -// ), -// T( -// "bytes=5-", -// 10, -// vec![HttpRange { -// start: 5, -// length: 5, -// }], -// ), -// T( -// "bytes=0-20", -// 10, -// vec![HttpRange { -// start: 0, -// length: 10, -// }], -// ), -// T( -// "bytes=15-,0-5", -// 10, -// vec![HttpRange { -// start: 0, -// length: 6, -// }], -// ), -// T( -// "bytes=1-2,5-", -// 10, -// vec![ -// HttpRange { -// start: 1, -// length: 2, -// }, -// HttpRange { -// start: 5, -// length: 5, -// }, -// ], -// ), -// T( -// "bytes=-2 , 7-", -// 11, -// vec![ -// HttpRange { -// start: 9, -// length: 2, -// }, -// HttpRange { -// start: 7, -// length: 4, -// }, -// ], -// ), -// T( -// "bytes=0-0 ,2-2, 7-", -// 11, -// vec![ -// HttpRange { -// start: 0, -// length: 1, -// }, -// HttpRange { -// start: 2, -// length: 1, -// }, -// HttpRange { -// start: 7, -// length: 4, -// }, -// ], -// ), -// T( -// "bytes=-5", -// 10, -// vec![HttpRange { -// start: 5, -// length: 5, -// }], -// ), -// T( -// "bytes=-15", -// 10, -// vec![HttpRange { -// start: 0, -// length: 10, -// }], -// ), -// T( -// "bytes=0-499", -// 10000, -// vec![HttpRange { -// start: 0, -// length: 500, -// }], -// ), -// T( -// "bytes=500-999", -// 10000, -// vec![HttpRange { -// start: 500, -// length: 500, -// }], -// ), -// T( -// "bytes=-500", -// 10000, -// vec![HttpRange { -// start: 9500, -// length: 500, -// }], -// ), -// T( -// "bytes=9500-", -// 10000, -// vec![HttpRange { -// start: 9500, -// length: 500, -// }], -// ), -// T( -// "bytes=0-0,-1", -// 10000, -// vec![ -// HttpRange { -// start: 0, -// length: 1, -// }, -// HttpRange { -// start: 9999, -// length: 1, -// }, -// ], -// ), -// T( -// "bytes=500-600,601-999", -// 10000, -// vec![ -// HttpRange { -// start: 500, -// length: 101, -// }, -// HttpRange { -// start: 601, -// length: 399, -// }, -// ], -// ), -// T( -// "bytes=500-700,601-999", -// 10000, -// vec![ -// HttpRange { -// start: 500, -// length: 201, -// }, -// HttpRange { -// start: 601, -// length: 399, -// }, -// ], -// ), -// // Match Apache laxity: -// T( -// "bytes= 1 -2 , 4- 5, 7 - 8 , ,,", -// 11, -// vec![ -// HttpRange { -// start: 1, -// length: 2, -// }, -// HttpRange { -// start: 4, -// length: 2, -// }, -// HttpRange { -// start: 7, -// length: 2, -// }, -// ], -// ), -// ]; - -// for t in tests { -// let header = t.0; -// let size = t.1; -// let expected = t.2; - -// let res = HttpRange::parse(header, size); - -// if res.is_err() { -// if expected.is_empty() { -// continue; -// } else { -// assert!( -// false, -// "parse({}, {}) returned error {:?}", -// header, -// size, -// res.unwrap_err() -// ); -// } -// } - -// let got = res.unwrap(); - -// if got.len() != expected.len() { -// assert!( -// false, -// "len(parseRange({}, {})) = {}, want {}", -// header, -// size, -// got.len(), -// expected.len() -// ); -// continue; -// } - -// for i in 0..expected.len() { -// if got[i].start != expected[i].start { -// assert!( -// false, -// "parseRange({}, {})[{}].start = {}, want {}", -// header, size, i, got[i].start, expected[i].start -// ) -// } -// if got[i].length != expected[i].length { -// assert!( -// false, -// "parseRange({}, {})[{}].length = {}, want {}", -// header, size, i, got[i].length, expected[i].length -// ) -// } -// } -// } -// } -// } From 3fc28c5d073abd131f3988019553409e0a14616c Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 09:27:02 -0800 Subject: [PATCH 040/109] simplify StaticFile constructor, move HttpRange to separate module --- actix-staticfiles/src/lib.rs | 447 +++------------------------------ actix-staticfiles/src/named.rs | 5 +- actix-staticfiles/src/range.rs | 375 +++++++++++++++++++++++++++ 3 files changed, 407 insertions(+), 420 deletions(-) create mode 100644 actix-staticfiles/src/range.rs diff --git a/actix-staticfiles/src/lib.rs b/actix-staticfiles/src/lib.rs index 01306d4b..81d8269c 100644 --- a/actix-staticfiles/src/lib.rs +++ b/actix-staticfiles/src/lib.rs @@ -27,10 +27,12 @@ use futures::future::{ok, FutureResult}; mod config; mod error; mod named; +mod range; use self::error::{StaticFilesError, UriSegmentError}; pub use crate::config::{DefaultConfig, StaticFileConfig}; pub use crate::named::NamedFile; +pub use crate::range::HttpRange; type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; @@ -212,17 +214,15 @@ fn directory_listing( /// Static files handling /// -/// `StaticFile` handler must be registered with `App::handler()` method, -/// because `StaticFile` handler requires access sub-path information. +/// `StaticFile` handler must be registered with `App::service()` method. /// -/// ```rust,ignore -/// # extern crate actix_web; -/// use actix_web::{fs, App}; +/// ```rust +/// use actix_web::App; +/// use actix_staticfiles as fs; /// /// fn main() { /// let app = App::new() -/// .handler("/static", fs::StaticFiles::new(".").unwrap()) -/// .finish(); +/// .service(fs::StaticFiles::new("/static", ".")); /// } /// ``` pub struct StaticFiles { @@ -243,7 +243,7 @@ impl StaticFiles { /// `StaticFile` 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) -> Result, Error> { + pub fn new>(path: &str, dir: T) -> StaticFiles { Self::with_config(path, dir, DefaultConfig) } } @@ -252,18 +252,13 @@ impl StaticFiles { /// Create new `StaticFiles` instance for specified base directory. /// /// Identical with `new` but allows to specify configiration to use. - pub fn with_config>( - path: &str, - dir: T, - _: C, - ) -> Result, Error> { - let dir = dir.into().canonicalize()?; - + pub fn with_config>(path: &str, dir: T, _: C) -> StaticFiles { + let dir = dir.into().canonicalize().unwrap_or_else(|_| PathBuf::new()); if !dir.is_dir() { - return Err(StaticFilesError::IsNotDirectory.into()); + log::error!("Specified path is not a directory"); } - Ok(StaticFiles { + StaticFiles { path: ResourceDef::root_prefix(path), directory: dir, index: None, @@ -273,7 +268,7 @@ impl StaticFiles { _chunk_size: 0, _follow_symlinks: false, _cd_map: PhantomData, - }) + } } /// Show files listing for directories. @@ -452,101 +447,6 @@ impl

    FromRequest

    for PathBufWrp { } } -/// HTTP Range header representation. -#[derive(Debug, Clone, Copy)] -struct HttpRange { - pub start: u64, - pub length: u64, -} - -static PREFIX: &'static str = "bytes="; -const PREFIX_LEN: usize = 6; - -impl HttpRange { - /// Parses Range HTTP header string as per RFC 2616. - /// - /// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`). - /// `size` is full size of response (file). - fn parse(header: &str, size: u64) -> Result, ()> { - if header.is_empty() { - return Ok(Vec::new()); - } - if !header.starts_with(PREFIX) { - return Err(()); - } - - let size_sig = size as i64; - let mut no_overlap = false; - - let all_ranges: Vec> = header[PREFIX_LEN..] - .split(',') - .map(|x| x.trim()) - .filter(|x| !x.is_empty()) - .map(|ra| { - let mut start_end_iter = ra.split('-'); - - let start_str = start_end_iter.next().ok_or(())?.trim(); - let end_str = start_end_iter.next().ok_or(())?.trim(); - - if start_str.is_empty() { - // If no start is specified, end specifies the - // range start relative to the end of the file. - let mut length: i64 = end_str.parse().map_err(|_| ())?; - - if length > size_sig { - length = size_sig; - } - - Ok(Some(HttpRange { - start: (size_sig - length) as u64, - length: length as u64, - })) - } else { - let start: i64 = start_str.parse().map_err(|_| ())?; - - if start < 0 { - return Err(()); - } - if start >= size_sig { - no_overlap = true; - return Ok(None); - } - - let length = if end_str.is_empty() { - // If no end is specified, range extends to end of the file. - size_sig - start - } else { - let mut end: i64 = end_str.parse().map_err(|_| ())?; - - if start > end { - return Err(()); - } - - if end >= size_sig { - end = size_sig - 1; - } - - end - start + 1 - }; - - Ok(Some(HttpRange { - start: start as u64, - length: length as u64, - })) - } - }) - .collect::>()?; - - let ranges: Vec = all_ranges.into_iter().filter_map(|x| x).collect(); - - if no_overlap && ranges.is_empty() { - return Err(()); - } - - Ok(ranges) - } -} - #[cfg(test)] mod tests { use std::fs; @@ -800,11 +700,7 @@ mod tests { #[test] fn test_named_file_ranges_status_code() { let mut srv = test::init_service( - App::new().service( - StaticFiles::new("/test", ".") - .unwrap() - .index_file("Cargo.toml"), - ), + App::new().service(StaticFiles::new("/test", ".").index_file("Cargo.toml")), ); // Valid range header @@ -828,11 +724,8 @@ mod tests { #[test] fn test_named_file_content_range_headers() { let mut srv = test::init_service( - App::new().service( - StaticFiles::new("/test", ".") - .unwrap() - .index_file("tests/test.binary"), - ), + App::new() + .service(StaticFiles::new("/test", ".").index_file("tests/test.binary")), ); // Valid range header @@ -871,11 +764,8 @@ mod tests { #[test] fn test_named_file_content_length_headers() { let mut srv = test::init_service( - App::new().service( - StaticFiles::new("test", ".") - .unwrap() - .index_file("tests/test.binary"), - ), + App::new() + .service(StaticFiles::new("test", ".").index_file("tests/test.binary")), ); // Valid range header @@ -948,8 +838,7 @@ mod tests { #[test] fn test_static_files_with_spaces() { let mut srv = test::init_service( - App::new() - .service(StaticFiles::new("/", ".").unwrap().index_file("Cargo.toml")), + App::new().service(StaticFiles::new("/", ".").index_file("Cargo.toml")), ); let request = TestRequest::get() .uri("/tests/test%20space.binary") @@ -1030,22 +919,21 @@ mod tests { #[test] fn test_static_files() { let mut srv = test::init_service( - App::new().service(StaticFiles::new("/", ".").unwrap().show_files_listing()), + App::new().service(StaticFiles::new("/", ".").show_files_listing()), ); let req = TestRequest::with_uri("/missing").to_request(); let resp = test::call_success(&mut srv, req); assert_eq!(resp.status(), StatusCode::NOT_FOUND); - let mut srv = - test::init_service(App::new().service(StaticFiles::new("/", ".").unwrap())); + let mut srv = test::init_service(App::new().service(StaticFiles::new("/", "."))); let req = TestRequest::default().to_request(); let resp = test::call_success(&mut srv, req); assert_eq!(resp.status(), StatusCode::NOT_FOUND); let mut srv = test::init_service( - App::new().service(StaticFiles::new("/", ".").unwrap().show_files_listing()), + App::new().service(StaticFiles::new("/", ".").show_files_listing()), ); let req = TestRequest::with_uri("/tests").to_request(); let mut resp = test::call_success(&mut srv, req); @@ -1065,17 +953,13 @@ mod tests { #[test] fn test_static_files_bad_directory() { - let st: Result, Error> = StaticFiles::new("/", "missing"); - assert!(st.is_err()); - - let st: Result, Error> = StaticFiles::new("/", "Cargo.toml"); - assert!(st.is_err()); + let _st: StaticFiles<()> = StaticFiles::new("/", "missing"); + let _st: StaticFiles<()> = StaticFiles::new("/", "Cargo.toml"); } // #[test] // fn test_default_handler_file_missing() { // let st = StaticFiles::new(".") - // .unwrap() // .default_handler(|_: &_| "default content"); // let req = TestRequest::with_uri("/missing") // .param("tail", "missing") @@ -1092,7 +976,7 @@ mod tests { // #[test] // fn test_serve_index() { - // let st = StaticFiles::new(".").unwrap().index_file("test.binary"); + // let st = StaticFiles::new(".").index_file("test.binary"); // let req = TestRequest::default().uri("/tests").finish(); // let resp = st.handle(&req).respond_to(&req).unwrap(); @@ -1138,7 +1022,7 @@ mod tests { // #[test] // fn test_serve_index_nested() { - // let st = StaticFiles::new(".").unwrap().index_file("mod.rs"); + // let st = StaticFiles::new(".").index_file("mod.rs"); // let req = TestRequest::default().uri("/src/client").finish(); // let resp = st.handle(&req).respond_to(&req).unwrap(); // let resp = resp.as_msg(); @@ -1158,7 +1042,7 @@ mod tests { // let mut srv = test::TestServer::with_factory(|| { // App::new().handler( // "test", - // StaticFiles::new(".").unwrap().index_file("Cargo.toml"), + // StaticFiles::new(".").index_file("Cargo.toml"), // ) // }); @@ -1191,7 +1075,7 @@ mod tests { // let mut srv = test::TestServer::with_factory(|| { // App::new().handler( // "test", - // StaticFiles::new(".").unwrap().index_file("Cargo.toml"), + // StaticFiles::new(".").index_file("Cargo.toml"), // ) // }); @@ -1204,279 +1088,4 @@ mod tests { // assert_eq!(response.status(), StatusCode::OK); // } - struct T(&'static str, u64, Vec); - - #[test] - fn test_parse() { - let tests = vec![ - T("", 0, vec![]), - T("", 1000, vec![]), - T("foo", 0, vec![]), - T("bytes=", 0, vec![]), - T("bytes=7", 10, vec![]), - T("bytes= 7 ", 10, vec![]), - T("bytes=1-", 0, vec![]), - T("bytes=5-4", 10, vec![]), - T("bytes=0-2,5-4", 10, vec![]), - T("bytes=2-5,4-3", 10, vec![]), - T("bytes=--5,4--3", 10, vec![]), - T("bytes=A-", 10, vec![]), - T("bytes=A- ", 10, vec![]), - T("bytes=A-Z", 10, vec![]), - T("bytes= -Z", 10, vec![]), - T("bytes=5-Z", 10, vec![]), - T("bytes=Ran-dom, garbage", 10, vec![]), - T("bytes=0x01-0x02", 10, vec![]), - T("bytes= ", 10, vec![]), - T("bytes= , , , ", 10, vec![]), - T( - "bytes=0-9", - 10, - vec![HttpRange { - start: 0, - length: 10, - }], - ), - T( - "bytes=0-", - 10, - vec![HttpRange { - start: 0, - length: 10, - }], - ), - T( - "bytes=5-", - 10, - vec![HttpRange { - start: 5, - length: 5, - }], - ), - T( - "bytes=0-20", - 10, - vec![HttpRange { - start: 0, - length: 10, - }], - ), - T( - "bytes=15-,0-5", - 10, - vec![HttpRange { - start: 0, - length: 6, - }], - ), - T( - "bytes=1-2,5-", - 10, - vec![ - HttpRange { - start: 1, - length: 2, - }, - HttpRange { - start: 5, - length: 5, - }, - ], - ), - T( - "bytes=-2 , 7-", - 11, - vec![ - HttpRange { - start: 9, - length: 2, - }, - HttpRange { - start: 7, - length: 4, - }, - ], - ), - T( - "bytes=0-0 ,2-2, 7-", - 11, - vec![ - HttpRange { - start: 0, - length: 1, - }, - HttpRange { - start: 2, - length: 1, - }, - HttpRange { - start: 7, - length: 4, - }, - ], - ), - T( - "bytes=-5", - 10, - vec![HttpRange { - start: 5, - length: 5, - }], - ), - T( - "bytes=-15", - 10, - vec![HttpRange { - start: 0, - length: 10, - }], - ), - T( - "bytes=0-499", - 10000, - vec![HttpRange { - start: 0, - length: 500, - }], - ), - T( - "bytes=500-999", - 10000, - vec![HttpRange { - start: 500, - length: 500, - }], - ), - T( - "bytes=-500", - 10000, - vec![HttpRange { - start: 9500, - length: 500, - }], - ), - T( - "bytes=9500-", - 10000, - vec![HttpRange { - start: 9500, - length: 500, - }], - ), - T( - "bytes=0-0,-1", - 10000, - vec![ - HttpRange { - start: 0, - length: 1, - }, - HttpRange { - start: 9999, - length: 1, - }, - ], - ), - T( - "bytes=500-600,601-999", - 10000, - vec![ - HttpRange { - start: 500, - length: 101, - }, - HttpRange { - start: 601, - length: 399, - }, - ], - ), - T( - "bytes=500-700,601-999", - 10000, - vec![ - HttpRange { - start: 500, - length: 201, - }, - HttpRange { - start: 601, - length: 399, - }, - ], - ), - // Match Apache laxity: - T( - "bytes= 1 -2 , 4- 5, 7 - 8 , ,,", - 11, - vec![ - HttpRange { - start: 1, - length: 2, - }, - HttpRange { - start: 4, - length: 2, - }, - HttpRange { - start: 7, - length: 2, - }, - ], - ), - ]; - - for t in tests { - let header = t.0; - let size = t.1; - let expected = t.2; - - let res = HttpRange::parse(header, size); - - if res.is_err() { - if expected.is_empty() { - continue; - } else { - assert!( - false, - "parse({}, {}) returned error {:?}", - header, - size, - res.unwrap_err() - ); - } - } - - let got = res.unwrap(); - - if got.len() != expected.len() { - assert!( - false, - "len(parseRange({}, {})) = {}, want {}", - header, - size, - got.len(), - expected.len() - ); - continue; - } - - for i in 0..expected.len() { - if got[i].start != expected[i].start { - assert!( - false, - "parseRange({}, {})[{}].start = {}, want {}", - header, size, i, got[i].start, expected[i].start - ) - } - if got[i].length != expected[i].length { - assert!( - false, - "parseRange({}, {})[{}].length = {}, want {}", - header, size, i, got[i].length, expected[i].length - ) - } - } - } - } } diff --git a/actix-staticfiles/src/named.rs b/actix-staticfiles/src/named.rs index 5fba0483..2fc1c454 100644 --- a/actix-staticfiles/src/named.rs +++ b/actix-staticfiles/src/named.rs @@ -17,7 +17,8 @@ use actix_web::http::{ContentEncoding, Method, StatusCode}; use actix_web::{HttpMessage, HttpRequest, HttpResponse, Responder}; use crate::config::{DefaultConfig, StaticFileConfig}; -use crate::{ChunkedReadFile, HttpRange}; +use crate::range::HttpRange; +use crate::ChunkedReadFile; /// A file with an associated name. #[derive(Debug)] @@ -303,6 +304,8 @@ impl Responder for NamedFile { type Future = Result; fn respond_to(self, req: &HttpRequest) -> Self::Future { + println!("RESP: {:?}", req); + if self.status_code != StatusCode::OK { let mut resp = HttpResponse::build(self.status_code); resp.set(header::ContentType(self.content_type.clone())) diff --git a/actix-staticfiles/src/range.rs b/actix-staticfiles/src/range.rs new file mode 100644 index 00000000..d97a35e7 --- /dev/null +++ b/actix-staticfiles/src/range.rs @@ -0,0 +1,375 @@ +/// HTTP Range header representation. +#[derive(Debug, Clone, Copy)] +pub struct HttpRange { + pub start: u64, + pub length: u64, +} + +static PREFIX: &'static str = "bytes="; +const PREFIX_LEN: usize = 6; + +impl HttpRange { + /// Parses Range HTTP header string as per RFC 2616. + /// + /// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`). + /// `size` is full size of response (file). + pub fn parse(header: &str, size: u64) -> Result, ()> { + if header.is_empty() { + return Ok(Vec::new()); + } + if !header.starts_with(PREFIX) { + return Err(()); + } + + let size_sig = size as i64; + let mut no_overlap = false; + + let all_ranges: Vec> = header[PREFIX_LEN..] + .split(',') + .map(|x| x.trim()) + .filter(|x| !x.is_empty()) + .map(|ra| { + let mut start_end_iter = ra.split('-'); + + let start_str = start_end_iter.next().ok_or(())?.trim(); + let end_str = start_end_iter.next().ok_or(())?.trim(); + + if start_str.is_empty() { + // If no start is specified, end specifies the + // range start relative to the end of the file. + let mut length: i64 = end_str.parse().map_err(|_| ())?; + + if length > size_sig { + length = size_sig; + } + + Ok(Some(HttpRange { + start: (size_sig - length) as u64, + length: length as u64, + })) + } else { + let start: i64 = start_str.parse().map_err(|_| ())?; + + if start < 0 { + return Err(()); + } + if start >= size_sig { + no_overlap = true; + return Ok(None); + } + + let length = if end_str.is_empty() { + // If no end is specified, range extends to end of the file. + size_sig - start + } else { + let mut end: i64 = end_str.parse().map_err(|_| ())?; + + if start > end { + return Err(()); + } + + if end >= size_sig { + end = size_sig - 1; + } + + end - start + 1 + }; + + Ok(Some(HttpRange { + start: start as u64, + length: length as u64, + })) + } + }) + .collect::>()?; + + let ranges: Vec = all_ranges.into_iter().filter_map(|x| x).collect(); + + if no_overlap && ranges.is_empty() { + return Err(()); + } + + Ok(ranges) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct T(&'static str, u64, Vec); + + #[test] + fn test_parse() { + let tests = vec![ + T("", 0, vec![]), + T("", 1000, vec![]), + T("foo", 0, vec![]), + T("bytes=", 0, vec![]), + T("bytes=7", 10, vec![]), + T("bytes= 7 ", 10, vec![]), + T("bytes=1-", 0, vec![]), + T("bytes=5-4", 10, vec![]), + T("bytes=0-2,5-4", 10, vec![]), + T("bytes=2-5,4-3", 10, vec![]), + T("bytes=--5,4--3", 10, vec![]), + T("bytes=A-", 10, vec![]), + T("bytes=A- ", 10, vec![]), + T("bytes=A-Z", 10, vec![]), + T("bytes= -Z", 10, vec![]), + T("bytes=5-Z", 10, vec![]), + T("bytes=Ran-dom, garbage", 10, vec![]), + T("bytes=0x01-0x02", 10, vec![]), + T("bytes= ", 10, vec![]), + T("bytes= , , , ", 10, vec![]), + T( + "bytes=0-9", + 10, + vec![HttpRange { + start: 0, + length: 10, + }], + ), + T( + "bytes=0-", + 10, + vec![HttpRange { + start: 0, + length: 10, + }], + ), + T( + "bytes=5-", + 10, + vec![HttpRange { + start: 5, + length: 5, + }], + ), + T( + "bytes=0-20", + 10, + vec![HttpRange { + start: 0, + length: 10, + }], + ), + T( + "bytes=15-,0-5", + 10, + vec![HttpRange { + start: 0, + length: 6, + }], + ), + T( + "bytes=1-2,5-", + 10, + vec![ + HttpRange { + start: 1, + length: 2, + }, + HttpRange { + start: 5, + length: 5, + }, + ], + ), + T( + "bytes=-2 , 7-", + 11, + vec![ + HttpRange { + start: 9, + length: 2, + }, + HttpRange { + start: 7, + length: 4, + }, + ], + ), + T( + "bytes=0-0 ,2-2, 7-", + 11, + vec![ + HttpRange { + start: 0, + length: 1, + }, + HttpRange { + start: 2, + length: 1, + }, + HttpRange { + start: 7, + length: 4, + }, + ], + ), + T( + "bytes=-5", + 10, + vec![HttpRange { + start: 5, + length: 5, + }], + ), + T( + "bytes=-15", + 10, + vec![HttpRange { + start: 0, + length: 10, + }], + ), + T( + "bytes=0-499", + 10000, + vec![HttpRange { + start: 0, + length: 500, + }], + ), + T( + "bytes=500-999", + 10000, + vec![HttpRange { + start: 500, + length: 500, + }], + ), + T( + "bytes=-500", + 10000, + vec![HttpRange { + start: 9500, + length: 500, + }], + ), + T( + "bytes=9500-", + 10000, + vec![HttpRange { + start: 9500, + length: 500, + }], + ), + T( + "bytes=0-0,-1", + 10000, + vec![ + HttpRange { + start: 0, + length: 1, + }, + HttpRange { + start: 9999, + length: 1, + }, + ], + ), + T( + "bytes=500-600,601-999", + 10000, + vec![ + HttpRange { + start: 500, + length: 101, + }, + HttpRange { + start: 601, + length: 399, + }, + ], + ), + T( + "bytes=500-700,601-999", + 10000, + vec![ + HttpRange { + start: 500, + length: 201, + }, + HttpRange { + start: 601, + length: 399, + }, + ], + ), + // Match Apache laxity: + T( + "bytes= 1 -2 , 4- 5, 7 - 8 , ,,", + 11, + vec![ + HttpRange { + start: 1, + length: 2, + }, + HttpRange { + start: 4, + length: 2, + }, + HttpRange { + start: 7, + length: 2, + }, + ], + ), + ]; + + for t in tests { + let header = t.0; + let size = t.1; + let expected = t.2; + + let res = HttpRange::parse(header, size); + + if res.is_err() { + if expected.is_empty() { + continue; + } else { + assert!( + false, + "parse({}, {}) returned error {:?}", + header, + size, + res.unwrap_err() + ); + } + } + + let got = res.unwrap(); + + if got.len() != expected.len() { + assert!( + false, + "len(parseRange({}, {})) = {}, want {}", + header, + size, + got.len(), + expected.len() + ); + continue; + } + + for i in 0..expected.len() { + if got[i].start != expected[i].start { + assert!( + false, + "parseRange({}, {})[{}].start = {}, want {}", + header, size, i, got[i].start, expected[i].start + ) + } + if got[i].length != expected[i].length { + assert!( + false, + "parseRange({}, {})[{}].length = {}, want {}", + header, size, i, got[i].length, expected[i].length + ) + } + } + } + } +} From db566a634cf7562f1d9ea69965aa8ecb8f92725e Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 10:03:18 -0800 Subject: [PATCH 041/109] make State type Send compatible --- src/state.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/state.rs b/src/state.rs index d4e4c894..265c6f01 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,5 +1,5 @@ use std::ops::Deref; -use std::rc::Rc; +use std::sync::Arc; use actix_http::error::{Error, ErrorInternalServerError}; use actix_http::Extensions; @@ -18,11 +18,11 @@ pub(crate) trait StateFactoryResult { } /// Application state -pub struct State(Rc); +pub struct State(Arc); impl State { pub(crate) fn new(state: T) -> State { - State(Rc::new(state)) + State(Arc::new(state)) } /// Get referecnce to inner state type. From db39a604ae0859e50f47f79cfc4a05e1fc2711ca Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 10:03:37 -0800 Subject: [PATCH 042/109] implement ResponseError trait for BlockingError --- src/blocking.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/blocking.rs b/src/blocking.rs index abf4282c..01be30dd 100644 --- a/src/blocking.rs +++ b/src/blocking.rs @@ -1,10 +1,15 @@ //! Thread pool for blocking operations +use std::fmt; + +use derive_more::Display; use futures::sync::oneshot; use futures::{Async, Future, Poll}; use parking_lot::Mutex; use threadpool::ThreadPool; +use crate::ResponseError; + /// Env variable for default cpu pool size const ENV_CPU_POOL_VAR: &str = "ACTIX_CPU_POOL"; @@ -36,18 +41,23 @@ thread_local! { }; } -pub enum BlockingError { +#[derive(Debug, Display)] +pub enum BlockingError { + #[display(fmt = "{:?}", _0)] Error(E), + #[display(fmt = "Thread pool is gone")] Canceled, } +impl ResponseError for BlockingError {} + /// Execute blocking function on a thread pool, returns future that resolves /// to result of the function execution. pub fn run(f: F) -> CpuFuture where F: FnOnce() -> Result + Send + 'static, I: Send + 'static, - E: Send + 'static, + E: Send + fmt::Debug + 'static, { let (tx, rx) = oneshot::channel(); POOL.with(|pool| { @@ -63,7 +73,7 @@ pub struct CpuFuture { rx: oneshot::Receiver>, } -impl Future for CpuFuture { +impl Future for CpuFuture { type Item = I; type Error = BlockingError; From 5cde4dc479ce5c08dc7a2db9bf47afa2d2e5a9e7 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 10:41:07 -0800 Subject: [PATCH 043/109] update actix-rt --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index acacb2f2..a17669d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,7 +62,7 @@ ssl = ["openssl", "actix-server/ssl"] actix-codec = "0.1.0" #actix-service = "0.3.2" #actix-utils = "0.3.1" -actix-rt = "0.1.0" +actix-rt = "0.2.0" actix-service = { git = "https://github.com/actix/actix-net.git" } actix-utils = { git = "https://github.com/actix/actix-net.git" } From fe22e831447551a661f1e3d75185a16b259eba88 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 15:47:15 -0800 Subject: [PATCH 044/109] refactor service registration process; unify services and resources --- actix-session/src/cookie.rs | 26 +- actix-session/src/lib.rs | 14 +- actix-staticfiles/src/lib.rs | 30 +- examples/basic.rs | 28 +- src/app.rs | 420 +++++++++------------------- src/config.rs | 103 +++++++ src/extract.rs | 104 +++---- src/guard.rs | 15 +- src/lib.rs | 91 +++++- src/middleware/defaultheaders.rs | 9 +- src/request.rs | 7 +- src/resource.rs | 117 ++++++-- src/responder.rs | 21 +- src/route.rs | 43 +-- src/scope.rs | 456 +++++++++++++------------------ src/server.rs | 40 +-- src/service.rs | 35 +++ tests/test_server.rs | 65 ++--- 18 files changed, 845 insertions(+), 779 deletions(-) create mode 100644 src/config.rs diff --git a/actix-session/src/cookie.rs b/actix-session/src/cookie.rs index 9cde02e0..7fd5ec64 100644 --- a/actix-session/src/cookie.rs +++ b/actix-session/src/cookie.rs @@ -174,7 +174,7 @@ impl CookieSessionInner { /// /// ```rust /// use actix_session::CookieSession; -/// use actix_web::{App, HttpResponse, HttpServer}; +/// use actix_web::{web, App, HttpResponse, HttpServer}; /// /// fn main() { /// let app = App::new().middleware( @@ -183,7 +183,7 @@ impl CookieSessionInner { /// .name("actix_session") /// .path("/") /// .secure(true)) -/// .resource("/", |r| r.to(|| HttpResponse::Ok())); +/// .service(web::resource("/").to(|| HttpResponse::Ok())); /// } /// ``` pub struct CookieSession(Rc); @@ -314,19 +314,17 @@ where #[cfg(test)] mod tests { use super::*; - use actix_web::{test, App}; + use actix_web::{test, web, App}; #[test] fn cookie_session() { let mut app = test::init_service( App::new() .middleware(CookieSession::signed(&[0; 32]).secure(false)) - .resource("/", |r| { - r.to(|ses: Session| { - let _ = ses.set("counter", 100); - "test" - }) - }), + .service(web::resource("/").to(|ses: Session| { + let _ = ses.set("counter", 100); + "test" + })), ); let request = test::TestRequest::get().to_request(); @@ -342,12 +340,10 @@ mod tests { let mut app = test::init_service( App::new() .middleware(CookieSession::signed(&[0; 32]).secure(false)) - .resource("/", |r| { - r.to(|ses: Session| { - let _ = ses.set("counter", 100); - "test" - }) - }), + .service(web::resource("/").to(|ses: Session| { + let _ = ses.set("counter", 100); + "test" + })), ); let request = test::TestRequest::get().to_request(); diff --git a/actix-session/src/lib.rs b/actix-session/src/lib.rs index f57e11f2..62bc5c8f 100644 --- a/actix-session/src/lib.rs +++ b/actix-session/src/lib.rs @@ -13,7 +13,7 @@ //! extractor allows us to get or set session data. //! //! ```rust -//! use actix_web::{App, HttpServer, HttpResponse, Error}; +//! use actix_web::{web, App, HttpServer, HttpResponse, Error}; //! use actix_session::{Session, CookieSession}; //! //! fn index(session: Session) -> Result<&'static str, Error> { @@ -29,19 +29,17 @@ //! } //! //! fn main() -> std::io::Result<()> { -//! let sys = actix_rt::System::new("example"); // <- create Actix runtime -//! +//! # std::thread::spawn(|| //! HttpServer::new( //! || App::new().middleware( //! CookieSession::signed(&[0; 32]) // <- create cookie based session middleware //! .secure(false) //! ) -//! .resource("/", |r| r.to(|| HttpResponse::Ok()))) +//! .service(web::resource("/").to(|| HttpResponse::Ok()))) //! .bind("127.0.0.1:59880")? -//! .start(); -//! # actix_rt::System::current().stop(); -//! sys.run(); -//! Ok(()) +//! .run() +//! # ); +//! # Ok(()) //! } //! ``` use std::cell::RefCell; diff --git a/actix-staticfiles/src/lib.rs b/actix-staticfiles/src/lib.rs index 81d8269c..7c3f6849 100644 --- a/actix-staticfiles/src/lib.rs +++ b/actix-staticfiles/src/lib.rs @@ -17,7 +17,7 @@ use v_htmlescape::escape as escape_html_entity; use actix_http::error::{Error, ErrorInternalServerError}; use actix_service::{boxed::BoxedNewService, NewService, Service}; -use actix_web::dev::{self, HttpServiceFactory, ResourceDef, Url}; +use actix_web::dev::{self, AppConfig, HttpServiceFactory, ResourceDef, Url}; use actix_web::{ blocking, FromRequest, HttpRequest, HttpResponse, Responder, ServiceFromRequest, ServiceRequest, ServiceResponse, @@ -226,7 +226,7 @@ fn directory_listing( /// } /// ``` pub struct StaticFiles { - path: ResourceDef, + path: String, directory: PathBuf, index: Option, show_index: bool, @@ -259,7 +259,7 @@ impl StaticFiles { } StaticFiles { - path: ResourceDef::root_prefix(path), + path: path.to_string(), directory: dir, index: None, show_index: false, @@ -300,15 +300,21 @@ impl StaticFiles { } } -impl HttpServiceFactory

    for StaticFiles { - type Factory = Self; - - fn rdef(&self) -> &ResourceDef { - &self.path - } - - fn create(self) -> Self { - self +impl HttpServiceFactory

    for StaticFiles +where + P: 'static, + C: StaticFileConfig + 'static, +{ + fn register(self, config: &mut AppConfig

    ) { + if self.default.borrow().is_none() { + *self.default.borrow_mut() = Some(config.default_service()); + } + let rdef = if config.is_root() { + ResourceDef::root_prefix(&self.path) + } else { + ResourceDef::prefix(&self.path) + }; + config.register_service(rdef, None, self) } } diff --git a/examples/basic.rs b/examples/basic.rs index 7c72439e..5fd862d4 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -27,23 +27,23 @@ fn main() -> std::io::Result<()> { App::new() .middleware(middleware::DefaultHeaders::new().header("X-Version", "0.2")) .middleware(middleware::Compress::default()) - .resource("/resource1/index.html", |r| r.route(web::get().to(index))) - .resource("/resource2/index.html", |r| { - r.middleware( - middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"), - ) - .default_resource(|r| { - r.route(web::route().to(|| HttpResponse::MethodNotAllowed())) - }) - .route(web::method(Method::GET).to_async(index_async)) - }) - .resource("/test1.html", |r| r.to(|| "Test\r\n")) - .resource("/", |r| r.to(no_params)) + .service(web::resource("/resource1/index.html").route(web::get().to(index))) + .service( + web::resource("/resource2/index.html") + .middleware( + middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"), + ) + .default_resource(|r| { + r.route(web::route().to(|| HttpResponse::MethodNotAllowed())) + }) + .route(web::method(Method::GET).to_async(index_async)), + ) + .service(web::resource("/test1.html").to(|| "Test\r\n")) + .service(web::resource("/").to(no_params)) }) .bind("127.0.0.1:8080")? .workers(1) .start(); - let _ = sys.run(); - Ok(()) + sys.run() } diff --git a/src/app.rs b/src/app.rs index d503d8dd..7c194d27 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,16 +7,20 @@ use actix_http::{Extensions, PayloadStream, Request, Response}; use actix_router::{Path, ResourceDef, ResourceInfo, Router, Url}; use actix_service::boxed::{self, BoxedNewService, BoxedService}; use actix_service::{ - AndThenNewService, ApplyTransform, IntoNewService, IntoTransform, NewService, - Service, Transform, + fn_service, AndThenNewService, ApplyTransform, IntoNewService, IntoTransform, + NewService, Service, Transform, }; use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; +use crate::config::AppConfig; use crate::guard::Guard; use crate::resource::Resource; -use crate::scope::{insert_slash, Scope}; -use crate::service::{ServiceRequest, ServiceResponse}; +use crate::route::Route; +use crate::service::{ + HttpServiceFactory, ServiceFactory, ServiceFactoryWrapper, ServiceRequest, + ServiceResponse, +}; use crate::state::{State, StateFactory, StateFactoryResult}; type Guards = Vec>; @@ -24,19 +28,6 @@ type HttpService

    = BoxedService, ServiceResponse, ()>; type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; type BoxedResponse = Box>; -pub trait HttpServiceFactory

    { - type Factory: NewService< - ServiceRequest

    , - Response = ServiceResponse, - Error = (), - InitError = (), - >; - - fn rdef(&self) -> &ResourceDef; - - fn create(self) -> Self::Factory; -} - /// Application builder - structure that follows the builder pattern /// for building application instances. pub struct App @@ -97,9 +88,9 @@ where /// fn main() { /// let app = App::new() /// .state(MyState{ counter: Cell::new(0) }) - /// .resource( - /// "/index.html", - /// |r| r.route(web::get().to(index))); + /// .service( + /// web::resource("/index.html").route( + /// web::get().to(index))); /// } /// ``` pub fn state(mut self, state: S) -> Self { @@ -120,112 +111,6 @@ where self } - /// Configure scope for common root path. - /// - /// Scopes collect multiple paths under a common path prefix. - /// Scope path can contain variable path segments as resources. - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{App, HttpRequest, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().scope("/{project_id}", |scope| { - /// scope - /// .resource("/path1", |r| r.to(|| HttpResponse::Ok())) - /// .resource("/path2", |r| r.to(|| HttpResponse::Ok())) - /// .resource("/path3", |r| r.to(|| HttpResponse::MethodNotAllowed())) - /// }); - /// } - /// ``` - /// - /// In the above example, three routes get added: - /// * /{project_id}/path1 - /// * /{project_id}/path2 - /// * /{project_id}/path3 - /// - pub fn scope(self, path: &str, f: F) -> AppRouter> - where - F: FnOnce(Scope

    ) -> Scope

    , - { - let mut scope = f(Scope::new(path)); - let rdef = scope.rdef().clone(); - let default = scope.get_default(); - let guards = scope.take_guards(); - - let fref = Rc::new(RefCell::new(None)); - AppRouter { - chain: self.chain, - services: vec![(rdef, boxed::new_service(scope.into_new_service()), guards)], - default: None, - defaults: vec![default], - endpoint: AppEntry::new(fref.clone()), - factory_ref: fref, - extensions: self.extensions, - state: self.state, - _t: PhantomData, - } - } - - /// Configure resource for a specific path. - /// - /// Resources may have variable path segments. For example, a - /// resource with the path `/a/{name}/c` would match all incoming - /// requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`. - /// - /// 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. This is done by - /// looking up the identifier in the `Params` object returned by - /// `HttpRequest.match_info()` method. - /// - /// By default, each segment matches the regular expression `[^{}/]+`. - /// - /// You can also specify a custom regex in the form `{identifier:regex}`: - /// - /// For instance, to route `GET`-requests on any route matching - /// `/users/{userid}/{friend}` and store `userid` and `friend` in - /// the exposed `Params` object: - /// - /// ```rust - /// # extern crate actix_web; - /// use actix_web::{web, http, App, HttpResponse}; - /// - /// fn main() { - /// let app = App::new().resource("/users/{userid}/{friend}", |r| { - /// r.route(web::get().to(|| HttpResponse::Ok())) - /// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) - /// }); - /// } - /// ``` - pub fn resource(self, path: &str, f: F) -> AppRouter> - where - F: FnOnce(Resource

    ) -> Resource, - U: NewService< - ServiceRequest

    , - Response = ServiceResponse, - Error = (), - InitError = (), - > + 'static, - { - let rdef = ResourceDef::new(&insert_slash(path)); - let res = f(Resource::new()); - let default = res.get_default(); - - let fref = Rc::new(RefCell::new(None)); - AppRouter { - chain: self.chain, - services: vec![(rdef, boxed::new_service(res.into_new_service()), None)], - default: None, - defaults: vec![default], - endpoint: AppEntry::new(fref.clone()), - factory_ref: fref, - extensions: self.extensions, - state: self.state, - _t: PhantomData, - } - } - /// Register a middleware. pub fn middleware( self, @@ -259,7 +144,6 @@ where state: self.state, services: Vec::new(), default: None, - defaults: Vec::new(), factory_ref: fref, extensions: self.extensions, _t: PhantomData, @@ -298,25 +182,52 @@ where } } - /// Register resource handler service. + /// 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, extract::Path}; + /// + /// fn index(data: 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

    , + ) -> AppRouter> { + self.service( + Resource::new(path) + .add_guards(route.take_guards()) + .route(route), + ) + } + + /// Register http service. pub fn service(self, service: F) -> AppRouter> where F: HttpServiceFactory

    + 'static, { let fref = Rc::new(RefCell::new(None)); + AppRouter { chain: self.chain, - services: vec![( - service.rdef().clone(), - boxed::new_service(service.create().map_init_err(|_| ())), - None, - )], default: None, - defaults: vec![], endpoint: AppEntry::new(fref.clone()), factory_ref: fref, extensions: self.extensions, state: self.state, + services: vec![Box::new(ServiceFactoryWrapper::new(service))], _t: PhantomData, } } @@ -338,10 +249,9 @@ where /// for building application instances. pub struct AppRouter { chain: C, - services: Vec<(ResourceDef, HttpNewService

    , Option)>, - default: Option>>, - defaults: Vec>>>>>, endpoint: T, + services: Vec>>, + default: Option>>, factory_ref: Rc>>>, extensions: Extensions, state: Vec>, @@ -359,131 +269,48 @@ where InitError = (), >, { - /// Configure scope for common root path. + /// Configure route for a specific path. /// - /// Scopes collect multiple paths under a common path prefix. - /// Scope path can contain variable path segments as resources. + /// 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 - /// # extern crate actix_web; - /// use actix_web::{App, HttpRequest, HttpResponse}; + /// use actix_web::{web, App, HttpResponse, extract::Path}; /// - /// fn main() { - /// let app = App::new().scope("/{project_id}", |scope| { - /// scope - /// .resource("/path1", |r| r.to(|| HttpResponse::Ok())) - /// .resource("/path2", |r| r.to(|| HttpResponse::Ok())) - /// .resource("/path3", |r| r.to(|| HttpResponse::MethodNotAllowed())) - /// }); + /// fn index(data: Path<(String, String)>) -> &'static str { + /// "Welcome!" /// } - /// ``` - /// - /// In the above example, three routes get added: - /// * /{project_id}/path1 - /// * /{project_id}/path2 - /// * /{project_id}/path3 - /// - pub fn scope(mut self, path: &str, f: F) -> Self - where - F: FnOnce(Scope

    ) -> Scope

    , - { - let mut scope = f(Scope::new(path)); - let rdef = scope.rdef().clone(); - let guards = scope.take_guards(); - self.defaults.push(scope.get_default()); - self.services - .push((rdef, boxed::new_service(scope.into_new_service()), guards)); - self - } - - /// Configure resource for a specific path. - /// - /// Resources may have variable path segments. For example, a - /// resource with the path `/a/{name}/c` would match all incoming - /// requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`. - /// - /// 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. This is done by - /// looking up the identifier in the `Params` object returned by - /// `HttpRequest.match_info()` method. - /// - /// By default, each segment matches the regular expression `[^{}/]+`. - /// - /// You can also specify a custom regex in the form `{identifier:regex}`: - /// - /// For instance, to route `GET`-requests on any route matching - /// `/users/{userid}/{friend}` and store `userid` and `friend` in - /// the exposed `Params` object: - /// - /// ```rust - /// use actix_web::{web, http, App, HttpResponse}; /// /// fn main() { /// let app = App::new() - /// .resource("/users/{userid}/{friend}", |r| { - /// r.route(web::to(|| HttpResponse::Ok())) - /// }) - /// .resource("/index.html", |r| { - /// r.route(web::head().to(|| HttpResponse::MethodNotAllowed())) - /// }); + /// .route("/test1", web::get().to(index)) + /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())); /// } /// ``` - pub fn resource(mut self, path: &str, f: F) -> Self - where - F: FnOnce(Resource

    ) -> Resource, - U: NewService< - ServiceRequest

    , - Response = ServiceResponse, - Error = (), - InitError = (), - > + 'static, - { - let rdef = ResourceDef::new(&insert_slash(path)); - let resource = f(Resource::new()); - self.defaults.push(resource.get_default()); - self.services.push(( - rdef, - boxed::new_service(resource.into_new_service()), - None, - )); - self + pub fn route(self, path: &str, mut route: Route

    ) -> Self { + self.service( + Resource::new(path) + .add_guards(route.take_guards()) + .route(route), + ) } - /// Default resource to be used if no matching route could be found. + /// Register http service. /// - /// Default resource works with resources only and does not work with - /// custom services. - pub fn default_resource(mut self, f: F) -> Self - where - F: FnOnce(Resource

    ) -> Resource, - U: NewService< - ServiceRequest

    , - Response = ServiceResponse, - Error = (), - InitError = (), - > + 'static, - { - // create and configure default resource - self.default = Some(Rc::new(boxed::new_service( - f(Resource::new()).into_new_service().map_init_err(|_| ()), - ))); - - self - } - - /// Register resource handler service. + /// Http service is any type that implements `HttpServiceFactory` trait. + /// + /// Actix web provides several services implementations: + /// + /// * *Resource* is an entry in route 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(mut self, factory: F) -> Self where F: HttpServiceFactory

    + 'static, { - let rdef = factory.rdef().clone(); - - self.services.push(( - rdef, - boxed::new_service(factory.create().map_init_err(|_| ())), - None, - )); + self.services + .push(Box::new(ServiceFactoryWrapper::new(factory))); self } @@ -520,13 +347,34 @@ where state: self.state, services: self.services, default: self.default, - defaults: self.defaults, factory_ref: self.factory_ref, extensions: self.extensions, _t: PhantomData, } } + /// Default resource to be used if no matching route could be found. + /// + /// Default resource works with resources only and does not work with + /// custom services. + pub fn default_resource(mut self, f: F) -> Self + where + F: FnOnce(Resource

    ) -> Resource, + U: NewService< + ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + > + 'static, + { + // create and configure default resource + self.default = Some(Rc::new(boxed::new_service( + f(Resource::new("")).into_new_service().map_init_err(|_| ()), + ))); + + self + } + /// Register an external resource. /// /// External resources are useful for URL generation purposes only @@ -583,19 +431,30 @@ where { fn into_new_service(self) -> AndThenNewService, T> { // update resource default service - if self.default.is_some() { - for default in &self.defaults { - if default.borrow_mut().is_none() { - *default.borrow_mut() = self.default.clone(); - } - } - } + let default = self.default.unwrap_or_else(|| { + Rc::new(boxed::new_service(fn_service(|req: ServiceRequest

    | { + Ok(req.into_response(Response::NotFound().finish())) + }))) + }); + + let mut config = AppConfig::new( + "127.0.0.1:8080".parse().unwrap(), + "localhost:8080".to_owned(), + false, + default.clone(), + ); + + // register services + self.services + .into_iter() + .for_each(|mut srv| srv.register(&mut config)); // set factory *self.factory_ref.borrow_mut() = Some(AppRoutingFactory { - default: self.default.clone(), + default: default, services: Rc::new( - self.services + config + .into_services() .into_iter() .map(|(rdef, srv, guards)| (rdef, srv, RefCell::new(guards))) .collect(), @@ -613,7 +472,7 @@ where pub struct AppRoutingFactory

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

    { @@ -624,12 +483,6 @@ impl NewService> for AppRoutingFactory

    { type Future = AppRoutingFactoryResponse

    ; fn new_service(&self, _: &()) -> Self::Future { - let default_fut = if let Some(ref default) = self.default { - Some(default.new_service(&())) - } else { - None - }; - AppRoutingFactoryResponse { fut: self .services @@ -643,7 +496,7 @@ impl NewService> for AppRoutingFactory

    { }) .collect(), default: None, - default_fut, + default_fut: Some(self.default.new_service(&())), } } } @@ -929,16 +782,15 @@ where #[cfg(test)] mod tests { - use actix_http::http::{Method, StatusCode}; - use super::*; - use crate::test::{self, block_on, TestRequest}; + use crate::http::{Method, StatusCode}; + use crate::test::{self, block_on, init_service, TestRequest}; use crate::{web, HttpResponse, State}; #[test] fn test_default_resource() { - let mut srv = test::init_service( - App::new().resource("/test", |r| r.to(|| HttpResponse::Ok())), + let mut srv = init_service( + App::new().service(web::resource("/test").to(|| HttpResponse::Ok())), ); let req = TestRequest::with_uri("/test").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -948,13 +800,14 @@ mod tests { let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); - let mut srv = test::init_service( + let mut srv = init_service( App::new() - .resource("/test", |r| r.to(|| HttpResponse::Ok())) - .resource("/test2", |r| { - r.default_resource(|r| r.to(|| HttpResponse::Created())) - .route(web::get().to(|| HttpResponse::Ok())) - }) + .service(web::resource("/test").to(|| HttpResponse::Ok())) + .service( + web::resource("/test2") + .default_resource(|r| r.to(|| HttpResponse::Created())) + .route(web::get().to(|| HttpResponse::Ok())), + ) .default_resource(|r| r.to(|| HttpResponse::MethodNotAllowed())), ); @@ -975,22 +828,21 @@ mod tests { #[test] fn test_state() { - let mut srv = test::init_service( + let mut srv = init_service( App::new() .state(10usize) - .resource("/", |r| r.to(|_: State| HttpResponse::Ok())), + .service(web::resource("/").to(|_: State| HttpResponse::Ok())), ); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); - let mut srv = test::init_service( + let mut srv = init_service( App::new() .state(10u32) - .resource("/", |r| r.to(|_: State| HttpResponse::Ok())), + .service(web::resource("/").to(|_: State| HttpResponse::Ok())), ); - let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); @@ -998,22 +850,20 @@ mod tests { #[test] fn test_state_factory() { - let mut srv = test::init_service( + let mut srv = init_service( App::new() .state_factory(|| Ok::<_, ()>(10usize)) - .resource("/", |r| r.to(|_: State| HttpResponse::Ok())), + .service(web::resource("/").to(|_: State| HttpResponse::Ok())), ); - let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); - let mut srv = test::init_service( + let mut srv = init_service( App::new() .state_factory(|| Ok::<_, ()>(10u32)) - .resource("/", |r| r.to(|_: State| HttpResponse::Ok())), + .service(web::resource("/").to(|_: State| HttpResponse::Ok())), ); - let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 00000000..483b0a50 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,103 @@ +use std::net::SocketAddr; +use std::rc::Rc; + +use actix_router::ResourceDef; +use actix_service::{boxed, IntoNewService, NewService}; + +use crate::guard::Guard; +use crate::service::{ServiceRequest, ServiceResponse}; + +type Guards = Vec>; +type HttpNewService

    = + boxed::BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; + +/// Application configuration +pub struct AppConfig

    { + addr: SocketAddr, + secure: bool, + host: String, + root: bool, + default: Rc>, + services: Vec<(ResourceDef, HttpNewService

    , Option)>, +} + +impl AppConfig

    { + /// Crate server settings instance + pub(crate) fn new( + addr: SocketAddr, + host: String, + secure: bool, + default: Rc>, + ) -> Self { + AppConfig { + addr, + secure, + host, + default, + root: true, + services: Vec::new(), + } + } + + /// Check if root is beeing configured + pub fn is_root(&self) -> bool { + self.root + } + + pub(crate) fn into_services( + self, + ) -> Vec<(ResourceDef, HttpNewService

    , Option)> { + self.services + } + + pub(crate) fn clone_config(&self) -> Self { + AppConfig { + addr: self.addr, + secure: self.secure, + host: self.host.clone(), + default: self.default.clone(), + services: Vec::new(), + root: false, + } + } + + /// Returns the socket address of the local half of this TCP connection + pub fn local_addr(&self) -> SocketAddr { + self.addr + } + + /// Returns true if connection is secure(https) + pub fn secure(&self) -> bool { + self.secure + } + + /// Returns host header value + pub fn host(&self) -> &str { + &self.host + } + + pub fn default_service(&self) -> Rc> { + self.default.clone() + } + + pub fn register_service( + &mut self, + rdef: ResourceDef, + guards: Option>>, + service: F, + ) where + F: IntoNewService>, + S: NewService< + ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + > + 'static, + { + self.services.push(( + rdef, + boxed::new_service(service.into_new_service()), + guards, + )); + } +} diff --git a/src/extract.rs b/src/extract.rs index 7350d7d9..0b212aba 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -88,9 +88,9 @@ impl ExtractorConfig for () { /// } /// /// fn main() { -/// let app = App::new().resource( -/// "/{username}/{count}/index.html", // <- define path parameters -/// |r| r.route(web::get().to(index)) // <- register handler with `Path` extractor +/// let app = App::new().service( +/// web::resource("/{username}/{count}/index.html") // <- define path parameters +/// .route(web::get().to(index)) // <- register handler with `Path` extractor /// ); /// } /// ``` @@ -113,9 +113,9 @@ impl ExtractorConfig for () { /// } /// /// fn main() { -/// let app = App::new().resource( -/// "/{username}/index.html", // <- define path parameters -/// |r| r.route(web::get().to(index)) // <- use handler with Path` extractor +/// let app = App::new().service( +/// web::resource("/{username}/index.html") // <- define path parameters +/// .route(web::get().to(index)) // <- use handler with Path` extractor /// ); /// } /// ``` @@ -180,9 +180,9 @@ impl From for Path { /// } /// /// fn main() { -/// let app = App::new().resource( -/// "/{username}/{count}/index.html", // <- define path parameters -/// |r| r.route(web::get().to(index)) // <- register handler with `Path` extractor +/// let app = App::new().service( +/// web::resource("/{username}/{count}/index.html") // <- define path parameters +/// .route(web::get().to(index)) // <- register handler with `Path` extractor /// ); /// } /// ``` @@ -205,9 +205,9 @@ impl From for Path { /// } /// /// fn main() { -/// let app = App::new().resource( -/// "/{username}/index.html", // <- define path parameters -/// |r| r.route(web::get().to(index)) // <- use handler with Path` extractor +/// let app = App::new().service( +/// web::resource("/{username}/index.html") // <- define path parameters +/// .route(web::get().to(index)) // <- use handler with Path` extractor /// ); /// } /// ``` @@ -266,9 +266,8 @@ impl fmt::Display for Path { /// } /// /// fn main() { -/// let app = App::new().resource( -/// "/index.html", -/// |r| r.route(web::get().to(index))); // <- use `Query` extractor +/// let app = App::new().service( +/// web::resource("/index.html").route(web::get().to(index))); // <- use `Query` extractor /// } /// ``` pub struct Query(T); @@ -322,9 +321,9 @@ impl Query { /// } /// /// fn main() { -/// let app = App::new().resource( -/// "/index.html", -/// |r| r.route(web::get().to(index))); // <- use `Query` extractor +/// let app = App::new().service( +/// web::resource("/index.html") +/// .route(web::get().to(index))); // <- use `Query` extractor /// } /// ``` impl FromRequest

    for Query @@ -462,14 +461,13 @@ impl fmt::Display for Form { /// } /// /// fn main() { -/// let app = App::new().resource( -/// "/index.html", -/// |r| { -/// r.route(web::get() +/// let app = App::new().service( +/// web::resource("/index.html") +/// .route(web::get() /// // change `Form` extractor configuration /// .config(extract::FormConfig::default().limit(4097)) /// .to(index)) -/// }); +/// ); /// } /// ``` #[derive(Clone)] @@ -535,9 +533,10 @@ impl Default for FormConfig { /// } /// /// fn main() { -/// let app = App::new().resource( -/// "/index.html", -/// |r| r.route(web::post().to(index))); +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::post().to(index)) +/// ); /// } /// ``` /// @@ -645,9 +644,10 @@ impl Responder for Json { /// } /// /// fn main() { -/// let app = App::new().resource( -/// "/index.html", -/// |r| r.route(web::post().to(index))); +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::post().to(index)) +/// ); /// } /// ``` impl FromRequest

    for Json @@ -692,16 +692,17 @@ where /// } /// /// fn main() { -/// let app = App::new().resource("/index.html", |r| { -/// r.route(web::post().config( -/// // change json extractor configuration -/// extract::JsonConfig::default().limit(4096) -/// .error_handler(|err, req| { // <- create custom error response -/// error::InternalError::from_response( -/// err, HttpResponse::Conflict().finish()).into() -/// })) -/// .to(index)) -/// }); +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::post().config( +/// // change json extractor configuration +/// extract::JsonConfig::default().limit(4096) +/// .error_handler(|err, req| { // <- create custom error response +/// error::InternalError::from_response( +/// err, HttpResponse::Conflict().finish()).into() +/// })) +/// .to(index)) +/// ); /// } /// ``` #[derive(Clone)] @@ -757,8 +758,10 @@ impl Default for JsonConfig { /// } /// /// fn main() { -/// let app = App::new() -/// .resource("/index.html", |r| r.route(web::get().to(index))); +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::get().to(index)) +/// ); /// } /// ``` impl

    FromRequest

    for Bytes @@ -801,12 +804,12 @@ where /// } /// /// fn main() { -/// let app = App::new().resource("/index.html", |r| { -/// r.route( +/// let app = App::new().service( +/// web::resource("/index.html").route( /// web::get() /// .config(extract::PayloadConfig::new(4096)) // <- limit size of the payload /// .to(index)) // <- register handler with extractor params -/// }); +/// ); /// } /// ``` impl

    FromRequest

    for String @@ -896,9 +899,10 @@ where /// } /// /// fn main() { -/// let app = App::new().resource("/users/:first", |r| { -/// r.route(web::post().to(index)) -/// }); +/// let app = App::new().service( +/// web::resource("/users/:first").route( +/// web::post().to(index)) +/// ); /// } /// ``` impl FromRequest

    for Option @@ -959,9 +963,9 @@ where /// } /// /// fn main() { -/// let app = App::new().resource("/users/:first", |r| { -/// r.route(web::post().to(index)) -/// }); +/// let app = App::new().service( +/// web::resource("/users/:first").route(web::post().to(index)) +/// ); /// } /// ``` impl FromRequest

    for Result diff --git a/src/guard.rs b/src/guard.rs index 93b6e132..1632b997 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -19,11 +19,10 @@ pub trait Guard { /// use actix_web::{web, guard, App, HttpResponse}; /// /// fn main() { -/// App::new().resource("/index.html", |r| -/// r.route( -/// web::route() -/// .guard(guard::Any(guard::Get()).or(guard::Post())) -/// .to(|| HttpResponse::MethodNotAllowed())) +/// App::new().service(web::resource("/index.html").route( +/// web::route() +/// .guard(guard::Any(guard::Get()).or(guard::Post())) +/// .to(|| HttpResponse::MethodNotAllowed())) /// ); /// } /// ``` @@ -60,12 +59,12 @@ impl Guard for AnyGuard { /// use actix_web::{guard, web, App, HttpResponse}; /// /// fn main() { -/// App::new().resource("/index.html", |r| { -/// r.route(web::route() +/// App::new().service(web::resource("/index.html").route( +/// web::route() /// .guard( /// guard::All(guard::Get()).and(guard::Header("content-type", "text/plain"))) /// .to(|| HttpResponse::MethodNotAllowed())) -/// }); +/// ); /// } /// ``` pub fn All(guard: F) -> AllGuard { diff --git a/src/lib.rs b/src/lib.rs index 44dcde35..3400fe29 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ pub mod extract; mod handler; // mod info; pub mod blocking; +mod config; pub mod guard; pub mod middleware; mod request; @@ -43,13 +44,24 @@ pub mod dev { //! use actix_web::dev::*; //! ``` - pub use crate::app::{AppRouter, HttpServiceFactory}; + pub use crate::app::AppRouter; + pub use crate::config::AppConfig; + pub use crate::service::HttpServiceFactory; + pub use actix_http::body::{Body, MessageBody, ResponseBody}; pub use actix_http::dev::ResponseBuilder as HttpResponseBuilder; pub use actix_http::{ Extensions, Payload, PayloadStream, RequestHead, ResponseHead, }; pub use actix_router::{Path, ResourceDef, Url}; + + pub(crate) fn insert_slash(path: &str) -> String { + let mut path = path.to_owned(); + if !path.is_empty() && !path.starts_with('/') { + path.insert(0, '/'); + }; + path + } } pub mod web { @@ -58,8 +70,74 @@ pub mod web { use crate::extract::FromRequest; use crate::handler::{AsyncFactory, Factory}; + use crate::resource::Resource; use crate::responder::Responder; - use crate::Route; + use crate::route::Route; + use crate::scope::Scope; + + /// Create resource for a specific path. + /// + /// Resources may have variable path segments. For example, a + /// resource with the path `/a/{name}/c` would match all incoming + /// requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`. + /// + /// 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. This is done by + /// looking up the identifier in the `Params` object returned by + /// `HttpRequest.match_info()` method. + /// + /// By default, each segment matches the regular expression `[^{}/]+`. + /// + /// You can also specify a custom regex in the form `{identifier:regex}`: + /// + /// For instance, to route `GET`-requests on any route matching + /// `/users/{userid}/{friend}` and store `userid` and `friend` in + /// the exposed `Params` object: + /// + /// ```rust + /// # extern crate actix_web; + /// use actix_web::{web, http, App, HttpResponse}; + /// + /// fn main() { + /// let app = App::new().service( + /// web::resource("/users/{userid}/{friend}") + /// .route(web::get().to(|| HttpResponse::Ok())) + /// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) + /// ); + /// } + /// ``` + pub fn resource(path: &str) -> Resource

    { + Resource::new(path) + } + + /// Configure scope for common root path. + /// + /// Scopes collect multiple paths under a common path prefix. + /// Scope path can contain variable path segments as resources. + /// + /// ```rust + /// # extern crate actix_web; + /// use actix_web::{web, App, HttpRequest, HttpResponse}; + /// + /// fn main() { + /// 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())) + /// ); + /// } + /// ``` + /// + /// In the above example, three routes get added: + /// * /{project_id}/path1 + /// * /{project_id}/path2 + /// * /{project_id}/path3 + /// + pub fn scope(path: &str) -> Scope

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

    { @@ -105,7 +183,10 @@ pub mod web { /// unimplemented!() /// } /// - /// App::new().resource("/", |r| r.route(web::to(index))); + /// App::new().service( + /// web::resource("/").route( + /// web::to(index)) + /// ); /// ``` pub fn to(handler: F) -> Route

    where @@ -125,7 +206,9 @@ pub mod web { /// futures::future::ok(HttpResponse::Ok().finish()) /// } /// - /// App::new().resource("/", |r| r.route(web::to_async(index))); + /// App::new().service(web::resource("/").route( + /// web::to_async(index)) + /// ); /// ``` pub fn to_async(handler: F) -> Route

    where diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index 5fd51919..137913d2 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -19,10 +19,11 @@ use crate::service::{ServiceRequest, ServiceResponse}; /// fn main() { /// let app = App::new() /// .middleware(middleware::DefaultHeaders::new().header("X-Version", "0.2")) -/// .resource("/test", |r| { -/// r.route(web::get().to(|| HttpResponse::Ok())) -/// .route(web::method(http::Method::HEAD).to(|| HttpResponse::MethodNotAllowed())) -/// }); +/// .service( +/// web::resource("/test") +/// .route(web::get().to(|| HttpResponse::Ok())) +/// .route(web::method(http::Method::HEAD).to(|| HttpResponse::MethodNotAllowed())) +/// ); /// } /// ``` #[derive(Clone)] diff --git a/src/request.rs b/src/request.rs index 211f60b8..1c86cac3 100644 --- a/src/request.rs +++ b/src/request.rs @@ -149,9 +149,10 @@ impl HttpMessage for HttpRequest { /// } /// /// fn main() { -/// let app = App::new().resource("/users/:first", |r| { -/// r.route(web::get().to(index)) -/// }); +/// let app = App::new().service( +/// web::resource("/users/{first}").route( +/// web::get().to(index)) +/// ); /// } /// ``` impl

    FromRequest

    for HttpRequest { diff --git a/src/resource.rs b/src/resource.rs index 8d81ead0..b5cf640c 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -9,7 +9,9 @@ use actix_service::{ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; +use crate::dev::{insert_slash, AppConfig, HttpServiceFactory, ResourceDef}; use crate::extract::FromRequest; +use crate::guard::Guard; use crate::handler::{AsyncFactory, Factory}; use crate::responder::Responder; use crate::route::{CreateRouteService, Route, RouteService}; @@ -32,38 +34,37 @@ type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, /// use actix_web::{web, App, HttpResponse}; /// /// fn main() { -/// let app = App::new() -/// .resource( -/// "/", |r| r.route(web::get().to(|| HttpResponse::Ok()))); +/// let app = App::new().service( +/// web::resource("/") +/// .route(web::get().to(|| HttpResponse::Ok()))); /// } pub struct Resource> { - routes: Vec>, endpoint: T, + rdef: String, + routes: Vec>, + guards: Vec>, default: Rc>>>>, factory_ref: Rc>>>, } impl

    Resource

    { - pub fn new() -> Resource

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

    { let fref = Rc::new(RefCell::new(None)); Resource { routes: Vec::new(), + rdef: path.to_string(), endpoint: ResourceEndpoint::new(fref.clone()), factory_ref: fref, + guards: Vec::new(), default: Rc::new(RefCell::new(None)), } } } -impl

    Default for Resource

    { - fn default() -> Self { - Self::new() - } -} - -impl Resource +impl Resource where + P: 'static, T: NewService< ServiceRequest

    , Response = ServiceResponse, @@ -71,19 +72,52 @@ where InitError = (), >, { + /// Add match guard to a resource. + /// + /// ```rust + /// use actix_web::{web, guard, App, HttpResponse, extract::Path}; + /// + /// fn index(data: Path<(String, String)>) -> &'static str { + /// "Welcome!" + /// } + /// + /// fn main() { + /// let app = App::new() + /// .service( + /// web::resource("/app") + /// .guard(guard::Header("content-type", "text/plain")) + /// .route(web::get().to(index)) + /// ) + /// .service( + /// web::resource("/app") + /// .guard(guard::Header("content-type", "text/json")) + /// .route(web::get().to(|| HttpResponse::MethodNotAllowed())) + /// ); + /// } + /// ``` + pub fn guard(mut self, guard: G) -> Self { + self.guards.push(Box::new(guard)); + self + } + + pub(crate) fn add_guards(mut self, guards: Vec>) -> Self { + self.guards.extend(guards); + self + } + /// Register a new route. /// /// ```rust /// use actix_web::{web, guard, App, HttpResponse}; /// /// fn main() { - /// let app = App::new() - /// .resource("/", |r| { - /// r.route(web::route() + /// let app = App::new().service( + /// web::resource("/").route( + /// web::route() /// .guard(guard::Any(guard::Get()).or(guard::Put())) /// .guard(guard::Header("Content-Type", "text/plain")) /// .to(|| HttpResponse::Ok())) - /// }); + /// ); /// } /// ``` /// @@ -93,12 +127,12 @@ where /// use actix_web::{web, guard, App, HttpResponse}; /// /// fn main() { - /// let app = App::new() - /// .resource("/container/", |r| { - /// r.route(web::get().to(get_handler)) + /// let app = App::new().service( + /// web::resource("/container/") + /// .route(web::get().to(get_handler)) /// .route(web::post().to(post_handler)) /// .route(web::delete().to(delete_handler)) - /// }); + /// ); /// } /// # fn get_handler() {} /// # fn post_handler() {} @@ -109,8 +143,7 @@ where self } - /// Register a new route and add handler. This route get called for all - /// requests. + /// Register a new route and add handler. This route matches all requests. /// /// ```rust /// use actix_web::*; @@ -119,7 +152,7 @@ where /// unimplemented!() /// } /// - /// App::new().resource("/", |r| r.to(index)); + /// App::new().service(web::resource("/").to(index)); /// ``` /// /// This is shortcut for: @@ -128,7 +161,7 @@ where /// # extern crate actix_web; /// # use actix_web::*; /// # fn index(req: HttpRequest) -> HttpResponse { unimplemented!() } - /// App::new().resource("/", |r| r.route(web::route().to(index))); + /// App::new().service(web::resource("/").route(web::route().to(index))); /// ``` pub fn to(mut self, handler: F) -> Self where @@ -150,7 +183,7 @@ where /// ok(HttpResponse::Ok().finish()) /// } /// - /// App::new().resource("/", |r| r.to_async(index)); + /// App::new().service(web::resource("/").to_async(index)); /// ``` /// /// This is shortcut for: @@ -161,7 +194,7 @@ where /// # fn index(req: HttpRequest) -> Box> { /// # unimplemented!() /// # } - /// App::new().resource("/", |r| r.route(web::route().to_async(index))); + /// App::new().service(web::resource("/").route(web::route().to_async(index))); /// ``` #[allow(clippy::wrong_self_convention)] pub fn to_async(mut self, handler: F) -> Self @@ -206,6 +239,8 @@ where let endpoint = ApplyTransform::new(mw, self.endpoint); Resource { endpoint, + rdef: self.rdef, + guards: self.guards, routes: self.routes, default: self.default, factory_ref: self.factory_ref, @@ -222,14 +257,38 @@ where { // 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(Resource::new("")).into_new_service().map_init_err(|_| ()), ))))); self } +} - pub(crate) fn get_default(&self) -> Rc>>>> { - self.default.clone() +impl HttpServiceFactory

    for Resource +where + P: 'static, + T: NewService< + ServiceRequest

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

    ) { + if self.default.borrow().is_none() { + *self.default.borrow_mut() = Some(config.default_service()); + } + let guards = if self.guards.is_empty() { + None + } else { + Some(std::mem::replace(&mut self.guards, Vec::new())) + }; + let rdef = if config.is_root() { + ResourceDef::new(&insert_slash(&self.rdef)) + } else { + ResourceDef::new(&insert_slash(&self.rdef)) + }; + config.register_service(rdef, guards, self) } } diff --git a/src/responder.rs b/src/responder.rs index dedfa1b4..9e9e0f10 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -288,22 +288,21 @@ where #[cfg(test)] mod tests { - // use actix_http::body::Body; - use actix_http::body::{Body, ResponseBody}; - use actix_http::http::StatusCode; - use actix_service::{IntoNewService, NewService, Service}; + use actix_service::Service; use bytes::Bytes; - use crate::test::TestRequest; - use crate::App; + use crate::body::{Body, ResponseBody}; + use crate::http::StatusCode; + use crate::test::{init_service, TestRequest}; + use crate::{web, App}; #[test] fn test_option_responder() { - let app = App::new() - .resource("/none", |r| r.to(|| -> Option<&'static str> { None })) - .resource("/some", |r| r.to(|| Some("some"))) - .into_new_service(); - let mut srv = TestRequest::block_on(app.new_service(&())).unwrap(); + let mut srv = init_service( + App::new() + .service(web::resource("/none").to(|| -> Option<&'static str> { None })) + .service(web::resource("/some").to(|| Some("some"))), + ); let req = TestRequest::with_uri("/none").to_request(); let resp = TestRequest::block_on(srv.call(req)).unwrap(); diff --git a/src/route.rs b/src/route.rs index 42e78488..bac897ab 100644 --- a/src/route.rs +++ b/src/route.rs @@ -84,6 +84,10 @@ impl Route

    { *self.config_ref.borrow_mut() = self.config.storage.clone(); self } + + pub(crate) fn take_guards(&mut self) -> Vec> { + std::mem::replace(Rc::get_mut(&mut self.guards).unwrap(), Vec::new()) + } } impl

    NewService> for Route

    { @@ -161,12 +165,12 @@ impl Route

    { /// ```rust /// # use actix_web::*; /// # fn main() { - /// App::new().resource("/path", |r| { - /// r.route(web::get() - /// .guard(guard::Get()) + /// App::new().service(web::resource("/path").route( + /// web::get() + /// .method(http::Method::CONNECT) /// .guard(guard::Header("content-type", "text/plain")) /// .to(|req: HttpRequest| HttpResponse::Ok())) - /// }); + /// ); /// # } /// ``` pub fn method(mut self, method: Method) -> Self { @@ -181,12 +185,12 @@ impl Route

    { /// ```rust /// # use actix_web::*; /// # fn main() { - /// App::new().resource("/path", |r| { - /// r.route(web::route() + /// App::new().service(web::resource("/path").route( + /// web::route() /// .guard(guard::Get()) /// .guard(guard::Header("content-type", "text/plain")) /// .to(|req: HttpRequest| HttpResponse::Ok())) - /// }); + /// ); /// # } /// ``` pub fn guard(mut self, f: F) -> Self { @@ -229,9 +233,9 @@ impl Route

    { /// } /// /// fn main() { - /// let app = App::new().resource( - /// "/{username}/index.html", // <- define path parameters - /// |r| r.route(web::get().to(index)), // <- register handler + /// let app = App::new().service( + /// web::resource("/{username}/index.html") // <- define path parameters + /// .route(web::get().to(index)) // <- register handler /// ); /// } /// ``` @@ -254,9 +258,9 @@ impl Route

    { /// } /// /// fn main() { - /// let app = App::new().resource( - /// "/{username}/index.html", // <- define path parameters - /// |r| r.route(web::get().to(index)), + /// let app = App::new().service( + /// web::resource("/{username}/index.html") // <- define path parameters + /// .route(web::get().to(index)) /// ); /// } /// ``` @@ -294,9 +298,9 @@ impl Route

    { /// } /// /// fn main() { - /// let app = App::new().resource( - /// "/{username}/index.html", // <- define path parameters - /// |r| r.route(web::get().to_async(index)), // <- register async handler + /// let app = App::new().service( + /// web::resource("/{username}/index.html") // <- define path parameters + /// .route(web::get().to_async(index)) // <- register async handler /// ); /// } /// ``` @@ -328,15 +332,14 @@ impl Route

    { /// } /// /// fn main() { - /// let app = App::new().resource("/index.html", |r| { - /// r.route( + /// let app = App::new().service( + /// web::resource("/index.html").route( /// web::get() /// // limit size of the payload /// .config(extract::PayloadConfig::new(4096)) /// // register handler /// .to(index) - /// ) - /// }); + /// )); /// } /// ``` pub fn config(mut self, config: C) -> Self { diff --git a/src/scope.rs b/src/scope.rs index dc88388f..9bc0b50d 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -10,10 +10,13 @@ use actix_service::{ use futures::future::{ok, Either, Future, FutureResult}; use futures::{Async, Poll}; +use crate::dev::{insert_slash, AppConfig, HttpServiceFactory}; use crate::guard::Guard; use crate::resource::Resource; use crate::route::Route; -use crate::service::{ServiceRequest, ServiceResponse}; +use crate::service::{ + ServiceFactory, ServiceFactoryWrapper, ServiceRequest, ServiceResponse, +}; type Guards = Vec>; type HttpService

    = BoxedService, ServiceResponse, ()>; @@ -35,12 +38,12 @@ type BoxedResponse = Box>; /// use actix_web::{web, App, HttpResponse}; /// /// fn main() { -/// let app = App::new().scope("/{project_id}/", |scope| { -/// scope -/// .resource("/path1", |r| r.to(|| HttpResponse::Ok())) -/// .resource("/path2", |r| r.route(web::get().to(|| HttpResponse::Ok()))) -/// .resource("/path3", |r| r.route(web::head().to(|| HttpResponse::MethodNotAllowed()))) -/// }); +/// let app = App::new().service( +/// web::scope("/{project_id}/") +/// .service(web::resource("/path1").to(|| HttpResponse::Ok())) +/// .service(web::resource("/path2").route(web::get().to(|| HttpResponse::Ok()))) +/// .service(web::resource("/path3").route(web::head().to(|| HttpResponse::MethodNotAllowed()))) +/// ); /// } /// ``` /// @@ -51,11 +54,10 @@ type BoxedResponse = Box>; /// pub struct Scope> { endpoint: T, - rdef: ResourceDef, - services: Vec<(ResourceDef, HttpNewService

    , Option)>, + rdef: String, + services: Vec>>, guards: Vec>, default: Rc>>>>, - defaults: Vec>>>>>, factory_ref: Rc>>>, } @@ -63,21 +65,20 @@ impl Scope

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

    { let fref = Rc::new(RefCell::new(None)); - let rdef = ResourceDef::prefix(&insert_slash(path)); Scope { endpoint: ScopeEndpoint::new(fref.clone()), - rdef: rdef.clone(), + rdef: path.to_string(), guards: Vec::new(), services: Vec::new(), default: Rc::new(RefCell::new(None)), - defaults: Vec::new(), factory_ref: fref, } } } -impl Scope +impl Scope where + P: 'static, T: NewService< ServiceRequest

    , Response = ServiceResponse, @@ -85,12 +86,7 @@ where InitError = (), >, { - #[inline] - pub(crate) fn rdef(&self) -> &ResourceDef { - &self.rdef - } - - /// Add guard to a scope. + /// Add match guard to a scope. /// /// ```rust /// use actix_web::{web, guard, App, HttpRequest, HttpResponse, extract::Path}; @@ -100,14 +96,14 @@ where /// } /// /// fn main() { - /// let app = App::new().scope("/app", |scope| { - /// scope + /// let app = App::new().service( + /// web::scope("/app") /// .guard(guard::Header("content-type", "text/plain")) - /// .route("/test1",web::get().to(index)) + /// .route("/test1", web::get().to(index)) /// .route("/test2", web::post().to(|r: HttpRequest| { /// HttpResponse::MethodNotAllowed() /// })) - /// }); + /// ); /// } /// ``` pub fn guard(mut self, guard: G) -> Self { @@ -115,10 +111,10 @@ where self } - /// Create nested scope. + /// Create nested service. /// /// ```rust - /// use actix_web::{App, HttpRequest}; + /// use actix_web::{web, App, HttpRequest}; /// /// struct AppState; /// @@ -127,28 +123,25 @@ where /// } /// /// fn main() { - /// let app = App::new().scope("/app", |scope| { - /// scope.nested("/v1", |scope| scope.resource("/test1", |r| r.to(index))) - /// }); + /// let app = App::new().service( + /// web::scope("/app").service( + /// web::scope("/v1") + /// .service(web::resource("/test1").to(index))) + /// ); /// } /// ``` - pub fn nested(mut self, path: &str, f: F) -> Self + pub fn service(mut self, factory: F) -> Self where - F: FnOnce(Scope

    ) -> Scope

    , + F: HttpServiceFactory

    + 'static, { - let mut scope = f(Scope::new(path)); - let rdef = scope.rdef().clone(); - let guards = scope.take_guards(); - self.defaults.push(scope.get_default()); self.services - .push((rdef, boxed::new_service(scope.into_new_service()), guards)); - + .push(Box::new(ServiceFactoryWrapper::new(factory))); self } /// Configure route for a specific path. /// - /// This is a simplified version of the `Scope::resource()` method. + /// This is a simplified version of the `Scope::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. /// @@ -160,58 +153,19 @@ where /// } /// /// fn main() { - /// let app = App::new().scope("/app", |scope| { - /// scope.route("/test1", web::get().to(index)) + /// let app = App::new().service( + /// web::scope("/app") + /// .route("/test1", web::get().to(index)) /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())) - /// }); + /// ); /// } /// ``` - pub fn route(self, path: &str, route: Route

    ) -> Self { - self.resource(path, move |r| r.route(route)) - } - - /// configure resource for a specific path. - /// - /// This method is similar to an `App::resource()` method. - /// Resources may have variable path segments. Resource path uses scope - /// path as a path prefix. - /// - /// ```rust - /// use actix_web::*; - /// - /// fn main() { - /// let app = App::new().scope("/api", |scope| { - /// scope.resource("/users/{userid}/{friend}", |r| { - /// r.route(web::get().to(|| HttpResponse::Ok())) - /// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) - /// .route(web::route() - /// .guard(guard::Any(guard::Get()).or(guard::Put())) - /// .guard(guard::Header("Content-Type", "text/plain")) - /// .to(|| HttpResponse::Ok())) - /// }) - /// }); - /// } - /// ``` - pub fn resource(mut self, path: &str, f: F) -> Self - where - F: FnOnce(Resource

    ) -> Resource, - U: NewService< - ServiceRequest

    , - Response = ServiceResponse, - Error = (), - InitError = (), - > + 'static, - { - // add resource - let rdef = ResourceDef::new(&insert_slash(path)); - let resource = f(Resource::new()); - self.defaults.push(resource.get_default()); - self.services.push(( - rdef, - boxed::new_service(resource.into_new_service()), - None, - )); - self + pub fn route(self, path: &str, mut route: Route

    ) -> Self { + self.service( + Resource::new(path) + .add_guards(route.take_guards()) + .route(route), + ) } /// Default resource to be used if no matching route could be found. @@ -227,7 +181,7 @@ where { // 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(Resource::new("")).into_new_service().map_init_err(|_| ()), ))))); self @@ -267,62 +221,53 @@ where guards: self.guards, services: self.services, default: self.default, - defaults: self.defaults, factory_ref: self.factory_ref, } } - - pub(crate) fn get_default(&self) -> Rc>>>> { - self.default.clone() - } - - pub(crate) fn take_guards(&mut self) -> Option>> { - if self.guards.is_empty() { - None - } else { - Some(std::mem::replace(&mut self.guards, Vec::new())) - } - } } -pub(crate) fn insert_slash(path: &str) -> String { - let mut path = path.to_owned(); - if !path.is_empty() && !path.starts_with('/') { - path.insert(0, '/'); - }; - path -} - -impl IntoNewService> for Scope +impl HttpServiceFactory

    for Scope where + P: 'static, T: NewService< - ServiceRequest

    , - Response = ServiceResponse, - Error = (), - InitError = (), - >, + ServiceRequest

    , + Response = ServiceResponse, + Error = (), + InitError = (), + > + 'static, { - fn into_new_service(self) -> T { - // update resource default service - if let Some(ref d) = *self.default.as_ref().borrow() { - for default in &self.defaults { - if default.borrow_mut().is_none() { - *default.borrow_mut() = Some(d.clone()); - } - } + fn register(self, config: &mut AppConfig

    ) { + if self.default.borrow().is_none() { + *self.default.borrow_mut() = Some(config.default_service()); } + // register services + let mut cfg = config.clone_config(); + self.services + .into_iter() + .for_each(|mut srv| srv.register(&mut cfg)); + *self.factory_ref.borrow_mut() = Some(ScopeFactory { default: self.default.clone(), services: Rc::new( - self.services + cfg.into_services() .into_iter() .map(|(rdef, srv, guards)| (rdef, srv, RefCell::new(guards))) .collect(), ), }); - self.endpoint + let guards = if self.guards.is_empty() { + None + } else { + Some(self.guards) + }; + let rdef = if config.is_root() { + ResourceDef::prefix(&insert_slash(&self.rdef)) + } else { + ResourceDef::prefix(&insert_slash(&self.rdef)) + }; + config.register_service(rdef, guards, self.endpoint) } } @@ -504,19 +449,22 @@ impl NewService> for ScopeEndpoint

    { #[cfg(test)] mod tests { - use actix_http::body::{Body, ResponseBody}; - use actix_http::http::{Method, StatusCode}; - use actix_service::{IntoNewService, NewService, Service}; + use actix_service::Service; use bytes::Bytes; - use crate::test::{self, block_on, TestRequest}; + use crate::body::{Body, ResponseBody}; + use crate::http::{Method, StatusCode}; + use crate::test::{block_on, init_service, TestRequest}; use crate::{guard, web, App, HttpRequest, HttpResponse}; #[test] fn test_scope() { - let mut srv = test::init_service(App::new().scope("/app", |scope| { - scope.resource("/path1", |r| r.to(|| HttpResponse::Ok())) - })); + let mut srv = init_service( + App::new().service( + web::scope("/app") + .service(web::resource("/path1").to(|| HttpResponse::Ok())), + ), + ); let req = TestRequest::with_uri("/app/path1").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -525,11 +473,13 @@ mod tests { #[test] fn test_scope_root() { - let mut srv = test::init_service(App::new().scope("/app", |scope| { - scope - .resource("", |r| r.to(|| HttpResponse::Ok())) - .resource("/", |r| r.to(|| HttpResponse::Created())) - })); + let mut srv = init_service( + App::new().service( + web::scope("/app") + .service(web::resource("").to(|| HttpResponse::Ok())) + .service(web::resource("/").to(|| HttpResponse::Created())), + ), + ); let req = TestRequest::with_uri("/app").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -542,9 +492,9 @@ mod tests { #[test] fn test_scope_root2() { - let mut srv = test::init_service(App::new().scope("/app/", |scope| { - scope.resource("", |r| r.to(|| HttpResponse::Ok())) - })); + let mut srv = init_service(App::new().service( + web::scope("/app/").service(web::resource("").to(|| HttpResponse::Ok())), + )); let req = TestRequest::with_uri("/app").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -557,12 +507,9 @@ mod tests { #[test] fn test_scope_root3() { - let app = App::new() - .scope("/app/", |scope| { - scope.resource("/", |r| r.to(|| HttpResponse::Ok())) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = init_service(App::new().service( + web::scope("/app/").service(web::resource("/").to(|| HttpResponse::Ok())), + )); let req = TestRequest::with_uri("/app").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -575,15 +522,13 @@ mod tests { #[test] fn test_scope_route() { - let app = App::new() - .scope("app", |scope| { - scope.resource("/path1", |r| { - r.route(web::get().to(|| HttpResponse::Ok())) - .route(web::delete().to(|| HttpResponse::Ok())) - }) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = init_service( + App::new().service( + web::scope("app") + .route("/path1", web::get().to(|| HttpResponse::Ok())) + .route("/path1", web::delete().to(|| HttpResponse::Ok())), + ), + ); let req = TestRequest::with_uri("/app/path1").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -604,15 +549,15 @@ mod tests { #[test] fn test_scope_route_without_leading_slash() { - let app = App::new() - .scope("app", |scope| { - scope.resource("path1", |r| { - r.route(web::get().to(|| HttpResponse::Ok())) - .route(web::delete().to(|| HttpResponse::Ok())) - }) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = init_service( + App::new().service( + web::scope("app").service( + web::resource("path1") + .route(web::get().to(|| HttpResponse::Ok())) + .route(web::delete().to(|| HttpResponse::Ok())), + ), + ), + ); let req = TestRequest::with_uri("/app/path1").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -633,14 +578,13 @@ mod tests { #[test] fn test_scope_guard() { - let app = App::new() - .scope("/app", |scope| { - scope + let mut srv = init_service( + App::new().service( + web::scope("/app") .guard(guard::Get()) - .resource("/path1", |r| r.to(|| HttpResponse::Ok())) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + .service(web::resource("/path1").to(|| HttpResponse::Ok())), + ), + ); let req = TestRequest::with_uri("/app/path1") .method(Method::POST) @@ -657,17 +601,13 @@ mod tests { #[test] fn test_scope_variable_segment() { - let app = App::new() - .scope("/ab-{project}", |scope| { - scope.resource("/path1", |r| { - r.to(|r: HttpRequest| { - HttpResponse::Ok() - .body(format!("project: {}", &r.match_info()["project"])) - }) - }) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = + init_service(App::new().service(web::scope("/ab-{project}").service( + web::resource("/path1").to(|r: HttpRequest| { + HttpResponse::Ok() + .body(format!("project: {}", &r.match_info()["project"])) + }), + ))); let req = TestRequest::with_uri("/ab-project1/path1").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -688,14 +628,14 @@ mod tests { #[test] fn test_nested_scope() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/t1", |scope| { - scope.resource("/path1", |r| r.to(|| HttpResponse::Created())) - }) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = init_service( + App::new().service( + web::scope("/app") + .service(web::scope("/t1").service( + web::resource("/path1").to(|| HttpResponse::Created()), + )), + ), + ); let req = TestRequest::with_uri("/app/t1/path1").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -704,14 +644,14 @@ mod tests { #[test] fn test_nested_scope_no_slash() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("t1", |scope| { - scope.resource("/path1", |r| r.to(|| HttpResponse::Created())) - }) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = init_service( + App::new().service( + web::scope("/app") + .service(web::scope("t1").service( + web::resource("/path1").to(|| HttpResponse::Created()), + )), + ), + ); let req = TestRequest::with_uri("/app/t1/path1").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -720,16 +660,15 @@ mod tests { #[test] fn test_nested_scope_root() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/t1", |scope| { - scope - .resource("", |r| r.to(|| HttpResponse::Ok())) - .resource("/", |r| r.to(|| HttpResponse::Created())) - }) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = init_service( + App::new().service( + web::scope("/app").service( + web::scope("/t1") + .service(web::resource("").to(|| HttpResponse::Ok())) + .service(web::resource("/").to(|| HttpResponse::Created())), + ), + ), + ); let req = TestRequest::with_uri("/app/t1").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -742,16 +681,15 @@ mod tests { #[test] fn test_nested_scope_filter() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/t1", |scope| { - scope + let mut srv = init_service( + App::new().service( + web::scope("/app").service( + web::scope("/t1") .guard(guard::Get()) - .resource("/path1", |r| r.to(|| HttpResponse::Ok())) - }) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + .service(web::resource("/path1").to(|| HttpResponse::Ok())), + ), + ), + ); let req = TestRequest::with_uri("/app/t1/path1") .method(Method::POST) @@ -768,21 +706,14 @@ mod tests { #[test] fn test_nested_scope_with_variable_segment() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/{project_id}", |scope| { - scope.resource("/path1", |r| { - r.to(|r: HttpRequest| { - HttpResponse::Created().body(format!( - "project: {}", - &r.match_info()["project_id"] - )) - }) - }) - }) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = init_service(App::new().service(web::scope("/app").service( + web::scope("/{project_id}").service(web::resource("/path1").to( + |r: HttpRequest| { + HttpResponse::Created() + .body(format!("project: {}", &r.match_info()["project_id"])) + }, + )), + ))); let req = TestRequest::with_uri("/app/project_1/path1").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -799,24 +730,17 @@ mod tests { #[test] fn test_nested2_scope_with_variable_segment() { - let app = App::new() - .scope("/app", |scope| { - scope.nested("/{project}", |scope| { - scope.nested("/{id}", |scope| { - scope.resource("/path1", |r| { - r.to(|r: HttpRequest| { - HttpResponse::Created().body(format!( - "project: {} - {}", - &r.match_info()["project"], - &r.match_info()["id"], - )) - }) - }) - }) - }) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = init_service(App::new().service(web::scope("/app").service( + web::scope("/{project}").service(web::scope("/{id}").service( + web::resource("/path1").to(|r: HttpRequest| { + HttpResponse::Created().body(format!( + "project: {} - {}", + &r.match_info()["project"], + &r.match_info()["id"], + )) + }), + )), + ))); let req = TestRequest::with_uri("/app/test/1/path1").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -837,14 +761,13 @@ mod tests { #[test] fn test_default_resource() { - let app = App::new() - .scope("/app", |scope| { - scope - .resource("/path1", |r| r.to(|| HttpResponse::Ok())) - .default_resource(|r| r.to(|| HttpResponse::BadRequest())) - }) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = init_service( + App::new().service( + web::scope("/app") + .service(web::resource("/path1").to(|| HttpResponse::Ok())) + .default_resource(|r| r.to(|| HttpResponse::BadRequest())), + ), + ); let req = TestRequest::with_uri("/app/path2").to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -857,14 +780,15 @@ mod tests { #[test] fn test_default_resource_propagation() { - let app = App::new() - .scope("/app1", |scope| { - scope.default_resource(|r| r.to(|| HttpResponse::BadRequest())) - }) - .scope("/app2", |scope| scope) - .default_resource(|r| r.to(|| HttpResponse::MethodNotAllowed())) - .into_new_service(); - let mut srv = block_on(app.new_service(&())).unwrap(); + let mut srv = init_service( + App::new() + .service( + web::scope("/app1") + .default_resource(|r| r.to(|| HttpResponse::BadRequest())), + ) + .service(web::scope("/app2")) + .default_resource(|r| r.to(|| HttpResponse::MethodNotAllowed())), + ); let req = TestRequest::with_uri("/non-exist").to_request(); let resp = block_on(srv.call(req)).unwrap(); diff --git a/src/server.rs b/src/server.rs index 78ba692e..c28cb280 100644 --- a/src/server.rs +++ b/src/server.rs @@ -33,20 +33,19 @@ struct Config { /// /// ```rust /// use std::io; -/// use actix_web::{App, HttpResponse, HttpServer}; +/// use actix_web::{web, App, HttpResponse, HttpServer}; /// /// fn main() -> io::Result<()> { /// let sys = actix_rt::System::new("example"); // <- create Actix runtime /// /// HttpServer::new( /// || App::new() -/// .resource("/", |r| r.to(|| HttpResponse::Ok()))) +/// .service(web::resource("/").to(|| HttpResponse::Ok()))) /// .bind("127.0.0.1:59090")? /// .start(); /// /// # actix_rt::System::current().stop(); -/// sys.run(); -/// Ok(()) +/// sys.run() /// } /// ``` pub struct HttpServer @@ -448,17 +447,17 @@ where /// configured. /// /// ```rust - /// use actix_web::{App, HttpResponse, HttpServer}; + /// use std::io; + /// use actix_web::{web, App, HttpResponse, HttpServer}; /// - /// fn main() { + /// fn main() -> io::Result<()> { /// let sys = actix_rt::System::new("example"); // <- create Actix system /// - /// HttpServer::new(|| App::new().resource("/", |r| r.to(|| HttpResponse::Ok()))) - /// .bind("127.0.0.1:0") - /// .expect("Can not bind to 127.0.0.1:0") + /// HttpServer::new(|| App::new().service(web::resource("/").to(|| HttpResponse::Ok()))) + /// .bind("127.0.0.1:0")? /// .start(); /// # actix_rt::System::current().stop(); - /// sys.run(); // <- Run actix system, this method starts all async processes + /// sys.run() // <- Run actix system, this method starts all async processes /// } /// ``` pub fn start(mut self) -> Server { @@ -472,20 +471,23 @@ where /// /// This methods panics if no socket addresses get bound. /// - /// ```rust,ignore - /// use actix_web::{App, HttpResponse, HttpServer}; + /// ```rust + /// use std::io; + /// use actix_web::{web, App, HttpResponse, HttpServer}; /// - /// fn main() { - /// HttpServer::new(|| App::new().resource("/", |r| r.to(|| HttpResponse::Ok()))) - /// .bind("127.0.0.1:0") - /// .expect("Can not bind to 127.0.0.1:0") - /// .run(); + /// fn main() -> io::Result<()> { + /// # std::thread::spawn(|| { + /// HttpServer::new(|| App::new().service(web::resource("/").to(|| HttpResponse::Ok()))) + /// .bind("127.0.0.1:0")? + /// .run() + /// # }); + /// # Ok(()) /// } /// ``` - pub fn run(self) { + pub fn run(self) -> io::Result<()> { let sys = System::new("http-server"); self.start(); - sys.run(); + sys.run() } } diff --git a/src/service.rs b/src/service.rs index c9666e31..e2213060 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use std::cell::{Ref, RefMut}; use std::fmt; +use std::marker::PhantomData; use std::rc::Rc; use actix_http::body::{Body, MessageBody, ResponseBody}; @@ -12,8 +13,42 @@ use actix_http::{ use actix_router::{Path, Resource, Url}; use futures::future::{ok, FutureResult, IntoFuture}; +use crate::config::AppConfig; use crate::request::HttpRequest; +pub trait HttpServiceFactory

    { + fn register(self, config: &mut AppConfig

    ); +} + +pub(crate) trait ServiceFactory

    { + fn register(&mut self, config: &mut AppConfig

    ); +} + +pub(crate) struct ServiceFactoryWrapper { + factory: Option, + _t: PhantomData

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

    for ServiceFactoryWrapper +where + T: HttpServiceFactory

    , +{ + fn register(&mut self, config: &mut AppConfig

    ) { + if let Some(item) = self.factory.take() { + item.register(config) + } + } +} + pub struct ServiceRequest

    { req: HttpRequest, payload: Payload

    , diff --git a/tests/test_server.rs b/tests/test_server.rs index d28f207c..ebe968fa 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -40,7 +40,8 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ fn test_body() { let mut srv = TestServer::new(|| { h1::H1Service::new( - App::new().resource("/", |r| r.route(web::to(|| Response::Ok().body(STR)))), + App::new() + .service(web::resource("/").route(web::to(|| Response::Ok().body(STR)))), ) }); @@ -59,7 +60,7 @@ fn test_body_gzip() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Gzip)) - .resource("/", |r| r.route(web::to(|| Response::Ok().body(STR)))), + .service(web::resource("/").route(web::to(|| Response::Ok().body(STR)))), ) }); @@ -87,9 +88,10 @@ fn test_body_gzip_large() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Gzip)) - .resource("/", |r| { - r.route(web::to(move || Response::Ok().body(data.clone()))) - }), + .service( + web::resource("/") + .route(web::to(move || Response::Ok().body(data.clone()))), + ), ) }); @@ -120,9 +122,10 @@ fn test_body_gzip_large_random() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Gzip)) - .resource("/", |r| { - r.route(web::to(move || Response::Ok().body(data.clone()))) - }), + .service( + web::resource("/") + .route(web::to(move || Response::Ok().body(data.clone()))), + ), ) }); @@ -147,13 +150,11 @@ fn test_body_chunked_implicit() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Gzip)) - .resource("/", |r| { - r.route(web::get().to(move || { - Response::Ok().streaming(once(Ok::<_, Error>( - Bytes::from_static(STR.as_ref()), - ))) - })) - }), + .service(web::resource("/").route(web::get().to(move || { + Response::Ok().streaming(once(Ok::<_, Error>(Bytes::from_static( + STR.as_ref(), + )))) + }))), ) }); @@ -181,13 +182,11 @@ fn test_body_br_streaming() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Br)) - .resource("/", |r| { - r.route(web::to(move || { - Response::Ok().streaming(once(Ok::<_, Error>( - Bytes::from_static(STR.as_ref()), - ))) - })) - }), + .service(web::resource("/").route(web::to(move || { + Response::Ok().streaming(once(Ok::<_, Error>(Bytes::from_static( + STR.as_ref(), + )))) + }))), ) }); @@ -208,9 +207,9 @@ fn test_body_br_streaming() { #[test] fn test_head_binary() { let mut srv = TestServer::new(move || { - h1::H1Service::new(App::new().resource("/", |r| { - r.route(web::head().to(move || Response::Ok().content_length(100).body(STR))) - })) + h1::H1Service::new(App::new().service(web::resource("/").route( + web::head().to(move || Response::Ok().content_length(100).body(STR)), + ))) }); let request = srv.head().finish().unwrap(); @@ -230,14 +229,14 @@ fn test_head_binary() { #[test] fn test_no_chunking() { let mut srv = TestServer::new(move || { - h1::H1Service::new(App::new().resource("/", |r| { - r.route(web::to(move || { + h1::H1Service::new(App::new().service(web::resource("/").route(web::to( + move || { Response::Ok() .no_chunking() .content_length(STR.len() as u64) .streaming(once(Ok::<_, Error>(Bytes::from_static(STR.as_ref())))) - })) - })) + }, + )))) }); let request = srv.get().finish().unwrap(); @@ -256,7 +255,9 @@ fn test_body_deflate() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Deflate)) - .resource("/", |r| r.route(web::to(move || Response::Ok().body(STR)))), + .service( + web::resource("/").route(web::to(move || Response::Ok().body(STR))), + ), ) }); @@ -281,7 +282,9 @@ fn test_body_brotli() { h1::H1Service::new( App::new() .middleware(middleware::Compress::new(ContentEncoding::Br)) - .resource("/", |r| r.route(web::to(move || Response::Ok().body(STR)))), + .service( + web::resource("/").route(web::to(move || Response::Ok().body(STR))), + ), ) }); From 561a89b8b3aa84fb1993c9f7380d8dc077972cef Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 17:32:41 -0800 Subject: [PATCH 045/109] copy logger --- logger.rs | 384 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 logger.rs diff --git a/logger.rs b/logger.rs new file mode 100644 index 00000000..b7bb1bb8 --- /dev/null +++ b/logger.rs @@ -0,0 +1,384 @@ +//! Request logging middleware +use std::collections::HashSet; +use std::env; +use std::fmt::{self, Display, Formatter}; + +use regex::Regex; +use time; + +use error::Result; +use httpmessage::HttpMessage; +use httprequest::HttpRequest; +use httpresponse::HttpResponse; +use middleware::{Finished, Middleware, Started}; + +/// `Middleware` for logging request and response info to the terminal. +/// +/// `Logger` middleware uses standard log crate to log information. You should +/// enable logger for `actix_web` package to see access log. +/// ([`env_logger`](https://docs.rs/env_logger/*/env_logger/) or similar) +/// +/// ## Usage +/// +/// Create `Logger` middleware with the specified `format`. +/// Default `Logger` could be created with `default` method, it uses the +/// default format: +/// +/// ```ignore +/// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T +/// ``` +/// ```rust +/// # extern crate actix_web; +/// extern crate env_logger; +/// use actix_web::middleware::Logger; +/// use actix_web::App; +/// +/// fn main() { +/// std::env::set_var("RUST_LOG", "actix_web=info"); +/// env_logger::init(); +/// +/// let app = App::new() +/// .middleware(Logger::default()) +/// .middleware(Logger::new("%a %{User-Agent}i")) +/// .finish(); +/// } +/// ``` +/// +/// ## Format +/// +/// `%%` The percent sign +/// +/// `%a` Remote IP-address (IP-address of proxy if using reverse proxy) +/// +/// `%t` Time when the request was started to process +/// +/// `%r` First line of request +/// +/// `%s` Response status code +/// +/// `%b` Size of response in bytes, including HTTP headers +/// +/// `%T` Time taken to serve the request, in seconds with floating fraction in +/// .06f format +/// +/// `%D` Time taken to serve the request, in milliseconds +/// +/// `%{FOO}i` request.headers['FOO'] +/// +/// `%{FOO}o` response.headers['FOO'] +/// +/// `%{FOO}e` os.environ['FOO'] +/// +pub struct Logger { + format: Format, + exclude: HashSet, +} + +impl Logger { + /// Create `Logger` middleware with the specified `format`. + pub fn new(format: &str) -> Logger { + Logger { + format: Format::new(format), + exclude: HashSet::new(), + } + } + + /// Ignore and do not log access info for specified path. + pub fn exclude>(mut self, path: T) -> Self { + self.exclude.insert(path.into()); + self + } +} + +impl Default for Logger { + /// Create `Logger` middleware with format: + /// + /// ```ignore + /// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T + /// ``` + fn default() -> Logger { + Logger { + format: Format::default(), + exclude: HashSet::new(), + } + } +} + +struct StartTime(time::Tm); + +impl Logger { + fn log(&self, req: &HttpRequest, resp: &HttpResponse) { + if let Some(entry_time) = req.extensions().get::() { + let render = |fmt: &mut Formatter| { + for unit in &self.format.0 { + unit.render(fmt, req, resp, entry_time.0)?; + } + Ok(()) + }; + info!("{}", FormatDisplay(&render)); + } + } +} + +impl Middleware for Logger { + fn start(&self, req: &HttpRequest) -> Result { + if !self.exclude.contains(req.path()) { + req.extensions_mut().insert(StartTime(time::now())); + } + Ok(Started::Done) + } + + fn finish(&self, req: &HttpRequest, resp: &HttpResponse) -> Finished { + self.log(req, resp); + Finished::Done + } +} + +/// A formatting style for the `Logger`, consisting of multiple +/// `FormatText`s concatenated into one line. +#[derive(Clone)] +#[doc(hidden)] +struct Format(Vec); + +impl Default for Format { + /// Return the default formatting style for the `Logger`: + fn default() -> Format { + Format::new(r#"%a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T"#) + } +} + +impl Format { + /// Create a `Format` from a format string. + /// + /// Returns `None` if the format string syntax is incorrect. + pub fn new(s: &str) -> Format { + trace!("Access log format: {}", s); + let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([ioe])|[atPrsbTD]?)").unwrap(); + + let mut idx = 0; + let mut results = Vec::new(); + for cap in fmt.captures_iter(s) { + let m = cap.get(0).unwrap(); + let pos = m.start(); + if idx != pos { + results.push(FormatText::Str(s[idx..pos].to_owned())); + } + idx = m.end(); + + 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()), + "e" => FormatText::EnvironHeader(key.as_str().to_owned()), + _ => unreachable!(), + }) + } else { + let m = cap.get(1).unwrap(); + results.push(match m.as_str() { + "%" => FormatText::Percent, + "a" => FormatText::RemoteAddr, + "t" => FormatText::RequestTime, + "r" => FormatText::RequestLine, + "s" => FormatText::ResponseStatus, + "b" => FormatText::ResponseSize, + "T" => FormatText::Time, + "D" => FormatText::TimeMillis, + _ => FormatText::Str(m.as_str().to_owned()), + }); + } + } + if idx != s.len() { + results.push(FormatText::Str(s[idx..].to_owned())); + } + + Format(results) + } +} + +/// A string of text to be logged. This is either one of the data +/// fields supported by the `Logger`, or a custom `String`. +#[doc(hidden)] +#[derive(Debug, Clone)] +pub enum FormatText { + Str(String), + Percent, + RequestLine, + RequestTime, + ResponseStatus, + ResponseSize, + Time, + TimeMillis, + RemoteAddr, + RequestHeader(String), + ResponseHeader(String), + EnvironHeader(String), +} + +impl FormatText { + fn render( + &self, fmt: &mut Formatter, req: &HttpRequest, resp: &HttpResponse, + entry_time: time::Tm, + ) -> Result<(), fmt::Error> { + match *self { + FormatText::Str(ref string) => fmt.write_str(string), + FormatText::Percent => "%".fmt(fmt), + FormatText::RequestLine => { + if req.query_string().is_empty() { + fmt.write_fmt(format_args!( + "{} {} {:?}", + req.method(), + req.path(), + req.version() + )) + } else { + fmt.write_fmt(format_args!( + "{} {}?{} {:?}", + req.method(), + req.path(), + req.query_string(), + req.version() + )) + } + } + FormatText::ResponseStatus => resp.status().as_u16().fmt(fmt), + FormatText::ResponseSize => resp.response_size().fmt(fmt), + FormatText::Time => { + let rt = time::now() - entry_time; + let rt = (rt.num_nanoseconds().unwrap_or(0) as f64) / 1_000_000_000.0; + fmt.write_fmt(format_args!("{:.6}", rt)) + } + FormatText::TimeMillis => { + let rt = time::now() - entry_time; + 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::RequestTime => entry_time + .strftime("[%d/%b/%Y:%H:%M:%S %z]") + .unwrap() + .fmt(fmt), + FormatText::RequestHeader(ref name) => { + let s = if let Some(val) = req.headers().get(name) { + if let Ok(s) = val.to_str() { + s + } else { + "-" + } + } else { + "-" + }; + fmt.write_fmt(format_args!("{}", s)) + } + FormatText::ResponseHeader(ref name) => { + let s = if let Some(val) = resp.headers().get(name) { + if let Ok(s) = val.to_str() { + s + } else { + "-" + } + } else { + "-" + }; + fmt.write_fmt(format_args!("{}", s)) + } + FormatText::EnvironHeader(ref name) => { + if let Ok(val) = env::var(name) { + fmt.write_fmt(format_args!("{}", val)) + } else { + "-".fmt(fmt) + } + } + } + } +} + +pub(crate) struct FormatDisplay<'a>(&'a Fn(&mut Formatter) -> Result<(), fmt::Error>); + +impl<'a> fmt::Display for FormatDisplay<'a> { + fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> { + (self.0)(fmt) + } +} + +#[cfg(test)] +mod tests { + use time; + + use super::*; + use http::{header, StatusCode}; + use test::TestRequest; + + #[test] + fn test_logger() { + let logger = Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test"); + + let req = TestRequest::with_header( + header::USER_AGENT, + header::HeaderValue::from_static("ACTIX-WEB"), + ).finish(); + let resp = HttpResponse::build(StatusCode::OK) + .header("X-Test", "ttt") + .force_close() + .finish(); + + match logger.start(&req) { + Ok(Started::Done) => (), + _ => panic!(), + }; + match logger.finish(&req, &resp) { + Finished::Done => (), + _ => panic!(), + } + let entry_time = time::now(); + let render = |fmt: &mut Formatter| { + for unit in &logger.format.0 { + unit.render(fmt, &req, &resp, entry_time)?; + } + Ok(()) + }; + let s = format!("{}", FormatDisplay(&render)); + assert!(s.contains("ACTIX-WEB ttt")); + } + + #[test] + fn test_default_format() { + let format = Format::default(); + + let req = TestRequest::with_header( + header::USER_AGENT, + header::HeaderValue::from_static("ACTIX-WEB"), + ).finish(); + let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); + let entry_time = time::now(); + + let render = |fmt: &mut Formatter| { + for unit in &format.0 { + unit.render(fmt, &req, &resp, entry_time)?; + } + Ok(()) + }; + let s = format!("{}", FormatDisplay(&render)); + assert!(s.contains("GET / HTTP/1.1")); + assert!(s.contains("200 0")); + assert!(s.contains("ACTIX-WEB")); + + let req = TestRequest::with_uri("/?test").finish(); + let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); + let entry_time = time::now(); + + let render = |fmt: &mut Formatter| { + for unit in &format.0 { + unit.render(fmt, &req, &resp, entry_time)?; + } + Ok(()) + }; + let s = format!("{}", FormatDisplay(&render)); + assert!(s.contains("GET /?test HTTP/1.1")); + } +} From 244fff9e0af6284e92cc38417f1d295c8e20d5aa Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 19:19:27 -0800 Subject: [PATCH 046/109] added Logger middleware --- Cargo.toml | 2 + src/app.rs | 4 +- src/lib.rs | 2 +- src/middleware/defaultheaders.rs | 6 +- logger.rs => src/middleware/logger.rs | 399 +++++++++++++++++--------- src/middleware/mod.rs | 3 + src/resource.rs | 2 +- src/route.rs | 5 +- src/scope.rs | 6 +- 9 files changed, 279 insertions(+), 150 deletions(-) rename logger.rs => src/middleware/logger.rs (52%) diff --git a/Cargo.toml b/Cargo.toml index a17669d1..2abb4c72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,10 +81,12 @@ mime = "0.3" net2 = "0.2.33" num_cpus = "1.10" parking_lot = "0.7" +regex = "1.0" serde = "1.0" serde_json = "1.0" serde_urlencoded = "^0.5.3" threadpool = "1.7" +time = "0.1" # middlewares # actix-session = { path="session", optional = true } diff --git a/src/app.rs b/src/app.rs index 7c194d27..f62c064a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -451,7 +451,7 @@ where // set factory *self.factory_ref.borrow_mut() = Some(AppRoutingFactory { - default: default, + default, services: Rc::new( config .into_services() @@ -784,7 +784,7 @@ where mod tests { use super::*; use crate::http::{Method, StatusCode}; - use crate::test::{self, block_on, init_service, TestRequest}; + use crate::test::{block_on, init_service, TestRequest}; use crate::{web, HttpResponse, State}; #[test] diff --git a/src/lib.rs b/src/lib.rs index 3400fe29..fd1b21f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,7 +48,7 @@ pub mod dev { pub use crate::config::AppConfig; pub use crate::service::HttpServiceFactory; - pub use actix_http::body::{Body, MessageBody, ResponseBody}; + pub use actix_http::body::{Body, BodyLength, MessageBody, ResponseBody}; pub use actix_http::dev::ResponseBuilder as HttpResponseBuilder; pub use actix_http::{ Extensions, Payload, PayloadStream, RequestHead, ResponseHead, diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index 137913d2..f4def58d 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -1,12 +1,12 @@ //! Middleware for setting default response headers use std::rc::Rc; -use actix_http::http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; -use actix_http::http::{HeaderMap, HttpTryFrom}; use actix_service::{Service, Transform}; use futures::future::{ok, FutureResult}; use futures::{Future, Poll}; +use crate::http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; +use crate::http::{HeaderMap, HttpTryFrom}; use crate::service::{ServiceRequest, ServiceResponse}; /// `Middleware` for setting default response headers. @@ -147,10 +147,10 @@ where #[cfg(test)] mod tests { - use actix_http::http::header::CONTENT_TYPE; use actix_service::FnService; use super::*; + use crate::http::header::CONTENT_TYPE; use crate::test::{block_on, TestRequest}; use crate::{HttpResponse, ServiceRequest}; diff --git a/logger.rs b/src/middleware/logger.rs similarity index 52% rename from logger.rs rename to src/middleware/logger.rs index b7bb1bb8..769d8428 100644 --- a/logger.rs +++ b/src/middleware/logger.rs @@ -2,15 +2,19 @@ use std::collections::HashSet; use std::env; use std::fmt::{self, Display, Formatter}; +use std::rc::Rc; +use actix_service::{Service, Transform}; +use bytes::Bytes; +use futures::future::{ok, FutureResult}; +use futures::{Async, Future, Poll}; use regex::Regex; use time; -use error::Result; -use httpmessage::HttpMessage; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Finished, Middleware, Started}; +use crate::dev::{BodyLength, MessageBody, ResponseBody}; +use crate::error::{Error, Result}; +use crate::service::{ServiceRequest, ServiceResponse}; +use crate::{HttpMessage, HttpResponse}; /// `Middleware` for logging request and response info to the terminal. /// @@ -69,7 +73,9 @@ use middleware::{Finished, Middleware, Started}; /// /// `%{FOO}e` os.environ['FOO'] /// -pub struct Logger { +pub struct Logger(Rc); + +struct Inner { format: Format, exclude: HashSet, } @@ -77,15 +83,18 @@ pub struct Logger { impl Logger { /// Create `Logger` middleware with the specified `format`. pub fn new(format: &str) -> Logger { - Logger { + Logger(Rc::new(Inner { format: Format::new(format), exclude: HashSet::new(), - } + })) } /// Ignore and do not log access info for specified path. pub fn exclude>(mut self, path: T) -> Self { - self.exclude.insert(path.into()); + Rc::get_mut(&mut self.0) + .unwrap() + .exclude + .insert(path.into()); self } } @@ -97,40 +106,147 @@ impl Default for Logger { /// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T /// ``` fn default() -> Logger { - Logger { + Logger(Rc::new(Inner { format: Format::default(), exclude: HashSet::new(), + })) + } +} + +impl Transform> for Logger +where + S: Service, Response = ServiceResponse>, + B: MessageBody, +{ + type Response = ServiceResponse>; + type Error = S::Error; + type InitError = (); + type Transform = LoggerMiddleware; + type Future = FutureResult; + + fn new_transform(&self, service: S) -> Self::Future { + ok(LoggerMiddleware { + service, + inner: self.0.clone(), + }) + } +} + +/// Logger middleware +pub struct LoggerMiddleware { + inner: Rc, + service: S, +} + +impl Service> for LoggerMiddleware +where + S: Service, Response = ServiceResponse>, + B: MessageBody, +{ + type Response = ServiceResponse>; + type Error = S::Error; + type Future = LoggerResponse; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.service.poll_ready() + } + + fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { + if self.inner.exclude.contains(req.path()) { + LoggerResponse { + fut: self.service.call(req), + format: None, + time: time::now(), + } + } else { + let now = time::now(); + let mut format = self.inner.format.clone(); + + for unit in &mut format.0 { + unit.render_request(now, &req); + } + LoggerResponse { + fut: self.service.call(req), + format: Some(format), + time: now, + } } } } -struct StartTime(time::Tm); +#[doc(hidden)] +pub struct LoggerResponse +where + B: MessageBody, + S: Service, Response = ServiceResponse>, +{ + fut: S::Future, + time: time::Tm, + format: Option, +} -impl Logger { - fn log(&self, req: &HttpRequest, resp: &HttpResponse) { - if let Some(entry_time) = req.extensions().get::() { +impl Future for LoggerResponse +where + B: MessageBody, + S: Service, Response = ServiceResponse>, +{ + type Item = ServiceResponse>; + type Error = S::Error; + + fn poll(&mut self) -> Poll { + let res = futures::try_ready!(self.fut.poll()); + + if let Some(ref mut format) = self.format { + for unit in &mut format.0 { + unit.render_response(&res); + } + } + + Ok(Async::Ready(res.map_body(move |_, body| { + ResponseBody::Body(StreamLog { + body, + size: 0, + time: self.time, + format: self.format.take(), + }) + }))) + } +} + +pub struct StreamLog { + body: ResponseBody, + format: Option, + size: usize, + time: time::Tm, +} + +impl Drop for StreamLog { + fn drop(&mut self) { + if let Some(ref format) = self.format { let render = |fmt: &mut Formatter| { - for unit in &self.format.0 { - unit.render(fmt, req, resp, entry_time.0)?; + for unit in &format.0 { + unit.render(fmt, self.size, self.time)?; } Ok(()) }; - info!("{}", FormatDisplay(&render)); + log::info!("{}", FormatDisplay(&render)); } } } -impl Middleware for Logger { - fn start(&self, req: &HttpRequest) -> Result { - if !self.exclude.contains(req.path()) { - req.extensions_mut().insert(StartTime(time::now())); - } - Ok(Started::Done) +impl MessageBody for StreamLog { + fn length(&self) -> BodyLength { + self.body.length() } - fn finish(&self, req: &HttpRequest, resp: &HttpResponse) -> Finished { - self.log(req, resp); - Finished::Done + fn poll_next(&mut self) -> Poll, Error> { + match self.body.poll_next()? { + Async::Ready(Some(chunk)) => { + self.size += chunk.len(); + Ok(Async::Ready(Some(chunk))) + } + val => Ok(val), + } } } @@ -152,7 +268,7 @@ impl Format { /// /// Returns `None` if the format string syntax is incorrect. pub fn new(s: &str) -> Format { - trace!("Access log format: {}", s); + log::trace!("Access log format: {}", s); let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([ioe])|[atPrsbTD]?)").unwrap(); let mut idx = 0; @@ -215,33 +331,16 @@ pub enum FormatText { } impl FormatText { - fn render( - &self, fmt: &mut Formatter, req: &HttpRequest, resp: &HttpResponse, + fn render( + &self, + fmt: &mut Formatter, + size: usize, entry_time: time::Tm, ) -> Result<(), fmt::Error> { match *self { FormatText::Str(ref string) => fmt.write_str(string), FormatText::Percent => "%".fmt(fmt), - FormatText::RequestLine => { - if req.query_string().is_empty() { - fmt.write_fmt(format_args!( - "{} {} {:?}", - req.method(), - req.path(), - req.version() - )) - } else { - fmt.write_fmt(format_args!( - "{} {}?{} {:?}", - req.method(), - req.path(), - req.query_string(), - req.version() - )) - } - } - FormatText::ResponseStatus => resp.status().as_u16().fmt(fmt), - FormatText::ResponseSize => resp.response_size().fmt(fmt), + FormatText::ResponseSize => size.fmt(fmt), FormatText::Time => { let rt = time::now() - entry_time; let rt = (rt.num_nanoseconds().unwrap_or(0) as f64) / 1_000_000_000.0; @@ -252,17 +351,71 @@ 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); + // 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)) } else { "-".fmt(fmt) } } - FormatText::RequestTime => entry_time - .strftime("[%d/%b/%Y:%H:%M:%S %z]") - .unwrap() - .fmt(fmt), + _ => Ok(()), + } + } + + fn render_response(&mut self, res: &HttpResponse) { + match *self { + FormatText::ResponseStatus => { + *self = FormatText::Str(format!("{}", res.status().as_u16())) + } + FormatText::ResponseHeader(ref name) => { + let s = if let Some(val) = res.headers().get(name) { + if let Ok(s) = val.to_str() { + s + } else { + "-" + } + } else { + "-" + }; + *self = FormatText::Str(s.to_string()) + } + _ => (), + } + } + + fn render_request

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

    ) { + match *self { + FormatText::RequestLine => { + *self = if req.query_string().is_empty() { + FormatText::Str(format!( + "{} {} {:?}", + req.method(), + req.path(), + req.version() + )) + } else { + FormatText::Str(format!( + "{} {}?{} {:?}", + req.method(), + req.path(), + req.query_string(), + req.version() + )) + }; + } + FormatText::RequestTime => { + *self = FormatText::Str(format!( + "{:?}", + now.strftime("[%d/%b/%Y:%H:%M:%S %z]").unwrap() + )) + } FormatText::RequestHeader(ref name) => { let s = if let Some(val) = req.headers().get(name) { if let Ok(s) = val.to_str() { @@ -273,27 +426,9 @@ impl FormatText { } else { "-" }; - fmt.write_fmt(format_args!("{}", s)) - } - FormatText::ResponseHeader(ref name) => { - let s = if let Some(val) = resp.headers().get(name) { - if let Ok(s) = val.to_str() { - s - } else { - "-" - } - } else { - "-" - }; - fmt.write_fmt(format_args!("{}", s)) - } - FormatText::EnvironHeader(ref name) => { - if let Ok(val) = env::var(name) { - fmt.write_fmt(format_args!("{}", val)) - } else { - "-".fmt(fmt) - } + *self = FormatText::Str(s.to_string()); } + _ => (), } } } @@ -308,77 +443,67 @@ impl<'a> fmt::Display for FormatDisplay<'a> { #[cfg(test)] mod tests { - use time; + use actix_service::{FnService, Service, Transform}; use super::*; - use http::{header, StatusCode}; - use test::TestRequest; + use crate::http::{header, StatusCode}; + use crate::test::{block_on, TestRequest}; #[test] fn test_logger() { + let srv = FnService::new(|req: ServiceRequest<_>| { + req.into_response( + HttpResponse::build(StatusCode::OK) + .header("X-Test", "ttt") + .finish(), + ) + }); let logger = Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test"); - let req = TestRequest::with_header( - header::USER_AGENT, - header::HeaderValue::from_static("ACTIX-WEB"), - ).finish(); - let resp = HttpResponse::build(StatusCode::OK) - .header("X-Test", "ttt") - .force_close() - .finish(); - - match logger.start(&req) { - Ok(Started::Done) => (), - _ => panic!(), - }; - match logger.finish(&req, &resp) { - Finished::Done => (), - _ => panic!(), - } - let entry_time = time::now(); - let render = |fmt: &mut Formatter| { - for unit in &logger.format.0 { - unit.render(fmt, &req, &resp, entry_time)?; - } - Ok(()) - }; - let s = format!("{}", FormatDisplay(&render)); - assert!(s.contains("ACTIX-WEB ttt")); - } - - #[test] - fn test_default_format() { - let format = Format::default(); + let mut srv = block_on(logger.new_transform(srv)).unwrap(); let req = TestRequest::with_header( header::USER_AGENT, header::HeaderValue::from_static("ACTIX-WEB"), - ).finish(); - let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); - let entry_time = time::now(); - - let render = |fmt: &mut Formatter| { - for unit in &format.0 { - unit.render(fmt, &req, &resp, entry_time)?; - } - Ok(()) - }; - let s = format!("{}", FormatDisplay(&render)); - assert!(s.contains("GET / HTTP/1.1")); - assert!(s.contains("200 0")); - assert!(s.contains("ACTIX-WEB")); - - let req = TestRequest::with_uri("/?test").finish(); - let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); - let entry_time = time::now(); - - let render = |fmt: &mut Formatter| { - for unit in &format.0 { - unit.render(fmt, &req, &resp, entry_time)?; - } - Ok(()) - }; - let s = format!("{}", FormatDisplay(&render)); - assert!(s.contains("GET /?test HTTP/1.1")); + ) + .to_service(); + let _res = block_on(srv.call(req)); } + + // #[test] + // fn test_default_format() { + // let format = Format::default(); + + // let req = TestRequest::with_header( + // header::USER_AGENT, + // header::HeaderValue::from_static("ACTIX-WEB"), + // ) + // .finish(); + // let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); + // let entry_time = time::now(); + + // let render = |fmt: &mut Formatter| { + // for unit in &format.0 { + // unit.render(fmt, &req, &resp, entry_time)?; + // } + // Ok(()) + // }; + // let s = format!("{}", FormatDisplay(&render)); + // assert!(s.contains("GET / HTTP/1.1")); + // assert!(s.contains("200 0")); + // assert!(s.contains("ACTIX-WEB")); + + // let req = TestRequest::with_uri("/?test").finish(); + // let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); + // let entry_time = time::now(); + + // let render = |fmt: &mut Formatter| { + // for unit in &format.0 { + // unit.render(fmt, &req, &resp, entry_time)?; + // } + // Ok(()) + // }; + // let s = format!("{}", FormatDisplay(&render)); + // assert!(s.contains("GET /?test HTTP/1.1")); + // } } diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 8c4cd754..d63ca893 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -8,3 +8,6 @@ pub use self::defaultheaders::DefaultHeaders; #[cfg(feature = "session")] pub use actix_session as session; + +mod logger; +pub use self::logger::Logger; diff --git a/src/resource.rs b/src/resource.rs index b5cf640c..ddcbbcd3 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -286,7 +286,7 @@ where let rdef = if config.is_root() { ResourceDef::new(&insert_slash(&self.rdef)) } else { - ResourceDef::new(&insert_slash(&self.rdef)) + ResourceDef::new(&self.rdef) }; config.register_service(rdef, guards, self) } diff --git a/src/route.rs b/src/route.rs index bac897ab..b611164e 100644 --- a/src/route.rs +++ b/src/route.rs @@ -50,9 +50,8 @@ impl Route

    { let config_ref = Rc::new(RefCell::new(None)); Route { service: Box::new(RouteNewService::new( - Extract::new(config_ref.clone()).and_then( - Handle::new(|| HttpResponse::NotFound()).map_err(|_| panic!()), - ), + Extract::new(config_ref.clone()) + .and_then(Handle::new(HttpResponse::NotFound).map_err(|_| panic!())), )), guards: Rc::new(Vec::new()), config: ConfigStorage::default(), diff --git a/src/scope.rs b/src/scope.rs index 9bc0b50d..29f44fc4 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -10,7 +10,7 @@ use actix_service::{ use futures::future::{ok, Either, Future, FutureResult}; use futures::{Async, Poll}; -use crate::dev::{insert_slash, AppConfig, HttpServiceFactory}; +use crate::dev::{AppConfig, HttpServiceFactory}; use crate::guard::Guard; use crate::resource::Resource; use crate::route::Route; @@ -263,9 +263,9 @@ where Some(self.guards) }; let rdef = if config.is_root() { - ResourceDef::prefix(&insert_slash(&self.rdef)) + ResourceDef::root_prefix(&self.rdef) } else { - ResourceDef::prefix(&insert_slash(&self.rdef)) + ResourceDef::prefix(&self.rdef) }; config.register_service(rdef, guards, self.endpoint) } From 60c048c8cd1e47e25aeb8973a941a58e9d8ee0e2 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 19:27:18 -0800 Subject: [PATCH 047/109] fix nested resources --- src/middleware/logger.rs | 5 +---- src/resource.rs | 2 +- src/scope.rs | 11 +++++------ 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index 769d8428..d8b4e643 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -32,8 +32,6 @@ use crate::{HttpMessage, HttpResponse}; /// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T /// ``` /// ```rust -/// # extern crate actix_web; -/// extern crate env_logger; /// use actix_web::middleware::Logger; /// use actix_web::App; /// @@ -43,8 +41,7 @@ use crate::{HttpMessage, HttpResponse}; /// /// let app = App::new() /// .middleware(Logger::default()) -/// .middleware(Logger::new("%a %{User-Agent}i")) -/// .finish(); +/// .middleware(Logger::new("%a %{User-Agent}i")); /// } /// ``` /// diff --git a/src/resource.rs b/src/resource.rs index ddcbbcd3..157b181e 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -283,7 +283,7 @@ where } else { Some(std::mem::replace(&mut self.guards, Vec::new())) }; - let rdef = if config.is_root() { + let rdef = if config.is_root() || !self.rdef.is_empty() { ResourceDef::new(&insert_slash(&self.rdef)) } else { ResourceDef::new(&self.rdef) diff --git a/src/scope.rs b/src/scope.rs index 29f44fc4..5580b15e 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -262,12 +262,11 @@ where } else { Some(self.guards) }; - let rdef = if config.is_root() { - ResourceDef::root_prefix(&self.rdef) - } else { - ResourceDef::prefix(&self.rdef) - }; - config.register_service(rdef, guards, self.endpoint) + config.register_service( + ResourceDef::root_prefix(&self.rdef), + guards, + self.endpoint, + ) } } From 6e638129c54a558cde24c34974de8443206f7517 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 23:06:14 -0800 Subject: [PATCH 048/109] use generic HttpService --- src/server.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/server.rs b/src/server.rs index c28cb280..d6d88d00 100644 --- a/src/server.rs +++ b/src/server.rs @@ -2,7 +2,9 @@ use std::marker::PhantomData; use std::sync::Arc; use std::{fmt, io, net}; -use actix_http::{body::MessageBody, h1, KeepAlive, Request, Response, ServiceConfig}; +use actix_http::{ + body::MessageBody, HttpService, KeepAlive, Request, Response, ServiceConfig, +}; use actix_rt::System; use actix_server::{Server, ServerBuilder}; use actix_service::{IntoNewService, NewService}; @@ -72,10 +74,10 @@ where F: Fn() -> I + Send + Clone + 'static, I: IntoNewService, S: NewService, - S::Error: fmt::Debug, + S::Error: fmt::Debug + 'static, S::Response: Into>, S::Service: 'static, - B: MessageBody, + B: MessageBody + 'static, { /// Create new http server with application factory pub fn new(factory: F) -> Self { @@ -244,7 +246,7 @@ where let c = cfg.lock(); let service_config = ServiceConfig::new(c.keep_alive, c.client_timeout, 0); - h1::H1Service::with_config(service_config, factory()) + HttpService::with_config(service_config, factory()) }, )); @@ -298,7 +300,7 @@ where let service_config = ServiceConfig::new(c.keep_alive, c.client_timeout, c.client_timeout); acceptor.clone().map_err(|e| SslError::Ssl(e)).and_then( - h1::H1Service::with_config(service_config, factory()) + HttpService::with_config(service_config, factory()) .map_err(|e| SslError::Service(e)) .map_init_err(|_| ()), ) From e56691bcf23b4a9b37dc42823562fef9e8f4c514 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 23:39:08 -0800 Subject: [PATCH 049/109] rename to Files --- Cargo.toml | 2 +- {actix-staticfiles => actix-files}/CHANGES.md | 0 {actix-staticfiles => actix-files}/Cargo.toml | 2 +- {actix-staticfiles => actix-files}/README.md | 0 .../src/config.rs | 0 .../src/error.rs | 6 +- {actix-staticfiles => actix-files}/src/lib.rs | 64 +++++++++--------- .../src/named.rs | 0 .../src/range.rs | 0 .../tests/test space.binary | 0 .../tests/test.binary | 0 .../tests/test.png | Bin 12 files changed, 37 insertions(+), 37 deletions(-) rename {actix-staticfiles => actix-files}/CHANGES.md (100%) rename {actix-staticfiles => actix-files}/Cargo.toml (97%) rename {actix-staticfiles => actix-files}/README.md (100%) rename {actix-staticfiles => actix-files}/src/config.rs (100%) rename {actix-staticfiles => actix-files}/src/error.rs (91%) rename {actix-staticfiles => actix-files}/src/lib.rs (94%) rename {actix-staticfiles => actix-files}/src/named.rs (100%) rename {actix-staticfiles => actix-files}/src/range.rs (100%) rename {actix-staticfiles => actix-files}/tests/test space.binary (100%) rename {actix-staticfiles => actix-files}/tests/test.binary (100%) rename {actix-staticfiles => actix-files}/tests/test.png (100%) diff --git a/Cargo.toml b/Cargo.toml index 2abb4c72..7b2e6c99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,8 @@ path = "src/lib.rs" [workspace] members = [ ".", + "actix-files", "actix-session", - "actix-staticfiles", ] [package.metadata.docs.rs] diff --git a/actix-staticfiles/CHANGES.md b/actix-files/CHANGES.md similarity index 100% rename from actix-staticfiles/CHANGES.md rename to actix-files/CHANGES.md diff --git a/actix-staticfiles/Cargo.toml b/actix-files/Cargo.toml similarity index 97% rename from actix-staticfiles/Cargo.toml rename to actix-files/Cargo.toml index 0a551792..7082d167 100644 --- a/actix-staticfiles/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "actix-staticfiles" +name = "actix-files" version = "0.1.0" authors = ["Nikolay Kim "] description = "Static files support for Actix web." diff --git a/actix-staticfiles/README.md b/actix-files/README.md similarity index 100% rename from actix-staticfiles/README.md rename to actix-files/README.md diff --git a/actix-staticfiles/src/config.rs b/actix-files/src/config.rs similarity index 100% rename from actix-staticfiles/src/config.rs rename to actix-files/src/config.rs diff --git a/actix-staticfiles/src/error.rs b/actix-files/src/error.rs similarity index 91% rename from actix-staticfiles/src/error.rs rename to actix-files/src/error.rs index f165a618..ca99fa81 100644 --- a/actix-staticfiles/src/error.rs +++ b/actix-files/src/error.rs @@ -3,7 +3,7 @@ use derive_more::Display; /// Errors which can occur when serving static files. #[derive(Display, Debug, PartialEq)] -pub enum StaticFilesError { +pub enum FilesError { /// Path is not a directory #[display(fmt = "Path is not a directory. Unable to serve static files")] IsNotDirectory, @@ -13,8 +13,8 @@ pub enum StaticFilesError { IsDirectory, } -/// Return `NotFound` for `StaticFilesError` -impl ResponseError for StaticFilesError { +/// Return `NotFound` for `FilesError` +impl ResponseError for FilesError { fn error_response(&self) -> HttpResponse { HttpResponse::new(StatusCode::NOT_FOUND) } diff --git a/actix-staticfiles/src/lib.rs b/actix-files/src/lib.rs similarity index 94% rename from actix-staticfiles/src/lib.rs rename to actix-files/src/lib.rs index 7c3f6849..c6b52f04 100644 --- a/actix-staticfiles/src/lib.rs +++ b/actix-files/src/lib.rs @@ -29,7 +29,7 @@ mod error; mod named; mod range; -use self::error::{StaticFilesError, UriSegmentError}; +use self::error::{FilesError, UriSegmentError}; pub use crate::config::{DefaultConfig, StaticFileConfig}; pub use crate::named::NamedFile; pub use crate::range::HttpRange; @@ -222,10 +222,10 @@ fn directory_listing( /// /// fn main() { /// let app = App::new() -/// .service(fs::StaticFiles::new("/static", ".")); +/// .service(fs::Files::new("/static", ".")); /// } /// ``` -pub struct StaticFiles { +pub struct Files { path: String, directory: PathBuf, index: Option, @@ -237,28 +237,28 @@ pub struct StaticFiles { _cd_map: PhantomData, } -impl StaticFiles { - /// Create new `StaticFiles` instance for specified base directory. +impl Files { + /// Create new `Files` instance for specified base directory. /// /// `StaticFile` 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) -> StaticFiles { + pub fn new>(path: &str, dir: T) -> Files { Self::with_config(path, dir, DefaultConfig) } } -impl StaticFiles { - /// Create new `StaticFiles` instance for specified base directory. +impl Files { + /// Create new `Files` instance for specified base directory. /// /// Identical with `new` but allows to specify configiration to use. - pub fn with_config>(path: &str, dir: T, _: C) -> StaticFiles { + pub fn with_config>(path: &str, dir: T, _: C) -> Files { let dir = dir.into().canonicalize().unwrap_or_else(|_| PathBuf::new()); if !dir.is_dir() { log::error!("Specified path is not a directory"); } - StaticFiles { + Files { path: path.to_string(), directory: dir, index: None, @@ -294,13 +294,13 @@ impl StaticFiles { /// /// Shows specific index file for directory "/" instead of /// showing files listing. - pub fn index_file>(mut self, index: T) -> StaticFiles { + pub fn index_file>(mut self, index: T) -> Files { self.index = Some(index.into()); self } } -impl HttpServiceFactory

    for StaticFiles +impl HttpServiceFactory

    for Files where P: 'static, C: StaticFileConfig + 'static, @@ -319,16 +319,16 @@ where } impl NewService> - for StaticFiles + for Files { type Response = ServiceResponse; type Error = (); - type Service = StaticFilesService; + type Service = FilesService; type InitError = (); type Future = FutureResult; fn new_service(&self, _: &()) -> Self::Future { - ok(StaticFilesService { + ok(FilesService { directory: self.directory.clone(), index: self.index.clone(), show_index: self.show_index, @@ -341,7 +341,7 @@ impl NewService> } } -pub struct StaticFilesService { +pub struct FilesService { directory: PathBuf, index: Option, show_index: bool, @@ -352,7 +352,7 @@ pub struct StaticFilesService { _cd_map: PhantomData, } -impl Service> for StaticFilesService { +impl Service> for FilesService { type Response = ServiceResponse; type Error = (); type Future = FutureResult; @@ -395,7 +395,7 @@ impl Service> for StaticFilesService

    = StaticFiles::new("/", "missing"); - let _st: StaticFiles<()> = StaticFiles::new("/", "Cargo.toml"); + let _st: Files<()> = Files::new("/", "missing"); + let _st: Files<()> = Files::new("/", "Cargo.toml"); } // #[test] // fn test_default_handler_file_missing() { - // let st = StaticFiles::new(".") + // let st = Files::new(".") // .default_handler(|_: &_| "default content"); // let req = TestRequest::with_uri("/missing") // .param("tail", "missing") @@ -982,7 +982,7 @@ mod tests { // #[test] // fn test_serve_index() { - // let st = StaticFiles::new(".").index_file("test.binary"); + // let st = Files::new(".").index_file("test.binary"); // let req = TestRequest::default().uri("/tests").finish(); // let resp = st.handle(&req).respond_to(&req).unwrap(); @@ -1028,7 +1028,7 @@ mod tests { // #[test] // fn test_serve_index_nested() { - // let st = StaticFiles::new(".").index_file("mod.rs"); + // let st = Files::new(".").index_file("mod.rs"); // let req = TestRequest::default().uri("/src/client").finish(); // let resp = st.handle(&req).respond_to(&req).unwrap(); // let resp = resp.as_msg(); @@ -1048,7 +1048,7 @@ mod tests { // let mut srv = test::TestServer::with_factory(|| { // App::new().handler( // "test", - // StaticFiles::new(".").index_file("Cargo.toml"), + // Files::new(".").index_file("Cargo.toml"), // ) // }); @@ -1081,7 +1081,7 @@ mod tests { // let mut srv = test::TestServer::with_factory(|| { // App::new().handler( // "test", - // StaticFiles::new(".").index_file("Cargo.toml"), + // Files::new(".").index_file("Cargo.toml"), // ) // }); diff --git a/actix-staticfiles/src/named.rs b/actix-files/src/named.rs similarity index 100% rename from actix-staticfiles/src/named.rs rename to actix-files/src/named.rs diff --git a/actix-staticfiles/src/range.rs b/actix-files/src/range.rs similarity index 100% rename from actix-staticfiles/src/range.rs rename to actix-files/src/range.rs diff --git a/actix-staticfiles/tests/test space.binary b/actix-files/tests/test space.binary similarity index 100% rename from actix-staticfiles/tests/test space.binary rename to actix-files/tests/test space.binary diff --git a/actix-staticfiles/tests/test.binary b/actix-files/tests/test.binary similarity index 100% rename from actix-staticfiles/tests/test.binary rename to actix-files/tests/test.binary diff --git a/actix-staticfiles/tests/test.png b/actix-files/tests/test.png similarity index 100% rename from actix-staticfiles/tests/test.png rename to actix-files/tests/test.png From 1151b5bf7c39f6beec1fdce83701df0c4c4b018e Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 6 Mar 2019 23:43:03 -0800 Subject: [PATCH 050/109] fix crate name --- actix-files/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 7082d167..bd61c880 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -14,7 +14,7 @@ edition = "2018" workspace = ".." [lib] -name = "actix_staticfiles" +name = "actix_files" path = "src/lib.rs" [dependencies] From 22708e78a9043c16038d24d5edb7941b4241891e Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Mar 2019 11:09:42 -0800 Subject: [PATCH 051/109] added proc-macros for route registration --- Cargo.toml | 2 + actix-files/src/lib.rs | 10 +-- actix-web-codegen/Cargo.toml | 15 +++++ actix-web-codegen/src/lib.rs | 115 ++++++++++++++++++++++++++++++++ actix-web-codegen/src/server.rs | 31 +++++++++ examples/basic.rs | 13 ++-- src/handler.rs | 48 ++++++------- src/lib.rs | 18 +++++ src/route.rs | 11 +-- 9 files changed, 221 insertions(+), 42 deletions(-) create mode 100644 actix-web-codegen/Cargo.toml create mode 100644 actix-web-codegen/src/lib.rs create mode 100644 actix-web-codegen/src/server.rs diff --git a/Cargo.toml b/Cargo.toml index 7b2e6c99..f9e2266e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ members = [ ".", "actix-files", "actix-session", + "actix-web-codegen", ] [package.metadata.docs.rs] @@ -63,6 +64,7 @@ actix-codec = "0.1.0" #actix-service = "0.3.2" #actix-utils = "0.3.1" actix-rt = "0.2.0" +actix-web-codegen = { path="actix-web-codegen" } actix-service = { git = "https://github.com/actix/actix-net.git" } actix-utils = { git = "https://github.com/actix/actix-net.git" } diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index c6b52f04..c08cae9c 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -318,9 +318,7 @@ where } } -impl NewService> - for Files -{ +impl NewService> for Files { type Response = ServiceResponse; type Error = (); type Service = FilesService; @@ -730,8 +728,7 @@ mod tests { #[test] fn test_named_file_content_range_headers() { let mut srv = test::init_service( - App::new() - .service(Files::new("/test", ".").index_file("tests/test.binary")), + App::new().service(Files::new("/test", ".").index_file("tests/test.binary")), ); // Valid range header @@ -770,8 +767,7 @@ mod tests { #[test] fn test_named_file_content_length_headers() { let mut srv = test::init_service( - App::new() - .service(Files::new("test", ".").index_file("tests/test.binary")), + App::new().service(Files::new("test", ".").index_file("tests/test.binary")), ); // Valid range header diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml new file mode 100644 index 00000000..24ed36b7 --- /dev/null +++ b/actix-web-codegen/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "actix-web-codegen" +description = "Actix web codegen macros" +version = "0.1.0" +authors = ["Nikolay Kim "] +license = "MIT/Apache-2.0" +edition = "2018" +workspace = ".." + +[lib] +proc-macro = true + +[dependencies] +quote = "0.6" +syn = { version = "0.15", features = ["full", "parsing"] } diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs new file mode 100644 index 00000000..1052c82a --- /dev/null +++ b/actix-web-codegen/src/lib.rs @@ -0,0 +1,115 @@ +#![recursion_limit = "512"] + +extern crate proc_macro; + +use proc_macro::TokenStream; +use quote::quote; +use syn::parse_macro_input; + +/// #[get("path")] attribute +#[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::AppConfig

    ) { + #ast + actix_web::dev::HttpServiceFactory::register( + actix_web::Resource::new(#path) + .route(actix_web::web::get().to(#name)), config); + } + } + }) + .into() +} + +/// #[post("path")] attribute +#[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: #[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::AppConfig

    ) { + #ast + actix_web::dev::HttpServiceFactory::register( + actix_web::Resource::new(#path) + .route(actix_web::web::post().to(#name)), config); + } + } + }) + .into() +} + +/// #[put("path")] attribute +#[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: #[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::AppConfig

    ) { + #ast + actix_web::dev::HttpServiceFactory::register( + actix_web::Resource::new(#path) + .route(actix_web::web::put().to(#name)), config); + } + } + }) + .into() +} diff --git a/actix-web-codegen/src/server.rs b/actix-web-codegen/src/server.rs new file mode 100644 index 00000000..43e663f3 --- /dev/null +++ b/actix-web-codegen/src/server.rs @@ -0,0 +1,31 @@ +use std::collections::HashSet; +use std::env; +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; + +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn; + +/// Thrift mux server impl +pub struct Server {} + +impl Server { + fn new() -> Server { + Server {} + } + + /// generate servers + pub fn generate(input: TokenStream) { + let mut srv = Server::new(); + let ast: syn::ItemFn = syn::parse2(input).unwrap(); + println!("T: {:?}", ast.ident); + + // quote! { + // #ast + + // #(#servers)* + // } + } +} diff --git a/examples/basic.rs b/examples/basic.rs index 5fd862d4..39633f52 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,9 +1,9 @@ use futures::IntoFuture; -use actix_web::{ - http::Method, middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer, -}; +use actix_web::macros::get; +use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer}; +#[get("/resource1/index.html")] fn index(req: HttpRequest) -> &'static str { println!("REQ: {:?}", req); "Hello world!\r\n" @@ -14,6 +14,7 @@ fn index_async(req: HttpRequest) -> impl IntoFuture &'static str { "Hello world!\r\n" } @@ -27,7 +28,8 @@ fn main() -> std::io::Result<()> { App::new() .middleware(middleware::DefaultHeaders::new().header("X-Version", "0.2")) .middleware(middleware::Compress::default()) - .service(web::resource("/resource1/index.html").route(web::get().to(index))) + .service(index) + .service(no_params) .service( web::resource("/resource2/index.html") .middleware( @@ -36,10 +38,9 @@ fn main() -> std::io::Result<()> { .default_resource(|r| { r.route(web::route().to(|| HttpResponse::MethodNotAllowed())) }) - .route(web::method(Method::GET).to_async(index_async)), + .route(web::get().to_async(index_async)), ) .service(web::resource("/test1.html").to(|| "Test\r\n")) - .service(web::resource("/").to(no_params)) }) .bind("127.0.0.1:8080")? .workers(1) diff --git a/src/handler.rs b/src/handler.rs index 442dc60d..435d9a8b 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -31,7 +31,7 @@ where } #[doc(hidden)] -pub struct Handle +pub struct Handler where F: Factory, R: Responder, @@ -40,19 +40,19 @@ where _t: PhantomData<(T, R)>, } -impl Handle +impl Handler where F: Factory, R: Responder, { pub fn new(hnd: F) -> Self { - Handle { + Handler { hnd, _t: PhantomData, } } } -impl NewService<(T, HttpRequest)> for Handle +impl NewService<(T, HttpRequest)> for Handler where F: Factory, R: Responder + 'static, @@ -60,11 +60,11 @@ where type Response = ServiceResponse; type Error = Void; type InitError = (); - type Service = HandleService; + type Service = HandlerService; type Future = FutureResult; fn new_service(&self, _: &()) -> Self::Future { - ok(HandleService { + ok(HandlerService { hnd: self.hnd.clone(), _t: PhantomData, }) @@ -72,7 +72,7 @@ where } #[doc(hidden)] -pub struct HandleService +pub struct HandlerService where F: Factory, R: Responder + 'static, @@ -81,14 +81,14 @@ where _t: PhantomData<(T, R)>, } -impl Service<(T, HttpRequest)> for HandleService +impl Service<(T, HttpRequest)> for HandlerService where F: Factory, R: Responder + 'static, { type Response = ServiceResponse; type Error = Void; - type Future = HandleServiceResponse<::Future>; + type Future = HandlerServiceResponse<::Future>; fn poll_ready(&mut self) -> Poll<(), Self::Error> { Ok(Async::Ready(())) @@ -96,19 +96,19 @@ where fn call(&mut self, (param, req): (T, HttpRequest)) -> Self::Future { let fut = self.hnd.call(param).respond_to(&req).into_future(); - HandleServiceResponse { + HandlerServiceResponse { fut, req: Some(req), } } } -pub struct HandleServiceResponse { +pub struct HandlerServiceResponse { fut: T, req: Option, } -impl Future for HandleServiceResponse +impl Future for HandlerServiceResponse where T: Future, T::Error: Into, @@ -157,7 +157,7 @@ where } #[doc(hidden)] -pub struct AsyncHandle +pub struct AsyncHandler where F: AsyncFactory, R: IntoFuture, @@ -168,7 +168,7 @@ where _t: PhantomData<(T, R)>, } -impl AsyncHandle +impl AsyncHandler where F: AsyncFactory, R: IntoFuture, @@ -176,13 +176,13 @@ where R::Error: Into, { pub fn new(hnd: F) -> Self { - AsyncHandle { + AsyncHandler { hnd, _t: PhantomData, } } } -impl NewService<(T, HttpRequest)> for AsyncHandle +impl NewService<(T, HttpRequest)> for AsyncHandler where F: AsyncFactory, R: IntoFuture, @@ -192,11 +192,11 @@ where type Response = ServiceResponse; type Error = (); type InitError = (); - type Service = AsyncHandleService; + type Service = AsyncHandlerService; type Future = FutureResult; fn new_service(&self, _: &()) -> Self::Future { - ok(AsyncHandleService { + ok(AsyncHandlerService { hnd: self.hnd.clone(), _t: PhantomData, }) @@ -204,7 +204,7 @@ where } #[doc(hidden)] -pub struct AsyncHandleService +pub struct AsyncHandlerService where F: AsyncFactory, R: IntoFuture, @@ -215,7 +215,7 @@ where _t: PhantomData<(T, R)>, } -impl Service<(T, HttpRequest)> for AsyncHandleService +impl Service<(T, HttpRequest)> for AsyncHandlerService where F: AsyncFactory, R: IntoFuture, @@ -224,14 +224,14 @@ where { type Response = ServiceResponse; type Error = (); - type Future = AsyncHandleServiceResponse; + type Future = AsyncHandlerServiceResponse; fn poll_ready(&mut self) -> Poll<(), Self::Error> { Ok(Async::Ready(())) } fn call(&mut self, (param, req): (T, HttpRequest)) -> Self::Future { - AsyncHandleServiceResponse { + AsyncHandlerServiceResponse { fut: self.hnd.call(param).into_future(), req: Some(req), } @@ -239,12 +239,12 @@ where } #[doc(hidden)] -pub struct AsyncHandleServiceResponse { +pub struct AsyncHandlerServiceResponse { fut: T, req: Option, } -impl Future for AsyncHandleServiceResponse +impl Future for AsyncHandlerServiceResponse where T: Future, T::Item: Into, diff --git a/src/lib.rs b/src/lib.rs index fd1b21f3..dd60c7b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,24 @@ mod service; mod state; pub mod test; +/// Attribute macros for route registration +/// +/// ```rust +/// use actix_web::{macros, App, HttpResponse}; +/// +/// #[macros::get("/index.html")] +/// fn index() -> HttpResponse { +/// HttpResponse::Ok().finish() +/// } +/// +/// fn main() { +/// let app = App::new().service(index); +/// } +/// ``` +pub mod macros { + pub use actix_web_codegen::{get, post, put}; +} + // re-export for convenience pub use actix_http::Response as HttpResponse; pub use actix_http::{body, error, http, Error, HttpMessage, ResponseError, Result}; diff --git a/src/route.rs b/src/route.rs index b611164e..9538dfd2 100644 --- a/src/route.rs +++ b/src/route.rs @@ -8,7 +8,7 @@ use futures::{Async, Future, IntoFuture, Poll}; use crate::extract::{ConfigStorage, ExtractorConfig, FromRequest}; use crate::guard::{self, Guard}; -use crate::handler::{AsyncFactory, AsyncHandle, Extract, Factory, Handle}; +use crate::handler::{AsyncFactory, AsyncHandler, Extract, Factory, Handler}; use crate::responder::Responder; use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; use crate::HttpResponse; @@ -50,8 +50,9 @@ impl Route

    { let config_ref = Rc::new(RefCell::new(None)); Route { service: Box::new(RouteNewService::new( - Extract::new(config_ref.clone()) - .and_then(Handle::new(HttpResponse::NotFound).map_err(|_| panic!())), + Extract::new(config_ref.clone()).and_then( + Handler::new(HttpResponse::NotFound).map_err(|_| panic!()), + ), )), guards: Rc::new(Vec::new()), config: ConfigStorage::default(), @@ -272,7 +273,7 @@ impl Route

    { T::Config::store_default(&mut self.config); self.service = Box::new(RouteNewService::new( Extract::new(self.config_ref.clone()) - .and_then(Handle::new(handler).map_err(|_| panic!())), + .and_then(Handler::new(handler).map_err(|_| panic!())), )); self } @@ -314,7 +315,7 @@ impl Route

    { { self.service = Box::new(RouteNewService::new( Extract::new(self.config_ref.clone()) - .and_then(AsyncHandle::new(handler).map_err(|_| panic!())), + .and_then(AsyncHandler::new(handler).map_err(|_| panic!())), )); self } From ceb6d45bf240ae228e4ce7f4bc17f5b747e4e6ae Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Mar 2019 11:43:46 -0800 Subject: [PATCH 052/109] reexpost extractors in web module --- actix-web-codegen/src/lib.rs | 9 +++++--- examples/basic.rs | 14 ++++++------ src/app.rs | 42 ++++++++++++++++-------------------- src/extract.rs | 8 +++---- src/lib.rs | 6 ++++-- src/route.rs | 4 ++-- 6 files changed, 41 insertions(+), 42 deletions(-) diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 1052c82a..26b422d7 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -35,7 +35,8 @@ pub fn get(args: TokenStream, input: TokenStream) -> TokenStream { #ast actix_web::dev::HttpServiceFactory::register( actix_web::Resource::new(#path) - .route(actix_web::web::get().to(#name)), config); + .guard(actix_web::guard::Get()) + .to(#name), config); } } }) @@ -71,7 +72,8 @@ pub fn post(args: TokenStream, input: TokenStream) -> TokenStream { #ast actix_web::dev::HttpServiceFactory::register( actix_web::Resource::new(#path) - .route(actix_web::web::post().to(#name)), config); + .guard(actix_web::guard::Post()) + .to(#name), config); } } }) @@ -107,7 +109,8 @@ pub fn put(args: TokenStream, input: TokenStream) -> TokenStream { #ast actix_web::dev::HttpServiceFactory::register( actix_web::Resource::new(#path) - .route(actix_web::web::put().to(#name)), config); + .guard(actix_web::guard::Put()) + .to(#name), config); } } }) diff --git a/examples/basic.rs b/examples/basic.rs index 39633f52..3f832780 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -3,10 +3,10 @@ use futures::IntoFuture; use actix_web::macros::get; use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer}; -#[get("/resource1/index.html")] -fn index(req: HttpRequest) -> &'static str { +#[get("/resource1/{name}/index.html")] +fn index(req: HttpRequest, name: web::Path) -> String { println!("REQ: {:?}", req); - "Hello world!\r\n" + format!("Hello: {}!\r\n", name) } fn index_async(req: HttpRequest) -> impl IntoFuture { @@ -20,14 +20,14 @@ fn no_params() -> &'static str { } fn main() -> std::io::Result<()> { - ::std::env::set_var("RUST_LOG", "actix_server=info,actix_web2=info"); + std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info"); env_logger::init(); - let sys = actix_rt::System::new("hello-world"); HttpServer::new(|| { App::new() .middleware(middleware::DefaultHeaders::new().header("X-Version", "0.2")) .middleware(middleware::Compress::default()) + .middleware(middleware::Logger::default()) .service(index) .service(no_params) .service( @@ -44,7 +44,5 @@ fn main() -> std::io::Result<()> { }) .bind("127.0.0.1:8080")? .workers(1) - .start(); - - sys.run() + .run() } diff --git a/src/app.rs b/src/app.rs index f62c064a..1cff1788 100644 --- a/src/app.rs +++ b/src/app.rs @@ -75,13 +75,13 @@ where /// /// ```rust /// use std::cell::Cell; - /// use actix_web::{web, State, App}; + /// use actix_web::{web, App}; /// /// struct MyState { /// counter: Cell, /// } /// - /// fn index(state: State) { + /// fn index(state: web::State) { /// state.counter.set(state.counter.get() + 1); /// } /// @@ -785,7 +785,7 @@ mod tests { use super::*; use crate::http::{Method, StatusCode}; use crate::test::{block_on, init_service, TestRequest}; - use crate::{web, HttpResponse, State}; + use crate::{web, HttpResponse}; #[test] fn test_default_resource() { @@ -828,21 +828,19 @@ mod tests { #[test] fn test_state() { - let mut srv = init_service( - App::new() - .state(10usize) - .service(web::resource("/").to(|_: State| HttpResponse::Ok())), - ); + let mut srv = + init_service(App::new().state(10usize).service( + web::resource("/").to(|_: web::State| HttpResponse::Ok()), + )); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); - let mut srv = init_service( - App::new() - .state(10u32) - .service(web::resource("/").to(|_: State| HttpResponse::Ok())), - ); + let mut srv = + init_service(App::new().state(10u32).service( + web::resource("/").to(|_: web::State| HttpResponse::Ok()), + )); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); @@ -850,20 +848,18 @@ mod tests { #[test] fn test_state_factory() { - let mut srv = init_service( - App::new() - .state_factory(|| Ok::<_, ()>(10usize)) - .service(web::resource("/").to(|_: State| HttpResponse::Ok())), - ); + let mut srv = + init_service(App::new().state_factory(|| Ok::<_, ()>(10usize)).service( + web::resource("/").to(|_: web::State| HttpResponse::Ok()), + )); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); - let mut srv = init_service( - App::new() - .state_factory(|| Ok::<_, ()>(10u32)) - .service(web::resource("/").to(|_: State| HttpResponse::Ok())), - ); + let mut srv = + init_service(App::new().state_factory(|| Ok::<_, ()>(10u32)).service( + web::resource("/").to(|_: web::State| HttpResponse::Ok()), + )); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); diff --git a/src/extract.rs b/src/extract.rs index 0b212aba..6c838901 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -554,8 +554,8 @@ impl Default for FormConfig { /// name: String, /// } /// -/// fn index(req: HttpRequest) -> Result> { -/// Ok(Json(MyObj { +/// fn index(req: HttpRequest) -> Result> { +/// Ok(web::Json(MyObj { /// name: req.match_info().get("name").unwrap().to_string(), /// })) /// } @@ -679,7 +679,7 @@ where /// /// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{error, extract, web, App, HttpResponse, Json}; +/// use actix_web::{error, extract, web, App, HttpResponse}; /// /// #[derive(Deserialize)] /// struct Info { @@ -687,7 +687,7 @@ where /// } /// /// /// deserialize `Info` from request's body, max payload size is 4kb -/// fn index(info: Json) -> String { +/// fn index(info: web::Json) -> String { /// format!("Welcome {}!", info.username) /// } /// diff --git a/src/lib.rs b/src/lib.rs index dd60c7b8..35a88b98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,7 +41,7 @@ pub use actix_http::Response as HttpResponse; pub use actix_http::{body, error, http, Error, HttpMessage, ResponseError, Result}; pub use crate::app::App; -pub use crate::extract::{FromRequest, Json}; +pub use crate::extract::FromRequest; pub use crate::request::HttpRequest; pub use crate::resource::Resource; pub use crate::responder::{Either, Responder}; @@ -49,7 +49,6 @@ pub use crate::route::Route; pub use crate::scope::Scope; pub use crate::server::HttpServer; pub use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; -pub use crate::state::State; pub mod dev { //! The `actix-web` prelude for library developers @@ -93,6 +92,9 @@ pub mod web { use crate::route::Route; use crate::scope::Scope; + pub use crate::extract::{Json, Path, Query}; + pub use crate::state::State; + /// Create resource for a specific path. /// /// Resources may have variable path segments. For example, a diff --git a/src/route.rs b/src/route.rs index 9538dfd2..45bc6534 100644 --- a/src/route.rs +++ b/src/route.rs @@ -245,7 +245,7 @@ impl Route

    { /// ```rust /// # use std::collections::HashMap; /// # use serde_derive::Deserialize; - /// use actix_web::{web, App, Json, extract::Path, extract::Query}; + /// use actix_web::{web, App}; /// /// #[derive(Deserialize)] /// struct Info { @@ -253,7 +253,7 @@ impl Route

    { /// } /// /// /// extract path info using serde - /// fn index(path: Path, query: Query>, body: Json) -> String { + /// fn index(path: web::Path, query: web::Query>, body: web::Json) -> String { /// format!("Welcome {}!", path.username) /// } /// From d77954d19e99aabaa8749ead28f5a03435b0b656 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Mar 2019 12:32:40 -0800 Subject: [PATCH 053/109] fix files test --- actix-files/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index c08cae9c..5dd98dcc 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -214,11 +214,11 @@ fn directory_listing( /// Static files handling /// -/// `StaticFile` handler must be registered with `App::service()` method. +/// `Files` service must be registered with `App::service()` method. /// /// ```rust /// use actix_web::App; -/// use actix_staticfiles as fs; +/// use actix_files as fs; /// /// fn main() { /// let app = App::new() @@ -240,7 +240,7 @@ pub struct Files { impl Files { /// Create new `Files` instance for specified base directory. /// - /// `StaticFile` uses `ThreadPool` for blocking filesystem operations. + /// `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 { From b211966c28112f808005ee19a92c94ad650ce86c Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Mar 2019 13:33:40 -0800 Subject: [PATCH 054/109] Payload extractor --- actix-web-codegen/src/server.rs | 31 ----------- examples/basic.rs | 4 +- src/extract.rs | 95 ++++++++++++++++++++++++++++++++- src/lib.rs | 29 ++++------ 4 files changed, 107 insertions(+), 52 deletions(-) delete mode 100644 actix-web-codegen/src/server.rs diff --git a/actix-web-codegen/src/server.rs b/actix-web-codegen/src/server.rs deleted file mode 100644 index 43e663f3..00000000 --- a/actix-web-codegen/src/server.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::collections::HashSet; -use std::env; -use std::fs::File; -use std::io::Read; -use std::path::PathBuf; - -use proc_macro2::TokenStream; -use quote::{quote, ToTokens}; -use syn; - -/// Thrift mux server impl -pub struct Server {} - -impl Server { - fn new() -> Server { - Server {} - } - - /// generate servers - pub fn generate(input: TokenStream) { - let mut srv = Server::new(); - let ast: syn::ItemFn = syn::parse2(input).unwrap(); - println!("T: {:?}", ast.ident); - - // quote! { - // #ast - - // #(#servers)* - // } - } -} diff --git a/examples/basic.rs b/examples/basic.rs index 3f832780..f8b81648 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,6 +1,8 @@ use futures::IntoFuture; -use actix_web::macros::get; +#[macro_use] +extern crate actix_web; + use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer}; #[get("/resource1/{name}/index.html")] diff --git a/src/extract.rs b/src/extract.rs index 6c838901..ac04f1c4 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -409,7 +409,7 @@ impl DerefMut for Form { impl FromRequest

    for Form where T: DeserializeOwned + 'static, - P: Stream + 'static, + P: Stream + 'static, { type Error = Error; type Future = Box>; @@ -653,7 +653,7 @@ impl Responder for Json { impl FromRequest

    for Json where T: DeserializeOwned + 'static, - P: Stream + 'static, + P: Stream + 'static, { type Error = Error; type Future = Box>; @@ -739,6 +739,97 @@ impl Default for JsonConfig { } } +/// Payload extractor returns request 's payload stream. +/// +/// ## Example +/// +/// ```rust +/// use futures::{Future, Stream}; +/// use actix_web::{web, error, App, Error, HttpResponse}; +/// +/// /// extract binary data from request +/// fn index

    (body: web::Payload

    ) -> impl Future +/// where +/// P: Stream +/// { +/// body.map_err(Error::from) +/// .fold(web::BytesMut::new(), move |mut body, chunk| { +/// body.extend_from_slice(&chunk); +/// Ok::<_, Error>(body) +/// }) +/// .and_then(|body| { +/// format!("Body {:?}!", body); +/// Ok(HttpResponse::Ok().finish()) +/// }) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::get().to_async(index)) +/// ); +/// } +/// ``` +pub struct Payload(crate::dev::Payload); + +impl Stream for Payload +where + T: Stream, +{ + type Item = Bytes; + type Error = PayloadError; + + #[inline] + fn poll(&mut self) -> Poll, PayloadError> { + self.0.poll() + } +} + +/// Get request's payload stream +/// +/// ## Example +/// +/// ```rust +/// use futures::{Future, Stream}; +/// use actix_web::{web, error, App, Error, HttpResponse}; +/// +/// /// extract binary data from request +/// fn index

    (body: web::Payload

    ) -> impl Future +/// where +/// P: Stream +/// { +/// body.map_err(Error::from) +/// .fold(web::BytesMut::new(), move |mut body, chunk| { +/// body.extend_from_slice(&chunk); +/// Ok::<_, Error>(body) +/// }) +/// .and_then(|body| { +/// format!("Body {:?}!", body); +/// Ok(HttpResponse::Ok().finish()) +/// }) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::get().to_async(index)) +/// ); +/// } +/// ``` +impl

    FromRequest

    for Payload

    +where + P: Stream, +{ + type Error = Error; + type Future = Result, Error>; + type Config = (); + + #[inline] + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { + Ok(Payload(req.take_payload())) + } +} + /// Request binary data from a request's payload. /// /// Loads request's payload and construct Bytes instance. diff --git a/src/lib.rs b/src/lib.rs index 35a88b98..ad4a2a86 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,23 +18,12 @@ mod service; mod state; pub mod test; -/// Attribute macros for route registration -/// -/// ```rust -/// use actix_web::{macros, App, HttpResponse}; -/// -/// #[macros::get("/index.html")] -/// fn index() -> HttpResponse { -/// HttpResponse::Ok().finish() -/// } -/// -/// fn main() { -/// let app = App::new().service(index); -/// } -/// ``` -pub mod macros { - pub use actix_web_codegen::{get, post, put}; -} +#[allow(unused_imports)] +#[macro_use] +extern crate actix_web_codegen; + +#[doc(hidden)] +pub use actix_web_codegen::*; // re-export for convenience pub use actix_http::Response as HttpResponse; @@ -85,6 +74,9 @@ pub mod web { use actix_http::{http::Method, Error, Response}; use futures::IntoFuture; + pub use actix_http::Response as HttpResponse; + pub use bytes::{Bytes, BytesMut}; + use crate::extract::FromRequest; use crate::handler::{AsyncFactory, Factory}; use crate::resource::Resource; @@ -92,7 +84,8 @@ pub mod web { use crate::route::Route; use crate::scope::Scope; - pub use crate::extract::{Json, Path, Query}; + pub use crate::extract::{Json, Path, Payload, Query}; + pub use crate::request::HttpRequest; pub use crate::state::State; /// Create resource for a specific path. From 0e57b4ad618bcf0d4c4140acd0bb268c4060211e Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Mar 2019 14:01:52 -0800 Subject: [PATCH 055/109] export extractor configs via web module --- src/app.rs | 8 ++++---- src/extract.rs | 50 ++++++++++++++++++++++++------------------------- src/lib.rs | 5 +++-- src/resource.rs | 4 ++-- src/route.rs | 12 ++++++------ src/scope.rs | 8 ++++---- 6 files changed, 44 insertions(+), 43 deletions(-) diff --git a/src/app.rs b/src/app.rs index 1cff1788..c1c019a3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -189,9 +189,9 @@ where /// multiple resources with one route would be registered for same resource path. /// /// ```rust - /// use actix_web::{web, App, HttpResponse, extract::Path}; + /// use actix_web::{web, App, HttpResponse}; /// - /// fn index(data: Path<(String, String)>) -> &'static str { + /// fn index(data: web::Path<(String, String)>) -> &'static str { /// "Welcome!" /// } /// @@ -276,9 +276,9 @@ where /// multiple resources with one route would be registered for same resource path. /// /// ```rust - /// use actix_web::{web, App, HttpResponse, extract::Path}; + /// use actix_web::{web, App, HttpResponse}; /// - /// fn index(data: Path<(String, String)>) -> &'static str { + /// fn index(data: web::Path<(String, String)>) -> &'static str { /// "Welcome!" /// } /// diff --git a/src/extract.rs b/src/extract.rs index ac04f1c4..c34d9df7 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -78,12 +78,12 @@ impl ExtractorConfig for () { /// ## Example /// /// ```rust -/// use actix_web::{web, http, App, extract::Path}; +/// use actix_web::{web, App}; /// /// /// extract path info from "/{username}/{count}/index.html" url /// /// {username} - deserializes to a String /// /// {count} - - deserializes to a u32 -/// fn index(info: Path<(String, u32)>) -> String { +/// fn index(info: web::Path<(String, u32)>) -> String { /// format!("Welcome {}! {}", info.0, info.1) /// } /// @@ -100,7 +100,7 @@ impl ExtractorConfig for () { /// /// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App, extract::Path, Error}; +/// use actix_web::{web, App, Error}; /// /// #[derive(Deserialize)] /// struct Info { @@ -108,7 +108,7 @@ impl ExtractorConfig for () { /// } /// /// /// extract `Info` from a path using serde -/// fn index(info: Path) -> Result { +/// fn index(info: web::Path) -> Result { /// Ok(format!("Welcome {}!", info.username)) /// } /// @@ -170,12 +170,12 @@ impl From for Path { /// ## Example /// /// ```rust -/// use actix_web::{web, http, App, extract::Path}; +/// use actix_web::{web, App}; /// /// /// extract path info from "/{username}/{count}/index.html" url /// /// {username} - deserializes to a String /// /// {count} - - deserializes to a u32 -/// fn index(info: Path<(String, u32)>) -> String { +/// fn index(info: web::Path<(String, u32)>) -> String { /// format!("Welcome {}! {}", info.0, info.1) /// } /// @@ -192,7 +192,7 @@ impl From for Path { /// /// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App, extract::Path, Error}; +/// use actix_web::{web, App, Error}; /// /// #[derive(Deserialize)] /// struct Info { @@ -200,7 +200,7 @@ impl From for Path { /// } /// /// /// extract `Info` from a path using serde -/// fn index(info: Path) -> Result { +/// fn index(info: web::Path) -> Result { /// Ok(format!("Welcome {}!", info.username)) /// } /// @@ -244,7 +244,7 @@ impl fmt::Display for Path { /// /// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, extract, App}; +/// use actix_web::{web, App}; /// /// #[derive(Debug, Deserialize)] /// pub enum ResponseType { @@ -261,7 +261,7 @@ impl fmt::Display for Path { /// // Use `Query` extractor for query information. /// // This handler get called only if request's query contains `username` field /// // The correct request for this handler would be `/index.html?id=64&response_type=Code"` -/// fn index(info: extract::Query) -> String { +/// fn index(info: web::Query) -> String { /// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) /// } /// @@ -299,7 +299,7 @@ impl Query { /// /// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, extract, App}; +/// use actix_web::{web, App}; /// /// #[derive(Debug, Deserialize)] /// pub enum ResponseType { @@ -316,7 +316,7 @@ impl Query { /// // Use `Query` extractor for query information. /// // This handler get called only if request's query contains `username` field /// // The correct request for this handler would be `/index.html?id=64&response_type=Code"` -/// fn index(info: extract::Query) -> String { +/// fn index(info: web::Query) -> String { /// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) /// } /// @@ -368,7 +368,7 @@ impl fmt::Display for Query { /// ```rust /// # extern crate actix_web; /// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App, extract::Form}; +/// use actix_web::{web, App}; /// /// #[derive(Deserialize)] /// struct FormData { @@ -378,7 +378,7 @@ impl fmt::Display for Query { /// /// Extract form data using serde. /// /// This handler get called only if content type is *x-www-form-urlencoded* /// /// and content of the request could be deserialized to a `FormData` struct -/// fn index(form: Form) -> String { +/// fn index(form: web::Form) -> String { /// format!("Welcome {}!", form.username) /// } /// # fn main() {} @@ -447,7 +447,7 @@ impl fmt::Display for Form { /// /// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, extract, App, Result}; +/// use actix_web::{web, App, Result}; /// /// #[derive(Deserialize)] /// struct FormData { @@ -456,7 +456,7 @@ impl fmt::Display for Form { /// /// /// Extract form data using serde. /// /// Custom configuration is used for this handler, max payload size is 4k -/// fn index(form: extract::Form) -> Result { +/// fn index(form: web::Form) -> Result { /// Ok(format!("Welcome {}!", form.username)) /// } /// @@ -465,7 +465,7 @@ impl fmt::Display for Form { /// web::resource("/index.html") /// .route(web::get() /// // change `Form` extractor configuration -/// .config(extract::FormConfig::default().limit(4097)) +/// .config(web::FormConfig::default().limit(4097)) /// .to(index)) /// ); /// } @@ -520,7 +520,7 @@ impl Default for FormConfig { /// /// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, extract, App}; +/// use actix_web::{web, App}; /// /// #[derive(Deserialize)] /// struct Info { @@ -528,7 +528,7 @@ impl Default for FormConfig { /// } /// /// /// deserialize `Info` from request's body -/// fn index(info: extract::Json) -> String { +/// fn index(info: web::Json) -> String { /// format!("Welcome {}!", info.username) /// } /// @@ -631,7 +631,7 @@ impl Responder for Json { /// /// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, extract, App}; +/// use actix_web::{web, App}; /// /// #[derive(Deserialize)] /// struct Info { @@ -639,7 +639,7 @@ impl Responder for Json { /// } /// /// /// deserialize `Info` from request's body -/// fn index(info: extract::Json) -> String { +/// fn index(info: web::Json) -> String { /// format!("Welcome {}!", info.username) /// } /// @@ -679,7 +679,7 @@ where /// /// ```rust /// #[macro_use] extern crate serde_derive; -/// use actix_web::{error, extract, web, App, HttpResponse}; +/// use actix_web::{error, web, App, HttpResponse}; /// /// #[derive(Deserialize)] /// struct Info { @@ -696,7 +696,7 @@ where /// web::resource("/index.html").route( /// web::post().config( /// // change json extractor configuration -/// extract::JsonConfig::default().limit(4096) +/// web::JsonConfig::default().limit(4096) /// .error_handler(|err, req| { // <- create custom error response /// error::InternalError::from_response( /// err, HttpResponse::Conflict().finish()).into() @@ -887,7 +887,7 @@ where /// ## Example /// /// ```rust -/// use actix_web::{web, extract, App}; +/// use actix_web::{web, App}; /// /// /// extract text data from request /// fn index(text: String) -> String { @@ -898,7 +898,7 @@ where /// let app = App::new().service( /// web::resource("/index.html").route( /// web::get() -/// .config(extract::PayloadConfig::new(4096)) // <- limit size of the payload +/// .config(web::PayloadConfig::new(4096)) // <- limit size of the payload /// .to(index)) // <- register handler with extractor params /// ); /// } diff --git a/src/lib.rs b/src/lib.rs index ad4a2a86..def2abcf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ #![allow(clippy::type_complexity)] mod app; -pub mod extract; +mod extract; mod handler; // mod info; pub mod blocking; @@ -84,7 +84,8 @@ pub mod web { use crate::route::Route; use crate::scope::Scope; - pub use crate::extract::{Json, Path, Payload, Query}; + pub use crate::extract::{Form, Json, Path, Payload, Query}; + pub use crate::extract::{FormConfig, JsonConfig, PayloadConfig}; pub use crate::request::HttpRequest; pub use crate::state::State; diff --git a/src/resource.rs b/src/resource.rs index 157b181e..13afff70 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -75,9 +75,9 @@ where /// Add match guard to a resource. /// /// ```rust - /// use actix_web::{web, guard, App, HttpResponse, extract::Path}; + /// use actix_web::{web, guard, App, HttpResponse}; /// - /// fn index(data: Path<(String, String)>) -> &'static str { + /// fn index(data: web::Path<(String, String)>) -> &'static str { /// "Welcome!" /// } /// diff --git a/src/route.rs b/src/route.rs index 45bc6534..d1a8320d 100644 --- a/src/route.rs +++ b/src/route.rs @@ -220,7 +220,7 @@ impl Route

    { /// /// ```rust /// #[macro_use] extern crate serde_derive; - /// use actix_web::{web, http, App, extract::Path}; + /// use actix_web::{web, http, App}; /// /// #[derive(Deserialize)] /// struct Info { @@ -228,7 +228,7 @@ impl Route

    { /// } /// /// /// extract path info using serde - /// fn index(info: Path) -> String { + /// fn index(info: web::Path) -> String { /// format!("Welcome {}!", info.username) /// } /// @@ -284,7 +284,7 @@ impl Route

    { /// ```rust /// # use futures::future::ok; /// #[macro_use] extern crate serde_derive; - /// use actix_web::{web, App, Error, extract::Path}; + /// use actix_web::{web, App, Error}; /// use futures::Future; /// /// #[derive(Deserialize)] @@ -293,7 +293,7 @@ impl Route

    { /// } /// /// /// extract path info using serde - /// fn index(info: Path) -> impl Future { + /// fn index(info: web::Path) -> impl Future { /// ok("Hello World!") /// } /// @@ -324,7 +324,7 @@ impl Route

    { /// for specific route. /// /// ```rust - /// use actix_web::{web, extract, App}; + /// use actix_web::{web, App}; /// /// /// extract text data from request /// fn index(body: String) -> String { @@ -336,7 +336,7 @@ impl Route

    { /// web::resource("/index.html").route( /// web::get() /// // limit size of the payload - /// .config(extract::PayloadConfig::new(4096)) + /// .config(web::PayloadConfig::new(4096)) /// // register handler /// .to(index) /// )); diff --git a/src/scope.rs b/src/scope.rs index 5580b15e..a6358869 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -89,9 +89,9 @@ where /// Add match guard to a scope. /// /// ```rust - /// use actix_web::{web, guard, App, HttpRequest, HttpResponse, extract::Path}; + /// use actix_web::{web, guard, App, HttpRequest, HttpResponse}; /// - /// fn index(data: Path<(String, String)>) -> &'static str { + /// fn index(data: web::Path<(String, String)>) -> &'static str { /// "Welcome!" /// } /// @@ -146,9 +146,9 @@ where /// multiple resources with one route would be registered for same resource path. /// /// ```rust - /// use actix_web::{web, App, HttpResponse, extract::Path}; + /// use actix_web::{web, App, HttpResponse}; /// - /// fn index(data: Path<(String, String)>) -> &'static str { + /// fn index(data: web::Path<(String, String)>) -> &'static str { /// "Welcome!" /// } /// From c2a350b33fba39ed7e175dfaa5b5ddd1b3da88aa Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Mar 2019 14:40:20 -0800 Subject: [PATCH 056/109] export blocking via web module --- src/lib.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index def2abcf..6cf18a2a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,6 +77,7 @@ pub mod web { pub use actix_http::Response as HttpResponse; pub use bytes::{Bytes, BytesMut}; + use crate::blocking::CpuFuture; use crate::extract::FromRequest; use crate::handler::{AsyncFactory, Factory}; use crate::resource::Resource; @@ -234,4 +235,15 @@ pub mod web { { Route::new().to_async(handler) } + + /// Execute blocking function on a thread pool, returns future that resolves + /// to result of the function execution. + pub fn blocking(f: F) -> CpuFuture + where + F: FnOnce() -> Result + Send + 'static, + I: Send + 'static, + E: Send + std::fmt::Debug + 'static, + { + crate::blocking::run(f) + } } From b6b2eadb3ac0230c0b2c688b6f3e586a5821884d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Mar 2019 14:41:43 -0800 Subject: [PATCH 057/109] rename blocking fn --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 6cf18a2a..6e809fb6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -238,7 +238,7 @@ pub mod web { /// Execute blocking function on a thread pool, returns future that resolves /// to result of the function execution. - pub fn blocking(f: F) -> CpuFuture + pub fn block(f: F) -> CpuFuture where F: FnOnce() -> Result + Send + 'static, I: Send + 'static, From 88e5059910bfc54be07b52043c2af97a1fa9790d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Mar 2019 15:37:39 -0800 Subject: [PATCH 058/109] add doc string to guards --- src/guard.rs | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/guard.rs b/src/guard.rs index 1632b997..f9565d0f 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -1,4 +1,30 @@ //! Route match guards. +//! +//! Guards are one of the way how actix-web router chooses +//! handler service. In essence it just function that accepts +//! reference to a `RequestHead` instance and returns boolean. +//! It is possible to add guards to *scopes*, *resources* +//! and *routes*. Actix provide several guards by default, like various +//! http methods, header, etc. To become a guard, type must implement `Guard` +//! trait. Simple functions coulds guards as well. +//! +//! Guard can not modify request object. But it is possible to +//! to store extra attributes on a request by using `Extensions` container. +//! Extensions container available via `RequestHead::extensions()` method. +//! +//! ```rust +//! use actix_web::{web, http, dev, guard, App, HttpResponse}; +//! +//! fn main() { +//! App::new().service(web::resource("/index.html").route( +//! web::route() +//! .guard(guard::Post()) +//! .guard(|head: &dev::RequestHead| head.method == http::Method::GET) +//! .to(|| HttpResponse::MethodNotAllowed())) +//! ); +//! } +//! ``` + #![allow(non_snake_case)] use actix_http::http::{self, header, HttpTryFrom}; use actix_http::RequestHead; @@ -13,6 +39,18 @@ pub trait Guard { fn check(&self, request: &RequestHead) -> bool; } +#[doc(hidden)] +pub struct FnGuard bool + 'static>(F); + +impl Guard for F +where + F: Fn(&RequestHead) -> bool + 'static, +{ + fn check(&self, head: &RequestHead) -> bool { + (*self)(head) + } +} + /// Return guard that matches if any of supplied guards. /// /// ```rust From eef687ec80897b476498a70f037472bef22a3994 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Mar 2019 15:51:24 -0800 Subject: [PATCH 059/109] remove unneeded methods --- src/lib.rs | 29 +++++++++++++++++------------ src/responder.rs | 2 +- src/route.rs | 20 -------------------- src/scope.rs | 2 +- 4 files changed, 19 insertions(+), 34 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6e809fb6..a61387e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,7 @@ pub use actix_web_codegen::*; // re-export for convenience pub use actix_http::Response as HttpResponse; -pub use actix_http::{body, error, http, Error, HttpMessage, ResponseError, Result}; +pub use actix_http::{error, http, Error, HttpMessage, ResponseError, Result}; pub use crate::app::App; pub use crate::extract::FromRequest; @@ -154,37 +154,42 @@ pub mod web { Scope::new(path) } - /// Create **route** without configuration. + /// Create *route* without configuration. pub fn route() -> Route

    { Route::new() } - /// Create **route** with `GET` method guard. + /// Create *route* with `GET` method guard. pub fn get() -> Route

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

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

    { - Route::put() + Route::new().method(Method::PUT) } - /// Create **route** with `DELETE` method guard. + /// Create *route* with `PATCH` method guard. + pub fn patch() -> Route

    { + Route::new().method(Method::PATCH) + } + + /// Create *route* with `DELETE` method guard. pub fn delete() -> Route

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

    { Route::new().method(Method::HEAD) } - /// Create **route** and add method guard. + /// Create *route* and add method guard. pub fn method(method: Method) -> Route

    { Route::new().method(method) } diff --git a/src/responder.rs b/src/responder.rs index 9e9e0f10..6dce300a 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -291,7 +291,7 @@ mod tests { use actix_service::Service; use bytes::Bytes; - use crate::body::{Body, ResponseBody}; + use crate::dev::{Body, ResponseBody}; use crate::http::StatusCode; use crate::test::{init_service, TestRequest}; use crate::{web, App}; diff --git a/src/route.rs b/src/route.rs index d1a8320d..f7b99050 100644 --- a/src/route.rs +++ b/src/route.rs @@ -60,26 +60,6 @@ impl Route

    { } } - /// Create new `GET` route. - pub fn get() -> Route

    { - Route::new().method(Method::GET) - } - - /// Create new `POST` route. - pub fn post() -> Route

    { - Route::new().method(Method::POST) - } - - /// Create new `PUT` route. - pub fn put() -> Route

    { - Route::new().method(Method::PUT) - } - - /// Create new `DELETE` route. - pub fn delete() -> Route

    { - Route::new().method(Method::DELETE) - } - pub(crate) fn finish(self) -> Self { *self.config_ref.borrow_mut() = self.config.storage.clone(); self diff --git a/src/scope.rs b/src/scope.rs index a6358869..2c2e3c2b 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -451,7 +451,7 @@ mod tests { use actix_service::Service; use bytes::Bytes; - use crate::body::{Body, ResponseBody}; + use crate::dev::{Body, ResponseBody}; use crate::http::{Method, StatusCode}; use crate::test::{block_on, init_service, TestRequest}; use crate::{guard, web, App, HttpRequest, HttpResponse}; From 2f6df111832112ad012687557f66b112ef0d1ed1 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 7 Mar 2019 19:31:17 -0800 Subject: [PATCH 060/109] do not execute blocking fn if result is not required --- src/blocking.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/blocking.rs b/src/blocking.rs index 01be30dd..fc9cec29 100644 --- a/src/blocking.rs +++ b/src/blocking.rs @@ -41,6 +41,7 @@ thread_local! { }; } +/// Blocking operation execution error #[derive(Debug, Display)] pub enum BlockingError { #[display(fmt = "{:?}", _0)] @@ -62,13 +63,17 @@ where let (tx, rx) = oneshot::channel(); POOL.with(|pool| { pool.execute(move || { - let _ = tx.send(f()); + if !tx.is_canceled() { + let _ = tx.send(f()); + } }) }); CpuFuture { rx } } +/// Blocking operation completion future. It resolves with results +/// of blocking function execution. pub struct CpuFuture { rx: oneshot::Receiver>, } From aadcdaa3d6b109bc169e2d083bad8817594b789f Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 07:39:34 -0800 Subject: [PATCH 061/109] add resource map, it allow to check if router has resource and it allows to generate urls for named resources --- Cargo.toml | 3 + actix-files/src/lib.rs | 31 +++++++ src/app.rs | 24 +++++- src/config.rs | 17 +++- src/error.rs | 20 +++++ src/lib.rs | 4 +- src/request.rs | 46 ++++++++++ src/resource.rs | 2 +- src/rmap.rs | 188 +++++++++++++++++++++++++++++++++++++++++ src/scope.rs | 19 ++++- src/server.rs | 8 +- src/service.rs | 4 +- src/test.rs | 13 ++- 13 files changed, 361 insertions(+), 18 deletions(-) create mode 100644 src/error.rs create mode 100644 src/rmap.rs diff --git a/Cargo.toml b/Cargo.toml index f9e2266e..5e2027c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,11 +72,13 @@ actix-utils = { git = "https://github.com/actix/actix-net.git" } actix-http = { git = "https://github.com/actix/actix-http.git" } actix-router = { git = "https://github.com/actix/actix-net.git" } actix-server = { git = "https://github.com/actix/actix-net.git" } +#actix-router = { path="../actix-net/router" } bytes = "0.4" derive_more = "0.14" encoding = "0.2" futures = "0.1" +hashbrown = "0.1.8" log = "0.4" lazy_static = "1.2" mime = "0.3" @@ -89,6 +91,7 @@ serde_json = "1.0" serde_urlencoded = "^0.5.3" threadpool = "1.7" time = "0.1" +url = { version="1.7", features=["query_encoding"] } # middlewares # actix-session = { path="session", optional = true } diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 5dd98dcc..17efdd81 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -1090,4 +1090,35 @@ mod tests { // assert_eq!(response.status(), StatusCode::OK); // } + #[test] + fn test_path_buf() { + assert_eq!( + PathBuf::from_param("/test/.tt"), + Err(UriSegmentError::BadStart('.')) + ); + assert_eq!( + PathBuf::from_param("/test/*tt"), + Err(UriSegmentError::BadStart('*')) + ); + assert_eq!( + PathBuf::from_param("/test/tt:"), + Err(UriSegmentError::BadEnd(':')) + ); + assert_eq!( + PathBuf::from_param("/test/tt<"), + Err(UriSegmentError::BadEnd('<')) + ); + assert_eq!( + PathBuf::from_param("/test/tt>"), + Err(UriSegmentError::BadEnd('>')) + ); + assert_eq!( + PathBuf::from_param("/seg1/seg2/"), + Ok(PathBuf::from_iter(vec!["seg1", "seg2"])) + ); + assert_eq!( + PathBuf::from_param("/seg1/../seg2/"), + Ok(PathBuf::from_iter(vec!["seg2"])) + ); + } } diff --git a/src/app.rs b/src/app.rs index c1c019a3..b4f6e535 100644 --- a/src/app.rs +++ b/src/app.rs @@ -16,6 +16,7 @@ use futures::{Async, Future, IntoFuture, Poll}; use crate::config::AppConfig; use crate::guard::Guard; use crate::resource::Resource; +use crate::rmap::ResourceMap; use crate::route::Route; use crate::service::{ HttpServiceFactory, ServiceFactory, ServiceFactoryWrapper, ServiceRequest, @@ -449,19 +450,29 @@ where .into_iter() .for_each(|mut srv| srv.register(&mut config)); - // set factory + let mut rmap = ResourceMap::new(ResourceDef::new("")); + + // complete pipeline creation *self.factory_ref.borrow_mut() = Some(AppRoutingFactory { default, services: Rc::new( config .into_services() .into_iter() - .map(|(rdef, srv, guards)| (rdef, srv, RefCell::new(guards))) + .map(|(mut rdef, srv, guards, nested)| { + rmap.add(&mut rdef, nested); + (rdef, srv, RefCell::new(guards)) + }) .collect(), ), }); + // complete ResourceMap tree creation + let rmap = Rc::new(rmap); + rmap.finish(rmap.clone()); + AppInit { + rmap, chain: self.chain, state: self.state, extensions: Rc::new(RefCell::new(Rc::new(self.extensions))), @@ -561,8 +572,7 @@ impl

    Future for AppRoutingFactoryResponse

    { .fold(Router::build(), |mut router, item| { match item { CreateAppRoutingItem::Service(path, guards, service) => { - router.rdef(path, service); - router.set_user_data(guards); + router.rdef(path, service).2 = guards; } CreateAppRoutingItem::Future(_, _, _) => unreachable!(), } @@ -683,6 +693,7 @@ where C: NewService>, { chain: C, + rmap: Rc, state: Vec>, extensions: Rc>>, } @@ -702,6 +713,7 @@ where chain: self.chain.new_service(&()), state: self.state.iter().map(|s| s.construct()).collect(), extensions: self.extensions.clone(), + rmap: self.rmap.clone(), } } } @@ -712,6 +724,7 @@ where C: NewService, InitError = ()>, { chain: C::Future, + rmap: Rc, state: Vec>, extensions: Rc>>, } @@ -744,6 +757,7 @@ where Ok(Async::Ready(AppInitService { chain, + rmap: self.rmap.clone(), extensions: self.extensions.borrow().clone(), })) } @@ -755,6 +769,7 @@ where C: Service>, { chain: C, + rmap: Rc, extensions: Rc, } @@ -774,6 +789,7 @@ where let req = ServiceRequest::new( Path::new(Url::new(req.uri().clone())), req, + self.rmap.clone(), self.extensions.clone(), ); self.chain.call(req) diff --git a/src/config.rs b/src/config.rs index 483b0a50..4afd213c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,6 +5,7 @@ use actix_router::ResourceDef; use actix_service::{boxed, IntoNewService, NewService}; use crate::guard::Guard; +use crate::rmap::ResourceMap; use crate::service::{ServiceRequest, ServiceResponse}; type Guards = Vec>; @@ -18,7 +19,12 @@ pub struct AppConfig

    { host: String, root: bool, default: Rc>, - services: Vec<(ResourceDef, HttpNewService

    , Option)>, + services: Vec<( + ResourceDef, + HttpNewService

    , + Option, + Option>, + )>, } impl AppConfig

    { @@ -46,7 +52,12 @@ impl AppConfig

    { pub(crate) fn into_services( self, - ) -> Vec<(ResourceDef, HttpNewService

    , Option)> { + ) -> Vec<( + ResourceDef, + HttpNewService

    , + Option, + Option>, + )> { self.services } @@ -85,6 +96,7 @@ impl AppConfig

    { rdef: ResourceDef, guards: Option>>, service: F, + nested: Option>, ) where F: IntoNewService>, S: NewService< @@ -98,6 +110,7 @@ impl AppConfig

    { rdef, boxed::new_service(service.into_new_service()), guards, + nested, )); } } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 00000000..d1c0d3ca --- /dev/null +++ b/src/error.rs @@ -0,0 +1,20 @@ +pub use actix_http::error::*; +use derive_more::{Display, From}; +use url::ParseError as UrlParseError; + +/// Errors which can occur when attempting to generate resource uri. +#[derive(Debug, PartialEq, Display, From)] +pub enum UrlGenerationError { + /// Resource not found + #[display(fmt = "Resource not found")] + ResourceNotFound, + /// Not all path pattern covered + #[display(fmt = "Not all path pattern covered")] + NotEnoughElements, + /// URL parse error + #[display(fmt = "{}", _0)] + ParseError(UrlParseError), +} + +/// `InternalServerError` for `UrlGeneratorError` +impl ResponseError for UrlGenerationError {} diff --git a/src/lib.rs b/src/lib.rs index a61387e8..94bf1ba7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,11 +6,13 @@ mod handler; // mod info; pub mod blocking; mod config; +pub mod error; pub mod guard; pub mod middleware; mod request; mod resource; mod responder; +mod rmap; mod route; mod scope; mod server; @@ -27,7 +29,7 @@ pub use actix_web_codegen::*; // re-export for convenience pub use actix_http::Response as HttpResponse; -pub use actix_http::{error, http, Error, HttpMessage, ResponseError, Result}; +pub use actix_http::{http, Error, HttpMessage, ResponseError, Result}; pub use crate::app::App; pub use crate::extract::FromRequest; diff --git a/src/request.rs b/src/request.rs index 1c86cac3..6655f1ba 100644 --- a/src/request.rs +++ b/src/request.rs @@ -7,7 +7,9 @@ use actix_http::http::{HeaderMap, Method, Uri, Version}; use actix_http::{Error, Extensions, HttpMessage, Message, Payload, RequestHead}; use actix_router::{Path, Url}; +use crate::error::UrlGenerationError; use crate::extract::FromRequest; +use crate::rmap::ResourceMap; use crate::service::ServiceFromRequest; #[derive(Clone)] @@ -15,6 +17,7 @@ use crate::service::ServiceFromRequest; pub struct HttpRequest { pub(crate) head: Message, pub(crate) path: Path, + rmap: Rc, extensions: Rc, } @@ -23,11 +26,13 @@ impl HttpRequest { pub(crate) fn new( head: Message, path: Path, + rmap: Rc, extensions: Rc, ) -> HttpRequest { HttpRequest { head, path, + rmap, extensions, } } @@ -93,6 +98,47 @@ impl HttpRequest { &self.extensions } + /// Generate url for named resource + /// + /// ```rust + /// # extern crate actix_web; + /// # use actix_web::{App, HttpRequest, HttpResponse, http}; + /// # + /// fn index(req: HttpRequest) -> HttpResponse { + /// let url = req.url_for("foo", &["1", "2", "3"]); // <- generate url for "foo" resource + /// HttpResponse::Ok().into() + /// } + /// + /// fn main() { + /// let app = App::new() + /// .resource("/test/{one}/{two}/{three}", |r| { + /// r.name("foo"); // <- set resource name, then it could be used in `url_for` + /// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); + /// }) + /// .finish(); + /// } + /// ``` + pub fn url_for( + &self, + name: &str, + elements: U, + ) -> Result + where + U: IntoIterator, + I: AsRef, + { + self.rmap.url_for(&self, name, elements) + } + + /// Generate url for named resource + /// + /// This method is similar to `HttpRequest::url_for()` but it can be used + /// for urls that do not contain variable parts. + pub fn url_for_static(&self, name: &str) -> Result { + const NO_PARAMS: [&str; 0] = []; + self.url_for(name, &NO_PARAMS) + } + // /// Get *ConnectionInfo* for the correct request. // #[inline] // pub fn connection_info(&self) -> Ref { diff --git a/src/resource.rs b/src/resource.rs index 13afff70..a1177ca0 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -288,7 +288,7 @@ where } else { ResourceDef::new(&self.rdef) }; - config.register_service(rdef, guards, self) + config.register_service(rdef, guards, self, None) } } diff --git a/src/rmap.rs b/src/rmap.rs new file mode 100644 index 00000000..4922084b --- /dev/null +++ b/src/rmap.rs @@ -0,0 +1,188 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use actix_router::ResourceDef; +use hashbrown::HashMap; +use url::Url; + +use crate::error::UrlGenerationError; +use crate::request::HttpRequest; + +#[derive(Clone, Debug)] +pub struct ResourceMap { + root: ResourceDef, + parent: RefCell>>, + named: HashMap, + patterns: Vec<(ResourceDef, Option>)>, +} + +impl ResourceMap { + pub fn new(root: ResourceDef) -> Self { + ResourceMap { + root, + parent: RefCell::new(None), + named: HashMap::new(), + patterns: Vec::new(), + } + } + + pub fn add(&mut self, pattern: &mut ResourceDef, nested: Option>) { + pattern.set_id(self.patterns.len() as u16); + self.patterns.push((pattern.clone(), nested)); + if !pattern.name().is_empty() { + self.named + .insert(pattern.name().to_string(), pattern.clone()); + } + } + + pub(crate) fn finish(&self, current: Rc) { + for (_, nested) in &self.patterns { + if let Some(ref nested) = nested { + *nested.parent.borrow_mut() = Some(current.clone()) + } + } + } +} + +impl ResourceMap { + /// Generate url for named resource + /// + /// Check [`HttpRequest::url_for()`](../struct.HttpRequest.html#method. + /// url_for) for detailed information. + pub fn url_for( + &self, + req: &HttpRequest, + name: &str, + elements: U, + ) -> Result + where + U: IntoIterator, + I: AsRef, + { + let mut path = String::new(); + let mut elements = elements.into_iter(); + + if self.patterns_for(name, &mut path, &mut elements)?.is_some() { + if path.starts_with('/') { + // let conn = req.connection_info(); + // Ok(Url::parse(&format!( + // "{}://{}{}", + // conn.scheme(), + // conn.host(), + // path + // ))?) + unimplemented!() + } else { + Ok(Url::parse(&path)?) + } + } else { + Err(UrlGenerationError::ResourceNotFound) + } + } + + pub fn has_resource(&self, path: &str) -> bool { + let path = if path.is_empty() { "/" } else { path }; + + for (pattern, rmap) in &self.patterns { + if let Some(ref rmap) = rmap { + if let Some(plen) = pattern.is_prefix_match(path) { + return rmap.has_resource(&path[plen..]); + } + } else if pattern.is_match(path) { + return true; + } + } + false + } + + fn patterns_for( + &self, + name: &str, + path: &mut String, + elements: &mut U, + ) -> Result, UrlGenerationError> + where + U: Iterator, + I: AsRef, + { + if self.pattern_for(name, path, elements)?.is_some() { + Ok(Some(())) + } else { + self.parent_pattern_for(name, path, elements) + } + } + + fn pattern_for( + &self, + name: &str, + path: &mut String, + elements: &mut U, + ) -> Result, UrlGenerationError> + where + U: Iterator, + I: AsRef, + { + if let Some(pattern) = self.named.get(name) { + self.fill_root(path, elements)?; + if pattern.resource_path(path, elements) { + Ok(Some(())) + } else { + Err(UrlGenerationError::NotEnoughElements) + } + } else { + for (_, rmap) in &self.patterns { + if let Some(ref rmap) = rmap { + if rmap.pattern_for(name, path, elements)?.is_some() { + return Ok(Some(())); + } + } + } + Ok(None) + } + } + + fn fill_root( + &self, + path: &mut String, + elements: &mut U, + ) -> Result<(), UrlGenerationError> + where + U: Iterator, + I: AsRef, + { + if let Some(ref parent) = *self.parent.borrow() { + parent.fill_root(path, elements)?; + } + if self.root.resource_path(path, elements) { + Ok(()) + } else { + Err(UrlGenerationError::NotEnoughElements) + } + } + + fn parent_pattern_for( + &self, + name: &str, + path: &mut String, + elements: &mut U, + ) -> Result, UrlGenerationError> + where + U: Iterator, + I: AsRef, + { + if let Some(ref parent) = *self.parent.borrow() { + if let Some(pattern) = parent.named.get(name) { + self.fill_root(path, elements)?; + if pattern.resource_path(path, elements) { + Ok(Some(())) + } else { + Err(UrlGenerationError::NotEnoughElements) + } + } else { + parent.parent_pattern_for(name, path, elements) + } + } else { + Ok(None) + } + } +} diff --git a/src/scope.rs b/src/scope.rs index 2c2e3c2b..15c652c8 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -13,6 +13,7 @@ use futures::{Async, Poll}; use crate::dev::{AppConfig, HttpServiceFactory}; use crate::guard::Guard; use crate::resource::Resource; +use crate::rmap::ResourceMap; use crate::route::Route; use crate::service::{ ServiceFactory, ServiceFactoryWrapper, ServiceRequest, ServiceResponse, @@ -237,35 +238,46 @@ where > + 'static, { fn register(self, config: &mut AppConfig

    ) { + // update default resource if needed if self.default.borrow().is_none() { *self.default.borrow_mut() = Some(config.default_service()); } - // register services + // register nested services let mut cfg = config.clone_config(); self.services .into_iter() .for_each(|mut srv| srv.register(&mut cfg)); + let mut rmap = ResourceMap::new(ResourceDef::root_prefix(&self.rdef)); + + // complete scope pipeline creation *self.factory_ref.borrow_mut() = Some(ScopeFactory { default: self.default.clone(), services: Rc::new( cfg.into_services() .into_iter() - .map(|(rdef, srv, guards)| (rdef, srv, RefCell::new(guards))) + .map(|(mut rdef, srv, guards, nested)| { + rmap.add(&mut rdef, nested); + (rdef, srv, RefCell::new(guards)) + }) .collect(), ), }); + // get guards let guards = if self.guards.is_empty() { None } else { Some(self.guards) }; + + // register final service config.register_service( ResourceDef::root_prefix(&self.rdef), guards, self.endpoint, + Some(Rc::new(rmap)), ) } } @@ -367,8 +379,7 @@ impl

    Future for ScopeFactoryResponse

    { .fold(Router::build(), |mut router, item| { match item { CreateScopeServiceItem::Service(path, guards, service) => { - router.rdef(path, service); - router.set_user_data(guards); + router.rdef(path, service).2 = guards; } CreateScopeServiceItem::Future(_, _, _) => unreachable!(), } diff --git a/src/server.rs b/src/server.rs index d6d88d00..d3ae5b2b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -230,7 +230,7 @@ where /// /// HttpServer does not change any configuration for TcpListener, /// it needs to be configured before passing it to listen() method. - pub fn listen(mut self, lst: net::TcpListener) -> Self { + pub fn listen(mut self, lst: net::TcpListener) -> io::Result { let cfg = self.config.clone(); let factory = self.factory.clone(); let addr = lst.local_addr().unwrap(); @@ -248,9 +248,9 @@ where ServiceConfig::new(c.keep_alive, c.client_timeout, 0); HttpService::with_config(service_config, factory()) }, - )); + )?); - self + Ok(self) } #[cfg(feature = "tls")] @@ -328,7 +328,7 @@ where let sockets = self.bind2(addr)?; for lst in sockets { - self = self.listen(lst); + self = self.listen(lst)?; } Ok(self) diff --git a/src/service.rs b/src/service.rs index e2213060..ba811458 100644 --- a/src/service.rs +++ b/src/service.rs @@ -15,6 +15,7 @@ use futures::future::{ok, FutureResult, IntoFuture}; use crate::config::AppConfig; use crate::request::HttpRequest; +use crate::rmap::ResourceMap; pub trait HttpServiceFactory

    { fn register(self, config: &mut AppConfig

    ); @@ -58,12 +59,13 @@ impl

    ServiceRequest

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

    , + rmap: Rc, extensions: Rc, ) -> Self { let (head, payload) = request.into_parts(); ServiceRequest { payload, - req: HttpRequest::new(head, path, extensions), + req: HttpRequest::new(head, path, rmap, extensions), } } diff --git a/src/test.rs b/src/test.rs index ccc4b38e..e4cdefbe 100644 --- a/src/test.rs +++ b/src/test.rs @@ -6,13 +6,14 @@ use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; use actix_http::http::{HttpTryFrom, Method, Version}; use actix_http::test::TestRequest as HttpTestRequest; use actix_http::{Extensions, PayloadStream, Request}; -use actix_router::{Path, Url}; +use actix_router::{Path, ResourceDef, Url}; use actix_rt::Runtime; use actix_service::{IntoNewService, NewService, Service}; use bytes::Bytes; use futures::Future; use crate::request::HttpRequest; +use crate::rmap::ResourceMap; use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; thread_local! { @@ -135,6 +136,7 @@ where pub struct TestRequest { req: HttpTestRequest, extensions: Extensions, + rmap: ResourceMap, } impl Default for TestRequest { @@ -142,6 +144,7 @@ impl Default for TestRequest { TestRequest { req: HttpTestRequest::default(), extensions: Extensions::new(), + rmap: ResourceMap::new(ResourceDef::new("")), } } } @@ -152,6 +155,7 @@ impl TestRequest { TestRequest { req: HttpTestRequest::default().uri(path).take(), extensions: Extensions::new(), + rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -160,6 +164,7 @@ impl TestRequest { TestRequest { req: HttpTestRequest::default().set(hdr).take(), extensions: Extensions::new(), + rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -172,6 +177,7 @@ impl TestRequest { TestRequest { req: HttpTestRequest::default().header(key, value).take(), extensions: Extensions::new(), + rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -180,6 +186,7 @@ impl TestRequest { TestRequest { req: HttpTestRequest::default().method(Method::GET).take(), extensions: Extensions::new(), + rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -188,6 +195,7 @@ impl TestRequest { TestRequest { req: HttpTestRequest::default().method(Method::POST).take(), extensions: Extensions::new(), + rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -244,6 +252,7 @@ impl TestRequest { ServiceRequest::new( Path::new(Url::new(req.uri().clone())), req, + Rc::new(self.rmap), Rc::new(self.extensions), ) } @@ -260,6 +269,7 @@ impl TestRequest { ServiceRequest::new( Path::new(Url::new(req.uri().clone())), req, + Rc::new(self.rmap), Rc::new(self.extensions), ) .into_request() @@ -272,6 +282,7 @@ impl TestRequest { let req = ServiceRequest::new( Path::new(Url::new(req.uri().clone())), req, + Rc::new(self.rmap), Rc::new(self.extensions), ); ServiceFromRequest::new(req, None) From fde55ffa14884e045590efb7135171f722cbde1d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 09:49:11 -0800 Subject: [PATCH 062/109] revert generic request parameter for service; support ServerConfig as new factory config --- Cargo.toml | 2 +- src/app.rs | 419 +++-------------------------- src/app_service.rs | 439 +++++++++++++++++++++++++++++++ src/config.rs | 4 +- src/handler.rs | 18 +- src/lib.rs | 1 + src/middleware/compress.rs | 17 +- src/middleware/defaultheaders.rs | 10 +- src/middleware/logger.rs | 18 +- src/resource.rs | 32 ++- src/route.rs | 30 ++- src/scope.rs | 21 +- src/server.rs | 13 +- src/test.rs | 17 +- 14 files changed, 581 insertions(+), 460 deletions(-) create mode 100644 src/app_service.rs diff --git a/Cargo.toml b/Cargo.toml index 5e2027c5..dbc0a65d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ actix-utils = { git = "https://github.com/actix/actix-net.git" } actix-http = { git = "https://github.com/actix/actix-http.git" } actix-router = { git = "https://github.com/actix/actix-net.git" } actix-server = { git = "https://github.com/actix/actix-net.git" } -#actix-router = { path="../actix-net/router" } +actix-server-config = { git = "https://github.com/actix/actix-net.git" } bytes = "0.4" derive_more = "0.14" diff --git a/src/app.rs b/src/app.rs index b4f6e535..76a3a1ce 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,37 +3,30 @@ use std::marker::PhantomData; use std::rc::Rc; use actix_http::body::{Body, MessageBody}; -use actix_http::{Extensions, PayloadStream, Request, Response}; -use actix_router::{Path, ResourceDef, ResourceInfo, Router, Url}; -use actix_service::boxed::{self, BoxedNewService, BoxedService}; +use actix_http::{Extensions, PayloadStream}; +use actix_server_config::ServerConfig; +use actix_service::boxed::{self, BoxedNewService}; use actix_service::{ - fn_service, AndThenNewService, ApplyTransform, IntoNewService, IntoTransform, - NewService, Service, Transform, + ApplyTransform, IntoNewService, IntoTransform, NewService, Transform, }; -use futures::future::{ok, Either, FutureResult}; -use futures::{Async, Future, IntoFuture, Poll}; +use futures::IntoFuture; -use crate::config::AppConfig; -use crate::guard::Guard; +use crate::app_service::{AppChain, AppEntry, AppInit, AppRouting, AppRoutingFactory}; use crate::resource::Resource; -use crate::rmap::ResourceMap; use crate::route::Route; use crate::service::{ HttpServiceFactory, ServiceFactory, ServiceFactoryWrapper, ServiceRequest, ServiceResponse, }; -use crate::state::{State, StateFactory, StateFactoryResult}; +use crate::state::{State, StateFactory}; -type Guards = Vec>; -type HttpService

    = BoxedService, ServiceResponse, ()>; type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; -type BoxedResponse = Box>; /// Application builder - structure that follows the builder pattern /// for building application instances. pub struct App where - T: NewService>, + T: NewService>, { chain: T, extensions: Extensions, @@ -58,7 +51,7 @@ impl App where P: 'static, T: NewService< - ServiceRequest, + Request = ServiceRequest, Response = ServiceRequest

    , Error = (), InitError = (), @@ -121,7 +114,7 @@ where P, B, impl NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -130,12 +123,12 @@ where where M: Transform< AppRouting

    , - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), >, - F: IntoTransform, ServiceRequest

    >, + F: IntoTransform>, { let fref = Rc::new(RefCell::new(None)); let endpoint = ApplyTransform::new(mw, AppEntry::new(fref.clone())); @@ -159,7 +152,7 @@ where ) -> App< P1, impl NewService< - ServiceRequest, + Request = ServiceRequest, Response = ServiceRequest, Error = (), InitError = (), @@ -167,12 +160,12 @@ where > where C: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceRequest, Error = (), InitError = (), >, - F: IntoNewService>, + F: IntoNewService, { let chain = self.chain.and_then(chain.into_new_service()); App { @@ -264,7 +257,7 @@ where P: 'static, B: MessageBody, T: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -324,7 +317,7 @@ where P, B1, impl NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -333,13 +326,13 @@ where where M: Transform< T::Service, - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), >, B1: MessageBody, - F: IntoTransform>, + F: IntoTransform, { let endpoint = ApplyTransform::new(mw, self.endpoint); AppRouter { @@ -362,7 +355,7 @@ where where F: FnOnce(Resource

    ) -> Resource, U: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -413,391 +406,39 @@ where } } -impl - IntoNewService, T>, Request> +impl IntoNewService, ServerConfig> for AppRouter where T: NewService< - ServiceRequest

    , + Request = ServiceRequest

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

    , Error = (), InitError = (), >, { - fn into_new_service(self) -> AndThenNewService, T> { - // update resource default service - let default = self.default.unwrap_or_else(|| { - Rc::new(boxed::new_service(fn_service(|req: ServiceRequest

    | { - Ok(req.into_response(Response::NotFound().finish())) - }))) - }); - - let mut config = AppConfig::new( - "127.0.0.1:8080".parse().unwrap(), - "localhost:8080".to_owned(), - false, - default.clone(), - ); - - // register services - self.services - .into_iter() - .for_each(|mut srv| srv.register(&mut config)); - - let mut rmap = ResourceMap::new(ResourceDef::new("")); - - // complete pipeline creation - *self.factory_ref.borrow_mut() = Some(AppRoutingFactory { - default, - services: Rc::new( - config - .into_services() - .into_iter() - .map(|(mut rdef, srv, guards, nested)| { - rmap.add(&mut rdef, nested); - (rdef, srv, RefCell::new(guards)) - }) - .collect(), - ), - }); - - // complete ResourceMap tree creation - let rmap = Rc::new(rmap); - rmap.finish(rmap.clone()); - + fn into_new_service(self) -> AppInit { AppInit { - rmap, chain: self.chain, state: self.state, + endpoint: self.endpoint, + services: RefCell::new(self.services), + default: self.default, + factory_ref: self.factory_ref, extensions: Rc::new(RefCell::new(Rc::new(self.extensions))), } - .and_then(self.endpoint) - } -} - -pub struct AppRoutingFactory

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

    { - type Response = ServiceResponse; - type Error = (); - type InitError = (); - type Service = AppRouting

    ; - type Future = AppRoutingFactoryResponse

    ; - - fn new_service(&self, _: &()) -> Self::Future { - AppRoutingFactoryResponse { - fut: self - .services - .iter() - .map(|(path, service, guards)| { - CreateAppRoutingItem::Future( - Some(path.clone()), - guards.borrow_mut().take(), - service.new_service(&()), - ) - }) - .collect(), - default: None, - default_fut: Some(self.default.new_service(&())), - } - } -} - -type HttpServiceFut

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

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

    { - Future(Option, Option, HttpServiceFut

    ), - Service(ResourceDef, Option, HttpService

    ), -} - -impl

    Future for AppRoutingFactoryResponse

    { - type Item = AppRouting

    ; - type Error = (); - - fn poll(&mut self) -> Poll { - let mut done = true; - - if let Some(ref mut fut) = self.default_fut { - match fut.poll()? { - Async::Ready(default) => self.default = Some(default), - Async::NotReady => done = false, - } - } - - // poll http services - for item in &mut self.fut { - let res = match item { - CreateAppRoutingItem::Future( - ref mut path, - ref mut guards, - ref mut fut, - ) => match fut.poll()? { - Async::Ready(service) => { - Some((path.take().unwrap(), guards.take(), service)) - } - Async::NotReady => { - done = false; - None - } - }, - CreateAppRoutingItem::Service(_, _, _) => continue, - }; - - if let Some((path, guards, service)) = res { - *item = CreateAppRoutingItem::Service(path, guards, service); - } - } - - if done { - let router = self - .fut - .drain(..) - .fold(Router::build(), |mut router, item| { - match item { - CreateAppRoutingItem::Service(path, guards, service) => { - router.rdef(path, service).2 = guards; - } - CreateAppRoutingItem::Future(_, _, _) => unreachable!(), - } - router - }); - Ok(Async::Ready(AppRouting { - ready: None, - router: router.finish(), - default: self.default.take(), - })) - } else { - Ok(Async::NotReady) - } - } -} - -pub struct AppRouting

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

    , ResourceInfo)>, - default: Option>, -} - -impl

    Service> for AppRouting

    { - type Response = ServiceResponse; - type Error = (); - type Future = Either>; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - if self.ready.is_none() { - Ok(Async::Ready(())) - } else { - Ok(Async::NotReady) - } - } - - 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 { - if !f.check(req.head()) { - return false; - } - } - } - true - }); - - if let Some((srv, _info)) = res { - Either::A(srv.call(req)) - } else if let Some(ref mut default) = self.default { - Either::A(default.call(req)) - } else { - let req = req.into_request(); - Either::B(ok(ServiceResponse::new(req, Response::NotFound().finish()))) - } - } -} - -#[doc(hidden)] -/// Wrapper service for routing -pub struct AppEntry

    { - factory: Rc>>>, -} - -impl

    AppEntry

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

    { - type Response = ServiceResponse; - type Error = (); - type InitError = (); - 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 Response = ServiceRequest; - type Error = (); - type InitError = (); - type Service = AppChain; - type Future = FutureResult; - - fn new_service(&self, _: &()) -> Self::Future { - ok(AppChain) - } -} - -impl Service for AppChain { - type Response = ServiceRequest; - type 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) - } -} - -/// Service factory to convert `Request` to a `ServiceRequest`. -/// It also executes state factories. -pub struct AppInit -where - C: NewService>, -{ - chain: C, - rmap: Rc, - state: Vec>, - extensions: Rc>>, -} - -impl NewService for AppInit -where - C: NewService, InitError = ()>, -{ - type Response = ServiceRequest

    ; - type Error = C::Error; - type InitError = C::InitError; - type Service = AppInitService; - type Future = AppInitResult; - - fn new_service(&self, _: &()) -> Self::Future { - AppInitResult { - chain: self.chain.new_service(&()), - state: self.state.iter().map(|s| s.construct()).collect(), - extensions: self.extensions.clone(), - rmap: self.rmap.clone(), - } - } -} - -#[doc(hidden)] -pub struct AppInitResult -where - C: NewService, InitError = ()>, -{ - chain: C::Future, - rmap: Rc, - state: Vec>, - extensions: Rc>>, -} - -impl Future for AppInitResult -where - C: NewService, InitError = ()>, -{ - type Item = AppInitService; - type Error = C::InitError; - - fn poll(&mut self) -> Poll { - if let Some(extensions) = Rc::get_mut(&mut *self.extensions.borrow_mut()) { - let mut idx = 0; - while idx < self.state.len() { - if let Async::Ready(_) = self.state[idx].poll_result(extensions)? { - self.state.remove(idx); - } else { - idx += 1; - } - } - if !self.state.is_empty() { - return Ok(Async::NotReady); - } - } else { - log::warn!("Multiple copies of app extensions exists"); - } - - let chain = futures::try_ready!(self.chain.poll()); - - Ok(Async::Ready(AppInitService { - chain, - rmap: self.rmap.clone(), - extensions: self.extensions.borrow().clone(), - })) - } -} - -/// Service to convert `Request` to a `ServiceRequest` -pub struct AppInitService -where - C: Service>, -{ - chain: C, - rmap: Rc, - extensions: Rc, -} - -impl Service for AppInitService -where - C: Service>, -{ - type Response = ServiceRequest

    ; - type Error = C::Error; - type Future = C::Future; - - fn poll_ready(&mut self) -> Poll<(), Self::Error> { - self.chain.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.extensions.clone(), - ); - self.chain.call(req) } } #[cfg(test)] mod tests { + use actix_service::Service; + use super::*; use crate::http::{Method, StatusCode}; use crate::test::{block_on, init_service, TestRequest}; diff --git a/src/app_service.rs b/src/app_service.rs new file mode 100644 index 00000000..094486d9 --- /dev/null +++ b/src/app_service.rs @@ -0,0 +1,439 @@ +use std::cell::RefCell; +use std::marker::PhantomData; +use std::rc::Rc; + +use actix_http::{Extensions, 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 futures::future::{ok, Either, FutureResult}; +use futures::{Async, Future, Poll}; + +use crate::config::AppConfig; +use crate::guard::Guard; +use crate::rmap::ResourceMap; +use crate::service::{ServiceFactory, ServiceRequest, ServiceResponse}; +use crate::state::{StateFactory, StateFactoryResult}; + +type Guards = Vec>; +type HttpService

    = BoxedService, ServiceResponse, ()>; +type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; +type BoxedResponse = Box>; + +/// Service factory to convert `Request` to a `ServiceRequest`. +/// It also executes state factories. +pub struct AppInit +where + C: NewService>, + T: NewService< + Request = ServiceRequest

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

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

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

    | { + Ok(req.into_response(Response::NotFound().finish())) + }))) + }); + + let mut config = AppConfig::new( + "127.0.0.1:8080".parse().unwrap(), + "localhost:8080".to_owned(), + false, + default.clone(), + ); + + // register services + std::mem::replace(&mut *self.services.borrow_mut(), Vec::new()) + .into_iter() + .for_each(|mut srv| srv.register(&mut config)); + + let mut rmap = ResourceMap::new(ResourceDef::new("")); + + // complete pipeline creation + *self.factory_ref.borrow_mut() = Some(AppRoutingFactory { + default, + services: Rc::new( + config + .into_services() + .into_iter() + .map(|(mut rdef, srv, guards, nested)| { + rmap.add(&mut rdef, nested); + (rdef, srv, RefCell::new(guards)) + }) + .collect(), + ), + }); + + // complete ResourceMap tree creation + let rmap = Rc::new(rmap); + rmap.finish(rmap.clone()); + + AppInitResult { + chain: None, + chain_fut: self.chain.new_service(&()), + endpoint: None, + endpoint_fut: self.endpoint.new_service(&()), + state: self.state.iter().map(|s| s.construct()).collect(), + extensions: self.extensions.clone(), + rmap, + _t: PhantomData, + } + } +} + +pub struct AppInitResult +where + C: NewService, + T: NewService, +{ + chain: Option, + endpoint: Option, + chain_fut: C::Future, + endpoint_fut: T::Future, + rmap: Rc, + state: Vec>, + extensions: Rc>>, + _t: PhantomData<(P, B)>, +} + +impl Future for AppInitResult +where + C: NewService< + Request = ServiceRequest, + Response = ServiceRequest

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

    , + Response = ServiceResponse, + Error = (), + InitError = (), + >, +{ + type Item = AndThen, T::Service>; + type Error = C::InitError; + + fn poll(&mut self) -> Poll { + if let Some(extensions) = Rc::get_mut(&mut *self.extensions.borrow_mut()) { + let mut idx = 0; + while idx < self.state.len() { + if let Async::Ready(_) = self.state[idx].poll_result(extensions)? { + self.state.remove(idx); + } else { + idx += 1; + } + } + if !self.state.is_empty() { + return Ok(Async::NotReady); + } + } else { + log::warn!("Multiple copies of app extensions exists"); + } + + 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(), + extensions: self.extensions.borrow().clone(), + } + .and_then(self.endpoint.take().unwrap()), + )) + } else { + Ok(Async::NotReady) + } + } +} + +/// Service to convert `Request` to a `ServiceRequest` +pub struct AppInitService +where + C: Service, Error = ()>, +{ + chain: C, + rmap: Rc, + extensions: Rc, +} + +impl Service for AppInitService +where + C: Service, Error = ()>, +{ + type Request = Request; + type Response = ServiceRequest

    ; + type Error = C::Error; + type Future = C::Future; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.chain.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.extensions.clone(), + ); + self.chain.call(req) + } +} + +pub struct AppRoutingFactory

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

    { + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + type Service = AppRouting

    ; + type Future = AppRoutingFactoryResponse

    ; + + fn new_service(&self, _: &()) -> Self::Future { + AppRoutingFactoryResponse { + fut: self + .services + .iter() + .map(|(path, service, guards)| { + CreateAppRoutingItem::Future( + Some(path.clone()), + guards.borrow_mut().take(), + service.new_service(&()), + ) + }) + .collect(), + default: None, + default_fut: Some(self.default.new_service(&())), + } + } +} + +type HttpServiceFut

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

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

    { + Future(Option, Option, HttpServiceFut

    ), + Service(ResourceDef, Option, HttpService

    ), +} + +impl

    Future for AppRoutingFactoryResponse

    { + type Item = AppRouting

    ; + type Error = (); + + fn poll(&mut self) -> Poll { + let mut done = true; + + if let Some(ref mut fut) = self.default_fut { + match fut.poll()? { + Async::Ready(default) => self.default = Some(default), + Async::NotReady => done = false, + } + } + + // poll http services + for item in &mut self.fut { + let res = match item { + CreateAppRoutingItem::Future( + ref mut path, + ref mut guards, + ref mut fut, + ) => match fut.poll()? { + Async::Ready(service) => { + Some((path.take().unwrap(), guards.take(), service)) + } + Async::NotReady => { + done = false; + None + } + }, + CreateAppRoutingItem::Service(_, _, _) => continue, + }; + + if let Some((path, guards, service)) = res { + *item = CreateAppRoutingItem::Service(path, guards, service); + } + } + + if done { + let router = self + .fut + .drain(..) + .fold(Router::build(), |mut router, item| { + match item { + CreateAppRoutingItem::Service(path, guards, service) => { + router.rdef(path, service).2 = guards; + } + CreateAppRoutingItem::Future(_, _, _) => unreachable!(), + } + router + }); + Ok(Async::Ready(AppRouting { + ready: None, + router: router.finish(), + default: self.default.take(), + })) + } else { + Ok(Async::NotReady) + } + } +} + +pub struct AppRouting

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

    , ResourceInfo)>, + default: Option>, +} + +impl

    Service for AppRouting

    { + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = (); + type Future = Either>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + if self.ready.is_none() { + Ok(Async::Ready(())) + } else { + Ok(Async::NotReady) + } + } + + 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 { + if !f.check(req.head()) { + return false; + } + } + } + true + }); + + if let Some((srv, _info)) = res { + Either::A(srv.call(req)) + } else if let Some(ref mut default) = self.default { + Either::A(default.call(req)) + } else { + let req = req.into_request(); + Either::B(ok(ServiceResponse::new(req, Response::NotFound().finish()))) + } + } +} + +/// Wrapper service for routing +pub struct AppEntry

    { + factory: Rc>>>, +} + +impl

    AppEntry

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

    { + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = (); + type InitError = (); + 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 = (); + 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 = (); + 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 4afd213c..47c2f7c4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -98,9 +98,9 @@ impl AppConfig

    { service: F, nested: Option>, ) where - F: IntoNewService>, + F: IntoNewService, S: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), diff --git a/src/handler.rs b/src/handler.rs index 435d9a8b..87645651 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -52,11 +52,12 @@ where } } } -impl NewService<(T, HttpRequest)> for Handler +impl NewService for Handler where F: Factory, R: Responder + 'static, { + type Request = (T, HttpRequest); type Response = ServiceResponse; type Error = Void; type InitError = (); @@ -81,11 +82,12 @@ where _t: PhantomData<(T, R)>, } -impl Service<(T, HttpRequest)> for HandlerService +impl Service for HandlerService where F: Factory, R: Responder + 'static, { + type Request = (T, HttpRequest); type Response = ServiceResponse; type Error = Void; type Future = HandlerServiceResponse<::Future>; @@ -182,13 +184,14 @@ where } } } -impl NewService<(T, HttpRequest)> for AsyncHandler +impl NewService for AsyncHandler where F: AsyncFactory, R: IntoFuture, R::Item: Into, R::Error: Into, { + type Request = (T, HttpRequest); type Response = ServiceResponse; type Error = (); type InitError = (); @@ -215,13 +218,14 @@ where _t: PhantomData<(T, R)>, } -impl Service<(T, HttpRequest)> for AsyncHandlerService +impl Service for AsyncHandlerService where F: AsyncFactory, R: IntoFuture, R::Item: Into, R::Error: Into, { + type Request = (T, HttpRequest); type Response = ServiceResponse; type Error = (); type Future = AsyncHandlerServiceResponse; @@ -286,7 +290,8 @@ impl> Extract { } } -impl> NewService> for Extract { +impl> NewService for Extract { + type Request = ServiceRequest

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

    ); type InitError = (); @@ -306,7 +311,8 @@ pub struct ExtractService> { _t: PhantomData<(P, T)>, } -impl> Service> for ExtractService { +impl> Service for ExtractService { + type Request = ServiceRequest

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

    ); type Future = ExtractResponse; diff --git a/src/lib.rs b/src/lib.rs index 94bf1ba7..19f466b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ #![allow(clippy::type_complexity)] mod app; +mod app_service; mod extract; mod handler; // mod info; diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index c6f090a6..b3880a53 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -1,4 +1,5 @@ use std::io::Write; +use std::marker::PhantomData; use std::str::FromStr; use std::{cmp, fmt, io}; @@ -36,13 +37,14 @@ impl Default for Compress { } } -impl Transform> for Compress +impl Transform for Compress where P: 'static, B: MessageBody, - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, { + type Request = ServiceRequest

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

    ; type Response = ServiceResponse>; type Error = S::Error; type Future = CompressResponse; @@ -92,6 +95,7 @@ where CompressResponse { encoding, fut: self.service.call(req), + _t: PhantomData, } } } @@ -101,18 +105,19 @@ pub struct CompressResponse where P: 'static, B: MessageBody, - S: Service, Response = ServiceResponse>, + S: Service, S::Future: 'static, { fut: S::Future, encoding: ContentEncoding, + _t: PhantomData<(P, B)>, } impl Future for CompressResponse where P: 'static, B: MessageBody, - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, { type Item = ServiceResponse>; diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index f4def58d..b4927962 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -85,11 +85,12 @@ impl DefaultHeaders { } } -impl Transform> for DefaultHeaders +impl Transform for DefaultHeaders where - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, { + type Request = ServiceRequest

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

    ; type Response = ServiceResponse; type Error = S::Error; type Future = Box>; diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index d8b4e643..4af3e10d 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -2,6 +2,7 @@ use std::collections::HashSet; use std::env; use std::fmt::{self, Display, Formatter}; +use std::marker::PhantomData; use std::rc::Rc; use actix_service::{Service, Transform}; @@ -110,11 +111,12 @@ impl Default for Logger { } } -impl Transform> for Logger +impl Transform for Logger where - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, B: MessageBody, { + type Request = ServiceRequest

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

    ; type Response = ServiceResponse>; type Error = S::Error; type Future = LoggerResponse; @@ -154,6 +157,7 @@ where fut: self.service.call(req), format: None, time: time::now(), + _t: PhantomData, } } else { let now = time::now(); @@ -166,6 +170,7 @@ where fut: self.service.call(req), format: Some(format), time: now, + _t: PhantomData, } } } @@ -175,17 +180,18 @@ where pub struct LoggerResponse where B: MessageBody, - S: Service, Response = ServiceResponse>, + S: Service, { fut: S::Future, time: time::Tm, format: Option, + _t: PhantomData<(P, B)>, } impl Future for LoggerResponse where B: MessageBody, - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, { type Item = ServiceResponse>; type Error = S::Error; diff --git a/src/resource.rs b/src/resource.rs index a1177ca0..cc831665 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -66,7 +66,7 @@ impl Resource where P: 'static, T: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -220,7 +220,7 @@ where ) -> Resource< P, impl NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -229,12 +229,12 @@ where where M: Transform< T::Service, - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), >, - F: IntoTransform>, + F: IntoTransform, { let endpoint = ApplyTransform::new(mw, self.endpoint); Resource { @@ -251,9 +251,12 @@ where pub fn default_resource(mut self, f: F) -> Self where F: FnOnce(Resource

    ) -> R, - R: IntoNewService>, - U: NewService, Response = ServiceResponse, Error = ()> - + 'static, + R: IntoNewService, + U: NewService< + Request = ServiceRequest

    , + Response = ServiceResponse, + Error = (), + > + 'static, { // create and configure default resource self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::new_service( @@ -268,7 +271,7 @@ impl HttpServiceFactory

    for Resource where P: 'static, T: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -292,10 +295,10 @@ where } } -impl IntoNewService> for Resource +impl IntoNewService for Resource where T: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -316,7 +319,8 @@ pub struct ResourceFactory

    { default: Rc>>>>, } -impl NewService> for ResourceFactory

    { +impl NewService for ResourceFactory

    { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type InitError = (); @@ -406,7 +410,8 @@ pub struct ResourceService

    { default: Option>, } -impl

    Service> for ResourceService

    { +impl

    Service for ResourceService

    { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type Future = Either< @@ -450,7 +455,8 @@ impl

    ResourceEndpoint

    { } } -impl NewService> for ResourceEndpoint

    { +impl NewService for ResourceEndpoint

    { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type InitError = (); diff --git a/src/route.rs b/src/route.rs index f7b99050..c189c61b 100644 --- a/src/route.rs +++ b/src/route.rs @@ -15,7 +15,7 @@ use crate::HttpResponse; type BoxedRouteService = Box< Service< - Req, + Request = Req, Response = Res, Error = (), Future = Box>, @@ -24,7 +24,7 @@ type BoxedRouteService = Box< type BoxedRouteNewService = Box< NewService< - Req, + Request = Req, Response = Res, Error = (), InitError = (), @@ -70,7 +70,8 @@ impl Route

    { } } -impl

    NewService> for Route

    { +impl

    NewService for Route

    { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type InitError = (); @@ -125,7 +126,8 @@ impl

    RouteService

    { } } -impl

    Service> for RouteService

    { +impl

    Service for RouteService

    { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type Future = Box>; @@ -330,7 +332,7 @@ impl Route

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

    )>, + T: NewService, Error = (Error, ServiceFromRequest

    )>, { service: T, _t: PhantomData

    , @@ -339,13 +341,13 @@ where impl RouteNewService where T: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (Error, ServiceFromRequest

    ), >, T::Future: 'static, T::Service: 'static, - >>::Future: 'static, + ::Future: 'static, { pub fn new(service: T) -> Self { RouteNewService { @@ -355,17 +357,18 @@ where } } -impl NewService> for RouteNewService +impl NewService for RouteNewService where T: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (Error, ServiceFromRequest

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

    ; type Response = ServiceResponse; type Error = (); type InitError = (); @@ -389,20 +392,21 @@ where } } -struct RouteServiceWrapper>> { +struct RouteServiceWrapper { service: T, _t: PhantomData

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

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (Error, ServiceFromRequest

    ), >, { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type Future = Box>; diff --git a/src/scope.rs b/src/scope.rs index 15c652c8..6c511c69 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -81,7 +81,7 @@ impl Scope where P: 'static, T: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -174,7 +174,7 @@ where where F: FnOnce(Resource

    ) -> Resource, U: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -199,7 +199,7 @@ where ) -> Scope< P, impl NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -208,12 +208,12 @@ where where M: Transform< T::Service, - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), >, - F: IntoTransform>, + F: IntoTransform, { let endpoint = ApplyTransform::new(mw, self.endpoint); Scope { @@ -231,7 +231,7 @@ impl HttpServiceFactory

    for Scope where P: 'static, T: NewService< - ServiceRequest

    , + Request = ServiceRequest

    , Response = ServiceResponse, Error = (), InitError = (), @@ -287,7 +287,8 @@ pub struct ScopeFactory

    { default: Rc>>>>, } -impl NewService> for ScopeFactory

    { +impl NewService for ScopeFactory

    { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type InitError = (); @@ -402,7 +403,8 @@ pub struct ScopeService

    { _ready: Option<(ServiceRequest

    , ResourceInfo)>, } -impl

    Service> for ScopeService

    { +impl

    Service for ScopeService

    { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type Future = Either>; @@ -445,7 +447,8 @@ impl

    ScopeEndpoint

    { } } -impl NewService> for ScopeEndpoint

    { +impl NewService for ScopeEndpoint

    { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type InitError = (); diff --git a/src/server.rs b/src/server.rs index d3ae5b2b..d9574365 100644 --- a/src/server.rs +++ b/src/server.rs @@ -7,6 +7,7 @@ use actix_http::{ }; use actix_rt::System; use actix_server::{Server, ServerBuilder}; +use actix_server_config::ServerConfig; use actix_service::{IntoNewService, NewService}; use parking_lot::Mutex; @@ -53,8 +54,8 @@ struct Config { pub struct HttpServer where F: Fn() -> I + Send + Clone + 'static, - I: IntoNewService, - S: NewService, + I: IntoNewService, + S: NewService, S::Error: fmt::Debug, S::Response: Into>, S::Service: 'static, @@ -72,8 +73,8 @@ where impl HttpServer where F: Fn() -> I + Send + Clone + 'static, - I: IntoNewService, - S: NewService, + I: IntoNewService, + S: NewService, S::Error: fmt::Debug + 'static, S::Response: Into>, S::Service: 'static, @@ -432,8 +433,8 @@ where impl HttpServer where F: Fn() -> I + Send + Clone + 'static, - I: IntoNewService, - S: NewService, + I: IntoNewService, + S: NewService, S::Error: fmt::Debug, S::Response: Into>, S::Service: 'static, diff --git a/src/test.rs b/src/test.rs index e4cdefbe..c88835a3 100644 --- a/src/test.rs +++ b/src/test.rs @@ -8,6 +8,7 @@ use actix_http::test::TestRequest as HttpTestRequest; use actix_http::{Extensions, PayloadStream, Request}; use actix_router::{Path, ResourceDef, Url}; use actix_rt::Runtime; +use actix_server_config::ServerConfig; use actix_service::{IntoNewService, NewService, Service}; use bytes::Bytes; use futures::Future; @@ -62,13 +63,19 @@ where /// ``` pub fn init_service( app: R, -) -> impl Service, Error = E> +) -> impl Service, Error = E> where - R: IntoNewService, - S: NewService, Error = E>, + R: IntoNewService, + S: NewService< + ServerConfig, + Request = Request, + Response = ServiceResponse, + Error = E, + >, S::InitError: std::fmt::Debug, { - block_on(app.into_new_service().new_service(&())).unwrap() + let cfg = ServerConfig::new("127.0.0.1:8080".parse().unwrap()); + block_on(app.into_new_service().new_service(&cfg)).unwrap() } /// Calls service and waits for response future completion. @@ -93,7 +100,7 @@ where /// ``` pub fn call_success(app: &mut S, req: R) -> S::Response where - S: Service, Error = E>, + S: Service, Error = E>, E: std::fmt::Debug, { block_on(app.call(req)).unwrap() From c0ce7f0bae77ed61330d80fba2b8ec6fdf838556 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 10:53:00 -0800 Subject: [PATCH 063/109] update http service usage; add app host --- src/app.rs | 11 ++++-- src/server.rs | 97 +++++++++++++++++++++++++-------------------------- 2 files changed, 56 insertions(+), 52 deletions(-) diff --git a/src/app.rs b/src/app.rs index 76a3a1ce..42ce62d8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -31,6 +31,7 @@ where chain: T, extensions: Extensions, state: Vec>, + host: String, _t: PhantomData<(P,)>, } @@ -42,6 +43,7 @@ impl App { chain: AppChain, extensions: Extensions::new(), state: Vec::new(), + host: "localhost:8080".to_string(), _t: PhantomData, } } @@ -140,6 +142,7 @@ where default: None, factory_ref: fref, extensions: self.extensions, + host: self.host, _t: PhantomData, } } @@ -172,6 +175,7 @@ where chain, state: self.state, extensions: self.extensions, + host: self.host, _t: PhantomData, } } @@ -221,6 +225,7 @@ where factory_ref: fref, extensions: self.extensions, state: self.state, + host: self.host, services: vec![Box::new(ServiceFactoryWrapper::new(service))], _t: PhantomData, } @@ -233,8 +238,8 @@ where /// html#method.host) documentation for more information. /// /// By default host name is set to a "localhost" value. - pub fn hostname(self, _val: &str) -> Self { - // self.host = val.to_owned(); + pub fn hostname(mut self, val: &str) -> Self { + self.host = val.to_owned(); self } } @@ -249,6 +254,7 @@ pub struct AppRouter { factory_ref: Rc>>>, extensions: Extensions, state: Vec>, + host: String, _t: PhantomData<(P, B)>, } @@ -343,6 +349,7 @@ where default: self.default, factory_ref: self.factory_ref, extensions: self.extensions, + host: self.host, _t: PhantomData, } } diff --git a/src/server.rs b/src/server.rs index d9574365..5d717817 100644 --- a/src/server.rs +++ b/src/server.rs @@ -2,9 +2,7 @@ use std::marker::PhantomData; use std::sync::Arc; use std::{fmt, io, net}; -use actix_http::{ - body::MessageBody, HttpService, KeepAlive, Request, Response, ServiceConfig, -}; +use actix_http::{body::MessageBody, HttpService, KeepAlive, Request, Response}; use actix_rt::System; use actix_server::{Server, ServerBuilder}; use actix_server_config::ServerConfig; @@ -13,8 +11,8 @@ use parking_lot::Mutex; use net2::TcpBuilder; -#[cfg(feature = "tls")] -use native_tls::TlsAcceptor; +// #[cfg(feature = "tls")] +// use native_tls::TlsAcceptor; #[cfg(feature = "ssl")] use openssl::ssl::{SslAcceptor, SslAcceptorBuilder}; @@ -245,27 +243,28 @@ where lst, move || { let c = cfg.lock(); - let service_config = - ServiceConfig::new(c.keep_alive, c.client_timeout, 0); - HttpService::with_config(service_config, factory()) + HttpService::build() + .keep_alive(c.keep_alive) + .client_timeout(c.client_timeout) + .finish(factory()) }, )?); Ok(self) } - #[cfg(feature = "tls")] - /// Use listener for accepting incoming tls connection requests - /// - /// HttpServer does not change any configuration for TcpListener, - /// it needs to be configured before passing it to listen() method. - pub fn listen_tls(self, lst: net::TcpListener, acceptor: TlsAcceptor) -> Self { - use actix_net::service::NewServiceExt; + // #[cfg(feature = "tls")] + // /// Use listener for accepting incoming tls connection requests + // /// + // /// HttpServer does not change any configuration for TcpListener, + // /// it needs to be configured before passing it to listen() method. + // pub fn listen_nativetls(self, lst: net::TcpListener, acceptor: TlsAcceptor) -> Self { + // use actix_server::ssl; - self.listen_with(lst, move || { - ssl::NativeTlsAcceptor::new(acceptor.clone()).map_err(|_| ()) - }) - } + // self.listen_with(lst, move || { + // ssl::NativeTlsAcceptor::new(acceptor.clone()).map_err(|_| ()) + // }) + // } #[cfg(feature = "ssl")] /// Use listener for accepting incoming tls connection requests @@ -276,12 +275,16 @@ where lst: net::TcpListener, builder: SslAcceptorBuilder, ) -> io::Result { - self.listen_ssl_inner(lst, openssl_acceptor(builder)?); + self.listen_ssl_inner(lst, openssl_acceptor(builder)?)?; Ok(self) } #[cfg(feature = "ssl")] - fn listen_ssl_inner(&mut self, lst: net::TcpListener, acceptor: SslAcceptor) { + fn listen_ssl_inner( + &mut self, + lst: net::TcpListener, + acceptor: SslAcceptor, + ) -> io::Result<()> { use actix_server::ssl::{OpensslAcceptor, SslError}; let acceptor = OpensslAcceptor::new(acceptor); @@ -298,15 +301,18 @@ where lst, move || { let c = cfg.lock(); - let service_config = - ServiceConfig::new(c.keep_alive, c.client_timeout, c.client_timeout); acceptor.clone().map_err(|e| SslError::Ssl(e)).and_then( - HttpService::with_config(service_config, factory()) + HttpService::build() + .keep_alive(c.keep_alive) + .client_timeout(c.client_timeout) + .client_disconnect(c.client_shutdown) + .finish(factory()) .map_err(|e| SslError::Service(e)) .map_init_err(|_| ()), ) }, - )); + )?); + Ok(()) } #[cfg(feature = "rust-tls")] @@ -315,7 +321,6 @@ where /// This method sets alpn protocols to "h2" and "http/1.1" pub fn listen_rustls(self, lst: net::TcpListener, config: ServerConfig) -> Self { use super::{RustlsAcceptor, ServerFlags}; - use actix_net::service::NewServiceExt; self.listen_with(lst, move || { RustlsAcceptor::with_flags(config.clone(), flags).map_err(|_| ()) @@ -366,22 +371,21 @@ where } } - #[cfg(feature = "tls")] - /// The ssl socket address to bind - /// - /// To bind multiple addresses this method can be called multiple times. - pub fn bind_tls( - self, - addr: A, - acceptor: TlsAcceptor, - ) -> io::Result { - use actix_net::service::NewServiceExt; - use actix_net::ssl::NativeTlsAcceptor; + // #[cfg(feature = "tls")] + // /// The ssl socket address to bind + // /// + // /// To bind multiple addresses this method can be called multiple times. + // pub fn bind_nativetls( + // self, + // addr: A, + // acceptor: TlsAcceptor, + // ) -> io::Result { + // use actix_server::ssl::NativeTlsAcceptor; - self.bind_with(addr, move || { - NativeTlsAcceptor::new(acceptor.clone()).map_err(|_| ()) - }) - } + // self.bind_with(addr, move || { + // NativeTlsAcceptor::new(acceptor.clone()).map_err(|_| ()) + // }) + // } #[cfg(feature = "ssl")] /// Start listening for incoming tls connections. @@ -399,7 +403,7 @@ where let acceptor = openssl_acceptor(builder)?; for lst in sockets { - self.listen_ssl_inner(lst, acceptor.clone()); + self.listen_ssl_inner(lst, acceptor.clone())?; } Ok(self) @@ -415,14 +419,7 @@ where builder: ServerConfig, ) -> io::Result { use super::{RustlsAcceptor, ServerFlags}; - use actix_net::service::NewServiceExt; - - // alpn support - let flags = if self.no_http2 { - ServerFlags::HTTP1 - } else { - ServerFlags::HTTP1 | ServerFlags::HTTP2 - }; + use actix_service::NewServiceExt; self.bind_with(addr, move || { RustlsAcceptor::with_flags(builder.clone(), flags).map_err(|_| ()) From 54678308d0da4e917ab1cb98a62c986044e5d824 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 14:06:24 -0800 Subject: [PATCH 064/109] propogate app config with http request; add tests for url_for --- Cargo.toml | 3 +- actix-files/src/lib.rs | 41 ++++----- actix-files/src/named.rs | 2 - actix-session/src/cookie.rs | 10 ++- actix-web-codegen/src/lib.rs | 6 +- src/app.rs | 61 +++++++------- src/app_service.rs | 59 ++++++------- src/config.rs | 106 +++++++++++++++++------- src/error.rs | 2 + src/info.rs | 61 ++++++-------- src/lib.rs | 12 +-- src/request.rs | 156 +++++++++++++++++++++++++++++++---- src/resource.rs | 20 ++++- src/rmap.rs | 15 ++-- src/scope.rs | 4 +- src/service.rs | 18 ++-- src/state.rs | 2 +- src/test.rs | 34 +++++--- 18 files changed, 397 insertions(+), 215 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dbc0a65d..9c340825 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,9 +70,10 @@ actix-service = { git = "https://github.com/actix/actix-net.git" } actix-utils = { git = "https://github.com/actix/actix-net.git" } actix-http = { git = "https://github.com/actix/actix-http.git" } -actix-router = { git = "https://github.com/actix/actix-net.git" } +#actix-router = { git = "https://github.com/actix/actix-net.git" } actix-server = { git = "https://github.com/actix/actix-net.git" } actix-server-config = { git = "https://github.com/actix/actix-net.git" } +actix-router = { path = "../actix-net/router" } bytes = "0.4" derive_more = "0.14" diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 17efdd81..14c25be7 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -17,7 +17,7 @@ use v_htmlescape::escape as escape_html_entity; use actix_http::error::{Error, ErrorInternalServerError}; use actix_service::{boxed::BoxedNewService, NewService, Service}; -use actix_web::dev::{self, AppConfig, HttpServiceFactory, ResourceDef, Url}; +use actix_web::dev::{HttpServiceFactory, ResourceDef, ServiceConfig}; use actix_web::{ blocking, FromRequest, HttpRequest, HttpResponse, Responder, ServiceFromRequest, ServiceRequest, ServiceResponse, @@ -305,7 +305,7 @@ where P: 'static, C: StaticFileConfig + 'static, { - fn register(self, config: &mut AppConfig

    ) { + fn register(self, config: &mut ServiceConfig

    ) { if self.default.borrow().is_none() { *self.default.borrow_mut() = Some(config.default_service()); } @@ -314,11 +314,12 @@ where } else { ResourceDef::prefix(&self.path) }; - config.register_service(rdef, None, self) + config.register_service(rdef, None, self, None) } } -impl NewService> for Files { +impl NewService for Files { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type Service = FilesService; @@ -350,7 +351,8 @@ pub struct FilesService { _cd_map: PhantomData, } -impl Service> for FilesService { +impl Service for FilesService { + type Request = ServiceRequest

    ; type Response = ServiceResponse; type Error = (); type Future = FutureResult; @@ -362,7 +364,7 @@ impl Service> for FilesService { fn call(&mut self, req: ServiceRequest

    ) -> Self::Future { let (req, _) = req.into_parts(); - let real_path = match PathBufWrp::get_pathbuf(req.match_info()) { + let real_path = match PathBufWrp::get_pathbuf(req.match_info().path()) { Ok(item) => item, Err(e) => return ok(ServiceResponse::from_err(e, req.clone())), }; @@ -409,13 +411,13 @@ impl Service> for FilesService { } } +#[derive(Debug)] struct PathBufWrp(PathBuf); impl PathBufWrp { - fn get_pathbuf(path: &dev::Path) -> Result { - let path_str = path.path(); + fn get_pathbuf(path: &str) -> Result { let mut buf = PathBuf::new(); - for segment in path_str.split('/') { + for segment in path.split('/') { if segment == ".." { buf.pop(); } else if segment.starts_with('.') { @@ -447,13 +449,14 @@ impl

    FromRequest

    for PathBufWrp { type Config = (); fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - PathBufWrp::get_pathbuf(req.match_info()) + PathBufWrp::get_pathbuf(req.match_info().path()) } } #[cfg(test)] mod tests { use std::fs; + use std::iter::FromIterator; use std::ops::Add; use std::time::{Duration, SystemTime}; @@ -1093,32 +1096,32 @@ mod tests { #[test] fn test_path_buf() { assert_eq!( - PathBuf::from_param("/test/.tt"), + PathBufWrp::get_pathbuf("/test/.tt").map(|t| t.0), Err(UriSegmentError::BadStart('.')) ); assert_eq!( - PathBuf::from_param("/test/*tt"), + PathBufWrp::get_pathbuf("/test/*tt").map(|t| t.0), Err(UriSegmentError::BadStart('*')) ); assert_eq!( - PathBuf::from_param("/test/tt:"), + PathBufWrp::get_pathbuf("/test/tt:").map(|t| t.0), Err(UriSegmentError::BadEnd(':')) ); assert_eq!( - PathBuf::from_param("/test/tt<"), + PathBufWrp::get_pathbuf("/test/tt<").map(|t| t.0), Err(UriSegmentError::BadEnd('<')) ); assert_eq!( - PathBuf::from_param("/test/tt>"), + PathBufWrp::get_pathbuf("/test/tt>").map(|t| t.0), Err(UriSegmentError::BadEnd('>')) ); assert_eq!( - PathBuf::from_param("/seg1/seg2/"), - Ok(PathBuf::from_iter(vec!["seg1", "seg2"])) + PathBufWrp::get_pathbuf("/seg1/seg2/").unwrap().0, + PathBuf::from_iter(vec!["seg1", "seg2"]) ); assert_eq!( - PathBuf::from_param("/seg1/../seg2/"), - Ok(PathBuf::from_iter(vec!["seg2"])) + PathBufWrp::get_pathbuf("/seg1/../seg2/").unwrap().0, + PathBuf::from_iter(vec!["seg2"]) ); } } diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 2fc1c454..6372a183 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -304,8 +304,6 @@ impl Responder for NamedFile { type Future = Result; fn respond_to(self, req: &HttpRequest) -> Self::Future { - println!("RESP: {:?}", req); - if self.status_code != StatusCode::OK { let mut resp = HttpResponse::build(self.status_code); resp.set(header::ContentType(self.content_type.clone())) diff --git a/actix-session/src/cookie.rs b/actix-session/src/cookie.rs index 7fd5ec64..e2503145 100644 --- a/actix-session/src/cookie.rs +++ b/actix-session/src/cookie.rs @@ -255,12 +255,13 @@ impl CookieSession { } } -impl Transform> for CookieSession +impl Transform for CookieSession where - S: Service, Response = ServiceResponse>, + S: Service, Response = ServiceResponse>, S::Future: 'static, S::Error: 'static, { + type Request = ServiceRequest

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

    ; type Response = ServiceResponse; type Error = S::Error; type Future = Box>; diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 26b422d7..13d1b97f 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -31,7 +31,7 @@ pub fn get(args: TokenStream, input: TokenStream) -> TokenStream { struct #name; impl actix_web::dev::HttpServiceFactory

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

    ) { + fn register(self, config: &mut actix_web::dev::ServiceConfig

    ) { #ast actix_web::dev::HttpServiceFactory::register( actix_web::Resource::new(#path) @@ -68,7 +68,7 @@ pub fn post(args: TokenStream, input: TokenStream) -> TokenStream { struct #name; impl actix_web::dev::HttpServiceFactory

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

    ) { + fn register(self, config: &mut actix_web::dev::ServiceConfig

    ) { #ast actix_web::dev::HttpServiceFactory::register( actix_web::Resource::new(#path) @@ -105,7 +105,7 @@ pub fn put(args: TokenStream, input: TokenStream) -> TokenStream { struct #name; impl actix_web::dev::HttpServiceFactory

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

    ) { + fn register(self, config: &mut actix_web::dev::ServiceConfig

    ) { #ast actix_web::dev::HttpServiceFactory::register( actix_web::Resource::new(#path) diff --git a/src/app.rs b/src/app.rs index 42ce62d8..29dd1ab6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,7 +3,8 @@ use std::marker::PhantomData; use std::rc::Rc; use actix_http::body::{Body, MessageBody}; -use actix_http::{Extensions, PayloadStream}; +use actix_http::PayloadStream; +use actix_router::ResourceDef; use actix_server_config::ServerConfig; use actix_service::boxed::{self, BoxedNewService}; use actix_service::{ @@ -12,6 +13,7 @@ use actix_service::{ use futures::IntoFuture; use crate::app_service::{AppChain, AppEntry, AppInit, AppRouting, AppRoutingFactory}; +use crate::config::{AppConfig, AppConfigInner}; use crate::resource::Resource; use crate::route::Route; use crate::service::{ @@ -29,9 +31,8 @@ where T: NewService>, { chain: T, - extensions: Extensions, state: Vec>, - host: String, + config: AppConfigInner, _t: PhantomData<(P,)>, } @@ -41,9 +42,8 @@ impl App { pub fn new() -> Self { App { chain: AppChain, - extensions: Extensions::new(), state: Vec::new(), - host: "localhost:8080".to_string(), + config: AppConfigInner::default(), _t: PhantomData, } } @@ -141,8 +141,8 @@ where services: Vec::new(), default: None, factory_ref: fref, - extensions: self.extensions, - host: self.host, + config: self.config, + external: Vec::new(), _t: PhantomData, } } @@ -174,8 +174,7 @@ where App { chain, state: self.state, - extensions: self.extensions, - host: self.host, + config: self.config, _t: PhantomData, } } @@ -223,10 +222,10 @@ where default: None, endpoint: AppEntry::new(fref.clone()), factory_ref: fref, - extensions: self.extensions, state: self.state, - host: self.host, + config: self.config, services: vec![Box::new(ServiceFactoryWrapper::new(service))], + external: Vec::new(), _t: PhantomData, } } @@ -239,7 +238,7 @@ where /// /// By default host name is set to a "localhost" value. pub fn hostname(mut self, val: &str) -> Self { - self.host = val.to_owned(); + self.config.host = val.to_owned(); self } } @@ -252,9 +251,9 @@ pub struct AppRouter { services: Vec>>, default: Option>>, factory_ref: Rc>>>, - extensions: Extensions, state: Vec>, - host: String, + config: AppConfigInner, + external: Vec, _t: PhantomData<(P, B)>, } @@ -348,8 +347,8 @@ where services: self.services, default: self.default, factory_ref: self.factory_ref, - extensions: self.extensions, - host: self.host, + config: self.config, + external: self.external, _t: PhantomData, } } @@ -382,33 +381,30 @@ where /// and are never considered for matching at request time. Calls to /// `HttpRequest::url_for()` will work as expected. /// - /// ```rust,ignore - /// # extern crate actix_web; - /// use actix_web::{App, HttpRequest, HttpResponse, Result}; + /// ```rust + /// use actix_web::{web, App, HttpRequest, HttpResponse, Result}; /// - /// fn index(req: &HttpRequest) -> Result { - /// let url = req.url_for("youtube", &["oHg5SJYRHA0"])?; - /// assert_eq!(url.as_str(), "https://youtube.com/watch/oHg5SJYRHA0"); + /// fn index(req: HttpRequest) -> Result { + /// let url = req.url_for("youtube", &["asdlkjqme"])?; + /// assert_eq!(url.as_str(), "https://youtube.com/watch/asdlkjqme"); /// Ok(HttpResponse::Ok().into()) /// } /// /// fn main() { /// let app = App::new() - /// .resource("/index.html", |r| r.get().f(index)) - /// .external_resource("youtube", "https://youtube.com/watch/{video_id}") - /// .finish(); + /// .service(web::resource("/index.html").route( + /// web::get().to(index))) + /// .external_resource("youtube", "https://youtube.com/watch/{video_id}"); /// } /// ``` - pub fn external_resource(self, _name: N, _url: U) -> Self + pub fn external_resource(mut self, name: N, url: U) -> Self where N: AsRef, U: AsRef, { - // self.parts - // .as_mut() - // .expect("Use after finish") - // .router - // .register_external(name.as_ref(), ResourceDef::external(url.as_ref())); + let mut rdef = ResourceDef::new(url.as_ref()); + *rdef.name_mut() = name.as_ref().to_string(); + self.external.push(rdef); self } } @@ -435,9 +431,10 @@ where state: self.state, endpoint: self.endpoint, services: RefCell::new(self.services), + external: RefCell::new(self.external), default: self.default, factory_ref: self.factory_ref, - extensions: Rc::new(RefCell::new(Rc::new(self.extensions))), + config: RefCell::new(AppConfig(Rc::new(self.config))), } } } diff --git a/src/app_service.rs b/src/app_service.rs index 094486d9..75e4b316 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -2,7 +2,7 @@ use std::cell::RefCell; use std::marker::PhantomData; use std::rc::Rc; -use actix_http::{Extensions, Request, Response}; +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}; @@ -10,7 +10,7 @@ use actix_service::{fn_service, AndThen, NewService, Service, ServiceExt}; use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, Poll}; -use crate::config::AppConfig; +use crate::config::{AppConfig, ServiceConfig}; use crate::guard::Guard; use crate::rmap::ResourceMap; use crate::service::{ServiceFactory, ServiceRequest, ServiceResponse}; @@ -36,10 +36,11 @@ where pub(crate) chain: C, pub(crate) endpoint: T, pub(crate) state: Vec>, - pub(crate) extensions: Rc>>, + pub(crate) config: RefCell, pub(crate) services: RefCell>>>, pub(crate) default: Option>>, pub(crate) factory_ref: Rc>>>, + pub(crate) external: RefCell>, } impl NewService for AppInit @@ -64,7 +65,7 @@ where type Service = AndThen, T::Service>; type Future = AppInitResult; - fn new_service(&self, _: &ServerConfig) -> Self::Future { + 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

    | { @@ -72,12 +73,15 @@ where }))) }); - let mut config = AppConfig::new( - "127.0.0.1:8080".parse().unwrap(), - "localhost:8080".to_owned(), - false, - default.clone(), - ); + { + let mut c = self.config.borrow_mut(); + let loc_cfg = Rc::get_mut(&mut c.0).unwrap(); + loc_cfg.secure = cfg.secure(); + loc_cfg.addr = cfg.local_addr(); + } + + let mut config = + ServiceConfig::new(self.config.borrow().clone(), default.clone()); // register services std::mem::replace(&mut *self.services.borrow_mut(), Vec::new()) @@ -101,6 +105,11 @@ where ), }); + // external resources + for mut rdef in std::mem::replace(&mut *self.external.borrow_mut(), Vec::new()) { + rmap.add(&mut rdef, None); + } + // complete ResourceMap tree creation let rmap = Rc::new(rmap); rmap.finish(rmap.clone()); @@ -111,7 +120,7 @@ where endpoint: None, endpoint_fut: self.endpoint.new_service(&()), state: self.state.iter().map(|s| s.construct()).collect(), - extensions: self.extensions.clone(), + config: self.config.borrow().clone(), rmap, _t: PhantomData, } @@ -129,7 +138,7 @@ where endpoint_fut: T::Future, rmap: Rc, state: Vec>, - extensions: Rc>>, + config: AppConfig, _t: PhantomData<(P, B)>, } @@ -152,20 +161,14 @@ where type Error = C::InitError; fn poll(&mut self) -> Poll { - if let Some(extensions) = Rc::get_mut(&mut *self.extensions.borrow_mut()) { - let mut idx = 0; - while idx < self.state.len() { - if let Async::Ready(_) = self.state[idx].poll_result(extensions)? { - self.state.remove(idx); - } else { - idx += 1; - } + let mut idx = 0; + let mut extensions = self.config.0.extensions.borrow_mut(); + while idx < self.state.len() { + if let Async::Ready(_) = self.state[idx].poll_result(&mut extensions)? { + self.state.remove(idx); + } else { + idx += 1; } - if !self.state.is_empty() { - return Ok(Async::NotReady); - } - } else { - log::warn!("Multiple copies of app extensions exists"); } if self.chain.is_none() { @@ -185,7 +188,7 @@ where AppInitService { chain: self.chain.take().unwrap(), rmap: self.rmap.clone(), - extensions: self.extensions.borrow().clone(), + config: self.config.clone(), } .and_then(self.endpoint.take().unwrap()), )) @@ -202,7 +205,7 @@ where { chain: C, rmap: Rc, - extensions: Rc, + config: AppConfig, } impl Service for AppInitService @@ -223,7 +226,7 @@ where Path::new(Url::new(req.uri().clone())), req, self.rmap.clone(), - self.extensions.clone(), + self.config.clone(), ); self.chain.call(req) } diff --git a/src/config.rs b/src/config.rs index 47c2f7c4..f84376c7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,8 @@ +use std::cell::{Ref, RefCell}; use std::net::SocketAddr; use std::rc::Rc; +use actix_http::Extensions; use actix_router::ResourceDef; use actix_service::{boxed, IntoNewService, NewService}; @@ -13,10 +15,8 @@ type HttpNewService

    = boxed::BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; /// Application configuration -pub struct AppConfig

    { - addr: SocketAddr, - secure: bool, - host: String, +pub struct ServiceConfig

    { + config: AppConfig, root: bool, default: Rc>, services: Vec<( @@ -27,18 +27,11 @@ pub struct AppConfig

    { )>, } -impl AppConfig

    { +impl ServiceConfig

    { /// Crate server settings instance - pub(crate) fn new( - addr: SocketAddr, - host: String, - secure: bool, - default: Rc>, - ) -> Self { - AppConfig { - addr, - secure, - host, + pub(crate) fn new(config: AppConfig, default: Rc>) -> Self { + ServiceConfig { + config, default, root: true, services: Vec::new(), @@ -62,31 +55,20 @@ impl AppConfig

    { } pub(crate) fn clone_config(&self) -> Self { - AppConfig { - addr: self.addr, - secure: self.secure, - host: self.host.clone(), + ServiceConfig { + config: self.config.clone(), default: self.default.clone(), services: Vec::new(), root: false, } } - /// Returns the socket address of the local half of this TCP connection - pub fn local_addr(&self) -> SocketAddr { - self.addr - } - - /// Returns true if connection is secure(https) - pub fn secure(&self) -> bool { - self.secure - } - - /// Returns host header value - pub fn host(&self) -> &str { - &self.host + /// Service configuration + pub fn config(&self) -> &AppConfig { + &self.config } + /// Default resource pub fn default_service(&self) -> Rc> { self.default.clone() } @@ -114,3 +96,63 @@ impl AppConfig

    { )); } } + +#[derive(Clone)] +pub struct AppConfig(pub(crate) Rc); + +impl AppConfig { + pub(crate) fn new(inner: AppConfigInner) -> Self { + AppConfig(Rc::new(inner)) + } + + /// Set server host name. + /// + /// Host name is used by application router aa a hostname for url + /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. + /// html#method.host) documentation for more information. + /// + /// By default host name is set to a "localhost" value. + pub fn host(&self) -> &str { + &self.0.host + } + + /// Returns true if connection is secure(https) + pub fn secure(&self) -> bool { + self.0.secure + } + + /// Returns the socket address of the local half of this TCP connection + pub fn local_addr(&self) -> SocketAddr { + self.0.addr + } + + /// Resource map + pub fn rmap(&self) -> &ResourceMap { + &self.0.rmap + } + + /// Application extensions + pub fn extensions(&self) -> Ref { + self.0.extensions.borrow() + } +} + +pub(crate) struct AppConfigInner { + pub(crate) secure: bool, + pub(crate) host: String, + pub(crate) addr: SocketAddr, + pub(crate) rmap: ResourceMap, + pub(crate) extensions: RefCell, +} + +impl Default for AppConfigInner { + fn default() -> AppConfigInner { + AppConfigInner { + secure: false, + addr: "127.0.0.1:8080".parse().unwrap(), + host: "localhost:8080".to_owned(), + rmap: ResourceMap::new(ResourceDef::new("")), + extensions: RefCell::new(Extensions::new()), + } + } +} diff --git a/src/error.rs b/src/error.rs index d1c0d3ca..54ca74dc 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,5 @@ +//! Error and Result module + pub use actix_http::error::*; use derive_more::{Display, From}; use url::ParseError as UrlParseError; diff --git a/src/info.rs b/src/info.rs index 3b51215f..c058bd51 100644 --- a/src/info.rs +++ b/src/info.rs @@ -1,19 +1,14 @@ use std::cell::Ref; -use actix_http::http::header::{self, HeaderName}; -use actix_http::RequestHead; +use crate::dev::{AppConfig, RequestHead}; +use crate::http::header::{self, HeaderName}; const X_FORWARDED_FOR: &[u8] = b"x-forwarded-for"; const X_FORWARDED_HOST: &[u8] = b"x-forwarded-host"; const X_FORWARDED_PROTO: &[u8] = b"x-forwarded-proto"; -pub enum ConnectionInfoError { - UnknownHost, - UnknownScheme, -} - /// `HttpRequest` connection information -#[derive(Clone, Default)] +#[derive(Debug, Clone, Default)] pub struct ConnectionInfo { scheme: String, host: String, @@ -23,19 +18,19 @@ pub struct ConnectionInfo { impl ConnectionInfo { /// Create *ConnectionInfo* instance for a request. - pub fn get(req: &RequestHead) -> Ref { + pub fn get<'a>(req: &'a RequestHead, cfg: &AppConfig) -> Ref<'a, Self> { if !req.extensions().contains::() { - req.extensions_mut().insert(ConnectionInfo::new(req)); + req.extensions_mut().insert(ConnectionInfo::new(req, cfg)); } Ref::map(req.extensions(), |e| e.get().unwrap()) } #[cfg_attr(feature = "cargo-clippy", allow(cyclomatic_complexity))] - fn new(req: &RequestHead) -> ConnectionInfo { + fn new(req: &RequestHead, cfg: &AppConfig) -> ConnectionInfo { let mut host = None; let mut scheme = None; let mut remote = None; - let mut peer = None; + let peer = None; // load forwarded header for hdr in req.headers.get_all(header::FORWARDED) { @@ -82,7 +77,7 @@ impl ConnectionInfo { } if scheme.is_none() { scheme = req.uri.scheme_part().map(|a| a.as_str()); - if scheme.is_none() && req.server_settings().secure() { + if scheme.is_none() && cfg.secure() { scheme = Some("https") } } @@ -105,7 +100,7 @@ impl ConnectionInfo { if host.is_none() { host = req.uri.authority_part().map(|a| a.as_str()); if host.is_none() { - host = Some(req.server_settings().host()); + host = Some(cfg.host()); } } } @@ -121,10 +116,10 @@ impl ConnectionInfo { 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 { @@ -186,9 +181,8 @@ mod tests { #[test] fn test_forwarded() { - let req = TestRequest::default().request(); - let mut info = ConnectionInfo::default(); - info.update(&req); + let req = TestRequest::default().to_http_request(); + let info = req.connection_info(); assert_eq!(info.scheme(), "http"); assert_eq!(info.host(), "localhost:8080"); @@ -197,44 +191,39 @@ mod tests { header::FORWARDED, "for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org", ) - .request(); + .to_http_request(); - let mut info = ConnectionInfo::default(); - info.update(&req); + let info = req.connection_info(); assert_eq!(info.scheme(), "https"); assert_eq!(info.host(), "rust-lang.org"); assert_eq!(info.remote(), Some("192.0.2.60")); let req = TestRequest::default() .header(header::HOST, "rust-lang.org") - .request(); + .to_http_request(); - let mut info = ConnectionInfo::default(); - info.update(&req); + let info = req.connection_info(); assert_eq!(info.scheme(), "http"); assert_eq!(info.host(), "rust-lang.org"); assert_eq!(info.remote(), None); let req = TestRequest::default() .header(X_FORWARDED_FOR, "192.0.2.60") - .request(); - let mut info = ConnectionInfo::default(); - info.update(&req); + .to_http_request(); + let info = req.connection_info(); assert_eq!(info.remote(), Some("192.0.2.60")); let req = TestRequest::default() .header(X_FORWARDED_HOST, "192.0.2.60") - .request(); - let mut info = ConnectionInfo::default(); - info.update(&req); + .to_http_request(); + let info = req.connection_info(); assert_eq!(info.host(), "192.0.2.60"); assert_eq!(info.remote(), None); let req = TestRequest::default() .header(X_FORWARDED_PROTO, "https") - .request(); - let mut info = ConnectionInfo::default(); - info.update(&req); + .to_http_request(); + let info = req.connection_info(); assert_eq!(info.scheme(), "https"); } } diff --git a/src/lib.rs b/src/lib.rs index 19f466b4..6329d53c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,13 +2,13 @@ mod app; mod app_service; -mod extract; -mod handler; -// mod info; pub mod blocking; mod config; pub mod error; +mod extract; pub mod guard; +mod handler; +mod info; pub mod middleware; mod request; mod resource; @@ -54,7 +54,9 @@ pub mod dev { //! ``` pub use crate::app::AppRouter; - pub use crate::config::AppConfig; + pub use crate::config::{AppConfig, ServiceConfig}; + pub use crate::info::ConnectionInfo; + pub use crate::rmap::ResourceMap; pub use crate::service::HttpServiceFactory; pub use actix_http::body::{Body, BodyLength, MessageBody, ResponseBody}; @@ -62,7 +64,7 @@ pub mod dev { pub use actix_http::{ Extensions, Payload, PayloadStream, RequestHead, ResponseHead, }; - pub use actix_router::{Path, ResourceDef, Url}; + pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; pub(crate) fn insert_slash(path: &str) -> String { let mut path = path.to_owned(); diff --git a/src/request.rs b/src/request.rs index 6655f1ba..71751483 100644 --- a/src/request.rs +++ b/src/request.rs @@ -7,8 +7,10 @@ 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::error::UrlGenerationError; use crate::extract::FromRequest; +use crate::info::ConnectionInfo; use crate::rmap::ResourceMap; use crate::service::ServiceFromRequest; @@ -18,7 +20,7 @@ pub struct HttpRequest { pub(crate) head: Message, pub(crate) path: Path, rmap: Rc, - extensions: Rc, + config: AppConfig, } impl HttpRequest { @@ -27,13 +29,13 @@ impl HttpRequest { head: Message, path: Path, rmap: Rc, - extensions: Rc, + config: AppConfig, ) -> HttpRequest { HttpRequest { head, path, rmap, - extensions, + config, } } } @@ -92,17 +94,17 @@ impl HttpRequest { &self.path } - /// Application extensions + /// App config #[inline] - pub fn app_extensions(&self) -> &Extensions { - &self.extensions + pub fn config(&self) -> &AppConfig { + &self.config } /// Generate url for named resource /// /// ```rust /// # extern crate actix_web; - /// # use actix_web::{App, HttpRequest, HttpResponse, http}; + /// # use actix_web::{web, App, HttpRequest, HttpResponse}; /// # /// fn index(req: HttpRequest) -> HttpResponse { /// let url = req.url_for("foo", &["1", "2", "3"]); // <- generate url for "foo" resource @@ -111,11 +113,10 @@ impl HttpRequest { /// /// fn main() { /// let app = App::new() - /// .resource("/test/{one}/{two}/{three}", |r| { - /// r.name("foo"); // <- set resource name, then it could be used in `url_for` - /// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); - /// }) - /// .finish(); + /// .service(web::resource("/test/{one}/{two}/{three}") + /// .name("foo") // <- set resource name, then it could be used in `url_for` + /// .route(web::get().to(|| HttpResponse::Ok())) + /// ); /// } /// ``` pub fn url_for( @@ -139,11 +140,11 @@ impl HttpRequest { self.url_for(name, &NO_PARAMS) } - // /// Get *ConnectionInfo* for the correct request. - // #[inline] - // pub fn connection_info(&self) -> Ref { - // ConnectionInfo::get(&*self) - // } + /// Get *ConnectionInfo* for the current request. + #[inline] + pub fn connection_info(&self) -> Ref { + ConnectionInfo::get(&*self, &*self.config()) + } } impl Deref for HttpRequest { @@ -234,3 +235,124 @@ impl fmt::Debug for HttpRequest { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::dev::{ResourceDef, ResourceMap}; + use crate::http::header; + use crate::test::TestRequest; + + #[test] + fn test_debug() { + let req = + TestRequest::with_header("content-type", "text/plain").to_http_request(); + let dbg = format!("{:?}", req); + assert!(dbg.contains("HttpRequest")); + } + + #[test] + fn test_no_request_cookies() { + let req = TestRequest::default().to_http_request(); + assert!(req.cookies().unwrap().is_empty()); + } + + #[test] + fn test_request_cookies() { + let req = TestRequest::default() + .header(header::COOKIE, "cookie1=value1") + .header(header::COOKIE, "cookie2=value2") + .to_http_request(); + { + let cookies = req.cookies().unwrap(); + assert_eq!(cookies.len(), 2); + assert_eq!(cookies[0].name(), "cookie1"); + assert_eq!(cookies[0].value(), "value1"); + assert_eq!(cookies[1].name(), "cookie2"); + assert_eq!(cookies[1].value(), "value2"); + } + + let cookie = req.cookie("cookie1"); + assert!(cookie.is_some()); + let cookie = cookie.unwrap(); + assert_eq!(cookie.name(), "cookie1"); + assert_eq!(cookie.value(), "value1"); + + let cookie = req.cookie("cookie-unknown"); + assert!(cookie.is_none()); + } + + #[test] + fn test_request_query() { + let req = TestRequest::with_uri("/?id=test").to_http_request(); + assert_eq!(req.query_string(), "id=test"); + } + + #[test] + fn test_url_for() { + let mut res = ResourceDef::new("/user/{name}.{ext}"); + *res.name_mut() = "index".to_string(); + + let mut rmap = ResourceMap::new(ResourceDef::new("")); + rmap.add(&mut res, None); + assert!(rmap.has_resource("/user/test.html")); + assert!(!rmap.has_resource("/test/unknown")); + + let req = TestRequest::with_header(header::HOST, "www.rust-lang.org") + .rmap(rmap) + .to_http_request(); + + assert_eq!( + req.url_for("unknown", &["test"]), + Err(UrlGenerationError::ResourceNotFound) + ); + assert_eq!( + req.url_for("index", &["test"]), + Err(UrlGenerationError::NotEnoughElements) + ); + let url = req.url_for("index", &["test", "html"]); + assert_eq!( + url.ok().unwrap().as_str(), + "http://www.rust-lang.org/user/test.html" + ); + } + + #[test] + fn test_url_for_static() { + let mut rdef = ResourceDef::new("/index.html"); + *rdef.name_mut() = "index".to_string(); + + let mut rmap = ResourceMap::new(ResourceDef::new("")); + rmap.add(&mut rdef, None); + + assert!(rmap.has_resource("/index.html")); + + let req = TestRequest::with_uri("/test") + .header(header::HOST, "www.rust-lang.org") + .rmap(rmap) + .to_http_request(); + let url = req.url_for_static("index"); + assert_eq!( + url.ok().unwrap().as_str(), + "http://www.rust-lang.org/index.html" + ); + } + + #[test] + fn test_url_for_external() { + let mut rdef = ResourceDef::new("https://youtube.com/watch/{video_id}"); + + *rdef.name_mut() = "youtube".to_string(); + + let mut rmap = ResourceMap::new(ResourceDef::new("")); + rmap.add(&mut rdef, None); + assert!(rmap.has_resource("https://youtube.com/watch/unknown")); + + let req = TestRequest::default().rmap(rmap).to_http_request(); + let url = req.url_for("youtube", &["oHg5SJYRHA0"]); + assert_eq!( + url.ok().unwrap().as_str(), + "https://youtube.com/watch/oHg5SJYRHA0" + ); + } +} diff --git a/src/resource.rs b/src/resource.rs index cc831665..57f6f710 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -9,7 +9,7 @@ use actix_service::{ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, IntoFuture, Poll}; -use crate::dev::{insert_slash, AppConfig, HttpServiceFactory, ResourceDef}; +use crate::dev::{insert_slash, HttpServiceFactory, ResourceDef, ServiceConfig}; use crate::extract::FromRequest; use crate::guard::Guard; use crate::handler::{AsyncFactory, Factory}; @@ -41,6 +41,7 @@ type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, pub struct Resource> { endpoint: T, rdef: String, + name: Option, routes: Vec>, guards: Vec>, default: Rc>>>>, @@ -54,6 +55,7 @@ impl

    Resource

    { Resource { routes: Vec::new(), rdef: path.to_string(), + name: None, endpoint: ResourceEndpoint::new(fref.clone()), factory_ref: fref, guards: Vec::new(), @@ -72,6 +74,14 @@ where InitError = (), >, { + /// Set resource name. + /// + /// Name is used for url generation. + pub fn name(mut self, name: &str) -> Self { + self.name = Some(name.to_string()); + self + } + /// Add match guard to a resource. /// /// ```rust @@ -240,6 +250,7 @@ where Resource { endpoint, rdef: self.rdef, + name: self.name, guards: self.guards, routes: self.routes, default: self.default, @@ -277,7 +288,7 @@ where InitError = (), > + 'static, { - fn register(mut self, config: &mut AppConfig

    ) { + fn register(mut self, config: &mut ServiceConfig

    ) { if self.default.borrow().is_none() { *self.default.borrow_mut() = Some(config.default_service()); } @@ -286,11 +297,14 @@ where } else { Some(std::mem::replace(&mut self.guards, Vec::new())) }; - let rdef = if config.is_root() || !self.rdef.is_empty() { + let mut rdef = if config.is_root() || !self.rdef.is_empty() { ResourceDef::new(&insert_slash(&self.rdef)) } else { ResourceDef::new(&self.rdef) }; + if let Some(ref name) = self.name { + *rdef.name_mut() = name.clone(); + } config.register_service(rdef, guards, self, None) } } diff --git a/src/rmap.rs b/src/rmap.rs index 4922084b..35fe8ee3 100644 --- a/src/rmap.rs +++ b/src/rmap.rs @@ -64,14 +64,13 @@ impl ResourceMap { if self.patterns_for(name, &mut path, &mut elements)?.is_some() { if path.starts_with('/') { - // let conn = req.connection_info(); - // Ok(Url::parse(&format!( - // "{}://{}{}", - // conn.scheme(), - // conn.host(), - // path - // ))?) - unimplemented!() + let conn = req.connection_info(); + Ok(Url::parse(&format!( + "{}://{}{}", + conn.scheme(), + conn.host(), + path + ))?) } else { Ok(Url::parse(&path)?) } diff --git a/src/scope.rs b/src/scope.rs index 6c511c69..3b506173 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -10,7 +10,7 @@ use actix_service::{ use futures::future::{ok, Either, Future, FutureResult}; use futures::{Async, Poll}; -use crate::dev::{AppConfig, HttpServiceFactory}; +use crate::dev::{HttpServiceFactory, ServiceConfig}; use crate::guard::Guard; use crate::resource::Resource; use crate::rmap::ResourceMap; @@ -237,7 +237,7 @@ where InitError = (), > + 'static, { - fn register(self, config: &mut AppConfig

    ) { + fn register(self, config: &mut ServiceConfig

    ) { // update default resource if needed if self.default.borrow().is_none() { *self.default.borrow_mut() = Some(config.default_service()); diff --git a/src/service.rs b/src/service.rs index ba811458..f4b63a46 100644 --- a/src/service.rs +++ b/src/service.rs @@ -13,16 +13,16 @@ use actix_http::{ use actix_router::{Path, Resource, Url}; use futures::future::{ok, FutureResult, IntoFuture}; -use crate::config::AppConfig; +use crate::config::{AppConfig, ServiceConfig}; use crate::request::HttpRequest; use crate::rmap::ResourceMap; pub trait HttpServiceFactory

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

    ); + fn register(self, config: &mut ServiceConfig

    ); } pub(crate) trait ServiceFactory

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

    ); + fn register(&mut self, config: &mut ServiceConfig

    ); } pub(crate) struct ServiceFactoryWrapper { @@ -43,7 +43,7 @@ impl ServiceFactory

    for ServiceFactoryWrapper where T: HttpServiceFactory

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

    ) { + fn register(&mut self, config: &mut ServiceConfig

    ) { if let Some(item) = self.factory.take() { item.register(config) } @@ -60,12 +60,12 @@ impl

    ServiceRequest

    { path: Path, request: Request

    , rmap: Rc, - extensions: Rc, + config: AppConfig, ) -> Self { let (head, payload) = request.into_parts(); ServiceRequest { payload, - req: HttpRequest::new(head, path, rmap, extensions), + req: HttpRequest::new(head, path, rmap, config), } } @@ -156,10 +156,10 @@ impl

    ServiceRequest

    { &mut self.req.path } - /// Application extensions + /// Service configuration #[inline] - pub fn app_extensions(&self) -> &Extensions { - self.req.app_extensions() + pub fn app_config(&self) -> &AppConfig { + self.req.config() } /// Deconstruct request into parts diff --git a/src/state.rs b/src/state.rs index 265c6f01..2c623c70 100644 --- a/src/state.rs +++ b/src/state.rs @@ -52,7 +52,7 @@ impl FromRequest

    for State { #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - if let Some(st) = req.app_extensions().get::>() { + if let Some(st) = req.config().extensions().get::>() { Ok(st.clone()) } else { Err(ErrorInternalServerError( diff --git a/src/test.rs b/src/test.rs index c88835a3..b47daa2c 100644 --- a/src/test.rs +++ b/src/test.rs @@ -5,7 +5,7 @@ use std::rc::Rc; use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; use actix_http::http::{HttpTryFrom, Method, Version}; use actix_http::test::TestRequest as HttpTestRequest; -use actix_http::{Extensions, PayloadStream, Request}; +use actix_http::{PayloadStream, Request}; use actix_router::{Path, ResourceDef, Url}; use actix_rt::Runtime; use actix_server_config::ServerConfig; @@ -13,6 +13,7 @@ use actix_service::{IntoNewService, NewService, Service}; use bytes::Bytes; use futures::Future; +use crate::config::{AppConfig, AppConfigInner}; use crate::request::HttpRequest; use crate::rmap::ResourceMap; use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; @@ -142,16 +143,16 @@ where /// ``` pub struct TestRequest { req: HttpTestRequest, - extensions: Extensions, rmap: ResourceMap, + config: AppConfigInner, } impl Default for TestRequest { fn default() -> TestRequest { TestRequest { req: HttpTestRequest::default(), - extensions: Extensions::new(), rmap: ResourceMap::new(ResourceDef::new("")), + config: AppConfigInner::default(), } } } @@ -161,8 +162,8 @@ impl TestRequest { pub fn with_uri(path: &str) -> TestRequest { TestRequest { req: HttpTestRequest::default().uri(path).take(), - extensions: Extensions::new(), rmap: ResourceMap::new(ResourceDef::new("")), + config: AppConfigInner::default(), } } @@ -170,7 +171,7 @@ impl TestRequest { pub fn with_hdr(hdr: H) -> TestRequest { TestRequest { req: HttpTestRequest::default().set(hdr).take(), - extensions: Extensions::new(), + config: AppConfigInner::default(), rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -183,7 +184,7 @@ impl TestRequest { { TestRequest { req: HttpTestRequest::default().header(key, value).take(), - extensions: Extensions::new(), + config: AppConfigInner::default(), rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -192,7 +193,7 @@ impl TestRequest { pub fn get() -> TestRequest { TestRequest { req: HttpTestRequest::default().method(Method::GET).take(), - extensions: Extensions::new(), + config: AppConfigInner::default(), rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -201,7 +202,7 @@ impl TestRequest { pub fn post() -> TestRequest { TestRequest { req: HttpTestRequest::default().method(Method::POST).take(), - extensions: Extensions::new(), + config: AppConfigInner::default(), rmap: ResourceMap::new(ResourceDef::new("")), } } @@ -247,8 +248,15 @@ impl TestRequest { } /// Set request config - pub fn config(mut self, data: T) -> Self { - self.extensions.insert(data); + pub fn config(self, data: T) -> Self { + self.config.extensions.borrow_mut().insert(data); + self + } + + #[cfg(test)] + /// Set request config + pub(crate) fn rmap(mut self, rmap: ResourceMap) -> Self { + self.rmap = rmap; self } @@ -260,7 +268,7 @@ impl TestRequest { Path::new(Url::new(req.uri().clone())), req, Rc::new(self.rmap), - Rc::new(self.extensions), + AppConfig::new(self.config), ) } @@ -277,7 +285,7 @@ impl TestRequest { Path::new(Url::new(req.uri().clone())), req, Rc::new(self.rmap), - Rc::new(self.extensions), + AppConfig::new(self.config), ) .into_request() } @@ -290,7 +298,7 @@ impl TestRequest { Path::new(Url::new(req.uri().clone())), req, Rc::new(self.rmap), - Rc::new(self.extensions), + AppConfig::new(self.config), ); ServiceFromRequest::new(req, None) } From d2dba028f60c1b9d4f75bc96ece6162baa3dfcd4 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 14:07:43 -0800 Subject: [PATCH 065/109] fix dependency link --- Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9c340825..dbc0a65d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,10 +70,9 @@ actix-service = { git = "https://github.com/actix/actix-net.git" } actix-utils = { git = "https://github.com/actix/actix-net.git" } actix-http = { git = "https://github.com/actix/actix-http.git" } -#actix-router = { git = "https://github.com/actix/actix-net.git" } +actix-router = { git = "https://github.com/actix/actix-net.git" } actix-server = { git = "https://github.com/actix/actix-net.git" } actix-server-config = { git = "https://github.com/actix/actix-net.git" } -actix-router = { path = "../actix-net/router" } bytes = "0.4" derive_more = "0.14" From 85664cc6f7499b57ca979341780278eabeed1f83 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 14:56:18 -0800 Subject: [PATCH 066/109] update deps --- Cargo.toml | 12 +++--------- actix-files/Cargo.toml | 3 +-- actix-session/Cargo.toml | 12 ++++-------- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dbc0a65d..6be1996e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,16 +61,13 @@ ssl = ["openssl", "actix-server/ssl"] [dependencies] actix-codec = "0.1.0" -#actix-service = "0.3.2" -#actix-utils = "0.3.1" +actix-service = "0.3.3" +actix-utils = "0.3.3" +actix-router = "0.1.0" actix-rt = "0.2.0" actix-web-codegen = { path="actix-web-codegen" } -actix-service = { git = "https://github.com/actix/actix-net.git" } -actix-utils = { git = "https://github.com/actix/actix-net.git" } - actix-http = { git = "https://github.com/actix/actix-http.git" } -actix-router = { git = "https://github.com/actix/actix-net.git" } actix-server = { git = "https://github.com/actix/actix-net.git" } actix-server-config = { git = "https://github.com/actix/actix-net.git" } @@ -116,6 +113,3 @@ serde_derive = "1.0" lto = true opt-level = 3 codegen-units = 1 - -[patch.crates-io] -actix-service = { git = "https://github.com/actix/actix-net.git" } diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index bd61c880..c0f38b9a 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -20,8 +20,7 @@ path = "src/lib.rs" [dependencies] actix-web = { path=".." } actix-http = { git = "https://github.com/actix/actix-http.git" } -actix-service = { git = "https://github.com/actix/actix-net.git" } -#actix-service = "0.3.0" +actix-service = "0.3.3" bytes = "0.4" futures = "0.1" diff --git a/actix-session/Cargo.toml b/actix-session/Cargo.toml index 3bbeb4f8..421c6fc4 100644 --- a/actix-session/Cargo.toml +++ b/actix-session/Cargo.toml @@ -25,13 +25,9 @@ cookie-session = ["cookie/secure"] [dependencies] actix-web = { path=".." } -actix-codec = "0.1.0" - -#actix-service = "0.3.2" -#actix-utils = "0.3.1" -actix-service = { git = "https://github.com/actix/actix-net.git" } -actix-utils = { git = "https://github.com/actix/actix-net.git" } - +actix-codec = "0.1.1" +actix-service = "0.3.3" +actix-utils = "0.3.3" actix-http = { git = "https://github.com/actix/actix-http.git" } actix-router = { git = "https://github.com/actix/actix-net.git" } actix-server = { git = "https://github.com/actix/actix-net.git" } @@ -48,4 +44,4 @@ serde_json = "1.0" time = "0.1" [dev-dependencies] -actix-rt = "0.1.0" +actix-rt = "0.2.0" From 134863d5c828fad574d75fa40347a7cfc379a5b4 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 18:04:40 -0800 Subject: [PATCH 067/109] move middlewares --- errhandlers.rs | 141 +++++++++++++++++ identity.rs | 399 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 540 insertions(+) create mode 100644 errhandlers.rs create mode 100644 identity.rs diff --git a/errhandlers.rs b/errhandlers.rs new file mode 100644 index 00000000..c7d19d33 --- /dev/null +++ b/errhandlers.rs @@ -0,0 +1,141 @@ +use std::collections::HashMap; + +use error::Result; +use http::StatusCode; +use httprequest::HttpRequest; +use httpresponse::HttpResponse; +use middleware::{Middleware, Response}; + +type ErrorHandler = Fn(&HttpRequest, HttpResponse) -> Result; + +/// `Middleware` for allowing custom handlers for responses. +/// +/// You can use `ErrorHandlers::handler()` method to register a custom error +/// handler for specific status code. You can modify existing response or +/// create completely new one. +/// +/// ## Example +/// +/// ```rust +/// # extern crate actix_web; +/// use actix_web::middleware::{ErrorHandlers, Response}; +/// use actix_web::{http, App, HttpRequest, HttpResponse, Result}; +/// +/// fn render_500(_: &HttpRequest, resp: HttpResponse) -> Result { +/// let mut builder = resp.into_builder(); +/// builder.header(http::header::CONTENT_TYPE, "application/json"); +/// Ok(Response::Done(builder.into())) +/// } +/// +/// fn main() { +/// let app = App::new() +/// .middleware( +/// ErrorHandlers::new() +/// .handler(http::StatusCode::INTERNAL_SERVER_ERROR, render_500), +/// ) +/// .resource("/test", |r| { +/// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); +/// r.method(http::Method::HEAD) +/// .f(|_| HttpResponse::MethodNotAllowed()); +/// }) +/// .finish(); +/// } +/// ``` +pub struct ErrorHandlers { + handlers: HashMap>>, +} + +impl Default for ErrorHandlers { + fn default() -> Self { + ErrorHandlers { + handlers: HashMap::new(), + } + } +} + +impl ErrorHandlers { + /// Construct new `ErrorHandlers` instance + pub fn new() -> Self { + ErrorHandlers::default() + } + + /// Register error handler for specified status code + pub fn handler(mut self, status: StatusCode, handler: F) -> Self + where + F: Fn(&HttpRequest, HttpResponse) -> Result + 'static, + { + self.handlers.insert(status, Box::new(handler)); + self + } +} + +impl Middleware for ErrorHandlers { + fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { + if let Some(handler) = self.handlers.get(&resp.status()) { + handler(req, resp) + } else { + Ok(Response::Done(resp)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use error::{Error, ErrorInternalServerError}; + use http::header::CONTENT_TYPE; + use http::StatusCode; + use httpmessage::HttpMessage; + use middleware::Started; + use test::{self, TestRequest}; + + fn render_500(_: &HttpRequest, resp: HttpResponse) -> Result { + let mut builder = resp.into_builder(); + builder.header(CONTENT_TYPE, "0001"); + Ok(Response::Done(builder.into())) + } + + #[test] + fn test_handler() { + let mw = + ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); + + let mut req = TestRequest::default().finish(); + let resp = HttpResponse::InternalServerError().finish(); + let resp = match mw.response(&mut req, resp) { + Ok(Response::Done(resp)) => resp, + _ => panic!(), + }; + assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); + + let resp = HttpResponse::Ok().finish(); + let resp = match mw.response(&mut req, resp) { + Ok(Response::Done(resp)) => resp, + _ => panic!(), + }; + assert!(!resp.headers().contains_key(CONTENT_TYPE)); + } + + struct MiddlewareOne; + + impl Middleware for MiddlewareOne { + fn start(&self, _: &HttpRequest) -> Result { + Err(ErrorInternalServerError("middleware error")) + } + } + + #[test] + fn test_middleware_start_error() { + let mut srv = test::TestServer::new(move |app| { + app.middleware( + ErrorHandlers::new() + .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500), + ).middleware(MiddlewareOne) + .handler(|_| HttpResponse::Ok()) + }); + + let request = srv.get().finish().unwrap(); + let response = srv.execute(request.send()).unwrap(); + assert_eq!(response.headers().get(CONTENT_TYPE).unwrap(), "0001"); + } +} diff --git a/identity.rs b/identity.rs new file mode 100644 index 00000000..a664ba1f --- /dev/null +++ b/identity.rs @@ -0,0 +1,399 @@ +//! Request identity service for Actix applications. +//! +//! [**IdentityService**](struct.IdentityService.html) middleware can be +//! used with different policies types to store identity information. +//! +//! By default, only cookie identity policy is implemented. Other backend +//! implementations can be added separately. +//! +//! [**CookieIdentityPolicy**](struct.CookieIdentityPolicy.html) +//! uses cookies as identity storage. +//! +//! To access current request identity +//! [**RequestIdentity**](trait.RequestIdentity.html) should be used. +//! *HttpRequest* implements *RequestIdentity* trait. +//! +//! ```rust +//! use actix_web::middleware::identity::RequestIdentity; +//! use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; +//! use actix_web::*; +//! +//! fn index(req: HttpRequest) -> Result { +//! // access request identity +//! if let Some(id) = req.identity() { +//! Ok(format!("Welcome! {}", id)) +//! } else { +//! Ok("Welcome Anonymous!".to_owned()) +//! } +//! } +//! +//! fn login(mut req: HttpRequest) -> HttpResponse { +//! req.remember("User1".to_owned()); // <- remember identity +//! HttpResponse::Ok().finish() +//! } +//! +//! fn logout(mut req: HttpRequest) -> HttpResponse { +//! req.forget(); // <- remove identity +//! HttpResponse::Ok().finish() +//! } +//! +//! fn main() { +//! let app = App::new().middleware(IdentityService::new( +//! // <- create identity middleware +//! CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend +//! .name("auth-cookie") +//! .secure(false), +//! )); +//! } +//! ``` +use std::rc::Rc; + +use cookie::{Cookie, CookieJar, Key, SameSite}; +use futures::future::{err as FutErr, ok as FutOk, FutureResult}; +use futures::Future; +use time::Duration; + +use error::{Error, Result}; +use http::header::{self, HeaderValue}; +use httprequest::HttpRequest; +use httpresponse::HttpResponse; +use middleware::{Middleware, Response, Started}; + +/// The helper trait to obtain your identity from a request. +/// +/// ```rust +/// use actix_web::middleware::identity::RequestIdentity; +/// use actix_web::*; +/// +/// fn index(req: HttpRequest) -> Result { +/// // access request identity +/// if let Some(id) = req.identity() { +/// Ok(format!("Welcome! {}", id)) +/// } else { +/// Ok("Welcome Anonymous!".to_owned()) +/// } +/// } +/// +/// fn login(mut req: HttpRequest) -> HttpResponse { +/// req.remember("User1".to_owned()); // <- remember identity +/// HttpResponse::Ok().finish() +/// } +/// +/// fn logout(mut req: HttpRequest) -> HttpResponse { +/// req.forget(); // <- remove identity +/// HttpResponse::Ok().finish() +/// } +/// # fn main() {} +/// ``` +pub trait RequestIdentity { + /// Return the claimed identity of the user associated request or + /// ``None`` if no identity can be found associated with the request. + fn identity(&self) -> Option; + + /// Remember identity. + fn remember(&self, identity: String); + + /// This method is used to 'forget' the current identity on subsequent + /// requests. + fn forget(&self); +} + +impl RequestIdentity for HttpRequest { + fn identity(&self) -> Option { + if let Some(id) = self.extensions().get::() { + return id.0.identity().map(|s| s.to_owned()); + } + None + } + + fn remember(&self, identity: String) { + if let Some(id) = self.extensions_mut().get_mut::() { + return id.0.as_mut().remember(identity); + } + } + + fn forget(&self) { + if let Some(id) = self.extensions_mut().get_mut::() { + return id.0.forget(); + } + } +} + +/// An identity +pub trait Identity: 'static { + /// Return the claimed identity of the user associated request or + /// ``None`` if no identity can be found associated with the request. + fn identity(&self) -> Option<&str>; + + /// Remember identity. + fn remember(&mut self, key: String); + + /// This method is used to 'forget' the current identity on subsequent + /// requests. + fn forget(&mut self); + + /// Write session to storage backend. + fn write(&mut self, resp: HttpResponse) -> Result; +} + +/// Identity policy definition. +pub trait IdentityPolicy: Sized + 'static { + /// The associated identity + type Identity: Identity; + + /// The return type of the middleware + type Future: Future; + + /// Parse the session from request and load data from a service identity. + fn from_request(&self, request: &HttpRequest) -> Self::Future; +} + +/// Request identity middleware +/// +/// ```rust +/// # extern crate actix_web; +/// use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; +/// use actix_web::App; +/// +/// fn main() { +/// let app = App::new().middleware(IdentityService::new( +/// // <- create identity middleware +/// CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend +/// .name("auth-cookie") +/// .secure(false), +/// )); +/// } +/// ``` +pub struct IdentityService { + backend: T, +} + +impl IdentityService { + /// Create new identity service with specified backend. + pub fn new(backend: T) -> Self { + IdentityService { backend } + } +} + +struct IdentityBox(Box); + +impl> Middleware for IdentityService { + fn start(&self, req: &HttpRequest) -> Result { + let req = req.clone(); + let fut = self.backend.from_request(&req).then(move |res| match res { + Ok(id) => { + req.extensions_mut().insert(IdentityBox(Box::new(id))); + FutOk(None) + } + Err(err) => FutErr(err), + }); + Ok(Started::Future(Box::new(fut))) + } + + fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { + if let Some(ref mut id) = req.extensions_mut().get_mut::() { + id.0.as_mut().write(resp) + } else { + Ok(Response::Done(resp)) + } + } +} + +#[doc(hidden)] +/// Identity that uses private cookies as identity storage. +pub struct CookieIdentity { + changed: bool, + identity: Option, + inner: Rc, +} + +impl Identity for CookieIdentity { + fn identity(&self) -> Option<&str> { + self.identity.as_ref().map(|s| s.as_ref()) + } + + fn remember(&mut self, value: String) { + self.changed = true; + self.identity = Some(value); + } + + fn forget(&mut self) { + self.changed = true; + self.identity = None; + } + + fn write(&mut self, mut resp: HttpResponse) -> Result { + if self.changed { + let _ = self.inner.set_cookie(&mut resp, self.identity.take()); + } + Ok(Response::Done(resp)) + } +} + +struct CookieIdentityInner { + key: Key, + name: String, + path: String, + domain: Option, + secure: bool, + max_age: Option, + same_site: Option, +} + +impl CookieIdentityInner { + fn new(key: &[u8]) -> CookieIdentityInner { + CookieIdentityInner { + key: Key::from_master(key), + name: "actix-identity".to_owned(), + path: "/".to_owned(), + domain: None, + secure: true, + max_age: None, + same_site: None, + } + } + + fn set_cookie(&self, resp: &mut HttpResponse, id: Option) -> Result<()> { + let some = id.is_some(); + { + let id = id.unwrap_or_else(String::new); + let mut cookie = Cookie::new(self.name.clone(), id); + cookie.set_path(self.path.clone()); + cookie.set_secure(self.secure); + cookie.set_http_only(true); + + if let Some(ref domain) = self.domain { + cookie.set_domain(domain.clone()); + } + + if let Some(max_age) = self.max_age { + cookie.set_max_age(max_age); + } + + if let Some(same_site) = self.same_site { + cookie.set_same_site(same_site); + } + + let mut jar = CookieJar::new(); + if some { + jar.private(&self.key).add(cookie); + } else { + jar.add_original(cookie.clone()); + jar.private(&self.key).remove(cookie); + } + + for cookie in jar.delta() { + let val = HeaderValue::from_str(&cookie.to_string())?; + resp.headers_mut().append(header::SET_COOKIE, val); + } + } + + Ok(()) + } + + fn load(&self, req: &HttpRequest) -> Option { + if let Ok(cookies) = req.cookies() { + for cookie in cookies.iter() { + if cookie.name() == self.name { + let mut jar = CookieJar::new(); + jar.add_original(cookie.clone()); + + let cookie_opt = jar.private(&self.key).get(&self.name); + if let Some(cookie) = cookie_opt { + return Some(cookie.value().into()); + } + } + } + } + None + } +} + +/// Use cookies for request identity storage. +/// +/// The constructors take a key as an argument. +/// This is the private key for cookie - when this value is changed, +/// all identities are lost. The constructors will panic if the key is less +/// than 32 bytes in length. +/// +/// # Example +/// +/// ```rust +/// # extern crate actix_web; +/// use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; +/// use actix_web::App; +/// +/// fn main() { +/// let app = App::new().middleware(IdentityService::new( +/// // <- create identity middleware +/// CookieIdentityPolicy::new(&[0; 32]) // <- construct cookie policy +/// .domain("www.rust-lang.org") +/// .name("actix_auth") +/// .path("/") +/// .secure(true), +/// )); +/// } +/// ``` +pub struct CookieIdentityPolicy(Rc); + +impl CookieIdentityPolicy { + /// Construct new `CookieIdentityPolicy` instance. + /// + /// Panics if key length is less than 32 bytes. + pub fn new(key: &[u8]) -> CookieIdentityPolicy { + CookieIdentityPolicy(Rc::new(CookieIdentityInner::new(key))) + } + + /// Sets the `path` field in the session cookie being built. + pub fn path>(mut self, value: S) -> CookieIdentityPolicy { + Rc::get_mut(&mut self.0).unwrap().path = value.into(); + self + } + + /// Sets the `name` field in the session cookie being built. + pub fn name>(mut self, value: S) -> CookieIdentityPolicy { + Rc::get_mut(&mut self.0).unwrap().name = value.into(); + self + } + + /// Sets the `domain` field in the session cookie being built. + pub fn domain>(mut self, value: S) -> CookieIdentityPolicy { + Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into()); + self + } + + /// Sets the `secure` field in the session cookie being built. + /// + /// If the `secure` field is set, a cookie will only be transmitted when the + /// connection is secure - i.e. `https` + pub fn secure(mut self, value: bool) -> CookieIdentityPolicy { + Rc::get_mut(&mut self.0).unwrap().secure = value; + self + } + + /// Sets the `max-age` field in the session cookie being built. + pub fn max_age(mut self, value: Duration) -> CookieIdentityPolicy { + Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); + self + } + + /// Sets the `same_site` field in the session cookie being built. + pub fn same_site(mut self, same_site: SameSite) -> Self { + Rc::get_mut(&mut self.0).unwrap().same_site = Some(same_site); + self + } +} + +impl IdentityPolicy for CookieIdentityPolicy { + type Identity = CookieIdentity; + type Future = FutureResult; + + fn from_request(&self, req: &HttpRequest) -> Self::Future { + let identity = self.0.load(req); + FutOk(CookieIdentity { + identity, + changed: false, + inner: Rc::clone(&self.0), + }) + } +} From 12f0c78091a7d2ec668345e231aed8ad3318b88d Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 20:40:09 -0800 Subject: [PATCH 068/109] port identity middleware --- Cargo.toml | 7 +- errhandlers.rs | 141 ---------- identity.rs => src/middleware/identity.rs | 320 +++++++++++++--------- src/middleware/mod.rs | 7 +- src/service.rs | 11 +- 5 files changed, 211 insertions(+), 275 deletions(-) delete mode 100644 errhandlers.rs rename identity.rs => src/middleware/identity.rs (53%) diff --git a/Cargo.toml b/Cargo.toml index 6be1996e..c64f4bc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,10 +33,10 @@ members = [ ] [package.metadata.docs.rs] -features = ["ssl", "tls", "rust-tls"] #, "session"] +features = ["ssl", "tls", "rust-tls", "session"] [features] -default = ["brotli", "flate2-c"] +default = ["brotli", "flate2-c", "session"] # brotli encoding, requires c compiler brotli = ["brotli2"] @@ -48,7 +48,7 @@ flate2-c = ["flate2/miniz-sys"] flate2-rust = ["flate2/rust_backend"] # sessions feature, session require "ring" crate and c compiler -# session = ["actix-session"] +session = ["cookie/secure"] # tls tls = ["native-tls", "actix-server/ssl"] @@ -72,6 +72,7 @@ actix-server = { git = "https://github.com/actix/actix-net.git" } actix-server-config = { git = "https://github.com/actix/actix-net.git" } bytes = "0.4" +cookie = { version="0.11", features=["percent-encode"] } derive_more = "0.14" encoding = "0.2" futures = "0.1" diff --git a/errhandlers.rs b/errhandlers.rs deleted file mode 100644 index c7d19d33..00000000 --- a/errhandlers.rs +++ /dev/null @@ -1,141 +0,0 @@ -use std::collections::HashMap; - -use error::Result; -use http::StatusCode; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response}; - -type ErrorHandler = Fn(&HttpRequest, HttpResponse) -> Result; - -/// `Middleware` for allowing custom handlers for responses. -/// -/// You can use `ErrorHandlers::handler()` method to register a custom error -/// handler for specific status code. You can modify existing response or -/// create completely new one. -/// -/// ## Example -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::{ErrorHandlers, Response}; -/// use actix_web::{http, App, HttpRequest, HttpResponse, Result}; -/// -/// fn render_500(_: &HttpRequest, resp: HttpResponse) -> Result { -/// let mut builder = resp.into_builder(); -/// builder.header(http::header::CONTENT_TYPE, "application/json"); -/// Ok(Response::Done(builder.into())) -/// } -/// -/// fn main() { -/// let app = App::new() -/// .middleware( -/// ErrorHandlers::new() -/// .handler(http::StatusCode::INTERNAL_SERVER_ERROR, render_500), -/// ) -/// .resource("/test", |r| { -/// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); -/// r.method(http::Method::HEAD) -/// .f(|_| HttpResponse::MethodNotAllowed()); -/// }) -/// .finish(); -/// } -/// ``` -pub struct ErrorHandlers { - handlers: HashMap>>, -} - -impl Default for ErrorHandlers { - fn default() -> Self { - ErrorHandlers { - handlers: HashMap::new(), - } - } -} - -impl ErrorHandlers { - /// Construct new `ErrorHandlers` instance - pub fn new() -> Self { - ErrorHandlers::default() - } - - /// Register error handler for specified status code - pub fn handler(mut self, status: StatusCode, handler: F) -> Self - where - F: Fn(&HttpRequest, HttpResponse) -> Result + 'static, - { - self.handlers.insert(status, Box::new(handler)); - self - } -} - -impl Middleware for ErrorHandlers { - fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { - if let Some(handler) = self.handlers.get(&resp.status()) { - handler(req, resp) - } else { - Ok(Response::Done(resp)) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use error::{Error, ErrorInternalServerError}; - use http::header::CONTENT_TYPE; - use http::StatusCode; - use httpmessage::HttpMessage; - use middleware::Started; - use test::{self, TestRequest}; - - fn render_500(_: &HttpRequest, resp: HttpResponse) -> Result { - let mut builder = resp.into_builder(); - builder.header(CONTENT_TYPE, "0001"); - Ok(Response::Done(builder.into())) - } - - #[test] - fn test_handler() { - let mw = - ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); - - let mut req = TestRequest::default().finish(); - let resp = HttpResponse::InternalServerError().finish(); - let resp = match mw.response(&mut req, resp) { - Ok(Response::Done(resp)) => resp, - _ => panic!(), - }; - assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); - - let resp = HttpResponse::Ok().finish(); - let resp = match mw.response(&mut req, resp) { - Ok(Response::Done(resp)) => resp, - _ => panic!(), - }; - assert!(!resp.headers().contains_key(CONTENT_TYPE)); - } - - struct MiddlewareOne; - - impl Middleware for MiddlewareOne { - fn start(&self, _: &HttpRequest) -> Result { - Err(ErrorInternalServerError("middleware error")) - } - } - - #[test] - fn test_middleware_start_error() { - let mut srv = test::TestServer::new(move |app| { - app.middleware( - ErrorHandlers::new() - .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500), - ).middleware(MiddlewareOne) - .handler(|_| HttpResponse::Ok()) - }); - - let request = srv.get().finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.headers().get(CONTENT_TYPE).unwrap(), "0001"); - } -} diff --git a/identity.rs b/src/middleware/identity.rs similarity index 53% rename from identity.rs rename to src/middleware/identity.rs index a664ba1f..d04ed717 100644 --- a/identity.rs +++ b/src/middleware/identity.rs @@ -14,26 +14,26 @@ //! *HttpRequest* implements *RequestIdentity* trait. //! //! ```rust -//! use actix_web::middleware::identity::RequestIdentity; +//! use actix_web::middleware::identity::Identity; //! use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; //! use actix_web::*; //! -//! fn index(req: HttpRequest) -> Result { +//! fn index(id: Identity) -> String { //! // access request identity -//! if let Some(id) = req.identity() { -//! Ok(format!("Welcome! {}", id)) +//! if let Some(id) = id.identity() { +//! format!("Welcome! {}", id) //! } else { -//! Ok("Welcome Anonymous!".to_owned()) +//! "Welcome Anonymous!".to_owned() //! } //! } //! -//! fn login(mut req: HttpRequest) -> HttpResponse { -//! req.remember("User1".to_owned()); // <- remember identity +//! fn login(id: Idenity) -> HttpResponse { +//! id.remember("User1".to_owned()); // <- remember identity //! HttpResponse::Ok().finish() //! } //! -//! fn logout(mut req: HttpRequest) -> HttpResponse { -//! req.forget(); // <- remove identity +//! fn logout(id: Identity) -> HttpResponse { +//! id.forget(); // <- remove identity //! HttpResponse::Ok().finish() //! } //! @@ -42,118 +42,144 @@ //! // <- create identity middleware //! CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend //! .name("auth-cookie") -//! .secure(false), +//! .secure(false)) +//! .service(web::resource("/index.html").to(index) +//! .service(web::resource("/login.html").to(login) +//! .service(web::resource("/logout.html").to(logout) //! )); //! } //! ``` +use std::cell::RefCell; use std::rc::Rc; +use actix_service::{Service, Transform}; use cookie::{Cookie, CookieJar, Key, SameSite}; -use futures::future::{err as FutErr, ok as FutOk, FutureResult}; -use futures::Future; +use futures::future::{ok, Either, FutureResult}; +use futures::{Future, IntoFuture, Poll}; use time::Duration; -use error::{Error, Result}; -use http::header::{self, HeaderValue}; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response, Started}; +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; -/// The helper trait to obtain your identity from a request. +/// The extractor type to obtain your identity from a request. /// /// ```rust -/// use actix_web::middleware::identity::RequestIdentity; /// use actix_web::*; +/// use actix_web::middleware::identity::Identity; /// -/// fn index(req: HttpRequest) -> Result { +/// fn index(id: Identity) -> Result { /// // access request identity -/// if let Some(id) = req.identity() { +/// if let Some(id) = id.identity() { /// Ok(format!("Welcome! {}", id)) /// } else { /// Ok("Welcome Anonymous!".to_owned()) /// } /// } /// -/// fn login(mut req: HttpRequest) -> HttpResponse { -/// req.remember("User1".to_owned()); // <- remember identity +/// fn login(id: Identity) -> HttpResponse { +/// id.remember("User1".to_owned()); // <- remember identity /// HttpResponse::Ok().finish() /// } /// -/// fn logout(mut req: HttpRequest) -> HttpResponse { -/// req.forget(); // <- remove identity +/// fn logout(id: Identity) -> HttpResponse { +/// id.forget(); // <- remove identity /// HttpResponse::Ok().finish() /// } /// # fn main() {} /// ``` -pub trait RequestIdentity { +#[derive(Clone)] +pub struct Identity(HttpRequest); + +impl Identity { /// Return the claimed identity of the user associated request or /// ``None`` if no identity can be found associated with the request. - fn identity(&self) -> Option; + pub fn identity(&self) -> Option { + if let Some(id) = self.0.extensions().get::() { + id.id.clone() + } else { + None + } + } /// Remember identity. - fn remember(&self, identity: String); + pub fn remember(&self, identity: String) { + if let Some(id) = self.0.extensions_mut().get_mut::() { + id.id = Some(identity); + id.changed = true; + } + } /// This method is used to 'forget' the current identity on subsequent /// requests. - fn forget(&self); -} - -impl RequestIdentity for HttpRequest { - fn identity(&self) -> Option { - if let Some(id) = self.extensions().get::() { - return id.0.identity().map(|s| s.to_owned()); - } - None - } - - fn remember(&self, identity: String) { - if let Some(id) = self.extensions_mut().get_mut::() { - return id.0.as_mut().remember(identity); - } - } - - fn forget(&self) { - if let Some(id) = self.extensions_mut().get_mut::() { - return id.0.forget(); + pub fn forget(&self) { + if let Some(id) = self.0.extensions_mut().get_mut::() { + id.id = None; + id.changed = true; } } } -/// An identity -pub trait Identity: 'static { - /// Return the claimed identity of the user associated request or - /// ``None`` if no identity can be found associated with the request. - fn identity(&self) -> Option<&str>; +struct IdentityItem { + id: Option, + changed: bool, +} - /// Remember identity. - fn remember(&mut self, key: String); +/// Extractor implementation for Identity type. +/// +/// ```rust +/// # use actix_web::*; +/// use actix_web::middleware::identity::Identity; +/// +/// fn index(id: Identity) -> String { +/// // access request identity +/// if let Some(id) = id.identity() { +/// format!("Welcome! {}", id) +/// } else { +/// "Welcome Anonymous!".to_owned() +/// } +/// } +/// # fn main() {} +/// ``` +impl

    FromRequest

    for Identity { + type Error = Error; + type Future = Result; + type Config = (); - /// This method is used to 'forget' the current identity on subsequent - /// requests. - fn forget(&mut self); - - /// Write session to storage backend. - fn write(&mut self, resp: HttpResponse) -> Result; + #[inline] + fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { + Ok(Identity(req.clone())) + } } /// Identity policy definition. -pub trait IdentityPolicy: Sized + 'static { - /// The associated identity - type Identity: Identity; +pub trait IdentityPolicy: Sized + 'static { + /// The return type of the middleware + type Future: IntoFuture, Error = Error>; /// The return type of the middleware - type Future: Future; + type ResponseFuture: IntoFuture; /// Parse the session from request and load data from a service identity. - fn from_request(&self, request: &HttpRequest) -> Self::Future; + fn from_request

    (&self, request: &mut ServiceRequest

    ) -> Self::Future; + + /// Write changes to response + fn to_response( + &self, + identity: Option, + changed: bool, + response: &mut ServiceResponse, + ) -> Self::ResponseFuture; } /// Request identity middleware /// /// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; /// use actix_web::App; +/// use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; /// /// fn main() { /// let app = App::new().middleware(IdentityService::new( @@ -165,68 +191,97 @@ pub trait IdentityPolicy: Sized + 'static { /// } /// ``` pub struct IdentityService { - backend: T, + backend: Rc, } impl IdentityService { /// Create new identity service with specified backend. pub fn new(backend: T) -> Self { - IdentityService { backend } - } -} - -struct IdentityBox(Box); - -impl> Middleware for IdentityService { - fn start(&self, req: &HttpRequest) -> Result { - let req = req.clone(); - let fut = self.backend.from_request(&req).then(move |res| match res { - Ok(id) => { - req.extensions_mut().insert(IdentityBox(Box::new(id))); - FutOk(None) - } - Err(err) => FutErr(err), - }); - Ok(Started::Future(Box::new(fut))) - } - - fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { - if let Some(ref mut id) = req.extensions_mut().get_mut::() { - id.0.as_mut().write(resp) - } else { - Ok(Response::Done(resp)) + IdentityService { + backend: Rc::new(backend), } } } -#[doc(hidden)] -/// Identity that uses private cookies as identity storage. -pub struct CookieIdentity { - changed: bool, - identity: Option, - inner: Rc, +impl Transform for IdentityService +where + P: 'static, + S: Service, Response = ServiceResponse> + 'static, + S::Future: 'static, + T: IdentityPolicy, + B: 'static, +{ + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = S::Error; + type InitError = (); + type Transform = IdentityServiceMiddleware; + type Future = FutureResult; + + fn new_transform(&self, service: S) -> Self::Future { + ok(IdentityServiceMiddleware { + backend: self.backend.clone(), + service: Rc::new(RefCell::new(service)), + }) + } } -impl Identity for CookieIdentity { - fn identity(&self) -> Option<&str> { - self.identity.as_ref().map(|s| s.as_ref()) +pub struct IdentityServiceMiddleware { + backend: Rc, + service: Rc>, +} + +impl Service for IdentityServiceMiddleware +where + P: 'static, + B: 'static, + S: Service, Response = ServiceResponse> + 'static, + S::Future: 'static, + T: IdentityPolicy, +{ + type Request = ServiceRequest

    ; + type Response = ServiceResponse; + type Error = S::Error; + type Future = Box>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.service.borrow_mut().poll_ready() } - fn remember(&mut self, value: String) { - self.changed = true; - self.identity = Some(value); - } + fn call(&mut self, mut req: ServiceRequest

    ) -> Self::Future { + let srv = self.service.clone(); + let backend = self.backend.clone(); - fn forget(&mut self) { - self.changed = true; - self.identity = None; - } + Box::new( + self.backend.from_request(&mut req).into_future().then( + move |res| match res { + Ok(id) => { + req.extensions_mut() + .insert(IdentityItem { id, changed: false }); - fn write(&mut self, mut resp: HttpResponse) -> Result { - if self.changed { - let _ = self.inner.set_cookie(&mut resp, self.identity.take()); - } - Ok(Response::Done(resp)) + Either::A(srv.borrow_mut().call(req).and_then(move |mut res| { + let id = + res.request().extensions_mut().remove::(); + + if let Some(id) = id { + return Either::A( + backend + .to_response(id.id, id.changed, &mut res) + .into_future() + .then(move |t| match t { + Ok(_) => Ok(res), + Err(e) => Ok(res.error_response(e)), + }), + ); + } else { + Either::B(ok(res)) + } + })) + } + Err(err) => Either::B(ok(req.error_response(err))), + }, + ), + ) } } @@ -253,7 +308,11 @@ impl CookieIdentityInner { } } - fn set_cookie(&self, resp: &mut HttpResponse, id: Option) -> Result<()> { + fn set_cookie( + &self, + resp: &mut ServiceResponse, + id: Option, + ) -> Result<()> { let some = id.is_some(); { let id = id.unwrap_or_else(String::new); @@ -291,7 +350,7 @@ impl CookieIdentityInner { Ok(()) } - fn load(&self, req: &HttpRequest) -> Option { + fn load(&self, req: &ServiceRequest) -> Option { if let Ok(cookies) = req.cookies() { for cookie in cookies.iter() { if cookie.name() == self.name { @@ -384,16 +443,23 @@ impl CookieIdentityPolicy { } } -impl IdentityPolicy for CookieIdentityPolicy { - type Identity = CookieIdentity; - type Future = FutureResult; +impl IdentityPolicy for CookieIdentityPolicy { + type Future = Result, Error>; + type ResponseFuture = Result<(), Error>; - fn from_request(&self, req: &HttpRequest) -> Self::Future { - let identity = self.0.load(req); - FutOk(CookieIdentity { - identity, - changed: false, - inner: Rc::clone(&self.0), - }) + fn from_request

    (&self, req: &mut ServiceRequest

    ) -> Self::Future { + Ok(self.0.load(req)) + } + + fn to_response( + &self, + id: Option, + changed: bool, + res: &mut ServiceResponse, + ) -> Self::ResponseFuture { + if changed { + let _ = self.0.set_cookie(res, id); + } + Ok(()) } } diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index d63ca893..288c1d63 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -6,8 +6,11 @@ pub use self::compress::Compress; mod defaultheaders; pub use self::defaultheaders::DefaultHeaders; -#[cfg(feature = "session")] -pub use actix_session as session; +// #[cfg(feature = "session")] +// pub use actix_session as session; mod logger; pub use self::logger::Logger; + +#[cfg(feature = "session")] +pub mod identity; diff --git a/src/service.rs b/src/service.rs index f4b63a46..612fe4e8 100644 --- a/src/service.rs +++ b/src/service.rs @@ -82,8 +82,9 @@ impl

    ServiceRequest

    { /// Create service response for error #[inline] - pub fn error_response>(self, err: E) -> ServiceResponse { - ServiceResponse::new(self.req, err.into().into()) + pub fn error_response>(self, err: E) -> ServiceResponse { + let res: Response = err.into().into(); + ServiceResponse::new(self.req, res.into_body()) } /// This method returns reference to the request head @@ -335,6 +336,12 @@ impl ServiceResponse { } } + /// Create service response for error + #[inline] + pub fn error_response>(self, err: E) -> Self { + Self::from_err(err, self.request) + } + /// Get reference to original request #[inline] pub fn request(&self) -> &HttpRequest { From be9031c55ef38b012476524b7562cfd58318931a Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 20:48:05 -0800 Subject: [PATCH 069/109] update doc api --- src/middleware/identity.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/middleware/identity.rs b/src/middleware/identity.rs index d04ed717..74e5f341 100644 --- a/src/middleware/identity.rs +++ b/src/middleware/identity.rs @@ -10,8 +10,7 @@ //! uses cookies as identity storage. //! //! To access current request identity -//! [**RequestIdentity**](trait.RequestIdentity.html) should be used. -//! *HttpRequest* implements *RequestIdentity* trait. +//! [**Identity**](trait.Identity.html) extractor should be used. //! //! ```rust //! use actix_web::middleware::identity::Identity; @@ -226,6 +225,7 @@ where } } +#[doc(hidden)] pub struct IdentityServiceMiddleware { backend: Rc, service: Rc>, From 3a2035a121048234b8cc63215b3df248a4e60c40 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 21:15:26 -0800 Subject: [PATCH 070/109] fix doc tests --- src/middleware/identity.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/middleware/identity.rs b/src/middleware/identity.rs index 74e5f341..7cf739bc 100644 --- a/src/middleware/identity.rs +++ b/src/middleware/identity.rs @@ -26,7 +26,7 @@ //! } //! } //! -//! fn login(id: Idenity) -> HttpResponse { +//! fn login(id: Identity) -> HttpResponse { //! id.remember("User1".to_owned()); // <- remember identity //! HttpResponse::Ok().finish() //! } @@ -41,11 +41,10 @@ //! // <- create identity middleware //! CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend //! .name("auth-cookie") -//! .secure(false)) -//! .service(web::resource("/index.html").to(index) -//! .service(web::resource("/login.html").to(login) -//! .service(web::resource("/logout.html").to(logout) -//! )); +//! .secure(false))) +//! .service(web::resource("/index.html").to(index)) +//! .service(web::resource("/login.html").to(login)) +//! .service(web::resource("/logout.html").to(logout)); //! } //! ``` use std::cell::RefCell; From 9b8812423c0efbb8acadf991f4ab883c6f1fee7b Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 09:20:58 -0700 Subject: [PATCH 071/109] reexport Server controller form actix-server --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 6329d53c..32207024 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,6 +65,7 @@ pub mod dev { Extensions, Payload, PayloadStream, RequestHead, ResponseHead, }; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; + pub use actix_server::Server; pub(crate) fn insert_slash(path: &str) -> String { let mut path = path.to_owned(); From 49d65fb07ace03b8f2750ebe3608255eb350f817 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 09:34:25 -0700 Subject: [PATCH 072/109] move extract to submodule --- src/{extract.rs => extract/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{extract.rs => extract/mod.rs} (100%) diff --git a/src/extract.rs b/src/extract/mod.rs similarity index 100% rename from src/extract.rs rename to src/extract/mod.rs From ee8725b58112d756117bf6fd5601fa14622dfb14 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 10:01:24 -0700 Subject: [PATCH 073/109] move extractors to separate submod --- src/extract/mod.rs | 997 +-------------------------------------------- 1 file changed, 20 insertions(+), 977 deletions(-) diff --git a/src/extract/mod.rs b/src/extract/mod.rs index c34d9df7..5d08dc07 100644 --- a/src/extract/mod.rs +++ b/src/extract/mod.rs @@ -1,32 +1,24 @@ -#![allow(dead_code)] -use std::ops::{Deref, DerefMut}; use std::rc::Rc; -use std::{fmt, str}; -use bytes::Bytes; -use encoding::all::UTF_8; -use encoding::types::{DecoderTrap, Encoding}; -use futures::future::{err, ok, Either, FutureResult}; -use futures::{future, Async, Future, IntoFuture, Poll, Stream}; -use mime::Mime; -use serde::de::{self, DeserializeOwned}; -use serde::Serialize; -use serde_json; -use serde_urlencoded; +use actix_http::error::Error; +use actix_http::Extensions; +use futures::future::ok; +use futures::{future, Async, Future, IntoFuture, Poll}; -use actix_http::dev::{JsonBody, MessageBody, UrlEncoded}; -use actix_http::error::{ - Error, ErrorBadRequest, ErrorNotFound, JsonPayloadError, PayloadError, - UrlencodedError, -}; -use actix_http::http::StatusCode; -use actix_http::{Extensions, HttpMessage, Response}; -use actix_router::PathDeserializer; - -use crate::request::HttpRequest; -use crate::responder::Responder; use crate::service::ServiceFromRequest; +mod form; +mod json; +mod path; +mod payload; +mod query; + +pub use self::form::{Form, FormConfig}; +pub use self::json::{Json, JsonConfig}; +pub use self::path::Path; +pub use self::payload::{Payload, PayloadConfig}; +pub use self::query::Query; + /// Trait implemented by types that can be extracted from request. /// /// Types that implement this trait can be used with `Route` handlers. @@ -72,882 +64,6 @@ impl ExtractorConfig for () { fn store_default(_: &mut ConfigStorage) {} } -#[derive(PartialEq, Eq, PartialOrd, Ord)] -/// Extract typed information from the request's path. -/// -/// ## Example -/// -/// ```rust -/// use actix_web::{web, App}; -/// -/// /// extract path info from "/{username}/{count}/index.html" url -/// /// {username} - deserializes to a String -/// /// {count} - - deserializes to a u32 -/// fn index(info: web::Path<(String, u32)>) -> String { -/// format!("Welcome {}! {}", info.0, info.1) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/{username}/{count}/index.html") // <- define path parameters -/// .route(web::get().to(index)) // <- register handler with `Path` extractor -/// ); -/// } -/// ``` -/// -/// It is possible to extract path information to a specific type that -/// implements `Deserialize` trait from *serde*. -/// -/// ```rust -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App, Error}; -/// -/// #[derive(Deserialize)] -/// struct Info { -/// username: String, -/// } -/// -/// /// extract `Info` from a path using serde -/// fn index(info: web::Path) -> Result { -/// Ok(format!("Welcome {}!", info.username)) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/{username}/index.html") // <- define path parameters -/// .route(web::get().to(index)) // <- use handler with Path` extractor -/// ); -/// } -/// ``` -pub struct Path { - inner: T, -} - -impl AsRef for Path { - fn as_ref(&self) -> &T { - &self.inner - } -} - -impl Deref for Path { - type Target = T; - - fn deref(&self) -> &T { - &self.inner - } -} - -impl DerefMut for Path { - fn deref_mut(&mut self) -> &mut T { - &mut self.inner - } -} - -impl Path { - /// Deconstruct to an inner value - 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: DeserializeOwned, - { - de::Deserialize::deserialize(PathDeserializer::new(req.match_info())) - .map(|inner| Path { inner }) - } -} - -impl From for Path { - fn from(inner: T) -> Path { - Path { inner } - } -} - -/// Extract typed information from the request's path. -/// -/// ## Example -/// -/// ```rust -/// use actix_web::{web, App}; -/// -/// /// extract path info from "/{username}/{count}/index.html" url -/// /// {username} - deserializes to a String -/// /// {count} - - deserializes to a u32 -/// fn index(info: web::Path<(String, u32)>) -> String { -/// format!("Welcome {}! {}", info.0, info.1) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/{username}/{count}/index.html") // <- define path parameters -/// .route(web::get().to(index)) // <- register handler with `Path` extractor -/// ); -/// } -/// ``` -/// -/// It is possible to extract path information to a specific type that -/// implements `Deserialize` trait from *serde*. -/// -/// ```rust -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App, Error}; -/// -/// #[derive(Deserialize)] -/// struct Info { -/// username: String, -/// } -/// -/// /// extract `Info` from a path using serde -/// fn index(info: web::Path) -> Result { -/// Ok(format!("Welcome {}!", info.username)) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/{username}/index.html") // <- define path parameters -/// .route(web::get().to(index)) // <- use handler with Path` extractor -/// ); -/// } -/// ``` -impl FromRequest

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

    ) -> Self::Future { - Self::extract(req).map_err(ErrorNotFound) - } -} - -impl fmt::Debug for Path { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.inner.fmt(f) - } -} - -impl fmt::Display for Path { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.inner.fmt(f) - } -} - -#[derive(PartialEq, Eq, PartialOrd, Ord)] -/// Extract typed information from from the request's query. -/// -/// ## Example -/// -/// ```rust -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App}; -/// -/// #[derive(Debug, Deserialize)] -/// pub enum ResponseType { -/// Token, -/// Code -/// } -/// -/// #[derive(Deserialize)] -/// pub struct AuthRequest { -/// id: u64, -/// response_type: ResponseType, -/// } -/// -/// // Use `Query` extractor for query information. -/// // This handler get called only if request's query contains `username` field -/// // The correct request for this handler would be `/index.html?id=64&response_type=Code"` -/// fn index(info: web::Query) -> String { -/// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/index.html").route(web::get().to(index))); // <- use `Query` extractor -/// } -/// ``` -pub struct Query(T); - -impl Deref for Query { - type Target = T; - - fn deref(&self) -> &T { - &self.0 - } -} - -impl DerefMut for Query { - fn deref_mut(&mut self) -> &mut T { - &mut self.0 - } -} - -impl Query { - /// Deconstruct to a inner value - pub fn into_inner(self) -> T { - self.0 - } -} - -/// Extract typed information from from the request's query. -/// -/// ## Example -/// -/// ```rust -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App}; -/// -/// #[derive(Debug, Deserialize)] -/// pub enum ResponseType { -/// Token, -/// Code -/// } -/// -/// #[derive(Deserialize)] -/// pub struct AuthRequest { -/// id: u64, -/// response_type: ResponseType, -/// } -/// -/// // Use `Query` extractor for query information. -/// // This handler get called only if request's query contains `username` field -/// // The correct request for this handler would be `/index.html?id=64&response_type=Code"` -/// fn index(info: web::Query) -> String { -/// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/index.html") -/// .route(web::get().to(index))); // <- use `Query` extractor -/// } -/// ``` -impl FromRequest

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

    ) -> Self::Future { - serde_urlencoded::from_str::(req.query_string()) - .map(|val| Ok(Query(val))) - .unwrap_or_else(|e| Err(e.into())) - } -} - -impl fmt::Debug for Query { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl fmt::Display for Query { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -#[derive(PartialEq, Eq, PartialOrd, Ord)] -/// Extract typed information from the request's body. -/// -/// To extract typed information from request's body, the type `T` must -/// implement the `Deserialize` trait from *serde*. -/// -/// [**FormConfig**](struct.FormConfig.html) allows to configure extraction -/// process. -/// -/// ## Example -/// -/// ```rust -/// # extern crate actix_web; -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App}; -/// -/// #[derive(Deserialize)] -/// struct FormData { -/// username: String, -/// } -/// -/// /// Extract form data using serde. -/// /// This handler get called only if content type is *x-www-form-urlencoded* -/// /// and content of the request could be deserialized to a `FormData` struct -/// fn index(form: web::Form) -> String { -/// format!("Welcome {}!", form.username) -/// } -/// # fn main() {} -/// ``` -pub struct Form(pub T); - -impl Form { - /// Deconstruct to an inner value - pub fn into_inner(self) -> T { - self.0 - } -} - -impl Deref for Form { - type Target = T; - - fn deref(&self) -> &T { - &self.0 - } -} - -impl DerefMut for Form { - fn deref_mut(&mut self) -> &mut T { - &mut self.0 - } -} - -impl FromRequest

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

    ) -> Self::Future { - let req2 = req.clone(); - let cfg = req.load_config::(); - - let limit = cfg.limit; - let err = Rc::clone(&cfg.ehandler); - Box::new( - UrlEncoded::new(req) - .limit(limit) - .map_err(move |e| (*err)(e, &req2)) - .map(Form), - ) - } -} - -impl fmt::Debug for Form { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl fmt::Display for Form { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -/// Form extractor configuration -/// -/// ```rust -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App, Result}; -/// -/// #[derive(Deserialize)] -/// struct FormData { -/// username: String, -/// } -/// -/// /// Extract form data using serde. -/// /// Custom configuration is used for this handler, max payload size is 4k -/// fn index(form: web::Form) -> Result { -/// Ok(format!("Welcome {}!", form.username)) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/index.html") -/// .route(web::get() -/// // change `Form` extractor configuration -/// .config(web::FormConfig::default().limit(4097)) -/// .to(index)) -/// ); -/// } -/// ``` -#[derive(Clone)] -pub struct FormConfig { - limit: usize, - ehandler: Rc Error>, -} - -impl FormConfig { - /// Change max size of payload. By default max size is 256Kb - pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; - self - } - - /// Set custom error handler - pub fn error_handler(mut self, f: F) -> Self - where - F: Fn(UrlencodedError, &HttpRequest) -> Error + 'static, - { - self.ehandler = Rc::new(f); - self - } -} - -impl ExtractorConfig for FormConfig {} - -impl Default for FormConfig { - fn default() -> Self { - FormConfig { - limit: 262_144, - ehandler: Rc::new(|e, _| e.into()), - } - } -} - -/// Json helper -/// -/// Json can be used for two different purpose. First is for json response -/// generation and second is for extracting typed information from request's -/// payload. -/// -/// To extract typed information from request's body, the type `T` must -/// implement the `Deserialize` trait from *serde*. -/// -/// [**JsonConfig**](struct.JsonConfig.html) allows to configure extraction -/// process. -/// -/// ## Example -/// -/// ```rust -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App}; -/// -/// #[derive(Deserialize)] -/// struct Info { -/// username: String, -/// } -/// -/// /// deserialize `Info` from request's body -/// fn index(info: web::Json) -> String { -/// format!("Welcome {}!", info.username) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/index.html").route( -/// web::post().to(index)) -/// ); -/// } -/// ``` -/// -/// The `Json` type allows you to respond with well-formed JSON data: simply -/// return a value of type Json where T is the type of a structure -/// to serialize into *JSON*. The type `T` must implement the `Serialize` -/// trait from *serde*. -/// -/// ```rust -/// # #[macro_use] extern crate serde_derive; -/// # use actix_web::*; -/// # -/// #[derive(Serialize)] -/// struct MyObj { -/// name: String, -/// } -/// -/// fn index(req: HttpRequest) -> Result> { -/// Ok(web::Json(MyObj { -/// name: req.match_info().get("name").unwrap().to_string(), -/// })) -/// } -/// # fn main() {} -/// ``` -pub struct Json(pub T); - -impl Json { - /// Deconstruct to an inner value - pub fn into_inner(self) -> T { - self.0 - } -} - -impl Deref for Json { - type Target = T; - - fn deref(&self) -> &T { - &self.0 - } -} - -impl DerefMut for Json { - fn deref_mut(&mut self) -> &mut T { - &mut self.0 - } -} - -impl fmt::Debug for Json -where - T: fmt::Debug, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Json: {:?}", self.0) - } -} - -impl fmt::Display for Json -where - T: fmt::Display, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.0, f) - } -} - -impl Responder for Json { - type Error = Error; - type Future = Result; - - fn respond_to(self, _: &HttpRequest) -> Self::Future { - let body = match serde_json::to_string(&self.0) { - Ok(body) => body, - Err(e) => return Err(e.into()), - }; - - Ok(Response::build(StatusCode::OK) - .content_type("application/json") - .body(body)) - } -} - -/// Json extractor. Allow to extract typed information from request's -/// payload. -/// -/// To extract typed information from request's body, the type `T` must -/// implement the `Deserialize` trait from *serde*. -/// -/// [**JsonConfig**](struct.JsonConfig.html) allows to configure extraction -/// process. -/// -/// ## Example -/// -/// ```rust -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App}; -/// -/// #[derive(Deserialize)] -/// struct Info { -/// username: String, -/// } -/// -/// /// deserialize `Info` from request's body -/// fn index(info: web::Json) -> String { -/// format!("Welcome {}!", info.username) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/index.html").route( -/// web::post().to(index)) -/// ); -/// } -/// ``` -impl FromRequest

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

    ) -> Self::Future { - let req2 = req.clone(); - let cfg = req.load_config::(); - - let limit = cfg.limit; - let err = Rc::clone(&cfg.ehandler); - Box::new( - JsonBody::new(req) - .limit(limit) - .map_err(move |e| (*err)(e, &req2)) - .map(Json), - ) - } -} - -/// Json extractor configuration -/// -/// ```rust -/// #[macro_use] extern crate serde_derive; -/// use actix_web::{error, web, App, HttpResponse}; -/// -/// #[derive(Deserialize)] -/// struct Info { -/// username: String, -/// } -/// -/// /// deserialize `Info` from request's body, max payload size is 4kb -/// fn index(info: web::Json) -> String { -/// format!("Welcome {}!", info.username) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/index.html").route( -/// web::post().config( -/// // 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() -/// })) -/// .to(index)) -/// ); -/// } -/// ``` -#[derive(Clone)] -pub struct JsonConfig { - limit: usize, - ehandler: Rc Error>, -} - -impl JsonConfig { - /// Change max size of payload. By default max size is 256Kb - pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; - self - } - - /// Set custom error handler - pub fn error_handler(mut self, f: F) -> Self - where - F: Fn(JsonPayloadError, &HttpRequest) -> Error + 'static, - { - self.ehandler = Rc::new(f); - self - } -} - -impl ExtractorConfig for JsonConfig {} - -impl Default for JsonConfig { - fn default() -> Self { - JsonConfig { - limit: 262_144, - ehandler: Rc::new(|e, _| e.into()), - } - } -} - -/// Payload extractor returns request 's payload stream. -/// -/// ## Example -/// -/// ```rust -/// use futures::{Future, Stream}; -/// use actix_web::{web, error, App, Error, HttpResponse}; -/// -/// /// extract binary data from request -/// fn index

    (body: web::Payload

    ) -> impl Future -/// where -/// P: Stream -/// { -/// body.map_err(Error::from) -/// .fold(web::BytesMut::new(), move |mut body, chunk| { -/// body.extend_from_slice(&chunk); -/// Ok::<_, Error>(body) -/// }) -/// .and_then(|body| { -/// format!("Body {:?}!", body); -/// Ok(HttpResponse::Ok().finish()) -/// }) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/index.html").route( -/// web::get().to_async(index)) -/// ); -/// } -/// ``` -pub struct Payload(crate::dev::Payload); - -impl Stream for Payload -where - T: Stream, -{ - type Item = Bytes; - type Error = PayloadError; - - #[inline] - fn poll(&mut self) -> Poll, PayloadError> { - self.0.poll() - } -} - -/// Get request's payload stream -/// -/// ## Example -/// -/// ```rust -/// use futures::{Future, Stream}; -/// use actix_web::{web, error, App, Error, HttpResponse}; -/// -/// /// extract binary data from request -/// fn index

    (body: web::Payload

    ) -> impl Future -/// where -/// P: Stream -/// { -/// body.map_err(Error::from) -/// .fold(web::BytesMut::new(), move |mut body, chunk| { -/// body.extend_from_slice(&chunk); -/// Ok::<_, Error>(body) -/// }) -/// .and_then(|body| { -/// format!("Body {:?}!", body); -/// Ok(HttpResponse::Ok().finish()) -/// }) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/index.html").route( -/// web::get().to_async(index)) -/// ); -/// } -/// ``` -impl

    FromRequest

    for Payload

    -where - P: Stream, -{ - type Error = Error; - type Future = Result, Error>; - type Config = (); - - #[inline] - fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - Ok(Payload(req.take_payload())) - } -} - -/// Request binary data from a request's payload. -/// -/// Loads request's payload and construct Bytes instance. -/// -/// [**PayloadConfig**](struct.PayloadConfig.html) allows to configure -/// extraction process. -/// -/// ## Example -/// -/// ```rust -/// use bytes::Bytes; -/// use actix_web::{web, App}; -/// -/// /// extract binary data from request -/// fn index(body: Bytes) -> String { -/// format!("Body {:?}!", body) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/index.html").route( -/// web::get().to(index)) -/// ); -/// } -/// ``` -impl

    FromRequest

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

    ) -> Self::Future { - let cfg = req.load_config::(); - - if let Err(e) = cfg.check_mimetype(req) { - return Either::B(err(e)); - } - - let limit = cfg.limit; - Either::A(Box::new(MessageBody::new(req).limit(limit).from_err())) - } -} - -/// Extract text information from a request's body. -/// -/// Text extractor automatically decode body according to the request's charset. -/// -/// [**PayloadConfig**](struct.PayloadConfig.html) allows to configure -/// extraction process. -/// -/// ## Example -/// -/// ```rust -/// use actix_web::{web, App}; -/// -/// /// extract text data from request -/// fn index(text: String) -> String { -/// format!("Body {}!", text) -/// } -/// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/index.html").route( -/// web::get() -/// .config(web::PayloadConfig::new(4096)) // <- limit size of the payload -/// .to(index)) // <- register handler with extractor params -/// ); -/// } -/// ``` -impl

    FromRequest

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

    ) -> Self::Future { - let cfg = req.load_config::(); - - // check content-type - if let Err(e) = cfg.check_mimetype(req) { - return Either::B(err(e)); - } - - // check charset - let encoding = match req.encoding() { - Ok(enc) => enc, - Err(e) => return Either::B(err(e.into())), - }; - let limit = cfg.limit; - - Either::A(Box::new( - MessageBody::new(req) - .limit(limit) - .from_err() - .and_then(move |body| { - let enc: *const Encoding = encoding as *const Encoding; - if enc == UTF_8 { - Ok(str::from_utf8(body.as_ref()) - .map_err(|_| ErrorBadRequest("Can not decode body"))? - .to_owned()) - } else { - Ok(encoding - .decode(&body, DecoderTrap::Strict) - .map_err(|_| ErrorBadRequest("Can not decode body"))?) - } - }), - )) - } -} - /// Optionally extract a field from the request /// /// If the FromRequest for T fails, return None rather than returning an error response @@ -1009,7 +125,10 @@ where 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(_) => future::ok(None), + Err(e) => { + log::debug!("Error for Option extractor: {}", e.into()); + future::ok(None) + } })) } } @@ -1078,64 +197,6 @@ where } } -/// Payload configuration for request's payload. -#[derive(Clone)] -pub struct PayloadConfig { - limit: usize, - mimetype: Option, -} - -impl PayloadConfig { - /// Create `PayloadConfig` instance and set max size of payload. - pub fn new(limit: usize) -> Self { - Self::default().limit(limit) - } - - /// Change max size of payload. By default max size is 256Kb - pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; - self - } - - /// Set required mime-type of the request. By default mime type is not - /// enforced. - pub fn mimetype(mut self, mt: Mime) -> Self { - self.mimetype = Some(mt); - self - } - - fn check_mimetype

    (&self, req: &ServiceFromRequest

    ) -> Result<(), Error> { - // check content-type - if let Some(ref mt) = self.mimetype { - match req.mime_type() { - Ok(Some(ref req_mt)) => { - if mt != req_mt { - return Err(ErrorBadRequest("Unexpected Content-Type")); - } - } - Ok(None) => { - return Err(ErrorBadRequest("Content-Type is expected")); - } - Err(err) => { - return Err(err.into()); - } - } - } - Ok(()) - } -} - -impl ExtractorConfig for PayloadConfig {} - -impl Default for PayloadConfig { - fn default() -> Self { - PayloadConfig { - limit: 262_144, - mimetype: None, - } - } -} - #[doc(hidden)] impl

    FromRequest

    for () { type Error = Error; @@ -1360,24 +421,6 @@ mod tests { assert!(r.is_err()); } - #[test] - fn test_payload_config() { - let req = TestRequest::default().to_from(); - let cfg = PayloadConfig::default().mimetype(mime::APPLICATION_JSON); - assert!(cfg.check_mimetype(&req).is_err()); - - let req = TestRequest::with_header( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ) - .to_from(); - assert!(cfg.check_mimetype(&req).is_err()); - - let req = - TestRequest::with_header(header::CONTENT_TYPE, "application/json").to_from(); - assert!(cfg.check_mimetype(&req).is_ok()); - } - #[derive(Deserialize)] struct MyStruct { key: String, From 16c42be4a2a753f0ec2f96875a37a0487bfbe4e5 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 10:53:56 -0700 Subject: [PATCH 074/109] simplify extractor configuration, config is optional now --- src/extract/form.rs | 197 +++++++++++++++++++++++ src/extract/json.rs | 259 ++++++++++++++++++++++++++++++ src/extract/mod.rs | 75 +-------- src/extract/path.rs | 176 +++++++++++++++++++++ src/extract/payload.rs | 313 +++++++++++++++++++++++++++++++++++++ src/extract/query.rs | 126 +++++++++++++++ src/middleware/identity.rs | 1 - src/request.rs | 1 - src/route.rs | 18 ++- src/service.rs | 10 +- src/state.rs | 1 - 11 files changed, 1086 insertions(+), 91 deletions(-) create mode 100644 src/extract/form.rs create mode 100644 src/extract/json.rs create mode 100644 src/extract/path.rs create mode 100644 src/extract/payload.rs create mode 100644 src/extract/query.rs diff --git a/src/extract/form.rs b/src/extract/form.rs new file mode 100644 index 00000000..19849ac8 --- /dev/null +++ b/src/extract/form.rs @@ -0,0 +1,197 @@ +//! Form extractor + +use std::rc::Rc; +use std::{fmt, ops}; + +use actix_http::dev::UrlEncoded; +use actix_http::error::{Error, UrlencodedError}; +use bytes::Bytes; +use futures::{Future, Stream}; +use serde::de::DeserializeOwned; + +use crate::extract::FromRequest; +use crate::request::HttpRequest; +use crate::service::ServiceFromRequest; + +#[derive(PartialEq, Eq, PartialOrd, Ord)] +/// Extract typed information from the request's body. +/// +/// To extract typed information from request's body, the type `T` must +/// implement the `Deserialize` trait from *serde*. +/// +/// [**FormConfig**](struct.FormConfig.html) allows to configure extraction +/// process. +/// +/// ## Example +/// +/// ```rust +/// # extern crate actix_web; +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App}; +/// +/// #[derive(Deserialize)] +/// struct FormData { +/// username: String, +/// } +/// +/// /// Extract form data using serde. +/// /// This handler get called only if content type is *x-www-form-urlencoded* +/// /// and content of the request could be deserialized to a `FormData` struct +/// fn index(form: web::Form) -> String { +/// format!("Welcome {}!", form.username) +/// } +/// # fn main() {} +/// ``` +pub struct Form(pub T); + +impl Form { + /// Deconstruct to an inner value + pub fn into_inner(self) -> T { + self.0 + } +} + +impl ops::Deref for Form { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +impl ops::DerefMut for Form { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl FromRequest

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

    ) -> Self::Future { + let req2 = req.clone(); + let (limit, err) = req + .load_config::() + .map(|c| (c.limit, c.ehandler.clone())) + .unwrap_or((16384, None)); + + Box::new( + UrlEncoded::new(req) + .limit(limit) + .map_err(move |e| { + if let Some(err) = err { + (*err)(e, &req2) + } else { + e.into() + } + }) + .map(Form), + ) + } +} + +impl fmt::Debug for Form { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl fmt::Display for Form { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +/// Form extractor configuration +/// +/// ```rust +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App, Result}; +/// +/// #[derive(Deserialize)] +/// struct FormData { +/// username: String, +/// } +/// +/// /// Extract form data using serde. +/// /// Custom configuration is used for this handler, max payload size is 4k +/// fn index(form: web::Form) -> Result { +/// Ok(format!("Welcome {}!", form.username)) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html") +/// .route(web::get() +/// // change `Form` extractor configuration +/// .config(web::FormConfig::default().limit(4097)) +/// .to(index)) +/// ); +/// } +/// ``` +#[derive(Clone)] +pub struct FormConfig { + limit: usize, + ehandler: Option Error>>, +} + +impl FormConfig { + /// Change max size of payload. By default max size is 16Kb + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } + + /// Set custom error handler + pub fn error_handler(mut self, f: F) -> Self + where + F: Fn(UrlencodedError, &HttpRequest) -> Error + 'static, + { + self.ehandler = Some(Rc::new(f)); + self + } +} + +impl Default for FormConfig { + fn default() -> Self { + FormConfig { + limit: 16384, + ehandler: None, + } + } +} + +#[cfg(test)] +mod tests { + use actix_http::http::header; + use bytes::Bytes; + use serde_derive::Deserialize; + + use super::*; + use crate::test::{block_on, TestRequest}; + + #[derive(Deserialize, Debug, PartialEq)] + struct Info { + hello: String, + } + + #[test] + fn test_form() { + let mut req = 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(); + + let s = block_on(Form::::from_request(&mut req)).unwrap(); + assert_eq!(s.hello, "world"); + } +} diff --git a/src/extract/json.rs b/src/extract/json.rs new file mode 100644 index 00000000..f74b082d --- /dev/null +++ b/src/extract/json.rs @@ -0,0 +1,259 @@ +//! Json extractor/responder + +use std::rc::Rc; +use std::{fmt, ops}; + +use bytes::Bytes; +use futures::{Future, Stream}; +use serde::de::DeserializeOwned; +use serde::Serialize; +use serde_json; + +use actix_http::dev::JsonBody; +use actix_http::error::{Error, JsonPayloadError}; +use actix_http::http::StatusCode; +use actix_http::Response; + +use crate::extract::FromRequest; +use crate::request::HttpRequest; +use crate::responder::Responder; +use crate::service::ServiceFromRequest; + +/// Json helper +/// +/// Json can be used for two different purpose. First is for json response +/// generation and second is for extracting typed information from request's +/// payload. +/// +/// To extract typed information from request's body, the type `T` must +/// implement the `Deserialize` trait from *serde*. +/// +/// [**JsonConfig**](struct.JsonConfig.html) allows to configure extraction +/// process. +/// +/// ## Example +/// +/// ```rust +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App}; +/// +/// #[derive(Deserialize)] +/// struct Info { +/// username: String, +/// } +/// +/// /// deserialize `Info` from request's body +/// fn index(info: web::Json) -> String { +/// format!("Welcome {}!", info.username) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::post().to(index)) +/// ); +/// } +/// ``` +/// +/// The `Json` type allows you to respond with well-formed JSON data: simply +/// return a value of type Json where T is the type of a structure +/// to serialize into *JSON*. The type `T` must implement the `Serialize` +/// trait from *serde*. +/// +/// ```rust +/// # #[macro_use] extern crate serde_derive; +/// # use actix_web::*; +/// # +/// #[derive(Serialize)] +/// struct MyObj { +/// name: String, +/// } +/// +/// fn index(req: HttpRequest) -> Result> { +/// Ok(web::Json(MyObj { +/// name: req.match_info().get("name").unwrap().to_string(), +/// })) +/// } +/// # fn main() {} +/// ``` +pub struct Json(pub T); + +impl Json { + /// Deconstruct to an inner value + pub fn into_inner(self) -> T { + self.0 + } +} + +impl ops::Deref for Json { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +impl ops::DerefMut for Json { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl fmt::Debug for Json +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Json: {:?}", self.0) + } +} + +impl fmt::Display for Json +where + T: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl Responder for Json { + type Error = Error; + type Future = Result; + + fn respond_to(self, _: &HttpRequest) -> Self::Future { + let body = match serde_json::to_string(&self.0) { + Ok(body) => body, + Err(e) => return Err(e.into()), + }; + + Ok(Response::build(StatusCode::OK) + .content_type("application/json") + .body(body)) + } +} + +/// Json extractor. Allow to extract typed information from request's +/// payload. +/// +/// To extract typed information from request's body, the type `T` must +/// implement the `Deserialize` trait from *serde*. +/// +/// [**JsonConfig**](struct.JsonConfig.html) allows to configure extraction +/// process. +/// +/// ## Example +/// +/// ```rust +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App}; +/// +/// #[derive(Deserialize)] +/// struct Info { +/// username: String, +/// } +/// +/// /// deserialize `Info` from request's body +/// fn index(info: web::Json) -> String { +/// format!("Welcome {}!", info.username) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::post().to(index)) +/// ); +/// } +/// ``` +impl FromRequest

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

    ) -> Self::Future { + let req2 = req.clone(); + let (limit, err) = req + .load_config::() + .map(|c| (c.limit, c.ehandler.clone())) + .unwrap_or((32768, None)); + + Box::new( + JsonBody::new(req) + .limit(limit) + .map_err(move |e| { + if let Some(err) = err { + (*err)(e, &req2) + } else { + e.into() + } + }) + .map(Json), + ) + } +} + +/// Json extractor configuration +/// +/// ```rust +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{error, web, App, HttpResponse}; +/// +/// #[derive(Deserialize)] +/// struct Info { +/// username: String, +/// } +/// +/// /// deserialize `Info` from request's body, max payload size is 4kb +/// fn index(info: web::Json) -> String { +/// format!("Welcome {}!", info.username) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::post().config( +/// // 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() +/// })) +/// .to(index)) +/// ); +/// } +/// ``` +#[derive(Clone)] +pub struct JsonConfig { + limit: usize, + ehandler: Option Error>>, +} + +impl JsonConfig { + /// Change max size of payload. By default max size is 32Kb + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } + + /// Set custom error handler + pub fn error_handler(mut self, f: F) -> Self + where + F: Fn(JsonPayloadError, &HttpRequest) -> Error + 'static, + { + self.ehandler = Some(Rc::new(f)); + self + } +} + +impl Default for JsonConfig { + fn default() -> Self { + JsonConfig { + limit: 32768, + ehandler: None, + } + } +} diff --git a/src/extract/mod.rs b/src/extract/mod.rs index 5d08dc07..d8958b2d 100644 --- a/src/extract/mod.rs +++ b/src/extract/mod.rs @@ -1,7 +1,6 @@ -use std::rc::Rc; +//! Request extractors use actix_http::error::Error; -use actix_http::Extensions; use futures::future::ok; use futures::{future, Async, Future, IntoFuture, Poll}; @@ -29,41 +28,10 @@ pub trait FromRequest

    : Sized { /// Future that resolves to a Self type Future: IntoFuture; - /// Configuration for the extractor - type Config: ExtractorConfig; - /// Convert request to a Self fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future; } -/// Storage for extractor configs -#[derive(Default)] -pub struct ConfigStorage { - pub(crate) storage: Option>, -} - -impl ConfigStorage { - pub fn store(&mut self, config: C) { - if self.storage.is_none() { - self.storage = Some(Rc::new(Extensions::new())); - } - if let Some(ref mut ext) = self.storage { - Rc::get_mut(ext).unwrap().insert(config); - } - } -} - -pub trait ExtractorConfig: Default + Clone + 'static { - /// Set default configuration to config storage - fn store_default(ext: &mut ConfigStorage) { - ext.store(Self::default()) - } -} - -impl ExtractorConfig for () { - fn store_default(_: &mut ConfigStorage) {} -} - /// Optionally extract a field from the request /// /// If the FromRequest for T fails, return None rather than returning an error response @@ -84,7 +52,6 @@ impl ExtractorConfig for () { /// impl

    FromRequest

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

    ) -> Self::Future { /// if rand::random() { @@ -119,7 +86,6 @@ where { type Error = Error; type Future = Box, Error = Error>>; - type Config = T::Config; #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { @@ -153,7 +119,6 @@ where /// impl

    FromRequest

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

    ) -> Self::Future { /// if rand::random() { @@ -186,7 +151,6 @@ where { type Error = Error; type Future = Box, Error = Error>>; - type Config = T::Config; #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { @@ -201,23 +165,12 @@ where impl

    FromRequest

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

    ) -> Self::Future { Ok(()) } } -macro_rules! tuple_config ({ $($T:ident),+} => { - impl<$($T,)+> ExtractorConfig for ($($T,)+) - where $($T: ExtractorConfig + Clone,)+ - { - fn store_default(ext: &mut ConfigStorage) { - $($T::store_default(ext);)+ - } - } -}); - macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { /// FromRequest implementation for tuple @@ -226,7 +179,6 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { { type Error = Error; type Future = $fut_type; - type Config = ($($T::Config,)+); fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { $fut_type { @@ -277,17 +229,6 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { mod m { use super::*; -tuple_config!(A); -tuple_config!(A, B); -tuple_config!(A, B, C); -tuple_config!(A, B, C, D); -tuple_config!(A, B, C, D, E); -tuple_config!(A, B, C, D, E, F); -tuple_config!(A, B, C, D, E, F, G); -tuple_config!(A, B, C, D, E, F, G, H); -tuple_config!(A, B, C, D, E, F, G, H, I); -tuple_config!(A, B, C, D, E, F, G, H, I, J); - tuple_from_req!(TupleFromRequest1, (0, A)); tuple_from_req!(TupleFromRequest2, (0, A), (1, B)); tuple_from_req!(TupleFromRequest3, (0, A), (1, B), (2, C)); @@ -335,20 +276,6 @@ mod tests { assert_eq!(s, "hello=world"); } - #[test] - fn test_form() { - let mut req = 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(); - - let s = block_on(Form::::from_request(&mut req)).unwrap(); - assert_eq!(s.hello, "world"); - } - #[test] fn test_option() { let mut req = TestRequest::with_header( diff --git a/src/extract/path.rs b/src/extract/path.rs new file mode 100644 index 00000000..d9461263 --- /dev/null +++ b/src/extract/path.rs @@ -0,0 +1,176 @@ +//! Path extractor + +use std::{fmt, ops}; + +use actix_http::error::{Error, ErrorNotFound}; +use actix_router::PathDeserializer; +use serde::de; + +use crate::request::HttpRequest; +use crate::service::ServiceFromRequest; + +use super::FromRequest; + +#[derive(PartialEq, Eq, PartialOrd, Ord)] +/// Extract typed information from the request's path. +/// +/// ## Example +/// +/// ```rust +/// use actix_web::{web, App}; +/// +/// /// extract path info from "/{username}/{count}/index.html" url +/// /// {username} - deserializes to a String +/// /// {count} - - deserializes to a u32 +/// fn index(info: web::Path<(String, u32)>) -> String { +/// format!("Welcome {}! {}", info.0, info.1) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/{username}/{count}/index.html") // <- define path parameters +/// .route(web::get().to(index)) // <- register handler with `Path` extractor +/// ); +/// } +/// ``` +/// +/// It is possible to extract path information to a specific type that +/// implements `Deserialize` trait from *serde*. +/// +/// ```rust +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App, Error}; +/// +/// #[derive(Deserialize)] +/// struct Info { +/// username: String, +/// } +/// +/// /// extract `Info` from a path using serde +/// fn index(info: web::Path) -> Result { +/// Ok(format!("Welcome {}!", info.username)) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/{username}/index.html") // <- define path parameters +/// .route(web::get().to(index)) // <- use handler with Path` extractor +/// ); +/// } +/// ``` +pub struct Path { + inner: T, +} + +impl Path { + /// Deconstruct to an inner value + 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 { + fn as_ref(&self) -> &T { + &self.inner + } +} + +impl ops::Deref for Path { + type Target = T; + + fn deref(&self) -> &T { + &self.inner + } +} + +impl ops::DerefMut for Path { + fn deref_mut(&mut self) -> &mut T { + &mut self.inner + } +} + +impl From for Path { + fn from(inner: T) -> Path { + Path { inner } + } +} + +impl fmt::Debug for Path { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.inner.fmt(f) + } +} + +impl fmt::Display for Path { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.inner.fmt(f) + } +} + +/// Extract typed information from the request's path. +/// +/// ## Example +/// +/// ```rust +/// use actix_web::{web, App}; +/// +/// /// extract path info from "/{username}/{count}/index.html" url +/// /// {username} - deserializes to a String +/// /// {count} - - deserializes to a u32 +/// fn index(info: web::Path<(String, u32)>) -> String { +/// format!("Welcome {}! {}", info.0, info.1) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/{username}/{count}/index.html") // <- define path parameters +/// .route(web::get().to(index)) // <- register handler with `Path` extractor +/// ); +/// } +/// ``` +/// +/// It is possible to extract path information to a specific type that +/// implements `Deserialize` trait from *serde*. +/// +/// ```rust +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App, Error}; +/// +/// #[derive(Deserialize)] +/// struct Info { +/// username: String, +/// } +/// +/// /// extract `Info` from a path using serde +/// fn index(info: web::Path) -> Result { +/// Ok(format!("Welcome {}!", info.username)) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/{username}/index.html") // <- define path parameters +/// .route(web::get().to(index)) // <- use handler with Path` extractor +/// ); +/// } +/// ``` +impl FromRequest

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

    ) -> Self::Future { + Self::extract(req).map_err(ErrorNotFound) + } +} diff --git a/src/extract/payload.rs b/src/extract/payload.rs new file mode 100644 index 00000000..82024c0d --- /dev/null +++ b/src/extract/payload.rs @@ -0,0 +1,313 @@ +//! Payload/Bytes/String extractors +use std::str; + +use actix_http::dev::MessageBody; +use actix_http::error::{Error, ErrorBadRequest, PayloadError}; +use actix_http::HttpMessage; +use bytes::Bytes; +use encoding::all::UTF_8; +use encoding::types::{DecoderTrap, Encoding}; +use futures::future::{err, Either, FutureResult}; +use futures::{Future, Poll, Stream}; +use mime::Mime; + +use crate::extract::FromRequest; +use crate::service::ServiceFromRequest; + +/// Payload extractor returns request 's payload stream. +/// +/// ## Example +/// +/// ```rust +/// use futures::{Future, Stream}; +/// use actix_web::{web, error, App, Error, HttpResponse}; +/// +/// /// extract binary data from request +/// fn index

    (body: web::Payload

    ) -> impl Future +/// where +/// P: Stream +/// { +/// body.map_err(Error::from) +/// .fold(web::BytesMut::new(), move |mut body, chunk| { +/// body.extend_from_slice(&chunk); +/// Ok::<_, Error>(body) +/// }) +/// .and_then(|body| { +/// format!("Body {:?}!", body); +/// Ok(HttpResponse::Ok().finish()) +/// }) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::get().to_async(index)) +/// ); +/// } +/// ``` +pub struct Payload(crate::dev::Payload); + +impl Stream for Payload +where + T: Stream, +{ + type Item = Bytes; + type Error = PayloadError; + + #[inline] + fn poll(&mut self) -> Poll, PayloadError> { + self.0.poll() + } +} + +/// Get request's payload stream +/// +/// ## Example +/// +/// ```rust +/// use futures::{Future, Stream}; +/// use actix_web::{web, error, App, Error, HttpResponse}; +/// +/// /// extract binary data from request +/// fn index

    (body: web::Payload

    ) -> impl Future +/// where +/// P: Stream +/// { +/// body.map_err(Error::from) +/// .fold(web::BytesMut::new(), move |mut body, chunk| { +/// body.extend_from_slice(&chunk); +/// Ok::<_, Error>(body) +/// }) +/// .and_then(|body| { +/// format!("Body {:?}!", body); +/// Ok(HttpResponse::Ok().finish()) +/// }) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::get().to_async(index)) +/// ); +/// } +/// ``` +impl

    FromRequest

    for Payload

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

    ) -> Self::Future { + Ok(Payload(req.take_payload())) + } +} + +/// Request binary data from a request's payload. +/// +/// Loads request's payload and construct Bytes instance. +/// +/// [**PayloadConfig**](struct.PayloadConfig.html) allows to configure +/// extraction process. +/// +/// ## Example +/// +/// ```rust +/// use bytes::Bytes; +/// use actix_web::{web, App}; +/// +/// /// extract binary data from request +/// fn index(body: Bytes) -> String { +/// format!("Body {:?}!", body) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::get().to(index)) +/// ); +/// } +/// ``` +impl

    FromRequest

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

    ) -> Self::Future { + let mut tmp; + let cfg = if let Some(cfg) = req.load_config::() { + cfg + } else { + tmp = PayloadConfig::default(); + &tmp + }; + + if let Err(e) = cfg.check_mimetype(req) { + return Either::B(err(e)); + } + + let limit = cfg.limit; + Either::A(Box::new(MessageBody::new(req).limit(limit).from_err())) + } +} + +/// Extract text information from a request's body. +/// +/// Text extractor automatically decode body according to the request's charset. +/// +/// [**PayloadConfig**](struct.PayloadConfig.html) allows to configure +/// extraction process. +/// +/// ## Example +/// +/// ```rust +/// use actix_web::{web, App}; +/// +/// /// extract text data from request +/// fn index(text: String) -> String { +/// format!("Body {}!", text) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::get() +/// .config(web::PayloadConfig::new(4096)) // <- limit size of the payload +/// .to(index)) // <- register handler with extractor params +/// ); +/// } +/// ``` +impl

    FromRequest

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

    ) -> Self::Future { + let mut tmp; + let cfg = if let Some(cfg) = req.load_config::() { + cfg + } else { + tmp = PayloadConfig::default(); + &tmp + }; + + // check content-type + if let Err(e) = cfg.check_mimetype(req) { + return Either::B(err(e)); + } + + // check charset + let encoding = match req.encoding() { + Ok(enc) => enc, + Err(e) => return Either::B(err(e.into())), + }; + let limit = cfg.limit; + + Either::A(Box::new( + MessageBody::new(req) + .limit(limit) + .from_err() + .and_then(move |body| { + let enc: *const Encoding = encoding as *const Encoding; + if enc == UTF_8 { + Ok(str::from_utf8(body.as_ref()) + .map_err(|_| ErrorBadRequest("Can not decode body"))? + .to_owned()) + } else { + Ok(encoding + .decode(&body, DecoderTrap::Strict) + .map_err(|_| ErrorBadRequest("Can not decode body"))?) + } + }), + )) + } +} +/// Payload configuration for request's payload. +#[derive(Clone)] +pub struct PayloadConfig { + limit: usize, + mimetype: Option, +} + +impl PayloadConfig { + /// Create `PayloadConfig` instance and set max size of payload. + pub fn new(limit: usize) -> Self { + Self::default().limit(limit) + } + + /// Change max size of payload. By default max size is 256Kb + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } + + /// Set required mime-type of the request. By default mime type is not + /// enforced. + pub fn mimetype(mut self, mt: Mime) -> Self { + self.mimetype = Some(mt); + self + } + + fn check_mimetype

    (&self, req: &ServiceFromRequest

    ) -> Result<(), Error> { + // check content-type + if let Some(ref mt) = self.mimetype { + match req.mime_type() { + Ok(Some(ref req_mt)) => { + if mt != req_mt { + return Err(ErrorBadRequest("Unexpected Content-Type")); + } + } + Ok(None) => { + return Err(ErrorBadRequest("Content-Type is expected")); + } + Err(err) => { + return Err(err.into()); + } + } + } + Ok(()) + } +} + +impl Default for PayloadConfig { + fn default() -> Self { + PayloadConfig { + limit: 262_144, + mimetype: None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::http::header; + use crate::test::TestRequest; + + #[test] + fn test_payload_config() { + let req = TestRequest::default().to_from(); + let cfg = PayloadConfig::default().mimetype(mime::APPLICATION_JSON); + assert!(cfg.check_mimetype(&req).is_err()); + + let req = TestRequest::with_header( + header::CONTENT_TYPE, + "application/x-www-form-urlencoded", + ) + .to_from(); + assert!(cfg.check_mimetype(&req).is_err()); + + let req = + TestRequest::with_header(header::CONTENT_TYPE, "application/json").to_from(); + assert!(cfg.check_mimetype(&req).is_ok()); + } +} diff --git a/src/extract/query.rs b/src/extract/query.rs new file mode 100644 index 00000000..f0eb6a7a --- /dev/null +++ b/src/extract/query.rs @@ -0,0 +1,126 @@ +//! Query extractor + +use std::{fmt, ops}; + +use actix_http::error::Error; +use serde::de; +use serde_urlencoded; + +use crate::extract::FromRequest; +use crate::service::ServiceFromRequest; + +#[derive(PartialEq, Eq, PartialOrd, Ord)] +/// Extract typed information from from the request's query. +/// +/// ## Example +/// +/// ```rust +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App}; +/// +/// #[derive(Debug, Deserialize)] +/// pub enum ResponseType { +/// Token, +/// Code +/// } +/// +/// #[derive(Deserialize)] +/// pub struct AuthRequest { +/// id: u64, +/// response_type: ResponseType, +/// } +/// +/// // Use `Query` extractor for query information. +/// // This handler get called only if request's query contains `username` field +/// // The correct request for this handler would be `/index.html?id=64&response_type=Code"` +/// fn index(info: web::Query) -> String { +/// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html").route(web::get().to(index))); // <- use `Query` extractor +/// } +/// ``` +pub struct Query(T); + +impl Query { + /// Deconstruct to a inner value + pub fn into_inner(self) -> T { + self.0 + } +} + +impl ops::Deref for Query { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +impl ops::DerefMut for Query { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl fmt::Debug for Query { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl fmt::Display for Query { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +/// Extract typed information from from the request's query. +/// +/// ## Example +/// +/// ```rust +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{web, App}; +/// +/// #[derive(Debug, Deserialize)] +/// pub enum ResponseType { +/// Token, +/// Code +/// } +/// +/// #[derive(Deserialize)] +/// pub struct AuthRequest { +/// id: u64, +/// response_type: ResponseType, +/// } +/// +/// // Use `Query` extractor for query information. +/// // This handler get called only if request's query contains `username` field +/// // The correct request for this handler would be `/index.html?id=64&response_type=Code"` +/// fn index(info: web::Query) -> String { +/// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html") +/// .route(web::get().to(index))); // <- use `Query` extractor +/// } +/// ``` +impl FromRequest

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

    ) -> Self::Future { + serde_urlencoded::from_str::(req.query_string()) + .map(|val| Ok(Query(val))) + .unwrap_or_else(|e| Err(e.into())) + } +} diff --git a/src/middleware/identity.rs b/src/middleware/identity.rs index 7cf739bc..f3ccca93 100644 --- a/src/middleware/identity.rs +++ b/src/middleware/identity.rs @@ -145,7 +145,6 @@ struct IdentityItem { impl

    FromRequest

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

    ) -> Self::Future { diff --git a/src/request.rs b/src/request.rs index 71751483..5517302f 100644 --- a/src/request.rs +++ b/src/request.rs @@ -205,7 +205,6 @@ impl HttpMessage for HttpRequest { impl

    FromRequest

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

    ) -> Self::Future { diff --git a/src/route.rs b/src/route.rs index c189c61b..1955a81a 100644 --- a/src/route.rs +++ b/src/route.rs @@ -6,7 +6,7 @@ use actix_http::{http::Method, Error, Extensions, Response}; use actix_service::{NewService, Service}; use futures::{Async, Future, IntoFuture, Poll}; -use crate::extract::{ConfigStorage, ExtractorConfig, FromRequest}; +use crate::extract::FromRequest; use crate::guard::{self, Guard}; use crate::handler::{AsyncFactory, AsyncHandler, Extract, Factory, Handler}; use crate::responder::Responder; @@ -40,7 +40,7 @@ type BoxedRouteNewService = Box< pub struct Route

    { service: BoxedRouteNewService, ServiceResponse>, guards: Rc>>, - config: ConfigStorage, + config: Option, config_ref: Rc>>>, } @@ -55,13 +55,13 @@ impl Route

    { ), )), guards: Rc::new(Vec::new()), - config: ConfigStorage::default(), + config: None, config_ref, } } - pub(crate) fn finish(self) -> Self { - *self.config_ref.borrow_mut() = self.config.storage.clone(); + pub(crate) fn finish(mut self) -> Self { + *self.config_ref.borrow_mut() = self.config.take().map(|e| Rc::new(e)); self } @@ -252,7 +252,6 @@ impl Route

    { T: FromRequest

    + 'static, R: Responder + 'static, { - T::Config::store_default(&mut self.config); self.service = Box::new(RouteNewService::new( Extract::new(self.config_ref.clone()) .and_then(Handler::new(handler).map_err(|_| panic!())), @@ -324,8 +323,11 @@ impl Route

    { /// )); /// } /// ``` - pub fn config(mut self, config: C) -> Self { - self.config.store(config); + pub fn config(mut self, config: C) -> Self { + if self.config.is_none() { + self.config = Some(Extensions::new()); + } + self.config.as_mut().unwrap().insert(config); self } } diff --git a/src/service.rs b/src/service.rs index 612fe4e8..08330282 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::cell::{Ref, RefMut}; use std::fmt; use std::marker::PhantomData; @@ -271,13 +270,12 @@ impl

    ServiceFromRequest

    { } /// Load extractor configuration - pub fn load_config(&self) -> Cow { + pub fn load_config(&self) -> Option<&T> { if let Some(ref ext) = self.config { - if let Some(cfg) = ext.get::() { - return Cow::Borrowed(cfg); - } + ext.get::() + } else { + None } - Cow::Owned(T::default()) } } diff --git a/src/state.rs b/src/state.rs index 2c623c70..b70540c0 100644 --- a/src/state.rs +++ b/src/state.rs @@ -48,7 +48,6 @@ impl Clone for State { impl FromRequest

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

    ) -> Self::Future { From b6c11357983a195b277edf76d0302baf5c51f459 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 10:56:53 -0700 Subject: [PATCH 075/109] hide blocking mod --- src/error.rs | 2 ++ src/lib.rs | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/error.rs b/src/error.rs index 54ca74dc..fd0ee998 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,6 +4,8 @@ pub use actix_http::error::*; use derive_more::{Display, From}; use url::ParseError as UrlParseError; +pub use crate::blocking::BlockingError; + /// Errors which can occur when attempting to generate resource uri. #[derive(Debug, PartialEq, Display, From)] pub enum UrlGenerationError { diff --git a/src/lib.rs b/src/lib.rs index 32207024..f6f722be 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ mod app; mod app_service; -pub mod blocking; +mod blocking; mod config; pub mod error; mod extract; @@ -54,6 +54,7 @@ pub mod dev { //! ``` pub use crate::app::AppRouter; + pub use crate::blocking::CpuFuture; pub use crate::config::{AppConfig, ServiceConfig}; pub use crate::info::ConnectionInfo; pub use crate::rmap::ResourceMap; From 039efc570384c87fcb3442cf3b0a2d5f930d92ef Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 11:04:50 -0700 Subject: [PATCH 076/109] move tests to different mods --- src/extract/mod.rs | 52 ------------------------------------------ src/extract/path.rs | 42 ++++++++++++++++++++++++++++++++++ src/extract/payload.rs | 24 ++++++++++++++++++- 3 files changed, 65 insertions(+), 53 deletions(-) diff --git a/src/extract/mod.rs b/src/extract/mod.rs index d8958b2d..78c6ba79 100644 --- a/src/extract/mod.rs +++ b/src/extract/mod.rs @@ -256,26 +256,6 @@ mod tests { hello: String, } - #[test] - fn test_bytes() { - let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .to_from(); - - let s = block_on(Bytes::from_request(&mut req)).unwrap(); - assert_eq!(s, Bytes::from_static(b"hello=world")); - } - - #[test] - fn test_string() { - let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11") - .set_payload(Bytes::from_static(b"hello=world")) - .to_from(); - - let s = block_on(String::from_request(&mut req)).unwrap(); - assert_eq!(s, "hello=world"); - } - #[test] fn test_option() { let mut req = TestRequest::with_header( @@ -400,36 +380,4 @@ mod tests { assert_eq!(res[1], "32".to_owned()); } - #[test] - fn test_extract_path_single() { - let resource = ResourceDef::new("/{value}/"); - - let mut req = TestRequest::with_uri("/32/").to_from(); - resource.match_path(req.match_info_mut()); - - assert_eq!(*Path::::from_request(&mut req).unwrap(), 32); - } - - #[test] - fn test_tuple_extract() { - let resource = ResourceDef::new("/{key}/{value}/"); - - let mut req = TestRequest::with_uri("/name/user1/?id=test").to_from(); - resource.match_path(req.match_info_mut()); - - let res = block_on(<(Path<(String, String)>,)>::from_request(&mut req)).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), - ) - .unwrap(); - assert_eq!((res.0).0, "name"); - assert_eq!((res.0).1, "user1"); - assert_eq!((res.1).0, "name"); - assert_eq!((res.1).1, "user1"); - - let () = <()>::from_request(&mut req).unwrap(); - } } diff --git a/src/extract/path.rs b/src/extract/path.rs index d9461263..fc6811c7 100644 --- a/src/extract/path.rs +++ b/src/extract/path.rs @@ -174,3 +174,45 @@ where Self::extract(req).map_err(ErrorNotFound) } } + +#[cfg(test)] +mod tests { + use actix_router::ResourceDef; + + use super::*; + use crate::test::{block_on, TestRequest}; + + #[test] + fn test_extract_path_single() { + let resource = ResourceDef::new("/{value}/"); + + let mut req = TestRequest::with_uri("/32/").to_from(); + resource.match_path(req.match_info_mut()); + + assert_eq!(*Path::::from_request(&mut req).unwrap(), 32); + } + + #[test] + fn test_tuple_extract() { + let resource = ResourceDef::new("/{key}/{value}/"); + + let mut req = TestRequest::with_uri("/name/user1/?id=test").to_from(); + resource.match_path(req.match_info_mut()); + + let res = block_on(<(Path<(String, String)>,)>::from_request(&mut req)).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), + ) + .unwrap(); + assert_eq!((res.0).0, "name"); + assert_eq!((res.0).1, "user1"); + assert_eq!((res.1).0, "name"); + assert_eq!((res.1).1, "user1"); + + let () = <()>::from_request(&mut req).unwrap(); + } + +} diff --git a/src/extract/payload.rs b/src/extract/payload.rs index 82024c0d..13532eee 100644 --- a/src/extract/payload.rs +++ b/src/extract/payload.rs @@ -289,9 +289,11 @@ impl Default for PayloadConfig { #[cfg(test)] mod tests { + use bytes::Bytes; + use super::*; use crate::http::header; - use crate::test::TestRequest; + use crate::test::{block_on, TestRequest}; #[test] fn test_payload_config() { @@ -310,4 +312,24 @@ mod tests { TestRequest::with_header(header::CONTENT_TYPE, "application/json").to_from(); assert!(cfg.check_mimetype(&req).is_ok()); } + + #[test] + fn test_bytes() { + let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11") + .set_payload(Bytes::from_static(b"hello=world")) + .to_from(); + + let s = block_on(Bytes::from_request(&mut req)).unwrap(); + assert_eq!(s, Bytes::from_static(b"hello=world")); + } + + #[test] + fn test_string() { + let mut req = TestRequest::with_header(header::CONTENT_LENGTH, "11") + .set_payload(Bytes::from_static(b"hello=world")) + .to_from(); + + let s = block_on(String::from_request(&mut req)).unwrap(); + assert_eq!(s, "hello=world"); + } } From 79875ea03955c9ae56b4efd79d54dfe7157d0a3b Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 14:22:53 -0700 Subject: [PATCH 077/109] update deps --- actix-files/src/lib.rs | 19 ++++++++----------- actix-session/src/lib.rs | 1 - 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 14c25be7..3ac17619 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -15,11 +15,11 @@ use mime_guess::get_mime_type; use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; use v_htmlescape::escape as escape_html_entity; -use actix_http::error::{Error, ErrorInternalServerError}; use actix_service::{boxed::BoxedNewService, NewService, Service}; -use actix_web::dev::{HttpServiceFactory, ResourceDef, ServiceConfig}; +use actix_web::dev::{CpuFuture, HttpServiceFactory, ResourceDef, ServiceConfig}; +use actix_web::error::{BlockingError, Error, ErrorInternalServerError}; use actix_web::{ - blocking, FromRequest, HttpRequest, HttpResponse, Responder, ServiceFromRequest, + web, FromRequest, HttpRequest, HttpResponse, Responder, ServiceFromRequest, ServiceRequest, ServiceResponse, }; use futures::future::{ok, FutureResult}; @@ -51,16 +51,14 @@ pub struct ChunkedReadFile { size: u64, offset: u64, file: Option, - fut: Option>, + fut: Option>, counter: u64, } -fn handle_error(err: blocking::BlockingError) -> Error { +fn handle_error(err: BlockingError) -> Error { match err { - blocking::BlockingError::Error(err) => err.into(), - blocking::BlockingError::Canceled => { - ErrorInternalServerError("Unexpected error").into() - } + BlockingError::Error(err) => err.into(), + BlockingError::Canceled => ErrorInternalServerError("Unexpected error").into(), } } @@ -90,7 +88,7 @@ impl Stream for ChunkedReadFile { Ok(Async::Ready(None)) } else { let mut file = self.file.take().expect("Use after completion"); - self.fut = Some(blocking::run(move || { + self.fut = Some(web::block(move || { let max_bytes: usize; max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; let mut buf = Vec::with_capacity(max_bytes); @@ -446,7 +444,6 @@ impl 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.match_info().path()) diff --git a/actix-session/src/lib.rs b/actix-session/src/lib.rs index 62bc5c8f..79b7e1f9 100644 --- a/actix-session/src/lib.rs +++ b/actix-session/src/lib.rs @@ -175,7 +175,6 @@ impl Session { impl

    FromRequest

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

    ) -> Self::Future { From 4d96abb639eca231ab26ef6bb11cd4ba102c4040 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 16:35:38 -0700 Subject: [PATCH 078/109] use actix_web::Error for middleware errors --- src/app.rs | 29 ++--- src/app_service.rs | 32 ++--- src/config.rs | 5 +- src/handler.rs | 6 +- src/lib.rs | 5 +- src/middleware/defaultheaders.rs | 3 +- src/middleware/errhandlers.rs | 210 +++++++++++++++++++++++++++++++ src/middleware/mod.rs | 8 +- src/resource.rs | 25 ++-- src/route.rs | 18 +-- src/scope.rs | 24 ++-- src/service.rs | 18 +-- src/test.rs | 7 +- 13 files changed, 305 insertions(+), 85 deletions(-) create mode 100644 src/middleware/errhandlers.rs diff --git a/src/app.rs b/src/app.rs index 29dd1ab6..54b5ded2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,8 +3,6 @@ use std::marker::PhantomData; use std::rc::Rc; use actix_http::body::{Body, MessageBody}; -use actix_http::PayloadStream; -use actix_router::ResourceDef; use actix_server_config::ServerConfig; use actix_service::boxed::{self, BoxedNewService}; use actix_service::{ @@ -14,6 +12,8 @@ use futures::IntoFuture; use crate::app_service::{AppChain, AppEntry, AppInit, AppRouting, AppRoutingFactory}; use crate::config::{AppConfig, AppConfigInner}; +use crate::dev::{PayloadStream, ResourceDef}; +use crate::error::Error; use crate::resource::Resource; use crate::route::Route; use crate::service::{ @@ -22,7 +22,8 @@ use crate::service::{ }; use crate::state::{State, StateFactory}; -type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; +type HttpNewService

    = + BoxedNewService<(), ServiceRequest

    , ServiceResponse, Error, ()>; /// Application builder - structure that follows the builder pattern /// for building application instances. @@ -55,7 +56,7 @@ where T: NewService< Request = ServiceRequest, Response = ServiceRequest

    , - Error = (), + Error = Error, InitError = (), >, { @@ -118,7 +119,7 @@ where impl NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, > @@ -127,7 +128,7 @@ where AppRouting

    , Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, F: IntoTransform>, @@ -157,7 +158,7 @@ where impl NewService< Request = ServiceRequest, Response = ServiceRequest, - Error = (), + Error = Error, InitError = (), >, > @@ -165,7 +166,7 @@ where C: NewService< Request = ServiceRequest

    , Response = ServiceRequest, - Error = (), + Error = Error, InitError = (), >, F: IntoNewService, @@ -264,7 +265,7 @@ where T: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, { @@ -324,7 +325,7 @@ where impl NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, > @@ -333,7 +334,7 @@ where T::Service, Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, B1: MessageBody, @@ -363,7 +364,7 @@ where U: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), > + 'static, { @@ -415,13 +416,13 @@ where T: NewService< Request = ServiceRequest

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

    , - Error = (), + Error = Error, InitError = (), >, { diff --git a/src/app_service.rs b/src/app_service.rs index 75e4b316..c59b80bc 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -11,15 +11,17 @@ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, Poll}; use crate::config::{AppConfig, ServiceConfig}; +use crate::error::Error; use crate::guard::Guard; use crate::rmap::ResourceMap; use crate::service::{ServiceFactory, ServiceRequest, ServiceResponse}; use crate::state::{StateFactory, StateFactoryResult}; type Guards = Vec>; -type HttpService

    = BoxedService, ServiceResponse, ()>; -type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; -type BoxedResponse = Box>; +type HttpService

    = BoxedService, ServiceResponse, Error>; +type HttpNewService

    = + BoxedNewService<(), ServiceRequest

    , ServiceResponse, Error, ()>; +type BoxedResponse = Box>; /// Service factory to convert `Request` to a `ServiceRequest`. /// It also executes state factories. @@ -29,7 +31,7 @@ where T: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, { @@ -48,13 +50,13 @@ where C: NewService< Request = ServiceRequest, Response = ServiceRequest

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

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, { @@ -147,13 +149,13 @@ where C: NewService< Request = ServiceRequest, Response = ServiceRequest

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

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, { @@ -201,7 +203,7 @@ where /// Service to convert `Request` to a `ServiceRequest` pub struct AppInitService where - C: Service, Error = ()>, + C: Service, Error = Error>, { chain: C, rmap: Rc, @@ -210,7 +212,7 @@ where impl Service for AppInitService where - C: Service, Error = ()>, + C: Service, Error = Error>, { type Request = Request; type Response = ServiceRequest

    ; @@ -240,7 +242,7 @@ pub struct AppRoutingFactory

    { impl NewService for AppRoutingFactory

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type InitError = (); type Service = AppRouting

    ; type Future = AppRoutingFactoryResponse

    ; @@ -350,7 +352,7 @@ pub struct AppRouting

    { impl

    Service for AppRouting

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type Future = Either>; fn poll_ready(&mut self) -> Poll<(), Self::Error> { @@ -398,7 +400,7 @@ impl

    AppEntry

    { impl NewService for AppEntry

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type InitError = (); type Service = AppRouting

    ; type Future = AppRoutingFactoryResponse

    ; @@ -414,7 +416,7 @@ pub struct AppChain; impl NewService for AppChain { type Request = ServiceRequest; type Response = ServiceRequest; - type Error = (); + type Error = Error; type InitError = (); type Service = AppChain; type Future = FutureResult; @@ -427,7 +429,7 @@ impl NewService for AppChain { impl Service for AppChain { type Request = ServiceRequest; type Response = ServiceRequest; - type Error = (); + type Error = Error; type Future = FutureResult; #[inline] diff --git a/src/config.rs b/src/config.rs index f84376c7..ceb58feb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,13 +6,14 @@ use actix_http::Extensions; use actix_router::ResourceDef; use actix_service::{boxed, IntoNewService, NewService}; +use crate::error::Error; use crate::guard::Guard; use crate::rmap::ResourceMap; use crate::service::{ServiceRequest, ServiceResponse}; type Guards = Vec>; type HttpNewService

    = - boxed::BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; + boxed::BoxedNewService<(), ServiceRequest

    , ServiceResponse, Error, ()>; /// Application configuration pub struct ServiceConfig

    { @@ -84,7 +85,7 @@ impl ServiceConfig

    { S: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), > + 'static, { diff --git a/src/handler.rs b/src/handler.rs index 87645651..4ff3193c 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -193,7 +193,7 @@ where { type Request = (T, HttpRequest); type Response = ServiceResponse; - type Error = (); + type Error = Error; type InitError = (); type Service = AsyncHandlerService; type Future = FutureResult; @@ -227,7 +227,7 @@ where { type Request = (T, HttpRequest); type Response = ServiceResponse; - type Error = (); + type Error = Error; type Future = AsyncHandlerServiceResponse; fn poll_ready(&mut self) -> Poll<(), Self::Error> { @@ -255,7 +255,7 @@ where T::Error: Into, { type Item = ServiceResponse; - type Error = (); + type Error = Error; fn poll(&mut self) -> Poll { match self.fut.poll() { diff --git a/src/lib.rs b/src/lib.rs index f6f722be..c04480af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,7 +40,6 @@ pub use crate::responder::{Either, Responder}; pub use crate::route::Route; pub use crate::scope::Scope; pub use crate::server::HttpServer; -pub use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; pub mod dev { //! The `actix-web` prelude for library developers @@ -58,7 +57,9 @@ pub mod dev { pub use crate::config::{AppConfig, ServiceConfig}; pub use crate::info::ConnectionInfo; pub use crate::rmap::ResourceMap; - pub use crate::service::HttpServiceFactory; + pub use crate::service::{ + HttpServiceFactory, ServiceFromRequest, ServiceRequest, ServiceResponse, + }; pub use actix_http::body::{Body, BodyLength, MessageBody, ResponseBody}; pub use actix_http::dev::ResponseBuilder as HttpResponseBuilder; diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index b4927962..bca2cf6e 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -152,9 +152,10 @@ mod tests { use actix_service::FnService; use super::*; + use crate::dev::ServiceRequest; use crate::http::header::CONTENT_TYPE; use crate::test::{block_on, TestRequest}; - use crate::{HttpResponse, ServiceRequest}; + use crate::HttpResponse; #[test] fn test_default_headers() { diff --git a/src/middleware/errhandlers.rs b/src/middleware/errhandlers.rs new file mode 100644 index 00000000..7a79aae1 --- /dev/null +++ b/src/middleware/errhandlers.rs @@ -0,0 +1,210 @@ +use std::rc::Rc; + +use actix_service::{Service, Transform}; +use futures::future::{err, ok, Either, Future, FutureResult}; +use futures::Poll; +use hashbrown::HashMap; + +use crate::dev::{ServiceRequest, ServiceResponse}; +use crate::error::{Error, Result}; +use crate::http::StatusCode; + +/// Error handler response +pub enum ErrorHandlerResponse { + /// New http response got generated + Response(ServiceResponse), + /// Result is a future that resolves to a new http response + Future(Box, Error = Error>>), +} + +type ErrorHandler = Fn(ServiceResponse) -> Result>; + +/// `Middleware` for allowing custom handlers for responses. +/// +/// You can use `ErrorHandlers::handler()` method to register a custom error +/// handler for specific status code. You can modify existing response or +/// create completely new one. +/// +/// ## Example +/// +/// ```rust +/// use actix_web::middleware::{ErrorHandlers, ErrorHandlerResponse}; +/// use actix_web::{web, http, dev, App, HttpRequest, HttpResponse, Result}; +/// +/// fn render_500(mut res: dev::ServiceResponse) -> Result> { +/// res.response_mut() +/// .headers_mut() +/// .insert(http::header::CONTENT_TYPE, http::HeaderValue::from_static("Error")); +/// Ok(ErrorHandlerResponse::Response(res)) +/// } +/// +/// fn main() { +/// let app = App::new() +/// .middleware( +/// ErrorHandlers::new() +/// .handler(http::StatusCode::INTERNAL_SERVER_ERROR, render_500), +/// ) +/// .service(web::resource("/test") +/// .route(web::get().to(|| HttpResponse::Ok())) +/// .route(web::head().to(|| HttpResponse::MethodNotAllowed()) +/// )); +/// } +/// ``` +pub struct ErrorHandlers { + handlers: Rc>>>, +} + +impl Default for ErrorHandlers { + fn default() -> Self { + ErrorHandlers { + handlers: Rc::new(HashMap::new()), + } + } +} + +impl ErrorHandlers { + /// Construct new `ErrorHandlers` instance + pub fn new() -> Self { + ErrorHandlers::default() + } + + /// Register error handler for specified status code + pub fn handler(mut self, status: StatusCode, handler: F) -> Self + where + F: Fn(ServiceResponse) -> Result> + 'static, + { + Rc::get_mut(&mut self.handlers) + .unwrap() + .insert(status, Box::new(handler)); + self + } +} + +impl Transform for ErrorHandlers +where + S: Service< + Request = ServiceRequest

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

    ; + type Response = ServiceResponse; + type Error = Error; + type InitError = (); + type Transform = ErrorHandlersMiddleware; + type Future = FutureResult; + + fn new_transform(&self, service: S) -> Self::Future { + ok(ErrorHandlersMiddleware { + service, + handlers: self.handlers.clone(), + }) + } +} + +pub struct ErrorHandlersMiddleware { + service: S, + handlers: Rc>>>, +} + +impl Service for ErrorHandlersMiddleware +where + S: Service< + Request = ServiceRequest

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

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

    ) -> Self::Future { + let handlers = self.handlers.clone(); + + Box::new(self.service.call(req).and_then(move |res| { + if let Some(handler) = handlers.get(&res.status()) { + match handler(res) { + Ok(ErrorHandlerResponse::Response(res)) => Either::A(ok(res)), + Ok(ErrorHandlerResponse::Future(fut)) => Either::B(fut), + Err(e) => Either::A(err(e)), + } + } else { + Either::A(ok(res)) + } + })) + } +} + +#[cfg(test)] +mod tests { + use actix_service::FnService; + use futures::future::ok; + + use super::*; + use crate::http::{header::CONTENT_TYPE, HeaderValue, StatusCode}; + use crate::test::{self, TestRequest}; + use crate::HttpResponse; + + fn render_500(mut res: ServiceResponse) -> Result> { + res.response_mut() + .headers_mut() + .insert(CONTENT_TYPE, HeaderValue::from_static("0001")); + Ok(ErrorHandlerResponse::Response(res)) + } + + #[test] + fn test_handler() { + let srv = FnService::new(|req: ServiceRequest<_>| { + req.into_response(HttpResponse::InternalServerError().finish()) + }); + + let mut mw = test::block_on( + ErrorHandlers::new() + .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500) + .new_transform(srv), + ) + .unwrap(); + + let resp = test::call_success(&mut mw, TestRequest::default().to_service()); + assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); + } + + fn render_500_async( + mut res: ServiceResponse, + ) -> Result> { + res.response_mut() + .headers_mut() + .insert(CONTENT_TYPE, HeaderValue::from_static("0001")); + Ok(ErrorHandlerResponse::Future(Box::new(ok(res)))) + } + + #[test] + fn test_handler_async() { + let srv = FnService::new(|req: ServiceRequest<_>| { + req.into_response(HttpResponse::InternalServerError().finish()) + }); + + let mut mw = test::block_on( + ErrorHandlers::new() + .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500_async) + .new_transform(srv), + ) + .unwrap(); + + let resp = test::call_success(&mut mw, TestRequest::default().to_service()); + assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); + } +} diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 288c1d63..6e55cd67 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -4,13 +4,15 @@ mod compress; pub use self::compress::Compress; mod defaultheaders; +mod errhandlers; +mod logger; + pub use self::defaultheaders::DefaultHeaders; +pub use self::errhandlers::{ErrorHandlerResponse, ErrorHandlers}; +pub use self::logger::Logger; // #[cfg(feature = "session")] // pub use actix_session as session; -mod logger; -pub use self::logger::Logger; - #[cfg(feature = "session")] pub mod identity; diff --git a/src/resource.rs b/src/resource.rs index 57f6f710..e4fe65c0 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -17,8 +17,9 @@ use crate::responder::Responder; use crate::route::{CreateRouteService, Route, RouteService}; use crate::service::{ServiceRequest, ServiceResponse}; -type HttpService

    = BoxedService, ServiceResponse, ()>; -type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; +type HttpService

    = BoxedService, ServiceResponse, Error>; +type HttpNewService

    = + BoxedNewService<(), ServiceRequest

    , ServiceResponse, Error, ()>; /// *Resource* is an entry in route table which corresponds to requested URL. /// @@ -70,7 +71,7 @@ where T: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, { @@ -232,7 +233,7 @@ where impl NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, > @@ -241,7 +242,7 @@ where T::Service, Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, F: IntoTransform, @@ -266,7 +267,7 @@ where U: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, > + 'static, { // create and configure default resource @@ -284,7 +285,7 @@ where T: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), > + 'static, { @@ -314,7 +315,7 @@ where T: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, { @@ -336,7 +337,7 @@ pub struct ResourceFactory

    { impl NewService for ResourceFactory

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type InitError = (); type Service = ResourceService

    ; type Future = CreateResourceService

    ; @@ -427,9 +428,9 @@ pub struct ResourceService

    { impl

    Service for ResourceService

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type Future = Either< - Box>, + Box>, Either< Box>, FutureResult, @@ -472,7 +473,7 @@ impl

    ResourceEndpoint

    { impl NewService for ResourceEndpoint

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type InitError = (); type Service = ResourceService

    ; type Future = CreateResourceService

    ; diff --git a/src/route.rs b/src/route.rs index 1955a81a..707da3d8 100644 --- a/src/route.rs +++ b/src/route.rs @@ -17,8 +17,8 @@ type BoxedRouteService = Box< Service< Request = Req, Response = Res, - Error = (), - Future = Box>, + Error = Error, + Future = Box>, >, >; @@ -26,7 +26,7 @@ type BoxedRouteNewService = Box< NewService< Request = Req, Response = Res, - Error = (), + Error = Error, InitError = (), Service = BoxedRouteService, Future = Box, Error = ()>>, @@ -73,7 +73,7 @@ impl Route

    { impl

    NewService for Route

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type InitError = (); type Service = RouteService

    ; type Future = CreateRouteService

    ; @@ -129,7 +129,7 @@ impl

    RouteService

    { impl

    Service for RouteService

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type Future = Box>; fn poll_ready(&mut self) -> Poll<(), Self::Error> { @@ -188,7 +188,7 @@ impl Route

    { // T: NewService< // Request = HandlerRequest, // Response = HandlerRequest, - // InitError = (), + // InitError = Error, // >, // { // RouteServiceBuilder { @@ -372,7 +372,7 @@ where { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type InitError = (); type Service = BoxedRouteService, Self::Response>; type Future = Box>; @@ -410,11 +410,11 @@ where { type Request = ServiceRequest

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

    ) -> Self::Future { diff --git a/src/scope.rs b/src/scope.rs index 3b506173..9f5b650c 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -11,6 +11,7 @@ use futures::future::{ok, Either, Future, FutureResult}; use futures::{Async, Poll}; use crate::dev::{HttpServiceFactory, ServiceConfig}; +use crate::error::Error; use crate::guard::Guard; use crate::resource::Resource; use crate::rmap::ResourceMap; @@ -20,9 +21,10 @@ use crate::service::{ }; type Guards = Vec>; -type HttpService

    = BoxedService, ServiceResponse, ()>; -type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; -type BoxedResponse = Box>; +type HttpService

    = BoxedService, ServiceResponse, Error>; +type HttpNewService

    = + BoxedNewService<(), ServiceRequest

    , ServiceResponse, Error, ()>; +type BoxedResponse = Box>; /// Resources scope /// @@ -83,7 +85,7 @@ where T: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, { @@ -176,7 +178,7 @@ where U: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), > + 'static, { @@ -201,7 +203,7 @@ where impl NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, > @@ -210,7 +212,7 @@ where T::Service, Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), >, F: IntoTransform, @@ -233,7 +235,7 @@ where T: NewService< Request = ServiceRequest

    , Response = ServiceResponse, - Error = (), + Error = Error, InitError = (), > + 'static, { @@ -290,7 +292,7 @@ pub struct ScopeFactory

    { impl NewService for ScopeFactory

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type InitError = (); type Service = ScopeService

    ; type Future = ScopeFactoryResponse

    ; @@ -406,7 +408,7 @@ pub struct ScopeService

    { impl

    Service for ScopeService

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type Future = Either>; fn poll_ready(&mut self) -> Poll<(), Self::Error> { @@ -450,7 +452,7 @@ impl

    ScopeEndpoint

    { impl NewService for ScopeEndpoint

    { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type InitError = (); type Service = ScopeService

    ; type Future = ScopeFactoryResponse

    ; diff --git a/src/service.rs b/src/service.rs index 08330282..e907a1ab 100644 --- a/src/service.rs +++ b/src/service.rs @@ -340,6 +340,12 @@ impl ServiceResponse { Self::from_err(err, self.request) } + /// Create service response + #[inline] + pub fn into_response(self, response: Response) -> ServiceResponse { + ServiceResponse::new(self.request, response) + } + /// Get reference to original request #[inline] pub fn request(&self) -> &HttpRequest { @@ -358,18 +364,6 @@ impl ServiceResponse { &mut self.response } - /// Get the headers from the response - #[inline] - pub fn headers(&self) -> &HeaderMap { - self.response.headers() - } - - /// Get a mutable reference to the headers - #[inline] - pub fn headers_mut(&mut self) -> &mut HeaderMap { - self.response.headers_mut() - } - /// Execute closure and in case of error convert it to response. pub fn checked_expr(mut self, f: F) -> Self where diff --git a/src/test.rs b/src/test.rs index b47daa2c..44592400 100644 --- a/src/test.rs +++ b/src/test.rs @@ -14,9 +14,9 @@ use bytes::Bytes; use futures::Future; use crate::config::{AppConfig, AppConfigInner}; -use crate::request::HttpRequest; use crate::rmap::ResourceMap; use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; +use crate::{HttpRequest, HttpResponse}; thread_local! { static RT: RefCell = { @@ -277,6 +277,11 @@ impl TestRequest { self.req.finish() } + /// Complete request creation and generate `ServiceResponse` instance + pub fn to_response(self, res: HttpResponse) -> ServiceResponse { + self.to_service().into_response(res) + } + /// Complete request creation and generate `HttpRequest` instance pub fn to_http_request(mut self) -> HttpRequest { let req = self.req.finish(); From 0f0d6b65cab6dfa162c13c4f6c6ba6dbd355357c Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 16:39:46 -0700 Subject: [PATCH 079/109] update service request/response location --- actix-files/src/lib.rs | 15 ++++++++------- actix-session/src/cookie.rs | 3 ++- actix-session/src/lib.rs | 2 +- src/extract/mod.rs | 8 ++++---- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 3ac17619..07fc0063 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -16,12 +16,12 @@ use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; use v_htmlescape::escape as escape_html_entity; use actix_service::{boxed::BoxedNewService, NewService, Service}; -use actix_web::dev::{CpuFuture, HttpServiceFactory, ResourceDef, ServiceConfig}; -use actix_web::error::{BlockingError, Error, ErrorInternalServerError}; -use actix_web::{ - web, FromRequest, HttpRequest, HttpResponse, Responder, ServiceFromRequest, +use actix_web::dev::{ + CpuFuture, HttpServiceFactory, ResourceDef, ServiceConfig, ServiceFromRequest, ServiceRequest, ServiceResponse, }; +use actix_web::error::{BlockingError, Error, ErrorInternalServerError}; +use actix_web::{web, FromRequest, HttpRequest, HttpResponse, Responder}; use futures::future::{ok, FutureResult}; mod config; @@ -34,7 +34,8 @@ pub use crate::config::{DefaultConfig, StaticFileConfig}; pub use crate::named::NamedFile; pub use crate::range::HttpRange; -type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, (), ()>; +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 @@ -319,7 +320,7 @@ where impl NewService for Files { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type Service = FilesService; type InitError = (); type Future = FutureResult; @@ -352,7 +353,7 @@ pub struct FilesService { impl Service for FilesService { type Request = ServiceRequest

    ; type Response = ServiceResponse; - type Error = (); + type Error = Error; type Future = FutureResult; fn poll_ready(&mut self) -> Poll<(), Self::Error> { diff --git a/actix-session/src/cookie.rs b/actix-session/src/cookie.rs index e2503145..37c552ea 100644 --- a/actix-session/src/cookie.rs +++ b/actix-session/src/cookie.rs @@ -19,8 +19,9 @@ use std::collections::HashMap; use std::rc::Rc; use actix_service::{Service, Transform}; +use actix_web::dev::{ServiceRequest, ServiceResponse}; use actix_web::http::{header::SET_COOKIE, HeaderValue}; -use actix_web::{Error, HttpMessage, ResponseError, ServiceRequest, ServiceResponse}; +use actix_web::{Error, HttpMessage, ResponseError}; use cookie::{Cookie, CookieJar, Key, SameSite}; use derive_more::{Display, From}; use futures::future::{ok, Future, FutureResult}; diff --git a/actix-session/src/lib.rs b/actix-session/src/lib.rs index 79b7e1f9..1dd367ba 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::{ServiceFromRequest, ServiceRequest, ServiceResponse}; use hashbrown::HashMap; use serde::de::DeserializeOwned; use serde::Serialize; diff --git a/src/extract/mod.rs b/src/extract/mod.rs index 78c6ba79..25a046d4 100644 --- a/src/extract/mod.rs +++ b/src/extract/mod.rs @@ -40,7 +40,7 @@ pub trait FromRequest

    : Sized { /// /// ```rust /// # #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App, Error, FromRequest, ServiceFromRequest}; +/// use actix_web::{web, dev, App, Error, FromRequest}; /// use actix_web::error::ErrorBadRequest; /// use rand; /// @@ -53,7 +53,7 @@ pub trait FromRequest

    : Sized { /// type Error = Error; /// type Future = Result; /// -/// fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { +/// fn from_request(req: &mut dev::ServiceFromRequest

    ) -> Self::Future { /// if rand::random() { /// Ok(Thing { name: "thingy".into() }) /// } else { @@ -107,7 +107,7 @@ where /// /// ```rust /// # #[macro_use] extern crate serde_derive; -/// use actix_web::{web, App, Result, Error, FromRequest, ServiceFromRequest}; +/// use actix_web::{web, dev, App, Result, Error, FromRequest}; /// use actix_web::error::ErrorBadRequest; /// use rand; /// @@ -120,7 +120,7 @@ where /// type Error = Error; /// type Future = Result; /// -/// fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { +/// fn from_request(req: &mut dev::ServiceFromRequest

    ) -> Self::Future { /// if rand::random() { /// Ok(Thing { name: "thingy".into() }) /// } else { From b8829bbf221dc5611973d524305ca59aaf10b3d6 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 17:10:41 -0700 Subject: [PATCH 080/109] add identity middleware tests --- src/middleware/identity.rs | 66 ++++++++++++++++++++++++++++++++++++++ src/test.rs | 7 ++++ 2 files changed, 73 insertions(+) diff --git a/src/middleware/identity.rs b/src/middleware/identity.rs index f3ccca93..d0a4146a 100644 --- a/src/middleware/identity.rs +++ b/src/middleware/identity.rs @@ -461,3 +461,69 @@ impl IdentityPolicy for CookieIdentityPolicy { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::http::StatusCode; + use crate::test::{self, TestRequest}; + use crate::{web, App, HttpResponse}; + + #[test] + fn test_identity() { + let mut srv = test::init_service( + App::new() + .middleware(IdentityService::new( + CookieIdentityPolicy::new(&[0; 32]) + .domain("www.rust-lang.org") + .name("actix_auth") + .path("/") + .secure(true), + )) + .service(web::resource("/index").to(|id: Identity| { + if id.identity().is_some() { + HttpResponse::Created() + } else { + HttpResponse::Ok() + } + })) + .service(web::resource("/login").to(|id: Identity| { + id.remember("test".to_string()); + HttpResponse::Ok() + })) + .service(web::resource("/logout").to(|id: Identity| { + if id.identity().is_some() { + id.forget(); + HttpResponse::Ok() + } else { + HttpResponse::BadRequest() + } + })), + ); + let resp = + test::call_success(&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()); + assert_eq!(resp.status(), StatusCode::OK); + let c = resp.cookies().next().unwrap().to_owned(); + + let resp = test::call_success( + &mut srv, + TestRequest::with_uri("/index") + .cookie(c.clone()) + .to_request(), + ); + assert_eq!(resp.status(), StatusCode::CREATED); + + let resp = test::call_success( + &mut srv, + TestRequest::with_uri("/logout") + .cookie(c.clone()) + .to_request(), + ); + assert_eq!(resp.status(), StatusCode::OK); + assert!(resp.headers().contains_key(header::SET_COOKIE)) + } +} diff --git a/src/test.rs b/src/test.rs index 44592400..03700b67 100644 --- a/src/test.rs +++ b/src/test.rs @@ -11,6 +11,7 @@ use actix_rt::Runtime; use actix_server_config::ServerConfig; use actix_service::{IntoNewService, NewService, Service}; use bytes::Bytes; +use cookie::Cookie; use futures::Future; use crate::config::{AppConfig, AppConfigInner}; @@ -241,6 +242,12 @@ impl TestRequest { self } + /// Set cookie for this request + pub fn cookie<'a>(mut self, cookie: Cookie<'a>) -> Self { + self.req.cookie(cookie); + self + } + /// Set request payload pub fn set_payload>(mut self, data: B) -> Self { self.req.set_payload(data); From 9680423025a9ece3e0d3bb4148655b9867120c7c Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 10 Mar 2019 18:33:47 -0700 Subject: [PATCH 081/109] Add more tests for route --- src/app.rs | 80 ---------------------------------------------------- src/lib.rs | 3 +- src/route.rs | 38 +++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 81 deletions(-) diff --git a/src/app.rs b/src/app.rs index 54b5ded2..2e2a8c2d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -526,84 +526,4 @@ mod tests { let resp = block_on(srv.call(req)).unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } - - // #[test] - // fn test_handler() { - // let app = App::new() - // .handler("/test", |_: &_| HttpResponse::Ok()) - // .finish(); - - // let req = TestRequest::with_uri("/test").request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::OK); - - // let req = TestRequest::with_uri("/test/").request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::OK); - - // let req = TestRequest::with_uri("/test/app").request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::OK); - - // let req = TestRequest::with_uri("/testapp").request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - // let req = TestRequest::with_uri("/blah").request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - // } - - // #[test] - // fn test_handler2() { - // let app = App::new() - // .handler("test", |_: &_| HttpResponse::Ok()) - // .finish(); - - // let req = TestRequest::with_uri("/test").request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::OK); - - // let req = TestRequest::with_uri("/test/").request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::OK); - - // let req = TestRequest::with_uri("/test/app").request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::OK); - - // let req = TestRequest::with_uri("/testapp").request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - - // let req = TestRequest::with_uri("/blah").request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - // } - - // #[test] - // fn test_route() { - // let app = App::new() - // .route("/test", Method::GET, |_: HttpRequest| HttpResponse::Ok()) - // .route("/test", Method::POST, |_: HttpRequest| { - // HttpResponse::Created() - // }) - // .finish(); - - // let req = TestRequest::with_uri("/test").method(Method::GET).request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::OK); - - // let req = TestRequest::with_uri("/test") - // .method(Method::POST) - // .request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::CREATED); - - // let req = TestRequest::with_uri("/test") - // .method(Method::HEAD) - // .request(); - // let resp = app.run(req); - // assert_eq!(resp.as_msg().status(), StatusCode::NOT_FOUND); - // } } diff --git a/src/lib.rs b/src/lib.rs index c04480af..62f6399d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,7 +79,7 @@ pub mod dev { } pub mod web { - use actix_http::{http::Method, Error, Response}; + use actix_http::{http::Method, Response}; use futures::IntoFuture; pub use actix_http::Response as HttpResponse; @@ -93,6 +93,7 @@ pub mod web { use crate::route::Route; use crate::scope::Scope; + pub use crate::error::Error; pub use crate::extract::{Form, Json, Path, Payload, Query}; pub use crate::extract::{FormConfig, JsonConfig, PayloadConfig}; pub use crate::request::HttpRequest; diff --git a/src/route.rs b/src/route.rs index 707da3d8..5d339a3b 100644 --- a/src/route.rs +++ b/src/route.rs @@ -424,3 +424,41 @@ where })) } } + +#[cfg(test)] +mod tests { + use crate::http::{Method, StatusCode}; + use crate::test::{call_success, init_service, TestRequest}; + use crate::{web, App, Error, HttpResponse}; + + #[test] + fn test_route() { + let mut srv = init_service( + App::new().service( + web::resource("/test") + .route(web::get().to(|| HttpResponse::Ok())) + .route( + web::post().to_async(|| Ok::<_, Error>(HttpResponse::Created())), + ), + ), + ); + + let req = TestRequest::with_uri("/test") + .method(Method::GET) + .to_request(); + let resp = call_success(&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); + assert_eq!(resp.status(), StatusCode::CREATED); + + let req = TestRequest::with_uri("/test") + .method(Method::HEAD) + .to_request(); + let resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } +} From eae48f9612d2391ead1963a2f49aabfd5b420aac Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 11 Mar 2019 15:26:05 -0700 Subject: [PATCH 082/109] use server backlog --- src/server.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/server.rs b/src/server.rs index 5d717817..f80e5a0e 100644 --- a/src/server.rs +++ b/src/server.rs @@ -83,12 +83,12 @@ where HttpServer { factory, host: None, - backlog: 2048, config: Arc::new(Mutex::new(Config { keep_alive: KeepAlive::Timeout(5), client_timeout: 5000, client_shutdown: 5000, })), + backlog: 1024, sockets: Vec::new(), builder: Some(ServerBuilder::default()), _t: PhantomData, @@ -114,8 +114,9 @@ where /// Generally set in the 64-2048 range. Default value is 2048. /// /// This method should be called before `bind()` method call. - pub fn backlog(mut self, num: i32) -> Self { - self.backlog = num; + pub fn backlog(mut self, backlog: i32) -> Self { + self.backlog = backlog; + self.builder = Some(self.builder.take().unwrap().backlog(backlog)); self } From a2c4639074baa0b1275d0b46b9560eafb88d0f28 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 11 Mar 2019 23:11:51 -0700 Subject: [PATCH 083/109] move blocking code to actix-rt --- Cargo.toml | 5 +-- src/blocking.rs | 93 ------------------------------------------------- src/error.rs | 2 -- src/lib.rs | 7 ++-- 4 files changed, 4 insertions(+), 103 deletions(-) delete mode 100644 src/blocking.rs diff --git a/Cargo.toml b/Cargo.toml index c64f4bc6..e2439294 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,7 @@ actix-codec = "0.1.0" actix-service = "0.3.3" actix-utils = "0.3.3" actix-router = "0.1.0" -actix-rt = "0.2.0" +actix-rt = "0.2.1" actix-web-codegen = { path="actix-web-codegen" } actix-http = { git = "https://github.com/actix/actix-http.git" } @@ -78,16 +78,13 @@ encoding = "0.2" futures = "0.1" hashbrown = "0.1.8" log = "0.4" -lazy_static = "1.2" mime = "0.3" net2 = "0.2.33" -num_cpus = "1.10" parking_lot = "0.7" regex = "1.0" serde = "1.0" serde_json = "1.0" serde_urlencoded = "^0.5.3" -threadpool = "1.7" time = "0.1" url = { version="1.7", features=["query_encoding"] } diff --git a/src/blocking.rs b/src/blocking.rs deleted file mode 100644 index fc9cec29..00000000 --- a/src/blocking.rs +++ /dev/null @@ -1,93 +0,0 @@ -//! Thread pool for blocking operations - -use std::fmt; - -use derive_more::Display; -use futures::sync::oneshot; -use futures::{Async, Future, Poll}; -use parking_lot::Mutex; -use threadpool::ThreadPool; - -use crate::ResponseError; - -/// Env variable for default cpu pool size -const ENV_CPU_POOL_VAR: &str = "ACTIX_CPU_POOL"; - -lazy_static::lazy_static! { - pub(crate) static ref DEFAULT_POOL: Mutex = { - let default = match std::env::var(ENV_CPU_POOL_VAR) { - Ok(val) => { - if let Ok(val) = val.parse() { - val - } else { - log::error!("Can not parse ACTIX_CPU_POOL value"); - num_cpus::get() * 5 - } - } - Err(_) => num_cpus::get() * 5, - }; - Mutex::new( - threadpool::Builder::new() - .thread_name("actix-web".to_owned()) - .num_threads(default) - .build(), - ) - }; -} - -thread_local! { - static POOL: ThreadPool = { - DEFAULT_POOL.lock().clone() - }; -} - -/// Blocking operation execution error -#[derive(Debug, Display)] -pub enum BlockingError { - #[display(fmt = "{:?}", _0)] - Error(E), - #[display(fmt = "Thread pool is gone")] - Canceled, -} - -impl ResponseError for BlockingError {} - -/// Execute blocking function on a thread pool, returns future that resolves -/// to result of the function execution. -pub fn run(f: F) -> CpuFuture -where - F: FnOnce() -> Result + Send + 'static, - I: Send + 'static, - E: Send + fmt::Debug + 'static, -{ - let (tx, rx) = oneshot::channel(); - POOL.with(|pool| { - pool.execute(move || { - if !tx.is_canceled() { - let _ = tx.send(f()); - } - }) - }); - - CpuFuture { rx } -} - -/// Blocking operation completion future. It resolves with results -/// of blocking function execution. -pub struct CpuFuture { - rx: oneshot::Receiver>, -} - -impl Future for CpuFuture { - type Item = I; - type Error = BlockingError; - - fn poll(&mut self) -> Poll { - let res = - futures::try_ready!(self.rx.poll().map_err(|_| BlockingError::Canceled)); - match res { - Ok(val) => Ok(Async::Ready(val)), - Err(err) => Err(BlockingError::Error(err)), - } - } -} diff --git a/src/error.rs b/src/error.rs index fd0ee998..54ca74dc 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,8 +4,6 @@ pub use actix_http::error::*; use derive_more::{Display, From}; use url::ParseError as UrlParseError; -pub use crate::blocking::BlockingError; - /// Errors which can occur when attempting to generate resource uri. #[derive(Debug, PartialEq, Display, From)] pub enum UrlGenerationError { diff --git a/src/lib.rs b/src/lib.rs index 62f6399d..0094818d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,6 @@ mod app; mod app_service; -mod blocking; mod config; pub mod error; mod extract; @@ -53,7 +52,6 @@ pub mod dev { //! ``` pub use crate::app::AppRouter; - pub use crate::blocking::CpuFuture; pub use crate::config::{AppConfig, ServiceConfig}; pub use crate::info::ConnectionInfo; pub use crate::rmap::ResourceMap; @@ -67,6 +65,7 @@ pub mod dev { Extensions, Payload, PayloadStream, RequestHead, ResponseHead, }; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; + pub use actix_rt::blocking::CpuFuture; pub use actix_server::Server; pub(crate) fn insert_slash(path: &str) -> String { @@ -80,12 +79,12 @@ pub mod dev { pub mod web { use actix_http::{http::Method, Response}; + use actix_rt::blocking::{self, CpuFuture}; use futures::IntoFuture; pub use actix_http::Response as HttpResponse; pub use bytes::{Bytes, BytesMut}; - use crate::blocking::CpuFuture; use crate::extract::FromRequest; use crate::handler::{AsyncFactory, Factory}; use crate::resource::Resource; @@ -258,6 +257,6 @@ pub mod web { I: Send + 'static, E: Send + std::fmt::Debug + 'static, { - crate::blocking::run(f) + blocking::run(f) } } From 7242d967014ae0266191d471210df52569cbc0b4 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 11 Mar 2019 23:19:05 -0700 Subject: [PATCH 084/109] map BlockingError --- actix-files/src/lib.rs | 10 +++++----- src/error.rs | 21 +++++++++++++++++++++ src/lib.rs | 11 +++++------ 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 07fc0063..b9240009 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -17,8 +17,8 @@ use v_htmlescape::escape as escape_html_entity; use actix_service::{boxed::BoxedNewService, NewService, Service}; use actix_web::dev::{ - CpuFuture, HttpServiceFactory, ResourceDef, ServiceConfig, ServiceFromRequest, - ServiceRequest, ServiceResponse, + HttpServiceFactory, ResourceDef, ServiceConfig, ServiceFromRequest, ServiceRequest, + ServiceResponse, }; use actix_web::error::{BlockingError, Error, ErrorInternalServerError}; use actix_web::{web, FromRequest, HttpRequest, HttpResponse, Responder}; @@ -52,7 +52,7 @@ pub struct ChunkedReadFile { size: u64, offset: u64, file: Option, - fut: Option>, + fut: Option>>>, counter: u64, } @@ -89,7 +89,7 @@ impl Stream for ChunkedReadFile { Ok(Async::Ready(None)) } else { let mut file = self.file.take().expect("Use after completion"); - self.fut = Some(web::block(move || { + self.fut = Some(Box::new(web::block(move || { let max_bytes: usize; max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; let mut buf = Vec::with_capacity(max_bytes); @@ -100,7 +100,7 @@ impl Stream for ChunkedReadFile { return Err(io::ErrorKind::UnexpectedEof.into()); } Ok((file, Bytes::from(buf))) - })); + }))); self.poll() } } diff --git a/src/error.rs b/src/error.rs index 54ca74dc..06840708 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,5 @@ //! Error and Result module +use std::fmt; pub use actix_http::error::*; use derive_more::{Display, From}; @@ -20,3 +21,23 @@ pub enum UrlGenerationError { /// `InternalServerError` for `UrlGeneratorError` impl ResponseError for UrlGenerationError {} + +/// Blocking operation execution error +#[derive(Debug, Display)] +pub enum BlockingError { + #[display(fmt = "{:?}", _0)] + Error(E), + #[display(fmt = "Thread pool is gone")] + Canceled, +} + +impl ResponseError for BlockingError {} + +impl From> for BlockingError { + fn from(err: actix_rt::blocking::BlockingError) -> Self { + match err { + actix_rt::blocking::BlockingError::Error(e) => BlockingError::Error(e), + actix_rt::blocking::BlockingError::Canceled => BlockingError::Canceled, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 0094818d..d653fd1c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,7 +65,6 @@ pub mod dev { Extensions, Payload, PayloadStream, RequestHead, ResponseHead, }; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; - pub use actix_rt::blocking::CpuFuture; pub use actix_server::Server; pub(crate) fn insert_slash(path: &str) -> String { @@ -79,8 +78,8 @@ pub mod dev { pub mod web { use actix_http::{http::Method, Response}; - use actix_rt::blocking::{self, CpuFuture}; - use futures::IntoFuture; + use actix_rt::blocking; + use futures::{Future, IntoFuture}; pub use actix_http::Response as HttpResponse; pub use bytes::{Bytes, BytesMut}; @@ -92,7 +91,7 @@ pub mod web { use crate::route::Route; use crate::scope::Scope; - pub use crate::error::Error; + pub use crate::error::{BlockingError, Error}; pub use crate::extract::{Form, Json, Path, Payload, Query}; pub use crate::extract::{FormConfig, JsonConfig, PayloadConfig}; pub use crate::request::HttpRequest; @@ -251,12 +250,12 @@ pub mod web { /// Execute blocking function on a thread pool, returns future that resolves /// to result of the function execution. - pub fn block(f: F) -> CpuFuture + pub fn block(f: F) -> impl Future> where F: FnOnce() -> Result + Send + 'static, I: Send + 'static, E: Send + std::fmt::Debug + 'static, { - blocking::run(f) + blocking::run(f).from_err() } } From 28f01beaec78568f38ddaea3d39ff13085b59923 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 12 Mar 2019 17:06:08 -0700 Subject: [PATCH 085/109] update deps --- Cargo.toml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e2439294..87a54b6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,15 +61,14 @@ ssl = ["openssl", "actix-server/ssl"] [dependencies] actix-codec = "0.1.0" -actix-service = "0.3.3" -actix-utils = "0.3.3" +actix-service = "0.3.4" +actix-utils = "0.3.4" actix-router = "0.1.0" actix-rt = "0.2.1" actix-web-codegen = { path="actix-web-codegen" } - actix-http = { git = "https://github.com/actix/actix-http.git" } -actix-server = { git = "https://github.com/actix/actix-net.git" } -actix-server-config = { git = "https://github.com/actix/actix-net.git" } +actix-server = "0.4.0" +actix-server-config = "0.1.0" bytes = "0.4" cookie = { version="0.11", features=["percent-encode"] } From 86405cfe7a3e4784200f41fa7ec2f29f572d7819 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 12 Mar 2019 22:57:09 -0700 Subject: [PATCH 086/109] more tests --- src/responder.rs | 95 +++++++++++++++++++++++++++++++++++++--- src/server.rs | 4 +- src/test.rs | 13 +++++- tests/test_httpserver.rs | 75 +++++++++++++++++++++++++++++++ 4 files changed, 179 insertions(+), 8 deletions(-) create mode 100644 tests/test_httpserver.rs diff --git a/src/responder.rs b/src/responder.rs index 6dce300a..ace360c6 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -261,7 +261,8 @@ where type Future = Result; fn respond_to(self, _: &HttpRequest) -> Self::Future { - Err(self.into()) + let err: Error = self.into(); + Ok(err.into()) } } @@ -289,12 +290,13 @@ where #[cfg(test)] mod tests { use actix_service::Service; - use bytes::Bytes; + use bytes::{Bytes, BytesMut}; + use super::*; use crate::dev::{Body, ResponseBody}; - use crate::http::StatusCode; - use crate::test::{init_service, TestRequest}; - use crate::{web, App}; + use crate::http::{header::CONTENT_TYPE, HeaderValue, StatusCode}; + use crate::test::{block_on, init_service, TestRequest}; + use crate::{error, web, App, HttpResponse}; #[test] fn test_option_responder() { @@ -319,4 +321,87 @@ mod tests { _ => panic!(), } } + + trait BodyTest { + fn bin_ref(&self) -> &[u8]; + fn body(&self) -> &Body; + } + + impl BodyTest for ResponseBody { + fn bin_ref(&self) -> &[u8] { + match self { + ResponseBody::Body(ref b) => match b { + Body::Bytes(ref bin) => &bin, + _ => panic!(), + }, + ResponseBody::Other(ref b) => match b { + Body::Bytes(ref bin) => &bin, + _ => panic!(), + }, + } + } + fn body(&self) -> &Body { + match self { + ResponseBody::Body(ref b) => b, + ResponseBody::Other(ref b) => b, + } + } + } + + #[test] + fn test_responder() { + let req = TestRequest::default().to_http_request(); + + let resp: HttpResponse = block_on(().respond_to(&req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(*resp.body().body(), Body::Empty); + + let resp: HttpResponse = block_on("test".respond_to(&req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(resp.body().bin_ref(), b"test"); + assert_eq!( + resp.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("text/plain; charset=utf-8") + ); + + let resp: HttpResponse = block_on(b"test".respond_to(&req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(resp.body().bin_ref(), b"test"); + assert_eq!( + resp.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("application/octet-stream") + ); + + let resp: HttpResponse = block_on("test".to_string().respond_to(&req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(resp.body().bin_ref(), b"test"); + assert_eq!( + resp.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("text/plain; charset=utf-8") + ); + + let resp: HttpResponse = + block_on(Bytes::from_static(b"test").respond_to(&req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(resp.body().bin_ref(), b"test"); + assert_eq!( + resp.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("application/octet-stream") + ); + + let resp: HttpResponse = + block_on(BytesMut::from(b"test".as_ref()).respond_to(&req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(resp.body().bin_ref(), b"test"); + assert_eq!( + resp.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("application/octet-stream") + ); + + let resp: HttpResponse = + error::InternalError::new("err", StatusCode::BAD_REQUEST) + .respond_to(&req) + .unwrap(); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } } diff --git a/src/server.rs b/src/server.rs index f80e5a0e..9055be30 100644 --- a/src/server.rs +++ b/src/server.rs @@ -182,8 +182,8 @@ where /// Host name is used by application router aa a hostname for url /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. /// html#method.host) documentation for more information. - pub fn server_hostname(mut self, val: String) -> Self { - self.host = Some(val); + pub fn server_hostname>(mut self, val: T) -> Self { + self.host = Some(val.as_ref().to_owned()); self } diff --git a/src/test.rs b/src/test.rs index 03700b67..57a6d396 100644 --- a/src/test.rs +++ b/src/test.rs @@ -12,7 +12,7 @@ use actix_server_config::ServerConfig; use actix_service::{IntoNewService, NewService, Service}; use bytes::Bytes; use cookie::Cookie; -use futures::Future; +use futures::future::{lazy, Future}; use crate::config::{AppConfig, AppConfigInner}; use crate::rmap::ResourceMap; @@ -42,6 +42,17 @@ where 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) -> Result +where + F: Fn() -> Result, +{ + RT.with(move |rt| rt.borrow_mut().block_on(lazy(|| f()))) +} + /// This method accepts application builder instance, and constructs /// service. /// diff --git a/tests/test_httpserver.rs b/tests/test_httpserver.rs new file mode 100644 index 00000000..d2bc07ac --- /dev/null +++ b/tests/test_httpserver.rs @@ -0,0 +1,75 @@ +use net2::TcpBuilder; +use std::sync::mpsc; +use std::{net, thread, time::Duration}; + +use actix_http::{client, Response}; + +use actix_web::{test, web, App, HttpServer}; + +fn unused_addr() -> net::SocketAddr { + let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); + let socket = TcpBuilder::new_v4().unwrap(); + socket.bind(&addr).unwrap(); + socket.reuse_address(true).unwrap(); + let tcp = socket.to_tcp_listener().unwrap(); + tcp.local_addr().unwrap() +} + +#[test] +#[cfg(unix)] +fn test_start() { + let addr = unused_addr(); + let (tx, rx) = mpsc::channel(); + + thread::spawn(move || { + let sys = actix_rt::System::new("test"); + + let srv = HttpServer::new(|| { + App::new().service( + web::resource("/").route(web::to(|| Response::Ok().body("test"))), + ) + }) + .workers(1) + .backlog(1) + .maxconn(10) + .maxconnrate(10) + .keep_alive(10) + .client_timeout(5000) + .client_shutdown(0) + .server_hostname("localhost") + .system_exit() + .disable_signals() + .bind(format!("{}", addr)) + .unwrap() + .start(); + + let _ = tx.send((srv, actix_rt::System::current())); + let _ = sys.run(); + }); + let (srv, sys) = rx.recv().unwrap(); + + let mut connector = test::run_on(|| { + Ok::<_, ()>( + client::Connector::default() + .timeout(Duration::from_millis(100)) + .service(), + ) + }) + .unwrap(); + let host = format!("http://{}", addr); + + let response = test::block_on( + client::ClientRequest::get(host.clone()) + .finish() + .unwrap() + .send(&mut connector), + ) + .unwrap(); + assert!(response.status().is_success()); + + // stop + let _ = srv.stop(false); + + thread::sleep(Duration::from_millis(100)); + let _ = sys.stop(); +} From 1f9467e880aaa53b3c2186070a0f1dca447499c5 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 14 Mar 2019 12:01:35 -0700 Subject: [PATCH 087/109] update tests --- tests/test_httpserver.rs | 2 +- tests/test_server.rs | 40 ++++++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/test_httpserver.rs b/tests/test_httpserver.rs index d2bc07ac..764d50ca 100644 --- a/tests/test_httpserver.rs +++ b/tests/test_httpserver.rs @@ -50,7 +50,7 @@ fn test_start() { let mut connector = test::run_on(|| { Ok::<_, ()>( - client::Connector::default() + client::Connector::new() .timeout(Duration::from_millis(100)) .service(), ) diff --git a/tests/test_server.rs b/tests/test_server.rs index ebe968fa..ffdc473a 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -69,7 +69,7 @@ fn test_body_gzip() { assert!(response.status().is_success()); // read response - let bytes = srv.execute(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); // decode let mut e = GzDecoder::new(&bytes[..]); @@ -100,7 +100,7 @@ fn test_body_gzip_large() { assert!(response.status().is_success()); // read response - let bytes = srv.execute(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); // decode let mut e = GzDecoder::new(&bytes[..]); @@ -134,7 +134,7 @@ fn test_body_gzip_large_random() { assert!(response.status().is_success()); // read response - let bytes = srv.execute(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); // decode let mut e = GzDecoder::new(&bytes[..]); @@ -167,7 +167,7 @@ fn test_body_chunked_implicit() { ); // read response - let bytes = srv.execute(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); // decode let mut e = GzDecoder::new(&bytes[..]); @@ -195,7 +195,7 @@ fn test_body_br_streaming() { assert!(response.status().is_success()); // read response - let bytes = srv.execute(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); // decode br let mut e = BrotliDecoder::new(Vec::with_capacity(2048)); @@ -222,7 +222,7 @@ fn test_head_binary() { } // read response - let bytes = srv.execute(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert!(bytes.is_empty()); } @@ -245,7 +245,7 @@ fn test_no_chunking() { assert!(!response.headers().contains_key(TRANSFER_ENCODING)); // read response - let bytes = srv.execute(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } @@ -267,7 +267,7 @@ fn test_body_deflate() { assert!(response.status().is_success()); // read response - let bytes = srv.execute(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); // decode deflate let mut e = ZlibDecoder::new(Vec::new()); @@ -294,7 +294,7 @@ fn test_body_brotli() { assert!(response.status().is_success()); // read response - let bytes = srv.execute(response.body()).unwrap(); + let bytes = srv.block_on(response.body()).unwrap(); // decode brotli let mut e = BrotliDecoder::new(Vec::with_capacity(2048)); @@ -327,11 +327,11 @@ fn test_body_brotli() { // .header(http::header::CONTENT_ENCODING, "gzip") // .body(enc.clone()) // .unwrap(); -// let response = srv.execute(request.send()).unwrap(); +// let response = srv.block_on(request.send()).unwrap(); // assert!(response.status().is_success()); // // read response -// let bytes = srv.execute(response.body()).unwrap(); +// let bytes = srv.block_on(response.body()).unwrap(); // assert_eq!(bytes, Bytes::from_static(STR.as_ref())); // } @@ -360,11 +360,11 @@ fn test_body_brotli() { // .header(http::header::CONTENT_ENCODING, "gzip") // .body(enc.clone()) // .unwrap(); -// let response = srv.execute(request.send()).unwrap(); +// let response = srv.block_on(request.send()).unwrap(); // assert!(response.status().is_success()); // // read response -// let bytes = srv.execute(response.body()).unwrap(); +// let bytes = srv.block_on(response.body()).unwrap(); // assert_eq!(bytes, Bytes::from(data)); // } @@ -397,11 +397,11 @@ fn test_body_brotli() { // .header(http::header::CONTENT_ENCODING, "gzip") // .body(enc.clone()) // .unwrap(); -// let response = srv.execute(request.send()).unwrap(); +// let response = srv.block_on(request.send()).unwrap(); // assert!(response.status().is_success()); // // read response -// let bytes = srv.execute(response.body()).unwrap(); +// let bytes = srv.block_on(response.body()).unwrap(); // assert_eq!(bytes.len(), data.len()); // assert_eq!(bytes, Bytes::from(data)); // } @@ -430,11 +430,11 @@ fn test_body_brotli() { // .header(http::header::CONTENT_ENCODING, "deflate") // .body(enc) // .unwrap(); -// let response = srv.execute(request.send()).unwrap(); +// let response = srv.block_on(request.send()).unwrap(); // assert!(response.status().is_success()); // // read response -// let bytes = srv.execute(response.body()).unwrap(); +// let bytes = srv.block_on(response.body()).unwrap(); // assert_eq!(bytes, Bytes::from_static(STR.as_ref())); // } @@ -463,11 +463,11 @@ fn test_body_brotli() { // .header(http::header::CONTENT_ENCODING, "deflate") // .body(enc) // .unwrap(); -// let response = srv.execute(request.send()).unwrap(); +// let response = srv.block_on(request.send()).unwrap(); // assert!(response.status().is_success()); // // read response -// let bytes = srv.execute(response.body()).unwrap(); +// let bytes = srv.block_on(response.body()).unwrap(); // assert_eq!(bytes, Bytes::from(data)); // } @@ -500,7 +500,7 @@ fn test_body_brotli() { // .header(http::header::CONTENT_ENCODING, "deflate") // .body(enc) // .unwrap(); -// let response = srv.execute(request.send()).unwrap(); +// let response = srv.block_on(request.send()).unwrap(); // assert!(response.status().is_success()); // // read response From 414614e1b50d3cc14c44341bcc0ea7cce21c7b43 Mon Sep 17 00:00:00 2001 From: lagudomeze Date: Sat, 16 Mar 2019 12:08:39 +0800 Subject: [PATCH 088/109] change marco import (#727) --- examples/basic.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/basic.rs b/examples/basic.rs index f8b81648..e8591f77 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,9 +1,6 @@ use futures::IntoFuture; -#[macro_use] -extern crate actix_web; - -use actix_web::{middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer}; +use actix_web::{get, middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer}; #[get("/resource1/{name}/index.html")] fn index(req: HttpRequest, name: web::Path) -> String { From d93fe157b9cbb568860c7830209248eda3eeeb11 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 16 Mar 2019 11:58:01 -0700 Subject: [PATCH 089/109] use better name Route::data instead of Route::config --- src/extract/form.rs | 2 +- src/extract/json.rs | 2 +- src/extract/mod.rs | 2 +- src/extract/payload.rs | 2 +- src/route.rs | 50 ++++++++++++++---------------------------- src/test.rs | 4 ++-- 6 files changed, 22 insertions(+), 40 deletions(-) diff --git a/src/extract/form.rs b/src/extract/form.rs index 19849ac8..6b13c5f8 100644 --- a/src/extract/form.rs +++ b/src/extract/form.rs @@ -130,7 +130,7 @@ impl fmt::Display for Form { /// web::resource("/index.html") /// .route(web::get() /// // change `Form` extractor configuration -/// .config(web::FormConfig::default().limit(4097)) +/// .data(web::FormConfig::default().limit(4097)) /// .to(index)) /// ); /// } diff --git a/src/extract/json.rs b/src/extract/json.rs index f74b082d..3847e71a 100644 --- a/src/extract/json.rs +++ b/src/extract/json.rs @@ -215,7 +215,7 @@ where /// fn main() { /// let app = App::new().service( /// web::resource("/index.html").route( -/// web::post().config( +/// web::post().data( /// // change json extractor configuration /// web::JsonConfig::default().limit(4096) /// .error_handler(|err, req| { // <- create custom error response diff --git a/src/extract/mod.rs b/src/extract/mod.rs index 25a046d4..738f8918 100644 --- a/src/extract/mod.rs +++ b/src/extract/mod.rs @@ -262,7 +262,7 @@ mod tests { header::CONTENT_TYPE, "application/x-www-form-urlencoded", ) - .config(FormConfig::default().limit(4096)) + .route_data(FormConfig::default().limit(4096)) .to_from(); let r = block_on(Option::>::from_request(&mut req)).unwrap(); diff --git a/src/extract/payload.rs b/src/extract/payload.rs index 13532eee..3fc0c964 100644 --- a/src/extract/payload.rs +++ b/src/extract/payload.rs @@ -177,7 +177,7 @@ where /// let app = App::new().service( /// web::resource("/index.html").route( /// web::get() -/// .config(web::PayloadConfig::new(4096)) // <- limit size of the payload +/// .data(web::PayloadConfig::new(4096)) // <- limit size of the payload /// .to(index)) // <- register handler with extractor params /// ); /// } diff --git a/src/route.rs b/src/route.rs index 5d339a3b..30905d40 100644 --- a/src/route.rs +++ b/src/route.rs @@ -40,28 +40,28 @@ type BoxedRouteNewService = Box< pub struct Route

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

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

    { - let config_ref = Rc::new(RefCell::new(None)); + let data_ref = Rc::new(RefCell::new(None)); Route { service: Box::new(RouteNewService::new( - Extract::new(config_ref.clone()).and_then( + Extract::new(data_ref.clone()).and_then( Handler::new(HttpResponse::NotFound).map_err(|_| panic!()), ), )), guards: Rc::new(Vec::new()), - config: None, - config_ref, + data: None, + data_ref, } } pub(crate) fn finish(mut self) -> Self { - *self.config_ref.borrow_mut() = self.config.take().map(|e| Rc::new(e)); + *self.data_ref.borrow_mut() = self.data.take().map(|e| Rc::new(e)); self } @@ -180,24 +180,6 @@ impl Route

    { self } - // pub fn map>( - // self, - // md: F, - // ) -> RouteServiceBuilder - // where - // T: NewService< - // Request = HandlerRequest, - // Response = HandlerRequest, - // InitError = Error, - // >, - // { - // RouteServiceBuilder { - // service: md.into_new_service(), - // guards: self.guards, - // _t: PhantomData, - // } - // } - /// Set handler function, use request extractors for parameters. /// /// ```rust @@ -253,7 +235,7 @@ impl Route

    { R: Responder + 'static, { self.service = Box::new(RouteNewService::new( - Extract::new(self.config_ref.clone()) + Extract::new(self.data_ref.clone()) .and_then(Handler::new(handler).map_err(|_| panic!())), )); self @@ -295,14 +277,14 @@ impl Route

    { R::Error: Into, { self.service = Box::new(RouteNewService::new( - Extract::new(self.config_ref.clone()) + Extract::new(self.data_ref.clone()) .and_then(AsyncHandler::new(handler).map_err(|_| panic!())), )); self } - /// This method allows to add extractor configuration - /// for specific route. + /// Provide route specific data. This method allows to add extractor + /// configuration or specific state available via `RouteData` extractor. /// /// ```rust /// use actix_web::{web, App}; @@ -317,17 +299,17 @@ impl Route

    { /// web::resource("/index.html").route( /// web::get() /// // limit size of the payload - /// .config(web::PayloadConfig::new(4096)) + /// .data(web::PayloadConfig::new(4096)) /// // register handler /// .to(index) /// )); /// } /// ``` - pub fn config(mut self, config: C) -> Self { - if self.config.is_none() { - self.config = Some(Extensions::new()); + pub fn data(mut self, data: C) -> Self { + if self.data.is_none() { + self.data = Some(Extensions::new()); } - self.config.as_mut().unwrap().insert(config); + self.data.as_mut().unwrap().insert(data); self } } diff --git a/src/test.rs b/src/test.rs index 57a6d396..4db268f1 100644 --- a/src/test.rs +++ b/src/test.rs @@ -265,8 +265,8 @@ impl TestRequest { self } - /// Set request config - pub fn config(self, data: T) -> Self { + /// Set route data + pub fn route_data(self, data: T) -> Self { self.config.extensions.borrow_mut().insert(data); self } From b1e267bce41be057d4c620c31c098fe099698f8f Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 16 Mar 2019 20:17:27 -0700 Subject: [PATCH 090/109] rename State to a Data --- src/app.rs | 75 +++++++++++++++++++-------------------- src/app_service.rs | 16 ++++----- src/{state.rs => data.rs} | 52 +++++++++++++-------------- src/lib.rs | 6 ++-- 4 files changed, 74 insertions(+), 75 deletions(-) rename src/{state.rs => data.rs} (63%) diff --git a/src/app.rs b/src/app.rs index 2e2a8c2d..b146fb4c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -12,6 +12,7 @@ use futures::IntoFuture; use crate::app_service::{AppChain, AppEntry, AppInit, AppRouting, AppRoutingFactory}; use crate::config::{AppConfig, AppConfigInner}; +use crate::data::{Data, DataFactory}; use crate::dev::{PayloadStream, ResourceDef}; use crate::error::Error; use crate::resource::Resource; @@ -20,7 +21,6 @@ use crate::service::{ HttpServiceFactory, ServiceFactory, ServiceFactoryWrapper, ServiceRequest, ServiceResponse, }; -use crate::state::{State, StateFactory}; type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, Error, ()>; @@ -32,18 +32,17 @@ where T: NewService>, { chain: T, - state: Vec>, + data: Vec>, config: AppConfigInner, _t: PhantomData<(P,)>, } impl App { - /// Create application builder with empty state. Application can - /// be configured with a builder-like pattern. + /// Create application builder. Application can be configured with a builder-like pattern. pub fn new() -> Self { App { chain: AppChain, - state: Vec::new(), + data: Vec::new(), config: AppConfigInner::default(), _t: PhantomData, } @@ -60,51 +59,51 @@ where InitError = (), >, { - /// Set application state. Applicatin state could be accessed - /// by using `State` extractor where `T` is state type. + /// Set application data. Applicatin 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 state must be constructed - /// multiple times. If you want to share state between different + /// 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 - /// state does not need to be `Send` or `Sync`. + /// data does not need to be `Send` or `Sync`. /// /// ```rust /// use std::cell::Cell; /// use actix_web::{web, App}; /// - /// struct MyState { + /// struct MyData { /// counter: Cell, /// } /// - /// fn index(state: web::State) { - /// state.counter.set(state.counter.get() + 1); + /// fn index(data: web::Data) { + /// data.counter.set(data.counter.get() + 1); /// } /// /// fn main() { /// let app = App::new() - /// .state(MyState{ counter: Cell::new(0) }) + /// .data(MyData{ counter: Cell::new(0) }) /// .service( /// web::resource("/index.html").route( /// web::get().to(index))); /// } /// ``` - pub fn state(mut self, state: S) -> Self { - self.state.push(Box::new(State::new(state))); + pub fn data(mut self, data: S) -> Self { + self.data.push(Box::new(Data::new(data))); self } - /// Set application state factory. This function is - /// similar to `.state()` but it accepts state factory. State get + /// Set application data factory. This function is + /// similar to `.data()` but it accepts data factory. Data object get /// constructed asynchronously during application initialization. - pub fn state_factory(mut self, state: F) -> Self + pub fn data_factory(mut self, data: F) -> Self where F: Fn() -> Out + 'static, Out: IntoFuture + 'static, Out::Error: std::fmt::Debug, { - self.state.push(Box::new(state)); + self.data.push(Box::new(data)); self } @@ -138,7 +137,7 @@ where AppRouter { endpoint, chain: self.chain, - state: self.state, + data: self.data, services: Vec::new(), default: None, factory_ref: fref, @@ -174,7 +173,7 @@ where let chain = self.chain.and_then(chain.into_new_service()); App { chain, - state: self.state, + data: self.data, config: self.config, _t: PhantomData, } @@ -183,7 +182,7 @@ where /// 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 + /// This method can be used multiple times with same path, in that case /// multiple resources with one route would be registered for same resource path. /// /// ```rust @@ -223,7 +222,7 @@ where default: None, endpoint: AppEntry::new(fref.clone()), factory_ref: fref, - state: self.state, + data: self.data, config: self.config, services: vec![Box::new(ServiceFactoryWrapper::new(service))], external: Vec::new(), @@ -233,7 +232,7 @@ where /// Set server host name. /// - /// Host name is used by application router aa a hostname for url + /// 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. /// @@ -252,7 +251,7 @@ pub struct AppRouter { services: Vec>>, default: Option>>, factory_ref: Rc>>>, - state: Vec>, + data: Vec>, config: AppConfigInner, external: Vec, _t: PhantomData<(P, B)>, @@ -344,7 +343,7 @@ where AppRouter { endpoint, chain: self.chain, - state: self.state, + data: self.data, services: self.services, default: self.default, factory_ref: self.factory_ref, @@ -429,7 +428,7 @@ where fn into_new_service(self) -> AppInit { AppInit { chain: self.chain, - state: self.state, + data: self.data, endpoint: self.endpoint, services: RefCell::new(self.services), external: RefCell::new(self.external), @@ -489,10 +488,10 @@ mod tests { } #[test] - fn test_state() { + fn test_data() { let mut srv = - init_service(App::new().state(10usize).service( - web::resource("/").to(|_: web::State| HttpResponse::Ok()), + init_service(App::new().data(10usize).service( + web::resource("/").to(|_: web::Data| HttpResponse::Ok()), )); let req = TestRequest::default().to_request(); @@ -500,8 +499,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); let mut srv = - init_service(App::new().state(10u32).service( - web::resource("/").to(|_: web::State| HttpResponse::Ok()), + init_service(App::new().data(10u32).service( + web::resource("/").to(|_: web::Data| HttpResponse::Ok()), )); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); @@ -509,18 +508,18 @@ mod tests { } #[test] - fn test_state_factory() { + fn test_data_factory() { let mut srv = - init_service(App::new().state_factory(|| Ok::<_, ()>(10usize)).service( - web::resource("/").to(|_: web::State| HttpResponse::Ok()), + init_service(App::new().data_factory(|| Ok::<_, ()>(10usize)).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 mut srv = - init_service(App::new().state_factory(|| Ok::<_, ()>(10u32)).service( - web::resource("/").to(|_: web::State| HttpResponse::Ok()), + init_service(App::new().data_factory(|| Ok::<_, ()>(10u32)).service( + web::resource("/").to(|_: web::Data| HttpResponse::Ok()), )); let req = TestRequest::default().to_request(); let resp = block_on(srv.call(req)).unwrap(); diff --git a/src/app_service.rs b/src/app_service.rs index c59b80bc..0bf3d309 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -11,11 +11,11 @@ use futures::future::{ok, Either, FutureResult}; use futures::{Async, Future, Poll}; use crate::config::{AppConfig, ServiceConfig}; +use crate::data::{DataFactory, DataFactoryResult}; use crate::error::Error; use crate::guard::Guard; use crate::rmap::ResourceMap; use crate::service::{ServiceFactory, ServiceRequest, ServiceResponse}; -use crate::state::{StateFactory, StateFactoryResult}; type Guards = Vec>; type HttpService

    = BoxedService, ServiceResponse, Error>; @@ -24,7 +24,7 @@ type HttpNewService

    = type BoxedResponse = Box>; /// Service factory to convert `Request` to a `ServiceRequest`. -/// It also executes state factories. +/// It also executes data factories. pub struct AppInit where C: NewService>, @@ -37,7 +37,7 @@ where { pub(crate) chain: C, pub(crate) endpoint: T, - pub(crate) state: Vec>, + pub(crate) data: Vec>, pub(crate) config: RefCell, pub(crate) services: RefCell>>>, pub(crate) default: Option>>, @@ -121,7 +121,7 @@ where chain_fut: self.chain.new_service(&()), endpoint: None, endpoint_fut: self.endpoint.new_service(&()), - state: self.state.iter().map(|s| s.construct()).collect(), + data: self.data.iter().map(|s| s.construct()).collect(), config: self.config.borrow().clone(), rmap, _t: PhantomData, @@ -139,7 +139,7 @@ where chain_fut: C::Future, endpoint_fut: T::Future, rmap: Rc, - state: Vec>, + data: Vec>, config: AppConfig, _t: PhantomData<(P, B)>, } @@ -165,9 +165,9 @@ where fn poll(&mut self) -> Poll { let mut idx = 0; let mut extensions = self.config.0.extensions.borrow_mut(); - while idx < self.state.len() { - if let Async::Ready(_) = self.state[idx].poll_result(&mut extensions)? { - self.state.remove(idx); + while idx < self.data.len() { + if let Async::Ready(_) = self.data[idx].poll_result(&mut extensions)? { + self.data.remove(idx); } else { idx += 1; } diff --git a/src/state.rs b/src/data.rs similarity index 63% rename from src/state.rs rename to src/data.rs index b70540c0..a172cb35 100644 --- a/src/state.rs +++ b/src/data.rs @@ -8,21 +8,21 @@ use futures::{Async, Future, IntoFuture, Poll}; use crate::extract::FromRequest; use crate::service::ServiceFromRequest; -/// Application state factory -pub(crate) trait StateFactory { - fn construct(&self) -> Box; +/// Application data factory +pub(crate) trait DataFactory { + fn construct(&self) -> Box; } -pub(crate) trait StateFactoryResult { +pub(crate) trait DataFactoryResult { fn poll_result(&mut self, extensions: &mut Extensions) -> Poll<(), ()>; } /// Application state -pub struct State(Arc); +pub struct Data(Arc); -impl State { - pub(crate) fn new(state: T) -> State { - State(Arc::new(state)) +impl Data { + pub(crate) fn new(state: T) -> Data { + Data(Arc::new(state)) } /// Get referecnce to inner state type. @@ -31,7 +31,7 @@ impl State { } } -impl Deref for State { +impl Deref for Data { type Target = T; fn deref(&self) -> &T { @@ -39,19 +39,19 @@ impl Deref for State { } } -impl Clone for State { - fn clone(&self) -> State { - State(self.0.clone()) +impl Clone for Data { + fn clone(&self) -> Data { + Data(self.0.clone()) } } -impl FromRequest

    for State { +impl FromRequest

    for Data { type Error = Error; type Future = Result; #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { - if let Some(st) = req.config().extensions().get::>() { + if let Some(st) = req.config().extensions().get::>() { Ok(st.clone()) } else { Err(ErrorInternalServerError( @@ -61,37 +61,37 @@ impl FromRequest

    for State { } } -impl StateFactory for State { - fn construct(&self) -> Box { - Box::new(StateFut { st: self.clone() }) +impl DataFactory for Data { + fn construct(&self) -> Box { + Box::new(DataFut { st: self.clone() }) } } -struct StateFut { - st: State, +struct DataFut { + st: Data, } -impl StateFactoryResult for StateFut { +impl DataFactoryResult for DataFut { fn poll_result(&mut self, extensions: &mut Extensions) -> Poll<(), ()> { extensions.insert(self.st.clone()); Ok(Async::Ready(())) } } -impl StateFactory for F +impl DataFactory for F where F: Fn() -> Out + 'static, Out: IntoFuture + 'static, Out::Error: std::fmt::Debug, { - fn construct(&self) -> Box { - Box::new(StateFactoryFut { + fn construct(&self) -> Box { + Box::new(DataFactoryFut { fut: (*self)().into_future(), }) } } -struct StateFactoryFut +struct DataFactoryFut where F: Future, F::Error: std::fmt::Debug, @@ -99,7 +99,7 @@ where fut: F, } -impl StateFactoryResult for StateFactoryFut +impl DataFactoryResult for DataFactoryFut where F: Future, F::Error: std::fmt::Debug, @@ -107,7 +107,7 @@ where fn poll_result(&mut self, extensions: &mut Extensions) -> Poll<(), ()> { match self.fut.poll() { Ok(Async::Ready(s)) => { - extensions.insert(State::new(s)); + extensions.insert(Data::new(s)); Ok(Async::Ready(())) } Ok(Async::NotReady) => Ok(Async::NotReady), diff --git a/src/lib.rs b/src/lib.rs index d653fd1c..843ad103 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ mod app; mod app_service; mod config; +mod data; pub mod error; mod extract; pub mod guard; @@ -17,7 +18,6 @@ mod route; mod scope; mod server; mod service; -mod state; pub mod test; #[allow(unused_imports)] @@ -37,7 +37,6 @@ pub use crate::request::HttpRequest; pub use crate::resource::Resource; pub use crate::responder::{Either, Responder}; pub use crate::route::Route; -pub use crate::scope::Scope; pub use crate::server::HttpServer; pub mod dev { @@ -77,6 +76,7 @@ pub mod dev { } pub mod web { + //! Various types use actix_http::{http::Method, Response}; use actix_rt::blocking; use futures::{Future, IntoFuture}; @@ -91,11 +91,11 @@ pub mod web { use crate::route::Route; use crate::scope::Scope; + pub use crate::data::Data; pub use crate::error::{BlockingError, Error}; pub use crate::extract::{Form, Json, Path, Payload, Query}; pub use crate::extract::{FormConfig, JsonConfig, PayloadConfig}; pub use crate::request::HttpRequest; - pub use crate::state::State; /// Create resource for a specific path. /// From 60386f1791e6bd005889d566eba1ba0b76699401 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 16 Mar 2019 21:09:11 -0700 Subject: [PATCH 091/109] introduce RouteData extractor --- examples/basic.rs | 4 +- src/app.rs | 20 ----- src/data.rs | 182 ++++++++++++++++++++++++++++++++++++++++- src/extract/form.rs | 2 +- src/extract/json.rs | 2 +- src/extract/payload.rs | 4 +- src/lib.rs | 2 +- src/route.rs | 3 +- src/service.rs | 16 ++-- 9 files changed, 198 insertions(+), 37 deletions(-) diff --git a/examples/basic.rs b/examples/basic.rs index e8591f77..756f1b79 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,6 +1,8 @@ use futures::IntoFuture; -use actix_web::{get, middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer}; +use actix_web::{ + get, middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer, +}; #[get("/resource1/{name}/index.html")] fn index(req: HttpRequest, name: web::Path) -> String { diff --git a/src/app.rs b/src/app.rs index b146fb4c..8c416808 100644 --- a/src/app.rs +++ b/src/app.rs @@ -487,26 +487,6 @@ mod tests { assert_eq!(resp.status(), StatusCode::CREATED); } - #[test] - fn test_data() { - let mut srv = - init_service(App::new().data(10usize).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 mut srv = - init_service(App::new().data(10u32).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::INTERNAL_SERVER_ERROR); - } - #[test] fn test_data_factory() { let mut srv = diff --git a/src/data.rs b/src/data.rs index a172cb35..6fb8e0b9 100644 --- a/src/data.rs +++ b/src/data.rs @@ -17,7 +17,45 @@ pub(crate) trait DataFactoryResult { fn poll_result(&mut self, extensions: &mut Extensions) -> Poll<(), ()>; } -/// Application state +/// Application data. +/// +/// Application data is an arbitrary data attached to the app. +/// Application data is available to all routes and could be added +/// during application configuration process +/// with `App::data()` method. +/// +/// Applicatin 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`. +/// +/// ```rust +/// use std::cell::Cell; +/// use actix_web::{web, App}; +/// +/// struct MyData { +/// counter: Cell, +/// } +/// +/// /// Use `Data` extractor to access data in handler. +/// fn index(data: web::Data) { +/// data.counter.set(data.counter.get() + 1); +/// } +/// +/// fn main() { +/// let app = App::new() +/// // Store `MyData` in application storage. +/// .data(MyData{ counter: Cell::new(0) }) +/// .service( +/// web::resource("/index.html").route( +/// web::get().to(index))); +/// } +/// ``` pub struct Data(Arc); impl Data { @@ -25,7 +63,7 @@ impl Data { Data(Arc::new(state)) } - /// Get referecnce to inner state type. + /// Get referecnce to inner app data. pub fn get_ref(&self) -> &T { self.0.as_ref() } @@ -55,7 +93,7 @@ impl FromRequest

    for Data { Ok(st.clone()) } else { Err(ErrorInternalServerError( - "State is not configured, to configure use App::state()", + "App data is not configured, to configure use App::data()", )) } } @@ -118,3 +156,141 @@ where } } } + +/// Route data. +/// +/// Route data is an arbitrary data attached to specific route. +/// Route data could be added to route during route configuration process +/// with `Route::data()` method. Route data is also used as an extractor +/// configuration storage. Route data could be accessed in handler +/// via `RouteData` extractor. +/// +/// ```rust +/// # use std::cell::Cell; +/// use actix_web::{web, App}; +/// +/// struct MyData { +/// counter: Cell, +/// } +/// +/// /// Use `RouteData` extractor to access data in handler. +/// fn index(data: web::RouteData) { +/// data.counter.set(data.counter.get() + 1); +/// } +/// +/// fn main() { +/// let app = App::new().service( +/// web::resource("/index.html").route( +/// web::get() +/// // Store `MyData` in route storage +/// .data(MyData{ counter: Cell::new(0) }) +/// // Route data could be used as extractor configuration storage, +/// // limit size of the payload +/// .data(web::PayloadConfig::new(4096)) +/// // register handler +/// .to(index) +/// )); +/// } +/// ``` +/// +/// If route data is not set for a handler, using `RouteData` extractor +/// would cause `Internal Server error` response. +pub struct RouteData(Arc); + +impl RouteData { + pub(crate) fn new(state: T) -> RouteData { + RouteData(Arc::new(state)) + } + + /// Get referecnce to inner data object. + pub fn get_ref(&self) -> &T { + self.0.as_ref() + } +} + +impl Deref for RouteData { + type Target = T; + + fn deref(&self) -> &T { + self.0.as_ref() + } +} + +impl Clone for RouteData { + fn clone(&self) -> RouteData { + RouteData(self.0.clone()) + } +} + +impl FromRequest

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

    ) -> Self::Future { + if let Some(st) = req.route_data::() { + Ok(st.clone()) + } else { + Err(ErrorInternalServerError( + "Route data is not configured, to configure use Route::data()", + )) + } + } +} + +#[cfg(test)] +mod tests { + use actix_service::Service; + + use crate::http::StatusCode; + use crate::test::{block_on, init_service, TestRequest}; + use crate::{web, App, HttpResponse}; + + #[test] + fn test_data_extractor() { + let mut srv = + init_service(App::new().data(10usize).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 mut srv = + init_service(App::new().data(10u32).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::INTERNAL_SERVER_ERROR); + } + + #[test] + fn test_route_data_extractor() { + let mut srv = init_service(App::new().service(web::resource("/").route( + web::get().data(10usize).to(|data: web::RouteData| { + let _ = data.clone(); + HttpResponse::Ok() + }), + ))); + + let req = TestRequest::default().to_request(); + let resp = block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + // different type + let mut srv = init_service( + App::new().service( + web::resource("/").route( + web::get() + .data(10u32) + .to(|_: web::RouteData| HttpResponse::Ok()), + ), + ), + ); + let req = TestRequest::default().to_request(); + let resp = block_on(srv.call(req)).unwrap(); + assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); + } +} diff --git a/src/extract/form.rs b/src/extract/form.rs index 6b13c5f8..4a5e9729 100644 --- a/src/extract/form.rs +++ b/src/extract/form.rs @@ -77,7 +77,7 @@ where fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { let req2 = req.clone(); let (limit, err) = req - .load_config::() + .route_data::() .map(|c| (c.limit, c.ehandler.clone())) .unwrap_or((16384, None)); diff --git a/src/extract/json.rs b/src/extract/json.rs index 3847e71a..92b7f20f 100644 --- a/src/extract/json.rs +++ b/src/extract/json.rs @@ -177,7 +177,7 @@ where fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { let req2 = req.clone(); let (limit, err) = req - .load_config::() + .route_data::() .map(|c| (c.limit, c.ehandler.clone())) .unwrap_or((32768, None)); diff --git a/src/extract/payload.rs b/src/extract/payload.rs index 3fc0c964..7164a544 100644 --- a/src/extract/payload.rs +++ b/src/extract/payload.rs @@ -140,7 +140,7 @@ where #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { let mut tmp; - let cfg = if let Some(cfg) = req.load_config::() { + let cfg = if let Some(cfg) = req.route_data::() { cfg } else { tmp = PayloadConfig::default(); @@ -193,7 +193,7 @@ where #[inline] fn from_request(req: &mut ServiceFromRequest

    ) -> Self::Future { let mut tmp; - let cfg = if let Some(cfg) = req.load_config::() { + let cfg = if let Some(cfg) = req.route_data::() { cfg } else { tmp = PayloadConfig::default(); diff --git a/src/lib.rs b/src/lib.rs index 843ad103..b22e05da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -91,7 +91,7 @@ pub mod web { use crate::route::Route; use crate::scope::Scope; - pub use crate::data::Data; + pub use crate::data::{Data, RouteData}; pub use crate::error::{BlockingError, Error}; pub use crate::extract::{Form, Json, Path, Payload, Query}; pub use crate::extract::{FormConfig, JsonConfig, PayloadConfig}; diff --git a/src/route.rs b/src/route.rs index 30905d40..c44ed713 100644 --- a/src/route.rs +++ b/src/route.rs @@ -6,6 +6,7 @@ use actix_http::{http::Method, Error, Extensions, Response}; use actix_service::{NewService, Service}; use futures::{Async, Future, IntoFuture, Poll}; +use crate::data::RouteData; use crate::extract::FromRequest; use crate::guard::{self, Guard}; use crate::handler::{AsyncFactory, AsyncHandler, Extract, Factory, Handler}; @@ -309,7 +310,7 @@ impl Route

    { if self.data.is_none() { self.data = Some(Extensions::new()); } - self.data.as_mut().unwrap().insert(data); + self.data.as_mut().unwrap().insert(RouteData::new(data)); self } } diff --git a/src/service.rs b/src/service.rs index e907a1ab..b8c3a158 100644 --- a/src/service.rs +++ b/src/service.rs @@ -13,6 +13,7 @@ use actix_router::{Path, Resource, Url}; use futures::future::{ok, FutureResult, IntoFuture}; use crate::config::{AppConfig, ServiceConfig}; +use crate::data::RouteData; use crate::request::HttpRequest; use crate::rmap::ResourceMap; @@ -241,15 +242,15 @@ impl

    fmt::Debug for ServiceRequest

    { pub struct ServiceFromRequest

    { req: HttpRequest, payload: Payload

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

    ServiceFromRequest

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

    , config: Option>) -> Self { + pub(crate) fn new(req: ServiceRequest

    , data: Option>) -> Self { Self { req: req.req, payload: req.payload, - config, + data, } } @@ -269,10 +270,11 @@ impl

    ServiceFromRequest

    { ServiceResponse::new(self.req, err.into().into()) } - /// Load extractor configuration - pub fn load_config(&self) -> Option<&T> { - if let Some(ref ext) = self.config { - ext.get::() + /// 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 } From e396c90c9ec0cdfb3b384dd4c16ea8a5b6e96757 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 16 Mar 2019 21:13:16 -0700 Subject: [PATCH 092/109] update api doc --- src/data.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/data.rs b/src/data.rs index 6fb8e0b9..a53015c2 100644 --- a/src/data.rs +++ b/src/data.rs @@ -34,6 +34,9 @@ pub(crate) trait DataFactoryResult { /// threads, a shared object should be used, e.g. `Arc`. Application /// data does not need to be `Send` or `Sync`. /// +/// If route data is not set for a handler, using `Data` extractor would +/// cause *Internal Server Error* response. +/// /// ```rust /// use std::cell::Cell; /// use actix_web::{web, App}; @@ -165,6 +168,9 @@ where /// configuration storage. Route data could be accessed in handler /// via `RouteData` extractor. /// +/// If route data is not set for a handler, using `RouteData` extractor +/// would cause *Internal Server Error* response. +/// /// ```rust /// # use std::cell::Cell; /// use actix_web::{web, App}; @@ -192,9 +198,6 @@ where /// )); /// } /// ``` -/// -/// If route data is not set for a handler, using `RouteData` extractor -/// would cause `Internal Server error` response. pub struct RouteData(Arc); impl RouteData { From 4a4826b23a72c539c26c9e69b9a69ec38a3fd828 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 16 Mar 2019 21:35:02 -0700 Subject: [PATCH 093/109] cleanup doc strings and clippy warnings --- src/app.rs | 15 ++++++++++----- src/info.rs | 4 ++-- src/lib.rs | 4 ++-- src/resource.rs | 13 +++++++------ src/route.rs | 2 +- src/scope.rs | 26 ++++++++++++++++++-------- src/test.rs | 5 +++-- 7 files changed, 43 insertions(+), 26 deletions(-) diff --git a/src/app.rs b/src/app.rs index 8c416808..c4f2e33b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -148,7 +148,7 @@ where } /// Register a request modifier. It can modify any request parameters - /// including payload stream. + /// including payload stream type. pub fn chain( self, chain: C, @@ -211,6 +211,14 @@ where } /// 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, @@ -301,7 +309,7 @@ where /// /// Actix web provides several services implementations: /// - /// * *Resource* is an entry in route table which corresponds to requested URL. + /// * *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(mut self, factory: F) -> Self @@ -354,9 +362,6 @@ where } /// Default resource to be used if no matching route could be found. - /// - /// Default resource works with resources only and does not work with - /// custom services. pub fn default_resource(mut self, f: F) -> Self where F: FnOnce(Resource

    ) -> Resource, diff --git a/src/info.rs b/src/info.rs index c058bd51..9a97c335 100644 --- a/src/info.rs +++ b/src/info.rs @@ -25,7 +25,7 @@ impl ConnectionInfo { Ref::map(req.extensions(), |e| e.get().unwrap()) } - #[cfg_attr(feature = "cargo-clippy", allow(cyclomatic_complexity))] + #[allow(clippy::cyclomatic_complexity)] fn new(req: &RequestHead, cfg: &AppConfig) -> ConnectionInfo { let mut host = None; let mut scheme = None; @@ -123,10 +123,10 @@ impl ConnectionInfo { } ConnectionInfo { + peer, scheme: scheme.unwrap_or("http").to_owned(), host: host.unwrap_or("localhost").to_owned(), remote: remote.map(|s| s.to_owned()), - peer: peer, } } diff --git a/src/lib.rs b/src/lib.rs index b22e05da..509fcc60 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(clippy::type_complexity)] +#![allow(clippy::type_complexity, clippy::new_without_default)] mod app; mod app_service; @@ -84,6 +84,7 @@ pub mod web { pub use actix_http::Response as HttpResponse; pub use bytes::{Bytes, BytesMut}; + use crate::error::{BlockingError, Error}; use crate::extract::FromRequest; use crate::handler::{AsyncFactory, Factory}; use crate::resource::Resource; @@ -92,7 +93,6 @@ pub mod web { use crate::scope::Scope; pub use crate::data::{Data, RouteData}; - pub use crate::error::{BlockingError, Error}; pub use crate::extract::{Form, Json, Path, Payload, Query}; pub use crate::extract::{FormConfig, JsonConfig, PayloadConfig}; pub use crate::request::HttpRequest; diff --git a/src/resource.rs b/src/resource.rs index e4fe65c0..46b3e2a8 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -21,7 +21,7 @@ type HttpService

    = BoxedService, ServiceResponse, Error>; type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, Error, ()>; -/// *Resource* is an entry in route table which corresponds to requested URL. +/// *Resource* is an entry in resources table which corresponds to requested URL. /// /// Resource in turn has at least one route. /// Route consists of an handlers objects and list of guards @@ -132,7 +132,8 @@ where /// } /// ``` /// - /// Multiple routes could be added to a resource. + /// Multiple routes could be added to a resource. Resource object uses + /// match guards for route selection. /// /// ```rust /// use actix_web::{web, guard, App, HttpResponse}; @@ -220,11 +221,11 @@ where self } - /// Register a resource middleware + /// Register a resource middleware. /// - /// This is similar to `App's` middlewares, but - /// middleware is not allowed to change response type (i.e modify response's body). - /// Middleware get invoked on resource level. + /// This is similar to `App's` middlewares, but middleware get invoked on resource level. + /// Resource level middlewares are not allowed to change response + /// type (i.e modify response's body). pub fn middleware( self, mw: F, diff --git a/src/route.rs b/src/route.rs index c44ed713..626b0951 100644 --- a/src/route.rs +++ b/src/route.rs @@ -62,7 +62,7 @@ impl Route

    { } pub(crate) fn finish(mut self) -> Self { - *self.data_ref.borrow_mut() = self.data.take().map(|e| Rc::new(e)); + *self.data_ref.borrow_mut() = self.data.take().map(Rc::new); self } diff --git a/src/scope.rs b/src/scope.rs index 9f5b650c..bf3261f2 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -26,7 +26,7 @@ type HttpNewService

    = BoxedNewService<(), ServiceRequest

    , ServiceResponse, Error, ()>; type BoxedResponse = Box>; -/// Resources scope +/// Resources scope. /// /// Scope is a set of resources with common root path. /// Scopes collect multiple paths under a common path prefix. @@ -114,7 +114,15 @@ where self } - /// Create nested service. + /// Register http service. + /// + /// This is similar to `App's` service registration. + /// + /// 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 /// /// ```rust /// use actix_web::{web, App, HttpRequest}; @@ -145,7 +153,7 @@ where /// Configure route for a specific path. /// /// This is a simplified version of the `Scope::service()` method. - /// This method can not be could multiple times, in that case + /// This method can be called multiple times, in that case /// multiple resources with one route would be registered for same resource path. /// /// ```rust @@ -172,6 +180,8 @@ where } /// Default resource 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 where F: FnOnce(Resource

    ) -> Resource, @@ -190,11 +200,11 @@ where self } - /// Register a scope middleware + /// Register a scope level middleware. /// - /// This is similar to `App's` middlewares, but - /// middleware is not allowed to change response type (i.e modify response's body). - /// Middleware get invoked on scope level. + /// This is similar to `App's` middlewares, but middleware get invoked on scope level. + /// Scope level middlewares are not allowed to change response + /// type (i.e modify response's body). pub fn middleware( self, mw: F, @@ -322,7 +332,7 @@ impl NewService for ScopeFactory

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

    { fut: Vec>, diff --git a/src/test.rs b/src/test.rs index 4db268f1..13db5977 100644 --- a/src/test.rs +++ b/src/test.rs @@ -50,7 +50,7 @@ pub fn run_on(f: F) -> Result where F: Fn() -> Result, { - RT.with(move |rt| rt.borrow_mut().block_on(lazy(|| f()))) + RT.with(move |rt| rt.borrow_mut().block_on(lazy(f))) } /// This method accepts application builder instance, and constructs @@ -169,6 +169,7 @@ impl Default for TestRequest { } } +#[allow(clippy::wrong_self_convention)] impl TestRequest { /// Create TestRequest and set request uri pub fn with_uri(path: &str) -> TestRequest { @@ -254,7 +255,7 @@ impl TestRequest { } /// Set cookie for this request - pub fn cookie<'a>(mut self, cookie: Cookie<'a>) -> Self { + pub fn cookie(mut self, cookie: Cookie) -> Self { self.req.cookie(cookie); self } From 725ee3d3961f9a76105c45579d1e7e8c999fee3c Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 16 Mar 2019 21:43:48 -0700 Subject: [PATCH 094/109] rename extract to types --- src/{extract/mod.rs => extract.rs} | 13 +------------ src/lib.rs | 5 +++-- src/{extract => types}/form.rs | 0 src/{extract => types}/json.rs | 0 src/types/mod.rs | 13 +++++++++++++ src/{extract => types}/path.rs | 3 +-- src/{extract => types}/payload.rs | 0 src/{extract => types}/query.rs | 0 8 files changed, 18 insertions(+), 16 deletions(-) rename src/{extract/mod.rs => extract.rs} (97%) rename src/{extract => types}/form.rs (100%) rename src/{extract => types}/json.rs (100%) create mode 100644 src/types/mod.rs rename src/{extract => types}/path.rs (99%) rename src/{extract => types}/payload.rs (100%) rename src/{extract => types}/query.rs (100%) diff --git a/src/extract/mod.rs b/src/extract.rs similarity index 97% rename from src/extract/mod.rs rename to src/extract.rs index 738f8918..4cd04be2 100644 --- a/src/extract/mod.rs +++ b/src/extract.rs @@ -6,18 +6,6 @@ use futures::{future, Async, Future, IntoFuture, Poll}; use crate::service::ServiceFromRequest; -mod form; -mod json; -mod path; -mod payload; -mod query; - -pub use self::form::{Form, FormConfig}; -pub use self::json::{Json, JsonConfig}; -pub use self::path::Path; -pub use self::payload::{Payload, PayloadConfig}; -pub use self::query::Query; - /// Trait implemented by types that can be extracted from request. /// /// Types that implement this trait can be used with `Route` handlers. @@ -250,6 +238,7 @@ mod tests { use super::*; use crate::test::{block_on, TestRequest}; + use crate::types::{Form, FormConfig, Path, Query}; #[derive(Deserialize, Debug, PartialEq)] struct Info { diff --git a/src/lib.rs b/src/lib.rs index 509fcc60..dc0493a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ mod scope; mod server; mod service; pub mod test; +mod types; #[allow(unused_imports)] #[macro_use] @@ -93,9 +94,9 @@ pub mod web { use crate::scope::Scope; pub use crate::data::{Data, RouteData}; - pub use crate::extract::{Form, Json, Path, Payload, Query}; - pub use crate::extract::{FormConfig, JsonConfig, PayloadConfig}; pub use crate::request::HttpRequest; + pub use crate::types::{Form, Json, Path, Payload, Query}; + pub use crate::types::{FormConfig, JsonConfig, PayloadConfig}; /// Create resource for a specific path. /// diff --git a/src/extract/form.rs b/src/types/form.rs similarity index 100% rename from src/extract/form.rs rename to src/types/form.rs diff --git a/src/extract/json.rs b/src/types/json.rs similarity index 100% rename from src/extract/json.rs rename to src/types/json.rs diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 00000000..b5f8de60 --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,13 @@ +//! Helper types + +mod form; +mod json; +mod path; +mod payload; +mod query; + +pub use self::form::{Form, FormConfig}; +pub use self::json::{Json, JsonConfig}; +pub use self::path::Path; +pub use self::payload::{Payload, PayloadConfig}; +pub use self::query::Query; diff --git a/src/extract/path.rs b/src/types/path.rs similarity index 99% rename from src/extract/path.rs rename to src/types/path.rs index fc6811c7..4e678479 100644 --- a/src/extract/path.rs +++ b/src/types/path.rs @@ -8,8 +8,7 @@ use serde::de; use crate::request::HttpRequest; use crate::service::ServiceFromRequest; - -use super::FromRequest; +use crate::FromRequest; #[derive(PartialEq, Eq, PartialOrd, Ord)] /// Extract typed information from the request's path. diff --git a/src/extract/payload.rs b/src/types/payload.rs similarity index 100% rename from src/extract/payload.rs rename to src/types/payload.rs diff --git a/src/extract/query.rs b/src/types/query.rs similarity index 100% rename from src/extract/query.rs rename to src/types/query.rs From c80884904ccc66ba926a2da69127e8e1d08ddb7b Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 16 Mar 2019 22:04:09 -0700 Subject: [PATCH 095/109] move JsonBody from actix-http --- src/lib.rs | 4 +- src/types/json.rs | 195 ++++++++++++++++++++++++++++++++++++++++++++-- src/types/mod.rs | 2 +- 3 files changed, 192 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index dc0493a8..18cf93f4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,6 +58,7 @@ pub mod dev { pub use crate::service::{ HttpServiceFactory, ServiceFromRequest, ServiceRequest, ServiceResponse, }; + pub use crate::types::json::JsonBody; pub use actix_http::body::{Body, BodyLength, MessageBody, ResponseBody}; pub use actix_http::dev::ResponseBuilder as HttpResponseBuilder; @@ -95,8 +96,7 @@ pub mod web { pub use crate::data::{Data, RouteData}; pub use crate::request::HttpRequest; - pub use crate::types::{Form, Json, Path, Payload, Query}; - pub use crate::types::{FormConfig, JsonConfig, PayloadConfig}; + pub use crate::types::*; /// Create resource for a specific path. /// diff --git a/src/types/json.rs b/src/types/json.rs index 92b7f20f..74ee5eb2 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -3,16 +3,15 @@ use std::rc::Rc; use std::{fmt, ops}; -use bytes::Bytes; -use futures::{Future, Stream}; +use bytes::{Bytes, BytesMut}; +use futures::{Future, Poll, Stream}; use serde::de::DeserializeOwned; use serde::Serialize; use serde_json; -use actix_http::dev::JsonBody; -use actix_http::error::{Error, JsonPayloadError}; -use actix_http::http::StatusCode; -use actix_http::Response; +use actix_http::error::{Error, JsonPayloadError, PayloadError}; +use actix_http::http::{header::CONTENT_LENGTH, StatusCode}; +use actix_http::{HttpMessage, Payload, Response}; use crate::extract::FromRequest; use crate::request::HttpRequest; @@ -257,3 +256,187 @@ impl Default for JsonConfig { } } } + +/// Request's payload json parser, it resolves to a deserialized `T` value. +/// This future could be used with `ServiceRequest` and `ServiceFromRequest`. +/// +/// Returns error: +/// +/// * content type is not `application/json` +/// * content length is greater than 256k +pub struct JsonBody { + limit: usize, + length: Option, + stream: Payload, + err: Option, + fut: Option>>, +} + +impl JsonBody +where + T: HttpMessage, + T::Stream: Stream + 'static, + U: DeserializeOwned + 'static, +{ + /// Create `JsonBody` for request. + pub fn new(req: &mut T) -> Self { + // check content-type + let json = if let Ok(Some(mime)) = req.mime_type() { + mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) + } else { + false + }; + if !json { + return JsonBody { + limit: 262_144, + length: None, + stream: Payload::None, + fut: None, + err: Some(JsonPayloadError::ContentType), + }; + } + + let mut len = None; + 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) + } + } + } + + JsonBody { + limit: 262_144, + length: len, + stream: req.take_payload(), + fut: None, + err: None, + } + } + + /// Change max size of payload. By default max size is 256Kb + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } +} + +impl Future for JsonBody +where + T: HttpMessage, + T::Stream: Stream + 'static, + U: DeserializeOwned + 'static, +{ + type Item = U; + type Error = JsonPayloadError; + + fn poll(&mut self) -> Poll { + if let Some(ref mut fut) = self.fut { + return fut.poll(); + } + + if let Some(err) = self.err.take() { + return Err(err); + } + + let limit = self.limit; + if let Some(len) = self.length.take() { + if len > limit { + return Err(JsonPayloadError::Overflow); + } + } + + let fut = std::mem::replace(&mut self.stream, Payload::None) + .from_err() + .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { + if (body.len() + chunk.len()) > limit { + Err(JsonPayloadError::Overflow) + } else { + body.extend_from_slice(&chunk); + Ok(body) + } + }) + .and_then(|body| Ok(serde_json::from_slice::(&body)?)); + self.fut = Some(Box::new(fut)); + self.poll() + } +} + +#[cfg(test)] +mod tests { + use bytes::Bytes; + use serde_derive::{Deserialize, Serialize}; + + use super::*; + use crate::http::header; + use crate::test::{block_on, TestRequest}; + + fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { + match err { + JsonPayloadError::Overflow => match other { + JsonPayloadError::Overflow => true, + _ => false, + }, + JsonPayloadError::ContentType => match other { + JsonPayloadError::ContentType => true, + _ => false, + }, + _ => false, + } + } + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct MyObject { + name: String, + } + + #[test] + fn test_json_body() { + let mut req = TestRequest::default().to_request(); + let json = block_on(req.json::()); + assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); + + let mut req = TestRequest::default() + .header( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/text"), + ) + .to_request(); + let json = block_on(req.json::()); + assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); + + let mut req = TestRequest::default() + .header( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + ) + .header( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("10000"), + ) + .to_request(); + + let json = block_on(req.json::().limit(100)); + assert!(json_eq(json.err().unwrap(), JsonPayloadError::Overflow)); + + let mut req = 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_request(); + + let json = block_on(req.json::()); + assert_eq!( + json.ok().unwrap(), + MyObject { + name: "test".to_owned() + } + ); + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index b5f8de60..2fc3ca93 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,7 +1,7 @@ //! Helper types mod form; -mod json; +pub(crate) mod json; mod path; mod payload; mod query; From 9012c46fe1e2ced25480e3aa4fd200899368e81a Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 00:48:40 -0700 Subject: [PATCH 096/109] move payload futures from actix-http --- Cargo.toml | 2 +- src/error.rs | 101 +++++++++++++++++ src/lib.rs | 3 + src/types/form.rs | 238 ++++++++++++++++++++++++++++++++++++++--- src/types/json.rs | 8 +- src/types/mod.rs | 5 +- src/types/payload.rs | 145 ++++++++++++++++++++++++- src/types/readlines.rs | 210 ++++++++++++++++++++++++++++++++++++ 8 files changed, 688 insertions(+), 24 deletions(-) create mode 100644 src/types/readlines.rs diff --git a/Cargo.toml b/Cargo.toml index 87a54b6a..ba36dd2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,7 +81,7 @@ mime = "0.3" net2 = "0.2.33" parking_lot = "0.7" regex = "1.0" -serde = "1.0" +serde = { version = "1.0", features=["derive"] } serde_json = "1.0" serde_urlencoded = "^0.5.3" time = "0.1" diff --git a/src/error.rs b/src/error.rs index 06840708..bf224a22 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,8 +3,12 @@ use std::fmt; pub use actix_http::error::*; use derive_more::{Display, From}; +use serde_json::error::Error as JsonError; use url::ParseError as UrlParseError; +use crate::http::StatusCode; +use crate::HttpResponse; + /// Errors which can occur when attempting to generate resource uri. #[derive(Debug, PartialEq, Display, From)] pub enum UrlGenerationError { @@ -41,3 +45,100 @@ impl From> for BlockingError } } } + +/// A set of errors that can occur during parsing urlencoded payloads +#[derive(Debug, Display, From)] +pub enum UrlencodedError { + /// Can not decode chunked transfer encoding + #[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)")] + Overflow, + /// Payload size is now known + #[display(fmt = "Payload size is now known")] + UnknownLength, + /// Content type error + #[display(fmt = "Content type error")] + ContentType, + /// Parse error + #[display(fmt = "Parse error")] + Parse, + /// Payload error + #[display(fmt = "Error that occur during reading payload: {}", _0)] + Payload(PayloadError), +} + +/// Return `BadRequest` for `UrlencodedError` +impl ResponseError for UrlencodedError { + fn error_response(&self) -> HttpResponse { + match *self { + UrlencodedError::Overflow => { + HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE) + } + UrlencodedError::UnknownLength => { + HttpResponse::new(StatusCode::LENGTH_REQUIRED) + } + _ => HttpResponse::new(StatusCode::BAD_REQUEST), + } + } +} + +/// A set of errors that can occur during parsing json payloads +#[derive(Debug, Display, From)] +pub enum JsonPayloadError { + /// Payload size is bigger than allowed. (default: 256kB) + #[display(fmt = "Json payload size is bigger than allowed. (default: 256kB)")] + Overflow, + /// Content type error + #[display(fmt = "Content type error")] + ContentType, + /// Deserialize error + #[display(fmt = "Json deserialize error: {}", _0)] + Deserialize(JsonError), + /// Payload error + #[display(fmt = "Error that occur during reading payload: {}", _0)] + Payload(PayloadError), +} + +/// Return `BadRequest` for `UrlencodedError` +impl ResponseError for JsonPayloadError { + fn error_response(&self) -> HttpResponse { + match *self { + JsonPayloadError::Overflow => { + HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE) + } + _ => HttpResponse::new(StatusCode::BAD_REQUEST), + } + } +} + +/// Error type returned when reading body as lines. +#[derive(From, Display, Debug)] +pub enum ReadlinesError { + /// Error when decoding a line. + #[display(fmt = "Encoding error")] + /// Payload size is bigger than allowed. (default: 256kB) + EncodingError, + /// Payload error. + #[display(fmt = "Error that occur during reading payload: {}", _0)] + Payload(PayloadError), + /// Line limit exceeded. + #[display(fmt = "Line limit exceeded")] + LimitOverflow, + /// ContentType error. + #[display(fmt = "Content-type error")] + ContentTypeError(ContentTypeError), +} + +/// Return `BadRequest` for `ReadlinesError` +impl ResponseError for ReadlinesError { + fn error_response(&self) -> HttpResponse { + match *self { + ReadlinesError::LimitOverflow => { + HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE) + } + _ => HttpResponse::new(StatusCode::BAD_REQUEST), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 18cf93f4..d6bcf4e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,7 +58,10 @@ pub mod dev { pub use crate::service::{ HttpServiceFactory, ServiceFromRequest, 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, BodyLength, MessageBody, ResponseBody}; pub use actix_http::dev::ResponseBuilder as HttpResponseBuilder; diff --git a/src/types/form.rs b/src/types/form.rs index 4a5e9729..58fa3761 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -3,13 +3,17 @@ use std::rc::Rc; use std::{fmt, ops}; -use actix_http::dev::UrlEncoded; -use actix_http::error::{Error, UrlencodedError}; -use bytes::Bytes; -use futures::{Future, Stream}; +use actix_http::error::{Error, PayloadError, UrlencodedError}; +use actix_http::{HttpMessage, Payload}; +use bytes::{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::extract::FromRequest; +use crate::http::header::CONTENT_LENGTH; use crate::request::HttpRequest; use crate::service::ServiceFromRequest; @@ -167,13 +171,145 @@ impl Default for FormConfig { } } +/// Future that resolves to a parsed urlencoded values. +/// +/// Parse `application/x-www-form-urlencoded` encoded request's body. +/// Return `UrlEncoded` future. Form can be deserialized to any type that +/// implements `Deserialize` trait from *serde*. +/// +/// Returns error: +/// +/// * content type is not `application/x-www-form-urlencoded` +/// * content-length is greater than 32k +/// +pub struct UrlEncoded { + stream: Payload, + limit: usize, + length: Option, + encoding: EncodingRef, + err: Option, + fut: Option>>, +} + +impl UrlEncoded +where + T: HttpMessage, + T::Stream: Stream, +{ + /// Create a new future to URL encode a request + pub fn new(req: &mut T) -> UrlEncoded { + // check content type + if req.content_type().to_lowercase() != "application/x-www-form-urlencoded" { + return Self::err(UrlencodedError::ContentType); + } + let encoding = match req.encoding() { + Ok(enc) => enc, + Err(_) => return Self::err(UrlencodedError::ContentType), + }; + + let mut len = None; + 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) + } else { + return Self::err(UrlencodedError::UnknownLength); + } + } else { + return Self::err(UrlencodedError::UnknownLength); + } + }; + + UrlEncoded { + encoding, + stream: req.take_payload(), + limit: 32_768, + length: len, + fut: None, + err: None, + } + } + + fn err(e: UrlencodedError) -> Self { + UrlEncoded { + stream: Payload::None, + limit: 32_768, + fut: None, + err: Some(e), + length: None, + encoding: UTF_8, + } + } + + /// Change max size of payload. By default max size is 256Kb + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } +} + +impl Future for UrlEncoded +where + T: HttpMessage, + T::Stream: Stream + 'static, + U: DeserializeOwned + 'static, +{ + type Item = U; + type Error = UrlencodedError; + + fn poll(&mut self) -> Poll { + if let Some(ref mut fut) = self.fut { + return fut.poll(); + } + + if let Some(err) = self.err.take() { + return Err(err); + } + + // payload size + let limit = self.limit; + if let Some(len) = self.length.take() { + if len > limit { + return Err(UrlencodedError::Overflow); + } + } + + // future + let encoding = self.encoding; + let fut = std::mem::replace(&mut self.stream, Payload::None) + .from_err() + .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { + if (body.len() + chunk.len()) > limit { + Err(UrlencodedError::Overflow) + } else { + body.extend_from_slice(&chunk); + Ok(body) + } + }) + .and_then(move |body| { + if (encoding as *const Encoding) == UTF_8 { + serde_urlencoded::from_bytes::(&body) + .map_err(|_| UrlencodedError::Parse) + } else { + let body = encoding + .decode(&body, DecoderTrap::Strict) + .map_err(|_| UrlencodedError::Parse)?; + serde_urlencoded::from_str::(&body) + .map_err(|_| UrlencodedError::Parse) + } + }); + self.fut = Some(Box::new(fut)); + self.poll() + } +} + #[cfg(test)] mod tests { - use actix_http::http::header; use bytes::Bytes; - use serde_derive::Deserialize; + use serde::Deserialize; use super::*; + use crate::http::header::CONTENT_TYPE; use crate::test::{block_on, TestRequest}; #[derive(Deserialize, Debug, PartialEq)] @@ -183,15 +319,91 @@ mod tests { #[test] fn test_form() { - let mut req = 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(); + let mut req = + TestRequest::with_header(CONTENT_TYPE, "application/x-www-form-urlencoded") + .header(CONTENT_LENGTH, "11") + .set_payload(Bytes::from_static(b"hello=world")) + .to_from(); let s = block_on(Form::::from_request(&mut req)).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, + }, + UrlencodedError::UnknownLength => match other { + UrlencodedError::UnknownLength => true, + _ => false, + }, + UrlencodedError::ContentType => match other { + UrlencodedError::ContentType => true, + _ => false, + }, + _ => false, + } + } + + #[test] + fn test_urlencoded_error() { + let mut req = + 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)); + assert!(eq(info.err().unwrap(), UrlencodedError::UnknownLength)); + + let mut req = + 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)); + assert!(eq(info.err().unwrap(), UrlencodedError::Overflow)); + + let mut req = TestRequest::with_header(CONTENT_TYPE, "text/plain") + .header(CONTENT_LENGTH, "10") + .to_request(); + let info = block_on(UrlEncoded::<_, Info>::new(&mut req)); + assert!(eq(info.err().unwrap(), UrlencodedError::ContentType)); + } + + #[test] + fn test_urlencoded() { + let mut req = + TestRequest::with_header(CONTENT_TYPE, "application/x-www-form-urlencoded") + .header(CONTENT_LENGTH, "11") + .set_payload(Bytes::from_static(b"hello=world")) + .to_request(); + + let info = block_on(UrlEncoded::<_, Info>::new(&mut req)).unwrap(); + assert_eq!( + info, + Info { + hello: "world".to_owned() + } + ); + + let mut req = 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(); + + let info = block_on(UrlEncoded::<_, Info>::new(&mut req)).unwrap(); + assert_eq!( + info, + Info { + hello: "world".to_owned() + } + ); + } } diff --git a/src/types/json.rs b/src/types/json.rs index 74ee5eb2..18a6be90 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -393,7 +393,7 @@ mod tests { #[test] fn test_json_body() { let mut req = TestRequest::default().to_request(); - let json = block_on(req.json::()); + let json = block_on(JsonBody::<_, MyObject>::new(&mut req)); assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); let mut req = TestRequest::default() @@ -402,7 +402,7 @@ mod tests { header::HeaderValue::from_static("application/text"), ) .to_request(); - let json = block_on(req.json::()); + let json = block_on(JsonBody::<_, MyObject>::new(&mut req)); assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); let mut req = TestRequest::default() @@ -416,7 +416,7 @@ mod tests { ) .to_request(); - let json = block_on(req.json::().limit(100)); + let json = block_on(JsonBody::<_, MyObject>::new(&mut req).limit(100)); assert!(json_eq(json.err().unwrap(), JsonPayloadError::Overflow)); let mut req = TestRequest::default() @@ -431,7 +431,7 @@ mod tests { .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) .to_request(); - let json = block_on(req.json::()); + let json = block_on(JsonBody::<_, MyObject>::new(&mut req)); assert_eq!( json.ok().unwrap(), MyObject { diff --git a/src/types/mod.rs b/src/types/mod.rs index 2fc3ca93..30ee7309 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,10 +1,11 @@ //! Helper types -mod form; +pub(crate) mod form; pub(crate) mod json; mod path; -mod payload; +pub(crate) mod payload; mod query; +pub(crate) mod readlines; pub use self::form::{Form, FormConfig}; pub use self::json::{Json, JsonConfig}; diff --git a/src/types/payload.rs b/src/types/payload.rs index 7164a544..402486b6 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -1,10 +1,9 @@ //! Payload/Bytes/String extractors use std::str; -use actix_http::dev::MessageBody; use actix_http::error::{Error, ErrorBadRequest, PayloadError}; use actix_http::HttpMessage; -use bytes::Bytes; +use bytes::{Bytes, BytesMut}; use encoding::all::UTF_8; use encoding::types::{DecoderTrap, Encoding}; use futures::future::{err, Either, FutureResult}; @@ -12,6 +11,7 @@ use futures::{Future, Poll, Stream}; use mime::Mime; use crate::extract::FromRequest; +use crate::http::header; use crate::service::ServiceFromRequest; /// Payload extractor returns request 's payload stream. @@ -152,7 +152,7 @@ where } let limit = cfg.limit; - Either::A(Box::new(MessageBody::new(req).limit(limit).from_err())) + Either::A(Box::new(HttpMessageBody::new(req).limit(limit).from_err())) } } @@ -213,7 +213,7 @@ where let limit = cfg.limit; Either::A(Box::new( - MessageBody::new(req) + HttpMessageBody::new(req) .limit(limit) .from_err() .and_then(move |body| { @@ -287,6 +287,109 @@ impl Default for PayloadConfig { } } +/// Future that resolves to a complete http message body. +/// +/// Load http message body. +/// +/// 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 { + limit: usize, + length: Option, + stream: actix_http::Payload, + err: Option, + fut: Option>>, +} + +impl HttpMessageBody +where + T: HttpMessage, + T::Stream: Stream, +{ + /// Create `MessageBody` for request. + pub fn new(req: &mut T) -> HttpMessageBody { + let mut len = None; + 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) + } else { + return Self::err(PayloadError::UnknownLength); + } + } else { + return Self::err(PayloadError::UnknownLength); + } + } + + HttpMessageBody { + stream: req.take_payload(), + limit: 262_144, + length: len, + fut: None, + err: None, + } + } + + /// Change max size of payload. By default max size is 256Kb + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } + + fn err(e: PayloadError) -> Self { + HttpMessageBody { + stream: actix_http::Payload::None, + limit: 262_144, + fut: None, + err: Some(e), + length: None, + } + } +} + +impl Future for HttpMessageBody +where + T: HttpMessage, + T::Stream: Stream + 'static, +{ + type Item = Bytes; + type Error = PayloadError; + + fn poll(&mut self) -> Poll { + if let Some(ref mut fut) = self.fut { + return fut.poll(); + } + + if let Some(err) = self.err.take() { + return Err(err); + } + + if let Some(len) = self.length.take() { + if len > self.limit { + return Err(PayloadError::Overflow); + } + } + + // future + let limit = self.limit; + self.fut = Some(Box::new( + std::mem::replace(&mut self.stream, actix_http::Payload::None) + .from_err() + .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { + if (body.len() + chunk.len()) > limit { + Err(PayloadError::Overflow) + } else { + body.extend_from_slice(&chunk); + Ok(body) + } + }) + .map(|body| body.freeze()), + )); + self.poll() + } +} + #[cfg(test)] mod tests { use bytes::Bytes; @@ -332,4 +435,38 @@ mod tests { let s = block_on(String::from_request(&mut req)).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)); + 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)); + match res.err().unwrap() { + PayloadError::Overflow => (), + _ => unreachable!("error"), + } + + let mut req = TestRequest::default() + .set_payload(Bytes::from_static(b"test")) + .to_request(); + let res = block_on(HttpMessageBody::new(&mut req)); + assert_eq!(res.ok().unwrap(), Bytes::from_static(b"test")); + + let mut req = TestRequest::default() + .set_payload(Bytes::from_static(b"11111111111111")) + .to_request(); + let res = block_on(HttpMessageBody::new(&mut req).limit(5)); + match res.err().unwrap() { + PayloadError::Overflow => (), + _ => unreachable!("error"), + } + } } diff --git a/src/types/readlines.rs b/src/types/readlines.rs new file mode 100644 index 00000000..2c7f699a --- /dev/null +++ b/src/types/readlines.rs @@ -0,0 +1,210 @@ +use std::str; + +use bytes::{Bytes, BytesMut}; +use encoding::all::UTF_8; +use encoding::types::{DecoderTrap, Encoding}; +use encoding::EncodingRef; +use futures::{Async, Poll, Stream}; + +use crate::dev::Payload; +use crate::error::{PayloadError, ReadlinesError}; +use crate::HttpMessage; + +/// Stream to read request line by line. +pub struct Readlines { + stream: Payload, + buff: BytesMut, + limit: usize, + checked_buff: bool, + encoding: EncodingRef, + err: Option, +} + +impl Readlines +where + T: HttpMessage, + T::Stream: Stream, +{ + /// Create a new stream to read request line by line. + pub fn new(req: &mut T) -> Self { + let encoding = match req.encoding() { + Ok(enc) => enc, + Err(err) => return Self::err(err.into()), + }; + + Readlines { + stream: req.take_payload(), + buff: BytesMut::with_capacity(262_144), + limit: 262_144, + checked_buff: true, + err: None, + encoding, + } + } + + /// Change max line size. By default max size is 256Kb + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } + + fn err(err: ReadlinesError) -> Self { + Readlines { + stream: Payload::None, + buff: BytesMut::new(), + limit: 262_144, + checked_buff: true, + encoding: UTF_8, + err: Some(err), + } + } +} + +impl Stream for Readlines +where + T: HttpMessage, + T::Stream: Stream, +{ + type Item = String; + type Error = ReadlinesError; + + fn poll(&mut self) -> Poll, Self::Error> { + if let Some(err) = self.err.take() { + return Err(err); + } + + // check if there is a newline in the buffer + if !self.checked_buff { + let mut found: Option = None; + for (ind, b) in self.buff.iter().enumerate() { + if *b == b'\n' { + found = Some(ind); + break; + } + } + if let Some(ind) = found { + // check if line is longer than limit + if ind + 1 > self.limit { + return Err(ReadlinesError::LimitOverflow); + } + let enc: *const Encoding = self.encoding as *const Encoding; + let line = if enc == UTF_8 { + str::from_utf8(&self.buff.split_to(ind + 1)) + .map_err(|_| ReadlinesError::EncodingError)? + .to_owned() + } else { + self.encoding + .decode(&self.buff.split_to(ind + 1), DecoderTrap::Strict) + .map_err(|_| ReadlinesError::EncodingError)? + }; + return Ok(Async::Ready(Some(line))); + } + self.checked_buff = true; + } + // poll req for more bytes + match self.stream.poll() { + Ok(Async::Ready(Some(mut bytes))) => { + // check if there is a newline in bytes + let mut found: Option = None; + for (ind, b) in bytes.iter().enumerate() { + if *b == b'\n' { + found = Some(ind); + break; + } + } + if let Some(ind) = found { + // check if line is longer than limit + if ind + 1 > self.limit { + return Err(ReadlinesError::LimitOverflow); + } + let enc: *const Encoding = self.encoding as *const Encoding; + let line = if enc == UTF_8 { + str::from_utf8(&bytes.split_to(ind + 1)) + .map_err(|_| ReadlinesError::EncodingError)? + .to_owned() + } else { + self.encoding + .decode(&bytes.split_to(ind + 1), DecoderTrap::Strict) + .map_err(|_| ReadlinesError::EncodingError)? + }; + // extend buffer with rest of the bytes; + self.buff.extend_from_slice(&bytes); + self.checked_buff = false; + return Ok(Async::Ready(Some(line))); + } + self.buff.extend_from_slice(&bytes); + Ok(Async::NotReady) + } + Ok(Async::NotReady) => Ok(Async::NotReady), + Ok(Async::Ready(None)) => { + if self.buff.is_empty() { + return Ok(Async::Ready(None)); + } + if self.buff.len() > self.limit { + return Err(ReadlinesError::LimitOverflow); + } + let enc: *const Encoding = self.encoding as *const Encoding; + let line = if enc == UTF_8 { + str::from_utf8(&self.buff) + .map_err(|_| ReadlinesError::EncodingError)? + .to_owned() + } else { + self.encoding + .decode(&self.buff, DecoderTrap::Strict) + .map_err(|_| ReadlinesError::EncodingError)? + }; + self.buff.clear(); + Ok(Async::Ready(Some(line))) + } + Err(e) => Err(ReadlinesError::from(e)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test::{block_on, TestRequest}; + + #[test] + fn test_readlines() { + let mut req = TestRequest::default() + .set_payload(Bytes::from_static( + b"Lorem Ipsum is simply dummy text of the printing and typesetting\n\ + industry. Lorem Ipsum has been the industry's standard dummy\n\ + Contrary to popular belief, Lorem Ipsum is not simply random text.", + )) + .to_request(); + let stream = match block_on(Readlines::new(&mut req).into_future()) { + Ok((Some(s), stream)) => { + assert_eq!( + s, + "Lorem Ipsum is simply dummy text of the printing and typesetting\n" + ); + stream + } + _ => unreachable!("error"), + }; + + let stream = match block_on(stream.into_future()) { + Ok((Some(s), stream)) => { + assert_eq!( + s, + "industry. Lorem Ipsum has been the industry's standard dummy\n" + ); + stream + } + _ => unreachable!("error"), + }; + + match block_on(stream.into_future()) { + Ok((Some(s), stream)) => { + assert_eq!( + s, + "Contrary to popular belief, Lorem Ipsum is not simply random text." + ); + } + _ => unreachable!("error"), + } + } +} From b550f9ecf449a71274f118f1b91e8c9a121e8986 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 01:08:56 -0700 Subject: [PATCH 097/109] update imports --- src/lib.rs | 2 +- src/responder.rs | 2 +- src/types/form.rs | 3 ++- src/types/json.rs | 2 +- src/types/readlines.rs | 2 +- tests/test_server.rs | 24 ++++++++++++------------ 6 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d6bcf4e3..dbf8f743 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,7 +64,7 @@ pub mod dev { pub use crate::types::readlines::Readlines; pub use actix_http::body::{Body, BodyLength, MessageBody, ResponseBody}; - pub use actix_http::dev::ResponseBuilder as HttpResponseBuilder; + pub use actix_http::ResponseBuilder as HttpResponseBuilder; pub use actix_http::{ Extensions, Payload, PayloadStream, RequestHead, ResponseHead, }; diff --git a/src/responder.rs b/src/responder.rs index ace360c6..5f98e6e8 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -1,5 +1,5 @@ use actix_http::error::InternalError; -use actix_http::{dev::ResponseBuilder, http::StatusCode, Error, Response}; +use actix_http::{http::StatusCode, Error, Response, ResponseBuilder}; use bytes::{Bytes, BytesMut}; use futures::future::{err, ok, Either as EitherFuture, FutureResult}; use futures::{Future, IntoFuture, Poll}; diff --git a/src/types/form.rs b/src/types/form.rs index 58fa3761..cd4d09bb 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -3,7 +3,7 @@ use std::rc::Rc; use std::{fmt, ops}; -use actix_http::error::{Error, PayloadError, UrlencodedError}; +use actix_http::error::{Error, PayloadError}; use actix_http::{HttpMessage, Payload}; use bytes::{Bytes, BytesMut}; use encoding::all::UTF_8; @@ -12,6 +12,7 @@ use encoding::EncodingRef; use futures::{Future, Poll, Stream}; use serde::de::DeserializeOwned; +use crate::error::UrlencodedError; use crate::extract::FromRequest; use crate::http::header::CONTENT_LENGTH; use crate::request::HttpRequest; diff --git a/src/types/json.rs b/src/types/json.rs index 18a6be90..4fc2748f 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -9,10 +9,10 @@ use serde::de::DeserializeOwned; use serde::Serialize; use serde_json; -use actix_http::error::{Error, JsonPayloadError, PayloadError}; use actix_http::http::{header::CONTENT_LENGTH, StatusCode}; use actix_http::{HttpMessage, Payload, Response}; +use crate::error::{Error, JsonPayloadError, PayloadError}; use crate::extract::FromRequest; use crate::request::HttpRequest; use crate::responder::Responder; diff --git a/src/types/readlines.rs b/src/types/readlines.rs index 2c7f699a..c23b8443 100644 --- a/src/types/readlines.rs +++ b/src/types/readlines.rs @@ -198,7 +198,7 @@ mod tests { }; match block_on(stream.into_future()) { - Ok((Some(s), stream)) => { + Ok((Some(s), _)) => { assert_eq!( s, "Contrary to popular belief, Lorem Ipsum is not simply random text." diff --git a/tests/test_server.rs b/tests/test_server.rs index ffdc473a..965d444f 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -3,7 +3,7 @@ use std::io::{Read, Write}; use actix_http::http::header::{ ContentEncoding, ACCEPT_ENCODING, CONTENT_LENGTH, TRANSFER_ENCODING, }; -use actix_http::{h1, Error, HttpMessage, Response}; +use actix_http::{h1, Error, Response}; use actix_http_test::TestServer; use brotli2::write::BrotliDecoder; use bytes::Bytes; @@ -12,7 +12,7 @@ use flate2::write::ZlibDecoder; use futures::stream::once; //Future, Stream use rand::{distributions::Alphanumeric, Rng}; -use actix_web::{middleware, web, App}; +use actix_web::{dev::HttpMessageBody, middleware, web, App}; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ @@ -50,7 +50,7 @@ fn test_body() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } @@ -69,7 +69,7 @@ fn test_body_gzip() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); // decode let mut e = GzDecoder::new(&bytes[..]); @@ -100,7 +100,7 @@ fn test_body_gzip_large() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); // decode let mut e = GzDecoder::new(&bytes[..]); @@ -134,7 +134,7 @@ fn test_body_gzip_large_random() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); // decode let mut e = GzDecoder::new(&bytes[..]); @@ -167,7 +167,7 @@ fn test_body_chunked_implicit() { ); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); // decode let mut e = GzDecoder::new(&bytes[..]); @@ -195,7 +195,7 @@ fn test_body_br_streaming() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); // decode br let mut e = BrotliDecoder::new(Vec::with_capacity(2048)); @@ -222,7 +222,7 @@ fn test_head_binary() { } // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); assert!(bytes.is_empty()); } @@ -245,7 +245,7 @@ fn test_no_chunking() { assert!(!response.headers().contains_key(TRANSFER_ENCODING)); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } @@ -267,7 +267,7 @@ fn test_body_deflate() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); // decode deflate let mut e = ZlibDecoder::new(Vec::new()); @@ -294,7 +294,7 @@ fn test_body_brotli() { assert!(response.status().is_success()); // read response - let bytes = srv.block_on(response.body()).unwrap(); + let bytes = srv.block_on(HttpMessageBody::new(&mut response)).unwrap(); // decode brotli let mut e = BrotliDecoder::new(Vec::with_capacity(2048)); From 7435c5e9bf8dc1db407a8a1659a9bea2934dc427 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 01:49:00 -0700 Subject: [PATCH 098/109] temp fix for tarpaulin --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 55a03ec8..32e6c136 100644 --- a/.travis.yml +++ b/.travis.yml @@ -49,7 +49,7 @@ after_success: fi - | if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then - cargo tarpaulin --out Xml --all + taskset -c 0 cargo tarpaulin --out Xml --all bash <(curl -s https://codecov.io/bash) echo "Uploaded code coverage" fi From c14c66d2b00427482f7fd3b3e54af80a257a2641 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 09:52:41 -0700 Subject: [PATCH 099/109] add json extractor tests --- .travis.yml | 2 +- src/error.rs | 4 +-- src/responder.rs | 4 +-- src/test.rs | 24 +++++++++++--- src/types/json.rs | 83 +++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 105 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 32e6c136..9caaac1b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ matrix: - rust: beta - rust: nightly-2019-03-02 allow_failures: - - rust: nightly + - rust: nightly-2019-03-02 env: global: diff --git a/src/error.rs b/src/error.rs index bf224a22..2231473f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -87,8 +87,8 @@ impl ResponseError for UrlencodedError { /// A set of errors that can occur during parsing json payloads #[derive(Debug, Display, From)] pub enum JsonPayloadError { - /// Payload size is bigger than allowed. (default: 256kB) - #[display(fmt = "Json payload size is bigger than allowed. (default: 256kB)")] + /// Payload size is bigger than allowed. (default: 32kB) + #[display(fmt = "Json payload size is bigger than allowed.")] Overflow, /// Content type error #[display(fmt = "Content type error")] diff --git a/src/responder.rs b/src/responder.rs index 5f98e6e8..871670bd 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -288,7 +288,7 @@ where } #[cfg(test)] -mod tests { +pub(crate) mod tests { use actix_service::Service; use bytes::{Bytes, BytesMut}; @@ -322,7 +322,7 @@ mod tests { } } - trait BodyTest { + pub(crate) trait BodyTest { fn bin_ref(&self) -> &[u8]; fn body(&self) -> &Body; } diff --git a/src/test.rs b/src/test.rs index 13db5977..fe9fb024 100644 --- a/src/test.rs +++ b/src/test.rs @@ -5,7 +5,7 @@ use std::rc::Rc; use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; use actix_http::http::{HttpTryFrom, Method, Version}; use actix_http::test::TestRequest as HttpTestRequest; -use actix_http::{PayloadStream, Request}; +use actix_http::{Extensions, PayloadStream, Request}; use actix_router::{Path, ResourceDef, Url}; use actix_rt::Runtime; use actix_server_config::ServerConfig; @@ -15,6 +15,7 @@ use cookie::Cookie; use futures::future::{lazy, Future}; use crate::config::{AppConfig, AppConfigInner}; +use crate::data::RouteData; use crate::rmap::ResourceMap; use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; use crate::{HttpRequest, HttpResponse}; @@ -157,6 +158,7 @@ pub struct TestRequest { req: HttpTestRequest, rmap: ResourceMap, config: AppConfigInner, + route_data: Extensions, } impl Default for TestRequest { @@ -165,6 +167,7 @@ impl Default for TestRequest { req: HttpTestRequest::default(), rmap: ResourceMap::new(ResourceDef::new("")), config: AppConfigInner::default(), + route_data: Extensions::new(), } } } @@ -177,6 +180,7 @@ impl TestRequest { req: HttpTestRequest::default().uri(path).take(), rmap: ResourceMap::new(ResourceDef::new("")), config: AppConfigInner::default(), + route_data: Extensions::new(), } } @@ -186,6 +190,7 @@ impl TestRequest { req: HttpTestRequest::default().set(hdr).take(), config: AppConfigInner::default(), rmap: ResourceMap::new(ResourceDef::new("")), + route_data: Extensions::new(), } } @@ -199,6 +204,7 @@ impl TestRequest { req: HttpTestRequest::default().header(key, value).take(), config: AppConfigInner::default(), rmap: ResourceMap::new(ResourceDef::new("")), + route_data: Extensions::new(), } } @@ -208,6 +214,7 @@ impl TestRequest { req: HttpTestRequest::default().method(Method::GET).take(), config: AppConfigInner::default(), rmap: ResourceMap::new(ResourceDef::new("")), + route_data: Extensions::new(), } } @@ -217,6 +224,7 @@ impl TestRequest { req: HttpTestRequest::default().method(Method::POST).take(), config: AppConfigInner::default(), rmap: ResourceMap::new(ResourceDef::new("")), + route_data: Extensions::new(), } } @@ -266,12 +274,20 @@ impl TestRequest { self } - /// Set route data - pub fn route_data(self, data: T) -> Self { + /// 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 } + /// Set route data. This is equivalent of `Route::data()` method + /// for testing purpose. + pub fn route_data(mut self, data: T) -> Self { + self.route_data.insert(RouteData::new(data)); + self + } + #[cfg(test)] /// Set request config pub(crate) fn rmap(mut self, rmap: ResourceMap) -> Self { @@ -324,7 +340,7 @@ impl TestRequest { Rc::new(self.rmap), AppConfig::new(self.config), ); - ServiceFromRequest::new(req, None) + ServiceFromRequest::new(req, Some(Rc::new(self.route_data))) } /// Runs the provided future, blocking the current thread until the future diff --git a/src/types/json.rs b/src/types/json.rs index 4fc2748f..9e13d994 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -371,6 +371,11 @@ mod tests { use crate::http::header; use crate::test::{block_on, TestRequest}; + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct MyObject { + name: String, + } + fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { match err { JsonPayloadError::Overflow => match other { @@ -385,9 +390,81 @@ mod tests { } } - #[derive(Serialize, Deserialize, PartialEq, Debug)] - struct MyObject { - name: String, + #[test] + fn test_responder() { + let req = TestRequest::default().to_http_request(); + + let j = Json(MyObject { + name: "test".to_string(), + }); + let resp = j.respond_to(&req).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + header::HeaderValue::from_static("application/json") + ); + + use crate::responder::tests::BodyTest; + assert_eq!(resp.body().bin_ref(), b"{\"name\":\"test\"}"); + } + + #[test] + fn test_extract() { + let mut req = 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_from(); + + let s = block_on(Json::::from_request(&mut req)).unwrap(); + assert_eq!(s.name, "test"); + assert_eq!( + s.into_inner(), + MyObject { + name: "test".to_string() + } + ); + + let mut req = 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\"}")) + .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.")); + + let mut req = 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\"}")) + .route_data( + JsonConfig::default() + .limit(10) + .error_handler(|_, _| JsonPayloadError::ContentType.into()), + ) + .to_from(); + let s = block_on(Json::::from_request(&mut req)); + assert!(format!("{}", s.err().unwrap()).contains("Content type error")); } #[test] From 9bd0f29ca3396924a607e8a1c5312f79ae157cab Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 10:11:10 -0700 Subject: [PATCH 100/109] add tests for error and some responders --- src/error.rs | 31 +++++++++++++++++++++++++++++++ src/responder.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/error.rs b/src/error.rs index 2231473f..fc0f9fdf 100644 --- a/src/error.rs +++ b/src/error.rs @@ -142,3 +142,34 @@ impl ResponseError for ReadlinesError { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_urlencoded_error() { + let resp: HttpResponse = UrlencodedError::Overflow.error_response(); + assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); + let resp: HttpResponse = UrlencodedError::UnknownLength.error_response(); + assert_eq!(resp.status(), StatusCode::LENGTH_REQUIRED); + let resp: HttpResponse = UrlencodedError::ContentType.error_response(); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } + + #[test] + fn test_json_payload_error() { + let resp: HttpResponse = JsonPayloadError::Overflow.error_response(); + assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); + let resp: HttpResponse = JsonPayloadError::ContentType.error_response(); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } + + #[test] + fn test_readlines_error() { + let resp: HttpResponse = ReadlinesError::LimitOverflow.error_response(); + assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); + let resp: HttpResponse = ReadlinesError::EncodingError.error_response(); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } +} diff --git a/src/responder.rs b/src/responder.rs index 871670bd..50467883 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -380,6 +380,15 @@ pub(crate) mod tests { HeaderValue::from_static("text/plain; charset=utf-8") ); + let resp: HttpResponse = + block_on((&"test".to_string()).respond_to(&req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(resp.body().bin_ref(), b"test"); + assert_eq!( + resp.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("text/plain; charset=utf-8") + ); + let resp: HttpResponse = block_on(Bytes::from_static(b"test").respond_to(&req)).unwrap(); assert_eq!(resp.status(), StatusCode::OK); @@ -398,10 +407,32 @@ pub(crate) mod tests { HeaderValue::from_static("application/octet-stream") ); + // InternalError let resp: HttpResponse = error::InternalError::new("err", StatusCode::BAD_REQUEST) .respond_to(&req) .unwrap(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } + + #[test] + fn test_result_responder() { + let req = TestRequest::default().to_http_request(); + + // Result + let resp: HttpResponse = + block_on(Ok::<_, Error>("test".to_string()).respond_to(&req)).unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(resp.body().bin_ref(), b"test"); + assert_eq!( + resp.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("text/plain; charset=utf-8") + ); + + let res = block_on( + Err::(error::InternalError::new("err", StatusCode::BAD_REQUEST)) + .respond_to(&req), + ); + assert!(res.is_err()); + } } From 6b66681827aa1f42041fef9c55bd13bc0d80990c Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 13:47:20 -0700 Subject: [PATCH 101/109] add basic actors integration --- Cargo.toml | 1 + actix-web-actors/Cargo.toml | 28 ++++ actix-web-actors/src/context.rs | 254 ++++++++++++++++++++++++++++++++ actix-web-actors/src/lib.rs | 4 + 4 files changed, 287 insertions(+) create mode 100644 actix-web-actors/Cargo.toml create mode 100644 actix-web-actors/src/context.rs create mode 100644 actix-web-actors/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index ba36dd2c..df06f3a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ members = [ ".", "actix-files", "actix-session", + "actix-web-actors", "actix-web-codegen", ] diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml new file mode 100644 index 00000000..698acc1c --- /dev/null +++ b/actix-web-actors/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "actix-web-actors" +version = "0.1.0" +authors = ["Nikolay Kim "] +description = "Actix actors 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-web-actors/" +license = "MIT/Apache-2.0" +exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] +workspace = ".." +edition = "2018" + +[lib] +name = "actix_web_actors" +path = "src/lib.rs" + +[dependencies] +actix-web = { path=".." } +actix = { git = "https://github.com/actix/actix.git" } + +bytes = "0.4" +futures = "0.1" + +[dev-dependencies] +actix-rt = "0.2.0" diff --git a/actix-web-actors/src/context.rs b/actix-web-actors/src/context.rs new file mode 100644 index 00000000..646698ef --- /dev/null +++ b/actix-web-actors/src/context.rs @@ -0,0 +1,254 @@ +use std::collections::VecDeque; + +use actix::dev::{ + AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, ToEnvelope, +}; +use actix::fut::ActorFuture; +use actix::{ + Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message, SpawnHandle, +}; +use actix_web::error::{Error, ErrorInternalServerError}; +use bytes::Bytes; +use futures::sync::oneshot::Sender; +use futures::{Async, Future, Poll, Stream}; + +/// Execution context for http actors +pub struct HttpContext +where + A: Actor>, +{ + inner: ContextParts, + stream: VecDeque>, +} + +impl ActorContext for HttpContext +where + A: Actor, +{ + fn stop(&mut self) { + self.inner.stop(); + } + fn terminate(&mut self) { + self.inner.terminate() + } + fn state(&self) -> ActorState { + self.inner.state() + } +} + +impl AsyncContext for HttpContext +where + A: Actor, +{ + #[inline] + fn spawn(&mut self, fut: F) -> SpawnHandle + where + F: ActorFuture + 'static, + { + self.inner.spawn(fut) + } + + #[inline] + fn wait(&mut self, fut: F) + where + F: ActorFuture + 'static, + { + self.inner.wait(fut) + } + + #[doc(hidden)] + #[inline] + fn waiting(&self) -> bool { + self.inner.waiting() + || self.inner.state() == ActorState::Stopping + || self.inner.state() == ActorState::Stopped + } + + #[inline] + fn cancel_future(&mut self, handle: SpawnHandle) -> bool { + self.inner.cancel_future(handle) + } + + #[inline] + fn address(&self) -> Addr { + self.inner.address() + } +} + +impl HttpContext +where + A: Actor, +{ + #[inline] + /// Create a new HTTP Context from a request and an actor + pub fn create(actor: A) -> impl Stream { + let mb = Mailbox::default(); + let ctx = HttpContext { + inner: ContextParts::new(mb.sender_producer()), + stream: VecDeque::new(), + }; + HttpContextFut::new(ctx, actor, mb) + } + + /// Create a new HTTP Context + pub fn with_factory(f: F) -> impl Stream + where + F: FnOnce(&mut Self) -> A + 'static, + { + let mb = Mailbox::default(); + let mut ctx = HttpContext { + inner: ContextParts::new(mb.sender_producer()), + stream: VecDeque::new(), + }; + + let act = f(&mut ctx); + HttpContextFut::new(ctx, act, mb) + } +} + +impl HttpContext +where + A: Actor, +{ + /// Write payload + #[inline] + pub fn write(&mut self, data: Bytes) { + self.stream.push_back(Some(data)); + } + + /// Indicate end of streaming payload. Also this method calls `Self::close`. + #[inline] + pub fn write_eof(&mut self) { + self.stream.push_back(None); + } + + /// Handle of the running future + /// + /// SpawnHandle is the handle returned by `AsyncContext::spawn()` method. + pub fn handle(&self) -> SpawnHandle { + self.inner.curr_handle() + } +} + +impl AsyncContextParts for HttpContext +where + A: Actor, +{ + fn parts(&mut self) -> &mut ContextParts { + &mut self.inner + } +} + +struct HttpContextFut +where + A: Actor>, +{ + fut: ContextFut>, +} + +impl HttpContextFut +where + A: Actor>, +{ + fn new(ctx: HttpContext, act: A, mailbox: Mailbox) -> Self { + let fut = ContextFut::new(ctx, act, mailbox); + HttpContextFut { fut } + } +} + +impl Stream for HttpContextFut +where + A: Actor>, +{ + type Item = Bytes; + type Error = Error; + + fn poll(&mut self) -> Poll, Error> { + if self.fut.alive() { + match self.fut.poll() { + Ok(Async::NotReady) | Ok(Async::Ready(())) => (), + Err(_) => return Err(ErrorInternalServerError("error")), + } + } + + // frames + if let Some(data) = self.fut.ctx().stream.pop_front() { + Ok(Async::Ready(data)) + } else if self.fut.alive() { + Ok(Async::NotReady) + } else { + Ok(Async::Ready(None)) + } + } +} + +impl ToEnvelope for HttpContext +where + A: Actor> + Handler, + M: Message + Send + 'static, + M::Result: Send, +{ + fn pack(msg: M, tx: Option>) -> Envelope { + Envelope::new(msg, tx) + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use actix::Actor; + use actix_web::dev::HttpMessageBody; + use actix_web::http::StatusCode; + use actix_web::test::{block_on, call_success, init_service, TestRequest}; + use actix_web::{web, App, HttpRequest, HttpResponse}; + use bytes::{Bytes, BytesMut}; + + use super::*; + + struct MyActor { + count: usize, + } + + impl Actor for MyActor { + type Context = HttpContext; + + fn started(&mut self, ctx: &mut Self::Context) { + ctx.run_later(Duration::from_millis(100), |slf, ctx| slf.write(ctx)); + } + } + + impl MyActor { + fn write(&mut self, ctx: &mut HttpContext) { + self.count += 1; + if self.count > 3 { + ctx.write_eof() + } else { + ctx.write(Bytes::from(format!("LINE-{}", self.count).as_bytes())); + ctx.run_later(Duration::from_millis(100), |slf, ctx| slf.write(ctx)); + } + } + } + + #[test] + fn test_default_resource() { + let mut srv = + init_service(App::new().service(web::resource("/test").to(|| { + HttpResponse::Ok().streaming(HttpContext::create(MyActor { count: 0 })) + }))); + + let req = TestRequest::with_uri("/test").to_request(); + let mut resp = call_success(&mut srv, req); + assert_eq!(resp.status(), StatusCode::OK); + + let body = block_on(resp.take_body().fold( + BytesMut::new(), + move |mut body, chunk| { + body.extend_from_slice(&chunk); + Ok::<_, Error>(body) + }, + )) + .unwrap(); + assert_eq!(body.freeze(), Bytes::from_static(b"LINE-1LINE-2LINE-3")); + } +} diff --git a/actix-web-actors/src/lib.rs b/actix-web-actors/src/lib.rs new file mode 100644 index 00000000..47adf5f0 --- /dev/null +++ b/actix-web-actors/src/lib.rs @@ -0,0 +1,4 @@ +//! Actix actors integration for Actix web framework +mod context; + +pub use self::context::HttpContext; From a07ea00cc4811466a09b8afda7b88e4bc115e663 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 13:55:03 -0700 Subject: [PATCH 102/109] add basic test for proc macro --- tests/test_macro.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/test_macro.rs diff --git a/tests/test_macro.rs b/tests/test_macro.rs new file mode 100644 index 00000000..62b5d618 --- /dev/null +++ b/tests/test_macro.rs @@ -0,0 +1,17 @@ +use actix_http::HttpService; +use actix_http_test::TestServer; +use actix_web::{get, App, HttpResponse, Responder}; + +#[get("/test")] +fn test() -> impl Responder { + HttpResponse::Ok() +} + +#[test] +fn test_body() { + let mut srv = TestServer::new(|| HttpService::new(App::new().service(test))); + + let request = srv.get().uri(srv.url("/test")).finish().unwrap(); + let response = srv.send_request(request).unwrap(); + assert!(response.status().is_success()); +} From 88152740c690d42b21f9bb5c90426661e4455885 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 20:20:10 -0700 Subject: [PATCH 103/109] move macros tests to codegen crate --- actix-web-codegen/Cargo.toml | 5 +++++ {tests => actix-web-codegen/tests}/test_macro.rs | 0 2 files changed, 5 insertions(+) rename {tests => actix-web-codegen/tests}/test_macro.rs (100%) diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 24ed36b7..d87b71ba 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -13,3 +13,8 @@ proc-macro = true [dependencies] quote = "0.6" syn = { version = "0.15", features = ["full", "parsing"] } + +[dev-dependencies] +actix-web = { path = ".." } +actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } +actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } diff --git a/tests/test_macro.rs b/actix-web-codegen/tests/test_macro.rs similarity index 100% rename from tests/test_macro.rs rename to actix-web-codegen/tests/test_macro.rs From fd3e351c318c31e98dbd0354afb0cba654b0cdbf Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 22:02:03 -0700 Subject: [PATCH 104/109] add websockets context --- Cargo.toml | 4 +- actix-web-actors/Cargo.toml | 7 +- actix-web-actors/src/context.rs | 3 +- actix-web-actors/src/lib.rs | 6 + actix-web-actors/src/ws.rs | 408 ++++++++++++++++++++++++++++++ actix-web-actors/tests/test_ws.rs | 67 +++++ src/lib.rs | 2 +- 7 files changed, 490 insertions(+), 7 deletions(-) create mode 100644 actix-web-actors/src/ws.rs create mode 100644 actix-web-actors/tests/test_ws.rs diff --git a/Cargo.toml b/Cargo.toml index df06f3a1..d98c926b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,14 +61,14 @@ ssl = ["openssl", "actix-server/ssl"] # rust-tls = ["rustls", "actix-server/rustls"] [dependencies] -actix-codec = "0.1.0" +actix-codec = "0.1.1" actix-service = "0.3.4" actix-utils = "0.3.4" actix-router = "0.1.0" actix-rt = "0.2.1" actix-web-codegen = { path="actix-web-codegen" } actix-http = { git = "https://github.com/actix/actix-http.git" } -actix-server = "0.4.0" +actix-server = "0.4.1" actix-server-config = "0.1.0" bytes = "0.4" diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 698acc1c..db42a1a2 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -20,9 +20,12 @@ path = "src/lib.rs" [dependencies] actix-web = { path=".." } actix = { git = "https://github.com/actix/actix.git" } - +actix-http = { git = "https://github.com/actix/actix-http.git" } +actix-codec = "0.1.1" bytes = "0.4" futures = "0.1" [dev-dependencies] -actix-rt = "0.2.0" +env_logger = "0.6" +actix-http = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } +actix-http-test = { git = "https://github.com/actix/actix-http.git", features=["ssl"] } diff --git a/actix-web-actors/src/context.rs b/actix-web-actors/src/context.rs index 646698ef..da473ff3 100644 --- a/actix-web-actors/src/context.rs +++ b/actix-web-actors/src/context.rs @@ -198,10 +198,9 @@ mod tests { use std::time::Duration; use actix::Actor; - use actix_web::dev::HttpMessageBody; use actix_web::http::StatusCode; use actix_web::test::{block_on, call_success, init_service, TestRequest}; - use actix_web::{web, App, HttpRequest, HttpResponse}; + use actix_web::{web, App, HttpResponse}; use bytes::{Bytes, BytesMut}; use super::*; diff --git a/actix-web-actors/src/lib.rs b/actix-web-actors/src/lib.rs index 47adf5f0..0d447865 100644 --- a/actix-web-actors/src/lib.rs +++ b/actix-web-actors/src/lib.rs @@ -1,4 +1,10 @@ //! Actix actors integration for Actix web framework mod context; +mod ws; pub use self::context::HttpContext; +pub use self::ws::{ws_handshake, ws_start, WebsocketContext}; + +pub use actix_http::ws::CloseCode as WsCloseCode; +pub use actix_http::ws::ProtocolError as WsProtocolError; +pub use actix_http::ws::{Frame as WsFrame, Message as WsMessage}; diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs new file mode 100644 index 00000000..dca0f46d --- /dev/null +++ b/actix-web-actors/src/ws.rs @@ -0,0 +1,408 @@ +use std::collections::VecDeque; +use std::io; + +use actix::dev::{ + AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, StreamHandler, + ToEnvelope, +}; +use actix::fut::ActorFuture; +use actix::{ + Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, + Message as ActixMessage, SpawnHandle, +}; +use actix_codec::{Decoder, Encoder}; +use actix_http::ws::{ + hash_key, CloseReason, Codec, Frame, HandshakeError, Message, ProtocolError, +}; +use actix_web::dev::{Head, HttpResponseBuilder}; +use actix_web::error::{Error, ErrorInternalServerError, PayloadError}; +use actix_web::http::{header, Method, StatusCode}; +use actix_web::{HttpMessage, HttpRequest, HttpResponse}; +use bytes::{Bytes, BytesMut}; +use futures::sync::oneshot::Sender; +use futures::{Async, Future, Poll, Stream}; + +/// Do websocket handshake and start ws actor. +pub fn ws_start( + actor: A, + req: &HttpRequest, + stream: T, +) -> Result +where + A: Actor> + StreamHandler, + T: Stream + 'static, +{ + let mut res = ws_handshake(req)?; + Ok(res.streaming(WebsocketContext::create(actor, stream))) +} + +/// Prepare `WebSocket` handshake response. +/// +/// This function returns handshake `HttpResponse`, ready to send to peer. +/// It does not perform any IO. +/// +// /// `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 ws_handshake(req: &HttpRequest) -> Result { + // WebSocket accepts only GET + if *req.method() != Method::GET { + return Err(HandshakeError::GetMethodRequired); + } + + // Check for "UPGRADE" to websocket header + let has_hdr = if let Some(hdr) = req.headers().get(header::UPGRADE) { + if let Ok(s) = hdr.to_str() { + s.to_lowercase().contains("websocket") + } else { + false + } + } else { + false + }; + if !has_hdr { + return Err(HandshakeError::NoWebsocketUpgrade); + } + + // Upgrade connection + if !req.upgrade() { + return Err(HandshakeError::NoConnectionUpgrade); + } + + // check supported version + if !req.headers().contains_key(header::SEC_WEBSOCKET_VERSION) { + return Err(HandshakeError::NoVersionHeader); + } + let supported_ver = { + if let Some(hdr) = req.headers().get(header::SEC_WEBSOCKET_VERSION) { + hdr == "13" || hdr == "8" || hdr == "7" + } else { + false + } + }; + if !supported_ver { + return Err(HandshakeError::UnsupportedVersion); + } + + // check client handshake for validity + if !req.headers().contains_key(header::SEC_WEBSOCKET_KEY) { + return Err(HandshakeError::BadWebsocketKey); + } + let key = { + let key = req.headers().get(header::SEC_WEBSOCKET_KEY).unwrap(); + hash_key(key.as_ref()) + }; + + Ok(HttpResponse::build(StatusCode::SWITCHING_PROTOCOLS) + .header(header::CONNECTION, "upgrade") + .header(header::UPGRADE, "websocket") + .header(header::TRANSFER_ENCODING, "chunked") + .header(header::SEC_WEBSOCKET_ACCEPT, key.as_str()) + .take()) +} + +/// Execution context for `WebSockets` actors +pub struct WebsocketContext +where + A: Actor>, +{ + inner: ContextParts, + messages: VecDeque>, +} + +impl ActorContext for WebsocketContext +where + A: Actor, +{ + fn stop(&mut self) { + self.inner.stop(); + } + + fn terminate(&mut self) { + self.inner.terminate() + } + + fn state(&self) -> ActorState { + self.inner.state() + } +} + +impl AsyncContext for WebsocketContext +where + A: Actor, +{ + fn spawn(&mut self, fut: F) -> SpawnHandle + where + F: ActorFuture + 'static, + { + self.inner.spawn(fut) + } + + fn wait(&mut self, fut: F) + where + F: ActorFuture + 'static, + { + self.inner.wait(fut) + } + + #[doc(hidden)] + #[inline] + fn waiting(&self) -> bool { + self.inner.waiting() + || self.inner.state() == ActorState::Stopping + || self.inner.state() == ActorState::Stopped + } + + fn cancel_future(&mut self, handle: SpawnHandle) -> bool { + self.inner.cancel_future(handle) + } + + #[inline] + fn address(&self) -> Addr { + self.inner.address() + } +} + +impl WebsocketContext +where + A: Actor, +{ + #[inline] + /// Create a new Websocket context from a request and an actor + pub fn create(actor: A, stream: S) -> impl Stream + where + A: StreamHandler, + S: Stream + 'static, + { + let mb = Mailbox::default(); + let mut ctx = WebsocketContext { + inner: ContextParts::new(mb.sender_producer()), + messages: VecDeque::new(), + }; + ctx.add_stream(WsStream::new(stream)); + + WebsocketContextFut::new(ctx, actor, mb) + } + + /// Create a new Websocket context + pub fn with_factory( + stream: S, + f: F, + ) -> impl Stream + where + F: FnOnce(&mut Self) -> A + 'static, + A: StreamHandler, + S: Stream + 'static, + { + let mb = Mailbox::default(); + let mut ctx = WebsocketContext { + inner: ContextParts::new(mb.sender_producer()), + messages: VecDeque::new(), + }; + ctx.add_stream(WsStream::new(stream)); + + let act = f(&mut ctx); + + WebsocketContextFut::new(ctx, act, mb) + } +} + +impl WebsocketContext +where + A: Actor, +{ + /// Write payload + /// + /// This is a low-level function that accepts framed messages that should + /// be created using `Frame::message()`. If you want to send text or binary + /// data you should prefer the `text()` or `binary()` convenience functions + /// that handle the framing for you. + #[inline] + pub fn write_raw(&mut self, msg: Message) { + self.messages.push_back(Some(msg)); + } + + /// Send text frame + #[inline] + pub fn text>(&mut self, text: T) { + self.write_raw(Message::Text(text.into())); + } + + /// Send binary frame + #[inline] + pub fn binary>(&mut self, data: B) { + self.write_raw(Message::Binary(data.into())); + } + + /// Send ping frame + #[inline] + pub fn ping(&mut self, message: &str) { + self.write_raw(Message::Ping(message.to_string())); + } + + /// Send pong frame + #[inline] + pub fn pong(&mut self, message: &str) { + self.write_raw(Message::Pong(message.to_string())); + } + + /// Send close frame + #[inline] + pub fn close(&mut self, reason: Option) { + self.write_raw(Message::Close(reason)); + } + + /// Handle of the running future + /// + /// SpawnHandle is the handle returned by `AsyncContext::spawn()` method. + pub fn handle(&self) -> SpawnHandle { + self.inner.curr_handle() + } + + /// Set mailbox capacity + /// + /// By default mailbox capacity is 16 messages. + pub fn set_mailbox_capacity(&mut self, cap: usize) { + self.inner.set_mailbox_capacity(cap) + } +} + +impl AsyncContextParts for WebsocketContext +where + A: Actor, +{ + fn parts(&mut self) -> &mut ContextParts { + &mut self.inner + } +} + +struct WebsocketContextFut +where + A: Actor>, +{ + fut: ContextFut>, + encoder: Codec, + buf: BytesMut, + closed: bool, +} + +impl WebsocketContextFut +where + A: Actor>, +{ + fn new(ctx: WebsocketContext, act: A, mailbox: Mailbox) -> Self { + let fut = ContextFut::new(ctx, act, mailbox); + WebsocketContextFut { + fut, + encoder: Codec::new(), + buf: BytesMut::new(), + closed: false, + } + } +} + +impl Stream for WebsocketContextFut +where + A: Actor>, +{ + type Item = Bytes; + type Error = Error; + + fn poll(&mut self) -> Poll, Error> { + if self.fut.alive() && self.fut.poll().is_err() { + return Err(ErrorInternalServerError("error")); + } + + // encode messages + while let Some(item) = self.fut.ctx().messages.pop_front() { + if let Some(msg) = item { + self.encoder.encode(msg, &mut self.buf)?; + } else { + self.closed = true; + break; + } + } + + if !self.buf.is_empty() { + Ok(Async::Ready(Some(self.buf.take().freeze()))) + } else if self.fut.alive() && !self.closed { + Ok(Async::NotReady) + } else { + Ok(Async::Ready(None)) + } + } +} + +impl ToEnvelope for WebsocketContext +where + A: Actor> + Handler, + M: ActixMessage + Send + 'static, + M::Result: Send, +{ + fn pack(msg: M, tx: Option>) -> Envelope { + Envelope::new(msg, tx) + } +} + +struct WsStream { + stream: S, + decoder: Codec, + buf: BytesMut, + closed: bool, +} + +impl WsStream +where + S: Stream, +{ + fn new(stream: S) -> Self { + Self { + stream, + decoder: Codec::new(), + buf: BytesMut::new(), + closed: false, + } + } +} + +impl Stream for WsStream +where + S: Stream, +{ + type Item = Frame; + type Error = ProtocolError; + + fn poll(&mut self) -> Poll, Self::Error> { + if !self.closed { + loop { + match self.stream.poll() { + Ok(Async::Ready(Some(chunk))) => { + self.buf.extend_from_slice(&chunk[..]); + } + Ok(Async::Ready(None)) => { + self.closed = true; + break; + } + Ok(Async::NotReady) => break, + Err(e) => { + return Err(ProtocolError::Io(io::Error::new( + io::ErrorKind::Other, + format!("{}", e), + ))); + } + } + } + } + + match self.decoder.decode(&mut self.buf)? { + None => { + if self.closed { + Ok(Async::Ready(None)) + } else { + Ok(Async::NotReady) + } + } + Some(frm) => Ok(Async::Ready(Some(frm))), + } + } +} diff --git a/actix-web-actors/tests/test_ws.rs b/actix-web-actors/tests/test_ws.rs new file mode 100644 index 00000000..0a214644 --- /dev/null +++ b/actix-web-actors/tests/test_ws.rs @@ -0,0 +1,67 @@ +use actix::prelude::*; +use actix_http::HttpService; +use actix_http_test::TestServer; +use actix_web::{web, App, HttpRequest}; +use actix_web_actors::*; +use bytes::{Bytes, BytesMut}; +use futures::{Sink, Stream}; + +struct Ws; + +impl Actor for Ws { + type Context = WebsocketContext; +} + +impl StreamHandler for Ws { + fn handle(&mut self, msg: WsFrame, ctx: &mut Self::Context) { + match msg { + WsFrame::Ping(msg) => ctx.pong(&msg), + WsFrame::Text(text) => { + ctx.text(String::from_utf8_lossy(&text.unwrap())).to_owned() + } + WsFrame::Binary(bin) => ctx.binary(bin.unwrap()), + WsFrame::Close(reason) => ctx.close(reason), + _ => (), + } + } +} + +#[test] +fn test_simple() { + let mut srv = + TestServer::new(|| { + HttpService::new(App::new().service(web::resource("/").to( + |req: HttpRequest, stream: web::Payload<_>| ws_start(Ws, &req, stream), + ))) + }); + + // client service + let framed = srv.ws().unwrap(); + let framed = srv + .block_on(framed.send(WsMessage::Text("text".to_string()))) + .unwrap(); + let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!(item, Some(WsFrame::Text(Some(BytesMut::from("text"))))); + + let framed = srv + .block_on(framed.send(WsMessage::Binary("text".into()))) + .unwrap(); + let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!( + item, + Some(WsFrame::Binary(Some(Bytes::from_static(b"text").into()))) + ); + + let framed = srv + .block_on(framed.send(WsMessage::Ping("text".into()))) + .unwrap(); + let (item, framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!(item, Some(WsFrame::Pong("text".to_string().into()))); + + let framed = srv + .block_on(framed.send(WsMessage::Close(Some(WsCloseCode::Normal.into())))) + .unwrap(); + + let (item, _framed) = srv.block_on(framed.into_future()).map_err(|_| ()).unwrap(); + assert_eq!(item, Some(WsFrame::Close(Some(WsCloseCode::Normal.into())))); +} diff --git a/src/lib.rs b/src/lib.rs index dbf8f743..f59cbc26 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,7 +66,7 @@ pub mod dev { pub use actix_http::body::{Body, BodyLength, MessageBody, ResponseBody}; pub use actix_http::ResponseBuilder as HttpResponseBuilder; pub use actix_http::{ - Extensions, Payload, PayloadStream, RequestHead, ResponseHead, + Extensions, Head, Payload, PayloadStream, RequestHead, ResponseHead, }; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; pub use actix_server::Server; From 6ab76658680b7a1b1f9c4ed01b68b1c00a3140bc Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 22:11:50 -0700 Subject: [PATCH 105/109] export ws module --- actix-web-actors/src/lib.rs | 7 +---- actix-web-actors/src/ws.rs | 17 ++++++------ actix-web-actors/tests/test_ws.rs | 44 ++++++++++++++++--------------- 3 files changed, 32 insertions(+), 36 deletions(-) diff --git a/actix-web-actors/src/lib.rs b/actix-web-actors/src/lib.rs index 0d447865..5b64d7e0 100644 --- a/actix-web-actors/src/lib.rs +++ b/actix-web-actors/src/lib.rs @@ -1,10 +1,5 @@ //! Actix actors integration for Actix web framework mod context; -mod ws; +pub mod ws; pub use self::context::HttpContext; -pub use self::ws::{ws_handshake, ws_start, WebsocketContext}; - -pub use actix_http::ws::CloseCode as WsCloseCode; -pub use actix_http::ws::ProtocolError as WsProtocolError; -pub use actix_http::ws::{Frame as WsFrame, Message as WsMessage}; diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index dca0f46d..b5f5c08c 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -1,3 +1,4 @@ +//! Websocket integration use std::collections::VecDeque; use std::io; @@ -11,9 +12,11 @@ use actix::{ Message as ActixMessage, SpawnHandle, }; use actix_codec::{Decoder, Encoder}; -use actix_http::ws::{ - hash_key, CloseReason, Codec, Frame, HandshakeError, Message, ProtocolError, +use actix_http::ws::hash_key; +pub use actix_http::ws::{ + CloseCode, CloseReason, Codec, Frame, HandshakeError, Message, ProtocolError, }; + use actix_web::dev::{Head, HttpResponseBuilder}; use actix_web::error::{Error, ErrorInternalServerError, PayloadError}; use actix_web::http::{header, Method, StatusCode}; @@ -23,16 +26,12 @@ use futures::sync::oneshot::Sender; use futures::{Async, Future, Poll, Stream}; /// Do websocket handshake and start ws actor. -pub fn ws_start( - actor: A, - req: &HttpRequest, - stream: T, -) -> Result +pub fn start(actor: A, req: &HttpRequest, stream: T) -> Result where A: Actor> + StreamHandler, T: Stream + 'static, { - let mut res = ws_handshake(req)?; + let mut res = handshake(req)?; Ok(res.streaming(WebsocketContext::create(actor, stream))) } @@ -44,7 +43,7 @@ where // /// `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 ws_handshake(req: &HttpRequest) -> Result { +pub fn handshake(req: &HttpRequest) -> Result { // WebSocket accepts only GET if *req.method() != Method::GET { return Err(HandshakeError::GetMethodRequired); diff --git a/actix-web-actors/tests/test_ws.rs b/actix-web-actors/tests/test_ws.rs index 0a214644..ea9c8d8f 100644 --- a/actix-web-actors/tests/test_ws.rs +++ b/actix-web-actors/tests/test_ws.rs @@ -9,18 +9,18 @@ use futures::{Sink, Stream}; struct Ws; impl Actor for Ws { - type Context = WebsocketContext; + type Context = ws::WebsocketContext; } -impl StreamHandler for Ws { - fn handle(&mut self, msg: WsFrame, ctx: &mut Self::Context) { +impl StreamHandler for Ws { + fn handle(&mut self, msg: ws::Frame, ctx: &mut Self::Context) { match msg { - WsFrame::Ping(msg) => ctx.pong(&msg), - WsFrame::Text(text) => { + ws::Frame::Ping(msg) => ctx.pong(&msg), + ws::Frame::Text(text) => { ctx.text(String::from_utf8_lossy(&text.unwrap())).to_owned() } - WsFrame::Binary(bin) => ctx.binary(bin.unwrap()), - WsFrame::Close(reason) => ctx.close(reason), + ws::Frame::Binary(bin) => ctx.binary(bin.unwrap()), + ws::Frame::Close(reason) => ctx.close(reason), _ => (), } } @@ -28,40 +28,42 @@ impl StreamHandler for Ws { #[test] fn test_simple() { - let mut srv = - TestServer::new(|| { - HttpService::new(App::new().service(web::resource("/").to( - |req: HttpRequest, stream: web::Payload<_>| ws_start(Ws, &req, stream), - ))) - }); + let mut srv = TestServer::new(|| { + HttpService::new(App::new().service(web::resource("/").to( + |req: HttpRequest, stream: web::Payload<_>| ws::start(Ws, &req, stream), + ))) + }); // client service let framed = srv.ws().unwrap(); let framed = srv - .block_on(framed.send(WsMessage::Text("text".to_string()))) + .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(WsFrame::Text(Some(BytesMut::from("text"))))); + assert_eq!(item, Some(ws::Frame::Text(Some(BytesMut::from("text"))))); let framed = srv - .block_on(framed.send(WsMessage::Binary("text".into()))) + .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(WsFrame::Binary(Some(Bytes::from_static(b"text").into()))) + Some(ws::Frame::Binary(Some(Bytes::from_static(b"text").into()))) ); let framed = srv - .block_on(framed.send(WsMessage::Ping("text".into()))) + .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(WsFrame::Pong("text".to_string().into()))); + assert_eq!(item, Some(ws::Frame::Pong("text".to_string().into()))); let framed = srv - .block_on(framed.send(WsMessage::Close(Some(WsCloseCode::Normal.into())))) + .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(WsFrame::Close(Some(WsCloseCode::Normal.into())))); + assert_eq!( + item, + Some(ws::Frame::Close(Some(ws::CloseCode::Normal.into()))) + ); } From b0343eb22d8a371fb84cdf304e4eb58f94dad700 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 22:31:10 -0700 Subject: [PATCH 106/109] simplify ws stream interface --- actix-web-actors/src/ws.rs | 35 ++++++++++++++++++++++++------- actix-web-actors/tests/test_ws.rs | 14 ++++++------- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index b5f5c08c..54632627 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -12,9 +12,9 @@ use actix::{ Message as ActixMessage, SpawnHandle, }; use actix_codec::{Decoder, Encoder}; -use actix_http::ws::hash_key; +use actix_http::ws::{hash_key, Codec}; pub use actix_http::ws::{ - CloseCode, CloseReason, Codec, Frame, HandshakeError, Message, ProtocolError, + CloseCode, CloseReason, Frame, HandshakeError, Message, ProtocolError, }; use actix_web::dev::{Head, HttpResponseBuilder}; @@ -28,7 +28,7 @@ use futures::{Async, Future, Poll, Stream}; /// Do websocket handshake and start ws actor. pub fn start(actor: A, req: &HttpRequest, stream: T) -> Result where - A: Actor> + StreamHandler, + A: Actor> + StreamHandler, T: Stream + 'static, { let mut res = handshake(req)?; @@ -170,7 +170,7 @@ where /// Create a new Websocket context from a request and an actor pub fn create(actor: A, stream: S) -> impl Stream where - A: StreamHandler, + A: StreamHandler, S: Stream + 'static, { let mb = Mailbox::default(); @@ -190,7 +190,7 @@ where ) -> impl Stream where F: FnOnce(&mut Self) -> A + 'static, - A: StreamHandler, + A: StreamHandler, S: Stream + 'static, { let mb = Mailbox::default(); @@ -368,7 +368,7 @@ impl Stream for WsStream where S: Stream, { - type Item = Frame; + type Item = Message; type Error = ProtocolError; fn poll(&mut self) -> Poll, Self::Error> { @@ -401,7 +401,28 @@ where Ok(Async::NotReady) } } - Some(frm) => Ok(Async::Ready(Some(frm))), + Some(frm) => { + let msg = match frm { + Frame::Text(data) => { + if let Some(data) = data { + Message::Text( + std::str::from_utf8(&data) + .map_err(|_| ProtocolError::BadEncoding)? + .to_string(), + ) + } else { + Message::Text(String::new()) + } + } + Frame::Binary(data) => Message::Binary( + data.map(|b| b.freeze()).unwrap_or_else(|| Bytes::new()), + ), + Frame::Ping(s) => Message::Ping(s), + Frame::Pong(s) => Message::Pong(s), + Frame::Close(reason) => Message::Close(reason), + }; + Ok(Async::Ready(Some(msg))) + } } } } diff --git a/actix-web-actors/tests/test_ws.rs b/actix-web-actors/tests/test_ws.rs index ea9c8d8f..202d562c 100644 --- a/actix-web-actors/tests/test_ws.rs +++ b/actix-web-actors/tests/test_ws.rs @@ -12,15 +12,13 @@ impl Actor for Ws { type Context = ws::WebsocketContext; } -impl StreamHandler for Ws { - fn handle(&mut self, msg: ws::Frame, ctx: &mut Self::Context) { +impl StreamHandler for Ws { + fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) { match msg { - ws::Frame::Ping(msg) => ctx.pong(&msg), - ws::Frame::Text(text) => { - ctx.text(String::from_utf8_lossy(&text.unwrap())).to_owned() - } - ws::Frame::Binary(bin) => ctx.binary(bin.unwrap()), - ws::Frame::Close(reason) => ctx.close(reason), + ws::Message::Ping(msg) => ctx.pong(&msg), + ws::Message::Text(text) => ctx.text(text), + ws::Message::Binary(bin) => ctx.binary(bin), + ws::Message::Close(reason) => ctx.close(reason), _ => (), } } From efe3025395ac2202a22639e3ac98f67617af0685 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 17 Mar 2019 22:57:27 -0700 Subject: [PATCH 107/109] add handshake test --- actix-web-actors/src/ws.rs | 122 ++++++++++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index 54632627..a5d26623 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -52,7 +52,7 @@ pub fn handshake(req: &HttpRequest) -> Result Date: Mon, 18 Mar 2019 05:30:18 -0700 Subject: [PATCH 108/109] fix ws upgrade --- actix-web-actors/src/ws.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index a5d26623..cef5080c 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -93,8 +93,7 @@ pub fn handshake(req: &HttpRequest) -> Result Date: Sat, 23 Mar 2019 10:06:54 -0700 Subject: [PATCH 109/109] make cookies optional --- Cargo.toml | 13 ++++++------- src/request.rs | 2 ++ src/test.rs | 2 ++ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d98c926b..6920bc09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,10 +34,10 @@ members = [ ] [package.metadata.docs.rs] -features = ["ssl", "tls", "rust-tls", "session"] +features = ["ssl", "tls", "rust-tls", "brotli", "flate2-c", "cookies"] [features] -default = ["brotli", "flate2-c", "session"] +default = ["brotli", "flate2-c", "cookies"] # brotli encoding, requires c compiler brotli = ["brotli2"] @@ -49,7 +49,7 @@ flate2-c = ["flate2/miniz-sys"] flate2-rust = ["flate2/rust_backend"] # sessions feature, session require "ring" crate and c compiler -session = ["cookie/secure"] +cookies = ["cookie", "actix-http/cookies"] # tls tls = ["native-tls", "actix-server/ssl"] @@ -67,12 +67,11 @@ actix-utils = "0.3.4" actix-router = "0.1.0" actix-rt = "0.2.1" actix-web-codegen = { path="actix-web-codegen" } -actix-http = { git = "https://github.com/actix/actix-http.git" } +actix-http = { git = "https://github.com/actix/actix-http.git", features=["fail"] } actix-server = "0.4.1" actix-server-config = "0.1.0" bytes = "0.4" -cookie = { version="0.11", features=["percent-encode"] } derive_more = "0.14" encoding = "0.2" futures = "0.1" @@ -88,8 +87,8 @@ serde_urlencoded = "^0.5.3" time = "0.1" url = { version="1.7", features=["query_encoding"] } -# middlewares -# actix-session = { path="session", optional = true } +# cookies support +cookie = { version="0.11", features=["secure", "percent-encode"], optional = true } # compression brotli2 = { version="^0.3.2", optional = true } diff --git a/src/request.rs b/src/request.rs index 5517302f..1722925f 100644 --- a/src/request.rs +++ b/src/request.rs @@ -251,12 +251,14 @@ mod tests { } #[test] + #[cfg(feature = "cookies")] fn test_no_request_cookies() { let req = TestRequest::default().to_http_request(); assert!(req.cookies().unwrap().is_empty()); } #[test] + #[cfg(feature = "cookies")] fn test_request_cookies() { let req = TestRequest::default() .header(header::COOKIE, "cookie1=value1") diff --git a/src/test.rs b/src/test.rs index fe9fb024..ed9cf27c 100644 --- a/src/test.rs +++ b/src/test.rs @@ -11,6 +11,7 @@ use actix_rt::Runtime; use actix_server_config::ServerConfig; use actix_service::{IntoNewService, NewService, Service}; use bytes::Bytes; +#[cfg(feature = "cookies")] use cookie::Cookie; use futures::future::{lazy, Future}; @@ -262,6 +263,7 @@ impl TestRequest { self } + #[cfg(feature = "cookies")] /// Set cookie for this request pub fn cookie(mut self, cookie: Cookie) -> Self { self.req.cookie(cookie);