From ec8aef6b433832d5ab384d7bf66f847356900189 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 10 Oct 2018 08:36:16 -0700 Subject: [PATCH 01/59] update dep versions --- CHANGES.md | 9 +++++++++ Cargo.toml | 9 ++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 260b6df74..39b97cc0b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,14 @@ # Changes +## [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 diff --git a/Cargo.toml b/Cargo.toml index 14102881a..ea400dc66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "0.7.11" +version = "0.7.12" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" @@ -61,11 +61,12 @@ flate2-rust = ["flate2/rust_backend"] cell = ["actix-net/cell"] [dependencies] -actix = "0.7.0" -actix-net = "0.1.0" +actix = "^0.7.5" +actix-net = "^0.1.1" base64 = "0.9" bitflags = "1.0" +failure = "^0.1.2" h2 = "0.1" htmlescape = "0.3" http = "^0.1.8" @@ -93,8 +94,6 @@ cookie = { version="0.11", features=["percent-encode"] } brotli2 = { version="^0.3.2", optional = true } flate2 = { version="^1.0.2", optional = true, default-features = false } -failure = "^0.1.2" - # io mio = "^0.6.13" net2 = "0.2" From 32145cf6c31a9d149041b0894029190e3c4086ba Mon Sep 17 00:00:00 2001 From: jeizsm Date: Thu, 11 Oct 2018 11:05:07 +0300 Subject: [PATCH 02/59] fix after update tokio-rustls (#542) --- CHANGES.md | 6 ++++++ src/client/connector.rs | 16 +++++----------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 39b97cc0b..ad5ae9e1b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Changes +## [0.7.13] - 2018-10-* + +### Fixed + +* Fixed rustls build + ## [0.7.12] - 2018-10-10 ### Changed diff --git a/src/client/connector.rs b/src/client/connector.rs index 07c7b646d..3f4ac27cb 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -37,15 +37,9 @@ use { ))] use { rustls::ClientConfig, std::io::Error as SslError, std::sync::Arc, - tokio_rustls::ClientConfigExt, webpki::DNSNameRef, webpki_roots, + tokio_rustls::TlsConnector as SslConnector, webpki::DNSNameRef, webpki_roots, }; -#[cfg(all( - feature = "rust-tls", - not(any(feature = "alpn", feature = "tls", feature = "ssl")) -))] -type SslConnector = Arc; - #[cfg(not(any( feature = "alpn", feature = "ssl", @@ -282,7 +276,7 @@ impl Default for ClientConnector { config .root_store .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); - Arc::new(config) + SslConnector::from(Arc::new(config)) } #[cfg_attr(rustfmt, rustfmt_skip)] @@ -373,7 +367,7 @@ impl ClientConnector { /// config /// .root_store /// .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); - /// let conn = ClientConnector::with_connector(Arc::new(config)).start(); + /// let conn = ClientConnector::with_connector(config).start(); /// /// conn.send( /// Connect::new("https://www.rust-lang.org").unwrap()) // <- connect to host @@ -390,7 +384,7 @@ impl ClientConnector { /// ``` pub fn with_connector(connector: ClientConfig) -> ClientConnector { // keep level of indirection for docstrings matching featureflags - Self::with_connector_impl(Arc::new(connector)) + Self::with_connector_impl(SslConnector::from(Arc::new(connector))) } #[cfg(all( @@ -832,7 +826,7 @@ impl ClientConnector { let host = DNSNameRef::try_from_ascii_str(&key.host).unwrap(); fut::Either::A( act.connector - .connect_async(host, stream) + .connect(host, stream) .into_actor(act) .then(move |res, _, _| { match res { From d145136e569b49caec0fe87735ab0c736b2eb5de Mon Sep 17 00:00:00 2001 From: Douman Date: Sat, 13 Oct 2018 09:54:03 +0300 Subject: [PATCH 03/59] Add individual check for TLS features --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 62867e030..6793745f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,6 +32,9 @@ script: - | if [[ "$TRAVIS_RUST_VERSION" != "nightly" ]]; then cargo clean + cargo check --feature rust-tls + cargo check --feature ssl + cargo check --feature tls cargo test --features="ssl,tls,rust-tls" -- --nocapture fi - | From 63a443fce0560f2d4275032cd12c3fb2d22dd931 Mon Sep 17 00:00:00 2001 From: Douman Date: Sat, 13 Oct 2018 10:05:21 +0300 Subject: [PATCH 04/59] Correct build script --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6793745f7..c5dfcd81b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,9 +32,9 @@ script: - | if [[ "$TRAVIS_RUST_VERSION" != "nightly" ]]; then cargo clean - cargo check --feature rust-tls - cargo check --feature ssl - cargo check --feature tls + cargo check --features rust-tls + cargo check --features ssl + cargo check --features tls cargo test --features="ssl,tls,rust-tls" -- --nocapture fi - | From dd948f836e75b4edecb912203fe9f6fe89365115 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 14 Oct 2018 08:08:12 -0700 Subject: [PATCH 05/59] HttpServer not sending streamed request body on HTTP/2 requests #544 --- CHANGES.md | 7 +++++-- src/httprequest.rs | 2 +- src/server/output.rs | 23 +++++++++-------------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ad5ae9e1b..62d2e9157 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,13 @@ # Changes -## [0.7.13] - 2018-10-* +## [0.7.13] - 2018-10-14 ### Fixed -* Fixed rustls build +* Fixed rustls support + +* HttpServer not sending streamed request body on HTTP/2 requests #544 + ## [0.7.12] - 2018-10-10 diff --git a/src/httprequest.rs b/src/httprequest.rs index d8c49496a..0e4f74e5e 100644 --- a/src/httprequest.rs +++ b/src/httprequest.rs @@ -216,7 +216,7 @@ impl HttpRequest { self.url_for(name, &NO_PARAMS) } - /// This method returns reference to current `RouteInfo` object. + /// This method returns reference to current `ResourceInfo` object. #[inline] pub fn resource(&self) -> &ResourceInfo { &self.resource diff --git a/src/server/output.rs b/src/server/output.rs index 35f3c7a45..104700d44 100644 --- a/src/server/output.rs +++ b/src/server/output.rs @@ -299,12 +299,11 @@ impl Output { match resp.chunked() { Some(true) => { // Enable transfer encoding - if version == Version::HTTP_2 { - info.length = ResponseLength::None; - TransferEncoding::eof(buf) - } else { - info.length = ResponseLength::Chunked; + info.length = ResponseLength::Chunked; + if version == Version::HTTP_11 { TransferEncoding::chunked(buf) + } else { + TransferEncoding::eof(buf) } } Some(false) => TransferEncoding::eof(buf), @@ -337,15 +336,11 @@ impl Output { } } else { // Enable transfer encoding - match version { - Version::HTTP_11 => { - info.length = ResponseLength::Chunked; - TransferEncoding::chunked(buf) - } - _ => { - info.length = ResponseLength::None; - TransferEncoding::eof(buf) - } + info.length = ResponseLength::Chunked; + if version == Version::HTTP_11 { + TransferEncoding::chunked(buf) + } else { + TransferEncoding::eof(buf) } } } From c04b4678f136b118fef40b979c0df90402a8e0e4 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 14 Oct 2018 08:10:41 -0700 Subject: [PATCH 06/59] bump version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ea400dc66..d98ce5eac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "0.7.12" +version = "0.7.13" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" From f383f618b537d6912ba9e2e7e27402b7bba964e1 Mon Sep 17 00:00:00 2001 From: ivan-ochc Date: Thu, 18 Oct 2018 21:27:31 +0300 Subject: [PATCH 07/59] Fix typo in error message (#554) --- src/pipeline.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pipeline.rs b/src/pipeline.rs index 1940f9308..a938f2eb2 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -551,12 +551,12 @@ impl ProcessResponse { if self.resp.as_ref().unwrap().status().is_server_error() { error!( - "Error occured during request handling, status: {} {}", + "Error occurred during request handling, status: {} {}", self.resp.as_ref().unwrap().status(), err ); } else { warn!( - "Error occured during request handling: {}", + "Error occurred during request handling: {}", err ); } From 960274ada8540064364579cb5a7caeb289ae7340 Mon Sep 17 00:00:00 2001 From: Douman Date: Fri, 19 Oct 2018 07:52:10 +0300 Subject: [PATCH 08/59] Refactoring of server output to not exclude HTTP_10 (#552) --- CHANGES.md | 6 ++++++ src/server/output.rs | 12 ++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 62d2e9157..8ac1724a3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Changes +## [0.7.14] - 2018-10-x + +### Fixed + +* HttpServer now treats streaming bodies the same for HTTP/1.x protocols. #549 + ## [0.7.13] - 2018-10-14 ### Fixed diff --git a/src/server/output.rs b/src/server/output.rs index 104700d44..ac89d6440 100644 --- a/src/server/output.rs +++ b/src/server/output.rs @@ -300,10 +300,10 @@ impl Output { Some(true) => { // Enable transfer encoding info.length = ResponseLength::Chunked; - if version == Version::HTTP_11 { - TransferEncoding::chunked(buf) - } else { + if version == Version::HTTP_2 { TransferEncoding::eof(buf) + } else { + TransferEncoding::chunked(buf) } } Some(false) => TransferEncoding::eof(buf), @@ -337,10 +337,10 @@ impl Output { } else { // Enable transfer encoding info.length = ResponseLength::Chunked; - if version == Version::HTTP_11 { - TransferEncoding::chunked(buf) - } else { + if version == Version::HTTP_2 { TransferEncoding::eof(buf) + } else { + TransferEncoding::chunked(buf) } } } From 42d5d48e7105270e89eb8af6b111c305c5536e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Sat, 20 Oct 2018 05:43:43 +0200 Subject: [PATCH 09/59] add a way to configure error treatment for Query and Path extractors (#550) * add a way to configure error treatment for Query extractor * allow error handler to be customized for Path extractor --- CHANGES.md | 4 ++ src/extractor.rs | 122 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 114 insertions(+), 12 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8ac1724a3..f5adb82c3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,10 @@ * HttpServer now treats streaming bodies the same for HTTP/1.x protocols. #549 +### Added + +* Add method to configure custom error handler to `Query` and `Path` extractors. + ## [0.7.13] - 2018-10-14 ### Fixed diff --git a/src/extractor.rs b/src/extractor.rs index 7b0b4b003..45e29ace0 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -111,18 +111,64 @@ impl FromRequest for Path where T: DeserializeOwned, { - type Config = (); + type Config = PathConfig; type Result = Result; #[inline] - fn from_request(req: &HttpRequest, _: &Self::Config) -> Self::Result { + 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)) - .map_err(ErrorNotFound) + .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>, +} +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 + } +} + +impl Default for PathConfig { + fn default() -> Self { + PathConfig { + ehandler: Rc::new(|e, _| ErrorNotFound(e)), + } + } +} + impl fmt::Debug for Path { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.inner.fmt(f) @@ -200,17 +246,69 @@ impl FromRequest for Query where T: de::DeserializeOwned, { - type Config = (); + type Config = QueryConfig; type Result = Result; #[inline] - fn from_request(req: &HttpRequest, _: &Self::Config) -> Self::Result { + fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { + let req2 = req.clone(); + let err = Rc::clone(&cfg.ehandler); serde_urlencoded::from_str::(req.query_string()) - .map_err(|e| e.into()) + .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()), + } + } +} + impl fmt::Debug for Query { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.0.fmt(f) @@ -951,15 +1049,15 @@ mod tests { let info = router.recognize(&req, &(), 0); let req = req.with_route_info(info); - let s = Path::::from_request(&req, &()).unwrap(); + let s = Path::::from_request(&req, &PathConfig::default()).unwrap(); assert_eq!(s.key, "name"); assert_eq!(s.value, "user1"); - let s = Path::<(String, String)>::from_request(&req, &()).unwrap(); + let s = Path::<(String, String)>::from_request(&req, &PathConfig::default()).unwrap(); assert_eq!(s.0, "name"); assert_eq!(s.1, "user1"); - let s = Query::::from_request(&req, &()).unwrap(); + let s = Query::::from_request(&req, &QueryConfig::default()).unwrap(); assert_eq!(s.id, "test"); let mut router = Router::<()>::default(); @@ -968,11 +1066,11 @@ mod tests { let info = router.recognize(&req, &(), 0); let req = req.with_route_info(info); - let s = Path::::from_request(&req, &()).unwrap(); + 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, &()).unwrap(); + let s = Path::<(String, u8)>::from_request(&req, &PathConfig::default()).unwrap(); assert_eq!(s.0, "name"); assert_eq!(s.1, 32); @@ -989,7 +1087,7 @@ mod tests { 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); + assert_eq!(*Path::::from_request(&req, &&PathConfig::default()).unwrap(), 32); } #[test] From 5f91f5eda6f3b61d91a63b5ef651ef9f2617d7b7 Mon Sep 17 00:00:00 2001 From: Douman Date: Fri, 26 Oct 2018 10:59:06 +0300 Subject: [PATCH 10/59] Correct IoStream::set_keepalive for UDS (#564) Enable uds feature in tests --- .travis.yml | 2 +- src/server/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c5dfcd81b..9b1bcff54 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,7 +35,7 @@ script: cargo check --features rust-tls cargo check --features ssl cargo check --features tls - cargo test --features="ssl,tls,rust-tls" -- --nocapture + cargo test --features="ssl,tls,rust-tls,uds" -- --nocapture fi - | if [[ "$TRAVIS_RUST_VERSION" == "nightly" ]]; then diff --git a/src/server/mod.rs b/src/server/mod.rs index 8d7195166..0a16f26b9 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -334,7 +334,7 @@ impl IoStream for ::tokio_uds::UnixStream { } #[inline] - fn set_keepalive(&mut self, _nodelay: bool) -> io::Result<()> { + fn set_keepalive(&mut self, _dur: Option) -> io::Result<()> { Ok(()) } } From cfd9a56ff74dd6cb6ad38b6a67b40ed1763d6071 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 28 Oct 2018 09:24:19 -0700 Subject: [PATCH 11/59] Add async/await ref --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 321f82abf..db3cc68c5 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Actix web is a simple, pragmatic and extremely fast web framework for Rust. * 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 From 3b536ee96c0118e8cf452b28f6acab6085db22a6 Mon Sep 17 00:00:00 2001 From: Stanislav Tkach Date: Thu, 1 Nov 2018 10:14:48 +0200 Subject: [PATCH 12/59] Use old clippy attributes syntax (#562) --- src/client/connector.rs | 2 +- src/client/writer.rs | 2 +- src/httpresponse.rs | 2 +- src/info.rs | 2 +- src/middleware/defaultheaders.rs | 2 +- src/scope.rs | 2 +- src/server/h2writer.rs | 2 +- src/server/http.rs | 2 +- src/server/output.rs | 4 ++-- src/ws/frame.rs | 2 +- src/ws/mask.rs | 6 +++--- 11 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/client/connector.rs b/src/client/connector.rs index 3f4ac27cb..3990c955c 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -287,7 +287,7 @@ impl Default for ClientConnector { } }; - #[cfg_attr(feature = "cargo-clippy", allow(clippy::let_unit_value))] + #[cfg_attr(feature = "cargo-clippy", allow(let_unit_value))] ClientConnector::with_connector_impl(connector) } } diff --git a/src/client/writer.rs b/src/client/writer.rs index e74f22332..321753bbf 100644 --- a/src/client/writer.rs +++ b/src/client/writer.rs @@ -1,6 +1,6 @@ #![cfg_attr( feature = "cargo-clippy", - allow(clippy::redundant_field_names) + allow(redundant_field_names) )] use std::cell::RefCell; diff --git a/src/httpresponse.rs b/src/httpresponse.rs index 8b091d42e..52dd8046b 100644 --- a/src/httpresponse.rs +++ b/src/httpresponse.rs @@ -694,7 +694,7 @@ impl HttpResponseBuilder { } #[inline] -#[cfg_attr(feature = "cargo-clippy", allow(clippy::borrowed_box))] +#[cfg_attr(feature = "cargo-clippy", allow(borrowed_box))] fn parts<'a>( parts: &'a mut Option>, err: &Option, ) -> Option<&'a mut Box> { diff --git a/src/info.rs b/src/info.rs index 5a2f21805..43c22123e 100644 --- a/src/info.rs +++ b/src/info.rs @@ -18,7 +18,7 @@ impl ConnectionInfo { /// Create *ConnectionInfo* instance for a request. #[cfg_attr( feature = "cargo-clippy", - allow(clippy::cyclomatic_complexity) + allow(cyclomatic_complexity) )] pub fn update(&mut self, req: &Request) { let mut host = None; diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index d980a2503..a33fa6a33 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -48,7 +48,7 @@ impl DefaultHeaders { /// Set a header. #[inline] - #[cfg_attr(feature = "cargo-clippy", allow(clippy::match_wild_err_arm))] + #[cfg_attr(feature = "cargo-clippy", allow(match_wild_err_arm))] pub fn header(mut self, key: K, value: V) -> Self where HeaderName: HttpTryFrom, diff --git a/src/scope.rs b/src/scope.rs index 43789d427..1bddc0e01 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -61,7 +61,7 @@ pub struct Scope { #[cfg_attr( feature = "cargo-clippy", - allow(clippy::new_without_default_derive) + allow(new_without_default_derive) )] impl Scope { /// Create a new scope diff --git a/src/server/h2writer.rs b/src/server/h2writer.rs index 66f2923c4..fef6f889a 100644 --- a/src/server/h2writer.rs +++ b/src/server/h2writer.rs @@ -1,6 +1,6 @@ #![cfg_attr( feature = "cargo-clippy", - allow(clippy::redundant_field_names) + allow(redundant_field_names) )] use std::{cmp, io}; diff --git a/src/server/http.rs b/src/server/http.rs index 9ecd4a5d2..0bec8be3f 100644 --- a/src/server/http.rs +++ b/src/server/http.rs @@ -326,7 +326,7 @@ where #[doc(hidden)] #[cfg_attr( feature = "cargo-clippy", - allow(clippy::needless_pass_by_value) + allow(needless_pass_by_value) )] pub fn bind_with(mut self, addr: S, acceptor: A) -> io::Result where diff --git a/src/server/output.rs b/src/server/output.rs index ac89d6440..4a86ffbb7 100644 --- a/src/server/output.rs +++ b/src/server/output.rs @@ -438,7 +438,7 @@ impl ContentEncoder { } } - #[cfg_attr(feature = "cargo-clippy", allow(clippy::inline_always))] + #[cfg_attr(feature = "cargo-clippy", allow(inline_always))] #[inline(always)] pub fn write_eof(&mut self) -> Result { let encoder = @@ -480,7 +480,7 @@ impl ContentEncoder { } } - #[cfg_attr(feature = "cargo-clippy", allow(clippy::inline_always))] + #[cfg_attr(feature = "cargo-clippy", allow(inline_always))] #[inline(always)] pub fn write(&mut self, data: &[u8]) -> Result<(), io::Error> { match *self { diff --git a/src/ws/frame.rs b/src/ws/frame.rs index d5fa98272..5e4fd8290 100644 --- a/src/ws/frame.rs +++ b/src/ws/frame.rs @@ -46,7 +46,7 @@ impl Frame { Frame::message(payload, OpCode::Close, true, genmask) } - #[cfg_attr(feature = "cargo-clippy", allow(clippy::type_complexity))] + #[cfg_attr(feature = "cargo-clippy", allow(type_complexity))] fn read_copy_md( pl: &mut PayloadBuffer, server: bool, max_size: usize, ) -> Poll)>, ProtocolError> diff --git a/src/ws/mask.rs b/src/ws/mask.rs index a88c21afb..18ce57bb7 100644 --- a/src/ws/mask.rs +++ b/src/ws/mask.rs @@ -1,5 +1,5 @@ //! This is code from [Tungstenite project](https://github.com/snapview/tungstenite-rs) -#![cfg_attr(feature = "cargo-clippy", allow(clippy::cast_ptr_alignment))] +#![cfg_attr(feature = "cargo-clippy", allow(cast_ptr_alignment))] use std::ptr::copy_nonoverlapping; use std::slice; @@ -19,7 +19,7 @@ impl<'a> ShortSlice<'a> { /// Faster version of `apply_mask()` which operates on 8-byte blocks. #[inline] -#[cfg_attr(feature = "cargo-clippy", allow(clippy::cast_lossless))] +#[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); @@ -52,7 +52,7 @@ pub(crate) fn apply_mask(buf: &mut [u8], mask_u32: u32) { // a `ShortSlice` must be smaller than a u64. #[cfg_attr( feature = "cargo-clippy", - allow(clippy::needless_pass_by_value) + allow(needless_pass_by_value) )] fn xor_short(buf: ShortSlice, mask: u64) { // Unsafe: we know that a `ShortSlice` fits in a u64 From 8e354021d47b2131180de1a312d308ca96e7eb9a Mon Sep 17 00:00:00 2001 From: Julian Tescher Date: Wed, 7 Nov 2018 12:24:06 -0800 Subject: [PATCH 13/59] Add SameSite option to identity middleware cookie (#581) --- CHANGES.md | 1 + src/middleware/identity.rs | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index f5adb82c3..2aa9cbfd2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ ### Added * Add method to configure custom error handler to `Query` and `Path` extractors. +* Add method to configure `SameSite` option in `CookieIdentityPolicy`. ## [0.7.13] - 2018-10-14 diff --git a/src/middleware/identity.rs b/src/middleware/identity.rs index d890bebef..a664ba1f0 100644 --- a/src/middleware/identity.rs +++ b/src/middleware/identity.rs @@ -48,7 +48,7 @@ //! ``` use std::rc::Rc; -use cookie::{Cookie, CookieJar, Key}; +use cookie::{Cookie, CookieJar, Key, SameSite}; use futures::future::{err as FutErr, ok as FutOk, FutureResult}; use futures::Future; use time::Duration; @@ -237,6 +237,7 @@ struct CookieIdentityInner { domain: Option, secure: bool, max_age: Option, + same_site: Option, } impl CookieIdentityInner { @@ -248,6 +249,7 @@ impl CookieIdentityInner { domain: None, secure: true, max_age: None, + same_site: None, } } @@ -268,6 +270,10 @@ impl CookieIdentityInner { 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); @@ -370,6 +376,12 @@ impl 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 { From 2677d325a727508e0d2b17ac412173b06528eb7a Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 7 Nov 2018 21:09:33 -0800 Subject: [PATCH 14/59] fix keep-alive timer reset --- CHANGES.md | 7 ++++++- Cargo.toml | 2 +- src/server/h1.rs | 21 +++++++++++++++------ src/server/h2.rs | 18 +++++++++++++----- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2aa9cbfd2..1e66cff87 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,16 +1,21 @@ # Changes -## [0.7.14] - 2018-10-x +## [0.7.14] - 2018-11-x ### Fixed +* Fix keep-alive timer reset + * HttpServer now treats streaming bodies the same for HTTP/1.x protocols. #549 + ### Added * Add method to configure custom error handler to `Query` and `Path` extractors. + * Add method to configure `SameSite` option in `CookieIdentityPolicy`. + ## [0.7.13] - 2018-10-14 ### Fixed diff --git a/Cargo.toml b/Cargo.toml index d98ce5eac..4a6e23173 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "0.7.13" +version = "0.7.14" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" diff --git a/src/server/h1.rs b/src/server/h1.rs index a2ffc0551..07f773eba 100644 --- a/src/server/h1.rs +++ b/src/server/h1.rs @@ -87,7 +87,10 @@ where H: HttpHandler + 'static, { pub fn new( - settings: ServiceConfig, stream: T, buf: BytesMut, is_eof: bool, + settings: ServiceConfig, + stream: T, + buf: BytesMut, + is_eof: bool, keepalive_timer: Option, ) -> Self { let addr = stream.peer_addr(); @@ -123,8 +126,11 @@ where } pub(crate) fn for_error( - settings: ServiceConfig, stream: T, status: StatusCode, - mut keepalive_timer: Option, buf: BytesMut, + 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)); @@ -298,16 +304,19 @@ where if let Some(deadline) = self.settings.client_shutdown_timer() { - timer.reset(deadline) + timer.reset(deadline); + let _ = timer.poll(); } else { return Ok(()); } } } else if let Some(dl) = self.settings.keep_alive_expire() { - timer.reset(dl) + timer.reset(dl); + let _ = timer.poll(); } } else { - timer.reset(self.ka_expire) + timer.reset(self.ka_expire); + let _ = timer.poll(); } } Ok(Async::NotReady) => (), diff --git a/src/server/h2.rs b/src/server/h2.rs index 35afa3397..c9e968a39 100644 --- a/src/server/h2.rs +++ b/src/server/h2.rs @@ -60,7 +60,10 @@ where H: HttpHandler + 'static, { pub fn new( - settings: ServiceConfig, io: T, buf: Bytes, keepalive_timer: Option, + settings: ServiceConfig, + io: T, + buf: Bytes, + keepalive_timer: Option, ) -> Self { let addr = io.peer_addr(); let extensions = io.extensions(); @@ -284,10 +287,12 @@ where if self.tasks.is_empty() { return Err(HttpDispatchError::ShutdownTimeout); } else if let Some(dl) = self.settings.keep_alive_expire() { - timer.reset(dl) + timer.reset(dl); + let _ = timer.poll(); } } else { - timer.reset(self.ka_expire) + timer.reset(self.ka_expire); + let _ = timer.poll(); } } Ok(Async::NotReady) => (), @@ -348,8 +353,11 @@ struct Entry { impl Entry { fn new( - parts: Parts, recv: RecvStream, resp: SendResponse, - addr: Option, settings: ServiceConfig, + parts: Parts, + recv: RecvStream, + resp: SendResponse, + addr: Option, + settings: ServiceConfig, extensions: Option>, ) -> Entry where From 62f1c90c8d245d3854e1ff2d7228c0c705aa1eb7 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 7 Nov 2018 21:18:40 -0800 Subject: [PATCH 15/59] update base64 dep --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4a6e23173..8041c783f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,7 @@ cell = ["actix-net/cell"] actix = "^0.7.5" actix-net = "^0.1.1" -base64 = "0.9" +base64 = "0.10" bitflags = "1.0" failure = "^0.1.2" h2 = "0.1" From 9ab586e24e1ea4be98c31e6b0eed09f962fad1f9 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 8 Nov 2018 16:06:23 -0800 Subject: [PATCH 16/59] update actix-net dep --- Cargo.toml | 2 +- tests/test_ws.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8041c783f..16be8cd41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,7 +62,7 @@ cell = ["actix-net/cell"] [dependencies] actix = "^0.7.5" -actix-net = "^0.1.1" +actix-net = "0.2.0" base64 = "0.10" bitflags = "1.0" diff --git a/tests/test_ws.rs b/tests/test_ws.rs index 5a0ce204f..cb46bc7e1 100644 --- a/tests/test_ws.rs +++ b/tests/test_ws.rs @@ -385,10 +385,11 @@ fn test_ws_stopped() { { 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(1000)); + thread::sleep(time::Duration::from_millis(100)); assert_eq!(num.load(Ordering::Relaxed), 1); } From 1a0bf32ec76411e6ae017ea680b4dad7db3f0c69 Mon Sep 17 00:00:00 2001 From: imaperson Date: Fri, 9 Nov 2018 01:08:06 +0100 Subject: [PATCH 17/59] Fix unnecessary owned string and change htmlescape in favor of askama_escape (#584) --- Cargo.toml | 2 +- src/fs.rs | 27 +++++++++++++++++++-------- src/lib.rs | 2 +- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 16be8cd41..0dcce54b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,11 +64,11 @@ cell = ["actix-net/cell"] actix = "^0.7.5" actix-net = "0.2.0" +askama_escape = "0.1.0" base64 = "0.10" bitflags = "1.0" failure = "^0.1.2" h2 = "0.1" -htmlescape = "0.3" http = "^0.1.8" httparse = "1.3" log = "0.4" diff --git a/src/fs.rs b/src/fs.rs index 10cdaff7b..51470846e 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -11,10 +11,10 @@ use std::{cmp, io}; #[cfg(unix)] use std::os::unix::fs::MetadataExt; +use askama_escape::{escape as escape_html_entity}; use bytes::Bytes; use futures::{Async, Future, Poll, Stream}; use futures_cpupool::{CpuFuture, CpuPool}; -use htmlescape::encode_minimal as escape_html_entity; use mime; use mime_guess::{get_mime_type, guess_mime_type}; use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; @@ -561,6 +561,20 @@ impl Directory { } } +// 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 { @@ -575,11 +589,6 @@ fn directory_listing( Ok(p) => base.join(p), Err(_) => continue, }; - // show file url as relative to static path - let file_url = utf8_percent_encode(&p.to_string_lossy(), DEFAULT_ENCODE_SET) - .to_string(); - // " -- " & -- & ' -- ' < -- < > -- > - let file_name = escape_html_entity(&entry.file_name().to_string_lossy()); // if file is a directory, add '/' to the end of the name if let Ok(metadata) = entry.metadata() { @@ -587,13 +596,15 @@ fn directory_listing( let _ = write!( body, "
  • {}/
  • ", - file_url, file_name + encode_file_url!(p), + encode_file_name!(entry), ); } else { let _ = write!( body, "
  • {}
  • ", - file_url, file_name + encode_file_url!(p), + encode_file_name!(entry), ); } } else { diff --git a/src/lib.rs b/src/lib.rs index 1ed408099..738153fab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,9 +100,9 @@ extern crate failure; extern crate lazy_static; #[macro_use] extern crate futures; +extern crate askama_escape; extern crate cookie; extern crate futures_cpupool; -extern crate htmlescape; extern crate http as modhttp; extern crate httparse; extern crate language_tags; From 5b7740dee3f0ddd5ff953755b62f1371c95e7489 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 8 Nov 2018 16:12:16 -0800 Subject: [PATCH 18/59] hide ChunkedReadFile --- src/fs.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/fs.rs b/src/fs.rs index 10cdaff7b..4fa112871 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -472,6 +472,7 @@ impl Responder for NamedFile { } } +#[doc(hidden)] /// A helper created from a `std::fs::File` which reads the file /// chunk-by-chunk on a `CpuPool`. pub struct ChunkedReadFile { @@ -562,7 +563,8 @@ impl Directory { } fn directory_listing( - dir: &Directory, req: &HttpRequest, + dir: &Directory, + req: &HttpRequest, ) -> Result { let index_of = format!("Index of {}", req.path()); let mut body = String::new(); @@ -656,7 +658,8 @@ impl StaticFiles { /// Create new `StaticFiles` instance for specified base directory and /// `CpuPool`. pub fn with_pool>( - dir: T, pool: CpuPool, + dir: T, + pool: CpuPool, ) -> Result, Error> { Self::with_config_pool(dir, pool, DefaultConfig) } @@ -667,7 +670,8 @@ impl StaticFiles { /// /// Identical with `new` but allows to specify configiration to use. pub fn with_config>( - dir: T, config: C, + dir: T, + config: C, ) -> Result, Error> { // use default CpuPool let pool = { DEFAULT_CPUPOOL.lock().clone() }; @@ -678,7 +682,9 @@ impl StaticFiles { /// Create new `StaticFiles` instance for specified base directory with config and /// `CpuPool`. pub fn with_config_pool>( - dir: T, pool: CpuPool, _: C, + dir: T, + pool: CpuPool, + _: C, ) -> Result, Error> { let dir = dir.into().canonicalize()?; @@ -736,7 +742,8 @@ impl StaticFiles { } fn try_handle( - &self, req: &HttpRequest, + &self, + req: &HttpRequest, ) -> Result, Error> { let tail: String = req.match_info().query("tail")?; let relpath = PathBuf::from_param(tail.trim_left_matches('/'))?; From 7065c540e1822fb15d4d040703c314c15ce81e95 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 8 Nov 2018 16:29:43 -0800 Subject: [PATCH 19/59] set nodelay on socket #560 --- CHANGES.md | 14 ++++++++------ src/server/builder.rs | 33 ++++++++++++++++++++++++++------- src/server/service.rs | 2 +- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1e66cff87..617237417 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,18 +2,20 @@ ## [0.7.14] - 2018-11-x +### Added + +* Add method to configure custom error handler to `Query` and `Path` extractors. + +* Add method to configure `SameSite` option in `CookieIdentityPolicy`. + + ### Fixed * Fix keep-alive timer reset * HttpServer now treats streaming bodies the same for HTTP/1.x protocols. #549 - -### Added - -* Add method to configure custom error handler to `Query` and `Path` extractors. - -* Add method to configure `SameSite` option in `CookieIdentityPolicy`. +* Set nodelay for socket #560 ## [0.7.13] - 2018-10-14 diff --git a/src/server/builder.rs b/src/server/builder.rs index 4f159af13..ea3638f10 100644 --- a/src/server/builder.rs +++ b/src/server/builder.rs @@ -9,14 +9,20 @@ use super::acceptor::{ }; use super::error::AcceptorError; use super::handler::IntoHttpHandler; -use super::service::HttpService; +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, + &self, + server: Server, + lst: net::TcpListener, + host: String, + addr: net::SocketAddr, + keep_alive: KeepAlive, + secure: bool, + client_timeout: u64, client_shutdown: u64, ) -> Server; } @@ -43,8 +49,13 @@ where } fn finish( - &self, host: String, addr: net::SocketAddr, keep_alive: KeepAlive, secure: bool, - client_timeout: u64, client_shutdown: u64, + &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(); @@ -65,6 +76,7 @@ where acceptor.create(), )).map_err(|_| ()) .map_init_err(|_| ()) + .and_then(StreamConfiguration::new().nodelay(true)) .and_then( HttpService::new(settings) .map_init_err(|_| ()) @@ -76,6 +88,7 @@ where 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(|_| ()) @@ -95,8 +108,14 @@ where H: IntoHttpHandler, { fn register( - &self, server: Server, lst: net::TcpListener, host: String, - addr: net::SocketAddr, keep_alive: KeepAlive, secure: bool, client_timeout: u64, + &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( diff --git a/src/server/service.rs b/src/server/service.rs index e3402e305..cd4b3d3fa 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -88,7 +88,7 @@ where Ok(Async::Ready(())) } - fn call(&mut self, req: Self::Request) -> Self::Future { + fn call(&mut self, mut req: Self::Request) -> Self::Future { HttpChannel::new(self.settings.clone(), req) } } From 61b1030882781f93c0228b5605041a197e5eb8f5 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 8 Nov 2018 20:35:47 -0800 Subject: [PATCH 20/59] Fix websockets connection drop if request contains content-length header #567 --- CHANGES.md | 2 ++ Cargo.toml | 4 ++-- src/server/h1decoder.rs | 31 +++++++++++++++++++++++++------ src/server/service.rs | 2 +- 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 617237417..b1717ea92 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,8 @@ ### 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 diff --git a/Cargo.toml b/Cargo.toml index 0dcce54b0..4abb64e27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,8 +61,8 @@ flate2-rust = ["flate2/rust_backend"] cell = ["actix-net/cell"] [dependencies] -actix = "^0.7.5" -actix-net = "0.2.0" +actix = "0.7.6" +actix-net = "0.2.1" askama_escape = "0.1.0" base64 = "0.10" diff --git a/src/server/h1decoder.rs b/src/server/h1decoder.rs index 434dc42df..10f7e68a0 100644 --- a/src/server/h1decoder.rs +++ b/src/server/h1decoder.rs @@ -43,7 +43,9 @@ impl H1Decoder { } pub fn decode( - &mut self, src: &mut BytesMut, settings: &ServiceConfig, + &mut self, + src: &mut BytesMut, + settings: &ServiceConfig, ) -> Result, DecoderError> { // read payload if self.decoder.is_some() { @@ -80,7 +82,9 @@ impl H1Decoder { } fn parse_message( - &self, buf: &mut BytesMut, settings: &ServiceConfig, + &self, + buf: &mut BytesMut, + settings: &ServiceConfig, ) -> Poll<(Request, Option), ParseError> { // Parse http message let mut has_upgrade = false; @@ -178,6 +182,13 @@ impl H1Decoder { } 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() { + if val == "websocket" { + content_length = None; + } + } } _ => (), } @@ -221,7 +232,9 @@ pub(crate) struct HeaderIndex { impl HeaderIndex { pub(crate) fn record( - bytes: &[u8], headers: &[httparse::Header], indices: &mut [HeaderIndex], + 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()) { @@ -369,7 +382,10 @@ macro_rules! byte ( impl ChunkedState { fn step( - &self, body: &mut BytesMut, size: &mut u64, buf: &mut Option, + &self, + body: &mut BytesMut, + size: &mut u64, + buf: &mut Option, ) -> Poll { use self::ChunkedState::*; match *self { @@ -432,7 +448,8 @@ impl ChunkedState { } } fn read_size_lf( - rdr: &mut BytesMut, size: &mut u64, + rdr: &mut BytesMut, + size: &mut u64, ) -> Poll { match byte!(rdr) { b'\n' if *size > 0 => Ok(Async::Ready(ChunkedState::Body)), @@ -445,7 +462,9 @@ impl ChunkedState { } fn read_body( - rdr: &mut BytesMut, rem: &mut u64, buf: &mut Option, + rdr: &mut BytesMut, + rem: &mut u64, + buf: &mut Option, ) -> Poll { trace!("Chunked read, remaining={:?}", rem); diff --git a/src/server/service.rs b/src/server/service.rs index cd4b3d3fa..e3402e305 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -88,7 +88,7 @@ where Ok(Async::Ready(())) } - fn call(&mut self, mut req: Self::Request) -> Self::Future { + fn call(&mut self, req: Self::Request) -> Self::Future { HttpChannel::new(self.settings.clone(), req) } } From 1ef0eed0bde2d66ea38762e7a8d1ec65b68e2cf4 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Thu, 8 Nov 2018 20:46:13 -0800 Subject: [PATCH 21/59] do not stop on keep-alive timer if sink is not completly flushed --- src/server/h1.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/h1.rs b/src/server/h1.rs index 07f773eba..f491ba597 100644 --- a/src/server/h1.rs +++ b/src/server/h1.rs @@ -286,7 +286,7 @@ where } if timer.deadline() >= self.ka_expire { // check for any outstanding request handling - if self.tasks.is_empty() { + 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"); From cd9901c928bfb7b016484f8c0c81c3629eca3e9f Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Wed, 14 Nov 2018 16:24:01 -0800 Subject: [PATCH 22/59] prepare release --- CHANGES.md | 2 +- Cargo.toml | 2 +- src/client/parser.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b1717ea92..efeaadf0a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Changes -## [0.7.14] - 2018-11-x +## [0.7.14] - 2018-11-14 ### Added diff --git a/Cargo.toml b/Cargo.toml index 4abb64e27..41f2e6676 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,7 +62,7 @@ cell = ["actix-net/cell"] [dependencies] actix = "0.7.6" -actix-net = "0.2.1" +actix-net = "0.2.2" askama_escape = "0.1.0" base64 = "0.10" diff --git a/src/client/parser.rs b/src/client/parser.rs index 11252fa52..92a7abe13 100644 --- a/src/client/parser.rs +++ b/src/client/parser.rs @@ -56,7 +56,7 @@ impl HttpResponseParser { return Ok(Async::Ready(msg)); } Async::NotReady => { - if buf.capacity() >= MAX_BUFFER_SIZE { + if buf.len() >= MAX_BUFFER_SIZE { return Err(HttpResponseParserError::Error( ParseError::TooLarge, )); From 6a9317847979a17fa8d39f05d15188cbb7dde902 Mon Sep 17 00:00:00 2001 From: Huston Bokinsky Date: Sat, 17 Nov 2018 15:25:44 -0800 Subject: [PATCH 23/59] Complete error helper functions. --- src/error.rs | 312 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 312 insertions(+) diff --git a/src/error.rs b/src/error.rs index 76c8e79ec..1766c1523 100644 --- a/src/error.rs +++ b/src/error.rs @@ -759,6 +759,16 @@ where 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)] @@ -789,6 +799,26 @@ where 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)] @@ -819,6 +849,16 @@ where 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)] @@ -829,6 +869,46 @@ where 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)] @@ -839,6 +919,106 @@ where 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)] @@ -889,6 +1069,66 @@ where 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::*; @@ -1068,6 +1308,9 @@ mod tests { 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); @@ -1077,6 +1320,12 @@ mod tests { 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); @@ -1086,12 +1335,57 @@ mod tests { 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); @@ -1106,5 +1400,23 @@ mod tests { 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); } } From 389cb13cd63704a024d7e668592c2aca06bcd876 Mon Sep 17 00:00:00 2001 From: Douman Date: Tue, 20 Nov 2018 23:06:38 +0300 Subject: [PATCH 24/59] Export PathConfig and QueryConfig Closes #597 --- CHANGES.md | 6 ++++++ src/lib.rs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index efeaadf0a..cb4488833 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Changes +## [0.7.15] - 2018-xx-xx + +## Changed + +* `QueryConfig` and `PathConfig` are made public. + ## [0.7.14] - 2018-11-14 ### Added diff --git a/src/lib.rs b/src/lib.rs index 738153fab..f8326886f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -255,7 +255,7 @@ pub mod dev { pub use body::BodyStream; pub use context::Drain; - pub use extractor::{FormConfig, PayloadConfig}; + pub use extractor::{FormConfig, PayloadConfig, QueryConfig, PathConfig}; pub use handler::{AsyncResult, Handler}; pub use httpmessage::{MessageBody, Readlines, UrlEncoded}; pub use httpresponse::HttpResponseBuilder; From 9aab382ea89395fcc627c5375ddd8721cc47c514 Mon Sep 17 00:00:00 2001 From: Douman Date: Thu, 22 Nov 2018 19:20:07 +0300 Subject: [PATCH 25/59] Allow user to provide addr to custom resolver We basically swaps Addr with Recipient to enable user to use custom resolver --- CHANGES.md | 2 ++ Cargo.toml | 2 +- src/client/connector.rs | 12 +++++++----- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index cb4488833..2e028d6db 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,8 @@ ## Changed +* `ClientConnector::resolver` now accepts `Into` instead of `Addr`. It enables user to implement own resolver. + * `QueryConfig` and `PathConfig` are made public. ## [0.7.14] - 2018-11-14 diff --git a/Cargo.toml b/Cargo.toml index 41f2e6676..e3fbd4e38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ flate2-rust = ["flate2/rust_backend"] cell = ["actix-net/cell"] [dependencies] -actix = "0.7.6" +actix = "0.7.7" actix-net = "0.2.2" askama_escape = "0.1.0" diff --git a/src/client/connector.rs b/src/client/connector.rs index 3990c955c..72132bc67 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -5,7 +5,7 @@ use std::{fmt, io, mem, time}; use actix::resolver::{Connect as ResolveConnect, Resolver, ResolverError}; use actix::{ - fut, Actor, ActorFuture, ActorResponse, Addr, AsyncContext, Context, + fut, Actor, ActorFuture, ActorResponse, AsyncContext, Context, ContextFutureSpawner, Handler, Message, Recipient, StreamHandler, Supervised, SystemService, WrapFuture, }; @@ -220,7 +220,7 @@ pub struct ClientConnector { acq_tx: mpsc::UnboundedSender, acq_rx: Option>, - resolver: Option>, + resolver: Option>, conn_lifetime: Duration, conn_keep_alive: Duration, limit: usize, @@ -239,7 +239,7 @@ impl Actor for ClientConnector { fn started(&mut self, ctx: &mut Self::Context) { if self.resolver.is_none() { - self.resolver = Some(Resolver::from_registry()) + self.resolver = Some(Resolver::from_registry().recipient()) } self.collect_periodic(ctx); ctx.add_stream(self.acq_rx.take().unwrap()); @@ -503,8 +503,10 @@ impl ClientConnector { } /// Use custom resolver actor - pub fn resolver(mut self, addr: Addr) -> Self { - self.resolver = Some(addr); + /// + /// By default actix's Resolver is used. + pub fn resolver>>(mut self, addr: A) -> Self { + self.resolver = Some(addr.into()); self } From c386353337cff83626941fca2b58628845b440f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Sat, 24 Nov 2018 14:54:11 +0100 Subject: [PATCH 26/59] decode reserved characters when extracting path with configuration (#577) * decode reserved characters when extracting path with configuration * remove useless clone * add a method to get decoded parameter by name --- CHANGES.md | 9 ++++ MIGRATION.md | 28 +++++++++++++ src/de.rs | 70 ++++++++++++++++++------------- src/extractor.rs | 76 ++++++++++++++++++++++++++++++++- src/param.rs | 33 ++++++++++++++- src/uri.rs | 95 ++++++++++++++++++++++-------------------- tests/test_handlers.rs | 2 +- 7 files changed, 234 insertions(+), 79 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2e028d6db..902a84f69 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,12 @@ * `QueryConfig` and `PathConfig` are made public. +### 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 @@ -16,6 +22,9 @@ * 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 diff --git a/MIGRATION.md b/MIGRATION.md index 3c0bdd943..26a314240 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,3 +1,31 @@ +## 0.7.15 + +* The `' '` character is not percent decoded anymore before matching routes. If you need to use it in + your routes, you should use `%20`. + + instead of + + ```rust + fn main() { + let app = App::new().resource("/my index", |r| { + r.method(http::Method::GET) + .with(index); + }); + } + ``` + + use + + ```rust + fn main() { + let app = App::new().resource("/my%20index", |r| { + r.method(http::Method::GET) + .with(index); + }); + } + ``` + + ## 0.7.4 * `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple diff --git a/src/de.rs b/src/de.rs index 59ab79ba9..05f8914f8 100644 --- a/src/de.rs +++ b/src/de.rs @@ -1,7 +1,10 @@ +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) => { @@ -13,6 +16,20 @@ macro_rules! unsupported_type { }; } +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 @@ -23,11 +40,11 @@ macro_rules! parse_single_value { format!("wrong number of parameters: {} expected 1", self.req.match_info().len()).as_str())) } else { - let v = self.req.match_info()[0].parse().map_err( - |_| de::value::Error::custom( - format!("can not parse {:?} to a {}", - &self.req.match_info()[0], $tp)))?; - visitor.$visit_fn(v) + 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) } } } @@ -35,11 +52,12 @@ macro_rules! parse_single_value { pub struct PathDeserializer<'de, S: 'de> { req: &'de HttpRequest, + decode: bool, } impl<'de, S: 'de> PathDeserializer<'de, S> { - pub fn new(req: &'de HttpRequest) -> Self { - PathDeserializer { req } + pub fn new(req: &'de HttpRequest, decode: bool) -> Self { + PathDeserializer { req, decode } } } @@ -53,6 +71,7 @@ impl<'de, S: 'de> Deserializer<'de> for PathDeserializer<'de, S> { visitor.visit_map(ParamsDeserializer { params: self.req.match_info().iter(), current: None, + decode: self.decode, }) } @@ -107,6 +126,7 @@ impl<'de, S: 'de> Deserializer<'de> for PathDeserializer<'de, S> { } else { visitor.visit_seq(ParamsSeq { params: self.req.match_info().iter(), + decode: self.decode, }) } } @@ -128,6 +148,7 @@ impl<'de, S: 'de> Deserializer<'de> for PathDeserializer<'de, S> { } else { visitor.visit_seq(ParamsSeq { params: self.req.match_info().iter(), + decode: self.decode, }) } } @@ -141,28 +162,13 @@ impl<'de, S: 'de> Deserializer<'de> for PathDeserializer<'de, S> { Err(de::value::Error::custom("unsupported type: enum")) } - fn deserialize_str(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 { - visitor.visit_str(&self.req.match_info()[0]) - } - } - fn deserialize_seq(self, visitor: V) -> Result where V: Visitor<'de>, { visitor.visit_seq(ParamsSeq { params: self.req.match_info().iter(), + decode: self.decode, }) } @@ -184,13 +190,16 @@ impl<'de, S: 'de> Deserializer<'de> for PathDeserializer<'de, S> { 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> { @@ -212,7 +221,7 @@ impl<'de> de::MapAccess<'de> for ParamsDeserializer<'de> { V: de::DeserializeSeed<'de>, { if let Some((_, value)) = self.current.take() { - seed.deserialize(Value { value }) + seed.deserialize(Value { value, decode: self.decode }) } else { Err(de::value::Error::custom("unexpected item")) } @@ -252,16 +261,18 @@ macro_rules! parse_value { fn $trait_fn(self, visitor: V) -> Result where V: Visitor<'de> { - let v = self.value.parse().map_err( - |_| de::value::Error::custom( - format!("can not parse {:?} to a {}", self.value, $tp)))?; - visitor.$visit_fn(v) + 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> { @@ -377,6 +388,7 @@ impl<'de> Deserializer<'de> for Value<'de> { struct ParamsSeq<'de> { params: ParamsIter<'de>, + decode: bool, } impl<'de> de::SeqAccess<'de> for ParamsSeq<'de> { @@ -387,7 +399,7 @@ impl<'de> de::SeqAccess<'de> for ParamsSeq<'de> { T: de::DeserializeSeed<'de>, { match self.params.next() { - Some(item) => Ok(Some(seed.deserialize(Value { value: item.1 })?)), + Some(item) => Ok(Some(seed.deserialize(Value { value: item.1, decode: self.decode })?)), None => Ok(None), } } diff --git a/src/extractor.rs b/src/extractor.rs index 45e29ace0..717e0f6c1 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -18,7 +18,8 @@ use httpmessage::{HttpMessage, MessageBody, UrlEncoded}; use httprequest::HttpRequest; #[derive(PartialEq, Eq, PartialOrd, Ord)] -/// Extract typed information from the request's path. +/// Extract typed information from the request's path. Information from the path is +/// URL decoded. Decoding of special characters can be disabled through `PathConfig`. /// /// ## Example /// @@ -119,7 +120,7 @@ where let req = req.clone(); let req2 = req.clone(); let err = Rc::clone(&cfg.ehandler); - de::Deserialize::deserialize(PathDeserializer::new(&req)) + de::Deserialize::deserialize(PathDeserializer::new(&req, cfg.decode)) .map_err(move |e| (*err)(e, &req2)) .map(|inner| Path { inner }) } @@ -149,6 +150,7 @@ where /// ``` pub struct PathConfig { ehandler: Rc) -> Error>, + decode: bool, } impl PathConfig { /// Set custom error handler @@ -159,12 +161,20 @@ impl PathConfig { 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, } } } @@ -1090,6 +1100,68 @@ mod tests { 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(); diff --git a/src/param.rs b/src/param.rs index d0664df99..a3f602599 100644 --- a/src/param.rs +++ b/src/param.rs @@ -8,7 +8,7 @@ use http::StatusCode; use smallvec::SmallVec; use error::{InternalError, ResponseError, UriSegmentError}; -use uri::Url; +use uri::{Url, RESERVED_QUOTER}; /// A trait to abstract the idea of creating a new instance of a type from a /// path parameter. @@ -103,6 +103,17 @@ impl Params { } } + /// 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)..] @@ -300,4 +311,24 @@ mod tests { 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/uri.rs b/src/uri.rs index 881cf20a8..c87cb3d5b 100644 --- a/src/uri.rs +++ b/src/uri.rs @@ -1,25 +1,12 @@ use http::Uri; use std::rc::Rc; -#[allow(dead_code)] -const GEN_DELIMS: &[u8] = b":/?#[]@"; -#[allow(dead_code)] -const SUB_DELIMS_WITHOUT_QS: &[u8] = b"!$'()*,"; -#[allow(dead_code)] -const SUB_DELIMS: &[u8] = b"!$'()*,+?=;"; -#[allow(dead_code)] -const RESERVED: &[u8] = b":/?#[]@!$'()*,+?=;"; -#[allow(dead_code)] -const UNRESERVED: &[u8] = b"abcdefghijklmnopqrstuvwxyz - ABCDEFGHIJKLMNOPQRSTUVWXYZ - 1234567890 - -._~"; -const ALLOWED: &[u8] = b"abcdefghijklmnopqrstuvwxyz - ABCDEFGHIJKLMNOPQRSTUVWXYZ - 1234567890 - -._~ - !$'()*,"; -const QS: &[u8] = b"+&=;b"; +// 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 { @@ -32,7 +19,8 @@ fn set_bit(array: &mut [u8], ch: u8) { } lazy_static! { - static ref DEFAULT_QUOTER: Quoter = { Quoter::new(b"@:", b"/+") }; + static ref UNRESERVED_QUOTER: Quoter = { Quoter::new(UNRESERVED) }; + pub(crate) static ref RESERVED_QUOTER: Quoter = { Quoter::new(RESERVED_PLUS_EXTRA) }; } #[derive(Default, Clone, Debug)] @@ -43,7 +31,7 @@ pub(crate) struct Url { impl Url { pub fn new(uri: Uri) -> Url { - let path = DEFAULT_QUOTER.requote(uri.path().as_bytes()); + let path = UNRESERVED_QUOTER.requote(uri.path().as_bytes()); Url { uri, path } } @@ -63,36 +51,19 @@ impl Url { pub(crate) struct Quoter { safe_table: [u8; 16], - protected_table: [u8; 16], } impl Quoter { - pub fn new(safe: &[u8], protected: &[u8]) -> Quoter { + pub fn new(safe: &[u8]) -> Quoter { let mut q = Quoter { safe_table: [0; 16], - protected_table: [0; 16], }; // prepare safe table - for i in 0..128 { - if ALLOWED.contains(&i) { - set_bit(&mut q.safe_table, i); - } - if QS.contains(&i) { - set_bit(&mut q.safe_table, i); - } - } - for ch in safe { set_bit(&mut q.safe_table, *ch) } - // prepare protected table - for ch in protected { - set_bit(&mut q.safe_table, *ch); - set_bit(&mut q.protected_table, *ch); - } - q } @@ -115,19 +86,17 @@ impl Quoter { if let Some(ch) = restore_ch(pct[1], pct[2]) { if ch < 128 { - if bit_at(&self.protected_table, ch) { - buf.extend_from_slice(&pct); - idx += 1; - continue; - } - 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); } - buf.push(ch); } else { buf.extend_from_slice(&pct[..]); } @@ -172,3 +141,37 @@ fn from_hex(v: u8) -> Option { 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/tests/test_handlers.rs b/tests/test_handlers.rs index 3ea709c92..debc1626a 100644 --- a/tests/test_handlers.rs +++ b/tests/test_handlers.rs @@ -672,6 +672,6 @@ fn test_unsafe_path_route() { let bytes = srv.execute(response.body()).unwrap(); assert_eq!( bytes, - Bytes::from_static(b"success: http:%2F%2Fexample.com") + Bytes::from_static(b"success: http%3A%2F%2Fexample.com") ); } From 68c5d6e6d69f14c14c62d03b9e280ebfc320b6e9 Mon Sep 17 00:00:00 2001 From: vemoo Date: Sun, 2 Dec 2018 06:32:55 +0100 Subject: [PATCH 27/59] impl `From>` for `Binary` (#611) impl `From` for `Cow<'static, [u8]>` and `From>` for `Binary` --- src/body.rs | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/body.rs b/src/body.rs index a93db1e92..5487dbba4 100644 --- a/src/body.rs +++ b/src/body.rs @@ -1,5 +1,6 @@ use bytes::{Bytes, BytesMut}; use futures::Stream; +use std::borrow::Cow; use std::sync::Arc; use std::{fmt, mem}; @@ -194,12 +195,30 @@ impl From> for Binary { } } +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))) @@ -287,6 +306,16 @@ mod tests { 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); @@ -307,6 +336,16 @@ mod tests { 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()); From 08c7743bb8431033b180a872f39eb006db0933fd Mon Sep 17 00:00:00 2001 From: Kelly Thomas Kline Date: Thu, 15 Nov 2018 18:59:36 -0800 Subject: [PATCH 28/59] Add set_mailbox_capacity() function --- src/ws/context.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ws/context.rs b/src/ws/context.rs index 4db83df5c..5e207d43e 100644 --- a/src/ws/context.rs +++ b/src/ws/context.rs @@ -231,6 +231,13 @@ where 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 From b1635bc0e6ab116c2ccb684c0440935fe6ac5395 Mon Sep 17 00:00:00 2001 From: silwol Date: Tue, 4 Dec 2018 07:58:22 +0100 Subject: [PATCH 29/59] Update some dependencies (#612) * Update rand to 0.6 * Update parking_lot to 0.7 * Update env_logger to 0.6 --- Cargo.toml | 6 +++--- tests/test_client.rs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e3fbd4e38..37e900515 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,7 +76,7 @@ mime = "0.3" mime_guess = "2.0.0-alpha" num_cpus = "1.0" percent-encoding = "1.0" -rand = "0.5" +rand = "0.6" regex = "1.0" serde = "1.0" serde_json = "1.0" @@ -87,7 +87,7 @@ encoding = "0.2" language-tags = "0.2" lazy_static = "1.0" lazycell = "1.0.0" -parking_lot = "0.6" +parking_lot = "0.7" serde_urlencoded = "^0.5.3" url = { version="1.7", features=["query_encoding"] } cookie = { version="0.11", features=["percent-encode"] } @@ -127,7 +127,7 @@ webpki-roots = { version = "0.15", optional = true } tokio-uds = { version="0.2", optional = true } [dev-dependencies] -env_logger = "0.5" +env_logger = "0.6" serde_derive = "1.0" [build-dependencies] diff --git a/tests/test_client.rs b/tests/test_client.rs index 8c5d5819d..9808f3e6f 100644 --- a/tests/test_client.rs +++ b/tests/test_client.rs @@ -179,7 +179,7 @@ fn test_client_gzip_encoding_large() { #[test] fn test_client_gzip_encoding_large_random() { let data = rand::thread_rng() - .gen_ascii_chars() + .sample_iter(&rand::distributions::Alphanumeric) .take(100_000) .collect::(); @@ -247,7 +247,7 @@ fn test_client_brotli_encoding() { #[test] fn test_client_brotli_encoding_large_random() { let data = rand::thread_rng() - .gen_ascii_chars() + .sample_iter(&rand::distributions::Alphanumeric) .take(70_000) .collect::(); @@ -309,7 +309,7 @@ fn test_client_deflate_encoding() { #[test] fn test_client_deflate_encoding_large_random() { let data = rand::thread_rng() - .gen_ascii_chars() + .sample_iter(&rand::distributions::Alphanumeric) .take(70_000) .collect::(); From 0745a1a9f8d43840454c6aae24df5e2c6f781c36 Mon Sep 17 00:00:00 2001 From: Douman Date: Wed, 5 Dec 2018 03:07:59 -0500 Subject: [PATCH 30/59] Remove usage of upcoming keyword async AsyncResult::async is replaced with AsyncResult::future --- CHANGES.md | 2 ++ MIGRATION.md | 2 ++ src/client/connector.rs | 2 +- src/client/request.rs | 2 +- src/handler.rs | 6 +++--- src/middleware/csrf.rs | 2 +- src/route.rs | 2 +- src/scope.rs | 2 +- src/with.rs | 4 ++-- 9 files changed, 14 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 902a84f69..4d8fa128f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,8 @@ * `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 diff --git a/MIGRATION.md b/MIGRATION.md index 26a314240..6b49e3e6a 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -25,6 +25,8 @@ } ``` +* If you used `AsyncResult::async` you need to replace it with `AsyncResult::future` + ## 0.7.4 diff --git a/src/client/connector.rs b/src/client/connector.rs index 72132bc67..f5affad37 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -942,7 +942,7 @@ impl Handler for ClientConnector { } let host = uri.host().unwrap().to_owned(); - let port = uri.port().unwrap_or_else(|| proto.port()); + let port = uri.port_part().map(|port| port.as_u16()).unwrap_or_else(|| proto.port()); let key = Key { host, port, diff --git a/src/client/request.rs b/src/client/request.rs index 76fb1be59..71da8f74d 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -631,7 +631,7 @@ impl ClientRequestBuilder { if !parts.headers.contains_key(header::HOST) { let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); - let _ = match parts.uri.port() { + 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), }; diff --git a/src/handler.rs b/src/handler.rs index 88210fbc0..6ed93f92e 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -250,7 +250,7 @@ pub(crate) enum AsyncResultItem { impl AsyncResult { /// Create async response #[inline] - pub fn async(fut: Box>) -> AsyncResult { + pub fn future(fut: Box>) -> AsyncResult { AsyncResult(Some(AsyncResultItem::Future(fut))) } @@ -401,7 +401,7 @@ where }, Err(e) => err(e), }); - Ok(AsyncResult::async(Box::new(fut))) + Ok(AsyncResult::future(Box::new(fut))) } } @@ -502,7 +502,7 @@ where Err(e) => Either::A(err(e)), } }); - AsyncResult::async(Box::new(fut)) + AsyncResult::future(Box::new(fut)) } } diff --git a/src/middleware/csrf.rs b/src/middleware/csrf.rs index 02cd150d5..cacfc8d53 100644 --- a/src/middleware/csrf.rs +++ b/src/middleware/csrf.rs @@ -76,7 +76,7 @@ impl ResponseError for CsrfError { } fn uri_origin(uri: &Uri) -> Option { - match (uri.scheme_part(), uri.host(), uri.port()) { + 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)) } diff --git a/src/route.rs b/src/route.rs index e4a7a9572..884a367ed 100644 --- a/src/route.rs +++ b/src/route.rs @@ -57,7 +57,7 @@ impl Route { pub(crate) fn compose( &self, req: HttpRequest, mws: Rc>>>, ) -> AsyncResult { - AsyncResult::async(Box::new(Compose::new(req, mws, self.handler.clone()))) + AsyncResult::future(Box::new(Compose::new(req, mws, self.handler.clone()))) } /// Add match predicate to route. diff --git a/src/scope.rs b/src/scope.rs index 1bddc0e01..fb9e7514a 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -356,7 +356,7 @@ impl RouteHandler for Scope { if self.middlewares.is_empty() { self.router.handle(&req2) } else { - AsyncResult::async(Box::new(Compose::new( + AsyncResult::future(Box::new(Compose::new( req2, Rc::clone(&self.router), Rc::clone(&self.middlewares), diff --git a/src/with.rs b/src/with.rs index c6d54dee8..140e086e1 100644 --- a/src/with.rs +++ b/src/with.rs @@ -86,7 +86,7 @@ where match fut.poll() { Ok(Async::Ready(resp)) => AsyncResult::ok(resp), - Ok(Async::NotReady) => AsyncResult::async(Box::new(fut)), + Ok(Async::NotReady) => AsyncResult::future(Box::new(fut)), Err(e) => AsyncResult::err(e), } } @@ -208,7 +208,7 @@ where match fut.poll() { Ok(Async::Ready(resp)) => AsyncResult::ok(resp), - Ok(Async::NotReady) => AsyncResult::async(Box::new(fut)), + Ok(Async::NotReady) => AsyncResult::future(Box::new(fut)), Err(e) => AsyncResult::err(e), } } From ac9fc662c625f5c6273744b98d804019249f887e Mon Sep 17 00:00:00 2001 From: Douman Date: Wed, 5 Dec 2018 18:27:06 +0300 Subject: [PATCH 31/59] Bump version to 0.7.15 --- CHANGES.md | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4d8fa128f..6092544e9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Changes -## [0.7.15] - 2018-xx-xx +## [0.7.15] - 2018-12-05 ## Changed diff --git a/Cargo.toml b/Cargo.toml index 37e900515..7b8dcec35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "0.7.14" +version = "0.7.15" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" From 86af02156bbaee37ebd2c0f6be95cc53e785e910 Mon Sep 17 00:00:00 2001 From: Akos Vandra Date: Mon, 10 Dec 2018 17:02:05 +0100 Subject: [PATCH 32/59] add impl FromRequest for Either (#618) --- CHANGES.md | 8 +- src/extractor.rs | 195 ++++++++++++++++++++++++++++++++++++++++++++++- src/handler.rs | 2 +- 3 files changed, 202 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6092544e9..11e639a88 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,8 +1,14 @@ # Changes +## [0.7.16] - xxxx-xx-xx + +### Added + +* Implement `FromRequest` extractor for `Either` + ## [0.7.15] - 2018-12-05 -## Changed +### Changed * `ClientConnector::resolver` now accepts `Into` instead of `Addr`. It enables user to implement own resolver. diff --git a/src/extractor.rs b/src/extractor.rs index 717e0f6c1..6f55487bc 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -12,10 +12,11 @@ use serde::de::{self, DeserializeOwned}; use serde_urlencoded; use de::PathDeserializer; -use error::{Error, ErrorBadRequest, ErrorNotFound, UrlencodedError}; +use error::{Error, ErrorBadRequest, ErrorNotFound, UrlencodedError, ErrorConflict}; use handler::{AsyncResult, FromRequest}; use httpmessage::{HttpMessage, MessageBody, UrlEncoded}; use httprequest::HttpRequest; +use Either; #[derive(PartialEq, Eq, PartialOrd, Ord)] /// Extract typed information from the request's path. Information from the path is @@ -634,6 +635,151 @@ where } } +/// 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 + } +} + +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 @@ -874,6 +1020,11 @@ mod tests { hello: String, } + #[derive(Deserialize, Debug, PartialEq)] + struct OtherInfo { + bye: String, + } + #[test] fn test_bytes() { let cfg = PayloadConfig::default(); @@ -977,6 +1128,48 @@ mod tests { } } + #[test] + fn test_either() { + let req = TestRequest::default().finish(); + let mut cfg: EitherConfig, Query, _> = EitherConfig::default(); + + assert!(Either::, Query>::from_request(&req, &cfg).poll().is_err()); + + let req = TestRequest::default().uri("/index?hello=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!(), + } + + 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!(), + } + + let req = TestRequest::default().uri("/index?hello=world&bye=world").finish(); + cfg.collision_strategy = EitherCollisionStrategy::PreferA; + + match Either::, Query>::from_request(&req, &cfg).poll().unwrap() { + Async::Ready(r) => assert_eq!(r, Either::A(Query(Info { hello: "world".into() }))), + _ => unreachable!(), + } + + cfg.collision_strategy = EitherCollisionStrategy::PreferB; + + match Either::, Query>::from_request(&req, &cfg).poll().unwrap() { + Async::Ready(r) => assert_eq!(r, Either::B(Query(OtherInfo { bye: "world".into() }))), + _ => unreachable!(), + } + + cfg.collision_strategy = EitherCollisionStrategy::ErrorA; + assert!(Either::, Query>::from_request(&req, &cfg).poll().is_err()); + + cfg.collision_strategy = EitherCollisionStrategy::FastestSuccessful; + assert!(Either::, Query>::from_request(&req, &cfg).poll().is_ok()); + } + #[test] fn test_result() { let req = TestRequest::with_header( diff --git a/src/handler.rs b/src/handler.rs index 6ed93f92e..c68808181 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -86,7 +86,7 @@ pub trait FromRequest: Sized { /// # fn is_a_variant() -> bool { true } /// # fn main() {} /// ``` -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum Either { /// First branch of the type A(A), From 90eef31cc0ee0304bb1d0eaab7e44aaa6181e99e Mon Sep 17 00:00:00 2001 From: ethanpailes Date: Tue, 11 Dec 2018 11:37:52 -0500 Subject: [PATCH 33/59] impl ResponseError for SendError when possible (#619) --- CHANGES.md | 3 +++ src/error.rs | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 11e639a88..05f831ae3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,9 @@ * Implement `FromRequest` extractor for `Either` +* Implement `ResponseError` for `SendError` + + ## [0.7.15] - 2018-12-05 ### Changed diff --git a/src/error.rs b/src/error.rs index 1766c1523..f4ea981c0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,7 +5,7 @@ use std::string::FromUtf8Error; use std::sync::Mutex; use std::{fmt, io, result}; -use actix::MailboxError; +use actix::{MailboxError, SendError}; use cookie; use failure::{self, Backtrace, Fail}; use futures::Canceled; @@ -136,6 +136,10 @@ pub trait ResponseError: Fail + InternalResponseErrorAsFail { } } +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) From 46db09428c2a38cab4daa8f272b36fb4112ff235 Mon Sep 17 00:00:00 2001 From: Douman Date: Tue, 11 Dec 2018 08:15:07 +0300 Subject: [PATCH 34/59] Prepare release 0.7.16 --- CHANGES.md | 2 +- Cargo.toml | 4 ++-- src/client/connector.rs | 4 ++-- src/client/mod.rs | 6 ++++-- src/client/pipeline.rs | 3 ++- src/client/request.rs | 3 ++- src/extractor.rs | 4 +++- src/httpmessage.rs | 3 ++- src/lib.rs | 16 +++++++--------- src/middleware/session.rs | 3 ++- src/server/http.rs | 3 ++- src/server/mod.rs | 3 ++- src/test.rs | 2 +- src/ws/mod.rs | 3 ++- 14 files changed, 34 insertions(+), 25 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 05f831ae3..7965bc75c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Changes -## [0.7.16] - xxxx-xx-xx +## [0.7.16] - 2018-12-11 ### Added diff --git a/Cargo.toml b/Cargo.toml index 7b8dcec35..80261110c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "0.7.15" +version = "0.7.16" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" @@ -61,7 +61,7 @@ flate2-rust = ["flate2/rust_backend"] cell = ["actix-net/cell"] [dependencies] -actix = "0.7.7" +actix = "0.7.9" actix-net = "0.2.2" askama_escape = "0.1.0" diff --git a/src/client/connector.rs b/src/client/connector.rs index f5affad37..1d0623023 100644 --- a/src/client/connector.rs +++ b/src/client/connector.rs @@ -3,8 +3,8 @@ use std::net::Shutdown; use std::time::{Duration, Instant}; use std::{fmt, io, mem, time}; -use actix::resolver::{Connect as ResolveConnect, Resolver, ResolverError}; -use actix::{ +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, diff --git a/src/client/mod.rs b/src/client/mod.rs index a0713fe32..7696efa97 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -2,11 +2,12 @@ //! //! ```rust //! # extern crate actix_web; +//! # extern crate actix; //! # extern crate futures; //! # extern crate tokio; //! # use futures::Future; //! # use std::process; -//! use actix_web::{actix, client}; +//! use actix_web::client; //! //! fn main() { //! actix::run( @@ -61,12 +62,13 @@ impl ResponseError for SendRequestError { /// /// ```rust /// # extern crate actix_web; +/// # extern crate actix; /// # extern crate futures; /// # extern crate tokio; /// # extern crate env_logger; /// # use futures::Future; /// # use std::process; -/// use actix_web::{actix, client}; +/// use actix_web::client; /// /// fn main() { /// actix::run( diff --git a/src/client/pipeline.rs b/src/client/pipeline.rs index 394b7a6cd..1dbd2e171 100644 --- a/src/client/pipeline.rs +++ b/src/client/pipeline.rs @@ -6,7 +6,8 @@ use std::time::{Duration, Instant}; use std::{io, mem}; use tokio_timer::Delay; -use actix::{Addr, Request, SystemService}; +use actix_inner::dev::Request; +use actix::{Addr, SystemService}; use super::{ ClientConnector, ClientConnectorError, ClientRequest, ClientResponse, Connect, diff --git a/src/client/request.rs b/src/client/request.rs index 71da8f74d..ad08ad135 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -27,11 +27,12 @@ use httprequest::HttpRequest; /// /// ```rust /// # extern crate actix_web; +/// # extern crate actix; /// # extern crate futures; /// # extern crate tokio; /// # use futures::Future; /// # use std::process; -/// use actix_web::{actix, client}; +/// use actix_web::client; /// /// fn main() { /// actix::run( diff --git a/src/extractor.rs b/src/extractor.rs index 6f55487bc..3c64de9e1 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -751,7 +751,6 @@ pub enum EitherCollisionStrategy { 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 @@ -764,6 +763,9 @@ impl Default for EitherCollisionStrategy { } } +///Determines Either extractor configuration +/// +///By default `EitherCollisionStrategy::FastestSuccessful` is used. pub struct EitherConfig where A: FromRequest, B: FromRequest { a: A::Config, b: B::Config, diff --git a/src/httpmessage.rs b/src/httpmessage.rs index 60f77b07e..e96dce48c 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -213,9 +213,10 @@ pub trait HttpMessage: Sized { /// # extern crate actix_web; /// # extern crate env_logger; /// # extern crate futures; + /// # extern crate actix; /// # use std::str; /// # use actix_web::*; - /// # use actix_web::actix::fut::FinishStream; + /// # use actix::FinishStream; /// # use futures::{Future, Stream}; /// # use futures::future::{ok, result, Either}; /// fn index(mut req: HttpRequest) -> Box> { diff --git a/src/lib.rs b/src/lib.rs index f8326886f..21515051e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -217,14 +217,12 @@ pub use server::Request; pub mod actix { //! Re-exports [actix's](https://docs.rs/actix/) prelude - - extern crate actix; - pub use self::actix::actors::resolver; - pub use self::actix::actors::signal; - pub use self::actix::fut; - pub use self::actix::msgs; - pub use self::actix::prelude::*; - pub use self::actix::{run, spawn}; + 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}; } #[cfg(feature = "openssl")] @@ -255,7 +253,7 @@ pub mod dev { pub use body::BodyStream; pub use context::Drain; - pub use extractor::{FormConfig, PayloadConfig, QueryConfig, PathConfig}; + pub use extractor::{FormConfig, PayloadConfig, QueryConfig, PathConfig, EitherConfig, EitherCollisionStrategy}; pub use handler::{AsyncResult, Handler}; pub use httpmessage::{MessageBody, Readlines, UrlEncoded}; pub use httpresponse::HttpResponseBuilder; diff --git a/src/middleware/session.rs b/src/middleware/session.rs index e8b0e5558..0271a13f8 100644 --- a/src/middleware/session.rs +++ b/src/middleware/session.rs @@ -33,7 +33,8 @@ //! //! ```rust //! # extern crate actix_web; -//! use actix_web::{actix, server, App, HttpRequest, Result}; +//! # 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> { diff --git a/src/server/http.rs b/src/server/http.rs index 0bec8be3f..76049981f 100644 --- a/src/server/http.rs +++ b/src/server/http.rs @@ -457,7 +457,8 @@ impl H + Send + Clone> HttpServer { /// /// ```rust /// extern crate actix_web; - /// use actix_web::{actix, server, App, HttpResponse}; + /// extern crate actix; + /// use actix_web::{server, App, HttpResponse}; /// /// fn main() { /// let sys = actix::System::new("example"); // <- create Actix system diff --git a/src/server/mod.rs b/src/server/mod.rs index 0a16f26b9..b84986047 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -166,7 +166,8 @@ const HW_BUFFER_SIZE: usize = 32_768; /// /// ```rust /// # extern crate actix_web; -/// use actix_web::{actix, server, App, HttpResponse}; +/// # extern crate actix; +/// use actix_web::{server, App, HttpResponse}; /// /// fn main() { /// let sys = actix::System::new("example"); // <- create Actix system diff --git a/src/test.rs b/src/test.rs index d0cfb255a..d543937c0 100644 --- a/src/test.rs +++ b/src/test.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use std::sync::mpsc; use std::{net, thread}; -use actix_inner::{Actor, Addr, System}; +use actix::{Actor, Addr, System}; use cookie::Cookie; use futures::Future; diff --git a/src/ws/mod.rs b/src/ws/mod.rs index c16f8d6d2..b0942c0d3 100644 --- a/src/ws/mod.rs +++ b/src/ws/mod.rs @@ -8,7 +8,8 @@ //! //! ```rust //! # extern crate actix_web; -//! # use actix_web::actix::*; +//! # extern crate actix; +//! # use actix::prelude::*; //! # use actix_web::*; //! use actix_web::{ws, HttpRequest, HttpResponse}; //! From e8bdcb1c08967431c825751e5c110b37ecde512f Mon Sep 17 00:00:00 2001 From: Douman Date: Sat, 15 Dec 2018 09:26:56 +0300 Subject: [PATCH 35/59] Update min version of http Closes #630 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 80261110c..cd13e341c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,7 @@ base64 = "0.10" bitflags = "1.0" failure = "^0.1.2" h2 = "0.1" -http = "^0.1.8" +http = "^0.1.14" httparse = "1.3" log = "0.4" mime = "0.3" From 1a940d4c18d7ab234800857817f087c9121272b1 Mon Sep 17 00:00:00 2001 From: Douman Date: Sun, 16 Dec 2018 13:46:11 +0300 Subject: [PATCH 36/59] H1 decoded should ignore header cases --- CHANGES.md | 6 ++++++ src/server/h1.rs | 33 +++++++++++++++++++++++++++++++++ src/server/h1decoder.rs | 18 +++++++----------- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7965bc75c..45faf35b5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Changes +## [0.7.17] - 2018-xx-xx + +### Fixed + +* HTTP1 decoder should perform case-insentive comparison for client requests (e.g. `Keep-Alive`). #631 + ## [0.7.16] - 2018-12-11 ### Added diff --git a/src/server/h1.rs b/src/server/h1.rs index f491ba597..f0edefae2 100644 --- a/src/server/h1.rs +++ b/src/server/h1.rs @@ -942,6 +942,14 @@ mod tests { 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] @@ -953,10 +961,26 @@ mod tests { 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", @@ -1009,6 +1033,15 @@ mod tests { 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] diff --git a/src/server/h1decoder.rs b/src/server/h1decoder.rs index 10f7e68a0..80b49983b 100644 --- a/src/server/h1decoder.rs +++ b/src/server/h1decoder.rs @@ -157,23 +157,19 @@ impl H1Decoder { } // transfer-encoding header::TRANSFER_ENCODING => { - if let Ok(s) = value.to_str() { - chunked = s.to_lowercase().contains("chunked"); + 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() { - if version == Version::HTTP_10 - && conn.contains("keep-alive") - { + 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 - .contains("close") - || conn.contains("upgrade")) + version == Version::HTTP_11 && !(conn.eq_ignore_ascii_case("close") || conn.eq_ignore_ascii_case("upgrade")) } } else { false @@ -184,8 +180,8 @@ impl H1Decoder { has_upgrade = true; // check content-length, some clients (dart) // sends "content-length: 0" with websocket upgrade - if let Ok(val) = value.to_str() { - if val == "websocket" { + if let Ok(val) = value.to_str().map(|val| val.trim()) { + if val.eq_ignore_ascii_case("websocket") { content_length = None; } } From e9fe3879df46fd066ca4bd3abd27919a0eadbfbf Mon Sep 17 00:00:00 2001 From: Phil Booth Date: Tue, 18 Dec 2018 17:53:03 +0000 Subject: [PATCH 37/59] Support custom content types in JsonConfig --- CHANGES.md | 4 +++ src/httpmessage.rs | 2 +- src/json.rs | 82 ++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 84 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 45faf35b5..232d85b8d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,10 @@ ## [0.7.17] - 2018-xx-xx +### Added + +* Support for custom content types in `JsonConfig`. #637 + ### Fixed * HTTP1 decoder should perform case-insentive comparison for client requests (e.g. `Keep-Alive`). #631 diff --git a/src/httpmessage.rs b/src/httpmessage.rs index e96dce48c..ea5e4d862 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -200,7 +200,7 @@ pub trait HttpMessage: Sized { /// # fn main() {} /// ``` fn json(&self) -> JsonBody { - JsonBody::new(self) + JsonBody::new::<()>(self, None) } /// Return stream to http payload processes as multipart. diff --git a/src/json.rs b/src/json.rs index 178143f11..b04cad2fb 100644 --- a/src/json.rs +++ b/src/json.rs @@ -143,7 +143,7 @@ where let req2 = req.clone(); let err = Rc::clone(&cfg.ehandler); Box::new( - JsonBody::new(req) + JsonBody::new(req, Some(cfg)) .limit(cfg.limit) .map_err(move |e| (*err)(e, &req2)) .map(Json), @@ -155,6 +155,7 @@ where /// /// ```rust /// # extern crate actix_web; +/// extern crate mime; /// #[macro_use] extern crate serde_derive; /// use actix_web::{error, http, App, HttpResponse, Json, Result}; /// @@ -173,6 +174,9 @@ where /// 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() @@ -184,6 +188,7 @@ where pub struct JsonConfig { limit: usize, ehandler: Rc) -> Error>, + content_type: Option bool>>, } impl JsonConfig { @@ -201,6 +206,15 @@ impl JsonConfig { 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 { @@ -208,6 +222,7 @@ impl Default for JsonConfig { JsonConfig { limit: 262_144, ehandler: Rc::new(|e, _| e.into()), + content_type: None, } } } @@ -217,6 +232,7 @@ impl Default for JsonConfig { /// Returns error: /// /// * content type is not `application/json` +/// (unless specified in [`JsonConfig`](struct.JsonConfig.html)) /// * content length is greater than 256k /// /// # Server example @@ -253,10 +269,13 @@ pub struct JsonBody { impl JsonBody { /// Create `JsonBody` for request. - pub fn new(req: &T) -> Self { + 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) + 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 }; @@ -440,4 +459,61 @@ mod tests { .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()) + } } From 477bf0d8ae0b22a4ac2d1f52d0cdcd91599546bd Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 23 Dec 2018 10:19:12 -0800 Subject: [PATCH 38/59] Send HTTP/1.1 100 Continue if request contains expect: continue header #634 --- CHANGES.md | 4 +++- Cargo.toml | 6 +++--- src/server/h1.rs | 46 ++++++++++++++++++++++++++++++----------- src/server/h1decoder.rs | 31 ++++++++++++++++++++------- 4 files changed, 64 insertions(+), 23 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 232d85b8d..bdc50fc51 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,11 +1,13 @@ # Changes -## [0.7.17] - 2018-xx-xx +## [0.7.17] - 2018-12-23 ### 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 diff --git a/Cargo.toml b/Cargo.toml index cd13e341c..32ca147c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "0.7.16" +version = "0.7.17" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" @@ -62,7 +62,7 @@ cell = ["actix-net/cell"] [dependencies] actix = "0.7.9" -actix-net = "0.2.2" +actix-net = "0.2.6" askama_escape = "0.1.0" base64 = "0.10" @@ -105,7 +105,7 @@ slab = "0.4" tokio = "0.1" tokio-io = "0.1" tokio-tcp = "0.1" -tokio-timer = "0.2" +tokio-timer = "0.2.8" tokio-reactor = "0.1" tokio-current-thread = "0.1" diff --git a/src/server/h1.rs b/src/server/h1.rs index f0edefae2..fa7d2fda5 100644 --- a/src/server/h1.rs +++ b/src/server/h1.rs @@ -7,6 +7,7 @@ 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}; @@ -50,32 +51,40 @@ pub struct Http1Dispatcher { } enum Entry { - Task(H::Task), + Task(H::Task, Option<()>), Error(Box), } impl Entry { fn into_task(self) -> H::Task { match self { - Entry::Task(task) => task, + Entry::Task(task, _) => task, Entry::Error(_) => panic!(), } } fn disconnected(&mut self) { match *self { - Entry::Task(ref mut task) => task.disconnected(), + 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) => task.poll_io(io), + 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::Task(ref mut task, _) => task.poll_completed(), Entry::Error(ref mut task) => task.poll_completed(), } } @@ -463,7 +472,11 @@ where 'outer: loop { match self.decoder.decode(&mut self.buf, &self.settings) { - Ok(Some(Message::Message { mut msg, payload })) => { + Ok(Some(Message::Message { + mut msg, + mut expect, + payload, + })) => { updated = true; self.flags.insert(Flags::STARTED); @@ -484,6 +497,12 @@ where 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 @@ -510,7 +529,10 @@ where } } } - self.tasks.push_back(Entry::Task(task)); + self.tasks.push_back(Entry::Task( + task, + if expect { Some(()) } else { None }, + )); continue 'outer; } Err(_) => { @@ -608,13 +630,13 @@ mod tests { impl Message { fn message(self) -> Request { match self { - Message::Message { msg, payload: _ } => msg, + Message::Message { msg, .. } => msg, _ => panic!("error"), } } fn is_payload(&self) -> bool { match *self { - Message::Message { msg: _, payload } => payload, + Message::Message { payload, .. } => payload, _ => panic!("error"), } } @@ -874,13 +896,13 @@ mod tests { let settings = wrk_settings(); let mut reader = H1Decoder::new(); - assert!{ reader.decode(&mut buf, &settings).unwrap().is_none() } + assert! { reader.decode(&mut buf, &settings).unwrap().is_none() } buf.extend(b"t"); - assert!{ reader.decode(&mut buf, &settings).unwrap().is_none() } + assert! { reader.decode(&mut buf, &settings).unwrap().is_none() } buf.extend(b"es"); - assert!{ reader.decode(&mut buf, &settings).unwrap().is_none() } + assert! { reader.decode(&mut buf, &settings).unwrap().is_none() } buf.extend(b"t: value\r\n\r\n"); match reader.decode(&mut buf, &settings) { diff --git a/src/server/h1decoder.rs b/src/server/h1decoder.rs index 80b49983b..ece6b3cce 100644 --- a/src/server/h1decoder.rs +++ b/src/server/h1decoder.rs @@ -20,7 +20,11 @@ pub(crate) struct H1Decoder { #[derive(Debug)] pub(crate) enum Message { - Message { msg: Request, payload: bool }, + Message { + msg: Request, + payload: bool, + expect: bool, + }, Chunk(Bytes), Eof, } @@ -63,10 +67,11 @@ impl H1Decoder { .parse_message(src, settings) .map_err(DecoderError::Error)? { - Async::Ready((msg, decoder)) => { + Async::Ready((msg, expect, decoder)) => { self.decoder = decoder; Ok(Some(Message::Message { msg, + expect, payload: self.decoder.is_some(), })) } @@ -85,11 +90,12 @@ impl H1Decoder { &self, buf: &mut BytesMut, settings: &ServiceConfig, - ) -> Poll<(Request, Option), ParseError> { + ) -> 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. @@ -165,11 +171,17 @@ impl H1Decoder { } // 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") { + 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")) + version == Version::HTTP_11 + && !(conn.eq_ignore_ascii_case("close") + || conn.eq_ignore_ascii_case("upgrade")) } } else { false @@ -186,6 +198,11 @@ impl H1Decoder { } } } + header::EXPECT => { + if value == "100-continue" { + expect_continue = true + } + } _ => (), } @@ -216,7 +233,7 @@ impl H1Decoder { None }; - Ok(Async::Ready((msg, decoder))) + Ok(Async::Ready((msg, expect_continue, decoder))) } } From bfdf762062e936aabfb068e7a0575adbfb407ecb Mon Sep 17 00:00:00 2001 From: BlueC0re Date: Mon, 24 Dec 2018 19:16:07 +0100 Subject: [PATCH 39/59] Only return a single Origin value (#644) Only return a single origin if matched. --- CHANGES.md | 1 + src/middleware/cors.rs | 76 +++++++++++++++++++++++++++++++++--------- 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index bdc50fc51..89c85b982 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,7 @@ ### 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 diff --git a/src/middleware/cors.rs b/src/middleware/cors.rs index 953f2911c..386d00078 100644 --- a/src/middleware/cors.rs +++ b/src/middleware/cors.rs @@ -442,11 +442,23 @@ impl Middleware for Cors { .insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.clone()); } } - AllOrSome::Some(_) => { - resp.headers_mut().insert( - header::ACCESS_CONTROL_ALLOW_ORIGIN, - self.inner.origins_str.as_ref().unwrap().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() + ); + }; } } @@ -1134,17 +1146,10 @@ mod tests { .to_str() .unwrap(); - if origins_str.starts_with("https://www.example.com") { - assert_eq!( - "https://www.example.com, https://www.google.com", - origins_str - ); - } else { - assert_eq!( - "https://www.google.com, https://www.example.com", - origins_str - ); - } + assert_eq!( + "https://www.example.com", + origins_str + ); } #[test] @@ -1180,4 +1185,43 @@ mod tests { 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() + ); + } } From 037a1c6a24d916ac0f0eb02c40c6b4bb72a5d858 Mon Sep 17 00:00:00 2001 From: Douman Date: Mon, 24 Dec 2018 21:17:09 +0300 Subject: [PATCH 40/59] Bump min version of rustc Due to actix & trust-dns requirement --- CHANGES.md | 3 ++- README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 89c85b982..ed15d7876 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Changes -## [0.7.17] - 2018-12-23 +## [0.7.17] - 2018-12-xx ### Added @@ -11,6 +11,7 @@ ### 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 diff --git a/README.md b/README.md index db3cc68c5..c7e195de9 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Actix web is a simple, pragmatic and extremely fast web framework for Rust. * [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.26 or later +* Minimum supported Rust version: 1.31 or later ## Example From 799c6eb71991daef3e08732f33f3f4c98fde3b39 Mon Sep 17 00:00:00 2001 From: Douman Date: Tue, 25 Dec 2018 16:28:36 +0300 Subject: [PATCH 41/59] 0.7.17 Bump --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index ed15d7876..1c55f2ac7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Changes -## [0.7.17] - 2018-12-xx +## [0.7.17] - 2018-12-25 ### Added From 61883042c2ff103b9fcf2aa94e66c0f3a50ef45c Mon Sep 17 00:00:00 2001 From: Ji Qu Date: Wed, 2 Jan 2019 18:24:08 +0800 Subject: [PATCH 42/59] Add with-cookie init-method for TestRequest (#647) --- CHANGES.md | 7 +++++++ src/test.rs | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 1c55f2ac7..a07388d3f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,12 @@ # Changes +## [0.7.18] - 2019-01-02 + +### 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. + ## [0.7.17] - 2018-12-25 ### Added diff --git a/src/test.rs b/src/test.rs index d543937c0..7039c4fce 100644 --- a/src/test.rs +++ b/src/test.rs @@ -507,6 +507,11 @@ 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 { @@ -543,6 +548,25 @@ impl TestRequest { self } + /// set cookie of this request + pub fn cookie(mut self, cookie: Cookie<'static>) -> Self { + if self.cookies.is_none() { + 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() { From 55a2a59906c05f834fe1278fafb8a8dd5c746510 Mon Sep 17 00:00:00 2001 From: Juan Aguilar Date: Thu, 3 Jan 2019 20:34:18 +0100 Subject: [PATCH 43/59] Improve change askama_escape in favor of v_htmlescape (#651) --- Cargo.toml | 2 +- src/fs.rs | 7 ++++++- src/lib.rs | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 32ca147c0..eb2e1cff9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,7 @@ cell = ["actix-net/cell"] actix = "0.7.9" actix-net = "0.2.6" -askama_escape = "0.1.0" +v_htmlescape = "0.3.2" base64 = "0.10" bitflags = "1.0" failure = "^0.1.2" diff --git a/src/fs.rs b/src/fs.rs index aec058aaf..05024da85 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -11,7 +11,7 @@ use std::{cmp, io}; #[cfg(unix)] use std::os::unix::fs::MetadataExt; -use askama_escape::{escape as escape_html_entity}; +use v_htmlescape::HTMLEscape; use bytes::Bytes; use futures::{Async, Future, Poll, Stream}; use futures_cpupool::{CpuFuture, CpuPool}; @@ -569,6 +569,11 @@ macro_rules! encode_file_url { }; } +#[inline] +fn escape_html_entity(s: &str) -> HTMLEscape { + HTMLEscape::from(s) +} + // " -- " & -- & ' -- ' < -- < > -- > / -- / macro_rules! encode_file_name { ($entry:ident) => { diff --git a/src/lib.rs b/src/lib.rs index 21515051e..3b00cda16 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,7 +100,6 @@ extern crate failure; extern crate lazy_static; #[macro_use] extern crate futures; -extern crate askama_escape; extern crate cookie; extern crate futures_cpupool; extern crate http as modhttp; @@ -137,6 +136,7 @@ extern crate serde_urlencoded; extern crate percent_encoding; extern crate serde_json; extern crate smallvec; +extern crate v_htmlescape; extern crate actix_net; #[macro_use] From 4d45313f9d21c5b9835f5effc260c54a4507ce84 Mon Sep 17 00:00:00 2001 From: Douman Date: Tue, 8 Jan 2019 10:46:58 +0300 Subject: [PATCH 44/59] Decode special characters when handling static files --- src/fs.rs | 23 ++++++++++++++++++++++- tests/test space.binary | 1 + 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 tests/test space.binary diff --git a/src/fs.rs b/src/fs.rs index 05024da85..47bd81a78 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -761,7 +761,7 @@ impl StaticFiles { &self, req: &HttpRequest, ) -> Result, Error> { - let tail: String = req.match_info().query("tail")?; + let tail: String = req.match_info().get_decoded("tail").unwrap_or_else(|| "".to_string()); let relpath = PathBuf::from_param(tail.trim_left_matches('/'))?; // full filepath @@ -1303,6 +1303,27 @@ mod tests { 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 { diff --git a/tests/test space.binary b/tests/test space.binary new file mode 100644 index 000000000..ef8ff0245 --- /dev/null +++ b/tests/test space.binary @@ -0,0 +1 @@ +TǑɂV2vI\R˙evD:藽RVYp;Gp!2C. pA !ߦx j+UcXc%;"yAI \ No newline at end of file From 4f2e9707325d02401bdeeb119b0ca12b91135469 Mon Sep 17 00:00:00 2001 From: Douman Date: Tue, 8 Jan 2019 10:49:03 +0300 Subject: [PATCH 45/59] Tidy up CHANGES.md --- CHANGES.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a07388d3f..7c02acbf1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,11 +1,16 @@ # Changes -## [0.7.18] - 2019-01-02 +## [0.7.18] - 2019-xx-xx ### 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. + +* Add `cookie` method for `TestRequest` to allow users to add cookie dynamically. + +### Fixed + +* StaticFiles decode special characters in request's path ## [0.7.17] - 2018-12-25 From e5cdd22720b6273114f153c47b78a08ce0ef9063 Mon Sep 17 00:00:00 2001 From: Julian Tescher Date: Tue, 8 Jan 2019 10:42:22 -0800 Subject: [PATCH 46/59] Fix test server listener thread leak (#655) --- CHANGES.md | 2 ++ src/test.rs | 22 +++++++++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7c02acbf1..84ee4b7cc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,8 @@ * StaticFiles decode special characters in request's path +* Fix test server listener leak #654 + ## [0.7.17] - 2018-12-25 ### Added diff --git a/src/test.rs b/src/test.rs index 7039c4fce..f2346115c 100644 --- a/src/test.rs +++ b/src/test.rs @@ -5,7 +5,9 @@ use std::sync::mpsc; use std::{net, thread}; use actix::{Actor, Addr, System}; +use actix::actors::signal; +use actix_net::server::Server; use cookie::Cookie; use futures::Future; use http::header::HeaderName; @@ -66,6 +68,7 @@ pub struct TestServer { ssl: bool, conn: Addr, rt: Runtime, + backend: Addr, } impl TestServer { @@ -112,24 +115,25 @@ impl TestServer { let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); let local_addr = tcp.local_addr().unwrap(); - let _ = HttpServer::new(factory) + let srv = HttpServer::new(factory) .disable_signals() .listen(tcp) .keep_alive(5) .start(); - tx.send((System::current(), local_addr, TestServer::get_conn())) + tx.send((System::current(), local_addr, TestServer::get_conn(), srv)) .unwrap(); sys.run(); }); - let (system, addr, conn) = rx.recv().unwrap(); + let (system, addr, conn, backend) = rx.recv().unwrap(); System::set_current(system); TestServer { addr, conn, ssl: false, rt: Runtime::new().unwrap(), + backend, } } @@ -197,6 +201,7 @@ impl TestServer { /// Stop http server fn stop(&mut self) { + let _ = self.backend.send(signal::Signal(signal::SignalType::Term)).wait(); System::current().stop(); } @@ -333,8 +338,7 @@ where .keep_alive(5) .disable_signals(); - tx.send((System::current(), addr, TestServer::get_conn())) - .unwrap(); + #[cfg(any(feature = "alpn", feature = "ssl"))] { @@ -356,18 +360,22 @@ where let tcp = net::TcpListener::bind(addr).unwrap(); srv = srv.listen(tcp); } - srv.start(); + let backend = srv.start(); + + tx.send((System::current(), addr, TestServer::get_conn(), backend)) + .unwrap(); sys.run(); }); - let (system, addr, conn) = rx.recv().unwrap(); + let (system, addr, conn, backend) = rx.recv().unwrap(); System::set_current(system); TestServer { addr, conn, ssl: has_ssl, rt: Runtime::new().unwrap(), + backend, } } } From 1fbb52ad3be39add2811f2a79be4dbf2fe63f68b Mon Sep 17 00:00:00 2001 From: Douman Date: Thu, 10 Jan 2019 17:05:18 +0300 Subject: [PATCH 47/59] 0.7.18 Bump --- CHANGES.md | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 84ee4b7cc..e40bad5bd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Changes -## [0.7.18] - 2019-xx-xx +## [0.7.18] - 2019-01-10 ### Added diff --git a/Cargo.toml b/Cargo.toml index eb2e1cff9..f927e24e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "0.7.17" +version = "0.7.18" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" From d6df2e33999c01e5346705a51fc404479b9d4b4b Mon Sep 17 00:00:00 2001 From: Sameer Puri <11097096+sameer@users.noreply.github.com> Date: Thu, 10 Jan 2019 16:26:01 -0600 Subject: [PATCH 48/59] Fix HttpResponse doc spelling "os" to "of" --- src/httpresponse.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/httpresponse.rs b/src/httpresponse.rs index 52dd8046b..168e9bf64 100644 --- a/src/httpresponse.rs +++ b/src/httpresponse.rs @@ -246,7 +246,7 @@ impl HttpResponse { self } - /// Get body os this response + /// Get body of this response #[inline] pub fn body(&self) -> &Body { &self.get_ref().body From 3431fff4d7f2c39576f8c6070df09f169abf12a8 Mon Sep 17 00:00:00 2001 From: rishflab Date: Mon, 14 Jan 2019 14:13:37 +1100 Subject: [PATCH 49/59] Fixed example in client documentation. This closes #665. --- src/client/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index 7696efa97..5321e4b05 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -5,9 +5,9 @@ //! # extern crate actix; //! # extern crate futures; //! # extern crate tokio; -//! # use futures::Future; //! # use std::process; //! use actix_web::client; +//! use futures::Future; //! //! fn main() { //! actix::run( @@ -66,9 +66,9 @@ impl ResponseError for SendRequestError { /// # extern crate futures; /// # extern crate tokio; /// # extern crate env_logger; -/// # use futures::Future; /// # use std::process; /// use actix_web::client; +/// use futures::Future; /// /// fn main() { /// actix::run( From a534fdd1257f5b5625fe6afdea356bdaf6ae76f0 Mon Sep 17 00:00:00 2001 From: Neil Jensen Date: Sat, 19 Jan 2019 10:41:48 -0700 Subject: [PATCH 50/59] Add io handling for ECONNRESET when data has already been received --- src/server/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/mod.rs b/src/server/mod.rs index b84986047..641298542 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -303,6 +303,8 @@ pub trait IoStream: AsyncRead + AsyncWrite + 'static { } else { Ok(Async::NotReady) } + } else if e.kind() == io::ErrorKind::ConnectionReset && read_some { + Ok(Async::Ready((read_some, true))) } else { Err(e) }; From f5bec968c754d7777fa69a1258c49f6f3ca07cbd Mon Sep 17 00:00:00 2001 From: Tomas Izquierdo Garcia-Faria Date: Fri, 25 Jan 2019 02:35:11 +0100 Subject: [PATCH 51/59] Bump v_htmlescape version to 0.4 --- Cargo.toml | 2 +- src/fs.rs | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f927e24e8..bd3cb306c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,7 @@ cell = ["actix-net/cell"] actix = "0.7.9" actix-net = "0.2.6" -v_htmlescape = "0.3.2" +v_htmlescape = "0.4" base64 = "0.10" bitflags = "1.0" failure = "^0.1.2" diff --git a/src/fs.rs b/src/fs.rs index 47bd81a78..b7370c64c 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -11,7 +11,7 @@ use std::{cmp, io}; #[cfg(unix)] use std::os::unix::fs::MetadataExt; -use v_htmlescape::HTMLEscape; +use v_htmlescape::escape as escape_html_entity; use bytes::Bytes; use futures::{Async, Future, Poll, Stream}; use futures_cpupool::{CpuFuture, CpuPool}; @@ -569,11 +569,6 @@ macro_rules! encode_file_url { }; } -#[inline] -fn escape_html_entity(s: &str) -> HTMLEscape { - HTMLEscape::from(s) -} - // " -- " & -- & ' -- ' < -- < > -- > / -- / macro_rules! encode_file_name { ($entry:ident) => { From 9968afe4a6512611497ca57e5ab2fbd5fdbdfac2 Mon Sep 17 00:00:00 2001 From: wildarch Date: Mon, 28 Jan 2019 06:07:28 +0100 Subject: [PATCH 52/59] Use NamedFile with an existing File (#670) --- CHANGES.md | 6 ++++++ src/fs.rs | 63 ++++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e40bad5bd..83803abb0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Changes +## [x.x.xx] - xxxx-xx-xx + +### Added + +* Add `from_file` and `from_file_with_config` to `NamedFile` to allow sending files without a known path. #670 + ## [0.7.18] - 2019-01-10 ### Added diff --git a/src/fs.rs b/src/fs.rs index b7370c64c..04ababd0d 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -120,6 +120,32 @@ pub struct NamedFile { } 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 + /// + /// ```no_run + /// 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 @@ -135,16 +161,29 @@ impl NamedFile { } impl NamedFile { - /// Attempts to open a file in read-only mode using provided configiration. + /// 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 - /// use actix_web::fs::{DefaultConfig, NamedFile}; + /// ```no_run + /// extern crate actix_web; /// - /// let file = NamedFile::open_with_config("foo.txt", DefaultConfig); + /// 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 open_with_config>(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 @@ -169,7 +208,6 @@ impl NamedFile { (ct, cd) }; - let file = File::open(&path)?; let md = file.metadata()?; let modified = md.modified().ok(); let cpu_pool = None; @@ -188,6 +226,19 @@ impl NamedFile { }) } + /// Attempts to open a file in read-only mode using provided configuration. + /// + /// # Examples + /// + /// ```rust + /// 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 { From 346d85a8848a771a324dddec056c8c7e370a2356 Mon Sep 17 00:00:00 2001 From: Vladislav Stepanov <8uk.8ak@gmail.com> Date: Mon, 4 Feb 2019 13:20:46 +0300 Subject: [PATCH 53/59] Serve static file directly instead of redirecting (#676) --- src/fs.rs | 126 +++++++++++++++++++++++++++++------------------------- 1 file changed, 68 insertions(+), 58 deletions(-) diff --git a/src/fs.rs b/src/fs.rs index 04ababd0d..dcf6c539a 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -790,7 +790,7 @@ impl StaticFiles { /// Set index file /// - /// Redirects to specific index file for directory "/" instead of + /// 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()); @@ -815,17 +815,11 @@ impl StaticFiles { if path.is_dir() { if let Some(ref redir_index) = self.index { - // TODO: Don't redirect, just return the index content. - // TODO: It'd be nice if there were a good usable URL manipulation - // library - let mut new_path: String = req.path().to_owned(); - if !new_path.ends_with('/') { - new_path.push('/'); - } - new_path.push_str(redir_index); - HttpResponse::Found() - .header(header::LOCATION, new_path.as_str()) - .finish() + 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) } else if self.show_index { let dir = Directory::new(self.directory.clone(), path); @@ -1482,43 +1476,66 @@ mod tests { } #[test] - fn test_redirect_to_index() { - let st = StaticFiles::new(".").unwrap().index_file("index.html"); + 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::FOUND); + assert_eq!(resp.status(), StatusCode::OK); assert_eq!( - resp.headers().get(header::LOCATION).unwrap(), - "/tests/index.html" + 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::FOUND); + assert_eq!(resp.status(), StatusCode::OK); assert_eq!( - resp.headers().get(header::LOCATION).unwrap(), - "/tests/index.html" + 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_redirect_to_index_nested() { + 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::FOUND); + assert_eq!(resp.status(), StatusCode::OK); assert_eq!( - resp.headers().get(header::LOCATION).unwrap(), - "/src/client/mod.rs" + 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_redirect_to_index_with_prefix() { + fn integration_serve_index_with_prefix() { let mut srv = test::TestServer::with_factory(|| { App::new() .prefix("public") @@ -1527,29 +1544,21 @@ mod tests { let request = srv.get().uri(srv.url("/public")).finish().unwrap(); let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::FOUND); - let loc = response - .headers() - .get(header::LOCATION) - .unwrap() - .to_str() - .unwrap(); - assert_eq!(loc, "/public/Cargo.toml"); + 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::FOUND); - let loc = response - .headers() - .get(header::LOCATION) - .unwrap() - .to_str() - .unwrap(); - assert_eq!(loc, "/public/Cargo.toml"); + 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_redirect_to_index() { + fn integration_serve_index() { let mut srv = test::TestServer::with_factory(|| { App::new().handler( "test", @@ -1559,25 +1568,26 @@ mod tests { let request = srv.get().uri(srv.url("/test")).finish().unwrap(); let response = srv.execute(request.send()).unwrap(); - assert_eq!(response.status(), StatusCode::FOUND); - let loc = response - .headers() - .get(header::LOCATION) - .unwrap() - .to_str() - .unwrap(); - assert_eq!(loc, "/test/Cargo.toml"); + 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::FOUND); - let loc = response - .headers() - .get(header::LOCATION) - .unwrap() - .to_str() - .unwrap(); - assert_eq!(loc, "/test/Cargo.toml"); + 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] From b018e4abafa79e2d00d98b40c51c282c4b90e730 Mon Sep 17 00:00:00 2001 From: Jason Hills Date: Wed, 6 Feb 2019 14:37:43 -0700 Subject: [PATCH 54/59] Fixes TestRequest::with_cookie panic --- src/test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test.rs b/src/test.rs index f2346115c..1d86db9ff 100644 --- a/src/test.rs +++ b/src/test.rs @@ -558,7 +558,7 @@ impl TestRequest { /// set cookie of this request pub fn cookie(mut self, cookie: Cookie<'static>) -> Self { - if self.cookies.is_none() { + 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() { From c695358bcb7dac708888309b9b47af852e900cd3 Mon Sep 17 00:00:00 2001 From: cuebyte Date: Fri, 8 Feb 2019 22:33:00 +0100 Subject: [PATCH 55/59] Ignored the If-Modified-Since if If-None-Match is specified (#680) (#692) --- CHANGES.md | 4 ++++ src/fs.rs | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 83803abb0..1a18e092f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,10 @@ * Add `from_file` and `from_file_with_config` to `NamedFile` to allow sending files without a known path. #670 +### Fixed + +* Ignored the `If-Modified-Since` if `If-None-Match` is specified. #680 + ## [0.7.18] - 2019-01-10 ### Added diff --git a/src/fs.rs b/src/fs.rs index dcf6c539a..604ac5504 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -441,6 +441,8 @@ impl Responder for NamedFile { // check last modified let not_modified = if !none_match(etag.as_ref(), req) { true + } else if req.headers().contains_key(header::IF_NONE_MATCH) { + false } else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) = (last_modified, req.get_header()) { @@ -944,6 +946,8 @@ impl HttpRange { #[cfg(test)] mod tests { use std::fs; + use std::time::Duration; + use std::ops::Add; use super::*; use application::App; @@ -963,6 +967,43 @@ mod tests { 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()); From 0059a55dfb1a535b95791cecf339247702f786e3 Mon Sep 17 00:00:00 2001 From: Michael Edwards Date: Wed, 6 Feb 2019 11:02:33 +0100 Subject: [PATCH 56/59] Fix typo --- src/server/http.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/server/http.rs b/src/server/http.rs index 76049981f..5ff621af2 100644 --- a/src/server/http.rs +++ b/src/server/http.rs @@ -451,9 +451,8 @@ impl H + Send + Clone> HttpServer { /// For each address this method starts separate thread which does /// `accept()` in a loop. /// - /// This methods panics if no socket addresses get bound. - /// - /// This method requires to run within properly configured `Actix` system. + /// This methods panics if no socket address can be bound or an `Actix` system is not yet + /// configured. /// /// ```rust /// extern crate actix_web; From 69d710dbce90e9d00539334c063e6d4f70ba45e3 Mon Sep 17 00:00:00 2001 From: Kornel Date: Wed, 27 Feb 2019 12:52:42 +0000 Subject: [PATCH 57/59] Add insert and remove() to response builder (#707) --- src/httpresponse.rs | 97 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 2 deletions(-) diff --git a/src/httpresponse.rs b/src/httpresponse.rs index 168e9bf64..226c847f3 100644 --- a/src/httpresponse.rs +++ b/src/httpresponse.rs @@ -366,7 +366,7 @@ impl HttpResponseBuilder { self } - /// Set a header. + /// Append a header. /// /// ```rust /// # extern crate actix_web; @@ -394,7 +394,7 @@ impl HttpResponseBuilder { self } - /// Set a header. + /// Append a header. /// /// ```rust /// # extern crate actix_web; @@ -426,6 +426,65 @@ impl HttpResponseBuilder { } 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] @@ -1128,6 +1187,40 @@ mod tests { 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(); From 80d4cbe301bb72302f409a31ad4144f2805adceb Mon Sep 17 00:00:00 2001 From: Douman Date: Wed, 27 Feb 2019 21:37:20 +0300 Subject: [PATCH 58/59] Add change notes for new HttpResponseBuilder --- CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 1a18e092f..d1d838f43 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,8 @@ * 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 From 6d11ee683f21c5f2902eb8353180aacadcbebd94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20Ben=C3=ADcio?= Date: Fri, 1 Mar 2019 05:34:58 -0300 Subject: [PATCH 59/59] fixing little typo in docs (#711) --- src/extractor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extractor.rs b/src/extractor.rs index 3c64de9e1..337057235 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -193,7 +193,7 @@ impl fmt::Display for Path { } #[derive(PartialEq, Eq, PartialOrd, Ord)] -/// Extract typed information from from the request's query. +/// Extract typed information from the request's query. /// /// ## Example ///