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