rusty_express/core/
cookie.rs

1use chrono::prelude::*;
2use std::cmp::Ordering;
3use std::time::{SystemTime, UNIX_EPOCH};
4
5#[derive(PartialEq, Eq, Hash, Clone)]
6pub enum KeyPrefix {
7    Secure,
8    Host,
9}
10
11pub struct Cookie {
12    key: String,
13    value: String,
14    key_prefix: Option<KeyPrefix>,
15    expires: Option<SystemTime>,
16    max_age: Option<u32>,
17    domain: String,
18    path: String,
19    secure: bool,
20    http_only: bool,
21}
22
23impl Cookie {
24    pub fn new(key: &str, value: &str) -> Self {
25        Cookie {
26            key: key.to_owned(),
27            value: value.to_owned(),
28            key_prefix: None,
29            expires: None,
30            max_age: None,
31            domain: String::new(),
32            path: String::new(),
33            secure: false,
34            http_only: false,
35        }
36    }
37
38    pub fn set_key_prefix(&mut self, prefix: Option<KeyPrefix>) {
39        self.key_prefix = match prefix {
40            Some(KeyPrefix::Secure) => {
41                self.secure = true;
42                prefix
43            }
44            Some(KeyPrefix::Host) => {
45                if !self.domain.is_empty() {
46                    self.domain.clear();
47                }
48                self.path = String::from("/");
49                self.secure = true;
50                prefix
51            }
52            _ => prefix,
53        };
54    }
55
56    pub fn set_expires(&mut self, expires_at: Option<SystemTime>) {
57        self.expires = match expires_at {
58            Some(time) if time.cmp(&SystemTime::now()) != Ordering::Greater => None,
59            _ => expires_at,
60        };
61    }
62
63    pub fn set_max_age(&mut self, max_age: Option<u32>) {
64        self.max_age = max_age;
65    }
66
67    pub fn set_path(&mut self, path: &str) {
68        self.path = match self.key_prefix {
69            Some(KeyPrefix::Host) => String::new(),
70            _ if path.is_empty() => String::new(),
71            _ => {
72                if path.starts_with('/') {
73                    path.to_owned()
74                } else {
75                    panic!("Cookie path must start with '/'");
76                }
77            }
78        };
79    }
80
81    pub fn set_domain(&mut self, domain: &str) {
82        self.domain = match self.key_prefix {
83            Some(KeyPrefix::Host) => String::new(),
84            _ => domain.to_owned(),
85        }
86    }
87
88    pub fn set_secure_attr(&mut self, is_secure: bool) {
89        self.secure = match self.key_prefix {
90            Some(KeyPrefix::Host) | Some(KeyPrefix::Secure) => true,
91            _ => is_secure,
92        };
93    }
94
95    pub fn set_http_only_attr(&mut self, http_only: bool) {
96        self.http_only = http_only;
97    }
98
99    pub fn update_session_key(&mut self, key: &str) {
100        if key.is_empty() {
101            panic!("Session key must have a value!");
102        }
103        self.key = key.to_owned();
104    }
105
106    pub fn update_session_value(&mut self, value: &str) {
107        if value.is_empty() {
108            panic!("Session key must have a value!");
109        }
110        self.value = value.to_owned();
111    }
112
113    pub fn is_valid(&self) -> bool {
114        (!self.key.is_empty()) && (!self.value.is_empty())
115    }
116
117    pub fn get_cookie_key(&self) -> String {
118        self.key.to_owned()
119    }
120
121    pub fn get_cookie_value(&self) -> String {
122        self.value.to_owned()
123    }
124}
125
126impl ToString for Cookie {
127    fn to_string(&self) -> String {
128        if self.key.is_empty() || self.value.is_empty() {
129            return String::new();
130        }
131
132        let mut cookie = match self.key_prefix {
133            Some(KeyPrefix::Secure) => {
134                ["__Secure-", &self.key[..], "=", &self.value[..], ";"].join("")
135            }
136            Some(KeyPrefix::Host) => ["__Host-", &self.key[..], "=", &self.value[..], ";"].join(""),
137            _ => [&self.key[..], "=", &self.value[..], ";"].join(""),
138        };
139
140        if let Some(time) = self.expires {
141            let dt = system_to_utc(time)
142                .format("%a, %e %b %Y %T GMT")
143                .to_string();
144
145            cookie.reserve_exact(10 + dt.len());
146            cookie.push_str(" Expires=");
147            cookie.push_str(&dt);
148            cookie.push(';');
149        }
150
151        match self.max_age {
152            Some(age) if age > 0 => {
153                let a = age.to_string();
154
155                cookie.reserve_exact(10 + a.len());
156                cookie.push_str(" Max-Age=");
157                cookie.push_str(&a);
158                cookie.push(';');
159            }
160            _ => { /* Nothing */ }
161        }
162
163        if !self.domain.is_empty() {
164            cookie.reserve_exact(9 + self.domain.len());
165
166            cookie.push_str(" Domain=");
167            cookie.push_str(&self.domain);
168            cookie.push(';');
169        }
170
171        if !self.path.is_empty() {
172            cookie.reserve_exact(7 + self.path.len());
173
174            cookie.push_str(" Path=");
175            cookie.push_str(&self.path);
176            cookie.push(';');
177        }
178
179        if self.secure {
180            cookie.push_str(" Secure;");
181        }
182
183        if self.http_only {
184            cookie.push_str(" HttpOnly;");
185        }
186
187        cookie
188    }
189}
190
191impl Clone for Cookie {
192    fn clone(&self) -> Self {
193        Cookie {
194            key: self.key.clone(),
195            value: self.value.clone(),
196            key_prefix: self.key_prefix.clone(),
197            expires: self.expires,
198            max_age: self.max_age,
199            domain: self.domain.clone(),
200            path: self.path.clone(),
201            secure: self.secure,
202            http_only: self.http_only,
203        }
204    }
205}
206
207fn system_to_utc(t: SystemTime) -> DateTime<Utc> {
208    let (sec, n_sec) = match t.duration_since(UNIX_EPOCH) {
209        Ok(dur) => (dur.as_secs() as i64, dur.subsec_nanos()),
210        Err(e) => {
211            let dur = e.duration();
212            let (sec, n_sec) = (dur.as_secs() as i64, dur.subsec_nanos());
213            if n_sec == 0 {
214                (-sec, 0)
215            } else {
216                (-sec - 1, 1_000_000_000 - n_sec)
217            }
218        }
219    };
220
221    Utc.timestamp(sec, n_sec)
222}