cookie/
prefix.rs

1use std::marker::PhantomData;
2use std::borrow::{Borrow, BorrowMut, Cow};
3
4use crate::{CookieJar, Cookie};
5
6/// A child jar that automatically [prefixes](Prefix) cookies.
7///
8/// Obtained via [`CookieJar::prefixed()`] and [`CookieJar::prefixed_mut()`].
9///
10/// This jar implements the [HTTP RFC6265 draft] "cookie prefixes" extension by
11/// automatically adding and removing a specified [`Prefix`] from cookies that
12/// are added and retrieved from this jar, respectively. Additionally, upon
13/// being added to this jar, cookies are automatically made to
14/// [conform](Prefix::conform()) to the corresponding prefix's specifications.
15///
16/// **Note:** Cookie prefixes are specified in an HTTP draft! Their meaning and
17/// definition are subject to change.
18///
19/// [HTTP RFC6265 draft]:
20/// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-cookie-name-prefixes
21pub struct PrefixedJar<P: Prefix, J> {
22    parent: J,
23    _prefix: PhantomData<fn() -> P>,
24}
25
26/// The [`"__Host-"`] cookie [`Prefix`].
27///
28/// See [`Prefix`] and [`PrefixedJar`] for usage details.
29///
30/// [`"__Host-"`]:
31/// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__host-prefix
32pub struct Host;
33
34/// The [`"__Secure-"`] cookie [`Prefix`].
35///
36/// See [`Prefix`] and [`PrefixedJar`] for usage details.
37///
38/// [`"__Secure-"`]:
39/// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__secure-prefix
40pub struct Secure;
41
42/// Trait identifying [HTTP RFC6265 draft] cookie prefixes.
43///
44/// A [`Prefix`] can be applied to cookies via a child [`PrefixedJar`], itself
45/// obtainable via [`CookieJar::prefixed()`] and [`CookieJar::prefixed_mut()`].
46/// Cookies added/retrieved to/from these child jars have the corresponding
47/// [prefix](Prefix::conform()) automatically prepended/removed as needed.
48/// Additionally, added cookies are automatically make to
49/// [conform](Prefix::conform()).
50///
51/// **Note:** Cookie prefixes are specified in an HTTP draft! Their meaning and
52/// definition are subject to change.
53///
54/// [HTTP RFC6265 draft]:
55/// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-cookie-name-prefixes
56pub trait Prefix: private::Sealed {
57    /// The prefix string to prepend.
58    ///
59    /// See [`Host::PREFIX`] and [`Secure::PREFIX`] for specifics.
60    const PREFIX: &'static str;
61
62    /// Alias to [`Host`].
63    #[allow(non_upper_case_globals)]
64    const Host: Host = Host;
65
66    /// Alias to [`Secure`].
67    #[allow(non_upper_case_globals)]
68    const Secure: Secure = Secure;
69
70    /// Modify `cookie` so it conforms to the requirements of `self`.
71    ///
72    /// See [`Host::conform()`] and [`Secure::conform()`] for specifics.
73    //
74    // This is the only required method. Everything else is shared across
75    // implementations via the default implementations below and should not be
76    // implemented.
77    fn conform(cookie: Cookie<'_>) -> Cookie<'_>;
78
79    /// Returns a string with `name` prefixed with `self`.
80    #[doc(hidden)]
81    #[inline(always)]
82    fn prefixed_name(name: &str) -> String {
83        format!("{}{}", Self::PREFIX, name)
84    }
85
86    /// Prefix `cookie`'s name with `Self`.
87    #[doc(hidden)]
88    fn prefix(mut cookie: Cookie<'_>) -> Cookie<'_> {
89        use crate::CookieStr;
90
91        cookie.name = CookieStr::Concrete(match cookie.name {
92            CookieStr::Concrete(Cow::Owned(mut string)) => {
93                string.insert_str(0, Self::PREFIX);
94                string.into()
95            }
96            _ => Self::prefixed_name(cookie.name()).into(),
97        });
98
99        cookie
100    }
101
102    /// Remove the prefix `Self` from `cookie`'s name and return it.
103    ///
104    /// If the prefix isn't in `cookie`, the cookie is returned unmodified. This
105    /// method is expected to be called only when `cookie`'s name is known to
106    /// contain the prefix.
107    #[doc(hidden)]
108    fn clip(mut cookie: Cookie<'_>) -> Cookie<'_> {
109        use std::borrow::Cow::*;
110        use crate::CookieStr::*;
111
112        if !cookie.name().starts_with(Self::PREFIX) {
113            return cookie;
114        }
115
116        let len = Self::PREFIX.len();
117        cookie.name = match cookie.name {
118            Indexed(i, j) => Indexed(i + len, j),
119            Concrete(Borrowed(v)) => Concrete(Borrowed(&v[len..])),
120            Concrete(Owned(v)) => Concrete(Owned(v[len..].to_string())),
121        };
122
123        cookie
124    }
125
126    /// Prefix and _conform_ `cookie`: prefix `cookie` with `Self` and make it
127    /// conform to the required specification by modifying it.
128    #[inline]
129    #[doc(hidden)]
130    fn apply(cookie: Cookie<'_>) -> Cookie<'_> {
131        Self::conform(Self::prefix(cookie))
132    }
133}
134
135impl<P: Prefix, J> PrefixedJar<P, J> {
136    #[inline(always)]
137    pub(crate) fn new(parent: J) -> Self {
138        Self { parent, _prefix: PhantomData }
139    }
140}
141
142impl<P: Prefix, J: Borrow<CookieJar>> PrefixedJar<P, J> {
143    /// Fetches the `Cookie` inside this jar with the prefix `P` and removes the
144    /// prefix before returning it. If the cookie isn't found, returns `None`.
145    ///
146    /// See [`CookieJar::prefixed()`] for more examples.
147    ///
148    /// # Example
149    ///
150    /// ```rust
151    /// use cookie::CookieJar;
152    /// use cookie::prefix::{Host, Secure};
153    ///
154    /// // Add a `Host` prefixed cookie.
155    /// let mut jar = CookieJar::new();
156    /// jar.prefixed_mut(Host).add(("h0st", "value"));
157    /// assert_eq!(jar.prefixed(Host).get("h0st").unwrap().name(), "h0st");
158    /// assert_eq!(jar.prefixed(Host).get("h0st").unwrap().value(), "value");
159    /// ```
160    pub fn get(&self, name: &str) -> Option<Cookie<'static>> {
161        self.parent.borrow()
162            .get(&P::prefixed_name(name))
163            .map(|c| P::clip(c.clone()))
164    }
165}
166
167impl<P: Prefix, J: BorrowMut<CookieJar>> PrefixedJar<P, J> {
168    /// Adds `cookie` to the parent jar. The cookie's name is prefixed with `P`,
169    /// and the cookie's attributes are made to [`conform`](Prefix::conform()).
170    ///
171    /// See [`CookieJar::prefixed_mut()`] for more examples.
172    ///
173    /// # Example
174    ///
175    /// ```rust
176    /// use cookie::{Cookie, CookieJar};
177    /// use cookie::prefix::{Host, Secure};
178    ///
179    /// // Add a `Host` prefixed cookie.
180    /// let mut jar = CookieJar::new();
181    /// jar.prefixed_mut(Secure).add(Cookie::build(("name", "value")).secure(false));
182    /// assert_eq!(jar.prefixed(Secure).get("name").unwrap().value(), "value");
183    /// assert_eq!(jar.prefixed(Secure).get("name").unwrap().secure(), Some(true));
184    /// ```
185    pub fn add<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
186        self.parent.borrow_mut().add(P::apply(cookie.into()));
187    }
188
189    /// Adds `cookie` to the parent jar. The cookie's name is prefixed with `P`,
190    /// and the cookie's attributes are made to [`conform`](Prefix::conform()).
191    ///
192    /// Adding an original cookie does not affect the [`CookieJar::delta()`]
193    /// computation. This method is intended to be used to seed the cookie jar
194    /// with cookies. For accurate `delta` computations, this method should not
195    /// be called after calling `remove`.
196    ///
197    /// # Example
198    ///
199    /// ```rust
200    /// use cookie::{Cookie, CookieJar};
201    /// use cookie::prefix::{Host, Secure};
202    ///
203    /// // Add a `Host` prefixed cookie.
204    /// let mut jar = CookieJar::new();
205    /// jar.prefixed_mut(Secure).add_original(("name", "value"));
206    /// assert_eq!(jar.iter().count(), 1);
207    /// assert_eq!(jar.delta().count(), 0);
208    /// ```
209    pub fn add_original<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
210        self.parent.borrow_mut().add_original(P::apply(cookie.into()));
211    }
212
213    /// Removes `cookie` from the parent jar.
214    ///
215    /// The cookie's name is prefixed with `P`, and the cookie's attributes are
216    /// made to [`conform`](Prefix::conform()) before attempting to remove the
217    /// cookie. For correct removal, the passed in `cookie` must contain the
218    /// same `path` and `domain` as the cookie that was initially set.
219    ///
220    /// # Example
221    ///
222    /// ```rust
223    /// use cookie::{Cookie, CookieJar};
224    /// use cookie::prefix::{Host, Secure};
225    ///
226    /// let mut jar = CookieJar::new();
227    /// let mut prefixed_jar = jar.prefixed_mut(Host);
228    ///
229    /// prefixed_jar.add(("name", "value"));
230    /// assert!(prefixed_jar.get("name").is_some());
231    ///
232    /// prefixed_jar.remove("name");
233    /// assert!(prefixed_jar.get("name").is_none());
234    /// ```
235    pub fn remove<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
236        self.parent.borrow_mut().remove(P::apply(cookie.into()));
237    }
238}
239
240impl Prefix for Host {
241    /// The [`"__Host-"` prefix] string.
242    ///
243    /// [`"__Host-"` prefix]:
244    /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__host-prefix
245    const PREFIX: &'static str = "__Host-";
246
247    /// Modify `cookie` so it conforms to the prefix's requirements.
248    ///
249    /// **Note: this method is called automatically by [`PrefixedJar`]. It _does
250    /// not need to_ and _should not_ be called manually under normal
251    /// circumstances.**
252    ///
253    /// According to [RFC 6265bis-12 §4.1.3.2]:
254    ///
255    /// ```text
256    /// If a cookie's name begins with a case-sensitive match for the string
257    /// __Host-, then the cookie will have been set with a Secure attribute,
258    /// a Path attribute with a value of /, and no Domain attribute.
259    /// ```
260    ///
261    /// As such, to make a cookie conforn, this method:
262    ///
263    ///   * Sets [`secure`](Cookie::set_secure()) to `true`.
264    ///   * Sets the [`path`](Cookie::set_path()) to `"/"`.
265    ///   * Removes the [`domain`](Cookie::unset_domain()), if any.
266    ///
267    /// [RFC 6265bis-12 §4.1.3.2]:
268    /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__host-prefix
269    ///
270    /// # Example
271    ///
272    /// ```rust
273    /// use cookie::{CookieJar, Cookie, prefix::Host};
274    ///
275    /// // A cookie with some non-conformant properties.
276    /// let cookie = Cookie::build(("name", "some-value"))
277    ///     .secure(false)
278    ///     .path("/foo/bar")
279    ///     .domain("rocket.rs")
280    ///     .http_only(true);
281    ///
282    /// // Add the cookie to the jar.
283    /// let mut jar = CookieJar::new();
284    /// jar.prefixed_mut(Host).add(cookie);
285    ///
286    /// // Fetch the cookie: notice it's been made to conform.
287    /// let cookie = jar.prefixed(Host).get("name").unwrap();
288    /// assert_eq!(cookie.name(), "name");
289    /// assert_eq!(cookie.value(), "some-value");
290    /// assert_eq!(cookie.secure(), Some(true));
291    /// assert_eq!(cookie.path(), Some("/"));
292    /// assert_eq!(cookie.domain(), None);
293    /// assert_eq!(cookie.http_only(), Some(true));
294    /// ```
295    fn conform(mut cookie: Cookie<'_>) -> Cookie<'_> {
296        cookie.set_secure(true);
297        cookie.set_path("/");
298        cookie.unset_domain();
299        cookie
300    }
301}
302
303impl Prefix for Secure {
304    /// The [`"__Secure-"` prefix] string.
305    ///
306    /// [`"__Secure-"` prefix]:
307    /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__secure-prefix
308    const PREFIX: &'static str = "__Secure-";
309
310    /// Modify `cookie` so it conforms to the prefix's requirements.
311    ///
312    /// **Note: this method is called automatically by [`PrefixedJar`]. It _does
313    /// not need to_ and _should not_ be called manually under normal
314    /// circumstances.**
315    ///
316    /// According to [RFC 6265bis-12 §4.1.3.1]:
317    ///
318    /// ```text
319    /// If a cookie's name begins with a case-sensitive match for the string
320    /// __Secure-, then the cookie will have been set with a Secure
321    /// attribute.
322    /// ```
323    ///
324    /// As such, to make a cookie conforn, this method:
325    ///
326    ///   * Sets [`secure`](Cookie::set_secure()) to `true`.
327    ///
328    /// [RFC 6265bis-12 §4.1.3.1]:
329    /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-the-__secure-prefix
330    ///
331    /// # Example
332    ///
333    /// ```rust
334    /// use cookie::{CookieJar, Cookie, prefix::Secure};
335    ///
336    /// // A cookie with some non-conformant properties.
337    /// let cookie = Cookie::build(("name", "some-value"))
338    ///     .secure(false)
339    ///     .path("/guide")
340    ///     .domain("rocket.rs")
341    ///     .http_only(true);
342    ///
343    /// // Add the cookie to the jar.
344    /// let mut jar = CookieJar::new();
345    /// jar.prefixed_mut(Secure).add(cookie);
346    ///
347    /// // Fetch the cookie: notice it's been made to conform.
348    /// let cookie = jar.prefixed(Secure).get("name").unwrap();
349    /// assert_eq!(cookie.name(), "name");
350    /// assert_eq!(cookie.value(), "some-value");
351    /// assert_eq!(cookie.secure(), Some(true));
352    /// assert_eq!(cookie.path(), Some("/guide"));
353    /// assert_eq!(cookie.domain(), Some("rocket.rs"));
354    /// assert_eq!(cookie.http_only(), Some(true));
355    /// ```
356    fn conform(mut cookie: Cookie<'_>) -> Cookie<'_> {
357        cookie.set_secure(true);
358        cookie
359    }
360}
361
362mod private {
363    pub trait Sealed {}
364
365    impl Sealed for super::Host {}
366    impl Sealed for super::Secure {}
367}