diff --git a/Cargo.toml b/Cargo.toml index 06c88c57e..1bd089f02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,10 +35,10 @@ members = [ ] [package.metadata.docs.rs] -features = ["ssl", "tls", "brotli", "flate2-zlib", "cookies", "client", "rust-tls"] +features = ["ssl", "tls", "brotli", "flate2-zlib", "secure-cookies", "client", "rust-tls"] [features] -default = ["brotli", "flate2-zlib", "cookies", "client"] +default = ["brotli", "flate2-zlib", "secure-cookies", "client"] # http client client = ["awc"] @@ -53,7 +53,7 @@ flate2-zlib = ["actix-http/flate2-zlib"] flate2-rust = ["actix-http/flate2-rust"] # sessions feature, session require "ring" crate and c compiler -cookies = ["cookie", "actix-http/cookies"] +secure-cookies = ["actix-http/secure-cookies"] # tls tls = ["native-tls", "actix-server/ssl"] @@ -94,9 +94,6 @@ serde_urlencoded = "^0.5.3" time = "0.1" url = { version="1.7", features=["query_encoding"] } -# cookies support -cookie = { version="0.11", features=["secure", "percent-encode"], optional = true } - # ssl support native-tls = { version="0.2", optional = true } openssl = { version="0.10", optional = true } @@ -112,9 +109,6 @@ tokio-timer = "0.2.8" brotli2 = "0.3.2" flate2 = "1.0.2" -[replace] -"cookie:0.11.0" = { git = 'https://github.com/alexcrichton/cookie-rs.git' } - [profile.release] lto = true opt-level = 3 diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index d6ae67540..058a19987 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -18,8 +18,8 @@ name = "actix_files" path = "src/lib.rs" [dependencies] -actix-web = "1.0.0-alpha.1" -actix-http = "0.1.0-alpha.1" +#actix-web = "1.0.0-alpha.1" +actix-web = { path = ".." } actix-service = "0.3.3" bitflags = "1" @@ -33,4 +33,5 @@ percent-encoding = "1.0" v_htmlescape = "0.4" [dev-dependencies] -actix-web = { version = "1.0.0-alpha.1", features=["ssl"] } +#actix-web = { version = "1.0.0-alpha.1", features=["ssl"] } +actix-web = { path = "..", features=["ssl"] } diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 0c910cb1e..e9bb5d9bb 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -16,7 +16,7 @@ edition = "2018" workspace = ".." [package.metadata.docs.rs] -features = ["ssl", "fail", "cookie", "brotli", "flate2-zlib"] +features = ["ssl", "fail", "brotli", "flate2-zlib", "secure-cookies"] [badges] travis-ci = { repository = "actix/actix-web", branch = "master" } @@ -32,9 +32,6 @@ default = [] # openssl ssl = ["openssl", "actix-connect/ssl"] -# cookies integration -cookies = ["cookie"] - # brotli encoding, requires c compiler brotli = ["brotli2"] @@ -47,6 +44,9 @@ flate2-rust = ["flate2/rust_backend"] # failure integration. actix does not use failure anymore fail = ["failure"] +# support for secure cookies +secure-cookies = ["ring"] + [dependencies] actix-service = "0.3.4" actix-codec = "0.1.2" @@ -85,12 +85,14 @@ tokio-timer = "0.2" tokio-current-thread = "0.1" trust-dns-resolver = { version="0.11.0-alpha.2", default-features = false } +# for secure cookie +ring = { version = "0.14.6", optional = true } + # compression brotli2 = { version="^0.3.2", optional = true } flate2 = { version="^1.0.2", optional = true, default-features = false } # optional deps -cookie = { version="0.11", features=["percent-encode"], optional = true } failure = { version = "0.1.5", optional = true } openssl = { version="0.10", optional = true } diff --git a/actix-http/src/client/connection.rs b/actix-http/src/client/connection.rs index 267c85d31..4522dbbd3 100644 --- a/actix-http/src/client/connection.rs +++ b/actix-http/src/client/connection.rs @@ -184,11 +184,11 @@ where match self { EitherConnection::A(con) => Box::new( con.open_tunnel(head) - .map(|(head, framed)| (head, framed.map_io(|io| EitherIo::A(io)))), + .map(|(head, framed)| (head, framed.map_io(EitherIo::A))), ), EitherConnection::B(con) => Box::new( con.open_tunnel(head) - .map(|(head, framed)| (head, framed.map_io(|io| EitherIo::B(io)))), + .map(|(head, framed)| (head, framed.map_io(EitherIo::B))), ), } } diff --git a/actix-http/src/cookie/builder.rs b/actix-http/src/cookie/builder.rs new file mode 100644 index 000000000..2635572ab --- /dev/null +++ b/actix-http/src/cookie/builder.rs @@ -0,0 +1,240 @@ +use std::borrow::Cow; + +use time::{Duration, Tm}; + +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; +/// use time::Duration; +/// +/// # fn main() { +/// let cookie: Cookie = Cookie::build("name", "value") +/// .domain("www.rust-lang.org") +/// .path("/") +/// .secure(true) +/// .http_only(true) +/// .max_age(Duration::days(1)) +/// .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; + /// + /// # fn main() { + /// let c = Cookie::build("foo", "bar") + /// .expires(time::now()) + /// .finish(); + /// + /// assert!(c.expires().is_some()); + /// # } + /// ``` + #[inline] + pub fn expires(mut self, when: Tm) -> CookieBuilder { + self.cookie.set_expires(when); + self + } + + /// Sets the `max_age` field in the cookie being built. + /// + /// # Example + /// + /// ```rust + /// use actix_http::cookie::Cookie; + /// + /// # fn main() { + /// let c = Cookie::build("foo", "bar") + /// .max_age(time::Duration::minutes(30)) + /// .finish(); + /// + /// assert_eq!(c.max_age(), Some(time::Duration::seconds(30 * 60))); + /// # } + /// ``` + #[inline] + pub fn max_age(mut self, value: Duration) -> CookieBuilder { + self.cookie.set_max_age(value); + 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; + /// + /// # fn main() { + /// 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 new file mode 100644 index 000000000..a001a5bb8 --- /dev/null +++ b/actix-http/src/cookie/delta.rs @@ -0,0 +1,71 @@ +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 new file mode 100644 index 000000000..362133946 --- /dev/null +++ b/actix-http/src/cookie/draft.rs @@ -0,0 +1,98 @@ +//! 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 (made explicit via the +/// `SameSite::None` variant), then the cookie will be sent as normal. +/// +/// **Note:** This cookie attribute is an HTTP draft! Its meaning and definition +/// are subject to change. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SameSite { + /// The "Strict" `SameSite` attribute. + Strict, + /// The "Lax" `SameSite` attribute. + Lax, + /// No `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 => Ok(()), + } + } +} diff --git a/actix-http/src/cookie/jar.rs b/actix-http/src/cookie/jar.rs new file mode 100644 index 000000000..d9ab8f056 --- /dev/null +++ b/actix-http/src/cookie/jar.rs @@ -0,0 +1,653 @@ +use std::collections::HashSet; +use std::mem::replace; + +use time::{self, Duration}; + +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 interator 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; + /// + /// # fn main() { + /// 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::seconds(0))); + /// # } + /// ``` + /// + /// 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::seconds(0)); + cookie.set_expires(time::now() - 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; + /// + /// # fn main() { + /// 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 replace(&mut self.original_cookies, HashSet::new()) { + 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 { + use super::{Cookie, CookieJar, Key}; + + #[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::seconds(0))); + } + + #[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 new file mode 100644 index 000000000..5545624a8 --- /dev/null +++ b/actix-http/src/cookie/mod.rs @@ -0,0 +1,1087 @@ +//! 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")] +#![deny(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 secure::*; + +use std::borrow::Cow; +use std::fmt; +use std::str::FromStr; + +use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; +use time::{Duration, Tm}; + +pub use builder::CookieBuilder; +pub use draft::*; +pub use jar::{CookieJar, Delta, Iter}; +use parse::parse_cookie; +pub use parse::ParseError; + +#[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 subslice 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>) -> &'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, + } + } + + 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.num_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.tm_year), Some(117)); + /// ``` + #[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; + /// + /// # fn main() { + /// 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; + /// + /// # fn main() { + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.expires(), None); + /// + /// let mut now = time::now(); + /// now.tm_year += 1; + /// + /// c.set_expires(now); + /// assert!(c.expires().is_some()) + /// # } + /// ``` + #[inline] + pub fn set_expires(&mut self, time: Tm) { + 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; + /// + /// # fn main() { + /// 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(time::now() + 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() { + if !same_site.is_none() { + 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.num_seconds())?; + } + + if let Some(time) = self.expires() { + write!(f, "; Expires={}", time.rfc822())?; + } + + 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>(&'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_ENCODE_SET); + let value = percent_encode(self.0.value().as_bytes(), USERINFO_ENCODE_SET); + + // 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::{strptime, Duration}; + + #[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(Duration::seconds(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 = strptime(time_str, "%a, %d %b %Y %H:%M:%S %Z").unwrap(); + 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"); + } + + #[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 new file mode 100644 index 000000000..ebbd6ffc9 --- /dev/null +++ b/actix-http/src/cookie/parse.rs @@ -0,0 +1,426 @@ +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::{self, Duration}; + +use super::{Cookie, CookieStr, SameSite}; + +/// 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 description(&self) -> &str { + self.as_str() + } +} + +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().num_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 { + // 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 strptime 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::strptime(v, "%a, %d %b %Y %H:%M:%S %Z") + .or_else(|_| time::strptime(v, "%A, %d-%b-%y %H:%M:%S %Z")) + .or_else(|_| time::strptime(v, "%a, %d-%b-%Y %H:%M:%S %Z")) + .or_else(|_| time::strptime(v, "%a %b %d %H:%M:%S %Y")); + + if let Ok(time) = tm { + cookie.expires = Some(time) + } + } + _ => { + // 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::{strptime, Duration}; + + 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); + } + + #[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 = strptime(time_str, "%a, %d %b %Y %H:%M:%S %Z").unwrap(); + 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 = strptime(time_str, "%a, %d %b %Y %H:%S:%M %Z").unwrap(); + 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 + ); + } + + #[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_seconds = Duration::max_value().num_seconds(); + let expected = Cookie::build("foo", "bar") + .max_age(Duration::seconds(max_seconds)) + .finish(); + assert_eq_parse!(format!(" foo=bar; Max-Age={:?}", max_seconds + 1), expected); + } +} diff --git a/actix-http/src/cookie/secure/key.rs b/actix-http/src/cookie/secure/key.rs new file mode 100644 index 000000000..3b8a0af78 --- /dev/null +++ b/actix-http/src/cookie/secure/key.rs @@ -0,0 +1,180 @@ +use ring::digest::{Algorithm, SHA256}; +use ring::hkdf::expand; +use ring::hmac::SigningKey; +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: &'static Algorithm = &SHA256; +const KEYS_INFO: &'static str = "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 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() + ); + } + + // Expand the user's key into two. + let prk = SigningKey::new(HKDF_DIGEST, key); + let mut both_keys = [0; SIGNED_KEY_LEN + PRIVATE_KEY_LEN]; + expand(&prk, KEYS_INFO.as_bytes(), &mut both_keys); + + // Copy the keys into their respective arrays. + let mut signing_key = [0; SIGNED_KEY_LEN]; + let mut encryption_key = [0; PRIVATE_KEY_LEN]; + signing_key.copy_from_slice(&both_keys[..SIGNED_KEY_LEN]); + encryption_key.copy_from_slice(&both_keys[SIGNED_KEY_LEN..]); + + Key { + signing_key: signing_key, + encryption_key: encryption_key, + } + } + + /// Generates signing/encryption keys from a secure, random source. Keys are + /// generated nondeterministically. + /// + /// # 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 nondeterministically. 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 new file mode 100644 index 000000000..089047c4e --- /dev/null +++ b/actix-http/src/cookie/secure/macros.rs @@ -0,0 +1,40 @@ +#[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 new file mode 100644 index 000000000..e0fba9733 --- /dev/null +++ b/actix-http/src/cookie/secure/mod.rs @@ -0,0 +1,10 @@ +//! 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 new file mode 100644 index 000000000..8b56991f1 --- /dev/null +++ b/actix-http/src/cookie/secure/private.rs @@ -0,0 +1,226 @@ +use ring::aead::{open_in_place, seal_in_place, Aad, Algorithm, Nonce, AES_256_GCM}; +use ring::aead::{OpeningKey, SealingKey}; +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: &'static 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: 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 = OpeningKey::new(ALGO, &self.key).expect("opening key"); + let (nonce, sealed) = data.split_at_mut(NONCE_LEN); + let nonce = + Nonce::try_assume_unique_for_key(nonce).expect("invalid length of `nonce`"); + let unsealed = open_in_place(&key, nonce, ad, 0, sealed) + .map_err(|_| "invalid key/nonce/value: bad seal")?; + + ::std::str::from_utf8(unsealed) + .map(|s| s.to_string()) + .map_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 mut data; + let output_len = { + // Create the `SealingKey` structure. + let key = SealingKey::new(ALGO, &self.key).expect("sealing key creation"); + + // Create a vec to hold the [nonce | cookie value | overhead]. + let overhead = ALGO.tag_len(); + let cookie_val = cookie.value().as_bytes(); + data = vec![0; NONCE_LEN + cookie_val.len() + overhead]; + + // Randomly generate the nonce, then copy the cookie value as input. + let (nonce, in_out) = data.split_at_mut(NONCE_LEN); + SystemRandom::new() + .fill(nonce) + .expect("couldn't random fill nonce"); + in_out[..cookie_val.len()].copy_from_slice(cookie_val); + let nonce = Nonce::try_assume_unique_for_key(nonce) + .expect("invalid length of `nonce`"); + + // Use cookie's name as associated data to prevent value swapping. + let ad = Aad::from(cookie.name().as_bytes()); + + // Perform the actual sealing operation and get the output length. + seal_in_place(&key, nonce, ad, in_out, overhead).expect("in-place seal") + }; + + // Base64 encode the nonce and encrypted value. + let sealed_value = base64::encode(&data[..(NONCE_LEN + output_len)]); + 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); + } +} + +#[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.private(&key)); + } + + #[test] + fn private() { + let key = Key::generate(); + let mut jar = CookieJar::new(); + assert_secure_behaviour!(jar, jar.private(&key)); + } +} diff --git a/actix-http/src/cookie/secure/signed.rs b/actix-http/src/cookie/secure/signed.rs new file mode 100644 index 000000000..5a4ffb76c --- /dev/null +++ b/actix-http/src/cookie/secure/signed.rs @@ -0,0 +1,185 @@ +use ring::digest::{Algorithm, SHA256}; +use ring::hmac::{sign, verify_with_own_key as verify, SigningKey}; + +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: &'static Algorithm = &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: SigningKey, +} + +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: parent, + key: SigningKey::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/error.rs b/actix-http/src/error.rs index b062fdf42..45bf067dc 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -6,8 +6,6 @@ use std::{fmt, io, result}; pub use actix_threadpool::BlockingError; use actix_utils::timeout::TimeoutError; -#[cfg(feature = "cookies")] -use cookie; use derive_more::{Display, From}; use futures::Canceled; use http::uri::InvalidUri; @@ -19,8 +17,7 @@ use serde_urlencoded::ser::Error as FormError; use tokio_timer::Error as TimerError; // re-export for convinience -#[cfg(feature = "cookies")] -pub use cookie::ParseError as CookieParseError; +pub use crate::cookie::ParseError as CookieParseError; use crate::body::Body; use crate::response::Response; @@ -79,7 +76,7 @@ impl fmt::Display for Error { impl fmt::Debug for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}\n", &self.cause) + writeln!(f, "{:?}", &self.cause) } } @@ -319,8 +316,7 @@ impl ResponseError for PayloadError { } /// Return `BadRequest` for `cookie::ParseError` -#[cfg(feature = "cookies")] -impl ResponseError for cookie::ParseError { +impl ResponseError for crate::cookie::ParseError { fn error_response(&self) -> Response { Response::new(StatusCode::BAD_REQUEST) } @@ -895,10 +891,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } - #[cfg(feature = "cookies")] #[test] fn test_cookie_parse() { - use cookie::ParseError as CookieParseError; let resp: Response = CookieParseError::EmptyName.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 34204bf5a..96db08122 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -525,7 +525,7 @@ where // keep-alive and stream errors if inner.state.is_empty() && inner.framed.is_write_buf_empty() { if let Some(err) = inner.error.take() { - return Err(err); + Err(err) } // disconnect if keep-alive is not enabled else if inner.flags.contains(Flags::STARTED) @@ -538,10 +538,10 @@ where else if inner.flags.contains(Flags::SHUTDOWN) { self.poll() } else { - return Ok(Async::NotReady); + Ok(Async::NotReady) } } else { - return Ok(Async::NotReady); + Ok(Async::NotReady) } } } diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index 6ab37919c..9d9a19e24 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -220,7 +220,7 @@ where Ok(Async::NotReady) => Ok(Async::NotReady), Err(err) => { trace!("H2 handshake error: {}", err); - return Err(err.into()); + Err(err.into()) } } } diff --git a/actix-http/src/httpmessage.rs b/actix-http/src/httpmessage.rs index 60821d300..7a2db52d6 100644 --- a/actix-http/src/httpmessage.rs +++ b/actix-http/src/httpmessage.rs @@ -7,17 +7,12 @@ use encoding::EncodingRef; use http::{header, HeaderMap}; use mime::Mime; -use crate::error::{ContentTypeError, ParseError}; +use crate::cookie::Cookie; +use crate::error::{ContentTypeError, CookieParseError, ParseError}; use crate::extensions::Extensions; use crate::header::Header; use crate::payload::Payload; -#[cfg(feature = "cookies")] -use crate::error::CookieParseError; -#[cfg(feature = "cookies")] -use cookie::Cookie; - -#[cfg(feature = "cookies")] struct Cookies(Vec>); /// Trait that implements general purpose operations on http messages @@ -110,7 +105,6 @@ pub trait HttpMessage: Sized { /// Load request cookies. #[inline] - #[cfg(feature = "cookies")] fn cookies(&self) -> Result>>, CookieParseError> { if self.extensions().get::().is_none() { let mut cookies = Vec::new(); @@ -131,7 +125,6 @@ pub trait HttpMessage: Sized { } /// Return request cookie. - #[cfg(feature = "cookies")] fn cookie(&self, name: &str) -> Option> { if let Ok(cookies) = self.cookies() { for cookie in cookies.iter() { diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index 565fe4455..a8c44e833 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -1,9 +1,5 @@ //! Basic http primitives for actix-net framework. -#![allow( - clippy::type_complexity, - clippy::new_without_default, - clippy::new_without_default_derive -)] +#![allow(clippy::type_complexity, clippy::new_without_default)] #[macro_use] extern crate log; @@ -24,6 +20,7 @@ mod request; mod response; mod service; +pub mod cookie; pub mod error; pub mod h1; pub mod h2; @@ -46,16 +43,11 @@ pub mod http { // re-exports pub use http::header::{HeaderName, HeaderValue}; + pub use http::uri::PathAndQuery; + pub use http::{uri, Error, HeaderMap, HttpTryFrom, Uri}; pub use http::{Method, StatusCode, Version}; - #[doc(hidden)] - pub use http::{uri, Error, HeaderMap, HttpTryFrom, Uri}; - - #[doc(hidden)] - pub use http::uri::PathAndQuery; - - #[cfg(feature = "cookies")] - pub use cookie::{Cookie, CookieBuilder}; + pub use crate::cookie::{Cookie, CookieBuilder}; /// Various http headers pub mod header { diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs index 4da0f642c..a6e8f613c 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/response.rs @@ -4,8 +4,6 @@ use std::io::Write; use std::{fmt, str}; use bytes::{BufMut, Bytes, BytesMut}; -#[cfg(feature = "cookies")] -use cookie::{Cookie, CookieJar}; use futures::future::{ok, FutureResult, IntoFuture}; use futures::Stream; use http::header::{self, HeaderName, HeaderValue}; @@ -14,6 +12,7 @@ use serde::Serialize; use serde_json; use crate::body::{Body, BodyStream, MessageBody, ResponseBody}; +use crate::cookie::{Cookie, CookieJar}; use crate::error::Error; use crate::extensions::Extensions; use crate::header::{Header, IntoHeaderValue}; @@ -131,7 +130,6 @@ impl Response { /// Get an iterator for the cookies set by this response #[inline] - #[cfg(feature = "cookies")] pub fn cookies(&self) -> CookieIter { CookieIter { iter: self.head.headers.get_all(header::SET_COOKIE).iter(), @@ -140,7 +138,6 @@ impl Response { /// Add a cookie to this response #[inline] - #[cfg(feature = "cookies")] pub fn add_cookie(&mut self, cookie: &Cookie) -> Result<(), HttpError> { let h = &mut self.head.headers; HeaderValue::from_str(&cookie.to_string()) @@ -153,7 +150,6 @@ impl Response { /// Remove all cookies with the given name from this response. Returns /// the number of cookies removed. #[inline] - #[cfg(feature = "cookies")] pub fn del_cookie(&mut self, name: &str) -> usize { let h = &mut self.head.headers; let vals: Vec = h @@ -245,8 +241,8 @@ impl Response { let body = f(&mut self.head, self.body); Response { + body, head: self.head, - body: body, error: self.error, } } @@ -285,12 +281,10 @@ impl IntoFuture for Response { } } -#[cfg(feature = "cookies")] pub struct CookieIter<'a> { iter: header::ValueIter<'a, HeaderValue>, } -#[cfg(feature = "cookies")] impl<'a> Iterator for CookieIter<'a> { type Item = Cookie<'a>; @@ -312,7 +306,6 @@ impl<'a> Iterator for CookieIter<'a> { pub struct ResponseBuilder { head: Option>, err: Option, - #[cfg(feature = "cookies")] cookies: Option, } @@ -325,7 +318,6 @@ impl ResponseBuilder { ResponseBuilder { head: Some(head), err: None, - #[cfg(feature = "cookies")] cookies: None, } } @@ -525,7 +517,6 @@ impl ResponseBuilder { /// .finish() /// } /// ``` - #[cfg(feature = "cookies")] pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self { if self.cookies.is_none() { let mut jar = CookieJar::new(); @@ -553,7 +544,6 @@ impl ResponseBuilder { /// builder.finish() /// } /// ``` - #[cfg(feature = "cookies")] pub fn del_cookie<'a>(&mut self, cookie: &Cookie<'a>) -> &mut Self { { if self.cookies.is_none() { @@ -620,20 +610,16 @@ impl ResponseBuilder { return Response::from(Error::from(e)).into_body(); } - #[allow(unused_mut)] let mut response = self.head.take().expect("cannot reuse response builder"); - #[cfg(feature = "cookies")] - { - if let Some(ref jar) = self.cookies { - for cookie in jar.delta() { - match HeaderValue::from_str(&cookie.to_string()) { - Ok(val) => { - let _ = response.headers.append(header::SET_COOKIE, val); - } - Err(e) => return Response::from(Error::from(e)).into_body(), - }; - } + if let Some(ref jar) = self.cookies { + for cookie in jar.delta() { + match HeaderValue::from_str(&cookie.to_string()) { + Ok(val) => { + let _ = response.headers.append(header::SET_COOKIE, val); + } + Err(e) => return Response::from(Error::from(e)).into_body(), + }; } } @@ -697,7 +683,6 @@ impl ResponseBuilder { ResponseBuilder { head: self.head.take(), err: self.err.take(), - #[cfg(feature = "cookies")] cookies: self.cookies.take(), } } @@ -718,9 +703,7 @@ fn parts<'a>( impl From> for ResponseBuilder { fn from(res: Response) -> ResponseBuilder { // If this response has cookies, load them into a jar - #[cfg(feature = "cookies")] let mut jar: Option = None; - #[cfg(feature = "cookies")] for c in res.cookies() { if let Some(ref mut j) = jar { j.add_original(c.into_owned()); @@ -734,7 +717,6 @@ impl From> for ResponseBuilder { ResponseBuilder { head: Some(res.head), err: None, - #[cfg(feature = "cookies")] cookies: jar, } } @@ -744,22 +726,18 @@ impl From> for ResponseBuilder { impl<'a> From<&'a ResponseHead> for ResponseBuilder { fn from(head: &'a ResponseHead) -> ResponseBuilder { // If this response has cookies, load them into a jar - #[cfg(feature = "cookies")] let mut jar: Option = None; - #[cfg(feature = "cookies")] - { - let cookies = CookieIter { - iter: head.headers.get_all(header::SET_COOKIE).iter(), - }; - for c in cookies { - if let Some(ref mut j) = jar { - j.add_original(c.into_owned()); - } else { - let mut j = CookieJar::new(); - j.add_original(c.into_owned()); - jar = Some(j); - } + let cookies = CookieIter { + iter: head.headers.get_all(header::SET_COOKIE).iter(), + }; + for c in cookies { + if let Some(ref mut j) = jar { + j.add_original(c.into_owned()); + } else { + let mut j = CookieJar::new(); + j.add_original(c.into_owned()); + jar = Some(j); } } @@ -773,7 +751,6 @@ impl<'a> From<&'a ResponseHead> for ResponseBuilder { ResponseBuilder { head: Some(msg), err: None, - #[cfg(feature = "cookies")] cookies: jar, } } @@ -870,7 +847,6 @@ mod tests { } #[test] - #[cfg(feature = "cookies")] fn test_response_cookies() { use crate::httpmessage::HttpMessage; @@ -907,7 +883,6 @@ mod tests { } #[test] - #[cfg(feature = "cookies")] fn test_update_response_cookies() { let mut r = Response::Ok() .cookie(crate::http::Cookie::new("original", "val100")) @@ -1068,7 +1043,6 @@ mod tests { } #[test] - #[cfg(feature = "cookies")] fn test_into_builder() { let mut resp: Response = "test".into(); assert_eq!(resp.status(), StatusCode::OK); diff --git a/actix-http/src/test.rs b/actix-http/src/test.rs index 2d4b3d0f9..023161246 100644 --- a/actix-http/src/test.rs +++ b/actix-http/src/test.rs @@ -1,12 +1,13 @@ //! Test Various helpers for Actix applications to use during testing. +use std::fmt::Write as FmtWrite; use std::str::FromStr; use bytes::Bytes; -#[cfg(feature = "cookies")] -use cookie::{Cookie, CookieJar}; -use http::header::HeaderName; +use http::header::{self, HeaderName, HeaderValue}; use http::{HeaderMap, HttpTryFrom, Method, Uri, Version}; +use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; +use crate::cookie::{Cookie, CookieJar}; use crate::header::{Header, IntoHeaderValue}; use crate::payload::Payload; use crate::Request; @@ -45,7 +46,6 @@ struct Inner { method: Method, uri: Uri, headers: HeaderMap, - #[cfg(feature = "cookies")] cookies: CookieJar, payload: Option, } @@ -57,7 +57,6 @@ impl Default for TestRequest { uri: Uri::from_str("/").unwrap(), version: Version::HTTP_11, headers: HeaderMap::new(), - #[cfg(feature = "cookies")] cookies: CookieJar::new(), payload: None, })) @@ -127,7 +126,6 @@ impl TestRequest { } /// Set cookie for this request - #[cfg(feature = "cookies")] pub fn cookie<'a>(&mut self, cookie: Cookie<'a>) -> &mut Self { parts(&mut self.0).cookies.add(cookie.into_owned()); self @@ -161,25 +159,17 @@ impl TestRequest { head.version = inner.version; head.headers = inner.headers; - #[cfg(feature = "cookies")] - { - use std::fmt::Write as FmtWrite; - - use http::header::{self, HeaderValue}; - use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; - - let mut cookie = String::new(); - for c in inner.cookies.delta() { - let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); - let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); - let _ = write!(&mut cookie, "; {}={}", name, value); - } - if !cookie.is_empty() { - head.headers.insert( - header::COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), - ); - } + let mut cookie = String::new(); + for c in inner.cookies.delta() { + let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); + let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); + let _ = write!(&mut cookie, "; {}={}", name, value); + } + if !cookie.is_empty() { + head.headers.insert( + header::COOKIE, + HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), + ); } req diff --git a/actix-http/tests/test_client.rs b/actix-http/tests/test_client.rs index 78b999703..ea0c5eb9a 100644 --- a/actix-http/tests/test_client.rs +++ b/actix-http/tests/test_client.rs @@ -1,11 +1,8 @@ use actix_service::NewService; -use bytes::{Bytes, BytesMut}; +use bytes::Bytes; use futures::future::{self, ok}; -use futures::{Future, Stream}; -use actix_http::{ - error::PayloadError, http, HttpMessage, HttpService, Request, Response, -}; +use actix_http::{http, HttpService, Request, Response}; use actix_http_test::TestServer; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ @@ -30,16 +27,6 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World"; -fn load_body(stream: S) -> impl Future -where - S: Stream, -{ - stream.fold(BytesMut::new(), move |mut body, chunk| { - body.extend_from_slice(&chunk); - Ok::<_, PayloadError>(body) - }) -} - #[test] fn test_h1_v2() { env_logger::init(); diff --git a/actix-session/Cargo.toml b/actix-session/Cargo.toml index a2b8e0e15..6569286cb 100644 --- a/actix-session/Cargo.toml +++ b/actix-session/Cargo.toml @@ -21,13 +21,13 @@ path = "src/lib.rs" default = ["cookie-session"] # sessions feature, session require "ring" crate and c compiler -cookie-session = ["cookie/secure"] +cookie-session = ["actix-web/secure-cookies"] [dependencies] -actix-web = "1.0.0-alpha.1" +#actix-web = "1.0.0-alpha.1" +actix-web = { path = ".." } actix-service = "0.3.3" bytes = "0.4" -cookie = { version="0.11", features=["percent-encode"], optional=true } derive_more = "0.14" futures = "0.1.25" hashbrown = "0.1.8" diff --git a/actix-session/src/cookie.rs b/actix-session/src/cookie.rs index 2d5bd7089..9e4fe78b2 100644 --- a/actix-session/src/cookie.rs +++ b/actix-session/src/cookie.rs @@ -19,10 +19,10 @@ use std::collections::HashMap; use std::rc::Rc; use actix_service::{Service, Transform}; +use actix_web::cookie::{Cookie, CookieJar, Key, SameSite}; use actix_web::dev::{ServiceRequest, ServiceResponse}; use actix_web::http::{header::SET_COOKIE, HeaderValue}; use actix_web::{Error, HttpMessage, ResponseError}; -use cookie::{Cookie, CookieJar, Key, SameSite}; use derive_more::{Display, From}; use futures::future::{ok, Future, FutureResult}; use futures::Poll; diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index c0ef89fac..55cfd47d7 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -18,14 +18,16 @@ name = "actix_web_actors" path = "src/lib.rs" [dependencies] -actix = "0.8.0-alpha.1" -actix-web = "1.0.0-alpha.1" -actix-http = "0.1.0-alpha.1" +actix = "0.8.0-alpha.2" +#actix-web = "1.0.0-alpha.1" +#actix-http = "0.1.0-alpha.1" +actix-web = { path=".." } +actix-http = { path="../actix-http" } actix-codec = "0.1.2" bytes = "0.4" futures = "0.1.25" [dev-dependencies] env_logger = "0.6" -actix-http = { version = "0.1.0-alpha.1", features=["ssl"] } -actix-http-test = { version = "0.1.0-alpha.1", features=["ssl"] } +#actix-http-test = { version = "0.1.0-alpha.1", features=["ssl"] } +actix-http-test = { path = "../test-server", features=["ssl"] } diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 1f04cfd57..c3d88883f 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -16,6 +16,9 @@ quote = "0.6" syn = { version = "0.15", features = ["full", "parsing"] } [dev-dependencies] -actix-web = { version = "1.0.0-alpha.1" } -actix-http = { version = "0.1.0-alpha.1", features=["ssl"] } -actix-http-test = { version = "0.1.0-alpha.1", features=["ssl"] } +#actix-web = { version = "1.0.0-alpha.1" } +#actix-http = { version = "0.1.0-alpha.1", features=["ssl"] } +#actix-http-test = { version = "0.1.0-alpha.1", features=["ssl"] } +actix-web = { path = ".." } +actix-http = { path = "../actix-http", features=["ssl"] } +actix-http-test = { path = "../test-server", features=["ssl"] } diff --git a/awc/Cargo.toml b/awc/Cargo.toml index cd94057fb..148bf97de 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -18,17 +18,14 @@ name = "awc" path = "src/lib.rs" [package.metadata.docs.rs] -features = ["ssl", "brotli", "flate2-zlib", "cookies"] +features = ["ssl", "brotli", "flate2-zlib"] [features] -default = ["cookies", "brotli", "flate2-zlib"] +default = ["brotli", "flate2-zlib"] # openssl ssl = ["openssl", "actix-http/ssl"] -# cookies integration -cookies = ["cookie", "actix-http/cookies"] - # brotli encoding, requires c compiler brotli = ["actix-http/brotli"] @@ -53,8 +50,6 @@ serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.5.3" tokio-timer = "0.2.8" - -cookie = { version="0.11", features=["percent-encode"], optional = true } openssl = { version="0.10", optional = true } [dev-dependencies] diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 92f749d03..ff1fb3fef 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -24,7 +24,7 @@ use std::cell::RefCell; use std::rc::Rc; use std::time::Duration; -pub use actix_http::{client::Connector, http}; +pub use actix_http::{client::Connector, cookie, http}; use actix_http::http::{HeaderMap, HttpTryFrom, Method, Uri}; use actix_http::RequestHead; diff --git a/awc/src/request.rs b/awc/src/request.rs index bdde6faf5..a462479ec 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -1,18 +1,19 @@ use std::fmt; +use std::fmt::Write as FmtWrite; use std::io::Write; use std::rc::Rc; use std::time::Duration; use bytes::{BufMut, Bytes, BytesMut}; -#[cfg(feature = "cookies")] -use cookie::{Cookie, CookieJar}; use futures::future::{err, Either}; use futures::{Future, Stream}; +use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use serde::Serialize; use serde_json; use tokio_timer::Timeout; use actix_http::body::{Body, BodyStream}; +use actix_http::cookie::{Cookie, CookieJar}; use actix_http::encoding::Decoder; use actix_http::http::header::{self, ContentEncoding, Header, IntoHeaderValue}; use actix_http::http::{ @@ -59,7 +60,6 @@ const HTTPS_ENCODING: &str = "gzip, deflate"; pub struct ClientRequest { pub(crate) head: RequestHead, err: Option, - #[cfg(feature = "cookies")] cookies: Option, default_headers: bool, response_decompress: bool, @@ -77,7 +77,6 @@ impl ClientRequest { config, head: RequestHead::default(), err: None, - #[cfg(feature = "cookies")] cookies: None, timeout: None, default_headers: true, @@ -268,7 +267,6 @@ impl ClientRequest { self.header(header::AUTHORIZATION, format!("Bearer {}", token)) } - #[cfg(feature = "cookies")] /// Set a cookie /// /// ```rust @@ -437,28 +435,20 @@ impl ClientRequest { } }; - #[allow(unused_mut)] let mut head = slf.head; - #[cfg(feature = "cookies")] - { - use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; - use std::fmt::Write; - - // set cookies - if let Some(ref mut jar) = slf.cookies { - let mut cookie = String::new(); - for c in jar.delta() { - let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); - let value = - percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); - let _ = write!(&mut cookie, "; {}={}", name, value); - } - head.headers.insert( - header::COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), - ); + // set cookies + if let Some(ref mut jar) = slf.cookies { + let mut cookie = String::new(); + for c in jar.delta() { + let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); + let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); + let _ = write!(&mut cookie, "; {}={}", name, value); } + head.headers.insert( + header::COOKIE, + HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), + ); } let config = slf.config; diff --git a/awc/src/response.rs b/awc/src/response.rs index 3b77eaa60..038a9a330 100644 --- a/awc/src/response.rs +++ b/awc/src/response.rs @@ -9,10 +9,8 @@ use actix_http::http::header::{CONTENT_LENGTH, SET_COOKIE}; use actix_http::http::{HeaderMap, StatusCode, Version}; use actix_http::{Extensions, HttpMessage, Payload, PayloadStream, ResponseHead}; -#[cfg(feature = "cookies")] +use actix_http::cookie::Cookie; use actix_http::error::CookieParseError; -#[cfg(feature = "cookies")] -use cookie::Cookie; /// Client Response pub struct ClientResponse { @@ -41,7 +39,6 @@ impl HttpMessage for ClientResponse { /// Load request cookies. #[inline] - #[cfg(feature = "cookies")] fn cookies(&self) -> Result>>, CookieParseError> { struct Cookies(Vec>); diff --git a/awc/src/test.rs b/awc/src/test.rs index 7723b9d2c..5e595d152 100644 --- a/awc/src/test.rs +++ b/awc/src/test.rs @@ -1,10 +1,12 @@ //! Test helpers for actix http client to use during testing. -use actix_http::http::header::{Header, IntoHeaderValue}; +use std::fmt::Write as FmtWrite; + +use actix_http::cookie::{Cookie, CookieJar}; +use actix_http::http::header::{self, Header, HeaderValue, IntoHeaderValue}; use actix_http::http::{HeaderName, HttpTryFrom, Version}; use actix_http::{h1, Payload, ResponseHead}; use bytes::Bytes; -#[cfg(feature = "cookies")] -use cookie::{Cookie, CookieJar}; +use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use crate::ClientResponse; @@ -30,7 +32,6 @@ where /// Test `ClientResponse` builder pub struct TestResponse { head: ResponseHead, - #[cfg(feature = "cookies")] cookies: CookieJar, payload: Option, } @@ -39,7 +40,6 @@ impl Default for TestResponse { fn default() -> TestResponse { TestResponse { head: ResponseHead::default(), - #[cfg(feature = "cookies")] cookies: CookieJar::new(), payload: None, } @@ -87,7 +87,6 @@ impl TestResponse { } /// Set cookie for this response - #[cfg(feature = "cookies")] pub fn cookie<'a>(mut self, cookie: Cookie<'a>) -> Self { self.cookies.add(cookie.into_owned()); self @@ -105,25 +104,17 @@ impl TestResponse { pub fn finish(self) -> ClientResponse { let mut head = self.head; - #[cfg(feature = "cookies")] - { - use std::fmt::Write as FmtWrite; - - use actix_http::http::header::{self, HeaderValue}; - use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; - - let mut cookie = String::new(); - for c in self.cookies.delta() { - let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); - let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); - let _ = write!(&mut cookie, "; {}={}", name, value); - } - if !cookie.is_empty() { - head.headers.insert( - header::SET_COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), - ); - } + let mut cookie = String::new(); + for c in self.cookies.delta() { + let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); + let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); + let _ = write!(&mut cookie, "; {}={}", name, value); + } + if !cookie.is_empty() { + head.headers.insert( + header::SET_COOKIE, + HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), + ); } if let Some(pl) = self.payload { diff --git a/awc/src/ws.rs b/awc/src/ws.rs index bc023c069..bbeaa061b 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -1,14 +1,15 @@ //! Websockets client +use std::fmt::Write as FmtWrite; use std::io::Write; use std::rc::Rc; use std::{fmt, str}; use actix_codec::Framed; +use actix_http::cookie::{Cookie, CookieJar}; use actix_http::{ws, Payload, RequestHead}; use bytes::{BufMut, BytesMut}; -#[cfg(feature = "cookies")] -use cookie::{Cookie, CookieJar}; use futures::future::{err, Either, Future}; +use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use tokio_timer::Timeout; pub use actix_http::ws::{CloseCode, CloseReason, Codec, Frame, Message}; @@ -33,7 +34,6 @@ pub struct WebsocketsRequest { max_size: usize, server_mode: bool, default_headers: bool, - #[cfg(feature = "cookies")] cookies: Option, config: Rc, } @@ -62,7 +62,6 @@ impl WebsocketsRequest { protocols: None, max_size: 65_536, server_mode: false, - #[cfg(feature = "cookies")] cookies: None, default_headers: true, } @@ -82,7 +81,6 @@ impl WebsocketsRequest { self } - #[cfg(feature = "cookies")] /// Set a cookie pub fn cookie<'c>(mut self, cookie: Cookie<'c>) -> Self { if self.cookies.is_none() { @@ -264,28 +262,20 @@ impl WebsocketsRequest { self }; - #[allow(unused_mut)] let mut head = slf.head; - #[cfg(feature = "cookies")] - { - use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; - use std::fmt::Write; - - // set cookies - if let Some(ref mut jar) = slf.cookies { - let mut cookie = String::new(); - for c in jar.delta() { - let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); - let value = - percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); - let _ = write!(&mut cookie, "; {}={}", name, value); - } - head.headers.insert( - header::COOKIE, - HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), - ); + // set cookies + if let Some(ref mut jar) = slf.cookies { + let mut cookie = String::new(); + for c in jar.delta() { + let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); + let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); + let _ = write!(&mut cookie, "; {}={}", name, value); } + head.headers.insert( + header::COOKIE, + HeaderValue::from_str(&cookie.as_str()[2..]).unwrap(), + ); } // origin diff --git a/src/lib.rs b/src/lib.rs index 7a4f4bfbb..2bef6a526 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -108,7 +108,7 @@ pub use actix_web_codegen::*; // re-export for convenience pub use actix_http::Response as HttpResponse; -pub use actix_http::{http, Error, HttpMessage, ResponseError, Result}; +pub use actix_http::{cookie, http, Error, HttpMessage, ResponseError, Result}; pub use crate::app::App; pub use crate::extract::FromRequest; diff --git a/src/middleware/identity.rs b/src/middleware/identity.rs index e94f99db1..34979e167 100644 --- a/src/middleware/identity.rs +++ b/src/middleware/identity.rs @@ -51,11 +51,11 @@ use std::cell::RefCell; use std::rc::Rc; use actix_service::{Service, Transform}; -use cookie::{Cookie, CookieJar, Key, SameSite}; use futures::future::{ok, Either, FutureResult}; use futures::{Future, IntoFuture, Poll}; use time::Duration; +use crate::cookie::{Cookie, CookieJar, Key, SameSite}; use crate::error::{Error, Result}; use crate::http::header::{self, HeaderValue}; use crate::request::HttpRequest; diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 037d00067..6b6253fbc 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -18,5 +18,5 @@ mod logger; pub use self::defaultheaders::DefaultHeaders; pub use self::logger::Logger; -#[cfg(feature = "cookies")] +#[cfg(feature = "secure-cookies")] pub mod identity; diff --git a/src/request.rs b/src/request.rs index 33b63acd7..c524d4978 100644 --- a/src/request.rs +++ b/src/request.rs @@ -263,14 +263,12 @@ mod tests { } #[test] - #[cfg(feature = "cookies")] fn test_no_request_cookies() { let req = TestRequest::default().to_http_request(); assert!(req.cookies().unwrap().is_empty()); } #[test] - #[cfg(feature = "cookies")] fn test_request_cookies() { let req = TestRequest::default() .header(header::COOKIE, "cookie1=value1") diff --git a/src/test.rs b/src/test.rs index 4fdb6ea38..a9aa22789 100644 --- a/src/test.rs +++ b/src/test.rs @@ -2,6 +2,7 @@ use std::cell::RefCell; use std::rc::Rc; +use actix_http::cookie::Cookie; use actix_http::http::header::{Header, HeaderName, IntoHeaderValue}; use actix_http::http::{HttpTryFrom, Method, StatusCode, Version}; use actix_http::test::TestRequest as HttpTestRequest; @@ -11,8 +12,6 @@ use actix_rt::Runtime; use actix_server_config::ServerConfig; use actix_service::{FnService, IntoNewService, NewService, Service}; use bytes::Bytes; -#[cfg(feature = "cookies")] -use cookie::Cookie; use futures::future::{lazy, Future}; use crate::config::{AppConfig, AppConfigInner}; @@ -285,7 +284,6 @@ impl TestRequest { self } - #[cfg(feature = "cookies")] /// Set cookie for this request pub fn cookie(mut self, cookie: Cookie) -> Self { self.req.cookie(cookie); diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index a9b58483c..97947230f 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -17,17 +17,14 @@ edition = "2018" workspace = ".." [package.metadata.docs.rs] -features = ["session"] +features = [] [lib] name = "actix_http_test" path = "src/lib.rs" [features] -default = ["session"] - -# sessions feature, session require "ring" crate and c compiler -session = ["cookie/secure"] +default = [] # openssl ssl = ["openssl", "actix-server/ssl", "awc/ssl"] @@ -42,7 +39,6 @@ awc = { path = "../awc" } base64 = "0.10" bytes = "0.4" -cookie = { version="0.11", features=["percent-encode"] } futures = "0.1" http = "0.1.8" log = "0.4" @@ -60,4 +56,5 @@ tokio-timer = "0.2" openssl = { version="0.10", optional = true } [dev-dependencies] -actix-web = "1.0.0-alpha.1" +actix-web = { path = ".." } +actix-http = { path = "../actix-http" } diff --git a/test-server/src/lib.rs b/test-server/src/lib.rs index 9eec065cd..75f75b1e9 100644 --- a/test-server/src/lib.rs +++ b/test-server/src/lib.rs @@ -23,7 +23,7 @@ use net2::TcpBuilder; /// use actix_http::HttpService; /// use actix_http_test::TestServer; /// use actix_web::{web, App, HttpResponse}; -/// # +/// /// fn my_handler() -> HttpResponse { /// HttpResponse::Ok().into() /// }