1#![forbid(unsafe_code)]
70#![deny(rust_2018_idioms, nonstandard_style)]
71#![warn(future_incompatible, missing_docs)]
72#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
73#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
74#![cfg_attr(docsrs, feature(doc_auto_cfg))]
75
76pub mod config;
77mod middleware;
78mod session;
79mod session_ext;
80pub mod storage;
81
82pub use self::{
83 middleware::SessionMiddleware,
84 session::{Session, SessionGetError, SessionStatus},
85 session_ext::SessionExt,
86};
87
88#[cfg(test)]
89pub mod test_helpers {
90 use crate::storage::SessionStore;
91 use actix_web::cookie::Key;
92
93 pub fn key() -> Key {
95 Key::generate()
96 }
97
98 pub async fn acceptance_test_suite<F, Store>(store_builder: F, is_invalidation_supported: bool)
106 where
107 Store: SessionStore + 'static,
108 F: Fn() -> Store + Clone + Send + 'static,
109 {
110 acceptance_tests::basic_workflow(store_builder.clone()).await;
111 acceptance_tests::expiration_is_refreshed_on_changes(store_builder.clone()).await;
112 acceptance_tests::complex_workflow(store_builder.clone(), is_invalidation_supported).await;
113 acceptance_tests::lifecycle(store_builder.clone()).await;
114 acceptance_tests::guard(store_builder.clone()).await;
115 }
116
117 mod acceptance_tests {
118 use actix_web::{
119 cookie::time,
120 dev::{Service, ServiceResponse},
121 guard, middleware, test,
122 web::{self, get, post, resource, Bytes},
123 App, HttpResponse, Result,
124 };
125 use serde::{Deserialize, Serialize};
126 use serde_json::{json, Value};
127
128 use crate::config::SessionLifecycle;
129 use crate::{
130 storage::SessionStore, test_helpers::key, Session, SessionExt, SessionMiddleware,
131 };
132
133 pub(super) async fn basic_workflow<F, Store>(store_builder: F)
134 where
135 Store: SessionStore + 'static,
136 F: Fn() -> Store + Clone + Send + 'static,
137 {
138 let app = test::init_service(
139 App::new()
140 .wrap(
141 SessionMiddleware::builder(store_builder(), key())
142 .cookie_path("/test/".into())
143 .cookie_name("actix-test".into())
144 .cookie_domain(Some("localhost".into()))
145 .session_ttl(time::Duration::seconds(100))
146 .build(),
147 )
148 .service(web::resource("/").to(|ses: Session| async move {
149 ses.insert("user_id", Value::from(1));
150 ses.insert("counter", Value::from(100));
151 "test"
152 }))
153 .service(web::resource("/test/").to(|ses: Session| async move {
154 let val: usize = ses.get("counter").unwrap().unwrap();
155 format!("counter: {val}")
156 })),
157 )
158 .await;
159
160 let request = test::TestRequest::get().to_request();
161 let response = app.call(request).await.unwrap();
162 let cookie = response.get_cookie("actix-test").unwrap().clone();
163 assert_eq!(cookie.path().unwrap(), "/test/");
164
165 let request = test::TestRequest::with_uri("/test/")
166 .cookie(cookie)
167 .to_request();
168 let body = test::call_and_read_body(&app, request).await;
169 assert_eq!(body, Bytes::from_static(b"counter: 100"));
170 }
171
172 pub(super) async fn expiration_is_refreshed_on_changes<F, Store>(store_builder: F)
173 where
174 Store: SessionStore + 'static,
175 F: Fn() -> Store + Clone + Send + 'static,
176 {
177 let session_ttl = time::Duration::seconds(60);
178 let app = test::init_service(
179 App::new()
180 .wrap(
181 SessionMiddleware::builder(store_builder(), key())
182 .session_ttl(session_ttl)
183 .build(),
184 )
185 .service(web::resource("/").to(|ses: Session| async move {
186 ses.insert("counter", Value::from(100));
187 "test"
188 }))
189 .service(web::resource("/test/").to(|| async move { "no-changes-in-session" })),
190 )
191 .await;
192
193 let request = test::TestRequest::get().to_request();
194 let response = app.call(request).await.unwrap();
195 let cookie_1 = response.get_cookie("id").expect("Cookie is set");
196 assert_eq!(cookie_1.max_age(), Some(session_ttl));
197
198 let request = test::TestRequest::with_uri("/test/")
199 .cookie(cookie_1.clone())
200 .to_request();
201 let response = app.call(request).await.unwrap();
202 assert!(response.response().cookies().next().is_none());
203
204 let request = test::TestRequest::get().cookie(cookie_1).to_request();
205 let response = app.call(request).await.unwrap();
206 let cookie_2 = response.get_cookie("id").expect("Cookie is set");
207 assert_eq!(cookie_2.max_age(), Some(session_ttl));
208 }
209
210 pub(super) async fn lifecycle<F, Store>(store_builder: F)
211 where
212 Store: SessionStore + 'static,
213 F: Fn() -> Store + Clone + Send + 'static,
214 {
215 let session_ttl = time::Duration::seconds(60);
216 let app = test::init_service(
217 App::new()
218 .wrap(
219 SessionMiddleware::builder(store_builder(), key())
220 .session_ttl(session_ttl)
221 .build(),
222 )
223 .service(web::resource("/").to(|ses: Session| async move {
224 ses.insert("counter", Value::from(100));
225 "test"
226 }))
227 .service(
228 web::resource("/switch_lifecycle/").to(|ses: Session| async move {
229 ses.insert("counter", Value::from(100));
230 ses.set_lifecycle(SessionLifecycle::BrowserSession);
231 "ok"
232 }),
233 ),
234 )
235 .await;
236
237 let request = test::TestRequest::get().to_request();
239 let response = app.call(request).await.unwrap();
240 let cookie_1 = response.get_cookie("id").expect("Cookie is set");
241 assert_eq!(cookie_1.max_age(), Some(session_ttl));
242
243 let request = test::TestRequest::with_uri("/switch_lifecycle/")
245 .cookie(cookie_1.clone())
246 .to_request();
247 let response = app.call(request).await.unwrap();
248 let cookie_1 = response.get_cookie("id").expect("Cookie is set");
249 assert!(response.response().status().is_success());
250
251 let request = test::TestRequest::get().cookie(cookie_1).to_request();
253 let response = app.call(request).await.unwrap();
254 let cookie_2 = response.get_cookie("id").expect("Cookie is set");
255 assert_eq!(cookie_2.max_age(), None);
256 }
257
258 pub(super) async fn guard<F, Store>(store_builder: F)
259 where
260 Store: SessionStore + 'static,
261 F: Fn() -> Store + Clone + Send + 'static,
262 {
263 let srv = actix_test::start(move || {
264 App::new()
265 .wrap(
266 SessionMiddleware::builder(store_builder(), key())
267 .cookie_name("test-session".into())
268 .session_ttl(time::Duration::days(7))
269 .build(),
270 )
271 .wrap(middleware::Logger::default())
272 .service(resource("/").route(get().to(index)))
273 .service(resource("/do_something").route(post().to(do_something)))
274 .service(resource("/login").route(post().to(login)))
275 .service(resource("/logout").route(post().to(logout)))
276 .service(
277 web::scope("/protected")
278 .guard(guard::fn_guard(|g| {
279 g.get_session().get::<String>("user_id").unwrap().is_some()
280 }))
281 .service(resource("/count").route(get().to(count))),
282 )
283 });
284
285 let req_1 = srv.get("/protected/count").send();
288 let resp_1 = req_1.await.unwrap();
289 assert!(!resp_1.status().is_success());
290
291 let req_2 = srv.post("/login").send_json(&json!({"user_id": "ferris"}));
295 let resp_2 = req_2.await.unwrap();
296 let cookie_1 = resp_2
297 .cookies()
298 .unwrap()
299 .clone()
300 .into_iter()
301 .find(|c| c.name() == "test-session")
302 .unwrap();
303
304 let req_3 = srv.post("/do_something").cookie(cookie_1.clone()).send();
309 let mut resp_3 = req_3.await.unwrap();
310 let result_3 = resp_3.json::<IndexResponse>().await.unwrap();
311 assert_eq!(
312 result_3,
313 IndexResponse {
314 user_id: Some("ferris".into()),
315 counter: 1
316 }
317 );
318 let cookie_2 = resp_3
319 .cookies()
320 .unwrap()
321 .clone()
322 .into_iter()
323 .find(|c| c.name() == "test-session")
324 .unwrap();
325
326 let req_4 = srv.get("/protected/count").cookie(cookie_2.clone()).send();
329 let mut resp_4 = req_4.await.unwrap();
330 let result_4 = resp_4.json::<IndexResponse>().await.unwrap();
331 assert_eq!(
332 result_4,
333 IndexResponse {
334 user_id: Some("ferris".into()),
335 counter: 1
336 }
337 );
338 }
339
340 pub(super) async fn complex_workflow<F, Store>(
341 store_builder: F,
342 is_invalidation_supported: bool,
343 ) where
344 Store: SessionStore + 'static,
345 F: Fn() -> Store + Clone + Send + 'static,
346 {
347 let session_ttl = time::Duration::days(7);
348 let srv = actix_test::start(move || {
349 App::new()
350 .wrap(
351 SessionMiddleware::builder(store_builder(), key())
352 .cookie_name("test-session".into())
353 .session_ttl(session_ttl)
354 .build(),
355 )
356 .wrap(middleware::Logger::default())
357 .service(resource("/").route(get().to(index)))
358 .service(resource("/do_something").route(post().to(do_something)))
359 .service(resource("/login").route(post().to(login)))
360 .service(resource("/logout").route(post().to(logout)))
361 });
362
363 let req_1a = srv.get("/").send();
367 let mut resp_1 = req_1a.await.unwrap();
368 assert!(resp_1.cookies().unwrap().is_empty());
369 let result_1 = resp_1.json::<IndexResponse>().await.unwrap();
370 assert_eq!(
371 result_1,
372 IndexResponse {
373 user_id: None,
374 counter: 0
375 }
376 );
377
378 let req_2 = srv.post("/do_something").send();
383 let mut resp_2 = req_2.await.unwrap();
384 let result_2 = resp_2.json::<IndexResponse>().await.unwrap();
385 assert_eq!(
386 result_2,
387 IndexResponse {
388 user_id: None,
389 counter: 1
390 }
391 );
392 let cookie_1 = resp_2
393 .cookies()
394 .unwrap()
395 .clone()
396 .into_iter()
397 .find(|c| c.name() == "test-session")
398 .unwrap();
399 assert_eq!(cookie_1.max_age(), Some(session_ttl));
400
401 let req_3 = srv.get("/").cookie(cookie_1.clone()).send();
405 let mut resp_3 = req_3.await.unwrap();
406 assert!(resp_3.cookies().unwrap().is_empty());
407 let result_3 = resp_3.json::<IndexResponse>().await.unwrap();
408 assert_eq!(
409 result_3,
410 IndexResponse {
411 user_id: None,
412 counter: 1
413 }
414 );
415
416 let req_4 = srv.post("/do_something").cookie(cookie_1.clone()).send();
421 let mut resp_4 = req_4.await.unwrap();
422 let result_4 = resp_4.json::<IndexResponse>().await.unwrap();
423 assert_eq!(
424 result_4,
425 IndexResponse {
426 user_id: None,
427 counter: 2
428 }
429 );
430 let cookie_2 = resp_4
431 .cookies()
432 .unwrap()
433 .clone()
434 .into_iter()
435 .find(|c| c.name() == "test-session")
436 .unwrap();
437 assert_eq!(cookie_2.max_age(), cookie_1.max_age());
438
439 let req_5 = srv
443 .post("/login")
444 .cookie(cookie_2.clone())
445 .send_json(&json!({"user_id": "ferris"}));
446 let mut resp_5 = req_5.await.unwrap();
447 let cookie_3 = resp_5
448 .cookies()
449 .unwrap()
450 .clone()
451 .into_iter()
452 .find(|c| c.name() == "test-session")
453 .unwrap();
454 assert_ne!(cookie_2.value(), cookie_3.value());
455
456 let result_5 = resp_5.json::<IndexResponse>().await.unwrap();
457 assert_eq!(
458 result_5,
459 IndexResponse {
460 user_id: Some("ferris".into()),
461 counter: 2
462 }
463 );
464
465 let req_6 = srv.get("/").cookie(cookie_3.clone()).send();
468 let mut resp_6 = req_6.await.unwrap();
469 let result_6 = resp_6.json::<IndexResponse>().await.unwrap();
470 assert_eq!(
471 result_6,
472 IndexResponse {
473 user_id: Some("ferris".into()),
474 counter: 2
475 }
476 );
477
478 let req_7 = srv.post("/do_something").cookie(cookie_3.clone()).send();
482 let mut resp_7 = req_7.await.unwrap();
483 let result_7 = resp_7.json::<IndexResponse>().await.unwrap();
484 assert_eq!(
485 result_7,
486 IndexResponse {
487 user_id: Some("ferris".into()),
488 counter: 3
489 }
490 );
491
492 let req_8 = srv.get("/").cookie(cookie_2.clone()).send();
496 let mut resp_8 = req_8.await.unwrap();
497 if is_invalidation_supported {
498 assert!(resp_8.cookies().unwrap().is_empty());
499 let result_8 = resp_8.json::<IndexResponse>().await.unwrap();
500 assert_eq!(
501 result_8,
502 IndexResponse {
503 user_id: None,
504 counter: 0
505 }
506 );
507 } else {
508 let result_8 = resp_8.json::<IndexResponse>().await.unwrap();
509 assert_eq!(
510 result_8,
511 IndexResponse {
512 user_id: None,
513 counter: 2
514 }
515 );
516 }
517
518 let req_9 = srv.post("/logout").cookie(cookie_3.clone()).send();
522 let resp_9 = req_9.await.unwrap();
523 let cookie_3 = resp_9
524 .cookies()
525 .unwrap()
526 .clone()
527 .into_iter()
528 .find(|c| c.name() == "test-session")
529 .unwrap();
530 assert_eq!(0, cookie_3.max_age().map(|t| t.whole_seconds()).unwrap());
531 assert_eq!("/", cookie_3.path().unwrap());
532
533 let req_10 = srv.get("/").cookie(cookie_3.clone()).send();
537 let mut resp_10 = req_10.await.unwrap();
538 if is_invalidation_supported {
539 assert!(resp_10.cookies().unwrap().is_empty());
540 }
541 let result_10 = resp_10.json::<IndexResponse>().await.unwrap();
542 assert_eq!(
543 result_10,
544 IndexResponse {
545 user_id: None,
546 counter: 0
547 }
548 );
549 }
550
551 #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
552 pub struct IndexResponse {
553 user_id: Option<String>,
554 counter: i32,
555 }
556
557 async fn index(session: Session) -> Result<HttpResponse> {
558 let user_id: Option<String> = session.get::<String>("user_id").unwrap();
559 let counter: i32 = session
560 .get::<i32>("counter")
561 .unwrap_or(Some(0))
562 .unwrap_or(0);
563
564 Ok(HttpResponse::Ok().json(&IndexResponse { user_id, counter }))
565 }
566
567 async fn do_something(session: Session) -> Result<HttpResponse> {
568 let user_id: Option<String> = session.get::<String>("user_id").unwrap();
569 let counter: i32 = session
570 .get::<i32>("counter")
571 .unwrap_or(Some(0))
572 .map_or(1, |inner| inner + 1);
573 session.insert("counter", Value::from(counter));
574
575 Ok(HttpResponse::Ok().json(&IndexResponse { user_id, counter }))
576 }
577
578 async fn count(session: Session) -> Result<HttpResponse> {
579 let user_id: Option<String> = session.get::<String>("user_id").unwrap();
580 let counter: i32 = session.get::<i32>("counter").unwrap().unwrap();
581
582 Ok(HttpResponse::Ok().json(&IndexResponse { user_id, counter }))
583 }
584
585 #[derive(Deserialize)]
586 struct Identity {
587 user_id: String,
588 }
589
590 async fn login(user_id: web::Json<Identity>, session: Session) -> Result<HttpResponse> {
591 let id = user_id.into_inner().user_id;
592 session.insert("user_id", Value::from(id.clone()));
593 session.renew();
594
595 let counter: i32 = session
596 .get::<i32>("counter")
597 .unwrap_or(Some(0))
598 .unwrap_or(0);
599
600 Ok(HttpResponse::Ok().json(&IndexResponse {
601 user_id: Some(id),
602 counter,
603 }))
604 }
605
606 async fn logout(session: Session) -> Result<HttpResponse> {
607 let id: Option<String> = session.get("user_id")?;
608
609 let body = if let Some(id) = id {
610 session.purge();
611 format!("Logged out: {id}")
612 } else {
613 "Could not log out anonymous user".to_owned()
614 };
615
616 Ok(HttpResponse::Ok().body(body))
617 }
618
619 trait ServiceResponseExt {
620 fn get_cookie(&self, cookie_name: &str) -> Option<actix_web::cookie::Cookie<'_>>;
621 }
622
623 impl ServiceResponseExt for ServiceResponse {
624 fn get_cookie(&self, cookie_name: &str) -> Option<actix_web::cookie::Cookie<'_>> {
625 self.response().cookies().find(|c| c.name() == cookie_name)
626 }
627 }
628 }
629}