/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::borrow::Cow;
use std::collections::VecDeque;
use std::rc::Rc;
use base::IpcSend;
use base::id::CookieStoreId;
use cookie::{Cookie, SameSite};
use dom_struct::dom_struct;
use hyper_serde::Serde;
use ipc_channel::ipc;
use ipc_channel::ipc::IpcSender;
use ipc_channel::router::ROUTER;
use itertools::Itertools;
use js::jsval::NullValue;
use net_traits::CookieSource::NonHTTP;
use net_traits::{CookieAsyncResponse, CookieData, CoreResourceMsg};
use script_bindings::script_runtime::CanGc;
use servo_url::ServoUrl;
use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::CookieStoreBinding::{
CookieInit, CookieListItem, CookieStoreDeleteOptions, CookieStoreGetOptions, CookieStoreMethods,
};
use crate::dom::bindings::error::Error;
use crate::dom::bindings::refcounted::Trusted;
use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object};
use crate::dom::bindings::root::DomRoot;
use crate::dom::bindings::str::USVString;
use crate::dom::eventtarget::EventTarget;
use crate::dom::globalscope::GlobalScope;
use crate::dom::promise::Promise;
use crate::dom::window::Window;
use crate::task_source::SendableTaskSource;
///
/// CookieStore provides an async API for pages and service workers to access and modify cookies.
/// This requires setting up communication with resource thread's cookie storage that allows for
/// the page to have multiple cookie storage promises in flight at the same time.
#[dom_struct]
pub(crate) struct CookieStore {
eventtarget: EventTarget,
#[ignore_malloc_size_of = "Rc"]
in_flight: DomRefCell>>,
// Store an id so that we can send it with requests and the resource thread knows who to respond to
#[no_trace]
store_id: CookieStoreId,
#[ignore_malloc_size_of = "Channels are hard"]
#[no_trace]
unregister_channel: IpcSender,
}
struct CookieListener {
// TODO:(whatwg/cookiestore#239) The spec is missing details for what task source to use
task_source: SendableTaskSource,
context: Trusted,
}
impl CookieListener {
pub(crate) fn handle(&self, message: CookieAsyncResponse) {
let context = self.context.clone();
self.task_source.queue(task!(cookie_message: move || {
let Some(promise) = context.root().in_flight.borrow_mut().pop_front() else {
warn!("No promise exists for cookie store response");
return;
};
match message.data {
CookieData::Get(cookie) => {
// If list is failure, then reject p with a TypeError and abort these steps.
// (There is currently no way for list to result in failure)
if let Some(cookie) = cookie {
// Otherwise, resolve p with the first item of list.
promise.resolve_native(&cookie_to_list_item(cookie.into_inner()), CanGc::note());
} else {
// If list is empty, then resolve p with null.
promise.resolve_native(&NullValue(), CanGc::note());
}
},
CookieData::GetAll(cookies) => {
// If list is failure, then reject p with a TypeError and abort these steps.
promise.resolve_native(
&cookies
.into_iter()
.map(|cookie| cookie_to_list_item(cookie.0))
.collect_vec(),
CanGc::note());
},
CookieData::Delete(_) | CookieData::Change(_) | CookieData::Set(_) => {
promise.resolve_native(&(), CanGc::note());
}
}
}));
}
}
impl CookieStore {
fn new_inherited(unregister_channel: IpcSender) -> CookieStore {
CookieStore {
eventtarget: EventTarget::new_inherited(),
in_flight: Default::default(),
store_id: CookieStoreId::new(),
unregister_channel,
}
}
pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot {
let store = reflect_dom_object(
Box::new(CookieStore::new_inherited(
global.resource_threads().core_thread.clone(),
)),
global,
can_gc,
);
store.setup_route();
store
}
fn setup_route(&self) {
let (cookie_sender, cookie_receiver) = ipc::channel().expect("ipc channel failure");
let context = Trusted::new(self);
let cs_listener = CookieListener {
task_source: self
.global()
.task_manager()
.dom_manipulation_task_source()
.to_sendable(),
context,
};
ROUTER.add_typed_route(
cookie_receiver,
Box::new(move |message| match message {
Ok(msg) => cs_listener.handle(msg),
Err(err) => warn!("Error receiving a CookieStore message: {:?}", err),
}),
);
let res = self
.global()
.resource_threads()
.send(CoreResourceMsg::NewCookieListener(
self.store_id,
cookie_sender,
self.global().creation_url().clone(),
));
if res.is_err() {
error!("Failed to send cookiestore message to resource threads");
}
}
}
///
fn cookie_to_list_item(cookie: Cookie) -> CookieListItem {
// TODO: Investigate if we need to explicitly UTF-8 decode without BOM here or if thats
// already being done by cookie-rs or implicitly by using rust strings
CookieListItem {
// Let name be the result of running UTF-8 decode without BOM on cookie’s name.
name: Some(cookie.name().to_string().into()),
// Let value be the result of running UTF-8 decode without BOM on cookie’s value.
value: Some(cookie.value().to_string().into()),
}
}
impl CookieStoreMethods for CookieStore {
///
fn Get(&self, name: USVString, can_gc: CanGc) -> Rc {
// 1. Let settings be this’s relevant settings object.
let global = self.global();
// 2. Let origin be settings’s origin.
let origin = global.origin();
// 5. Let p be a new promise.
let p = Promise::new(&global, can_gc);
// 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
if !origin.is_tuple() {
p.reject_error(Error::Security, can_gc);
return p;
}
// 4. Let url be settings’s creation URL.
let creation_url = global.creation_url();
// 6. Run the following steps in parallel:
let res = self
.global()
.resource_threads()
.send(CoreResourceMsg::GetCookieDataForUrlAsync(
self.store_id,
creation_url.clone(),
Some(name.to_string()),
));
if res.is_err() {
error!("Failed to send cookiestore message to resource threads");
} else {
self.in_flight.borrow_mut().push_back(p.clone());
}
// 7. Return p.
p
}
///
fn Get_(&self, options: &CookieStoreGetOptions, can_gc: CanGc) -> Rc {
// 1. Let settings be this’s relevant settings object.
let global = self.global();
// 2. Let origin be settings’s origin.
let origin = global.origin();
// 7. Let p be a new promise.
let p = Promise::new(&global, can_gc);
// 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
if !origin.is_tuple() {
p.reject_error(Error::Security, can_gc);
return p;
}
// 4. Let url be settings’s creation URL.
let creation_url = global.creation_url();
// 5. If options is empty, then return a promise rejected with a TypeError.
// "is empty" is not strictly defined anywhere in the spec but the only value we require here is "url"
if options.url.is_none() && options.name.is_none() {
p.reject_error(Error::Type("Options cannot be empty".to_string()), can_gc);
return p;
}
let mut final_url = creation_url.clone();
// 6. If options["url"] is present, then run these steps:
if let Some(get_url) = &options.url {
// 6.1. Let parsed be the result of parsing options["url"] with settings’s API base URL.
let parsed_url = ServoUrl::parse_with_base(Some(&global.api_base_url()), get_url);
// 6.2. If this’s relevant global object is a Window object and parsed does not equal url with exclude fragments set to true,
// then return a promise rejected with a TypeError.
if let Some(_window) = DomRoot::downcast::(self.global()) {
if parsed_url
.as_ref()
.is_ok_and(|parsed| !parsed.is_equal_excluding_fragments(creation_url))
{
p.reject_error(
Error::Type("URL does not match context".to_string()),
can_gc,
);
return p;
}
}
// 6.3. If parsed’s origin and url’s origin are not the same origin,
// then return a promise rejected with a TypeError.
if parsed_url
.as_ref()
.is_ok_and(|parsed| creation_url.origin() != parsed.origin())
{
p.reject_error(Error::Type("Not same origin".to_string()), can_gc);
return p;
}
// 6.4. Set url to parsed.
if let Ok(url) = parsed_url {
final_url = url;
}
}
// 6. Run the following steps in parallel:
let res = self
.global()
.resource_threads()
.send(CoreResourceMsg::GetCookieDataForUrlAsync(
self.store_id,
final_url.clone(),
options.name.clone().map(|val| val.0),
));
if res.is_err() {
error!("Failed to send cookiestore message to resource threads");
} else {
self.in_flight.borrow_mut().push_back(p.clone());
}
p
}
///
fn GetAll(&self, name: USVString, can_gc: CanGc) -> Rc {
// 1. Let settings be this’s relevant settings object.
let global = self.global();
// 2. Let origin be settings’s origin.
let origin = global.origin();
// 5. Let p be a new promise.
let p = Promise::new(&global, can_gc);
// 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
if !origin.is_tuple() {
p.reject_error(Error::Security, can_gc);
return p;
}
// 4. Let url be settings’s creation URL.
let creation_url = global.creation_url();
// 6. Run the following steps in parallel:
let res =
self.global()
.resource_threads()
.send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
self.store_id,
creation_url.clone(),
Some(name.to_string()),
));
if res.is_err() {
error!("Failed to send cookiestore message to resource threads");
} else {
self.in_flight.borrow_mut().push_back(p.clone());
}
// 7. Return p.
p
}
///
fn GetAll_(&self, options: &CookieStoreGetOptions, can_gc: CanGc) -> Rc {
// 1. Let settings be this’s relevant settings object.
let global = self.global();
// 2. Let origin be settings’s origin.
let origin = global.origin();
// 6. Let p be a new promise.
let p = Promise::new(&global, can_gc);
// 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
if !origin.is_tuple() {
p.reject_error(Error::Security, can_gc);
return p;
}
// 4. Let url be settings’s creation URL.
let creation_url = global.creation_url();
let mut final_url = creation_url.clone();
// 5. If options["url"] is present, then run these steps:
if let Some(get_url) = &options.url {
// 5.1. Let parsed be the result of parsing options["url"] with settings’s API base URL.
let parsed_url = ServoUrl::parse_with_base(Some(&global.api_base_url()), get_url);
// If this’s relevant global object is a Window object and parsed does not equal url with exclude fragments set to true,
// then return a promise rejected with a TypeError.
if let Some(_window) = DomRoot::downcast::(self.global()) {
if parsed_url
.as_ref()
.is_ok_and(|parsed| !parsed.is_equal_excluding_fragments(creation_url))
{
p.reject_error(
Error::Type("URL does not match context".to_string()),
can_gc,
);
return p;
}
}
// 5.3. If parsed’s origin and url’s origin are not the same origin,
// then return a promise rejected with a TypeError.
if parsed_url
.as_ref()
.is_ok_and(|parsed| creation_url.origin() != parsed.origin())
{
p.reject_error(Error::Type("Not same origin".to_string()), can_gc);
return p;
}
// 5.4. Set url to parsed.
if let Ok(url) = parsed_url {
final_url = url;
}
}
// 7. Run the following steps in parallel:
let res =
self.global()
.resource_threads()
.send(CoreResourceMsg::GetAllCookieDataForUrlAsync(
self.store_id,
final_url.clone(),
options.name.clone().map(|val| val.0),
));
if res.is_err() {
error!("Failed to send cookiestore message to resource threads");
} else {
self.in_flight.borrow_mut().push_back(p.clone());
}
// 8. Return p
p
}
///
fn Set(&self, name: USVString, value: USVString, can_gc: CanGc) -> Rc {
// 1. Let settings be this’s relevant settings object.
let global = self.global();
// 2. Let origin be settings’s origin.
let origin = global.origin();
// 9. Let p be a new promise.
let p = Promise::new(&global, can_gc);
// 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
if !origin.is_tuple() {
p.reject_error(Error::Security, can_gc);
return p;
}
// 4. Let url be settings’s creation URL.
// 5. Let domain be null.
// 6. Let path be "/".
// 7. Let sameSite be strict.
// 8. Let partitioned be false.
let cookie = Cookie::build((Cow::Owned(name.to_string()), Cow::Owned(value.to_string())))
.path("/")
.secure(true)
.same_site(SameSite::Strict)
.partitioned(false);
// TODO: This currently doesn't implement all the "set a cookie" steps which involves
// additional processing of the name and value
// 10. Run the following steps in parallel:
let res = self
.global()
.resource_threads()
.send(CoreResourceMsg::SetCookieForUrlAsync(
self.store_id,
self.global().creation_url().clone(),
Serde(cookie.build()),
NonHTTP,
));
if res.is_err() {
error!("Failed to send cookiestore message to resource threads");
} else {
self.in_flight.borrow_mut().push_back(p.clone());
}
// 11. Return p.
p
}
///
fn Set_(&self, options: &CookieInit, can_gc: CanGc) -> Rc {
// 1. Let settings be this’s relevant settings object.
let global = self.global();
// 2. Let origin be settings’s origin.
let origin = global.origin();
// 5. Let p be a new promise.
let p = Promise::new(&global, can_gc);
// 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
if !origin.is_tuple() {
p.reject_error(Error::Security, can_gc);
return p;
}
// 4. Let url be settings’s creation URL.
let creation_url = global.creation_url();
// 6.1. Let r be the result of running set a cookie with url, options["name"], options["value"],
// options["expires"], options["domain"], options["path"], options["sameSite"], and options["partitioned"].
let cookie = Cookie::build((
Cow::Owned(options.name.to_string()),
Cow::Owned(options.value.to_string()),
));
// TODO: This currently doesn't implement all the "set a cookie" steps which involves
// additional processing of the name and value
// 6. Run the following steps in parallel:
let res = self
.global()
.resource_threads()
.send(CoreResourceMsg::SetCookieForUrlAsync(
self.store_id,
creation_url.clone(),
Serde(cookie.build()),
NonHTTP,
));
if res.is_err() {
error!("Failed to send cookiestore message to resource threads");
} else {
self.in_flight.borrow_mut().push_back(p.clone());
}
// 7. Return p
p
}
///
fn Delete(&self, name: USVString, can_gc: CanGc) -> Rc {
// 1. Let settings be this’s relevant settings object.
let global = self.global();
// 2. Let origin be settings’s origin.
let origin = global.origin();
// 5. Let p be a new promise.
let p = Promise::new(&global, can_gc);
// 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
if !origin.is_tuple() {
p.reject_error(Error::Security, can_gc);
return p;
}
// 6. Run the following steps in parallel:
// TODO: the spec passes additional parameters to _delete a cookie_ that we don't handle yet
let res = global
.resource_threads()
.send(CoreResourceMsg::DeleteCookieAsync(
self.store_id,
global.creation_url().clone(),
name.0,
));
if res.is_err() {
error!("Failed to send cookiestore message to resource threads");
} else {
self.in_flight.borrow_mut().push_back(p.clone());
}
// 7. Return p.
p
}
///
fn Delete_(&self, options: &CookieStoreDeleteOptions, can_gc: CanGc) -> Rc {
// 1. Let settings be this’s relevant settings object.
let global = self.global();
// 2. Let origin be settings’s origin.
let origin = global.origin();
// 5. Let p be a new promise.
let p = Promise::new(&global, can_gc);
// 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
if !origin.is_tuple() {
p.reject_error(Error::Security, can_gc);
return p;
}
// 6. Run the following steps in parallel:
// TODO: the spec passes additional parameters to _delete a cookie_ that we don't handle yet
let res = global
.resource_threads()
.send(CoreResourceMsg::DeleteCookieAsync(
self.store_id,
global.creation_url().clone(),
options.name.to_string(),
));
if res.is_err() {
error!("Failed to send cookiestore message to resource threads");
} else {
self.in_flight.borrow_mut().push_back(p.clone());
}
// 7. Return p.
p
}
}
impl Drop for CookieStore {
fn drop(&mut self) {
let res = self
.unregister_channel
.send(CoreResourceMsg::RemoveCookieListener(self.store_id));
if res.is_err() {
error!("Failed to send cookiestore message to resource threads");
}
}
}