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            .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;