From ca9368d8201cf282faac82c0411a0f96d9b45626 Mon Sep 17 00:00:00 2001 From: Charles Edward Gagnon Date: Fri, 27 Sep 2024 16:46:02 -0400 Subject: [PATCH] more progress --- Cargo.toml | 4 +- README.md | 4 - src/lib.rs | 2 - src/service.rs | 258 ++++++++------------------------- src/session.rs | 46 +++++- tower-sessions-core/Cargo.toml | 2 - tower-sessions-core/src/lib.rs | 2 - 7 files changed, 99 insertions(+), 219 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6bff033..5386681 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,6 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = ["axum-core"] -axum-core = ["tower-sessions-core/axum-core"] # memory-store = ["tower-sessions-memory-store"] signed = ["tower-cookies/signed"] private = ["tower-cookies/private"] @@ -54,6 +53,7 @@ async-trait = "0.1.74" [dependencies] async-trait = { workspace = true } +axum-core = { version = "0.4", optional = true } http = "1.0" pin-project-lite = "0.2.14" tokio = { version = "1.32.0", features = ["sync"] } @@ -63,7 +63,7 @@ tower-sessions-core = { workspace = true } # tower-sessions-memory-store = { workspace = true, optional = true } tracing = { version = "0.1.40", features = ["log"] } time = { version = "0.3.29", features = ["serde"] } -cookie = "0.18.1" +cookie = { version = "0.18.1", features = ["percent-encode"] } [dev-dependencies] async-trait = "0.1.74" diff --git a/README.md b/README.md index 8ce58d1..8dc632c 100644 --- a/README.md +++ b/README.md @@ -130,10 +130,6 @@ You can find this [example][counter-example] as well as other example projects i > [!NOTE] > See the [crate documentation][docs] for more usage information. -## 🦺 Safety - -This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in 100% safe Rust. - ## 🛟 Getting Help We've put together a number of [examples][examples] to help get you started. You're also welcome to [open a discussion](https://github.com/maxcountryman/tower-sessions/discussions/new?category=q-a) and ask additional questions you might have. diff --git a/src/lib.rs b/src/lib.rs index 9cf3a74..c2e109a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -434,10 +434,8 @@ missing_debug_implementations )] #![deny(missing_docs)] -#![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_cfg))] -pub use tower_cookies::cookie; pub use tower_sessions_core::session_store; #[doc(inline)] pub use tower_sessions_core::{ diff --git a/src/service.rs b/src/service.rs index ec0372a..70ac0f6 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,7 +1,7 @@ //! A middleware that provides [`Session`] as a request extension. use std::{ - borrow::Cow, future::Future, + marker::PhantomData, pin::Pin, sync::{Arc, Mutex}, task::{Context, Poll}, @@ -15,18 +15,21 @@ use tower_layer::Layer; use tower_service::Service; use tower_sessions_core::{expires::Expiry, id::Id}; -use crate::{LazySession, SessionStore}; +use crate::{ + session::{SessionUpdate, Updater}, + LazySession, SessionStore, +}; #[derive(Debug, Copy, Clone)] pub struct SessionConfig<'a> { - name: &'a str, - http_only: bool, - same_site: SameSite, - expiry: Expiry, - secure: bool, - path: &'a str, - domain: Option<&'a str>, - always_save: bool, + pub name: &'a str, + pub http_only: bool, + pub same_site: SameSite, + pub expiry: Expiry, + pub secure: bool, + pub path: &'a str, + pub domain: Option<&'a str>, + pub always_save: bool, } impl<'a> SessionConfig<'a> { @@ -70,34 +73,34 @@ impl Default for SessionConfig<'static> { /// A middleware that provides [`Session`] as a request extension. #[derive(Debug, Clone)] -pub struct SessionManager { +pub struct SessionManager { inner: S, store: Store, config: SessionConfig<'static>, + _record: PhantomData, } -impl SessionManager -where - S: Service, - Store: SessionStore + Clone, -{ +impl SessionManager { /// Create a new [`SessionManager`]. - pub fn new(inner: S, session_store: Store) -> Self { + pub fn new(inner: S, store: Store, config: SessionConfig<'static>) -> Self { Self { inner, - store: Arc::new(session_store), - config: Default::default(), + store, + config, + _record: PhantomData, } } } -impl + Clone> Service> - for SessionManager +impl Service> + for SessionManager where S: Service, Response = Response> + Clone + Send + 'static, S::Future: Send, ReqBody: Send + 'static, ResBody: Default + Send, + Store: SessionStore + Clone + 'static, + Record: Send + Sync + 'static, { type Response = S::Response; type Error = S::Error; @@ -124,7 +127,7 @@ where .find(|cookie| cookie.name() == self.config.name); let id = session_cookie - .map(|cookie| { + .and_then(|cookie| { cookie .value() .parse::() @@ -135,14 +138,13 @@ where ) }) .ok() - }) - .flatten(); + }); let updater = Arc::new(Mutex::new(None)); let session = LazySession { id, store: self.store.clone(), - data: std::marker::PhantomData, - updater, + data: std::marker::PhantomData::, + updater: Arc::clone(&updater), }; req.extensions_mut().insert(session); @@ -155,7 +157,8 @@ where pin_project! { #[derive(Debug, Clone)] - struct ResponseFuture { + /// The future returned by [`SessionManager`]. + pub struct ResponseFuture { #[pin] inner: F, updater: Updater, @@ -175,7 +178,13 @@ where Poll::Pending => return Poll::Pending, }; - let that = this.updater.lock().unwrap(); + match *this.updater.lock().expect("updater should not be poisoned") { + Some(SessionUpdate::Delete) => todo!(), + Some(SessionUpdate::Set(id)) => todo!(), + None => {} + }; + + Poll::Ready(resp) } } @@ -285,169 +294,15 @@ where /// A layer for providing [`Session`] as a request extension. #[derive(Debug, Clone)] -pub struct SessionManagerLayer { - session_store: Arc, - session_config: SessionConfig<'static>, - cookie_controller: C, -} - -impl SessionManagerLayer { - /// Configures the name of the cookie used for the session. - /// The default value is `"id"`. - /// - /// # Examples - /// - /// ```rust - /// use tower_sessions::{MemoryStore, SessionManagerLayer}; - /// - /// let session_store = MemoryStore::default(); - /// let session_service = SessionManagerLayer::new(session_store).with_name("my.sid"); - /// ``` - pub fn with_name>>(mut self, name: &'static str) -> Self { - self.session_config.name = name.into(); - self - } - - /// Configures the `"HttpOnly"` attribute of the cookie used for the - /// session. - /// - /// # ⚠️ **Warning: Cross-site scripting risk** - /// - /// Applications should generally **not** override the default value of - /// `true`. If you do, you are exposing your application to increased risk - /// of cookie theft via techniques like cross-site scripting. - /// - /// # Examples - /// - /// ```rust - /// use tower_sessions::{MemoryStore, SessionManagerLayer}; - /// - /// let session_store = MemoryStore::default(); - /// let session_service = SessionManagerLayer::new(session_store).with_http_only(true); - /// ``` - pub fn with_http_only(mut self, http_only: bool) -> Self { - self.session_config.http_only = http_only; - self - } - - /// Configures the `"SameSite"` attribute of the cookie used for the - /// session. - /// The default value is [`SameSite::Strict`]. - /// - /// # Examples - /// - /// ```rust - /// use tower_sessions::{cookie::SameSite, MemoryStore, SessionManagerLayer}; - /// - /// let session_store = MemoryStore::default(); - /// let session_service = SessionManagerLayer::new(session_store).with_same_site(SameSite::Lax); - /// ``` - pub fn with_same_site(mut self, same_site: SameSite) -> Self { - self.session_config.same_site = same_site; - self - } - - /// Configures the `"Max-Age"` attribute of the cookie used for the session. - /// The default value is `None`. - /// - /// # Examples - /// - /// ```rust - /// use time::Duration; - /// use tower_sessions::{Expiry, MemoryStore, SessionManagerLayer}; - /// - /// let session_store = MemoryStore::default(); - /// let session_expiry = Expiry::OnInactivity(Duration::hours(1)); - /// let session_service = SessionManagerLayer::new(session_store).with_expiry(session_expiry); - /// ``` - pub fn with_expiry(mut self, expiry: Expiry) -> Self { - self.session_config.expiry = Some(expiry); - self - } - - /// Configures the `"Secure"` attribute of the cookie used for the session. - /// The default value is `true`. - /// - /// # Examples - /// - /// ```rust - /// use tower_sessions::{MemoryStore, SessionManagerLayer}; - /// - /// let session_store = MemoryStore::default(); - /// let session_service = SessionManagerLayer::new(session_store).with_secure(true); - /// ``` - pub fn with_secure(mut self, secure: bool) -> Self { - self.session_config.secure = secure; - self - } - - /// Configures the `"Path"` attribute of the cookie used for the session. - /// The default value is `"/"`. - /// - /// # Examples - /// - /// ```rust - /// use tower_sessions::{MemoryStore, SessionManagerLayer}; - /// - /// let session_store = MemoryStore::default(); - /// let session_service = SessionManagerLayer::new(session_store).with_path("/some/path"); - /// ``` - pub fn with_path>>(mut self, path: P) -> Self { - self.session_config.path = path.into(); - self - } - - /// Configures the `"Domain"` attribute of the cookie used for the session. - /// The default value is `None`. - /// - /// # Examples - /// - /// ```rust - /// use tower_sessions::{MemoryStore, SessionManagerLayer}; - /// - /// let session_store = MemoryStore::default(); - /// let session_service = SessionManagerLayer::new(session_store).with_domain("localhost"); - /// ``` - pub fn with_domain>>(mut self, domain: D) -> Self { - self.session_config.domain = Some(domain.into()); - self - } - - /// Configures whether unmodified session should be saved on read or not. - /// When the value is `true`, the session will be saved even if it was not - /// changed. - /// - /// This is useful when you want to reset [`Session`] expiration time - /// on any valid request at the cost of higher [`SessionStore`] write - /// activity and transmitting `set-cookie` header with each response. - /// - /// It makes sense to use this setting with relative session expiration - /// values, such as `Expiry::OnInactivity(Duration)`. This setting will - /// _not_ cause session id to be cycled on save. - /// - /// The default value is `false`. - /// - /// # Examples - /// - /// ```rust - /// use time::Duration; - /// use tower_sessions::{Expiry, MemoryStore, SessionManagerLayer}; - /// - /// let session_store = MemoryStore::default(); - /// let session_expiry = Expiry::OnInactivity(Duration::hours(1)); - /// let session_service = SessionManagerLayer::new(session_store) - /// .with_expiry(session_expiry) - /// .with_always_save(true); - /// ``` - pub fn with_always_save(mut self, always_save: bool) -> Self { - self.session_config.always_save = always_save; - self - } +pub struct SessionManagerLayer { + store: Store, + config: SessionConfig<'static>, + _record: PhantomData, } -impl SessionManagerLayer { +impl SessionManagerLayer { /// Create a new [`SessionManagerLayer`] with the provided session store - /// and default cookie configuration. + /// and configuration. /// /// # Examples /// @@ -457,26 +312,29 @@ impl SessionManagerLayer { /// let session_store = MemoryStore::default(); /// let session_service = SessionManagerLayer::new(session_store); /// ``` - pub fn new(session_store: Store) -> Self { - let session_config = SessionConfig::default(); - + pub fn new(store: Store, config: SessionConfig<'static>) -> Self { Self { - session_store: Arc::new(session_store), - session_config, - cookie_controller: PlaintextCookie, + store, + config, + _record: PhantomData, } } } -impl Layer for SessionManagerLayer { - type Service = CookieManager>; +impl Layer for SessionManagerLayer +where + Record: Default + Send + Sync, + Store: SessionStore + Clone, +{ + type Service = SessionManager; fn layer(&self, inner: S) -> Self::Service { - let session_manager = SessionManager { + SessionManager { inner, - session_store: self.session_store.clone(), - session_config: self.session_config.clone(), - }; + store: self.store.clone(), + config: self.config, + _record: PhantomData, + } } } diff --git a/src/session.rs b/src/session.rs index 227d5a0..2945d60 100644 --- a/src/session.rs +++ b/src/session.rs @@ -8,20 +8,25 @@ use std::{ sync::{Arc, Mutex}, }; -use axum_core::extract::{FromRef, FromRequestParts}; +use axum_core::{ + body::Body, + extract::FromRequestParts, + response::{IntoResponse, Response}, +}; + use http::request::Parts; -use std::convert::Infallible; // TODO: Remove send + sync bounds on `R` once return type notation is stable. use tower_sessions_core::{id::Id, SessionStore}; -enum SessionUpdate { +#[derive(Debug, Clone, Copy)] +pub(crate) enum SessionUpdate { Delete, Set(Id), } -type Updater = Arc>>; +pub(crate) type Updater = Arc>>; /// A session that is lazily loaded, and that can be extracted from a request. /// @@ -41,6 +46,20 @@ pub struct LazySession { pub(crate) updater: Updater, } +impl Clone for LazySession +where + Store: Clone, +{ + fn clone(&self) -> Self { + Self { + id: self.id.clone(), + store: self.store.clone(), + data: self.data.clone(), + updater: self.updater.clone(), + } + } +} + impl Debug for LazySession { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Session") @@ -103,16 +122,27 @@ impl Display for NoMiddleware { impl Error for NoMiddleware {} +impl IntoResponse for NoMiddleware { + fn into_response(self) -> Response { + let mut resp = Response::new(Body::from(self.to_string())); + *resp.status_mut() = http::StatusCode::INTERNAL_SERVER_ERROR; + resp + } +} + #[async_trait::async_trait] impl FromRequestParts for LazySession where State: Send + Sync, - Record: Send + Sync, - Store: SessionStore + FromRef, + Record: Send + Sync + 'static, + Store: SessionStore + 'static, { type Rejection = NoMiddleware; - async fn from_request_parts(parts: &mut Parts, state: &State) -> Result { + async fn from_request_parts( + parts: &mut Parts, + _state: &State, + ) -> Result { let session = parts .extensions .remove::>() @@ -234,6 +264,8 @@ impl> DataMut { let self_ = ManuallyDrop::new(self); // Safety: https://internals.rust-lang.org/t/destructuring-droppable-structs/20993/16, // we need to destructure the struct but it implements `Drop`. + // This is safe because the ptr comes from a reference: the pointer is valid for reads + // and the value is properly initialized. Ok(Some(unsafe { std::ptr::read(&self_.session as *const _) })) } else { let _ = ManuallyDrop::new(self); diff --git a/tower-sessions-core/Cargo.toml b/tower-sessions-core/Cargo.toml index 3d4abc3..070f488 100644 --- a/tower-sessions-core/Cargo.toml +++ b/tower-sessions-core/Cargo.toml @@ -10,13 +10,11 @@ repository.workspace = true [features] default = [] -axum-core = ["dep:axum-core", "dep:http"] deletion-task = ["dep:tokio", "tokio/time"] id-access = [] [dependencies] time = "0.3.36" -axum-core = { version = "0.4", optional = true } http = { version = "1.1.0", optional = true } base64 = "0.22.0" futures = { version = "0.3.28", default-features = false, features = [ diff --git a/tower-sessions-core/src/lib.rs b/tower-sessions-core/src/lib.rs index 0646980..f46e2a5 100644 --- a/tower-sessions-core/src/lib.rs +++ b/tower-sessions-core/src/lib.rs @@ -3,8 +3,6 @@ pub use self::session_store::SessionStore; pub use self::id::Id; pub use self::expires::Expiry; -#[cfg(feature = "axum-core")] -#[cfg_attr(docsrs, doc(cfg(feature = "axum-core")))] pub mod session_store; pub mod expires; pub mod id;