Skip to main content

go_http/
cookie.rs

1// SPDX-License-Identifier: Apache-2.0
2
3/// Cookie and CookieJar — port of Go net/http cookie handling.
4use std::time::SystemTime;
5
6use crate::header::Header;
7
8// ---------------------------------------------------------------------------
9// SameSite
10// ---------------------------------------------------------------------------
11
12#[derive(Debug, Clone, PartialEq, Eq, Default)]
13pub enum SameSite {
14    #[default]
15    Default,
16    Lax,
17    Strict,
18    None,
19}
20
21// ---------------------------------------------------------------------------
22// Cookie
23// ---------------------------------------------------------------------------
24
25/// An HTTP cookie.  Mirrors Go's `http.Cookie`.
26#[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    /// Serialize to `Set-Cookie` header value.
49    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
66// ---------------------------------------------------------------------------
67// Parse request cookies from Cookie header
68// ---------------------------------------------------------------------------
69
70/// Parse all cookies from the `Cookie` header of an incoming request.
71pub 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
88// ---------------------------------------------------------------------------
89// CookieJar trait
90// ---------------------------------------------------------------------------
91
92pub 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
97// ---------------------------------------------------------------------------
98// MemoryCookieJar
99// ---------------------------------------------------------------------------
100
101use std::sync::Mutex;
102use std::collections::HashMap;
103
104/// In-memory cookie jar.
105pub 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}