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