1use crate::{BootError, Result};
2use std::time::Duration;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum CookieSameSite {
7 Strict,
8 Lax,
9 None,
10}
11
12impl CookieSameSite {
13 pub fn as_str(self) -> &'static str {
14 match self {
15 Self::Strict => "Strict",
16 Self::Lax => "Lax",
17 Self::None => "None",
18 }
19 }
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct CookieOptions {
25 path: Option<String>,
26 domain: Option<String>,
27 max_age: Option<u64>,
28 http_only: bool,
29 secure: bool,
30 same_site: Option<CookieSameSite>,
31}
32
33impl Default for CookieOptions {
34 fn default() -> Self {
35 Self {
36 path: Some("/".to_string()),
37 domain: None,
38 max_age: None,
39 http_only: false,
40 secure: false,
41 same_site: None,
42 }
43 }
44}
45
46impl CookieOptions {
47 pub fn new() -> Self {
48 Self::default()
49 }
50
51 pub fn with_path(mut self, path: impl Into<String>) -> Self {
52 self.path = Some(path.into());
53 self
54 }
55
56 pub fn without_path(mut self) -> Self {
57 self.path = None;
58 self
59 }
60
61 pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
62 self.domain = Some(domain.into());
63 self
64 }
65
66 pub fn without_domain(mut self) -> Self {
67 self.domain = None;
68 self
69 }
70
71 pub fn with_max_age(mut self, max_age: Duration) -> Self {
72 self.max_age = Some(max_age.as_secs());
73 self
74 }
75
76 pub fn with_max_age_seconds(mut self, max_age: u64) -> Self {
77 self.max_age = Some(max_age);
78 self
79 }
80
81 pub fn without_max_age(mut self) -> Self {
82 self.max_age = None;
83 self
84 }
85
86 pub fn with_http_only(mut self, http_only: bool) -> Self {
87 self.http_only = http_only;
88 self
89 }
90
91 pub fn with_secure(mut self, secure: bool) -> Self {
92 self.secure = secure;
93 self
94 }
95
96 pub fn with_same_site(mut self, same_site: CookieSameSite) -> Self {
97 self.same_site = Some(same_site);
98 self
99 }
100
101 pub fn without_same_site(mut self) -> Self {
102 self.same_site = None;
103 self
104 }
105
106 pub fn path(&self) -> Option<&str> {
107 self.path.as_deref()
108 }
109
110 pub fn domain(&self) -> Option<&str> {
111 self.domain.as_deref()
112 }
113
114 pub fn max_age_seconds(&self) -> Option<u64> {
115 self.max_age
116 }
117
118 pub fn http_only(&self) -> bool {
119 self.http_only
120 }
121
122 pub fn secure(&self) -> bool {
123 self.secure
124 }
125
126 pub fn same_site(&self) -> Option<CookieSameSite> {
127 self.same_site
128 }
129
130 pub(crate) fn set_cookie_header(&self, name: &str, value: &str) -> Result<String> {
131 validate_cookie_name(name)?;
132 validate_cookie_value(value)?;
133 self.cookie_header(name, value, self.max_age)
134 }
135
136 pub(crate) fn delete_cookie_header(&self, name: &str) -> Result<String> {
137 validate_cookie_name(name)?;
138 self.cookie_header(name, "", Some(0))
139 }
140
141 fn cookie_header(&self, name: &str, value: &str, max_age: Option<u64>) -> Result<String> {
142 let mut cookie = format!("{name}={value}");
143 if let Some(path) = &self.path {
144 validate_cookie_attribute("cookie path", path)?;
145 cookie.push_str("; Path=");
146 cookie.push_str(path);
147 }
148 if let Some(domain) = &self.domain {
149 validate_cookie_attribute("cookie domain", domain)?;
150 cookie.push_str("; Domain=");
151 cookie.push_str(domain);
152 }
153 if let Some(max_age) = max_age {
154 cookie.push_str(&format!("; Max-Age={max_age}"));
155 }
156 if self.http_only {
157 cookie.push_str("; HttpOnly");
158 }
159 if self.secure {
160 cookie.push_str("; Secure");
161 }
162 if let Some(same_site) = self.same_site {
163 cookie.push_str("; SameSite=");
164 cookie.push_str(same_site.as_str());
165 }
166 Ok(cookie)
167 }
168}
169
170fn validate_cookie_name(name: &str) -> Result<()> {
171 if name.is_empty() {
172 return Err(BootError::Internal(
173 "cookie name cannot be empty".to_string(),
174 ));
175 }
176
177 if name.bytes().all(is_cookie_name_byte) {
178 Ok(())
179 } else {
180 Err(BootError::Internal(format!(
181 "invalid cookie name {name:?}: cookie name contains invalid characters"
182 )))
183 }
184}
185
186fn validate_cookie_value(value: &str) -> Result<()> {
187 if value.bytes().all(is_cookie_value_byte) {
188 Ok(())
189 } else {
190 Err(BootError::Internal(
191 "cookie value contains invalid characters".to_string(),
192 ))
193 }
194}
195
196fn validate_cookie_attribute(name: &str, value: &str) -> Result<()> {
197 if value.is_empty() {
198 return Err(BootError::Internal(format!("{name} cannot be empty")));
199 }
200
201 if value
202 .bytes()
203 .all(|byte| matches!(byte, 0x20..=0x7e) && byte != b';')
204 {
205 Ok(())
206 } else {
207 Err(BootError::Internal(format!(
208 "{name} contains invalid characters"
209 )))
210 }
211}
212
213fn is_cookie_name_byte(byte: u8) -> bool {
214 matches!(
215 byte,
216 b'!' | b'#'
217 | b'$'
218 | b'%'
219 | b'&'
220 | b'\''
221 | b'*'
222 | b'+'
223 | b'-'
224 | b'.'
225 | b'^'
226 | b'_'
227 | b'`'
228 | b'|'
229 | b'~'
230 | b'0'..=b'9'
231 | b'a'..=b'z'
232 | b'A'..=b'Z'
233 )
234}
235
236fn is_cookie_value_byte(byte: u8) -> bool {
237 matches!(byte, 0x21 | 0x23..=0x2b | 0x2d..=0x3a | 0x3c..=0x5b | 0x5d..=0x7e)
238}