1use std::borrow::Cow;
6use std::collections::VecDeque;
7
8use cookie::{Cookie, SameSite};
9use dom_struct::dom_struct;
10use hyper_serde::Serde;
11use itertools::Itertools;
12use js::context::JSContext;
13use js::jsval::NullValue;
14use net_traits::CookieSource::NonHTTP;
15use net_traits::{CookieAsyncResponse, CookieData, CoreResourceMsg};
16use script_bindings::cell::DomRefCell;
17use script_bindings::codegen::GenericBindings::CookieStoreBinding::CookieSameSite;
18use script_bindings::reflector::reflect_dom_object_with_cx;
19use servo_base::generic_channel::{GenericCallback, GenericSend, GenericSender};
20use servo_base::id::CookieStoreId;
21use servo_url::ServoUrl;
22use style::str::starts_with_ignore_ascii_case;
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::dom::{RootedPromise, TracedPromise};
39use crate::tasks::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 in_flight: DomRefCell<VecDeque<TracedPromise>>,
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().map(|promise| promise.root(cx)) 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) -> RootedPromise {
185 let global = self.global();
187
188 let origin = global.origin();
190
191 let p = Promise::new_rooted(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.to_traced());
220 }
221
222 p
224 }
225
226 fn Get_(&self, cx: &mut JSContext, options: &CookieStoreGetOptions) -> RootedPromise {
228 let global = self.global();
230
231 let origin = global.origin();
233
234 let p = Promise::new_rooted(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.to_traced());
302 }
303
304 p
305 }
306
307 fn GetAll(&self, cx: &mut JSContext, name: USVString) -> RootedPromise {
309 let global = self.global();
311
312 let origin = global.origin();
314
315 let p = Promise::new_rooted(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.to_traced());
344 }
345
346 p
348 }
349
350 fn GetAll_(&self, cx: &mut JSContext, options: &CookieStoreGetOptions) -> RootedPromise {
352 let global = self.global();
354
355 let origin = global.origin();
357
358 let p = Promise::new_rooted(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.to_traced());
419 }
420
421 p
423 }
424
425 fn Set(&self, cx: &mut JSContext, name: USVString, value: USVString) -> RootedPromise {
427 let global = self.global();
429
430 let origin = global.origin();
432
433 let p = Promise::new_rooted(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.to_traced());
475 }
476
477 p
479 }
480
481 fn Set_(&self, cx: &mut JSContext, options: &CookieInit) -> RootedPromise {
483 let global = self.global();
485
486 let origin = global.origin();
488
489 let p = Promise::new_rooted(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.to_traced());
524 }
525
526 p
528 }
529
530 fn Delete(&self, cx: &mut JSContext, name: USVString) -> RootedPromise {
532 let global = self.global();
534
535 let origin = global.origin();
537
538 let p = Promise::new_rooted(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.to_traced());
562 }
563
564 p
566 }
567
568 fn Delete_(&self, cx: &mut JSContext, options: &CookieStoreDeleteOptions) -> RootedPromise {
570 let global = self.global();
572
573 let origin = global.origin();
575
576 let p = Promise::new_rooted(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.to_traced());
600 }
601
602 p
604 }
605}
606
607impl CookieStore {
608 pub(crate) 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 if ["__host-", "__host-http-", "__http-", "__secure-"]
641 .into_iter()
642 .any(|prefix| starts_with_ignore_ascii_case(&value, prefix))
643 {
644 return None;
645 }
646 }
647
648 if ["__host-http-", "__http-"]
650 .into_iter()
651 .any(|prefix| starts_with_ignore_ascii_case(&name, prefix))
652 {
653 return None;
654 }
655 let name_starts_with_host = starts_with_ignore_ascii_case(&name, "__host-");
656
657 if name.len() + value.len() > 4096 {
659 return None;
660 }
661
662 let mut cookie = Cookie::build((Cow::Owned(name), Cow::Owned(value)))
663 .secure(true)
665 .partitioned(properties.partitioned)
667 .same_site(match properties.sameSite {
669 CookieSameSite::Lax => SameSite::Lax,
670 CookieSameSite::Strict => SameSite::Strict,
671 CookieSameSite::None => SameSite::None,
672 });
673
674 if let Some(domain) = &properties.domain {
676 let host = url.host()?.to_owned();
678 if domain.starts_with('.') {
680 return None;
681 }
682 if name_starts_with_host {
684 return None;
685 }
686
687 let domain = get_registrable_domain_suffix_of_or_is_equal_to(domain, host)?;
692
693 let domain = domain.to_string();
695 if domain.len() > 1024 {
697 return None;
698 }
699 cookie.inner_mut().set_domain(domain);
701 }
702
703 if let Some(expiry) = properties.expires {
705 cookie.inner_mut().set_expires(
708 OffsetDateTime::from_unix_timestamp((*expiry / 1000.0) as i64)
709 .expect("cookie expiry out of range"),
710 );
711 }
712
713 let path = if properties.path.is_empty() {
715 let mut cloned_url = url.clone();
717 {
718 let mut path_segments = cloned_url
721 .as_mut_url()
722 .path_segments_mut()
723 .expect("document creation url cannot be a base");
724 if url.path_segments().is_some_and(|ps| ps.count() > 1) {
726 path_segments.pop();
727 } else {
728 path_segments.clear();
730 }
731 }
732 cloned_url.path().to_owned()
733 } else {
734 properties.path.to_string()
735 };
736 if !path.starts_with('/') {
738 return None;
739 }
740 if path != "/" && name_starts_with_host {
742 return None;
743 }
744 if path.len() > 1024 {
746 return None;
747 }
748 cookie.inner_mut().set_path(path);
750
751 Some(cookie.build())
752 }
753
754 fn contains_control_characters(val: &str) -> bool {
756 val.contains(
758 |v| matches!(v, '\u{0000}'..='\u{0008}' | '\u{000a}'..='\u{001f}' | '\u{007f}' | ';'),
759 )
760 }
761}