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}
18
19pub struct CookieJar {
21 incoming: BTreeMap<String, String>,
22 outgoing: Vec<SetCookie>,
23 secret: Vec<u8>,
24}
25
26impl CookieJar {
27 pub fn from_header(cookie_header: Option<&str>, secret: Vec<u8>) -> Self {
29 let mut incoming = BTreeMap::new();
30 if let Some(header) = cookie_header {
31 for (name, value) in header
32 .split(';')
33 .filter_map(|pair| pair.trim().split_once('='))
34 {
35 incoming.insert(name.to_string(), value.to_string());
36 }
37 }
38 Self {
39 incoming,
40 outgoing: Vec::new(),
41 secret,
42 }
43 }
44
45 pub fn get(&self, name: &str) -> Option<&str> {
47 self.incoming.get(name).map(String::as_str)
48 }
49
50 pub fn set(&mut self, name: impl Into<String>, value: impl Into<String>) {
52 self.outgoing.push(SetCookie {
53 name: name.into(),
54 value: value.into(),
55 });
56 }
57
58 pub fn get_signed(&self, name: &str) -> Option<String> {
61 let raw = self.incoming.get(name)?;
62 let (msg, sig) = raw.split_once('.')?;
63 if !signing::verify(&self.secret, msg.as_bytes(), sig) {
64 return None;
65 }
66 let bytes = URL_SAFE_NO_PAD.decode(msg).ok()?;
67 String::from_utf8(bytes).ok()
68 }
69
70 pub fn set_signed(&mut self, name: impl Into<String>, value: impl AsRef<str>) {
72 let msg = URL_SAFE_NO_PAD.encode(value.as_ref().as_bytes());
73 let sig = signing::sign(&self.secret, msg.as_bytes());
74 self.outgoing.push(SetCookie {
75 name: name.into(),
76 value: format!("{msg}.{sig}"),
77 });
78 }
79
80 pub fn to_set_cookie_headers(&self) -> Vec<String> {
82 self.outgoing
83 .iter()
84 .map(|c| format!("{}={}; Path=/; HttpOnly", c.name, c.value))
85 .collect()
86 }
87}