Impl 'try' for 'Outcome'. Document 'Cookies'.

The 'try' impl for 'Outcome' allows the '?' operator to be used with
'Outcome' values. This is likely to make 'FromRequest' and 'FromData'
implementations more ergonomic.

This commit also expands the 'IntoOutcome' trait. It is now
implemented for 'Option'. It also now includes an additional
'or_forward' method.
This commit is contained in:
Sergio Benitez 2017-06-24 02:49:16 -07:00
parent 0376fb5fe5
commit 150aef7764
9 changed files with 297 additions and 32 deletions

View File

@ -6,7 +6,7 @@ extern crate rocket;
use std::collections::HashMap; use std::collections::HashMap;
use rocket::Outcome; use rocket::outcome::IntoOutcome;
use rocket::request::{self, Form, FlashMessage, FromRequest, Request}; use rocket::request::{self, Form, FlashMessage, FromRequest, Request};
use rocket::response::{Redirect, Flash}; use rocket::response::{Redirect, Flash};
use rocket::http::{Cookie, Cookies}; use rocket::http::{Cookie, Cookies};
@ -25,15 +25,11 @@ impl<'a, 'r> FromRequest<'a, 'r> for User {
type Error = (); type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<User, ()> { fn from_request(request: &'a Request<'r>) -> request::Outcome<User, ()> {
let user = request.cookies() request.cookies()
.get_private("user_id") .get_private("user_id")
.and_then(|cookie| cookie.value().parse().ok()) .and_then(|cookie| cookie.value().parse().ok())
.map(|id| User(id)); .map(|id| User(id))
.or_forward(())
match user {
Some(user) => Outcome::Success(user),
None => Outcome::Forward(())
}
} }
} }

View File

@ -33,12 +33,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for Conn {
type Error = (); type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<Conn, ()> { fn from_request(request: &'a Request<'r>) -> request::Outcome<Conn, ()> {
let pool = match request.guard::<State<Pool>>() { let pool = request.guard::<State<Pool>>()?;
Outcome::Success(pool) => pool,
Outcome::Failure(e) => return Outcome::Failure(e),
Outcome::Forward(_) => return Outcome::Forward(()),
};
match pool.get() { match pool.get() {
Ok(conn) => Outcome::Success(Conn(conn)), Ok(conn) => Outcome::Success(Conn(conn)),
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())) Err(_) => Outcome::Failure((Status::ServiceUnavailable, ()))

View File

@ -10,7 +10,8 @@ use data::Data;
pub type Outcome<S, E> = outcome::Outcome<S, (Status, E), Data>; pub type Outcome<S, E> = outcome::Outcome<S, (Status, E), Data>;
impl<'a, S, E> IntoOutcome<S, (Status, E), Data> for Result<S, E> { impl<'a, S, E> IntoOutcome<S, (Status, E), Data> for Result<S, E> {
type Input = Status; type Failure = Status;
type Forward = Data;
#[inline] #[inline]
fn into_outcome(self, status: Status) -> Outcome<S, E> { fn into_outcome(self, status: Status) -> Outcome<S, E> {
@ -19,6 +20,11 @@ impl<'a, S, E> IntoOutcome<S, (Status, E), Data> for Result<S, E> {
Err(err) => Failure((status, err)) Err(err) => Failure((status, err))
} }
} }
#[inline]
fn or_forward(self, data: Data) -> Outcome<S, E> {
Forward(data)
}
} }
/// Trait used to derive an object from incoming request data. /// Trait used to derive an object from incoming request data.

View File

@ -6,8 +6,113 @@ use cookie::{SameSite, Delta};
use http::Header; 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<String> {
/// 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<User, ()> {
/// 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> { pub enum Cookies<'a> {
#[doc(hidden)]
Jarred(RefMut<'a, CookieJar>, &'a Key), Jarred(RefMut<'a, CookieJar>, &'a Key),
#[doc(hidden)]
Empty(CookieJar) Empty(CookieJar)
} }
@ -27,6 +132,18 @@ impl<'a> Cookies<'a> {
Cookie::parse_encoded(cookie_str).map(|c| c.into_owned()).ok() 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>> { 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),
@ -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<Cookie<'static>> { pub fn get_private(&mut self, name: &str) -> Option<Cookie<'static>> {
match *self { match *self {
Cookies::Jarred(ref mut jar, key) => jar.private(key).get(name), 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>) { pub fn add(&mut self, cookie: Cookie<'static>) {
if let Cookies::Jarred(ref mut jar, _) = *self { if let Cookies::Jarred(ref mut jar, _) = *self {
jar.add(cookie) 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>) { 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 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) 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>) { pub fn remove(&mut self, cookie: Cookie<'static>) {
if let Cookies::Jarred(ref mut jar, _) = *self { if let Cookies::Jarred(ref mut jar, _) = *self {
jar.remove(cookie) 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>) { 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 let Cookies::Jarred(ref mut jar, key) = *self {
if cookie.path().is_none() {
cookie.set_path("/");
}
jar.private(key).remove(cookie) 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<Item=&'s Cookie<'static>> { pub fn iter<'s>(&'s self) -> impl Iterator<Item=&'s Cookie<'static>> {
match *self { match *self {
Cookies::Jarred(ref jar, _) => jar.iter(), Cookies::Jarred(ref jar, _) => jar.iter(),

View File

@ -104,6 +104,7 @@ impl FromStr for Method {
} }
impl fmt::Display for Method { impl fmt::Display for Method {
#[inline(always)]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.as_str().fmt(f) self.as_str().fmt(f)
} }

View File

@ -7,6 +7,7 @@
#![feature(plugin)] #![feature(plugin)]
#![feature(never_type)] #![feature(never_type)]
#![feature(more_io_inner_methods)] #![feature(more_io_inner_methods)]
#![feature(try_trait)]
#![plugin(pear_codegen)] #![plugin(pear_codegen)]

View File

@ -80,6 +80,7 @@
//! `None`. //! `None`.
use std::fmt; use std::fmt;
use std::ops::Try;
use yansi::{Paint, Color}; use yansi::{Paint, Color};
@ -103,9 +104,24 @@ pub enum Outcome<S, E, F> {
/// Conversion trait from some type into an Outcome type. /// Conversion trait from some type into an Outcome type.
pub trait IntoOutcome<S, E, F> { pub trait IntoOutcome<S, E, F> {
type Input: Sized; type Failure: Sized;
type Forward: Sized;
fn into_outcome(self, input: Self::Input) -> Outcome<S, E, F>; fn into_outcome(self, failure: Self::Failure) -> Outcome<S, E, F>;
fn or_forward(self, forward: Self::Forward) -> Outcome<S, E, F>;
}
impl<S, E, F> IntoOutcome<S, E, F> for Option<S> {
type Failure = E;
type Forward = F;
fn into_outcome(self, val: E) -> Outcome<S, E, F> {
Failure(val)
}
fn or_forward(self, val: F) -> Outcome<S, E, F> {
Forward(val)
}
} }
impl<S, E, F> Outcome<S, E, F> { impl<S, E, F> Outcome<S, E, F> {
@ -427,6 +443,30 @@ impl<S, E, F> Outcome<S, E, F> {
} }
} }
impl<S, E, F> Try for Outcome<S, E, F> {
type Ok = S;
type Error = Result<F, E>;
fn into_result(self) -> Result<Self::Ok, Self::Error> {
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<S, E, F> fmt::Debug for Outcome<S, E, F> { impl<S, E, F> fmt::Debug for Outcome<S, E, F> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Outcome::{}", self.formatting().1) write!(f, "Outcome::{}", self.formatting().1)

View File

@ -13,7 +13,8 @@ use http::uri::URI;
pub type Outcome<S, E> = outcome::Outcome<S, (Status, E), ()>; pub type Outcome<S, E> = outcome::Outcome<S, (Status, E), ()>;
impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> { impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
type Input = Status; type Failure = Status;
type Forward = ();
#[inline] #[inline]
fn into_outcome(self, status: Status) -> Outcome<S, E> { fn into_outcome(self, status: Status) -> Outcome<S, E> {
@ -22,6 +23,11 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
Err(err) => Failure((status, err)) Err(err) => Failure((status, err))
} }
} }
#[inline]
fn or_forward(self, _: ()) -> Outcome<S, E> {
Forward(())
}
} }
/// Trait used to derive an object from incoming request metadata. /// Trait used to derive an object from incoming request metadata.

View File

@ -276,6 +276,7 @@ impl<'r> Request<'r> {
/// # }); /// # });
/// ``` /// ```
pub fn cookies(&self) -> Cookies { pub fn cookies(&self) -> Cookies {
// FIXME: Can we do better? This is disappointing.
match self.state.cookies.try_borrow_mut() { match self.state.cookies.try_borrow_mut() {
Ok(jar) => Cookies::new(jar, self.state.config.secret_key()), Ok(jar) => Cookies::new(jar, self.state.config.secret_key()),
Err(_) => { Err(_) => {