nucleus_http/
cookies.rs

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>, //datetime string
20    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
52/// http cookie, can be converted into a header
53impl 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    /// Signs value of cookie and returns struct containing value and signature
67    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    /// Default settings for cookie
89    /// defaults are Strict same site, secure, http only, path = /, and no expiration
90    /// Note: Domain isnt specifed since that keeps subdomains from having access
91    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}
103/// Settings for Cookie, cookies can be built from this
104/// a cookie config will also generate a random key for signing
105impl 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"); //FIXME: How should we
262                                                                                           //handle an error here ?
263        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 expected = "set-cookie: id=hi; Secure; HttpOnly; SameSite=Strict; Path=/";
303        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}