net/
cookie_storage.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Implementation of cookie storage as specified in
6//! <http://tools.ietf.org/html/rfc6265>
7
8use std::cmp::Ordering;
9use std::collections::HashMap;
10use std::collections::hash_map::Entry;
11use std::time::SystemTime;
12
13use cookie::Cookie;
14use itertools::Itertools;
15use log::info;
16use net_traits::pub_domains::reg_suffix;
17use net_traits::{CookieSource, SiteDescriptor};
18use serde::{Deserialize, Serialize};
19use servo_url::ServoUrl;
20
21use crate::cookie::ServoCookie;
22
23#[derive(Clone, Debug, Deserialize, Serialize)]
24pub struct CookieStorage {
25    version: u32,
26    cookies_map: HashMap<String, Vec<ServoCookie>>,
27    max_per_host: usize,
28}
29
30#[derive(Debug)]
31pub enum RemoveCookieError {
32    Overlapping,
33    NonHTTP,
34}
35
36impl CookieStorage {
37    pub fn new(max_cookies: usize) -> CookieStorage {
38        CookieStorage {
39            version: 1,
40            cookies_map: HashMap::new(),
41            max_per_host: max_cookies,
42        }
43    }
44
45    // http://tools.ietf.org/html/rfc6265#section-5.3
46    pub fn remove(
47        &mut self,
48        cookie: &ServoCookie,
49        url: &ServoUrl,
50        source: CookieSource,
51    ) -> Result<Option<ServoCookie>, RemoveCookieError> {
52        let domain = reg_host(cookie.cookie.domain().as_ref().unwrap_or(&""));
53        let cookies = self.cookies_map.entry(domain).or_default();
54
55        // https://www.ietf.org/id/draft-ietf-httpbis-cookie-alone-01.txt Step 2
56        if !cookie.cookie.secure().unwrap_or(false) && !url.is_secure_scheme() {
57            let new_domain = cookie.cookie.domain().as_ref().unwrap().to_owned();
58            let new_path = cookie.cookie.path().as_ref().unwrap().to_owned();
59
60            let any_overlapping = cookies.iter().any(|c| {
61                let existing_domain = c.cookie.domain().as_ref().unwrap().to_owned();
62                let existing_path = c.cookie.path().as_ref().unwrap().to_owned();
63
64                c.cookie.name() == cookie.cookie.name() &&
65                    c.cookie.secure().unwrap_or(false) &&
66                    (ServoCookie::domain_match(new_domain, existing_domain) ||
67                        ServoCookie::domain_match(existing_domain, new_domain)) &&
68                    ServoCookie::path_match(new_path, existing_path)
69            });
70
71            if any_overlapping {
72                return Err(RemoveCookieError::Overlapping);
73            }
74        }
75
76        // Step 11.1
77        let position = cookies.iter().position(|c| {
78            c.cookie.domain() == cookie.cookie.domain() &&
79                c.cookie.path() == cookie.cookie.path() &&
80                c.cookie.name() == cookie.cookie.name()
81        });
82
83        if let Some(ind) = position {
84            // Step 11.4
85            let c = cookies.remove(ind);
86
87            // http://tools.ietf.org/html/rfc6265#section-5.3 step 11.2
88            if c.cookie.http_only().unwrap_or(false) && source == CookieSource::NonHTTP {
89                // Undo the removal.
90                cookies.push(c);
91                Err(RemoveCookieError::NonHTTP)
92            } else {
93                Ok(Some(c))
94            }
95        } else {
96            Ok(None)
97        }
98    }
99
100    pub fn delete_cookies_for_sites(&mut self, sites: &Vec<String>) {
101        // Note: We assume the number of sites is smaller than the number of
102        // entries in the cookies map. If this assumption stops holding in
103        // practice, this implementation can be revised to use `retain`
104        // together with a temporary `HashSet` of sites.
105        for site in sites {
106            // TODO: We currently mark cookies as expired instead of removing
107            // them immediately (same behavior as in the functions below).
108            // This is safe because higher-level cookie accessors always call
109            // `remove_expired_cookies_for_url` / `remove_all_expired_cookies`.
110            // Consider whether we should instead delete the entries directly.
111            if let Some(cookies) = self.cookies_map.get_mut(site) {
112                for cookie in cookies.iter_mut() {
113                    cookie.set_expiry_time_in_past();
114                }
115            }
116        }
117    }
118
119    pub fn clear_storage(&mut self, url: Option<&ServoUrl>) {
120        if let Some(url) = url {
121            let domain = reg_host(url.host_str().unwrap_or(""));
122            // TODO: This creates an empty cookie list if none existed? Should
123            // we just use `get_mut` here?
124            let cookies = self.cookies_map.entry(domain).or_default();
125            for cookie in cookies.iter_mut() {
126                cookie.set_expiry_time_in_past();
127            }
128        } else {
129            self.cookies_map.clear();
130        }
131    }
132
133    pub fn delete_cookie_with_name(&mut self, url: &ServoUrl, name: String) {
134        let domain = reg_host(url.host_str().unwrap_or(""));
135        // TODO: This creates an empty cookie list if none existed? Should we
136        // just use `get_mut` here?
137        let cookies = self.cookies_map.entry(domain).or_default();
138        for cookie in cookies.iter_mut() {
139            if cookie.cookie.name() == name {
140                cookie.set_expiry_time_in_past();
141            }
142        }
143    }
144
145    // http://tools.ietf.org/html/rfc6265#section-5.3
146    pub fn push(&mut self, mut cookie: ServoCookie, url: &ServoUrl, source: CookieSource) {
147        // https://www.ietf.org/id/draft-ietf-httpbis-cookie-alone-01.txt Step 1
148        if cookie.cookie.secure().unwrap_or(false) && !url.is_secure_scheme() {
149            return;
150        }
151
152        let old_cookie = self.remove(&cookie, url, source);
153        if old_cookie.is_err() {
154            // This new cookie is not allowed to overwrite an existing one.
155            return;
156        }
157
158        // Step 11
159        if let Some(old_cookie) = old_cookie.unwrap() {
160            // Step 11.3
161            cookie.creation_time = old_cookie.creation_time;
162        }
163
164        // Step 12
165        let domain = reg_host(cookie.cookie.domain().as_ref().unwrap_or(&""));
166        let cookies = self.cookies_map.entry(domain).or_default();
167
168        if cookies.len() == self.max_per_host {
169            let old_len = cookies.len();
170            cookies.retain(|c| !is_cookie_expired(c));
171            let new_len = cookies.len();
172
173            // https://www.ietf.org/id/draft-ietf-httpbis-cookie-alone-01.txt
174            if new_len == old_len &&
175                !evict_one_cookie(cookie.cookie.secure().unwrap_or(false), cookies)
176            {
177                return;
178            }
179        }
180        cookies.push(cookie);
181    }
182
183    pub fn cookie_comparator(a: &ServoCookie, b: &ServoCookie) -> Ordering {
184        let a_path_len = a.cookie.path().as_ref().map_or(0, |p| p.len());
185        let b_path_len = b.cookie.path().as_ref().map_or(0, |p| p.len());
186        match a_path_len.cmp(&b_path_len) {
187            Ordering::Equal => a.creation_time.cmp(&b.creation_time),
188            // Ensure that longer paths are sorted earlier than shorter paths
189            Ordering::Greater => Ordering::Less,
190            Ordering::Less => Ordering::Greater,
191        }
192    }
193
194    pub fn remove_expired_cookies_for_url(&mut self, url: &ServoUrl) {
195        let domain = reg_host(url.host_str().unwrap_or(""));
196        if let Entry::Occupied(mut entry) = self.cookies_map.entry(domain) {
197            let cookies = entry.get_mut();
198            cookies.retain(|c| !is_cookie_expired(c));
199            if cookies.is_empty() {
200                entry.remove_entry();
201            }
202        }
203    }
204
205    pub fn remove_all_expired_cookies(&mut self) {
206        self.cookies_map.retain(|_, cookies| {
207            cookies.retain(|c| !is_cookie_expired(c));
208            !cookies.is_empty()
209        });
210    }
211
212    // http://tools.ietf.org/html/rfc6265#section-5.4
213    pub fn cookies_for_url(&mut self, url: &ServoUrl, source: CookieSource) -> Option<String> {
214        // Let cookie-list be the set of cookies from the cookie store
215        let cookie_list = self.cookies_data_for_url(url, source);
216
217        let reducer = |acc: String, cookie: Cookie<'static>| -> String {
218            // Serialize the cookie-list into a cookie-string by processing each cookie in the cookie-list in order:
219            // If the cookies' name is not empty, output the cookie's name followed by the %x3D ("=") character.
220            // If the cookies' value is not empty, output the cookie's value.
221            // If there is an unprocessed cookie in the cookie-list, output the characters %x3B and %x20 ("; ").
222            // Security: the above steps allow for "nameless" cookies which have proved to be a security footgun
223            // especially with the new cookie name prefix proposals
224            (match acc.len() {
225                0 => acc,
226                _ => acc + "; ",
227            }) + cookie.name() +
228                "=" +
229                cookie.value()
230        };
231
232        // Serialize the cookie-list into a cookie-string by processing each cookie in the cookie-list in order
233        let result = cookie_list.fold("".to_owned(), reducer);
234
235        info!(" === COOKIES SENT: {}", result);
236        match result.len() {
237            0 => None,
238            _ => Some(result),
239        }
240    }
241
242    /// <https://cookiestore.spec.whatwg.org/#query-cookies>
243    pub fn query_cookies(&mut self, url: &ServoUrl, name: Option<String>) -> Vec<Cookie<'static>> {
244        // 1. Retrieve cookie-list given request-uri and "non-HTTP" source
245        let cookie_list = self.cookies_data_for_url(url, CookieSource::NonHTTP);
246
247        // 3. For each cookie in cookie-list, run these steps:
248        // 3.2. If name is given, then run these steps:
249        if let Some(name) = name {
250            // Let cookieName be the result of running UTF-8 decode without BOM on cookie’s name.
251            // If cookieName does not equal name, then continue.
252            cookie_list.filter(|cookie| cookie.name() == name).collect()
253        } else {
254            cookie_list.collect()
255        }
256
257        // Note: we do not convert the list into CookieListItem's here, we do that in script to not not have to define
258        // the binding types in net.
259
260        // Return list
261    }
262
263    pub fn cookies_data_for_url<'a>(
264        &'a mut self,
265        url: &'a ServoUrl,
266        source: CookieSource,
267    ) -> impl Iterator<Item = cookie::Cookie<'static>> + 'a {
268        let domain = reg_host(url.host_str().unwrap_or(""));
269        let cookies = self.cookies_map.entry(domain).or_default();
270
271        cookies
272            .iter_mut()
273            .filter(move |c| c.appropriate_for_url(url, source))
274            .sorted_by(|a: &&mut ServoCookie, b: &&mut ServoCookie| {
275                // The user agent SHOULD sort the cookie-list
276                CookieStorage::cookie_comparator(a, b)
277            })
278            .map(|c| {
279                // Update the last-access-time of each cookie in the cookie-list to the current date and time
280                c.touch();
281                c.cookie.clone()
282            })
283    }
284
285    pub fn cookie_site_descriptors(&self) -> Vec<SiteDescriptor> {
286        self.cookies_map
287            .keys()
288            .cloned()
289            .map(SiteDescriptor::new)
290            .collect()
291    }
292}
293
294fn reg_host(url: &str) -> String {
295    reg_suffix(url).to_lowercase()
296}
297
298fn is_cookie_expired(cookie: &ServoCookie) -> bool {
299    matches!(cookie.expiry_time, Some(date_time) if date_time <= SystemTime::now())
300}
301
302fn evict_one_cookie(is_secure_cookie: bool, cookies: &mut Vec<ServoCookie>) -> bool {
303    // Remove non-secure cookie with oldest access time
304    let oldest_accessed = get_oldest_accessed(false, cookies);
305
306    if let Some((index, _)) = oldest_accessed {
307        cookies.remove(index);
308    } else {
309        // All secure cookies were found
310        if !is_secure_cookie {
311            return false;
312        }
313        let oldest_accessed = get_oldest_accessed(true, cookies);
314        if let Some((index, _)) = oldest_accessed {
315            cookies.remove(index);
316        }
317    }
318    true
319}
320
321fn get_oldest_accessed(
322    is_secure_cookie: bool,
323    cookies: &mut [ServoCookie],
324) -> Option<(usize, SystemTime)> {
325    let mut oldest_accessed = None;
326    for (i, c) in cookies.iter().enumerate() {
327        if (c.cookie.secure().unwrap_or(false) == is_secure_cookie) &&
328            oldest_accessed
329                .as_ref()
330                .is_none_or(|(_, current_oldest_time)| c.last_access < *current_oldest_time)
331        {
332            oldest_accessed = Some((i, c.last_access));
333        }
334    }
335    oldest_accessed
336}