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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
use chrono::{Duration, NaiveDateTime};
use data_encoding::BASE64;
use percent_encoding::{percent_decode, utf8_percent_encode, AsciiSet, CONTROLS};
use ring::hmac::{sign, Key};
use serde::de::{Deserializer, Error};
use serde::ser::Serializer;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use uuid::Uuid;
const PERCENT_ENCODING_ASCII_SET: &AsciiSet = &CONTROLS.add(b':').add(b'=');
#[derive(Clone, Debug)]
pub struct CookieValue {
pub id: Uuid,
pub signature: String,
}
impl CookieValue {
pub(crate) fn new(key: &Key) -> Result<Self, anyhow::Error> {
let id = Uuid::new_v4();
let signature = BASE64.encode(sign(key, id.as_bytes()).as_ref());
Ok(Self { id, signature })
}
pub(crate) fn encode(&self) -> String {
serde_plain::to_string(&self).unwrap()
}
}
impl Serialize for CookieValue {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let formatted = format!(
"s:{}.{}",
self.id.as_simple().encode_lower(&mut Uuid::encode_buffer()),
self.signature
);
let encoded = utf8_percent_encode(&formatted, PERCENT_ENCODING_ASCII_SET).to_string();
serializer.collect_str(&encoded)
}
}
impl<'de> Deserialize<'de> for CookieValue {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let encoded_cookie_value = String::deserialize(deserializer)?;
let cookie_value = percent_decode(encoded_cookie_value.as_bytes())
.decode_utf8()
.map_err(|_| D::Error::custom("invalid cookie value"))?;
if &cookie_value[0..2] != "s:" {
return Err(D::Error::custom("invalid cookie value"));
}
let mut sequence: Vec<_> = cookie_value[2..].split('.').map(|item| item.to_owned()).collect();
if sequence.len() != 2 {
return Err(D::Error::custom("invalid cookie value"));
}
let signature = sequence.pop().unwrap();
let id = Uuid::parse_str(&sequence.pop().unwrap()).map_err(|_| D::Error::custom("invalid cookie value"))?;
Ok(CookieValue { signature, id })
}
}
#[derive(Clone, Debug)]
pub struct CookieConfig<'a, T: 'a + Clone> {
pub value: &'a T,
pub http_only: bool,
pub secure: bool,
pub same_site: SameSite,
pub domain: Option<Cow<'a, str>>,
pub path: Option<Cow<'a, str>>,
pub max_age: Option<Duration>,
pub expires: Option<NaiveDateTime>,
}
impl<'a, T: 'a + Clone + Deserialize<'a> + Serialize> CookieConfig<'a, T> {
pub fn new(value: &'a T) -> Self {
Self {
http_only: true,
secure: true,
same_site: SameSite::Lax,
domain: None,
path: None,
max_age: None,
expires: None,
value,
}
}
pub fn http_only(mut self, http_only: bool) -> Self {
self.http_only = http_only;
self
}
pub fn secure(mut self, secure: bool) -> Self {
self.secure = secure;
self
}
pub fn same_site(mut self, same_site: SameSite) -> Self {
self.same_site = same_site;
self
}
pub fn max_age(mut self, max_age: impl Into<Option<Duration>>) -> Self {
self.max_age = max_age.into();
self
}
pub fn expires(mut self, expires: impl Into<Option<NaiveDateTime>>) -> Self {
self.expires = expires.into();
self
}
pub fn domain<S: Into<Cow<'a, str>>>(mut self, domain: impl Into<Option<S>>) -> Self {
self.domain = domain.into().map(Into::into);
self
}
pub fn path<S: Into<Cow<'a, str>>>(mut self, path: impl Into<Option<S>>) -> Self {
self.path = path.into().map(Into::into);
self
}
}
#[derive(Clone, Debug)]
pub enum SameSite {
Strict,
Lax,
None,
}
impl std::fmt::Display for SameSite {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self {
Self::Strict => write!(f, "Strict"),
Self::Lax => write!(f, "Lax"),
Self::None => write!(f, "None"),
}
}
}