Compare commits

...

7 Commits

  1. 13
      CHANGELOG.md
  2. 10
      Cargo.toml
  3. 24
      README.md
  4. 38
      examples/dog_list/main.rs
  5. 28
      examples/minimal/main.rs
  6. 17
      examples/visit_counter/main.rs
  7. 48
      src/lib.rs

@ -0,0 +1,13 @@
# [0.3.0]
- Update dependencies
- Added new example
- Port to rocket `0.5.0-rc.2`
# [0.2.2]
- Update dependencies
# [0.2.1]
- change from `thread_rng` to `OsRng` for better session ID entropy

@ -1,8 +1,8 @@
[package] [package]
name = "rocket_session" name = "rocket_session"
version = "0.2.0" version = "0.3.0"
authors = ["Ondřej Hruška <ondra@ondrovo.com>"] authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
edition = "2018" edition = "2021"
license = "MIT" license = "MIT"
description = "Rocket.rs plug-in for cookie-based sessions holding arbitrary data" description = "Rocket.rs plug-in for cookie-based sessions holding arbitrary data"
repository = "https://git.ondrovo.com/packages/rocket_session" repository = "https://git.ondrovo.com/packages/rocket_session"
@ -16,6 +16,6 @@ categories = [
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
rand = "0.7.2" rand = "0.8"
rocket = "0.4.2" rocket = "0.5.0-rc.2"
parking_lot = "0.10.0" parking_lot = "0.12"

@ -13,6 +13,8 @@ The implementation is generic to support any type as session data: a custom stru
The session lifetime, cookie name, and other parameters can be configured by calling chained 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. methods on the fairing. When a session expires, the data associated with it is dropped.
Example: `Session::fairing().with_lifetime(Duration::from_secs(15))`
## Usage ## Usage
To use session in a route, first make sure you have the fairing attached by calling To use session in a route, first make sure you have the fairing attached by calling
@ -31,7 +33,7 @@ the session list does not waste memory.
## Examples ## Examples
(More examples are in the examples folder) More examples are in the "examples" folder - run with `cargo run --example=NAME`
### Basic Example ### Basic Example
@ -39,19 +41,16 @@ This simple example uses u64 as the session variable; note that it can be a stru
it just needs to implement `Send + Sync + Default`. it just needs to implement `Send + Sync + Default`.
```rust ```rust
#![feature(proc_macro_hygiene, decl_macro)] #[macro_use]
#[macro_use] extern crate rocket; extern crate rocket;
use std::time::Duration;
// It's convenient to define a type alias: type Session<'a> = rocket_session::Session<'a, u64>;
pub type Session<'a> = rocket_session::Session<'a, u64>;
fn main() { #[launch]
rocket::ignite() fn rocket() -> _ {
rocket::build()
.attach(Session::fairing()) .attach(Session::fairing())
.mount("/", routes![index]) .mount("/", routes![index])
.launch();
} }
#[get("/")] #[get("/")]
@ -67,6 +66,7 @@ fn index(session: Session) -> String {
format!("{} visits", count) format!("{} visits", count)
} }
``` ```
## Extending Session by a Trait ## Extending Session by a Trait
@ -76,8 +76,8 @@ The `.tap()` method is powerful, but sometimes you may wish for something more c
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()`. Note that this approach is prone to data races if you're accessing the session object multiple times per request,
It may be safer to simply call the `.dot_*()` methods manually in one shared closure. 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;

@ -1,22 +1,20 @@
#![feature(proc_macro_hygiene, decl_macro)]
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;
use rocket::request::Form; use rocket::response::content::RawHtml;
use rocket::response::content::Html;
use rocket::response::Redirect; use rocket::response::Redirect;
type Session<'a> = rocket_session::Session<'a, Vec<String>>; type Session<'a> = rocket_session::Session<'a, Vec<String>>;
fn main() { #[launch]
rocket::ignite() fn rocket() -> _ {
rocket::build()
.attach(Session::fairing()) .attach(Session::fairing())
.mount("/", routes![index, add, remove]) .mount("/", routes![index, add, remove])
.launch();
} }
#[get("/")] #[get("/")]
fn index(session: Session) -> Html<String> { fn index(session: Session) -> RawHtml<String> {
let mut page = String::new(); let mut page = String::new();
page.push_str( page.push_str(
r#" r#"
@ -30,38 +28,23 @@ fn index(session: Session) -> Html<String> {
<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!( page.push_str(&format!(
r#" r#"<li>&#x1F436; {} <a href="/remove/{}">Remove</a></li>"#,
<li>&#x1F436; {} <a href="/remove/{}">Remove</a></li>
"#,
dog, n dog, n
)); ));
} }
}); });
page.push_str("</ul>");
page.push_str( RawHtml(page)
r#"
</ul>
"#,
);
Html(page)
}
#[derive(FromForm)]
struct AddForm {
name: String,
} }
#[post("/add", data = "<dog>")] #[post("/add", data = "<dog>")]
fn add(session: Session, dog: Form<AddForm>) -> Redirect { fn add(session: Session, dog: String) -> Redirect {
session.tap(move |sess| { session.tap(move |sess| {
sess.push(dog.into_inner().name); sess.push(dog);
}); });
Redirect::found("/") Redirect::found("/")
} }
@ -72,6 +55,5 @@ fn remove(session: Session, dog: usize) -> Redirect {
sess.remove(dog); sess.remove(dog);
} }
}); });
Redirect::found("/") Redirect::found("/")
} }

@ -0,0 +1,28 @@
#[macro_use]
extern crate rocket;
use std::time::Duration;
type Session<'a> = rocket_session::Session<'a, u64>;
#[launch]
fn rocket() -> _ {
// This session expires in 15 seconds as a demonstration of session configuration
rocket::build()
.attach(Session::fairing().with_lifetime(Duration::from_secs(15)))
.mount("/", routes![index])
}
#[get("/")]
fn index(session: Session) -> String {
let count = session.tap(|n| {
// Change the stored value (it is &mut)
*n += 1;
// Return something to the caller.
// This can be any type, 'tap' is generic.
*n
});
format!("{} visits", count)
}

@ -2,11 +2,10 @@
//! //!
//! The expiry time is set to 10 seconds to illustrate how a session is cleared if inactive. //! The expiry time is set to 10 seconds to illustrate how a session is cleared if inactive.
#![feature(proc_macro_hygiene, decl_macro)]
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;
use rocket::response::content::Html; use rocket::response::content::RawHtml;
use std::time::Duration; use std::time::Duration;
#[derive(Default, Clone)] #[derive(Default, Clone)]
@ -18,8 +17,9 @@ struct SessionData {
// It's convenient to define a type alias: // It's convenient to define a type alias:
type Session<'a> = rocket_session::Session<'a, SessionData>; type Session<'a> = rocket_session::Session<'a, SessionData>;
fn main() { #[launch]
rocket::ignite() fn rocket() -> _ {
rocket::build()
.attach( .attach(
Session::fairing() Session::fairing()
// 10 seconds of inactivity until session expires // 10 seconds of inactivity until session expires
@ -30,11 +30,10 @@ fn main() {
.with_cookie_len(20), .with_cookie_len(20),
) )
.mount("/", routes![index, about]) .mount("/", routes![index, about])
.launch();
} }
#[get("/")] #[get("/")]
fn index(session: Session) -> Html<String> { fn index(session: Session) -> RawHtml<String> {
// Here we build the entire response inside the 'tap' closure. // Here we build the entire response inside the 'tap' closure.
// While inside, the session is locked to parallel changes, e.g. // While inside, the session is locked to parallel changes, e.g.
@ -42,7 +41,7 @@ fn index(session: Session) -> Html<String> {
session.tap(|sess| { session.tap(|sess| {
sess.visits1 += 1; sess.visits1 += 1;
Html(format!( RawHtml(format!(
r##" r##"
<!DOCTYPE html> <!DOCTYPE html>
<h1>Home</h1> <h1>Home</h1>
@ -55,14 +54,14 @@ fn index(session: Session) -> Html<String> {
} }
#[get("/about")] #[get("/about")]
fn about(session: Session) -> Html<String> { fn about(session: Session) -> RawHtml<String> {
// Here we return a value from the tap function and use it below // Here we return a value from the tap function and use it below
let count = session.tap(|sess| { let count = session.tap(|sess| {
sess.visits2 += 1; sess.visits2 += 1;
sess.visits2 sess.visits2
}); });
Html(format!( RawHtml(format!(
r##" r##"
<!DOCTYPE html> <!DOCTYPE html>
<h1>About</h1> <h1>About</h1>

@ -1,13 +1,3 @@
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
use rand::Rng;
use rocket::{
fairing::{self, Fairing, Info},
http::{Cookie, Status},
request::FromRequest,
Outcome, Request, Response, Rocket, State,
};
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::{self, Display, Formatter}; use std::fmt::{self, Display, Formatter};
@ -15,6 +5,16 @@ use std::marker::PhantomData;
use std::ops::Add; use std::ops::Add;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
use rand::{rngs::OsRng, Rng};
use rocket::{
fairing::{self, Fairing, Info},
http::{Cookie, Status},
outcome::Outcome,
request::FromRequest,
Build, Request, Response, Rocket, State,
};
/// Session store (shared state) /// Session store (shared state)
#[derive(Debug)] #[derive(Debug)]
pub struct SessionStore<D> pub struct SessionStore<D>
@ -114,29 +114,29 @@ 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: &'a State<SessionStore<D>>,
/// Session ID /// Session ID
id: &'a SessionID, id: &'a SessionID,
} }
impl<'a, 'r, D> FromRequest<'a, 'r> for Session<'a, D> #[rocket::async_trait]
impl<'r, D> FromRequest<'r> for Session<'r, D>
where where
D: 'static + Sync + Send + Default, D: 'static + Sync + Send + Default,
{ {
type Error = (); type Error = ();
fn from_request(request: &'a Request<'r>) -> Outcome<Self, (Status, Self::Error), ()> { async fn from_request(request: &'r Request<'_>) -> Outcome<Self, (Status, Self::Error), ()> {
let store: State<SessionStore<D>> = request.guard().unwrap(); let store = request.guard::<&State<SessionStore<D>>>().await.unwrap();
Outcome::Success(Session { Outcome::Success(Session {
id: request.local_cache(|| { id: request.local_cache(|| {
let store_ug = store.inner.upgradable_read(); let store_ug = store.inner.upgradable_read();
// Resolve session ID // Resolve session ID
let id = if let Some(cookie) = request.cookies().get(&store.config.cookie_name) { let id = request
Some(SessionID(cookie.value().to_string())) .cookies()
} else { .get(&store.config.cookie_name)
None .map(|cookie| SessionID(cookie.value().to_string()));
};
let expires = Instant::now().add(store.config.lifespan); let expires = Instant::now().add(store.config.lifespan);
@ -176,9 +176,10 @@ where
// Find a new unique ID - we are still safely inside the write guard // Find a new unique ID - we are still safely inside the write guard
let new_id = SessionID(loop { let new_id = SessionID(loop {
let token: String = rand::thread_rng() let token: String = OsRng
.sample_iter(&rand::distributions::Alphanumeric) .sample_iter(&rand::distributions::Alphanumeric)
.take(store.config.cookie_len) .take(store.config.cookie_len)
.map(char::from)
.collect(); .collect();
if !store_wg.sessions.contains_key(&token) { if !store_wg.sessions.contains_key(&token) {
@ -294,6 +295,7 @@ where
} }
} }
#[rocket::async_trait]
impl<D> Fairing for SessionFairing<D> impl<D> Fairing for SessionFairing<D>
where where
D: 'static + Sync + Send + Default, D: 'static + Sync + Send + Default,
@ -301,11 +303,11 @@ where
fn info(&self) -> Info { fn info(&self) -> Info {
Info { Info {
name: "Session", name: "Session",
kind: fairing::Kind::Attach | fairing::Kind::Response, kind: fairing::Kind::Ignite | fairing::Kind::Response,
} }
} }
fn on_attach(&self, rocket: Rocket) -> Result<Rocket, Rocket> { async fn on_ignite(&self, rocket: Rocket<Build>) -> Result<Rocket<Build>, Rocket<Build>> {
// install the store singleton // install the store singleton
Ok(rocket.manage(SessionStore::<D> { Ok(rocket.manage(SessionStore::<D> {
inner: Default::default(), inner: Default::default(),
@ -313,7 +315,7 @@ where
})) }))
} }
fn on_response<'r>(&self, request: &'r Request, response: &mut Response) { async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response) {
// send the session cookie, if session started // send the session cookie, if session started
let session = request.local_cache(|| SessionID("".to_string())); let session = request.local_cache(|| SessionID("".to_string()));

Loading…
Cancel
Save