diff --git a/examples/session/src/main.rs b/examples/session/src/main.rs index 454119a8..fbb8b09c 100644 --- a/examples/session/src/main.rs +++ b/examples/session/src/main.rs @@ -6,7 +6,7 @@ extern crate rocket; use std::collections::HashMap; -use rocket::Outcome; +use rocket::outcome::IntoOutcome; use rocket::request::{self, Form, FlashMessage, FromRequest, Request}; use rocket::response::{Redirect, Flash}; use rocket::http::{Cookie, Cookies}; @@ -25,15 +25,11 @@ impl<'a, 'r> FromRequest<'a, 'r> for User { type Error = (); fn from_request(request: &'a Request<'r>) -> request::Outcome { - let user = request.cookies() + request.cookies() .get_private("user_id") .and_then(|cookie| cookie.value().parse().ok()) - .map(|id| User(id)); - - match user { - Some(user) => Outcome::Success(user), - None => Outcome::Forward(()) - } + .map(|id| User(id)) + .or_forward(()) } } diff --git a/examples/todo/src/db.rs b/examples/todo/src/db.rs index 542d7455..6f9a82a9 100644 --- a/examples/todo/src/db.rs +++ b/examples/todo/src/db.rs @@ -33,12 +33,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for Conn { type Error = (); fn from_request(request: &'a Request<'r>) -> request::Outcome { - let pool = match request.guard::>() { - Outcome::Success(pool) => pool, - Outcome::Failure(e) => return Outcome::Failure(e), - Outcome::Forward(_) => return Outcome::Forward(()), - }; - + let pool = request.guard::>()?; match pool.get() { Ok(conn) => Outcome::Success(Conn(conn)), Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())) diff --git a/lib/src/data/from_data.rs b/lib/src/data/from_data.rs index bd233dcd..b9b0c125 100644 --- a/lib/src/data/from_data.rs +++ b/lib/src/data/from_data.rs @@ -10,7 +10,8 @@ use data::Data; pub type Outcome = outcome::Outcome; impl<'a, S, E> IntoOutcome for Result { - type Input = Status; + type Failure = Status; + type Forward = Data; #[inline] fn into_outcome(self, status: Status) -> Outcome { @@ -19,6 +20,11 @@ impl<'a, S, E> IntoOutcome for Result { Err(err) => Failure((status, err)) } } + + #[inline] + fn or_forward(self, data: Data) -> Outcome { + Forward(data) + } } /// Trait used to derive an object from incoming request data. diff --git a/lib/src/http/cookies.rs b/lib/src/http/cookies.rs index 253bbd1f..9e6242af 100644 --- a/lib/src/http/cookies.rs +++ b/lib/src/http/cookies.rs @@ -6,8 +6,113 @@ use cookie::{SameSite, Delta}; use http::Header; +/// Collection of one or more HTTP cookies. +/// +/// The `Cookies` type allows for retrieval of cookies from an incoming request +/// as well as modifications to cookies to be reflected by Rocket on outgoing +/// responses. `Cookies` is a smart-pointer; it internally borrows and refers to +/// the collection of cookies active during a request's life-cycle. +/// +/// # Usage +/// +/// A type of `Cookies` can be retrieved via its `FromRequest` implementation as +/// a request guard or via the [`Request::cookies`] method. Individual cookies +/// can be retrieved via the [`get`] and [`get_private`] methods. Cookies can be +/// added or removed via the [`add`], [`add_private`], [`remove`], and +/// [`remove_private`] methods. +/// +/// [`get`]: /rocket/http/enum.Cookies.html#method.get +/// [`get_private`]: /rocket/http/enum.Cookies.html#method.get_private +/// [`add`]: /rocket/http/enum.Cookies.html#method.add +/// [`add_private`]: /rocket/http/enum.Cookies.html#method.add_private +/// [`remove`]: /rocket/http/enum.Cookies.html#method.remove +/// [`remove_private`]: /rocket/http/enum.Cookies.html#method.remove_private +/// [`Request::cookies`]: /rocket/struct.Request.html#method.cookies +/// +/// ## Examples +/// +/// The following short snippet shows `Cookies` being used as a request guard in +/// a handler to retrieve the value of a "message" cookie. +/// +/// ```rust +/// # #![feature(plugin)] +/// # #![plugin(rocket_codegen)] +/// # extern crate rocket; +/// use rocket::http::Cookies; +/// +/// #[get("/message")] +/// fn message(cookies: Cookies) -> Option { +/// cookies.get("message").map(|c| format!("Message: {}", c.value())) +/// } +/// # fn main() { } +/// ``` +/// +/// The following snippet shows `Cookies` being retrieved from a `Request` in a +/// custom request guard implementation for `User`. A [private cookie] +/// containing a user's ID is retrieved. If the cookie exists and the ID parses +/// as an integer, a `User` structure is validated. Otherwise, the guard +/// forwards. +/// +/// [private cookie]: /rocket/http/enum.Cookies.html#private-cookies +/// +/// ```rust +/// # #![feature(plugin)] +/// # #![plugin(rocket_codegen)] +/// # extern crate rocket; +/// # +/// use rocket::http::Status; +/// use rocket::outcome::IntoOutcome; +/// use rocket::request::{self, Request, FromRequest}; +/// +/// // In practice, we'd probably fetch the user from the database. +/// struct User(usize); +/// +/// impl<'a, 'r> FromRequest<'a, 'r> for User { +/// type Error = (); +/// +/// fn from_request(request: &'a Request<'r>) -> request::Outcome { +/// request.cookies() +/// .get_private("user_id") +/// .and_then(|cookie| cookie.value().parse().ok()) +/// .map(|id| User(id)) +/// .or_forward(()) +/// } +/// } +/// # fn main() { } +/// ``` +/// +/// # Private Cookies +/// +/// _Private_ cookies are just like regular cookies except that they are +/// encrypted using authenticated encryption, a form of encryption which +/// simultaneously provides confidentiality, integrity, and authenticity. This +/// means that private cookies cannot be inspected, tampered with, or +/// manufactured by clients. If you prefer, you can think of private cookies as +/// being signed and encrypted. +/// +/// Private cookies can be retrieved, added, and removed from a `Cookies` +/// collection via the [`get_private`], [`add_private`], and [`remove_private`] +/// methods. +/// +/// ## Encryption Key +/// +/// To encrypt private cookies, Rocket uses the 256-bit key specified in the +/// `secret_key` configuration parameter. If one is not specified, Rocket will +/// automatically generate a fresh key. Note, however, that a private cookie can +/// only be decrypted with the same key with which it was encrypted. As such, it +/// is important to set a `secret_key` configuration parameter when using +/// private cookies so that cookies decrypt properly after an application +/// restart. Rocket will emit a warning if an application is run in production +/// mode without a configured `secret_key`. +/// +/// Generating a string suitable for use as a `secret_key` configuration value +/// is usually done through tools like `openssl`. Using `openssl`, for instance, +/// a 256-bit base64 key can be generated with the command `openssl rand -base64 +/// 32`. pub enum Cookies<'a> { + #[doc(hidden)] Jarred(RefMut<'a, CookieJar>, &'a Key), + #[doc(hidden)] Empty(CookieJar) } @@ -27,6 +132,18 @@ impl<'a> Cookies<'a> { Cookie::parse_encoded(cookie_str).map(|c| c.into_owned()).ok() } + /// Returns a reference to the `Cookie` inside this container with the name + /// `name`. If no such cookie exists, returns `None`. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::Cookies; + /// + /// fn handler(cookies: Cookies) { + /// let cookie = cookies.get("name"); + /// } + /// ``` pub fn get(&self, name: &str) -> Option<&Cookie<'static>> { match *self { Cookies::Jarred(ref jar, _) => jar.get(name), @@ -34,6 +151,20 @@ 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 + /// 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), @@ -41,44 +172,132 @@ impl<'a> Cookies<'a> { } } + /// Adds `cookie` to this collection. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::{Cookie, Cookies}; + /// + /// fn handler(mut cookies: Cookies) { + /// cookies.add(Cookie::new("name", "value")); + /// + /// let cookie = Cookie::build("name", "value") + /// .path("/") + /// .secure(true) + /// .finish(); + /// + /// cookies.add(cookie); + /// } + /// ``` pub fn add(&mut self, cookie: Cookie<'static>) { if let Cookies::Jarred(ref mut jar, _) = *self { jar.add(cookie) } } + /// 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 + /// [`get_private`](#method.get_private) and removed using + /// [`remove_private`](#method.remove_private). + /// + /// If a path is not set on `cookie`, the `"/"` path will automatically be + /// set. If a `SameSite` attribute is not set, the attribute will be set to + /// `Strict`. These defaults ensure maximum usability and security. For + /// additional security, you may wish to set the `http_only` flag. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::{Cookie, Cookies}; + /// + /// fn handler(mut cookies: Cookies) { + /// cookies.add_private(Cookie::new("name", "value")); + /// + /// // Set the `HttpOnly` flag. + /// let mut cookie = Cookie::new("name", "value"); + /// cookie.set_http_only(true); + /// cookies.add(cookie); + /// } + /// ``` pub fn add_private(&mut self, mut cookie: Cookie<'static>) { - cookie.set_http_only(true); - - if cookie.path().is_none() { - cookie.set_path("/"); - } - - if cookie.same_site().is_none() { - cookie.set_same_site(SameSite::Strict); - } - if let Cookies::Jarred(ref mut jar, key) = *self { + if cookie.path().is_none() { + cookie.set_path("/"); + } + + if cookie.same_site().is_none() { + cookie.set_same_site(SameSite::Strict); + } + jar.private(key).add(cookie) } } + /// 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 initual `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 + /// 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. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::{Cookie, Cookies}; + /// + /// fn handler(mut cookies: Cookies) { + /// cookies.remove_private(Cookie::named("name")); + /// } + /// ``` pub fn remove_private(&mut self, mut cookie: Cookie<'static>) { - if cookie.path().is_none() { - cookie.set_path("/"); - } - if let Cookies::Jarred(ref mut jar, key) = *self { + if cookie.path().is_none() { + cookie.set_path("/"); + } + jar.private(key).remove(cookie) } } + /// Returns an iterator over all of the cookies present in this collection. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::Cookies; + /// + /// fn handler(cookies: Cookies) { + /// for c in cookies.iter() { + /// println!("Name: '{}', Value: '{}'", c.name(), c.value()); + /// } + /// } + /// ``` pub fn iter<'s>(&'s self) -> impl Iterator> { match *self { Cookies::Jarred(ref jar, _) => jar.iter(), diff --git a/lib/src/http/method.rs b/lib/src/http/method.rs index a61c9128..4f0b5efa 100644 --- a/lib/src/http/method.rs +++ b/lib/src/http/method.rs @@ -104,6 +104,7 @@ impl FromStr for Method { } impl fmt::Display for Method { + #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.as_str().fmt(f) } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index af73237d..1b4b6e49 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -7,6 +7,7 @@ #![feature(plugin)] #![feature(never_type)] #![feature(more_io_inner_methods)] +#![feature(try_trait)] #![plugin(pear_codegen)] diff --git a/lib/src/outcome.rs b/lib/src/outcome.rs index 204174ab..3b2db994 100644 --- a/lib/src/outcome.rs +++ b/lib/src/outcome.rs @@ -80,6 +80,7 @@ //! `None`. use std::fmt; +use std::ops::Try; use yansi::{Paint, Color}; @@ -103,9 +104,24 @@ pub enum Outcome { /// Conversion trait from some type into an Outcome type. pub trait IntoOutcome { - type Input: Sized; + type Failure: Sized; + type Forward: Sized; - fn into_outcome(self, input: Self::Input) -> Outcome; + fn into_outcome(self, failure: Self::Failure) -> Outcome; + fn or_forward(self, forward: Self::Forward) -> Outcome; +} + +impl IntoOutcome for Option { + type Failure = E; + type Forward = F; + + fn into_outcome(self, val: E) -> Outcome { + Failure(val) + } + + fn or_forward(self, val: F) -> Outcome { + Forward(val) + } } impl Outcome { @@ -427,6 +443,30 @@ impl Outcome { } } +impl Try for Outcome { + type Ok = S; + type Error = Result; + + fn into_result(self) -> Result { + match self { + Success(val) => Ok(val), + Forward(val) => Err(Ok(val)), + Failure(val) => Err(Err(val)), + } + } + + fn from_error(val: Self::Error) -> Self { + match val { + Ok(val) => Forward(val), + Err(val) => Failure(val), + } + } + + fn from_ok(val: Self::Ok) -> Self { + Success(val) + } +} + impl fmt::Debug for Outcome { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Outcome::{}", self.formatting().1) diff --git a/lib/src/request/from_request.rs b/lib/src/request/from_request.rs index 00f344bf..8b78c0e2 100644 --- a/lib/src/request/from_request.rs +++ b/lib/src/request/from_request.rs @@ -13,7 +13,8 @@ use http::uri::URI; pub type Outcome = outcome::Outcome; impl IntoOutcome for Result { - type Input = Status; + type Failure = Status; + type Forward = (); #[inline] fn into_outcome(self, status: Status) -> Outcome { @@ -22,6 +23,11 @@ impl IntoOutcome for Result { Err(err) => Failure((status, err)) } } + + #[inline] + fn or_forward(self, _: ()) -> Outcome { + Forward(()) + } } /// Trait used to derive an object from incoming request metadata. diff --git a/lib/src/request/request.rs b/lib/src/request/request.rs index f13bf926..73620039 100644 --- a/lib/src/request/request.rs +++ b/lib/src/request/request.rs @@ -276,6 +276,7 @@ impl<'r> Request<'r> { /// # }); /// ``` pub fn cookies(&self) -> Cookies { + // FIXME: Can we do better? This is disappointing. match self.state.cookies.try_borrow_mut() { Ok(jar) => Cookies::new(jar, self.state.config.secret_key()), Err(_) => {