From ee517f33d845ca277ee98c925f7599cc80826eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Tue, 31 Dec 2019 03:54:24 +0100 Subject: [PATCH] updated readme --- Cargo.toml | 11 ++- src/lib.rs | 210 ++++++++++++++++++++++++++++++++++++++++++++++++- src/session.rs | 206 ------------------------------------------------ 3 files changed, 218 insertions(+), 209 deletions(-) delete mode 100644 src/session.rs diff --git a/Cargo.toml b/Cargo.toml index a012dfd..ec430ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,17 @@ [package] name = "rocket_session" -version = "0.1.0" +version = "0.1.1" authors = ["Ondřej Hruška "] edition = "2018" +license = "MIT" +description = "Rocket.rs plug-in for cookie-based sessions holding arbitrary data" +repository = "https://git.ondrovo.com/packages/rocket_session" +readme = "README.md" +keywords = ["rocket", "rocket-rs", "session", "cookie"] +categories = [ + "web-programming", + "web-programming::http-server" +] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/lib.rs b/src/lib.rs index 6e813f2..177a68f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,208 @@ -mod session; -pub use session::Session; +use parking_lot::RwLock; +use rand::Rng; + +use rocket::{ + fairing::{self, Fairing, Info}, + http::{Cookie, Status}, + request::FromRequest, + Outcome, Request, Response, Rocket, State, +}; + +use std::collections::HashMap; +use std::marker::PhantomData; +use std::ops::Add; +use std::time::{Duration, Instant}; + +const SESSION_COOKIE: &str = "SESSID"; +const SESSION_ID_LEN: usize = 16; + +/// Session, as stored in the sessions store +#[derive(Debug)] +struct SessionInstance + where + D: 'static + Sync + Send + Default, +{ + /// Data object + data: D, + /// Expiry + expires: Instant, +} + +/// Session store (shared state) +#[derive(Default, Debug)] +pub struct SessionStore + where + D: 'static + Sync + Send + Default, +{ + /// The internaly mutable map of sessions + inner: RwLock>>, + /// Sessions lifespan + lifespan: Duration, +} + +impl SessionStore + where + D: 'static + Sync + Send + Default, +{ + /// Remove all expired sessions + pub fn remove_expired(&self) { + let now = Instant::now(); + self.inner.write().retain(|_k, v| v.expires > now); + } +} + +/// Session ID newtype for rocket's "local_cache" +#[derive(PartialEq, Hash, Clone, Debug)] +struct SessionID(String); + +impl<'a, 'r> FromRequest<'a, 'r> for &'a SessionID { + type Error = (); + + fn from_request(request: &'a Request<'r>) -> Outcome { + Outcome::Success(request.local_cache(|| { + if let Some(cookie) = request.cookies().get(SESSION_COOKIE) { + SessionID(cookie.value().to_string()) // FIXME avoid cloning (cow?) + } else { + SessionID( + rand::thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(16) + .collect(), + ) + } + })) + } +} + +/// Session instance +#[derive(Debug)] +pub struct Session<'a, D> + where + D: 'static + Sync + Send + Default, +{ + /// The shared state reference + store: State<'a, SessionStore>, + /// Session ID + id: &'a SessionID, +} + +impl<'a, 'r, D> FromRequest<'a, 'r> for Session<'a, D> + where + D: 'static + Sync + Send + Default, +{ + type Error = (); + + fn from_request(request: &'a Request<'r>) -> Outcome { + Outcome::Success(Session { + id: request.local_cache(|| { + if let Some(cookie) = request.cookies().get(SESSION_COOKIE) { + SessionID(cookie.value().to_string()) + } else { + SessionID( + rand::thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(SESSION_ID_LEN) + .collect(), + ) + } + }), + store: request.guard().unwrap(), + }) + } +} + +impl<'a, D> Session<'a, D> + where + D: 'static + Sync + Send + Default, +{ + /// Get the fairing object + pub fn fairing(lifespan: Duration) -> impl Fairing { + SessionFairing:: { + lifespan, + _phantom: PhantomData, + } + } + + /// Access the session store + pub fn get_store(&self) -> &SessionStore { + &self.store + } + + /// Set the session object to its default state + pub fn reset(&self) { + self.tap(|m| { + *m = D::default(); + }) + } + + /// Renew the session without changing any data + pub fn renew(&self) { + self.tap(|_| ()) + } + + /// Run a closure with a mutable reference to the session object. + /// The closure's return value is send to the caller. + pub fn tap(&self, func: impl FnOnce(&mut D) -> T) -> T { + let mut wg = self.store.inner.write(); + if let Some(instance) = wg.get_mut(&self.id.0) { + // wipe session data if expired + if instance.expires <= Instant::now() { + instance.data = D::default(); + } + // update expiry timestamp + instance.expires = Instant::now().add(self.store.lifespan); + + func(&mut instance.data) + } else { + // no object in the store yet, start fresh + let mut data = D::default(); + let result = func(&mut data); + wg.insert( + self.id.0.clone(), + SessionInstance { + data, + expires: Instant::now().add(self.store.lifespan), + }, + ); + result + } + } +} + +/// Fairing struct +struct SessionFairing + where + D: 'static + Sync + Send + Default, +{ + lifespan: Duration, + _phantom: PhantomData, +} + +impl Fairing for SessionFairing + where + D: 'static + Sync + Send + Default, +{ + fn info(&self) -> Info { + Info { + name: "Session", + kind: fairing::Kind::Attach | fairing::Kind::Response, + } + } + + fn on_attach(&self, rocket: Rocket) -> Result { + // install the store singleton + Ok(rocket.manage(SessionStore:: { + inner: Default::default(), + lifespan: self.lifespan, + })) + } + + fn on_response<'r>(&self, request: &'r Request, response: &mut Response) { + // send the session cookie, if session started + let session = request.local_cache(|| SessionID("".to_string())); + + if !session.0.is_empty() { + response.adjoin_header(Cookie::build(SESSION_COOKIE, session.0.clone()).finish()); + } + } +} diff --git a/src/session.rs b/src/session.rs deleted file mode 100644 index 583a011..0000000 --- a/src/session.rs +++ /dev/null @@ -1,206 +0,0 @@ -use parking_lot::RwLock; -use rand::Rng; - -use rocket::{ - fairing::{self, Fairing, Info}, - http::{Cookie, Status}, - request::FromRequest, - Outcome, Request, Response, Rocket, State, -}; - -use std::collections::HashMap; -use std::marker::PhantomData; -use std::ops::Add; -use std::time::{Duration, Instant}; - -const SESSION_COOKIE: &str = "SESSID"; -const SESSION_ID_LEN: usize = 16; - -/// Session, as stored in the sessions store -#[derive(Debug)] -struct SessionInstance -where - D: 'static + Sync + Send + Default, -{ - /// Data object - data: D, - /// Expiry - expires: Instant, -} - -/// Session store (shared state) -#[derive(Default, Debug)] -pub struct SessionStore -where - D: 'static + Sync + Send + Default, -{ - /// The internaly mutable map of sessions - inner: RwLock>>, - /// Sessions lifespan - lifespan: Duration, -} - -impl SessionStore -where - D: 'static + Sync + Send + Default, -{ - /// Remove all expired sessions - pub fn remove_expired(&self) { - let now = Instant::now(); - self.inner.write().retain(|_k, v| v.expires > now); - } -} - -/// Session ID newtype for rocket's "local_cache" -#[derive(PartialEq, Hash, Clone, Debug)] -struct SessionID(String); - -impl<'a, 'r> FromRequest<'a, 'r> for &'a SessionID { - type Error = (); - - fn from_request(request: &'a Request<'r>) -> Outcome { - Outcome::Success(request.local_cache(|| { - if let Some(cookie) = request.cookies().get(SESSION_COOKIE) { - SessionID(cookie.value().to_string()) // FIXME avoid cloning (cow?) - } else { - SessionID( - rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(16) - .collect(), - ) - } - })) - } -} - -/// Session instance -#[derive(Debug)] -pub struct Session<'a, D> -where - D: 'static + Sync + Send + Default, -{ - /// The shared state reference - store: State<'a, SessionStore>, - /// Session ID - id: &'a SessionID, -} - -impl<'a, 'r, D> FromRequest<'a, 'r> for Session<'a, D> -where - D: 'static + Sync + Send + Default, -{ - type Error = (); - - fn from_request(request: &'a Request<'r>) -> Outcome { - Outcome::Success(Session { - id: request.local_cache(|| { - if let Some(cookie) = request.cookies().get(SESSION_COOKIE) { - SessionID(cookie.value().to_string()) - } else { - SessionID( - rand::thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(SESSION_ID_LEN) - .collect(), - ) - } - }), - store: request.guard().unwrap(), - }) - } -} - -impl<'a, D> Session<'a, D> -where - D: 'static + Sync + Send + Default, -{ - /// Get the fairing object - pub fn fairing(lifespan: Duration) -> impl Fairing { - SessionFairing:: { - lifespan, - _phantom: PhantomData, - } - } - - /// Access the session store - pub fn get_store(&self) -> &SessionStore { - &self.store - } - - /// Set the session object to its default state - pub fn reset(&self) { - self.tap(|m| { - *m = D::default(); - }) - } - - /// Renew the session without changing any data - pub fn renew(&self) { - self.tap(|_| ()) - } - - /// Run a closure with a mutable reference to the session object. - /// The closure's return value is send to the caller. - pub fn tap(&self, func: impl FnOnce(&mut D) -> T) -> T { - let mut wg = self.store.inner.write(); - if let Some(instance) = wg.get_mut(&self.id.0) { - // wipe session data if expired - if instance.expires <= Instant::now() { - instance.data = D::default(); - } - // update expiry timestamp - instance.expires = Instant::now().add(self.store.lifespan); - - func(&mut instance.data) - } else { - // no object in the store yet, start fresh - let mut data = D::default(); - let result = func(&mut data); - wg.insert( - self.id.0.clone(), - SessionInstance { - data, - expires: Instant::now().add(self.store.lifespan), - }, - ); - result - } - } -} - -/// Fairing struct -struct SessionFairing -where - D: 'static + Sync + Send + Default, -{ - lifespan: Duration, - _phantom: PhantomData, -} - -impl Fairing for SessionFairing -where - D: 'static + Sync + Send + Default, -{ - fn info(&self) -> Info { - Info { - name: "Session", - kind: fairing::Kind::Attach | fairing::Kind::Response, - } - } - - fn on_attach(&self, rocket: Rocket) -> Result { - Ok(rocket.manage(SessionStore:: { - inner: Default::default(), - lifespan: self.lifespan, - })) - } - - fn on_response<'r>(&self, request: &'r Request, response: &mut Response) { - let session = request.local_cache(|| SessionID("".to_string())); - - if !session.0.is_empty() { - response.adjoin_header(Cookie::build(SESSION_COOKIE, session.0.clone()).finish()); - } - } -}