Update to cookie 0.7. Use 256-bit session_keys.

This commit involves several breaking changes:
  * `session_key` config param must be a 256-bit base64 encoded string.
  * `FromRequest` is implemented for `Cookies`, not `Cookie`.
  * Only a single `Cookies` instance can be retrieved at a time.
  * `Config::take_session_key` returns a `Vec<u8>`.
  * `Into<Header>` is implemented for `&Cookie`, not `Cookie`.
This commit is contained in:
Sergio Benitez 2017-03-07 01:19:06 -08:00
parent 6be902162d
commit 722ee93f8b
13 changed files with 129 additions and 75 deletions

View File

@ -16,7 +16,7 @@ struct User<'a> {
fn get<'r>(name: &str, fn get<'r>(name: &str,
query: User<'r>, query: User<'r>,
user: Form<'r, User<'r>>, user: Form<'r, User<'r>>,
cookies: &Cookies) cookies: Cookies)
-> &'static str { -> &'static str {
"hi" "hi"
} }

View File

@ -15,7 +15,7 @@ port = 80
log = "normal" log = "normal"
workers = 8 workers = 8
# don't use this key! generate your own and keep it private! # don't use this key! generate your own and keep it private!
session_key = "VheMwXIBygSmOlZAhuWl2B+zgvTN3WW5" session_key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg="
[production] [production]
address = "0.0.0.0" address = "0.0.0.0"
@ -23,4 +23,4 @@ port = 80
workers = 12 workers = 12
log = "critical" log = "critical"
# don't use this key! generate your own and keep it private! # don't use this key! generate your own and keep it private!
session_key = "adL5fFIPmZBrlyHk2YT4NLV3YCk2gFXz" session_key = "hPRYyVRiMyxpw5sBB1XeCMN1kFsDCqKvBi2QJxBVHQk="

View File

@ -20,14 +20,14 @@ struct Message {
} }
#[post("/submit", data = "<message>")] #[post("/submit", data = "<message>")]
fn submit(cookies: &Cookies, message: Form<Message>) -> Redirect { fn submit(mut cookies: Cookies, message: Form<Message>) -> Redirect {
cookies.add(Cookie::new("message", message.into_inner().message)); cookies.add(Cookie::new("message", message.into_inner().message));
Redirect::to("/") Redirect::to("/")
} }
#[get("/")] #[get("/")]
fn index(cookies: &Cookies) -> Template { fn index(cookies: Cookies) -> Template {
let cookie = cookies.find("message"); let cookie = cookies.get("message");
let mut context = HashMap::new(); let mut context = HashMap::new();
if let Some(ref cookie) = cookie { if let Some(ref cookie) = cookie {
context.insert("message", cookie.value()); context.insert("message", cookie.value());

View File

@ -25,10 +25,13 @@ state = "^0.2"
time = "^0.1" time = "^0.1"
memchr = "1" memchr = "1"
# FIXME: session support should be optional
base64 = "0.4"
# FIXME: session support should be optional
[dependencies.cookie] [dependencies.cookie]
version = "^0.6" version = "^0.7"
default-features = false features = ["percent-encode", "secure"]
features = ["percent-encode"]
[dev-dependencies] [dev-dependencies]
lazy_static = "0.2" lazy_static = "0.2"

View File

@ -152,12 +152,12 @@ impl ConfigBuilder {
/// use rocket::LoggingLevel; /// use rocket::LoggingLevel;
/// use rocket::config::{Config, Environment}; /// use rocket::config::{Config, Environment};
/// ///
/// let key = "VheMwXIBygSmOlZAhuWl2B+zgvTN3WW5"; /// let key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg=";
/// let mut config = Config::build(Environment::Staging) /// let mut config = Config::build(Environment::Staging)
/// .session_key(key) /// .session_key(key)
/// .unwrap(); /// .unwrap();
/// ///
/// assert_eq!(config.take_session_key(), Some(key.to_string())); /// 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

@ -9,7 +9,7 @@ use std::env;
use config::Environment::*; use config::Environment::*;
use config::{self, Value, ConfigBuilder, Environment, ConfigError}; use config::{self, Value, ConfigBuilder, Environment, ConfigError};
use num_cpus; use {num_cpus, base64};
use logger::LoggingLevel; use logger::LoggingLevel;
/// Structure for Rocket application configuration. /// Structure for Rocket application configuration.
@ -44,7 +44,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<String>>, session_key: RwLock<Option<Vec<u8>>>,
} }
macro_rules! parse { macro_rules! parse {
@ -175,12 +175,6 @@ impl Config {
ConfigError::BadType(id, expect, actual, self.config_path.clone()) ConfigError::BadType(id, expect, actual, self.config_path.clone())
} }
// Aliases to `set` before the method is removed.
pub(crate) fn set_raw(&mut self, name: &str, val: &Value) -> config::Result<()> {
#[allow(deprecated)]
self.set(name, val)
}
/// Sets the configuration `val` for the `name` entry. If the `name` is one /// Sets the configuration `val` for the `name` entry. If the `name` is one
/// of "address", "port", "session_key", "log", or "workers" (the "default" /// of "address", "port", "session_key", "log", or "workers" (the "default"
/// values), the appropriate value in the `self` Config structure is set. /// values), the appropriate value in the `self` Config structure is set.
@ -195,8 +189,7 @@ impl Config {
/// * **workers**: Integer (16-bit unsigned) /// * **workers**: Integer (16-bit unsigned)
/// * **log**: String /// * **log**: String
/// * **session_key**: String (192-bit base64) /// * **session_key**: String (192-bit base64)
#[deprecated(since="0.2", note="use the set_{param} methods instead")] pub(crate) fn set_raw(&mut self, name: &str, val: &Value) -> config::Result<()> {
pub fn set(&mut self, name: &str, val: &Value) -> config::Result<()> {
if name == "address" { if name == "address" {
let address_str = parse!(self, name, val, as_str, "a string")?; let address_str = parse!(self, name, val, as_str, "a string")?;
self.set_address(address_str)?; self.set_address(address_str)?;
@ -335,20 +328,27 @@ impl Config {
/// # use rocket::config::ConfigError; /// # use rocket::config::ConfigError;
/// # fn config_test() -> Result<(), ConfigError> { /// # fn config_test() -> Result<(), ConfigError> {
/// let mut config = Config::new(Environment::Staging)?; /// let mut config = Config::new(Environment::Staging)?;
/// assert!(config.set_session_key("VheMwXIBygSmOlZAhuWl2B+zgvTN3WW5").is_ok()); /// let key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg=";
/// assert!(config.set_session_key(key).is_ok());
/// assert!(config.set_session_key("hello? anyone there?").is_err()); /// assert!(config.set_session_key("hello? anyone there?").is_err());
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
pub fn set_session_key<K: Into<String>>(&mut self, key: K) -> config::Result<()> { pub fn set_session_key<K: Into<String>>(&mut self, key: K) -> config::Result<()> {
let key = key.into(); let key = key.into();
if key.len() != 32 { let error = self.bad_type("session_key", "string",
return Err(self.bad_type("session_key", "a 256-bit base64 encoded string");
"string",
"a 192-bit base64 string")); if key.len() != 44 {
return Err(error);
} }
self.session_key = RwLock::new(Some(key)); let bytes = match base64::decode(&key) {
Ok(bytes) => bytes,
Err(_) => return Err(error)
};
self.session_key = RwLock::new(Some(bytes));
Ok(()) Ok(())
} }
@ -435,21 +435,21 @@ impl Config {
/// use rocket::config::{Config, Environment}; /// use rocket::config::{Config, Environment};
/// ///
/// // Create a new config with a session key. /// // Create a new config with a session key.
/// let key = "adL5fFIPmZBrlyHk2YT4NLV3YCk2gFXz"; /// let key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg=";
/// let config = Config::build(Environment::Staging) /// let config = Config::build(Environment::Staging)
/// .session_key(key) /// .session_key(key)
/// .unwrap(); /// .unwrap();
/// ///
/// // Get the key for the first time. /// // Get the key for the first time.
/// let session_key = config.take_session_key(); /// let session_key = config.take_session_key();
/// assert_eq!(session_key, Some(key.to_string())); /// assert!(session_key.is_some());
/// ///
/// // Try to get the key again. /// // Try to get the key again.
/// let session_key_again = config.take_session_key(); /// let session_key_again = config.take_session_key();
/// assert_eq!(session_key_again, None); /// assert_eq!(session_key_again, None);
/// ``` /// ```
#[inline] #[inline]
pub fn take_session_key(&self) -> Option<String> { pub fn take_session_key(&self) -> Option<Vec<u8>> {
let mut key = self.session_key.write().expect("couldn't lock session key"); let mut key = self.session_key.write().expect("couldn't lock session key");
key.take() key.take()
} }

View File

@ -40,9 +40,9 @@
//! * examples: `12`, `1`, `4` //! * examples: `12`, `1`, `4`
//! * **log**: _[string]_ how much information to log; one of `"normal"`, //! * **log**: _[string]_ how much information to log; one of `"normal"`,
//! `"debug"`, or `"critical"` //! `"debug"`, or `"critical"`
//! * **session_key**: _[string]_ a 192-bit base64 encoded string (32 //! * **session_key**: _[string]_ a 256-bit base64 encoded string (44
//! characters) to use as the session key //! characters) to use as the session key
//! * example: `"VheMwXIBygSmOlZAhuWl2B+zgvTN3WW5"` //! * example: `"8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg="`
//! //!
//! ### Rocket.toml //! ### Rocket.toml
//! //!
@ -70,7 +70,7 @@
//! 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! //! # don't use this key! generate your own and keep it private!
//! session_key = "VheMwXIBygSmOlZAhuWl2B+zgvTN3WW5" //! session_key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg="
//! //!
//! [production] //! [production]
//! address = "0.0.0.0" //! address = "0.0.0.0"
@ -78,7 +78,7 @@
//! 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! //! # don't use this key! generate your own and keep it private!
//! session_key = "adL5fFIPmZBrlyHk2YT4NLV3YCk2gFXz" //! session_key = "hPRYyVRiMyxpw5sBB1XeCMN1kFsDCqKvBi2QJxBVHQk="
//! ``` //! ```
//! //!
//! The `workers` parameter is computed by Rocket automatically; the value above //! The `workers` parameter is computed by Rocket automatically; the value above
@ -587,7 +587,7 @@ mod test {
port = 7810 port = 7810
workers = 21 workers = 21
log = "critical" log = "critical"
session_key = "01234567890123456789012345678901" session_key = "8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg="
template_dir = "mine" template_dir = "mine"
json = true json = true
pi = 3.14 pi = 3.14
@ -598,7 +598,7 @@ mod test {
.port(7810) .port(7810)
.workers(21) .workers(21)
.log_level(LoggingLevel::Critical) .log_level(LoggingLevel::Critical)
.session_key("01234567890123456789012345678901") .session_key("8Xui8SN4mI+7egV/9dlfYYLGQJeEx4+DwmSQLwDVXJg=")
.extra("template_dir", "mine") .extra("template_dir", "mine")
.extra("json", true) .extra("json", true)
.extra("pi", 3.14); .extra("pi", 3.14);
@ -873,19 +873,19 @@ mod test {
check_config!(RocketConfig::parse(r#" check_config!(RocketConfig::parse(r#"
[stage] [stage]
session_key = "VheMwXIBygSmOlZAhuWl2B+zgvTN3WW5" session_key = "TpUiXK2d/v5DFxJnWL12suJKPExKR8h9zd/o+E7SU+0="
"#.to_string(), TEST_CONFIG_FILENAME), { "#.to_string(), TEST_CONFIG_FILENAME), {
default_config(Staging).session_key( default_config(Staging).session_key(
"VheMwXIBygSmOlZAhuWl2B+zgvTN3WW5" "TpUiXK2d/v5DFxJnWL12suJKPExKR8h9zd/o+E7SU+0="
) )
}); });
check_config!(RocketConfig::parse(r#" check_config!(RocketConfig::parse(r#"
[stage] [stage]
session_key = "adL5fFIPmZBrlyHk2YT4NLV3YCk2gFXz" session_key = "jTyprDberFUiUFsJ3vcb1XKsYHWNBRvWAnXTlbTgGFU="
"#.to_string(), TEST_CONFIG_FILENAME), { "#.to_string(), TEST_CONFIG_FILENAME), {
default_config(Staging).session_key( default_config(Staging).session_key(
"adL5fFIPmZBrlyHk2YT4NLV3YCk2gFXz" "jTyprDberFUiUFsJ3vcb1XKsYHWNBRvWAnXTlbTgGFU="
) )
}); });
} }

View File

@ -1,17 +1,62 @@
use http::Header; use http::Header;
pub use cookie::Cookie; use std::cell::RefMut;
pub use cookie::CookieJar;
pub use cookie::CookieBuilder;
/// Type alias to a `'static` CookieJar. pub use cookie::{Cookie, CookieJar, Iter, CookieBuilder, Delta};
///
/// A `CookieJar` should never be used without a `'static` lifetime. As a
/// result, you should always use this alias.
pub type Cookies = self::CookieJar<'static>;
impl<'c> From<Cookie<'c>> for Header<'static> { #[derive(Debug)]
fn from(cookie: Cookie) -> Header<'static> { pub enum Cookies<'a> {
Jarred(RefMut<'a, CookieJar>),
Empty(CookieJar)
}
impl<'a> From<RefMut<'a, CookieJar>> for Cookies<'a> {
fn from(jar: RefMut<'a, CookieJar>) -> Cookies<'a> {
Cookies::Jarred(jar)
}
}
impl<'a> Cookies<'a> {
pub(crate) fn empty() -> Cookies<'static> {
Cookies::Empty(CookieJar::new())
}
pub fn get(&self, name: &str) -> Option<&Cookie<'static>> {
match *self {
Cookies::Jarred(ref jar) => jar.get(name),
Cookies::Empty(_) => None
}
}
pub fn add(&mut self, cookie: Cookie<'static>) {
if let Cookies::Jarred(ref mut jar) = *self {
jar.add(cookie)
}
}
pub fn remove(&mut self, cookie: Cookie<'static>) {
if let Cookies::Jarred(ref mut jar) = *self {
jar.remove(cookie)
}
}
pub fn iter(&self) -> Iter {
match *self {
Cookies::Jarred(ref jar) => jar.iter(),
Cookies::Empty(ref jar) => jar.iter()
}
}
pub fn delta(&self) -> Delta {
match *self {
Cookies::Jarred(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()) Header::new("Set-Cookie", cookie.encoded().to_string())
} }
} }

View File

@ -340,10 +340,10 @@ impl<'h> HeaderMap<'h> {
/// ///
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// map.add(Cookie::new("a", "b")); /// map.add(&Cookie::new("a", "b"));
/// assert_eq!(map.get("Set-Cookie").count(), 1); /// assert_eq!(map.get("Set-Cookie").count(), 1);
/// ///
/// map.add(Cookie::new("c", "d")); /// map.add(&Cookie::new("c", "d"));
/// assert_eq!(map.get("Set-Cookie").count(), 2); /// assert_eq!(map.get("Set-Cookie").count(), 2);
/// ``` /// ```
#[inline(always)] #[inline(always)]

View File

@ -104,6 +104,7 @@ extern crate state;
extern crate cookie; extern crate cookie;
extern crate time; extern crate time;
extern crate memchr; extern crate memchr;
extern crate base64;
#[cfg(test)] #[macro_use] extern crate lazy_static; #[cfg(test)] #[macro_use] extern crate lazy_static;

View File

@ -204,7 +204,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for Method {
} }
} }
impl<'a, 'r> FromRequest<'a, 'r> for &'a Cookies { impl<'a, 'r> FromRequest<'a, 'r> for Cookies<'a> {
type Error = (); type Error = ();
fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> { fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> {

View File

@ -14,6 +14,8 @@ 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, Cookie, Cookies};
use http::CookieJar;
use http::hyper; use http::hyper;
/// The type of an incoming web request. /// The type of an incoming web request.
@ -29,7 +31,7 @@ pub struct Request<'r> {
headers: HeaderMap<'r>, headers: HeaderMap<'r>,
remote: Option<SocketAddr>, remote: Option<SocketAddr>,
params: RefCell<Vec<(usize, usize)>>, params: RefCell<Vec<(usize, usize)>>,
cookies: Cookies, cookies: RefCell<CookieJar>,
state: Option<&'r Container>, state: Option<&'r Container>,
} }
@ -54,7 +56,7 @@ impl<'r> Request<'r> {
headers: HeaderMap::new(), headers: HeaderMap::new(),
remote: None, remote: None,
params: RefCell::new(Vec::new()), params: RefCell::new(Vec::new()),
cookies: Cookies::new(&[]), cookies: RefCell::new(CookieJar::new()),
state: None state: None
} }
} }
@ -251,15 +253,24 @@ impl<'r> Request<'r> {
/// request.cookies().add(Cookie::new("key", "val")); /// request.cookies().add(Cookie::new("key", "val"));
/// request.cookies().add(Cookie::new("ans", format!("life: {}", 38 + 4))); /// request.cookies().add(Cookie::new("ans", format!("life: {}", 38 + 4)));
/// ``` /// ```
#[inline(always)] #[inline]
pub fn cookies(&self) -> &Cookies { pub fn cookies(&self) -> Cookies {
&self.cookies match self.cookies.try_borrow_mut() {
Ok(jar) => Cookies::from(jar),
Err(_) => {
error_!("Multiple `Cookies` instances are active at once.");
info_!("An instance of `Cookies` must be dropped before another \
can be retrieved.");
warn_!("The retrieved `Cookies` instance will be empty.");
Cookies::empty()
}
}
} }
/// Replace all of the cookies in `self` with `cookies`. /// Replace all of the cookies in `self` with `cookies`.
#[inline] #[inline]
pub(crate) fn set_cookies(&mut self, cookies: Cookies) { pub(crate) fn set_cookies(&mut self, jar: CookieJar) {
self.cookies = cookies; self.cookies = 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
@ -419,7 +430,7 @@ impl<'r> Request<'r> {
// Set the request cookies, if they exist. TODO: Use session key. // Set the request cookies, if they exist. TODO: Use session key.
if let Some(cookie_headers) = h_headers.get_raw("Cookie") { if let Some(cookie_headers) = h_headers.get_raw("Cookie") {
let mut cookies = Cookies::new(&[]); let mut 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,
@ -432,11 +443,11 @@ impl<'r> Request<'r> {
Err(_) => continue Err(_) => continue
}; };
cookies.add_original(cookie); jar.add_original(cookie);
} }
} }
request.set_cookies(cookies); request.set_cookies(jar);
} }
// Set the rest of the headers. // Set the rest of the headers.

View File

@ -1,6 +1,6 @@
use std::convert::AsRef; use std::convert::AsRef;
use time::{self, Duration}; use time::Duration;
use outcome::IntoOutcome; use outcome::IntoOutcome;
use response::{Response, Responder}; use response::{Response, Responder};
@ -184,7 +184,7 @@ impl<'r, R: Responder<'r>> Responder<'r> for Flash<R> {
trace_!("Flash: setting message: {}:{}", self.name, self.message); trace_!("Flash: setting message: {}:{}", self.name, self.message);
let cookie = self.cookie(); let cookie = self.cookie();
Response::build_from(self.responder.respond()?) Response::build_from(self.responder.respond()?)
.header_adjoin(cookie) .header_adjoin(&cookie)
.ok() .ok()
} }
} }
@ -220,18 +220,12 @@ impl<'a, 'r> FromRequest<'a, 'r> for Flash<()> {
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
trace_!("Flash: attemping to retrieve message."); trace_!("Flash: attemping to retrieve message.");
let r = request.cookies().find(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);
// Create the "deletion" cookie. We'll use it to clear the cookie. // Delete the flash cookie from the jar.
let delete_cookie = Cookie::build(FLASH_COOKIE_NAME, "") let orig_cookie = Cookie::build(FLASH_COOKIE_NAME, "").path("/").finish();
.max_age(Duration::seconds(0)) request.cookies().remove(orig_cookie);
.expires(time::now() - Duration::days(365))
.path("/")
.finish();
// Add the deletion to the cookie jar, replacing the existing cookie.
request.cookies().add(delete_cookie);
// Parse the flash message. // Parse the flash message.
let content = cookie.value(); let content = cookie.value();