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::cell::DomRefCell;
18use script_bindings::codegen::GenericBindings::CookieStoreBinding::CookieSameSite;
19use script_bindings::reflector::reflect_dom_object;
20use script_bindings::script_runtime::CanGc;
21use servo_base::generic_channel::{GenericCallback, GenericSend, GenericSender};
22use servo_base::id::CookieStoreId;
23use servo_url::ServoUrl;
24use time::OffsetDateTime;
25
26use crate::dom::bindings::codegen::Bindings::CookieStoreBinding::{
27 CookieInit, CookieListItem, CookieStoreDeleteOptions, CookieStoreGetOptions, CookieStoreMethods,
28};
29use crate::dom::bindings::error::Error;
30use crate::dom::bindings::refcounted::Trusted;
31use crate::dom::bindings::reflector::DomGlobal;
32use crate::dom::bindings::root::DomRoot;
33use crate::dom::bindings::str::USVString;
34use crate::dom::document::get_registrable_domain_suffix_of_or_is_equal_to;
35use crate::dom::eventtarget::EventTarget;
36use crate::dom::globalscope::GlobalScope;
37use crate::dom::promise::Promise;
38use crate::dom::window::Window;
39use crate::task_source::SendableTaskSource;
40
41#[derive(JSTraceable, MallocSizeOf)]
42struct DroppableCookieStore {
43 #[no_trace]
45 store_id: CookieStoreId,
46 #[no_trace]
47 unregister_channel: GenericSender<CoreResourceMsg>,
48}
49
50impl Drop for DroppableCookieStore {
51 fn drop(&mut self) {
52 let res = self
53 .unregister_channel
54 .send(CoreResourceMsg::RemoveCookieListener(self.store_id));
55 if res.is_err() {
56 error!("Failed to send cookiestore message to resource threads");
57 }
58 }
59}
60
61#[dom_struct]
66pub(crate) struct CookieStore {
67 eventtarget: EventTarget,
68 #[conditional_malloc_size_of]
69 in_flight: DomRefCell<VecDeque<Rc<Promise>>>,
70 droppable: DroppableCookieStore,
71}
72
73struct CookieListener {
74 task_source: SendableTaskSource,
76 context: Trusted<CookieStore>,
77}
78
79impl CookieListener {
80 pub(crate) fn handle(&self, message: CookieAsyncResponse) {
81 let context = self.context.clone();
82 self.task_source.queue(task!(cookie_message: move || {
83 let Some(promise) = context.root().in_flight.borrow_mut().pop_front() else {
84 warn!("No promise exists for cookie store response");
85 return;
86 };
87 match message.data {
88 CookieData::Get(cookie) => {
89 if let Some(cookie) = cookie {
92 promise.resolve_native(&cookie_to_list_item(cookie.into_inner()), CanGc::deprecated_note());
94 } else {
95 promise.resolve_native(&NullValue(), CanGc::deprecated_note());
97 }
98 },
99 CookieData::GetAll(cookies) => {
100 promise.resolve_native(
102 &cookies
103 .into_iter()
104 .map(|cookie| cookie_to_list_item(cookie.0))
105 .collect_vec(),
106 CanGc::deprecated_note());
107 },
108 CookieData::Delete(_) | CookieData::Change(_) | CookieData::Set(_) => {
109 promise.resolve_native(&(), CanGc::deprecated_note());
110 }
111 }
112 }));
113 }
114}
115
116impl CookieStore {
117 fn new_inherited(unregister_channel: GenericSender<CoreResourceMsg>) -> CookieStore {
118 CookieStore {
119 eventtarget: EventTarget::new_inherited(),
120 in_flight: Default::default(),
121 droppable: DroppableCookieStore {
122 store_id: CookieStoreId::new(),
123 unregister_channel,
124 },
125 }
126 }
127
128 pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot<CookieStore> {
129 let store = reflect_dom_object(
130 Box::new(CookieStore::new_inherited(
131 global.resource_threads().core_thread.clone(),
132 )),
133 global,
134 can_gc,
135 );
136 store.setup_route();
137 store
138 }
139
140 fn setup_route(&self) {
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 let callback = GenericCallback::new(move |message| match message {
152 Ok(msg) => cs_listener.handle(msg),
153 Err(err) => warn!("Error receiving a CookieStore message: {:?}", err),
154 })
155 .expect("Could not create cookie store callback");
156
157 let res = self
158 .global()
159 .resource_threads()
160 .send(CoreResourceMsg::NewCookieListener(
161 self.droppable.store_id,
162 callback,
163 self.global().creation_url(),
164 ));
165 if res.is_err() {
166 error!("Failed to send cookiestore message to resource threads");
167 }
168 }
169}
170
171fn cookie_to_list_item(cookie: Cookie) -> CookieListItem {
173 CookieListItem {
176 name: Some(cookie.name().to_string().into()),
178
179 value: Some(cookie.value().to_string().into()),
181 }
182}
183
184impl CookieStoreMethods<crate::DomTypeHolder> for CookieStore {
185 fn Get(&self, cx: &mut JSContext, name: USVString) -> Rc<Promise> {
187 let global = self.global();
189
190 let origin = global.origin();
192
193 let p = Promise::new(&global, CanGc::from_cx(cx));
195
196 if !origin.is_tuple() {
198 p.reject_error(Error::Security(None), CanGc::from_cx(cx));
199 return p;
200 }
201
202 let creation_url = global.creation_url();
204
205 let name = CookieStore::normalize(&name);
206
207 let res = self
209 .global()
210 .resource_threads()
211 .send(CoreResourceMsg::GetCookieDataForUrlAsync(
212 self.droppable.store_id,
213 creation_url,
214 Some(name),
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 p
224 }
225
226 fn Get_(&self, cx: &mut JSContext, options: &CookieStoreGetOptions) -> Rc<Promise> {
228 let global = self.global();
230
231 let origin = global.origin();
233
234 let p = Promise::new(&global, CanGc::from_cx(cx));
236
237 if !origin.is_tuple() {
239 p.reject_error(Error::Security(None), CanGc::from_cx(cx));
240 return p;
241 }
242
243 let creation_url = global.creation_url();
245
246 if options.url.is_none() && options.name.is_none() {
249 p.reject_error(
250 Error::Type(c"Options cannot be empty".to_owned()),
251 CanGc::from_cx(cx),
252 );
253 return p;
254 }
255
256 let mut final_url = creation_url.clone();
257
258 if let Some(get_url) = &options.url {
260 let parsed_url = ServoUrl::parse_with_base(Some(&global.api_base_url()), get_url);
262
263 if let Some(_window) = DomRoot::downcast::<Window>(self.global()) &&
266 parsed_url
267 .as_ref()
268 .is_ok_and(|parsed| !parsed.is_equal_excluding_fragments(&creation_url))
269 {
270 p.reject_error(
271 Error::Type(c"URL does not match context".to_owned()),
272 CanGc::from_cx(cx),
273 );
274 return p;
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 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 if parsed_url
399 .as_ref()
400 .is_ok_and(|parsed| creation_url.origin() != parsed.origin())
401 {
402 p.reject_error(
403 Error::Type(c"Not same origin".to_owned()),
404 CanGc::from_cx(cx),
405 );
406 return p;
407 }
408
409 if let Ok(url) = parsed_url {
411 final_url = url;
412 }
413 }
414
415 let res =
417 self.global()
418 .resource_threads()
419 .send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
420 self.droppable.store_id,
421 final_url,
422 options.name.clone().map(|val| CookieStore::normalize(&val)),
423 ));
424 if res.is_err() {
425 error!("Failed to send cookiestore message to resource threads");
426 } else {
427 self.in_flight.borrow_mut().push_back(p.clone());
428 }
429
430 p
432 }
433
434 fn Set(&self, cx: &mut JSContext, name: USVString, value: USVString) -> Rc<Promise> {
436 let global = self.global();
438
439 let origin = global.origin();
441
442 let p = Promise::new(&global, CanGc::from_cx(cx));
444
445 if !origin.is_tuple() {
447 p.reject_error(Error::Security(None), CanGc::from_cx(cx));
448 return p;
449 }
450
451 let properties = CookieInit {
453 name,
454 value,
455 expires: None,
456 domain: None,
457 path: USVString(String::from("/")),
458 sameSite: CookieSameSite::Strict,
459 partitioned: false,
460 };
461 let creation_url = global.creation_url();
462 let Some(cookie) = CookieStore::set_a_cookie(&creation_url, &properties) else {
463 p.reject_error(
465 Error::Type(c"Invalid cookie".to_owned()),
466 CanGc::from_cx(cx),
467 );
468 return p;
469 };
470
471 let res = self
473 .global()
474 .resource_threads()
475 .send(CoreResourceMsg::SetCookieForUrlAsync(
476 self.droppable.store_id,
477 creation_url.clone(),
478 Serde(cookie.into_owned()),
479 NonHTTP,
480 ));
481 if res.is_err() {
482 error!("Failed to send cookiestore message to resource threads");
483 } else {
484 self.in_flight.borrow_mut().push_back(p.clone());
485 }
486
487 p
489 }
490
491 fn Set_(&self, cx: &mut JSContext, options: &CookieInit) -> Rc<Promise> {
493 let global = self.global();
495
496 let origin = global.origin();
498
499 let p = Promise::new(&global, CanGc::from_cx(cx));
501
502 if !origin.is_tuple() {
504 p.reject_error(Error::Security(None), CanGc::from_cx(cx));
505 return p;
506 }
507
508 let creation_url = global.creation_url();
510
511 let Some(cookie) = CookieStore::set_a_cookie(&creation_url, options) else {
514 p.reject_error(
515 Error::Type(c"Invalid cookie".to_owned()),
516 CanGc::from_cx(cx),
517 );
518 return p;
519 };
520
521 let res = self
523 .global()
524 .resource_threads()
525 .send(CoreResourceMsg::SetCookieForUrlAsync(
526 self.droppable.store_id,
527 creation_url.clone(),
528 Serde(cookie.into_owned()),
529 NonHTTP,
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 p
539 }
540
541 fn Delete(&self, cx: &mut JSContext, name: USVString) -> Rc<Promise> {
543 let global = self.global();
545
546 let origin = global.origin();
548
549 let p = Promise::new(&global, CanGc::from_cx(cx));
551
552 if !origin.is_tuple() {
554 p.reject_error(Error::Security(None), CanGc::from_cx(cx));
555 return p;
556 }
557
558 let res = global
561 .resource_threads()
562 .send(CoreResourceMsg::DeleteCookieAsync(
563 self.droppable.store_id,
564 global.creation_url(),
565 name.0,
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 p
575 }
576
577 fn Delete_(&self, cx: &mut JSContext, options: &CookieStoreDeleteOptions) -> Rc<Promise> {
579 let global = self.global();
581
582 let origin = global.origin();
584
585 let p = Promise::new(&global, CanGc::from_cx(cx));
587
588 if !origin.is_tuple() {
590 p.reject_error(Error::Security(None), CanGc::from_cx(cx));
591 return p;
592 }
593
594 let res = global
597 .resource_threads()
598 .send(CoreResourceMsg::DeleteCookieAsync(
599 self.droppable.store_id,
600 global.creation_url(),
601 options.name.to_string(),
602 ));
603 if res.is_err() {
604 error!("Failed to send cookiestore message to resource threads");
605 } else {
606 self.in_flight.borrow_mut().push_back(p.clone());
607 }
608
609 p
611 }
612}
613
614impl CookieStore {
615 fn normalize(value: &USVString) -> String {
617 value.trim_matches([' ', '\t']).into()
618 }
619
620 fn set_a_cookie<'a>(url: &'a ServoUrl, properties: &'a CookieInit) -> Option<Cookie<'a>> {
622 let name = CookieStore::normalize(&properties.name);
624 let value = CookieStore::normalize(&properties.value);
626
627 if CookieStore::contains_control_characters(&name) ||
629 CookieStore::contains_control_characters(&value)
630 {
631 return None;
632 }
633
634 if name.contains('=') {
636 return None;
637 }
638
639 if name.is_empty() {
641 if value.contains('=') || value.is_empty() {
644 return None;
645 }
646 let lowercased_value = value.to_ascii_lowercase();
648 if ["__host-", "__host-http-", "__http-", "__secure-"]
649 .iter()
650 .any(|prefix| lowercased_value.starts_with(prefix))
651 {
652 return None;
653 }
654 }
655
656 let lowercased_name = name.to_ascii_lowercase();
658 if lowercased_name.starts_with("__host-http-") || lowercased_name.starts_with("__http-") {
659 return None;
660 }
661
662 if name.len() + value.len() > 4096 {
664 return None;
665 }
666
667 let mut cookie = Cookie::build((Cow::Owned(name), Cow::Owned(value)))
668 .secure(true)
670 .partitioned(properties.partitioned)
672 .same_site(match properties.sameSite {
674 CookieSameSite::Lax => SameSite::Lax,
675 CookieSameSite::Strict => SameSite::Strict,
676 CookieSameSite::None => SameSite::None,
677 });
678
679 if let Some(domain) = &properties.domain {
681 let host = match url.host() {
683 Some(host) => host.to_owned(),
684 None => return None,
685 };
686 if domain.starts_with('.') {
688 return None;
689 }
690 if lowercased_name.starts_with("__host-") {
692 return None;
693 }
694
695 let domain = get_registrable_domain_suffix_of_or_is_equal_to(domain, host)?;
700
701 let domain = domain.to_string();
703 if domain.len() > 1024 {
705 return None;
706 }
707 cookie.inner_mut().set_domain(domain);
709 }
710
711 if let Some(expiry) = properties.expires {
713 cookie.inner_mut().set_expires(
716 OffsetDateTime::from_unix_timestamp((*expiry / 1000.0) as i64)
717 .expect("cookie expiry out of range"),
718 );
719 }
720
721 let path = if properties.path.is_empty() {
723 let mut cloned_url = url.clone();
725 {
726 let mut path_segments = cloned_url
729 .as_mut_url()
730 .path_segments_mut()
731 .expect("document creation url cannot be a base");
732 if url.path_segments().is_some_and(|ps| ps.count() > 1) {
734 path_segments.pop();
735 } else {
736 path_segments.clear();
738 }
739 }
740 cloned_url.path().to_owned()
741 } else {
742 properties.path.to_string()
743 };
744 if !path.starts_with('/') {
746 return None;
747 }
748 if path != "/" && lowercased_name.starts_with("__host-") {
750 return None;
751 }
752 if path.len() > 1024 {
754 return None;
755 }
756 cookie.inner_mut().set_path(path);
758
759 Some(cookie.build())
760 }
761
762 fn contains_control_characters(val: &str) -> bool {
764 val.contains(
766 |v| matches!(v, '\u{0000}'..='\u{0008}' | '\u{000a}'..='\u{001f}' | '\u{007f}' | ';'),
767 )
768 }
769}