doido_controller/
cookies.rs1use crate::signing;
10use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
11use std::collections::BTreeMap;
12
13struct SetCookie {
15 name: String,
16 value: String,
17 max_age: Option<i64>,
19}
20
21pub struct CookieJar {
23 incoming: BTreeMap<String, String>,
24 outgoing: Vec<SetCookie>,
25 secret: Vec<u8>,
26}
27
28impl CookieJar {
29 pub fn from_header(cookie_header: Option<&str>, secret: Vec<u8>) -> Self {
31 let mut incoming = BTreeMap::new();
32 if let Some(header) = cookie_header {
33 for (name, value) in header
34 .split(';')
35 .filter_map(|pair| pair.trim().split_once('='))
36 {
37 incoming.insert(name.to_string(), value.to_string());
38 }
39 }
40 Self {
41 incoming,
42 outgoing: Vec::new(),
43 secret,
44 }
45 }
46
47 pub fn get(&self, name: &str) -> Option<&str> {
49 self.incoming.get(name).map(String::as_str)
50 }
51
52 pub fn set(&mut self, name: impl Into<String>, value: impl Into<String>) {
54 self.outgoing.push(SetCookie {
55 name: name.into(),
56 value: value.into(),
57 max_age: None,
58 });
59 }
60
61 pub fn get_signed(&self, name: &str) -> Option<String> {
64 let raw = self.incoming.get(name)?;
65 let (msg, sig) = raw.split_once('.')?;
66 if !signing::verify(&self.secret, msg.as_bytes(), sig) {
67 return None;
68 }
69 let bytes = URL_SAFE_NO_PAD.decode(msg).ok()?;
70 String::from_utf8(bytes).ok()
71 }
72
73 pub fn set_signed(&mut self, name: impl Into<String>, value: impl AsRef<str>) {
75 self.push_signed(name, value, None);
76 }
77
78 pub fn set_signed_permanent(
82 &mut self,
83 name: impl Into<String>,
84 value: impl AsRef<str>,
85 max_age: i64,
86 ) {
87 self.push_signed(name, value, Some(max_age));
88 }
89
90 fn push_signed(
91 &mut self,
92 name: impl Into<String>,
93 value: impl AsRef<str>,
94 max_age: Option<i64>,
95 ) {
96 let msg = URL_SAFE_NO_PAD.encode(value.as_ref().as_bytes());
97 let sig = signing::sign(&self.secret, msg.as_bytes());
98 self.outgoing.push(SetCookie {
99 name: name.into(),
100 value: format!("{msg}.{sig}"),
101 max_age,
102 });
103 }
104
105 pub fn to_set_cookie_headers(&self) -> Vec<String> {
107 self.outgoing
108 .iter()
109 .map(|c| match c.max_age {
110 Some(age) => format!("{}={}; Path=/; HttpOnly; Max-Age={age}", c.name, c.value),
111 None => format!("{}={}; Path=/; HttpOnly", c.name, c.value),
112 })
113 .collect()
114 }
115}