diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /third_party/rust/cookie/src | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | third_party/rust/cookie/src/builder.rs | 246 | ||||
-rw-r--r-- | third_party/rust/cookie/src/delta.rs | 65 | ||||
-rw-r--r-- | third_party/rust/cookie/src/draft.rs | 108 | ||||
-rw-r--r-- | third_party/rust/cookie/src/expiration.rs | 137 | ||||
-rw-r--r-- | third_party/rust/cookie/src/jar.rs | 728 | ||||
-rw-r--r-- | third_party/rust/cookie/src/lib.rs | 1422 | ||||
-rw-r--r-- | third_party/rust/cookie/src/parse.rs | 550 | ||||
-rw-r--r-- | third_party/rust/cookie/src/secure/key.rs | 301 | ||||
-rw-r--r-- | third_party/rust/cookie/src/secure/macros.rs | 49 | ||||
-rw-r--r-- | third_party/rust/cookie/src/secure/mod.rs | 14 | ||||
-rw-r--r-- | third_party/rust/cookie/src/secure/private.rs | 264 | ||||
-rw-r--r-- | third_party/rust/cookie/src/secure/signed.rs | 251 |
12 files changed, 4135 insertions, 0 deletions
diff --git a/third_party/rust/cookie/src/builder.rs b/third_party/rust/cookie/src/builder.rs new file mode 100644 index 0000000000..78d1219a9a --- /dev/null +++ b/third_party/rust/cookie/src/builder.rs @@ -0,0 +1,246 @@ +use std::borrow::Cow; + +use crate::{Cookie, SameSite, Expiration}; + +/// Structure that follows the builder pattern for building `Cookie` structs. +/// +/// To construct a cookie: +/// +/// 1. Call [`Cookie::build`] to start building. +/// 2. Use any of the builder methods to set fields in the cookie. +/// 3. Call [`CookieBuilder::finish()`] to retrieve the built cookie. +/// +/// # Example +/// +/// ```rust +/// # extern crate cookie; +/// use cookie::Cookie; +/// use cookie::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<'c> { + /// The cookie being built. + cookie: Cookie<'c>, +} + +impl<'c> CookieBuilder<'c> { + /// Creates a new `CookieBuilder` instance from the given name and value. + /// + /// This method is typically called indirectly via [`Cookie::build()`]. + /// + /// # Example + /// + /// ```rust + /// use cookie::Cookie; + /// + /// let c = Cookie::build("foo", "bar").finish(); + /// assert_eq!(c.name_value(), ("foo", "bar")); + /// ``` + pub fn new<N, V>(name: N, value: V) -> Self + where N: Into<Cow<'c, str>>, + V: Into<Cow<'c, str>> + { + CookieBuilder { cookie: Cookie::new(name, value) } + } + + /// Sets the `expires` field in the cookie being built. + /// + /// # Example + /// + /// ```rust + /// # extern crate cookie; + /// use cookie::{Cookie, Expiration}; + /// use cookie::time::OffsetDateTime; + /// + /// # fn main() { + /// let c = Cookie::build("foo", "bar") + /// .expires(OffsetDateTime::now_utc()) + /// .finish(); + /// + /// assert!(c.expires().is_some()); + /// + /// let c = Cookie::build("foo", "bar") + /// .expires(None) + /// .finish(); + /// + /// assert_eq!(c.expires(), Some(Expiration::Session)); + /// # } + /// ``` + #[inline] + pub fn expires<E: Into<Expiration>>(mut self, when: E) -> Self { + self.cookie.set_expires(when); + self + } + + /// Sets the `max_age` field in the cookie being built. + /// + /// # Example + /// + /// ```rust + /// # extern crate cookie; + /// use cookie::Cookie; + /// use cookie::time::Duration; + /// + /// # fn main() { + /// let c = Cookie::build("foo", "bar") + /// .max_age(Duration::minutes(30)) + /// .finish(); + /// + /// assert_eq!(c.max_age(), Some(Duration::seconds(30 * 60))); + /// # } + /// ``` + #[inline] + pub fn max_age(mut self, value: time::Duration) -> Self { + self.cookie.set_max_age(value); + self + } + + /// Sets the `domain` field in the cookie being built. + /// + /// # Example + /// + /// ```rust + /// use 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<D: Into<Cow<'c, str>>>(mut self, value: D) -> Self { + self.cookie.set_domain(value); + self + } + + /// Sets the `path` field in the cookie being built. + /// + /// # Example + /// + /// ```rust + /// use cookie::Cookie; + /// + /// let c = Cookie::build("foo", "bar") + /// .path("/") + /// .finish(); + /// + /// assert_eq!(c.path(), Some("/")); + /// ``` + pub fn path<P: Into<Cow<'c, str>>>(mut self, path: P) -> Self { + self.cookie.set_path(path); + self + } + + /// Sets the `secure` field in the cookie being built. + /// + /// # Example + /// + /// ```rust + /// use 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) -> Self { + self.cookie.set_secure(value); + self + } + + /// Sets the `http_only` field in the cookie being built. + /// + /// # Example + /// + /// ```rust + /// use 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) -> Self { + self.cookie.set_http_only(value); + self + } + + /// Sets the `same_site` field in the cookie being built. + /// + /// # Example + /// + /// ```rust + /// use 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) -> Self { + 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 + /// # extern crate cookie; + /// use cookie::Cookie; + /// use cookie::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) -> Self { + self.cookie.make_permanent(); + self + } + + /// Finishes building and returns the built `Cookie`. + /// + /// # Example + /// + /// ```rust + /// use 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<'c> { + self.cookie + } +} diff --git a/third_party/rust/cookie/src/delta.rs b/third_party/rust/cookie/src/delta.rs new file mode 100644 index 0000000000..3ef35fadd7 --- /dev/null +++ b/third_party/rust/cookie/src/delta.rs @@ -0,0 +1,65 @@ +use std::ops::{Deref, DerefMut}; +use std::hash::{Hash, Hasher}; +use std::borrow::Borrow; + +use crate::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<H: Hasher>(&self, state: &mut H) { + self.name().hash(state); + } +} + +impl Borrow<str> for DeltaCookie { + fn borrow(&self) -> &str { + self.name() + } +} diff --git a/third_party/rust/cookie/src/draft.rs b/third_party/rust/cookie/src/draft.rs new file mode 100644 index 0000000000..e625e81c90 --- /dev/null +++ b/third_party/rust/cookie/src/draft.rs @@ -0,0 +1,108 @@ +//! 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 "None", the cookie is sent in all cross-site +/// requests if the "Secure" flag is also set, otherwise the cookie is ignored. +/// This library automatically sets the "Secure" flag on cookies when +/// `same_site` is set to `SameSite::None` as long as `secure` is not explicitly +/// set to `false`. +/// +/// If the `SameSite` attribute is not present (by not setting `SameSite` +/// initally or passing `None` to [`Cookie::set_same_site()`]), then the cookie +/// will be sent as normal. +/// +/// **Note:** This cookie attribute is an [HTTP draft]! Its meaning and +/// definition are subject to change. +/// +/// [`Cookie::set_same_site()`]: crate::Cookie::set_same_site() +/// [HTTP draft]: https://tools.ietf.org/html/draft-west-cookie-incrementalism-00 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SameSite { + /// The "Strict" `SameSite` attribute. + Strict, + /// The "Lax" `SameSite` attribute. + Lax, + /// The "None" `SameSite` attribute. + None +} + +impl SameSite { + /// Returns `true` if `self` is `SameSite::Strict` and `false` otherwise. + /// + /// # Example + /// + /// ```rust + /// use 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 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 cookie::SameSite; + /// + /// let none = SameSite::None; + /// assert!(none.is_none()); + /// assert!(!none.is_lax()); + /// assert!(!none.is_strict()); + /// ``` + #[inline] + pub fn is_none(&self) -> bool { + match *self { + SameSite::None => true, + SameSite::Lax | SameSite::Strict => false + } + } +} + +impl fmt::Display for SameSite { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + SameSite::Strict => write!(f, "Strict"), + SameSite::Lax => write!(f, "Lax"), + SameSite::None => write!(f, "None"), + } + } +} diff --git a/third_party/rust/cookie/src/expiration.rs b/third_party/rust/cookie/src/expiration.rs new file mode 100644 index 0000000000..2ff2885483 --- /dev/null +++ b/third_party/rust/cookie/src/expiration.rs @@ -0,0 +1,137 @@ +use time::OffsetDateTime; + +/// A cookie's expiration: either session or a date-time. +/// +/// An `Expiration` is constructible via `Expiration::from()` with an +/// `Option<OffsetDateTime>` or an `OffsetDateTime`: +/// +/// * `None` -> `Expiration::Session` +/// * `Some(OffsetDateTime)` -> `Expiration::DateTime` +/// * `OffsetDateTime` -> `Expiration::DateTime` +/// +/// ```rust +/// use cookie::Expiration; +/// use time::OffsetDateTime; +/// +/// let expires = Expiration::from(None); +/// assert_eq!(expires, Expiration::Session); +/// +/// let now = OffsetDateTime::now_utc(); +/// let expires = Expiration::from(now); +/// assert_eq!(expires, Expiration::DateTime(now)); +/// +/// let expires = Expiration::from(Some(now)); +/// assert_eq!(expires, Expiration::DateTime(now)); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Expiration { + /// Expiration for a "permanent" cookie at a specific date-time. + DateTime(OffsetDateTime), + /// Expiration for a "session" cookie. Browsers define the notion of a + /// "session" and will automatically expire session cookies when they deem + /// the "session" to be over. This is typically, but need not be, when the + /// browser is closed. + Session, +} + +impl Expiration { + /// Returns `true` if `self` is an `Expiration::DateTime`. + /// + /// # Example + /// + /// ```rust + /// use cookie::Expiration; + /// use time::OffsetDateTime; + /// + /// let expires = Expiration::from(None); + /// assert!(!expires.is_datetime()); + /// + /// let expires = Expiration::from(OffsetDateTime::now_utc()); + /// assert!(expires.is_datetime()); + /// ``` + pub fn is_datetime(&self) -> bool { + match self { + Expiration::DateTime(_) => true, + Expiration::Session => false + } + } + + /// Returns `true` if `self` is an `Expiration::Session`. + /// + /// # Example + /// + /// ```rust + /// use cookie::Expiration; + /// use time::OffsetDateTime; + /// + /// let expires = Expiration::from(None); + /// assert!(expires.is_session()); + /// + /// let expires = Expiration::from(OffsetDateTime::now_utc()); + /// assert!(!expires.is_session()); + /// ``` + pub fn is_session(&self) -> bool { + match self { + Expiration::DateTime(_) => false, + Expiration::Session => true + } + } + + /// Returns the inner `OffsetDateTime` if `self` is a `DateTime`. + /// + /// # Example + /// + /// ```rust + /// use cookie::Expiration; + /// use time::OffsetDateTime; + /// + /// let expires = Expiration::from(None); + /// assert!(expires.datetime().is_none()); + /// + /// let now = OffsetDateTime::now_utc(); + /// let expires = Expiration::from(now); + /// assert_eq!(expires.datetime(), Some(now)); + /// ``` + pub fn datetime(self) -> Option<OffsetDateTime> { + match self { + Expiration::Session => None, + Expiration::DateTime(v) => Some(v) + } + } + + /// Applied `f` to the inner `OffsetDateTime` if `self` is a `DateTime` and + /// returns the mapped `Expiration`. + /// + /// # Example + /// + /// ```rust + /// use cookie::Expiration; + /// use time::{OffsetDateTime, Duration}; + /// + /// let now = OffsetDateTime::now_utc(); + /// let one_week = Duration::weeks(1); + /// + /// let expires = Expiration::from(now); + /// assert_eq!(expires.map(|t| t + one_week).datetime(), Some(now + one_week)); + /// + /// let expires = Expiration::from(None); + /// assert_eq!(expires.map(|t| t + one_week).datetime(), None); + /// ``` + pub fn map<F>(self, f: F) -> Self + where F: FnOnce(OffsetDateTime) -> OffsetDateTime + { + match self { + Expiration::Session => Expiration::Session, + Expiration::DateTime(v) => Expiration::DateTime(f(v)), + } + } +} + +impl<T: Into<Option<OffsetDateTime>>> From<T> for Expiration { + fn from(option: T) -> Self { + match option.into() { + Some(value) => Expiration::DateTime(value), + None => Expiration::Session + } + } +} diff --git a/third_party/rust/cookie/src/jar.rs b/third_party/rust/cookie/src/jar.rs new file mode 100644 index 0000000000..83f2a15fdc --- /dev/null +++ b/third_party/rust/cookie/src/jar.rs @@ -0,0 +1,728 @@ +use std::collections::HashSet; + +#[cfg(feature = "signed")] use crate::secure::SignedJar; +#[cfg(feature = "private")] use crate::secure::PrivateJar; +#[cfg(any(feature = "signed", feature = "private"))] use crate::secure::Key; + +use crate::delta::DeltaCookie; +use crate::Cookie; + +/// A collection of cookies that tracks its modifications. +/// +/// A `CookieJar` provides storage for any number of cookies. Any changes made +/// to the jar are tracked; the changes can be retrieved via the +/// [delta](#method.delta) method which returns an iterator over the changes. +/// +/// # Usage +/// +/// A jar's life begins via [new](#method.new) and calls to +/// [`add_original`](#method.add_original): +/// +/// ```rust +/// use 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 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 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<DeltaCookie>, + delta_cookies: HashSet<DeltaCookie>, +} + +impl CookieJar { + /// Creates an empty cookie jar. + /// + /// # Example + /// + /// ```rust + /// use 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 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 { None } else { Some(&c.cookie) }) + } + + /// 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 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 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. See also [`Cookie::make_removal()`]. + /// + /// # Example + /// + /// Removing an _original_ cookie results in a _removal_ cookie: + /// + /// ```rust + /// # extern crate cookie; + /// use cookie::{CookieJar, Cookie}; + /// use cookie::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 unless + /// there's an original cookie with the same name: + /// + /// ```rust + /// use 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); + /// + /// jar.add_original(Cookie::new("name", "value")); + /// jar.add(Cookie::new("name", "value")); + /// assert_eq!(jar.delta().count(), 1); + /// + /// jar.remove(Cookie::named("name")); + /// assert_eq!(jar.delta().count(), 1); + /// ``` + pub fn remove(&mut self, mut cookie: Cookie<'static>) { + if self.original_cookies.contains(cookie.name()) { + cookie.make_removal(); + 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 + /// # extern crate cookie; + /// use cookie::{CookieJar, Cookie}; + /// use cookie::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::named("name")); + /// assert_eq!(jar.delta().count(), 1); + /// assert_eq!(jar.iter().count(), 1); + /// + /// // Now force remove the new cookie. + /// jar.force_remove(&Cookie::named("key")); + /// 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 delta cookies, i.e. all cookies not added via + /// [`CookieJar::add_original()`], from this `CookieJar`. This undoes any + /// changes from [`CookieJar::add()`] and [`CookieJar::remove()`] + /// operations. + /// + /// # Example + /// + /// ```rust + /// use cookie::{CookieJar, Cookie}; + /// + /// let mut jar = CookieJar::new(); + /// + /// // Only original cookies will remain after calling `reset_delta`. + /// jar.add_original(Cookie::new("name", "value")); + /// jar.add_original(Cookie::new("language", "Rust")); + /// + /// // These operations, represented by delta cookies, will be reset. + /// jar.add(Cookie::new("language", "C++")); + /// jar.remove(Cookie::named("name")); + /// + /// // All is normal. + /// assert_eq!(jar.get("name"), None); + /// assert_eq!(jar.get("language").map(Cookie::value), Some("C++")); + /// assert_eq!(jar.iter().count(), 1); + /// assert_eq!(jar.delta().count(), 2); + /// + /// // Resetting undoes delta operations. + /// jar.reset_delta(); + /// assert_eq!(jar.get("name").map(Cookie::value), Some("value")); + /// assert_eq!(jar.get("language").map(Cookie::value), Some("Rust")); + /// assert_eq!(jar.iter().count(), 2); + /// assert_eq!(jar.delta().count(), 0); + /// ``` + pub fn reset_delta(&mut self) { + self.delta_cookies = HashSet::new(); + } + + /// 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 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 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 read-only `PrivateJar` with `self` as its parent jar using the + /// key `key` to verify/decrypt cookies retrieved from the child jar. Any + /// retrievals from the child jar will be made from the parent jar. + /// + /// # Example + /// + /// ```rust + /// use 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_mut(&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 = "private")] + #[cfg_attr(all(nightly, doc), doc(cfg(feature = "private")))] + pub fn private<'a>(&'a self, key: &Key) -> PrivateJar<&'a Self> { + PrivateJar::new(self, key) + } + + /// Returns a read/write `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. + /// + /// # Example + /// + /// ```rust + /// use 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_mut(&key).add(Cookie::new("private", "text")); + /// + /// // Remove a cookie using the child jar. + /// jar.private_mut(&key).remove(Cookie::named("private")); + /// ``` + #[cfg(feature = "private")] + #[cfg_attr(all(nightly, doc), doc(cfg(feature = "private")))] + pub fn private_mut<'a>(&'a mut self, key: &Key) -> PrivateJar<&'a mut Self> { + PrivateJar::new(self, key) + } + + /// Returns a read-only `SignedJar` with `self` as its parent jar using the + /// key `key` to verify cookies retrieved from the child jar. Any retrievals + /// from the child jar will be made from the parent jar. + /// + /// # Example + /// + /// ```rust + /// use cookie::{Cookie, CookieJar, Key}; + /// + /// // Generate a secure key. + /// let key = Key::generate(); + /// + /// // Add a signed cookie. + /// let mut jar = CookieJar::new(); + /// jar.signed_mut(&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 = "signed")] + #[cfg_attr(all(nightly, doc), doc(cfg(feature = "signed")))] + pub fn signed<'a>(&'a self, key: &Key) -> SignedJar<&'a Self> { + SignedJar::new(self, key) + } + + /// Returns a read/write `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. + /// + /// # Example + /// + /// ```rust + /// use cookie::{Cookie, CookieJar, Key}; + /// + /// // Generate a secure key. + /// let key = Key::generate(); + /// + /// // Add a signed cookie. + /// let mut jar = CookieJar::new(); + /// jar.signed_mut(&key).add(Cookie::new("signed", "text")); + /// + /// // Remove a cookie. + /// jar.signed_mut(&key).remove(Cookie::named("signed")); + /// ``` + #[cfg(feature = "signed")] + #[cfg_attr(all(nightly, doc), doc(cfg(feature = "signed")))] + pub fn signed_mut<'a>(&'a mut self, key: &Key) -> SignedJar<&'a mut Self> { + 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_set::Difference; +use std::collections::hash_map::RandomState; +use std::iter::Chain; + +/// Iterator over all of the cookies in a jar. +pub struct Iter<'a> { + delta_cookies: Chain<HashSetIter<'a, DeltaCookie>, 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::CookieJar; + use crate::Cookie; + + #[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.remove(Cookie::named("test2")); + c.remove(Cookie::named("test3")); + + 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: Send>(_: T) -> bool { + true + } + + assert!(is_send(CookieJar::new())) + } + + #[test] + #[cfg(all(feature = "signed", feature = "private"))] + fn iter() { + let key = crate::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_mut(&key).add(Cookie::new("signed", "signed")); + c.private_mut(&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] + 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/third_party/rust/cookie/src/lib.rs b/third_party/rust/cookie/src/lib.rs new file mode 100644 index 0000000000..392728efbc --- /dev/null +++ b/third_party/rust/cookie/src/lib.rs @@ -0,0 +1,1422 @@ +//! HTTP cookie parsing and cookie jar management. +//! +//! This crates provides the [`Cookie`] type, representing an HTTP cookie, and +//! the [`CookieJar`] type, which manages a collection of cookies for session +//! management, recording changes as they are made, and optional automatic +//! cookie encryption and signing. +//! +//! # Usage +//! +//! Add the following to the `[dependencies]` section of your `Cargo.toml`: +//! +//! ```toml +//! cookie = "0.16" +//! ``` +//! +//! # Features +//! +//! This crate exposes several features, all of which are disabled by default: +//! +//! * **`percent-encode`** +//! +//! Enables _percent encoding and decoding_ of names and values in cookies. +//! +//! When this feature is enabled, the [`Cookie::encoded()`] and +//! [`Cookie::parse_encoded()`] methods are 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. +//! +//! * **`signed`** +//! +//! Enables _signed_ cookies via [`CookieJar::signed()`]. +//! +//! When this feature is enabled, the [`CookieJar::signed()`] method, +//! [`SignedJar`] type, and [`Key`] type are available. The jar acts as "child +//! jar"; operations on the jar automatically sign and verify cookies as they +//! are added and retrieved from the parent jar. +//! +//! * **`private`** +//! +//! Enables _private_ (authenticated, encrypted) cookies via +//! [`CookieJar::private()`]. +//! +//! When this feature is enabled, the [`CookieJar::private()`] method, +//! [`PrivateJar`] type, and [`Key`] type are available. The jar acts as "child +//! jar"; operations on the jar automatically encrypt and decrypt/authenticate +//! cookies as they are added and retrieved from the parent jar. +//! +//! * **`key-expansion`** +//! +//! Enables _key expansion_ or _key derivation_ via [`Key::derive_from()`]. +//! +//! When this feature is enabled, and either `signed` or `private` are _also_ +//! enabled, the [`Key::derive_from()`] method is available. The method can be +//! used to derive a `Key` structure appropriate for use with signed and +//! private jars from cryptographically valid key material that is shorter in +//! length than the full key. +//! +//! * **`secure`** +//! +//! A meta-feature that simultaneously enables `signed`, `private`, and +//! `key-expansion`. +//! +//! You can enable features via `Cargo.toml`: +//! +//! ```toml +//! [dependencies.cookie] +//! features = ["secure", "percent-encode"] +//! ``` + +#![cfg_attr(all(nightly, doc), feature(doc_cfg))] + +#![doc(html_root_url = "https://docs.rs/cookie/0.16")] +#![deny(missing_docs)] + +pub use time; + +mod builder; +mod parse; +mod jar; +mod delta; +mod draft; +mod expiration; + +#[cfg(any(feature = "private", feature = "signed"))] #[macro_use] mod secure; +#[cfg(any(feature = "private", feature = "signed"))] pub use secure::*; + +use std::borrow::Cow; +use std::fmt; +use std::str::FromStr; + +#[allow(unused_imports, deprecated)] +use std::ascii::AsciiExt; + +use time::{Duration, OffsetDateTime, UtcOffset, macros::datetime}; + +use crate::parse::parse_cookie; +pub use crate::parse::ParseError; +pub use crate::builder::CookieBuilder; +pub use crate::jar::{CookieJar, Delta, Iter}; +pub use crate::draft::*; +pub use crate::expiration::*; + +#[derive(Debug, Clone)] +enum CookieStr<'c> { + /// An string derived from indexes (start, end). + Indexed(usize, usize), + /// A string derived from a concrete string. + Concrete(Cow<'c, str>), +} + +impl<'c> CookieStr<'c> { + /// 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<str>>) -> &'s str { + match *self { + CookieStr::Indexed(i, j) => { + let s = string.expect("`Some` base string must exist when \ + converting indexed str to str! (This is a module invariant.)"); + &s[i..j] + }, + CookieStr::Concrete(ref cstr) => &*cstr, + } + } + + #[allow(clippy::ptr_arg)] + fn to_raw_str<'s, 'b: 's>(&'s self, string: &'s Cow<'b, str>) -> Option<&'b str> { + match *self { + CookieStr::Indexed(i, j) => { + match *string { + Cow::Borrowed(s) => Some(&s[i..j]), + Cow::Owned(_) => None, + } + }, + CookieStr::Concrete(_) => None, + } + } + + fn into_owned(self) -> CookieStr<'static> { + use crate::CookieStr::*; + + match self { + Indexed(a, b) => Indexed(a, b), + Concrete(Cow::Owned(c)) => Concrete(Cow::Owned(c)), + Concrete(Cow::Borrowed(c)) => Concrete(Cow::Owned(c.into())), + } + } +} + +/// Representation of an HTTP cookie. +/// +/// # Constructing a `Cookie` +/// +/// To construct a cookie with only a name/value, use [`Cookie::new()`]: +/// +/// ```rust +/// use cookie::Cookie; +/// +/// let cookie = Cookie::new("name", "value"); +/// assert_eq!(&cookie.to_string(), "name=value"); +/// ``` +/// +/// To construct more elaborate cookies, use [`Cookie::build()`] and +/// [`CookieBuilder`] methods: +/// +/// ```rust +/// use 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<Cow<'c, str>>, + /// The cookie's name. + name: CookieStr<'c>, + /// The cookie's value. + value: CookieStr<'c>, + /// The cookie's expiration, if any. + expires: Option<Expiration>, + /// The cookie's maximum age, if any. + max_age: Option<Duration>, + /// The cookie's domain, if any. + domain: Option<CookieStr<'c>>, + /// The cookie's path domain, if any. + path: Option<CookieStr<'c>>, + /// Whether this cookie was marked Secure. + secure: Option<bool>, + /// Whether this cookie was marked HttpOnly. + http_only: Option<bool>, + /// The draft `SameSite` attribute. + same_site: Option<SameSite>, +} + +impl<'c> Cookie<'c> { + /// Creates a new `Cookie` with the given name and value. + /// + /// # Example + /// + /// ```rust + /// use cookie::Cookie; + /// + /// let cookie = Cookie::new("name", "value"); + /// assert_eq!(cookie.name_value(), ("name", "value")); + /// ``` + pub fn new<N, V>(name: N, value: V) -> Self + where N: Into<Cow<'c, str>>, + V: Into<Cow<'c, str>> + { + 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 cookie::Cookie; + /// + /// let cookie = Cookie::named("name"); + /// assert_eq!(cookie.name(), "name"); + /// assert!(cookie.value().is_empty()); + /// ``` + pub fn named<N>(name: N) -> Cookie<'c> + where N: Into<Cow<'c, str>> + { + Cookie::new(name, "") + } + + /// Creates a new `CookieBuilder` instance from the given key and value + /// strings. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let c = Cookie::build("foo", "bar").finish(); + /// assert_eq!(c.name_value(), ("foo", "bar")); + /// ``` + pub fn build<N, V>(name: N, value: V) -> CookieBuilder<'c> + where N: Into<Cow<'c, str>>, + V: Into<Cow<'c, str>> + { + CookieBuilder::new(name, value) + } + + /// Parses a `Cookie` from the given HTTP cookie header value string. Does + /// not perform any percent-decoding. + /// + /// # Example + /// + /// ``` + /// use 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: S) -> Result<Cookie<'c>, ParseError> + where S: Into<Cow<'c, str>> + { + 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. + /// + /// # Example + /// + /// ``` + /// use 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)); + /// ``` + #[cfg(feature = "percent-encode")] + #[cfg_attr(all(nightly, doc), doc(cfg(feature = "percent-encode")))] + pub fn parse_encoded<S>(s: S) -> Result<Cookie<'c>, ParseError> + where S: Into<Cow<'c, str>> + { + parse_cookie(s, true) + } + + /// Converts `self` into a `Cookie` with a static lifetime with as few + /// allocations as possible. + /// + /// # Example + /// + /// ``` + /// use 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.into_owned(), + value: self.value.into_owned(), + expires: self.expires, + max_age: self.max_age, + domain: self.domain.map(|s| s.into_owned()), + path: self.path.map(|s| s.into_owned()), + secure: self.secure, + http_only: self.http_only, + same_site: self.same_site, + } + } + + /// Returns the name of `self`. + /// + /// # Example + /// + /// ``` + /// use 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 + /// + /// ``` + /// use 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 + /// + /// ``` + /// use 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 + /// + /// ``` + /// use 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<bool> { + 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 + /// + /// ``` + /// use 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<bool> { + self.secure + } + + /// Returns the `SameSite` attribute of this cookie if one was specified. + /// + /// # Example + /// + /// ``` + /// use 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<SameSite> { + self.same_site + } + + /// Returns the specified max-age of the cookie if one was specified. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let c = Cookie::parse("name=value").unwrap(); + /// assert_eq!(c.max_age(), None); + /// + /// let c = Cookie::parse("name=value; Max-Age=3600").unwrap(); + /// assert_eq!(c.max_age().map(|age| age.whole_hours()), Some(1)); + /// ``` + #[inline] + pub fn max_age(&self) -> Option<Duration> { + self.max_age + } + + /// Returns the `Path` of the cookie if one was specified. + /// + /// # Example + /// + /// ``` + /// use 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 + /// + /// ``` + /// use 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 [`Expiration`] of the cookie if one was specified. + /// + /// # Example + /// + /// ``` + /// use cookie::{Cookie, Expiration}; + /// + /// let c = Cookie::parse("name=value").unwrap(); + /// assert_eq!(c.expires(), None); + /// + /// // Here, `cookie.expires_datetime()` returns `None`. + /// let c = Cookie::build("name", "value").expires(None).finish(); + /// assert_eq!(c.expires(), Some(Expiration::Session)); + /// + /// 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().and_then(|e| e.datetime()).map(|t| t.year()), Some(2017)); + /// ``` + #[inline] + pub fn expires(&self) -> Option<Expiration> { + self.expires + } + + /// Returns the expiration date-time of the cookie if one was specified. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let c = Cookie::parse("name=value").unwrap(); + /// assert_eq!(c.expires_datetime(), None); + /// + /// // Here, `cookie.expires()` returns `Some`. + /// let c = Cookie::build("name", "value").expires(None).finish(); + /// assert_eq!(c.expires_datetime(), 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_datetime().map(|t| t.year()), Some(2017)); + /// ``` + #[inline] + pub fn expires_datetime(&self) -> Option<OffsetDateTime> { + self.expires.and_then(|e| e.datetime()) + } + + /// Sets the name of `self` to `name`. + /// + /// # Example + /// + /// ``` + /// use 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<N: Into<Cow<'c, str>>>(&mut self, name: N) { + self.name = CookieStr::Concrete(name.into()) + } + + /// Sets the value of `self` to `value`. + /// + /// # Example + /// + /// ``` + /// use 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<V: Into<Cow<'c, str>>>(&mut self, value: V) { + self.value = CookieStr::Concrete(value.into()) + } + + /// Sets the value of `http_only` in `self` to `value`. If `value` is + /// `None`, the field is unset. + /// + /// # Example + /// + /// ``` + /// use 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)); + /// + /// c.set_http_only(false); + /// assert_eq!(c.http_only(), Some(false)); + /// + /// c.set_http_only(None); + /// assert_eq!(c.http_only(), None); + /// ``` + #[inline] + pub fn set_http_only<T: Into<Option<bool>>>(&mut self, value: T) { + self.http_only = value.into(); + } + + /// Sets the value of `secure` in `self` to `value`. If `value` is `None`, + /// the field is unset. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.secure(), None); + /// + /// c.set_secure(true); + /// assert_eq!(c.secure(), Some(true)); + /// + /// c.set_secure(false); + /// assert_eq!(c.secure(), Some(false)); + /// + /// c.set_secure(None); + /// assert_eq!(c.secure(), None); + /// ``` + #[inline] + pub fn set_secure<T: Into<Option<bool>>>(&mut self, value: T) { + self.secure = value.into(); + } + + /// Sets the value of `same_site` in `self` to `value`. If `value` is + /// `None`, the field is unset. If `value` is `SameSite::None`, the "Secure" + /// flag will be set when the cookie is written out unless `secure` is + /// explicitly set to `false` via [`Cookie::set_secure()`] or the equivalent + /// builder method. + /// + /// [HTTP draft]: https://tools.ietf.org/html/draft-west-cookie-incrementalism-00 + /// + /// # Example + /// + /// ``` + /// use cookie::{Cookie, SameSite}; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.same_site(), None); + /// + /// c.set_same_site(SameSite::None); + /// assert_eq!(c.same_site(), Some(SameSite::None)); + /// assert_eq!(c.to_string(), "name=value; SameSite=None; Secure"); + /// + /// c.set_secure(false); + /// assert_eq!(c.to_string(), "name=value; SameSite=None"); + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.same_site(), None); + /// + /// c.set_same_site(SameSite::Strict); + /// assert_eq!(c.same_site(), Some(SameSite::Strict)); + /// assert_eq!(c.to_string(), "name=value; SameSite=Strict"); + /// + /// c.set_same_site(None); + /// assert_eq!(c.same_site(), None); + /// assert_eq!(c.to_string(), "name=value"); + /// ``` + #[inline] + pub fn set_same_site<T: Into<Option<SameSite>>>(&mut self, value: T) { + self.same_site = value.into(); + } + + /// Sets the value of `max_age` in `self` to `value`. If `value` is `None`, + /// the field is unset. + /// + /// # Example + /// + /// ```rust + /// # extern crate cookie; + /// use cookie::Cookie; + /// use cookie::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))); + /// + /// c.set_max_age(None); + /// assert!(c.max_age().is_none()); + /// # } + /// ``` + #[inline] + pub fn set_max_age<D: Into<Option<Duration>>>(&mut self, value: D) { + self.max_age = value.into(); + } + + /// Sets the `path` of `self` to `path`. + /// + /// # Example + /// + /// ```rust + /// use 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<P: Into<Cow<'c, str>>>(&mut self, path: P) { + self.path = Some(CookieStr::Concrete(path.into())); + } + + /// Unsets the `path` of `self`. + /// + /// # Example + /// + /// ``` + /// use cookie::Cookie; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.path(), None); + /// + /// c.set_path("/"); + /// assert_eq!(c.path(), Some("/")); + /// + /// c.unset_path(); + /// assert_eq!(c.path(), None); + /// ``` + pub fn unset_path(&mut self) { + self.path = None; + } + + /// Sets the `domain` of `self` to `domain`. + /// + /// # Example + /// + /// ``` + /// use 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<D: Into<Cow<'c, str>>>(&mut self, domain: D) { + self.domain = Some(CookieStr::Concrete(domain.into())); + } + + /// Unsets the `domain` of `self`. + /// + /// # Example + /// + /// ``` + /// use 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")); + /// + /// c.unset_domain(); + /// assert_eq!(c.domain(), None); + /// ``` + pub fn unset_domain(&mut self) { + self.domain = None; + } + + /// Sets the expires field of `self` to `time`. If `time` is `None`, an + /// expiration of [`Session`](Expiration::Session) is set. + /// + /// # Example + /// + /// ``` + /// # extern crate cookie; + /// use cookie::{Cookie, Expiration}; + /// use cookie::time::{Duration, OffsetDateTime}; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.expires(), None); + /// + /// let mut now = OffsetDateTime::now_utc(); + /// now += Duration::weeks(52); + /// + /// c.set_expires(now); + /// assert!(c.expires().is_some()); + /// + /// c.set_expires(None); + /// assert_eq!(c.expires(), Some(Expiration::Session)); + /// ``` + pub fn set_expires<T: Into<Expiration>>(&mut self, time: T) { + static MAX_DATETIME: OffsetDateTime = datetime!(9999-12-31 23:59:59.999_999 UTC); + + // RFC 6265 requires dates not to exceed 9999 years. + self.expires = Some(time.into() + .map(|time| std::cmp::min(time, MAX_DATETIME))); + } + + /// Unsets the `expires` of `self`. + /// + /// # Example + /// + /// ``` + /// use cookie::{Cookie, Expiration}; + /// + /// let mut c = Cookie::new("name", "value"); + /// assert_eq!(c.expires(), None); + /// + /// c.set_expires(None); + /// assert_eq!(c.expires(), Some(Expiration::Session)); + /// + /// c.unset_expires(); + /// assert_eq!(c.expires(), None); + /// ``` + pub fn unset_expires(&mut self) { + self.expires = None; + } + + /// Makes `self` a "permanent" cookie by extending its expiration and max + /// age 20 years into the future. + /// + /// # Example + /// + /// ```rust + /// # extern crate cookie; + /// use cookie::Cookie; + /// use cookie::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(OffsetDateTime::now_utc() + twenty_years); + } + + /// Make `self` a "removal" cookie by clearing its value, setting a max-age + /// of `0`, and setting an expiration date far in the past. + /// + /// # Example + /// + /// ```rust + /// # extern crate cookie; + /// use cookie::Cookie; + /// use cookie::time::Duration; + /// + /// # fn main() { + /// let mut c = Cookie::new("foo", "bar"); + /// c.make_permanent(); + /// assert_eq!(c.max_age(), Some(Duration::days(365 * 20))); + /// assert_eq!(c.value(), "bar"); + /// + /// c.make_removal(); + /// assert_eq!(c.value(), ""); + /// assert_eq!(c.max_age(), Some(Duration::ZERO)); + /// # } + /// ``` + pub fn make_removal(&mut self) { + self.set_value(""); + self.set_max_age(Duration::seconds(0)); + self.set_expires(OffsetDateTime::now_utc() - Duration::days(365)); + } + + fn fmt_parameters(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(true) = self.http_only() { + write!(f, "; HttpOnly")?; + } + + if let Some(same_site) = self.same_site() { + write!(f, "; SameSite={}", same_site)?; + + if same_site.is_none() && self.secure().is_none() { + write!(f, "; Secure")?; + } + } + + if let Some(true) = self.secure() { + write!(f, "; Secure")?; + } + + if let Some(path) = self.path() { + write!(f, "; Path={}", path)?; + } + + if let Some(domain) = self.domain() { + write!(f, "; Domain={}", domain)?; + } + + if let Some(max_age) = self.max_age() { + write!(f, "; Max-Age={}", max_age.whole_seconds())?; + } + + if let Some(time) = self.expires_datetime() { + let time = time.to_offset(UtcOffset::UTC); + write!(f, "; Expires={}", time.format(&crate::parse::FMT1).map_err(|_| fmt::Error)?)?; + } + + 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 [`Cookie::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 [`Cookie::name()`]. + /// + /// # Example + /// + /// ``` + /// use 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 [`Cookie::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 [`Cookie::value()`]. + /// + /// # Example + /// + /// ``` + /// use 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 [`Cookie::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 [`Cookie::path()`]. + /// + /// # Example + /// + /// ``` + /// use 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 [`Cookie::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 + /// [`Cookie::domain()`]. + /// + /// # Example + /// + /// ``` + /// use 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, + } + } + + /// Wraps `self` in an encoded [`Display`]: a cost-free wrapper around + /// `Cookie` whose [`fmt::Display`] implementation percent-encodes the name + /// and value of the wrapped `Cookie`. + /// + /// The returned structure can be chained with [`Display::stripped()`] to + /// display only the name and value. + /// + /// # Example + /// + /// ```rust + /// use cookie::Cookie; + /// + /// let mut c = Cookie::build("my name", "this; value?").secure(true).finish(); + /// assert_eq!(&c.encoded().to_string(), "my%20name=this%3B%20value%3F; Secure"); + /// assert_eq!(&c.encoded().stripped().to_string(), "my%20name=this%3B%20value%3F"); + /// ``` + #[cfg(feature = "percent-encode")] + #[cfg_attr(all(nightly, doc), doc(cfg(feature = "percent-encode")))] + #[inline(always)] + pub fn encoded<'a>(&'a self) -> Display<'a, 'c> { + Display::new_encoded(self) + } + + /// Wraps `self` in a stripped `Display`]: a cost-free wrapper around + /// `Cookie` whose [`fmt::Display`] implementation prints only the `name` + /// and `value` of the wrapped `Cookie`. + /// + /// The returned structure can be chained with [`Display::encoded()`] to + /// encode the name and value. + /// + /// # Example + /// + /// ```rust + /// use cookie::Cookie; + /// + /// let mut c = Cookie::build("key?", "value").secure(true).path("/").finish(); + /// assert_eq!(&c.stripped().to_string(), "key?=value"); + #[cfg_attr(feature = "percent-encode", doc = r##" +// Note: `encoded()` is only available when `percent-encode` is enabled. +assert_eq!(&c.stripped().encoded().to_string(), "key%3F=value"); + #"##)] + /// ``` + #[inline(always)] + pub fn stripped<'a>(&'a self) -> Display<'a, 'c> { + Display::new_stripped(self) + } +} + +#[cfg(feature = "percent-encode")] +mod encoding { + use percent_encoding::{AsciiSet, CONTROLS}; + + /// https://url.spec.whatwg.org/#fragment-percent-encode-set + const FRAGMENT: &AsciiSet = &CONTROLS + .add(b' ') + .add(b'"') + .add(b'<') + .add(b'>') + .add(b'`'); + + /// https://url.spec.whatwg.org/#path-percent-encode-set + const PATH: &AsciiSet = &FRAGMENT + .add(b'#') + .add(b'?') + .add(b'{') + .add(b'}'); + + /// https://url.spec.whatwg.org/#userinfo-percent-encode-set + const USERINFO: &AsciiSet = &PATH + .add(b'/') + .add(b':') + .add(b';') + .add(b'=') + .add(b'@') + .add(b'[') + .add(b'\\') + .add(b']') + .add(b'^') + .add(b'|') + .add(b'%'); + + /// https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1 + '(', ')' + const COOKIE: &AsciiSet = &USERINFO + .add(b'(') + .add(b')') + .add(b','); + + /// Percent-encode a cookie name or value with the proper encoding set. + pub fn encode(string: &str) -> impl std::fmt::Display + '_ { + percent_encoding::percent_encode(string.as_bytes(), COOKIE) + } +} + +/// Wrapper around `Cookie` whose `Display` implementation either +/// percent-encodes the cookie's name and value, skips displaying the cookie's +/// parameters (only displaying it's name and value), or both. +/// +/// A value of this type can be obtained via [`Cookie::encoded()`] and +/// [`Cookie::stripped()`], or an arbitrary chaining of the two methods. This +/// type should only be used for its `Display` implementation. +/// +/// # Example +/// +/// ```rust +/// use cookie::Cookie; +/// +/// let c = Cookie::build("my name", "this; value%?").secure(true).finish(); +/// assert_eq!(&c.stripped().to_string(), "my name=this; value%?"); +#[cfg_attr(feature = "percent-encode", doc = r##" +// Note: `encoded()` is only available when `percent-encode` is enabled. +assert_eq!(&c.encoded().to_string(), "my%20name=this%3B%20value%25%3F; Secure"); +assert_eq!(&c.stripped().encoded().to_string(), "my%20name=this%3B%20value%25%3F"); +assert_eq!(&c.encoded().stripped().to_string(), "my%20name=this%3B%20value%25%3F"); +"##)] +/// ``` +pub struct Display<'a, 'c: 'a> { + cookie: &'a Cookie<'c>, + #[cfg(feature = "percent-encode")] + encode: bool, + strip: bool, +} + +impl<'a, 'c: 'a> fmt::Display for Display<'a, 'c> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + #[cfg(feature = "percent-encode")] { + if self.encode { + let name = encoding::encode(self.cookie.name()); + let value = encoding::encode(self.cookie.value()); + write!(f, "{}={}", name, value)?; + } else { + write!(f, "{}={}", self.cookie.name(), self.cookie.value())?; + } + } + + #[cfg(not(feature = "percent-encode"))] { + write!(f, "{}={}", self.cookie.name(), self.cookie.value())?; + } + + match self.strip { + true => Ok(()), + false => self.cookie.fmt_parameters(f) + } + } +} + +impl<'a, 'c> Display<'a, 'c> { + #[cfg(feature = "percent-encode")] + fn new_encoded(cookie: &'a Cookie<'c>) -> Self { + Display { cookie, strip: false, encode: true } + } + + fn new_stripped(cookie: &'a Cookie<'c>) -> Self { + Display { cookie, strip: true, #[cfg(feature = "percent-encode")] encode: false } + } + + /// Percent-encode the name and value pair. + #[inline] + #[cfg(feature = "percent-encode")] + #[cfg_attr(all(nightly, doc), doc(cfg(feature = "percent-encode")))] + pub fn encoded(mut self) -> Self { + self.encode = true; + self + } + + /// Only display the name and value. + #[inline] + pub fn stripped(mut self) -> Self { + self.strip = true; + self + } +} + +impl<'c> fmt::Display for Cookie<'c> { + /// Formats the cookie `self` as a `Set-Cookie` header value. + /// + /// Does _not_ percent-encode any values. To percent-encode, use + /// [`Cookie::encoded()`]. + /// + /// # Example + /// + /// ```rust + /// use 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<Cookie<'static>, ParseError> { + Cookie::parse(s).map(|c| c.into_owned()) + } +} + +impl<'a, 'b> PartialEq<Cookie<'b>> 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 crate::{Cookie, SameSite, parse::parse_date}; + use time::{Duration, OffsetDateTime}; + + #[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 = parse_date(time_str, &crate::parse::FMT1).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 mut cookie = Cookie::build("foo", "bar") + .same_site(SameSite::None).finish(); + assert_eq!(&cookie.to_string(), "foo=bar; SameSite=None; Secure"); + + cookie.set_same_site(None); + assert_eq!(&cookie.to_string(), "foo=bar"); + + let mut cookie = Cookie::build("foo", "bar") + .same_site(SameSite::None) + .secure(false) + .finish(); + assert_eq!(&cookie.to_string(), "foo=bar; SameSite=None"); + cookie.set_secure(true); + assert_eq!(&cookie.to_string(), "foo=bar; SameSite=None; Secure"); + } + + #[test] + #[ignore] + fn format_date_wraps() { + let expires = OffsetDateTime::UNIX_EPOCH + Duration::MAX; + let cookie = Cookie::build("foo", "bar").expires(expires).finish(); + assert_eq!(&cookie.to_string(), "foo=bar; Expires=Fri, 31 Dec 9999 23:59:59 GMT"); + + let expires = time::macros::datetime!(9999-01-01 0:00 UTC) + Duration::days(1000); + let cookie = Cookie::build("foo", "bar").expires(expires).finish(); + assert_eq!(&cookie.to_string(), "foo=bar; Expires=Fri, 31 Dec 9999 23:59:59 GMT"); + } + + #[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] + #[cfg(feature = "percent-encode")] + fn format_encoded() { + let cookie = Cookie::build("foo !%?=", "bar;;, a").finish(); + let cookie_str = cookie.encoded().to_string(); + assert_eq!(&cookie_str, "foo%20!%25%3F%3D=bar%3B%3B%2C%20a"); + + let cookie = Cookie::parse_encoded(cookie_str).unwrap(); + assert_eq!(cookie.name_value(), ("foo !%?=", "bar;;, a")); + } +} diff --git a/third_party/rust/cookie/src/parse.rs b/third_party/rust/cookie/src/parse.rs new file mode 100644 index 0000000000..dcd15b7b32 --- /dev/null +++ b/third_party/rust/cookie/src/parse.rs @@ -0,0 +1,550 @@ +use std::borrow::Cow; +use std::error::Error; +use std::convert::{From, TryFrom}; +use std::str::Utf8Error; +use std::fmt; + +#[allow(unused_imports, deprecated)] +use std::ascii::AsciiExt; + +#[cfg(feature = "percent-encode")] +use percent_encoding::percent_decode; +use time::{PrimitiveDateTime, Duration, OffsetDateTime}; +use time::{parsing::Parsable, macros::format_description, format_description::FormatItem}; + +use crate::{Cookie, SameSite, CookieStr}; + +// The three formats spec'd in http://tools.ietf.org/html/rfc2616#section-3.3.1. +// Additional ones as encountered in the real world. +pub static FMT1: &[FormatItem<'_>] = format_description!("[weekday repr:short], [day] [month repr:short] [year padding:none] [hour]:[minute]:[second] GMT"); +pub static FMT2: &[FormatItem<'_>] = format_description!("[weekday], [day]-[month repr:short]-[year repr:last_two] [hour]:[minute]:[second] GMT"); +pub static FMT3: &[FormatItem<'_>] = format_description!("[weekday repr:short] [month repr:short] [day padding:space] [hour]:[minute]:[second] [year padding:none]"); +pub static FMT4: &[FormatItem<'_>] = format_description!("[weekday repr:short], [day]-[month repr:short]-[year padding:none] [hour]:[minute]:[second] GMT"); + +/// Enum corresponding to a parsing error. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[non_exhaustive] +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), +} + +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" + } + } + } +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl From<Utf8Error> 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)) +} + +#[cfg(feature = "percent-encode")] +fn name_val_decoded( + name: &str, + val: &str +) -> Result<Option<(CookieStr<'static>, CookieStr<'static>)>, ParseError> { + let decoded_name = percent_decode(name.as_bytes()).decode_utf8()?; + let decoded_value = percent_decode(val.as_bytes()).decode_utf8()?; + + if let (&Cow::Borrowed(_), &Cow::Borrowed(_)) = (&decoded_name, &decoded_value) { + Ok(None) + } else { + let name = CookieStr::Concrete(Cow::Owned(decoded_name.into())); + let val = CookieStr::Concrete(Cow::Owned(decoded_value.into())); + Ok(Some((name, val))) + } +} + +#[cfg(not(feature = "percent-encode"))] +fn name_val_decoded( + _: &str, + _: &str +) -> Result<Option<(CookieStr<'static>, CookieStr<'static>)>, ParseError> { + unreachable!("This function should never be called with 'percent-encode' disabled!") +} + +fn trim_quotes(s: &str) -> &str { + if s.len() < 2 { + return s; + } + + match (s.chars().next(), s.chars().last()) { + (Some('"'), Some('"')) => &s[1..(s.len() - 1)], + _ => s + } +} + +// 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<Cookie<'c>, ParseError> { + let mut attributes = s.split(';'); + + // Determine the name = val. + let key_value = attributes.next().expect("first str::split().next() returns Some"); + let (name, value) = match key_value.find('=') { + Some(i) => { + let (key, value) = (key_value[..i].trim(), key_value[(i + 1)..].trim()); + (key, trim_quotes(value).trim()) + }, + None => return Err(ParseError::MissingPair) + }; + + if name.is_empty() { + return Err(ParseError::EmptyName); + } + + // If there is nothing to decode, or we're not decoding, use indexes. + let indexed_names = |s, name, value| { + 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) + }; + + // 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 { + match name_val_decoded(name, value)? { + Some((name, value)) => (name, value), + None => indexed_names(s, name, value) + } + } else { + indexed_names(s, name, value) + }; + + let mut cookie: Cookie<'c> = 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(mut v)) => cookie.max_age = { + let is_negative = v.starts_with('-'); + if is_negative { + v = &v[1..]; + } + + if !v.chars().all(|d| d.is_digit(10)) { + continue + } + + // From RFC 6265 5.2.2: neg values indicate that the earliest + // expiration should be used, so set the max age to 0 seconds. + if is_negative { + Some(Duration::ZERO) + } else { + Some(v.parse::<i64>() + .map(Duration::seconds) + .unwrap_or_else(|_| Duration::seconds(i64::max_value()))) + } + }, + ("domain", Some(mut domain)) if !domain.is_empty() => { + if domain.starts_with('.') { + domain = &domain[1..]; + } + + let (i, j) = indexes_of(domain, s).expect("domain sub"); + cookie.domain = Some(CookieStr::Indexed(i, j)); + } + ("path", Some(v)) => { + let (i, j) = indexes_of(v, s).expect("path sub"); + cookie.path = Some(CookieStr::Indexed(i, j)); + } + ("samesite", Some(v)) => { + if v.eq_ignore_ascii_case("strict") { + cookie.same_site = Some(SameSite::Strict); + } else if v.eq_ignore_ascii_case("lax") { + cookie.same_site = Some(SameSite::Lax); + } else if v.eq_ignore_ascii_case("none") { + cookie.same_site = Some(SameSite::None); + } else { + // We do nothing here, for now. When/if the `SameSite` + // attribute becomes standard, the spec says that we should + // ignore this cookie, i.e, fail to parse it, when an + // invalid value is passed in. The draft is at + // http://httpwg.org/http-extensions/draft-ietf-httpbis-cookie-same-site.html. + } + } + ("expires", Some(v)) => { + let tm = parse_date(v, &FMT1) + .or_else(|_| parse_date(v, &FMT2)) + .or_else(|_| parse_date(v, &FMT3)) + .or_else(|_| parse_date(v, &FMT4)); + // .or_else(|_| parse_date(v, &FMT5)); + + if let Ok(time) = tm { + cookie.expires = Some(time.into()) + } + } + _ => { + // 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(crate) fn parse_cookie<'c, S>(cow: S, decode: bool) -> Result<Cookie<'c>, ParseError> + where S: Into<Cow<'c, str>> +{ + let s = cow.into(); + let mut cookie = parse_inner(&s, decode)?; + cookie.cookie_string = Some(s); + Ok(cookie) +} + +pub(crate) fn parse_date(s: &str, format: &impl Parsable) -> Result<OffsetDateTime, time::Error> { + // Parse. Handle "abbreviated" dates like Chromium. See cookie#162. + let mut date = format.parse(s.as_bytes())?; + if let Some(y) = date.year().or_else(|| date.year_last_two().map(|v| v as i32)) { + let offset = match y { + 0..=68 => 2000, + 69..=99 => 1900, + _ => 0, + }; + + date.set_year(y + offset); + } + + Ok(PrimitiveDateTime::try_from(date)?.assume_utc()) +} + +#[cfg(test)] +mod tests { + use super::parse_date; + use crate::{Cookie, SameSite}; + use time::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); + + let expected = Cookie::build("foo", "bar") + .same_site(SameSite::None) + .finish(); + + assert_eq_parse!("foo=bar; SameSite=None", expected); + assert_eq_parse!("foo=bar; SameSITE=none", expected); + assert_eq_parse!("foo=bar; SameSite=NOne", expected); + assert_eq_parse!("foo=bar; SameSite=nOne", expected); + } + + #[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 expected = Cookie::build("foo", "\"bar\"").finish(); + assert_eq_parse!("foo=\"\"bar\"\"", expected); + + let 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 \"", expected); + assert_eq_parse!("foo=\"\"bar \" ", expected); + + let 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\" \" ", 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 ", 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 = parse_date(time_str, &super::FMT1).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 = parse_date(time_str, &super::FMT1).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 parse_abbreviated_years() { + let cookie_str = "foo=bar; expires=Thu, 10-Sep-20 20:00:00 GMT"; + let cookie = Cookie::parse(cookie_str).unwrap(); + assert_eq!(cookie.expires_datetime().unwrap().year(), 2020); + + let cookie_str = "foo=bar; expires=Thu, 10-Sep-68 20:00:00 GMT"; + let cookie = Cookie::parse(cookie_str).unwrap(); + assert_eq!(cookie.expires_datetime().unwrap().year(), 2068); + + let cookie_str = "foo=bar; expires=Thu, 10-Sep-69 20:00:00 GMT"; + let cookie = Cookie::parse(cookie_str).unwrap(); + assert_eq!(cookie.expires_datetime().unwrap().year(), 1969); + + let cookie_str = "foo=bar; expires=Thu, 10-Sep-99 20:00:00 GMT"; + let cookie = Cookie::parse(cookie_str).unwrap(); + assert_eq!(cookie.expires_datetime().unwrap().year(), 1999); + + let cookie_str = "foo=bar; expires=Thu, 10-Sep-2069 20:00:00 GMT"; + let cookie = Cookie::parse(cookie_str).unwrap(); + assert_eq!(cookie.expires_datetime().unwrap().year(), 2069); + } + + #[test] + fn parse_variant_date_fmts() { + let cookie_str = "foo=bar; expires=Sun, 06 Nov 1994 08:49:37 GMT"; + Cookie::parse(cookie_str).unwrap().expires_datetime().unwrap(); + + let cookie_str = "foo=bar; expires=Sunday, 06-Nov-94 08:49:37 GMT"; + Cookie::parse(cookie_str).unwrap().expires_datetime().unwrap(); + + let cookie_str = "foo=bar; expires=Sun Nov 6 08:49:37 1994"; + Cookie::parse(cookie_str).unwrap().expires_datetime().unwrap(); + } + + #[test] + fn parse_very_large_max_ages() { + let mut expected = Cookie::build("foo", "bar") + .max_age(Duration::seconds(i64::max_value())) + .finish(); + + let string = format!("foo=bar; Max-Age={}", 1u128 << 100); + assert_eq_parse!(&string, expected); + + expected.set_max_age(Duration::seconds(0)); + assert_eq_parse!("foo=bar; Max-Age=-129", expected); + + let string = format!("foo=bar; Max-Age=-{}", 1u128 << 100); + assert_eq_parse!(&string, expected); + + let string = format!("foo=bar; Max-Age=-{}", i64::max_value()); + assert_eq_parse!(&string, expected); + + let string = format!("foo=bar; Max-Age={}", i64::max_value()); + expected.set_max_age(Duration::seconds(i64::max_value())); + assert_eq_parse!(&string, expected); + } + + #[test] + fn odd_characters() { + let expected = Cookie::new("foo", "b%2Fr"); + assert_eq_parse!("foo=b%2Fr", expected); + } + + #[test] + #[cfg(feature = "percent-encode")] + 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.whole_seconds(); + let expected = Cookie::build("foo", "bar") + .max_age(Duration::seconds(max_seconds)) + .finish(); + let too_many_seconds = (max_seconds as u64) + 1; + assert_eq_parse!(format!(" foo=bar; Max-Age={:?}", too_many_seconds), expected); + } +} diff --git a/third_party/rust/cookie/src/secure/key.rs b/third_party/rust/cookie/src/secure/key.rs new file mode 100644 index 0000000000..9c2228eb3a --- /dev/null +++ b/third_party/rust/cookie/src/secure/key.rs @@ -0,0 +1,301 @@ +use std::convert::TryFrom; + +const SIGNING_KEY_LEN: usize = 32; +const ENCRYPTION_KEY_LEN: usize = 32; +const COMBINED_KEY_LENGTH: usize = SIGNING_KEY_LEN + ENCRYPTION_KEY_LEN; + +// Statically ensure the numbers above are in-sync. +#[cfg(feature = "signed")] +const_assert!(crate::secure::signed::KEY_LEN == SIGNING_KEY_LEN); +#[cfg(feature = "private")] +const_assert!(crate::secure::private::KEY_LEN == ENCRYPTION_KEY_LEN); + +/// A cryptographic master key for use with `Signed` and/or `Private` jars. +/// +/// This structure encapsulates secure, cryptographic keys for use with both +/// [`PrivateJar`](crate::PrivateJar) and [`SignedJar`](crate::SignedJar). A +/// single instance of a `Key` can be used for both a `PrivateJar` and a +/// `SignedJar` simultaneously with no notable security implications. +#[cfg_attr(all(nightly, doc), doc(cfg(any(feature = "private", feature = "signed"))))] +#[derive(Clone)] +pub struct Key([u8; COMBINED_KEY_LENGTH /* SIGNING | ENCRYPTION */]); + +impl PartialEq for Key { + fn eq(&self, other: &Self) -> bool { + use subtle::ConstantTimeEq; + + self.0.ct_eq(&other.0).into() + } +} + +impl Key { + // An empty key structure, to be filled. + const fn zero() -> Self { + Key([0; COMBINED_KEY_LENGTH]) + } + + /// Creates a new `Key` from a 512-bit cryptographically random string. + /// + /// The supplied key must be at least 512-bits (64 bytes). For security, the + /// master key _must_ be cryptographically random. + /// + /// # Panics + /// + /// Panics if `key` is less than 64 bytes in length. + /// + /// For a non-panicking version, use [`Key::try_from()`] or generate a key with + /// [`Key::generate()`] or [`Key::try_generate()`]. + /// + /// # Example + /// + /// ```rust + /// use cookie::Key; + /// + /// # /* + /// let key = { /* a cryptographically random key >= 64 bytes */ }; + /// # */ + /// # let key: &Vec<u8> = &(0..64).collect(); + /// + /// let key = Key::from(key); + /// ``` + #[inline] + pub fn from(key: &[u8]) -> Key { + Key::try_from(key).unwrap() + } + + /// 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 cookie::Key; + /// + /// # /* + /// let master_key = { /* a cryptographically random key >= 32 bytes */ }; + /// # */ + /// # let master_key: &Vec<u8> = &(0..32).collect(); + /// + /// let key = Key::derive_from(master_key); + /// ``` + #[cfg(feature = "key-expansion")] + #[cfg_attr(all(nightly, doc), doc(cfg(feature = "key-expansion")))] + pub fn derive_from(master_key: &[u8]) -> Self { + if master_key.len() < 32 { + panic!("bad master key length: expected >= 32 bytes, found {}", master_key.len()); + } + + // Expand the master key into two HKDF generated keys. + const KEYS_INFO: &[u8] = b"COOKIE;SIGNED:HMAC-SHA256;PRIVATE:AEAD-AES-256-GCM"; + let mut both_keys = [0; COMBINED_KEY_LENGTH]; + let hk = hkdf::Hkdf::<sha2::Sha256>::from_prk(master_key).expect("key length prechecked"); + hk.expand(KEYS_INFO, &mut both_keys).expect("expand into keys"); + Key::from(&both_keys) + } + + /// 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 + /// [`Key::try_generate()`] for a non-panicking version. + /// + /// # Example + /// + /// ```rust + /// use 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 cookie::Key; + /// + /// let key = Key::try_generate(); + /// ``` + pub fn try_generate() -> Option<Key> { + use crate::secure::rand::RngCore; + + let mut rng = crate::secure::rand::thread_rng(); + let mut key = Key::zero(); + rng.try_fill_bytes(&mut key.0).ok()?; + Some(key) + } + + /// Returns the raw bytes of a key suitable for signing cookies. Guaranteed + /// to be at least 32 bytes. + /// + /// # Example + /// + /// ```rust + /// use cookie::Key; + /// + /// let key = Key::generate(); + /// let signing_key = key.signing(); + /// ``` + pub fn signing(&self) -> &[u8] { + &self.0[..SIGNING_KEY_LEN] + } + + /// Returns the raw bytes of a key suitable for encrypting cookies. + /// Guaranteed to be at least 32 bytes. + /// + /// # Example + /// + /// ```rust + /// use cookie::Key; + /// + /// let key = Key::generate(); + /// let encryption_key = key.encryption(); + /// ``` + pub fn encryption(&self) -> &[u8] { + &self.0[SIGNING_KEY_LEN..] + } + + /// Returns the raw bytes of the master key. Guaranteed to be at least 64 + /// bytes. + /// + /// # Example + /// + /// ```rust + /// use cookie::Key; + /// + /// let key = Key::generate(); + /// let master_key = key.master(); + /// ``` + pub fn master(&self) -> &[u8] { + &self.0 + } +} + +/// An error indicating an issue with generating or constructing a key. +#[cfg_attr(all(nightly, doc), doc(cfg(any(feature = "private", feature = "signed"))))] +#[derive(Debug)] +#[non_exhaustive] +pub enum KeyError { + /// Too few bytes (`.0`) were provided to generate a key. + /// + /// See [`Key::from()`] for minimum requirements. + TooShort(usize), +} + +impl std::error::Error for KeyError { } + +impl std::fmt::Display for KeyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + KeyError::TooShort(n) => { + write!(f, "key material is too short: expected >= {} bytes, got {} bytes", + COMBINED_KEY_LENGTH, n) + } + } + } +} + +impl TryFrom<&[u8]> for Key { + type Error = KeyError; + + /// A fallible version of [`Key::from()`]. + /// + /// Succeeds when [`Key::from()`] succeds and returns an error where + /// [`Key::from()`] panics, namely, if `key` is too short. + /// + /// # Example + /// + /// ```rust + /// # use std::convert::TryFrom; + /// use cookie::Key; + /// + /// # /* + /// let key = { /* a cryptographically random key >= 64 bytes */ }; + /// # */ + /// # let key: &Vec<u8> = &(0..64).collect(); + /// # let key: &[u8] = &key[..]; + /// assert!(Key::try_from(key).is_ok()); + /// + /// // A key that's far too short to use. + /// let key = &[1, 2, 3, 4][..]; + /// assert!(Key::try_from(key).is_err()); + /// ``` + fn try_from(key: &[u8]) -> Result<Self, Self::Error> { + if key.len() < COMBINED_KEY_LENGTH { + Err(KeyError::TooShort(key.len())) + } else { + let mut output = Key::zero(); + output.0.copy_from_slice(&key[..COMBINED_KEY_LENGTH]); + Ok(output) + } + } +} + +#[cfg(test)] +mod test { + use super::Key; + + #[test] + fn from_works() { + let key = Key::from(&(0..64).collect::<Vec<_>>()); + + let signing: Vec<u8> = (0..32).collect(); + assert_eq!(key.signing(), &*signing); + + let encryption: Vec<u8> = (32..64).collect(); + assert_eq!(key.encryption(), &*encryption); + } + + #[test] + fn try_from_works() { + use core::convert::TryInto; + let data = (0..64).collect::<Vec<_>>(); + let key_res: Result<Key, _> = data[0..63].try_into(); + assert!(key_res.is_err()); + + let key_res: Result<Key, _> = data.as_slice().try_into(); + assert!(key_res.is_ok()); + } + + #[test] + #[cfg(feature = "key-expansion")] + fn deterministic_derive() { + let master_key: Vec<u8> = (0..32).collect(); + + let key_a = Key::derive_from(&master_key); + let key_b = Key::derive_from(&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<u8> = (32..64).collect(); + let key_2 = Key::derive_from(&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/third_party/rust/cookie/src/secure/macros.rs b/third_party/rust/cookie/src/secure/macros.rs new file mode 100644 index 0000000000..dfbddcf928 --- /dev/null +++ b/third_party/rust/cookie/src/secure/macros.rs @@ -0,0 +1,49 @@ +#[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()); + }) +} + +// This is courtesty of `static_assertions`. That library is Copyright (c) 2017 +// Nikolai Vazquez. See https://github.com/nvzqz/static-assertions-rs for more. +macro_rules! const_assert { + ($x:expr $(,)?) => { + #[allow(unknown_lints, clippy::eq_op)] + const _: [(); 0 - !{ const ASSERT: bool = $x; ASSERT } as usize] = []; + }; +} diff --git a/third_party/rust/cookie/src/secure/mod.rs b/third_party/rust/cookie/src/secure/mod.rs new file mode 100644 index 0000000000..ca066888e7 --- /dev/null +++ b/third_party/rust/cookie/src/secure/mod.rs @@ -0,0 +1,14 @@ +extern crate rand; +extern crate base64; + +#[macro_use] +mod macros; +mod key; + +pub use self::key::*; + +#[cfg(feature = "private")] mod private; +#[cfg(feature = "private")] pub use self::private::*; + +#[cfg(feature = "signed")] mod signed; +#[cfg(feature = "signed")] pub use self::signed::*; diff --git a/third_party/rust/cookie/src/secure/private.rs b/third_party/rust/cookie/src/secure/private.rs new file mode 100644 index 0000000000..1264c3983c --- /dev/null +++ b/third_party/rust/cookie/src/secure/private.rs @@ -0,0 +1,264 @@ +extern crate aes_gcm; + +use std::convert::TryInto; +use std::borrow::{Borrow, BorrowMut}; + +use crate::secure::{base64, rand, Key}; +use crate::{Cookie, CookieJar}; + +use self::aes_gcm::aead::{generic_array::GenericArray, Aead, AeadInPlace, KeyInit, Payload}; +use self::aes_gcm::Aes256Gcm; +use self::rand::RngCore; + +// Keep these in sync, and keep the key len synced with the `private` docs as +// well as the `KEYS_INFO` const in secure::Key. +pub(crate) const NONCE_LEN: usize = 12; +pub(crate) const TAG_LEN: usize = 16; +pub(crate) 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. +#[cfg_attr(all(nightly, doc), doc(cfg(feature = "private")))] +pub struct PrivateJar<J> { + parent: J, + key: [u8; KEY_LEN] +} + +impl<J> PrivateJar<J> { + /// Creates a new child `PrivateJar` with parent `parent` and key `key`. + /// This method is typically called indirectly via the `signed` method of + /// `CookieJar`. + pub(crate) fn new(parent: J, key: &Key) -> PrivateJar<J> { + PrivateJar { parent, key: key.encryption().try_into().expect("enc key len") } + } + + /// Encrypts the cookie's value with authenticated encryption providing + /// confidentiality, integrity, and authenticity. + fn encrypt_cookie(&self, cookie: &mut Cookie) { + // Create a vec to hold the [nonce | cookie value | tag]. + let cookie_val = cookie.value().as_bytes(); + let mut data = vec![0; NONCE_LEN + cookie_val.len() + TAG_LEN]; + + // Split data into three: nonce, input/output, tag. Copy input. + let (nonce, in_out) = data.split_at_mut(NONCE_LEN); + let (in_out, tag) = in_out.split_at_mut(cookie_val.len()); + in_out.copy_from_slice(cookie_val); + + // Fill nonce piece with random data. + let mut rng = self::rand::thread_rng(); + rng.try_fill_bytes(nonce).expect("couldn't random fill nonce"); + let nonce = GenericArray::clone_from_slice(nonce); + + // Perform the actual sealing operation, using the cookie's name as + // associated data to prevent value swapping. + let aad = cookie.name().as_bytes(); + let aead = Aes256Gcm::new(GenericArray::from_slice(&self.key)); + let aad_tag = aead.encrypt_in_place_detached(&nonce, aad, in_out) + .expect("encryption failure!"); + + // Copy the tag into the tag piece. + tag.copy_from_slice(&aad_tag); + + // Base64 encode [nonce | encrypted value | tag]. + cookie.set_value(base64::encode(&data)); + } + + /// 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<String, &'static str> { + let data = base64::decode(value).map_err(|_| "bad base64 value")?; + if data.len() <= NONCE_LEN { + return Err("length of decoded data is <= NONCE_LEN"); + } + + let (nonce, cipher) = data.split_at(NONCE_LEN); + let payload = Payload { msg: cipher, aad: name.as_bytes() }; + + let aead = Aes256Gcm::new(GenericArray::from_slice(&self.key)); + aead.decrypt(GenericArray::from_slice(nonce), payload) + .map_err(|_| "invalid key/nonce/value: bad seal") + .and_then(|s| String::from_utf8(s).map_err(|_| "bad unsealed utf8")) + } + + /// Authenticates and decrypts `cookie`, returning the plaintext version if + /// decryption succeeds or `None` otherwise. Authenticatation and decryption + /// _always_ succeeds if `cookie` was generated by a `PrivateJar` with the + /// same key as `self`. + /// + /// # Example + /// + /// ```rust + /// use cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// assert!(jar.private(&key).get("name").is_none()); + /// + /// jar.private_mut(&key).add(Cookie::new("name", "value")); + /// assert_eq!(jar.private(&key).get("name").unwrap().value(), "value"); + /// + /// let plain = jar.get("name").cloned().unwrap(); + /// assert_ne!(plain.value(), "value"); + /// let decrypted = jar.private(&key).decrypt(plain).unwrap(); + /// assert_eq!(decrypted.value(), "value"); + /// + /// let plain = Cookie::new("plaintext", "hello"); + /// assert!(jar.private(&key).decrypt(plain).is_none()); + /// ``` + pub fn decrypt(&self, mut cookie: Cookie<'static>) -> Option<Cookie<'static>> { + if let Ok(value) = self.unseal(cookie.name(), cookie.value()) { + cookie.set_value(value); + return Some(cookie); + } + + None + } +} + +impl<J: Borrow<CookieJar>> PrivateJar<J> { + /// 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 cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let jar = CookieJar::new(); + /// assert!(jar.private(&key).get("name").is_none()); + /// + /// let mut jar = jar; + /// let mut private_jar = jar.private_mut(&key); + /// private_jar.add(Cookie::new("name", "value")); + /// assert_eq!(private_jar.get("name").unwrap().value(), "value"); + /// ``` + pub fn get(&self, name: &str) -> Option<Cookie<'static>> { + self.parent.borrow().get(name).and_then(|c| self.decrypt(c.clone())) + } +} + +impl<J: BorrowMut<CookieJar>> PrivateJar<J> { + /// Adds `cookie` to the parent jar. The cookie's value is encrypted with + /// authenticated encryption assuring confidentiality, integrity, and + /// authenticity. + /// + /// # Example + /// + /// ```rust + /// use cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// jar.private_mut(&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); + self.parent.borrow_mut().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()`] 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 cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// jar.private_mut(&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); + self.parent.borrow_mut().add_original(cookie); + } + + /// 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. + /// + /// This is identical to [`CookieJar::remove()`]. See the method's + /// documentation for more details. + /// + /// # Example + /// + /// ```rust + /// use cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// let mut private_jar = jar.private_mut(&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.borrow_mut().remove(cookie); + } +} + +#[cfg(test)] +mod test { + use crate::{CookieJar, Cookie, Key}; + + #[test] + fn simple() { + let key = Key::generate(); + let mut jar = CookieJar::new(); + assert_simple_behaviour!(jar, jar.private_mut(&key)); + } + + #[test] + fn secure() { + let key = Key::generate(); + let mut jar = CookieJar::new(); + assert_secure_behaviour!(jar, jar.private_mut(&key)); + } + + #[test] + fn roundtrip() { + // Secret is SHA-256 hash of 'Super secret!' passed through HKDF-SHA256. + let key = Key::from(&[89, 202, 200, 125, 230, 90, 197, 245, 166, 249, + 34, 169, 135, 31, 20, 197, 94, 154, 254, 79, 60, 26, 8, 143, 254, + 24, 116, 138, 92, 225, 159, 60, 157, 41, 135, 129, 31, 226, 196, 16, + 198, 168, 134, 4, 42, 1, 196, 24, 57, 103, 241, 147, 201, 185, 233, + 10, 180, 170, 187, 89, 252, 137, 110, 107]); + + let mut jar = CookieJar::new(); + jar.add(Cookie::new("encrypted_with_ring014", + "lObeZJorGVyeSWUA8khTO/8UCzFVBY9g0MGU6/J3NN1R5x11dn2JIA==")); + jar.add(Cookie::new("encrypted_with_ring016", + "SU1ujceILyMBg3fReqRmA9HUtAIoSPZceOM/CUpObROHEujXIjonkA==")); + + let private = jar.private(&key); + assert_eq!(private.get("encrypted_with_ring014").unwrap().value(), "Tamper-proof"); + assert_eq!(private.get("encrypted_with_ring016").unwrap().value(), "Tamper-proof"); + } +} diff --git a/third_party/rust/cookie/src/secure/signed.rs b/third_party/rust/cookie/src/secure/signed.rs new file mode 100644 index 0000000000..b46fae4e80 --- /dev/null +++ b/third_party/rust/cookie/src/secure/signed.rs @@ -0,0 +1,251 @@ +use std::convert::TryInto; +use std::borrow::{Borrow, BorrowMut}; + +use sha2::Sha256; +use hmac::{Hmac, Mac}; + +use crate::secure::{base64, Key}; +use crate::{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. +pub(crate) const BASE64_DIGEST_LEN: usize = 44; +pub(crate) 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 provided +/// 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. +#[cfg_attr(all(nightly, doc), doc(cfg(feature = "signed")))] +pub struct SignedJar<J> { + parent: J, + key: [u8; KEY_LEN], +} + +impl<J> SignedJar<J> { + /// Creates a new child `SignedJar` with parent `parent` and key `key`. This + /// method is typically called indirectly via the `signed{_mut}` methods of + /// `CookieJar`. + pub(crate) fn new(parent: J, key: &Key) -> SignedJar<J> { + SignedJar { parent, key: key.signing().try_into().expect("sign key len") } + } + + /// Signs the cookie's value providing integrity and authenticity. + fn sign_cookie(&self, cookie: &mut Cookie) { + // Compute HMAC-SHA256 of the cookie's value. + let mut mac = Hmac::<Sha256>::new_from_slice(&self.key).expect("good key"); + mac.update(cookie.value().as_bytes()); + + // Cookie's new value is [MAC | original-value]. + let mut new_value = base64::encode(&mac.finalize().into_bytes()); + new_value.push_str(cookie.value()); + cookie.set_value(new_value); + } + + /// 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<String, &'static str> { + if !cookie_value.is_char_boundary(BASE64_DIGEST_LEN) { + return Err("missing or invalid digest"); + } + + // Split [MAC | original-value] into its two parts. + let (digest_str, value) = cookie_value.split_at(BASE64_DIGEST_LEN); + let digest = base64::decode(digest_str).map_err(|_| "bad base64 digest")?; + + // Perform the verification. + let mut mac = Hmac::<Sha256>::new_from_slice(&self.key).expect("good key"); + mac.update(value.as_bytes()); + mac.verify_slice(&digest) + .map(|_| value.to_string()) + .map_err(|_| "value did not verify") + } + + /// Verifies the authenticity and integrity of `cookie`, returning the + /// plaintext version if verification succeeds or `None` otherwise. + /// Verification _always_ succeeds if `cookie` was generated by a + /// `SignedJar` with the same key as `self`. + /// + /// # Example + /// + /// ```rust + /// use cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// assert!(jar.signed(&key).get("name").is_none()); + /// + /// jar.signed_mut(&key).add(Cookie::new("name", "value")); + /// assert_eq!(jar.signed(&key).get("name").unwrap().value(), "value"); + /// + /// let plain = jar.get("name").cloned().unwrap(); + /// assert_ne!(plain.value(), "value"); + /// let verified = jar.signed(&key).verify(plain).unwrap(); + /// assert_eq!(verified.value(), "value"); + /// + /// let plain = Cookie::new("plaintext", "hello"); + /// assert!(jar.signed(&key).verify(plain).is_none()); + /// ``` + pub fn verify(&self, mut cookie: Cookie<'static>) -> Option<Cookie<'static>> { + if let Ok(value) = self._verify(cookie.value()) { + cookie.set_value(value); + return Some(cookie); + } + + None + } +} + +impl<J: Borrow<CookieJar>> SignedJar<J> { + /// 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 cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let jar = CookieJar::new(); + /// assert!(jar.signed(&key).get("name").is_none()); + /// + /// let mut jar = jar; + /// let mut signed_jar = jar.signed_mut(&key); + /// signed_jar.add(Cookie::new("name", "value")); + /// assert_eq!(signed_jar.get("name").unwrap().value(), "value"); + /// ``` + pub fn get(&self, name: &str) -> Option<Cookie<'static>> { + self.parent.borrow().get(name).and_then(|c| self.verify(c.clone())) + } +} + +impl<J: BorrowMut<CookieJar>> SignedJar<J> { + /// Adds `cookie` to the parent jar. The cookie's value is signed assuring + /// integrity and authenticity. + /// + /// # Example + /// + /// ```rust + /// use cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// jar.signed_mut(&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.borrow_mut().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()`] 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 cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// jar.signed_mut(&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.borrow_mut().add_original(cookie); + } + + /// 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. + /// + /// This is identical to [`CookieJar::remove()`]. See the method's + /// documentation for more details. + /// + /// # Example + /// + /// ```rust + /// use cookie::{CookieJar, Cookie, Key}; + /// + /// let key = Key::generate(); + /// let mut jar = CookieJar::new(); + /// let mut signed_jar = jar.signed_mut(&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.borrow_mut().remove(cookie); + } +} + +#[cfg(test)] +mod test { + use crate::{CookieJar, Cookie, Key}; + + #[test] + fn simple() { + let key = Key::generate(); + let mut jar = CookieJar::new(); + assert_simple_behaviour!(jar, jar.signed_mut(&key)); + } + + #[test] + fn private() { + let key = Key::generate(); + let mut jar = CookieJar::new(); + assert_secure_behaviour!(jar, jar.signed_mut(&key)); + } + + #[test] + fn roundtrip() { + // Secret is SHA-256 hash of 'Super secret!' passed through HKDF-SHA256. + let key = Key::from(&[89, 202, 200, 125, 230, 90, 197, 245, 166, 249, + 34, 169, 135, 31, 20, 197, 94, 154, 254, 79, 60, 26, 8, 143, 254, + 24, 116, 138, 92, 225, 159, 60, 157, 41, 135, 129, 31, 226, 196, 16, + 198, 168, 134, 4, 42, 1, 196, 24, 57, 103, 241, 147, 201, 185, 233, + 10, 180, 170, 187, 89, 252, 137, 110, 107]); + + let mut jar = CookieJar::new(); + jar.add(Cookie::new("signed_with_ring014", + "3tdHXEQ2kf6fxC7dWzBGmpSLMtJenXLKrZ9cHkSsl1w=Tamper-proof")); + jar.add(Cookie::new("signed_with_ring016", + "3tdHXEQ2kf6fxC7dWzBGmpSLMtJenXLKrZ9cHkSsl1w=Tamper-proof")); + + let signed = jar.signed(&key); + assert_eq!(signed.get("signed_with_ring014").unwrap().value(), "Tamper-proof"); + assert_eq!(signed.get("signed_with_ring016").unwrap().value(), "Tamper-proof"); + } + + #[test] + fn issue_178() { + let data = "x=yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy£"; + let c = Cookie::parse(data).expect("failed to parse cookie"); + let key = Key::from(&[0u8; 64]); + let mut jar = CookieJar::new(); + let signed = jar.signed_mut(&key); + assert!(signed.verify(c).is_none()); + } +} |