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