wasm_cookies/
cookies.rs

1//! This module provides the same functions as the root module, but which don't operate
2//! directly on the browser's cookie string, so it can be used outside a browser.
3//!
4//! Instead of reading the browser's cookie string, functions in this module take it as an
5//! argument. Instead of writing to the browser's cookie string, they return it.
6
7#[cfg(not(target_arch = "wasm32"))]
8use chrono::offset::Utc;
9#[cfg(not(target_arch = "wasm32"))]
10use chrono::NaiveDateTime;
11#[cfg(target_arch = "wasm32")]
12use js_sys::Date;
13use std::borrow::Cow;
14use std::collections::HashMap;
15use std::time::Duration;
16use urlencoding::FromUrlEncodingError;
17#[cfg(target_arch = "wasm32")]
18use wasm_bindgen::JsValue;
19
20/// URI decoding error on a key or a value, when calling `wasm_cookie::all`.
21#[derive(Debug)]
22pub enum AllDecodeError {
23    /// URI decoding error on a key.
24    ///
25    /// - The first field is the raw key.
26    /// - The second field is the URI decoding error.
27    Key(String, FromUrlEncodingError),
28
29    /// URI decoding error on a value.
30    ///
31    /// - The first field is the URI decoded key corresponding to the value.
32    /// - The second field is the URI decoding error.
33    Value(String, FromUrlEncodingError),
34}
35
36fn process_key_value_str(key_value_str: &str) -> Result<(&str, &str), ()> {
37    match key_value_str.split_once('=') {
38        Some((key, value)) => Ok((key.trim(), value.trim())),
39        None => Err(()),
40    }
41}
42
43/// Returns all cookies as key-value pairs, with undecoded keys and values.
44pub fn all_iter_raw(cookie_string: &str) -> impl Iterator<Item = (&str, &str)> {
45    cookie_string.split(';').filter_map(|key_value_str| {
46        match process_key_value_str(key_value_str) {
47            Ok((key, value)) => Some((key, value)),
48            Err(_) => None,
49        }
50    })
51}
52
53/// Returns all cookies as key-value pairs, with URI decoded keys and values
54/// (with the [urlencoding crate](https://crates.io/crates/urlencoding)),
55/// or an error if URI decoding fails on a key or a value.
56pub fn all_iter(
57    cookie_string: &str,
58) -> impl Iterator<Item = Result<(String, String), AllDecodeError>> + '_ {
59    all_iter_raw(cookie_string).map(|(key, value)| match urlencoding::decode(key) {
60        Ok(key) => match urlencoding::decode(value) {
61            Ok(value) => Ok((key, value)),
62            Err(error) => Err(AllDecodeError::Value(key, error)),
63        },
64
65        Err(error) => Err(AllDecodeError::Key(key.to_owned(), error)),
66    })
67}
68
69/// Returns all cookies, with undecoded keys and values.
70pub fn all_raw(cookie_string: &str) -> HashMap<String, String> {
71    all_iter_raw(cookie_string)
72        .map(|(key, value)| (key.to_owned(), value.to_owned()))
73        .collect()
74}
75
76/// Returns all cookies, with URI decoded keys and values
77/// (with the [urlencoding crate](https://crates.io/crates/urlencoding)),
78/// or an error if URI decoding fails on a key or a value.
79pub fn all(cookie_string: &str) -> Result<HashMap<String, String>, AllDecodeError> {
80    all_iter(cookie_string).collect()
81}
82
83/// Returns undecoded cookie if it exists.
84pub fn get_raw(cookie_string: &str, name: &str) -> Option<String> {
85    cookie_string
86        .split(';')
87        .find_map(|key_value_str| match process_key_value_str(key_value_str) {
88            Ok((key, value)) => {
89                if key == name {
90                    Some(value.to_owned())
91                } else {
92                    None
93                }
94            }
95
96            Err(_) => None,
97        })
98}
99
100/// If it exists, returns URI decoded cookie
101/// (with the [urlencoding crate](https://crates.io/crates/urlencoding))
102/// or an error if the value's URI decoding fails.
103pub fn get(cookie_string: &str, name: &str) -> Option<Result<String, FromUrlEncodingError>> {
104    let name = urlencoding::encode(name);
105
106    cookie_string
107        .split(';')
108        .find_map(|key_value_str| match process_key_value_str(key_value_str) {
109            Ok((key, value)) => {
110                if key == name {
111                    Some(urlencoding::decode(value))
112                } else {
113                    None
114                }
115            }
116
117            Err(_) => None,
118        })
119}
120
121/// Cookies options (see [https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie](https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie)).
122///
123/// You can create it by calling `CookieOptions::default()`.
124#[derive(Default, Clone, Debug)]
125pub struct CookieOptions<'a> {
126    /// If `None`, defaults to the current path of the current document location.
127    pub path: Option<&'a str>,
128
129    /// If `None`, defaults to the host portion of the current document location.
130    pub domain: Option<&'a str>,
131
132    /// Expiration date in GMT string format.
133    /// If `None`, the cookie will expire at the end of session.
134    pub expires: Option<Cow<'a, str>>,
135
136    /// If true, the cookie will only be transmitted over secure protocol as HTTPS.
137    /// The default value is false.
138    pub secure: bool,
139
140    /// SameSite prevents the browser from sending the cookie along with cross-site requests
141    /// (see [https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#SameSite_attribute](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#SameSite_attribute)).
142    pub same_site: SameSite,
143}
144
145impl<'a> CookieOptions<'a> {
146    /// Set the path.
147    /// The default value is the current path of the current document location.
148    pub fn with_path(mut self, path: &'a str) -> Self {
149        self.path = Some(path);
150        self
151    }
152
153    /// Set the domain.
154    /// The default value is the host portion of the current document location.
155    pub fn with_domain(mut self, domain: &'a str) -> Self {
156        self.domain = Some(domain);
157        self
158    }
159
160    /// Expires the cookie at a specific date.
161    ///
162    /// `date` must be a GMT string (see <https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Global_Objects/Date/toUTCString>).
163    ///
164    /// The default behavior of the cookie is to expire at the end of session.
165    pub fn expires_at_date(mut self, date: &'a str) -> Self {
166        self.expires = Some(Cow::Borrowed(date));
167        self
168    }
169
170    /// Expires the cookie at a specific timestamp (in milliseconds, UTC, with leap seconds ignored).
171    /// The default behavior of the cookie is to expire at the end of session.
172    pub fn expires_at_timestamp(mut self, timestamp: i64) -> Self {
173        #[cfg(target_arch = "wasm32")]
174        let date: String = Date::new(&JsValue::from_f64(timestamp as f64))
175            .to_utc_string()
176            .into();
177
178        #[cfg(not(target_arch = "wasm32"))]
179        let date = NaiveDateTime::from_timestamp_millis(timestamp)
180            .unwrap()
181            .format("%a, %d %b %Y %T GMT")
182            .to_string();
183
184        self.expires = Some(Cow::Owned(date));
185        self
186    }
187
188    /// Expires the cookie after a certain duration.
189    /// The default behavior of the cookie is to expire at the end of session.
190    pub fn expires_after(self, duration: Duration) -> Self {
191        #[cfg(target_arch = "wasm32")]
192        let now = Date::now() as i64;
193        #[cfg(not(target_arch = "wasm32"))]
194        let now = Utc::now().timestamp_millis();
195        self.expires_at_timestamp(now + duration.as_millis() as i64)
196    }
197
198    /// Set the cookie to be only transmitted over secure protocol as HTTPS.
199    pub fn secure(mut self) -> Self {
200        self.secure = true;
201        self
202    }
203
204    /// Set the SameSite value.
205    /// SameSite prevents the browser from sending the cookie along with cross-site requests
206    /// (see [https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#SameSite_attribute](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#SameSite_attribute)).
207    pub fn with_same_site(mut self, same_site: SameSite) -> Self {
208        self.same_site = same_site;
209        self
210    }
211}
212
213/// SameSite value for [CookieOptions](struct.CookieOptions.html).
214///
215/// SameSite prevents the browser from sending the cookie along with cross-site requests
216/// (see [https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#SameSite_attribute](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#SameSite_attribute)).
217#[derive(Clone, Debug)]
218pub enum SameSite {
219    /// The `Lax` value value will send the cookie for all same-site requests and top-level navigation GET requests.
220    /// This is sufficient for user tracking, but it will prevent many CSRF attacks.
221    /// This is the default value when calling `SameSite::default()`.
222    Lax,
223
224    /// The `Strict` value will prevent the cookie from being sent by the browser to the
225    /// target site in all cross-site browsing contexts, even when following a regular link.
226    Strict,
227
228    /// The `None` value explicitly states no restrictions will be applied.
229    /// The cookie will be sent in all requests - both cross-site and same-site.
230    None,
231}
232
233impl Default for SameSite {
234    fn default() -> Self {
235        Self::Lax
236    }
237}
238
239impl SameSite {
240    fn cookie_string_value(&self) -> &'static str {
241        match self {
242            SameSite::Lax => "lax",
243            SameSite::Strict => "strict",
244            SameSite::None => "none",
245        }
246    }
247}
248
249/// Return the cookie string that sets a cookie, with non encoded name and value.
250pub fn set_raw(name: &str, value: &str, options: &CookieOptions) -> String {
251    let mut cookie_string = name.to_owned();
252    cookie_string.push('=');
253    cookie_string.push_str(value);
254
255    if let Some(path) = options.path {
256        cookie_string.push_str(";path=");
257        cookie_string.push_str(path);
258    }
259
260    if let Some(domain) = options.domain {
261        cookie_string.push_str(";domain=");
262        cookie_string.push_str(domain);
263    }
264
265    if let Some(expires_str) = &options.expires {
266        cookie_string.push_str(";expires=");
267        cookie_string.push_str(expires_str);
268    }
269
270    if options.secure {
271        cookie_string.push_str(";secure");
272    }
273
274    cookie_string.push_str(";samesite=");
275    cookie_string.push_str(options.same_site.cookie_string_value());
276
277    cookie_string
278}
279
280/// Return the cookie string that sets a cookie, with URI encoded name and value
281/// (with the [urlencoding crate](https://crates.io/crates/urlencoding)).
282pub fn set(name: &str, value: &str, options: &CookieOptions) -> String {
283    set_raw(
284        &urlencoding::encode(name),
285        &urlencoding::encode(value),
286        options,
287    )
288}
289
290/// Return the cookie string that deletes a cookie without encoding its name.
291pub fn delete_raw(name: &str) -> String {
292    format!("{}=;expires=Thu, 01 Jan 1970 00:00:00 GMT", name)
293}
294
295/// Return the cookie string that deletes a cookie, URI encoding its name.
296pub fn delete(name: &str) -> String {
297    delete_raw(&urlencoding::encode(name))
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    #[test]
305    fn test_all_raw() {
306        let cookies = all_raw(" key1=value1;key2=value2 ; key3  = value3");
307        assert_eq!(cookies.len(), 3);
308        assert_eq!(cookies["key1"], "value1");
309        assert_eq!(cookies["key2"], "value2");
310        assert_eq!(cookies["key3"], "value3");
311
312        let cookies = all_raw("");
313        assert!(cookies.is_empty());
314    }
315
316    #[test]
317    fn test_all() {
318        let cookies =
319            all("key%20%251=value%20%251  ;key%20%252=value%20%252;key%20%253  = value%20%253")
320                .unwrap();
321        assert_eq!(cookies.len(), 3);
322        assert_eq!(cookies["key %1"], "value %1");
323        assert_eq!(cookies["key %2"], "value %2");
324        assert_eq!(cookies["key %3"], "value %3");
325
326        let cookies = all("").unwrap();
327        assert!(cookies.is_empty());
328
329        let error = all("key1=value1;key2%AA=value2").unwrap_err();
330
331        match error {
332            AllDecodeError::Key(raw_key, _) => assert_eq!(raw_key, "key2%AA"),
333            _ => panic!(),
334        }
335
336        let error = all("key1=value1;key%202=value2%AA").unwrap_err();
337
338        match error {
339            AllDecodeError::Value(key, _) => assert_eq!(key, "key 2"),
340            _ => panic!(),
341        }
342    }
343
344    #[test]
345    fn test_get_raw() {
346        assert_eq!(
347            get_raw("key1=value1 ; key2= value2;key3=value3", "key2"),
348            Some("value2".to_owned())
349        );
350
351        assert_eq!(
352            get_raw("key1=value1 ; key2= value2;key3=value3", "key4"),
353            None
354        );
355    }
356
357    #[test]
358    fn test_get() {
359        assert_eq!(
360            get("key1=value1 ; key%202= value%202=;key3=value3", "key 2")
361                .map(|result| result.unwrap()),
362            Some("value 2=".to_owned())
363        );
364
365        assert!(get("key1=value1 ; key2= value2;key3=value3", "key4").is_none());
366        assert!(get("key1=value1 ; key2= value2%AA;key3=value3", "key2")
367            .unwrap()
368            .is_err());
369    }
370
371    #[test]
372    fn test_set_raw() {
373        assert_eq!(
374            set_raw("key", "value", &CookieOptions::default()),
375            "key=value;samesite=lax"
376        );
377
378        assert_eq!(
379            set_raw("key", "value", &CookieOptions::default().with_path("/path")),
380            "key=value;path=/path;samesite=lax"
381        );
382
383        assert_eq!(
384            set_raw(
385                "key",
386                "value",
387                &CookieOptions::default()
388                    .with_path("/path")
389                    .with_domain("example.com")
390            ),
391            "key=value;path=/path;domain=example.com;samesite=lax"
392        );
393
394        assert_eq!(
395            set_raw(
396                "key",
397                "value",
398                &CookieOptions::default()
399                    .with_path("/path")
400                    .with_domain("example.com")
401                    .secure()
402            ),
403            "key=value;path=/path;domain=example.com;secure;samesite=lax"
404        );
405
406        assert_eq!(
407            set_raw(
408                "key",
409                "value",
410                &CookieOptions::default().expires_at_timestamp(1100000000000),
411            ),
412            "key=value;expires=Tue, 09 Nov 2004 11:33:20 GMT;samesite=lax",
413        );
414    }
415}