Skip to main content

caelix_core/
cookie.rs

1use std::time::{Duration, SystemTime};
2
3/// The `SameSite` attribute applied to a response cookie.
4#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5pub enum SameSite {
6    /// Send only in same-site contexts.
7    Strict,
8    /// Send in same-site contexts and top-level safe navigations.
9    Lax,
10    /// Allow cross-site sending; requires `Secure` in modern browsers.
11    None,
12}
13
14/// A response cookie with secure defaults.
15#[derive(Clone, Debug, Eq, PartialEq)]
16pub struct Cookie {
17    name: String,
18    value: String,
19    http_only: bool,
20    secure: bool,
21    same_site: SameSite,
22    path: Option<String>,
23    domain: Option<String>,
24    max_age: Option<Duration>,
25    expires: Option<SystemTime>,
26}
27
28impl Cookie {
29    /// Creates a cookie with secure framework defaults.
30    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
31        Self {
32            name: name.into(),
33            value: value.into(),
34            http_only: true,
35            secure: true,
36            same_site: SameSite::Lax,
37            path: Some("/".into()),
38            domain: None,
39            max_age: None,
40            expires: None,
41        }
42    }
43
44    /// Creates an expired cookie suitable for removing a stored cookie.
45    pub fn removal(name: impl Into<String>) -> Self {
46        Self::new(name, "")
47            .max_age(Duration::ZERO)
48            .expires(SystemTime::UNIX_EPOCH)
49    }
50
51    /// Sets the `HttpOnly` attribute.
52    pub fn http_only(mut self, value: bool) -> Self {
53        self.http_only = value;
54        self
55    }
56    /// Sets the `Secure` attribute.
57    pub fn secure(mut self, value: bool) -> Self {
58        self.secure = value;
59        self
60    }
61    /// Sets the `SameSite` attribute.
62    pub fn same_site(mut self, value: SameSite) -> Self {
63        self.same_site = value;
64        self
65    }
66    /// Sets the cookie path.
67    pub fn path(mut self, value: impl Into<String>) -> Self {
68        self.path = Some(value.into());
69        self
70    }
71    /// Sets the cookie domain.
72    pub fn domain(mut self, value: impl Into<String>) -> Self {
73        self.domain = Some(value.into());
74        self
75    }
76    /// Sets the relative lifetime.
77    pub fn max_age(mut self, value: Duration) -> Self {
78        self.max_age = Some(value);
79        self
80    }
81    /// Sets the absolute expiry time.
82    pub fn expires(mut self, value: SystemTime) -> Self {
83        self.expires = Some(value);
84        self
85    }
86
87    /// Returns the cookie name.
88    pub fn name(&self) -> &str {
89        &self.name
90    }
91    /// Returns the cookie value.
92    pub fn value(&self) -> &str {
93        &self.value
94    }
95    /// Returns whether `HttpOnly` is enabled.
96    pub fn is_http_only(&self) -> bool {
97        self.http_only
98    }
99    /// Returns whether `Secure` is enabled.
100    pub fn is_secure(&self) -> bool {
101        self.secure
102    }
103    /// Returns the `SameSite` policy.
104    pub fn same_site_value(&self) -> SameSite {
105        self.same_site
106    }
107    /// Returns the configured path.
108    pub fn path_value(&self) -> Option<&str> {
109        self.path.as_deref()
110    }
111    /// Returns the configured domain.
112    pub fn domain_value(&self) -> Option<&str> {
113        self.domain.as_deref()
114    }
115    /// Returns the configured relative lifetime.
116    pub fn max_age_value(&self) -> Option<Duration> {
117        self.max_age
118    }
119    /// Returns the configured absolute expiry time.
120    pub fn expires_value(&self) -> Option<SystemTime> {
121        self.expires
122    }
123
124    /// Serializes this cookie for a `Set-Cookie` header.
125    pub fn to_header_value(&self) -> String {
126        let mut cookie = cookie::Cookie::new(self.name.clone(), self.value.clone());
127        cookie.set_http_only(self.http_only);
128        cookie.set_secure(self.secure);
129        cookie.set_same_site(match self.same_site {
130            SameSite::Strict => cookie::SameSite::Strict,
131            SameSite::Lax => cookie::SameSite::Lax,
132            SameSite::None => cookie::SameSite::None,
133        });
134        if let Some(path) = &self.path {
135            cookie.set_path(path.clone());
136        }
137        if let Some(domain) = &self.domain {
138            cookie.set_domain(domain.clone());
139        }
140        if let Some(max_age) = self.max_age {
141            cookie.set_max_age(
142                cookie::time::Duration::try_from(max_age).unwrap_or(cookie::time::Duration::MAX),
143            );
144        }
145        if let Some(expires) = self.expires {
146            cookie.set_expires(cookie::time::OffsetDateTime::from(expires));
147        }
148        cookie.encoded().to_string()
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn defaults_and_removal_are_safe() {
158        let cookie = Cookie::new("session", "secret");
159        assert!(cookie.is_http_only());
160        assert!(cookie.is_secure());
161        assert_eq!(cookie.same_site_value(), SameSite::Lax);
162        assert_eq!(cookie.path_value(), Some("/"));
163
164        let removal = Cookie::removal("session")
165            .domain("example.com")
166            .path("/auth");
167        assert_eq!(removal.value(), "");
168        assert_eq!(removal.max_age_value(), Some(Duration::ZERO));
169        assert_eq!(removal.expires_value(), Some(SystemTime::UNIX_EPOCH));
170        assert_eq!(removal.domain_value(), Some("example.com"));
171        assert_eq!(removal.path_value(), Some("/auth"));
172    }
173}