rocket/http/cookies.rs
1use std::fmt;
2
3use parking_lot::Mutex;
4
5use crate::{Rocket, Orbit};
6
7#[doc(inline)]
8pub use cookie::{Cookie, SameSite, Iter};
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;
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;
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/// # #[macro_use] extern crate 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;
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;
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.private(&self.state.config.secret_key.key).get(name)
231 }
232
233 /// Returns a reference to the _original or pending_ `Cookie` inside this
234 /// container with the name `name`, irrespective of whether the cookie was
235 /// private or not. If no such cookie exists, returns `None`.
236 ///
237 /// In general, due to performance overhead, calling this method should be
238 /// avoided if it is known that a cookie called `name` is not pending.
239 /// Instead, prefer to use [`CookieJar::get()`] or
240 /// [`CookieJar::get_private()`].
241 ///
242 /// # Example
243 ///
244 /// ```rust
245 /// # #[macro_use] extern crate rocket;
246 /// use rocket::http::CookieJar;
247 ///
248 /// #[get("/")]
249 /// fn handler(jar: &CookieJar<'_>) {
250 /// let pending_cookie = jar.get_pending("name");
251 /// }
252 /// ```
253 pub fn get_pending(&self, name: &str) -> Option<Cookie<'static>> {
254 let ops = self.ops.lock();
255 if let Some(op) = ops.iter().rev().find(|op| op.cookie().name() == name) {
256 return match op {
257 Op::Add(c, _) => Some(c.clone()),
258 Op::Remove(_) => None,
259 };
260 }
261
262 drop(ops);
263
264 #[cfg(feature = "secrets")] {
265 self.get_private(name).or_else(|| self.get(name).cloned())
266 }
267
268 #[cfg(not(feature = "secrets"))] {
269 self.get(name).cloned()
270 }
271 }
272
273 /// Adds `cookie` to this collection.
274 ///
275 /// Unless a value is set for the given property, the following defaults are
276 /// set on `cookie` before being added to `self`:
277 ///
278 /// * `path`: `"/"`
279 /// * `SameSite`: `Strict`
280 /// * `Secure`: `true` if [`Request::context_is_likely_secure()`]
281 ///
282 /// These defaults ensure maximum usability and security. For additional
283 /// security, you may wish to set the `secure` flag explicitly.
284 ///
285 /// [`Request::context_is_likely_secure()`]: crate::Request::context_is_likely_secure()
286 ///
287 /// # Example
288 ///
289 /// ```rust
290 /// # #[macro_use] extern crate rocket;
291 /// use rocket::http::{Cookie, SameSite, CookieJar};
292 ///
293 /// #[get("/")]
294 /// fn handler(jar: &CookieJar<'_>) {
295 /// jar.add(("first", "value"));
296 ///
297 /// let cookie = Cookie::build(("other", "value_two"))
298 /// .path("/")
299 /// .secure(true)
300 /// .same_site(SameSite::Lax);
301 ///
302 /// jar.add(cookie);
303 /// }
304 /// ```
305 pub fn add<C: Into<Cookie<'static>>>(&self, cookie: C) {
306 let mut cookie = cookie.into();
307 self.set_defaults(&mut cookie);
308 self.ops.lock().push(Op::Add(cookie, false));
309 }
310
311 /// Adds `cookie` to the collection. The cookie's value is encrypted with
312 /// authenticated encryption assuring confidentiality, integrity, and
313 /// authenticity. The cookie can later be retrieved using
314 /// [`get_private`](#method.get_private) and removed using
315 /// [`remove_private`](#method.remove_private).
316 ///
317 /// Unless a value is set for the given property, the following defaults are
318 /// set on `cookie` before being added to `self`:
319 ///
320 /// * `path`: `"/"`
321 /// * `SameSite`: `Strict`
322 /// * `HttpOnly`: `true`
323 /// * `Expires`: 1 week from now
324 /// * `Secure`: `true` if [`Request::context_is_likely_secure()`]
325 ///
326 /// These defaults ensure maximum usability and security. For additional
327 /// security, you may wish to set the `secure` flag explicitly and
328 /// unconditionally.
329 ///
330 /// [`Request::context_is_likely_secure()`]: crate::Request::context_is_likely_secure()
331 ///
332 /// # Example
333 ///
334 /// ```rust
335 /// # #[macro_use] extern crate rocket;
336 /// use rocket::http::CookieJar;
337 ///
338 /// #[get("/")]
339 /// fn handler(jar: &CookieJar<'_>) {
340 /// jar.add_private(("name", "value"));
341 /// }
342 /// ```
343 #[cfg(feature = "secrets")]
344 #[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
345 pub fn add_private<C: Into<Cookie<'static>>>(&self, cookie: C) {
346 let mut cookie = cookie.into();
347 self.set_private_defaults(&mut cookie);
348 self.ops.lock().push(Op::Add(cookie, true));
349 }
350
351 /// Removes `cookie` from this collection and generates a "removal" cookie
352 /// to send to the client on response. A "removal" cookie is a cookie that
353 /// has the same name as the original cookie but has an empty value, a
354 /// max-age of 0, and an expiration date far in the past.
355 ///
356 /// **For successful removal, `cookie` must contain the same `path` and
357 /// `domain` as the cookie that was originally set. The cookie will fail to
358 /// be deleted if any other `path` and `domain` are provided. For
359 /// convenience, a path of `"/"` is automatically set when one is not
360 /// specified.** The full list of defaults when corresponding values aren't
361 /// specified is:
362 ///
363 /// * `path`: `"/"`
364 /// * `SameSite`: `Lax`
365 ///
366 /// <small>Note: a default setting of `Lax` for `SameSite` carries no
367 /// security implications: the removal cookie has expired, so it is never
368 /// transferred to any origin.</small>
369 ///
370 /// # Example
371 ///
372 /// ```rust
373 /// # #[macro_use] extern crate rocket;
374 /// use rocket::http::{Cookie, CookieJar};
375 ///
376 /// #[get("/")]
377 /// fn handler(jar: &CookieJar<'_>) {
378 /// // `path` and `SameSite` are set to defaults (`/` and `Lax`)
379 /// jar.remove("name");
380 ///
381 /// // Use a custom-built cookie to set a custom path.
382 /// jar.remove(Cookie::build("name").path("/login"));
383 ///
384 /// // Use a custom-built cookie to set a custom path and domain.
385 /// jar.remove(Cookie::build("id").path("/guide").domain("rocket.rs"));
386 /// }
387 /// ```
388 pub fn remove<C: Into<Cookie<'static>>>(&self, cookie: C) {
389 let mut cookie = cookie.into();
390 Self::set_removal_defaults(&mut cookie);
391 self.ops.lock().push(Op::Remove(cookie));
392 }
393
394 /// Removes the private `cookie` from the collection.
395 ///
396 /// **For successful removal, `cookie` must contain the same `path` and
397 /// `domain` as the cookie that was originally set. The cookie will fail to
398 /// be deleted if any other `path` and `domain` are provided. For
399 /// convenience, a path of `"/"` is automatically set when one is not
400 /// specified.** The full list of defaults when corresponding values aren't
401 /// specified is:
402 ///
403 /// * `path`: `"/"`
404 /// * `SameSite`: `Lax`
405 ///
406 /// <small>Note: a default setting of `Lax` for `SameSite` carries no
407 /// security implications: the removal cookie has expired, so it is never
408 /// transferred to any origin.</small>
409 ///
410 /// # Example
411 ///
412 /// ```rust
413 /// # #[macro_use] extern crate rocket;
414 /// use rocket::http::{CookieJar, Cookie};
415 ///
416 /// #[get("/")]
417 /// fn handler(jar: &CookieJar<'_>) {
418 /// // `path` and `SameSite` are set to defaults (`/` and `Lax`)
419 /// jar.remove_private("name");
420 ///
421 /// // Use a custom-built cookie to set a custom path.
422 /// jar.remove_private(Cookie::build("name").path("/login"));
423 ///
424 /// // Use a custom-built cookie to set a custom path and domain.
425 /// let cookie = Cookie::build("id").path("/guide").domain("rocket.rs");
426 /// jar.remove_private(cookie);
427 /// }
428 /// ```
429 #[cfg(feature = "secrets")]
430 #[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
431 pub fn remove_private<C: Into<Cookie<'static>>>(&self, cookie: C) {
432 let mut cookie = cookie.into();
433 Self::set_removal_defaults(&mut cookie);
434 self.ops.lock().push(Op::Remove(cookie));
435 }
436
437 /// Returns an iterator over all of the _original_ cookies present in this
438 /// collection.
439 ///
440 /// **Note:** This method _does not_ observe changes made via additions and
441 /// removals to the cookie jar.
442 ///
443 /// # Example
444 ///
445 /// ```rust
446 /// # #[macro_use] extern crate rocket;
447 /// use rocket::http::CookieJar;
448 ///
449 /// #[get("/")]
450 /// fn handler(jar: &CookieJar<'_>) {
451 /// for c in jar.iter() {
452 /// println!("Name: {:?}, Value: {:?}", c.name(), c.value());
453 /// }
454 /// }
455 /// ```
456 pub fn iter(&self) -> impl Iterator<Item=&Cookie<'static>> {
457 self.jar.iter()
458 }
459
460 /// Removes all delta cookies.
461 #[inline(always)]
462 pub(crate) fn reset_delta(&self) {
463 self.ops.lock().clear();
464 }
465
466 /// TODO: This could be faster by just returning the cookies directly via
467 /// an ordered hash-set of sorts.
468 pub(crate) fn take_delta_jar(&self) -> cookie::CookieJar {
469 let ops = std::mem::take(&mut *self.ops.lock());
470 let mut jar = cookie::CookieJar::new();
471
472 for op in ops {
473 match op {
474 Op::Add(c, false) => jar.add(c),
475 #[cfg(feature = "secrets")]
476 Op::Add(c, true) => {
477 jar.private_mut(&self.state.config.secret_key.key).add(c);
478 }
479 Op::Remove(mut c) => {
480 if self.jar.get(c.name()).is_some() {
481 c.make_removal();
482 jar.add(c);
483 } else {
484 jar.remove(c);
485 }
486 }
487 #[allow(unreachable_patterns)]
488 _ => unreachable!()
489 }
490 }
491
492 jar
493 }
494
495 /// Adds an original `cookie` to this collection.
496 #[inline(always)]
497 pub(crate) fn add_original(&mut self, cookie: Cookie<'static>) {
498 self.jar.add_original(cookie)
499 }
500
501 /// Adds an original, private `cookie` to the collection.
502 #[cfg(feature = "secrets")]
503 #[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
504 #[inline(always)]
505 pub(crate) fn add_original_private(&mut self, cookie: Cookie<'static>) {
506 self.jar.private_mut(&self.state.config.secret_key.key).add_original(cookie);
507 }
508
509 /// For each property mentioned below, this method checks if there is a
510 /// provided value and if there is none, sets a default value. Default
511 /// values are:
512 ///
513 /// * `path`: `"/"`
514 /// * `SameSite`: `Strict`
515 /// * `Secure`: `true` if `Request::context_is_likely_secure()`
516 fn set_defaults(&self, cookie: &mut Cookie<'static>) {
517 if cookie.path().is_none() {
518 cookie.set_path("/");
519 }
520
521 if cookie.same_site().is_none() {
522 cookie.set_same_site(SameSite::Strict);
523 }
524
525 if cookie.secure().is_none() && self.state.secure {
526 cookie.set_secure(true);
527 }
528 }
529
530 /// For each property below, this method checks if there is a provided value
531 /// and if there is none, sets a default value. Default values are:
532 ///
533 /// * `path`: `"/"`
534 /// * `SameSite`: `Lax`
535 fn set_removal_defaults(cookie: &mut Cookie<'static>) {
536 if cookie.path().is_none() {
537 cookie.set_path("/");
538 }
539
540 if cookie.same_site().is_none() {
541 cookie.set_same_site(SameSite::Lax);
542 }
543 }
544
545 /// For each property mentioned below, this method checks if there is a
546 /// provided value and if there is none, sets a default value. Default
547 /// values are:
548 ///
549 /// * `path`: `"/"`
550 /// * `SameSite`: `Strict`
551 /// * `HttpOnly`: `true`
552 /// * `Expires`: 1 week from now
553 /// * `Secure`: `true` if `Request::context_is_likely_secure()`
554 #[cfg(feature = "secrets")]
555 #[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
556 fn set_private_defaults(&self, cookie: &mut Cookie<'static>) {
557 self.set_defaults(cookie);
558
559 if cookie.http_only().is_none() {
560 cookie.set_http_only(true);
561 }
562
563 if cookie.expires().is_none() {
564 cookie.set_expires(time::OffsetDateTime::now_utc() + time::Duration::weeks(1));
565 }
566 }
567}
568
569impl fmt::Debug for CookieJar<'_> {
570 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
571 let pending: Vec<_> = self.ops.lock()
572 .iter()
573 .map(|c| c.cookie())
574 .cloned()
575 .collect();
576
577 f.debug_struct("CookieJar")
578 .field("original", &self.jar)
579 .field("pending", &pending)
580 .finish()
581 }
582}
583
584impl<'a> Clone for CookieJar<'a> {
585 fn clone(&self) -> Self {
586 CookieJar {
587 jar: self.jar.clone(),
588 ops: Mutex::new(self.ops.lock().clone()),
589 state: self.state,
590 }
591 }
592}
593
594impl Op {
595 fn cookie(&self) -> &Cookie<'static> {
596 match self {
597 Op::Add(c, _) | Op::Remove(c) => c
598 }
599 }
600}