Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixup to latest middleware setup #2

Merged
merged 1 commit into from
May 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@ authors = ["Tom Houlé <tom@tomhoule.com>"]
edition = "2018"

[dependencies]
tide = { path = "../tide-aturon" }
futures-preview = "0.3.0-alpha.13"
http-service = "0.1.4"
http = "0.1.16"
tide = "0.2.0"
tide-core = { git = "https://github.com/rustasync/tide", branch = "master" }
tide-cookies = { git = "https://github.com/rustasync/tide", branch = "master" }
cookie = { version = "0.12", features = ["percent-encode"] }
futures-preview = "0.3.0-alpha.16"
#http-service = "0.2.0"
http = "0.1"
regex = "1.1.2"
uuid = { version = "0.7.2", features = ["v4"] }
failure = "0.1.5"
http-service-mock = "0.1.0"
log = "0.4.6"
pretty_env_logger = "0.3.0"

[patch.crates-io]
http-service = { git = "https://github.com/rustasync/http-service", branch = "master" }
http-service-hyper = { git = "https://github.com/rustasync/http-service", branch = "master" }
tide = { git = "https://github.com/rustasync/tide", branch = "master" }
tide-core = { git = "https://github.com/rustasync/tide", branch = "master" }
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ store (implementing the `SessionStorage` trait)

## Examples

### Setting the middleware

```rust
let state: InMemorySession<()> = InMemorySession::new();
app.middleware(CookieSessionMiddleware::new("MySession".to_string(), state));
```

### Reading the contents of the session

```rust
Expand Down
137 changes: 65 additions & 72 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,47 @@

#![feature(associated_type_defaults)]
#![feature(async_await)]
#![feature(await_macro)]
#![feature(futures_api)]

pub mod storage;

use storage::*;

use tide_core::{error::StringError, Context, box_async};
use futures::channel::oneshot;
use futures::future::{FutureExt, FutureObj};
use futures::future::BoxFuture;
use tide_cookies::ContextExt as _;
use cookie::{Cookie,SameSite};
const MIDDLEWARE_MISSING_MSG: &str =
"SessionMiddleware must be used to populate request and response cookies";

pub trait WithSession {
fn set_session<T: 'static + Sync + Send + Default>(&mut self, new_session: T);
fn take_session<T: 'static + Sync + Send + Default>(&mut self) -> T;
pub trait ContextExt {
fn set_session<T: 'static + Sync + Send + Default>(&mut self, new_session: T) -> Result<(), StringError>;
fn take_session<T: 'static + Sync + Send + Default>(&mut self) -> Result<T, StringError>;

// see rails security guide, reset_session
// could be implemented with a channel signaling the reset to the middleware
// fn reset(&self);
}

impl<AppData> WithSession for tide::Context<AppData> {
fn set_session<T: 'static + Sync + Send + Default>(&mut self, new_session: T) {
match self.remove_extension::<Session<T>>() {
Some(session) => {
session
.sender
.send(new_session)
.map_err(|_| ())
.expect("TODO: error handling");
}
None => (),
}
impl<AppData> ContextExt for tide::Context<AppData> {
fn set_session<T: 'static + Sync + Send + Default>(&mut self, new_session: T) -> Result<(), StringError> {
let session = self
.extensions_mut()
.remove::<Session<T>>()
.ok_or_else(|| StringError(MIDDLEWARE_MISSING_MSG.to_owned()))?;
session
.sender
.send(new_session)
.map_err(|_| StringError("Unable to handle session".to_owned()))
}

fn take_session<T: 'static + Sync + Send + Default>(&mut self) -> T {
self.remove_extension::<Session<T>>()
fn take_session<T: 'static + Sync + Send + Default>(&mut self) -> Result<T, StringError> {
let session = self
.extensions_mut()
.remove::<Session<T>>()
.ok_or_else(|| StringError(MIDDLEWARE_MISSING_MSG.to_owned()));
session
.map(|s| s.data)
.unwrap_or_else(Default::default)
}
}

Expand All @@ -62,87 +66,76 @@ where
type SessionId = String;

/// The cookie session middleware.
pub struct CookieSessionMiddleware<Store> {
pub struct CookieSessionMiddleware<Storage> {
/// The name of the cookie used to store the session id.
cookie_name: String,
storage: Store,
storage: Storage,
}

/// The `Shape` parameter is the user-defined shape of the sessions managed by the
/// middleware.
impl<Storage, Shape> CookieSessionMiddleware<Storage>
where
Storage: SessionStorage<Value = Shape>,
Shape: Send + Sync + 'static + Clone + Default,
//Shape: Send + Sync + 'static + Clone + Default,
Shape: 'static,
{
/// `cookie_name` will be the name of the cookie used to store the session id.
pub fn new(cookie_name: String, storage: Storage) -> Result<Self, regex::Error> {
Ok(CookieSessionMiddleware {
pub fn new(cookie_name: String, storage: Storage) -> Self {
CookieSessionMiddleware {
cookie_name,
storage,
})
}
}

/// Attempt to read the session id from the cookies on a request.
fn extract_session_id<A>(&self, ctx: &mut tide::Context<A>) -> Option<String> {
use tide::cookies::Cookies as _;

ctx.cookie(&self.cookie_name).map(|c| c.value().to_owned())
ctx.get_cookie(&self.cookie_name).expect("can't read cookies").map(|c| c.value().to_owned())
}

async fn middleware_impl<'a, AppData>(
&'a self,
mut ctx: tide::Context<AppData>,
next: tide::middleware::Next<'a, AppData>,
) -> tide::Response
where
AppData: Send + 'static + Clone,
{
let session_id = self
.extract_session_id(&mut ctx)
.unwrap_or_else(new_session_id);

let set_cookie_header =
http::header::HeaderValue::from_str(&format!("{}={}", &self.cookie_name, &session_id))
.expect("TODO: error handling");

let session_shape = await!(self.storage.get(&session_id))
.ok()
.and_then(|a| a)
.unwrap_or_default();

let (session, mut receiver) = Session::new(session_shape);

ctx.insert_extension::<Session<Shape>>(session);

let mut res = await! { next.run(ctx) };

let received = receiver.try_recv().ok().and_then(|a| a);

if let Some(received) = received {
await!(self.storage.set(&session_id, received)).expect("TODO: error handling");
}

res.headers_mut().insert("Set-Cookie", set_cookie_header);

res
}
}

impl<AppData, Storage, Shape> tide::middleware::Middleware<AppData>
for CookieSessionMiddleware<Storage>
where
AppData: Clone + Sync + Send + 'static,
AppData: Send + Sync + 'static,
Storage: SessionStorage<Value = Shape> + Sync + Send + 'static,
Shape: Clone + Send + Sync + 'static + Default,
{
fn handle<'a>(
&'a self,
ctx: tide::Context<AppData>,
mut ctx: tide::Context<AppData>,
next: tide::middleware::Next<'a, AppData>,
) -> FutureObj<'a, tide::Response> {
let fut = self.middleware_impl(ctx, next).boxed();
FutureObj::new(fut)
) -> BoxFuture<'a, tide::Response> {
box_async! {
let session_id = self
.extract_session_id(&mut ctx)
.unwrap_or_else(new_session_id);

let session_shape = self.storage.get(&session_id).await
.ok()
.and_then(|a| a)
.unwrap_or_default();

let (session, mut receiver) = Session::new(session_shape);
ctx.extensions_mut().insert::<Session<Shape>>(session);

let mut session_cookie = Cookie::new(self.cookie_name.clone(), session_id.clone());
session_cookie.set_path("/");
session_cookie.set_http_only(true);
session_cookie.set_same_site(SameSite::Strict);
ctx.set_cookie(session_cookie);

let res = next.run(ctx).await;

let received = receiver.try_recv().ok().and_then(|a| a);

if let Some(received) = received {
self.storage.set(&session_id, received).await.expect("TODO: error handling");
}

res
}
}
}

Expand Down
Binary file added tests/.cookies.rs.swp
Binary file not shown.