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}