From f81d4bdae755fe1a7ba60dcde1ceae39e50771fb Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sat, 19 Jun 2021 23:40:30 +0100 Subject: [PATCH 01/88] remove unused private hidden methods --- src/request.rs | 12 ------------ src/service.rs | 6 ------ 2 files changed, 18 deletions(-) diff --git a/src/request.rs b/src/request.rs index 42c722c46..a364f8b1f 100644 --- a/src/request.rs +++ b/src/request.rs @@ -60,18 +60,6 @@ impl HttpRequest { }), } } - - #[doc(hidden)] - pub fn __priv_test_new( - path: Path, - head: Message, - rmap: Rc, - config: AppConfig, - app_data: Rc, - ) -> HttpRequest { - let app_state = AppInitServiceState::new(rmap, config); - Self::new(path, head, app_state, app_data) - } } impl HttpRequest { diff --git a/src/service.rs b/src/service.rs index 2956fe6cb..e3e975cef 100644 --- a/src/service.rs +++ b/src/service.rs @@ -74,12 +74,6 @@ impl ServiceRequest { Self { req, payload } } - /// Construct service request. - #[doc(hidden)] - pub fn __priv_test_new(req: HttpRequest, payload: Payload) -> Self { - Self::new(req, payload) - } - /// Deconstruct request into parts #[inline] pub fn into_parts(self) -> (HttpRequest, Payload) { From 7faeffc5ab240bb6101724adf315d77c530e72b4 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sun, 20 Jun 2021 19:47:42 +0100 Subject: [PATCH 02/88] prepare actix-test release 0.1.0-beta.3 --- Cargo.toml | 2 +- actix-files/Cargo.toml | 2 +- actix-test/CHANGES.md | 4 ++++ actix-test/Cargo.toml | 2 +- actix-web-actors/Cargo.toml | 2 +- actix-web-codegen/Cargo.toml | 2 +- awc/Cargo.toml | 2 +- src/config.rs | 1 + 8 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 770c9a050..320751c66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,7 +103,7 @@ time = { version = "0.2.23", default-features = false, features = ["std"] } url = "2.1" [dev-dependencies] -actix-test = { version = "0.1.0-beta.2", features = ["openssl", "rustls"] } +actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] } awc = { version = "3.0.0-beta.6", features = ["openssl"] } brotli2 = "0.3.2" diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 44c29dc92..65dce628b 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -36,4 +36,4 @@ percent-encoding = "2.1" [dev-dependencies] actix-rt = "2.2" actix-web = "4.0.0-beta.7" -actix-test = "0.1.0-beta.2" +actix-test = "0.1.0-beta.3" diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index 2276fe745..fa554ba2e 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 0.1.0-beta.3 - 2021-06-20 +* No significant changes from `0.1.0-beta.2`. + + ## 0.1.0-beta.2 - 2021-04-17 * No significant changes from `0.1.0-beta.1`. diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index 607038377..ca814e0e5 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-test" -version = "0.1.0-beta.2" +version = "0.1.0-beta.3" authors = [ "Nikolay Kim ", "Rob Ede ", diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 159b10d58..deb461760 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -29,7 +29,7 @@ tokio = { version = "1", features = ["sync"] } [dev-dependencies] actix-rt = "2.2" -actix-test = "0.1.0-beta.2" +actix-test = "0.1.0-beta.3" awc = { version = "3.0.0-beta.6", default-features = false } env_logger = "0.8" diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 29565f74a..327f16bc5 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -20,7 +20,7 @@ proc-macro2 = "1" [dev-dependencies] actix-rt = "2.2" -actix-test = "0.1.0-beta.2" +actix-test = "0.1.0-beta.3" actix-utils = "3.0.0" actix-web = "4.0.0-beta.7" diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 7d6ee52c4..26c625a05 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -83,7 +83,7 @@ actix-http-test = { version = "3.0.0-beta.4", features = ["openssl"] } actix-utils = "3.0.0" actix-server = "2.0.0-beta.3" actix-tls = { version = "3.0.0-beta.5", features = ["openssl", "rustls"] } -actix-test = { version = "0.1.0-beta.2", features = ["openssl", "rustls"] } +actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] } brotli2 = "0.3.2" env_logger = "0.8" diff --git a/src/config.rs b/src/config.rs index 4bd76f2b7..d22bc856e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -116,6 +116,7 @@ impl AppConfig { AppConfig { secure, host, addr } } + /// Needed in actix-test crate. #[doc(hidden)] pub fn __priv_test_new(secure: bool, host: String, addr: SocketAddr) -> Self { AppConfig::new(secure, host, addr) From 2d8530feb37447a1dd2e58700b31b987ae8163ef Mon Sep 17 00:00:00 2001 From: Grzegorz Baranski Date: Tue, 22 Jun 2021 15:00:28 +0200 Subject: [PATCH 03/88] chore: bump actix to 0.12.0 in actix-web-actors (#2277) Co-authored-by: Rob Ede --- actix-web-actors/CHANGES.md | 3 +++ actix-web-actors/Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index a7ee7a9e1..decbe2219 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx +* Update `actix` to `0.12`. [#2277] + +[#2277]: https://github.com/actix/actix-web/pull/2277 ## 4.0.0-beta.5 - 2021-06-17 diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index deb461760..669cd4001 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -16,7 +16,7 @@ name = "actix_web_actors" path = "src/lib.rs" [dependencies] -actix = { version = "0.11.0-beta.3", default-features = false } +actix = { version = "0.12.0", default-features = false } actix-codec = "0.4.0" actix-http = "3.0.0-beta.7" actix-web = { version = "4.0.0-beta.7", default-features = false } From 12f7720309a5bffc0ae93ece4b7a0462ed901f56 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 22 Jun 2021 15:50:58 +0100 Subject: [PATCH 04/88] deprecate `App::data` and `App::data_factory` (#2271) --- CHANGES.md | 2 ++ src/app.rs | 15 ++++++++++++--- src/app_service.rs | 2 ++ src/data.rs | 6 ++++++ src/request.rs | 2 ++ src/resource.rs | 5 +++++ src/scope.rs | 5 +++++ src/service.rs | 2 ++ src/test.rs | 2 ++ src/types/payload.rs | 2 ++ tests/test_server.rs | 2 ++ 11 files changed, 42 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 17ae711d6..fa4aa8595 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,8 +3,10 @@ ## Unreleased - 2021-xx-xx ### Changed * Change compression algorithm features flags. [#2250] +* Deprecate `App::data` and `App::data_factory`. [#2271] [#2250]: https://github.com/actix/actix-web/pull/2250 +[#2271]: https://github.com/actix/actix-web/pull/2271 ## 4.0.0-beta.7 - 2021-06-17 diff --git a/src/app.rs b/src/app.rs index 357d45eeb..8c622dd36 100644 --- a/src/app.rs +++ b/src/app.rs @@ -98,13 +98,18 @@ where /// web::resource("/index.html").route( /// web::get().to(index))); /// ``` + #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] pub fn data(self, data: U) -> Self { self.app_data(Data::new(data)) } - /// Set application data factory. This function is - /// similar to `.data()` but it accepts data factory. Data object get - /// constructed asynchronously during application initialization. + /// Add application data factory. This function is similar to `.data()` but it accepts a + /// "data factory". Data values are constructed asynchronously during application + /// initialization, before the server starts accepting requests. + #[deprecated( + since = "4.0.0", + note = "Construct data value before starting server and use `.app_data(Data::new(val))` instead." + )] pub fn data_factory(mut self, data: F) -> Self where F: Fn() -> Out + 'static, @@ -518,6 +523,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::CREATED); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_data_factory() { let srv = init_service( @@ -541,6 +548,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_data_factory_errors() { let srv = try_init_service( diff --git a/src/app_service.rs b/src/app_service.rs index ca6f36202..a9247b19f 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -349,6 +349,8 @@ mod tests { } } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_drop_data() { let data = Arc::new(AtomicBool::new(false)); diff --git a/src/data.rs b/src/data.rs index f09a88891..51db6ce4c 100644 --- a/src/data.rs +++ b/src/data.rs @@ -154,6 +154,8 @@ mod tests { web, App, HttpResponse, }; + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_data_extractor() { let srv = init_service(App::new().data("TEST".to_string()).service( @@ -221,6 +223,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_route_data_extractor() { let srv = init_service( @@ -250,6 +254,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_override_data() { let srv = diff --git a/src/request.rs b/src/request.rs index a364f8b1f..bff66f08e 100644 --- a/src/request.rs +++ b/src/request.rs @@ -711,6 +711,8 @@ mod tests { assert_eq!(body, Bytes::from_static(b"1")); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_extensions_dropped() { struct Tracker { diff --git a/src/resource.rs b/src/resource.rs index 8c2b83b60..9455895e9 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -196,6 +196,7 @@ where /// )); /// } /// ``` + #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] pub fn data(self, data: U) -> Self { self.app_data(Data::new(data)) } @@ -694,6 +695,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::NO_CONTENT); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_data() { let srv = init_service( @@ -726,6 +729,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_data_default_service() { let srv = init_service( diff --git a/src/scope.rs b/src/scope.rs index 412c01d95..86304074b 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -146,6 +146,7 @@ where /// ); /// } /// ``` + #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] pub fn data(self, data: U) -> Self { self.app_data(Data::new(data)) } @@ -990,6 +991,8 @@ mod tests { ); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_override_data() { let srv = init_service(App::new().data(1usize).service( @@ -1008,6 +1011,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_override_data_default_service() { let srv = init_service(App::new().data(1usize).service( diff --git a/src/service.rs b/src/service.rs index e3e975cef..a772e20b7 100644 --- a/src/service.rs +++ b/src/service.rs @@ -649,6 +649,8 @@ mod tests { assert_eq!(resp.status(), http::StatusCode::NOT_FOUND); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_service_data() { let srv = diff --git a/src/test.rs b/src/test.rs index de97dc8aa..05a4ba7f2 100644 --- a/src/test.rs +++ b/src/test.rs @@ -839,6 +839,8 @@ mod tests { assert!(res.status().is_success()); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_server_data() { async fn handler(data: web::Data) -> impl Responder { diff --git a/src/types/payload.rs b/src/types/payload.rs index 3b0d1d6c6..87378701b 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -398,6 +398,8 @@ mod tests { assert!(cfg.check_mimetype(&req).is_ok()); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_config_recall_locations() { async fn bytes_handler(_: Bytes) -> impl Responder { diff --git a/tests/test_server.rs b/tests/test_server.rs index 520eb5ce2..9131d1f29 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -1028,6 +1028,8 @@ async fn test_normalize() { assert!(response.status().is_success()); } +// allow deprecated App::data +#[allow(deprecated)] #[actix_rt::test] async fn test_data_drop() { use std::sync::{ From b1148fd735fddd03d3717b2a90b697be51fac4d2 Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Tue, 22 Jun 2021 12:32:03 -0400 Subject: [PATCH 05/88] Implement `FromRequest` for request parts (#2263) Co-authored-by: Rob Ede --- CHANGES.md | 5 +++ src/extract.rs | 68 ++++++++++++++++++++++++++++-- src/info.rs | 111 +++++++++++++++++++++++++++++++++++++++++++++++-- src/lib.rs | 2 +- 4 files changed, 177 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index fa4aa8595..b69b5a18c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,12 +1,17 @@ # Changes ## Unreleased - 2021-xx-xx +### Added +* Add extractors for `Uri` and `Method`. [#2263] +* Add extractor for `ConnectionInfo` and `PeerAddr`. [#2263] + ### Changed * Change compression algorithm features flags. [#2250] * Deprecate `App::data` and `App::data_factory`. [#2271] [#2250]: https://github.com/actix/actix-web/pull/2250 [#2271]: https://github.com/actix/actix-web/pull/2271 +[#2263]: https://github.com/actix/actix-web/pull/2263 ## 4.0.0-beta.7 - 2021-06-17 diff --git a/src/extract.rs b/src/extract.rs index 45cb330a3..d7b67cd90 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -1,12 +1,14 @@ //! Request extractors use std::{ + convert::Infallible, future::Future, pin::Pin, task::{Context, Poll}, }; -use actix_utils::future::{ready, Ready}; +use actix_http::http::{Method, Uri}; +use actix_utils::future::{ok, Ready}; use futures_core::ready; use crate::{dev::Payload, Error, HttpRequest}; @@ -216,14 +218,58 @@ where } } +/// Extract the request's URI. +/// +/// # Examples +/// ``` +/// use actix_web::{http::Uri, web, App, Responder}; +/// +/// async fn handler(uri: Uri) -> impl Responder { +/// format!("Requested path: {}", uri.path()) +/// } +/// +/// let app = App::new().default_service(web::to(handler)); +/// ``` +impl FromRequest for Uri { + type Error = Infallible; + type Future = Ready>; + type Config = (); + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + ok(req.uri().clone()) + } +} + +/// Extract the request's method. +/// +/// # Examples +/// ``` +/// use actix_web::{http::Method, web, App, Responder}; +/// +/// async fn handler(method: Method) -> impl Responder { +/// format!("Request method: {}", method) +/// } +/// +/// let app = App::new().default_service(web::to(handler)); +/// ``` +impl FromRequest for Method { + type Error = Infallible; + type Future = Ready>; + type Config = (); + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + ok(req.method().clone()) + } +} + #[doc(hidden)] impl FromRequest for () { - type Error = Error; - type Future = Ready>; + type Error = Infallible; + type Future = Ready>; type Config = (); fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future { - ready(Ok(())) + ok(()) } } @@ -411,4 +457,18 @@ mod tests { .unwrap(); assert!(r.is_err()); } + + #[actix_rt::test] + async fn test_uri() { + let req = TestRequest::default().uri("/foo/bar").to_http_request(); + let uri = Uri::extract(&req).await.unwrap(); + assert_eq!(uri.path(), "/foo/bar"); + } + + #[actix_rt::test] + async fn test_method() { + let req = TestRequest::default().method(Method::GET).to_http_request(); + let method = Method::extract(&req).await.unwrap(); + assert_eq!(method, Method::GET); + } } diff --git a/src/info.rs b/src/info.rs index c9ddf6ec4..c6ff54efe 100644 --- a/src/info.rs +++ b/src/info.rs @@ -1,13 +1,36 @@ -use std::cell::Ref; +use std::{cell::Ref, convert::Infallible, net::SocketAddr}; -use crate::dev::{AppConfig, RequestHead}; -use crate::http::header::{self, HeaderName}; +use actix_utils::future::{err, ok, Ready}; +use derive_more::{Display, Error}; + +use crate::{ + dev::{AppConfig, Payload, RequestHead}, + http::header::{self, HeaderName}, + FromRequest, HttpRequest, ResponseError, +}; const X_FORWARDED_FOR: &[u8] = b"x-forwarded-for"; const X_FORWARDED_HOST: &[u8] = b"x-forwarded-host"; const X_FORWARDED_PROTO: &[u8] = b"x-forwarded-proto"; -/// `HttpRequest` connection information +/// HTTP connection information. +/// +/// `ConnectionInfo` implements `FromRequest` and can be extracted in handlers. +/// +/// # Examples +/// ``` +/// # use actix_web::{HttpResponse, Responder}; +/// use actix_web::dev::ConnectionInfo; +/// +/// async fn handler(conn: ConnectionInfo) -> impl Responder { +/// match conn.host() { +/// "actix.rs" => HttpResponse::Ok().body("Welcome!"), +/// "admin.actix.rs" => HttpResponse::Ok().body("Admin portal."), +/// _ => HttpResponse::NotFound().finish() +/// } +/// } +/// # let _svc = actix_web::web::to(handler); +/// ``` #[derive(Debug, Clone, Default)] pub struct ConnectionInfo { scheme: String, @@ -187,6 +210,65 @@ impl ConnectionInfo { } } +impl FromRequest for ConnectionInfo { + type Error = Infallible; + type Future = Ready>; + type Config = (); + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + ok(req.connection_info().clone()) + } +} + +/// Extractor for peer's socket address. +/// +/// Also see [`HttpRequest::peer_addr`]. +/// +/// # Examples +/// ``` +/// # use actix_web::Responder; +/// use actix_web::dev::PeerAddr; +/// +/// async fn handler(peer_addr: PeerAddr) -> impl Responder { +/// let socket_addr = peer_addr.0; +/// socket_addr.to_string() +/// } +/// # let _svc = actix_web::web::to(handler); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Display)] +#[display(fmt = "{}", _0)] +pub struct PeerAddr(pub SocketAddr); + +impl PeerAddr { + /// Unwrap into inner `SocketAddr` value. + pub fn into_inner(self) -> SocketAddr { + self.0 + } +} + +#[derive(Debug, Display, Error)] +#[non_exhaustive] +#[display(fmt = "Missing peer address")] +pub struct MissingPeerAddr; + +impl ResponseError for MissingPeerAddr {} + +impl FromRequest for PeerAddr { + type Error = MissingPeerAddr; + type Future = Ready>; + type Config = (); + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + match req.peer_addr() { + Some(addr) => ok(PeerAddr(addr)), + None => { + log::error!("Missing peer address."); + err(MissingPeerAddr) + } + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -239,4 +321,25 @@ mod tests { let info = req.connection_info(); assert_eq!(info.scheme(), "https"); } + + #[actix_rt::test] + async fn test_conn_info() { + let req = TestRequest::default() + .uri("http://actix.rs/") + .to_http_request(); + let conn_info = ConnectionInfo::extract(&req).await.unwrap(); + assert_eq!(conn_info.scheme(), "http"); + } + + #[actix_rt::test] + async fn test_peer_addr() { + let addr = "127.0.0.1:8080".parse().unwrap(); + let req = TestRequest::default().peer_addr(addr).to_http_request(); + let peer_addr = PeerAddr::extract(&req).await.unwrap(); + assert_eq!(peer_addr, PeerAddr(addr)); + + let req = TestRequest::default().to_http_request(); + let res = PeerAddr::extract(&req).await; + assert!(res.is_err()); + } } diff --git a/src/lib.rs b/src/lib.rs index 4bcef3988..9d8bf62e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -131,7 +131,7 @@ pub mod dev { pub use crate::config::{AppConfig, AppService}; #[doc(hidden)] pub use crate::handler::Handler; - pub use crate::info::ConnectionInfo; + pub use crate::info::{ConnectionInfo, PeerAddr}; pub use crate::rmap::ResourceMap; pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse, WebService}; From 3b6333e65f2da0c0ddf84f014088ef69c12dd2e2 Mon Sep 17 00:00:00 2001 From: Luca Palmieri Date: Tue, 22 Jun 2021 23:22:33 +0200 Subject: [PATCH 06/88] Propagate error cause to middlewares (#2280) --- src/response/response.rs | 5 +- tests/test_error_propagation.rs | 100 ++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 tests/test_error_propagation.rs diff --git a/src/response/response.rs b/src/response/response.rs index 9dd804be0..9a3bb2874 100644 --- a/src/response/response.rs +++ b/src/response/response.rs @@ -49,7 +49,10 @@ impl HttpResponse { /// Create an error response. #[inline] pub fn from_error(error: impl Into) -> Self { - error.into().as_response_error().error_response() + let error = error.into(); + let mut response = error.as_response_error().error_response(); + response.error = Some(error); + response } } diff --git a/tests/test_error_propagation.rs b/tests/test_error_propagation.rs new file mode 100644 index 000000000..1b56615a0 --- /dev/null +++ b/tests/test_error_propagation.rs @@ -0,0 +1,100 @@ +use actix_utils::future::{ok, Ready}; +use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; +use actix_web::test::{call_service, init_service, TestRequest}; +use actix_web::{HttpResponse, ResponseError}; +use futures_util::lock::Mutex; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; + +#[derive(Debug, Clone)] +pub struct MyError; + +impl ResponseError for MyError {} + +impl std::fmt::Display for MyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "A custom error") + } +} + +#[actix_web::get("/test")] +async fn test() -> Result { + Err(MyError)?; + Ok(HttpResponse::NoContent().finish()) +} + +#[derive(Clone)] +pub struct SpyMiddleware(Arc>>); + +impl Transform for SpyMiddleware +where + S: Service, Error = actix_web::Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = actix_web::Error; + type Transform = Middleware; + type InitError = (); + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ok(Middleware { + was_error: self.0.clone(), + service, + }) + } +} + +#[doc(hidden)] +pub struct Middleware { + was_error: Arc>>, + service: S, +} + +impl Service for Middleware +where + S: Service, Error = actix_web::Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = actix_web::Error; + type Future = Pin>>>; + + fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { + self.service.poll_ready(cx) + } + + fn call(&self, req: ServiceRequest) -> Self::Future { + let lock = self.was_error.clone(); + let response_future = self.service.call(req); + Box::pin(async move { + let response = response_future.await; + if let Ok(success) = &response { + *lock.lock().await = Some(success.response().error().is_some()); + } + response + }) + } +} + +#[actix_rt::test] +async fn error_cause_should_be_propagated_to_middlewares() { + let lock = Arc::new(Mutex::new(None)); + let spy_middleware = SpyMiddleware(lock.clone()); + + let app = init_service( + actix_web::App::new() + .wrap(spy_middleware.clone()) + .service(test), + ) + .await; + + call_service(&app, TestRequest::with_uri("/test").to_request()).await; + + let was_error_captured = lock.lock().await.unwrap(); + assert!(was_error_captured); +} From 8846808804e2ac885a5a81b27817b365dfcd4c9a Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Tue, 22 Jun 2021 19:42:00 -0400 Subject: [PATCH 07/88] ServiceRequest::parts_mut (#2177) --- CHANGES.md | 2 ++ src/service.rs | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index b69b5a18c..31fa4690f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## Unreleased - 2021-xx-xx ### Added +* Add `ServiceRequest::parts_mut`. [#2177] * Add extractors for `Uri` and `Method`. [#2263] * Add extractor for `ConnectionInfo` and `PeerAddr`. [#2263] @@ -9,6 +10,7 @@ * Change compression algorithm features flags. [#2250] * Deprecate `App::data` and `App::data_factory`. [#2271] +[#2177]: https://github.com/actix/actix-web/pull/2177 [#2250]: https://github.com/actix/actix-web/pull/2250 [#2271]: https://github.com/actix/actix-web/pull/2271 [#2263]: https://github.com/actix/actix-web/pull/2263 diff --git a/src/service.rs b/src/service.rs index a772e20b7..8d73a87fa 100644 --- a/src/service.rs +++ b/src/service.rs @@ -80,6 +80,12 @@ impl ServiceRequest { (self.req, self.payload) } + /// Get mutable access to inner `HttpRequest` and `Payload` + #[inline] + pub fn parts_mut(&mut self) -> (&mut HttpRequest, &mut Payload) { + (&mut self.req, &mut self.payload) + } + /// Construct request from parts. pub fn from_parts(req: HttpRequest, payload: Payload) -> Self { Self { req, payload } From 7535a1ade8c1ee392ae1aa337821cf8f7c7c4c9a Mon Sep 17 00:00:00 2001 From: Jonas Malaco Date: Wed, 23 Jun 2021 12:54:25 -0300 Subject: [PATCH 08/88] Note that Form cannot require data ordering (#2283) --- src/types/form.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/types/form.rs b/src/types/form.rs index 44d1b952e..4ce075d99 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -80,6 +80,10 @@ use crate::{ /// }) /// } /// ``` +/// +/// # Panics +/// URL encoded forms consist of unordered `key=value` pairs, therefore they cannot be decoded into +/// any type which depends upon data ordering (eg. tuples). Trying to do so will result in a panic. #[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct Form(pub T); From ed0516d724ed225b0b83c01f9278e47bd5f7f104 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 23 Jun 2021 20:47:17 +0100 Subject: [PATCH 09/88] try to fix doc test failures (#2284) --- .cargo/config.toml | 7 ++++--- .github/workflows/ci.yml | 36 ++++++++++++-------------------- .github/workflows/clippy-fmt.yml | 2 +- tests/test_server.rs | 2 +- 4 files changed, 19 insertions(+), 28 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 0cf09f710..72f445d8a 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,6 +3,7 @@ chk = "check --workspace --all-features --tests --examples --bins" lint = "clippy --workspace --tests --examples" ci-min = "hack check --workspace --no-default-features" ci-min-test = "hack check --workspace --no-default-features --tests --examples" -ci-default = "hack check --workspace" -ci-full = "check --workspace --bins --examples --tests" -ci-test = "test --workspace --all-features --no-fail-fast" +ci-default = "check --workspace --bins --tests --examples" +ci-full = "check --workspace --all-features --bins --tests --examples" +ci-test = "test --workspace --all-features --lib --tests --no-fail-fast -- --nocapture" +ci-doctest = "hack test --workspace --all-features --doc --no-fail-fast -- --nocapture" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c57db463a..be595e35c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,43 +66,33 @@ jobs: - name: check minimal uses: actions-rs/cargo@v1 - with: - command: hack - args: check --workspace --no-default-features + with: { command: ci-min } - name: check minimal + tests uses: actions-rs/cargo@v1 - with: - command: hack - args: check --workspace --no-default-features --tests --examples + with: { command: ci-min-test } + - name: check default + uses: actions-rs/cargo@v1 + with: { command: ci-default } + - name: check full uses: actions-rs/cargo@v1 - with: - command: check - args: --workspace --bins --examples --tests + with: { command: ci-full } - name: tests - uses: actions-rs/cargo@v1 - with: - command: test - args: --workspace --all-features --no-fail-fast -- --nocapture - --skip=test_h2_content_length - --skip=test_reading_deflate_encoding_large_random_rustls - - - name: tests (actix-http) uses: actions-rs/cargo@v1 timeout-minutes: 40 with: - command: test - args: --package=actix-http --no-default-features --features=rustls -- --nocapture + command: ci-test + args: --skip=test_reading_deflate_encoding_large_random_rustls - - name: tests (awc) + - name: doc tests + # due to unknown issue with running doc tests on macOS + if: matrix.target.os == 'ubuntu-latest' uses: actions-rs/cargo@v1 timeout-minutes: 40 - with: - command: test - args: --package=awc --no-default-features --features=rustls -- --nocapture + with: { command: ci-doctest } - name: Generate coverage file if: > diff --git a/.github/workflows/clippy-fmt.yml b/.github/workflows/clippy-fmt.yml index e966fa4ab..957256d32 100644 --- a/.github/workflows/clippy-fmt.yml +++ b/.github/workflows/clippy-fmt.yml @@ -36,4 +36,4 @@ jobs: uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - args: --workspace --tests --all-features + args: --workspace --all-features --tests diff --git a/tests/test_server.rs b/tests/test_server.rs index 9131d1f29..afea39dd9 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -879,7 +879,7 @@ async fn test_brotli_encoding_large_openssl() { assert_eq!(bytes, Bytes::from(data)); } -#[cfg(all(feature = "rustls", feature = "openssl"))] +#[cfg(feature = "rustls")] mod plus_rustls { use std::io::BufReader; From 083ee05d50519897db96fb1873b6eebd5f6df186 Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Wed, 23 Jun 2021 16:30:06 -0400 Subject: [PATCH 10/88] `Route::service` (#2262) Co-authored-by: Rob Ede --- CHANGES.md | 4 +- Cargo.toml | 1 - src/extract.rs | 8 ++-- src/lib.rs | 4 +- src/request.rs | 2 +- src/route.rs | 122 +++++++++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 128 insertions(+), 13 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 31fa4690f..876e1c03d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,8 @@ ### Added * Add `ServiceRequest::parts_mut`. [#2177] * Add extractors for `Uri` and `Method`. [#2263] -* Add extractor for `ConnectionInfo` and `PeerAddr`. [#2263] +* Add extractors for `ConnectionInfo` and `PeerAddr`. [#2263] +* Add `Route::service` for using hand-written services as handlers. [#2262] ### Changed * Change compression algorithm features flags. [#2250] @@ -13,6 +14,7 @@ [#2177]: https://github.com/actix/actix-web/pull/2177 [#2250]: https://github.com/actix/actix-web/pull/2250 [#2271]: https://github.com/actix/actix-web/pull/2271 +[#2262]: https://github.com/actix/actix-web/pull/2262 [#2263]: https://github.com/actix/actix-web/pull/2263 diff --git a/Cargo.toml b/Cargo.toml index 320751c66..779b52255 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -113,7 +113,6 @@ flate2 = "1.0.13" zstd = "0.7" rand = "0.8" rcgen = "0.8" -serde_derive = "1.0" tls-openssl = { package = "openssl", version = "0.10.9" } tls-rustls = { package = "rustls", version = "0.19.0" } diff --git a/src/extract.rs b/src/extract.rs index d7b67cd90..592f7ab83 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -54,7 +54,7 @@ pub trait FromRequest: Sized { /// use actix_web::{web, dev, App, Error, HttpRequest, FromRequest}; /// use actix_web::error::ErrorBadRequest; /// use futures_util::future::{ok, err, Ready}; -/// use serde_derive::Deserialize; +/// use serde::Deserialize; /// use rand; /// /// #[derive(Debug, Deserialize)] @@ -145,7 +145,7 @@ where /// use actix_web::{web, dev, App, Result, Error, HttpRequest, FromRequest}; /// use actix_web::error::ErrorBadRequest; /// use futures_util::future::{ok, err, Ready}; -/// use serde_derive::Deserialize; +/// use serde::Deserialize; /// use rand; /// /// #[derive(Debug, Deserialize)] @@ -265,7 +265,7 @@ impl FromRequest for Method { #[doc(hidden)] impl FromRequest for () { type Error = Infallible; - type Future = Ready>; + type Future = Ready>; type Config = (); fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future { @@ -376,7 +376,7 @@ mod m { mod tests { use actix_http::http::header; use bytes::Bytes; - use serde_derive::Deserialize; + use serde::Deserialize; use super::*; use crate::test::TestRequest; diff --git a/src/lib.rs b/src/lib.rs index 9d8bf62e7..920abccb6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -149,7 +149,9 @@ pub mod dev { pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, ResponseHead}; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; pub use actix_server::Server; - pub use actix_service::{always_ready, forward_ready, Service, Transform}; + pub use actix_service::{ + always_ready, fn_factory, fn_service, forward_ready, Service, Transform, + }; pub(crate) fn insert_slash(mut patterns: Vec) -> Vec { for path in &mut patterns { diff --git a/src/request.rs b/src/request.rs index bff66f08e..5c5c43d26 100644 --- a/src/request.rs +++ b/src/request.rs @@ -347,7 +347,7 @@ impl Drop for HttpRequest { /// # Examples /// ``` /// use actix_web::{web, App, HttpRequest}; -/// use serde_derive::Deserialize; +/// use serde::Deserialize; /// /// /// extract `Thing` from request /// async fn index(req: HttpRequest) -> String { diff --git a/src/route.rs b/src/route.rs index 44f7e30b8..d85b940bd 100644 --- a/src/route.rs +++ b/src/route.rs @@ -5,7 +5,7 @@ use std::{future::Future, rc::Rc}; use actix_http::http::Method; use actix_service::{ boxed::{self, BoxService, BoxServiceFactory}, - Service, ServiceFactory, + Service, ServiceFactory, ServiceFactoryExt, }; use futures_core::future::LocalBoxFuture; @@ -128,9 +128,10 @@ impl Route { /// Set handler function, use request extractors for parameters. /// + /// # Examples /// ``` /// use actix_web::{web, http, App}; - /// use serde_derive::Deserialize; + /// use serde::Deserialize; /// /// #[derive(Deserialize)] /// struct Info { @@ -154,7 +155,7 @@ impl Route { /// /// ``` /// # use std::collections::HashMap; - /// # use serde_derive::Deserialize; + /// # use serde::Deserialize; /// use actix_web::{web, App}; /// /// #[derive(Deserialize)] @@ -184,6 +185,53 @@ impl Route { self.service = boxed::factory(HandlerService::new(handler)); self } + + /// Set raw service to be constructed and called as the request handler. + /// + /// # Examples + /// ``` + /// # use std::convert::Infallible; + /// # use futures_util::future::LocalBoxFuture; + /// # use actix_web::{*, dev::*, http::header}; + /// struct HelloWorld; + /// + /// impl Service for HelloWorld { + /// type Response = ServiceResponse; + /// type Error = Infallible; + /// type Future = LocalBoxFuture<'static, Result>; + /// + /// always_ready!(); + /// + /// fn call(&self, req: ServiceRequest) -> Self::Future { + /// let (req, _) = req.into_parts(); + /// + /// let res = HttpResponse::Ok() + /// .insert_header(header::ContentType::plaintext()) + /// .body("Hello world!"); + /// + /// Box::pin(async move { Ok(ServiceResponse::new(req, res)) }) + /// } + /// } + /// + /// App::new().route( + /// "/", + /// web::get().service(fn_factory(|| async { Ok(HelloWorld) })), + /// ); + /// ``` + pub fn service(mut self, service_factory: S) -> Self + where + S: ServiceFactory< + ServiceRequest, + Response = ServiceResponse, + Error = E, + InitError = (), + Config = (), + > + 'static, + E: Into + 'static, + { + self.service = boxed::factory(service_factory.map_err(Into::into)); + self + } } #[cfg(test)] @@ -192,9 +240,12 @@ mod tests { use actix_rt::time::sleep; use bytes::Bytes; - use serde_derive::Serialize; + use futures_core::future::LocalBoxFuture; + use serde::Serialize; - use crate::http::{Method, StatusCode}; + use crate::dev::{always_ready, fn_factory, fn_service, Service}; + use crate::http::{header, Method, StatusCode}; + use crate::service::{ServiceRequest, ServiceResponse}; use crate::test::{call_service, init_service, read_body, TestRequest}; use crate::{error, web, App, HttpResponse}; @@ -268,4 +319,65 @@ mod tests { let body = read_body(resp).await; assert_eq!(body, Bytes::from_static(b"{\"name\":\"test\"}")); } + + #[actix_rt::test] + async fn test_service_handler() { + struct HelloWorld; + + impl Service for HelloWorld { + type Response = ServiceResponse; + type Error = crate::Error; + type Future = LocalBoxFuture<'static, Result>; + + always_ready!(); + + fn call(&self, req: ServiceRequest) -> Self::Future { + let (req, _) = req.into_parts(); + + let res = HttpResponse::Ok() + .insert_header(header::ContentType::plaintext()) + .body("Hello world!"); + + Box::pin(async move { Ok(ServiceResponse::new(req, res)) }) + } + } + + let srv = init_service( + App::new() + .route( + "/hello", + web::get().service(fn_factory(|| async { Ok(HelloWorld) })), + ) + .route( + "/bye", + web::get().service(fn_factory(|| async { + Ok::<_, ()>(fn_service(|req: ServiceRequest| async { + let (req, _) = req.into_parts(); + + let res = HttpResponse::Ok() + .insert_header(header::ContentType::plaintext()) + .body("Goodbye, and thanks for all the fish!"); + + Ok::<_, Infallible>(ServiceResponse::new(req, res)) + })) + })), + ), + ) + .await; + + let req = TestRequest::get().uri("/hello").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + let body = read_body(resp).await; + assert_eq!(body, Bytes::from_static(b"Hello world!")); + + let req = TestRequest::get().uri("/bye").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + let body = read_body(resp).await; + assert_eq!( + body, + Bytes::from_static(b"Goodbye, and thanks for all the fish!") + ); + } } From 2d8d2f5ab08c7db6ccfeba5a15a5085165a06ed3 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 24 Jun 2021 15:10:51 +0100 Subject: [PATCH 11/88] app data doc improvements --- src/app.rs | 85 +++++++++++++++++++----------- src/app_service.rs | 9 +++- src/config.rs | 11 ++-- src/data.rs | 5 ++ src/resource.rs | 76 ++++++++++++++------------- src/scope.rs | 128 +++++++++++++++++++++++---------------------- src/service.rs | 3 +- src/web.rs | 1 - 8 files changed, 181 insertions(+), 137 deletions(-) diff --git a/src/app.rs b/src/app.rs index 8c622dd36..677f73805 100644 --- a/src/app.rs +++ b/src/app.rs @@ -68,36 +68,71 @@ where InitError = (), >, { - /// Set application data. Application data could be accessed - /// by using `Data` extractor where `T` is data type. + /// Set application (root level) data. /// - /// **Note**: HTTP server accepts an application factory rather than - /// an application instance. Http server constructs an application - /// instance for each thread, thus application data must be constructed - /// multiple times. If you want to share data between different - /// threads, a shared object should be used, e.g. `Arc`. Internally `Data` type - /// uses `Arc` so data could be created outside of app factory and clones could - /// be stored via `App::app_data()` method. + /// Application data stored with `App::app_data()` method is available through the + /// [`HttpRequest::app_data`](crate::HttpRequest::app_data) method at runtime. + /// + /// # [`Data`] + /// Any [`Data`] type added here can utilize it's extractor implementation in handlers. + /// Types not wrapped in `Data` cannot use this extractor. See [its docs](Data) for more + /// about its usage and patterns. /// /// ``` /// use std::cell::Cell; - /// use actix_web::{web, App, HttpResponse, Responder}; + /// use actix_web::{web, App, HttpRequest, HttpResponse, Responder}; /// /// struct MyData { - /// counter: Cell, + /// count: std::cell::Cell, /// } /// - /// async fn index(data: web::Data) -> impl Responder { - /// data.counter.set(data.counter.get() + 1); - /// HttpResponse::Ok() + /// async fn handler(req: HttpRequest, counter: web::Data) -> impl Responder { + /// // note this cannot use the Data extractor because it was not added with it + /// let incr = *req.app_data::().unwrap(); + /// assert_eq!(incr, 3); + /// + /// // update counter using other value from app data + /// counter.count.set(counter.count.get() + incr); + /// + /// HttpResponse::Ok().body(counter.count.get().to_string()) /// } /// - /// let app = App::new() - /// .data(MyData{ counter: Cell::new(0) }) - /// .service( - /// web::resource("/index.html").route( - /// web::get().to(index))); + /// let app = App::new().service( + /// web::resource("/") + /// .app_data(3usize) + /// .app_data(web::Data::new(MyData { count: Default::default() })) + /// .route(web::get().to(handler)) + /// ); /// ``` + /// + /// # Shared Mutable State + /// [`HttpServer::new`](crate::HttpServer::new) accepts an application factory rather than an + /// application instance; the factory closure is called on each worker thread independently. + /// Therefore, if you want to share a data object between different workers, a shareable object + /// needs to be created first, outside the `HttpServer::new` closure and cloned into it. + /// [`Data`] is an example of such a sharable object. + /// + /// ```ignore + /// let counter = web::Data::new(AppStateWithCounter { + /// counter: Mutex::new(0), + /// }); + /// + /// HttpServer::new(move || { + /// // move counter object into the closure and clone for each worker + /// + /// App::new() + /// .app_data(counter.clone()) + /// .route("/", web::get().to(handler)) + /// }) + /// ``` + pub fn app_data(mut self, ext: U) -> Self { + self.extensions.insert(ext); + self + } + + /// Add application (root) data after wrapping in `Data`. + /// + /// Deprecated in favor of [`app_data`](Self::app_data). #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] pub fn data(self, data: U) -> Self { self.app_data(Data::new(data)) @@ -138,18 +173,6 @@ where self } - /// Set application level arbitrary data item. - /// - /// Application data stored with `App::app_data()` method is available - /// via `HttpRequest::app_data()` method at runtime. - /// - /// This method could be used for storing `Data` as well, in that case - /// data could be accessed by using `Data` extractor. - pub fn app_data(mut self, ext: U) -> Self { - self.extensions.insert(ext); - self - } - /// Run external configuration as part of the application building /// process /// diff --git a/src/app_service.rs b/src/app_service.rs index a9247b19f..feb9f22a4 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -144,7 +144,9 @@ where } } -/// Service that takes a [`Request`] and delegates to a service that take a [`ServiceRequest`]. +/// The [`Service`] that is passed to `actix-http`'s server builder. +/// +/// Wraps a service receiving a [`ServiceRequest`] into one receiving a [`Request`]. pub struct AppInitService where T: Service, Error = Error>, @@ -275,6 +277,7 @@ impl ServiceFactory for AppRoutingFactory { } } +/// The Actix Web router default entry point. pub struct AppRouting { router: Router, default: HttpService, @@ -299,6 +302,10 @@ impl Service for AppRouting { true }); + // you might expect to find `req.add_data_container()` called here but `HttpRequest` objects + // are created with the root data already set (in `AppInitService::call`) and root data is + // retained when releasing requests back to the pool + if let Some((srv, _info)) = res { srv.call(req) } else { diff --git a/src/config.rs b/src/config.rs index d22bc856e..966141193 100644 --- a/src/config.rs +++ b/src/config.rs @@ -62,6 +62,8 @@ impl AppService { (self.config, self.services) } + /// Clones inner config and default service, returning new `AppService` with empty service list + /// marked as non-root. pub(crate) fn clone_config(&self) -> Self { AppService { config: self.config.clone(), @@ -71,12 +73,12 @@ impl AppService { } } - /// Service configuration + /// Returns reference to configuration. pub fn config(&self) -> &AppConfig { &self.config } - /// Default resource + /// Returns default handler factory. pub fn default_service(&self) -> Rc { self.default.clone() } @@ -116,7 +118,7 @@ impl AppConfig { AppConfig { secure, host, addr } } - /// Needed in actix-test crate. + /// Needed in actix-test crate. Semver exempt. #[doc(hidden)] pub fn __priv_test_new(secure: bool, host: String, addr: SocketAddr) -> Self { AppConfig::new(secure, host, addr) @@ -192,6 +194,7 @@ impl ServiceConfig { /// Add shared app data item. /// /// Counterpart to [`App::data()`](crate::App::data). + #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] pub fn data(&mut self, data: U) -> &mut Self { self.app_data(Data::new(data)); self @@ -257,6 +260,8 @@ mod tests { use crate::test::{call_service, init_service, read_body, TestRequest}; use crate::{web, App, HttpRequest, HttpResponse}; + // allow deprecated `ServiceConfig::data` + #[allow(deprecated)] #[actix_rt::test] async fn test_data() { let cfg = |cfg: &mut ServiceConfig| { diff --git a/src/data.rs b/src/data.rs index 51db6ce4c..174faba37 100644 --- a/src/data.rs +++ b/src/data.rs @@ -36,6 +36,11 @@ pub(crate) type FnDataFactory = /// If route data is not set for a handler, using `Data` extractor would cause *Internal /// Server Error* response. /// +// TODO: document `dyn T` functionality through converting an Arc +// TODO: note equivalence of req.app_data> and Data extractor +// TODO: note that data must be inserted using Data in order to extract it +/// +/// # Examples /// ``` /// use std::sync::Mutex; /// use actix_web::{web, App, HttpResponse, Responder}; diff --git a/src/resource.rs b/src/resource.rs index 9455895e9..7a4c1248b 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -169,41 +169,38 @@ where self } - /// Provide resource specific data. This method allows to add extractor - /// configuration or specific state available via `Data` extractor. - /// Provided data is available for all routes registered for the current resource. - /// Resource data overrides data registered by `App::data()` method. - /// - /// ``` - /// use actix_web::{web, App, FromRequest}; - /// - /// /// extract text data from request - /// async fn index(body: String) -> String { - /// format!("Body {}!", body) - /// } - /// - /// fn main() { - /// let app = App::new().service( - /// web::resource("/index.html") - /// // limit size of the payload - /// .data(String::configure(|cfg| { - /// cfg.limit(4096) - /// })) - /// .route( - /// web::get() - /// // register handler - /// .to(index) - /// )); - /// } - /// ``` - #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] - pub fn data(self, data: U) -> Self { - self.app_data(Data::new(data)) - } - /// Add resource data. /// - /// Data of different types from parent contexts will still be accessible. + /// Data of different types from parent contexts will still be accessible. Any `Data` types + /// set here can be extracted in handlers using the `Data` extractor. + /// + /// # Examples + /// ``` + /// use std::cell::Cell; + /// use actix_web::{web, App, HttpRequest, HttpResponse, Responder}; + /// + /// struct MyData { + /// count: std::cell::Cell, + /// } + /// + /// async fn handler(req: HttpRequest, counter: web::Data) -> impl Responder { + /// // note this cannot use the Data extractor because it was not added with it + /// let incr = *req.app_data::().unwrap(); + /// assert_eq!(incr, 3); + /// + /// // update counter using other value from app data + /// counter.count.set(counter.count.get() + incr); + /// + /// HttpResponse::Ok().body(counter.count.get().to_string()) + /// } + /// + /// let app = App::new().service( + /// web::resource("/") + /// .app_data(3usize) + /// .app_data(web::Data::new(MyData { count: Default::default() })) + /// .route(web::get().to(handler)) + /// ); + /// ``` pub fn app_data(mut self, data: U) -> Self { self.app_data .get_or_insert_with(Extensions::new) @@ -212,6 +209,14 @@ where self } + /// Add resource data after wrapping in `Data`. + /// + /// Deprecated in favor of [`app_data`](Self::app_data). + #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] + pub fn data(self, data: U) -> Self { + self.app_data(Data::new(data)) + } + /// Register a new route and add handler. This route matches all requests. /// /// ``` @@ -227,7 +232,6 @@ where /// This is shortcut for: /// /// ``` - /// # extern crate actix_web; /// # use actix_web::*; /// # fn index(req: HttpRequest) -> HttpResponse { unimplemented!() } /// App::new().service(web::resource("/").route(web::route().to(index))); @@ -695,7 +699,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::NO_CONTENT); } - // allow deprecated App::data + // allow deprecated `{App, Resource}::data` #[allow(deprecated)] #[actix_rt::test] async fn test_data() { @@ -729,7 +733,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); } - // allow deprecated App::data + // allow deprecated `{App, Resource}::data` #[allow(deprecated)] #[actix_rt::test] async fn test_data_default_service() { diff --git a/src/scope.rs b/src/scope.rs index 86304074b..0caf06ee3 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,28 +1,23 @@ -use std::cell::RefCell; -use std::fmt; -use std::future::Future; -use std::rc::Rc; +use std::{cell::RefCell, fmt, future::Future, mem, rc::Rc}; use actix_http::Extensions; use actix_router::{ResourceDef, Router}; -use actix_service::boxed::{self, BoxService, BoxServiceFactory}; use actix_service::{ - apply, apply_fn_factory, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt, - Transform, + apply, apply_fn_factory, + boxed::{self, BoxService, BoxServiceFactory}, + IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt, Transform, }; use futures_core::future::LocalBoxFuture; use futures_util::future::join_all; -use crate::config::ServiceConfig; -use crate::data::Data; -use crate::dev::{AppService, HttpServiceFactory}; -use crate::error::Error; -use crate::guard::Guard; -use crate::resource::Resource; -use crate::rmap::ResourceMap; -use crate::route::Route; -use crate::service::{ - AppServiceFactory, ServiceFactoryWrapper, ServiceRequest, ServiceResponse, +use crate::{ + config::ServiceConfig, + data::Data, + dev::{AppService, HttpServiceFactory}, + guard::Guard, + rmap::ResourceMap, + service::{AppServiceFactory, ServiceFactoryWrapper, ServiceRequest, ServiceResponse}, + Error, Resource, Route, }; type Guards = Vec>; @@ -71,16 +66,17 @@ pub struct Scope { impl Scope { /// Create a new scope pub fn new(path: &str) -> Scope { - let fref = Rc::new(RefCell::new(None)); + let factory_ref = Rc::new(RefCell::new(None)); + Scope { - endpoint: ScopeEndpoint::new(fref.clone()), + endpoint: ScopeEndpoint::new(Rc::clone(&factory_ref)), rdef: path.to_string(), app_data: None, guards: Vec::new(), services: Vec::new(), default: None, external: Vec::new(), - factory_ref: fref, + factory_ref, } } } @@ -120,40 +116,38 @@ where self } - /// Set or override application data. Application data could be accessed - /// by using `Data` extractor where `T` is data type. - /// - /// ``` - /// use std::cell::Cell; - /// use actix_web::{web, App, HttpResponse, Responder}; - /// - /// struct MyData { - /// counter: Cell, - /// } - /// - /// async fn index(data: web::Data) -> impl Responder { - /// data.counter.set(data.counter.get() + 1); - /// HttpResponse::Ok() - /// } - /// - /// fn main() { - /// let app = App::new().service( - /// web::scope("/app") - /// .data(MyData{ counter: Cell::new(0) }) - /// .service( - /// web::resource("/index.html").route( - /// web::get().to(index))) - /// ); - /// } - /// ``` - #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] - pub fn data(self, data: U) -> Self { - self.app_data(Data::new(data)) - } - /// Add scope data. /// - /// Data of different types from parent contexts will still be accessible. + /// Data of different types from parent contexts will still be accessible. Any `Data` types + /// set here can be extracted in handlers using the `Data` extractor. + /// + /// # Examples + /// ``` + /// use std::cell::Cell; + /// use actix_web::{web, App, HttpRequest, HttpResponse, Responder}; + /// + /// struct MyData { + /// count: std::cell::Cell, + /// } + /// + /// async fn handler(req: HttpRequest, counter: web::Data) -> impl Responder { + /// // note this cannot use the Data extractor because it was not added with it + /// let incr = *req.app_data::().unwrap(); + /// assert_eq!(incr, 3); + /// + /// // update counter using other value from app data + /// counter.count.set(counter.count.get() + incr); + /// + /// HttpResponse::Ok().body(counter.count.get().to_string()) + /// } + /// + /// let app = App::new().service( + /// web::scope("/app") + /// .app_data(3usize) + /// .app_data(web::Data::new(MyData { count: Default::default() })) + /// .route("/", web::get().to(handler)) + /// ); + /// ``` pub fn app_data(mut self, data: U) -> Self { self.app_data .get_or_insert_with(Extensions::new) @@ -162,15 +156,20 @@ where self } - /// Run external configuration as part of the scope building - /// process + /// Add scope data after wrapping in `Data`. /// - /// This function is useful for moving parts of configuration to a - /// different module or even library. For example, - /// some of the resource's configuration could be moved to different module. + /// Deprecated in favor of [`app_data`](Self::app_data). + #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] + pub fn data(self, data: U) -> Self { + self.app_data(Data::new(data)) + } + + /// Run external configuration as part of the scope building process. + /// + /// This function is useful for moving parts of configuration to a different module or library. + /// For example, some of the resource's configuration could be moved to different module. /// /// ``` - /// # extern crate actix_web; /// use actix_web::{web, middleware, App, HttpResponse}; /// /// // this function could be located in different module @@ -191,18 +190,21 @@ where /// .route("/index.html", web::get().to(|| HttpResponse::Ok())); /// } /// ``` - pub fn configure(mut self, f: F) -> Self + pub fn configure(mut self, cfg_fn: F) -> Self where F: FnOnce(&mut ServiceConfig), { let mut cfg = ServiceConfig::new(); - f(&mut cfg); + cfg_fn(&mut cfg); + self.services.extend(cfg.services); self.external.extend(cfg.external); + // TODO: add Extensions::is_empty check and conditionally insert data self.app_data .get_or_insert_with(Extensions::new) .extend(cfg.app_data); + self } @@ -419,7 +421,7 @@ where let mut rmap = ResourceMap::new(ResourceDef::root_prefix(&self.rdef)); // external resources - for mut rdef in std::mem::take(&mut self.external) { + for mut rdef in mem::take(&mut self.external) { rmap.add(&mut rdef, None); } @@ -991,7 +993,7 @@ mod tests { ); } - // allow deprecated App::data + // allow deprecated {App, Scope}::data #[allow(deprecated)] #[actix_rt::test] async fn test_override_data() { @@ -1011,7 +1013,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); } - // allow deprecated App::data + // allow deprecated `{App, Scope}::data` #[allow(deprecated)] #[actix_rt::test] async fn test_override_data_default_service() { diff --git a/src/service.rs b/src/service.rs index 8d73a87fa..592577467 100644 --- a/src/service.rs +++ b/src/service.rs @@ -17,9 +17,8 @@ use crate::{ dev::insert_slash, guard::Guard, info::ConnectionInfo, - request::HttpRequest, rmap::ResourceMap, - Error, HttpResponse, + Error, HttpRequest, HttpResponse, }; pub trait HttpServiceFactory { diff --git a/src/web.rs b/src/web.rs index 8662848a4..40ac46275 100644 --- a/src/web.rs +++ b/src/web.rs @@ -43,7 +43,6 @@ pub use crate::types::*; /// the exposed `Params` object: /// /// ``` -/// # extern crate actix_web; /// use actix_web::{web, App, HttpResponse}; /// /// let app = App::new().service( From 93aa86e30bd2bc664aac80d7f6e02769f9a64750 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 24 Jun 2021 15:11:01 +0100 Subject: [PATCH 12/88] clippy --- awc/src/request.rs | 7 ++++++- tests/test_error_propagation.rs | 29 ++++++++++++++--------------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/awc/src/request.rs b/awc/src/request.rs index 46dae7fa3..3f312f6e7 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -480,6 +480,7 @@ impl ClientRequest { // supported, so we cannot guess Accept-Encoding HTTP header. if slf.response_decompress { // Set Accept-Encoding with compression algorithm awc is built with. + #[allow(clippy::vec_init_then_push)] #[cfg(feature = "__compress")] let accept_encoding = { let mut encoding = vec![]; @@ -496,7 +497,11 @@ impl ClientRequest { #[cfg(feature = "compress-zstd")] encoding.push("zstd"); - assert!(!encoding.is_empty(), "encoding cannot be empty unless __compress feature has been explictily enabled."); + assert!( + !encoding.is_empty(), + "encoding can not be empty unless __compress feature has been explicitly enabled" + ); + encoding.join(", ") }; diff --git a/tests/test_error_propagation.rs b/tests/test_error_propagation.rs index 1b56615a0..3e7320920 100644 --- a/tests/test_error_propagation.rs +++ b/tests/test_error_propagation.rs @@ -1,12 +1,14 @@ -use actix_utils::future::{ok, Ready}; -use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; -use actix_web::test::{call_service, init_service, TestRequest}; -use actix_web::{HttpResponse, ResponseError}; -use futures_util::lock::Mutex; -use std::future::Future; -use std::pin::Pin; use std::sync::Arc; -use std::task::{Context, Poll}; + +use actix_utils::future::{ok, Ready}; +use actix_web::{ + dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, + get, + test::{call_service, init_service, TestRequest}, + ResponseError, +}; +use futures_core::future::LocalBoxFuture; +use futures_util::lock::Mutex; #[derive(Debug, Clone)] pub struct MyError; @@ -19,10 +21,9 @@ impl std::fmt::Display for MyError { } } -#[actix_web::get("/test")] +#[get("/test")] async fn test() -> Result { - Err(MyError)?; - Ok(HttpResponse::NoContent().finish()) + return Err(MyError.into()); } #[derive(Clone)] @@ -62,11 +63,9 @@ where { type Response = ServiceResponse; type Error = actix_web::Error; - type Future = Pin>>>; + type Future = LocalBoxFuture<'static, Result>; - fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { - self.service.poll_ready(cx) - } + forward_ready!(service); fn call(&self, req: ServiceRequest) -> Self::Future { let lock = self.was_error.clone(); From e559a197cc909a6ec9f60900645dfa26a195ca03 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 24 Jun 2021 15:30:11 +0100 Subject: [PATCH 13/88] remove comment --- src/app_service.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/app_service.rs b/src/app_service.rs index feb9f22a4..0e590e2b7 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -302,10 +302,6 @@ impl Service for AppRouting { true }); - // you might expect to find `req.add_data_container()` called here but `HttpRequest` objects - // are created with the root data already set (in `AppInitService::call`) and root data is - // retained when releasing requests back to the pool - if let Some((srv, _info)) = res { srv.call(req) } else { From 767e4efe224fe52e66fcb4378a25076ec8beeb9a Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Fri, 25 Jun 2021 05:53:53 -0400 Subject: [PATCH 14/88] Remove downcast macro from actix-http (#2291) --- actix-http/CHANGES.md | 4 ++ actix-http/src/lib.rs | 3 -- actix-http/src/macros.rs | 110 --------------------------------------- 3 files changed, 4 insertions(+), 113 deletions(-) delete mode 100644 actix-http/src/macros.rs diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index c8d65e393..435607463 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -4,6 +4,10 @@ ### Changed * Change compression algorithm features flags. [#2250] +### Removed +* `downcast` and `downcast_get_type_id` macros. [#2291] + +[#2291]: https://github.com/actix/actix-web/pull/2291 [#2250]: https://github.com/actix/actix-web/pull/2250 diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index 924d5441f..d22e1ee44 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -27,9 +27,6 @@ #[macro_use] extern crate log; -#[macro_use] -mod macros; - pub mod body; mod builder; pub mod client; diff --git a/actix-http/src/macros.rs b/actix-http/src/macros.rs deleted file mode 100644 index be8e63d6e..000000000 --- a/actix-http/src/macros.rs +++ /dev/null @@ -1,110 +0,0 @@ -#[macro_export] -#[doc(hidden)] -macro_rules! downcast_get_type_id { - () => { - /// A helper method to get the type ID of the type - /// this trait is implemented on. - /// This method is unsafe to *implement*, since `downcast_ref` relies - /// on the returned `TypeId` to perform a cast. - /// - /// Unfortunately, Rust has no notion of a trait method that is - /// unsafe to implement (marking it as `unsafe` makes it unsafe - /// to *call*). As a workaround, we require this method - /// to return a private type along with the `TypeId`. This - /// private type (`PrivateHelper`) has a private constructor, - /// making it impossible for safe code to construct outside of - /// this module. This ensures that safe code cannot violate - /// type-safety by implementing this method. - /// - /// We also take `PrivateHelper` as a parameter, to ensure that - /// safe code cannot obtain a `PrivateHelper` instance by - /// delegating to an existing implementation of `__private_get_type_id__` - #[doc(hidden)] - fn __private_get_type_id__( - &self, - _: PrivateHelper, - ) -> (std::any::TypeId, PrivateHelper) - where - Self: 'static, - { - (std::any::TypeId::of::(), PrivateHelper(())) - } - }; -} - -//Generate implementation for dyn $name -#[doc(hidden)] -#[macro_export] -macro_rules! downcast { - ($name:ident) => { - /// A struct with a private constructor, for use with - /// `__private_get_type_id__`. Its single field is private, - /// ensuring that it can only be constructed from this module - #[doc(hidden)] - pub struct PrivateHelper(()); - - impl dyn $name + 'static { - /// Downcasts generic body to a specific type. - pub fn downcast_ref(&self) -> Option<&T> { - if self.__private_get_type_id__(PrivateHelper(())).0 - == std::any::TypeId::of::() - { - // SAFETY: external crates cannot override the default - // implementation of `__private_get_type_id__`, since - // it requires returning a private type. We can therefore - // rely on the returned `TypeId`, which ensures that this - // case is correct. - unsafe { Some(&*(self as *const dyn $name as *const T)) } - } else { - None - } - } - - /// Downcasts a generic body to a mutable specific type. - pub fn downcast_mut(&mut self) -> Option<&mut T> { - if self.__private_get_type_id__(PrivateHelper(())).0 - == std::any::TypeId::of::() - { - // SAFETY: external crates cannot override the default - // implementation of `__private_get_type_id__`, since - // it requires returning a private type. We can therefore - // rely on the returned `TypeId`, which ensures that this - // case is correct. - unsafe { - Some(&mut *(self as *const dyn $name as *const T as *mut T)) - } - } else { - None - } - } - } - }; -} - -#[cfg(test)] -mod tests { - #![allow(clippy::upper_case_acronyms)] - - trait MB { - downcast_get_type_id!(); - } - - downcast!(MB); - - impl MB for String {} - impl MB for () {} - - #[actix_rt::test] - async fn test_any_casting() { - let mut body = String::from("hello cast"); - let resp_body: &mut dyn MB = &mut body; - let body = resp_body.downcast_ref::().unwrap(); - assert_eq!(body, "hello cast"); - let body = &mut resp_body.downcast_mut::().unwrap(); - body.push('!'); - let body = resp_body.downcast_ref::().unwrap(); - assert_eq!(body, "hello cast!"); - let not_body = resp_body.downcast_ref::<()>(); - assert!(not_body.is_none()); - } -} From 2eacb735a4f2f284eeaf80244a119ca89621234d Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Fri, 25 Jun 2021 07:25:50 -0400 Subject: [PATCH 15/88] Don't leak internal macros (#2290) --- src/error/macros.rs | 16 ++++------ src/error/mod.rs | 1 + src/error/response_error.rs | 6 ++-- src/http/header/accept.rs | 10 +++--- src/http/header/accept_charset.rs | 4 +-- src/http/header/accept_encoding.rs | 10 +++--- src/http/header/accept_language.rs | 6 ++-- src/http/header/allow.rs | 8 ++--- src/http/header/cache_control.rs | 4 +-- src/http/header/content_language.rs | 6 ++-- src/http/header/content_range.rs | 24 +++++++------- src/http/header/content_type.rs | 4 +-- src/http/header/date.rs | 4 +-- src/http/header/etag.rs | 32 +++++++++---------- src/http/header/expires.rs | 4 +-- src/http/header/if_match.rs | 8 ++--- src/http/header/if_modified_since.rs | 4 +-- src/http/header/if_none_match.rs | 12 +++---- src/http/header/if_range.rs | 6 ++-- src/http/header/if_unmodified_since.rs | 4 +-- src/http/header/last_modified.rs | 4 +-- src/http/header/macros.rs | 44 ++++++++++++-------------- src/http/header/mod.rs | 4 +++ 23 files changed, 113 insertions(+), 112 deletions(-) diff --git a/src/error/macros.rs b/src/error/macros.rs index aeab74308..38650c5e8 100644 --- a/src/error/macros.rs +++ b/src/error/macros.rs @@ -1,6 +1,4 @@ -#[macro_export] -#[doc(hidden)] -macro_rules! __downcast_get_type_id { +macro_rules! downcast_get_type_id { () => { /// A helper method to get the type ID of the type /// this trait is implemented on. @@ -30,10 +28,8 @@ macro_rules! __downcast_get_type_id { }; } -//Generate implementation for dyn $name -#[doc(hidden)] -#[macro_export] -macro_rules! __downcast_dyn { +// Generate implementation for dyn $name +macro_rules! downcast_dyn { ($name:ident) => { /// A struct with a private constructor, for use with /// `__private_get_type_id__`. Its single field is private, @@ -80,15 +76,17 @@ macro_rules! __downcast_dyn { }; } +pub(crate) use {downcast_dyn, downcast_get_type_id}; + #[cfg(test)] mod tests { #![allow(clippy::upper_case_acronyms)] trait MB { - __downcast_get_type_id!(); + downcast_get_type_id!(); } - __downcast_dyn!(MB); + downcast_dyn!(MB); impl MB for String {} impl MB for () {} diff --git a/src/error/mod.rs b/src/error/mod.rs index 637d6ff16..3ccd5bba6 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -18,6 +18,7 @@ mod response_error; pub use self::error::Error; pub use self::internal::*; pub use self::response_error::ResponseError; +pub(crate) use macros::{downcast_dyn, downcast_get_type_id}; /// A convenience [`Result`](std::result::Result) for Actix Web operations. /// diff --git a/src/error/response_error.rs b/src/error/response_error.rs index c58fff8be..41cf20eba 100644 --- a/src/error/response_error.rs +++ b/src/error/response_error.rs @@ -9,7 +9,7 @@ use std::{ use actix_http::{body::AnyBody, header, Response, StatusCode}; use bytes::BytesMut; -use crate::{__downcast_dyn, __downcast_get_type_id}; +use crate::error::{downcast_dyn, downcast_get_type_id}; use crate::{helpers, HttpResponse}; /// Errors that can generate responses. @@ -41,10 +41,10 @@ pub trait ResponseError: fmt::Debug + fmt::Display { res.set_body(AnyBody::from(buf)) } - __downcast_get_type_id!(); + downcast_get_type_id!(); } -__downcast_dyn!(ResponseError); +downcast_dyn!(ResponseError); impl ResponseError for Box {} diff --git a/src/http/header/accept.rs b/src/http/header/accept.rs index 1b6a963da..75366dfae 100644 --- a/src/http/header/accept.rs +++ b/src/http/header/accept.rs @@ -5,7 +5,7 @@ use mime::Mime; use super::{qitem, QualityItem}; use crate::http::header; -crate::__define_common_header! { +crate::http::header::common_header! { /// `Accept` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.2) /// /// The `Accept` header field can be used by user agents to specify @@ -81,14 +81,14 @@ crate::__define_common_header! { test_accept { // Tests from the RFC - crate::__common_header_test!( + crate::http::header::common_header_test!( test1, vec![b"audio/*; q=0.2, audio/basic"], Some(Accept(vec![ QualityItem::new("audio/*".parse().unwrap(), q(200)), qitem("audio/basic".parse().unwrap()), ]))); - crate::__common_header_test!( + crate::http::header::common_header_test!( test2, vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"], Some(Accept(vec![ @@ -100,13 +100,13 @@ crate::__define_common_header! { qitem("text/x-c".parse().unwrap()), ]))); // Custom tests - crate::__common_header_test!( + crate::http::header::common_header_test!( test3, vec![b"text/plain; charset=utf-8"], Some(Accept(vec![ qitem(mime::TEXT_PLAIN_UTF_8), ]))); - crate::__common_header_test!( + crate::http::header::common_header_test!( test4, vec![b"text/plain; charset=utf-8; q=0.5"], Some(Accept(vec![ diff --git a/src/http/header/accept_charset.rs b/src/http/header/accept_charset.rs index 2c6a0b9f6..bb7d86516 100644 --- a/src/http/header/accept_charset.rs +++ b/src/http/header/accept_charset.rs @@ -1,6 +1,6 @@ use super::{Charset, QualityItem, ACCEPT_CHARSET}; -crate::__define_common_header! { +crate::http::header::common_header! { /// `Accept-Charset` header, defined in /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.3) /// @@ -57,6 +57,6 @@ crate::__define_common_header! { test_accept_charset { // Test case from RFC - crate::__common_header_test!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]); + crate::http::header::common_header_test!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]); } } diff --git a/src/http/header/accept_encoding.rs b/src/http/header/accept_encoding.rs index 734a435b3..cfd29bf77 100644 --- a/src/http/header/accept_encoding.rs +++ b/src/http/header/accept_encoding.rs @@ -64,12 +64,12 @@ header! { test_accept_encoding { // From the RFC - crate::__common_header_test!(test1, vec![b"compress, gzip"]); - crate::__common_header_test!(test2, vec![b""], Some(AcceptEncoding(vec![]))); - crate::__common_header_test!(test3, vec![b"*"]); + crate::http::header::common_header_test!(test1, vec![b"compress, gzip"]); + crate::http::header::common_header_test!(test2, vec![b""], Some(AcceptEncoding(vec![]))); + crate::http::header::common_header_test!(test3, vec![b"*"]); // Note: Removed quality 1 from gzip - crate::__common_header_test!(test4, vec![b"compress;q=0.5, gzip"]); + crate::http::header::common_header_test!(test4, vec![b"compress;q=0.5, gzip"]); // Note: Removed quality 1 from gzip - crate::__common_header_test!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]); + crate::http::header::common_header_test!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]); } } diff --git a/src/http/header/accept_language.rs b/src/http/header/accept_language.rs index 034946d4d..1552f6578 100644 --- a/src/http/header/accept_language.rs +++ b/src/http/header/accept_language.rs @@ -2,7 +2,7 @@ use language_tags::LanguageTag; use super::{QualityItem, ACCEPT_LANGUAGE}; -crate::__define_common_header! { +crate::http::header::common_header! { /// `Accept-Language` header, defined in /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.5) /// @@ -53,9 +53,9 @@ crate::__define_common_header! { test_accept_language { // From the RFC - crate::__common_header_test!(test1, vec![b"da, en-gb;q=0.8, en;q=0.7"]); + crate::http::header::common_header_test!(test1, vec![b"da, en-gb;q=0.8, en;q=0.7"]); // Own test - crate::__common_header_test!( + crate::http::header::common_header_test!( test2, vec![b"en-US, en; q=0.5, fr"], Some(AcceptLanguage(vec![ qitem("en-US".parse().unwrap()), diff --git a/src/http/header/allow.rs b/src/http/header/allow.rs index 15a627b8f..946f70e0a 100644 --- a/src/http/header/allow.rs +++ b/src/http/header/allow.rs @@ -1,7 +1,7 @@ use crate::http::header; use actix_http::http::Method; -crate::__define_common_header! { +crate::http::header::common_header! { /// `Allow` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.4.1) /// /// The `Allow` header field lists the set of methods advertised as @@ -49,12 +49,12 @@ crate::__define_common_header! { test_allow { // From the RFC - crate::__common_header_test!( + crate::http::header::common_header_test!( test1, vec![b"GET, HEAD, PUT"], Some(HeaderField(vec![Method::GET, Method::HEAD, Method::PUT]))); // Own tests - crate::__common_header_test!( + crate::http::header::common_header_test!( test2, vec![b"OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH"], Some(HeaderField(vec![ @@ -67,7 +67,7 @@ crate::__define_common_header! { Method::TRACE, Method::CONNECT, Method::PATCH]))); - crate::__common_header_test!( + crate::http::header::common_header_test!( test3, vec![b""], Some(HeaderField(Vec::::new()))); diff --git a/src/http/header/cache_control.rs b/src/http/header/cache_control.rs index 620c576ae..05903e3a3 100644 --- a/src/http/header/cache_control.rs +++ b/src/http/header/cache_control.rs @@ -49,9 +49,9 @@ use crate::http::header; #[derive(PartialEq, Clone, Debug)] pub struct CacheControl(pub Vec); -crate::__common_header_deref!(CacheControl => Vec); +crate::http::header::common_header_deref!(CacheControl => Vec); -// TODO: this could just be the __define_common_header! macro +// TODO: this could just be the crate::http::header::common_header! macro impl Header for CacheControl { fn name() -> header::HeaderName { header::CACHE_CONTROL diff --git a/src/http/header/content_language.rs b/src/http/header/content_language.rs index c2469edd1..604ada83c 100644 --- a/src/http/header/content_language.rs +++ b/src/http/header/content_language.rs @@ -1,7 +1,7 @@ use super::{QualityItem, CONTENT_LANGUAGE}; use language_tags::LanguageTag; -crate::__define_common_header! { +crate::http::header::common_header! { /// `Content-Language` header, defined in /// [RFC7231](https://tools.ietf.org/html/rfc7231#section-3.1.3.2) /// @@ -50,7 +50,7 @@ crate::__define_common_header! { (ContentLanguage, CONTENT_LANGUAGE) => (QualityItem)+ test_content_language { - crate::__common_header_test!(test1, vec![b"da"]); - crate::__common_header_test!(test2, vec![b"mi, en"]); + crate::http::header::common_header_test!(test1, vec![b"da"]); + crate::http::header::common_header_test!(test2, vec![b"mi, en"]); } } diff --git a/src/http/header/content_range.rs b/src/http/header/content_range.rs index ba0d51742..3bdead2c0 100644 --- a/src/http/header/content_range.rs +++ b/src/http/header/content_range.rs @@ -4,65 +4,65 @@ use std::str::FromStr; use super::{HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer, CONTENT_RANGE}; use crate::error::ParseError; -crate::__define_common_header! { +crate::http::header::common_header! { /// `Content-Range` header, defined in /// [RFC7233](http://tools.ietf.org/html/rfc7233#section-4.2) (ContentRange, CONTENT_RANGE) => [ContentRangeSpec] test_content_range { - crate::__common_header_test!(test_bytes, + crate::http::header::common_header_test!(test_bytes, vec![b"bytes 0-499/500"], Some(ContentRange(ContentRangeSpec::Bytes { range: Some((0, 499)), instance_length: Some(500) }))); - crate::__common_header_test!(test_bytes_unknown_len, + crate::http::header::common_header_test!(test_bytes_unknown_len, vec![b"bytes 0-499/*"], Some(ContentRange(ContentRangeSpec::Bytes { range: Some((0, 499)), instance_length: None }))); - crate::__common_header_test!(test_bytes_unknown_range, + crate::http::header::common_header_test!(test_bytes_unknown_range, vec![b"bytes */500"], Some(ContentRange(ContentRangeSpec::Bytes { range: None, instance_length: Some(500) }))); - crate::__common_header_test!(test_unregistered, + crate::http::header::common_header_test!(test_unregistered, vec![b"seconds 1-2"], Some(ContentRange(ContentRangeSpec::Unregistered { unit: "seconds".to_owned(), resp: "1-2".to_owned() }))); - crate::__common_header_test!(test_no_len, + crate::http::header::common_header_test!(test_no_len, vec![b"bytes 0-499"], None::); - crate::__common_header_test!(test_only_unit, + crate::http::header::common_header_test!(test_only_unit, vec![b"bytes"], None::); - crate::__common_header_test!(test_end_less_than_start, + crate::http::header::common_header_test!(test_end_less_than_start, vec![b"bytes 499-0/500"], None::); - crate::__common_header_test!(test_blank, + crate::http::header::common_header_test!(test_blank, vec![b""], None::); - crate::__common_header_test!(test_bytes_many_spaces, + crate::http::header::common_header_test!(test_bytes_many_spaces, vec![b"bytes 1-2/500 3"], None::); - crate::__common_header_test!(test_bytes_many_slashes, + crate::http::header::common_header_test!(test_bytes_many_slashes, vec![b"bytes 1-2/500/600"], None::); - crate::__common_header_test!(test_bytes_many_dashes, + crate::http::header::common_header_test!(test_bytes_many_dashes, vec![b"bytes 1-2-3/500"], None::); diff --git a/src/http/header/content_type.rs b/src/http/header/content_type.rs index 65cb2a986..e1c419c22 100644 --- a/src/http/header/content_type.rs +++ b/src/http/header/content_type.rs @@ -1,7 +1,7 @@ use super::CONTENT_TYPE; use mime::Mime; -crate::__define_common_header! { +crate::http::header::common_header! { /// `Content-Type` header, defined in /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-3.1.1.5) /// @@ -52,7 +52,7 @@ crate::__define_common_header! { (ContentType, CONTENT_TYPE) => [Mime] test_content_type { - crate::__common_header_test!( + crate::http::header::common_header_test!( test1, vec![b"text/html"], Some(HeaderField(mime::TEXT_HTML))); diff --git a/src/http/header/date.rs b/src/http/header/date.rs index 982a1455c..4d1717886 100644 --- a/src/http/header/date.rs +++ b/src/http/header/date.rs @@ -1,7 +1,7 @@ use super::{HttpDate, DATE}; use std::time::SystemTime; -crate::__define_common_header! { +crate::http::header::common_header! { /// `Date` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.1.1.2) /// /// The `Date` header field represents the date and time at which the @@ -32,7 +32,7 @@ crate::__define_common_header! { (Date, DATE) => [HttpDate] test_date { - crate::__common_header_test!(test1, vec![b"Tue, 15 Nov 1994 08:12:31 GMT"]); + crate::http::header::common_header_test!(test1, vec![b"Tue, 15 Nov 1994 08:12:31 GMT"]); } } diff --git a/src/http/header/etag.rs b/src/http/header/etag.rs index b121fe26f..aded72665 100644 --- a/src/http/header/etag.rs +++ b/src/http/header/etag.rs @@ -1,6 +1,6 @@ use super::{EntityTag, ETAG}; -crate::__define_common_header! { +crate::http::header::common_header! { /// `ETag` header, defined in [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.3) /// /// The `ETag` header field in a response provides the current entity-tag @@ -50,50 +50,50 @@ crate::__define_common_header! { test_etag { // From the RFC - crate::__common_header_test!(test1, + crate::http::header::common_header_test!(test1, vec![b"\"xyzzy\""], Some(ETag(EntityTag::new(false, "xyzzy".to_owned())))); - crate::__common_header_test!(test2, + crate::http::header::common_header_test!(test2, vec![b"W/\"xyzzy\""], Some(ETag(EntityTag::new(true, "xyzzy".to_owned())))); - crate::__common_header_test!(test3, + crate::http::header::common_header_test!(test3, vec![b"\"\""], Some(ETag(EntityTag::new(false, "".to_owned())))); // Own tests - crate::__common_header_test!(test4, + crate::http::header::common_header_test!(test4, vec![b"\"foobar\""], Some(ETag(EntityTag::new(false, "foobar".to_owned())))); - crate::__common_header_test!(test5, + crate::http::header::common_header_test!(test5, vec![b"\"\""], Some(ETag(EntityTag::new(false, "".to_owned())))); - crate::__common_header_test!(test6, + crate::http::header::common_header_test!(test6, vec![b"W/\"weak-etag\""], Some(ETag(EntityTag::new(true, "weak-etag".to_owned())))); - crate::__common_header_test!(test7, + crate::http::header::common_header_test!(test7, vec![b"W/\"\x65\x62\""], Some(ETag(EntityTag::new(true, "\u{0065}\u{0062}".to_owned())))); - crate::__common_header_test!(test8, + crate::http::header::common_header_test!(test8, vec![b"W/\"\""], Some(ETag(EntityTag::new(true, "".to_owned())))); - crate::__common_header_test!(test9, + crate::http::header::common_header_test!(test9, vec![b"no-dquotes"], None::); - crate::__common_header_test!(test10, + crate::http::header::common_header_test!(test10, vec![b"w/\"the-first-w-is-case-sensitive\""], None::); - crate::__common_header_test!(test11, + crate::http::header::common_header_test!(test11, vec![b""], None::); - crate::__common_header_test!(test12, + crate::http::header::common_header_test!(test12, vec![b"\"unmatched-dquotes1"], None::); - crate::__common_header_test!(test13, + crate::http::header::common_header_test!(test13, vec![b"unmatched-dquotes2\""], None::); - crate::__common_header_test!(test14, + crate::http::header::common_header_test!(test14, vec![b"matched-\"dquotes\""], None::); - crate::__common_header_test!(test15, + crate::http::header::common_header_test!(test15, vec![b"\""], None::); } diff --git a/src/http/header/expires.rs b/src/http/header/expires.rs index 759e7d280..e810fe267 100644 --- a/src/http/header/expires.rs +++ b/src/http/header/expires.rs @@ -1,6 +1,6 @@ use super::{HttpDate, EXPIRES}; -crate::__define_common_header! { +crate::http::header::common_header! { /// `Expires` header, defined in [RFC7234](http://tools.ietf.org/html/rfc7234#section-5.3) /// /// The `Expires` header field gives the date/time after which the @@ -36,6 +36,6 @@ crate::__define_common_header! { test_expires { // Test case from RFC - crate::__common_header_test!(test1, vec![b"Thu, 01 Dec 1994 16:00:00 GMT"]); + crate::http::header::common_header_test!(test1, vec![b"Thu, 01 Dec 1994 16:00:00 GMT"]); } } diff --git a/src/http/header/if_match.rs b/src/http/header/if_match.rs index d4402715d..87a94a809 100644 --- a/src/http/header/if_match.rs +++ b/src/http/header/if_match.rs @@ -1,6 +1,6 @@ use super::{EntityTag, IF_MATCH}; -crate::__define_common_header! { +crate::http::header::common_header! { /// `If-Match` header, defined in /// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.1) /// @@ -53,18 +53,18 @@ crate::__define_common_header! { (IfMatch, IF_MATCH) => {Any / (EntityTag)+} test_if_match { - crate::__common_header_test!( + crate::http::header::common_header_test!( test1, vec![b"\"xyzzy\""], Some(HeaderField::Items( vec![EntityTag::new(false, "xyzzy".to_owned())]))); - crate::__common_header_test!( + crate::http::header::common_header_test!( test2, vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""], Some(HeaderField::Items( vec![EntityTag::new(false, "xyzzy".to_owned()), EntityTag::new(false, "r2d2xxxx".to_owned()), EntityTag::new(false, "c3piozzzz".to_owned())]))); - crate::__common_header_test!(test3, vec![b"*"], Some(IfMatch::Any)); + crate::http::header::common_header_test!(test3, vec![b"*"], Some(IfMatch::Any)); } } diff --git a/src/http/header/if_modified_since.rs b/src/http/header/if_modified_since.rs index ba393032d..254003523 100644 --- a/src/http/header/if_modified_since.rs +++ b/src/http/header/if_modified_since.rs @@ -1,6 +1,6 @@ use super::{HttpDate, IF_MODIFIED_SINCE}; -crate::__define_common_header! { +crate::http::header::common_header! { /// `If-Modified-Since` header, defined in /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.3) /// @@ -36,6 +36,6 @@ crate::__define_common_header! { test_if_modified_since { // Test case from RFC - crate::__common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); + crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); } } diff --git a/src/http/header/if_none_match.rs b/src/http/header/if_none_match.rs index f16b196cc..e1422bd36 100644 --- a/src/http/header/if_none_match.rs +++ b/src/http/header/if_none_match.rs @@ -1,6 +1,6 @@ use super::{EntityTag, IF_NONE_MATCH}; -crate::__define_common_header! { +crate::http::header::common_header! { /// `If-None-Match` header, defined in /// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.2) /// @@ -55,11 +55,11 @@ crate::__define_common_header! { (IfNoneMatch, IF_NONE_MATCH) => {Any / (EntityTag)+} test_if_none_match { - crate::__common_header_test!(test1, vec![b"\"xyzzy\""]); - crate::__common_header_test!(test2, vec![b"W/\"xyzzy\""]); - crate::__common_header_test!(test3, vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""]); - crate::__common_header_test!(test4, vec![b"W/\"xyzzy\", W/\"r2d2xxxx\", W/\"c3piozzzz\""]); - crate::__common_header_test!(test5, vec![b"*"]); + crate::http::header::common_header_test!(test1, vec![b"\"xyzzy\""]); + crate::http::header::common_header_test!(test2, vec![b"W/\"xyzzy\""]); + crate::http::header::common_header_test!(test3, vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""]); + crate::http::header::common_header_test!(test4, vec![b"W/\"xyzzy\", W/\"r2d2xxxx\", W/\"c3piozzzz\""]); + crate::http::header::common_header_test!(test5, vec![b"*"]); } } diff --git a/src/http/header/if_range.rs b/src/http/header/if_range.rs index 9612405e8..cf69e7269 100644 --- a/src/http/header/if_range.rs +++ b/src/http/header/if_range.rs @@ -113,7 +113,7 @@ mod test_if_range { use crate::http::header::*; use std::str; - crate::__common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); - crate::__common_header_test!(test2, vec![b"\"abc\""]); - crate::__common_header_test!(test3, vec![b"this-is-invalid"], None::); + crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); + crate::http::header::common_header_test!(test2, vec![b"\"abc\""]); + crate::http::header::common_header_test!(test3, vec![b"this-is-invalid"], None::); } diff --git a/src/http/header/if_unmodified_since.rs b/src/http/header/if_unmodified_since.rs index 26b16b513..1cc7b304e 100644 --- a/src/http/header/if_unmodified_since.rs +++ b/src/http/header/if_unmodified_since.rs @@ -1,6 +1,6 @@ use super::{HttpDate, IF_UNMODIFIED_SINCE}; -crate::__define_common_header! { +crate::http::header::common_header! { /// `If-Unmodified-Since` header, defined in /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.4) /// @@ -37,6 +37,6 @@ crate::__define_common_header! { test_if_unmodified_since { // Test case from RFC - crate::__common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); + crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); } } diff --git a/src/http/header/last_modified.rs b/src/http/header/last_modified.rs index 0de2fc06b..c43bf3ac9 100644 --- a/src/http/header/last_modified.rs +++ b/src/http/header/last_modified.rs @@ -1,6 +1,6 @@ use super::{HttpDate, LAST_MODIFIED}; -crate::__define_common_header! { +crate::http::header::common_header! { /// `Last-Modified` header, defined in /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.2) /// @@ -36,6 +36,6 @@ crate::__define_common_header! { test_last_modified { // Test case from RFC - crate::__common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); + crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); } } diff --git a/src/http/header/macros.rs b/src/http/header/macros.rs index 1718a8663..419d4fb6e 100644 --- a/src/http/header/macros.rs +++ b/src/http/header/macros.rs @@ -1,6 +1,4 @@ -#[doc(hidden)] -#[macro_export] -macro_rules! __common_header_deref { +macro_rules! common_header_deref { ($from:ty => $to:ty) => { impl ::std::ops::Deref for $from { type Target = $to; @@ -20,9 +18,7 @@ macro_rules! __common_header_deref { }; } -#[doc(hidden)] -#[macro_export] -macro_rules! __common_header_test_module { +macro_rules! common_header_test_module { ($id:ident, $tm:ident{$($tf:item)*}) => { #[allow(unused_imports)] #[cfg(test)] @@ -37,9 +33,8 @@ macro_rules! __common_header_test_module { } } -#[doc(hidden)] -#[macro_export] -macro_rules! __common_header_test { +#[cfg(test)] +macro_rules! common_header_test { ($id:ident, $raw:expr) => { #[test] fn $id() { @@ -99,9 +94,7 @@ macro_rules! __common_header_test { }; } -#[doc(hidden)] -#[macro_export] -macro_rules! __define_common_header { +macro_rules! common_header { // $a:meta: Attributes associated with the header item (usually docs) // $id:ident: Identifier of the header // $n:expr: Lowercase name of the header @@ -112,7 +105,7 @@ macro_rules! __define_common_header { $(#[$a])* #[derive(Clone, Debug, PartialEq)] pub struct $id(pub Vec<$item>); - crate::__common_header_deref!($id => Vec<$item>); + crate::http::header::common_header_deref!($id => Vec<$item>); impl $crate::http::header::Header for $id { #[inline] fn name() -> $crate::http::header::HeaderName { @@ -148,7 +141,7 @@ macro_rules! __define_common_header { $(#[$a])* #[derive(Clone, Debug, PartialEq)] pub struct $id(pub Vec<$item>); - crate::__common_header_deref!($id => Vec<$item>); + crate::http::header::common_header_deref!($id => Vec<$item>); impl $crate::http::header::Header for $id { #[inline] fn name() -> $crate::http::header::HeaderName { @@ -184,7 +177,7 @@ macro_rules! __define_common_header { $(#[$a])* #[derive(Clone, Debug, PartialEq)] pub struct $id(pub $value); - crate::__common_header_deref!($id => $value); + crate::http::header::common_header_deref!($id => $value); impl $crate::http::header::Header for $id { #[inline] fn name() -> $crate::http::header::HeaderName { @@ -267,34 +260,39 @@ macro_rules! __define_common_header { // optional test module ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)* $tm:ident{$($tf:item)*}) => { - crate::__define_common_header! { + crate::http::header::common_header! { $(#[$a])* ($id, $name) => ($item)* } - crate::__common_header_test_module! { $id, $tm { $($tf)* }} + crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }} }; ($(#[$a:meta])*($id:ident, $n:expr) => ($item:ty)+ $tm:ident{$($tf:item)*}) => { - crate::__define_common_header! { + crate::http::header::common_header! { $(#[$a])* ($id, $n) => ($item)+ } - crate::__common_header_test_module! { $id, $tm { $($tf)* }} + crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }} }; ($(#[$a:meta])*($id:ident, $name:expr) => [$item:ty] $tm:ident{$($tf:item)*}) => { - crate::__define_common_header! { + crate::http::header::common_header! { $(#[$a])* ($id, $name) => [$item] } - crate::__common_header_test_module! { $id, $tm { $($tf)* }} + crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }} }; ($(#[$a:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+} $tm:ident{$($tf:item)*}) => { - crate::__define_common_header! { + crate::http::header::common_header! { $(#[$a])* ($id, $name) => {Any / ($item)+} } - crate::__common_header_test_module! { $id, $tm { $($tf)* }} + crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }} }; } + +pub(crate) use {common_header, common_header_deref, common_header_test_module}; + +#[cfg(test)] +pub(crate) use common_header_test; diff --git a/src/http/header/mod.rs b/src/http/header/mod.rs index 0e5651a77..79ba5772b 100644 --- a/src/http/header/mod.rs +++ b/src/http/header/mod.rs @@ -84,4 +84,8 @@ mod if_none_match; mod if_range; mod if_unmodified_since; mod last_modified; + mod macros; +#[cfg(test)] +pub(crate) use macros::common_header_test; +pub(crate) use macros::{common_header, common_header_deref, common_header_test_module}; From 9a263933757ab6e0bd1a6a688b8bce04584e7dfe Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Fri, 25 Jun 2021 15:27:22 +0400 Subject: [PATCH 16/88] Remove duplicated step from CI workflow (#2289) --- .github/workflows/ci.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be595e35c..8bc04dbd7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,17 +44,11 @@ jobs: profile: minimal override: true - - name: Install ${{ matrix.version }} - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ matrix.version }}-${{ matrix.target.triple }} - profile: minimal - override: true - - name: Generate Cargo.lock uses: actions-rs/cargo@v1 with: command: generate-lockfile + - name: Cache Dependencies uses: Swatinem/rust-cache@v1.2.0 @@ -102,6 +96,7 @@ jobs: run: | cargo install cargo-tarpaulin --vers "^0.13" cargo tarpaulin --out Xml --verbose + - name: Upload to Codecov if: > matrix.target.os == 'ubuntu-latest' From 5a480d1d789fc55e9c26434f4294d8ac13eea1bf Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 25 Jun 2021 12:28:04 +0100 Subject: [PATCH 17/88] re-add serde error impls --- src/error/response_error.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/error/response_error.rs b/src/error/response_error.rs index 41cf20eba..c3c543419 100644 --- a/src/error/response_error.rs +++ b/src/error/response_error.rs @@ -57,6 +57,10 @@ impl ResponseError for serde::de::value::Error { } } +impl ResponseError for serde_json::Error {} + +impl ResponseError for serde_urlencoded::ser::Error {} + impl ResponseError for std::str::Utf8Error { fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST From 539697292adabb0f682004f47d81b9ccd9eba121 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 25 Jun 2021 13:19:42 +0100 Subject: [PATCH 18/88] fix scope and resource middleware data access (#2288) --- CHANGES.md | 4 ++ src/app.rs | 7 ++-- src/app_service.rs | 26 ++++++------- src/config.rs | 2 +- src/resource.rs | 95 +++++++++++++++++++++++++++------------------- src/scope.rs | 83 ++++++++++++++++++++++++++++------------ 6 files changed, 136 insertions(+), 81 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 876e1c03d..3da742f9d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,11 +11,15 @@ * Change compression algorithm features flags. [#2250] * Deprecate `App::data` and `App::data_factory`. [#2271] +### Fixed +* Scope and Resource middleware can access data items set on their own layer. [#2288] + [#2177]: https://github.com/actix/actix-web/pull/2177 [#2250]: https://github.com/actix/actix-web/pull/2250 [#2271]: https://github.com/actix/actix-web/pull/2271 [#2262]: https://github.com/actix/actix-web/pull/2262 [#2263]: https://github.com/actix/actix-web/pull/2263 +[#2288]: https://github.com/actix/actix-web/pull/2288 ## 4.0.0-beta.7 - 2021-06-17 diff --git a/src/app.rs b/src/app.rs index 677f73805..5cff20568 100644 --- a/src/app.rs +++ b/src/app.rs @@ -43,13 +43,14 @@ impl App { /// Create application builder. Application can be configured with a builder-like pattern. #[allow(clippy::new_without_default)] pub fn new() -> Self { - let fref = Rc::new(RefCell::new(None)); + let factory_ref = Rc::new(RefCell::new(None)); + App { - endpoint: AppEntry::new(fref.clone()), + endpoint: AppEntry::new(factory_ref.clone()), data_factories: Vec::new(), services: Vec::new(), default: None, - factory_ref: fref, + factory_ref, external: Vec::new(), extensions: Extensions::new(), _phantom: PhantomData, diff --git a/src/app_service.rs b/src/app_service.rs index 0e590e2b7..bdb7ec433 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -1,22 +1,22 @@ -use std::cell::RefCell; -use std::rc::Rc; +use std::{cell::RefCell, mem, rc::Rc}; use actix_http::{Extensions, Request}; use actix_router::{Path, ResourceDef, Router, Url}; -use actix_service::boxed::{self, BoxService, BoxServiceFactory}; -use actix_service::{fn_service, Service, ServiceFactory}; +use actix_service::{ + boxed::{self, BoxService, BoxServiceFactory}, + fn_service, Service, ServiceFactory, +}; use futures_core::future::LocalBoxFuture; use futures_util::future::join_all; -use crate::data::FnDataFactory; -use crate::error::Error; -use crate::guard::Guard; -use crate::request::{HttpRequest, HttpRequestPool}; -use crate::rmap::ResourceMap; -use crate::service::{AppServiceFactory, ServiceRequest, ServiceResponse}; use crate::{ config::{AppConfig, AppService}, - HttpResponse, + data::FnDataFactory, + guard::Guard, + request::{HttpRequest, HttpRequestPool}, + rmap::ResourceMap, + service::{AppServiceFactory, ServiceRequest, ServiceResponse}, + Error, HttpResponse, }; type Guards = Vec>; @@ -75,7 +75,7 @@ where let mut config = AppService::new(config, default.clone()); // register services - std::mem::take(&mut *self.services.borrow_mut()) + mem::take(&mut *self.services.borrow_mut()) .into_iter() .for_each(|mut srv| srv.register(&mut config)); @@ -98,7 +98,7 @@ where }); // external resources - for mut rdef in std::mem::take(&mut *self.external.borrow_mut()) { + for mut rdef in mem::take(&mut *self.external.borrow_mut()) { rmap.add(&mut rdef, None); } diff --git a/src/config.rs b/src/config.rs index 966141193..884128308 100644 --- a/src/config.rs +++ b/src/config.rs @@ -94,9 +94,9 @@ impl AppService { F: IntoServiceFactory, S: ServiceFactory< ServiceRequest, - Config = (), Response = ServiceResponse, Error = Error, + Config = (), InitError = (), > + 'static, { diff --git a/src/resource.rs b/src/resource.rs index 7a4c1248b..9f5cf3cb2 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -400,34 +400,28 @@ where *rdef.name_mut() = name.clone(); } - config.register_service(rdef, guards, self, None) - } -} - -impl IntoServiceFactory for Resource -where - T: ServiceFactory< - ServiceRequest, - Config = (), - Response = ServiceResponse, - Error = Error, - InitError = (), - >, -{ - fn into_factory(self) -> T { *self.factory_ref.borrow_mut() = Some(ResourceFactory { routes: self.routes, - app_data: self.app_data.map(Rc::new), default: self.default, }); - self.endpoint + let resource_data = self.app_data.map(Rc::new); + + // wraps endpoint service (including middleware) call and injects app data for this scope + let endpoint = apply_fn_factory(self.endpoint, move |mut req: ServiceRequest, srv| { + if let Some(ref data) = resource_data { + req.add_data_container(Rc::clone(data)); + } + + srv.call(req) + }); + + config.register_service(rdef, guards, endpoint, None) } } pub struct ResourceFactory { routes: Vec, - app_data: Option>, default: HttpNewService, } @@ -446,8 +440,6 @@ impl ServiceFactory for ResourceFactory { // construct route service factory futures let factory_fut = join_all(self.routes.iter().map(|route| route.new_service(()))); - let app_data = self.app_data.clone(); - Box::pin(async move { let default = default_fut.await?; let routes = factory_fut @@ -455,18 +447,13 @@ impl ServiceFactory for ResourceFactory { .into_iter() .collect::, _>>()?; - Ok(ResourceService { - routes, - app_data, - default, - }) + Ok(ResourceService { routes, default }) }) } } pub struct ResourceService { routes: Vec, - app_data: Option>, default: HttpService, } @@ -480,18 +467,10 @@ impl Service for ResourceService { fn call(&self, mut req: ServiceRequest) -> Self::Future { for route in self.routes.iter() { if route.check(&mut req) { - if let Some(ref app_data) = self.app_data { - req.add_data_container(app_data.clone()); - } - return route.call(req); } } - if let Some(ref app_data) = self.app_data { - req.add_data_container(app_data.clone()); - } - self.default.call(req) } } @@ -528,11 +507,14 @@ mod tests { use actix_service::Service; use actix_utils::future::ok; - use crate::http::{header, HeaderValue, Method, StatusCode}; - use crate::middleware::DefaultHeaders; - use crate::service::ServiceRequest; - use crate::test::{call_service, init_service, TestRequest}; - use crate::{guard, web, App, Error, HttpResponse}; + use crate::{ + guard, + http::{header, HeaderValue, Method, StatusCode}, + middleware::DefaultHeaders, + service::{ServiceRequest, ServiceResponse}, + test::{call_service, init_service, TestRequest}, + web, App, Error, HttpMessage, HttpResponse, + }; #[actix_rt::test] async fn test_middleware() { @@ -753,4 +735,39 @@ mod tests { let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } + + #[actix_rt::test] + async fn test_middleware_app_data() { + let srv = init_service( + App::new().service( + web::resource("test") + .app_data(1usize) + .wrap_fn(|req, srv| { + assert_eq!(req.app_data::(), Some(&1usize)); + req.extensions_mut().insert(1usize); + srv.call(req) + }) + .route(web::get().to(HttpResponse::Ok)) + .default_service(|req: ServiceRequest| async move { + let (req, _) = req.into_parts(); + + assert_eq!(req.extensions().get::(), Some(&1)); + + Ok(ServiceResponse::new( + req, + HttpResponse::BadRequest().finish(), + )) + }), + ), + ) + .await; + + let req = TestRequest::get().uri("/test").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::post().uri("/test").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } } diff --git a/src/scope.rs b/src/scope.rs index 0caf06ee3..aa546c422 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -427,7 +427,6 @@ where // complete scope pipeline creation *self.factory_ref.borrow_mut() = Some(ScopeFactory { - app_data: self.app_data.take().map(Rc::new), default, services: cfg .into_services() @@ -449,18 +448,28 @@ where Some(self.guards) }; + let scope_data = self.app_data.map(Rc::new); + + // wraps endpoint service (including middleware) call and injects app data for this scope + let endpoint = apply_fn_factory(self.endpoint, move |mut req: ServiceRequest, srv| { + if let Some(ref data) = scope_data { + req.add_data_container(Rc::clone(data)); + } + + srv.call(req) + }); + // register final service config.register_service( ResourceDef::root_prefix(&self.rdef), guards, - self.endpoint, + endpoint, Some(Rc::new(rmap)), ) } } pub struct ScopeFactory { - app_data: Option>, services: Rc<[(ResourceDef, HttpNewService, RefCell>)]>, default: Rc, } @@ -488,8 +497,6 @@ impl ServiceFactory for ScopeFactory { } })); - let app_data = self.app_data.clone(); - Box::pin(async move { let default = default_fut.await?; @@ -505,17 +512,12 @@ impl ServiceFactory for ScopeFactory { }) .finish(); - Ok(ScopeService { - app_data, - router, - default, - }) + Ok(ScopeService { router, default }) }) } } pub struct ScopeService { - app_data: Option>, router: Router>>, default: HttpService, } @@ -539,10 +541,6 @@ impl Service for ScopeService { true }); - if let Some(ref app_data) = self.app_data { - req.add_data_container(app_data.clone()); - } - if let Some((srv, _info)) = res { srv.call(req) } else { @@ -581,12 +579,15 @@ mod tests { use actix_utils::future::ok; use bytes::Bytes; - use crate::dev::Body; - use crate::http::{header, HeaderValue, Method, StatusCode}; - use crate::middleware::DefaultHeaders; - use crate::service::ServiceRequest; - use crate::test::{call_service, init_service, read_body, TestRequest}; - use crate::{guard, web, App, HttpRequest, HttpResponse}; + use crate::{ + dev::Body, + guard, + http::{header, HeaderValue, Method, StatusCode}, + middleware::DefaultHeaders, + service::{ServiceRequest, ServiceResponse}, + test::{call_service, init_service, read_body, TestRequest}, + web, App, HttpMessage, HttpRequest, HttpResponse, + }; #[actix_rt::test] async fn test_scope() { @@ -918,10 +919,7 @@ mod tests { async fn test_default_resource_propagation() { let srv = init_service( App::new() - .service( - web::scope("/app1") - .default_service(web::resource("").to(HttpResponse::BadRequest)), - ) + .service(web::scope("/app1").default_service(web::to(HttpResponse::BadRequest))) .service(web::scope("/app2")) .default_service(|r: ServiceRequest| { ok(r.into_response(HttpResponse::MethodNotAllowed())) @@ -993,6 +991,41 @@ mod tests { ); } + #[actix_rt::test] + async fn test_middleware_app_data() { + let srv = init_service( + App::new().service( + web::scope("app") + .app_data(1usize) + .wrap_fn(|req, srv| { + assert_eq!(req.app_data::(), Some(&1usize)); + req.extensions_mut().insert(1usize); + srv.call(req) + }) + .route("/test", web::get().to(HttpResponse::Ok)) + .default_service(|req: ServiceRequest| async move { + let (req, _) = req.into_parts(); + + assert_eq!(req.extensions().get::(), Some(&1)); + + Ok(ServiceResponse::new( + req, + HttpResponse::BadRequest().finish(), + )) + }), + ), + ) + .await; + + let req = TestRequest::with_uri("/app/test").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/app/default").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } + // allow deprecated {App, Scope}::data #[allow(deprecated)] #[actix_rt::test] From 09afd033fce961db3f46e4aabdaefe85e99fb5d2 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Fri, 25 Jun 2021 16:21:57 +0300 Subject: [PATCH 19/88] files: file path filtering closure (#2274) Co-authored-by: Rob Ede --- actix-files/CHANGES.md | 3 +++ actix-files/src/files.rs | 50 +++++++++++++++++++++++++++++++++++--- actix-files/src/lib.rs | 41 ++++++++++++++++++++++++++++++- actix-files/src/service.rs | 15 +++++++++++- 4 files changed, 104 insertions(+), 5 deletions(-) diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index bec67dd4e..54b0344a4 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx +* Added `Files::path_filter()`. [#2274] + +[#2274]: https://github.com/actix/actix-web/pull/2274 ## 0.6.0-beta.5 - 2021-06-17 diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs index c48cf59a7..49d81eb03 100644 --- a/actix-files/src/files.rs +++ b/actix-files/src/files.rs @@ -1,9 +1,17 @@ -use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc}; +use std::{ + cell::RefCell, + fmt, io, + path::{Path, PathBuf}, + rc::Rc, +}; use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt}; use actix_utils::future::ok; use actix_web::{ - dev::{AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse}, + dev::{ + AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest, + ServiceResponse, + }, error::Error, guard::Guard, http::header::DispositionType, @@ -13,7 +21,7 @@ use futures_core::future::LocalBoxFuture; use crate::{ directory_listing, named, Directory, DirectoryRenderer, FilesService, HttpNewService, - MimeOverride, + MimeOverride, PathFilter, }; /// Static files handling service. @@ -36,6 +44,7 @@ pub struct Files { default: Rc>>>, renderer: Rc, mime_override: Option>, + path_filter: Option>, file_flags: named::Flags, use_guards: Option>, guards: Vec>, @@ -60,6 +69,7 @@ impl Clone for Files { file_flags: self.file_flags, path: self.path.clone(), mime_override: self.mime_override.clone(), + path_filter: self.path_filter.clone(), use_guards: self.use_guards.clone(), guards: self.guards.clone(), hidden_files: self.hidden_files, @@ -104,6 +114,7 @@ impl Files { default: Rc::new(RefCell::new(None)), renderer: Rc::new(directory_listing), mime_override: None, + path_filter: None, file_flags: named::Flags::default(), use_guards: None, guards: Vec::new(), @@ -149,6 +160,38 @@ impl Files { self } + /// Sets path filtering closure. + /// + /// The path provided to the closure is relative to `serve_from` path. + /// You can safely join this path with the `serve_from` path to get the real path. + /// However, the real path may not exist since the filter is called before checking path existence. + /// + /// When a path doesn't pass the filter, [`Files::default_handler`] is called if set, otherwise, + /// `404 Not Found` is returned. + /// + /// # Examples + /// ``` + /// use std::path::Path; + /// use actix_files::Files; + /// + /// // prevent searching subdirectories and following symlinks + /// let files_service = Files::new("/", "./static").path_filter(|path, _| { + /// path.components().count() == 1 + /// && Path::new("./static") + /// .join(path) + /// .symlink_metadata() + /// .map(|m| !m.file_type().is_symlink()) + /// .unwrap_or(false) + /// }); + /// ``` + pub fn path_filter(mut self, f: F) -> Self + where + F: Fn(&Path, &RequestHead) -> bool + 'static, + { + self.path_filter = Some(Rc::new(f)); + self + } + /// Set index file /// /// Shows specific index file for directories instead of @@ -318,6 +361,7 @@ impl ServiceFactory for Files { default: None, renderer: self.renderer.clone(), mime_override: self.mime_override.clone(), + path_filter: self.path_filter.clone(), file_flags: self.file_flags, guards: self.use_guards.clone(), hidden_files: self.hidden_files, diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index c9cc79193..1eb091aaf 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -16,11 +16,12 @@ use actix_service::boxed::{BoxService, BoxServiceFactory}; use actix_web::{ - dev::{ServiceRequest, ServiceResponse}, + dev::{RequestHead, ServiceRequest, ServiceResponse}, error::Error, http::header::DispositionType, }; use mime_guess::from_ext; +use std::path::Path; mod chunked; mod directory; @@ -56,6 +57,8 @@ pub fn file_extension_to_mime(ext: &str) -> mime::Mime { type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType; +type PathFilter = dyn Fn(&Path, &RequestHead) -> bool; + #[cfg(test)] mod tests { use std::{ @@ -901,4 +904,40 @@ mod tests { let bytes = test::read_body(resp).await; assert!(format!("{:?}", bytes).contains("/tests/test.png")); } + + #[actix_rt::test] + async fn test_path_filter() { + // prevent searching subdirectories + let st = Files::new("/", ".") + .path_filter(|path, _| path.components().count() == 1) + .new_service(()) + .await + .unwrap(); + + let req = TestRequest::with_uri("/Cargo.toml").to_srv_request(); + let resp = test::call_service(&st, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/src/lib.rs").to_srv_request(); + let resp = test::call_service(&st, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[actix_rt::test] + async fn test_default_handler_filter() { + let st = Files::new("/", ".") + .default_handler(|req: ServiceRequest| { + ok(req.into_response(HttpResponse::Ok().body("default content"))) + }) + .path_filter(|path, _| path.extension() == Some("png".as_ref())) + .new_service(()) + .await + .unwrap(); + let req = TestRequest::with_uri("/Cargo.toml").to_srv_request(); + let resp = test::call_service(&st, req).await; + + assert_eq!(resp.status(), StatusCode::OK); + let bytes = test::read_body(resp).await; + assert_eq!(bytes, web::Bytes::from_static(b"default content")); + } } diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs index 64938e5ef..09122c63e 100644 --- a/actix-files/src/service.rs +++ b/actix-files/src/service.rs @@ -13,7 +13,7 @@ use futures_core::future::LocalBoxFuture; use crate::{ named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile, - PathBufWrap, + PathBufWrap, PathFilter, }; /// Assembled file serving service. @@ -25,6 +25,7 @@ pub struct FilesService { pub(crate) default: Option, pub(crate) renderer: Rc, pub(crate) mime_override: Option>, + pub(crate) path_filter: Option>, pub(crate) file_flags: named::Flags, pub(crate) guards: Option>, pub(crate) hidden_files: bool, @@ -82,6 +83,18 @@ impl Service for FilesService { Err(e) => return Box::pin(ok(req.error_response(e))), }; + if let Some(filter) = &self.path_filter { + if !filter(real_path.as_ref(), req.head()) { + if let Some(ref default) = self.default { + return Box::pin(default.call(req)); + } else { + return Box::pin(ok( + req.into_response(actix_web::HttpResponse::NotFound().finish()) + )); + } + } + } + // full file path let path = self.directory.join(&real_path); if let Err(err) = path.canonicalize() { From 5eba95b731856ab3d1c4fd7f4fb18f5e647d021a Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Fri, 25 Jun 2021 19:39:06 -0400 Subject: [PATCH 20/88] simplify `ConnectionInfo::new` (#2282) --- .github/workflows/ci.yml | 2 - CHANGES.md | 2 + Cargo.toml | 2 +- src/config.rs | 5 + src/info.rs | 355 +++++++++++++++++++++++++-------------- src/test.rs | 5 + 6 files changed, 245 insertions(+), 126 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8bc04dbd7..22b92759a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,6 @@ jobs: uses: actions-rs/cargo@v1 with: command: generate-lockfile - - name: Cache Dependencies uses: Swatinem/rust-cache@v1.2.0 @@ -96,7 +95,6 @@ jobs: run: | cargo install cargo-tarpaulin --vers "^0.13" cargo tarpaulin --out Xml --verbose - - name: Upload to Codecov if: > matrix.target.os == 'ubuntu-latest' diff --git a/CHANGES.md b/CHANGES.md index 3da742f9d..b4320d783 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ ### Changed * Change compression algorithm features flags. [#2250] * Deprecate `App::data` and `App::data_factory`. [#2271] +* Smarter extraction of `ConnectionInfo` parts. [#2282] ### Fixed * Scope and Resource middleware can access data items set on their own layer. [#2288] @@ -19,6 +20,7 @@ [#2271]: https://github.com/actix/actix-web/pull/2271 [#2262]: https://github.com/actix/actix-web/pull/2262 [#2263]: https://github.com/actix/actix-web/pull/2263 +[#2282]: https://github.com/actix/actix-web/pull/2282 [#2288]: https://github.com/actix/actix-web/pull/2288 diff --git a/Cargo.toml b/Cargo.toml index 779b52255..293e6bccd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,7 +107,7 @@ actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] } awc = { version = "3.0.0-beta.6", features = ["openssl"] } brotli2 = "0.3.2" -criterion = "0.3" +criterion = { version = "0.3", features = ["html_reports"] } env_logger = "0.8" flate2 = "1.0.13" zstd = "0.7" diff --git a/src/config.rs b/src/config.rs index 884128308..b072ace16 100644 --- a/src/config.rs +++ b/src/config.rs @@ -144,6 +144,11 @@ impl AppConfig { pub fn local_addr(&self) -> SocketAddr { self.addr } + + #[cfg(test)] + pub(crate) fn set_host(&mut self, host: &str) { + self.host = host.to_owned(); + } } impl Default for AppConfig { diff --git a/src/info.rs b/src/info.rs index c6ff54efe..1f8263add 100644 --- a/src/info.rs +++ b/src/info.rs @@ -2,16 +2,35 @@ use std::{cell::Ref, convert::Infallible, net::SocketAddr}; use actix_utils::future::{err, ok, Ready}; use derive_more::{Display, Error}; +use once_cell::sync::Lazy; use crate::{ dev::{AppConfig, Payload, RequestHead}, - http::header::{self, HeaderName}, + http::{ + header::{self, HeaderName}, + uri::{Authority, Scheme}, + }, FromRequest, HttpRequest, ResponseError, }; -const X_FORWARDED_FOR: &[u8] = b"x-forwarded-for"; -const X_FORWARDED_HOST: &[u8] = b"x-forwarded-host"; -const X_FORWARDED_PROTO: &[u8] = b"x-forwarded-proto"; +static X_FORWARDED_FOR: Lazy = + Lazy::new(|| HeaderName::from_static("x-forwarded-for")); +static X_FORWARDED_HOST: Lazy = + Lazy::new(|| HeaderName::from_static("x-forwarded-host")); +static X_FORWARDED_PROTO: Lazy = + Lazy::new(|| HeaderName::from_static("x-forwarded-proto")); + +/// Trim whitespace then any quote marks. +fn unquote(val: &str) -> &str { + val.trim().trim_start_matches('"').trim_end_matches('"') +} + +/// Extracts and trims first value for given header name. +fn first_header_value<'a>(req: &'a RequestHead, name: &'_ HeaderName) -> Option<&'a str> { + let hdr = req.headers.get(name)?.to_str().ok()?; + let val = hdr.split(',').next()?.trim(); + Some(val) +} /// HTTP connection information. /// @@ -31,6 +50,19 @@ const X_FORWARDED_PROTO: &[u8] = b"x-forwarded-proto"; /// } /// # let _svc = actix_web::web::to(handler); /// ``` +/// +/// # Implementation Notes +/// Parses `Forwarded` header information according to [RFC 7239][rfc7239] but does not try to +/// interpret the values for each property. As such, the getter methods on `ConnectionInfo` return +/// strings instead of IP addresses or other types to acknowledge that they may be +/// [obfuscated][rfc7239-63] or [unknown][rfc7239-62]. +/// +/// If the older, related headers are also present (eg. `X-Forwarded-For`), then `Forwarded` +/// is preferred. +/// +/// [rfc7239]: https://datatracker.ietf.org/doc/html/rfc7239 +/// [rfc7239-62]: https://datatracker.ietf.org/doc/html/rfc7239#section-6.2 +/// [rfc7239-63]: https://datatracker.ietf.org/doc/html/rfc7239#section-6.3 #[derive(Debug, Clone, Default)] pub struct ConnectionInfo { scheme: String, @@ -48,105 +80,75 @@ impl ConnectionInfo { Ref::map(req.extensions(), |e| e.get().unwrap()) } - #[allow(clippy::cognitive_complexity, clippy::borrow_interior_mutable_const)] fn new(req: &RequestHead, cfg: &AppConfig) -> ConnectionInfo { let mut host = None; let mut scheme = None; let mut realip_remote_addr = None; - // load forwarded header - for hdr in req.headers.get_all(&header::FORWARDED) { - if let Ok(val) = hdr.to_str() { - for pair in val.split(';') { - for el in pair.split(',') { - let mut items = el.trim().splitn(2, '='); - if let Some(name) = items.next() { - if let Some(val) = items.next() { - match &name.to_lowercase() as &str { - "for" => { - if realip_remote_addr.is_none() { - realip_remote_addr = Some(val.trim()); - } - } - "proto" => { - if scheme.is_none() { - scheme = Some(val.trim()); - } - } - "host" => { - if host.is_none() { - host = Some(val.trim()); - } - } - _ => {} - } - } - } - } + for (name, val) in req + .headers + .get_all(&header::FORWARDED) + .into_iter() + .filter_map(|hdr| hdr.to_str().ok()) + // "for=1.2.3.4, for=5.6.7.8; scheme=https" + .flat_map(|val| val.split(';')) + // ["for=1.2.3.4, for=5.6.7.8", " scheme=https"] + .flat_map(|vals| vals.split(',')) + // ["for=1.2.3.4", " for=5.6.7.8", " scheme=https"] + .flat_map(|pair| { + let mut items = pair.trim().splitn(2, '='); + Some((items.next()?, items.next()?)) + }) + { + // [(name , val ), ... ] + // [("for", "1.2.3.4"), ("for", "5.6.7.8"), ("scheme", "https")] + + // taking the first value for each property is correct because spec states that first + // "for" value is client and rest are proxies; multiple values other properties have + // no defined semantics + // + // > In a chain of proxy servers where this is fully utilized, the first + // > "for" parameter will disclose the client where the request was first + // > made, followed by any subsequent proxy identifiers. + // --- https://datatracker.ietf.org/doc/html/rfc7239#section-5.2 + + match name.trim().to_lowercase().as_str() { + "for" => realip_remote_addr.get_or_insert_with(|| unquote(val)), + "proto" => scheme.get_or_insert_with(|| unquote(val)), + "host" => host.get_or_insert_with(|| unquote(val)), + "by" => { + // TODO: implement https://datatracker.ietf.org/doc/html/rfc7239#section-5.1 + continue; } - } + _ => continue, + }; } - // scheme - if scheme.is_none() { - if let Some(h) = req - .headers - .get(&HeaderName::from_lowercase(X_FORWARDED_PROTO).unwrap()) - { - if let Ok(h) = h.to_str() { - scheme = h.split(',').next().map(|v| v.trim()); - } - } - if scheme.is_none() { - scheme = req.uri.scheme().map(|a| a.as_str()); - if scheme.is_none() && cfg.secure() { - scheme = Some("https") - } - } - } + let scheme = scheme + .or_else(|| first_header_value(req, &*X_FORWARDED_PROTO)) + .or_else(|| req.uri.scheme().map(Scheme::as_str)) + .or_else(|| Some("https").filter(|_| cfg.secure())) + .unwrap_or("http") + .to_owned(); - // host - if host.is_none() { - if let Some(h) = req - .headers - .get(&HeaderName::from_lowercase(X_FORWARDED_HOST).unwrap()) - { - if let Ok(h) = h.to_str() { - host = h.split(',').next().map(|v| v.trim()); - } - } - if host.is_none() { - if let Some(h) = req.headers.get(&header::HOST) { - host = h.to_str().ok(); - } - if host.is_none() { - host = req.uri.authority().map(|a| a.as_str()); - if host.is_none() { - host = Some(cfg.host()); - } - } - } - } + let host = host + .or_else(|| first_header_value(req, &*X_FORWARDED_HOST)) + .or_else(|| req.headers.get(&header::HOST)?.to_str().ok()) + .or_else(|| req.uri.authority().map(Authority::as_str)) + .unwrap_or(cfg.host()) + .to_owned(); - // get remote_addraddr from socketaddr - let remote_addr = req.peer_addr.map(|addr| format!("{}", addr)); + let realip_remote_addr = realip_remote_addr + .or_else(|| first_header_value(req, &*X_FORWARDED_FOR)) + .map(str::to_owned); - if realip_remote_addr.is_none() { - if let Some(h) = req - .headers - .get(&HeaderName::from_lowercase(X_FORWARDED_FOR).unwrap()) - { - if let Ok(h) = h.to_str() { - realip_remote_addr = h.split(',').next().map(|v| v.trim()); - } - } - } + let remote_addr = req.peer_addr.map(|addr| addr.to_string()); ConnectionInfo { remote_addr, - scheme: scheme.unwrap_or("http").to_owned(), - host: host.unwrap_or("localhost").to_owned(), - realip_remote_addr: realip_remote_addr.map(|s| s.to_owned()), + scheme, + host, + realip_remote_addr, } } @@ -175,19 +177,16 @@ impl ConnectionInfo { &self.host } - /// remote_addr address of the request. + /// Remote address of the connection. /// - /// Get remote_addr address from socket address + /// Get remote_addr address from socket address. pub fn remote_addr(&self) -> Option<&str> { - if let Some(ref remote_addr) = self.remote_addr { - Some(remote_addr) - } else { - None - } + self.remote_addr.as_deref() } - /// Real ip remote addr of client initiated HTTP request. + + /// Real IP (remote address) of client that initiated request. /// - /// The addr is resolved through the following headers, in this order: + /// The address is resolved through the following headers, in this order: /// /// - Forwarded /// - X-Forwarded-For @@ -196,17 +195,14 @@ impl ConnectionInfo { /// # Security /// Do not use this function for security purposes, unless you can ensure the Forwarded and /// X-Forwarded-For headers cannot be spoofed by the client. If you want the client's socket - /// address explicitly, use - /// [`HttpRequest::peer_addr()`](super::web::HttpRequest::peer_addr()) instead. + /// address explicitly, use [`HttpRequest::peer_addr()`][peer_addr] instead. + /// + /// [peer_addr]: crate::web::HttpRequest::peer_addr() #[inline] pub fn realip_remote_addr(&self) -> Option<&str> { - if let Some(ref r) = self.realip_remote_addr { - Some(r) - } else if let Some(ref remote_addr) = self.remote_addr { - Some(remote_addr) - } else { - None - } + self.realip_remote_addr + .as_deref() + .or_else(|| self.remote_addr.as_deref()) } } @@ -274,13 +270,60 @@ mod tests { use super::*; use crate::test::TestRequest; + const X_FORWARDED_FOR: &str = "x-forwarded-for"; + const X_FORWARDED_HOST: &str = "x-forwarded-host"; + const X_FORWARDED_PROTO: &str = "x-forwarded-proto"; + #[test] - fn test_forwarded() { + fn info_default() { let req = TestRequest::default().to_http_request(); let info = req.connection_info(); assert_eq!(info.scheme(), "http"); assert_eq!(info.host(), "localhost:8080"); + } + #[test] + fn host_header() { + let req = TestRequest::default() + .insert_header((header::HOST, "rust-lang.org")) + .to_http_request(); + + let info = req.connection_info(); + assert_eq!(info.scheme(), "http"); + assert_eq!(info.host(), "rust-lang.org"); + assert_eq!(info.realip_remote_addr(), None); + } + + #[test] + fn x_forwarded_for_header() { + let req = TestRequest::default() + .insert_header((X_FORWARDED_FOR, "192.0.2.60")) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); + } + + #[test] + fn x_forwarded_host_header() { + let req = TestRequest::default() + .insert_header((X_FORWARDED_HOST, "192.0.2.60")) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.host(), "192.0.2.60"); + assert_eq!(info.realip_remote_addr(), None); + } + + #[test] + fn x_forwarded_proto_header() { + let req = TestRequest::default() + .insert_header((X_FORWARDED_PROTO, "https")) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.scheme(), "https"); + } + + #[test] + fn forwarded_header() { let req = TestRequest::default() .insert_header(( header::FORWARDED, @@ -294,45 +337,111 @@ mod tests { assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); let req = TestRequest::default() - .insert_header((header::HOST, "rust-lang.org")) + .insert_header(( + header::FORWARDED, + "for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org", + )) .to_http_request(); let info = req.connection_info(); - assert_eq!(info.scheme(), "http"); + assert_eq!(info.scheme(), "https"); assert_eq!(info.host(), "rust-lang.org"); - assert_eq!(info.realip_remote_addr(), None); + assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); + } + #[test] + fn forwarded_case_sensitivity() { let req = TestRequest::default() - .insert_header((X_FORWARDED_FOR, "192.0.2.60")) + .insert_header((header::FORWARDED, "For=192.0.2.60")) .to_http_request(); let info = req.connection_info(); assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); + } + #[test] + fn forwarded_weird_whitespace() { let req = TestRequest::default() - .insert_header((X_FORWARDED_HOST, "192.0.2.60")) + .insert_header((header::FORWARDED, "for= 1.2.3.4; proto= https")) .to_http_request(); let info = req.connection_info(); - assert_eq!(info.host(), "192.0.2.60"); - assert_eq!(info.realip_remote_addr(), None); + assert_eq!(info.realip_remote_addr(), Some("1.2.3.4")); + assert_eq!(info.scheme(), "https"); let req = TestRequest::default() - .insert_header((X_FORWARDED_PROTO, "https")) + .insert_header((header::FORWARDED, " for = 1.2.3.4 ")) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.realip_remote_addr(), Some("1.2.3.4")); + } + + #[test] + fn forwarded_for_quoted() { + let req = TestRequest::default() + .insert_header((header::FORWARDED, r#"for="192.0.2.60:8080""#)) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.realip_remote_addr(), Some("192.0.2.60:8080")); + } + + #[test] + fn forwarded_for_ipv6() { + let req = TestRequest::default() + .insert_header((header::FORWARDED, r#"for="[2001:db8:cafe::17]:4711""#)) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.realip_remote_addr(), Some("[2001:db8:cafe::17]:4711")); + } + + #[test] + fn forwarded_for_multiple() { + let req = TestRequest::default() + .insert_header((header::FORWARDED, "for=192.0.2.60, for=198.51.100.17")) + .to_http_request(); + let info = req.connection_info(); + // takes the first value + assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); + } + + #[test] + fn scheme_from_uri() { + let req = TestRequest::get() + .uri("https://actix.rs/test") .to_http_request(); let info = req.connection_info(); assert_eq!(info.scheme(), "https"); } - #[actix_rt::test] - async fn test_conn_info() { - let req = TestRequest::default() - .uri("http://actix.rs/") + #[test] + fn host_from_uri() { + let req = TestRequest::get() + .uri("https://actix.rs/test") .to_http_request(); - let conn_info = ConnectionInfo::extract(&req).await.unwrap(); - assert_eq!(conn_info.scheme(), "http"); + let info = req.connection_info(); + assert_eq!(info.host(), "actix.rs"); + } + + #[test] + fn host_from_server_hostname() { + let mut req = TestRequest::get(); + req.set_server_hostname("actix.rs"); + let req = req.to_http_request(); + + let info = req.connection_info(); + assert_eq!(info.host(), "actix.rs"); } #[actix_rt::test] - async fn test_peer_addr() { + async fn conn_info_extract() { + let req = TestRequest::default() + .uri("https://actix.rs/test") + .to_http_request(); + let conn_info = ConnectionInfo::extract(&req).await.unwrap(); + assert_eq!(conn_info.scheme(), "https"); + assert_eq!(conn_info.host(), "actix.rs"); + } + + #[actix_rt::test] + async fn peer_addr_extract() { let addr = "127.0.0.1:8080".parse().unwrap(); let req = TestRequest::default().peer_addr(addr).to_http_request(); let peer_addr = PeerAddr::extract(&req).await.unwrap(); diff --git a/src/test.rs b/src/test.rs index 05a4ba7f2..634826d19 100644 --- a/src/test.rs +++ b/src/test.rs @@ -613,6 +613,11 @@ impl TestRequest { let req = self.to_request(); call_service(app, req).await } + + #[cfg(test)] + pub fn set_server_hostname(&mut self, host: &str) { + self.config.set_host(host) + } } #[cfg(test)] From 262c6bc828d58a4acf0d1c8b291f5a6c6b3f82fd Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Sat, 26 Jun 2021 18:33:43 +0400 Subject: [PATCH 21/88] Various refactorings (#2281) Co-authored-by: Rob Ede --- .gitignore | 3 ++ CHANGES.md | 1 + actix-files/src/named.rs | 8 +++--- actix-http/benches/uninit-headers.rs | 6 ++-- actix-http/src/client/connector.rs | 8 ++---- actix-http/src/client/h2proto.rs | 13 ++++----- actix-http/src/config.rs | 8 +++--- actix-http/src/error.rs | 2 +- actix-http/src/h1/decoder.rs | 6 ++-- actix-http/src/h1/dispatcher.rs | 3 +- actix-http/src/h1/payload.rs | 6 ++-- actix-http/src/header/map.rs | 6 ++-- actix-http/src/message.rs | 27 +++++++++--------- actix-web-codegen/src/route.rs | 7 ++--- awc/src/request.rs | 4 ++- awc/src/ws.rs | 2 +- benches/responder.rs | 11 ++++---- benches/service.rs | 6 ++-- src/app_service.rs | 4 +-- src/http/header/content_disposition.rs | 18 ++++-------- src/middleware/compress.rs | 3 +- src/request.rs | 6 +--- src/resource.rs | 2 +- src/response/builder.rs | 2 +- src/server.rs | 39 ++++++++++++-------------- src/service.rs | 6 +--- src/types/form.rs | 3 +- src/types/path.rs | 3 +- src/types/payload.rs | 3 +- src/types/query.rs | 3 +- 30 files changed, 98 insertions(+), 121 deletions(-) diff --git a/.gitignore b/.gitignore index 638a4397a..543403267 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ guide/build/ # Configuration directory generated by CLion .idea + +# Configuration directory generated by VSCode +.vscode diff --git a/CHANGES.md b/CHANGES.md index b4320d783..f6a0b28c8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ * Deprecate `App::data` and `App::data_factory`. [#2271] * Smarter extraction of `ConnectionInfo` parts. [#2282] + ### Fixed * Scope and Resource middleware can access data items set on their own layer. [#2288] diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 37f8def3e..241e78cf0 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -355,8 +355,8 @@ impl NamedFile { } else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) = (last_modified, req.get_header()) { - let t1: SystemTime = m.clone().into(); - let t2: SystemTime = since.clone().into(); + let t1: SystemTime = (*m).into(); + let t2: SystemTime = (*since).into(); match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { (Ok(t1), Ok(t2)) => t1.as_secs() > t2.as_secs(), @@ -374,8 +374,8 @@ impl NamedFile { } else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) = (last_modified, req.get_header()) { - let t1: SystemTime = m.clone().into(); - let t2: SystemTime = since.clone().into(); + let t1: SystemTime = (*m).into(); + let t2: SystemTime = (*since).into(); match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { (Ok(t1), Ok(t2)) => t1.as_secs() <= t2.as_secs(), diff --git a/actix-http/benches/uninit-headers.rs b/actix-http/benches/uninit-headers.rs index 83e74171c..53a2528ab 100644 --- a/actix-http/benches/uninit-headers.rs +++ b/actix-http/benches/uninit-headers.rs @@ -78,12 +78,12 @@ impl HeaderIndex { // test cases taken from: // https://github.com/seanmonstar/httparse/blob/master/benches/parse.rs -const REQ_SHORT: &'static [u8] = b"\ +const REQ_SHORT: &[u8] = b"\ GET / HTTP/1.0\r\n\ Host: example.com\r\n\ Cookie: session=60; user_id=1\r\n\r\n"; -const REQ: &'static [u8] = b"\ +const REQ: &[u8] = b"\ GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n\ Host: www.kittyhell.com\r\n\ User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 Pathtraq/0.9\r\n\ @@ -119,6 +119,8 @@ mod _original { use std::mem::MaybeUninit; pub fn parse_headers(src: &mut BytesMut) -> usize { + #![allow(clippy::uninit_assumed_init)] + let mut headers: [HeaderIndex; MAX_HEADERS] = unsafe { MaybeUninit::uninit().assume_init() }; diff --git a/actix-http/src/client/connector.rs b/actix-http/src/client/connector.rs index 508fe748b..bd46919e8 100644 --- a/actix-http/src/client/connector.rs +++ b/actix-http/src/client/connector.rs @@ -85,7 +85,7 @@ impl Connector<()> { use bytes::{BufMut, BytesMut}; let mut alpn = BytesMut::with_capacity(20); - for proto in protocols.iter() { + for proto in &protocols { alpn.put_u8(proto.len() as u8); alpn.put(proto.as_slice()); } @@ -290,8 +290,7 @@ where let h2 = sock .ssl() .selected_alpn_protocol() - .map(|protos| protos.windows(2).any(|w| w == H2)) - .unwrap_or(false); + .map_or(false, |protos| protos.windows(2).any(|w| w == H2)); if h2 { (Box::new(sock), Protocol::Http2) } else { @@ -325,8 +324,7 @@ where .get_ref() .1 .get_alpn_protocol() - .map(|protos| protos.windows(2).any(|w| w == H2)) - .unwrap_or(false); + .map_or(false, |protos| protos.windows(2).any(|w| w == H2)); if h2 { (Box::new(sock), Protocol::Http2) } else { diff --git a/actix-http/src/client/h2proto.rs b/actix-http/src/client/h2proto.rs index cf423ef12..b9d5f96bd 100644 --- a/actix-http/src/client/h2proto.rs +++ b/actix-http/src/client/h2proto.rs @@ -168,14 +168,13 @@ where if let Err(e) = send.send_data(bytes, false) { return Err(e.into()); - } else { - if !b.is_empty() { - send.reserve_capacity(b.len()); - } else { - buf = None; - } - continue; } + if !b.is_empty() { + send.reserve_capacity(b.len()); + } else { + buf = None; + } + continue; } Some(Err(e)) => return Err(e.into()), } diff --git a/actix-http/src/config.rs b/actix-http/src/config.rs index 9a2293e92..0e01e8748 100644 --- a/actix-http/src/config.rs +++ b/actix-http/src/config.rs @@ -152,8 +152,8 @@ impl ServiceConfig { } } - #[inline] /// Return keep-alive timer delay is configured. + #[inline] pub fn keep_alive_timer(&self) -> Option { self.keep_alive().map(|ka| sleep_until(self.now() + ka)) } @@ -365,11 +365,11 @@ mod tests { let clone3 = service.clone(); drop(clone1); - assert_eq!(false, notify_on_drop::is_dropped()); + assert!(!notify_on_drop::is_dropped()); drop(clone2); - assert_eq!(false, notify_on_drop::is_dropped()); + assert!(!notify_on_drop::is_dropped()); drop(clone3); - assert_eq!(false, notify_on_drop::is_dropped()); + assert!(!notify_on_drop::is_dropped()); drop(service); assert!(notify_on_drop::is_dropped()); diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index d9e1a1ed2..6c3d692c3 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -125,7 +125,7 @@ impl fmt::Display for Error { impl StdError for Error { fn source(&self) -> Option<&(dyn StdError + 'static)> { - self.inner.cause.as_ref().map(|err| err.as_ref()) + self.inner.cause.as_ref().map(Box::as_ref) } } diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index 8aba9f623..f240710c2 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -102,7 +102,7 @@ pub(crate) trait MessageType: Sized { } // transfer-encoding header::TRANSFER_ENCODING => { - if let Ok(s) = value.to_str().map(|s| s.trim()) { + if let Ok(s) = value.to_str().map(str::trim) { chunked = s.eq_ignore_ascii_case("chunked"); } else { return Err(ParseError::Header); @@ -110,7 +110,7 @@ pub(crate) trait MessageType: Sized { } // connection keep-alive state header::CONNECTION => { - ka = if let Ok(conn) = value.to_str().map(|conn| conn.trim()) { + ka = if let Ok(conn) = value.to_str().map(str::trim) { if conn.eq_ignore_ascii_case("keep-alive") { Some(ConnectionType::KeepAlive) } else if conn.eq_ignore_ascii_case("close") { @@ -125,7 +125,7 @@ pub(crate) trait MessageType: Sized { }; } header::UPGRADE => { - if let Ok(val) = value.to_str().map(|val| val.trim()) { + if let Ok(val) = value.to_str().map(str::trim) { if val.eq_ignore_ascii_case("websocket") { has_upgrade_websocket = true; } diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index b4adde638..deb25763c 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -515,14 +515,13 @@ where cx: &mut Context<'_>, ) -> Result<(), DispatchError> { // Handle `EXPECT: 100-Continue` header + let mut this = self.as_mut().project(); if req.head().expect() { // set dispatcher state so the future is pinned. - let mut this = self.as_mut().project(); let task = this.flow.expect.call(req); this.state.set(State::ExpectCall(task)); } else { // the same as above. - let mut this = self.as_mut().project(); let task = this.flow.service.call(req); this.state.set(State::ServiceCall(task)); }; diff --git a/actix-http/src/h1/payload.rs b/actix-http/src/h1/payload.rs index e72493fa2..cc771f28a 100644 --- a/actix-http/src/h1/payload.rs +++ b/actix-http/src/h1/payload.rs @@ -186,8 +186,7 @@ impl Inner { if self .task .as_ref() - .map(|w| !cx.waker().will_wake(w)) - .unwrap_or(true) + .map_or(true, |w| !cx.waker().will_wake(w)) { self.task = Some(cx.waker().clone()); } @@ -199,8 +198,7 @@ impl Inner { if self .io_task .as_ref() - .map(|w| !cx.waker().will_wake(w)) - .unwrap_or(true) + .map_or(true, |w| !cx.waker().will_wake(w)) { self.io_task = Some(cx.waker().clone()); } diff --git a/actix-http/src/header/map.rs b/actix-http/src/header/map.rs index be33ec02a..634d9282f 100644 --- a/actix-http/src/header/map.rs +++ b/actix-http/src/header/map.rs @@ -249,7 +249,7 @@ impl HeaderMap { /// assert!(map.get("INVALID HEADER NAME").is_none()); /// ``` pub fn get(&self, key: impl AsHeaderName) -> Option<&HeaderValue> { - self.get_value(key).map(|val| val.first()) + self.get_value(key).map(Value::first) } /// Returns a mutable reference to the _first_ value associated a header name. @@ -280,8 +280,8 @@ impl HeaderMap { /// ``` pub fn get_mut(&mut self, key: impl AsHeaderName) -> Option<&mut HeaderValue> { match key.try_as_name(super::as_name::Seal).ok()? { - Cow::Borrowed(name) => self.inner.get_mut(name).map(|v| v.first_mut()), - Cow::Owned(name) => self.inner.get_mut(&name).map(|v| v.first_mut()), + Cow::Borrowed(name) => self.inner.get_mut(name).map(Value::first_mut), + Cow::Owned(name) => self.inner.get_mut(&name).map(Value::first_mut), } } diff --git a/actix-http/src/message.rs b/actix-http/src/message.rs index 0a3f3a915..e85d686b7 100644 --- a/actix-http/src/message.rs +++ b/actix-http/src/message.rs @@ -152,15 +152,16 @@ impl RequestHead { /// Connection upgrade status pub fn upgrade(&self) -> bool { - if let Some(hdr) = self.headers().get(header::CONNECTION) { - if let Ok(s) = hdr.to_str() { - s.to_ascii_lowercase().contains("upgrade") - } else { - false - } - } else { - false - } + self.headers() + .get(header::CONNECTION) + .map(|hdr| { + if let Ok(s) = hdr.to_str() { + s.to_ascii_lowercase().contains("upgrade") + } else { + false + } + }) + .unwrap_or(false) } #[inline] @@ -308,13 +309,11 @@ impl ResponseHead { /// Get custom reason for the response #[inline] pub fn reason(&self) -> &str { - if let Some(reason) = self.reason { - reason - } else { + self.reason.unwrap_or_else(|| { self.status .canonical_reason() .unwrap_or("") - } + }) } #[inline] @@ -356,7 +355,7 @@ pub struct Message { impl Message { /// Get new message from the pool of objects pub fn new() -> Self { - T::with_pool(|p| p.get_message()) + T::with_pool(MessagePool::get_message) } } diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index ac0b7cea1..747042527 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -6,7 +6,7 @@ use std::convert::TryFrom; use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{format_ident, quote, ToTokens, TokenStreamExt}; -use syn::{parse_macro_input, AttributeArgs, Ident, NestedMeta}; +use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, NestedMeta}; enum ResourceType { Async, @@ -227,8 +227,7 @@ impl Route { format!( r#"invalid service definition, expected #[{}("")]"#, method - .map(|it| it.as_str()) - .unwrap_or("route") + .map_or("route", |it| it.as_str()) .to_ascii_lowercase() ), )); @@ -298,7 +297,7 @@ impl ToTokens for Route { } = self; let resource_name = resource_name .as_ref() - .map_or_else(|| name.to_string(), |n| n.value()); + .map_or_else(|| name.to_string(), LitStr::value); let method_guards = { let mut others = methods.iter(); // unwrapping since length is checked to be at least one diff --git a/awc/src/request.rs b/awc/src/request.rs index 3f312f6e7..812c76318 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -486,7 +486,9 @@ impl ClientRequest { let mut encoding = vec![]; #[cfg(feature = "compress-brotli")] - encoding.push("br"); + { + encoding.push("br"); + } #[cfg(feature = "compress-gzip")] { diff --git a/awc/src/ws.rs b/awc/src/ws.rs index 34b71f052..2fe36399c 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -517,7 +517,7 @@ mod tests { "test-origin" ); assert_eq!(req.max_size, 100); - assert_eq!(req.server_mode, true); + assert!(req.server_mode); assert_eq!(req.protocols, Some("v1,v2".to_string())); assert_eq!( req.head.headers.get(header::CONTENT_TYPE).unwrap(), diff --git a/benches/responder.rs b/benches/responder.rs index 0dfc8cd18..5d0b98d5f 100644 --- a/benches/responder.rs +++ b/benches/responder.rs @@ -1,6 +1,5 @@ use std::{future::Future, time::Instant}; -use actix_http::Response; use actix_utils::future::{ready, Ready}; use actix_web::http::StatusCode; use actix_web::test::TestRequest; @@ -24,11 +23,11 @@ struct StringResponder(String); impl FutureResponder for StringResponder { type Error = Error; - type Future = Ready>; + type Future = Ready>; fn future_respond_to(self, _: &HttpRequest) -> Self::Future { // this is default builder for string response in both new and old responder trait. - ready(Ok(Response::build(StatusCode::OK) + ready(Ok(HttpResponse::build(StatusCode::OK) .content_type("text/plain; charset=utf-8") .body(self.0))) } @@ -37,7 +36,7 @@ impl FutureResponder for StringResponder { impl FutureResponder for OptionResponder where T: FutureResponder, - T::Future: Future>, + T::Future: Future>, { type Error = Error; type Future = Either>>; @@ -52,7 +51,7 @@ where impl Responder for StringResponder { fn respond_to(self, _: &HttpRequest) -> HttpResponse { - Response::build(StatusCode::OK) + HttpResponse::build(StatusCode::OK) .content_type("text/plain; charset=utf-8") .body(self.0) } @@ -62,7 +61,7 @@ impl Responder for OptionResponder { fn respond_to(self, req: &HttpRequest) -> HttpResponse { match self.0 { Some(t) => t.respond_to(req), - None => Response::from_error(error::ErrorInternalServerError("err")), + None => HttpResponse::from_error(error::ErrorInternalServerError("err")), } } } diff --git a/benches/service.rs b/benches/service.rs index 30708477d..87e51f170 100644 --- a/benches/service.rs +++ b/benches/service.rs @@ -51,9 +51,8 @@ where fut.await.unwrap(); } }); - let elapsed = start.elapsed(); // check that at least first request succeeded - elapsed + start.elapsed() }) }); } @@ -93,9 +92,8 @@ fn async_web_service(c: &mut Criterion) { fut.await.unwrap(); } }); - let elapsed = start.elapsed(); // check that at least first request succeeded - elapsed + start.elapsed() }) }); } diff --git a/src/app_service.rs b/src/app_service.rs index bdb7ec433..3c1b78474 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -131,9 +131,9 @@ where let service = endpoint_fut.await?; // populate app data container from (async) data factories. - async_data_factories.iter().for_each(|factory| { + for factory in &async_data_factories { factory.create(&mut app_data); - }); + } Ok(AppInitService { service, diff --git a/src/http/header/content_disposition.rs b/src/http/header/content_disposition.rs index 71c610157..9f67baffb 100644 --- a/src/http/header/content_disposition.rs +++ b/src/http/header/content_disposition.rs @@ -410,41 +410,33 @@ impl ContentDisposition { /// Return the value of *name* if exists. pub fn get_name(&self) -> Option<&str> { - self.parameters.iter().filter_map(|p| p.as_name()).next() + self.parameters.iter().find_map(DispositionParam::as_name) } /// Return the value of *filename* if exists. pub fn get_filename(&self) -> Option<&str> { self.parameters .iter() - .filter_map(|p| p.as_filename()) - .next() + .find_map(DispositionParam::as_filename) } /// Return the value of *filename\** if exists. pub fn get_filename_ext(&self) -> Option<&ExtendedValue> { self.parameters .iter() - .filter_map(|p| p.as_filename_ext()) - .next() + .find_map(DispositionParam::as_filename_ext) } /// Return the value of the parameter which the `name` matches. pub fn get_unknown(&self, name: impl AsRef) -> Option<&str> { let name = name.as_ref(); - self.parameters - .iter() - .filter_map(|p| p.as_unknown(name)) - .next() + self.parameters.iter().find_map(|p| p.as_unknown(name)) } /// Return the value of the extended parameter which the `name` matches. pub fn get_unknown_ext(&self, name: impl AsRef) -> Option<&ExtendedValue> { let name = name.as_ref(); - self.parameters - .iter() - .filter_map(|p| p.as_unknown_ext(name)) - .next() + self.parameters.iter().find_map(|p| p.as_unknown_ext(name)) } } diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 0eb4d0a83..a9128bc47 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -200,8 +200,7 @@ impl AcceptEncoding { let mut encodings = raw .replace(' ', "") .split(',') - .map(|l| AcceptEncoding::new(l)) - .flatten() + .filter_map(|l| AcceptEncoding::new(l)) .collect::>(); encodings.sort(); diff --git a/src/request.rs b/src/request.rs index 5c5c43d26..36d9aba98 100644 --- a/src/request.rs +++ b/src/request.rs @@ -111,11 +111,7 @@ impl HttpRequest { /// E.g., id=10 #[inline] pub fn query_string(&self) -> &str { - if let Some(query) = self.uri().query().as_ref() { - query - } else { - "" - } + self.uri().query().unwrap_or_default() } /// Get a reference to the Path parameters. diff --git a/src/resource.rs b/src/resource.rs index 9f5cf3cb2..4e609f31a 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -465,7 +465,7 @@ impl Service for ResourceService { actix_service::always_ready!(); fn call(&self, mut req: ServiceRequest) -> Self::Future { - for route in self.routes.iter() { + for route in &self.routes { if route.check(&mut req) { return route.call(req); } diff --git a/src/response/builder.rs b/src/response/builder.rs index 6e013cae2..56d30d9d0 100644 --- a/src/response/builder.rs +++ b/src/response/builder.rs @@ -406,7 +406,7 @@ impl HttpResponseBuilder { return None; } - self.res.as_mut().map(|res| res.head_mut()) + self.res.as_mut().map(Response::head_mut) } } diff --git a/src/server.rs b/src/server.rs index 89328215d..804b3e367 100644 --- a/src/server.rs +++ b/src/server.rs @@ -292,15 +292,15 @@ where let c = cfg.lock().unwrap(); let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); - let svc = HttpService::build() + let mut svc = HttpService::build() .keep_alive(c.keep_alive) .client_timeout(c.client_timeout) .local_addr(addr); - let svc = if let Some(handler) = on_connect_fn.clone() { - svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)) - } else { - svc + if let Some(handler) = on_connect_fn.clone() { + svc = svc.on_connect_ext(move |io: &_, ext: _| { + (handler)(io as &dyn Any, ext) + }) }; let fac = factory() @@ -461,17 +461,15 @@ where } } - if !success { - if let Some(e) = err.take() { - Err(e) - } else { - Err(io::Error::new( - io::ErrorKind::Other, - "Can not bind to address.", - )) - } - } else { + if success { Ok(sockets) + } else if let Some(e) = err.take() { + Err(e) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + "Can not bind to address.", + )) } } @@ -537,15 +535,14 @@ where ); fn_service(|io: UnixStream| async { Ok((io, Protocol::Http1, None)) }).and_then({ - let svc = HttpService::build() + let mut svc = HttpService::build() .keep_alive(c.keep_alive) .client_timeout(c.client_timeout); - let svc = if let Some(handler) = on_connect_fn.clone() { - svc.on_connect_ext(move |io: &_, ext: _| (&*handler)(io as &dyn Any, ext)) - } else { - svc - }; + if let Some(handler) = on_connect_fn.clone() { + svc = svc + .on_connect_ext(move |io: &_, ext: _| (&*handler)(io as &dyn Any, ext)); + } let fac = factory() .into_factory() diff --git a/src/service.rs b/src/service.rs index 592577467..c1bffac49 100644 --- a/src/service.rs +++ b/src/service.rs @@ -167,11 +167,7 @@ impl ServiceRequest { /// E.g., id=10 #[inline] pub fn query_string(&self) -> &str { - if let Some(query) = self.uri().query().as_ref() { - query - } else { - "" - } + self.uri().query().unwrap_or_default() } /// Peer socket address. diff --git a/src/types/form.rs b/src/types/form.rs index 4ce075d99..c81f73554 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -1,6 +1,7 @@ //! For URL encoded form helper documentation, see [`Form`]. use std::{ + borrow::Cow, fmt, future::Future, ops, @@ -384,7 +385,7 @@ where } else { let body = encoding .decode_without_bom_handling_and_without_replacement(&body) - .map(|s| s.into_owned()) + .map(Cow::into_owned) .ok_or(UrlencodedError::Encoding)?; serde_urlencoded::from_str::(&body).map_err(UrlencodedError::Parse) diff --git a/src/types/path.rs b/src/types/path.rs index 9dab79414..f2273a59b 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -103,8 +103,7 @@ where fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { let error_handler = req .app_data::() - .map(|c| c.ehandler.clone()) - .unwrap_or(None); + .and_then(|c| c.ehandler.clone()); ready( de::Deserialize::deserialize(PathDeserializer::new(req.match_info())) diff --git a/src/types/payload.rs b/src/types/payload.rs index 87378701b..188da6201 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -1,6 +1,7 @@ //! Basic binary and string payload extractors. use std::{ + borrow::Cow, future::Future, pin::Pin, str, @@ -190,7 +191,7 @@ fn bytes_to_string(body: Bytes, encoding: &'static Encoding) -> Result Self::Future { let error_handler = req .app_data::() - .map(|c| c.err_handler.clone()) - .unwrap_or(None); + .and_then(|c| c.err_handler.clone()); serde_urlencoded::from_str::(req.query_string()) .map(|val| ok(Query(val))) From 604be5495f920a9f11056fad989ff757ed364f2b Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sat, 26 Jun 2021 16:33:36 +0100 Subject: [PATCH 22/88] prepare beta.8 releases (#2292) --- .github/ISSUE_TEMPLATE/config.yml | 13 +++---------- CHANGES.md | 4 +++- Cargo.toml | 6 +++--- README.md | 4 ++-- actix-files/CHANGES.md | 10 ++++++---- actix-files/Cargo.toml | 12 +++++------- actix-files/README.md | 7 +++---- actix-http-test/Cargo.toml | 6 +++--- actix-http-test/README.md | 6 ++++-- actix-http/CHANGES.md | 3 +++ actix-http/Cargo.toml | 6 ++---- actix-http/README.md | 7 +++---- actix-multipart/Cargo.toml | 4 ++-- actix-multipart/README.md | 2 +- actix-test/Cargo.toml | 6 +++--- actix-web-actors/CHANGES.md | 3 +++ actix-web-actors/Cargo.toml | 12 +++++------- actix-web-actors/README.md | 7 +++---- actix-web-codegen/Cargo.toml | 2 +- actix-web-codegen/README.md | 3 +-- awc/CHANGES.md | 3 +++ awc/Cargo.toml | 8 ++++---- awc/README.md | 7 +++---- src/lib.rs | 1 - 24 files changed, 69 insertions(+), 73 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index d8c6d66ca..bfa124ffd 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,15 +1,8 @@ blank_issues_enabled: true contact_links: - - name: GitHub Discussions - url: https://github.com/actix/actix-web/discussions - about: Actix Web Q&A - - name: Gitter chat (actix-web) - url: https://gitter.im/actix/actix-web - about: Actix Web Q&A - - name: Gitter chat (actix) - url: https://gitter.im/actix/actix - about: Actix (actor framework) Q&A - name: Actix Discord url: https://discord.gg/NWpN5mmg3x about: Actix developer discussion and community chat - + - name: GitHub Discussions + url: https://github.com/actix/actix-web/discussions + about: Actix Web Q&A diff --git a/CHANGES.md b/CHANGES.md index f6a0b28c8..d0f2188a7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 4.0.0-beta.8 - 2021-06-26 ### Added * Add `ServiceRequest::parts_mut`. [#2177] * Add extractors for `Uri` and `Method`. [#2263] @@ -12,7 +15,6 @@ * Deprecate `App::data` and `App::data_factory`. [#2271] * Smarter extraction of `ConnectionInfo` parts. [#2282] - ### Fixed * Scope and Resource middleware can access data items set on their own layer. [#2288] diff --git a/Cargo.toml b/Cargo.toml index 293e6bccd..7556bd8d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "4.0.0-beta.7" +version = "4.0.0-beta.8" authors = ["Nikolay Kim "] description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" keywords = ["actix", "http", "web", "framework", "async"] @@ -75,7 +75,7 @@ actix-utils = "3.0.0" actix-tls = { version = "3.0.0-beta.5", default-features = false, optional = true } actix-web-codegen = "0.5.0-beta.2" -actix-http = "3.0.0-beta.7" +actix-http = "3.0.0-beta.8" ahash = "0.7" bytes = "1" @@ -104,7 +104,7 @@ url = "2.1" [dev-dependencies] actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] } -awc = { version = "3.0.0-beta.6", features = ["openssl"] } +awc = { version = "3.0.0-beta.7", features = ["openssl"] } brotli2 = "0.3.2" criterion = { version = "0.3", features = ["html_reports"] } diff --git a/README.md b/README.md index d9048a06b..309a18466 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@

[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) -[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.7)](https://docs.rs/actix-web/4.0.0-beta.7) +[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.8)](https://docs.rs/actix-web/4.0.0-beta.8) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) -[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.7/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.7) +[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.8/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.8)
[![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 54b0344a4..db047c44c 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,9 +1,14 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.6.0-beta.6 - 2021-06-26 * Added `Files::path_filter()`. [#2274] +* `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228] [#2274]: https://github.com/actix/actix-web/pull/2274 +[#2228]: https://github.com/actix/actix-web/pull/2228 ## 0.6.0-beta.5 - 2021-06-17 @@ -11,22 +16,19 @@ * For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156] * `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225] * `application/{javascript, json, wasm}` mime type now have `inline` disposition by default. [#2257] -* `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228] [#2135]: https://github.com/actix/actix-web/pull/2135 [#2156]: https://github.com/actix/actix-web/pull/2156 [#2225]: https://github.com/actix/actix-web/pull/2225 [#2257]: https://github.com/actix/actix-web/pull/2257 -[#2228]: https://github.com/actix/actix-web/pull/2228 ## 0.6.0-beta.4 - 2021-04-02 -* No notable changes. - * Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046] [#2046]: https://github.com/actix/actix-web/pull/2046 + ## 0.6.0-beta.3 - 2021-03-09 * No notable changes. diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 65dce628b..ef288215b 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,13 +1,11 @@ [package] name = "actix-files" -version = "0.6.0-beta.5" +version = "0.6.0-beta.6" authors = ["Nikolay Kim "] description = "Static file serving for Actix Web" -readme = "README.md" keywords = ["actix", "http", "async", "futures"] homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web.git" -documentation = "https://docs.rs/actix-files/" +repository = "https://github.com/actix/actix-web" categories = ["asynchronous", "web-programming::http-server"] license = "MIT OR Apache-2.0" edition = "2018" @@ -17,8 +15,8 @@ name = "actix_files" path = "src/lib.rs" [dependencies] -actix-web = { version = "4.0.0-beta.7", default-features = false } -actix-http = "3.0.0-beta.7" +actix-web = { version = "4.0.0-beta.8", default-features = false } +actix-http = "3.0.0-beta.8" actix-service = "2.0.0" actix-utils = "3.0.0" @@ -35,5 +33,5 @@ percent-encoding = "2.1" [dev-dependencies] actix-rt = "2.2" -actix-web = "4.0.0-beta.7" +actix-web = "4.0.0-beta.8" actix-test = "0.1.0-beta.3" diff --git a/actix-files/README.md b/actix-files/README.md index 524f5c38e..13c301c56 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -3,17 +3,16 @@ > Static file serving for Actix Web [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) -[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.5)](https://docs.rs/actix-files/0.6.0-beta.5) +[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.6)](https://docs.rs/actix-files/0.6.0-beta.6) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) ![License](https://img.shields.io/crates/l/actix-files.svg)
-[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.5/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.5) +[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.6/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.6) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) -[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/actix-files/) - [Example Project](https://github.com/actix/examples/tree/master/basics/static_index) -- [Chat on Gitter](https://gitter.im/actix/actix-web) - Minimum supported Rust version: 1.46 or later diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 5d797aaa9..c04b5da49 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -35,7 +35,7 @@ actix-tls = "3.0.0-beta.5" actix-utils = "3.0.0" actix-rt = "2.2" actix-server = "2.0.0-beta.3" -awc = { version = "3.0.0-beta.6", default-features = false } +awc = { version = "3.0.0-beta.7", default-features = false } base64 = "0.13" bytes = "1" @@ -51,5 +51,5 @@ time = { version = "0.2.23", default-features = false, features = ["std"] } tls-openssl = { version = "0.10.9", package = "openssl", optional = true } [dev-dependencies] -actix-web = { version = "4.0.0-beta.7", default-features = false, features = ["cookies"] } -actix-http = "3.0.0-beta.7" +actix-web = { version = "4.0.0-beta.8", default-features = false, features = ["cookies"] } +actix-http = "3.0.0-beta.8" diff --git a/actix-http-test/README.md b/actix-http-test/README.md index b8cf450d4..74260a352 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -4,12 +4,14 @@ [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) [![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.4)](https://docs.rs/actix-http-test/3.0.0-beta.4) +[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test) +
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.4/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.4) -[![Join the chat at https://gitter.im/actix/actix-web](https://badges.gitter.im/actix/actix-web.svg)](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/actix-http-test) -- [Chat on Gitter](https://gitter.im/actix/actix-web) - Minimum Supported Rust Version (MSRV): 1.46.0 diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 435607463..8ead43718 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.8 - 2021-06-26 ### Changed * Change compression algorithm features flags. [#2250] diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 35ea89862..a12fed4b9 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,13 +1,11 @@ [package] name = "actix-http" -version = "3.0.0-beta.7" +version = "3.0.0-beta.8" authors = ["Nikolay Kim "] description = "HTTP primitives for the Actix ecosystem" -readme = "README.md" keywords = ["actix", "http", "framework", "async", "futures"] homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web.git" -documentation = "https://docs.rs/actix-http/" +repository = "https://github.com/actix/actix-web" categories = ["network-programming", "asynchronous", "web-programming::http-server", "web-programming::websocket"] diff --git a/actix-http/README.md b/actix-http/README.md index 5271d8738..de1ef0a9b 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -3,18 +3,17 @@ > HTTP primitives for the Actix ecosystem. [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) -[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.7)](https://docs.rs/actix-http/3.0.0-beta.7) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.8)](https://docs.rs/actix-http/3.0.0-beta.8) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
-[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.7/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.7) +[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.8/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.8) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) -[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/actix-http) -- [Chat on Gitter](https://gitter.im/actix/actix-web) - Minimum Supported Rust Version (MSRV): 1.46.0 ## Example diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 41b0fbae7..5103407ca 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -16,7 +16,7 @@ name = "actix_multipart" path = "src/lib.rs" [dependencies] -actix-web = { version = "4.0.0-beta.7", default-features = false } +actix-web = { version = "4.0.0-beta.8", default-features = false } actix-utils = "3.0.0" bytes = "1" @@ -31,6 +31,6 @@ twoway = "0.2" [dev-dependencies] actix-rt = "2.2" -actix-http = "3.0.0-beta.7" +actix-http = "3.0.0-beta.8" tokio = { version = "1", features = ["sync"] } tokio-stream = "0.1" diff --git a/actix-multipart/README.md b/actix-multipart/README.md index f6d008fc3..78855b815 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -9,9 +9,9 @@
[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.5/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.5) [![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/actix-multipart) -- [Chat on Gitter](https://gitter.im/actix/actix-web) - Minimum Supported Rust Version (MSRV): 1.46.0 diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index ca814e0e5..b732cf744 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -20,13 +20,13 @@ openssl = ["tls-openssl", "actix-http/openssl"] [dependencies] actix-codec = "0.4.0" -actix-http = "3.0.0-beta.7" +actix-http = "3.0.0-beta.8" actix-http-test = { version = "3.0.0-beta.4", features = [] } actix-service = "2.0.0" actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.7", default-features = false, features = ["cookies"] } +actix-web = { version = "4.0.0-beta.8", default-features = false, features = ["cookies"] } actix-rt = "2.1" -awc = { version = "3.0.0-beta.6", default-features = false, features = ["cookies"] } +awc = { version = "3.0.0-beta.7", default-features = false, features = ["cookies"] } futures-core = { version = "0.3.7", default-features = false, features = ["std"] } futures-util = { version = "0.3.7", default-features = false, features = [] } diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index decbe2219..bf642ef95 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 4.0.0-beta.6 - 2021-06-26 * Update `actix` to `0.12`. [#2277] [#2277]: https://github.com/actix/actix-web/pull/2277 diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 669cd4001..fcb5195b8 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -1,13 +1,11 @@ [package] name = "actix-web-actors" -version = "4.0.0-beta.5" +version = "4.0.0-beta.6" authors = ["Nikolay Kim "] description = "Actix actors support for Actix Web" -readme = "README.md" keywords = ["actix", "http", "web", "framework", "async"] homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web.git" -documentation = "https://docs.rs/actix-web-actors/" +repository = "https://github.com/actix/actix-web" license = "MIT OR Apache-2.0" edition = "2018" @@ -18,8 +16,8 @@ path = "src/lib.rs" [dependencies] actix = { version = "0.12.0", default-features = false } actix-codec = "0.4.0" -actix-http = "3.0.0-beta.7" -actix-web = { version = "4.0.0-beta.7", default-features = false } +actix-http = "3.0.0-beta.8" +actix-web = { version = "4.0.0-beta.8", default-features = false } bytes = "1" bytestring = "1" @@ -31,6 +29,6 @@ tokio = { version = "1", features = ["sync"] } actix-rt = "2.2" actix-test = "0.1.0-beta.3" -awc = { version = "3.0.0-beta.6", default-features = false } +awc = { version = "3.0.0-beta.7", default-features = false } env_logger = "0.8" futures-util = { version = "0.3.7", default-features = false } diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md index 0d926f5ee..5f8f78bde 100644 --- a/actix-web-actors/README.md +++ b/actix-web-actors/README.md @@ -3,16 +3,15 @@ > Actix actors support for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) -[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.5)](https://docs.rs/actix-web-actors/4.0.0-beta.5) +[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.6)](https://docs.rs/actix-web-actors/4.0.0-beta.6) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
-[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.5/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.5) +[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.6/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.6) [![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors) -[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/actix-web-actors) -- [Chat on Gitter](https://gitter.im/actix/actix-web) - Minimum supported Rust version: 1.46 or later diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 327f16bc5..4d0fd5e26 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -22,7 +22,7 @@ proc-macro2 = "1" actix-rt = "2.2" actix-test = "0.1.0-beta.3" actix-utils = "3.0.0" -actix-web = "4.0.0-beta.7" +actix-web = "4.0.0-beta.8" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } trybuild = "1" diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index ef3aa72df..96e4cb51f 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -9,12 +9,11 @@
[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.3/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.3) [![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen) -[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/actix-web-codegen) -- [Chat on Gitter](https://gitter.im/actix/actix-web) - Minimum supported Rust version: 1.46 or later. ## Compile Testing diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 2e56eb958..16132be1c 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.7 - 2021-06-26 ### Changed * Change compression algorithm features flags. [#2250] diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 26c625a05..016d3b48b 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "3.0.0-beta.6" +version = "3.0.0-beta.7" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", @@ -55,7 +55,7 @@ __compress = [] [dependencies] actix-codec = "0.4.0" actix-service = "2.0.0" -actix-http = "3.0.0-beta.7" +actix-http = "3.0.0-beta.8" actix-rt = { version = "2.1", default-features = false } base64 = "0.13" @@ -77,8 +77,8 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true } tls-rustls = { version = "0.19.0", package = "rustls", optional = true, features = ["dangerous_configuration"] } [dev-dependencies] -actix-web = { version = "4.0.0-beta.7", features = ["openssl"] } -actix-http = { version = "3.0.0-beta.7", features = ["openssl"] } +actix-web = { version = "4.0.0-beta.8", features = ["openssl"] } +actix-http = { version = "3.0.0-beta.8", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.4", features = ["openssl"] } actix-utils = "3.0.0" actix-server = "2.0.0-beta.3" diff --git a/awc/README.md b/awc/README.md index 5076c59a4..dd08c6e10 100644 --- a/awc/README.md +++ b/awc/README.md @@ -3,16 +3,15 @@ > Async HTTP and WebSocket client library. [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) -[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.6)](https://docs.rs/awc/3.0.0-beta.6) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.7)](https://docs.rs/awc/3.0.0-beta.7) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.6/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.6) -[![Join the chat at https://gitter.im/actix/actix-web](https://badges.gitter.im/actix/actix-web.svg)](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.7/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.7) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/awc) - [Example Project](https://github.com/actix/examples/tree/HEAD/security/awc_https) -- [Chat on Gitter](https://gitter.im/actix/actix-web) - Minimum Supported Rust Version (MSRV): 1.46.0 ## Example diff --git a/src/lib.rs b/src/lib.rs index 920abccb6..6905da79f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,6 @@ //! * [Website & User Guide](https://actix.rs/) //! * [Examples Repository](https://github.com/actix/examples) //! * [Community Chat on Discord](https://discord.gg/NWpN5mmg3x) -//! * [Community Chat on Gitter](https://gitter.im/actix/actix-web) //! //! To get started navigating the API docs, you may consider looking at the following pages first: //! From 2504c2ecb0906d261688cd61d93444d4f0537cde Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Sun, 27 Jun 2021 02:44:56 -0400 Subject: [PATCH 23/88] Move dev module to separate file, update description (#2293) --- src/dev.rs | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 107 +---------------------------------------------------- 2 files changed, 103 insertions(+), 106 deletions(-) create mode 100644 src/dev.rs diff --git a/src/dev.rs b/src/dev.rs new file mode 100644 index 000000000..a656604e3 --- /dev/null +++ b/src/dev.rs @@ -0,0 +1,102 @@ +//! Lower level `actix-web` types. +//! +//! Most users will not have to interact with the types in this module, +//! but it is useful as a glob import for those writing middleware, developing libraries, +//! or interacting with the service API directly: +//! +//! ``` +//! # #![allow(unused_imports)] +//! use actix_web::dev::*; +//! ``` + +pub use crate::config::{AppConfig, AppService}; +#[doc(hidden)] +pub use crate::handler::Handler; +pub use crate::info::{ConnectionInfo, PeerAddr}; +pub use crate::rmap::ResourceMap; +pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse, WebService}; + +pub use crate::types::form::UrlEncoded; +pub use crate::types::json::JsonBody; +pub use crate::types::readlines::Readlines; + +pub use actix_http::body::{AnyBody, Body, BodySize, MessageBody, ResponseBody, SizedStream}; + +#[cfg(feature = "__compress")] +pub use actix_http::encoding::Decoder as Decompress; +pub use actix_http::ResponseBuilder as BaseHttpResponseBuilder; +pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, ResponseHead}; +pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; +pub use actix_server::Server; +pub use actix_service::{ + always_ready, fn_factory, fn_service, forward_ready, Service, Transform, +}; + +pub(crate) fn insert_slash(mut patterns: Vec) -> Vec { + for path in &mut patterns { + if !path.is_empty() && !path.starts_with('/') { + path.insert(0, '/'); + }; + } + patterns +} + +use crate::http::header::ContentEncoding; +use actix_http::{Response, ResponseBuilder}; + +struct Enc(ContentEncoding); + +/// Helper trait that allows to set specific encoding for response. +pub trait BodyEncoding { + /// Get content encoding + fn get_encoding(&self) -> Option; + + /// Set content encoding + /// + /// Must be used with [`crate::middleware::Compress`] to take effect. + fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self; +} + +impl BodyEncoding for ResponseBuilder { + fn get_encoding(&self) -> Option { + self.extensions().get::().map(|enc| enc.0) + } + + fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { + self.extensions_mut().insert(Enc(encoding)); + self + } +} + +impl BodyEncoding for Response { + fn get_encoding(&self) -> Option { + self.extensions().get::().map(|enc| enc.0) + } + + fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { + self.extensions_mut().insert(Enc(encoding)); + self + } +} + +impl BodyEncoding for crate::HttpResponseBuilder { + fn get_encoding(&self) -> Option { + self.extensions().get::().map(|enc| enc.0) + } + + fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { + self.extensions_mut().insert(Enc(encoding)); + self + } +} + +impl BodyEncoding for crate::HttpResponse { + fn get_encoding(&self) -> Option { + self.extensions().get::().map(|enc| enc.0) + } + + fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { + self.extensions_mut().insert(Enc(encoding)); + self + } +} diff --git a/src/lib.rs b/src/lib.rs index 6905da79f..714c759cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -73,6 +73,7 @@ mod app; mod app_service; mod config; mod data; +pub mod dev; pub mod error; mod extract; pub mod guard; @@ -115,109 +116,3 @@ pub use crate::scope::Scope; pub use crate::server::HttpServer; // TODO: is exposing the error directly really needed pub use crate::types::{Either, EitherExtractError}; - -pub mod dev { - //! The `actix-web` prelude for library developers - //! - //! The purpose of this module is to alleviate imports of many common actix - //! traits by adding a glob import to the top of actix heavy modules: - //! - //! ``` - //! # #![allow(unused_imports)] - //! use actix_web::dev::*; - //! ``` - - pub use crate::config::{AppConfig, AppService}; - #[doc(hidden)] - pub use crate::handler::Handler; - pub use crate::info::{ConnectionInfo, PeerAddr}; - pub use crate::rmap::ResourceMap; - pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse, WebService}; - - pub use crate::types::form::UrlEncoded; - pub use crate::types::json::JsonBody; - pub use crate::types::readlines::Readlines; - - pub use actix_http::body::{ - AnyBody, Body, BodySize, MessageBody, ResponseBody, SizedStream, - }; - - #[cfg(feature = "__compress")] - pub use actix_http::encoding::Decoder as Decompress; - pub use actix_http::ResponseBuilder as BaseHttpResponseBuilder; - pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, ResponseHead}; - pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; - pub use actix_server::Server; - pub use actix_service::{ - always_ready, fn_factory, fn_service, forward_ready, Service, Transform, - }; - - pub(crate) fn insert_slash(mut patterns: Vec) -> Vec { - for path in &mut patterns { - if !path.is_empty() && !path.starts_with('/') { - path.insert(0, '/'); - }; - } - patterns - } - - use crate::http::header::ContentEncoding; - use actix_http::{Response, ResponseBuilder}; - - struct Enc(ContentEncoding); - - /// Helper trait that allows to set specific encoding for response. - pub trait BodyEncoding { - /// Get content encoding - fn get_encoding(&self) -> Option; - - /// Set content encoding - /// - /// Must be used with [`crate::middleware::Compress`] to take effect. - fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self; - } - - impl BodyEncoding for ResponseBuilder { - fn get_encoding(&self) -> Option { - self.extensions().get::().map(|enc| enc.0) - } - - fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { - self.extensions_mut().insert(Enc(encoding)); - self - } - } - - impl BodyEncoding for Response { - fn get_encoding(&self) -> Option { - self.extensions().get::().map(|enc| enc.0) - } - - fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { - self.extensions_mut().insert(Enc(encoding)); - self - } - } - - impl BodyEncoding for crate::HttpResponseBuilder { - fn get_encoding(&self) -> Option { - self.extensions().get::().map(|enc| enc.0) - } - - fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { - self.extensions_mut().insert(Enc(encoding)); - self - } - } - - impl BodyEncoding for crate::HttpResponse { - fn get_encoding(&self) -> Option { - self.extensions().get::().map(|enc| enc.0) - } - - fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { - self.extensions_mut().insert(Enc(encoding)); - self - } - } -} From d8deed0475a5801d531e72b7e2619e18222f704f Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Sat, 10 Jul 2021 01:57:21 +0300 Subject: [PATCH 24/88] fix tests with tokio 1.8.1 (#2317) --- actix-http/src/config.rs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/actix-http/src/config.rs b/actix-http/src/config.rs index 0e01e8748..6661e18f3 100644 --- a/actix-http/src/config.rs +++ b/actix-http/src/config.rs @@ -326,7 +326,7 @@ mod notify_on_drop { mod tests { use super::*; - use actix_rt::task::yield_now; + use actix_rt::{task::yield_now, time::sleep}; #[actix_rt::test] async fn test_date_service_update() { @@ -350,7 +350,14 @@ mod tests { assert_ne!(buf1, buf2); drop(settings); - assert!(notify_on_drop::is_dropped()); + + // Ensure the task will drop eventually + let mut times = 0; + while !notify_on_drop::is_dropped() { + sleep(Duration::from_millis(100)).await; + times += 1; + assert!(times < 10, "Timeout waiting for task drop"); + } } #[actix_rt::test] @@ -372,7 +379,14 @@ mod tests { assert!(!notify_on_drop::is_dropped()); drop(service); - assert!(notify_on_drop::is_dropped()); + + // Ensure the task will drop eventually + let mut times = 0; + while !notify_on_drop::is_dropped() { + sleep(Duration::from_millis(100)).await; + times += 1; + assert!(times < 10, "Timeout waiting for task drop"); + } } #[test] From 7ae132cb688c213b22878b8425e73b9431300629 Mon Sep 17 00:00:00 2001 From: CGMossa Date: Mon, 12 Jul 2021 03:02:19 +0200 Subject: [PATCH 25/88] Update MIGRATION.md (#2315) Minor edit --- MIGRATION.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 9c29b8db9..785974366 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -5,8 +5,8 @@ routes defined with trailing slashes will become inaccessible when using `NormalizePath::default()`. - Before: `#[get("/test/")` - After: `#[get("/test")` + Before: `#[get("/test/")]` + After: `#[get("/test")]` Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`. From 5a14ffeef28859d387810151d5a685b585f36be2 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 12 Jul 2021 16:55:24 +0100 Subject: [PATCH 26/88] clippy fixes (#2296) --- .cargo/config.toml | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 2 +- actix-http/src/config.rs | 2 ++ actix-http/src/error.rs | 2 ++ src/info.rs | 10 +++++----- src/server.rs | 16 +++++++++++----- tests/test_error_propagation.rs | 2 +- 7 files changed, 23 insertions(+), 13 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 72f445d8a..db47ca46d 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,6 +1,6 @@ [alias] chk = "check --workspace --all-features --tests --examples --bins" -lint = "clippy --workspace --tests --examples" +lint = "clippy --workspace --all-features --tests --examples --bins" ci-min = "hack check --workspace --no-default-features" ci-min-test = "hack check --workspace --no-default-features --tests --examples" ci-default = "check --workspace --bins --tests --examples" diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 42deadf5a..d617cf708 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -8,7 +8,7 @@ PR_TYPE ## PR Checklist - - [ ] Tests for the changes have been added / updated. diff --git a/actix-http/src/config.rs b/actix-http/src/config.rs index 6661e18f3..97750ff76 100644 --- a/actix-http/src/config.rs +++ b/actix-http/src/config.rs @@ -104,6 +104,8 @@ impl ServiceConfig { } /// Returns the local address that this server is bound to. + /// + /// Returns `None` for connections via UDS (Unix Domain Socket). #[inline] pub fn local_addr(&self) -> Option { self.0.local_addr diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index 6c3d692c3..54666e072 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -55,6 +55,8 @@ impl Error { Self::new(Kind::Io) } + // used in encoder behind feature flag so ignore unused warning + #[allow(unused)] pub(crate) fn new_encoder() -> Self { Self::new(Kind::Encoder) } diff --git a/src/info.rs b/src/info.rs index 1f8263add..de8ad67ee 100644 --- a/src/info.rs +++ b/src/info.rs @@ -65,10 +65,10 @@ fn first_header_value<'a>(req: &'a RequestHead, name: &'_ HeaderName) -> Option< /// [rfc7239-63]: https://datatracker.ietf.org/doc/html/rfc7239#section-6.3 #[derive(Debug, Clone, Default)] pub struct ConnectionInfo { - scheme: String, host: String, - realip_remote_addr: Option, + scheme: String, remote_addr: Option, + realip_remote_addr: Option, } impl ConnectionInfo { @@ -135,7 +135,7 @@ impl ConnectionInfo { .or_else(|| first_header_value(req, &*X_FORWARDED_HOST)) .or_else(|| req.headers.get(&header::HOST)?.to_str().ok()) .or_else(|| req.uri.authority().map(Authority::as_str)) - .unwrap_or(cfg.host()) + .unwrap_or_else(|| cfg.host()) .to_owned(); let realip_remote_addr = realip_remote_addr @@ -145,9 +145,9 @@ impl ConnectionInfo { let remote_addr = req.peer_addr.map(|addr| addr.to_string()); ConnectionInfo { - remote_addr, - scheme, host, + scheme, + remote_addr, realip_remote_addr, } } diff --git a/src/server.rs b/src/server.rs index 804b3e367..f15183f85 100644 --- a/src/server.rs +++ b/src/server.rs @@ -295,6 +295,7 @@ where let mut svc = HttpService::build() .keep_alive(c.keep_alive) .client_timeout(c.client_timeout) + .client_disconnect(c.client_shutdown) .local_addr(addr); if let Some(handler) = on_connect_fn.clone() { @@ -352,7 +353,8 @@ where let svc = HttpService::build() .keep_alive(c.keep_alive) .client_timeout(c.client_timeout) - .client_disconnect(c.client_shutdown); + .client_disconnect(c.client_shutdown) + .local_addr(addr); let svc = if let Some(handler) = on_connect_fn.clone() { svc.on_connect_ext(move |io: &_, ext: _| { @@ -523,10 +525,11 @@ where addr: socket_addr, }); - let addr = format!("actix-web-service-{:?}", lst.local_addr()?); + let addr = lst.local_addr()?; + let name = format!("actix-web-service-{:?}", addr); let on_connect_fn = self.on_connect_fn.clone(); - self.builder = self.builder.listen_uds(addr, lst, move || { + self.builder = self.builder.listen_uds(name, lst, move || { let c = cfg.lock().unwrap(); let config = AppConfig::new( false, @@ -537,7 +540,8 @@ where fn_service(|io: UnixStream| async { Ok((io, Protocol::Http1, None)) }).and_then({ let mut svc = HttpService::build() .keep_alive(c.keep_alive) - .client_timeout(c.client_timeout); + .client_timeout(c.client_timeout) + .client_disconnect(c.client_shutdown); if let Some(handler) = on_connect_fn.clone() { svc = svc @@ -554,8 +558,8 @@ where Ok(self) } - #[cfg(unix)] /// Start listening for incoming unix domain connections. + #[cfg(unix)] pub fn bind_uds(mut self, addr: A) -> io::Result where A: AsRef, @@ -568,6 +572,7 @@ where let factory = self.factory.clone(); let socket_addr = net::SocketAddr::new(net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), 8080); + self.sockets.push(Socket { scheme: "http", addr: socket_addr, @@ -592,6 +597,7 @@ where HttpService::build() .keep_alive(c.keep_alive) .client_timeout(c.client_timeout) + .client_disconnect(c.client_shutdown) .finish(map_config(fac, move |_| config.clone())), ) }, diff --git a/tests/test_error_propagation.rs b/tests/test_error_propagation.rs index 3e7320920..958276b62 100644 --- a/tests/test_error_propagation.rs +++ b/tests/test_error_propagation.rs @@ -23,7 +23,7 @@ impl std::fmt::Display for MyError { #[get("/test")] async fn test() -> Result { - return Err(MyError.into()); + Err(MyError.into()) } #[derive(Clone)] From 293c52c3ef42663d90d4b09acd22869ee6919788 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 12 Jul 2021 16:55:41 +0100 Subject: [PATCH 27/88] re-export ServiceFactory (#2325) --- CHANGES.md | 4 ++++ actix-http/src/request.rs | 2 +- src/dev.rs | 25 +++++++++---------------- src/request.rs | 4 ++-- src/resource.rs | 4 ++-- src/response/response.rs | 16 ++++++++-------- src/service.rs | 9 +++++---- 7 files changed, 31 insertions(+), 33 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d0f2188a7..88295ec12 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,10 @@ # Changes ## Unreleased - 2021-xx-xx +### Added +* Re-export actix-service `ServiceFactory` in `dev` module. [#2325] + +[#2325]: https://github.com/actix/actix-web/pull/2325 ## 4.0.0-beta.8 - 2021-06-26 diff --git a/actix-http/src/request.rs b/actix-http/src/request.rs index 09c6dd296..401e9745c 100644 --- a/actix-http/src/request.rs +++ b/actix-http/src/request.rs @@ -15,7 +15,7 @@ use crate::{ HttpMessage, }; -/// Request +/// An HTTP request. pub struct Request

{ pub(crate) payload: Payload

, pub(crate) head: Message, diff --git a/src/dev.rs b/src/dev.rs index a656604e3..b8d95efbb 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -1,13 +1,7 @@ -//! Lower level `actix-web` types. +//! Lower-level types and re-exports. //! -//! Most users will not have to interact with the types in this module, -//! but it is useful as a glob import for those writing middleware, developing libraries, -//! or interacting with the service API directly: -//! -//! ``` -//! # #![allow(unused_imports)] -//! use actix_web::dev::*; -//! ``` +//! Most users will not have to interact with the types in this module, but it is useful for those +//! writing extractors, middleware and libraries, or interacting with the service API directly. pub use crate::config::{AppConfig, AppService}; #[doc(hidden)] @@ -24,26 +18,25 @@ pub use actix_http::body::{AnyBody, Body, BodySize, MessageBody, ResponseBody, S #[cfg(feature = "__compress")] pub use actix_http::encoding::Decoder as Decompress; -pub use actix_http::ResponseBuilder as BaseHttpResponseBuilder; pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, ResponseHead}; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; pub use actix_server::Server; pub use actix_service::{ - always_ready, fn_factory, fn_service, forward_ready, Service, Transform, + always_ready, fn_factory, fn_service, forward_ready, Service, ServiceFactory, Transform, }; -pub(crate) fn insert_slash(mut patterns: Vec) -> Vec { +use crate::http::header::ContentEncoding; +use actix_http::{Response, ResponseBuilder}; + +pub(crate) fn insert_leading_slash(mut patterns: Vec) -> Vec { for path in &mut patterns { if !path.is_empty() && !path.starts_with('/') { path.insert(0, '/'); }; } + patterns } - -use crate::http::header::ContentEncoding; -use actix_http::{Response, ResponseBuilder}; - struct Enc(ContentEncoding); /// Helper trait that allows to set specific encoding for response. diff --git a/src/request.rs b/src/request.rs index 36d9aba98..4b950e758 100644 --- a/src/request.rs +++ b/src/request.rs @@ -23,10 +23,10 @@ use crate::{ #[cfg(feature = "cookies")] struct Cookies(Vec>); +/// An incoming request. #[derive(Clone)] -/// An HTTP Request pub struct HttpRequest { - /// # Panics + /// # Invariant /// `Rc` is used exclusively and NO `Weak` /// is allowed anywhere in the code. Weak pointer is purposely ignored when /// doing `Rc`'s ref counter check. Expect panics if this invariant is violated. diff --git a/src/resource.rs b/src/resource.rs index 4e609f31a..20d1ee17e 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -15,7 +15,7 @@ use futures_util::future::join_all; use crate::{ data::Data, - dev::{insert_slash, AppService, HttpServiceFactory, ResourceDef}, + dev::{insert_leading_slash, AppService, HttpServiceFactory, ResourceDef}, guard::Guard, handler::Handler, responder::Responder, @@ -391,7 +391,7 @@ where }; let mut rdef = if config.is_root() || !self.rdef.is_empty() { - ResourceDef::new(insert_slash(self.rdef.clone())) + ResourceDef::new(insert_leading_slash(self.rdef.clone())) } else { ResourceDef::new(self.rdef.clone()) }; diff --git a/src/response/response.rs b/src/response/response.rs index 9a3bb2874..09515c839 100644 --- a/src/response/response.rs +++ b/src/response/response.rs @@ -24,20 +24,14 @@ use { use crate::{error::Error, HttpResponseBuilder}; -/// An HTTP Response +/// An outgoing response. pub struct HttpResponse { res: Response, pub(crate) error: Option, } impl HttpResponse { - /// Create HTTP response builder with specific status. - #[inline] - pub fn build(status: StatusCode) -> HttpResponseBuilder { - HttpResponseBuilder::new(status) - } - - /// Create a response. + /// Constructs a response. #[inline] pub fn new(status: StatusCode) -> Self { Self { @@ -46,6 +40,12 @@ impl HttpResponse { } } + /// Constructs a response builder with specific HTTP status. + #[inline] + pub fn build(status: StatusCode) -> HttpResponseBuilder { + HttpResponseBuilder::new(status) + } + /// Create an error response. #[inline] pub fn from_error(error: impl Into) -> Self { diff --git a/src/service.rs b/src/service.rs index c1bffac49..47e7e4acc 100644 --- a/src/service.rs +++ b/src/service.rs @@ -14,7 +14,7 @@ use cookie::{Cookie, ParseError as CookieParseError}; use crate::{ config::{AppConfig, AppService}, - dev::insert_slash, + dev::insert_leading_slash, guard::Guard, info::ConnectionInfo, rmap::ResourceMap, @@ -59,9 +59,9 @@ where } } -/// An service http request +/// A service level request wrapper. /// -/// ServiceRequest allows mutable access to request's internal structures +/// Allows mutable access to request's internal structures. pub struct ServiceRequest { req: HttpRequest, payload: Payload, @@ -325,6 +325,7 @@ impl fmt::Debug for ServiceRequest { } } +/// A service level response wrapper. pub struct ServiceResponse { request: HttpRequest, response: HttpResponse, @@ -550,7 +551,7 @@ where }; let mut rdef = if config.is_root() || !self.rdef.is_empty() { - ResourceDef::new(insert_slash(self.rdef)) + ResourceDef::new(insert_leading_slash(self.rdef)) } else { ResourceDef::new(self.rdef) }; From f6e69919ede4872c1d987b4b932c44580190971c Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 6 Aug 2021 22:42:31 +0100 Subject: [PATCH 28/88] update to router 0.5.0 beta (#2339) --- Cargo.toml | 4 +- actix-router/CHANGES.md | 120 ++ actix-router/Cargo.toml | 38 + actix-router/LICENSE-APACHE | 1 + actix-router/LICENSE-MIT | 1 + actix-router/benches/router.rs | 194 +++ actix-router/examples/flamegraph.rs | 169 +++ actix-router/src/de.rs | 723 +++++++++++ actix-router/src/lib.rs | 149 +++ actix-router/src/path.rs | 220 ++++ actix-router/src/resource.rs | 1803 +++++++++++++++++++++++++++ actix-router/src/router.rs | 281 +++++ actix-router/src/url.rs | 288 +++++ src/app.rs | 2 +- src/app_service.rs | 2 +- src/config.rs | 2 +- src/dev.rs | 21 +- src/request.rs | 8 +- src/resource.rs | 12 +- src/rmap.rs | 44 +- src/scope.rs | 2 +- src/service.rs | 20 +- src/types/path.rs | 8 +- src/web.rs | 8 +- 24 files changed, 4063 insertions(+), 57 deletions(-) create mode 100644 actix-router/CHANGES.md create mode 100644 actix-router/Cargo.toml create mode 120000 actix-router/LICENSE-APACHE create mode 120000 actix-router/LICENSE-MIT create mode 100644 actix-router/benches/router.rs create mode 100644 actix-router/examples/flamegraph.rs create mode 100644 actix-router/src/de.rs create mode 100644 actix-router/src/lib.rs create mode 100644 actix-router/src/path.rs create mode 100644 actix-router/src/resource.rs create mode 100644 actix-router/src/router.rs create mode 100644 actix-router/src/url.rs diff --git a/Cargo.toml b/Cargo.toml index 7556bd8d7..ff3321f47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ members = [ "actix-web-codegen", "actix-http-test", "actix-test", + "actix-router", ] # enable when MSRV is 1.51+ # resolver = "2" @@ -67,7 +68,7 @@ __compress = [] [dependencies] actix-codec = "0.4.0" actix-macros = "0.2.1" -actix-router = "0.2.7" +actix-router = "0.5.0-beta.1" actix-rt = "2.2" actix-server = "2.0.0-beta.3" actix-service = "2.0.0" @@ -126,6 +127,7 @@ actix-files = { path = "actix-files" } actix-http = { path = "actix-http" } actix-http-test = { path = "actix-http-test" } actix-multipart = { path = "actix-multipart" } +actix-router = { path = "actix-router" } actix-test = { path = "actix-test" } actix-web = { path = "." } actix-web-actors = { path = "actix-web-actors" } diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md new file mode 100644 index 000000000..dea7cb76f --- /dev/null +++ b/actix-router/CHANGES.md @@ -0,0 +1,120 @@ +# Changes + +## Unreleased - 2021-xx-xx +* Introduce `ResourceDef::join`. [#380] +* Disallow prefix routes with tail segments. [#379] +* Enforce path separators on dynamic prefixes. [#378] +* Improve malformed path error message. [#384] + +[#378]: https://github.com/actix/actix-net/pull/378 +[#379]: https://github.com/actix/actix-net/pull/379 +[#380]: https://github.com/actix/actix-net/pull/380 +[#384]: https://github.com/actix/actix-net/pull/384 + + +## 0.5.0-beta.1 - 2021-07-20 +* Fix a bug in multi-patterns where static patterns are interpreted as regex. [#366] +* Introduce `ResourceDef::pattern_iter` to get an iterator over all patterns in a multi-pattern resource. [#373] +* Fix segment interpolation leaving `Path` in unintended state after matching. [#368] +* Fix `ResourceDef` `PartialEq` implementation. [#373] +* Re-work `IntoPatterns` trait, adding a `Patterns` enum. [#372] +* Implement `IntoPatterns` for `bytestring::ByteString`. [#372] +* Rename `Path::{len => segment_count}` to be more descriptive of it's purpose. [#370] +* Rename `ResourceDef::{resource_path => resource_path_from_iter}`. [#371] +* `ResourceDef::resource_path_from_iter` now takes an `IntoIterator`. [#373] +* Rename `ResourceDef::{resource_path_named => resource_path_from_map}`. [#371] +* Rename `ResourceDef::{is_prefix_match => find_match}`. [#373] +* Rename `ResourceDef::{match_path => capture_match_info}`. [#373] +* Rename `ResourceDef::{match_path_checked => capture_match_info_fn}`. [#373] +* Remove `ResourceDef::name_mut` and introduce `ResourceDef::set_name`. [#373] +* Rename `Router::{*_checked => *_fn}`. [#373] +* Return type of `ResourceDef::name` is now `Option<&str>`. [#373] +* Return type of `ResourceDef::pattern` is now `Option<&str>`. [#373] + +[#368]: https://github.com/actix/actix-net/pull/368 +[#366]: https://github.com/actix/actix-net/pull/366 +[#368]: https://github.com/actix/actix-net/pull/368 +[#370]: https://github.com/actix/actix-net/pull/370 +[#371]: https://github.com/actix/actix-net/pull/371 +[#372]: https://github.com/actix/actix-net/pull/372 +[#373]: https://github.com/actix/actix-net/pull/373 + + +## 0.4.0 - 2021-06-06 +* When matching path parameters, `%25` is now kept in the percent-encoded form; no longer decoded to `%`. [#357] +* Path tail patterns now match new lines (`\n`) in request URL. [#360] +* Fixed a safety bug where `Path` could return a malformed string after percent decoding. [#359] +* Methods `Path::{add, add_static}` now take `impl Into>`. [#345] + +[#345]: https://github.com/actix/actix-net/pull/345 +[#357]: https://github.com/actix/actix-net/pull/357 +[#359]: https://github.com/actix/actix-net/pull/359 +[#360]: https://github.com/actix/actix-net/pull/360 + + +## 0.3.0 - 2019-12-31 +* Version was yanked previously. See https://crates.io/crates/actix-router/0.3.0 + + +## 0.2.7 - 2021-02-06 +* Add `Router::recognize_checked` [#247] + +[#247]: https://github.com/actix/actix-net/pull/247 + + +## 0.2.6 - 2021-01-09 +* Use `bytestring` version range compatible with Bytes v1.0. [#246] + +[#246]: https://github.com/actix/actix-net/pull/246 + + +## 0.2.5 - 2020-09-20 +* Fix `from_hex()` method + + +## 0.2.4 - 2019-12-31 +* Add `ResourceDef::resource_path_named()` path generation method + + +## 0.2.3 - 2019-12-25 +* Add impl `IntoPattern` for `&String` + + +## 0.2.2 - 2019-12-25 +* Use `IntoPattern` for `RouterBuilder::path()` + + +## 0.2.1 - 2019-12-25 +* Add `IntoPattern` trait +* Add multi-pattern resources + + +## 0.2.0 - 2019-12-07 +* Update http to 0.2 +* Update regex to 1.3 +* Use bytestring instead of string + + +## 0.1.5 - 2019-05-15 +* Remove debug prints + + +## 0.1.4 - 2019-05-15 +* Fix checked resource match + + +## 0.1.3 - 2019-04-22 +* Added support for `remainder match` (i.e "/path/{tail}*") + + +## 0.1.2 - 2019-04-07 +* Export `Quoter` type +* Allow to reset `Path` instance + + +## 0.1.1 - 2019-04-03 +* Get dynamic segment by name instead of iterator. + + +## 0.1.0 - 2019-03-09 +* Initial release diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml new file mode 100644 index 000000000..2a2ce1cc1 --- /dev/null +++ b/actix-router/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "actix-router" +version = "0.5.0-beta.1" +authors = [ + "Nikolay Kim ", + "Ali MJ Al-Nasrawy ", + "Rob Ede ", +] +description = "Resource path matching and router" +keywords = ["actix", "router", "routing"] +repository = "https://github.com/actix/actix-net.git" +license = "MIT OR Apache-2.0" +edition = "2018" + +[lib] +name = "actix_router" +path = "src/lib.rs" + +[features] +default = ["http"] + +[dependencies] +bytestring = ">=0.1.5, <2" +firestorm = "0.4" +http = { version = "0.2.3", optional = true } +log = "0.4" +regex = "1.5" +serde = "1" + +[dev-dependencies] +criterion = { version = "0.3", features = ["html_reports"] } +firestorm = { version = "0.4", features = ["enable_system_time"] } +http = "0.2.3" +serde = { version = "1", features = ["derive"] } + +[[bench]] +name = "router" +harness = false diff --git a/actix-router/LICENSE-APACHE b/actix-router/LICENSE-APACHE new file mode 120000 index 000000000..965b606f3 --- /dev/null +++ b/actix-router/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/actix-router/LICENSE-MIT b/actix-router/LICENSE-MIT new file mode 120000 index 000000000..76219eb72 --- /dev/null +++ b/actix-router/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/actix-router/benches/router.rs b/actix-router/benches/router.rs new file mode 100644 index 000000000..a428b9f13 --- /dev/null +++ b/actix-router/benches/router.rs @@ -0,0 +1,194 @@ +//! Based on https://github.com/ibraheemdev/matchit/blob/master/benches/bench.rs + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +macro_rules! register { + (colon) => {{ + register!(finish => ":p1", ":p2", ":p3", ":p4") + }}; + (brackets) => {{ + register!(finish => "{p1}", "{p2}", "{p3}", "{p4}") + }}; + (regex) => {{ + register!(finish => "(.*)", "(.*)", "(.*)", "(.*)") + }}; + (finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{ + let arr = [ + concat!("/authorizations"), + concat!("/authorizations/", $p1), + concat!("/applications/", $p1, "/tokens/", $p2), + concat!("/events"), + concat!("/repos/", $p1, "/", $p2, "/events"), + concat!("/networks/", $p1, "/", $p2, "/events"), + concat!("/orgs/", $p1, "/events"), + concat!("/users/", $p1, "/received_events"), + concat!("/users/", $p1, "/received_events/public"), + concat!("/users/", $p1, "/events"), + concat!("/users/", $p1, "/events/public"), + concat!("/users/", $p1, "/events/orgs/", $p2), + concat!("/feeds"), + concat!("/notifications"), + concat!("/repos/", $p1, "/", $p2, "/notifications"), + concat!("/notifications/threads/", $p1), + concat!("/notifications/threads/", $p1, "/subscription"), + concat!("/repos/", $p1, "/", $p2, "/stargazers"), + concat!("/users/", $p1, "/starred"), + concat!("/user/starred"), + concat!("/user/starred/", $p1, "/", $p2), + concat!("/repos/", $p1, "/", $p2, "/subscribers"), + concat!("/users/", $p1, "/subscriptions"), + concat!("/user/subscriptions"), + concat!("/repos/", $p1, "/", $p2, "/subscription"), + concat!("/user/subscriptions/", $p1, "/", $p2), + concat!("/users/", $p1, "/gists"), + concat!("/gists"), + concat!("/gists/", $p1), + concat!("/gists/", $p1, "/star"), + concat!("/repos/", $p1, "/", $p2, "/git/blobs/", $p3), + concat!("/repos/", $p1, "/", $p2, "/git/commits/", $p3), + concat!("/repos/", $p1, "/", $p2, "/git/refs"), + concat!("/repos/", $p1, "/", $p2, "/git/tags/", $p3), + concat!("/repos/", $p1, "/", $p2, "/git/trees/", $p3), + concat!("/issues"), + concat!("/user/issues"), + concat!("/orgs/", $p1, "/issues"), + concat!("/repos/", $p1, "/", $p2, "/issues"), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3), + concat!("/repos/", $p1, "/", $p2, "/assignees"), + concat!("/repos/", $p1, "/", $p2, "/assignees/", $p3), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/comments"), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/events"), + concat!("/repos/", $p1, "/", $p2, "/labels"), + concat!("/repos/", $p1, "/", $p2, "/labels/", $p3), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/labels"), + concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3, "/labels"), + concat!("/repos/", $p1, "/", $p2, "/milestones/"), + concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3), + concat!("/emojis"), + concat!("/gitignore/templates"), + concat!("/gitignore/templates/", $p1), + concat!("/meta"), + concat!("/rate_limit"), + concat!("/users/", $p1, "/orgs"), + concat!("/user/orgs"), + concat!("/orgs/", $p1), + concat!("/orgs/", $p1, "/members"), + concat!("/orgs/", $p1, "/members", $p2), + concat!("/orgs/", $p1, "/public_members"), + concat!("/orgs/", $p1, "/public_members/", $p2), + concat!("/orgs/", $p1, "/teams"), + concat!("/teams/", $p1), + concat!("/teams/", $p1, "/members"), + concat!("/teams/", $p1, "/members", $p2), + concat!("/teams/", $p1, "/repos"), + concat!("/teams/", $p1, "/repos/", $p2, "/", $p3), + concat!("/user/teams"), + concat!("/repos/", $p1, "/", $p2, "/pulls"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/commits"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/files"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/merge"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/comments"), + concat!("/user/repos"), + concat!("/users/", $p1, "/repos"), + concat!("/orgs/", $p1, "/repos"), + concat!("/repositories"), + concat!("/repos/", $p1, "/", $p2), + concat!("/repos/", $p1, "/", $p2, "/contributors"), + concat!("/repos/", $p1, "/", $p2, "/languages"), + concat!("/repos/", $p1, "/", $p2, "/teams"), + concat!("/repos/", $p1, "/", $p2, "/tags"), + concat!("/repos/", $p1, "/", $p2, "/branches"), + concat!("/repos/", $p1, "/", $p2, "/branches/", $p3), + concat!("/repos/", $p1, "/", $p2, "/collaborators"), + concat!("/repos/", $p1, "/", $p2, "/collaborators/", $p3), + concat!("/repos/", $p1, "/", $p2, "/comments"), + concat!("/repos/", $p1, "/", $p2, "/commits/", $p3, "/comments"), + concat!("/repos/", $p1, "/", $p2, "/commits"), + concat!("/repos/", $p1, "/", $p2, "/commits/", $p3), + concat!("/repos/", $p1, "/", $p2, "/readme"), + concat!("/repos/", $p1, "/", $p2, "/keys"), + concat!("/repos/", $p1, "/", $p2, "/keys", $p3), + concat!("/repos/", $p1, "/", $p2, "/downloads"), + concat!("/repos/", $p1, "/", $p2, "/downloads", $p3), + concat!("/repos/", $p1, "/", $p2, "/forks"), + concat!("/repos/", $p1, "/", $p2, "/hooks"), + concat!("/repos/", $p1, "/", $p2, "/hooks", $p3), + concat!("/repos/", $p1, "/", $p2, "/releases"), + concat!("/repos/", $p1, "/", $p2, "/releases/", $p3), + concat!("/repos/", $p1, "/", $p2, "/releases/", $p3, "/assets"), + concat!("/repos/", $p1, "/", $p2, "/stats/contributors"), + concat!("/repos/", $p1, "/", $p2, "/stats/commit_activity"), + concat!("/repos/", $p1, "/", $p2, "/stats/code_frequency"), + concat!("/repos/", $p1, "/", $p2, "/stats/participation"), + concat!("/repos/", $p1, "/", $p2, "/stats/punch_card"), + concat!("/repos/", $p1, "/", $p2, "/statuses/", $p3), + concat!("/search/repositories"), + concat!("/search/code"), + concat!("/search/issues"), + concat!("/search/users"), + concat!("/legacy/issues/search/", $p1, "/", $p2, "/", $p3, "/", $p4), + concat!("/legacy/repos/search/", $p1), + concat!("/legacy/user/search/", $p1), + concat!("/legacy/user/email/", $p1), + concat!("/users/", $p1), + concat!("/user"), + concat!("/users"), + concat!("/user/emails"), + concat!("/users/", $p1, "/followers"), + concat!("/user/followers"), + concat!("/users/", $p1, "/following"), + concat!("/user/following"), + concat!("/user/following/", $p1), + concat!("/users/", $p1, "/following", $p2), + concat!("/users/", $p1, "/keys"), + concat!("/user/keys"), + concat!("/user/keys/", $p1), + ]; + std::array::IntoIter::new(arr) + }}; +} + +fn call() -> impl Iterator { + let arr = [ + "/authorizations", + "/user/repos", + "/repos/rust-lang/rust/stargazers", + "/orgs/rust-lang/public_members/nikomatsakis", + "/repos/rust-lang/rust/releases/1.51.0", + ]; + + std::array::IntoIter::new(arr) +} + +fn compare_routers(c: &mut Criterion) { + let mut group = c.benchmark_group("Compare Routers"); + + let mut actix = actix_router::Router::::build(); + for route in register!(brackets) { + actix.path(route, true); + } + let actix = actix.finish(); + group.bench_function("actix", |b| { + b.iter(|| { + for route in call() { + let mut path = actix_router::Path::new(route); + black_box(actix.recognize(&mut path).unwrap()); + } + }); + }); + + let regex_set = regex::RegexSet::new(register!(regex)).unwrap(); + group.bench_function("regex", |b| { + b.iter(|| { + for route in call() { + black_box(regex_set.matches(route)); + } + }); + }); + + group.finish(); +} + +criterion_group!(benches, compare_routers); +criterion_main!(benches); diff --git a/actix-router/examples/flamegraph.rs b/actix-router/examples/flamegraph.rs new file mode 100644 index 000000000..798cc22d9 --- /dev/null +++ b/actix-router/examples/flamegraph.rs @@ -0,0 +1,169 @@ +macro_rules! register { + (brackets) => {{ + register!(finish => "{p1}", "{p2}", "{p3}", "{p4}") + }}; + (finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{ + let arr = [ + concat!("/authorizations"), + concat!("/authorizations/", $p1), + concat!("/applications/", $p1, "/tokens/", $p2), + concat!("/events"), + concat!("/repos/", $p1, "/", $p2, "/events"), + concat!("/networks/", $p1, "/", $p2, "/events"), + concat!("/orgs/", $p1, "/events"), + concat!("/users/", $p1, "/received_events"), + concat!("/users/", $p1, "/received_events/public"), + concat!("/users/", $p1, "/events"), + concat!("/users/", $p1, "/events/public"), + concat!("/users/", $p1, "/events/orgs/", $p2), + concat!("/feeds"), + concat!("/notifications"), + concat!("/repos/", $p1, "/", $p2, "/notifications"), + concat!("/notifications/threads/", $p1), + concat!("/notifications/threads/", $p1, "/subscription"), + concat!("/repos/", $p1, "/", $p2, "/stargazers"), + concat!("/users/", $p1, "/starred"), + concat!("/user/starred"), + concat!("/user/starred/", $p1, "/", $p2), + concat!("/repos/", $p1, "/", $p2, "/subscribers"), + concat!("/users/", $p1, "/subscriptions"), + concat!("/user/subscriptions"), + concat!("/repos/", $p1, "/", $p2, "/subscription"), + concat!("/user/subscriptions/", $p1, "/", $p2), + concat!("/users/", $p1, "/gists"), + concat!("/gists"), + concat!("/gists/", $p1), + concat!("/gists/", $p1, "/star"), + concat!("/repos/", $p1, "/", $p2, "/git/blobs/", $p3), + concat!("/repos/", $p1, "/", $p2, "/git/commits/", $p3), + concat!("/repos/", $p1, "/", $p2, "/git/refs"), + concat!("/repos/", $p1, "/", $p2, "/git/tags/", $p3), + concat!("/repos/", $p1, "/", $p2, "/git/trees/", $p3), + concat!("/issues"), + concat!("/user/issues"), + concat!("/orgs/", $p1, "/issues"), + concat!("/repos/", $p1, "/", $p2, "/issues"), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3), + concat!("/repos/", $p1, "/", $p2, "/assignees"), + concat!("/repos/", $p1, "/", $p2, "/assignees/", $p3), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/comments"), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/events"), + concat!("/repos/", $p1, "/", $p2, "/labels"), + concat!("/repos/", $p1, "/", $p2, "/labels/", $p3), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/labels"), + concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3, "/labels"), + concat!("/repos/", $p1, "/", $p2, "/milestones/"), + concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3), + concat!("/emojis"), + concat!("/gitignore/templates"), + concat!("/gitignore/templates/", $p1), + concat!("/meta"), + concat!("/rate_limit"), + concat!("/users/", $p1, "/orgs"), + concat!("/user/orgs"), + concat!("/orgs/", $p1), + concat!("/orgs/", $p1, "/members"), + concat!("/orgs/", $p1, "/members", $p2), + concat!("/orgs/", $p1, "/public_members"), + concat!("/orgs/", $p1, "/public_members/", $p2), + concat!("/orgs/", $p1, "/teams"), + concat!("/teams/", $p1), + concat!("/teams/", $p1, "/members"), + concat!("/teams/", $p1, "/members", $p2), + concat!("/teams/", $p1, "/repos"), + concat!("/teams/", $p1, "/repos/", $p2, "/", $p3), + concat!("/user/teams"), + concat!("/repos/", $p1, "/", $p2, "/pulls"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/commits"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/files"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/merge"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/comments"), + concat!("/user/repos"), + concat!("/users/", $p1, "/repos"), + concat!("/orgs/", $p1, "/repos"), + concat!("/repositories"), + concat!("/repos/", $p1, "/", $p2), + concat!("/repos/", $p1, "/", $p2, "/contributors"), + concat!("/repos/", $p1, "/", $p2, "/languages"), + concat!("/repos/", $p1, "/", $p2, "/teams"), + concat!("/repos/", $p1, "/", $p2, "/tags"), + concat!("/repos/", $p1, "/", $p2, "/branches"), + concat!("/repos/", $p1, "/", $p2, "/branches/", $p3), + concat!("/repos/", $p1, "/", $p2, "/collaborators"), + concat!("/repos/", $p1, "/", $p2, "/collaborators/", $p3), + concat!("/repos/", $p1, "/", $p2, "/comments"), + concat!("/repos/", $p1, "/", $p2, "/commits/", $p3, "/comments"), + concat!("/repos/", $p1, "/", $p2, "/commits"), + concat!("/repos/", $p1, "/", $p2, "/commits/", $p3), + concat!("/repos/", $p1, "/", $p2, "/readme"), + concat!("/repos/", $p1, "/", $p2, "/keys"), + concat!("/repos/", $p1, "/", $p2, "/keys", $p3), + concat!("/repos/", $p1, "/", $p2, "/downloads"), + concat!("/repos/", $p1, "/", $p2, "/downloads", $p3), + concat!("/repos/", $p1, "/", $p2, "/forks"), + concat!("/repos/", $p1, "/", $p2, "/hooks"), + concat!("/repos/", $p1, "/", $p2, "/hooks", $p3), + concat!("/repos/", $p1, "/", $p2, "/releases"), + concat!("/repos/", $p1, "/", $p2, "/releases/", $p3), + concat!("/repos/", $p1, "/", $p2, "/releases/", $p3, "/assets"), + concat!("/repos/", $p1, "/", $p2, "/stats/contributors"), + concat!("/repos/", $p1, "/", $p2, "/stats/commit_activity"), + concat!("/repos/", $p1, "/", $p2, "/stats/code_frequency"), + concat!("/repos/", $p1, "/", $p2, "/stats/participation"), + concat!("/repos/", $p1, "/", $p2, "/stats/punch_card"), + concat!("/repos/", $p1, "/", $p2, "/statuses/", $p3), + concat!("/search/repositories"), + concat!("/search/code"), + concat!("/search/issues"), + concat!("/search/users"), + concat!("/legacy/issues/search/", $p1, "/", $p2, "/", $p3, "/", $p4), + concat!("/legacy/repos/search/", $p1), + concat!("/legacy/user/search/", $p1), + concat!("/legacy/user/email/", $p1), + concat!("/users/", $p1), + concat!("/user"), + concat!("/users"), + concat!("/user/emails"), + concat!("/users/", $p1, "/followers"), + concat!("/user/followers"), + concat!("/users/", $p1, "/following"), + concat!("/user/following"), + concat!("/user/following/", $p1), + concat!("/users/", $p1, "/following", $p2), + concat!("/users/", $p1, "/keys"), + concat!("/user/keys"), + concat!("/user/keys/", $p1), + ]; + + arr.to_vec() + }}; +} + +static PATHS: [&str; 5] = [ + "/authorizations", + "/user/repos", + "/repos/rust-lang/rust/stargazers", + "/orgs/rust-lang/public_members/nikomatsakis", + "/repos/rust-lang/rust/releases/1.51.0", +]; + +fn main() { + let mut router = actix_router::Router::::build(); + + for route in register!(brackets) { + router.path(route, true); + } + + let actix = router.finish(); + + if firestorm::enabled() { + firestorm::bench("target", || { + for &route in &PATHS { + let mut path = actix_router::Path::new(route); + actix.recognize(&mut path).unwrap(); + } + }) + .unwrap(); + } +} diff --git a/actix-router/src/de.rs b/actix-router/src/de.rs new file mode 100644 index 000000000..775c48b8a --- /dev/null +++ b/actix-router/src/de.rs @@ -0,0 +1,723 @@ +use serde::de::{self, Deserializer, Error as DeError, Visitor}; +use serde::forward_to_deserialize_any; + +use crate::path::{Path, PathIter}; +use crate::ResourcePath; + +macro_rules! unsupported_type { + ($trait_fn:ident, $name:expr) => { + fn $trait_fn(self, _: V) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom(concat!( + "unsupported type: ", + $name + ))) + } + }; +} + +macro_rules! parse_single_value { + ($trait_fn:ident, $visit_fn:ident, $tp:tt) => { + fn $trait_fn(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + if self.path.segment_count() != 1 { + Err(de::value::Error::custom( + format!( + "wrong number of parameters: {} expected 1", + self.path.segment_count() + ) + .as_str(), + )) + } else { + let v = self.path[0].parse().map_err(|_| { + de::value::Error::custom(format!( + "can not parse {:?} to a {}", + &self.path[0], $tp + )) + })?; + visitor.$visit_fn(v) + } + } + }; +} + +pub struct PathDeserializer<'de, T: ResourcePath> { + path: &'de Path, +} + +impl<'de, T: ResourcePath + 'de> PathDeserializer<'de, T> { + pub fn new(path: &'de Path) -> Self { + PathDeserializer { path } + } +} + +impl<'de, T: ResourcePath + 'de> Deserializer<'de> for PathDeserializer<'de, T> { + type Error = de::value::Error; + + fn deserialize_map(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_map(ParamsDeserializer { + params: self.path.iter(), + current: None, + }) + } + + fn deserialize_struct( + self, + _: &'static str, + _: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + self.deserialize_map(visitor) + } + + fn deserialize_unit(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_unit_struct( + self, + _: &'static str, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + self.deserialize_unit(visitor) + } + + fn deserialize_newtype_struct( + self, + _: &'static str, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + visitor.visit_newtype_struct(self) + } + + fn deserialize_tuple(self, len: usize, visitor: V) -> Result + where + V: Visitor<'de>, + { + if self.path.segment_count() < len { + Err(de::value::Error::custom( + format!( + "wrong number of parameters: {} expected {}", + self.path.segment_count(), + len + ) + .as_str(), + )) + } else { + visitor.visit_seq(ParamsSeq { + params: self.path.iter(), + }) + } + } + + fn deserialize_tuple_struct( + self, + _: &'static str, + len: usize, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + if self.path.segment_count() < len { + Err(de::value::Error::custom( + format!( + "wrong number of parameters: {} expected {}", + self.path.segment_count(), + len + ) + .as_str(), + )) + } else { + visitor.visit_seq(ParamsSeq { + params: self.path.iter(), + }) + } + } + + fn deserialize_enum( + self, + _: &'static str, + _: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + if self.path.is_empty() { + Err(de::value::Error::custom("expected at least one parameters")) + } else { + visitor.visit_enum(ValueEnum { + value: &self.path[0], + }) + } + } + + fn deserialize_str(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + if self.path.segment_count() != 1 { + Err(de::value::Error::custom( + format!( + "wrong number of parameters: {} expected 1", + self.path.segment_count() + ) + .as_str(), + )) + } else { + visitor.visit_str(&self.path[0]) + } + } + + fn deserialize_seq(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_seq(ParamsSeq { + params: self.path.iter(), + }) + } + + unsupported_type!(deserialize_any, "'any'"); + unsupported_type!(deserialize_bytes, "bytes"); + unsupported_type!(deserialize_option, "Option"); + unsupported_type!(deserialize_identifier, "identifier"); + unsupported_type!(deserialize_ignored_any, "ignored_any"); + + parse_single_value!(deserialize_bool, visit_bool, "bool"); + parse_single_value!(deserialize_i8, visit_i8, "i8"); + parse_single_value!(deserialize_i16, visit_i16, "i16"); + parse_single_value!(deserialize_i32, visit_i32, "i32"); + parse_single_value!(deserialize_i64, visit_i64, "i64"); + parse_single_value!(deserialize_u8, visit_u8, "u8"); + parse_single_value!(deserialize_u16, visit_u16, "u16"); + parse_single_value!(deserialize_u32, visit_u32, "u32"); + parse_single_value!(deserialize_u64, visit_u64, "u64"); + parse_single_value!(deserialize_f32, visit_f32, "f32"); + parse_single_value!(deserialize_f64, visit_f64, "f64"); + parse_single_value!(deserialize_string, visit_string, "String"); + parse_single_value!(deserialize_byte_buf, visit_string, "String"); + parse_single_value!(deserialize_char, visit_char, "char"); +} + +struct ParamsDeserializer<'de, T: ResourcePath> { + params: PathIter<'de, T>, + current: Option<(&'de str, &'de str)>, +} + +impl<'de, T: ResourcePath> de::MapAccess<'de> for ParamsDeserializer<'de, T> { + type Error = de::value::Error; + + fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> + where + K: de::DeserializeSeed<'de>, + { + self.current = self.params.next().map(|ref item| (item.0, item.1)); + match self.current { + Some((key, _)) => Ok(Some(seed.deserialize(Key { key })?)), + None => Ok(None), + } + } + + fn next_value_seed(&mut self, seed: V) -> Result + where + V: de::DeserializeSeed<'de>, + { + if let Some((_, value)) = self.current.take() { + seed.deserialize(Value { value }) + } else { + Err(de::value::Error::custom("unexpected item")) + } + } +} + +struct Key<'de> { + key: &'de str, +} + +impl<'de> Deserializer<'de> for Key<'de> { + type Error = de::value::Error; + + fn deserialize_identifier(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_str(self.key) + } + + fn deserialize_any(self, _visitor: V) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("Unexpected")) + } + + forward_to_deserialize_any! { + bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str string bytes + byte_buf option unit unit_struct newtype_struct seq tuple + tuple_struct map struct enum ignored_any + } +} + +macro_rules! parse_value { + ($trait_fn:ident, $visit_fn:ident, $tp:tt) => { + fn $trait_fn(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + let v = self.value.parse().map_err(|_| { + de::value::Error::custom(format!("can not parse {:?} to a {}", self.value, $tp)) + })?; + visitor.$visit_fn(v) + } + }; +} + +struct Value<'de> { + value: &'de str, +} + +impl<'de> Deserializer<'de> for Value<'de> { + type Error = de::value::Error; + + parse_value!(deserialize_bool, visit_bool, "bool"); + parse_value!(deserialize_i8, visit_i8, "i8"); + parse_value!(deserialize_i16, visit_i16, "i16"); + parse_value!(deserialize_i32, visit_i32, "i16"); + parse_value!(deserialize_i64, visit_i64, "i64"); + parse_value!(deserialize_u8, visit_u8, "u8"); + parse_value!(deserialize_u16, visit_u16, "u16"); + parse_value!(deserialize_u32, visit_u32, "u32"); + parse_value!(deserialize_u64, visit_u64, "u64"); + parse_value!(deserialize_f32, visit_f32, "f32"); + parse_value!(deserialize_f64, visit_f64, "f64"); + parse_value!(deserialize_string, visit_string, "String"); + parse_value!(deserialize_byte_buf, visit_string, "String"); + parse_value!(deserialize_char, visit_char, "char"); + + fn deserialize_ignored_any(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_unit(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_unit_struct( + self, + _: &'static str, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_bytes(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_borrowed_bytes(self.value.as_bytes()) + } + + fn deserialize_str(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_borrowed_str(self.value) + } + + fn deserialize_option(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_some(self) + } + + fn deserialize_enum( + self, + _: &'static str, + _: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + visitor.visit_enum(ValueEnum { value: self.value }) + } + + fn deserialize_newtype_struct( + self, + _: &'static str, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + visitor.visit_newtype_struct(self) + } + + fn deserialize_tuple(self, _: usize, _: V) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("unsupported type: tuple")) + } + + fn deserialize_struct( + self, + _: &'static str, + _: &'static [&'static str], + _: V, + ) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("unsupported type: struct")) + } + + fn deserialize_tuple_struct( + self, + _: &'static str, + _: usize, + _: V, + ) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("unsupported type: tuple struct")) + } + + unsupported_type!(deserialize_any, "any"); + unsupported_type!(deserialize_seq, "seq"); + unsupported_type!(deserialize_map, "map"); + unsupported_type!(deserialize_identifier, "identifier"); +} + +struct ParamsSeq<'de, T: ResourcePath> { + params: PathIter<'de, T>, +} + +impl<'de, T: ResourcePath> de::SeqAccess<'de> for ParamsSeq<'de, T> { + type Error = de::value::Error; + + fn next_element_seed(&mut self, seed: U) -> Result, Self::Error> + where + U: de::DeserializeSeed<'de>, + { + match self.params.next() { + Some(item) => Ok(Some(seed.deserialize(Value { value: item.1 })?)), + None => Ok(None), + } + } +} + +struct ValueEnum<'de> { + value: &'de str, +} + +impl<'de> de::EnumAccess<'de> for ValueEnum<'de> { + type Error = de::value::Error; + type Variant = UnitVariant; + + fn variant_seed(self, seed: V) -> Result<(V::Value, Self::Variant), Self::Error> + where + V: de::DeserializeSeed<'de>, + { + Ok((seed.deserialize(Key { key: self.value })?, UnitVariant)) + } +} + +struct UnitVariant; + +impl<'de> de::VariantAccess<'de> for UnitVariant { + type Error = de::value::Error; + + fn unit_variant(self) -> Result<(), Self::Error> { + Ok(()) + } + + fn newtype_variant_seed(self, _seed: T) -> Result + where + T: de::DeserializeSeed<'de>, + { + Err(de::value::Error::custom("not supported")) + } + + fn tuple_variant(self, _len: usize, _visitor: V) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("not supported")) + } + + fn struct_variant( + self, + _: &'static [&'static str], + _: V, + ) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("not supported")) + } +} + +#[cfg(test)] +mod tests { + use serde::{de, Deserialize}; + + use super::*; + use crate::path::Path; + use crate::router::Router; + + #[derive(Deserialize)] + struct MyStruct { + key: String, + value: String, + } + + #[derive(Deserialize)] + struct Id { + _id: String, + } + + #[derive(Debug, Deserialize)] + struct Test1(String, u32); + + #[derive(Debug, Deserialize)] + struct Test2 { + key: String, + value: u32, + } + + #[derive(Debug, Deserialize, PartialEq)] + #[serde(rename_all = "lowercase")] + enum TestEnum { + Val1, + Val2, + } + + #[derive(Debug, Deserialize)] + struct Test3 { + val: TestEnum, + } + + #[test] + fn test_request_extract() { + let mut router = Router::<()>::build(); + router.path("/{key}/{value}/", ()); + let router = router.finish(); + + let mut path = Path::new("/name/user1/"); + assert!(router.recognize(&mut path).is_some()); + + let s: MyStruct = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(s.key, "name"); + assert_eq!(s.value, "user1"); + + let s: (String, String) = + de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(s.0, "name"); + assert_eq!(s.1, "user1"); + + let mut router = Router::<()>::build(); + router.path("/{key}/{value}/", ()); + let router = router.finish(); + + let mut path = Path::new("/name/32/"); + assert!(router.recognize(&mut path).is_some()); + + let s: Test1 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(s.0, "name"); + assert_eq!(s.1, 32); + + let s: Test2 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(s.key, "name"); + assert_eq!(s.value, 32); + + let s: (String, u8) = + de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(s.0, "name"); + assert_eq!(s.1, 32); + + let res: Vec = + de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(res[0], "name".to_owned()); + assert_eq!(res[1], "32".to_owned()); + } + + #[test] + fn test_extract_path_single() { + let mut router = Router::<()>::build(); + router.path("/{value}/", ()); + let router = router.finish(); + + let mut path = Path::new("/32/"); + assert!(router.recognize(&mut path).is_some()); + let i: i8 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(i, 32); + } + + #[test] + fn test_extract_enum() { + let mut router = Router::<()>::build(); + router.path("/{val}/", ()); + let router = router.finish(); + + let mut path = Path::new("/val1/"); + assert!(router.recognize(&mut path).is_some()); + let i: TestEnum = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(i, TestEnum::Val1); + + let mut router = Router::<()>::build(); + router.path("/{val1}/{val2}/", ()); + let router = router.finish(); + + let mut path = Path::new("/val1/val2/"); + assert!(router.recognize(&mut path).is_some()); + let i: (TestEnum, TestEnum) = + de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(i, (TestEnum::Val1, TestEnum::Val2)); + } + + #[test] + fn test_extract_enum_value() { + let mut router = Router::<()>::build(); + router.path("/{val}/", ()); + let router = router.finish(); + + let mut path = Path::new("/val1/"); + assert!(router.recognize(&mut path).is_some()); + let i: Test3 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(i.val, TestEnum::Val1); + + let mut path = Path::new("/val3/"); + assert!(router.recognize(&mut path).is_some()); + let i: Result = + de::Deserialize::deserialize(PathDeserializer::new(&path)); + assert!(i.is_err()); + assert!(format!("{:?}", i).contains("unknown variant")); + } + + #[test] + fn test_extract_errors() { + let mut router = Router::<()>::build(); + router.path("/{value}/", ()); + let router = router.finish(); + + let mut path = Path::new("/name/"); + assert!(router.recognize(&mut path).is_some()); + + let s: Result = + de::Deserialize::deserialize(PathDeserializer::new(&path)); + assert!(s.is_err()); + assert!(format!("{:?}", s).contains("wrong number of parameters")); + + let s: Result = + de::Deserialize::deserialize(PathDeserializer::new(&path)); + assert!(s.is_err()); + assert!(format!("{:?}", s).contains("can not parse")); + + let s: Result<(String, String), de::value::Error> = + de::Deserialize::deserialize(PathDeserializer::new(&path)); + assert!(s.is_err()); + assert!(format!("{:?}", s).contains("wrong number of parameters")); + + let s: Result = + de::Deserialize::deserialize(PathDeserializer::new(&path)); + assert!(s.is_err()); + assert!(format!("{:?}", s).contains("can not parse")); + } + + // #[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" + // ); + // } +} diff --git a/actix-router/src/lib.rs b/actix-router/src/lib.rs new file mode 100644 index 000000000..463e59e42 --- /dev/null +++ b/actix-router/src/lib.rs @@ -0,0 +1,149 @@ +//! Resource path matching and router. + +#![deny(rust_2018_idioms, nonstandard_style)] +#![doc(html_logo_url = "https://actix.rs/img/logo.png")] +#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] + +mod de; +mod path; +mod resource; +mod router; + +pub use self::de::PathDeserializer; +pub use self::path::Path; +pub use self::resource::ResourceDef; +pub use self::router::{ResourceInfo, Router, RouterBuilder}; + +// TODO: this trait is necessary, document it +// see impl Resource for ServiceRequest +pub trait Resource { + fn resource_path(&mut self) -> &mut Path; +} + +pub trait ResourcePath { + fn path(&self) -> &str; +} + +impl ResourcePath for String { + fn path(&self) -> &str { + self.as_str() + } +} + +impl<'a> ResourcePath for &'a str { + fn path(&self) -> &str { + self + } +} + +impl ResourcePath for bytestring::ByteString { + fn path(&self) -> &str { + &*self + } +} + +/// One or many patterns. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Patterns { + Single(String), + List(Vec), +} + +impl Patterns { + pub fn is_empty(&self) -> bool { + match self { + Patterns::Single(_) => false, + Patterns::List(pats) => pats.is_empty(), + } + } +} + +/// Helper trait for type that could be converted to one or more path pattern. +pub trait IntoPatterns { + fn patterns(&self) -> Patterns; +} + +impl IntoPatterns for String { + fn patterns(&self) -> Patterns { + Patterns::Single(self.clone()) + } +} + +impl<'a> IntoPatterns for &'a String { + fn patterns(&self) -> Patterns { + Patterns::Single((*self).clone()) + } +} + +impl<'a> IntoPatterns for &'a str { + fn patterns(&self) -> Patterns { + Patterns::Single((*self).to_owned()) + } +} + +impl IntoPatterns for bytestring::ByteString { + fn patterns(&self) -> Patterns { + Patterns::Single(self.to_string()) + } +} + +impl IntoPatterns for Patterns { + fn patterns(&self) -> Patterns { + self.clone() + } +} + +impl> IntoPatterns for Vec { + fn patterns(&self) -> Patterns { + let mut patterns = self.iter().map(|v| v.as_ref().to_owned()); + + match patterns.size_hint() { + (1, _) => Patterns::Single(patterns.next().unwrap()), + _ => Patterns::List(patterns.collect()), + } + } +} + +macro_rules! array_patterns_single (($tp:ty) => { + impl IntoPatterns for [$tp; 1] { + fn patterns(&self) -> Patterns { + Patterns::Single(self[0].to_owned()) + } + } +}); + +macro_rules! array_patterns_multiple (($tp:ty, $str_fn:expr, $($num:tt) +) => { + // for each array length specified in $num + $( + impl IntoPatterns for [$tp; $num] { + fn patterns(&self) -> Patterns { + Patterns::List(self.iter().map($str_fn).collect()) + } + } + )+ +}); + +array_patterns_single!(&str); +array_patterns_multiple!(&str, |&v| v.to_owned(), 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16); + +array_patterns_single!(String); +array_patterns_multiple!(String, |v| v.clone(), 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16); + +#[cfg(feature = "http")] +mod url; + +#[cfg(feature = "http")] +pub use self::url::{Quoter, Url}; + +#[cfg(feature = "http")] +mod http_impls { + use http::Uri; + + use super::ResourcePath; + + impl ResourcePath for Uri { + fn path(&self) -> &str { + self.path() + } + } +} diff --git a/actix-router/src/path.rs b/actix-router/src/path.rs new file mode 100644 index 000000000..e29591f96 --- /dev/null +++ b/actix-router/src/path.rs @@ -0,0 +1,220 @@ +use std::borrow::Cow; +use std::ops::Index; + +use firestorm::profile_method; +use serde::de; + +use crate::{de::PathDeserializer, Resource, ResourcePath}; + +#[derive(Debug, Clone)] +pub(crate) enum PathItem { + Static(Cow<'static, str>), + Segment(u16, u16), +} + +impl Default for PathItem { + fn default() -> Self { + Self::Static(Cow::Borrowed("")) + } +} + +/// Resource path match information. +/// +/// If resource path contains variable patterns, `Path` stores them. +#[derive(Debug, Clone, Default)] +pub struct Path { + path: T, + pub(crate) skip: u16, + pub(crate) segments: Vec<(Cow<'static, str>, PathItem)>, +} + +impl Path { + pub fn new(path: T) -> Path { + Path { + path, + skip: 0, + segments: Vec::new(), + } + } + + /// Get reference to inner path instance. + #[inline] + pub fn get_ref(&self) -> &T { + &self.path + } + + /// Get mutable reference to inner path instance. + #[inline] + pub fn get_mut(&mut self) -> &mut T { + &mut self.path + } + + /// Path. + #[inline] + pub fn path(&self) -> &str { + profile_method!(path); + + let skip = self.skip as usize; + let path = self.path.path(); + if skip <= path.len() { + &path[skip..] + } else { + "" + } + } + + /// Set new path. + #[inline] + pub fn set(&mut self, path: T) { + self.skip = 0; + self.path = path; + self.segments.clear(); + } + + /// Reset state. + #[inline] + pub fn reset(&mut self) { + self.skip = 0; + self.segments.clear(); + } + + /// Skip first `n` chars in path. + #[inline] + pub fn skip(&mut self, n: u16) { + self.skip += n; + } + + pub(crate) fn add(&mut self, name: impl Into>, value: PathItem) { + profile_method!(add); + + match value { + PathItem::Static(s) => self.segments.push((name.into(), PathItem::Static(s))), + PathItem::Segment(begin, end) => self.segments.push(( + name.into(), + PathItem::Segment(self.skip + begin, self.skip + end), + )), + } + } + + #[doc(hidden)] + pub fn add_static( + &mut self, + name: impl Into>, + value: impl Into>, + ) { + self.segments + .push((name.into(), PathItem::Static(value.into()))); + } + + /// Check if there are any matched patterns. + #[inline] + pub fn is_empty(&self) -> bool { + self.segments.is_empty() + } + + /// Returns number of interpolated segments. + #[inline] + pub fn segment_count(&self) -> usize { + self.segments.len() + } + + /// Get matched parameter by name without type conversion + pub fn get(&self, name: &str) -> Option<&str> { + profile_method!(get); + + for (seg_name, val) in self.segments.iter() { + if name == seg_name { + return match val { + PathItem::Static(ref s) => Some(&s), + PathItem::Segment(s, e) => { + Some(&self.path.path()[(*s as usize)..(*e as usize)]) + } + }; + } + } + + None + } + + /// Get unprocessed part of the path + pub fn unprocessed(&self) -> &str { + &self.path.path()[(self.skip as usize)..] + } + + /// Get matched parameter by name. + /// + /// If keyed parameter is not available empty string is used as default value. + pub fn query(&self, key: &str) -> &str { + profile_method!(query); + + if let Some(s) = self.get(key) { + s + } else { + "" + } + } + + /// Return iterator to items in parameter container. + pub fn iter(&self) -> PathIter<'_, T> { + PathIter { + idx: 0, + params: self, + } + } + + /// Try to deserialize matching parameters to a specified type `U` + pub fn load<'de, U: serde::Deserialize<'de>>(&'de self) -> Result { + profile_method!(load); + de::Deserialize::deserialize(PathDeserializer::new(self)) + } +} + +#[derive(Debug)] +pub struct PathIter<'a, T> { + idx: usize, + params: &'a Path, +} + +impl<'a, T: ResourcePath> Iterator for PathIter<'a, T> { + type Item = (&'a str, &'a str); + + #[inline] + fn next(&mut self) -> Option<(&'a str, &'a str)> { + if self.idx < self.params.segment_count() { + let idx = self.idx; + let res = match self.params.segments[idx].1 { + PathItem::Static(ref s) => &s, + PathItem::Segment(s, e) => &self.params.path.path()[(s as usize)..(e as usize)], + }; + self.idx += 1; + return Some((&self.params.segments[idx].0, res)); + } + None + } +} + +impl<'a, T: ResourcePath> Index<&'a str> for Path { + type Output = str; + + fn index(&self, name: &'a str) -> &str { + self.get(name) + .expect("Value for parameter is not available") + } +} + +impl Index for Path { + type Output = str; + + fn index(&self, idx: usize) -> &str { + match self.segments[idx].1 { + PathItem::Static(ref s) => &s, + PathItem::Segment(s, e) => &self.path.path()[(s as usize)..(e as usize)], + } + } +} + +impl Resource for Path { + fn resource_path(&mut self) -> &mut Self { + self + } +} diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs new file mode 100644 index 000000000..61ff587a5 --- /dev/null +++ b/actix-router/src/resource.rs @@ -0,0 +1,1803 @@ +use std::{ + borrow::{Borrow, Cow}, + collections::HashMap, + hash::{BuildHasher, Hash, Hasher}, + mem, +}; + +use firestorm::{profile_fn, profile_method, profile_section}; +use regex::{escape, Regex, RegexSet}; + +use crate::{ + path::{Path, PathItem}, + IntoPatterns, Patterns, Resource, ResourcePath, +}; + +const MAX_DYNAMIC_SEGMENTS: usize = 16; + +/// Regex flags to allow '.' in regex to match '\n' +/// +/// See the docs under: https://docs.rs/regex/1/regex/#grouping-and-flags +const REGEX_FLAGS: &str = "(?s-m)"; + +/// Describes the set of paths that match to a resource. +/// +/// `ResourceDef`s are effectively a way to transform the a custom resource pattern syntax into +/// suitable regular expressions from which to check matches with paths and capture portions of a +/// matched path into variables. Common cases are on a fast path that avoids going through the +/// regex engine. +/// +/// +/// # Static Resources +/// A static resource is the most basic type of definition. Pass a regular string to +/// [new][Self::new]. Conforming paths must match the string exactly. +/// +/// ## Examples +/// ``` +/// # use actix_router::ResourceDef; +/// let resource = ResourceDef::new("/home"); +/// +/// assert!(resource.is_match("/home")); +/// +/// assert!(!resource.is_match("/home/new")); +/// assert!(!resource.is_match("/homes")); +/// assert!(!resource.is_match("/search")); +/// ``` +/// +/// +/// # Dynamic Segments +/// Also known as "path parameters". Resources can define sections of a pattern that be extracted +/// from a conforming path, if it conforms to (one of) the resource pattern(s). +/// +/// The marker for a dynamic segment is curly braces wrapping an identifier. For example, +/// `/user/{id}` would match paths like `/user/123` or `/user/james` and be able to extract the user +/// IDs "123" and "james", respectively. +/// +/// However, this resource pattern (`/user/{id}`) would, not cover `/user/123/stars` (unless +/// constructed as a prefix; see next section) since the default pattern for segments matches all +/// characters until it finds a `/` character (or the end of the path). Custom segment patterns are +/// covered further down. +/// +/// Dynamic segments do not need to be delimited by `/` characters, they can be defined within a +/// path segment. For example, `/rust-is-{opinion}` can match the paths `/rust-is-cool` and +/// `/rust-is-hard`. +/// +/// For information on capturing segment values from paths or other custom resource types, +/// see [`capture_match_info`][Self::capture_match_info] +/// and [`capture_match_info_fn`][Self::capture_match_info_fn]. +/// +/// A resource definition can contain at most 16 dynamic segments. +/// +/// ## Examples +/// ``` +/// use actix_router::{Path, ResourceDef}; +/// +/// let resource = ResourceDef::prefix("/user/{id}"); +/// +/// assert!(resource.is_match("/user/123")); +/// assert!(!resource.is_match("/user")); +/// assert!(!resource.is_match("/user/")); +/// +/// let mut path = Path::new("/user/123"); +/// resource.capture_match_info(&mut path); +/// assert_eq!(path.get("id").unwrap(), "123"); +/// ``` +/// +/// +/// # Prefix Resources +/// A prefix resource is defined as pattern that can match just the start of a path. +/// +/// This library chooses to restrict that definition slightly. In particular, when matching, the +/// prefix must be separated from the remaining part of the path by a `/` character, either at the +/// end of the prefix pattern or at the start of the the remaining slice. In practice, this is not +/// much of a limitation. +/// +/// Prefix resources can contain dynamic segments. +/// +/// ## Examples +/// ``` +/// # use actix_router::ResourceDef; +/// let resource = ResourceDef::prefix("/home"); +/// assert!(resource.is_match("/home")); +/// assert!(resource.is_match("/home/new")); +/// assert!(!resource.is_match("/homes")); +/// +/// let resource = ResourceDef::prefix("/user/{id}/"); +/// assert!(resource.is_match("/user/123/")); +/// assert!(resource.is_match("/user/123/stars")); +/// ``` +/// +/// +/// # Custom Regex Segments +/// Dynamic segments can be customised to only match a specific regular expression. It can be +/// helpful to do this if resource definitions would otherwise conflict and cause one to +/// be inaccessible. +/// +/// The regex used when capturing segment values can be specified explicitly using this syntax: +/// `{name:regex}`. For example, `/user/{id:\d+}` will only match paths where the user ID +/// is numeric. +/// +/// By default, dynamic segments use this regex: `[^/]+`. This shows why it is the case, as shown in +/// the earlier section, that segments capture a slice of the path up to the next `/` character. +/// +/// Custom regex segments can be used in static and prefix resource definition variants. +/// +/// ## Examples +/// ``` +/// # use actix_router::ResourceDef; +/// let resource = ResourceDef::new(r"/user/{id:\d+}"); +/// assert!(resource.is_match("/user/123")); +/// assert!(resource.is_match("/user/314159")); +/// assert!(!resource.is_match("/user/abc")); +/// ``` +/// +/// +/// # Tail Segments +/// As a shortcut to defining a custom regex for matching _all_ remaining characters (not just those +/// up until a `/` character), there is a special pattern to match (and capture) the remaining +/// path portion. +/// +/// To do this, use the segment pattern: `{name}*`. Since a tail segment also has a name, values are +/// extracted in the same way as non-tail dynamic segments. +/// +/// ## Examples +/// ```rust +/// # use actix_router::{Path, ResourceDef}; +/// let resource = ResourceDef::new("/blob/{tail}*"); +/// assert!(resource.is_match("/blob/HEAD/Cargo.toml")); +/// assert!(resource.is_match("/blob/HEAD/README.md")); +/// +/// let mut path = Path::new("/blob/main/LICENSE"); +/// resource.capture_match_info(&mut path); +/// assert_eq!(path.get("tail").unwrap(), "main/LICENSE"); +/// ``` +/// +/// +/// # Multi-Pattern Resources +/// For resources that can map to multiple distinct paths, it may be suitable to use +/// multi-pattern resources by passing an array/vec to [`new`][Self::new]. They will be combined +/// into a regex set which is usually quicker to check matches on than checking each +/// pattern individually. +/// +/// Multi-pattern resources can contain dynamic segments just like single pattern ones. +/// However, take care to use consistent and semantically-equivalent segment names; it could affect +/// expectations in the router using these definitions and cause runtime panics. +/// +/// ## Examples +/// ```rust +/// # use actix_router::ResourceDef; +/// let resource = ResourceDef::new(["/home", "/index"]); +/// assert!(resource.is_match("/home")); +/// assert!(resource.is_match("/index")); +/// ``` +/// +/// +/// # Trailing Slashes +/// It should be noted that this library takes no steps to normalize intra-path or trailing slashes. +/// As such, all resource definitions implicitly expect a pre-processing step to normalize paths if +/// they you wish to accommodate "recoverable" path errors. Below are several examples of +/// resource-path pairs that would not be compatible. +/// +/// ## Examples +/// ```rust +/// # use actix_router::ResourceDef; +/// assert!(!ResourceDef::new("/root").is_match("/root/")); +/// assert!(!ResourceDef::new("/root/").is_match("/root")); +/// assert!(!ResourceDef::prefix("/root/").is_match("/root")); +/// ``` +#[derive(Clone, Debug)] +pub struct ResourceDef { + id: u16, + + /// Optional name of resource. + name: Option, + + /// Pattern that generated the resource definition. + /// + /// `None` when pattern type is `DynamicSet`. + patterns: Patterns, + + /// Pattern type. + pat_type: PatternType, + + /// List of segments that compose the pattern, in order. + /// + /// `None` when pattern type is `DynamicSet`. + segments: Option>, +} + +#[derive(Debug, Clone, PartialEq)] +enum PatternSegment { + /// Literal slice of pattern. + Const(String), + + /// Name of dynamic segment. + Var(String), +} + +#[derive(Clone, Debug)] +#[allow(clippy::large_enum_variant)] +enum PatternType { + /// Single constant/literal segment. + Static(String), + + /// Single constant/literal prefix segment. + Prefix(String), + + /// Single regular expression and list of dynamic segment names. + Dynamic(Regex, Vec<&'static str>), + + /// Regular expression set and list of component expressions plus dynamic segment names. + DynamicSet(RegexSet, Vec<(Regex, Vec<&'static str>)>), +} + +impl ResourceDef { + /// Constructs a new resource definition from patterns. + /// + /// Multi-pattern resources can be constructed by providing a slice (or vec) of patterns. + /// + /// # Panics + /// Panics if path pattern is malformed. + /// + /// # Examples + /// ``` + /// use actix_router::ResourceDef; + /// + /// let resource = ResourceDef::new("/user/{id}"); + /// assert!(resource.is_match("/user/123")); + /// assert!(!resource.is_match("/user/123/stars")); + /// assert!(!resource.is_match("user/1234")); + /// assert!(!resource.is_match("/foo")); + /// + /// let resource = ResourceDef::new(["/profile", "/user/{id}"]); + /// assert!(resource.is_match("/profile")); + /// assert!(resource.is_match("/user/123")); + /// assert!(!resource.is_match("user/123")); + /// assert!(!resource.is_match("/foo")); + /// ``` + pub fn new(paths: T) -> Self { + profile_method!(new); + + match paths.patterns() { + Patterns::Single(pattern) => ResourceDef::from_single_pattern(&pattern, false), + + // since zero length pattern sets are possible + // just return a useless `ResourceDef` + Patterns::List(patterns) if patterns.is_empty() => ResourceDef { + id: 0, + name: None, + patterns: Patterns::List(patterns), + pat_type: PatternType::DynamicSet(RegexSet::empty(), Vec::new()), + segments: None, + }, + + Patterns::List(patterns) => { + let mut re_set = Vec::with_capacity(patterns.len()); + let mut pattern_data = Vec::new(); + + for pattern in &patterns { + match ResourceDef::parse(&pattern, false, true) { + (PatternType::Dynamic(re, names), _) => { + re_set.push(re.as_str().to_owned()); + pattern_data.push((re, names)); + } + _ => unreachable!(), + } + } + + let pattern_re_set = RegexSet::new(re_set).unwrap(); + + ResourceDef { + id: 0, + name: None, + patterns: Patterns::List(patterns), + pat_type: PatternType::DynamicSet(pattern_re_set, pattern_data), + segments: None, + } + } + } + } + + /// Constructs a new resource definition using a string pattern that performs prefix matching. + /// + /// More specifically, the regular expressions generated for matching are different when using + /// this method vs using `new`; they will not be appended with the `$` meta-character that + /// matches the end of an input. + /// + /// Although it will compile and run correctly, it is meaningless to construct a prefix + /// resource definition with a tail segment; use [`new`][Self::new] in this case. + /// + /// # Panics + /// Panics if path regex pattern is malformed. + /// + /// # Examples + /// ``` + /// use actix_router::ResourceDef; + /// + /// let resource = ResourceDef::prefix("/user/{id}"); + /// assert!(resource.is_match("/user/123")); + /// assert!(resource.is_match("/user/123/stars")); + /// assert!(!resource.is_match("user/123")); + /// assert!(!resource.is_match("user/123/stars")); + /// assert!(!resource.is_match("/foo")); + /// + /// let resource = ResourceDef::prefix("user/{id}"); + /// assert!(resource.is_match("user/123")); + /// assert!(resource.is_match("user/123/stars")); + /// assert!(!resource.is_match("/user/123")); + /// assert!(!resource.is_match("/user/123/stars")); + /// assert!(!resource.is_match("foo")); + /// ``` + pub fn prefix(path: &str) -> Self { + profile_method!(prefix); + ResourceDef::from_single_pattern(path, true) + } + + /// Constructs a new resource definition using a string pattern that performs prefix matching, + /// inserting a `/` to beginning of the pattern if absent and pattern is not empty. + /// + /// # Panics + /// Panics if path regex pattern is malformed. + /// + /// # Examples + /// ``` + /// use actix_router::ResourceDef; + /// + /// let resource = ResourceDef::root_prefix("user/{id}"); + /// + /// assert_eq!(&resource, &ResourceDef::prefix("/user/{id}")); + /// assert_eq!(&resource, &ResourceDef::root_prefix("/user/{id}")); + /// assert_ne!(&resource, &ResourceDef::new("user/{id}")); + /// assert_ne!(&resource, &ResourceDef::new("/user/{id}")); + /// + /// assert!(resource.is_match("/user/123")); + /// assert!(!resource.is_match("user/123")); + /// ``` + pub fn root_prefix(path: &str) -> Self { + profile_method!(root_prefix); + ResourceDef::prefix(&insert_slash(path)) + } + + /// Returns a numeric resource ID. + /// + /// If not explicitly set using [`set_id`][Self::set_id], this will return `0`. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let mut resource = ResourceDef::new("/root"); + /// assert_eq!(resource.id(), 0); + /// + /// resource.set_id(42); + /// assert_eq!(resource.id(), 42); + /// ``` + pub fn id(&self) -> u16 { + self.id + } + + /// Set numeric resource ID. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let mut resource = ResourceDef::new("/root"); + /// resource.set_id(42); + /// assert_eq!(resource.id(), 42); + /// ``` + pub fn set_id(&mut self, id: u16) { + self.id = id; + } + + /// Returns resource definition name, if set. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let mut resource = ResourceDef::new("/root"); + /// assert!(resource.name().is_none()); + /// + /// resource.set_name("root"); + /// assert_eq!(resource.name().unwrap(), "root"); + pub fn name(&self) -> Option<&str> { + self.name.as_deref() + } + + /// Assigns a new name to the resource. + /// + /// # Panics + /// Panics if `name` is an empty string. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let mut resource = ResourceDef::new("/root"); + /// resource.set_name("root"); + /// assert_eq!(resource.name().unwrap(), "root"); + /// ``` + pub fn set_name(&mut self, name: impl Into) { + let name = name.into(); + + if name.is_empty() { + panic!("resource name should not be empty"); + } + + self.name = Some(name) + } + + /// Returns `true` if pattern type is prefix. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// assert!(ResourceDef::prefix("/user").is_prefix()); + /// assert!(!ResourceDef::new("/user").is_prefix()); + /// ``` + pub fn is_prefix(&self) -> bool { + match &self.pat_type { + PatternType::Prefix(_) => true, + PatternType::Dynamic(re, _) if !re.as_str().ends_with('$') => true, + _ => false, + } + } + + /// Returns the pattern string that generated the resource definition. + /// + /// Returns `None` if definition was constructed with multiple patterns. + /// See [`patterns_iter`][Self::pattern_iter]. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let mut resource = ResourceDef::new("/user/{id}"); + /// assert_eq!(resource.pattern().unwrap(), "/user/{id}"); + /// + /// let mut resource = ResourceDef::new(["/profile", "/user/{id}"]); + /// assert!(resource.pattern().is_none()); + pub fn pattern(&self) -> Option<&str> { + match &self.patterns { + Patterns::Single(pattern) => Some(pattern.as_str()), + Patterns::List(_) => None, + } + } + + /// Returns iterator of pattern strings that generated the resource definition. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let mut resource = ResourceDef::new("/root"); + /// let mut iter = resource.pattern_iter(); + /// assert_eq!(iter.next().unwrap(), "/root"); + /// assert!(iter.next().is_none()); + /// + /// let mut resource = ResourceDef::new(["/root", "/backup"]); + /// let mut iter = resource.pattern_iter(); + /// assert_eq!(iter.next().unwrap(), "/root"); + /// assert_eq!(iter.next().unwrap(), "/backup"); + /// assert!(iter.next().is_none()); + pub fn pattern_iter(&self) -> impl Iterator { + struct PatternIter<'a> { + patterns: &'a Patterns, + list_idx: usize, + done: bool, + } + + impl<'a> Iterator for PatternIter<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option { + match &self.patterns { + Patterns::Single(pattern) => { + if self.done { + return None; + } + + self.done = true; + Some(pattern.as_str()) + } + Patterns::List(patterns) if patterns.is_empty() => None, + Patterns::List(patterns) => match patterns.get(self.list_idx) { + Some(pattern) => { + self.list_idx += 1; + Some(pattern.as_str()) + } + None => { + // fast path future call + self.done = true; + None + } + }, + } + } + + fn size_hint(&self) -> (usize, Option) { + match &self.patterns { + Patterns::Single(_) => (1, Some(1)), + Patterns::List(patterns) => (patterns.len(), Some(patterns.len())), + } + } + } + + PatternIter { + patterns: &self.patterns, + list_idx: 0, + done: false, + } + } + + /// Joins two resources. + /// + /// Resulting resource is prefix if `other` is prefix. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let joined = ResourceDef::prefix("/root").join(&ResourceDef::prefix("/seg")); + /// assert_eq!(joined, ResourceDef::prefix("/root/seg")); + /// ``` + pub fn join(&self, other: &ResourceDef) -> ResourceDef { + let patterns = self + .pattern_iter() + .flat_map(move |this| other.pattern_iter().map(move |other| (this, other))) + .map(|(this, other)| [this, other].join("")) + .collect::>(); + + match patterns.len() { + 1 => ResourceDef::from_single_pattern(&patterns[0], other.is_prefix()), + _ => ResourceDef::new(patterns), + } + } + + /// Returns `true` if `path` matches this resource. + /// + /// The behavior of this method depends on how the `ResourceDef` was constructed. For example, + /// static resources will not be able to match as many paths as dynamic and prefix resources. + /// See [`ResourceDef`] struct docs for details on resource definition types. + /// + /// This method will always agree with [`find_match`][Self::find_match] on whether the path + /// matches or not. + /// + /// # Examples + /// ``` + /// use actix_router::ResourceDef; + /// + /// // static resource + /// let resource = ResourceDef::new("/user"); + /// assert!(resource.is_match("/user")); + /// assert!(!resource.is_match("/users")); + /// assert!(!resource.is_match("/user/123")); + /// assert!(!resource.is_match("/foo")); + /// + /// // dynamic resource + /// let resource = ResourceDef::new("/user/{user_id}"); + /// assert!(resource.is_match("/user/123")); + /// assert!(!resource.is_match("/user/123/stars")); + /// + /// // prefix resource + /// let resource = ResourceDef::prefix("/root"); + /// assert!(resource.is_match("/root")); + /// assert!(resource.is_match("/root/leaf")); + /// assert!(!resource.is_match("/roots")); + /// + /// // more examples are shown in the `ResourceDef` struct docs + /// ``` + #[inline] + pub fn is_match(&self, path: &str) -> bool { + profile_method!(is_match); + + // this function could be expressed as: + // `self.find_match(path).is_some()` + // but this skips some checks and uses potentially faster regex methods + + match self.pat_type { + PatternType::Static(ref s) => s == path, + + PatternType::Prefix(ref prefix) if prefix == path => true, + PatternType::Prefix(ref prefix) => is_strict_prefix(prefix, path), + + // dynamic prefix + PatternType::Dynamic(ref re, _) if !re.as_str().ends_with('$') => { + match re.find(path) { + // prefix matches exactly + Some(m) if m.end() == path.len() => true, + + // prefix matches part + Some(m) => is_strict_prefix(m.as_str(), path), + + // prefix does not match + None => false, + } + } + + PatternType::Dynamic(ref re, _) => re.is_match(path), + PatternType::DynamicSet(ref re, _) => re.is_match(path), + } + } + + /// Tries to match `path` to this resource, returning the position in the path where the + /// match ends. + /// + /// This method will always agree with [`is_match`][Self::is_match] on whether the path matches + /// or not. + /// + /// # Examples + /// ``` + /// use actix_router::ResourceDef; + /// + /// // static resource + /// let resource = ResourceDef::new("/user"); + /// assert_eq!(resource.find_match("/user"), Some(5)); + /// assert!(resource.find_match("/user/").is_none()); + /// assert!(resource.find_match("/user/123").is_none()); + /// assert!(resource.find_match("/foo").is_none()); + /// + /// // constant prefix resource + /// let resource = ResourceDef::prefix("/user"); + /// assert_eq!(resource.find_match("/user"), Some(5)); + /// assert_eq!(resource.find_match("/user/"), Some(5)); + /// assert_eq!(resource.find_match("/user/123"), Some(5)); + /// + /// // dynamic prefix resource + /// let resource = ResourceDef::prefix("/user/{id}"); + /// assert_eq!(resource.find_match("/user/123"), Some(9)); + /// assert_eq!(resource.find_match("/user/1234/"), Some(10)); + /// assert_eq!(resource.find_match("/user/12345/stars"), Some(11)); + /// assert!(resource.find_match("/user/").is_none()); + /// + /// // multi-pattern resource + /// let resource = ResourceDef::new(["/user/{id}", "/profile/{id}"]); + /// assert_eq!(resource.find_match("/user/123"), Some(9)); + /// assert_eq!(resource.find_match("/profile/1234"), Some(13)); + /// ``` + pub fn find_match(&self, path: &str) -> Option { + profile_method!(find_match); + + match &self.pat_type { + PatternType::Static(segment) if path == segment => Some(segment.len()), + PatternType::Static(_) => None, + + PatternType::Prefix(prefix) if path == prefix => Some(prefix.len()), + PatternType::Prefix(prefix) if is_strict_prefix(prefix, path) => Some(prefix.len()), + PatternType::Prefix(_) => None, + + // dynamic prefix + PatternType::Dynamic(ref re, _) if !re.as_str().ends_with('$') => { + match re.find(path) { + // prefix matches exactly + Some(m) if m.end() == path.len() => Some(m.end()), + + // prefix matches part + Some(m) if is_strict_prefix(m.as_str(), path) => Some(m.end()), + + // prefix does not match + _ => None, + } + } + + PatternType::Dynamic(re, _) => re.find(path).map(|m| m.end()), + + PatternType::DynamicSet(re, params) => { + let idx = re.matches(path).into_iter().next()?; + let (ref pattern, _) = params[idx]; + pattern.find(path).map(|m| m.end()) + } + } + } + + /// Collects dynamic segment values into `path`. + /// + /// Returns `true` if `path` matches this resource. + /// + /// # Examples + /// ``` + /// use actix_router::{Path, ResourceDef}; + /// + /// let resource = ResourceDef::prefix("/user/{id}"); + /// let mut path = Path::new("/user/123/stars"); + /// assert!(resource.capture_match_info(&mut path)); + /// assert_eq!(path.get("id").unwrap(), "123"); + /// assert_eq!(path.unprocessed(), "/stars"); + /// + /// let resource = ResourceDef::new("/blob/{path}*"); + /// let mut path = Path::new("/blob/HEAD/Cargo.toml"); + /// assert!(resource.capture_match_info(&mut path)); + /// assert_eq!(path.get("path").unwrap(), "HEAD/Cargo.toml"); + /// assert_eq!(path.unprocessed(), ""); + /// ``` + pub fn capture_match_info(&self, path: &mut Path) -> bool { + profile_method!(capture_match_info); + self.capture_match_info_fn(path, |_, _| true, ()) + } + + /// Collects dynamic segment values into `resource` after matching paths and executing + /// check function. + /// + /// The check function is given a reference to the passed resource and optional arbitrary data. + /// This is useful if you want to conditionally match on some non-path related aspect of the + /// resource type. + /// + /// Returns `true` if resource path matches this resource definition _and_ satisfies the + /// given check function. + /// + /// # Examples + /// ``` + /// use actix_router::{Path, ResourceDef}; + /// + /// fn try_match(resource: &ResourceDef, path: &mut Path<&str>) -> bool { + /// let admin_allowed = std::env::var("ADMIN_ALLOWED").ok(); + /// + /// resource.capture_match_info_fn( + /// path, + /// // when env var is not set, reject when path contains "admin" + /// |res, admin_allowed| !res.path().contains("admin"), + /// &admin_allowed + /// ) + /// } + /// + /// let resource = ResourceDef::prefix("/user/{id}"); + /// + /// // path matches; segment values are collected into path + /// let mut path = Path::new("/user/james/stars"); + /// assert!(try_match(&resource, &mut path)); + /// assert_eq!(path.get("id").unwrap(), "james"); + /// assert_eq!(path.unprocessed(), "/stars"); + /// + /// // path matches but fails check function; no segments are collected + /// let mut path = Path::new("/user/admin/stars"); + /// assert!(!try_match(&resource, &mut path)); + /// assert_eq!(path.unprocessed(), "/user/admin/stars"); + /// ``` + pub fn capture_match_info_fn( + &self, + resource: &mut R, + check_fn: F, + user_data: U, + ) -> bool + where + R: Resource, + T: ResourcePath, + F: FnOnce(&R, U) -> bool, + { + profile_method!(capture_match_info_fn); + + let mut segments = <[PathItem; MAX_DYNAMIC_SEGMENTS]>::default(); + let path = resource.resource_path(); + let path_str = path.path(); + + let (matched_len, matched_vars) = match &self.pat_type { + PatternType::Static(_) | PatternType::Prefix(_) => { + profile_section!(pattern_static_or_prefix); + + match self.find_match(path_str) { + Some(len) => (len, None), + None => return false, + } + } + + PatternType::Dynamic(re, names) => { + profile_section!(pattern_dynamic); + + let captures = { + profile_section!(pattern_dynamic_regex_exec); + + match re.captures(path.path()) { + Some(captures) => captures, + _ => return false, + } + }; + + { + profile_section!(pattern_dynamic_extract_captures); + + for (no, name) in names.iter().enumerate() { + if let Some(m) = captures.name(&name) { + segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); + } else { + log::error!( + "Dynamic path match but not all segments found: {}", + name + ); + return false; + } + } + }; + + (captures[0].len(), Some(names)) + } + + PatternType::DynamicSet(re, params) => { + profile_section!(pattern_dynamic_set); + + let path = path.path(); + let (pattern, names) = match re.matches(path).into_iter().next() { + Some(idx) => ¶ms[idx], + _ => return false, + }; + + let captures = match pattern.captures(path.path()) { + Some(captures) => captures, + _ => return false, + }; + + for (no, name) in names.iter().enumerate() { + if let Some(m) = captures.name(&name) { + segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); + } else { + log::error!("Dynamic path match but not all segments found: {}", name); + return false; + } + } + + (captures[0].len(), Some(names)) + } + }; + + if !check_fn(resource, user_data) { + return false; + } + + // Modify `path` to skip matched part and store matched segments + let path = resource.resource_path(); + + if let Some(vars) = matched_vars { + for i in 0..vars.len() { + path.add(vars[i], mem::take(&mut segments[i])); + } + } + + path.skip(matched_len as u16); + + true + } + + /// Assembles resource path using a closure that maps variable segment names to values. + fn build_resource_path(&self, path: &mut String, mut vars: F) -> bool + where + F: FnMut(&str) -> Option, + I: AsRef, + { + for el in match self.segments { + Some(ref segments) => segments, + None => return false, + } { + match *el { + PatternSegment::Const(ref val) => path.push_str(val), + PatternSegment::Var(ref name) => match vars(name) { + Some(val) => path.push_str(val.as_ref()), + _ => return false, + }, + } + } + + true + } + + /// Assembles full resource path from iterator of dynamic segment values. + /// + /// Returns `true` on success. + /// + /// Resource paths can not be built from multi-pattern resources; this call will always return + /// false and will not add anything to the string buffer. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let mut s = String::new(); + /// let resource = ResourceDef::new("/user/{id}/post/{title}"); + /// + /// assert!(resource.resource_path_from_iter(&mut s, &["123", "my-post"])); + /// assert_eq!(s, "/user/123/post/my-post"); + /// ``` + pub fn resource_path_from_iter(&self, path: &mut String, values: I) -> bool + where + I: IntoIterator, + I::Item: AsRef, + { + profile_method!(resource_path_from_iter); + let mut iter = values.into_iter(); + self.build_resource_path(path, |_| iter.next()) + } + + /// Assembles resource path from map of dynamic segment values. + /// + /// Returns `true` on success. + /// + /// Resource paths can not be built from multi-pattern resources; this call will always return + /// false and will not add anything to the string buffer. + /// + /// # Examples + /// ``` + /// # use std::collections::HashMap; + /// # use actix_router::ResourceDef; + /// let mut s = String::new(); + /// let resource = ResourceDef::new("/user/{id}/post/{title}"); + /// + /// let mut map = HashMap::new(); + /// map.insert("id", "123"); + /// map.insert("title", "my-post"); + /// + /// assert!(resource.resource_path_from_map(&mut s, &map)); + /// assert_eq!(s, "/user/123/post/my-post"); + /// ``` + pub fn resource_path_from_map( + &self, + path: &mut String, + values: &HashMap, + ) -> bool + where + K: Borrow + Eq + Hash, + V: AsRef, + S: BuildHasher, + { + profile_method!(resource_path_from_map); + self.build_resource_path(path, |name| values.get(name).map(AsRef::::as_ref)) + } + + /// Parse path pattern and create a new instance. + fn from_single_pattern(pattern: &str, is_prefix: bool) -> Self { + profile_method!(from_single_pattern); + + let pattern = pattern.to_owned(); + let (pat_type, segments) = ResourceDef::parse(&pattern, is_prefix, false); + + ResourceDef { + id: 0, + name: None, + patterns: Patterns::Single(pattern), + pat_type, + segments: Some(segments), + } + } + + /// Parses a dynamic segment definition from a pattern. + /// + /// The returned tuple includes: + /// - the segment descriptor, either `Var` or `Tail` + /// - the segment's regex to check values against + /// - the remaining, unprocessed string slice + /// - whether the parsed parameter represents a tail pattern + /// + /// # Panics + /// Panics if given patterns does not contain a dynamic segment. + fn parse_param(pattern: &str) -> (PatternSegment, String, &str, bool) { + profile_method!(parse_param); + + const DEFAULT_PATTERN: &str = "[^/]+"; + const DEFAULT_PATTERN_TAIL: &str = ".*"; + + let mut params_nesting = 0usize; + let close_idx = pattern + .find(|c| match c { + '{' => { + params_nesting += 1; + false + } + '}' => { + params_nesting -= 1; + params_nesting == 0 + } + _ => false, + }) + .unwrap_or_else(|| { + panic!(r#"path "{}" contains malformed dynamic segment"#, pattern) + }); + + let (mut param, mut unprocessed) = pattern.split_at(close_idx + 1); + + // remove outer curly brackets + param = ¶m[1..param.len() - 1]; + + let tail = unprocessed == "*"; + + let (name, pattern) = match param.find(':') { + Some(idx) => { + if tail { + panic!("custom regex is not supported for tail match"); + } + + let (name, pattern) = param.split_at(idx); + (name, &pattern[1..]) + } + None => ( + param, + if tail { + unprocessed = &unprocessed[1..]; + DEFAULT_PATTERN_TAIL + } else { + DEFAULT_PATTERN + }, + ), + }; + + let segment = PatternSegment::Var(name.to_string()); + let regex = format!(r"(?P<{}>{})", &name, &pattern); + + (segment, regex, unprocessed, tail) + } + + /// Parse `pattern` using `is_prefix` and `force_dynamic` flags. + /// + /// Parameters: + /// - `is_prefix`: Use `true` if `pattern` should be treated as a prefix; i.e., a conforming + /// path will be a match even if it has parts remaining to process + /// - `force_dynamic`: Use `true` to disallow the return of static and prefix segments. + /// + /// The returned tuple includes: + /// - the pattern type detected, either `Static`, `Prefix`, or `Dynamic` + /// - a list of segment descriptors from the pattern + fn parse( + pattern: &str, + is_prefix: bool, + force_dynamic: bool, + ) -> (PatternType, Vec) { + profile_method!(parse); + + let mut unprocessed = pattern; + + if !force_dynamic && unprocessed.find('{').is_none() && !unprocessed.ends_with('*') { + // pattern is static + + let tp = if is_prefix { + PatternType::Prefix(unprocessed.to_owned()) + } else { + PatternType::Static(unprocessed.to_owned()) + }; + + return (tp, vec![PatternSegment::Const(unprocessed.to_owned())]); + } + + let mut segments = Vec::new(); + let mut re = format!("{}^", REGEX_FLAGS); + let mut dyn_segment_count = 0; + let mut has_tail_segment = false; + + while let Some(idx) = unprocessed.find('{') { + let (prefix, rem) = unprocessed.split_at(idx); + + segments.push(PatternSegment::Const(prefix.to_owned())); + re.push_str(&escape(prefix)); + + let (param_pattern, re_part, rem, tail) = Self::parse_param(rem); + + if tail { + has_tail_segment = true; + } + + segments.push(param_pattern); + re.push_str(&re_part); + + unprocessed = rem; + dyn_segment_count += 1; + } + + if is_prefix && has_tail_segment { + // tail segments in prefixes have no defined semantics + + #[cfg(not(test))] + log::warn!( + "Prefix resources should not have tail segments. \ + Use `ResourceDef::new` constructor. \ + This may become a panic in the future." + ); + + // panic in tests to make this case detectable + #[cfg(test)] + panic!("prefix resource definitions should not have tail segments"); + } + + if unprocessed.ends_with('*') { + // unnamed tail segment + + #[cfg(not(test))] + log::warn!( + "Tail segments must have names. \ + Consider `.../{{tail}}*`. \ + This may become a panic in the future." + ); + + // panic in tests to make this case detectable + #[cfg(test)] + panic!("tail segments must have names"); + } else if !has_tail_segment && !unprocessed.is_empty() { + // prevent `Const("")` element from being added after last dynamic segment + + segments.push(PatternSegment::Const(unprocessed.to_owned())); + re.push_str(&escape(unprocessed)); + } + + if dyn_segment_count > MAX_DYNAMIC_SEGMENTS { + panic!( + "Only {} dynamic segments are allowed, provided: {}", + MAX_DYNAMIC_SEGMENTS, dyn_segment_count + ); + } + + if !is_prefix && !has_tail_segment { + re.push('$'); + } + + let re = match Regex::new(&re) { + Ok(re) => re, + Err(err) => panic!("Wrong path pattern: \"{}\" {}", pattern, err), + }; + + // `Bok::leak(Box::new(name))` is an intentional memory leak. In typical applications the + // routing table is only constructed once (per worker) so leak is bounded. If you are + // constructing `ResourceDef`s more than once in your application's lifecycle you would + // expect a linear increase in leaked memory over time. + let names = re + .capture_names() + .filter_map(|name| name.map(|name| Box::leak(Box::new(name.to_owned())).as_str())) + .collect(); + + (PatternType::Dynamic(re, names), segments) + } +} + +impl Eq for ResourceDef {} + +impl PartialEq for ResourceDef { + fn eq(&self, other: &ResourceDef) -> bool { + self.patterns == other.patterns + && match &self.pat_type { + PatternType::Static(_) => matches!(&other.pat_type, PatternType::Static(_)), + PatternType::Prefix(_) => matches!(&other.pat_type, PatternType::Prefix(_)), + PatternType::Dynamic(re, _) => match &other.pat_type { + PatternType::Dynamic(other_re, _) => re.as_str() == other_re.as_str(), + _ => false, + }, + PatternType::DynamicSet(_, _) => { + matches!(&other.pat_type, PatternType::DynamicSet(..)) + } + } + } +} + +impl Hash for ResourceDef { + fn hash(&self, state: &mut H) { + self.patterns.hash(state); + } +} + +impl<'a> From<&'a str> for ResourceDef { + fn from(path: &'a str) -> ResourceDef { + ResourceDef::new(path) + } +} + +impl From for ResourceDef { + fn from(path: String) -> ResourceDef { + ResourceDef::new(path) + } +} + +pub(crate) fn insert_slash(path: &str) -> Cow<'_, str> { + profile_fn!(insert_slash); + + if !path.is_empty() && !path.starts_with('/') { + let mut new_path = String::with_capacity(path.len() + 1); + new_path.push('/'); + new_path.push_str(path); + Cow::Owned(new_path) + } else { + Cow::Borrowed(path) + } +} + +/// Returns true if `prefix` acts as a proper prefix (i.e., separated by a slash) in `path`. +/// +/// The `strict` refers to the fact that this will return `false` if `prefix == path`. +fn is_strict_prefix(prefix: &str, path: &str) -> bool { + path.starts_with(prefix) && (prefix.ends_with('/') || path[prefix.len()..].starts_with('/')) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn equivalence() { + assert_eq!( + ResourceDef::root_prefix("/root"), + ResourceDef::prefix("/root") + ); + assert_eq!( + ResourceDef::root_prefix("root"), + ResourceDef::prefix("/root") + ); + assert_eq!( + ResourceDef::root_prefix("/{id}"), + ResourceDef::prefix("/{id}") + ); + assert_eq!( + ResourceDef::root_prefix("{id}"), + ResourceDef::prefix("/{id}") + ); + + assert_eq!(ResourceDef::new("/"), ResourceDef::new(["/"])); + assert_eq!(ResourceDef::new("/"), ResourceDef::new(vec!["/"])); + + assert_ne!(ResourceDef::new(""), ResourceDef::prefix("")); + assert_ne!(ResourceDef::new("/"), ResourceDef::prefix("/")); + assert_ne!(ResourceDef::new("/{id}"), ResourceDef::prefix("/{id}")); + } + + #[test] + fn parse_static() { + let re = ResourceDef::new(""); + + assert!(!re.is_prefix()); + + assert!(re.is_match("")); + assert!(!re.is_match("/")); + assert_eq!(re.find_match(""), Some(0)); + assert_eq!(re.find_match("/"), None); + + let re = ResourceDef::new("/"); + assert!(re.is_match("/")); + assert!(!re.is_match("")); + assert!(!re.is_match("/foo")); + + let re = ResourceDef::new("/name"); + assert!(re.is_match("/name")); + assert!(!re.is_match("/name1")); + assert!(!re.is_match("/name/")); + assert!(!re.is_match("/name~")); + + let mut path = Path::new("/name"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.unprocessed(), ""); + + assert_eq!(re.find_match("/name"), Some(5)); + assert_eq!(re.find_match("/name1"), None); + assert_eq!(re.find_match("/name/"), None); + assert_eq!(re.find_match("/name~"), None); + + let re = ResourceDef::new("/name/"); + assert!(re.is_match("/name/")); + assert!(!re.is_match("/name")); + assert!(!re.is_match("/name/gs")); + + let re = ResourceDef::new("/user/profile"); + assert!(re.is_match("/user/profile")); + assert!(!re.is_match("/user/profile/profile")); + + let mut path = Path::new("/user/profile"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.unprocessed(), ""); + } + + #[test] + fn parse_param() { + let re = ResourceDef::new("/user/{id}"); + assert!(re.is_match("/user/profile")); + assert!(re.is_match("/user/2345")); + assert!(!re.is_match("/user/2345/")); + assert!(!re.is_match("/user/2345/sdg")); + + let mut path = Path::new("/user/profile"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "profile"); + assert_eq!(path.unprocessed(), ""); + + let mut path = Path::new("/user/1245125"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "1245125"); + assert_eq!(path.unprocessed(), ""); + + let re = ResourceDef::new("/v{version}/resource/{id}"); + assert!(re.is_match("/v1/resource/320120")); + assert!(!re.is_match("/v/resource/1")); + assert!(!re.is_match("/resource")); + + let mut path = Path::new("/v151/resource/adage32"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("version").unwrap(), "151"); + assert_eq!(path.get("id").unwrap(), "adage32"); + assert_eq!(path.unprocessed(), ""); + + let re = ResourceDef::new("/{id:[[:digit:]]{6}}"); + assert!(re.is_match("/012345")); + assert!(!re.is_match("/012")); + assert!(!re.is_match("/01234567")); + assert!(!re.is_match("/XXXXXX")); + + let mut path = Path::new("/012345"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "012345"); + assert_eq!(path.unprocessed(), ""); + } + + #[allow(clippy::cognitive_complexity)] + #[test] + fn dynamic_set() { + let re = ResourceDef::new(vec![ + "/user/{id}", + "/v{version}/resource/{id}", + "/{id:[[:digit:]]{6}}", + "/static", + ]); + assert!(re.is_match("/user/profile")); + assert!(re.is_match("/user/2345")); + assert!(!re.is_match("/user/2345/")); + assert!(!re.is_match("/user/2345/sdg")); + + let mut path = Path::new("/user/profile"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "profile"); + assert_eq!(path.unprocessed(), ""); + + let mut path = Path::new("/user/1245125"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "1245125"); + assert_eq!(path.unprocessed(), ""); + + assert!(re.is_match("/v1/resource/320120")); + assert!(!re.is_match("/v/resource/1")); + assert!(!re.is_match("/resource")); + + let mut path = Path::new("/v151/resource/adage32"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("version").unwrap(), "151"); + assert_eq!(path.get("id").unwrap(), "adage32"); + + assert!(re.is_match("/012345")); + assert!(!re.is_match("/012")); + assert!(!re.is_match("/01234567")); + assert!(!re.is_match("/XXXXXX")); + + assert!(re.is_match("/static")); + assert!(!re.is_match("/a/static")); + assert!(!re.is_match("/static/a")); + + let mut path = Path::new("/012345"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "012345"); + + let re = ResourceDef::new([ + "/user/{id}", + "/v{version}/resource/{id}", + "/{id:[[:digit:]]{6}}", + ]); + assert!(re.is_match("/user/profile")); + assert!(re.is_match("/user/2345")); + assert!(!re.is_match("/user/2345/")); + assert!(!re.is_match("/user/2345/sdg")); + + let re = ResourceDef::new([ + "/user/{id}".to_string(), + "/v{version}/resource/{id}".to_string(), + "/{id:[[:digit:]]{6}}".to_string(), + ]); + assert!(re.is_match("/user/profile")); + assert!(re.is_match("/user/2345")); + assert!(!re.is_match("/user/2345/")); + assert!(!re.is_match("/user/2345/sdg")); + } + + #[test] + fn parse_tail() { + let re = ResourceDef::new("/user/-{id}*"); + + let mut path = Path::new("/user/-profile"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "profile"); + + let mut path = Path::new("/user/-2345"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "2345"); + + let mut path = Path::new("/user/-2345/"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "2345/"); + + let mut path = Path::new("/user/-2345/sdg"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "2345/sdg"); + } + + #[test] + fn static_tail() { + let re = ResourceDef::new("/user{tail}*"); + assert!(re.is_match("/users")); + assert!(re.is_match("/user-foo")); + assert!(re.is_match("/user/profile")); + assert!(re.is_match("/user/2345")); + assert!(re.is_match("/user/2345/")); + assert!(re.is_match("/user/2345/sdg")); + assert!(!re.is_match("/foo/profile")); + + let re = ResourceDef::new("/user/{tail}*"); + assert!(re.is_match("/user/profile")); + assert!(re.is_match("/user/2345")); + assert!(re.is_match("/user/2345/")); + assert!(re.is_match("/user/2345/sdg")); + assert!(!re.is_match("/foo/profile")); + } + + #[test] + fn dynamic_tail() { + let re = ResourceDef::new("/user/{id}/{tail}*"); + assert!(!re.is_match("/user/2345")); + let mut path = Path::new("/user/2345/sdg"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "2345"); + assert_eq!(path.get("tail").unwrap(), "sdg"); + assert_eq!(path.unprocessed(), ""); + } + + #[test] + fn newline_patterns_and_paths() { + let re = ResourceDef::new("/user/a\nb"); + assert!(re.is_match("/user/a\nb")); + assert!(!re.is_match("/user/a\nb/profile")); + + let re = ResourceDef::new("/a{x}b/test/a{y}b"); + let mut path = Path::new("/a\nb/test/a\nb"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("x").unwrap(), "\n"); + assert_eq!(path.get("y").unwrap(), "\n"); + + let re = ResourceDef::new("/user/{tail}*"); + assert!(re.is_match("/user/a\nb/")); + + let re = ResourceDef::new("/user/{id}*"); + let mut path = Path::new("/user/a\nb/a\nb"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "a\nb/a\nb"); + + let re = ResourceDef::new("/user/{id:.*}"); + let mut path = Path::new("/user/a\nb/a\nb"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "a\nb/a\nb"); + } + + #[cfg(feature = "http")] + #[test] + fn parse_urlencoded_param() { + use std::convert::TryFrom; + + let re = ResourceDef::new("/user/{id}/test"); + + let mut path = Path::new("/user/2345/test"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "2345"); + + let mut path = Path::new("/user/qwe%25/test"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "qwe%25"); + + let uri = http::Uri::try_from("/user/qwe%25/test").unwrap(); + let mut path = Path::new(uri); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "qwe%25"); + } + + #[test] + fn prefix_static() { + let re = ResourceDef::prefix("/name"); + + assert!(re.is_prefix()); + + assert!(re.is_match("/name")); + assert!(re.is_match("/name/")); + assert!(re.is_match("/name/test/test")); + assert!(!re.is_match("/name1")); + assert!(!re.is_match("/name~")); + + let mut path = Path::new("/name"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.unprocessed(), ""); + + let mut path = Path::new("/name/test"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.unprocessed(), "/test"); + + assert_eq!(re.find_match("/name"), Some(5)); + assert_eq!(re.find_match("/name/"), Some(5)); + assert_eq!(re.find_match("/name/test/test"), Some(5)); + assert_eq!(re.find_match("/name1"), None); + assert_eq!(re.find_match("/name~"), None); + + let re = ResourceDef::prefix("/name/"); + assert!(re.is_match("/name/")); + assert!(re.is_match("/name/gs")); + assert!(!re.is_match("/name")); + + let mut path = Path::new("/name/gs"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.unprocessed(), "gs"); + + let re = ResourceDef::root_prefix("name/"); + assert!(re.is_match("/name/")); + assert!(re.is_match("/name/gs")); + assert!(!re.is_match("/name")); + + let mut path = Path::new("/name/gs"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.unprocessed(), "gs"); + } + + #[test] + fn prefix_dynamic() { + let re = ResourceDef::prefix("/{name}/"); + + assert!(re.is_prefix()); + + assert!(re.is_match("/name/")); + assert!(re.is_match("/name/gs")); + assert!(!re.is_match("/name")); + + assert_eq!(re.find_match("/name/"), Some(6)); + assert_eq!(re.find_match("/name/gs"), Some(6)); + assert_eq!(re.find_match("/name"), None); + + let mut path = Path::new("/test2/"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(&path["name"], "test2"); + assert_eq!(&path[0], "test2"); + assert_eq!(path.unprocessed(), ""); + + let mut path = Path::new("/test2/subpath1/subpath2/index.html"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(&path["name"], "test2"); + assert_eq!(&path[0], "test2"); + assert_eq!(path.unprocessed(), "subpath1/subpath2/index.html"); + + let resource = ResourceDef::prefix("/user"); + // input string shorter than prefix + assert!(resource.find_match("/foo").is_none()); + } + + #[test] + fn build_path_list() { + let mut s = String::new(); + let resource = ResourceDef::new("/user/{item1}/test"); + assert!(resource.resource_path_from_iter(&mut s, &mut (&["user1"]).iter())); + assert_eq!(s, "/user/user1/test"); + + let mut s = String::new(); + let resource = ResourceDef::new("/user/{item1}/{item2}/test"); + assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter())); + assert_eq!(s, "/user/item/item2/test"); + + let mut s = String::new(); + let resource = ResourceDef::new("/user/{item1}/{item2}"); + assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter())); + assert_eq!(s, "/user/item/item2"); + + let mut s = String::new(); + let resource = ResourceDef::new("/user/{item1}/{item2}/"); + assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter())); + assert_eq!(s, "/user/item/item2/"); + + let mut s = String::new(); + assert!(!resource.resource_path_from_iter(&mut s, &mut (&["item"]).iter())); + + let mut s = String::new(); + assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter())); + assert_eq!(s, "/user/item/item2/"); + assert!(!resource.resource_path_from_iter(&mut s, &mut (&["item"]).iter())); + + let mut s = String::new(); + assert!(resource.resource_path_from_iter(&mut s, &mut vec!["item", "item2"].iter())); + assert_eq!(s, "/user/item/item2/"); + } + + #[test] + fn multi_pattern_cannot_build_path() { + let resource = ResourceDef::new(["/user/{id}", "/profile/{id}"]); + let mut s = String::new(); + assert!(!resource.resource_path_from_iter(&mut s, &mut ["123"].iter())); + } + + #[test] + fn multi_pattern_capture_segment_values() { + let resource = ResourceDef::new(["/user/{id}", "/profile/{id}"]); + + let mut path = Path::new("/user/123"); + assert!(resource.capture_match_info(&mut path)); + assert!(path.get("id").is_some()); + + let mut path = Path::new("/profile/123"); + assert!(resource.capture_match_info(&mut path)); + assert!(path.get("id").is_some()); + + let resource = ResourceDef::new(["/user/{id}", "/profile/{uid}"]); + + let mut path = Path::new("/user/123"); + assert!(resource.capture_match_info(&mut path)); + assert!(path.get("id").is_some()); + assert!(path.get("uid").is_none()); + + let mut path = Path::new("/profile/123"); + assert!(resource.capture_match_info(&mut path)); + assert!(path.get("id").is_none()); + assert!(path.get("uid").is_some()); + } + + #[test] + fn dynamic_prefix_proper_segmentation() { + let resource = ResourceDef::prefix(r"/id/{id:\d{3}}"); + + assert!(resource.is_match("/id/123")); + assert!(resource.is_match("/id/123/foo")); + assert!(!resource.is_match("/id/1234")); + assert!(!resource.is_match("/id/123a")); + + assert_eq!(resource.find_match("/id/123"), Some(7)); + assert_eq!(resource.find_match("/id/123/foo"), Some(7)); + assert_eq!(resource.find_match("/id/1234"), None); + assert_eq!(resource.find_match("/id/123a"), None); + } + + #[test] + fn build_path_map() { + let resource = ResourceDef::new("/user/{item1}/{item2}/"); + + let mut map = HashMap::new(); + map.insert("item1", "item"); + + let mut s = String::new(); + assert!(!resource.resource_path_from_map(&mut s, &map)); + + map.insert("item2", "item2"); + + let mut s = String::new(); + assert!(resource.resource_path_from_map(&mut s, &map)); + assert_eq!(s, "/user/item/item2/"); + } + + #[test] + fn build_path_tail() { + let resource = ResourceDef::new("/user/{item1}*"); + + let mut s = String::new(); + assert!(!resource.resource_path_from_iter(&mut s, &mut (&[""; 0]).iter())); + + let mut s = String::new(); + assert!(resource.resource_path_from_iter(&mut s, &mut (&["user1"]).iter())); + assert_eq!(s, "/user/user1"); + + let mut s = String::new(); + let mut map = HashMap::new(); + map.insert("item1", "item"); + assert!(resource.resource_path_from_map(&mut s, &map)); + assert_eq!(s, "/user/item"); + } + + #[test] + fn consistent_match_length() { + let result = Some(5); + + let re = ResourceDef::prefix("/abc/"); + assert_eq!(re.find_match("/abc/def"), result); + + let re = ResourceDef::prefix("/{id}/"); + assert_eq!(re.find_match("/abc/def"), result); + } + + #[test] + fn join() { + // test joined defs match the same paths as each component separately + + fn seq_find_match(re1: &ResourceDef, re2: &ResourceDef, path: &str) -> Option { + let len1 = re1.find_match(path)?; + let len2 = re2.find_match(&path[len1..])?; + Some(len1 + len2) + } + + macro_rules! join_test { + ($pat1:expr, $pat2:expr => $($test:expr),+) => {{ + let pat1 = $pat1; + let pat2 = $pat2; + $({ + let _path = $test; + let (re1, re2) = (ResourceDef::prefix(pat1), ResourceDef::new(pat2)); + let _seq = seq_find_match(&re1, &re2, _path); + let _join = re1.join(&re2).find_match(_path); + assert_eq!( + _seq, _join, + "patterns: prefix {:?}, {:?}; mismatch on \"{}\"; seq={:?}; join={:?}", + pat1, pat2, _path, _seq, _join + ); + assert!(!re1.join(&re2).is_prefix()); + + let (re1, re2) = (ResourceDef::prefix(pat1), ResourceDef::prefix(pat2)); + let _seq = seq_find_match(&re1, &re2, _path); + let _join = re1.join(&re2).find_match(_path); + assert_eq!( + _seq, _join, + "patterns: prefix {:?}, prefix {:?}; mismatch on \"{}\"; seq={:?}; join={:?}", + pat1, pat2, _path, _seq, _join + ); + assert!(re1.join(&re2).is_prefix()); + })+ + }} + } + + join_test!("", "" => "", "/hello", "/"); + join_test!("/user", "" => "", "/user", "/user/123", "/user11", "user", "user/123"); + join_test!("", "/user"=> "", "/user", "foo", "/user11", "user", "user/123"); + join_test!("/user", "/xx"=> "", "", "/", "/user", "/xx", "/userxx", "/user/xx"); + } + + #[test] + fn match_methods_agree() { + macro_rules! match_methods_agree { + ($pat:expr => $($test:expr),+) => {{ + match_methods_agree!(finish $pat, ResourceDef::new($pat), $($test),+); + }}; + (prefix $pat:expr => $($test:expr),+) => {{ + match_methods_agree!(finish $pat, ResourceDef::prefix($pat), $($test),+); + }}; + (finish $pat:expr, $re:expr, $($test:expr),+) => {{ + let re = $re; + $({ + let _is = re.is_match($test); + let _find = re.find_match($test).is_some(); + assert_eq!( + _is, _find, + "pattern: {:?}; mismatch on \"{}\"; is={}; find={}", + $pat, $test, _is, _find + ); + })+ + }} + } + + match_methods_agree!("" => "", "/", "/foo"); + match_methods_agree!("/" => "", "/", "/foo"); + match_methods_agree!("/user" => "user", "/user", "/users", "/user/123", "/foo"); + match_methods_agree!("/v{v}" => "v", "/v", "/v1", "/v222", "/foo"); + match_methods_agree!(["/v{v}", "/version/{v}"] => "/v", "/v1", "/version", "/version/1", "/foo"); + + match_methods_agree!("/path{tail}*" => "/path", "/path1", "/path/123"); + match_methods_agree!("/path/{tail}*" => "/path", "/path1", "/path/123"); + + match_methods_agree!(prefix "" => "", "/", "/foo"); + match_methods_agree!(prefix "/user" => "user", "/user", "/users", "/user/123", "/foo"); + match_methods_agree!(prefix r"/id/{id:\d{3}}" => "/id/123", "/id/1234"); + } + + #[test] + #[should_panic] + fn invalid_dynamic_segment_delimiter() { + ResourceDef::new("/user/{username"); + } + + #[test] + #[should_panic] + fn invalid_dynamic_segment_name() { + ResourceDef::new("/user/{}"); + } + + #[test] + #[should_panic] + fn invalid_too_many_dynamic_segments() { + // valid + ResourceDef::new("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}"); + + // panics + ResourceDef::new( + "/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}", + ); + } + + #[test] + #[should_panic] + fn invalid_custom_regex_for_tail() { + ResourceDef::new(r"/{tail:\d+}*"); + } + + #[test] + #[should_panic] + fn invalid_unnamed_tail_segment() { + ResourceDef::new("/*"); + } + + #[test] + #[should_panic] + fn prefix_plus_tail_match_is_allowed() { + ResourceDef::prefix("/user/{id}*"); + } +} diff --git a/actix-router/src/router.rs b/actix-router/src/router.rs new file mode 100644 index 000000000..f5deb8583 --- /dev/null +++ b/actix-router/src/router.rs @@ -0,0 +1,281 @@ +use firestorm::profile_method; + +use crate::{IntoPatterns, Resource, ResourceDef, ResourcePath}; + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct ResourceId(pub u16); + +/// Information about current resource +#[derive(Clone, Debug)] +pub struct ResourceInfo { + resource: ResourceId, +} + +/// Resource router. +// T is the resource itself +// U is any other data needed for routing like method guards +pub struct Router { + routes: Vec<(ResourceDef, T, Option)>, +} + +impl Router { + pub fn build() -> RouterBuilder { + RouterBuilder { + resources: Vec::new(), + } + } + + pub fn recognize(&self, resource: &mut R) -> Option<(&T, ResourceId)> + where + R: Resource

, + P: ResourcePath, + { + profile_method!(recognize); + + for item in self.routes.iter() { + if item.0.capture_match_info(resource.resource_path()) { + return Some((&item.1, ResourceId(item.0.id()))); + } + } + + None + } + + pub fn recognize_mut(&mut self, resource: &mut R) -> Option<(&mut T, ResourceId)> + where + R: Resource

, + P: ResourcePath, + { + profile_method!(recognize_mut); + + for item in self.routes.iter_mut() { + if item.0.capture_match_info(resource.resource_path()) { + return Some((&mut item.1, ResourceId(item.0.id()))); + } + } + + None + } + + pub fn recognize_fn(&self, resource: &mut R, check: F) -> Option<(&T, ResourceId)> + where + F: Fn(&R, &Option) -> bool, + R: Resource

, + P: ResourcePath, + { + profile_method!(recognize_checked); + + for item in self.routes.iter() { + if item.0.capture_match_info_fn(resource, &check, &item.2) { + return Some((&item.1, ResourceId(item.0.id()))); + } + } + + None + } + + pub fn recognize_mut_fn( + &mut self, + resource: &mut R, + check: F, + ) -> Option<(&mut T, ResourceId)> + where + F: Fn(&R, &Option) -> bool, + R: Resource

, + P: ResourcePath, + { + profile_method!(recognize_mut_checked); + + for item in self.routes.iter_mut() { + if item.0.capture_match_info_fn(resource, &check, &item.2) { + return Some((&mut item.1, ResourceId(item.0.id()))); + } + } + + None + } +} + +pub struct RouterBuilder { + resources: Vec<(ResourceDef, T, Option)>, +} + +impl RouterBuilder { + /// Register resource for specified path. + pub fn path( + &mut self, + path: P, + resource: T, + ) -> &mut (ResourceDef, T, Option) { + profile_method!(path); + + self.resources + .push((ResourceDef::new(path), resource, None)); + self.resources.last_mut().unwrap() + } + + /// Register resource for specified path prefix. + pub fn prefix(&mut self, prefix: &str, resource: T) -> &mut (ResourceDef, T, Option) { + profile_method!(prefix); + + self.resources + .push((ResourceDef::prefix(prefix), resource, None)); + self.resources.last_mut().unwrap() + } + + /// Register resource for ResourceDef + pub fn rdef(&mut self, rdef: ResourceDef, resource: T) -> &mut (ResourceDef, T, Option) { + profile_method!(rdef); + + self.resources.push((rdef, resource, None)); + self.resources.last_mut().unwrap() + } + + /// Finish configuration and create router instance. + pub fn finish(self) -> Router { + Router { + routes: self.resources, + } + } +} + +#[cfg(test)] +mod tests { + use crate::path::Path; + use crate::router::{ResourceId, Router}; + + #[allow(clippy::cognitive_complexity)] + #[test] + fn test_recognizer_1() { + let mut router = Router::::build(); + router.path("/name", 10).0.set_id(0); + router.path("/name/{val}", 11).0.set_id(1); + router.path("/name/{val}/index.html", 12).0.set_id(2); + router.path("/file/{file}.{ext}", 13).0.set_id(3); + router.path("/v{val}/{val2}/index.html", 14).0.set_id(4); + router.path("/v/{tail:.*}", 15).0.set_id(5); + router.path("/test2/{test}.html", 16).0.set_id(6); + router.path("/{test}/index.html", 17).0.set_id(7); + let mut router = router.finish(); + + let mut path = Path::new("/unknown"); + assert!(router.recognize_mut(&mut path).is_none()); + + let mut path = Path::new("/name"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 10); + assert_eq!(info, ResourceId(0)); + assert!(path.is_empty()); + + let mut path = Path::new("/name/value"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 11); + assert_eq!(info, ResourceId(1)); + assert_eq!(path.get("val").unwrap(), "value"); + assert_eq!(&path["val"], "value"); + + let mut path = Path::new("/name/value2/index.html"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 12); + assert_eq!(info, ResourceId(2)); + assert_eq!(path.get("val").unwrap(), "value2"); + + let mut path = Path::new("/file/file.gz"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 13); + assert_eq!(info, ResourceId(3)); + assert_eq!(path.get("file").unwrap(), "file"); + assert_eq!(path.get("ext").unwrap(), "gz"); + + let mut path = Path::new("/vtest/ttt/index.html"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 14); + assert_eq!(info, ResourceId(4)); + assert_eq!(path.get("val").unwrap(), "test"); + assert_eq!(path.get("val2").unwrap(), "ttt"); + + let mut path = Path::new("/v/blah-blah/index.html"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 15); + assert_eq!(info, ResourceId(5)); + assert_eq!(path.get("tail").unwrap(), "blah-blah/index.html"); + + let mut path = Path::new("/test2/index.html"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 16); + assert_eq!(info, ResourceId(6)); + assert_eq!(path.get("test").unwrap(), "index"); + + let mut path = Path::new("/bbb/index.html"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 17); + assert_eq!(info, ResourceId(7)); + assert_eq!(path.get("test").unwrap(), "bbb"); + } + + #[test] + fn test_recognizer_2() { + let mut router = Router::::build(); + router.path("/index.json", 10); + router.path("/{source}.json", 11); + let mut router = router.finish(); + + let mut path = Path::new("/index.json"); + let (h, _) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 10); + + let mut path = Path::new("/test.json"); + let (h, _) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 11); + } + + #[test] + fn test_recognizer_with_prefix() { + let mut router = Router::::build(); + router.path("/name", 10).0.set_id(0); + router.path("/name/{val}", 11).0.set_id(1); + let mut router = router.finish(); + + let mut path = Path::new("/name"); + path.skip(5); + assert!(router.recognize_mut(&mut path).is_none()); + + let mut path = Path::new("/test/name"); + path.skip(5); + let (h, _) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 10); + + let mut path = Path::new("/test/name/value"); + path.skip(5); + let (h, id) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 11); + assert_eq!(id, ResourceId(1)); + assert_eq!(path.get("val").unwrap(), "value"); + assert_eq!(&path["val"], "value"); + + // same patterns + let mut router = Router::::build(); + router.path("/name", 10); + router.path("/name/{val}", 11); + let mut router = router.finish(); + + let mut path = Path::new("/name"); + path.skip(6); + assert!(router.recognize_mut(&mut path).is_none()); + + let mut path = Path::new("/test2/name"); + path.skip(6); + let (h, _) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 10); + + let mut path = Path::new("/test2/name-test"); + path.skip(6); + assert!(router.recognize_mut(&mut path).is_none()); + + let mut path = Path::new("/test2/name/ttt"); + path.skip(6); + let (h, _) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 11); + assert_eq!(&path["val"], "ttt"); + } +} diff --git a/actix-router/src/url.rs b/actix-router/src/url.rs new file mode 100644 index 000000000..e08a7171a --- /dev/null +++ b/actix-router/src/url.rs @@ -0,0 +1,288 @@ +use crate::ResourcePath; + +#[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"; + +#[inline] +fn bit_at(array: &[u8], ch: u8) -> bool { + array[(ch >> 3) as usize] & (1 << (ch & 7)) != 0 +} + +#[inline] +fn set_bit(array: &mut [u8], ch: u8) { + array[(ch >> 3) as usize] |= 1 << (ch & 7) +} + +thread_local! { + static DEFAULT_QUOTER: Quoter = Quoter::new(b"@:", b"%/+"); +} + +#[derive(Default, Clone, Debug)] +pub struct Url { + uri: http::Uri, + path: Option, +} + +impl Url { + pub fn new(uri: http::Uri) -> Url { + let path = DEFAULT_QUOTER.with(|q| q.requote(uri.path().as_bytes())); + + Url { uri, path } + } + + pub fn with_quoter(uri: http::Uri, quoter: &Quoter) -> Url { + Url { + path: quoter.requote(uri.path().as_bytes()), + uri, + } + } + + pub fn uri(&self) -> &http::Uri { + &self.uri + } + + pub fn path(&self) -> &str { + if let Some(ref s) = self.path { + s + } else { + self.uri.path() + } + } + + #[inline] + pub fn update(&mut self, uri: &http::Uri) { + self.uri = uri.clone(); + self.path = DEFAULT_QUOTER.with(|q| q.requote(uri.path().as_bytes())); + } + + #[inline] + pub fn update_with_quoter(&mut self, uri: &http::Uri, quoter: &Quoter) { + self.uri = uri.clone(); + self.path = quoter.requote(uri.path().as_bytes()); + } +} + +impl ResourcePath for Url { + #[inline] + fn path(&self) -> &str { + self.path() + } +} + +pub struct Quoter { + safe_table: [u8; 16], + protected_table: [u8; 16], +} + +impl Quoter { + pub fn new(safe: &[u8], protected: &[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 + } + + pub fn requote(&self, val: &[u8]) -> Option { + let mut has_pct = 0; + let mut pct = [b'%', 0, 0]; + let mut idx = 0; + let mut cloned: Option> = None; + + let len = val.len(); + while idx < len { + let ch = val[idx]; + + if has_pct != 0 { + pct[has_pct] = val[idx]; + has_pct += 1; + if has_pct == 3 { + has_pct = 0; + let buf = cloned.as_mut().unwrap(); + + if let Some(ch) = restore_ch(pct[1], pct[2]) { + if ch < 128 { + if bit_at(&self.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.push(ch); + } else { + buf.extend_from_slice(&pct[..]); + } + } + } else if ch == b'%' { + has_pct = 1; + if cloned.is_none() { + let mut c = Vec::with_capacity(len); + c.extend_from_slice(&val[..idx]); + cloned = Some(c); + } + } else if let Some(ref mut cloned) = cloned { + cloned.push(ch) + } + idx += 1; + } + + cloned.map(|data| String::from_utf8_lossy(&data).into_owned()) + } +} + +#[inline] +fn from_hex(v: u8) -> Option { + if (b'0'..=b'9').contains(&v) { + Some(v - 0x30) // ord('0') == 0x30 + } else if (b'A'..=b'F').contains(&v) { + Some(v - 0x41 + 10) // ord('A') == 0x41 + } else if (b'a'..=b'f').contains(&v) { + Some(v - 0x61 + 10) // ord('a') == 0x61 + } else { + None + } +} + +#[inline] +fn restore_ch(d1: u8, d2: u8) -> Option { + from_hex(d1).and_then(|d1| from_hex(d2).map(move |d2| d1 << 4 | d2)) +} + +#[cfg(test)] +mod tests { + use http::Uri; + use std::convert::TryFrom; + + use super::*; + use crate::{Path, ResourceDef}; + + const PROTECTED: &[u8] = b"%/+"; + + fn match_url(pattern: &'static str, url: impl AsRef) -> Path { + let re = ResourceDef::new(pattern); + let uri = Uri::try_from(url.as_ref()).unwrap(); + let mut path = Path::new(Url::new(uri)); + assert!(re.capture_match_info(&mut path)); + path + } + + fn percent_encode(data: &[u8]) -> String { + data.iter().map(|c| format!("%{:02X}", c)).collect() + } + + #[test] + fn test_parse_url() { + let re = "/user/{id}/test"; + + let path = match_url(re, "/user/2345/test"); + assert_eq!(path.get("id").unwrap(), "2345"); + + // "%25" should never be decoded into '%' to guarantee the output is a valid + // percent-encoded format + let path = match_url(re, "/user/qwe%25/test"); + assert_eq!(path.get("id").unwrap(), "qwe%25"); + + let path = match_url(re, "/user/qwe%25rty/test"); + assert_eq!(path.get("id").unwrap(), "qwe%25rty"); + } + + #[test] + fn test_protected_chars() { + let encoded = percent_encode(PROTECTED); + let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded)); + assert_eq!(path.get("id").unwrap(), &encoded); + } + + #[test] + fn test_non_protecteed_ascii() { + let nonprotected_ascii = ('\u{0}'..='\u{7F}') + .filter(|&c| c.is_ascii() && !PROTECTED.contains(&(c as u8))) + .collect::(); + let encoded = percent_encode(nonprotected_ascii.as_bytes()); + let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded)); + assert_eq!(path.get("id").unwrap(), &nonprotected_ascii); + } + + #[test] + fn test_valid_utf8_multibyte() { + let test = ('\u{FF00}'..='\u{FFFF}').collect::(); + let encoded = percent_encode(test.as_bytes()); + let path = match_url("/a/{id}/b", format!("/a/{}/b", &encoded)); + assert_eq!(path.get("id").unwrap(), &test); + } + + #[test] + fn test_invalid_utf8() { + let invalid_utf8 = percent_encode((0x80..=0xff).collect::>().as_slice()); + let uri = Uri::try_from(format!("/{}", invalid_utf8)).unwrap(); + let path = Path::new(Url::new(uri)); + + // We should always get a valid utf8 string + assert!(String::from_utf8(path.path().as_bytes().to_owned()).is_ok()); + } + + #[test] + fn test_from_hex() { + let hex = b"0123456789abcdefABCDEF"; + + for i in 0..256 { + let c = i as u8; + if hex.contains(&c) { + assert!(from_hex(c).is_some()) + } else { + assert!(from_hex(c).is_none()) + } + } + + let expected = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 10, 11, 12, 13, 14, 15, + ]; + for i in 0..hex.len() { + assert_eq!(from_hex(hex[i]).unwrap(), expected[i]); + } + } +} diff --git a/src/app.rs b/src/app.rs index 5cff20568..da5b45f3a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -334,7 +334,7 @@ where U: AsRef, { let mut rdef = ResourceDef::new(url.as_ref()); - *rdef.name_mut() = name.as_ref().to_string(); + rdef.set_name(name.as_ref()); self.external.push(rdef); self } diff --git a/src/app_service.rs b/src/app_service.rs index 3c1b78474..ce52543b8 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -291,7 +291,7 @@ impl Service for AppRouting { actix_service::always_ready!(); fn call(&self, mut req: ServiceRequest) -> Self::Future { - let res = self.router.recognize_checked(&mut req, |req, guards| { + let res = self.router.recognize_fn(&mut req, |req, guards| { if let Some(ref guards) = guards { for f in guards { if !f.check(req.head()) { diff --git a/src/config.rs b/src/config.rs index b072ace16..9e77c0f96 100644 --- a/src/config.rs +++ b/src/config.rs @@ -249,7 +249,7 @@ impl ServiceConfig { U: AsRef, { let mut rdef = ResourceDef::new(url.as_ref()); - *rdef.name_mut() = name.as_ref().to_string(); + rdef.set_name(name.as_ref()); self.external.push(rdef); self } diff --git a/src/dev.rs b/src/dev.rs index b8d95efbb..0817d902f 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -28,11 +28,22 @@ pub use actix_service::{ use crate::http::header::ContentEncoding; use actix_http::{Response, ResponseBuilder}; -pub(crate) fn insert_leading_slash(mut patterns: Vec) -> Vec { - for path in &mut patterns { - if !path.is_empty() && !path.starts_with('/') { - path.insert(0, '/'); - }; +use actix_router::Patterns; + +pub(crate) fn ensure_leading_slash(mut patterns: Patterns) -> Patterns { + match &mut patterns { + Patterns::Single(pat) => { + if !pat.is_empty() && !pat.starts_with('/') { + pat.insert(0, '/'); + }; + } + Patterns::List(pats) => { + for pat in pats { + if !pat.is_empty() && !pat.starts_with('/') { + pat.insert(0, '/'); + }; + } + } } patterns diff --git a/src/request.rs b/src/request.rs index 4b950e758..41c8252a8 100644 --- a/src/request.rs +++ b/src/request.rs @@ -509,7 +509,7 @@ mod tests { #[test] fn test_url_for() { let mut res = ResourceDef::new("/user/{name}.{ext}"); - *res.name_mut() = "index".to_string(); + res.set_name("index"); let mut rmap = ResourceMap::new(ResourceDef::new("")); rmap.add(&mut res, None); @@ -539,7 +539,7 @@ mod tests { #[test] fn test_url_for_static() { let mut rdef = ResourceDef::new("/index.html"); - *rdef.name_mut() = "index".to_string(); + rdef.set_name("index"); let mut rmap = ResourceMap::new(ResourceDef::new("")); rmap.add(&mut rdef, None); @@ -560,7 +560,7 @@ mod tests { #[test] fn test_match_name() { let mut rdef = ResourceDef::new("/index.html"); - *rdef.name_mut() = "index".to_string(); + rdef.set_name("index"); let mut rmap = ResourceMap::new(ResourceDef::new("")); rmap.add(&mut rdef, None); @@ -579,7 +579,7 @@ mod tests { fn test_url_for_external() { let mut rdef = ResourceDef::new("https://youtube.com/watch/{video_id}"); - *rdef.name_mut() = "youtube".to_string(); + rdef.set_name("youtube"); let mut rmap = ResourceMap::new(ResourceDef::new("")); rmap.add(&mut rdef, None); diff --git a/src/resource.rs b/src/resource.rs index 20d1ee17e..851ce0fc9 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -4,7 +4,7 @@ use std::future::Future; use std::rc::Rc; use actix_http::Extensions; -use actix_router::IntoPattern; +use actix_router::{IntoPatterns, Patterns}; use actix_service::boxed::{self, BoxService, BoxServiceFactory}; use actix_service::{ apply, apply_fn_factory, fn_service, IntoServiceFactory, Service, ServiceFactory, @@ -15,7 +15,7 @@ use futures_util::future::join_all; use crate::{ data::Data, - dev::{insert_leading_slash, AppService, HttpServiceFactory, ResourceDef}, + dev::{ensure_leading_slash, AppService, HttpServiceFactory, ResourceDef}, guard::Guard, handler::Handler, responder::Responder, @@ -51,7 +51,7 @@ type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Err /// Default behavior could be overridden with `default_resource()` method. pub struct Resource { endpoint: T, - rdef: Vec, + rdef: Patterns, name: Option, routes: Vec, app_data: Option, @@ -61,7 +61,7 @@ pub struct Resource { } impl Resource { - pub fn new(path: T) -> Resource { + pub fn new(path: T) -> Resource { let fref = Rc::new(RefCell::new(None)); Resource { @@ -391,13 +391,13 @@ where }; let mut rdef = if config.is_root() || !self.rdef.is_empty() { - ResourceDef::new(insert_leading_slash(self.rdef.clone())) + ResourceDef::new(ensure_leading_slash(self.rdef.clone())) } else { ResourceDef::new(self.rdef.clone()) }; if let Some(ref name) = self.name { - *rdef.name_mut() = name.clone(); + rdef.set_name(name); } *self.factory_ref.borrow_mut() = Some(ResourceFactory { diff --git a/src/rmap.rs b/src/rmap.rs index 3c8805d57..0ee4de47e 100644 --- a/src/rmap.rs +++ b/src/rmap.rs @@ -29,9 +29,8 @@ impl ResourceMap { pub fn add(&mut self, pattern: &mut ResourceDef, nested: Option>) { pattern.set_id(self.patterns.len() as u16); self.patterns.push((pattern.clone(), nested)); - if !pattern.name().is_empty() { - self.named - .insert(pattern.name().to_string(), pattern.clone()); + if let Some(name) = pattern.name() { + self.named.insert(name.to_owned(), pattern.clone()); } } @@ -83,10 +82,10 @@ impl ResourceMap { for (pattern, rmap) in &self.patterns { if let Some(ref rmap) = rmap { - if let Some(plen) = pattern.is_prefix_match(path) { - return rmap.has_resource(&path[plen..]); + if let Some(pat_len) = pattern.find_match(path) { + return rmap.has_resource(&path[pat_len..]); } - } else if pattern.is_match(path) || pattern.pattern() == "" && path == "/" { + } else if pattern.is_match(path) || pattern.pattern() == Some("") && path == "/" { return true; } } @@ -100,14 +99,11 @@ impl ResourceMap { for (pattern, rmap) in &self.patterns { if let Some(ref rmap) = rmap { - if let Some(plen) = pattern.is_prefix_match(path) { + if let Some(plen) = pattern.find_match(path) { return rmap.match_name(&path[plen..]); } } else if pattern.is_match(path) { - return match pattern.name() { - "" => None, - s => Some(s), - }; + return pattern.name(); } } @@ -136,8 +132,9 @@ impl ResourceMap { fn traverse_resource_pattern(&self, remaining: &str) -> String { for (pattern, rmap) in &self.patterns { if let Some(ref rmap) = rmap { - if let Some(prefix_len) = pattern.is_prefix_match(remaining) { - let prefix = pattern.pattern().to_owned(); + if let Some(prefix_len) = pattern.find_match(remaining) { + // TODO: think about unwrap_or + let prefix = pattern.pattern().unwrap_or("").to_owned(); return [ prefix, @@ -146,7 +143,8 @@ impl ResourceMap { .concat(); } } else if pattern.is_match(remaining) { - return pattern.pattern().to_owned(); + // TODO: think about unwrap_or + return pattern.pattern().unwrap_or("").to_owned(); } } @@ -181,10 +179,15 @@ impl ResourceMap { I: AsRef, { if let Some(pattern) = self.named.get(name) { - if pattern.pattern().starts_with('/') { + if pattern + .pattern() + .map(|pat| pat.starts_with('/')) + .unwrap_or(false) + { self.fill_root(path, elements)?; } - if pattern.resource_path(path, elements) { + + if pattern.resource_path_from_iter(path, elements) { Ok(Some(())) } else { Err(UrlGenerationError::NotEnoughElements) @@ -213,7 +216,8 @@ impl ResourceMap { if let Some(ref parent) = self.parent.borrow().upgrade() { parent.fill_root(path, elements)?; } - if self.root.resource_path(path, elements) { + + if self.root.resource_path_from_iter(path, elements) { Ok(()) } else { Err(UrlGenerationError::NotEnoughElements) @@ -233,7 +237,7 @@ impl ResourceMap { if let Some(ref parent) = self.parent.borrow().upgrade() { if let Some(pattern) = parent.named.get(name) { self.fill_root(path, elements)?; - if pattern.resource_path(path, elements) { + if pattern.resource_path_from_iter(path, elements) { Ok(Some(())) } else { Err(UrlGenerationError::NotEnoughElements) @@ -329,7 +333,7 @@ mod tests { let mut root = ResourceMap::new(ResourceDef::root_prefix("")); let mut rdef = ResourceDef::new("/info"); - *rdef.name_mut() = "root_info".to_owned(); + rdef.set_name("root_info"); root.add(&mut rdef, None); let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); @@ -337,7 +341,7 @@ mod tests { user_map.add(&mut rdef, None); let mut rdef = ResourceDef::new("/post/{post_id}"); - *rdef.name_mut() = "user_post".to_owned(); + rdef.set_name("user_post"); user_map.add(&mut rdef, None); root.add( diff --git a/src/scope.rs b/src/scope.rs index aa546c422..97db53eeb 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -530,7 +530,7 @@ impl Service for ScopeService { actix_service::always_ready!(); fn call(&self, mut req: ServiceRequest) -> Self::Future { - let res = self.router.recognize_checked(&mut req, |req, guards| { + let res = self.router.recognize_fn(&mut req, |req, guards| { if let Some(ref guards) = guards { for f in guards { if !f.check(req.head()) { diff --git a/src/service.rs b/src/service.rs index 47e7e4acc..148199407 100644 --- a/src/service.rs +++ b/src/service.rs @@ -7,14 +7,14 @@ use actix_http::{ http::{HeaderMap, Method, StatusCode, Uri, Version}, Extensions, HttpMessage, Payload, PayloadStream, RequestHead, Response, ResponseHead, }; -use actix_router::{IntoPattern, Path, Resource, ResourceDef, Url}; +use actix_router::{IntoPatterns, Path, Patterns, Resource, ResourceDef, Url}; use actix_service::{IntoServiceFactory, ServiceFactory}; #[cfg(feature = "cookies")] use cookie::{Cookie, ParseError as CookieParseError}; use crate::{ config::{AppConfig, AppService}, - dev::insert_leading_slash, + dev::ensure_leading_slash, guard::Guard, info::ConnectionInfo, rmap::ResourceMap, @@ -212,14 +212,14 @@ impl ServiceRequest { self.req.match_pattern() } - #[inline] /// Get a mutable reference to the Path parameters. + #[inline] pub fn match_info_mut(&mut self) -> &mut Path { self.req.match_info_mut() } - #[inline] /// Get a reference to a `ResourceMap` of current application. + #[inline] pub fn resource_map(&self) -> &ResourceMap { self.req.resource_map() } @@ -459,14 +459,14 @@ where } pub struct WebService { - rdef: Vec, + rdef: Patterns, name: Option, guards: Vec>, } impl WebService { /// Create new `WebService` instance. - pub fn new(path: T) -> Self { + pub fn new(path: T) -> Self { WebService { rdef: path.patterns(), name: None, @@ -528,7 +528,7 @@ impl WebService { struct WebServiceImpl { srv: T, - rdef: Vec, + rdef: Patterns, name: Option, guards: Vec>, } @@ -551,13 +551,15 @@ where }; let mut rdef = if config.is_root() || !self.rdef.is_empty() { - ResourceDef::new(insert_leading_slash(self.rdef)) + ResourceDef::new(ensure_leading_slash(self.rdef)) } else { ResourceDef::new(self.rdef) }; + if let Some(ref name) = self.name { - *rdef.name_mut() = name.clone(); + rdef.set_name(name); } + config.register_service(rdef, guards, self.srv, None) } } diff --git a/src/types/path.rs b/src/types/path.rs index f2273a59b..4052646e3 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -209,7 +209,7 @@ mod tests { let resource = ResourceDef::new("/{value}/"); let mut req = TestRequest::with_uri("/32/").to_srv_request(); - resource.match_path(req.match_info_mut()); + resource.capture_match_info(req.match_info_mut()); let (req, mut pl) = req.into_parts(); assert_eq!(*Path::::from_request(&req, &mut pl).await.unwrap(), 32); @@ -221,7 +221,7 @@ mod tests { let resource = ResourceDef::new("/{key}/{value}/"); let mut req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request(); - resource.match_path(req.match_info_mut()); + resource.capture_match_info(req.match_info_mut()); let (req, mut pl) = req.into_parts(); let (Path(res),) = <(Path<(String, String)>,)>::from_request(&req, &mut pl) @@ -247,7 +247,7 @@ mod tests { let mut req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request(); let resource = ResourceDef::new("/{key}/{value}/"); - resource.match_path(req.match_info_mut()); + resource.capture_match_info(req.match_info_mut()); let (req, mut pl) = req.into_parts(); let mut s = Path::::from_request(&req, &mut pl).await.unwrap(); @@ -270,7 +270,7 @@ mod tests { let mut req = TestRequest::with_uri("/name/32/").to_srv_request(); let resource = ResourceDef::new("/{key}/{value}/"); - resource.match_path(req.match_info_mut()); + resource.capture_match_info(req.match_info_mut()); let (req, mut pl) = req.into_parts(); let s = Path::::from_request(&req, &mut pl).await.unwrap(); diff --git a/src/web.rs b/src/web.rs index 40ac46275..108ff314f 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1,10 +1,10 @@ //! Essentials helper functions and types for application registration. -use actix_http::http::Method; -use actix_router::IntoPattern; use std::future::Future; +use actix_http::http::Method; pub use actix_http::Response as HttpResponse; +use actix_router::IntoPatterns; pub use bytes::{Buf, BufMut, Bytes, BytesMut}; use crate::error::BlockingError; @@ -51,7 +51,7 @@ pub use crate::types::*; /// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) /// ); /// ``` -pub fn resource(path: T) -> Resource { +pub fn resource(path: T) -> Resource { Resource::new(path) } @@ -268,7 +268,7 @@ where /// .finish(my_service) /// ); /// ``` -pub fn service(path: T) -> WebService { +pub fn service(path: T) -> WebService { WebService::new(path) } From e965d8298f421e9c89fe98b1300b8361e948c324 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 12 Aug 2021 20:18:09 +0100 Subject: [PATCH 29/88] HRS security fixes (#2363) --- actix-http/CHANGES.md | 10 + actix-http/Cargo.toml | 2 +- actix-http/README.md | 4 +- actix-http/src/error.rs | 2 +- actix-http/src/h1/chunked.rs | 432 +++++++++++++++++++++++++++++++ actix-http/src/h1/decoder.rs | 481 ++++++++++------------------------- actix-http/src/h1/encoder.rs | 1 + actix-http/src/h1/mod.rs | 2 + 8 files changed, 583 insertions(+), 351 deletions(-) create mode 100644 actix-http/src/h1/chunked.rs diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 8ead43718..f52f5ba68 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -3,6 +3,11 @@ ## Unreleased - 2021-xx-xx +## 3.0.0-beta.8 - 2021-08-09 +### Fixed +* Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977) + + ## 3.0.0-beta.8 - 2021-06-26 ### Changed * Change compression algorithm features flags. [#2250] @@ -210,6 +215,11 @@ [#1878]: https://github.com/actix/actix-web/pull/1878 +## 2.2.1 - 2021-08-09 +### Fixed +* Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977) + + ## 2.2.0 - 2020-11-25 ### Added * HttpResponse builders for 1xx status codes. [#1768] diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index a12fed4b9..4ce55dca1 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "3.0.0-beta.8" +version = "3.0.0-beta.9" authors = ["Nikolay Kim "] description = "HTTP primitives for the Actix ecosystem" keywords = ["actix", "http", "framework", "async", "futures"] diff --git a/actix-http/README.md b/actix-http/README.md index de1ef0a9b..5b06583bc 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -3,11 +3,11 @@ > HTTP primitives for the Actix ecosystem. [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) -[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.8)](https://docs.rs/actix-http/3.0.0-beta.8) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.9)](https://docs.rs/actix-http/3.0.0-beta.9) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
-[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.8/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.8) +[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.9/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.9) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index 54666e072..f7d7f696a 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -196,7 +196,7 @@ pub enum ParseError { #[display(fmt = "IO error: {}", _0)] Io(io::Error), - /// Parsing a field as string failed + /// Parsing a field as string failed. #[display(fmt = "UTF8 error: {}", _0)] Utf8(Utf8Error), } diff --git a/actix-http/src/h1/chunked.rs b/actix-http/src/h1/chunked.rs new file mode 100644 index 000000000..1224ce08c --- /dev/null +++ b/actix-http/src/h1/chunked.rs @@ -0,0 +1,432 @@ +use std::{io, task::Poll}; + +use bytes::{Buf as _, Bytes, BytesMut}; + +macro_rules! byte ( + ($rdr:ident) => ({ + if $rdr.len() > 0 { + let b = $rdr[0]; + $rdr.advance(1); + b + } else { + return Poll::Pending + } + }) +); + +#[derive(Debug, PartialEq, Clone)] +pub(super) enum ChunkedState { + Size, + SizeLws, + Extension, + SizeLf, + Body, + BodyCr, + BodyLf, + EndCr, + EndLf, + End, +} + +impl ChunkedState { + pub(super) fn step( + &self, + body: &mut BytesMut, + size: &mut u64, + buf: &mut Option, + ) -> Poll> { + use self::ChunkedState::*; + match *self { + Size => ChunkedState::read_size(body, size), + SizeLws => ChunkedState::read_size_lws(body), + Extension => ChunkedState::read_extension(body), + SizeLf => ChunkedState::read_size_lf(body, size), + Body => ChunkedState::read_body(body, size, buf), + BodyCr => ChunkedState::read_body_cr(body), + BodyLf => ChunkedState::read_body_lf(body), + EndCr => ChunkedState::read_end_cr(body), + EndLf => ChunkedState::read_end_lf(body), + End => Poll::Ready(Ok(ChunkedState::End)), + } + } + + fn read_size( + rdr: &mut BytesMut, + size: &mut u64, + ) -> Poll> { + let radix = 16; + + let rem = match byte!(rdr) { + b @ b'0'..=b'9' => b - b'0', + b @ b'a'..=b'f' => b + 10 - b'a', + b @ b'A'..=b'F' => b + 10 - b'A', + b'\t' | b' ' => return Poll::Ready(Ok(ChunkedState::SizeLws)), + b';' => return Poll::Ready(Ok(ChunkedState::Extension)), + b'\r' => return Poll::Ready(Ok(ChunkedState::SizeLf)), + _ => { + return Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk size line: Invalid Size", + ))); + } + }; + + match size.checked_mul(radix) { + Some(n) => { + *size = n as u64; + *size += rem as u64; + + Poll::Ready(Ok(ChunkedState::Size)) + } + None => { + log::debug!("chunk size would overflow u64"); + Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk size line: Size is too big", + ))) + } + } + } + + fn read_size_lws(rdr: &mut BytesMut) -> Poll> { + match byte!(rdr) { + // LWS can follow the chunk size, but no more digits can come + b'\t' | b' ' => Poll::Ready(Ok(ChunkedState::SizeLws)), + b';' => Poll::Ready(Ok(ChunkedState::Extension)), + b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)), + _ => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk size linear white space", + ))), + } + } + fn read_extension(rdr: &mut BytesMut) -> Poll> { + match byte!(rdr) { + b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)), + // strictly 0x20 (space) should be disallowed but we don't parse quoted strings here + 0x00..=0x08 | 0x0a..=0x1f | 0x7f => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid character in chunk extension", + ))), + _ => Poll::Ready(Ok(ChunkedState::Extension)), // no supported extensions + } + } + fn read_size_lf( + rdr: &mut BytesMut, + size: &mut u64, + ) -> Poll> { + match byte!(rdr) { + b'\n' if *size > 0 => Poll::Ready(Ok(ChunkedState::Body)), + b'\n' if *size == 0 => Poll::Ready(Ok(ChunkedState::EndCr)), + _ => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk size LF", + ))), + } + } + + fn read_body( + rdr: &mut BytesMut, + rem: &mut u64, + buf: &mut Option, + ) -> Poll> { + log::trace!("Chunked read, remaining={:?}", rem); + + let len = rdr.len() as u64; + if len == 0 { + Poll::Ready(Ok(ChunkedState::Body)) + } else { + let slice; + if *rem > len { + slice = rdr.split().freeze(); + *rem -= len; + } else { + slice = rdr.split_to(*rem as usize).freeze(); + *rem = 0; + } + *buf = Some(slice); + if *rem > 0 { + Poll::Ready(Ok(ChunkedState::Body)) + } else { + Poll::Ready(Ok(ChunkedState::BodyCr)) + } + } + } + + fn read_body_cr(rdr: &mut BytesMut) -> Poll> { + match byte!(rdr) { + b'\r' => Poll::Ready(Ok(ChunkedState::BodyLf)), + _ => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk body CR", + ))), + } + } + fn read_body_lf(rdr: &mut BytesMut) -> Poll> { + match byte!(rdr) { + b'\n' => Poll::Ready(Ok(ChunkedState::Size)), + _ => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk body LF", + ))), + } + } + fn read_end_cr(rdr: &mut BytesMut) -> Poll> { + match byte!(rdr) { + b'\r' => Poll::Ready(Ok(ChunkedState::EndLf)), + _ => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk end CR", + ))), + } + } + fn read_end_lf(rdr: &mut BytesMut) -> Poll> { + match byte!(rdr) { + b'\n' => Poll::Ready(Ok(ChunkedState::End)), + _ => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk end LF", + ))), + } + } +} + +#[cfg(test)] +mod tests { + use actix_codec::Decoder as _; + use bytes::{Bytes, BytesMut}; + use http::Method; + + use crate::{ + error::ParseError, + h1::decoder::{MessageDecoder, PayloadItem}, + HttpMessage as _, Request, + }; + + macro_rules! parse_ready { + ($e:expr) => {{ + match MessageDecoder::::default().decode($e) { + Ok(Some((msg, _))) => msg, + Ok(_) => unreachable!("Eof during parsing http request"), + Err(err) => unreachable!("Error during parsing http request: {:?}", err), + } + }}; + } + + macro_rules! expect_parse_err { + ($e:expr) => {{ + match MessageDecoder::::default().decode($e) { + Err(err) => match err { + ParseError::Io(_) => unreachable!("Parse error expected"), + _ => {} + }, + _ => unreachable!("Error expected"), + } + }}; + } + + #[test] + fn test_parse_chunked_payload_chunk_extension() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\ + \r\n", + ); + + let mut reader = MessageDecoder::::default(); + let (msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + assert!(msg.chunked().unwrap()); + + buf.extend(b"4;test\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); // test: test\r\n\r\n") + let chunk = pl.decode(&mut buf).unwrap().unwrap().chunk(); + assert_eq!(chunk, Bytes::from_static(b"data")); + let chunk = pl.decode(&mut buf).unwrap().unwrap().chunk(); + assert_eq!(chunk, Bytes::from_static(b"line")); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert!(msg.eof()); + } + + #[test] + fn test_request_chunked() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n", + ); + let req = parse_ready!(&mut buf); + + if let Ok(val) = req.chunked() { + assert!(val); + } else { + unreachable!("Error"); + } + + // intentional typo in "chunked" + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chnked\r\n\r\n", + ); + expect_parse_err!(&mut buf); + } + + #[test] + fn test_http_request_chunked_payload() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n", + ); + let mut reader = MessageDecoder::::default(); + let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + assert!(req.chunked().unwrap()); + + buf.extend(b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); + assert_eq!( + pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), + b"data" + ); + assert_eq!( + pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), + b"line" + ); + assert!(pl.decode(&mut buf).unwrap().unwrap().eof()); + } + + #[test] + fn test_http_request_chunked_payload_and_next_message() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n", + ); + let mut reader = MessageDecoder::::default(); + let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + assert!(req.chunked().unwrap()); + + buf.extend( + b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n\ + POST /test2 HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n" + .iter(), + ); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"data"); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"line"); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert!(msg.eof()); + + let (req, _) = reader.decode(&mut buf).unwrap().unwrap(); + assert!(req.chunked().unwrap()); + assert_eq!(*req.method(), Method::POST); + assert!(req.chunked().unwrap()); + } + + #[test] + fn test_http_request_chunked_payload_chunks() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n", + ); + + let mut reader = MessageDecoder::::default(); + let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + assert!(req.chunked().unwrap()); + + buf.extend(b"4\r\n1111\r\n"); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"1111"); + + buf.extend(b"4\r\ndata\r"); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"data"); + + buf.extend(b"\n4"); + assert!(pl.decode(&mut buf).unwrap().is_none()); + + buf.extend(b"\r"); + assert!(pl.decode(&mut buf).unwrap().is_none()); + buf.extend(b"\n"); + assert!(pl.decode(&mut buf).unwrap().is_none()); + + buf.extend(b"li"); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"li"); + + //trailers + //buf.feed_data("test: test\r\n"); + //not_ready!(reader.parse(&mut buf, &mut readbuf)); + + buf.extend(b"ne\r\n0\r\n"); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"ne"); + assert!(pl.decode(&mut buf).unwrap().is_none()); + + buf.extend(b"\r\n"); + assert!(pl.decode(&mut buf).unwrap().unwrap().eof()); + } + + #[test] + fn chunk_extension_quoted() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + Host: localhost:8080\r\n\ + Transfer-Encoding: chunked\r\n\ + \r\n\ + 2;hello=b;one=\"1 2 3\"\r\n\ + xx", + ); + + let mut reader = MessageDecoder::::default(); + let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + + let chunk = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"xx"))); + } + + #[test] + fn hrs_chunk_extension_invalid() { + let mut buf = BytesMut::from( + "GET / HTTP/1.1\r\n\ + Host: localhost:8080\r\n\ + Transfer-Encoding: chunked\r\n\ + \r\n\ + 2;x\nx\r\n\ + 4c\r\n\ + 0\r\n", + ); + + let mut reader = MessageDecoder::::default(); + let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + + let err = pl.decode(&mut buf).unwrap_err(); + assert!(err + .to_string() + .contains("Invalid character in chunk extension")); + } + + #[test] + fn hrs_chunk_size_overflow() { + let mut buf = BytesMut::from( + "GET / HTTP/1.1\r\n\ + Host: example.com\r\n\ + Transfer-Encoding: chunked\r\n\ + \r\n\ + f0000000000000003\r\n\ + abc\r\n\ + 0\r\n", + ); + + let mut reader = MessageDecoder::::default(); + let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + + let err = pl.decode(&mut buf).unwrap_err(); + assert!(err + .to_string() + .contains("Invalid chunk size line: Size is too big")); + } +} diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index f240710c2..313ffd5e0 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -1,18 +1,18 @@ -use std::convert::TryFrom; -use std::io; -use std::marker::PhantomData; -use std::task::Poll; +use std::{convert::TryFrom, io, marker::PhantomData, task::Poll}; use actix_codec::Decoder; -use bytes::{Buf, Bytes, BytesMut}; +use bytes::{Bytes, BytesMut}; use http::header::{HeaderName, HeaderValue}; use http::{header, Method, StatusCode, Uri, Version}; use log::{debug, error, trace}; -use crate::error::ParseError; -use crate::header::HeaderMap; -use crate::message::{ConnectionType, ResponseHead}; -use crate::request::Request; +use super::chunked::ChunkedState; +use crate::{ + error::ParseError, + header::HeaderMap, + message::{ConnectionType, ResponseHead}, + request::Request, +}; pub(crate) const MAX_BUFFER_SIZE: usize = 131_072; const MAX_HEADERS: usize = 96; @@ -67,6 +67,7 @@ pub(crate) trait MessageType: Sized { let mut has_upgrade_websocket = false; let mut expect = false; let mut chunked = false; + let mut seen_te = false; let mut content_length = None; { @@ -85,8 +86,17 @@ pub(crate) trait MessageType: Sized { }; match name { - header::CONTENT_LENGTH => { - if let Ok(s) = value.to_str() { + header::CONTENT_LENGTH if content_length.is_some() => { + debug!("multiple Content-Length"); + return Err(ParseError::Header); + } + + header::CONTENT_LENGTH => match value.to_str() { + Ok(s) if s.trim().starts_with('+') => { + debug!("illegal Content-Length: {:?}", s); + return Err(ParseError::Header); + } + Ok(s) => { if let Ok(len) = s.parse::() { if len != 0 { content_length = Some(len); @@ -95,15 +105,31 @@ pub(crate) trait MessageType: Sized { debug!("illegal Content-Length: {:?}", s); return Err(ParseError::Header); } - } else { + } + Err(_) => { debug!("illegal Content-Length: {:?}", value); return Err(ParseError::Header); } - } + }, + // transfer-encoding + header::TRANSFER_ENCODING if seen_te => { + debug!("multiple Transfer-Encoding not allowed"); + return Err(ParseError::Header); + } + header::TRANSFER_ENCODING => { + seen_te = true; + if let Ok(s) = value.to_str().map(str::trim) { - chunked = s.eq_ignore_ascii_case("chunked"); + if s.eq_ignore_ascii_case("chunked") { + chunked = true; + } else if s.eq_ignore_ascii_case("identity") { + // allow silently since multiple TE headers are already checked + } else { + debug!("illegal Transfer-Encoding: {:?}", s); + return Err(ParseError::Header); + } } else { return Err(ParseError::Header); } @@ -408,20 +434,6 @@ enum Kind { Eof, } -#[derive(Debug, PartialEq, Clone)] -enum ChunkedState { - Size, - SizeLws, - Extension, - SizeLf, - Body, - BodyCr, - BodyLf, - EndCr, - EndLf, - End, -} - impl Decoder for PayloadDecoder { type Item = PayloadItem; type Error = io::Error; @@ -451,19 +463,23 @@ impl Decoder for PayloadDecoder { Kind::Chunked(ref mut state, ref mut size) => { loop { let mut buf = None; + // advances the chunked state *state = match state.step(src, size, &mut buf) { Poll::Pending => return Ok(None), Poll::Ready(Ok(state)) => state, Poll::Ready(Err(e)) => return Err(e), }; + if *state == ChunkedState::End { trace!("End of chunked stream"); return Ok(Some(PayloadItem::Eof)); } + if let Some(buf) = buf { return Ok(Some(PayloadItem::Chunk(buf))); } + if src.is_empty() { return Ok(None); } @@ -480,201 +496,40 @@ impl Decoder for PayloadDecoder { } } -macro_rules! byte ( - ($rdr:ident) => ({ - if $rdr.len() > 0 { - let b = $rdr[0]; - $rdr.advance(1); - b - } else { - return Poll::Pending - } - }) -); - -impl ChunkedState { - fn step( - &self, - body: &mut BytesMut, - size: &mut u64, - buf: &mut Option, - ) -> Poll> { - use self::ChunkedState::*; - match *self { - Size => ChunkedState::read_size(body, size), - SizeLws => ChunkedState::read_size_lws(body), - Extension => ChunkedState::read_extension(body), - SizeLf => ChunkedState::read_size_lf(body, size), - Body => ChunkedState::read_body(body, size, buf), - BodyCr => ChunkedState::read_body_cr(body), - BodyLf => ChunkedState::read_body_lf(body), - EndCr => ChunkedState::read_end_cr(body), - EndLf => ChunkedState::read_end_lf(body), - End => Poll::Ready(Ok(ChunkedState::End)), - } - } - - fn read_size( - rdr: &mut BytesMut, - size: &mut u64, - ) -> Poll> { - let radix = 16; - match byte!(rdr) { - b @ b'0'..=b'9' => { - *size *= radix; - *size += u64::from(b - b'0'); - } - b @ b'a'..=b'f' => { - *size *= radix; - *size += u64::from(b + 10 - b'a'); - } - b @ b'A'..=b'F' => { - *size *= radix; - *size += u64::from(b + 10 - b'A'); - } - b'\t' | b' ' => return Poll::Ready(Ok(ChunkedState::SizeLws)), - b';' => return Poll::Ready(Ok(ChunkedState::Extension)), - b'\r' => return Poll::Ready(Ok(ChunkedState::SizeLf)), - _ => { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk size line: Invalid Size", - ))); - } - } - Poll::Ready(Ok(ChunkedState::Size)) - } - - fn read_size_lws(rdr: &mut BytesMut) -> Poll> { - trace!("read_size_lws"); - match byte!(rdr) { - // LWS can follow the chunk size, but no more digits can come - b'\t' | b' ' => Poll::Ready(Ok(ChunkedState::SizeLws)), - b';' => Poll::Ready(Ok(ChunkedState::Extension)), - b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)), - _ => Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk size linear white space", - ))), - } - } - fn read_extension(rdr: &mut BytesMut) -> Poll> { - match byte!(rdr) { - b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)), - _ => Poll::Ready(Ok(ChunkedState::Extension)), // no supported extensions - } - } - fn read_size_lf( - rdr: &mut BytesMut, - size: &mut u64, - ) -> Poll> { - match byte!(rdr) { - b'\n' if *size > 0 => Poll::Ready(Ok(ChunkedState::Body)), - b'\n' if *size == 0 => Poll::Ready(Ok(ChunkedState::EndCr)), - _ => Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk size LF", - ))), - } - } - - fn read_body( - rdr: &mut BytesMut, - rem: &mut u64, - buf: &mut Option, - ) -> Poll> { - trace!("Chunked read, remaining={:?}", rem); - - let len = rdr.len() as u64; - if len == 0 { - Poll::Ready(Ok(ChunkedState::Body)) - } else { - let slice; - if *rem > len { - slice = rdr.split().freeze(); - *rem -= len; - } else { - slice = rdr.split_to(*rem as usize).freeze(); - *rem = 0; - } - *buf = Some(slice); - if *rem > 0 { - Poll::Ready(Ok(ChunkedState::Body)) - } else { - Poll::Ready(Ok(ChunkedState::BodyCr)) - } - } - } - - fn read_body_cr(rdr: &mut BytesMut) -> Poll> { - match byte!(rdr) { - b'\r' => Poll::Ready(Ok(ChunkedState::BodyLf)), - _ => Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk body CR", - ))), - } - } - fn read_body_lf(rdr: &mut BytesMut) -> Poll> { - match byte!(rdr) { - b'\n' => Poll::Ready(Ok(ChunkedState::Size)), - _ => Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk body LF", - ))), - } - } - fn read_end_cr(rdr: &mut BytesMut) -> Poll> { - match byte!(rdr) { - b'\r' => Poll::Ready(Ok(ChunkedState::EndLf)), - _ => Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk end CR", - ))), - } - } - fn read_end_lf(rdr: &mut BytesMut) -> Poll> { - match byte!(rdr) { - b'\n' => Poll::Ready(Ok(ChunkedState::End)), - _ => Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk end LF", - ))), - } - } -} - #[cfg(test)] mod tests { use bytes::{Bytes, BytesMut}; use http::{Method, Version}; use super::*; - use crate::error::ParseError; - use crate::http::header::{HeaderName, SET_COOKIE}; - use crate::HttpMessage; + use crate::{ + error::ParseError, + http::header::{HeaderName, SET_COOKIE}, + HttpMessage as _, + }; impl PayloadType { - fn unwrap(self) -> PayloadDecoder { + pub(crate) fn unwrap(self) -> PayloadDecoder { match self { PayloadType::Payload(pl) => pl, _ => panic!(), } } - fn is_unhandled(&self) -> bool { + pub(crate) fn is_unhandled(&self) -> bool { matches!(self, PayloadType::Stream(_)) } } impl PayloadItem { - fn chunk(self) -> Bytes { + pub(crate) fn chunk(self) -> Bytes { match self { PayloadItem::Chunk(chunk) => chunk, _ => panic!("error"), } } - fn eof(&self) -> bool { + + pub(crate) fn eof(&self) -> bool { matches!(*self, PayloadItem::Eof) } } @@ -967,34 +822,6 @@ mod tests { assert!(req.upgrade()); } - #[test] - fn test_request_chunked() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - if let Ok(val) = req.chunked() { - assert!(val); - } else { - unreachable!("Error"); - } - - // intentional typo in "chunked" - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chnked\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - if let Ok(val) = req.chunked() { - assert!(!val); - } else { - unreachable!("Error"); - } - } - #[test] fn test_headers_content_length_err_1() { let mut buf = BytesMut::from( @@ -1112,126 +939,6 @@ mod tests { expect_parse_err!(&mut buf); } - #[test] - fn test_http_request_chunked_payload() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - let mut reader = MessageDecoder::::default(); - let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); - let mut pl = pl.unwrap(); - assert!(req.chunked().unwrap()); - - buf.extend(b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); - assert_eq!( - pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), - b"data" - ); - assert_eq!( - pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), - b"line" - ); - assert!(pl.decode(&mut buf).unwrap().unwrap().eof()); - } - - #[test] - fn test_http_request_chunked_payload_and_next_message() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - let mut reader = MessageDecoder::::default(); - let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); - let mut pl = pl.unwrap(); - assert!(req.chunked().unwrap()); - - buf.extend( - b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n\ - POST /test2 HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n" - .iter(), - ); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"data"); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"line"); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert!(msg.eof()); - - let (req, _) = reader.decode(&mut buf).unwrap().unwrap(); - assert!(req.chunked().unwrap()); - assert_eq!(*req.method(), Method::POST); - assert!(req.chunked().unwrap()); - } - - #[test] - fn test_http_request_chunked_payload_chunks() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - - let mut reader = MessageDecoder::::default(); - let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); - let mut pl = pl.unwrap(); - assert!(req.chunked().unwrap()); - - buf.extend(b"4\r\n1111\r\n"); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"1111"); - - buf.extend(b"4\r\ndata\r"); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"data"); - - buf.extend(b"\n4"); - assert!(pl.decode(&mut buf).unwrap().is_none()); - - buf.extend(b"\r"); - assert!(pl.decode(&mut buf).unwrap().is_none()); - buf.extend(b"\n"); - assert!(pl.decode(&mut buf).unwrap().is_none()); - - buf.extend(b"li"); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"li"); - - //trailers - //buf.feed_data("test: test\r\n"); - //not_ready!(reader.parse(&mut buf, &mut readbuf)); - - buf.extend(b"ne\r\n0\r\n"); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"ne"); - assert!(pl.decode(&mut buf).unwrap().is_none()); - - buf.extend(b"\r\n"); - assert!(pl.decode(&mut buf).unwrap().unwrap().eof()); - } - - #[test] - fn test_parse_chunked_payload_chunk_extension() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\ - \r\n", - ); - - let mut reader = MessageDecoder::::default(); - let (msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); - let mut pl = pl.unwrap(); - assert!(msg.chunked().unwrap()); - - buf.extend(b"4;test\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); // test: test\r\n\r\n") - let chunk = pl.decode(&mut buf).unwrap().unwrap().chunk(); - assert_eq!(chunk, Bytes::from_static(b"data")); - let chunk = pl.decode(&mut buf).unwrap().unwrap().chunk(); - assert_eq!(chunk, Bytes::from_static(b"line")); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert!(msg.eof()); - } - #[test] fn test_response_http10_read_until_eof() { let mut buf = BytesMut::from("HTTP/1.0 200 Ok\r\n\r\ntest data"); @@ -1243,4 +950,84 @@ mod tests { let chunk = pl.decode(&mut buf).unwrap().unwrap(); assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"test data"))); } + + #[test] + fn hrs_multiple_content_length() { + let mut buf = BytesMut::from( + "GET / HTTP/1.1\r\n\ + Host: example.com\r\n\ + Content-Length: 4\r\n\ + Content-Length: 2\r\n\ + \r\n\ + abcd", + ); + + expect_parse_err!(&mut buf); + } + + #[test] + fn hrs_content_length_plus() { + let mut buf = BytesMut::from( + "GET / HTTP/1.1\r\n\ + Host: example.com\r\n\ + Content-Length: +3\r\n\ + \r\n\ + 000", + ); + + expect_parse_err!(&mut buf); + } + + #[test] + fn hrs_unknown_transfer_encoding() { + let mut buf = BytesMut::from( + "GET / HTTP/1.1\r\n\ + Host: example.com\r\n\ + Transfer-Encoding: JUNK\r\n\ + Transfer-Encoding: chunked\r\n\ + \r\n\ + 5\r\n\ + hello\r\n\ + 0", + ); + + expect_parse_err!(&mut buf); + } + + #[test] + fn hrs_multiple_transfer_encoding() { + let mut buf = BytesMut::from( + "GET / HTTP/1.1\r\n\ + Host: example.com\r\n\ + Content-Length: 51\r\n\ + Transfer-Encoding: identity\r\n\ + Transfer-Encoding: chunked\r\n\ + \r\n\ + 0\r\n\ + \r\n\ + GET /forbidden HTTP/1.1\r\n\ + Host: example.com\r\n\r\n", + ); + + expect_parse_err!(&mut buf); + } + + #[test] + fn transfer_encoding_agrees() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + Host: example.com\r\n\ + Content-Length: 3\r\n\ + Transfer-Encoding: identity\r\n\ + \r\n\ + 0\r\n", + ); + + let mut reader = MessageDecoder::::default(); + let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + + let chunk = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"0\r\n"))); + } } diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index 254981123..4e5c9d238 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -81,6 +81,7 @@ pub(crate) trait MessageType: Sized { match length { BodySize::Stream => { if chunked { + skip_len = true; if camel_case { dst.put_slice(b"\r\nTransfer-Encoding: chunked\r\n") } else { diff --git a/actix-http/src/h1/mod.rs b/actix-http/src/h1/mod.rs index 7e6df6ceb..17cbfb90f 100644 --- a/actix-http/src/h1/mod.rs +++ b/actix-http/src/h1/mod.rs @@ -1,6 +1,8 @@ //! HTTP/1 protocol implementation. + use bytes::{Bytes, BytesMut}; +mod chunked; mod client; mod codec; mod decoder; From 384164cc148e4bf31a8ff3ddffd1139e64a1c15f Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 6 Aug 2021 20:10:58 +0100 Subject: [PATCH 30/88] update graphs --- docs/graphs/net-only.dot | 3 +-- docs/graphs/web-focus.dot | 3 ++- docs/graphs/web-only.dot | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/graphs/net-only.dot b/docs/graphs/net-only.dot index bee0185ab..8a58ec2b8 100644 --- a/docs/graphs/net-only.dot +++ b/docs/graphs/net-only.dot @@ -4,7 +4,7 @@ digraph { subgraph cluster_net { label="actix-net" "actix-codec" "actix-macros" "actix-rt" "actix-server" "actix-service" - "actix-tls" "actix-tracing" "actix-utils" "actix-router" + "actix-tls" "actix-tracing" "actix-utils" } subgraph cluster_other { @@ -25,7 +25,6 @@ digraph { "actix-tls" -> { "tokio-util" }[color="#009900"] "actix-server" -> { "actix-service" "actix-rt" "actix-utils" "tokio" } "actix-rt" -> { "actix-macros" "tokio" } - "actix-router" -> { "bytestring" } "local-channel" -> { "local-waker" } diff --git a/docs/graphs/web-focus.dot b/docs/graphs/web-focus.dot index 2c6e2779b..63b3eaa82 100644 --- a/docs/graphs/web-focus.dot +++ b/docs/graphs/web-focus.dot @@ -10,6 +10,7 @@ digraph { "web-actors" "web-codegen" "http-test" + "router" { rank=same; "multipart" "web-actors" "http-test" }; { rank=same; "files" "awc" "web" }; @@ -36,7 +37,7 @@ digraph { "rt" -> { "macros" } { rank=same; "utils" "codec" }; - { rank=same; "rt" "macros" "service" "router" }; + { rank=same; "rt" "macros" "service" }; // actix diff --git a/docs/graphs/web-only.dot b/docs/graphs/web-only.dot index b0decd818..ee74c292b 100644 --- a/docs/graphs/web-only.dot +++ b/docs/graphs/web-only.dot @@ -10,9 +10,10 @@ digraph { "actix-web-codegen" "actix-http-test" "actix-test" + "actix-router" } - "actix-web" -> { "actix-web-codegen" "actix-http" } + "actix-web" -> { "actix-web-codegen" "actix-http" "actix-router" } "awc" -> { "actix-http" } "actix-web-actors" -> { "actix" "actix-web" "actix-http" } "actix-multipart" -> { "actix-web" } From a0c0bff944febe1d984aedc4866acee1bed95bdd Mon Sep 17 00:00:00 2001 From: Thales <46510852+thalesfragoso@users.noreply.github.com> Date: Fri, 13 Aug 2021 14:41:19 -0300 Subject: [PATCH 31/88] Don't create a slice to potential uninit data on h1 encoder (#2364) Co-authored-by: Rob Ede --- actix-http/CHANGES.md | 4 ++++ actix-http/benches/write-camel-case.rs | 10 +++++++--- actix-http/src/h1/encoder.rs | 15 +++++++++++---- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index f52f5ba68..9ed28105f 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -2,6 +2,10 @@ ## Unreleased - 2021-xx-xx +### Fixed +* Remove slice creation pointing to potential uninitialized data on h1 encoder. [#2364] + +[#2364]: https://github.com/actix/actix-web/pull/2364 ## 3.0.0-beta.8 - 2021-08-09 ### Fixed diff --git a/actix-http/benches/write-camel-case.rs b/actix-http/benches/write-camel-case.rs index fa4930eb9..ccf09b37e 100644 --- a/actix-http/benches/write-camel-case.rs +++ b/actix-http/benches/write-camel-case.rs @@ -18,7 +18,8 @@ fn bench_write_camel_case(c: &mut Criterion) { group.bench_with_input(BenchmarkId::new("New", i), bts, |b, bts| { b.iter(|| { let mut buf = black_box([0; 24]); - _new::write_camel_case(black_box(bts), &mut buf) + let len = black_box(bts.len()); + _new::write_camel_case(black_box(bts), buf.as_mut_ptr(), len) }); }); } @@ -30,9 +31,12 @@ criterion_group!(benches, bench_write_camel_case); criterion_main!(benches); mod _new { - pub fn write_camel_case(value: &[u8], buffer: &mut [u8]) { + pub fn write_camel_case(value: &[u8], buf: *mut u8, len: usize) { // first copy entire (potentially wrong) slice to output - buffer[..value.len()].copy_from_slice(value); + let buffer = unsafe { + std::ptr::copy_nonoverlapping(value.as_ptr(), buf, len); + std::slice::from_raw_parts_mut(buf, len) + }; let mut iter = value.iter(); diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index 4e5c9d238..5e1d47785 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -175,7 +175,7 @@ pub(crate) trait MessageType: Sized { unsafe { if camel_case { // use Camel-Case headers - write_camel_case(k, from_raw_parts_mut(buf, k_len)); + write_camel_case(k, buf, k_len); } else { write_data(k, buf, k_len); } @@ -473,15 +473,22 @@ impl TransferEncoding { } /// # Safety -/// Callers must ensure that the given length matches given value length. +/// Callers must ensure that the given `len` matches the given `value` length and that `buf` is +/// valid for writes of at least `len` bytes. unsafe fn write_data(value: &[u8], buf: *mut u8, len: usize) { debug_assert_eq!(value.len(), len); copy_nonoverlapping(value.as_ptr(), buf, len); } -fn write_camel_case(value: &[u8], buffer: &mut [u8]) { +/// # Safety +/// Callers must ensure that the given `len` matches the given `value` length and that `buf` is +/// valid for writes of at least `len` bytes. +unsafe fn write_camel_case(value: &[u8], buf: *mut u8, len: usize) { // first copy entire (potentially wrong) slice to output - buffer[..value.len()].copy_from_slice(value); + write_data(value, buf, len); + + // SAFETY: We just initialized the buffer with `value` + let buffer = from_raw_parts_mut(buf, len); let mut iter = value.iter(); From 5f412c67db4c65dba51942bd098b58acc8fae035 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 13 Aug 2021 18:49:58 +0100 Subject: [PATCH 32/88] clippy --- actix-files/src/error.rs | 1 + actix-http/src/header/map.rs | 2 +- actix-http/src/lib.rs | 2 +- actix-http/src/message.rs | 4 ++-- actix-router/src/path.rs | 6 +++--- actix-router/src/resource.rs | 6 +++--- src/http/header/content_disposition.rs | 2 +- src/middleware/logger.rs | 2 +- src/request.rs | 4 ++-- src/service.rs | 2 +- src/types/either.rs | 4 ++-- src/types/json.rs | 2 +- 12 files changed, 19 insertions(+), 18 deletions(-) diff --git a/actix-files/src/error.rs b/actix-files/src/error.rs index e5f2d4779..f8e32eef7 100644 --- a/actix-files/src/error.rs +++ b/actix-files/src/error.rs @@ -21,6 +21,7 @@ impl ResponseError for FilesError { } } +#[allow(clippy::enum_variant_names)] #[derive(Display, Debug, PartialEq)] pub enum UriSegmentError { /// The segment started with the wrapped invalid character. diff --git a/actix-http/src/header/map.rs b/actix-http/src/header/map.rs index 634d9282f..a8fd9715b 100644 --- a/actix-http/src/header/map.rs +++ b/actix-http/src/header/map.rs @@ -684,7 +684,7 @@ impl<'a> Iterator for Iter<'a> { fn next(&mut self) -> Option { // handle in-progress multi value lists first - if let Some((ref name, ref mut vals)) = self.multi_inner { + if let Some((name, ref mut vals)) = self.multi_inner { match vals.get(self.multi_idx) { Some(val) => { self.multi_idx += 1; diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index d22e1ee44..17ee3ff29 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -14,7 +14,7 @@ //! [rustls]: https://crates.io/crates/rustls //! [trust-dns]: https://crates.io/crates/trust-dns -#![deny(rust_2018_idioms, nonstandard_style)] +#![deny(rust_2018_idioms, nonstandard_style, clippy::uninit_assumed_init)] #![allow( clippy::type_complexity, clippy::too_many_arguments, diff --git a/actix-http/src/message.rs b/actix-http/src/message.rs index e85d686b7..84125fb3a 100644 --- a/actix-http/src/message.rs +++ b/actix-http/src/message.rs @@ -209,7 +209,7 @@ impl RequestHeadType { impl AsRef for RequestHeadType { fn as_ref(&self) -> &RequestHead { match self { - RequestHeadType::Owned(head) => &head, + RequestHeadType::Owned(head) => head, RequestHeadType::Rc(head, _) => head.as_ref(), } } @@ -363,7 +363,7 @@ impl std::ops::Deref for Message { type Target = T; fn deref(&self) -> &Self::Target { - &self.head.as_ref() + self.head.as_ref() } } diff --git a/actix-router/src/path.rs b/actix-router/src/path.rs index e29591f96..9af7b0b8b 100644 --- a/actix-router/src/path.rs +++ b/actix-router/src/path.rs @@ -125,7 +125,7 @@ impl Path { for (seg_name, val) in self.segments.iter() { if name == seg_name { return match val { - PathItem::Static(ref s) => Some(&s), + PathItem::Static(ref s) => Some(s), PathItem::Segment(s, e) => { Some(&self.path.path()[(*s as usize)..(*e as usize)]) } @@ -183,7 +183,7 @@ impl<'a, T: ResourcePath> Iterator for PathIter<'a, T> { if self.idx < self.params.segment_count() { let idx = self.idx; let res = match self.params.segments[idx].1 { - PathItem::Static(ref s) => &s, + PathItem::Static(ref s) => s, PathItem::Segment(s, e) => &self.params.path.path()[(s as usize)..(e as usize)], }; self.idx += 1; @@ -207,7 +207,7 @@ impl Index for Path { fn index(&self, idx: usize) -> &str { match self.segments[idx].1 { - PathItem::Static(ref s) => &s, + PathItem::Static(ref s) => s, PathItem::Segment(s, e) => &self.path.path()[(s as usize)..(e as usize)], } } diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs index 61ff587a5..69e10b2bd 100644 --- a/actix-router/src/resource.rs +++ b/actix-router/src/resource.rs @@ -276,7 +276,7 @@ impl ResourceDef { let mut pattern_data = Vec::new(); for pattern in &patterns { - match ResourceDef::parse(&pattern, false, true) { + match ResourceDef::parse(pattern, false, true) { (PatternType::Dynamic(re, names), _) => { re_set.push(re.as_str().to_owned()); pattern_data.push((re, names)); @@ -790,7 +790,7 @@ impl ResourceDef { profile_section!(pattern_dynamic_extract_captures); for (no, name) in names.iter().enumerate() { - if let Some(m) = captures.name(&name) { + if let Some(m) = captures.name(name) { segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); } else { log::error!( @@ -820,7 +820,7 @@ impl ResourceDef { }; for (no, name) in names.iter().enumerate() { - if let Some(m) = captures.name(&name) { + if let Some(m) = captures.name(name) { segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); } else { log::error!("Dynamic path match but not all segments found: {}", name); diff --git a/src/http/header/content_disposition.rs b/src/http/header/content_disposition.rs index 9f67baffb..6e75fde92 100644 --- a/src/http/header/content_disposition.rs +++ b/src/http/header/content_disposition.rs @@ -457,7 +457,7 @@ impl Header for ContentDisposition { fn parse(msg: &T) -> Result { if let Some(h) = msg.headers().get(&Self::name()) { - Self::from_raw(&h) + Self::from_raw(h) } else { Err(crate::error::ParseError::Header) } diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index bbb0e3dc4..0f09b6ad6 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -553,7 +553,7 @@ impl FormatText { *self = FormatText::Str(s.to_string()); } FormatText::RemoteAddr => { - let s = if let Some(ref peer) = req.connection_info().remote_addr() { + let s = if let Some(peer) = req.connection_info().remote_addr() { FormatText::Str((*peer).to_string()) } else { FormatText::Str("-".to_string()) diff --git a/src/request.rs b/src/request.rs index 41c8252a8..59850b4ca 100644 --- a/src/request.rs +++ b/src/request.rs @@ -184,7 +184,7 @@ impl HttpRequest { U: IntoIterator, I: AsRef, { - self.resource_map().url_for(&self, name, elements) + self.resource_map().url_for(self, name, elements) } /// Generate url for named resource @@ -199,7 +199,7 @@ impl HttpRequest { #[inline] /// Get a reference to a `ResourceMap` of current application. pub fn resource_map(&self) -> &ResourceMap { - &self.app_state().rmap() + self.app_state().rmap() } /// Peer socket address. diff --git a/src/service.rs b/src/service.rs index 148199407..48167e5b3 100644 --- a/src/service.rs +++ b/src/service.rs @@ -117,7 +117,7 @@ impl ServiceRequest { /// This method returns reference to the request head #[inline] pub fn head(&self) -> &RequestHead { - &self.req.head() + self.req.head() } /// This method returns reference to the request head diff --git a/src/types/either.rs b/src/types/either.rs index d3b003587..35e63cec9 100644 --- a/src/types/either.rs +++ b/src/types/either.rs @@ -253,7 +253,7 @@ where Ok(bytes) => { let fallback = bytes.clone(); let left = - L::from_request(&this.req, &mut payload_from_bytes(bytes)); + L::from_request(this.req, &mut payload_from_bytes(bytes)); EitherExtractState::Left { left, fallback } } Err(err) => break Err(EitherExtractError::Bytes(err)), @@ -265,7 +265,7 @@ where Ok(extracted) => break Ok(Either::Left(extracted)), Err(left_err) => { let right = R::from_request( - &this.req, + this.req, &mut payload_from_bytes(mem::take(fallback)), ); EitherExtractState::Right { diff --git a/src/types/json.rs b/src/types/json.rs index fc02c8854..ab9708c53 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -425,7 +425,7 @@ where } } None => { - let json = serde_json::from_slice::(&buf) + let json = serde_json::from_slice::(buf) .map_err(JsonPayloadError::Deserialize)?; return Poll::Ready(Ok(json)); } From ff07816b650997b0050811c1fd300c0da1104b59 Mon Sep 17 00:00:00 2001 From: fakeshadow <24548779@qq.com> Date: Sun, 29 Aug 2021 08:42:22 +0800 Subject: [PATCH 33/88] update httparse for uninit header parsing (#2374) --- actix-http/Cargo.toml | 2 +- actix-http/src/h1/decoder.rs | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 4ce55dca1..68f980982 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -59,7 +59,7 @@ futures-core = { version = "0.3.7", default-features = false, features = ["alloc futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] } h2 = "0.3.1" http = "0.2.2" -httparse = "1.3" +httparse = "1.5.1" itoa = "0.4" language-tags = "0.3" local-channel = "0.1" diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index 313ffd5e0..91a3af44f 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -1,4 +1,4 @@ -use std::{convert::TryFrom, io, marker::PhantomData, task::Poll}; +use std::{convert::TryFrom, io, marker::PhantomData, mem::MaybeUninit, task::Poll}; use actix_codec::Decoder; use bytes::{Bytes, BytesMut}; @@ -212,10 +212,17 @@ impl MessageType for Request { let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY; let (len, method, uri, ver, h_len) = { - let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY; + // SAFETY: + // Create an uninitialized array of `MaybeUninit`. The `assume_init` is + // safe because the type we are claiming to have initialized here is a + // bunch of `MaybeUninit`s, which do not require initialization. + let mut parsed = unsafe { + MaybeUninit::<[MaybeUninit>; MAX_HEADERS]>::uninit() + .assume_init() + }; - let mut req = httparse::Request::new(&mut parsed); - match req.parse(src)? { + let mut req = httparse::Request::new(&mut []); + match req.parse_with_uninit_headers(src, &mut parsed)? { httparse::Status::Complete(len) => { let method = Method::from_bytes(req.method.unwrap().as_bytes()) .map_err(|_| ParseError::Method)?; From f9da6e48e0aef496001528daa68298ff9107a895 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Mon, 30 Aug 2021 22:05:49 +0300 Subject: [PATCH 34/88] ResourceDef: define behavior for prefix with trailing slash (#2355) * ResourceDef: define behavior * fix tests * add scope test * revert firestorm bump * update changelog * fmt Co-authored-by: Rob Ede --- actix-files/src/files.rs | 2 +- actix-router/CHANGES.md | 3 + actix-router/src/resource.rs | 169 +++++++++++++++++++---------------- src/scope.rs | 66 ++++++++++++++ 4 files changed, 163 insertions(+), 77 deletions(-) diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs index 49d81eb03..68879822a 100644 --- a/actix-files/src/files.rs +++ b/actix-files/src/files.rs @@ -106,7 +106,7 @@ impl Files { }; Files { - path: mount_path.to_owned(), + path: mount_path.trim_end_matches('/').to_owned(), directory: dir, index: None, show_index: false, diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index dea7cb76f..140d108e2 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -5,11 +5,14 @@ * Disallow prefix routes with tail segments. [#379] * Enforce path separators on dynamic prefixes. [#378] * Improve malformed path error message. [#384] +* Prefix segments now always end with with a segment delimiter or end-of-input. [#2355] +* Prefix segments with trailing slashes define a trailing empty segment. [#2355] [#378]: https://github.com/actix/actix-net/pull/378 [#379]: https://github.com/actix/actix-net/pull/379 [#380]: https://github.com/actix/actix-net/pull/380 [#384]: https://github.com/actix/actix-net/pull/384 +[#2355]: https://github.com/actix/actix-web/pull/2355 ## 0.5.0-beta.1 - 2021-07-20 diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs index 69e10b2bd..fbf29cc7a 100644 --- a/actix-router/src/resource.rs +++ b/actix-router/src/resource.rs @@ -28,9 +28,27 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// regex engine. /// /// +/// # Pattern Format and Matching Behavior +/// +/// Resource pattern is defined as a string of zero or more _segments_ where each segment is +/// preceeded by a slash `/`. +/// +/// This means that pattern string __must__ either be empty or begin with a slash (`/`). +/// This also implies that a trailing slash in pattern defines an empty segment. +/// For example, the pattern `"/user/"` has two segments: `["user", ""]` +/// +/// A key point to undertand is that `ResourceDef` matches segments, not strings. +/// It matches segments individually. +/// For example, the pattern `/user/` is not considered a prefix for the path `/user/123/456`, +/// because the second segment doesn't match: `["user", ""]` vs `["user", "123", "456"]`. +/// +/// This definition is consistent with the definition of absolute URL path in +/// [RFC 3986 (section 3.3)](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3) +/// +/// /// # Static Resources -/// A static resource is the most basic type of definition. Pass a regular string to -/// [new][Self::new]. Conforming paths must match the string exactly. +/// A static resource is the most basic type of definition. Pass a pattern to +/// [new][Self::new]. Conforming paths must match the pattern exactly. /// /// ## Examples /// ``` @@ -39,6 +57,7 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// /// assert!(resource.is_match("/home")); /// +/// assert!(!resource.is_match("/home/")); /// assert!(!resource.is_match("/home/new")); /// assert!(!resource.is_match("/homes")); /// assert!(!resource.is_match("/search")); @@ -85,12 +104,13 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// /// /// # Prefix Resources -/// A prefix resource is defined as pattern that can match just the start of a path. +/// A prefix resource is defined as pattern that can match just the start of a path, up to a +/// segment boundary. /// -/// This library chooses to restrict that definition slightly. In particular, when matching, the -/// prefix must be separated from the remaining part of the path by a `/` character, either at the -/// end of the prefix pattern or at the start of the the remaining slice. In practice, this is not -/// much of a limitation. +/// Prefix patterns with a trailing slash may have an unexpected, though correct, behavior. +/// They define and therefore require an empty segment in order to match. Examples are given below. +/// +/// Empty pattern matches any path as a prefix. /// /// Prefix resources can contain dynamic segments. /// @@ -102,9 +122,12 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert!(resource.is_match("/home/new")); /// assert!(!resource.is_match("/homes")); /// +/// // prefix pattern with a trailing slash /// let resource = ResourceDef::prefix("/user/{id}/"); /// assert!(resource.is_match("/user/123/")); -/// assert!(resource.is_match("/user/123/stars")); +/// assert!(resource.is_match("/user/123//stars")); +/// assert!(!resource.is_match("/user/123/stars")); +/// assert!(!resource.is_match("/user/123")); /// ``` /// /// @@ -117,6 +140,10 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// `{name:regex}`. For example, `/user/{id:\d+}` will only match paths where the user ID /// is numeric. /// +/// The regex could potentially match multiple segments. If this is not wanted, then care must be +/// taken to avoid matching a slash `/`. It is guaranteed, however, that the match ends at a +/// segment boundary; the pattern `r"(/|$)` is always appended to the regex. +/// /// By default, dynamic segments use this regex: `[^/]+`. This shows why it is the case, as shown in /// the earlier section, that segments capture a slice of the path up to the next `/` character. /// @@ -298,7 +325,7 @@ impl ResourceDef { } } - /// Constructs a new resource definition using a string pattern that performs prefix matching. + /// Constructs a new resource definition using a pattern that performs prefix matching. /// /// More specifically, the regular expressions generated for matching are different when using /// this method vs using `new`; they will not be appended with the `$` meta-character that @@ -320,13 +347,6 @@ impl ResourceDef { /// assert!(!resource.is_match("user/123")); /// assert!(!resource.is_match("user/123/stars")); /// assert!(!resource.is_match("/foo")); - /// - /// let resource = ResourceDef::prefix("user/{id}"); - /// assert!(resource.is_match("user/123")); - /// assert!(resource.is_match("user/123/stars")); - /// assert!(!resource.is_match("/user/123")); - /// assert!(!resource.is_match("/user/123/stars")); - /// assert!(!resource.is_match("foo")); /// ``` pub fn prefix(path: &str) -> Self { profile_method!(prefix); @@ -591,24 +611,7 @@ impl ResourceDef { match self.pat_type { PatternType::Static(ref s) => s == path, - - PatternType::Prefix(ref prefix) if prefix == path => true, - PatternType::Prefix(ref prefix) => is_strict_prefix(prefix, path), - - // dynamic prefix - PatternType::Dynamic(ref re, _) if !re.as_str().ends_with('$') => { - match re.find(path) { - // prefix matches exactly - Some(m) if m.end() == path.len() => true, - - // prefix matches part - Some(m) => is_strict_prefix(m.as_str(), path), - - // prefix does not match - None => false, - } - } - + PatternType::Prefix(ref prefix) => is_prefix(prefix, path), PatternType::Dynamic(ref re, _) => re.is_match(path), PatternType::DynamicSet(ref re, _) => re.is_match(path), } @@ -656,30 +659,15 @@ impl ResourceDef { PatternType::Static(segment) if path == segment => Some(segment.len()), PatternType::Static(_) => None, - PatternType::Prefix(prefix) if path == prefix => Some(prefix.len()), - PatternType::Prefix(prefix) if is_strict_prefix(prefix, path) => Some(prefix.len()), + PatternType::Prefix(prefix) if is_prefix(prefix, path) => Some(prefix.len()), PatternType::Prefix(_) => None, - // dynamic prefix - PatternType::Dynamic(ref re, _) if !re.as_str().ends_with('$') => { - match re.find(path) { - // prefix matches exactly - Some(m) if m.end() == path.len() => Some(m.end()), - - // prefix matches part - Some(m) if is_strict_prefix(m.as_str(), path) => Some(m.end()), - - // prefix does not match - _ => None, - } - } - - PatternType::Dynamic(re, _) => re.find(path).map(|m| m.end()), + PatternType::Dynamic(re, _) => Some(re.captures(path)?[1].len()), PatternType::DynamicSet(re, params) => { let idx = re.matches(path).into_iter().next()?; let (ref pattern, _) = params[idx]; - pattern.find(path).map(|m| m.end()) + Some(pattern.captures(path)?[1].len()) } } } @@ -802,7 +790,7 @@ impl ResourceDef { } }; - (captures[0].len(), Some(names)) + (captures[1].len(), Some(names)) } PatternType::DynamicSet(re, params) => { @@ -828,7 +816,7 @@ impl ResourceDef { } } - (captures[0].len(), Some(names)) + (captures[1].len(), Some(names)) } }; @@ -1112,8 +1100,16 @@ impl ResourceDef { ); } - if !is_prefix && !has_tail_segment { - re.push('$'); + // Store the pattern in capture group #1 to have context info outside it + let mut re = format!("({})", re); + + // Ensure the match ends at a segment boundary + if !has_tail_segment { + if is_prefix { + re.push_str(r"(/|$)"); + } else { + re.push('$'); + } } let re = match Regex::new(&re) { @@ -1185,10 +1181,12 @@ pub(crate) fn insert_slash(path: &str) -> Cow<'_, str> { } /// Returns true if `prefix` acts as a proper prefix (i.e., separated by a slash) in `path`. -/// -/// The `strict` refers to the fact that this will return `false` if `prefix == path`. -fn is_strict_prefix(prefix: &str, path: &str) -> bool { - path.starts_with(prefix) && (prefix.ends_with('/') || path[prefix.len()..].starts_with('/')) +fn is_prefix(prefix: &str, path: &str) -> bool { + match path.strip_prefix(prefix) { + // Ensure the match ends at segment boundary + Some(rem) if rem.is_empty() || rem.starts_with('/') => true, + _ => false, + } } #[cfg(test)] @@ -1501,54 +1499,70 @@ mod tests { let re = ResourceDef::prefix("/name/"); assert!(re.is_match("/name/")); - assert!(re.is_match("/name/gs")); + assert!(re.is_match("/name//gs")); + assert!(!re.is_match("/name/gs")); assert!(!re.is_match("/name")); let mut path = Path::new("/name/gs"); + assert!(!re.capture_match_info(&mut path)); + + let mut path = Path::new("/name//gs"); assert!(re.capture_match_info(&mut path)); - assert_eq!(path.unprocessed(), "gs"); + assert_eq!(path.unprocessed(), "/gs"); let re = ResourceDef::root_prefix("name/"); assert!(re.is_match("/name/")); - assert!(re.is_match("/name/gs")); + assert!(re.is_match("/name//gs")); + assert!(!re.is_match("/name/gs")); assert!(!re.is_match("/name")); let mut path = Path::new("/name/gs"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.unprocessed(), "gs"); + assert!(!re.capture_match_info(&mut path)); } #[test] fn prefix_dynamic() { - let re = ResourceDef::prefix("/{name}/"); + let re = ResourceDef::prefix("/{name}"); assert!(re.is_prefix()); assert!(re.is_match("/name/")); assert!(re.is_match("/name/gs")); - assert!(!re.is_match("/name")); + assert!(re.is_match("/name")); - assert_eq!(re.find_match("/name/"), Some(6)); - assert_eq!(re.find_match("/name/gs"), Some(6)); - assert_eq!(re.find_match("/name"), None); + assert_eq!(re.find_match("/name/"), Some(5)); + assert_eq!(re.find_match("/name/gs"), Some(5)); + assert_eq!(re.find_match("/name"), Some(5)); + assert_eq!(re.find_match(""), None); let mut path = Path::new("/test2/"); assert!(re.capture_match_info(&mut path)); assert_eq!(&path["name"], "test2"); assert_eq!(&path[0], "test2"); - assert_eq!(path.unprocessed(), ""); + assert_eq!(path.unprocessed(), "/"); let mut path = Path::new("/test2/subpath1/subpath2/index.html"); assert!(re.capture_match_info(&mut path)); assert_eq!(&path["name"], "test2"); assert_eq!(&path[0], "test2"); - assert_eq!(path.unprocessed(), "subpath1/subpath2/index.html"); + assert_eq!(path.unprocessed(), "/subpath1/subpath2/index.html"); let resource = ResourceDef::prefix("/user"); // input string shorter than prefix assert!(resource.find_match("/foo").is_none()); } + #[test] + fn prefix_empty() { + let re = ResourceDef::prefix(""); + + assert!(re.is_prefix()); + + assert!(re.is_match("")); + assert!(re.is_match("/")); + assert!(re.is_match("/name/test/test")); + } + #[test] fn build_path_list() { let mut s = String::new(); @@ -1667,14 +1681,17 @@ mod tests { } #[test] - fn consistent_match_length() { - let result = Some(5); + fn prefix_trailing_slash() { + // The prefix "/abc/" matches two segments: ["user", ""] + // These are not prefixes let re = ResourceDef::prefix("/abc/"); - assert_eq!(re.find_match("/abc/def"), result); + assert_eq!(re.find_match("/abc/def"), None); + assert_eq!(re.find_match("/abc//def"), Some(5)); let re = ResourceDef::prefix("/{id}/"); - assert_eq!(re.find_match("/abc/def"), result); + assert_eq!(re.find_match("/abc/def"), None); + assert_eq!(re.find_match("/abc//def"), Some(5)); } #[test] diff --git a/src/scope.rs b/src/scope.rs index 97db53eeb..b2edaedab 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1153,4 +1153,70 @@ mod tests { Bytes::from_static(b"http://localhost:8080/a/b/c/12345") ); } + + #[actix_rt::test] + async fn dynamic_scopes() { + let srv = init_service( + App::new().service( + web::scope("/{a}/").service( + web::scope("/{b}/") + .route("", web::get().to(|_: HttpRequest| HttpResponse::Created())) + .route( + "/", + web::get().to(|_: HttpRequest| HttpResponse::Accepted()), + ) + .route("/{c}", web::get().to(|_: HttpRequest| HttpResponse::Ok())), + ), + ), + ) + .await; + + // note the unintuitive behavior with trailing slashes on scopes with dynamic segments + let req = TestRequest::with_uri("/a//b//c").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/a//b/").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::CREATED); + + let req = TestRequest::with_uri("/a//b//").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::ACCEPTED); + + let req = TestRequest::with_uri("/a//b//c/d").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let srv = init_service( + App::new().service( + web::scope("/{a}").service( + web::scope("/{b}") + .route("", web::get().to(|_: HttpRequest| HttpResponse::Created())) + .route( + "/", + web::get().to(|_: HttpRequest| HttpResponse::Accepted()), + ) + .route("/{c}", web::get().to(|_: HttpRequest| HttpResponse::Ok())), + ), + ), + ) + .await; + + let req = TestRequest::with_uri("/a/b/c").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/a/b").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::CREATED); + + let req = TestRequest::with_uri("/a/b/").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::ACCEPTED); + + let req = TestRequest::with_uri("/a/b/c/d").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } } From 4bb32fb19b5fb4105e7ca2b7371557d0d21b0346 Mon Sep 17 00:00:00 2001 From: Sam De Roeck <31270289+sadroeck@users.noreply.github.com> Date: Mon, 30 Aug 2021 21:07:12 +0200 Subject: [PATCH 35/88] [fix] Bump actix-http dependency to 3.0.0-beta.9, up from 3.0.0-beta.8 (#2360) Fixes https://rustsec.org/advisories/RUSTSEC-2021-0081 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ff3321f47..f2ce46ee1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,7 +76,7 @@ actix-utils = "3.0.0" actix-tls = { version = "3.0.0-beta.5", default-features = false, optional = true } actix-web-codegen = "0.5.0-beta.2" -actix-http = "3.0.0-beta.8" +actix-http = "3.0.0-beta.9" ahash = "0.7" bytes = "1" From 168b2f227d1959252dc47518641da7d214a81ed3 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Tue, 31 Aug 2021 02:20:40 +0530 Subject: [PATCH 36/88] compile time validation of path (#2350) * compile time validation of path * added trybuild err message * Update Cargo.toml * add changelog entry * test more cases of path validation * fmt Co-authored-by: Rob Ede --- actix-router/src/resource.rs | 5 ++- actix-web-codegen/CHANGES.md | 3 ++ actix-web-codegen/Cargo.toml | 1 + actix-web-codegen/src/route.rs | 2 + actix-web-codegen/tests/trybuild.rs | 1 + .../trybuild/route-malformed-path-fail.rs | 33 +++++++++++++++ .../trybuild/route-malformed-path-fail.stderr | 42 +++++++++++++++++++ 7 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 actix-web-codegen/tests/trybuild/route-malformed-path-fail.rs create mode 100644 actix-web-codegen/tests/trybuild/route-malformed-path-fail.stderr diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs index fbf29cc7a..57ce36804 100644 --- a/actix-router/src/resource.rs +++ b/actix-router/src/resource.rs @@ -967,7 +967,10 @@ impl ResourceDef { _ => false, }) .unwrap_or_else(|| { - panic!(r#"path "{}" contains malformed dynamic segment"#, pattern) + panic!( + r#"pattern "{}" contains malformed dynamic segment"#, + pattern + ) }); let (mut param, mut unprocessed) = pattern.split_at(close_idx + 1); diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index a8a901f72..4fd393b4d 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx +* In routing macros, paths are now validated at compile time. [#2350] + +[#2350]: https://github.com/actix/actix-web/pull/2350 ## 0.5.0-beta.3 - 2021-06-17 diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 4d0fd5e26..66f7acf6d 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -17,6 +17,7 @@ proc-macro = true quote = "1" syn = { version = "1", features = ["full", "parsing"] } proc-macro2 = "1" +actix-router = "0.5.0-beta.1" [dev-dependencies] actix-rt = "2.2" diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index 747042527..c2f851a0e 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -3,6 +3,7 @@ extern crate proc_macro; use std::collections::HashSet; use std::convert::TryFrom; +use actix_router::ResourceDef; use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{format_ident, quote, ToTokens, TokenStreamExt}; @@ -101,6 +102,7 @@ impl Args { match arg { NestedMeta::Lit(syn::Lit::Str(lit)) => match path { None => { + let _ = ResourceDef::new(lit.value()); path = Some(lit); } _ => { diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs index 12e848cf3..c97211e9f 100644 --- a/actix-web-codegen/tests/trybuild.rs +++ b/actix-web-codegen/tests/trybuild.rs @@ -10,6 +10,7 @@ fn compile_macros() { t.compile_fail("tests/trybuild/route-missing-method-fail.rs"); t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs"); t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs"); + t.compile_fail("tests/trybuild/route-malformed-path-fail.rs"); t.pass("tests/trybuild/docstring-ok.rs"); } diff --git a/actix-web-codegen/tests/trybuild/route-malformed-path-fail.rs b/actix-web-codegen/tests/trybuild/route-malformed-path-fail.rs new file mode 100644 index 000000000..1258a6f2f --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-malformed-path-fail.rs @@ -0,0 +1,33 @@ +use actix_web_codegen::get; + +#[get("/{")] +async fn zero() -> &'static str { + "malformed resource def" +} + +#[get("/{foo")] +async fn one() -> &'static str { + "malformed resource def" +} + +#[get("/{}")] +async fn two() -> &'static str { + "malformed resource def" +} + +#[get("/*")] +async fn three() -> &'static str { + "malformed resource def" +} + +#[get("/{tail:\\d+}*")] +async fn four() -> &'static str { + "malformed resource def" +} + +#[get("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}")] +async fn five() -> &'static str { + "malformed resource def" +} + +fn main() {} diff --git a/actix-web-codegen/tests/trybuild/route-malformed-path-fail.stderr b/actix-web-codegen/tests/trybuild/route-malformed-path-fail.stderr new file mode 100644 index 000000000..93c510109 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-malformed-path-fail.stderr @@ -0,0 +1,42 @@ +error: custom attribute panicked + --> $DIR/route-malformed-path-fail.rs:3:1 + | +3 | #[get("/{")] + | ^^^^^^^^^^^^ + | + = help: message: pattern "{" contains malformed dynamic segment + +error: custom attribute panicked + --> $DIR/route-malformed-path-fail.rs:8:1 + | +8 | #[get("/{foo")] + | ^^^^^^^^^^^^^^^ + | + = help: message: pattern "{foo" contains malformed dynamic segment + +error: custom attribute panicked + --> $DIR/route-malformed-path-fail.rs:13:1 + | +13 | #[get("/{}")] + | ^^^^^^^^^^^^^ + | + = help: message: Wrong path pattern: "/{}" regex parse error: + ((?s-m)^/(?P<>[^/]+))$ + ^ + error: empty capture group name + +error: custom attribute panicked + --> $DIR/route-malformed-path-fail.rs:23:1 + | +23 | #[get("/{tail:\\d+}*")] + | ^^^^^^^^^^^^^^^^^^^^^^^ + | + = help: message: custom regex is not supported for tail match + +error: custom attribute panicked + --> $DIR/route-malformed-path-fail.rs:28:1 + | +28 | #[get("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = help: message: Only 16 dynamic segments are allowed, provided: 17 From 5128b1bdfc0c47fc744f2bc1f417ef5fd0e7f3c1 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 30 Aug 2021 23:19:03 +0100 Subject: [PATCH 37/88] bump msrv to 1.51 --- .github/workflows/ci.yml | 2 +- CHANGES.md | 3 +++ README.md | 4 ++-- actix-files/CHANGES.md | 1 + actix-files/README.md | 4 ++-- actix-http-test/CHANGES.md | 1 + actix-http-test/README.md | 4 ++-- actix-http/CHANGES.md | 3 +++ actix-http/README.md | 4 ++-- actix-http/src/body/mod.rs | 2 +- actix-http/src/h1/chunked.rs | 8 ++++---- actix-http/src/h1/dispatcher.rs | 2 +- actix-multipart/CHANGES.md | 1 + actix-multipart/README.md | 4 ++-- actix-router/CHANGES.md | 1 + actix-test/CHANGES.md | 1 + actix-web-actors/CHANGES.md | 1 + actix-web-actors/README.md | 4 ++-- actix-web-codegen/CHANGES.md | 1 + actix-web-codegen/README.md | 4 ++-- actix-web-codegen/tests/trybuild.rs | 2 +- awc/README.md | 2 +- clippy.toml | 2 +- src/lib.rs | 2 +- src/responder.rs | 6 +++--- src/types/query.rs | 4 ++-- 26 files changed, 43 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22b92759a..221d2fb40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } - { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc } version: - - 1.46.0 # MSRV + - 1.51.0 # MSRV - stable - nightly diff --git a/CHANGES.md b/CHANGES.md index 88295ec12..5325caf48 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,9 @@ ### Added * Re-export actix-service `ServiceFactory` in `dev` module. [#2325] +### Changes +* Minimum supported Rust version (MSRV) is now 1.51. + [#2325]: https://github.com/actix/actix-web/pull/2325 diff --git a/README.md b/README.md index 309a18466..33784d66a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.8)](https://docs.rs/actix-web/4.0.0-beta.8) -[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) +[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) [![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.8/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.8)
@@ -32,7 +32,7 @@ * SSL support using OpenSSL or Rustls * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) * Includes an async [HTTP client](https://docs.rs/awc/) -* Runs on stable Rust 1.46+ +* Runs on stable Rust 1.51+ ## Documentation diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index db047c44c..533f72291 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.51. ## 0.6.0-beta.6 - 2021-06-26 diff --git a/actix-files/README.md b/actix-files/README.md index 13c301c56..5815ef563 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) [![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.6)](https://docs.rs/actix-files/0.6.0-beta.6) -[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) +[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![License](https://img.shields.io/crates/l/actix-files.svg)
[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.6/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.6) @@ -15,4 +15,4 @@ - [API Documentation](https://docs.rs/actix-files/) - [Example Project](https://github.com/actix/examples/tree/master/basics/static_index) -- Minimum supported Rust version: 1.46 or later +- Minimum supported Rust version: 1.51 or later diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index 1dbd9a15b..39b6a3a66 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.51. ## 3.0.0-beta.4 - 2021-04-02 diff --git a/actix-http-test/README.md b/actix-http-test/README.md index 74260a352..099fb385d 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) [![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.4)](https://docs.rs/actix-http-test/3.0.0-beta.4) -[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) +[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.4/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.4) @@ -14,4 +14,4 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-http-test) -- Minimum Supported Rust Version (MSRV): 1.46.0 +- Minimum Supported Rust Version (MSRV): 1.51.0 diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 9ed28105f..57c09d2d8 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,12 +1,15 @@ # Changes ## Unreleased - 2021-xx-xx +### Changes +* Minimum supported Rust version (MSRV) is now 1.51. ### Fixed * Remove slice creation pointing to potential uninitialized data on h1 encoder. [#2364] [#2364]: https://github.com/actix/actix-web/pull/2364 + ## 3.0.0-beta.8 - 2021-08-09 ### Fixed * Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977) diff --git a/actix-http/README.md b/actix-http/README.md index 5b06583bc..c509eaff8 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) [![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.9)](https://docs.rs/actix-http/3.0.0-beta.9) -[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) +[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.9/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.9) @@ -14,7 +14,7 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-http) -- Minimum Supported Rust Version (MSRV): 1.46.0 +- Minimum Supported Rust Version (MSRV): 1.51.0 ## Example diff --git a/actix-http/src/body/mod.rs b/actix-http/src/body/mod.rs index 8a08dbd2b..a60a8895c 100644 --- a/actix-http/src/body/mod.rs +++ b/actix-http/src/body/mod.rs @@ -80,7 +80,7 @@ mod tests { impl Body { pub(crate) fn get_ref(&self) -> &[u8] { match *self { - Body::Bytes(ref bin) => &bin, + Body::Bytes(ref bin) => bin, _ => panic!(), } } diff --git a/actix-http/src/h1/chunked.rs b/actix-http/src/h1/chunked.rs index 1224ce08c..e5b734fff 100644 --- a/actix-http/src/h1/chunked.rs +++ b/actix-http/src/h1/chunked.rs @@ -40,7 +40,7 @@ impl ChunkedState { Size => ChunkedState::read_size(body, size), SizeLws => ChunkedState::read_size_lws(body), Extension => ChunkedState::read_extension(body), - SizeLf => ChunkedState::read_size_lf(body, size), + SizeLf => ChunkedState::read_size_lf(body, *size), Body => ChunkedState::read_body(body, size, buf), BodyCr => ChunkedState::read_body_cr(body), BodyLf => ChunkedState::read_body_lf(body), @@ -113,11 +113,11 @@ impl ChunkedState { } fn read_size_lf( rdr: &mut BytesMut, - size: &mut u64, + size: u64, ) -> Poll> { match byte!(rdr) { - b'\n' if *size > 0 => Poll::Ready(Ok(ChunkedState::Body)), - b'\n' if *size == 0 => Poll::Ready(Ok(ChunkedState::EndCr)), + b'\n' if size > 0 => Poll::Ready(Ok(ChunkedState::Body)), + b'\n' if size == 0 => Poll::Ready(Ok(ChunkedState::EndCr)), _ => Poll::Ready(Err(io::Error::new( io::ErrorKind::InvalidInput, "Invalid chunk size LF", diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index deb25763c..aef765b89 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -1060,7 +1060,7 @@ mod tests { fn stabilize_date_header(payload: &mut [u8]) { let mut from = 0; - while let Some(pos) = find_slice(&payload, b"date", from) { + while let Some(pos) = find_slice(payload, b"date", from) { payload[(from + pos)..(from + pos + 35)] .copy_from_slice(b"date: Thu, 01 Jan 1970 12:34:56 UTC"); from += 35; diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 0b6affa3c..1e768ddf5 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.51. ## 0.4.0-beta.5 - 2021-06-17 diff --git a/actix-multipart/README.md b/actix-multipart/README.md index 78855b815..aed16721c 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) [![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.5)](https://docs.rs/actix-multipart/0.4.0-beta.5) -[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) +[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.5/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.5) @@ -14,4 +14,4 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-multipart) -- Minimum Supported Rust Version (MSRV): 1.46.0 +- Minimum Supported Rust Version (MSRV): 1.51.0 diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index 140d108e2..804f7778d 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -7,6 +7,7 @@ * Improve malformed path error message. [#384] * Prefix segments now always end with with a segment delimiter or end-of-input. [#2355] * Prefix segments with trailing slashes define a trailing empty segment. [#2355] +* Minimum supported Rust version (MSRV) is now 1.51. [#378]: https://github.com/actix/actix-net/pull/378 [#379]: https://github.com/actix/actix-net/pull/379 diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index fa554ba2e..dc76ba3fd 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.51. ## 0.1.0-beta.3 - 2021-06-20 diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index bf642ef95..084e7b272 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.51. ## 4.0.0-beta.6 - 2021-06-26 diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md index 5f8f78bde..2858d3f20 100644 --- a/actix-web-actors/README.md +++ b/actix-web-actors/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) [![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.6)](https://docs.rs/actix-web-actors/4.0.0-beta.6) -[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) +[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.6/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.6) @@ -14,4 +14,4 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-web-actors) -- Minimum supported Rust version: 1.46 or later +- Minimum supported Rust version: 1.51 or later diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index 4fd393b4d..f0a56b30f 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -2,6 +2,7 @@ ## Unreleased - 2021-xx-xx * In routing macros, paths are now validated at compile time. [#2350] +* Minimum supported Rust version (MSRV) is now 1.51. [#2350]: https://github.com/actix/actix-web/pull/2350 diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index 96e4cb51f..e69cfbbe5 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) [![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.3)](https://docs.rs/actix-web-codegen/0.5.0-beta.3) -[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) +[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.3/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.3) @@ -14,7 +14,7 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-web-codegen) -- Minimum supported Rust version: 1.46 or later. +- Minimum supported Rust version: 1.51 or later. ## Compile Testing diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs index c97211e9f..54bc1caec 100644 --- a/actix-web-codegen/tests/trybuild.rs +++ b/actix-web-codegen/tests/trybuild.rs @@ -1,4 +1,4 @@ -#[rustversion::stable(1.46)] // MSRV +#[rustversion::stable(1.51)] // MSRV #[test] fn compile_macros() { let t = trybuild::TestCases::new(); diff --git a/awc/README.md b/awc/README.md index dd08c6e10..fe91383ca 100644 --- a/awc/README.md +++ b/awc/README.md @@ -12,7 +12,7 @@ - [API Documentation](https://docs.rs/awc) - [Example Project](https://github.com/actix/examples/tree/HEAD/security/awc_https) -- Minimum Supported Rust Version (MSRV): 1.46.0 +- Minimum Supported Rust Version (MSRV): 1.51.0 ## Example diff --git a/clippy.toml b/clippy.toml index eb66960ac..829dd1c59 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -msrv = "1.46" +msrv = "1.51" diff --git a/src/lib.rs b/src/lib.rs index 714c759cf..e7cf46361 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,7 +53,7 @@ //! * SSL support using OpenSSL or Rustls //! * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) //! * Includes an async [HTTP client](https://docs.rs/awc/) -//! * Runs on stable Rust 1.46+ +//! * Runs on stable Rust 1.51+ //! //! # Crate Features //! * `cookies` - cookies support (enabled by default) diff --git a/src/responder.rs b/src/responder.rs index c5852a501..005bff03e 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -270,7 +270,7 @@ pub(crate) mod tests { impl BodyTest for Body { fn bin_ref(&self) -> &[u8] { match self { - Body::Bytes(ref bin) => &bin, + Body::Bytes(ref bin) => bin, _ => unreachable!("bug in test impl"), } } @@ -283,11 +283,11 @@ pub(crate) mod tests { fn bin_ref(&self) -> &[u8] { match self { ResponseBody::Body(ref b) => match b { - Body::Bytes(ref bin) => &bin, + Body::Bytes(ref bin) => bin, _ => unreachable!("bug in test impl"), }, ResponseBody::Other(ref b) => match b { - Body::Bytes(ref bin) => &bin, + Body::Bytes(ref bin) => bin, _ => unreachable!("bug in test impl"), }, } diff --git a/src/types/query.rs b/src/types/query.rs index 8762547e6..1e6f1111f 100644 --- a/src/types/query.rs +++ b/src/types/query.rs @@ -213,10 +213,10 @@ mod tests { #[actix_rt::test] async fn test_service_request_extract() { let req = TestRequest::with_uri("/name/user1/").to_srv_request(); - assert!(Query::::from_query(&req.query_string()).is_err()); + assert!(Query::::from_query(req.query_string()).is_err()); let req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request(); - let mut s = Query::::from_query(&req.query_string()).unwrap(); + let mut s = Query::::from_query(req.query_string()).unwrap(); assert_eq!(s.id, "test"); assert_eq!( From ae35e69382805164704d8d7c79f41c85089b3d36 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 31 Aug 2021 02:52:29 +0100 Subject: [PATCH 38/88] use rust 1.51 features --- Cargo.toml | 1 + actix-http/src/body/body.rs | 24 +++++++++--------------- actix-http/src/body/response_body.rs | 9 ++------- actix-http/src/encoding/encoder.rs | 13 ++----------- actix-http/src/h1/utils.rs | 5 +---- src/middleware/logger.rs | 1 - 6 files changed, 15 insertions(+), 38 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f2ce46ee1..cee401363 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ name = "actix_web" path = "src/lib.rs" [workspace] +resolver = "2" members = [ ".", "awc", diff --git a/actix-http/src/body/body.rs b/actix-http/src/body/body.rs index f04837d07..cd3e4c5c4 100644 --- a/actix-http/src/body/body.rs +++ b/actix-http/src/body/body.rs @@ -7,7 +7,7 @@ use std::{ }; use bytes::{Bytes, BytesMut}; -use futures_core::{ready, Stream}; +use futures_core::Stream; use crate::error::Error; @@ -74,14 +74,10 @@ impl MessageBody for AnyBody { } } - // TODO: MSRV 1.51: poll_map_err - AnyBody::Message(body) => match ready!(body.as_pin_mut().poll_next(cx)) { - Some(Err(err)) => { - Poll::Ready(Some(Err(Error::new_body().with_cause(err)))) - } - Some(Ok(val)) => Poll::Ready(Some(Ok(val))), - None => Poll::Ready(None), - }, + AnyBody::Message(body) => body + .as_pin_mut() + .poll_next(cx) + .map_err(|err| Error::new_body().with_cause(err)), } } } @@ -223,11 +219,9 @@ impl MessageBody for BoxAnyBody { mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll>> { - // TODO: MSRV 1.51: poll_map_err - match ready!(self.0.as_mut().poll_next(cx)) { - Some(Err(err)) => Poll::Ready(Some(Err(Error::new_body().with_cause(err)))), - Some(Ok(val)) => Poll::Ready(Some(Ok(val))), - None => Poll::Ready(None), - } + self.0 + .as_mut() + .poll_next(cx) + .map_err(|err| Error::new_body().with_cause(err)) } } diff --git a/actix-http/src/body/response_body.rs b/actix-http/src/body/response_body.rs index 855c742f2..699ea9384 100644 --- a/actix-http/src/body/response_body.rs +++ b/actix-http/src/body/response_body.rs @@ -5,7 +5,7 @@ use std::{ }; use bytes::Bytes; -use futures_core::{ready, Stream}; +use futures_core::Stream; use pin_project::pin_project; use crate::error::Error; @@ -77,12 +77,7 @@ where cx: &mut Context<'_>, ) -> Poll> { match self.project() { - // TODO: MSRV 1.51: poll_map_err - ResponseBodyProj::Body(body) => match ready!(body.poll_next(cx)) { - Some(Err(err)) => Poll::Ready(Some(Err(err.into()))), - Some(Ok(val)) => Poll::Ready(Some(Ok(val))), - None => Poll::Ready(None), - }, + ResponseBodyProj::Body(body) => body.poll_next(cx).map_err(Into::into), ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx), } } diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index 1e69990a0..c39c0e888 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -131,18 +131,9 @@ where Poll::Ready(Some(Ok(std::mem::take(b)))) } } - // TODO: MSRV 1.51: poll_map_err - EncoderBodyProj::Stream(b) => match ready!(b.poll_next(cx)) { - Some(Err(err)) => Poll::Ready(Some(Err(EncoderError::Body(err)))), - Some(Ok(val)) => Poll::Ready(Some(Ok(val))), - None => Poll::Ready(None), - }, + EncoderBodyProj::Stream(b) => b.poll_next(cx).map_err(EncoderError::Body), EncoderBodyProj::BoxedStream(ref mut b) => { - match ready!(b.as_pin_mut().poll_next(cx)) { - Some(Err(err)) => Poll::Ready(Some(Err(EncoderError::Boxed(err)))), - Some(Ok(val)) => Poll::Ready(Some(Ok(val))), - None => Poll::Ready(None), - } + b.as_pin_mut().poll_next(cx).map_err(EncoderError::Boxed) } } } diff --git a/actix-http/src/h1/utils.rs b/actix-http/src/h1/utils.rs index 523e652fd..5fd3cc21c 100644 --- a/actix-http/src/h1/utils.rs +++ b/actix-http/src/h1/utils.rs @@ -63,12 +63,9 @@ where .is_write_buf_full() { let next = - // TODO: MSRV 1.51: poll_map_err match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx) { Poll::Ready(Some(Ok(item))) => Poll::Ready(Some(item)), - Poll::Ready(Some(Err(err))) => { - return Poll::Ready(Err(err.into())) - } + Poll::Ready(Some(Err(err))) => return Poll::Ready(Err(err.into())), Poll::Ready(None) => Poll::Ready(None), Poll::Pending => Poll::Pending, }; diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index 0f09b6ad6..9574b02f7 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -341,7 +341,6 @@ where ) -> Poll>> { let this = self.project(); - // TODO: MSRV 1.51: poll_map_err match ready!(this.body.poll_next(cx)) { Some(Ok(chunk)) => { *this.size += chunk.len(); From dade818ebaab441e8cc5d359068209daa002b488 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 31 Aug 2021 04:18:54 +0100 Subject: [PATCH 39/88] add middleware composition tests (#2375) --- actix-http/CHANGES.md | 2 ++ actix-http/src/body/message_body.rs | 4 --- actix-http/src/encoding/encoder.rs | 4 +-- actix-http/src/h1/utils.rs | 4 ++- src/middleware/mod.rs | 40 +++++++++++++++++++++++++++++ 5 files changed, 46 insertions(+), 8 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 57c09d2d8..63172e56d 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -6,8 +6,10 @@ ### Fixed * Remove slice creation pointing to potential uninitialized data on h1 encoder. [#2364] +* Remove `Into` bound on `Encoder` body types. [#2375] [#2364]: https://github.com/actix/actix-web/pull/2364 +[#2375]: https://github.com/actix/actix-web/pull/2375 ## 3.0.0-beta.8 - 2021-08-09 diff --git a/actix-http/src/body/message_body.rs b/actix-http/src/body/message_body.rs index 2d2642ba7..edb4c550c 100644 --- a/actix-http/src/body/message_body.rs +++ b/actix-http/src/body/message_body.rs @@ -11,8 +11,6 @@ use bytes::{Bytes, BytesMut}; use futures_core::ready; use pin_project_lite::pin_project; -use crate::error::Error; - use super::BodySize; /// An interface for response bodies. @@ -47,7 +45,6 @@ impl MessageBody for () { impl MessageBody for Box where B: MessageBody + Unpin, - B::Error: Into, { type Error = B::Error; @@ -66,7 +63,6 @@ where impl MessageBody for Pin> where B: MessageBody, - B::Error: Into, { type Error = B::Error; diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index c39c0e888..abd8cedba 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -29,7 +29,7 @@ use crate::{ header::{ContentEncoding, CONTENT_ENCODING}, HeaderValue, StatusCode, }, - Error, ResponseHead, + ResponseHead, }; use super::Writer; @@ -107,7 +107,6 @@ enum EncoderBody { impl MessageBody for EncoderBody where B: MessageBody, - B::Error: Into, { type Error = EncoderError; @@ -142,7 +141,6 @@ where impl MessageBody for Encoder where B: MessageBody, - B::Error: Into, { type Error = EncoderError; diff --git a/actix-http/src/h1/utils.rs b/actix-http/src/h1/utils.rs index 5fd3cc21c..2547f4494 100644 --- a/actix-http/src/h1/utils.rs +++ b/actix-http/src/h1/utils.rs @@ -65,7 +65,9 @@ where let next = match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx) { Poll::Ready(Some(Ok(item))) => Poll::Ready(Some(item)), - Poll::Ready(Some(Err(err))) => return Poll::Ready(Err(err.into())), + Poll::Ready(Some(Err(err))) => { + return Poll::Ready(Err(err.into())) + } Poll::Ready(None) => Poll::Ready(None), Poll::Pending => Poll::Pending, }; diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 96a361fcf..d19cb64e9 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -19,3 +19,43 @@ mod compress; #[cfg(feature = "__compress")] pub use self::compress::Compress; + +#[cfg(test)] +mod tests { + use crate::{http::StatusCode, App}; + + use super::*; + + #[test] + fn common_combinations() { + // ensure there's no reason that the built-in middleware cannot compose + + let _ = App::new() + .wrap(Compat::new(Logger::default())) + .wrap(Condition::new(true, DefaultHeaders::new())) + .wrap(DefaultHeaders::new().header("X-Test2", "X-Value2")) + .wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| { + Ok(ErrorHandlerResponse::Response(res)) + })) + .wrap(Logger::default()) + .wrap(NormalizePath::new(TrailingSlash::Trim)); + + let _ = App::new() + .wrap(NormalizePath::new(TrailingSlash::Trim)) + .wrap(Logger::default()) + .wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| { + Ok(ErrorHandlerResponse::Response(res)) + })) + .wrap(DefaultHeaders::new().header("X-Test2", "X-Value2")) + .wrap(Condition::new(true, DefaultHeaders::new())) + .wrap(Compat::new(Logger::default())); + + #[cfg(feature = "__compress")] + { + let _ = App::new().wrap(Compress::default()).wrap(Logger::default()); + let _ = App::new().wrap(Logger::default()).wrap(Compress::default()); + let _ = App::new().wrap(Compat::new(Compress::default())); + let _ = App::new().wrap(Condition::new(true, Compat::new(Compress::default()))); + } + } +} From c50eef61664e0614255d02924d43f18c4630e447 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 31 Aug 2021 04:07:53 +0100 Subject: [PATCH 40/88] "deprecate" calls to NormalizePath::default --- MIGRATION.md | 3 ++- src/middleware/normalize.rs | 24 +++++++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 785974366..9a70adb95 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -3,7 +3,8 @@ * The default `NormalizePath` behavior now strips trailing slashes by default. This was previously documented to be the case in v3 but the behavior now matches. The effect is that routes defined with trailing slashes will become inaccessible when - using `NormalizePath::default()`. + using `NormalizePath::default()`. As such, calling `NormalizePath::default()` will log a warning. + It is advised that the `new` method be used instead. Before: `#[get("/test/")]` After: `#[get("/test")]` diff --git a/src/middleware/normalize.rs b/src/middleware/normalize.rs index 219af1c6a..8ad0bb3f0 100644 --- a/src/middleware/normalize.rs +++ b/src/middleware/normalize.rs @@ -59,7 +59,7 @@ impl Default for TrailingSlash { /// /// # actix_web::rt::System::new().block_on(async { /// let app = App::new() -/// .wrap(middleware::NormalizePath::default()) +/// .wrap(middleware::NormalizePath::trim()) /// .route("/test", web::get().to(|| async { "test" })) /// .route("/unmatchable/", web::get().to(|| async { "unmatchable" })); /// @@ -85,13 +85,31 @@ impl Default for TrailingSlash { /// assert_eq!(res.status(), StatusCode::NOT_FOUND); /// # }) /// ``` -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy)] pub struct NormalizePath(TrailingSlash); +impl Default for NormalizePath { + fn default() -> Self { + log::warn!( + "`NormalizePath::default()` is deprecated. The default trailing slash behavior changed \ + in v4 from `Always` to `Trim`. Update your call to `NormalizePath::new(...)`." + ); + + Self(TrailingSlash::Trim) + } +} + impl NormalizePath { /// Create new `NormalizePath` middleware with the specified trailing slash style. pub fn new(trailing_slash_style: TrailingSlash) -> Self { - NormalizePath(trailing_slash_style) + Self(trailing_slash_style) + } + + /// Constructs a new `NormalizePath` middleware with [trim](TrailingSlash::Trim) semantics. + /// + /// Use this instead of `NormalizePath::default()` to avoid deprecation warning. + pub fn trim() -> Self { + Self::new(TrailingSlash::Trim) } } From 7d01ece3556e77c0555f4e7da6c8699d8fc34fb1 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Tue, 31 Aug 2021 16:15:22 +0300 Subject: [PATCH 41/88] ResourceDef: support multiple-patterns as prefix (#2356) Co-authored-by: Rob Ede --- actix-router/CHANGES.md | 4 + actix-router/src/resource.rs | 253 +++++++++++++++++------------------ 2 files changed, 128 insertions(+), 129 deletions(-) diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index 804f7778d..990382512 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -7,6 +7,9 @@ * Improve malformed path error message. [#384] * Prefix segments now always end with with a segment delimiter or end-of-input. [#2355] * Prefix segments with trailing slashes define a trailing empty segment. [#2355] +* Support multi-pattern prefixes and joins. [#2356] +* `ResourceDef::pattern` now returns the first pattern in multi-pattern resources. [#2356] +* Support `build_resource_path` on multi-pattern resources. [#2356] * Minimum supported Rust version (MSRV) is now 1.51. [#378]: https://github.com/actix/actix-net/pull/378 @@ -14,6 +17,7 @@ [#380]: https://github.com/actix/actix-net/pull/380 [#384]: https://github.com/actix/actix-net/pull/384 [#2355]: https://github.com/actix/actix-web/pull/2355 +[#2356]: https://github.com/actix/actix-web/pull/2356 ## 0.5.0-beta.1 - 2021-07-20 diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs index 57ce36804..be54336e9 100644 --- a/actix-router/src/resource.rs +++ b/actix-router/src/resource.rs @@ -31,13 +31,13 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// # Pattern Format and Matching Behavior /// /// Resource pattern is defined as a string of zero or more _segments_ where each segment is -/// preceeded by a slash `/`. +/// preceded by a slash `/`. /// /// This means that pattern string __must__ either be empty or begin with a slash (`/`). /// This also implies that a trailing slash in pattern defines an empty segment. /// For example, the pattern `"/user/"` has two segments: `["user", ""]` /// -/// A key point to undertand is that `ResourceDef` matches segments, not strings. +/// A key point to underhand is that `ResourceDef` matches segments, not strings. /// It matches segments individually. /// For example, the pattern `/user/` is not considered a prefix for the path `/user/123/456`, /// because the second segment doesn't match: `["user", ""]` vs `["user", "123", "456"]`. @@ -220,17 +220,15 @@ pub struct ResourceDef { name: Option, /// Pattern that generated the resource definition. - /// - /// `None` when pattern type is `DynamicSet`. patterns: Patterns, + is_prefix: bool, + /// Pattern type. pat_type: PatternType, /// List of segments that compose the pattern, in order. - /// - /// `None` when pattern type is `DynamicSet`. - segments: Option>, + segments: Vec, } #[derive(Debug, Clone, PartialEq)] @@ -248,9 +246,6 @@ enum PatternType { /// Single constant/literal segment. Static(String), - /// Single constant/literal prefix segment. - Prefix(String), - /// Single regular expression and list of dynamic segment names. Dynamic(Regex, Vec<&'static str>), @@ -284,45 +279,7 @@ impl ResourceDef { /// ``` pub fn new(paths: T) -> Self { profile_method!(new); - - match paths.patterns() { - Patterns::Single(pattern) => ResourceDef::from_single_pattern(&pattern, false), - - // since zero length pattern sets are possible - // just return a useless `ResourceDef` - Patterns::List(patterns) if patterns.is_empty() => ResourceDef { - id: 0, - name: None, - patterns: Patterns::List(patterns), - pat_type: PatternType::DynamicSet(RegexSet::empty(), Vec::new()), - segments: None, - }, - - Patterns::List(patterns) => { - let mut re_set = Vec::with_capacity(patterns.len()); - let mut pattern_data = Vec::new(); - - for pattern in &patterns { - match ResourceDef::parse(pattern, false, true) { - (PatternType::Dynamic(re, names), _) => { - re_set.push(re.as_str().to_owned()); - pattern_data.push((re, names)); - } - _ => unreachable!(), - } - } - - let pattern_re_set = RegexSet::new(re_set).unwrap(); - - ResourceDef { - id: 0, - name: None, - patterns: Patterns::List(patterns), - pat_type: PatternType::DynamicSet(pattern_re_set, pattern_data), - segments: None, - } - } - } + Self::new2(paths, false) } /// Constructs a new resource definition using a pattern that performs prefix matching. @@ -348,9 +305,9 @@ impl ResourceDef { /// assert!(!resource.is_match("user/123/stars")); /// assert!(!resource.is_match("/foo")); /// ``` - pub fn prefix(path: &str) -> Self { + pub fn prefix(paths: T) -> Self { profile_method!(prefix); - ResourceDef::from_single_pattern(path, true) + ResourceDef::new2(paths, true) } /// Constructs a new resource definition using a string pattern that performs prefix matching, @@ -375,7 +332,7 @@ impl ResourceDef { /// ``` pub fn root_prefix(path: &str) -> Self { profile_method!(root_prefix); - ResourceDef::prefix(&insert_slash(path)) + ResourceDef::prefix(insert_slash(path).into_owned()) } /// Returns a numeric resource ID. @@ -453,17 +410,14 @@ impl ResourceDef { /// assert!(!ResourceDef::new("/user").is_prefix()); /// ``` pub fn is_prefix(&self) -> bool { - match &self.pat_type { - PatternType::Prefix(_) => true, - PatternType::Dynamic(re, _) if !re.as_str().ends_with('$') => true, - _ => false, - } + self.is_prefix } /// Returns the pattern string that generated the resource definition. /// - /// Returns `None` if definition was constructed with multiple patterns. - /// See [`patterns_iter`][Self::pattern_iter]. + /// If definition is constructed with multiple patterns, the first pattern is returned. To get + /// all patterns, use [`patterns_iter`][Self::pattern_iter]. If resource has 0 patterns, + /// returns `None`. /// /// # Examples /// ``` @@ -472,11 +426,11 @@ impl ResourceDef { /// assert_eq!(resource.pattern().unwrap(), "/user/{id}"); /// /// let mut resource = ResourceDef::new(["/profile", "/user/{id}"]); - /// assert!(resource.pattern().is_none()); + /// assert_eq!(resource.pattern(), Some("/profile")); pub fn pattern(&self) -> Option<&str> { match &self.patterns { Patterns::Single(pattern) => Some(pattern.as_str()), - Patterns::List(_) => None, + Patterns::List(patterns) => patterns.first().map(AsRef::as_ref), } } @@ -563,8 +517,8 @@ impl ResourceDef { .collect::>(); match patterns.len() { - 1 => ResourceDef::from_single_pattern(&patterns[0], other.is_prefix()), - _ => ResourceDef::new(patterns), + 1 => ResourceDef::new2(&patterns[0], other.is_prefix()), + _ => ResourceDef::new2(patterns, other.is_prefix()), } } @@ -609,11 +563,10 @@ impl ResourceDef { // `self.find_match(path).is_some()` // but this skips some checks and uses potentially faster regex methods - match self.pat_type { - PatternType::Static(ref s) => s == path, - PatternType::Prefix(ref prefix) => is_prefix(prefix, path), - PatternType::Dynamic(ref re, _) => re.is_match(path), - PatternType::DynamicSet(ref re, _) => re.is_match(path), + match &self.pat_type { + PatternType::Static(pattern) => self.static_match(pattern, path).is_some(), + PatternType::Dynamic(re, _) => re.is_match(path), + PatternType::DynamicSet(re, _) => re.is_match(path), } } @@ -656,11 +609,7 @@ impl ResourceDef { profile_method!(find_match); match &self.pat_type { - PatternType::Static(segment) if path == segment => Some(segment.len()), - PatternType::Static(_) => None, - - PatternType::Prefix(prefix) if is_prefix(prefix, path) => Some(prefix.len()), - PatternType::Prefix(_) => None, + PatternType::Static(pattern) => self.static_match(pattern, path), PatternType::Dynamic(re, _) => Some(re.captures(path)?[1].len()), @@ -753,10 +702,10 @@ impl ResourceDef { let path_str = path.path(); let (matched_len, matched_vars) = match &self.pat_type { - PatternType::Static(_) | PatternType::Prefix(_) => { + PatternType::Static(pattern) => { profile_section!(pattern_static_or_prefix); - match self.find_match(path_str) { + match self.static_match(pattern, path_str) { Some(len) => (len, None), None => return false, } @@ -844,13 +793,10 @@ impl ResourceDef { F: FnMut(&str) -> Option, I: AsRef, { - for el in match self.segments { - Some(ref segments) => segments, - None => return false, - } { - match *el { - PatternSegment::Const(ref val) => path.push_str(val), - PatternSegment::Var(ref name) => match vars(name) { + for segment in &self.segments { + match segment { + PatternSegment::Const(val) => path.push_str(val), + PatternSegment::Var(name) => match vars(name) { Some(val) => path.push_str(val.as_ref()), _ => return false, }, @@ -864,8 +810,8 @@ impl ResourceDef { /// /// Returns `true` on success. /// - /// Resource paths can not be built from multi-pattern resources; this call will always return - /// false and will not add anything to the string buffer. + /// For multi-pattern resources, the first pattern is used under the assumption that it would be + /// equivalent to any other choice. /// /// # Examples /// ``` @@ -890,8 +836,8 @@ impl ResourceDef { /// /// Returns `true` on success. /// - /// Resource paths can not be built from multi-pattern resources; this call will always return - /// false and will not add anything to the string buffer. + /// For multi-pattern resources, the first pattern is used under the assumption that it would be + /// equivalent to any other choice. /// /// # Examples /// ``` @@ -921,19 +867,69 @@ impl ResourceDef { self.build_resource_path(path, |name| values.get(name).map(AsRef::::as_ref)) } - /// Parse path pattern and create a new instance. - fn from_single_pattern(pattern: &str, is_prefix: bool) -> Self { - profile_method!(from_single_pattern); + /// Returns true if `prefix` acts as a proper prefix (i.e., separated by a slash) in `path`. + fn static_match(&self, pattern: &str, path: &str) -> Option { + let rem = path.strip_prefix(pattern)?; - let pattern = pattern.to_owned(); - let (pat_type, segments) = ResourceDef::parse(&pattern, is_prefix, false); + match self.is_prefix { + // resource is not a prefix so an exact match is needed + false if rem.is_empty() => Some(pattern.len()), + + // resource is a prefix so rem should start with a path delimiter + true if rem.is_empty() || rem.starts_with('/') => Some(pattern.len()), + + // otherwise, no match + _ => None, + } + } + + fn new2(paths: T, is_prefix: bool) -> Self { + profile_method!(new2); + + let patterns = paths.patterns(); + let (pat_type, segments) = match &patterns { + Patterns::Single(pattern) => ResourceDef::parse(pattern, is_prefix, false), + + // since zero length pattern sets are possible + // just return a useless `ResourceDef` + Patterns::List(patterns) if patterns.is_empty() => ( + PatternType::DynamicSet(RegexSet::empty(), Vec::new()), + Vec::new(), + ), + + Patterns::List(patterns) => { + let mut re_set = Vec::with_capacity(patterns.len()); + let mut pattern_data = Vec::new(); + let mut segments = None; + + for pattern in patterns { + match ResourceDef::parse(pattern, is_prefix, true) { + (PatternType::Dynamic(re, names), segs) => { + re_set.push(re.as_str().to_owned()); + pattern_data.push((re, names)); + segments.get_or_insert(segs); + } + _ => unreachable!(), + } + } + + let pattern_re_set = RegexSet::new(re_set).unwrap(); + let segments = segments.unwrap_or_else(Vec::new); + + ( + PatternType::DynamicSet(pattern_re_set, pattern_data), + segments, + ) + } + }; ResourceDef { id: 0, name: None, - patterns: Patterns::Single(pattern), + patterns, + is_prefix, pat_type, - segments: Some(segments), + segments, } } @@ -1023,20 +1019,15 @@ impl ResourceDef { ) -> (PatternType, Vec) { profile_method!(parse); - let mut unprocessed = pattern; - - if !force_dynamic && unprocessed.find('{').is_none() && !unprocessed.ends_with('*') { + if !force_dynamic && pattern.find('{').is_none() && !pattern.ends_with('*') { // pattern is static - - let tp = if is_prefix { - PatternType::Prefix(unprocessed.to_owned()) - } else { - PatternType::Static(unprocessed.to_owned()) - }; - - return (tp, vec![PatternSegment::Const(unprocessed.to_owned())]); + return ( + PatternType::Static(pattern.to_owned()), + vec![PatternSegment::Const(pattern.to_owned())], + ); } + let mut unprocessed = pattern; let mut segments = Vec::new(); let mut re = format!("{}^", REGEX_FLAGS); let mut dyn_segment_count = 0; @@ -1137,18 +1128,7 @@ impl Eq for ResourceDef {} impl PartialEq for ResourceDef { fn eq(&self, other: &ResourceDef) -> bool { - self.patterns == other.patterns - && match &self.pat_type { - PatternType::Static(_) => matches!(&other.pat_type, PatternType::Static(_)), - PatternType::Prefix(_) => matches!(&other.pat_type, PatternType::Prefix(_)), - PatternType::Dynamic(re, _) => match &other.pat_type { - PatternType::Dynamic(other_re, _) => re.as_str() == other_re.as_str(), - _ => false, - }, - PatternType::DynamicSet(_, _) => { - matches!(&other.pat_type, PatternType::DynamicSet(..)) - } - } + self.patterns == other.patterns && self.is_prefix == other.is_prefix } } @@ -1183,15 +1163,6 @@ pub(crate) fn insert_slash(path: &str) -> Cow<'_, str> { } } -/// Returns true if `prefix` acts as a proper prefix (i.e., separated by a slash) in `path`. -fn is_prefix(prefix: &str, path: &str) -> bool { - match path.strip_prefix(prefix) { - // Ensure the match ends at segment boundary - Some(rem) if rem.is_empty() || rem.starts_with('/') => true, - _ => false, - } -} - #[cfg(test)] mod tests { use super::*; @@ -1376,6 +1347,24 @@ mod tests { assert!(!re.is_match("/user/2345/sdg")); } + #[test] + fn dynamic_set_prefix() { + let re = ResourceDef::prefix(vec!["/u/{id}", "/{id:[[:digit:]]{3}}"]); + + assert_eq!(re.find_match("/u/abc"), Some(6)); + assert_eq!(re.find_match("/u/abc/123"), Some(6)); + assert_eq!(re.find_match("/s/user/profile"), None); + + assert_eq!(re.find_match("/123"), Some(4)); + assert_eq!(re.find_match("/123/456"), Some(4)); + assert_eq!(re.find_match("/12345"), None); + + let mut path = Path::new("/151/res"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "151"); + assert_eq!(path.unprocessed(), "/res"); + } + #[test] fn parse_tail() { let re = ResourceDef::new("/user/-{id}*"); @@ -1602,10 +1591,11 @@ mod tests { } #[test] - fn multi_pattern_cannot_build_path() { + fn multi_pattern_build_path() { let resource = ResourceDef::new(["/user/{id}", "/profile/{id}"]); let mut s = String::new(); - assert!(!resource.resource_path_from_iter(&mut s, &mut ["123"].iter())); + assert!(resource.resource_path_from_iter(&mut s, &mut ["123"].iter())); + assert_eq!(s, "/user/123"); } #[test] @@ -1738,8 +1728,12 @@ mod tests { join_test!("", "" => "", "/hello", "/"); join_test!("/user", "" => "", "/user", "/user/123", "/user11", "user", "user/123"); - join_test!("", "/user"=> "", "/user", "foo", "/user11", "user", "user/123"); - join_test!("/user", "/xx"=> "", "", "/", "/user", "/xx", "/userxx", "/user/xx"); + join_test!("", "/user" => "", "/user", "foo", "/user11", "user", "user/123"); + join_test!("/user", "/xx" => "", "", "/", "/user", "/xx", "/userxx", "/user/xx"); + + join_test!(["/ver/{v}", "/v{v}"], ["/req/{req}", "/{req}"] => "/v1/abc", + "/ver/1/abc", "/v1/req/abc", "/ver/1/req/abc", "/v1/abc/def", + "/ver1/req/abc/def", "", "/", "/v1/"); } #[test] @@ -1777,6 +1771,7 @@ mod tests { match_methods_agree!(prefix "" => "", "/", "/foo"); match_methods_agree!(prefix "/user" => "user", "/user", "/users", "/user/123", "/foo"); match_methods_agree!(prefix r"/id/{id:\d{3}}" => "/id/123", "/id/1234"); + match_methods_agree!(["/v{v}", "/ver/{v}"] => "", "s/v", "/v1", "/v1/xx", "/ver/i3/5", "/ver/1"); } #[test] From 373b3f91dff58ac6c5b1158206e7380376906541 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Wed, 1 Sep 2021 06:48:43 +0300 Subject: [PATCH 42/88] rework `ResourceMap` internals (#2337) --- src/app_service.rs | 4 +- src/request.rs | 9 +- src/rmap.rs | 422 ++++++++++++++++++++++++--------------------- 3 files changed, 233 insertions(+), 202 deletions(-) diff --git a/src/app_service.rs b/src/app_service.rs index ce52543b8..cf34b302e 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -79,7 +79,7 @@ where .into_iter() .for_each(|mut srv| srv.register(&mut config)); - let mut rmap = ResourceMap::new(ResourceDef::new("")); + let mut rmap = ResourceMap::new(ResourceDef::prefix("")); let (config, services) = config.into_services(); @@ -104,7 +104,7 @@ where // complete ResourceMap tree creation let rmap = Rc::new(rmap); - rmap.finish(rmap.clone()); + ResourceMap::finish(&rmap); // construct all async data factory futures let factory_futs = join_all(self.async_data_factories.iter().map(|f| f())); diff --git a/src/request.rs b/src/request.rs index 59850b4ca..c25a5397a 100644 --- a/src/request.rs +++ b/src/request.rs @@ -511,7 +511,7 @@ mod tests { let mut res = ResourceDef::new("/user/{name}.{ext}"); res.set_name("index"); - let mut rmap = ResourceMap::new(ResourceDef::new("")); + let mut rmap = ResourceMap::new(ResourceDef::prefix("")); rmap.add(&mut res, None); assert!(rmap.has_resource("/user/test.html")); assert!(!rmap.has_resource("/test/unknown")); @@ -541,7 +541,7 @@ mod tests { let mut rdef = ResourceDef::new("/index.html"); rdef.set_name("index"); - let mut rmap = ResourceMap::new(ResourceDef::new("")); + let mut rmap = ResourceMap::new(ResourceDef::prefix("")); rmap.add(&mut rdef, None); assert!(rmap.has_resource("/index.html")); @@ -562,7 +562,7 @@ mod tests { let mut rdef = ResourceDef::new("/index.html"); rdef.set_name("index"); - let mut rmap = ResourceMap::new(ResourceDef::new("")); + let mut rmap = ResourceMap::new(ResourceDef::prefix("")); rmap.add(&mut rdef, None); assert!(rmap.has_resource("/index.html")); @@ -581,9 +581,8 @@ mod tests { rdef.set_name("youtube"); - let mut rmap = ResourceMap::new(ResourceDef::new("")); + let mut rmap = ResourceMap::new(ResourceDef::prefix("")); rmap.add(&mut rdef, None); - assert!(rmap.has_resource("https://youtube.com/watch/unknown")); let req = TestRequest::default().rmap(rmap).to_http_request(); let url = req.url_for("youtube", &["oHg5SJYRHA0"]); diff --git a/src/rmap.rs b/src/rmap.rs index 0ee4de47e..8466eda28 100644 --- a/src/rmap.rs +++ b/src/rmap.rs @@ -10,43 +10,75 @@ use crate::request::HttpRequest; #[derive(Clone, Debug)] pub struct ResourceMap { - root: ResourceDef, + pattern: ResourceDef, + + /// Named resources within the tree or, for external resources, + /// it points to isolated nodes outside the tree. + named: AHashMap>, + parent: RefCell>, - named: AHashMap, - patterns: Vec<(ResourceDef, Option>)>, + + /// Must be `None` for "edge" nodes. + nodes: Option>>, } impl ResourceMap { + /// Creates a _container_ node in the `ResourceMap` tree. pub fn new(root: ResourceDef) -> Self { ResourceMap { - root, - parent: RefCell::new(Weak::new()), + pattern: root, named: AHashMap::default(), - patterns: Vec::new(), + parent: RefCell::new(Weak::new()), + nodes: Some(Vec::new()), } } + /// Adds a (possibly nested) resource. + /// + /// To add a non-prefix pattern, `nested` must be `None`. + /// To add external resource, supply a pattern without a leading `/`. + /// The root pattern of `nested`, if present, should match `pattern`. pub fn add(&mut self, pattern: &mut ResourceDef, nested: Option>) { - pattern.set_id(self.patterns.len() as u16); - self.patterns.push((pattern.clone(), nested)); - if let Some(name) = pattern.name() { - self.named.insert(name.to_owned(), pattern.clone()); + pattern.set_id(self.nodes.as_ref().unwrap().len() as u16); + + if let Some(new_node) = nested { + assert_eq!(&new_node.pattern, pattern, "`patern` and `nested` mismatch"); + self.named.extend(new_node.named.clone().into_iter()); + self.nodes.as_mut().unwrap().push(new_node); + } else { + let new_node = Rc::new(ResourceMap { + pattern: pattern.clone(), + named: AHashMap::default(), + parent: RefCell::new(Weak::new()), + nodes: None, + }); + + if let Some(name) = pattern.name() { + self.named.insert(name.to_owned(), Rc::clone(&new_node)); + } + + let is_external = match pattern.pattern() { + Some(p) => !p.is_empty() && !p.starts_with('/'), + None => false, + }; + + // Don't add external resources to the tree + if !is_external { + self.nodes.as_mut().unwrap().push(new_node); + } } } - pub(crate) fn finish(&self, current: Rc) { - for (_, nested) in &self.patterns { - if let Some(ref nested) = nested { - *nested.parent.borrow_mut() = Rc::downgrade(¤t); - nested.finish(nested.clone()); - } + pub(crate) fn finish(self: &Rc) { + for node in self.nodes.iter().flatten() { + node.parent.replace(Rc::downgrade(self)); + ResourceMap::finish(node); } } /// Generate url for named resource /// - /// Check [`HttpRequest::url_for()`](../struct.HttpRequest.html#method. - /// url_for) for detailed information. + /// Check [`HttpRequest::url_for`] for detailed information. pub fn url_for( &self, req: &HttpRequest, @@ -57,197 +89,97 @@ impl ResourceMap { U: IntoIterator, I: AsRef, { - let mut path = String::new(); let mut elements = elements.into_iter(); - if self.patterns_for(name, &mut path, &mut elements)?.is_some() { - if path.starts_with('/') { - let conn = req.connection_info(); - Ok(Url::parse(&format!( - "{}://{}{}", - conn.scheme(), - conn.host(), - path - ))?) - } else { - Ok(Url::parse(&path)?) - } + let path = self + .named + .get(name) + .ok_or(UrlGenerationError::ResourceNotFound)? + .root_rmap_fn(String::with_capacity(24), |mut acc, node| { + node.pattern + .resource_path_from_iter(&mut acc, &mut elements) + .then(|| acc) + }) + .ok_or(UrlGenerationError::NotEnoughElements)?; + + if path.starts_with('/') { + let conn = req.connection_info(); + Ok(Url::parse(&format!( + "{}://{}{}", + conn.scheme(), + conn.host(), + path + ))?) } else { - Err(UrlGenerationError::ResourceNotFound) + Ok(Url::parse(&path)?) } } pub fn has_resource(&self, path: &str) -> bool { - let path = if path.is_empty() { "/" } else { path }; - - for (pattern, rmap) in &self.patterns { - if let Some(ref rmap) = rmap { - if let Some(pat_len) = pattern.find_match(path) { - return rmap.has_resource(&path[pat_len..]); - } - } else if pattern.is_match(path) || pattern.pattern() == Some("") && path == "/" { - return true; - } - } - false + self.find_matching_node(path).is_some() } /// Returns the name of the route that matches the given path or None if no full match - /// is possible. + /// is possible or the matching resource is not named. pub fn match_name(&self, path: &str) -> Option<&str> { - let path = if path.is_empty() { "/" } else { path }; - - for (pattern, rmap) in &self.patterns { - if let Some(ref rmap) = rmap { - if let Some(plen) = pattern.find_match(path) { - return rmap.match_name(&path[plen..]); - } - } else if pattern.is_match(path) { - return pattern.name(); - } - } - - None + self.find_matching_node(path)?.pattern.name() } /// Returns the full resource pattern matched against a path or None if no full match /// is possible. pub fn match_pattern(&self, path: &str) -> Option { - let path = if path.is_empty() { "/" } else { path }; - - // ensure a full match exists - if !self.has_resource(path) { - return None; - } - - Some(self.traverse_resource_pattern(path)) + self.find_matching_node(path)?.root_rmap_fn( + String::with_capacity(24), + |mut acc, node| { + acc.push_str(node.pattern.pattern()?); + Some(acc) + }, + ) } - /// Takes remaining path and tries to match it up against a resource definition within the - /// current resource map recursively, returning a concatenation of all resource prefixes and - /// patterns matched in the tree. - /// - /// Should only be used after checking the resource exists in the map so that partial match - /// patterns are not returned. - fn traverse_resource_pattern(&self, remaining: &str) -> String { - for (pattern, rmap) in &self.patterns { - if let Some(ref rmap) = rmap { - if let Some(prefix_len) = pattern.find_match(remaining) { - // TODO: think about unwrap_or - let prefix = pattern.pattern().unwrap_or("").to_owned(); - - return [ - prefix, - rmap.traverse_resource_pattern(&remaining[prefix_len..]), - ] - .concat(); - } - } else if pattern.is_match(remaining) { - // TODO: think about unwrap_or - return pattern.pattern().unwrap_or("").to_owned(); - } - } - - String::new() + fn find_matching_node(&self, path: &str) -> Option<&ResourceMap> { + self._find_matching_node(path).flatten() } - fn patterns_for( - &self, - name: &str, - path: &mut String, - elements: &mut U, - ) -> Result, UrlGenerationError> + /// Returns `None` if root pattern doesn't match; + /// `Some(None)` if root pattern matches but there is no matching child pattern. + /// Don't search sideways when `Some(none)` is returned. + fn _find_matching_node(&self, path: &str) -> Option> { + let matched_len = self.pattern.find_match(path)?; + let path = &path[matched_len..]; + + Some(match &self.nodes { + // find first sub-node to match remaining path + Some(nodes) => nodes + .iter() + .filter_map(|node| node._find_matching_node(path)) + .next() + .flatten(), + + // only terminate at edge nodes + None => Some(self), + }) + } + + /// Find `self`'s highest ancestor and then run `F`, providing `B`, in that rmap context. + fn root_rmap_fn(&self, init: B, mut f: F) -> Option where - U: Iterator, - I: AsRef, + F: FnMut(B, &ResourceMap) -> Option, { - if self.pattern_for(name, path, elements)?.is_some() { - Ok(Some(())) - } else { - self.parent_pattern_for(name, path, elements) - } + self._root_rmap_fn(init, &mut f) } - fn pattern_for( - &self, - name: &str, - path: &mut String, - elements: &mut U, - ) -> Result, UrlGenerationError> + /// Run `F`, providing `B`, if `self` is top-level resource map, else recurse to parent map. + fn _root_rmap_fn(&self, init: B, f: &mut F) -> Option where - U: Iterator, - I: AsRef, + F: FnMut(B, &ResourceMap) -> Option, { - if let Some(pattern) = self.named.get(name) { - if pattern - .pattern() - .map(|pat| pat.starts_with('/')) - .unwrap_or(false) - { - self.fill_root(path, elements)?; - } + let data = match self.parent.borrow().upgrade() { + Some(ref parent) => parent._root_rmap_fn(init, f)?, + None => init, + }; - if pattern.resource_path_from_iter(path, elements) { - Ok(Some(())) - } else { - Err(UrlGenerationError::NotEnoughElements) - } - } else { - for (_, rmap) in &self.patterns { - if let Some(ref rmap) = rmap { - if rmap.pattern_for(name, path, elements)?.is_some() { - return Ok(Some(())); - } - } - } - Ok(None) - } - } - - fn fill_root( - &self, - path: &mut String, - elements: &mut U, - ) -> Result<(), UrlGenerationError> - where - U: Iterator, - I: AsRef, - { - if let Some(ref parent) = self.parent.borrow().upgrade() { - parent.fill_root(path, elements)?; - } - - if self.root.resource_path_from_iter(path, elements) { - Ok(()) - } else { - Err(UrlGenerationError::NotEnoughElements) - } - } - - fn parent_pattern_for( - &self, - name: &str, - path: &mut String, - elements: &mut U, - ) -> Result, UrlGenerationError> - where - U: Iterator, - I: AsRef, - { - if let Some(ref parent) = self.parent.borrow().upgrade() { - if let Some(pattern) = parent.named.get(name) { - self.fill_root(path, elements)?; - if pattern.resource_path_from_iter(path, elements) { - Ok(Some(())) - } else { - Err(UrlGenerationError::NotEnoughElements) - } - } else { - parent.parent_pattern_for(name, path, elements) - } - } else { - Ok(None) - } + f(data, self) } } @@ -259,7 +191,7 @@ mod tests { fn extract_matched_pattern() { let mut root = ResourceMap::new(ResourceDef::root_prefix("")); - let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); + let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}")); user_map.add(&mut ResourceDef::new("/"), None); user_map.add(&mut ResourceDef::new("/profile"), None); user_map.add(&mut ResourceDef::new("/article/{id}"), None); @@ -275,9 +207,10 @@ mod tests { &mut ResourceDef::root_prefix("/user/{id}"), Some(Rc::new(user_map)), ); + root.add(&mut ResourceDef::new("/info"), None); let root = Rc::new(root); - root.finish(Rc::clone(&root)); + ResourceMap::finish(&root); // sanity check resource map setup @@ -288,7 +221,7 @@ mod tests { assert!(root.has_resource("/v2")); assert!(!root.has_resource("/v33")); - assert!(root.has_resource("/user/22")); + assert!(!root.has_resource("/user/22")); assert!(root.has_resource("/user/22/")); assert!(root.has_resource("/user/22/profile")); @@ -336,7 +269,7 @@ mod tests { rdef.set_name("root_info"); root.add(&mut rdef, None); - let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); + let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}")); let mut rdef = ResourceDef::new("/"); user_map.add(&mut rdef, None); @@ -350,14 +283,14 @@ mod tests { ); let root = Rc::new(root); - root.finish(Rc::clone(&root)); + ResourceMap::finish(&root); // sanity check resource map setup assert!(root.has_resource("/info")); assert!(!root.has_resource("/bar")); - assert!(root.has_resource("/user/22")); + assert!(!root.has_resource("/user/22")); assert!(root.has_resource("/user/22/")); assert!(root.has_resource("/user/22/post/55")); @@ -377,7 +310,7 @@ mod tests { // ref: https://github.com/actix/actix-web/issues/1582 let mut root = ResourceMap::new(ResourceDef::root_prefix("")); - let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); + let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}")); user_map.add(&mut ResourceDef::new("/"), None); user_map.add(&mut ResourceDef::new("/profile"), None); user_map.add(&mut ResourceDef::new("/article/{id}"), None); @@ -393,20 +326,119 @@ mod tests { ); let root = Rc::new(root); - root.finish(Rc::clone(&root)); + ResourceMap::finish(&root); // check root has no parent assert!(root.parent.borrow().upgrade().is_none()); // check child has parent reference - assert!(root.patterns[0].1.is_some()); + assert!(root.nodes.as_ref().unwrap()[0] + .parent + .borrow() + .upgrade() + .is_some()); // check child's parent root id matches root's root id - assert_eq!( - root.patterns[0].1.as_ref().unwrap().root.id(), - root.root.id() - ); + assert!(Rc::ptr_eq( + &root.nodes.as_ref().unwrap()[0] + .parent + .borrow() + .upgrade() + .unwrap(), + &root + )); let output = format!("{:?}", root); assert!(output.starts_with("ResourceMap {")); assert!(output.ends_with(" }")); } + + #[test] + fn short_circuit() { + let mut root = ResourceMap::new(ResourceDef::prefix("")); + + let mut user_root = ResourceDef::prefix("/user"); + let mut user_map = ResourceMap::new(user_root.clone()); + user_map.add(&mut ResourceDef::new("/u1"), None); + user_map.add(&mut ResourceDef::new("/u2"), None); + + root.add(&mut ResourceDef::new("/user/u3"), None); + root.add(&mut user_root, Some(Rc::new(user_map))); + root.add(&mut ResourceDef::new("/user/u4"), None); + + let rmap = Rc::new(root); + ResourceMap::finish(&rmap); + + assert!(rmap.has_resource("/user/u1")); + assert!(rmap.has_resource("/user/u2")); + assert!(rmap.has_resource("/user/u3")); + assert!(!rmap.has_resource("/user/u4")); + } + + #[test] + fn url_for() { + let mut root = ResourceMap::new(ResourceDef::prefix("")); + + let mut user_scope_rdef = ResourceDef::prefix("/user"); + let mut user_scope_map = ResourceMap::new(user_scope_rdef.clone()); + + let mut user_rdef = ResourceDef::new("/{user_id}"); + let mut user_map = ResourceMap::new(user_rdef.clone()); + + let mut post_rdef = ResourceDef::new("/post/{sub_id}"); + post_rdef.set_name("post"); + + user_map.add(&mut post_rdef, None); + user_scope_map.add(&mut user_rdef, Some(Rc::new(user_map))); + root.add(&mut user_scope_rdef, Some(Rc::new(user_scope_map))); + + let rmap = Rc::new(root); + ResourceMap::finish(&rmap); + + let mut req = crate::test::TestRequest::default(); + req.set_server_hostname("localhost:8888"); + let req = req.to_http_request(); + + let url = rmap + .url_for(&req, "post", &["u123", "foobar"]) + .unwrap() + .to_string(); + assert_eq!(url, "http://localhost:8888/user/u123/post/foobar"); + + assert!(rmap.url_for(&req, "missing", &["u123"]).is_err()); + } + + #[test] + fn external_resource_with_no_name() { + let mut root = ResourceMap::new(ResourceDef::prefix("")); + + let mut rdef = ResourceDef::new("https://duck.com/{query}"); + root.add(&mut rdef, None); + + let rmap = Rc::new(root); + ResourceMap::finish(&rmap); + + assert!(!rmap.has_resource("https://duck.com/abc")); + } + + #[test] + fn external_resource_with_name() { + let mut root = ResourceMap::new(ResourceDef::prefix("")); + + let mut rdef = ResourceDef::new("https://duck.com/{query}"); + rdef.set_name("duck"); + root.add(&mut rdef, None); + + let rmap = Rc::new(root); + ResourceMap::finish(&rmap); + + assert!(!rmap.has_resource("https://duck.com/abc")); + + let mut req = crate::test::TestRequest::default(); + req.set_server_hostname("localhost:8888"); + let req = req.to_http_request(); + + assert_eq!( + rmap.url_for(&req, "duck", &["abcd"]).unwrap().to_string(), + "https://duck.com/abcd" + ); + } } From ddc8c16cb34c9730b9a4922413fbf027e401052d Mon Sep 17 00:00:00 2001 From: Arthur Le Moigne Date: Wed, 1 Sep 2021 10:08:29 +0200 Subject: [PATCH 43/88] Fix quality parse error in Accept-Encoding HTTP header (#2344) --- CHANGES.md | 7 +- actix-http/CHANGES.md | 5 +- actix-http/src/encoding/decoder.rs | 3 +- .../src/header/shared/content_encoding.rs | 56 ++--- actix-http/src/header/shared/quality_item.rs | 29 ++- src/middleware/compress.rs | 217 +++++++++++++++--- tests/test_server.rs | 19 ++ 7 files changed, 259 insertions(+), 77 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5325caf48..217ec4f78 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,10 +4,15 @@ ### Added * Re-export actix-service `ServiceFactory` in `dev` module. [#2325] -### Changes +### Changed * Minimum supported Rust version (MSRV) is now 1.51. +* Compress middleware will return 406 Not Acceptable when no content encoding is acceptable to the client. [#2344] + +### Fixed +* Fix quality parse error in Accept-Encoding header. [#2344] [#2325]: https://github.com/actix/actix-web/pull/2325 +[#2344]: https://github.com/actix/actix-web/pull/2344 ## 4.0.0-beta.8 - 2021-06-26 diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 63172e56d..f4efef54a 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,22 +1,23 @@ # Changes ## Unreleased - 2021-xx-xx -### Changes +### Changed * Minimum supported Rust version (MSRV) is now 1.51. ### Fixed * Remove slice creation pointing to potential uninitialized data on h1 encoder. [#2364] * Remove `Into` bound on `Encoder` body types. [#2375] +* Fix quality parse error in Accept-Encoding header. [#2344] [#2364]: https://github.com/actix/actix-web/pull/2364 [#2375]: https://github.com/actix/actix-web/pull/2375 +[#2344]: https://github.com/actix/actix-web/pull/2344 ## 3.0.0-beta.8 - 2021-08-09 ### Fixed * Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977) - ## 3.0.0-beta.8 - 2021-06-26 ### Changed * Change compression algorithm features flags. [#2250] diff --git a/actix-http/src/encoding/decoder.rs b/actix-http/src/encoding/decoder.rs index d3e304836..81e97d916 100644 --- a/actix-http/src/encoding/decoder.rs +++ b/actix-http/src/encoding/decoder.rs @@ -1,6 +1,7 @@ //! Stream decoders. use std::{ + convert::TryFrom, future::Future, io::{self, Write as _}, pin::Pin, @@ -80,7 +81,7 @@ where let encoding = headers .get(&CONTENT_ENCODING) .and_then(|val| val.to_str().ok()) - .map(ContentEncoding::from) + .and_then(|x| ContentEncoding::try_from(x).ok()) .unwrap_or(ContentEncoding::Identity); Self::new(stream, encoding) diff --git a/actix-http/src/header/shared/content_encoding.rs b/actix-http/src/header/shared/content_encoding.rs index b9c1d2795..375e8c2fa 100644 --- a/actix-http/src/header/shared/content_encoding.rs +++ b/actix-http/src/header/shared/content_encoding.rs @@ -1,4 +1,4 @@ -use std::{convert::Infallible, str::FromStr}; +use std::{convert::TryFrom, error, fmt, str::FromStr}; use http::header::InvalidHeaderValue; @@ -8,6 +8,20 @@ use crate::{ HttpMessage, }; +/// Error return when a content encoding is unknown. +/// +/// Example: 'compress' +#[derive(Debug)] +pub struct ContentEncodingParseError; + +impl fmt::Display for ContentEncodingParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Unsupported content encoding") + } +} + +impl error::Error for ContentEncodingParseError {} + /// Represents a supported content encoding. #[derive(Copy, Clone, PartialEq, Debug)] pub enum ContentEncoding { @@ -37,7 +51,7 @@ impl ContentEncoding { matches!(self, ContentEncoding::Identity | ContentEncoding::Auto) } - /// Convert content encoding to string + /// Convert content encoding to string. #[inline] pub fn as_str(self) -> &'static str { match self { @@ -48,18 +62,6 @@ impl ContentEncoding { ContentEncoding::Identity | ContentEncoding::Auto => "identity", } } - - /// Default Q-factor (quality) value. - #[inline] - pub fn quality(self) -> f64 { - match self { - ContentEncoding::Br => 1.1, - ContentEncoding::Gzip => 1.0, - ContentEncoding::Deflate => 0.9, - ContentEncoding::Identity | ContentEncoding::Auto => 0.1, - ContentEncoding::Zstd => 0.0, - } - } } impl Default for ContentEncoding { @@ -69,31 +71,33 @@ impl Default for ContentEncoding { } impl FromStr for ContentEncoding { - type Err = Infallible; + type Err = ContentEncodingParseError; fn from_str(val: &str) -> Result { - Ok(Self::from(val)) - } -} - -impl From<&str> for ContentEncoding { - fn from(val: &str) -> ContentEncoding { let val = val.trim(); if val.eq_ignore_ascii_case("br") { - ContentEncoding::Br + Ok(ContentEncoding::Br) } else if val.eq_ignore_ascii_case("gzip") { - ContentEncoding::Gzip + Ok(ContentEncoding::Gzip) } else if val.eq_ignore_ascii_case("deflate") { - ContentEncoding::Deflate + Ok(ContentEncoding::Deflate) } else if val.eq_ignore_ascii_case("zstd") { - ContentEncoding::Zstd + Ok(ContentEncoding::Zstd) } else { - ContentEncoding::default() + Err(ContentEncodingParseError) } } } +impl TryFrom<&str> for ContentEncoding { + type Error = ContentEncodingParseError; + + fn try_from(val: &str) -> Result { + val.parse() + } +} + impl IntoHeaderValue for ContentEncoding { type Error = InvalidHeaderValue; diff --git a/actix-http/src/header/shared/quality_item.rs b/actix-http/src/header/shared/quality_item.rs index 240a0afa2..63fa02e7b 100644 --- a/actix-http/src/header/shared/quality_item.rs +++ b/actix-http/src/header/shared/quality_item.rs @@ -1,11 +1,14 @@ use std::{ cmp, convert::{TryFrom, TryInto}, - fmt, str, + fmt, + str::{self, FromStr}, }; use derive_more::{Display, Error}; +use crate::error::ParseError; + const MAX_QUALITY: u16 = 1000; const MAX_FLOAT_QUALITY: f32 = 1.0; @@ -113,12 +116,12 @@ impl fmt::Display for QualityItem { } } -impl str::FromStr for QualityItem { - type Err = crate::error::ParseError; +impl FromStr for QualityItem { + type Err = ParseError; - fn from_str(qitem_str: &str) -> Result, crate::error::ParseError> { + fn from_str(qitem_str: &str) -> Result { if !qitem_str.is_ascii() { - return Err(crate::error::ParseError::Header); + return Err(ParseError::Header); } // Set defaults used if parsing fails. @@ -139,7 +142,7 @@ impl str::FromStr for QualityItem { if parts[0].len() < 2 { // Can't possibly be an attribute since an attribute needs at least a name followed // by an equals sign. And bare identifiers are forbidden. - return Err(crate::error::ParseError::Header); + return Err(ParseError::Header); } let start = &parts[0][0..2]; @@ -148,25 +151,21 @@ impl str::FromStr for QualityItem { let q_val = &parts[0][2..]; if q_val.len() > 5 { // longer than 5 indicates an over-precise q-factor - return Err(crate::error::ParseError::Header); + return Err(ParseError::Header); } - let q_value = q_val - .parse::() - .map_err(|_| crate::error::ParseError::Header)?; + let q_value = q_val.parse::().map_err(|_| ParseError::Header)?; if (0f32..=1f32).contains(&q_value) { quality = q_value; raw_item = parts[1]; } else { - return Err(crate::error::ParseError::Header); + return Err(ParseError::Header); } } } - let item = raw_item - .parse::() - .map_err(|_| crate::error::ParseError::Header)?; + let item = raw_item.parse::().map_err(|_| ParseError::Header)?; // we already checked above that the quality is within range Ok(QualityItem::new(item, Quality::from_f32(quality))) @@ -224,7 +223,7 @@ mod tests { } } - impl str::FromStr for Encoding { + impl FromStr for Encoding { type Err = crate::error::ParseError; fn from_str(s: &str) -> Result { use Encoding::*; diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index a9128bc47..0e61a8e7e 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -2,10 +2,10 @@ use std::{ cmp, + convert::TryFrom, future::Future, marker::PhantomData, pin::Pin, - str::FromStr, task::{Context, Poll}, }; @@ -13,16 +13,18 @@ use actix_http::{ body::{MessageBody, ResponseBody}, encoding::Encoder, http::header::{ContentEncoding, ACCEPT_ENCODING}, + StatusCode, }; use actix_service::{Service, Transform}; -use actix_utils::future::{ok, Ready}; +use actix_utils::future::{ok, Either, Ready}; use futures_core::ready; +use once_cell::sync::Lazy; use pin_project::pin_project; use crate::{ dev::BodyEncoding, service::{ServiceRequest, ServiceResponse}, - Error, + Error, HttpResponse, }; /// Middleware for compressing response payloads. @@ -78,34 +80,78 @@ pub struct CompressMiddleware { encoding: ContentEncoding, } +static SUPPORTED_ALGORITHM_NAMES: Lazy = Lazy::new(|| { + let mut encoding = vec![]; + + #[cfg(feature = "compress-brotli")] + { + encoding.push("br"); + } + + #[cfg(feature = "compress-gzip")] + { + encoding.push("gzip"); + encoding.push("deflate"); + } + + #[cfg(feature = "compress-zstd")] + encoding.push("zstd"); + + assert!( + !encoding.is_empty(), + "encoding can not be empty unless __compress feature has been explicitly enabled by itself" + ); + + encoding.join(", ") +}); + impl Service for CompressMiddleware where - B: MessageBody, S: Service, Error = Error>, + B: MessageBody, { type Response = ServiceResponse>>; type Error = Error; - type Future = CompressResponse; + type Future = Either, Ready>>; actix_service::forward_ready!(service); #[allow(clippy::borrow_interior_mutable_const)] fn call(&self, req: ServiceRequest) -> Self::Future { // negotiate content-encoding - let encoding = if let Some(val) = req.headers().get(&ACCEPT_ENCODING) { - if let Ok(enc) = val.to_str() { - AcceptEncoding::parse(enc, self.encoding) - } else { - ContentEncoding::Identity - } - } else { - ContentEncoding::Identity - }; + let encoding_result = req + .headers() + .get(&ACCEPT_ENCODING) + .and_then(|val| val.to_str().ok()) + .map(|enc| AcceptEncoding::try_parse(enc, self.encoding)); - CompressResponse { - encoding, - fut: self.service.call(req), - _phantom: PhantomData, + match encoding_result { + // Missing header => fallback to identity + None => Either::left(CompressResponse { + encoding: ContentEncoding::Identity, + fut: self.service.call(req), + _phantom: PhantomData, + }), + + // Valid encoding + Some(Ok(encoding)) => Either::left(CompressResponse { + encoding, + fut: self.service.call(req), + _phantom: PhantomData, + }), + + // There is an HTTP header but we cannot match what client as asked for + Some(Err(_)) => { + let res = HttpResponse::with_body( + StatusCode::NOT_ACCEPTABLE, + SUPPORTED_ALGORITHM_NAMES.as_str(), + ); + let enc = ContentEncoding::Identity; + + Either::right(ok(req.into_response(res.map_body(move |head, body| { + Encoder::response(enc, head, ResponseBody::Other(body.into())) + })))) + } } } } @@ -114,7 +160,6 @@ where pub struct CompressResponse where S: Service, - B: MessageBody, { #[pin] fut: S::Future, @@ -151,6 +196,7 @@ where struct AcceptEncoding { encoding: ContentEncoding, + // TODO: use Quality or QualityItem quality: f64, } @@ -177,26 +223,56 @@ impl PartialOrd for AcceptEncoding { impl PartialEq for AcceptEncoding { fn eq(&self, other: &AcceptEncoding) -> bool { - self.quality == other.quality + self.encoding == other.encoding && self.quality == other.quality } } +/// Parse q-factor from quality strings. +/// +/// If parse fail, then fallback to default value which is 1. +/// More details available here: +fn parse_quality(parts: &[&str]) -> f64 { + for part in parts { + if part.trim().starts_with("q=") { + return part[2..].parse().unwrap_or(1.0); + } + } + + 1.0 +} + +#[derive(Debug, PartialEq, Eq)] +enum AcceptEncodingError { + /// This error occurs when client only support compressed response and server do not have any + /// algorithm that match client accepted algorithms. + CompressionAlgorithmMismatch, +} + impl AcceptEncoding { fn new(tag: &str) -> Option { let parts: Vec<&str> = tag.split(';').collect(); let encoding = match parts.len() { 0 => return None, - _ => ContentEncoding::from(parts[0]), - }; - let quality = match parts.len() { - 1 => encoding.quality(), - _ => f64::from_str(parts[1]).unwrap_or(0.0), + _ => match ContentEncoding::try_from(parts[0]) { + Err(_) => return None, + Ok(x) => x, + }, }; + + let quality = parse_quality(&parts[1..]); + if quality <= 0.0 || quality > 1.0 { + return None; + } + Some(AcceptEncoding { encoding, quality }) } - /// Parse a raw Accept-Encoding header value into an ordered list. - pub fn parse(raw: &str, encoding: ContentEncoding) -> ContentEncoding { + /// Parse a raw Accept-Encoding header value into an ordered list then return the best match + /// based on middleware configuration. + pub fn try_parse( + raw: &str, + encoding: ContentEncoding, + ) -> Result { let mut encodings = raw .replace(' ', "") .split(',') @@ -206,13 +282,90 @@ impl AcceptEncoding { encodings.sort(); for enc in encodings { - if encoding == ContentEncoding::Auto { - return enc.encoding; - } else if encoding == enc.encoding { - return encoding; + if encoding == ContentEncoding::Auto || encoding == enc.encoding { + return Ok(enc.encoding); } } - ContentEncoding::Identity + // Special case if user cannot accept uncompressed data. + // See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding + // TODO: account for whitespace + if raw.contains("*;q=0") || raw.contains("identity;q=0") { + return Err(AcceptEncodingError::CompressionAlgorithmMismatch); + } + + Ok(ContentEncoding::Identity) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! assert_parse_eq { + ($raw:expr, $result:expr) => { + assert_eq!( + AcceptEncoding::try_parse($raw, ContentEncoding::Auto), + Ok($result) + ); + }; + } + + macro_rules! assert_parse_fail { + ($raw:expr) => { + assert!(AcceptEncoding::try_parse($raw, ContentEncoding::Auto).is_err()); + }; + } + + #[test] + fn test_parse_encoding() { + // Test simple case + assert_parse_eq!("br", ContentEncoding::Br); + assert_parse_eq!("gzip", ContentEncoding::Gzip); + assert_parse_eq!("deflate", ContentEncoding::Deflate); + assert_parse_eq!("zstd", ContentEncoding::Zstd); + + // Test space, trim, missing values + assert_parse_eq!("br,,,,", ContentEncoding::Br); + assert_parse_eq!("gzip , br, zstd", ContentEncoding::Gzip); + + // Test float number parsing + assert_parse_eq!("br;q=1 ,", ContentEncoding::Br); + assert_parse_eq!("br;q=1.0 , br", ContentEncoding::Br); + + // Test wildcard + assert_parse_eq!("*", ContentEncoding::Identity); + assert_parse_eq!("*;q=1.0", ContentEncoding::Identity); + } + + #[test] + fn test_parse_encoding_qfactor_ordering() { + assert_parse_eq!("gzip, br, zstd", ContentEncoding::Gzip); + assert_parse_eq!("zstd, br, gzip", ContentEncoding::Zstd); + + assert_parse_eq!("gzip;q=0.4, br;q=0.6", ContentEncoding::Br); + assert_parse_eq!("gzip;q=0.8, br;q=0.4", ContentEncoding::Gzip); + } + + #[test] + fn test_parse_encoding_qfactor_invalid() { + // Out of range + assert_parse_eq!("gzip;q=-5.0", ContentEncoding::Identity); + assert_parse_eq!("gzip;q=5.0", ContentEncoding::Identity); + + // Disabled + assert_parse_eq!("gzip;q=0", ContentEncoding::Identity); + } + + #[test] + fn test_parse_compression_required() { + // Check we fallback to identity if there is an unsupported compression algorithm + assert_parse_eq!("compress", ContentEncoding::Identity); + + // User do not want any compression + assert_parse_fail!("compress, identity;q=0"); + assert_parse_fail!("compress, identity;q=0.0"); + assert_parse_fail!("compress, *;q=0"); + assert_parse_fail!("compress, *;q=0.0"); } } diff --git a/tests/test_server.rs b/tests/test_server.rs index afea39dd9..beb8ff0f5 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -1077,3 +1077,22 @@ async fn test_data_drop() { assert_eq!(num.load(Ordering::SeqCst), 0); } + +#[actix_rt::test] +async fn test_accept_encoding_no_match() { + let srv = actix_test::start_with(actix_test::config().h1(), || { + App::new() + .wrap(Compress::default()) + .service(web::resource("/").route(web::to(move || HttpResponse::Ok().finish()))) + }); + + let response = srv + .get("/") + .append_header((ACCEPT_ENCODING, "compress, identity;q=0")) + .no_decompress() + .send() + .await + .unwrap(); + + assert_eq!(response.status().as_u16(), 406); +} From 93112644d3da17833ea03fc7856329ec2f35ba1c Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 1 Sep 2021 09:53:26 +0100 Subject: [PATCH 44/88] non exhaustive content encoding (#2377) --- Cargo.toml | 2 +- actix-http/CHANGES.md | 3 ++ actix-http/Cargo.toml | 2 +- actix-http/src/encoding/decoder.rs | 3 +- .../src/header/shared/content_encoding.rs | 17 ++++------- src/scope.rs | 4 +-- src/test.rs | 2 +- src/types/form.rs | 2 +- src/types/json.rs | 30 ++++--------------- src/types/query.rs | 30 ++++++++----------- 10 files changed, 35 insertions(+), 60 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cee401363..699717b4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,7 +99,7 @@ regex = "1.4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_urlencoded = "0.7" -smallvec = "1.6" +smallvec = "1.6.1" socket2 = "0.4.0" time = { version = "0.2.23", default-features = false, features = ["std"] } url = "2.1" diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index f4efef54a..65206cf9a 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -2,6 +2,7 @@ ## Unreleased - 2021-xx-xx ### Changed +* `ContentEncoding` is now marked `#[non_exhaustive]`. [#2377] * Minimum supported Rust version (MSRV) is now 1.51. ### Fixed @@ -12,12 +13,14 @@ [#2364]: https://github.com/actix/actix-web/pull/2364 [#2375]: https://github.com/actix/actix-web/pull/2375 [#2344]: https://github.com/actix/actix-web/pull/2344 +[#2377]: https://github.com/actix/actix-web/pull/2377 ## 3.0.0-beta.8 - 2021-08-09 ### Fixed * Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977) + ## 3.0.0-beta.8 - 2021-06-26 ### Changed * Change compression algorithm features flags. [#2250] diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 68f980982..54505a215 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -73,7 +73,7 @@ rand = "0.8" regex = "1.3" serde = "1.0" sha-1 = "0.9" -smallvec = "1.6" +smallvec = "1.6.1" time = { version = "0.2.23", default-features = false, features = ["std"] } tokio = { version = "1.2", features = ["sync"] } diff --git a/actix-http/src/encoding/decoder.rs b/actix-http/src/encoding/decoder.rs index 81e97d916..c32983fc7 100644 --- a/actix-http/src/encoding/decoder.rs +++ b/actix-http/src/encoding/decoder.rs @@ -1,7 +1,6 @@ //! Stream decoders. use std::{ - convert::TryFrom, future::Future, io::{self, Write as _}, pin::Pin, @@ -81,7 +80,7 @@ where let encoding = headers .get(&CONTENT_ENCODING) .and_then(|val| val.to_str().ok()) - .and_then(|x| ContentEncoding::try_from(x).ok()) + .and_then(|x| x.parse().ok()) .unwrap_or(ContentEncoding::Identity); Self::new(stream, encoding) diff --git a/actix-http/src/header/shared/content_encoding.rs b/actix-http/src/header/shared/content_encoding.rs index 375e8c2fa..1af109c06 100644 --- a/actix-http/src/header/shared/content_encoding.rs +++ b/actix-http/src/header/shared/content_encoding.rs @@ -1,5 +1,6 @@ -use std::{convert::TryFrom, error, fmt, str::FromStr}; +use std::{convert::TryFrom, str::FromStr}; +use derive_more::{Display, Error}; use http::header::InvalidHeaderValue; use crate::{ @@ -11,19 +12,13 @@ use crate::{ /// Error return when a content encoding is unknown. /// /// Example: 'compress' -#[derive(Debug)] +#[derive(Debug, Display, Error)] +#[display(fmt = "unsupported content encoding")] pub struct ContentEncodingParseError; -impl fmt::Display for ContentEncodingParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Unsupported content encoding") - } -} - -impl error::Error for ContentEncodingParseError {} - /// Represents a supported content encoding. -#[derive(Copy, Clone, PartialEq, Debug)] +#[derive(Debug, Clone, Copy, PartialEq)] +#[non_exhaustive] pub enum ContentEncoding { /// Automatically select encoding based on encoding negotiation. Auto, diff --git a/src/scope.rs b/src/scope.rs index b2edaedab..7d914f581 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -41,9 +41,9 @@ type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Err /// fn main() { /// let app = App::new().service( /// web::scope("/{project_id}/") -/// .service(web::resource("/path1").to(|| async { HttpResponse::Ok() })) +/// .service(web::resource("/path1").to(|| async { "OK" })) /// .service(web::resource("/path2").route(web::get().to(|| HttpResponse::Ok()))) -/// .service(web::resource("/path3").route(web::head().to(|| HttpResponse::MethodNotAllowed()))) +/// .service(web::resource("/path3").route(web::head().to(HttpResponse::MethodNotAllowed))) /// ); /// } /// ``` diff --git a/src/test.rs b/src/test.rs index 634826d19..34dd6f2d3 100644 --- a/src/test.rs +++ b/src/test.rs @@ -56,7 +56,7 @@ pub fn default_service( /// async fn test_init_service() { /// let app = test::init_service( /// App::new() -/// .service(web::resource("/test").to(|| async { HttpResponse::Ok() })) +/// .service(web::resource("/test").to(|| async { "OK" })) /// ).await; /// /// // Create request object diff --git a/src/types/form.rs b/src/types/form.rs index c81f73554..2ace0e063 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -30,7 +30,7 @@ use crate::{ /// /// # Extractor /// To extract typed data from a request body, the inner type `T` must implement the -/// [`serde::Deserialize`] trait. +/// [`DeserializeOwned`] trait. /// /// Use [`FormConfig`] to configure extraction process. /// diff --git a/src/types/json.rs b/src/types/json.rs index ab9708c53..8c2f51a68 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -97,19 +97,13 @@ impl ops::DerefMut for Json { } } -impl fmt::Display for Json -where - T: fmt::Display, -{ +impl fmt::Display for Json { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(&self.0, f) } } -impl Serialize for Json -where - T: Serialize, -{ +impl Serialize for Json { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, @@ -133,10 +127,7 @@ impl Responder for Json { } /// See [here](#extractor) for example of usage as an extractor. -impl FromRequest for Json -where - T: DeserializeOwned + 'static, -{ +impl FromRequest for Json { type Error = Error; type Future = JsonExtractFut; type Config = JsonConfig; @@ -166,10 +157,7 @@ pub struct JsonExtractFut { err_handler: JsonErrorHandler, } -impl Future for JsonExtractFut -where - T: DeserializeOwned + 'static, -{ +impl Future for JsonExtractFut { type Output = Result, Error>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { @@ -311,10 +299,7 @@ pub enum JsonBody { impl Unpin for JsonBody {} -impl JsonBody -where - T: DeserializeOwned + 'static, -{ +impl JsonBody { /// Create a new future to decode a JSON request payload. #[allow(clippy::borrow_interior_mutable_const)] pub fn new( @@ -395,10 +380,7 @@ where } } -impl Future for JsonBody -where - T: DeserializeOwned + 'static, -{ +impl Future for JsonBody { type Output = Result; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { diff --git a/src/types/query.rs b/src/types/query.rs index 1e6f1111f..73d08d092 100644 --- a/src/types/query.rs +++ b/src/types/query.rs @@ -3,14 +3,14 @@ use std::{fmt, ops, sync::Arc}; use actix_utils::future::{err, ok, Ready}; -use serde::de; +use serde::de::DeserializeOwned; use crate::{dev::Payload, error::QueryPayloadError, Error, FromRequest, HttpRequest}; /// Extract typed information from the request's query. /// /// To extract typed data from the URL query string, the inner type `T` must implement the -/// [`serde::Deserialize`] trait. +/// [`DeserializeOwned`] trait. /// /// Use [`QueryConfig`] to configure extraction process. /// @@ -46,18 +46,18 @@ use crate::{dev::Payload, error::QueryPayloadError, Error, FromRequest, HttpRequ /// // To access the entire underlying query struct, use `.into_inner()`. /// #[get("/debug1")] /// async fn debug1(info: web::Query) -> String { -/// dbg!("Authorization object={:?}", info.into_inner()); +/// dbg!("Authorization object = {:?}", info.into_inner()); /// "OK".to_string() /// } /// -/// // Or use `.0`, which is equivalent to `.into_inner()`. +/// // Or use destructuring, which is equivalent to `.into_inner()`. /// #[get("/debug2")] -/// async fn debug2(info: web::Query) -> String { -/// dbg!("Authorization object={:?}", info.0); +/// async fn debug2(web::Query(info): web::Query) -> String { +/// dbg!("Authorization object = {:?}", info); /// "OK".to_string() /// } /// ``` -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Query(pub T); impl Query { @@ -65,8 +65,10 @@ impl Query { pub fn into_inner(self) -> T { self.0 } +} - /// Deserialize `T` from a URL encoded query parameter string. +impl Query { + /// Deserialize a `T` from the URL encoded query parameter string. /// /// ``` /// # use std::collections::HashMap; @@ -76,10 +78,7 @@ impl Query { /// assert_eq!(numbers.get("two"), Some(&2)); /// assert!(numbers.get("three").is_none()); /// ``` - pub fn from_query(query_str: &str) -> Result - where - T: de::DeserializeOwned, - { + pub fn from_query(query_str: &str) -> Result { serde_urlencoded::from_str::(query_str) .map(Self) .map_err(QueryPayloadError::Deserialize) @@ -107,10 +106,7 @@ impl fmt::Display for Query { } /// See [here](#usage) for example of usage as an extractor. -impl FromRequest for Query -where - T: de::DeserializeOwned, -{ +impl FromRequest for Query { type Error = Error; type Future = Ready>; type Config = QueryConfig; @@ -165,7 +161,7 @@ where /// let query_cfg = web::QueryConfig::default() /// // use custom error handler /// .error_handler(|err, req| { -/// error::InternalError::from_response(err, HttpResponse::Conflict().into()).into() +/// error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into() /// }); /// /// App::new() From 53ec66caf47592b2bdfbbab2936d7ac727bcf315 Mon Sep 17 00:00:00 2001 From: Omid Rad Date: Wed, 1 Sep 2021 21:16:41 +0200 Subject: [PATCH 45/88] Send headers within the redirect requests. (#2310) --- awc/CHANGES.md | 3 + awc/src/middleware/redirect.rs | 365 +++++++++++++++++++++++++++------ 2 files changed, 303 insertions(+), 65 deletions(-) diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 16132be1c..9c6f258aa 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,7 +1,10 @@ # Changes ## Unreleased - 2021-xx-xx +### Changed +* Send headers within the redirect requests. [#2310] +[#2310]: https://github.com/actix/actix-web/pull/2310 ## 3.0.0-beta.7 - 2021-06-26 ### Changed diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs index ae09edf9c..a8c14d549 100644 --- a/awc/src/middleware/redirect.rs +++ b/awc/src/middleware/redirect.rs @@ -85,10 +85,12 @@ where let max_redirect_times = self.max_redirect_times; // backup the uri and method for reuse schema and authority. - let (uri, method) = match head { - RequestHeadType::Owned(ref head) => (head.uri.clone(), head.method.clone()), + let (uri, method, headers) = match head { + RequestHeadType::Owned(ref head) => { + (head.uri.clone(), head.method.clone(), head.headers.clone()) + } RequestHeadType::Rc(ref head, ..) => { - (head.uri.clone(), head.method.clone()) + (head.uri.clone(), head.method.clone(), head.headers.clone()) } }; @@ -104,6 +106,7 @@ where max_redirect_times, uri: Some(uri), method: Some(method), + headers: Some(headers), body: body_opt, addr, connector: Some(connector), @@ -127,9 +130,10 @@ pin_project_lite::pin_project! { max_redirect_times: u8, uri: Option, method: Option, + headers: Option, body: Option, addr: Option, - connector: Option> + connector: Option>, } } } @@ -148,6 +152,7 @@ where max_redirect_times, uri, method, + headers, body, addr, connector, @@ -156,79 +161,60 @@ where StatusCode::MOVED_PERMANENTLY | StatusCode::FOUND | StatusCode::SEE_OTHER + | StatusCode::TEMPORARY_REDIRECT + | StatusCode::PERMANENT_REDIRECT if *max_redirect_times > 0 => { - let org_uri = uri.take().unwrap(); - // rebuild uri from the location header value. - let uri = rebuild_uri(&res, org_uri)?; + let is_redirect = res.head().status == StatusCode::TEMPORARY_REDIRECT + || res.head().status == StatusCode::PERMANENT_REDIRECT; - // reset method - let method = method.take().unwrap(); - let method = match method { - Method::GET | Method::HEAD => method, - _ => Method::GET, - }; + let prev_uri = uri.take().unwrap(); + + // rebuild uri from the location header value. + let next_uri = build_next_uri(&res, &prev_uri)?; // take ownership of states that could be reused let addr = addr.take(); let connector = connector.take(); - let mut max_redirect_times = *max_redirect_times; - // use a new request head. - let mut head = RequestHead::default(); - head.uri = uri.clone(); - head.method = method.clone(); - - let head = RequestHeadType::Owned(head); - - max_redirect_times -= 1; - - let fut = connector - .as_ref() - .unwrap() - // remove body - .call(ConnectRequest::Client(head, Body::None, addr)); - - self.set(RedirectServiceFuture::Client { - fut, - max_redirect_times, - uri: Some(uri), - method: Some(method), - // body is dropped on 301,302,303 - body: None, - addr, - connector, - }); - - self.poll(cx) - } - StatusCode::TEMPORARY_REDIRECT | StatusCode::PERMANENT_REDIRECT - if *max_redirect_times > 0 => - { - let org_uri = uri.take().unwrap(); - // rebuild uri from the location header value. - let uri = rebuild_uri(&res, org_uri)?; - - // try to reuse body - let body = body.take(); - let body_new = match body { - Some(ref bytes) => Body::Bytes(bytes.clone()), - // TODO: should this be Body::Empty or Body::None. - _ => Body::Empty, + // reset method + let method = if is_redirect { + method.take().unwrap() + } else { + let method = method.take().unwrap(); + match method { + Method::GET | Method::HEAD => method, + _ => Method::GET, + } }; - let addr = addr.take(); - let method = method.take().unwrap(); - let connector = connector.take(); - let mut max_redirect_times = *max_redirect_times; + let mut body = body.take(); + let body_new = if is_redirect { + // try to reuse body + match body { + Some(ref bytes) => Body::Bytes(bytes.clone()), + // TODO: should this be Body::Empty or Body::None. + _ => Body::Empty, + } + } else { + body = None; + // remove body + Body::None + }; + + let mut headers = headers.take().unwrap(); + + remove_sensitive_headers(&mut headers, &prev_uri, &next_uri); // use a new request head. let mut head = RequestHead::default(); - head.uri = uri.clone(); + head.uri = next_uri.clone(); head.method = method.clone(); + head.headers = headers.clone(); let head = RequestHeadType::Owned(head); + let mut max_redirect_times = *max_redirect_times; max_redirect_times -= 1; let fut = connector @@ -239,8 +225,9 @@ where self.set(RedirectServiceFuture::Client { fut, max_redirect_times, - uri: Some(uri), + uri: Some(next_uri), method: Some(method), + headers: Some(headers), body, addr, connector, @@ -256,7 +243,7 @@ where } } -fn rebuild_uri(res: &ClientResponse, org_uri: Uri) -> Result { +fn build_next_uri(res: &ClientResponse, prev_uri: &Uri) -> Result { let uri = res .headers() .get(header::LOCATION) @@ -266,8 +253,8 @@ fn rebuild_uri(res: &ClientResponse, org_uri: Uri) -> Result(uri) @@ -281,12 +268,25 @@ fn rebuild_uri(res: &ClientResponse, org_uri: Uri) -> Result HttpResponse { + HttpResponse::TemporaryRedirect() + .append_header(("location", "/test")) + .finish() + } + + async fn test(req: HttpRequest, body: Bytes) -> HttpResponse { + if req.method() == Method::POST && !body.is_empty() { + HttpResponse::Ok().finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + App::new() + .service(web::resource("/").route(web::to(root))) + .service(web::resource("/test").route(web::to(test))) + }); + + let res = srv.post("/").send_body("Hello").await.unwrap(); + assert_eq!(res.status().as_u16(), 200); + } + + #[actix_rt::test] + async fn test_redirect_status_kind_301_302_303() { + let srv = actix_test::start(|| { + async fn root() -> HttpResponse { + HttpResponse::Found() + .append_header(("location", "/test")) + .finish() + } + + async fn test(req: HttpRequest, body: Bytes) -> HttpResponse { + if (req.method() == Method::GET || req.method() == Method::HEAD) + && body.is_empty() + { + HttpResponse::Ok().finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + App::new() + .service(web::resource("/").route(web::to(root))) + .service(web::resource("/test").route(web::to(test))) + }); + + let res = srv.post("/").send_body("Hello").await.unwrap(); + assert_eq!(res.status().as_u16(), 200); + + let res = srv.post("/").send().await.unwrap(); + assert_eq!(res.status().as_u16(), 200); + } + + #[actix_rt::test] + async fn test_redirect_headers() { + let srv = actix_test::start(|| { + async fn root(req: HttpRequest) -> HttpResponse { + if req + .headers() + .get("custom") + .unwrap_or(&HeaderValue::from_str("").unwrap()) + == "value" + { + HttpResponse::Found() + .append_header(("location", "/test")) + .finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + async fn test(req: HttpRequest) -> HttpResponse { + if req + .headers() + .get("custom") + .unwrap_or(&HeaderValue::from_str("").unwrap()) + == "value" + { + HttpResponse::Ok().finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + App::new() + .service(web::resource("/").route(web::to(root))) + .service(web::resource("/test").route(web::to(test))) + }); + + let client = ClientBuilder::new() + .header("custom", "value") + .disable_redirects() + .finish(); + let res = client.get(srv.url("/")).send().await.unwrap(); + assert_eq!(res.status().as_u16(), 302); + + let client = ClientBuilder::new().header("custom", "value").finish(); + let res = client.get(srv.url("/")).send().await.unwrap(); + assert_eq!(res.status().as_u16(), 200); + + let client = ClientBuilder::new().finish(); + let res = client + .get(srv.url("/")) + .insert_header(("custom", "value")) + .send() + .await + .unwrap(); + assert_eq!(res.status().as_u16(), 200); + } + + #[actix_rt::test] + async fn test_redirect_cross_origin_headers() { + // defining two services to have two different origins + let srv2 = actix_test::start(|| { + async fn root(req: HttpRequest) -> HttpResponse { + if req.headers().get(header::AUTHORIZATION).is_none() { + HttpResponse::Ok().finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + App::new().service(web::resource("/").route(web::to(root))) + }); + let srv2_port: u16 = srv2.addr().port(); + + let srv1 = actix_test::start(move || { + async fn root(req: HttpRequest) -> HttpResponse { + let port = *req.app_data::().unwrap(); + if req.headers().get(header::AUTHORIZATION).is_some() { + HttpResponse::Found() + .append_header(( + "location", + format!("http://localhost:{}/", port).as_str(), + )) + .finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + async fn test1(req: HttpRequest) -> HttpResponse { + if req.headers().get(header::AUTHORIZATION).is_some() { + HttpResponse::Found() + .append_header(("location", "/test2")) + .finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + async fn test2(req: HttpRequest) -> HttpResponse { + if req.headers().get(header::AUTHORIZATION).is_some() { + HttpResponse::Ok().finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + App::new() + .app_data(srv2_port) + .service(web::resource("/").route(web::to(root))) + .service(web::resource("/test1").route(web::to(test1))) + .service(web::resource("/test2").route(web::to(test2))) + }); + + // send a request to different origins, http://srv1/ then http://srv2/. So it should remove the header + let client = ClientBuilder::new() + .header(header::AUTHORIZATION, "auth_key_value") + .finish(); + let res = client.get(srv1.url("/")).send().await.unwrap(); + assert_eq!(res.status().as_u16(), 200); + + // send a request to same origin, http://srv1/test1 then http://srv1/test2. So it should NOT remove any header + let res = client.get(srv1.url("/test1")).send().await.unwrap(); + assert_eq!(res.status().as_u16(), 200); + } + + #[actix_rt::test] + async fn test_remove_sensitive_headers() { + fn gen_headers() -> header::HeaderMap { + let mut headers = header::HeaderMap::new(); + headers.insert(header::USER_AGENT, HeaderValue::from_str("value").unwrap()); + headers.insert( + header::AUTHORIZATION, + HeaderValue::from_str("value").unwrap(), + ); + headers.insert( + header::PROXY_AUTHORIZATION, + HeaderValue::from_str("value").unwrap(), + ); + headers.insert(header::COOKIE, HeaderValue::from_str("value").unwrap()); + headers + } + + // Same origin + let prev_uri = Uri::from_str("https://host/path1").unwrap(); + let next_uri = Uri::from_str("https://host/path2").unwrap(); + let mut headers = gen_headers(); + remove_sensitive_headers(&mut headers, &prev_uri, &next_uri); + assert_eq!(headers.len(), 4); + + // different schema + let prev_uri = Uri::from_str("http://host/").unwrap(); + let next_uri = Uri::from_str("https://host/").unwrap(); + let mut headers = gen_headers(); + remove_sensitive_headers(&mut headers, &prev_uri, &next_uri); + assert_eq!(headers.len(), 1); + + // different host + let prev_uri = Uri::from_str("https://host1/").unwrap(); + let next_uri = Uri::from_str("https://host2/").unwrap(); + let mut headers = gen_headers(); + remove_sensitive_headers(&mut headers, &prev_uri, &next_uri); + assert_eq!(headers.len(), 1); + + // different port + let prev_uri = Uri::from_str("https://host:12/").unwrap(); + let next_uri = Uri::from_str("https://host:23/").unwrap(); + let mut headers = gen_headers(); + remove_sensitive_headers(&mut headers, &prev_uri, &next_uri); + assert_eq!(headers.len(), 1); + + // different everything! + let prev_uri = Uri::from_str("http://host1:12/path1").unwrap(); + let next_uri = Uri::from_str("https://host2:23/path2").unwrap(); + let mut headers = gen_headers(); + remove_sensitive_headers(&mut headers, &prev_uri, &next_uri); + assert_eq!(headers.len(), 1); + } } From d8a0f46f264dd52a8d17a8c97036dcf9fc717cbb Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 3 Sep 2021 18:00:43 +0100 Subject: [PATCH 46/88] refactor web module (#2379) --- .cargo/config.toml | 2 +- .github/workflows/ci.yml | 40 ++++- CHANGES.md | 5 +- src/dev.rs | 4 +- src/http/header/content_disposition.rs | 10 +- src/lib.rs | 1 - src/service.rs | 2 +- src/web.rs | 220 +++++++------------------ 8 files changed, 103 insertions(+), 181 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index db47ca46d..f417a7053 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -6,4 +6,4 @@ ci-min-test = "hack check --workspace --no-default-features --tests --examples" ci-default = "check --workspace --bins --tests --examples" ci-full = "check --workspace --all-features --bins --tests --examples" ci-test = "test --workspace --all-features --lib --tests --no-fail-fast -- --nocapture" -ci-doctest = "hack test --workspace --all-features --doc --no-fail-fast -- --nocapture" +ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 221d2fb40..647501579 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,13 +80,6 @@ jobs: command: ci-test args: --skip=test_reading_deflate_encoding_large_random_rustls - - name: doc tests - # due to unknown issue with running doc tests on macOS - if: matrix.target.os == 'ubuntu-latest' - uses: actions-rs/cargo@v1 - timeout-minutes: 40 - with: { command: ci-doctest } - - name: Generate coverage file if: > matrix.target.os == 'ubuntu-latest' @@ -106,5 +99,36 @@ jobs: - name: Clear the cargo caches run: | - cargo install cargo-cache --version 0.6.2 --no-default-features --features ci-autoclean + cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean cargo-cache + + rustdoc: + name: rustdoc + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Install Rust (nightly) + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: { command: generate-lockfile } + - name: Cache Dependencies + uses: Swatinem/rust-cache@v1.3.0 + + - name: Install cargo-hack + uses: actions-rs/cargo@v1 + with: + command: install + args: cargo-hack + + - name: doc tests + uses: actions-rs/cargo@v1 + timeout-minutes: 40 + with: { command: ci-doctest } diff --git a/CHANGES.md b/CHANGES.md index 217ec4f78..6826be075 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,14 +5,17 @@ * Re-export actix-service `ServiceFactory` in `dev` module. [#2325] ### Changed -* Minimum supported Rust version (MSRV) is now 1.51. * Compress middleware will return 406 Not Acceptable when no content encoding is acceptable to the client. [#2344] +* Move `BaseHttpResponse` to `dev::Response`. [#2379] +* Minimum supported Rust version (MSRV) is now 1.51. ### Fixed * Fix quality parse error in Accept-Encoding header. [#2344] +* Re-export correct type at `web::HttpResponse`. [#2379] [#2325]: https://github.com/actix/actix-web/pull/2325 [#2344]: https://github.com/actix/actix-web/pull/2344 +[#2379]: https://github.com/actix/actix-web/pull/2379 ## 4.0.0-beta.8 - 2021-06-26 diff --git a/src/dev.rs b/src/dev.rs index 0817d902f..be3af86a8 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -18,7 +18,7 @@ pub use actix_http::body::{AnyBody, Body, BodySize, MessageBody, ResponseBody, S #[cfg(feature = "__compress")] pub use actix_http::encoding::Decoder as Decompress; -pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, ResponseHead}; +pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, Response, ResponseHead}; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; pub use actix_server::Server; pub use actix_service::{ @@ -26,7 +26,7 @@ pub use actix_service::{ }; use crate::http::header::ContentEncoding; -use actix_http::{Response, ResponseBuilder}; +use actix_http::ResponseBuilder; use actix_router::Patterns; diff --git a/src/http/header/content_disposition.rs b/src/http/header/content_disposition.rs index 6e75fde92..fdd8a7dac 100644 --- a/src/http/header/content_disposition.rs +++ b/src/http/header/content_disposition.rs @@ -1,10 +1,10 @@ //! # References //! -//! "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt -//! "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt -//! "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc7578.txt -//! Browser conformance tests at: http://greenbytes.de/tech/tc2231/ -//! IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml +//! "The Content-Disposition Header Field" +//! "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" +//! "Returning Values from Forms: multipart/form-data" +//! Browser conformance tests at: +//! IANA assignment: use once_cell::sync::Lazy; use regex::Regex; diff --git a/src/lib.rs b/src/lib.rs index e7cf46361..d008fdb7f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -96,7 +96,6 @@ pub mod test; pub(crate) mod types; pub mod web; -pub use actix_http::Response as BaseHttpResponse; pub use actix_http::{body, HttpMessage}; #[doc(inline)] pub use actix_rt as rt; diff --git a/src/service.rs b/src/service.rs index 48167e5b3..b9fa0e128 100644 --- a/src/service.rs +++ b/src/service.rs @@ -476,7 +476,7 @@ impl WebService { /// Set service name. /// - /// Name is used for url generation. + /// Name is used for URL generation. pub fn name(mut self, name: &str) -> Self { self.name = Some(name.to_string()); self diff --git a/src/web.rs b/src/web.rs index 108ff314f..40d7636cf 100644 --- a/src/web.rs +++ b/src/web.rs @@ -3,44 +3,36 @@ use std::future::Future; use actix_http::http::Method; -pub use actix_http::Response as HttpResponse; use actix_router::IntoPatterns; pub use bytes::{Buf, BufMut, Bytes, BytesMut}; -use crate::error::BlockingError; -use crate::extract::FromRequest; -use crate::handler::Handler; -use crate::resource::Resource; -use crate::responder::Responder; -use crate::route::Route; -use crate::scope::Scope; -use crate::service::WebService; +use crate::{ + error::BlockingError, extract::FromRequest, handler::Handler, resource::Resource, + responder::Responder, route::Route, scope::Scope, service::WebService, +}; pub use crate::config::ServiceConfig; pub use crate::data::Data; pub use crate::request::HttpRequest; pub use crate::request_data::ReqData; +pub use crate::response::HttpResponse; pub use crate::types::*; -/// Create resource for a specific path. +/// Creates a new resource for a specific path. /// -/// Resources may have variable path segments. For example, a -/// resource with the path `/a/{name}/c` would match all incoming -/// requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`. +/// Resources may have dynamic path segments. For example, a resource with the path `/a/{name}/c` +/// would match all incoming requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`. /// -/// A variable segment is specified in the form `{identifier}`, -/// where the identifier can be used later in a request handler to -/// access the matched value for that segment. This is done by -/// looking up the identifier in the `Params` object returned by -/// `HttpRequest.match_info()` method. +/// A dynamic segment is specified in the form `{identifier}`, where the identifier can be used +/// later in a request handler to access the matched value for that segment. This is done by looking +/// up the identifier in the `Path` object returned by [`HttpRequest.match_info()`] method. /// /// By default, each segment matches the regular expression `[^{}/]+`. /// /// You can also specify a custom regex in the form `{identifier:regex}`: /// -/// For instance, to route `GET`-requests on any route matching -/// `/users/{userid}/{friend}` and store `userid` and `friend` in -/// the exposed `Params` object: +/// For instance, to route `GET`-requests on any route matching `/users/{userid}/{friend}` and store +/// `userid` and `friend` in the exposed `Path` object: /// /// ``` /// use actix_web::{web, App, HttpResponse}; @@ -55,10 +47,16 @@ pub fn resource(path: T) -> Resource { Resource::new(path) } -/// Configure scope for common root path. +/// Creates scope for common path prefix. /// -/// Scopes collect multiple paths under a common path prefix. -/// Scope path can contain variable path segments as resources. +/// Scopes collect multiple paths under a common path prefix. The scope's path can contain dynamic +/// path segments. +/// +/// # Examples +/// In this example, three routes are set up (and will handle any method): +/// * `/{project_id}/path1` +/// * `/{project_id}/path2` +/// * `/{project_id}/path3` /// /// ``` /// use actix_web::{web, App, HttpResponse}; @@ -70,148 +68,50 @@ pub fn resource(path: T) -> Resource { /// .service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed())) /// ); /// ``` -/// -/// In the above example, three routes get added: -/// * /{project_id}/path1 -/// * /{project_id}/path2 -/// * /{project_id}/path3 -/// pub fn scope(path: &str) -> Scope { Scope::new(path) } -/// Create *route* without configuration. +/// Creates a new un-configured route. pub fn route() -> Route { Route::new() } -/// Create *route* with `GET` method guard. -/// -/// ``` -/// use actix_web::{web, App, HttpResponse}; -/// -/// let app = App::new().service( -/// web::resource("/{project_id}") -/// .route(web::get().to(|| HttpResponse::Ok())) -/// ); -/// ``` -/// -/// In the above example, one `GET` route gets added: -/// * /{project_id} -/// -pub fn get() -> Route { - method(Method::GET) +macro_rules! method_route { + ($method_fn:ident, $method_const:ident) => { + paste::paste! { + #[doc = "Creates a new route with `" $method_const "` method guard."] + /// + /// # Examples + #[doc = "In this example, one `" $method_const " /{project_id}` route is set up:"] + /// ``` + /// use actix_web::{web, App, HttpResponse}; + /// + /// let app = App::new().service( + /// web::resource("/{project_id}") + #[doc = " .route(web::" $method_fn "().to(|| HttpResponse::Ok()))"] + /// + /// ); + /// ``` + pub fn $method_fn() -> Route { + method(Method::$method_const) + } + } + }; } -/// Create *route* with `POST` method guard. -/// -/// ``` -/// use actix_web::{web, App, HttpResponse}; -/// -/// let app = App::new().service( -/// web::resource("/{project_id}") -/// .route(web::post().to(|| HttpResponse::Ok())) -/// ); -/// ``` -/// -/// In the above example, one `POST` route gets added: -/// * /{project_id} -/// -pub fn post() -> Route { - method(Method::POST) -} +method_route!(get, GET); +method_route!(post, POST); +method_route!(put, PUT); +method_route!(patch, PATCH); +method_route!(delete, DELETE); +method_route!(head, HEAD); +method_route!(trace, TRACE); -/// Create *route* with `PUT` method guard. +/// Creates a new route with specified method guard. /// -/// ``` -/// use actix_web::{web, App, HttpResponse}; -/// -/// let app = App::new().service( -/// web::resource("/{project_id}") -/// .route(web::put().to(|| HttpResponse::Ok())) -/// ); -/// ``` -/// -/// In the above example, one `PUT` route gets added: -/// * /{project_id} -/// -pub fn put() -> Route { - method(Method::PUT) -} - -/// Create *route* with `PATCH` method guard. -/// -/// ``` -/// use actix_web::{web, App, HttpResponse}; -/// -/// let app = App::new().service( -/// web::resource("/{project_id}") -/// .route(web::patch().to(|| HttpResponse::Ok())) -/// ); -/// ``` -/// -/// In the above example, one `PATCH` route gets added: -/// * /{project_id} -/// -pub fn patch() -> Route { - method(Method::PATCH) -} - -/// Create *route* with `DELETE` method guard. -/// -/// ``` -/// use actix_web::{web, App, HttpResponse}; -/// -/// let app = App::new().service( -/// web::resource("/{project_id}") -/// .route(web::delete().to(|| HttpResponse::Ok())) -/// ); -/// ``` -/// -/// In the above example, one `DELETE` route gets added: -/// * /{project_id} -/// -pub fn delete() -> Route { - method(Method::DELETE) -} - -/// Create *route* with `HEAD` method guard. -/// -/// ``` -/// use actix_web::{web, App, HttpResponse}; -/// -/// let app = App::new().service( -/// web::resource("/{project_id}") -/// .route(web::head().to(|| HttpResponse::Ok())) -/// ); -/// ``` -/// -/// In the above example, one `HEAD` route gets added: -/// * /{project_id} -/// -pub fn head() -> Route { - method(Method::HEAD) -} - -/// Create *route* with `TRACE` method guard. -/// -/// ``` -/// use actix_web::{web, App, HttpResponse}; -/// -/// let app = App::new().service( -/// web::resource("/{project_id}") -/// .route(web::trace().to(|| HttpResponse::Ok())) -/// ); -/// ``` -/// -/// In the above example, one `HEAD` route gets added: -/// * /{project_id} -/// -pub fn trace() -> Route { - method(Method::TRACE) -} - -/// Create *route* and add method guard. +/// # Examples +/// In this example, one `GET /{project_id}` route is set up: /// /// ``` /// use actix_web::{web, http, App, HttpResponse}; @@ -221,15 +121,11 @@ pub fn trace() -> Route { /// .route(web::method(http::Method::GET).to(|| HttpResponse::Ok())) /// ); /// ``` -/// -/// In the above example, one `GET` route gets added: -/// * /{project_id} -/// pub fn method(method: Method) -> Route { Route::new().method(method) } -/// Create a new route and add handler. +/// Creates a new any-method route with handler. /// /// ``` /// use actix_web::{web, App, HttpResponse, Responder}; @@ -253,7 +149,7 @@ where Route::new().to(handler) } -/// Create raw service for a specific path. +/// Creates a raw service for a specific path. /// /// ``` /// use actix_web::{dev, web, guard, App, Error, HttpResponse}; @@ -272,8 +168,8 @@ pub fn service(path: T) -> WebService { WebService::new(path) } -/// Execute blocking function on a thread pool, returns future that resolves -/// to result of the function execution. +/// Executes blocking function on a thread pool, returns future that resolves to result of the +/// function execution. pub fn block(f: F) -> impl Future> where F: FnOnce() -> R + Send + 'static, From 1383c7d701c35df45abc425e70dae69d9bab1317 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 8 Sep 2021 17:42:14 +0100 Subject: [PATCH 47/88] speed up ci --- .github/workflows/ci.yml | 2 ++ Cargo.toml | 4 ++++ src/web.rs | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 647501579..1ec034bc8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,8 @@ jobs: runs-on: ${{ matrix.target.os }} env: + CI: 1 + CARGO_INCREMENTAL: 0 VCPKGRS_DYNAMIC: 1 steps: diff --git a/Cargo.toml b/Cargo.toml index 699717b4d..05ed2eb2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,6 +118,10 @@ rcgen = "0.8" tls-openssl = { package = "openssl", version = "0.10.9" } tls-rustls = { package = "rustls", version = "0.19.0" } +[profile.dev] +# Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much. +debug = 0 + [profile.release] lto = true opt-level = 3 diff --git a/src/web.rs b/src/web.rs index 40d7636cf..e9f5c8518 100644 --- a/src/web.rs +++ b/src/web.rs @@ -80,10 +80,10 @@ pub fn route() -> Route { macro_rules! method_route { ($method_fn:ident, $method_const:ident) => { paste::paste! { - #[doc = "Creates a new route with `" $method_const "` method guard."] + #[doc = " Creates a new route with `" $method_const "` method guard."] /// /// # Examples - #[doc = "In this example, one `" $method_const " /{project_id}` route is set up:"] + #[doc = " In this example, one `" $method_const " /{project_id}` route is set up:"] /// ``` /// use actix_web::{web, App, HttpResponse}; /// From 8dd30611faf552108638e7719025cbe5ac3d76e8 Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Wed, 8 Sep 2021 19:42:40 -0400 Subject: [PATCH 48/88] accept owned strings in TestRequest::param (#2172) * accept owned strings in TestRequest::param * bump actix-router to 0.4.0 * update changelog Co-authored-by: Rob Ede --- CHANGES.md | 2 ++ src/test.rs | 22 +++++++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6826be075..33898794b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,12 +7,14 @@ ### Changed * Compress middleware will return 406 Not Acceptable when no content encoding is acceptable to the client. [#2344] * Move `BaseHttpResponse` to `dev::Response`. [#2379] +* Enable `TestRequest::param` to accept more than just static strings. [#2172] * Minimum supported Rust version (MSRV) is now 1.51. ### Fixed * Fix quality parse error in Accept-Encoding header. [#2344] * Re-export correct type at `web::HttpResponse`. [#2379] +[#2172]: https://github.com/actix/actix-web/pull/2172 [#2325]: https://github.com/actix/actix-web/pull/2325 [#2344]: https://github.com/actix/actix-web/pull/2344 [#2379]: https://github.com/actix/actix-web/pull/2379 diff --git a/src/test.rs b/src/test.rs index 34dd6f2d3..99e708592 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,6 +1,6 @@ //! Various helpers for Actix applications to use during testing. -use std::{net::SocketAddr, rc::Rc}; +use std::{borrow::Cow, net::SocketAddr, rc::Rc}; pub use actix_http::test::TestBuffer; use actix_http::{ @@ -470,19 +470,31 @@ impl TestRequest { self } - /// Set request path pattern parameter - pub fn param(mut self, name: &'static str, value: &'static str) -> Self { + /// Set request path pattern parameter. + /// + /// # Examples + /// ``` + /// use actix_web::test::TestRequest; + /// + /// let req = TestRequest::default().param("foo", "bar"); + /// let req = TestRequest::default().param("foo".to_owned(), "bar".to_owned()); + /// ``` + pub fn param( + mut self, + name: impl Into>, + value: impl Into>, + ) -> Self { self.path.add_static(name, value); self } - /// Set peer addr + /// Set peer addr. pub fn peer_addr(mut self, addr: SocketAddr) -> Self { self.peer_addr = Some(addr); self } - /// Set request payload + /// Set request payload. pub fn set_payload>(mut self, data: B) -> Self { self.req.set_payload(data); self From ba88d3b4bf1cc3cccfd17d53f907422257e16944 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 9 Sep 2021 01:35:41 +0100 Subject: [PATCH 49/88] prepare actix-web beta.9 releases (#2381) * prepare actix-router release 0.5.0-beta.2 * prepare actix-web-codegen release 0.5.0-beta.4 * prepare actix-http release 3.0.0-beta.10 * prepare awc release 3.0.0-beta.8 * prepare actix-web release 4.0.0-beta.9 * prepare actix-http-test release 3.0.0-beta.6 * prepare actix-test release 0.1.0-beta.4 * prepare actix-files release 0.6.0-beta.7 * prepare actix-multipart release 0.4.0-beta.6 * prepare actix-web-actors release 4.0.0-beta.7 * fix http test version * re-add patch * update router repo url * fix http test readme version --- CHANGES.md | 3 +++ Cargo.toml | 10 +++++----- README.md | 4 ++-- actix-files/CHANGES.md | 3 +++ actix-files/Cargo.toml | 8 ++++---- actix-files/README.md | 4 ++-- actix-http-test/CHANGES.md | 3 +++ actix-http-test/Cargo.toml | 20 ++++++++++---------- actix-http-test/README.md | 4 ++-- actix-http/CHANGES.md | 5 ++++- actix-http/Cargo.toml | 4 ++-- actix-http/README.md | 4 ++-- actix-multipart/CHANGES.md | 3 +++ actix-multipart/Cargo.toml | 8 +++----- actix-multipart/README.md | 4 ++-- actix-router/CHANGES.md | 3 +++ actix-router/Cargo.toml | 4 ++-- actix-test/CHANGES.md | 3 +++ actix-test/Cargo.toml | 10 +++++----- actix-web-actors/CHANGES.md | 3 +++ actix-web-actors/Cargo.toml | 8 ++++---- actix-web-actors/README.md | 4 ++-- actix-web-codegen/CHANGES.md | 3 +++ actix-web-codegen/Cargo.toml | 6 +++--- actix-web-codegen/README.md | 4 ++-- awc/CHANGES.md | 4 ++++ awc/Cargo.toml | 10 +++++----- awc/README.md | 4 ++-- 28 files changed, 91 insertions(+), 62 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 33898794b..398ac477a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 4.0.0-beta.9 - 2021-09-09 ### Added * Re-export actix-service `ServiceFactory` in `dev` module. [#2325] diff --git a/Cargo.toml b/Cargo.toml index 05ed2eb2d..60525f3ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "4.0.0-beta.8" +version = "4.0.0-beta.9" authors = ["Nikolay Kim "] description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" keywords = ["actix", "http", "web", "framework", "async"] @@ -69,15 +69,15 @@ __compress = [] [dependencies] actix-codec = "0.4.0" actix-macros = "0.2.1" -actix-router = "0.5.0-beta.1" +actix-router = "0.5.0-beta.2" actix-rt = "2.2" actix-server = "2.0.0-beta.3" actix-service = "2.0.0" actix-utils = "3.0.0" actix-tls = { version = "3.0.0-beta.5", default-features = false, optional = true } -actix-web-codegen = "0.5.0-beta.2" -actix-http = "3.0.0-beta.9" +actix-web-codegen = "0.5.0-beta.4" +actix-http = "3.0.0-beta.10" ahash = "0.7" bytes = "1" @@ -106,7 +106,7 @@ url = "2.1" [dev-dependencies] actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] } -awc = { version = "3.0.0-beta.7", features = ["openssl"] } +awc = { version = "3.0.0-beta.8", features = ["openssl"] } brotli2 = "0.3.2" criterion = { version = "0.3", features = ["html_reports"] } diff --git a/README.md b/README.md index 33784d66a..13ec3a01a 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@

[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) -[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.8)](https://docs.rs/actix-web/4.0.0-beta.8) +[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.9)](https://docs.rs/actix-web/4.0.0-beta.9) [![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) -[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.8/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.8) +[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.9/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.9)
[![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 533f72291..6d1512c22 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.6.0-beta.7 - 2021-09-09 * Minimum supported Rust version (MSRV) is now 1.51. diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index ef288215b..eccf49a77 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.6.0-beta.6" +version = "0.6.0-beta.7" authors = ["Nikolay Kim "] description = "Static file serving for Actix Web" keywords = ["actix", "http", "async", "futures"] @@ -15,8 +15,8 @@ name = "actix_files" path = "src/lib.rs" [dependencies] -actix-web = { version = "4.0.0-beta.8", default-features = false } -actix-http = "3.0.0-beta.8" +actix-web = { version = "4.0.0-beta.9", default-features = false } +actix-http = "3.0.0-beta.10" actix-service = "2.0.0" actix-utils = "3.0.0" @@ -33,5 +33,5 @@ percent-encoding = "2.1" [dev-dependencies] actix-rt = "2.2" -actix-web = "4.0.0-beta.8" +actix-web = "4.0.0-beta.9" actix-test = "0.1.0-beta.3" diff --git a/actix-files/README.md b/actix-files/README.md index 5815ef563..31bbd036f 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -3,11 +3,11 @@ > Static file serving for Actix Web [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) -[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.6)](https://docs.rs/actix-files/0.6.0-beta.6) +[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.7)](https://docs.rs/actix-files/0.6.0-beta.7) [![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![License](https://img.shields.io/crates/l/actix-files.svg)
-[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.6/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.6) +[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.7/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.7) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index 39b6a3a66..69e96f98d 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.5 - 2021-09-09 * Minimum supported Rust version (MSRV) is now 1.51. diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index c04b5da49..e7fe7adc0 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -1,18 +1,18 @@ [package] name = "actix-http-test" -version = "3.0.0-beta.4" +version = "3.0.0-beta.5" authors = ["Nikolay Kim "] description = "Various helpers for Actix applications to use during testing" -readme = "README.md" keywords = ["http", "web", "framework", "async", "futures"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" -documentation = "https://docs.rs/actix-http-test/" -categories = ["network-programming", "asynchronous", - "web-programming::http-server", - "web-programming::websocket"] +categories = [ + "network-programming", + "asynchronous", + "web-programming::http-server", + "web-programming::websocket", +] license = "MIT OR Apache-2.0" -exclude = [".gitignore", ".cargo/config"] edition = "2018" [package.metadata.docs.rs] @@ -35,7 +35,7 @@ actix-tls = "3.0.0-beta.5" actix-utils = "3.0.0" actix-rt = "2.2" actix-server = "2.0.0-beta.3" -awc = { version = "3.0.0-beta.7", default-features = false } +awc = { version = "3.0.0-beta.8", default-features = false } base64 = "0.13" bytes = "1" @@ -51,5 +51,5 @@ time = { version = "0.2.23", default-features = false, features = ["std"] } tls-openssl = { version = "0.10.9", package = "openssl", optional = true } [dev-dependencies] -actix-web = { version = "4.0.0-beta.8", default-features = false, features = ["cookies"] } -actix-http = "3.0.0-beta.8" +actix-web = { version = "4.0.0-beta.9", default-features = false, features = ["cookies"] } +actix-http = "3.0.0-beta.10" diff --git a/actix-http-test/README.md b/actix-http-test/README.md index 099fb385d..f75b9c137 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -3,11 +3,11 @@ > Various helpers for Actix applications to use during testing. [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) -[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.4)](https://docs.rs/actix-http-test/3.0.0-beta.4) +[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.5)](https://docs.rs/actix-http-test/3.0.0-beta.5) [![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
-[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.4/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.4) +[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.5/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.5) [![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 65206cf9a..775b9e6d5 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.10 - 2021-09-09 ### Changed * `ContentEncoding` is now marked `#[non_exhaustive]`. [#2377] * Minimum supported Rust version (MSRV) is now 1.51. @@ -16,7 +19,7 @@ [#2377]: https://github.com/actix/actix-web/pull/2377 -## 3.0.0-beta.8 - 2021-08-09 +## 3.0.0-beta.9 - 2021-08-09 ### Fixed * Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977) diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 54505a215..0e0da8f43 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "3.0.0-beta.9" +version = "3.0.0-beta.10" authors = ["Nikolay Kim "] description = "HTTP primitives for the Actix ecosystem" keywords = ["actix", "http", "framework", "async", "futures"] @@ -86,7 +86,7 @@ trust-dns-resolver = { version = "0.20.0", optional = true } [dev-dependencies] actix-server = "2.0.0-beta.3" -actix-http-test = { version = "3.0.0-beta.4", features = ["openssl"] } +actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] } actix-tls = { version = "3.0.0-beta.5", features = ["openssl"] } async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } diff --git a/actix-http/README.md b/actix-http/README.md index c509eaff8..b58b47f5c 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -3,11 +3,11 @@ > HTTP primitives for the Actix ecosystem. [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) -[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.9)](https://docs.rs/actix-http/3.0.0-beta.9) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.10)](https://docs.rs/actix-http/3.0.0-beta.10) [![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
-[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.9/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.9) +[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.10/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.10) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 1e768ddf5..c32583f08 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.4.0-beta.6 - 2021-09-09 * Minimum supported Rust version (MSRV) is now 1.51. diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 5103407ca..6db81cca9 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,13 +1,11 @@ [package] name = "actix-multipart" -version = "0.4.0-beta.5" +version = "0.4.0-beta.6" authors = ["Nikolay Kim "] description = "Multipart form support for Actix Web" -readme = "README.md" keywords = ["http", "web", "framework", "async", "futures"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" -documentation = "https://docs.rs/actix-multipart" license = "MIT OR Apache-2.0" edition = "2018" @@ -16,7 +14,7 @@ name = "actix_multipart" path = "src/lib.rs" [dependencies] -actix-web = { version = "4.0.0-beta.8", default-features = false } +actix-web = { version = "4.0.0-beta.9", default-features = false } actix-utils = "3.0.0" bytes = "1" @@ -31,6 +29,6 @@ twoway = "0.2" [dev-dependencies] actix-rt = "2.2" -actix-http = "3.0.0-beta.8" +actix-http = "3.0.0-beta.10" tokio = { version = "1", features = ["sync"] } tokio-stream = "0.1" diff --git a/actix-multipart/README.md b/actix-multipart/README.md index aed16721c..f3366f50c 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -3,11 +3,11 @@ > Multipart form support for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) -[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.5)](https://docs.rs/actix-multipart/0.4.0-beta.5) +[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.6)](https://docs.rs/actix-multipart/0.4.0-beta.6) [![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
-[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.5/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.5) +[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.6/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.6) [![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index 990382512..001903438 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.5.0-beta.2 - 2021-09-09 * Introduce `ResourceDef::join`. [#380] * Disallow prefix routes with tail segments. [#379] * Enforce path separators on dynamic prefixes. [#378] diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml index 2a2ce1cc1..e32f0edd6 100644 --- a/actix-router/Cargo.toml +++ b/actix-router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-router" -version = "0.5.0-beta.1" +version = "0.5.0-beta.2" authors = [ "Nikolay Kim ", "Ali MJ Al-Nasrawy ", @@ -8,7 +8,7 @@ authors = [ ] description = "Resource path matching and router" keywords = ["actix", "router", "routing"] -repository = "https://github.com/actix/actix-net.git" +repository = "https://github.com/actix/actix-web.git" license = "MIT OR Apache-2.0" edition = "2018" diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index dc76ba3fd..58e05c4b6 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.1.0-beta.4 - 2021-09-09 * Minimum supported Rust version (MSRV) is now 1.51. diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index b732cf744..41d32257c 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-test" -version = "0.1.0-beta.3" +version = "0.1.0-beta.4" authors = [ "Nikolay Kim ", "Rob Ede ", @@ -20,13 +20,13 @@ openssl = ["tls-openssl", "actix-http/openssl"] [dependencies] actix-codec = "0.4.0" -actix-http = "3.0.0-beta.8" -actix-http-test = { version = "3.0.0-beta.4", features = [] } +actix-http = "3.0.0-beta.10" +actix-http-test = "3.0.0-beta.5" actix-service = "2.0.0" actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.8", default-features = false, features = ["cookies"] } +actix-web = { version = "4.0.0-beta.9", default-features = false, features = ["cookies"] } actix-rt = "2.1" -awc = { version = "3.0.0-beta.7", default-features = false, features = ["cookies"] } +awc = { version = "3.0.0-beta.8", default-features = false, features = ["cookies"] } futures-core = { version = "0.3.7", default-features = false, features = ["std"] } futures-util = { version = "0.3.7", default-features = false, features = [] } diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index 084e7b272..2e453063f 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 4.0.0-beta.7 - 2021-09-09 * Minimum supported Rust version (MSRV) is now 1.51. diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index fcb5195b8..ef6bd919d 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-actors" -version = "4.0.0-beta.6" +version = "4.0.0-beta.7" authors = ["Nikolay Kim "] description = "Actix actors support for Actix Web" keywords = ["actix", "http", "web", "framework", "async"] @@ -16,8 +16,8 @@ path = "src/lib.rs" [dependencies] actix = { version = "0.12.0", default-features = false } actix-codec = "0.4.0" -actix-http = "3.0.0-beta.8" -actix-web = { version = "4.0.0-beta.8", default-features = false } +actix-http = "3.0.0-beta.10" +actix-web = { version = "4.0.0-beta.9", default-features = false } bytes = "1" bytestring = "1" @@ -29,6 +29,6 @@ tokio = { version = "1", features = ["sync"] } actix-rt = "2.2" actix-test = "0.1.0-beta.3" -awc = { version = "3.0.0-beta.7", default-features = false } +awc = { version = "3.0.0-beta.8", default-features = false } env_logger = "0.8" futures-util = { version = "0.3.7", default-features = false } diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md index 2858d3f20..a647e4bc9 100644 --- a/actix-web-actors/README.md +++ b/actix-web-actors/README.md @@ -3,11 +3,11 @@ > Actix actors support for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) -[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.6)](https://docs.rs/actix-web-actors/4.0.0-beta.6) +[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.7)](https://docs.rs/actix-web-actors/4.0.0-beta.7) [![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
-[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.6/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.6) +[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.7/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.7) [![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index f0a56b30f..c154d8af4 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.5.0-beta.4 - 2021-09-09 * In routing macros, paths are now validated at compile time. [#2350] * Minimum supported Rust version (MSRV) is now 1.51. diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 66f7acf6d..2ad714f40 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-codegen" -version = "0.5.0-beta.3" +version = "0.5.0-beta.4" description = "Routing and runtime macros for Actix Web" readme = "README.md" homepage = "https://actix.rs" @@ -17,13 +17,13 @@ proc-macro = true quote = "1" syn = { version = "1", features = ["full", "parsing"] } proc-macro2 = "1" -actix-router = "0.5.0-beta.1" +actix-router = "0.5.0-beta.2" [dev-dependencies] actix-rt = "2.2" actix-test = "0.1.0-beta.3" actix-utils = "3.0.0" -actix-web = "4.0.0-beta.8" +actix-web = "4.0.0-beta.9" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } trybuild = "1" diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index e69cfbbe5..268e8b01d 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -3,11 +3,11 @@ > Routing and runtime macros for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) -[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.3)](https://docs.rs/actix-web-codegen/0.5.0-beta.3) +[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.4)](https://docs.rs/actix-web-codegen/0.5.0-beta.4) [![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
-[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.3/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.3) +[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.4/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.4) [![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 9c6f258aa..252b62efa 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,11 +1,15 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.8 - 2021-09-09 ### Changed * Send headers within the redirect requests. [#2310] [#2310]: https://github.com/actix/actix-web/pull/2310 + ## 3.0.0-beta.7 - 2021-06-26 ### Changed * Change compression algorithm features flags. [#2250] diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 016d3b48b..262c3dce5 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "3.0.0-beta.7" +version = "3.0.0-beta.8" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", @@ -55,7 +55,7 @@ __compress = [] [dependencies] actix-codec = "0.4.0" actix-service = "2.0.0" -actix-http = "3.0.0-beta.8" +actix-http = "3.0.0-beta.10" actix-rt = { version = "2.1", default-features = false } base64 = "0.13" @@ -77,9 +77,9 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true } tls-rustls = { version = "0.19.0", package = "rustls", optional = true, features = ["dangerous_configuration"] } [dev-dependencies] -actix-web = { version = "4.0.0-beta.8", features = ["openssl"] } -actix-http = { version = "3.0.0-beta.8", features = ["openssl"] } -actix-http-test = { version = "3.0.0-beta.4", features = ["openssl"] } +actix-web = { version = "4.0.0-beta.9", features = ["openssl"] } +actix-http = { version = "3.0.0-beta.10", features = ["openssl"] } +actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] } actix-utils = "3.0.0" actix-server = "2.0.0-beta.3" actix-tls = { version = "3.0.0-beta.5", features = ["openssl", "rustls"] } diff --git a/awc/README.md b/awc/README.md index fe91383ca..868bc5cae 100644 --- a/awc/README.md +++ b/awc/README.md @@ -3,9 +3,9 @@ > Async HTTP and WebSocket client library. [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) -[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.7)](https://docs.rs/awc/3.0.0-beta.7) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.8)](https://docs.rs/awc/3.0.0-beta.8) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.7/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.7) +[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.8/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.8) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources From 46699e34299ed14401df8ea8022efe47a83041e0 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sat, 11 Sep 2021 00:01:01 +0100 Subject: [PATCH 50/88] remove time dep from actix-http (#2383) --- Cargo.toml | 2 +- actix-http-test/Cargo.toml | 1 - actix-http-test/src/lib.rs | 11 ++- actix-http/Cargo.toml | 7 +- actix-http/src/config.rs | 29 +++---- actix-http/src/header/shared/http_date.rs | 82 +++++++++++++++++++ actix-http/src/header/shared/httpdate.rs | 97 ----------------------- actix-http/src/header/shared/mod.rs | 4 +- actix-http/src/lib.rs | 1 - actix-http/src/time_parser.rs | 72 ----------------- actix-http/tests/test_server.rs | 1 + src/middleware/logger.rs | 6 +- 12 files changed, 109 insertions(+), 204 deletions(-) create mode 100644 actix-http/src/header/shared/http_date.rs delete mode 100644 actix-http/src/header/shared/httpdate.rs delete mode 100644 actix-http/src/time_parser.rs diff --git a/Cargo.toml b/Cargo.toml index 60525f3ac..73a52182c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,7 +101,7 @@ serde_json = "1.0" serde_urlencoded = "0.7" smallvec = "1.6.1" socket2 = "0.4.0" -time = { version = "0.2.23", default-features = false, features = ["std"] } +time = { version = "0.3", default-features = false, features = ["formatting"] } url = "2.1" [dev-dependencies] diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index e7fe7adc0..ee4971a1e 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -47,7 +47,6 @@ serde = "1.0" serde_json = "1.0" slab = "0.4" serde_urlencoded = "0.7" -time = { version = "0.2.23", default-features = false, features = ["std"] } tls-openssl = { version = "0.10.9", package = "openssl", optional = true } [dev-dependencies] diff --git a/actix-http-test/src/lib.rs b/actix-http-test/src/lib.rs index 0f126c99a..ec7b46ffb 100644 --- a/actix-http-test/src/lib.rs +++ b/actix-http-test/src/lib.rs @@ -7,8 +7,7 @@ #[cfg(feature = "openssl")] extern crate tls_openssl as openssl; -use std::sync::mpsc; -use std::{net, thread, time}; +use std::{net, sync::mpsc, thread, time::Duration}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_rt::{net::TcpStream, System}; @@ -95,15 +94,15 @@ pub async fn test_server_with_addr>( .set_alpn_protos(b"\x02h2\x08http/1.1") .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); Connector::new() - .conn_lifetime(time::Duration::from_secs(0)) - .timeout(time::Duration::from_millis(30000)) + .conn_lifetime(Duration::from_secs(0)) + .timeout(Duration::from_millis(30000)) .ssl(builder.build()) } #[cfg(not(feature = "openssl"))] { Connector::new() - .conn_lifetime(time::Duration::from_secs(0)) - .timeout(time::Duration::from_millis(30000)) + .conn_lifetime(Duration::from_secs(0)) + .timeout(Duration::from_millis(30000)) } }; diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 0e0da8f43..889c91331 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -60,6 +60,7 @@ futures-util = { version = "0.3.7", default-features = false, features = ["alloc h2 = "0.3.1" http = "0.2.2" httparse = "1.5.1" +httpdate = "1.0.1" itoa = "0.4" language-tags = "0.3" local-channel = "0.1" @@ -70,11 +71,8 @@ percent-encoding = "2.1" pin-project = "1.0.0" pin-project-lite = "0.2" rand = "0.8" -regex = "1.3" -serde = "1.0" sha-1 = "0.9" smallvec = "1.6.1" -time = { version = "0.2.23", default-features = false, features = ["std"] } tokio = { version = "1.2", features = ["sync"] } # compression @@ -92,11 +90,12 @@ async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } env_logger = "0.8" rcgen = "0.8" +regex = "1.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tls-openssl = { version = "0.10", package = "openssl" } tls-rustls = { version = "0.19", package = "rustls" } -webpki = { version = "0.21.0" } +webpki = { version = "0.21" } [[example]] name = "ws" diff --git a/actix-http/src/config.rs b/actix-http/src/config.rs index 97750ff76..069099b8c 100644 --- a/actix-http/src/config.rs +++ b/actix-http/src/config.rs @@ -1,18 +1,19 @@ -use std::cell::Cell; -use std::fmt::Write; -use std::rc::Rc; -use std::time::Duration; -use std::{fmt, net}; +use std::{ + cell::Cell, + fmt::{self, Write}, + net, + rc::Rc, + time::{Duration, SystemTime}, +}; use actix_rt::{ task::JoinHandle, time::{interval, sleep_until, Instant, Sleep}, }; use bytes::BytesMut; -use time::OffsetDateTime; /// "Sun, 06 Nov 1994 08:49:37 GMT".len() -const DATE_VALUE_LENGTH: usize = 29; +pub(crate) const DATE_VALUE_LENGTH: usize = 29; #[derive(Debug, PartialEq, Clone, Copy)] /// Server keep-alive setting @@ -206,12 +207,7 @@ impl Date { fn update(&mut self) { self.pos = 0; - write!( - self, - "{}", - OffsetDateTime::now_utc().format("%a, %d %b %Y %H:%M:%S GMT") - ) - .unwrap(); + write!(self, "{}", httpdate::fmt_http_date(SystemTime::now())).unwrap(); } } @@ -269,11 +265,11 @@ impl DateService { } // TODO: move to a util module for testing all spawn handle drop style tasks. -#[cfg(test)] /// Test Module for checking the drop state of certain async tasks that are spawned /// with `actix_rt::spawn` /// /// The target task must explicitly generate `NotifyOnDrop` when spawn the task +#[cfg(test)] mod notify_on_drop { use std::cell::RefCell; @@ -283,9 +279,8 @@ mod notify_on_drop { /// Check if the spawned task is dropped. /// - /// # Panic: - /// - /// When there was no `NotifyOnDrop` instance on current thread + /// # Panics + /// Panics when there was no `NotifyOnDrop` instance on current thread. pub(crate) fn is_dropped() -> bool { NOTIFY_DROPPED.with(|bool| { bool.borrow() diff --git a/actix-http/src/header/shared/http_date.rs b/actix-http/src/header/shared/http_date.rs new file mode 100644 index 000000000..3441f90af --- /dev/null +++ b/actix-http/src/header/shared/http_date.rs @@ -0,0 +1,82 @@ +use std::{fmt, io::Write, str::FromStr, time::SystemTime}; + +use bytes::BytesMut; +use http::header::{HeaderValue, InvalidHeaderValue}; + +use crate::{ + config::DATE_VALUE_LENGTH, error::ParseError, header::IntoHeaderValue, + helpers::MutWriter, +}; + +/// A timestamp with HTTP formatting and parsing. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct HttpDate(SystemTime); + +impl FromStr for HttpDate { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + match httpdate::parse_http_date(s) { + Ok(sys_time) => Ok(HttpDate(sys_time)), + Err(_) => Err(ParseError::Header), + } + } +} + +impl fmt::Display for HttpDate { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let date_str = httpdate::fmt_http_date(self.0); + f.write_str(&date_str) + } +} + +impl IntoHeaderValue for HttpDate { + type Error = InvalidHeaderValue; + + fn try_into_value(self) -> Result { + let mut buf = BytesMut::with_capacity(DATE_VALUE_LENGTH); + let mut wrt = MutWriter(&mut buf); + + // unwrap: date output is known to be well formed and of known length + write!(wrt, "{}", httpdate::fmt_http_date(self.0)).unwrap(); + + HeaderValue::from_maybe_shared(buf.split().freeze()) + } +} + +impl From for HttpDate { + fn from(sys_time: SystemTime) -> HttpDate { + HttpDate(sys_time) + } +} + +impl From for SystemTime { + fn from(HttpDate(sys_time): HttpDate) -> SystemTime { + sys_time + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use super::*; + + #[test] + fn date_header() { + macro_rules! assert_parsed_date { + ($case:expr, $exp:expr) => { + assert_eq!($case.parse::().unwrap(), $exp); + }; + } + + // 784198117 = SystemTime::from(datetime!(1994-11-07 08:48:37).assume_utc()).duration_since(SystemTime::UNIX_EPOCH)); + let nov_07 = HttpDate(SystemTime::UNIX_EPOCH + Duration::from_secs(784198117)); + + assert_parsed_date!("Mon, 07 Nov 1994 08:48:37 GMT", nov_07); + assert_parsed_date!("Monday, 07-Nov-94 08:48:37 GMT", nov_07); + assert_parsed_date!("Mon Nov 7 08:48:37 1994", nov_07); + + assert!("this-is-no-date".parse::().is_err()); + } +} diff --git a/actix-http/src/header/shared/httpdate.rs b/actix-http/src/header/shared/httpdate.rs deleted file mode 100644 index 18278a6d8..000000000 --- a/actix-http/src/header/shared/httpdate.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::{ - fmt, - io::Write, - str::FromStr, - time::{SystemTime, UNIX_EPOCH}, -}; - -use bytes::buf::BufMut; -use bytes::BytesMut; -use http::header::{HeaderValue, InvalidHeaderValue}; -use time::{OffsetDateTime, PrimitiveDateTime, UtcOffset}; - -use crate::error::ParseError; -use crate::header::IntoHeaderValue; -use crate::time_parser; - -/// A timestamp with HTTP formatting and parsing. -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct HttpDate(OffsetDateTime); - -impl FromStr for HttpDate { - type Err = ParseError; - - fn from_str(s: &str) -> Result { - match time_parser::parse_http_date(s) { - Some(t) => Ok(HttpDate(t.assume_utc())), - None => Err(ParseError::Header), - } - } -} - -impl fmt::Display for HttpDate { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(&self.0.format("%a, %d %b %Y %H:%M:%S GMT"), f) - } -} - -impl From for HttpDate { - fn from(sys: SystemTime) -> HttpDate { - HttpDate(PrimitiveDateTime::from(sys).assume_utc()) - } -} - -impl IntoHeaderValue for HttpDate { - type Error = InvalidHeaderValue; - - fn try_into_value(self) -> Result { - let mut wrt = BytesMut::with_capacity(29).writer(); - write!( - wrt, - "{}", - self.0 - .to_offset(UtcOffset::UTC) - .format("%a, %d %b %Y %H:%M:%S GMT") - ) - .unwrap(); - HeaderValue::from_maybe_shared(wrt.get_mut().split().freeze()) - } -} - -impl From for SystemTime { - fn from(date: HttpDate) -> SystemTime { - let dt = date.0; - let epoch = OffsetDateTime::unix_epoch(); - - UNIX_EPOCH + (dt - epoch) - } -} - -#[cfg(test)] -mod tests { - use super::HttpDate; - use time::{date, time, PrimitiveDateTime}; - - #[test] - fn test_date() { - let nov_07 = HttpDate( - PrimitiveDateTime::new(date!(1994 - 11 - 07), time!(8:48:37)).assume_utc(), - ); - - assert_eq!( - "Sun, 07 Nov 1994 08:48:37 GMT".parse::().unwrap(), - nov_07 - ); - assert_eq!( - "Sunday, 07-Nov-94 08:48:37 GMT" - .parse::() - .unwrap(), - nov_07 - ); - assert_eq!( - "Sun Nov 7 08:48:37 1994".parse::().unwrap(), - nov_07 - ); - assert!("this-is-no-date".parse::().is_err()); - } -} diff --git a/actix-http/src/header/shared/mod.rs b/actix-http/src/header/shared/mod.rs index b8f9173f9..274e13146 100644 --- a/actix-http/src/header/shared/mod.rs +++ b/actix-http/src/header/shared/mod.rs @@ -3,12 +3,12 @@ mod charset; mod content_encoding; mod extended; -mod httpdate; +mod http_date; mod quality_item; pub use self::charset::Charset; pub use self::content_encoding::ContentEncoding; pub use self::extended::{parse_extended_value, ExtendedValue}; -pub use self::httpdate::HttpDate; +pub use self::http_date::HttpDate; pub use self::quality_item::{q, qitem, Quality, QualityItem}; pub use language_tags::LanguageTag; diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index 17ee3ff29..3ad8d095e 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -44,7 +44,6 @@ mod request; mod response; mod response_builder; mod service; -mod time_parser; pub mod error; pub mod h1; diff --git a/actix-http/src/time_parser.rs b/actix-http/src/time_parser.rs deleted file mode 100644 index fd82fd42e..000000000 --- a/actix-http/src/time_parser.rs +++ /dev/null @@ -1,72 +0,0 @@ -use time::{Date, OffsetDateTime, PrimitiveDateTime}; - -/// Attempt to parse a `time` string as one of either RFC 1123, RFC 850, or asctime. -pub(crate) fn parse_http_date(time: &str) -> Option { - try_parse_rfc_1123(time) - .or_else(|| try_parse_rfc_850(time)) - .or_else(|| try_parse_asctime(time)) -} - -/// Attempt to parse a `time` string as a RFC 1123 formatted date time string. -/// -/// Eg: `Fri, 12 Feb 2021 00:14:29 GMT` -fn try_parse_rfc_1123(time: &str) -> Option { - time::parse(time, "%a, %d %b %Y %H:%M:%S").ok() -} - -/// Attempt to parse a `time` string as a RFC 850 formatted date time string. -/// -/// Eg: `Wednesday, 11-Jan-21 13:37:41 UTC` -fn try_parse_rfc_850(time: &str) -> Option { - let dt = PrimitiveDateTime::parse(time, "%A, %d-%b-%y %H:%M:%S").ok()?; - - // If the `time` string contains a two-digit year, then as per RFC 2616 § 19.3, - // we consider the year as part of this century if it's within the next 50 years, - // otherwise we consider as part of the previous century. - - let now = OffsetDateTime::now_utc(); - let century_start_year = (now.year() / 100) * 100; - let mut expanded_year = century_start_year + dt.year(); - - if expanded_year > now.year() + 50 { - expanded_year -= 100; - } - - let date = Date::try_from_ymd(expanded_year, dt.month(), dt.day()).ok()?; - Some(PrimitiveDateTime::new(date, dt.time())) -} - -/// Attempt to parse a `time` string using ANSI C's `asctime` format. -/// -/// Eg: `Wed Feb 13 15:46:11 2013` -fn try_parse_asctime(time: &str) -> Option { - time::parse(time, "%a %b %_d %H:%M:%S %Y").ok() -} - -#[cfg(test)] -mod tests { - use time::{date, time}; - - use super::*; - - #[test] - fn test_rfc_850_year_shift() { - let date = try_parse_rfc_850("Friday, 19-Nov-82 16:14:55 EST").unwrap(); - assert_eq!(date, date!(1982 - 11 - 19).with_time(time!(16:14:55))); - - let date = try_parse_rfc_850("Wednesday, 11-Jan-62 13:37:41 EST").unwrap(); - assert_eq!(date, date!(2062 - 01 - 11).with_time(time!(13:37:41))); - - let date = try_parse_rfc_850("Wednesday, 11-Jan-21 13:37:41 EST").unwrap(); - assert_eq!(date, date!(2021 - 01 - 11).with_time(time!(13:37:41))); - - let date = try_parse_rfc_850("Wednesday, 11-Jan-23 13:37:41 EST").unwrap(); - assert_eq!(date, date!(2023 - 01 - 11).with_time(time!(13:37:41))); - - let date = try_parse_rfc_850("Wednesday, 11-Jan-99 13:37:41 EST").unwrap(); - assert_eq!(date, date!(1999 - 01 - 11).with_time(time!(13:37:41))); - - let date = try_parse_rfc_850("Wednesday, 11-Jan-00 13:37:41 EST").unwrap(); - assert_eq!(date, date!(2000 - 01 - 11).with_time(time!(13:37:41))); - } -} diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index 1e6d0b637..c04aeae00 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -183,6 +183,7 @@ async fn test_chunked_payload() { Some(caps) => caps.get(1).unwrap().as_str().parse().unwrap(), None => panic!("Failed to find size in HTTP Response: {}", data), }; + size }; diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index 9574b02f7..961eca496 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -18,7 +18,7 @@ use bytes::Bytes; use futures_core::ready; use log::{debug, warn}; use regex::{Regex, RegexSet}; -use time::OffsetDateTime; +use time::{format_description::well_known::Rfc3339, OffsetDateTime}; use crate::{ dev::{BodySize, MessageBody}, @@ -538,7 +538,7 @@ impl FormatText { }; } FormatText::UrlPath => *self = FormatText::Str(req.path().to_string()), - FormatText::RequestTime => *self = FormatText::Str(now.format("%Y-%m-%dT%H:%M:%S")), + FormatText::RequestTime => *self = FormatText::Str(now.format(&Rfc3339).unwrap()), FormatText::RequestHeader(ref name) => { let s = if let Some(val) = req.headers().get(name) { if let Ok(s) = val.to_str() { @@ -767,7 +767,7 @@ mod tests { Ok(()) }; let s = format!("{}", FormatDisplay(&render)); - assert!(s.contains(&now.format("%Y-%m-%dT%H:%M:%S"))); + assert!(s.contains(&now.format(&Rfc3339).unwrap())); } #[actix_rt::test] From 8ae278cb68eda1e6c4fbd3463b018e0f0fe1c313 Mon Sep 17 00:00:00 2001 From: Arniu Tseng Date: Sat, 11 Sep 2021 08:11:16 +0800 Subject: [PATCH 51/88] Remove `FromRequest::Config` (#2233) Co-authored-by: Jonas Platte Co-authored-by: Igor Aleksanov Co-authored-by: Rob Ede --- CHANGES.md | 3 ++ Cargo.toml | 1 + MIGRATION.md | 2 ++ actix-files/src/path_buf.rs | 1 - actix-multipart/src/extractor.rs | 1 - src/data.rs | 1 - src/extract.rs | 55 ++++++++++++++++++++------------ src/info.rs | 2 -- src/request.rs | 1 - src/request_data.rs | 1 - src/types/either.rs | 1 - src/types/form.rs | 30 +++++++++-------- src/types/header.rs | 1 - src/types/json.rs | 1 - src/types/path.rs | 3 +- src/types/payload.rs | 17 ++++------ src/types/query.rs | 3 +- 17 files changed, 66 insertions(+), 58 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 398ac477a..d8831602d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,10 @@ # Changes ## Unreleased - 2021-xx-xx +### Changed +* Asscociated type `FromRequest::Config` was removed. [#2233] +[#2233]: https://github.com/actix/actix-web/pull/2233 ## 4.0.0-beta.9 - 2021-09-09 ### Added diff --git a/Cargo.toml b/Cargo.toml index 73a52182c..dc7e9af3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ edition = "2018" [package.metadata.docs.rs] # features that docs.rs will build with features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "secure-cookies"] +rustdoc-args = ["--cfg", "docsrs"] [lib] name = "actix_web" diff --git a/MIGRATION.md b/MIGRATION.md index 9a70adb95..d53bd7bf8 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -11,6 +11,8 @@ Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`. +* The `type Config` of `FromRequest` was removed. + * Feature flag `compress` has been split into its supported algorithm (brotli, gzip, zstd). By default all compression algorithms are enabled. To select algorithm you want to include with `middleware::Compress` use following flags: diff --git a/actix-files/src/path_buf.rs b/actix-files/src/path_buf.rs index 8a87acd5d..76f589307 100644 --- a/actix-files/src/path_buf.rs +++ b/actix-files/src/path_buf.rs @@ -59,7 +59,6 @@ impl AsRef for PathBufWrap { impl FromRequest for PathBufWrap { type Error = UriSegmentError; type Future = Ready>; - type Config = (); fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { ready(req.match_info().path().parse()) diff --git a/actix-multipart/src/extractor.rs b/actix-multipart/src/extractor.rs index c87f8cc2d..1ad1f203d 100644 --- a/actix-multipart/src/extractor.rs +++ b/actix-multipart/src/extractor.rs @@ -33,7 +33,6 @@ use crate::server::Multipart; impl FromRequest for Multipart { type Error = Error; type Future = Ready>; - type Config = (); #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { diff --git a/src/data.rs b/src/data.rs index 174faba37..9d4fe0840 100644 --- a/src/data.rs +++ b/src/data.rs @@ -120,7 +120,6 @@ where } impl FromRequest for Data { - type Config = (); type Error = Error; type Future = Ready>; diff --git a/src/extract.rs b/src/extract.rs index 592f7ab83..39062dd1c 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -13,13 +13,42 @@ use futures_core::ready; use crate::{dev::Payload, Error, HttpRequest}; -/// Trait implemented by types that can be extracted from request. +/// A type that implements [`FromRequest`] is called an **extractor** and can extract data +/// from the request. Examples of types that implement this trait are [`Json`], [`Form`], [`Path`]. /// -/// Types that implement this trait can be used with `Route` handlers. +/// An extractor can be customized by injecting the corresponding configuration with one of: +/// +/// - [`App::app_data()`](`crate::App::app_data`) +/// - [`Scope::app_data()`](`crate::Scope::app_data`) +/// - [`Resource::app_data()`](`crate::Resource::app_data`) +/// +/// Here are some built-in extractors and their corresponding configuration. +/// Please refer to the respective documentation for details. +/// +/// | Extractor | Configuration | +/// |-------------|-------------------| +/// | [`Json`] | [`JsonConfig`] | +/// | [`Form`] | [`FormConfig`] | +/// | [`Path`] | [`PathConfig`] | +/// | [`Query`] | [`QueryConfig`] | +/// | [`Payload`] | [`PayloadConfig`] | +/// | [`String`] | [`PayloadConfig`] | +/// | [`Bytes`] | [`PayloadConfig`] | +/// +/// [`Json`]: crate::web::Json +/// [`JsonConfig`]: crate::web::JsonConfig +/// [`Form`]: crate::web::Form +/// [`FormConfig`]: crate::web::FormConfig +/// [`Path`]: crate::web::Path +/// [`PathConfig`]: crate::web::PathConfig +/// [`Query`]: crate::web::Query +/// [`QueryConfig`]: crate::web::QueryConfig +/// [`Payload`]: crate::web::Payload +/// [`PayloadConfig`]: crate::web::PayloadConfig +/// [`String`]: FromRequest#impl-FromRequest-for-String +/// [`Bytes`]: crate::web::Bytes#impl-FromRequest +#[cfg_attr(docsrs, doc(alias = "Extractor"))] pub trait FromRequest: Sized { - /// Configuration for this extractor. - type Config: Default + 'static; - /// The associated error which can be returned. type Error: Into; @@ -35,14 +64,6 @@ pub trait FromRequest: Sized { fn extract(req: &HttpRequest) -> Self::Future { Self::from_request(req, &mut Payload::None) } - - /// Create and configure config instance. - fn configure(f: F) -> Self::Config - where - F: FnOnce(Self::Config) -> Self::Config, - { - f(Self::Config::default()) - } } /// Optionally extract a field from the request @@ -65,7 +86,6 @@ pub trait FromRequest: Sized { /// impl FromRequest for Thing { /// type Error = Error; /// type Future = Ready>; -/// type Config = (); /// /// fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { /// if rand::random() { @@ -100,7 +120,6 @@ where { type Error = Error; type Future = FromRequestOptFuture; - type Config = T::Config; #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { @@ -156,7 +175,6 @@ where /// impl FromRequest for Thing { /// type Error = Error; /// type Future = Ready>; -/// type Config = (); /// /// fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { /// if rand::random() { @@ -189,7 +207,6 @@ where { type Error = Error; type Future = FromRequestResFuture; - type Config = T::Config; #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { @@ -233,7 +250,6 @@ where impl FromRequest for Uri { type Error = Infallible; type Future = Ready>; - type Config = (); fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { ok(req.uri().clone()) @@ -255,7 +271,6 @@ impl FromRequest for Uri { impl FromRequest for Method { type Error = Infallible; type Future = Ready>; - type Config = (); fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { ok(req.method().clone()) @@ -266,7 +281,6 @@ impl FromRequest for Method { impl FromRequest for () { type Error = Infallible; type Future = Ready>; - type Config = (); fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future { ok(()) @@ -306,7 +320,6 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { { type Error = Error; type Future = $fut_type<$($T),+>; - type Config = ($($T::Config),+); fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { $fut_type { diff --git a/src/info.rs b/src/info.rs index de8ad67ee..d928a1e63 100644 --- a/src/info.rs +++ b/src/info.rs @@ -209,7 +209,6 @@ impl ConnectionInfo { impl FromRequest for ConnectionInfo { type Error = Infallible; type Future = Ready>; - type Config = (); fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { ok(req.connection_info().clone()) @@ -252,7 +251,6 @@ impl ResponseError for MissingPeerAddr {} impl FromRequest for PeerAddr { type Error = MissingPeerAddr; type Future = Ready>; - type Config = (); fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { match req.peer_addr() { diff --git a/src/request.rs b/src/request.rs index c25a5397a..0027f9b4b 100644 --- a/src/request.rs +++ b/src/request.rs @@ -358,7 +358,6 @@ impl Drop for HttpRequest { /// } /// ``` impl FromRequest for HttpRequest { - type Config = (); type Error = Error; type Future = Ready>; diff --git a/src/request_data.rs b/src/request_data.rs index 581943015..575dc1eb3 100644 --- a/src/request_data.rs +++ b/src/request_data.rs @@ -64,7 +64,6 @@ impl Deref for ReqData { } impl FromRequest for ReqData { - type Config = (); type Error = Error; type Future = Ready>; diff --git a/src/types/either.rs b/src/types/either.rs index 35e63cec9..5700b63c7 100644 --- a/src/types/either.rs +++ b/src/types/either.rs @@ -187,7 +187,6 @@ where { type Error = EitherExtractError; type Future = EitherExtractFut; - type Config = (); fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { EitherExtractFut { diff --git a/src/types/form.rs b/src/types/form.rs index 2ace0e063..71100eb97 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -126,20 +126,12 @@ impl FromRequest for Form where T: DeserializeOwned + 'static, { - type Config = FormConfig; type Error = Error; type Future = FormExtractFut; #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { - let (limit, err_handler) = req - .app_data::() - .or_else(|| { - req.app_data::>() - .map(|d| d.as_ref()) - }) - .map(|c| (c.limit, c.err_handler.clone())) - .unwrap_or((16384, None)); + let FormConfig { limit, err_handler } = FormConfig::from_req(req).clone(); FormExtractFut { fut: UrlEncoded::new(req, payload).limit(limit), @@ -241,14 +233,26 @@ impl FormConfig { self.err_handler = Some(Rc::new(f)); self } + + /// Extract payload config from app data. + /// + /// Checks both `T` and `Data`, in that order, and falls back to the default payload config. + fn from_req(req: &HttpRequest) -> &Self { + req.app_data::() + .or_else(|| req.app_data::>().map(|d| d.as_ref())) + .unwrap_or(&DEFAULT_CONFIG) + } } +/// Allow shared refs used as default. +const DEFAULT_CONFIG: FormConfig = FormConfig { + limit: 16_384, // 2^14 bytes (~16kB) + err_handler: None, +}; + impl Default for FormConfig { fn default() -> Self { - FormConfig { - limit: 16_384, // 2^14 bytes (~16kB) - err_handler: None, - } + DEFAULT_CONFIG } } diff --git a/src/types/header.rs b/src/types/header.rs index 9b64f445d..6ea77faf6 100644 --- a/src/types/header.rs +++ b/src/types/header.rs @@ -62,7 +62,6 @@ where { type Error = ParseError; type Future = Ready>; - type Config = (); #[inline] fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { diff --git a/src/types/json.rs b/src/types/json.rs index 8c2f51a68..19443ea96 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -130,7 +130,6 @@ impl Responder for Json { impl FromRequest for Json { type Error = Error; type Future = JsonExtractFut; - type Config = JsonConfig; #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { diff --git a/src/types/path.rs b/src/types/path.rs index 4052646e3..aed897fa9 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -97,12 +97,11 @@ where { type Error = Error; type Future = Ready>; - type Config = PathConfig; #[inline] fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { let error_handler = req - .app_data::() + .app_data::() .and_then(|c| c.ehandler.clone()); ready( diff --git a/src/types/payload.rs b/src/types/payload.rs index 188da6201..46ad96beb 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -63,7 +63,6 @@ impl Stream for Payload { /// See [here](#usage) for example of usage as an extractor. impl FromRequest for Payload { - type Config = PayloadConfig; type Error = Error; type Future = Ready>; @@ -90,7 +89,6 @@ impl FromRequest for Payload { /// } /// ``` impl FromRequest for Bytes { - type Config = PayloadConfig; type Error = Error; type Future = Either>>; @@ -126,8 +124,7 @@ impl<'a> Future for BytesExtractFut { /// /// Text extractor automatically decode body according to the request's charset. /// -/// [**PayloadConfig**](PayloadConfig) allows to configure -/// extraction process. +/// Use [`PayloadConfig`] to configure extraction process. /// /// # Examples /// ``` @@ -139,7 +136,6 @@ impl<'a> Future for BytesExtractFut { /// format!("Body {}!", text) /// } impl FromRequest for String { - type Config = PayloadConfig; type Error = Error; type Future = Either>>; @@ -198,14 +194,15 @@ fn bytes_to_string(body: Bytes, encoding: &'static Encoding) -> Result fmt::Display for Query { impl FromRequest for Query { type Error = Error; type Future = Ready>; - type Config = QueryConfig; #[inline] fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { let error_handler = req - .app_data::() + .app_data::() .and_then(|c| c.err_handler.clone()); serde_urlencoded::from_str::(req.query_string()) From 450ff5fa1dbbf9aa9adb68f711ed5e3b53445bab Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sat, 11 Sep 2021 16:48:47 +0100 Subject: [PATCH 52/88] improve extract docs (#2384) --- CHANGES.md | 5 ++++- src/extract.rs | 28 +++++++++++++++++++--------- src/types/form.rs | 2 +- src/types/json.rs | 2 +- src/types/path.rs | 2 +- src/types/payload.rs | 3 ++- 6 files changed, 28 insertions(+), 14 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d8831602d..e2bd6ec8b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,9 +2,12 @@ ## Unreleased - 2021-xx-xx ### Changed -* Asscociated type `FromRequest::Config` was removed. [#2233] +* Associated type `FromRequest::Config` was removed. [#2233] +* Inner field made private on `web::Payload`. [#????] [#2233]: https://github.com/actix/actix-web/pull/2233 +[#????]: https://github.com/actix/actix-web/pull/???? + ## 4.0.0-beta.9 - 2021-09-09 ### Added diff --git a/src/extract.rs b/src/extract.rs index 39062dd1c..29fd0d05e 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -13,28 +13,37 @@ use futures_core::ready; use crate::{dev::Payload, Error, HttpRequest}; -/// A type that implements [`FromRequest`] is called an **extractor** and can extract data -/// from the request. Examples of types that implement this trait are [`Json`], [`Form`], [`Path`]. +/// A type that implements [`FromRequest`] is called an **extractor** and can extract data from +/// the request. Some types that implement this trait are: [`Json`], [`Header`], and [`Path`]. /// +/// # Configuration /// An extractor can be customized by injecting the corresponding configuration with one of: /// -/// - [`App::app_data()`](`crate::App::app_data`) -/// - [`Scope::app_data()`](`crate::Scope::app_data`) -/// - [`Resource::app_data()`](`crate::Resource::app_data`) +/// - [`App::app_data()`][crate::App::app_data] +/// - [`Scope::app_data()`][crate::Scope::app_data] +/// - [`Resource::app_data()`][crate::Resource::app_data] /// /// Here are some built-in extractors and their corresponding configuration. /// Please refer to the respective documentation for details. /// /// | Extractor | Configuration | /// |-------------|-------------------| +/// | [`Header`] | _None_ | +/// | [`Path`] | [`PathConfig`] | /// | [`Json`] | [`JsonConfig`] | /// | [`Form`] | [`FormConfig`] | -/// | [`Path`] | [`PathConfig`] | /// | [`Query`] | [`QueryConfig`] | -/// | [`Payload`] | [`PayloadConfig`] | -/// | [`String`] | [`PayloadConfig`] | /// | [`Bytes`] | [`PayloadConfig`] | +/// | [`String`] | [`PayloadConfig`] | +/// | [`Payload`] | [`PayloadConfig`] | /// +/// # Implementing An Extractor +/// To reduce duplicate code in handlers where extracting certain parts of a request has a common +/// structure, you can implement `FromRequest` for your own types. +/// +/// Note that the request payload can only be consumed by one extractor. +/// +/// [`Header`]: crate::web::Header /// [`Json`]: crate::web::Json /// [`JsonConfig`]: crate::web::JsonConfig /// [`Form`]: crate::web::Form @@ -47,7 +56,8 @@ use crate::{dev::Payload, Error, HttpRequest}; /// [`PayloadConfig`]: crate::web::PayloadConfig /// [`String`]: FromRequest#impl-FromRequest-for-String /// [`Bytes`]: crate::web::Bytes#impl-FromRequest -#[cfg_attr(docsrs, doc(alias = "Extractor"))] +/// [`Either`]: crate::web::Either +#[doc(alias = "extract", alias = "extractor")] pub trait FromRequest: Sized { /// The associated error which can be returned. type Error: Into; diff --git a/src/types/form.rs b/src/types/form.rs index 71100eb97..098a864de 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -32,7 +32,7 @@ use crate::{ /// To extract typed data from a request body, the inner type `T` must implement the /// [`DeserializeOwned`] trait. /// -/// Use [`FormConfig`] to configure extraction process. +/// Use [`FormConfig`] to configure extraction options. /// /// ``` /// use actix_web::{post, web}; diff --git a/src/types/json.rs b/src/types/json.rs index 19443ea96..df01fdb34 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -34,7 +34,7 @@ use crate::{ /// To extract typed data from a request body, the inner type `T` must implement the /// [`serde::Deserialize`] trait. /// -/// Use [`JsonConfig`] to configure extraction process. +/// Use [`JsonConfig`] to configure extraction options. /// /// ``` /// use actix_web::{post, web, App}; diff --git a/src/types/path.rs b/src/types/path.rs index aed897fa9..b58aec18d 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -14,7 +14,7 @@ use crate::{ /// Extract typed data from request path segments. /// -/// Use [`PathConfig`] to configure extraction process. +/// Use [`PathConfig`] to configure extraction option. /// /// # Examples /// ``` diff --git a/src/types/payload.rs b/src/types/payload.rs index 46ad96beb..00047e8b1 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -43,10 +43,11 @@ use crate::{ /// Ok(format!("Request Body Bytes:\n{:?}", bytes)) /// } /// ``` -pub struct Payload(pub crate::dev::Payload); +pub struct Payload(crate::dev::Payload); impl Payload { /// Unwrap to inner Payload type. + #[inline] pub fn into_inner(self) -> crate::dev::Payload { self.0 } From efefa0d0ce79e24acc79fb0333369e7ab6d79c41 Mon Sep 17 00:00:00 2001 From: Jake Date: Sat, 11 Sep 2021 09:27:50 -0700 Subject: [PATCH 53/88] web: add option to not require content type header for Json (#2362) Co-authored-by: Rob Ede --- CHANGES.md | 8 ++++-- src/types/json.rs | 68 ++++++++++++++++++++++++++++++++++------------- 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e2bd6ec8b..439dd0540 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,12 +1,16 @@ # Changes ## Unreleased - 2021-xx-xx +### Added +* Option to allow `Json` extractor to work without a `Content-Length` header present. [#2362] + ### Changed * Associated type `FromRequest::Config` was removed. [#2233] -* Inner field made private on `web::Payload`. [#????] +* Inner field made private on `web::Payload`. [#2384] [#2233]: https://github.com/actix/actix-web/pull/2233 -[#????]: https://github.com/actix/actix-web/pull/???? +[#2362]: https://github.com/actix/actix-web/pull/2362 +[#2384]: https://github.com/actix/actix-web/pull/2384 ## 4.0.0-beta.9 - 2021-09-09 diff --git a/src/types/json.rs b/src/types/json.rs index df01fdb34..6d07fe45a 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -127,7 +127,7 @@ impl Responder for Json { } /// See [here](#extractor) for example of usage as an extractor. -impl FromRequest for Json { +impl FromRequest for Json { type Error = Error; type Future = JsonExtractFut; @@ -136,12 +136,13 @@ impl FromRequest for Json { let config = JsonConfig::from_req(req); let limit = config.limit; - let ctype = config.content_type.as_deref(); + let ctype_required = config.content_type_required; + let ctype_fn = config.content_type.as_deref(); let err_handler = config.err_handler.clone(); JsonExtractFut { req: Some(req.clone()), - fut: JsonBody::new(req, payload, ctype).limit(limit), + fut: JsonBody::new(req, payload, ctype_fn, ctype_required).limit(limit), err_handler, } } @@ -156,7 +157,7 @@ pub struct JsonExtractFut { err_handler: JsonErrorHandler, } -impl Future for JsonExtractFut { +impl Future for JsonExtractFut { type Output = Result, Error>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { @@ -224,6 +225,7 @@ pub struct JsonConfig { limit: usize, err_handler: JsonErrorHandler, content_type: Option bool + Send + Sync>>, + content_type_required: bool, } impl JsonConfig { @@ -251,6 +253,12 @@ impl JsonConfig { self } + /// Sets whether or not the request must have a `Content-Type` header to be parsed. + pub fn content_type_required(mut self, content_type_required: bool) -> Self { + self.content_type_required = content_type_required; + self + } + /// Extract payload config from app data. Check both `T` and `Data`, in that order, and fall /// back to the default payload config. fn from_req(req: &HttpRequest) -> &Self { @@ -267,6 +275,7 @@ const DEFAULT_CONFIG: JsonConfig = JsonConfig { limit: DEFAULT_LIMIT, err_handler: None, content_type: None, + content_type_required: true, }; impl Default for JsonConfig { @@ -277,15 +286,18 @@ impl Default for JsonConfig { /// Future that resolves to some `T` when parsed from a JSON payload. /// -/// Form can be deserialized from any type `T` that implements [`serde::Deserialize`]. +/// Can deserialize any type `T` that implements [`Deserialize`][serde::Deserialize]. /// /// Returns error if: -/// - content type is not `application/json` -/// - content length is greater than [limit](JsonBody::limit()) +/// - `Content-Type` is not `application/json` when `ctype_required` (passed to [`new`][Self::new]) +/// is `true`. +/// - `Content-Length` is greater than [limit](JsonBody::limit()). +/// - The payload, when consumed, is not valid JSON. pub enum JsonBody { Error(Option), Body { limit: usize, + /// Length as reported by `Content-Length` header, if present. length: Option, #[cfg(feature = "__compress")] payload: Decompress, @@ -304,18 +316,21 @@ impl JsonBody { pub fn new( req: &HttpRequest, payload: &mut Payload, - ctype: Option<&(dyn Fn(mime::Mime) -> bool + Send + Sync)>, + ctype_fn: Option<&(dyn Fn(mime::Mime) -> bool + Send + Sync)>, + ctype_required: bool, ) -> Self { // check content-type - let json = if let Ok(Some(mime)) = req.mime_type() { + let can_parse_json = if let Ok(Some(mime)) = req.mime_type() { mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) - || ctype.map_or(false, |predicate| predicate(mime)) + || ctype_fn.map_or(false, |predicate| predicate(mime)) } else { - false + // if `ctype_required` is false, assume payload is + // json even when content-type header is missing + !ctype_required }; - if !json { + if !can_parse_json { return JsonBody::Error(Some(JsonPayloadError::ContentType)); } @@ -325,7 +340,7 @@ impl JsonBody { .and_then(|l| l.to_str().ok()) .and_then(|s| s.parse::().ok()); - // Notice the content_length is not checked against limit of json config here. + // Notice the content-length is not checked against limit of json config here. // As the internal usage always call JsonBody::limit after JsonBody::new. // And limit check to return an error variant of JsonBody happens there. @@ -379,7 +394,7 @@ impl JsonBody { } } -impl Future for JsonBody { +impl Future for JsonBody { type Output = Result; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { @@ -562,7 +577,7 @@ mod tests { #[actix_rt::test] async fn test_json_body() { let (req, mut pl) = TestRequest::default().to_http_parts(); - let json = JsonBody::::new(&req, &mut pl, None).await; + let json = JsonBody::::new(&req, &mut pl, None, true).await; assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); let (req, mut pl) = TestRequest::default() @@ -571,7 +586,7 @@ mod tests { header::HeaderValue::from_static("application/text"), )) .to_http_parts(); - let json = JsonBody::::new(&req, &mut pl, None).await; + let json = JsonBody::::new(&req, &mut pl, None, true).await; assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); let (req, mut pl) = TestRequest::default() @@ -585,7 +600,7 @@ mod tests { )) .to_http_parts(); - let json = JsonBody::::new(&req, &mut pl, None) + let json = JsonBody::::new(&req, &mut pl, None, true) .limit(100) .await; assert!(json_eq( @@ -604,7 +619,7 @@ mod tests { .set_payload(Bytes::from_static(&[0u8; 1000])) .to_http_parts(); - let json = JsonBody::::new(&req, &mut pl, None) + let json = JsonBody::::new(&req, &mut pl, None, true) .limit(100) .await; @@ -625,7 +640,7 @@ mod tests { .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) .to_http_parts(); - let json = JsonBody::::new(&req, &mut pl, None).await; + let json = JsonBody::::new(&req, &mut pl, None, true).await; assert_eq!( json.ok().unwrap(), MyObject { @@ -695,6 +710,21 @@ mod tests { assert!(s.is_err()) } + #[actix_rt::test] + async fn test_json_with_no_content_type() { + let (req, mut pl) = TestRequest::default() + .insert_header(( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("16"), + )) + .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) + .app_data(JsonConfig::default().content_type_required(false)) + .to_http_parts(); + + let s = Json::::from_request(&req, &mut pl).await; + assert!(s.is_ok()) + } + #[actix_rt::test] async fn test_with_config_in_data_wrapper() { let (req, mut pl) = TestRequest::default() From a3806cde190ec27703576a91b470a6c8706cd5b1 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sun, 12 Sep 2021 22:41:08 +0100 Subject: [PATCH 54/88] fix changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 439dd0540..05055a517 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,7 @@ ## Unreleased - 2021-xx-xx ### Added -* Option to allow `Json` extractor to work without a `Content-Length` header present. [#2362] +* Option to allow `Json` extractor to work without a `Content-Type` header present. [#2362] ### Changed * Associated type `FromRequest::Config` was removed. [#2233] From a6707fb7ee401d248268a8ac28ca96a854ad797a Mon Sep 17 00:00:00 2001 From: Omid Rad Date: Mon, 11 Oct 2021 19:28:09 +0200 Subject: [PATCH 55/88] Remove checked_expr (#2401) --- CHANGES.md | 4 ++++ src/service.rs | 10 ---------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 05055a517..848a908a3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,9 +8,13 @@ * Associated type `FromRequest::Config` was removed. [#2233] * Inner field made private on `web::Payload`. [#2384] +### Removed +* `ServiceResponse::checked_expr` was a legacy and just removed. [#2401] + [#2233]: https://github.com/actix/actix-web/pull/2233 [#2362]: https://github.com/actix/actix-web/pull/2362 [#2384]: https://github.com/actix/actix-web/pull/2384 +[#2401]: https://github.com/actix/actix-web/pull/2401 ## 4.0.0-beta.9 - 2021-09-09 diff --git a/src/service.rs b/src/service.rs index b9fa0e128..515d782d9 100644 --- a/src/service.rs +++ b/src/service.rs @@ -393,16 +393,6 @@ impl ServiceResponse { self.response.headers_mut() } - /// Execute closure and in case of error convert it to response. - pub fn checked_expr(mut self, f: F) -> Result - where - F: FnOnce(&mut Self) -> Result<(), E>, - E: Into, - { - f(&mut self).map_err(Into::into)?; - Ok(self) - } - /// Extract response body pub fn into_body(self) -> B { self.response.into_body() From 99985fc4ecdc694182cbbcbb490f0e3e0f6701c6 Mon Sep 17 00:00:00 2001 From: James Rhodes <30299230+jarhodes314@users.noreply.github.com> Date: Tue, 12 Oct 2021 18:35:33 +0100 Subject: [PATCH 56/88] web: implement `into_inner` for `Data` (#2407) --- CHANGES.md | 1 + src/data.rs | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 848a908a3..2509197fa 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ ### Changed * Associated type `FromRequest::Config` was removed. [#2233] * Inner field made private on `web::Payload`. [#2384] +* `Data::into_inner` and `Data::get_ref` no longer require T: Sized. [#2403] ### Removed * `ServiceResponse::checked_expr` was a legacy and just removed. [#2401] diff --git a/src/data.rs b/src/data.rs index 9d4fe0840..7e01d3462 100644 --- a/src/data.rs +++ b/src/data.rs @@ -75,7 +75,9 @@ impl Data { pub fn new(state: T) -> Data { Data(Arc::new(state)) } +} +impl Data { /// Get reference to inner app data. pub fn get_ref(&self) -> &T { self.0.as_ref() @@ -304,4 +306,38 @@ mod tests { let data_arc = Data::from(dyn_arc); assert_eq!(data_arc_box.get_num(), data_arc.get_num()) } + + #[actix_rt::test] + async fn test_dyn_data_into_arc() { + trait TestTrait { + fn get_num(&self) -> i32; + } + struct A {} + impl TestTrait for A { + fn get_num(&self) -> i32 { + 42 + } + } + let dyn_arc: Arc = Arc::new(A {}); + let data_arc = Data::from(dyn_arc); + let arc_from_data = data_arc.clone().into_inner(); + assert_eq!(data_arc.get_num(), arc_from_data.get_num()) + } + + #[actix_rt::test] + async fn test_get_ref_from_dyn_data() { + trait TestTrait { + fn get_num(&self) -> i32; + } + struct A {} + impl TestTrait for A { + fn get_num(&self) -> i32 { + 42 + } + } + let dyn_arc: Arc = Arc::new(A {}); + let data_arc = Data::from(dyn_arc); + let ref_data = data_arc.get_ref(); + assert_eq!(data_arc.get_num(), ref_data.get_num()) + } } From 6b3ea4fc619ea0c3951d1beefdb13c131e43fd7e Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 14 Oct 2021 18:06:31 +0100 Subject: [PATCH 57/88] copy original route macro input with compile errors (#2410) --- actix-web-codegen/CHANGES.md | 3 +++ actix-web-codegen/Cargo.toml | 1 + actix-web-codegen/src/route.rs | 17 +++++++++++++++-- .../trybuild/route-duplicate-method-fail.stderr | 4 ++-- .../trybuild/route-missing-method-fail.stderr | 4 ++-- .../route-unexpected-method-fail.stderr | 4 ++-- 6 files changed, 25 insertions(+), 8 deletions(-) diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index c154d8af4..dae9830aa 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx +* Improve error recovery potential when macro input is invalid. [#2410] + +[#2410]: https://github.com/actix/actix-web/pull/2410 ## 0.5.0-beta.4 - 2021-09-09 diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 2ad714f40..b8b346b8e 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -21,6 +21,7 @@ actix-router = "0.5.0-beta.2" [dev-dependencies] actix-rt = "2.2" +actix-macros = "0.2.2" actix-test = "0.1.0-beta.3" actix-utils = "3.0.0" actix-web = "4.0.0-beta.9" diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index c2f851a0e..4d4af7eca 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -349,8 +349,21 @@ pub(crate) fn with_method( input: TokenStream, ) -> TokenStream { let args = parse_macro_input!(args as syn::AttributeArgs); - match Route::new(args, input, method) { + match Route::new(args, input.clone(), method) { Ok(route) => route.into_token_stream().into(), - Err(err) => err.to_compile_error().into(), + // on parse err, make IDEs happy; see fn docs + Err(err) => input_and_compile_error(input, err), } } + +/// Converts the error to a token stream and appends it to the original input. +/// +/// Returning the original input in addition to the error is good for IDEs which can gracefully +/// recover and show more precise errors within the macro body. +/// +/// See for more info. +fn input_and_compile_error(mut item: TokenStream, err: syn::Error) -> TokenStream { + let compile_err = TokenStream::from(err.to_compile_error()); + item.extend(compile_err); + return item; +} diff --git a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr index abdc895d7..90cff1b1c 100644 --- a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr @@ -4,8 +4,8 @@ error: HTTP method defined more than once: `GET` 3 | #[route("/", method="GET", method="GET")] | ^^^^^ -error[E0425]: cannot find value `index` in this scope +error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied --> $DIR/route-duplicate-method-fail.rs:12:55 | 12 | let srv = actix_test::start(|| App::new().service(index)); - | ^^^^^ not found in this scope + | ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr index 0e16b5e27..c36b090c0 100644 --- a/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr @@ -6,8 +6,8 @@ error: The #[route(..)] macro requires at least one `method` attribute | = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0425]: cannot find value `index` in this scope +error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied --> $DIR/route-missing-method-fail.rs:12:55 | 12 | let srv = actix_test::start(|| App::new().service(index)); - | ^^^^^ not found in this scope + | ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` diff --git a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr index a638a96a6..dda366067 100644 --- a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr @@ -4,8 +4,8 @@ error: Unexpected HTTP method: `UNEXPECTED` 3 | #[route("/", method="UNEXPECTED")] | ^^^^^^^^^^^^ -error[E0425]: cannot find value `index` in this scope +error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied --> $DIR/route-unexpected-method-fail.rs:12:55 | 12 | let srv = actix_test::start(|| App::new().service(index)); - | ^^^^^ not found in this scope + | ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` From efdf3ab1c3ddc80508d020802c79a175134a30aa Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 19 Oct 2021 01:32:58 +0100 Subject: [PATCH 58/88] clippy --- actix-http/src/h1/dispatcher.rs | 14 +++++++------- actix-http/src/lib.rs | 7 +------ actix-http/src/ws/mask.rs | 6 +++--- actix-router/src/resource.rs | 20 ++++++++------------ actix-web-codegen/src/route.rs | 2 +- src/middleware/compress.rs | 2 +- src/types/path.rs | 14 ++++---------- src/types/query.rs | 8 +------- 8 files changed, 26 insertions(+), 47 deletions(-) diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index aef765b89..69530ed11 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -303,9 +303,9 @@ where body: &impl MessageBody, ) -> Result { let size = body.size(); - let mut this = self.project(); + let this = self.project(); this.codec - .encode(Message::Item((message, size)), &mut this.write_buf) + .encode(Message::Item((message, size)), this.write_buf) .map_err(|err| { if let Some(mut payload) = this.payload.take() { payload.set_error(PayloadError::Incomplete(None)); @@ -425,13 +425,13 @@ where Poll::Ready(Some(Ok(item))) => { this.codec.encode( Message::Chunk(Some(item)), - &mut this.write_buf, + this.write_buf, )?; } Poll::Ready(None) => { this.codec - .encode(Message::Chunk(None), &mut this.write_buf)?; + .encode(Message::Chunk(None), this.write_buf)?; // payload stream finished. // set state to None and handle next message this.state.set(State::None); @@ -460,13 +460,13 @@ where Poll::Ready(Some(Ok(item))) => { this.codec.encode( Message::Chunk(Some(item)), - &mut this.write_buf, + this.write_buf, )?; } Poll::Ready(None) => { this.codec - .encode(Message::Chunk(None), &mut this.write_buf)?; + .encode(Message::Chunk(None), this.write_buf)?; // payload stream finished. // set state to None and handle next message this.state.set(State::None); @@ -592,7 +592,7 @@ where let mut updated = false; let mut this = self.as_mut().project(); loop { - match this.codec.decode(&mut this.read_buf) { + match this.codec.decode(this.read_buf) { Ok(Some(msg)) => { updated = true; this.flags.insert(Flags::STARTED); diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index 3ad8d095e..42ce4ffe4 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -103,14 +103,9 @@ type ConnectCallback = dyn Fn(&IO, &mut Extensions); /// /// # Implementation Details /// Uses Option to reduce necessary allocations when merging with request extensions. +#[derive(Default)] pub(crate) struct OnConnectData(Option); -impl Default for OnConnectData { - fn default() -> Self { - Self(None) - } -} - impl OnConnectData { /// Construct by calling the on-connect callback with the underlying transport I/O. pub(crate) fn from_io( diff --git a/actix-http/src/ws/mask.rs b/actix-http/src/ws/mask.rs index 276ca4a85..11a6ddc32 100644 --- a/actix-http/src/ws/mask.rs +++ b/actix-http/src/ws/mask.rs @@ -25,8 +25,8 @@ pub fn apply_mask_fast32(buf: &mut [u8], mask: [u8; 4]) { // // un aligned prefix and suffix would be mask/unmask per byte. // proper aligned middle slice goes into fast path and operates on 4-byte blocks. - let (mut prefix, words, mut suffix) = unsafe { buf.align_to_mut::() }; - apply_mask_fallback(&mut prefix, mask); + let (prefix, words, suffix) = unsafe { buf.align_to_mut::() }; + apply_mask_fallback(prefix, mask); let head = prefix.len() & 3; let mask_u32 = if head > 0 { if cfg!(target_endian = "big") { @@ -40,7 +40,7 @@ pub fn apply_mask_fast32(buf: &mut [u8], mask: [u8; 4]) { for word in words.iter_mut() { *word ^= mask_u32; } - apply_mask_fallback(&mut suffix, mask_u32.to_ne_bytes()); + apply_mask_fallback(suffix, mask_u32.to_ne_bytes()); } #[cfg(test)] diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs index be54336e9..dcd655350 100644 --- a/actix-router/src/resource.rs +++ b/actix-router/src/resource.rs @@ -394,9 +394,7 @@ impl ResourceDef { pub fn set_name(&mut self, name: impl Into) { let name = name.into(); - if name.is_empty() { - panic!("resource name should not be empty"); - } + assert!(!name.is_empty(), "resource name should not be empty"); self.name = Some(name) } @@ -978,9 +976,7 @@ impl ResourceDef { let (name, pattern) = match param.find(':') { Some(idx) => { - if tail { - panic!("custom regex is not supported for tail match"); - } + assert!(!tail, "custom regex is not supported for tail match"); let (name, pattern) = param.split_at(idx); (name, &pattern[1..]) @@ -1087,12 +1083,12 @@ impl ResourceDef { re.push_str(&escape(unprocessed)); } - if dyn_segment_count > MAX_DYNAMIC_SEGMENTS { - panic!( - "Only {} dynamic segments are allowed, provided: {}", - MAX_DYNAMIC_SEGMENTS, dyn_segment_count - ); - } + assert!( + dyn_segment_count <= MAX_DYNAMIC_SEGMENTS, + "Only {} dynamic segments are allowed, provided: {}", + MAX_DYNAMIC_SEGMENTS, + dyn_segment_count + ); // Store the pattern in capture group #1 to have context info outside it let mut re = format!("({})", re); diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index 4d4af7eca..b18252002 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -365,5 +365,5 @@ pub(crate) fn with_method( fn input_and_compile_error(mut item: TokenStream, err: syn::Error) -> TokenStream { let compile_err = TokenStream::from(err.to_compile_error()); item.extend(compile_err); - return item; + item } diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 0e61a8e7e..4854f4beb 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -276,7 +276,7 @@ impl AcceptEncoding { let mut encodings = raw .replace(' ', "") .split(',') - .filter_map(|l| AcceptEncoding::new(l)) + .filter_map(AcceptEncoding::new) .collect::>(); encodings.sort(); diff --git a/src/types/path.rs b/src/types/path.rs index b58aec18d..cd24deb81 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -102,7 +102,7 @@ where fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { let error_handler = req .app_data::() - .and_then(|c| c.ehandler.clone()); + .and_then(|c| c.err_handler.clone()); ready( de::Deserialize::deserialize(PathDeserializer::new(req.match_info())) @@ -158,9 +158,9 @@ where /// ); /// } /// ``` -#[derive(Clone)] +#[derive(Clone, Default)] pub struct PathConfig { - ehandler: Option Error + Send + Sync>>, + err_handler: Option Error + Send + Sync>>, } impl PathConfig { @@ -169,17 +169,11 @@ impl PathConfig { where F: Fn(PathError, &HttpRequest) -> Error + Send + Sync + 'static, { - self.ehandler = Some(Arc::new(f)); + self.err_handler = Some(Arc::new(f)); self } } -impl Default for PathConfig { - fn default() -> Self { - PathConfig { ehandler: None } - } -} - #[cfg(test)] mod tests { use actix_router::ResourceDef; diff --git a/src/types/query.rs b/src/types/query.rs index eed337194..ba2034bfc 100644 --- a/src/types/query.rs +++ b/src/types/query.rs @@ -167,7 +167,7 @@ impl FromRequest for Query { /// .app_data(query_cfg) /// .service(index); /// ``` -#[derive(Clone)] +#[derive(Clone, Default)] pub struct QueryConfig { err_handler: Option Error + Send + Sync>>, } @@ -183,12 +183,6 @@ impl QueryConfig { } } -impl Default for QueryConfig { - fn default() -> Self { - QueryConfig { err_handler: None } - } -} - #[cfg(test)] mod tests { use actix_http::http::StatusCode; From ad22cc4e7f57ef11dcfc9706f1e1e747a3475815 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 19 Oct 2021 01:59:28 +0100 Subject: [PATCH 59/88] bump msrv to 1.52.1 --- .github/workflows/ci.yml | 2 +- CHANGES.md | 1 + Cargo.toml | 2 -- README.md | 4 ++-- actix-files/CHANGES.md | 1 + actix-files/README.md | 4 ++-- actix-http-test/CHANGES.md | 1 + actix-http-test/README.md | 4 ++-- actix-http/CHANGES.md | 1 + actix-http/README.md | 4 ++-- actix-multipart/CHANGES.md | 1 + actix-multipart/README.md | 4 ++-- actix-router/CHANGES.md | 1 + actix-test/CHANGES.md | 1 + actix-web-actors/CHANGES.md | 1 + actix-web-actors/README.md | 4 ++-- actix-web-codegen/CHANGES.md | 1 + actix-web-codegen/README.md | 4 ++-- actix-web-codegen/tests/trybuild.rs | 2 +- awc/README.md | 2 +- clippy.toml | 2 +- src/lib.rs | 2 +- 22 files changed, 28 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ec034bc8..e4d713b48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } - { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc } version: - - 1.51.0 # MSRV + - 1.52.0 # MSRV - stable - nightly diff --git a/CHANGES.md b/CHANGES.md index 2509197fa..1654b0856 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ * Associated type `FromRequest::Config` was removed. [#2233] * Inner field made private on `web::Payload`. [#2384] * `Data::into_inner` and `Data::get_ref` no longer require T: Sized. [#2403] +* Minimum supported Rust version (MSRV) is now 1.52. ### Removed * `ServiceResponse::checked_expr` was a legacy and just removed. [#2401] diff --git a/Cargo.toml b/Cargo.toml index dc7e9af3f..ae47398e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,8 +38,6 @@ members = [ "actix-test", "actix-router", ] -# enable when MSRV is 1.51+ -# resolver = "2" [features] default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] diff --git a/README.md b/README.md index 13ec3a01a..00e8fa6ce 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.9)](https://docs.rs/actix-web/4.0.0-beta.9) -[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) +[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) [![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.9/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.9)
@@ -32,7 +32,7 @@ * SSL support using OpenSSL or Rustls * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) * Includes an async [HTTP client](https://docs.rs/awc/) -* Runs on stable Rust 1.51+ +* Runs on stable Rust 1.52+ ## Documentation diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 6d1512c22..8e0a3eecf 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.52. ## 0.6.0-beta.7 - 2021-09-09 diff --git a/actix-files/README.md b/actix-files/README.md index 31bbd036f..ed15e3333 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) [![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.7)](https://docs.rs/actix-files/0.6.0-beta.7) -[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) +[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![License](https://img.shields.io/crates/l/actix-files.svg)
[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.7/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.7) @@ -15,4 +15,4 @@ - [API Documentation](https://docs.rs/actix-files/) - [Example Project](https://github.com/actix/examples/tree/master/basics/static_index) -- Minimum supported Rust version: 1.51 or later +- Minimum Supported Rust Version (MSRV): 1.52 diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index 69e96f98d..98b197bcf 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.52. ## 3.0.0-beta.5 - 2021-09-09 diff --git a/actix-http-test/README.md b/actix-http-test/README.md index f75b9c137..6bf0d710a 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) [![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.5)](https://docs.rs/actix-http-test/3.0.0-beta.5) -[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) +[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.5/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.5) @@ -14,4 +14,4 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-http-test) -- Minimum Supported Rust Version (MSRV): 1.51.0 +- Minimum Supported Rust Version (MSRV): 1.52 diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 775b9e6d5..71aa8668d 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.52. ## 3.0.0-beta.10 - 2021-09-09 diff --git a/actix-http/README.md b/actix-http/README.md index b58b47f5c..68a6e0a5d 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) [![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.10)](https://docs.rs/actix-http/3.0.0-beta.10) -[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) +[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.10/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.10) @@ -14,7 +14,7 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-http) -- Minimum Supported Rust Version (MSRV): 1.51.0 +- Minimum Supported Rust Version (MSRV): 1.52 ## Example diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index c32583f08..33da6a202 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.52. ## 0.4.0-beta.6 - 2021-09-09 diff --git a/actix-multipart/README.md b/actix-multipart/README.md index f3366f50c..254ef877b 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) [![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.6)](https://docs.rs/actix-multipart/0.4.0-beta.6) -[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) +[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.6/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.6) @@ -14,4 +14,4 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-multipart) -- Minimum Supported Rust Version (MSRV): 1.51.0 +- Minimum Supported Rust Version (MSRV): 1.52 diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index 001903438..c2858f2ba 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.52. ## 0.5.0-beta.2 - 2021-09-09 diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index 58e05c4b6..9c0a9ee81 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.52. ## 0.1.0-beta.4 - 2021-09-09 diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index 2e453063f..e3693f0f6 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.52. ## 4.0.0-beta.7 - 2021-09-09 diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md index a647e4bc9..2c29dedf2 100644 --- a/actix-web-actors/README.md +++ b/actix-web-actors/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) [![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.7)](https://docs.rs/actix-web-actors/4.0.0-beta.7) -[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) +[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.7/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.7) @@ -14,4 +14,4 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-web-actors) -- Minimum supported Rust version: 1.51 or later +- Minimum Supported Rust Version (MSRV): 1.52 diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index dae9830aa..4b3e04acd 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -2,6 +2,7 @@ ## Unreleased - 2021-xx-xx * Improve error recovery potential when macro input is invalid. [#2410] +* Minimum supported Rust version (MSRV) is now 1.52. [#2410]: https://github.com/actix/actix-web/pull/2410 diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index 268e8b01d..ee552cfb5 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) [![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.4)](https://docs.rs/actix-web-codegen/0.5.0-beta.4) -[![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html) +[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.4/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.4) @@ -14,7 +14,7 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-web-codegen) -- Minimum supported Rust version: 1.51 or later. +- Minimum Supported Rust Version (MSRV): 1.52 ## Compile Testing diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs index 54bc1caec..edbe1a8ed 100644 --- a/actix-web-codegen/tests/trybuild.rs +++ b/actix-web-codegen/tests/trybuild.rs @@ -1,4 +1,4 @@ -#[rustversion::stable(1.51)] // MSRV +#[rustversion::stable(1.52)] // MSRV #[test] fn compile_macros() { let t = trybuild::TestCases::new(); diff --git a/awc/README.md b/awc/README.md index 868bc5cae..38c967e69 100644 --- a/awc/README.md +++ b/awc/README.md @@ -12,7 +12,7 @@ - [API Documentation](https://docs.rs/awc) - [Example Project](https://github.com/actix/examples/tree/HEAD/security/awc_https) -- Minimum Supported Rust Version (MSRV): 1.51.0 +- Minimum Supported Rust Version (MSRV): 1.52 ## Example diff --git a/clippy.toml b/clippy.toml index 829dd1c59..cef91fde7 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -msrv = "1.51" +msrv = "1.52" diff --git a/src/lib.rs b/src/lib.rs index d008fdb7f..3ad77ff5f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,7 +53,7 @@ //! * SSL support using OpenSSL or Rustls //! * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) //! * Includes an async [HTTP client](https://docs.rs/awc/) -//! * Runs on stable Rust 1.51+ +//! * Runs on stable Rust 1.52+ //! //! # Crate Features //! * `cookies` - cookies support (enabled by default) From 591abc37c31d969998247dda91baf4c950d0cd91 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 19 Oct 2021 17:30:32 +0100 Subject: [PATCH 60/88] add test runtime macro (#2409) --- Cargo.toml | 2 +- actix-files/src/lib.rs | 2 +- actix-files/tests/encoding.rs | 2 +- actix-files/tests/guard.rs | 2 +- actix-http-test/src/lib.rs | 2 +- actix-http/src/client/pool.rs | 1 + actix-http/src/h1/encoder.rs | 1 + actix-router/src/router.rs | 3 +- actix-test/src/lib.rs | 4 +- actix-web-codegen/CHANGES.md | 2 + actix-web-codegen/Cargo.toml | 11 ++--- actix-web-codegen/src/lib.rs | 40 ++++++++++++++----- actix-web-codegen/src/route.rs | 22 +++++----- actix-web-codegen/tests/test_macro.rs | 2 +- actix-web-codegen/tests/trybuild.rs | 2 + .../tests/trybuild/test-runtime.rs | 6 +++ examples/basic.rs | 2 +- src/test.rs | 16 ++++---- tests/test-macro-import-conflict.rs | 15 +++++++ 19 files changed, 94 insertions(+), 43 deletions(-) create mode 100644 actix-web-codegen/tests/trybuild/test-runtime.rs create mode 100644 tests/test-macro-import-conflict.rs diff --git a/Cargo.toml b/Cargo.toml index ae47398e1..630ef5642 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,7 +67,7 @@ __compress = [] [dependencies] actix-codec = "0.4.0" -actix-macros = "0.2.1" +actix-macros = "0.2.3" actix-router = "0.5.0-beta.2" actix-rt = "2.2" actix-server = "2.0.0-beta.3" diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 1eb091aaf..175c6eaee 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -83,7 +83,7 @@ mod tests { use super::*; - #[actix_rt::test] + #[actix_web::test] async fn test_file_extension_to_mime() { let m = file_extension_to_mime(""); assert_eq!(m, mime::APPLICATION_OCTET_STREAM); diff --git a/actix-files/tests/encoding.rs b/actix-files/tests/encoding.rs index d21d4f8fd..652a7c12b 100644 --- a/actix-files/tests/encoding.rs +++ b/actix-files/tests/encoding.rs @@ -8,7 +8,7 @@ use actix_web::{ App, }; -#[actix_rt::test] +#[actix_web::test] async fn test_utf8_file_contents() { // use default ISO-8859-1 encoding let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await; diff --git a/actix-files/tests/guard.rs b/actix-files/tests/guard.rs index 8b1785e7f..d053f3fdc 100644 --- a/actix-files/tests/guard.rs +++ b/actix-files/tests/guard.rs @@ -7,7 +7,7 @@ use actix_web::{ }; use bytes::Bytes; -#[actix_rt::test] +#[actix_web::test] async fn test_guard_filter() { let srv = test::init_service( App::new() diff --git a/actix-http-test/src/lib.rs b/actix-http-test/src/lib.rs index ec7b46ffb..c7b083b5e 100644 --- a/actix-http-test/src/lib.rs +++ b/actix-http-test/src/lib.rs @@ -36,7 +36,7 @@ use socket2::{Domain, Protocol, Socket, Type}; /// Ok(HttpResponse::Ok().into()) /// } /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_example() { /// let mut srv = TestServer::start( /// || HttpService::new( diff --git a/actix-http/src/client/pool.rs b/actix-http/src/client/pool.rs index 88188038f..7c36dcff9 100644 --- a/actix-http/src/client/pool.rs +++ b/actix-http/src/client/pool.rs @@ -186,6 +186,7 @@ where let mut conn = None; // check if there is idle connection for given key. + #[allow(clippy::must_not_suspend)] let mut map = inner.available.borrow_mut(); if let Some(conns) = map.get_mut(&key) { diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index 5e1d47785..ead14206b 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -20,6 +20,7 @@ const AVERAGE_HEADER_SIZE: usize = 30; #[derive(Debug)] pub(crate) struct MessageEncoder { + #[allow(dead_code)] pub length: BodySize, pub te: TransferEncoding, _phantom: PhantomData, diff --git a/actix-router/src/router.rs b/actix-router/src/router.rs index f5deb8583..fad1a440b 100644 --- a/actix-router/src/router.rs +++ b/actix-router/src/router.rs @@ -6,8 +6,9 @@ use crate::{IntoPatterns, Resource, ResourceDef, ResourcePath}; pub struct ResourceId(pub u16); /// Information about current resource -#[derive(Clone, Debug)] +#[derive(Debug, Clone)] pub struct ResourceInfo { + #[allow(dead_code)] resource: ResourceId, } diff --git a/actix-test/src/lib.rs b/actix-test/src/lib.rs index c863af44a..23a7eeba1 100644 --- a/actix-test/src/lib.rs +++ b/actix-test/src/lib.rs @@ -64,7 +64,7 @@ pub use actix_web::test::{ /// Ok(HttpResponse::Ok()) /// } /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_example() { /// let srv = actix_test::start(|| /// App::new().service(my_handler) @@ -104,7 +104,7 @@ where /// Ok(HttpResponse::Ok()) /// } /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_example() { /// let srv = actix_test::start_with(actix_test::config().h1(), || /// App::new().service(my_handler) diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index 4b3e04acd..f1f050b2c 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -2,9 +2,11 @@ ## Unreleased - 2021-xx-xx * Improve error recovery potential when macro input is invalid. [#2410] +* Add `#[actix_web::test]` macro for setting up tests with a runtime. [#2409] * Minimum supported Rust version (MSRV) is now 1.52. [#2410]: https://github.com/actix/actix-web/pull/2410 +[#2409]: https://github.com/actix/actix-web/pull/2409 ## 0.5.0-beta.4 - 2021-09-09 diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index b8b346b8e..afedafdfd 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -2,11 +2,12 @@ name = "actix-web-codegen" version = "0.5.0-beta.4" description = "Routing and runtime macros for Actix Web" -readme = "README.md" homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web" -documentation = "https://docs.rs/actix-web-codegen" -authors = ["Nikolay Kim "] +repository = "https://github.com/actix/actix-web.git" +authors = [ + "Nikolay Kim ", + "Rob Ede ", +] license = "MIT OR Apache-2.0" edition = "2018" @@ -21,7 +22,7 @@ actix-router = "0.5.0-beta.2" [dev-dependencies] actix-rt = "2.2" -actix-macros = "0.2.2" +actix-macros = "0.2.3" actix-test = "0.1.0-beta.3" actix-utils = "3.0.0" actix-web = "4.0.0-beta.9" diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 2237f422c..85faf6bca 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -59,6 +59,7 @@ #![recursion_limit = "512"] use proc_macro::TokenStream; +use quote::quote; mod route; @@ -157,24 +158,41 @@ method_macro! { } /// Marks async main function as the actix system entry-point. -/// -/// # Actix Web Re-export -/// This macro can be applied with `#[actix_web::main]` when used in Actix Web applications. -/// + /// # Examples /// ``` -/// #[actix_web_codegen::main] +/// #[actix_web::main] /// async fn main() { /// async { println!("Hello world"); }.await /// } /// ``` #[proc_macro_attribute] pub fn main(_: TokenStream, item: TokenStream) -> TokenStream { - use quote::quote; - let input = syn::parse_macro_input!(item as syn::ItemFn); - (quote! { - #[actix_web::rt::main(system = "::actix_web::rt::System")] - #input + let mut output: TokenStream = (quote! { + #[::actix_web::rt::main(system = "::actix_web::rt::System")] }) - .into() + .into(); + + output.extend(item); + output +} + +/// Marks async test functions to use the actix system entry-point. +/// +/// # Examples +/// ``` +/// #[actix_web::test] +/// async fn test() { +/// assert_eq!(async { "Hello world" }.await, "Hello world"); +/// } +/// ``` +#[proc_macro_attribute] +pub fn test(_: TokenStream, item: TokenStream) -> TokenStream { + let mut output: TokenStream = (quote! { + #[::actix_web::rt::test(system = "::actix_web::rt::System")] + }) + .into(); + + output.extend(item); + output } diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index b18252002..eac1948a7 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -220,7 +220,7 @@ fn guess_resource_type(typ: &syn::Type) -> ResourceType { impl Route { pub fn new( args: AttributeArgs, - input: TokenStream, + ast: syn::ItemFn, method: Option, ) -> syn::Result { if args.is_empty() { @@ -234,14 +234,11 @@ impl Route { ), )); } - let ast: syn::ItemFn = syn::parse(input)?; + let name = ast.sig.ident.clone(); - // Try and pull out the doc comments so that we can reapply them to the - // generated struct. - // - // Note that multi line doc comments are converted to multiple doc - // attributes. + // Try and pull out the doc comments so that we can reapply them to the generated struct. + // Note that multi line doc comments are converted to multiple doc attributes. let doc_attributes = ast .attrs .iter() @@ -349,9 +346,16 @@ pub(crate) fn with_method( input: TokenStream, ) -> TokenStream { let args = parse_macro_input!(args as syn::AttributeArgs); - match Route::new(args, input.clone(), method) { + + let ast = match syn::parse::(input.clone()) { + Ok(ast) => ast, + // on parse error, make IDEs happy; see fn docs + Err(err) => return input_and_compile_error(input, err), + }; + + match Route::new(args, ast, method) { Ok(route) => route.into_token_stream().into(), - // on parse err, make IDEs happy; see fn docs + // on macro related error, make IDEs happy; see fn docs Err(err) => input_and_compile_error(input, err), } } diff --git a/actix-web-codegen/tests/test_macro.rs b/actix-web-codegen/tests/test_macro.rs index 6b08c409c..769cf2bc3 100644 --- a/actix-web-codegen/tests/test_macro.rs +++ b/actix-web-codegen/tests/test_macro.rs @@ -256,7 +256,7 @@ async fn test_auto_async() { assert!(response.status().is_success()); } -#[actix_rt::test] +#[actix_web::test] async fn test_wrap() { let srv = actix_test::start(|| App::new().service(get_wrap)); diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs index edbe1a8ed..dd70cb7ca 100644 --- a/actix-web-codegen/tests/trybuild.rs +++ b/actix-web-codegen/tests/trybuild.rs @@ -13,4 +13,6 @@ fn compile_macros() { t.compile_fail("tests/trybuild/route-malformed-path-fail.rs"); t.pass("tests/trybuild/docstring-ok.rs"); + + t.pass("tests/trybuild/test-runtime.rs"); } diff --git a/actix-web-codegen/tests/trybuild/test-runtime.rs b/actix-web-codegen/tests/trybuild/test-runtime.rs new file mode 100644 index 000000000..0b901b258 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/test-runtime.rs @@ -0,0 +1,6 @@ +#[actix_web::test] +async fn my_test() { + assert!(async { 1 }.await, 1); +} + +fn main() {} diff --git a/examples/basic.rs b/examples/basic.rs index 796f002e8..d29546129 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -35,7 +35,7 @@ async fn main() -> std::io::Result<()> { ) .service(web::resource("/test1.html").to(|| async { "Test\r\n" })) }) - .bind("127.0.0.1:8080")? + .bind(("127.0.0.1", 8080))? .workers(1) .run() .await diff --git a/src/test.rs b/src/test.rs index 99e708592..43bf612c6 100644 --- a/src/test.rs +++ b/src/test.rs @@ -52,7 +52,7 @@ pub fn default_service( /// use actix_service::Service; /// use actix_web::{test, web, App, HttpResponse, http::StatusCode}; /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_init_service() { /// let app = test::init_service( /// App::new() @@ -98,7 +98,7 @@ where /// ``` /// use actix_web::{test, web, App, HttpResponse, http::StatusCode}; /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_response() { /// let app = test::init_service( /// App::new() @@ -129,7 +129,7 @@ where /// use actix_web::{test, web, App, HttpResponse, http::header}; /// use bytes::Bytes; /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_index() { /// let app = test::init_service( /// App::new().service( @@ -176,7 +176,7 @@ where /// use actix_web::{test, web, App, HttpResponse, http::header}; /// use bytes::Bytes; /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_index() { /// let app = test::init_service( /// App::new().service( @@ -224,7 +224,7 @@ where /// name: String, /// } /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_post_person() { /// let app = test::init_service( /// App::new().service( @@ -296,7 +296,7 @@ where /// name: String /// } /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_add_person() { /// let app = test::init_service( /// App::new().service( @@ -356,8 +356,8 @@ where /// } /// } /// -/// #[test] -/// fn test_index() { +/// #[actix_web::test] +/// async fn test_index() { /// let req = test::TestRequest::default().insert_header("content-type", "text/plain") /// .to_http_request(); /// diff --git a/tests/test-macro-import-conflict.rs b/tests/test-macro-import-conflict.rs new file mode 100644 index 000000000..0d23bb41d --- /dev/null +++ b/tests/test-macro-import-conflict.rs @@ -0,0 +1,15 @@ +//! Checks that test macro does not cause problems in the presence of imports named "test" that +//! could be either a module with test items or the "test with runtime" macro itself. +//! +//! Before actix/actix-net#399 was implemented, this macro was running twice. The first run output +//! `#[test]` and it got run again and since it was in scope. +//! +//! Prevented by using the fully-qualified test marker (`#[::core::prelude::v1::test]`). + +use actix_web::test; + +#[actix_web::test] +async fn test_macro_naming_conflict() { + let _req = test::TestRequest::default(); + assert_eq!(async { 1 }.await, 1); +} From 4f6f0b0137ebe6ff106a0ed7b67cb15c68b29fc8 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 20 Oct 2021 02:00:11 +0100 Subject: [PATCH 61/88] chore: Bump rustls to 0.20.0 (#2416) Co-authored-by: Kirill Mironov --- CHANGES.md | 2 + Cargo.toml | 10 +++-- actix-http-test/Cargo.toml | 2 +- actix-http/CHANGES.md | 4 ++ actix-http/Cargo.toml | 11 ++--- actix-http/examples/ws.rs | 23 +++++++---- actix-http/src/client/connector.rs | 13 +++--- actix-http/src/h2/service.rs | 2 +- actix-http/src/service.rs | 7 ++-- actix-http/tests/test_rustls.rs | 58 +++++++++++++++++++++------ actix-test/Cargo.toml | 2 +- awc/CHANGES.md | 3 ++ awc/Cargo.toml | 10 +++-- awc/tests/test_rustls_client.rs | 64 ++++++++++++++++++++++-------- tests/test_server.rs | 24 ++++++----- 15 files changed, 162 insertions(+), 73 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1654b0856..cea963ca1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ * Inner field made private on `web::Payload`. [#2384] * `Data::into_inner` and `Data::get_ref` no longer require T: Sized. [#2403] * Minimum supported Rust version (MSRV) is now 1.52. +* Updated rustls to v0.20. [#2414] ### Removed * `ServiceResponse::checked_expr` was a legacy and just removed. [#2401] @@ -17,6 +18,7 @@ [#2362]: https://github.com/actix/actix-web/pull/2362 [#2384]: https://github.com/actix/actix-web/pull/2384 [#2401]: https://github.com/actix/actix-web/pull/2401 +[#2414]: https://github.com/actix/actix-web/pull/2414 ## 4.0.0-beta.9 - 2021-09-09 diff --git a/Cargo.toml b/Cargo.toml index 630ef5642..e53f4411b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,7 +73,7 @@ actix-rt = "2.2" actix-server = "2.0.0-beta.3" actix-service = "2.0.0" actix-utils = "3.0.0" -actix-tls = { version = "3.0.0-beta.5", default-features = false, optional = true } +actix-tls = { version = "3.0.0-beta.6", default-features = false, optional = true } actix-web-codegen = "0.5.0-beta.4" actix-http = "3.0.0-beta.10" @@ -111,11 +111,15 @@ brotli2 = "0.3.2" criterion = { version = "0.3", features = ["html_reports"] } env_logger = "0.8" flate2 = "1.0.13" -zstd = "0.7" +futures-util = { version = "0.3.7", default-features = false, features = ["std"] } rand = "0.8" rcgen = "0.8" +rustls-pemfile = "0.2" tls-openssl = { package = "openssl", version = "0.10.9" } -tls-rustls = { package = "rustls", version = "0.19.0" } +tls-rustls = { package = "rustls", version = "0.20.0" } +webpki = "0.22" +webpki-roots = "0.22" +zstd = "0.7" [profile.dev] # Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much. diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index ee4971a1e..2bdd6969d 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -31,7 +31,7 @@ openssl = ["tls-openssl", "awc/openssl"] [dependencies] actix-service = "2.0.0" actix-codec = "0.4.0" -actix-tls = "3.0.0-beta.5" +actix-tls = "3.0.0-beta.6" actix-utils = "3.0.0" actix-rt = "2.2" actix-server = "2.0.0-beta.3" diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 71aa8668d..3911fc00a 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,8 +1,12 @@ # Changes ## Unreleased - 2021-xx-xx +### Changed +* Updated rustls to v0.20. [#2414] * Minimum supported Rust version (MSRV) is now 1.52. +[#2414]: https://github.com/actix/actix-web/pull/2414 + ## 3.0.0-beta.10 - 2021-09-09 ### Changed diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 889c91331..d0724ba5f 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -46,7 +46,7 @@ actix-service = "2.0.0" actix-codec = "0.4.0" actix-utils = "3.0.0" actix-rt = "2.2" -actix-tls = { version = "3.0.0-beta.5", features = ["accept", "connect"] } +actix-tls = { version = "3.0.0-beta.6", features = ["accept", "connect"] } ahash = "0.7" base64 = "0.13" @@ -85,17 +85,18 @@ trust-dns-resolver = { version = "0.20.0", optional = true } [dev-dependencies] actix-server = "2.0.0-beta.3" actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] } -actix-tls = { version = "3.0.0-beta.5", features = ["openssl"] } +actix-tls = { version = "3.0.0-beta.6", features = ["openssl"] } async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } env_logger = "0.8" rcgen = "0.8" regex = "1.3" +rustls-pemfile = "0.2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -tls-openssl = { version = "0.10", package = "openssl" } -tls-rustls = { version = "0.19", package = "rustls" } -webpki = { version = "0.21" } +tls-openssl = { package = "openssl", version = "0.10.9" } +tls-rustls = { package = "rustls", version = "0.20.0" } +webpki = { version = "0.22" } [[example]] name = "ws" diff --git a/actix-http/examples/ws.rs b/actix-http/examples/ws.rs index d3cedf870..b6be4d2f1 100644 --- a/actix-http/examples/ws.rs +++ b/actix-http/examples/ws.rs @@ -85,22 +85,31 @@ impl Stream for Heartbeat { fn tls_config() -> rustls::ServerConfig { use std::io::BufReader; - use rustls::{ - internal::pemfile::{certs, pkcs8_private_keys}, - NoClientAuth, ServerConfig, - }; + use rustls::{Certificate, PrivateKey}; + use rustls_pemfile::{certs, pkcs8_private_keys}; let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap(); let cert_file = cert.serialize_pem().unwrap(); let key_file = cert.serialize_private_key_pem(); - let mut config = ServerConfig::new(NoClientAuth::new()); let cert_file = &mut BufReader::new(cert_file.as_bytes()); let key_file = &mut BufReader::new(key_file.as_bytes()); - let cert_chain = certs(cert_file).unwrap(); + let cert_chain = certs(cert_file) + .unwrap() + .into_iter() + .map(Certificate) + .collect(); let mut keys = pkcs8_private_keys(key_file).unwrap(); - config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); + + let mut config = rustls::ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(cert_chain, PrivateKey(keys.remove(0))) + .unwrap(); + + config.alpn_protocols.push(b"http/1.1".to_vec()); + config.alpn_protocols.push(b"h2".to_vec()); config } diff --git a/actix-http/src/client/connector.rs b/actix-http/src/client/connector.rs index bd46919e8..bde5e4853 100644 --- a/actix-http/src/client/connector.rs +++ b/actix-http/src/client/connector.rs @@ -313,18 +313,15 @@ where SslConnector::Rustls(tls) => { const H2: &[u8] = b"h2"; - use actix_tls::connect::ssl::rustls::{ - RustlsConnector, Session, TlsStream, - }; + use actix_tls::connect::ssl::rustls::{RustlsConnector, TlsStream}; impl IntoConnectionIo for TcpConnection> { fn into_connection_io(self) -> (Box, Protocol) { let sock = self.into_parts().0; - let h2 = sock - .get_ref() - .1 - .get_alpn_protocol() - .map_or(false, |protos| protos.windows(2).any(|w| w == H2)); + let h2 = + sock.get_ref().1.alpn_protocol().map_or(false, |protos| { + protos.windows(2).any(|w| w == H2) + }); if h2 { (Box::new(sock), Protocol::Http2) } else { diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index 09e24045b..32dae8ac3 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -177,7 +177,7 @@ mod rustls { > { let mut protos = vec![b"h2".to_vec()]; protos.extend_from_slice(&config.alpn_protocols); - config.set_protocols(&protos); + config.alpn_protocols = protos; Acceptor::new(config) .map_err(TlsError::Tls) diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index afe47bf2d..62c968870 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -263,7 +263,7 @@ mod openssl { mod rustls { use std::io; - use actix_tls::accept::rustls::{Acceptor, ServerConfig, Session, TlsStream}; + use actix_tls::accept::rustls::{Acceptor, ServerConfig, TlsStream}; use actix_tls::accept::TlsError; use super::*; @@ -308,14 +308,13 @@ mod rustls { > { let mut protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; protos.extend_from_slice(&config.alpn_protocols); - config.set_protocols(&protos); + config.alpn_protocols = protos; Acceptor::new(config) .map_err(TlsError::Tls) .map_init_err(|_| panic!()) .and_then(|io: TlsStream| async { - let proto = if let Some(protos) = io.get_ref().1.get_alpn_protocol() - { + let proto = if let Some(protos) = io.get_ref().1.alpn_protocol() { if protos.windows(2).any(|window| window == b"h2") { Protocol::Http2 } else { diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index cb7c77ad6..69c7db74d 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -3,7 +3,7 @@ extern crate tls_rustls as rustls; use std::{ - convert::Infallible, + convert::{Infallible, TryFrom}, io::{self, BufReader, Write}, net::{SocketAddr, TcpStream as StdTcpStream}, sync::Arc, @@ -20,16 +20,17 @@ use actix_http::{ }; use actix_http_test::test_server; use actix_service::{fn_factory_with_config, fn_service}; +use actix_tls::connect::ssl::rustls::TLS_SERVER_ROOTS; use actix_utils::future::{err, ok}; use bytes::{Bytes, BytesMut}; use derive_more::{Display, Error}; use futures_core::Stream; use futures_util::stream::{once, StreamExt as _}; use rustls::{ - internal::pemfile::{certs, pkcs8_private_keys}, - NoClientAuth, ServerConfig as RustlsServerConfig, Session, + Certificate, OwnedTrustAnchor, PrivateKey, RootCertStore, + ServerConfig as RustlsServerConfig, ServerName, }; -use webpki::DNSNameRef; +use rustls_pemfile::{certs, pkcs8_private_keys}; async fn load_body(mut stream: S) -> Result where @@ -47,13 +48,24 @@ fn tls_config() -> RustlsServerConfig { let cert_file = cert.serialize_pem().unwrap(); let key_file = cert.serialize_private_key_pem(); - let mut config = RustlsServerConfig::new(NoClientAuth::new()); let cert_file = &mut BufReader::new(cert_file.as_bytes()); let key_file = &mut BufReader::new(key_file.as_bytes()); - let cert_chain = certs(cert_file).unwrap(); + let cert_chain = certs(cert_file) + .unwrap() + .into_iter() + .map(Certificate) + .collect(); let mut keys = pkcs8_private_keys(key_file).unwrap(); - config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); + + let mut config = RustlsServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(cert_chain, PrivateKey(keys.remove(0))) + .unwrap(); + + config.alpn_protocols.push(HTTP1_1_ALPN_PROTOCOL.to_vec()); + config.alpn_protocols.push(H2_ALPN_PROTOCOL.to_vec()); config } @@ -62,19 +74,39 @@ pub fn get_negotiated_alpn_protocol( addr: SocketAddr, client_alpn_protocol: &[u8], ) -> Option> { - let mut config = rustls::ClientConfig::new(); + let mut root_certs = RootCertStore::empty(); + for cert in TLS_SERVER_ROOTS.0 { + let cert = OwnedTrustAnchor::from_subject_spki_name_constraints( + cert.subject, + cert.spki, + cert.name_constraints, + ); + let certs = vec![cert].into_iter(); + root_certs.add_server_trust_anchors(certs); + } + + let mut config = rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(root_certs) + .with_no_client_auth(); + config.alpn_protocols.push(client_alpn_protocol.to_vec()); - let mut sess = rustls::ClientSession::new( - &Arc::new(config), - DNSNameRef::try_from_ascii_str("localhost").unwrap(), - ); + + let mut sess = rustls::ClientConnection::new( + Arc::new(config), + ServerName::try_from("localhost").unwrap(), + ) + .unwrap(); + let mut sock = StdTcpStream::connect(addr).unwrap(); let mut stream = rustls::Stream::new(&mut sess, &mut sock); + // The handshake will fails because the client will not be able to verify the server // certificate, but it doesn't matter here as we are just interested in the negotiated ALPN // protocol let _ = stream.flush(); - sess.get_alpn_protocol().map(|proto| proto.to_vec()) + + sess.alpn_protocol().map(|proto| proto.to_vec()) } #[actix_rt::test] diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index 41d32257c..62a27e5a5 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -35,4 +35,4 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" serde_urlencoded = "0.7" tls-openssl = { package = "openssl", version = "0.10.9", optional = true } -tls-rustls = { package = "rustls", version = "0.19.0", optional = true } +tls-rustls = { package = "rustls", version = "0.20.0", optional = true } diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 252b62efa..49d88e5e8 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx +* Updated rustls to v0.20. [#2414] + +[#2414]: https://github.com/actix/actix-web/pull/2414 ## 3.0.0-beta.8 - 2021-09-09 diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 262c3dce5..967d49602 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -73,8 +73,8 @@ rand = "0.8" serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.7" -tls-openssl = { version = "0.10.9", package = "openssl", optional = true } -tls-rustls = { version = "0.19.0", package = "rustls", optional = true, features = ["dangerous_configuration"] } +tls-openssl = { package = "openssl", version = "0.10.9", optional = true } +tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features = ["dangerous_configuration"] } [dev-dependencies] actix-web = { version = "4.0.0-beta.9", features = ["openssl"] } @@ -82,7 +82,7 @@ actix-http = { version = "3.0.0-beta.10", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] } actix-utils = "3.0.0" actix-server = "2.0.0-beta.3" -actix-tls = { version = "3.0.0-beta.5", features = ["openssl", "rustls"] } +actix-tls = { version = "3.0.0-beta.6", features = ["openssl", "rustls"] } actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] } brotli2 = "0.3.2" @@ -90,7 +90,9 @@ env_logger = "0.8" flate2 = "1.0.13" futures-util = { version = "0.3.7", default-features = false } rcgen = "0.8" -webpki = "0.21" +rustls-pemfile = "0.2" +webpki = "0.22" +webpki-roots = "0.22" [[example]] name = "client" diff --git a/awc/tests/test_rustls_client.rs b/awc/tests/test_rustls_client.rs index bc811c046..95f2d0616 100644 --- a/awc/tests/test_rustls_client.rs +++ b/awc/tests/test_rustls_client.rs @@ -8,6 +8,7 @@ use std::{ atomic::{AtomicUsize, Ordering}, Arc, }, + time::SystemTime, }; use actix_http::HttpService; @@ -15,37 +16,52 @@ use actix_http_test::test_server; use actix_service::{fn_service, map_config, ServiceFactoryExt}; use actix_utils::future::ok; use actix_web::{dev::AppConfig, http::Version, web, App, HttpResponse}; -use rustls::internal::pemfile::{certs, pkcs8_private_keys}; -use rustls::{ClientConfig, NoClientAuth, ServerConfig}; +use rustls::{ + client::{ServerCertVerified, ServerCertVerifier}, + Certificate, ClientConfig, OwnedTrustAnchor, PrivateKey, RootCertStore, ServerConfig, + ServerName, +}; +use rustls_pemfile::{certs, pkcs8_private_keys}; +use webpki_roots::TLS_SERVER_ROOTS; fn tls_config() -> ServerConfig { let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap(); let cert_file = cert.serialize_pem().unwrap(); let key_file = cert.serialize_private_key_pem(); - let mut config = ServerConfig::new(NoClientAuth::new()); let cert_file = &mut BufReader::new(cert_file.as_bytes()); let key_file = &mut BufReader::new(key_file.as_bytes()); - let cert_chain = certs(cert_file).unwrap(); + let cert_chain = certs(cert_file) + .unwrap() + .into_iter() + .map(Certificate) + .collect(); let mut keys = pkcs8_private_keys(key_file).unwrap(); - config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); - config + ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(cert_chain, PrivateKey(keys.remove(0))) + .unwrap() } mod danger { + use super::*; + pub struct NoCertificateVerification; - impl rustls::ServerCertVerifier for NoCertificateVerification { + impl ServerCertVerifier for NoCertificateVerification { fn verify_server_cert( &self, - _roots: &rustls::RootCertStore, - _presented_certs: &[rustls::Certificate], - _dns_name: webpki::DNSNameRef<'_>, - _ocsp: &[u8], - ) -> Result { - Ok(rustls::ServerCertVerified::assertion()) + _end_entity: &Certificate, + _intermediates: &[Certificate], + _server_name: &ServerName, + _scts: &mut dyn Iterator, + _ocsp_response: &[u8], + _now: SystemTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) } } } @@ -73,10 +89,26 @@ async fn test_connection_reuse_h2() { }) .await; - // disable TLS verification - let mut config = ClientConfig::new(); + let mut root_certs = RootCertStore::empty(); + for cert in TLS_SERVER_ROOTS.0 { + let cert = OwnedTrustAnchor::from_subject_spki_name_constraints( + cert.subject, + cert.spki, + cert.name_constraints, + ); + let certs = vec![cert].into_iter(); + root_certs.add_server_trust_anchors(certs); + } + + let mut config = ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(root_certs) + .with_no_client_auth(); + let protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; - config.set_protocols(&protos); + config.alpn_protocols = protos; + + // disable TLS verification config .dangerous() .set_certificate_verifier(Arc::new(danger::NoCertificateVerification)); diff --git a/tests/test_server.rs b/tests/test_server.rs index beb8ff0f5..ff6f5ae5e 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -883,27 +883,31 @@ async fn test_brotli_encoding_large_openssl() { mod plus_rustls { use std::io::BufReader; - use rustls::{ - internal::pemfile::{certs, pkcs8_private_keys}, - NoClientAuth, ServerConfig as RustlsServerConfig, - }; + use rustls::{Certificate, PrivateKey, ServerConfig as RustlsServerConfig}; + use rustls_pemfile::{certs, pkcs8_private_keys}; use super::*; - fn rustls_config() -> RustlsServerConfig { + fn tls_config() -> RustlsServerConfig { let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap(); let cert_file = cert.serialize_pem().unwrap(); let key_file = cert.serialize_private_key_pem(); - let mut config = RustlsServerConfig::new(NoClientAuth::new()); let cert_file = &mut BufReader::new(cert_file.as_bytes()); let key_file = &mut BufReader::new(key_file.as_bytes()); - let cert_chain = certs(cert_file).unwrap(); + let cert_chain = certs(cert_file) + .unwrap() + .into_iter() + .map(Certificate) + .collect(); let mut keys = pkcs8_private_keys(key_file).unwrap(); - config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); - config + RustlsServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(cert_chain, PrivateKey(keys.remove(0))) + .unwrap() } #[actix_rt::test] @@ -914,7 +918,7 @@ mod plus_rustls { .map(char::from) .collect::(); - let srv = actix_test::start_with(actix_test::config().rustls(rustls_config()), || { + let srv = actix_test::start_with(actix_test::config().rustls(tls_config()), || { App::new().service(web::resource("/").route(web::to(|bytes: Bytes| { HttpResponse::Ok() .encoding(actix_web::http::ContentEncoding::Identity) From 37f2bf562559a314b9d2f6846cc359d5ea7cf4dc Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 20 Oct 2021 02:06:51 +0100 Subject: [PATCH 62/88] clippy --- actix-http/src/client/pool.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/actix-http/src/client/pool.rs b/actix-http/src/client/pool.rs index 7c36dcff9..88188038f 100644 --- a/actix-http/src/client/pool.rs +++ b/actix-http/src/client/pool.rs @@ -186,7 +186,6 @@ where let mut conn = None; // check if there is idle connection for given key. - #[allow(clippy::must_not_suspend)] let mut map = inner.available.borrow_mut(); if let Some(conns) = map.get_mut(&key) { From c09ec6af4cb74366108d359357196b0e23b2f94d Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 20 Oct 2021 02:27:30 +0100 Subject: [PATCH 63/88] split off coverage ci job --- .github/workflows/ci.yml | 51 ++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4d713b48..b5e92bb77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,8 +48,7 @@ jobs: - name: Generate Cargo.lock uses: actions-rs/cargo@v1 - with: - command: generate-lockfile + with: { command: generate-lockfile } - name: Cache Dependencies uses: Swatinem/rust-cache@v1.2.0 @@ -82,32 +81,44 @@ jobs: command: ci-test args: --skip=test_reading_deflate_encoding_large_random_rustls - - name: Generate coverage file - if: > - matrix.target.os == 'ubuntu-latest' - && matrix.version == 'stable' - && github.ref == 'refs/heads/master' - run: | - cargo install cargo-tarpaulin --vers "^0.13" - cargo tarpaulin --out Xml --verbose - - name: Upload to Codecov - if: > - matrix.target.os == 'ubuntu-latest' - && matrix.version == 'stable' - && github.ref == 'refs/heads/master' - uses: codecov/codecov-action@v1 - with: - file: cobertura.xml - - name: Clear the cargo caches run: | cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean cargo-cache + coverage: + name: coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: { command: generate-lockfile } + - name: Cache Dependencies + uses: Swatinem/rust-cache@v1.2.0 + + - name: Generate coverage file + if: github.ref == 'refs/heads/master' + run: | + cargo install cargo-tarpaulin --vers "^0.13" + cargo tarpaulin --out Xml --verbose + - name: Upload to Codecov + if: github.ref == 'refs/heads/master' + uses: codecov/codecov-action@v1 + with: { file: cobertura.xml } + + rustdoc: name: rustdoc runs-on: ubuntu-latest - steps: - uses: actions/checkout@v2 From 9abe166d52c8cfae53627a9a683291c8569aaa69 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 20 Oct 2021 22:32:05 +0100 Subject: [PATCH 64/88] actix-web beta 10 releases (#2417) --- .cargo/config.toml | 5 ++ .github/workflows/ci.yml | 35 +++++++++++++- CHANGES.md | 7 ++- Cargo.toml | 18 ++++---- README.md | 4 +- actix-files/Cargo.toml | 8 ++-- actix-http-test/Cargo.toml | 8 ++-- actix-http/CHANGES.md | 3 ++ actix-http/Cargo.toml | 18 ++++---- actix-http/README.md | 4 +- actix-http/src/client/connector.rs | 74 +++++++++++++++++------------- actix-http/tests/test_rustls.rs | 15 +----- actix-multipart/Cargo.toml | 4 +- actix-test/CHANGES.md | 6 +++ actix-test/Cargo.toml | 19 ++++++-- actix-web-actors/Cargo.toml | 8 ++-- actix-web-codegen/CHANGES.md | 3 ++ actix-web-codegen/Cargo.toml | 6 +-- actix-web-codegen/README.md | 4 +- awc/CHANGES.md | 3 ++ awc/Cargo.toml | 16 +++---- awc/README.md | 4 +- awc/tests/test_rustls_client.rs | 15 +----- 23 files changed, 169 insertions(+), 118 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index f417a7053..40a513efd 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -7,3 +7,8 @@ ci-default = "check --workspace --bins --tests --examples" ci-full = "check --workspace --all-features --bins --tests --examples" ci-test = "test --workspace --all-features --lib --tests --no-fail-fast -- --nocapture" ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture" + +ci-feature-powerset-check-no-tls="hack --workspace --feature-powerset --skip=__compress,rustls,openssl check" +ci-feature-powerset-check-rustls="hack --workspace --feature-powerset --features=rustls --skip=__compress,openssl check" +ci-feature-powerset-check-openssl="hack --workspace --feature-powerset --features=openssl --skip=__compress,rustls check" +ci-feature-powerset-check-all="hack --workspace --feature-powerset --skip=__compress check" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5e92bb77..aff0b9348 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: target: - { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu } - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } - - { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc } + - { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc } version: - 1.52.0 # MSRV - stable @@ -32,6 +32,8 @@ jobs: - uses: actions/checkout@v2 # install OpenSSL on Windows + # TODO: GitHub actions docs state that OpenSSL is + # already installed on these Windows machines somewhere - name: Set vcpkg root if: matrix.target.triple == 'x86_64-pc-windows-msvc' run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append @@ -86,6 +88,36 @@ jobs: cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean cargo-cache + ci_feature_powerset_check: + name: Verify Feature Combinations + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: { command: generate-lockfile } + - name: Cache Dependencies + uses: Swatinem/rust-cache@v1.2.0 + + - name: Install cargo-hack + uses: actions-rs/cargo@v1 + with: + command: install + args: cargo-hack + + - name: check feature combinations + # if: github.ref == 'refs/heads/master' + uses: actions-rs/cargo@v1 + with: { command: ci-feature-powerset-check-all } + coverage: name: coverage runs-on: ubuntu-latest @@ -115,7 +147,6 @@ jobs: uses: codecov/codecov-action@v1 with: { file: cobertura.xml } - rustdoc: name: rustdoc runs-on: ubuntu-latest diff --git a/CHANGES.md b/CHANGES.md index cea963ca1..a2b6b14ba 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,15 +1,19 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 4.0.0-beta.10 - 2021-10-20 ### Added * Option to allow `Json` extractor to work without a `Content-Type` header present. [#2362] +* `#[actix_web::test]` macro for setting up tests with a runtime. [#2409] ### Changed * Associated type `FromRequest::Config` was removed. [#2233] * Inner field made private on `web::Payload`. [#2384] * `Data::into_inner` and `Data::get_ref` no longer require T: Sized. [#2403] -* Minimum supported Rust version (MSRV) is now 1.52. * Updated rustls to v0.20. [#2414] +* Minimum supported Rust version (MSRV) is now 1.52. ### Removed * `ServiceResponse::checked_expr` was a legacy and just removed. [#2401] @@ -18,6 +22,7 @@ [#2362]: https://github.com/actix/actix-web/pull/2362 [#2384]: https://github.com/actix/actix-web/pull/2384 [#2401]: https://github.com/actix/actix-web/pull/2401 +[#2409]: https://github.com/actix/actix-web/pull/2409 [#2414]: https://github.com/actix/actix-web/pull/2414 diff --git a/Cargo.toml b/Cargo.toml index e53f4411b..152282207 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "4.0.0-beta.9" +version = "4.0.0-beta.10" authors = ["Nikolay Kim "] description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" keywords = ["actix", "http", "web", "framework", "async"] @@ -11,7 +11,7 @@ categories = [ "web-programming::websocket" ] homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web" +repository = "https://github.com/actix/actix-web.git" license = "MIT OR Apache-2.0" edition = "2018" @@ -61,22 +61,22 @@ openssl = ["actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"] # rustls rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"] -# Internal (PRIVATE!) features used to aid testing and cheking feature status. +# Internal (PRIVATE!) features used to aid testing and checking feature status. # Don't rely on these whatsoever. They may disappear at anytime. __compress = [] [dependencies] actix-codec = "0.4.0" actix-macros = "0.2.3" -actix-router = "0.5.0-beta.2" actix-rt = "2.2" actix-server = "2.0.0-beta.3" actix-service = "2.0.0" actix-utils = "3.0.0" -actix-tls = { version = "3.0.0-beta.6", default-features = false, optional = true } +actix-tls = { version = "3.0.0-beta.7", default-features = false, optional = true } -actix-web-codegen = "0.5.0-beta.4" -actix-http = "3.0.0-beta.10" +actix-http = "3.0.0-beta.11" +actix-router = "0.5.0-beta.2" +actix-web-codegen = "0.5.0-beta.5" ahash = "0.7" bytes = "1" @@ -105,7 +105,7 @@ url = "2.1" [dev-dependencies] actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] } -awc = { version = "3.0.0-beta.8", features = ["openssl"] } +awc = { version = "3.0.0-beta.9", features = ["openssl"] } brotli2 = "0.3.2" criterion = { version = "0.3", features = ["html_reports"] } @@ -117,8 +117,6 @@ rcgen = "0.8" rustls-pemfile = "0.2" tls-openssl = { package = "openssl", version = "0.10.9" } tls-rustls = { package = "rustls", version = "0.20.0" } -webpki = "0.22" -webpki-roots = "0.22" zstd = "0.7" [profile.dev] diff --git a/README.md b/README.md index 00e8fa6ce..25b595361 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@

[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) -[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.9)](https://docs.rs/actix-web/4.0.0-beta.9) +[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.10)](https://docs.rs/actix-web/4.0.0-beta.10) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) -[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.9/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.9) +[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.10/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.10)
[![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index eccf49a77..664978776 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -15,8 +15,8 @@ name = "actix_files" path = "src/lib.rs" [dependencies] -actix-web = { version = "4.0.0-beta.9", default-features = false } -actix-http = "3.0.0-beta.10" +actix-web = { version = "4.0.0-beta.10", default-features = false } +actix-http = "3.0.0-beta.11" actix-service = "2.0.0" actix-utils = "3.0.0" @@ -33,5 +33,5 @@ percent-encoding = "2.1" [dev-dependencies] actix-rt = "2.2" -actix-web = "4.0.0-beta.9" -actix-test = "0.1.0-beta.3" +actix-web = "4.0.0-beta.10" +actix-test = "0.1.0-beta.5" diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 2bdd6969d..d3fc8a47f 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -31,11 +31,11 @@ openssl = ["tls-openssl", "awc/openssl"] [dependencies] actix-service = "2.0.0" actix-codec = "0.4.0" -actix-tls = "3.0.0-beta.6" +actix-tls = "3.0.0-beta.7" actix-utils = "3.0.0" actix-rt = "2.2" actix-server = "2.0.0-beta.3" -awc = { version = "3.0.0-beta.8", default-features = false } +awc = { version = "3.0.0-beta.9", default-features = false } base64 = "0.13" bytes = "1" @@ -50,5 +50,5 @@ serde_urlencoded = "0.7" tls-openssl = { version = "0.10.9", package = "openssl", optional = true } [dev-dependencies] -actix-web = { version = "4.0.0-beta.9", default-features = false, features = ["cookies"] } -actix-http = "3.0.0-beta.10" +actix-web = { version = "4.0.0-beta.10", default-features = false, features = ["cookies"] } +actix-http = "3.0.0-beta.11" diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 3911fc00a..3273847c5 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.11 - 2021-10-20 ### Changed * Updated rustls to v0.20. [#2414] * Minimum supported Rust version (MSRV) is now 1.52. diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index d0724ba5f..3abf537fa 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,14 +1,17 @@ [package] name = "actix-http" -version = "3.0.0-beta.10" +version = "3.0.0-beta.11" authors = ["Nikolay Kim "] description = "HTTP primitives for the Actix ecosystem" keywords = ["actix", "http", "framework", "async", "futures"] homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web" -categories = ["network-programming", "asynchronous", - "web-programming::http-server", - "web-programming::websocket"] +repository = "https://github.com/actix/actix-web.git" +categories = [ + "network-programming", + "asynchronous", + "web-programming::http-server", + "web-programming::websocket", +] license = "MIT OR Apache-2.0" edition = "2018" @@ -46,7 +49,7 @@ actix-service = "2.0.0" actix-codec = "0.4.0" actix-utils = "3.0.0" actix-rt = "2.2" -actix-tls = { version = "3.0.0-beta.6", features = ["accept", "connect"] } +actix-tls = { version = "3.0.0-beta.7", features = ["accept", "connect"] } ahash = "0.7" base64 = "0.13" @@ -85,7 +88,7 @@ trust-dns-resolver = { version = "0.20.0", optional = true } [dev-dependencies] actix-server = "2.0.0-beta.3" actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] } -actix-tls = { version = "3.0.0-beta.6", features = ["openssl"] } +actix-tls = { version = "3.0.0-beta.7", features = ["openssl"] } async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } env_logger = "0.8" @@ -96,7 +99,6 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tls-openssl = { package = "openssl", version = "0.10.9" } tls-rustls = { package = "rustls", version = "0.20.0" } -webpki = { version = "0.22" } [[example]] name = "ws" diff --git a/actix-http/README.md b/actix-http/README.md index 68a6e0a5d..5b1e552fd 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -3,11 +3,11 @@ > HTTP primitives for the Actix ecosystem. [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) -[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.10)](https://docs.rs/actix-http/3.0.0-beta.10) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.11)](https://docs.rs/actix-http/3.0.0-beta.11) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
-[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.10/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.10) +[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.11/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.11) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-http/src/client/connector.rs b/actix-http/src/client/connector.rs index bde5e4853..4314511a3 100644 --- a/actix-http/src/client/connector.rs +++ b/actix-http/src/client/connector.rs @@ -28,18 +28,13 @@ use super::pool::ConnectionPool; use super::Connect; use super::Protocol; -#[cfg(feature = "openssl")] -use actix_tls::connect::ssl::openssl::SslConnector as OpensslConnector; -#[cfg(feature = "rustls")] -use actix_tls::connect::ssl::rustls::ClientConfig; - enum SslConnector { #[allow(dead_code)] None, #[cfg(feature = "openssl")] - Openssl(OpensslConnector), + Openssl(actix_tls::connect::ssl::openssl::SslConnector), #[cfg(feature = "rustls")] - Rustls(std::sync::Arc), + Rustls(std::sync::Arc), } /// Manages HTTP client network connectivity. @@ -78,10 +73,35 @@ impl Connector<()> { } } - // Build Ssl connector with openssl, based on supplied alpn protocols - #[cfg(feature = "openssl")] + /// Provides an empty TLS connector when no TLS feature is enabled. + #[cfg(not(any(feature = "openssl", feature = "rustls")))] + fn build_ssl(_: Vec>) -> SslConnector { + SslConnector::None + } + + /// Build TLS connector with rustls, based on supplied ALPN protocols + /// + /// Note that if both `openssl` and `rustls` features are enabled, rustls will be used. + #[cfg(feature = "rustls")] fn build_ssl(protocols: Vec>) -> SslConnector { - use actix_tls::connect::ssl::openssl::SslMethod; + use actix_tls::connect::tls::rustls::{webpki_roots_cert_store, ClientConfig}; + + let mut config = ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(webpki_roots_cert_store()) + .with_no_client_auth(); + + config.alpn_protocols = protocols; + + SslConnector::Rustls(std::sync::Arc::new(config)) + } + + /// Build TLS connector with openssl, based on supplied ALPN protocols + #[cfg(all(feature = "openssl", not(feature = "rustls")))] + fn build_ssl(protocols: Vec>) -> SslConnector { + use actix_tls::connect::tls::openssl::{ + SslConnector as OpensslConnector, SslMethod, + }; use bytes::{BufMut, BytesMut}; let mut alpn = BytesMut::with_capacity(20); @@ -91,28 +111,12 @@ impl Connector<()> { } let mut ssl = OpensslConnector::builder(SslMethod::tls()).unwrap(); - let _ = ssl - .set_alpn_protos(&alpn) - .map_err(|e| error!("Can not set alpn protocol: {:?}", e)); + if let Err(err) = ssl.set_alpn_protos(&alpn) { + error!("Can not set ALPN protocol: {:?}", err); + } + SslConnector::Openssl(ssl.build()) } - - // Build Ssl connector with rustls, based on supplied alpn protocols - #[cfg(all(not(feature = "openssl"), feature = "rustls"))] - fn build_ssl(protocols: Vec>) -> SslConnector { - let mut config = ClientConfig::new(); - config.set_protocols(&protocols); - config.root_store.add_server_trust_anchors( - &actix_tls::connect::ssl::rustls::TLS_SERVER_ROOTS, - ); - SslConnector::Rustls(std::sync::Arc::new(config)) - } - - // ssl turned off, provides empty ssl connector - #[cfg(not(any(feature = "openssl", feature = "rustls")))] - fn build_ssl(_: Vec>) -> SslConnector { - SslConnector::None - } } impl Connector { @@ -167,14 +171,20 @@ where #[cfg(feature = "openssl")] /// Use custom `SslConnector` instance. - pub fn ssl(mut self, connector: OpensslConnector) -> Self { + pub fn ssl( + mut self, + connector: actix_tls::connect::ssl::openssl::SslConnector, + ) -> Self { self.ssl = SslConnector::Openssl(connector); self } #[cfg(feature = "rustls")] /// Use custom `SslConnector` instance. - pub fn rustls(mut self, connector: std::sync::Arc) -> Self { + pub fn rustls( + mut self, + connector: std::sync::Arc, + ) -> Self { self.ssl = SslConnector::Rustls(connector); self } diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index 69c7db74d..924ef49ad 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -20,7 +20,7 @@ use actix_http::{ }; use actix_http_test::test_server; use actix_service::{fn_factory_with_config, fn_service}; -use actix_tls::connect::ssl::rustls::TLS_SERVER_ROOTS; +use actix_tls::connect::tls::rustls::webpki_roots_cert_store; use actix_utils::future::{err, ok}; use bytes::{Bytes, BytesMut}; use derive_more::{Display, Error}; @@ -74,20 +74,9 @@ pub fn get_negotiated_alpn_protocol( addr: SocketAddr, client_alpn_protocol: &[u8], ) -> Option> { - let mut root_certs = RootCertStore::empty(); - for cert in TLS_SERVER_ROOTS.0 { - let cert = OwnedTrustAnchor::from_subject_spki_name_constraints( - cert.subject, - cert.spki, - cert.name_constraints, - ); - let certs = vec![cert].into_iter(); - root_certs.add_server_trust_anchors(certs); - } - let mut config = rustls::ClientConfig::builder() .with_safe_defaults() - .with_root_certificates(root_certs) + .with_root_certificates(webpki_roots_cert_store()) .with_no_client_auth(); config.alpn_protocols.push(client_alpn_protocol.to_vec()); diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 6db81cca9..f2b1d08a4 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -14,7 +14,7 @@ name = "actix_multipart" path = "src/lib.rs" [dependencies] -actix-web = { version = "4.0.0-beta.9", default-features = false } +actix-web = { version = "4.0.0-beta.10", default-features = false } actix-utils = "3.0.0" bytes = "1" @@ -29,6 +29,6 @@ twoway = "0.2" [dev-dependencies] actix-rt = "2.2" -actix-http = "3.0.0-beta.10" +actix-http = "3.0.0-beta.11" tokio = { version = "1", features = ["sync"] } tokio-stream = "0.1" diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index 9c0a9ee81..070892581 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -1,8 +1,14 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.1.0-beta.5 - 2021-10-20 +* Updated rustls to v0.20. [#2414] * Minimum supported Rust version (MSRV) is now 1.52. +[#2414]: https://github.com/actix/actix-web/pull/2414 + ## 0.1.0-beta.4 - 2021-09-09 * Minimum supported Rust version (MSRV) is now 1.51. diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index 62a27e5a5..ede72f219 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -1,13 +1,22 @@ [package] name = "actix-test" -version = "0.1.0-beta.4" +version = "0.1.0-beta.5" authors = [ "Nikolay Kim ", "Rob Ede ", ] -edition = "2018" description = "Integration testing tools for Actix Web applications" +keywords = ["http", "web", "framework", "async", "futures"] +homepage = "https://actix.rs" +repository = "https://github.com/actix/actix-web.git" +categories = [ + "network-programming", + "asynchronous", + "web-programming::http-server", + "web-programming::websocket", +] license = "MIT OR Apache-2.0" +edition = "2018" [features] default = [] @@ -20,13 +29,13 @@ openssl = ["tls-openssl", "actix-http/openssl"] [dependencies] actix-codec = "0.4.0" -actix-http = "3.0.0-beta.10" +actix-http = "3.0.0-beta.11" actix-http-test = "3.0.0-beta.5" actix-service = "2.0.0" actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.9", default-features = false, features = ["cookies"] } +actix-web = { version = "4.0.0-beta.10", default-features = false, features = ["cookies"] } actix-rt = "2.1" -awc = { version = "3.0.0-beta.8", default-features = false, features = ["cookies"] } +awc = { version = "3.0.0-beta.9", default-features = false, features = ["cookies"] } futures-core = { version = "0.3.7", default-features = false, features = ["std"] } futures-util = { version = "0.3.7", default-features = false, features = [] } diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index ef6bd919d..2d987a131 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -16,8 +16,8 @@ path = "src/lib.rs" [dependencies] actix = { version = "0.12.0", default-features = false } actix-codec = "0.4.0" -actix-http = "3.0.0-beta.10" -actix-web = { version = "4.0.0-beta.9", default-features = false } +actix-http = "3.0.0-beta.11" +actix-web = { version = "4.0.0-beta.10", default-features = false } bytes = "1" bytestring = "1" @@ -27,8 +27,8 @@ tokio = { version = "1", features = ["sync"] } [dev-dependencies] actix-rt = "2.2" -actix-test = "0.1.0-beta.3" +actix-test = "0.1.0-beta.5" -awc = { version = "3.0.0-beta.8", default-features = false } +awc = { version = "3.0.0-beta.9", default-features = false } env_logger = "0.8" futures-util = { version = "0.3.7", default-features = false } diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index f1f050b2c..3811ef030 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.5.0-beta.5 - 2021-10-20 * Improve error recovery potential when macro input is invalid. [#2410] * Add `#[actix_web::test]` macro for setting up tests with a runtime. [#2409] * Minimum supported Rust version (MSRV) is now 1.52. diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index afedafdfd..c04ca435a 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-codegen" -version = "0.5.0-beta.4" +version = "0.5.0-beta.5" description = "Routing and runtime macros for Actix Web" homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" @@ -23,9 +23,9 @@ actix-router = "0.5.0-beta.2" [dev-dependencies] actix-rt = "2.2" actix-macros = "0.2.3" -actix-test = "0.1.0-beta.3" +actix-test = "0.1.0-beta.5" actix-utils = "3.0.0" -actix-web = "4.0.0-beta.9" +actix-web = "4.0.0-beta.10" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } trybuild = "1" diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index ee552cfb5..2ffd5b31c 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -3,11 +3,11 @@ > Routing and runtime macros for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) -[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.4)](https://docs.rs/actix-web-codegen/0.5.0-beta.4) +[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.5)](https://docs.rs/actix-web-codegen/0.5.0-beta.5) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
-[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.4/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.4) +[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.5/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.5) [![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 49d88e5e8..5682a237c 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.9 - 2021-10-20 * Updated rustls to v0.20. [#2414] [#2414]: https://github.com/actix/actix-web/pull/2414 diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 967d49602..5a8235336 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "3.0.0-beta.8" +version = "3.0.0-beta.9" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", @@ -14,7 +14,7 @@ categories = [ "web-programming::websocket", ] homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web" +repository = "https://github.com/actix/actix-web.git" license = "MIT OR Apache-2.0" edition = "2018" @@ -55,7 +55,7 @@ __compress = [] [dependencies] actix-codec = "0.4.0" actix-service = "2.0.0" -actix-http = "3.0.0-beta.10" +actix-http = "3.0.0-beta.11" actix-rt = { version = "2.1", default-features = false } base64 = "0.13" @@ -77,13 +77,13 @@ tls-openssl = { package = "openssl", version = "0.10.9", optional = true } tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features = ["dangerous_configuration"] } [dev-dependencies] -actix-web = { version = "4.0.0-beta.9", features = ["openssl"] } -actix-http = { version = "3.0.0-beta.10", features = ["openssl"] } +actix-web = { version = "4.0.0-beta.10", features = ["openssl"] } +actix-http = { version = "3.0.0-beta.11", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] } actix-utils = "3.0.0" actix-server = "2.0.0-beta.3" -actix-tls = { version = "3.0.0-beta.6", features = ["openssl", "rustls"] } -actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] } +actix-tls = { version = "3.0.0-beta.7", features = ["openssl", "rustls"] } +actix-test = { version = "0.1.0-beta.5", features = ["openssl", "rustls"] } brotli2 = "0.3.2" env_logger = "0.8" @@ -91,8 +91,6 @@ flate2 = "1.0.13" futures-util = { version = "0.3.7", default-features = false } rcgen = "0.8" rustls-pemfile = "0.2" -webpki = "0.22" -webpki-roots = "0.22" [[example]] name = "client" diff --git a/awc/README.md b/awc/README.md index 38c967e69..67bcb9659 100644 --- a/awc/README.md +++ b/awc/README.md @@ -3,9 +3,9 @@ > Async HTTP and WebSocket client library. [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) -[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.8)](https://docs.rs/awc/3.0.0-beta.8) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.9)](https://docs.rs/awc/3.0.0-beta.9) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.8/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.8) +[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.9/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.9) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources diff --git a/awc/tests/test_rustls_client.rs b/awc/tests/test_rustls_client.rs index 95f2d0616..c075a6090 100644 --- a/awc/tests/test_rustls_client.rs +++ b/awc/tests/test_rustls_client.rs @@ -14,6 +14,7 @@ use std::{ use actix_http::HttpService; use actix_http_test::test_server; use actix_service::{fn_service, map_config, ServiceFactoryExt}; +use actix_tls::connect::tls::rustls::webpki_roots_cert_store; use actix_utils::future::ok; use actix_web::{dev::AppConfig, http::Version, web, App, HttpResponse}; use rustls::{ @@ -22,7 +23,6 @@ use rustls::{ ServerName, }; use rustls_pemfile::{certs, pkcs8_private_keys}; -use webpki_roots::TLS_SERVER_ROOTS; fn tls_config() -> ServerConfig { let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap(); @@ -89,20 +89,9 @@ async fn test_connection_reuse_h2() { }) .await; - let mut root_certs = RootCertStore::empty(); - for cert in TLS_SERVER_ROOTS.0 { - let cert = OwnedTrustAnchor::from_subject_spki_name_constraints( - cert.subject, - cert.spki, - cert.name_constraints, - ); - let certs = vec![cert].into_iter(); - root_certs.add_server_trust_anchors(certs); - } - let mut config = ClientConfig::builder() .with_safe_defaults() - .with_root_certificates(root_certs) + .with_root_certificates(webpki_roots_cert_store()) .with_no_client_auth(); let protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; From 4af414064baa2debc963ee00a01e2019a859cf3a Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 20 Oct 2021 23:31:46 +0100 Subject: [PATCH 65/88] prepare actix-multipart release 0.4.0-beta.7 --- CHANGES.md | 3 ++- actix-multipart/CHANGES.md | 3 +++ actix-multipart/Cargo.toml | 2 +- actix-multipart/README.md | 4 ++-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a2b6b14ba..9d7b3180d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,12 +16,13 @@ * Minimum supported Rust version (MSRV) is now 1.52. ### Removed -* `ServiceResponse::checked_expr` was a legacy and just removed. [#2401] +* Useless `ServiceResponse::checked_expr` method. [#2401] [#2233]: https://github.com/actix/actix-web/pull/2233 [#2362]: https://github.com/actix/actix-web/pull/2362 [#2384]: https://github.com/actix/actix-web/pull/2384 [#2401]: https://github.com/actix/actix-web/pull/2401 +[#2403]: https://github.com/actix/actix-web/pull/2403 [#2409]: https://github.com/actix/actix-web/pull/2409 [#2414]: https://github.com/actix/actix-web/pull/2414 diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 33da6a202..09cc707be 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.4.0-beta.7 - 2021-10-20 * Minimum supported Rust version (MSRV) is now 1.52. diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index f2b1d08a4..92637cef9 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-multipart" -version = "0.4.0-beta.6" +version = "0.4.0-beta.7" authors = ["Nikolay Kim "] description = "Multipart form support for Actix Web" keywords = ["http", "web", "framework", "async", "futures"] diff --git a/actix-multipart/README.md b/actix-multipart/README.md index 254ef877b..674814294 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -3,11 +3,11 @@ > Multipart form support for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) -[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.6)](https://docs.rs/actix-multipart/0.4.0-beta.6) +[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.7)](https://docs.rs/actix-multipart/0.4.0-beta.7) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
-[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.6/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.6) +[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.7/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.7) [![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) From c79b9a0df3ba7690271f0efd5f5f35a97813480b Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 20 Oct 2021 23:32:46 +0100 Subject: [PATCH 66/88] prepare actix-files release 0.6.0-beta.8 --- actix-files/CHANGES.md | 3 +++ actix-files/Cargo.toml | 2 +- actix-files/README.md | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 8e0a3eecf..e1a2c90c5 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.6.0-beta.8 - 2021-10-20 * Minimum supported Rust version (MSRV) is now 1.52. diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 664978776..3d7340607 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.6.0-beta.7" +version = "0.6.0-beta.8" authors = ["Nikolay Kim "] description = "Static file serving for Actix Web" keywords = ["actix", "http", "async", "futures"] diff --git a/actix-files/README.md b/actix-files/README.md index ed15e3333..eac7339ab 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -3,11 +3,11 @@ > Static file serving for Actix Web [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) -[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.7)](https://docs.rs/actix-files/0.6.0-beta.7) +[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.8)](https://docs.rs/actix-files/0.6.0-beta.8) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![License](https://img.shields.io/crates/l/actix-files.svg)
-[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.7/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.7) +[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.8/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.8) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) From d40b6748bc84a98483657a9422493bc16db760aa Mon Sep 17 00:00:00 2001 From: fakeshadow <24548779@qq.com> Date: Fri, 22 Oct 2021 07:22:58 +0800 Subject: [PATCH 67/88] remove dead dep (#2420) --- actix-http/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 3abf537fa..a8fc4255f 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -67,7 +67,6 @@ httpdate = "1.0.1" itoa = "0.4" language-tags = "0.3" local-channel = "0.1" -once_cell = "1.5" log = "0.4" mime = "0.3" percent-encoding = "2.1" From d13854505feae2fc1c2435563e209b991bdb4399 Mon Sep 17 00:00:00 2001 From: fakeshadow <24548779@qq.com> Date: Tue, 26 Oct 2021 07:37:40 +0800 Subject: [PATCH 68/88] move actix_http::client module to awc (#2425) --- actix-http/CHANGES.md | 5 + actix-http/Cargo.toml | 7 +- actix-http/src/lib.rs | 1 - actix-test/Cargo.toml | 4 +- awc/Cargo.toml | 19 +- awc/src/builder.rs | 6 +- {actix-http => awc}/src/client/config.rs | 3 +- {actix-http => awc}/src/client/connection.rs | 50 ++--- {actix-http => awc}/src/client/connector.rs | 181 +++++++++---------- {actix-http => awc}/src/client/error.rs | 7 +- {actix-http => awc}/src/client/h1proto.rs | 35 ++-- {actix-http => awc}/src/client/h2proto.rs | 15 +- {actix-http => awc}/src/client/mod.rs | 1 - {actix-http => awc}/src/client/pool.rs | 46 ++--- awc/src/connect.rs | 10 +- awc/src/error.rs | 14 +- awc/src/lib.rs | 32 ++-- awc/src/middleware/redirect.rs | 2 +- 18 files changed, 197 insertions(+), 241 deletions(-) rename {actix-http => awc}/src/client/config.rs (96%) rename {actix-http => awc}/src/client/connection.rs (89%) rename {actix-http => awc}/src/client/connector.rs (88%) rename {actix-http => awc}/src/client/error.rs (98%) rename {actix-http => awc}/src/client/h1proto.rs (92%) rename {actix-http => awc}/src/client/h2proto.rs (96%) rename {actix-http => awc}/src/client/mod.rs (95%) rename {actix-http => awc}/src/client/pool.rs (95%) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 3273847c5..81595c92d 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,11 @@ # Changes ## Unreleased - 2021-xx-xx +### Removed +* `client` module. [#2425] +* `trust-dns` feature. [#2425] + +[#2425]: https://github.com/actix/actix-web/pull/2425 ## 3.0.0-beta.11 - 2021-10-20 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index a8fc4255f..3d45cc8ce 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -37,9 +37,6 @@ compress-brotli = ["brotli2", "__compress"] compress-gzip = ["flate2", "__compress"] compress-zstd = ["zstd", "__compress"] -# trust-dns as client dns resolver -trust-dns = ["trust-dns-resolver"] - # Internal (PRIVATE!) features used to aid testing and cheking feature status. # Don't rely on these whatsoever. They may disappear at anytime. __compress = [] @@ -49,7 +46,7 @@ actix-service = "2.0.0" actix-codec = "0.4.0" actix-utils = "3.0.0" actix-rt = "2.2" -actix-tls = { version = "3.0.0-beta.7", features = ["accept", "connect"] } +actix-tls = { version = "3.0.0-beta.7", features = ["accept"] } ahash = "0.7" base64 = "0.13" @@ -82,8 +79,6 @@ brotli2 = { version="0.3.2", optional = true } flate2 = { version = "1.0.13", optional = true } zstd = { version = "0.7", optional = true } -trust-dns-resolver = { version = "0.20.0", optional = true } - [dev-dependencies] actix-server = "2.0.0-beta.3" actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] } diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index 42ce4ffe4..bfb6b8c55 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -29,7 +29,6 @@ extern crate log; pub mod body; mod builder; -pub mod client; mod config; #[cfg(feature = "__compress")] diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index ede72f219..002e7662e 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -22,10 +22,10 @@ edition = "2018" default = [] # rustls -rustls = ["tls-rustls", "actix-http/rustls"] +rustls = ["tls-rustls", "actix-http/rustls", "awc/rustls"] # openssl -openssl = ["tls-openssl", "actix-http/openssl"] +openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"] [dependencies] actix-codec = "0.4.0" diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 5a8235336..6eeb9ce51 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -30,10 +30,10 @@ features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-z default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] # openssl -openssl = ["tls-openssl", "actix-http/openssl"] +openssl = ["tls-openssl", "actix-tls/openssl"] # rustls -rustls = ["tls-rustls", "actix-http/rustls"] +rustls = ["tls-rustls", "actix-tls/rustls"] # Brotli algorithm content-encoding support compress-brotli = ["actix-http/compress-brotli", "__compress"] @@ -46,7 +46,7 @@ compress-zstd = ["actix-http/compress-zstd", "__compress"] cookies = ["cookie"] # trust-dns as dns resolver -trust-dns = ["actix-http/trust-dns"] +trust-dns = ["trust-dns-resolver"] # Internal (PRIVATE!) features used to aid testing and cheking feature status. # Don't rely on these whatsoever. They may disappear at anytime. @@ -57,13 +57,18 @@ actix-codec = "0.4.0" actix-service = "2.0.0" actix-http = "3.0.0-beta.11" actix-rt = { version = "2.1", default-features = false } +actix-tls = { version = "3.0.0-beta.7", features = ["connect"] } +actix-utils = "3.0.0" +ahash = "0.7" base64 = "0.13" bytes = "1" cfg-if = "1" -cookie = { version = "0.15", features = ["percent-encode"], optional = true } derive_more = "0.99.5" futures-core = { version = "0.3.7", default-features = false } +futures-util = { version = "0.3.7", default-features = false } +h2 = "0.3" +http = "0.2" itoa = "0.4" log =" 0.4" mime = "0.3" @@ -73,9 +78,15 @@ rand = "0.8" serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.7" +tokio = { version = "1", features = ["sync"] } + +cookie = { version = "0.15", features = ["percent-encode"], optional = true } + tls-openssl = { package = "openssl", version = "0.10.9", optional = true } tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features = ["dangerous_configuration"] } +trust-dns-resolver = { version = "0.20.0", optional = true } + [dev-dependencies] actix-web = { version = "4.0.0-beta.10", features = ["openssl"] } actix-http = { version = "3.0.0-beta.11", features = ["openssl"] } diff --git a/awc/src/builder.rs b/awc/src/builder.rs index c594b4836..11ececa70 100644 --- a/awc/src/builder.rs +++ b/awc/src/builder.rs @@ -4,13 +4,11 @@ use std::net::IpAddr; use std::rc::Rc; use std::time::Duration; -use actix_http::{ - client::{Connector, ConnectorService, TcpConnect, TcpConnectError, TcpConnection}, - http::{self, header, Error as HttpError, HeaderMap, HeaderName, Uri}, -}; +use actix_http::http::{self, header, Error as HttpError, HeaderMap, HeaderName, Uri}; use actix_rt::net::{ActixStream, TcpStream}; use actix_service::{boxed, Service}; +use crate::client::{Connector, ConnectorService, TcpConnect, TcpConnectError, TcpConnection}; use crate::connect::DefaultConnector; use crate::error::SendRequestError; use crate::middleware::{NestTransform, Redirect, Transform}; diff --git a/actix-http/src/client/config.rs b/awc/src/client/config.rs similarity index 96% rename from actix-http/src/client/config.rs rename to awc/src/client/config.rs index 1c0405cbc..530c1e03b 100644 --- a/actix-http/src/client/config.rs +++ b/awc/src/client/config.rs @@ -1,5 +1,4 @@ -use std::net::IpAddr; -use std::time::Duration; +use std::{net::IpAddr, time::Duration}; const DEFAULT_H2_CONN_WINDOW: u32 = 1024 * 1024 * 2; // 2MB const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1MB diff --git a/actix-http/src/client/connection.rs b/awc/src/client/connection.rs similarity index 89% rename from actix-http/src/client/connection.rs rename to awc/src/client/connection.rs index a30f651ca..97b96fc0a 100644 --- a/actix-http/src/client/connection.rs +++ b/awc/src/client/connection.rs @@ -12,10 +12,9 @@ use bytes::Bytes; use futures_core::future::LocalBoxFuture; use h2::client::SendRequest; -use crate::h1::ClientCodec; -use crate::message::{RequestHeadType, ResponseHead}; -use crate::payload::Payload; -use crate::{body::MessageBody, Error}; +use actix_http::{ + body::MessageBody, h1::ClientCodec, Error, Payload, RequestHeadType, ResponseHead, +}; use super::error::SendRequestError; use super::pool::Acquired; @@ -219,11 +218,7 @@ impl ConnectionType { } } - pub(super) fn from_h1( - io: Io, - created: time::Instant, - acquired: Acquired, - ) -> Self { + pub(super) fn from_h1(io: Io, created: time::Instant, acquired: Acquired) -> Self { Self::H1(H1Connection { io: Some(io), created, @@ -271,9 +266,7 @@ where Connection::Tls(ConnectionType::H2(conn)) => { h2proto::send_request(conn, head.into(), body).await } - _ => unreachable!( - "Plain Tcp connection can be used only in Http1 protocol" - ), + _ => unreachable!("Plain Tcp connection can be used only in Http1 protocol"), } }) } @@ -301,9 +294,7 @@ where Err(SendRequestError::TunnelNotSupported) } Connection::Tcp(ConnectionType::H2(_)) => { - unreachable!( - "Plain Tcp connection can be used only in Http1 protocol" - ) + unreachable!("Plain Tcp connection can be used only in Http1 protocol") } } }) @@ -321,12 +312,8 @@ where buf: &mut ReadBuf<'_>, ) -> Poll> { match self.get_mut() { - Connection::Tcp(ConnectionType::H1(conn)) => { - Pin::new(conn).poll_read(cx, buf) - } - Connection::Tls(ConnectionType::H1(conn)) => { - Pin::new(conn).poll_read(cx, buf) - } + Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_read(cx, buf), + Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_read(cx, buf), _ => unreachable!("H2Connection can not impl AsyncRead trait"), } } @@ -345,12 +332,8 @@ where buf: &[u8], ) -> Poll> { match self.get_mut() { - Connection::Tcp(ConnectionType::H1(conn)) => { - Pin::new(conn).poll_write(cx, buf) - } - Connection::Tls(ConnectionType::H1(conn)) => { - Pin::new(conn).poll_write(cx, buf) - } + Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_write(cx, buf), + Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_write(cx, buf), _ => unreachable!(H2_UNREACHABLE_WRITE), } } @@ -363,17 +346,10 @@ where } } - fn poll_shutdown( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { match self.get_mut() { - Connection::Tcp(ConnectionType::H1(conn)) => { - Pin::new(conn).poll_shutdown(cx) - } - Connection::Tls(ConnectionType::H1(conn)) => { - Pin::new(conn).poll_shutdown(cx) - } + Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_shutdown(cx), + Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_shutdown(cx), _ => unreachable!(H2_UNREACHABLE_WRITE), } } diff --git a/actix-http/src/client/connector.rs b/awc/src/client/connector.rs similarity index 88% rename from actix-http/src/client/connector.rs rename to awc/src/client/connector.rs index 4314511a3..8a162c4f8 100644 --- a/actix-http/src/client/connector.rs +++ b/awc/src/client/connector.rs @@ -8,6 +8,7 @@ use std::{ time::Duration, }; +use actix_http::Protocol; use actix_rt::{ net::{ActixStream, TcpStream}, time::{sleep, Sleep}, @@ -19,14 +20,13 @@ use actix_tls::connect::{ }; use futures_core::{future::LocalBoxFuture, ready}; use http::Uri; -use pin_project::pin_project; +use pin_project_lite::pin_project; use super::config::ConnectorConfig; use super::connection::{Connection, ConnectionIo}; use super::error::ConnectError; use super::pool::ConnectionPool; use super::Connect; -use super::Protocol; enum SslConnector { #[allow(dead_code)] @@ -99,9 +99,7 @@ impl Connector<()> { /// Build TLS connector with openssl, based on supplied ALPN protocols #[cfg(all(feature = "openssl", not(feature = "rustls")))] fn build_ssl(protocols: Vec>) -> SslConnector { - use actix_tls::connect::tls::openssl::{ - SslConnector as OpensslConnector, SslMethod, - }; + use actix_tls::connect::tls::openssl::{SslConnector as OpensslConnector, SslMethod}; use bytes::{BufMut, BytesMut}; let mut alpn = BytesMut::with_capacity(20); @@ -112,7 +110,7 @@ impl Connector<()> { let mut ssl = OpensslConnector::builder(SslMethod::tls()).unwrap(); if let Err(err) = ssl.set_alpn_protos(&alpn) { - error!("Can not set ALPN protocol: {:?}", err); + log::error!("Can not set ALPN protocol: {:?}", err); } SslConnector::Openssl(ssl.build()) @@ -148,11 +146,8 @@ where // This remap is to hide ActixStream's trait methods. They are not meant to be called // from user code. Io: ActixStream + fmt::Debug + 'static, - S: Service< - TcpConnect, - Response = TcpConnection, - Error = TcpConnectError, - > + Clone + S: Service, Response = TcpConnection, Error = TcpConnectError> + + Clone + 'static, { /// Tcp connection timeout, i.e. max time to connect to remote host including dns name @@ -171,10 +166,7 @@ where #[cfg(feature = "openssl")] /// Use custom `SslConnector` instance. - pub fn ssl( - mut self, - connector: actix_tls::connect::ssl::openssl::SslConnector, - ) -> Self { + pub fn ssl(mut self, connector: actix_tls::connect::ssl::openssl::SslConnector) -> Self { self.ssl = SslConnector::Openssl(connector); self } @@ -328,10 +320,11 @@ where impl IntoConnectionIo for TcpConnection> { fn into_connection_io(self) -> (Box, Protocol) { let sock = self.into_parts().0; - let h2 = - sock.get_ref().1.alpn_protocol().map_or(false, |protos| { - protos.windows(2).any(|w| w == H2) - }); + let h2 = sock + .get_ref() + .1 + .alpn_protocol() + .map_or(false, |protos| protos.windows(2).any(|w| w == H2)); if h2 { (Box::new(sock), Protocol::Http2) } else { @@ -357,8 +350,8 @@ where let tcp_pool = ConnectionPool::new(tcp_service, tcp_config); let tls_config = self.config; - let tls_pool = tls_service - .map(move |tls_service| ConnectionPool::new(tls_service, tls_config)); + let tls_pool = + tls_service.map(move |tls_service| ConnectionPool::new(tls_service, tls_config)); ConnectorServicePriv { tcp_pool, tls_pool } } @@ -389,10 +382,12 @@ where } } -#[pin_project] -pub struct TcpConnectorFuture { - #[pin] - fut: Fut, +pin_project! { + #[project = TcpConnectorFutureProj] + pub struct TcpConnectorFuture { + #[pin] + fut: Fut, + } } impl Future for TcpConnectorFuture @@ -451,23 +446,25 @@ where } } -#[pin_project(project = TlsConnectorProj)] -#[allow(clippy::large_enum_variant)] -enum TlsConnectorFuture { - TcpConnect { - #[pin] - fut: Fut1, - tls_service: Option, - timeout: Duration, - }, - TlsConnect { - #[pin] - fut: Fut2, - #[pin] - timeout: Sleep, - }, -} +pin_project! { + #[project = TlsConnectorProj] + #[allow(clippy::large_enum_variant)] + enum TlsConnectorFuture { + TcpConnect { + #[pin] + fut: Fut1, + tls_service: Option, + timeout: Duration, + }, + TlsConnect { + #[pin] + fut: Fut2, + #[pin] + timeout: Sleep, + }, + } +} /// helper trait for generic over different TlsStream types between tls crates. trait IntoConnectionIo { fn into_connection_io(self) -> (Box, Protocol); @@ -475,12 +472,7 @@ trait IntoConnectionIo { impl Future for TlsConnectorFuture where - S: Service< - TcpConnection, - Response = Res, - Error = std::io::Error, - Future = Fut2, - >, + S: Service, Response = Res, Error = std::io::Error, Future = Fut2>, S::Response: IntoConnectionIo, Fut1: Future, ConnectError>>, Fut2: Future>, @@ -522,11 +514,7 @@ pub struct TcpConnectorInnerService { } impl TcpConnectorInnerService { - fn new( - service: S, - timeout: Duration, - local_address: Option, - ) -> Self { + fn new(service: S, timeout: Duration, local_address: Option) -> Self { Self { service, timeout, @@ -537,11 +525,8 @@ impl TcpConnectorInnerService { impl Service for TcpConnectorInnerService where - S: Service< - TcpConnect, - Response = TcpConnection, - Error = TcpConnectError, - > + Clone + S: Service, Response = TcpConnection, Error = TcpConnectError> + + Clone + 'static, { type Response = S::Response; @@ -564,12 +549,14 @@ where } } -#[pin_project] -pub struct TcpConnectorInnerFuture { - #[pin] - fut: Fut, - #[pin] - timeout: Sleep, +pin_project! { + #[project = TcpConnectorInnerFutureProj] + pub struct TcpConnectorInnerFuture { + #[pin] + fut: Fut, + #[pin] + timeout: Sleep, + } } impl Future for TcpConnectorInnerFuture @@ -618,12 +605,8 @@ where impl Service for ConnectorServicePriv where - S1: Service - + Clone - + 'static, - S2: Service - + Clone - + 'static, + S1: Service + Clone + 'static, + S2: Service + Clone + 'static, Io1: ConnectionIo, Io2: ConnectionIo, { @@ -643,38 +626,46 @@ where match req.uri.scheme_str() { Some("https") | Some("wss") => match self.tls_pool { None => ConnectorServiceFuture::SslIsNotSupported, - Some(ref pool) => ConnectorServiceFuture::Tls(pool.call(req)), + Some(ref pool) => ConnectorServiceFuture::Tls { + fut: pool.call(req), + }, + }, + _ => ConnectorServiceFuture::Tcp { + fut: self.tcp_pool.call(req), }, - _ => ConnectorServiceFuture::Tcp(self.tcp_pool.call(req)), } } } -#[pin_project(project = ConnectorServiceProj)] -pub enum ConnectorServiceFuture -where - S1: Service - + Clone - + 'static, - S2: Service - + Clone - + 'static, - Io1: ConnectionIo, - Io2: ConnectionIo, -{ - Tcp(#[pin] as Service>::Future), - Tls(#[pin] as Service>::Future), - SslIsNotSupported, +pin_project! { + #[project = ConnectorServiceFutureProj] + pub enum ConnectorServiceFuture + where + S1: Service, + S1: Clone, + S1: 'static, + S2: Service, + S2: Clone, + S2: 'static, + Io1: ConnectionIo, + Io2: ConnectionIo, + { + Tcp { + #[pin] + fut: as Service>::Future + }, + Tls { + #[pin] + fut: as Service>::Future + }, + SslIsNotSupported + } } impl Future for ConnectorServiceFuture where - S1: Service - + Clone - + 'static, - S2: Service - + Clone - + 'static, + S1: Service + Clone + 'static, + S2: Service + Clone + 'static, Io1: ConnectionIo, Io2: ConnectionIo, { @@ -682,9 +673,9 @@ where fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { match self.project() { - ConnectorServiceProj::Tcp(fut) => fut.poll(cx).map_ok(Connection::Tcp), - ConnectorServiceProj::Tls(fut) => fut.poll(cx).map_ok(Connection::Tls), - ConnectorServiceProj::SslIsNotSupported => { + ConnectorServiceFutureProj::Tcp { fut } => fut.poll(cx).map_ok(Connection::Tcp), + ConnectorServiceFutureProj::Tls { fut } => fut.poll(cx).map_ok(Connection::Tls), + ConnectorServiceFutureProj::SslIsNotSupported => { Poll::Ready(Err(ConnectError::SslIsNotSupported)) } } diff --git a/actix-http/src/client/error.rs b/awc/src/client/error.rs similarity index 98% rename from actix-http/src/client/error.rs rename to awc/src/client/error.rs index 34833503b..0f3b1fdea 100644 --- a/actix-http/src/client/error.rs +++ b/awc/src/client/error.rs @@ -2,12 +2,13 @@ use std::{error::Error as StdError, fmt, io}; use derive_more::{Display, From}; +use actix_http::{ + error::{Error, ParseError}, + http::Error as HttpError, +}; #[cfg(feature = "openssl")] use actix_tls::accept::openssl::SslError; -use crate::error::{Error, ParseError}; -use crate::http::Error as HttpError; - /// A set of errors that can occur while connecting to an HTTP host #[derive(Debug, Display, From)] #[non_exhaustive] diff --git a/actix-http/src/client/h1proto.rs b/awc/src/client/h1proto.rs similarity index 92% rename from actix-http/src/client/h1proto.rs rename to awc/src/client/h1proto.rs index 65a30748c..3c2bb7cc1 100644 --- a/actix-http/src/client/h1proto.rs +++ b/awc/src/client/h1proto.rs @@ -5,24 +5,25 @@ use std::{ }; use actix_codec::Framed; +use actix_http::{ + body::{BodySize, MessageBody}, + error::PayloadError, + h1, + http::{ + header::{HeaderMap, IntoHeaderValue, EXPECT, HOST}, + StatusCode, + }, + Error, Payload, RequestHeadType, ResponseHead, +}; use actix_utils::future::poll_fn; use bytes::buf::BufMut; use bytes::{Bytes, BytesMut}; use futures_core::{ready, Stream}; use futures_util::SinkExt as _; - -use crate::h1; -use crate::http::{ - header::{HeaderMap, IntoHeaderValue, EXPECT, HOST}, - StatusCode, -}; -use crate::message::{RequestHeadType, ResponseHead}; -use crate::payload::Payload; -use crate::{error::PayloadError, Error}; +use pin_project_lite::pin_project; use super::connection::{ConnectionIo, H1Connection}; use super::error::{ConnectError, SendRequestError}; -use crate::body::{BodySize, MessageBody}; pub(crate) async fn send_request( io: H1Connection, @@ -194,10 +195,11 @@ where Ok(()) } -#[pin_project::pin_project] -pub(crate) struct PlStream { - #[pin] - framed: Framed, h1::ClientPayloadCodec>, +pin_project! { + pub(crate) struct PlStream { + #[pin] + framed: Framed, h1::ClientPayloadCodec>, + } } impl PlStream { @@ -211,10 +213,7 @@ impl PlStream { impl Stream for PlStream { type Item = Result; - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let mut this = self.project(); match ready!(this.framed.as_mut().next_item(cx)?) { diff --git a/actix-http/src/client/h2proto.rs b/awc/src/client/h2proto.rs similarity index 96% rename from actix-http/src/client/h2proto.rs rename to awc/src/client/h2proto.rs index b9d5f96bd..feb2dbd06 100644 --- a/actix-http/src/client/h2proto.rs +++ b/awc/src/client/h2proto.rs @@ -8,13 +8,12 @@ use h2::{ }; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, TRANSFER_ENCODING}; use http::{request::Request, Method, Version}; +use log::trace; -use crate::{ +use actix_http::{ body::{BodySize, MessageBody}, header::HeaderMap, - message::{RequestHeadType, ResponseHead}, - payload::Payload, - Error, + Error, Payload, RequestHeadType, ResponseHead, }; use super::{ @@ -131,10 +130,7 @@ where Ok((head, payload)) } -async fn send_body( - body: B, - mut send: SendStream, -) -> Result<(), SendRequestError> +async fn send_body(body: B, mut send: SendStream) -> Result<(), SendRequestError> where B: MessageBody, B::Error: Into, @@ -184,8 +180,7 @@ where pub(crate) fn handshake( io: Io, config: &ConnectorConfig, -) -> impl Future, Connection), h2::Error>> -{ +) -> impl Future, Connection), h2::Error>> { let mut builder = Builder::new(); builder .initial_window_size(config.stream_window_size) diff --git a/actix-http/src/client/mod.rs b/awc/src/client/mod.rs similarity index 95% rename from actix-http/src/client/mod.rs rename to awc/src/client/mod.rs index 41d5fef2a..3abbf50a5 100644 --- a/actix-http/src/client/mod.rs +++ b/awc/src/client/mod.rs @@ -17,7 +17,6 @@ pub use actix_tls::connect::{ pub use self::connection::{Connection, ConnectionIo}; pub use self::connector::{Connector, ConnectorService}; pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; -pub use crate::Protocol; #[derive(Clone)] pub struct Connect { diff --git a/actix-http/src/client/pool.rs b/awc/src/client/pool.rs similarity index 95% rename from actix-http/src/client/pool.rs rename to awc/src/client/pool.rs index 88188038f..229c6324a 100644 --- a/actix-http/src/client/pool.rs +++ b/awc/src/client/pool.rs @@ -14,22 +14,20 @@ use std::{ }; use actix_codec::{AsyncRead, AsyncWrite, ReadBuf}; +use actix_http::Protocol; use actix_rt::time::{sleep, Sleep}; use actix_service::Service; use ahash::AHashMap; use futures_core::future::LocalBoxFuture; use http::uri::Authority; -use pin_project::pin_project; +use pin_project_lite::pin_project; use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use super::config::ConnectorConfig; -use super::connection::{ - ConnectionInnerType, ConnectionIo, ConnectionType, H2ConnectionInner, -}; +use super::connection::{ConnectionInnerType, ConnectionIo, ConnectionType, H2ConnectionInner}; use super::error::ConnectError; use super::h2proto::handshake; use super::Connect; -use super::Protocol; #[derive(Hash, Eq, PartialEq, Clone, Debug)] pub struct Key { @@ -152,9 +150,7 @@ where impl Service for ConnectionPool where - S: Service - + Clone - + 'static, + S: Service + Clone + 'static, Io: ConnectionIo, { type Response = ConnectionType; @@ -195,8 +191,8 @@ where let config = &inner.config; let idle_dur = now - c.used; let age = now - c.created; - let conn_ineligible = idle_dur > config.conn_keep_alive - || age > config.conn_lifetime; + let conn_ineligible = + idle_dur > config.conn_keep_alive || age > config.conn_lifetime; if conn_ineligible { // drop connections that are too old @@ -231,9 +227,7 @@ where // match the connection and spawn new one if did not get anything. match conn { - Some(conn) => { - Ok(ConnectionType::from_pool(conn.conn, conn.created, acquired)) - } + Some(conn) => Ok(ConnectionType::from_pool(conn.conn, conn.created, acquired)), None => { let (io, proto) = connector.call(req).await?; @@ -284,9 +278,7 @@ where let mut read_buf = ReadBuf::new(&mut buf); let state = match Pin::new(&mut this.io).poll_read(cx, &mut read_buf) { - Poll::Ready(Ok(())) if !read_buf.filled().is_empty() => { - ConnectionState::Tainted - } + Poll::Ready(Ok(())) if !read_buf.filled().is_empty() => ConnectionState::Tainted, Poll::Pending => ConnectionState::Live, _ => ConnectionState::Skip, @@ -302,11 +294,13 @@ struct PooledConnection { created: Instant, } -#[pin_project] -struct CloseConnection { - io: Io, - #[pin] - timeout: Sleep, +pin_project! { + #[project = CloseConnectionProj] + struct CloseConnection { + io: Io, + #[pin] + timeout: Sleep, + } } impl CloseConnection @@ -413,17 +407,11 @@ mod test { unimplemented!() } - fn poll_flush( - self: Pin<&mut Self>, - _: &mut Context<'_>, - ) -> Poll> { + fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { unimplemented!() } - fn poll_shutdown( - self: Pin<&mut Self>, - _: &mut Context<'_>, - ) -> Poll> { + fn poll_shutdown(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } } diff --git a/awc/src/connect.rs b/awc/src/connect.rs index 6a9fc4630..f27a8c368 100644 --- a/awc/src/connect.rs +++ b/awc/src/connect.rs @@ -8,16 +8,14 @@ use std::{ use actix_codec::Framed; use actix_http::{ - body::Body, - client::{ - Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError, - }, - h1::ClientCodec, - Payload, RequestHead, RequestHeadType, ResponseHead, + body::Body, h1::ClientCodec, Payload, RequestHead, RequestHeadType, ResponseHead, }; use actix_service::Service; use futures_core::{future::LocalBoxFuture, ready}; +use crate::client::{ + Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError, +}; use crate::response::ClientResponse; pub type BoxConnectorService = Rc< diff --git a/awc/src/error.rs b/awc/src/error.rs index c83c5ebbf..d415efe95 100644 --- a/awc/src/error.rs +++ b/awc/src/error.rs @@ -1,15 +1,15 @@ //! HTTP client errors -pub use actix_http::client::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; -pub use actix_http::error::PayloadError; -pub use actix_http::http::Error as HttpError; -pub use actix_http::ws::HandshakeError as WsHandshakeError; -pub use actix_http::ws::ProtocolError as WsProtocolError; +pub use actix_http::{ + error::PayloadError, + http::{header::HeaderValue, Error as HttpError, StatusCode}, + ws::{HandshakeError as WsHandshakeError, ProtocolError as WsProtocolError}, +}; +use derive_more::{Display, From}; use serde_json::error::Error as JsonError; -use actix_http::http::{header::HeaderValue, StatusCode}; -use derive_more::{Display, From}; +pub use crate::client::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; /// Websocket client error #[derive(Debug, Display, From)] diff --git a/awc/src/lib.rs b/awc/src/lib.rs index c0290ddcf..05f97aa3d 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -104,22 +104,8 @@ #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] -use std::{convert::TryFrom, rc::Rc, time::Duration}; - -#[cfg(feature = "cookies")] -pub use cookie; - -pub use actix_http::{client::Connector, http}; - -use actix_http::{ - client::{TcpConnect, TcpConnectError, TcpConnection}, - http::{Error as HttpError, HeaderMap, Method, Uri}, - RequestHead, -}; -use actix_rt::net::TcpStream; -use actix_service::Service; - mod builder; +mod client; mod connect; pub mod error; mod frozen; @@ -130,13 +116,29 @@ mod sender; pub mod test; pub mod ws; +pub use actix_http::http; +#[cfg(feature = "cookies")] +pub use cookie; + pub use self::builder::ClientBuilder; +pub use self::client::Connector; pub use self::connect::{BoxConnectorService, BoxedSocket, ConnectRequest, ConnectResponse}; pub use self::frozen::{FrozenClientRequest, FrozenSendBuilder}; pub use self::request::ClientRequest; pub use self::response::{ClientResponse, JsonBody, MessageBody}; pub use self::sender::SendClientRequest; +use std::{convert::TryFrom, rc::Rc, time::Duration}; + +use actix_http::{ + http::{Error as HttpError, HeaderMap, Method, Uri}, + RequestHead, +}; +use actix_rt::net::TcpStream; +use actix_service::Service; + +use self::client::{TcpConnect, TcpConnectError, TcpConnection}; + /// An asynchronous HTTP and WebSocket client. /// /// You should take care to create, at most, one `Client` per thread. Otherwise, expect higher CPU diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs index a8c14d549..8a79a6596 100644 --- a/awc/src/middleware/redirect.rs +++ b/awc/src/middleware/redirect.rs @@ -9,7 +9,6 @@ use std::{ use actix_http::{ body::Body, - client::{InvalidUrl, SendRequestError}, http::{header, Method, StatusCode, Uri}, RequestHead, RequestHeadType, }; @@ -19,6 +18,7 @@ use futures_core::ready; use super::Transform; +use crate::client::{InvalidUrl, SendRequestError}; use crate::connect::{ConnectRequest, ConnectResponse}; use crate::ClientResponse; From 855e260fdb0f983537c463fd8d64c3eeb38ab546 Mon Sep 17 00:00:00 2001 From: Luca Palmieri Date: Tue, 26 Oct 2021 09:24:38 +0100 Subject: [PATCH 69/88] Add `html_utf8` content type. (#2423) --- CHANGES.md | 2 ++ src/http/header/content_type.rs | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9d7b3180d..3be41d468 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,8 @@ # Changes ## Unreleased - 2021-xx-xx +### Changed +* `ContentType::html` now returns `Content-Type: text/html; charset=utf-8` instead of `Content-Type: text/html`. ## 4.0.0-beta.10 - 2021-10-20 diff --git a/src/http/header/content_type.rs b/src/http/header/content_type.rs index e1c419c22..230460003 100644 --- a/src/http/header/content_type.rs +++ b/src/http/header/content_type.rs @@ -60,52 +60,53 @@ crate::http::header::common_header! { } impl ContentType { - /// A constructor to easily create a `Content-Type: application/json` + /// A constructor to easily create a `Content-Type: application/json` /// header. #[inline] pub fn json() -> ContentType { ContentType(mime::APPLICATION_JSON) } - /// A constructor to easily create a `Content-Type: text/plain; + /// A constructor to easily create a `Content-Type: text/plain; /// charset=utf-8` header. #[inline] pub fn plaintext() -> ContentType { ContentType(mime::TEXT_PLAIN_UTF_8) } - /// A constructor to easily create a `Content-Type: text/html` header. + /// A constructor to easily create a `Content-Type: text/html; charset=utf-8` + /// header. #[inline] pub fn html() -> ContentType { - ContentType(mime::TEXT_HTML) + ContentType(mime::TEXT_HTML_UTF_8) } - /// A constructor to easily create a `Content-Type: text/xml` header. + /// A constructor to easily create a `Content-Type: text/xml` header. #[inline] pub fn xml() -> ContentType { ContentType(mime::TEXT_XML) } - /// A constructor to easily create a `Content-Type: + /// A constructor to easily create a `Content-Type: /// application/www-form-url-encoded` header. #[inline] pub fn form_url_encoded() -> ContentType { ContentType(mime::APPLICATION_WWW_FORM_URLENCODED) } - /// A constructor to easily create a `Content-Type: image/jpeg` header. + /// A constructor to easily create a `Content-Type: image/jpeg` header. #[inline] pub fn jpeg() -> ContentType { ContentType(mime::IMAGE_JPEG) } - /// A constructor to easily create a `Content-Type: image/png` header. + /// A constructor to easily create a `Content-Type: image/png` header. #[inline] pub fn png() -> ContentType { ContentType(mime::IMAGE_PNG) } - /// A constructor to easily create a `Content-Type: + /// A constructor to easily create a `Content-Type: /// application/octet-stream` header. #[inline] pub fn octet_stream() -> ContentType { From be9530eb729035cf4225753a2d6f64a12e0e6140 Mon Sep 17 00:00:00 2001 From: fakeshadow <24548779@qq.com> Date: Tue, 26 Oct 2021 20:16:48 +0800 Subject: [PATCH 70/88] avoid building actix-tls with no-default-features (#2426) --- actix-http/Cargo.toml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 3d45cc8ce..7d39e000a 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -27,10 +27,10 @@ path = "src/lib.rs" default = [] # openssl -openssl = ["actix-tls/openssl"] +openssl = ["actix-tls/accept", "actix-tls/openssl"] # rustls support -rustls = ["actix-tls/rustls"] +rustls = ["actix-tls/accept", "actix-tls/rustls"] # enable compression support compress-brotli = ["brotli2", "__compress"] @@ -46,7 +46,6 @@ actix-service = "2.0.0" actix-codec = "0.4.0" actix-utils = "3.0.0" actix-rt = "2.2" -actix-tls = { version = "3.0.0-beta.7", features = ["accept"] } ahash = "0.7" base64 = "0.13" @@ -74,6 +73,9 @@ sha-1 = "0.9" smallvec = "1.6.1" tokio = { version = "1.2", features = ["sync"] } +# tls +actix-tls = { version = "3.0.0-beta.7", default-features = false, optional = true } + # compression brotli2 = { version="0.3.2", optional = true } flate2 = { version = "1.0.13", optional = true } From ec6d284a8e393d8c8f5bf60eece49abbab62658f Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Sun, 31 Oct 2021 16:19:21 +0300 Subject: [PATCH 71/88] improve "data no configured" message (#2429) --- src/data.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data.rs b/src/data.rs index 7e01d3462..d27ad196b 100644 --- a/src/data.rs +++ b/src/data.rs @@ -137,7 +137,7 @@ impl FromRequest for Data { type_name::(), ); err(ErrorInternalServerError( - "App data is not configured, to configure use App::data()", + "App data is not configured, to configure construct it with web::Data::new() and pass it to App::app_data()", )) } } From 6ec2d7b90966bc55b72ee4c90129074742d087bc Mon Sep 17 00:00:00 2001 From: fakeshadow <24548779@qq.com> Date: Thu, 4 Nov 2021 23:15:23 +0800 Subject: [PATCH 72/88] add keep alive to h2 through ping pong (#2433) --- actix-http/src/h2/dispatcher.rs | 147 ++++++++++++++++++-------- actix-http/tests/test_h2_ping_pong.rs | 77 ++++++++++++++ 2 files changed, 180 insertions(+), 44 deletions(-) create mode 100644 actix-http/tests/test_h2_ping_pong.rs diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index ea149b1e0..7326dfff1 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -10,11 +10,15 @@ use std::{ }; use actix_codec::{AsyncRead, AsyncWrite}; +use actix_rt::time::Sleep; use actix_service::Service; use actix_utils::future::poll_fn; use bytes::{Bytes, BytesMut}; use futures_core::ready; -use h2::server::{Connection, SendResponse}; +use h2::{ + server::{Connection, SendResponse}, + Ping, PingPong, +}; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; use log::{error, trace}; use pin_project_lite::pin_project; @@ -36,29 +40,46 @@ pin_project! { on_connect_data: OnConnectData, config: ServiceConfig, peer_addr: Option, - _phantom: PhantomData, + ping_pong: Option, + _phantom: PhantomData } } -impl Dispatcher { +impl Dispatcher +where + T: AsyncRead + AsyncWrite + Unpin, +{ pub(crate) fn new( flow: Rc>, - connection: Connection, + mut connection: Connection, on_connect_data: OnConnectData, config: ServiceConfig, peer_addr: Option, ) -> Self { + let ping_pong = config.keep_alive_timer().map(|timer| H2PingPong { + timer: Box::pin(timer), + on_flight: false, + ping_pong: connection.ping_pong().unwrap(), + }); + Self { flow, config, peer_addr, connection, on_connect_data, + ping_pong, _phantom: PhantomData, } } } +struct H2PingPong { + timer: Pin>, + on_flight: bool, + ping_pong: PingPong, +} + impl Future for Dispatcher where T: AsyncRead + AsyncWrite + Unpin, @@ -77,54 +98,92 @@ where fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); - while let Some((req, tx)) = - ready!(Pin::new(&mut this.connection).poll_accept(cx)?) - { - let (parts, body) = req.into_parts(); - let pl = crate::h2::Payload::new(body); - let pl = Payload::::H2(pl); - let mut req = Request::with_payload(pl); + loop { + match Pin::new(&mut this.connection).poll_accept(cx)? { + Poll::Ready(Some((req, tx))) => { + let (parts, body) = req.into_parts(); + let pl = crate::h2::Payload::new(body); + let pl = Payload::::H2(pl); + let mut req = Request::with_payload(pl); - let head = req.head_mut(); - head.uri = parts.uri; - head.method = parts.method; - head.version = parts.version; - head.headers = parts.headers.into(); - head.peer_addr = this.peer_addr; + let head = req.head_mut(); + head.uri = parts.uri; + head.method = parts.method; + head.version = parts.version; + head.headers = parts.headers.into(); + head.peer_addr = this.peer_addr; - // merge on_connect_ext data into request extensions - this.on_connect_data.merge_into(&mut req); + // merge on_connect_ext data into request extensions + this.on_connect_data.merge_into(&mut req); - let fut = this.flow.service.call(req); - let config = this.config.clone(); + let fut = this.flow.service.call(req); + let config = this.config.clone(); - // multiplex request handling with spawn task - actix_rt::spawn(async move { - // resolve service call and send response. - let res = match fut.await { - Ok(res) => handle_response(res.into(), tx, config).await, - Err(err) => { - let res: Response = err.into(); - handle_response(res, tx, config).await - } - }; + // multiplex request handling with spawn task + actix_rt::spawn(async move { + // resolve service call and send response. + let res = match fut.await { + Ok(res) => handle_response(res.into(), tx, config).await, + Err(err) => { + let res: Response = err.into(); + handle_response(res, tx, config).await + } + }; - // log error. - if let Err(err) = res { - match err { - DispatchError::SendResponse(err) => { - trace!("Error sending HTTP/2 response: {:?}", err) + // log error. + if let Err(err) = res { + match err { + DispatchError::SendResponse(err) => { + trace!("Error sending HTTP/2 response: {:?}", err) + } + DispatchError::SendData(err) => warn!("{:?}", err), + DispatchError::ResponseBody(err) => { + error!("Response payload stream error: {:?}", err) + } + } } - DispatchError::SendData(err) => warn!("{:?}", err), - DispatchError::ResponseBody(err) => { - error!("Response payload stream error: {:?}", err) - } - } + }); } - }); - } + Poll::Ready(None) => return Poll::Ready(Ok(())), + Poll::Pending => match this.ping_pong.as_mut() { + Some(ping_pong) => loop { + if ping_pong.on_flight { + // When have on flight ping pong. poll pong and and keep alive timer. + // on success pong received update keep alive timer to determine the next timing of + // ping pong. + match ping_pong.ping_pong.poll_pong(cx)? { + Poll::Ready(_) => { + ping_pong.on_flight = false; - Poll::Ready(Ok(())) + let dead_line = + this.config.keep_alive_expire().unwrap(); + ping_pong.timer.as_mut().reset(dead_line); + } + Poll::Pending => { + return ping_pong + .timer + .as_mut() + .poll(cx) + .map(|_| Ok(())) + } + } + } else { + // When there is no on flight ping pong. keep alive timer is used to wait for next + // timing of ping pong. Therefore at this point it serves as an interval instead. + ready!(ping_pong.timer.as_mut().poll(cx)); + + ping_pong.ping_pong.send_ping(Ping::opaque())?; + + let dead_line = this.config.keep_alive_expire().unwrap(); + ping_pong.timer.as_mut().reset(dead_line); + + ping_pong.on_flight = true; + } + }, + None => return Poll::Pending, + }, + } + } } } diff --git a/actix-http/tests/test_h2_ping_pong.rs b/actix-http/tests/test_h2_ping_pong.rs new file mode 100644 index 000000000..5e03785a1 --- /dev/null +++ b/actix-http/tests/test_h2_ping_pong.rs @@ -0,0 +1,77 @@ +use std::io; + +use actix_http::{error::Error, HttpService, Response}; +use actix_server::Server; + +#[actix_rt::test] +async fn h2_ping_pong() -> io::Result<()> { + let (tx, rx) = std::sync::mpsc::sync_channel(1); + + let lst = std::net::TcpListener::bind("127.0.0.1:0")?; + + let addr = lst.local_addr().unwrap(); + + let join = std::thread::spawn(move || { + actix_rt::System::new().block_on(async move { + let handle = Server::build() + .disable_signals() + .workers(1) + .listen("h2_ping_pong", lst, || { + HttpService::build() + .keep_alive(3) + .h2(|_| async { Ok::<_, Error>(Response::ok()) }) + .tcp() + })? + .run(); + + tx.send(handle.clone()).unwrap(); + + handle.await + }) + }); + + let handle = rx.recv().unwrap(); + + let (sync_tx, rx) = std::sync::mpsc::sync_channel(1); + + // use a separate thread for h2 client so it can be blocked. + std::thread::spawn(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async move { + let stream = tokio::net::TcpStream::connect(addr).await.unwrap(); + + let (mut tx, conn) = h2::client::handshake(stream).await.unwrap(); + + tokio::spawn(async move { conn.await.unwrap() }); + + let (res, _) = tx.send_request(::http::Request::new(()), true).unwrap(); + let res = res.await.unwrap(); + + assert_eq!(res.status().as_u16(), 200); + + sync_tx.send(()).unwrap(); + + // intentionally block the client thread so it can not answer ping pong. + std::thread::sleep(std::time::Duration::from_secs(1000)); + }) + }); + + rx.recv().unwrap(); + + let now = std::time::Instant::now(); + + // stop server gracefully. this step would take up to 30 seconds. + handle.stop(true).await; + + // join server thread. only when connection are all gone this step would finish. + join.join().unwrap()?; + + // check the time used for join server thread so it's known that the server shutdown + // is from keep alive and not server graceful shutdown timeout. + assert!(now.elapsed() < std::time::Duration::from_secs(30)); + + Ok(()) +} From 5e554dca35844c241e813663da09e20575d586d3 Mon Sep 17 00:00:00 2001 From: fakeshadow <24548779@qq.com> Date: Thu, 4 Nov 2021 23:57:55 +0800 Subject: [PATCH 73/88] fix awc clippy warning (#2431) --- awc/src/client/pool.rs | 3 ++- awc/tests/test_client.rs | 14 ++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/awc/src/client/pool.rs b/awc/src/client/pool.rs index 229c6324a..9d130412b 100644 --- a/awc/src/client/pool.rs +++ b/awc/src/client/pool.rs @@ -19,6 +19,7 @@ use actix_rt::time::{sleep, Sleep}; use actix_service::Service; use ahash::AHashMap; use futures_core::future::LocalBoxFuture; +use futures_util::FutureExt; use http::uri::Authority; use pin_project_lite::pin_project; use tokio::sync::{OwnedSemaphorePermit, Semaphore}; @@ -201,7 +202,7 @@ where // check if the connection is still usable if let ConnectionInnerType::H1(ref mut io) = c.conn { let check = ConnectionCheckFuture { io }; - match check.await { + match check.now_or_never().expect("ConnectionCheckFuture must never yield with Poll::Pending.") { ConnectionState::Tainted => { inner.close(c.conn); continue; diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index 615789fb3..a0af0cab6 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -795,17 +795,15 @@ async fn client_unread_response() { let lst = std::net::TcpListener::bind(addr).unwrap(); std::thread::spawn(move || { - for stream in lst.incoming() { - let mut stream = stream.unwrap(); - let mut b = [0; 1000]; - let _ = stream.read(&mut b).unwrap(); - let _ = stream.write_all( - b"HTTP/1.1 200 OK\r\n\ + let (mut stream, _) = lst.accept().unwrap(); + let mut b = [0; 1000]; + let _ = stream.read(&mut b).unwrap(); + let _ = stream.write_all( + b"HTTP/1.1 200 OK\r\n\ connection: close\r\n\ \r\n\ welcome!", - ); - } + ); }); // client request From c020cedb631d08528d42de24741975b0a5b6e0b6 Mon Sep 17 00:00:00 2001 From: Ibraheem Ahmed Date: Sun, 7 Nov 2021 12:02:23 -0500 Subject: [PATCH 74/88] Log internal server errors (#2387) --- src/middleware/logger.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index 961eca496..f3f6aa5a9 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -275,9 +275,7 @@ where }; if let Some(error) = res.response().error() { - if res.response().head().status != StatusCode::INTERNAL_SERVER_ERROR { - debug!("Error in response: {:?}", error); - } + debug!("Error in response: {:?}", error); } if let Some(ref mut format) = this.format { From 2754608f3c98821a28c4f2a67d2ef40612587c04 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 8 Nov 2021 02:46:43 +0000 Subject: [PATCH 75/88] fix codegen tests --- src/middleware/logger.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index f3f6aa5a9..b4d100b3e 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -22,7 +22,7 @@ use time::{format_description::well_known::Rfc3339, OffsetDateTime}; use crate::{ dev::{BodySize, MessageBody}, - http::{HeaderName, StatusCode}, + http::HeaderName, service::{ServiceRequest, ServiceResponse}, Error, HttpResponse, Result, }; From a2f59c02f72c0c50cb69dd9e17439f70b6a8b6db Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 15 Nov 2021 04:03:33 +0000 Subject: [PATCH 76/88] bump actix-server to beta 9 (#2442) --- CHANGES.md | 12 ++- Cargo.toml | 10 +-- README.md | 4 +- actix-files/Cargo.toml | 8 +- actix-http-test/CHANGES.md | 7 ++ actix-http-test/Cargo.toml | 13 +-- actix-http-test/README.md | 4 +- actix-http-test/src/lib.rs | 105 ++++++++++++++-------- actix-http/CHANGES.md | 7 ++ actix-http/Cargo.toml | 10 +-- actix-http/README.md | 4 +- actix-http/tests/test_h2_ping_pong.rs | 6 +- actix-http/tests/test_openssl.rs | 33 +++---- actix-http/tests/test_rustls.rs | 5 +- actix-http/tests/test_server.rs | 82 +++++++++++++---- actix-multipart/Cargo.toml | 4 +- actix-router/Cargo.toml | 2 +- actix-test/CHANGES.md | 4 + actix-test/Cargo.toml | 11 +-- actix-test/src/lib.rs | 79 +++++++++++----- actix-web-actors/Cargo.toml | 8 +- actix-web-codegen/Cargo.toml | 4 +- awc/CHANGES.md | 3 + awc/Cargo.toml | 16 ++-- awc/README.md | 4 +- awc/src/client/connection.rs | 3 +- awc/tests/test_rustls_client.rs | 3 +- examples/on_connect.rs | 1 + src/dev.rs | 2 +- src/server.rs | 4 +- tests/test_httpserver.rs | 125 ++++++++++++-------------- tests/test_server.rs | 68 +++++++++++++- 32 files changed, 415 insertions(+), 236 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3be41d468..4ecbd0c2e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,8 +1,18 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 4.0.0-beta.11 - 2021-11-15 +### Added +* Re-export `dev::ServerHandle` from `actix-server`. [#2442] + ### Changed -* `ContentType::html` now returns `Content-Type: text/html; charset=utf-8` instead of `Content-Type: text/html`. +* `ContentType::html` now produces `text/html; charset=utf-8` instead of `text/html`. [#2423] +* Update `actix-server` to `2.0.0-beta.9`. [#2442] + +[#2423]: https://github.com/actix/actix-web/pull/2423 +[#2442]: https://github.com/actix/actix-web/pull/2442 ## 4.0.0-beta.10 - 2021-10-20 diff --git a/Cargo.toml b/Cargo.toml index 152282207..8ca34c924 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "4.0.0-beta.10" +version = "4.0.0-beta.11" authors = ["Nikolay Kim "] description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" keywords = ["actix", "http", "web", "framework", "async"] @@ -69,12 +69,12 @@ __compress = [] actix-codec = "0.4.0" actix-macros = "0.2.3" actix-rt = "2.2" -actix-server = "2.0.0-beta.3" +actix-server = "2.0.0-beta.9" actix-service = "2.0.0" actix-utils = "3.0.0" actix-tls = { version = "3.0.0-beta.7", default-features = false, optional = true } -actix-http = "3.0.0-beta.11" +actix-http = "3.0.0-beta.12" actix-router = "0.5.0-beta.2" actix-web-codegen = "0.5.0-beta.5" @@ -104,8 +104,8 @@ time = { version = "0.3", default-features = false, features = ["formatting"] } url = "2.1" [dev-dependencies] -actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] } -awc = { version = "3.0.0-beta.9", features = ["openssl"] } +actix-test = { version = "0.1.0-beta.6", features = ["openssl", "rustls"] } +awc = { version = "3.0.0-beta.10", features = ["openssl"] } brotli2 = "0.3.2" criterion = { version = "0.3", features = ["html_reports"] } diff --git a/README.md b/README.md index 25b595361..9444f130d 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@

[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) -[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.10)](https://docs.rs/actix-web/4.0.0-beta.10) +[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.11)](https://docs.rs/actix-web/4.0.0-beta.11) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) -[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.10/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.10) +[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.11/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.11)
[![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 3d7340607..bbb9f551a 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -15,8 +15,8 @@ name = "actix_files" path = "src/lib.rs" [dependencies] -actix-web = { version = "4.0.0-beta.10", default-features = false } -actix-http = "3.0.0-beta.11" +actix-web = { version = "4.0.0-beta.11", default-features = false } +actix-http = "3.0.0-beta.12" actix-service = "2.0.0" actix-utils = "3.0.0" @@ -33,5 +33,5 @@ percent-encoding = "2.1" [dev-dependencies] actix-rt = "2.2" -actix-web = "4.0.0-beta.10" -actix-test = "0.1.0-beta.5" +actix-web = "4.0.0-beta.11" +actix-test = "0.1.0-beta.6" diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index 98b197bcf..ea00acb0c 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -1,8 +1,15 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.6 - 2021-11-15 +* `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442] +* Update `actix-server` to `2.0.0-beta.9`. [#2442] * Minimum supported Rust version (MSRV) is now 1.52. +[#2442]: https://github.com/actix/actix-web/pull/2442 + ## 3.0.0-beta.5 - 2021-09-09 * Minimum supported Rust version (MSRV) is now 1.51. diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index d3fc8a47f..b111f8685 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http-test" -version = "3.0.0-beta.5" +version = "3.0.0-beta.6" authors = ["Nikolay Kim "] description = "Various helpers for Actix applications to use during testing" keywords = ["http", "web", "framework", "async", "futures"] @@ -34,13 +34,13 @@ actix-codec = "0.4.0" actix-tls = "3.0.0-beta.7" actix-utils = "3.0.0" actix-rt = "2.2" -actix-server = "2.0.0-beta.3" -awc = { version = "3.0.0-beta.9", default-features = false } +actix-server = "2.0.0-beta.9" +awc = { version = "3.0.0-beta.10", default-features = false } base64 = "0.13" bytes = "1" futures-core = { version = "0.3.7", default-features = false } -http = "0.2.2" +http = "0.2.5" log = "0.4" socket2 = "0.4" serde = "1.0" @@ -48,7 +48,8 @@ serde_json = "1.0" slab = "0.4" serde_urlencoded = "0.7" tls-openssl = { version = "0.10.9", package = "openssl", optional = true } +tokio = { version = "1.2", features = ["sync"] } [dev-dependencies] -actix-web = { version = "4.0.0-beta.10", default-features = false, features = ["cookies"] } -actix-http = "3.0.0-beta.11" +actix-web = { version = "4.0.0-beta.11", default-features = false, features = ["cookies"] } +actix-http = "3.0.0-beta.12" diff --git a/actix-http-test/README.md b/actix-http-test/README.md index 6bf0d710a..3eee66451 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -3,11 +3,11 @@ > Various helpers for Actix applications to use during testing. [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) -[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.5)](https://docs.rs/actix-http-test/3.0.0-beta.5) +[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.6)](https://docs.rs/actix-http-test/3.0.0-beta.6) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
-[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.5/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.5) +[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.6/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.6) [![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-http-test/src/lib.rs b/actix-http-test/src/lib.rs index c7b083b5e..cda98cea5 100644 --- a/actix-http-test/src/lib.rs +++ b/actix-http-test/src/lib.rs @@ -7,7 +7,7 @@ #[cfg(feature = "openssl")] extern crate tls_openssl as openssl; -use std::{net, sync::mpsc, thread, time::Duration}; +use std::{net, thread, time::Duration}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_rt::{net::TcpStream, System}; @@ -19,6 +19,7 @@ use bytes::Bytes; use futures_core::stream::Stream; use http::Method; use socket2::{Domain, Protocol, Socket, Type}; +use tokio::sync::mpsc; /// Start test server /// @@ -55,12 +56,13 @@ pub async fn test_server>(factory: F) -> TestServer test_server_with_addr(tcp, factory).await } -/// Start [`test server`](test_server()) on a concrete Address +/// Start [`test server`](test_server()) on an existing address binding. pub async fn test_server_with_addr>( tcp: net::TcpListener, factory: F, ) -> TestServer { - let (tx, rx) = mpsc::channel(); + let (started_tx, started_rx) = std::sync::mpsc::channel(); + let (thread_stop_tx, thread_stop_rx) = mpsc::channel(1); // run server in separate thread thread::spawn(move || { @@ -68,59 +70,73 @@ pub async fn test_server_with_addr>( let local_addr = tcp.local_addr().unwrap(); let srv = Server::build() - .listen("test", tcp, factory)? .workers(1) - .disable_signals(); + .disable_signals() + .listen("test", tcp, factory) + .expect("test server could not be created"); - sys.block_on(async { - srv.run(); - tx.send((System::current(), local_addr)).unwrap(); - }); + let srv = srv.run(); + started_tx + .send((System::current(), srv.handle(), local_addr)) + .unwrap(); - sys.run() + // drive server loop + sys.block_on(srv).unwrap(); + + // start system event loop + sys.run().unwrap(); + + // notify TestServer that server and system have shut down + // all thread managed resources should be dropped at this point + let _ = thread_stop_tx.send(()); }); - let (system, addr) = rx.recv().unwrap(); + let (system, server, addr) = started_rx.recv().unwrap(); let client = { + #[cfg(feature = "openssl")] let connector = { - #[cfg(feature = "openssl")] - { - use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; + use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; - let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); - builder.set_verify(SslVerifyMode::NONE); - let _ = builder - .set_alpn_protos(b"\x02h2\x08http/1.1") - .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); - Connector::new() - .conn_lifetime(Duration::from_secs(0)) - .timeout(Duration::from_millis(30000)) - .ssl(builder.build()) - } - #[cfg(not(feature = "openssl"))] - { - Connector::new() - .conn_lifetime(Duration::from_secs(0)) - .timeout(Duration::from_millis(30000)) - } + let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); + + builder.set_verify(SslVerifyMode::NONE); + let _ = builder + .set_alpn_protos(b"\x02h2\x08http/1.1") + .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); + + Connector::new() + .conn_lifetime(Duration::from_secs(0)) + .timeout(Duration::from_millis(30000)) + .ssl(builder.build()) + }; + + #[cfg(not(feature = "openssl"))] + let connector = { + Connector::new() + .conn_lifetime(Duration::from_secs(0)) + .timeout(Duration::from_millis(30000)) }; Client::builder().connector(connector).finish() }; TestServer { - addr, + server, client, system, + addr, + thread_stop_rx, } } /// Test server controller pub struct TestServer { + server: actix_server::ServerHandle, + client: awc::Client, + system: actix_rt::System, addr: net::SocketAddr, - client: Client, - system: System, + thread_stop_rx: mpsc::Receiver<()>, } impl TestServer { @@ -257,15 +273,32 @@ impl TestServer { self.client.headers() } - /// Stop HTTP server - fn stop(&mut self) { + /// Gracefully stop HTTP server. + /// + /// Waits for spawned `Server` and `System` to shutdown gracefully. + pub async fn stop(&mut self) { + // signal server to stop + self.server.stop(true).await; + + // also signal system to stop + // though this is handled by `ServerBuilder::exit_system` too self.system.stop(); + + // wait for thread to be stopped but don't care about result + let _ = self.thread_stop_rx.recv().await; } } impl Drop for TestServer { fn drop(&mut self) { - self.stop() + // calls in this Drop impl should be enough to shut down the server, system, and thread + // without needing to await anything + + // signal server to stop + let _ = self.server.stop(true); + + // signal system to stop + self.system.stop(); } } diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 81595c92d..9ec75b4bc 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,11 +1,18 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.12 - 2021-11-15 +### Changed +* Update `actix-server` to `2.0.0-beta.9`. [#2442] + ### Removed * `client` module. [#2425] * `trust-dns` feature. [#2425] [#2425]: https://github.com/actix/actix-web/pull/2425 +[#2442]: https://github.com/actix/actix-web/pull/2442 ## 3.0.0-beta.11 - 2021-10-20 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 7d39e000a..784312445 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "3.0.0-beta.11" +version = "3.0.0-beta.12" authors = ["Nikolay Kim "] description = "HTTP primitives for the Actix ecosystem" keywords = ["actix", "http", "framework", "async", "futures"] @@ -57,7 +57,7 @@ encoding_rs = "0.8" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] } h2 = "0.3.1" -http = "0.2.2" +http = "0.2.5" httparse = "1.5.1" httpdate = "1.0.1" itoa = "0.4" @@ -71,7 +71,6 @@ pin-project-lite = "0.2" rand = "0.8" sha-1 = "0.9" smallvec = "1.6.1" -tokio = { version = "1.2", features = ["sync"] } # tls actix-tls = { version = "3.0.0-beta.7", default-features = false, optional = true } @@ -82,8 +81,8 @@ flate2 = { version = "1.0.13", optional = true } zstd = { version = "0.7", optional = true } [dev-dependencies] -actix-server = "2.0.0-beta.3" -actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] } +actix-server = "2.0.0-beta.9" +actix-http-test = { version = "3.0.0-beta.6", features = ["openssl"] } actix-tls = { version = "3.0.0-beta.7", features = ["openssl"] } async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } @@ -95,6 +94,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tls-openssl = { package = "openssl", version = "0.10.9" } tls-rustls = { package = "rustls", version = "0.20.0" } +tokio = { version = "1.2", features = ["net", "rt"] } [[example]] name = "ws" diff --git a/actix-http/README.md b/actix-http/README.md index 5b1e552fd..536d17074 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -3,11 +3,11 @@ > HTTP primitives for the Actix ecosystem. [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) -[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.11)](https://docs.rs/actix-http/3.0.0-beta.11) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.12)](https://docs.rs/actix-http/3.0.0-beta.12) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
-[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.11/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.11) +[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.12/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.12) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-http/tests/test_h2_ping_pong.rs b/actix-http/tests/test_h2_ping_pong.rs index 5e03785a1..30ce9aa51 100644 --- a/actix-http/tests/test_h2_ping_pong.rs +++ b/actix-http/tests/test_h2_ping_pong.rs @@ -13,7 +13,7 @@ async fn h2_ping_pong() -> io::Result<()> { let join = std::thread::spawn(move || { actix_rt::System::new().block_on(async move { - let handle = Server::build() + let srv = Server::build() .disable_signals() .workers(1) .listen("h2_ping_pong", lst, || { @@ -24,9 +24,9 @@ async fn h2_ping_pong() -> io::Result<()> { })? .run(); - tx.send(handle.clone()).unwrap(); + tx.send(srv.handle()).unwrap(); - handle.await + srv.await }) }); diff --git a/actix-http/tests/test_openssl.rs b/actix-http/tests/test_openssl.rs index a58d0cc70..0eaaabcc7 100644 --- a/actix-http/tests/test_openssl.rs +++ b/actix-http/tests/test_openssl.rs @@ -8,7 +8,7 @@ use actix_http::{ body::{AnyBody, Body, SizedStream}, error::PayloadError, http::{ - header::{self, HeaderName, HeaderValue}, + header::{self, HeaderValue}, Method, StatusCode, Version, }, Error, HttpMessage, HttpService, Request, Response, @@ -143,38 +143,25 @@ async fn test_h2_content_length() { }) .await; - let header = HeaderName::from_static("content-length"); - let value = HeaderValue::from_static("0"); + static VALUE: HeaderValue = HeaderValue::from_static("0"); { - for &i in &[0] { - let req = srv - .request(Method::HEAD, srv.surl(&format!("/{}", i))) - .send(); - let _response = req.await.expect_err("should timeout on recv 1xx frame"); - // assert_eq!(response.headers().get(&header), None); + let req = srv.request(Method::HEAD, srv.surl("/0")).send(); + req.await.expect_err("should timeout on recv 1xx frame"); - let req = srv - .request(Method::GET, srv.surl(&format!("/{}", i))) - .send(); - let _response = req.await.expect_err("should timeout on recv 1xx frame"); - // assert_eq!(response.headers().get(&header), None); - } + let req = srv.request(Method::GET, srv.surl("/0")).send(); + req.await.expect_err("should timeout on recv 1xx frame"); - for &i in &[1] { - let req = srv - .request(Method::GET, srv.surl(&format!("/{}", i))) - .send(); - let response = req.await.unwrap(); - assert_eq!(response.headers().get(&header), None); - } + let req = srv.request(Method::GET, srv.surl("/1")).send(); + let response = req.await.unwrap(); + assert!(response.headers().get("content-length").is_none()); for &i in &[2, 3] { let req = srv .request(Method::GET, srv.surl(&format!("/{}", i))) .send(); let response = req.await.unwrap(); - assert_eq!(response.headers().get(&header), Some(&value)); + assert_eq!(response.headers().get("content-length"), Some(&VALUE)); } } } diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index 924ef49ad..a9f6e99f8 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -26,10 +26,7 @@ use bytes::{Bytes, BytesMut}; use derive_more::{Display, Error}; use futures_core::Stream; use futures_util::stream::{once, StreamExt as _}; -use rustls::{ - Certificate, OwnedTrustAnchor, PrivateKey, RootCertStore, - ServerConfig as RustlsServerConfig, ServerName, -}; +use rustls::{Certificate, PrivateKey, ServerConfig as RustlsServerConfig, ServerName}; use rustls_pemfile::{certs, pkcs8_private_keys}; async fn load_body(mut stream: S) -> Result diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index c04aeae00..ea78ce113 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -24,7 +24,7 @@ use regex::Regex; #[actix_rt::test] async fn test_h1() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .keep_alive(KeepAlive::Disabled) .client_timeout(1000) @@ -39,11 +39,13 @@ async fn test_h1() { let response = srv.get("/").send().await.unwrap(); assert!(response.status().is_success()); + + srv.stop().await; } #[actix_rt::test] async fn test_h1_2() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .keep_alive(KeepAlive::Disabled) .client_timeout(1000) @@ -59,6 +61,8 @@ async fn test_h1_2() { let response = srv.get("/").send().await.unwrap(); assert!(response.status().is_success()); + + srv.stop().await; } #[derive(Debug, Display, Error)] @@ -73,7 +77,7 @@ impl From for Response { #[actix_rt::test] async fn test_expect_continue() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .expect(fn_service(|req: Request| { if req.head().uri.query() == Some("yes=") { @@ -98,11 +102,13 @@ async fn test_expect_continue() { let mut data = String::new(); let _ = stream.read_to_string(&mut data); assert!(data.starts_with("HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\n")); + + srv.stop().await; } #[actix_rt::test] async fn test_expect_continue_h1() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .expect(fn_service(|req: Request| { sleep(Duration::from_millis(20)).then(move |_| { @@ -129,6 +135,8 @@ async fn test_expect_continue_h1() { let mut data = String::new(); let _ = stream.read_to_string(&mut data); assert!(data.starts_with("HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\n")); + + srv.stop().await; } #[actix_rt::test] @@ -136,7 +144,7 @@ async fn test_chunked_payload() { let chunk_sizes = vec![32768, 32, 32768]; let total_size: usize = chunk_sizes.iter().sum(); - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .h1(fn_service(|mut request: Request| { request @@ -188,11 +196,13 @@ async fn test_chunked_payload() { }; assert_eq!(returned_size, total_size); + + srv.stop().await; } #[actix_rt::test] async fn test_slow_request() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .client_timeout(100) .finish(|_| ok::<_, Infallible>(Response::ok())) @@ -205,11 +215,13 @@ async fn test_slow_request() { let mut data = String::new(); let _ = stream.read_to_string(&mut data); assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); + + srv.stop().await; } #[actix_rt::test] async fn test_http1_malformed_request() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() @@ -221,11 +233,13 @@ async fn test_http1_malformed_request() { let mut data = String::new(); let _ = stream.read_to_string(&mut data); assert!(data.starts_with("HTTP/1.1 400 Bad Request")); + + srv.stop().await; } #[actix_rt::test] async fn test_http1_keepalive() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() @@ -242,11 +256,13 @@ async fn test_http1_keepalive() { let mut data = vec![0; 1024]; let _ = stream.read(&mut data); assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); + + srv.stop().await; } #[actix_rt::test] async fn test_http1_keepalive_timeout() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .keep_alive(1) .h1(|_| ok::<_, Infallible>(Response::ok())) @@ -264,11 +280,13 @@ async fn test_http1_keepalive_timeout() { let mut data = vec![0; 1024]; let res = stream.read(&mut data).unwrap(); assert_eq!(res, 0); + + srv.stop().await; } #[actix_rt::test] async fn test_http1_keepalive_close() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() @@ -285,11 +303,13 @@ async fn test_http1_keepalive_close() { let mut data = vec![0; 1024]; let res = stream.read(&mut data).unwrap(); assert_eq!(res, 0); + + srv.stop().await; } #[actix_rt::test] async fn test_http10_keepalive_default_close() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() @@ -305,11 +325,13 @@ async fn test_http10_keepalive_default_close() { let mut data = vec![0; 1024]; let res = stream.read(&mut data).unwrap(); assert_eq!(res, 0); + + srv.stop().await; } #[actix_rt::test] async fn test_http10_keepalive() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() @@ -332,11 +354,13 @@ async fn test_http10_keepalive() { let mut data = vec![0; 1024]; let res = stream.read(&mut data).unwrap(); assert_eq!(res, 0); + + srv.stop().await; } #[actix_rt::test] async fn test_http1_keepalive_disabled() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .keep_alive(KeepAlive::Disabled) .h1(|_| ok::<_, Infallible>(Response::ok())) @@ -353,6 +377,8 @@ async fn test_http1_keepalive_disabled() { let mut data = vec![0; 1024]; let res = stream.read(&mut data).unwrap(); assert_eq!(res, 0); + + srv.stop().await; } #[actix_rt::test] @@ -362,7 +388,7 @@ async fn test_content_length() { StatusCode, }; - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .h1(|req: Request| { let indx: usize = req.uri().path()[1..].parse().unwrap(); @@ -400,6 +426,8 @@ async fn test_content_length() { assert_eq!(response.headers().get(&header), Some(&value)); } } + + srv.stop().await; } #[actix_rt::test] @@ -439,6 +467,8 @@ async fn test_h1_headers() { // read response let bytes = srv.load_body(response).await.unwrap(); assert_eq!(bytes, Bytes::from(data2)); + + srv.stop().await; } const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ @@ -478,6 +508,8 @@ async fn test_h1_body() { // read response let bytes = srv.load_body(response).await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -503,6 +535,8 @@ async fn test_h1_head_empty() { // read response let bytes = srv.load_body(response).await.unwrap(); assert!(bytes.is_empty()); + + srv.stop().await; } #[actix_rt::test] @@ -528,11 +562,13 @@ async fn test_h1_head_binary() { // read response let bytes = srv.load_body(response).await.unwrap(); assert!(bytes.is_empty()); + + srv.stop().await; } #[actix_rt::test] async fn test_h1_head_binary2() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .tcp() @@ -549,6 +585,8 @@ async fn test_h1_head_binary2() { .unwrap(); assert_eq!(format!("{}", STR.len()), len.to_str().unwrap()); } + + srv.stop().await; } #[actix_rt::test] @@ -571,6 +609,8 @@ async fn test_h1_body_length() { // read response let bytes = srv.load_body(response).await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -606,6 +646,8 @@ async fn test_h1_body_chunked_explicit() { // decode assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -635,6 +677,8 @@ async fn test_h1_body_chunked_implicit() { // read response let bytes = srv.load_body(response).await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -662,6 +706,8 @@ async fn test_h1_response_http_error_handling() { bytes, Bytes::from_static(b"error processing HTTP: failed to parse header value") ); + + srv.stop().await; } #[derive(Debug, Display, Error)] @@ -689,11 +735,13 @@ async fn test_h1_service_error() { // read response let bytes = srv.load_body(response).await.unwrap(); assert_eq!(bytes, Bytes::from_static(b"error")); + + srv.stop().await; } #[actix_rt::test] async fn test_h1_on_connect() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .on_connect_ext(|_, data| { data.insert(20isize); @@ -708,4 +756,6 @@ async fn test_h1_on_connect() { let response = srv.get("/").send().await.unwrap(); assert!(response.status().is_success()); + + srv.stop().await; } diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 92637cef9..b2f3e391c 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -14,7 +14,7 @@ name = "actix_multipart" path = "src/lib.rs" [dependencies] -actix-web = { version = "4.0.0-beta.10", default-features = false } +actix-web = { version = "4.0.0-beta.11", default-features = false } actix-utils = "3.0.0" bytes = "1" @@ -29,6 +29,6 @@ twoway = "0.2" [dev-dependencies] actix-rt = "2.2" -actix-http = "3.0.0-beta.11" +actix-http = "3.0.0-beta.12" tokio = { version = "1", features = ["sync"] } tokio-stream = "0.1" diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml index e32f0edd6..b95bca505 100644 --- a/actix-router/Cargo.toml +++ b/actix-router/Cargo.toml @@ -30,7 +30,7 @@ serde = "1" [dev-dependencies] criterion = { version = "0.3", features = ["html_reports"] } firestorm = { version = "0.4", features = ["enable_system_time"] } -http = "0.2.3" +http = "0.2.5" serde = { version = "1", features = ["derive"] } [[bench]] diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index 070892581..5c22139ae 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 0.1.0-beta.6 - 2021-11-15 +* No significant changes from `0.1.0-beta.5`. + + ## 0.1.0-beta.5 - 2021-10-20 * Updated rustls to v0.20. [#2414] * Minimum supported Rust version (MSRV) is now 1.52. diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index 002e7662e..58c0d31a5 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-test" -version = "0.1.0-beta.5" +version = "0.1.0-beta.6" authors = [ "Nikolay Kim ", "Rob Ede ", @@ -29,13 +29,13 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"] [dependencies] actix-codec = "0.4.0" -actix-http = "3.0.0-beta.11" -actix-http-test = "3.0.0-beta.5" +actix-http = "3.0.0-beta.12" +actix-http-test = "3.0.0-beta.6" actix-service = "2.0.0" actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.10", default-features = false, features = ["cookies"] } +actix-web = { version = "4.0.0-beta.11", default-features = false, features = ["cookies"] } actix-rt = "2.1" -awc = { version = "3.0.0-beta.9", default-features = false, features = ["cookies"] } +awc = { version = "3.0.0-beta.10", default-features = false, features = ["cookies"] } futures-core = { version = "0.3.7", default-features = false, features = ["std"] } futures-util = { version = "0.3.7", default-features = false, features = [] } @@ -45,3 +45,4 @@ serde_json = "1" serde_urlencoded = "0.7" tls-openssl = { package = "openssl", version = "0.10.9", optional = true } tls-rustls = { package = "rustls", version = "0.20.0", optional = true } +tokio = { version = "1.2", features = ["sync"] } diff --git a/actix-test/src/lib.rs b/actix-test/src/lib.rs index 23a7eeba1..cf5738aa0 100644 --- a/actix-test/src/lib.rs +++ b/actix-test/src/lib.rs @@ -31,7 +31,7 @@ extern crate tls_openssl as openssl; #[cfg(feature = "rustls")] extern crate tls_rustls as rustls; -use std::{error::Error as StdError, fmt, net, sync::mpsc, thread, time}; +use std::{error::Error as StdError, fmt, net, thread, time::Duration}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; pub use actix_http::test::TestBuffer; @@ -41,8 +41,9 @@ use actix_http::{ }; use actix_service::{map_config, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _}; use actix_web::{ - dev::{AppConfig, MessageBody, Server, Service}, - rt, web, Error, + dev::{AppConfig, MessageBody, Server, ServerHandle, Service}, + rt::{self, System}, + web, Error, }; use awc::{error::PayloadError, Client, ClientRequest, ClientResponse, Connector}; use futures_core::Stream; @@ -52,6 +53,7 @@ pub use actix_web::test::{ call_service, default_service, init_service, load_stream, ok_service, read_body, read_body_json, read_response, read_response_json, TestRequest, }; +use tokio::sync::mpsc; /// Start default [`TestServer`]. /// @@ -128,7 +130,11 @@ where B: MessageBody + 'static, B::Error: Into>, { - let (tx, rx) = mpsc::channel(); + // for sending handles and server info back from the spawned thread + let (started_tx, started_rx) = std::sync::mpsc::channel(); + + // for signaling the shutdown of spawned server and system + let (thread_stop_tx, thread_stop_rx) = mpsc::channel(1); let tls = match cfg.stream { StreamType::Tcp => false, @@ -138,7 +144,7 @@ where StreamType::Rustls(_) => true, }; - // run server in separate thread + // run server in separate orphaned thread thread::spawn(move || { let sys = rt::System::new(); let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); @@ -146,7 +152,7 @@ where let factory = factory.clone(); let srv_cfg = cfg.clone(); let timeout = cfg.client_timeout; - let builder = Server::build().workers(1).disable_signals(); + let builder = Server::build().workers(1).disable_signals().system_exit(); let srv = match srv_cfg.stream { StreamType::Tcp => match srv_cfg.tp { @@ -275,17 +281,25 @@ where }), }, } - .unwrap(); + .expect("test server could not be created"); - sys.block_on(async { - let srv = srv.run(); - tx.send((rt::System::current(), srv, local_addr)).unwrap(); - }); + let srv = srv.run(); + started_tx + .send((System::current(), srv.handle(), local_addr)) + .unwrap(); - sys.run() + // drive server loop + sys.block_on(srv).unwrap(); + + // start system event loop + sys.run().unwrap(); + + // notify TestServer that server and system have shut down + // all thread managed resources should be dropped at this point + let _ = thread_stop_tx.send(()); }); - let (system, server, addr) = rx.recv().unwrap(); + let (system, server, addr) = started_rx.recv().unwrap(); let client = { let connector = { @@ -299,15 +313,15 @@ where .set_alpn_protos(b"\x02h2\x08http/1.1") .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); Connector::new() - .conn_lifetime(time::Duration::from_secs(0)) - .timeout(time::Duration::from_millis(30000)) + .conn_lifetime(Duration::from_secs(0)) + .timeout(Duration::from_millis(30000)) .ssl(builder.build()) } #[cfg(not(feature = "openssl"))] { Connector::new() - .conn_lifetime(time::Duration::from_secs(0)) - .timeout(time::Duration::from_millis(30000)) + .conn_lifetime(Duration::from_secs(0)) + .timeout(Duration::from_millis(30000)) } }; @@ -315,11 +329,12 @@ where }; TestServer { - addr, + server, + thread_stop_rx, client, system, + addr, tls, - server, } } @@ -405,11 +420,12 @@ impl TestServerConfig { /// /// See [`start`] for usage example. pub struct TestServer { - addr: net::SocketAddr, + server: ServerHandle, + thread_stop_rx: mpsc::Receiver<()>, client: awc::Client, system: rt::System, + addr: net::SocketAddr, tls: bool, - server: Server, } impl TestServer { @@ -505,15 +521,30 @@ impl TestServer { } /// Gracefully stop HTTP server. - pub async fn stop(self) { + /// + /// Waits for spawned `Server` and `System` to shutdown gracefully. + pub async fn stop(mut self) { + // signal server to stop self.server.stop(true).await; + + // also signal system to stop + // though this is handled by `ServerBuilder::exit_system` too self.system.stop(); - rt::time::sleep(time::Duration::from_millis(100)).await; + + // wait for thread to be stopped but don't care about result + let _ = self.thread_stop_rx.recv().await; } } impl Drop for TestServer { fn drop(&mut self) { - self.system.stop() + // calls in this Drop impl should be enough to shut down the server, system, and thread + // without needing to await anything + + // signal server to stop + let _ = self.server.stop(true); + + // signal system to stop + self.system.stop(); } } diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 2d987a131..706a90c00 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -16,8 +16,8 @@ path = "src/lib.rs" [dependencies] actix = { version = "0.12.0", default-features = false } actix-codec = "0.4.0" -actix-http = "3.0.0-beta.11" -actix-web = { version = "4.0.0-beta.10", default-features = false } +actix-http = "3.0.0-beta.12" +actix-web = { version = "4.0.0-beta.11", default-features = false } bytes = "1" bytestring = "1" @@ -27,8 +27,8 @@ tokio = { version = "1", features = ["sync"] } [dev-dependencies] actix-rt = "2.2" -actix-test = "0.1.0-beta.5" +actix-test = "0.1.0-beta.6" -awc = { version = "3.0.0-beta.9", default-features = false } +awc = { version = "3.0.0-beta.10", default-features = false } env_logger = "0.8" futures-util = { version = "0.3.7", default-features = false } diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index c04ca435a..a407d00fc 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -23,9 +23,9 @@ actix-router = "0.5.0-beta.2" [dev-dependencies] actix-rt = "2.2" actix-macros = "0.2.3" -actix-test = "0.1.0-beta.5" +actix-test = "0.1.0-beta.6" actix-utils = "3.0.0" -actix-web = "4.0.0-beta.10" +actix-web = "4.0.0-beta.11" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } trybuild = "1" diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 5682a237c..6b9531c70 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -3,6 +3,9 @@ ## Unreleased - 2021-xx-xx +## 3.0.0-beta.10 - 2021-11-15 + + ## 3.0.0-beta.9 - 2021-10-20 * Updated rustls to v0.20. [#2414] diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 6eeb9ce51..ce710d58d 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "3.0.0-beta.9" +version = "3.0.0-beta.10" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", @@ -55,7 +55,7 @@ __compress = [] [dependencies] actix-codec = "0.4.0" actix-service = "2.0.0" -actix-http = "3.0.0-beta.11" +actix-http = "3.0.0-beta.12" actix-rt = { version = "2.1", default-features = false } actix-tls = { version = "3.0.0-beta.7", features = ["connect"] } actix-utils = "3.0.0" @@ -68,7 +68,7 @@ derive_more = "0.99.5" futures-core = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false } h2 = "0.3" -http = "0.2" +http = "0.2.5" itoa = "0.4" log =" 0.4" mime = "0.3" @@ -88,13 +88,13 @@ tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features trust-dns-resolver = { version = "0.20.0", optional = true } [dev-dependencies] -actix-web = { version = "4.0.0-beta.10", features = ["openssl"] } -actix-http = { version = "3.0.0-beta.11", features = ["openssl"] } -actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] } +actix-web = { version = "4.0.0-beta.11", features = ["openssl"] } +actix-http = { version = "3.0.0-beta.12", features = ["openssl"] } +actix-http-test = { version = "3.0.0-beta.6", features = ["openssl"] } actix-utils = "3.0.0" -actix-server = "2.0.0-beta.3" +actix-server = "2.0.0-beta.9" actix-tls = { version = "3.0.0-beta.7", features = ["openssl", "rustls"] } -actix-test = { version = "0.1.0-beta.5", features = ["openssl", "rustls"] } +actix-test = { version = "0.1.0-beta.6", features = ["openssl", "rustls"] } brotli2 = "0.3.2" env_logger = "0.8" diff --git a/awc/README.md b/awc/README.md index 67bcb9659..96c5ed405 100644 --- a/awc/README.md +++ b/awc/README.md @@ -3,9 +3,9 @@ > Async HTTP and WebSocket client library. [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) -[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.9)](https://docs.rs/awc/3.0.0-beta.9) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.10)](https://docs.rs/awc/3.0.0-beta.10) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.9/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.9) +[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.10/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.10) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources diff --git a/awc/src/client/connection.rs b/awc/src/client/connection.rs index 97b96fc0a..6bbc9ad07 100644 --- a/awc/src/client/connection.rs +++ b/awc/src/client/connection.rs @@ -173,6 +173,7 @@ impl H2ConnectionInner { /// Cancel spawned connection task on drop. impl Drop for H2ConnectionInner { fn drop(&mut self) { + // TODO: this can end up sending extraneous requests; see if there is a better way to handle if self .sender .send_request(http::Request::new(()), true) @@ -183,8 +184,8 @@ impl Drop for H2ConnectionInner { } } +/// Unified connection type cover HTTP/1 Plain/TLS and HTTP/2 protocols. #[allow(dead_code)] -/// Unified connection type cover Http1 Plain/Tls and Http2 protocols pub enum Connection> where A: ConnectionIo, diff --git a/awc/tests/test_rustls_client.rs b/awc/tests/test_rustls_client.rs index c075a6090..355fcb6fb 100644 --- a/awc/tests/test_rustls_client.rs +++ b/awc/tests/test_rustls_client.rs @@ -19,8 +19,7 @@ use actix_utils::future::ok; use actix_web::{dev::AppConfig, http::Version, web, App, HttpResponse}; use rustls::{ client::{ServerCertVerified, ServerCertVerifier}, - Certificate, ClientConfig, OwnedTrustAnchor, PrivateKey, RootCertStore, ServerConfig, - ServerName, + Certificate, ClientConfig, PrivateKey, ServerConfig, ServerName, }; use rustls_pemfile::{certs, pkcs8_private_keys}; diff --git a/examples/on_connect.rs b/examples/on_connect.rs index 24ac86c6b..9709835e6 100644 --- a/examples/on_connect.rs +++ b/examples/on_connect.rs @@ -8,6 +8,7 @@ use std::{any::Any, io, net::SocketAddr}; use actix_web::{dev::Extensions, rt::net::TcpStream, web, App, HttpServer}; +#[allow(dead_code)] #[derive(Debug, Clone)] struct ConnectionInfo { bind: SocketAddr, diff --git a/src/dev.rs b/src/dev.rs index be3af86a8..4fac207a7 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -20,7 +20,7 @@ pub use actix_http::body::{AnyBody, Body, BodySize, MessageBody, ResponseBody, S pub use actix_http::encoding::Decoder as Decompress; pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, Response, ResponseHead}; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; -pub use actix_server::Server; +pub use actix_server::{Server, ServerHandle}; pub use actix_service::{ always_ready, fn_factory, fn_service, forward_ready, Service, ServiceFactory, Transform, }; diff --git a/src/server.rs b/src/server.rs index f15183f85..0f3d7c59a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -159,7 +159,7 @@ where /// /// By default max connections is set to a 25k. pub fn max_connections(mut self, num: usize) -> Self { - self.builder = self.builder.maxconn(num); + self.builder = self.builder.max_concurrent_connections(num); self } @@ -233,7 +233,7 @@ where self } - /// Stop actix system. + /// Stop Actix `System` after server shutdown. pub fn system_exit(mut self) -> Self { self.builder = self.builder.system_exit(); self diff --git a/tests/test_httpserver.rs b/tests/test_httpserver.rs index 881c6ce94..887b51d41 100644 --- a/tests/test_httpserver.rs +++ b/tests/test_httpserver.rs @@ -14,57 +14,45 @@ async fn test_start() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { - let sys = actix_rt::System::new(); + actix_rt::System::new() + .block_on(async { + let srv = HttpServer::new(|| { + App::new().service( + web::resource("/").route(web::to(|| HttpResponse::Ok().body("test"))), + ) + }) + .workers(1) + .backlog(1) + .max_connections(10) + .max_connection_rate(10) + .keep_alive(10) + .client_timeout(5000) + .client_shutdown(0) + .server_hostname("localhost") + .system_exit() + .disable_signals() + .bind(format!("{}", addr)) + .unwrap() + .run(); - sys.block_on(async { - let srv = HttpServer::new(|| { - App::new().service( - web::resource("/").route(web::to(|| HttpResponse::Ok().body("test"))), - ) + tx.send(srv.handle()).unwrap(); + + srv.await }) - .workers(1) - .backlog(1) - .max_connections(10) - .max_connection_rate(10) - .keep_alive(10) - .client_timeout(5000) - .client_shutdown(0) - .server_hostname("localhost") - .system_exit() - .disable_signals() - .bind(format!("{}", addr)) - .unwrap() - .run(); - - let _ = tx.send((srv, actix_rt::System::current())); - }); - - let _ = sys.run(); + .unwrap(); }); - let (srv, sys) = rx.recv().unwrap(); - #[cfg(feature = "client")] - { - use actix_http::client; + let srv = rx.recv().unwrap(); - let client = awc::Client::builder() - .connector( - client::Connector::new() - .timeout(Duration::from_millis(100)) - .finish(), - ) - .finish(); + let client = awc::Client::builder() + .connector(awc::Connector::new().timeout(Duration::from_millis(100))) + .finish(); - let host = format!("http://{}", addr); - let response = client.get(host.clone()).send().await.unwrap(); - assert!(response.status().is_success()); - } + let host = format!("http://{}", addr); + let response = client.get(host.clone()).send().await.unwrap(); + assert!(response.status().is_success()); - // stop - let _ = srv.stop(false); - - thread::sleep(Duration::from_millis(100)); - let _ = sys.stop(); + srv.stop(false).await; } #[cfg(feature = "openssl")] @@ -92,37 +80,38 @@ fn ssl_acceptor() -> openssl::ssl::SslAcceptorBuilder { #[cfg(feature = "openssl")] async fn test_start_ssl() { use actix_web::HttpRequest; + use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; let addr = actix_test::unused_addr(); let (tx, rx) = mpsc::channel(); thread::spawn(move || { - let sys = actix_rt::System::new(); - let builder = ssl_acceptor(); + actix_rt::System::new() + .block_on(async { + let builder = ssl_acceptor(); - let srv = HttpServer::new(|| { - App::new().service(web::resource("/").route(web::to(|req: HttpRequest| { - assert!(req.app_config().secure()); - HttpResponse::Ok().body("test") - }))) - }) - .workers(1) - .shutdown_timeout(1) - .system_exit() - .disable_signals() - .bind_openssl(format!("{}", addr), builder) - .unwrap(); + let srv = HttpServer::new(|| { + App::new().service(web::resource("/").route(web::to(|req: HttpRequest| { + assert!(req.app_config().secure()); + HttpResponse::Ok().body("test") + }))) + }) + .workers(1) + .shutdown_timeout(1) + .system_exit() + .disable_signals() + .bind_openssl(format!("{}", addr), builder) + .unwrap(); - sys.block_on(async { - let srv = srv.run(); - let _ = tx.send((srv, actix_rt::System::current())); - }); + let srv = srv.run(); + tx.send(srv.handle()).unwrap(); - let _ = sys.run(); + srv.await + }) + .unwrap() }); - let (srv, sys) = rx.recv().unwrap(); + let srv = rx.recv().unwrap(); - use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); builder.set_verify(SslVerifyMode::NONE); let _ = builder @@ -141,9 +130,5 @@ async fn test_start_ssl() { let response = client.get(host.clone()).send().await.unwrap(); assert!(response.status().is_success()); - // stop - let _ = srv.stop(false); - - thread::sleep(Duration::from_millis(100)); - let _ = sys.stop(); + srv.stop(false).await; } diff --git a/tests/test_server.rs b/tests/test_server.rs index ff6f5ae5e..d21dac8cf 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -127,6 +127,8 @@ async fn test_body() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -154,6 +156,8 @@ async fn test_body_gzip() { let mut dec = Vec::new(); e.read_to_end(&mut dec).unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -181,6 +185,8 @@ async fn test_body_gzip2() { let mut dec = Vec::new(); e.read_to_end(&mut dec).unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -241,6 +247,8 @@ async fn test_body_encoding_override() { e.write_all(bytes.as_ref()).unwrap(); let dec = e.finish().unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -275,6 +283,8 @@ async fn test_body_gzip_large() { let mut dec = Vec::new(); e.read_to_end(&mut dec).unwrap(); assert_eq!(Bytes::from(dec), Bytes::from(data)); + + srv.stop().await; } #[actix_rt::test] @@ -314,6 +324,8 @@ async fn test_body_gzip_large_random() { e.read_to_end(&mut dec).unwrap(); assert_eq!(dec.len(), data.len()); assert_eq!(Bytes::from(dec), Bytes::from(data)); + + srv.stop().await; } #[actix_rt::test] @@ -348,6 +360,8 @@ async fn test_body_chunked_implicit() { let mut dec = Vec::new(); e.read_to_end(&mut dec).unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -380,6 +394,8 @@ async fn test_body_br_streaming() { let dec = e.finish().unwrap(); println!("T: {:?}", Bytes::copy_from_slice(&dec)); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -401,6 +417,8 @@ async fn test_head_binary() { // read response let bytes = response.body().await.unwrap(); assert!(bytes.is_empty()); + + srv.stop().await; } #[actix_rt::test] @@ -420,6 +438,8 @@ async fn test_no_chunking() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -447,6 +467,8 @@ async fn test_body_deflate() { e.write_all(bytes.as_ref()).unwrap(); let dec = e.finish().unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -475,6 +497,8 @@ async fn test_body_brotli() { e.write_all(bytes.as_ref()).unwrap(); let dec = e.finish().unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -503,6 +527,8 @@ async fn test_body_zstd() { let mut dec = Vec::new(); e.read_to_end(&mut dec).unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -534,6 +560,8 @@ async fn test_body_zstd_streaming() { let mut dec = Vec::new(); e.read_to_end(&mut dec).unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -559,6 +587,8 @@ async fn test_zstd_encoding() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -594,6 +624,8 @@ async fn test_zstd_encoding_large() { // read response let bytes = response.body().limit(320_000).await.unwrap(); assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } #[actix_rt::test] @@ -619,6 +651,8 @@ async fn test_encoding() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -644,6 +678,8 @@ async fn test_gzip_encoding() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -670,6 +706,8 @@ async fn test_gzip_encoding_large() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } #[actix_rt::test] @@ -702,6 +740,8 @@ async fn test_reading_gzip_encoding_large_random() { let bytes = response.body().await.unwrap(); assert_eq!(bytes.len(), data.len()); assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } #[actix_rt::test] @@ -727,6 +767,8 @@ async fn test_reading_deflate_encoding() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -753,6 +795,8 @@ async fn test_reading_deflate_encoding_large() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } #[actix_rt::test] @@ -785,6 +829,8 @@ async fn test_reading_deflate_encoding_large_random() { let bytes = response.body().await.unwrap(); assert_eq!(bytes.len(), data.len()); assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } #[actix_rt::test] @@ -810,6 +856,8 @@ async fn test_brotli_encoding() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -845,6 +893,8 @@ async fn test_brotli_encoding_large() { // read response let bytes = response.body().limit(320_000).await.unwrap(); assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } #[cfg(feature = "openssl")] @@ -861,9 +911,9 @@ async fn test_brotli_encoding_large_openssl() { }); // body - let mut e = BrotliEncoder::new(Vec::new(), 3); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); + let mut enc = BrotliEncoder::new(Vec::new(), 3); + enc.write_all(data.as_ref()).unwrap(); + let enc = enc.finish().unwrap(); // client request let mut response = srv @@ -877,6 +927,8 @@ async fn test_brotli_encoding_large_openssl() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } #[cfg(feature = "rustls")] @@ -944,6 +996,8 @@ mod plus_rustls { let bytes = response.body().await.unwrap(); assert_eq!(bytes.len(), data.len()); assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } } @@ -998,6 +1052,8 @@ async fn test_server_cookies() { assert_eq!(cookies[0], second_cookie); assert_eq!(cookies[1], first_cookie); } + + srv.stop().await; } #[actix_rt::test] @@ -1018,6 +1074,8 @@ async fn test_slow_request() { let mut data = String::new(); let _ = stream.read_to_string(&mut data); assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); + + srv.stop().await; } #[actix_rt::test] @@ -1030,6 +1088,8 @@ async fn test_normalize() { let response = srv.get("/one/").send().await.unwrap(); assert!(response.status().is_success()); + + srv.stop().await } // allow deprecated App::data @@ -1099,4 +1159,6 @@ async fn test_accept_encoding_no_match() { .unwrap(); assert_eq!(response.status().as_u16(), 406); + + srv.stop().await; } From e8a0e168636a439a2eb37323f4acb22be685116a Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 15 Nov 2021 18:11:51 +0000 Subject: [PATCH 77/88] run tarpaulin on workspace --- .github/workflows/ci.yml | 2 +- CHANGES.md | 2 +- awc/CHANGES.md | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aff0b9348..a8b21b7fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -141,7 +141,7 @@ jobs: if: github.ref == 'refs/heads/master' run: | cargo install cargo-tarpaulin --vers "^0.13" - cargo tarpaulin --out Xml --verbose + cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose - name: Upload to Codecov if: github.ref == 'refs/heads/master' uses: codecov/codecov-action@v1 diff --git a/CHANGES.md b/CHANGES.md index 4ecbd0c2e..0cb5ccb23 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,7 +23,7 @@ ### Changed * Associated type `FromRequest::Config` was removed. [#2233] * Inner field made private on `web::Payload`. [#2384] -* `Data::into_inner` and `Data::get_ref` no longer require T: Sized. [#2403] +* `Data::into_inner` and `Data::get_ref` no longer requires `T: Sized`. [#2403] * Updated rustls to v0.20. [#2414] * Minimum supported Rust version (MSRV) is now 1.52. diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 6b9531c70..98998fd5c 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -4,6 +4,7 @@ ## 3.0.0-beta.10 - 2021-11-15 +* No significant changes from `3.0.0-beta.9`. ## 3.0.0-beta.9 - 2021-10-20 From 4df1cd78b7534cba98aaa930e3a6aaca2ca36ea3 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 16 Nov 2021 09:21:10 +0000 Subject: [PATCH 78/88] simplify `AnyBody` and `BodySize` (#2446) --- actix-http/CHANGES.md | 11 +++++++++++ actix-http/src/body/body.rs | 24 +++++++++++------------- actix-http/src/body/message_body.rs | 2 +- actix-http/src/body/mod.rs | 14 +++++++------- actix-http/src/body/size.rs | 12 ++++-------- actix-http/src/encoding/encoder.rs | 3 +-- actix-http/src/h1/dispatcher.rs | 8 ++++---- actix-http/src/h1/encoder.rs | 15 ++++++--------- actix-http/src/h2/dispatcher.rs | 4 +++- actix-http/src/response.rs | 2 +- actix-http/src/response_builder.rs | 4 ++-- awc/src/client/h1proto.rs | 4 ++-- awc/src/client/h2proto.rs | 18 +++++++++--------- awc/src/middleware/redirect.rs | 2 +- awc/src/sender.rs | 2 +- src/response/builder.rs | 4 ++-- src/response/http_codes.rs | 3 +-- 17 files changed, 67 insertions(+), 65 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 9ec75b4bc..2beda3dcc 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,17 @@ # Changes ## Unreleased - 2021-xx-xx +### Added +* `AnyBody::empty` for quickly creating an empty body. [#2446] + +### Changed +* Rename `AnyBody::{Message => Stream}`. [#2446] + +### Removed +* `AnyBody::Empty`; an empty body can now only be represented as a zero-length `Bytes` variant. [#2446] +* `BodySize::Empty`; an empty body can now only be represented as a `Sized(0)` variant. [#2446] + +[#2446]: https://github.com/actix/actix-web/pull/2446 ## 3.0.0-beta.12 - 2021-11-15 diff --git a/actix-http/src/body/body.rs b/actix-http/src/body/body.rs index cd3e4c5c4..32b464486 100644 --- a/actix-http/src/body/body.rs +++ b/actix-http/src/body/body.rs @@ -20,17 +20,19 @@ pub enum AnyBody { /// Empty response. `Content-Length` header is not set. None, - /// Zero sized response body. `Content-Length` header is set to `0`. - Empty, - /// Specific response body. Bytes(Bytes), /// Generic message body. - Message(BoxAnyBody), + Stream(BoxAnyBody), } impl AnyBody { + /// Constructs a new, empty body. + pub fn empty() -> Self { + Self::Bytes(Bytes::new()) + } + /// Create body from slice (copy) pub fn from_slice(s: &[u8]) -> Self { Self::Bytes(Bytes::copy_from_slice(s)) @@ -42,7 +44,7 @@ impl AnyBody { B: MessageBody + 'static, B::Error: Into>, { - Self::Message(BoxAnyBody::from_body(body)) + Self::Stream(BoxAnyBody::from_body(body)) } } @@ -52,9 +54,8 @@ impl MessageBody for AnyBody { fn size(&self) -> BodySize { match self { AnyBody::None => BodySize::None, - AnyBody::Empty => BodySize::Empty, AnyBody::Bytes(ref bin) => BodySize::Sized(bin.len() as u64), - AnyBody::Message(ref body) => body.size(), + AnyBody::Stream(ref body) => body.size(), } } @@ -64,7 +65,6 @@ impl MessageBody for AnyBody { ) -> Poll>> { match self.get_mut() { AnyBody::None => Poll::Ready(None), - AnyBody::Empty => Poll::Ready(None), AnyBody::Bytes(ref mut bin) => { let len = bin.len(); if len == 0 { @@ -74,7 +74,7 @@ impl MessageBody for AnyBody { } } - AnyBody::Message(body) => body + AnyBody::Stream(body) => body .as_pin_mut() .poll_next(cx) .map_err(|err| Error::new_body().with_cause(err)), @@ -86,12 +86,11 @@ impl PartialEq for AnyBody { fn eq(&self, other: &Body) -> bool { match *self { AnyBody::None => matches!(*other, AnyBody::None), - AnyBody::Empty => matches!(*other, AnyBody::Empty), AnyBody::Bytes(ref b) => match *other { AnyBody::Bytes(ref b2) => b == b2, _ => false, }, - AnyBody::Message(_) => false, + AnyBody::Stream(_) => false, } } } @@ -100,9 +99,8 @@ impl fmt::Debug for AnyBody { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { AnyBody::None => write!(f, "AnyBody::None"), - AnyBody::Empty => write!(f, "AnyBody::Empty"), AnyBody::Bytes(ref b) => write!(f, "AnyBody::Bytes({:?})", b), - AnyBody::Message(_) => write!(f, "AnyBody::Message(_)"), + AnyBody::Stream(_) => write!(f, "AnyBody::Message(_)"), } } } diff --git a/actix-http/src/body/message_body.rs b/actix-http/src/body/message_body.rs index edb4c550c..62a7e9b1c 100644 --- a/actix-http/src/body/message_body.rs +++ b/actix-http/src/body/message_body.rs @@ -31,7 +31,7 @@ impl MessageBody for () { type Error = Infallible; fn size(&self) -> BodySize { - BodySize::Empty + BodySize::Sized(0) } fn poll_next( diff --git a/actix-http/src/body/mod.rs b/actix-http/src/body/mod.rs index a60a8895c..0d5b0f079 100644 --- a/actix-http/src/body/mod.rs +++ b/actix-http/src/body/mod.rs @@ -33,7 +33,7 @@ pub use self::sized_stream::SizedStream; /// use bytes::Bytes; /// /// # async fn test_to_bytes() { -/// let body = Body::Empty; +/// let body = Body::None; /// let bytes = to_bytes(body).await.unwrap(); /// assert!(bytes.is_empty()); /// @@ -44,8 +44,9 @@ pub use self::sized_stream::SizedStream; /// ``` pub async fn to_bytes(body: B) -> Result { let cap = match body.size() { - BodySize::None | BodySize::Empty | BodySize::Sized(0) => return Ok(Bytes::new()), + BodySize::None | BodySize::Sized(0) => return Ok(Bytes::new()), BodySize::Sized(size) => size as usize, + // good enough first guess for chunk size BodySize::Stream => 32_768, }; @@ -184,7 +185,7 @@ mod tests { #[actix_rt::test] async fn test_unit() { - assert_eq!(().size(), BodySize::Empty); + assert_eq!(().size(), BodySize::Sized(0)); assert!(poll_fn(|cx| Pin::new(&mut ()).poll_next(cx)) .await .is_none()); @@ -194,11 +195,11 @@ mod tests { async fn test_box_and_pin() { let val = Box::new(()); pin!(val); - assert_eq!(val.size(), BodySize::Empty); + assert_eq!(val.size(), BodySize::Sized(0)); assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none()); let mut val = Box::pin(()); - assert_eq!(val.size(), BodySize::Empty); + assert_eq!(val.size(), BodySize::Sized(0)); assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none()); } @@ -214,7 +215,6 @@ mod tests { #[actix_rt::test] async fn test_body_debug() { assert!(format!("{:?}", Body::None).contains("Body::None")); - assert!(format!("{:?}", Body::Empty).contains("Body::Empty")); assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains('1')); } @@ -252,7 +252,7 @@ mod tests { #[actix_rt::test] async fn test_to_bytes() { - let body = Body::Empty; + let body = Body::empty(); let bytes = to_bytes(body).await.unwrap(); assert!(bytes.is_empty()); diff --git a/actix-http/src/body/size.rs b/actix-http/src/body/size.rs index 775d5b8f1..e238eadac 100644 --- a/actix-http/src/body/size.rs +++ b/actix-http/src/body/size.rs @@ -6,14 +6,9 @@ pub enum BodySize { /// Will skip writing Content-Length header. None, - /// Zero size body. - /// - /// Will write `Content-Length: 0` header. - Empty, - /// Known size body. /// - /// Will write `Content-Length: N` header. `Sized(0)` is treated the same as `Empty`. + /// Will write `Content-Length: N` header. Sized(u64), /// Unknown size body. @@ -25,16 +20,17 @@ pub enum BodySize { impl BodySize { /// Returns true if size hint indicates no or empty body. /// + /// Streams will return false because it cannot be known without reading the stream. + /// /// ``` /// # use actix_http::body::BodySize; /// assert!(BodySize::None.is_eof()); - /// assert!(BodySize::Empty.is_eof()); /// assert!(BodySize::Sized(0).is_eof()); /// /// assert!(!BodySize::Sized(64).is_eof()); /// assert!(!BodySize::Stream.is_eof()); /// ``` pub fn is_eof(&self) -> bool { - matches!(self, BodySize::None | BodySize::Empty | BodySize::Sized(0)) + matches!(self, BodySize::None | BodySize::Sized(0)) } } diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index abd8cedba..6cb034b76 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -61,7 +61,6 @@ impl Encoder { let body = match body { ResponseBody::Other(b) => match b { Body::None => return ResponseBody::Other(Body::None), - Body::Empty => return ResponseBody::Other(Body::Empty), Body::Bytes(buf) => { if can_encode { EncoderBody::Bytes(buf) @@ -69,7 +68,7 @@ impl Encoder { return ResponseBody::Other(Body::Bytes(buf)); } } - Body::Message(stream) => EncoderBody::BoxedStream(stream), + Body::Stream(stream) => EncoderBody::BoxedStream(stream), }, ResponseBody::Body(stream) => EncoderBody::Stream(stream), }; diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 69530ed11..844bc61ea 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -325,7 +325,7 @@ where ) -> Result<(), DispatchError> { let size = self.as_mut().send_response_inner(message, &body)?; let state = match size { - BodySize::None | BodySize::Empty => State::None, + BodySize::None | BodySize::Sized(0) => State::None, _ => State::SendPayload(body), }; self.project().state.set(state); @@ -339,7 +339,7 @@ where ) -> Result<(), DispatchError> { let size = self.as_mut().send_response_inner(message, &body)?; let state = match size { - BodySize::None | BodySize::Empty => State::None, + BodySize::None | BodySize::Sized(0) => State::None, _ => State::SendErrorPayload(body), }; self.project().state.set(state); @@ -380,7 +380,7 @@ where // send_response would update InnerDispatcher state to SendPayload or // None(If response body is empty). // continue loop to poll it. - self.as_mut().send_error_response(res, AnyBody::Empty)?; + self.as_mut().send_error_response(res, AnyBody::empty())?; } // return with upgrade request and poll it exclusively. @@ -772,7 +772,7 @@ where trace!("Slow request timeout"); let _ = self.as_mut().send_error_response( Response::with_body(StatusCode::REQUEST_TIMEOUT, ()), - AnyBody::Empty, + AnyBody::empty(), ); this = self.project(); this.flags.insert(Flags::STARTED | Flags::SHUTDOWN); diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index ead14206b..e07c32956 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -93,13 +93,10 @@ pub(crate) trait MessageType: Sized { dst.put_slice(b"\r\n"); } } - BodySize::Empty => { - if camel_case { - dst.put_slice(b"\r\nContent-Length: 0\r\n"); - } else { - dst.put_slice(b"\r\ncontent-length: 0\r\n"); - } + BodySize::Sized(0) if camel_case => { + dst.put_slice(b"\r\nContent-Length: 0\r\n") } + BodySize::Sized(0) => dst.put_slice(b"\r\ncontent-length: 0\r\n"), BodySize::Sized(len) => helpers::write_content_length(len, dst), BodySize::None => dst.put_slice(b"\r\n"), } @@ -336,7 +333,7 @@ impl MessageEncoder { // transfer encoding if !head { self.te = match length { - BodySize::Empty => TransferEncoding::empty(), + BodySize::Sized(0) => TransferEncoding::empty(), BodySize::Sized(len) => TransferEncoding::length(len), BodySize::Stream => { if message.chunked() && !stream { @@ -553,7 +550,7 @@ mod tests { let _ = head.encode_headers( &mut bytes, Version::HTTP_11, - BodySize::Empty, + BodySize::Sized(0), ConnectionType::Close, &ServiceConfig::default(), ); @@ -624,7 +621,7 @@ mod tests { let _ = head.encode_headers( &mut bytes, Version::HTTP_11, - BodySize::Empty, + BodySize::Sized(0), ConnectionType::Close, &ServiceConfig::default(), ); diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index 7326dfff1..8b922b2cd 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -285,9 +285,11 @@ fn prepare_response( let _ = match size { BodySize::None | BodySize::Stream => None, - BodySize::Empty => res + + BodySize::Sized(0) => res .headers_mut() .insert(CONTENT_LENGTH, HeaderValue::from_static("0")), + BodySize::Sized(len) => { let mut buf = itoa::Buffer::new(); diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs index 2aa38c153..47f1c37e2 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/response.rs @@ -28,7 +28,7 @@ impl Response { pub fn new(status: StatusCode) -> Self { Response { head: BoxedResponseHead::new(status), - body: AnyBody::Empty, + body: AnyBody::empty(), } } diff --git a/actix-http/src/response_builder.rs b/actix-http/src/response_builder.rs index e46d9a28c..a1cb1a423 100644 --- a/actix-http/src/response_builder.rs +++ b/actix-http/src/response_builder.rs @@ -270,7 +270,7 @@ impl ResponseBuilder { /// This `ResponseBuilder` will be left in a useless state. #[inline] pub fn finish(&mut self) -> Response { - self.body(AnyBody::Empty) + self.body(AnyBody::empty()) } /// Create an owned `ResponseBuilder`, leaving the original in a useless state. @@ -390,7 +390,7 @@ mod tests { fn test_content_type() { let resp = Response::build(StatusCode::OK) .content_type("text/plain") - .body(Body::Empty); + .body(Body::empty()); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") } diff --git a/awc/src/client/h1proto.rs b/awc/src/client/h1proto.rs index 3c2bb7cc1..7f3ba1b6e 100644 --- a/awc/src/client/h1proto.rs +++ b/awc/src/client/h1proto.rs @@ -70,7 +70,7 @@ where // RFC: https://tools.ietf.org/html/rfc7231#section-5.1.1 let is_expect = if head.as_ref().headers.contains_key(EXPECT) { match body.size() { - BodySize::None | BodySize::Empty | BodySize::Sized(0) => { + BodySize::None | BodySize::Sized(0) => { let keep_alive = framed.codec_ref().keepalive(); framed.io_mut().on_release(keep_alive); @@ -104,7 +104,7 @@ where if do_send { // send request body match body.size() { - BodySize::None | BodySize::Empty | BodySize::Sized(0) => {} + BodySize::None | BodySize::Sized(0) => {} _ => send_body(body, pin_framed.as_mut()).await?, }; diff --git a/awc/src/client/h2proto.rs b/awc/src/client/h2proto.rs index feb2dbd06..2618e1908 100644 --- a/awc/src/client/h2proto.rs +++ b/awc/src/client/h2proto.rs @@ -36,10 +36,7 @@ where let head_req = head.as_ref().method == Method::HEAD; let length = body.size(); - let eof = matches!( - length, - BodySize::None | BodySize::Empty | BodySize::Sized(0) - ); + let eof = matches!(length, BodySize::None | BodySize::Sized(0)); let mut req = Request::new(()); *req.uri_mut() = head.as_ref().uri.clone(); @@ -52,13 +49,11 @@ where // Content length let _ = match length { BodySize::None => None, - BodySize::Stream => { - skip_len = false; - None - } - BodySize::Empty => req + + BodySize::Sized(0) => req .headers_mut() .insert(CONTENT_LENGTH, HeaderValue::from_static("0")), + BodySize::Sized(len) => { let mut buf = itoa::Buffer::new(); @@ -67,6 +62,11 @@ where HeaderValue::from_str(buf.format(len)).unwrap(), ) } + + BodySize::Stream => { + skip_len = false; + None + } }; // Extracting extra headers from RequestHeadType. HeaderMap::new() does not allocate. diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs index 8a79a6596..f01136d14 100644 --- a/awc/src/middleware/redirect.rs +++ b/awc/src/middleware/redirect.rs @@ -194,7 +194,7 @@ where match body { Some(ref bytes) => Body::Bytes(bytes.clone()), // TODO: should this be Body::Empty or Body::None. - _ => Body::Empty, + _ => Body::empty(), } } else { body = None; diff --git a/awc/src/sender.rs b/awc/src/sender.rs index c0639606e..fcd0c71af 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -297,7 +297,7 @@ impl RequestSender { timeout: Option, config: &ClientConfig, ) -> SendClientRequest { - self.send_body(addr, response_decompress, timeout, config, Body::Empty) + self.send_body(addr, response_decompress, timeout, config, Body::empty()) } fn set_header_if_none(&mut self, key: HeaderName, value: V) -> Result<(), HttpError> diff --git a/src/response/builder.rs b/src/response/builder.rs index 56d30d9d0..f6099a019 100644 --- a/src/response/builder.rs +++ b/src/response/builder.rs @@ -387,7 +387,7 @@ impl HttpResponseBuilder { /// `HttpResponseBuilder` can not be used after this call. #[inline] pub fn finish(&mut self) -> HttpResponse { - self.body(AnyBody::Empty) + self.body(AnyBody::empty()) } /// This method construct new `HttpResponseBuilder` @@ -475,7 +475,7 @@ mod tests { fn test_content_type() { let resp = HttpResponseBuilder::new(StatusCode::OK) .content_type("text/plain") - .body(Body::Empty); + .body(Body::empty()); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") } diff --git a/src/response/http_codes.rs b/src/response/http_codes.rs index d67ef3f92..44ddb78f9 100644 --- a/src/response/http_codes.rs +++ b/src/response/http_codes.rs @@ -87,13 +87,12 @@ impl HttpResponse { #[cfg(test)] mod tests { - use crate::dev::Body; use crate::http::StatusCode; use crate::HttpResponse; #[test] fn test_build() { - let resp = HttpResponse::Ok().body(Body::Empty); + let resp = HttpResponse::Ok().finish(); assert_eq!(resp.status(), StatusCode::OK); } } From 13cf5a9e445b31a6b418bc3fb47555b1e45aa5c3 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 16 Nov 2021 16:55:45 +0000 Subject: [PATCH 79/88] remove chunked encoding header for websockets --- actix-http/src/ws/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/actix-http/src/ws/mod.rs b/actix-http/src/ws/mod.rs index 7df924cf5..70e0e62a2 100644 --- a/actix-http/src/ws/mod.rs +++ b/actix-http/src/ws/mod.rs @@ -210,7 +210,6 @@ pub fn handshake_response(req: &RequestHead) -> ResponseBuilder { Response::build(StatusCode::SWITCHING_PROTOCOLS) .upgrade("websocket") - .insert_header((header::TRANSFER_ENCODING, "chunked")) .insert_header(( header::SEC_WEBSOCKET_ACCEPT, // key is known to be header value safe ascii From d8cbb879dde831f5b6083664660e90ab1ecc584d Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 16 Nov 2021 21:41:35 +0000 Subject: [PATCH 80/88] make `AnyBody` generic on `Body` type (#2448) --- CHANGES.md | 10 ++ actix-http/CHANGES.md | 17 ++- actix-http/Cargo.toml | 1 + actix-http/src/body/body.rs | 174 ++++++++++++++++++++------- actix-http/src/body/body_stream.rs | 36 ++++++ actix-http/src/body/mod.rs | 8 +- actix-http/src/body/response_body.rs | 84 ------------- actix-http/src/body/sized_stream.rs | 45 +++++++ actix-http/src/encoding/encoder.rs | 39 +++--- actix-http/src/h1/dispatcher.rs | 2 +- actix-http/src/response_builder.rs | 2 +- awc/src/sender.rs | 4 +- src/dev.rs | 2 +- src/middleware/compat.rs | 4 +- src/middleware/compress.rs | 31 +++-- src/responder.rs | 27 +---- src/response/builder.rs | 4 +- src/response/response.rs | 3 + 18 files changed, 283 insertions(+), 210 deletions(-) delete mode 100644 actix-http/src/body/response_body.rs diff --git a/CHANGES.md b/CHANGES.md index 0cb5ccb23..784500d9e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,16 @@ # Changes ## Unreleased - 2021-xx-xx +### Changed +* Compress middleware's response type is now `AnyBody>`. [#2448] + +### Fixed +* Relax `Unpin` bound on `S` (stream) parameter of `HttpResponseBuilder::streaming`. [#2448] + +### Removed +* `dev::ResponseBody` re-export; is function is replaced by the new `dev::AnyBody` enum. [#2446] + +[#2423]: https://github.com/actix/actix-web/pull/2423 ## 4.0.0-beta.11 - 2021-11-15 diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 2beda3dcc..71cdd6d4c 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -2,14 +2,23 @@ ## Unreleased - 2021-xx-xx ### Added -* `AnyBody::empty` for quickly creating an empty body. [#2446] +* `body::AnyBody::empty` for quickly creating an empty body. [#2446] +* `impl Clone` for `body::AnyBody where S: Clone`. [#2448] +* `body::AnyBody::into_boxed` for quickly converting to a type-erased, boxed body type. [#2448] ### Changed -* Rename `AnyBody::{Message => Stream}`. [#2446] +* Rename `body::AnyBody::{Message => Body}`. [#2446] +* Rename `body::AnyBody::{from_message => new_boxed}`. [#2448] +* Rename `body::AnyBody::{from_slice => copy_from_slice}`. [#2448] +* Rename `body::{BoxAnyBody => BoxBody}`. [#2448] +* Change representation of `AnyBody` to include a type parameter in `Body` variant. Defaults to `BoxBody`. [#2448] +* `Encoder::response` now returns `AnyBody>`. [#2448] ### Removed -* `AnyBody::Empty`; an empty body can now only be represented as a zero-length `Bytes` variant. [#2446] -* `BodySize::Empty`; an empty body can now only be represented as a `Sized(0)` variant. [#2446] +* `body::AnyBody::Empty`; an empty body can now only be represented as a zero-length `Bytes` variant. [#2446] +* `body::BodySize::Empty`; an empty body can now only be represented as a `Sized(0)` variant. [#2446] +* `EncoderError::Boxed`; it is no longer required. [#2446] +* `body::ResponseBody`; is function is replaced by the new `body::AnyBody` enum. [#2446] [#2446]: https://github.com/actix/actix-web/pull/2446 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 784312445..27a147379 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -92,6 +92,7 @@ regex = "1.3" rustls-pemfile = "0.2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +static_assertions = "1" tls-openssl = { package = "openssl", version = "0.10.9" } tls-rustls = { package = "rustls", version = "0.20.0" } tokio = { version = "1.2", features = ["net", "rt"] } diff --git a/actix-http/src/body/body.rs b/actix-http/src/body/body.rs index 32b464486..d51173a57 100644 --- a/actix-http/src/body/body.rs +++ b/actix-http/src/body/body.rs @@ -8,6 +8,7 @@ use std::{ use bytes::{Bytes, BytesMut}; use futures_core::Stream; +use pin_project::pin_project; use crate::error::Error; @@ -16,15 +17,17 @@ use super::{BodySize, BodyStream, MessageBody, MessageBodyMapErr, SizedStream}; pub type Body = AnyBody; /// Represents various types of HTTP message body. -pub enum AnyBody { +#[pin_project(project = AnyBodyProj)] +#[derive(Clone)] +pub enum AnyBody { /// Empty response. `Content-Length` header is not set. None, - /// Specific response body. + /// Complete, in-memory response body. Bytes(Bytes), - /// Generic message body. - Stream(BoxAnyBody), + /// Generic / Other message body. + Body(#[pin] B), } impl AnyBody { @@ -33,29 +36,60 @@ impl AnyBody { Self::Bytes(Bytes::new()) } - /// Create body from slice (copy) - pub fn from_slice(s: &[u8]) -> Self { - Self::Bytes(Bytes::copy_from_slice(s)) - } - - /// Create body from generic message body. - pub fn from_message(body: B) -> Self + /// Create boxed body from generic message body. + pub fn new_boxed(body: B) -> Self where B: MessageBody + 'static, B::Error: Into>, { - Self::Stream(BoxAnyBody::from_body(body)) + Self::Body(BoxBody::from_body(body)) + } + + /// Constructs new `AnyBody` instance from a slice of bytes by copying it. + /// + /// If your bytes container is owned, it may be cheaper to use a `From` impl. + pub fn copy_from_slice(s: &[u8]) -> Self { + Self::Bytes(Bytes::copy_from_slice(s)) + } + + #[doc(hidden)] + #[deprecated(since = "4.0.0", note = "Renamed to `copy_from_slice`.")] + pub fn from_slice(s: &[u8]) -> Self { + Self::Bytes(Bytes::copy_from_slice(s)) } } -impl MessageBody for AnyBody { +impl AnyBody +where + B: MessageBody + 'static, + B::Error: Into>, +{ + /// Create body from generic message body. + pub fn new(body: B) -> Self { + Self::Body(body) + } + + pub fn into_boxed(self) -> AnyBody { + match self { + Self::None => AnyBody::None, + Self::Bytes(bytes) => AnyBody::Bytes(bytes), + Self::Body(body) => AnyBody::new_boxed(body), + } + } +} + +impl MessageBody for AnyBody +where + B: MessageBody, + B::Error: Into> + 'static, +{ type Error = Error; fn size(&self) -> BodySize { match self { AnyBody::None => BodySize::None, AnyBody::Bytes(ref bin) => BodySize::Sized(bin.len() as u64), - AnyBody::Stream(ref body) => body.size(), + AnyBody::Body(ref body) => body.size(), } } @@ -63,9 +97,9 @@ impl MessageBody for AnyBody { self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll>> { - match self.get_mut() { - AnyBody::None => Poll::Ready(None), - AnyBody::Bytes(ref mut bin) => { + match self.project() { + AnyBodyProj::None => Poll::Ready(None), + AnyBodyProj::Bytes(bin) => { let len = bin.len(); if len == 0 { Poll::Ready(None) @@ -74,8 +108,7 @@ impl MessageBody for AnyBody { } } - AnyBody::Stream(body) => body - .as_pin_mut() + AnyBodyProj::Body(body) => body .poll_next(cx) .map_err(|err| Error::new_body().with_cause(err)), } @@ -90,30 +123,30 @@ impl PartialEq for AnyBody { AnyBody::Bytes(ref b2) => b == b2, _ => false, }, - AnyBody::Stream(_) => false, + AnyBody::Body(_) => false, } } } -impl fmt::Debug for AnyBody { +impl fmt::Debug for AnyBody { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { AnyBody::None => write!(f, "AnyBody::None"), - AnyBody::Bytes(ref b) => write!(f, "AnyBody::Bytes({:?})", b), - AnyBody::Stream(_) => write!(f, "AnyBody::Message(_)"), + AnyBody::Bytes(ref bytes) => write!(f, "AnyBody::Bytes({:?})", bytes), + AnyBody::Body(ref stream) => write!(f, "AnyBody::Message({:?})", stream), } } } impl From<&'static str> for AnyBody { - fn from(s: &'static str) -> Body { - AnyBody::Bytes(Bytes::from_static(s.as_ref())) + fn from(string: &'static str) -> Body { + AnyBody::Bytes(Bytes::from_static(string.as_ref())) } } impl From<&'static [u8]> for AnyBody { - fn from(s: &'static [u8]) -> Body { - AnyBody::Bytes(Bytes::from_static(s)) + fn from(bytes: &'static [u8]) -> Body { + AnyBody::Bytes(Bytes::from_static(bytes)) } } @@ -124,20 +157,20 @@ impl From> for AnyBody { } impl From for AnyBody { - fn from(s: String) -> Body { - s.into_bytes().into() + fn from(string: String) -> Body { + string.into_bytes().into() } } impl From<&'_ String> for AnyBody { - fn from(s: &String) -> Body { - AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&s))) + fn from(string: &String) -> Body { + AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&string))) } } impl From> for AnyBody { - fn from(s: Cow<'_, str>) -> Body { - match s { + fn from(string: Cow<'_, str>) -> Body { + match string { Cow::Owned(s) => AnyBody::from(s), Cow::Borrowed(s) => { AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s))) @@ -147,14 +180,14 @@ impl From> for AnyBody { } impl From for AnyBody { - fn from(s: Bytes) -> Body { - AnyBody::Bytes(s) + fn from(bytes: Bytes) -> Body { + AnyBody::Bytes(bytes) } } impl From for AnyBody { - fn from(s: BytesMut) -> Body { - AnyBody::Bytes(s.freeze()) + fn from(bytes: BytesMut) -> Body { + AnyBody::Bytes(bytes.freeze()) } } @@ -163,8 +196,8 @@ where S: Stream> + 'static, E: Into> + 'static, { - fn from(s: SizedStream) -> Body { - AnyBody::from_message(s) + fn from(stream: SizedStream) -> Body { + AnyBody::new_boxed(stream) } } @@ -173,15 +206,15 @@ where S: Stream> + 'static, E: Into> + 'static, { - fn from(s: BodyStream) -> Body { - AnyBody::from_message(s) + fn from(stream: BodyStream) -> Body { + AnyBody::new_boxed(stream) } } /// A boxed message body with boxed errors. -pub struct BoxAnyBody(Pin>>>); +pub struct BoxBody(Pin>>>); -impl BoxAnyBody { +impl BoxBody { /// Boxes a `MessageBody` and any errors it generates. pub fn from_body(body: B) -> Self where @@ -195,18 +228,18 @@ impl BoxAnyBody { /// Returns a mutable pinned reference to the inner message body type. pub fn as_pin_mut( &mut self, - ) -> Pin<&mut (dyn MessageBody>)> { + ) -> Pin<&mut (dyn MessageBody>)> { self.0.as_mut() } } -impl fmt::Debug for BoxAnyBody { +impl fmt::Debug for BoxBody { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("BoxAnyBody(dyn MessageBody)") } } -impl MessageBody for BoxAnyBody { +impl MessageBody for BoxBody { type Error = Error; fn size(&self) -> BodySize { @@ -223,3 +256,52 @@ impl MessageBody for BoxAnyBody { .map_err(|err| Error::new_body().with_cause(err)) } } + +#[cfg(test)] +mod tests { + use std::marker::PhantomPinned; + + use static_assertions::{assert_impl_all, assert_not_impl_all}; + + use super::*; + use crate::body::to_bytes; + + struct PinType(PhantomPinned); + + impl MessageBody for PinType { + type Error = crate::Error; + + fn size(&self) -> BodySize { + unimplemented!() + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + unimplemented!() + } + } + + assert_impl_all!(AnyBody<()>: MessageBody, fmt::Debug, Send, Sync, Unpin); + assert_impl_all!(AnyBody>: MessageBody, fmt::Debug, Send, Sync, Unpin); + assert_impl_all!(AnyBody: MessageBody, fmt::Debug, Send, Sync, Unpin); + assert_impl_all!(AnyBody: MessageBody, fmt::Debug, Unpin); + assert_impl_all!(BoxBody: MessageBody, fmt::Debug, Unpin); + assert_impl_all!(AnyBody: MessageBody); + + assert_not_impl_all!(AnyBody: Send, Sync, Unpin); + assert_not_impl_all!(BoxBody: Send, Sync, Unpin); + assert_not_impl_all!(AnyBody: Send, Sync, Unpin); + + #[actix_rt::test] + async fn nested_boxed_body() { + let body = AnyBody::copy_from_slice(&[1, 2, 3]); + let boxed_body = BoxBody::from_body(BoxBody::from_body(body)); + + assert_eq!( + to_bytes(boxed_body).await.unwrap(), + Bytes::from(vec![1, 2, 3]), + ); + } +} diff --git a/actix-http/src/body/body_stream.rs b/actix-http/src/body/body_stream.rs index f726f4475..31de9b48f 100644 --- a/actix-http/src/body/body_stream.rs +++ b/actix-http/src/body/body_stream.rs @@ -75,10 +75,22 @@ mod tests { use derive_more::{Display, Error}; use futures_core::ready; use futures_util::{stream, FutureExt as _}; + use static_assertions::{assert_impl_all, assert_not_impl_all}; use super::*; use crate::body::to_bytes; + assert_impl_all!(BodyStream>>: MessageBody); + assert_impl_all!(BodyStream>>: MessageBody); + assert_impl_all!(BodyStream>>: MessageBody); + assert_impl_all!(BodyStream>>: MessageBody); + assert_impl_all!(BodyStream>>: MessageBody); + + assert_not_impl_all!(BodyStream>: MessageBody); + assert_not_impl_all!(BodyStream>: MessageBody); + // crate::Error is not Clone + assert_not_impl_all!(BodyStream>>: MessageBody); + #[actix_rt::test] async fn skips_empty_chunks() { let body = BodyStream::new(stream::iter( @@ -124,6 +136,30 @@ mod tests { assert!(matches!(to_bytes(body).await, Err(StreamErr))); } + #[actix_rt::test] + async fn stream_string_error() { + // `&'static str` does not impl `Error` + // but it does impl `Into>` + + let body = BodyStream::new(stream::once(async { Err("stringy error") })); + assert!(matches!(to_bytes(body).await, Err("stringy error"))); + } + + #[actix_rt::test] + async fn stream_boxed_error() { + // `Box` does not impl `Error` + // but it does impl `Into>` + + let body = BodyStream::new(stream::once(async { + Err(Box::::from("stringy error")) + })); + + assert_eq!( + to_bytes(body).await.unwrap_err().to_string(), + "stringy error" + ); + } + #[actix_rt::test] async fn stream_delayed_error() { let body = diff --git a/actix-http/src/body/mod.rs b/actix-http/src/body/mod.rs index 0d5b0f079..07e5e67ce 100644 --- a/actix-http/src/body/mod.rs +++ b/actix-http/src/body/mod.rs @@ -11,15 +11,13 @@ use futures_core::ready; mod body; mod body_stream; mod message_body; -mod response_body; mod size; mod sized_stream; -pub use self::body::{AnyBody, Body, BoxAnyBody}; +pub use self::body::{AnyBody, Body, BoxBody}; pub use self::body_stream::BodyStream; pub use self::message_body::MessageBody; pub(crate) use self::message_body::MessageBodyMapErr; -pub use self::response_body::ResponseBody; pub use self::size::BodySize; pub use self::sized_stream::SizedStream; @@ -108,10 +106,10 @@ mod tests { assert_eq!(Body::from(b"test".as_ref()).size(), BodySize::Sized(4)); assert_eq!(Body::from(b"test".as_ref()).get_ref(), b"test"); assert_eq!( - Body::from_slice(b"test".as_ref()).size(), + Body::copy_from_slice(b"test".as_ref()).size(), BodySize::Sized(4) ); - assert_eq!(Body::from_slice(b"test".as_ref()).get_ref(), b"test"); + assert_eq!(Body::copy_from_slice(b"test".as_ref()).get_ref(), b"test"); let sb = Bytes::from(&b"test"[..]); pin!(sb); diff --git a/actix-http/src/body/response_body.rs b/actix-http/src/body/response_body.rs deleted file mode 100644 index 699ea9384..000000000 --- a/actix-http/src/body/response_body.rs +++ /dev/null @@ -1,84 +0,0 @@ -use std::{ - mem, - pin::Pin, - task::{Context, Poll}, -}; - -use bytes::Bytes; -use futures_core::Stream; -use pin_project::pin_project; - -use crate::error::Error; - -use super::{Body, BodySize, MessageBody}; - -#[pin_project(project = ResponseBodyProj)] -pub enum ResponseBody { - Body(#[pin] B), - Other(Body), -} - -impl ResponseBody { - pub fn into_body(self) -> ResponseBody { - match self { - ResponseBody::Body(b) => ResponseBody::Other(b), - ResponseBody::Other(b) => ResponseBody::Other(b), - } - } -} - -impl ResponseBody { - pub fn take_body(&mut self) -> ResponseBody { - mem::replace(self, ResponseBody::Other(Body::None)) - } -} - -impl ResponseBody { - pub fn as_ref(&self) -> Option<&B> { - if let ResponseBody::Body(ref b) = self { - Some(b) - } else { - None - } - } -} - -impl MessageBody for ResponseBody -where - B: MessageBody, - B::Error: Into, -{ - type Error = Error; - - fn size(&self) -> BodySize { - match self { - ResponseBody::Body(ref body) => body.size(), - ResponseBody::Other(ref body) => body.size(), - } - } - - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - Stream::poll_next(self, cx) - } -} - -impl Stream for ResponseBody -where - B: MessageBody, - B::Error: Into, -{ - type Item = Result; - - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - match self.project() { - ResponseBodyProj::Body(body) => body.poll_next(cx).map_err(Into::into), - ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx), - } - } -} diff --git a/actix-http/src/body/sized_stream.rs b/actix-http/src/body/sized_stream.rs index b6ceb32fe..b92de44cc 100644 --- a/actix-http/src/body/sized_stream.rs +++ b/actix-http/src/body/sized_stream.rs @@ -72,10 +72,22 @@ mod tests { use actix_rt::pin; use actix_utils::future::poll_fn; use futures_util::stream; + use static_assertions::{assert_impl_all, assert_not_impl_all}; use super::*; use crate::body::to_bytes; + assert_impl_all!(SizedStream>>: MessageBody); + assert_impl_all!(SizedStream>>: MessageBody); + assert_impl_all!(SizedStream>>: MessageBody); + assert_impl_all!(SizedStream>>: MessageBody); + assert_impl_all!(SizedStream>>: MessageBody); + + assert_not_impl_all!(SizedStream>: MessageBody); + assert_not_impl_all!(SizedStream>: MessageBody); + // crate::Error is not Clone + assert_not_impl_all!(SizedStream>>: MessageBody); + #[actix_rt::test] async fn skips_empty_chunks() { let body = SizedStream::new( @@ -119,4 +131,37 @@ mod tests { assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12"))); } + + #[actix_rt::test] + async fn stream_string_error() { + // `&'static str` does not impl `Error` + // but it does impl `Into>` + + let body = SizedStream::new(0, stream::once(async { Err("stringy error") })); + assert_eq!(to_bytes(body).await, Ok(Bytes::new())); + + let body = SizedStream::new(1, stream::once(async { Err("stringy error") })); + assert!(matches!(to_bytes(body).await, Err("stringy error"))); + } + + #[actix_rt::test] + async fn stream_boxed_error() { + // `Box` does not impl `Error` + // but it does impl `Into>` + + let body = SizedStream::new( + 0, + stream::once(async { Err(Box::::from("stringy error")) }), + ); + assert_eq!(to_bytes(body).await.unwrap(), Bytes::new()); + + let body = SizedStream::new( + 1, + stream::once(async { Err(Box::::from("stringy error")) }), + ); + assert_eq!( + to_bytes(body).await.unwrap_err().to_string(), + "stringy error" + ); + } } diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index 6cb034b76..62100ff1d 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -24,7 +24,7 @@ use flate2::write::{GzEncoder, ZlibEncoder}; use zstd::stream::write::Encoder as ZstdEncoder; use crate::{ - body::{Body, BodySize, BoxAnyBody, MessageBody, ResponseBody}, + body::{AnyBody, BodySize, MessageBody}, http::{ header::{ContentEncoding, CONTENT_ENCODING}, HeaderValue, StatusCode, @@ -50,8 +50,8 @@ impl Encoder { pub fn response( encoding: ContentEncoding, head: &mut ResponseHead, - body: ResponseBody, - ) -> ResponseBody> { + body: AnyBody, + ) -> AnyBody> { let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING) || head.status == StatusCode::SWITCHING_PROTOCOLS || head.status == StatusCode::NO_CONTENT @@ -59,18 +59,15 @@ impl Encoder { || encoding == ContentEncoding::Auto); let body = match body { - ResponseBody::Other(b) => match b { - Body::None => return ResponseBody::Other(Body::None), - Body::Bytes(buf) => { - if can_encode { - EncoderBody::Bytes(buf) - } else { - return ResponseBody::Other(Body::Bytes(buf)); - } + AnyBody::None => return AnyBody::None, + AnyBody::Bytes(buf) => { + if can_encode { + EncoderBody::Bytes(buf) + } else { + return AnyBody::Bytes(buf); } - Body::Stream(stream) => EncoderBody::BoxedStream(stream), - }, - ResponseBody::Body(stream) => EncoderBody::Stream(stream), + } + AnyBody::Body(body) => EncoderBody::Stream(body), }; if can_encode { @@ -78,7 +75,8 @@ impl Encoder { if let Some(enc) = ContentEncoder::encoder(encoding) { update_head(encoding, head); head.no_chunking(false); - return ResponseBody::Body(Encoder { + + return AnyBody::Body(Encoder { body, eof: false, fut: None, @@ -87,7 +85,7 @@ impl Encoder { } } - ResponseBody::Body(Encoder { + AnyBody::Body(Encoder { body, eof: false, fut: None, @@ -100,7 +98,6 @@ impl Encoder { enum EncoderBody { Bytes(Bytes), Stream(#[pin] B), - BoxedStream(BoxAnyBody), } impl MessageBody for EncoderBody @@ -113,7 +110,6 @@ where match self { EncoderBody::Bytes(ref b) => b.size(), EncoderBody::Stream(ref b) => b.size(), - EncoderBody::BoxedStream(ref b) => b.size(), } } @@ -130,9 +126,6 @@ where } } EncoderBodyProj::Stream(b) => b.poll_next(cx).map_err(EncoderError::Body), - EncoderBodyProj::BoxedStream(ref mut b) => { - b.as_pin_mut().poll_next(cx).map_err(EncoderError::Boxed) - } } } } @@ -348,9 +341,6 @@ pub enum EncoderError { #[display(fmt = "body")] Body(E), - #[display(fmt = "boxed")] - Boxed(Box), - #[display(fmt = "blocking")] Blocking(BlockingError), @@ -362,7 +352,6 @@ impl StdError for EncoderError { fn source(&self) -> Option<&(dyn StdError + 'static)> { match self { EncoderError::Body(err) => Some(err), - EncoderError::Boxed(err) => Some(&**err), EncoderError::Blocking(err) => Some(err), EncoderError::Io(err) => Some(err), } diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 844bc61ea..163d84f5b 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -1077,7 +1077,7 @@ mod tests { fn_service(|req: Request| { let path = req.path().as_bytes(); ready(Ok::<_, Error>( - Response::ok().set_body(AnyBody::from_slice(path)), + Response::ok().set_body(AnyBody::copy_from_slice(path)), )) }) } diff --git a/actix-http/src/response_builder.rs b/actix-http/src/response_builder.rs index a1cb1a423..e934f94dc 100644 --- a/actix-http/src/response_builder.rs +++ b/actix-http/src/response_builder.rs @@ -262,7 +262,7 @@ impl ResponseBuilder { S: Stream> + 'static, E: Into> + 'static, { - self.body(AnyBody::from_message(BodyStream::new(stream))) + self.body(AnyBody::new_boxed(BodyStream::new(stream))) } /// Generate response with an empty body. diff --git a/awc/src/sender.rs b/awc/src/sender.rs index fcd0c71af..02870aea9 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -9,7 +9,7 @@ use std::{ }; use actix_http::{ - body::{Body, BodyStream}, + body::{AnyBody, Body, BodyStream}, http::{ header::{self, HeaderMap, HeaderName, IntoHeaderValue}, Error as HttpError, @@ -286,7 +286,7 @@ impl RequestSender { response_decompress, timeout, config, - Body::from_message(BodyStream::new(stream)), + AnyBody::new_boxed(BodyStream::new(stream)), ) } diff --git a/src/dev.rs b/src/dev.rs index 4fac207a7..27c206e70 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -14,7 +14,7 @@ pub use crate::types::form::UrlEncoded; pub use crate::types::json::JsonBody; pub use crate::types::readlines::Readlines; -pub use actix_http::body::{AnyBody, Body, BodySize, MessageBody, ResponseBody, SizedStream}; +pub use actix_http::body::{AnyBody, Body, BodySize, MessageBody, SizedStream}; #[cfg(feature = "__compress")] pub use actix_http::encoding::Decoder as Decompress; diff --git a/src/middleware/compat.rs b/src/middleware/compat.rs index 0a6256fe2..752e90f94 100644 --- a/src/middleware/compat.rs +++ b/src/middleware/compat.rs @@ -7,7 +7,7 @@ use std::{ task::{Context, Poll}, }; -use actix_http::body::{Body, MessageBody}; +use actix_http::body::{AnyBody, MessageBody}; use actix_service::{Service, Transform}; use futures_core::{future::LocalBoxFuture, ready}; @@ -124,7 +124,7 @@ where B::Error: Into>, { fn map_body(self) -> ServiceResponse { - self.map_body(|_, body| Body::from_message(body)) + self.map_body(|_, body| AnyBody::new_boxed(body)) } } diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 4854f4beb..3e85cb846 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -10,13 +10,14 @@ use std::{ }; use actix_http::{ - body::{MessageBody, ResponseBody}, + body::{AnyBody, MessageBody}, encoding::Encoder, http::header::{ContentEncoding, ACCEPT_ENCODING}, StatusCode, }; use actix_service::{Service, Transform}; use actix_utils::future::{ok, Either, Ready}; +use bytes::Bytes; use futures_core::ready; use once_cell::sync::Lazy; use pin_project::pin_project; @@ -61,7 +62,7 @@ where B: MessageBody, S: Service, Error = Error>, { - type Response = ServiceResponse>>; + type Response = ServiceResponse>>; type Error = Error; type Transform = CompressMiddleware; type InitError = (); @@ -110,7 +111,7 @@ where S: Service, Error = Error>, B: MessageBody, { - type Response = ServiceResponse>>; + type Response = ServiceResponse>>; type Error = Error; type Future = Either, Ready>>; @@ -142,15 +143,19 @@ where // There is an HTTP header but we cannot match what client as asked for Some(Err(_)) => { - let res = HttpResponse::with_body( - StatusCode::NOT_ACCEPTABLE, - SUPPORTED_ALGORITHM_NAMES.as_str(), - ); - let enc = ContentEncoding::Identity; + let res = HttpResponse::new(StatusCode::NOT_ACCEPTABLE); - Either::right(ok(req.into_response(res.map_body(move |head, body| { - Encoder::response(enc, head, ResponseBody::Other(body.into())) - })))) + let res: HttpResponse>> = res.map_body(move |head, _| { + let body_bytes = Bytes::from(SUPPORTED_ALGORITHM_NAMES.as_bytes()); + + Encoder::response( + ContentEncoding::Identity, + head, + AnyBody::Bytes(body_bytes), + ) + }); + + Either::right(ok(req.into_response(res))) } } } @@ -172,7 +177,7 @@ where B: MessageBody, S: Service, Error = Error>, { - type Output = Result>>, Error>; + type Output = Result>>, Error>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); @@ -186,7 +191,7 @@ where }; Poll::Ready(Ok(resp.map_body(move |head, body| { - Encoder::response(enc, head, ResponseBody::Body(body)) + Encoder::response(enc, head, AnyBody::Body(body)) }))) } Err(e) => Poll::Ready(Err(e)), diff --git a/src/responder.rs b/src/responder.rs index 005bff03e..4d2e97c36 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -232,7 +232,7 @@ pub(crate) mod tests { use bytes::{Bytes, BytesMut}; use super::*; - use crate::dev::{Body, ResponseBody}; + use crate::dev::AnyBody; use crate::http::{header::CONTENT_TYPE, HeaderValue, StatusCode}; use crate::test::{init_service, TestRequest}; use crate::{error, web, App}; @@ -264,13 +264,13 @@ pub(crate) mod tests { pub(crate) trait BodyTest { fn bin_ref(&self) -> &[u8]; - fn body(&self) -> &Body; + fn body(&self) -> &AnyBody; } impl BodyTest for Body { fn bin_ref(&self) -> &[u8] { match self { - Body::Bytes(ref bin) => bin, + AnyBody::Bytes(ref bin) => bin, _ => unreachable!("bug in test impl"), } } @@ -279,27 +279,6 @@ pub(crate) mod tests { } } - impl BodyTest for ResponseBody { - fn bin_ref(&self) -> &[u8] { - match self { - ResponseBody::Body(ref b) => match b { - Body::Bytes(ref bin) => bin, - _ => unreachable!("bug in test impl"), - }, - ResponseBody::Other(ref b) => match b { - Body::Bytes(ref bin) => bin, - _ => unreachable!("bug in test impl"), - }, - } - } - fn body(&self) -> &Body { - match self { - ResponseBody::Body(ref b) => b, - ResponseBody::Other(ref b) => b, - } - } - } - #[actix_rt::test] async fn test_responder() { let req = TestRequest::default().to_http_request(); diff --git a/src/response/builder.rs b/src/response/builder.rs index f6099a019..e42d85f59 100644 --- a/src/response/builder.rs +++ b/src/response/builder.rs @@ -354,10 +354,10 @@ impl HttpResponseBuilder { #[inline] pub fn streaming(&mut self, stream: S) -> HttpResponse where - S: Stream> + Unpin + 'static, + S: Stream> + 'static, E: Into> + 'static, { - self.body(AnyBody::from_message(BodyStream::new(stream))) + self.body(AnyBody::new_boxed(BodyStream::new(stream))) } /// Set a json body and generate `Response` diff --git a/src/response/response.rs b/src/response/response.rs index 09515c839..46360e536 100644 --- a/src/response/response.rs +++ b/src/response/response.rs @@ -227,6 +227,9 @@ impl HttpResponse { } } + // TODO: into_body equivalent + // TODO: into_boxed_body + /// Extract response body pub fn into_body(self) -> B { self.res.into_body() From 668a33c793a41fd8cedb7170f4930fa543f25d74 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 16 Nov 2021 22:10:30 +0000 Subject: [PATCH 81/88] remove internal usage of Body --- actix-http/examples/echo2.rs | 4 +- actix-http/src/body/body.rs | 43 +++++++++---- actix-http/src/body/mod.rs | 68 +++++++++++--------- actix-http/src/error.rs | 25 +++---- actix-http/src/header/shared/quality_item.rs | 1 + actix-http/src/response_builder.rs | 6 +- actix-http/tests/test_openssl.rs | 4 +- actix-http/tests/test_rustls.rs | 6 +- actix-http/tests/test_server.rs | 4 +- awc/src/connect.rs | 4 +- awc/src/frozen.rs | 6 +- awc/src/middleware/redirect.rs | 12 ++-- awc/src/request.rs | 4 +- awc/src/sender.rs | 10 +-- src/app.rs | 4 +- src/dev.rs | 1 + src/error/internal.rs | 4 +- src/responder.rs | 10 +-- src/response/builder.rs | 4 +- src/response/response.rs | 12 ++-- src/scope.rs | 8 +-- src/test.rs | 6 +- tests/test_server.rs | 2 +- 23 files changed, 137 insertions(+), 111 deletions(-) diff --git a/actix-http/examples/echo2.rs b/actix-http/examples/echo2.rs index db195d65b..6e5ddec7c 100644 --- a/actix-http/examples/echo2.rs +++ b/actix-http/examples/echo2.rs @@ -1,12 +1,12 @@ use std::io; -use actix_http::{body::Body, http::HeaderValue, http::StatusCode}; +use actix_http::{body::AnyBody, http::HeaderValue, http::StatusCode}; use actix_http::{Error, HttpService, Request, Response}; use actix_server::Server; use bytes::BytesMut; use futures_util::StreamExt as _; -async fn handle_request(mut req: Request) -> Result, Error> { +async fn handle_request(mut req: Request) -> Result, Error> { let mut body = BytesMut::new(); while let Some(item) = req.payload().next().await { body.extend_from_slice(&item?) diff --git a/actix-http/src/body/body.rs b/actix-http/src/body/body.rs index d51173a57..1d88777bc 100644 --- a/actix-http/src/body/body.rs +++ b/actix-http/src/body/body.rs @@ -14,6 +14,7 @@ use crate::error::Error; use super::{BodySize, BodyStream, MessageBody, MessageBodyMapErr, SizedStream}; +#[deprecated(since = "4.0.0", note = "Renamed to `AnyBody`.")] pub type Body = AnyBody; /// Represents various types of HTTP message body. @@ -116,7 +117,7 @@ where } impl PartialEq for AnyBody { - fn eq(&self, other: &Body) -> bool { + fn eq(&self, other: &AnyBody) -> bool { match *self { AnyBody::None => matches!(*other, AnyBody::None), AnyBody::Bytes(ref b) => match *other { @@ -139,37 +140,37 @@ impl fmt::Debug for AnyBody { } impl From<&'static str> for AnyBody { - fn from(string: &'static str) -> Body { + fn from(string: &'static str) -> AnyBody { AnyBody::Bytes(Bytes::from_static(string.as_ref())) } } impl From<&'static [u8]> for AnyBody { - fn from(bytes: &'static [u8]) -> Body { + fn from(bytes: &'static [u8]) -> AnyBody { AnyBody::Bytes(Bytes::from_static(bytes)) } } impl From> for AnyBody { - fn from(vec: Vec) -> Body { + fn from(vec: Vec) -> AnyBody { AnyBody::Bytes(Bytes::from(vec)) } } impl From for AnyBody { - fn from(string: String) -> Body { + fn from(string: String) -> AnyBody { string.into_bytes().into() } } impl From<&'_ String> for AnyBody { - fn from(string: &String) -> Body { + fn from(string: &String) -> AnyBody { AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&string))) } } impl From> for AnyBody { - fn from(string: Cow<'_, str>) -> Body { + fn from(string: Cow<'_, str>) -> AnyBody { match string { Cow::Owned(s) => AnyBody::from(s), Cow::Borrowed(s) => { @@ -180,33 +181,53 @@ impl From> for AnyBody { } impl From for AnyBody { - fn from(bytes: Bytes) -> Body { + fn from(bytes: Bytes) -> Self { AnyBody::Bytes(bytes) } } impl From for AnyBody { - fn from(bytes: BytesMut) -> Body { + fn from(bytes: BytesMut) -> Self { AnyBody::Bytes(bytes.freeze()) } } +impl From> for AnyBody> +where + S: Stream> + 'static, + E: Into> + 'static, +{ + fn from(stream: SizedStream) -> Self { + AnyBody::new(stream) + } +} + impl From> for AnyBody where S: Stream> + 'static, E: Into> + 'static, { - fn from(stream: SizedStream) -> Body { + fn from(stream: SizedStream) -> Self { AnyBody::new_boxed(stream) } } +impl From> for AnyBody> +where + S: Stream> + 'static, + E: Into> + 'static, +{ + fn from(stream: BodyStream) -> Self { + AnyBody::new(stream) + } +} + impl From> for AnyBody where S: Stream> + 'static, E: Into> + 'static, { - fn from(stream: BodyStream) -> Body { + fn from(stream: BodyStream) -> Self { AnyBody::new_boxed(stream) } } diff --git a/actix-http/src/body/mod.rs b/actix-http/src/body/mod.rs index 07e5e67ce..83299a471 100644 --- a/actix-http/src/body/mod.rs +++ b/actix-http/src/body/mod.rs @@ -14,6 +14,7 @@ mod message_body; mod size; mod sized_stream; +#[allow(deprecated)] pub use self::body::{AnyBody, Body, BoxBody}; pub use self::body_stream::BodyStream; pub use self::message_body::MessageBody; @@ -76,10 +77,10 @@ mod tests { use super::*; - impl Body { + impl AnyBody { pub(crate) fn get_ref(&self) -> &[u8] { match *self { - Body::Bytes(ref bin) => bin, + AnyBody::Bytes(ref bin) => bin, _ => panic!(), } } @@ -87,9 +88,9 @@ mod tests { #[actix_rt::test] async fn test_static_str() { - assert_eq!(Body::from("").size(), BodySize::Sized(0)); - assert_eq!(Body::from("test").size(), BodySize::Sized(4)); - assert_eq!(Body::from("test").get_ref(), b"test"); + assert_eq!(AnyBody::from("").size(), BodySize::Sized(0)); + assert_eq!(AnyBody::from("test").size(), BodySize::Sized(4)); + assert_eq!(AnyBody::from("test").get_ref(), b"test"); assert_eq!("test".size(), BodySize::Sized(4)); assert_eq!( @@ -103,13 +104,16 @@ mod tests { #[actix_rt::test] async fn test_static_bytes() { - assert_eq!(Body::from(b"test".as_ref()).size(), BodySize::Sized(4)); - assert_eq!(Body::from(b"test".as_ref()).get_ref(), b"test"); + assert_eq!(AnyBody::from(b"test".as_ref()).size(), BodySize::Sized(4)); + assert_eq!(AnyBody::from(b"test".as_ref()).get_ref(), b"test"); assert_eq!( - Body::copy_from_slice(b"test".as_ref()).size(), + AnyBody::copy_from_slice(b"test".as_ref()).size(), BodySize::Sized(4) ); - assert_eq!(Body::copy_from_slice(b"test".as_ref()).get_ref(), b"test"); + assert_eq!( + AnyBody::copy_from_slice(b"test".as_ref()).get_ref(), + b"test" + ); let sb = Bytes::from(&b"test"[..]); pin!(sb); @@ -122,8 +126,8 @@ mod tests { #[actix_rt::test] async fn test_vec() { - assert_eq!(Body::from(Vec::from("test")).size(), BodySize::Sized(4)); - assert_eq!(Body::from(Vec::from("test")).get_ref(), b"test"); + assert_eq!(AnyBody::from(Vec::from("test")).size(), BodySize::Sized(4)); + assert_eq!(AnyBody::from(Vec::from("test")).get_ref(), b"test"); let test_vec = Vec::from("test"); pin!(test_vec); @@ -140,8 +144,8 @@ mod tests { #[actix_rt::test] async fn test_bytes() { let b = Bytes::from("test"); - assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4)); - assert_eq!(Body::from(b.clone()).get_ref(), b"test"); + assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4)); + assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test"); pin!(b); assert_eq!(b.size(), BodySize::Sized(4)); @@ -154,8 +158,8 @@ mod tests { #[actix_rt::test] async fn test_bytes_mut() { let b = BytesMut::from("test"); - assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4)); - assert_eq!(Body::from(b.clone()).get_ref(), b"test"); + assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4)); + assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test"); pin!(b); assert_eq!(b.size(), BodySize::Sized(4)); @@ -168,10 +172,10 @@ mod tests { #[actix_rt::test] async fn test_string() { let b = "test".to_owned(); - assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4)); - assert_eq!(Body::from(b.clone()).get_ref(), b"test"); - assert_eq!(Body::from(&b).size(), BodySize::Sized(4)); - assert_eq!(Body::from(&b).get_ref(), b"test"); + assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4)); + assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test"); + assert_eq!(AnyBody::from(&b).size(), BodySize::Sized(4)); + assert_eq!(AnyBody::from(&b).get_ref(), b"test"); pin!(b); assert_eq!(b.size(), BodySize::Sized(4)); @@ -204,29 +208,33 @@ mod tests { #[actix_rt::test] async fn test_body_eq() { assert!( - Body::Bytes(Bytes::from_static(b"1")) - == Body::Bytes(Bytes::from_static(b"1")) + AnyBody::Bytes(Bytes::from_static(b"1")) + == AnyBody::Bytes(Bytes::from_static(b"1")) ); - assert!(Body::Bytes(Bytes::from_static(b"1")) != Body::None); + assert!(AnyBody::Bytes(Bytes::from_static(b"1")) != AnyBody::None); } #[actix_rt::test] async fn test_body_debug() { - assert!(format!("{:?}", Body::None).contains("Body::None")); - assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains('1')); + assert!(format!("{:?}", AnyBody::::None).contains("Body::None")); + assert!(format!("{:?}", AnyBody::from(Bytes::from_static(b"1"))).contains('1')); } #[actix_rt::test] async fn test_serde_json() { use serde_json::{json, Value}; assert_eq!( - Body::from(serde_json::to_vec(&Value::String("test".to_owned())).unwrap()) - .size(), + AnyBody::from( + serde_json::to_vec(&Value::String("test".to_owned())).unwrap() + ) + .size(), BodySize::Sized(6) ); assert_eq!( - Body::from(serde_json::to_vec(&json!({"test-key":"test-value"})).unwrap()) - .size(), + AnyBody::from( + serde_json::to_vec(&json!({"test-key":"test-value"})).unwrap() + ) + .size(), BodySize::Sized(25) ); } @@ -250,11 +258,11 @@ mod tests { #[actix_rt::test] async fn test_to_bytes() { - let body = Body::empty(); + let body = AnyBody::empty(); let bytes = to_bytes(body).await.unwrap(); assert!(bytes.is_empty()); - let body = Body::Bytes(Bytes::from_static(b"123")); + let body = AnyBody::copy_from_slice(b"123"); let bytes = to_bytes(body).await.unwrap(); assert_eq!(bytes, b"123"[..]); } diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index f7d7f696a..c7c0cce0e 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -5,10 +5,7 @@ use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Err use derive_more::{Display, Error, From}; use http::{uri::InvalidUri, StatusCode}; -use crate::{ - body::{AnyBody, Body}, - ws, Response, -}; +use crate::{body::AnyBody, ws, Response}; pub use http::Error as HttpError; @@ -29,6 +26,11 @@ impl Error { } } + pub(crate) fn with_cause(mut self, cause: impl Into>) -> Self { + self.inner.cause = Some(cause.into()); + self + } + pub(crate) fn new_http() -> Self { Self::new(Kind::Http) } @@ -49,14 +51,12 @@ impl Error { Self::new(Kind::SendResponse) } - // TODO: remove allow - #[allow(dead_code)] + #[allow(unused)] // reserved for future use (TODO: remove allow when being used) pub(crate) fn new_io() -> Self { Self::new(Kind::Io) } - // used in encoder behind feature flag so ignore unused warning - #[allow(unused)] + #[allow(unused)] // used in encoder behind feature flag so ignore unused warning pub(crate) fn new_encoder() -> Self { Self::new(Kind::Encoder) } @@ -64,11 +64,6 @@ impl Error { pub(crate) fn new_ws() -> Self { Self::new(Kind::Ws) } - - pub(crate) fn with_cause(mut self, cause: impl Into>) -> Self { - self.inner.cause = Some(cause.into()); - self - } } impl From for Response { @@ -78,12 +73,12 @@ impl From for Response { _ => StatusCode::INTERNAL_SERVER_ERROR, }; - Response::new(status_code).set_body(Body::from(err.to_string())) + Response::new(status_code).set_body(AnyBody::from(err.to_string())) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Display)] -pub enum Kind { +pub(crate) enum Kind { #[display(fmt = "error processing HTTP")] Http, diff --git a/actix-http/src/header/shared/quality_item.rs b/actix-http/src/header/shared/quality_item.rs index 63fa02e7b..431e9fb3e 100644 --- a/actix-http/src/header/shared/quality_item.rs +++ b/actix-http/src/header/shared/quality_item.rs @@ -195,6 +195,7 @@ mod tests { use super::*; // copy of encoding from actix-web headers + #[allow(clippy::enum_variant_names)] // allow Encoding prefix on EncodingExt #[derive(Clone, PartialEq, Debug)] pub enum Encoding { Chunked, diff --git a/actix-http/src/response_builder.rs b/actix-http/src/response_builder.rs index e934f94dc..c5fcb625c 100644 --- a/actix-http/src/response_builder.rs +++ b/actix-http/src/response_builder.rs @@ -357,7 +357,7 @@ impl fmt::Debug for ResponseBuilder { #[cfg(test)] mod tests { use super::*; - use crate::body::Body; + use crate::body::AnyBody; use crate::http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; #[test] @@ -390,13 +390,13 @@ mod tests { fn test_content_type() { let resp = Response::build(StatusCode::OK) .content_type("text/plain") - .body(Body::empty()); + .body(AnyBody::empty()); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") } #[test] fn test_into_builder() { - let mut resp: Response = "test".into(); + let mut resp: Response = "test".into(); assert_eq!(resp.status(), StatusCode::OK); resp.headers_mut().insert( diff --git a/actix-http/tests/test_openssl.rs b/actix-http/tests/test_openssl.rs index 0eaaabcc7..e7dd78171 100644 --- a/actix-http/tests/test_openssl.rs +++ b/actix-http/tests/test_openssl.rs @@ -5,7 +5,7 @@ extern crate tls_openssl as openssl; use std::{convert::Infallible, io}; use actix_http::{ - body::{AnyBody, Body, SizedStream}, + body::{AnyBody, SizedStream}, error::PayloadError, http::{ header::{self, HeaderValue}, @@ -409,7 +409,7 @@ impl From for Response { async fn test_h2_service_error() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| err::, _>(BadRequest)) + .h2(|_| err::, _>(BadRequest)) .openssl(tls_config()) .map_err(|_| ()) }) diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index a9f6e99f8..320c9ad92 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -10,7 +10,7 @@ use std::{ }; use actix_http::{ - body::{AnyBody, Body, SizedStream}, + body::{AnyBody, SizedStream}, error::PayloadError, http::{ header::{self, HeaderName, HeaderValue}, @@ -477,7 +477,7 @@ impl From for Response { async fn test_h2_service_error() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| err::, _>(BadRequest)) + .h2(|_| err::, _>(BadRequest)) .rustls(tls_config()) }) .await; @@ -494,7 +494,7 @@ async fn test_h2_service_error() { async fn test_h1_service_error() { let mut srv = test_server(move || { HttpService::build() - .h1(|_| err::, _>(BadRequest)) + .h1(|_| err::, _>(BadRequest)) .rustls(tls_config()) }) .await; diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index ea78ce113..2dca09e21 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -6,7 +6,7 @@ use std::{ }; use actix_http::{ - body::{AnyBody, Body, SizedStream}, + body::{AnyBody, SizedStream}, header, http, Error, HttpMessage, HttpService, KeepAlive, Request, Response, StatusCode, }; @@ -724,7 +724,7 @@ impl From for Response { async fn test_h1_service_error() { let mut srv = test_server(|| { HttpService::build() - .h1(|_| err::, _>(BadRequest)) + .h1(|_| err::, _>(BadRequest)) .tcp() }) .await; diff --git a/awc/src/connect.rs b/awc/src/connect.rs index f27a8c368..05f2a6495 100644 --- a/awc/src/connect.rs +++ b/awc/src/connect.rs @@ -8,7 +8,7 @@ use std::{ use actix_codec::Framed; use actix_http::{ - body::Body, h1::ClientCodec, Payload, RequestHead, RequestHeadType, ResponseHead, + body::AnyBody, h1::ClientCodec, Payload, RequestHead, RequestHeadType, ResponseHead, }; use actix_service::Service; use futures_core::{future::LocalBoxFuture, ready}; @@ -30,7 +30,7 @@ pub type BoxConnectorService = Rc< pub type BoxedSocket = Box; pub enum ConnectRequest { - Client(RequestHeadType, Body, Option), + Client(RequestHeadType, AnyBody, Option), Tunnel(RequestHead, Option), } diff --git a/awc/src/frozen.rs b/awc/src/frozen.rs index cb8c0f1bf..46a00b000 100644 --- a/awc/src/frozen.rs +++ b/awc/src/frozen.rs @@ -5,7 +5,7 @@ use futures_core::Stream; use serde::Serialize; use actix_http::{ - body::Body, + body::AnyBody, http::{header::IntoHeaderValue, Error as HttpError, HeaderMap, HeaderName, Method, Uri}, RequestHead, }; @@ -45,7 +45,7 @@ impl FrozenClientRequest { /// Send a body. pub fn send_body(&self, body: B) -> SendClientRequest where - B: Into, + B: Into, { RequestSender::Rc(self.head.clone(), None).send_body( self.addr, @@ -158,7 +158,7 @@ impl FrozenSendBuilder { /// Complete request construction and send a body. pub fn send_body(self, body: B) -> SendClientRequest where - B: Into, + B: Into, { if let Some(e) = self.err { return e.into(); diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs index f01136d14..12a71f7cb 100644 --- a/awc/src/middleware/redirect.rs +++ b/awc/src/middleware/redirect.rs @@ -8,7 +8,7 @@ use std::{ }; use actix_http::{ - body::Body, + body::AnyBody, http::{header, Method, StatusCode, Uri}, RequestHead, RequestHeadType, }; @@ -95,7 +95,7 @@ where }; let body_opt = match body { - Body::Bytes(ref b) => Some(b.clone()), + AnyBody::Bytes(ref b) => Some(b.clone()), _ => None, }; @@ -192,14 +192,14 @@ where let body_new = if is_redirect { // try to reuse body match body { - Some(ref bytes) => Body::Bytes(bytes.clone()), - // TODO: should this be Body::Empty or Body::None. - _ => Body::empty(), + Some(ref bytes) => AnyBody::Bytes(bytes.clone()), + // TODO: should this be AnyBody::Empty or AnyBody::None. + _ => AnyBody::empty(), } } else { body = None; // remove body - Body::None + AnyBody::None }; let mut headers = headers.take().unwrap(); diff --git a/awc/src/request.rs b/awc/src/request.rs index 812c76318..bc3859e2e 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -5,7 +5,7 @@ use futures_core::Stream; use serde::Serialize; use actix_http::{ - body::Body, + body::AnyBody, http::{ header::{self, IntoHeaderPair}, ConnectionType, Error as HttpError, HeaderMap, HeaderValue, Method, Uri, Version, @@ -350,7 +350,7 @@ impl ClientRequest { /// Complete request construction and send body. pub fn send_body(self, body: B) -> SendClientRequest where - B: Into, + B: Into, { let slf = match self.prep_for_sending() { Ok(slf) => slf, diff --git a/awc/src/sender.rs b/awc/src/sender.rs index 02870aea9..7e1bcd646 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -9,7 +9,7 @@ use std::{ }; use actix_http::{ - body::{AnyBody, Body, BodyStream}, + body::{AnyBody, BodyStream}, http::{ header::{self, HeaderMap, HeaderName, IntoHeaderValue}, Error as HttpError, @@ -196,7 +196,7 @@ impl RequestSender { body: B, ) -> SendClientRequest where - B: Into, + B: Into, { let req = match self { RequestSender::Owned(head) => { @@ -236,7 +236,7 @@ impl RequestSender { response_decompress, timeout, config, - Body::Bytes(Bytes::from(body)), + AnyBody::Bytes(Bytes::from(body)), ) } @@ -265,7 +265,7 @@ impl RequestSender { response_decompress, timeout, config, - Body::Bytes(Bytes::from(body)), + AnyBody::Bytes(Bytes::from(body)), ) } @@ -297,7 +297,7 @@ impl RequestSender { timeout: Option, config: &ClientConfig, ) -> SendClientRequest { - self.send_body(addr, response_decompress, timeout, config, Body::empty()) + self.send_body(addr, response_decompress, timeout, config, AnyBody::empty()) } fn set_header_if_none(&mut self, key: HeaderName, value: V) -> Result<(), HttpError> diff --git a/src/app.rs b/src/app.rs index da5b45f3a..a291a959e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,7 +4,7 @@ use std::future::Future; use std::marker::PhantomData; use std::rc::Rc; -use actix_http::body::{Body, MessageBody}; +use actix_http::body::{AnyBody, MessageBody}; use actix_http::{Extensions, Request}; use actix_service::boxed::{self, BoxServiceFactory}; use actix_service::{ @@ -39,7 +39,7 @@ pub struct App { _phantom: PhantomData, } -impl App { +impl App { /// Create application builder. Application can be configured with a builder-like pattern. #[allow(clippy::new_without_default)] pub fn new() -> Self { diff --git a/src/dev.rs b/src/dev.rs index 27c206e70..59805b822 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -14,6 +14,7 @@ pub use crate::types::form::UrlEncoded; pub use crate::types::json::JsonBody; pub use crate::types::readlines::Readlines; +#[allow(deprecated)] pub use actix_http::body::{AnyBody, Body, BodySize, MessageBody, SizedStream}; #[cfg(feature = "__compress")] diff --git a/src/error/internal.rs b/src/error/internal.rs index 1d9ca904e..3d99012dc 100644 --- a/src/error/internal.rs +++ b/src/error/internal.rs @@ -1,6 +1,6 @@ use std::{cell::RefCell, fmt, io::Write as _}; -use actix_http::{body::Body, header, StatusCode}; +use actix_http::{body::AnyBody, header, StatusCode}; use bytes::{BufMut as _, BytesMut}; use crate::{Error, HttpRequest, HttpResponse, Responder, ResponseError}; @@ -88,7 +88,7 @@ where header::CONTENT_TYPE, header::HeaderValue::from_static("text/plain; charset=utf-8"), ); - res.set_body(Body::from(buf.into_inner())) + res.set_body(AnyBody::from(buf.into_inner())) } InternalErrorType::Response(ref resp) => { diff --git a/src/responder.rs b/src/responder.rs index 4d2e97c36..8a84be598 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use actix_http::{ - body::Body, + body::AnyBody, http::{header::IntoHeaderPair, Error as HttpError, HeaderMap, StatusCode}, }; use bytes::{Bytes, BytesMut}; @@ -65,7 +65,7 @@ impl Responder for HttpResponse { } } -impl Responder for actix_http::Response { +impl Responder for actix_http::Response { #[inline] fn respond_to(self, _: &HttpRequest) -> HttpResponse { HttpResponse::from(self) @@ -254,7 +254,7 @@ pub(crate) mod tests { let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); match resp.response().body() { - Body::Bytes(ref b) => { + AnyBody::Bytes(ref b) => { let bytes = b.clone(); assert_eq!(bytes, Bytes::from_static(b"some")); } @@ -267,14 +267,14 @@ pub(crate) mod tests { fn body(&self) -> &AnyBody; } - impl BodyTest for Body { + impl BodyTest for AnyBody { fn bin_ref(&self) -> &[u8] { match self { AnyBody::Bytes(ref bin) => bin, _ => unreachable!("bug in test impl"), } } - fn body(&self) -> &Body { + fn body(&self) -> &AnyBody { self } } diff --git a/src/response/builder.rs b/src/response/builder.rs index e42d85f59..e61f7e16f 100644 --- a/src/response/builder.rs +++ b/src/response/builder.rs @@ -436,7 +436,7 @@ mod tests { use super::*; use crate::{ - dev::Body, + dev::AnyBody, http::{ header::{self, HeaderValue, CONTENT_TYPE}, StatusCode, @@ -475,7 +475,7 @@ mod tests { fn test_content_type() { let resp = HttpResponseBuilder::new(StatusCode::OK) .content_type("text/plain") - .body(Body::empty()); + .body(AnyBody::empty()); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") } diff --git a/src/response/response.rs b/src/response/response.rs index 46360e536..6475a3816 100644 --- a/src/response/response.rs +++ b/src/response/response.rs @@ -8,7 +8,7 @@ use std::{ }; use actix_http::{ - body::{AnyBody, Body, MessageBody}, + body::{AnyBody, MessageBody}, http::{header::HeaderMap, StatusCode}, Extensions, Response, ResponseHead, }; @@ -273,14 +273,14 @@ impl From> for Response { } } -// Future is only implemented for Body payload type because it's the most useful for making simple -// handlers without async blocks. Making it generic over all MessageBody types requires a future -// impl on Response which would cause it's body field to be, undesirably, Option. +// Future is only implemented for AnyBody payload type because it's the most useful for making +// simple handlers without async blocks. Making it generic over all MessageBody types requires a +// future impl on Response which would cause it's body field to be, undesirably, Option. // // This impl is not particularly efficient due to the Response construction and should probably // not be invoked if performance is important. Prefer an async fn/block in such cases. -impl Future for HttpResponse { - type Output = Result, Error>; +impl Future for HttpResponse { + type Output = Result, Error>; fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll { if let Some(err) = self.error.take() { diff --git a/src/scope.rs b/src/scope.rs index 7d914f581..c20b5d7c8 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -580,7 +580,7 @@ mod tests { use bytes::Bytes; use crate::{ - dev::Body, + dev::AnyBody, guard, http::{header, HeaderValue, Method, StatusCode}, middleware::DefaultHeaders, @@ -752,7 +752,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); match resp.response().body() { - Body::Bytes(ref b) => { + AnyBody::Bytes(ref b) => { let bytes = b.clone(); assert_eq!(bytes, Bytes::from_static(b"project: project1")); } @@ -853,7 +853,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::CREATED); match resp.response().body() { - Body::Bytes(ref b) => { + AnyBody::Bytes(ref b) => { let bytes = b.clone(); assert_eq!(bytes, Bytes::from_static(b"project: project_1")); } @@ -881,7 +881,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::CREATED); match resp.response().body() { - Body::Bytes(ref b) => { + AnyBody::Bytes(ref b) => { let bytes = b.clone(); assert_eq!(bytes, Bytes::from_static(b"project: test - 1")); } diff --git a/src/test.rs b/src/test.rs index 43bf612c6..77765e267 100644 --- a/src/test.rs +++ b/src/test.rs @@ -22,7 +22,7 @@ use crate::{ app_service::AppInitServiceState, config::AppConfig, data::Data, - dev::{Body, MessageBody, Payload}, + dev::{AnyBody, MessageBody, Payload}, http::header::ContentType, rmap::ResourceMap, service::{ServiceRequest, ServiceResponse}, @@ -32,14 +32,14 @@ use crate::{ /// Create service that always responds with `HttpResponse::Ok()` and no body. pub fn ok_service( -) -> impl Service, Error = Error> { +) -> impl Service, Error = Error> { default_service(StatusCode::OK) } /// Create service that always responds with given status code and no body. pub fn default_service( status_code: StatusCode, -) -> impl Service, Error = Error> { +) -> impl Service, Error = Error> { (move |req: ServiceRequest| { ok(req.into_response(HttpResponseBuilder::new(status_code).finish())) }) diff --git a/tests/test_server.rs b/tests/test_server.rs index d21dac8cf..3f0fbfccc 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -200,7 +200,7 @@ async fn test_body_encoding_override() { .body(STR) }))) .service(web::resource("/raw").route(web::to(|| { - let body = actix_web::dev::Body::Bytes(STR.into()); + let body = actix_web::dev::AnyBody::Bytes(STR.into()); let mut response = HttpResponse::with_body(actix_web::http::StatusCode::OK, body); From 0a135c7dc912191b11ed3350bf18ce82b6045483 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 16 Nov 2021 22:41:24 +0000 Subject: [PATCH 82/88] bump actix-codec to 0.4.1 --- Cargo.toml | 2 +- actix-http-test/Cargo.toml | 2 +- actix-http/Cargo.toml | 2 +- actix-test/Cargo.toml | 2 +- actix-web-actors/Cargo.toml | 2 +- awc/Cargo.toml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8ca34c924..9a00db7b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,7 +66,7 @@ rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"] __compress = [] [dependencies] -actix-codec = "0.4.0" +actix-codec = "0.4.1" actix-macros = "0.2.3" actix-rt = "2.2" actix-server = "2.0.0-beta.9" diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index b111f8685..f118d1627 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -30,7 +30,7 @@ openssl = ["tls-openssl", "awc/openssl"] [dependencies] actix-service = "2.0.0" -actix-codec = "0.4.0" +actix-codec = "0.4.1" actix-tls = "3.0.0-beta.7" actix-utils = "3.0.0" actix-rt = "2.2" diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 27a147379..a2e5e284d 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -43,7 +43,7 @@ __compress = [] [dependencies] actix-service = "2.0.0" -actix-codec = "0.4.0" +actix-codec = "0.4.1" actix-utils = "3.0.0" actix-rt = "2.2" diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index 58c0d31a5..bc660293d 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -28,7 +28,7 @@ rustls = ["tls-rustls", "actix-http/rustls", "awc/rustls"] openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"] [dependencies] -actix-codec = "0.4.0" +actix-codec = "0.4.1" actix-http = "3.0.0-beta.12" actix-http-test = "3.0.0-beta.6" actix-service = "2.0.0" diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 706a90c00..c20e508a4 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -15,7 +15,7 @@ path = "src/lib.rs" [dependencies] actix = { version = "0.12.0", default-features = false } -actix-codec = "0.4.0" +actix-codec = "0.4.1" actix-http = "3.0.0-beta.12" actix-web = { version = "4.0.0-beta.11", default-features = false } diff --git a/awc/Cargo.toml b/awc/Cargo.toml index ce710d58d..7bc99c08c 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -53,7 +53,7 @@ trust-dns = ["trust-dns-resolver"] __compress = [] [dependencies] -actix-codec = "0.4.0" +actix-codec = "0.4.1" actix-service = "2.0.0" actix-http = "3.0.0-beta.12" actix-rt = { version = "2.1", default-features = false } From 84c6d25fd32ea06febf8a84d148ab234fc835efb Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 16 Nov 2021 23:07:08 +0000 Subject: [PATCH 83/88] bump env logger dep --- Cargo.toml | 2 +- actix-http/Cargo.toml | 2 +- actix-web-actors/Cargo.toml | 2 +- awc/Cargo.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9a00db7b3..23a5fc8ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,7 +109,7 @@ awc = { version = "3.0.0-beta.10", features = ["openssl"] } brotli2 = "0.3.2" criterion = { version = "0.3", features = ["html_reports"] } -env_logger = "0.8" +env_logger = "0.9" flate2 = "1.0.13" futures-util = { version = "0.3.7", default-features = false, features = ["std"] } rand = "0.8" diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index a2e5e284d..682104cd1 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -86,7 +86,7 @@ actix-http-test = { version = "3.0.0-beta.6", features = ["openssl"] } actix-tls = { version = "3.0.0-beta.7", features = ["openssl"] } async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } -env_logger = "0.8" +env_logger = "0.9" rcgen = "0.8" regex = "1.3" rustls-pemfile = "0.2" diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index c20e508a4..c938c6a1d 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -30,5 +30,5 @@ actix-rt = "2.2" actix-test = "0.1.0-beta.6" awc = { version = "3.0.0-beta.10", default-features = false } -env_logger = "0.8" +env_logger = "0.9" futures-util = { version = "0.3.7", default-features = false } diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 7bc99c08c..048fe78d7 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -97,7 +97,7 @@ actix-tls = { version = "3.0.0-beta.7", features = ["openssl", "rustls"] } actix-test = { version = "0.1.0-beta.6", features = ["openssl", "rustls"] } brotli2 = "0.3.2" -env_logger = "0.8" +env_logger = "0.9" flate2 = "1.0.13" futures-util = { version = "0.3.7", default-features = false } rcgen = "0.8" From 68a3acb9c290945eab82feeb87a98c47abcf0fac Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 16 Nov 2021 23:22:29 +0000 Subject: [PATCH 84/88] bump zstd dep --- Cargo.toml | 2 +- actix-http/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 23a5fc8ca..3f1f54fcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -117,7 +117,7 @@ rcgen = "0.8" rustls-pemfile = "0.2" tls-openssl = { package = "openssl", version = "0.10.9" } tls-rustls = { package = "rustls", version = "0.20.0" } -zstd = "0.7" +zstd = "0.9" [profile.dev] # Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much. diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 682104cd1..c2de71e10 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -78,7 +78,7 @@ actix-tls = { version = "3.0.0-beta.7", default-features = false, optional = tru # compression brotli2 = { version="0.3.2", optional = true } flate2 = { version = "1.0.13", optional = true } -zstd = { version = "0.7", optional = true } +zstd = { version = "0.9", optional = true } [dev-dependencies] actix-server = "2.0.0-beta.9" From 168a7284d300b212f53fb40183d860792cabd5f1 Mon Sep 17 00:00:00 2001 From: fakeshadow <24548779@qq.com> Date: Wed, 17 Nov 2021 21:13:05 +0800 Subject: [PATCH 85/88] fix actix_http::Error conversion. (#2449) --- actix-http/src/body/body.rs | 46 ++++++++++++++++++------------------- actix-http/src/body/mod.rs | 45 +++++++++++++++++++----------------- actix-http/src/error.rs | 2 +- 3 files changed, 48 insertions(+), 45 deletions(-) diff --git a/actix-http/src/body/body.rs b/actix-http/src/body/body.rs index 1d88777bc..04fc957c7 100644 --- a/actix-http/src/body/body.rs +++ b/actix-http/src/body/body.rs @@ -139,56 +139,56 @@ impl fmt::Debug for AnyBody { } } -impl From<&'static str> for AnyBody { - fn from(string: &'static str) -> AnyBody { - AnyBody::Bytes(Bytes::from_static(string.as_ref())) +impl From<&'static str> for AnyBody { + fn from(string: &'static str) -> Self { + Self::Bytes(Bytes::from_static(string.as_ref())) } } -impl From<&'static [u8]> for AnyBody { - fn from(bytes: &'static [u8]) -> AnyBody { - AnyBody::Bytes(Bytes::from_static(bytes)) +impl From<&'static [u8]> for AnyBody { + fn from(bytes: &'static [u8]) -> Self { + Self::Bytes(Bytes::from_static(bytes)) } } -impl From> for AnyBody { - fn from(vec: Vec) -> AnyBody { - AnyBody::Bytes(Bytes::from(vec)) +impl From> for AnyBody { + fn from(vec: Vec) -> Self { + Self::Bytes(Bytes::from(vec)) } } -impl From for AnyBody { - fn from(string: String) -> AnyBody { - string.into_bytes().into() +impl From for AnyBody { + fn from(string: String) -> Self { + Self::Bytes(Bytes::from(string)) } } -impl From<&'_ String> for AnyBody { - fn from(string: &String) -> AnyBody { - AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&string))) +impl From<&'_ String> for AnyBody { + fn from(string: &String) -> Self { + Self::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&string))) } } -impl From> for AnyBody { - fn from(string: Cow<'_, str>) -> AnyBody { +impl From> for AnyBody { + fn from(string: Cow<'_, str>) -> Self { match string { - Cow::Owned(s) => AnyBody::from(s), + Cow::Owned(s) => Self::from(s), Cow::Borrowed(s) => { - AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s))) + Self::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s))) } } } } -impl From for AnyBody { +impl From for AnyBody { fn from(bytes: Bytes) -> Self { - AnyBody::Bytes(bytes) + Self::Bytes(bytes) } } -impl From for AnyBody { +impl From for AnyBody { fn from(bytes: BytesMut) -> Self { - AnyBody::Bytes(bytes.freeze()) + Self::Bytes(bytes.freeze()) } } diff --git a/actix-http/src/body/mod.rs b/actix-http/src/body/mod.rs index 83299a471..724e20597 100644 --- a/actix-http/src/body/mod.rs +++ b/actix-http/src/body/mod.rs @@ -86,11 +86,14 @@ mod tests { } } + /// AnyBody alias because rustc does not (can not?) infer the default type parameter. + type TestBody = AnyBody; + #[actix_rt::test] async fn test_static_str() { - assert_eq!(AnyBody::from("").size(), BodySize::Sized(0)); - assert_eq!(AnyBody::from("test").size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from("test").get_ref(), b"test"); + assert_eq!(TestBody::from("").size(), BodySize::Sized(0)); + assert_eq!(TestBody::from("test").size(), BodySize::Sized(4)); + assert_eq!(TestBody::from("test").get_ref(), b"test"); assert_eq!("test".size(), BodySize::Sized(4)); assert_eq!( @@ -104,14 +107,14 @@ mod tests { #[actix_rt::test] async fn test_static_bytes() { - assert_eq!(AnyBody::from(b"test".as_ref()).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(b"test".as_ref()).get_ref(), b"test"); + assert_eq!(TestBody::from(b"test".as_ref()).size(), BodySize::Sized(4)); + assert_eq!(TestBody::from(b"test".as_ref()).get_ref(), b"test"); assert_eq!( - AnyBody::copy_from_slice(b"test".as_ref()).size(), + TestBody::copy_from_slice(b"test".as_ref()).size(), BodySize::Sized(4) ); assert_eq!( - AnyBody::copy_from_slice(b"test".as_ref()).get_ref(), + TestBody::copy_from_slice(b"test".as_ref()).get_ref(), b"test" ); let sb = Bytes::from(&b"test"[..]); @@ -126,8 +129,8 @@ mod tests { #[actix_rt::test] async fn test_vec() { - assert_eq!(AnyBody::from(Vec::from("test")).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(Vec::from("test")).get_ref(), b"test"); + assert_eq!(TestBody::from(Vec::from("test")).size(), BodySize::Sized(4)); + assert_eq!(TestBody::from(Vec::from("test")).get_ref(), b"test"); let test_vec = Vec::from("test"); pin!(test_vec); @@ -144,8 +147,8 @@ mod tests { #[actix_rt::test] async fn test_bytes() { let b = Bytes::from("test"); - assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test"); + assert_eq!(TestBody::from(b.clone()).size(), BodySize::Sized(4)); + assert_eq!(TestBody::from(b.clone()).get_ref(), b"test"); pin!(b); assert_eq!(b.size(), BodySize::Sized(4)); @@ -158,8 +161,8 @@ mod tests { #[actix_rt::test] async fn test_bytes_mut() { let b = BytesMut::from("test"); - assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test"); + assert_eq!(TestBody::from(b.clone()).size(), BodySize::Sized(4)); + assert_eq!(TestBody::from(b.clone()).get_ref(), b"test"); pin!(b); assert_eq!(b.size(), BodySize::Sized(4)); @@ -172,10 +175,10 @@ mod tests { #[actix_rt::test] async fn test_string() { let b = "test".to_owned(); - assert_eq!(AnyBody::from(b.clone()).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(b.clone()).get_ref(), b"test"); - assert_eq!(AnyBody::from(&b).size(), BodySize::Sized(4)); - assert_eq!(AnyBody::from(&b).get_ref(), b"test"); + assert_eq!(TestBody::from(b.clone()).size(), BodySize::Sized(4)); + assert_eq!(TestBody::from(b.clone()).get_ref(), b"test"); + assert_eq!(TestBody::from(&b).size(), BodySize::Sized(4)); + assert_eq!(TestBody::from(&b).get_ref(), b"test"); pin!(b); assert_eq!(b.size(), BodySize::Sized(4)); @@ -216,22 +219,22 @@ mod tests { #[actix_rt::test] async fn test_body_debug() { - assert!(format!("{:?}", AnyBody::::None).contains("Body::None")); - assert!(format!("{:?}", AnyBody::from(Bytes::from_static(b"1"))).contains('1')); + assert!(format!("{:?}", TestBody::None).contains("Body::None")); + assert!(format!("{:?}", TestBody::from(Bytes::from_static(b"1"))).contains('1')); } #[actix_rt::test] async fn test_serde_json() { use serde_json::{json, Value}; assert_eq!( - AnyBody::from( + TestBody::from( serde_json::to_vec(&Value::String("test".to_owned())).unwrap() ) .size(), BodySize::Sized(6) ); assert_eq!( - AnyBody::from( + TestBody::from( serde_json::to_vec(&json!({"test-key":"test-value"})).unwrap() ) .size(), diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index c7c0cce0e..970c0c564 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -66,7 +66,7 @@ impl Error { } } -impl From for Response { +impl From for Response> { fn from(err: Error) -> Self { let status_code = match err.inner.kind { Kind::Parse => StatusCode::BAD_REQUEST, From 1fe309bcc6b0a472f1d92b3dc51e36d2d1f9db4a Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 17 Nov 2021 15:32:42 +0000 Subject: [PATCH 86/88] increase ci test timeout --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8b21b7fb..8f586d8d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,7 +78,7 @@ jobs: - name: tests uses: actions-rs/cargo@v1 - timeout-minutes: 40 + timeout-minutes: 60 with: command: ci-test args: --skip=test_reading_deflate_encoding_large_random_rustls @@ -174,5 +174,5 @@ jobs: - name: doc tests uses: actions-rs/cargo@v1 - timeout-minutes: 40 + timeout-minutes: 60 with: { command: ci-doctest } From e33618ed6d222ffbf7d095adaa53500e024204ea Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 17 Nov 2021 17:43:24 +0000 Subject: [PATCH 87/88] ensure content disposition header in multipart (#2451) Co-authored-by: Craig Pastro --- actix-multipart/CHANGES.md | 8 + actix-multipart/src/error.rs | 23 ++- actix-multipart/src/server.rs | 228 ++++++++++++++++++------- src/http/header/content_disposition.rs | 24 ++- 4 files changed, 213 insertions(+), 70 deletions(-) diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 09cc707be..97c011393 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,6 +1,14 @@ # Changes ## Unreleased - 2021-xx-xx +* Ensure a correct Content-Disposition header is included in every part of a multipart message. [#2451] +* Added `MultipartError::NoContentDisposition` variant. [#2451] +* Since Content-Disposition is now ensured, `Field::content_disposition` is now infallible. [#2451] +* Added `Field::name` method for getting the field name. [#2451] +* `MultipartError` now marks variants with inner errors as the source. [#2451] +* `MultipartError` is now marked as non-exhaustive. [#2451] + +[#2451]: https://github.com/actix/actix-web/pull/2451 ## 0.4.0-beta.7 - 2021-10-20 diff --git a/actix-multipart/src/error.rs b/actix-multipart/src/error.rs index 5f91c60df..de4594b8f 100644 --- a/actix-multipart/src/error.rs +++ b/actix-multipart/src/error.rs @@ -2,39 +2,52 @@ use actix_web::error::{ParseError, PayloadError}; use actix_web::http::StatusCode; use actix_web::ResponseError; -use derive_more::{Display, From}; +use derive_more::{Display, Error, From}; /// A set of errors that can occur during parsing multipart streams -#[derive(Debug, Display, From)] +#[non_exhaustive] +#[derive(Debug, Display, From, Error)] pub enum MultipartError { + /// Content-Disposition header is not found or is not equal to "form-data". + /// + /// According to [RFC 7578](https://tools.ietf.org/html/rfc7578#section-4.2) a + /// Content-Disposition header must always be present and equal to "form-data". + #[display(fmt = "No Content-Disposition `form-data` header")] + NoContentDisposition, + /// Content-Type header is not found - #[display(fmt = "No Content-type header found")] + #[display(fmt = "No Content-Type header found")] NoContentType, + /// Can not parse Content-Type header #[display(fmt = "Can not parse Content-Type header")] ParseContentType, + /// Multipart boundary is not found #[display(fmt = "Multipart boundary is not found")] Boundary, + /// Nested multipart is not supported #[display(fmt = "Nested multipart is not supported")] Nested, + /// Multipart stream is incomplete #[display(fmt = "Multipart stream is incomplete")] Incomplete, + /// Error during field parsing #[display(fmt = "{}", _0)] Parse(ParseError), + /// Payload error #[display(fmt = "{}", _0)] Payload(PayloadError), + /// Not consumed #[display(fmt = "Multipart stream is not consumed")] NotConsumed, } -impl std::error::Error for MultipartError {} - /// Return `BadRequest` for `MultipartError` impl ResponseError for MultipartError { fn status_code(&self) -> StatusCode { diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs index b7d251537..43f9ccf5f 100644 --- a/actix-multipart/src/server.rs +++ b/actix-multipart/src/server.rs @@ -1,15 +1,20 @@ //! Multipart response payload support. -use std::cell::{Cell, RefCell, RefMut}; -use std::convert::TryFrom; -use std::marker::PhantomData; -use std::pin::Pin; -use std::rc::Rc; -use std::task::{Context, Poll}; -use std::{cmp, fmt}; +use std::{ + cell::{Cell, RefCell, RefMut}, + cmp, + convert::TryFrom, + fmt, + marker::PhantomData, + pin::Pin, + rc::Rc, + task::{Context, Poll}, +}; -use actix_web::error::{ParseError, PayloadError}; -use actix_web::http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue}; +use actix_web::{ + error::{ParseError, PayloadError}, + http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue}, +}; use bytes::{Bytes, BytesMut}; use futures_core::stream::{LocalBoxStream, Stream}; use futures_util::stream::StreamExt as _; @@ -40,10 +45,13 @@ enum InnerMultipartItem { enum InnerState { /// Stream eof Eof, + /// Skip data until first boundary FirstBoundary, + /// Reading boundary Boundary, + /// Reading Headers, Headers, } @@ -332,31 +340,55 @@ impl InnerMultipart { return Poll::Pending; }; - // content type - let mut mt = mime::APPLICATION_OCTET_STREAM; - if let Some(content_type) = headers.get(&header::CONTENT_TYPE) { - if let Ok(content_type) = content_type.to_str() { - if let Ok(ct) = content_type.parse::() { - mt = ct; - } - } - } + // According to [RFC 7578](https://tools.ietf.org/html/rfc7578#section-4.2) a + // Content-Disposition header must always be present and set to "form-data". + + let content_disposition = headers + .get(&header::CONTENT_DISPOSITION) + .and_then(|cd| ContentDisposition::from_raw(cd).ok()) + .filter(|content_disposition| { + let is_form_data = + content_disposition.disposition == header::DispositionType::FormData; + + let has_field_name = content_disposition + .parameters + .iter() + .any(|param| matches!(param, header::DispositionParam::Name(_))); + + is_form_data && has_field_name + }); + + let cd = if let Some(content_disposition) = content_disposition { + content_disposition + } else { + return Poll::Ready(Some(Err(MultipartError::NoContentDisposition))); + }; + + let ct: mime::Mime = headers + .get(&header::CONTENT_TYPE) + .and_then(|ct| ct.to_str().ok()) + .and_then(|ct| ct.parse().ok()) + .unwrap_or(mime::APPLICATION_OCTET_STREAM); self.state = InnerState::Boundary; - // nested multipart stream - if mt.type_() == mime::MULTIPART { - Poll::Ready(Some(Err(MultipartError::Nested))) - } else { - let field = Rc::new(RefCell::new(InnerField::new( - self.payload.clone(), - self.boundary.clone(), - &headers, - )?)); - self.item = InnerMultipartItem::Field(Rc::clone(&field)); - - Poll::Ready(Some(Ok(Field::new(safety.clone(cx), headers, mt, field)))) + // nested multipart stream is not supported + if ct.type_() == mime::MULTIPART { + return Poll::Ready(Some(Err(MultipartError::Nested))); } + + let field = + InnerField::new_in_rc(self.payload.clone(), self.boundary.clone(), &headers)?; + + self.item = InnerMultipartItem::Field(Rc::clone(&field)); + + Poll::Ready(Some(Ok(Field::new( + safety.clone(cx), + headers, + ct, + cd, + field, + )))) } } } @@ -371,6 +403,7 @@ impl Drop for InnerMultipart { /// A single field in a multipart stream pub struct Field { ct: mime::Mime, + cd: ContentDisposition, headers: HeaderMap, inner: Rc>, safety: Safety, @@ -381,35 +414,51 @@ impl Field { safety: Safety, headers: HeaderMap, ct: mime::Mime, + cd: ContentDisposition, inner: Rc>, ) -> Self { Field { ct, + cd, headers, inner, safety, } } - /// Get a map of headers + /// Returns a reference to the field's header map. pub fn headers(&self) -> &HeaderMap { &self.headers } - /// Get the content type of the field + /// Returns a reference to the field's content (mime) type. pub fn content_type(&self) -> &mime::Mime { &self.ct } - /// Get the content disposition of the field, if it exists - pub fn content_disposition(&self) -> Option { - // RFC 7578: 'Each part MUST contain a Content-Disposition header field - // where the disposition type is "form-data".' - if let Some(content_disposition) = self.headers.get(&header::CONTENT_DISPOSITION) { - ContentDisposition::from_raw(content_disposition).ok() - } else { - None - } + /// Returns the field's Content-Disposition. + /// + /// Per [RFC 7578 §4.2]: 'Each part MUST contain a Content-Disposition header field where the + /// disposition type is "form-data". The Content-Disposition header field MUST also contain an + /// additional parameter of "name"; the value of the "name" parameter is the original field name + /// from the form.' + /// + /// This crate validates that it exists before returning a `Field`. As such, it is safe to + /// unwrap `.content_disposition().get_name()`. The [name](Self::name) method is provided as + /// a convenience. + /// + /// [RFC 7578 §4.2]: https://datatracker.ietf.org/doc/html/rfc7578#section-4.2 + pub fn content_disposition(&self) -> &ContentDisposition { + &self.cd + } + + /// Returns the field's name. + /// + /// See [content_disposition] regarding guarantees about + pub fn name(&self) -> &str { + self.content_disposition() + .get_name() + .expect("field name should be guaranteed to exist in multipart form-data") } } @@ -451,20 +500,23 @@ struct InnerField { } impl InnerField { + fn new_in_rc( + payload: PayloadRef, + boundary: String, + headers: &HeaderMap, + ) -> Result>, PayloadError> { + Self::new(payload, boundary, headers).map(|this| Rc::new(RefCell::new(this))) + } + fn new( payload: PayloadRef, boundary: String, headers: &HeaderMap, ) -> Result { let len = if let Some(len) = headers.get(&header::CONTENT_LENGTH) { - if let Ok(s) = len.to_str() { - if let Ok(len) = s.parse::() { - Some(len) - } else { - return Err(PayloadError::Incomplete(None)); - } - } else { - return Err(PayloadError::Incomplete(None)); + match len.to_str().ok().and_then(|len| len.parse::().ok()) { + Some(len) => Some(len), + None => return Err(PayloadError::Incomplete(None)), } } else { None @@ -658,9 +710,8 @@ impl Clone for PayloadRef { } } -/// Counter. It tracks of number of clones of payloads and give access to -/// payload only to top most task panics if Safety get destroyed and it not top -/// most task. +/// Counter. It tracks of number of clones of payloads and give access to payload only to top most +/// task panics if Safety get destroyed and it not top most task. #[derive(Debug)] struct Safety { task: LocalWaker, @@ -707,11 +758,12 @@ impl Drop for Safety { if Rc::strong_count(&self.payload) != self.level { self.clean.set(true); } + self.task.wake(); } } -/// Payload buffer +/// Payload buffer. struct PayloadBuffer { eof: bool, buf: BytesMut, @@ -719,7 +771,7 @@ struct PayloadBuffer { } impl PayloadBuffer { - /// Create new `PayloadBuffer` instance + /// Constructs new `PayloadBuffer` instance. fn new(stream: S) -> Self where S: Stream> + 'static, @@ -767,7 +819,7 @@ impl PayloadBuffer { } /// Read until specified ending - pub fn read_until(&mut self, line: &[u8]) -> Result, MultipartError> { + fn read_until(&mut self, line: &[u8]) -> Result, MultipartError> { let res = twoway::find_bytes(&self.buf, line) .map(|idx| self.buf.split_to(idx + line.len()).freeze()); @@ -779,12 +831,12 @@ impl PayloadBuffer { } /// Read bytes until new line delimiter - pub fn readline(&mut self) -> Result, MultipartError> { + fn readline(&mut self) -> Result, MultipartError> { self.read_until(b"\n") } /// Read bytes until new line delimiter or eof - pub fn readline_or_eof(&mut self) -> Result, MultipartError> { + fn readline_or_eof(&mut self) -> Result, MultipartError> { match self.readline() { Err(MultipartError::Incomplete) if self.eof => Ok(Some(self.buf.split().freeze())), line => line, @@ -792,7 +844,7 @@ impl PayloadBuffer { } /// Put unprocessed data back to the buffer - pub fn unprocessed(&mut self, data: Bytes) { + fn unprocessed(&mut self, data: Bytes) { let buf = BytesMut::from(data.as_ref()); let buf = std::mem::replace(&mut self.buf, buf); self.buf.extend_from_slice(&buf); @@ -914,6 +966,7 @@ mod tests { Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ test\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ + Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\ Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ data\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0--\r\n", @@ -965,7 +1018,7 @@ mod tests { let mut multipart = Multipart::new(&headers, payload); match multipart.next().await { Some(Ok(mut field)) => { - let cd = field.content_disposition().unwrap(); + let cd = field.content_disposition(); assert_eq!(cd.disposition, DispositionType::FormData); assert_eq!(cd.parameters[0], DispositionParam::Name("file".into())); @@ -1027,7 +1080,7 @@ mod tests { let mut multipart = Multipart::new(&headers, payload); match multipart.next().await.unwrap() { Ok(mut field) => { - let cd = field.content_disposition().unwrap(); + let cd = field.content_disposition(); assert_eq!(cd.disposition, DispositionType::FormData); assert_eq!(cd.parameters[0], DispositionParam::Name("file".into())); @@ -1182,4 +1235,59 @@ mod tests { _ => unreachable!(), } } + + #[actix_rt::test] + async fn no_content_disposition() { + let bytes = Bytes::from( + "testasdadsad\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ + Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ + test\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n", + ); + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static( + "multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", + ), + ); + let payload = SlowStream::new(bytes); + + let mut multipart = Multipart::new(&headers, payload); + let res = multipart.next().await.unwrap(); + assert!(res.is_err()); + assert!(matches!( + res.unwrap_err(), + MultipartError::NoContentDisposition, + )); + } + + #[actix_rt::test] + async fn no_name_in_content_disposition() { + let bytes = Bytes::from( + "testasdadsad\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ + Content-Disposition: form-data; filename=\"fn.txt\"\r\n\ + Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ + test\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n", + ); + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static( + "multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", + ), + ); + let payload = SlowStream::new(bytes); + + let mut multipart = Multipart::new(&headers, payload); + let res = multipart.next().await.unwrap(); + assert!(res.is_err()); + assert!(matches!( + res.unwrap_err(), + MultipartError::NoContentDisposition, + )); + } } diff --git a/src/http/header/content_disposition.rs b/src/http/header/content_disposition.rs index fdd8a7dac..6d07a41bd 100644 --- a/src/http/header/content_disposition.rs +++ b/src/http/header/content_disposition.rs @@ -34,15 +34,18 @@ fn split_once_and_trim(haystack: &str, needle: char) -> (&str, &str) { /// The implied disposition of the content of the HTTP body. #[derive(Clone, Debug, PartialEq)] pub enum DispositionType { - /// Inline implies default processing + /// Inline implies default processing. Inline, + /// Attachment implies that the recipient should prompt the user to save the response locally, /// rather than process it normally (as per its media type). Attachment, - /// Used in *multipart/form-data* as defined in - /// [RFC7578](https://tools.ietf.org/html/rfc7578) to carry the field name and the file name. + + /// Used in *multipart/form-data* as defined in [RFC7578](https://tools.ietf.org/html/rfc7578) + /// to carry the field name and optional filename. FormData, - /// Extension type. Should be handled by recipients the same way as Attachment + + /// Extension type. Should be handled by recipients the same way as Attachment. Ext(String), } @@ -76,6 +79,7 @@ pub enum DispositionParam { /// For [`DispositionType::FormData`] (i.e. *multipart/form-data*), the name of an field from /// the form. Name(String), + /// A plain file name. /// /// It is [not supposed](https://tools.ietf.org/html/rfc6266#appendix-D) to contain any @@ -83,14 +87,17 @@ pub enum DispositionParam { /// [`FilenameExt`](DispositionParam::FilenameExt) with charset UTF-8 may be used instead /// in case there are Unicode characters in file names. Filename(String), + /// An extended file name. It must not exist for `ContentType::Formdata` according to /// [RFC7578 Section 4.2](https://tools.ietf.org/html/rfc7578#section-4.2). FilenameExt(ExtendedValue), + /// An unrecognized regular parameter as defined in /// [RFC5987](https://tools.ietf.org/html/rfc5987) as *reg-parameter*, in /// [RFC6266](https://tools.ietf.org/html/rfc6266) as *token "=" value*. Recipients should /// ignore unrecognizable parameters. Unknown(String, String), + /// An unrecognized extended parameter as defined in /// [RFC5987](https://tools.ietf.org/html/rfc5987) as *ext-parameter*, in /// [RFC6266](https://tools.ietf.org/html/rfc6266) as *ext-token "=" ext-value*. The single @@ -205,7 +212,6 @@ impl DispositionParam { /// itself, *Content-Disposition* has no effect. /// /// # ABNF - /// ```text /// content-disposition = "Content-Disposition" ":" /// disposition-type *( ";" disposition-parm ) @@ -289,10 +295,12 @@ impl DispositionParam { /// If "filename" parameter is supplied, do not use the file name blindly, check and possibly /// change to match local file system conventions if applicable, and do not use directory path /// information that may be present. See [RFC2183](https://tools.ietf.org/html/rfc2183#section-2.3). +// TODO: private fields and use smallvec #[derive(Clone, Debug, PartialEq)] pub struct ContentDisposition { /// The disposition type pub disposition: DispositionType, + /// Disposition parameters pub parameters: Vec, } @@ -509,22 +517,28 @@ impl fmt::Display for DispositionParam { // // // See also comments in test_from_raw_unnecessary_percent_decode. + static RE: Lazy = Lazy::new(|| Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap()); + match self { DispositionParam::Name(ref value) => write!(f, "name={}", value), + DispositionParam::Filename(ref value) => { write!(f, "filename=\"{}\"", RE.replace_all(value, "\\$0").as_ref()) } + DispositionParam::Unknown(ref name, ref value) => write!( f, "{}=\"{}\"", name, &RE.replace_all(value, "\\$0").as_ref() ), + DispositionParam::FilenameExt(ref ext_value) => { write!(f, "filename*={}", ext_value) } + DispositionParam::UnknownExt(ref name, ref ext_value) => { write!(f, "{}*={}", name, ext_value) } From 66620a101211c424ffc7e2f1b1c7bb9e2a78ff87 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Wed, 17 Nov 2021 23:11:35 +0300 Subject: [PATCH 88/88] simplify handler.rs (#2450) --- src/handler.rs | 152 ++++++------------------------------------------- src/route.rs | 6 +- 2 files changed, 21 insertions(+), 137 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index bc91ce41b..ddefe8d53 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,16 +1,13 @@ use std::future::Future; -use std::marker::PhantomData; -use std::pin::Pin; -use std::task::{Context, Poll}; -use actix_service::{Service, ServiceFactory}; -use actix_utils::future::{ready, Ready}; -use futures_core::ready; -use pin_project::pin_project; +use actix_service::{ + boxed::{self, BoxServiceFactory}, + fn_service, +}; use crate::{ service::{ServiceRequest, ServiceResponse}, - Error, FromRequest, HttpRequest, HttpResponse, Responder, + Error, FromRequest, HttpResponse, Responder, }; /// A request handler is an async function that accepts zero or more parameters that can be @@ -27,139 +24,26 @@ where fn call(&self, param: T) -> R; } -#[doc(hidden)] -/// Extract arguments from request, run factory function and make response. -pub struct HandlerService +pub fn handler_service( + handler: F, +) -> BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()> where F: Handler, T: FromRequest, R: Future, R::Output: Responder, { - hnd: F, - _phantom: PhantomData<(T, R)>, -} - -impl HandlerService -where - F: Handler, - T: FromRequest, - R: Future, - R::Output: Responder, -{ - pub fn new(hnd: F) -> Self { - Self { - hnd, - _phantom: PhantomData, + boxed::factory(fn_service(move |req: ServiceRequest| { + let handler = handler.clone(); + async move { + let (req, mut payload) = req.into_parts(); + let res = match T::from_request(&req, &mut payload).await { + Err(err) => HttpResponse::from_error(err), + Ok(data) => handler.call(data).await.respond_to(&req), + }; + Ok(ServiceResponse::new(req, res)) } - } -} - -impl Clone for HandlerService -where - F: Handler, - T: FromRequest, - R: Future, - R::Output: Responder, -{ - fn clone(&self) -> Self { - Self { - hnd: self.hnd.clone(), - _phantom: PhantomData, - } - } -} - -impl ServiceFactory for HandlerService -where - F: Handler, - T: FromRequest, - R: Future, - R::Output: Responder, -{ - type Response = ServiceResponse; - type Error = Error; - type Config = (); - type Service = Self; - type InitError = (); - type Future = Ready>; - - fn new_service(&self, _: ()) -> Self::Future { - ready(Ok(self.clone())) - } -} - -/// HandlerService is both it's ServiceFactory and Service Type. -impl Service for HandlerService -where - F: Handler, - T: FromRequest, - R: Future, - R::Output: Responder, -{ - type Response = ServiceResponse; - type Error = Error; - type Future = HandlerServiceFuture; - - actix_service::always_ready!(); - - fn call(&self, req: ServiceRequest) -> Self::Future { - let (req, mut payload) = req.into_parts(); - let fut = T::from_request(&req, &mut payload); - HandlerServiceFuture::Extract(fut, Some(req), self.hnd.clone()) - } -} - -#[doc(hidden)] -#[pin_project(project = HandlerProj)] -pub enum HandlerServiceFuture -where - F: Handler, - T: FromRequest, - R: Future, - R::Output: Responder, -{ - Extract(#[pin] T::Future, Option, F), - Handle(#[pin] R, Option), -} - -impl Future for HandlerServiceFuture -where - F: Handler, - T: FromRequest, - R: Future, - R::Output: Responder, -{ - // Error type in this future is a placeholder type. - // all instances of error must be converted to ServiceResponse and return in Ok. - type Output = Result; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - loop { - match self.as_mut().project() { - HandlerProj::Extract(fut, req, handle) => { - match ready!(fut.poll(cx)) { - Ok(item) => { - let fut = handle.call(item); - let state = HandlerServiceFuture::Handle(fut, req.take()); - self.as_mut().set(state); - } - Err(err) => { - let req = req.take().unwrap(); - let res = HttpResponse::from_error(err.into()); - return Poll::Ready(Ok(ServiceResponse::new(req, res))); - } - }; - } - HandlerProj::Handle(fut, req) => { - let res = ready!(fut.poll(cx)); - let req = req.take().unwrap(); - let res = res.respond_to(&req); - return Poll::Ready(Ok(ServiceResponse::new(req, res))); - } - } - } - } + })) } /// FromRequest trait impl for tuples diff --git a/src/route.rs b/src/route.rs index d85b940bd..0c0699430 100644 --- a/src/route.rs +++ b/src/route.rs @@ -11,7 +11,7 @@ use futures_core::future::LocalBoxFuture; use crate::{ guard::{self, Guard}, - handler::{Handler, HandlerService}, + handler::{handler_service, Handler}, service::{ServiceRequest, ServiceResponse}, Error, FromRequest, HttpResponse, Responder, }; @@ -30,7 +30,7 @@ impl Route { #[allow(clippy::new_without_default)] pub fn new() -> Route { Route { - service: boxed::factory(HandlerService::new(HttpResponse::NotFound)), + service: handler_service(HttpResponse::NotFound), guards: Rc::new(Vec::new()), } } @@ -182,7 +182,7 @@ impl Route { R: Future + 'static, R::Output: Responder + 'static, { - self.service = boxed::factory(HandlerService::new(handler)); + self.service = handler_service(handler); self }