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