Skip to main content

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