From 4d69e6d0b49608e92f1fe876117bd3ad172ab922 Mon Sep 17 00:00:00 2001 From: axon-q Date: Tue, 12 Jun 2018 13:47:49 +0000 Subject: [PATCH 01/10] fs: minor cleanups to content_disposition --- src/fs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fs.rs b/src/fs.rs index 101459527..35c78b736 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -276,7 +276,7 @@ impl Responder for NamedFile { if self.status_code != StatusCode::OK { let mut resp = HttpResponse::build(self.status_code); resp.set(header::ContentType(self.content_type.clone())) - .header("Content-Disposition", format!("{}", &self.content_disposition)); + .header(header::CONTENT_DISPOSITION, self.content_disposition.to_string()); if let Some(current_encoding) = self.encoding { resp.content_encoding(current_encoding); @@ -327,7 +327,7 @@ impl Responder for NamedFile { let mut resp = HttpResponse::build(self.status_code); resp.set(header::ContentType(self.content_type.clone())) - .header("Content-Disposition", format!("{}", &self.content_disposition)); + .header(header::CONTENT_DISPOSITION, self.content_disposition.to_string()); if let Some(current_encoding) = self.encoding { resp.content_encoding(current_encoding); From e414a52b5102e8d364510393582af6a86f345d05 Mon Sep 17 00:00:00 2001 From: axon-q Date: Tue, 12 Jun 2018 13:48:23 +0000 Subject: [PATCH 02/10] content_disposition: remove unnecessary allocations --- src/header/common/content_disposition.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/header/common/content_disposition.rs b/src/header/common/content_disposition.rs index 0edebfedb..ff04ef565 100644 --- a/src/header/common/content_disposition.rs +++ b/src/header/common/content_disposition.rs @@ -193,8 +193,9 @@ impl fmt::Display for ContentDisposition { } } if use_simple_format { + use std::str; try!(write!(f, "; filename=\"{}\"", - match String::from_utf8(bytes.clone()) { + match str::from_utf8(bytes) { Ok(s) => s, Err(_) => return Err(fmt::Error), })); From d8e1fd102de6c1014f9dad688344da4f9d0b6009 Mon Sep 17 00:00:00 2001 From: axon-q Date: Tue, 12 Jun 2018 13:49:07 +0000 Subject: [PATCH 03/10] add cookie methods to HttpResponse --- CHANGES.md | 5 +++ src/httpresponse.rs | 104 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 106 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 29046f8f8..f7a753543 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,8 @@ ### Added +* Add methods to `HttpResponse` to retrieve, add, and delete cookies + * Add `.set_content_type()` and `.set_content_disposition()` methods to `fs::NamedFile` to allow overriding the values inferred by default @@ -22,6 +24,9 @@ * Min rustc version is 1.26 +* `HttpResponse::into_builder()` now moves cookies into the builder + instead of dropping them + * Use tokio instead of tokio-core * Use `&mut self` instead of `&self` for Middleware trait diff --git a/src/httpresponse.rs b/src/httpresponse.rs index f42be2b9f..40a8cc61e 100644 --- a/src/httpresponse.rs +++ b/src/httpresponse.rs @@ -97,14 +97,25 @@ impl HttpResponse { /// Convert `HttpResponse` to a `HttpResponseBuilder` #[inline] pub fn into_builder(mut self) -> HttpResponseBuilder { + // If this response has cookies, load them into a jar + let mut jar: Option = None; + for c in self.cookies() { + if let Some(ref mut j) = jar { + j.add_original(c.into_owned()); + } else { + let mut j = CookieJar::new(); + j.add_original(c.into_owned()); + jar = Some(j); + } + } + let response = self.0.take(); let pool = Some(Rc::clone(&self.1)); - HttpResponseBuilder { response, pool, err: None, - cookies: None, // TODO: convert set-cookie headers + cookies: jar, } } @@ -132,6 +143,49 @@ impl HttpResponse { &mut self.get_mut().headers } + /// Get an iterator for the cookies set by this response + #[inline] + pub fn cookies(&self) -> CookieIter { + CookieIter { + iter: self.get_ref().headers.get_all(header::SET_COOKIE).iter() + } + } + + /// Add a cookie to this response + #[inline] + pub fn add_cookie(&mut self, cookie: Cookie) -> Result<(), HttpError> { + let h = &mut self.get_mut().headers; + HeaderValue::from_str(&cookie.to_string()) + .map(|c| { h.append(header::SET_COOKIE, c); }) + .map_err(|e| e.into()) + } + + /// Remove all cookies with the given name from this response. Returns + /// the number of cookies removed. + #[inline] + pub fn del_cookie(&mut self, name: &str) -> usize { + let h = &mut self.get_mut().headers; + let vals: Vec = h.get_all(header::SET_COOKIE) + .iter() + .map(|v| v.to_owned()) + .collect(); + h.remove(header::SET_COOKIE); + + let mut count: usize = 0; + for v in vals { + if let Ok(s) = v.to_str() { + if let Ok(c) = Cookie::parse(s) { + if c.name() == name { + count += 1; + continue; + } + } + } + h.append(header::SET_COOKIE, v); + } + return count; + } + /// Get the response status code #[inline] pub fn status(&self) -> StatusCode { @@ -269,6 +323,24 @@ impl fmt::Debug for HttpResponse { } } +pub struct CookieIter<'a> { + iter: header::ValueIter<'a, HeaderValue>, +} + +impl<'a> Iterator for CookieIter<'a> { + type Item = Cookie<'a>; + + #[inline] + fn next(&mut self) -> Option> { + for v in self.iter.by_ref() { + if let Some(c) = (|| Cookie::parse(v.to_str().ok()?).ok())() { + return Some(c); + } + } + None + } +} + /// An HTTP response builder /// /// This type can be used to construct an instance of `HttpResponse` through a @@ -984,6 +1056,27 @@ mod tests { ); } + + #[test] + fn test_update_response_cookies() { + let mut r = HttpResponse::Ok() + .cookie(http::Cookie::new("original", "val100")) + .finish(); + + r.add_cookie(http::Cookie::new("cookie2", "val200")).unwrap(); + r.add_cookie(http::Cookie::new("cookie2", "val250")).unwrap(); + r.add_cookie(http::Cookie::new("cookie3", "val300")).unwrap(); + + assert_eq!(r.cookies().count(), 4); + r.del_cookie("cookie2"); + + let mut iter = r.cookies(); + let v = iter.next().unwrap(); + assert_eq!((v.name(), v.value()), ("original", "val100")); + let v = iter.next().unwrap(); + assert_eq!((v.name(), v.value()), ("cookie3", "val300")); + } + #[test] fn test_basic_builder() { let resp = HttpResponse::Ok() @@ -1191,11 +1284,16 @@ mod tests { #[test] fn test_into_builder() { - let resp: HttpResponse = "test".into(); + let mut resp: HttpResponse = "test".into(); assert_eq!(resp.status(), StatusCode::OK); + resp.add_cookie(http::Cookie::new("cookie1", "val100")).unwrap(); + let mut builder = resp.into_builder(); let resp = builder.status(StatusCode::BAD_REQUEST).finish(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + + let cookie = resp.cookies().next().unwrap(); + assert_eq!((cookie.name(), cookie.value()), ("cookie1", "val100")); } } From d4d3add17d18e19137ec3058962c3142d6bed076 Mon Sep 17 00:00:00 2001 From: Ozgur Akkurt Date: Tue, 12 Jun 2018 19:30:00 +0300 Subject: [PATCH 04/10] add ClientRequestBuilder::form() --- src/client/request.rs | 19 ++++++++++++++++++ src/error.rs | 45 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/client/request.rs b/src/client/request.rs index bb338482b..bc8feb3e7 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -10,6 +10,7 @@ use futures::Stream; use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use serde::Serialize; use serde_json; +use serde_urlencoded; use url::Url; use super::body::ClientBody; @@ -658,6 +659,24 @@ impl ClientRequestBuilder { self.body(body) } + + /// Set a urlencoded body and generate `ClientRequest` + /// + /// `ClientRequestBuilder` can not be used after this call. + pub fn form(&mut self, value: T) -> Result { + let body = serde_urlencoded::to_string(&value)?; + + let contains = if let Some(parts) = parts(&mut self.request, &self.err) { + parts.headers.contains_key(header::CONTENT_TYPE) + } else { + true + }; + if !contains { + self.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded"); + } + + self.body(body) + } /// Set a streaming body and generate `ClientRequest`. /// diff --git a/src/error.rs b/src/error.rs index f3327c2b6..39e66a0db 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,6 +16,7 @@ use http_range::HttpRangeParseError; use httparse; use serde::de::value::Error as DeError; use serde_json::error::Error as JsonError; +use serde_urlencoded::ser::Error as FormError; use tokio_timer::Error as TimerError; pub use url::ParseError as UrlParseError; @@ -205,6 +206,9 @@ impl From for Error { /// `InternalServerError` for `JsonError` impl ResponseError for JsonError {} +/// `InternalServerError` for `FormError` +impl ResponseError for FormError {} + /// `InternalServerError` for `TimerError` impl ResponseError for TimerError {} @@ -586,6 +590,47 @@ impl From for JsonPayloadError { } } +/// A set of errors that can occur during parsing json payloads +#[derive(Fail, Debug)] +pub enum FormPayloadError { + /// Payload size is bigger than allowed. (default: 256kB) + #[fail(display = "Form payload size is bigger than allowed. (default: 256kB)")] + Overflow, + /// Content type error + #[fail(display = "Content type error")] + ContentType, + /// Deserialize error + #[fail(display = "Form deserialize error: {}", _0)] + Deserialize(#[cause] FormError), + /// Payload error + #[fail(display = "Error that occur during reading payload: {}", _0)] + Payload(#[cause] PayloadError), +} + +/// Return `BadRequest` for `UrlencodedError` +impl ResponseError for FormPayloadError { + fn error_response(&self) -> HttpResponse { + match *self { + FormPayloadError::Overflow => { + HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE) + } + _ => HttpResponse::new(StatusCode::BAD_REQUEST), + } + } +} + +impl From for FormPayloadError { + fn from(err: PayloadError) -> FormPayloadError { + FormPayloadError::Payload(err) + } +} + +impl From for FormPayloadError { + fn from(err: FormError) -> FormPayloadError { + FormPayloadError::Deserialize(err) + } +} + /// Errors which can occur when attempting to interpret a segment string as a /// valid path segment. #[derive(Fail, Debug, PartialEq)] From 8af082d8732816a3b3e8711a45a65038cef0abdc Mon Sep 17 00:00:00 2001 From: Dursun Akkurt Date: Tue, 12 Jun 2018 20:26:09 +0300 Subject: [PATCH 05/10] remove FormPayloadError --- src/error.rs | 41 ----------------------------------------- 1 file changed, 41 deletions(-) diff --git a/src/error.rs b/src/error.rs index 39e66a0db..bbafb1c41 100644 --- a/src/error.rs +++ b/src/error.rs @@ -590,47 +590,6 @@ impl From for JsonPayloadError { } } -/// A set of errors that can occur during parsing json payloads -#[derive(Fail, Debug)] -pub enum FormPayloadError { - /// Payload size is bigger than allowed. (default: 256kB) - #[fail(display = "Form payload size is bigger than allowed. (default: 256kB)")] - Overflow, - /// Content type error - #[fail(display = "Content type error")] - ContentType, - /// Deserialize error - #[fail(display = "Form deserialize error: {}", _0)] - Deserialize(#[cause] FormError), - /// Payload error - #[fail(display = "Error that occur during reading payload: {}", _0)] - Payload(#[cause] PayloadError), -} - -/// Return `BadRequest` for `UrlencodedError` -impl ResponseError for FormPayloadError { - fn error_response(&self) -> HttpResponse { - match *self { - FormPayloadError::Overflow => { - HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE) - } - _ => HttpResponse::new(StatusCode::BAD_REQUEST), - } - } -} - -impl From for FormPayloadError { - fn from(err: PayloadError) -> FormPayloadError { - FormPayloadError::Payload(err) - } -} - -impl From for FormPayloadError { - fn from(err: FormError) -> FormPayloadError { - FormPayloadError::Deserialize(err) - } -} - /// Errors which can occur when attempting to interpret a segment string as a /// valid path segment. #[derive(Fail, Debug, PartialEq)] From 9cc7651c222fd61df1e6550594d24e54e494f542 Mon Sep 17 00:00:00 2001 From: Dursun Akkurt Date: Tue, 12 Jun 2018 20:32:16 +0300 Subject: [PATCH 06/10] add change to CHANGES.md --- CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index f7a753543..e9970d8f8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,8 @@ ### Added +* Add `ClientRequestBuilder::form()` for sending `application/x-www-form-urlencoded` requests. + * Add methods to `HttpResponse` to retrieve, add, and delete cookies * Add `.set_content_type()` and `.set_content_disposition()` methods From ffca4164639e80c947c190f679dda4ef216d7c33 Mon Sep 17 00:00:00 2001 From: Dursun Akkurt Date: Tue, 12 Jun 2018 22:16:20 +0300 Subject: [PATCH 07/10] add test for ClientRequestBuilder::form() --- tests/test_handlers.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_handlers.rs b/tests/test_handlers.rs index 5ece53eed..a031b5cc1 100644 --- a/tests/test_handlers.rs +++ b/tests/test_handlers.rs @@ -96,6 +96,35 @@ fn test_async_extractor_async() { assert_eq!(bytes, Bytes::from_static(b"{\"test\":1}")); } +#[derive(Deserialize)] +struct FormData { + username: String, +} + +#[test] +fn test_form_extractor() { + let mut srv = test::TestServer::new(|app| { + app.resource("/{username}/index.html", |r| { + r.route().with(|form: Form| { + Ok(format!("{}", form.username)) + }) + }); + }); + + // client request + let request = srv + .post() + .uri(srv.url("/test1/index.html")) + .form(FormData{username: "test".into_string()}) + .unwrap(); + let response = srv.execute(request.send()).unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = srv.execute(response.body()).unwrap(); + assert_eq!(bytes, Bytes::from_static(b"test")); +} + #[test] fn test_path_and_query_extractor() { let mut srv = test::TestServer::new(|app| { From 94283a73c2a524904faaca2b2c1ebe8118ac7755 Mon Sep 17 00:00:00 2001 From: Dursun Akkurt Date: Tue, 12 Jun 2018 22:31:33 +0300 Subject: [PATCH 08/10] make into_string, to_string --- tests/test_handlers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_handlers.rs b/tests/test_handlers.rs index a031b5cc1..d59c71306 100644 --- a/tests/test_handlers.rs +++ b/tests/test_handlers.rs @@ -115,7 +115,7 @@ fn test_form_extractor() { let request = srv .post() .uri(srv.url("/test1/index.html")) - .form(FormData{username: "test".into_string()}) + .form(FormData{username: "test".to_string()}) .unwrap(); let response = srv.execute(request.send()).unwrap(); assert!(response.status().is_success()); From e6bbda0efcbc3787f0407816d01c4647beb6bdd2 Mon Sep 17 00:00:00 2001 From: Dursun Akkurt Date: Tue, 12 Jun 2018 22:42:15 +0300 Subject: [PATCH 09/10] add serialize --- tests/test_handlers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_handlers.rs b/tests/test_handlers.rs index d59c71306..7bef3900c 100644 --- a/tests/test_handlers.rs +++ b/tests/test_handlers.rs @@ -96,7 +96,7 @@ fn test_async_extractor_async() { assert_eq!(bytes, Bytes::from_static(b"{\"test\":1}")); } -#[derive(Deserialize)] +#[derive(Deserialize, Serialize)] struct FormData { username: String, } From ed7cbaa772fdbb0fcd3b89d695a576db671ee897 Mon Sep 17 00:00:00 2001 From: Dursun Akkurt Date: Tue, 12 Jun 2018 23:04:54 +0300 Subject: [PATCH 10/10] fix form_extractor test --- tests/test_handlers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_handlers.rs b/tests/test_handlers.rs index 7bef3900c..8f1ee9439 100644 --- a/tests/test_handlers.rs +++ b/tests/test_handlers.rs @@ -106,7 +106,7 @@ fn test_form_extractor() { let mut srv = test::TestServer::new(|app| { app.resource("/{username}/index.html", |r| { r.route().with(|form: Form| { - Ok(format!("{}", form.username)) + format!("{}", form.username) }) }); });