From d68d01127fa5221919c68970fa60d9475da7cc46 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 6 Oct 2023 18:55:35 -0700 Subject: [PATCH] Implement cookie prefixes via 'PrefixedJar'. Closes #214. --- src/jar.rs | 83 +++++++- src/lib.rs | 10 +- src/prefix.rs | 367 +++++++++++++++++++++++++++++++++ src/{draft.rs => same_site.rs} | 0 src/secure/signed.rs | 6 +- 5 files changed, 460 insertions(+), 6 deletions(-) create mode 100644 src/prefix.rs rename src/{draft.rs => same_site.rs} (100%) diff --git a/src/jar.rs b/src/jar.rs index 76d7ea83..e6753165 100644 --- a/src/jar.rs +++ b/src/jar.rs @@ -5,6 +5,7 @@ use std::collections::HashSet; #[cfg(any(feature = "signed", feature = "private"))] use crate::secure::Key; use crate::delta::DeltaCookie; +use crate::prefix::{Prefix, PrefixedJar}; use crate::Cookie; /// A collection of cookies that tracks its modifications. @@ -486,7 +487,7 @@ impl CookieJar { /// # Example /// /// ```rust - /// use cookie::{Cookie, CookieJar, Key}; + /// use cookie::{CookieJar, Key}; /// /// // Generate a secure key. /// let key = Key::generate(); @@ -503,6 +504,86 @@ impl CookieJar { pub fn signed_mut<'a>(&'a mut self, key: &Key) -> SignedJar<&'a mut Self> { SignedJar::new(self, key) } + + /// Returns a read-only `PrefixedJar` with `self` as its parent jar that + /// prefixes the name of cookies with `prefix`. Any retrievals from the + /// child jar will be made from the parent jar. + /// + /// **Note:** Cookie prefixes are specified in an [HTTP draft]! Their + /// meaning and definition are subject to change. + /// + /// # Example + /// + /// ```rust + /// use cookie::CookieJar; + /// use cookie::prefix::{Host, Secure}; + /// + /// // Add a `Host` prefixed cookie. + /// let mut jar = CookieJar::new(); + /// jar.prefixed_mut(Host).add(("h0st", "value")); + /// jar.prefixed_mut(Secure).add(("secur3", "value")); + /// + /// // The cookie's name is prefixed in the parent jar. + /// assert!(matches!(jar.get("h0st"), None)); + /// assert!(matches!(jar.get("__Host-h0st"), Some(_))); + /// assert!(matches!(jar.get("secur3"), None)); + /// assert!(matches!(jar.get("__Secure-secur3"), Some(_))); + /// + /// // The prefixed jar automatically removes the prefix. + /// assert_eq!(jar.prefixed(Host).get("h0st").unwrap().name(), "h0st"); + /// assert_eq!(jar.prefixed(Host).get("h0st").unwrap().value(), "value"); + /// assert_eq!(jar.prefixed(Secure).get("secur3").unwrap().name(), "secur3"); + /// assert_eq!(jar.prefixed(Secure).get("secur3").unwrap().value(), "value"); + /// + /// // Only the correct prefixed jar retrieves the cookie. + /// assert!(matches!(jar.prefixed(Host).get("secur3"), None)); + /// assert!(matches!(jar.prefixed(Secure).get("h0st"), None)); + /// ``` + #[inline(always)] + pub fn prefixed<'a, P: Prefix>(&'a self, prefix: P) -> PrefixedJar { + let _ = prefix; + PrefixedJar::new(self) + } + + /// Returns a read/write `PrefixedJar` with `self` as its parent jar that + /// prefixes the name of cookies with `prefix` and makes the cookie conform + /// to the prefix's requirements. This means that added cookies: + /// + /// 1. Have the [`Prefix::PREFIX`] prepended to their name. + /// 2. Modify the cookie via [`Prefix::conform()`] so that it conforms to + /// the prefix's requirements. + /// + /// 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. + /// + /// **Note:** Cookie prefixes are specified in an [HTTP draft]! Their + /// meaning and definition are subject to change. + /// + /// # Example + /// + /// ```rust + /// use cookie::CookieJar; + /// use cookie::prefix::{Host, Secure}; + /// + /// // Add some prefixed cookies. + /// let mut jar = CookieJar::new(); + /// jar.prefixed_mut(Host).add(("one", "1")); + /// jar.prefixed_mut(Secure).add((2.to_string(), "2")); + /// jar.prefixed_mut(Host).add((format!("{:0b}", 3), "0b11")); + /// + /// // Fetch cookies with either `prefixed()` or `prefixed_mut()`. + /// assert_eq!(jar.prefixed(Host).get("one").unwrap().value(), "1"); + /// assert_eq!(jar.prefixed(Secure).get("2").unwrap().value(), "2"); + /// assert_eq!(jar.prefixed_mut(Host).get("11").unwrap().value(), "0b11"); + /// + /// // Remove cookies. + /// jar.prefixed_mut(Host).remove("one"); + /// assert!(jar.prefixed(Host).get("one").is_none()); + /// ``` + pub fn prefixed_mut<'a, P: Prefix>(&'a mut self, prefix: P) -> PrefixedJar { + let _ = prefix; + PrefixedJar::new(self) + } } use std::collections::hash_set::Iter as HashSetIter; diff --git a/src/lib.rs b/src/lib.rs index 640a45ef..f9a1689b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,9 +78,15 @@ mod builder; mod parse; mod jar; mod delta; -mod draft; +mod same_site; mod expiration; +/// Implementation of [HTTP RFC6265 draft] cookie prefixes. +/// +/// [HTTP RFC6265 draft]: +/// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-cookie-name-prefixes +pub mod prefix; + #[cfg(any(feature = "private", feature = "signed"))] #[macro_use] mod secure; #[cfg(any(feature = "private", feature = "signed"))] pub use secure::*; @@ -97,7 +103,7 @@ 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::same_site::*; pub use crate::expiration::*; #[derive(Debug, Clone)] diff --git a/src/prefix.rs b/src/prefix.rs new file mode 100644 index 00000000..8d0edd5e --- /dev/null +++ b/src/prefix.rs @@ -0,0 +1,367 @@ +use std::marker::PhantomData; +use std::borrow::{Borrow, BorrowMut, Cow}; + +use crate::{CookieJar, Cookie}; + +/// A child jar that automatically [prefixes](Prefix) cookies. +/// +/// Obtained via [`CookieJar::prefixed()`] and [`CookieJar::prefixed_mut()`]. +/// +/// This jar implements the [HTTP RFC6265 draft] "cookie prefixes" extension by +/// automatically adding and removing a specified [`Prefix`] from cookies that +/// are added and retrieved from this jar, respectively. Additionally, upon +/// being added to this jar, cookies are automatically made to +/// [conform](Prefix::conform()) to the corresponding prefix's specifications. +/// +/// **Note:** Cookie prefixes are specified in an HTTP draft! Their meaning and +/// definition are subject to change. +/// +/// [HTTP RFC6265 draft]: +/// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-cookie-name-prefixes +pub struct PrefixedJar { + parent: J, + _prefix: PhantomData P>, +} + +/// The [`"__Host-"`] cookie [`Prefix`]. +/// +/// See [`Prefix`] and [`PrefixedJar`] for usage details. +/// +/// [`"__Host-"`]: +/// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__host-prefix +pub struct Host; + +/// The [`"__Secure-"`] cookie [`Prefix`]. +/// +/// See [`Prefix`] and [`PrefixedJar`] for usage details. +/// +/// [`"__Prefix-"`]: +/// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__prefix-prefix +pub struct Secure; + +/// Trait identifying [HTTP RFC6265 draft] cookie prefixes. +/// +/// A [`Prefix`] can be applied to cookies via a child [`PrefixedJar`], itself +/// obtainable via [`CookieJar::prefixed()`] and [`CookieJar::prefixed_mut()`]. +/// Cookies added/retrieved to/from these child jars have the corresponding +/// [prefix](Prefix::conform()) automatically prepended/removed as needed. +/// Additionally, added cookies are automatically make to +/// [conform](Prefix::conform()). +/// +/// **Note:** Cookie prefixes are specified in an HTTP draft! Their meaning and +/// definition are subject to change. +/// +/// [HTTP RFC6265 draft]: +/// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-cookie-name-prefixes +pub trait Prefix: private::Sealed { + /// The prefix string to prepend. + /// + /// See [`Host::PREFIX`] and [`Secure::PREFIX`] for specifics. + const PREFIX: &'static str; + + /// Alias to [`Host`]. + #[allow(non_upper_case_globals)] + const Host: Host = Host; + + /// Alias to [`Secure`]. + #[allow(non_upper_case_globals)] + const Secure: Secure = Secure; + + /// Modify `cookie` so it confirms with the requirements of `self`. + /// + /// See [`Host::conform()`] and [`Secure::conform()`] for specifics. + // + // This is the only required method. Everything else is shared across + // implementations via the default implementations below and should not be + // implemented. + fn conform(cookie: Cookie<'_>) -> Cookie<'_>; + + /// Returns a string with `name` prefixed with `self`. + #[doc(hidden)] + #[inline(always)] + fn prefixed_name(name: &str) -> String { + format!("{}{}", Self::PREFIX, name) + } + + /// Prefix `cookie`'s name with `Self`. + #[doc(hidden)] + fn prefix(mut cookie: Cookie<'_>) -> Cookie<'_> { + use crate::CookieStr; + + cookie.name = CookieStr::Concrete(match cookie.name { + CookieStr::Concrete(Cow::Owned(mut string)) => { + string.insert_str(0, Self::PREFIX); + string.into() + } + _ => Self::prefixed_name(cookie.name()).into(), + }); + + cookie + } + + /// Remove the prefix `Self` from `cookie`'s name and return it. + /// + /// If the prefix isn't in `cookie`, the cookie is returned unmodified. This + /// method is expected to be called only when `cookie`'s name is known to + /// contain the prefix. + #[doc(hidden)] + fn clip(mut cookie: Cookie<'_>) -> Cookie<'_> { + use std::borrow::Cow::*; + use crate::CookieStr::*; + + if !cookie.name().starts_with(Self::PREFIX) { + return cookie; + } + + let len = Self::PREFIX.len(); + cookie.name = match cookie.name { + Indexed(i, j) => Indexed(i + len, j), + Concrete(Borrowed(v)) => Concrete(Borrowed(&v[len..])), + Concrete(Owned(v)) => Concrete(Owned(v[len..].to_string())), + }; + + cookie + } + + /// Prefix and _conform_ `cookie`: prefix `cookie` with `Self` and make it + /// conform to the required specification by modifying it. + #[inline] + #[doc(hidden)] + fn apply(cookie: Cookie<'_>) -> Cookie<'_> { + Self::conform(Self::prefix(cookie)) + } +} + +impl PrefixedJar { + #[inline(always)] + pub(crate) fn new(parent: J) -> Self { + Self { parent, _prefix: PhantomData } + } +} + +impl> PrefixedJar { + /// Fetches the `Cookie` inside this jar with the prefix `P` and removes the + /// prefix before returning it. If the cookie isn't found, returns `None`. + /// + /// See [`CookieJar::prefixed()`] for more examples. + /// + /// # Example + /// + /// ```rust + /// use cookie::CookieJar; + /// use cookie::prefix::{Host, Secure}; + /// + /// // Add a `Host` prefixed cookie. + /// let mut jar = CookieJar::new(); + /// jar.prefixed_mut(Host).add(("h0st", "value")); + /// assert_eq!(jar.prefixed(Host).get("h0st").unwrap().name(), "h0st"); + /// assert_eq!(jar.prefixed(Host).get("h0st").unwrap().value(), "value"); + /// ``` + pub fn get(&self, name: &str) -> Option> { + self.parent.borrow() + .get(&P::prefixed_name(name)) + .map(|c| P::clip(c.clone())) + } +} + +impl> PrefixedJar { + /// Adds `cookie` to the parent jar. The cookie's name is prefixed with `P`, + /// and the cookie's attributes are made to [`conform`](Prefix::conform()). + /// + /// See [`CookieJar::prefixed_mut()`] for more examples. + /// + /// # Example + /// + /// ```rust + /// use cookie::{Cookie, CookieJar}; + /// use cookie::prefix::{Host, Secure}; + /// + /// // Add a `Host` prefixed cookie. + /// let mut jar = CookieJar::new(); + /// jar.prefixed_mut(Secure).add(Cookie::build(("name", "value")).secure(false)); + /// assert_eq!(jar.prefixed(Secure).get("name").unwrap().value(), "value"); + /// assert_eq!(jar.prefixed(Secure).get("name").unwrap().secure(), Some(true)); + /// ``` + pub fn add>>(&mut self, cookie: C) { + self.parent.borrow_mut().add(P::apply(cookie.into())); + } + + /// Adds `cookie` to the parent jar. The cookie's name is prefixed with `P`, + /// and the cookie's attributes are made to [`conform`](Prefix::conform()). + /// + /// 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. For accurate `delta` computations, this method should not + /// be called after calling `remove`. + /// + /// # Example + /// + /// ```rust + /// use cookie::{Cookie, CookieJar}; + /// use cookie::prefix::{Host, Secure}; + /// + /// // Add a `Host` prefixed cookie. + /// let mut jar = CookieJar::new(); + /// jar.prefixed_mut(Secure).add_original(("name", "value")); + /// assert_eq!(jar.iter().count(), 1); + /// assert_eq!(jar.delta().count(), 0); + /// ``` + pub fn add_original>>(&mut self, cookie: C) { + self.parent.borrow_mut().add_original(P::apply(cookie.into())); + } + + /// Removes `cookie` from the parent jar. + /// + /// The cookie's name is prefixed with `P`, and the cookie's attributes are + /// made to [`conform`](Prefix::conform()) before attempting to remove the + /// cookie. For correct removal, the passed in `cookie` must contain the + /// same `path` and `domain` as the cookie that was initially set. + /// + /// # Example + /// + /// ```rust + /// use cookie::{Cookie, CookieJar}; + /// use cookie::prefix::{Host, Secure}; + /// + /// let mut jar = CookieJar::new(); + /// let mut prefixed_jar = jar.prefixed_mut(Host); + /// + /// prefixed_jar.add(("name", "value")); + /// assert!(prefixed_jar.get("name").is_some()); + /// + /// prefixed_jar.remove("name"); + /// assert!(prefixed_jar.get("name").is_none()); + /// ``` + pub fn remove>>(&mut self, cookie: C) { + self.parent.borrow_mut().remove(P::apply(cookie.into())); + } +} + +impl Prefix for Host { + /// The [`"__Host-"` prefix] string. + /// + /// [`"__Host-"` prefix]: + /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__host-prefix + const PREFIX: &'static str = "__Host-"; + + /// Modify `cookie` so it conforms with the prefix requirements. + /// + /// **Note: this method is called automatically by [`PrefixedJar`]. It _does + /// not need to_ and _should not_ be called manually under normal + /// circumstances.** + /// + /// According to [RFC 6265bis-12 §4.1.3.2]: + /// + /// ```text + /// If a cookie's name begins with a case-sensitive match for the string + /// __Host-, then the cookie will have been set with a Secure attribute, + /// a Path attribute with a value of /, and no Domain attribute. + /// ``` + /// + /// As such, to make a cookie conforn, this method: + /// + /// * Sets [`secure`](Cookie::set_secure()) to `true`. + /// * Sets the [`path`](Cookie::set_path()) to `"/"`. + /// * Removes the [`domain`](Cookie::unset_domain()), if any. + /// + /// [RFC 6265bis-12 §4.1.3.2]: + /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__host-prefix + /// + /// # Example + /// + /// ```rust + /// use cookie::{CookieJar, Cookie, prefix::Host}; + /// + /// // A cookie with some non-conformant properties. + /// let cookie = Cookie::build(("name", "some-value")) + /// .secure(false) + /// .path("/foo/bar") + /// .domain("rocket.rs") + /// .http_only(true); + /// + /// // Add the cookie to the jar. + /// let mut jar = CookieJar::new(); + /// jar.prefixed_mut(Host).add(cookie); + /// + /// // Fetch the cookie: notice it's been made to conform. + /// let cookie = jar.prefixed(Host).get("name").unwrap(); + /// assert_eq!(cookie.name(), "name"); + /// assert_eq!(cookie.value(), "some-value"); + /// assert_eq!(cookie.secure(), Some(true)); + /// assert_eq!(cookie.path(), Some("/")); + /// assert_eq!(cookie.domain(), None); + /// assert_eq!(cookie.http_only(), Some(true)); + /// ``` + fn conform(mut cookie: Cookie<'_>) -> Cookie<'_> { + cookie.set_secure(true); + cookie.set_path("/"); + cookie.unset_domain(); + cookie + } +} + +impl Prefix for Secure { + /// The [`"__Secure-"` prefix] string. + /// + /// [`"__Secure-"` prefix]: + /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__secure-prefix + const PREFIX: &'static str = "__Secure-"; + + /// Modify `cookie` so it conforms with the prefix requirements. + /// + /// **Note: this method is called automatically by [`PrefixedJar`]. It _does + /// not need to_ and _should not_ be called manually under normal + /// circumstances.** + /// + /// According to [RFC 6265bis-12 §4.1.3.1]: + /// + /// ```text + /// If a cookie's name begins with a case-sensitive match for the string + /// __Secure-, then the cookie will have been set with a Secure + /// attribute. + /// ``` + /// + /// As such, to make a cookie conforn, this method: + /// + /// * Sets [`secure`](Cookie::set_secure()) to `true`. + /// + /// [RFC 6265bis-12 §4.1.3.1]: + /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__secure-prefix + /// + /// # Example + /// + /// ```rust + /// use cookie::{CookieJar, Cookie, prefix::Secure}; + /// + /// // A cookie with some non-conformant properties. + /// let cookie = Cookie::build(("name", "some-value")) + /// .secure(false) + /// .path("/guide") + /// .domain("rocket.rs") + /// .http_only(true); + /// + /// // Add the cookie to the jar. + /// let mut jar = CookieJar::new(); + /// jar.prefixed_mut(Secure).add(cookie); + /// + /// // Fetch the cookie: notice it's been made to conform. + /// let cookie = jar.prefixed(Secure).get("name").unwrap(); + /// assert_eq!(cookie.name(), "name"); + /// assert_eq!(cookie.value(), "some-value"); + /// assert_eq!(cookie.secure(), Some(true)); + /// assert_eq!(cookie.path(), Some("/guide")); + /// assert_eq!(cookie.domain(), Some("rocket.rs")); + /// assert_eq!(cookie.http_only(), Some(true)); + /// ``` + fn conform(mut cookie: Cookie<'_>) -> Cookie<'_> { + cookie.set_secure(true); + cookie + } +} + +mod private { + pub trait Sealed {} + + impl Sealed for super::Host {} + impl Sealed for super::Secure {} +} diff --git a/src/draft.rs b/src/same_site.rs similarity index 100% rename from src/draft.rs rename to src/same_site.rs diff --git a/src/secure/signed.rs b/src/secure/signed.rs index eaa2e82e..9dbb732f 100644 --- a/src/secure/signed.rs +++ b/src/secure/signed.rs @@ -136,7 +136,7 @@ impl> SignedJar { /// /// let key = Key::generate(); /// let mut jar = CookieJar::new(); - /// jar.signed_mut(&key).add(Cookie::new("name", "value")); + /// jar.signed_mut(&key).add(("name", "value")); /// /// assert_ne!(jar.get("name").unwrap().value(), "value"); /// assert!(jar.get("name").unwrap().value().contains("value")); @@ -164,7 +164,7 @@ impl> SignedJar { /// /// let key = Key::generate(); /// let mut jar = CookieJar::new(); - /// jar.signed_mut(&key).add_original(Cookie::new("name", "value")); + /// jar.signed_mut(&key).add_original(("name", "value")); /// /// assert_eq!(jar.iter().count(), 1); /// assert_eq!(jar.delta().count(), 0); @@ -192,7 +192,7 @@ impl> SignedJar { /// let mut jar = CookieJar::new(); /// let mut signed_jar = jar.signed_mut(&key); /// - /// signed_jar.add(Cookie::new("name", "value")); + /// signed_jar.add(("name", "value")); /// assert!(signed_jar.get("name").is_some()); /// /// signed_jar.remove("name");