1use crate::{
2 http::{Header, IntoHeader},
3 utils::{self, base64_decode, base64_encode},
4};
5use anyhow::Context;
6use hmac::{Hmac, Mac};
7use secrecy::{ExposeSecret, SecretString};
8use serde::{Deserialize, Serialize};
9use sha2::Sha256;
10use std::{collections::HashMap, format, vec};
11
12#[derive(Debug, Clone)]
13pub struct CookieConfig {
14 secure: bool,
15 http_only: bool,
16 same_site: Option<String>,
17 domain: Option<String>,
18 path: Option<String>,
19 expiration: Option<String>, secret: SecretString,
21}
22
23#[derive(Debug, PartialEq, Eq, Clone)]
24pub struct Cookie {
25 config: CookieConfig,
26 name: String,
27 value: String,
28}
29
30#[derive(Serialize, Deserialize, Debug)]
31pub struct CookiePayload {
32 value: String,
33 signature: Vec<u8>,
34}
35
36type HmacSha256 = Hmac<Sha256>;
37
38impl PartialEq for CookieConfig {
39 fn eq(&self, other: &Self) -> bool {
40 self.secure == other.secure
41 && self.http_only == other.http_only
42 && self.same_site == other.same_site
43 && self.domain == other.domain
44 && self.path == other.path
45 && self.expiration == other.expiration
46 && self.secret.expose_secret() == other.secret.expose_secret()
47 }
48}
49
50impl Eq for CookieConfig {}
51
52impl Cookie {
54 pub fn new_with_config(config: &CookieConfig, name: &str, value: &str) -> Cookie {
55 Cookie {
56 config: config.clone(),
57 name: name.into(),
58 value: value.into(),
59 }
60 }
61
62 pub fn delete(&mut self) {
63 self.config.expiration = Some("Thu, 01 Jan 1970 00:00:00 GMT".into())
64 }
65
66 pub fn sign(&self) -> CookiePayload {
68 let mut mac =
69 HmacSha256::new_from_slice(self.config.secret.expose_secret().as_bytes()).unwrap();
70 mac.update(self.value.as_bytes());
71 let sig = mac.finalize().into_bytes().to_vec();
72 CookiePayload {
73 value: self.value.to_string(),
74 signature: sig,
75 }
76 }
77
78 pub fn value(&self) -> &str {
79 self.value.as_str()
80 }
81
82 pub fn name(&self) -> &str {
83 self.name.as_str()
84 }
85}
86
87impl Default for CookieConfig {
88 fn default() -> Self {
92 CookieConfig {
93 secure: true,
94 http_only: true,
95 same_site: Some("Strict".into()),
96 domain: None,
97 path: Some("/".into()),
98 expiration: None,
99 secret: utils::generate_random_secret(),
100 }
101 }
102}
103impl CookieConfig {
106 pub fn new_cookie(&self, name: &str, value: &str) -> Cookie {
107 Cookie::new_with_config(self, name, value)
108 }
109
110 pub fn delete_cookie(&self, name: &str) -> Cookie {
111 let mut config = self.clone();
112 config.expiration = Some("Thu, 01 Jan 1970 00:00:00 GMT".into());
113 config.new_cookie(name, "")
114 }
115
116 pub fn is_valid_signature(&self, payload: &CookiePayload) -> Result<(), anyhow::Error> {
117 let mut mac = HmacSha256::new_from_slice(self.secret.expose_secret().as_bytes())
118 .context("Error Creating Signature Hash")?;
119 mac.update(payload.value.as_bytes());
120 mac
121 .verify_slice(&payload.signature)
122 .context("Invalid Signature")
123 }
124
125 pub fn cookies_from_str(&self, value: &str) -> Result<HashMap<String, Cookie>, anyhow::Error> {
126 let values: Vec<_> = value.split("; ").collect();
127 let iterator = values.into_iter();
128 let mut config = self.clone();
129 let mut map = HashMap::new();
130 let mut raw_cookie_list = vec![];
131
132 for item in iterator {
133 let split: Vec<_> = item.split('=').collect();
134 let n = split[0];
135 match n {
136 "Secure" => {
137 config.secure = true;
138 }
139 "HttpOnly" => {
140 config.http_only = true;
141 }
142 "SameSite" => {
143 if split.len() > 1 {
144 config.same_site = Some(split[1].to_string());
145 }
146 }
147 "Domain" => {
148 if split.len() > 1 {
149 config.domain = Some(split[1].to_string());
150 }
151 }
152 "Path" => {
153 if split.len() > 1 {
154 config.path = Some(split[1].to_string());
155 }
156 }
157 "Expires" => {
158 if split.len() > 1 {
159 config.expiration = Some(split[1].to_string());
160 }
161 }
162 _ => {
163 if split.len() == 2 {
164 raw_cookie_list.push((n.to_string(), split[1].to_string()));
165 } else {
166 raw_cookie_list.push((n.to_string(), String::new()));
167 }
168 }
169 }
170 }
171
172 for (n, v) in raw_cookie_list {
173 let encoded_value = v;
174 if let Ok(decoded_value) = base64_decode(encoded_value) {
175 if let Ok(json_string) = String::from_utf8(decoded_value) {
176 match serde_json::from_str(&json_string) {
177 Ok(payload) => {
178 if self.is_valid_signature(&payload).is_ok() {
179 let cookie = config.new_cookie(&n, &payload.value);
180 map.insert(n, cookie);
181 }
182 }
183 Err(e) => {
184 log::warn!("Cookie Serialaztion Error: {}", e.to_string());
185 }
186 }
187 } else {
188 log::warn!("Got a cookie with invalid signature");
189 }
190 } else {
191 log::warn!("Got a cookie not from us")
192 }
193 }
194 Ok(map)
195 }
196
197 pub fn cookies_from_header(
198 &self,
199 header: Header,
200 ) -> Result<HashMap<String, Cookie>, anyhow::Error> {
201 if header.key == "set-cookie" {
202 self.cookies_from_str(&header.value)
203 } else {
204 Err(anyhow::Error::msg("Invalid Header Name For Cookie"))
205 }
206 }
207
208 pub fn secure(&self) -> bool {
209 self.secure
210 }
211
212 pub fn set_secure(&mut self, secure: bool) {
213 self.secure = secure;
214 }
215
216 pub fn http_only(&self) -> bool {
217 self.http_only
218 }
219
220 pub fn set_http_only(&mut self, http_only: bool) {
221 self.http_only = http_only;
222 }
223
224 pub fn same_site(&self) -> Option<&String> {
225 self.same_site.as_ref()
226 }
227
228 pub fn set_same_site(&mut self, same_site: Option<String>) {
229 self.same_site = same_site;
230 }
231
232 pub fn domain(&self) -> Option<&String> {
233 self.domain.as_ref()
234 }
235
236 pub fn set_domain(&mut self, domain: Option<String>) {
237 self.domain = domain;
238 }
239
240 pub fn set_path(&mut self, path: Option<String>) {
241 self.path = path;
242 }
243
244 pub fn path(&self) -> Option<&String> {
245 self.path.as_ref()
246 }
247
248 pub fn expiration(&self) -> Option<&String> {
249 self.expiration.as_ref()
250 }
251
252 pub fn set_expiration(&mut self, expiration: Option<String>) {
253 self.expiration = expiration;
254 }
255}
256
257impl IntoHeader for Cookie {
258 fn into_header(self) -> crate::http::Header {
259 let cookie_value = self.sign();
260 let cookie_json =
261 serde_json::to_string(&cookie_value).expect("Error Serializing Cookie value"); let cookie_base64 = base64_encode(cookie_json.into());
264 let mut header_value = format!("{}={}", self.name, cookie_base64);
265
266 if self.config.secure {
267 header_value = format!("{}; Secure", header_value);
268 }
269
270 if self.config.http_only {
271 header_value = format!("{}; HttpOnly", header_value);
272 }
273
274 if let Some(ss) = &self.config.same_site {
275 header_value = format!("{}; SameSite={}", header_value, ss);
276 }
277
278 if let Some(domain) = &self.config.domain {
279 header_value = format!("{}; Domain={}", header_value, domain);
280 }
281
282 if let Some(p) = &self.config.path {
283 header_value = format!("{}; Path={}", header_value, p);
284 }
285
286 if let Some(exp) = &self.config.expiration {
287 header_value = format!("{}; Expires={}", header_value, exp);
288 }
289
290 Header::new("Set-Cookie", &header_value)
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use std::dbg;
297
298 use super::*;
299
300 #[test]
301 fn set_cookie_header() {
302 let config = CookieConfig::default();
304 let cookie = config.new_cookie("id", "hi");
305 let header = cookie.clone().into_header();
306 let decoded_coookie = config.cookies_from_header(header).unwrap();
307 assert_eq!(&cookie, decoded_coookie.get("id").unwrap());
308 }
309
310 #[test]
311 fn cookie_builder() {
312 let config = CookieConfig::default();
313 let cookie = config.new_cookie("id", "hi");
314 let header = cookie.clone().into_header();
315 let decoded_cookie = config.cookies_from_header(header).unwrap();
316 assert_eq!(&cookie, decoded_cookie.get("id").unwrap());
317 }
318
319 #[test]
320 fn cookie_delete() {
321 let config = CookieConfig::default();
322 let mut cookie = config.new_cookie("id", "hi");
323 let header = cookie.clone().into_header();
324 let decoded_cookie = config.cookies_from_header(header).unwrap();
325 assert_eq!(&cookie, decoded_cookie.get("id").unwrap());
326
327 cookie.delete();
328 let header = cookie.clone().into_header();
329 let decoded_cookie = config.cookies_from_header(header).unwrap();
330 assert_eq!(&cookie, decoded_cookie.get("id").unwrap());
331 }
332
333 #[test]
334 fn other_cookies() {
335 let config = CookieConfig::default();
336 let cookie = config.new_cookie("id", "hi");
337 let header = cookie.clone().into_header();
338 let decoded_cookie = config.cookies_from_header(header.clone()).unwrap();
339 assert_eq!(&cookie, decoded_cookie.get("id").expect("no cookie"));
340
341 let cookie_str = format!("bob=; robert=bob; this_is=c2VjcmV0; {}", header.value);
342 dbg!(&cookie_str);
343 let cookies = config
344 .cookies_from_str(&cookie_str)
345 .expect("Error Parsing String into Cookies");
346 dbg!(&cookies);
347 assert_eq!("hi", cookies.get("id").unwrap().value());
348 }
349}