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::new2(cx, &global);
195
196 if !origin.is_tuple() {
198 p.reject_error_with_cx(cx, Error::Security(None));
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::new2(cx, &global);
236
237 if !origin.is_tuple() {
239 p.reject_error_with_cx(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_with_cx(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_with_cx(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_with_cx(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.borrow_mut().push_back(p.clone());
300 }
301
302 p
303 }
304
305 fn GetAll(&self, cx: &mut JSContext, name: USVString) -> Rc<Promise> {
307 let global = self.global();
309
310 let origin = global.origin();
312
313 let p = Promise::new2(cx, &global);
315
316 if !origin.is_tuple() {
318 p.reject_error_with_cx(cx, Error::Security(None));
319 return p;
320 }
321 let creation_url = global.creation_url();
323
324 let name = CookieStore::normalize(&name);
326
327 let res =
329 self.global()
330 .resource_threads()
331 .send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
332 self.droppable.store_id,
333 creation_url,
334 Some(name),
335 ));
336 if res.is_err() {
337 error!("Failed to send cookiestore message to resource threads");
338 } else {
339 self.in_flight.borrow_mut().push_back(p.clone());
340 }
341
342 p
344 }
345
346 fn GetAll_(&self, cx: &mut JSContext, options: &CookieStoreGetOptions) -> Rc<Promise> {
348 let global = self.global();
350
351 let origin = global.origin();
353
354 let p = Promise::new2(cx, &global);
356
357 if !origin.is_tuple() {
359 p.reject_error_with_cx(cx, Error::Security(None));
360 return p;
361 }
362
363 let creation_url = global.creation_url();
365
366 let mut final_url = creation_url.clone();
367
368 if let Some(get_url) = &options.url {
370 let parsed_url = ServoUrl::parse_with_base(Some(&global.api_base_url()), get_url);
372
373 if let Some(_window) = DomRoot::downcast::<Window>(self.global()) &&
376 parsed_url
377 .as_ref()
378 .is_ok_and(|parsed| !parsed.is_equal_excluding_fragments(&creation_url))
379 {
380 p.reject_error_with_cx(cx, Error::Type(c"URL does not match context".to_owned()));
381 return p;
382 }
383
384 if parsed_url
387 .as_ref()
388 .is_ok_and(|parsed| creation_url.origin() != parsed.origin())
389 {
390 p.reject_error_with_cx(cx, Error::Type(c"Not same origin".to_owned()));
391 return p;
392 }
393
394 if let Ok(url) = parsed_url {
396 final_url = url;
397 }
398 }
399
400 let res =
402 self.global()
403 .resource_threads()
404 .send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
405 self.droppable.store_id,
406 final_url,
407 options.name.clone().map(|val| CookieStore::normalize(&val)),
408 ));
409 if res.is_err() {
410 error!("Failed to send cookiestore message to resource threads");
411 } else {
412 self.in_flight.borrow_mut().push_back(p.clone());
413 }
414
415 p
417 }
418
419 fn Set(&self, cx: &mut JSContext, name: USVString, value: USVString) -> Rc<Promise> {
421 let global = self.global();
423
424 let origin = global.origin();
426
427 let p = Promise::new2(cx, &global);
429
430 if !origin.is_tuple() {
432 p.reject_error_with_cx(cx, Error::Security(None));
433 return p;
434 }
435
436 let properties = CookieInit {
438 name,
439 value,
440 expires: None,
441 domain: None,
442 path: USVString(String::from("/")),
443 sameSite: CookieSameSite::Strict,
444 partitioned: false,
445 };
446 let creation_url = global.creation_url();
447 let Some(cookie) = CookieStore::set_a_cookie(&creation_url, &properties) else {
448 p.reject_error_with_cx(cx, Error::Type(c"Invalid cookie".to_owned()));
450 return p;
451 };
452
453 let res = self
455 .global()
456 .resource_threads()
457 .send(CoreResourceMsg::SetCookieForUrlAsync(
458 self.droppable.store_id,
459 creation_url.clone(),
460 Serde(cookie.into_owned()),
461 NonHTTP,
462 ));
463 if res.is_err() {
464 error!("Failed to send cookiestore message to resource threads");
465 } else {
466 self.in_flight.borrow_mut().push_back(p.clone());
467 }
468
469 p
471 }
472
473 fn Set_(&self, cx: &mut JSContext, options: &CookieInit) -> Rc<Promise> {
475 let global = self.global();
477
478 let origin = global.origin();
480
481 let p = Promise::new2(cx, &global);
483
484 if !origin.is_tuple() {
486 p.reject_error_with_cx(cx, Error::Security(None));
487 return p;
488 }
489
490 let creation_url = global.creation_url();
492
493 let Some(cookie) = CookieStore::set_a_cookie(&creation_url, options) else {
496 p.reject_error_with_cx(cx, Error::Type(c"Invalid cookie".to_owned()));
497 return p;
498 };
499
500 let res = self
502 .global()
503 .resource_threads()
504 .send(CoreResourceMsg::SetCookieForUrlAsync(
505 self.droppable.store_id,
506 creation_url.clone(),
507 Serde(cookie.into_owned()),
508 NonHTTP,
509 ));
510 if res.is_err() {
511 error!("Failed to send cookiestore message to resource threads");
512 } else {
513 self.in_flight.borrow_mut().push_back(p.clone());
514 }
515
516 p
518 }
519
520 fn Delete(&self, cx: &mut JSContext, name: USVString) -> Rc<Promise> {
522 let global = self.global();
524
525 let origin = global.origin();
527
528 let p = Promise::new2(cx, &global);
530
531 if !origin.is_tuple() {
533 p.reject_error_with_cx(cx, Error::Security(None));
534 return p;
535 }
536
537 let res = global
540 .resource_threads()
541 .send(CoreResourceMsg::DeleteCookieAsync(
542 self.droppable.store_id,
543 global.creation_url(),
544 name.0,
545 ));
546 if res.is_err() {
547 error!("Failed to send cookiestore message to resource threads");
548 } else {
549 self.in_flight.borrow_mut().push_back(p.clone());
550 }
551
552 p
554 }
555
556 fn Delete_(&self, cx: &mut JSContext, options: &CookieStoreDeleteOptions) -> Rc<Promise> {
558 let global = self.global();
560
561 let origin = global.origin();
563
564 let p = Promise::new2(cx, &global);
566
567 if !origin.is_tuple() {
569 p.reject_error_with_cx(cx, Error::Security(None));
570 return p;
571 }
572
573 let res = global
576 .resource_threads()
577 .send(CoreResourceMsg::DeleteCookieAsync(
578 self.droppable.store_id,
579 global.creation_url(),
580 options.name.to_string(),
581 ));
582 if res.is_err() {
583 error!("Failed to send cookiestore message to resource threads");
584 } else {
585 self.in_flight.borrow_mut().push_back(p.clone());
586 }
587
588 p
590 }
591}
592
593impl CookieStore {
594 fn normalize(value: &USVString) -> String {
596 value.trim_matches([' ', '\t']).into()
597 }
598
599 fn set_a_cookie<'a>(url: &'a ServoUrl, properties: &'a CookieInit) -> Option<Cookie<'a>> {
601 let name = CookieStore::normalize(&properties.name);
603 let value = CookieStore::normalize(&properties.value);
605
606 if CookieStore::contains_control_characters(&name) ||
608 CookieStore::contains_control_characters(&value)
609 {
610 return None;
611 }
612
613 if name.contains('=') {
615 return None;
616 }
617
618 if name.is_empty() {
620 if value.contains('=') || value.is_empty() {
623 return None;
624 }
625 let lowercased_value = value.to_ascii_lowercase();
627 if ["__host-", "__host-http-", "__http-", "__secure-"]
628 .iter()
629 .any(|prefix| lowercased_value.starts_with(prefix))
630 {
631 return None;
632 }
633 }
634
635 let lowercased_name = name.to_ascii_lowercase();
637 if lowercased_name.starts_with("__host-http-") || lowercased_name.starts_with("__http-") {
638 return None;
639 }
640
641 if name.len() + value.len() > 4096 {
643 return None;
644 }
645
646 let mut cookie = Cookie::build((Cow::Owned(name), Cow::Owned(value)))
647 .secure(true)
649 .partitioned(properties.partitioned)
651 .same_site(match properties.sameSite {
653 CookieSameSite::Lax => SameSite::Lax,
654 CookieSameSite::Strict => SameSite::Strict,
655 CookieSameSite::None => SameSite::None,
656 });
657
658 if let Some(domain) = &properties.domain {
660 let host = match url.host() {
662 Some(host) => host.to_owned(),
663 None => return None,
664 };
665 if domain.starts_with('.') {
667 return None;
668 }
669 if lowercased_name.starts_with("__host-") {
671 return None;
672 }
673
674 let domain = get_registrable_domain_suffix_of_or_is_equal_to(domain, host)?;
679
680 let domain = domain.to_string();
682 if domain.len() > 1024 {
684 return None;
685 }
686 cookie.inner_mut().set_domain(domain);
688 }
689
690 if let Some(expiry) = properties.expires {
692 cookie.inner_mut().set_expires(
695 OffsetDateTime::from_unix_timestamp((*expiry / 1000.0) as i64)
696 .expect("cookie expiry out of range"),
697 );
698 }
699
700 let path = if properties.path.is_empty() {
702 let mut cloned_url = url.clone();
704 {
705 let mut path_segments = cloned_url
708 .as_mut_url()
709 .path_segments_mut()
710 .expect("document creation url cannot be a base");
711 if url.path_segments().is_some_and(|ps| ps.count() > 1) {
713 path_segments.pop();
714 } else {
715 path_segments.clear();
717 }
718 }
719 cloned_url.path().to_owned()
720 } else {
721 properties.path.to_string()
722 };
723 if !path.starts_with('/') {
725 return None;
726 }
727 if path != "/" && lowercased_name.starts_with("__host-") {
729 return None;
730 }
731 if path.len() > 1024 {
733 return None;
734 }
735 cookie.inner_mut().set_path(path);
737
738 Some(cookie.build())
739 }
740
741 fn contains_control_characters(val: &str) -> bool {
743 val.contains(
745 |v| matches!(v, '\u{0000}'..='\u{0008}' | '\u{000a}'..='\u{001f}' | '\u{007f}' | ';'),
746 )
747 }
748}