1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3use std::time::SystemTime;
4
5use tokio::sync::watch;
6use url::Url;
7
8use crate::cookie::parse;
9use crate::cookie::record::Cookie;
10use crate::util::lock;
11
12use self::evict::settle;
13
14mod autosave;
15mod evict;
16mod persist;
17
18pub use autosave::JarAutosave;
19
20#[derive(Clone)]
21pub struct Jar {
22 inner: Arc<Mutex<JarInner>>,
23 changes: Arc<watch::Sender<u64>>,
24}
25
26struct JarInner {
27 cookies: HashMap<String, Vec<Cookie>>,
28 total: usize,
29}
30
31impl Jar {
32 pub fn new() -> Self {
33 Self::from_buckets(HashMap::new(), 0)
34 }
35
36 fn from_buckets(cookies: HashMap<String, Vec<Cookie>>, total: usize) -> Self {
37 Self {
38 inner: Arc::new(Mutex::new(JarInner { cookies, total })),
39 changes: Arc::new(watch::Sender::new(0)),
40 }
41 }
42
43 pub(crate) fn changes(&self) -> watch::Receiver<u64> {
44 self.changes.subscribe()
45 }
46
47 fn changed(&self) {
48 self.changes.send_modify(|generation| *generation += 1);
49 }
50
51 pub fn snapshot(&self) -> Self {
52 let jar = lock(&self.inner);
53 Self::from_buckets(jar.cookies.clone(), jar.total)
54 }
55
56 pub fn extend_from(&self, other: &Jar) {
57 if Arc::ptr_eq(&self.inner, &other.inner) {
58 return;
59 }
60 let incoming: Vec<Cookie> = lock(&other.inner)
61 .cookies
62 .values()
63 .flatten()
64 .filter(|c| c.within_limits())
65 .cloned()
66 .collect();
67 if incoming.is_empty() {
68 return;
69 }
70 let mut jar = lock(&self.inner);
71 for cookie in incoming {
72 let domain = cookie.domain.to_lowercase();
73 let entries = jar.cookies.entry(domain.clone()).or_default();
74 let added = match entries.iter().position(|c| c.same_slot(&cookie)) {
75 Some(pos) => {
76 entries[pos] = cookie;
77 false
78 }
79 None => {
80 entries.push(cookie);
81 true
82 }
83 };
84 settle(&mut jar, &domain, added);
85 }
86 drop(jar);
87 self.changed();
88 }
89
90 pub fn store_set_cookie(&self, set_cookie: &str, url: &Url) {
91 self.store(set_cookie, url);
92 }
93
94 fn store(&self, set_cookie: &str, url: &Url) {
95 if self.apply(set_cookie, url) {
96 self.changed();
97 }
98 }
99
100 fn apply(&self, set_cookie: &str, url: &Url) -> bool {
101 let Some(mut cookie) =
102 parse::parse_set_cookie(set_cookie, url).filter(Cookie::within_limits)
103 else {
104 return false;
105 };
106 if !cookie.secure
107 && url.scheme() != "https"
108 && lock(&self.inner)
109 .cookies
110 .values()
111 .flatten()
112 .any(|existing| cookie.shadows_secure(existing))
113 {
114 return false;
115 }
116
117 if cookie.is_expired() {
118 let mut jar = lock(&self.inner);
119 let domain = cookie.domain.to_lowercase();
120 if let Some(entries) = jar.cookies.get_mut(&domain)
121 && let Some(pos) = entries.iter().position(|c| c.same_slot(&cookie))
122 {
123 if entries[pos].secure && url.scheme() != "https" {
124 return false;
125 }
126 return jar.remove_in(&domain, |c| c.same_slot(&cookie)) > 0;
127 }
128 return false;
129 }
130
131 let mut jar = lock(&self.inner);
132 let domain = cookie.domain.to_lowercase();
133
134 let entries = jar.cookies.entry(domain.clone()).or_default();
135
136 let mut added = false;
137 if let Some(pos) = entries.iter().position(|c| c.same_slot(&cookie)) {
138 if entries[pos].secure && !cookie.secure {
139 return false;
140 }
141 cookie.creation_time = entries[pos].creation_time;
142 entries[pos] = cookie;
143 } else {
144 entries.push(cookie);
145 added = true;
146 }
147 settle(&mut jar, &domain, added);
148 true
149 }
150
151 pub(crate) fn store_response_cookies(&self, headers: &[&str], url: &Url) {
152 for header in headers {
153 self.store(header, url);
154 }
155 }
156
157 pub fn cookie_header(&self, url: &Url) -> Option<String> {
158 self.cookie_header_for(url, false, true)
159 }
160
161 pub(crate) fn cookie_header_for(
162 &self,
163 url: &Url,
164 cross_site: bool,
165 lax_allowed: bool,
166 ) -> Option<String> {
167 use crate::cookie::record::SameSite;
168 let domain = url.host_str().unwrap_or("");
169 let path = url.path();
170 let is_secure = url.scheme() == "https";
171
172 let mut jar = lock(&self.inner);
173 let now = SystemTime::now();
174
175 let mut matching: Vec<&mut Cookie> = Vec::with_capacity(8);
176 for entries in jar.cookies.values_mut() {
177 for cookie in entries.iter_mut() {
178 if cookie.is_expired() {
179 continue;
180 }
181 if !cookie.matches(domain, path, is_secure) {
182 continue;
183 }
184 if cross_site {
185 match cookie.same_site {
186 SameSite::Strict => continue,
187 SameSite::Lax if !lax_allowed => continue,
188 _ => {}
189 }
190 }
191 cookie.last_access = now;
192 matching.push(cookie);
193 }
194 }
195
196 if matching.is_empty() {
197 return None;
198 }
199
200 matching.sort_by(|a, b| {
201 b.path
202 .len()
203 .cmp(&a.path.len())
204 .then(a.creation_time.cmp(&b.creation_time))
205 });
206
207 let header = matching
208 .iter()
209 .map(|c| format!("{}={}", c.name, c.value))
210 .collect::<Vec<_>>()
211 .join("; ");
212
213 Some(header)
214 }
215
216 pub fn remove_named(&self, name: &str) -> usize {
217 let removed = lock(&self.inner).remove_where(|c| c.name == name);
218 self.changed_if(removed)
219 }
220
221 pub fn remove(&self, url: &Url, name: &str) -> usize {
222 let host = url.host_str().unwrap_or("");
223 let removed =
224 lock(&self.inner).remove_where(|c| c.name == name && c.matches(host, &c.path, true));
225 self.changed_if(removed)
226 }
227
228 fn changed_if(&self, removed: usize) -> usize {
229 if removed > 0 {
230 self.changed();
231 }
232 removed
233 }
234
235 pub fn all_cookies(&self) -> Vec<Cookie> {
236 let jar = lock(&self.inner);
237 let mut out: Vec<Cookie> = jar.cookies.values().flatten().cloned().collect();
238 out.sort_by(|a, b| a.domain.cmp(&b.domain).then_with(|| a.name.cmp(&b.name)));
239 out
240 }
241
242 pub fn get_cookie(&self, url: &Url, name: &str) -> Option<String> {
243 let domain = url.host_str().unwrap_or("");
244 let path = url.path();
245 let is_secure = url.scheme() == "https";
246
247 let jar = lock(&self.inner);
248 jar.cookies
249 .values()
250 .flatten()
251 .find(|c| c.name == name && c.matches(domain, path, is_secure) && !c.is_expired())
252 .map(|c| c.value.clone())
253 }
254
255 pub fn set_cookie(&self, url: &Url, name: &str, value: &str) {
256 if plain_token(name) && !name.contains('=') && plain_token(value) {
257 self.store(&format!("{name}={value}; Path=/"), url);
258 }
259 }
260
261 pub fn load_cookies(&self, cookie_str: &str, url: &Url) {
262 for pair in cookie_str.split(';') {
263 if let Some((name, value)) = pair.trim().split_once('=') {
264 self.set_cookie(url, name, value);
265 }
266 }
267 }
268
269 pub fn clear(&self) {
270 let removed = {
271 let mut jar = lock(&self.inner);
272 jar.cookies.clear();
273 std::mem::take(&mut jar.total)
274 };
275 self.changed_if(removed);
276 }
277}
278
279impl Default for Jar {
280 fn default() -> Self {
281 Self::new()
282 }
283}
284
285impl std::fmt::Debug for Jar {
286 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287 let jar = lock(&self.inner);
288 f.debug_struct("Jar")
289 .field("domains", &jar.cookies.len())
290 .field("total", &jar.total)
291 .finish()
292 }
293}
294
295fn plain_token(text: &str) -> bool {
296 !text.chars().any(|c| c == ';' || c.is_control())
297}
298
299#[cfg(test)]
300mod tests;