diff --git a/CHANGES.md b/CHANGES.md index c2f73fe2a..1bc29ae19 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,8 @@ ## 0.5.4 (2018-04-xx) +* Add identity service middleware + * Middleware response() is not invoked if there was an error in async handler #187 diff --git a/Cargo.toml b/Cargo.toml index adb1060b3..73a9c4530 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "0.5.3" +version = "0.5.4" authors = ["Nikolay Kim "] description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." readme = "README.md" diff --git a/src/httprequest.rs b/src/httprequest.rs index 08e8f2bc1..e917b5c82 100644 --- a/src/httprequest.rs +++ b/src/httprequest.rs @@ -201,6 +201,19 @@ impl HttpRequest { &mut self.as_mut().extensions } + /// Request extensions + #[inline] + #[doc(hidden)] + pub fn extensions_ro(&self) -> &Extensions { + &self.as_ref().extensions + } + + /// Mutable refernece to a the request's extensions + #[inline] + pub fn extensions_mut(&mut self) -> &mut Extensions { + &mut self.as_mut().extensions + } + /// Default `CpuPool` #[inline] #[doc(hidden)] diff --git a/src/middleware/cors.rs b/src/middleware/cors.rs index b99e1a8b9..243ea1e80 100644 --- a/src/middleware/cors.rs +++ b/src/middleware/cors.rs @@ -7,8 +7,8 @@ //! //! 1. Call [`Cors::build`](struct.Cors.html#method.build) to start building. //! 2. Use any of the builder methods to set fields in the backend. -//! 3. Call [finish](struct.Cors.html#method.finish) to retrieve the -//! constructed backend. +//! 3. Call [finish](struct.Cors.html#method.finish) to retrieve the +//! constructed backend. //! //! Cors middleware could be used as parameter for `App::middleware()` or //! `ResourceHandler::middleware()` methods. But you have to use diff --git a/src/middleware/identity.rs b/src/middleware/identity.rs new file mode 100644 index 000000000..f88b3f979 --- /dev/null +++ b/src/middleware/identity.rs @@ -0,0 +1,391 @@ +//! Request identity service for Actix applications. +//! +//! [**IdentityService**](struct.IdentityService.html) middleware can be +//! used with different policies types to store identity information. +//! +//! Bu default, only cookie identity policy is implemented. Other backend implementations +//! can be added separately. +//! +//! [**CookieIdentityPolicy**](struct.CookieIdentityPolicy.html) +//! uses cookies as identity storage. +//! +//! To access current request identity +//! [**RequestIdentity**](trait.RequestIdentity.html) should be used. +//! *HttpRequest* implements *RequestIdentity* trait. +//! +//! ```rust +//! use actix_web::*; +//! use actix_web::middleware::identity::RequestIdentity; +//! use actix_web::middleware::identity::{IdentityService, CookieIdentityPolicy}; +//! +//! fn index(req: HttpRequest) -> Result { +//! // access request identity +//! if let Some(id) = req.identity() { +//! Ok(format!("Welcome! {}", id)) +//! } else { +//! Ok("Welcome Anonymous!".to_owned()) +//! } +//! } +//! +//! fn login(mut req: HttpRequest) -> HttpResponse { +//! req.remember("User1".to_owned()); // <- remember identity +//! HttpResponse::Ok().finish() +//! } +//! +//! fn logout(mut req: HttpRequest) -> HttpResponse { +//! req.forget(); // <- remove identity +//! HttpResponse::Ok().finish() +//! } +//! +//! fn main() { +//! let app = App::new().middleware( +//! IdentityService::new( // <- create identity middleware +//! CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend +//! .name("auth-cookie") +//! .secure(false)) +//! ); +//! } +//! ``` +use std::rc::Rc; + +use cookie::{Cookie, CookieJar, Key}; +use futures::Future; +use futures::future::{FutureResult, err as FutErr, ok as FutOk}; +use time::Duration; + +use httprequest::HttpRequest; +use httpresponse::HttpResponse; +use error::{Error, Result}; +use http::header::{self, HeaderValue}; +use middleware::{Middleware, Response, Started}; + + +/// The helper trait to obtain your identity from a request. +/// +/// ```rust +/// use actix_web::*; +/// use actix_web::middleware::identity::RequestIdentity; +/// +/// fn index(req: HttpRequest) -> Result { +/// // access request identity +/// if let Some(id) = req.identity() { +/// Ok(format!("Welcome! {}", id)) +/// } else { +/// Ok("Welcome Anonymous!".to_owned()) +/// } +/// } +/// +/// fn login(mut req: HttpRequest) -> HttpResponse { +/// req.remember("User1".to_owned()); // <- remember identity +/// HttpResponse::Ok().finish() +/// } +/// +/// fn logout(mut req: HttpRequest) -> HttpResponse { +/// req.forget(); // <- remove identity +/// HttpResponse::Ok().finish() +/// } +/// # fn main() {} +/// ``` +pub trait RequestIdentity { + + /// Return the claimed identity of the user associated request or + /// ``None`` if no identity can be found associated with the request. + fn identity(&self) -> Option<&str>; + + /// Remember identity. + fn remember(&mut self, identity: String); + + /// This method is used to 'forget' the current identity on subsequent requests. + fn forget(&mut self); +} + +impl RequestIdentity for HttpRequest { + fn identity(&self) -> Option<&str> { + if let Some(id) = self.extensions_ro().get::() { + return id.0.identity() + } + None + } + + fn remember(&mut self, identity: String) { + if let Some(id) = self.extensions_mut().get_mut::() { + return id.0.remember(identity) + } + } + + fn forget(&mut self) { + if let Some(id) = self.extensions_mut().get_mut::() { + return id.0.forget() + } + } +} + +/// An identity +pub trait Identity: 'static { + + fn identity(&self) -> Option<&str>; + + fn remember(&mut self, key: String); + + fn forget(&mut self); + + /// Write session to storage backend. + fn write(&mut self, resp: HttpResponse) -> Result; +} + +/// Identity policy definition. +pub trait IdentityPolicy: Sized + 'static { + type Identity: Identity; + type Future: Future; + + /// Parse the session from request and load data from a service identity. + fn from_request(&self, request: &mut HttpRequest) -> Self::Future; +} + +/// Request identity middleware +/// +/// ```rust +/// # extern crate actix; +/// # extern crate actix_web; +/// use actix_web::App; +/// use actix_web::middleware::identity::{IdentityService, CookieIdentityPolicy}; +/// +/// fn main() { +/// let app = App::new().middleware( +/// IdentityService::new( // <- create identity middleware +/// CookieIdentityPolicy::new(&[0; 32]) // <- create cookie session backend +/// .name("auth-cookie") +/// .secure(false)) +/// ); +/// } +/// ``` +pub struct IdentityService { + backend: T, +} + +impl IdentityService { + /// Create new identity service with specified backend. + pub fn new(backend: T) -> Self { + IdentityService { backend } + } +} + +struct IdentityBox(Box); + +#[doc(hidden)] +unsafe impl Send for IdentityBox {} +#[doc(hidden)] +unsafe impl Sync for IdentityBox {} + + +impl> Middleware for IdentityService { + fn start(&self, req: &mut HttpRequest) -> Result { + let mut req = req.clone(); + + let fut = self.backend + .from_request(&mut req) + .then(move |res| match res { + Ok(id) => { + req.extensions().insert(IdentityBox(Box::new(id))); + FutOk(None) + } + Err(err) => FutErr(err), + }); + Ok(Started::Future(Box::new(fut))) + } + + fn response(&self, req: &mut HttpRequest, resp: HttpResponse) -> Result { + if let Some(mut id) = req.extensions().remove::() { + id.0.write(resp) + } else { + Ok(Response::Done(resp)) + } + } +} + +#[doc(hidden)] +/// Identity that uses private cookies as identity storage. +pub struct CookieIdentity { + changed: bool, + identity: Option, + inner: Rc, +} + +impl Identity for CookieIdentity { + fn identity(&self) -> Option<&str> { + self.identity.as_ref().map(|s| s.as_ref()) + } + + fn remember(&mut self, value: String) { + self.changed = true; + self.identity = Some(value); + } + + fn forget(&mut self) { + self.changed = true; + self.identity = None; + } + + fn write(&mut self, mut resp: HttpResponse) -> Result { + if self.changed { + let _ = self.inner.set_cookie(&mut resp, self.identity.take()); + } + Ok(Response::Done(resp)) + } +} + +struct CookieIdentityInner { + key: Key, + name: String, + path: String, + domain: Option, + secure: bool, + max_age: Option, +} + +impl CookieIdentityInner { + fn new(key: &[u8]) -> CookieIdentityInner { + CookieIdentityInner { + key: Key::from_master(key), + name: "actix-identity".to_owned(), + path: "/".to_owned(), + domain: None, + secure: true, + max_age: None, + } + } + + fn set_cookie(&self, resp: &mut HttpResponse, id: Option) -> Result<()> { + let some = id.is_some(); + { + let id = id.unwrap_or_else(String::new); + let mut cookie = Cookie::new(self.name.clone(), id); + cookie.set_path(self.path.clone()); + cookie.set_secure(self.secure); + cookie.set_http_only(true); + + if let Some(ref domain) = self.domain { + cookie.set_domain(domain.clone()); + } + + if let Some(max_age) = self.max_age { + cookie.set_max_age(max_age); + } + + let mut jar = CookieJar::new(); + if some { + jar.private(&self.key).add(cookie); + } else { + jar.add_original(cookie.clone()); + jar.private(&self.key).remove(cookie); + } + + for cookie in jar.delta() { + let val = HeaderValue::from_str(&cookie.to_string())?; + resp.headers_mut().append(header::SET_COOKIE, val); + } + } + + Ok(()) + } + + fn load(&self, req: &mut HttpRequest) -> Option { + if let Ok(cookies) = req.cookies() { + for cookie in cookies { + if cookie.name() == self.name { + let mut jar = CookieJar::new(); + jar.add_original(cookie.clone()); + + let cookie_opt = jar.private(&self.key).get(&self.name); + if let Some(cookie) = cookie_opt { + return Some(cookie.value().into()) + } + } + } + } + None + } +} + +/// Use cookies for request identity storage. +/// +/// The constructors take a key as an argument. +/// This is the private key for cookie - when this value is changed, +/// all identities are lost. The constructors will panic if the key is less +/// than 32 bytes in length. +/// +/// # Example +/// +/// ```rust +/// # extern crate actix_web; +/// use actix_web::App; +/// use actix_web::middleware::identity::{IdentityService, CookieIdentityPolicy}; +/// +/// fn main() { +/// let app = App::new().middleware( +/// IdentityService::new( // <- create identity middleware +/// CookieIdentityPolicy::new(&[0; 32]) // <- construct cookie policy +/// .domain("www.rust-lang.org") +/// .name("actix_auth") +/// .path("/") +/// .secure(true))); +/// } +/// ``` +pub struct CookieIdentityPolicy(Rc); + +impl CookieIdentityPolicy { + + /// Construct new `CookieIdentityPolicy` instance. + /// + /// Panics if key length is less than 32 bytes. + pub fn new(key: &[u8]) -> CookieIdentityPolicy { + CookieIdentityPolicy(Rc::new(CookieIdentityInner::new(key))) + } + + /// Sets the `path` field in the session cookie being built. + pub fn path>(mut self, value: S) -> CookieIdentityPolicy { + Rc::get_mut(&mut self.0).unwrap().path = value.into(); + self + } + + /// Sets the `name` field in the session cookie being built. + pub fn name>(mut self, value: S) -> CookieIdentityPolicy { + Rc::get_mut(&mut self.0).unwrap().name = value.into(); + self + } + + /// Sets the `domain` field in the session cookie being built. + pub fn domain>(mut self, value: S) -> CookieIdentityPolicy { + Rc::get_mut(&mut self.0).unwrap().domain = Some(value.into()); + self + } + + /// Sets the `secure` field in the session cookie being built. + /// + /// If the `secure` field is set, a cookie will only be transmitted when the + /// connection is secure - i.e. `https` + pub fn secure(mut self, value: bool) -> CookieIdentityPolicy { + Rc::get_mut(&mut self.0).unwrap().secure = value; + self + } + + /// Sets the `max-age` field in the session cookie being built. + pub fn max_age(mut self, value: Duration) -> CookieIdentityPolicy { + Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); + self + } +} + +impl IdentityPolicy for CookieIdentityPolicy { + type Identity = CookieIdentity; + type Future = FutureResult; + + fn from_request(&self, req: &mut HttpRequest) -> Self::Future { + let identity = self.0.load(req); + FutOk(CookieIdentity { + identity, + changed: false, + inner: Rc::clone(&self.0), + }) + } +} diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index d41660eeb..d38e3054c 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -9,6 +9,7 @@ mod logger; pub mod cors; pub mod csrf; +pub mod identity; mod defaultheaders; mod errhandlers; #[cfg(feature = "session")]