siderite_sessions/
config.rs1use http::HeaderName;
4use siderite_core::SameSite;
5use std::fmt;
6use std::time::Duration;
7
8const HOST_PREFIX: &str = "__Host-";
9const SECURE_PREFIX: &str = "__Secure-";
10const MIN_CSRF_KEY: usize = 32;
11
12#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
15#[error("invalid session configuration: {0}")]
16pub struct SessionConfigError(&'static str);
17
18#[derive(Clone)]
25pub struct SessionConfig {
26 pub(crate) name: String,
27 pub(crate) secure: bool,
28 pub(crate) same_site: SameSite,
29 pub(crate) idle: Duration,
30 pub(crate) absolute: Duration,
31 pub(crate) write_interval: Option<Duration>,
32 pub(crate) store_timeout: Duration,
33 pub(crate) csrf_key: Option<Vec<u8>>,
34 pub(crate) csrf_header: String,
35}
36
37impl Default for SessionConfig {
38 fn default() -> Self {
39 Self {
40 name: format!("{HOST_PREFIX}session"),
41 secure: true,
42 same_site: SameSite::Lax,
43 idle: Duration::from_secs(30 * 60),
44 absolute: Duration::from_secs(12 * 60 * 60),
45 write_interval: None,
46 store_timeout: Duration::from_secs(2),
47 csrf_key: None,
48 csrf_header: "x-csrf-token".to_owned(),
49 }
50 }
51}
52
53impl fmt::Debug for SessionConfig {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 f.debug_struct("SessionConfig")
56 .field("name", &self.name)
57 .field("secure", &self.secure)
58 .field("same_site", &self.same_site)
59 .field("idle", &self.idle)
60 .field("absolute", &self.absolute)
61 .field("write_interval", &self.effective_write_interval())
62 .field("store_timeout", &self.store_timeout)
63 .field("csrf_key", &self.csrf_key.as_ref().map(|_| "[REDACTED]"))
64 .field("csrf_header", &self.csrf_header)
65 .finish()
66 }
67}
68
69impl SessionConfig {
70 #[must_use]
72 pub fn new() -> Self {
73 Self::default()
74 }
75
76 #[must_use]
82 pub fn cookie_name(mut self, name: impl Into<String>) -> Self {
83 self.name = name.into();
84 self
85 }
86
87 #[must_use]
94 pub fn same_site(mut self, same_site: SameSite) -> Self {
95 self.same_site = same_site;
96 self
97 }
98
99 #[must_use]
105 pub fn idle_timeout(mut self, idle: Duration) -> Self {
106 self.idle = idle;
107 self
108 }
109
110 #[must_use]
113 pub fn absolute_lifetime(mut self, absolute: Duration) -> Self {
114 self.absolute = absolute;
115 self
116 }
117
118 #[must_use]
121 pub fn write_interval(mut self, interval: Duration) -> Self {
122 self.write_interval = Some(interval);
123 self
124 }
125
126 #[must_use]
129 pub fn store_timeout(mut self, timeout: Duration) -> Self {
130 self.store_timeout = timeout;
131 self
132 }
133
134 #[must_use]
140 pub fn insecure_development(mut self) -> Self {
141 self.secure = false;
142 for prefix in [HOST_PREFIX, SECURE_PREFIX] {
143 if let Some(rest) = self.name.strip_prefix(prefix) {
144 self.name = rest.to_owned();
145 break;
146 }
147 }
148 self
149 }
150
151 #[must_use]
156 pub fn csrf_key(mut self, key: impl Into<Vec<u8>>) -> Self {
157 self.csrf_key = Some(key.into());
158 self
159 }
160
161 #[must_use]
164 pub fn csrf_header(mut self, name: impl Into<String>) -> Self {
165 self.csrf_header = name.into();
166 self
167 }
168
169 #[must_use]
171 pub fn name(&self) -> &str {
172 &self.name
173 }
174
175 pub fn validate(&self) -> Result<(), SessionConfigError> {
182 let err = |message| Err(SessionConfigError(message));
183 if !is_token(&self.name) {
184 return err("cookie name is not a valid token");
185 }
186 let prefixed = self.name.starts_with(HOST_PREFIX)
187 || self.name.starts_with(SECURE_PREFIX);
188 if prefixed && !self.secure {
189 return err("a prefixed cookie name requires Secure");
190 }
191 if self.same_site == SameSite::None
192 && (!self.secure || self.csrf_key.is_none())
193 {
194 return err("SameSite=None requires Secure and a csrf_key");
195 }
196 if self
197 .csrf_key
198 .as_ref()
199 .is_some_and(|k| k.len() < MIN_CSRF_KEY)
200 {
201 return err("csrf_key must be at least 32 bytes");
202 }
203 if self.csrf_header.parse::<HeaderName>().is_err() {
204 return err("csrf_header is not a valid header name");
205 }
206 let write = self.effective_write_interval();
207 if self.idle.is_zero() || self.absolute.is_zero() {
208 return err("session lifetimes must be positive");
209 }
210 if self.store_timeout.is_zero() || write.is_zero() {
211 return err("timeouts and intervals must be positive");
212 }
213 if write > self.idle {
214 return err("write_interval must not exceed idle_timeout");
215 }
216 Ok(())
217 }
218
219 pub(crate) fn effective_write_interval(&self) -> Duration {
221 self.write_interval.unwrap_or(self.idle / 4)
222 }
223}
224
225fn is_token(name: &str) -> bool {
227 !name.is_empty()
228 && name.bytes().all(|b| {
229 b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b)
230 })
231}