1use std::sync::Arc;
4
5use rainier_http::{Cookie, Request, Response, SameSite};
6use rainier_middleware::{Middleware, Next};
7use rainier_support::Result;
8
9use crate::manager::{SessionConfig, SessionManager};
10use crate::session::Session;
11use crate::store::SessionStore;
12
13pub struct StartSession {
20 store: Arc<dyn SessionStore>,
21 config: SessionConfig,
22}
23
24impl StartSession {
25 pub fn new(store: Arc<dyn SessionStore>) -> Self {
27 Self { store, config: SessionConfig::default() }
28 }
29
30 pub fn with_config(store: Arc<dyn SessionStore>, config: SessionConfig) -> Self {
32 Self { store, config }
33 }
34
35 pub fn from_manager(manager: &SessionManager) -> Self {
38 Self::with_config(Arc::clone(manager.store()), manager.config().clone())
39 }
40
41 fn cookie(&self, value: &str) -> Cookie {
48 let config = &self.config;
49 let mut cookie = Cookie::new(&config.cookie, value)
50 .path(&config.path)
51 .http_only(true)
52 .secure(config.secure)
53 .same_site(config.same_site)
54 .max_age(config.lifetime.num_seconds());
55
56 if let Some(domain) = &config.domain {
57 cookie = cookie.domain(domain);
58 }
59 cookie
60 }
61
62 async fn load(&self, request: &Request) -> Session {
64 let Some(value) = request.cookie(&self.config.cookie) else {
65 return Session::new();
66 };
67
68 let (id, carried) = match self.store.decode(value) {
73 Ok(decoded) => decoded,
74 Err(_) => return Session::new(),
75 };
76
77 if let Some(data) = carried {
80 return Session::restore(id, data);
81 }
82
83 match self.store.read(&id).await {
84 Ok(Some(data)) => Session::restore(id, data),
85 Ok(None) => Session::new(),
86 Err(e) => {
87 tracing::error!(error = %e, "could not read the session; starting a new one");
91 Session::new()
92 }
93 }
94 }
95
96 async fn save(&self, session: &Session) -> Result<String> {
101 if !self.store.is_client_side() {
102 for id in session.superseded_ids() {
103 self.store.destroy(&id).await?;
104 }
105 }
106
107 let id = session.id();
108 let data = session.age_and_take();
109
110 if !self.store.is_client_side() {
111 self.store.write(&id, &data).await?;
112 }
113
114 self.store.encode(&id, &data)
117 }
118}
119
120#[async_trait::async_trait]
121impl Middleware for StartSession {
122 async fn handle(&self, mut request: Request, next: Next) -> Response {
123 let session = self.load(&request).await;
124 let started_empty = session.is_empty();
125
126 request.extensions_mut().insert(session.clone());
127 let response = next.run(request).await;
128
129 if started_empty && !session.is_dirty() {
133 return response;
134 }
135
136 match self.save(&session).await {
137 Ok(value) => response.with_cookie(&self.cookie(&value)),
138 Err(e) => {
139 tracing::error!(error = %e, "could not save the session");
143 response
144 }
145 }
146 }
147
148 fn name(&self) -> &'static str {
149 "StartSession"
150 }
151}
152
153impl std::fmt::Debug for StartSession {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 f.debug_struct("StartSession")
156 .field("store", &self.store.name())
157 .field("cookie", &self.config.cookie)
158 .finish()
159 }
160}
161
162pub trait SessionRequestExt {
164 fn session(&self) -> Option<&Session>;
166}
167
168impl SessionRequestExt for Request {
169 fn session(&self) -> Option<&Session> {
170 self.extension::<Session>()
171 }
172}
173
174impl Default for SessionConfig {
176 fn default() -> Self {
177 Self {
178 cookie: "rainier_session".to_string(),
179 path: "/".to_string(),
180 domain: None,
181 secure: false,
182 same_site: SameSite::Lax,
183 lifetime: chrono::Duration::hours(2),
184 }
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use crate::session::SessionData;
192 use crate::store::MemorySessionStore;
193 use rainier_middleware::Pipeline;
194 use rainier_support::BoxedFuture;
195
196 fn middleware() -> (Arc<StartSession>, Arc<MemorySessionStore>) {
197 let store = Arc::new(MemorySessionStore::default());
198 (Arc::new(StartSession::new(Arc::clone(&store) as Arc<dyn SessionStore>)), store)
199 }
200
201 async fn run<F>(start: &Arc<StartSession>, request: Request, handler: F) -> Response
202 where
203 F: Fn(Request) -> BoxedFuture<Response> + Send + Sync + 'static,
204 {
205 Pipeline::new()
206 .through_arc(Arc::clone(start) as Arc<dyn Middleware>)
207 .then(handler)
208 .run(request)
209 .await
210 }
211
212 fn session_cookie(response: &Response) -> Option<String> {
213 let header = response.header("set-cookie")?;
214 let value = header.split(';').next()?.strip_prefix("rainier_session=")?;
215 Some(value.to_string())
216 }
217
218 #[tokio::test]
219 async fn a_handler_that_writes_gets_a_cookie_and_a_stored_row() {
220 let (start, store) = middleware();
221
222 let response = run(&start, Request::builder().build(), |request| {
223 Box::pin(async move {
224 request.session().expect("a session").put("user_id", 42u64).unwrap();
225 Response::text("ok")
226 })
227 })
228 .await;
229
230 let id = session_cookie(&response).expect("a session cookie");
231 assert_eq!(store.read(&id).await.unwrap().unwrap().values["user_id"], 42);
232 }
233
234 #[tokio::test]
235 async fn an_anonymous_request_allocates_nothing() {
236 let (start, store) = middleware();
237
238 let response =
239 run(&start, Request::builder().build(), |_| Box::pin(async { Response::text("ok") }))
240 .await;
241
242 assert!(response.header("set-cookie").is_none(), "no cookie for a read-only visit");
243 assert!(store.is_empty(), "and no row");
244 }
245
246 #[tokio::test]
247 async fn a_session_survives_a_second_request() {
248 let (start, _store) = middleware();
249
250 let first = run(&start, Request::builder().build(), |request| {
251 Box::pin(async move {
252 request.session().unwrap().put("user_id", 7u64).unwrap();
253 Response::text("ok")
254 })
255 })
256 .await;
257 let id = session_cookie(&first).unwrap();
258
259 let second = run(
260 &start,
261 Request::builder().header("cookie", &format!("rainier_session={id}")).build(),
262 |request| {
263 let value = request.session().unwrap().get::<u64>("user_id");
264 Box::pin(async move { Response::text(format!("{value:?}")) })
265 },
266 )
267 .await;
268
269 let body = second.into_http().into_body().collect().await.unwrap();
270 assert_eq!(body, "Some(7)");
271 }
272
273 #[tokio::test]
274 async fn an_unknown_or_malformed_cookie_starts_a_fresh_session() {
275 let (start, store) = middleware();
276
277 for hostile in ["nonsense", "../../etc/passwd", &"f".repeat(64), &"z".repeat(64)] {
278 let response = run(
279 &start,
280 Request::builder().header("cookie", &format!("rainier_session={hostile}")).build(),
281 |request| {
282 Box::pin(async move {
283 request.session().unwrap().put("x", 1).unwrap();
284 Response::text("ok")
285 })
286 },
287 )
288 .await;
289
290 let id = session_cookie(&response).expect("a fresh cookie");
291 assert_ne!(id, hostile, "a client must not choose its own session id");
292 }
293 assert!(!store.is_empty());
294 }
295
296 #[tokio::test]
297 async fn regenerating_deletes_the_old_row() {
298 let (start, store) = middleware();
299
300 let first = run(&start, Request::builder().build(), |request| {
301 Box::pin(async move {
302 request.session().unwrap().put("user_id", 7u64).unwrap();
303 Response::text("ok")
304 })
305 })
306 .await;
307 let before = session_cookie(&first).unwrap();
308
309 let second = run(
310 &start,
311 Request::builder().header("cookie", &format!("rainier_session={before}")).build(),
312 |request| {
313 request.session().unwrap().regenerate();
315 Box::pin(async { Response::text("ok") })
316 },
317 )
318 .await;
319 let after = session_cookie(&second).unwrap();
320
321 assert_ne!(after, before);
322 assert!(store.read(&before).await.unwrap().is_none(), "the old row must not linger");
323 assert_eq!(store.read(&after).await.unwrap().unwrap().values["user_id"], 7);
324 }
325
326 #[tokio::test]
327 async fn the_cookie_is_http_only_and_same_site() {
328 let (start, _) = middleware();
329
330 let response = run(&start, Request::builder().build(), |request| {
331 Box::pin(async move {
332 request.session().unwrap().put("x", 1).unwrap();
333 Response::text("ok")
334 })
335 })
336 .await;
337
338 let header = response.header("set-cookie").unwrap();
339 assert!(header.contains("HttpOnly"), "{header}");
340 assert!(header.contains("SameSite=Lax"), "{header}");
341 assert!(header.contains("Path=/"), "{header}");
342 }
343
344 #[tokio::test]
345 async fn flash_data_is_readable_next_request_and_gone_after() {
346 let (start, _) = middleware();
347
348 let first = run(&start, Request::builder().build(), |request| {
349 Box::pin(async move {
350 request.session().unwrap().flash("status", "Saved.").unwrap();
351 Response::text("ok")
352 })
353 })
354 .await;
355 let id = session_cookie(&first).unwrap();
356 let cookie = format!("rainier_session={id}");
357
358 let read = |cookie: String| {
359 let start = Arc::clone(&start);
360 async move {
361 let response =
362 run(&start, Request::builder().header("cookie", &cookie).build(), |request| {
363 let status = request.session().unwrap().string("status");
364 Box::pin(async move { Response::text(status.unwrap_or_default()) })
365 })
366 .await;
367 String::from_utf8(
368 response.into_http().into_body().collect().await.unwrap().to_vec(),
369 )
370 .unwrap()
371 }
372 };
373
374 assert_eq!(read(cookie.clone()).await, "Saved.");
375 assert_eq!(read(cookie).await, "", "flash data must not survive twice");
376 }
377
378 #[tokio::test]
379 async fn a_route_without_the_middleware_has_no_session() {
380 let response = Pipeline::new()
381 .then(|request: Request| async move {
382 Response::text(format!("{}", request.session().is_some()))
383 })
384 .run(Request::builder().build())
385 .await;
386
387 let body = response.into_http().into_body().collect().await.unwrap();
388 assert_eq!(body, "false");
389 }
390
391 #[tokio::test]
392 async fn a_store_that_fails_does_not_fail_the_request() {
393 struct Broken;
394 impl SessionStore for Broken {
395 fn name(&self) -> &str {
396 "broken"
397 }
398 fn read<'a>(
399 &'a self,
400 _: &'a str,
401 ) -> rainier_support::BoxFuture<'a, Result<Option<SessionData>>> {
402 Box::pin(async { Err(rainier_support::Error::internal("down")) })
403 }
404 fn write<'a>(
405 &'a self,
406 _: &'a str,
407 _: &'a SessionData,
408 ) -> rainier_support::BoxFuture<'a, Result<()>> {
409 Box::pin(async { Err(rainier_support::Error::internal("down")) })
410 }
411 fn destroy<'a>(&'a self, _: &'a str) -> rainier_support::BoxFuture<'a, Result<()>> {
412 Box::pin(async { Ok(()) })
413 }
414 }
415
416 let start = Arc::new(StartSession::new(Arc::new(Broken)));
417 let response = run(&start, Request::builder().build(), |request| {
418 Box::pin(async move {
419 request.session().unwrap().put("x", 1).unwrap();
420 Response::text("the page still renders")
421 })
422 })
423 .await;
424
425 assert_eq!(response.status(), rainier_http::StatusCode::OK);
426 }
427}