1use std::borrow::Cow;
6use std::collections::VecDeque;
7use std::rc::Rc;
8
9use base::generic_channel::{GenericSend, GenericSender};
10use base::id::CookieStoreId;
11use cookie::{Cookie, SameSite};
12use dom_struct::dom_struct;
13use hyper_serde::Serde;
14use ipc_channel::ipc;
15use ipc_channel::router::ROUTER;
16use itertools::Itertools;
17use js::jsval::NullValue;
18use net_traits::CookieSource::NonHTTP;
19use net_traits::{CookieAsyncResponse, CookieData, CoreResourceMsg};
20use script_bindings::codegen::GenericBindings::CookieStoreBinding::CookieSameSite;
21use script_bindings::script_runtime::CanGc;
22use servo_url::ServoUrl;
23use time::OffsetDateTime;
24
25use crate::dom::bindings::cell::DomRefCell;
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, reflect_dom_object};
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::note());
94 } else {
95 promise.resolve_native(&NullValue(), CanGc::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::note());
107 },
108 CookieData::Delete(_) | CookieData::Change(_) | CookieData::Set(_) => {
109 promise.resolve_native(&(), CanGc::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 (cookie_sender, cookie_receiver) = ipc::channel().expect("ipc channel failure");
142
143 let context = Trusted::new(self);
144 let cs_listener = CookieListener {
145 task_source: self
146 .global()
147 .task_manager()
148 .dom_manipulation_task_source()
149 .to_sendable(),
150 context,
151 };
152
153 ROUTER.add_typed_route(
154 cookie_receiver,
155 Box::new(move |message| match message {
156 Ok(msg) => cs_listener.handle(msg),
157 Err(err) => warn!("Error receiving a CookieStore message: {:?}", err),
158 }),
159 );
160
161 let res = self
162 .global()
163 .resource_threads()
164 .send(CoreResourceMsg::NewCookieListener(
165 self.droppable.store_id,
166 cookie_sender,
167 self.global().creation_url().clone(),
168 ));
169 if res.is_err() {
170 error!("Failed to send cookiestore message to resource threads");
171 }
172 }
173}
174
175fn cookie_to_list_item(cookie: Cookie) -> CookieListItem {
177 CookieListItem {
180 name: Some(cookie.name().to_string().into()),
182
183 value: Some(cookie.value().to_string().into()),
185 }
186}
187
188impl CookieStoreMethods<crate::DomTypeHolder> for CookieStore {
189 fn Get(&self, name: USVString, can_gc: CanGc) -> Rc<Promise> {
191 let global = self.global();
193
194 let origin = global.origin();
196
197 let p = Promise::new(&global, can_gc);
199
200 if !origin.is_tuple() {
202 p.reject_error(Error::Security(None), can_gc);
203 return p;
204 }
205
206 let creation_url = global.creation_url();
208
209 let name = CookieStore::normalize(&name);
210
211 let res = self
213 .global()
214 .resource_threads()
215 .send(CoreResourceMsg::GetCookieDataForUrlAsync(
216 self.droppable.store_id,
217 creation_url.clone(),
218 Some(name),
219 ));
220 if res.is_err() {
221 error!("Failed to send cookiestore message to resource threads");
222 } else {
223 self.in_flight.borrow_mut().push_back(p.clone());
224 }
225
226 p
228 }
229
230 fn Get_(&self, options: &CookieStoreGetOptions, can_gc: CanGc) -> Rc<Promise> {
232 let global = self.global();
234
235 let origin = global.origin();
237
238 let p = Promise::new(&global, can_gc);
240
241 if !origin.is_tuple() {
243 p.reject_error(Error::Security(None), can_gc);
244 return p;
245 }
246
247 let creation_url = global.creation_url();
249
250 if options.url.is_none() && options.name.is_none() {
253 p.reject_error(Error::Type(c"Options cannot be empty".to_owned()), can_gc);
254 return p;
255 }
256
257 let mut final_url = creation_url.clone();
258
259 if let Some(get_url) = &options.url {
261 let parsed_url = ServoUrl::parse_with_base(Some(&global.api_base_url()), get_url);
263
264 if let Some(_window) = DomRoot::downcast::<Window>(self.global()) {
267 if parsed_url
268 .as_ref()
269 .is_ok_and(|parsed| !parsed.is_equal_excluding_fragments(&creation_url))
270 {
271 p.reject_error(
272 Error::Type(c"URL does not match context".to_owned()),
273 can_gc,
274 );
275 return p;
276 }
277 }
278
279 if parsed_url
282 .as_ref()
283 .is_ok_and(|parsed| creation_url.origin() != parsed.origin())
284 {
285 p.reject_error(Error::Type(c"Not same origin".to_owned()), can_gc);
286 return p;
287 }
288
289 if let Ok(url) = parsed_url {
291 final_url = url;
292 }
293 }
294
295 let res = self
297 .global()
298 .resource_threads()
299 .send(CoreResourceMsg::GetCookieDataForUrlAsync(
300 self.droppable.store_id,
301 final_url.clone(),
302 options.name.clone().map(|val| CookieStore::normalize(&val)),
303 ));
304 if res.is_err() {
305 error!("Failed to send cookiestore message to resource threads");
306 } else {
307 self.in_flight.borrow_mut().push_back(p.clone());
308 }
309
310 p
311 }
312
313 fn GetAll(&self, name: USVString, can_gc: CanGc) -> Rc<Promise> {
315 let global = self.global();
317
318 let origin = global.origin();
320
321 let p = Promise::new(&global, can_gc);
323
324 if !origin.is_tuple() {
326 p.reject_error(Error::Security(None), can_gc);
327 return p;
328 }
329 let creation_url = global.creation_url();
331
332 let name = CookieStore::normalize(&name);
334
335 let res =
337 self.global()
338 .resource_threads()
339 .send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
340 self.droppable.store_id,
341 creation_url.clone(),
342 Some(name),
343 ));
344 if res.is_err() {
345 error!("Failed to send cookiestore message to resource threads");
346 } else {
347 self.in_flight.borrow_mut().push_back(p.clone());
348 }
349
350 p
352 }
353
354 fn GetAll_(&self, options: &CookieStoreGetOptions, can_gc: CanGc) -> Rc<Promise> {
356 let global = self.global();
358
359 let origin = global.origin();
361
362 let p = Promise::new(&global, can_gc);
364
365 if !origin.is_tuple() {
367 p.reject_error(Error::Security(None), can_gc);
368 return p;
369 }
370
371 let creation_url = global.creation_url();
373
374 let mut final_url = creation_url.clone();
375
376 if let Some(get_url) = &options.url {
378 let parsed_url = ServoUrl::parse_with_base(Some(&global.api_base_url()), get_url);
380
381 if let Some(_window) = DomRoot::downcast::<Window>(self.global()) {
384 if parsed_url
385 .as_ref()
386 .is_ok_and(|parsed| !parsed.is_equal_excluding_fragments(&creation_url))
387 {
388 p.reject_error(
389 Error::Type(c"URL does not match context".to_owned()),
390 can_gc,
391 );
392 return p;
393 }
394 }
395
396 if parsed_url
399 .as_ref()
400 .is_ok_and(|parsed| creation_url.origin() != parsed.origin())
401 {
402 p.reject_error(Error::Type(c"Not same origin".to_owned()), can_gc);
403 return p;
404 }
405
406 if let Ok(url) = parsed_url {
408 final_url = url;
409 }
410 }
411
412 let res =
414 self.global()
415 .resource_threads()
416 .send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
417 self.droppable.store_id,
418 final_url.clone(),
419 options.name.clone().map(|val| CookieStore::normalize(&val)),
420 ));
421 if res.is_err() {
422 error!("Failed to send cookiestore message to resource threads");
423 } else {
424 self.in_flight.borrow_mut().push_back(p.clone());
425 }
426
427 p
429 }
430
431 fn Set(&self, name: USVString, value: USVString, can_gc: CanGc) -> Rc<Promise> {
433 let global = self.global();
435
436 let origin = global.origin();
438
439 let p = Promise::new(&global, can_gc);
441
442 if !origin.is_tuple() {
444 p.reject_error(Error::Security(None), can_gc);
445 return p;
446 }
447
448 let properties = CookieInit {
450 name,
451 value,
452 expires: None,
453 domain: None,
454 path: USVString(String::from("/")),
455 sameSite: CookieSameSite::Strict,
456 partitioned: false,
457 };
458 let creation_url = global.creation_url();
459 let Some(cookie) = CookieStore::set_a_cookie(&creation_url, &properties) else {
460 p.reject_error(Error::Type(c"Invalid cookie".to_owned()), can_gc);
462 return p;
463 };
464
465 let res = self
467 .global()
468 .resource_threads()
469 .send(CoreResourceMsg::SetCookieForUrlAsync(
470 self.droppable.store_id,
471 creation_url.clone(),
472 Serde(cookie.into_owned()),
473 NonHTTP,
474 ));
475 if res.is_err() {
476 error!("Failed to send cookiestore message to resource threads");
477 } else {
478 self.in_flight.borrow_mut().push_back(p.clone());
479 }
480
481 p
483 }
484
485 fn Set_(&self, options: &CookieInit, can_gc: CanGc) -> Rc<Promise> {
487 let global = self.global();
489
490 let origin = global.origin();
492
493 let p = Promise::new(&global, can_gc);
495
496 if !origin.is_tuple() {
498 p.reject_error(Error::Security(None), can_gc);
499 return p;
500 }
501
502 let creation_url = global.creation_url();
504
505 let Some(cookie) = CookieStore::set_a_cookie(&creation_url, options) else {
508 p.reject_error(Error::Type(c"Invalid cookie".to_owned()), can_gc);
509 return p;
510 };
511
512 let res = self
514 .global()
515 .resource_threads()
516 .send(CoreResourceMsg::SetCookieForUrlAsync(
517 self.droppable.store_id,
518 creation_url.clone(),
519 Serde(cookie.into_owned()),
520 NonHTTP,
521 ));
522 if res.is_err() {
523 error!("Failed to send cookiestore message to resource threads");
524 } else {
525 self.in_flight.borrow_mut().push_back(p.clone());
526 }
527
528 p
530 }
531
532 fn Delete(&self, name: USVString, can_gc: CanGc) -> Rc<Promise> {
534 let global = self.global();
536
537 let origin = global.origin();
539
540 let p = Promise::new(&global, can_gc);
542
543 if !origin.is_tuple() {
545 p.reject_error(Error::Security(None), can_gc);
546 return p;
547 }
548
549 let res = global
552 .resource_threads()
553 .send(CoreResourceMsg::DeleteCookieAsync(
554 self.droppable.store_id,
555 global.creation_url().clone(),
556 name.0,
557 ));
558 if res.is_err() {
559 error!("Failed to send cookiestore message to resource threads");
560 } else {
561 self.in_flight.borrow_mut().push_back(p.clone());
562 }
563
564 p
566 }
567
568 fn Delete_(&self, options: &CookieStoreDeleteOptions, can_gc: CanGc) -> Rc<Promise> {
570 let global = self.global();
572
573 let origin = global.origin();
575
576 let p = Promise::new(&global, can_gc);
578
579 if !origin.is_tuple() {
581 p.reject_error(Error::Security(None), can_gc);
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().clone(),
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.borrow_mut().push_back(p.clone());
598 }
599
600 p
602 }
603}
604
605impl CookieStore {
606 fn normalize(value: &USVString) -> String {
608 value.trim_matches([' ', '\t']).into()
609 }
610
611 fn set_a_cookie<'a>(url: &'a ServoUrl, properties: &'a CookieInit) -> Option<Cookie<'a>> {
613 let name = CookieStore::normalize(&properties.name);
615 let value = CookieStore::normalize(&properties.value);
617
618 if CookieStore::contains_control_characters(&name) ||
620 CookieStore::contains_control_characters(&value)
621 {
622 return None;
623 }
624
625 if name.contains('=') {
627 return None;
628 }
629
630 if name.is_empty() {
632 if value.contains('=') || value.is_empty() {
635 return None;
636 }
637 let lowercased_value = value.to_ascii_lowercase();
639 if ["__host-", "__host-http-", "__http-", "__secure-"]
640 .iter()
641 .any(|prefix| lowercased_value.starts_with(prefix))
642 {
643 return None;
644 }
645 }
646
647 let lowercased_name = name.to_ascii_lowercase();
649 if lowercased_name.starts_with("__host-http-") || lowercased_name.starts_with("__http-") {
650 return None;
651 }
652
653 if name.len() + value.len() > 4096 {
655 return None;
656 }
657
658 let mut cookie = Cookie::build((Cow::Owned(name), Cow::Owned(value)))
659 .secure(true)
661 .partitioned(properties.partitioned)
663 .same_site(match properties.sameSite {
665 CookieSameSite::Lax => SameSite::Lax,
666 CookieSameSite::Strict => SameSite::Strict,
667 CookieSameSite::None => SameSite::None,
668 });
669
670 if let Some(domain) = &properties.domain {
672 let host = match url.host() {
674 Some(host) => host.to_owned(),
675 None => return None,
676 };
677 if domain.starts_with('.') {
679 return None;
680 }
681 if lowercased_name.starts_with("__host-") {
683 return None;
684 }
685
686 let domain = get_registrable_domain_suffix_of_or_is_equal_to(domain, host)?;
691
692 let domain = domain.to_string();
694 if domain.len() > 1024 {
696 return None;
697 }
698 cookie.inner_mut().set_domain(domain);
700 }
701
702 if let Some(expiry) = properties.expires {
704 cookie.inner_mut().set_expires(
707 OffsetDateTime::from_unix_timestamp((*expiry / 1000.0) as i64)
708 .expect("cookie expiry out of range"),
709 );
710 }
711
712 let path = if properties.path.is_empty() {
714 let mut cloned_url = url.clone();
716 {
717 let mut path_segments = cloned_url
720 .as_mut_url()
721 .path_segments_mut()
722 .expect("document creation url cannot be a base");
723 if url.path_segments().is_some_and(|ps| ps.count() > 1) {
725 path_segments.pop();
726 } else {
727 path_segments.clear();
729 }
730 }
731 cloned_url.path().to_owned()
732 } else {
733 properties.path.to_string()
734 };
735 if !path.starts_with('/') {
737 return None;
738 }
739 if path != "/" && lowercased_name.starts_with("__host-") {
741 return None;
742 }
743 if path.len() > 1024 {
745 return None;
746 }
747 cookie.inner_mut().set_path(path);
749
750 Some(cookie.build())
751 }
752
753 fn contains_control_characters(val: &str) -> bool {
755 val.contains(
757 |v| matches!(v, '\u{0000}'..='\u{0008}' | '\u{000a}'..='\u{001f}' | '\u{007f}' | ';'),
758 )
759 }
760}