1use std::time::{Duration, SystemTime, UNIX_EPOCH};
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4
5use crate::util::epoch_plus;
6
7const MAX_LIFETIME: Duration = Duration::from_secs(400 * 24 * 60 * 60);
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[non_exhaustive]
11pub enum SameSite {
12 Strict,
13 Lax,
14 None,
15}
16
17#[derive(Clone, Serialize, Deserialize)]
18#[non_exhaustive]
19pub struct Cookie {
20 pub name: String,
21 pub value: String,
22 pub domain: String,
23 pub path: String,
24 pub secure: bool,
25 pub http_only: bool,
26 pub same_site: SameSite,
27 #[serde(with = "systime_opt_ms")]
28 pub expires: Option<SystemTime>,
29 #[serde(with = "systime_ms")]
30 pub creation_time: SystemTime,
31 #[serde(with = "systime_ms", default = "SystemTime::now")]
32 pub(crate) last_access: SystemTime,
33 pub host_only: bool,
34}
35
36impl std::fmt::Debug for Cookie {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 f.debug_struct("Cookie")
39 .field("name", &self.name)
40 .field("value", &"***")
41 .field("domain", &self.domain)
42 .field("path", &self.path)
43 .field("secure", &self.secure)
44 .field("http_only", &self.http_only)
45 .field("same_site", &self.same_site)
46 .field("expires", &self.expires)
47 .field("host_only", &self.host_only)
48 .finish_non_exhaustive()
49 }
50}
51
52impl Cookie {
53 pub fn is_expired(&self) -> bool {
54 if let Some(expires) = self.expires {
55 SystemTime::now() > expires
56 } else {
57 false
58 }
59 }
60
61 pub(crate) fn matches(&self, url_domain: &str, url_path: &str, is_secure: bool) -> bool {
62 if self.secure && !is_secure {
63 return false;
64 }
65
66 if self.host_only {
67 if !url_domain.eq_ignore_ascii_case(&self.domain) {
68 return false;
69 }
70 } else if !domain_match(url_domain, &self.domain) {
71 return false;
72 }
73 path_match(url_path, &self.path)
74 }
75
76 pub(crate) fn same_slot(&self, other: &Cookie) -> bool {
77 self.name == other.name && self.path == other.path && self.host_only == other.host_only
78 }
79
80 pub(crate) fn shadows_secure(&self, existing: &Cookie) -> bool {
81 existing.secure
82 && existing.name == self.name
83 && (domain_match(&self.domain, &existing.domain)
84 || domain_match(&existing.domain, &self.domain))
85 && (path_match(&self.path, &existing.path) || path_match(&existing.path, &self.path))
86 }
87}
88
89fn domain_match(host: &str, domain: &str) -> bool {
90 let host = host.to_lowercase();
91 let domain = domain.to_lowercase();
92 host == domain || host.ends_with(&format!(".{domain}"))
93}
94
95fn path_match(request_path: &str, cookie_path: &str) -> bool {
96 if request_path == cookie_path {
97 return true;
98 }
99 request_path.starts_with(cookie_path)
100 && (cookie_path.ends_with('/')
101 || request_path.as_bytes().get(cookie_path.len()) == Some(&b'/'))
102}
103
104pub(crate) fn capped_expiry(now: SystemTime, requested: Option<SystemTime>) -> SystemTime {
105 let cap = now + MAX_LIFETIME;
106 requested.map_or(cap, |at| at.min(cap))
107}
108
109fn from_unix_millis(ms: i64) -> Option<SystemTime> {
110 let ms = if ms < 0 { 0 } else { ms as u64 };
111 epoch_plus(Duration::from_millis(ms))
112}
113
114fn unix_millis(t: &SystemTime) -> i64 {
115 t.duration_since(UNIX_EPOCH).map_or(0, |since| {
116 i64::try_from(since.as_millis()).unwrap_or(i64::MAX)
117 })
118}
119
120mod systime_ms {
121 use super::*;
122
123 pub(super) fn serialize<S: Serializer>(t: &SystemTime, s: S) -> Result<S::Ok, S::Error> {
124 s.serialize_i64(unix_millis(t))
125 }
126
127 pub(super) fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<SystemTime, D::Error> {
128 let ms = i64::deserialize(d)?;
129 Ok(from_unix_millis(ms).unwrap_or_else(SystemTime::now))
130 }
131}
132
133mod systime_opt_ms {
134 use super::*;
135
136 pub(super) fn serialize<S: Serializer>(
137 t: &Option<SystemTime>,
138 s: S,
139 ) -> Result<S::Ok, S::Error> {
140 match t {
141 Some(t) => s.serialize_some(&unix_millis(t)),
142 None => s.serialize_none(),
143 }
144 }
145
146 pub(super) fn deserialize<'de, D: Deserializer<'de>>(
147 d: D,
148 ) -> Result<Option<SystemTime>, D::Error> {
149 let opt = Option::<i64>::deserialize(d)?;
150 Ok(opt.map(|ms| capped_expiry(SystemTime::now(), from_unix_millis(ms))))
151 }
152}