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_with_cx;
20use servo_base::generic_channel::{GenericCallback, GenericSend, GenericSender};
21use servo_base::id::CookieStoreId;
22use servo_url::ServoUrl;
23use time::OffsetDateTime;
24
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;
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 |cx| {
82 let Some(promise) = context.root().in_flight.safe_borrow_mut(cx.no_gc()).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(cx, &cookie_to_list_item(cookie.into_inner()));
93 } else {
94 promise.resolve_native(cx, &NullValue());
96 }
97 },
98 CookieData::GetAll(cookies) => {
99 promise.resolve_native(cx,
101 &cookies
102 .into_iter()
103 .map(|cookie| cookie_to_list_item(cookie.0))
104 .collect_vec(),);
105 },
106 CookieData::Delete(_) | CookieData::Change(_) | CookieData::Set(_) => {
107 promise.resolve_native(cx, &());
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(cx: &mut JSContext, global: &GlobalScope) -> DomRoot<CookieStore> {
127 let store = reflect_dom_object_with_cx(
128 Box::new(CookieStore::new_inherited(
129 global.resource_threads().core_thread.clone(),
130 )),
131 global,
132 cx,
133 );
134 store.setup_route();
135 store
136 }
137
138 fn setup_route(&self) {
139 let context = Trusted::new(self);
140 let cs_listener = CookieListener {
141 task_source: self
142 .global()
143 .task_manager()
144 .dom_manipulation_task_source()
145 .to_sendable(),
146 context,
147 };
148
149 let callback = GenericCallback::new(move |message| match message {
150 Ok(msg) => cs_listener.handle(msg),
151 Err(err) => warn!("Error receiving a CookieStore message: {:?}", err),
152 })
153 .expect("Could not create cookie store callback");
154
155 let res = self
156 .global()
157 .resource_threads()
158 .send(CoreResourceMsg::NewCookieListener(
159 self.droppable.store_id,
160 callback,
161 self.global().creation_url(),
162 ));
163 if res.is_err() {
164 error!("Failed to send cookiestore message to resource threads");
165 }
166 }
167}
168
169fn cookie_to_list_item(cookie: Cookie) -> CookieListItem {
171 CookieListItem {
174 name: Some(cookie.name().to_string().into()),
176
177 value: Some(cookie.value().to_string().into()),
179 }
180}
181
182impl CookieStoreMethods<crate::DomTypeHolder> for CookieStore {
183 fn Get(&self, cx: &mut JSContext, name: USVString) -> Rc<Promise> {
185 let global = self.global();
187
188 let origin = global.origin();
190
191 let p = Promise::new(cx, &global);
193
194 if !origin.is_tuple() {
196 p.reject_error(cx, Error::Security(None));
197 return p;
198 }
199
200 let creation_url = global.creation_url();
202
203 let name = CookieStore::normalize(&name);
204
205 let res = self
207 .global()
208 .resource_threads()
209 .send(CoreResourceMsg::GetCookieDataForUrlAsync(
210 self.droppable.store_id,
211 creation_url,
212 Some(name),
213 ));
214 if res.is_err() {
215 error!("Failed to send cookiestore message to resource threads");
216 } else {
217 self.in_flight
218 .safe_borrow_mut(cx.no_gc())
219 .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(cx, &global);
236
237 if !origin.is_tuple() {
239 p.reject_error(cx, Error::Security(None));
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(cx, Error::Type(c"Options cannot be empty".to_owned()));
250 return p;
251 }
252
253 let mut final_url = creation_url.clone();
254
255 if let Some(get_url) = &options.url {
257 let parsed_url = ServoUrl::parse_with_base(Some(&global.api_base_url()), get_url);
259
260 if let Some(_window) = DomRoot::downcast::<Window>(self.global()) &&
263 parsed_url
264 .as_ref()
265 .is_ok_and(|parsed| !parsed.is_equal_excluding_fragments(&creation_url))
266 {
267 p.reject_error(cx, Error::Type(c"URL does not match context".to_owned()));
268 return p;
269 }
270
271 if parsed_url
274 .as_ref()
275 .is_ok_and(|parsed| creation_url.origin() != parsed.origin())
276 {
277 p.reject_error(cx, Error::Type(c"Not same origin".to_owned()));
278 return p;
279 }
280
281 if let Ok(url) = parsed_url {
283 final_url = url;
284 }
285 }
286
287 let res = self
289 .global()
290 .resource_threads()
291 .send(CoreResourceMsg::GetCookieDataForUrlAsync(
292 self.droppable.store_id,
293 final_url,
294 options.name.clone().map(|val| CookieStore::normalize(&val)),
295 ));
296 if res.is_err() {
297 error!("Failed to send cookiestore message to resource threads");
298 } else {
299 self.in_flight
300 .safe_borrow_mut(cx.no_gc())
301 .push_back(p.clone());
302 }
303
304 p
305 }
306
307 fn GetAll(&self, cx: &mut JSContext, name: USVString) -> Rc<Promise> {
309 let global = self.global();
311
312 let origin = global.origin();
314
315 let p = Promise::new(cx, &global);
317
318 if !origin.is_tuple() {
320 p.reject_error(cx, Error::Security(None));
321 return p;
322 }
323 let creation_url = global.creation_url();
325
326 let name = CookieStore::normalize(&name);
328
329 let res =
331 self.global()
332 .resource_threads()
333 .send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
334 self.droppable.store_id,
335 creation_url,
336 Some(name),
337 ));
338 if res.is_err() {
339 error!("Failed to send cookiestore message to resource threads");
340 } else {
341 self.in_flight
342 .safe_borrow_mut(cx.no_gc())
343 .push_back(p.clone());
344 }
345
346 p
348 }
349
350 fn GetAll_(&self, cx: &mut JSContext, options: &CookieStoreGetOptions) -> Rc<Promise> {
352 let global = self.global();
354
355 let origin = global.origin();
357
358 let p = Promise::new(cx, &global);
360
361 if !origin.is_tuple() {
363 p.reject_error(cx, Error::Security(None));
364 return p;
365 }
366
367 let creation_url = global.creation_url();
369
370 let mut final_url = creation_url.clone();
371
372 if let Some(get_url) = &options.url {
374 let parsed_url = ServoUrl::parse_with_base(Some(&global.api_base_url()), get_url);
376
377 if let Some(_window) = DomRoot::downcast::<Window>(self.global()) &&
380 parsed_url
381 .as_ref()
382 .is_ok_and(|parsed| !parsed.is_equal_excluding_fragments(&creation_url))
383 {
384 p.reject_error(cx, Error::Type(c"URL does not match context".to_owned()));
385 return p;
386 }
387
388 if parsed_url
391 .as_ref()
392 .is_ok_and(|parsed| creation_url.origin() != parsed.origin())
393 {
394 p.reject_error(cx, Error::Type(c"Not same origin".to_owned()));
395 return p;
396 }
397
398 if let Ok(url) = parsed_url {
400 final_url = url;
401 }
402 }
403
404 let res =
406 self.global()
407 .resource_threads()
408 .send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
409 self.droppable.store_id,
410 final_url,
411 options.name.clone().map(|val| CookieStore::normalize(&val)),
412 ));
413 if res.is_err() {
414 error!("Failed to send cookiestore message to resource threads");
415 } else {
416 self.in_flight
417 .safe_borrow_mut(cx.no_gc())
418 .push_back(p.clone());
419 }
420
421 p
423 }
424
425 fn Set(&self, cx: &mut JSContext, name: USVString, value: USVString) -> Rc<Promise> {
427 let global = self.global();
429
430 let origin = global.origin();
432
433 let p = Promise::new(cx, &global);
435
436 if !origin.is_tuple() {
438 p.reject_error(cx, Error::Security(None));
439 return p;
440 }
441
442 let properties = CookieInit {
444 name,
445 value,
446 expires: None,
447 domain: None,
448 path: USVString(String::from("/")),
449 sameSite: CookieSameSite::Strict,
450 partitioned: false,
451 };
452 let creation_url = global.creation_url();
453 let Some(cookie) = CookieStore::set_a_cookie(&creation_url, &properties) else {
454 p.reject_error(cx, Error::Type(c"Invalid cookie".to_owned()));
456 return p;
457 };
458
459 let res = self
461 .global()
462 .resource_threads()
463 .send(CoreResourceMsg::SetCookieForUrlAsync(
464 self.droppable.store_id,
465 creation_url.clone(),
466 Serde(cookie.into_owned()),
467 NonHTTP,
468 ));
469 if res.is_err() {
470 error!("Failed to send cookiestore message to resource threads");
471 } else {
472 self.in_flight
473 .safe_borrow_mut(cx.no_gc())
474 .push_back(p.clone());
475 }
476
477 p
479 }
480
481 fn Set_(&self, cx: &mut JSContext, options: &CookieInit) -> Rc<Promise> {
483 let global = self.global();
485
486 let origin = global.origin();
488
489 let p = Promise::new(cx, &global);
491
492 if !origin.is_tuple() {
494 p.reject_error(cx, Error::Security(None));
495 return p;
496 }
497
498 let creation_url = global.creation_url();
500
501 let Some(cookie) = CookieStore::set_a_cookie(&creation_url, options) else {
504 p.reject_error(cx, Error::Type(c"Invalid cookie".to_owned()));
505 return p;
506 };
507
508 let res = self
510 .global()
511 .resource_threads()
512 .send(CoreResourceMsg::SetCookieForUrlAsync(
513 self.droppable.store_id,
514 creation_url.clone(),
515 Serde(cookie.into_owned()),
516 NonHTTP,
517 ));
518 if res.is_err() {
519 error!("Failed to send cookiestore message to resource threads");
520 } else {
521 self.in_flight
522 .safe_borrow_mut(cx.no_gc())
523 .push_back(p.clone());
524 }
525
526 p
528 }
529
530 fn Delete(&self, cx: &mut JSContext, name: USVString) -> Rc<Promise> {
532 let global = self.global();
534
535 let origin = global.origin();
537
538 let p = Promise::new(cx, &global);
540
541 if !origin.is_tuple() {
543 p.reject_error(cx, Error::Security(None));
544 return p;
545 }
546
547 let res = global
550 .resource_threads()
551 .send(CoreResourceMsg::DeleteCookieAsync(
552 self.droppable.store_id,
553 global.creation_url(),
554 name.0,
555 ));
556 if res.is_err() {
557 error!("Failed to send cookiestore message to resource threads");
558 } else {
559 self.in_flight
560 .safe_borrow_mut(cx.no_gc())
561 .push_back(p.clone());
562 }
563
564 p
566 }
567
568 fn Delete_(&self, cx: &mut JSContext, options: &CookieStoreDeleteOptions) -> Rc<Promise> {
570 let global = self.global();
572
573 let origin = global.origin();
575
576 let p = Promise::new(cx, &global);
578
579 if !origin.is_tuple() {
581 p.reject_error(cx, Error::Security(None));
582 return p;
583 }
584
585 let res = global
588 .resource_threads()
589 .send(CoreResourceMsg::DeleteCookieAsync(
590 self.droppable.store_id,
591 global.creation_url(),
592 options.name.to_string(),
593 ));
594 if res.is_err() {
595 error!("Failed to send cookiestore message to resource threads");
596 } else {
597 self.in_flight
598 .safe_borrow_mut(cx.no_gc())
599 .push_back(p.clone());
600 }
601
602 p
604 }
605}
606
607impl CookieStore {
608 fn normalize(value: &USVString) -> String {
610 value.trim_matches([' ', '\t']).into()
611 }
612
613 fn set_a_cookie<'a>(url: &'a ServoUrl, properties: &'a CookieInit) -> Option<Cookie<'a>> {
615 let name = CookieStore::normalize(&properties.name);
617 let value = CookieStore::normalize(&properties.value);
619
620 if CookieStore::contains_control_characters(&name) ||
622 CookieStore::contains_control_characters(&value)
623 {
624 return None;
625 }
626
627 if name.contains('=') {
629 return None;
630 }
631
632 if name.is_empty() {
634 if value.contains('=') || value.is_empty() {
637 return None;
638 }
639 let lowercased_value = value.to_ascii_lowercase();
641 if ["__host-", "__host-http-", "__http-", "__secure-"]
642 .iter()
643 .any(|prefix| lowercased_value.starts_with(prefix))
644 {
645 return None;
646 }
647 }
648
649 let lowercased_name = name.to_ascii_lowercase();
651 if lowercased_name.starts_with("__host-http-") || lowercased_name.starts_with("__http-") {
652 return None;
653 }
654
655 if name.len() + value.len() > 4096 {
657 return None;
658 }
659
660 let mut cookie = Cookie::build((Cow::Owned(name), Cow::Owned(value)))
661 .secure(true)
663 .partitioned(properties.partitioned)
665 .same_site(match properties.sameSite {
667 CookieSameSite::Lax => SameSite::Lax,
668 CookieSameSite::Strict => SameSite::Strict,
669 CookieSameSite::None => SameSite::None,
670 });
671
672 if let Some(domain) = &properties.domain {
674 let host = match url.host() {
676 Some(host) => host.to_owned(),
677 None => return None,
678 };
679 if domain.starts_with('.') {
681 return None;
682 }
683 if lowercased_name.starts_with("__host-") {
685 return None;
686 }
687
688 let domain = get_registrable_domain_suffix_of_or_is_equal_to(domain, host)?;
693
694 let domain = domain.to_string();
696 if domain.len() > 1024 {
698 return None;
699 }
700 cookie.inner_mut().set_domain(domain);
702 }
703
704 if let Some(expiry) = properties.expires {
706 cookie.inner_mut().set_expires(
709 OffsetDateTime::from_unix_timestamp((*expiry / 1000.0) as i64)
710 .expect("cookie expiry out of range"),
711 );
712 }
713
714 let path = if properties.path.is_empty() {
716 let mut cloned_url = url.clone();
718 {
719 let mut path_segments = cloned_url
722 .as_mut_url()
723 .path_segments_mut()
724 .expect("document creation url cannot be a base");
725 if url.path_segments().is_some_and(|ps| ps.count() > 1) {
727 path_segments.pop();
728 } else {
729 path_segments.clear();
731 }
732 }
733 cloned_url.path().to_owned()
734 } else {
735 properties.path.to_string()
736 };
737 if !path.starts_with('/') {
739 return None;
740 }
741 if path != "/" && lowercased_name.starts_with("__host-") {
743 return None;
744 }
745 if path.len() > 1024 {
747 return None;
748 }
749 cookie.inner_mut().set_path(path);
751
752 Some(cookie.build())
753 }
754
755 fn contains_control_characters(val: &str) -> bool {
757 val.contains(
759 |v| matches!(v, '\u{0000}'..='\u{0008}' | '\u{000a}'..='\u{001f}' | '\u{007f}' | ';'),
760 )
761 }
762}