From e72ee2823261c4a08dfd4133963ae6161b6f7ff9 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 17 Jun 2020 00:58:23 -0700 Subject: [PATCH 01/78] Enforce HW_BUFFER_SIZE inside h1::dispatcher (#1550) --- CHANGES.md | 6 ++++++ actix-http/src/h1/dispatcher.rs | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 739f1b13c..3b4b8dc0b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Changes +## [Unreleased] + +### Changed + +* Fix actix_http::h1::dispatcher so it returns when HW_BUFFER_SIZE is reached. Should reduce peak memory consumption during large uploads. [#1550] + ## [3.0.0-alpha.3] - 2020-05-21 ### Added diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index e16d3536f..51f107efb 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -861,7 +861,14 @@ where T: AsyncRead + Unpin, { let mut read_some = false; + loop { + // If buf is full return but do not disconnect since + // there is more reading to be done + if buf.len() >= HW_BUFFER_SIZE { + return Ok(Some(false)); + } + let remaining = buf.capacity() - buf.len(); if remaining < LW_BUFFER_SIZE { buf.reserve(HW_BUFFER_SIZE - remaining); From 9af07d66ae092a0efe90829951f22aa948bc761f Mon Sep 17 00:00:00 2001 From: Fabianstelmach Date: Wed, 17 Jun 2020 11:54:20 +0200 Subject: [PATCH 02/78] Fix NormalizePath trailing slash behavior (#1548) --- CHANGES.md | 4 ++++ src/middleware/normalize.rs | 31 ++++++++++++++++++++++++++----- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3b4b8dc0b..3c21c301d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,10 @@ * Fix actix_http::h1::dispatcher so it returns when HW_BUFFER_SIZE is reached. Should reduce peak memory consumption during large uploads. [#1550] +### Fixed + +* `NormalizePath` improved consistency when path needs slashes added _and_ removed. + ## [3.0.0-alpha.3] - 2020-05-21 ### Added diff --git a/src/middleware/normalize.rs b/src/middleware/normalize.rs index d23f03445..3d5d30c80 100644 --- a/src/middleware/normalize.rs +++ b/src/middleware/normalize.rs @@ -16,6 +16,7 @@ use crate::Error; /// Performs following: /// /// - Merges multiple slashes into one. +/// - Appends a trailing slash if one is not present. /// /// ```rust /// use actix_web::{web, http, middleware, App, HttpResponse}; @@ -75,14 +76,26 @@ where fn call(&mut self, mut req: ServiceRequest) -> Self::Future { let head = req.head_mut(); + let original_path = head.uri.path(); + // always add trailing slash, might be an extra one - let path = head.uri.path().to_string() + "/"; - let original_len = path.len(); + let path = original_path.to_string() + "/"; // normalize multiple /'s to one / let path = self.merge_slash.replace_all(&path, "/"); - if original_len != path.len() { + // Check whether the path has been changed + // + // This check was previously implemented as string length comparison + // + // That approach fails when a trailing slash is added, + // and a duplicate slash is removed, + // since the length of the strings remains the same + // + // For example, the path "/v1//s" will be normalized to "/v1/s/" + // Both of the paths have the same length, + // so the change can not be deduced from the length comparison + if path != original_path { let mut parts = head.uri.clone().into_parts(); let pq = parts.path_and_query.as_ref().unwrap(); @@ -131,6 +144,10 @@ mod tests { let req3 = TestRequest::with_uri("//v1//////something").to_request(); let res3 = call_service(&mut app, req3).await; assert!(res3.status().is_success()); + + let req4 = TestRequest::with_uri("/v1//something").to_request(); + let res4 = call_service(&mut app, req4).await; + assert!(res4.status().is_success()); } #[actix_rt::test] @@ -156,6 +173,10 @@ mod tests { let req3 = TestRequest::with_uri("//v1///something").to_srv_request(); let res3 = normalize.call(req3).await.unwrap(); assert!(res3.status().is_success()); + + let req4 = TestRequest::with_uri("/v1//something").to_srv_request(); + let res4 = normalize.call(req4).await.unwrap(); + assert!(res4.status().is_success()); } #[actix_rt::test] @@ -178,11 +199,11 @@ mod tests { } #[actix_rt::test] - async fn should_normalize_nothing_notrail() { + async fn should_normalize_notrail() { const URI: &str = "/v1/something"; let srv = |req: ServiceRequest| { - assert_eq!(URI, req.path()); + assert_eq!(URI.to_string() + "/", req.path()); ok(req.into_response(HttpResponse::Ok().finish())) }; From 0ba73fc44c7771975f761c5e2704d95ac5b18b10 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 18 Jun 2020 11:23:36 +0100 Subject: [PATCH 03/78] exclude default -web features on -actors (#1562) --- actix-web-actors/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 8db7a35ef..746aca2d5 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -17,7 +17,7 @@ path = "src/lib.rs" [dependencies] actix = "0.10.0-alpha.2" -actix-web = "3.0.0-alpha.3" +actix-web = { version = "3.0.0-alpha.3", default-features = false } actix-http = "2.0.0-alpha.4" actix-codec = "0.2.0" bytes = "0.5.2" From dc74db1f2f6a0686410633531960807836bb38b6 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 18 Jun 2020 15:45:30 +0100 Subject: [PATCH 04/78] re-export actix_rt::main macro (#1559) --- CHANGES.md | 4 ++++ Cargo.toml | 2 +- README.md | 3 +-- examples/basic.rs | 2 +- examples/client.rs | 2 +- examples/uds.rs | 2 +- src/lib.rs | 1 + 7 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3c21c301d..c2dd43973 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +* Re-export `actix_rt::main` as `actix_web::main`. + ### Changed * Fix actix_http::h1::dispatcher so it returns when HW_BUFFER_SIZE is reached. Should reduce peak memory consumption during large uploads. [#1550] diff --git a/Cargo.toml b/Cargo.toml index 8c6d461d6..ade432c6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,7 @@ actix-codec = "0.2.0" actix-service = "1.0.2" actix-utils = "1.0.6" actix-router = "0.2.4" -actix-rt = "1.0.0" +actix-rt = "1.1.1" actix-server = "1.0.0" actix-testing = "1.0.0" actix-macros = "0.1.0" diff --git a/README.md b/README.md index 97e3ceeae..6382abd4d 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,6 @@ Dependencies: ```toml [dependencies] actix-web = "2" -actix-rt = "1" ``` Code: @@ -66,7 +65,7 @@ async fn index(info: web::Path<(u32, String)>) -> impl Responder { format!("Hello {}! id:{}", info.1, info.0) } -#[actix_rt::main] +#[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| App::new().service(index)) .bind("127.0.0.1:8080")? diff --git a/examples/basic.rs b/examples/basic.rs index bd6f8146f..8b2bf2319 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -16,7 +16,7 @@ async fn no_params() -> &'static str { "Hello world!\r\n" } -#[actix_rt::main] +#[actix_web::main] async fn main() -> std::io::Result<()> { std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info"); env_logger::init(); diff --git a/examples/client.rs b/examples/client.rs index 874e08e1b..15cf24fa8 100644 --- a/examples/client.rs +++ b/examples/client.rs @@ -1,6 +1,6 @@ use actix_http::Error; -#[actix_rt::main] +#[actix_web::main] async fn main() -> Result<(), Error> { std::env::set_var("RUST_LOG", "actix_http=trace"); env_logger::init(); diff --git a/examples/uds.rs b/examples/uds.rs index 06aa87f4d..e34fa5ac9 100644 --- a/examples/uds.rs +++ b/examples/uds.rs @@ -20,7 +20,7 @@ async fn no_params() -> &'static str { } #[cfg(unix)] -#[actix_rt::main] +#[actix_web::main] async fn main() -> std::io::Result<()> { std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info"); env_logger::init(); diff --git a/src/lib.rs b/src/lib.rs index cff4acf27..09642806f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -112,6 +112,7 @@ pub use actix_web_codegen::*; // re-export for convenience pub use actix_http::Response as HttpResponse; pub use actix_http::{body, cookie, http, Error, HttpMessage, ResponseError, Result}; +pub use actix_macros::{main, test as test_rt}; pub use crate::app::App; pub use crate::extract::FromRequest; From eb0eda69c6c986475e927bf707c8dd86bda4d70e Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 19 Jun 2020 14:34:14 +0100 Subject: [PATCH 05/78] migrate cookie handling to cookie crate (#1558) --- CHANGES.md | 1 + Cargo.toml | 2 +- actix-http/CHANGES.md | 6 + actix-http/Cargo.toml | 6 +- actix-http/src/cookie/builder.rs | 252 ------ actix-http/src/cookie/delta.rs | 71 -- actix-http/src/cookie/draft.rs | 106 --- actix-http/src/cookie/jar.rs | 651 -------------- actix-http/src/cookie/mod.rs | 1100 ----------------------- actix-http/src/cookie/parse.rs | 467 ---------- actix-http/src/cookie/secure/key.rs | 190 ---- actix-http/src/cookie/secure/macros.rs | 40 - actix-http/src/cookie/secure/mod.rs | 10 - actix-http/src/cookie/secure/private.rs | 275 ------ actix-http/src/cookie/secure/signed.rs | 184 ---- actix-http/src/lib.rs | 2 +- actix-http/src/response.rs | 2 +- actix-http/src/test.rs | 24 +- awc/src/request.rs | 24 +- awc/src/test.rs | 14 +- awc/src/ws.rs | 23 +- 21 files changed, 49 insertions(+), 3401 deletions(-) delete mode 100644 actix-http/src/cookie/builder.rs delete mode 100644 actix-http/src/cookie/delta.rs delete mode 100644 actix-http/src/cookie/draft.rs delete mode 100644 actix-http/src/cookie/jar.rs delete mode 100644 actix-http/src/cookie/mod.rs delete mode 100644 actix-http/src/cookie/parse.rs delete mode 100644 actix-http/src/cookie/secure/key.rs delete mode 100644 actix-http/src/cookie/secure/macros.rs delete mode 100644 actix-http/src/cookie/secure/mod.rs delete mode 100644 actix-http/src/cookie/secure/private.rs delete mode 100644 actix-http/src/cookie/secure/signed.rs diff --git a/CHANGES.md b/CHANGES.md index c2dd43973..58cddf78d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ ### Changed * Fix actix_http::h1::dispatcher so it returns when HW_BUFFER_SIZE is reached. Should reduce peak memory consumption during large uploads. [#1550] +* Migrate cookie handling to `cookie` crate. Actix-web no longer requires `ring` dependency. ### Fixed diff --git a/Cargo.toml b/Cargo.toml index ade432c6d..de7222f1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ default = ["compress"] # content-encoding support compress = ["actix-http/compress", "awc/compress"] -# sessions feature, session require "ring" crate and c compiler +# sessions feature secure-cookies = ["actix-http/secure-cookies"] # openssl diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index c19e40e4c..49599b9be 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,5 +1,11 @@ # Changes +## [Unreleased] + +### Changed + +* Migrate cookie handling to `cookie` crate. + ## [2.0.0-alpha.4] - 2020-05-21 ### Changed diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index d2ae7698e..8b9b8c825 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -34,7 +34,7 @@ rustls = ["actix-tls/rustls", "actix-connect/rustls"] compress = ["flate2", "brotli2"] # support for secure cookies -secure-cookies = ["ring"] +secure-cookies = ["cookie/secure"] # support for actix Actor messages actors = ["actix"] @@ -52,6 +52,7 @@ actix = { version = "0.10.0-alpha.1", optional = true } base64 = "0.12" bitflags = "1.2" bytes = "0.5.3" +cookie = { version = "0.14.1", features = ["percent-encode"] } copyless = "0.1.4" derive_more = "0.99.2" either = "1.5.3" @@ -80,9 +81,6 @@ slab = "0.4" serde_urlencoded = "0.6.1" time = { version = "0.2.7", default-features = false, features = ["std"] } -# for secure cookie -ring = { version = "0.16.9", optional = true } - # compression brotli2 = { version="0.3.2", optional = true } flate2 = { version = "1.0.13", optional = true } diff --git a/actix-http/src/cookie/builder.rs b/actix-http/src/cookie/builder.rs deleted file mode 100644 index b64352e35..000000000 --- a/actix-http/src/cookie/builder.rs +++ /dev/null @@ -1,252 +0,0 @@ -use std::borrow::Cow; - -use time::{Duration, OffsetDateTime}; - -use super::{Cookie, SameSite}; - -/// Structure that follows the builder pattern for building `Cookie` structs. -/// -/// To construct a cookie: -/// -/// 1. Call [`Cookie::build`](struct.Cookie.html#method.build) to start building. -/// 2. Use any of the builder methods to set fields in the cookie. -/// 3. Call [finish](#method.finish) to retrieve the built cookie. -/// -/// # Example -/// -/// ```rust -/// use actix_http::cookie::Cookie; -/// -/// let cookie: Cookie = Cookie::build("name", "value") -/// .domain("www.rust-lang.org") -/// .path("/") -/// .secure(true) -/// .http_only(true) -/// .max_age(84600) -/// .finish(); -/// ``` -#[derive(Debug, Clone)] -pub struct CookieBuilder { - /// The cookie being built. - cookie: Cookie<'static>, -} - -impl CookieBuilder { - /// Creates a new `CookieBuilder` instance from the given name and value. - /// - /// This method is typically called indirectly via - /// [Cookie::build](struct.Cookie.html#method.build). - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::build("foo", "bar").finish(); - /// assert_eq!(c.name_value(), ("foo", "bar")); - /// ``` - pub fn new(name: N, value: V) -> CookieBuilder - where - N: Into>, - V: Into>, - { - CookieBuilder { - cookie: Cookie::new(name, value), - } - } - - /// Sets the `expires` field in the cookie being built. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::build("foo", "bar") - /// .expires(time::OffsetDateTime::now_utc()) - /// .finish(); - /// - /// assert!(c.expires().is_some()); - /// ``` - #[inline] - pub fn expires(mut self, when: OffsetDateTime) -> CookieBuilder { - self.cookie.set_expires(when); - self - } - - /// Sets the `max_age` field in seconds in the cookie being built. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::build("foo", "bar") - /// .max_age(1800) - /// .finish(); - /// - /// assert_eq!(c.max_age(), Some(time::Duration::seconds(30 * 60))); - /// ``` - #[inline] - pub fn max_age(self, seconds: i64) -> CookieBuilder { - self.max_age_time(Duration::seconds(seconds)) - } - - /// Sets the `max_age` field in the cookie being built. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::build("foo", "bar") - /// .max_age_time(time::Duration::minutes(30)) - /// .finish(); - /// - /// assert_eq!(c.max_age(), Some(time::Duration::seconds(30 * 60))); - /// ``` - #[inline] - pub fn max_age_time(mut self, value: Duration) -> CookieBuilder { - // Truncate any nanoseconds from the Duration, as they aren't represented within `Max-Age` - // and would cause two otherwise identical `Cookie` instances to not be equivalent to one another. - self.cookie - .set_max_age(Duration::seconds(value.whole_seconds())); - self - } - - /// Sets the `domain` field in the cookie being built. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::build("foo", "bar") - /// .domain("www.rust-lang.org") - /// .finish(); - /// - /// assert_eq!(c.domain(), Some("www.rust-lang.org")); - /// ``` - pub fn domain>>(mut self, value: D) -> CookieBuilder { - self.cookie.set_domain(value); - self - } - - /// Sets the `path` field in the cookie being built. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::build("foo", "bar") - /// .path("/") - /// .finish(); - /// - /// assert_eq!(c.path(), Some("/")); - /// ``` - pub fn path>>(mut self, path: P) -> CookieBuilder { - self.cookie.set_path(path); - self - } - - /// Sets the `secure` field in the cookie being built. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::build("foo", "bar") - /// .secure(true) - /// .finish(); - /// - /// assert_eq!(c.secure(), Some(true)); - /// ``` - #[inline] - pub fn secure(mut self, value: bool) -> CookieBuilder { - self.cookie.set_secure(value); - self - } - - /// Sets the `http_only` field in the cookie being built. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::build("foo", "bar") - /// .http_only(true) - /// .finish(); - /// - /// assert_eq!(c.http_only(), Some(true)); - /// ``` - #[inline] - pub fn http_only(mut self, value: bool) -> CookieBuilder { - self.cookie.set_http_only(value); - self - } - - /// Sets the `same_site` field in the cookie being built. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{Cookie, SameSite}; - /// - /// let c = Cookie::build("foo", "bar") - /// .same_site(SameSite::Strict) - /// .finish(); - /// - /// assert_eq!(c.same_site(), Some(SameSite::Strict)); - /// ``` - #[inline] - pub fn same_site(mut self, value: SameSite) -> CookieBuilder { - self.cookie.set_same_site(value); - self - } - - /// Makes the cookie being built 'permanent' by extending its expiration and - /// max age 20 years into the future. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// use time::Duration; - /// - /// let c = Cookie::build("foo", "bar") - /// .permanent() - /// .finish(); - /// - /// assert_eq!(c.max_age(), Some(Duration::days(365 * 20))); - /// # assert!(c.expires().is_some()); - /// ``` - #[inline] - pub fn permanent(mut self) -> CookieBuilder { - self.cookie.make_permanent(); - self - } - - /// Finishes building and returns the built `Cookie`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::build("foo", "bar") - /// .domain("crates.io") - /// .path("/") - /// .finish(); - /// - /// assert_eq!(c.name_value(), ("foo", "bar")); - /// assert_eq!(c.domain(), Some("crates.io")); - /// assert_eq!(c.path(), Some("/")); - /// ``` - #[inline] - pub fn finish(self) -> Cookie<'static> { - self.cookie - } -} diff --git a/actix-http/src/cookie/delta.rs b/actix-http/src/cookie/delta.rs deleted file mode 100644 index a001a5bb8..000000000 --- a/actix-http/src/cookie/delta.rs +++ /dev/null @@ -1,71 +0,0 @@ -use std::borrow::Borrow; -use std::hash::{Hash, Hasher}; -use std::ops::{Deref, DerefMut}; - -use super::Cookie; - -/// A `DeltaCookie` is a helper structure used in a cookie jar. It wraps a -/// `Cookie` so that it can be hashed and compared purely by name. It further -/// records whether the wrapped cookie is a "removal" cookie, that is, a cookie -/// that when sent to the client removes the named cookie on the client's -/// machine. -#[derive(Clone, Debug)] -pub struct DeltaCookie { - pub cookie: Cookie<'static>, - pub removed: bool, -} - -impl DeltaCookie { - /// Create a new `DeltaCookie` that is being added to a jar. - #[inline] - pub fn added(cookie: Cookie<'static>) -> DeltaCookie { - DeltaCookie { - cookie, - removed: false, - } - } - - /// Create a new `DeltaCookie` that is being removed from a jar. The - /// `cookie` should be a "removal" cookie. - #[inline] - pub fn removed(cookie: Cookie<'static>) -> DeltaCookie { - DeltaCookie { - cookie, - removed: true, - } - } -} - -impl Deref for DeltaCookie { - type Target = Cookie<'static>; - - fn deref(&self) -> &Cookie<'static> { - &self.cookie - } -} - -impl DerefMut for DeltaCookie { - fn deref_mut(&mut self) -> &mut Cookie<'static> { - &mut self.cookie - } -} - -impl PartialEq for DeltaCookie { - fn eq(&self, other: &DeltaCookie) -> bool { - self.name() == other.name() - } -} - -impl Eq for DeltaCookie {} - -impl Hash for DeltaCookie { - fn hash(&self, state: &mut H) { - self.name().hash(state); - } -} - -impl Borrow for DeltaCookie { - fn borrow(&self) -> &str { - self.name() - } -} diff --git a/actix-http/src/cookie/draft.rs b/actix-http/src/cookie/draft.rs deleted file mode 100644 index a6525a605..000000000 --- a/actix-http/src/cookie/draft.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! This module contains types that represent cookie properties that are not yet -//! standardized. That is, _draft_ features. - -use std::fmt; - -/// The `SameSite` cookie attribute. -/// -/// A cookie with a `SameSite` attribute is imposed restrictions on when it is -/// sent to the origin server in a cross-site request. If the `SameSite` -/// attribute is "Strict", then the cookie is never sent in cross-site requests. -/// If the `SameSite` attribute is "Lax", the cookie is only sent in cross-site -/// requests with "safe" HTTP methods, i.e, `GET`, `HEAD`, `OPTIONS`, `TRACE`. -/// If the `SameSite` attribute is not present then the cookie will be sent as -/// normal. In some browsers, this will implicitly handle the cookie as if "Lax" -/// and in others, "None". It's best to explicitly set the `SameSite` attribute -/// to avoid inconsistent behavior. -/// -/// **Note:** Depending on browser, the `Secure` attribute may be required for -/// `SameSite` "None" cookies to be accepted. -/// -/// **Note:** This cookie attribute is an HTTP draft! Its meaning and definition -/// are subject to change. -/// -/// More info about these draft changes can be found in the draft spec: -/// - https://tools.ietf.org/html/draft-west-cookie-incrementalism-00 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum SameSite { - /// The "Strict" `SameSite` attribute. - Strict, - /// The "Lax" `SameSite` attribute. - Lax, - /// The "None" `SameSite` attribute. - None, -} - -impl SameSite { - /// Returns `true` if `self` is `SameSite::Strict` and `false` otherwise. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::SameSite; - /// - /// let strict = SameSite::Strict; - /// assert!(strict.is_strict()); - /// assert!(!strict.is_lax()); - /// assert!(!strict.is_none()); - /// ``` - #[inline] - pub fn is_strict(self) -> bool { - match self { - SameSite::Strict => true, - SameSite::Lax | SameSite::None => false, - } - } - - /// Returns `true` if `self` is `SameSite::Lax` and `false` otherwise. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::SameSite; - /// - /// let lax = SameSite::Lax; - /// assert!(lax.is_lax()); - /// assert!(!lax.is_strict()); - /// assert!(!lax.is_none()); - /// ``` - #[inline] - pub fn is_lax(self) -> bool { - match self { - SameSite::Lax => true, - SameSite::Strict | SameSite::None => false, - } - } - - /// Returns `true` if `self` is `SameSite::None` and `false` otherwise. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::SameSite; - /// - /// let none = SameSite::None; - /// assert!(none.is_none()); - /// assert!(!none.is_lax()); - /// assert!(!none.is_strict()); - /// ``` - #[inline] - pub fn is_none(self) -> bool { - match self { - SameSite::None => true, - SameSite::Lax | SameSite::Strict => false, - } - } -} - -impl fmt::Display for SameSite { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - SameSite::Strict => write!(f, "Strict"), - SameSite::Lax => write!(f, "Lax"), - SameSite::None => write!(f, "None"), - } - } -} diff --git a/actix-http/src/cookie/jar.rs b/actix-http/src/cookie/jar.rs deleted file mode 100644 index fbefa1bbf..000000000 --- a/actix-http/src/cookie/jar.rs +++ /dev/null @@ -1,651 +0,0 @@ -use std::collections::HashSet; -use std::mem; - -use time::{Duration, OffsetDateTime}; - -use super::delta::DeltaCookie; -use super::Cookie; - -#[cfg(feature = "secure-cookies")] -use super::secure::{Key, PrivateJar, SignedJar}; - -/// A collection of cookies that tracks its modifications. -/// -/// A `CookieJar` provides storage for any number of cookies. Any changes made -/// to the jar are tracked; the changes can be retrieved via the -/// [delta](#method.delta) method which returns an iterator over the changes. -/// -/// # Usage -/// -/// A jar's life begins via [new](#method.new) and calls to -/// [`add_original`](#method.add_original): -/// -/// ```rust -/// use actix_http::cookie::{Cookie, CookieJar}; -/// -/// let mut jar = CookieJar::new(); -/// jar.add_original(Cookie::new("name", "value")); -/// jar.add_original(Cookie::new("second", "another")); -/// ``` -/// -/// Cookies can be added via [add](#method.add) and removed via -/// [remove](#method.remove). Finally, cookies can be looked up via -/// [get](#method.get): -/// -/// ```rust -/// # use actix_http::cookie::{Cookie, CookieJar}; -/// let mut jar = CookieJar::new(); -/// jar.add(Cookie::new("a", "one")); -/// jar.add(Cookie::new("b", "two")); -/// -/// assert_eq!(jar.get("a").map(|c| c.value()), Some("one")); -/// assert_eq!(jar.get("b").map(|c| c.value()), Some("two")); -/// -/// jar.remove(Cookie::named("b")); -/// assert!(jar.get("b").is_none()); -/// ``` -/// -/// # Deltas -/// -/// A jar keeps track of any modifications made to it over time. The -/// modifications are recorded as cookies. The modifications can be retrieved -/// via [delta](#method.delta). Any new `Cookie` added to a jar via `add` -/// results in the same `Cookie` appearing in the `delta`; cookies added via -/// `add_original` do not count towards the delta. Any _original_ cookie that is -/// removed from a jar results in a "removal" cookie appearing in the delta. A -/// "removal" cookie is a cookie that a server sends so that the cookie is -/// removed from the client's machine. -/// -/// Deltas are typically used to create `Set-Cookie` headers corresponding to -/// the changes made to a cookie jar over a period of time. -/// -/// ```rust -/// # use actix_http::cookie::{Cookie, CookieJar}; -/// let mut jar = CookieJar::new(); -/// -/// // original cookies don't affect the delta -/// jar.add_original(Cookie::new("original", "value")); -/// assert_eq!(jar.delta().count(), 0); -/// -/// // new cookies result in an equivalent `Cookie` in the delta -/// jar.add(Cookie::new("a", "one")); -/// jar.add(Cookie::new("b", "two")); -/// assert_eq!(jar.delta().count(), 2); -/// -/// // removing an original cookie adds a "removal" cookie to the delta -/// jar.remove(Cookie::named("original")); -/// assert_eq!(jar.delta().count(), 3); -/// -/// // removing a new cookie that was added removes that `Cookie` from the delta -/// jar.remove(Cookie::named("a")); -/// assert_eq!(jar.delta().count(), 2); -/// ``` -#[derive(Default, Debug, Clone)] -pub struct CookieJar { - original_cookies: HashSet, - delta_cookies: HashSet, -} - -impl CookieJar { - /// Creates an empty cookie jar. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::CookieJar; - /// - /// let jar = CookieJar::new(); - /// assert_eq!(jar.iter().count(), 0); - /// ``` - pub fn new() -> CookieJar { - CookieJar::default() - } - - /// Returns a reference to the `Cookie` inside this jar with the name - /// `name`. If no such cookie exists, returns `None`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie}; - /// - /// let mut jar = CookieJar::new(); - /// assert!(jar.get("name").is_none()); - /// - /// jar.add(Cookie::new("name", "value")); - /// assert_eq!(jar.get("name").map(|c| c.value()), Some("value")); - /// ``` - pub fn get(&self, name: &str) -> Option<&Cookie<'static>> { - self.delta_cookies - .get(name) - .or_else(|| self.original_cookies.get(name)) - .and_then(|c| if !c.removed { Some(&c.cookie) } else { None }) - } - - /// Adds an "original" `cookie` to this jar. If an original cookie with the - /// same name already exists, it is replaced with `cookie`. Cookies added - /// with `add` take precedence and are not replaced by this method. - /// - /// Adding an original cookie does not affect the [delta](#method.delta) - /// computation. This method is intended to be used to seed the cookie jar - /// with cookies received from a client's HTTP message. - /// - /// For accurate `delta` computations, this method should not be called - /// after calling `remove`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie}; - /// - /// let mut jar = CookieJar::new(); - /// jar.add_original(Cookie::new("name", "value")); - /// jar.add_original(Cookie::new("second", "two")); - /// - /// assert_eq!(jar.get("name").map(|c| c.value()), Some("value")); - /// assert_eq!(jar.get("second").map(|c| c.value()), Some("two")); - /// assert_eq!(jar.iter().count(), 2); - /// assert_eq!(jar.delta().count(), 0); - /// ``` - pub fn add_original(&mut self, cookie: Cookie<'static>) { - self.original_cookies.replace(DeltaCookie::added(cookie)); - } - - /// Adds `cookie` to this jar. If a cookie with the same name already - /// exists, it is replaced with `cookie`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie}; - /// - /// let mut jar = CookieJar::new(); - /// jar.add(Cookie::new("name", "value")); - /// jar.add(Cookie::new("second", "two")); - /// - /// assert_eq!(jar.get("name").map(|c| c.value()), Some("value")); - /// assert_eq!(jar.get("second").map(|c| c.value()), Some("two")); - /// assert_eq!(jar.iter().count(), 2); - /// assert_eq!(jar.delta().count(), 2); - /// ``` - pub fn add(&mut self, cookie: Cookie<'static>) { - self.delta_cookies.replace(DeltaCookie::added(cookie)); - } - - /// Removes `cookie` from this jar. If an _original_ cookie with the same - /// name as `cookie` is present in the jar, a _removal_ cookie will be - /// present in the `delta` computation. To properly generate the removal - /// cookie, `cookie` must contain the same `path` and `domain` as the cookie - /// that was initially set. - /// - /// A "removal" cookie is a cookie that has the same name as the original - /// cookie but has an empty value, a max-age of 0, and an expiration date - /// far in the past. - /// - /// # Example - /// - /// Removing an _original_ cookie results in a _removal_ cookie: - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie}; - /// use time::Duration; - /// - /// let mut jar = CookieJar::new(); - /// - /// // Assume this cookie originally had a path of "/" and domain of "a.b". - /// jar.add_original(Cookie::new("name", "value")); - /// - /// // If the path and domain were set, they must be provided to `remove`. - /// jar.remove(Cookie::build("name", "").path("/").domain("a.b").finish()); - /// - /// // The delta will contain the removal cookie. - /// let delta: Vec<_> = jar.delta().collect(); - /// assert_eq!(delta.len(), 1); - /// assert_eq!(delta[0].name(), "name"); - /// assert_eq!(delta[0].max_age(), Some(Duration::zero())); - /// ``` - /// - /// Removing a new cookie does not result in a _removal_ cookie: - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie}; - /// - /// let mut jar = CookieJar::new(); - /// jar.add(Cookie::new("name", "value")); - /// assert_eq!(jar.delta().count(), 1); - /// - /// jar.remove(Cookie::named("name")); - /// assert_eq!(jar.delta().count(), 0); - /// ``` - pub fn remove(&mut self, mut cookie: Cookie<'static>) { - if self.original_cookies.contains(cookie.name()) { - cookie.set_value(""); - cookie.set_max_age(Duration::zero()); - cookie.set_expires(OffsetDateTime::now_utc() - Duration::days(365)); - self.delta_cookies.replace(DeltaCookie::removed(cookie)); - } else { - self.delta_cookies.remove(cookie.name()); - } - } - - /// Removes `cookie` from this jar completely. This method differs from - /// `remove` in that no delta cookie is created under any condition. Neither - /// the `delta` nor `iter` methods will return a cookie that is removed - /// using this method. - /// - /// # Example - /// - /// Removing an _original_ cookie; no _removal_ cookie is generated: - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie}; - /// use time::Duration; - /// - /// let mut jar = CookieJar::new(); - /// - /// // Add an original cookie and a new cookie. - /// jar.add_original(Cookie::new("name", "value")); - /// jar.add(Cookie::new("key", "value")); - /// assert_eq!(jar.delta().count(), 1); - /// assert_eq!(jar.iter().count(), 2); - /// - /// // Now force remove the original cookie. - /// jar.force_remove(Cookie::new("name", "value")); - /// assert_eq!(jar.delta().count(), 1); - /// assert_eq!(jar.iter().count(), 1); - /// - /// // Now force remove the new cookie. - /// jar.force_remove(Cookie::new("key", "value")); - /// assert_eq!(jar.delta().count(), 0); - /// assert_eq!(jar.iter().count(), 0); - /// ``` - pub fn force_remove<'a>(&mut self, cookie: Cookie<'a>) { - self.original_cookies.remove(cookie.name()); - self.delta_cookies.remove(cookie.name()); - } - - /// Removes all cookies from this cookie jar. - #[deprecated( - since = "0.7.0", - note = "calling this method may not remove \ - all cookies since the path and domain are not specified; use \ - `remove` instead" - )] - pub fn clear(&mut self) { - self.delta_cookies.clear(); - for delta in mem::take(&mut self.original_cookies) { - self.remove(delta.cookie); - } - } - - /// Returns an iterator over cookies that represent the changes to this jar - /// over time. These cookies can be rendered directly as `Set-Cookie` header - /// values to affect the changes made to this jar on the client. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie}; - /// - /// let mut jar = CookieJar::new(); - /// jar.add_original(Cookie::new("name", "value")); - /// jar.add_original(Cookie::new("second", "two")); - /// - /// // Add new cookies. - /// jar.add(Cookie::new("new", "third")); - /// jar.add(Cookie::new("another", "fourth")); - /// jar.add(Cookie::new("yac", "fifth")); - /// - /// // Remove some cookies. - /// jar.remove(Cookie::named("name")); - /// jar.remove(Cookie::named("another")); - /// - /// // Delta contains two new cookies ("new", "yac") and a removal ("name"). - /// assert_eq!(jar.delta().count(), 3); - /// ``` - pub fn delta(&self) -> Delta<'_> { - Delta { - iter: self.delta_cookies.iter(), - } - } - - /// Returns an iterator over all of the cookies present in this jar. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie}; - /// - /// let mut jar = CookieJar::new(); - /// - /// jar.add_original(Cookie::new("name", "value")); - /// jar.add_original(Cookie::new("second", "two")); - /// - /// jar.add(Cookie::new("new", "third")); - /// jar.add(Cookie::new("another", "fourth")); - /// jar.add(Cookie::new("yac", "fifth")); - /// - /// jar.remove(Cookie::named("name")); - /// jar.remove(Cookie::named("another")); - /// - /// // There are three cookies in the jar: "second", "new", and "yac". - /// # assert_eq!(jar.iter().count(), 3); - /// for cookie in jar.iter() { - /// match cookie.name() { - /// "second" => assert_eq!(cookie.value(), "two"), - /// "new" => assert_eq!(cookie.value(), "third"), - /// "yac" => assert_eq!(cookie.value(), "fifth"), - /// _ => unreachable!("there are only three cookies in the jar") - /// } - /// } - /// ``` - pub fn iter(&self) -> Iter<'_> { - Iter { - delta_cookies: self - .delta_cookies - .iter() - .chain(self.original_cookies.difference(&self.delta_cookies)), - } - } - - /// Returns a `PrivateJar` with `self` as its parent jar using the key `key` - /// to sign/encrypt and verify/decrypt cookies added/retrieved from the - /// child jar. - /// - /// Any modifications to the child jar will be reflected on the parent jar, - /// and any retrievals from the child jar will be made from the parent jar. - /// - /// This method is only available when the `secure` feature is enabled. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{Cookie, CookieJar, Key}; - /// - /// // Generate a secure key. - /// let key = Key::generate(); - /// - /// // Add a private (signed + encrypted) cookie. - /// let mut jar = CookieJar::new(); - /// jar.private(&key).add(Cookie::new("private", "text")); - /// - /// // The cookie's contents are encrypted. - /// assert_ne!(jar.get("private").unwrap().value(), "text"); - /// - /// // They can be decrypted and verified through the child jar. - /// assert_eq!(jar.private(&key).get("private").unwrap().value(), "text"); - /// - /// // A tampered with cookie does not validate but still exists. - /// let mut cookie = jar.get("private").unwrap().clone(); - /// jar.add(Cookie::new("private", cookie.value().to_string() + "!")); - /// assert!(jar.private(&key).get("private").is_none()); - /// assert!(jar.get("private").is_some()); - /// ``` - #[cfg(feature = "secure-cookies")] - pub fn private(&mut self, key: &Key) -> PrivateJar<'_> { - PrivateJar::new(self, key) - } - - /// Returns a `SignedJar` with `self` as its parent jar using the key `key` - /// to sign/verify cookies added/retrieved from the child jar. - /// - /// Any modifications to the child jar will be reflected on the parent jar, - /// and any retrievals from the child jar will be made from the parent jar. - /// - /// This method is only available when the `secure` feature is enabled. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{Cookie, CookieJar, Key}; - /// - /// // Generate a secure key. - /// let key = Key::generate(); - /// - /// // Add a signed cookie. - /// let mut jar = CookieJar::new(); - /// jar.signed(&key).add(Cookie::new("signed", "text")); - /// - /// // The cookie's contents are signed but still in plaintext. - /// assert_ne!(jar.get("signed").unwrap().value(), "text"); - /// assert!(jar.get("signed").unwrap().value().contains("text")); - /// - /// // They can be verified through the child jar. - /// assert_eq!(jar.signed(&key).get("signed").unwrap().value(), "text"); - /// - /// // A tampered with cookie does not validate but still exists. - /// let mut cookie = jar.get("signed").unwrap().clone(); - /// jar.add(Cookie::new("signed", cookie.value().to_string() + "!")); - /// assert!(jar.signed(&key).get("signed").is_none()); - /// assert!(jar.get("signed").is_some()); - /// ``` - #[cfg(feature = "secure-cookies")] - pub fn signed(&mut self, key: &Key) -> SignedJar<'_> { - SignedJar::new(self, key) - } -} - -use std::collections::hash_set::Iter as HashSetIter; - -/// Iterator over the changes to a cookie jar. -pub struct Delta<'a> { - iter: HashSetIter<'a, DeltaCookie>, -} - -impl<'a> Iterator for Delta<'a> { - type Item = &'a Cookie<'static>; - - fn next(&mut self) -> Option<&'a Cookie<'static>> { - self.iter.next().map(|c| &c.cookie) - } -} - -use std::collections::hash_map::RandomState; -use std::collections::hash_set::Difference; -use std::iter::Chain; - -/// Iterator over all of the cookies in a jar. -pub struct Iter<'a> { - delta_cookies: - Chain, Difference<'a, DeltaCookie, RandomState>>, -} - -impl<'a> Iterator for Iter<'a> { - type Item = &'a Cookie<'static>; - - fn next(&mut self) -> Option<&'a Cookie<'static>> { - for cookie in self.delta_cookies.by_ref() { - if !cookie.removed { - return Some(&*cookie); - } - } - - None - } -} - -#[cfg(test)] -mod test { - #[cfg(feature = "secure-cookies")] - use super::Key; - use super::{Cookie, CookieJar}; - - #[test] - #[allow(deprecated)] - fn simple() { - let mut c = CookieJar::new(); - - c.add(Cookie::new("test", "")); - c.add(Cookie::new("test2", "")); - c.remove(Cookie::named("test")); - - assert!(c.get("test").is_none()); - assert!(c.get("test2").is_some()); - - c.add(Cookie::new("test3", "")); - c.clear(); - - assert!(c.get("test").is_none()); - assert!(c.get("test2").is_none()); - assert!(c.get("test3").is_none()); - } - - #[test] - fn jar_is_send() { - fn is_send(_: T) -> bool { - true - } - - assert!(is_send(CookieJar::new())) - } - - #[test] - #[cfg(feature = "secure-cookies")] - fn iter() { - let key = Key::generate(); - let mut c = CookieJar::new(); - - c.add_original(Cookie::new("original", "original")); - - c.add(Cookie::new("test", "test")); - c.add(Cookie::new("test2", "test2")); - c.add(Cookie::new("test3", "test3")); - assert_eq!(c.iter().count(), 4); - - c.signed(&key).add(Cookie::new("signed", "signed")); - c.private(&key).add(Cookie::new("encrypted", "encrypted")); - assert_eq!(c.iter().count(), 6); - - c.remove(Cookie::named("test")); - assert_eq!(c.iter().count(), 5); - - c.remove(Cookie::named("signed")); - c.remove(Cookie::named("test2")); - assert_eq!(c.iter().count(), 3); - - c.add(Cookie::new("test2", "test2")); - assert_eq!(c.iter().count(), 4); - - c.remove(Cookie::named("test2")); - assert_eq!(c.iter().count(), 3); - } - - #[test] - #[cfg(feature = "secure-cookies")] - fn delta() { - use std::collections::HashMap; - use time::Duration; - - let mut c = CookieJar::new(); - - c.add_original(Cookie::new("original", "original")); - c.add_original(Cookie::new("original1", "original1")); - - c.add(Cookie::new("test", "test")); - c.add(Cookie::new("test2", "test2")); - c.add(Cookie::new("test3", "test3")); - c.add(Cookie::new("test4", "test4")); - - c.remove(Cookie::named("test")); - c.remove(Cookie::named("original")); - - assert_eq!(c.delta().count(), 4); - - let names: HashMap<_, _> = c.delta().map(|c| (c.name(), c.max_age())).collect(); - - assert!(names.get("test2").unwrap().is_none()); - assert!(names.get("test3").unwrap().is_none()); - assert!(names.get("test4").unwrap().is_none()); - assert_eq!(names.get("original").unwrap(), &Some(Duration::zero())); - } - - #[test] - fn replace_original() { - let mut jar = CookieJar::new(); - jar.add_original(Cookie::new("original_a", "a")); - jar.add_original(Cookie::new("original_b", "b")); - assert_eq!(jar.get("original_a").unwrap().value(), "a"); - - jar.add(Cookie::new("original_a", "av2")); - assert_eq!(jar.get("original_a").unwrap().value(), "av2"); - } - - #[test] - fn empty_delta() { - let mut jar = CookieJar::new(); - jar.add(Cookie::new("name", "val")); - assert_eq!(jar.delta().count(), 1); - - jar.remove(Cookie::named("name")); - assert_eq!(jar.delta().count(), 0); - - jar.add_original(Cookie::new("name", "val")); - assert_eq!(jar.delta().count(), 0); - - jar.remove(Cookie::named("name")); - assert_eq!(jar.delta().count(), 1); - - jar.add(Cookie::new("name", "val")); - assert_eq!(jar.delta().count(), 1); - - jar.remove(Cookie::named("name")); - assert_eq!(jar.delta().count(), 1); - } - - #[test] - fn add_remove_add() { - let mut jar = CookieJar::new(); - jar.add_original(Cookie::new("name", "val")); - assert_eq!(jar.delta().count(), 0); - - jar.remove(Cookie::named("name")); - assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); - assert_eq!(jar.delta().count(), 1); - - // The cookie's been deleted. Another original doesn't change that. - jar.add_original(Cookie::new("name", "val")); - assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); - assert_eq!(jar.delta().count(), 1); - - jar.remove(Cookie::named("name")); - assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); - assert_eq!(jar.delta().count(), 1); - - jar.add(Cookie::new("name", "val")); - assert_eq!(jar.delta().filter(|c| !c.value().is_empty()).count(), 1); - assert_eq!(jar.delta().count(), 1); - - jar.remove(Cookie::named("name")); - assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); - assert_eq!(jar.delta().count(), 1); - } - - #[test] - fn replace_remove() { - let mut jar = CookieJar::new(); - jar.add_original(Cookie::new("name", "val")); - assert_eq!(jar.delta().count(), 0); - - jar.add(Cookie::new("name", "val")); - assert_eq!(jar.delta().count(), 1); - assert_eq!(jar.delta().filter(|c| !c.value().is_empty()).count(), 1); - - jar.remove(Cookie::named("name")); - assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); - } - - #[test] - fn remove_with_path() { - let mut jar = CookieJar::new(); - jar.add_original(Cookie::build("name", "val").finish()); - assert_eq!(jar.iter().count(), 1); - assert_eq!(jar.delta().count(), 0); - assert_eq!(jar.iter().filter(|c| c.path().is_none()).count(), 1); - - jar.remove(Cookie::build("name", "").path("/").finish()); - assert_eq!(jar.iter().count(), 0); - assert_eq!(jar.delta().count(), 1); - assert_eq!(jar.delta().filter(|c| c.value().is_empty()).count(), 1); - assert_eq!(jar.delta().filter(|c| c.path() == Some("/")).count(), 1); - } -} diff --git a/actix-http/src/cookie/mod.rs b/actix-http/src/cookie/mod.rs deleted file mode 100644 index b94e0fe0f..000000000 --- a/actix-http/src/cookie/mod.rs +++ /dev/null @@ -1,1100 +0,0 @@ -//! https://github.com/alexcrichton/cookie-rs fork -//! -//! HTTP cookie parsing and cookie jar management. -//! -//! This crates provides the [`Cookie`](struct.Cookie.html) type, which directly -//! maps to an HTTP cookie, and the [`CookieJar`](struct.CookieJar.html) type, -//! which allows for simple management of many cookies as well as encryption and -//! signing of cookies for session management. -//! -//! # Features -//! -//! This crates can be configured at compile-time through the following Cargo -//! features: -//! -//! -//! * **secure** (disabled by default) -//! -//! Enables signed and private (signed + encrypted) cookie jars. -//! -//! When this feature is enabled, the -//! [signed](struct.CookieJar.html#method.signed) and -//! [private](struct.CookieJar.html#method.private) method of `CookieJar` and -//! [`SignedJar`](struct.SignedJar.html) and -//! [`PrivateJar`](struct.PrivateJar.html) structures are available. The jars -//! act as "children jars", allowing for easy retrieval and addition of signed -//! and/or encrypted cookies to a cookie jar. When this feature is disabled, -//! none of the types are available. -//! -//! * **percent-encode** (disabled by default) -//! -//! Enables percent encoding and decoding of names and values in cookies. -//! -//! When this feature is enabled, the -//! [encoded](struct.Cookie.html#method.encoded) and -//! [`parse_encoded`](struct.Cookie.html#method.parse_encoded) methods of -//! `Cookie` become available. The `encoded` method returns a wrapper around a -//! `Cookie` whose `Display` implementation percent-encodes the name and value -//! of the cookie. The `parse_encoded` method percent-decodes the name and -//! value of a `Cookie` during parsing. When this feature is disabled, the -//! `encoded` and `parse_encoded` methods are not available. -//! -//! You can enable features via the `Cargo.toml` file: -//! -//! ```ignore -//! [dependencies.cookie] -//! features = ["secure", "percent-encode"] -//! ``` - -#![doc(html_root_url = "https://docs.rs/cookie/0.11")] -#![warn(missing_docs)] - -mod builder; -mod delta; -mod draft; -mod jar; -mod parse; - -#[cfg(feature = "secure-cookies")] -#[macro_use] -mod secure; -#[cfg(feature = "secure-cookies")] -pub use self::secure::*; - -use std::borrow::Cow; -use std::fmt; -use std::str::FromStr; - -use percent_encoding::{percent_encode, AsciiSet, CONTROLS}; -use time::{Duration, OffsetDateTime}; - -pub use self::builder::CookieBuilder; -pub use self::draft::*; -pub use self::jar::{CookieJar, Delta, Iter}; -use self::parse::parse_cookie; -pub use self::parse::ParseError; - -/// https://url.spec.whatwg.org/#fragment-percent-encode-set -const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); - -/// https://url.spec.whatwg.org/#path-percent-encode-set -const PATH: &AsciiSet = &FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}'); - -/// https://url.spec.whatwg.org/#userinfo-percent-encode-set -pub const USERINFO: &AsciiSet = &PATH - .add(b'/') - .add(b':') - .add(b';') - .add(b'=') - .add(b'@') - .add(b'[') - .add(b'\\') - .add(b']') - .add(b'^') - .add(b'|'); - -#[derive(Debug, Clone)] -enum CookieStr { - /// An string derived from indexes (start, end). - Indexed(usize, usize), - /// A string derived from a concrete string. - Concrete(Cow<'static, str>), -} - -impl CookieStr { - /// Retrieves the string `self` corresponds to. If `self` is derived from - /// indexes, the corresponding sub-slice of `string` is returned. Otherwise, - /// the concrete string is returned. - /// - /// # Panics - /// - /// Panics if `self` is an indexed string and `string` is None. - fn to_str<'s>(&'s self, string: Option<&'s Cow<'_, str>>) -> &'s str { - match *self { - CookieStr::Indexed(i, j) => { - let s = string.expect( - "`Some` base string must exist when \ - converting indexed str to str! (This is a module invariant.)", - ); - &s[i..j] - } - CookieStr::Concrete(ref cstr) => &*cstr, - } - } - - #[allow(clippy::ptr_arg)] - fn to_raw_str<'s, 'c: 's>(&'s self, string: &'s Cow<'c, str>) -> Option<&'c str> { - match *self { - CookieStr::Indexed(i, j) => match *string { - Cow::Borrowed(s) => Some(&s[i..j]), - Cow::Owned(_) => None, - }, - CookieStr::Concrete(_) => None, - } - } -} - -/// Representation of an HTTP cookie. -/// -/// # Constructing a `Cookie` -/// -/// To construct a cookie with only a name/value, use the [new](#method.new) -/// method: -/// -/// ```rust -/// use actix_http::cookie::Cookie; -/// -/// let cookie = Cookie::new("name", "value"); -/// assert_eq!(&cookie.to_string(), "name=value"); -/// ``` -/// -/// To construct more elaborate cookies, use the [build](#method.build) method -/// and [`CookieBuilder`](struct.CookieBuilder.html) methods: -/// -/// ```rust -/// use actix_http::cookie::Cookie; -/// -/// let cookie = Cookie::build("name", "value") -/// .domain("www.rust-lang.org") -/// .path("/") -/// .secure(true) -/// .http_only(true) -/// .finish(); -/// ``` -#[derive(Debug, Clone)] -pub struct Cookie<'c> { - /// Storage for the cookie string. Only used if this structure was derived - /// from a string that was subsequently parsed. - cookie_string: Option>, - /// The cookie's name. - name: CookieStr, - /// The cookie's value. - value: CookieStr, - /// The cookie's expiration, if any. - expires: Option, - /// The cookie's maximum age, if any. - max_age: Option, - /// The cookie's domain, if any. - domain: Option, - /// The cookie's path domain, if any. - path: Option, - /// Whether this cookie was marked Secure. - secure: Option, - /// Whether this cookie was marked HttpOnly. - http_only: Option, - /// The draft `SameSite` attribute. - same_site: Option, -} - -impl Cookie<'static> { - /// Creates a new `Cookie` with the given name and value. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let cookie = Cookie::new("name", "value"); - /// assert_eq!(cookie.name_value(), ("name", "value")); - /// ``` - pub fn new(name: N, value: V) -> Cookie<'static> - where - N: Into>, - V: Into>, - { - Cookie { - cookie_string: None, - name: CookieStr::Concrete(name.into()), - value: CookieStr::Concrete(value.into()), - expires: None, - max_age: None, - domain: None, - path: None, - secure: None, - http_only: None, - same_site: None, - } - } - - /// Creates a new `Cookie` with the given name and an empty value. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let cookie = Cookie::named("name"); - /// assert_eq!(cookie.name(), "name"); - /// assert!(cookie.value().is_empty()); - /// ``` - pub fn named(name: N) -> Cookie<'static> - where - N: Into>, - { - Cookie::new(name, "") - } - - /// Creates a new `CookieBuilder` instance from the given key and value - /// strings. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::build("foo", "bar").finish(); - /// assert_eq!(c.name_value(), ("foo", "bar")); - /// ``` - pub fn build(name: N, value: V) -> CookieBuilder - where - N: Into>, - V: Into>, - { - CookieBuilder::new(name, value) - } -} - -impl<'c> Cookie<'c> { - /// Parses a `Cookie` from the given HTTP cookie header value string. Does - /// not perform any percent-decoding. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::parse("foo=bar%20baz; HttpOnly").unwrap(); - /// assert_eq!(c.name_value(), ("foo", "bar%20baz")); - /// assert_eq!(c.http_only(), Some(true)); - /// ``` - pub fn parse(s: S) -> Result, ParseError> - where - S: Into>, - { - parse_cookie(s, false) - } - - /// Parses a `Cookie` from the given HTTP cookie header value string where - /// the name and value fields are percent-encoded. Percent-decodes the - /// name/value fields. - /// - /// This API requires the `percent-encode` feature to be enabled on this - /// crate. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::parse_encoded("foo=bar%20baz; HttpOnly").unwrap(); - /// assert_eq!(c.name_value(), ("foo", "bar baz")); - /// assert_eq!(c.http_only(), Some(true)); - /// ``` - pub fn parse_encoded(s: S) -> Result, ParseError> - where - S: Into>, - { - parse_cookie(s, true) - } - - /// Wraps `self` in an `EncodedCookie`: a cost-free wrapper around `Cookie` - /// whose `Display` implementation percent-encodes the name and value of the - /// wrapped `Cookie`. - /// - /// This method is only available when the `percent-encode` feature is - /// enabled. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let mut c = Cookie::new("my name", "this; value?"); - /// assert_eq!(&c.encoded().to_string(), "my%20name=this%3B%20value%3F"); - /// ``` - pub fn encoded<'a>(&'a self) -> EncodedCookie<'a, 'c> { - EncodedCookie(self) - } - - /// Converts `self` into a `Cookie` with a static lifetime. This method - /// results in at most one allocation. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::new("a", "b"); - /// let owned_cookie = c.into_owned(); - /// assert_eq!(owned_cookie.name_value(), ("a", "b")); - /// ``` - pub fn into_owned(self) -> Cookie<'static> { - Cookie { - cookie_string: self.cookie_string.map(|s| s.into_owned().into()), - name: self.name, - value: self.value, - expires: self.expires, - max_age: self.max_age, - domain: self.domain, - path: self.path, - secure: self.secure, - http_only: self.http_only, - same_site: self.same_site, - } - } - - /// Returns the name of `self`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::new("name", "value"); - /// assert_eq!(c.name(), "name"); - /// ``` - #[inline] - pub fn name(&self) -> &str { - self.name.to_str(self.cookie_string.as_ref()) - } - - /// Returns the value of `self`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::new("name", "value"); - /// assert_eq!(c.value(), "value"); - /// ``` - #[inline] - pub fn value(&self) -> &str { - self.value.to_str(self.cookie_string.as_ref()) - } - - /// Returns the name and value of `self` as a tuple of `(name, value)`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::new("name", "value"); - /// assert_eq!(c.name_value(), ("name", "value")); - /// ``` - #[inline] - pub fn name_value(&self) -> (&str, &str) { - (self.name(), self.value()) - } - - /// Returns whether this cookie was marked `HttpOnly` or not. Returns - /// `Some(true)` when the cookie was explicitly set (manually or parsed) as - /// `HttpOnly`, `Some(false)` when `http_only` was manually set to `false`, - /// and `None` otherwise. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::parse("name=value; httponly").unwrap(); - /// assert_eq!(c.http_only(), Some(true)); - /// - /// let mut c = Cookie::new("name", "value"); - /// assert_eq!(c.http_only(), None); - /// - /// let mut c = Cookie::new("name", "value"); - /// assert_eq!(c.http_only(), None); - /// - /// // An explicitly set "false" value. - /// c.set_http_only(false); - /// assert_eq!(c.http_only(), Some(false)); - /// - /// // An explicitly set "true" value. - /// c.set_http_only(true); - /// assert_eq!(c.http_only(), Some(true)); - /// ``` - #[inline] - pub fn http_only(&self) -> Option { - self.http_only - } - - /// Returns whether this cookie was marked `Secure` or not. Returns - /// `Some(true)` when the cookie was explicitly set (manually or parsed) as - /// `Secure`, `Some(false)` when `secure` was manually set to `false`, and - /// `None` otherwise. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::parse("name=value; Secure").unwrap(); - /// assert_eq!(c.secure(), Some(true)); - /// - /// let mut c = Cookie::parse("name=value").unwrap(); - /// assert_eq!(c.secure(), None); - /// - /// let mut c = Cookie::new("name", "value"); - /// assert_eq!(c.secure(), None); - /// - /// // An explicitly set "false" value. - /// c.set_secure(false); - /// assert_eq!(c.secure(), Some(false)); - /// - /// // An explicitly set "true" value. - /// c.set_secure(true); - /// assert_eq!(c.secure(), Some(true)); - /// ``` - #[inline] - pub fn secure(&self) -> Option { - self.secure - } - - /// Returns the `SameSite` attribute of this cookie if one was specified. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{Cookie, SameSite}; - /// - /// let c = Cookie::parse("name=value; SameSite=Lax").unwrap(); - /// assert_eq!(c.same_site(), Some(SameSite::Lax)); - /// ``` - #[inline] - pub fn same_site(&self) -> Option { - self.same_site - } - - /// Returns the specified max-age of the cookie if one was specified. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::parse("name=value").unwrap(); - /// assert_eq!(c.max_age(), None); - /// - /// let c = Cookie::parse("name=value; Max-Age=3600").unwrap(); - /// assert_eq!(c.max_age().map(|age| age.whole_hours()), Some(1)); - /// ``` - #[inline] - pub fn max_age(&self) -> Option { - self.max_age - } - - /// Returns the `Path` of the cookie if one was specified. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::parse("name=value").unwrap(); - /// assert_eq!(c.path(), None); - /// - /// let c = Cookie::parse("name=value; Path=/").unwrap(); - /// assert_eq!(c.path(), Some("/")); - /// - /// let c = Cookie::parse("name=value; path=/sub").unwrap(); - /// assert_eq!(c.path(), Some("/sub")); - /// ``` - #[inline] - pub fn path(&self) -> Option<&str> { - match self.path { - Some(ref c) => Some(c.to_str(self.cookie_string.as_ref())), - None => None, - } - } - - /// Returns the `Domain` of the cookie if one was specified. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::parse("name=value").unwrap(); - /// assert_eq!(c.domain(), None); - /// - /// let c = Cookie::parse("name=value; Domain=crates.io").unwrap(); - /// assert_eq!(c.domain(), Some("crates.io")); - /// ``` - #[inline] - pub fn domain(&self) -> Option<&str> { - match self.domain { - Some(ref c) => Some(c.to_str(self.cookie_string.as_ref())), - None => None, - } - } - - /// Returns the `Expires` time of the cookie if one was specified. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let c = Cookie::parse("name=value").unwrap(); - /// assert_eq!(c.expires(), None); - /// - /// let expire_time = "Wed, 21 Oct 2017 07:28:00 GMT"; - /// let cookie_str = format!("name=value; Expires={}", expire_time); - /// let c = Cookie::parse(cookie_str).unwrap(); - /// assert_eq!(c.expires().map(|t| t.year()), Some(2017)); - /// ``` - #[inline] - pub fn expires(&self) -> Option { - self.expires - } - - /// Sets the name of `self` to `name`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let mut c = Cookie::new("name", "value"); - /// assert_eq!(c.name(), "name"); - /// - /// c.set_name("foo"); - /// assert_eq!(c.name(), "foo"); - /// ``` - pub fn set_name>>(&mut self, name: N) { - self.name = CookieStr::Concrete(name.into()) - } - - /// Sets the value of `self` to `value`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let mut c = Cookie::new("name", "value"); - /// assert_eq!(c.value(), "value"); - /// - /// c.set_value("bar"); - /// assert_eq!(c.value(), "bar"); - /// ``` - pub fn set_value>>(&mut self, value: V) { - self.value = CookieStr::Concrete(value.into()) - } - - /// Sets the value of `http_only` in `self` to `value`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let mut c = Cookie::new("name", "value"); - /// assert_eq!(c.http_only(), None); - /// - /// c.set_http_only(true); - /// assert_eq!(c.http_only(), Some(true)); - /// ``` - #[inline] - pub fn set_http_only(&mut self, value: bool) { - self.http_only = Some(value); - } - - /// Sets the value of `secure` in `self` to `value`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let mut c = Cookie::new("name", "value"); - /// assert_eq!(c.secure(), None); - /// - /// c.set_secure(true); - /// assert_eq!(c.secure(), Some(true)); - /// ``` - #[inline] - pub fn set_secure(&mut self, value: bool) { - self.secure = Some(value); - } - - /// Sets the value of `same_site` in `self` to `value`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{Cookie, SameSite}; - /// - /// let mut c = Cookie::new("name", "value"); - /// assert!(c.same_site().is_none()); - /// - /// c.set_same_site(SameSite::Strict); - /// assert_eq!(c.same_site(), Some(SameSite::Strict)); - /// ``` - #[inline] - pub fn set_same_site(&mut self, value: SameSite) { - self.same_site = Some(value); - } - - /// Sets the value of `max_age` in `self` to `value`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// use time::Duration; - /// - /// let mut c = Cookie::new("name", "value"); - /// assert_eq!(c.max_age(), None); - /// - /// c.set_max_age(Duration::hours(10)); - /// assert_eq!(c.max_age(), Some(Duration::hours(10))); - /// ``` - #[inline] - pub fn set_max_age(&mut self, value: Duration) { - self.max_age = Some(value); - } - - /// Sets the `path` of `self` to `path`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let mut c = Cookie::new("name", "value"); - /// assert_eq!(c.path(), None); - /// - /// c.set_path("/"); - /// assert_eq!(c.path(), Some("/")); - /// ``` - pub fn set_path>>(&mut self, path: P) { - self.path = Some(CookieStr::Concrete(path.into())); - } - - /// Sets the `domain` of `self` to `domain`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let mut c = Cookie::new("name", "value"); - /// assert_eq!(c.domain(), None); - /// - /// c.set_domain("rust-lang.org"); - /// assert_eq!(c.domain(), Some("rust-lang.org")); - /// ``` - pub fn set_domain>>(&mut self, domain: D) { - self.domain = Some(CookieStr::Concrete(domain.into())); - } - - /// Sets the expires field of `self` to `time`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// use time::{Duration, OffsetDateTime}; - /// - /// let mut c = Cookie::new("name", "value"); - /// assert_eq!(c.expires(), None); - /// - /// let mut now = OffsetDateTime::now(); - /// now += Duration::week(); - /// - /// c.set_expires(now); - /// assert!(c.expires().is_some()) - /// ``` - #[inline] - pub fn set_expires(&mut self, time: OffsetDateTime) { - self.expires = Some(time); - } - - /// Makes `self` a "permanent" cookie by extending its expiration and max - /// age 20 years into the future. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// use time::Duration; - /// - /// let mut c = Cookie::new("foo", "bar"); - /// assert!(c.expires().is_none()); - /// assert!(c.max_age().is_none()); - /// - /// c.make_permanent(); - /// assert!(c.expires().is_some()); - /// assert_eq!(c.max_age(), Some(Duration::days(365 * 20))); - /// ``` - pub fn make_permanent(&mut self) { - let twenty_years = Duration::days(365 * 20); - self.set_max_age(twenty_years); - self.set_expires(OffsetDateTime::now_utc() + twenty_years); - } - - fn fmt_parameters(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(true) = self.http_only() { - write!(f, "; HttpOnly")?; - } - - if let Some(true) = self.secure() { - write!(f, "; Secure")?; - } - - if let Some(same_site) = self.same_site() { - write!(f, "; SameSite={}", same_site)?; - } - - if let Some(path) = self.path() { - write!(f, "; Path={}", path)?; - } - - if let Some(domain) = self.domain() { - write!(f, "; Domain={}", domain)?; - } - - if let Some(max_age) = self.max_age() { - write!(f, "; Max-Age={}", max_age.whole_seconds())?; - } - - if let Some(time) = self.expires() { - write!(f, "; Expires={}", time.format("%a, %d %b %Y %H:%M:%S GMT"))?; - } - - Ok(()) - } - - /// Returns the name of `self` as a string slice of the raw string `self` - /// was originally parsed from. If `self` was not originally parsed from a - /// raw string, returns `None`. - /// - /// This method differs from [name](#method.name) in that it returns a - /// string with the same lifetime as the originally parsed string. This - /// lifetime may outlive `self`. If a longer lifetime is not required, or - /// you're unsure if you need a longer lifetime, use [name](#method.name). - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let cookie_string = format!("{}={}", "foo", "bar"); - /// - /// // `c` will be dropped at the end of the scope, but `name` will live on - /// let name = { - /// let c = Cookie::parse(cookie_string.as_str()).unwrap(); - /// c.name_raw() - /// }; - /// - /// assert_eq!(name, Some("foo")); - /// ``` - #[inline] - pub fn name_raw(&self) -> Option<&'c str> { - self.cookie_string - .as_ref() - .and_then(|s| self.name.to_raw_str(s)) - } - - /// Returns the value of `self` as a string slice of the raw string `self` - /// was originally parsed from. If `self` was not originally parsed from a - /// raw string, returns `None`. - /// - /// This method differs from [value](#method.value) in that it returns a - /// string with the same lifetime as the originally parsed string. This - /// lifetime may outlive `self`. If a longer lifetime is not required, or - /// you're unsure if you need a longer lifetime, use [value](#method.value). - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let cookie_string = format!("{}={}", "foo", "bar"); - /// - /// // `c` will be dropped at the end of the scope, but `value` will live on - /// let value = { - /// let c = Cookie::parse(cookie_string.as_str()).unwrap(); - /// c.value_raw() - /// }; - /// - /// assert_eq!(value, Some("bar")); - /// ``` - #[inline] - pub fn value_raw(&self) -> Option<&'c str> { - self.cookie_string - .as_ref() - .and_then(|s| self.value.to_raw_str(s)) - } - - /// Returns the `Path` of `self` as a string slice of the raw string `self` - /// was originally parsed from. If `self` was not originally parsed from a - /// raw string, or if `self` doesn't contain a `Path`, or if the `Path` has - /// changed since parsing, returns `None`. - /// - /// This method differs from [path](#method.path) in that it returns a - /// string with the same lifetime as the originally parsed string. This - /// lifetime may outlive `self`. If a longer lifetime is not required, or - /// you're unsure if you need a longer lifetime, use [path](#method.path). - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let cookie_string = format!("{}={}; Path=/", "foo", "bar"); - /// - /// // `c` will be dropped at the end of the scope, but `path` will live on - /// let path = { - /// let c = Cookie::parse(cookie_string.as_str()).unwrap(); - /// c.path_raw() - /// }; - /// - /// assert_eq!(path, Some("/")); - /// ``` - #[inline] - pub fn path_raw(&self) -> Option<&'c str> { - match (self.path.as_ref(), self.cookie_string.as_ref()) { - (Some(path), Some(string)) => path.to_raw_str(string), - _ => None, - } - } - - /// Returns the `Domain` of `self` as a string slice of the raw string - /// `self` was originally parsed from. If `self` was not originally parsed - /// from a raw string, or if `self` doesn't contain a `Domain`, or if the - /// `Domain` has changed since parsing, returns `None`. - /// - /// This method differs from [domain](#method.domain) in that it returns a - /// string with the same lifetime as the originally parsed string. This - /// lifetime may outlive `self` struct. If a longer lifetime is not - /// required, or you're unsure if you need a longer lifetime, use - /// [domain](#method.domain). - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let cookie_string = format!("{}={}; Domain=crates.io", "foo", "bar"); - /// - /// //`c` will be dropped at the end of the scope, but `domain` will live on - /// let domain = { - /// let c = Cookie::parse(cookie_string.as_str()).unwrap(); - /// c.domain_raw() - /// }; - /// - /// assert_eq!(domain, Some("crates.io")); - /// ``` - #[inline] - pub fn domain_raw(&self) -> Option<&'c str> { - match (self.domain.as_ref(), self.cookie_string.as_ref()) { - (Some(domain), Some(string)) => domain.to_raw_str(string), - _ => None, - } - } -} - -/// Wrapper around `Cookie` whose `Display` implementation percent-encodes the -/// cookie's name and value. -/// -/// A value of this type can be obtained via the -/// [encoded](struct.Cookie.html#method.encoded) method on -/// [Cookie](struct.Cookie.html). This type should only be used for its -/// `Display` implementation. -/// -/// This type is only available when the `percent-encode` feature is enabled. -/// -/// # Example -/// -/// ```rust -/// use actix_http::cookie::Cookie; -/// -/// let mut c = Cookie::new("my name", "this; value?"); -/// assert_eq!(&c.encoded().to_string(), "my%20name=this%3B%20value%3F"); -/// ``` -pub struct EncodedCookie<'a, 'c>(&'a Cookie<'c>); - -impl<'a, 'c: 'a> fmt::Display for EncodedCookie<'a, 'c> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Percent-encode the name and value. - let name = percent_encode(self.0.name().as_bytes(), USERINFO); - let value = percent_encode(self.0.value().as_bytes(), USERINFO); - - // Write out the name/value pair and the cookie's parameters. - write!(f, "{}={}", name, value)?; - self.0.fmt_parameters(f) - } -} - -impl<'c> fmt::Display for Cookie<'c> { - /// Formats the cookie `self` as a `Set-Cookie` header value. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Cookie; - /// - /// let mut cookie = Cookie::build("foo", "bar") - /// .path("/") - /// .finish(); - /// - /// assert_eq!(&cookie.to_string(), "foo=bar; Path=/"); - /// ``` - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}={}", self.name(), self.value())?; - self.fmt_parameters(f) - } -} - -impl FromStr for Cookie<'static> { - type Err = ParseError; - - fn from_str(s: &str) -> Result, ParseError> { - Cookie::parse(s).map(|c| c.into_owned()) - } -} - -impl<'a, 'b> PartialEq> for Cookie<'a> { - fn eq(&self, other: &Cookie<'b>) -> bool { - let so_far_so_good = self.name() == other.name() - && self.value() == other.value() - && self.http_only() == other.http_only() - && self.secure() == other.secure() - && self.max_age() == other.max_age() - && self.expires() == other.expires(); - - if !so_far_so_good { - return false; - } - - match (self.path(), other.path()) { - (Some(a), Some(b)) if a.eq_ignore_ascii_case(b) => {} - (None, None) => {} - _ => return false, - }; - - match (self.domain(), other.domain()) { - (Some(a), Some(b)) if a.eq_ignore_ascii_case(b) => {} - (None, None) => {} - _ => return false, - }; - - true - } -} - -#[cfg(test)] -mod tests { - use super::{Cookie, SameSite}; - use time::PrimitiveDateTime; - - #[test] - fn format() { - let cookie = Cookie::new("foo", "bar"); - assert_eq!(&cookie.to_string(), "foo=bar"); - - let cookie = Cookie::build("foo", "bar").http_only(true).finish(); - assert_eq!(&cookie.to_string(), "foo=bar; HttpOnly"); - - let cookie = Cookie::build("foo", "bar").max_age(10).finish(); - assert_eq!(&cookie.to_string(), "foo=bar; Max-Age=10"); - - let cookie = Cookie::build("foo", "bar").secure(true).finish(); - assert_eq!(&cookie.to_string(), "foo=bar; Secure"); - - let cookie = Cookie::build("foo", "bar").path("/").finish(); - assert_eq!(&cookie.to_string(), "foo=bar; Path=/"); - - let cookie = Cookie::build("foo", "bar") - .domain("www.rust-lang.org") - .finish(); - assert_eq!(&cookie.to_string(), "foo=bar; Domain=www.rust-lang.org"); - - let time_str = "Wed, 21 Oct 2015 07:28:00 GMT"; - let expires = PrimitiveDateTime::parse(time_str, "%a, %d %b %Y %H:%M:%S") - .unwrap() - .assume_utc(); - let cookie = Cookie::build("foo", "bar").expires(expires).finish(); - assert_eq!( - &cookie.to_string(), - "foo=bar; Expires=Wed, 21 Oct 2015 07:28:00 GMT" - ); - - let cookie = Cookie::build("foo", "bar") - .same_site(SameSite::Strict) - .finish(); - assert_eq!(&cookie.to_string(), "foo=bar; SameSite=Strict"); - - let cookie = Cookie::build("foo", "bar") - .same_site(SameSite::Lax) - .finish(); - assert_eq!(&cookie.to_string(), "foo=bar; SameSite=Lax"); - - let cookie = Cookie::build("foo", "bar") - .same_site(SameSite::None) - .finish(); - assert_eq!(&cookie.to_string(), "foo=bar; SameSite=None"); - } - - #[test] - fn cookie_string_long_lifetimes() { - let cookie_string = - "bar=baz; Path=/subdir; HttpOnly; Domain=crates.io".to_owned(); - let (name, value, path, domain) = { - // Create a cookie passing a slice - let c = Cookie::parse(cookie_string.as_str()).unwrap(); - (c.name_raw(), c.value_raw(), c.path_raw(), c.domain_raw()) - }; - - assert_eq!(name, Some("bar")); - assert_eq!(value, Some("baz")); - assert_eq!(path, Some("/subdir")); - assert_eq!(domain, Some("crates.io")); - } - - #[test] - fn owned_cookie_string() { - let cookie_string = - "bar=baz; Path=/subdir; HttpOnly; Domain=crates.io".to_owned(); - let (name, value, path, domain) = { - // Create a cookie passing an owned string - let c = Cookie::parse(cookie_string).unwrap(); - (c.name_raw(), c.value_raw(), c.path_raw(), c.domain_raw()) - }; - - assert_eq!(name, None); - assert_eq!(value, None); - assert_eq!(path, None); - assert_eq!(domain, None); - } - - #[test] - fn owned_cookie_struct() { - let cookie_string = "bar=baz; Path=/subdir; HttpOnly; Domain=crates.io"; - let (name, value, path, domain) = { - // Create an owned cookie - let c = Cookie::parse(cookie_string).unwrap().into_owned(); - - (c.name_raw(), c.value_raw(), c.path_raw(), c.domain_raw()) - }; - - assert_eq!(name, None); - assert_eq!(value, None); - assert_eq!(path, None); - assert_eq!(domain, None); - } - - #[test] - fn format_encoded() { - let cookie = Cookie::build("foo !?=", "bar;; a").finish(); - let cookie_str = cookie.encoded().to_string(); - assert_eq!(&cookie_str, "foo%20!%3F%3D=bar%3B%3B%20a"); - - let cookie = Cookie::parse_encoded(cookie_str).unwrap(); - assert_eq!(cookie.name_value(), ("foo !?=", "bar;; a")); - } -} diff --git a/actix-http/src/cookie/parse.rs b/actix-http/src/cookie/parse.rs deleted file mode 100644 index d472b32b6..000000000 --- a/actix-http/src/cookie/parse.rs +++ /dev/null @@ -1,467 +0,0 @@ -use std::borrow::Cow; -use std::cmp; -use std::convert::From; -use std::error::Error; -use std::fmt; -use std::str::Utf8Error; - -use percent_encoding::percent_decode; -use time::Duration; - -use super::{Cookie, CookieStr, SameSite}; - -use crate::time_parser; - -/// Enum corresponding to a parsing error. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum ParseError { - /// The cookie did not contain a name/value pair. - MissingPair, - /// The cookie's name was empty. - EmptyName, - /// Decoding the cookie's name or value resulted in invalid UTF-8. - Utf8Error(Utf8Error), - /// It is discouraged to exhaustively match on this enum as its variants may - /// grow without a breaking-change bump in version numbers. - #[doc(hidden)] - __Nonexhasutive, -} - -impl ParseError { - /// Returns a description of this error as a string - pub fn as_str(&self) -> &'static str { - match *self { - ParseError::MissingPair => "the cookie is missing a name/value pair", - ParseError::EmptyName => "the cookie's name is empty", - ParseError::Utf8Error(_) => { - "decoding the cookie's name or value resulted in invalid UTF-8" - } - ParseError::__Nonexhasutive => unreachable!("__Nonexhasutive ParseError"), - } - } -} - -impl fmt::Display for ParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.as_str()) - } -} - -impl From for ParseError { - fn from(error: Utf8Error) -> ParseError { - ParseError::Utf8Error(error) - } -} - -impl Error for ParseError {} - -fn indexes_of(needle: &str, haystack: &str) -> Option<(usize, usize)> { - let haystack_start = haystack.as_ptr() as usize; - let needle_start = needle.as_ptr() as usize; - - if needle_start < haystack_start { - return None; - } - - if (needle_start + needle.len()) > (haystack_start + haystack.len()) { - return None; - } - - let start = needle_start - haystack_start; - let end = start + needle.len(); - Some((start, end)) -} - -fn name_val_decoded( - name: &str, - val: &str, -) -> Result<(CookieStr, CookieStr), ParseError> { - let decoded_name = percent_decode(name.as_bytes()).decode_utf8()?; - let decoded_value = percent_decode(val.as_bytes()).decode_utf8()?; - let name = CookieStr::Concrete(Cow::Owned(decoded_name.into_owned())); - let val = CookieStr::Concrete(Cow::Owned(decoded_value.into_owned())); - - Ok((name, val)) -} - -// This function does the real parsing but _does not_ set the `cookie_string` in -// the returned cookie object. This only exists so that the borrow to `s` is -// returned at the end of the call, allowing the `cookie_string` field to be -// set in the outer `parse` function. -fn parse_inner<'c>(s: &str, decode: bool) -> Result, ParseError> { - let mut attributes = s.split(';'); - let key_value = match attributes.next() { - Some(s) => s, - _ => panic!(), - }; - - // Determine the name = val. - let (name, value) = match key_value.find('=') { - Some(i) => (key_value[..i].trim(), key_value[(i + 1)..].trim()), - None => return Err(ParseError::MissingPair), - }; - - if name.is_empty() { - return Err(ParseError::EmptyName); - } - - // Create a cookie with all of the defaults. We'll fill things in while we - // iterate through the parameters below. - let (name, value) = if decode { - name_val_decoded(name, value)? - } else { - let name_indexes = indexes_of(name, s).expect("name sub"); - let value_indexes = indexes_of(value, s).expect("value sub"); - let name = CookieStr::Indexed(name_indexes.0, name_indexes.1); - let value = CookieStr::Indexed(value_indexes.0, value_indexes.1); - - (name, value) - }; - - let mut cookie = Cookie { - name, - value, - cookie_string: None, - expires: None, - max_age: None, - domain: None, - path: None, - secure: None, - http_only: None, - same_site: None, - }; - - for attr in attributes { - let (key, value) = match attr.find('=') { - Some(i) => (attr[..i].trim(), Some(attr[(i + 1)..].trim())), - None => (attr.trim(), None), - }; - - match (&*key.to_ascii_lowercase(), value) { - ("secure", _) => cookie.secure = Some(true), - ("httponly", _) => cookie.http_only = Some(true), - ("max-age", Some(v)) => { - // See RFC 6265 Section 5.2.2, negative values indicate that the - // earliest possible expiration time should be used, so set the - // max age as 0 seconds. - cookie.max_age = match v.parse() { - Ok(val) if val <= 0 => Some(Duration::zero()), - Ok(val) => { - // Don't panic if the max age seconds is greater than what's supported by - // `Duration`. - let val = cmp::min(val, Duration::max_value().whole_seconds()); - Some(Duration::seconds(val)) - } - Err(_) => continue, - }; - } - ("domain", Some(mut domain)) if !domain.is_empty() => { - if domain.starts_with('.') { - domain = &domain[1..]; - } - - let (i, j) = indexes_of(domain, s).expect("domain sub"); - cookie.domain = Some(CookieStr::Indexed(i, j)); - } - ("path", Some(v)) => { - let (i, j) = indexes_of(v, s).expect("path sub"); - cookie.path = Some(CookieStr::Indexed(i, j)); - } - ("samesite", Some(v)) => { - if v.eq_ignore_ascii_case("strict") { - cookie.same_site = Some(SameSite::Strict); - } else if v.eq_ignore_ascii_case("lax") { - cookie.same_site = Some(SameSite::Lax); - } else if v.eq_ignore_ascii_case("none") { - cookie.same_site = Some(SameSite::None); - } else { - // We do nothing here, for now. When/if the `SameSite` - // attribute becomes standard, the spec says that we should - // ignore this cookie, i.e, fail to parse it, when an - // invalid value is passed in. The draft is at - // http://httpwg.org/http-extensions/draft-ietf-httpbis-cookie-same-site.html. - } - } - ("expires", Some(v)) => { - // Try parsing with three date formats according to - // http://tools.ietf.org/html/rfc2616#section-3.3.1. Try - // additional ones as encountered in the real world. - let tm = time_parser::parse_http_date(v) - .or_else(|| time::parse(v, "%a, %d-%b-%Y %H:%M:%S").ok()); - - if let Some(time) = tm { - cookie.expires = Some(time.assume_utc()) - } - } - _ => { - // We're going to be permissive here. If we have no idea what - // this is, then it's something nonstandard. We're not going to - // store it (because it's not compliant), but we're also not - // going to emit an error. - } - } - } - - Ok(cookie) -} - -pub fn parse_cookie<'c, S>(cow: S, decode: bool) -> Result, ParseError> -where - S: Into>, -{ - let s = cow.into(); - let mut cookie = parse_inner(&s, decode)?; - cookie.cookie_string = Some(s); - Ok(cookie) -} - -#[cfg(test)] -mod tests { - use super::{Cookie, SameSite}; - use time::{Duration, PrimitiveDateTime}; - - macro_rules! assert_eq_parse { - ($string:expr, $expected:expr) => { - let cookie = match Cookie::parse($string) { - Ok(cookie) => cookie, - Err(e) => panic!("Failed to parse {:?}: {:?}", $string, e), - }; - - assert_eq!(cookie, $expected); - }; - } - - macro_rules! assert_ne_parse { - ($string:expr, $expected:expr) => { - let cookie = match Cookie::parse($string) { - Ok(cookie) => cookie, - Err(e) => panic!("Failed to parse {:?}: {:?}", $string, e), - }; - - assert_ne!(cookie, $expected); - }; - } - - #[test] - fn parse_same_site() { - let expected = Cookie::build("foo", "bar") - .same_site(SameSite::Lax) - .finish(); - - assert_eq_parse!("foo=bar; SameSite=Lax", expected); - assert_eq_parse!("foo=bar; SameSite=lax", expected); - assert_eq_parse!("foo=bar; SameSite=LAX", expected); - assert_eq_parse!("foo=bar; samesite=Lax", expected); - assert_eq_parse!("foo=bar; SAMESITE=Lax", expected); - - let expected = Cookie::build("foo", "bar") - .same_site(SameSite::Strict) - .finish(); - - assert_eq_parse!("foo=bar; SameSite=Strict", expected); - assert_eq_parse!("foo=bar; SameSITE=Strict", expected); - assert_eq_parse!("foo=bar; SameSite=strict", expected); - assert_eq_parse!("foo=bar; SameSite=STrICT", expected); - assert_eq_parse!("foo=bar; SameSite=STRICT", expected); - - let expected = Cookie::build("foo", "bar") - .same_site(SameSite::None) - .finish(); - - assert_eq_parse!("foo=bar; SameSite=None", expected); - assert_eq_parse!("foo=bar; SameSITE=None", expected); - assert_eq_parse!("foo=bar; SameSite=nOne", expected); - assert_eq_parse!("foo=bar; SameSite=NoNE", expected); - assert_eq_parse!("foo=bar; SameSite=NONE", expected); - } - - #[test] - fn parse() { - assert!(Cookie::parse("bar").is_err()); - assert!(Cookie::parse("=bar").is_err()); - assert!(Cookie::parse(" =bar").is_err()); - assert!(Cookie::parse("foo=").is_ok()); - - let expected = Cookie::build("foo", "bar=baz").finish(); - assert_eq_parse!("foo=bar=baz", expected); - - let mut expected = Cookie::build("foo", "bar").finish(); - assert_eq_parse!("foo=bar", expected); - assert_eq_parse!("foo = bar", expected); - assert_eq_parse!(" foo=bar ", expected); - assert_eq_parse!(" foo=bar ;Domain=", expected); - assert_eq_parse!(" foo=bar ;Domain= ", expected); - assert_eq_parse!(" foo=bar ;Ignored", expected); - - let mut unexpected = Cookie::build("foo", "bar").http_only(false).finish(); - assert_ne_parse!(" foo=bar ;HttpOnly", unexpected); - assert_ne_parse!(" foo=bar; httponly", unexpected); - - expected.set_http_only(true); - assert_eq_parse!(" foo=bar ;HttpOnly", expected); - assert_eq_parse!(" foo=bar ;httponly", expected); - assert_eq_parse!(" foo=bar ;HTTPONLY=whatever", expected); - assert_eq_parse!(" foo=bar ; sekure; HTTPONLY", expected); - - expected.set_secure(true); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure", expected); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure=aaaa", expected); - - unexpected.set_http_only(true); - unexpected.set_secure(true); - assert_ne_parse!(" foo=bar ;HttpOnly; skeure", unexpected); - assert_ne_parse!(" foo=bar ;HttpOnly; =secure", unexpected); - assert_ne_parse!(" foo=bar ;HttpOnly;", unexpected); - - unexpected.set_secure(false); - assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected); - assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected); - assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected); - - expected.set_max_age(Duration::zero()); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=0", expected); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 0 ", expected); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=-1", expected); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = -1 ", expected); - - expected.set_max_age(Duration::minutes(1)); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=60", expected); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 60 ", expected); - - expected.set_max_age(Duration::seconds(4)); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4", expected); - assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 4 ", expected); - - unexpected.set_secure(true); - unexpected.set_max_age(Duration::minutes(1)); - assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=122", unexpected); - assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 38 ", unexpected); - assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=51", unexpected); - assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = -1 ", unexpected); - assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 0", unexpected); - - expected.set_path("/"); - assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/", expected); - assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/", expected); - - expected.set_path("/foo"); - assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/foo", expected); - assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/foo", expected); - assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;path=/foo", expected); - assert_eq_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;path = /foo", expected); - - unexpected.set_max_age(Duration::seconds(4)); - unexpected.set_path("/bar"); - assert_ne_parse!("foo=bar;HttpOnly; Secure; Max-Age=4; Path=/foo", unexpected); - assert_ne_parse!("foo=bar;HttpOnly; Secure; Max-Age=4;Path=/baz", unexpected); - - expected.set_domain("www.foo.com"); - assert_eq_parse!( - " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ - Domain=www.foo.com", - expected - ); - - expected.set_domain("foo.com"); - assert_eq_parse!( - " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ - Domain=foo.com", - expected - ); - assert_eq_parse!( - " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ - Domain=FOO.COM", - expected - ); - - unexpected.set_path("/foo"); - unexpected.set_domain("bar.com"); - assert_ne_parse!( - " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ - Domain=foo.com", - unexpected - ); - assert_ne_parse!( - " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ - Domain=FOO.COM", - unexpected - ); - - let time_str = "Wed, 21 Oct 2015 07:28:00 GMT"; - let expires = PrimitiveDateTime::parse(time_str, "%a, %d %b %Y %H:%M:%S") - .unwrap() - .assume_utc(); - expected.set_expires(expires); - assert_eq_parse!( - " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ - Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT", - expected - ); - - unexpected.set_domain("foo.com"); - let bad_expires = PrimitiveDateTime::parse(time_str, "%a, %d %b %Y %H:%S:%M") - .unwrap() - .assume_utc(); - expected.set_expires(bad_expires); - assert_ne_parse!( - " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ - Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT", - unexpected - ); - - expected.set_expires(expires); - expected.set_same_site(SameSite::Lax); - assert_eq_parse!( - " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ - Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT; \ - SameSite=Lax", - expected - ); - expected.set_same_site(SameSite::Strict); - assert_eq_parse!( - " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ - Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT; \ - SameSite=Strict", - expected - ); - expected.set_same_site(SameSite::None); - assert_eq_parse!( - " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ - Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT; \ - SameSite=None", - expected - ); - } - - #[test] - fn odd_characters() { - let expected = Cookie::new("foo", "b%2Fr"); - assert_eq_parse!("foo=b%2Fr", expected); - } - - #[test] - fn odd_characters_encoded() { - let expected = Cookie::new("foo", "b/r"); - let cookie = match Cookie::parse_encoded("foo=b%2Fr") { - Ok(cookie) => cookie, - Err(e) => panic!("Failed to parse: {:?}", e), - }; - - assert_eq!(cookie, expected); - } - - #[test] - fn do_not_panic_on_large_max_ages() { - let max_duration = Duration::max_value(); - let expected = Cookie::build("foo", "bar") - .max_age_time(max_duration) - .finish(); - let overflow_duration = max_duration - .checked_add(Duration::nanoseconds(1)) - .unwrap_or(max_duration); - assert_eq_parse!( - format!(" foo=bar; Max-Age={:?}", overflow_duration.whole_seconds()), - expected - ); - } -} diff --git a/actix-http/src/cookie/secure/key.rs b/actix-http/src/cookie/secure/key.rs deleted file mode 100644 index 41413921f..000000000 --- a/actix-http/src/cookie/secure/key.rs +++ /dev/null @@ -1,190 +0,0 @@ -use ring::hkdf::{Algorithm, KeyType, Prk, HKDF_SHA256}; -use ring::rand::{SecureRandom, SystemRandom}; - -use super::private::KEY_LEN as PRIVATE_KEY_LEN; -use super::signed::KEY_LEN as SIGNED_KEY_LEN; - -static HKDF_DIGEST: Algorithm = HKDF_SHA256; -const KEYS_INFO: &[&[u8]] = &[b"COOKIE;SIGNED:HMAC-SHA256;PRIVATE:AEAD-AES-256-GCM"]; - -/// A cryptographic master key for use with `Signed` and/or `Private` jars. -/// -/// This structure encapsulates secure, cryptographic keys for use with both -/// [PrivateJar](struct.PrivateJar.html) and [SignedJar](struct.SignedJar.html). -/// It can be derived from a single master key via -/// [from_master](#method.from_master) or generated from a secure random source -/// via [generate](#method.generate). A single instance of `Key` can be used for -/// both a `PrivateJar` and a `SignedJar`. -/// -/// This type is only available when the `secure` feature is enabled. -#[derive(Clone)] -pub struct Key { - signing_key: [u8; SIGNED_KEY_LEN], - encryption_key: [u8; PRIVATE_KEY_LEN], -} - -impl KeyType for &Key { - #[inline] - fn len(&self) -> usize { - SIGNED_KEY_LEN + PRIVATE_KEY_LEN - } -} - -impl Key { - /// Derives new signing/encryption keys from a master key. - /// - /// The master key must be at least 256-bits (32 bytes). For security, the - /// master key _must_ be cryptographically random. The keys are derived - /// deterministically from the master key. - /// - /// # Panics - /// - /// Panics if `key` is less than 32 bytes in length. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Key; - /// - /// # /* - /// let master_key = { /* a cryptographically random key >= 32 bytes */ }; - /// # */ - /// # let master_key: &Vec = &(0..32).collect(); - /// - /// let key = Key::from_master(master_key); - /// ``` - pub fn from_master(key: &[u8]) -> Key { - if key.len() < 32 { - panic!( - "bad master key length: expected at least 32 bytes, found {}", - key.len() - ); - } - - // An empty `Key` structure; will be filled in with HKDF derived keys. - let mut output_key = Key { - signing_key: [0; SIGNED_KEY_LEN], - encryption_key: [0; PRIVATE_KEY_LEN], - }; - - // Expand the master key into two HKDF generated keys. - let mut both_keys = [0; SIGNED_KEY_LEN + PRIVATE_KEY_LEN]; - let prk = Prk::new_less_safe(HKDF_DIGEST, key); - let okm = prk.expand(KEYS_INFO, &output_key).expect("okm expand"); - okm.fill(&mut both_keys).expect("fill keys"); - - // Copy the key parts into their respective fields. - output_key - .signing_key - .copy_from_slice(&both_keys[..SIGNED_KEY_LEN]); - output_key - .encryption_key - .copy_from_slice(&both_keys[SIGNED_KEY_LEN..]); - output_key - } - - /// Generates signing/encryption keys from a secure, random source. Keys are - /// generated non-deterministically. - /// - /// # Panics - /// - /// Panics if randomness cannot be retrieved from the operating system. See - /// [try_generate](#method.try_generate) for a non-panicking version. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Key; - /// - /// let key = Key::generate(); - /// ``` - pub fn generate() -> Key { - Self::try_generate().expect("failed to generate `Key` from randomness") - } - - /// Attempts to generate signing/encryption keys from a secure, random - /// source. Keys are generated non-deterministically. If randomness cannot be - /// retrieved from the underlying operating system, returns `None`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Key; - /// - /// let key = Key::try_generate(); - /// ``` - pub fn try_generate() -> Option { - let mut sign_key = [0; SIGNED_KEY_LEN]; - let mut enc_key = [0; PRIVATE_KEY_LEN]; - - let rng = SystemRandom::new(); - if rng.fill(&mut sign_key).is_err() || rng.fill(&mut enc_key).is_err() { - return None; - } - - Some(Key { - signing_key: sign_key, - encryption_key: enc_key, - }) - } - - /// Returns the raw bytes of a key suitable for signing cookies. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Key; - /// - /// let key = Key::generate(); - /// let signing_key = key.signing(); - /// ``` - pub fn signing(&self) -> &[u8] { - &self.signing_key[..] - } - - /// Returns the raw bytes of a key suitable for encrypting cookies. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::Key; - /// - /// let key = Key::generate(); - /// let encryption_key = key.encryption(); - /// ``` - pub fn encryption(&self) -> &[u8] { - &self.encryption_key[..] - } -} - -#[cfg(test)] -mod test { - use super::Key; - - #[test] - fn deterministic_from_master() { - let master_key: Vec = (0..32).collect(); - - let key_a = Key::from_master(&master_key); - let key_b = Key::from_master(&master_key); - - assert_eq!(key_a.signing(), key_b.signing()); - assert_eq!(key_a.encryption(), key_b.encryption()); - assert_ne!(key_a.encryption(), key_a.signing()); - - let master_key_2: Vec = (32..64).collect(); - let key_2 = Key::from_master(&master_key_2); - - assert_ne!(key_2.signing(), key_a.signing()); - assert_ne!(key_2.encryption(), key_a.encryption()); - } - - #[test] - fn non_deterministic_generate() { - let key_a = Key::generate(); - let key_b = Key::generate(); - - assert_ne!(key_a.signing(), key_b.signing()); - assert_ne!(key_a.encryption(), key_b.encryption()); - } -} diff --git a/actix-http/src/cookie/secure/macros.rs b/actix-http/src/cookie/secure/macros.rs deleted file mode 100644 index 089047c4e..000000000 --- a/actix-http/src/cookie/secure/macros.rs +++ /dev/null @@ -1,40 +0,0 @@ -#[cfg(test)] -macro_rules! assert_simple_behaviour { - ($clear:expr, $secure:expr) => {{ - assert_eq!($clear.iter().count(), 0); - - $secure.add(Cookie::new("name", "val")); - assert_eq!($clear.iter().count(), 1); - assert_eq!($secure.get("name").unwrap().value(), "val"); - assert_ne!($clear.get("name").unwrap().value(), "val"); - - $secure.add(Cookie::new("another", "two")); - assert_eq!($clear.iter().count(), 2); - - $clear.remove(Cookie::named("another")); - assert_eq!($clear.iter().count(), 1); - - $secure.remove(Cookie::named("name")); - assert_eq!($clear.iter().count(), 0); - }}; -} - -#[cfg(test)] -macro_rules! assert_secure_behaviour { - ($clear:expr, $secure:expr) => {{ - $secure.add(Cookie::new("secure", "secure")); - assert!($clear.get("secure").unwrap().value() != "secure"); - assert!($secure.get("secure").unwrap().value() == "secure"); - - let mut cookie = $clear.get("secure").unwrap().clone(); - let new_val = format!("{}l", cookie.value()); - cookie.set_value(new_val); - $clear.add(cookie); - assert!($secure.get("secure").is_none()); - - let mut cookie = $clear.get("secure").unwrap().clone(); - cookie.set_value("foobar"); - $clear.add(cookie); - assert!($secure.get("secure").is_none()); - }}; -} diff --git a/actix-http/src/cookie/secure/mod.rs b/actix-http/src/cookie/secure/mod.rs deleted file mode 100644 index e0fba9733..000000000 --- a/actix-http/src/cookie/secure/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Fork of https://github.com/alexcrichton/cookie-rs -#[macro_use] -mod macros; -mod key; -mod private; -mod signed; - -pub use self::key::*; -pub use self::private::*; -pub use self::signed::*; diff --git a/actix-http/src/cookie/secure/private.rs b/actix-http/src/cookie/secure/private.rs deleted file mode 100644 index f05e23800..000000000 --- a/actix-http/src/cookie/secure/private.rs +++ /dev/null @@ -1,275 +0,0 @@ -use std::str; - -use log::warn; -use ring::aead::{Aad, Algorithm, Nonce, AES_256_GCM}; -use ring::aead::{LessSafeKey, UnboundKey}; -use ring::rand::{SecureRandom, SystemRandom}; - -use super::Key; -use crate::cookie::{Cookie, CookieJar}; - -// Keep these in sync, and keep the key len synced with the `private` docs as -// well as the `KEYS_INFO` const in secure::Key. -static ALGO: &Algorithm = &AES_256_GCM; -const NONCE_LEN: usize = 12; -pub const KEY_LEN: usize = 32; - -/// A child cookie jar that provides authenticated encryption for its cookies. -/// -/// A _private_ child jar signs and encrypts all the cookies added to it and -/// verifies and decrypts cookies retrieved from it. Any cookies stored in a -/// `PrivateJar` are simultaneously assured confidentiality, integrity, and -/// authenticity. In other words, clients cannot discover nor tamper with the -/// contents of a cookie, nor can they fabricate cookie data. -/// -/// This type is only available when the `secure` feature is enabled. -pub struct PrivateJar<'a> { - parent: &'a mut CookieJar, - key: [u8; KEY_LEN], -} - -impl<'a> PrivateJar<'a> { - /// Creates a new child `PrivateJar` with parent `parent` and key `key`. - /// This method is typically called indirectly via the `signed` method of - /// `CookieJar`. - #[doc(hidden)] - pub fn new(parent: &'a mut CookieJar, key: &Key) -> PrivateJar<'a> { - let mut key_array = [0u8; KEY_LEN]; - key_array.copy_from_slice(key.encryption()); - PrivateJar { - parent, - key: key_array, - } - } - - /// Given a sealed value `str` and a key name `name`, where the nonce is - /// prepended to the original value and then both are Base64 encoded, - /// verifies and decrypts the sealed value and returns it. If there's a - /// problem, returns an `Err` with a string describing the issue. - fn unseal(&self, name: &str, value: &str) -> Result { - let mut data = base64::decode(value).map_err(|_| "bad base64 value")?; - if data.len() <= NONCE_LEN { - return Err("length of decoded data is <= NONCE_LEN"); - } - - let ad = Aad::from(name.as_bytes()); - let key = LessSafeKey::new( - UnboundKey::new(&ALGO, &self.key).expect("matching key length"), - ); - let (nonce, mut sealed) = data.split_at_mut(NONCE_LEN); - let nonce = - Nonce::try_assume_unique_for_key(nonce).expect("invalid length of `nonce`"); - let unsealed = key - .open_in_place(nonce, ad, &mut sealed) - .map_err(|_| "invalid key/nonce/value: bad seal")?; - - if let Ok(unsealed_utf8) = str::from_utf8(unsealed) { - Ok(unsealed_utf8.to_string()) - } else { - warn!( - "Private cookie does not have utf8 content! -It is likely the secret key used to encrypt them has been leaked. -Please change it as soon as possible." - ); - Err("bad unsealed utf8") - } - } - - /// Returns a reference to the `Cookie` inside this jar with the name `name` - /// and authenticates and decrypts the cookie's value, returning a `Cookie` - /// with the decrypted value. If the cookie cannot be found, or the cookie - /// fails to authenticate or decrypt, `None` is returned. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie, Key}; - /// - /// let key = Key::generate(); - /// let mut jar = CookieJar::new(); - /// let mut private_jar = jar.private(&key); - /// assert!(private_jar.get("name").is_none()); - /// - /// private_jar.add(Cookie::new("name", "value")); - /// assert_eq!(private_jar.get("name").unwrap().value(), "value"); - /// ``` - pub fn get(&self, name: &str) -> Option> { - if let Some(cookie_ref) = self.parent.get(name) { - let mut cookie = cookie_ref.clone(); - if let Ok(value) = self.unseal(name, cookie.value()) { - cookie.set_value(value); - return Some(cookie); - } - } - - None - } - - /// Adds `cookie` to the parent jar. The cookie's value is encrypted with - /// authenticated encryption assuring confidentiality, integrity, and - /// authenticity. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie, Key}; - /// - /// let key = Key::generate(); - /// let mut jar = CookieJar::new(); - /// jar.private(&key).add(Cookie::new("name", "value")); - /// - /// assert_ne!(jar.get("name").unwrap().value(), "value"); - /// assert_eq!(jar.private(&key).get("name").unwrap().value(), "value"); - /// ``` - pub fn add(&mut self, mut cookie: Cookie<'static>) { - self.encrypt_cookie(&mut cookie); - - // Add the sealed cookie to the parent. - self.parent.add(cookie); - } - - /// Adds an "original" `cookie` to parent jar. The cookie's value is - /// encrypted with authenticated encryption assuring confidentiality, - /// integrity, and authenticity. Adding an original cookie does not affect - /// the [`CookieJar::delta()`](struct.CookieJar.html#method.delta) - /// computation. This method is intended to be used to seed the cookie jar - /// with cookies received from a client's HTTP message. - /// - /// For accurate `delta` computations, this method should not be called - /// after calling `remove`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie, Key}; - /// - /// let key = Key::generate(); - /// let mut jar = CookieJar::new(); - /// jar.private(&key).add_original(Cookie::new("name", "value")); - /// - /// assert_eq!(jar.iter().count(), 1); - /// assert_eq!(jar.delta().count(), 0); - /// ``` - pub fn add_original(&mut self, mut cookie: Cookie<'static>) { - self.encrypt_cookie(&mut cookie); - - // Add the sealed cookie to the parent. - self.parent.add_original(cookie); - } - - /// Encrypts the cookie's value with - /// authenticated encryption assuring confidentiality, integrity, and authenticity. - fn encrypt_cookie(&self, cookie: &mut Cookie<'_>) { - let name = cookie.name().as_bytes(); - let value = cookie.value().as_bytes(); - let data = encrypt_name_value(name, value, &self.key); - - // Base64 encode the nonce and encrypted value. - let sealed_value = base64::encode(&data); - cookie.set_value(sealed_value); - } - - /// Removes `cookie` from the parent jar. - /// - /// For correct removal, the passed in `cookie` must contain the same `path` - /// and `domain` as the cookie that was initially set. - /// - /// See [CookieJar::remove](struct.CookieJar.html#method.remove) for more - /// details. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie, Key}; - /// - /// let key = Key::generate(); - /// let mut jar = CookieJar::new(); - /// let mut private_jar = jar.private(&key); - /// - /// private_jar.add(Cookie::new("name", "value")); - /// assert!(private_jar.get("name").is_some()); - /// - /// private_jar.remove(Cookie::named("name")); - /// assert!(private_jar.get("name").is_none()); - /// ``` - pub fn remove(&mut self, cookie: Cookie<'static>) { - self.parent.remove(cookie); - } -} - -fn encrypt_name_value(name: &[u8], value: &[u8], key: &[u8]) -> Vec { - // Create the `SealingKey` structure. - let unbound = UnboundKey::new(&ALGO, key).expect("matching key length"); - let key = LessSafeKey::new(unbound); - - // Create a vec to hold the [nonce | cookie value | overhead]. - let mut data = vec![0; NONCE_LEN + value.len() + ALGO.tag_len()]; - - // Randomly generate the nonce, then copy the cookie value as input. - let (nonce, in_out) = data.split_at_mut(NONCE_LEN); - let (in_out, tag) = in_out.split_at_mut(value.len()); - in_out.copy_from_slice(value); - - // Randomly generate the nonce into the nonce piece. - SystemRandom::new() - .fill(nonce) - .expect("couldn't random fill nonce"); - let nonce = Nonce::try_assume_unique_for_key(nonce).expect("invalid `nonce` length"); - - // Use cookie's name as associated data to prevent value swapping. - let ad = Aad::from(name); - let ad_tag = key - .seal_in_place_separate_tag(nonce, ad, in_out) - .expect("in-place seal"); - - // Copy the tag into the tag piece. - tag.copy_from_slice(ad_tag.as_ref()); - - // Remove the overhead and return the sealed content. - data -} - -#[cfg(test)] -mod test { - use super::{encrypt_name_value, Cookie, CookieJar, Key}; - - #[test] - fn simple() { - let key = Key::generate(); - let mut jar = CookieJar::new(); - assert_simple_behaviour!(jar, jar.private(&key)); - } - - #[test] - fn private() { - let key = Key::generate(); - let mut jar = CookieJar::new(); - assert_secure_behaviour!(jar, jar.private(&key)); - } - - #[test] - fn non_utf8() { - let key = Key::generate(); - let mut jar = CookieJar::new(); - - let name = "malicious"; - let mut assert_non_utf8 = |value: &[u8]| { - let sealed = encrypt_name_value(name.as_bytes(), value, &key.encryption()); - let encoded = base64::encode(&sealed); - assert_eq!( - jar.private(&key).unseal(name, &encoded), - Err("bad unsealed utf8") - ); - jar.add(Cookie::new(name, encoded)); - assert_eq!(jar.private(&key).get(name), None); - }; - - assert_non_utf8(&[0x72, 0xfb, 0xdf, 0x74]); // rûst in ISO/IEC 8859-1 - - let mut malicious = - String::from(r#"{"id":"abc123??%X","admin":true}"#).into_bytes(); - malicious[8] |= 0b1100_0000; - malicious[9] |= 0b1100_0000; - assert_non_utf8(&malicious); - } -} diff --git a/actix-http/src/cookie/secure/signed.rs b/actix-http/src/cookie/secure/signed.rs deleted file mode 100644 index 64e8d5dda..000000000 --- a/actix-http/src/cookie/secure/signed.rs +++ /dev/null @@ -1,184 +0,0 @@ -use ring::hmac::{self, sign, verify}; - -use super::Key; -use crate::cookie::{Cookie, CookieJar}; - -// Keep these in sync, and keep the key len synced with the `signed` docs as -// well as the `KEYS_INFO` const in secure::Key. -static HMAC_DIGEST: hmac::Algorithm = hmac::HMAC_SHA256; -const BASE64_DIGEST_LEN: usize = 44; -pub const KEY_LEN: usize = 32; - -/// A child cookie jar that authenticates its cookies. -/// -/// A _signed_ child jar signs all the cookies added to it and verifies cookies -/// retrieved from it. Any cookies stored in a `SignedJar` are assured integrity -/// and authenticity. In other words, clients cannot tamper with the contents of -/// a cookie nor can they fabricate cookie values, but the data is visible in -/// plaintext. -/// -/// This type is only available when the `secure` feature is enabled. -pub struct SignedJar<'a> { - parent: &'a mut CookieJar, - key: hmac::Key, -} - -impl<'a> SignedJar<'a> { - /// Creates a new child `SignedJar` with parent `parent` and key `key`. This - /// method is typically called indirectly via the `signed` method of - /// `CookieJar`. - #[doc(hidden)] - pub fn new(parent: &'a mut CookieJar, key: &Key) -> SignedJar<'a> { - SignedJar { - parent, - key: hmac::Key::new(HMAC_DIGEST, key.signing()), - } - } - - /// Given a signed value `str` where the signature is prepended to `value`, - /// verifies the signed value and returns it. If there's a problem, returns - /// an `Err` with a string describing the issue. - fn verify(&self, cookie_value: &str) -> Result { - if cookie_value.len() < BASE64_DIGEST_LEN { - return Err("length of value is <= BASE64_DIGEST_LEN"); - } - - let (digest_str, value) = cookie_value.split_at(BASE64_DIGEST_LEN); - let sig = base64::decode(digest_str).map_err(|_| "bad base64 digest")?; - - verify(&self.key, value.as_bytes(), &sig) - .map(|_| value.to_string()) - .map_err(|_| "value did not verify") - } - - /// Returns a reference to the `Cookie` inside this jar with the name `name` - /// and verifies the authenticity and integrity of the cookie's value, - /// returning a `Cookie` with the authenticated value. If the cookie cannot - /// be found, or the cookie fails to verify, `None` is returned. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie, Key}; - /// - /// let key = Key::generate(); - /// let mut jar = CookieJar::new(); - /// let mut signed_jar = jar.signed(&key); - /// assert!(signed_jar.get("name").is_none()); - /// - /// signed_jar.add(Cookie::new("name", "value")); - /// assert_eq!(signed_jar.get("name").unwrap().value(), "value"); - /// ``` - pub fn get(&self, name: &str) -> Option> { - if let Some(cookie_ref) = self.parent.get(name) { - let mut cookie = cookie_ref.clone(); - if let Ok(value) = self.verify(cookie.value()) { - cookie.set_value(value); - return Some(cookie); - } - } - - None - } - - /// Adds `cookie` to the parent jar. The cookie's value is signed assuring - /// integrity and authenticity. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie, Key}; - /// - /// let key = Key::generate(); - /// let mut jar = CookieJar::new(); - /// jar.signed(&key).add(Cookie::new("name", "value")); - /// - /// assert_ne!(jar.get("name").unwrap().value(), "value"); - /// assert!(jar.get("name").unwrap().value().contains("value")); - /// assert_eq!(jar.signed(&key).get("name").unwrap().value(), "value"); - /// ``` - pub fn add(&mut self, mut cookie: Cookie<'static>) { - self.sign_cookie(&mut cookie); - self.parent.add(cookie); - } - - /// Adds an "original" `cookie` to this jar. The cookie's value is signed - /// assuring integrity and authenticity. Adding an original cookie does not - /// affect the [`CookieJar::delta()`](struct.CookieJar.html#method.delta) - /// computation. This method is intended to be used to seed the cookie jar - /// with cookies received from a client's HTTP message. - /// - /// For accurate `delta` computations, this method should not be called - /// after calling `remove`. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie, Key}; - /// - /// let key = Key::generate(); - /// let mut jar = CookieJar::new(); - /// jar.signed(&key).add_original(Cookie::new("name", "value")); - /// - /// assert_eq!(jar.iter().count(), 1); - /// assert_eq!(jar.delta().count(), 0); - /// ``` - pub fn add_original(&mut self, mut cookie: Cookie<'static>) { - self.sign_cookie(&mut cookie); - self.parent.add_original(cookie); - } - - /// Signs the cookie's value assuring integrity and authenticity. - fn sign_cookie(&self, cookie: &mut Cookie<'_>) { - let digest = sign(&self.key, cookie.value().as_bytes()); - let mut new_value = base64::encode(digest.as_ref()); - new_value.push_str(cookie.value()); - cookie.set_value(new_value); - } - - /// Removes `cookie` from the parent jar. - /// - /// For correct removal, the passed in `cookie` must contain the same `path` - /// and `domain` as the cookie that was initially set. - /// - /// See [CookieJar::remove](struct.CookieJar.html#method.remove) for more - /// details. - /// - /// # Example - /// - /// ```rust - /// use actix_http::cookie::{CookieJar, Cookie, Key}; - /// - /// let key = Key::generate(); - /// let mut jar = CookieJar::new(); - /// let mut signed_jar = jar.signed(&key); - /// - /// signed_jar.add(Cookie::new("name", "value")); - /// assert!(signed_jar.get("name").is_some()); - /// - /// signed_jar.remove(Cookie::named("name")); - /// assert!(signed_jar.get("name").is_none()); - /// ``` - pub fn remove(&mut self, cookie: Cookie<'static>) { - self.parent.remove(cookie); - } -} - -#[cfg(test)] -mod test { - use super::{Cookie, CookieJar, Key}; - - #[test] - fn simple() { - let key = Key::generate(); - let mut jar = CookieJar::new(); - assert_simple_behaviour!(jar, jar.signed(&key)); - } - - #[test] - fn private() { - let key = Key::generate(); - let mut jar = CookieJar::new(); - assert_secure_behaviour!(jar, jar.signed(&key)); - } -} diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index 9f615a129..dd8f5ee12 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -32,7 +32,7 @@ mod response; mod service; mod time_parser; -pub mod cookie; +pub use cookie; pub mod error; pub mod h1; pub mod h2; diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs index 9086212f1..2def67168 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/response.rs @@ -877,7 +877,7 @@ mod tests { .domain("www.rust-lang.org") .path("/test") .http_only(true) - .max_age_time(time::Duration::days(1)) + .max_age(time::Duration::days(1)) .finish(), ) .del_cookie(&cookies[1]) diff --git a/actix-http/src/test.rs b/actix-http/src/test.rs index 061ba610f..b79f5a73c 100644 --- a/actix-http/src/test.rs +++ b/actix-http/src/test.rs @@ -1,6 +1,5 @@ //! Test Various helpers for Actix applications to use during testing. use std::convert::TryFrom; -use std::fmt::Write as FmtWrite; use std::io::{self, Read, Write}; use std::pin::Pin; use std::str::FromStr; @@ -10,9 +9,8 @@ use actix_codec::{AsyncRead, AsyncWrite}; use bytes::{Bytes, BytesMut}; use http::header::{self, HeaderName, HeaderValue}; use http::{Error as HttpError, Method, Uri, Version}; -use percent_encoding::percent_encode; -use crate::cookie::{Cookie, CookieJar, USERINFO}; +use crate::cookie::{Cookie, CookieJar}; use crate::header::HeaderMap; use crate::header::{Header, IntoHeaderValue}; use crate::payload::Payload; @@ -163,17 +161,17 @@ impl TestRequest { head.version = inner.version; head.headers = inner.headers; - let mut cookie = String::new(); - for c in inner.cookies.delta() { - let name = percent_encode(c.name().as_bytes(), USERINFO); - let value = percent_encode(c.value().as_bytes(), USERINFO); - let _ = write!(&mut cookie, "; {}={}", name, value); - } + let cookie: String = inner + .cookies + .delta() + // ensure only name=value is written to cookie header + .map(|c| Cookie::new(c.name(), c.value()).encoded().to_string()) + .collect::>() + .join("; "); + if !cookie.is_empty() { - head.headers.insert( - header::COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), - ); + head.headers + .insert(header::COOKIE, HeaderValue::from_str(&cookie).unwrap()); } req diff --git a/awc/src/request.rs b/awc/src/request.rs index 21a7cd911..3dd8cb2ce 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -1,16 +1,14 @@ use std::convert::TryFrom; -use std::fmt::Write as FmtWrite; use std::rc::Rc; use std::time::Duration; use std::{fmt, net}; use bytes::Bytes; use futures_core::Stream; -use percent_encoding::percent_encode; use serde::Serialize; use actix_http::body::Body; -use actix_http::cookie::{Cookie, CookieJar, USERINFO}; +use actix_http::cookie::{Cookie, CookieJar}; use actix_http::http::header::{self, Header, IntoHeaderValue}; use actix_http::http::{ uri, ConnectionType, Error as HttpError, HeaderMap, HeaderName, HeaderValue, Method, @@ -527,16 +525,18 @@ impl ClientRequest { // set cookies if let Some(ref mut jar) = self.cookies { - let mut cookie = String::new(); - for c in jar.delta() { - let name = percent_encode(c.name().as_bytes(), USERINFO); - let value = percent_encode(c.value().as_bytes(), USERINFO); - let _ = write!(&mut cookie, "; {}={}", name, value); + let cookie: String = jar + .delta() + // ensure only name=value is written to cookie header + .map(|c| Cookie::new(c.name(), c.value()).encoded().to_string()) + .collect::>() + .join("; "); + + if !cookie.is_empty() { + self.head + .headers + .insert(header::COOKIE, HeaderValue::from_str(&cookie).unwrap()); } - self.head.headers.insert( - header::COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), - ); } let mut slf = self; diff --git a/awc/src/test.rs b/awc/src/test.rs index a6cbd03e6..68e5c9dc5 100644 --- a/awc/src/test.rs +++ b/awc/src/test.rs @@ -1,13 +1,11 @@ //! Test helpers for actix http client to use during testing. use std::convert::TryFrom; -use std::fmt::Write as FmtWrite; -use actix_http::cookie::{Cookie, CookieJar, USERINFO}; +use actix_http::cookie::{Cookie, CookieJar}; use actix_http::http::header::{self, Header, HeaderValue, IntoHeaderValue}; use actix_http::http::{Error as HttpError, HeaderName, StatusCode, Version}; use actix_http::{h1, Payload, ResponseHead}; use bytes::Bytes; -use percent_encoding::percent_encode; use crate::ClientResponse; @@ -88,16 +86,10 @@ impl TestResponse { pub fn finish(self) -> ClientResponse { let mut head = self.head; - let mut cookie = String::new(); - for c in self.cookies.delta() { - let name = percent_encode(c.name().as_bytes(), USERINFO); - let value = percent_encode(c.value().as_bytes(), USERINFO); - let _ = write!(&mut cookie, "; {}={}", name, value); - } - if !cookie.is_empty() { + for cookie in self.cookies.delta() { head.headers.insert( header::SET_COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), + HeaderValue::from_str(&cookie.encoded().to_string()).unwrap(), ); } diff --git a/awc/src/ws.rs b/awc/src/ws.rs index 89ca50b59..6ad660c41 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -1,6 +1,5 @@ //! Websockets client use std::convert::TryFrom; -use std::fmt::Write as FmtWrite; use std::net::SocketAddr; use std::rc::Rc; use std::{fmt, str}; @@ -9,9 +8,7 @@ use actix_codec::Framed; use actix_http::cookie::{Cookie, CookieJar}; use actix_http::{ws, Payload, RequestHead}; use actix_rt::time::timeout; -use percent_encoding::percent_encode; -use actix_http::cookie::USERINFO; pub use actix_http::ws::{CloseCode, CloseReason, Codec, Frame, Message}; use crate::connect::BoxedSocket; @@ -246,16 +243,18 @@ impl WebsocketsRequest { // set cookies if let Some(ref mut jar) = self.cookies { - let mut cookie = String::new(); - for c in jar.delta() { - let name = percent_encode(c.name().as_bytes(), USERINFO); - let value = percent_encode(c.value().as_bytes(), USERINFO); - let _ = write!(&mut cookie, "; {}={}", name, value); + let cookie: String = jar + .delta() + // ensure only name=value is written to cookie header + .map(|c| Cookie::new(c.name(), c.value()).encoded().to_string()) + .collect::>() + .join("; "); + + if !cookie.is_empty() { + self.head + .headers + .insert(header::COOKIE, HeaderValue::from_str(&cookie).unwrap()); } - self.head.headers.insert( - header::COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), - ); } // origin From a7c853329193d7eb4f05c62d1a57b900ce62b0e9 Mon Sep 17 00:00:00 2001 From: Abhishek Yadav <55960554+b4skyx@users.noreply.github.com> Date: Mon, 22 Jun 2020 04:14:48 +0000 Subject: [PATCH 06/78] Update benchmark url in README.md Updated benchmark url from r18 to the latest r19. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6382abd4d..ade632877 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ You may consider checking out ## Benchmarks -* [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r18) +* [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r19) ## License From c11052f826517d4b169238ccb49cf996834daa81 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 22 Jun 2020 10:28:06 +0100 Subject: [PATCH 07/78] add PR template for bugs and features (#1570) Co-authored-by: Yuki Okushi --- .github/ISSUE_TEMPLATE/bug_report.md | 4 +-- .../PULL_REQUEST_TEMPLATE/bug_or_feature.md | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE/bug_or_feature.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 128f51ffd..2df863ae8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,6 @@ --- -name: bug report -about: create a bug report +name: Bug Report +about: Create a bug report. --- Your issue may already be reported! diff --git a/.github/PULL_REQUEST_TEMPLATE/bug_or_feature.md b/.github/PULL_REQUEST_TEMPLATE/bug_or_feature.md new file mode 100644 index 000000000..a1fa98ea8 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/bug_or_feature.md @@ -0,0 +1,33 @@ +--- +name: Bug / Feature +about: Submit a bug fix or feature. +--- + +## PR Type +What kind of change does this PR make? + + + +- [ ] Bug fix +- [ ] Feature +- [ ] Refactor / code style change (no functional or public API changes) +- [ ] Other + + +## PR Checklist +Check your PR fulfills the following: + + + +- [ ] Tests for the changes have been added / updated. +- [ ] Documentation comments have been added / updated. +- [ ] A changelog entry has been made for the appropriate packages. + + +## Overview + + + + + + From a70e599ff5b4e0a24aaf4565da075bfac1bfdec3 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 22 Jun 2020 20:09:48 +0100 Subject: [PATCH 08/78] re-export rt in web and add main macro (#1575) --- Cargo.toml | 4 +-- actix-files/Cargo.toml | 1 - actix-web-codegen/CHANGES.md | 5 +++ actix-web-codegen/Cargo.toml | 1 - actix-web-codegen/README.md | 2 +- actix-web-codegen/src/lib.rs | 53 ++++++++++++++++++++++++++++---- rust-toolchain | 1 + src/app.rs | 1 + src/info.rs | 2 +- src/lib.rs | 21 ++++--------- src/middleware/compress.rs | 1 + src/middleware/defaultheaders.rs | 1 + src/middleware/logger.rs | 2 +- src/route.rs | 1 + src/types/form.rs | 1 + src/types/json.rs | 1 + src/types/payload.rs | 1 + test-server/Cargo.toml | 1 - 18 files changed, 71 insertions(+), 29 deletions(-) create mode 100644 rust-toolchain diff --git a/Cargo.toml b/Cargo.toml index de7222f1f..8c4719b92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,8 +97,8 @@ serde_json = "1.0" serde_urlencoded = "0.6.1" time = { version = "0.2.7", default-features = false, features = ["std"] } url = "2.1" -open-ssl = { version="0.10", package = "openssl", optional = true } -rust-tls = { version = "0.17.0", package = "rustls", optional = true } +open-ssl = { package = "openssl", version = "0.10", optional = true } +rust-tls = { package = "rustls", version = "0.17.0", optional = true } tinyvec = { version = "0.3", features = ["alloc"] } [dev-dependencies] diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 356c7a413..76e528b12 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -11,7 +11,6 @@ documentation = "https://docs.rs/actix-files/" categories = ["asynchronous", "web-programming::http-server"] license = "MIT/Apache-2.0" edition = "2018" -workspace = ".." [lib] name = "actix_files" diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index b2e80591f..8259aaac2 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,5 +1,10 @@ # Changes +## [Unreleased] - XXXX-XX-XX + +* Add main entry-point macro that uses re-exported runtime. + + ## [0.2.2] - 2020-05-23 * Add resource middleware on actix-web-codegen [#1467] diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 60480a7a1..178eeeb7e 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -9,7 +9,6 @@ documentation = "https://docs.rs/actix-web-codegen" authors = ["Nikolay Kim "] license = "MIT/Apache-2.0" edition = "2018" -workspace = ".." [lib] proc-macro = true diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index c482a6b36..45eb82c2c 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -1,4 +1,4 @@ -# Macros for actix-web framework [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web-codegen)](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) +# Helper and convenience macros for Actix-web. [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web-codegen)](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) ## Documentation & Resources diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 2a49b4714..b6df3f0dd 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -1,11 +1,12 @@ #![recursion_limit = "512"] -//! Actix-web codegen module + +//! Helper and convenience macros for Actix-web. //! -//! Generators for routes and scopes +//! ## Runtime Setup //! -//! ## Route +//! - [main](attr.main.html) //! -//! Macros: +//! ## Resource Macros: //! //! - [get](attr.get.html) //! - [post](attr.post.html) @@ -23,12 +24,12 @@ //! - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard` //! - `wrap="Middleware"` - Registers a resource middleware. //! -//! ## Notes +//! ### Notes //! //! Function name can be specified as any expression that is going to be accessible to the generate //! code (e.g `my_guard` or `my_module::my_guard`) //! -//! ## Example: +//! ### Example: //! //! ```rust //! use actix_web::HttpResponse; @@ -139,3 +140,43 @@ pub fn trace(args: TokenStream, input: TokenStream) -> TokenStream { pub fn patch(args: TokenStream, input: TokenStream) -> TokenStream { route::generate(args, input, route::GuardType::Patch) } + +/// Marks async main function as the actix system entry-point. +/// +/// ## Usage +/// +/// ```rust +/// #[actix_web::main] +/// async fn main() { +/// async { println!("Hello world"); }.await +/// } +/// ``` +#[proc_macro_attribute] +#[cfg(not(test))] // Work around for rust-lang/rust#62127 +pub fn main(_: TokenStream, item: TokenStream) -> TokenStream { + use quote::quote; + + let mut input = syn::parse_macro_input!(item as syn::ItemFn); + let attrs = &input.attrs; + let vis = &input.vis; + let sig = &mut input.sig; + let body = &input.block; + let name = &sig.ident; + + if sig.asyncness.is_none() { + return syn::Error::new_spanned(sig.fn_token, "only async fn is supported") + .to_compile_error() + .into(); + } + + sig.asyncness = None; + + (quote! { + #(#attrs)* + #vis #sig { + actix_web::rt::System::new(stringify!(#name)) + .block_on(async move { #body }) + } + }) + .into() +} diff --git a/rust-toolchain b/rust-toolchain new file mode 100644 index 000000000..32b7211cb --- /dev/null +++ b/rust-toolchain @@ -0,0 +1 @@ +1.40.0 diff --git a/src/app.rs b/src/app.rs index 8178d57fe..ae3d9fdf0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -42,6 +42,7 @@ pub struct App { 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)); App { diff --git a/src/info.rs b/src/info.rs index 5b506d85a..1d9b402a7 100644 --- a/src/info.rs +++ b/src/info.rs @@ -25,7 +25,7 @@ impl ConnectionInfo { Ref::map(req.extensions(), |e| e.get().unwrap()) } - #[allow(clippy::cognitive_complexity)] + #[allow(clippy::cognitive_complexity, clippy::borrow_interior_mutable_const)] fn new(req: &RequestHead, cfg: &AppConfig) -> ConnectionInfo { let mut host = None; let mut scheme = None; diff --git a/src/lib.rs b/src/lib.rs index 09642806f..844f952cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,18 +1,11 @@ #![warn(rust_2018_idioms, warnings)] -#![allow( - clippy::needless_doctest_main, - clippy::type_complexity, - clippy::borrow_interior_mutable_const -)] +#![allow(clippy::needless_doctest_main, clippy::type_complexity)] + //! Actix web is a small, pragmatic, and extremely fast web framework //! for Rust. //! //! ## Example //! -//! The `#[actix_rt::main]` macro in the example below is provided by the Actix runtime -//! crate, [`actix-rt`](https://crates.io/crates/actix-rt). You will need to include -//! `actix-rt` in your dependencies for it to run. -//! //! ```rust,no_run //! use actix_web::{web, App, Responder, HttpServer}; //! @@ -20,7 +13,7 @@ //! format!("Hello {}! id:{}", info.0, info.1) //! } //! -//! #[actix_rt::main] +//! #[actix_web::main] //! async fn main() -> std::io::Result<()> { //! HttpServer::new(|| App::new().service( //! web::resource("/{name}/{id}/index.html").to(index)) @@ -80,9 +73,7 @@ //! * `compress` - enables content encoding compression support (default enabled) //! * `openssl` - enables ssl support via `openssl` crate, supports `http/2` //! * `rustls` - enables ssl support via `rustls` crate, supports `http/2` -//! * `secure-cookies` - enables secure cookies support, includes `ring` crate as -//! dependency -#![allow(clippy::type_complexity, clippy::new_without_default)] +//! * `secure-cookies` - enables secure cookies support mod app; mod app_service; @@ -106,13 +97,12 @@ pub mod test; mod types; pub mod web; -#[doc(hidden)] pub use actix_web_codegen::*; +pub use actix_rt as rt; // re-export for convenience pub use actix_http::Response as HttpResponse; pub use actix_http::{body, cookie, http, Error, HttpMessage, ResponseError, Result}; -pub use actix_macros::{main, test as test_rt}; pub use crate::app::App; pub use crate::extract::FromRequest; @@ -230,6 +220,7 @@ pub mod client { //! println!("Response: {:?}", response); //! } //! ``` + pub use awc::error::{ ConnectError, InvalidUrl, PayloadError, SendRequestError, WsClientError, }; diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 6de451c84..fe3ba841c 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -90,6 +90,7 @@ where self.service.poll_ready(cx) } + #[allow(clippy::borrow_interior_mutable_const)] fn call(&mut self, req: ServiceRequest) -> Self::Future { // negotiate content-encoding let encoding = if let Some(val) = req.headers().get(&ACCEPT_ENCODING) { diff --git a/src/middleware/defaultheaders.rs b/src/middleware/defaultheaders.rs index ef2e56e69..6d43aba95 100644 --- a/src/middleware/defaultheaders.rs +++ b/src/middleware/defaultheaders.rs @@ -128,6 +128,7 @@ where self.service.poll_ready(cx) } + #[allow(clippy::borrow_interior_mutable_const)] fn call(&mut self, req: ServiceRequest) -> Self::Future { let inner = self.inner.clone(); let fut = self.service.call(req); diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index 8b881c0a4..57b640bdd 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -478,7 +478,7 @@ impl FormatText { } FormatText::RemoteAddr => { let s = if let Some(ref peer) = req.connection_info().remote_addr() { - FormatText::Str(peer.to_string()) + FormatText::Str((*peer).to_string()) } else { FormatText::Str("-".to_string()) }; diff --git a/src/route.rs b/src/route.rs index 2763f3b1a..b17fa9b06 100644 --- a/src/route.rs +++ b/src/route.rs @@ -46,6 +46,7 @@ pub struct Route { impl Route { /// Create new route which matches any request. + #[allow(clippy::new_without_default)] pub fn new() -> Route { Route { service: Box::new(RouteNewService::new(Extract::new(Handler::new(|| { diff --git a/src/types/form.rs b/src/types/form.rs index ca1a4b103..c10ed4649 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -252,6 +252,7 @@ pub struct UrlEncoded { fut: Option>>, } +#[allow(clippy::borrow_interior_mutable_const)] impl UrlEncoded { /// Create a new future to URL encode a request pub fn new(req: &HttpRequest, payload: &mut Payload) -> UrlEncoded { diff --git a/src/types/json.rs b/src/types/json.rs index f746fd432..6de9e0d86 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -319,6 +319,7 @@ where U: DeserializeOwned + 'static, { /// Create `JsonBody` for request. + #[allow(clippy::borrow_interior_mutable_const)] pub fn new( req: &HttpRequest, payload: &mut Payload, diff --git a/src/types/payload.rs b/src/types/payload.rs index bad33bfc6..0efdc2c09 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -315,6 +315,7 @@ pub struct HttpMessageBody { impl HttpMessageBody { /// Create `MessageBody` for request. + #[allow(clippy::borrow_interior_mutable_const)] pub fn new(req: &HttpRequest, payload: &mut dev::Payload) -> HttpMessageBody { let mut len = None; if let Some(l) = req.headers().get(&header::CONTENT_LENGTH) { diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index f90cef0dd..6265cd415 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -14,7 +14,6 @@ categories = ["network-programming", "asynchronous", license = "MIT/Apache-2.0" exclude = [".gitignore", ".cargo/config"] edition = "2018" -workspace = ".." [package.metadata.docs.rs] features = [] From fa28175a740dc589eeb52d730d18aae396e7b5bf Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 23 Jun 2020 00:58:20 +0100 Subject: [PATCH 09/78] add method to extract matched resource pattern (#1566) --- CHANGES.md | 2 + src/request.rs | 44 +++++++++++++++++- src/rmap.rs | 119 ++++++++++++++++++++++++++++++++++++++++++++++++- src/service.rs | 6 +++ 4 files changed, 168 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 58cddf78d..22a389d10 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,8 @@ ### Added * Re-export `actix_rt::main` as `actix_web::main`. +* `HttpRequest::match_pattern` and `ServiceRequest::match_pattern` for extracting the matched + resource pattern. ### Changed diff --git a/src/request.rs b/src/request.rs index f8abeb1bb..8ca897442 100644 --- a/src/request.rs +++ b/src/request.rs @@ -126,6 +126,17 @@ impl HttpRequest { &mut Rc::get_mut(&mut self.0).unwrap().path } + /// The resource definition pattern that matched the path. Useful for logging and metrics. + /// + /// For example, when a resource with pattern `/user/{id}/profile` is defined and a call is made + /// to `/user/123/profile` this function would return `Some("/user/{id}/profile")`. + /// + /// Returns a None when no resource is fully matched, including default services. + #[inline] + pub fn match_pattern(&self) -> Option { + self.0.rmap.match_pattern(self.path()) + } + /// Request extensions #[inline] pub fn extensions(&self) -> Ref<'_, Extensions> { @@ -141,7 +152,6 @@ impl HttpRequest { /// Generate url for named resource /// /// ```rust - /// # extern crate actix_web; /// # use actix_web::{web, App, HttpRequest, HttpResponse}; /// # /// fn index(req: HttpRequest) -> HttpResponse { @@ -599,4 +609,36 @@ mod tests { assert!(tracker.borrow().dropped); } + + #[actix_rt::test] + async fn extract_path_pattern() { + let mut srv = init_service( + App::new().service( + web::scope("/user/{id}") + .service(web::resource("/profile").route(web::get().to( + move |req: HttpRequest| { + assert_eq!( + req.match_pattern(), + Some("/user/{id}/profile".to_owned()) + ); + + HttpResponse::Ok().finish() + }, + ))) + .default_service(web::to(move |req: HttpRequest| { + assert!(req.match_pattern().is_none()); + HttpResponse::Ok().finish() + })), + ), + ) + .await; + + let req = TestRequest::get().uri("/user/22/profile").to_request(); + let res = call_service(&mut srv, req).await; + assert_eq!(res.status(), StatusCode::OK); + + let req = TestRequest::get().uri("/user/22/not-exist").to_request(); + let res = call_service(&mut srv, req).await; + assert_eq!(res.status(), StatusCode::OK); + } } diff --git a/src/rmap.rs b/src/rmap.rs index 47092608c..0a0c96777 100644 --- a/src/rmap.rs +++ b/src/rmap.rs @@ -43,9 +43,7 @@ impl ResourceMap { } } } -} -impl ResourceMap { /// Generate url for named resource /// /// Check [`HttpRequest::url_for()`](../struct.HttpRequest.html#method. @@ -95,6 +93,45 @@ impl ResourceMap { false } + /// 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)) + } + + /// 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.is_prefix_match(remaining) { + let prefix = pattern.pattern().to_owned(); + + return [ + prefix, + rmap.traverse_resource_pattern(&remaining[prefix_len..]), + ] + .concat(); + } + } else if pattern.is_match(remaining) { + return pattern.pattern().to_owned(); + } + } + + String::new() + } + fn patterns_for( &self, name: &str, @@ -188,3 +225,81 @@ impl ResourceMap { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn extract_matched_pattern() { + let mut root = ResourceMap::new(ResourceDef::root_prefix("")); + + let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); + user_map.add(&mut ResourceDef::new("/"), None); + user_map.add(&mut ResourceDef::new("/profile"), None); + user_map.add(&mut ResourceDef::new("/article/{id}"), None); + user_map.add(&mut ResourceDef::new("/post/{post_id}"), None); + user_map.add( + &mut ResourceDef::new("/post/{post_id}/comment/{comment_id}"), + None, + ); + + root.add(&mut ResourceDef::new("/info"), None); + root.add(&mut ResourceDef::new("/v{version:[[:digit:]]{1}}"), None); + root.add( + &mut ResourceDef::root_prefix("/user/{id}"), + Some(Rc::new(user_map)), + ); + + let root = Rc::new(root); + root.finish(Rc::clone(&root)); + + // sanity check resource map setup + + assert!(root.has_resource("/info")); + assert!(!root.has_resource("/bar")); + + assert!(root.has_resource("/v1")); + 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/profile")); + + // extract patterns from paths + + assert!(root.match_pattern("/bar").is_none()); + assert!(root.match_pattern("/v44").is_none()); + + assert_eq!(root.match_pattern("/info"), Some("/info".to_owned())); + assert_eq!( + root.match_pattern("/v1"), + Some("/v{version:[[:digit:]]{1}}".to_owned()) + ); + assert_eq!( + root.match_pattern("/v2"), + Some("/v{version:[[:digit:]]{1}}".to_owned()) + ); + assert_eq!( + root.match_pattern("/user/22/profile"), + Some("/user/{id}/profile".to_owned()) + ); + assert_eq!( + root.match_pattern("/user/602CFB82-7709-4B17-ADCF-4C347B6F2203/profile"), + Some("/user/{id}/profile".to_owned()) + ); + assert_eq!( + root.match_pattern("/user/22/article/44"), + Some("/user/{id}/article/{id}".to_owned()) + ); + assert_eq!( + root.match_pattern("/user/22/post/my-post"), + Some("/user/{id}/post/{post_id}".to_owned()) + ); + assert_eq!( + root.match_pattern("/user/22/post/other-post/comment/42"), + Some("/user/{id}/post/{post_id}/comment/{comment_id}".to_owned()) + ); + } +} diff --git a/src/service.rs b/src/service.rs index 232a2f132..f7e201779 100644 --- a/src/service.rs +++ b/src/service.rs @@ -195,6 +195,12 @@ impl ServiceRequest { pub fn match_info(&self) -> &Path { self.0.match_info() } + + /// Counterpart to [`HttpRequest::match_pattern`](../struct.HttpRequest.html#method.match_pattern). + #[inline] + pub fn match_pattern(&self) -> Option { + self.0.match_pattern() + } #[inline] /// Get a mutable reference to the Path parameters. From 487f90be5bc8f520a726c6e4bf4e18dc034dd2eb Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 23 Jun 2020 06:26:07 +0100 Subject: [PATCH 10/78] move pull request template (#1578) --- .../bug_or_feature.md => PULL_REQUEST_TEMPLATE.md} | 5 ----- 1 file changed, 5 deletions(-) rename .github/{PULL_REQUEST_TEMPLATE/bug_or_feature.md => PULL_REQUEST_TEMPLATE.md} (91%) diff --git a/.github/PULL_REQUEST_TEMPLATE/bug_or_feature.md b/.github/PULL_REQUEST_TEMPLATE.md similarity index 91% rename from .github/PULL_REQUEST_TEMPLATE/bug_or_feature.md rename to .github/PULL_REQUEST_TEMPLATE.md index a1fa98ea8..6426eab65 100644 --- a/.github/PULL_REQUEST_TEMPLATE/bug_or_feature.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,8 +1,3 @@ ---- -name: Bug / Feature -about: Submit a bug fix or feature. ---- - ## PR Type What kind of change does this PR make? From 23c8191cca7264076d58816923b6f25eb89e6107 Mon Sep 17 00:00:00 2001 From: Takashi Idobe Date: Sat, 27 Jun 2020 11:22:16 -0400 Subject: [PATCH 11/78] add method to extract matched resource name (#1577) Co-authored-by: Rob Ede --- CHANGES.md | 1 + src/request.rs | 26 ++++++++++++++++++++ src/rmap.rs | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/service.rs | 8 ++++++- 4 files changed, 99 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 22a389d10..f4c04cd05 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ * Re-export `actix_rt::main` as `actix_web::main`. * `HttpRequest::match_pattern` and `ServiceRequest::match_pattern` for extracting the matched resource pattern. +* `HttpRequest::match_name` and `ServiceRequest::match_name` for extracting matched resource name. ### Changed diff --git a/src/request.rs b/src/request.rs index 8ca897442..85f409016 100644 --- a/src/request.rs +++ b/src/request.rs @@ -137,6 +137,14 @@ impl HttpRequest { self.0.rmap.match_pattern(self.path()) } + /// The resource name that matched the path. Useful for logging and metrics. + /// + /// Returns a None when no resource is fully matched, including default services. + #[inline] + pub fn match_name(&self) -> Option<&str> { + self.0.rmap.match_name(self.path()) + } + /// Request extensions #[inline] pub fn extensions(&self) -> Ref<'_, Extensions> { @@ -462,6 +470,24 @@ mod tests { ); } + #[test] + fn test_match_name() { + let mut rdef = ResourceDef::new("/index.html"); + *rdef.name_mut() = "index".to_string(); + + let mut rmap = ResourceMap::new(ResourceDef::new("")); + rmap.add(&mut rdef, None); + + assert!(rmap.has_resource("/index.html")); + + let req = TestRequest::default() + .uri("/index.html") + .rmap(rmap) + .to_http_request(); + + assert_eq!(req.match_name(), Some("index")); + } + #[test] fn test_url_for_external() { let mut rdef = ResourceDef::new("https://youtube.com/watch/{video_id}"); diff --git a/src/rmap.rs b/src/rmap.rs index 0a0c96777..5e79830ec 100644 --- a/src/rmap.rs +++ b/src/rmap.rs @@ -93,6 +93,27 @@ impl ResourceMap { false } + /// Returns the name of the route that matches the given path or None if no full match + /// is possible. + 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.is_prefix_match(path) { + return rmap.match_name(&path[plen..]); + } + } else if pattern.is_match(path) { + return match pattern.name() { + "" => None, + s => Some(s), + }; + } + } + + None + } + /// 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 { @@ -302,4 +323,48 @@ mod tests { Some("/user/{id}/post/{post_id}/comment/{comment_id}".to_owned()) ); } + + #[test] + fn extract_matched_name() { + let mut root = ResourceMap::new(ResourceDef::root_prefix("")); + + let mut rdef = ResourceDef::new("/info"); + *rdef.name_mut() = "root_info".to_owned(); + root.add(&mut rdef, None); + + let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); + let mut rdef = ResourceDef::new("/"); + user_map.add(&mut rdef, None); + + let mut rdef = ResourceDef::new("/post/{post_id}"); + *rdef.name_mut() = "user_post".to_owned(); + user_map.add(&mut rdef, None); + + root.add( + &mut ResourceDef::root_prefix("/user/{id}"), + Some(Rc::new(user_map)), + ); + + let root = Rc::new(root); + root.finish(Rc::clone(&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/post/55")); + + // extract patterns from paths + + assert!(root.match_name("/bar").is_none()); + assert!(root.match_name("/v44").is_none()); + + assert_eq!(root.match_name("/info"), Some("root_info")); + assert_eq!(root.match_name("/user/22"), None); + assert_eq!(root.match_name("/user/22/"), None); + assert_eq!(root.match_name("/user/22/post/55"), Some("user_post")); + } } diff --git a/src/service.rs b/src/service.rs index f7e201779..cba852a9c 100644 --- a/src/service.rs +++ b/src/service.rs @@ -195,7 +195,13 @@ impl ServiceRequest { pub fn match_info(&self) -> &Path { self.0.match_info() } - + + /// Counterpart to [`HttpRequest::match_name`](../struct.HttpRequest.html#method.match_name). + #[inline] + pub fn match_name(&self) -> Option<&str> { + self.0.match_name() + } + /// Counterpart to [`HttpRequest::match_pattern`](../struct.HttpRequest.html#method.match_pattern). #[inline] pub fn match_pattern(&self) -> Option { From f2d641b772cc0d61384f9dcbf91ec132065a0f24 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Thu, 2 Jul 2020 17:52:42 +0900 Subject: [PATCH 12/78] Update `v_htmlescape` to 0.10 --- actix-files/CHANGES.md | 4 ++++ actix-files/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index abf143997..5d1845e37 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,5 +1,9 @@ # Changes +## [unreleased] - xxx + +* Update `v_htmlescape` to 0.10 + ## [0.3.0-alpha.1] - 2020-05-23 * Update `actix-web` and `actix-http` dependencies to alpha diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 76e528b12..12fd03ce1 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -29,7 +29,7 @@ log = "0.4" mime = "0.3" mime_guess = "2.0.1" percent-encoding = "2.1" -v_htmlescape = "0.4" +v_htmlescape = "0.10" [dev-dependencies] actix-rt = "1.0.0" From deab634247356b79e10ce6ef537377214fb0cd75 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Fri, 3 Jul 2020 09:08:24 +0900 Subject: [PATCH 13/78] actix-http: Update `sha-1` to 0.9 (#1586) --- .github/workflows/linux.yml | 2 +- CHANGES.md | 1 + README.md | 2 +- actix-http/CHANGES.md | 2 ++ actix-http/Cargo.toml | 2 +- actix-http/src/ws/proto.rs | 6 +++--- rust-toolchain | 2 +- test-server/Cargo.toml | 2 -- 8 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index ae804cc53..a2dfc5da6 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -8,7 +8,7 @@ jobs: fail-fast: false matrix: version: - - 1.40.0 # MSRV + - 1.41.1 # MSRV - stable - nightly diff --git a/CHANGES.md b/CHANGES.md index f4c04cd05..8f15eea15 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,7 @@ * Fix actix_http::h1::dispatcher so it returns when HW_BUFFER_SIZE is reached. Should reduce peak memory consumption during large uploads. [#1550] * Migrate cookie handling to `cookie` crate. Actix-web no longer requires `ring` dependency. +* MSRV is now 1.41.1 ### Fixed diff --git a/README.md b/README.md index ade632877..a9b5e0116 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![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) [![Documentation](https://docs.rs/actix-web/badge.svg)](https://docs.rs/actix-web) [![Download](https://img.shields.io/crates/d/actix-web.svg)](https://crates.io/crates/actix-web) -[![Version](https://img.shields.io/badge/rustc-1.40+-lightgray.svg)](https://blog.rust-lang.org/2019/12/19/Rust-1.40.0.html) +[![Version](https://img.shields.io/badge/rustc-1.41+-lightgray.svg)](https://blog.rust-lang.org/2020/02/27/Rust-1.41.1.html) ![License](https://img.shields.io/crates/l/actix-web.svg)

diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 49599b9be..d18af162d 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -5,6 +5,8 @@ ### Changed * Migrate cookie handling to `cookie` crate. +* Update `sha-1` to 0.9 +* MSRV is now 1.41.1 ## [2.0.0-alpha.4] - 2020-05-21 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 8b9b8c825..6095f89ea 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -76,7 +76,7 @@ rand = "0.7" regex = "1.3" serde = "1.0" serde_json = "1.0" -sha-1 = "0.8" +sha-1 = "0.9" slab = "0.4" serde_urlencoded = "0.6.1" time = { version = "0.2.7", default-features = false, features = ["std"] } diff --git a/actix-http/src/ws/proto.rs b/actix-http/src/ws/proto.rs index dd3078a6c..fc271a8f5 100644 --- a/actix-http/src/ws/proto.rs +++ b/actix-http/src/ws/proto.rs @@ -208,10 +208,10 @@ pub fn hash_key(key: &[u8]) -> String { use sha1::Digest; let mut hasher = sha1::Sha1::new(); - hasher.input(key); - hasher.input(WS_GUID.as_bytes()); + hasher.update(key); + hasher.update(WS_GUID.as_bytes()); - base64::encode(hasher.result().as_ref()) + base64::encode(&hasher.finalize()) } #[cfg(test)] diff --git a/rust-toolchain b/rust-toolchain index 32b7211cb..f86fb9cbc 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -1.40.0 +1.41.1 diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index 6265cd415..2c526e672 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -43,11 +43,9 @@ bytes = "0.5.3" futures-core = { version = "0.3.5", default-features = false } http = "0.2.0" log = "0.4" -env_logger = "0.7" socket2 = "0.3" serde = "1.0" serde_json = "1.0" -sha1 = "0.6" slab = "0.4" serde_urlencoded = "0.6.1" time = { version = "0.2.7", default-features = false, features = ["std"] } From 056803d534234b15698a5a34280d340f9446bfe9 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sun, 5 Jul 2020 01:16:53 +0100 Subject: [PATCH 14/78] revamp readme and root doc page (#1590) --- README.md | 90 ++++++++++++++++++++++++++++-------------------------- src/lib.rs | 90 +++++++++++++++++++++++++----------------------------- 2 files changed, 87 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index a9b5e0116..4d6bac29c 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,58 @@
-

Actix web

-

Actix web is a small, pragmatic, and extremely fast rust web framework

+

Actix web

+

+ Actix web is a powerful, pragmatic, and extremely fast web framework for Rust +

-[![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) -[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) -[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Documentation](https://docs.rs/actix-web/badge.svg)](https://docs.rs/actix-web) -[![Download](https://img.shields.io/crates/d/actix-web.svg)](https://crates.io/crates/actix-web) [![Version](https://img.shields.io/badge/rustc-1.41+-lightgray.svg)](https://blog.rust-lang.org/2020/02/27/Rust-1.41.1.html) ![License](https://img.shields.io/crates/l/actix-web.svg) +
+[![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) +[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) +[![Download](https://img.shields.io/crates/d/actix-web.svg)](https://crates.io/crates/actix-web) +[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

- -

- Website - | - Chat - | - Examples -

-
-Actix web is a simple, pragmatic and extremely fast web framework for Rust. +## Features -* Supported *HTTP/1.x* and *HTTP/2.0* protocols +* Supports *HTTP/1.x* and *HTTP/2* * Streaming and pipelining * Keep-alive and slow requests handling * Client/server [WebSockets](https://actix.rs/docs/websockets/) support * Transparent content compression/decompression (br, gzip, deflate) -* Configurable [request routing](https://actix.rs/docs/url-dispatch/) +* Powerful [request routing](https://actix.rs/docs/url-dispatch/) * Multipart streams * Static assets -* SSL support with OpenSSL or Rustls +* SSL support using OpenSSL or Rustls * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) -* Includes an asynchronous [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html) +* Includes an async [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html) * Supports [Actix actor framework](https://github.com/actix/actix) -* Supports Rust 1.40+ +* Runs on stable Rust 1.41+ -## Docs +## Documentation -* [API documentation (master)](https://actix.rs/actix-web/actix_web) -* [API documentation (docs.rs)](https://docs.rs/actix-web) -* [User guide](https://actix.rs) +* [Website & User Guide](https://actix.rs) +* [Examples Repository](https://actix.rs/actix-web/actix_web) +* [API Documentation](https://docs.rs/actix-web) +* [API Documentation (master branch)](https://actix.rs/actix-web/actix_web) ## Example +

+ WARNING: This example is for the master branch which is currently in beta stages for v3. For + Actix web v2 see the getting started guide. +

+ Dependencies: ```toml [dependencies] -actix-web = "2" +actix-web = "3" ``` Code: @@ -76,37 +76,39 @@ async fn main() -> std::io::Result<()> { ### More examples -* [Basics](https://github.com/actix/examples/tree/master/basics/) -* [Stateful](https://github.com/actix/examples/tree/master/state/) -* [Multipart streams](https://github.com/actix/examples/tree/master/multipart/) -* [Simple websocket](https://github.com/actix/examples/tree/master/websocket/) -* [Tera](https://github.com/actix/examples/tree/master/template_tera/) -* [Askama](https://github.com/actix/examples/tree/master/template_askama/) templates -* [Diesel integration](https://github.com/actix/examples/tree/master/diesel/) -* [r2d2](https://github.com/actix/examples/tree/master/r2d2/) -* [OpenSSL](https://github.com/actix/examples/tree/master/openssl/) -* [Rustls](https://github.com/actix/examples/tree/master/rustls/) -* [Tcp/Websocket chat](https://github.com/actix/examples/tree/master/websocket-chat/) -* [Json](https://github.com/actix/examples/tree/master/json/) +* [Basic Setup](https://github.com/actix/examples/tree/master/basics/) +* [Application State](https://github.com/actix/examples/tree/master/state/) +* [JSON Handling](https://github.com/actix/examples/tree/master/json/) +* [Multipart Streams](https://github.com/actix/examples/tree/master/multipart/) +* [Diesel Integration](https://github.com/actix/examples/tree/master/diesel/) +* [r2d2 Integration](https://github.com/actix/examples/tree/master/r2d2/) +* [Simple WebSocket](https://github.com/actix/examples/tree/master/websocket/) +* [Tera Templates](https://github.com/actix/examples/tree/master/template_tera/) +* [Askama Templates](https://github.com/actix/examples/tree/master/template_askama/) +* [HTTPS using Rustls](https://github.com/actix/examples/tree/master/rustls/) +* [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/openssl/) +* [WebSocket Chat](https://github.com/actix/examples/tree/master/websocket-chat/) You may consider checking out [this directory](https://github.com/actix/examples/tree/master/) for more examples. ## Benchmarks -* [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r19) +One of the fastest web frameworks available according to the +[TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r19). ## License This project is licensed under either of -* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)) -* MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT)) +* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or + [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)) +* MIT license ([LICENSE-MIT](LICENSE-MIT) or + [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT)) at your option. ## Code of Conduct -Contribution to the actix-web crate is organized under the terms of the -Contributor Covenant, the maintainer of actix-web, @fafhrd91, promises to -intervene to uphold that code of conduct. +Contribution to the actix-web crate is organized under the terms of the Contributor Covenant, the +maintainers of Actix web, promises to intervene to uphold that code of conduct. diff --git a/src/lib.rs b/src/lib.rs index 844f952cc..eb46af664 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,79 +1,72 @@ #![warn(rust_2018_idioms, warnings)] #![allow(clippy::needless_doctest_main, clippy::type_complexity)] -//! Actix web is a small, pragmatic, and extremely fast web framework -//! for Rust. +//! Actix web is a powerful, pragmatic, and extremely fast web framework for Rust. //! //! ## Example //! //! ```rust,no_run -//! use actix_web::{web, App, Responder, HttpServer}; +//! use actix_web::{get, web, App, HttpServer, Responder}; //! -//! async fn index(info: web::Path<(String, u32)>) -> impl Responder { -//! format!("Hello {}! id:{}", info.0, info.1) +//! #[get("/{id}/{name}/index.html")] +//! async fn index(info: web::Path<(u32, String)>) -> impl Responder { +//! format!("Hello {}! id:{}", info.1, info.0) //! } //! //! #[actix_web::main] //! async fn main() -> std::io::Result<()> { -//! HttpServer::new(|| App::new().service( -//! web::resource("/{name}/{id}/index.html").to(index)) -//! ) +//! HttpServer::new(|| App::new().service(index)) //! .bind("127.0.0.1:8080")? //! .run() //! .await //! } //! ``` //! -//! ## Documentation & community resources +//! ## Documentation & Community Resources //! -//! Besides the API documentation (which you are currently looking -//! at!), several other resources are available: +//! In addition to this API documentation, several other resources are available: //! -//! * [User Guide](https://actix.rs/docs/) -//! * [Chat on gitter](https://gitter.im/actix/actix) -//! * [GitHub repository](https://github.com/actix/actix-web) -//! * [Cargo package](https://crates.io/crates/actix-web) +//! * [Website & User Guide](https://actix.rs/) +//! * [Examples Repository](https://github.com/actix/examples) +//! * [Community Chat on Gitter](https://gitter.im/actix/actix-web) //! -//! To get started navigating the API documentation you may want to -//! consider looking at the following pages: +//! To get started navigating the API docs, you may consider looking at the following pages first: //! -//! * [App](struct.App.html): This struct represents an actix-web -//! application and is used to configure routes and other common -//! settings. +//! * [App](struct.App.html): This struct represents an Actix web application and is used to +//! configure routes and other common application settings. //! -//! * [HttpServer](struct.HttpServer.html): This struct -//! represents an HTTP server instance and is used to instantiate and -//! configure servers. +//! * [HttpServer](struct.HttpServer.html): This struct represents an HTTP server instance and is +//! used to instantiate and configure servers. //! -//! * [web](web/index.html): This module -//! provides essential helper functions and types for application registration. +//! * [web](web/index.html): This module provides essential types for route registration as well as +//! common utilities for request handlers. //! -//! * [HttpRequest](struct.HttpRequest.html) and -//! [HttpResponse](struct.HttpResponse.html): These structs -//! represent HTTP requests and responses and expose various methods -//! for inspecting, creating and otherwise utilizing them. +//! * [HttpRequest](struct.HttpRequest.html) and [HttpResponse](struct.HttpResponse.html): These +//! structs represent HTTP requests and responses and expose methods for creating, inspecting, +//! and otherwise utilizing them. //! //! ## Features //! -//! * Supported *HTTP/1.x* and *HTTP/2.0* protocols +//! * Supports *HTTP/1.x* and *HTTP/2* //! * Streaming and pipelining //! * Keep-alive and slow requests handling -//! * `WebSockets` server/client +//! * Client/server [WebSockets](https://actix.rs/docs/websockets/) support //! * Transparent content compression/decompression (br, gzip, deflate) -//! * Configurable request routing +//! * Powerful [request routing](https://actix.rs/docs/url-dispatch/) //! * Multipart streams -//! * SSL support with OpenSSL or `native-tls` -//! * Middlewares (`Logger`, `Session`, `CORS`, `DefaultHeaders`) +//! * Static assets +//! * SSL support using OpenSSL or Rustls +//! * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) +//! * Includes an async [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html) //! * Supports [Actix actor framework](https://github.com/actix/actix) -//! * Supported Rust version: 1.40 or later +//! * Runs on stable Rust 1.41+ //! -//! ## Package feature +//! ## Crate Features //! -//! * `client` - enables http client (default enabled) -//! * `compress` - enables content encoding compression support (default enabled) -//! * `openssl` - enables ssl support via `openssl` crate, supports `http/2` -//! * `rustls` - enables ssl support via `rustls` crate, supports `http/2` -//! * `secure-cookies` - enables secure cookies support +//! * `compress` - content encoding compression support (enabled by default) +//! * `openssl` - HTTPS support via `openssl` crate, supports `HTTP/2` +//! * `rustls` - HTTPS support via `rustls` crate, supports `HTTP/2` +//! * `secure-cookies` - secure cookies support mod app; mod app_service; @@ -97,12 +90,10 @@ pub mod test; mod types; pub mod web; -pub use actix_web_codegen::*; -pub use actix_rt as rt; - -// re-export for convenience pub use actix_http::Response as HttpResponse; pub use actix_http::{body, cookie, http, Error, HttpMessage, ResponseError, Result}; +pub use actix_rt as rt; +pub use actix_web_codegen::*; pub use crate::app::App; pub use crate::extract::FromRequest; @@ -203,19 +194,20 @@ pub mod dev { } pub mod client { - //! An HTTP Client + //! Actix web async HTTP client. //! //! ```rust //! use actix_web::client::Client; //! - //! #[actix_rt::main] + //! #[actix_web::main] //! async fn main() { //! let mut client = Client::default(); //! //! // Create request builder and send request //! let response = client.get("http://www.rust-lang.org") - //! .header("User-Agent", "Actix-web") - //! .send().await; // <- Send http request + //! .header("User-Agent", "actix-web/3.0") + //! .send() // <- Send request + //! .await; // <- Wait for response //! //! println!("Response: {:?}", response); //! } From 08f9a340754fbf9b62bf88440e8fa46e32a8f8fb Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Tue, 7 Jul 2020 04:00:18 +0900 Subject: [PATCH 15/78] Use tarpaulin 0.13 to avoid failure --- .github/workflows/linux.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index a2dfc5da6..7529c8494 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -55,7 +55,7 @@ jobs: - name: Generate coverage file if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request') run: | - cargo install cargo-tarpaulin + cargo install cargo-tarpaulin --vers "^0.13" cargo tarpaulin --out Xml - name: Upload to Codecov if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request') From a2662b928bda6c79110d04b73db048ea530be5e7 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 10 Jul 2020 14:55:56 +0100 Subject: [PATCH 16/78] Update PULL_REQUEST_TEMPLATE.md --- .github/PULL_REQUEST_TEMPLATE.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6426eab65..cc34e2bc6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,12 +1,8 @@ ## PR Type What kind of change does this PR make? - - -- [ ] Bug fix -- [ ] Feature -- [ ] Refactor / code style change (no functional or public API changes) -- [ ] Other + +INSERT_PR_TYPE ## PR Checklist From e10eb648d94ad2b4edaef648847360be42d51b82 Mon Sep 17 00:00:00 2001 From: Patrick Tescher Date: Fri, 10 Jul 2020 14:35:22 -0700 Subject: [PATCH 17/78] Fix leaks with actix_http's client (#1580) --- actix-http/CHANGES.md | 1 + actix-http/src/client/pool.rs | 117 ++++++++++++++++++---------------- awc/src/request.rs | 28 ++++---- benches/server.rs | 7 +- 4 files changed, 81 insertions(+), 72 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index d18af162d..138210f95 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -7,6 +7,7 @@ * Migrate cookie handling to `cookie` crate. * Update `sha-1` to 0.9 * MSRV is now 1.41.1 +* Fix client leak [#1580] ## [2.0.0-alpha.4] - 2020-05-21 diff --git a/actix-http/src/client/pool.rs b/actix-http/src/client/pool.rs index 5a10725b0..3ce443794 100644 --- a/actix-http/src/client/pool.rs +++ b/actix-http/src/client/pool.rs @@ -2,7 +2,7 @@ use std::cell::RefCell; use std::collections::VecDeque; use std::future::Future; use std::pin::Pin; -use std::rc::Rc; +use std::rc::{Rc, Weak}; use std::task::{Context, Poll}; use std::time::{Duration, Instant}; @@ -53,16 +53,25 @@ where + 'static, { pub(crate) fn new(connector: T, config: ConnectorConfig) -> Self { + let connector_rc = Rc::new(RefCell::new(connector)); + let inner_rc = Rc::new(RefCell::new(Inner { + config, + acquired: 0, + waiters: Slab::new(), + waiters_queue: IndexSet::new(), + available: FxHashMap::default(), + waker: LocalWaker::new(), + })); + + // start support future + actix_rt::spawn(ConnectorPoolSupport { + connector: connector_rc.clone(), + inner: Rc::downgrade(&inner_rc), + }); + ConnectionPool( - Rc::new(RefCell::new(connector)), - Rc::new(RefCell::new(Inner { - config, - acquired: 0, - waiters: Slab::new(), - waiters_queue: IndexSet::new(), - available: FxHashMap::default(), - waker: LocalWaker::new(), - })), + connector_rc, + inner_rc, ) } } @@ -92,12 +101,6 @@ where } fn call(&mut self, req: Connect) -> Self::Future { - // start support future - actix_rt::spawn(ConnectorPoolSupport { - connector: self.0.clone(), - inner: self.1.clone(), - }); - let mut connector = self.0.clone(); let inner = self.1.clone(); @@ -421,7 +424,7 @@ where Io: AsyncRead + AsyncWrite + Unpin + 'static, { connector: T, - inner: Rc>>, + inner: Weak>>, } impl Future for ConnectorPoolSupport @@ -435,51 +438,55 @@ where fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); - let mut inner = this.inner.as_ref().borrow_mut(); - inner.waker.register(cx.waker()); + if let Some(this_inner) = this.inner.upgrade() { + let mut inner = this_inner.as_ref().borrow_mut(); + inner.waker.register(cx.waker()); - // check waiters - loop { - let (key, token) = { - if let Some((key, token)) = inner.waiters_queue.get_index(0) { - (key.clone(), *token) - } else { - break; + // check waiters + loop { + let (key, token) = { + if let Some((key, token)) = inner.waiters_queue.get_index(0) { + (key.clone(), *token) + } else { + break; + } + }; + if inner.waiters.get(token).unwrap().is_none() { + continue; } - }; - if inner.waiters.get(token).unwrap().is_none() { - continue; - } - match inner.acquire(&key, cx) { - Acquire::NotAvailable => break, - Acquire::Acquired(io, created) => { - let tx = inner.waiters.get_mut(token).unwrap().take().unwrap().1; - if let Err(conn) = tx.send(Ok(IoConnection::new( - io, - created, - Some(Acquired(key.clone(), Some(this.inner.clone()))), - ))) { - let (io, created) = conn.unwrap().into_inner(); - inner.release_conn(&key, io, created); + match inner.acquire(&key, cx) { + Acquire::NotAvailable => break, + Acquire::Acquired(io, created) => { + let tx = inner.waiters.get_mut(token).unwrap().take().unwrap().1; + if let Err(conn) = tx.send(Ok(IoConnection::new( + io, + created, + Some(Acquired(key.clone(), Some(this_inner.clone()))), + ))) { + let (io, created) = conn.unwrap().into_inner(); + inner.release_conn(&key, io, created); + } + } + Acquire::Available => { + let (connect, tx) = + inner.waiters.get_mut(token).unwrap().take().unwrap(); + OpenWaitingConnection::spawn( + key.clone(), + tx, + this_inner.clone(), + this.connector.call(connect), + inner.config.clone(), + ); } } - Acquire::Available => { - let (connect, tx) = - inner.waiters.get_mut(token).unwrap().take().unwrap(); - OpenWaitingConnection::spawn( - key.clone(), - tx, - this.inner.clone(), - this.connector.call(connect), - inner.config.clone(), - ); - } + let _ = inner.waiters_queue.swap_remove_index(0); } - let _ = inner.waiters_queue.swap_remove_index(0); - } - Poll::Pending + Poll::Pending + } else { + Poll::Ready(()) + } } } diff --git a/awc/src/request.rs b/awc/src/request.rs index 3dd8cb2ce..c34a8e221 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -586,16 +586,16 @@ mod tests { use super::*; use crate::Client; - #[test] - fn test_debug() { + #[actix_rt::test] + async fn test_debug() { let request = Client::new().get("/").header("x-test", "111"); let repr = format!("{:?}", request); assert!(repr.contains("ClientRequest")); assert!(repr.contains("x-test")); } - #[test] - fn test_basics() { + #[actix_rt::test] + async fn test_basics() { let mut req = Client::new() .put("/") .version(Version::HTTP_2) @@ -621,8 +621,8 @@ mod tests { let _ = req.send_body(""); } - #[test] - fn test_client_header() { + #[actix_rt::test] + async fn test_client_header() { let req = Client::build() .header(header::CONTENT_TYPE, "111") .finish() @@ -639,8 +639,8 @@ mod tests { ); } - #[test] - fn test_client_header_override() { + #[actix_rt::test] + async fn test_client_header_override() { let req = Client::build() .header(header::CONTENT_TYPE, "111") .finish() @@ -658,8 +658,8 @@ mod tests { ); } - #[test] - fn client_basic_auth() { + #[actix_rt::test] + async fn client_basic_auth() { let req = Client::new() .get("/") .basic_auth("username", Some("password")); @@ -685,8 +685,8 @@ mod tests { ); } - #[test] - fn client_bearer_auth() { + #[actix_rt::test] + async fn client_bearer_auth() { let req = Client::new().get("/").bearer_auth("someS3cr3tAutht0k3n"); assert_eq!( req.head @@ -699,8 +699,8 @@ mod tests { ); } - #[test] - fn client_query() { + #[actix_rt::test] + async fn client_query() { let req = Client::new() .get("/") .query(&[("key1", "val1"), ("key2", "val2")]) diff --git a/benches/server.rs b/benches/server.rs index 70531adf7..041d0fa57 100644 --- a/benches/server.rs +++ b/benches/server.rs @@ -27,15 +27,16 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ // benchmark sending all requests at the same time fn bench_async_burst(c: &mut Criterion) { + // We are using System here, since Runtime requires preinitialized tokio + // Maybe add to actix_rt docs + let mut rt = actix_rt::System::new("test"); + let srv = test::start(|| { App::new() .service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR)))) }); - // We are using System here, since Runtime requires preinitialized tokio - // Maybe add to actix_rt docs let url = srv.url("/"); - let mut rt = actix_rt::System::new("test"); c.bench_function("get_body_async_burst", move |b| { b.iter_custom(|iters| { From 327e472e440c98815c4705c2dcec0e1f9ffaac18 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 13 Jul 2020 15:35:30 +0100 Subject: [PATCH 18/78] prepare http-2.0.0-beta.1 release (#1596) --- actix-http/CHANGES.md | 16 +++++++++++----- actix-http/Cargo.toml | 4 ++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 138210f95..a185a9f81 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,13 +1,19 @@ # Changes -## [Unreleased] +## [Unreleased] - xxx + +## [2.0.0-beta.1] - 2020-07-11 ### Changed -* Migrate cookie handling to `cookie` crate. -* Update `sha-1` to 0.9 -* MSRV is now 1.41.1 -* Fix client leak [#1580] +* Migrate cookie handling to `cookie` crate. [#1558] +* Update `sha-1` to 0.9. [#1586] +* Fix leak in client pool. [#1580] +* MSRV is now 1.41.1. + +[#1558]: https://github.com/actix/actix-web/pull/1558 +[#1586]: https://github.com/actix/actix-web/pull/1586 +[#1580]: https://github.com/actix/actix-web/pull/1580 ## [2.0.0-alpha.4] - 2020-05-21 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 6095f89ea..a9bec59de 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "actix-http" -version = "2.0.0-alpha.4" +version = "2.0.0-beta.1" authors = ["Nikolay Kim "] -description = "Actix http primitives" +description = "Actix HTTP primitives" readme = "README.md" keywords = ["actix", "http", "framework", "async", "futures"] homepage = "https://actix.rs" From 78594a72bdd5705531e0974a65685e770f7832b9 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 14 Jul 2020 03:16:26 +0100 Subject: [PATCH 19/78] prepare awc v2.0.0-beta.1 release --- awc/CHANGES.md | 4 ++++ awc/Cargo.toml | 6 +++--- test-server/Cargo.toml | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 6d5a81b5e..19154d35d 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,5 +1,9 @@ # Changes +## [2.0.0-beta.1] - 2020-07-14 +### Changed +* Update `actix-http` dependency to 2.0.0-beta.1 + ## [2.0.0-alpha.2] - 2020-05-21 ### Changed diff --git a/awc/Cargo.toml b/awc/Cargo.toml index b36e735ca..e0f89c8dc 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "2.0.0-alpha.2" +version = "2.0.0-beta.1" authors = ["Nikolay Kim "] description = "Actix http client." readme = "README.md" @@ -36,7 +36,7 @@ compress = ["actix-http/compress"] [dependencies] actix-codec = "0.2.0" actix-service = "1.0.1" -actix-http = "2.0.0-alpha.4" +actix-http = "2.0.0-beta.1" actix-rt = "1.0.0" base64 = "0.12" @@ -56,7 +56,7 @@ rust-tls = { version = "0.17.0", package = "rustls", optional = true, features = [dev-dependencies] actix-connect = { version = "2.0.0-alpha.2", features = ["openssl"] } actix-web = { version = "3.0.0-alpha.3", features = ["openssl"] } -actix-http = { version = "2.0.0-alpha.4", features = ["openssl"] } +actix-http = { version = "2.0.0-beta.1", features = ["openssl"] } actix-http-test = { version = "2.0.0-alpha.1", features = ["openssl"] } actix-utils = "1.0.3" actix-server = "1.0.0" diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index 2c526e672..aeb40daee 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -2,7 +2,7 @@ name = "actix-http-test" version = "2.0.0-alpha.1" authors = ["Nikolay Kim "] -description = "Actix http test server" +description = "Actix HTTP test server" readme = "README.md" keywords = ["http", "web", "framework", "async", "futures"] homepage = "https://actix.rs" @@ -53,4 +53,4 @@ open-ssl = { version = "0.10", package = "openssl", optional = true } [dev-dependencies] actix-web = "3.0.0-alpha.3" -actix-http = "2.0.0-alpha.4" +actix-http = "2.0.0-beta.1" From 1382094c152d9d201513ae280934d58fc5158b40 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Tue, 14 Jul 2020 11:19:56 +0900 Subject: [PATCH 20/78] Avoid using deprecated `/` in license field --- Cargo.toml | 2 +- actix-files/Cargo.toml | 2 +- actix-http/Cargo.toml | 2 +- actix-multipart/Cargo.toml | 2 +- actix-web-actors/Cargo.toml | 2 +- actix-web-codegen/Cargo.toml | 2 +- awc/Cargo.toml | 2 +- test-server/Cargo.toml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8c4719b92..146c4004f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ documentation = "https://docs.rs/actix-web/" categories = ["network-programming", "asynchronous", "web-programming::http-server", "web-programming::websocket"] -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" edition = "2018" [package.metadata.docs.rs] diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 12fd03ce1..b7ba75960 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -9,7 +9,7 @@ homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" documentation = "https://docs.rs/actix-files/" categories = ["asynchronous", "web-programming::http-server"] -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" edition = "2018" [lib] diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index a9bec59de..232b5c3f0 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -11,7 +11,7 @@ documentation = "https://docs.rs/actix-http/" categories = ["network-programming", "asynchronous", "web-programming::http-server", "web-programming::websocket"] -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" edition = "2018" [package.metadata.docs.rs] diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index c5f315d75..f6d6a37a1 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -8,7 +8,7 @@ keywords = ["http", "web", "framework", "async", "futures"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" documentation = "https://docs.rs/actix-multipart/" -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" edition = "2018" [lib] diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 746aca2d5..7383b2032 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -8,7 +8,7 @@ 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/" -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" edition = "2018" [lib] diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 178eeeb7e..10c33ea74 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -7,7 +7,7 @@ homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web" documentation = "https://docs.rs/actix-web-codegen" authors = ["Nikolay Kim "] -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" edition = "2018" [lib] diff --git a/awc/Cargo.toml b/awc/Cargo.toml index b36e735ca..563c5cae5 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -11,7 +11,7 @@ documentation = "https://docs.rs/awc/" categories = ["network-programming", "asynchronous", "web-programming::http-client", "web-programming::websocket"] -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" edition = "2018" [lib] diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index 2c526e672..1cdf77031 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -11,7 +11,7 @@ documentation = "https://docs.rs/actix-http-test/" categories = ["network-programming", "asynchronous", "web-programming::http-server", "web-programming::websocket"] -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" exclude = [".gitignore", ".cargo/config"] edition = "2018" From ad7c6d26332395e4693a68da8c15de799b259ca5 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 15 Jul 2020 00:44:44 +0100 Subject: [PATCH 21/78] prepare actix-web v3.0.0-beta.1 release (#1600) --- CHANGES.md | 14 ++++---------- Cargo.toml | 6 +++--- actix-web-codegen/CHANGES.md | 11 ++++++----- actix-web-codegen/Cargo.toml | 4 ++-- 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8f15eea15..149ff8fcb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,38 +1,32 @@ # Changes -## [Unreleased] +## Unreleased - 2020-xx-xx + +## 3.0.0-beta.1 - 2020-07-13 ### Added - * Re-export `actix_rt::main` as `actix_web::main`. * `HttpRequest::match_pattern` and `ServiceRequest::match_pattern` for extracting the matched resource pattern. * `HttpRequest::match_name` and `ServiceRequest::match_name` for extracting matched resource name. ### Changed - * Fix actix_http::h1::dispatcher so it returns when HW_BUFFER_SIZE is reached. Should reduce peak memory consumption during large uploads. [#1550] * Migrate cookie handling to `cookie` crate. Actix-web no longer requires `ring` dependency. * MSRV is now 1.41.1 ### Fixed - * `NormalizePath` improved consistency when path needs slashes added _and_ removed. -## [3.0.0-alpha.3] - 2020-05-21 +## 3.0.0-alpha.3 - 2020-05-21 ### Added - * Add option to create `Data` from `Arc` [#1509] ### Changed - * Resources and Scopes can now access non-overridden data types set on App (or containing scopes) when setting their own data. [#1486] - * Fix audit issue logging by default peer address [#1485] - * Bump minimum supported Rust version to 1.40 - * Replace deprecated `net2` crate with `socket2` [#1485]: https://github.com/actix/actix-web/pull/1485 diff --git a/Cargo.toml b/Cargo.toml index 146c4004f..49de9fd96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "3.0.0-alpha.3" +version = "3.0.0-beta.1" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" @@ -76,9 +76,9 @@ actix-macros = "0.1.0" actix-threadpool = "0.3.1" actix-tls = "2.0.0-alpha.1" -actix-web-codegen = "0.2.2" +actix-web-codegen = "0.3.0-beta.1" actix-http = "2.0.0-alpha.4" -awc = { version = "2.0.0-alpha.2", default-features = false } +awc = { version = "2.0.0-beta.1", default-features = false } bytes = "0.5.3" derive_more = "0.99.2" diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index 8259aaac2..242c5f8de 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,18 +1,20 @@ # Changes -## [Unreleased] - XXXX-XX-XX +## Unreleased - 2020-xx-xx -* Add main entry-point macro that uses re-exported runtime. + +## 0.3.0-beta.1 - 2020-07-14 +* Add main entry-point macro that uses re-exported runtime. [#1559] + +[#1559]: https://github.com/actix/actix-web/pull/1559 ## [0.2.2] - 2020-05-23 - * Add resource middleware on actix-web-codegen [#1467] [#1467]: https://github.com/actix/actix-web/pull/1467 ## [0.2.1] - 2020-02-25 - * Add `#[allow(missing_docs)]` attribute to generated structs [#1368] * Allow the handler function to be named as `config` [#1290] @@ -26,7 +28,6 @@ ## [0.1.3] - 2019-10-14 * Bump up `syn` & `quote` to 1.0 - * Provide better error message ## [0.1.2] - 2019-06-04 diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 10c33ea74..070c09899 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-codegen" -version = "0.2.2" +version = "0.3.0-beta.1" description = "Actix web proc macros" readme = "README.md" homepage = "https://actix.rs" @@ -20,5 +20,5 @@ proc-macro2 = "1" [dev-dependencies] actix-rt = "1.0.0" -actix-web = "3.0.0-alpha.3" +actix-web = "3.0.0-beta.1" futures-util = { version = "0.3.5", default-features = false } From 2fd96c03e5a2d93671412252d864859e5de1b345 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 16 Jul 2020 11:38:57 +0100 Subject: [PATCH 22/78] prepare beta.1 release for multipart/files/actors (#1605) --- actix-files/CHANGES.md | 5 +++-- actix-files/Cargo.toml | 8 ++++---- actix-multipart/CHANGES.md | 12 +++++++----- actix-multipart/Cargo.toml | 6 +++--- actix-web-actors/CHANGES.md | 5 ++++- actix-web-actors/Cargo.toml | 6 +++--- 6 files changed, 24 insertions(+), 18 deletions(-) diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 5d1845e37..75d616ff9 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,11 +1,12 @@ # Changes -## [unreleased] - xxx +## [Unreleased] - 2020-xx-xx +## [0.3.0-beta.1] - 2020-07-15 * Update `v_htmlescape` to 0.10 +* Update `actix-web` and `actix-http` dependencies to beta.1 ## [0.3.0-alpha.1] - 2020-05-23 - * Update `actix-web` and `actix-http` dependencies to alpha * Fix some typos in the docs * Bump minimum supported Rust version to 1.40 diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index b7ba75960..379ba8f89 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.3.0-alpha.1" +version = "0.3.0-beta.1" authors = ["Nikolay Kim "] description = "Static files support for actix web." readme = "README.md" @@ -17,8 +17,8 @@ name = "actix_files" path = "src/lib.rs" [dependencies] -actix-web = { version = "3.0.0-alpha.3", default-features = false } -actix-http = "2.0.0-alpha.4" +actix-web = { version = "3.0.0-beta.1", default-features = false } +actix-http = "2.0.0-beta.1" actix-service = "1.0.1" bitflags = "1" bytes = "0.5.3" @@ -33,4 +33,4 @@ v_htmlescape = "0.10" [dev-dependencies] actix-rt = "1.0.0" -actix-web = { version = "3.0.0-alpha.3", features = ["openssl"] } +actix-web = { version = "3.0.0-beta.1", features = ["openssl"] } diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index df3cecf71..261836223 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,15 +1,17 @@ # Changes -## [0.3.0-alpha.1] - 2020-05-25 +## Unreleased - 2020-xx-xx + +## 0.3.0-beta.1 - 2020-07-15 +* Update `actix-web` to 3.0.0-beta.1 + + +## 0.3.0-alpha.1 - 2020-05-25 * Update `actix-web` to 3.0.0-alpha.3 - * Bump minimum supported Rust version to 1.40 - * Minimize `futures` dependencies - * Remove the unused `time` dependency - * Fix missing `std::error::Error` implement for `MultipartError`. ## [0.2.0] - 2019-12-20 diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index f6d6a37a1..7e81035cc 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-multipart" -version = "0.3.0-alpha.1" +version = "0.3.0-beta.1" authors = ["Nikolay Kim "] description = "Multipart support for actix web framework." readme = "README.md" @@ -16,7 +16,7 @@ name = "actix_multipart" path = "src/lib.rs" [dependencies] -actix-web = { version = "3.0.0-alpha.3", default-features = false } +actix-web = { version = "3.0.0-beta.1", default-features = false } actix-service = "1.0.1" actix-utils = "1.0.3" bytes = "0.5.3" @@ -29,4 +29,4 @@ twoway = "0.2" [dev-dependencies] actix-rt = "1.0.0" -actix-http = "2.0.0-alpha.4" +actix-http = "2.0.0-beta.1" diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index 8fd48f77c..868f17e2a 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -2,10 +2,13 @@ ## [Unreleased] - 2020-xx-xx + +## [3.0.0-beta.1] - 2020-xx-xx +* Update `actix-web` & `actix-http` dependencies to beta.1 * Bump minimum supported Rust version to 1.40 -## [3.0.0-alpha.1] - 2020-05-08 +## [3.0.0-alpha.1] - 2020-05-08 * Update the actix-web dependency to 3.0.0-alpha.1 * Update the actix dependency to 0.10.0-alpha.2 * Update the actix-http dependency to 2.0.0-alpha.3 diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 7383b2032..8de679da5 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-actors" -version = "3.0.0-alpha.1" +version = "3.0.0-beta.1" authors = ["Nikolay Kim "] description = "Actix actors support for actix web framework." readme = "README.md" @@ -17,8 +17,8 @@ path = "src/lib.rs" [dependencies] actix = "0.10.0-alpha.2" -actix-web = { version = "3.0.0-alpha.3", default-features = false } -actix-http = "2.0.0-alpha.4" +actix-web = { version = "3.0.0-beta.1", default-features = false } +actix-http = "2.0.0-beta.1" actix-codec = "0.2.0" bytes = "0.5.2" futures-channel = { version = "0.3.5", default-features = false } From 971ba3eee14b4191ccddd6ddae0a5c135ae789e9 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sat, 18 Jul 2020 16:17:00 +0100 Subject: [PATCH 23/78] fix continous growth of app data in pooled requests (#1609) fixes #1606 fixes #1607 --- CHANGES.md | 4 ++++ src/app_service.rs | 3 ++- src/request.rs | 22 +++++++++++++++------- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 149ff8fcb..fca46d1c1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,10 @@ # Changes ## Unreleased - 2020-xx-xx +### Fixed +* Memory leak of app data in pooled requests. [#1609] + +[#1609]: https://github.com/actix/actix-web/pull/1609 ## 3.0.0-beta.1 - 2020-07-13 diff --git a/src/app_service.rs b/src/app_service.rs index 233cfc0d5..d41cee9fd 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -10,6 +10,7 @@ use actix_router::{Path, ResourceDef, ResourceInfo, Router, Url}; use actix_service::boxed::{self, BoxService, BoxServiceFactory}; use actix_service::{fn_service, Service, ServiceFactory}; use futures_util::future::{join_all, ok, FutureExt, LocalBoxFuture}; +use tinyvec::tiny_vec; use crate::config::{AppConfig, AppService}; use crate::data::{DataFactory, FnDataFactory}; @@ -245,7 +246,7 @@ where inner.path.reset(); inner.head = head; inner.payload = payload; - inner.app_data.push(self.data.clone()); + inner.app_data = tiny_vec![self.data.clone()]; req } else { HttpRequest::new( diff --git a/src/request.rs b/src/request.rs index 85f409016..a1b42f926 100644 --- a/src/request.rs +++ b/src/request.rs @@ -276,6 +276,7 @@ impl HttpMessage for HttpRequest { impl Drop for HttpRequest { fn drop(&mut self) { + // if possible, contribute to current worker's HttpRequest allocation pool if Rc::strong_count(&self.0) == 1 { let v = &mut self.0.pool.0.borrow_mut(); if v.len() < 128 { @@ -340,25 +341,32 @@ impl fmt::Debug for HttpRequest { } } -/// Request's objects pool +/// Slab-allocated `HttpRequest` Pool +/// +/// Since request processing may yield for asynchronous events to complete, a worker may have many +/// requests in-flight at any time. Pooling requests like this amortizes the performance and memory +/// costs of allocating and de-allocating HttpRequest objects as frequently as they otherwise would. +/// +/// Request objects are added when they are dropped (see `::drop`) and re-used +/// in `::call` when there are available objects in the list. +/// +/// The pool's initial capacity is 128 items. pub(crate) struct HttpRequestPool(RefCell>>); impl HttpRequestPool { + /// Allocates a slab of memory for pool use. pub(crate) fn create() -> &'static HttpRequestPool { let pool = HttpRequestPool(RefCell::new(Vec::with_capacity(128))); Box::leak(Box::new(pool)) } - /// Get message from the pool + /// Re-use a previously allocated (but now completed/discarded) HttpRequest object. #[inline] pub(crate) fn get_request(&self) -> Option { - if let Some(inner) = self.0.borrow_mut().pop() { - Some(HttpRequest(inner)) - } else { - None - } + self.0.borrow_mut().pop().map(HttpRequest) } + /// Clears all allocated HttpRequest objects. pub(crate) fn clear(&self) { self.0.borrow_mut().clear() } From 43c362779dabb6f021d42e345d214aaea043adf9 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 20 Jul 2020 17:40:58 +0100 Subject: [PATCH 24/78] also try extracting payload config as Data (#1610) --- CHANGES.md | 5 ++ src/types/payload.rs | 133 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 116 insertions(+), 22 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index fca46d1c1..4b6697a7f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,15 @@ # Changes ## Unreleased - 2020-xx-xx +### Changed +* `PayloadConfig` is now also considered in `Bytes` and `String` extractors when set + using `App::data`. [#1610] + ### Fixed * Memory leak of app data in pooled requests. [#1609] [#1609]: https://github.com/actix/actix-web/pull/1609 +[#1610]: https://github.com/actix/actix-web/pull/1610 ## 3.0.0-beta.1 - 2020-07-13 diff --git a/src/types/payload.rs b/src/types/payload.rs index 0efdc2c09..653abf089 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -13,10 +13,10 @@ use futures_util::future::{err, ok, Either, FutureExt, LocalBoxFuture, Ready}; use futures_util::StreamExt; use mime::Mime; -use crate::dev; use crate::extract::FromRequest; use crate::http::header; use crate::request::HttpRequest; +use crate::{dev, web}; /// Payload extractor returns request 's payload stream. /// @@ -142,13 +142,8 @@ impl FromRequest for Bytes { #[inline] fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { - let tmp; - let cfg = if let Some(cfg) = req.app_data::() { - cfg - } else { - tmp = PayloadConfig::default(); - &tmp - }; + // allow both Config and Data + let cfg = PayloadConfig::from_req(req); if let Err(e) = cfg.check_mimetype(req) { return Either::Right(err(e)); @@ -197,13 +192,7 @@ impl FromRequest for String { #[inline] fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { - let tmp; - let cfg = if let Some(cfg) = req.app_data::() { - cfg - } else { - tmp = PayloadConfig::default(); - &tmp - }; + let cfg = PayloadConfig::from_req(req); // check content-type if let Err(e) = cfg.check_mimetype(req) { @@ -237,7 +226,12 @@ impl FromRequest for String { ) } } -/// Payload configuration for request's payload. + +/// Configuration for request's payload. +/// +/// Applies to the built-in `Bytes` and `String` extractors. Note that the Payload extractor does +/// not automatically check conformance with this configuration to allow more flexibility when +/// building extractors on top of `Payload`. #[derive(Clone)] pub struct PayloadConfig { limit: usize, @@ -284,14 +278,28 @@ impl PayloadConfig { } Ok(()) } + + /// Allow payload config extraction from app data checking both `T` and `Data`, in that + /// order, and falling back to the default payload config. + fn from_req(req: &HttpRequest) -> &PayloadConfig { + req.app_data::() + .or_else(|| { + req.app_data::>() + .map(|d| d.as_ref()) + }) + .unwrap_or_else(|| &DEFAULT_PAYLOAD_CONFIG) + } } +// Allow shared refs to default. +static DEFAULT_PAYLOAD_CONFIG: PayloadConfig = PayloadConfig { + limit: 262_144, // 2^18 bytes (~256kB) + mimetype: None, +}; + impl Default for PayloadConfig { fn default() -> Self { - PayloadConfig { - limit: 262_144, - mimetype: None, - } + DEFAULT_PAYLOAD_CONFIG.clone() } } @@ -407,8 +415,9 @@ mod tests { use bytes::Bytes; use super::*; - use crate::http::header; - use crate::test::TestRequest; + use crate::http::{header, StatusCode}; + use crate::test::{call_service, init_service, TestRequest}; + use crate::{web, App, Responder}; #[actix_rt::test] async fn test_payload_config() { @@ -428,6 +437,86 @@ mod tests { assert!(cfg.check_mimetype(&req).is_ok()); } + #[actix_rt::test] + async fn test_config_recall_locations() { + async fn bytes_handler(_: Bytes) -> impl Responder { + "payload is probably json bytes" + } + + async fn string_handler(_: String) -> impl Responder { + "payload is probably json string" + } + + let mut srv = init_service( + App::new() + .service( + web::resource("/bytes-app-data") + .app_data( + PayloadConfig::default().mimetype(mime::APPLICATION_JSON), + ) + .route(web::get().to(bytes_handler)), + ) + .service( + web::resource("/bytes-data") + .data(PayloadConfig::default().mimetype(mime::APPLICATION_JSON)) + .route(web::get().to(bytes_handler)), + ) + .service( + web::resource("/string-app-data") + .app_data( + PayloadConfig::default().mimetype(mime::APPLICATION_JSON), + ) + .route(web::get().to(string_handler)), + ) + .service( + web::resource("/string-data") + .data(PayloadConfig::default().mimetype(mime::APPLICATION_JSON)) + .route(web::get().to(string_handler)), + ), + ) + .await; + + let req = TestRequest::with_uri("/bytes-app-data").to_request(); + let resp = call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + + let req = TestRequest::with_uri("/bytes-data").to_request(); + let resp = call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + + let req = TestRequest::with_uri("/string-app-data").to_request(); + let resp = call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + + let req = TestRequest::with_uri("/string-data").to_request(); + let resp = call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + + let req = TestRequest::with_uri("/bytes-app-data") + .header(header::CONTENT_TYPE, mime::APPLICATION_JSON) + .to_request(); + let resp = call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/bytes-data") + .header(header::CONTENT_TYPE, mime::APPLICATION_JSON) + .to_request(); + let resp = call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/string-app-data") + .header(header::CONTENT_TYPE, mime::APPLICATION_JSON) + .to_request(); + let resp = call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/string-data") + .header(header::CONTENT_TYPE, mime::APPLICATION_JSON) + .to_request(); + let resp = call_service(&mut srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + } + #[actix_rt::test] async fn test_bytes() { let (req, mut pl) = TestRequest::with_header(header::CONTENT_LENGTH, "11") From f8d5ad6b5349843d16ec391c711b7b05eddce428 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 21 Jul 2020 01:54:26 +0200 Subject: [PATCH 25/78] Make web::Path a tuple struct with a public inner value (#1594) Co-authored-by: Rob Ede --- CHANGES.md | 3 +++ MIGRATION.md | 20 +++++++++++++++++ README.md | 4 ++-- src/lib.rs | 4 ++-- src/types/path.rs | 57 +++++++++++++++++++++++------------------------ 5 files changed, 55 insertions(+), 33 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4b6697a7f..5783da75a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,10 +4,13 @@ ### Changed * `PayloadConfig` is now also considered in `Bytes` and `String` extractors when set using `App::data`. [#1610] +* `web::Path` now has a public representation: `web::Path(pub T)` that enables + destructuring. [#1594] ### Fixed * Memory leak of app data in pooled requests. [#1609] +[#1594]: https://github.com/actix/actix-web/pull/1594 [#1609]: https://github.com/actix/actix-web/pull/1609 [#1610]: https://github.com/actix/actix-web/pull/1610 diff --git a/MIGRATION.md b/MIGRATION.md index d2e9735fb..7459b5b2d 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -12,6 +12,26 @@ * `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a `u64` instead of a `usize`. +* Code that was using `path.` to access a `web::Path<(A, B, C)>`s elements now needs to use + destructuring or `.into_inner()`. For example: + + ```rust + // Previously: + async fn some_route(path: web::Path<(String, String)>) -> String { + format!("Hello, {} {}", path.0, path.1) + } + + // Now (this also worked before): + async fn some_route(path: web::Path<(String, String)>) -> String { + let (first_name, last_name) = path.into_inner(); + format!("Hello, {} {}", first_name, last_name) + } + // Or (this wasn't previously supported): + async fn some_route(web::Path((first_name, last_name)): web::Path<(String, String)>) -> String { + format!("Hello, {} {}", first_name, last_name) + } + ``` + ## 2.0.0 * `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to diff --git a/README.md b/README.md index 4d6bac29c..4f6a16199 100644 --- a/README.md +++ b/README.md @@ -61,8 +61,8 @@ Code: use actix_web::{get, web, App, HttpServer, Responder}; #[get("/{id}/{name}/index.html")] -async fn index(info: web::Path<(u32, String)>) -> impl Responder { - format!("Hello {}! id:{}", info.1, info.0) +async fn index(web::Path((id, name)): web::Path<(u32, String)>) -> impl Responder { + format!("Hello {}! id:{}", name, id) } #[actix_web::main] diff --git a/src/lib.rs b/src/lib.rs index eb46af664..8776a62b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,8 +9,8 @@ //! use actix_web::{get, web, App, HttpServer, Responder}; //! //! #[get("/{id}/{name}/index.html")] -//! async fn index(info: web::Path<(u32, String)>) -> impl Responder { -//! format!("Hello {}! id:{}", info.1, info.0) +//! async fn index(web::Path((id, name)): web::Path<(u32, String)>) -> impl Responder { +//! format!("Hello {}! id:{}", name, id) //! } //! //! #[actix_web::main] diff --git a/src/types/path.rs b/src/types/path.rs index 82050171c..dbb5f3ee0 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -25,8 +25,8 @@ use crate::FromRequest; /// /// extract path info from "/{username}/{count}/index.html" url /// /// {username} - deserializes to a String /// /// {count} - - deserializes to a u32 -/// async fn index(info: web::Path<(String, u32)>) -> String { -/// format!("Welcome {}! {}", info.0, info.1) +/// async fn index(web::Path((username, count)): web::Path<(String, u32)>) -> String { +/// format!("Welcome {}! {}", username, count) /// } /// /// fn main() { @@ -61,20 +61,18 @@ use crate::FromRequest; /// ); /// } /// ``` -pub struct Path { - inner: T, -} +pub struct Path(pub T); impl Path { /// Deconstruct to an inner value pub fn into_inner(self) -> T { - self.inner + self.0 } } impl AsRef for Path { fn as_ref(&self) -> &T { - &self.inner + &self.0 } } @@ -82,31 +80,31 @@ impl ops::Deref for Path { type Target = T; fn deref(&self) -> &T { - &self.inner + &self.0 } } impl ops::DerefMut for Path { fn deref_mut(&mut self) -> &mut T { - &mut self.inner + &mut self.0 } } impl From for Path { fn from(inner: T) -> Path { - Path { inner } + Path(inner) } } impl fmt::Debug for Path { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.inner.fmt(f) + self.0.fmt(f) } } impl fmt::Display for Path { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.inner.fmt(f) + self.0.fmt(f) } } @@ -120,8 +118,8 @@ impl fmt::Display for Path { /// /// extract path info from "/{username}/{count}/index.html" url /// /// {username} - deserializes to a String /// /// {count} - - deserializes to a u32 -/// async fn index(info: web::Path<(String, u32)>) -> String { -/// format!("Welcome {}! {}", info.0, info.1) +/// async fn index(web::Path((username, count)): web::Path<(String, u32)>) -> String { +/// format!("Welcome {}! {}", username, count) /// } /// /// fn main() { @@ -173,7 +171,7 @@ where ready( de::Deserialize::deserialize(PathDeserializer::new(req.match_info())) - .map(|inner| Path { inner }) + .map(Path) .map_err(move |e| { log::debug!( "Failed during Path extractor deserialization. \ @@ -290,21 +288,22 @@ mod tests { resource.match_path(req.match_info_mut()); let (req, mut pl) = req.into_parts(); - let res = <(Path<(String, String)>,)>::from_request(&req, &mut pl) + let (Path(res),) = <(Path<(String, String)>,)>::from_request(&req, &mut pl) .await .unwrap(); - assert_eq!((res.0).0, "name"); - assert_eq!((res.0).1, "user1"); + assert_eq!(res.0, "name"); + assert_eq!(res.1, "user1"); - let res = <(Path<(String, String)>, Path<(String, String)>)>::from_request( - &req, &mut pl, - ) - .await - .unwrap(); - assert_eq!((res.0).0, "name"); - assert_eq!((res.0).1, "user1"); - assert_eq!((res.1).0, "name"); - assert_eq!((res.1).1, "user1"); + let (Path(a), Path(b)) = + <(Path<(String, String)>, Path<(String, String)>)>::from_request( + &req, &mut pl, + ) + .await + .unwrap(); + assert_eq!(a.0, "name"); + assert_eq!(a.1, "user1"); + assert_eq!(b.0, "name"); + assert_eq!(b.1, "user1"); let () = <()>::from_request(&req, &mut pl).await.unwrap(); } @@ -329,7 +328,7 @@ mod tests { let s = s.into_inner(); assert_eq!(s.value, "user2"); - let s = Path::<(String, String)>::from_request(&req, &mut pl) + let Path(s) = Path::<(String, String)>::from_request(&req, &mut pl) .await .unwrap(); assert_eq!(s.0, "name"); @@ -344,7 +343,7 @@ mod tests { assert_eq!(s.as_ref().key, "name"); assert_eq!(s.value, 32); - let s = Path::<(String, u8)>::from_request(&req, &mut pl) + let Path(s) = Path::<(String, u8)>::from_request(&req, &mut pl) .await .unwrap(); assert_eq!(s.0, "name"); From 0ec335a39c660c89290a45e3d848d5e7c3ae5f02 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 21 Jul 2020 08:40:30 +0100 Subject: [PATCH 26/78] bump MSRV to 1.42 (#1616) --- .github/PULL_REQUEST_TEMPLATE.md | 6 ++++-- .github/workflows/linux.yml | 2 +- CHANGES.md | 1 + README.md | 4 ++-- actix-http/src/body.rs | 10 ++-------- actix-http/src/client/h2proto.rs | 5 +---- actix-http/src/h1/decoder.rs | 10 ++-------- actix-http/src/h1/dispatcher.rs | 10 ++-------- .../src/header/common/content_disposition.rs | 15 +++------------ actix-http/src/header/mod.rs | 5 +---- actix-http/src/ws/frame.rs | 5 +---- awc/src/response.rs | 11 +++-------- rust-toolchain | 2 +- src/types/form.rs | 15 +++------------ src/types/json.rs | 10 ++-------- 15 files changed, 29 insertions(+), 82 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index cc34e2bc6..1c0d467b1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,8 @@ -## PR Type -What kind of change does this PR make? + + +## PR Type + INSERT_PR_TYPE diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 7529c8494..688db6cee 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -8,7 +8,7 @@ jobs: fail-fast: false matrix: version: - - 1.41.1 # MSRV + - 1.42.0 # MSRV - stable - nightly diff --git a/CHANGES.md b/CHANGES.md index 5783da75a..d80e87946 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ using `App::data`. [#1610] * `web::Path` now has a public representation: `web::Path(pub T)` that enables destructuring. [#1594] +* MSRV is now 1.42.0. ### Fixed * Memory leak of app data in pooled requests. [#1609] diff --git a/README.md b/README.md index 4f6a16199..a42a1a6f8 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![crates.io](https://meritbadge.herokuapp.com/actix-web)](https://crates.io/crates/actix-web) [![Documentation](https://docs.rs/actix-web/badge.svg)](https://docs.rs/actix-web) -[![Version](https://img.shields.io/badge/rustc-1.41+-lightgray.svg)](https://blog.rust-lang.org/2020/02/27/Rust-1.41.1.html) +[![Version](https://img.shields.io/badge/rustc-1.42+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html) ![License](https://img.shields.io/crates/l/actix-web.svg)
[![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) @@ -32,7 +32,7 @@ * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) * Includes an async [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html) * Supports [Actix actor framework](https://github.com/actix/actix) -* Runs on stable Rust 1.41+ +* Runs on stable Rust 1.42+ ## Documentation diff --git a/actix-http/src/body.rs b/actix-http/src/body.rs index 0b01aa8ce..137f462a4 100644 --- a/actix-http/src/body.rs +++ b/actix-http/src/body.rs @@ -192,14 +192,8 @@ impl MessageBody for Body { impl PartialEq for Body { fn eq(&self, other: &Body) -> bool { match *self { - Body::None => match *other { - Body::None => true, - _ => false, - }, - Body::Empty => match *other { - Body::Empty => true, - _ => false, - }, + Body::None => matches!(*other, Body::None), + Body::Empty => matches!(*other, Body::Empty), Body::Bytes(ref b) => match *other { Body::Bytes(ref b2) => b == b2, _ => false, diff --git a/actix-http/src/client/h2proto.rs b/actix-http/src/client/h2proto.rs index 48ab9fe4a..7d1d3dc51 100644 --- a/actix-http/src/client/h2proto.rs +++ b/actix-http/src/client/h2proto.rs @@ -37,10 +37,7 @@ where trace!("Sending client request: {:?} {:?}", head, body.size()); let head_req = head.as_ref().method == Method::HEAD; let length = body.size(); - let eof = match length { - BodySize::None | BodySize::Empty | BodySize::Sized(0) => true, - _ => false, - }; + let eof = matches!(length, BodySize::None | BodySize::Empty | BodySize::Sized(0)); let mut req = Request::new(()); *req.uri_mut() = head.as_ref().uri.clone(); diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index c9d3edf33..89a18aea9 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -655,10 +655,7 @@ mod tests { } fn is_unhandled(&self) -> bool { - match self { - PayloadType::Stream(_) => true, - _ => false, - } + matches!(self, PayloadType::Stream(_)) } } @@ -670,10 +667,7 @@ mod tests { } } fn eof(&self) -> bool { - match *self { - PayloadItem::Eof => true, - _ => false, - } + matches!(*self, PayloadItem::Eof) } } diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 51f107efb..a9ab29881 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -156,14 +156,8 @@ enum PollResponse { impl PartialEq for PollResponse { fn eq(&self, other: &PollResponse) -> bool { match self { - PollResponse::DrainWriteBuf => match other { - PollResponse::DrainWriteBuf => true, - _ => false, - }, - PollResponse::DoNothing => match other { - PollResponse::DoNothing => true, - _ => false, - }, + PollResponse::DrainWriteBuf => matches!(other, PollResponse::DrainWriteBuf), + PollResponse::DoNothing => matches!(other, PollResponse::DoNothing), _ => false, } } diff --git a/actix-http/src/header/common/content_disposition.rs b/actix-http/src/header/common/content_disposition.rs index d65933901..051dcfe80 100644 --- a/actix-http/src/header/common/content_disposition.rs +++ b/actix-http/src/header/common/content_disposition.rs @@ -387,26 +387,17 @@ impl ContentDisposition { /// Returns `true` if it is [`Inline`](DispositionType::Inline). pub fn is_inline(&self) -> bool { - match self.disposition { - DispositionType::Inline => true, - _ => false, - } + matches!(self.disposition, DispositionType::Inline) } /// Returns `true` if it is [`Attachment`](DispositionType::Attachment). pub fn is_attachment(&self) -> bool { - match self.disposition { - DispositionType::Attachment => true, - _ => false, - } + matches!(self.disposition, DispositionType::Attachment) } /// Returns `true` if it is [`FormData`](DispositionType::FormData). pub fn is_form_data(&self) -> bool { - match self.disposition { - DispositionType::FormData => true, - _ => false, - } + matches!(self.disposition, DispositionType::FormData) } /// Returns `true` if it is [`Ext`](DispositionType::Ext) and the `disp_type` matches. diff --git a/actix-http/src/header/mod.rs b/actix-http/src/header/mod.rs index 0db26ceb0..46fb31a62 100644 --- a/actix-http/src/header/mod.rs +++ b/actix-http/src/header/mod.rs @@ -148,10 +148,7 @@ impl ContentEncoding { #[inline] /// Is the content compressed? pub fn is_compression(self) -> bool { - match self { - ContentEncoding::Identity | ContentEncoding::Auto => false, - _ => true, - } + matches!(self, ContentEncoding::Identity | ContentEncoding::Auto) } #[inline] diff --git a/actix-http/src/ws/frame.rs b/actix-http/src/ws/frame.rs index 8f7004f18..0598a9b4e 100644 --- a/actix-http/src/ws/frame.rs +++ b/actix-http/src/ws/frame.rs @@ -229,10 +229,7 @@ mod tests { fn is_none( frm: &Result)>, ProtocolError>, ) -> bool { - match *frm { - Ok(None) => true, - _ => false, - } + matches!(*frm, Ok(None)) } fn extract( diff --git a/awc/src/response.rs b/awc/src/response.rs index ffc8c5408..d1490cb98 100644 --- a/awc/src/response.rs +++ b/awc/src/response.rs @@ -402,14 +402,9 @@ mod tests { fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { match err { - JsonPayloadError::Payload(PayloadError::Overflow) => match other { - JsonPayloadError::Payload(PayloadError::Overflow) => true, - _ => false, - }, - JsonPayloadError::ContentType => match other { - JsonPayloadError::ContentType => true, - _ => false, - }, + JsonPayloadError::Payload(PayloadError::Overflow) => + matches!(other, JsonPayloadError::Payload(PayloadError::Overflow)), + JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType), _ => false, } } diff --git a/rust-toolchain b/rust-toolchain index f86fb9cbc..a50908ca3 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -1.41.1 +1.42.0 diff --git a/src/types/form.rs b/src/types/form.rs index c10ed4649..d7db595ca 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -407,18 +407,9 @@ mod tests { fn eq(err: UrlencodedError, other: UrlencodedError) -> bool { match err { - UrlencodedError::Overflow { .. } => match other { - UrlencodedError::Overflow { .. } => true, - _ => false, - }, - UrlencodedError::UnknownLength => match other { - UrlencodedError::UnknownLength => true, - _ => false, - }, - UrlencodedError::ContentType => match other { - UrlencodedError::ContentType => true, - _ => false, - }, + UrlencodedError::Overflow { .. } => matches!(other, UrlencodedError::Overflow { .. }), + UrlencodedError::UnknownLength => matches!(other, UrlencodedError::UnknownLength), + UrlencodedError::ContentType => matches!(other, UrlencodedError::ContentType), _ => false, } } diff --git a/src/types/json.rs b/src/types/json.rs index 6de9e0d86..1665ca95a 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -433,14 +433,8 @@ mod tests { fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { match err { - JsonPayloadError::Overflow => match other { - JsonPayloadError::Overflow => true, - _ => false, - }, - JsonPayloadError::ContentType => match other { - JsonPayloadError::ContentType => true, - _ => false, - }, + JsonPayloadError::Overflow => matches!(other, JsonPayloadError::Overflow), + JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType), _ => false, } } From 6dc47c4093daada3caecd1fe059cdb73098a29e3 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 21 Jul 2020 16:25:33 +0100 Subject: [PATCH 27/78] fix soundness concern in h1 decoder (#1614) Co-authored-by: Yuki Okushi --- actix-http/CHANGES.md | 5 + actix-http/Cargo.toml | 4 + actix-http/benches/uninit-headers.rs | 137 +++++++++++++++++++++++++++ actix-http/src/h1/decoder.rs | 34 +++---- 4 files changed, 163 insertions(+), 17 deletions(-) create mode 100644 actix-http/benches/uninit-headers.rs diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index a185a9f81..f7923916a 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,11 @@ # Changes ## [Unreleased] - xxx +### Fixed +* Potential UB in h1 decoder using uninitialized memory. [#1614] + +[#1614]: https://github.com/actix/actix-web/pull/1614 + ## [2.0.0-beta.1] - 2020-07-11 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 232b5c3f0..bbb2a2143 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -103,3 +103,7 @@ harness = false [[bench]] name = "status-line" harness = false + +[[bench]] +name = "uninit-headers" +harness = false diff --git a/actix-http/benches/uninit-headers.rs b/actix-http/benches/uninit-headers.rs new file mode 100644 index 000000000..83e74171c --- /dev/null +++ b/actix-http/benches/uninit-headers.rs @@ -0,0 +1,137 @@ +use criterion::{criterion_group, criterion_main, Criterion}; + +use bytes::BytesMut; + +// A Miri run detects UB, seen on this playground: +// https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f5d9aa166aa48df8dca05fce2b6c3915 + +fn bench_header_parsing(c: &mut Criterion) { + c.bench_function("Original (Unsound) [short]", |b| { + b.iter(|| { + let mut buf = BytesMut::from(REQ_SHORT); + _original::parse_headers(&mut buf); + }) + }); + + c.bench_function("New (safe) [short]", |b| { + b.iter(|| { + let mut buf = BytesMut::from(REQ_SHORT); + _new::parse_headers(&mut buf); + }) + }); + + c.bench_function("Original (Unsound) [realistic]", |b| { + b.iter(|| { + let mut buf = BytesMut::from(REQ); + _original::parse_headers(&mut buf); + }) + }); + + c.bench_function("New (safe) [realistic]", |b| { + b.iter(|| { + let mut buf = BytesMut::from(REQ); + _new::parse_headers(&mut buf); + }) + }); +} + +criterion_group!(benches, bench_header_parsing); +criterion_main!(benches); + +const MAX_HEADERS: usize = 96; + +const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] = + [httparse::EMPTY_HEADER; MAX_HEADERS]; + +#[derive(Clone, Copy)] +struct HeaderIndex { + name: (usize, usize), + value: (usize, usize), +} + +const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex { + name: (0, 0), + value: (0, 0), +}; + +const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] = + [EMPTY_HEADER_INDEX; MAX_HEADERS]; + +impl HeaderIndex { + fn record( + bytes: &[u8], + headers: &[httparse::Header<'_>], + indices: &mut [HeaderIndex], + ) { + let bytes_ptr = bytes.as_ptr() as usize; + for (header, indices) in headers.iter().zip(indices.iter_mut()) { + let name_start = header.name.as_ptr() as usize - bytes_ptr; + let name_end = name_start + header.name.len(); + indices.name = (name_start, name_end); + let value_start = header.value.as_ptr() as usize - bytes_ptr; + let value_end = value_start + header.value.len(); + indices.value = (value_start, value_end); + } + } +} + +// test cases taken from: +// https://github.com/seanmonstar/httparse/blob/master/benches/parse.rs + +const REQ_SHORT: &'static [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"\ +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\ +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n\ +Accept-Language: ja,en-us;q=0.7,en;q=0.3\r\n\ +Accept-Encoding: gzip,deflate\r\n\ +Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7\r\n\ +Keep-Alive: 115\r\n\ +Connection: keep-alive\r\n\ +Cookie: wp_ozh_wsa_visits=2; wp_ozh_wsa_visit_lasttime=xxxxxxxxxx; __utma=xxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.x; __utmz=xxxxxxxxx.xxxxxxxxxx.x.x.utmccn=(referral)|utmcsr=reader.livedoor.com|utmcct=/reader/|utmcmd=referral|padding=under256\r\n\r\n"; + +mod _new { + use super::*; + + pub fn parse_headers(src: &mut BytesMut) -> usize { + let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY; + let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY; + + let mut req = httparse::Request::new(&mut parsed); + match req.parse(src).unwrap() { + httparse::Status::Complete(_len) => { + HeaderIndex::record(src, req.headers, &mut headers); + req.headers.len() + } + _ => unreachable!(), + } + } +} + +mod _original { + use super::*; + + use std::mem::MaybeUninit; + + pub fn parse_headers(src: &mut BytesMut) -> usize { + let mut headers: [HeaderIndex; MAX_HEADERS] = + unsafe { MaybeUninit::uninit().assume_init() }; + + let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = + unsafe { MaybeUninit::uninit().assume_init() }; + + let mut req = httparse::Request::new(&mut parsed); + match req.parse(src).unwrap() { + httparse::Status::Complete(_len) => { + HeaderIndex::record(src, req.headers, &mut headers); + req.headers.len() + } + _ => unreachable!(), + } + } +} diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index 89a18aea9..a6c52d351 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -1,7 +1,6 @@ use std::convert::TryFrom; use std::io; use std::marker::PhantomData; -use std::mem::MaybeUninit; use std::task::Poll; use actix_codec::Decoder; @@ -77,7 +76,7 @@ pub(crate) trait MessageType: Sized { let name = HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]).unwrap(); - // Unsafe: httparse check header value for valid utf-8 + // SAFETY: httparse checks header value is valid UTF-8 let value = unsafe { HeaderValue::from_maybe_shared_unchecked( slice.slice(idx.value.0..idx.value.1), @@ -184,16 +183,11 @@ impl MessageType for Request { &mut self.head_mut().headers } - #[allow(clippy::uninit_assumed_init)] fn decode(src: &mut BytesMut) -> Result, ParseError> { - // Unsafe: we read only this data only after httparse parses headers into. - // performance bump for pipeline benchmarks. - let mut headers: [HeaderIndex; MAX_HEADERS] = - unsafe { MaybeUninit::uninit().assume_init() }; + let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY; let (len, method, uri, ver, h_len) = { - let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = - unsafe { MaybeUninit::uninit().assume_init() }; + let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY; let mut req = httparse::Request::new(&mut parsed); match req.parse(src)? { @@ -260,16 +254,11 @@ impl MessageType for ResponseHead { &mut self.headers } - #[allow(clippy::uninit_assumed_init)] fn decode(src: &mut BytesMut) -> Result, ParseError> { - // Unsafe: we read only this data only after httparse parses headers into. - // performance bump for pipeline benchmarks. - let mut headers: [HeaderIndex; MAX_HEADERS] = - unsafe { MaybeUninit::uninit().assume_init() }; + let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY; let (len, ver, status, h_len) = { - let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = - unsafe { MaybeUninit::uninit().assume_init() }; + let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY; let mut res = httparse::Response::new(&mut parsed); match res.parse(src)? { @@ -324,6 +313,17 @@ pub(crate) struct HeaderIndex { pub(crate) value: (usize, usize), } +pub(crate) const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex { + name: (0, 0), + value: (0, 0), +}; + +pub(crate) const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] = + [EMPTY_HEADER_INDEX; MAX_HEADERS]; + +pub(crate) const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] = + [httparse::EMPTY_HEADER; MAX_HEADERS]; + impl HeaderIndex { pub(crate) fn record( bytes: &[u8], @@ -973,7 +973,7 @@ mod tests { unreachable!("Error"); } - // type in chunked + // intentional typo in "chunked" let mut buf = BytesMut::from( "GET /test HTTP/1.1\r\n\ transfer-encoding: chnked\r\n\r\n", From 701bdacfa213f2096a50272da7bc7bcf48aede8a Mon Sep 17 00:00:00 2001 From: masnagam Date: Wed, 22 Jul 2020 01:24:56 +0900 Subject: [PATCH 28/78] Fix illegal chunked encoding (#1615) Co-authored-by: Rob Ede --- actix-http/CHANGES.md | 6 ++++++ actix-http/src/h1/decoder.rs | 37 ++++++++++++++++++++++++++---------- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index f7923916a..a4bf7be93 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -7,6 +7,12 @@ [#1614]: https://github.com/actix/actix-web/pull/1614 +### Changed + +* Fix illegal chunked encoding. [#1615] + +[#1615]: https://github.com/actix/actix-web/pull/1615 + ## [2.0.0-beta.1] - 2020-07-11 ### Changed diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index a6c52d351..8fdd11be5 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -45,7 +45,7 @@ impl Decoder for MessageDecoder { pub(crate) enum PayloadLength { Payload(PayloadType), - Upgrade, + UpgradeWebSocket, None, } @@ -64,7 +64,7 @@ pub(crate) trait MessageType: Sized { raw_headers: &[HeaderIndex], ) -> Result { let mut ka = None; - let mut has_upgrade = false; + let mut has_upgrade_websocket = false; let mut expect = false; let mut chunked = false; let mut content_length = None; @@ -123,12 +123,9 @@ pub(crate) trait MessageType: Sized { }; } header::UPGRADE => { - has_upgrade = true; - // check content-length, some clients (dart) - // sends "content-length: 0" with websocket upgrade if let Ok(val) = value.to_str().map(|val| val.trim()) { if val.eq_ignore_ascii_case("websocket") { - content_length = None; + has_upgrade_websocket = true; } } } @@ -155,13 +152,13 @@ pub(crate) trait MessageType: Sized { Ok(PayloadLength::Payload(PayloadType::Payload( PayloadDecoder::chunked(), ))) + } else if has_upgrade_websocket { + Ok(PayloadLength::UpgradeWebSocket) } else if let Some(len) = content_length { // Content-Length Ok(PayloadLength::Payload(PayloadType::Payload( PayloadDecoder::length(len), ))) - } else if has_upgrade { - Ok(PayloadLength::Upgrade) } else { Ok(PayloadLength::None) } @@ -216,7 +213,7 @@ impl MessageType for Request { // payload decoder let decoder = match length { PayloadLength::Payload(pl) => pl, - PayloadLength::Upgrade => { + PayloadLength::UpgradeWebSocket => { // upgrade(websocket) PayloadType::Stream(PayloadDecoder::eof()) } @@ -1034,7 +1031,7 @@ mod tests { } #[test] - fn test_http_request_upgrade() { + fn test_http_request_upgrade_websocket() { let mut buf = BytesMut::from( "GET /test HTTP/1.1\r\n\ connection: upgrade\r\n\ @@ -1048,6 +1045,26 @@ mod tests { assert!(pl.is_unhandled()); } + #[test] + fn test_http_request_upgrade_h2c() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + connection: upgrade, http2-settings\r\n\ + upgrade: h2c\r\n\ + http2-settings: dummy\r\n\r\n", + ); + let mut reader = MessageDecoder::::default(); + let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); + // `connection: upgrade, http2-settings` doesn't work properly.. + // see MessageType::set_headers(). + // + // The line below should be: + // assert_eq!(req.head().connection_type(), ConnectionType::Upgrade); + assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive); + assert!(req.upgrade()); + assert!(!pl.is_unhandled()); + } + #[test] fn test_http_request_parser_utf8() { let mut buf = BytesMut::from( From 92b5bcd13fc8e2014b8a5dfb9c217079c72c0a8f Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Wed, 22 Jul 2020 08:28:33 +0900 Subject: [PATCH 29/78] Check format and tweak CI config (#1619) --- .github/PULL_REQUEST_TEMPLATE.md | 1 + .github/workflows/bench.yml | 7 +- .github/workflows/clippy-fmt.yml | 32 ++++++++ .github/workflows/linux.yml | 7 +- .github/workflows/macos.yml | 7 +- .github/workflows/windows.yml | 7 +- actix-files/src/lib.rs | 50 +++++------- actix-files/src/named.rs | 7 +- actix-http/src/body.rs | 15 +--- actix-http/src/client/h2proto.rs | 5 +- actix-http/src/client/pool.rs | 5 +- actix-http/src/error.rs | 1 - actix-http/src/extensions.rs | 2 +- actix-http/src/h1/dispatcher.rs | 12 +-- actix-http/src/h2/dispatcher.rs | 80 ++++++++++--------- actix-http/src/header/common/allow.rs | 2 +- actix-http/src/header/common/mod.rs | 17 ++-- actix-http/src/httpmessage.rs | 1 - actix-http/src/ws/mask.rs | 2 +- actix-http/tests/test_openssl.rs | 4 +- actix-http/tests/test_rustls.rs | 4 +- actix-http/tests/test_server.rs | 4 +- actix-multipart/src/server.rs | 8 +- actix-web-actors/tests/test_ws.rs | 6 +- actix-web-codegen/src/route.rs | 9 ++- actix-web-codegen/tests/test_macro.rs | 13 ++- awc/src/response.rs | 9 ++- awc/tests/test_client.rs | 6 +- awc/tests/test_connector.rs | 3 +- awc/tests/test_rustls_client.rs | 5 +- awc/tests/test_ssl_client.rs | 5 +- src/app.rs | 21 +++-- src/app_service.rs | 2 +- src/config.rs | 11 ++- src/data.rs | 17 ++-- src/middleware/logger.rs | 2 +- src/middleware/normalize.rs | 6 +- src/resource.rs | 19 +++-- src/responder.rs | 2 +- src/route.rs | 2 +- src/scope.rs | 109 +++++++++++--------------- src/test.rs | 1 - src/types/form.rs | 12 ++- src/types/json.rs | 6 +- tests/test_server.rs | 2 +- 45 files changed, 276 insertions(+), 272 deletions(-) create mode 100644 .github/workflows/clippy-fmt.yml diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1c0d467b1..b779b33fa 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -15,6 +15,7 @@ Check your PR fulfills the following: - [ ] Tests for the changes have been added / updated. - [ ] Documentation comments have been added / updated. - [ ] A changelog entry has been made for the appropriate packages. +- [ ] Format code with the latest stable rustfmt ## Overview diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index ce8a7da7e..754091dde 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -1,6 +1,11 @@ name: Benchmark (Linux) -on: [push, pull_request] +on: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: + - master jobs: check_benchmark: diff --git a/.github/workflows/clippy-fmt.yml b/.github/workflows/clippy-fmt.yml new file mode 100644 index 000000000..fb1ed7f32 --- /dev/null +++ b/.github/workflows/clippy-fmt.yml @@ -0,0 +1,32 @@ +on: + pull_request: + types: [opened, synchronize, reopened] + +name: Clippy and rustfmt Check +jobs: + clippy_check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: rustfmt + override: true + - name: Check with rustfmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + - uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + components: clippy + override: true + - name: Check with Clippy + uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features --all --tests diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 688db6cee..d49874ce3 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -1,6 +1,11 @@ name: CI (Linux) -on: [push, pull_request] +on: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: + - master jobs: build_and_test: diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 6c360bacc..0683fcc8c 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -1,6 +1,11 @@ name: CI (macOS) -on: [push, pull_request] +on: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: + - master jobs: build_and_test: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 5fd785fad..c09c7cea3 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,6 +1,11 @@ name: CI (Windows) -on: [push, pull_request] +on: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: + - master env: VCPKGRS_DYNAMIC: 1 diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 76c68ce25..ae0204a71 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -26,7 +26,6 @@ use actix_web::{web, FromRequest, HttpRequest, HttpResponse}; use bytes::Bytes; use futures_core::Stream; use futures_util::future::{ok, ready, Either, FutureExt, LocalBoxFuture, Ready}; -use mime; use mime_guess::from_ext; use percent_encoding::{utf8_percent_encode, CONTROLS}; use v_htmlescape::escape as escape_html_entity; @@ -250,6 +249,8 @@ pub struct Files { renderer: Rc, mime_override: Option>, file_flags: named::Flags, + // FIXME: Should re-visit later. + #[allow(clippy::redundant_allocation)] guards: Option>>, } @@ -462,6 +463,8 @@ pub struct FilesService { renderer: Rc, mime_override: Option>, file_flags: named::Flags, + // FIXME: Should re-visit later. + #[allow(clippy::redundant_allocation)] guards: Option>>, } @@ -501,11 +504,8 @@ impl Service for FilesService { // execute user defined guards (**guard).check(req.head()) } else { - // default behaviour - match *req.method() { - Method::HEAD | Method::GET => true, - _ => false, - } + // default behavior + matches!(*req.method(), Method::HEAD | Method::GET) }; if !is_method_valid { @@ -952,9 +952,7 @@ mod tests { #[actix_rt::test] async fn test_named_file_content_range_headers() { - let srv = test::start(|| { - App::new().service(Files::new("/", ".")) - }); + let srv = test::start(|| App::new().service(Files::new("/", "."))); // Valid range header let response = srv @@ -979,9 +977,7 @@ mod tests { #[actix_rt::test] async fn test_named_file_content_length_headers() { - let srv = test::start(|| { - App::new().service(Files::new("/", ".")) - }); + let srv = test::start(|| App::new().service(Files::new("/", "."))); // Valid range header let response = srv @@ -1020,15 +1016,9 @@ mod tests { #[actix_rt::test] async fn test_head_content_length_headers() { - let srv = test::start(|| { - App::new().service(Files::new("/", ".")) - }); + let srv = test::start(|| App::new().service(Files::new("/", "."))); - let response = srv - .head("/tests/test.binary") - .send() - .await - .unwrap(); + let response = srv.head("/tests/test.binary").send().await.unwrap(); let content_length = response .headers() @@ -1097,12 +1087,10 @@ mod tests { #[actix_rt::test] async fn test_named_file_content_encoding() { let mut srv = test::init_service(App::new().wrap(Compress::default()).service( - web::resource("/").to(|| { - async { - NamedFile::open("Cargo.toml") - .unwrap() - .set_content_encoding(header::ContentEncoding::Identity) - } + web::resource("/").to(|| async { + NamedFile::open("Cargo.toml") + .unwrap() + .set_content_encoding(header::ContentEncoding::Identity) }), )) .await; @@ -1119,12 +1107,10 @@ mod tests { #[actix_rt::test] async fn test_named_file_content_encoding_gzip() { let mut srv = test::init_service(App::new().wrap(Compress::default()).service( - web::resource("/").to(|| { - async { - NamedFile::open("Cargo.toml") - .unwrap() - .set_content_encoding(header::ContentEncoding::Gzip) - } + web::resource("/").to(|| async { + NamedFile::open("Cargo.toml") + .unwrap() + .set_content_encoding(header::ContentEncoding::Gzip) }), )) .await; diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 6ee561a4b..12da722d2 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -8,7 +8,6 @@ use std::time::{SystemTime, UNIX_EPOCH}; use std::os::unix::fs::MetadataExt; use bitflags::bitflags; -use mime; use mime_guess::from_path; use actix_http::body::SizedStream; @@ -90,7 +89,7 @@ impl NamedFile { }; let ct = from_path(&path).first_or_octet_stream(); - let disposition_type = match ct.type_() { + let disposition = match ct.type_() { mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, _ => DispositionType::Attachment, }; @@ -104,8 +103,8 @@ impl NamedFile { })) } let cd = ContentDisposition { - disposition: disposition_type, - parameters: parameters, + disposition, + parameters, }; (ct, cd) }; diff --git a/actix-http/src/body.rs b/actix-http/src/body.rs index 137f462a4..8bea8e6b8 100644 --- a/actix-http/src/body.rs +++ b/actix-http/src/body.rs @@ -21,12 +21,7 @@ pub enum BodySize { impl BodySize { pub fn is_eof(&self) -> bool { - match self { - BodySize::None - | BodySize::Empty - | BodySize::Sized(0) => true, - _ => false, - } + matches!(self, BodySize::None | BodySize::Empty | BodySize::Sized(0)) } } @@ -470,9 +465,9 @@ where #[cfg(test)] mod tests { use super::*; - use futures_util::stream; use futures_util::future::poll_fn; use futures_util::pin_mut; + use futures_util::stream; impl Body { pub(crate) fn get_ref(&self) -> &[u8] { @@ -606,10 +601,6 @@ mod tests { #[actix_rt::test] async fn test_body_eq() { - assert!(Body::None == Body::None); - assert!(Body::None != Body::Empty); - assert!(Body::Empty == Body::Empty); - assert!(Body::Empty != Body::None); assert!( Body::Bytes(Bytes::from_static(b"1")) == Body::Bytes(Bytes::from_static(b"1")) @@ -621,7 +612,7 @@ mod tests { 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")); + assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains('1')); } #[actix_rt::test] diff --git a/actix-http/src/client/h2proto.rs b/actix-http/src/client/h2proto.rs index 7d1d3dc51..3f9a981f4 100644 --- a/actix-http/src/client/h2proto.rs +++ b/actix-http/src/client/h2proto.rs @@ -37,7 +37,10 @@ where trace!("Sending client request: {:?} {:?}", head, body.size()); 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::Empty | BodySize::Sized(0) + ); let mut req = Request::new(()); *req.uri_mut() = head.as_ref().uri.clone(); diff --git a/actix-http/src/client/pool.rs b/actix-http/src/client/pool.rs index 3ce443794..f2c5b0410 100644 --- a/actix-http/src/client/pool.rs +++ b/actix-http/src/client/pool.rs @@ -69,10 +69,7 @@ where inner: Rc::downgrade(&inner_rc), }); - ConnectionPool( - connector_rc, - inner_rc, - ) + ConnectionPool(connector_rc, inner_rc) } } diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index f0a4b70bc..d0754ce88 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -964,7 +964,6 @@ impl ResponseError for actix::actors::resolver::ResolverError {} mod tests { use super::*; use http::{Error as HttpError, StatusCode}; - use httparse; use std::io; #[test] diff --git a/actix-http/src/extensions.rs b/actix-http/src/extensions.rs index 4e3918537..96e01767b 100644 --- a/actix-http/src/extensions.rs +++ b/actix-http/src/extensions.rs @@ -72,7 +72,7 @@ impl fmt::Debug for Extensions { #[cfg(test)] mod tests { use super::*; - + #[test] fn test_remove() { let mut map = Extensions::new(); diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index a9ab29881..00b36562e 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -132,19 +132,11 @@ where B: MessageBody, { fn is_empty(&self) -> bool { - if let State::None = self { - true - } else { - false - } + matches!(self, State::None) } fn is_call(&self) -> bool { - if let State::ServiceCall(_) = self { - true - } else { - false - } + matches!(self, State::ServiceCall(_)) } } enum PollResponse { diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index 33fb3a814..534ce928a 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -303,55 +303,59 @@ where } } }, - ServiceResponseStateProj::SendPayload(ref mut stream, ref mut body) => loop { + ServiceResponseStateProj::SendPayload(ref mut stream, ref mut body) => { loop { - if let Some(ref mut buffer) = this.buffer { - match stream.poll_capacity(cx) { - Poll::Pending => return Poll::Pending, - Poll::Ready(None) => return Poll::Ready(()), - Poll::Ready(Some(Ok(cap))) => { - let len = buffer.len(); - let bytes = buffer.split_to(std::cmp::min(cap, len)); + loop { + if let Some(ref mut buffer) = this.buffer { + match stream.poll_capacity(cx) { + Poll::Pending => return Poll::Pending, + Poll::Ready(None) => return Poll::Ready(()), + Poll::Ready(Some(Ok(cap))) => { + let len = buffer.len(); + let bytes = buffer.split_to(std::cmp::min(cap, len)); - if let Err(e) = stream.send_data(bytes, false) { + if let Err(e) = stream.send_data(bytes, false) { + warn!("{:?}", e); + return Poll::Ready(()); + } else if !buffer.is_empty() { + let cap = + std::cmp::min(buffer.len(), CHUNK_SIZE); + stream.reserve_capacity(cap); + } else { + this.buffer.take(); + } + } + Poll::Ready(Some(Err(e))) => { warn!("{:?}", e); return Poll::Ready(()); - } else if !buffer.is_empty() { - let cap = std::cmp::min(buffer.len(), CHUNK_SIZE); - stream.reserve_capacity(cap); - } else { - this.buffer.take(); } } - Poll::Ready(Some(Err(e))) => { - warn!("{:?}", e); - return Poll::Ready(()); - } - } - } else { - match body.as_mut().poll_next(cx) { - Poll::Pending => return Poll::Pending, - Poll::Ready(None) => { - if let Err(e) = stream.send_data(Bytes::new(), true) { - warn!("{:?}", e); + } else { + match body.as_mut().poll_next(cx) { + Poll::Pending => return Poll::Pending, + Poll::Ready(None) => { + if let Err(e) = stream.send_data(Bytes::new(), true) + { + warn!("{:?}", e); + } + return Poll::Ready(()); + } + Poll::Ready(Some(Ok(chunk))) => { + stream.reserve_capacity(std::cmp::min( + chunk.len(), + CHUNK_SIZE, + )); + *this.buffer = Some(chunk); + } + Poll::Ready(Some(Err(e))) => { + error!("Response payload stream error: {:?}", e); + return Poll::Ready(()); } - return Poll::Ready(()); - } - Poll::Ready(Some(Ok(chunk))) => { - stream.reserve_capacity(std::cmp::min( - chunk.len(), - CHUNK_SIZE, - )); - *this.buffer = Some(chunk); - } - Poll::Ready(Some(Err(e))) => { - error!("Response payload stream error: {:?}", e); - return Poll::Ready(()); } } } } - }, + } } } } diff --git a/actix-http/src/header/common/allow.rs b/actix-http/src/header/common/allow.rs index 432cc00d5..88c21763c 100644 --- a/actix-http/src/header/common/allow.rs +++ b/actix-http/src/header/common/allow.rs @@ -1,5 +1,5 @@ -use http::Method; use http::header; +use http::Method; header! { /// `Allow` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.4.1) diff --git a/actix-http/src/header/common/mod.rs b/actix-http/src/header/common/mod.rs index 08950ea8b..83489b864 100644 --- a/actix-http/src/header/common/mod.rs +++ b/actix-http/src/header/common/mod.rs @@ -9,11 +9,13 @@ pub use self::accept_charset::AcceptCharset; //pub use self::accept_encoding::AcceptEncoding; -pub use self::accept_language::AcceptLanguage; pub use self::accept::Accept; +pub use self::accept_language::AcceptLanguage; pub use self::allow::Allow; pub use self::cache_control::{CacheControl, CacheDirective}; -pub use self::content_disposition::{ContentDisposition, DispositionType, DispositionParam}; +pub use self::content_disposition::{ + ContentDisposition, DispositionParam, DispositionType, +}; pub use self::content_language::ContentLanguage; pub use self::content_range::{ContentRange, ContentRangeSpec}; pub use self::content_type::ContentType; @@ -47,7 +49,7 @@ macro_rules! __hyper__deref { &mut self.0 } } - } + }; } #[doc(hidden)] @@ -74,8 +76,8 @@ macro_rules! test_header { ($id:ident, $raw:expr) => { #[test] fn $id() { - use $crate::test; use super::*; + use $crate::test; let raw = $raw; let a: Vec> = raw.iter().map(|x| x.to_vec()).collect(); @@ -118,7 +120,7 @@ macro_rules! test_header { // Test formatting if typed.is_some() { let raw = &($raw)[..]; - let mut iter = raw.iter().map(|b|str::from_utf8(&b[..]).unwrap()); + let mut iter = raw.iter().map(|b| str::from_utf8(&b[..]).unwrap()); let mut joined = String::new(); joined.push_str(iter.next().unwrap()); for s in iter { @@ -128,7 +130,7 @@ macro_rules! test_header { assert_eq!(format!("{}", typed.unwrap()), joined); } } - } + }; } #[macro_export] @@ -330,11 +332,10 @@ macro_rules! header { }; } - mod accept_charset; //mod accept_encoding; -mod accept_language; mod accept; +mod accept_language; mod allow; mod cache_control; mod content_disposition; diff --git a/actix-http/src/httpmessage.rs b/actix-http/src/httpmessage.rs index e1c4136b0..471fbbcdc 100644 --- a/actix-http/src/httpmessage.rs +++ b/actix-http/src/httpmessage.rs @@ -167,7 +167,6 @@ where mod tests { use bytes::Bytes; use encoding_rs::ISO_8859_2; - use mime; use super::*; use crate::test::TestRequest; diff --git a/actix-http/src/ws/mask.rs b/actix-http/src/ws/mask.rs index 7eb5d148f..367fb0212 100644 --- a/actix-http/src/ws/mask.rs +++ b/actix-http/src/ws/mask.rs @@ -139,7 +139,7 @@ mod tests { let mut masked = unmasked.clone(); apply_mask_fallback(&mut masked[1..], &mask); - let mut masked_fast = unmasked.clone(); + let mut masked_fast = unmasked; apply_mask(&mut masked_fast[1..], mask_u32); assert_eq!(masked, masked_fast); diff --git a/actix-http/tests/test_openssl.rs b/actix-http/tests/test_openssl.rs index 3a7bfa409..795deacdc 100644 --- a/actix-http/tests/test_openssl.rs +++ b/actix-http/tests/test_openssl.rs @@ -274,9 +274,7 @@ async fn test_h2_head_empty() { async fn test_h2_head_binary() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| { - ok::<_, ()>(Response::Ok().body(STR)) - }) + .h2(|_| ok::<_, ()>(Response::Ok().body(STR))) .openssl(ssl_acceptor()) .map_err(|_| ()) }) diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index 465cba6df..beae359d9 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -280,9 +280,7 @@ async fn test_h2_head_empty() { async fn test_h2_head_binary() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| { - ok::<_, ()>(Response::Ok().body(STR)) - }) + .h2(|_| ok::<_, ()>(Response::Ok().body(STR))) .rustls(ssl_acceptor()) }) .await; diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index bee5ebef2..0375b6f66 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -489,9 +489,7 @@ async fn test_h1_head_empty() { async fn test_h1_head_binary() { let mut srv = test_server(|| { HttpService::build() - .h1(|_| { - ok::<_, ()>(Response::Ok().body(STR)) - }) + .h1(|_| ok::<_, ()>(Response::Ok().body(STR))) .tcp() }) .await; diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs index f96a7821a..449c7da29 100644 --- a/actix-multipart/src/server.rs +++ b/actix-multipart/src/server.rs @@ -9,8 +9,6 @@ use std::{cmp, fmt}; use bytes::{Bytes, BytesMut}; use futures_util::stream::{LocalBoxStream, Stream, StreamExt}; -use httparse; -use mime; use actix_utils::task::LocalWaker; use actix_web::error::{ParseError, PayloadError}; @@ -876,11 +874,11 @@ mod tests { impl SlowStream { fn new(bytes: Bytes) -> SlowStream { - return SlowStream { - bytes: bytes, + SlowStream { + bytes, pos: 0, ready: false, - }; + } } } diff --git a/actix-web-actors/tests/test_ws.rs b/actix-web-actors/tests/test_ws.rs index 25977c2c2..dda9f6f0b 100644 --- a/actix-web-actors/tests/test_ws.rs +++ b/actix-web-actors/tests/test_ws.rs @@ -30,8 +30,8 @@ impl StreamHandler> for Ws { async fn test_simple() { let mut srv = test::start(|| { App::new().service(web::resource("/").to( - |req: HttpRequest, stream: web::Payload| { - async move { ws::start(Ws, &req, stream) } + |req: HttpRequest, stream: web::Payload| async move { + ws::start(Ws, &req, stream) }, )) }); @@ -51,7 +51,7 @@ async fn test_simple() { .await .unwrap(); let item = framed.next().await.unwrap().unwrap(); - assert_eq!(item, ws::Frame::Binary(Bytes::from_static(b"text").into())); + assert_eq!(item, ws::Frame::Binary(Bytes::from_static(b"text"))); framed.send(ws::Message::Ping("text".into())).await.unwrap(); let item = framed.next().await.unwrap().unwrap(); diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index 7e3d43f1d..676e75e07 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -3,7 +3,7 @@ extern crate proc_macro; use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{format_ident, quote, ToTokens, TokenStreamExt}; -use syn::{AttributeArgs, Ident, NestedMeta, parse_macro_input}; +use syn::{parse_macro_input, AttributeArgs, Ident, NestedMeta}; enum ResourceType { Async, @@ -196,7 +196,12 @@ impl ToTokens for Route { name, guard, ast, - args: Args { path, guards, wrappers }, + args: + Args { + path, + guards, + wrappers, + }, resource_type, } = self; let resource_name = name.to_string(); diff --git a/actix-web-codegen/tests/test_macro.rs b/actix-web-codegen/tests/test_macro.rs index 0ef7e1c75..13e9120f6 100644 --- a/actix-web-codegen/tests/test_macro.rs +++ b/actix-web-codegen/tests/test_macro.rs @@ -2,11 +2,11 @@ use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; -use actix_web::{http, test, web::Path, App, HttpResponse, Responder, Error}; -use actix_web::dev::{Service, Transform, ServiceRequest, ServiceResponse}; +use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; +use actix_web::http::header::{HeaderName, HeaderValue}; +use actix_web::{http, test, web::Path, App, Error, HttpResponse, Responder}; use actix_web_codegen::{connect, delete, get, head, options, patch, post, put, trace}; use futures_util::future; -use actix_web::http::header::{HeaderName, HeaderValue}; // Make sure that we can name function as 'config' #[get("/config")] @@ -112,6 +112,7 @@ where type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; + #[allow(clippy::type_complexity)] type Future = Pin>>>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { @@ -119,7 +120,6 @@ where } fn call(&mut self, req: ServiceRequest) -> Self::Future { - let fut = self.service.call(req); Box::pin(async move { @@ -223,10 +223,7 @@ async fn test_auto_async() { #[actix_rt::test] async fn test_wrap() { - let srv = test::start(|| { - App::new() - .service(get_wrap) - }); + let srv = test::start(|| App::new().service(get_wrap)); let request = srv.request(http::Method::GET, srv.url("/test/wrap")); let response = request.send().await.unwrap(); diff --git a/awc/src/response.rs b/awc/src/response.rs index d1490cb98..8364aa556 100644 --- a/awc/src/response.rs +++ b/awc/src/response.rs @@ -402,9 +402,12 @@ mod tests { fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { match err { - JsonPayloadError::Payload(PayloadError::Overflow) => - matches!(other, JsonPayloadError::Payload(PayloadError::Overflow)), - JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType), + JsonPayloadError::Payload(PayloadError::Overflow) => { + matches!(other, JsonPayloadError::Payload(PayloadError::Overflow)) + } + JsonPayloadError::ContentType => { + matches!(other, JsonPayloadError::ContentType) + } _ => false, } } diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index cc61f1006..21be155e8 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -167,8 +167,7 @@ async fn test_connection_reuse() { }) .and_then( HttpService::new(map_config( - App::new() - .service(web::resource("/").route(web::to(|| HttpResponse::Ok()))), + App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))), |_| AppConfig::default(), )) .tcp(), @@ -205,8 +204,7 @@ async fn test_connection_force_close() { }) .and_then( HttpService::new(map_config( - App::new() - .service(web::resource("/").route(web::to(|| HttpResponse::Ok()))), + App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))), |_| AppConfig::default(), )) .tcp(), diff --git a/awc/tests/test_connector.rs b/awc/tests/test_connector.rs index b352eaab4..633ac2d50 100644 --- a/awc/tests/test_connector.rs +++ b/awc/tests/test_connector.rs @@ -32,8 +32,7 @@ async fn test_connection_window_size() { let srv = test_server(move || { HttpService::build() .h2(map_config( - App::new() - .service(web::resource("/").route(web::to(|| HttpResponse::Ok()))), + App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))), |_| AppConfig::default(), )) .openssl(ssl_acceptor()) diff --git a/awc/tests/test_rustls_client.rs b/awc/tests/test_rustls_client.rs index 0c6be76d4..8fb43c439 100644 --- a/awc/tests/test_rustls_client.rs +++ b/awc/tests/test_rustls_client.rs @@ -64,9 +64,8 @@ async fn _test_connection_reuse_h2() { .and_then( HttpService::build() .h2(map_config( - App::new().service( - web::resource("/").route(web::to(|| HttpResponse::Ok())), - ), + App::new() + .service(web::resource("/").route(web::to(HttpResponse::Ok))), |_| AppConfig::default(), )) .openssl(ssl_acceptor()) diff --git a/awc/tests/test_ssl_client.rs b/awc/tests/test_ssl_client.rs index b2a2e1785..ca65fb248 100644 --- a/awc/tests/test_ssl_client.rs +++ b/awc/tests/test_ssl_client.rs @@ -45,9 +45,8 @@ async fn test_connection_reuse_h2() { .and_then( HttpService::build() .h2(map_config( - App::new().service( - web::resource("/").route(web::to(|| HttpResponse::Ok())), - ), + App::new() + .service(web::resource("/").route(web::to(HttpResponse::Ok))), |_| AppConfig::default(), )) .openssl(ssl_acceptor()) diff --git a/src/app.rs b/src/app.rs index ae3d9fdf0..fdedb0a75 100644 --- a/src/app.rs +++ b/src/app.rs @@ -489,7 +489,7 @@ mod tests { #[actix_rt::test] async fn test_default_resource() { let mut srv = init_service( - App::new().service(web::resource("/test").to(|| HttpResponse::Ok())), + App::new().service(web::resource("/test").to(HttpResponse::Ok)), ) .await; let req = TestRequest::with_uri("/test").to_request(); @@ -502,13 +502,13 @@ mod tests { let mut srv = init_service( App::new() - .service(web::resource("/test").to(|| HttpResponse::Ok())) + .service(web::resource("/test").to(HttpResponse::Ok)) .service( web::resource("/test2") .default_service(|r: ServiceRequest| { ok(r.into_response(HttpResponse::Created())) }) - .route(web::get().to(|| HttpResponse::Ok())), + .route(web::get().to(HttpResponse::Ok)), ) .default_service(|r: ServiceRequest| { ok(r.into_response(HttpResponse::MethodNotAllowed())) @@ -585,7 +585,7 @@ mod tests { DefaultHeaders::new() .header(header::CONTENT_TYPE, HeaderValue::from_static("0001")), ) - .route("/test", web::get().to(|| HttpResponse::Ok())), + .route("/test", web::get().to(HttpResponse::Ok)), ) .await; let req = TestRequest::with_uri("/test").to_request(); @@ -601,7 +601,7 @@ mod tests { async fn test_router_wrap() { let mut srv = init_service( App::new() - .route("/test", web::get().to(|| HttpResponse::Ok())) + .route("/test", web::get().to(HttpResponse::Ok)) .wrap( DefaultHeaders::new() .header(header::CONTENT_TYPE, HeaderValue::from_static("0001")), @@ -632,7 +632,7 @@ mod tests { Ok(res) } }) - .service(web::resource("/test").to(|| HttpResponse::Ok())), + .service(web::resource("/test").to(HttpResponse::Ok)), ) .await; let req = TestRequest::with_uri("/test").to_request(); @@ -648,7 +648,7 @@ mod tests { async fn test_router_wrap_fn() { let mut srv = init_service( App::new() - .route("/test", web::get().to(|| HttpResponse::Ok())) + .route("/test", web::get().to(HttpResponse::Ok)) .wrap_fn(|req, srv| { let fut = srv.call(req); async { @@ -679,10 +679,9 @@ mod tests { .route( "/test", web::get().to(|req: HttpRequest| { - HttpResponse::Ok().body(format!( - "{}", - req.url_for("youtube", &["12345"]).unwrap() - )) + HttpResponse::Ok().body( + req.url_for("youtube", &["12345"]).unwrap().to_string(), + ) }), ), ) diff --git a/src/app_service.rs b/src/app_service.rs index d41cee9fd..98d8c8a8d 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -475,7 +475,7 @@ mod tests { let mut app = init_service( App::new() .data(DropData(data.clone())) - .service(web::resource("/test").to(|| HttpResponse::Ok())), + .service(web::resource("/test").to(HttpResponse::Ok)), ) .await; let req = TestRequest::with_uri("/test").to_request(); diff --git a/src/config.rs b/src/config.rs index 19a5ccc7b..0f49288ec 100644 --- a/src/config.rs +++ b/src/config.rs @@ -311,10 +311,9 @@ mod tests { .route( "/test", web::get().to(|req: HttpRequest| { - HttpResponse::Ok().body(format!( - "{}", - req.url_for("youtube", &["12345"]).unwrap() - )) + HttpResponse::Ok().body( + req.url_for("youtube", &["12345"]).unwrap().to_string(), + ) }), ), ) @@ -330,9 +329,9 @@ mod tests { async fn test_service() { let mut srv = init_service(App::new().configure(|cfg| { cfg.service( - web::resource("/test").route(web::get().to(|| HttpResponse::Created())), + web::resource("/test").route(web::get().to(HttpResponse::Created)), ) - .route("/index.html", web::get().to(|| HttpResponse::Ok())); + .route("/index.html", web::get().to(HttpResponse::Ok)); })) .await; diff --git a/src/data.rs b/src/data.rs index 34ada863d..c50b30328 100644 --- a/src/data.rs +++ b/src/data.rs @@ -200,14 +200,14 @@ mod tests { #[actix_rt::test] async fn test_route_data_extractor() { - let mut srv = - init_service(App::new().service(web::resource("/").data(10usize).route( - web::get().to(|data: web::Data| { - let _ = data.clone(); - HttpResponse::Ok() - }), - ))) - .await; + let mut srv = init_service( + App::new().service( + web::resource("/") + .data(10usize) + .route(web::get().to(|_data: web::Data| HttpResponse::Ok())), + ), + ) + .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); @@ -233,7 +233,6 @@ mod tests { web::resource("/").data(10usize).route(web::get().to( |data: web::Data| { assert_eq!(**data, 10); - let _ = data.clone(); HttpResponse::Ok() }, )), diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index 57b640bdd..dc6468ab3 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -626,7 +626,7 @@ mod tests { Ok(()) }; let s = format!("{}", FormatDisplay(&render)); - assert!(s.contains(&format!("{}", now.format("%Y-%m-%dT%H:%M:%S")))); + assert!(s.contains(&now.format("%Y-%m-%dT%H:%M:%S"))); } #[actix_rt::test] diff --git a/src/middleware/normalize.rs b/src/middleware/normalize.rs index 3d5d30c80..a1021ed14 100644 --- a/src/middleware/normalize.rs +++ b/src/middleware/normalize.rs @@ -91,9 +91,9 @@ where // That approach fails when a trailing slash is added, // and a duplicate slash is removed, // since the length of the strings remains the same - // + // // For example, the path "/v1//s" will be normalized to "/v1/s/" - // Both of the paths have the same length, + // Both of the paths have the same length, // so the change can not be deduced from the length comparison if path != original_path { let mut parts = head.uri.clone().into_parts(); @@ -129,7 +129,7 @@ mod tests { let mut app = init_service( App::new() .wrap(NormalizePath::default()) - .service(web::resource("/v1/something/").to(|| HttpResponse::Ok())), + .service(web::resource("/v1/something/").to(HttpResponse::Ok)), ) .await; diff --git a/src/resource.rs b/src/resource.rs index 5da1de62f..dd9b23012 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -607,7 +607,7 @@ mod tests { header::CONTENT_TYPE, HeaderValue::from_static("0001"), )) - .route(web::get().to(|| HttpResponse::Ok())), + .route(web::get().to(HttpResponse::Ok)), ), ) .await; @@ -637,7 +637,7 @@ mod tests { }) } }) - .route(web::get().to(|| HttpResponse::Ok())), + .route(web::get().to(HttpResponse::Ok)), ), ) .await; @@ -684,9 +684,7 @@ mod tests { async fn test_default_resource() { let mut srv = init_service( App::new() - .service( - web::resource("/test").route(web::get().to(|| HttpResponse::Ok())), - ) + .service(web::resource("/test").route(web::get().to(HttpResponse::Ok))) .default_service(|r: ServiceRequest| { ok(r.into_response(HttpResponse::BadRequest())) }), @@ -705,7 +703,7 @@ mod tests { let mut srv = init_service( App::new().service( web::resource("/test") - .route(web::get().to(|| HttpResponse::Ok())) + .route(web::get().to(HttpResponse::Ok)) .default_service(|r: ServiceRequest| { ok(r.into_response(HttpResponse::BadRequest())) }), @@ -731,17 +729,17 @@ mod tests { .service( web::resource("/test/{p}") .guard(guard::Get()) - .to(|| HttpResponse::Ok()), + .to(HttpResponse::Ok), ) .service( web::resource("/test/{p}") .guard(guard::Put()) - .to(|| HttpResponse::Created()), + .to(HttpResponse::Created), ) .service( web::resource("/test/{p}") .guard(guard::Delete()) - .to(|| HttpResponse::NoContent()), + .to(HttpResponse::NoContent), ), ) .await; @@ -783,7 +781,8 @@ mod tests { data3: web::Data| { assert_eq!(**data1, 10); assert_eq!(**data2, '*'); - assert_eq!(**data3, 1.0); + let error = std::f64::EPSILON; + assert!((**data3 - 1.0).abs() < error); HttpResponse::Ok() }, ), diff --git a/src/responder.rs b/src/responder.rs index e102d23e1..fc80831b8 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -480,7 +480,7 @@ pub(crate) mod tests { assert_eq!(resp.status(), StatusCode::OK); match resp.response().body() { ResponseBody::Body(Body::Bytes(ref b)) => { - let bytes: Bytes = b.clone().into(); + let bytes = b.clone(); assert_eq!(bytes, Bytes::from_static(b"some")); } _ => panic!(), diff --git a/src/route.rs b/src/route.rs index b17fa9b06..3a833bdf3 100644 --- a/src/route.rs +++ b/src/route.rs @@ -362,7 +362,7 @@ mod tests { App::new() .service( web::resource("/test") - .route(web::get().to(|| HttpResponse::Ok())) + .route(web::get().to(HttpResponse::Ok)) .route(web::put().to(|| async { Err::(error::ErrorBadRequest("err")) })) diff --git a/src/scope.rs b/src/scope.rs index b9166a4bf..c4b01d266 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -678,12 +678,9 @@ mod tests { #[actix_rt::test] async fn test_scope() { - let mut srv = init_service( - App::new().service( - web::scope("/app") - .service(web::resource("/path1").to(|| HttpResponse::Ok())), - ), - ) + let mut srv = init_service(App::new().service( + web::scope("/app").service(web::resource("/path1").to(HttpResponse::Ok)), + )) .await; let req = TestRequest::with_uri("/app/path1").to_request(); @@ -696,8 +693,8 @@ mod tests { let mut srv = init_service( App::new().service( web::scope("/app") - .service(web::resource("").to(|| HttpResponse::Ok())) - .service(web::resource("/").to(|| HttpResponse::Created())), + .service(web::resource("").to(HttpResponse::Ok)) + .service(web::resource("/").to(HttpResponse::Created)), ), ) .await; @@ -714,7 +711,7 @@ mod tests { #[actix_rt::test] async fn test_scope_root2() { let mut srv = init_service(App::new().service( - web::scope("/app/").service(web::resource("").to(|| HttpResponse::Ok())), + web::scope("/app/").service(web::resource("").to(HttpResponse::Ok)), )) .await; @@ -730,7 +727,7 @@ mod tests { #[actix_rt::test] async fn test_scope_root3() { let mut srv = init_service(App::new().service( - web::scope("/app/").service(web::resource("/").to(|| HttpResponse::Ok())), + web::scope("/app/").service(web::resource("/").to(HttpResponse::Ok)), )) .await; @@ -748,8 +745,8 @@ mod tests { let mut srv = init_service( App::new().service( web::scope("app") - .route("/path1", web::get().to(|| HttpResponse::Ok())) - .route("/path1", web::delete().to(|| HttpResponse::Ok())), + .route("/path1", web::get().to(HttpResponse::Ok)) + .route("/path1", web::delete().to(HttpResponse::Ok)), ), ) .await; @@ -777,8 +774,8 @@ mod tests { App::new().service( web::scope("app").service( web::resource("path1") - .route(web::get().to(|| HttpResponse::Ok())) - .route(web::delete().to(|| HttpResponse::Ok())), + .route(web::get().to(HttpResponse::Ok)) + .route(web::delete().to(HttpResponse::Ok)), ), ), ) @@ -807,7 +804,7 @@ mod tests { App::new().service( web::scope("/app") .guard(guard::Get()) - .service(web::resource("/path1").to(|| HttpResponse::Ok())), + .service(web::resource("/path1").to(HttpResponse::Ok)), ), ) .await; @@ -842,7 +839,7 @@ mod tests { match resp.response().body() { ResponseBody::Body(Body::Bytes(ref b)) => { - let bytes: Bytes = b.clone().into(); + let bytes = b.clone(); assert_eq!(bytes, Bytes::from_static(b"project: project1")); } _ => panic!(), @@ -855,14 +852,9 @@ mod tests { #[actix_rt::test] async fn test_nested_scope() { - let mut srv = init_service( - App::new().service( - web::scope("/app") - .service(web::scope("/t1").service( - web::resource("/path1").to(|| HttpResponse::Created()), - )), - ), - ) + let mut srv = init_service(App::new().service(web::scope("/app").service( + web::scope("/t1").service(web::resource("/path1").to(HttpResponse::Created)), + ))) .await; let req = TestRequest::with_uri("/app/t1/path1").to_request(); @@ -872,14 +864,9 @@ mod tests { #[actix_rt::test] async fn test_nested_scope_no_slash() { - let mut srv = init_service( - App::new().service( - web::scope("/app") - .service(web::scope("t1").service( - web::resource("/path1").to(|| HttpResponse::Created()), - )), - ), - ) + let mut srv = init_service(App::new().service(web::scope("/app").service( + web::scope("t1").service(web::resource("/path1").to(HttpResponse::Created)), + ))) .await; let req = TestRequest::with_uri("/app/t1/path1").to_request(); @@ -893,8 +880,8 @@ mod tests { App::new().service( web::scope("/app").service( web::scope("/t1") - .service(web::resource("").to(|| HttpResponse::Ok())) - .service(web::resource("/").to(|| HttpResponse::Created())), + .service(web::resource("").to(HttpResponse::Ok)) + .service(web::resource("/").to(HttpResponse::Created)), ), ), ) @@ -916,7 +903,7 @@ mod tests { web::scope("/app").service( web::scope("/t1") .guard(guard::Get()) - .service(web::resource("/path1").to(|| HttpResponse::Ok())), + .service(web::resource("/path1").to(HttpResponse::Ok)), ), ), ) @@ -953,7 +940,7 @@ mod tests { match resp.response().body() { ResponseBody::Body(Body::Bytes(ref b)) => { - let bytes: Bytes = b.clone().into(); + let bytes = b.clone(); assert_eq!(bytes, Bytes::from_static(b"project: project_1")); } _ => panic!(), @@ -981,7 +968,7 @@ mod tests { match resp.response().body() { ResponseBody::Body(Body::Bytes(ref b)) => { - let bytes: Bytes = b.clone().into(); + let bytes = b.clone(); assert_eq!(bytes, Bytes::from_static(b"project: test - 1")); } _ => panic!(), @@ -997,7 +984,7 @@ mod tests { let mut srv = init_service( App::new().service( web::scope("/app") - .service(web::resource("/path1").to(|| HttpResponse::Ok())) + .service(web::resource("/path1").to(HttpResponse::Ok)) .default_service(|r: ServiceRequest| { ok(r.into_response(HttpResponse::BadRequest())) }), @@ -1018,9 +1005,10 @@ mod tests { async fn test_default_resource_propagation() { let mut srv = init_service( App::new() - .service(web::scope("/app1").default_service( - web::resource("").to(|| HttpResponse::BadRequest()), - )) + .service( + web::scope("/app1") + .default_service(web::resource("").to(HttpResponse::BadRequest)), + ) .service(web::scope("/app2")) .default_service(|r: ServiceRequest| { ok(r.into_response(HttpResponse::MethodNotAllowed())) @@ -1043,21 +1031,21 @@ mod tests { #[actix_rt::test] async fn test_middleware() { - let mut srv = - init_service( - App::new().service( - web::scope("app") - .wrap(DefaultHeaders::new().header( + let mut srv = init_service( + App::new().service( + web::scope("app") + .wrap( + DefaultHeaders::new().header( header::CONTENT_TYPE, HeaderValue::from_static("0001"), - )) - .service( - web::resource("/test") - .route(web::get().to(|| HttpResponse::Ok())), ), - ), - ) - .await; + ) + .service( + web::resource("/test").route(web::get().to(HttpResponse::Ok)), + ), + ), + ) + .await; let req = TestRequest::with_uri("/app/test").to_request(); let resp = call_service(&mut srv, req).await; @@ -1084,7 +1072,7 @@ mod tests { Ok(res) } }) - .route("/test", web::get().to(|| HttpResponse::Ok())), + .route("/test", web::get().to(HttpResponse::Ok)), ), ) .await; @@ -1105,7 +1093,6 @@ mod tests { "/t", web::get().to(|data: web::Data| { assert_eq!(**data, 10); - let _ = data.clone(); HttpResponse::Ok() }), ), @@ -1141,7 +1128,6 @@ mod tests { "/t", web::get().to(|data: web::Data| { assert_eq!(**data, 10); - let _ = data.clone(); HttpResponse::Ok() }), ), @@ -1157,7 +1143,7 @@ mod tests { async fn test_scope_config() { let mut srv = init_service(App::new().service(web::scope("/app").configure(|s| { - s.route("/path1", web::get().to(|| HttpResponse::Ok())); + s.route("/path1", web::get().to(HttpResponse::Ok)); }))) .await; @@ -1171,7 +1157,7 @@ mod tests { let mut srv = init_service(App::new().service(web::scope("/app").configure(|s| { s.service(web::scope("/v1").configure(|s| { - s.route("/", web::get().to(|| HttpResponse::Ok())); + s.route("/", web::get().to(HttpResponse::Ok)); })); }))) .await; @@ -1193,10 +1179,9 @@ mod tests { s.route( "/", web::get().to(|req: HttpRequest| async move { - HttpResponse::Ok().body(format!( - "{}", - req.url_for("youtube", &["xxxxxx"]).unwrap().as_str() - )) + HttpResponse::Ok().body( + req.url_for("youtube", &["xxxxxx"]).unwrap().to_string(), + ) }), ); })); diff --git a/src/test.rs b/src/test.rs index a64ec3a73..49c5cc214 100644 --- a/src/test.rs +++ b/src/test.rs @@ -23,7 +23,6 @@ use futures_util::future::ok; use futures_util::StreamExt; use serde::de::DeserializeOwned; use serde::Serialize; -use serde_json; use socket2::{Domain, Protocol, Socket, Type}; pub use actix_http::test::TestBuffer; diff --git a/src/types/form.rs b/src/types/form.rs index d7db595ca..ea061d553 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -407,9 +407,15 @@ mod tests { fn eq(err: UrlencodedError, other: UrlencodedError) -> bool { match err { - UrlencodedError::Overflow { .. } => matches!(other, UrlencodedError::Overflow { .. }), - UrlencodedError::UnknownLength => matches!(other, UrlencodedError::UnknownLength), - UrlencodedError::ContentType => matches!(other, UrlencodedError::ContentType), + UrlencodedError::Overflow { .. } => { + matches!(other, UrlencodedError::Overflow { .. }) + } + UrlencodedError::UnknownLength => { + matches!(other, UrlencodedError::UnknownLength) + } + UrlencodedError::ContentType => { + matches!(other, UrlencodedError::ContentType) + } _ => false, } } diff --git a/src/types/json.rs b/src/types/json.rs index 1665ca95a..527b4b611 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -434,7 +434,9 @@ mod tests { fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { match err { JsonPayloadError::Overflow => matches!(other, JsonPayloadError::Overflow), - JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType), + JsonPayloadError::ContentType => { + matches!(other, JsonPayloadError::ContentType) + } _ => false, } } @@ -480,7 +482,7 @@ mod tests { .to_http_parts(); let s = Json::::from_request(&req, &mut pl).await; - let mut resp = Response::from_error(s.err().unwrap().into()); + let mut resp = Response::from_error(s.err().unwrap()); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let body = load_stream(resp.take_body()).await.unwrap(); diff --git a/tests/test_server.rs b/tests/test_server.rs index 0ac4b0232..fa8a93f06 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -851,7 +851,7 @@ async fn test_slow_request() { use std::net; let srv = test::start_with(test::config().client_timeout(200), || { - App::new().service(web::resource("/").route(web::to(|| HttpResponse::Ok()))) + App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))) }); let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); From cf1c8abe62d1d94e86c6a62ff8aa9fd6f23bc88a Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 22 Jul 2020 01:13:10 +0100 Subject: [PATCH 30/78] prepare release http & awc (#1617) --- actix-http/CHANGES.md | 11 ++++++----- actix-http/Cargo.toml | 2 +- awc/CHANGES.md | 8 ++++++++ awc/Cargo.toml | 17 ++++++++++------- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index a4bf7be93..b2b816d54 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,18 +1,19 @@ # Changes -## [Unreleased] - xxx +## Unreleased - 2020-xx-xx + + +## 2.0.0-beta.2 - 2020-07-21 ### Fixed * Potential UB in h1 decoder using uninitialized memory. [#1614] -[#1614]: https://github.com/actix/actix-web/pull/1614 - - ### Changed - * Fix illegal chunked encoding. [#1615] +[#1614]: https://github.com/actix/actix-web/pull/1614 [#1615]: https://github.com/actix/actix-web/pull/1615 + ## [2.0.0-beta.1] - 2020-07-11 ### Changed diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index bbb2a2143..7f07f67c3 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "2.0.0-beta.1" +version = "2.0.0-beta.2" authors = ["Nikolay Kim "] description = "Actix HTTP primitives" readme = "README.md" diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 19154d35d..3d2806a9f 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,5 +1,13 @@ # Changes +## Unreleased - 2020-xx-xx + + +## 2.0.0-beta.2 - 2020-07-21 +### Changed +* Update `actix-http` dependency to 2.0.0-beta.2 + + ## [2.0.0-beta.1] - 2020-07-14 ### Changed * Update `actix-http` dependency to 2.0.0-beta.1 diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 73dc6e0e5..54db46901 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,16 +1,19 @@ [package] name = "awc" -version = "2.0.0-beta.1" +version = "2.0.0-beta.2" authors = ["Nikolay Kim "] -description = "Actix http client." +description = "Async HTTP client library that uses the Actix runtime." readme = "README.md" keywords = ["actix", "http", "framework", "async", "web"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" documentation = "https://docs.rs/awc/" -categories = ["network-programming", "asynchronous", - "web-programming::http-client", - "web-programming::websocket"] +categories = [ + "network-programming", + "asynchronous", + "web-programming::http-client", + "web-programming::websocket", +] license = "MIT OR Apache-2.0" edition = "2018" @@ -36,7 +39,7 @@ compress = ["actix-http/compress"] [dependencies] actix-codec = "0.2.0" actix-service = "1.0.1" -actix-http = "2.0.0-beta.1" +actix-http = "2.0.0-beta.2" actix-rt = "1.0.0" base64 = "0.12" @@ -56,7 +59,7 @@ rust-tls = { version = "0.17.0", package = "rustls", optional = true, features = [dev-dependencies] actix-connect = { version = "2.0.0-alpha.2", features = ["openssl"] } actix-web = { version = "3.0.0-alpha.3", features = ["openssl"] } -actix-http = { version = "2.0.0-beta.1", features = ["openssl"] } +actix-http = { version = "2.0.0-beta.2", features = ["openssl"] } actix-http-test = { version = "2.0.0-alpha.1", features = ["openssl"] } actix-utils = "1.0.3" actix-server = "1.0.0" From a78380739e85049c0ef890deb6fcb0cd8de9bc8b Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sun, 9 Aug 2020 13:32:37 +0100 Subject: [PATCH 31/78] require rustls feature for client example (#1625) --- .gitignore | 1 + Cargo.toml | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 42d0755dd..11a3b5f37 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ guide/build/ *.pid *.sock *~ +.DS_Store # These are backup files generated by rustfmt **/*.rs.bk diff --git a/Cargo.toml b/Cargo.toml index 49de9fd96..afaeac09d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,6 +124,10 @@ actix-files = { path = "actix-files" } actix-multipart = { path = "actix-multipart" } awc = { path = "awc" } +[[example]] +name = "client" +required-features = ["rustls"] + [[bench]] name = "server" harness = false From 46627be36ff06aa42e907f1dc223b09b3f93387c Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sun, 9 Aug 2020 13:54:35 +0100 Subject: [PATCH 32/78] add dep graph dot graphs (#1601) --- docs/graphs/.gitignore | 2 ++ docs/graphs/dependency-graphs.md | 11 +++++++++++ docs/graphs/net-only.dot | 25 +++++++++++++++++++++++++ docs/graphs/web-focus.dot | 30 ++++++++++++++++++++++++++++++ docs/graphs/web-only.dot | 19 +++++++++++++++++++ 5 files changed, 87 insertions(+) create mode 100644 docs/graphs/.gitignore create mode 100644 docs/graphs/dependency-graphs.md create mode 100644 docs/graphs/net-only.dot create mode 100644 docs/graphs/web-focus.dot create mode 100644 docs/graphs/web-only.dot diff --git a/docs/graphs/.gitignore b/docs/graphs/.gitignore new file mode 100644 index 000000000..284a286c9 --- /dev/null +++ b/docs/graphs/.gitignore @@ -0,0 +1,2 @@ +# do not track rendered graphs +*.png diff --git a/docs/graphs/dependency-graphs.md b/docs/graphs/dependency-graphs.md new file mode 100644 index 000000000..6a4392d6b --- /dev/null +++ b/docs/graphs/dependency-graphs.md @@ -0,0 +1,11 @@ +# Actix Ecosystem Dependency Graphs + +See rendered versions of these dot graphs [on the wiki](https://github.com/actix/actix-web/wiki/Dependency-Graph). + +## Rendering + +Dot graphs were rendered using the `dot` command from [GraphViz](https://www.graphviz.org/doc/info/command.html): + +```sh +for f in $(ls docs/graphs/*.dot | xargs); do dot $f -Tpng -o${f:r}.png; done +``` diff --git a/docs/graphs/net-only.dot b/docs/graphs/net-only.dot new file mode 100644 index 000000000..d9f2317a1 --- /dev/null +++ b/docs/graphs/net-only.dot @@ -0,0 +1,25 @@ +digraph { + subgraph cluster_net { + label="actix/actix-net"; + "actix-codec" + "actix-connect" + "actix-macros" + "actix-rt" + "actix-server" + "actix-service" + "actix-testing" + "actix-threadpool" + "actix-tls" + "actix-tracing" + "actix-utils" + "actix-router" + } + + "actix-utils" -> { "actix-service" "actix-rt" "actix-codec" } + "actix-tracing" -> { "actix-service" } + "actix-tls" -> { "actix-service" "actix-codec" "actix-utils" "actix-rt" } + "actix-testing" -> { "actix-rt" "actix-macros" "actix-server" "actix-service" } + "actix-server" -> { "actix-service" "actix-rt" "actix-codec" "actix-utils" } + "actix-rt" -> { "actix-macros" "actix-threadpool" } + "actix-connect" -> { "actix-service" "actix-codec" "actix-utils" "actix-rt" } +} diff --git a/docs/graphs/web-focus.dot b/docs/graphs/web-focus.dot new file mode 100644 index 000000000..b0ce18d02 --- /dev/null +++ b/docs/graphs/web-focus.dot @@ -0,0 +1,30 @@ +digraph { + subgraph cluster_web { + label="actix/actix-web" + "awc" + "actix-web" + "actix-files" + "actix-http" + "actix-multipart" + "actix-web-actors" + "actix-web-codegen" + } + + "actix-web" -> { "actix-codec" "actix-service" "actix-utils" "actix-router" "actix-rt" "actix-server" "actix-testing" "actix-macros" "actix-threadpool" "actix-tls" "actix-web-codegen" "actix-http" "awc" } + "awc" -> { "actix-codec" "actix-service" "actix-http" "actix-rt" } + "actix-web-actors" -> { "actix" "actix-web" "actix-http" "actix-codec" } + "actix-multipart" -> { "actix-web" "actix-service" "actix-utils" } + "actix-http" -> { "actix-service" "actix-codec" "actix-connect" "actix-utils" "actix-rt" "actix-threadpool" } + "actix-http" -> { "actix" "actix-tls" }[color=blue] // optional + "actix-files" -> { "actix-web" "actix-http" } + + // net + + "actix-utils" -> { "actix-service" "actix-rt" "actix-codec" } + "actix-tracing" -> { "actix-service" } + "actix-tls" -> { "actix-service" "actix-codec" "actix-utils" "actix-rt" } + "actix-testing" -> { "actix-rt" "actix-macros" "actix-server" "actix-service" } + "actix-server" -> { "actix-service" "actix-rt" "actix-codec" "actix-utils" } + "actix-rt" -> { "actix-macros" "actix-threadpool" } + "actix-connect" -> { "actix-service" "actix-codec" "actix-utils" "actix-rt" } +} diff --git a/docs/graphs/web-only.dot b/docs/graphs/web-only.dot new file mode 100644 index 000000000..6e41fdc27 --- /dev/null +++ b/docs/graphs/web-only.dot @@ -0,0 +1,19 @@ +digraph { + subgraph cluster_web { + label="actix/actix-web" + "awc" + "actix-web" + "actix-files" + "actix-http" + "actix-multipart" + "actix-web-actors" + "actix-web-codegen" + } + + "actix-web" -> { "actix-web-codegen" "actix-http" "awc" } + "awc" -> { "actix-http" } + "actix-web-actors" -> { "actix" "actix-web" "actix-http" } + "actix-multipart" -> { "actix-web" } + "actix-http" -> { "actix" }[color=blue] // optional + "actix-files" -> { "actix-web" "actix-http" } +} From 187646b2f9900ec56cbec6b4d1b168b1d0e78058 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sun, 9 Aug 2020 15:51:38 +0100 Subject: [PATCH 33/78] match HttpRequest app_data behavior in ServiceRequest (#1618) --- CHANGES.md | 3 +++ src/service.rs | 32 ++++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d80e87946..adc343050 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,8 @@ using `App::data`. [#1610] * `web::Path` now has a public representation: `web::Path(pub T)` that enables destructuring. [#1594] +* `ServiceRequest::app_data` allows retrieval of non-Data data without splitting into parts to + access `HttpRequest` which already allows this. [#1618] * MSRV is now 1.42.0. ### Fixed @@ -14,6 +16,7 @@ [#1594]: https://github.com/actix/actix-web/pull/1594 [#1609]: https://github.com/actix/actix-web/pull/1609 [#1610]: https://github.com/actix/actix-web/pull/1610 +[#1618]: https://github.com/actix/actix-web/pull/1610 ## 3.0.0-beta.1 - 2020-07-13 diff --git a/src/service.rs b/src/service.rs index cba852a9c..a861ba38c 100644 --- a/src/service.rs +++ b/src/service.rs @@ -12,7 +12,6 @@ use actix_router::{IntoPattern, Path, Resource, ResourceDef, Url}; use actix_service::{IntoServiceFactory, ServiceFactory}; use crate::config::{AppConfig, AppService}; -use crate::data::Data; use crate::dev::insert_slash; use crate::guard::Guard; use crate::info::ConnectionInfo; @@ -226,12 +225,11 @@ impl ServiceRequest { self.0.app_config() } - /// Get an application data stored with `App::data()` method during - /// application configuration. - pub fn app_data(&self) -> Option> { + /// Counterpart to [`HttpRequest::app_data`](../struct.HttpRequest.html#method.app_data). + pub fn app_data(&self) -> Option<&T> { for container in (self.0).0.app_data.iter().rev() { - if let Some(data) = container.get::>() { - return Some(Data::clone(&data)); + if let Some(data) = container.get::() { + return Some(data); } } @@ -595,6 +593,28 @@ mod tests { let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), http::StatusCode::NOT_FOUND); } + + #[actix_rt::test] + async fn test_service_data() { + let mut srv = init_service( + App::new() + .data(42u32) + .service(web::service("/test").name("test").finish( + |req: ServiceRequest| { + assert_eq!( + req.app_data::>().unwrap().as_ref(), + &42 + ); + ok(req.into_response(HttpResponse::Ok().finish())) + }, + )), + ) + .await; + let req = TestRequest::with_uri("/test").to_request(); + let resp = srv.call(req).await.unwrap(); + assert_eq!(resp.status(), http::StatusCode::OK); + } + #[test] fn test_fmt_debug() { let req = TestRequest::get() From 160995b8d49b8f1e556e4944617c9982f1ac2ad8 Mon Sep 17 00:00:00 2001 From: fakeshadow <24548779@qq.com> Date: Mon, 10 Aug 2020 04:49:43 +0800 Subject: [PATCH 34/78] fix awc pool leak (#1626) --- actix-http/CHANGES.md | 6 +- actix-http/src/client/pool.rs | 103 ++++++++++++++++++---------------- 2 files changed, 61 insertions(+), 48 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index b2b816d54..8d44ea6b9 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,9 +1,13 @@ # Changes ## Unreleased - 2020-xx-xx +### Fixed +* Memory leak of `client::pool::ConnectorPoolSupport`. [#1626] + +[#1626]: https://github.com/actix/actix-web/pull/1626 -## 2.0.0-beta.2 - 2020-07-21 +## [2.0.0-beta.2] - 2020-07-21 ### Fixed * Potential UB in h1 decoder using uninitialized memory. [#1614] diff --git a/actix-http/src/client/pool.rs b/actix-http/src/client/pool.rs index f2c5b0410..013a79671 100644 --- a/actix-http/src/client/pool.rs +++ b/actix-http/src/client/pool.rs @@ -2,7 +2,7 @@ use std::cell::RefCell; use std::collections::VecDeque; use std::future::Future; use std::pin::Pin; -use std::rc::{Rc, Weak}; +use std::rc::Rc; use std::task::{Context, Poll}; use std::time::{Duration, Instant}; @@ -65,8 +65,8 @@ where // start support future actix_rt::spawn(ConnectorPoolSupport { - connector: connector_rc.clone(), - inner: Rc::downgrade(&inner_rc), + connector: Rc::clone(&connector_rc), + inner: Rc::clone(&inner_rc), }); ConnectionPool(connector_rc, inner_rc) @@ -82,6 +82,13 @@ where } } +impl Drop for ConnectionPool { + fn drop(&mut self) { + // wake up the ConnectorPoolSupport when dropping so it can exit properly. + self.1.borrow().waker.wake(); + } +} + impl Service for ConnectionPool where Io: AsyncRead + AsyncWrite + Unpin + 'static, @@ -421,7 +428,7 @@ where Io: AsyncRead + AsyncWrite + Unpin + 'static, { connector: T, - inner: Weak>>, + inner: Rc>>, } impl Future for ConnectorPoolSupport @@ -435,55 +442,57 @@ where fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); - if let Some(this_inner) = this.inner.upgrade() { - let mut inner = this_inner.as_ref().borrow_mut(); - inner.waker.register(cx.waker()); + if Rc::strong_count(this.inner) == 1 { + // If we are last copy of Inner it means the ConnectionPool is already gone + // and we are safe to exit. + return Poll::Ready(()); + } - // check waiters - loop { - let (key, token) = { - if let Some((key, token)) = inner.waiters_queue.get_index(0) { - (key.clone(), *token) - } else { - break; - } - }; - if inner.waiters.get(token).unwrap().is_none() { - continue; - } + let mut inner = this.inner.borrow_mut(); + inner.waker.register(cx.waker()); - match inner.acquire(&key, cx) { - Acquire::NotAvailable => break, - Acquire::Acquired(io, created) => { - let tx = inner.waiters.get_mut(token).unwrap().take().unwrap().1; - if let Err(conn) = tx.send(Ok(IoConnection::new( - io, - created, - Some(Acquired(key.clone(), Some(this_inner.clone()))), - ))) { - let (io, created) = conn.unwrap().into_inner(); - inner.release_conn(&key, io, created); - } - } - Acquire::Available => { - let (connect, tx) = - inner.waiters.get_mut(token).unwrap().take().unwrap(); - OpenWaitingConnection::spawn( - key.clone(), - tx, - this_inner.clone(), - this.connector.call(connect), - inner.config.clone(), - ); - } + // check waiters + loop { + let (key, token) = { + if let Some((key, token)) = inner.waiters_queue.get_index(0) { + (key.clone(), *token) + } else { + break; } - let _ = inner.waiters_queue.swap_remove_index(0); + }; + if inner.waiters.get(token).unwrap().is_none() { + continue; } - Poll::Pending - } else { - Poll::Ready(()) + match inner.acquire(&key, cx) { + Acquire::NotAvailable => break, + Acquire::Acquired(io, created) => { + let tx = inner.waiters.get_mut(token).unwrap().take().unwrap().1; + if let Err(conn) = tx.send(Ok(IoConnection::new( + io, + created, + Some(Acquired(key.clone(), Some(this.inner.clone()))), + ))) { + let (io, created) = conn.unwrap().into_inner(); + inner.release_conn(&key, io, created); + } + } + Acquire::Available => { + let (connect, tx) = + inner.waiters.get_mut(token).unwrap().take().unwrap(); + OpenWaitingConnection::spawn( + key.clone(), + tx, + this.inner.clone(), + this.connector.call(connect), + inner.config.clone(), + ); + } + } + let _ = inner.waiters_queue.swap_remove_index(0); } + + Poll::Pending } } From 6e97bc09f8b4f9f62d08286e557a73ecce76529b Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Thu, 13 Aug 2020 16:04:50 +0900 Subject: [PATCH 35/78] Use action to upload docs --- .github/workflows/bench.yml | 2 +- .github/workflows/linux.yml | 2 +- .github/workflows/macos.yml | 2 +- .github/workflows/upload-doc.yml | 12 +++++++----- .github/workflows/windows.yml | 2 +- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 754091dde..d19471a16 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@master + - uses: actions/checkout@v2 - name: Install Rust uses: actions-rs/toolchain@v1 diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index d49874ce3..a068070ff 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@master + - uses: actions/checkout@v2 - name: Install ${{ matrix.version }} uses: actions-rs/toolchain@v1 diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 0683fcc8c..dc8558ac1 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -20,7 +20,7 @@ jobs: runs-on: macOS-latest steps: - - uses: actions/checkout@master + - uses: actions/checkout@v2 - name: Install ${{ matrix.version }} uses: actions-rs/toolchain@v1 diff --git a/.github/workflows/upload-doc.yml b/.github/workflows/upload-doc.yml index 75d534b28..99fa663cc 100644 --- a/.github/workflows/upload-doc.yml +++ b/.github/workflows/upload-doc.yml @@ -11,7 +11,7 @@ jobs: if: github.repository == 'actix/actix-web' steps: - - uses: actions/checkout@master + - uses: actions/checkout@v2 - name: Install Rust uses: actions-rs/toolchain@v1 @@ -29,7 +29,9 @@ jobs: - name: Tweak HTML run: echo "" > target/doc/index.html - - name: Upload documentation - run: | - git clone https://github.com/davisp/ghp-import.git - ./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://${{ secrets.GITHUB_TOKEN }}@github.com/"${{ github.repository }}.git" target/doc + - name: Deploy to GitHub Pages + uses: JamesIves/github-pages-deploy-action@3.5.8 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: gh-pages + FOLDER: target/doc diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index c09c7cea3..d53d50a61 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -23,7 +23,7 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@master + - uses: actions/checkout@v2 - name: Install ${{ matrix.version }} uses: actions-rs/toolchain@v1 From 5aad8e24c74beb4c5c2b0df118a67f5005b5cc23 Mon Sep 17 00:00:00 2001 From: William Myers Date: Thu, 13 Aug 2020 17:24:35 -0700 Subject: [PATCH 36/78] Re-export all error types from awc (#1621) --- CHANGES.md | 4 +++- src/lib.rs | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index adc343050..2b079ab03 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ destructuring. [#1594] * `ServiceRequest::app_data` allows retrieval of non-Data data without splitting into parts to access `HttpRequest` which already allows this. [#1618] +* Re-export all error types from `awc`. [#1621] * MSRV is now 1.42.0. ### Fixed @@ -16,7 +17,8 @@ [#1594]: https://github.com/actix/actix-web/pull/1594 [#1609]: https://github.com/actix/actix-web/pull/1609 [#1610]: https://github.com/actix/actix-web/pull/1610 -[#1618]: https://github.com/actix/actix-web/pull/1610 +[#1618]: https://github.com/actix/actix-web/pull/1618 +[#1621]: https://github.com/actix/actix-web/pull/1621 ## 3.0.0-beta.1 - 2020-07-13 diff --git a/src/lib.rs b/src/lib.rs index 8776a62b5..3f65e49dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -213,9 +213,7 @@ pub mod client { //! } //! ``` - pub use awc::error::{ - ConnectError, InvalidUrl, PayloadError, SendRequestError, WsClientError, - }; + pub use awc::error::*; pub use awc::{ test, Client, ClientBuilder, ClientRequest, ClientResponse, Connector, }; From aa2bd6fbfbe104c2e2a1aef2f8d5a7d8d62f6e95 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Fri, 14 Aug 2020 19:42:14 +0900 Subject: [PATCH 37/78] http: Bump up to 2.0.0-beta.3 (#1630) --- Cargo.toml | 2 +- actix-files/Cargo.toml | 2 +- actix-http/CHANGES.md | 3 ++- actix-http/Cargo.toml | 2 +- actix-multipart/Cargo.toml | 2 +- actix-web-actors/Cargo.toml | 2 +- awc/Cargo.toml | 4 ++-- test-server/Cargo.toml | 2 +- 8 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index afaeac09d..a56d44aa8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,7 @@ actix-threadpool = "0.3.1" actix-tls = "2.0.0-alpha.1" actix-web-codegen = "0.3.0-beta.1" -actix-http = "2.0.0-alpha.4" +actix-http = "2.0.0-beta.3" awc = { version = "2.0.0-beta.1", default-features = false } bytes = "0.5.3" diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 379ba8f89..7946cf556 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -18,7 +18,7 @@ path = "src/lib.rs" [dependencies] actix-web = { version = "3.0.0-beta.1", default-features = false } -actix-http = "2.0.0-beta.1" +actix-http = "2.0.0-beta.3" actix-service = "1.0.1" bitflags = "1" bytes = "0.5.3" diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 8d44ea6b9..f1d8ed000 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,7 @@ # Changes -## Unreleased - 2020-xx-xx +## [2.0.0-beta.3] - 2020-08-14 + ### Fixed * Memory leak of `client::pool::ConnectorPoolSupport`. [#1626] diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 7f07f67c3..d72024ac0 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "2.0.0-beta.2" +version = "2.0.0-beta.3" authors = ["Nikolay Kim "] description = "Actix HTTP primitives" readme = "README.md" diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 7e81035cc..97c44c28d 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -29,4 +29,4 @@ twoway = "0.2" [dev-dependencies] actix-rt = "1.0.0" -actix-http = "2.0.0-beta.1" +actix-http = "2.0.0-beta.3" diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 8de679da5..96fcce144 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -18,7 +18,7 @@ path = "src/lib.rs" [dependencies] actix = "0.10.0-alpha.2" actix-web = { version = "3.0.0-beta.1", default-features = false } -actix-http = "2.0.0-beta.1" +actix-http = "2.0.0-beta.3" actix-codec = "0.2.0" bytes = "0.5.2" futures-channel = { version = "0.3.5", default-features = false } diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 54db46901..2a1acb8e2 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -39,7 +39,7 @@ compress = ["actix-http/compress"] [dependencies] actix-codec = "0.2.0" actix-service = "1.0.1" -actix-http = "2.0.0-beta.2" +actix-http = "2.0.0-beta.3" actix-rt = "1.0.0" base64 = "0.12" @@ -59,7 +59,7 @@ rust-tls = { version = "0.17.0", package = "rustls", optional = true, features = [dev-dependencies] actix-connect = { version = "2.0.0-alpha.2", features = ["openssl"] } actix-web = { version = "3.0.0-alpha.3", features = ["openssl"] } -actix-http = { version = "2.0.0-beta.2", features = ["openssl"] } +actix-http = { version = "2.0.0-beta.3", features = ["openssl"] } actix-http-test = { version = "2.0.0-alpha.1", features = ["openssl"] } actix-utils = "1.0.3" actix-server = "1.0.0" diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index 3b3c8e91e..5481ed1ce 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -53,4 +53,4 @@ open-ssl = { version = "0.10", package = "openssl", optional = true } [dev-dependencies] actix-web = "3.0.0-alpha.3" -actix-http = "2.0.0-beta.1" +actix-http = "2.0.0-beta.3" From 59ad1738e9dbdbc9bd232f0f3e5ccd6fc44cdc4b Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Mon, 17 Aug 2020 19:32:38 +0900 Subject: [PATCH 38/78] web: Bump up to 3.0.0-beta.2 (#1636) --- CHANGES.md | 5 ++++- Cargo.toml | 2 +- actix-files/Cargo.toml | 4 ++-- actix-multipart/Cargo.toml | 2 +- actix-web-actors/Cargo.toml | 2 +- actix-web-codegen/Cargo.toml | 2 +- awc/Cargo.toml | 2 +- test-server/Cargo.toml | 2 +- 8 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2b079ab03..5d6b3b73d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,9 @@ # Changes -## Unreleased - 2020-xx-xx +## Unreleased + + +## 3.0.0-beta.2 - 2020-08-17 ### Changed * `PayloadConfig` is now also considered in `Bytes` and `String` extractors when set using `App::data`. [#1610] diff --git a/Cargo.toml b/Cargo.toml index a56d44aa8..8b14e6602 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "3.0.0-beta.1" +version = "3.0.0-beta.2" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 7946cf556..06920bf79 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -17,7 +17,7 @@ name = "actix_files" path = "src/lib.rs" [dependencies] -actix-web = { version = "3.0.0-beta.1", default-features = false } +actix-web = { version = "3.0.0-beta.2", default-features = false } actix-http = "2.0.0-beta.3" actix-service = "1.0.1" bitflags = "1" @@ -33,4 +33,4 @@ v_htmlescape = "0.10" [dev-dependencies] actix-rt = "1.0.0" -actix-web = { version = "3.0.0-beta.1", features = ["openssl"] } +actix-web = { version = "3.0.0-beta.2", features = ["openssl"] } diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 97c44c28d..ecfdeb014 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 = "3.0.0-beta.1", default-features = false } +actix-web = { version = "3.0.0-beta.2", default-features = false } actix-service = "1.0.1" actix-utils = "1.0.3" bytes = "0.5.3" diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 96fcce144..773305880 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -17,7 +17,7 @@ path = "src/lib.rs" [dependencies] actix = "0.10.0-alpha.2" -actix-web = { version = "3.0.0-beta.1", default-features = false } +actix-web = { version = "3.0.0-beta.2", default-features = false } actix-http = "2.0.0-beta.3" actix-codec = "0.2.0" bytes = "0.5.2" diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 070c09899..13b12fd6c 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -20,5 +20,5 @@ proc-macro2 = "1" [dev-dependencies] actix-rt = "1.0.0" -actix-web = "3.0.0-beta.1" +actix-web = "3.0.0-beta.2" futures-util = { version = "0.3.5", default-features = false } diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 2a1acb8e2..27cf0d7ef 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -58,7 +58,7 @@ rust-tls = { version = "0.17.0", package = "rustls", optional = true, features = [dev-dependencies] actix-connect = { version = "2.0.0-alpha.2", features = ["openssl"] } -actix-web = { version = "3.0.0-alpha.3", features = ["openssl"] } +actix-web = { version = "3.0.0-beta.2", features = ["openssl"] } actix-http = { version = "2.0.0-beta.3", features = ["openssl"] } actix-http-test = { version = "2.0.0-alpha.1", features = ["openssl"] } actix-utils = "1.0.3" diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index 5481ed1ce..946fa82bd 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -52,5 +52,5 @@ time = { version = "0.2.7", default-features = false, features = ["std"] } open-ssl = { version = "0.10", package = "openssl", optional = true } [dev-dependencies] -actix-web = "3.0.0-alpha.3" +actix-web = "3.0.0-beta.2" actix-http = "2.0.0-beta.3" From ff2ca0f420a25b52c39d49b6a189230d26522b07 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Tue, 18 Aug 2020 00:28:39 +0900 Subject: [PATCH 39/78] Update rustls to 0.18 (#1637) --- CHANGES.md | 3 ++- Cargo.toml | 4 ++-- actix-http/CHANGES.md | 3 +++ actix-http/Cargo.toml | 10 +++++----- awc/CHANGES.md | 2 ++ awc/Cargo.toml | 6 +++--- test-server/Cargo.toml | 2 +- 7 files changed, 18 insertions(+), 12 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5d6b3b73d..087754856 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,8 @@ # Changes ## Unreleased - +### Changed +* Update `rustls` to 0.18 ## 3.0.0-beta.2 - 2020-08-17 ### Changed diff --git a/Cargo.toml b/Cargo.toml index 8b14e6602..e232f5eba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,7 +74,7 @@ actix-server = "1.0.0" actix-testing = "1.0.0" actix-macros = "0.1.0" actix-threadpool = "0.3.1" -actix-tls = "2.0.0-alpha.1" +actix-tls = "2.0.0-alpha.2" actix-web-codegen = "0.3.0-beta.1" actix-http = "2.0.0-beta.3" @@ -98,7 +98,7 @@ serde_urlencoded = "0.6.1" time = { version = "0.2.7", default-features = false, features = ["std"] } url = "2.1" open-ssl = { package = "openssl", version = "0.10", optional = true } -rust-tls = { package = "rustls", version = "0.17.0", optional = true } +rust-tls = { package = "rustls", version = "0.18.0", optional = true } tinyvec = { version = "0.3", features = ["alloc"] } [dev-dependencies] diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index f1d8ed000..e223f10c3 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,5 +1,8 @@ # Changes +## Unreleased + + ## [2.0.0-beta.3] - 2020-08-14 ### Fixed diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index d72024ac0..5b65e83a9 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -42,11 +42,11 @@ actors = ["actix"] [dependencies] actix-service = "1.0.5" actix-codec = "0.2.0" -actix-connect = "2.0.0-alpha.3" +actix-connect = "2.0.0-alpha.4" actix-utils = "1.0.6" actix-rt = "1.0.0" actix-threadpool = "0.3.1" -actix-tls = { version = "2.0.0-alpha.1", optional = true } +actix-tls = { version = "2.0.0-alpha.2", optional = true } actix = { version = "0.10.0-alpha.1", optional = true } base64 = "0.12" @@ -87,14 +87,14 @@ flate2 = { version = "1.0.13", optional = true } [dev-dependencies] actix-server = "1.0.1" -actix-connect = { version = "2.0.0-alpha.2", features = ["openssl"] } +actix-connect = { version = "2.0.0-alpha.4", features = ["openssl"] } actix-http-test = { version = "2.0.0-alpha.1", features = ["openssl"] } -actix-tls = { version = "2.0.0-alpha.1", features = ["openssl"] } +actix-tls = { version = "2.0.0-alpha.2", features = ["openssl"] } criterion = "0.3" env_logger = "0.7" serde_derive = "1.0" open-ssl = { version="0.10", package = "openssl" } -rust-tls = { version="0.17", package = "rustls" } +rust-tls = { version="0.18", package = "rustls" } [[bench]] name = "content-length" diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 3d2806a9f..90b44b367 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,6 +1,8 @@ # Changes ## Unreleased - 2020-xx-xx +### Changed +* Update `rustls` to 0.18 ## 2.0.0-beta.2 - 2020-07-21 diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 27cf0d7ef..20ba40c32 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -54,16 +54,16 @@ serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.6.1" open-ssl = { version = "0.10", package = "openssl", optional = true } -rust-tls = { version = "0.17.0", package = "rustls", optional = true, features = ["dangerous_configuration"] } +rust-tls = { version = "0.18.0", package = "rustls", optional = true, features = ["dangerous_configuration"] } [dev-dependencies] -actix-connect = { version = "2.0.0-alpha.2", features = ["openssl"] } +actix-connect = { version = "2.0.0-alpha.4", features = ["openssl"] } actix-web = { version = "3.0.0-beta.2", features = ["openssl"] } actix-http = { version = "2.0.0-beta.3", features = ["openssl"] } actix-http-test = { version = "2.0.0-alpha.1", features = ["openssl"] } actix-utils = "1.0.3" actix-server = "1.0.0" -actix-tls = { version = "2.0.0-alpha.1", features = ["openssl", "rustls"] } +actix-tls = { version = "2.0.0-alpha.2", features = ["openssl", "rustls"] } brotli2 = "0.3.2" flate2 = "1.0.13" futures-util = { version = "0.3.5", default-features = false } diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index 946fa82bd..5746f9da3 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -31,7 +31,7 @@ openssl = ["open-ssl", "awc/openssl"] [dependencies] actix-service = "1.0.1" actix-codec = "0.2.0" -actix-connect = "2.0.0-alpha.2" +actix-connect = "2.0.0-alpha.4" actix-utils = "1.0.3" actix-rt = "1.0.0" actix-server = "1.0.0" From 5802eb797f812273443491227102e273d5052bb1 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Tue, 18 Aug 2020 01:08:40 +0900 Subject: [PATCH 40/78] awc,web: Bump up to next beta releases (#1638) --- CHANGES.md | 2 +- Cargo.toml | 4 ++-- actix-files/Cargo.toml | 4 ++-- actix-multipart/Cargo.toml | 2 +- actix-web-actors/Cargo.toml | 2 +- actix-web-codegen/Cargo.toml | 2 +- awc/CHANGES.md | 2 +- awc/Cargo.toml | 4 ++-- test-server/Cargo.toml | 4 ++-- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 087754856..a44b04d40 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Changes -## Unreleased +## 3.0.0-beta.3 - 2020-08-17 ### Changed * Update `rustls` to 0.18 diff --git a/Cargo.toml b/Cargo.toml index e232f5eba..52d19c572 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "3.0.0-beta.2" +version = "3.0.0-beta.3" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" @@ -78,7 +78,7 @@ actix-tls = "2.0.0-alpha.2" actix-web-codegen = "0.3.0-beta.1" actix-http = "2.0.0-beta.3" -awc = { version = "2.0.0-beta.1", default-features = false } +awc = { version = "2.0.0-beta.3", default-features = false } bytes = "0.5.3" derive_more = "0.99.2" diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 06920bf79..c3444809e 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -17,7 +17,7 @@ name = "actix_files" path = "src/lib.rs" [dependencies] -actix-web = { version = "3.0.0-beta.2", default-features = false } +actix-web = { version = "3.0.0-beta.3", default-features = false } actix-http = "2.0.0-beta.3" actix-service = "1.0.1" bitflags = "1" @@ -33,4 +33,4 @@ v_htmlescape = "0.10" [dev-dependencies] actix-rt = "1.0.0" -actix-web = { version = "3.0.0-beta.2", features = ["openssl"] } +actix-web = { version = "3.0.0-beta.3", features = ["openssl"] } diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index ecfdeb014..04c3415e0 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 = "3.0.0-beta.2", default-features = false } +actix-web = { version = "3.0.0-beta.3", default-features = false } actix-service = "1.0.1" actix-utils = "1.0.3" bytes = "0.5.3" diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 773305880..a5f682cd5 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -17,7 +17,7 @@ path = "src/lib.rs" [dependencies] actix = "0.10.0-alpha.2" -actix-web = { version = "3.0.0-beta.2", default-features = false } +actix-web = { version = "3.0.0-beta.3", default-features = false } actix-http = "2.0.0-beta.3" actix-codec = "0.2.0" bytes = "0.5.2" diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 13b12fd6c..62cbf2941 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -20,5 +20,5 @@ proc-macro2 = "1" [dev-dependencies] actix-rt = "1.0.0" -actix-web = "3.0.0-beta.2" +actix-web = "3.0.0-beta.3" futures-util = { version = "0.3.5", default-features = false } diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 90b44b367..f4b31aef6 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,6 +1,6 @@ # Changes -## Unreleased - 2020-xx-xx +## 2.0.0-beta.3 - 2020-08-17 ### Changed * Update `rustls` to 0.18 diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 20ba40c32..d4d621025 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "2.0.0-beta.2" +version = "2.0.0-beta.3" authors = ["Nikolay Kim "] description = "Async HTTP client library that uses the Actix runtime." readme = "README.md" @@ -58,7 +58,7 @@ rust-tls = { version = "0.18.0", package = "rustls", optional = true, features = [dev-dependencies] actix-connect = { version = "2.0.0-alpha.4", features = ["openssl"] } -actix-web = { version = "3.0.0-beta.2", features = ["openssl"] } +actix-web = { version = "3.0.0-beta.3", features = ["openssl"] } actix-http = { version = "2.0.0-beta.3", features = ["openssl"] } actix-http-test = { version = "2.0.0-alpha.1", features = ["openssl"] } actix-utils = "1.0.3" diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index 5746f9da3..f3df6d1f6 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -36,7 +36,7 @@ actix-utils = "1.0.3" actix-rt = "1.0.0" actix-server = "1.0.0" actix-testing = "1.0.0" -awc = "2.0.0-alpha.2" +awc = "2.0.0-beta.3" base64 = "0.12" bytes = "0.5.3" @@ -52,5 +52,5 @@ time = { version = "0.2.7", default-features = false, features = ["std"] } open-ssl = { version = "0.10", package = "openssl", optional = true } [dev-dependencies] -actix-web = "3.0.0-beta.2" +actix-web = "3.0.0-beta.3" actix-http = "2.0.0-beta.3" From 3892a95c11a7a616e687191d3a4f1405bd96de76 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Tue, 18 Aug 2020 01:16:18 +0900 Subject: [PATCH 41/78] Fix actix-web version to publish --- awc/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awc/Cargo.toml b/awc/Cargo.toml index d4d621025..ea4450bb2 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -58,7 +58,7 @@ rust-tls = { version = "0.18.0", package = "rustls", optional = true, features = [dev-dependencies] actix-connect = { version = "2.0.0-alpha.4", features = ["openssl"] } -actix-web = { version = "3.0.0-beta.3", features = ["openssl"] } +actix-web = { version = "3.0.0-beta.2", features = ["openssl"] } actix-http = { version = "2.0.0-beta.3", features = ["openssl"] } actix-http-test = { version = "2.0.0-alpha.1", features = ["openssl"] } actix-utils = "1.0.3" From 75d86a6bebe2829f8e4bc3f54945b08073fb1710 Mon Sep 17 00:00:00 2001 From: LJ <5854483+ljoonal@users.noreply.github.com> Date: Wed, 19 Aug 2020 11:21:52 +0000 Subject: [PATCH 42/78] Configurable trailing slash behaviour for NormalizePath (#1639) Co-authored-by: ljoonal --- CHANGES.md | 5 +++ MIGRATION.md | 4 +++ src/middleware/condition.rs | 2 +- src/middleware/normalize.rs | 71 ++++++++++++++++++++++++++++++++----- 4 files changed, 73 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a44b04d40..0a7b26d7d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,10 @@ # Changes +## Unreleased +### Added +* `middleware::NormalizePath` now has configurable behaviour for either always having a trailing slash, + or as the new addition, always trimming trailing slashes. + ## 3.0.0-beta.3 - 2020-08-17 ### Changed * Update `rustls` to 0.18 diff --git a/MIGRATION.md b/MIGRATION.md index 7459b5b2d..0e73b7d47 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -32,6 +32,10 @@ } ``` +* `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one. + It will need `middleware::normalize::TrailingSlash` when being constructed with `NormalizePath::new(...)`, + or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::default())`. + ## 2.0.0 * `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to diff --git a/src/middleware/condition.rs b/src/middleware/condition.rs index 7ff81437b..ab1c69746 100644 --- a/src/middleware/condition.rs +++ b/src/middleware/condition.rs @@ -17,7 +17,7 @@ use futures_util::future::{ok, Either, FutureExt, LocalBoxFuture}; /// # fn main() { /// let enable_normalize = std::env::var("NORMALIZE_PATH") == Ok("true".into()); /// let app = App::new() -/// .wrap(Condition::new(enable_normalize, NormalizePath)); +/// .wrap(Condition::new(enable_normalize, NormalizePath::default())); /// # } /// ``` pub struct Condition { diff --git a/src/middleware/normalize.rs b/src/middleware/normalize.rs index a1021ed14..1b6894796 100644 --- a/src/middleware/normalize.rs +++ b/src/middleware/normalize.rs @@ -10,20 +10,37 @@ use regex::Regex; use crate::service::{ServiceRequest, ServiceResponse}; use crate::Error; +/// To be used when constructing `NormalizePath` to define it's behavior. +#[non_exhaustive] +#[derive(Clone, Copy)] +pub enum TrailingSlash { + /// Always add a trailing slash to the end of the path. + /// This will require all routes to end in a trailing slash for them to be accessible. + Always, + /// Trim trailing slashes from the end of the path. + Trim, +} + +impl Default for TrailingSlash { + fn default() -> Self { + TrailingSlash::Always + } +} + #[derive(Default, Clone, Copy)] /// `Middleware` to normalize request's URI in place /// /// Performs following: /// /// - Merges multiple slashes into one. -/// - Appends a trailing slash if one is not present. +/// - Appends a trailing slash if one is not present, or removes one if present, depending on the supplied `TrailingSlash`. /// /// ```rust /// use actix_web::{web, http, middleware, App, HttpResponse}; /// /// # fn main() { /// let app = App::new() -/// .wrap(middleware::NormalizePath) +/// .wrap(middleware::NormalizePath::default()) /// .service( /// web::resource("/test") /// .route(web::get().to(|| HttpResponse::Ok())) @@ -32,7 +49,14 @@ use crate::Error; /// # } /// ``` -pub struct NormalizePath; +pub struct NormalizePath(TrailingSlash); + +impl NormalizePath { + /// Create new `NormalizePath` middleware with the specified trailing slash style. + pub fn new(trailing_slash_style: TrailingSlash) -> Self { + NormalizePath(trailing_slash_style) + } +} impl Transform for NormalizePath where @@ -50,6 +74,7 @@ where ok(NormalizePathNormalization { service, merge_slash: Regex::new("//+").unwrap(), + trailing_slash_behavior: self.0, }) } } @@ -57,6 +82,7 @@ where pub struct NormalizePathNormalization { service: S, merge_slash: Regex, + trailing_slash_behavior: TrailingSlash, } impl Service for NormalizePathNormalization @@ -78,8 +104,11 @@ where let original_path = head.uri.path(); - // always add trailing slash, might be an extra one - let path = original_path.to_string() + "/"; + // Either adds a string to the end (duplicates will be removed anyways) or trims all slashes from the end + let path = match self.trailing_slash_behavior { + TrailingSlash::Always => original_path.to_string() + "/", + TrailingSlash::Trim => original_path.trim_end_matches('/').to_string(), + }; // normalize multiple /'s to one / let path = self.merge_slash.replace_all(&path, "/"); @@ -150,6 +179,32 @@ mod tests { assert!(res4.status().is_success()); } + #[actix_rt::test] + async fn trim_trailing_slashes() { + let mut app = init_service( + App::new() + .wrap(NormalizePath(TrailingSlash::Trim)) + .service(web::resource("/v1/something").to(HttpResponse::Ok)), + ) + .await; + + let req = TestRequest::with_uri("/v1/something////").to_request(); + let res = call_service(&mut app, req).await; + assert!(res.status().is_success()); + + let req2 = TestRequest::with_uri("/v1/something/").to_request(); + let res2 = call_service(&mut app, req2).await; + assert!(res2.status().is_success()); + + let req3 = TestRequest::with_uri("//v1//something//").to_request(); + let res3 = call_service(&mut app, req3).await; + assert!(res3.status().is_success()); + + let req4 = TestRequest::with_uri("//v1//something").to_request(); + let res4 = call_service(&mut app, req4).await; + assert!(res4.status().is_success()); + } + #[actix_rt::test] async fn test_in_place_normalization() { let srv = |req: ServiceRequest| { @@ -157,7 +212,7 @@ mod tests { ok(req.into_response(HttpResponse::Ok().finish())) }; - let mut normalize = NormalizePath + let mut normalize = NormalizePath::default() .new_transform(srv.into_service()) .await .unwrap(); @@ -188,7 +243,7 @@ mod tests { ok(req.into_response(HttpResponse::Ok().finish())) }; - let mut normalize = NormalizePath + let mut normalize = NormalizePath::default() .new_transform(srv.into_service()) .await .unwrap(); @@ -207,7 +262,7 @@ mod tests { ok(req.into_response(HttpResponse::Ok().finish())) }; - let mut normalize = NormalizePath + let mut normalize = NormalizePath::default() .new_transform(srv.into_service()) .await .unwrap(); From 8497b5f4901f0a78f3769615f9e23a0c0de2382d Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 24 Aug 2020 10:13:35 +0100 Subject: [PATCH 43/78] integrate with updated actix-{codec, utils} (#1634) --- CHANGES.md | 11 +++-- Cargo.toml | 6 +-- actix-http/CHANGES.md | 3 +- actix-http/Cargo.toml | 4 +- actix-http/src/client/connection.rs | 12 ++++-- actix-http/src/client/h1proto.rs | 65 ++++++++++++++++------------- actix-http/src/error.rs | 7 ++-- actix-http/src/h1/client.rs | 5 +-- actix-http/src/h1/codec.rs | 5 +-- actix-http/src/h1/service.rs | 16 ++++--- actix-http/src/h1/utils.rs | 28 +++++++++---- actix-http/src/ws/codec.rs | 3 +- actix-http/src/ws/dispatcher.rs | 16 +++---- actix-http/tests/test_ws.rs | 4 +- actix-multipart/Cargo.toml | 2 +- actix-web-actors/Cargo.toml | 2 +- awc/CHANGES.md | 5 +++ awc/Cargo.toml | 4 +- awc/src/connect.rs | 4 +- awc/src/ws.rs | 2 +- awc/tests/test_ws.rs | 2 +- test-server/CHANGES.md | 3 ++ test-server/Cargo.toml | 4 +- 23 files changed, 127 insertions(+), 86 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0a7b26d7d..0d7311748 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,14 +1,19 @@ # Changes -## Unreleased +## Unreleased - 2020-xx-xx ### Added -* `middleware::NormalizePath` now has configurable behaviour for either always having a trailing slash, - or as the new addition, always trimming trailing slashes. +* `middleware::NormalizePath` now has configurable behaviour for either always having a trailing + slash, or as the new addition, always trimming trailing slashes. + +### Changed +* Update actix-codec and actix-utils dependencies. + ## 3.0.0-beta.3 - 2020-08-17 ### Changed * Update `rustls` to 0.18 + ## 3.0.0-beta.2 - 2020-08-17 ### Changed * `PayloadConfig` is now also considered in `Bytes` and `String` extractors when set diff --git a/Cargo.toml b/Cargo.toml index 52d19c572..dbdab5937 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,9 +65,9 @@ name = "test_server" required-features = ["compress"] [dependencies] -actix-codec = "0.2.0" -actix-service = "1.0.2" -actix-utils = "1.0.6" +actix-codec = "0.3.0" +actix-service = "1.0.6" +actix-utils = "2.0.0" actix-router = "0.2.4" actix-rt = "1.1.1" actix-server = "1.0.0" diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index e223f10c3..d8d674fb1 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,7 +1,8 @@ # Changes ## Unreleased - +### Changed +* Update actix-codec and actix-utils dependencies. ## [2.0.0-beta.3] - 2020-08-14 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 5b65e83a9..750d1e0af 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -41,9 +41,9 @@ actors = ["actix"] [dependencies] actix-service = "1.0.5" -actix-codec = "0.2.0" +actix-codec = "0.3.0" actix-connect = "2.0.0-alpha.4" -actix-utils = "1.0.6" +actix-utils = "2.0.0" actix-rt = "1.0.0" actix-threadpool = "0.3.1" actix-tls = { version = "2.0.0-alpha.2", optional = true } diff --git a/actix-http/src/client/connection.rs b/actix-http/src/client/connection.rs index eecf2ee6f..ec86dabb0 100644 --- a/actix-http/src/client/connection.rs +++ b/actix-http/src/client/connection.rs @@ -46,10 +46,10 @@ pub trait Connection { pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static { /// Close connection - fn close(&mut self); + fn close(self: Pin<&mut Self>); /// Release connection to the connection pool - fn release(&mut self); + fn release(self: Pin<&mut Self>); } #[doc(hidden)] @@ -195,11 +195,15 @@ where match self { EitherConnection::A(con) => con .open_tunnel(head) - .map(|res| res.map(|(head, framed)| (head, framed.map_io(EitherIo::A)))) + .map(|res| { + res.map(|(head, framed)| (head, framed.into_map_io(EitherIo::A))) + }) .boxed_local(), EitherConnection::B(con) => con .open_tunnel(head) - .map(|res| res.map(|(head, framed)| (head, framed.map_io(EitherIo::B)))) + .map(|res| { + res.map(|(head, framed)| (head, framed.into_map_io(EitherIo::B))) + }) .boxed_local(), } } diff --git a/actix-http/src/client/h1proto.rs b/actix-http/src/client/h1proto.rs index 51e853b3d..06cc05404 100644 --- a/actix-http/src/client/h1proto.rs +++ b/actix-http/src/client/h1proto.rs @@ -67,17 +67,17 @@ where }; // create Framed and send request - let mut framed = Framed::new(io, h1::ClientCodec::default()); - framed.send((head, body.size()).into()).await?; + let mut framed_inner = Framed::new(io, h1::ClientCodec::default()); + framed_inner.send((head, body.size()).into()).await?; // send request body match body.size() { BodySize::None | BodySize::Empty | BodySize::Sized(0) => (), - _ => send_body(body, &mut framed).await?, + _ => send_body(body, Pin::new(&mut framed_inner)).await?, }; // read response and init read body - let res = framed.into_future().await; + let res = Pin::new(&mut framed_inner).into_future().await; let (head, framed) = if let (Some(result), framed) = res { let item = result.map_err(SendRequestError::from)?; (item, framed) @@ -85,14 +85,14 @@ where return Err(SendRequestError::from(ConnectError::Disconnected)); }; - match framed.get_codec().message_type() { + match framed.codec_ref().message_type() { h1::MessageType::None => { - let force_close = !framed.get_codec().keepalive(); + let force_close = !framed.codec_ref().keepalive(); release_connection(framed, force_close); Ok((head, Payload::None)) } _ => { - let pl: PayloadStream = PlStream::new(framed).boxed_local(); + let pl: PayloadStream = PlStream::new(framed_inner).boxed_local(); Ok((head, pl.into())) } } @@ -119,35 +119,36 @@ where } /// send request body to the peer -pub(crate) async fn send_body( +pub(crate) async fn send_body( body: B, - framed: &mut Framed, + mut framed: Pin<&mut Framed>, ) -> Result<(), SendRequestError> where - I: ConnectionLifetime, + T: ConnectionLifetime + Unpin, B: MessageBody, { - let mut eof = false; pin_mut!(body); + + let mut eof = false; while !eof { - while !eof && !framed.is_write_buf_full() { + while !eof && !framed.as_ref().is_write_buf_full() { match poll_fn(|cx| body.as_mut().poll_next(cx)).await { Some(result) => { - framed.write(h1::Message::Chunk(Some(result?)))?; + framed.as_mut().write(h1::Message::Chunk(Some(result?)))?; } None => { eof = true; - framed.write(h1::Message::Chunk(None))?; + framed.as_mut().write(h1::Message::Chunk(None))?; } } } - if !framed.is_write_buf_empty() { - poll_fn(|cx| match framed.flush(cx) { + if !framed.as_ref().is_write_buf_empty() { + poll_fn(|cx| match framed.as_mut().flush(cx) { Poll::Ready(Ok(_)) => Poll::Ready(Ok(())), Poll::Ready(Err(err)) => Poll::Ready(Err(err)), Poll::Pending => { - if !framed.is_write_buf_full() { + if !framed.as_ref().is_write_buf_full() { Poll::Ready(Ok(())) } else { Poll::Pending @@ -158,13 +159,14 @@ where } } - SinkExt::flush(framed).await?; + SinkExt::flush(Pin::into_inner(framed)).await?; Ok(()) } #[doc(hidden)] /// HTTP client connection pub struct H1Connection { + /// T should be `Unpin` io: Option, created: time::Instant, pool: Option>, @@ -175,7 +177,7 @@ where T: AsyncRead + AsyncWrite + Unpin + 'static, { /// Close connection - fn close(&mut self) { + fn close(mut self: Pin<&mut Self>) { if let Some(mut pool) = self.pool.take() { if let Some(io) = self.io.take() { pool.close(IoConnection::new( @@ -188,7 +190,7 @@ where } /// Release this connection to the connection pool - fn release(&mut self) { + fn release(mut self: Pin<&mut Self>) { if let Some(mut pool) = self.pool.take() { if let Some(io) = self.io.take() { pool.release(IoConnection::new( @@ -242,14 +244,18 @@ impl AsyncWrite for H1Connection } } +#[pin_project::pin_project] pub(crate) struct PlStream { + #[pin] framed: Option>, } impl PlStream { fn new(framed: Framed) -> Self { + let framed = framed.into_map_codec(|codec| codec.into_payload_codec()); + PlStream { - framed: Some(framed.map_codec(|codec| codec.into_payload_codec())), + framed: Some(framed), } } } @@ -261,16 +267,16 @@ impl Stream for PlStream { self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll> { - let this = self.get_mut(); + let mut this = self.project(); - match this.framed.as_mut().unwrap().next_item(cx)? { + match this.framed.as_mut().as_pin_mut().unwrap().next_item(cx)? { Poll::Pending => Poll::Pending, Poll::Ready(Some(chunk)) => { if let Some(chunk) = chunk { Poll::Ready(Some(Ok(chunk))) } else { - let framed = this.framed.take().unwrap(); - let force_close = !framed.get_codec().keepalive(); + let framed = this.framed.as_mut().as_pin_mut().unwrap(); + let force_close = !framed.codec_ref().keepalive(); release_connection(framed, force_close); Poll::Ready(None) } @@ -280,14 +286,13 @@ impl Stream for PlStream { } } -fn release_connection(framed: Framed, force_close: bool) +fn release_connection(framed: Pin<&mut Framed>, force_close: bool) where T: ConnectionLifetime, { - let mut parts = framed.into_parts(); - if !force_close && parts.read_buf.is_empty() && parts.write_buf.is_empty() { - parts.io.release() + if !force_close && framed.is_read_buf_empty() && framed.is_write_buf_empty() { + framed.io_pin().release() } else { - parts.io.close() + framed.io_pin().close() } } diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index d0754ce88..e93c077af 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -1,4 +1,5 @@ //! Error and Result module + use std::cell::RefCell; use std::io::Write; use std::str::Utf8Error; @@ -7,7 +8,7 @@ use std::{fmt, io, result}; use actix_codec::{Decoder, Encoder}; pub use actix_threadpool::BlockingError; -use actix_utils::framed::DispatcherError as FramedDispatcherError; +use actix_utils::dispatcher::DispatcherError as FramedDispatcherError; use actix_utils::timeout::TimeoutError; use bytes::BytesMut; use derive_more::{Display, From}; @@ -452,10 +453,10 @@ impl ResponseError for ContentTypeError { } } -impl ResponseError for FramedDispatcherError +impl + Decoder, I> ResponseError for FramedDispatcherError where E: fmt::Debug + fmt::Display, - ::Error: fmt::Debug, + >::Error: fmt::Debug, ::Error: fmt::Debug, { } diff --git a/actix-http/src/h1/client.rs b/actix-http/src/h1/client.rs index bcfc18cde..2e0103409 100644 --- a/actix-http/src/h1/client.rs +++ b/actix-http/src/h1/client.rs @@ -173,13 +173,12 @@ impl Decoder for ClientPayloadCodec { } } -impl Encoder for ClientCodec { - type Item = Message<(RequestHeadType, BodySize)>; +impl Encoder> for ClientCodec { type Error = io::Error; fn encode( &mut self, - item: Self::Item, + item: Message<(RequestHeadType, BodySize)>, dst: &mut BytesMut, ) -> Result<(), Self::Error> { match item { diff --git a/actix-http/src/h1/codec.rs b/actix-http/src/h1/codec.rs index de2af9ee7..036f16670 100644 --- a/actix-http/src/h1/codec.rs +++ b/actix-http/src/h1/codec.rs @@ -144,13 +144,12 @@ impl Decoder for Codec { } } -impl Encoder for Codec { - type Item = Message<(Response<()>, BodySize)>; +impl Encoder, BodySize)>> for Codec { type Error = io::Error; fn encode( &mut self, - item: Self::Item, + item: Message<(Response<()>, BodySize)>, dst: &mut BytesMut, ) -> Result<(), Self::Error> { match item { diff --git a/actix-http/src/h1/service.rs b/actix-http/src/h1/service.rs index 4d1a1dc1b..339a0f538 100644 --- a/actix-http/src/h1/service.rs +++ b/actix-http/src/h1/service.rs @@ -548,10 +548,12 @@ where } #[doc(hidden)] +#[pin_project::pin_project] pub struct OneRequestServiceResponse where T: AsyncRead + AsyncWrite + Unpin, { + #[pin] framed: Option>, } @@ -562,16 +564,18 @@ where type Output = Result<(Request, Framed), ParseError>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - match self.framed.as_mut().unwrap().next_item(cx) { - Poll::Ready(Some(Ok(req))) => match req { + let this = self.as_mut().project(); + + match ready!(this.framed.as_pin_mut().unwrap().next_item(cx)) { + Some(Ok(req)) => match req { Message::Item(req) => { - Poll::Ready(Ok((req, self.framed.take().unwrap()))) + let mut this = self.as_mut().project(); + Poll::Ready(Ok((req, this.framed.take().unwrap()))) } Message::Chunk(_) => unreachable!("Something is wrong"), }, - Poll::Ready(Some(Err(err))) => Poll::Ready(Err(err)), - Poll::Ready(None) => Poll::Ready(Err(ParseError::Incomplete)), - Poll::Pending => Poll::Pending, + Some(Err(err)) => Poll::Ready(Err(err)), + None => Poll::Ready(Err(ParseError::Incomplete)), } } } diff --git a/actix-http/src/h1/utils.rs b/actix-http/src/h1/utils.rs index c44925c7a..9e9c57137 100644 --- a/actix-http/src/h1/utils.rs +++ b/actix-http/src/h1/utils.rs @@ -9,12 +9,13 @@ use crate::error::Error; use crate::h1::{Codec, Message}; use crate::response::Response; -/// Send http/1 response +/// Send HTTP/1 response #[pin_project::pin_project] pub struct SendResponse { res: Option, BodySize)>>, #[pin] body: Option>, + #[pin] framed: Option>, } @@ -35,23 +36,30 @@ where impl Future for SendResponse where - T: AsyncRead + AsyncWrite, + T: AsyncRead + AsyncWrite + Unpin, B: MessageBody + Unpin, { type Output = Result, Error>; // TODO: rethink if we need loops in polls - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let mut this = self.project(); + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut this = self.as_mut().project(); let mut body_done = this.body.is_none(); loop { let mut body_ready = !body_done; - let framed = this.framed.as_mut().unwrap(); // send body if this.res.is_none() && body_ready { - while body_ready && !body_done && !framed.is_write_buf_full() { + while body_ready + && !body_done + && !this + .framed + .as_ref() + .as_pin_ref() + .unwrap() + .is_write_buf_full() + { match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx)? { Poll::Ready(item) => { // body is done when item is None @@ -59,6 +67,7 @@ where if body_done { let _ = this.body.take(); } + let framed = this.framed.as_mut().as_pin_mut().unwrap(); framed.write(Message::Chunk(item))?; } Poll::Pending => body_ready = false, @@ -66,6 +75,8 @@ where } } + let framed = this.framed.as_mut().as_pin_mut().unwrap(); + // flush write buffer if !framed.is_write_buf_empty() { match framed.flush(cx)? { @@ -96,6 +107,9 @@ where break; } } - Poll::Ready(Ok(this.framed.take().unwrap())) + + let framed = this.framed.take().unwrap(); + + Poll::Ready(Ok(framed)) } } diff --git a/actix-http/src/ws/codec.rs b/actix-http/src/ws/codec.rs index 733976a78..7c9628b1a 100644 --- a/actix-http/src/ws/codec.rs +++ b/actix-http/src/ws/codec.rs @@ -91,8 +91,7 @@ impl Codec { } } -impl Encoder for Codec { - type Item = Message; +impl Encoder for Codec { type Error = ProtocolError; fn encode(&mut self, item: Message, dst: &mut BytesMut) -> Result<(), Self::Error> { diff --git a/actix-http/src/ws/dispatcher.rs b/actix-http/src/ws/dispatcher.rs index 7a6b11b18..b114217a0 100644 --- a/actix-http/src/ws/dispatcher.rs +++ b/actix-http/src/ws/dispatcher.rs @@ -4,16 +4,18 @@ use std::task::{Context, Poll}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_service::{IntoService, Service}; -use actix_utils::framed; +use actix_utils::dispatcher::{Dispatcher as InnerDispatcher, DispatcherError}; use super::{Codec, Frame, Message}; +#[pin_project::pin_project] pub struct Dispatcher where S: Service + 'static, T: AsyncRead + AsyncWrite, { - inner: framed::Dispatcher, + #[pin] + inner: InnerDispatcher, } impl Dispatcher @@ -25,13 +27,13 @@ where { pub fn new>(io: T, service: F) -> Self { Dispatcher { - inner: framed::Dispatcher::new(Framed::new(io, Codec::new()), service), + inner: InnerDispatcher::new(Framed::new(io, Codec::new()), service), } } pub fn with>(framed: Framed, service: F) -> Self { Dispatcher { - inner: framed::Dispatcher::new(framed, service), + inner: InnerDispatcher::new(framed, service), } } } @@ -43,9 +45,9 @@ where S::Future: 'static, S::Error: 'static, { - type Output = Result<(), framed::DispatcherError>; + type Output = Result<(), DispatcherError>; - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - Pin::new(&mut self.inner).poll(cx) + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.project().inner.poll(cx) } } diff --git a/actix-http/tests/test_ws.rs b/actix-http/tests/test_ws.rs index ff9def85b..5d86605f4 100644 --- a/actix-http/tests/test_ws.rs +++ b/actix-http/tests/test_ws.rs @@ -8,7 +8,7 @@ use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_http::{body, h1, ws, Error, HttpService, Request, Response}; use actix_http_test::test_server; use actix_service::{fn_factory, Service}; -use actix_utils::framed::Dispatcher; +use actix_utils::dispatcher::Dispatcher; use bytes::Bytes; use futures_util::future; use futures_util::task::{Context, Poll}; @@ -59,7 +59,7 @@ where .await .unwrap(); - Dispatcher::new(framed.into_framed(ws::Codec::new()), service) + Dispatcher::new(framed.replace_codec(ws::Codec::new()), service) .await .map_err(|_| panic!()) }; diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 04c3415e0..58f7113e5 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -18,7 +18,7 @@ path = "src/lib.rs" [dependencies] actix-web = { version = "3.0.0-beta.3", default-features = false } actix-service = "1.0.1" -actix-utils = "1.0.3" +actix-utils = "2.0.0" bytes = "0.5.3" derive_more = "0.99.2" httparse = "1.3" diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index a5f682cd5..b33acc222 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -19,7 +19,7 @@ path = "src/lib.rs" actix = "0.10.0-alpha.2" actix-web = { version = "3.0.0-beta.3", default-features = false } actix-http = "2.0.0-beta.3" -actix-codec = "0.2.0" +actix-codec = "0.3.0" bytes = "0.5.2" futures-channel = { version = "0.3.5", default-features = false } futures-core = { version = "0.3.5", default-features = false } diff --git a/awc/CHANGES.md b/awc/CHANGES.md index f4b31aef6..8d1b58856 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,5 +1,10 @@ # Changes +## Unreleased - 2020-xx-xx +### Changed +* Update actix-codec dependency. + + ## 2.0.0-beta.3 - 2020-08-17 ### Changed * Update `rustls` to 0.18 diff --git a/awc/Cargo.toml b/awc/Cargo.toml index ea4450bb2..ff0afaa1c 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -37,7 +37,7 @@ rustls = ["rust-tls", "actix-http/rustls"] compress = ["actix-http/compress"] [dependencies] -actix-codec = "0.2.0" +actix-codec = "0.3.0" actix-service = "1.0.1" actix-http = "2.0.0-beta.3" actix-rt = "1.0.0" @@ -61,7 +61,7 @@ actix-connect = { version = "2.0.0-alpha.4", features = ["openssl"] } actix-web = { version = "3.0.0-beta.2", features = ["openssl"] } actix-http = { version = "2.0.0-beta.3", features = ["openssl"] } actix-http-test = { version = "2.0.0-alpha.1", features = ["openssl"] } -actix-utils = "1.0.3" +actix-utils = "2.0.0" actix-server = "1.0.0" actix-tls = { version = "2.0.0-alpha.2", features = ["openssl", "rustls"] } brotli2 = "0.3.2" diff --git a/awc/src/connect.rs b/awc/src/connect.rs index 618d653f5..7fbe1543a 100644 --- a/awc/src/connect.rs +++ b/awc/src/connect.rs @@ -152,7 +152,7 @@ where let (head, framed) = connection.open_tunnel(RequestHeadType::from(head)).await?; - let framed = framed.map_io(|io| BoxedSocket(Box::new(Socket(io)))); + let framed = framed.into_map_io(|io| BoxedSocket(Box::new(Socket(io)))); Ok((head, framed)) }) } @@ -186,7 +186,7 @@ where .open_tunnel(RequestHeadType::Rc(head, extra_headers)) .await?; - let framed = framed.map_io(|io| BoxedSocket(Box::new(Socket(io)))); + let framed = framed.into_map_io(|io| BoxedSocket(Box::new(Socket(io)))); Ok((head, framed)) }) } diff --git a/awc/src/ws.rs b/awc/src/ws.rs index 6ad660c41..8b01e5716 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -366,7 +366,7 @@ impl WebsocketsRequest { // response and ws framed Ok(( ClientResponse::new(head, Payload::None), - framed.map_codec(|_| { + framed.into_map_codec(|_| { if server_mode { ws::Codec::new().max_size(max_size) } else { diff --git a/awc/tests/test_ws.rs b/awc/tests/test_ws.rs index d3f66814f..1c1068668 100644 --- a/awc/tests/test_ws.rs +++ b/awc/tests/test_ws.rs @@ -32,7 +32,7 @@ async fn test_simple() { .await?; // start websocket service - let framed = framed.into_framed(ws::Codec::new()); + let framed = framed.replace_codec(ws::Codec::new()); ws::Dispatcher::with(framed, ws_service).await } }) diff --git a/test-server/CHANGES.md b/test-server/CHANGES.md index 079cad74a..71b906b9f 100644 --- a/test-server/CHANGES.md +++ b/test-server/CHANGES.md @@ -1,5 +1,8 @@ # Changes +## Unreleased - 2020-xx-xx +* Update actix-codec and actix-utils dependencies. + ## [2.0.0-alpha.1] - 2020-05-23 * Update the `time` dependency to 0.2.7 diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index f3df6d1f6..13f27ab59 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -30,9 +30,9 @@ openssl = ["open-ssl", "awc/openssl"] [dependencies] actix-service = "1.0.1" -actix-codec = "0.2.0" +actix-codec = "0.3.0" actix-connect = "2.0.0-alpha.4" -actix-utils = "1.0.3" +actix-utils = "2.0.0" actix-rt = "1.0.0" actix-server = "1.0.0" actix-testing = "1.0.0" From 01cbef700fd9d7ce20f44bed06c649f6b238b9bb Mon Sep 17 00:00:00 2001 From: Matt Kantor Date: Fri, 28 Aug 2020 14:16:41 -0700 Subject: [PATCH 44/78] Fix a small typo in a doc comment. (#1649) --- 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 dc6468ab3..51d4722d7 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -85,7 +85,7 @@ use crate::HttpResponse; /// [`ConnectionInfo::realip_remote_addr()`](../dev/struct.ConnectionInfo.html#method.realip_remote_addr) /// /// If you use this value ensure that all requests come from trusted hosts, since it is trivial -/// for the remote client to simulate been another client. +/// for the remote client to simulate being another client. /// pub struct Logger(Rc); From 4e321595bcf4a450efcfe5655e68b7c917c38fa8 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 2 Sep 2020 22:12:07 +0100 Subject: [PATCH 45/78] extract more config types from Data as well (#1641) --- CHANGES.md | 9 +++++-- src/types/form.rs | 38 +++++++++++++++++++++++------ src/types/json.rs | 58 +++++++++++++++++++++++++++++++++----------- src/types/payload.rs | 19 ++++++--------- 4 files changed, 89 insertions(+), 35 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0d7311748..82c562f5c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,11 +3,16 @@ ## Unreleased - 2020-xx-xx ### Added * `middleware::NormalizePath` now has configurable behaviour for either always having a trailing - slash, or as the new addition, always trimming trailing slashes. + slash, or as the new addition, always trimming trailing slashes. [#1639] ### Changed -* Update actix-codec and actix-utils dependencies. +* Update actix-codec and actix-utils dependencies. [#1634] +* `FormConfig` and `JsonConfig` configurations are now also considered when set + using `App::data`. [#1641] +[#1639]: https://github.com/actix/actix-web/pull/1639 +[#1641]: https://github.com/actix/actix-web/pull/1641 +[#1634]: https://github.com/actix/actix-web/pull/1634 ## 3.0.0-beta.3 - 2020-08-17 ### Changed diff --git a/src/types/form.rs b/src/types/form.rs index ea061d553..de88c2a94 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -23,7 +23,7 @@ use crate::http::{ StatusCode, }; use crate::request::HttpRequest; -use crate::responder::Responder; +use crate::{responder::Responder, web}; /// Form data helper (`application/x-www-form-urlencoded`) /// @@ -121,8 +121,12 @@ where fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { let req2 = req.clone(); let (limit, err) = req - .app_data::() - .map(|c| (c.limit, c.ehandler.clone())) + .app_data::() + .or_else(|| { + req.app_data::>() + .map(|d| d.as_ref()) + }) + .map(|c| (c.limit, c.err_handler.clone())) .unwrap_or((16384, None)); UrlEncoded::new(req, payload) @@ -200,7 +204,7 @@ impl Responder for Form { #[derive(Clone)] pub struct FormConfig { limit: usize, - ehandler: Option Error>>, + err_handler: Option Error>>, } impl FormConfig { @@ -215,7 +219,7 @@ impl FormConfig { where F: Fn(UrlencodedError, &HttpRequest) -> Error + 'static, { - self.ehandler = Some(Rc::new(f)); + self.err_handler = Some(Rc::new(f)); self } } @@ -223,8 +227,8 @@ impl FormConfig { impl Default for FormConfig { fn default() -> Self { FormConfig { - limit: 16384, - ehandler: None, + limit: 16_384, // 2^14 bytes (~16kB) + err_handler: None, } } } @@ -378,7 +382,7 @@ mod tests { use serde::{Deserialize, Serialize}; use super::*; - use crate::http::header::{HeaderValue, CONTENT_TYPE}; + use crate::http::header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE}; use crate::test::TestRequest; #[derive(Deserialize, Serialize, Debug, PartialEq)] @@ -499,4 +503,22 @@ mod tests { use crate::responder::tests::BodyTest; assert_eq!(resp.body().bin_ref(), b"hello=world&counter=123"); } + + #[actix_rt::test] + async fn test_with_config_in_data_wrapper() { + let ctype = HeaderValue::from_static("application/x-www-form-urlencoded"); + + let (req, mut pl) = TestRequest::default() + .header(CONTENT_TYPE, ctype) + .header(CONTENT_LENGTH, HeaderValue::from_static("20")) + .set_payload(Bytes::from_static(b"hello=test&counter=4")) + .app_data(web::Data::new(FormConfig::default().limit(10))) + .to_http_parts(); + + let s = Form::::from_request(&req, &mut pl).await; + assert!(s.is_err()); + + let err_str = s.err().unwrap().to_string(); + assert!(err_str.contains("Urlencoded payload size is bigger")); + } } diff --git a/src/types/json.rs b/src/types/json.rs index 527b4b611..ab7978dff 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -20,7 +20,7 @@ use crate::dev::Decompress; use crate::error::{Error, JsonPayloadError}; use crate::extract::FromRequest; use crate::request::HttpRequest; -use crate::responder::Responder; +use crate::{responder::Responder, web}; /// Json helper /// @@ -179,10 +179,11 @@ where #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { let req2 = req.clone(); - let (limit, err, ctype) = req - .app_data::() - .map(|c| (c.limit, c.ehandler.clone(), c.content_type.clone())) - .unwrap_or((32768, None, None)); + let config = JsonConfig::from_req(req); + + let limit = config.limit; + let ctype = config.content_type.clone(); + let err_handler = config.err_handler.clone(); JsonBody::new(req, payload, ctype) .limit(limit) @@ -193,7 +194,8 @@ where Request path: {}", req2.path() ); - if let Some(err) = err { + + if let Some(err) = err_handler { Err((*err)(e, &req2)) } else { Err(e.into()) @@ -255,7 +257,8 @@ where #[derive(Clone)] pub struct JsonConfig { limit: usize, - ehandler: Option Error + Send + Sync>>, + err_handler: + Option Error + Send + Sync>>, content_type: Option bool + Send + Sync>>, } @@ -271,7 +274,7 @@ impl JsonConfig { where F: Fn(JsonPayloadError, &HttpRequest) -> Error + Send + Sync + 'static, { - self.ehandler = Some(Arc::new(f)); + self.err_handler = Some(Arc::new(f)); self } @@ -283,15 +286,26 @@ impl JsonConfig { self.content_type = Some(Arc::new(predicate)); 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 { + req.app_data::() + .or_else(|| req.app_data::>().map(|d| d.as_ref())) + .unwrap_or_else(|| &DEFAULT_CONFIG) + } } +// Allow shared refs to default. +const DEFAULT_CONFIG: JsonConfig = JsonConfig { + limit: 32_768, // 2^15 bytes, (~32kB) + err_handler: None, + content_type: None, +}; + impl Default for JsonConfig { fn default() -> Self { - JsonConfig { - limit: 32768, - ehandler: None, - content_type: None, - } + DEFAULT_CONFIG.clone() } } @@ -422,7 +436,7 @@ mod tests { use super::*; use crate::error::InternalError; - use crate::http::header; + use crate::http::header::{self, HeaderValue, CONTENT_LENGTH, CONTENT_TYPE}; use crate::test::{load_stream, TestRequest}; use crate::HttpResponse; @@ -659,4 +673,20 @@ mod tests { let s = Json::::from_request(&req, &mut pl).await; assert!(s.is_err()) } + + #[actix_rt::test] + async fn test_with_config_in_data_wrapper() { + let (req, mut pl) = TestRequest::default() + .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) + .header(CONTENT_LENGTH, HeaderValue::from_static("16")) + .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) + .app_data(web::Data::new(JsonConfig::default().limit(10))) + .to_http_parts(); + + let s = Json::::from_request(&req, &mut pl).await; + assert!(s.is_err()); + + let err_str = s.err().unwrap().to_string(); + assert!(err_str.contains("Json payload size is bigger than allowed")); + } } diff --git a/src/types/payload.rs b/src/types/payload.rs index 653abf089..bbdd89525 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -279,27 +279,24 @@ impl PayloadConfig { Ok(()) } - /// Allow payload config extraction from app data checking both `T` and `Data`, in that - /// order, and falling back to the default payload config. - fn from_req(req: &HttpRequest) -> &PayloadConfig { - req.app_data::() - .or_else(|| { - req.app_data::>() - .map(|d| d.as_ref()) - }) - .unwrap_or_else(|| &DEFAULT_PAYLOAD_CONFIG) + /// 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 { + req.app_data::() + .or_else(|| req.app_data::>().map(|d| d.as_ref())) + .unwrap_or_else(|| &DEFAULT_CONFIG) } } // Allow shared refs to default. -static DEFAULT_PAYLOAD_CONFIG: PayloadConfig = PayloadConfig { +const DEFAULT_CONFIG: PayloadConfig = PayloadConfig { limit: 262_144, // 2^18 bytes (~256kB) mimetype: None, }; impl Default for PayloadConfig { fn default() -> Self { - DEFAULT_PAYLOAD_CONFIG.clone() + DEFAULT_CONFIG.clone() } } From 9a9d4b182eff659894f32ddab360e3060c747e3c Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 3 Sep 2020 10:00:24 +0100 Subject: [PATCH 46/78] document all remaining unsafe usages (#1642) adds some debug assertions where appropriate --- actix-http/src/config.rs | 3 +- actix-http/src/h1/decoder.rs | 4 +- actix-http/src/h1/dispatcher.rs | 4 ++ actix-http/src/h1/encoder.rs | 69 +++++++++++++++++++++++++++------ actix-http/src/h2/dispatcher.rs | 8 ++-- actix-http/src/macros.rs | 5 ++- actix-http/src/ws/mask.rs | 15 ++++--- 7 files changed, 84 insertions(+), 24 deletions(-) diff --git a/actix-http/src/config.rs b/actix-http/src/config.rs index abf3d8ff9..b314d4c99 100644 --- a/actix-http/src/config.rs +++ b/actix-http/src/config.rs @@ -17,7 +17,7 @@ const DATE_VALUE_LENGTH: usize = 29; pub enum KeepAlive { /// Keep alive in seconds Timeout(usize), - /// Relay on OS to shutdown tcp connection + /// Rely on OS to shutdown tcp connection Os, /// Disabled Disabled, @@ -209,6 +209,7 @@ impl Date { date.update(); date } + fn update(&mut self) { self.pos = 0; write!( diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index 8fdd11be5..8e891dc5c 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -76,12 +76,14 @@ pub(crate) trait MessageType: Sized { let name = HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]).unwrap(); - // SAFETY: httparse checks header value is valid UTF-8 + // SAFETY: httparse already checks header value is only visible ASCII bytes + // from_maybe_shared_unchecked contains debug assertions so they are omitted here let value = unsafe { HeaderValue::from_maybe_shared_unchecked( slice.slice(idx.value.0..idx.value.1), ) }; + match name { header::CONTENT_LENGTH => { if let Ok(s) = value.to_str() { diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 00b36562e..7c4de9707 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -314,11 +314,15 @@ where Poll::Ready(Err(err)) => return Err(DispatchError::Io(err)), } } + if written == write_buf.len() { + // SAFETY: setting length to 0 is safe + // skips one length check vs truncate unsafe { write_buf.set_len(0) } } else { write_buf.advance(written); } + Ok(false) } diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index eb8c337dd..e16b4c3dc 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -129,89 +129,133 @@ pub(crate) trait MessageType: Sized { .chain(extra_headers.inner.iter()); // write headers - let mut pos = 0; + let mut has_date = false; - let mut remaining = dst.capacity() - dst.len(); + let mut buf = dst.bytes_mut().as_mut_ptr() as *mut u8; + let mut remaining = dst.capacity() - dst.len(); + + // tracks bytes written since last buffer resize + // since buf is a raw pointer to a bytes container storage but is written to without the + // container's knowledge, this is used to sync the containers cursor after data is written + let mut pos = 0; + for (key, value) in headers { match *key { CONNECTION => continue, TRANSFER_ENCODING | CONTENT_LENGTH if skip_len => continue, - DATE => { - has_date = true; - } + DATE => has_date = true, _ => (), } + let k = key.as_str().as_bytes(); + let k_len = k.len(); + match value { map::Value::One(ref val) => { let v = val.as_ref(); let v_len = v.len(); - let k_len = k.len(); + + // key length + value length + colon + space + \r\n let len = k_len + v_len + 4; + if len > remaining { + // not enough room in buffer for this header; reserve more space + + // SAFETY: all the bytes written up to position "pos" are initialized + // the written byte count and pointer advancement are kept in sync unsafe { dst.advance_mut(pos); } + pos = 0; dst.reserve(len * 2); remaining = dst.capacity() - dst.len(); + + // re-assign buf raw pointer since it's possible that the buffer was + // reallocated and/or resized buf = dst.bytes_mut().as_mut_ptr() as *mut u8; } - // use upper Camel-Case + + // SAFETY: on each write, it is enough to ensure that the advancement of the + // cursor matches the number of bytes written unsafe { + // use upper Camel-Case if camel_case { write_camel_case(k, from_raw_parts_mut(buf, k_len)) } else { write_data(k, buf, k_len) } + buf = buf.add(k_len); + write_data(b": ", buf, 2); buf = buf.add(2); + write_data(v, buf, v_len); buf = buf.add(v_len); + write_data(b"\r\n", buf, 2); buf = buf.add(2); - pos += len; - remaining -= len; } + + pos += len; + remaining -= len; } + map::Value::Multi(ref vec) => { for val in vec { let v = val.as_ref(); let v_len = v.len(); - let k_len = k.len(); let len = k_len + v_len + 4; + if len > remaining { + // SAFETY: all the bytes written up to position "pos" are initialized + // the written byte count and pointer advancement are kept in sync unsafe { dst.advance_mut(pos); } pos = 0; dst.reserve(len * 2); remaining = dst.capacity() - dst.len(); + + // re-assign buf raw pointer since it's possible that the buffer was + // reallocated and/or resized buf = dst.bytes_mut().as_mut_ptr() as *mut u8; } - // use upper Camel-Case + + // SAFETY: on each write, it is enough to ensure that the advancement of + // the cursor matches the number of bytes written unsafe { if camel_case { write_camel_case(k, from_raw_parts_mut(buf, k_len)); } else { write_data(k, buf, k_len); } + buf = buf.add(k_len); + write_data(b": ", buf, 2); buf = buf.add(2); + write_data(v, buf, v_len); buf = buf.add(v_len); + write_data(b"\r\n", buf, 2); buf = buf.add(2); }; + pos += len; remaining -= len; } } } } + + // final cursor synchronization with the bytes container + // + // SAFETY: all the bytes written up to position "pos" are initialized + // the written byte count and pointer advancement are kept in sync unsafe { dst.advance_mut(pos); } @@ -477,7 +521,10 @@ impl<'a> io::Write for Writer<'a> { } } +/// # Safety +/// Callers must ensure that the given length matches given value length. unsafe fn write_data(value: &[u8], buf: *mut u8, len: usize) { + debug_assert_eq!(value.len(), len); copy_nonoverlapping(value.as_ptr(), buf, len); } diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index 534ce928a..daa651f4d 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -227,9 +227,11 @@ where if !has_date { let mut bytes = BytesMut::with_capacity(29); self.config.set_date_header(&mut bytes); - res.headers_mut().insert(DATE, unsafe { - HeaderValue::from_maybe_shared_unchecked(bytes.freeze()) - }); + res.headers_mut().insert( + DATE, + // SAFETY: serialized date-times are known ASCII strings + unsafe { HeaderValue::from_maybe_shared_unchecked(bytes.freeze()) }, + ); } res diff --git a/actix-http/src/macros.rs b/actix-http/src/macros.rs index b970b14f2..e08d62ba6 100644 --- a/actix-http/src/macros.rs +++ b/actix-http/src/macros.rs @@ -38,7 +38,7 @@ macro_rules! downcast { /// Downcasts generic body to a specific type. pub fn downcast_ref(&self) -> Option<&T> { if self.__private_get_type_id__().0 == std::any::TypeId::of::() { - // Safety: external crates cannot override the default + // 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 @@ -48,10 +48,11 @@ macro_rules! downcast { 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__().0 == std::any::TypeId::of::() { - // Safety: external crates cannot override the default + // 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 diff --git a/actix-http/src/ws/mask.rs b/actix-http/src/ws/mask.rs index 367fb0212..726b1a4a1 100644 --- a/actix-http/src/ws/mask.rs +++ b/actix-http/src/ws/mask.rs @@ -7,6 +7,8 @@ use std::slice; struct ShortSlice<'a>(&'a mut [u8]); impl<'a> ShortSlice<'a> { + /// # Safety + /// Given slice must be shorter than 8 bytes. unsafe fn new(slice: &'a mut [u8]) -> Self { // Sanity check for debug builds debug_assert!(slice.len() < 8); @@ -46,13 +48,13 @@ pub(crate) fn apply_mask(buf: &mut [u8], mask_u32: u32) { } } -#[inline] // TODO: copy_nonoverlapping here compiles to call memcpy. While it is not so // inefficient, it could be done better. The compiler does not understand that // a `ShortSlice` must be smaller than a u64. +#[inline] #[allow(clippy::needless_pass_by_value)] fn xor_short(buf: ShortSlice<'_>, mask: u64) { - // Unsafe: we know that a `ShortSlice` fits in a u64 + // SAFETY: we know that a `ShortSlice` fits in a u64 unsafe { let (ptr, len) = (buf.0.as_mut_ptr(), buf.0.len()); let mut b: u64 = 0; @@ -64,8 +66,9 @@ fn xor_short(buf: ShortSlice<'_>, mask: u64) { } } +/// # Safety +/// Caller must ensure the buffer has the correct size and alignment. #[inline] -// Unsafe: caller must ensure the buffer has the correct size and alignment unsafe fn cast_slice(buf: &mut [u8]) -> &mut [u64] { // Assert correct size and alignment in debug builds debug_assert!(buf.len().trailing_zeros() >= 3); @@ -74,9 +77,9 @@ unsafe fn cast_slice(buf: &mut [u8]) -> &mut [u64] { slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut u64, buf.len() >> 3) } -#[inline] // Splits a slice into three parts: an unaligned short head and tail, plus an aligned // u64 mid section. +#[inline] fn align_buf(buf: &mut [u8]) -> (ShortSlice<'_>, &mut [u64], ShortSlice<'_>) { let start_ptr = buf.as_ptr() as usize; let end_ptr = start_ptr + buf.len(); @@ -91,13 +94,13 @@ fn align_buf(buf: &mut [u8]) -> (ShortSlice<'_>, &mut [u64], ShortSlice<'_>) { let (tmp, tail) = buf.split_at_mut(end_aligned - start_ptr); let (head, mid) = tmp.split_at_mut(start_aligned - start_ptr); - // Unsafe: we know the middle section is correctly aligned, and the outer + // SAFETY: we know the middle section is correctly aligned, and the outer // sections are smaller than 8 bytes unsafe { (ShortSlice::new(head), cast_slice(mid), ShortSlice(tail)) } } else { // We didn't cross even one aligned boundary! - // Unsafe: The outer sections are smaller than 8 bytes + // SAFETY: The outer sections are smaller than 8 bytes unsafe { (ShortSlice::new(buf), &mut [], ShortSlice::new(&mut [])) } } } From c54d73e0bbf517284ef2d0092e0e77746db2b60e Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Mon, 7 Sep 2020 14:04:54 +0300 Subject: [PATCH 47/78] Improve awc websocket docs (#1654) Co-authored-by: Rob Ede --- awc/src/lib.rs | 3 ++- awc/src/ws.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 1cc31a194..a98d6767d 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -193,7 +193,8 @@ impl Client { self.request(Method::OPTIONS, url) } - /// Construct WebSockets request. + /// Initialize a WebSocket connection. + /// Returns a WebSocket connection builder. pub fn ws(&self, url: U) -> ws::WebsocketsRequest where Uri: TryFrom, diff --git a/awc/src/ws.rs b/awc/src/ws.rs index 8b01e5716..96687ac74 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -1,4 +1,31 @@ //! Websockets client +//! +//! Type definitions required to use [`awc::Client`](../struct.Client.html) as a WebSocket client. +//! +//! # Example +//! +//! ``` +//! use awc::{Client, ws}; +//! use futures_util::{sink::SinkExt, stream::StreamExt}; +//! +//! #[actix_rt::main] +//! async fn main() { +//! let (_resp, mut connection) = Client::new() +//! .ws("ws://echo.websocket.org") +//! .connect() +//! .await +//! .unwrap(); +//! +//! connection +//! .send(ws::Message::Text("Echo".to_string())) +//! .await +//! .unwrap(); +//! let response = connection.next().await.unwrap().unwrap(); +//! +//! assert_eq!(response, ws::Frame::Text("Echo".as_bytes().into())); +//! } +//! ``` + use std::convert::TryFrom; use std::net::SocketAddr; use std::rc::Rc; From 9d0534999d91bb512581021e85da552cf91badc0 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 9 Sep 2020 09:20:54 +0100 Subject: [PATCH 48/78] bump connect and tls versions (#1655) --- CHANGES.md | 4 ++++ Cargo.toml | 2 +- MIGRATION.md | 4 ++++ actix-http/Cargo.toml | 8 ++++---- actix-http/src/h1/service.rs | 16 ++++++++-------- actix-http/src/h2/service.rs | 16 ++++++++-------- actix-http/src/service.rs | 16 ++++++++-------- awc/Cargo.toml | 4 ++-- docs/graphs/net-only.dot | 2 +- docs/graphs/web-focus.dot | 2 +- src/server.rs | 29 ++++++++++++++++------------- test-server/Cargo.toml | 2 +- tests/test_httpserver.rs | 4 ++-- 13 files changed, 60 insertions(+), 49 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 82c562f5c..291aa8227 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,10 +9,14 @@ * Update actix-codec and actix-utils dependencies. [#1634] * `FormConfig` and `JsonConfig` configurations are now also considered when set using `App::data`. [#1641] +* `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`. [#1655] +* `HttpServer::maxconnrate` is renamed to the more expressive + `HttpServer::max_connection_rate`. [#1655] [#1639]: https://github.com/actix/actix-web/pull/1639 [#1641]: https://github.com/actix/actix-web/pull/1641 [#1634]: https://github.com/actix/actix-web/pull/1634 +[#1655]: https://github.com/actix/actix-web/pull/1655 ## 3.0.0-beta.3 - 2020-08-17 ### Changed diff --git a/Cargo.toml b/Cargo.toml index dbdab5937..6a02b30f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,7 +74,7 @@ actix-server = "1.0.0" actix-testing = "1.0.0" actix-macros = "0.1.0" actix-threadpool = "0.3.1" -actix-tls = "2.0.0-alpha.2" +actix-tls = "2.0.0" actix-web-codegen = "0.3.0-beta.1" actix-http = "2.0.0-beta.3" diff --git a/MIGRATION.md b/MIGRATION.md index 0e73b7d47..15045ed69 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -36,6 +36,10 @@ It will need `middleware::normalize::TrailingSlash` when being constructed with `NormalizePath::new(...)`, or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::default())`. +* `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`. + +* `HttpServer::maxconnrate` is renamed to the more expressive `HttpServer::max_connection_rate`. + ## 2.0.0 * `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 750d1e0af..3c8fb2e21 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -42,11 +42,11 @@ actors = ["actix"] [dependencies] actix-service = "1.0.5" actix-codec = "0.3.0" -actix-connect = "2.0.0-alpha.4" +actix-connect = "2.0.0" actix-utils = "2.0.0" actix-rt = "1.0.0" actix-threadpool = "0.3.1" -actix-tls = { version = "2.0.0-alpha.2", optional = true } +actix-tls = { version = "2.0.0", optional = true } actix = { version = "0.10.0-alpha.1", optional = true } base64 = "0.12" @@ -87,9 +87,9 @@ flate2 = { version = "1.0.13", optional = true } [dev-dependencies] actix-server = "1.0.1" -actix-connect = { version = "2.0.0-alpha.4", features = ["openssl"] } +actix-connect = { version = "2.0.0", features = ["openssl"] } actix-http-test = { version = "2.0.0-alpha.1", features = ["openssl"] } -actix-tls = { version = "2.0.0-alpha.2", features = ["openssl"] } +actix-tls = { version = "2.0.0", features = ["openssl"] } criterion = "0.3" env_logger = "0.7" serde_derive = "1.0" diff --git a/actix-http/src/h1/service.rs b/actix-http/src/h1/service.rs index 339a0f538..6aafd4089 100644 --- a/actix-http/src/h1/service.rs +++ b/actix-http/src/h1/service.rs @@ -98,7 +98,7 @@ mod openssl { use super::*; use actix_tls::openssl::{Acceptor, SslAcceptor, SslStream}; - use actix_tls::{openssl::HandshakeError, SslError}; + use actix_tls::{openssl::HandshakeError, TlsError}; impl H1Service, S, B, X, U> where @@ -126,19 +126,19 @@ mod openssl { Config = (), Request = TcpStream, Response = (), - Error = SslError, DispatchError>, + Error = TlsError, DispatchError>, InitError = (), > { pipeline_factory( Acceptor::new(acceptor) - .map_err(SslError::Ssl) + .map_err(TlsError::Tls) .map_init_err(|_| panic!()), ) .and_then(|io: SslStream| { let peer_addr = io.get_ref().peer_addr().ok(); ok((io, peer_addr)) }) - .and_then(self.map_err(SslError::Service)) + .and_then(self.map_err(TlsError::Service)) } } } @@ -147,7 +147,7 @@ mod openssl { mod rustls { use super::*; use actix_tls::rustls::{Acceptor, ServerConfig, TlsStream}; - use actix_tls::SslError; + use actix_tls::TlsError; use std::{fmt, io}; impl H1Service, S, B, X, U> @@ -176,19 +176,19 @@ mod rustls { Config = (), Request = TcpStream, Response = (), - Error = SslError, + Error = TlsError, InitError = (), > { pipeline_factory( Acceptor::new(config) - .map_err(SslError::Ssl) + .map_err(TlsError::Tls) .map_init_err(|_| panic!()), ) .and_then(|io: TlsStream| { let peer_addr = io.get_ref().0.peer_addr().ok(); ok((io, peer_addr)) }) - .and_then(self.map_err(SslError::Service)) + .and_then(self.map_err(TlsError::Service)) } } } diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index eef5dd02c..6b5620e02 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -97,7 +97,7 @@ where mod openssl { use actix_service::{fn_factory, fn_service}; use actix_tls::openssl::{Acceptor, SslAcceptor, SslStream}; - use actix_tls::{openssl::HandshakeError, SslError}; + use actix_tls::{openssl::HandshakeError, TlsError}; use super::*; @@ -117,12 +117,12 @@ mod openssl { Config = (), Request = TcpStream, Response = (), - Error = SslError, DispatchError>, + Error = TlsError, DispatchError>, InitError = S::InitError, > { pipeline_factory( Acceptor::new(acceptor) - .map_err(SslError::Ssl) + .map_err(TlsError::Tls) .map_init_err(|_| panic!()), ) .and_then(fn_factory(|| { @@ -131,7 +131,7 @@ mod openssl { ok((io, peer_addr)) })) })) - .and_then(self.map_err(SslError::Service)) + .and_then(self.map_err(TlsError::Service)) } } } @@ -140,7 +140,7 @@ mod openssl { mod rustls { use super::*; use actix_tls::rustls::{Acceptor, ServerConfig, TlsStream}; - use actix_tls::SslError; + use actix_tls::TlsError; use std::io; impl H2Service, S, B> @@ -159,7 +159,7 @@ mod rustls { Config = (), Request = TcpStream, Response = (), - Error = SslError, + Error = TlsError, InitError = S::InitError, > { let protos = vec!["h2".to_string().into()]; @@ -167,7 +167,7 @@ mod rustls { pipeline_factory( Acceptor::new(config) - .map_err(SslError::Ssl) + .map_err(TlsError::Tls) .map_init_err(|_| panic!()), ) .and_then(fn_factory(|| { @@ -176,7 +176,7 @@ mod rustls { ok((io, peer_addr)) })) })) - .and_then(self.map_err(SslError::Service)) + .and_then(self.map_err(TlsError::Service)) } } } diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index 94cdbc828..9ee579702 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -195,7 +195,7 @@ where mod openssl { use super::*; use actix_tls::openssl::{Acceptor, SslAcceptor, SslStream}; - use actix_tls::{openssl::HandshakeError, SslError}; + use actix_tls::{openssl::HandshakeError, TlsError}; impl HttpService, S, B, X, U> where @@ -226,12 +226,12 @@ mod openssl { Config = (), Request = TcpStream, Response = (), - Error = SslError, DispatchError>, + Error = TlsError, DispatchError>, InitError = (), > { pipeline_factory( Acceptor::new(acceptor) - .map_err(SslError::Ssl) + .map_err(TlsError::Tls) .map_init_err(|_| panic!()), ) .and_then(|io: SslStream| { @@ -247,7 +247,7 @@ mod openssl { let peer_addr = io.get_ref().peer_addr().ok(); ok((io, proto, peer_addr)) }) - .and_then(self.map_err(SslError::Service)) + .and_then(self.map_err(TlsError::Service)) } } } @@ -256,7 +256,7 @@ mod openssl { mod rustls { use super::*; use actix_tls::rustls::{Acceptor, ServerConfig, Session, TlsStream}; - use actix_tls::SslError; + use actix_tls::TlsError; use std::io; impl HttpService, S, B, X, U> @@ -288,7 +288,7 @@ mod rustls { Config = (), Request = TcpStream, Response = (), - Error = SslError, + Error = TlsError, InitError = (), > { let protos = vec!["h2".to_string().into(), "http/1.1".to_string().into()]; @@ -296,7 +296,7 @@ mod rustls { pipeline_factory( Acceptor::new(config) - .map_err(SslError::Ssl) + .map_err(TlsError::Tls) .map_init_err(|_| panic!()), ) .and_then(|io: TlsStream| { @@ -312,7 +312,7 @@ mod rustls { let peer_addr = io.get_ref().0.peer_addr().ok(); ok((io, proto, peer_addr)) }) - .and_then(self.map_err(SslError::Service)) + .and_then(self.map_err(TlsError::Service)) } } } diff --git a/awc/Cargo.toml b/awc/Cargo.toml index ff0afaa1c..054f465c0 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -57,13 +57,13 @@ open-ssl = { version = "0.10", package = "openssl", optional = true } rust-tls = { version = "0.18.0", package = "rustls", optional = true, features = ["dangerous_configuration"] } [dev-dependencies] -actix-connect = { version = "2.0.0-alpha.4", features = ["openssl"] } +actix-connect = { version = "2.0.0", features = ["openssl"] } actix-web = { version = "3.0.0-beta.2", features = ["openssl"] } actix-http = { version = "2.0.0-beta.3", features = ["openssl"] } actix-http-test = { version = "2.0.0-alpha.1", features = ["openssl"] } actix-utils = "2.0.0" actix-server = "1.0.0" -actix-tls = { version = "2.0.0-alpha.2", features = ["openssl", "rustls"] } +actix-tls = { version = "2.0.0", features = ["openssl", "rustls"] } brotli2 = "0.3.2" flate2 = "1.0.13" futures-util = { version = "0.3.5", default-features = false } diff --git a/docs/graphs/net-only.dot b/docs/graphs/net-only.dot index d9f2317a1..0eebf2a6f 100644 --- a/docs/graphs/net-only.dot +++ b/docs/graphs/net-only.dot @@ -17,7 +17,7 @@ digraph { "actix-utils" -> { "actix-service" "actix-rt" "actix-codec" } "actix-tracing" -> { "actix-service" } - "actix-tls" -> { "actix-service" "actix-codec" "actix-utils" "actix-rt" } + "actix-tls" -> { "actix-service" "actix-codec" "actix-utils" } "actix-testing" -> { "actix-rt" "actix-macros" "actix-server" "actix-service" } "actix-server" -> { "actix-service" "actix-rt" "actix-codec" "actix-utils" } "actix-rt" -> { "actix-macros" "actix-threadpool" } diff --git a/docs/graphs/web-focus.dot b/docs/graphs/web-focus.dot index b0ce18d02..7abd51268 100644 --- a/docs/graphs/web-focus.dot +++ b/docs/graphs/web-focus.dot @@ -22,7 +22,7 @@ digraph { "actix-utils" -> { "actix-service" "actix-rt" "actix-codec" } "actix-tracing" -> { "actix-service" } - "actix-tls" -> { "actix-service" "actix-codec" "actix-utils" "actix-rt" } + "actix-tls" -> { "actix-service" "actix-codec" "actix-utils" } "actix-testing" -> { "actix-rt" "actix-macros" "actix-server" "actix-service" } "actix-server" -> { "actix-service" "actix-rt" "actix-codec" "actix-utils" } "actix-rt" -> { "actix-macros" "actix-threadpool" } diff --git a/src/server.rs b/src/server.rs index b2695b004..2b86f7416 100644 --- a/src/server.rs +++ b/src/server.rs @@ -122,23 +122,23 @@ where /// Sets the maximum per-worker number of concurrent connections. /// - /// All socket listeners will stop accepting connections when this limit is reached - /// for each worker. + /// All socket listeners will stop accepting connections when this limit is reached for + /// each worker. /// /// By default max connections is set to a 25k. - pub fn maxconn(mut self, num: usize) -> Self { + pub fn max_connections(mut self, num: usize) -> Self { self.builder = self.builder.maxconn(num); self } /// Sets the maximum per-worker concurrent connection establish process. /// - /// All listeners will stop accepting connections when this limit is reached. It - /// can be used to limit the global SSL CPU usage. + /// All listeners will stop accepting connections when this limit is reached. It can be used to + /// limit the global TLS CPU usage. /// /// By default max connections is set to a 256. - pub fn maxconnrate(self, num: usize) -> Self { - actix_tls::max_concurrent_ssl_connect(num); + pub fn max_connection_rate(self, num: usize) -> Self { + actix_tls::max_concurrent_tls_connect(num); self } @@ -375,19 +375,20 @@ where addr: A, ) -> io::Result> { let mut err = None; - let mut succ = false; + let mut success = false; let mut sockets = Vec::new(); + for addr in addr.to_socket_addrs()? { match create_tcp_listener(addr, self.backlog) { Ok(lst) => { - succ = true; + success = true; sockets.push(lst); } Err(e) => err = Some(e), } } - if !succ { + if !success { if let Some(e) = err.take() { Err(e) } else { @@ -575,17 +576,19 @@ fn create_tcp_listener( #[cfg(feature = "openssl")] /// Configure `SslAcceptorBuilder` with custom server flags. fn openssl_acceptor(mut builder: SslAcceptorBuilder) -> io::Result { - builder.set_alpn_select_callback(|_, protos| { + builder.set_alpn_select_callback(|_, protocols| { const H2: &[u8] = b"\x02h2"; const H11: &[u8] = b"\x08http/1.1"; - if protos.windows(3).any(|window| window == H2) { + + if protocols.windows(3).any(|window| window == H2) { Ok(b"h2") - } else if protos.windows(9).any(|window| window == H11) { + } else if protocols.windows(9).any(|window| window == H11) { Ok(b"http/1.1") } else { Err(AlpnError::NOACK) } }); + builder.set_alpn_protos(b"\x08http/1.1\x02h2")?; Ok(builder.build()) diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index 13f27ab59..b82dc5432 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -31,7 +31,7 @@ openssl = ["open-ssl", "awc/openssl"] [dependencies] actix-service = "1.0.1" actix-codec = "0.3.0" -actix-connect = "2.0.0-alpha.4" +actix-connect = "2.0.0" actix-utils = "2.0.0" actix-rt = "1.0.0" actix-server = "1.0.0" diff --git a/tests/test_httpserver.rs b/tests/test_httpserver.rs index 750084fdc..50c0a7649 100644 --- a/tests/test_httpserver.rs +++ b/tests/test_httpserver.rs @@ -22,8 +22,8 @@ async fn test_start() { }) .workers(1) .backlog(1) - .maxconn(10) - .maxconnrate(10) + .max_connections(10) + .max_connection_rate(10) .keep_alive(10) .client_timeout(5000) .client_shutdown(0) From 3a27580ebe89ad7bb082bcb7bc2c1c26b2be2690 Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Wed, 9 Sep 2020 16:24:12 +0300 Subject: [PATCH 49/78] awc: improve module documentation (#1656) Co-authored-by: Rob Ede --- awc/src/lib.rs | 101 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 86 insertions(+), 15 deletions(-) diff --git a/awc/src/lib.rs b/awc/src/lib.rs index a98d6767d..7167dbb96 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -4,24 +4,93 @@ clippy::borrow_interior_mutable_const, clippy::needless_doctest_main )] -//! An HTTP Client + +//! `awc` is a HTTP and WebSocket client library built using the Actix ecosystem. +//! +//! ## Making a GET request //! //! ```rust -//! use actix_rt::System; -//! use awc::Client; +//! # #[actix_rt::main] +//! # async fn main() -> Result<(), awc::error::SendRequestError> { +//! let mut client = awc::Client::default(); +//! let response = client.get("http://www.rust-lang.org") // <- Create request builder +//! .header("User-Agent", "Actix-web") +//! .send() // <- Send http request +//! .await?; //! -//! #[actix_rt::main] -//! async fn main() { -//! let mut client = Client::default(); -//! -//! let response = client.get("http://www.rust-lang.org") // <- Create request builder -//! .header("User-Agent", "Actix-web") -//! .send() // <- Send http request -//! .await; -//! -//! println!("Response: {:?}", response); -//! } +//! println!("Response: {:?}", response); +//! # Ok(()) +//! # } //! ``` +//! +//! ## Making POST requests +//! +//! ### Raw body contents +//! +//! ```rust +//! # #[actix_rt::main] +//! # async fn main() -> Result<(), awc::error::SendRequestError> { +//! let mut client = awc::Client::default(); +//! let response = client.post("http://httpbin.org/post") +//! .send_body("Raw body contents") +//! .await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ### Forms +//! +//! ```rust +//! # #[actix_rt::main] +//! # async fn main() -> Result<(), awc::error::SendRequestError> { +//! let params = [("foo", "bar"), ("baz", "quux")]; +//! +//! let mut client = awc::Client::default(); +//! let response = client.post("http://httpbin.org/post") +//! .send_form(¶ms) +//! .await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ### JSON +//! +//! ```rust +//! # #[actix_rt::main] +//! # async fn main() -> Result<(), awc::error::SendRequestError> { +//! let request = serde_json::json!({ +//! "lang": "rust", +//! "body": "json" +//! }); +//! +//! let mut client = awc::Client::default(); +//! let response = client.post("http://httpbin.org/post") +//! .send_json(&request) +//! .await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ## WebSocket support +//! +//! ``` +//! # #[actix_rt::main] +//! # async fn main() -> Result<(), Box> { +//! use futures_util::{sink::SinkExt, stream::StreamExt}; +//! let (_resp, mut connection) = awc::Client::new() +//! .ws("ws://echo.websocket.org") +//! .connect() +//! .await?; +//! +//! connection +//! .send(awc::ws::Message::Text("Echo".to_string())) +//! .await?; +//! let response = connection.next().await.unwrap()?; +//! # assert_eq!(response, awc::ws::Frame::Text("Echo".as_bytes().into())); +//! # Ok(()) +//! # } +//! ``` + use std::cell::RefCell; use std::convert::TryFrom; use std::rc::Rc; @@ -51,7 +120,9 @@ pub use self::sender::SendClientRequest; use self::connect::{Connect, ConnectorWrapper}; -/// An HTTP Client +/// An asynchronous HTTP and WebSocket client. +/// +/// ## Examples /// /// ```rust /// use awc::Client; From 059d1671d7649aad833b5271bcd8172235352bc7 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 9 Sep 2020 22:14:11 +0100 Subject: [PATCH 50/78] prepare release beta 4 (#1659) --- Cargo.toml | 6 +- actix-files/Cargo.toml | 6 +- actix-http/CHANGES.md | 7 +- actix-http/Cargo.toml | 2 +- actix-http/LICENSE-APACHE | 202 +---------------------------------- actix-http/LICENSE-MIT | 26 +---- actix-multipart/Cargo.toml | 4 +- actix-web-actors/Cargo.toml | 4 +- actix-web-codegen/Cargo.toml | 2 +- awc/CHANGES.md | 5 +- awc/Cargo.toml | 8 +- test-server/Cargo.toml | 6 +- 12 files changed, 31 insertions(+), 247 deletions(-) mode change 100644 => 120000 actix-http/LICENSE-APACHE mode change 100644 => 120000 actix-http/LICENSE-MIT diff --git a/Cargo.toml b/Cargo.toml index 6a02b30f9..11559bcae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "3.0.0-beta.3" +version = "3.0.0-beta.4" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" @@ -77,8 +77,8 @@ actix-threadpool = "0.3.1" actix-tls = "2.0.0" actix-web-codegen = "0.3.0-beta.1" -actix-http = "2.0.0-beta.3" -awc = { version = "2.0.0-beta.3", default-features = false } +actix-http = "2.0.0-beta.4" +awc = { version = "2.0.0-beta.4", default-features = false } bytes = "0.5.3" derive_more = "0.99.2" diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index c3444809e..4c6441324 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -17,8 +17,8 @@ name = "actix_files" path = "src/lib.rs" [dependencies] -actix-web = { version = "3.0.0-beta.3", default-features = false } -actix-http = "2.0.0-beta.3" +actix-web = { version = "3.0.0-beta.4", default-features = false } +actix-http = "2.0.0-beta.4" actix-service = "1.0.1" bitflags = "1" bytes = "0.5.3" @@ -33,4 +33,4 @@ v_htmlescape = "0.10" [dev-dependencies] actix-rt = "1.0.0" -actix-web = { version = "3.0.0-beta.3", features = ["openssl"] } +actix-web = { version = "3.0.0-beta.4", features = ["openssl"] } diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index d8d674fb1..dfa55e7f1 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,8 +1,13 @@ # Changes -## Unreleased +## Unreleased - 2020-xx-xx + + +## 2.0.0-beta.4 - 2020-09-09 ### Changed * Update actix-codec and actix-utils dependencies. +* Update actix-connect and actix-tls dependencies. + ## [2.0.0-beta.3] - 2020-08-14 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 3c8fb2e21..6d8d143ed 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "2.0.0-beta.3" +version = "2.0.0-beta.4" authors = ["Nikolay Kim "] description = "Actix HTTP primitives" readme = "README.md" diff --git a/actix-http/LICENSE-APACHE b/actix-http/LICENSE-APACHE deleted file mode 100644 index 6cdf2d16c..000000000 --- a/actix-http/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2017-NOW Nikolay Kim - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/actix-http/LICENSE-APACHE b/actix-http/LICENSE-APACHE new file mode 120000 index 000000000..965b606f3 --- /dev/null +++ b/actix-http/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/actix-http/LICENSE-MIT b/actix-http/LICENSE-MIT deleted file mode 100644 index 0f80296ae..000000000 --- a/actix-http/LICENSE-MIT +++ /dev/null @@ -1,25 +0,0 @@ -Copyright (c) 2017 Nikolay Kim - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. diff --git a/actix-http/LICENSE-MIT b/actix-http/LICENSE-MIT new file mode 120000 index 000000000..76219eb72 --- /dev/null +++ b/actix-http/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 58f7113e5..1ea2c7aac 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 = "3.0.0-beta.3", default-features = false } +actix-web = { version = "3.0.0-beta.4", default-features = false } actix-service = "1.0.1" actix-utils = "2.0.0" bytes = "0.5.3" @@ -29,4 +29,4 @@ twoway = "0.2" [dev-dependencies] actix-rt = "1.0.0" -actix-http = "2.0.0-beta.3" +actix-http = "2.0.0-beta.4" diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index b33acc222..0ca340605 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -17,8 +17,8 @@ path = "src/lib.rs" [dependencies] actix = "0.10.0-alpha.2" -actix-web = { version = "3.0.0-beta.3", default-features = false } -actix-http = "2.0.0-beta.3" +actix-web = { version = "3.0.0-beta.4", default-features = false } +actix-http = "2.0.0-beta.4" actix-codec = "0.3.0" bytes = "0.5.2" futures-channel = { version = "0.3.5", default-features = false } diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 62cbf2941..ddd886c40 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -20,5 +20,5 @@ proc-macro2 = "1" [dev-dependencies] actix-rt = "1.0.0" -actix-web = "3.0.0-beta.3" +actix-web = "3.0.0-beta.4" futures-util = { version = "0.3.5", default-features = false } diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 8d1b58856..4a830b8fc 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,8 +1,11 @@ # Changes ## Unreleased - 2020-xx-xx + + +## 2.0.0-beta.4 - 2020-09-09 ### Changed -* Update actix-codec dependency. +* Update actix-codec & actix-tls dependencies. ## 2.0.0-beta.3 - 2020-08-17 diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 054f465c0..b55f4dabb 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "2.0.0-beta.3" +version = "2.0.0-beta.4" authors = ["Nikolay Kim "] description = "Async HTTP client library that uses the Actix runtime." readme = "README.md" @@ -39,7 +39,7 @@ compress = ["actix-http/compress"] [dependencies] actix-codec = "0.3.0" actix-service = "1.0.1" -actix-http = "2.0.0-beta.3" +actix-http = "2.0.0-beta.4" actix-rt = "1.0.0" base64 = "0.12" @@ -58,8 +58,8 @@ rust-tls = { version = "0.18.0", package = "rustls", optional = true, features = [dev-dependencies] actix-connect = { version = "2.0.0", features = ["openssl"] } -actix-web = { version = "3.0.0-beta.2", features = ["openssl"] } -actix-http = { version = "2.0.0-beta.3", features = ["openssl"] } +actix-web = { version = "3.0.0-beta.4", features = ["openssl"] } +actix-http = { version = "2.0.0-beta.4", features = ["openssl"] } actix-http-test = { version = "2.0.0-alpha.1", features = ["openssl"] } actix-utils = "2.0.0" actix-server = "1.0.0" diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index b82dc5432..8aecda401 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -36,7 +36,7 @@ actix-utils = "2.0.0" actix-rt = "1.0.0" actix-server = "1.0.0" actix-testing = "1.0.0" -awc = "2.0.0-beta.3" +awc = "2.0.0-beta.4" base64 = "0.12" bytes = "0.5.3" @@ -52,5 +52,5 @@ time = { version = "0.2.7", default-features = false, features = ["std"] } open-ssl = { version = "0.10", package = "openssl", optional = true } [dev-dependencies] -actix-web = "3.0.0-beta.3" -actix-http = "2.0.0-beta.3" +actix-web = "3.0.0-beta.4" +actix-http = "2.0.0-beta.4" From e39d166a1751dfc03e39613f015b164ed6061cfb Mon Sep 17 00:00:00 2001 From: Mufeed VH Date: Thu, 10 Sep 2020 04:42:50 +0530 Subject: [PATCH 51/78] Fix examples hyperlink in README (#1660) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a42a1a6f8..48f9d1442 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ ## Documentation * [Website & User Guide](https://actix.rs) -* [Examples Repository](https://actix.rs/actix-web/actix_web) +* [Examples Repository](https://github.com/actix/examples) * [API Documentation](https://docs.rs/actix-web) * [API Documentation (master branch)](https://actix.rs/actix-web/actix_web) From 2f6e9738c4bbb7478671a0e92f178524febf2fea Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 10 Sep 2020 12:54:27 +0100 Subject: [PATCH 52/78] prepare multipart and actors releases (#1663) --- CHANGES.md | 3 +++ actix-multipart/CHANGES.md | 4 ++++ actix-multipart/Cargo.toml | 2 +- actix-multipart/src/lib.rs | 2 ++ actix-web-actors/CHANGES.md | 6 +++++- actix-web-actors/Cargo.toml | 2 +- actix-web-actors/src/lib.rs | 4 +++- 7 files changed, 19 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 291aa8227..1779af9ee 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2020-xx-xx + + +## 3.0.0-beta.4 - 2020-09-09 ### Added * `middleware::NormalizePath` now has configurable behaviour for either always having a trailing slash, or as the new addition, always trimming trailing slashes. [#1639] diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 261836223..7149f6713 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2020-xx-xx +## 3.0.0-beta.2 - 2020-09-10 +* Update `actix-*` dependencies to latest versions. + + ## 0.3.0-beta.1 - 2020-07-15 * Update `actix-web` to 3.0.0-beta.1 diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 1ea2c7aac..0fd5d0922 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-multipart" -version = "0.3.0-beta.1" +version = "0.3.0-beta.2" authors = ["Nikolay Kim "] description = "Multipart support for actix web framework." readme = "README.md" diff --git a/actix-multipart/src/lib.rs b/actix-multipart/src/lib.rs index 43eb048ca..b502c63d8 100644 --- a/actix-multipart/src/lib.rs +++ b/actix-multipart/src/lib.rs @@ -1,3 +1,5 @@ +//! Multipart form support for Actix web. + #![allow(clippy::borrow_interior_mutable_const)] mod error; diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index 868f17e2a..4dcd403a3 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -1,6 +1,10 @@ # Changes -## [Unreleased] - 2020-xx-xx +## Unreleased - 2020-xx-xx + + +## 3.0.0-beta.2 - 2020-09-10 +* Update `actix-*` dependencies to latest versions. ## [3.0.0-beta.1] - 2020-xx-xx diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 0ca340605..cb7fd3a80 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-actors" -version = "3.0.0-beta.1" +version = "3.0.0-beta.2" authors = ["Nikolay Kim "] description = "Actix actors support for actix web framework." readme = "README.md" diff --git a/actix-web-actors/src/lib.rs b/actix-web-actors/src/lib.rs index 6360917cd..d6be5eeaf 100644 --- a/actix-web-actors/src/lib.rs +++ b/actix-web-actors/src/lib.rs @@ -1,5 +1,7 @@ -#![allow(clippy::borrow_interior_mutable_const)] //! Actix actors integration for Actix web framework + +#![allow(clippy::borrow_interior_mutable_const)] + mod context; pub mod ws; From 7787638f262cf84c86556a2f3538ee8fe7226a26 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 10 Sep 2020 14:46:35 +0100 Subject: [PATCH 53/78] fix CI clippy warnings (#1664) --- actix-files/src/lib.rs | 27 +++++++++++++++------------ actix-http/src/body.rs | 2 +- actix-http/src/client/pool.rs | 4 ++-- actix-http/src/lib.rs | 3 ++- actix-http/src/macros.rs | 2 +- actix-multipart/src/lib.rs | 1 + actix-multipart/src/server.rs | 18 +++++++++++------- actix-web-actors/src/lib.rs | 1 + awc/src/lib.rs | 2 +- awc/src/sender.rs | 6 +----- src/lib.rs | 2 +- 11 files changed, 37 insertions(+), 31 deletions(-) diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index ae0204a71..91c054947 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -1,6 +1,8 @@ -#![allow(clippy::borrow_interior_mutable_const, clippy::type_complexity)] - //! Static files support + +#![deny(rust_2018_idioms)] +#![allow(clippy::borrow_interior_mutable_const)] + use std::cell::RefCell; use std::fmt::Write; use std::fs::{DirEntry, File}; @@ -62,6 +64,7 @@ pub struct ChunkedReadFile { size: u64, offset: u64, file: Option, + #[allow(clippy::type_complexity)] fut: Option>>>, counter: u64, @@ -72,7 +75,7 @@ impl Stream for ChunkedReadFile { fn poll_next( mut self: Pin<&mut Self>, - cx: &mut Context, + cx: &mut Context<'_>, ) -> Poll> { if let Some(ref mut fut) = self.fut { return match Pin::new(fut).poll(cx) { @@ -224,7 +227,7 @@ fn directory_listing( )) } -type MimeOverride = dyn Fn(&mime::Name) -> DispositionType; +type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType; /// Static files handling /// @@ -232,12 +235,10 @@ type MimeOverride = dyn Fn(&mime::Name) -> DispositionType; /// /// ```rust /// use actix_web::App; -/// use actix_files as fs; +/// use actix_files::Files; /// -/// fn main() { -/// let app = App::new() -/// .service(fs::Files::new("/static", ".")); -/// } +/// let app = App::new() +/// .service(Files::new("/static", ".")); /// ``` pub struct Files { path: String, @@ -330,7 +331,7 @@ impl Files { /// Specifies mime override callback pub fn mime_override(mut self, f: F) -> Self where - F: Fn(&mime::Name) -> DispositionType + 'static, + F: Fn(&mime::Name<'_>) -> DispositionType + 'static, { self.mime_override = Some(Rc::new(f)); self @@ -469,6 +470,7 @@ pub struct FilesService { } impl FilesService { + #[allow(clippy::type_complexity)] fn handle_err( &mut self, e: io::Error, @@ -490,12 +492,13 @@ impl Service for FilesService { type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; + #[allow(clippy::type_complexity)] type Future = Either< Ready>, LocalBoxFuture<'static, Result>, >; - fn poll_ready(&mut self, _: &mut Context) -> Poll> { + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { Poll::Ready(Ok(())) } @@ -898,7 +901,7 @@ mod tests { #[actix_rt::test] async fn test_mime_override() { - fn all_attachment(_: &mime::Name) -> DispositionType { + fn all_attachment(_: &mime::Name<'_>) -> DispositionType { DispositionType::Attachment } diff --git a/actix-http/src/body.rs b/actix-http/src/body.rs index 8bea8e6b8..c5d831c45 100644 --- a/actix-http/src/body.rs +++ b/actix-http/src/body.rs @@ -714,7 +714,7 @@ mod tests { let body = resp_body.downcast_ref::().unwrap(); assert_eq!(body, "hello cast"); let body = &mut resp_body.downcast_mut::().unwrap(); - body.push_str("!"); + body.push('!'); let body = resp_body.downcast_ref::().unwrap(); assert_eq!(body, "hello cast!"); let not_body = resp_body.downcast_ref::<()>(); diff --git a/actix-http/src/client/pool.rs b/actix-http/src/client/pool.rs index 013a79671..08abc6277 100644 --- a/actix-http/src/client/pool.rs +++ b/actix-http/src/client/pool.rs @@ -119,11 +119,11 @@ where match poll_fn(|cx| Poll::Ready(inner.borrow_mut().acquire(&key, cx))).await { Acquire::Acquired(io, created) => { // use existing connection - return Ok(IoConnection::new( + Ok(IoConnection::new( io, created, Some(Acquired(key, Some(inner))), - )); + )) } Acquire::Available => { // open tcp connection diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index dd8f5ee12..b52e8179c 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -1,5 +1,6 @@ //! Basic http primitives for actix-net framework. -#![warn(rust_2018_idioms, warnings)] + +#![deny(rust_2018_idioms)] #![allow( clippy::type_complexity, clippy::too_many_arguments, diff --git a/actix-http/src/macros.rs b/actix-http/src/macros.rs index e08d62ba6..8973aa39b 100644 --- a/actix-http/src/macros.rs +++ b/actix-http/src/macros.rs @@ -87,7 +87,7 @@ mod tests { let body = resp_body.downcast_ref::().unwrap(); assert_eq!(body, "hello cast"); let body = &mut resp_body.downcast_mut::().unwrap(); - body.push_str("!"); + body.push('!'); let body = resp_body.downcast_ref::().unwrap(); assert_eq!(body, "hello cast!"); let not_body = resp_body.downcast_ref::<()>(); diff --git a/actix-multipart/src/lib.rs b/actix-multipart/src/lib.rs index b502c63d8..46dd0ee9b 100644 --- a/actix-multipart/src/lib.rs +++ b/actix-multipart/src/lib.rs @@ -1,5 +1,6 @@ //! Multipart form support for Actix web. +#![deny(rust_2018_idioms)] #![allow(clippy::borrow_interior_mutable_const)] mod error; diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs index 449c7da29..1507959b8 100644 --- a/actix-multipart/src/server.rs +++ b/actix-multipart/src/server.rs @@ -1,4 +1,5 @@ //! Multipart payload support + use std::cell::{Cell, RefCell, RefMut}; use std::convert::TryFrom; use std::marker::PhantomData; @@ -108,7 +109,7 @@ impl Stream for Multipart { fn poll_next( mut self: Pin<&mut Self>, - cx: &mut Context, + cx: &mut Context<'_>, ) -> Poll> { if let Some(err) = self.error.take() { Poll::Ready(Some(Err(err))) @@ -244,7 +245,7 @@ impl InnerMultipart { fn poll( &mut self, safety: &Safety, - cx: &mut Context, + cx: &mut Context<'_>, ) -> Poll>> { if self.state == InnerState::Eof { Poll::Ready(None) @@ -416,7 +417,10 @@ impl Field { impl Stream for Field { 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> { if self.safety.current() { let mut inner = self.inner.borrow_mut(); if let Some(mut payload) = @@ -434,7 +438,7 @@ impl Stream for Field { } impl fmt::Debug for Field { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "\nField: {}", self.ct)?; writeln!(f, " boundary: {}", self.inner.borrow().boundary)?; writeln!(f, " headers:")?; @@ -689,7 +693,7 @@ impl Safety { self.clean.get() } - fn clone(&self, cx: &mut Context) -> Safety { + fn clone(&self, cx: &mut Context<'_>) -> Safety { let payload = Rc::clone(&self.payload); let s = Safety { task: LocalWaker::new(), @@ -734,7 +738,7 @@ impl PayloadBuffer { } } - fn poll_stream(&mut self, cx: &mut Context) -> Result<(), PayloadError> { + fn poll_stream(&mut self, cx: &mut Context<'_>) -> Result<(), PayloadError> { loop { match Pin::new(&mut self.stream).poll_next(cx) { Poll::Ready(Some(Ok(data))) => self.buf.extend_from_slice(&data), @@ -887,7 +891,7 @@ mod tests { fn poll_next( self: Pin<&mut Self>, - cx: &mut Context, + cx: &mut Context<'_>, ) -> Poll> { let this = self.get_mut(); if !this.ready { diff --git a/actix-web-actors/src/lib.rs b/actix-web-actors/src/lib.rs index d6be5eeaf..0421f05fb 100644 --- a/actix-web-actors/src/lib.rs +++ b/actix-web-actors/src/lib.rs @@ -1,5 +1,6 @@ //! Actix actors integration for Actix web framework +#![deny(rust_2018_idioms)] #![allow(clippy::borrow_interior_mutable_const)] mod context; diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 7167dbb96..4850b78f0 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -1,4 +1,4 @@ -#![warn(rust_2018_idioms, warnings)] +#![deny(rust_2018_idioms)] #![allow( clippy::type_complexity, clippy::borrow_interior_mutable_const, diff --git a/awc/src/sender.rs b/awc/src/sender.rs index 5e0f5beec..0bcdf4307 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -193,11 +193,7 @@ impl RequestSender { } }; - SendClientRequest::new( - fut, - response_decompress, - timeout.or_else(|| config.timeout), - ) + SendClientRequest::new(fut, response_decompress, timeout.or(config.timeout)) } pub(crate) fn send_json( diff --git a/src/lib.rs b/src/lib.rs index 3f65e49dd..97141599c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![warn(rust_2018_idioms, warnings)] +#![deny(rust_2018_idioms)] #![allow(clippy::needless_doctest_main, clippy::type_complexity)] //! Actix web is a powerful, pragmatic, and extremely fast web framework for Rust. From 22089aff8773b8197a2d6adae6b87fe6853f056a Mon Sep 17 00:00:00 2001 From: Robert Gabriel Jakabosky Date: Thu, 10 Sep 2020 22:40:20 +0800 Subject: [PATCH 54/78] Improve json, form and query extractor config docs (#1661) --- src/types/form.rs | 2 +- src/types/json.rs | 37 ++++++++++++++----------------------- src/types/query.rs | 6 +++--- 3 files changed, 18 insertions(+), 27 deletions(-) diff --git a/src/types/form.rs b/src/types/form.rs index de88c2a94..2a7101287 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -195,7 +195,7 @@ impl Responder for Form { /// web::resource("/index.html") /// // change `Form` extractor configuration /// .app_data( -/// web::Form::::configure(|cfg| cfg.limit(4097)) +/// web::FormConfig::default().limit(4097) /// ) /// .route(web::get().to(index)) /// ); diff --git a/src/types/json.rs b/src/types/json.rs index ab7978dff..8da5a3bdb 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -209,10 +209,10 @@ where /// Json extractor configuration /// -/// # Examples +/// # Example /// /// ```rust -/// use actix_web::{error, web, App, FromRequest, HttpRequest, HttpResponse}; +/// use actix_web::{error, web, App, FromRequest, HttpResponse}; /// use serde_derive::Deserialize; /// /// #[derive(Deserialize)] @@ -225,35 +225,26 @@ where /// format!("Welcome {}!", info.username) /// } /// -/// /// Return either a 400 or 415, and include the error message from serde -/// /// in the response body -/// fn json_error_handler(err: error::JsonPayloadError, _req: &HttpRequest) -> error::Error { -/// let detail = err.to_string(); -/// let response = match &err { -/// error::JsonPayloadError::ContentType => { -/// HttpResponse::UnsupportedMediaType().content_type("text/plain").body(detail) -/// } -/// _ => HttpResponse::BadRequest().content_type("text/plain").body(detail), -/// }; -/// error::InternalError::from_response(err, response).into() -/// } -/// /// fn main() { /// let app = App::new().service( /// web::resource("/index.html") /// .app_data( -/// // change json extractor configuration -/// web::Json::::configure(|cfg| { -/// cfg.limit(4096) -/// .content_type(|mime| { // <- accept text/plain content type -/// mime.type_() == mime::TEXT && mime.subtype() == mime::PLAIN -/// }) -/// .error_handler(json_error_handler) // Use our custom error response -/// })) +/// // Json extractor configuration for this resource. +/// web::JsonConfig::default() +/// .limit(4096) // Limit request payload size +/// .content_type(|mime| { // <- accept text/plain content type +/// mime.type_() == mime::TEXT && mime.subtype() == mime::PLAIN +/// }) +/// .error_handler(|err, req| { // <- create custom error response +/// error::InternalError::from_response( +/// err, HttpResponse::Conflict().finish()).into() +/// }) +/// ) /// .route(web::post().to(index)) /// ); /// } /// ``` +/// #[derive(Clone)] pub struct JsonConfig { limit: usize, diff --git a/src/types/query.rs b/src/types/query.rs index cf1a8930d..f9440e1b4 100644 --- a/src/types/query.rs +++ b/src/types/query.rs @@ -188,12 +188,12 @@ where /// let app = App::new().service( /// web::resource("/index.html").app_data( /// // change query extractor configuration -/// web::Query::::configure(|cfg| { -/// cfg.error_handler(|err, req| { // <- create custom error response +/// web::QueryConfig::default() +/// .error_handler(|err, req| { // <- create custom error response /// error::InternalError::from_response( /// err, HttpResponse::Conflict().finish()).into() /// }) -/// })) +/// ) /// .route(web::post().to(index)) /// ); /// } From 121075c1effdf210973fedc4ee4fe072ca3f87d5 Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Fri, 11 Sep 2020 11:24:39 +0300 Subject: [PATCH 55/78] awc: Rename Client::build to Client::builder (#1665) --- awc/CHANGES.md | 2 ++ awc/src/lib.rs | 5 +++-- awc/src/request.rs | 4 ++-- awc/src/ws.rs | 2 +- awc/tests/test_client.rs | 8 ++++---- awc/tests/test_connector.rs | 2 +- awc/tests/test_rustls_client.rs | 2 +- awc/tests/test_ssl_client.rs | 2 +- src/test.rs | 2 +- test-server/src/lib.rs | 2 +- tests/test_httpserver.rs | 4 ++-- 11 files changed, 19 insertions(+), 16 deletions(-) diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 4a830b8fc..38faba459 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,6 +1,8 @@ # Changes ## Unreleased - 2020-xx-xx +### Changed +* `Client::build` was renamed to `Client::builder`. ## 2.0.0-beta.4 - 2020-09-09 diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 4850b78f0..45c52092a 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -166,8 +166,9 @@ impl Client { Client::default() } - /// Build client instance. - pub fn build() -> ClientBuilder { + /// Create `Client` builder. + /// This function is equivalent of `ClientBuilder::new()`. + pub fn builder() -> ClientBuilder { ClientBuilder::new() } diff --git a/awc/src/request.rs b/awc/src/request.rs index c34a8e221..dcada2c6d 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -623,7 +623,7 @@ mod tests { #[actix_rt::test] async fn test_client_header() { - let req = Client::build() + let req = Client::builder() .header(header::CONTENT_TYPE, "111") .finish() .get("/"); @@ -641,7 +641,7 @@ mod tests { #[actix_rt::test] async fn test_client_header_override() { - let req = Client::build() + let req = Client::builder() .header(header::CONTENT_TYPE, "111") .finish() .get("/") diff --git a/awc/src/ws.rs b/awc/src/ws.rs index 96687ac74..57e80bd46 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -434,7 +434,7 @@ mod tests { #[actix_rt::test] async fn test_header_override() { - let req = Client::build() + let req = Client::builder() .header(header::CONTENT_TYPE, "111") .finish() .ws("/") diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index 21be155e8..a9552d0d5 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -120,7 +120,7 @@ async fn test_timeout() { .timeout(Duration::from_secs(15)) .finish(); - let client = awc::Client::build() + let client = awc::Client::builder() .connector(connector) .timeout(Duration::from_millis(50)) .finish(); @@ -141,7 +141,7 @@ async fn test_timeout_override() { }))) }); - let client = awc::Client::build() + let client = awc::Client::builder() .timeout(Duration::from_millis(50000)) .finish(); let request = client @@ -291,7 +291,7 @@ async fn test_connection_wait_queue() { }) .await; - let client = awc::Client::build() + let client = awc::Client::builder() .connector(awc::Connector::new().limit(1).finish()) .finish(); @@ -340,7 +340,7 @@ async fn test_connection_wait_queue_force_close() { }) .await; - let client = awc::Client::build() + let client = awc::Client::builder() .connector(awc::Connector::new().limit(1).finish()) .finish(); diff --git a/awc/tests/test_connector.rs b/awc/tests/test_connector.rs index 633ac2d50..888f7a900 100644 --- a/awc/tests/test_connector.rs +++ b/awc/tests/test_connector.rs @@ -47,7 +47,7 @@ async fn test_connection_window_size() { .set_alpn_protos(b"\x02h2\x08http/1.1") .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); - let client = awc::Client::build() + let client = awc::Client::builder() .connector(awc::Connector::new().ssl(builder.build()).finish()) .initial_window_size(100) .initial_connection_window_size(100) diff --git a/awc/tests/test_rustls_client.rs b/awc/tests/test_rustls_client.rs index 8fb43c439..0df6b154c 100644 --- a/awc/tests/test_rustls_client.rs +++ b/awc/tests/test_rustls_client.rs @@ -82,7 +82,7 @@ async fn _test_connection_reuse_h2() { .dangerous() .set_certificate_verifier(Arc::new(danger::NoCertificateVerification {})); - let client = awc::Client::build() + let client = awc::Client::builder() .connector(awc::Connector::new().rustls(Arc::new(config)).finish()) .finish(); diff --git a/awc/tests/test_ssl_client.rs b/awc/tests/test_ssl_client.rs index ca65fb248..eced5f14b 100644 --- a/awc/tests/test_ssl_client.rs +++ b/awc/tests/test_ssl_client.rs @@ -62,7 +62,7 @@ async fn test_connection_reuse_h2() { .set_alpn_protos(b"\x02h2\x08http/1.1") .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); - let client = awc::Client::build() + let client = awc::Client::builder() .connector(awc::Connector::new().ssl(builder.build()).finish()) .finish(); diff --git a/src/test.rs b/src/test.rs index 49c5cc214..3cc6ae45e 100644 --- a/src/test.rs +++ b/src/test.rs @@ -822,7 +822,7 @@ where } }; - Client::build().connector(connector).finish() + Client::builder().connector(connector).finish() }; TestServer { diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index f6c1183b4..5d5750279 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -90,7 +90,7 @@ pub async fn test_server>(factory: F) -> TestServer } }; - Client::build().connector(connector).finish() + Client::builder().connector(connector).finish() }; actix_connect::start_default_resolver().await.unwrap(); diff --git a/tests/test_httpserver.rs b/tests/test_httpserver.rs index 50c0a7649..118640aca 100644 --- a/tests/test_httpserver.rs +++ b/tests/test_httpserver.rs @@ -43,7 +43,7 @@ async fn test_start() { { use actix_http::client; - let client = awc::Client::build() + let client = awc::Client::builder() .connector( client::Connector::new() .timeout(Duration::from_millis(100)) @@ -115,7 +115,7 @@ async fn test_start_ssl() { .set_alpn_protos(b"\x02h2\x08http/1.1") .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); - let client = awc::Client::build() + let client = awc::Client::builder() .connector( awc::Connector::new() .ssl(builder.build()) From cf5138e740d4377dfc26fa595987fea5e5759bc3 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 11 Sep 2020 11:29:17 +0100 Subject: [PATCH 56/78] fix clippy async_yields_async lints (#1667) --- src/route.rs | 2 +- src/scope.rs | 10 +++++----- src/test.rs | 43 +++++++++++++++++++++---------------------- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/route.rs b/src/route.rs index 3a833bdf3..129a67332 100644 --- a/src/route.rs +++ b/src/route.rs @@ -368,7 +368,7 @@ mod tests { })) .route(web::post().to(|| async { delay_for(Duration::from_millis(100)).await; - HttpResponse::Created() + Ok::<_, ()>(HttpResponse::Created()) })) .route(web::delete().to(|| async { delay_for(Duration::from_millis(100)).await; diff --git a/src/scope.rs b/src/scope.rs index c4b01d266..25b5366d8 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -826,7 +826,7 @@ mod tests { async fn test_scope_variable_segment() { let mut srv = init_service(App::new().service(web::scope("/ab-{project}").service( - web::resource("/path1").to(|r: HttpRequest| async move { + web::resource("/path1").to(|r: HttpRequest| { HttpResponse::Ok() .body(format!("project: {}", &r.match_info()["project"])) }), @@ -926,7 +926,7 @@ mod tests { async fn test_nested_scope_with_variable_segment() { let mut srv = init_service(App::new().service(web::scope("/app").service( web::scope("/{project_id}").service(web::resource("/path1").to( - |r: HttpRequest| async move { + |r: HttpRequest| { HttpResponse::Created() .body(format!("project: {}", &r.match_info()["project_id"])) }, @@ -951,7 +951,7 @@ mod tests { async fn test_nested2_scope_with_variable_segment() { let mut srv = init_service(App::new().service(web::scope("/app").service( web::scope("/{project}").service(web::scope("/{id}").service( - web::resource("/path1").to(|r: HttpRequest| async move { + web::resource("/path1").to(|r: HttpRequest| { HttpResponse::Created().body(format!( "project: {} - {}", &r.match_info()["project"], @@ -1178,7 +1178,7 @@ mod tests { ); s.route( "/", - web::get().to(|req: HttpRequest| async move { + web::get().to(|req: HttpRequest| { HttpResponse::Ok().body( req.url_for("youtube", &["xxxxxx"]).unwrap().to_string(), ) @@ -1199,7 +1199,7 @@ mod tests { async fn test_url_for_nested() { let mut srv = init_service(App::new().service(web::scope("/a").service( web::scope("/b").service(web::resource("/c/{stuff}").name("c").route( - web::get().to(|req: HttpRequest| async move { + web::get().to(|req: HttpRequest| { HttpResponse::Ok() .body(format!("{}", req.url_for("c", &["12345"]).unwrap())) }), diff --git a/src/test.rs b/src/test.rs index 3cc6ae45e..2620e190e 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1072,14 +1072,9 @@ mod tests { let mut app = init_service( App::new().service( web::resource("/index.html") - .route(web::put().to(|| async { HttpResponse::Ok().body("put!") })) - .route( - web::patch().to(|| async { HttpResponse::Ok().body("patch!") }), - ) - .route( - web::delete() - .to(|| async { HttpResponse::Ok().body("delete!") }), - ), + .route(web::put().to(|| HttpResponse::Ok().body("put!"))) + .route(web::patch().to(|| HttpResponse::Ok().body("patch!"))) + .route(web::delete().to(|| HttpResponse::Ok().body("delete!"))), ), ) .await; @@ -1107,11 +1102,13 @@ mod tests { #[actix_rt::test] async fn test_response() { - let mut app = - init_service(App::new().service(web::resource("/index.html").route( - web::post().to(|| async { HttpResponse::Ok().body("welcome!") }), - ))) - .await; + let mut app = init_service( + App::new().service( + web::resource("/index.html") + .route(web::post().to(|| HttpResponse::Ok().body("welcome!"))), + ), + ) + .await; let req = TestRequest::post() .uri("/index.html") @@ -1124,11 +1121,13 @@ mod tests { #[actix_rt::test] async fn test_send_request() { - let mut app = - init_service(App::new().service(web::resource("/index.html").route( - web::get().to(|| async { HttpResponse::Ok().body("welcome!") }), - ))) - .await; + let mut app = init_service( + App::new().service( + web::resource("/index.html") + .route(web::get().to(|| HttpResponse::Ok().body("welcome!"))), + ), + ) + .await; let resp = TestRequest::get() .uri("/index.html") @@ -1148,7 +1147,7 @@ mod tests { #[actix_rt::test] async fn test_response_json() { let mut app = init_service(App::new().service(web::resource("/people").route( - web::post().to(|person: web::Json| async { + web::post().to(|person: web::Json| { HttpResponse::Ok().json(person.into_inner()) }), ))) @@ -1169,7 +1168,7 @@ mod tests { #[actix_rt::test] async fn test_body_json() { let mut app = init_service(App::new().service(web::resource("/people").route( - web::post().to(|person: web::Json| async { + web::post().to(|person: web::Json| { HttpResponse::Ok().json(person.into_inner()) }), ))) @@ -1191,7 +1190,7 @@ mod tests { #[actix_rt::test] async fn test_request_response_form() { let mut app = init_service(App::new().service(web::resource("/people").route( - web::post().to(|person: web::Form| async { + web::post().to(|person: web::Form| { HttpResponse::Ok().json(person.into_inner()) }), ))) @@ -1217,7 +1216,7 @@ mod tests { #[actix_rt::test] async fn test_request_response_json() { let mut app = init_service(App::new().service(web::resource("/people").route( - web::post().to(|person: web::Json| async { + web::post().to(|person: web::Json| { HttpResponse::Ok().json(person.into_inner()) }), ))) From bf53fe5a22449bb99da2192f2b3cc363f7302969 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 11 Sep 2020 12:09:52 +0100 Subject: [PATCH 57/78] bump actix dependency to v0.10 (#1666) --- Cargo.toml | 5 +- actix-http/Cargo.toml | 2 +- .../src/header/common/content_disposition.rs | 6 +- actix-web-actors/Cargo.toml | 2 +- src/lib.rs | 2 +- src/test.rs | 93 ++++++++++--------- 6 files changed, 56 insertions(+), 54 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 11559bcae..8d55ffe11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,7 +92,7 @@ mime = "0.3" socket2 = "0.3" pin-project = "0.4.17" regex = "1.3" -serde = { version = "1.0", features=["derive"] } +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_urlencoded = "0.6.1" time = { version = "0.2.7", default-features = false, features = ["std"] } @@ -102,7 +102,8 @@ rust-tls = { package = "rustls", version = "0.18.0", optional = true } tinyvec = { version = "0.3", features = ["alloc"] } [dev-dependencies] -actix = "0.10.0-alpha.1" +actix = "0.10.0" +actix-http = { version = "2.0.0-beta.4", features = ["actors"] } rand = "0.7" env_logger = "0.7" serde_derive = "1.0" diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 6d8d143ed..fa002b309 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -47,7 +47,7 @@ actix-utils = "2.0.0" actix-rt = "1.0.0" actix-threadpool = "0.3.1" actix-tls = { version = "2.0.0", optional = true } -actix = { version = "0.10.0-alpha.1", optional = true } +actix = { version = "0.10.0", optional = true } base64 = "0.12" bitflags = "1.2" diff --git a/actix-http/src/header/common/content_disposition.rs b/actix-http/src/header/common/content_disposition.rs index 051dcfe80..37da830ca 100644 --- a/actix-http/src/header/common/content_disposition.rs +++ b/actix-http/src/header/common/content_disposition.rs @@ -283,11 +283,11 @@ impl DispositionParam { /// Some("\u{1f600}.svg".as_bytes())); /// ``` /// -/// # WARN +/// # Security Note +/// /// 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) -/// . +/// information that may be present. See [RFC2183](https://tools.ietf.org/html/rfc2183#section-2.3). #[derive(Clone, Debug, PartialEq)] pub struct ContentDisposition { /// The disposition type diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index cb7fd3a80..917f0cd94 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 = "0.10.0-alpha.2" +actix = "0.10.0" actix-web = { version = "3.0.0-beta.4", default-features = false } actix-http = "2.0.0-beta.4" actix-codec = "0.3.0" diff --git a/src/lib.rs b/src/lib.rs index 97141599c..0eced5b42 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,7 +59,7 @@ //! * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) //! * Includes an async [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html) //! * Supports [Actix actor framework](https://github.com/actix/actix) -//! * Runs on stable Rust 1.41+ +//! * Runs on stable Rust 1.42+ //! //! ## Crate Features //! diff --git a/src/test.rs b/src/test.rs index 2620e190e..ee51b71ee 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1281,53 +1281,54 @@ mod tests { assert!(res.status().is_success()); } - /* + #[actix_rt::test] + async fn test_actor() { + use crate::Error; + use actix::prelude::*; - Comment out until actix decoupled of actix-http: - https://github.com/actix/actix/issues/321 + struct MyActor; - use futures::FutureExt; - - #[actix_rt::test] - async fn test_actor() { - use actix::Actor; - - struct MyActor; - - struct Num(usize); - impl actix::Message for Num { - type Result = usize; - } - impl actix::Actor for MyActor { - type Context = actix::Context; - } - impl actix::Handler for MyActor { - type Result = usize; - fn handle(&mut self, msg: Num, _: &mut Self::Context) -> Self::Result { - msg.0 - } - } - - - let mut app = init_service(App::new().service(web::resource("/index.html").to( - move || { - addr.send(Num(1)).map(|res| match res { - Ok(res) => { - if res == 1 { - Ok(HttpResponse::Ok()) - } else { - Ok(HttpResponse::BadRequest()) - } - } - Err(err) => Err(err), - }) - }, - ))) - .await; - - let req = TestRequest::post().uri("/index.html").to_request(); - let res = app.call(req).await.unwrap(); - assert!(res.status().is_success()); + impl Actor for MyActor { + type Context = Context; } - */ + + struct Num(usize); + + impl Message for Num { + type Result = usize; + } + + impl Handler for MyActor { + type Result = usize; + + fn handle(&mut self, msg: Num, _: &mut Self::Context) -> Self::Result { + msg.0 + } + } + + let addr = MyActor.start(); + + async fn actor_handler( + addr: Data>, + ) -> Result { + // `?` operator tests "actors" feature flag on actix-http + let res = addr.send(Num(1)).await?; + + if res == 1 { + Ok(HttpResponse::Ok()) + } else { + Ok(HttpResponse::BadRequest()) + } + } + + let srv = App::new() + .data(addr.clone()) + .service(web::resource("/").to(actor_handler)); + + let mut app = init_service(srv).await; + + let req = TestRequest::post().uri("/").to_request(); + let res = app.call(req).await.unwrap(); + assert!(res.status().is_success()); + } } From 64a2c13cdfed3616bf6f3ac2c39a5e1b40bfada4 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 11 Sep 2020 13:50:10 +0100 Subject: [PATCH 58/78] the big three point oh (#1668) --- CHANGES.md | 4 ++++ Cargo.toml | 8 ++++---- LICENSE-APACHE | 2 +- LICENSE-MIT | 2 +- MIGRATION.md | 4 ++++ README.md | 5 ----- actix-cors/README.md | 11 ----------- actix-files/Cargo.toml | 10 +++++----- actix-framed/README.md | 3 --- actix-http/CHANGES.md | 4 ++++ actix-http/Cargo.toml | 6 +++--- actix-identity/README.md | 11 ----------- actix-multipart/CHANGES.md | 4 ++++ actix-multipart/Cargo.toml | 8 ++++---- actix-session/README.md | 11 ----------- actix-web-actors/CHANGES.md | 4 ++++ actix-web-actors/Cargo.toml | 8 ++++---- actix-web-codegen/CHANGES.md | 4 ++++ actix-web-codegen/Cargo.toml | 4 ++-- awc/CHANGES.md | 3 +++ awc/Cargo.toml | 12 ++++++------ test-server/CHANGES.md | 5 ++++- test-server/Cargo.toml | 12 ++++++------ 23 files changed, 67 insertions(+), 78 deletions(-) delete mode 100644 actix-cors/README.md delete mode 100644 actix-framed/README.md delete mode 100644 actix-identity/README.md delete mode 100644 actix-session/README.md diff --git a/CHANGES.md b/CHANGES.md index 1779af9ee..995ef884a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2020-xx-xx +## 3.0.0 - 2020-09-11 +* No significant changes from `3.0.0-beta.4`. + + ## 3.0.0-beta.4 - 2020-09-09 ### Added * `middleware::NormalizePath` now has configurable behaviour for either always having a trailing diff --git a/Cargo.toml b/Cargo.toml index 8d55ffe11..ce33097be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "3.0.0-beta.4" +version = "3.0.0" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" @@ -76,9 +76,9 @@ actix-macros = "0.1.0" actix-threadpool = "0.3.1" actix-tls = "2.0.0" -actix-web-codegen = "0.3.0-beta.1" -actix-http = "2.0.0-beta.4" -awc = { version = "2.0.0-beta.4", default-features = false } +actix-web-codegen = "0.3.0" +actix-http = "2.0.0" +awc = { version = "2.0.0", default-features = false } bytes = "0.5.3" derive_more = "0.99.2" diff --git a/LICENSE-APACHE b/LICENSE-APACHE index 6cdf2d16c..8f5ba39b8 100644 --- a/LICENSE-APACHE +++ b/LICENSE-APACHE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2017-NOW Nikolay Kim + Copyright 2017-NOW Actix Team Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/LICENSE-MIT b/LICENSE-MIT index 0f80296ae..95938ef15 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,4 +1,4 @@ -Copyright (c) 2017 Nikolay Kim +Copyright (c) 2017 Actix Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated diff --git a/MIGRATION.md b/MIGRATION.md index 15045ed69..fd940651f 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,5 +1,8 @@ ## Unreleased + +## 3.0.0 + * Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now result in `SameSite=None` being sent with the response Set-Cookie header. To create a cookie without a SameSite attribute, remove any calls setting same_site. @@ -40,6 +43,7 @@ * `HttpServer::maxconnrate` is renamed to the more expressive `HttpServer::max_connection_rate`. + ## 2.0.0 * `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to diff --git a/README.md b/README.md index 48f9d1442..3e3ce8bf1 100644 --- a/README.md +++ b/README.md @@ -43,11 +43,6 @@ ## Example -

- WARNING: This example is for the master branch which is currently in beta stages for v3. For - Actix web v2 see the getting started guide. -

- Dependencies: ```toml diff --git a/actix-cors/README.md b/actix-cors/README.md deleted file mode 100644 index c860ec5ae..000000000 --- a/actix-cors/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Cors Middleware for actix web framework [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-cors)](https://crates.io/crates/actix-cors) [![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) - -**This crate moved to https://github.com/actix/actix-extras.** - -## Documentation & community resources - -* [User Guide](https://actix.rs/docs/) -* [API Documentation](https://docs.rs/actix-cors/) -* [Chat on gitter](https://gitter.im/actix/actix) -* Cargo package: [actix-cors](https://crates.io/crates/actix-cors) -* Minimum supported Rust version: 1.34 or later diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 4c6441324..8841f7fb1 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.3.0-beta.1" +version = "0.3.0" authors = ["Nikolay Kim "] description = "Static files support for actix web." readme = "README.md" @@ -17,9 +17,9 @@ name = "actix_files" path = "src/lib.rs" [dependencies] -actix-web = { version = "3.0.0-beta.4", default-features = false } -actix-http = "2.0.0-beta.4" -actix-service = "1.0.1" +actix-web = { version = "3.0.0", default-features = false } +actix-http = "2.0.0" +actix-service = "1.0.6" bitflags = "1" bytes = "0.5.3" futures-core = { version = "0.3.5", default-features = false } @@ -33,4 +33,4 @@ v_htmlescape = "0.10" [dev-dependencies] actix-rt = "1.0.0" -actix-web = { version = "3.0.0-beta.4", features = ["openssl"] } +actix-web = { version = "3.0.0", features = ["openssl"] } diff --git a/actix-framed/README.md b/actix-framed/README.md deleted file mode 100644 index a4eaadf21..000000000 --- a/actix-framed/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Framed app for actix web - -**This crate has been deprecated and removed.** diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index dfa55e7f1..6a98c4ca7 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2020-xx-xx +## 2.0.0 - 2020-09-11 +* No significant changes from `2.0.0-beta.4`. + + ## 2.0.0-beta.4 - 2020-09-09 ### Changed * Update actix-codec and actix-utils dependencies. diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index fa002b309..0bbde881d 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "2.0.0-beta.4" +version = "2.0.0" authors = ["Nikolay Kim "] description = "Actix HTTP primitives" readme = "README.md" @@ -40,7 +40,7 @@ secure-cookies = ["cookie/secure"] actors = ["actix"] [dependencies] -actix-service = "1.0.5" +actix-service = "1.0.6" actix-codec = "0.3.0" actix-connect = "2.0.0" actix-utils = "2.0.0" @@ -88,7 +88,7 @@ flate2 = { version = "1.0.13", optional = true } [dev-dependencies] actix-server = "1.0.1" actix-connect = { version = "2.0.0", features = ["openssl"] } -actix-http-test = { version = "2.0.0-alpha.1", features = ["openssl"] } +actix-http-test = { version = "2.0.0", features = ["openssl"] } actix-tls = { version = "2.0.0", features = ["openssl"] } criterion = "0.3" env_logger = "0.7" diff --git a/actix-identity/README.md b/actix-identity/README.md deleted file mode 100644 index 62a40137f..000000000 --- a/actix-identity/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Identity service for actix web framework [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-identity)](https://crates.io/crates/actix-identity) [![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) - -**This crate moved to https://github.com/actix/actix-extras.** - -## Documentation & community resources - -* [User Guide](https://actix.rs/docs/) -* [API Documentation](https://docs.rs/actix-identity/) -* [Chat on gitter](https://gitter.im/actix/actix) -* Cargo package: [actix-session](https://crates.io/crates/actix-identity) -* Minimum supported Rust version: 1.34 or later diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 7149f6713..b25053025 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2020-xx-xx +## 3.0.0 - 2020-09-11 +* No significant changes from `3.0.0-beta.2`. + + ## 3.0.0-beta.2 - 2020-09-10 * Update `actix-*` dependencies to latest versions. diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 0fd5d0922..e2e9dbf14 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-multipart" -version = "0.3.0-beta.2" +version = "0.3.0" authors = ["Nikolay Kim "] description = "Multipart support for actix web framework." readme = "README.md" @@ -16,8 +16,8 @@ name = "actix_multipart" path = "src/lib.rs" [dependencies] -actix-web = { version = "3.0.0-beta.4", default-features = false } -actix-service = "1.0.1" +actix-web = { version = "3.0.0", default-features = false } +actix-service = "1.0.6" actix-utils = "2.0.0" bytes = "0.5.3" derive_more = "0.99.2" @@ -29,4 +29,4 @@ twoway = "0.2" [dev-dependencies] actix-rt = "1.0.0" -actix-http = "2.0.0-beta.4" +actix-http = "2.0.0" diff --git a/actix-session/README.md b/actix-session/README.md deleted file mode 100644 index 00e580120..000000000 --- a/actix-session/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Session for actix web framework [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-session)](https://crates.io/crates/actix-session) [![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) - -**This crate moved to https://github.com/actix/actix-extras.** - -## Documentation & community resources - -* [User Guide](https://actix.rs/docs/) -* [API Documentation](https://docs.rs/actix-session/) -* [Chat on gitter](https://gitter.im/actix/actix) -* Cargo package: [actix-session](https://crates.io/crates/actix-session) -* Minimum supported Rust version: 1.34 or later diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index 4dcd403a3..4b9381a33 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2020-xx-xx +## 3.0.0 - 2020-09-11 +* No significant changes from `3.0.0-beta.2`. + + ## 3.0.0-beta.2 - 2020-09-10 * Update `actix-*` dependencies to latest versions. diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 917f0cd94..2f3c63022 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-actors" -version = "3.0.0-beta.2" +version = "3.0.0" authors = ["Nikolay Kim "] description = "Actix actors support for actix web framework." readme = "README.md" @@ -17,8 +17,8 @@ path = "src/lib.rs" [dependencies] actix = "0.10.0" -actix-web = { version = "3.0.0-beta.4", default-features = false } -actix-http = "2.0.0-beta.4" +actix-web = { version = "3.0.0", default-features = false } +actix-http = "2.0.0" actix-codec = "0.3.0" bytes = "0.5.2" futures-channel = { version = "0.3.5", default-features = false } @@ -26,6 +26,6 @@ futures-core = { version = "0.3.5", default-features = false } pin-project = "0.4.17" [dev-dependencies] -actix-rt = "1.0.0" +actix-rt = "1.1.1" env_logger = "0.7" futures-util = { version = "0.3.5", default-features = false } diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index 242c5f8de..b735be9ce 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2020-xx-xx +## 0.3.0 - 2020-09-11 +* No significant changes from `0.3.0-beta.1`. + + ## 0.3.0-beta.1 - 2020-07-14 * Add main entry-point macro that uses re-exported runtime. [#1559] diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index ddd886c40..05b52c9db 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-codegen" -version = "0.3.0-beta.1" +version = "0.3.0" description = "Actix web proc macros" readme = "README.md" homepage = "https://actix.rs" @@ -20,5 +20,5 @@ proc-macro2 = "1" [dev-dependencies] actix-rt = "1.0.0" -actix-web = "3.0.0-beta.4" +actix-web = "3.0.0" futures-util = { version = "0.3.5", default-features = false } diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 38faba459..07a469746 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2020-xx-xx + + +## 2.0.0 - 2020-09-11 ### Changed * `Client::build` was renamed to `Client::builder`. diff --git a/awc/Cargo.toml b/awc/Cargo.toml index b55f4dabb..c67b6ba6f 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "2.0.0-beta.4" +version = "2.0.0" authors = ["Nikolay Kim "] description = "Async HTTP client library that uses the Actix runtime." readme = "README.md" @@ -38,8 +38,8 @@ compress = ["actix-http/compress"] [dependencies] actix-codec = "0.3.0" -actix-service = "1.0.1" -actix-http = "2.0.0-beta.4" +actix-service = "1.0.6" +actix-http = "2.0.0" actix-rt = "1.0.0" base64 = "0.12" @@ -58,9 +58,9 @@ rust-tls = { version = "0.18.0", package = "rustls", optional = true, features = [dev-dependencies] actix-connect = { version = "2.0.0", features = ["openssl"] } -actix-web = { version = "3.0.0-beta.4", features = ["openssl"] } -actix-http = { version = "2.0.0-beta.4", features = ["openssl"] } -actix-http-test = { version = "2.0.0-alpha.1", features = ["openssl"] } +actix-web = { version = "3.0.0", features = ["openssl"] } +actix-http = { version = "2.0.0", features = ["openssl"] } +actix-http-test = { version = "2.0.0", features = ["openssl"] } actix-utils = "2.0.0" actix-server = "1.0.0" actix-tls = { version = "2.0.0", features = ["openssl", "rustls"] } diff --git a/test-server/CHANGES.md b/test-server/CHANGES.md index 71b906b9f..e71e9d0b8 100644 --- a/test-server/CHANGES.md +++ b/test-server/CHANGES.md @@ -1,10 +1,13 @@ # Changes ## Unreleased - 2020-xx-xx + + +## 2.0.0 - 2020-09-11 * Update actix-codec and actix-utils dependencies. -## [2.0.0-alpha.1] - 2020-05-23 +## 2.0.0-alpha.1 - 2020-05-23 * Update the `time` dependency to 0.2.7 * Update `actix-connect` dependency to 2.0.0-alpha.2 * Make `test_server` `async` fn. diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index 8aecda401..d06bd5dec 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http-test" -version = "2.0.0-alpha.1" +version = "2.0.0" authors = ["Nikolay Kim "] description = "Actix HTTP test server" readme = "README.md" @@ -29,14 +29,14 @@ default = [] openssl = ["open-ssl", "awc/openssl"] [dependencies] -actix-service = "1.0.1" +actix-service = "1.0.6" actix-codec = "0.3.0" actix-connect = "2.0.0" actix-utils = "2.0.0" -actix-rt = "1.0.0" +actix-rt = "1.1.1" actix-server = "1.0.0" actix-testing = "1.0.0" -awc = "2.0.0-beta.4" +awc = "2.0.0" base64 = "0.12" bytes = "0.5.3" @@ -52,5 +52,5 @@ time = { version = "0.2.7", default-features = false, features = ["std"] } open-ssl = { version = "0.10", package = "openssl", optional = true } [dev-dependencies] -actix-web = "3.0.0-beta.4" -actix-http = "2.0.0-beta.4" +actix-web = "3.0.0" +actix-http = "2.0.0" From a4546f02d2924b23c689cf08f47341b885aece80 Mon Sep 17 00:00:00 2001 From: Damian Lesiuk Date: Sun, 13 Sep 2020 01:55:39 +0200 Subject: [PATCH 59/78] make TrailingSlash enum accessible (#1673) Co-authored-by: Damian Lesiuk --- .gitignore | 3 +++ CHANGES.md | 3 ++- src/middleware/mod.rs | 2 +- tests/test_server.rs | 17 ++++++++++++++++- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 11a3b5f37..638a4397a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ guide/build/ # These are backup files generated by rustfmt **/*.rs.bk + +# Configuration directory generated by CLion +.idea diff --git a/CHANGES.md b/CHANGES.md index 995ef884a..bc5f7136f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,8 @@ # Changes ## Unreleased - 2020-xx-xx - +### Changed +* `middleware::normalize::TrailingSlash` enum is now accessible. [#1673] ## 3.0.0 - 2020-09-11 * No significant changes from `3.0.0-beta.4`. diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index f0d42cc2a..12c12a98c 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -9,7 +9,7 @@ mod condition; mod defaultheaders; pub mod errhandlers; mod logger; -mod normalize; +pub mod normalize; pub use self::condition::Condition; pub use self::defaultheaders::DefaultHeaders; diff --git a/tests/test_server.rs b/tests/test_server.rs index fa8a93f06..f8a9ab86d 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -16,7 +16,8 @@ use futures_util::ready; use rand::{distributions::Alphanumeric, Rng}; use actix_web::dev::BodyEncoding; -use actix_web::middleware::Compress; +use actix_web::middleware::normalize::TrailingSlash; +use actix_web::middleware::{Compress, NormalizePath}; use actix_web::{dev, test, web, App, Error, HttpResponse}; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ @@ -866,6 +867,20 @@ async fn test_slow_request() { assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); } +#[actix_rt::test] +async fn test_normalize() { + let srv = test::start_with(test::config().h1(), || { + App::new() + .wrap(NormalizePath::new(TrailingSlash::Trim)) + .service( + web::resource("/one").route(web::to(|| HttpResponse::Ok().finish())), + ) + }); + + let response = srv.get("/one/").send().await.unwrap(); + assert!(response.status().is_success()); +} + // #[cfg(feature = "openssl")] // #[actix_rt::test] // async fn test_ssl_handshake_timeout() { From f8615087893863a35f3e835068dd833ce5c5af5b Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sun, 13 Sep 2020 03:24:44 +0100 Subject: [PATCH 60/78] prepare web release 3.0.1 (#1676) --- CHANGES.md | 6 ++++++ Cargo.toml | 4 ++-- src/lib.rs | 8 +++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index bc5f7136f..aadf627bc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,9 +1,15 @@ # Changes ## Unreleased - 2020-xx-xx + + +## 3.0.1 - 2020-09-13 ### Changed * `middleware::normalize::TrailingSlash` enum is now accessible. [#1673] +[#1673]: https://github.com/actix/actix-web/pull/1673 + + ## 3.0.0 - 2020-09-11 * No significant changes from `3.0.0-beta.4`. diff --git a/Cargo.toml b/Cargo.toml index ce33097be..b2cffbc48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "actix-web" -version = "3.0.0" +version = "3.0.1" authors = ["Nikolay Kim "] -description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." +description = "Actix web is a powerful, pragmatic, and extremely fast web framework for Rust." readme = "README.md" keywords = ["actix", "http", "web", "framework", "async"] homepage = "https://actix.rs" diff --git a/src/lib.rs b/src/lib.rs index 0eced5b42..327cba954 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,3 @@ -#![deny(rust_2018_idioms)] -#![allow(clippy::needless_doctest_main, clippy::type_complexity)] - //! Actix web is a powerful, pragmatic, and extremely fast web framework for Rust. //! //! ## Example @@ -68,6 +65,11 @@ //! * `rustls` - HTTPS support via `rustls` crate, supports `HTTP/2` //! * `secure-cookies` - secure cookies support +#![deny(rust_2018_idioms)] +#![allow(clippy::needless_doctest_main, clippy::type_complexity)] +#![doc(html_logo_url = "https://actix.rs/img/logo.png")] +#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] + mod app; mod app_service; mod config; From 3fde3be3d863a42fab6799bdfd4552bcb43d7e8e Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sun, 13 Sep 2020 16:31:08 +0100 Subject: [PATCH 61/78] add trybuild tests to routing codegen (#1677) --- actix-web-codegen/CHANGES.md | 3 ++ actix-web-codegen/Cargo.toml | 1 + actix-web-codegen/README.md | 24 ++++++++++--- actix-web-codegen/src/lib.rs | 34 +++++++++---------- actix-web-codegen/tests/trybuild.rs | 7 ++++ .../tests/trybuild/simple-fail.rs | 25 ++++++++++++++ .../tests/trybuild/simple-fail.stderr | 23 +++++++++++++ actix-web-codegen/tests/trybuild/simple.rs | 15 ++++++++ 8 files changed, 110 insertions(+), 22 deletions(-) create mode 100644 actix-web-codegen/tests/trybuild.rs create mode 100644 actix-web-codegen/tests/trybuild/simple-fail.rs create mode 100644 actix-web-codegen/tests/trybuild/simple-fail.stderr create mode 100644 actix-web-codegen/tests/trybuild/simple.rs diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index b735be9ce..5c0ce828a 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2020-xx-xx +* Added compile success and failure testing. [#1677] + +[#1677]: https://github.com/actix/actix-web/pull/1677 ## 0.3.0 - 2020-09-11 diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 05b52c9db..1bf78f997 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -22,3 +22,4 @@ proc-macro2 = "1" actix-rt = "1.0.0" actix-web = "3.0.0" futures-util = { version = "0.3.5", default-features = false } +trybuild = "1" diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index 45eb82c2c..6eca847b8 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -1,8 +1,22 @@ -# Helper and convenience macros for Actix-web. [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web-codegen)](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) +# actix-web-codegen + +> Helper and convenience macros for Actix Web + +[![crates.io](https://meritbadge.herokuapp.com/actix-web-codegen)](https://crates.io/crates/actix-web-codegen) +[![Documentation](https://docs.rs/actix-web-codegen/badge.svg)](https://docs.rs/actix-web) +[![Version](https://img.shields.io/badge/rustc-1.42+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html) +[![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) +[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) +[![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) ## Documentation & Resources -* [API Documentation](https://docs.rs/actix-web-codegen/) -* [Chat on gitter](https://gitter.im/actix/actix) -* Cargo package: [actix-web-codegen](https://crates.io/crates/actix-web-codegen) -* Minimum supported Rust version: 1.40 or later +- [API Documentation](https://docs.rs/actix-web-codegen) +- [Chat on Gitter](https://gitter.im/actix/actix-web) +- Cargo package: [actix-web-codegen](https://crates.io/crates/actix-web-codegen) +- Minimum supported Rust version: 1.42 or later. + +## Compile Testing +Uses the [`trybuild`] crate. All compile fail tests should include a stderr file generated by `trybuild`. See the [workflow section](https://github.com/dtolnay/trybuild#workflow) of the trybuild docs for info on how to do this. + +[`trybuild`]: https://github.com/dtolnay/trybuild diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index b6df3f0dd..445fe924d 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -49,13 +49,13 @@ use proc_macro::TokenStream; /// Creates route handler with `GET` method guard. /// -/// Syntax: `#[get("path"[, attributes])]` +/// Syntax: `#[get("path" [, attributes])]` /// /// ## Attributes: /// /// - `"path"` - Raw literal string with path for which to register handler. Mandatory. -/// - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard` -/// - `wrap="Middleware"` - Registers a resource middleware. +/// - `guard = "function_name"` - Registers function as guard using `actix_web::guard::fn_guard` +/// - `wrap = "Middleware"` - Registers a resource middleware. #[proc_macro_attribute] pub fn get(args: TokenStream, input: TokenStream) -> TokenStream { route::generate(args, input, route::GuardType::Get) @@ -63,7 +63,7 @@ pub fn get(args: TokenStream, input: TokenStream) -> TokenStream { /// Creates route handler with `POST` method guard. /// -/// Syntax: `#[post("path"[, attributes])]` +/// Syntax: `#[post("path" [, attributes])]` /// /// Attributes are the same as in [get](attr.get.html) #[proc_macro_attribute] @@ -73,7 +73,7 @@ pub fn post(args: TokenStream, input: TokenStream) -> TokenStream { /// Creates route handler with `PUT` method guard. /// -/// Syntax: `#[put("path"[, attributes])]` +/// Syntax: `#[put("path" [, attributes])]` /// /// Attributes are the same as in [get](attr.get.html) #[proc_macro_attribute] @@ -83,9 +83,9 @@ pub fn put(args: TokenStream, input: TokenStream) -> TokenStream { /// Creates route handler with `DELETE` method guard. /// -/// Syntax: `#[delete("path"[, attributes])]` +/// Syntax: `#[delete("path" [, attributes])]` /// -/// Attributes are the same as in [get](attr.get.html) +/// Attributes are the same as in [get](attr.get.html). #[proc_macro_attribute] pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream { route::generate(args, input, route::GuardType::Delete) @@ -93,9 +93,9 @@ pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream { /// Creates route handler with `HEAD` method guard. /// -/// Syntax: `#[head("path"[, attributes])]` +/// Syntax: `#[head("path" [, attributes])]` /// -/// Attributes are the same as in [head](attr.head.html) +/// Attributes are the same as in [get](attr.get.html). #[proc_macro_attribute] pub fn head(args: TokenStream, input: TokenStream) -> TokenStream { route::generate(args, input, route::GuardType::Head) @@ -103,9 +103,9 @@ pub fn head(args: TokenStream, input: TokenStream) -> TokenStream { /// Creates route handler with `CONNECT` method guard. /// -/// Syntax: `#[connect("path"[, attributes])]` +/// Syntax: `#[connect("path" [, attributes])]` /// -/// Attributes are the same as in [connect](attr.connect.html) +/// Attributes are the same as in [get](attr.get.html). #[proc_macro_attribute] pub fn connect(args: TokenStream, input: TokenStream) -> TokenStream { route::generate(args, input, route::GuardType::Connect) @@ -113,9 +113,9 @@ pub fn connect(args: TokenStream, input: TokenStream) -> TokenStream { /// Creates route handler with `OPTIONS` method guard. /// -/// Syntax: `#[options("path"[, attributes])]` +/// Syntax: `#[options("path" [, attributes])]` /// -/// Attributes are the same as in [options](attr.options.html) +/// Attributes are the same as in [get](attr.get.html). #[proc_macro_attribute] pub fn options(args: TokenStream, input: TokenStream) -> TokenStream { route::generate(args, input, route::GuardType::Options) @@ -123,9 +123,9 @@ pub fn options(args: TokenStream, input: TokenStream) -> TokenStream { /// Creates route handler with `TRACE` method guard. /// -/// Syntax: `#[trace("path"[, attributes])]` +/// Syntax: `#[trace("path" [, attributes])]` /// -/// Attributes are the same as in [trace](attr.trace.html) +/// Attributes are the same as in [get](attr.get.html). #[proc_macro_attribute] pub fn trace(args: TokenStream, input: TokenStream) -> TokenStream { route::generate(args, input, route::GuardType::Trace) @@ -133,9 +133,9 @@ pub fn trace(args: TokenStream, input: TokenStream) -> TokenStream { /// Creates route handler with `PATCH` method guard. /// -/// Syntax: `#[patch("path"[, attributes])]` +/// Syntax: `#[patch("path" [, attributes])]` /// -/// Attributes are the same as in [patch](attr.patch.html) +/// Attributes are the same as in [get](attr.get.html). #[proc_macro_attribute] pub fn patch(args: TokenStream, input: TokenStream) -> TokenStream { route::generate(args, input, route::GuardType::Patch) diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs new file mode 100644 index 000000000..b675947d3 --- /dev/null +++ b/actix-web-codegen/tests/trybuild.rs @@ -0,0 +1,7 @@ +#[test] +fn compile_macros() { + let t = trybuild::TestCases::new(); + + t.pass("tests/trybuild/simple.rs"); + t.compile_fail("tests/trybuild/simple-fail.rs"); +} diff --git a/actix-web-codegen/tests/trybuild/simple-fail.rs b/actix-web-codegen/tests/trybuild/simple-fail.rs new file mode 100644 index 000000000..140497687 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/simple-fail.rs @@ -0,0 +1,25 @@ +use actix_web::*; + +#[get("/one", other)] +async fn one() -> impl Responder { + HttpResponse::Ok() +} + +#[post(/two)] +async fn two() -> impl Responder { + HttpResponse::Ok() +} + +static PATCH_PATH: &str = "/three"; + +#[patch(PATCH_PATH)] +async fn three() -> impl Responder { + HttpResponse::Ok() +} + +#[delete("/four", "/five")] +async fn four() -> impl Responder { + HttpResponse::Ok() +} + +fn main() {} diff --git a/actix-web-codegen/tests/trybuild/simple-fail.stderr b/actix-web-codegen/tests/trybuild/simple-fail.stderr new file mode 100644 index 000000000..12c32c00d --- /dev/null +++ b/actix-web-codegen/tests/trybuild/simple-fail.stderr @@ -0,0 +1,23 @@ +error: Unknown attribute. + --> $DIR/simple-fail.rs:3:15 + | +3 | #[get("/one", other)] + | ^^^^^ + +error: expected identifier or literal + --> $DIR/simple-fail.rs:8:8 + | +8 | #[post(/two)] + | ^ + +error: Unknown attribute. + --> $DIR/simple-fail.rs:15:9 + | +15 | #[patch(PATCH_PATH)] + | ^^^^^^^^^^ + +error: Multiple paths specified! Should be only one! + --> $DIR/simple-fail.rs:20:19 + | +20 | #[delete("/four", "/five")] + | ^^^^^^^ diff --git a/actix-web-codegen/tests/trybuild/simple.rs b/actix-web-codegen/tests/trybuild/simple.rs new file mode 100644 index 000000000..6b1e67442 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/simple.rs @@ -0,0 +1,15 @@ +use actix_web::*; + +#[get("/config")] +async fn config() -> impl Responder { + HttpResponse::Ok() +} + +#[actix_web::main] +async fn main() { + let srv = test::start(|| App::new().service(config)); + + let request = srv.get("/config"); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); +} From 4b4c9d1b9341f1316157ed29e4f27223228f6009 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 14 Sep 2020 22:26:03 +0100 Subject: [PATCH 62/78] update migration guide closes #1680 --- MIGRATION.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/MIGRATION.md b/MIGRATION.md index fd940651f..98b22ae4e 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -3,12 +3,23 @@ ## 3.0.0 +* The return type for `ServiceRequest::app_data::()` was changed from returning a `Data` to + simply a `T`. To access a `Data` use `ServiceRequest::app_data::>()`. + +* Cookie handling has been offloaded to the `cookie` crate: + * `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs. + * Some types now require lifetime parameters. + +* The time crate was updated to `v0.2`, a major breaking change to the time crate, which affects + any `actix-web` method previously expecting a time v0.1 input. + * Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now result in `SameSite=None` being sent with the response Set-Cookie header. To create a cookie without a SameSite attribute, remove any calls setting same_site. * actix-http support for Actors messages was moved to actix-http crate and is enabled with feature `actors` + * content_length function is removed from actix-http. You can set Content-Length by normally setting the response body or calling no_chunking function. From 7f8073233a4b9230f5bf9bdd04cbc5e931553cb5 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 15 Sep 2020 11:32:31 +0100 Subject: [PATCH 63/78] fix trimming to inaccessible root path (#1678) --- CHANGES.md | 4 ++++ src/middleware/normalize.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index aadf627bc..3379875ec 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,10 @@ # Changes ## Unreleased - 2020-xx-xx +### Fixed +* `NormalizePath` when used with `TrailingSlash::Trim` no longer trims the root path "/". [#1678] + +[#1678]: https://github.com/actix/actix-web/pull/1678 ## 3.0.1 - 2020-09-13 diff --git a/src/middleware/normalize.rs b/src/middleware/normalize.rs index 1b6894796..8452c9083 100644 --- a/src/middleware/normalize.rs +++ b/src/middleware/normalize.rs @@ -79,6 +79,7 @@ where } } +#[doc(hidden)] pub struct NormalizePathNormalization { service: S, merge_slash: Regex, @@ -113,6 +114,10 @@ where // normalize multiple /'s to one / let path = self.merge_slash.replace_all(&path, "/"); + // Ensure root paths are still resolvable. If resulting path is blank after previous step + // it means the path was one or more slashes. Reduce to single slash. + let path = if path.is_empty() { "/" } else { path.as_ref() }; + // Check whether the path has been changed // // This check was previously implemented as string length comparison @@ -158,10 +163,23 @@ mod tests { let mut app = init_service( App::new() .wrap(NormalizePath::default()) + .service(web::resource("/").to(HttpResponse::Ok)) .service(web::resource("/v1/something/").to(HttpResponse::Ok)), ) .await; + let req = TestRequest::with_uri("/").to_request(); + let res = call_service(&mut app, req).await; + assert!(res.status().is_success()); + + let req = TestRequest::with_uri("/?query=test").to_request(); + let res = call_service(&mut app, req).await; + assert!(res.status().is_success()); + + let req = TestRequest::with_uri("///").to_request(); + let res = call_service(&mut app, req).await; + assert!(res.status().is_success()); + let req = TestRequest::with_uri("/v1//something////").to_request(); let res = call_service(&mut app, req).await; assert!(res.status().is_success()); @@ -184,10 +202,24 @@ mod tests { let mut app = init_service( App::new() .wrap(NormalizePath(TrailingSlash::Trim)) + .service(web::resource("/").to(HttpResponse::Ok)) .service(web::resource("/v1/something").to(HttpResponse::Ok)), ) .await; + // root paths should still work + let req = TestRequest::with_uri("/").to_request(); + let res = call_service(&mut app, req).await; + assert!(res.status().is_success()); + + let req = TestRequest::with_uri("/?query=test").to_request(); + let res = call_service(&mut app, req).await; + assert!(res.status().is_success()); + + let req = TestRequest::with_uri("///").to_request(); + let res = call_service(&mut app, req).await; + assert!(res.status().is_success()); + let req = TestRequest::with_uri("/v1/something////").to_request(); let res = call_service(&mut app, req).await; assert!(res.status().is_success()); From a429ee66466c8ddb1f6c922dfc9b0758b22d5815 Mon Sep 17 00:00:00 2001 From: Aleksandrov Vladimir Date: Tue, 15 Sep 2020 14:09:16 +0300 Subject: [PATCH 64/78] Add possibility to set address for test_server (#1645) --- test-server/CHANGES.md | 4 +++- test-server/src/lib.rs | 10 +++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/test-server/CHANGES.md b/test-server/CHANGES.md index e71e9d0b8..0a11e2cae 100644 --- a/test-server/CHANGES.md +++ b/test-server/CHANGES.md @@ -2,11 +2,13 @@ ## Unreleased - 2020-xx-xx +* add ability to set address for `TestServer` [#1645] + +[#1645]: https://github.com/actix/actix-web/pull/1645 ## 2.0.0 - 2020-09-11 * Update actix-codec and actix-utils dependencies. - ## 2.0.0-alpha.1 - 2020-05-23 * Update the `time` dependency to 0.2.7 * Update `actix-connect` dependency to 2.0.0-alpha.2 diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index 5d5750279..4159c8d86 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -44,12 +44,20 @@ pub use actix_testing::*; /// } /// ``` pub async fn test_server>(factory: F) -> TestServer { + let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); + test_server_with_addr(tcp, factory).await +} + +/// Start [`test server`](./fn.test_server.html) on a concrete Address +pub async fn test_server_with_addr>( + tcp: net::TcpListener, + factory: F, +) -> TestServer { let (tx, rx) = mpsc::channel(); // run server in separate thread thread::spawn(move || { let sys = System::new("actix-test-server"); - let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); let local_addr = tcp.local_addr().unwrap(); Server::build() From d7077045565ca7c4d484c2fb14b5fd5f07dfecc2 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 15 Sep 2020 13:14:14 +0100 Subject: [PATCH 65/78] prepare web release 3.0.2 (#1681) --- CHANGES.md | 3 +++ Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 3379875ec..15d3c81ce 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2020-xx-xx + + +## 3.0.2 - 2020-09-15 ### Fixed * `NormalizePath` when used with `TrailingSlash::Trim` no longer trims the root path "/". [#1678] diff --git a/Cargo.toml b/Cargo.toml index b2cffbc48..8ce615433 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "3.0.1" +version = "3.0.2" authors = ["Nikolay Kim "] description = "Actix web is a powerful, pragmatic, and extremely fast web framework for Rust." readme = "README.md" From 509b2e6eec004fd622755b2aeac4db9cdc7ac8c7 Mon Sep 17 00:00:00 2001 From: Matt Gathu Date: Wed, 16 Sep 2020 23:37:41 +0200 Subject: [PATCH 66/78] Provide attribute macro for multiple HTTP methods (#1674) Co-authored-by: Rob Ede --- actix-web-codegen/CHANGES.md | 2 + actix-web-codegen/Cargo.toml | 1 + actix-web-codegen/src/lib.rs | 17 ++++ actix-web-codegen/src/route.rs | 80 ++++++++++++++++++- actix-web-codegen/tests/test_macro.rs | 26 +++++- actix-web-codegen/tests/trybuild.rs | 20 +++++ .../trybuild/route-duplicate-method-fail.rs | 15 ++++ .../route-duplicate-method-fail.stderr | 11 +++ .../route-missing-method-fail-msrv.rs | 15 ++++ .../route-missing-method-fail-msrv.stderr | 11 +++ .../trybuild/route-missing-method-fail.rs | 15 ++++ .../trybuild/route-missing-method-fail.stderr | 13 +++ actix-web-codegen/tests/trybuild/route-ok.rs | 15 ++++ .../trybuild/route-unexpected-method-fail.rs | 15 ++++ .../route-unexpected-method-fail.stderr | 11 +++ 15 files changed, 263 insertions(+), 4 deletions(-) create mode 100644 actix-web-codegen/tests/trybuild/route-duplicate-method-fail.rs create mode 100644 actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr create mode 100644 actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.rs create mode 100644 actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.stderr create mode 100644 actix-web-codegen/tests/trybuild/route-missing-method-fail.rs create mode 100644 actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr create mode 100644 actix-web-codegen/tests/trybuild/route-ok.rs create mode 100644 actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs create mode 100644 actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index 5c0ce828a..793864d43 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -2,8 +2,10 @@ ## Unreleased - 2020-xx-xx * Added compile success and failure testing. [#1677] +* Add `route` macro for supporting multiple HTTP methods guards. [#1677]: https://github.com/actix/actix-web/pull/1677 +[#1674]: https://github.com/actix/actix-web/pull/1674 ## 0.3.0 - 2020-09-11 diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 1bf78f997..da37b8de6 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -23,3 +23,4 @@ actix-rt = "1.0.0" actix-web = "3.0.0" futures-util = { version = "0.3.5", default-features = false } trybuild = "1" +rustversion = "1" diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 445fe924d..7ae6a26b1 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -141,6 +141,23 @@ pub fn patch(args: TokenStream, input: TokenStream) -> TokenStream { route::generate(args, input, route::GuardType::Patch) } +/// Creates resource handler, allowing multiple HTTP method guards. +/// +/// Syntax: `#[route("path"[, attributes])]` +/// +/// Example: `#[route("/", method="GET", method="HEAD")]` +/// +/// ## Attributes +/// +/// - `"path"` - Raw literal string with path for which to register handler. Mandatory. +/// - `method="HTTP_METHOD"` - Registers HTTP method to provide guard for. +/// - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard` +/// - `wrap="Middleware"` - Registers a resource middleware. +#[proc_macro_attribute] +pub fn route(args: TokenStream, input: TokenStream) -> TokenStream { + route::generate(args, input, route::GuardType::Multi) +} + /// Marks async main function as the actix system entry-point. /// /// ## Usage diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index 676e75e07..394ced212 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -1,5 +1,8 @@ extern crate proc_macro; +use std::collections::HashSet; +use std::convert::TryFrom; + use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{format_ident, quote, ToTokens, TokenStreamExt}; @@ -17,7 +20,7 @@ impl ToTokens for ResourceType { } } -#[derive(PartialEq)] +#[derive(Debug, PartialEq, Eq, Hash)] pub enum GuardType { Get, Post, @@ -28,6 +31,7 @@ pub enum GuardType { Options, Trace, Patch, + Multi, } impl GuardType { @@ -42,6 +46,7 @@ impl GuardType { GuardType::Options => "Options", GuardType::Trace => "Trace", GuardType::Patch => "Patch", + GuardType::Multi => "Multi", } } } @@ -53,10 +58,33 @@ impl ToTokens for GuardType { } } +impl TryFrom<&syn::LitStr> for GuardType { + type Error = syn::Error; + + fn try_from(value: &syn::LitStr) -> Result { + match value.value().as_str() { + "CONNECT" => Ok(GuardType::Connect), + "DELETE" => Ok(GuardType::Delete), + "GET" => Ok(GuardType::Get), + "HEAD" => Ok(GuardType::Head), + "OPTIONS" => Ok(GuardType::Options), + "PATCH" => Ok(GuardType::Patch), + "POST" => Ok(GuardType::Post), + "PUT" => Ok(GuardType::Put), + "TRACE" => Ok(GuardType::Trace), + _ => Err(syn::Error::new_spanned( + value, + &format!("Unexpected HTTP Method: `{}`", value.value()), + )), + } + } +} + struct Args { path: syn::LitStr, guards: Vec, wrappers: Vec, + methods: HashSet, } impl Args { @@ -64,6 +92,7 @@ impl Args { let mut path = None; let mut guards = Vec::new(); let mut wrappers = Vec::new(); + let mut methods = HashSet::new(); for arg in args { match arg { NestedMeta::Lit(syn::Lit::Str(lit)) => match path { @@ -96,10 +125,28 @@ impl Args { "Attribute wrap expects type", )); } + } else if nv.path.is_ident("method") { + if let syn::Lit::Str(ref lit) = nv.lit { + let guard = GuardType::try_from(lit)?; + if !methods.insert(guard) { + return Err(syn::Error::new_spanned( + &nv.lit, + &format!( + "HTTP Method defined more than once: `{}`", + lit.value() + ), + )); + } + } else { + return Err(syn::Error::new_spanned( + nv.lit, + "Attribute method expects literal string!", + )); + } } else { return Err(syn::Error::new_spanned( nv.path, - "Unknown attribute key is specified. Allowed: guard and wrap", + "Unknown attribute key is specified. Allowed: guard, method and wrap", )); } } @@ -112,6 +159,7 @@ impl Args { path: path.unwrap(), guards, wrappers, + methods, }) } } @@ -166,6 +214,13 @@ impl Route { let args = Args::new(args)?; + if guard == GuardType::Multi && args.methods.is_empty() { + return Err(syn::Error::new( + Span::call_site(), + "The #[route(..)] macro requires at least one `method` attribute", + )); + } + let resource_type = if ast.sig.asyncness.is_some() { ResourceType::Async } else { @@ -201,10 +256,29 @@ impl ToTokens for Route { path, guards, wrappers, + methods, }, resource_type, } = self; let resource_name = name.to_string(); + let mut methods = methods.iter(); + + let method_guards = if *guard == GuardType::Multi { + // unwrapping since length is checked to be at least one + let first = methods.next().unwrap(); + + quote! { + .guard( + actix_web::guard::Any(actix_web::guard::#first()) + #(.or(actix_web::guard::#methods()))* + ) + } + } else { + quote! { + .guard(actix_web::guard::#guard()) + } + }; + let stream = quote! { #[allow(non_camel_case_types, missing_docs)] pub struct #name; @@ -214,7 +288,7 @@ impl ToTokens for Route { #ast let __resource = actix_web::Resource::new(#path) .name(#resource_name) - .guard(actix_web::guard::#guard()) + #method_guards #(.guard(actix_web::guard::fn_guard(#guards)))* #(.wrap(#wrappers))* .#resource_type(#name); diff --git a/actix-web-codegen/tests/test_macro.rs b/actix-web-codegen/tests/test_macro.rs index 13e9120f6..dd2bccd7f 100644 --- a/actix-web-codegen/tests/test_macro.rs +++ b/actix-web-codegen/tests/test_macro.rs @@ -5,7 +5,9 @@ use std::task::{Context, Poll}; use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; use actix_web::http::header::{HeaderName, HeaderValue}; use actix_web::{http, test, web::Path, App, Error, HttpResponse, Responder}; -use actix_web_codegen::{connect, delete, get, head, options, patch, post, put, trace}; +use actix_web_codegen::{ + connect, delete, get, head, options, patch, post, put, route, trace, +}; use futures_util::future; // Make sure that we can name function as 'config' @@ -79,6 +81,11 @@ async fn get_param_test(_: Path) -> impl Responder { HttpResponse::Ok() } +#[route("/multi", method = "GET", method = "POST", method = "HEAD")] +async fn route_test() -> impl Responder { + HttpResponse::Ok() +} + pub struct ChangeStatusCode; impl Transform for ChangeStatusCode @@ -172,6 +179,7 @@ async fn test_body() { .service(trace_test) .service(patch_test) .service(test_handler) + .service(route_test) }); let request = srv.request(http::Method::GET, srv.url("/test")); let response = request.send().await.unwrap(); @@ -210,6 +218,22 @@ async fn test_body() { let request = srv.request(http::Method::GET, srv.url("/test")); let response = request.send().await.unwrap(); assert!(response.status().is_success()); + + let request = srv.request(http::Method::GET, srv.url("/multi")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::POST, srv.url("/multi")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::HEAD, srv.url("/multi")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::PATCH, srv.url("/multi")); + let response = request.send().await.unwrap(); + assert!(!response.status().is_success()); } #[actix_rt::test] diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs index b675947d3..1bc2bd25e 100644 --- a/actix-web-codegen/tests/trybuild.rs +++ b/actix-web-codegen/tests/trybuild.rs @@ -4,4 +4,24 @@ fn compile_macros() { t.pass("tests/trybuild/simple.rs"); t.compile_fail("tests/trybuild/simple-fail.rs"); + + t.pass("tests/trybuild/route-ok.rs"); + t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs"); + t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs"); + + test_route_missing_method(&t) } + +#[rustversion::stable(1.42)] +fn test_route_missing_method(t: &trybuild::TestCases) { + t.compile_fail("tests/trybuild/route-missing-method-fail-msrv.rs"); +} + +#[rustversion::not(stable(1.42))] +#[rustversion::not(nightly)] +fn test_route_missing_method(t: &trybuild::TestCases) { + t.compile_fail("tests/trybuild/route-missing-method-fail.rs"); +} + +#[rustversion::nightly] +fn test_route_missing_method(_t: &trybuild::TestCases) {} diff --git a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.rs b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.rs new file mode 100644 index 000000000..9ce980251 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.rs @@ -0,0 +1,15 @@ +use actix_web::*; + +#[route("/", method="GET", method="GET")] +async fn index() -> impl Responder { + HttpResponse::Ok() +} + +#[actix_web::main] +async fn main() { + let srv = test::start(|| App::new().service(index)); + + let request = srv.get("/"); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); +} diff --git a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr new file mode 100644 index 000000000..8bf857c4d --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr @@ -0,0 +1,11 @@ +error: HTTP Method defined more than once: `GET` + --> $DIR/route-duplicate-method-fail.rs:3:35 + | +3 | #[route("/", method="GET", method="GET")] + | ^^^^^ + +error[E0425]: cannot find value `index` in this scope + --> $DIR/route-duplicate-method-fail.rs:10:49 + | +10 | let srv = test::start(|| App::new().service(index)); + | ^^^^^ not found in this scope diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.rs b/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.rs new file mode 100644 index 000000000..5c30b57ce --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.rs @@ -0,0 +1,15 @@ +use actix_web::*; + +#[route("/")] +async fn index() -> impl Responder { + HttpResponse::Ok() +} + +#[actix_web::main] +async fn main() { + let srv = test::start(|| App::new().service(index)); + + let request = srv.get("/"); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); +} diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.stderr b/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.stderr new file mode 100644 index 000000000..f59f6c27e --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.stderr @@ -0,0 +1,11 @@ +error: The #[route(..)] macro requires at least one `method` attribute + --> $DIR/route-missing-method-fail-msrv.rs:3:1 + | +3 | #[route("/")] + | ^^^^^^^^^^^^^ + +error[E0425]: cannot find value `index` in this scope + --> $DIR/route-missing-method-fail-msrv.rs:10:49 + | +10 | let srv = test::start(|| App::new().service(index)); + | ^^^^^ not found in this scope diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail.rs b/actix-web-codegen/tests/trybuild/route-missing-method-fail.rs new file mode 100644 index 000000000..5c30b57ce --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail.rs @@ -0,0 +1,15 @@ +use actix_web::*; + +#[route("/")] +async fn index() -> impl Responder { + HttpResponse::Ok() +} + +#[actix_web::main] +async fn main() { + let srv = test::start(|| App::new().service(index)); + + let request = srv.get("/"); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); +} diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr new file mode 100644 index 000000000..6d35ea600 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr @@ -0,0 +1,13 @@ +error: The #[route(..)] macro requires at least one `method` attribute + --> $DIR/route-missing-method-fail.rs:3:1 + | +3 | #[route("/")] + | ^^^^^^^^^^^^^ + | + = 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 + --> $DIR/route-missing-method-fail.rs:10:49 + | +10 | let srv = test::start(|| App::new().service(index)); + | ^^^^^ not found in this scope diff --git a/actix-web-codegen/tests/trybuild/route-ok.rs b/actix-web-codegen/tests/trybuild/route-ok.rs new file mode 100644 index 000000000..bfac56e12 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-ok.rs @@ -0,0 +1,15 @@ +use actix_web::*; + +#[route("/", method="GET", method="HEAD")] +async fn index() -> impl Responder { + HttpResponse::Ok() +} + +#[actix_web::main] +async fn main() { + let srv = test::start(|| App::new().service(index)); + + let request = srv.get("/"); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); +} diff --git a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs new file mode 100644 index 000000000..f4d8d9445 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs @@ -0,0 +1,15 @@ +use actix_web::*; + +#[route("/", method="UNEXPECTED")] +async fn index() -> impl Responder { + HttpResponse::Ok() +} + +#[actix_web::main] +async fn main() { + let srv = test::start(|| App::new().service(index)); + + let request = srv.get("/"); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); +} diff --git a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr new file mode 100644 index 000000000..3fe49f774 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr @@ -0,0 +1,11 @@ +error: Unexpected HTTP Method: `UNEXPECTED` + --> $DIR/route-unexpected-method-fail.rs:3:21 + | +3 | #[route("/", method="UNEXPECTED")] + | ^^^^^^^^^^^^ + +error[E0425]: cannot find value `index` in this scope + --> $DIR/route-unexpected-method-fail.rs:10:49 + | +10 | let srv = test::start(|| App::new().service(index)); + | ^^^^^ not found in this scope From 2a2474ca09711263ba429491ed908466354452d3 Mon Sep 17 00:00:00 2001 From: Lokathor Date: Thu, 17 Sep 2020 11:09:42 -0600 Subject: [PATCH 67/78] Update `tinyvec` to 1.0 (#1689) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8ce615433..091e67dd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,7 +99,7 @@ time = { version = "0.2.7", default-features = false, features = ["std"] } url = "2.1" open-ssl = { package = "openssl", version = "0.10", optional = true } rust-tls = { package = "rustls", version = "0.18.0", optional = true } -tinyvec = { version = "0.3", features = ["alloc"] } +tinyvec = { version = "1", features = ["alloc"] } [dev-dependencies] actix = "0.10.0" From 1596893ef7275a2c6530e09abaec7e2cc2f0bf2c Mon Sep 17 00:00:00 2001 From: Silentdoer <1010993610@qq.com> Date: Sat, 19 Sep 2020 22:20:34 +0800 Subject: [PATCH 68/78] update actix-http dev-dependencies (#1696) Co-authored-by: luojinming --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 091e67dd7..d4da4af11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,7 +103,7 @@ tinyvec = { version = "1", features = ["alloc"] } [dev-dependencies] actix = "0.10.0" -actix-http = { version = "2.0.0-beta.4", features = ["actors"] } +actix-http = { version = "2.0.0", features = ["actors"] } rand = "0.7" env_logger = "0.7" serde_derive = "1.0" From f9e3f78e456e56dadaa70554d86b130e2655b148 Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Sun, 20 Sep 2020 19:21:53 +0300 Subject: [PATCH 69/78] eemove non-relevant comment from actix-http README.md (#1701) --- actix-http/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/actix-http/README.md b/actix-http/README.md index d4c96f2a7..96fc54d2e 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -13,12 +13,11 @@ Actix http ## Example ```rust -// see examples/framed_hello.rs for complete list of used crates. use std::{env, io}; use actix_http::{HttpService, Response}; use actix_server::Server; -use futures::future; +use futures_util::future; use http::header::HeaderValue; use log::info; From f7bcad95672940a71abdab9046c45b1ef8cb6c22 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sun, 20 Sep 2020 23:18:25 +0100 Subject: [PATCH 70/78] split up files lib (#1685) --- actix-files/Cargo.toml | 1 - actix-files/src/chunked.rs | 94 +++++ actix-files/src/directory.rs | 114 ++++++ actix-files/src/files.rs | 250 ++++++++++++ actix-files/src/lib.rs | 715 +++-------------------------------- actix-files/src/named.rs | 54 ++- actix-files/src/path_buf.rs | 99 +++++ actix-files/src/range.rs | 5 +- actix-files/src/service.rs | 167 ++++++++ 9 files changed, 825 insertions(+), 674 deletions(-) create mode 100644 actix-files/src/chunked.rs create mode 100644 actix-files/src/directory.rs create mode 100644 actix-files/src/files.rs create mode 100644 actix-files/src/path_buf.rs create mode 100644 actix-files/src/service.rs diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 8841f7fb1..634296c56 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -18,7 +18,6 @@ path = "src/lib.rs" [dependencies] actix-web = { version = "3.0.0", default-features = false } -actix-http = "2.0.0" actix-service = "1.0.6" bitflags = "1" bytes = "0.5.3" diff --git a/actix-files/src/chunked.rs b/actix-files/src/chunked.rs new file mode 100644 index 000000000..580b06787 --- /dev/null +++ b/actix-files/src/chunked.rs @@ -0,0 +1,94 @@ +use std::{ + cmp, fmt, + fs::File, + future::Future, + io::{self, Read, Seek}, + pin::Pin, + task::{Context, Poll}, +}; + +use actix_web::{ + error::{BlockingError, Error}, + web, +}; +use bytes::Bytes; +use futures_core::{ready, Stream}; +use futures_util::future::{FutureExt, LocalBoxFuture}; + +use crate::handle_error; + +type ChunkedBoxFuture = + LocalBoxFuture<'static, Result<(File, Bytes), BlockingError>>; + +#[doc(hidden)] +/// A helper created from a `std::fs::File` which reads the file +/// chunk-by-chunk on a `ThreadPool`. +pub struct ChunkedReadFile { + pub(crate) size: u64, + pub(crate) offset: u64, + pub(crate) file: Option, + pub(crate) fut: Option, + pub(crate) counter: u64, +} + +impl fmt::Debug for ChunkedReadFile { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("ChunkedReadFile") + } +} + +impl Stream for ChunkedReadFile { + type Item = Result; + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + if let Some(ref mut fut) = self.fut { + return match ready!(Pin::new(fut).poll(cx)) { + Ok((file, bytes)) => { + self.fut.take(); + self.file = Some(file); + + self.offset += bytes.len() as u64; + self.counter += bytes.len() as u64; + + Poll::Ready(Some(Ok(bytes))) + } + Err(e) => Poll::Ready(Some(Err(handle_error(e)))), + }; + } + + let size = self.size; + let offset = self.offset; + let counter = self.counter; + + if size == counter { + Poll::Ready(None) + } else { + let mut file = self.file.take().expect("Use after completion"); + + self.fut = Some( + web::block(move || { + let max_bytes = + cmp::min(size.saturating_sub(counter), 65_536) as usize; + + let mut buf = Vec::with_capacity(max_bytes); + file.seek(io::SeekFrom::Start(offset))?; + + let n_bytes = + file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?; + + if n_bytes == 0 { + return Err(io::ErrorKind::UnexpectedEof.into()); + } + + Ok((file, Bytes::from(buf))) + }) + .boxed_local(), + ); + + self.poll_next(cx) + } + } +} diff --git a/actix-files/src/directory.rs b/actix-files/src/directory.rs new file mode 100644 index 000000000..3717985d3 --- /dev/null +++ b/actix-files/src/directory.rs @@ -0,0 +1,114 @@ +use std::{fmt::Write, fs::DirEntry, io, path::Path, path::PathBuf}; + +use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse}; +use percent_encoding::{utf8_percent_encode, CONTROLS}; +use v_htmlescape::escape as escape_html_entity; + +/// A directory; responds with the generated directory listing. +#[derive(Debug)] +pub struct Directory { + /// Base directory. + pub base: PathBuf, + + /// Path of subdirectory to generate listing for. + pub path: PathBuf, +} + +impl Directory { + /// Create a new directory + pub fn new(base: PathBuf, path: PathBuf) -> Directory { + Directory { base, path } + } + + /// Is this entry visible from this directory? + pub fn is_visible(&self, entry: &io::Result) -> bool { + if let Ok(ref entry) = *entry { + if let Some(name) = entry.file_name().to_str() { + if name.starts_with('.') { + return false; + } + } + if let Ok(ref md) = entry.metadata() { + let ft = md.file_type(); + return ft.is_dir() || ft.is_file() || ft.is_symlink(); + } + } + false + } +} + +pub(crate) type DirectoryRenderer = + dyn Fn(&Directory, &HttpRequest) -> Result; + +// show file url as relative to static path +macro_rules! encode_file_url { + ($path:ident) => { + utf8_percent_encode(&$path, CONTROLS) + }; +} + +// " -- " & -- & ' -- ' < -- < > -- > / -- / +macro_rules! encode_file_name { + ($entry:ident) => { + escape_html_entity(&$entry.file_name().to_string_lossy()) + }; +} + +pub(crate) fn directory_listing( + dir: &Directory, + req: &HttpRequest, +) -> Result { + let index_of = format!("Index of {}", req.path()); + let mut body = String::new(); + let base = Path::new(req.path()); + + for entry in dir.path.read_dir()? { + if dir.is_visible(&entry) { + let entry = entry.unwrap(); + let p = match entry.path().strip_prefix(&dir.path) { + Ok(p) if cfg!(windows) => { + base.join(p).to_string_lossy().replace("\\", "/") + } + Ok(p) => base.join(p).to_string_lossy().into_owned(), + Err(_) => continue, + }; + + // if file is a directory, add '/' to the end of the name + if let Ok(metadata) = entry.metadata() { + if metadata.is_dir() { + let _ = write!( + body, + "
  • {}/
  • ", + encode_file_url!(p), + encode_file_name!(entry), + ); + } else { + let _ = write!( + body, + "
  • {}
  • ", + encode_file_url!(p), + encode_file_name!(entry), + ); + } + } else { + continue; + } + } + } + + let html = format!( + "\ + {}\ +

    {}

    \ +
      \ + {}\ +
    \n", + index_of, index_of, body + ); + Ok(ServiceResponse::new( + req.clone(), + HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(html), + )) +} diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs new file mode 100644 index 000000000..2b55e1aa9 --- /dev/null +++ b/actix-files/src/files.rs @@ -0,0 +1,250 @@ +use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc}; + +use actix_service::{boxed, IntoServiceFactory, ServiceFactory}; +use actix_web::{ + dev::{ + AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse, + }, + error::Error, + guard::Guard, + http::header::DispositionType, + HttpRequest, +}; +use futures_util::future::{ok, FutureExt, LocalBoxFuture}; + +use crate::{ + directory_listing, named, Directory, DirectoryRenderer, FilesService, + HttpNewService, MimeOverride, +}; + +/// Static files handling service. +/// +/// `Files` service must be registered with `App::service()` method. +/// +/// ```rust +/// use actix_web::App; +/// use actix_files::Files; +/// +/// let app = App::new() +/// .service(Files::new("/static", ".")); +/// ``` +pub struct Files { + path: String, + directory: PathBuf, + index: Option, + show_index: bool, + redirect_to_slash: bool, + default: Rc>>>, + renderer: Rc, + mime_override: Option>, + file_flags: named::Flags, + guards: Option>, +} + +impl fmt::Debug for Files { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Files") + } +} + +impl Clone for Files { + fn clone(&self) -> Self { + Self { + directory: self.directory.clone(), + index: self.index.clone(), + show_index: self.show_index, + redirect_to_slash: self.redirect_to_slash, + default: self.default.clone(), + renderer: self.renderer.clone(), + file_flags: self.file_flags, + path: self.path.clone(), + mime_override: self.mime_override.clone(), + guards: self.guards.clone(), + } + } +} + +impl Files { + /// Create new `Files` instance for specified base directory. + /// + /// `File` uses `ThreadPool` for blocking filesystem operations. + /// By default pool with 5x threads of available cpus is used. + /// Pool size can be changed by setting ACTIX_THREADPOOL environment variable. + pub fn new>(path: &str, dir: T) -> Files { + let orig_dir = dir.into(); + let dir = match orig_dir.canonicalize() { + Ok(canon_dir) => canon_dir, + Err(_) => { + log::error!("Specified path is not a directory: {:?}", orig_dir); + PathBuf::new() + } + }; + + Files { + path: path.to_string(), + directory: dir, + index: None, + show_index: false, + redirect_to_slash: false, + default: Rc::new(RefCell::new(None)), + renderer: Rc::new(directory_listing), + mime_override: None, + file_flags: named::Flags::default(), + guards: None, + } + } + + /// Show files listing for directories. + /// + /// By default show files listing is disabled. + pub fn show_files_listing(mut self) -> Self { + self.show_index = true; + self + } + + /// Redirects to a slash-ended path when browsing a directory. + /// + /// By default never redirect. + pub fn redirect_to_slash_directory(mut self) -> Self { + self.redirect_to_slash = true; + self + } + + /// Set custom directory renderer + pub fn files_listing_renderer(mut self, f: F) -> Self + where + for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result + + 'static, + { + self.renderer = Rc::new(f); + self + } + + /// Specifies mime override callback + pub fn mime_override(mut self, f: F) -> Self + where + F: Fn(&mime::Name<'_>) -> DispositionType + 'static, + { + self.mime_override = Some(Rc::new(f)); + self + } + + /// Set index file + /// + /// Shows specific index file for directory "/" instead of + /// showing files listing. + pub fn index_file>(mut self, index: T) -> Self { + self.index = Some(index.into()); + self + } + + #[inline] + /// Specifies whether to use ETag or not. + /// + /// Default is true. + pub fn use_etag(mut self, value: bool) -> Self { + self.file_flags.set(named::Flags::ETAG, value); + self + } + + #[inline] + /// Specifies whether to use Last-Modified or not. + /// + /// Default is true. + pub fn use_last_modified(mut self, value: bool) -> Self { + self.file_flags.set(named::Flags::LAST_MD, value); + self + } + + /// Specifies custom guards to use for directory listings and files. + /// + /// Default behaviour allows GET and HEAD. + #[inline] + pub fn use_guards(mut self, guards: G) -> Self { + self.guards = Some(Rc::new(guards)); + self + } + + /// Disable `Content-Disposition` header. + /// + /// By default Content-Disposition` header is enabled. + #[inline] + pub fn disable_content_disposition(mut self) -> Self { + self.file_flags.remove(named::Flags::CONTENT_DISPOSITION); + self + } + + /// Sets default handler which is used when no matched file could be found. + pub fn default_handler(mut self, f: F) -> Self + where + F: IntoServiceFactory, + U: ServiceFactory< + Config = (), + Request = ServiceRequest, + Response = ServiceResponse, + Error = Error, + > + 'static, + { + // create and configure default resource + self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory( + f.into_factory().map_init_err(|_| ()), + ))))); + + self + } +} + +impl HttpServiceFactory for Files { + fn register(self, config: &mut AppService) { + if self.default.borrow().is_none() { + *self.default.borrow_mut() = Some(config.default_service()); + } + + let rdef = if config.is_root() { + ResourceDef::root_prefix(&self.path) + } else { + ResourceDef::prefix(&self.path) + }; + + config.register_service(rdef, None, self, None) + } +} + +impl ServiceFactory for Files { + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = Error; + type Config = (); + type Service = FilesService; + type InitError = (); + type Future = LocalBoxFuture<'static, Result>; + + fn new_service(&self, _: ()) -> Self::Future { + let mut srv = FilesService { + directory: self.directory.clone(), + index: self.index.clone(), + show_index: self.show_index, + redirect_to_slash: self.redirect_to_slash, + default: None, + renderer: self.renderer.clone(), + mime_override: self.mime_override.clone(), + file_flags: self.file_flags, + guards: self.guards.clone(), + }; + + if let Some(ref default) = *self.default.borrow() { + default + .new_service(()) + .map(move |result| match result { + Ok(default) => { + srv.default = Some(default); + Ok(srv) + } + Err(_) => Err(()), + }) + .boxed_local() + } else { + ok(srv).boxed_local() + } + } +} diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 91c054947..1fc7cb3f3 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -1,44 +1,52 @@ -//! Static files support +//! Static files support for Actix Web. +//! +//! Provides a non-blocking service for serving static files from disk. +//! +//! # Example +//! ```rust +//! use actix_web::App; +//! use actix_files::Files; +//! +//! let app = App::new() +//! .service(Files::new("/static", ".")); +//! ``` +//! +//! # Implementation Quirks +//! - If a filename contains non-ascii characters, that file will be served with the `charset=utf-8` +//! extension on the Content-Type header. #![deny(rust_2018_idioms)] -#![allow(clippy::borrow_interior_mutable_const)] +#![warn(missing_docs, missing_debug_implementations)] -use std::cell::RefCell; -use std::fmt::Write; -use std::fs::{DirEntry, File}; -use std::future::Future; -use std::io::{Read, Seek}; -use std::path::{Path, PathBuf}; -use std::pin::Pin; -use std::rc::Rc; -use std::task::{Context, Poll}; -use std::{cmp, io}; +use std::io; -use actix_service::boxed::{self, BoxService, BoxServiceFactory}; -use actix_service::{IntoServiceFactory, Service, ServiceFactory}; -use actix_web::dev::{ - AppService, HttpServiceFactory, Payload, ResourceDef, ServiceRequest, - ServiceResponse, +use actix_service::boxed::{BoxService, BoxServiceFactory}; +use actix_web::{ + dev::{ServiceRequest, ServiceResponse}, + error::{BlockingError, Error, ErrorInternalServerError}, + http::header::DispositionType, }; -use actix_web::error::{BlockingError, Error, ErrorInternalServerError}; -use actix_web::guard::Guard; -use actix_web::http::header::{self, DispositionType}; -use actix_web::http::Method; -use actix_web::{web, FromRequest, HttpRequest, HttpResponse}; -use bytes::Bytes; -use futures_core::Stream; -use futures_util::future::{ok, ready, Either, FutureExt, LocalBoxFuture, Ready}; use mime_guess::from_ext; -use percent_encoding::{utf8_percent_encode, CONTROLS}; -use v_htmlescape::escape as escape_html_entity; +mod chunked; +mod directory; mod error; +mod files; mod named; +mod path_buf; mod range; +mod service; -use self::error::{FilesError, UriSegmentError}; +pub use crate::chunked::ChunkedReadFile; +pub use crate::directory::Directory; +pub use crate::files::Files; pub use crate::named::NamedFile; pub use crate::range::HttpRange; +pub use crate::service::FilesService; + +use self::directory::{directory_listing, DirectoryRenderer}; +use self::error::FilesError; +use self::path_buf::PathBufWrap; type HttpService = BoxService; type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; @@ -51,612 +59,37 @@ pub fn file_extension_to_mime(ext: &str) -> mime::Mime { from_ext(ext).first_or_octet_stream() } -fn handle_error(err: BlockingError) -> Error { +pub(crate) fn handle_error(err: BlockingError) -> Error { match err { BlockingError::Error(err) => err.into(), BlockingError::Canceled => ErrorInternalServerError("Unexpected error"), } } -#[doc(hidden)] -/// A helper created from a `std::fs::File` which reads the file -/// chunk-by-chunk on a `ThreadPool`. -pub struct ChunkedReadFile { - size: u64, - offset: u64, - file: Option, - #[allow(clippy::type_complexity)] - fut: - Option>>>, - counter: u64, -} - -impl Stream for ChunkedReadFile { - type Item = Result; - - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - if let Some(ref mut fut) = self.fut { - return match Pin::new(fut).poll(cx) { - Poll::Ready(Ok((file, bytes))) => { - self.fut.take(); - self.file = Some(file); - self.offset += bytes.len() as u64; - self.counter += bytes.len() as u64; - Poll::Ready(Some(Ok(bytes))) - } - Poll::Ready(Err(e)) => Poll::Ready(Some(Err(handle_error(e)))), - Poll::Pending => Poll::Pending, - }; - } - - let size = self.size; - let offset = self.offset; - let counter = self.counter; - - if size == counter { - Poll::Ready(None) - } else { - let mut file = self.file.take().expect("Use after completion"); - self.fut = Some( - web::block(move || { - let max_bytes: usize; - max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; - let mut buf = Vec::with_capacity(max_bytes); - file.seek(io::SeekFrom::Start(offset))?; - let nbytes = - file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?; - if nbytes == 0 { - return Err(io::ErrorKind::UnexpectedEof.into()); - } - Ok((file, Bytes::from(buf))) - }) - .boxed_local(), - ); - self.poll_next(cx) - } - } -} - -type DirectoryRenderer = - dyn Fn(&Directory, &HttpRequest) -> Result; - -/// A directory; responds with the generated directory listing. -#[derive(Debug)] -pub struct Directory { - /// Base directory - pub base: PathBuf, - /// Path of subdirectory to generate listing for - pub path: PathBuf, -} - -impl Directory { - /// Create a new directory - pub fn new(base: PathBuf, path: PathBuf) -> Directory { - Directory { base, path } - } - - /// Is this entry visible from this directory? - pub fn is_visible(&self, entry: &io::Result) -> bool { - if let Ok(ref entry) = *entry { - if let Some(name) = entry.file_name().to_str() { - if name.starts_with('.') { - return false; - } - } - if let Ok(ref md) = entry.metadata() { - let ft = md.file_type(); - return ft.is_dir() || ft.is_file() || ft.is_symlink(); - } - } - false - } -} - -// show file url as relative to static path -macro_rules! encode_file_url { - ($path:ident) => { - utf8_percent_encode(&$path, CONTROLS) - }; -} - -// " -- " & -- & ' -- ' < -- < > -- > / -- / -macro_rules! encode_file_name { - ($entry:ident) => { - escape_html_entity(&$entry.file_name().to_string_lossy()) - }; -} - -fn directory_listing( - dir: &Directory, - req: &HttpRequest, -) -> Result { - let index_of = format!("Index of {}", req.path()); - let mut body = String::new(); - let base = Path::new(req.path()); - - for entry in dir.path.read_dir()? { - if dir.is_visible(&entry) { - let entry = entry.unwrap(); - let p = match entry.path().strip_prefix(&dir.path) { - Ok(p) if cfg!(windows) => { - base.join(p).to_string_lossy().replace("\\", "/") - } - Ok(p) => base.join(p).to_string_lossy().into_owned(), - Err(_) => continue, - }; - - // if file is a directory, add '/' to the end of the name - if let Ok(metadata) = entry.metadata() { - if metadata.is_dir() { - let _ = write!( - body, - "
  • {}/
  • ", - encode_file_url!(p), - encode_file_name!(entry), - ); - } else { - let _ = write!( - body, - "
  • {}
  • ", - encode_file_url!(p), - encode_file_name!(entry), - ); - } - } else { - continue; - } - } - } - - let html = format!( - "\ - {}\ -

    {}

    \ -
      \ - {}\ -
    \n", - index_of, index_of, body - ); - Ok(ServiceResponse::new( - req.clone(), - HttpResponse::Ok() - .content_type("text/html; charset=utf-8") - .body(html), - )) -} type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType; -/// Static files handling -/// -/// `Files` service must be registered with `App::service()` method. -/// -/// ```rust -/// use actix_web::App; -/// use actix_files::Files; -/// -/// let app = App::new() -/// .service(Files::new("/static", ".")); -/// ``` -pub struct Files { - path: String, - directory: PathBuf, - index: Option, - show_index: bool, - redirect_to_slash: bool, - default: Rc>>>, - renderer: Rc, - mime_override: Option>, - file_flags: named::Flags, - // FIXME: Should re-visit later. - #[allow(clippy::redundant_allocation)] - guards: Option>>, -} - -impl Clone for Files { - fn clone(&self) -> Self { - Self { - directory: self.directory.clone(), - index: self.index.clone(), - show_index: self.show_index, - redirect_to_slash: self.redirect_to_slash, - default: self.default.clone(), - renderer: self.renderer.clone(), - file_flags: self.file_flags, - path: self.path.clone(), - mime_override: self.mime_override.clone(), - guards: self.guards.clone(), - } - } -} - -impl Files { - /// Create new `Files` instance for specified base directory. - /// - /// `File` uses `ThreadPool` for blocking filesystem operations. - /// By default pool with 5x threads of available cpus is used. - /// Pool size can be changed by setting ACTIX_THREADPOOL environment variable. - pub fn new>(path: &str, dir: T) -> Files { - let orig_dir = dir.into(); - let dir = match orig_dir.canonicalize() { - Ok(canon_dir) => canon_dir, - Err(_) => { - log::error!("Specified path is not a directory: {:?}", orig_dir); - PathBuf::new() - } - }; - - Files { - path: path.to_string(), - directory: dir, - index: None, - show_index: false, - redirect_to_slash: false, - default: Rc::new(RefCell::new(None)), - renderer: Rc::new(directory_listing), - mime_override: None, - file_flags: named::Flags::default(), - guards: None, - } - } - - /// Show files listing for directories. - /// - /// By default show files listing is disabled. - pub fn show_files_listing(mut self) -> Self { - self.show_index = true; - self - } - - /// Redirects to a slash-ended path when browsing a directory. - /// - /// By default never redirect. - pub fn redirect_to_slash_directory(mut self) -> Self { - self.redirect_to_slash = true; - self - } - - /// Set custom directory renderer - pub fn files_listing_renderer(mut self, f: F) -> Self - where - for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result - + 'static, - { - self.renderer = Rc::new(f); - self - } - - /// Specifies mime override callback - pub fn mime_override(mut self, f: F) -> Self - where - F: Fn(&mime::Name<'_>) -> DispositionType + 'static, - { - self.mime_override = Some(Rc::new(f)); - self - } - - /// Set index file - /// - /// Shows specific index file for directory "/" instead of - /// showing files listing. - pub fn index_file>(mut self, index: T) -> Self { - self.index = Some(index.into()); - self - } - - #[inline] - /// Specifies whether to use ETag or not. - /// - /// Default is true. - pub fn use_etag(mut self, value: bool) -> Self { - self.file_flags.set(named::Flags::ETAG, value); - self - } - - #[inline] - /// Specifies whether to use Last-Modified or not. - /// - /// Default is true. - pub fn use_last_modified(mut self, value: bool) -> Self { - self.file_flags.set(named::Flags::LAST_MD, value); - self - } - - /// Specifies custom guards to use for directory listings and files. - /// - /// Default behaviour allows GET and HEAD. - #[inline] - pub fn use_guards(mut self, guards: G) -> Self { - self.guards = Some(Rc::new(Box::new(guards))); - self - } - - /// Disable `Content-Disposition` header. - /// - /// By default Content-Disposition` header is enabled. - #[inline] - pub fn disable_content_disposition(mut self) -> Self { - self.file_flags.remove(named::Flags::CONTENT_DISPOSITION); - self - } - - /// Sets default handler which is used when no matched file could be found. - pub fn default_handler(mut self, f: F) -> Self - where - F: IntoServiceFactory, - U: ServiceFactory< - Config = (), - Request = ServiceRequest, - Response = ServiceResponse, - Error = Error, - > + 'static, - { - // create and configure default resource - self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory( - f.into_factory().map_init_err(|_| ()), - ))))); - - self - } -} - -impl HttpServiceFactory for Files { - fn register(self, config: &mut AppService) { - if self.default.borrow().is_none() { - *self.default.borrow_mut() = Some(config.default_service()); - } - let rdef = if config.is_root() { - ResourceDef::root_prefix(&self.path) - } else { - ResourceDef::prefix(&self.path) - }; - config.register_service(rdef, None, self, None) - } -} - -impl ServiceFactory for Files { - type Request = ServiceRequest; - type Response = ServiceResponse; - type Error = Error; - type Config = (); - type Service = FilesService; - type InitError = (); - type Future = LocalBoxFuture<'static, Result>; - - fn new_service(&self, _: ()) -> Self::Future { - let mut srv = FilesService { - directory: self.directory.clone(), - index: self.index.clone(), - show_index: self.show_index, - redirect_to_slash: self.redirect_to_slash, - default: None, - renderer: self.renderer.clone(), - mime_override: self.mime_override.clone(), - file_flags: self.file_flags, - guards: self.guards.clone(), - }; - - if let Some(ref default) = *self.default.borrow() { - default - .new_service(()) - .map(move |result| match result { - Ok(default) => { - srv.default = Some(default); - Ok(srv) - } - Err(_) => Err(()), - }) - .boxed_local() - } else { - ok(srv).boxed_local() - } - } -} - -pub struct FilesService { - directory: PathBuf, - index: Option, - show_index: bool, - redirect_to_slash: bool, - default: Option, - renderer: Rc, - mime_override: Option>, - file_flags: named::Flags, - // FIXME: Should re-visit later. - #[allow(clippy::redundant_allocation)] - guards: Option>>, -} - -impl FilesService { - #[allow(clippy::type_complexity)] - fn handle_err( - &mut self, - e: io::Error, - req: ServiceRequest, - ) -> Either< - Ready>, - LocalBoxFuture<'static, Result>, - > { - log::debug!("Files: Failed to handle {}: {}", req.path(), e); - if let Some(ref mut default) = self.default { - Either::Right(default.call(req)) - } else { - Either::Left(ok(req.error_response(e))) - } - } -} - -impl Service for FilesService { - type Request = ServiceRequest; - type Response = ServiceResponse; - type Error = Error; - #[allow(clippy::type_complexity)] - type Future = Either< - Ready>, - LocalBoxFuture<'static, Result>, - >; - - fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, req: ServiceRequest) -> Self::Future { - let is_method_valid = if let Some(guard) = &self.guards { - // execute user defined guards - (**guard).check(req.head()) - } else { - // default behavior - matches!(*req.method(), Method::HEAD | Method::GET) - }; - - if !is_method_valid { - return Either::Left(ok(req.into_response( - actix_web::HttpResponse::MethodNotAllowed() - .header(header::CONTENT_TYPE, "text/plain") - .body("Request did not meet this resource's requirements."), - ))); - } - - let real_path = match PathBufWrp::get_pathbuf(req.match_info().path()) { - Ok(item) => item, - Err(e) => return Either::Left(ok(req.error_response(e))), - }; - - // full file path - let path = match self.directory.join(&real_path.0).canonicalize() { - Ok(path) => path, - Err(e) => return self.handle_err(e, req), - }; - - if path.is_dir() { - if let Some(ref redir_index) = self.index { - if self.redirect_to_slash && !req.path().ends_with('/') { - let redirect_to = format!("{}/", req.path()); - return Either::Left(ok(req.into_response( - HttpResponse::Found() - .header(header::LOCATION, redirect_to) - .body("") - .into_body(), - ))); - } - - let path = path.join(redir_index); - - match NamedFile::open(path) { - Ok(mut named_file) => { - if let Some(ref mime_override) = self.mime_override { - let new_disposition = - mime_override(&named_file.content_type.type_()); - named_file.content_disposition.disposition = new_disposition; - } - - named_file.flags = self.file_flags; - let (req, _) = req.into_parts(); - Either::Left(ok(match named_file.into_response(&req) { - Ok(item) => ServiceResponse::new(req, item), - Err(e) => ServiceResponse::from_err(e, req), - })) - } - Err(e) => self.handle_err(e, req), - } - } else if self.show_index { - let dir = Directory::new(self.directory.clone(), path); - let (req, _) = req.into_parts(); - let x = (self.renderer)(&dir, &req); - match x { - Ok(resp) => Either::Left(ok(resp)), - Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))), - } - } else { - Either::Left(ok(ServiceResponse::from_err( - FilesError::IsDirectory, - req.into_parts().0, - ))) - } - } else { - match NamedFile::open(path) { - Ok(mut named_file) => { - if let Some(ref mime_override) = self.mime_override { - let new_disposition = - mime_override(&named_file.content_type.type_()); - named_file.content_disposition.disposition = new_disposition; - } - - named_file.flags = self.file_flags; - let (req, _) = req.into_parts(); - match named_file.into_response(&req) { - Ok(item) => { - Either::Left(ok(ServiceResponse::new(req.clone(), item))) - } - Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))), - } - } - Err(e) => self.handle_err(e, req), - } - } - } -} - -#[derive(Debug)] -struct PathBufWrp(PathBuf); - -impl PathBufWrp { - fn get_pathbuf(path: &str) -> Result { - let mut buf = PathBuf::new(); - for segment in path.split('/') { - if segment == ".." { - buf.pop(); - } else if segment.starts_with('.') { - return Err(UriSegmentError::BadStart('.')); - } else if segment.starts_with('*') { - return Err(UriSegmentError::BadStart('*')); - } else if segment.ends_with(':') { - return Err(UriSegmentError::BadEnd(':')); - } else if segment.ends_with('>') { - return Err(UriSegmentError::BadEnd('>')); - } else if segment.ends_with('<') { - return Err(UriSegmentError::BadEnd('<')); - } else if segment.is_empty() { - continue; - } else if cfg!(windows) && segment.contains('\\') { - return Err(UriSegmentError::BadChar('\\')); - } else { - buf.push(segment) - } - } - - Ok(PathBufWrp(buf)) - } -} - -impl FromRequest for PathBufWrp { - type Error = UriSegmentError; - type Future = Ready>; - type Config = (); - - fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { - ready(PathBufWrp::get_pathbuf(req.match_info().path())) - } -} - #[cfg(test)] mod tests { - use std::fs; - use std::iter::FromIterator; - use std::ops::Add; - use std::time::{Duration, SystemTime}; + use std::{ + fs::{self, File}, + ops::Add, + time::{Duration, SystemTime}, + }; + + use actix_service::ServiceFactory; + use actix_web::{ + guard, + http::{ + header::{self, ContentDisposition, DispositionParam, DispositionType}, + Method, StatusCode, + }, + middleware::Compress, + test::{self, TestRequest}, + web, App, HttpResponse, Responder, + }; + use futures_util::future::ok; use super::*; - use actix_web::guard; - use actix_web::http::header::{ - self, ContentDisposition, DispositionParam, DispositionType, - }; - use actix_web::http::{Method, StatusCode}; - use actix_web::middleware::Compress; - use actix_web::test::{self, TestRequest}; - use actix_web::{App, Responder}; #[actix_rt::test] async fn test_file_extension_to_mime() { @@ -1013,7 +446,7 @@ mod tests { // Check file contents let bytes = response.body().await.unwrap(); - let data = Bytes::from(fs::read("tests/test.binary").unwrap()); + let data = web::Bytes::from(fs::read("tests/test.binary").unwrap()); assert_eq!(bytes, data); } @@ -1046,7 +479,7 @@ mod tests { assert_eq!(response.status(), StatusCode::OK); let bytes = test::read_body(response).await; - let data = Bytes::from(fs::read("tests/test space.binary").unwrap()); + let data = web::Bytes::from(fs::read("tests/test space.binary").unwrap()); assert_eq!(bytes, data); } @@ -1224,7 +657,7 @@ mod tests { let resp = test::call_service(&mut st, req).await; assert_eq!(resp.status(), StatusCode::OK); let bytes = test::read_body(resp).await; - assert_eq!(bytes, Bytes::from_static(b"default content")); + assert_eq!(bytes, web::Bytes::from_static(b"default content")); } // #[actix_rt::test] @@ -1340,36 +773,4 @@ mod tests { // let response = srv.execute(request.send()).unwrap(); // assert_eq!(response.status(), StatusCode::OK); // } - - #[actix_rt::test] - async fn test_path_buf() { - assert_eq!( - PathBufWrp::get_pathbuf("/test/.tt").map(|t| t.0), - Err(UriSegmentError::BadStart('.')) - ); - assert_eq!( - PathBufWrp::get_pathbuf("/test/*tt").map(|t| t.0), - Err(UriSegmentError::BadStart('*')) - ); - assert_eq!( - PathBufWrp::get_pathbuf("/test/tt:").map(|t| t.0), - Err(UriSegmentError::BadEnd(':')) - ); - assert_eq!( - PathBufWrp::get_pathbuf("/test/tt<").map(|t| t.0), - Err(UriSegmentError::BadEnd('<')) - ); - assert_eq!( - PathBufWrp::get_pathbuf("/test/tt>").map(|t| t.0), - Err(UriSegmentError::BadEnd('>')) - ); - assert_eq!( - PathBufWrp::get_pathbuf("/seg1/seg2/").unwrap().0, - PathBuf::from_iter(vec!["seg1", "seg2"]) - ); - assert_eq!( - PathBufWrp::get_pathbuf("/seg1/../seg2/").unwrap().0, - PathBuf::from_iter(vec!["seg2"]) - ); - } } diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 12da722d2..3caa4a809 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -7,17 +7,20 @@ use std::time::{SystemTime, UNIX_EPOCH}; #[cfg(unix)] use std::os::unix::fs::MetadataExt; -use bitflags::bitflags; -use mime_guess::from_path; - -use actix_http::body::SizedStream; -use actix_web::dev::BodyEncoding; -use actix_web::http::header::{ - self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue, +use actix_web::{ + dev::{BodyEncoding, SizedStream}, + http::{ + header::{ + self, Charset, ContentDisposition, DispositionParam, DispositionType, + ExtendedValue, + }, + ContentEncoding, StatusCode, + }, + Error, HttpMessage, HttpRequest, HttpResponse, Responder, }; -use actix_web::http::{ContentEncoding, StatusCode}; -use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, Responder}; +use bitflags::bitflags; use futures_util::future::{ready, Ready}; +use mime_guess::from_path; use crate::range::HttpRange; use crate::ChunkedReadFile; @@ -93,8 +96,10 @@ impl NamedFile { mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, _ => DispositionType::Attachment, }; + let mut parameters = vec![DispositionParam::Filename(String::from(filename.as_ref()))]; + if !filename.is_ascii() { parameters.push(DispositionParam::FilenameExt(ExtendedValue { charset: Charset::Ext(String::from("UTF-8")), @@ -102,16 +107,19 @@ impl NamedFile { value: filename.into_owned().into_bytes(), })) } + let cd = ContentDisposition { disposition, parameters, }; + (ct, cd) }; let md = file.metadata()?; let modified = md.modified().ok(); let encoding = None; + Ok(NamedFile { path, file, @@ -242,6 +250,7 @@ impl NamedFile { let dur = mtime .duration_since(UNIX_EPOCH) .expect("modification time must be after epoch"); + header::EntityTag::strong(format!( "{:x}:{:x}:{:x}:{:x}", ino, @@ -256,9 +265,11 @@ impl NamedFile { self.modified.map(|mtime| mtime.into()) } + /// Creates an `HttpResponse` with file as a streaming body. pub fn into_response(self, req: &HttpRequest) -> Result { if self.status_code != StatusCode::OK { let mut resp = HttpResponse::build(self.status_code); + resp.set(header::ContentType(self.content_type.clone())) .if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| { res.header( @@ -266,9 +277,11 @@ impl NamedFile { self.content_disposition.to_string(), ); }); + if let Some(current_encoding) = self.encoding { resp.encoding(current_encoding); } + let reader = ChunkedReadFile { size: self.md.len(), offset: 0, @@ -276,6 +289,7 @@ impl NamedFile { fut: None, counter: 0, }; + return Ok(resp.streaming(reader)); } @@ -284,6 +298,7 @@ impl NamedFile { } else { None }; + let last_modified = if self.flags.contains(Flags::LAST_MD) { self.last_modified() } else { @@ -298,6 +313,7 @@ impl NamedFile { { let t1: SystemTime = m.clone().into(); let t2: SystemTime = since.clone().into(); + match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { (Ok(t1), Ok(t2)) => t1 > t2, _ => false, @@ -309,13 +325,14 @@ impl NamedFile { // check last modified let not_modified = if !none_match(etag.as_ref(), req) { true - } else if req.headers().contains_key(&header::IF_NONE_MATCH) { + } else if req.headers().contains_key(header::IF_NONE_MATCH) { false } else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) = (last_modified, req.get_header()) { let t1: SystemTime = m.clone().into(); let t2: SystemTime = since.clone().into(); + match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { (Ok(t1), Ok(t2)) => t1 <= t2, _ => false, @@ -332,6 +349,7 @@ impl NamedFile { self.content_disposition.to_string(), ); }); + // default compressing if let Some(current_encoding) = self.encoding { resp.encoding(current_encoding); @@ -350,11 +368,12 @@ impl NamedFile { let mut offset = 0; // check for range header - if let Some(ranges) = req.headers().get(&header::RANGE) { - if let Ok(rangesheader) = ranges.to_str() { - if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) { - length = rangesvec[0].length; - offset = rangesvec[0].start; + if let Some(ranges) = req.headers().get(header::RANGE) { + if let Ok(ranges_header) = ranges.to_str() { + if let Ok(ranges) = HttpRange::parse(ranges_header, length) { + length = ranges[0].length; + offset = ranges[0].start; + resp.encoding(ContentEncoding::Identity); resp.header( header::CONTENT_RANGE, @@ -414,6 +433,7 @@ impl DerefMut for NamedFile { fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { match req.get_header::() { None | Some(header::IfMatch::Any) => true, + Some(header::IfMatch::Items(ref items)) => { if let Some(some_etag) = etag { for item in items { @@ -422,6 +442,7 @@ fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { } } } + false } } @@ -431,6 +452,7 @@ fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { match req.get_header::() { Some(header::IfNoneMatch::Any) => false, + Some(header::IfNoneMatch::Items(ref items)) => { if let Some(some_etag) = etag { for item in items { @@ -439,8 +461,10 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { } } } + true } + None => true, } } diff --git a/actix-files/src/path_buf.rs b/actix-files/src/path_buf.rs new file mode 100644 index 000000000..2f3ae84d4 --- /dev/null +++ b/actix-files/src/path_buf.rs @@ -0,0 +1,99 @@ +use std::{ + path::{Path, PathBuf}, + str::FromStr, +}; + +use actix_web::{dev::Payload, FromRequest, HttpRequest}; +use futures_util::future::{ready, Ready}; + +use crate::error::UriSegmentError; + +#[derive(Debug)] +pub(crate) struct PathBufWrap(PathBuf); + +impl FromStr for PathBufWrap { + type Err = UriSegmentError; + + fn from_str(path: &str) -> Result { + let mut buf = PathBuf::new(); + + for segment in path.split('/') { + if segment == ".." { + buf.pop(); + } else if segment.starts_with('.') { + return Err(UriSegmentError::BadStart('.')); + } else if segment.starts_with('*') { + return Err(UriSegmentError::BadStart('*')); + } else if segment.ends_with(':') { + return Err(UriSegmentError::BadEnd(':')); + } else if segment.ends_with('>') { + return Err(UriSegmentError::BadEnd('>')); + } else if segment.ends_with('<') { + return Err(UriSegmentError::BadEnd('<')); + } else if segment.is_empty() { + continue; + } else if cfg!(windows) && segment.contains('\\') { + return Err(UriSegmentError::BadChar('\\')); + } else { + buf.push(segment) + } + } + + Ok(PathBufWrap(buf)) + } +} + +impl AsRef for PathBufWrap { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +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()) + } +} + +#[cfg(test)] +mod tests { + use std::iter::FromIterator; + + use super::*; + + #[test] + fn test_path_buf() { + assert_eq!( + PathBufWrap::from_str("/test/.tt").map(|t| t.0), + Err(UriSegmentError::BadStart('.')) + ); + assert_eq!( + PathBufWrap::from_str("/test/*tt").map(|t| t.0), + Err(UriSegmentError::BadStart('*')) + ); + assert_eq!( + PathBufWrap::from_str("/test/tt:").map(|t| t.0), + Err(UriSegmentError::BadEnd(':')) + ); + assert_eq!( + PathBufWrap::from_str("/test/tt<").map(|t| t.0), + Err(UriSegmentError::BadEnd('<')) + ); + assert_eq!( + PathBufWrap::from_str("/test/tt>").map(|t| t.0), + Err(UriSegmentError::BadEnd('>')) + ); + assert_eq!( + PathBufWrap::from_str("/seg1/seg2/").unwrap().0, + PathBuf::from_iter(vec!["seg1", "seg2"]) + ); + assert_eq!( + PathBufWrap::from_str("/seg1/../seg2/").unwrap().0, + PathBuf::from_iter(vec!["seg2"]) + ); + } +} diff --git a/actix-files/src/range.rs b/actix-files/src/range.rs index 47673b0b0..e891ca7ec 100644 --- a/actix-files/src/range.rs +++ b/actix-files/src/range.rs @@ -1,11 +1,14 @@ /// HTTP Range header representation. #[derive(Debug, Clone, Copy)] pub struct HttpRange { + /// Start of range. pub start: u64, + + /// Length of range. pub length: u64, } -static PREFIX: &str = "bytes="; +const PREFIX: &str = "bytes="; const PREFIX_LEN: usize = 6; impl HttpRange { diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs new file mode 100644 index 000000000..cbf4c2d3b --- /dev/null +++ b/actix-files/src/service.rs @@ -0,0 +1,167 @@ +use std::{ + fmt, io, + path::PathBuf, + rc::Rc, + task::{Context, Poll}, +}; + +use actix_service::Service; +use actix_web::{ + dev::{ServiceRequest, ServiceResponse}, + error::Error, + guard::Guard, + http::{header, Method}, + HttpResponse, +}; +use futures_util::future::{ok, Either, LocalBoxFuture, Ready}; + +use crate::{ + named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, + NamedFile, PathBufWrap, +}; + +/// Assembled file serving service. +pub struct FilesService { + pub(crate) directory: PathBuf, + pub(crate) index: Option, + pub(crate) show_index: bool, + pub(crate) redirect_to_slash: bool, + pub(crate) default: Option, + pub(crate) renderer: Rc, + pub(crate) mime_override: Option>, + pub(crate) file_flags: named::Flags, + pub(crate) guards: Option>, +} + +type FilesServiceFuture = Either< + Ready>, + LocalBoxFuture<'static, Result>, +>; + +impl FilesService { + fn handle_err(&mut self, e: io::Error, req: ServiceRequest) -> FilesServiceFuture { + log::debug!("Failed to handle {}: {}", req.path(), e); + + if let Some(ref mut default) = self.default { + Either::Right(default.call(req)) + } else { + Either::Left(ok(req.error_response(e))) + } + } +} + +impl fmt::Debug for FilesService { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("FilesService") + } +} + +impl Service for FilesService { + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = Error; + type Future = FilesServiceFuture; + + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: ServiceRequest) -> Self::Future { + let is_method_valid = if let Some(guard) = &self.guards { + // execute user defined guards + (**guard).check(req.head()) + } else { + // default behavior + matches!(*req.method(), Method::HEAD | Method::GET) + }; + + if !is_method_valid { + return Either::Left(ok(req.into_response( + actix_web::HttpResponse::MethodNotAllowed() + .header(header::CONTENT_TYPE, "text/plain") + .body("Request did not meet this resource's requirements."), + ))); + } + + let real_path: PathBufWrap = match req.match_info().path().parse() { + Ok(item) => item, + Err(e) => return Either::Left(ok(req.error_response(e))), + }; + + // full file path + let path = match self.directory.join(&real_path).canonicalize() { + Ok(path) => path, + Err(e) => return self.handle_err(e, req), + }; + + if path.is_dir() { + if let Some(ref redir_index) = self.index { + if self.redirect_to_slash && !req.path().ends_with('/') { + let redirect_to = format!("{}/", req.path()); + + return Either::Left(ok(req.into_response( + HttpResponse::Found() + .header(header::LOCATION, redirect_to) + .body("") + .into_body(), + ))); + } + + let path = path.join(redir_index); + + match NamedFile::open(path) { + Ok(mut named_file) => { + if let Some(ref mime_override) = self.mime_override { + let new_disposition = + mime_override(&named_file.content_type.type_()); + named_file.content_disposition.disposition = new_disposition; + } + named_file.flags = self.file_flags; + + let (req, _) = req.into_parts(); + Either::Left(ok(match named_file.into_response(&req) { + Ok(item) => ServiceResponse::new(req, item), + Err(e) => ServiceResponse::from_err(e, req), + })) + } + Err(e) => self.handle_err(e, req), + } + } else if self.show_index { + let dir = Directory::new(self.directory.clone(), path); + + let (req, _) = req.into_parts(); + let x = (self.renderer)(&dir, &req); + + match x { + Ok(resp) => Either::Left(ok(resp)), + Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))), + } + } else { + Either::Left(ok(ServiceResponse::from_err( + FilesError::IsDirectory, + req.into_parts().0, + ))) + } + } else { + match NamedFile::open(path) { + Ok(mut named_file) => { + if let Some(ref mime_override) = self.mime_override { + let new_disposition = + mime_override(&named_file.content_type.type_()); + named_file.content_disposition.disposition = new_disposition; + } + named_file.flags = self.file_flags; + + let (req, _) = req.into_parts(); + match named_file.into_response(&req) { + Ok(item) => { + Either::Left(ok(ServiceResponse::new(req.clone(), item))) + } + Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))), + } + } + Err(e) => self.handle_err(e, req), + } + } + } +} From 162121bf8d51d497565961d73dde22ba1c36f3a4 Mon Sep 17 00:00:00 2001 From: Arniu Tseng Date: Wed, 23 Sep 2020 05:42:51 +0800 Subject: [PATCH 71/78] Unify route macros (#1705) --- actix-web-codegen/src/lib.rs | 230 +++++++----------- actix-web-codegen/src/route.rs | 159 ++++++------ .../route-duplicate-method-fail.stderr | 2 +- .../route-unexpected-method-fail.stderr | 2 +- .../tests/trybuild/simple-fail.rs | 5 + .../tests/trybuild/simple-fail.stderr | 6 + 6 files changed, 182 insertions(+), 222 deletions(-) diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 7ae6a26b1..62a1cc5fa 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -1,161 +1,103 @@ #![recursion_limit = "512"] -//! Helper and convenience macros for Actix-web. -//! -//! ## Runtime Setup -//! -//! - [main](attr.main.html) -//! -//! ## Resource Macros: -//! -//! - [get](attr.get.html) -//! - [post](attr.post.html) -//! - [put](attr.put.html) -//! - [delete](attr.delete.html) -//! - [head](attr.head.html) -//! - [connect](attr.connect.html) -//! - [options](attr.options.html) -//! - [trace](attr.trace.html) -//! - [patch](attr.patch.html) -//! -//! ### Attributes: -//! -//! - `"path"` - Raw literal string with path for which to register handle. Mandatory. -//! - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard` -//! - `wrap="Middleware"` - Registers a resource middleware. -//! -//! ### Notes -//! -//! Function name can be specified as any expression that is going to be accessible to the generate -//! code (e.g `my_guard` or `my_module::my_guard`) -//! -//! ### Example: -//! -//! ```rust -//! use actix_web::HttpResponse; -//! use actix_web_codegen::get; -//! -//! #[get("/test")] -//! async fn async_test() -> Result { -//! Ok(HttpResponse::Ok().finish()) -//! } -//! ``` - extern crate proc_macro; -mod route; - use proc_macro::TokenStream; -/// Creates route handler with `GET` method guard. -/// -/// Syntax: `#[get("path" [, attributes])]` -/// -/// ## Attributes: -/// -/// - `"path"` - Raw literal string with path for which to register handler. Mandatory. -/// - `guard = "function_name"` - Registers function as guard using `actix_web::guard::fn_guard` -/// - `wrap = "Middleware"` - Registers a resource middleware. -#[proc_macro_attribute] -pub fn get(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Get) -} - -/// Creates route handler with `POST` method guard. -/// -/// Syntax: `#[post("path" [, attributes])]` -/// -/// Attributes are the same as in [get](attr.get.html) -#[proc_macro_attribute] -pub fn post(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Post) -} - -/// Creates route handler with `PUT` method guard. -/// -/// Syntax: `#[put("path" [, attributes])]` -/// -/// Attributes are the same as in [get](attr.get.html) -#[proc_macro_attribute] -pub fn put(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Put) -} - -/// Creates route handler with `DELETE` method guard. -/// -/// Syntax: `#[delete("path" [, attributes])]` -/// -/// Attributes are the same as in [get](attr.get.html). -#[proc_macro_attribute] -pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Delete) -} - -/// Creates route handler with `HEAD` method guard. -/// -/// Syntax: `#[head("path" [, attributes])]` -/// -/// Attributes are the same as in [get](attr.get.html). -#[proc_macro_attribute] -pub fn head(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Head) -} - -/// Creates route handler with `CONNECT` method guard. -/// -/// Syntax: `#[connect("path" [, attributes])]` -/// -/// Attributes are the same as in [get](attr.get.html). -#[proc_macro_attribute] -pub fn connect(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Connect) -} - -/// Creates route handler with `OPTIONS` method guard. -/// -/// Syntax: `#[options("path" [, attributes])]` -/// -/// Attributes are the same as in [get](attr.get.html). -#[proc_macro_attribute] -pub fn options(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Options) -} - -/// Creates route handler with `TRACE` method guard. -/// -/// Syntax: `#[trace("path" [, attributes])]` -/// -/// Attributes are the same as in [get](attr.get.html). -#[proc_macro_attribute] -pub fn trace(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Trace) -} - -/// Creates route handler with `PATCH` method guard. -/// -/// Syntax: `#[patch("path" [, attributes])]` -/// -/// Attributes are the same as in [get](attr.get.html). -#[proc_macro_attribute] -pub fn patch(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Patch) -} +mod route; /// Creates resource handler, allowing multiple HTTP method guards. /// -/// Syntax: `#[route("path"[, attributes])]` +/// ## Syntax +/// ```text +/// #[route("path", method="HTTP_METHOD"[, attributes])] +/// ``` /// -/// Example: `#[route("/", method="GET", method="HEAD")]` -/// -/// ## Attributes -/// -/// - `"path"` - Raw literal string with path for which to register handler. Mandatory. -/// - `method="HTTP_METHOD"` - Registers HTTP method to provide guard for. +/// ### Attributes +/// - `"path"` - Raw literal string with path for which to register handler. +/// - `method="HTTP_METHOD"` - Registers HTTP method to provide guard for. Upper-case string, "GET", "POST" for example. /// - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard` /// - `wrap="Middleware"` - Registers a resource middleware. +/// +/// ### Notes +/// Function name can be specified as any expression that is going to be accessible to the generate +/// code, e.g `my_guard` or `my_module::my_guard`. +/// +/// ## Example +/// +/// ```rust +/// use actix_web::HttpResponse; +/// use actix_web_codegen::route; +/// +/// #[route("/", method="GET", method="HEAD")] +/// async fn example() -> HttpResponse { +/// HttpResponse::Ok().finish() +/// } +/// ``` #[proc_macro_attribute] pub fn route(args: TokenStream, input: TokenStream) -> TokenStream { - route::generate(args, input, route::GuardType::Multi) + route::with_method(None, args, input) +} + +macro_rules! doc_comment { + ($x:expr; $($tt:tt)*) => { + #[doc = $x] + $($tt)* + }; +} + +macro_rules! method_macro { + ( + $($variant:ident, $method:ident,)+ + ) => { + $(doc_comment! { +concat!(" +Creates route handler with `actix_web::guard::", stringify!($variant), "`. + +## Syntax +```text +#[", stringify!($method), r#"("path"[, attributes])] +``` + +### Attributes +- `"path"` - Raw literal string with path for which to register handler. +- `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`. +- `wrap="Middleware"` - Registers a resource middleware. + +### Notes +Function name can be specified as any expression that is going to be accessible to the generate +code, e.g `my_guard` or `my_module::my_guard`. + +## Example + +```rust +use actix_web::HttpResponse; +use actix_web_codegen::"#, stringify!($method), "; + +#[", stringify!($method), r#"("/")] +async fn example() -> HttpResponse { + HttpResponse::Ok().finish() +} +``` +"#); + #[proc_macro_attribute] + pub fn $method(args: TokenStream, input: TokenStream) -> TokenStream { + route::with_method(Some(route::MethodType::$variant), args, input) + } + })+ + }; +} + +method_macro! { + Get, get, + Post, post, + Put, put, + Delete, delete, + Head, head, + Connect, connect, + Options, options, + Trace, trace, + Patch, patch, } /// Marks async main function as the actix system entry-point. diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index 394ced212..ddbd42454 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -20,63 +20,59 @@ impl ToTokens for ResourceType { } } -#[derive(Debug, PartialEq, Eq, Hash)] -pub enum GuardType { - Get, - Post, - Put, - Delete, - Head, - Connect, - Options, - Trace, - Patch, - Multi, -} - -impl GuardType { - fn as_str(&self) -> &'static str { - match self { - GuardType::Get => "Get", - GuardType::Post => "Post", - GuardType::Put => "Put", - GuardType::Delete => "Delete", - GuardType::Head => "Head", - GuardType::Connect => "Connect", - GuardType::Options => "Options", - GuardType::Trace => "Trace", - GuardType::Patch => "Patch", - GuardType::Multi => "Multi", +macro_rules! method_type { + ( + $($variant:ident, $upper:ident,)+ + ) => { + #[derive(Debug, PartialEq, Eq, Hash)] + pub enum MethodType { + $( + $variant, + )+ } - } + + impl MethodType { + fn as_str(&self) -> &'static str { + match self { + $(Self::$variant => stringify!($variant),)+ + } + } + + fn parse(method: &str) -> Result { + match method { + $(stringify!($upper) => Ok(Self::$variant),)+ + _ => Err(format!("Unexpected HTTP method: `{}`", method)), + } + } + } + }; } -impl ToTokens for GuardType { +method_type! { + Get, GET, + Post, POST, + Put, PUT, + Delete, DELETE, + Head, HEAD, + Connect, CONNECT, + Options, OPTIONS, + Trace, TRACE, + Patch, PATCH, +} + +impl ToTokens for MethodType { fn to_tokens(&self, stream: &mut TokenStream2) { let ident = Ident::new(self.as_str(), Span::call_site()); stream.append(ident); } } -impl TryFrom<&syn::LitStr> for GuardType { +impl TryFrom<&syn::LitStr> for MethodType { type Error = syn::Error; fn try_from(value: &syn::LitStr) -> Result { - match value.value().as_str() { - "CONNECT" => Ok(GuardType::Connect), - "DELETE" => Ok(GuardType::Delete), - "GET" => Ok(GuardType::Get), - "HEAD" => Ok(GuardType::Head), - "OPTIONS" => Ok(GuardType::Options), - "PATCH" => Ok(GuardType::Patch), - "POST" => Ok(GuardType::Post), - "PUT" => Ok(GuardType::Put), - "TRACE" => Ok(GuardType::Trace), - _ => Err(syn::Error::new_spanned( - value, - &format!("Unexpected HTTP Method: `{}`", value.value()), - )), - } + Self::parse(value.value().as_str()) + .map_err(|message| syn::Error::new_spanned(value, message)) } } @@ -84,15 +80,21 @@ struct Args { path: syn::LitStr, guards: Vec, wrappers: Vec, - methods: HashSet, + methods: HashSet, } impl Args { - fn new(args: AttributeArgs) -> syn::Result { + fn new(args: AttributeArgs, method: Option) -> syn::Result { let mut path = None; let mut guards = Vec::new(); let mut wrappers = Vec::new(); let mut methods = HashSet::new(); + + let is_route_macro = method.is_none(); + if let Some(method) = method { + methods.insert(method); + } + for arg in args { match arg { NestedMeta::Lit(syn::Lit::Str(lit)) => match path { @@ -126,13 +128,18 @@ impl Args { )); } } else if nv.path.is_ident("method") { - if let syn::Lit::Str(ref lit) = nv.lit { - let guard = GuardType::try_from(lit)?; - if !methods.insert(guard) { + if !is_route_macro { + return Err(syn::Error::new_spanned( + &nv, + "HTTP method forbidden here. To handle multiple methods, use `route` instead", + )); + } else if let syn::Lit::Str(ref lit) = nv.lit { + let method = MethodType::try_from(lit)?; + if !methods.insert(method) { return Err(syn::Error::new_spanned( &nv.lit, &format!( - "HTTP Method defined more than once: `{}`", + "HTTP method defined more than once: `{}`", lit.value() ), )); @@ -169,7 +176,6 @@ pub struct Route { args: Args, ast: syn::ItemFn, resource_type: ResourceType, - guard: GuardType, } fn guess_resource_type(typ: &syn::Type) -> ResourceType { @@ -198,23 +204,25 @@ impl Route { pub fn new( args: AttributeArgs, input: TokenStream, - guard: GuardType, + method: Option, ) -> syn::Result { if args.is_empty() { return Err(syn::Error::new( Span::call_site(), format!( - r#"invalid server definition, expected #[{}("")]"#, - guard.as_str().to_ascii_lowercase() + r#"invalid service definition, expected #[{}("")]"#, + method + .map(|it| it.as_str()) + .unwrap_or("route") + .to_ascii_lowercase() ), )); } let ast: syn::ItemFn = syn::parse(input)?; let name = ast.sig.ident.clone(); - let args = Args::new(args)?; - - if guard == GuardType::Multi && args.methods.is_empty() { + let args = Args::new(args, method)?; + if args.methods.is_empty() { return Err(syn::Error::new( Span::call_site(), "The #[route(..)] macro requires at least one `method` attribute", @@ -240,7 +248,6 @@ impl Route { args, ast, resource_type, - guard, }) } } @@ -249,7 +256,6 @@ impl ToTokens for Route { fn to_tokens(&self, output: &mut TokenStream2) { let Self { name, - guard, ast, args: Args { @@ -261,21 +267,22 @@ impl ToTokens for Route { resource_type, } = self; let resource_name = name.to_string(); - let mut methods = methods.iter(); - - let method_guards = if *guard == GuardType::Multi { + let method_guards = { + let mut others = methods.iter(); // unwrapping since length is checked to be at least one - let first = methods.next().unwrap(); + let first = others.next().unwrap(); - quote! { - .guard( - actix_web::guard::Any(actix_web::guard::#first()) - #(.or(actix_web::guard::#methods()))* - ) - } - } else { - quote! { - .guard(actix_web::guard::#guard()) + if methods.len() > 1 { + quote! { + .guard( + actix_web::guard::Any(actix_web::guard::#first()) + #(.or(actix_web::guard::#others()))* + ) + } + } else { + quote! { + .guard(actix_web::guard::#first()) + } } }; @@ -302,13 +309,13 @@ impl ToTokens for Route { } } -pub(crate) fn generate( +pub(crate) fn with_method( + method: Option, args: TokenStream, input: TokenStream, - guard: GuardType, ) -> TokenStream { let args = parse_macro_input!(args as syn::AttributeArgs); - match Route::new(args, input, guard) { + match Route::new(args, input, method) { Ok(route) => route.into_token_stream().into(), Err(err) => err.to_compile_error().into(), } 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 8bf857c4d..613054de5 100644 --- a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr @@ -1,4 +1,4 @@ -error: HTTP Method defined more than once: `GET` +error: HTTP method defined more than once: `GET` --> $DIR/route-duplicate-method-fail.rs:3:35 | 3 | #[route("/", method="GET", method="GET")] 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 3fe49f774..fe17fdf12 100644 --- a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr @@ -1,4 +1,4 @@ -error: Unexpected HTTP Method: `UNEXPECTED` +error: Unexpected HTTP method: `UNEXPECTED` --> $DIR/route-unexpected-method-fail.rs:3:21 | 3 | #[route("/", method="UNEXPECTED")] diff --git a/actix-web-codegen/tests/trybuild/simple-fail.rs b/actix-web-codegen/tests/trybuild/simple-fail.rs index 140497687..368cff046 100644 --- a/actix-web-codegen/tests/trybuild/simple-fail.rs +++ b/actix-web-codegen/tests/trybuild/simple-fail.rs @@ -22,4 +22,9 @@ async fn four() -> impl Responder { HttpResponse::Ok() } +#[delete("/five", method="GET")] +async fn five() -> impl Responder { + HttpResponse::Ok() +} + fn main() {} diff --git a/actix-web-codegen/tests/trybuild/simple-fail.stderr b/actix-web-codegen/tests/trybuild/simple-fail.stderr index 12c32c00d..cffc81ff8 100644 --- a/actix-web-codegen/tests/trybuild/simple-fail.stderr +++ b/actix-web-codegen/tests/trybuild/simple-fail.stderr @@ -21,3 +21,9 @@ error: Multiple paths specified! Should be only one! | 20 | #[delete("/four", "/five")] | ^^^^^^^ + +error: HTTP method forbidden here. To handle multiple methods, use `route` instead + --> $DIR/simple-fail.rs:25:19 + | +25 | #[delete("/five", method="GET")] + | ^^^^^^^^^^^^ From c53e9468bc1d977afb3514fe7f8239ed9deb2e68 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 24 Sep 2020 23:54:01 +0100 Subject: [PATCH 72/78] prepare codegen 0.4.0 release (#1702) --- actix-web-codegen/CHANGES.md | 3 + actix-web-codegen/Cargo.toml | 4 +- actix-web-codegen/src/lib.rs | 97 +++++++++++++++---- .../trybuild/route-duplicate-method-fail.rs | 8 +- .../route-duplicate-method-fail.stderr | 4 +- .../route-missing-method-fail-msrv.rs | 16 +-- .../route-missing-method-fail-msrv.stderr | 4 +- .../trybuild/route-missing-method-fail.rs | 8 +- .../trybuild/route-missing-method-fail.stderr | 4 +- actix-web-codegen/tests/trybuild/route-ok.rs | 8 +- .../trybuild/route-unexpected-method-fail.rs | 8 +- .../route-unexpected-method-fail.stderr | 4 +- .../tests/trybuild/simple-fail.rs | 22 ++--- actix-web-codegen/tests/trybuild/simple.rs | 3 +- 14 files changed, 124 insertions(+), 69 deletions(-) mode change 100644 => 120000 actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.rs diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index 793864d43..ad1a22b88 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2020-xx-xx + + +## 0.4.0 - 2020-09-20 * Added compile success and failure testing. [#1677] * Add `route` macro for supporting multiple HTTP methods guards. diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index da37b8de6..fd99a8376 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-codegen" -version = "0.3.0" +version = "0.4.0" description = "Actix web proc macros" readme = "README.md" homepage = "https://actix.rs" @@ -19,7 +19,7 @@ syn = { version = "1", features = ["full", "parsing"] } proc-macro2 = "1" [dev-dependencies] -actix-rt = "1.0.0" +actix-rt = "1.1.1" actix-web = "3.0.0" futures-util = { version = "0.3.5", default-features = false } trybuild = "1" diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 62a1cc5fa..af2bc7f18 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -1,6 +1,64 @@ -#![recursion_limit = "512"] +//! Macros for reducing boilerplate code in Actix Web applications. +//! +//! ## Actix Web Re-exports +//! Actix Web re-exports a version of this crate in it's entirety so you usually don't have to +//! specify a dependency on this crate explicitly. Sometimes, however, updates are made to this +//! crate before the actix-web dependency is updated. Therefore, code examples here will show +//! explicit imports. Check the latest [actix-web attributes docs] to see which macros +//! are re-exported. +//! +//! # Runtime Setup +//! Used for setting up the actix async runtime. See [main] macro docs. +//! +//! ```rust +//! #[actix_web_codegen::main] // or `#[actix_web::main]` in Actix Web apps +//! async fn main() { +//! async { println!("Hello world"); }.await +//! } +//! ``` +//! +//! # Single Method Handler +//! There is a macro to set up a handler for each of the most common HTTP methods that also define +//! additional guards and route-specific middleware. +//! +//! See docs for: [GET], [POST], [PATCH], [PUT], [DELETE], [HEAD], [CONNECT], [OPTIONS], [TRACE] +//! +//! ```rust +//! # use actix_web::HttpResponse; +//! # use actix_web_codegen::get; +//! #[get("/test")] +//! async fn get_handler() -> HttpResponse { +//! HttpResponse::Ok().finish() +//! } +//! ``` +//! +//! # Multiple Method Handlers +//! Similar to the single method handler macro but takes one or more arguments for the HTTP methods +//! it should respond to. See [route] macro docs. +//! +//! ```rust +//! # use actix_web::HttpResponse; +//! # use actix_web_codegen::route; +//! #[route("/test", method="GET", method="HEAD")] +//! async fn get_and_head_handler() -> HttpResponse { +//! HttpResponse::Ok().finish() +//! } +//! ``` +//! +//! [actix-web attributes docs]: https://docs.rs/actix-web/*/actix_web/#attributes +//! [main]: attr.main.html +//! [route]: attr.route.html +//! [GET]: attr.get.html +//! [POST]: attr.post.html +//! [PUT]: attr.put.html +//! [DELETE]: attr.delete.html +//! [HEAD]: attr.head.html +//! [CONNECT]: attr.connect.html +//! [OPTIONS]: attr.options.html +//! [TRACE]: attr.trace.html +//! [PATCH]: attr.patch.html -extern crate proc_macro; +#![recursion_limit = "512"] use proc_macro::TokenStream; @@ -8,28 +66,27 @@ mod route; /// Creates resource handler, allowing multiple HTTP method guards. /// -/// ## Syntax +/// # Syntax /// ```text /// #[route("path", method="HTTP_METHOD"[, attributes])] /// ``` /// -/// ### Attributes +/// # Attributes /// - `"path"` - Raw literal string with path for which to register handler. /// - `method="HTTP_METHOD"` - Registers HTTP method to provide guard for. Upper-case string, "GET", "POST" for example. /// - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard` /// - `wrap="Middleware"` - Registers a resource middleware. /// -/// ### Notes +/// # Notes /// Function name can be specified as any expression that is going to be accessible to the generate /// code, e.g `my_guard` or `my_module::my_guard`. /// -/// ## Example +/// # Example /// /// ```rust -/// use actix_web::HttpResponse; -/// use actix_web_codegen::route; -/// -/// #[route("/", method="GET", method="HEAD")] +/// # use actix_web::HttpResponse; +/// # use actix_web_codegen::route; +/// #[route("/test", method="GET", method="HEAD")] /// async fn example() -> HttpResponse { /// HttpResponse::Ok().finish() /// } @@ -54,26 +111,25 @@ macro_rules! method_macro { concat!(" Creates route handler with `actix_web::guard::", stringify!($variant), "`. -## Syntax +# Syntax ```text #[", stringify!($method), r#"("path"[, attributes])] ``` -### Attributes +# Attributes - `"path"` - Raw literal string with path for which to register handler. - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`. - `wrap="Middleware"` - Registers a resource middleware. -### Notes +# Notes Function name can be specified as any expression that is going to be accessible to the generate code, e.g `my_guard` or `my_module::my_guard`. -## Example +# Example ```rust -use actix_web::HttpResponse; -use actix_web_codegen::"#, stringify!($method), "; - +# use actix_web::HttpResponse; +# use actix_web_codegen::"#, stringify!($method), "; #[", stringify!($method), r#"("/")] async fn example() -> HttpResponse { HttpResponse::Ok().finish() @@ -102,16 +158,17 @@ method_macro! { /// Marks async main function as the actix system entry-point. /// -/// ## Usage +/// # Actix Web Re-export +/// This macro can be applied with `#[actix_web::main]` when used in Actix Web applications. /// +/// # Usage /// ```rust -/// #[actix_web::main] +/// #[actix_web_codegen::main] /// async fn main() { /// async { println!("Hello world"); }.await /// } /// ``` #[proc_macro_attribute] -#[cfg(not(test))] // Work around for rust-lang/rust#62127 pub fn main(_: TokenStream, item: TokenStream) -> TokenStream { use quote::quote; diff --git a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.rs b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.rs index 9ce980251..9a38050f7 100644 --- a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.rs +++ b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.rs @@ -1,12 +1,14 @@ -use actix_web::*; +use actix_web_codegen::*; #[route("/", method="GET", method="GET")] -async fn index() -> impl Responder { - HttpResponse::Ok() +async fn index() -> String { + "Hello World!".to_owned() } #[actix_web::main] async fn main() { + use actix_web::{App, test}; + let srv = test::start(|| App::new().service(index)); let request = srv.get("/"); 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 613054de5..f3eda68af 100644 --- a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr @@ -5,7 +5,7 @@ error: HTTP method defined more than once: `GET` | ^^^^^ error[E0425]: cannot find value `index` in this scope - --> $DIR/route-duplicate-method-fail.rs:10:49 + --> $DIR/route-duplicate-method-fail.rs:12:49 | -10 | let srv = test::start(|| App::new().service(index)); +12 | let srv = test::start(|| App::new().service(index)); | ^^^^^ not found in this scope diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.rs b/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.rs deleted file mode 100644 index 5c30b57ce..000000000 --- a/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.rs +++ /dev/null @@ -1,15 +0,0 @@ -use actix_web::*; - -#[route("/")] -async fn index() -> impl Responder { - HttpResponse::Ok() -} - -#[actix_web::main] -async fn main() { - let srv = test::start(|| App::new().service(index)); - - let request = srv.get("/"); - let response = request.send().await.unwrap(); - assert!(response.status().is_success()); -} diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.rs b/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.rs new file mode 120000 index 000000000..70a5c0e33 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.rs @@ -0,0 +1 @@ +route-missing-method-fail.rs \ No newline at end of file diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.stderr b/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.stderr index f59f6c27e..d3e2b60ae 100644 --- a/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.stderr +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail-msrv.stderr @@ -5,7 +5,7 @@ error: The #[route(..)] macro requires at least one `method` attribute | ^^^^^^^^^^^^^ error[E0425]: cannot find value `index` in this scope - --> $DIR/route-missing-method-fail-msrv.rs:10:49 + --> $DIR/route-missing-method-fail-msrv.rs:12:49 | -10 | let srv = test::start(|| App::new().service(index)); +12 | let srv = test::start(|| App::new().service(index)); | ^^^^^ not found in this scope diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail.rs b/actix-web-codegen/tests/trybuild/route-missing-method-fail.rs index 5c30b57ce..ce87a55a4 100644 --- a/actix-web-codegen/tests/trybuild/route-missing-method-fail.rs +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail.rs @@ -1,12 +1,14 @@ -use actix_web::*; +use actix_web_codegen::*; #[route("/")] -async fn index() -> impl Responder { - HttpResponse::Ok() +async fn index() -> String { + "Hello World!".to_owned() } #[actix_web::main] async fn main() { + use actix_web::{App, test}; + let srv = test::start(|| App::new().service(index)); let request = srv.get("/"); 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 6d35ea600..0518a61ed 100644 --- a/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr @@ -7,7 +7,7 @@ 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 - --> $DIR/route-missing-method-fail.rs:10:49 + --> $DIR/route-missing-method-fail.rs:12:49 | -10 | let srv = test::start(|| App::new().service(index)); +12 | let srv = test::start(|| App::new().service(index)); | ^^^^^ not found in this scope diff --git a/actix-web-codegen/tests/trybuild/route-ok.rs b/actix-web-codegen/tests/trybuild/route-ok.rs index bfac56e12..c4f679604 100644 --- a/actix-web-codegen/tests/trybuild/route-ok.rs +++ b/actix-web-codegen/tests/trybuild/route-ok.rs @@ -1,12 +1,14 @@ -use actix_web::*; +use actix_web_codegen::*; #[route("/", method="GET", method="HEAD")] -async fn index() -> impl Responder { - HttpResponse::Ok() +async fn index() -> String { + "Hello World!".to_owned() } #[actix_web::main] async fn main() { + use actix_web::{App, test}; + let srv = test::start(|| App::new().service(index)); let request = srv.get("/"); diff --git a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs index f4d8d9445..28cd1344c 100644 --- a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs +++ b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs @@ -1,12 +1,14 @@ -use actix_web::*; +use actix_web_codegen::*; #[route("/", method="UNEXPECTED")] -async fn index() -> impl Responder { - HttpResponse::Ok() +async fn index() -> String { + "Hello World!".to_owned() } #[actix_web::main] async fn main() { + use actix_web::{App, test}; + let srv = test::start(|| App::new().service(index)); let request = srv.get("/"); 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 fe17fdf12..9d87f310b 100644 --- a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr @@ -5,7 +5,7 @@ error: Unexpected HTTP method: `UNEXPECTED` | ^^^^^^^^^^^^ error[E0425]: cannot find value `index` in this scope - --> $DIR/route-unexpected-method-fail.rs:10:49 + --> $DIR/route-unexpected-method-fail.rs:12:49 | -10 | let srv = test::start(|| App::new().service(index)); +12 | let srv = test::start(|| App::new().service(index)); | ^^^^^ not found in this scope diff --git a/actix-web-codegen/tests/trybuild/simple-fail.rs b/actix-web-codegen/tests/trybuild/simple-fail.rs index 368cff046..a57fdc16d 100644 --- a/actix-web-codegen/tests/trybuild/simple-fail.rs +++ b/actix-web-codegen/tests/trybuild/simple-fail.rs @@ -1,30 +1,30 @@ -use actix_web::*; +use actix_web_codegen::*; #[get("/one", other)] -async fn one() -> impl Responder { - HttpResponse::Ok() +async fn one() -> String { + "Hello World!".to_owned() } #[post(/two)] -async fn two() -> impl Responder { - HttpResponse::Ok() +async fn two() -> String { + "Hello World!".to_owned() } static PATCH_PATH: &str = "/three"; #[patch(PATCH_PATH)] -async fn three() -> impl Responder { - HttpResponse::Ok() +async fn three() -> String { + "Hello World!".to_owned() } #[delete("/four", "/five")] -async fn four() -> impl Responder { - HttpResponse::Ok() +async fn four() -> String { + "Hello World!".to_owned() } #[delete("/five", method="GET")] -async fn five() -> impl Responder { - HttpResponse::Ok() +async fn five() -> String { + "Hello World!".to_owned() } fn main() {} diff --git a/actix-web-codegen/tests/trybuild/simple.rs b/actix-web-codegen/tests/trybuild/simple.rs index 6b1e67442..761b04905 100644 --- a/actix-web-codegen/tests/trybuild/simple.rs +++ b/actix-web-codegen/tests/trybuild/simple.rs @@ -1,4 +1,5 @@ -use actix_web::*; +use actix_web::{Responder, HttpResponse, App, test}; +use actix_web_codegen::*; #[get("/config")] async fn config() -> impl Responder { From 60e7e52276ee60d46336563a91a8ef72be2ad699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LIU=20An=20=28=E5=8A=89=E5=AE=89=29?= Date: Fri, 25 Sep 2020 19:50:59 +0800 Subject: [PATCH 73/78] Add TrailingSlash::MergeOnly behavior (#1695) Co-authored-by: Rob Ede --- CHANGES.md | 3 +++ MIGRATION.md | 2 +- src/middleware/normalize.rs | 40 ++++++++++++++++++++++++++++++++++++- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 15d3c81ce..608c237b2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2020-xx-xx +### Changed +* Add `TrailingSlash::MergeOnly` behaviour to `NormalizePath`, which allow `NormalizePath` + to keep the trailing slash's existance as it is. [#1695] ## 3.0.2 - 2020-09-15 diff --git a/MIGRATION.md b/MIGRATION.md index 98b22ae4e..5c4650194 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -48,7 +48,7 @@ * `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one. It will need `middleware::normalize::TrailingSlash` when being constructed with `NormalizePath::new(...)`, - or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::default())`. + or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly))`. * `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`. diff --git a/src/middleware/normalize.rs b/src/middleware/normalize.rs index 8452c9083..e0ecd90dc 100644 --- a/src/middleware/normalize.rs +++ b/src/middleware/normalize.rs @@ -17,6 +17,10 @@ pub enum TrailingSlash { /// Always add a trailing slash to the end of the path. /// This will require all routes to end in a trailing slash for them to be accessible. Always, + /// Only merge any present multiple trailing slashes. + /// + /// Note: This option provides the best compatibility with the v2 version of this middlware. + MergeOnly, /// Trim trailing slashes from the end of the path. Trim, } @@ -33,7 +37,8 @@ impl Default for TrailingSlash { /// Performs following: /// /// - Merges multiple slashes into one. -/// - Appends a trailing slash if one is not present, or removes one if present, depending on the supplied `TrailingSlash`. +/// - Appends a trailing slash if one is not present, removes one if present, or keeps trailing +/// slashes as-is, depending on the supplied `TrailingSlash` variant. /// /// ```rust /// use actix_web::{web, http, middleware, App, HttpResponse}; @@ -108,6 +113,7 @@ where // Either adds a string to the end (duplicates will be removed anyways) or trims all slashes from the end let path = match self.trailing_slash_behavior { TrailingSlash::Always => original_path.to_string() + "/", + TrailingSlash::MergeOnly => original_path.to_string(), TrailingSlash::Trim => original_path.trim_end_matches('/').to_string(), }; @@ -237,6 +243,38 @@ mod tests { assert!(res4.status().is_success()); } + #[actix_rt::test] + async fn keep_trailing_slash_unchange() { + let mut app = init_service( + App::new() + .wrap(NormalizePath(TrailingSlash::MergeOnly)) + .service(web::resource("/").to(HttpResponse::Ok)) + .service(web::resource("/v1/something").to(HttpResponse::Ok)) + .service(web::resource("/v1/").to(HttpResponse::Ok)), + ) + .await; + + let tests = vec![ + ("/", true), // root paths should still work + ("/?query=test", true), + ("///", true), + ("/v1/something////", false), + ("/v1/something/", false), + ("//v1//something", true), + ("/v1/", true), + ("/v1", false), + ("/v1////", true), + ("//v1//", true), + ("///v1", false), + ]; + + for (path, success) in tests { + let req = TestRequest::with_uri(path).to_request(); + let res = call_service(&mut app, req).await; + assert_eq!(res.status().is_success(), success); + } + } + #[actix_rt::test] async fn test_in_place_normalization() { let srv = |req: ServiceRequest| { From 37c76a39ab3ab43c46af458ebfd08a0d3d97adf1 Mon Sep 17 00:00:00 2001 From: Matt Gathu Date: Fri, 25 Sep 2020 15:50:37 +0200 Subject: [PATCH 74/78] Fix Multipart consuming payload before header checks (#1704) * Fix Multipart consuming payload before header checks What -- Split up logic in the constructor into two functions: - **from_boundary:** build Multipart from boundary and stream - **from_error:** build Multipart for MultipartError Also we make the `boundary`, `from_boundary`, `from_error` methods public within the crate so that we can use them in the extractor. The extractor is then able to perform header checks and only consume the payload if the checks pass. * Add tests * Add payload consumption test Co-authored-by: Rob Ede --- actix-multipart/CHANGES.md | 1 + actix-multipart/src/extractor.rs | 5 +- actix-multipart/src/server.rs | 81 +++++++++++++++++++++++++------- 3 files changed, 70 insertions(+), 17 deletions(-) diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index b25053025..446ca5ad2 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2020-xx-xx +* Fix multipart consuming payload before header checks #1513 ## 3.0.0 - 2020-09-11 diff --git a/actix-multipart/src/extractor.rs b/actix-multipart/src/extractor.rs index 4e4caee01..6aaa415c4 100644 --- a/actix-multipart/src/extractor.rs +++ b/actix-multipart/src/extractor.rs @@ -36,6 +36,9 @@ impl FromRequest for Multipart { #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { - ok(Multipart::new(req.headers(), payload.take())) + ok(match Multipart::boundary(req.headers()) { + Ok(boundary) => Multipart::from_boundary(boundary, payload.take()), + Err(err) => Multipart::from_error(err), + }) } } diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs index 1507959b8..b9ebf97cc 100644 --- a/actix-multipart/src/server.rs +++ b/actix-multipart/src/server.rs @@ -64,26 +64,13 @@ impl Multipart { S: Stream> + Unpin + 'static, { match Self::boundary(headers) { - Ok(boundary) => Multipart { - error: None, - safety: Safety::new(), - inner: Some(Rc::new(RefCell::new(InnerMultipart { - boundary, - payload: PayloadRef::new(PayloadBuffer::new(Box::new(stream))), - state: InnerState::FirstBoundary, - item: InnerMultipartItem::None, - }))), - }, - Err(err) => Multipart { - error: Some(err), - safety: Safety::new(), - inner: None, - }, + Ok(boundary) => Multipart::from_boundary(boundary, stream), + Err(err) => Multipart::from_error(err), } } /// Extract boundary info from headers. - fn boundary(headers: &HeaderMap) -> Result { + pub(crate) fn boundary(headers: &HeaderMap) -> Result { if let Some(content_type) = headers.get(&header::CONTENT_TYPE) { if let Ok(content_type) = content_type.to_str() { if let Ok(ct) = content_type.parse::() { @@ -102,6 +89,32 @@ impl Multipart { Err(MultipartError::NoContentType) } } + + /// Create multipart instance for given boundary and stream + pub(crate) fn from_boundary(boundary: String, stream: S) -> Multipart + where + S: Stream> + Unpin + 'static, + { + Multipart { + error: None, + safety: Safety::new(), + inner: Some(Rc::new(RefCell::new(InnerMultipart { + boundary, + payload: PayloadRef::new(PayloadBuffer::new(Box::new(stream))), + state: InnerState::FirstBoundary, + item: InnerMultipartItem::None, + }))), + } + } + + /// Create Multipart instance from MultipartError + pub(crate) fn from_error(err: MultipartError) -> Multipart { + Multipart { + error: Some(err), + safety: Safety::new(), + inner: None, + } + } } impl Stream for Multipart { @@ -815,6 +828,8 @@ mod tests { use actix_http::h1::Payload; use actix_utils::mpsc; use actix_web::http::header::{DispositionParam, DispositionType}; + use actix_web::test::TestRequest; + use actix_web::FromRequest; use bytes::Bytes; use futures_util::future::lazy; @@ -1151,4 +1166,38 @@ mod tests { ); assert_eq!(payload.buf.len(), 0); } + + #[actix_rt::test] + async fn test_multipart_from_error() { + let err = MultipartError::NoContentType; + let mut multipart = Multipart::from_error(err); + assert!(multipart.next().await.unwrap().is_err()) + } + + #[actix_rt::test] + async fn test_multipart_from_boundary() { + let (_, payload) = create_stream(); + let (_, headers) = create_simple_request_with_header(); + let boundary = Multipart::boundary(&headers); + assert!(boundary.is_ok()); + let _ = Multipart::from_boundary(boundary.unwrap(), payload); + } + + #[actix_rt::test] + async fn test_multipart_payload_consumption() { + // with sample payload and HttpRequest with no headers + let (_, inner_payload) = Payload::create(false); + let mut payload = actix_web::dev::Payload::from(inner_payload); + let req = TestRequest::default().to_http_request(); + + // multipart should generate an error + let mut mp = Multipart::from_request(&req, &mut payload).await.unwrap(); + assert!(mp.next().await.unwrap().is_err()); + + // and should not consume the payload + match payload { + actix_web::dev::Payload::H1(_) => {} //expected + _ => unreachable!(), + } + } } From b4e02fe29a3399601a03e9fa429c9af0e85d1649 Mon Sep 17 00:00:00 2001 From: Matt Gathu Date: Fri, 25 Sep 2020 18:42:49 +0200 Subject: [PATCH 75/78] Fix cyclic references in ResourceMap (#1708) --- CHANGES.md | 2 ++ src/rmap.rs | 50 ++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 608c237b2..3419ce818 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,9 @@ ### Changed * Add `TrailingSlash::MergeOnly` behaviour to `NormalizePath`, which allow `NormalizePath` to keep the trailing slash's existance as it is. [#1695] +* Fix `ResourceMap` recursive references when printing/debugging. [#1708] +[#1708]: https://github.com/actix/actix-web/pull/1708 ## 3.0.2 - 2020-09-15 ### Fixed diff --git a/src/rmap.rs b/src/rmap.rs index 5e79830ec..05c1f3f15 100644 --- a/src/rmap.rs +++ b/src/rmap.rs @@ -1,5 +1,5 @@ use std::cell::RefCell; -use std::rc::Rc; +use std::rc::{Rc, Weak}; use actix_router::ResourceDef; use fxhash::FxHashMap; @@ -11,7 +11,7 @@ use crate::request::HttpRequest; #[derive(Clone, Debug)] pub struct ResourceMap { root: ResourceDef, - parent: RefCell>>, + parent: RefCell>, named: FxHashMap, patterns: Vec<(ResourceDef, Option>)>, } @@ -20,7 +20,7 @@ impl ResourceMap { pub fn new(root: ResourceDef) -> Self { ResourceMap { root, - parent: RefCell::new(None), + parent: RefCell::new(Weak::new()), named: FxHashMap::default(), patterns: Vec::new(), } @@ -38,7 +38,7 @@ impl ResourceMap { pub(crate) fn finish(&self, current: Rc) { for (_, nested) in &self.patterns { if let Some(ref nested) = nested { - *nested.parent.borrow_mut() = Some(current.clone()); + *nested.parent.borrow_mut() = Rc::downgrade(¤t); nested.finish(nested.clone()); } } @@ -210,7 +210,7 @@ impl ResourceMap { U: Iterator, I: AsRef, { - if let Some(ref parent) = *self.parent.borrow() { + if let Some(ref parent) = self.parent.borrow().upgrade() { parent.fill_root(path, elements)?; } if self.root.resource_path(path, elements) { @@ -230,7 +230,7 @@ impl ResourceMap { U: Iterator, I: AsRef, { - if let Some(ref parent) = *self.parent.borrow() { + 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) { @@ -367,4 +367,42 @@ mod tests { assert_eq!(root.match_name("/user/22/"), None); assert_eq!(root.match_name("/user/22/post/55"), Some("user_post")); } + + #[test] + fn bug_fix_issue_1582_debug_print_exits() { + // 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("")); + user_map.add(&mut ResourceDef::new("/"), None); + user_map.add(&mut ResourceDef::new("/profile"), None); + user_map.add(&mut ResourceDef::new("/article/{id}"), None); + user_map.add(&mut ResourceDef::new("/post/{post_id}"), None); + user_map.add( + &mut ResourceDef::new("/post/{post_id}/comment/{comment_id}"), + None, + ); + + root.add( + &mut ResourceDef::root_prefix("/user/{id}"), + Some(Rc::new(user_map)), + ); + + let root = Rc::new(root); + root.finish(Rc::clone(&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()); + // 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() + ); + + let output = format!("{:?}", root); + assert!(output.starts_with("ResourceMap {")); + assert!(output.ends_with(" }")); + } } From b5812b15f07271850367b085e4933a0989dd84f9 Mon Sep 17 00:00:00 2001 From: PeterUlb Date: Tue, 29 Sep 2020 23:44:12 +0200 Subject: [PATCH 76/78] Remove Sized Bound for web::Data (#1712) --- CHANGES.md | 1 + src/data.rs | 32 ++++++++++++++++++++++++++------ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3419ce818..86e022409 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ * Add `TrailingSlash::MergeOnly` behaviour to `NormalizePath`, which allow `NormalizePath` to keep the trailing slash's existance as it is. [#1695] * Fix `ResourceMap` recursive references when printing/debugging. [#1708] +* Remove bound `std::marker::Sized` from `web::Data` to support storing `Arc` via `web::Data::from` [#1710] [#1708]: https://github.com/actix/actix-web/pull/1708 diff --git a/src/data.rs b/src/data.rs index c50b30328..6405fd901 100644 --- a/src/data.rs +++ b/src/data.rs @@ -66,7 +66,7 @@ pub(crate) type FnDataFactory = /// } /// ``` #[derive(Debug)] -pub struct Data(Arc); +pub struct Data(Arc); impl Data { /// Create new `Data` instance. @@ -89,7 +89,7 @@ impl Data { } } -impl Deref for Data { +impl Deref for Data { type Target = Arc; fn deref(&self) -> &Arc { @@ -97,19 +97,19 @@ impl Deref for Data { } } -impl Clone for Data { +impl Clone for Data { fn clone(&self) -> Data { Data(self.0.clone()) } } -impl From> for Data { +impl From> for Data { fn from(arc: Arc) -> Self { Data(arc) } } -impl FromRequest for Data { +impl FromRequest for Data { type Config = (); type Error = Error; type Future = Ready>; @@ -131,7 +131,7 @@ impl FromRequest for Data { } } -impl DataFactory for Data { +impl DataFactory for Data { fn create(&self, extensions: &mut Extensions) -> bool { if !extensions.contains::>() { extensions.insert(Data(self.0.clone())); @@ -293,4 +293,24 @@ mod tests { let data_from_arc = Data::from(Arc::new(String::from("test-123"))); assert_eq!(data_new.0, data_from_arc.0) } + + #[actix_rt::test] + async fn test_data_from_dyn_arc() { + trait TestTrait { + fn get_num(&self) -> i32; + } + struct A {} + impl TestTrait for A { + fn get_num(&self) -> i32 { + 42 + } + } + // This works when Sized is required + let dyn_arc_box: Arc> = Arc::new(Box::new(A {})); + let data_arc_box = Data::from(dyn_arc_box); + // This works when Data Sized Bound is removed + let dyn_arc: Arc = Arc::new(A {}); + let data_arc = Data::from(dyn_arc); + assert_eq!(data_arc_box.get_num(), data_arc.get_num()) + } } From aa11231ee5285016e7d326c834ba061401ac511d Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 30 Sep 2020 11:07:35 +0100 Subject: [PATCH 77/78] prepare web release 3.1.0 (#1716) --- CHANGES.md | 25 +++++++++++++++++-------- Cargo.toml | 2 +- actix-web-codegen/CHANGES.md | 23 ++++++++++++----------- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 86e022409..5fd3869f9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,13 +1,22 @@ # Changes ## Unreleased - 2020-xx-xx -### Changed -* Add `TrailingSlash::MergeOnly` behaviour to `NormalizePath`, which allow `NormalizePath` - to keep the trailing slash's existance as it is. [#1695] -* Fix `ResourceMap` recursive references when printing/debugging. [#1708] -* Remove bound `std::marker::Sized` from `web::Data` to support storing `Arc` via `web::Data::from` [#1710] + +## 3.1.0 - 2020-09-29 +### Changed +* Add `TrailingSlash::MergeOnly` behaviour to `NormalizePath`, which allows `NormalizePath` + to retain any trailing slashes. [#1695] +* Remove bound `std::marker::Sized` from `web::Data` to support storing `Arc` + via `web::Data::from` [#1710] + +### Fixed +* `ResourceMap` debug printing is no longer infinitely recursive. [#1708] + +[#1695]: https://github.com/actix/actix-web/pull/1695 [#1708]: https://github.com/actix/actix-web/pull/1708 +[#1710]: https://github.com/actix/actix-web/pull/1710 + ## 3.0.2 - 2020-09-15 ### Fixed @@ -177,7 +186,7 @@ ### Deleted -* Delete HttpServer::run(), it is not useful witht async/await +* Delete HttpServer::run(), it is not useful with async/await ## [2.0.0-alpha.3] - 2019-12-07 @@ -222,7 +231,7 @@ ### Changed -* Make UrlEncodedError::Overflow more informativve +* Make UrlEncodedError::Overflow more informative * Use actix-testing for testing utils @@ -240,7 +249,7 @@ * Re-implement Host predicate (#989) -* Form immplements Responder, returning a `application/x-www-form-urlencoded` response +* Form implements Responder, returning a `application/x-www-form-urlencoded` response * Add `into_inner` to `Data` diff --git a/Cargo.toml b/Cargo.toml index d4da4af11..56158389c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "3.0.2" +version = "3.1.0" authors = ["Nikolay Kim "] description = "Actix web is a powerful, pragmatic, and extremely fast web framework for Rust." readme = "README.md" diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index ad1a22b88..1ab51f924 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -5,7 +5,7 @@ ## 0.4.0 - 2020-09-20 * Added compile success and failure testing. [#1677] -* Add `route` macro for supporting multiple HTTP methods guards. +* Add `route` macro for supporting multiple HTTP methods guards. [#1674] [#1677]: https://github.com/actix/actix-web/pull/1677 [#1674]: https://github.com/actix/actix-web/pull/1674 @@ -21,47 +21,48 @@ [#1559]: https://github.com/actix/actix-web/pull/1559 -## [0.2.2] - 2020-05-23 +## 0.2.2 - 2020-05-23 * Add resource middleware on actix-web-codegen [#1467] [#1467]: https://github.com/actix/actix-web/pull/1467 -## [0.2.1] - 2020-02-25 + +## 0.2.1 - 2020-02-25 * Add `#[allow(missing_docs)]` attribute to generated structs [#1368] * Allow the handler function to be named as `config` [#1290] [#1368]: https://github.com/actix/actix-web/issues/1368 [#1290]: https://github.com/actix/actix-web/issues/1290 -## [0.2.0] - 2019-12-13 +## 0.2.0 - 2019-12-13 * Generate code for actix-web 2.0 -## [0.1.3] - 2019-10-14 +## 0.1.3 - 2019-10-14 * Bump up `syn` & `quote` to 1.0 * Provide better error message -## [0.1.2] - 2019-06-04 +## 0.1.2 - 2019-06-04 * Add macros for head, options, trace, connect and patch http methods -## [0.1.1] - 2019-06-01 +## 0.1.1 - 2019-06-01 * Add syn "extra-traits" feature -## [0.1.0] - 2019-05-18 +## 0.1.0 - 2019-05-18 * Release -## [0.1.0-beta.1] - 2019-04-20 +## 0.1.0-beta.1 - 2019-04-20 * Gen code for actix-web 1.0.0-beta.1 -## [0.1.0-alpha.6] - 2019-04-14 +## 0.1.0-alpha.6 - 2019-04-14 * Gen code for actix-web 1.0.0-alpha.6 -## [0.1.0-alpha.1] - 2019-03-28 +## 0.1.0-alpha.1 - 2019-03-28 * Initial impl From c2c71cc6269fa5a432a51e2802be5f2327d7633a Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Thu, 1 Oct 2020 18:19:09 +0900 Subject: [PATCH 78/78] Fix/suppress clippy warnings (#1720) --- actix-http/src/lib.rs | 1 + src/lib.rs | 1 + src/scope.rs | 1 - src/types/json.rs | 2 +- src/types/payload.rs | 2 +- 5 files changed, 4 insertions(+), 3 deletions(-) diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index b52e8179c..fab91be2b 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -7,6 +7,7 @@ clippy::new_without_default, clippy::borrow_interior_mutable_const )] +#![allow(clippy::manual_strip)] // Allow this to keep MSRV(1.42). #[macro_use] extern crate log; diff --git a/src/lib.rs b/src/lib.rs index 327cba954..edc8456ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,6 +67,7 @@ #![deny(rust_2018_idioms)] #![allow(clippy::needless_doctest_main, clippy::type_complexity)] +#![allow(clippy::rc_buffer)] // FXIME: We should take a closer look for the warnings at some point. #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] diff --git a/src/scope.rs b/src/scope.rs index 25b5366d8..2520fd7ae 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -58,7 +58,6 @@ type BoxedResponse = LocalBoxFuture<'static, Result>; /// * /{project_id}/path1 - responds to all http method /// * /{project_id}/path2 - `GET` requests /// * /{project_id}/path3 - `HEAD` requests -/// pub struct Scope { endpoint: T, rdef: String, diff --git a/src/types/json.rs b/src/types/json.rs index 8da5a3bdb..081a022e8 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -283,7 +283,7 @@ impl JsonConfig { fn from_req(req: &HttpRequest) -> &Self { req.app_data::() .or_else(|| req.app_data::>().map(|d| d.as_ref())) - .unwrap_or_else(|| &DEFAULT_CONFIG) + .unwrap_or(&DEFAULT_CONFIG) } } diff --git a/src/types/payload.rs b/src/types/payload.rs index bbdd89525..4ff5ef4b4 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -284,7 +284,7 @@ impl PayloadConfig { fn from_req(req: &HttpRequest) -> &Self { req.app_data::() .or_else(|| req.app_data::>().map(|d| d.as_ref())) - .unwrap_or_else(|| &DEFAULT_CONFIG) + .unwrap_or(&DEFAULT_CONFIG) } }