Skip to main content

leyline/cookie/
record.rs

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}