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