use std::collections::HashMap;
use cookie::{
Cookie as RawCookie,
time::{Duration, OffsetDateTime},
};
use http::Uri;
use url::Host;
use crate::ext::UriExt;
pub const DEFAULT_PATH: &str = "/";
type CanonicalHost = Host<Box<str>>;
type NameMap = HashMap<Box<str>, CookieEntry>;
type PathMap = HashMap<Box<str>, CookieScopeMap>;
type DomainMap = HashMap<CanonicalHost, PathMap>;
#[derive(Debug)]
pub struct CookieEntry {
pub cookie: RawCookie<'static>,
pub creation_index: u64,
}
#[derive(Debug, Default)]
pub struct CookieScopeMap {
host_only: NameMap,
domain: NameMap,
}
impl CookieScopeMap {
pub fn get_mut(&mut self, name: &str, host_only: bool) -> Option<&mut CookieEntry> {
if host_only {
self.host_only.get_mut(name)
} else {
self.domain.get_mut(name)
}
}
pub fn entries(&self, name: &str) -> impl Iterator<Item = &CookieEntry> {
self.host_only
.get(name)
.into_iter()
.chain(self.domain.get(name))
}
pub fn insert(&mut self, name: Box<str>, host_only: bool, entry: CookieEntry) {
if host_only {
self.host_only.insert(name, entry);
} else {
self.domain.insert(name, entry);
}
}
pub fn remove(&mut self, name: &str, host_only: bool) {
if host_only {
self.host_only.remove(name);
} else {
self.domain.remove(name);
}
}
pub fn remove_all(&mut self, name: &str) {
self.host_only.remove(name);
self.domain.remove(name);
}
pub fn values(&self) -> impl Iterator<Item = &CookieEntry> {
self.host_only.values().chain(self.domain.values())
}
pub fn is_empty(&self) -> bool {
self.host_only.is_empty() && self.domain.is_empty()
}
}
#[derive(Debug, Default)]
pub struct Store {
pub cookies: DomainMap,
next_creation_index: u64,
}
impl Store {
pub fn insert_stored_cookie(
&mut self,
domain: CanonicalHost,
path: String,
cookie: RawCookie<'static>,
) {
let host_only = cookie.domain().is_none();
if let Some(entry) = self
.cookies
.get_mut(&domain)
.and_then(|path_map| path_map.get_mut(path.as_str()))
.and_then(|cookie_map| cookie_map.get_mut(cookie.name(), host_only))
{
if entry.cookie.value() != cookie.value() {
entry.creation_index = self.next_creation_index;
self.next_creation_index = self.next_creation_index.saturating_add(1);
}
entry.cookie = cookie;
return;
}
let creation_index = self.next_creation_index;
self.next_creation_index = self.next_creation_index.saturating_add(1);
let name = Box::from(cookie.name());
self.cookies
.entry(domain)
.or_default()
.entry(path.into_boxed_str())
.or_default()
.insert(
name,
host_only,
CookieEntry {
cookie,
creation_index,
},
);
}
pub fn remove_stored_cookie(
&mut self,
domain: &CanonicalHost,
path: &str,
name: &str,
host_only: bool,
) {
self.remove_stored_cookie_inner(domain, path, name, Some(host_only));
}
pub fn remove_stored_cookies(&mut self, domain: &CanonicalHost, path: &str, name: &str) {
self.remove_stored_cookie_inner(domain, path, name, None);
}
fn remove_stored_cookie_inner(
&mut self,
domain: &CanonicalHost,
path: &str,
name: &str,
host_only: Option<bool>,
) {
let remove_domain = if let Some(path_map) = self.cookies.get_mut(domain) {
let remove_path = if let Some(cookie_map) = path_map.get_mut(path) {
if let Some(host_only) = host_only {
cookie_map.remove(name, host_only);
} else {
cookie_map.remove_all(name);
}
cookie_map.is_empty()
} else {
false
};
if remove_path {
path_map.remove(path);
}
path_map.is_empty()
} else {
false
};
if remove_domain {
self.cookies.remove(domain);
}
}
pub fn matching_cookies<'a>(
&'a self,
uri: &'a Uri,
request_host: &'a CanonicalHost,
now: OffsetDateTime,
) -> impl Iterator<Item = (&'a CanonicalHost, &'a str, &'a CookieEntry)> + 'a {
self.cookies.iter().flat_map(move |(domain, path_map)| {
path_map.iter().flat_map(move |(path, cookie_map)| {
cookie_map.values().filter_map(move |entry| {
request_matches_cookie(uri, request_host, domain, path, &entry.cookie, now)
.then_some((domain, path.as_ref(), entry))
})
})
})
}
pub fn would_overlay_secure_cookie(
&self,
name: &str,
domain: &CanonicalHost,
path: &str,
now: OffsetDateTime,
) -> bool {
self.cookies.iter().any(|(stored_domain, path_map)| {
(domain_match(stored_domain, domain) || domain_match(domain, stored_domain))
&& path_map.iter().any(|(stored_path, cookie_map)| {
path_match(path, stored_path)
&& cookie_map.entries(name).any(|entry| {
entry.cookie.secure() == Some(true)
&& !cookie_is_expired(&entry.cookie, now)
})
})
})
}
}
fn request_matches_cookie(
uri: &Uri,
request_host: &CanonicalHost,
cookie_domain: &CanonicalHost,
cookie_path: &str,
cookie: &RawCookie<'_>,
now: OffsetDateTime,
) -> bool {
if !(uri.is_http() || uri.is_https())
|| !domain_match(request_host, cookie_domain)
|| cookie.domain().is_none() && request_host != cookie_domain
|| !path_match(uri.path(), cookie_path)
|| cookie.secure() == Some(true) && uri.is_http()
{
return false;
}
!cookie_is_expired(cookie, now)
}
pub fn cookie_is_expired(cookie: &RawCookie<'_>, now: OffsetDateTime) -> bool {
cookie
.max_age()
.is_some_and(|max_age| max_age <= Duration::ZERO)
|| cookie
.expires_datetime()
.is_some_and(|deadline| deadline <= now)
}
pub fn domain_match(host: &CanonicalHost, domain: &CanonicalHost) -> bool {
if host == domain {
return true;
}
let (Host::Domain(host), Host::Domain(domain)) = (host, domain) else {
return false;
};
host.len() > domain.len()
&& host.as_bytes()[host.len() - domain.len() - 1] == b'.'
&& host.ends_with(domain.as_ref())
}
fn path_match(request_path: &str, cookie_path: &str) -> bool {
request_path == cookie_path
|| request_path.starts_with(cookie_path)
&& (cookie_path.ends_with(DEFAULT_PATH)
|| request_path[cookie_path.len()..].starts_with(DEFAULT_PATH))
}
pub fn canonical_host(host: &str) -> Option<CanonicalHost> {
let host = host.strip_prefix('.').unwrap_or(host);
match Host::parse(host).ok()? {
Host::Domain(domain) => Some(Host::Domain(domain.into_boxed_str())),
Host::Ipv4(address) => Some(Host::Ipv4(address)),
Host::Ipv6(address) => Some(Host::Ipv6(address)),
}
}
pub fn normalize_path(path: &str) -> &str {
if !path.starts_with(DEFAULT_PATH) {
return DEFAULT_PATH;
}
if let Some(pos) = path.rfind(DEFAULT_PATH) {
if pos == 0 {
return DEFAULT_PATH;
}
return &path[..pos];
}
DEFAULT_PATH
}