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