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

Handling Sessions with an 'Environment' #246

Closed
wants to merge 13 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
feat(session): add Session plugin based on the Cookie store
  • Loading branch information
Ryman committed Aug 1, 2015
commit 47a728d8472cc0b0e18c442327631944aff832dc
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ mustache = "*"
lazy_static = "*"
modifier = "*"
cookie = "*"
byteorder = "*"

[dependencies.compiletest_rs]
version = "*"
Expand All @@ -46,6 +47,11 @@ path = "examples/cookies_example.rs"

[[example]]

name = "session_example"
path = "examples/session_example.rs"

[[example]]

name = "example_with_default_router"
path = "examples/example_with_default_router.rs"

Expand Down
72 changes: 72 additions & 0 deletions examples/session_example.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#[macro_use] extern crate nickel;
extern crate rustc_serialize;
extern crate time;

use std::io::Write;
use nickel::*;
use nickel::status::StatusCode;
use time::Duration;

#[derive(RustcDecodable, RustcEncodable)]
struct User {
name: String,
password: String,
}

struct ServerData;
static SECRET_KEY: &'static cookies::SecretKey = &cookies::SecretKey([0; 32]);
impl AsRef<cookies::SecretKey> for ServerData {
fn as_ref(&self) -> &cookies::SecretKey { SECRET_KEY }
}
impl SessionStore for ServerData {
type Store = Option<String>;

fn timeout() -> Duration {
Duration::seconds(5)
}
}


fn main() {
let mut server = Nickel::with_data(ServerData);

/* Anyone should be able to reach thist route. */
server.get("/", middleware! { |mut res|
format!("You are logged in as: {:?}\n", res.session())
});

server.post("/login", middleware!{|mut res|
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may not see the forest for the tree here. How do I fully test this example? Of course I can send the right json to the running server using something like curl or postman and I get the "successfully logged in" text. But I'd like to test out the entire example (session cookie). So I thought, ok, let's just render a small <form> for the / route that can be used for the login. But that's not gonna work as non ajax submitted forms aren't send as JSON so I would have to change other parts of the example, too.

So I wonder, did you actually test the entire example to see if the session cookie is created by the browser after successful login? If so, how did you test that out?

@Ryman

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See nickel-org/nickel-auth#1, it has a test shell script. (but yes we should have a test written in rust!)

if let Ok(u) = res.request.json_as::<User>() {
if u.name == "foo" && u.password == "bar" {
*res.session_mut() = Some(u.name);
return res.send("Successfully logged in.")
}
}
(StatusCode::BadRequest, "Access denied.")
});

server.get("/secret", middleware! { |mut res|
match *res.session() {
Some(ref user) if user == "foo" => (StatusCode::Ok, "Some hidden information!"),
_ => (StatusCode::Forbidden, "Access denied.")
}
});

fn custom_403<'a>(err: &mut NickelError<ServerData>) -> Action {
if let Some(ref mut res) = err.response_mut() {
if res.status() == StatusCode::Forbidden {
let _ = res.write_all(b"Access denied!\n");
return Halt(())
}
}

Continue(())
}

// issue #20178
let custom_handler: fn(&mut NickelError<ServerData>) -> Action = custom_403;

server.handle_error(custom_handler);

server.listen("127.0.0.1:6767");
}
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ extern crate mustache;
extern crate groupable;
extern crate modifier;
extern crate cookie;
extern crate byteorder;

#[macro_use] extern crate log;
#[macro_use] extern crate lazy_static;
Expand All @@ -29,6 +30,7 @@ pub use nickel_error::NickelError;
pub use mimes::MediaType;
pub use responder::Responder;
pub use cookies::Cookies;
pub use session::{Session, SessionStore};

#[macro_use] pub mod macros;

Expand All @@ -48,6 +50,7 @@ mod urlencoded;
mod nickel_error;
mod default_error_handler;
pub mod cookies;
pub mod session;

pub mod status {
pub use hyper::status::StatusCode;
Expand Down
2 changes: 1 addition & 1 deletion src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ impl<'a, 'k, D> Response<'a, 'k, D, Fresh> {

pub fn start(mut self) -> Result<Response<'a, 'k, D, Streaming>, NickelError<'a, 'k, D>> {
let on_send = mem::replace(&mut self.on_send, vec![]);
for mut f in on_send.into_iter() {
for mut f in on_send.into_iter().rev() {
// TODO: Ensure `f` doesn't call on_send again
f(&mut self)
}
Expand Down
138 changes: 138 additions & 0 deletions src/session.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
use {Response, Cookies};
use cookie::Cookie;
use plugin::{Plugin, Pluggable};
use typemap::Key;
use std::marker::PhantomData;
use std::any::Any;
use time::{Timespec, Duration, self};
use serialize::{Encodable, Decodable, json};
use byteorder::{ByteOrder, BigEndian};
use std::error::Error;
use std::str;
use std::fmt::Debug;

static COOKIE_KEY : &'static str = "__SESSION";

pub trait SessionStore {
type Store: Encodable + Decodable + Default + Debug;

fn timeout() -> Duration { Duration::minutes(60) }
}

// Plugin boilerplate
pub struct SessionPlugin<T: 'static + Any>(PhantomData<T>);
impl<T: 'static + Any> Key for SessionPlugin<T> { type Value = T; }

impl<'a, 'k, D, T> Plugin<Response<'a, 'k, D>> for SessionPlugin<T>
where Response<'a, 'k, D> : Cookies,
T: 'static + Any + Encodable + Decodable + Default + Debug,
D: SessionStore<Store=T> {
type Error = ();

fn eval(res: &mut Response<'a, 'k, D>) -> Result<T, ()> {
// Ensure our dependencies register their on_send
// FIXME: would be nice if this was more robust, but at least for now
// this minimizes the 'bug potential' to the library author rather than
// the library user.
let _ = res.cookies_mut().encrypted();

// Schedule the session to be written when headers are being sent
res.on_send(|res| {
let encoded = {
let session = res.session();
encode_data(session)
};

let jar = res.cookies_mut().encrypted();
let mut cookie = Cookie::new(COOKIE_KEY.into(), encoded);
cookie.httponly = true;
jar.add(cookie);
});

let jar = res.cookies_mut().encrypted();
let data = jar.find(COOKIE_KEY).and_then(|cookie| {
let timeout = <D as SessionStore>::timeout();
match decode_data(&*cookie.value, timeout) {
Ok(data) => Some(data),
Err(e) => {
println!("Error parsing session: {:?}", e);
None
}
}
});

// Any error should reset the session
Ok(data.unwrap_or_else(|| T::default()))
}
}

fn decode_data<T: Decodable + Default>(raw: &str, timeout: Duration) -> Result<T, Box<Error + Send + Sync>> {
use serialize::base64::FromBase64;

let timestamp_and_plaintext = try!(raw.from_base64());

let len = timestamp_and_plaintext.len();
let (plaintext, timestamp) = timestamp_and_plaintext.split_at(len - 8);

let timestamp = BigEndian::read_i64(timestamp);
let plaintext = try!(str::from_utf8(plaintext));
let timestamp = Timespec::new(timestamp, 0);

if timestamp + timeout > time::now().to_timespec() {
let decoded = try!(json::decode(plaintext));
Ok(decoded)
} else {
// Reset the session, not an error
Ok(T::default())
}
}

fn encode_data<T: Encodable + Debug>(data: &T) -> String {
use serialize::base64::{ToBase64, STANDARD};

// TODO: log if this fails
let json = match json::encode(data) {
Ok(json) => json,
Err(e) => {
println!("Failed to encode '{:?}' as json: {:?}", data, e);
return "".into()
},
};

let mut raw = json.into_bytes();

let mut timestamp = [0u8; 8];
BigEndian::write_i64(&mut timestamp, time::now().to_timespec().sec);
raw.extend(timestamp.iter().cloned());

raw.to_base64(STANDARD)
}

pub trait Session {
type Store;

/// Provides access to an immutable Session.
///
/// Currently requires a mutable reciever, hopefully this can change in future.
fn session(&mut self) -> &Self::Store;

/// Provides access to a mutable Session.
fn session_mut(&mut self) -> &mut Self::Store;
}

impl<'a, 'k, D> Session for Response<'a, 'k, D>
where Response<'a, 'k, D> : Cookies,
D: SessionStore,
D::Store: 'static + Any + Encodable + Decodable + Default + Debug {
type Store = D::Store;

fn session(&mut self) -> &Self::Store {
// Unwrap is safe as we reset the session on bad parses
self.get_ref::<SessionPlugin<D::Store>>().unwrap()
}

fn session_mut(&mut self) -> &mut Self::Store {
// Unwrap is safe as we reset the session on bad parses
self.get_mut::<SessionPlugin<D::Store>>().unwrap()
}
}