Use thread-safe 'CookieJar's.

The user-facing changes effected by this commit are:

  * The 'http::Cookies<'_>' guard is now '&http::CookieJar<'_>'.
  * The "one-at-a-time" jar restriction is no longer imposed.
  * 'CookieJar' retrieval methods return 'http::CookieCrumb'.
  * The 'private-cookies' feature is now called 'secrets'.
  * Docs flag private cookie methods with feature cfg.
  * Local, async request dispatching is never serialized.
  * 'Client::cookies()' returns the tracked 'CookieJar'.
  * 'LocalResponse::cookies()' returns a 'CookieJar'.
  * 'Response::cookies()' returns an 'impl Iterator'.
  * A path of '/' is set by default on all cookies.
  * 'SameSite=strict' is set by default on all cookies.
  * 'LocalRequest::cookies()' accepts any 'Cookie' iterator.
  * The 'Debug' impl for 'Request' prints the cookie jar.

Resolves #1332.
This commit is contained in:
Sergio Benitez 2020-07-22 12:21:19 -07:00
parent 549c9241c4
commit 52320020bc
36 changed files with 564 additions and 558 deletions

View File

@ -1,10 +0,0 @@
// #[post("/<_name>?<_query>", format = "application/json", data = "<user>", rank = 2)]
// fn get(
// _name: &RawStr,
// _query: User,
// user: Form<User>,
// _cookies: Cookies
// ) -> String {
// format!("{}:{}", user.name, user.nickname)
// }

View File

@ -4,7 +4,7 @@
use std::path::PathBuf; use std::path::PathBuf;
use rocket::http::{RawStr, Cookies}; use rocket::http::{RawStr, CookieJar};
use rocket::http::uri::{FromUriParam, Query}; use rocket::http::uri::{FromUriParam, Query};
use rocket::request::Form; use rocket::request::Form;
@ -51,13 +51,13 @@ fn simple4_flipped(name: String, id: i32) { }
fn unused_param(used: i32, _unused: i32) { } fn unused_param(used: i32, _unused: i32) { }
#[post("/<id>")] #[post("/<id>")]
fn guard_1(cookies: Cookies<'_>, id: i32) { } fn guard_1(cookies: &CookieJar<'_>, id: i32) { }
#[post("/<id>/<name>")] #[post("/<id>/<name>")]
fn guard_2(name: String, cookies: Cookies<'_>, id: i32) { } fn guard_2(name: String, cookies: &CookieJar<'_>, id: i32) { }
#[post("/a/<id>/hi/<name>/hey")] #[post("/a/<id>/hi/<name>/hey")]
fn guard_3(id: i32, name: String, cookies: Cookies<'_>) { } fn guard_3(id: i32, name: String, cookies: &CookieJar<'_>) { }
#[post("/<id>", data = "<form>")] #[post("/<id>", data = "<form>")]
fn no_uri_display_okay(id: i32, form: Form<Second>) { } fn no_uri_display_okay(id: i32, form: Form<Second>) { }
@ -69,7 +69,7 @@ fn complex<'r>(
query: Form<User<'r>>, query: Form<User<'r>>,
user: Form<User<'r>>, user: Form<User<'r>>,
bar: &RawStr, bar: &RawStr,
cookies: Cookies<'_> cookies: &CookieJar<'_>
) { } ) { }
#[post("/a/<path..>")] #[post("/a/<path..>")]
@ -79,7 +79,7 @@ fn segments(path: PathBuf) { }
fn param_and_segments(path: PathBuf, id: usize) { } fn param_and_segments(path: PathBuf, id: usize) { }
#[post("/a/<id>/then/<path..>")] #[post("/a/<id>/then/<path..>")]
fn guarded_segments(cookies: Cookies<'_>, path: PathBuf, id: usize) { } fn guarded_segments(cookies: &CookieJar<'_>, path: PathBuf, id: usize) { }
macro_rules! assert_uri_eq { macro_rules! assert_uri_eq {
($($uri:expr => $expected:expr,)+) => { ($($uri:expr => $expected:expr,)+) => {

View File

@ -14,7 +14,9 @@ error[E0277]: the trait bound `rocket::http::Header<'_>: std::convert::From<u8>`
| |
= help: the following implementations were found: = help: the following implementations were found:
<rocket::http::Header<'static> as std::convert::From<&rocket::http::Cookie<'_>>> <rocket::http::Header<'static> as std::convert::From<&rocket::http::Cookie<'_>>>
<rocket::http::Header<'static> as std::convert::From<&rocket::http::CookieCrumb>>
<rocket::http::Header<'static> as std::convert::From<rocket::http::Cookie<'_>>> <rocket::http::Header<'static> as std::convert::From<rocket::http::Cookie<'_>>>
<rocket::http::Header<'static> as std::convert::From<rocket::http::CookieCrumb>>
= note: required because of the requirements on the impl of `std::convert::Into<rocket::http::Header<'_>>` for `u8` = note: required because of the requirements on the impl of `std::convert::Into<rocket::http::Header<'_>>` for `u8`
error[E0277]: the trait bound `u8: rocket::response::Responder<'_, '_>` is not satisfied error[E0277]: the trait bound `u8: rocket::response::Responder<'_, '_>` is not satisfied
@ -33,7 +35,9 @@ error[E0277]: the trait bound `rocket::http::Header<'_>: std::convert::From<u8>`
| |
= help: the following implementations were found: = help: the following implementations were found:
<rocket::http::Header<'static> as std::convert::From<&rocket::http::Cookie<'_>>> <rocket::http::Header<'static> as std::convert::From<&rocket::http::Cookie<'_>>>
<rocket::http::Header<'static> as std::convert::From<&rocket::http::CookieCrumb>>
<rocket::http::Header<'static> as std::convert::From<rocket::http::Cookie<'_>>> <rocket::http::Header<'static> as std::convert::From<rocket::http::Cookie<'_>>>
<rocket::http::Header<'static> as std::convert::From<rocket::http::CookieCrumb>>
= note: required because of the requirements on the impl of `std::convert::Into<rocket::http::Header<'_>>` for `u8` = note: required because of the requirements on the impl of `std::convert::Into<rocket::http::Header<'_>>` for `u8`
error[E0277]: the trait bound `rocket::http::Header<'_>: std::convert::From<std::string::String>` is not satisfied error[E0277]: the trait bound `rocket::http::Header<'_>: std::convert::From<std::string::String>` is not satisfied
@ -44,7 +48,9 @@ error[E0277]: the trait bound `rocket::http::Header<'_>: std::convert::From<std:
| |
= help: the following implementations were found: = help: the following implementations were found:
<rocket::http::Header<'static> as std::convert::From<&rocket::http::Cookie<'_>>> <rocket::http::Header<'static> as std::convert::From<&rocket::http::Cookie<'_>>>
<rocket::http::Header<'static> as std::convert::From<&rocket::http::CookieCrumb>>
<rocket::http::Header<'static> as std::convert::From<rocket::http::Cookie<'_>>> <rocket::http::Header<'static> as std::convert::From<rocket::http::Cookie<'_>>>
<rocket::http::Header<'static> as std::convert::From<rocket::http::CookieCrumb>>
= note: required because of the requirements on the impl of `std::convert::Into<rocket::http::Header<'_>>` for `std::string::String` = note: required because of the requirements on the impl of `std::convert::Into<rocket::http::Header<'_>>` for `std::string::String`
error[E0277]: the trait bound `usize: rocket::response::Responder<'_, '_>` is not satisfied error[E0277]: the trait bound `usize: rocket::response::Responder<'_, '_>` is not satisfied

View File

@ -14,7 +14,9 @@ error[E0277]: the trait bound `rocket::http::Header<'_>: std::convert::From<u8>`
| |
= help: the following implementations were found: = help: the following implementations were found:
<rocket::http::Header<'static> as std::convert::From<&rocket::http::Cookie<'_>>> <rocket::http::Header<'static> as std::convert::From<&rocket::http::Cookie<'_>>>
<rocket::http::Header<'static> as std::convert::From<&rocket::http::CookieCrumb>>
<rocket::http::Header<'static> as std::convert::From<rocket::http::Cookie<'_>>> <rocket::http::Header<'static> as std::convert::From<rocket::http::Cookie<'_>>>
<rocket::http::Header<'static> as std::convert::From<rocket::http::CookieCrumb>>
= note: required because of the requirements on the impl of `std::convert::Into<rocket::http::Header<'_>>` for `u8` = note: required because of the requirements on the impl of `std::convert::Into<rocket::http::Header<'_>>` for `u8`
error[E0277]: the trait bound `u8: rocket::response::Responder<'_, '_>` is not satisfied error[E0277]: the trait bound `u8: rocket::response::Responder<'_, '_>` is not satisfied
@ -33,7 +35,9 @@ error[E0277]: the trait bound `rocket::http::Header<'_>: std::convert::From<u8>`
| |
= help: the following implementations were found: = help: the following implementations were found:
<rocket::http::Header<'static> as std::convert::From<&rocket::http::Cookie<'_>>> <rocket::http::Header<'static> as std::convert::From<&rocket::http::Cookie<'_>>>
<rocket::http::Header<'static> as std::convert::From<&rocket::http::CookieCrumb>>
<rocket::http::Header<'static> as std::convert::From<rocket::http::Cookie<'_>>> <rocket::http::Header<'static> as std::convert::From<rocket::http::Cookie<'_>>>
<rocket::http::Header<'static> as std::convert::From<rocket::http::CookieCrumb>>
= note: required because of the requirements on the impl of `std::convert::Into<rocket::http::Header<'_>>` for `u8` = note: required because of the requirements on the impl of `std::convert::Into<rocket::http::Header<'_>>` for `u8`
error[E0277]: the trait bound `rocket::http::Header<'_>: std::convert::From<std::string::String>` is not satisfied error[E0277]: the trait bound `rocket::http::Header<'_>: std::convert::From<std::string::String>` is not satisfied
@ -44,7 +48,9 @@ error[E0277]: the trait bound `rocket::http::Header<'_>: std::convert::From<std:
| |
= help: the following implementations were found: = help: the following implementations were found:
<rocket::http::Header<'static> as std::convert::From<&rocket::http::Cookie<'_>>> <rocket::http::Header<'static> as std::convert::From<&rocket::http::Cookie<'_>>>
<rocket::http::Header<'static> as std::convert::From<&rocket::http::CookieCrumb>>
<rocket::http::Header<'static> as std::convert::From<rocket::http::Cookie<'_>>> <rocket::http::Header<'static> as std::convert::From<rocket::http::Cookie<'_>>>
<rocket::http::Header<'static> as std::convert::From<rocket::http::CookieCrumb>>
= note: required because of the requirements on the impl of `std::convert::Into<rocket::http::Header<'_>>` for `std::string::String` = note: required because of the requirements on the impl of `std::convert::Into<rocket::http::Header<'_>>` for `std::string::String`
error[E0277]: the trait bound `usize: rocket::response::Responder<'_, '_>` is not satisfied error[E0277]: the trait bound `usize: rocket::response::Responder<'_, '_>` is not satisfied

View File

@ -1,15 +1,15 @@
#[macro_use] extern crate rocket; #[macro_use] extern crate rocket;
use rocket::http::{Cookies, RawStr}; use rocket::http::{CookieJar, RawStr};
#[post("/<id>")] #[post("/<id>")]
fn has_one(id: i32) { } fn has_one(id: i32) { }
#[post("/<id>")] #[post("/<id>")]
fn has_one_guarded(cookies: Cookies, id: i32) { } fn has_one_guarded(cookies: &CookieJar<'_>, id: i32) { }
#[post("/<id>?<name>")] #[post("/<id>?<name>")]
fn has_two(cookies: Cookies, id: i32, name: String) { } fn has_two(cookies: &CookieJar<'_>, id: i32, name: String) { }
#[post("/<id>/<name>")] #[post("/<id>/<name>")]
fn optionals(id: Option<i32>, name: Result<String, &RawStr>) { } fn optionals(id: Option<i32>, name: Result<String, &RawStr>) { }

View File

@ -30,14 +30,21 @@ indexmap = "1.0"
state = "0.4" state = "0.4"
tokio-rustls = { version = "0.14.0", optional = true } tokio-rustls = { version = "0.14.0", optional = true }
tokio = { version = "0.2.9", features = ["sync", "tcp", "time"] } tokio = { version = "0.2.9", features = ["sync", "tcp", "time"] }
cookie = { version = "0.14.0", features = ["percent-encode"] }
unicode-xid = "0.2" unicode-xid = "0.2"
log = "0.4" log = "0.4"
ref-cast = "1.0" ref-cast = "1.0"
[dependencies.cookie]
git = "https://github.com/SergioBenitez/cookie-rs.git"
rev = "3795f2e"
features = ["percent-encode"]
[dependencies.pear] [dependencies.pear]
git = "https://github.com/SergioBenitez/Pear.git" git = "https://github.com/SergioBenitez/Pear.git"
rev = "4b68055" rev = "4b68055"
[dev-dependencies] [dev-dependencies]
rocket = { version = "0.5.0-dev", path = "../lib" } rocket = { version = "0.5.0-dev", path = "../lib" }
[build-dependencies]
version_check = "0.9"

5
core/http/build.rs Normal file
View File

@ -0,0 +1,5 @@
fn main() {
if let Some(true) = version_check::is_feature_flaggable() {
println!("cargo:rustc-cfg=nightly");
}
}

View File

@ -1,10 +1,9 @@
use std::fmt; use std::fmt;
use crate::Header; use crate::Header;
use cookie::Delta;
pub use cookie::{Cookie, CookieCrumb, SameSite, Iter};
#[doc(hidden)] pub use self::key::*; #[doc(hidden)] pub use self::key::*;
pub use cookie::{Cookie, CookieJar, SameSite};
/// Types and methods to manage a `Key` when private cookies are enabled. /// Types and methods to manage a `Key` when private cookies are enabled.
#[cfg(feature = "private-cookies")] #[cfg(feature = "private-cookies")]
@ -27,18 +26,17 @@ mod 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 `CookieJar` type allows for retrieval of cookies from an incoming
/// as well as modifications to cookies to be reflected by Rocket on outgoing /// request as well as modifications to cookies to be reflected by Rocket on
/// responses. `Cookies` is a smart-pointer; it internally borrows and refers to /// outgoing responses.
/// the collection of cookies active during a request's life-cycle.
/// ///
/// # Usage /// # Usage
/// ///
/// A type of `Cookies` can be retrieved via its `FromRequest` implementation as /// A type of `&CookieJar` can be retrieved via its `FromRequest` implementation
/// a request guard or via the [`Request::cookies()`] method. Individual cookies /// as a request guard or via the [`Request::cookies()`] method. Individual
/// can be retrieved via the [`get()`] and [`get_private()`] methods. Cookies /// cookies can be retrieved via the [`get()`] and [`get_private()`] methods.
/// can be added or removed via the [`add()`], [`add_private()`], [`remove()`], /// Cookies can be added or removed via the [`add()`], [`add_private()`],
/// and [`remove_private()`] methods. /// [`remove()`], and [`remove_private()`] methods.
/// ///
/// [`Request::cookies()`]: rocket::Request::cookies() /// [`Request::cookies()`]: rocket::Request::cookies()
/// [`get()`]: #method.get /// [`get()`]: #method.get
@ -50,27 +48,27 @@ mod key {
/// ///
/// ## Examples /// ## Examples
/// ///
/// The following short snippet shows `Cookies` being used as a request guard in /// The following example shows `&CookieJar` being used as a request guard in a
/// a handler to retrieve the value of a "message" cookie. /// handler to retrieve the value of a "message" cookie.
/// ///
/// ```rust /// ```rust
/// # #[macro_use] extern crate rocket; /// # #[macro_use] extern crate rocket;
/// use rocket::http::Cookies; /// use rocket::http::CookieJar;
/// ///
/// #[get("/message")] /// #[get("/message")]
/// fn message(cookies: Cookies) -> Option<String> { /// fn message(jar: &CookieJar<'_>) -> Option<String> {
/// cookies.get("message").map(|c| format!("Message: {}", c.value())) /// jar.get("message").map(|c| format!("Message: {}", c.value()))
/// } /// }
/// # fn main() { } /// # fn main() { }
/// ``` /// ```
/// ///
/// The following snippet shows `Cookies` being retrieved from a `Request` in a /// The following snippet shows `&CookieJar` being retrieved from a `Request` in
/// custom request guard implementation for `User`. A [private cookie] /// 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 /// 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 /// as an integer, a `User` structure is validated. Otherwise, the guard
/// forwards. /// forwards.
/// ///
/// [private cookie]: Cookies::add_private() /// [private cookie]: #method.add_private
/// ///
/// ```rust /// ```rust
/// # #[macro_use] extern crate rocket; /// # #[macro_use] extern crate rocket;
@ -89,7 +87,7 @@ mod key {
/// async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { /// async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
/// request.cookies() /// request.cookies()
/// .get_private("user_id") /// .get_private("user_id")
/// .and_then(|cookie| cookie.value().parse().ok()) /// .and_then(|c| c.value().parse().ok())
/// .map(|id| User(id)) /// .map(|id| User(id))
/// .or_forward(()) /// .or_forward(())
/// } /// }
@ -106,7 +104,7 @@ mod key {
/// manufactured by clients. If you prefer, you can think of private cookies as /// manufactured by clients. If you prefer, you can think of private cookies as
/// being signed and encrypted. /// being signed and encrypted.
/// ///
/// Private cookies can be retrieved, added, and removed from a `Cookies` /// Private cookies can be retrieved, added, and removed from a `CookieJar`
/// collection via the [`get_private()`], [`add_private()`], and /// collection via the [`get_private()`], [`add_private()`], and
/// [`remove_private()`] methods. /// [`remove_private()`] methods.
/// ///
@ -125,194 +123,82 @@ mod key {
/// is usually done through tools like `openssl`. Using `openssl`, for instance, /// 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 /// a 256-bit base64 key can be generated with the command `openssl rand -base64
/// 32`. /// 32`.
pub enum Cookies<'a> { #[derive(Clone)]
#[doc(hidden)] pub struct CookieJar<'a> {
Jarred(CookieJar, &'a Key, Box<dyn FnOnce(CookieJar) + Send + 'a>), jar: cookie::CookieJar,
#[doc(hidden)] key: &'a Key,
Empty(CookieJar)
} }
impl<'a> Cookies<'a> { impl<'a> CookieJar<'a> {
/// WARNING: This is unstable! Do not use this method outside of Rocket!
#[inline]
#[doc(hidden)]
pub fn new<F: FnOnce(CookieJar) + Send + 'a>(jar: CookieJar, key: &'a Key, on_drop: F) -> Cookies<'a> {
Cookies::Jarred(jar, key, Box::new(on_drop))
}
/// WARNING: This is unstable! Do not use this method outside of Rocket!
#[doc(hidden)]
#[inline(always)]
pub fn empty() -> Cookies<'static> {
Cookies::Empty(CookieJar::new())
}
/// WARNING: This is unstable! Do not use this method outside of Rocket!
#[doc(hidden)]
#[inline(always)]
pub fn parse_cookie(cookie_str: &str) -> Option<Cookie<'static>> {
Cookie::parse_encoded(cookie_str).map(|c| c.into_owned()).ok()
}
/// Adds an original `cookie` to this collection.
/// WARNING: This is unstable! Do not use this method outside of Rocket!
#[inline]
#[doc(hidden)]
pub fn add_original(&mut self, cookie: Cookie<'static>) {
if let Cookies::Jarred(ref mut jar, _, _) = *self {
jar.add_original(cookie)
}
}
/// Returns a reference to the `Cookie` inside this container with the name /// Returns a reference to the `Cookie` inside this container with the name
/// `name`. If no such cookie exists, returns `None`. /// `name`. If no such cookie exists, returns `None`.
/// ///
/// # Example /// # Example
/// ///
/// ```rust /// ```rust
/// # extern crate rocket; /// # #[macro_use] extern crate rocket;
/// use rocket::http::Cookies; /// use rocket::http::{Cookie, CookieJar};
/// ///
/// fn handler(cookies: Cookies) { /// #[get("/")]
/// let cookie = cookies.get("name"); /// fn handler(jar: &CookieJar<'_>) {
/// let cookie = jar.get("name");
/// } /// }
/// ``` /// ```
pub fn get(&self, name: &str) -> Option<&Cookie<'static>> { pub fn get(&self, name: &str) -> Option<CookieCrumb> {
match *self { self.jar.get(name)
Cookies::Jarred(ref jar, _, _) => jar.get(name),
Cookies::Empty(_) => None
}
} }
/// Adds `cookie` to this collection.
///
/// # Example
///
/// ```rust
/// # extern crate rocket;
/// 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)
}
}
/// 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 all delta cookies.
/// WARNING: This is unstable! Do not use this method outside of Rocket!
#[inline]
#[doc(hidden)]
pub fn reset_delta(&mut self) {
match *self {
Cookies::Jarred(ref mut jar, ..) => jar.reset_delta(),
Cookies::Empty(ref mut jar) => jar.reset_delta()
}
}
/// 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!
#[inline]
#[doc(hidden)]
pub fn delta(&self) -> Delta<'_> {
match *self {
Cookies::Jarred(ref jar, _, _) => jar.delta(),
Cookies::Empty(ref jar) => jar.delta()
}
}
}
impl<'a> Drop for Cookies<'a> {
fn drop(&mut self) {
if let Cookies::Jarred(ref mut jar, _, ref mut on_drop) = *self {
let jar = std::mem::replace(jar, CookieJar::new());
let on_drop = std::mem::replace(on_drop, Box::new(|_| {}));
on_drop(jar);
}
}
}
#[cfg(feature = "private-cookies")]
impl Cookies<'_> {
/// Returns a reference to the `Cookie` inside this collection with the name /// Returns a reference to the `Cookie` inside this collection with the name
/// `name` and authenticates and decrypts the cookie's value, returning a /// `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` with the decrypted value. If the cookie cannot be found, or the
/// cookie fails to authenticate or decrypt, `None` is returned. /// cookie fails to authenticate or decrypt, `None` is returned.
/// ///
/// This method is only available when the `private-cookies` feature is /// # Example
/// enabled. ///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::http::{Cookie, CookieJar};
///
/// #[get("/")]
/// fn handler(jar: &CookieJar<'_>) {
/// let cookie = jar.get_private("name");
/// }
/// ```
#[cfg(feature = "private-cookies")]
#[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
pub fn get_private(&self, name: &str) -> Option<Cookie<'static>> {
self.jar.private(&*self.key).get(name)
}
/// Adds `cookie` to this collection.
///
/// Unless a value is set for the given property, the following defaults are
/// set on `cookie` before being added to `self`:
///
/// * `path`: `"/"`
/// * `SameSite`: `Strict`
/// ///
/// # Example /// # Example
/// ///
/// ```rust /// ```rust
/// # extern crate rocket; /// # #[macro_use] extern crate rocket;
/// use rocket::http::Cookies; /// use rocket::http::{Cookie, SameSite, CookieJar};
/// ///
/// fn handler(mut cookies: Cookies) { /// #[get("/")]
/// let cookie = cookies.get_private("name"); /// fn handler(jar: &CookieJar<'_>) {
/// jar.add(Cookie::new("first", "value"));
///
/// let cookie = Cookie::build("other", "value_two")
/// .path("/")
/// .secure(true)
/// .same_site(SameSite::Lax);
///
/// jar.add(cookie.finish());
/// } /// }
/// ``` /// ```
pub fn get_private(&mut self, name: &str) -> Option<Cookie<'static>> { pub fn add(&self, mut cookie: Cookie<'static>) {
match *self { Self::set_defaults(&mut cookie);
Cookies::Jarred(ref mut jar, key, _) => jar.private(key).get(name), self.jar.add(cookie)
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
@ -321,7 +207,7 @@ impl Cookies<'_> {
/// [`get_private`](#method.get_private) and removed using /// [`get_private`](#method.get_private) and removed using
/// [`remove_private`](#method.remove_private). /// [`remove_private`](#method.remove_private).
/// ///
/// Unless a value is supplied for the given key, the following defaults are /// Unless a value is set for the given property, the following defaults are
/// set on `cookie` before being added to `self`: /// set on `cookie` before being added to `self`:
/// ///
/// * `path`: `"/"` /// * `path`: `"/"`
@ -332,51 +218,176 @@ impl Cookies<'_> {
/// 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 /// # Example
/// enabled. ///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::http::{Cookie, CookieJar};
///
/// #[get("/")]
/// fn handler(jar: &CookieJar<'_>) {
/// jar.add_private(Cookie::new("name", "value"));
/// }
/// ```
#[cfg(feature = "private-cookies")]
#[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
pub fn add_private(&self, mut cookie: Cookie<'static>) {
Self::set_private_defaults(&mut cookie);
self.jar.private(&*self.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 initial `path` and `domain` will result in
/// cookies that are not properly removed. For convenience, if a path is not
/// set on `cookie`, the `"/"` path will automatically be set.
///
/// 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 /// # Example
/// ///
/// ```rust /// ```rust
/// # extern crate rocket; /// # #[macro_use] extern crate rocket;
/// use rocket::http::{Cookie, Cookies}; /// use rocket::http::{Cookie, CookieJar};
/// ///
/// fn handler(mut cookies: Cookies) { /// #[get("/")]
/// cookies.add_private(Cookie::new("name", "value")); /// fn handler(jar: &CookieJar<'_>) {
/// jar.remove(Cookie::named("name"));
/// } /// }
/// ``` /// ```
pub fn add_private(&mut self, mut cookie: Cookie<'static>) { pub fn remove(&self, mut cookie: Cookie<'static>) {
if let Cookies::Jarred(ref mut jar, key, _) = *self { if cookie.path().is_none() {
Cookies::set_private_defaults(&mut cookie); cookie.set_path("/");
jar.private(key).add(cookie)
} }
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
/// # #[macro_use] extern crate rocket;
/// use rocket::http::{Cookie, CookieJar};
///
/// #[get("/")]
/// fn handler(jar: &CookieJar<'_>) {
/// jar.remove_private(Cookie::named("name"));
/// }
/// ```
#[cfg(feature = "private-cookies")]
#[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
pub fn remove_private(&self, mut cookie: Cookie<'static>) {
if cookie.path().is_none() {
cookie.set_path("/");
}
self.jar.private(&*self.key).remove(cookie)
}
/// Returns an iterator over all of the cookies present in this collection.
///
/// # Example
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::http::{Cookie, CookieJar};
///
/// #[get("/")]
/// fn handler(jar: &CookieJar<'_>) {
/// for c in jar.iter() {
/// println!("Name: {:?}, Value: {:?}", c.name(), c.value());
/// }
/// }
/// ```
pub fn iter(&self) -> impl Iterator<Item = CookieCrumb> + '_ {
self.jar.iter()
}
}
/// WARNING: These is unstable! Do not use outside of Rocket!
#[doc(hidden)]
impl<'a> CookieJar<'a> {
#[inline(always)]
pub fn new(key: &'a Key) -> CookieJar<'a> {
CookieJar { jar: cookie::CookieJar::new(), key }
}
#[inline(always)]
pub fn from(jar: cookie::CookieJar, key: &'a Key) -> CookieJar<'a> {
CookieJar { jar, key }
}
/// Removes all delta cookies.
#[inline(always)]
pub fn reset_delta(&self) {
self.jar.reset_delta()
}
#[inline(always)]
pub fn delta(&self) -> cookie::Delta {
self.jar.delta()
}
/// Adds an original `cookie` to this collection.
#[inline(always)]
pub fn add_original(&self, cookie: Cookie<'static>) {
self.jar.add_original(cookie)
} }
/// Adds an original, private `cookie` to the collection. /// Adds an original, private `cookie` to the collection.
/// WARNING: This is unstable! Do not use this method outside of Rocket! #[inline(always)]
#[doc(hidden)] #[cfg(feature = "private-cookies")]
pub fn add_original_private(&mut self, mut cookie: Cookie<'static>) { #[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
if let Cookies::Jarred(ref mut jar, key, _) = *self { pub fn add_original_private(&self, cookie: Cookie<'static>) {
Cookies::set_private_defaults(&mut cookie); self.jar.private(&*self.key).add_original(cookie);
jar.private(key).add_original(cookie) }
/// For each property mentioned below, this method checks if there is a
/// provided value and if there is none, sets a default value. Default
/// values are:
///
/// * `path`: `"/"`
/// * `SameSite`: `Strict`
///
fn set_defaults(cookie: &mut Cookie<'static>) {
if cookie.path().is_none() {
cookie.set_path("/");
}
if cookie.same_site().is_none() {
cookie.set_same_site(SameSite::Strict);
} }
} }
/// For each property mentioned below, this method checks /// For each property mentioned below, this method checks if there is a
/// if there is a provided value and if there is none, sets /// provided value and if there is none, sets a default value. Default
/// a default value. /// values are:
/// Default values are:
/// ///
/// * `path`: `"/"` /// * `path`: `"/"`
/// * `SameSite`: `Strict` /// * `SameSite`: `Strict`
/// * `HttpOnly`: `true` /// * `HttpOnly`: `true`
/// * `Expires`: 1 week from now /// * `Expires`: 1 week from now
/// ///
#[cfg(feature = "private-cookies")]
#[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
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("/");
} }
if cookie.same_site().is_none() {
cookie.set_same_site(SameSite::Strict);
}
if cookie.http_only().is_none() { if cookie.http_only().is_none() {
cookie.set_http_only(true); cookie.set_http_only(true);
} }
@ -384,48 +395,12 @@ impl Cookies<'_> {
if cookie.expires().is_none() { if cookie.expires().is_none() {
cookie.set_expires(time::OffsetDateTime::now_utc() + time::Duration::weeks(1)); cookie.set_expires(time::OffsetDateTime::now_utc() + time::Duration::weeks(1));
} }
if cookie.same_site().is_none() {
cookie.set_same_site(SameSite::Strict);
}
}
/// 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
/// # extern crate rocket;
/// 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 let Cookies::Jarred(ref mut jar, key, _) = *self {
if cookie.path().is_none() {
cookie.set_path("/");
}
jar.private(key).remove(cookie)
}
} }
} }
impl fmt::Debug for Cookies<'_> { impl fmt::Debug for CookieJar<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self { self.jar.fmt(f)
Cookies::Jarred(ref jar, _, _) => jar.fmt(f),
Cookies::Empty(ref jar) => jar.fmt(f)
}
} }
} }
@ -440,3 +415,15 @@ impl From<&Cookie<'_>> for Header<'static> {
Header::new("Set-Cookie", cookie.encoded().to_string()) Header::new("Set-Cookie", cookie.encoded().to_string())
} }
} }
impl From<CookieCrumb> for Header<'static> {
fn from(cookie: CookieCrumb) -> Header<'static> {
Header::new("Set-Cookie", cookie.encoded().to_string())
}
}
impl From<&CookieCrumb> for Header<'static> {
fn from(cookie: &CookieCrumb) -> Header<'static> {
Header::new("Set-Cookie", cookie.encoded().to_string())
}
}

View File

@ -1,5 +1,7 @@
#![recursion_limit="512"] #![recursion_limit="512"]
#![cfg_attr(nightly, feature(doc_cfg))]
#![warn(rust_2018_idioms)] #![warn(rust_2018_idioms)]
//! Types that map to concepts in HTTP. //! Types that map to concepts in HTTP.
@ -44,13 +46,17 @@ pub mod uncased;
pub mod private { pub mod private {
// We need to export these for codegen, but otherwise it's unnecessary. // We need to export these for codegen, but otherwise it's unnecessary.
// TODO: Expose a `const fn` from ContentType when possible. (see RFC#1817) // TODO: Expose a `const fn` from ContentType when possible. (see RFC#1817)
// FIXME(rustc): These show up in the rexported module.
pub use crate::parse::Indexed; pub use crate::parse::Indexed;
pub use crate::media_type::{MediaParams, Source}; pub use crate::media_type::{MediaParams, Source};
pub use smallvec::{SmallVec, Array}; pub use smallvec::{SmallVec, Array};
// This one we need to expose for core. // These we need to expose for core.
pub use crate::cookies::{Key, CookieJar}; pub mod cookie {
pub use cookie::*;
pub use crate::cookies::Key;
}
// These as well.
pub use crate::listener::{Incoming, Listener, Connection, bind_tcp}; pub use crate::listener::{Incoming, Listener, Connection, bind_tcp};
} }
@ -60,6 +66,5 @@ pub use crate::accept::{Accept, QMediaType};
pub use crate::status::{Status, StatusClass}; pub use crate::status::{Status, StatusClass};
pub use crate::header::{Header, HeaderMap}; pub use crate::header::{Header, HeaderMap};
pub use crate::raw_str::RawStr; pub use crate::raw_str::RawStr;
pub use crate::media_type::MediaType; pub use crate::media_type::MediaType;
pub use crate::cookies::{Cookie, SameSite, Cookies}; pub use crate::cookies::{Cookie, CookieJar, CookieCrumb, SameSite};

View File

@ -19,9 +19,9 @@ edition = "2018"
all-features = true all-features = true
[features] [features]
default = ["private-cookies"] default = ["secrets"]
tls = ["rocket_http/tls"] tls = ["rocket_http/tls"]
private-cookies = ["rocket_http/private-cookies"] secrets = ["rocket_http/private-cookies"]
[dependencies] [dependencies]
rocket_codegen = { version = "0.5.0-dev", path = "../codegen" } rocket_codegen = { version = "0.5.0-dev", path = "../codegen" }
@ -41,6 +41,11 @@ ref-cast = "1.0"
atomic = "0.4" atomic = "0.4"
ubyte = "0.9.1" ubyte = "0.9.1"
[dependencies.cookie]
git = "https://github.com/SergioBenitez/cookie-rs.git"
rev = "3795f2e"
features = ["percent-encode"]
[dependencies.pear] [dependencies.pear]
git = "https://github.com/SergioBenitez/Pear.git" git = "https://github.com/SergioBenitez/Pear.git"
rev = "4b68055" rev = "4b68055"

View File

@ -22,4 +22,8 @@ fn main() {
println!("cargo:warning={}", "Rocket was unable to check rustc compiler compatibility."); println!("cargo:warning={}", "Rocket was unable to check rustc compiler compatibility.");
println!("cargo:warning={}", "Build may fail due to incompatible rustc version."); println!("cargo:warning={}", "Build may fail due to incompatible rustc version.");
} }
if let Some(true) = version_check::is_feature_flaggable() {
println!("cargo:rustc-cfg=nightly");
}
} }

View File

@ -4,11 +4,11 @@ use std::path::{Path, PathBuf};
use std::convert::AsRef; use std::convert::AsRef;
use std::fmt; use std::fmt;
use crate::http::private::cookie::Key;
use crate::config::Environment::*; use crate::config::Environment::*;
use crate::config::{Result, ConfigBuilder, Environment, ConfigError, LoggingLevel}; use crate::config::{Result, ConfigBuilder, Environment, ConfigError, LoggingLevel};
use crate::config::{FullConfig, Table, Value, Array, Datetime}; use crate::config::{FullConfig, Table, Value, Array, Datetime};
use crate::data::Limits; use crate::data::Limits;
use crate::http::private::Key;
use super::custom_values::*; use super::custom_values::*;

View File

@ -2,7 +2,7 @@ use std::fmt;
#[cfg(feature = "tls")] use crate::http::tls::{Certificate, PrivateKey}; #[cfg(feature = "tls")] use crate::http::tls::{Certificate, PrivateKey};
use crate::http::private::Key; use crate::http::private::cookie::Key;
use crate::config::{Result, Config, Value, ConfigError, LoggingLevel}; use crate::config::{Result, Config, Value, ConfigError, LoggingLevel};
use crate::data::Limits; use crate::data::Limits;
@ -23,7 +23,7 @@ impl SecretKey {
#[inline] #[inline]
pub(crate) fn is_generated(&self) -> bool { pub(crate) fn is_generated(&self) -> bool {
match *self { match *self {
#[cfg(feature = "private-cookies")] #[cfg(feature = "secrets")]
SecretKey::Generated(_) => true, SecretKey::Generated(_) => true,
_ => false _ => false
} }
@ -31,15 +31,17 @@ impl SecretKey {
} }
impl fmt::Display for SecretKey { impl fmt::Display for SecretKey {
#[cfg(feature = "secrets")]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
#[cfg(feature = "private-cookies")]
match *self { match *self {
SecretKey::Generated(_) => write!(f, "generated"), SecretKey::Generated(_) => write!(f, "generated"),
SecretKey::Provided(_) => write!(f, "provided"), SecretKey::Provided(_) => write!(f, "provided"),
} }
}
#[cfg(not(feature = "private-cookies"))] #[cfg(not(feature = "secrets"))]
write!(f, "private-cookies disabled") fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
"private-cookies disabled".fmt(f)
} }
} }

View File

@ -225,9 +225,9 @@ impl Drop for LaunchError {
use crate::http::uri; use crate::http::uri;
use crate::http::ext::IntoOwned; use crate::http::ext::IntoOwned;
use crate::http::route::{Error as SegmentError}; use crate::http::route::Error as SegmentError;
/// Error returned by [`set_uri()`](crate::Route::set_uri()) on invalid URIs. /// Error returned by [`Route::map_base()`] on invalid URIs.
#[derive(Debug)] #[derive(Debug)]
pub enum RouteUriError { pub enum RouteUriError {
/// The base (mount point) or route path contains invalid segments. /// The base (mount point) or route path contains invalid segments.

View File

@ -3,6 +3,7 @@
#![doc(html_root_url = "https://api.rocket.rs/v0.5")] #![doc(html_root_url = "https://api.rocket.rs/v0.5")]
#![doc(html_favicon_url = "https://rocket.rs/images/favicon.ico")] #![doc(html_favicon_url = "https://rocket.rs/images/favicon.ico")]
#![doc(html_logo_url = "https://rocket.rs/images/logo-boxed.png")] #![doc(html_logo_url = "https://rocket.rs/images/logo-boxed.png")]
#![cfg_attr(nightly, feature(doc_cfg))]
#![warn(rust_2018_idioms)] #![warn(rust_2018_idioms)]
@ -35,21 +36,13 @@
//! //!
//! ## Usage //! ## Usage
//! //!
//! First, depend on `rocket` in `Cargo.toml`: //! Depend on `rocket` in `Cargo.toml`:
//! //!
//! ```toml //! ```toml
//! [dependencies] //! [dependencies]
//! rocket = "0.5.0-dev" //! rocket = "0.5.0-dev"
//! ``` //! ```
//! //!
//! Then, add the following to the top of your `main.rs` file:
//!
//! ```rust
//! #[macro_use] extern crate rocket;
//! # #[get("/")] fn hello() { }
//! # fn main() { rocket::ignite().mount("/", routes![hello]); }
//! ```
//!
//! See the [guide](https://rocket.rs/v0.5/guide) for more information on how to //! See the [guide](https://rocket.rs/v0.5/guide) for more information on how to
//! write Rocket applications. Here's a simple example to get you started: //! write Rocket applications. Here's a simple example to get you started:
//! //!
@ -67,6 +60,20 @@
//! } //! }
//! ``` //! ```
//! //!
//! ## Features
//!
//! The `secrets` feature, which enables [private cookies], is enabled by
//! default. This necessitates pulling in additional dependencies. To avoid
//! these dependencies when your application does not use private cookies,
//! disable the `secrets` feature:
//!
//! ```toml
//! [dependencies]
//! rocket = { version = "0.5.0-dev", default-features = false }
//! ```
//!
//! [private cookies]: crate::http::CookieJar#private-cookies
//!
//! ## Configuration //! ## Configuration
//! //!
//! Rocket and Rocket libraries are configured via the `Rocket.toml` file and/or //! Rocket and Rocket libraries are configured via the `Rocket.toml` file and/or

View File

@ -1,9 +1,8 @@
use std::sync::RwLock;
use std::borrow::Cow; use std::borrow::Cow;
use crate::local::asynchronous::{LocalRequest, LocalResponse}; use crate::local::asynchronous::{LocalRequest, LocalResponse};
use crate::rocket::{Rocket, Cargo}; use crate::rocket::{Rocket, Cargo};
use crate::http::{Method, private::CookieJar}; use crate::http::{private::cookie, Method};
use crate::error::LaunchError; use crate::error::LaunchError;
/// An `async` client to construct and dispatch local requests. /// An `async` client to construct and dispatch local requests.
@ -14,19 +13,17 @@ use crate::error::LaunchError;
/// ///
/// ## Multithreaded Synchronization Pitfalls /// ## Multithreaded Synchronization Pitfalls
/// ///
/// Unlike its [`blocking`](crate::local::blocking) variant, this `async` `Client` /// Unlike its [`blocking`](crate::local::blocking) variant, this `async`
/// implements `Sync`. However, using it in a multithreaded environment while /// `Client` implements `Sync`. However, using it in a multithreaded environment
/// tracking cookies can result in surprising, non-deterministic behavior. This /// while tracking cookies can result in surprising, non-deterministic behavior.
/// is because while cookie modifications are serialized, the exact ordering /// This is because while cookie modifications are serialized, the ordering
/// depends on when requests are dispatched. Specifically, when cookie tracking /// depends on the ordering of request dispatch.
/// is enabled, all request dispatches are serialized, which in-turn serializes
/// modifications to the internally tracked cookies.
/// ///
/// If possible, refrain from sharing a single instance of `Client` across /// If possible, refrain from sharing a single instance of `Client` across
/// multiple threads. Instead, prefer to create a unique instance of `Client` /// multiple threads. Instead, prefer to create a unique instance of `Client`
/// per thread. If it's not possible, ensure that either you are not depending /// per thread. If this is not possible, ensure that you are not depending on
/// on cookies, the ordering of their modifications, or both, or have arranged /// the ordering of cookie modifications or have arranged for request dispatch
/// for dispatches to occur in a deterministic ordering. /// to occur in a deterministic manner.
/// ///
/// ## Example /// ## Example
/// ///
@ -47,7 +44,8 @@ use crate::error::LaunchError;
/// ``` /// ```
pub struct Client { pub struct Client {
cargo: Cargo, cargo: Cargo,
pub(in super) cookies: Option<RwLock<CookieJar>>, pub(in super) tracked: bool,
pub(in super) cookies: cookie::CookieJar,
} }
impl Client { impl Client {
@ -56,13 +54,9 @@ impl Client {
tracked: bool tracked: bool
) -> Result<Client, LaunchError> { ) -> Result<Client, LaunchError> {
rocket.prelaunch_check().await?; rocket.prelaunch_check().await?;
let cargo = rocket.into_cargo().await;
let cookies = match tracked { Ok(Client { cargo, tracked, cookies: cookie::CookieJar::new() })
true => Some(RwLock::new(CookieJar::new())),
false => None
};
Ok(Client { cargo: rocket.into_cargo().await, cookies })
} }
// WARNING: This is unstable! Do not use this method outside of Rocket! // WARNING: This is unstable! Do not use this method outside of Rocket!
@ -84,6 +78,11 @@ impl Client {
&self.cargo &self.cargo
} }
#[inline(always)]
pub(crate) fn _cookies(&self) -> &cookie::CookieJar {
&self.cookies
}
#[inline(always)] #[inline(always)]
fn _req<'c, 'u: 'c, U>(&'c self, method: Method, uri: U) -> LocalRequest<'c> fn _req<'c, 'u: 'c, U>(&'c self, method: Method, uri: U) -> LocalRequest<'c>
where U: Into<Cow<'u, str>> where U: Into<Cow<'u, str>>

View File

@ -30,8 +30,8 @@ use super::{Client, LocalResponse};
/// # }); /// # });
/// ``` /// ```
pub struct LocalRequest<'c> { pub struct LocalRequest<'c> {
client: &'c Client, pub(in super) client: &'c Client,
request: Request<'c>, pub(in super) request: Request<'c>,
data: Vec<u8>, data: Vec<u8>,
uri: Cow<'c, str>, uri: Cow<'c, str>,
} }
@ -47,11 +47,10 @@ impl<'c> LocalRequest<'c> {
let origin = Origin::parse(&uri).unwrap_or_else(|_| Origin::dummy()); let origin = Origin::parse(&uri).unwrap_or_else(|_| Origin::dummy());
let request = Request::new(client.rocket(), method, origin.into_owned()); let request = Request::new(client.rocket(), method, origin.into_owned());
// Set up any cookies we know about. // Add any cookies we know about.
if let Some(ref jar) = client.cookies { if client.tracked {
let cookies = jar.read().expect("LocalRequest::new() read lock"); for crumb in client.cookies.iter() {
for cookie in cookies.iter() { request.cookies().add_original(crumb.into_cookie());
request.cookies().add_original(cookie.clone().into_owned());
} }
} }
@ -86,24 +85,24 @@ impl<'c> LocalRequest<'c> {
// Actually dispatch the request. // Actually dispatch the request.
let mut data = Data::local(self.data); let mut data = Data::local(self.data);
let token = rocket.preprocess_request(&mut self.request, &mut data).await; let token = rocket.preprocess_request(&mut self.request, &mut data).await;
let response = LocalResponse::new(self.request, move |request| { let response = LocalResponse::new(self.request, move |req| {
rocket.dispatch(token, request, data) rocket.dispatch(token, req, data)
}).await; }).await;
// If the client is tracking cookies, updates the internal cookie jar // If the client is tracking cookies, updates the internal cookie jar
// with the changes reflected by `response`. // with the changes reflected by `response`.
if let Some(ref jar) = self.client.cookies { if self.client.tracked {
let mut jar = jar.write().expect("LocalRequest::_dispatch() write lock"); let jar = &self.client.cookies;
let current_time = time::OffsetDateTime::now_utc(); let current_time = time::OffsetDateTime::now_utc();
for cookie in response.cookies() { for cookie in response.cookies().iter() {
if let Some(expires) = cookie.expires() { if let Some(expires) = cookie.expires() {
if expires <= current_time { if expires <= current_time {
jar.force_remove(cookie); jar.force_remove(&cookie);
continue; continue;
} }
} }
jar.add(cookie.into_owned()); jar.add(cookie.into_cookie());
} }
} }

View File

@ -4,6 +4,7 @@ use std::{pin::Pin, task::{Context, Poll}};
use tokio::io::AsyncRead; use tokio::io::AsyncRead;
use crate::http::CookieJar;
use crate::{Request, Response}; use crate::{Request, Response};
/// An `async` response from a dispatched [`LocalRequest`](super::LocalRequest). /// An `async` response from a dispatched [`LocalRequest`](super::LocalRequest).
@ -55,6 +56,7 @@ use crate::{Request, Response};
pub struct LocalResponse<'c> { pub struct LocalResponse<'c> {
_request: Box<Request<'c>>, _request: Box<Request<'c>>,
response: Response<'c>, response: Response<'c>,
cookies: CookieJar<'c>,
} }
impl<'c> LocalResponse<'c> { impl<'c> LocalResponse<'c> {
@ -84,11 +86,15 @@ impl<'c> LocalResponse<'c> {
// away as `'_`, ensuring it is not used for any output value. // away as `'_`, ensuring it is not used for any output value.
let boxed_req = Box::new(req); let boxed_req = Box::new(req);
let request: &'c Request<'c> = unsafe { &*(&*boxed_req as *const _) }; let request: &'c Request<'c> = unsafe { &*(&*boxed_req as *const _) };
async move { async move {
LocalResponse { let response: Response<'c> = f(request).await;
_request: boxed_req, let cookies = CookieJar::new(request.state.config.secret_key());
response: f(request).await for cookie in response.cookies() {
cookies.add(cookie.into_owned());
} }
LocalResponse { cookies, _request: boxed_req, response, }
} }
} }
} }
@ -98,6 +104,10 @@ impl LocalResponse<'_> {
&self.response &self.response
} }
pub(crate) fn _cookies(&self) -> &CookieJar<'_> {
&self.cookies
}
pub(crate) async fn _into_string(mut self) -> Option<String> { pub(crate) async fn _into_string(mut self) -> Option<String> {
self.response.body_string().await self.response.body_string().await
} }

View File

@ -2,9 +2,9 @@ use std::borrow::Cow;
use std::cell::RefCell; use std::cell::RefCell;
use crate::error::LaunchError; use crate::error::LaunchError;
use crate::http::Method;
use crate::local::{asynchronous, blocking::{LocalRequest, LocalResponse}}; use crate::local::{asynchronous, blocking::{LocalRequest, LocalResponse}};
use crate::rocket::{Rocket, Cargo}; use crate::rocket::{Rocket, Cargo};
use crate::http::Method;
/// A `blocking` client to construct and dispatch local requests. /// A `blocking` client to construct and dispatch local requests.
/// ///
@ -67,6 +67,11 @@ impl Client {
self.inner._cargo() self.inner._cargo()
} }
#[inline(always)]
fn _cookies(&self) -> &cookie::CookieJar {
self.inner._cookies()
}
#[inline(always)] #[inline(always)]
pub(crate) fn _req<'c, 'u: 'c, U>( pub(crate) fn _req<'c, 'u: 'c, U>(
&'c self, &'c self,

View File

@ -1,7 +1,7 @@
use std::io; use std::io;
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
use crate::{Response, local::asynchronous}; use crate::{Response, local::asynchronous, http::CookieJar};
use super::Client; use super::Client;
@ -60,6 +60,10 @@ impl LocalResponse<'_> {
&self.inner._response() &self.inner._response()
} }
pub(crate) fn _cookies(&self) -> &CookieJar<'_> {
self.inner._cookies()
}
fn _into_string(self) -> Option<String> { fn _into_string(self) -> Option<String> {
self.client.block_on(self.inner._into_string()) self.client.block_on(self.inner._into_string())
} }

View File

@ -54,7 +54,7 @@ macro_rules! pub_client_impl {
/// ///
/// This is typically the desired mode of operation for a `Client` as it /// This is typically the desired mode of operation for a `Client` as it
/// removes the burden of manually tracking cookies. Under some /// removes the burden of manually tracking cookies. Under some
/// circumstances, however, disabling this tracking may be desired. The /// circumstances, however, disabling tracking may be desired. The
/// [`untracked()`](Client::untracked()) method creates a `Client` that /// [`untracked()`](Client::untracked()) method creates a `Client` that
/// _will not_ track cookies. /// _will not_ track cookies.
/// ///
@ -133,6 +133,29 @@ macro_rules! pub_client_impl {
self._cargo() self._cargo()
} }
/// Returns a cookie jar containing all of the cookies this client is
/// currently tracking.
///
/// If cookie tracking is disabled, the returned jar will always be empty.
/// Otherwise, it will contains all of the cookies collected from responses
/// to requests dispatched by this client that have not expired.
///
/// # Example
///
/// ```rust
#[doc = $import]
///
/// # Client::_test(|client, _, _| {
/// let client: &Client = client;
/// let cookie = client.cookies();
/// # });
/// ```
#[inline(always)]
pub fn cookies(&self) -> crate::http::CookieJar<'_> {
let key = self.rocket().config.secret_key();
crate::http::CookieJar::from(self._cookies().clone(), key)
}
req_method!($import, "GET", get, Method::Get); req_method!($import, "GET", get, Method::Get);
req_method!($import, "PUT", put, Method::Put); req_method!($import, "PUT", put, Method::Put);
req_method!($import, "POST", post, Method::Post); req_method!($import, "POST", post, Method::Post);

View File

@ -118,9 +118,7 @@ macro_rules! pub_request_impl {
/// Add all of the cookies in `cookies` to this request. /// Add all of the cookies in `cookies` to this request.
/// ///
/// # Examples /// # Example
///
/// Add `user_id` cookie:
/// ///
/// ```rust /// ```rust
#[doc = $import] #[doc = $import]
@ -133,7 +131,9 @@ macro_rules! pub_request_impl {
/// # }); /// # });
/// ``` /// ```
#[inline] #[inline]
pub fn cookies(self, cookies: Vec<crate::http::Cookie<'_>>) -> Self { pub fn cookies<'a, C>(self, cookies: C) -> Self
where C: IntoIterator<Item = crate::http::Cookie<'a>>
{
for cookie in cookies { for cookie in cookies {
self._request().cookies().add_original(cookie.into_owned()); self._request().cookies().add_original(cookie.into_owned());
} }
@ -143,10 +143,7 @@ macro_rules! pub_request_impl {
/// Add a [private cookie] to this request. /// Add a [private cookie] to this request.
/// ///
/// This method is only available when the `private-cookies` feature is /// [private cookie]: crate::http::CookieJar::add_private()
/// enabled.
///
/// [private cookie]: crate::http::Cookies::add_private()
/// ///
/// # Examples /// # Examples
/// ///
@ -161,8 +158,9 @@ macro_rules! pub_request_impl {
/// let req = request.private_cookie(Cookie::new("user_id", "sb")); /// let req = request.private_cookie(Cookie::new("user_id", "sb"));
/// # }); /// # });
/// ``` /// ```
#[cfg(feature = "secrets")]
#[cfg_attr(nightly, doc(cfg(feature = "secrets")))]
#[inline] #[inline]
#[cfg(feature = "private-cookies")]
pub fn private_cookie(self, cookie: crate::http::Cookie<'static>) -> Self { pub fn private_cookie(self, cookie: crate::http::Cookie<'static>) -> Self {
self._request().cookies().add_original_private(cookie); self._request().cookies().add_original_private(cookie);
self self

View File

@ -37,8 +37,22 @@ macro_rules! pub_response_impl {
getter_method!($doc_prelude, "HTTP headers", getter_method!($doc_prelude, "HTTP headers",
headers -> &crate::http::HeaderMap<'_>); headers -> &crate::http::HeaderMap<'_>);
getter_method!($doc_prelude, "HTTP cookies as set in the `Set-Cookie` header", /// Return a cookie jar containing the HTTP cookies in the response.
cookies -> Vec<crate::http::Cookie<'_>>); ///
/// # Example
///
/// ```rust
#[doc = $doc_prelude]
///
/// # Client::_test(|_, _, response| {
/// let response: LocalResponse = response;
/// let string = response.cookies();
/// # });
/// ```
#[inline(always)]
pub fn cookies(&self) -> &crate::http::CookieJar<'_> {
self._cookies()
}
getter_method!($doc_prelude, "response body, if there is one,", getter_method!($doc_prelude, "response body, if there is one,",
body -> Option<&crate::response::ResponseBody<'_>>); body -> Option<&crate::response::ResponseBody<'_>>);

View File

@ -8,7 +8,7 @@ use crate::request::Request;
use crate::outcome::{self, IntoOutcome}; use crate::outcome::{self, IntoOutcome};
use crate::outcome::Outcome::*; use crate::outcome::Outcome::*;
use crate::http::{Status, ContentType, Accept, Method, Cookies, uri::Origin}; use crate::http::{Status, ContentType, Accept, Method, CookieJar, uri::Origin};
/// Type alias for the `Outcome` of a `FromRequest` conversion. /// Type alias for the `Outcome` of a `FromRequest` conversion.
pub type Outcome<S, E> = outcome::Outcome<S, (Status, E), ()>; pub type Outcome<S, E> = outcome::Outcome<S, (Status, E), ()>;
@ -140,10 +140,10 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
/// For information on when an `&Route` is available, see /// For information on when an `&Route` is available, see
/// [`Request::route()`]. /// [`Request::route()`].
/// ///
/// * **Cookies** /// * **&CookieJar**
/// ///
/// Returns a borrow to the [`Cookies`] in the incoming request. Note that /// Returns a borrow to the [`CookieJar`] in the incoming request. Note that
/// `Cookies` implements internal mutability, so a handle to `Cookies` /// `CookieJar` implements internal mutability, so a handle to a `CookieJar`
/// allows you to get _and_ set cookies in the request. /// allows you to get _and_ set cookies in the request.
/// ///
/// _This implementation always returns successfully._ /// _This implementation always returns successfully._
@ -242,7 +242,7 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
/// ///
/// ```rust /// ```rust
/// # #[macro_use] extern crate rocket; /// # #[macro_use] extern crate rocket;
/// # #[cfg(feature = "private-cookies")] mod inner { /// # #[cfg(feature = "secrets")] mod wrapper {
/// # use rocket::outcome::IntoOutcome; /// # use rocket::outcome::IntoOutcome;
/// # use rocket::request::{self, Outcome, FromRequest, Request}; /// # use rocket::request::{self, Outcome, FromRequest, Request};
/// # struct User { id: String, is_admin: bool } /// # struct User { id: String, is_admin: bool }
@ -296,7 +296,7 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
/// ///
/// #[get("/dashboard", rank = 2)] /// #[get("/dashboard", rank = 2)]
/// fn user_dashboard(user: User) { } /// fn user_dashboard(user: User) { }
/// # } /// # } // end of cfg wrapper
/// ``` /// ```
/// ///
/// When a non-admin user is logged in, the database will be queried twice: once /// When a non-admin user is logged in, the database will be queried twice: once
@ -306,7 +306,7 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
/// ///
/// ```rust /// ```rust
/// # #[macro_use] extern crate rocket; /// # #[macro_use] extern crate rocket;
/// # #[cfg(feature = "private-cookies")] mod inner { /// # #[cfg(feature = "secrets")] mod wrapper {
/// # use rocket::outcome::IntoOutcome; /// # use rocket::outcome::IntoOutcome;
/// # use rocket::request::{self, Outcome, FromRequest, Request}; /// # use rocket::request::{self, Outcome, FromRequest, Request};
/// # struct User { id: String, is_admin: bool } /// # struct User { id: String, is_admin: bool }
@ -358,14 +358,13 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
/// } /// }
/// } /// }
/// } /// }
/// # } /// # } // end of cfg wrapper
/// ``` /// ```
/// ///
/// Notice that these request guards provide access to *borrowed* data (`&'a /// Notice that these request guards provide access to *borrowed* data (`&'a
/// User` and `Admin<'a>`) as the data is now owned by the request's cache. /// User` and `Admin<'a>`) as the data is now owned by the request's cache.
/// ///
/// [request-local state]: https://rocket.rs/v0.5/guide/state/#request-local-state /// [request-local state]: https://rocket.rs/v0.5/guide/state/#request-local-state
#[crate::async_trait] #[crate::async_trait]
pub trait FromRequest<'a, 'r>: Sized { pub trait FromRequest<'a, 'r>: Sized {
/// The associated error to be returned if derivation fails. /// The associated error to be returned if derivation fails.
@ -411,7 +410,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for &'r Route {
} }
#[crate::async_trait] #[crate::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for Cookies<'a> { impl<'a, 'r> FromRequest<'a, 'r> for &'a CookieJar<'r> {
type Error = std::convert::Infallible; type Error = std::convert::Infallible;
async fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> { async fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> {

View File

@ -1,4 +1,4 @@
use std::sync::{Arc, RwLock, Mutex}; use std::sync::Arc;
use std::net::{IpAddr, SocketAddr}; use std::net::{IpAddr, SocketAddr};
use std::future::Future; use std::future::Future;
use std::fmt; use std::fmt;
@ -7,20 +7,18 @@ use std::str;
use yansi::Paint; use yansi::Paint;
use state::{Container, Storage}; use state::{Container, Storage};
use futures::future::BoxFuture; use futures::future::BoxFuture;
use atomic::Atomic; use atomic::{Atomic, Ordering};
use crate::request::{FromParam, FromSegments, FromRequest, Outcome}; use crate::request::{FromParam, FromSegments, FromRequest, Outcome};
use crate::request::{FromFormValue, FormItems, FormItem}; use crate::request::{FromFormValue, FormItems, FormItem};
use crate::{Rocket, Config, Shutdown, Route}; use crate::{Rocket, Config, Shutdown, Route};
use crate::http::{hyper, uri::{Origin, Segments}}; use crate::http::{hyper, uri::{Origin, Segments}};
use crate::http::{Method, Header, HeaderMap, Cookies}; use crate::http::{Method, Header, HeaderMap};
use crate::http::{RawStr, ContentType, Accept, MediaType}; use crate::http::{RawStr, ContentType, Accept, MediaType, CookieJar, Cookie};
use crate::http::private::{Indexed, SmallVec, CookieJar}; use crate::http::private::{Indexed, SmallVec};
use crate::data::Limits; use crate::data::Limits;
type Indices = (usize, usize);
/// The type of an incoming web request. /// The type of an incoming web request.
/// ///
/// This should be used sparingly in Rocket applications. In particular, it /// This should be used sparingly in Rocket applications. In particular, it
@ -41,14 +39,14 @@ pub(crate) struct RequestState<'r> {
pub shutdown: &'r Shutdown, pub shutdown: &'r Shutdown,
pub path_segments: SmallVec<[Indices; 12]>, pub path_segments: SmallVec<[Indices; 12]>,
pub query_items: Option<SmallVec<[IndexedFormItem; 6]>>, pub query_items: Option<SmallVec<[IndexedFormItem; 6]>>,
pub route: RwLock<Option<&'r Route>>, pub route: Atomic<Option<&'r Route>>,
pub cookies: Mutex<Option<CookieJar>>, pub cookies: CookieJar<'r>,
pub accept: Storage<Option<Accept>>, pub accept: Storage<Option<Accept>>,
pub content_type: Storage<Option<ContentType>>, pub content_type: Storage<Option<ContentType>>,
pub cache: Arc<Container>, pub cache: Arc<Container>,
} }
impl<'r> Request<'r> { impl Request<'_> {
pub(crate) fn clone(&self) -> Self { pub(crate) fn clone(&self) -> Self {
Request { Request {
method: Atomic::new(self.method()), method: Atomic::new(self.method()),
@ -60,24 +58,16 @@ impl<'r> Request<'r> {
} }
} }
impl<'r> RequestState<'r> { impl RequestState<'_> {
fn clone(&self) -> RequestState<'r> { fn clone(&self) -> Self {
let route = self.route.try_read()
.map(|r| r.clone())
.unwrap_or(None);
let cookies = self.cookies.try_lock()
.map(|j| j.clone())
.unwrap_or_else(|_| Some(CookieJar::new()));
RequestState { RequestState {
config: self.config, config: self.config,
managed: self.managed, managed: self.managed,
shutdown: self.shutdown, shutdown: self.shutdown,
path_segments: self.path_segments.clone(), path_segments: self.path_segments.clone(),
query_items: self.query_items.clone(), query_items: self.query_items.clone(),
route: RwLock::new(route), route: Atomic::new(self.route.load(Ordering::Acquire)),
cookies: Mutex::new(cookies), cookies: self.cookies.clone(),
accept: self.accept.clone(), accept: self.accept.clone(),
content_type: self.content_type.clone(), content_type: self.content_type.clone(),
cache: self.cache.clone(), cache: self.cache.clone(),
@ -85,13 +75,6 @@ impl<'r> RequestState<'r> {
} }
} }
#[derive(Clone)]
pub(crate) struct IndexedFormItem {
raw: Indices,
key: Indices,
value: Indices
}
impl<'r> Request<'r> { impl<'r> Request<'r> {
/// Create a new `Request` with the given `method` and `uri`. /// Create a new `Request` with the given `method` and `uri`.
#[inline(always)] #[inline(always)]
@ -111,8 +94,8 @@ impl<'r> Request<'r> {
config: &rocket.config, config: &rocket.config,
managed: &rocket.managed_state, managed: &rocket.managed_state,
shutdown: &rocket.shutdown_handle, shutdown: &rocket.shutdown_handle,
route: RwLock::new(None), route: Atomic::new(None),
cookies: Mutex::new(Some(CookieJar::new())), cookies: CookieJar::new(rocket.config.secret_key()),
accept: Storage::new(), accept: Storage::new(),
content_type: Storage::new(), content_type: Storage::new(),
cache: Arc::new(Container::new()), cache: Arc::new(Container::new()),
@ -138,7 +121,7 @@ impl<'r> Request<'r> {
/// ``` /// ```
#[inline(always)] #[inline(always)]
pub fn method(&self) -> Method { pub fn method(&self) -> Method {
self.method.load(atomic::Ordering::Acquire) self.method.load(Ordering::Acquire)
} }
/// Set the method of `self`. /// Set the method of `self`.
@ -308,8 +291,8 @@ impl<'r> Request<'r> {
/// Returns a wrapped borrow to the cookies in `self`. /// Returns a wrapped borrow to the cookies in `self`.
/// ///
/// [`Cookies`] implements internal mutability, so this method allows you to /// [`CookieJar`] implements internal mutability, so this method allows you
/// get _and_ add/remove cookies in `self`. /// to get _and_ add/remove cookies in `self`.
/// ///
/// # Example /// # Example
/// ///
@ -325,26 +308,8 @@ impl<'r> Request<'r> {
/// request.cookies().add(Cookie::new("ans", format!("life: {}", 38 + 4))); /// request.cookies().add(Cookie::new("ans", format!("life: {}", 38 + 4)));
/// # }); /// # });
/// ``` /// ```
pub fn cookies(&self) -> Cookies<'_> { pub fn cookies(&self) -> &CookieJar<'r> {
// FIXME: Can we do better? This is disappointing. &self.state.cookies
let mut guard = self.state.cookies.lock().expect("cookies lock");
match guard.take() {
Some(jar) => {
let mutex = &self.state.cookies;
let on_drop = move |jar| {
*mutex.lock().expect("cookies lock") = Some(jar);
};
Cookies::new(jar, self.state.config.secret_key(), on_drop)
}
None => {
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()
}
}
} }
/// Returns a [`HeaderMap`] of all of the headers in `self`. /// Returns a [`HeaderMap`] of all of the headers in `self`.
@ -542,7 +507,7 @@ impl<'r> Request<'r> {
/// # }); /// # });
/// ``` /// ```
pub fn route(&self) -> Option<&'r Route> { pub fn route(&self) -> Option<&'r Route> {
*self.state.route.read().unwrap() self.state.route.load(Ordering::Acquire)
} }
/// Invokes the request guard implementation for `T`, returning its outcome. /// Invokes the request guard implementation for `T`, returning its outcome.
@ -779,7 +744,7 @@ impl<'r> Request<'r> {
} }
// All of these methods only exist for internal, including codegen, purposes. // All of these methods only exist for internal, including codegen, purposes.
// They _are not_ part of the stable API. // They _are not_ part of the stable API. Please, don't use these.
#[doc(hidden)] #[doc(hidden)]
impl<'r> Request<'r> { impl<'r> Request<'r> {
// Only used by doc-tests! Needs to be `pub` because doc-test are external. // Only used by doc-tests! Needs to be `pub` because doc-test are external.
@ -857,14 +822,14 @@ impl<'r> Request<'r> {
/// was `route`. Use during routing when attempting a given route. /// was `route`. Use during routing when attempting a given route.
#[inline(always)] #[inline(always)]
pub(crate) fn set_route(&self, route: &'r Route) { pub(crate) fn set_route(&self, route: &'r Route) {
*self.state.route.write().unwrap() = Some(route); self.state.route.store(Some(route), Ordering::Release)
} }
/// Set the method of `self`, even when `self` is a shared reference. Used /// Set the method of `self`, even when `self` is a shared reference. Used
/// during routing to override methods for re-routing. /// during routing to override methods for re-routing.
#[inline(always)] #[inline(always)]
pub(crate) fn _set_method(&self, method: Method) { pub(crate) fn _set_method(&self, method: Method) {
self.method.store(method, atomic::Ordering::Release) self.method.store(method, Ordering::Release)
} }
/// Convert from Hyper types into a Rocket Request. /// Convert from Hyper types into a Rocket Request.
@ -895,7 +860,6 @@ impl<'r> Request<'r> {
request.set_remote(h_addr); request.set_remote(h_addr);
// Set the request cookies, if they exist. // Set the request cookies, if they exist.
let mut cookie_jar = CookieJar::new();
for header in h_headers.get_all("Cookie") { for header in h_headers.get_all("Cookie") {
let raw_str = match std::str::from_utf8(header.as_bytes()) { let raw_str = match std::str::from_utf8(header.as_bytes()) {
Ok(string) => string, Ok(string) => string,
@ -903,12 +867,11 @@ impl<'r> Request<'r> {
}; };
for cookie_str in raw_str.split(';').map(|s| s.trim()) { for cookie_str in raw_str.split(';').map(|s| s.trim()) {
if let Some(cookie) = Cookies::parse_cookie(cookie_str) { if let Ok(cookie) = Cookie::parse_encoded(cookie_str) {
cookie_jar.add_original(cookie); request.state.cookies.add_original(cookie.into_owned());
} }
} }
} }
request.state.cookies = Mutex::new(Some(cookie_jar));
// Set the rest of the headers. // Set the rest of the headers.
for (name, value) in h_headers.iter() { for (name, value) in h_headers.iter() {
@ -929,6 +892,7 @@ impl fmt::Debug for Request<'_> {
.field("uri", &self.uri) .field("uri", &self.uri)
.field("headers", &self.headers()) .field("headers", &self.headers())
.field("remote", &self.remote()) .field("remote", &self.remote())
.field("cookies", &self.cookies())
.finish() .finish()
} }
} }
@ -950,6 +914,15 @@ impl fmt::Display for Request<'_> {
} }
} }
type Indices = (usize, usize);
#[derive(Clone)]
pub(crate) struct IndexedFormItem {
raw: Indices,
key: Indices,
value: Indices
}
impl IndexedFormItem { impl IndexedFormItem {
#[inline(always)] #[inline(always)]
fn from(s: &str, i: FormItem<'_>) -> Self { fn from(s: &str, i: FormItem<'_>) -> Self {

View File

@ -742,17 +742,13 @@ impl<'r> Response<'r> {
/// ///
/// let mut response = Response::new(); /// let mut response = Response::new();
/// response.set_header(Cookie::new("hello", "world!")); /// response.set_header(Cookie::new("hello", "world!"));
/// assert_eq!(response.cookies(), vec![Cookie::new("hello", "world!")]); /// let cookies: Vec<_> = response.cookies().collect();
/// assert_eq!(cookies, vec![Cookie::new("hello", "world!")]);
/// ``` /// ```
pub fn cookies(&self) -> Vec<Cookie<'_>> { pub fn cookies(&self) -> impl Iterator<Item = Cookie<'_>> {
let mut cookies = vec![]; self.headers()
for header in self.headers().get("Set-Cookie") { .get("Set-Cookie")
if let Ok(cookie) = Cookie::parse_encoded(header) { .filter_map(|header| Cookie::parse_encoded(header).ok())
cookies.push(cookie);
}
}
cookies
} }
/// Returns a [`HeaderMap`] of all of the headers in `self`. /// Returns a [`HeaderMap`] of all of the headers in `self`.

View File

@ -359,8 +359,8 @@ impl Rocket {
// Set the cookies. Note that error responses will only include // Set the cookies. Note that error responses will only include
// cookies set by the error handler. See `handle_error` for more. // cookies set by the error handler. See `handle_error` for more.
for cookie in request.cookies().delta() { for crumb in request.cookies().delta() {
response.adjoin_header(cookie); response.adjoin_header(crumb)
} }
response response
@ -651,7 +651,7 @@ impl Rocket {
} }
if config.secret_key.is_generated() && config.environment.is_prod() { if config.secret_key.is_generated() && config.environment.is_prod() {
warn!("environment is 'production', but no `secret_key` is configured"); warn!("environment is 'production' but no `secret_key` is configured");
} }
for (name, value) in config.extras() { for (name, value) in config.extras() {

View File

@ -1,16 +1,16 @@
#[macro_use] extern crate rocket; #[macro_use] extern crate rocket;
use rocket::request::Request; use rocket::request::Request;
use rocket::http::{Cookie, Cookies}; use rocket::http::{Cookie, CookieJar};
#[catch(404)] #[catch(404)]
fn not_found(request: &Request) -> &'static str { fn not_found(request: &Request) -> &'static str {
request.cookies().add(Cookie::new("not_found", "hi")); request.cookies().add(Cookie::new("not_found", "404"));
"404 - Not Found" "404 - Not Found"
} }
#[get("/")] #[get("/")]
fn index(mut cookies: Cookies) -> &'static str { fn index(cookies: &CookieJar<'_>) -> &'static str {
cookies.add(Cookie::new("index", "hi")); cookies.add(Cookie::new("index", "hi"));
"Hello, world!" "Hello, world!"
} }
@ -26,7 +26,7 @@ mod tests {
.mount("/", routes![index]) .mount("/", routes![index])
.register(catchers![not_found]) .register(catchers![not_found])
.attach(AdHoc::on_request("Add Fairing Cookie", |req, _| Box::pin(async move { .attach(AdHoc::on_request("Add Fairing Cookie", |req, _| Box::pin(async move {
req.cookies().add(Cookie::new("fairing", "hi")); req.cookies().add(Cookie::new("fairing", "woo"));
}))); })));
let client = Client::new(rocket).unwrap(); let client = Client::new(rocket).unwrap();
@ -34,12 +34,14 @@ mod tests {
// Check that the index returns the `index` and `fairing` cookie. // Check that the index returns the `index` and `fairing` cookie.
let response = client.get("/").dispatch(); let response = client.get("/").dispatch();
let cookies = response.cookies(); let cookies = response.cookies();
assert_eq!(cookies.len(), 2); assert_eq!(cookies.iter().count(), 2);
assert!(cookies.iter().find(|c| c.name() == "index").is_some()); assert_eq!(cookies.get("index").unwrap().value(), "hi");
assert!(cookies.iter().find(|c| c.name() == "fairing").is_some()); assert_eq!(cookies.get("fairing").unwrap().value(), "woo");
// Check that the catcher returns only the `not_found` cookie. // Check that the catcher returns only the `not_found` cookie.
let response = client.get("/not-existent").dispatch(); let response = client.get("/not-existent").dispatch();
assert_eq!(response.cookies(), vec![Cookie::new("not_found", "hi")]); let cookies = response.cookies();
assert_eq!(cookies.iter().count(), 1);
assert_eq!(cookies.get("not_found").unwrap().value(), "404");
} }
} }

View File

@ -1,13 +1,9 @@
#[macro_use] #[cfg(feature = "secrets")]
#[cfg(feature = "private-cookies")] mod private_cookies {
extern crate rocket; use rocket::http::CookieJar;
#[cfg(feature = "private-cookies")] #[rocket::get("/")]
mod private_cookie_test { fn return_private_cookie(cookies: &CookieJar<'_>) -> Option<String> {
use rocket::http::Cookies;
#[get("/")]
fn return_private_cookie(mut cookies: Cookies) -> Option<String> {
match cookies.get_private("cookie_name") { match cookies.get_private("cookie_name") {
Some(cookie) => Some(cookie.value().into()), Some(cookie) => Some(cookie.value().into()),
None => None, None => None,
@ -16,9 +12,9 @@ mod private_cookie_test {
mod tests { mod tests {
use super::*; use super::*;
use rocket::routes;
use rocket::local::blocking::Client; use rocket::local::blocking::Client;
use rocket::http::Cookie; use rocket::http::{Cookie, Status};
use rocket::http::Status;
#[test] #[test]
fn private_cookie_is_returned() { fn private_cookie_is_returned() {

View File

@ -0,0 +1,49 @@
#[macro_use] extern crate rocket;
use rocket::http::{Cookie, CookieJar};
#[post("/")]
fn multi_add(jar_a: &CookieJar<'_>, jar_b: &CookieJar<'_>) {
jar_a.add(Cookie::new("a", "v1"));
jar_b.add(Cookie::new("b", "v2"));
}
#[get("/")]
fn multi_get(jar_a: &CookieJar<'_>, jar_b: &CookieJar<'_>, jar_c: &CookieJar<'_>) -> String {
let (a, a2, a3) = (jar_a.get("a"), jar_b.get("a"), jar_c.get("a"));
let (b, b2, b3) = (jar_a.get("b"), jar_b.get("b"), jar_c.get("b"));
assert_eq!(a, a2); assert_eq!(a2, a3);
assert_eq!(b, b2); assert_eq!(b2, b3);
format!("{}{}", a.unwrap().value(), b.unwrap().value())
}
#[cfg(test)]
mod many_cookie_jars_tests {
use super::*;
use rocket::local::blocking::Client;
fn rocket() -> rocket::Rocket {
rocket::ignite().mount("/", routes![multi_add, multi_get])
}
#[test]
fn test_mutli_add() {
let client = Client::new(rocket()).unwrap();
let response = client.post("/").dispatch();
let cookies = response.cookies();
assert_eq!(cookies.iter().count(), 2);
assert_eq!(cookies.get("a").unwrap().value(), "v1");
assert_eq!(cookies.get("b").unwrap().value(), "v2");
}
#[test]
fn test_mutli_get() {
let client = Client::new(rocket()).unwrap();
let response = client.get("/")
.cookie(Cookie::new("a", "a_val"))
.cookie(Cookie::new("b", "hi!"))
.dispatch();
assert_eq!(response.into_string().unwrap(), "a_valhi!");
}
}

View File

@ -7,7 +7,7 @@ use std::collections::HashMap;
use rocket::request::Form; use rocket::request::Form;
use rocket::response::Redirect; use rocket::response::Redirect;
use rocket::http::{Cookie, Cookies}; use rocket::http::{Cookie, CookieJar};
use rocket_contrib::templates::Template; use rocket_contrib::templates::Template;
#[derive(FromForm)] #[derive(FromForm)]
@ -16,13 +16,13 @@ struct Message {
} }
#[post("/submit", data = "<message>")] #[post("/submit", data = "<message>")]
fn submit(mut cookies: Cookies<'_>, message: Form<Message>) -> Redirect { fn submit(cookies: &CookieJar<'_>, 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: &CookieJar<'_>) -> Template {
let cookie = cookies.get("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 {

View File

@ -14,11 +14,12 @@ fn test_submit() {
.dispatch(); .dispatch();
let cookie_headers: Vec<_> = response.headers().get("Set-Cookie").collect(); let cookie_headers: Vec<_> = response.headers().get("Set-Cookie").collect();
let location_headers: Vec<_> = response.headers().get("Location").collect(); assert_eq!(cookie_headers.len(), 1);
assert!(cookie_headers[0].starts_with("message=Hello%20from%20Rocket!"));
assert_eq!(response.status(), Status::SeeOther); let location_headers: Vec<_> = response.headers().get("Location").collect();
assert_eq!(cookie_headers, vec!["message=Hello%20from%20Rocket!".to_string()]);
assert_eq!(location_headers, vec!["/".to_string()]); assert_eq!(location_headers, vec!["/".to_string()]);
assert_eq!(response.status(), Status::SeeOther);
} }
fn test_body(optional_cookie: Option<Cookie<'static>>, expected_body: String) { fn test_body(optional_cookie: Option<Cookie<'static>>, expected_body: String) {

View File

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

View File

@ -7,7 +7,7 @@ use std::collections::HashMap;
use rocket::outcome::IntoOutcome; 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, CookieJar};
use rocket_contrib::templates::Template; use rocket_contrib::templates::Template;
#[derive(FromForm)] #[derive(FromForm)]
@ -33,7 +33,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for User {
} }
#[post("/login", data = "<login>")] #[post("/login", data = "<login>")]
fn login(mut cookies: Cookies<'_>, login: Form<Login>) -> Result<Redirect, Flash<Redirect>> { fn login(cookies: &CookieJar<'_>, login: Form<Login>) -> Result<Redirect, Flash<Redirect>> {
if login.username == "Sergio" && login.password == "password" { if login.username == "Sergio" && login.password == "password" {
cookies.add_private(Cookie::new("user_id", 1.to_string())); cookies.add_private(Cookie::new("user_id", 1.to_string()));
Ok(Redirect::to(uri!(index))) Ok(Redirect::to(uri!(index)))
@ -43,7 +43,7 @@ fn login(mut cookies: Cookies<'_>, login: Form<Login>) -> Result<Redirect, Flash
} }
#[post("/logout")] #[post("/logout")]
fn logout(mut cookies: Cookies<'_>) -> Flash<Redirect> { fn logout(cookies: &CookieJar<'_>) -> Flash<Redirect> {
cookies.remove_private(Cookie::named("user_id")); cookies.remove_private(Cookie::named("user_id"));
Flash::success(Redirect::to(uri!(login_page)), "Successfully logged out.") Flash::success(Redirect::to(uri!(login_page)), "Successfully logged out.")
} }

View File

@ -106,7 +106,7 @@ if [ "$1" = "--contrib" ]; then
popd > /dev/null 2>&1 popd > /dev/null 2>&1
elif [ "$1" = "--core" ]; then elif [ "$1" = "--core" ]; then
FEATURES=( FEATURES=(
private-cookies # this is already tested since it's the default feature secrets
tls tls
) )
@ -117,7 +117,6 @@ elif [ "$1" = "--core" ]; then
for feature in "${FEATURES[@]}"; do for feature in "${FEATURES[@]}"; do
echo ":: Building and testing core [${feature}]..." echo ":: Building and testing core [${feature}]..."
$CARGO test --no-default-features --features "${feature}" $CARGO test --no-default-features --features "${feature}"
done done

View File

@ -438,7 +438,7 @@ more about request guards and implementing them, see the [`FromRequest`]
documentation. documentation.
[`FromRequest`]: @api/rocket/request/trait.FromRequest.html [`FromRequest`]: @api/rocket/request/trait.FromRequest.html
[`Cookies`]: @api/rocket/http/enum.Cookies.html [`CookieJar`]: @api/rocket/http/struct.CookieJar.html
### Custom Guards ### Custom Guards
@ -568,34 +568,33 @@ it always succeeds. The user is redirected to a log in page.
## Cookies ## Cookies
[`Cookies`] is an important, built-in request guard: it allows you to get, set, A reference to a [`CookieJar`] is an important, built-in request guard: it
and remove cookies. Because `Cookies` is a request guard, an argument of its allows you to get, set, and remove cookies. Because `&CookieJar` is a request
type can simply be added to a handler: guard, an argument of its type can simply be added to a handler:
```rust ```rust
# #[macro_use] extern crate rocket; # #[macro_use] extern crate rocket;
# fn main() {} # fn main() {}
use rocket::http::Cookies; use rocket::http::CookieJar;
#[get("/")] #[get("/")]
fn index(cookies: Cookies) -> Option<String> { fn index(cookies: &CookieJar<'_>) -> Option<String> {
cookies.get("message") cookies.get("message").map(|crumb| format!("Message: {}", crumb.value()))
.map(|value| format!("Message: {}", value))
} }
``` ```
This results in the incoming request's cookies being accessible from the This results in the incoming request's cookies being accessible from the
handler. The example above retrieves a cookie named `message`. Cookies can also handler. The example above retrieves a cookie named `message`. Cookies can also
be set and removed using the `Cookies` guard. The [cookies example] on GitHub be set and removed using the `CookieJar` guard. The [cookies example] on GitHub
illustrates further use of the `Cookies` type to get and set cookies, while the illustrates further use of the `CookieJar` type to get and set cookies, while
[`Cookies`] documentation contains complete usage information. the [`CookieJar`] documentation contains complete usage information.
[cookies example]: @example/cookies [cookies example]: @example/cookies
### Private Cookies ### Private Cookies
Cookies added via the [`Cookies::add()`] method are set _in the clear._ In other Cookies added via the [`CookieJar::add()`] method are set _in the clear._ In
words, the value set is visible by the client. For sensitive data, Rocket other words, the value set is visible to the client. For sensitive data, Rocket
provides _private_ cookies. provides _private_ cookies.
Private cookies are just like regular cookies except that they are encrypted Private cookies are just like regular cookies except that they are encrypted
@ -612,37 +611,25 @@ methods are suffixed with `_private`. These methods are: [`get_private`],
# #[macro_use] extern crate rocket; # #[macro_use] extern crate rocket;
# fn main() {} # fn main() {}
use rocket::http::{Cookie, Cookies}; use rocket::http::{Cookie, CookieJar};
use rocket::response::{Flash, Redirect}; use rocket::response::{Flash, Redirect};
/// Retrieve the user's ID, if any. /// Retrieve the user's ID, if any.
#[get("/user_id")] #[get("/user_id")]
fn user_id(mut cookies: Cookies) -> Option<String> { fn user_id(cookies: &CookieJar<'_>) -> Option<String> {
cookies.get_private("user_id") cookies.get_private("user_id")
.map(|cookie| format!("User ID: {}", cookie.value())) .map(|crumb| format!("User ID: {}", crumb.value()))
} }
/// Remove the `user_id` cookie. /// Remove the `user_id` cookie.
#[post("/logout")] #[post("/logout")]
fn logout(mut cookies: Cookies) -> Flash<Redirect> { fn logout(cookies: &CookieJar<'_>) -> Flash<Redirect> {
cookies.remove_private(Cookie::named("user_id")); cookies.remove_private(Cookie::named("user_id"));
Flash::success(Redirect::to("/"), "Successfully logged out.") Flash::success(Redirect::to("/"), "Successfully logged out.")
} }
``` ```
[`Cookies::add()`]: @api/rocket/http/enum.Cookies.html#method.add [`CookieJar::add()`]: @api/rocket/http/struct.CookieJar.html#method.add
Support for private cookies, which depends on the [`ring`] library, can be
omitted at build time by disabling Rocket's default features, in-turn disabling
the default `private-cookies` feature. To do so, modify your `Cargo.toml` file
so that you depend on `rocket` as follows:
```toml
[dependencies]
rocket = { version = "0.5.0-dev", default-features = false }
```
[`ring`]: https://github.com/briansmith/ring
### Secret Key ### Secret Key
@ -662,81 +649,9 @@ can be generated with the command `openssl rand -base64 32`.
For more information on configuration, see the [Configuration](../configuration) For more information on configuration, see the [Configuration](../configuration)
section of the guide. section of the guide.
[`get_private`]: @api/rocket/http/enum.Cookies.html#method.get_private [`get_private`]: @api/rocket/http/struct.CookieJar.html#method.get_private
[`add_private`]: @api/rocket/http/enum.Cookies.html#method.add_private [`add_private`]: @api/rocket/http/struct.CookieJar.html#method.add_private
[`remove_private`]: @api/rocket/http/enum.Cookies.html#method.remove_private [`remove_private`]: @api/rocket/http/struct.CookieJar.html#method.remove_private
### One-At-A-Time
For safety reasons, Rocket currently requires that at most one `Cookies`
instance be active at a time. It's uncommon to run into this restriction, but it
can be confusing to handle if it does crop up.
If this does happen, Rocket will emit messages to the console that look as
follows:
```text
=> Error: Multiple `Cookies` instances are active at once.
=> An instance of `Cookies` must be dropped before another can be retrieved.
=> Warning: The retrieved `Cookies` instance will be empty.
```
The messages will be emitted when a violating handler is called. The issue can
be resolved by ensuring that two instances of `Cookies` cannot be active at once
due to the offending handler. A common error is to have a handler that uses a
`Cookies` request guard as well as a `Custom` request guard that retrieves
`Cookies`, as so:
```rust
# #[macro_use] extern crate rocket;
# fn main() {}
# use rocket::http::Cookies;
# type Custom = rocket::http::Method;
#[get("/")]
fn bad(cookies: Cookies, custom: Custom) { /* .. */ }
```
Because the `cookies` guard will fire before the `custom` guard, the `custom`
guard will retrieve an instance of `Cookies` when one already exists for
`cookies`. This scenario can be fixed by simply swapping the order of the
guards:
```rust
# #[macro_use] extern crate rocket;
# fn main() {}
# use rocket::http::Cookies;
# type Custom = rocket::http::Method;
#[get("/")]
fn good(custom: Custom, cookies: Cookies) { /* .. */ }
```
When using request guards that modify cookies on-demand, such as
`FlashMessage`, a similar problem occurs. The fix in this case is to `drop` the
`Cookies` instance before accessing the `FlashMessage`.
```rust
# #[macro_use] extern crate rocket;
# fn main() {}
# use rocket::http::Cookies;
use rocket::request::FlashMessage;
#[get("/")]
fn bad(cookies: Cookies, flash: FlashMessage) {
// Oh no! `flash` holds a reference to `Cookies` too!
let msg = flash.msg();
}
#[get("/")]
fn good(cookies: Cookies, flash: FlashMessage) {
std::mem::drop(cookies);
// Now, `flash` holds an _exclusive_ reference to `Cookies`. Whew.
let msg = flash.msg();
}
```
## Format ## Format