Skip to main content

cookie_monster/
jar.rs

1use std::{borrow::Borrow, collections::HashSet, fmt::Debug, hash::Hash};
2
3use crate::Cookie;
4
5/// A generic `CookieJar` for cookie management. Can be used to read update or delete cookies from
6/// a user session.
7///
8/// ## `axum` feature
9///
10/// Note that to set the cookies, the jar _must_ be returned from the handler. Otherwise the
11/// cookies are not updated.
12///
13///
14/// ## Example
15/// ```rust
16/// use cookie_monster::{CookieJar, Cookie};
17///
18/// static COOKIE_NAME: &str = "session";
19///
20/// async fn handler(mut jar: CookieJar) -> CookieJar {
21///
22///     if let Some(cookie) = jar.get(COOKIE_NAME) {
23///         println!("Removing cookie {cookie:?}");
24///         jar.remove(Cookie::named(COOKIE_NAME));
25///     } else {
26///         let cookie = Cookie::new(COOKIE_NAME, "hello, world");
27///         println!("Setting cookie {cookie:?}");
28///         jar.add(cookie);
29///     }
30///
31///     // Important, return the jar to update the cookies!
32///     jar
33/// }
34/// ```
35#[derive(Default, Debug)]
36pub struct CookieJar {
37    cookies: HashSet<HashCookie>,
38}
39
40pub(crate) enum HashCookie {
41    // An original cookie. These should never be sent back to the user-agent.
42    Original(Cookie),
43    // A new cookie, the should always be sent back to the user-agent.
44    New(Cookie),
45    // A removed cookie, the should always be sent back to the user-agent but should never be
46    // visible by the user.
47    Removal(Cookie),
48}
49
50impl HashCookie {
51    fn name(&self) -> &str {
52        match self {
53            HashCookie::Original(c) => c.name(),
54            HashCookie::New(c) => c.name(),
55            HashCookie::Removal(c) => c.name(),
56        }
57    }
58}
59
60impl Hash for HashCookie {
61    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
62        self.name().hash(state);
63    }
64}
65
66impl PartialEq for HashCookie {
67    fn eq(&self, other: &Self) -> bool {
68        self.name() == other.name()
69    }
70}
71
72impl Borrow<str> for HashCookie {
73    fn borrow(&self) -> &str {
74        self.name()
75    }
76}
77
78impl Eq for HashCookie {}
79
80impl Debug for HashCookie {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        match self {
83            HashCookie::Original(cookie) => cookie.fmt(f),
84            HashCookie::New(cookie) => cookie.fmt(f),
85            HashCookie::Removal(cookie) => cookie.fmt(f),
86        }
87    }
88}
89
90impl CookieJar {
91    /// Creates an empty `CookieJar`.
92    pub fn new() -> Self {
93        Self::default()
94    }
95
96    /// Parses the given `cookie` header value and return a `CookieJar`. This function ignores
97    /// cookies that were not able to be parsed.
98    ///
99    /// # Duplicate names
100    /// If the header contains the same cookie name more than once, the **last**
101    /// occurrence wins. This matches the `cookie` crate, `axum-extra`, Python's
102    /// `SimpleCookie` and ASP.NET Core.
103    ///
104    /// Duplicate-name resolution is **not** a security boundary. Note that the
105    /// `__Host-` / `__Secure-` prefix is stripped from the name when parsing, so a
106    /// prefixed cookie and a plain cookie of the same logical name collide here and the
107    /// last one wins. If you rely on a prefix as a trust signal, reject requests that
108    /// carry duplicate cookie names.
109    ///
110    /// ```rust
111    /// use cookie_monster::CookieJar;
112    ///
113    /// let jar = CookieJar::from_cookie("name=first; name=second");
114    /// assert_eq!(jar.get("name").map(|c| c.value()), Some("second"));
115    /// ```
116    pub fn from_cookie(header: &str) -> Self {
117        Self::from_original(header.split(';').flat_map(Cookie::parse_cookie))
118    }
119
120    /// Parses the given `cookie` header value and return a `CookieJar`. The cookie name and values
121    /// are percent-decoded. Cookies that were not able to be parsed are ignored.
122    ///
123    /// Like [`from_cookie`](Self::from_cookie), duplicate cookie names resolve to
124    /// the **last** occurrence; see its docs for the cookie-shadowing note.
125    #[cfg(feature = "percent-encode")]
126    pub fn from_encoded_cookie(header: &str) -> Self {
127        Self::from_original(header.split(';').flat_map(Cookie::parse_cookie_encoded))
128    }
129
130    /// Adds an __original__ cookie to the jar. These are never sent back to the
131    /// user-agent, but are visible in the cookie jar.
132    ///
133    /// If a cookie with the same name is already present it is replaced
134    /// (last-wins), matching [`add`](Self::add).
135    pub fn add_original(&mut self, cookie: Cookie) {
136        self.cookies.replace(HashCookie::Original(cookie));
137    }
138
139    // Creates a `CookieJar` from an iterator of cookies. It is assumed that the cookies are
140    // __original__. E.g. from a `Cookie` header value.
141    pub fn from_original<T: IntoIterator<Item = Cookie>>(cookies: T) -> Self {
142        let mut jar = Self::new();
143
144        for cookie in cookies {
145            jar.add_original(cookie);
146        }
147
148        jar
149    }
150
151    /// Get a cookie by name. Gives back either an __original__ or newly added cookie.
152    ///
153    /// Parsing strips the `__Host-` / `__Secure-` prefix from the cookie name, so a cookie
154    /// received (or built) with a prefix is looked up by its logical (unprefixed) name.
155    pub fn get(&self, name: &str) -> Option<&Cookie> {
156        self.cookies.get(name).and_then(|c| match c {
157            HashCookie::New(c) | HashCookie::Original(c) => Some(c),
158            HashCookie::Removal(_) => None,
159        })
160    }
161
162    /// Iterate over all changes. This returns all removed and newly created cookies.
163    pub fn set_cookie_headers(&self) -> impl Iterator<Item = crate::Result<String>> {
164        self.cookies
165            .iter()
166            .filter_map(|c| match c {
167                HashCookie::Original(_) => None,
168                HashCookie::New(c) | HashCookie::Removal(c) => Some(c),
169            })
170            .map(Cookie::serialize)
171    }
172
173    /// Removes the cookie from the local cookie store and issues a cookie with an Expires
174    /// attribute in the past and Max-Age of 0 seconds.
175    ///
176    /// If one of the `time`, `chrono` or `jiff` features are enabled, the Expires tag is set to the
177    /// current time minus one year. If none of the those features are enabled, the Expires
178    /// attribute is set to 1 Jan 1970 00:00.
179    ///
180    /// **To ensure a cookie is removed from the user-agent, set the `Path` and `Domain` attributes
181    /// with the same values that were used to create the cookie.**
182    pub fn remove(&mut self, cookie: impl Into<Cookie>) -> Option<Cookie> {
183        let cookie = HashCookie::Removal(cookie.into().into_remove());
184        self.cookies.replace(cookie).and_then(|c| match c {
185            HashCookie::Original(cookie) => Some(cookie),
186            HashCookie::New(cookie) => Some(cookie),
187            HashCookie::Removal(_) => None,
188        })
189    }
190
191    /// Adds a cookie to the jar. If a cookie with the same name is already in the jar, it is
192    /// replaced with the given cookie.
193    pub fn add(&mut self, cookie: impl Into<Cookie>) {
194        self.cookies.replace(HashCookie::New(cookie.into()));
195    }
196
197    #[allow(unused)]
198    pub(crate) fn iter_non_original(&self) -> impl Iterator<Item = &Cookie> {
199        self.cookies.iter().flat_map(|cookie| match cookie {
200            HashCookie::Original(_) => None,
201            HashCookie::New(cookie) | HashCookie::Removal(cookie) => Some(cookie),
202        })
203    }
204}