diff --git a/Cargo.toml b/Cargo.toml index ec430ff..875017f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rocket_session" -version = "0.1.1" +version = "0.2.0" authors = ["Ondřej Hruška "] edition = "2018" license = "MIT" diff --git a/README.md b/README.md index f3eca94..2c05c13 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,38 @@ Adding cookie-based sessions to a rocket application is extremely simple with this crate. +Sessions are used to share data between related requests, such as user authentication, shopping basket, +form values that failed validation for re-filling, etc. + +## Configuration + The implementation is generic to support any type as session data: a custom struct, `String`, `HashMap`, or perhaps `serde_json::Value`. You're free to choose. -The session expiry time is configurable through the Fairing. When a session expires, -the data associated with it is dropped. All expired sessions may be cleared by calling `.remove_expired()` -on the `SessionStore`, which is be obtained in routes as `State`, or from a -session instance by calling `.get_store()`. +The session lifetime, cookie name, and other parameters can be configured by calling chained +methods on the fairing. When a session expires, the data associated with it is dropped. + +## Usage + +To use session in a route, first make sure you have the fairing attached by calling +`rocket.attach(Session::fairing())` at start-up, and then add something like `session : Session` +to the parameter list of your route(s). Everything else--session init, expiration, cookie +management--is done for you behind the scenes. -The session cookie is currently hardcoded to "SESSID" and contains 16 random characters. +Session data is accessed in a closure run in the session context, using the `session.tap()` +method. This closure runs inside a per-session mutex, avoiding simultaneous mutation +from different requests. Try to *avoid lengthy operations inside the closure*, +as it effectively blocks any other request to session-enabled routes by the client. -## Basic Example +Every request to a session-enabled route extends the session's lifetime to the full +configured time (defaults to 1 hour). Automatic clean-up removes expired sessions to make sure +the session list does not waste memory. + +## Examples + +(More examples are in the examples folder) + +### Basic Example This simple example uses u64 as the session variable; note that it can be a struct, map, or anything else, it just needs to implement `Send + Sync + Default`. @@ -28,7 +49,7 @@ pub type Session<'a> = rocket_session::Session<'a, u64>; fn main() { rocket::ignite() - .attach(Session::fairing(Duration::from_secs(3600))) + .attach(Session::fairing()) .mount("/", routes![index]) .launch(); } @@ -53,7 +74,10 @@ fn index(session: Session) -> String { The `.tap()` method is powerful, but sometimes you may wish for something more convenient. Here is an example of using a custom trait and the `json_dotpath` crate to implement -a polymorphic store based on serde serialization: +a polymorphic store based on serde serialization. + +Note that this approach is prone to data races, since every method contains its own `.tap()`. +It may be safer to simply call the `.dot_*()` methods manually in one shared closure. ```rust use serde_json::Value; diff --git a/examples/dog_list/main.rs b/examples/dog_list/main.rs index 976ece7..d265f43 100644 --- a/examples/dog_list/main.rs +++ b/examples/dog_list/main.rs @@ -2,9 +2,9 @@ #[macro_use] extern crate rocket; +use rocket::request::Form; use rocket::response::content::Html; use rocket::response::Redirect; -use rocket::request::Form; type Session<'a> = rocket_session::Session<'a, Vec>; @@ -18,7 +18,8 @@ fn main() { #[get("/")] fn index(session: Session) -> Html { let mut page = String::new(); - page.push_str(r#" + page.push_str( + r#"

My Dogs

@@ -27,19 +28,25 @@ fn index(session: Session) -> Html {
    - "#); + "#, + ); session.tap(|sess| { for (n, dog) in sess.iter().enumerate() { - page.push_str(&format!(r#" + page.push_str(&format!( + r#"
  • 🐶 {} Remove
  • - "#, dog, n)); + "#, + dog, n + )); } }); - page.push_str(r#" + page.push_str( + r#"
- "#); + "#, + ); Html(page) } @@ -49,8 +56,8 @@ struct AddForm { name: String, } -#[post("/add", data="")] -fn add(session: Session, dog : Form) -> Redirect { +#[post("/add", data = "")] +fn add(session: Session, dog: Form) -> Redirect { session.tap(move |sess| { sess.push(dog.into_inner().name); }); @@ -59,7 +66,7 @@ fn add(session: Session, dog : Form) -> Redirect { } #[get("/remove/")] -fn remove(session: Session, dog : usize) -> Redirect { +fn remove(session: Session, dog: usize) -> Redirect { session.tap(|sess| { if dog < sess.len() { sess.remove(dog); diff --git a/examples/visit_counter/main.rs b/examples/visit_counter/main.rs index b4e29ae..e65d053 100644 --- a/examples/visit_counter/main.rs +++ b/examples/visit_counter/main.rs @@ -6,8 +6,8 @@ #[macro_use] extern crate rocket; -use std::time::Duration; use rocket::response::content::Html; +use std::time::Duration; #[derive(Default, Clone)] struct SessionData { @@ -20,13 +20,14 @@ type Session<'a> = rocket_session::Session<'a, SessionData>; fn main() { rocket::ignite() - .attach(Session::fairing() - // 10 seconds of inactivity until session expires - // (wait 10s and refresh, the numbers will reset) - .with_lifetime(Duration::from_secs(10)) - // custom cookie name and length - .with_cookie_name("my_cookie") - .with_cookie_len(20) + .attach( + Session::fairing() + // 10 seconds of inactivity until session expires + // (wait 10s and refresh, the numbers will reset) + .with_lifetime(Duration::from_secs(10)) + // custom cookie name and length + .with_cookie_name("my_cookie") + .with_cookie_len(20), ) .mount("/", routes![index, about]) .launch(); @@ -41,14 +42,14 @@ fn index(session: Session) -> Html { session.tap(|sess| { sess.visits1 += 1; - Html(format!(r##" + Html(format!( + r##"

Home

Refreshgo to About

Visits: home {}, about {}

"##, - sess.visits1, - sess.visits2 + sess.visits1, sess.visits2 )) }) } @@ -61,7 +62,8 @@ fn about(session: Session) -> Html { sess.visits2 }); - Html(format!(r##" + Html(format!( + r##"

About

Refreshgo home diff --git a/src/lib.rs b/src/lib.rs index b23a2cd..3d8d3d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -use parking_lot::{RwLock, RwLockUpgradableReadGuard, Mutex}; +use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use rand::Rng; use rocket::{ @@ -8,18 +8,18 @@ use rocket::{ Outcome, Request, Response, Rocket, State, }; +use std::borrow::Cow; use std::collections::HashMap; +use std::fmt::{self, Display, Formatter}; use std::marker::PhantomData; use std::ops::Add; use std::time::{Duration, Instant}; -use std::borrow::Cow; -use std::fmt::{Display, Formatter, self}; /// Session store (shared state) #[derive(Debug)] pub struct SessionStore - where - D: 'static + Sync + Send + Default, +where + D: 'static + Sync + Send + Default, { /// The internally mutable map of sessions inner: RwLock>, @@ -54,15 +54,17 @@ impl Default for SessionConfig { /// Mutable object stored inside SessionStore behind a RwLock #[derive(Debug)] struct StoreInner - where - D: 'static + Sync + Send + Default { +where + D: 'static + Sync + Send + Default, +{ sessions: HashMap>>, last_expiry_sweep: Instant, } impl Default for StoreInner - where - D: 'static + Sync + Send + Default { +where + D: 'static + Sync + Send + Default, +{ fn default() -> Self { Self { sessions: Default::default(), @@ -75,8 +77,8 @@ impl Default for StoreInner /// Session, as stored in the sessions store #[derive(Debug)] struct SessionInstance - where - D: 'static + Sync + Send + Default, +where + D: 'static + Sync + Send + Default, { /// Data object data: D, @@ -108,8 +110,8 @@ impl Display for SessionID { /// when a `Session` is prepared for one of the route functions. #[derive(Debug)] pub struct Session<'a, D> - where - D: 'static + Sync + Send + Default, +where + D: 'static + Sync + Send + Default, { /// The shared state reference store: State<'a, SessionStore>, @@ -118,8 +120,8 @@ pub struct Session<'a, D> } impl<'a, 'r, D> FromRequest<'a, 'r> for Session<'a, D> - where - D: 'static + Sync + Send + Default, +where + D: 'static + Sync + Send + Default, { type Error = (); @@ -138,7 +140,8 @@ impl<'a, 'r, D> FromRequest<'a, 'r> for Session<'a, D> let expires = Instant::now().add(store.config.lifespan); - if let Some(m) = id.as_ref() + if let Some(m) = id + .as_ref() .and_then(|token| store_ug.sessions.get(token.as_str())) { // --- ID obtained from a cookie && session found in the store --- @@ -166,8 +169,7 @@ impl<'a, 'r, D> FromRequest<'a, 'r> for Session<'a, D> // Throttle by lifespan - e.g. sweep every hour if store_wg.last_expiry_sweep.elapsed() > store.config.lifespan { let now = Instant::now(); - store_wg.sessions - .retain(|_k, v| v.lock().expires > now); + store_wg.sessions.retain(|_k, v| v.lock().expires > now); store_wg.last_expiry_sweep = now; } @@ -201,8 +203,8 @@ impl<'a, 'r, D> FromRequest<'a, 'r> for Session<'a, D> } impl<'a, D> Session<'a, D> - where - D: 'static + Sync + Send + Default, +where + D: 'static + Sync + Send + Default, { /// Create the session fairing. /// @@ -231,7 +233,9 @@ impl<'a, D> Session<'a, D> // Unlock the session's mutex. // Expiry was checked and prolonged at the beginning of the request - let mut instance = store_rg.sessions.get(self.id.as_str()) + let mut instance = store_rg + .sessions + .get(self.id.as_str()) .expect("Session data unexpectedly missing") .lock(); @@ -242,16 +246,16 @@ impl<'a, D> Session<'a, D> /// Fairing struct #[derive(Default)] pub struct SessionFairing - where - D: 'static + Sync + Send + Default, +where + D: 'static + Sync + Send + Default, { config: SessionConfig, phantom: PhantomData, } impl SessionFairing - where - D: 'static + Sync + Send + Default +where + D: 'static + Sync + Send + Default, { fn new() -> Self { Self::default() @@ -291,8 +295,8 @@ impl SessionFairing } impl Fairing for SessionFairing - where - D: 'static + Sync + Send + Default, +where + D: 'static + Sync + Send + Default, { fn info(&self) -> Info { Info { @@ -314,8 +318,11 @@ impl Fairing for SessionFairing let session = request.local_cache(|| SessionID("".to_string())); if !session.0.is_empty() { - response.adjoin_header(Cookie::build(self.config.cookie_name.clone(), session.to_string()) - .path("/").finish()); + response.adjoin_header( + Cookie::build(self.config.cookie_name.clone(), session.to_string()) + .path("/") + .finish(), + ); } } }