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}