Compare commits

...

7 Commits

  1. 13
      CHANGELOG.md
  2. 10
      Cargo.toml
  3. 30
      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]
name = "rocket_session"
version = "0.2.0"
version = "0.3.0"
authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
edition = "2018"
edition = "2021"
license = "MIT"
description = "Rocket.rs plug-in for cookie-based sessions holding arbitrary data"
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
[dependencies]
rand = "0.7.2"
rocket = "0.4.2"
parking_lot = "0.10.0"
rand = "0.8"
rocket = "0.5.0-rc.2"
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
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
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
(More examples are in the examples folder)
More examples are in the "examples" folder - run with `cargo run --example=NAME`
### Basic Example
@ -39,34 +41,32 @@ This simple example uses u64 as the session variable; note that it can be a stru
it just needs to implement `Send + Sync + Default`.
```rust
#![feature(proc_macro_hygiene, decl_macro)]
#[macro_use] extern crate rocket;
use std::time::Duration;
#[macro_use]
extern crate rocket;
// It's convenient to define a type alias:
pub type Session<'a> = rocket_session::Session<'a, u64>;
type Session<'a> = rocket_session::Session<'a, u64>;
fn main() {
rocket::ignite()
#[launch]
fn rocket() -> _ {
rocket::build()
.attach(Session::fairing())
.mount("/", routes![index])
.launch();
}
#[get("/")]
fn index(session: Session) -> String {
let count = session.tap(|n| {
// Change the stored value (it is &mut)
// Change the stored value (it is &mut)
*n += 1;
// Return something to the caller.
// This can be any type, 'tap' is generic.
// Return something to the caller.
// This can be any type, 'tap' is generic.
*n
});
format!("{} visits", count)
}
```
## 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
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.
Note that this approach is prone to data races if you're accessing the session object multiple times per request,
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;

@ -1,22 +1,20 @@
#![feature(proc_macro_hygiene, decl_macro)]
#[macro_use]
extern crate rocket;
use rocket::request::Form;
use rocket::response::content::Html;
use rocket::response::content::RawHtml;
use rocket::response::Redirect;
type Session<'a> = rocket_session::Session<'a, Vec<String>>;
fn main() {
rocket::ignite()
#[launch]
fn rocket() -> _ {
rocket::build()
.attach(Session::fairing())
.mount("/", routes![index, add, remove])
.launch();
}
#[get("/")]
fn index(session: Session) -> Html<String> {
fn index(session: Session) -> RawHtml<String> {
let mut page = String::new();
page.push_str(
r#"
@ -30,38 +28,23 @@ fn index(session: Session) -> Html<String> {
<ul>
"#,
);
session.tap(|sess| {
for (n, dog) in sess.iter().enumerate() {
page.push_str(&format!(
r#"
<li>&#x1F436; {} <a href="/remove/{}">Remove</a></li>
"#,
r#"<li>&#x1F436; {} <a href="/remove/{}">Remove</a></li>"#,
dog, n
));
}
});
page.push_str(
r#"
</ul>
"#,
);
Html(page)
}
#[derive(FromForm)]
struct AddForm {
name: String,
page.push_str("</ul>");
RawHtml(page)
}
#[post("/add", data = "<dog>")]
fn add(session: Session, dog: Form<AddForm>) -> Redirect {
fn add(session: Session, dog: String) -> Redirect {
session.tap(move |sess| {
sess.push(dog.into_inner().name);
sess.push(dog);
});
Redirect::found("/")
}
@ -72,6 +55,5 @@ fn remove(session: Session, dog: usize) -> Redirect {
sess.remove(dog);
}
});
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.
#![feature(proc_macro_hygiene, decl_macro)]
#[macro_use]
extern crate rocket;
use rocket::response::content::Html;
use rocket::response::content::RawHtml;
use std::time::Duration;
#[derive(Default, Clone)]
@ -18,8 +17,9 @@ struct SessionData {
// It's convenient to define a type alias:
type Session<'a> = rocket_session::Session<'a, SessionData>;
fn main() {
rocket::ignite()
#[launch]
fn rocket() -> _ {
rocket::build()
.attach(
Session::fairing()
// 10 seconds of inactivity until session expires
@ -30,11 +30,10 @@ fn main() {
.with_cookie_len(20),
)
.mount("/", routes![index, about])
.launch();
}
#[get("/")]
fn index(session: Session) -> Html<String> {
fn index(session: Session) -> RawHtml<String> {
// Here we build the entire response inside the 'tap' closure.
// While inside, the session is locked to parallel changes, e.g.
@ -42,7 +41,7 @@ fn index(session: Session) -> Html<String> {
session.tap(|sess| {
sess.visits1 += 1;
Html(format!(
RawHtml(format!(
r##"
<!DOCTYPE html>
<h1>Home</h1>
@ -55,14 +54,14 @@ fn index(session: Session) -> Html<String> {
}
#[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
let count = session.tap(|sess| {
sess.visits2 += 1;
sess.visits2
});
Html(format!(
RawHtml(format!(
r##"
<!DOCTYPE html>
<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::collections::HashMap;
use std::fmt::{self, Display, Formatter};
@ -15,6 +5,16 @@ use std::marker::PhantomData;
use std::ops::Add;
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)
#[derive(Debug)]
pub struct SessionStore<D>
@ -114,29 +114,29 @@ where
D: 'static + Sync + Send + Default,
{
/// The shared state reference
store: State<'a, SessionStore<D>>,
store: &'a State<SessionStore<D>>,
/// Session ID
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
D: 'static + Sync + Send + Default,
{
type Error = ();
fn from_request(request: &'a Request<'r>) -> Outcome<Self, (Status, Self::Error), ()> {
let store: State<SessionStore<D>> = request.guard().unwrap();
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, (Status, Self::Error), ()> {
let store = request.guard::<&State<SessionStore<D>>>().await.unwrap();
Outcome::Success(Session {
id: request.local_cache(|| {
let store_ug = store.inner.upgradable_read();
// Resolve session ID
let id = if let Some(cookie) = request.cookies().get(&store.config.cookie_name) {
Some(SessionID(cookie.value().to_string()))
} else {
None
};
let id = request
.cookies()
.get(&store.config.cookie_name)
.map(|cookie| SessionID(cookie.value().to_string()));
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
let new_id = SessionID(loop {
let token: String = rand::thread_rng()
let token: String = OsRng
.sample_iter(&rand::distributions::Alphanumeric)
.take(store.config.cookie_len)
.map(char::from)
.collect();
if !store_wg.sessions.contains_key(&token) {
@ -294,6 +295,7 @@ where
}
}
#[rocket::async_trait]
impl<D> Fairing for SessionFairing<D>
where
D: 'static + Sync + Send + Default,
@ -301,11 +303,11 @@ where
fn info(&self) -> Info {
Info {
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
Ok(rocket.manage(SessionStore::<D> {
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
let session = request.local_cache(|| SessionID("".to_string()));

Loading…
Cancel
Save