diff --git a/CHANGES.md b/CHANGES.md index ad06fc03c..1f9688f66 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,11 +2,16 @@ ## [0.7.1] - 2018-07-21 +### Added + + * Add implementation of `FromRequest` for `Option` and `Result` + ### Fixed * Fixed default_resource 'not yet implemented' panic #410 * removed the timestamp from the default logger middleware +* Add `CookieSessionBackend::http_only` method to set `HttpOnly` directive of cookies ## [0.7.0] - 2018-07-21 diff --git a/README.md b/README.md index 632a33dc9..ec8c439ef 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,7 @@ Actix web is a simple, pragmatic and extremely fast web framework for Rust. * Multipart streams * Static assets * SSL support with OpenSSL or `native-tls` -* Middlewares ([Logger](https://actix.rs/book/actix-web/sec-9-middlewares.html#logging), - [Session](https://actix.rs/book/actix-web/sec-9-middlewares.html#user-sessions), - [Redis sessions](https://github.com/actix/actix-redis), - [DefaultHeaders](https://actix.rs/book/actix-web/sec-9-middlewares.html#default-headers), - [CORS](https://actix.rs/actix-web/actix_web/middleware/cors/index.html), - [CSRF](https://actix.rs/actix-web/actix_web/middleware/csrf/index.html)) +* Middlewares ([Logger,Session,CORS,CSRF,etc](https://actix.rs/docs/middleware/)) * Includes an asynchronous [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html) * Built on top of [Actix actor framework](https://github.com/actix/actix) diff --git a/src/application.rs b/src/application.rs index 72ecff3da..f36adf69e 100644 --- a/src/application.rs +++ b/src/application.rs @@ -610,7 +610,6 @@ impl Iterator for App { mod tests { use super::*; use body::{Binary, Body}; - use fs; use http::StatusCode; use httprequest::HttpRequest; use httpresponse::HttpResponse; diff --git a/src/extractor.rs b/src/extractor.rs index 8e4745f86..768edfb76 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -6,7 +6,7 @@ use std::{fmt, str}; use bytes::Bytes; use encoding::all::UTF_8; use encoding::types::{DecoderTrap, Encoding}; -use futures::{Async, Future, Poll}; +use futures::{Async, Future, Poll, future}; use mime::Mime; use serde::de::{self, DeserializeOwned}; use serde_urlencoded; @@ -17,6 +17,7 @@ use handler::{AsyncResult, FromRequest}; use httpmessage::{HttpMessage, MessageBody, UrlEncoded}; use httprequest::HttpRequest; +#[derive(PartialEq, Eq, PartialOrd, Ord)] /// Extract typed information from the request's path. /// /// ## Example @@ -128,6 +129,7 @@ impl fmt::Display for Path { } } +#[derive(PartialEq, Eq, PartialOrd, Ord)] /// Extract typed information from from the request's query. /// /// ## Example @@ -215,6 +217,7 @@ impl fmt::Display for Query { } } +#[derive(PartialEq, Eq, PartialOrd, Ord)] /// Extract typed information from the request's body. /// /// To extract typed information from request's body, the type `T` must @@ -455,6 +458,124 @@ impl FromRequest for String { } } +/// Optionally extract a field from the request +/// +/// If the FromRequest for T fails, return None rather than returning an error response +/// +/// ## Example +/// +/// ```rust +/// # extern crate actix_web; +/// extern crate rand; +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{http, App, Result, HttpRequest, Error, FromRequest}; +/// use actix_web::error::ErrorBadRequest; +/// +/// #[derive(Debug, Deserialize)] +/// struct Thing { name: String } +/// +/// impl FromRequest for Thing { +/// type Config = (); +/// type Result = Result; +/// +/// #[inline] +/// fn from_request(req: &HttpRequest, _cfg: &Self::Config) -> Self::Result { +/// if rand::random() { +/// Ok(Thing { name: "thingy".into() }) +/// } else { +/// Err(ErrorBadRequest("no luck")) +/// } +/// +/// } +/// } +/// +/// /// extract text data from request +/// fn index(supplied_thing: Option) -> Result { +/// match supplied_thing { +/// // Puns not intended +/// Some(thing) => Ok(format!("Got something: {:?}", thing)), +/// None => Ok(format!("No thing!")) +/// } +/// } +/// +/// fn main() { +/// let app = App::new().resource("/users/:first", |r| { +/// r.method(http::Method::POST).with(index) +/// }); +/// } +/// ``` +impl FromRequest for Option where T: FromRequest { + type Config = T::Config; + type Result = Box, Error = Error>>; + + #[inline] + fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { + Box::new(T::from_request(req, cfg).into().then( |r| { + match r { + Ok(v) => future::ok(Some(v)), + Err(e) => { + future::ok(None) + } + } + })) + } +} + +/// Optionally extract a field from the request or extract the Error if unsuccessful +/// +/// If the FromRequest for T fails, inject Err into handler rather than returning an error response +/// +/// ## Example +/// +/// ```rust +/// # extern crate actix_web; +/// extern crate rand; +/// #[macro_use] extern crate serde_derive; +/// use actix_web::{http, App, Result, HttpRequest, Error, FromRequest}; +/// use actix_web::error::ErrorBadRequest; +/// +/// #[derive(Debug, Deserialize)] +/// struct Thing { name: String } +/// +/// impl FromRequest for Thing { +/// type Config = (); +/// type Result = Result; +/// +/// #[inline] +/// fn from_request(req: &HttpRequest, _cfg: &Self::Config) -> Self::Result { +/// if rand::random() { +/// Ok(Thing { name: "thingy".into() }) +/// } else { +/// Err(ErrorBadRequest("no luck")) +/// } +/// +/// } +/// } +/// +/// /// extract text data from request +/// fn index(supplied_thing: Result) -> Result { +/// match supplied_thing { +/// Ok(thing) => Ok(format!("Got thing: {:?}", thing)), +/// Err(e) => Ok(format!("Error extracting thing: {}", e)) +/// } +/// } +/// +/// fn main() { +/// let app = App::new().resource("/users/:first", |r| { +/// r.method(http::Method::POST).with(index) +/// }); +/// } +/// ``` +impl FromRequest for Result where T: FromRequest{ + type Config = T::Config; + type Result = Box, Error = Error>>; + + #[inline] + fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result { + Box::new(T::from_request(req, cfg).into().then( |r| { future::ok(r) })) + } +} + /// Payload configuration for request's payload. pub struct PayloadConfig { limit: usize, @@ -680,6 +801,75 @@ mod tests { } } + #[test] + fn test_option() { + let req = TestRequest::with_header( + header::CONTENT_TYPE, + "application/x-www-form-urlencoded", + ).finish(); + + let mut cfg = FormConfig::default(); + cfg.limit(4096); + + match Option::>::from_request(&req, &cfg).poll().unwrap() { + Async::Ready(r) => assert_eq!(r, None), + _ => unreachable!(), + } + + let req = TestRequest::with_header( + header::CONTENT_TYPE, + "application/x-www-form-urlencoded", + ).header(header::CONTENT_LENGTH, "9") + .set_payload(Bytes::from_static(b"hello=world")) + .finish(); + + match Option::>::from_request(&req, &cfg).poll().unwrap() { + Async::Ready(r) => assert_eq!(r, Some(Form(Info { hello: "world".into() }))), + _ => unreachable!(), + } + + let req = TestRequest::with_header( + header::CONTENT_TYPE, + "application/x-www-form-urlencoded", + ).header(header::CONTENT_LENGTH, "9") + .set_payload(Bytes::from_static(b"bye=world")) + .finish(); + + match Option::>::from_request(&req, &cfg).poll().unwrap() { + Async::Ready(r) => assert_eq!(r, None), + _ => unreachable!(), + } + } + + #[test] + fn test_result() { + let req = TestRequest::with_header( + header::CONTENT_TYPE, + "application/x-www-form-urlencoded", + ).header(header::CONTENT_LENGTH, "11") + .set_payload(Bytes::from_static(b"hello=world")) + .finish(); + + match Result::, Error>::from_request(&req, &FormConfig::default()).poll().unwrap() { + Async::Ready(Ok(r)) => assert_eq!(r, Form(Info { hello: "world".into() })), + _ => unreachable!(), + } + + let req = TestRequest::with_header( + header::CONTENT_TYPE, + "application/x-www-form-urlencoded", + ).header(header::CONTENT_LENGTH, "9") + .set_payload(Bytes::from_static(b"bye=world")) + .finish(); + + match Result::, Error>::from_request(&req, &FormConfig::default()).poll().unwrap() { + Async::Ready(r) => assert!(r.is_err()), + _ => unreachable!(), + } + } + + + #[test] fn test_payload_config() { let req = TestRequest::default().finish(); diff --git a/src/middleware/session.rs b/src/middleware/session.rs index 40ba0f4dd..cc7aab6b4 100644 --- a/src/middleware/session.rs +++ b/src/middleware/session.rs @@ -358,6 +358,7 @@ struct CookieSessionInner { path: String, domain: Option, secure: bool, + http_only: bool, max_age: Option, same_site: Option, } @@ -371,6 +372,7 @@ impl CookieSessionInner { path: "/".to_owned(), domain: None, secure: true, + http_only: true, max_age: None, same_site: None, } @@ -388,7 +390,7 @@ impl CookieSessionInner { let mut cookie = Cookie::new(self.name.clone(), value); cookie.set_path(self.path.clone()); cookie.set_secure(self.secure); - cookie.set_http_only(true); + cookie.set_http_only(self.http_only); if let Some(ref domain) = self.domain { cookie.set_domain(domain.clone()); @@ -532,6 +534,12 @@ impl CookieSessionBackend { self } + /// Sets the `http_only` field in the session cookie being built. + pub fn http_only(mut self, value: bool) -> CookieSessionBackend { + Rc::get_mut(&mut self.0).unwrap().http_only = value; + self + } + /// Sets the `same_site` field in the session cookie being built. pub fn same_site(mut self, value: SameSite) -> CookieSessionBackend { Rc::get_mut(&mut self.0).unwrap().same_site = Some(value);