From 98c9ce9ccd1c7a805d2949e7a2011c38acf88357 Mon Sep 17 00:00:00 2001 From: Nikolai Hellwig Date: Sat, 21 Dec 2019 13:52:10 +0100 Subject: [PATCH 01/11] Ported Cookie implementation from tide-split Tests are still missing as well as cleanup --- Cargo.toml | 4 +++- src/middleware/mod.rs | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 88258fb62..514e908f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,8 @@ rustdoc-args = ["--cfg", "feature=\"docs\""] [features] default = ["hyper-server"] hyper-server = ["http-service-hyper"] -docs = ["unstable"] +docs = ["unstable", "cookies"] +cookies = ["cookie"] unstable = [] [dependencies] @@ -41,6 +42,7 @@ serde_qs = "0.5.0" async-std = { version = "1.0.1", features = ["unstable"] } pin-project-lite = "0.1.0" mime = "0.3.14" +cookie = { version="0.12.0", features = ["percent-encode"], optional = true } [dev-dependencies] async-std = { version = "1.0.1", features = ["unstable", "attributes"] } diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 41562c983..c41cfed20 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -10,17 +10,20 @@ use crate::utils::BoxFuture; use crate::{Request, Response}; // mod compression; -// mod cookies; +#[cfg(feature = "cookies")] +mod cookies; // mod cors; // mod default_headers; mod logger; // pub use compression::{Compression, Decompression}; -// pub use cookies::CookiesMiddleware; // pub use cors::{Cors, Origin}; // pub use default_headers::DefaultHeaders; pub use logger::RequestLogger; +#[cfg(feature = "cookies")] +pub use cookies::CookiesMiddleware; + /// Middleware that wraps around remaining middleware chain. pub trait Middleware: 'static + Send + Sync { /// Asynchronously handle the request, and return a response. From f322a4c119efb0c212049df5b60a297ff8925149 Mon Sep 17 00:00:00 2001 From: Nikolai Hellwig Date: Sat, 21 Dec 2019 20:51:08 +0100 Subject: [PATCH 02/11] Adding Tests for Cookies --- src/middleware/cookies.rs | 269 ++++++++++++++++++++++++++++++++++++++ src/response/mod.rs | 7 + 2 files changed, 276 insertions(+) create mode 100644 src/middleware/cookies.rs diff --git a/src/middleware/cookies.rs b/src/middleware/cookies.rs new file mode 100644 index 000000000..dd834d061 --- /dev/null +++ b/src/middleware/cookies.rs @@ -0,0 +1,269 @@ +use crate::{ + middleware::{Middleware, Next}, + Request, Response, +}; +use cookie::{Cookie, CookieJar, ParseError}; +use futures::future::BoxFuture; + +use crate::Error; +use http::{status::StatusCode, HeaderMap}; +use std::sync::{Arc, RwLock}; + +const MIDDLEWARE_MISSING_MSG: &str = + "CookiesMiddleware must be used to populate request and response cookies"; + +/// A simple requests logger +/// +/// # Examples +/// +/// ```rust +/// +/// let mut app = tide::Server::new(); +/// app.middleware(tide::middleware::CookiesMiddleware::new()); +/// ``` +#[derive(Debug, Clone, Default)] +pub struct CookiesMiddleware; + +impl CookiesMiddleware { + pub fn new() -> Self { + Self::default() + } + + async fn handle_cookie<'a, State: Send + Sync + 'static>( + &'a self, + ctx: Request, + next: Next<'a, State>, + ) -> Response { + let mut ctx = ctx; + + let cookie_jar = if let Some(cookie_data) = ctx.local::() { + cookie_data.content.clone() + } else { + let cookie_data = CookieData::from_headers(ctx.headers()); + let cloned_content = cookie_data.content.clone(); // TODO: hmm does look ugly but needed because of moved cookie_data + ctx = ctx.set_local(cookie_data); + cloned_content + }; + + let mut res = next.run(ctx).await; + + for cookie in cookie_jar.read().unwrap().delta() { + res = res.append_header( + http::header::SET_COOKIE.as_ref(), + cookie.encoded().to_string(), + ); + } + res + } +} + +impl Middleware for CookiesMiddleware { + fn handle<'a>(&'a self, ctx: Request, next: Next<'a, State>) -> BoxFuture<'a, Response> { + Box::pin(async move { self.handle_cookie(ctx, next).await }) + } +} + +#[derive(Debug, Clone)] +pub(crate) struct CookieData { + pub(crate) content: Arc>, +} + +impl CookieData { + pub fn from_headers(headers: &HeaderMap) -> Self { + CookieData { + content: Arc::new(RwLock::new( + headers + .get(http::header::COOKIE) + .and_then(|raw| parse_from_header(raw.to_str().unwrap()).ok()) + .unwrap_or_default(), + )), + } + } +} + +fn parse_from_header(s: &str) -> Result { + let mut jar = CookieJar::new(); + + s.split(';').try_for_each(|s| -> Result<_, ParseError> { + jar.add_original(Cookie::parse(s.trim().to_owned())?); + Ok(()) + })?; + + Ok(jar) +} + +/// An extension to `Context` that provides cached access to cookies +pub trait RequestExt { + /// returns a `Cookie` by name of the cookie + fn get_cookie(&self, name: &str) -> Result>, Error>; + + /// Add cookie to the cookie jar + fn set_cookie(&mut self, cookie: Cookie<'static>) -> Result<(), Error>; + + /// Removes the cookie. This instructs the `CookiesMiddleware` to send a cookie with empty value + /// in the response. + fn remove_cookie(&mut self, cookie: Cookie<'static>) -> Result<(), Error>; +} + +impl RequestExt for Request { + fn get_cookie(&self, name: &str) -> Result>, Error> { + let cookie_data = self + .local::() + .ok_or_else(|| Error::from(StatusCode::INTERNAL_SERVER_ERROR))?; + + let locked_jar = cookie_data + .content + .read() + .map_err(|e| Error::from(StatusCode::INTERNAL_SERVER_ERROR))?; // better mapping here + // .map_err(|e| StringError(format!("Failed to get write lock: {}", e)))?; + Ok(locked_jar.get(name).cloned()) + } + + fn set_cookie(&mut self, cookie: Cookie<'static>) -> Result<(), Error> { + let cookie_data = self + .local::() + .ok_or_else(|| Error::from(StatusCode::INTERNAL_SERVER_ERROR))?; + + let mut locked_jar = cookie_data + .content + .write() + .map_err(|e| Error::from(StatusCode::INTERNAL_SERVER_ERROR))?; // better mapping here + locked_jar.add(cookie); + Ok(()) + } + + fn remove_cookie(&mut self, cookie: Cookie<'static>) -> Result<(), Error> { + let cookie_data = self + .local::() + .ok_or_else(|| Error::from(StatusCode::INTERNAL_SERVER_ERROR))?; + + let mut locked_jar = cookie_data + .content + .write() + .map_err(|e| Error::from(StatusCode::INTERNAL_SERVER_ERROR))?; // better mapping here + locked_jar.remove(cookie); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cookie::Cookie; + use futures::executor::block_on; + use futures::AsyncReadExt; + use http_service::Body; + use http_service_mock::make_server; + + static COOKIE_NAME: &str = "testCookie"; + + /// Tide will use the the `Cookies`'s `Extract` implementation to build this parameter. + async fn retrieve_cookie(cx: Request<()>) -> String { + cx.get_cookie(COOKIE_NAME) + .unwrap() + .unwrap() + .value() + .to_string() + } + + async fn set_cookie(mut cx: Request<()>) -> Response { + cx.set_cookie(Cookie::new(COOKIE_NAME, "NewCookieValue")) + .unwrap(); + Response::new(200) + } + + async fn remove_cookie(mut cx: Request<()>) -> Response { + cx.remove_cookie(Cookie::named(COOKIE_NAME)).unwrap(); + Response::new(200) + } + + async fn set_multiple_cookie(mut cx: Request<()>) -> Response { + cx.set_cookie(Cookie::new("C1", "V1")).unwrap(); + cx.set_cookie(Cookie::new("C2", "V2")).unwrap(); + Response::new(200) + } + + fn app() -> crate::Server<()> { + let mut app = crate::new(); + app.middleware(CookiesMiddleware::new()); + + app.at("/get").get(retrieve_cookie); + app.at("/set").get(set_cookie); + app.at("/remove").get(remove_cookie); + app.at("/multi").get(set_multiple_cookie); + app + } + + fn make_request(endpoint: &str) -> http::response::Response { + let app = app(); + let mut server = make_server(app.into_http_service()).unwrap(); + let req = http::Request::get(endpoint) + .header(http::header::COOKIE, "testCookie=RequestCookieValue") + .body(Body::empty()) + .unwrap(); + server.simulate(req).unwrap() + } + + #[test] + fn successfully_retrieve_request_cookie() { + let mut res = make_request("/get"); + assert_eq!(res.status(), 200); + + let body = block_on(async move { + let mut buffer = Vec::new(); // init the buffer to read the data into + res.body_mut().read_to_end(&mut buffer).await.unwrap(); + buffer + }); + + assert_eq!(&*body, &*b"RequestCookieValue"); + } + + #[test] + fn successfully_set_cookie() { + let res = make_request("/set"); + assert_eq!(res.status(), 200); + let test_cookie_header = res.headers().get(http::header::SET_COOKIE).unwrap(); + assert_eq!( + test_cookie_header.to_str().unwrap(), + "testCookie=NewCookieValue" + ); + } + + #[test] + fn successfully_remove_cookie() { + let res = make_request("/remove"); + assert_eq!(res.status(), 200); + let test_cookie_header = res.headers().get(http::header::SET_COOKIE).unwrap(); + assert!(test_cookie_header + .to_str() + .unwrap() + .starts_with("testCookie=;")); + let cookie = Cookie::parse_encoded(test_cookie_header.to_str().unwrap()).unwrap(); + assert_eq!(cookie.name(), COOKIE_NAME); + assert_eq!(cookie.value(), ""); + assert_eq!(cookie.http_only(), None); + assert_eq!(cookie.max_age().unwrap().num_nanoseconds(), Some(0)); + } + + #[test] + fn successfully_set_multiple_cookies() { + let res = make_request("/multi"); + assert_eq!(res.status(), 200); + let cookie_header = res.headers().get_all(http::header::SET_COOKIE); + let mut iter = cookie_header.iter(); + + let cookie1 = iter.next().unwrap(); + let cookie2 = iter.next().unwrap(); + + //Headers can be out of order + if cookie1.to_str().unwrap().starts_with("C1") { + assert_eq!(cookie1, "C1=V1"); + assert_eq!(cookie2, "C2=V2"); + } else { + assert_eq!(cookie2, "C1=V1"); + assert_eq!(cookie1, "C2=V2"); + } + + assert!(iter.next().is_none()); + } +} diff --git a/src/response/mod.rs b/src/response/mod.rs index 04415a92c..c83174870 100644 --- a/src/response/mod.rs +++ b/src/response/mod.rs @@ -57,6 +57,13 @@ impl Response { self } + /// Append an HTTP header. + pub fn append_header(mut self, key: &'static str, value: impl AsRef) -> Self { + let value = value.as_ref().to_owned(); + self.res.headers_mut().append(key, value.parse().unwrap()); + self + } + /// Set the request MIME. /// /// [Read more on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) From 6b9200fdf2c018cf59d2edf521563ddcd5ca8324 Mon Sep 17 00:00:00 2001 From: Nikolai Hellwig Date: Sun, 22 Dec 2019 11:24:13 +0100 Subject: [PATCH 03/11] Proper Error Handling --- src/error.rs | 14 ++++++++++++++ src/middleware/cookies.rs | 25 +++++++++++++++---------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/error.rs b/src/error.rs index 33a9c19ce..347ff73c7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -35,6 +35,20 @@ impl From for Error { } } +/// A simple error type that wraps a String +#[derive(Debug)] +pub struct StringError(pub String); +impl std::error::Error for StringError {} + +impl std::fmt::Display for StringError { + fn fmt( + &self, + formatter: &mut std::fmt::Formatter<'_>, + ) -> std::result::Result<(), std::fmt::Error> { + self.0.fmt(formatter) + } +} + /// Extension methods for `Result`. pub trait ResultExt: Sized { /// Convert to an `Result`, treating the `Err` case as a client diff --git a/src/middleware/cookies.rs b/src/middleware/cookies.rs index dd834d061..da25d1edd 100644 --- a/src/middleware/cookies.rs +++ b/src/middleware/cookies.rs @@ -5,7 +5,7 @@ use crate::{ use cookie::{Cookie, CookieJar, ParseError}; use futures::future::BoxFuture; -use crate::Error; +use crate::{error::ResultExt, error::StringError, Error}; use http::{status::StatusCode, HeaderMap}; use std::sync::{Arc, RwLock}; @@ -109,25 +109,29 @@ impl RequestExt for Request { fn get_cookie(&self, name: &str) -> Result>, Error> { let cookie_data = self .local::() - .ok_or_else(|| Error::from(StatusCode::INTERNAL_SERVER_ERROR))?; + .ok_or_else(|| StringError(MIDDLEWARE_MISSING_MSG.to_owned())) + .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; let locked_jar = cookie_data .content .read() - .map_err(|e| Error::from(StatusCode::INTERNAL_SERVER_ERROR))?; // better mapping here - // .map_err(|e| StringError(format!("Failed to get write lock: {}", e)))?; + .map_err(|e| StringError(format!("Failed to get read lock: {}", e))) + .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(locked_jar.get(name).cloned()) } fn set_cookie(&mut self, cookie: Cookie<'static>) -> Result<(), Error> { let cookie_data = self .local::() - .ok_or_else(|| Error::from(StatusCode::INTERNAL_SERVER_ERROR))?; + .ok_or_else(|| StringError(MIDDLEWARE_MISSING_MSG.to_owned())) + .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; let mut locked_jar = cookie_data .content .write() - .map_err(|e| Error::from(StatusCode::INTERNAL_SERVER_ERROR))?; // better mapping here + .map_err(|e| StringError(format!("Failed to get read lock: {}", e))) + .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; locked_jar.add(cookie); Ok(()) } @@ -135,12 +139,14 @@ impl RequestExt for Request { fn remove_cookie(&mut self, cookie: Cookie<'static>) -> Result<(), Error> { let cookie_data = self .local::() - .ok_or_else(|| Error::from(StatusCode::INTERNAL_SERVER_ERROR))?; + .ok_or_else(|| StringError(MIDDLEWARE_MISSING_MSG.to_owned())) + .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; let mut locked_jar = cookie_data .content .write() - .map_err(|e| Error::from(StatusCode::INTERNAL_SERVER_ERROR))?; // better mapping here + .map_err(|e| StringError(format!("Failed to get read lock: {}", e))) + .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; locked_jar.remove(cookie); Ok(()) } @@ -157,7 +163,6 @@ mod tests { static COOKIE_NAME: &str = "testCookie"; - /// Tide will use the the `Cookies`'s `Extract` implementation to build this parameter. async fn retrieve_cookie(cx: Request<()>) -> String { cx.get_cookie(COOKIE_NAME) .unwrap() @@ -210,7 +215,7 @@ mod tests { assert_eq!(res.status(), 200); let body = block_on(async move { - let mut buffer = Vec::new(); // init the buffer to read the data into + let mut buffer = Vec::new(); res.body_mut().read_to_end(&mut buffer).await.unwrap(); buffer }); From 629f92ffeeb82352a82b6486430ac78d5fcf78c5 Mon Sep 17 00:00:00 2001 From: Nikolai Hellwig Date: Sun, 22 Dec 2019 11:27:54 +0100 Subject: [PATCH 04/11] Cleanup --- src/middleware/cookies.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/middleware/cookies.rs b/src/middleware/cookies.rs index da25d1edd..8343893aa 100644 --- a/src/middleware/cookies.rs +++ b/src/middleware/cookies.rs @@ -40,9 +40,9 @@ impl CookiesMiddleware { cookie_data.content.clone() } else { let cookie_data = CookieData::from_headers(ctx.headers()); - let cloned_content = cookie_data.content.clone(); // TODO: hmm does look ugly but needed because of moved cookie_data + let content = cookie_data.content.clone(); ctx = ctx.set_local(cookie_data); - cloned_content + content }; let mut res = next.run(ctx).await; From 39da9b34a46f5f97191af94e9f7d061c262b6215 Mon Sep 17 00:00:00 2001 From: Nikolai Hellwig Date: Fri, 17 Jan 2020 12:42:45 +0100 Subject: [PATCH 05/11] Implementing suggestions from Pull-Request --- Cargo.toml | 5 +- backup/examples/cookies.rs | 26 ----- examples/cookies.rs | 33 +++++++ src/middleware/cookies.rs | 191 +------------------------------------ src/middleware/mod.rs | 4 +- src/request.rs | 58 ++++++++++- tests/cookies.rs | 118 +++++++++++++++++++++++ 7 files changed, 214 insertions(+), 221 deletions(-) delete mode 100644 backup/examples/cookies.rs create mode 100644 examples/cookies.rs create mode 100644 tests/cookies.rs diff --git a/Cargo.toml b/Cargo.toml index 514e908f2..c0c47aecf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,8 +25,7 @@ rustdoc-args = ["--cfg", "feature=\"docs\""] [features] default = ["hyper-server"] hyper-server = ["http-service-hyper"] -docs = ["unstable", "cookies"] -cookies = ["cookie"] +docs = ["unstable"] unstable = [] [dependencies] @@ -42,7 +41,7 @@ serde_qs = "0.5.0" async-std = { version = "1.0.1", features = ["unstable"] } pin-project-lite = "0.1.0" mime = "0.3.14" -cookie = { version="0.12.0", features = ["percent-encode"], optional = true } +cookie = { version="0.12.0", features = ["percent-encode"]} [dev-dependencies] async-std = { version = "1.0.1", features = ["unstable", "attributes"] } diff --git a/backup/examples/cookies.rs b/backup/examples/cookies.rs deleted file mode 100644 index 4ad1bed43..000000000 --- a/backup/examples/cookies.rs +++ /dev/null @@ -1,26 +0,0 @@ -use cookie::Cookie; -use tide::{cookies::RequestExt, middleware::CookiesMiddleware, Request}; - -/// Tide will use the the `Cookies`'s `Extract` implementation to build this parameter. -/// -async fn retrieve_cookie(cx: Request<()>) -> String { - format!("hello cookies: {:?}", cx.get_cookie("hello").unwrap()) -} - -async fn set_cookie(mut cx: Request<()>) { - cx.set_cookie(Cookie::new("hello", "world")).unwrap(); -} - -async fn remove_cookie(mut cx: Request<()>) { - cx.remove_cookie(Cookie::named("hello")).unwrap(); -} - -fn main() { - let mut app = tide::Server::new(); - app.middleware(CookiesMiddleware::new()); - - app.at("/").get(retrieve_cookie); - app.at("/set").get(set_cookie); - app.at("/remove").get(remove_cookie); - app.run("127.0.0.1:8000").unwrap(); -} diff --git a/examples/cookies.rs b/examples/cookies.rs new file mode 100644 index 000000000..5f6276fc1 --- /dev/null +++ b/examples/cookies.rs @@ -0,0 +1,33 @@ +use async_std::task; +use cookie::Cookie; +use tide::{middleware::CookiesMiddleware, Request, Response}; + +/// Tide will use the the `Cookies`'s `Extract` implementation to build this parameter. +/// +async fn retrieve_cookie(cx: Request<()>) -> String { + format!("hello cookies: {:?}", cx.get_cookie("hello").unwrap()) +} + +async fn set_cookie(mut cx: Request<()>) -> Response { + cx.set_cookie(Cookie::new("hello", "world")).unwrap(); + tide::Response::new(200) +} + +async fn remove_cookie(mut cx: Request<()>) -> Response { + cx.remove_cookie(Cookie::named("hello")).unwrap(); + tide::Response::new(200) +} + +fn main() -> Result<(), std::io::Error> { + task::block_on(async { + let mut app = tide::new(); + app.middleware(CookiesMiddleware::new()); + + app.at("/").get(retrieve_cookie); + app.at("/set").get(set_cookie); + app.at("/remove").get(remove_cookie); + app.listen("127.0.0.1:8080").await?; + + Ok(()) + }) +} diff --git a/src/middleware/cookies.rs b/src/middleware/cookies.rs index 8343893aa..d4b9a962f 100644 --- a/src/middleware/cookies.rs +++ b/src/middleware/cookies.rs @@ -5,14 +5,10 @@ use crate::{ use cookie::{Cookie, CookieJar, ParseError}; use futures::future::BoxFuture; -use crate::{error::ResultExt, error::StringError, Error}; -use http::{status::StatusCode, HeaderMap}; +use http::HeaderMap; use std::sync::{Arc, RwLock}; -const MIDDLEWARE_MISSING_MSG: &str = - "CookiesMiddleware must be used to populate request and response cookies"; - -/// A simple requests logger +/// A middleware for making cookie data available in requests. /// /// # Examples /// @@ -69,7 +65,7 @@ pub(crate) struct CookieData { } impl CookieData { - pub fn from_headers(headers: &HeaderMap) -> Self { + pub(crate) fn from_headers(headers: &HeaderMap) -> Self { CookieData { content: Arc::new(RwLock::new( headers @@ -91,184 +87,3 @@ fn parse_from_header(s: &str) -> Result { Ok(jar) } - -/// An extension to `Context` that provides cached access to cookies -pub trait RequestExt { - /// returns a `Cookie` by name of the cookie - fn get_cookie(&self, name: &str) -> Result>, Error>; - - /// Add cookie to the cookie jar - fn set_cookie(&mut self, cookie: Cookie<'static>) -> Result<(), Error>; - - /// Removes the cookie. This instructs the `CookiesMiddleware` to send a cookie with empty value - /// in the response. - fn remove_cookie(&mut self, cookie: Cookie<'static>) -> Result<(), Error>; -} - -impl RequestExt for Request { - fn get_cookie(&self, name: &str) -> Result>, Error> { - let cookie_data = self - .local::() - .ok_or_else(|| StringError(MIDDLEWARE_MISSING_MSG.to_owned())) - .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; - - let locked_jar = cookie_data - .content - .read() - .map_err(|e| StringError(format!("Failed to get read lock: {}", e))) - .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; - - Ok(locked_jar.get(name).cloned()) - } - - fn set_cookie(&mut self, cookie: Cookie<'static>) -> Result<(), Error> { - let cookie_data = self - .local::() - .ok_or_else(|| StringError(MIDDLEWARE_MISSING_MSG.to_owned())) - .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; - - let mut locked_jar = cookie_data - .content - .write() - .map_err(|e| StringError(format!("Failed to get read lock: {}", e))) - .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; - locked_jar.add(cookie); - Ok(()) - } - - fn remove_cookie(&mut self, cookie: Cookie<'static>) -> Result<(), Error> { - let cookie_data = self - .local::() - .ok_or_else(|| StringError(MIDDLEWARE_MISSING_MSG.to_owned())) - .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; - - let mut locked_jar = cookie_data - .content - .write() - .map_err(|e| StringError(format!("Failed to get read lock: {}", e))) - .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; - locked_jar.remove(cookie); - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use cookie::Cookie; - use futures::executor::block_on; - use futures::AsyncReadExt; - use http_service::Body; - use http_service_mock::make_server; - - static COOKIE_NAME: &str = "testCookie"; - - async fn retrieve_cookie(cx: Request<()>) -> String { - cx.get_cookie(COOKIE_NAME) - .unwrap() - .unwrap() - .value() - .to_string() - } - - async fn set_cookie(mut cx: Request<()>) -> Response { - cx.set_cookie(Cookie::new(COOKIE_NAME, "NewCookieValue")) - .unwrap(); - Response::new(200) - } - - async fn remove_cookie(mut cx: Request<()>) -> Response { - cx.remove_cookie(Cookie::named(COOKIE_NAME)).unwrap(); - Response::new(200) - } - - async fn set_multiple_cookie(mut cx: Request<()>) -> Response { - cx.set_cookie(Cookie::new("C1", "V1")).unwrap(); - cx.set_cookie(Cookie::new("C2", "V2")).unwrap(); - Response::new(200) - } - - fn app() -> crate::Server<()> { - let mut app = crate::new(); - app.middleware(CookiesMiddleware::new()); - - app.at("/get").get(retrieve_cookie); - app.at("/set").get(set_cookie); - app.at("/remove").get(remove_cookie); - app.at("/multi").get(set_multiple_cookie); - app - } - - fn make_request(endpoint: &str) -> http::response::Response { - let app = app(); - let mut server = make_server(app.into_http_service()).unwrap(); - let req = http::Request::get(endpoint) - .header(http::header::COOKIE, "testCookie=RequestCookieValue") - .body(Body::empty()) - .unwrap(); - server.simulate(req).unwrap() - } - - #[test] - fn successfully_retrieve_request_cookie() { - let mut res = make_request("/get"); - assert_eq!(res.status(), 200); - - let body = block_on(async move { - let mut buffer = Vec::new(); - res.body_mut().read_to_end(&mut buffer).await.unwrap(); - buffer - }); - - assert_eq!(&*body, &*b"RequestCookieValue"); - } - - #[test] - fn successfully_set_cookie() { - let res = make_request("/set"); - assert_eq!(res.status(), 200); - let test_cookie_header = res.headers().get(http::header::SET_COOKIE).unwrap(); - assert_eq!( - test_cookie_header.to_str().unwrap(), - "testCookie=NewCookieValue" - ); - } - - #[test] - fn successfully_remove_cookie() { - let res = make_request("/remove"); - assert_eq!(res.status(), 200); - let test_cookie_header = res.headers().get(http::header::SET_COOKIE).unwrap(); - assert!(test_cookie_header - .to_str() - .unwrap() - .starts_with("testCookie=;")); - let cookie = Cookie::parse_encoded(test_cookie_header.to_str().unwrap()).unwrap(); - assert_eq!(cookie.name(), COOKIE_NAME); - assert_eq!(cookie.value(), ""); - assert_eq!(cookie.http_only(), None); - assert_eq!(cookie.max_age().unwrap().num_nanoseconds(), Some(0)); - } - - #[test] - fn successfully_set_multiple_cookies() { - let res = make_request("/multi"); - assert_eq!(res.status(), 200); - let cookie_header = res.headers().get_all(http::header::SET_COOKIE); - let mut iter = cookie_header.iter(); - - let cookie1 = iter.next().unwrap(); - let cookie2 = iter.next().unwrap(); - - //Headers can be out of order - if cookie1.to_str().unwrap().starts_with("C1") { - assert_eq!(cookie1, "C1=V1"); - assert_eq!(cookie2, "C2=V2"); - } else { - assert_eq!(cookie2, "C1=V1"); - assert_eq!(cookie1, "C2=V2"); - } - - assert!(iter.next().is_none()); - } -} diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index c41cfed20..67a0a03bf 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -10,8 +10,7 @@ use crate::utils::BoxFuture; use crate::{Request, Response}; // mod compression; -#[cfg(feature = "cookies")] -mod cookies; +pub(crate) mod cookies; // mod cors; // mod default_headers; mod logger; @@ -21,7 +20,6 @@ mod logger; // pub use default_headers::DefaultHeaders; pub use logger::RequestLogger; -#[cfg(feature = "cookies")] pub use cookies::CookiesMiddleware; /// Middleware that wraps around remaining middleware chain. diff --git a/src/request.rs b/src/request.rs index 4b04b6767..28f67d2c8 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,4 +1,5 @@ -use http::{HeaderMap, Method, Uri, Version}; +use cookie::Cookie; +use http::{status::StatusCode, HeaderMap, Method, Uri, Version}; use http_service::Body; use route_recognizer::Params; use serde::Deserialize; @@ -9,6 +10,12 @@ use async_std::task::{Context, Poll}; use std::pin::Pin; use std::{str::FromStr, sync::Arc}; +use crate::error::{Error, ResultExt, StringError}; +use crate::middleware::cookies::CookieData; + +const MIDDLEWARE_MISSING_MSG: &str = + "CookiesMiddleware must be used to populate request and response cookies"; + pin_project_lite::pin_project! { /// An HTTP request. /// @@ -289,6 +296,55 @@ impl Request { })?; Ok(res) } + + /// returns a `Cookie` by name of the cookie. + pub fn get_cookie(&self, name: &str) -> Result>, Error> { + let cookie_data = self + .local::() + .ok_or_else(|| StringError(MIDDLEWARE_MISSING_MSG.to_owned())) + .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; + + let locked_jar = cookie_data + .content + .read() + .map_err(|e| StringError(format!("Failed to get read lock: {}", e))) + .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(locked_jar.get(name).cloned()) + } + + /// Add cookie to the cookie jar. + pub fn set_cookie(&mut self, cookie: Cookie<'static>) -> Result<(), Error> { + let cookie_data = self + .local::() + .ok_or_else(|| StringError(MIDDLEWARE_MISSING_MSG.to_owned())) + .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; + + let mut locked_jar = cookie_data + .content + .write() + .map_err(|e| StringError(format!("Failed to get read lock: {}", e))) + .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; + locked_jar.add(cookie); + Ok(()) + } + + /// Removes the cookie. This instructs the `CookiesMiddleware` to send a cookie with empty value + /// in the response. + pub fn remove_cookie(&mut self, cookie: Cookie<'static>) -> Result<(), Error> { + let cookie_data = self + .local::() + .ok_or_else(|| StringError(MIDDLEWARE_MISSING_MSG.to_owned())) + .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; + + let mut locked_jar = cookie_data + .content + .write() + .map_err(|e| StringError(format!("Failed to get read lock: {}", e))) + .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; + locked_jar.remove(cookie); + Ok(()) + } } impl Read for Request { diff --git a/tests/cookies.rs b/tests/cookies.rs new file mode 100644 index 000000000..2d210a7dd --- /dev/null +++ b/tests/cookies.rs @@ -0,0 +1,118 @@ +use cookie::Cookie; +use futures::executor::block_on; +use futures::AsyncReadExt; +use http_service::Body; +use http_service_mock::make_server; + +use tide::{middleware::CookiesMiddleware, Request, Response, Server}; + +static COOKIE_NAME: &str = "testCookie"; + +async fn retrieve_cookie(cx: Request<()>) -> String { + cx.get_cookie(COOKIE_NAME) + .unwrap() + .unwrap() + .value() + .to_string() +} + +async fn set_cookie(mut cx: Request<()>) -> Response { + cx.set_cookie(Cookie::new(COOKIE_NAME, "NewCookieValue")) + .unwrap(); + Response::new(200) +} + +async fn remove_cookie(mut cx: Request<()>) -> Response { + cx.remove_cookie(Cookie::named(COOKIE_NAME)).unwrap(); + Response::new(200) +} + +async fn set_multiple_cookie(mut cx: Request<()>) -> Response { + cx.set_cookie(Cookie::new("C1", "V1")).unwrap(); + cx.set_cookie(Cookie::new("C2", "V2")).unwrap(); + Response::new(200) +} + +fn app() -> crate::Server<()> { + let mut app = tide::new(); + app.middleware(CookiesMiddleware::new()); + + app.at("/get").get(retrieve_cookie); + app.at("/set").get(set_cookie); + app.at("/remove").get(remove_cookie); + app.at("/multi").get(set_multiple_cookie); + app +} + +fn make_request(endpoint: &str) -> http::response::Response { + let app = app(); + let mut server = make_server(app.into_http_service()).unwrap(); + let req = http::Request::get(endpoint) + .header(http::header::COOKIE, "testCookie=RequestCookieValue") + .body(Body::empty()) + .unwrap(); + server.simulate(req).unwrap() +} + +#[test] +fn successfully_retrieve_request_cookie() { + let mut res = make_request("/get"); + assert_eq!(res.status(), 200); + + let body = block_on(async move { + let mut buffer = Vec::new(); + res.body_mut().read_to_end(&mut buffer).await.unwrap(); + buffer + }); + + assert_eq!(&*body, &*b"RequestCookieValue"); +} + +#[test] +fn successfully_set_cookie() { + let res = make_request("/set"); + assert_eq!(res.status(), 200); + let test_cookie_header = res.headers().get(http::header::SET_COOKIE).unwrap(); + assert_eq!( + test_cookie_header.to_str().unwrap(), + "testCookie=NewCookieValue" + ); +} + +#[test] +fn successfully_remove_cookie() { + let res = make_request("/remove"); + assert_eq!(res.status(), 200); + let test_cookie_header = res.headers().get(http::header::SET_COOKIE).unwrap(); + assert!(test_cookie_header + .to_str() + .unwrap() + .starts_with("testCookie=;")); + let cookie = Cookie::parse_encoded(test_cookie_header.to_str().unwrap()).unwrap(); + assert_eq!(cookie.name(), COOKIE_NAME); + assert_eq!(cookie.value(), ""); + assert_eq!(cookie.http_only(), None); + assert_eq!(cookie.max_age().unwrap().num_nanoseconds(), Some(0)); +} + +#[test] +fn successfully_set_multiple_cookies() { + let res = make_request("/multi"); + assert_eq!(res.status(), 200); + let cookie_header = res.headers().get_all(http::header::SET_COOKIE); + let mut iter = cookie_header.iter(); + + let cookie1 = iter.next().unwrap(); + let cookie2 = iter.next().unwrap(); + + //Headers can be out of order + if cookie1.to_str().unwrap().starts_with("C1") { + assert_eq!(cookie1, "C1=V1"); + assert_eq!(cookie2, "C2=V2"); + } else { + assert_eq!(cookie2, "C1=V1"); + assert_eq!(cookie1, "C2=V2"); + } + + assert!(iter.next().is_none()); +} From 627d852b77111c6eb525428f8053b451d452875f Mon Sep 17 00:00:00 2001 From: Nikolai Hellwig Date: Tue, 21 Jan 2020 20:46:56 +0100 Subject: [PATCH 06/11] Update src/request.rs Co-Authored-By: Yoshua Wuyts --- src/request.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/request.rs b/src/request.rs index 8641ced1a..450070f67 100644 --- a/src/request.rs +++ b/src/request.rs @@ -14,7 +14,7 @@ use crate::error::{Error, ResultExt, StringError}; use crate::middleware::cookies::CookieData; const MIDDLEWARE_MISSING_MSG: &str = - "CookiesMiddleware must be used to populate request and response cookies"; + "`CookiesMiddleware` has not been enabled"; pin_project_lite::pin_project! { /// An HTTP request. From fbee219d5ae082422b55a5dc2b50a7dfa89fe5a7 Mon Sep 17 00:00:00 2001 From: Nikolai Hellwig Date: Tue, 21 Jan 2020 20:48:07 +0100 Subject: [PATCH 07/11] Update src/request.rs Co-Authored-By: Yoshua Wuyts --- src/request.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/request.rs b/src/request.rs index 450070f67..3e598c8be 100644 --- a/src/request.rs +++ b/src/request.rs @@ -302,7 +302,7 @@ impl Request { } /// returns a `Cookie` by name of the cookie. - pub fn get_cookie(&self, name: &str) -> Result>, Error> { + pub fn cookie(&self, name: &str) -> Result>, Error> { let cookie_data = self .local::() .ok_or_else(|| StringError(MIDDLEWARE_MISSING_MSG.to_owned())) From 1b2e0273472ee46f92288e33a7d7039eba0dc9e2 Mon Sep 17 00:00:00 2001 From: Nikolai Hellwig Date: Tue, 21 Jan 2020 20:53:53 +0100 Subject: [PATCH 08/11] Changes after review --- examples/cookies.rs | 2 +- src/middleware/cookies.rs | 87 +++++++++++++++++++-------------------- src/request.rs | 22 ++-------- tests/cookies.rs | 6 +-- 4 files changed, 49 insertions(+), 68 deletions(-) diff --git a/examples/cookies.rs b/examples/cookies.rs index 5f6276fc1..feee20694 100644 --- a/examples/cookies.rs +++ b/examples/cookies.rs @@ -5,7 +5,7 @@ use tide::{middleware::CookiesMiddleware, Request, Response}; /// Tide will use the the `Cookies`'s `Extract` implementation to build this parameter. /// async fn retrieve_cookie(cx: Request<()>) -> String { - format!("hello cookies: {:?}", cx.get_cookie("hello").unwrap()) + format!("hello cookies: {:?}", cx.cookie("hello").unwrap()) } async fn set_cookie(mut cx: Request<()>) -> Response { diff --git a/src/middleware/cookies.rs b/src/middleware/cookies.rs index d4b9a962f..6b7b44478 100644 --- a/src/middleware/cookies.rs +++ b/src/middleware/cookies.rs @@ -21,41 +21,39 @@ use std::sync::{Arc, RwLock}; pub struct CookiesMiddleware; impl CookiesMiddleware { + /// Creates a new CookiesMiddleware. pub fn new() -> Self { Self::default() } +} - async fn handle_cookie<'a, State: Send + Sync + 'static>( +impl Middleware for CookiesMiddleware { + fn handle<'a>( &'a self, - ctx: Request, + mut ctx: Request, next: Next<'a, State>, - ) -> Response { - let mut ctx = ctx; - - let cookie_jar = if let Some(cookie_data) = ctx.local::() { - cookie_data.content.clone() - } else { - let cookie_data = CookieData::from_headers(ctx.headers()); - let content = cookie_data.content.clone(); - ctx = ctx.set_local(cookie_data); - content - }; + ) -> BoxFuture<'a, Response> { + Box::pin(async move { + let cookie_jar = if let Some(cookie_data) = ctx.local::() { + cookie_data.content.clone() + } else { + // no cookie data in local context, so we need to create it + let cookie_data = CookieData::from_headers(ctx.headers()); + let content = cookie_data.content.clone(); + ctx = ctx.set_local(cookie_data); + content + }; - let mut res = next.run(ctx).await; - - for cookie in cookie_jar.read().unwrap().delta() { - res = res.append_header( - http::header::SET_COOKIE.as_ref(), - cookie.encoded().to_string(), - ); - } - res - } -} + let mut res = next.run(ctx).await; -impl Middleware for CookiesMiddleware { - fn handle<'a>(&'a self, ctx: Request, next: Next<'a, State>) -> BoxFuture<'a, Response> { - Box::pin(async move { self.handle_cookie(ctx, next).await }) + // iterate over added and removed cookies + for cookie in cookie_jar.read().unwrap().delta() { + let set_cookie_header = http::header::SET_COOKIE.as_ref(); + let encoded_cookie = cookie.encoded().to_string(); + res = res.append_header(set_cookie_header, encoded_cookie); + } + res + }) } } @@ -66,24 +64,25 @@ pub(crate) struct CookieData { impl CookieData { pub(crate) fn from_headers(headers: &HeaderMap) -> Self { - CookieData { - content: Arc::new(RwLock::new( - headers - .get(http::header::COOKIE) - .and_then(|raw| parse_from_header(raw.to_str().unwrap()).ok()) - .unwrap_or_default(), - )), - } - } -} + let cookie_header = headers.get(http::header::COOKIE); + let cookie_jar = cookie_header.and_then(|raw| { + let mut jar = CookieJar::new(); -fn parse_from_header(s: &str) -> Result { - let mut jar = CookieJar::new(); + // as long as we have an ascii string this will start parsing the cookie + if let Some(raw_str) = raw.to_str().ok() { + raw_str + .split(';') + .try_for_each(|s| -> Result<_, ParseError> { + jar.add_original(Cookie::parse(s.trim().to_owned())?); + Ok(()) + }) + .ok()?; + } - s.split(';').try_for_each(|s| -> Result<_, ParseError> { - jar.add_original(Cookie::parse(s.trim().to_owned())?); - Ok(()) - })?; + Some(jar) + }); + let content = Arc::new(RwLock::new(cookie_jar.unwrap_or_default())); - Ok(jar) + CookieData { content } + } } diff --git a/src/request.rs b/src/request.rs index 3e598c8be..f5854808c 100644 --- a/src/request.rs +++ b/src/request.rs @@ -13,8 +13,7 @@ use std::{str::FromStr, sync::Arc}; use crate::error::{Error, ResultExt, StringError}; use crate::middleware::cookies::CookieData; -const MIDDLEWARE_MISSING_MSG: &str = - "`CookiesMiddleware` has not been enabled"; +const MIDDLEWARE_MISSING_MSG: &str = "`CookiesMiddleware` has not been enabled"; pin_project_lite::pin_project! { /// An HTTP request. @@ -308,12 +307,7 @@ impl Request { .ok_or_else(|| StringError(MIDDLEWARE_MISSING_MSG.to_owned())) .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; - let locked_jar = cookie_data - .content - .read() - .map_err(|e| StringError(format!("Failed to get read lock: {}", e))) - .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; - + let locked_jar = cookie_data.content.read().unwrap(); Ok(locked_jar.get(name).cloned()) } @@ -324,11 +318,7 @@ impl Request { .ok_or_else(|| StringError(MIDDLEWARE_MISSING_MSG.to_owned())) .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; - let mut locked_jar = cookie_data - .content - .write() - .map_err(|e| StringError(format!("Failed to get read lock: {}", e))) - .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; + let mut locked_jar = cookie_data.content.write().unwrap(); locked_jar.add(cookie); Ok(()) } @@ -341,11 +331,7 @@ impl Request { .ok_or_else(|| StringError(MIDDLEWARE_MISSING_MSG.to_owned())) .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; - let mut locked_jar = cookie_data - .content - .write() - .map_err(|e| StringError(format!("Failed to get read lock: {}", e))) - .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; + let mut locked_jar = cookie_data.content.write().unwrap(); locked_jar.remove(cookie); Ok(()) } diff --git a/tests/cookies.rs b/tests/cookies.rs index 2d210a7dd..65c623cd9 100644 --- a/tests/cookies.rs +++ b/tests/cookies.rs @@ -9,11 +9,7 @@ use tide::{middleware::CookiesMiddleware, Request, Response, Server}; static COOKIE_NAME: &str = "testCookie"; async fn retrieve_cookie(cx: Request<()>) -> String { - cx.get_cookie(COOKIE_NAME) - .unwrap() - .unwrap() - .value() - .to_string() + cx.cookie(COOKIE_NAME).unwrap().unwrap().value().to_string() } async fn set_cookie(mut cx: Request<()>) -> Response { From 56a293a28d6d33600f69884f82ddb310f9563d95 Mon Sep 17 00:00:00 2001 From: Nikolai Hellwig Date: Wed, 22 Jan 2020 20:35:44 +0100 Subject: [PATCH 09/11] Moving set_cookie and remove_cookie to Response --- examples/cookies.rs | 19 +++++++++++++------ src/middleware/cookies.rs | 11 +++++++++++ src/request.rs | 25 ------------------------- src/response/mod.rs | 35 ++++++++++++++++++++++++++++++++--- tests/cookies.rs | 24 +++++++++++++----------- 5 files changed, 69 insertions(+), 45 deletions(-) diff --git a/examples/cookies.rs b/examples/cookies.rs index feee20694..351ee9b68 100644 --- a/examples/cookies.rs +++ b/examples/cookies.rs @@ -8,14 +8,21 @@ async fn retrieve_cookie(cx: Request<()>) -> String { format!("hello cookies: {:?}", cx.cookie("hello").unwrap()) } -async fn set_cookie(mut cx: Request<()>) -> Response { - cx.set_cookie(Cookie::new("hello", "world")).unwrap(); - tide::Response::new(200) +// async fn set_cookie(mut cx: Request<()>) -> Response { +// cx.set_cookie(Cookie::new("hello", "world")).unwrap(); +// tide::Response::new(200) +// } + +async fn set_cookie(_req: Request<()>) -> Response { + let mut res = tide::Response::new(200); + res.set_cookie(Cookie::new("hello", "world")); + res } -async fn remove_cookie(mut cx: Request<()>) -> Response { - cx.remove_cookie(Cookie::named("hello")).unwrap(); - tide::Response::new(200) +async fn remove_cookie(_req: Request<()>) -> Response { + let mut res = tide::Response::new(200); + res.remove_cookie(Cookie::named("hello")); + res } fn main() -> Result<(), std::io::Error> { diff --git a/src/middleware/cookies.rs b/src/middleware/cookies.rs index 6b7b44478..836ca7550 100644 --- a/src/middleware/cookies.rs +++ b/src/middleware/cookies.rs @@ -1,5 +1,6 @@ use crate::{ middleware::{Middleware, Next}, + response::CookieEvent, Request, Response, }; use cookie::{Cookie, CookieJar, ParseError}; @@ -46,6 +47,16 @@ impl Middleware for CookiesMiddleware { let mut res = next.run(ctx).await; + // add modifications from response to original + for cookie in res.cookie_events.drain(..) { + match cookie { + CookieEvent::Added(cookie) => cookie_jar.write().unwrap().add(cookie.clone()), + CookieEvent::Removed(cookie) => { + cookie_jar.write().unwrap().remove(cookie.clone()) + } + } + } + // iterate over added and removed cookies for cookie in cookie_jar.read().unwrap().delta() { let set_cookie_header = http::header::SET_COOKIE.as_ref(); diff --git a/src/request.rs b/src/request.rs index f5854808c..de9574afe 100644 --- a/src/request.rs +++ b/src/request.rs @@ -310,31 +310,6 @@ impl Request { let locked_jar = cookie_data.content.read().unwrap(); Ok(locked_jar.get(name).cloned()) } - - /// Add cookie to the cookie jar. - pub fn set_cookie(&mut self, cookie: Cookie<'static>) -> Result<(), Error> { - let cookie_data = self - .local::() - .ok_or_else(|| StringError(MIDDLEWARE_MISSING_MSG.to_owned())) - .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; - - let mut locked_jar = cookie_data.content.write().unwrap(); - locked_jar.add(cookie); - Ok(()) - } - - /// Removes the cookie. This instructs the `CookiesMiddleware` to send a cookie with empty value - /// in the response. - pub fn remove_cookie(&mut self, cookie: Cookie<'static>) -> Result<(), Error> { - let cookie_data = self - .local::() - .ok_or_else(|| StringError(MIDDLEWARE_MISSING_MSG.to_owned())) - .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; - - let mut locked_jar = cookie_data.content.write().unwrap(); - locked_jar.remove(cookie); - Ok(()) - } } impl Read for Request { diff --git a/src/response/mod.rs b/src/response/mod.rs index ab7348adf..8bd2338ef 100644 --- a/src/response/mod.rs +++ b/src/response/mod.rs @@ -1,5 +1,6 @@ use async_std::io::prelude::*; +use cookie::Cookie; use http::StatusCode; use http_service::Body; use mime::Mime; @@ -9,10 +10,18 @@ pub use into_response::IntoResponse; mod into_response; +#[derive(Debug)] +pub(crate) enum CookieEvent { + Added(Cookie<'static>), + Removed(Cookie<'static>), +} + /// An HTTP response #[derive(Debug)] pub struct Response { res: http_service::Response, + // tracking here + pub(crate) cookie_events: Vec, } impl Response { @@ -23,7 +32,10 @@ impl Response { .status(status) .body(Body::empty()) .unwrap(); - Self { res } + Self { + res, + cookie_events: vec![], + } } /// Create a new instance from a reader. @@ -36,7 +48,10 @@ impl Response { .status(status) .body(Box::pin(reader).into()) .unwrap(); - Self { res } + Self { + res, + cookie_events: vec![], + } } /// Returns the statuscode. @@ -138,6 +153,17 @@ impl Response { // Ok(Multipart::with_body(Cursor::new(body), boundary)) // }) // } + + /// Add cookie to the cookie jar. + pub fn set_cookie(&mut self, cookie: Cookie<'static>) { + self.cookie_events.push(CookieEvent::Added(cookie)); + } + + /// Removes the cookie. This instructs the `CookiesMiddleware` to send a cookie with empty value + /// in the response. + pub fn remove_cookie(&mut self, cookie: Cookie<'static>) { + self.cookie_events.push(CookieEvent::Removed(cookie)); + } } #[doc(hidden)] @@ -150,6 +176,9 @@ impl Into for Response { #[doc(hidden)] impl From for Response { fn from(res: http_service::Response) -> Self { - Self { res } + Self { + res, + cookie_events: vec![], + } } } diff --git a/tests/cookies.rs b/tests/cookies.rs index 65c623cd9..9ed5c6273 100644 --- a/tests/cookies.rs +++ b/tests/cookies.rs @@ -12,21 +12,23 @@ async fn retrieve_cookie(cx: Request<()>) -> String { cx.cookie(COOKIE_NAME).unwrap().unwrap().value().to_string() } -async fn set_cookie(mut cx: Request<()>) -> Response { - cx.set_cookie(Cookie::new(COOKIE_NAME, "NewCookieValue")) - .unwrap(); - Response::new(200) +async fn set_cookie(_req: Request<()>) -> Response { + let mut res = Response::new(200); + res.set_cookie(Cookie::new(COOKIE_NAME, "NewCookieValue")); + res } -async fn remove_cookie(mut cx: Request<()>) -> Response { - cx.remove_cookie(Cookie::named(COOKIE_NAME)).unwrap(); - Response::new(200) +async fn remove_cookie(_req: Request<()>) -> Response { + let mut res = Response::new(200); + res.remove_cookie(Cookie::named(COOKIE_NAME)); + res } -async fn set_multiple_cookie(mut cx: Request<()>) -> Response { - cx.set_cookie(Cookie::new("C1", "V1")).unwrap(); - cx.set_cookie(Cookie::new("C2", "V2")).unwrap(); - Response::new(200) +async fn set_multiple_cookie(_req: Request<()>) -> Response { + let mut res = Response::new(200); + res.set_cookie(Cookie::new("C1", "V1")); + res.set_cookie(Cookie::new("C2", "V2")); + res } fn app() -> crate::Server<()> { From 9b8bad2ad6e65937b3e8b5f7307aff41ae0085b7 Mon Sep 17 00:00:00 2001 From: Nikolai Hellwig Date: Sat, 25 Jan 2020 16:34:25 +0100 Subject: [PATCH 10/11] Adding example for cookies --- src/middleware/cookies.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/middleware/cookies.rs b/src/middleware/cookies.rs index 836ca7550..d28f87efd 100644 --- a/src/middleware/cookies.rs +++ b/src/middleware/cookies.rs @@ -16,7 +16,14 @@ use std::sync::{Arc, RwLock}; /// ```rust /// /// let mut app = tide::Server::new(); +/// app.at("/get").get(|cx: tide::Request<()>| async move { cx.cookie("testCookie").unwrap().unwrap().value().to_string() }); +/// app.at("/set").get(|_| async { +/// let mut res = tide::Response::new(200); +/// res.set_cookie(cookie::Cookie::new("testCookie", "NewCookieValue")); +/// res +/// }); /// app.middleware(tide::middleware::CookiesMiddleware::new()); +/// /// ``` #[derive(Debug, Clone, Default)] pub struct CookiesMiddleware; From 3f4cfadbcdc935c0fbf25542e73c7e7cdf4c4644 Mon Sep 17 00:00:00 2001 From: Nikolai Hellwig Date: Mon, 27 Jan 2020 19:41:40 +0100 Subject: [PATCH 11/11] Adding Cookie Middleware as default --- examples/cookies.rs | 8 +------- src/middleware/cookies.rs | 3 +-- src/middleware/mod.rs | 3 --- src/request.rs | 9 +++------ src/server/mod.rs | 4 +++- tests/cookies.rs | 3 +-- 6 files changed, 9 insertions(+), 21 deletions(-) diff --git a/examples/cookies.rs b/examples/cookies.rs index 351ee9b68..740c6ad73 100644 --- a/examples/cookies.rs +++ b/examples/cookies.rs @@ -1,6 +1,6 @@ use async_std::task; use cookie::Cookie; -use tide::{middleware::CookiesMiddleware, Request, Response}; +use tide::{Request, Response}; /// Tide will use the the `Cookies`'s `Extract` implementation to build this parameter. /// @@ -8,11 +8,6 @@ async fn retrieve_cookie(cx: Request<()>) -> String { format!("hello cookies: {:?}", cx.cookie("hello").unwrap()) } -// async fn set_cookie(mut cx: Request<()>) -> Response { -// cx.set_cookie(Cookie::new("hello", "world")).unwrap(); -// tide::Response::new(200) -// } - async fn set_cookie(_req: Request<()>) -> Response { let mut res = tide::Response::new(200); res.set_cookie(Cookie::new("hello", "world")); @@ -28,7 +23,6 @@ async fn remove_cookie(_req: Request<()>) -> Response { fn main() -> Result<(), std::io::Error> { task::block_on(async { let mut app = tide::new(); - app.middleware(CookiesMiddleware::new()); app.at("/").get(retrieve_cookie); app.at("/set").get(set_cookie); diff --git a/src/middleware/cookies.rs b/src/middleware/cookies.rs index d28f87efd..615244edb 100644 --- a/src/middleware/cookies.rs +++ b/src/middleware/cookies.rs @@ -22,11 +22,10 @@ use std::sync::{Arc, RwLock}; /// res.set_cookie(cookie::Cookie::new("testCookie", "NewCookieValue")); /// res /// }); -/// app.middleware(tide::middleware::CookiesMiddleware::new()); /// /// ``` #[derive(Debug, Clone, Default)] -pub struct CookiesMiddleware; +pub(crate) struct CookiesMiddleware; impl CookiesMiddleware { /// Creates a new CookiesMiddleware. diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 09bad9369..04f43d1e2 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -16,13 +16,10 @@ mod cors; mod logger; // pub use compression::{Compression, Decompression}; -// pub use cookies::CookiesMiddleware; pub use cors::{Cors, Origin}; // pub use default_headers::DefaultHeaders; pub use logger::RequestLogger; -pub use cookies::CookiesMiddleware; - /// Middleware that wraps around remaining middleware chain. pub trait Middleware: 'static + Send + Sync { /// Asynchronously handle the request, and return a response. diff --git a/src/request.rs b/src/request.rs index de9574afe..df4807176 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,5 +1,5 @@ use cookie::Cookie; -use http::{status::StatusCode, HeaderMap, Method, Uri, Version}; +use http::{HeaderMap, Method, Uri, Version}; use http_service::Body; use route_recognizer::Params; use serde::Deserialize; @@ -10,11 +10,9 @@ use async_std::task::{Context, Poll}; use std::pin::Pin; use std::{str::FromStr, sync::Arc}; -use crate::error::{Error, ResultExt, StringError}; +use crate::error::Error; use crate::middleware::cookies::CookieData; -const MIDDLEWARE_MISSING_MSG: &str = "`CookiesMiddleware` has not been enabled"; - pin_project_lite::pin_project! { /// An HTTP request. /// @@ -304,8 +302,7 @@ impl Request { pub fn cookie(&self, name: &str) -> Result>, Error> { let cookie_data = self .local::() - .ok_or_else(|| StringError(MIDDLEWARE_MISSING_MSG.to_owned())) - .with_err_status(StatusCode::INTERNAL_SERVER_ERROR)?; + .expect("should always be set by the cookies middleware"); let locked_jar = cookie_data.content.read().unwrap(); Ok(locked_jar.get(name).cloned()) diff --git a/src/server/mod.rs b/src/server/mod.rs index 188e159a4..662cc3094 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -203,7 +203,9 @@ impl Server { pub fn with_state(state: State) -> Server { Server { router: Router::new(), - middleware: Vec::new(), + middleware: vec![Arc::new( + crate::middleware::cookies::CookiesMiddleware::new(), + )], state, } } diff --git a/tests/cookies.rs b/tests/cookies.rs index 9ed5c6273..36a2e7fc4 100644 --- a/tests/cookies.rs +++ b/tests/cookies.rs @@ -4,7 +4,7 @@ use futures::AsyncReadExt; use http_service::Body; use http_service_mock::make_server; -use tide::{middleware::CookiesMiddleware, Request, Response, Server}; +use tide::{Request, Response, Server}; static COOKIE_NAME: &str = "testCookie"; @@ -33,7 +33,6 @@ async fn set_multiple_cookie(_req: Request<()>) -> Response { fn app() -> crate::Server<()> { let mut app = tide::new(); - app.middleware(CookiesMiddleware::new()); app.at("/get").get(retrieve_cookie); app.at("/set").get(set_cookie);