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