1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
//! A request/response cookie jar with plain and signed cookies (Rails
//! `cookies[...]` / `cookies.signed[...]`).
//!
//! Incoming cookies are parsed from the request `Cookie` header; outgoing
//! cookies are collected and rendered as `Set-Cookie` header values. Signed
//! cookies carry an HMAC-SHA256 signature so a tampered value is rejected on
//! read.
use crate::signing;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use std::collections::BTreeMap;
/// A cookie to be written back in a `Set-Cookie` header.
struct SetCookie {
name: String,
value: String,
}
/// Read incoming cookies and stage outgoing ones for a single request.
pub struct CookieJar {
incoming: BTreeMap<String, String>,
outgoing: Vec<SetCookie>,
secret: Vec<u8>,
}
impl CookieJar {
/// Build a jar from an optional `Cookie` header and a signing secret.
pub fn from_header(cookie_header: Option<&str>, secret: Vec<u8>) -> Self {
let mut incoming = BTreeMap::new();
if let Some(header) = cookie_header {
for (name, value) in header
.split(';')
.filter_map(|pair| pair.trim().split_once('='))
{
incoming.insert(name.to_string(), value.to_string());
}
}
Self {
incoming,
outgoing: Vec::new(),
secret,
}
}
/// Read a plain incoming cookie value.
pub fn get(&self, name: &str) -> Option<&str> {
self.incoming.get(name).map(String::as_str)
}
/// Stage a plain cookie to be written on the response.
pub fn set(&mut self, name: impl Into<String>, value: impl Into<String>) {
self.outgoing.push(SetCookie {
name: name.into(),
value: value.into(),
});
}
/// Read a signed incoming cookie, returning `None` if it is missing,
/// malformed, or its signature does not verify.
pub fn get_signed(&self, name: &str) -> Option<String> {
let raw = self.incoming.get(name)?;
let (msg, sig) = raw.split_once('.')?;
if !signing::verify(&self.secret, msg.as_bytes(), sig) {
return None;
}
let bytes = URL_SAFE_NO_PAD.decode(msg).ok()?;
String::from_utf8(bytes).ok()
}
/// Stage a signed cookie (`base64url(value).signature`).
pub fn set_signed(&mut self, name: impl Into<String>, value: impl AsRef<str>) {
let msg = URL_SAFE_NO_PAD.encode(value.as_ref().as_bytes());
let sig = signing::sign(&self.secret, msg.as_bytes());
self.outgoing.push(SetCookie {
name: name.into(),
value: format!("{msg}.{sig}"),
});
}
/// Render the staged cookies as `Set-Cookie` header values.
pub fn to_set_cookie_headers(&self) -> Vec<String> {
self.outgoing
.iter()
.map(|c| format!("{}={}; Path=/; HttpOnly", c.name, c.value))
.collect()
}
}