1#![cfg_attr(test, allow(clippy::unwrap_used))]
2#![doc(html_favicon_url = "https://salvo.rs/favicon-32x32.png")]
63#![doc(html_logo_url = "https://salvo.rs/images/logo.svg")]
64#![cfg_attr(docsrs, feature(doc_cfg))]
65
66use std::fmt::{self, Formatter};
67use std::time::Duration;
68
69use cookie::{Cookie, Key, KeyError, SameSite};
70use salvo_core::http::SecureCookiePolicy;
71use salvo_core::{Depot, Error, FlowCtrl, Handler, Request, Response, async_trait};
72use saysion::base64::Engine as _;
73use saysion::base64::engine::general_purpose;
74use saysion::hmac::{Hmac, Mac};
75use saysion::sha2::Sha256;
76pub use saysion::{CookieStore, MemoryStore, Session, SessionStore};
77
78pub const SESSION_KEY: &str = "::salvo::session";
80const BASE64_DIGEST_LEN: usize = 44;
81
82pub trait SessionDepotExt {
84 fn set_session(&mut self, session: Session) -> &mut Self;
86 fn take_session(&mut self) -> Option<Session>;
88 fn session(&self) -> Option<&Session>;
90 fn session_mut(&mut self) -> Option<&mut Session>;
92}
93
94impl SessionDepotExt for Depot {
95 #[inline]
96 fn set_session(&mut self, session: Session) -> &mut Self {
97 self.insert(SESSION_KEY, session);
98 self
99 }
100 #[inline]
101 fn take_session(&mut self) -> Option<Session> {
102 self.remove(SESSION_KEY)
103 .and_then(|v| v.downcast::<Session>().ok())
104 .map(|v| *v)
105 }
106 #[inline]
107 fn session(&self) -> Option<&Session> {
108 self.get(SESSION_KEY).ok()
109 }
110 #[inline]
111 fn session_mut(&mut self) -> Option<&mut Session> {
112 self.get_mut(SESSION_KEY).ok()
113 }
114}
115
116pub struct HandlerBuilder<S> {
118 store: S,
119 cookie_path: String,
120 cookie_name: String,
121 cookie_domain: Option<String>,
122 session_ttl: Option<Duration>,
123 save_unchanged: bool,
124 same_site_policy: SameSite,
125 secure_cookie_policy: SecureCookiePolicy,
126 key: Key,
127 fallback_keys: Vec<Key>,
128}
129impl<S> fmt::Debug for HandlerBuilder<S>
130where
131 S: SessionStore + fmt::Debug,
132{
133 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
134 f.debug_struct("HandlerBuilder")
135 .field("store", &self.store)
136 .field("cookie_path", &self.cookie_path)
137 .field("cookie_name", &self.cookie_name)
138 .field("cookie_domain", &self.cookie_domain)
139 .field("session_ttl", &self.session_ttl)
140 .field("same_site_policy", &self.same_site_policy)
141 .field("secure_cookie_policy", &self.secure_cookie_policy)
142 .field("key", &"..")
143 .field("fallback_keys", &"..")
144 .field("save_unchanged", &self.save_unchanged)
145 .finish()
146 }
147}
148
149impl<S> HandlerBuilder<S>
150where
151 S: SessionStore,
152{
153 #[inline]
167 #[must_use]
168 pub fn new(store: S, secret: &[u8]) -> Self {
169 Self::try_new(store, secret).expect("secret key must be at least 64 bytes")
170 }
171
172 #[inline]
185 pub fn try_new(store: S, secret: &[u8]) -> Result<Self, KeyError> {
186 let key = Key::try_from(secret)?;
187 Ok(Self {
188 store,
189 save_unchanged: true,
190 cookie_path: "/".into(),
191 cookie_name: "salvo.session.id".into(),
192 cookie_domain: None,
193 same_site_policy: SameSite::Lax,
194 secure_cookie_policy: SecureCookiePolicy::AutoFromScheme,
195 session_ttl: Some(Duration::from_secs(24 * 60 * 60)),
196 key,
197 fallback_keys: vec![],
198 })
199 }
200
201 #[inline]
205 #[must_use]
206 pub fn cookie_path(mut self, cookie_path: impl Into<String>) -> Self {
207 self.cookie_path = cookie_path.into();
208 self
209 }
210
211 #[inline]
217 #[must_use]
218 pub fn session_ttl(mut self, session_ttl: Option<Duration>) -> Self {
219 self.session_ttl = session_ttl;
220 self
221 }
222
223 #[inline]
229 #[must_use]
230 pub fn cookie_name(mut self, cookie_name: impl Into<String>) -> Self {
231 self.cookie_name = cookie_name.into();
232 self
233 }
234
235 #[inline]
245 #[must_use]
246 pub fn save_unchanged(mut self, value: bool) -> Self {
247 self.save_unchanged = value;
248 self
249 }
250
251 #[inline]
256 #[must_use]
257 pub fn same_site_policy(mut self, policy: SameSite) -> Self {
258 self.same_site_policy = policy;
259 self
260 }
261
262 #[inline]
267 #[must_use]
268 pub fn secure_cookie(mut self, secure: bool) -> Self {
269 self.secure_cookie_policy = SecureCookiePolicy::from_bool(secure);
270 self
271 }
272
273 #[inline]
278 #[must_use]
279 pub fn secure_cookie_policy(mut self, policy: SecureCookiePolicy) -> Self {
280 self.secure_cookie_policy = policy;
281 self
282 }
283
284 #[inline]
286 #[must_use]
287 pub fn cookie_domain(mut self, cookie_domain: impl AsRef<str>) -> Self {
288 self.cookie_domain = Some(cookie_domain.as_ref().to_owned());
289 self
290 }
291 #[inline]
293 #[must_use]
294 pub fn fallback_keys(mut self, keys: Vec<impl Into<Key>>) -> Self {
295 self.fallback_keys = keys.into_iter().map(|s| s.into()).collect();
296 self
297 }
298
299 #[inline]
301 #[must_use]
302 pub fn add_fallback_key(mut self, key: impl Into<Key>) -> Self {
303 self.fallback_keys.push(key.into());
304 self
305 }
306
307 pub fn build(self) -> Result<SessionHandler<S>, Error> {
309 let Self {
310 store,
311 save_unchanged,
312 cookie_path,
313 cookie_name,
314 cookie_domain,
315 session_ttl,
316 same_site_policy,
317 secure_cookie_policy,
318 key,
319 fallback_keys,
320 } = self;
321 let hmac = Hmac::<Sha256>::new_from_slice(key.signing())
322 .map_err(|_| Error::Other("invalid key length".into()))?;
323 let fallback_hmacs = fallback_keys
324 .iter()
325 .map(|key| Hmac::<Sha256>::new_from_slice(key.signing()))
326 .collect::<Result<Vec<_>, _>>()
327 .map_err(|_| Error::Other("invalid key length".into()))?;
328 Ok(SessionHandler {
329 store,
330 save_unchanged,
331 cookie_path,
332 cookie_name,
333 cookie_domain,
334 session_ttl,
335 same_site_policy,
336 secure_cookie_policy,
337 hmac,
338 fallback_hmacs,
339 })
340 }
341}
342
343pub struct SessionHandler<S> {
345 store: S,
346 cookie_path: String,
347 cookie_name: String,
348 cookie_domain: Option<String>,
349 session_ttl: Option<Duration>,
350 save_unchanged: bool,
351 same_site_policy: SameSite,
352 secure_cookie_policy: SecureCookiePolicy,
353 hmac: Hmac<Sha256>,
354 fallback_hmacs: Vec<Hmac<Sha256>>,
355}
356impl<S> fmt::Debug for SessionHandler<S>
357where
358 S: SessionStore + fmt::Debug,
359{
360 #[inline]
361 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
362 f.debug_struct("SessionHandler")
363 .field("store", &self.store)
364 .field("cookie_path", &self.cookie_path)
365 .field("cookie_name", &self.cookie_name)
366 .field("cookie_domain", &self.cookie_domain)
367 .field("session_ttl", &self.session_ttl)
368 .field("same_site_policy", &self.same_site_policy)
369 .field("secure_cookie_policy", &self.secure_cookie_policy)
370 .field("key", &"..")
371 .field("fallback_keys", &"..")
372 .field("save_unchanged", &self.save_unchanged)
373 .finish()
374 }
375}
376#[async_trait]
377impl<S> Handler for SessionHandler<S>
378where
379 S: SessionStore + Send + Sync + 'static,
380{
381 async fn handle(
382 &self,
383 req: &mut Request,
384 depot: &mut Depot,
385 res: &mut Response,
386 ctrl: &mut FlowCtrl,
387 ) {
388 let cookie = req.cookies().get(&self.cookie_name);
389 let cookie_value = cookie.and_then(|cookie| self.verify_signature(cookie.value()).ok());
390
391 let mut session = self.load_or_create(cookie_value).await;
392
393 if let Some(ttl) = self.session_ttl {
394 session.expire_in(ttl);
395 }
396
397 depot.set_session(session);
398
399 ctrl.call_next(req, depot, res).await;
400 if ctrl.is_ceased() {
401 return;
402 }
403
404 let session = depot.take_session().expect("session should exist in depot");
405 if session.is_destroyed() {
406 if let Err(e) = self.store.destroy_session(session).await {
407 tracing::error!(error = ?e, "unable to destroy session");
408 }
409 let secure_cookie =
410 self.same_site_policy == SameSite::None || self.secure_cookie_policy.is_secure(req);
411 res.remove_cookie_with(self.build_removal_cookie(secure_cookie));
412 } else if self.save_unchanged || session.data_changed() {
413 match self.store.store_session(session).await {
414 Ok(cookie_value) => {
415 if let Some(cookie_value) = cookie_value {
416 let secure_cookie = self.same_site_policy == SameSite::None
417 || self.secure_cookie_policy.is_secure(req);
418 let cookie = self.build_cookie(secure_cookie, cookie_value);
419 res.add_cookie(cookie);
420 }
421 }
422 Err(e) => {
423 tracing::error!(error = ?e, "store session error");
424 }
425 }
426 }
427 }
428}
429
430impl<S> SessionHandler<S>
431where
432 S: SessionStore + Send + Sync + 'static,
433{
434 pub fn builder(store: S, secret: &[u8]) -> HandlerBuilder<S> {
436 HandlerBuilder::new(store, secret)
437 }
438 #[inline]
439 async fn load_or_create(&self, cookie_value: Option<String>) -> Session {
440 let session = match cookie_value {
441 Some(cookie_value) => self.store.load_session(cookie_value).await.ok().flatten(),
442 None => None,
443 };
444
445 session
446 .and_then(|session| session.validate())
447 .unwrap_or_default()
448 }
449 fn verify_signature(&self, cookie_value: &str) -> Result<String, Error> {
455 let Some((digest_str, value)) = cookie_value.split_at_checked(BASE64_DIGEST_LEN) else {
461 return Err(Error::Other(
462 "length of value is <= BASE64_DIGEST_LEN".into(),
463 ));
464 };
465 let digest = general_purpose::STANDARD
466 .decode(digest_str)
467 .map_err(|_| Error::Other("bad base64 digest".into()))?;
468
469 let mut hmac = self.hmac.clone();
471 hmac.update(value.as_bytes());
472 if hmac.verify_slice(&digest).is_ok() {
473 return Ok(value.to_owned());
474 }
475 for hmac in &self.fallback_hmacs {
476 let mut hmac = hmac.clone();
477 hmac.update(value.as_bytes());
478 if hmac.verify_slice(&digest).is_ok() {
479 return Ok(value.to_owned());
480 }
481 }
482 Err(Error::Other("value did not verify".into()))
483 }
484 fn build_cookie(&self, secure: bool, cookie_value: String) -> Cookie<'static> {
485 let mut cookie = Cookie::build((self.cookie_name.clone(), cookie_value))
486 .http_only(true)
487 .same_site(self.same_site_policy)
488 .secure(secure)
489 .path(self.cookie_path.clone())
490 .build();
491
492 if let Some(ttl) = self.session_ttl {
493 cookie.set_expires(Some((std::time::SystemTime::now() + ttl).into()));
494 }
495
496 if let Some(cookie_domain) = self.cookie_domain.clone() {
497 cookie.set_domain(cookie_domain)
498 }
499
500 self.sign_cookie(&mut cookie);
501
502 cookie
503 }
504 fn build_removal_cookie(&self, secure: bool) -> Cookie<'static> {
505 let mut cookie = Cookie::build((self.cookie_name.clone(), ""))
506 .http_only(true)
507 .same_site(self.same_site_policy)
508 .secure(secure)
509 .path(self.cookie_path.clone())
510 .build();
511
512 if let Some(cookie_domain) = self.cookie_domain.clone() {
513 cookie.set_domain(cookie_domain)
514 }
515
516 cookie
517 }
518 fn sign_cookie(&self, cookie: &mut Cookie<'_>) {
522 let mut mac = self.hmac.clone();
524 mac.update(cookie.value().as_bytes());
525
526 let mut new_value = general_purpose::STANDARD.encode(mac.finalize().into_bytes());
528 new_value.push_str(cookie.value());
529 cookie.set_value(new_value);
530 }
531}
532
533#[cfg(test)]
534mod tests {
535 use salvo_core::http::Method;
536 use salvo_core::http::header::*;
537 use salvo_core::prelude::*;
538 use salvo_core::test::{ResponseExt, TestClient};
539
540 use super::*;
541
542 #[test]
543 fn test_session_data() {
544 let builder = SessionHandler::builder(
545 saysion::CookieStore,
546 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
547 )
548 .cookie_domain("test.domain")
549 .cookie_name("test_cookie")
550 .cookie_path("/abc")
551 .same_site_policy(SameSite::Strict)
552 .session_ttl(Some(Duration::from_secs(30)));
553 assert!(format!("{builder:?}").contains("test_cookie"));
554
555 let handler = builder.build().unwrap();
556 assert!(format!("{handler:?}").contains("test_cookie"));
557 assert_eq!(handler.cookie_domain, Some("test.domain".into()));
558 assert_eq!(handler.cookie_name, "test_cookie");
559 assert_eq!(handler.cookie_path, "/abc");
560 assert_eq!(handler.same_site_policy, SameSite::Strict);
561 assert_eq!(handler.session_ttl, Some(Duration::from_secs(30)));
562 }
563
564 #[tokio::test]
565 async fn test_session_login() {
566 #[handler]
567 pub async fn login(req: &mut Request, depot: &mut Depot, res: &mut Response) {
568 if req.method() == Method::POST {
569 let mut session = Session::new();
570 session
571 .insert("username", req.form::<String>("username").await.unwrap())
572 .unwrap();
573 depot.set_session(session);
574 res.render(Redirect::other("/"));
575 } else {
576 res.render(Text::Html("login page"));
577 }
578 }
579
580 #[handler]
581 pub async fn logout(depot: &mut Depot, res: &mut Response) {
582 if let Some(session) = depot.session_mut() {
583 session.remove("username");
584 }
585 res.render(Redirect::other("/"));
586 }
587
588 #[handler]
589 pub async fn home(depot: &mut Depot, res: &mut Response) {
590 let mut content = r#"home"#.into();
591 if let Some(session) = depot.session_mut()
592 && let Some(username) = session.get::<String>("username")
593 {
594 content = username;
595 }
596 res.render(Text::Html(content));
597 }
598
599 let session_handler = SessionHandler::builder(
600 MemoryStore::new(),
601 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
602 )
603 .build()
604 .unwrap();
605 let router = Router::new()
606 .hoop(session_handler)
607 .get(home)
608 .push(Router::with_path("login").get(login).post(login))
609 .push(Router::with_path("logout").get(logout));
610 let service = Service::new(router);
611
612 let response = TestClient::post("http://127.0.0.1:8698/login")
613 .raw_form("username=salvo")
614 .send(&service)
615 .await;
616 assert_eq!(response.status_code, Some(StatusCode::SEE_OTHER));
617 let cookie = response.headers().get(SET_COOKIE).unwrap();
618
619 let mut response = TestClient::get("http://127.0.0.1:8698/")
620 .add_header(COOKIE, cookie, true)
621 .send(&service)
622 .await;
623 assert_eq!(response.take_string().await.unwrap(), "salvo");
624
625 let response = TestClient::get("http://127.0.0.1:8698/logout")
626 .send(&service)
627 .await;
628 assert_eq!(response.status_code, Some(StatusCode::SEE_OTHER));
629
630 let mut response = TestClient::get("http://127.0.0.1:8698/")
631 .send(&service)
632 .await;
633 assert_eq!(response.take_string().await.unwrap(), "home");
634 }
635
636 #[test]
637 fn test_verify_signature_rejects_non_ascii_at_digest_boundary() {
638 let handler = SessionHandler::builder(
642 MemoryStore::new(),
643 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
644 )
645 .build()
646 .unwrap();
647
648 let malformed = "A".repeat(43) + "Ä" + "rest";
649 assert_eq!(malformed.len(), 43 + 2 + 4);
650 assert!(handler.verify_signature(&malformed).is_err());
651 }
652
653 #[test]
654 fn test_verify_signature_rejects_short_value() {
655 let handler = SessionHandler::builder(
656 MemoryStore::new(),
657 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
658 )
659 .build()
660 .unwrap();
661
662 assert!(handler.verify_signature("too-short").is_err());
663 assert!(handler.verify_signature("").is_err());
664 }
665
666 #[test]
668 fn test_handler_builder_new() {
669 let builder = HandlerBuilder::new(
670 MemoryStore::new(),
671 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
672 );
673 assert_eq!(builder.cookie_path, "/");
674 assert_eq!(builder.cookie_name, "salvo.session.id");
675 assert!(builder.cookie_domain.is_none());
676 assert!(builder.save_unchanged);
677 assert_eq!(builder.same_site_policy, SameSite::Lax);
678 assert_eq!(
679 builder.secure_cookie_policy,
680 SecureCookiePolicy::AutoFromScheme
681 );
682 assert_eq!(builder.session_ttl, Some(Duration::from_secs(24 * 60 * 60)));
683 }
684
685 #[test]
686 fn test_handler_builder_cookie_path() {
687 let builder = HandlerBuilder::new(
688 MemoryStore::new(),
689 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
690 )
691 .cookie_path("/custom");
692 assert_eq!(builder.cookie_path, "/custom");
693 }
694
695 #[test]
696 fn test_handler_builder_session_ttl() {
697 let builder = HandlerBuilder::new(
698 MemoryStore::new(),
699 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
700 )
701 .session_ttl(Some(Duration::from_secs(3600)));
702 assert_eq!(builder.session_ttl, Some(Duration::from_secs(3600)));
703 }
704
705 #[test]
706 fn test_handler_builder_session_ttl_none() {
707 let builder = HandlerBuilder::new(
708 MemoryStore::new(),
709 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
710 )
711 .session_ttl(None);
712 assert!(builder.session_ttl.is_none());
713 }
714
715 #[test]
716 fn test_handler_builder_cookie_name() {
717 let builder = HandlerBuilder::new(
718 MemoryStore::new(),
719 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
720 )
721 .cookie_name("my_session");
722 assert_eq!(builder.cookie_name, "my_session");
723 }
724
725 #[test]
726 fn test_handler_builder_save_unchanged() {
727 let builder = HandlerBuilder::new(
728 MemoryStore::new(),
729 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
730 )
731 .save_unchanged(false);
732 assert!(!builder.save_unchanged);
733 }
734
735 #[test]
736 fn test_handler_builder_same_site_policy() {
737 let builder = HandlerBuilder::new(
738 MemoryStore::new(),
739 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
740 )
741 .same_site_policy(SameSite::None);
742 assert_eq!(builder.same_site_policy, SameSite::None);
743 }
744
745 #[test]
746 fn test_handler_builder_cookie_domain() {
747 let builder = HandlerBuilder::new(
748 MemoryStore::new(),
749 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
750 )
751 .cookie_domain("example.com");
752 assert_eq!(builder.cookie_domain, Some("example.com".to_owned()));
753 }
754
755 #[test]
756 fn test_handler_builder_fallback_keys() {
757 let builder = HandlerBuilder::new(
758 MemoryStore::new(),
759 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
760 )
761 .fallback_keys(vec![Key::from(
762 b"fallbackfallbackfallbackfallbackfallbackfallbackfallbackfallback" as &[u8],
763 )]);
764 assert_eq!(builder.fallback_keys.len(), 1);
765 }
766
767 #[test]
768 fn test_handler_builder_add_fallback_key() {
769 let builder = HandlerBuilder::new(
770 MemoryStore::new(),
771 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
772 )
773 .add_fallback_key(Key::from(
774 b"fallbackfallbackfallbackfallbackfallbackfallbackfallbackfallback" as &[u8],
775 ))
776 .add_fallback_key(Key::from(
777 b"anotherkeyanotherkeyanotherkeyanotherkeyanotherkeyanotherkeyanot" as &[u8],
778 ));
779 assert_eq!(builder.fallback_keys.len(), 2);
780 }
781
782 #[test]
783 fn test_handler_builder_build() {
784 let handler = HandlerBuilder::new(
785 MemoryStore::new(),
786 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
787 )
788 .build()
789 .unwrap();
790 assert_eq!(handler.cookie_path, "/");
791 assert_eq!(handler.cookie_name, "salvo.session.id");
792 }
793
794 #[test]
795 fn test_handler_builder_debug() {
796 let builder = HandlerBuilder::new(
797 MemoryStore::new(),
798 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
799 );
800 let debug_str = format!("{builder:?}");
801 assert!(debug_str.contains("HandlerBuilder"));
802 assert!(debug_str.contains("cookie_path"));
803 assert!(debug_str.contains("cookie_name"));
804 }
805
806 #[test]
807 fn test_handler_builder_chain() {
808 let handler = HandlerBuilder::new(
809 MemoryStore::new(),
810 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
811 )
812 .cookie_path("/app")
813 .cookie_name("app_session")
814 .cookie_domain("app.example.com")
815 .session_ttl(Some(Duration::from_secs(7200)))
816 .save_unchanged(false)
817 .same_site_policy(SameSite::Strict)
818 .secure_cookie(true)
819 .build()
820 .unwrap();
821
822 assert_eq!(handler.cookie_path, "/app");
823 assert_eq!(handler.cookie_name, "app_session");
824 assert_eq!(handler.cookie_domain, Some("app.example.com".to_owned()));
825 assert_eq!(handler.session_ttl, Some(Duration::from_secs(7200)));
826 assert!(!handler.save_unchanged);
827 assert_eq!(handler.same_site_policy, SameSite::Strict);
828 assert_eq!(handler.secure_cookie_policy, SecureCookiePolicy::Always);
829 }
830
831 #[test]
832 fn test_handler_builder_secure_cookie_policy() {
833 let builder = HandlerBuilder::new(
834 MemoryStore::new(),
835 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
836 )
837 .secure_cookie_policy(SecureCookiePolicy::Never);
838
839 assert_eq!(builder.secure_cookie_policy, SecureCookiePolicy::Never);
840 }
841
842 #[test]
844 fn test_session_handler_builder() {
845 let handler = SessionHandler::builder(
846 MemoryStore::new(),
847 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
848 )
849 .build()
850 .unwrap();
851 assert_eq!(handler.cookie_name, "salvo.session.id");
852 }
853
854 #[test]
855 fn test_session_handler_debug() {
856 let handler = SessionHandler::builder(
857 MemoryStore::new(),
858 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
859 )
860 .build()
861 .unwrap();
862 let debug_str = format!("{handler:?}");
863 assert!(debug_str.contains("SessionHandler"));
864 assert!(debug_str.contains("cookie_path"));
865 }
866
867 #[test]
869 fn test_depot_set_session() {
870 let mut depot = Depot::new();
871 let session = Session::new();
872 depot.set_session(session);
873 assert!(depot.session().is_some());
874 }
875
876 #[test]
877 fn test_depot_take_session() {
878 let mut depot = Depot::new();
879 let session = Session::new();
880 depot.set_session(session);
881 let taken = depot.take_session();
882 assert!(taken.is_some());
883 assert!(depot.session().is_none());
884 }
885
886 #[test]
887 fn test_depot_session() {
888 let mut depot = Depot::new();
889 assert!(depot.session().is_none());
890
891 depot.set_session(Session::new());
892 assert!(depot.session().is_some());
893 }
894
895 #[test]
896 fn test_depot_session_mut() {
897 let mut depot = Depot::new();
898 depot.set_session(Session::new());
899
900 if let Some(session) = depot.session_mut() {
901 session.insert("key", "value").unwrap();
902 }
903
904 if let Some(session) = depot.session() {
905 assert_eq!(session.get::<String>("key"), Some("value".to_owned()));
906 }
907 }
908
909 #[tokio::test]
911 async fn test_session_destroy() {
912 #[handler]
913 pub async fn destroy_session(depot: &mut Depot, res: &mut Response) {
914 if let Some(session) = depot.session_mut() {
915 session.destroy();
916 }
917 res.render("destroyed");
918 }
919
920 let session_handler = SessionHandler::builder(
921 MemoryStore::new(),
922 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
923 )
924 .build()
925 .unwrap();
926
927 let router = Router::new()
928 .hoop(session_handler)
929 .push(Router::with_path("destroy").get(destroy_session));
930 let service = Service::new(router);
931
932 let response = TestClient::get("http://127.0.0.1:8698/destroy")
933 .send(&service)
934 .await;
935 assert_eq!(response.status_code, Some(StatusCode::OK));
936 }
937
938 #[tokio::test]
939 async fn test_session_destroy_preserves_cookie_path_and_domain() {
940 #[handler]
941 pub async fn destroy_session(depot: &mut Depot) {
942 if let Some(session) = depot.session_mut() {
943 session.destroy();
944 }
945 }
946
947 let session_handler = SessionHandler::builder(
948 MemoryStore::new(),
949 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
950 )
951 .cookie_domain("example.com")
952 .cookie_path("/app")
953 .build()
954 .unwrap();
955
956 let router = Router::new()
957 .hoop(session_handler)
958 .push(Router::with_path("destroy").get(destroy_session));
959 let service = Service::new(router);
960
961 let response = TestClient::get("http://127.0.0.1:8698/destroy")
962 .add_header(COOKIE, "salvo.session.id=stale", true)
963 .send(&service)
964 .await;
965
966 let cookie = response
967 .headers()
968 .get(SET_COOKIE)
969 .expect("set-cookie header")
970 .to_str()
971 .expect("set-cookie should be valid");
972 assert!(cookie.starts_with("salvo.session.id="));
973 assert!(cookie.contains("Max-Age=0"));
974 assert!(cookie.contains("Path=/app"));
975 assert!(cookie.contains("Domain=example.com"));
976 }
977
978 #[tokio::test]
979 async fn test_session_can_force_secure_cookie_behind_tls_terminator() {
980 #[handler]
981 pub async fn index() -> &'static str {
982 "ok"
983 }
984
985 let session_handler = SessionHandler::builder(
986 MemoryStore::new(),
987 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
988 )
989 .secure_cookie(true)
990 .build()
991 .unwrap();
992
993 let router = Router::new().hoop(session_handler).get(index);
994 let service = Service::new(router);
995
996 let response = TestClient::get("http://127.0.0.1:8698/")
997 .send(&service)
998 .await;
999 let cookie = response
1000 .headers()
1001 .get(SET_COOKIE)
1002 .unwrap()
1003 .to_str()
1004 .unwrap();
1005 assert!(cookie.contains("Secure"));
1006 }
1007
1008 #[tokio::test]
1009 async fn test_session_same_site_none_forces_secure_cookie() {
1010 #[handler]
1011 pub async fn index() -> &'static str {
1012 "ok"
1013 }
1014
1015 let session_handler = SessionHandler::builder(
1016 MemoryStore::new(),
1017 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
1018 )
1019 .same_site_policy(SameSite::None)
1020 .secure_cookie(false)
1021 .build()
1022 .unwrap();
1023
1024 let router = Router::new().hoop(session_handler).get(index);
1025 let service = Service::new(router);
1026
1027 let response = TestClient::get("http://127.0.0.1:8698/")
1028 .send(&service)
1029 .await;
1030 let cookie = response
1031 .headers()
1032 .get(SET_COOKIE)
1033 .unwrap()
1034 .to_str()
1035 .unwrap();
1036 assert!(cookie.contains("SameSite=None"));
1037 assert!(cookie.contains("Secure"));
1038 }
1039
1040 #[tokio::test]
1042 async fn test_session_save_unchanged_false() {
1043 #[handler]
1044 pub async fn no_change(depot: &mut Depot, res: &mut Response) {
1045 let _ = depot.session();
1047 res.render("no change");
1048 }
1049
1050 let session_handler = SessionHandler::builder(
1051 MemoryStore::new(),
1052 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
1053 )
1054 .save_unchanged(false)
1055 .build()
1056 .unwrap();
1057
1058 let router = Router::new()
1059 .hoop(session_handler)
1060 .push(Router::with_path("nochange").get(no_change));
1061 let service = Service::new(router);
1062
1063 let response = TestClient::get("http://127.0.0.1:8698/nochange")
1064 .send(&service)
1065 .await;
1066 assert_eq!(response.status_code, Some(StatusCode::OK));
1067 }
1070
1071 #[tokio::test]
1073 async fn test_session_data_persistence() {
1074 #[handler]
1075 pub async fn set_data(depot: &mut Depot, res: &mut Response) {
1076 if let Some(session) = depot.session_mut() {
1077 session.insert("counter", 1).unwrap();
1078 }
1079 res.render("set");
1080 }
1081
1082 #[handler]
1083 pub async fn get_data(depot: &mut Depot, res: &mut Response) {
1084 let counter = if let Some(session) = depot.session() {
1085 session.get::<i32>("counter").unwrap_or(0)
1086 } else {
1087 0
1088 };
1089 res.render(format!("{counter}"));
1090 }
1091
1092 let session_handler = SessionHandler::builder(
1093 MemoryStore::new(),
1094 b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
1095 )
1096 .build()
1097 .unwrap();
1098
1099 let router = Router::new()
1100 .hoop(session_handler)
1101 .push(Router::with_path("set").get(set_data))
1102 .push(Router::with_path("get").get(get_data));
1103 let service = Service::new(router);
1104
1105 let response = TestClient::get("http://127.0.0.1:8698/set")
1107 .send(&service)
1108 .await;
1109 let cookie = response.headers().get(SET_COOKIE).unwrap();
1110
1111 let mut response = TestClient::get("http://127.0.0.1:8698/get")
1113 .add_header(COOKIE, cookie, true)
1114 .send(&service)
1115 .await;
1116 assert_eq!(response.take_string().await.unwrap(), "1");
1117 }
1118
1119 #[test]
1121 fn test_session_key_constant() {
1122 assert_eq!(SESSION_KEY, "::salvo::session");
1123 }
1124
1125 #[test]
1127 fn test_base64_digest_len() {
1128 assert_eq!(BASE64_DIGEST_LEN, 44);
1129 }
1130}