version bump, comments, improve examples, update readme

refactor
Ondřej Hruška 5 years ago
parent cde08fe788
commit f8d5445cdc
Signed by untrusted user: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 2
      Cargo.toml
  2. 40
      README.md
  3. 27
      examples/dog_list/main.rs
  4. 26
      examples/visit_counter/main.rs
  5. 65
      src/lib.rs

@ -1,6 +1,6 @@
[package] [package]
name = "rocket_session" name = "rocket_session"
version = "0.1.1" version = "0.2.0"
authors = ["Ondřej Hruška <ondra@ondrovo.com>"] authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
edition = "2018" edition = "2018"
license = "MIT" license = "MIT"

@ -2,17 +2,38 @@
Adding cookie-based sessions to a rocket application is extremely simple with this crate. 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`, 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. `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 session lifetime, cookie name, and other parameters can be configured by calling chained
the data associated with it is dropped. All expired sessions may be cleared by calling `.remove_expired()` methods on the fairing. When a session expires, the data associated with it is dropped.
on the `SessionStore`, which is be obtained in routes as `State<SessionStore>`, or from a
session instance by calling `.get_store()`. ## 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, 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`. it just needs to implement `Send + Sync + Default`.
@ -28,7 +49,7 @@ pub type Session<'a> = rocket_session::Session<'a, u64>;
fn main() { fn main() {
rocket::ignite() rocket::ignite()
.attach(Session::fairing(Duration::from_secs(3600))) .attach(Session::fairing())
.mount("/", routes![index]) .mount("/", routes![index])
.launch(); .launch();
} }
@ -53,7 +74,10 @@ fn index(session: Session) -> String {
The `.tap()` method is powerful, but sometimes you may wish for something more convenient. 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 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 ```rust
use serde_json::Value; use serde_json::Value;

@ -2,9 +2,9 @@
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;
use rocket::request::Form;
use rocket::response::content::Html; use rocket::response::content::Html;
use rocket::response::Redirect; use rocket::response::Redirect;
use rocket::request::Form;
type Session<'a> = rocket_session::Session<'a, Vec<String>>; type Session<'a> = rocket_session::Session<'a, Vec<String>>;
@ -18,7 +18,8 @@ fn main() {
#[get("/")] #[get("/")]
fn index(session: Session) -> Html<String> { fn index(session: Session) -> Html<String> {
let mut page = String::new(); let mut page = String::new();
page.push_str(r#" page.push_str(
r#"
<!DOCTYPE html> <!DOCTYPE html>
<h1>My Dogs</h1> <h1>My Dogs</h1>
@ -27,19 +28,25 @@ fn index(session: Session) -> Html<String> {
</form> </form>
<ul> <ul>
"#); "#,
);
session.tap(|sess| { session.tap(|sess| {
for (n, dog) in sess.iter().enumerate() { for (n, dog) in sess.iter().enumerate() {
page.push_str(&format!(r#" page.push_str(&format!(
r#"
<li>&#x1F436; {} <a href="/remove/{}">Remove</a></li> <li>&#x1F436; {} <a href="/remove/{}">Remove</a></li>
"#, dog, n)); "#,
dog, n
));
} }
}); });
page.push_str(r#" page.push_str(
r#"
</ul> </ul>
"#); "#,
);
Html(page) Html(page)
} }
@ -49,8 +56,8 @@ struct AddForm {
name: String, name: String,
} }
#[post("/add", data="<dog>")] #[post("/add", data = "<dog>")]
fn add(session: Session, dog : Form<AddForm>) -> Redirect { fn add(session: Session, dog: Form<AddForm>) -> Redirect {
session.tap(move |sess| { session.tap(move |sess| {
sess.push(dog.into_inner().name); sess.push(dog.into_inner().name);
}); });
@ -59,7 +66,7 @@ fn add(session: Session, dog : Form<AddForm>) -> Redirect {
} }
#[get("/remove/<dog>")] #[get("/remove/<dog>")]
fn remove(session: Session, dog : usize) -> Redirect { fn remove(session: Session, dog: usize) -> Redirect {
session.tap(|sess| { session.tap(|sess| {
if dog < sess.len() { if dog < sess.len() {
sess.remove(dog); sess.remove(dog);

@ -6,8 +6,8 @@
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;
use std::time::Duration;
use rocket::response::content::Html; use rocket::response::content::Html;
use std::time::Duration;
#[derive(Default, Clone)] #[derive(Default, Clone)]
struct SessionData { struct SessionData {
@ -20,13 +20,14 @@ type Session<'a> = rocket_session::Session<'a, SessionData>;
fn main() { fn main() {
rocket::ignite() rocket::ignite()
.attach(Session::fairing() .attach(
// 10 seconds of inactivity until session expires Session::fairing()
// (wait 10s and refresh, the numbers will reset) // 10 seconds of inactivity until session expires
.with_lifetime(Duration::from_secs(10)) // (wait 10s and refresh, the numbers will reset)
// custom cookie name and length .with_lifetime(Duration::from_secs(10))
.with_cookie_name("my_cookie") // custom cookie name and length
.with_cookie_len(20) .with_cookie_name("my_cookie")
.with_cookie_len(20),
) )
.mount("/", routes![index, about]) .mount("/", routes![index, about])
.launch(); .launch();
@ -41,14 +42,14 @@ fn index(session: Session) -> Html<String> {
session.tap(|sess| { session.tap(|sess| {
sess.visits1 += 1; sess.visits1 += 1;
Html(format!(r##" Html(format!(
r##"
<!DOCTYPE html> <!DOCTYPE html>
<h1>Home</h1> <h1>Home</h1>
<a href="/">Refresh</a> &bull; <a href="/about/">go to About</a> <a href="/">Refresh</a> &bull; <a href="/about/">go to About</a>
<p>Visits: home {}, about {}</p> <p>Visits: home {}, about {}</p>
"##, "##,
sess.visits1, sess.visits1, sess.visits2
sess.visits2
)) ))
}) })
} }
@ -61,7 +62,8 @@ fn about(session: Session) -> Html<String> {
sess.visits2 sess.visits2
}); });
Html(format!(r##" Html(format!(
r##"
<!DOCTYPE html> <!DOCTYPE html>
<h1>About</h1> <h1>About</h1>
<a href="/about">Refresh</a> &bull; <a href="/">go home</a> <a href="/about">Refresh</a> &bull; <a href="/">go home</a>

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

Loading…
Cancel
Save