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);
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}