script/dom/
cookiestore.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
5use std::borrow::Cow;
6use std::collections::VecDeque;
7use std::rc::Rc;
8
9use base::id::CookieStoreId;
10use cookie::Expiration::DateTime;
11use cookie::{Cookie, SameSite};
12use dom_struct::dom_struct;
13use hyper_serde::Serde;
14use ipc_channel::ipc;
15use ipc_channel::router::ROUTER;
16use itertools::Itertools;
17use js::jsval::NullValue;
18use net_traits::CookieSource::NonHTTP;
19use net_traits::{CookieAsyncResponse, CookieData, CoreResourceMsg, IpcSend};
20use script_bindings::script_runtime::CanGc;
21use servo_url::ServoUrl;
22
23use crate::dom::bindings::cell::DomRefCell;
24use crate::dom::bindings::codegen::Bindings::CookieStoreBinding::{
25    CookieInit, CookieListItem, CookieSameSite, CookieStoreDeleteOptions, CookieStoreGetOptions,
26    CookieStoreMethods,
27};
28use crate::dom::bindings::error::Error;
29use crate::dom::bindings::num::Finite;
30use crate::dom::bindings::refcounted::Trusted;
31use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object};
32use crate::dom::bindings::root::DomRoot;
33use crate::dom::bindings::str::USVString;
34use crate::dom::eventtarget::EventTarget;
35use crate::dom::globalscope::GlobalScope;
36use crate::dom::promise::Promise;
37use crate::dom::window::Window;
38use crate::task_source::SendableTaskSource;
39
40/// <https://cookiestore.spec.whatwg.org/>
41/// CookieStore provides an async API for pages and service workers to access and modify cookies.
42/// This requires setting up communication with resource thread's cookie storage that allows for
43/// the page to have multiple cookie storage promises in flight at the same time.
44#[dom_struct]
45pub(crate) struct CookieStore {
46    eventtarget: EventTarget,
47    #[ignore_malloc_size_of = "Rc"]
48    in_flight: DomRefCell<VecDeque<Rc<Promise>>>,
49    // Store an id so that we can send it with requests and the resource thread knows who to respond to
50    #[no_trace]
51    store_id: CookieStoreId,
52}
53
54struct CookieListener {
55    // TODO:(whatwg/cookiestore#239) The spec is missing details for what task source to use
56    task_source: SendableTaskSource,
57    context: Trusted<CookieStore>,
58}
59
60impl CookieListener {
61    pub(crate) fn handle(&self, message: CookieAsyncResponse) {
62        let context = self.context.clone();
63        self.task_source.queue(task!(cookie_message: move || {
64            let Some(promise) = context.root().in_flight.borrow_mut().pop_front() else {
65                warn!("No promise exists for cookie store response");
66                return;
67            };
68            match message.data {
69                CookieData::Get(cookie) => {
70                    // If list is failure, then reject p with a TypeError and abort these steps.
71                    // (There is currently no way for list to result in failure)
72                    if let Some(cookie) = cookie {
73                        // Otherwise, resolve p with the first item of list.
74                        promise.resolve_native(&cookie_to_list_item(cookie.into_inner()), CanGc::note());
75                    } else {
76                        // If list is empty, then resolve p with null.
77                        promise.resolve_native(&NullValue(), CanGc::note());
78                    }
79                },
80                CookieData::GetAll(cookies) => {
81                    // If list is failure, then reject p with a TypeError and abort these steps.
82                    promise.resolve_native(
83                        &cookies
84                        .into_iter()
85                        .map(|cookie| cookie_to_list_item(cookie.0))
86                        .collect_vec(),
87                    CanGc::note());
88                },
89                CookieData::Delete(_) | CookieData::Change(_) | CookieData::Set(_) => {
90                    promise.resolve_native(&(), CanGc::note());
91                }
92            }
93        }));
94    }
95}
96
97impl CookieStore {
98    fn new_inherited() -> CookieStore {
99        CookieStore {
100            eventtarget: EventTarget::new_inherited(),
101            in_flight: Default::default(),
102            store_id: CookieStoreId::new(),
103        }
104    }
105
106    pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot<CookieStore> {
107        let store = reflect_dom_object(Box::new(CookieStore::new_inherited()), global, can_gc);
108        store.setup_route();
109        store
110    }
111
112    fn setup_route(&self) {
113        let (cookie_sender, cookie_receiver) = ipc::channel().expect("ipc channel failure");
114
115        let context = Trusted::new(self);
116        let cs_listener = CookieListener {
117            task_source: self
118                .global()
119                .task_manager()
120                .dom_manipulation_task_source()
121                .to_sendable(),
122            context,
123        };
124
125        ROUTER.add_typed_route(
126            cookie_receiver,
127            Box::new(move |message| match message {
128                Ok(msg) => cs_listener.handle(msg),
129                Err(err) => warn!("Error receiving a CookieStore message: {:?}", err),
130            }),
131        );
132
133        let res = self
134            .global()
135            .resource_threads()
136            .send(CoreResourceMsg::NewCookieListener(
137                self.store_id,
138                cookie_sender,
139                self.global().creation_url().clone(),
140            ));
141        if res.is_err() {
142            error!("Failed to send cookiestore message to resource threads");
143        }
144    }
145}
146
147/// <https://cookiestore.spec.whatwg.org/#create-a-cookielistitem>
148fn cookie_to_list_item(cookie: Cookie) -> CookieListItem {
149    // TODO: Investigate if we need to explicitly UTF-8 decode without BOM here or if thats
150    // already being done by cookie-rs or implicitly by using rust strings
151    CookieListItem {
152        // Let domain be the result of running UTF-8 decode without BOM on cookie’s domain.
153        domain: cookie
154            .domain()
155            .map(|domain| Some(domain.to_string().into())),
156
157        // Let expires be cookie’s expiry-time (as a timestamp).
158        expires: match cookie.expires() {
159            None | Some(cookie::Expiration::Session) => None,
160            Some(DateTime(time)) => Some(Some(Finite::wrap((time.unix_timestamp() * 1000) as f64))),
161        },
162
163        // Let name be the result of running UTF-8 decode without BOM on cookie’s name.
164        name: Some(cookie.name().to_string().into()),
165
166        // Let partitioned be a boolean indicating that the user agent supports cookie partitioning and that i
167        // that cookie has a partition key.
168        partitioned: Some(false), // Do we support partitioning? Spec says true only if UA supports it
169
170        // Let path be the result of running UTF-8 decode without BOM on cookie’s path.
171        path: cookie.path().map(|path| path.to_string().into()),
172
173        sameSite: match cookie.same_site() {
174            Some(SameSite::None) => Some(CookieSameSite::None),
175            Some(SameSite::Lax) => Some(CookieSameSite::Lax),
176            Some(SameSite::Strict) => Some(CookieSameSite::Strict),
177            None => None, // The spec doesnt handle this case, which implies the default of Lax?
178        },
179
180        // Let secure be cookie’s secure-only-flag.
181        secure: cookie.secure(),
182
183        // Let value be the result of running UTF-8 decode without BOM on cookie’s value.
184        value: Some(cookie.value().to_string().into()),
185    }
186}
187
188impl CookieStoreMethods<crate::DomTypeHolder> for CookieStore {
189    /// <https://cookiestore.spec.whatwg.org/#dom-cookiestore-get>
190    fn Get(&self, name: USVString, can_gc: CanGc) -> Rc<Promise> {
191        // 1. Let settings be this’s relevant settings object.
192        let global = self.global();
193
194        // 2. Let origin be settings’s origin.
195        let origin = global.origin();
196
197        // 5. Let p be a new promise.
198        let p = Promise::new(&global, can_gc);
199
200        // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
201        if !origin.is_tuple() {
202            p.reject_error(Error::Security, can_gc);
203            return p;
204        }
205
206        // 4. Let url be settings’s creation URL.
207        let creation_url = global.creation_url();
208
209        // 6. Run the following steps in parallel:
210        let res = self
211            .global()
212            .resource_threads()
213            .send(CoreResourceMsg::GetCookieDataForUrlAsync(
214                self.store_id,
215                creation_url.clone(),
216                Some(name.to_string()),
217            ));
218        if res.is_err() {
219            error!("Failed to send cookiestore message to resource threads");
220        } else {
221            self.in_flight.borrow_mut().push_back(p.clone());
222        }
223
224        // 7. Return p.
225        p
226    }
227
228    /// <https://cookiestore.spec.whatwg.org/#dom-cookiestore-get-options>
229    fn Get_(&self, options: &CookieStoreGetOptions, can_gc: CanGc) -> Rc<Promise> {
230        // 1. Let settings be this’s relevant settings object.
231        let global = self.global();
232
233        // 2. Let origin be settings’s origin.
234        let origin = global.origin();
235
236        // 7. Let p be a new promise.
237        let p = Promise::new(&global, can_gc);
238
239        // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
240        if !origin.is_tuple() {
241            p.reject_error(Error::Security, can_gc);
242            return p;
243        }
244
245        // 4. Let url be settings’s creation URL.
246        let creation_url = global.creation_url();
247
248        // 5. If options is empty, then return a promise rejected with a TypeError.
249        // "is empty" is not strictly defined anywhere in the spec but the only value we require here is "url"
250        if options.url.is_none() && options.name.is_none() {
251            p.reject_error(Error::Type("Options cannot be empty".to_string()), can_gc);
252            return p;
253        }
254
255        let mut final_url = creation_url.clone();
256
257        // 6. If options["url"] is present, then run these steps:
258        if let Some(get_url) = &options.url {
259            // 6.1. Let parsed be the result of parsing options["url"] with settings’s API base URL.
260            let parsed_url = ServoUrl::parse_with_base(Some(&global.api_base_url()), get_url);
261
262            // 6.2. If this’s relevant global object is a Window object and parsed does not equal url with exclude fragments set to true,
263            // then return a promise rejected with a TypeError.
264            if let Some(_window) = DomRoot::downcast::<Window>(self.global()) {
265                if parsed_url
266                    .as_ref()
267                    .is_ok_and(|parsed| !parsed.is_equal_excluding_fragments(creation_url))
268                {
269                    p.reject_error(
270                        Error::Type("URL does not match context".to_string()),
271                        can_gc,
272                    );
273                    return p;
274                }
275            }
276
277            // 6.3. If parsed’s origin and url’s origin are not the same origin,
278            // then return a promise rejected with a TypeError.
279            if parsed_url
280                .as_ref()
281                .is_ok_and(|parsed| creation_url.origin() != parsed.origin())
282            {
283                p.reject_error(Error::Type("Not same origin".to_string()), can_gc);
284                return p;
285            }
286
287            // 6.4. Set url to parsed.
288            if let Ok(url) = parsed_url {
289                final_url = url;
290            }
291        }
292
293        // 6. Run the following steps in parallel:
294        let res = self
295            .global()
296            .resource_threads()
297            .send(CoreResourceMsg::GetCookieDataForUrlAsync(
298                self.store_id,
299                final_url.clone(),
300                options.name.clone().map(|val| val.0),
301            ));
302        if res.is_err() {
303            error!("Failed to send cookiestore message to resource threads");
304        } else {
305            self.in_flight.borrow_mut().push_back(p.clone());
306        }
307
308        p
309    }
310
311    /// <https://cookiestore.spec.whatwg.org/#dom-cookiestore-getall>
312    fn GetAll(&self, name: USVString, can_gc: CanGc) -> Rc<Promise> {
313        // 1. Let settings be this’s relevant settings object.
314        let global = self.global();
315
316        // 2. Let origin be settings’s origin.
317        let origin = global.origin();
318
319        // 5. Let p be a new promise.
320        let p = Promise::new(&global, can_gc);
321
322        // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
323        if !origin.is_tuple() {
324            p.reject_error(Error::Security, can_gc);
325            return p;
326        }
327        // 4. Let url be settings’s creation URL.
328        let creation_url = global.creation_url();
329
330        // 6. Run the following steps in parallel:
331        let res =
332            self.global()
333                .resource_threads()
334                .send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
335                    self.store_id,
336                    creation_url.clone(),
337                    Some(name.to_string()),
338                ));
339        if res.is_err() {
340            error!("Failed to send cookiestore message to resource threads");
341        } else {
342            self.in_flight.borrow_mut().push_back(p.clone());
343        }
344
345        // 7. Return p.
346        p
347    }
348
349    /// <https://cookiestore.spec.whatwg.org/#dom-cookiestore-getall-options>
350    fn GetAll_(&self, options: &CookieStoreGetOptions, can_gc: CanGc) -> Rc<Promise> {
351        // 1. Let settings be this’s relevant settings object.
352        let global = self.global();
353
354        // 2. Let origin be settings’s origin.
355        let origin = global.origin();
356
357        // 6. Let p be a new promise.
358        let p = Promise::new(&global, can_gc);
359
360        // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
361        if !origin.is_tuple() {
362            p.reject_error(Error::Security, can_gc);
363            return p;
364        }
365
366        // 4. Let url be settings’s creation URL.
367        let creation_url = global.creation_url();
368
369        let mut final_url = creation_url.clone();
370
371        // 5. If options["url"] is present, then run these steps:
372        if let Some(get_url) = &options.url {
373            // 5.1. Let parsed be the result of parsing options["url"] with settings’s API base URL.
374            let parsed_url = ServoUrl::parse_with_base(Some(&global.api_base_url()), get_url);
375
376            // If this’s relevant global object is a Window object and parsed does not equal url with exclude fragments set to true,
377            // then return a promise rejected with a TypeError.
378            if let Some(_window) = DomRoot::downcast::<Window>(self.global()) {
379                if parsed_url
380                    .as_ref()
381                    .is_ok_and(|parsed| !parsed.is_equal_excluding_fragments(creation_url))
382                {
383                    p.reject_error(
384                        Error::Type("URL does not match context".to_string()),
385                        can_gc,
386                    );
387                    return p;
388                }
389            }
390
391            // 5.3. If parsed’s origin and url’s origin are not the same origin,
392            // then return a promise rejected with a TypeError.
393            if parsed_url
394                .as_ref()
395                .is_ok_and(|parsed| creation_url.origin() != parsed.origin())
396            {
397                p.reject_error(Error::Type("Not same origin".to_string()), can_gc);
398                return p;
399            }
400
401            // 5.4. Set url to parsed.
402            if let Ok(url) = parsed_url {
403                final_url = url;
404            }
405        }
406
407        // 7. Run the following steps in parallel:
408        let res =
409            self.global()
410                .resource_threads()
411                .send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
412                    self.store_id,
413                    final_url.clone(),
414                    options.name.clone().map(|val| val.0),
415                ));
416        if res.is_err() {
417            error!("Failed to send cookiestore message to resource threads");
418        } else {
419            self.in_flight.borrow_mut().push_back(p.clone());
420        }
421
422        // 8. Return p
423        p
424    }
425
426    /// <https://cookiestore.spec.whatwg.org/#dom-cookiestore-set>
427    fn Set(&self, name: USVString, value: USVString, can_gc: CanGc) -> Rc<Promise> {
428        // 1. Let settings be this’s relevant settings object.
429        let global = self.global();
430
431        // 2. Let origin be settings’s origin.
432        let origin = global.origin();
433
434        // 9. Let p be a new promise.
435        let p = Promise::new(&global, can_gc);
436
437        // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
438        if !origin.is_tuple() {
439            p.reject_error(Error::Security, can_gc);
440            return p;
441        }
442
443        // 4. Let url be settings’s creation URL.
444        // 5. Let domain be null.
445        // 6. Let path be "/".
446        // 7. Let sameSite be strict.
447        // 8. Let partitioned be false.
448        let cookie = Cookie::build((Cow::Owned(name.to_string()), Cow::Owned(value.to_string())))
449            .path("/")
450            .secure(true)
451            .same_site(SameSite::Strict)
452            .partitioned(false);
453        // TODO: This currently doesn't implement all the "set a cookie" steps which involves
454        // additional processing of the name and value
455
456        // 10. Run the following steps in parallel:
457        let res = self
458            .global()
459            .resource_threads()
460            .send(CoreResourceMsg::SetCookieForUrlAsync(
461                self.store_id,
462                self.global().creation_url().clone(),
463                Serde(cookie.build()),
464                NonHTTP,
465            ));
466        if res.is_err() {
467            error!("Failed to send cookiestore message to resource threads");
468        } else {
469            self.in_flight.borrow_mut().push_back(p.clone());
470        }
471
472        // 11. Return p.
473        p
474    }
475
476    /// <https://cookiestore.spec.whatwg.org/#dom-cookiestore-set-options>
477    fn Set_(&self, options: &CookieInit, can_gc: CanGc) -> Rc<Promise> {
478        // 1. Let settings be this’s relevant settings object.
479        let global = self.global();
480
481        // 2. Let origin be settings’s origin.
482        let origin = global.origin();
483
484        // 5. Let p be a new promise.
485        let p = Promise::new(&global, can_gc);
486
487        // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
488        if !origin.is_tuple() {
489            p.reject_error(Error::Security, can_gc);
490            return p;
491        }
492
493        // 4. Let url be settings’s creation URL.
494        let creation_url = global.creation_url();
495
496        // 6.1. Let r be the result of running set a cookie with url, options["name"], options["value"],
497        // options["expires"], options["domain"], options["path"], options["sameSite"], and options["partitioned"].
498        let cookie = Cookie::build((
499            Cow::Owned(options.name.to_string()),
500            Cow::Owned(options.value.to_string()),
501        ));
502        // TODO: This currently doesn't implement all the "set a cookie" steps which involves
503        // additional processing of the name and value
504
505        // 6. Run the following steps in parallel:
506        let res = self
507            .global()
508            .resource_threads()
509            .send(CoreResourceMsg::SetCookieForUrlAsync(
510                self.store_id,
511                creation_url.clone(),
512                Serde(cookie.build()),
513                NonHTTP,
514            ));
515        if res.is_err() {
516            error!("Failed to send cookiestore message to resource threads");
517        } else {
518            self.in_flight.borrow_mut().push_back(p.clone());
519        }
520
521        // 7. Return p
522        p
523    }
524
525    /// <https://cookiestore.spec.whatwg.org/#dom-cookiestore-delete>
526    fn Delete(&self, name: USVString, can_gc: CanGc) -> Rc<Promise> {
527        // 1. Let settings be this’s relevant settings object.
528        let global = self.global();
529
530        // 2. Let origin be settings’s origin.
531        let origin = global.origin();
532
533        // 5. Let p be a new promise.
534        let p = Promise::new(&global, can_gc);
535
536        // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
537        if !origin.is_tuple() {
538            p.reject_error(Error::Security, can_gc);
539            return p;
540        }
541
542        // 6. Run the following steps in parallel:
543        // TODO: the spec passes additional parameters to _delete a cookie_ that we don't handle yet
544        let res = global
545            .resource_threads()
546            .send(CoreResourceMsg::DeleteCookieAsync(
547                self.store_id,
548                global.creation_url().clone(),
549                name.0,
550            ));
551        if res.is_err() {
552            error!("Failed to send cookiestore message to resource threads");
553        } else {
554            self.in_flight.borrow_mut().push_back(p.clone());
555        }
556
557        // 7. Return p.
558        p
559    }
560
561    /// <https://cookiestore.spec.whatwg.org/#dom-cookiestore-delete-options>
562    fn Delete_(&self, options: &CookieStoreDeleteOptions, can_gc: CanGc) -> Rc<Promise> {
563        // 1. Let settings be this’s relevant settings object.
564        let global = self.global();
565
566        // 2. Let origin be settings’s origin.
567        let origin = global.origin();
568
569        // 5. Let p be a new promise.
570        let p = Promise::new(&global, can_gc);
571
572        // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
573        if !origin.is_tuple() {
574            p.reject_error(Error::Security, can_gc);
575            return p;
576        }
577
578        // 6. Run the following steps in parallel:
579        // TODO: the spec passes additional parameters to _delete a cookie_ that we don't handle yet
580        let res = global
581            .resource_threads()
582            .send(CoreResourceMsg::DeleteCookieAsync(
583                self.store_id,
584                global.creation_url().clone(),
585                options.name.to_string(),
586            ));
587        if res.is_err() {
588            error!("Failed to send cookiestore message to resource threads");
589        } else {
590            self.in_flight.borrow_mut().push_back(p.clone());
591        }
592
593        // 7. Return p.
594        p
595    }
596}
597
598impl Drop for CookieStore {
599    fn drop(&mut self) {
600        let res = self
601            .global()
602            .resource_threads()
603            .send(CoreResourceMsg::RemoveCookieListener(self.store_id));
604        if res.is_err() {
605            error!("Failed to send cookiestore message to resource threads");
606        }
607    }
608}