From 53758c6dd7453c906f134b12112a1c4702ab0277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Mon, 5 Nov 2018 20:29:03 +0000 Subject: [PATCH] Introduce the 'private-cookies' feature. --- .travis.yml | 1 + core/http/Cargo.toml | 3 +- core/http/src/cookies.rs | 177 ++++++++++-------- core/http/src/lib.rs | 3 +- core/lib/Cargo.toml | 1 + core/lib/src/config/config.rs | 14 +- core/lib/src/config/custom_values.rs | 6 +- core/lib/src/local/request.rs | 3 + core/lib/src/request/request.rs | 3 + core/lib/src/rocket.rs | 7 +- .../local_request_private_cookie-issue-368.rs | 2 + examples/session/Cargo.toml | 2 +- scripts/test.sh | 20 ++ site/guide/4-requests.md | 9 + 14 files changed, 164 insertions(+), 87 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4c18e8f5..c0858672 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ env: - TEST_FLAGS= - TEST_FLAGS=--release - TEST_FLAGS=--contrib + - TEST_FLAGS=--core rust: - nightly script: ./scripts/test.sh $TEST_FLAGS diff --git a/core/http/Cargo.toml b/core/http/Cargo.toml index bbec63f3..66cb1110 100644 --- a/core/http/Cargo.toml +++ b/core/http/Cargo.toml @@ -15,6 +15,7 @@ categories = ["web-programming"] [features] tls = ["rustls", "hyper-sync-rustls"] +private-cookies = ["cookie/secure"] [dependencies] smallvec = "0.6" @@ -24,7 +25,7 @@ time = "0.1" indexmap = "1.0" rustls = { version = "0.14", optional = true } state = "0.4" -cookie = { version = "0.11", features = ["percent-encode", "secure"] } +cookie = { version = "0.11", features = ["percent-encode"] } pear = "0.1" unicode-xid = "0.1" diff --git a/core/http/src/cookies.rs b/core/http/src/cookies.rs index 80512e31..bfdb3065 100644 --- a/core/http/src/cookies.rs +++ b/core/http/src/cookies.rs @@ -2,10 +2,16 @@ use std::fmt; use std::cell::RefMut; 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; +#[cfg(not(feature = "private-cookies"))] +type Key = (); + /// Collection of one or more HTTP cookies. /// /// 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> { - match *self { - Cookies::Jarred(ref mut jar, key) => jar.private(key).get(name), - Cookies::Empty(_) => None - } - } - /// Adds `cookie` to this collection. /// /// # 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> { + 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> { + 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 /// authenticated encryption assuring confidentiality, integrity, and /// 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 /// security, you may wish to set the `secure` flag. /// + /// This method is only available when the `private-cookies` feature is enabled. + /// /// # Example /// /// ```rust @@ -267,6 +337,8 @@ impl<'a> Cookies<'a> { /// * `HttpOnly`: `true` /// * `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>) { if cookie.path().is_none() { 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. /// /// 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 /// on `cookie`, the `"/"` path will automatically be set. /// + /// This method is only available when the `private-cookies` feature is enabled. + /// /// # Example /// /// ```rust @@ -336,37 +384,6 @@ impl<'a> Cookies<'a> { 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> { - 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> { diff --git a/core/http/src/lib.rs b/core/http/src/lib.rs index 3b5819ea..e9bbbed2 100644 --- a/core/http/src/lib.rs +++ b/core/http/src/lib.rs @@ -59,7 +59,8 @@ pub mod uncased; #[doc(hidden)] pub use smallvec::{SmallVec, Array}; // 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 content_type::ContentType; diff --git a/core/lib/Cargo.toml b/core/lib/Cargo.toml index 5779a8e2..7a6efcd2 100644 --- a/core/lib/Cargo.toml +++ b/core/lib/Cargo.toml @@ -16,6 +16,7 @@ categories = ["web-programming::http-server"] [features] tls = ["rocket_http/tls"] +private-cookies = ["rocket_http/private-cookies"] [dependencies] rocket_codegen = { version = "0.4.0-rc.1", path = "../codegen" } diff --git a/core/lib/src/config/config.rs b/core/lib/src/config/config.rs index 66546d63..7e0e985e 100644 --- a/core/lib/src/config/config.rs +++ b/core/lib/src/config/config.rs @@ -10,7 +10,8 @@ use {num_cpus, base64}; use config::Environment::*; use config::{Result, ConfigBuilder, Environment, ConfigError, LoggingLevel}; use config::{Table, Value, Array, Datetime}; -use http::Key; + +#[cfg(feature = "private-cookies")] use http::Key; /// Structure for Rocket application configuration. /// @@ -49,6 +50,7 @@ pub struct Config { /// How much information to log. pub log_level: LoggingLevel, /// The secret key. + #[cfg(feature = "private-cookies")] crate secret_key: SecretKey, /// TLS configuration. crate tls: Option, @@ -231,6 +233,7 @@ impl Config { let default_workers = (num_cpus::get() * 2) as u16; // Use a generated secret key by default. + #[cfg(feature = "private-cookies")] let key = SecretKey::Generated(Key::generate()); Ok(match env { @@ -242,6 +245,7 @@ impl Config { workers: default_workers, keep_alive: Some(5), log_level: LoggingLevel::Normal, + #[cfg(feature = "private-cookies")] secret_key: key, tls: None, limits: Limits::default(), @@ -257,6 +261,7 @@ impl Config { workers: default_workers, keep_alive: Some(5), log_level: LoggingLevel::Normal, + #[cfg(feature = "private-cookies")] secret_key: key, tls: None, limits: Limits::default(), @@ -272,6 +277,7 @@ impl Config { workers: default_workers, keep_alive: Some(5), log_level: LoggingLevel::Critical, + #[cfg(feature = "private-cookies")] secret_key: key, tls: None, limits: Limits::default(), @@ -473,6 +479,7 @@ impl Config { /// # Ok(()) /// # } /// ``` + #[cfg(feature = "private-cookies")] pub fn set_secret_key>(&mut self, key: K) -> Result<()> { let key = key.into(); let error = self.bad_type("secret_key", "string", @@ -490,6 +497,10 @@ impl Config { self.secret_key = SecretKey::Provided(Key::from_master(&bytes)); Ok(()) } + #[cfg(not(feature = "private-cookies"))] + pub fn set_secret_key>(&mut self, key: K) -> Result<()> { + Ok(()) + } /// Sets the logging level for `self` to `log_level`. /// @@ -663,6 +674,7 @@ impl Config { } /// Retrieves the secret key from `self`. + #[cfg(feature = "private-cookies")] #[inline] crate fn secret_key(&self) -> &Key { self.secret_key.inner() diff --git a/core/lib/src/config/custom_values.rs b/core/lib/src/config/custom_values.rs index cb9c7702..1efbe0c7 100644 --- a/core/lib/src/config/custom_values.rs +++ b/core/lib/src/config/custom_values.rs @@ -3,14 +3,17 @@ use std::fmt; #[cfg(feature = "tls")] use http::tls::{Certificate, PrivateKey}; use config::{Result, Config, Value, ConfigError, LoggingLevel}; -use http::Key; +#[cfg(feature = "private-cookies")] use http::Key; + +#[cfg(feature = "private-cookies")] #[derive(Clone)] pub enum SecretKey { Generated(Key), Provided(Key) } +#[cfg(feature = "private-cookies")] impl SecretKey { #[inline] crate fn inner(&self) -> &Key { @@ -28,6 +31,7 @@ impl SecretKey { } } +#[cfg(feature = "private-cookies")] impl fmt::Display for SecretKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { diff --git a/core/lib/src/local/request.rs b/core/lib/src/local/request.rs index e6e3fea8..c13b6389 100644 --- a/core/lib/src/local/request.rs +++ b/core/lib/src/local/request.rs @@ -269,6 +269,8 @@ impl<'c> LocalRequest<'c> { /// /// [private cookie]: ::http::Cookies::add_private() /// + /// This method is only available when the `private-cookies` feature is enabled. + /// /// # Examples /// /// Add `user_id` as a private cookie: @@ -281,6 +283,7 @@ impl<'c> LocalRequest<'c> { /// # #[allow(unused_variables)] /// let req = client.get("/").private_cookie(Cookie::new("user_id", "sb")); /// ``` + #[cfg(feature = "private-cookies")] #[inline] pub fn private_cookie(self, cookie: Cookie<'static>) -> Self { self.request.cookies().add_original_private(cookie); diff --git a/core/lib/src/request/request.rs b/core/lib/src/request/request.rs index 84cbb783..707ec3f8 100644 --- a/core/lib/src/request/request.rs +++ b/core/lib/src/request/request.rs @@ -290,7 +290,10 @@ impl<'r> Request<'r> { pub fn cookies(&self) -> Cookies { // FIXME: Can we do better? This is disappointing. match self.state.cookies.try_borrow_mut() { + #[cfg(feature = "private-cookies")] Ok(jar) => Cookies::new(jar, self.state.config.secret_key()), + #[cfg(not(feature = "private-cookies"))] + Ok(jar) => Cookies::new(jar, &()), Err(_) => { error_!("Multiple `Cookies` instances are active at once."); info_!("An instance of `Cookies` must be dropped before another \ diff --git a/core/lib/src/rocket.rs b/core/lib/src/rocket.rs index 70b7be05..11e7792d 100644 --- a/core/lib/src/rocket.rs +++ b/core/lib/src/rocket.rs @@ -396,6 +396,7 @@ impl Rocket { launch_info_!("port: {}", Paint::white(&config.port)); launch_info_!("log: {}", Paint::white(config.log_level)); launch_info_!("workers: {}", Paint::white(config.workers)); + #[cfg(feature = "private-cookies")] launch_info_!("secret key: {}", Paint::white(&config.secret_key)); launch_info_!("limits: {}", Paint::white(&config.limits)); @@ -414,8 +415,10 @@ impl Rocket { launch_info_!("tls: {}", Paint::white("disabled")); } - if config.secret_key.is_generated() && config.environment.is_prod() { - warn!("environment is 'production', but no `secret_key` is configured"); + #[cfg(feature = "private-cookies")] { + 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() { diff --git a/core/lib/tests/local_request_private_cookie-issue-368.rs b/core/lib/tests/local_request_private_cookie-issue-368.rs index 2b762cd0..defafe01 100644 --- a/core/lib/tests/local_request_private_cookie-issue-368.rs +++ b/core/lib/tests/local_request_private_cookie-issue-368.rs @@ -4,6 +4,7 @@ use rocket::http::Cookies; +#[cfg(feature = "private-cookies")] #[get("/")] fn return_private_cookie(mut cookies: Cookies) -> Option { match cookies.get_private("cookie_name") { @@ -12,6 +13,7 @@ fn return_private_cookie(mut cookies: Cookies) -> Option { } } +#[cfg(feature = "private-cookies")] mod tests { use super::*; use rocket::local::Client; diff --git a/examples/session/Cargo.toml b/examples/session/Cargo.toml index 8da4bd9f..4a2de9af 100644 --- a/examples/session/Cargo.toml +++ b/examples/session/Cargo.toml @@ -5,7 +5,7 @@ workspace = "../../" publish = false [dependencies] -rocket = { path = "../../core/lib" } +rocket = { path = "../../core/lib", features = ["private-cookies"] } [dependencies.rocket_contrib] path = "../../contrib/lib" diff --git a/scripts/test.sh b/scripts/test.sh index f44686dc..10888b57 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -107,6 +107,26 @@ if [ "$1" = "--contrib" ]; then CARGO_INCREMENTAL=0 cargo test --no-default-features --features "${feature}" 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 else echo ":: Bootstrapping examples..." diff --git a/site/guide/4-requests.md b/site/guide/4-requests.md index 6f8b6183..aa5e5450 100644 --- a/site/guide/4-requests.md +++ b/site/guide/4-requests.md @@ -503,6 +503,15 @@ fn logout(mut cookies: Cookies) -> Flash { [`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 To encrypt private cookies, Rocket uses the 256-bit key specified in the