1use std::borrow::Cow;
6use std::collections::VecDeque;
7use std::rc::Rc;
8
9use cookie::{Cookie, SameSite};
10use dom_struct::dom_struct;
11use hyper_serde::Serde;
12use itertools::Itertools;
13use js::context::JSContext;
14use js::jsval::NullValue;
15use net_traits::CookieSource::NonHTTP;
16use net_traits::{CookieAsyncResponse, CookieData, CoreResourceMsg};
17use script_bindings::codegen::GenericBindings::CookieStoreBinding::CookieSameSite;
18use script_bindings::script_runtime::CanGc;
19use servo_base::generic_channel::{GenericCallback, GenericSend, GenericSender};
20use servo_base::id::CookieStoreId;
21use servo_url::ServoUrl;
22use time::OffsetDateTime;
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::document::get_registrable_domain_suffix_of_or_is_equal_to;
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#[derive(JSTraceable, MallocSizeOf)]
41struct DroppableCookieStore {
42 #[no_trace]
44 store_id: CookieStoreId,
45 #[no_trace]
46 unregister_channel: GenericSender<CoreResourceMsg>,
47}
48
49impl Drop for DroppableCookieStore {
50 fn drop(&mut self) {
51 let res = self
52 .unregister_channel
53 .send(CoreResourceMsg::RemoveCookieListener(self.store_id));
54 if res.is_err() {
55 error!("Failed to send cookiestore message to resource threads");
56 }
57 }
58}
59
60#[dom_struct]
65pub(crate) struct CookieStore {
66 eventtarget: EventTarget,
67 #[conditional_malloc_size_of]
68 in_flight: DomRefCell<VecDeque<Rc<Promise>>>,
69 droppable: DroppableCookieStore,
70}
71
72struct CookieListener {
73 task_source: SendableTaskSource,
75 context: Trusted<CookieStore>,
76}
77
78impl CookieListener {
79 pub(crate) fn handle(&self, message: CookieAsyncResponse) {
80 let context = self.context.clone();
81 self.task_source.queue(task!(cookie_message: move || {
82 let Some(promise) = context.root().in_flight.borrow_mut().pop_front() else {
83 warn!("No promise exists for cookie store response");
84 return;
85 };
86 match message.data {
87 CookieData::Get(cookie) => {
88 if let Some(cookie) = cookie {
91 promise.resolve_native(&cookie_to_list_item(cookie.into_inner()), CanGc::note());
93 } else {
94 promise.resolve_native(&NullValue(), CanGc::note());
96 }
97 },
98 CookieData::GetAll(cookies) => {
99 promise.resolve_native(
101 &cookies
102 .into_iter()
103 .map(|cookie| cookie_to_list_item(cookie.0))
104 .collect_vec(),
105 CanGc::note());
106 },
107 CookieData::Delete(_) | CookieData::Change(_) | CookieData::Set(_) => {
108 promise.resolve_native(&(), CanGc::note());
109 }
110 }
111 }));
112 }
113}
114
115impl CookieStore {
116 fn new_inherited(unregister_channel: GenericSender<CoreResourceMsg>) -> CookieStore {
117 CookieStore {
118 eventtarget: EventTarget::new_inherited(),
119 in_flight: Default::default(),
120 droppable: DroppableCookieStore {
121 store_id: CookieStoreId::new(),
122 unregister_channel,
123 },
124 }
125 }
126
127 pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot<CookieStore> {
128 let store = reflect_dom_object(
129 Box::new(CookieStore::new_inherited(
130 global.resource_threads().core_thread.clone(),
131 )),
132 global,
133 can_gc,
134 );
135 store.setup_route();
136 store
137 }
138
139 fn setup_route(&self) {
140 let context = Trusted::new(self);
141 let cs_listener = CookieListener {
142 task_source: self
143 .global()
144 .task_manager()
145 .dom_manipulation_task_source()
146 .to_sendable(),
147 context,
148 };
149
150 let callback = GenericCallback::new(move |message| match message {
151 Ok(msg) => cs_listener.handle(msg),
152 Err(err) => warn!("Error receiving a CookieStore message: {:?}", err),
153 })
154 .expect("Could not create cookie store callback");
155
156 let res = self
157 .global()
158 .resource_threads()
159 .send(CoreResourceMsg::NewCookieListener(
160 self.droppable.store_id,
161 callback,
162 self.global().creation_url(),
163 ));
164 if res.is_err() {
165 error!("Failed to send cookiestore message to resource threads");
166 }
167 }
168}
169
170fn cookie_to_list_item(cookie: Cookie) -> CookieListItem {
172 CookieListItem {
175 name: Some(cookie.name().to_string().into()),
177
178 value: Some(cookie.value().to_string().into()),
180 }
181}
182
183impl CookieStoreMethods<crate::DomTypeHolder> for CookieStore {
184 fn Get(&self, cx: &mut JSContext, name: USVString) -> Rc<Promise> {
186 let global = self.global();
188
189 let origin = global.origin();
191
192 let p = Promise::new(&global, CanGc::from_cx(cx));
194
195 if !origin.is_tuple() {
197 p.reject_error(Error::Security(None), CanGc::from_cx(cx));
198 return p;
199 }
200
201 let creation_url = global.creation_url();
203
204 let name = CookieStore::normalize(&name);
205
206 let res = self
208 .global()
209 .resource_threads()
210 .send(CoreResourceMsg::GetCookieDataForUrlAsync(
211 self.droppable.store_id,
212 creation_url,
213 Some(name),
214 ));
215 if res.is_err() {
216 error!("Failed to send cookiestore message to resource threads");
217 } else {
218 self.in_flight.borrow_mut().push_back(p.clone());
219 }
220
221 p
223 }
224
225 fn Get_(&self, cx: &mut JSContext, options: &CookieStoreGetOptions) -> Rc<Promise> {
227 let global = self.global();
229
230 let origin = global.origin();
232
233 let p = Promise::new(&global, CanGc::from_cx(cx));
235
236 if !origin.is_tuple() {
238 p.reject_error(Error::Security(None), CanGc::from_cx(cx));
239 return p;
240 }
241
242 let creation_url = global.creation_url();
244
245 if options.url.is_none() && options.name.is_none() {
248 p.reject_error(
249 Error::Type(c"Options cannot be empty".to_owned()),
250 CanGc::from_cx(cx),
251 );
252 return p;
253 }
254
255 let mut final_url = creation_url.clone();
256
257 if let Some(get_url) = &options.url {
259 let parsed_url = ServoUrl::parse_with_base(Some(&global.api_base_url()), get_url);
261
262 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(c"URL does not match context".to_owned()),
271 CanGc::from_cx(cx),
272 );
273 return p;
274 }
275 }
276
277 if parsed_url
280 .as_ref()
281 .is_ok_and(|parsed| creation_url.origin() != parsed.origin())
282 {
283 p.reject_error(
284 Error::Type(c"Not same origin".to_owned()),
285 CanGc::from_cx(cx),
286 );
287 return p;
288 }
289
290 if let Ok(url) = parsed_url {
292 final_url = url;
293 }
294 }
295
296 let res = self
298 .global()
299 .resource_threads()
300 .send(CoreResourceMsg::GetCookieDataForUrlAsync(
301 self.droppable.store_id,
302 final_url,
303 options.name.clone().map(|val| CookieStore::normalize(&val)),
304 ));
305 if res.is_err() {
306 error!("Failed to send cookiestore message to resource threads");
307 } else {
308 self.in_flight.borrow_mut().push_back(p.clone());
309 }
310
311 p
312 }
313
314 fn GetAll(&self, cx: &mut JSContext, name: USVString) -> Rc<Promise> {
316 let global = self.global();
318
319 let origin = global.origin();
321
322 let p = Promise::new(&global, CanGc::from_cx(cx));
324
325 if !origin.is_tuple() {
327 p.reject_error(Error::Security(None), CanGc::from_cx(cx));
328 return p;
329 }
330 let creation_url = global.creation_url();
332
333 let name = CookieStore::normalize(&name);
335
336 let res =
338 self.global()
339 .resource_threads()
340 .send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
341 self.droppable.store_id,
342 creation_url,
343 Some(name),
344 ));
345 if res.is_err() {
346 error!("Failed to send cookiestore message to resource threads");
347 } else {
348 self.in_flight.borrow_mut().push_back(p.clone());
349 }
350
351 p
353 }
354
355 fn GetAll_(&self, cx: &mut JSContext, options: &CookieStoreGetOptions) -> Rc<Promise> {
357 let global = self.global();
359
360 let origin = global.origin();
362
363 let p = Promise::new(&global, CanGc::from_cx(cx));
365
366 if !origin.is_tuple() {
368 p.reject_error(Error::Security(None), CanGc::from_cx(cx));
369 return p;
370 }
371
372 let creation_url = global.creation_url();
374
375 let mut final_url = creation_url.clone();
376
377 if let Some(get_url) = &options.url {
379 let parsed_url = ServoUrl::parse_with_base(Some(&global.api_base_url()), get_url);
381
382 if let Some(_window) = DomRoot::downcast::<Window>(self.global()) {
385 if parsed_url
386 .as_ref()
387 .is_ok_and(|parsed| !parsed.is_equal_excluding_fragments(&creation_url))
388 {
389 p.reject_error(
390 Error::Type(c"URL does not match context".to_owned()),
391 CanGc::from_cx(cx),
392 );
393 return p;
394 }
395 }
396
397 if parsed_url
400 .as_ref()
401 .is_ok_and(|parsed| creation_url.origin() != parsed.origin())
402 {
403 p.reject_error(
404 Error::Type(c"Not same origin".to_owned()),
405 CanGc::from_cx(cx),
406 );
407 return p;
408 }
409
410 if let Ok(url) = parsed_url {
412 final_url = url;
413 }
414 }
415
416 let res =
418 self.global()
419 .resource_threads()
420 .send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
421 self.droppable.store_id,
422 final_url,
423 options.name.clone().map(|val| CookieStore::normalize(&val)),
424 ));
425 if res.is_err() {
426 error!("Failed to send cookiestore message to resource threads");
427 } else {
428 self.in_flight.borrow_mut().push_back(p.clone());
429 }
430
431 p
433 }
434
435 fn Set(&self, cx: &mut JSContext, name: USVString, value: USVString) -> Rc<Promise> {
437 let global = self.global();
439
440 let origin = global.origin();
442
443 let p = Promise::new(&global, CanGc::from_cx(cx));
445
446 if !origin.is_tuple() {
448 p.reject_error(Error::Security(None), CanGc::from_cx(cx));
449 return p;
450 }
451
452 let properties = CookieInit {
454 name,
455 value,
456 expires: None,
457 domain: None,
458 path: USVString(String::from("/")),
459 sameSite: CookieSameSite::Strict,
460 partitioned: false,
461 };
462 let creation_url = global.creation_url();
463 let Some(cookie) = CookieStore::set_a_cookie(&creation_url, &properties) else {
464 p.reject_error(
466 Error::Type(c"Invalid cookie".to_owned()),
467 CanGc::from_cx(cx),
468 );
469 return p;
470 };
471
472 let res = self
474 .global()
475 .resource_threads()
476 .send(CoreResourceMsg::SetCookieForUrlAsync(
477 self.droppable.store_id,
478 creation_url.clone(),
479 Serde(cookie.into_owned()),
480 NonHTTP,
481 ));
482 if res.is_err() {
483 error!("Failed to send cookiestore message to resource threads");
484 } else {
485 self.in_flight.borrow_mut().push_back(p.clone());
486 }
487
488 p
490 }
491
492 fn Set_(&self, cx: &mut JSContext, options: &CookieInit) -> Rc<Promise> {
494 let global = self.global();
496
497 let origin = global.origin();
499
500 let p = Promise::new(&global, CanGc::from_cx(cx));
502
503 if !origin.is_tuple() {
505 p.reject_error(Error::Security(None), CanGc::from_cx(cx));
506 return p;
507 }
508
509 let creation_url = global.creation_url();
511
512 let Some(cookie) = CookieStore::set_a_cookie(&creation_url, options) else {
515 p.reject_error(
516 Error::Type(c"Invalid cookie".to_owned()),
517 CanGc::from_cx(cx),
518 );
519 return p;
520 };
521
522 let res = self
524 .global()
525 .resource_threads()
526 .send(CoreResourceMsg::SetCookieForUrlAsync(
527 self.droppable.store_id,
528 creation_url.clone(),
529 Serde(cookie.into_owned()),
530 NonHTTP,
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 p
540 }
541
542 fn Delete(&self, cx: &mut JSContext, name: USVString) -> Rc<Promise> {
544 let global = self.global();
546
547 let origin = global.origin();
549
550 let p = Promise::new(&global, CanGc::from_cx(cx));
552
553 if !origin.is_tuple() {
555 p.reject_error(Error::Security(None), CanGc::from_cx(cx));
556 return p;
557 }
558
559 let res = global
562 .resource_threads()
563 .send(CoreResourceMsg::DeleteCookieAsync(
564 self.droppable.store_id,
565 global.creation_url(),
566 name.0,
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 p
576 }
577
578 fn Delete_(&self, cx: &mut JSContext, options: &CookieStoreDeleteOptions) -> Rc<Promise> {
580 let global = self.global();
582
583 let origin = global.origin();
585
586 let p = Promise::new(&global, CanGc::from_cx(cx));
588
589 if !origin.is_tuple() {
591 p.reject_error(Error::Security(None), CanGc::from_cx(cx));
592 return p;
593 }
594
595 let res = global
598 .resource_threads()
599 .send(CoreResourceMsg::DeleteCookieAsync(
600 self.droppable.store_id,
601 global.creation_url(),
602 options.name.to_string(),
603 ));
604 if res.is_err() {
605 error!("Failed to send cookiestore message to resource threads");
606 } else {
607 self.in_flight.borrow_mut().push_back(p.clone());
608 }
609
610 p
612 }
613}
614
615impl CookieStore {
616 fn normalize(value: &USVString) -> String {
618 value.trim_matches([' ', '\t']).into()
619 }
620
621 fn set_a_cookie<'a>(url: &'a ServoUrl, properties: &'a CookieInit) -> Option<Cookie<'a>> {
623 let name = CookieStore::normalize(&properties.name);
625 let value = CookieStore::normalize(&properties.value);
627
628 if CookieStore::contains_control_characters(&name) ||
630 CookieStore::contains_control_characters(&value)
631 {
632 return None;
633 }
634
635 if name.contains('=') {
637 return None;
638 }
639
640 if name.is_empty() {
642 if value.contains('=') || value.is_empty() {
645 return None;
646 }
647 let lowercased_value = value.to_ascii_lowercase();
649 if ["__host-", "__host-http-", "__http-", "__secure-"]
650 .iter()
651 .any(|prefix| lowercased_value.starts_with(prefix))
652 {
653 return None;
654 }
655 }
656
657 let lowercased_name = name.to_ascii_lowercase();
659 if lowercased_name.starts_with("__host-http-") || lowercased_name.starts_with("__http-") {
660 return None;
661 }
662
663 if name.len() + value.len() > 4096 {
665 return None;
666 }
667
668 let mut cookie = Cookie::build((Cow::Owned(name), Cow::Owned(value)))
669 .secure(true)
671 .partitioned(properties.partitioned)
673 .same_site(match properties.sameSite {
675 CookieSameSite::Lax => SameSite::Lax,
676 CookieSameSite::Strict => SameSite::Strict,
677 CookieSameSite::None => SameSite::None,
678 });
679
680 if let Some(domain) = &properties.domain {
682 let host = match url.host() {
684 Some(host) => host.to_owned(),
685 None => return None,
686 };
687 if domain.starts_with('.') {
689 return None;
690 }
691 if lowercased_name.starts_with("__host-") {
693 return None;
694 }
695
696 let domain = get_registrable_domain_suffix_of_or_is_equal_to(domain, host)?;
701
702 let domain = domain.to_string();
704 if domain.len() > 1024 {
706 return None;
707 }
708 cookie.inner_mut().set_domain(domain);
710 }
711
712 if let Some(expiry) = properties.expires {
714 cookie.inner_mut().set_expires(
717 OffsetDateTime::from_unix_timestamp((*expiry / 1000.0) as i64)
718 .expect("cookie expiry out of range"),
719 );
720 }
721
722 let path = if properties.path.is_empty() {
724 let mut cloned_url = url.clone();
726 {
727 let mut path_segments = cloned_url
730 .as_mut_url()
731 .path_segments_mut()
732 .expect("document creation url cannot be a base");
733 if url.path_segments().is_some_and(|ps| ps.count() > 1) {
735 path_segments.pop();
736 } else {
737 path_segments.clear();
739 }
740 }
741 cloned_url.path().to_owned()
742 } else {
743 properties.path.to_string()
744 };
745 if !path.starts_with('/') {
747 return None;
748 }
749 if path != "/" && lowercased_name.starts_with("__host-") {
751 return None;
752 }
753 if path.len() > 1024 {
755 return None;
756 }
757 cookie.inner_mut().set_path(path);
759
760 Some(cookie.build())
761 }
762
763 fn contains_control_characters(val: &str) -> bool {
765 val.contains(
767 |v| matches!(v, '\u{0000}'..='\u{0008}' | '\u{000a}'..='\u{001f}' | '\u{007f}' | ';'),
768 )
769 }
770}