rocket/
cookies.rs

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