rocket_community/http/
cookies.rs

1use std::fmt;
2
3use parking_lot::Mutex;
4
5use crate::{Orbit, Rocket};
6
7#[doc(inline)]
8pub use cookie::{Cookie, Iter, SameSite};
9
10/// Collection of one or more HTTP cookies.
11///
12/// `CookieJar` allows for retrieval of cookies from an incoming request. It
13/// also tracks modifications (additions and removals) and marks them as
14/// pending.
15///
16/// # Pending
17///
18/// Changes to a `CookieJar` are _not_ visible via the normal [`get()`] and
19/// [`get_private()`] methods. This is typically the desired effect as a
20/// `CookieJar` always reflects the cookies in an incoming request. In cases
21/// where this is not desired, the [`get_pending()`] method is available, which
22/// always returns the latest changes.
23///
24/// ```rust
25/// # #[macro_use] extern crate rocket_community as rocket;
26/// use rocket::http::{CookieJar, Cookie};
27///
28/// #[get("/message")]
29/// fn message(jar: &CookieJar<'_>) {
30///     jar.add(("message", "hello!"));
31///     jar.add(Cookie::build(("session", "bye!")).expires(None));
32///
33///     // `get()` does not reflect changes.
34///     assert!(jar.get("session").is_none());
35///     assert_eq!(jar.get("message").map(|c| c.value()), Some("hi"));
36///
37///     // `get_pending()` does.
38///     let session_pending = jar.get_pending("session");
39///     let message_pending = jar.get_pending("message");
40///     assert_eq!(session_pending.as_ref().map(|c| c.value()), Some("bye!"));
41///     assert_eq!(message_pending.as_ref().map(|c| c.value()), Some("hello!"));
42///     # jar.remove("message");
43///     # assert_eq!(jar.get("message").map(|c| c.value()), Some("hi"));
44///     # assert!(jar.get_pending("message").is_none());
45/// }
46/// # fn main() {
47/// #     use rocket::local::blocking::Client;
48/// #     let client = Client::debug_with(routes![message]).unwrap();
49/// #     let response = client.get("/message")
50/// #         .cookie(("message", "hi"))
51/// #         .dispatch();
52/// #
53/// #     assert!(response.status().class().is_success());
54/// # }
55/// ```
56///
57/// # Usage
58///
59/// A type of `&CookieJar` can be retrieved via its `FromRequest` implementation
60/// as a request guard or via the [`Request::cookies()`] method. Individual
61/// cookies can be retrieved via the [`get()`] and [`get_private()`] methods.
62/// Pending changes can be observed via the [`get_pending()`] method. Cookies
63/// can be added or removed via the [`add()`], [`add_private()`], [`remove()`],
64/// and [`remove_private()`] methods.
65///
66/// [`Request::cookies()`]: crate::Request::cookies()
67/// [`get()`]: #method.get
68/// [`get_private()`]: #method.get_private
69/// [`get_pending()`]: #method.get_pending
70/// [`add()`]: #method.add
71/// [`add_private()`]: #method.add_private
72/// [`remove()`]: #method.remove
73/// [`remove_private()`]: #method.remove_private
74///
75/// ## Examples
76///
77/// The following example shows `&CookieJar` being used as a request guard in a
78/// handler to retrieve the value of a "message" cookie.
79///
80/// ```rust
81/// # #[macro_use] extern crate rocket_community as rocket;
82/// use rocket::http::CookieJar;
83///
84/// #[get("/message")]
85/// fn message<'a>(jar: &'a CookieJar<'_>) -> Option<&'a str> {
86///     jar.get("message").map(|cookie| cookie.value())
87/// }
88/// # fn main() {  }
89/// ```
90///
91/// The following snippet shows `&CookieJar` being retrieved from a `Request` in
92/// a custom request guard implementation for `User`. A [private cookie]
93/// containing a user's ID is retrieved. If the cookie exists and the ID parses
94/// as an integer, a `User` structure is validated. Otherwise, the guard
95/// forwards.
96///
97/// [private cookie]: #method.add_private
98///
99/// ```rust
100/// # extern crate rocket_community as rocket;
101/// # #[cfg(feature = "secrets")] {
102/// use rocket::http::Status;
103/// use rocket::request::{self, Request, FromRequest};
104/// use rocket::outcome::IntoOutcome;
105///
106/// // In practice, we'd probably fetch the user from the database.
107/// struct User(usize);
108///
109/// #[rocket::async_trait]
110/// impl<'r> FromRequest<'r> for User {
111///     type Error = std::convert::Infallible;
112///
113///     async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
114///         request.cookies()
115///             .get_private("user_id")
116///             .and_then(|c| c.value().parse().ok())
117///             .map(|id| User(id))
118///             .or_forward(Status::Unauthorized)
119///     }
120/// }
121/// # }
122/// # fn main() { }
123/// ```
124///
125/// # Private Cookies
126///
127/// _Private_ cookies are just like regular cookies except that they are
128/// encrypted using authenticated encryption, a form of encryption which
129/// simultaneously provides confidentiality, integrity, and authenticity. This
130/// means that private cookies cannot be inspected, tampered with, or
131/// manufactured by clients. If you prefer, you can think of private cookies as
132/// being signed and encrypted.
133///
134/// Private cookies can be retrieved, added, and removed from a `CookieJar`
135/// collection via the [`get_private()`], [`add_private()`], and
136/// [`remove_private()`] methods.
137///
138/// ## Encryption Key
139///
140/// To encrypt private cookies, Rocket uses the 256-bit key specified in the
141/// `secret_key` configuration parameter. If one is not specified, Rocket will
142/// automatically generate a fresh key. Note, however, that a private cookie can
143/// only be decrypted with the same key with which it was encrypted. As such, it
144/// is important to set a `secret_key` configuration parameter when using
145/// private cookies so that cookies decrypt properly after an application
146/// restart. Rocket will emit a warning if an application is run in production
147/// mode without a configured `secret_key`.
148///
149/// Generating a string suitable for use as a `secret_key` configuration value
150/// is usually done through tools like `openssl`. Using `openssl`, for instance,
151/// a 256-bit base64 key can be generated with the command `openssl rand -base64
152/// 32`.
153pub struct CookieJar<'a> {
154    jar: cookie::CookieJar,
155    ops: Mutex<Vec<Op>>,
156    pub(crate) state: CookieState<'a>,
157}
158
159#[derive(Copy, Clone)]
160pub(crate) struct CookieState<'a> {
161    pub secure: bool,
162    #[cfg_attr(not(feature = "secrets"), allow(unused))]
163    pub config: &'a crate::Config,
164}
165
166#[derive(Clone)]
167enum Op {
168    Add(Cookie<'static>, bool),
169    Remove(Cookie<'static>),
170}
171
172impl<'a> CookieJar<'a> {
173    pub(crate) fn new(base: Option<cookie::CookieJar>, rocket: &'a Rocket<Orbit>) -> Self {
174        CookieJar {
175            jar: base.unwrap_or_default(),
176            ops: Mutex::new(Vec::new()),
177            state: CookieState {
178                // This is updated dynamically when headers are received.
179                secure: rocket.endpoints().all(|e| e.is_tls()),
180                config: rocket.config(),
181            },
182        }
183    }
184
185    /// Returns a reference to the _original_ `Cookie` inside this container
186    /// with the name `name`. If no such cookie exists, returns `None`.
187    ///
188    /// **Note:** This method _does not_ observe changes made via additions and
189    /// removals to the cookie jar. To observe those changes, use
190    /// [`CookieJar::get_pending()`].
191    ///
192    /// # Example
193    ///
194    /// ```rust
195    /// # #[macro_use] extern crate rocket_community as rocket;
196    /// use rocket::http::CookieJar;
197    ///
198    /// #[get("/")]
199    /// fn handler(jar: &CookieJar<'_>) {
200    ///     let cookie = jar.get("name");
201    /// }
202    /// ```
203    pub fn get(&self, name: &str) -> Option<&Cookie<'static>> {
204        self.jar.get(name)
205    }
206
207    /// Retrieves the _original_ `Cookie` inside this collection with the name
208    /// `name` and authenticates and decrypts the cookie's value. If the cookie
209    /// cannot be found, or the cookie fails to authenticate or decrypt, `None`
210    /// is returned.
211    ///
212    /// **Note:** This method _does not_ observe changes made via additions and
213    /// removals to the cookie jar. To observe those changes, use
214    /// [`CookieJar::get_pending()`].
215    ///
216    /// # Example
217    ///
218    /// ```rust
219    /// # #[macro_use] extern crate rocket_community as rocket;
220    /// use rocket::http::CookieJar;
221    ///
222    /// #[get("/")]
223    /// fn handler(jar: &CookieJar<'_>) {
224    ///     let cookie = jar.get_private("name");
225    /// }
226    /// ```
227    #[cfg(feature = "secrets")]
228    #[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
229    pub fn get_private(&self, name: &str) -> Option<Cookie<'static>> {
230        self.jar
231            .private(&self.state.config.secret_key.key)
232            .get(name)
233    }
234
235    /// Returns a reference to the _original or pending_ `Cookie` inside this
236    /// container with the name `name`, irrespective of whether the cookie was
237    /// private or not. If no such cookie exists, returns `None`.
238    ///
239    /// In general, due to performance overhead, calling this method should be
240    /// avoided if it is known that a cookie called `name` is not pending.
241    /// Instead, prefer to use [`CookieJar::get()`] or
242    /// [`CookieJar::get_private()`].
243    ///
244    /// # Example
245    ///
246    /// ```rust
247    /// # #[macro_use] extern crate rocket_community as rocket;
248    /// use rocket::http::CookieJar;
249    ///
250    /// #[get("/")]
251    /// fn handler(jar: &CookieJar<'_>) {
252    ///     let pending_cookie = jar.get_pending("name");
253    /// }
254    /// ```
255    pub fn get_pending(&self, name: &str) -> Option<Cookie<'static>> {
256        let ops = self.ops.lock();
257        if let Some(op) = ops.iter().rev().find(|op| op.cookie().name() == name) {
258            return match op {
259                Op::Add(c, _) => Some(c.clone()),
260                Op::Remove(_) => None,
261            };
262        }
263
264        drop(ops);
265
266        #[cfg(feature = "secrets")]
267        {
268            self.get_private(name).or_else(|| self.get(name).cloned())
269        }
270
271        #[cfg(not(feature = "secrets"))]
272        {
273            self.get(name).cloned()
274        }
275    }
276
277    /// Adds `cookie` to this collection.
278    ///
279    /// Unless a value is set for the given property, the following defaults are
280    /// set on `cookie` before being added to `self`:
281    ///
282    ///    * `path`: `"/"`
283    ///    * `SameSite`: `Strict`
284    ///    * `Secure`: `true` if [`Request::context_is_likely_secure()`]
285    ///
286    /// These defaults ensure maximum usability and security. For additional
287    /// security, you may wish to set the `secure` flag explicitly.
288    ///
289    /// [`Request::context_is_likely_secure()`]: crate::Request::context_is_likely_secure()
290    ///
291    /// # Example
292    ///
293    /// ```rust
294    /// # #[macro_use] extern crate rocket_community as rocket;
295    /// use rocket::http::{Cookie, SameSite, CookieJar};
296    ///
297    /// #[get("/")]
298    /// fn handler(jar: &CookieJar<'_>) {
299    ///     jar.add(("first", "value"));
300    ///
301    ///     let cookie = Cookie::build(("other", "value_two"))
302    ///         .path("/")
303    ///         .secure(true)
304    ///         .same_site(SameSite::Lax);
305    ///
306    ///     jar.add(cookie);
307    /// }
308    /// ```
309    pub fn add<C: Into<Cookie<'static>>>(&self, cookie: C) {
310        let mut cookie = cookie.into();
311        self.set_defaults(&mut cookie);
312        self.ops.lock().push(Op::Add(cookie, false));
313    }
314
315    /// Adds `cookie` to the collection. The cookie's value is encrypted with
316    /// authenticated encryption assuring confidentiality, integrity, and
317    /// authenticity. The cookie can later be retrieved using
318    /// [`get_private`](#method.get_private) and removed using
319    /// [`remove_private`](#method.remove_private).
320    ///
321    /// Unless a value is set for the given property, the following defaults are
322    /// set on `cookie` before being added to `self`:
323    ///
324    ///    * `path`: `"/"`
325    ///    * `SameSite`: `Strict`
326    ///    * `HttpOnly`: `true`
327    ///    * `Expires`: 1 week from now
328    ///    * `Secure`: `true` if [`Request::context_is_likely_secure()`]
329    ///
330    /// These defaults ensure maximum usability and security. For additional
331    /// security, you may wish to set the `secure` flag explicitly and
332    /// unconditionally.
333    ///
334    /// [`Request::context_is_likely_secure()`]: crate::Request::context_is_likely_secure()
335    ///
336    /// # Example
337    ///
338    /// ```rust
339    /// # #[macro_use] extern crate rocket_community as rocket;
340    /// use rocket::http::CookieJar;
341    ///
342    /// #[get("/")]
343    /// fn handler(jar: &CookieJar<'_>) {
344    ///     jar.add_private(("name", "value"));
345    /// }
346    /// ```
347    #[cfg(feature = "secrets")]
348    #[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
349    pub fn add_private<C: Into<Cookie<'static>>>(&self, cookie: C) {
350        let mut cookie = cookie.into();
351        self.set_private_defaults(&mut cookie);
352        self.ops.lock().push(Op::Add(cookie, true));
353    }
354
355    /// Removes `cookie` from this collection and generates a "removal" cookie
356    /// to send to the client on response. A "removal" cookie is a cookie that
357    /// has the same name as the original cookie but has an empty value, a
358    /// max-age of 0, and an expiration date far in the past.
359    ///
360    /// **For successful removal, `cookie` must contain the same `path` and
361    /// `domain` as the cookie that was originally set. The cookie will fail to
362    /// be deleted if any other `path` and `domain` are provided. For
363    /// convenience, a path of `"/"` is automatically set when one is not
364    /// specified.** The full list of defaults when corresponding values aren't
365    /// specified is:
366    ///
367    ///    * `path`: `"/"`
368    ///    * `SameSite`: `Lax`
369    ///
370    /// <small>Note: a default setting of `Lax` for `SameSite` carries no
371    /// security implications: the removal cookie has expired, so it is never
372    /// transferred to any origin.</small>
373    ///
374    /// # Example
375    ///
376    /// ```rust
377    /// # #[macro_use] extern crate rocket_community as rocket;
378    /// use rocket::http::{Cookie, CookieJar};
379    ///
380    /// #[get("/")]
381    /// fn handler(jar: &CookieJar<'_>) {
382    ///     // `path` and `SameSite` are set to defaults (`/` and `Lax`)
383    ///     jar.remove("name");
384    ///
385    ///     // Use a custom-built cookie to set a custom path.
386    ///     jar.remove(Cookie::build("name").path("/login"));
387    ///
388    ///     // Use a custom-built cookie to set a custom path and domain.
389    ///     jar.remove(Cookie::build("id").path("/guide").domain("rocket.rs"));
390    /// }
391    /// ```
392    pub fn remove<C: Into<Cookie<'static>>>(&self, cookie: C) {
393        let mut cookie = cookie.into();
394        Self::set_removal_defaults(&mut cookie);
395        self.ops.lock().push(Op::Remove(cookie));
396    }
397
398    /// Removes the private `cookie` from the collection.
399    ///
400    /// **For successful removal, `cookie` must contain the same `path` and
401    /// `domain` as the cookie that was originally set. The cookie will fail to
402    /// be deleted if any other `path` and `domain` are provided. For
403    /// convenience, a path of `"/"` is automatically set when one is not
404    /// specified.** The full list of defaults when corresponding values aren't
405    /// specified is:
406    ///
407    ///    * `path`: `"/"`
408    ///    * `SameSite`: `Lax`
409    ///
410    /// <small>Note: a default setting of `Lax` for `SameSite` carries no
411    /// security implications: the removal cookie has expired, so it is never
412    /// transferred to any origin.</small>
413    ///
414    /// # Example
415    ///
416    /// ```rust
417    /// # #[macro_use] extern crate rocket_community as rocket;
418    /// use rocket::http::{CookieJar, Cookie};
419    ///
420    /// #[get("/")]
421    /// fn handler(jar: &CookieJar<'_>) {
422    ///     // `path` and `SameSite` are set to defaults (`/` and `Lax`)
423    ///     jar.remove_private("name");
424    ///
425    ///     // Use a custom-built cookie to set a custom path.
426    ///     jar.remove_private(Cookie::build("name").path("/login"));
427    ///
428    ///     // Use a custom-built cookie to set a custom path and domain.
429    ///     let cookie = Cookie::build("id").path("/guide").domain("rocket.rs");
430    ///     jar.remove_private(cookie);
431    /// }
432    /// ```
433    #[cfg(feature = "secrets")]
434    #[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
435    pub fn remove_private<C: Into<Cookie<'static>>>(&self, cookie: C) {
436        let mut cookie = cookie.into();
437        Self::set_removal_defaults(&mut cookie);
438        self.ops.lock().push(Op::Remove(cookie));
439    }
440
441    /// Returns an iterator over all of the _original_ cookies present in this
442    /// collection.
443    ///
444    /// **Note:** This method _does not_ observe changes made via additions and
445    /// removals to the cookie jar.
446    ///
447    /// # Example
448    ///
449    /// ```rust
450    /// # #[macro_use] extern crate rocket_community as rocket;
451    /// use rocket::http::CookieJar;
452    ///
453    /// #[get("/")]
454    /// fn handler(jar: &CookieJar<'_>) {
455    ///     for c in jar.iter() {
456    ///         println!("Name: {:?}, Value: {:?}", c.name(), c.value());
457    ///     }
458    /// }
459    /// ```
460    pub fn iter(&self) -> impl Iterator<Item = &Cookie<'static>> {
461        self.jar.iter()
462    }
463
464    /// Removes all delta cookies.
465    #[inline(always)]
466    pub(crate) fn reset_delta(&self) {
467        self.ops.lock().clear();
468    }
469
470    /// TODO: This could be faster by just returning the cookies directly via
471    /// an ordered hash-set of sorts.
472    pub(crate) fn take_delta_jar(&self) -> cookie::CookieJar {
473        let ops = std::mem::take(&mut *self.ops.lock());
474        let mut jar = cookie::CookieJar::new();
475
476        for op in ops {
477            match op {
478                Op::Add(c, false) => jar.add(c),
479                #[cfg(feature = "secrets")]
480                Op::Add(c, true) => {
481                    jar.private_mut(&self.state.config.secret_key.key).add(c);
482                }
483                Op::Remove(mut c) => {
484                    if self.jar.get(c.name()).is_some() {
485                        c.make_removal();
486                        jar.add(c);
487                    } else {
488                        jar.remove(c);
489                    }
490                }
491                #[allow(unreachable_patterns)]
492                _ => unreachable!(),
493            }
494        }
495
496        jar
497    }
498
499    /// Adds an original `cookie` to this collection.
500    #[inline(always)]
501    pub(crate) fn add_original(&mut self, cookie: Cookie<'static>) {
502        self.jar.add_original(cookie)
503    }
504
505    /// Adds an original, private `cookie` to the collection.
506    #[cfg(feature = "secrets")]
507    #[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
508    #[inline(always)]
509    pub(crate) fn add_original_private(&mut self, cookie: Cookie<'static>) {
510        self.jar
511            .private_mut(&self.state.config.secret_key.key)
512            .add_original(cookie);
513    }
514
515    /// For each property mentioned below, this method checks if there is a
516    /// provided value and if there is none, sets a default value. Default
517    /// values are:
518    ///
519    ///    * `path`: `"/"`
520    ///    * `SameSite`: `Strict`
521    ///    * `Secure`: `true` if `Request::context_is_likely_secure()`
522    fn set_defaults(&self, cookie: &mut Cookie<'static>) {
523        if cookie.path().is_none() {
524            cookie.set_path("/");
525        }
526
527        if cookie.same_site().is_none() {
528            cookie.set_same_site(SameSite::Strict);
529        }
530
531        if cookie.secure().is_none() && self.state.secure {
532            cookie.set_secure(true);
533        }
534    }
535
536    /// For each property below, this method checks if there is a provided value
537    /// and if there is none, sets a default value. Default values are:
538    ///
539    ///    * `path`: `"/"`
540    ///    * `SameSite`: `Lax`
541    fn set_removal_defaults(cookie: &mut Cookie<'static>) {
542        if cookie.path().is_none() {
543            cookie.set_path("/");
544        }
545
546        if cookie.same_site().is_none() {
547            cookie.set_same_site(SameSite::Lax);
548        }
549    }
550
551    /// For each property mentioned below, this method checks if there is a
552    /// provided value and if there is none, sets a default value. Default
553    /// values are:
554    ///
555    ///    * `path`: `"/"`
556    ///    * `SameSite`: `Strict`
557    ///    * `HttpOnly`: `true`
558    ///    * `Expires`: 1 week from now
559    ///    * `Secure`: `true` if `Request::context_is_likely_secure()`
560    #[cfg(feature = "secrets")]
561    #[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
562    fn set_private_defaults(&self, cookie: &mut Cookie<'static>) {
563        self.set_defaults(cookie);
564
565        if cookie.http_only().is_none() {
566            cookie.set_http_only(true);
567        }
568
569        if cookie.expires().is_none() {
570            cookie.set_expires(time::OffsetDateTime::now_utc() + time::Duration::weeks(1));
571        }
572    }
573}
574
575impl fmt::Debug for CookieJar<'_> {
576    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
577        let pending: Vec<_> = self
578            .ops
579            .lock()
580            .iter()
581            .map(|c| c.cookie())
582            .cloned()
583            .collect();
584
585        f.debug_struct("CookieJar")
586            .field("original", &self.jar)
587            .field("pending", &pending)
588            .finish()
589    }
590}
591
592impl<'a> Clone for CookieJar<'a> {
593    fn clone(&self) -> Self {
594        CookieJar {
595            jar: self.jar.clone(),
596            ops: Mutex::new(self.ops.lock().clone()),
597            state: self.state,
598        }
599    }
600}
601
602impl Op {
603    fn cookie(&self) -> &Cookie<'static> {
604        match self {
605            Op::Add(c, _) | Op::Remove(c) => c,
606        }
607    }
608}