Skip to main content

leyline/cookie/
jar.rs

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;