1use std::time::SystemTime;
5
6use crate::header::Header;
7
8#[derive(Debug, Clone, PartialEq, Eq, Default)]
13pub enum SameSite {
14 #[default]
15 Default,
16 Lax,
17 Strict,
18 None,
19}
20
21#[derive(Debug, Clone, Default)]
27pub struct Cookie {
28 pub name: String,
29 pub value: String,
30 pub path: String,
31 pub domain: String,
32 pub expires: Option<SystemTime>,
33 pub max_age: i32,
34 pub secure: bool,
35 pub http_only: bool,
36 pub same_site: SameSite,
37}
38
39impl Cookie {
40 pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
41 Self {
42 name: name.into(),
43 value: value.into(),
44 ..Default::default()
45 }
46 }
47
48 pub fn to_set_cookie_header(&self) -> String {
50 let mut s = format!("{}={}", self.name, self.value);
51 if !self.path.is_empty() { s.push_str(&format!("; Path={}", self.path)); }
52 if !self.domain.is_empty() { s.push_str(&format!("; Domain={}", self.domain)); }
53 if self.max_age > 0 { s.push_str(&format!("; Max-Age={}", self.max_age)); }
54 if self.secure { s.push_str("; Secure"); }
55 if self.http_only { s.push_str("; HttpOnly"); }
56 match self.same_site {
57 SameSite::Lax => s.push_str("; SameSite=Lax"),
58 SameSite::Strict => s.push_str("; SameSite=Strict"),
59 SameSite::None => s.push_str("; SameSite=None"),
60 SameSite::Default => {}
61 }
62 s
63 }
64}
65
66pub fn parse_request_cookies(h: &Header) -> Vec<Cookie> {
72 let mut cookies = Vec::new();
73 for val in h.values("Cookie") {
74 for pair in val.split(';') {
75 let pair = pair.trim();
76 if let Some(eq) = pair.find('=') {
77 let name = pair[..eq].trim().to_owned();
78 let value = pair[eq + 1..].trim().to_owned();
79 if !name.is_empty() {
80 cookies.push(Cookie::new(name, value));
81 }
82 }
83 }
84 }
85 cookies
86}
87
88pub trait CookieJar: Send + Sync {
93 fn cookies(&self, url: &url::Url) -> Vec<Cookie>;
94 fn set_cookies(&self, url: &url::Url, cookies: &[Cookie]);
95}
96
97use std::sync::Mutex;
102use std::collections::HashMap;
103
104pub struct MemoryCookieJar {
106 store: Mutex<HashMap<String, Vec<Cookie>>>,
107}
108
109impl MemoryCookieJar {
110 pub fn new() -> Self {
111 Self { store: Mutex::new(HashMap::new()) }
112 }
113}
114
115impl Default for MemoryCookieJar {
116 fn default() -> Self { Self::new() }
117}
118
119impl CookieJar for MemoryCookieJar {
120 fn cookies(&self, url: &url::Url) -> Vec<Cookie> {
121 let key = url.host_str().unwrap_or("").to_owned();
122 self.store.lock().unwrap().get(&key).cloned().unwrap_or_default()
123 }
124
125 fn set_cookies(&self, url: &url::Url, cookies: &[Cookie]) {
126 let key = url.host_str().unwrap_or("").to_owned();
127 self.store.lock().unwrap()
128 .entry(key)
129 .or_default()
130 .extend_from_slice(cookies);
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn to_set_cookie_header_basic() {
140 let c = Cookie::new("session", "abc123");
141 let s = c.to_set_cookie_header();
142 assert_eq!(s, "session=abc123");
143 }
144
145 #[test]
146 fn to_set_cookie_header_full() {
147 let c = Cookie {
148 name: "id".into(),
149 value: "42".into(),
150 path: "/".into(),
151 secure: true,
152 http_only: true,
153 same_site: SameSite::Lax,
154 ..Default::default()
155 };
156 let s = c.to_set_cookie_header();
157 assert!(s.contains("id=42"));
158 assert!(s.contains("Path=/"));
159 assert!(s.contains("Secure"));
160 assert!(s.contains("HttpOnly"));
161 assert!(s.contains("SameSite=Lax"));
162 }
163
164 #[test]
165 fn parse_request_cookies_basic() {
166 let mut h = Header::new();
167 h.set("Cookie", "a=1; b=2");
168 let cookies = parse_request_cookies(&h);
169 assert_eq!(cookies.len(), 2);
170 assert_eq!(cookies[0].name, "a");
171 assert_eq!(cookies[1].value, "2");
172 }
173}