Initial session support.

This commit includes the following additions:
  * A `session` example was added.
  * `Config::take_session_key` was removed.
  * If a `session_key` is not supplied, one is automatically generated.
  * The `Session` type implements signed, encrypted sessions.
  * A `Session` can be retrieved via its request guard.
This commit is contained in:
Sergio Benitez 2017-03-08 03:28:12 -08:00
parent 05a458942d
commit 16cb7297ab
18 changed files with 370 additions and 115 deletions

View File

@ -30,5 +30,6 @@ members = [
"examples/pastebin", "examples/pastebin",
"examples/state", "examples/state",
"examples/uuid", "examples/uuid",
"examples/session",
# "examples/raw_sqlite", # "examples/raw_sqlite",
] ]

View File

@ -42,9 +42,6 @@ pub fn test_config(environment: Environment) {
assert_eq!(config.extras().count(), 0); assert_eq!(config.extras().count(), 0);
} }
} }
// Rocket `take`s the key, so this should always be `None`.
assert_eq!(config.take_session_key(), None);
} }
pub fn test_hello() { pub fn test_hello() {

View File

@ -0,0 +1,16 @@
[package]
name = "session"
version = "0.0.1"
workspace = "../../"
[dependencies]
rocket = { path = "../../lib" }
rocket_codegen = { path = "../../codegen" }
[dependencies.rocket_contrib]
path = "../../contrib"
default-features = false
features = ["handlebars_templates"]
[dev-dependencies]
rocket = { path = "../../lib", features = ["testing"] }

View File

@ -0,0 +1,7 @@
[staging]
session_key = "itlYmFR2vYKrOmFhupMIn/hyB6lYCCTXz4yaQX89XVg="
address = "localhost"
port = 8000
[production]
session_key = "itlYmFR2vYKrOmFhupMIn/hyB6lYCCTXz4yaQX89XVg="

View File

@ -0,0 +1,87 @@
#![feature(plugin, custom_derive, custom_attribute)]
#![plugin(rocket_codegen)]
extern crate rocket_contrib;
extern crate rocket;
use std::collections::HashMap;
use rocket::Outcome;
use rocket::request::{self, Form, FlashMessage, FromRequest, Request};
use rocket::response::{Redirect, Flash};
use rocket::http::{Cookie, Session};
use rocket_contrib::Template;
#[derive(FromForm)]
struct Login {
username: String,
password: String
}
#[derive(Debug)]
struct User(usize);
impl<'a, 'r> FromRequest<'a, 'r> for User {
type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<User, ()> {
let user = request.session()
.get("user_id")
.and_then(|cookie| cookie.value().parse().ok())
.map(|id| User(id));
match user {
Some(user) => Outcome::Success(user),
None => Outcome::Forward(())
}
}
}
#[post("/login", data = "<login>")]
fn login(mut session: Session, login: Form<Login>) -> Flash<Redirect> {
if login.get().username == "Sergio" && login.get().password == "password" {
session.add(Cookie::new("user_id", 1.to_string()));
Flash::success(Redirect::to("/"), "Successfully logged in.")
} else {
Flash::error(Redirect::to("/login"), "Invalid username/password.")
}
}
#[post("/logout")]
fn logout(mut session: Session) -> Flash<Redirect> {
session.remove(Cookie::named("user_id"));
Flash::success(Redirect::to("/login"), "Successfully logged out.")
}
#[get("/login")]
fn login_user(_user: User) -> Redirect {
Redirect::to("/")
}
#[get("/login", rank = 2)]
fn login_page(flash: Option<FlashMessage>) -> Template {
let mut context = HashMap::new();
if let Some(ref msg) = flash {
context.insert("flash", msg.msg());
}
Template::render("login", &context)
}
#[get("/")]
fn user_index(user: User) -> Template {
let mut context = HashMap::new();
context.insert("user_id", user.0);
Template::render("index", &context)
}
#[get("/", rank = 2)]
fn index() -> Redirect {
Redirect::to("/login")
}
fn main() {
rocket::ignite()
.mount("/", routes![index, user_index, login, logout, login_user, login_page])
.launch()
}

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Rocket: Session Example</title>
</head>
<body>
<h1>Rocket Session Example</h1>
<p>Logged in with user ID {{ user_id }}.</p>
<form action="/logout" method="post" accept-charset="utf-8">
<input type="submit" name="logout" id="logout" value="logout" />
</form>
</body>
</html>

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Rocket: Sessions</title>
</head>
<body>
<h1>Rocket Session: Please Login</h1>
{{#if flash}}
<p>{{ flash }}</p>
{{else}}
<p>Please login to continue.</p>
{{/if}}
<form action="/login" method="post" accept-charset="utf-8">
<label for="username">username</label>
<input type="text" name="username" id="username" value="" />
<label for="password">password</label>
<input type="password" name="password" id="password" value="" />
<p><input type="submit" value="login"></p>
</form>
</body>
</html>

View File

@ -14,21 +14,21 @@ license = "MIT/Apache-2.0"
build = "build.rs" build = "build.rs"
categories = ["web-programming::http-server"] categories = ["web-programming::http-server"]
[dependencies] [features]
term-painter = "^0.2" testing = []
log = "^0.3"
url = "^1"
hyper = { version = "0.10.4", default-features = false }
toml = { version = "^0.2", default-features = false }
num_cpus = "1"
state = "^0.2"
time = "^0.1"
memchr = "1"
# FIXME: session support should be optional [dependencies]
term-painter = "0.2"
log = "0.3"
url = "1"
hyper = { version = "0.10.4", default-features = false }
toml = { version = "0.2", default-features = false }
num_cpus = "1"
state = "0.2"
time = "0.1"
memchr = "1"
base64 = "0.4" base64 = "0.4"
# FIXME: session support should be optional
[dependencies.cookie] [dependencies.cookie]
version = "^0.7" version = "^0.7"
features = ["percent-encode", "secure"] features = ["percent-encode", "secure"]
@ -40,6 +40,3 @@ rocket_codegen = { version = "0.2.2", path = "../codegen" }
[build-dependencies] [build-dependencies]
ansi_term = "^0.9" ansi_term = "^0.9"
version_check = "^0.1" version_check = "^0.1"
[features]
testing = []

View File

@ -156,8 +156,6 @@ impl ConfigBuilder {
/// let mut config = Config::build(Environment::Staging) /// let mut config = Config::build(Environment::Staging)
/// .session_key(key) /// .session_key(key)
/// .unwrap(); /// .unwrap();
///
/// assert!(config.take_session_key().is_some());
/// ``` /// ```
pub fn session_key<K: Into<String>>(mut self, key: K) -> Self { pub fn session_key<K: Into<String>>(mut self, key: K) -> Self {
self.session_key = Some(key.into()); self.session_key = Some(key.into());

View File

@ -1,7 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::net::{IpAddr, lookup_host}; use std::net::{IpAddr, lookup_host};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::RwLock;
use std::convert::AsRef; use std::convert::AsRef;
use std::fmt; use std::fmt;
use std::env; use std::env;
@ -11,6 +10,29 @@ use config::{self, Value, ConfigBuilder, Environment, ConfigError};
use {num_cpus, base64}; use {num_cpus, base64};
use logger::LoggingLevel; use logger::LoggingLevel;
use http::Key;
pub enum SessionKey {
Generated(Key),
Provided(Key)
}
impl SessionKey {
#[inline]
pub fn kind(&self) -> &'static str {
match *self {
SessionKey::Generated(_) => "generated",
SessionKey::Provided(_) => "provided",
}
}
#[inline]
fn inner(&self) -> &Key {
match *self {
SessionKey::Generated(ref key) | SessionKey::Provided(ref key) => key
}
}
}
/// Structure for Rocket application configuration. /// Structure for Rocket application configuration.
/// ///
@ -44,7 +66,7 @@ pub struct Config {
/// The path to the configuration file this config belongs to. /// The path to the configuration file this config belongs to.
pub config_path: PathBuf, pub config_path: PathBuf,
/// The session key. /// The session key.
session_key: RwLock<Option<Vec<u8>>>, pub(crate) session_key: SessionKey,
} }
macro_rules! parse { macro_rules! parse {
@ -102,22 +124,17 @@ impl Config {
Config::default(env, cwd.as_path().join("Rocket.custom.toml")) Config::default(env, cwd.as_path().join("Rocket.custom.toml"))
} }
// Aliases to `default_for` before the method is removed.
pub(crate) fn default<P>(env: Environment, path: P) -> config::Result<Config>
where P: AsRef<Path>
{
#[allow(deprecated)]
Config::default_for(env, path)
}
/// Returns the default configuration for the environment `env` given that /// Returns the default configuration for the environment `env` given that
/// the configuration was stored at `config_path`. If `config_path` is not /// the configuration was stored at `config_path`. If `config_path` is not
/// an absolute path, an `Err` of `ConfigError::BadFilePath` is returned. /// an absolute path, an `Err` of `ConfigError::BadFilePath` is returned.
#[deprecated(since="0.2", note="use the `new` or `build` methods instead")] ///
pub fn default_for<P>(env: Environment, config_path: P) -> config::Result<Config> /// # Panics
///
/// Panics if randomness cannot be retrieved from the OS.
pub(crate) fn default<P>(env: Environment, path: P) -> config::Result<Config>
where P: AsRef<Path> where P: AsRef<Path>
{ {
let config_path = config_path.as_ref().to_path_buf(); let config_path = path.as_ref().to_path_buf();
if config_path.parent().is_none() { if config_path.parent().is_none() {
return Err(ConfigError::BadFilePath(config_path, return Err(ConfigError::BadFilePath(config_path,
"Configuration files must be rooted in a directory.")); "Configuration files must be rooted in a directory."));
@ -126,6 +143,9 @@ impl Config {
// Note: This may truncate if num_cpus::get() > u16::max. That's okay. // Note: This may truncate if num_cpus::get() > u16::max. That's okay.
let default_workers = ::std::cmp::max(num_cpus::get(), 2) as u16; let default_workers = ::std::cmp::max(num_cpus::get(), 2) as u16;
// Use a generated session key by default.
let key = SessionKey::Generated(Key::generate());
Ok(match env { Ok(match env {
Development => { Development => {
Config { Config {
@ -134,7 +154,7 @@ impl Config {
port: 8000, port: 8000,
workers: default_workers, workers: default_workers,
log_level: LoggingLevel::Normal, log_level: LoggingLevel::Normal,
session_key: RwLock::new(None), session_key: key,
extras: HashMap::new(), extras: HashMap::new(),
config_path: config_path, config_path: config_path,
} }
@ -146,7 +166,7 @@ impl Config {
port: 80, port: 80,
workers: default_workers, workers: default_workers,
log_level: LoggingLevel::Normal, log_level: LoggingLevel::Normal,
session_key: RwLock::new(None), session_key: key,
extras: HashMap::new(), extras: HashMap::new(),
config_path: config_path, config_path: config_path,
} }
@ -158,7 +178,7 @@ impl Config {
port: 80, port: 80,
workers: default_workers, workers: default_workers,
log_level: LoggingLevel::Critical, log_level: LoggingLevel::Critical,
session_key: RwLock::new(None), session_key: key,
extras: HashMap::new(), extras: HashMap::new(),
config_path: config_path, config_path: config_path,
} }
@ -348,7 +368,7 @@ impl Config {
Err(_) => return Err(error) Err(_) => return Err(error)
}; };
self.session_key = RwLock::new(Some(bytes)); self.session_key = SessionKey::Provided(Key::from_master(&bytes));
Ok(()) Ok(())
} }
@ -425,33 +445,10 @@ impl Config {
self.extras.iter().map(|(k, v)| (k.as_str(), v)) self.extras.iter().map(|(k, v)| (k.as_str(), v))
} }
/// Moves the session key string out of the `self` Config, if there is one. /// Retrieves the session key from `self`.
/// Because the value is moved out, subsequent calls will result in a return
/// value of `None`.
///
/// # Example
///
/// ```rust
/// use rocket::config::{Config, Environment};
///
/// // Create a new config with a session key.
/// let key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg=";
/// let config = Config::build(Environment::Staging)
/// .session_key(key)
/// .unwrap();
///
/// // Get the key for the first time.
/// let session_key = config.take_session_key();
/// assert!(session_key.is_some());
///
/// // Try to get the key again.
/// let session_key_again = config.take_session_key();
/// assert_eq!(session_key_again, None);
/// ```
#[inline] #[inline]
pub fn take_session_key(&self) -> Option<Vec<u8>> { pub(crate) fn session_key(&self) -> &Key {
let mut key = self.session_key.write().expect("couldn't lock session key"); self.session_key.inner()
key.take()
} }
/// Attempts to retrieve the extra named `name` as a string. /// Attempts to retrieve the extra named `name` as a string.

View File

@ -63,27 +63,29 @@
//! port = 8000 //! port = 8000
//! workers = max(number_of_cpus, 2) //! workers = max(number_of_cpus, 2)
//! log = "normal" //! log = "normal"
//! session_key = [randomly generated at launch]
//! //!
//! [staging] //! [staging]
//! address = "0.0.0.0" //! address = "0.0.0.0"
//! port = 80 //! port = 80
//! workers = max(number_of_cpus, 2) //! workers = max(number_of_cpus, 2)
//! log = "normal" //! log = "normal"
//! # don't use this key! generate your own and keep it private! //! session_key = [randomly generated at launch]
//! session_key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg="
//! //!
//! [production] //! [production]
//! address = "0.0.0.0" //! address = "0.0.0.0"
//! port = 80 //! port = 80
//! workers = max(number_of_cpus, 2) //! workers = max(number_of_cpus, 2)
//! log = "critical" //! log = "critical"
//! # don't use this key! generate your own and keep it private! //! session_key = [randomly generated at launch]
//! session_key = "hPRYyVRiMyxpw5sBB1XeCMN1kFsDCqKvBi2QJxBVHQk="
//! ``` //! ```
//! //!
//! The `workers` parameter is computed by Rocket automatically; the value above //! The `workers` and `session_key` default parameters are computed by Rocket
//! is not valid TOML syntax. When manually specifying the number of workers, //! automatically; the values above are not valid TOML syntax. When manually
//! the value should be an integer: `workers = 10`. //! specifying the number of workers, the value should be an integer: `workers =
//! 10`. When manually specifying the session key, the value should a 256-bit
//! base64 encoded string. Such a string can be generated with the `openssl`
//! command line tool: `openssl rand -base64 32`.
//! //!
//! The "global" pseudo-environment can be used to set and/or override //! The "global" pseudo-environment can be used to set and/or override
//! configuration parameters globally. A parameter defined in a `[global]` table //! configuration parameters globally. A parameter defined in a `[global]` table

View File

@ -4,23 +4,34 @@ use std::cell::RefMut;
pub use cookie::{Cookie, CookieJar, Iter, CookieBuilder, Delta}; pub use cookie::{Cookie, CookieJar, Iter, CookieBuilder, Delta};
use cookie::{PrivateJar, Key};
impl<'a, 'c> From<&'a Cookie<'c>> for Header<'static> {
fn from(cookie: &Cookie) -> Header<'static> {
Header::new("Set-Cookie", cookie.encoded().to_string())
}
}
#[derive(Debug)] #[derive(Debug)]
pub enum Cookies<'a> { pub enum Cookies<'a> {
Jarred(RefMut<'a, CookieJar>), Jarred(RefMut<'a, CookieJar>),
Empty(CookieJar) Empty(CookieJar)
} }
impl<'a> From<RefMut<'a, CookieJar>> for Cookies<'a> { impl<'a> Cookies<'a> {
fn from(jar: RefMut<'a, CookieJar>) -> Cookies<'a> { pub(crate) fn new(jar: RefMut<'a, CookieJar>) -> Cookies<'a> {
Cookies::Jarred(jar) Cookies::Jarred(jar)
} }
}
impl<'a> Cookies<'a> {
pub(crate) fn empty() -> Cookies<'static> { pub(crate) fn empty() -> Cookies<'static> {
Cookies::Empty(CookieJar::new()) Cookies::Empty(CookieJar::new())
} }
#[inline(always)]
pub(crate) fn parse_cookie(cookie_str: &str) -> Option<Cookie<'static>> {
Cookie::parse_encoded(cookie_str).map(|c| c.into_owned()).ok()
}
pub fn get(&self, name: &str) -> Option<&Cookie<'static>> { pub fn get(&self, name: &str) -> Option<&Cookie<'static>> {
match *self { match *self {
Cookies::Jarred(ref jar) => jar.get(name), Cookies::Jarred(ref jar) => jar.get(name),
@ -40,6 +51,13 @@ impl<'a> Cookies<'a> {
} }
} }
pub(crate) fn private(&mut self, key: &Key) -> PrivateJar {
match *self {
Cookies::Jarred(ref mut jar) => jar.private(key),
Cookies::Empty(ref mut jar) => jar.private(key)
}
}
pub fn iter(&self) -> Iter { pub fn iter(&self) -> Iter {
match *self { match *self {
Cookies::Jarred(ref jar) => jar.iter(), Cookies::Jarred(ref jar) => jar.iter(),
@ -47,16 +65,10 @@ impl<'a> Cookies<'a> {
} }
} }
pub fn delta(&self) -> Delta { pub(crate) fn delta(&self) -> Delta {
match *self { match *self {
Cookies::Jarred(ref jar) => jar.delta(), Cookies::Jarred(ref jar) => jar.delta(),
Cookies::Empty(ref jar) => jar.delta() Cookies::Empty(ref jar) => jar.delta()
} }
} }
} }
impl<'a, 'c> From<&'a Cookie<'c>> for Header<'static> {
fn from(cookie: &Cookie) -> Header<'static> {
Header::new("Set-Cookie", cookie.encoded().to_string())
}
}

View File

@ -9,6 +9,7 @@ pub mod hyper;
pub mod uri; pub mod uri;
mod cookies; mod cookies;
mod session;
mod method; mod method;
mod content_type; mod content_type;
mod status; mod status;
@ -23,4 +24,5 @@ pub use self::content_type::ContentType;
pub use self::status::{Status, StatusClass}; pub use self::status::{Status, StatusClass};
pub use self::header::{Header, HeaderMap}; pub use self::header::{Header, HeaderMap};
pub use self::cookies::{Cookie, Cookies, CookieJar, CookieBuilder}; pub use self::cookies::*;
pub use self::session::*;

62
lib/src/http/session.rs Normal file
View File

@ -0,0 +1,62 @@
use std::cell::{RefCell, RefMut};
use cookie::{Cookie, CookieJar, Delta};
pub use cookie::Key;
use http::{Header, Cookies};
const SESSION_PREFIX: &'static str = "__sess_";
pub struct Session<'a> {
cookies: RefCell<Cookies<'a>>,
key: &'a Key
}
impl<'a> Session<'a> {
#[inline(always)]
pub(crate) fn new(jar: RefMut<'a, CookieJar>, key: &'a Key) -> Session<'a> {
Session { cookies: RefCell::new(Cookies::new(jar)), key: key }
}
#[inline(always)]
pub(crate) fn empty(key: &'a Key) -> Session<'a> {
Session { cookies: RefCell::new(Cookies::empty()), key: key }
}
#[inline(always)]
pub(crate) fn header_for(cookie: &Cookie) -> Header<'static> {
Header::new("Set-Cookie", format!("{}{}", SESSION_PREFIX, cookie))
}
#[inline(always)]
pub(crate) fn parse_cookie(cookie_str: &str) -> Option<Cookie<'static>> {
if !cookie_str.starts_with(SESSION_PREFIX) {
return None;
}
let string = cookie_str[SESSION_PREFIX.len()..].to_string();
Cookie::parse(string).ok()
}
pub fn get(&self, name: &str) -> Option<Cookie<'static>> {
self.cookies.borrow_mut().private(&self.key).get(name)
}
pub fn add(&mut self, mut cookie: Cookie<'static>) {
cookie.set_http_only(true);
if cookie.path().is_none() {
cookie.set_path("/");
}
self.cookies.get_mut().private(&self.key).add(cookie)
}
pub fn remove(&mut self, cookie: Cookie<'static>) {
self.cookies.get_mut().private(&self.key).remove(cookie)
}
#[inline(always)]
pub(crate) fn delta(&mut self) -> Delta {
self.cookies.get_mut().delta()
}
}

View File

@ -5,7 +5,7 @@ use outcome::{self, IntoOutcome};
use request::Request; use request::Request;
use outcome::Outcome::*; use outcome::Outcome::*;
use http::{Status, ContentType, Method, Cookies}; use http::{Status, ContentType, Method, Cookies, Session};
use http::uri::URI; use http::uri::URI;
/// Type alias for the `Outcome` of a `FromRequest` conversion. /// Type alias for the `Outcome` of a `FromRequest` conversion.
@ -85,11 +85,11 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
/// ///
/// _This implementation always returns successfully._ /// _This implementation always returns successfully._
/// ///
/// * **&Cookies** /// * **Cookies**
/// ///
/// Returns a borrow to the [Cookies](/rocket/http/type.Cookies.html) in the /// Returns a borrow to the [Cookies](/rocket/http/type.Cookies.html) in the
/// incoming request. Note that `Cookies` implements internal mutability, so /// incoming request. Note that `Cookies` implements internal mutability, so
/// a handle to `&Cookies` allows you to get _and_ set cookies in the /// a handle to `Cookies` allows you to get _and_ set cookies in the
/// request. /// request.
/// ///
/// _This implementation always returns successfully._ /// _This implementation always returns successfully._
@ -212,6 +212,14 @@ impl<'a, 'r> FromRequest<'a, 'r> for Cookies<'a> {
} }
} }
impl<'a, 'r> FromRequest<'a, 'r> for Session<'a> {
type Error = ();
fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> {
Success(request.session())
}
}
impl<'a, 'r> FromRequest<'a, 'r> for ContentType { impl<'a, 'r> FromRequest<'a, 'r> for ContentType {
type Error = (); type Error = ();

View File

@ -12,10 +12,7 @@ use super::{FromParam, FromSegments};
use router::Route; use router::Route;
use http::uri::{URI, Segments}; use http::uri::{URI, Segments};
use http::{Method, ContentType, Header, HeaderMap, Cookie, Cookies}; use http::{Method, ContentType, Header, HeaderMap, Cookies, Session, CookieJar, Key};
use http::CookieJar;
use http::hyper; use http::hyper;
/// The type of an incoming web request. /// The type of an incoming web request.
@ -28,10 +25,12 @@ use http::hyper;
pub struct Request<'r> { pub struct Request<'r> {
method: Method, method: Method,
uri: URI<'r>, uri: URI<'r>,
key: Option<&'r Key>,
headers: HeaderMap<'r>, headers: HeaderMap<'r>,
remote: Option<SocketAddr>, remote: Option<SocketAddr>,
params: RefCell<Vec<(usize, usize)>>, params: RefCell<Vec<(usize, usize)>>,
cookies: RefCell<CookieJar>, cookies: RefCell<CookieJar>,
session: RefCell<CookieJar>,
state: Option<&'r Container>, state: Option<&'r Container>,
} }
@ -54,9 +53,11 @@ impl<'r> Request<'r> {
method: method, method: method,
uri: uri.into(), uri: uri.into(),
headers: HeaderMap::new(), headers: HeaderMap::new(),
key: None,
remote: None, remote: None,
params: RefCell::new(Vec::new()), params: RefCell::new(Vec::new()),
cookies: RefCell::new(CookieJar::new()), cookies: RefCell::new(CookieJar::new()),
session: RefCell::new(CookieJar::new()),
state: None state: None
} }
} }
@ -256,7 +257,7 @@ impl<'r> Request<'r> {
#[inline] #[inline]
pub fn cookies(&self) -> Cookies { pub fn cookies(&self) -> Cookies {
match self.cookies.try_borrow_mut() { match self.cookies.try_borrow_mut() {
Ok(jar) => Cookies::from(jar), Ok(jar) => Cookies::new(jar),
Err(_) => { Err(_) => {
error_!("Multiple `Cookies` instances are active at once."); error_!("Multiple `Cookies` instances are active at once.");
info_!("An instance of `Cookies` must be dropped before another \ info_!("An instance of `Cookies` must be dropped before another \
@ -267,12 +268,32 @@ impl<'r> Request<'r> {
} }
} }
/// Replace all of the cookies in `self` with `cookies`. #[inline]
pub fn session(&self) -> Session {
match self.session.try_borrow_mut() {
Ok(jar) => Session::new(jar, self.key.unwrap()),
Err(_) => {
error_!("Multiple `Session` instances are active at once.");
info_!("An instance of `Session` must be dropped before another \
can be retrieved.");
warn_!("The retrieved `Session` instance will be empty.");
Session::empty(self.key.unwrap())
}
}
}
/// Replace all of the cookies in `self` with those in `jar`.
#[inline] #[inline]
pub(crate) fn set_cookies(&mut self, jar: CookieJar) { pub(crate) fn set_cookies(&mut self, jar: CookieJar) {
self.cookies = RefCell::new(jar); self.cookies = RefCell::new(jar);
} }
/// Replace all of the session cookie in `self` with those in `jar`.
#[inline]
pub(crate) fn set_session(&mut self, jar: CookieJar) {
self.session = RefCell::new(jar);
}
/// Returns `Some` of the Content-Type header of `self`. If the header is /// Returns `Some` of the Content-Type header of `self`. If the header is
/// not present, returns `None`. /// not present, returns `None`.
/// ///
@ -407,6 +428,12 @@ impl<'r> Request<'r> {
self.state = Some(state); self.state = Some(state);
} }
/// Set the session key. For internal use only!
#[inline]
pub(crate) fn set_key(&mut self, key: &'r Key) {
self.key = Some(key);
}
/// Convert from Hyper types into a Rocket Request. /// Convert from Hyper types into a Rocket Request.
pub(crate) fn from_hyp(h_method: hyper::Method, pub(crate) fn from_hyp(h_method: hyper::Method,
h_headers: hyper::header::Headers, h_headers: hyper::header::Headers,
@ -428,26 +455,27 @@ impl<'r> Request<'r> {
// Construct the request object. // Construct the request object.
let mut request = Request::new(method, uri); let mut request = Request::new(method, uri);
// Set the request cookies, if they exist. TODO: Use session key. // Set the request cookies, if they exist.
if let Some(cookie_headers) = h_headers.get_raw("Cookie") { if let Some(cookie_headers) = h_headers.get_raw("Cookie") {
let mut jar = CookieJar::new(); let mut cookie_jar = CookieJar::new();
let mut session_jar = CookieJar::new();
for header in cookie_headers { for header in cookie_headers {
let raw_str = match ::std::str::from_utf8(header) { let raw_str = match ::std::str::from_utf8(header) {
Ok(string) => string, Ok(string) => string,
Err(_) => continue Err(_) => continue
}; };
for cookie_str in raw_str.split(";") { for cookie_str in raw_str.split(";").map(|s| s.trim()) {
let cookie = match Cookie::parse_encoded(cookie_str.to_string()) { if let Some(cookie) = Session::parse_cookie(cookie_str) {
Ok(cookie) => cookie, session_jar.add_original(cookie);
Err(_) => continue } else if let Some(cookie) = Cookies::parse_cookie(cookie_str) {
}; cookie_jar.add_original(cookie);
}
jar.add_original(cookie);
} }
} }
request.set_cookies(jar); request.set_cookies(cookie_jar);
request.set_session(session_jar);
} }
// Set the rest of the headers. // Set the rest of the headers.

View File

@ -223,10 +223,6 @@ impl<'a, 'r> FromRequest<'a, 'r> for Flash<()> {
let r = request.cookies().get(FLASH_COOKIE_NAME).ok_or(()).and_then(|cookie| { let r = request.cookies().get(FLASH_COOKIE_NAME).ok_or(()).and_then(|cookie| {
trace_!("Flash: retrieving message: {:?}", cookie); trace_!("Flash: retrieving message: {:?}", cookie);
// Delete the flash cookie from the jar.
let orig_cookie = Cookie::build(FLASH_COOKIE_NAME, "").path("/").finish();
request.cookies().remove(orig_cookie);
// Parse the flash message. // Parse the flash message.
let content = cookie.value(); let content = cookie.value();
let (len_str, rest) = match content.find(|c: char| !c.is_digit(10)) { let (len_str, rest) = match content.find(|c: char| !c.is_digit(10)) {
@ -239,6 +235,12 @@ impl<'a, 'r> FromRequest<'a, 'r> for Flash<()> {
Ok(Flash::named(name, msg)) Ok(Flash::named(name, msg))
}); });
// If we found a flash cookie, delete it from the jar.
if r.is_ok() {
let cookie = Cookie::build(FLASH_COOKIE_NAME, "").path("/").finish();
request.cookies().remove(cookie);
}
r.into_outcome() r.into_outcome()
} }
} }

View File

@ -20,7 +20,7 @@ use catcher::{self, Catcher};
use outcome::Outcome; use outcome::Outcome;
use error::Error; use error::Error;
use http::{Method, Status, Header}; use http::{Method, Status, Header, Session};
use http::hyper::{self, header}; use http::hyper::{self, header};
use http::uri::URI; use http::uri::URI;
@ -182,8 +182,9 @@ impl Rocket {
-> Response<'r> { -> Response<'r> {
info!("{}:", request); info!("{}:", request);
// Inform the request about the state. // Inform the request about the state and session key.
request.set_state(&self.state); request.set_state(&self.state);
request.set_key(&self.config.session_key());
// Do a bit of preprocessing before routing. // Do a bit of preprocessing before routing.
self.preprocess_request(request, &data); self.preprocess_request(request, &data);
@ -191,11 +192,16 @@ impl Rocket {
// Route the request to get a response. // Route the request to get a response.
match self.route(request, data) { match self.route(request, data) {
Outcome::Success(mut response) => { Outcome::Success(mut response) => {
// A user's route responded! // A user's route responded! Set the regular cookies.
for cookie in request.cookies().delta() { for cookie in request.cookies().delta() {
response.adjoin_header(cookie); response.adjoin_header(cookie);
} }
// And now the session cookies.
for cookie in request.session().delta() {
response.adjoin_header(Session::header_for(cookie));
}
response response
} }
Outcome::Forward(data) => { Outcome::Forward(data) => {
@ -342,13 +348,7 @@ impl Rocket {
info_!("port: {}", White.paint(&config.port)); info_!("port: {}", White.paint(&config.port));
info_!("log: {}", White.paint(config.log_level)); info_!("log: {}", White.paint(config.log_level));
info_!("workers: {}", White.paint(config.workers)); info_!("workers: {}", White.paint(config.workers));
info_!("session key: {}", White.paint(config.session_key.kind()));
let session_key = config.take_session_key();
if session_key.is_some() {
info_!("session key: {}", White.paint("present"));
warn_!("Signing and encryption of cookies is currently disabled.");
warn_!("See https://github.com/SergioBenitez/Rocket/issues/20 for info.");
}
for (name, value) in config.extras() { for (name, value) in config.extras() {
info_!("{} {}: {}", Yellow.paint("[extra]"), name, White.paint(value)); info_!("{} {}: {}", Yellow.paint("[extra]"), name, White.paint(value));