Introduce the 'private-cookies' feature.

This commit is contained in:
Linus Unnebäck 2018-11-05 20:29:03 +00:00 committed by Sergio Benitez
parent b16269a30e
commit 53758c6dd7
14 changed files with 164 additions and 87 deletions

View File

@ -6,6 +6,7 @@ env:
- TEST_FLAGS= - TEST_FLAGS=
- TEST_FLAGS=--release - TEST_FLAGS=--release
- TEST_FLAGS=--contrib - TEST_FLAGS=--contrib
- TEST_FLAGS=--core
rust: rust:
- nightly - nightly
script: ./scripts/test.sh $TEST_FLAGS script: ./scripts/test.sh $TEST_FLAGS

View File

@ -15,6 +15,7 @@ categories = ["web-programming"]
[features] [features]
tls = ["rustls", "hyper-sync-rustls"] tls = ["rustls", "hyper-sync-rustls"]
private-cookies = ["cookie/secure"]
[dependencies] [dependencies]
smallvec = "0.6" smallvec = "0.6"
@ -24,7 +25,7 @@ time = "0.1"
indexmap = "1.0" indexmap = "1.0"
rustls = { version = "0.14", optional = true } rustls = { version = "0.14", optional = true }
state = "0.4" state = "0.4"
cookie = { version = "0.11", features = ["percent-encode", "secure"] } cookie = { version = "0.11", features = ["percent-encode"] }
pear = "0.1" pear = "0.1"
unicode-xid = "0.1" unicode-xid = "0.1"

View File

@ -2,10 +2,16 @@ use std::fmt;
use std::cell::RefMut; use std::cell::RefMut;
use cookie::Delta; use cookie::Delta;
pub use cookie::{Cookie, Key, CookieJar, SameSite}; pub use cookie::{Cookie, CookieJar, SameSite};
#[cfg(feature = "private-cookies")]
pub use cookie::Key;
use Header; use Header;
#[cfg(not(feature = "private-cookies"))]
type Key = ();
/// Collection of one or more HTTP cookies. /// Collection of one or more HTTP cookies.
/// ///
/// The `Cookies` type allows for retrieval of cookies from an incoming request /// The `Cookies` type allows for retrieval of cookies from an incoming request
@ -166,28 +172,6 @@ impl<'a> Cookies<'a> {
} }
} }
/// Returns a reference to the `Cookie` inside this collection with the name
/// `name` and authenticates and decrypts the cookie's value, returning a
/// `Cookie` with the decrypted value. If the cookie cannot be found, or the
/// cookie fails to authenticate or decrypt, `None` is returned.
///
/// # Example
///
/// ```rust
/// # extern crate rocket;
/// use rocket::http::Cookies;
///
/// fn handler(mut cookies: Cookies) {
/// let cookie = cookies.get_private("name");
/// }
/// ```
pub fn get_private(&mut self, name: &str) -> Option<Cookie<'static>> {
match *self {
Cookies::Jarred(ref mut jar, key) => jar.private(key).get(name),
Cookies::Empty(_) => None
}
}
/// Adds `cookie` to this collection. /// Adds `cookie` to this collection.
/// ///
/// # Example /// # Example
@ -213,6 +197,90 @@ impl<'a> Cookies<'a> {
} }
} }
/// Removes `cookie` from this collection and generates a "removal" cookies
/// to send to the client on response. For correctness, `cookie` must
/// contain the same `path` and `domain` as the cookie that was initially
/// set. Failure to provide the initial `path` and `domain` will result in
/// cookies that are not properly removed.
///
/// A "removal" cookie is a cookie that has the same name as the original
/// cookie but has an empty value, a max-age of 0, and an expiration date
/// far in the past.
///
/// # Example
///
/// ```rust
/// # extern crate rocket;
/// use rocket::http::{Cookie, Cookies};
///
/// fn handler(mut cookies: Cookies) {
/// cookies.remove(Cookie::named("name"));
/// }
/// ```
pub fn remove(&mut self, cookie: Cookie<'static>) {
if let Cookies::Jarred(ref mut jar, _) = *self {
jar.remove(cookie)
}
}
/// Returns an iterator over all of the cookies present in this collection.
///
/// # Example
///
/// ```rust
/// # extern crate rocket;
/// use rocket::http::Cookies;
///
/// fn handler(cookies: Cookies) {
/// for c in cookies.iter() {
/// println!("Name: '{}', Value: '{}'", c.name(), c.value());
/// }
/// }
/// ```
pub fn iter(&self) -> impl Iterator<Item=&Cookie<'static>> {
match *self {
Cookies::Jarred(ref jar, _) => jar.iter(),
Cookies::Empty(ref jar) => jar.iter()
}
}
/// WARNING: This is unstable! Do not use this method outside of Rocket!
#[doc(hidden)]
#[inline]
pub fn delta(&self) -> Delta {
match *self {
Cookies::Jarred(ref jar, _) => jar.delta(),
Cookies::Empty(ref jar) => jar.delta()
}
}
}
#[cfg(feature = "private-cookies")]
impl<'a> Cookies<'a> {
/// Returns a reference to the `Cookie` inside this collection with the name
/// `name` and authenticates and decrypts the cookie's value, returning a
/// `Cookie` with the decrypted value. If the cookie cannot be found, or the
/// cookie fails to authenticate or decrypt, `None` is returned.
///
/// This method is only available when the `private-cookies` feature is enabled.
///
/// # Example
///
/// ```rust
/// # extern crate rocket;
/// use rocket::http::Cookies;
///
/// fn handler(mut cookies: Cookies) {
/// let cookie = cookies.get_private("name");
/// }
/// ```
pub fn get_private(&mut self, name: &str) -> Option<Cookie<'static>> {
match *self {
Cookies::Jarred(ref mut jar, key) => jar.private(key).get(name),
Cookies::Empty(_) => None
}
}
/// Adds `cookie` to the collection. The cookie's value is encrypted with /// Adds `cookie` to the collection. The cookie's value is encrypted with
/// authenticated encryption assuring confidentiality, integrity, and /// authenticated encryption assuring confidentiality, integrity, and
/// authenticity. The cookie can later be retrieved using /// authenticity. The cookie can later be retrieved using
@ -230,6 +298,8 @@ impl<'a> Cookies<'a> {
/// These defaults ensure maximum usability and security. For additional /// These defaults ensure maximum usability and security. For additional
/// security, you may wish to set the `secure` flag. /// security, you may wish to set the `secure` flag.
/// ///
/// This method is only available when the `private-cookies` feature is enabled.
///
/// # Example /// # Example
/// ///
/// ```rust /// ```rust
@ -267,6 +337,8 @@ impl<'a> Cookies<'a> {
/// * `HttpOnly`: `true` /// * `HttpOnly`: `true`
/// * `Expires`: 1 week from now /// * `Expires`: 1 week from now
/// ///
/// This method is only available when the `private-cookies` feature is enabled.
///
fn set_private_defaults(cookie: &mut Cookie<'static>) { fn set_private_defaults(cookie: &mut Cookie<'static>) {
if cookie.path().is_none() { if cookie.path().is_none() {
cookie.set_path("/"); cookie.set_path("/");
@ -285,38 +357,14 @@ impl<'a> Cookies<'a> {
} }
} }
/// Removes `cookie` from this collection and generates a "removal" cookies
/// to send to the client on response. For correctness, `cookie` must
/// contain the same `path` and `domain` as the cookie that was initially
/// set. Failure to provide the initial `path` and `domain` will result in
/// cookies that are not properly removed.
///
/// A "removal" cookie is a cookie that has the same name as the original
/// cookie but has an empty value, a max-age of 0, and an expiration date
/// far in the past.
///
/// # Example
///
/// ```rust
/// # extern crate rocket;
/// use rocket::http::{Cookie, Cookies};
///
/// fn handler(mut cookies: Cookies) {
/// cookies.remove(Cookie::named("name"));
/// }
/// ```
pub fn remove(&mut self, cookie: Cookie<'static>) {
if let Cookies::Jarred(ref mut jar, _) = *self {
jar.remove(cookie)
}
}
/// Removes the private `cookie` from the collection. /// Removes the private `cookie` from the collection.
/// ///
/// For correct removal, the passed in `cookie` must contain the same `path` /// For correct removal, the passed in `cookie` must contain the same `path`
/// and `domain` as the cookie that was initially set. If a path is not set /// and `domain` as the cookie that was initially set. If a path is not set
/// on `cookie`, the `"/"` path will automatically be set. /// on `cookie`, the `"/"` path will automatically be set.
/// ///
/// This method is only available when the `private-cookies` feature is enabled.
///
/// # Example /// # Example
/// ///
/// ```rust /// ```rust
@ -336,37 +384,6 @@ impl<'a> Cookies<'a> {
jar.private(key).remove(cookie) jar.private(key).remove(cookie)
} }
} }
/// Returns an iterator over all of the cookies present in this collection.
///
/// # Example
///
/// ```rust
/// # extern crate rocket;
/// use rocket::http::Cookies;
///
/// fn handler(cookies: Cookies) {
/// for c in cookies.iter() {
/// println!("Name: '{}', Value: '{}'", c.name(), c.value());
/// }
/// }
/// ```
pub fn iter(&self) -> impl Iterator<Item=&Cookie<'static>> {
match *self {
Cookies::Jarred(ref jar, _) => jar.iter(),
Cookies::Empty(ref jar) => jar.iter()
}
}
/// WARNING: This is unstable! Do not use this method outside of Rocket!
#[doc(hidden)]
#[inline]
pub fn delta(&self) -> Delta {
match *self {
Cookies::Jarred(ref jar, _) => jar.delta(),
Cookies::Empty(ref jar) => jar.delta()
}
}
} }
impl<'a> fmt::Debug for Cookies<'a> { impl<'a> fmt::Debug for Cookies<'a> {

View File

@ -59,7 +59,8 @@ pub mod uncased;
#[doc(hidden)] pub use smallvec::{SmallVec, Array}; #[doc(hidden)] pub use smallvec::{SmallVec, Array};
// This one we need to expose for core. // This one we need to expose for core.
#[doc(hidden)] pub use cookies::{Key, CookieJar}; #[doc(hidden)] pub use cookies::CookieJar;
#[doc(hidden)] #[cfg(feature = "private-cookies")] pub use cookies::Key;
pub use method::Method; pub use method::Method;
pub use content_type::ContentType; pub use content_type::ContentType;

View File

@ -16,6 +16,7 @@ categories = ["web-programming::http-server"]
[features] [features]
tls = ["rocket_http/tls"] tls = ["rocket_http/tls"]
private-cookies = ["rocket_http/private-cookies"]
[dependencies] [dependencies]
rocket_codegen = { version = "0.4.0-rc.1", path = "../codegen" } rocket_codegen = { version = "0.4.0-rc.1", path = "../codegen" }

View File

@ -10,7 +10,8 @@ use {num_cpus, base64};
use config::Environment::*; use config::Environment::*;
use config::{Result, ConfigBuilder, Environment, ConfigError, LoggingLevel}; use config::{Result, ConfigBuilder, Environment, ConfigError, LoggingLevel};
use config::{Table, Value, Array, Datetime}; use config::{Table, Value, Array, Datetime};
use http::Key;
#[cfg(feature = "private-cookies")] use http::Key;
/// Structure for Rocket application configuration. /// Structure for Rocket application configuration.
/// ///
@ -49,6 +50,7 @@ pub struct Config {
/// How much information to log. /// How much information to log.
pub log_level: LoggingLevel, pub log_level: LoggingLevel,
/// The secret key. /// The secret key.
#[cfg(feature = "private-cookies")]
crate secret_key: SecretKey, crate secret_key: SecretKey,
/// TLS configuration. /// TLS configuration.
crate tls: Option<TlsConfig>, crate tls: Option<TlsConfig>,
@ -231,6 +233,7 @@ impl Config {
let default_workers = (num_cpus::get() * 2) as u16; let default_workers = (num_cpus::get() * 2) as u16;
// Use a generated secret key by default. // Use a generated secret key by default.
#[cfg(feature = "private-cookies")]
let key = SecretKey::Generated(Key::generate()); let key = SecretKey::Generated(Key::generate());
Ok(match env { Ok(match env {
@ -242,6 +245,7 @@ impl Config {
workers: default_workers, workers: default_workers,
keep_alive: Some(5), keep_alive: Some(5),
log_level: LoggingLevel::Normal, log_level: LoggingLevel::Normal,
#[cfg(feature = "private-cookies")]
secret_key: key, secret_key: key,
tls: None, tls: None,
limits: Limits::default(), limits: Limits::default(),
@ -257,6 +261,7 @@ impl Config {
workers: default_workers, workers: default_workers,
keep_alive: Some(5), keep_alive: Some(5),
log_level: LoggingLevel::Normal, log_level: LoggingLevel::Normal,
#[cfg(feature = "private-cookies")]
secret_key: key, secret_key: key,
tls: None, tls: None,
limits: Limits::default(), limits: Limits::default(),
@ -272,6 +277,7 @@ impl Config {
workers: default_workers, workers: default_workers,
keep_alive: Some(5), keep_alive: Some(5),
log_level: LoggingLevel::Critical, log_level: LoggingLevel::Critical,
#[cfg(feature = "private-cookies")]
secret_key: key, secret_key: key,
tls: None, tls: None,
limits: Limits::default(), limits: Limits::default(),
@ -473,6 +479,7 @@ impl Config {
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
#[cfg(feature = "private-cookies")]
pub fn set_secret_key<K: Into<String>>(&mut self, key: K) -> Result<()> { pub fn set_secret_key<K: Into<String>>(&mut self, key: K) -> Result<()> {
let key = key.into(); let key = key.into();
let error = self.bad_type("secret_key", "string", let error = self.bad_type("secret_key", "string",
@ -490,6 +497,10 @@ impl Config {
self.secret_key = SecretKey::Provided(Key::from_master(&bytes)); self.secret_key = SecretKey::Provided(Key::from_master(&bytes));
Ok(()) Ok(())
} }
#[cfg(not(feature = "private-cookies"))]
pub fn set_secret_key<K: Into<String>>(&mut self, key: K) -> Result<()> {
Ok(())
}
/// Sets the logging level for `self` to `log_level`. /// Sets the logging level for `self` to `log_level`.
/// ///
@ -663,6 +674,7 @@ impl Config {
} }
/// Retrieves the secret key from `self`. /// Retrieves the secret key from `self`.
#[cfg(feature = "private-cookies")]
#[inline] #[inline]
crate fn secret_key(&self) -> &Key { crate fn secret_key(&self) -> &Key {
self.secret_key.inner() self.secret_key.inner()

View File

@ -3,14 +3,17 @@ use std::fmt;
#[cfg(feature = "tls")] use http::tls::{Certificate, PrivateKey}; #[cfg(feature = "tls")] use http::tls::{Certificate, PrivateKey};
use config::{Result, Config, Value, ConfigError, LoggingLevel}; use config::{Result, Config, Value, ConfigError, LoggingLevel};
use http::Key;
#[cfg(feature = "private-cookies")] use http::Key;
#[cfg(feature = "private-cookies")]
#[derive(Clone)] #[derive(Clone)]
pub enum SecretKey { pub enum SecretKey {
Generated(Key), Generated(Key),
Provided(Key) Provided(Key)
} }
#[cfg(feature = "private-cookies")]
impl SecretKey { impl SecretKey {
#[inline] #[inline]
crate fn inner(&self) -> &Key { crate fn inner(&self) -> &Key {
@ -28,6 +31,7 @@ impl SecretKey {
} }
} }
#[cfg(feature = "private-cookies")]
impl fmt::Display for SecretKey { impl fmt::Display for SecretKey {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self { match *self {

View File

@ -269,6 +269,8 @@ impl<'c> LocalRequest<'c> {
/// ///
/// [private cookie]: ::http::Cookies::add_private() /// [private cookie]: ::http::Cookies::add_private()
/// ///
/// This method is only available when the `private-cookies` feature is enabled.
///
/// # Examples /// # Examples
/// ///
/// Add `user_id` as a private cookie: /// Add `user_id` as a private cookie:
@ -281,6 +283,7 @@ impl<'c> LocalRequest<'c> {
/// # #[allow(unused_variables)] /// # #[allow(unused_variables)]
/// let req = client.get("/").private_cookie(Cookie::new("user_id", "sb")); /// let req = client.get("/").private_cookie(Cookie::new("user_id", "sb"));
/// ``` /// ```
#[cfg(feature = "private-cookies")]
#[inline] #[inline]
pub fn private_cookie(self, cookie: Cookie<'static>) -> Self { pub fn private_cookie(self, cookie: Cookie<'static>) -> Self {
self.request.cookies().add_original_private(cookie); self.request.cookies().add_original_private(cookie);

View File

@ -290,7 +290,10 @@ impl<'r> Request<'r> {
pub fn cookies(&self) -> Cookies { pub fn cookies(&self) -> Cookies {
// FIXME: Can we do better? This is disappointing. // FIXME: Can we do better? This is disappointing.
match self.state.cookies.try_borrow_mut() { match self.state.cookies.try_borrow_mut() {
#[cfg(feature = "private-cookies")]
Ok(jar) => Cookies::new(jar, self.state.config.secret_key()), Ok(jar) => Cookies::new(jar, self.state.config.secret_key()),
#[cfg(not(feature = "private-cookies"))]
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 \

View File

@ -396,6 +396,7 @@ impl Rocket {
launch_info_!("port: {}", Paint::white(&config.port)); launch_info_!("port: {}", Paint::white(&config.port));
launch_info_!("log: {}", Paint::white(config.log_level)); launch_info_!("log: {}", Paint::white(config.log_level));
launch_info_!("workers: {}", Paint::white(config.workers)); launch_info_!("workers: {}", Paint::white(config.workers));
#[cfg(feature = "private-cookies")]
launch_info_!("secret key: {}", Paint::white(&config.secret_key)); launch_info_!("secret key: {}", Paint::white(&config.secret_key));
launch_info_!("limits: {}", Paint::white(&config.limits)); launch_info_!("limits: {}", Paint::white(&config.limits));
@ -414,8 +415,10 @@ impl Rocket {
launch_info_!("tls: {}", Paint::white("disabled")); launch_info_!("tls: {}", Paint::white("disabled"));
} }
if config.secret_key.is_generated() && config.environment.is_prod() { #[cfg(feature = "private-cookies")] {
warn!("environment is 'production', but no `secret_key` is configured"); if config.secret_key.is_generated() && config.environment.is_prod() {
warn!("environment is 'production', but no `secret_key` is configured");
}
} }
for (name, value) in config.extras() { for (name, value) in config.extras() {

View File

@ -4,6 +4,7 @@
use rocket::http::Cookies; use rocket::http::Cookies;
#[cfg(feature = "private-cookies")]
#[get("/")] #[get("/")]
fn return_private_cookie(mut cookies: Cookies) -> Option<String> { fn return_private_cookie(mut cookies: Cookies) -> Option<String> {
match cookies.get_private("cookie_name") { match cookies.get_private("cookie_name") {
@ -12,6 +13,7 @@ fn return_private_cookie(mut cookies: Cookies) -> Option<String> {
} }
} }
#[cfg(feature = "private-cookies")]
mod tests { mod tests {
use super::*; use super::*;
use rocket::local::Client; use rocket::local::Client;

View File

@ -5,7 +5,7 @@ workspace = "../../"
publish = false publish = false
[dependencies] [dependencies]
rocket = { path = "../../core/lib" } rocket = { path = "../../core/lib", features = ["private-cookies"] }
[dependencies.rocket_contrib] [dependencies.rocket_contrib]
path = "../../contrib/lib" path = "../../contrib/lib"

View File

@ -107,6 +107,26 @@ if [ "$1" = "--contrib" ]; then
CARGO_INCREMENTAL=0 cargo test --no-default-features --features "${feature}" CARGO_INCREMENTAL=0 cargo test --no-default-features --features "${feature}"
done done
popd > /dev/null 2>&1
elif [ "$1" = "--core" ]; then
FEATURES=(
private-cookies
tls
)
pushd "${CORE_ROOT}" > /dev/null 2>&1
echo ":: Building and testing core [no features]..."
CARGO_INCREMENTAL=0 cargo test --no-default-features
echo ":: Building and testing core [default]..."
CARGO_INCREMENTAL=0 cargo test
for feature in "${FEATURES[@]}"; do
echo ":: Building and testing core [${feature}]..."
CARGO_INCREMENTAL=0 cargo test --no-default-features --features "${feature}"
done
popd > /dev/null 2>&1 popd > /dev/null 2>&1
else else
echo ":: Bootstrapping examples..." echo ":: Bootstrapping examples..."

View File

@ -503,6 +503,15 @@ fn logout(mut cookies: Cookies) -> Flash<Redirect> {
[`Cookies::add()`]: @api/rocket/http/enum.Cookies.html#method.add [`Cookies::add()`]: @api/rocket/http/enum.Cookies.html#method.add
Private Cookies can be omitted at build time by excluding the feature
`private-cookies`. You can do this by setting the `default-features`
directive to `false` in your `Cargo.toml`:
```toml
[dependencies.rocket]
default-features = false
```
### Secret Key ### Secret Key
To encrypt private cookies, Rocket uses the 256-bit key specified in the To encrypt private cookies, Rocket uses the 256-bit key specified in the