From 134863d5c828fad574d75fa40347a7cfc379a5b4 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 9 Mar 2019 18:04:40 -0800 Subject: [PATCH] move middlewares --- errhandlers.rs | 141 +++++++++++++++++ identity.rs | 399 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 540 insertions(+) create mode 100644 errhandlers.rs create mode 100644 identity.rs diff --git a/errhandlers.rs b/errhandlers.rs new file mode 100644 index 00000000..c7d19d33 --- /dev/null +++ b/errhandlers.rs @@ -0,0 +1,141 @@ +use std::collections::HashMap; + +use error::Result; +use http::StatusCode; +use httprequest::HttpRequest; +use httpresponse::HttpResponse; +use middleware::{Middleware, Response}; + +type ErrorHandler = Fn(&HttpRequest, HttpResponse) -> Result; + +/// `Middleware` for allowing custom handlers for responses. +/// +/// You can use `ErrorHandlers::handler()` method to register a custom error +/// handler for specific status code. You can modify existing response or +/// create completely new one. +/// +/// ## Example +/// +/// ```rust +/// # extern crate actix_web; +/// use actix_web::middleware::{ErrorHandlers, Response}; +/// use actix_web::{http, App, HttpRequest, HttpResponse, Result}; +/// +/// fn render_500(_: &HttpRequest, resp: HttpResponse) -> Result { +/// let mut builder = resp.into_builder(); +/// builder.header(http::header::CONTENT_TYPE, "application/json"); +/// Ok(Response::Done(builder.into())) +/// } +/// +/// fn main() { +/// let app = App::new() +/// .middleware( +/// ErrorHandlers::new() +/// .handler(http::StatusCode::INTERNAL_SERVER_ERROR, render_500), +/// ) +/// .resource("/test", |r| { +/// r.method(http::Method::GET).f(|_| HttpResponse::Ok()); +/// r.method(http::Method::HEAD) +/// .f(|_| HttpResponse::MethodNotAllowed()); +/// }) +/// .finish(); +/// } +/// ``` +pub struct ErrorHandlers { + handlers: HashMap>>, +} + +impl Default for ErrorHandlers { + fn default() -> Self { + ErrorHandlers { + handlers: HashMap::new(), + } + } +} + +impl ErrorHandlers { + /// Construct new `ErrorHandlers` instance + pub fn new() -> Self { + ErrorHandlers::default() + } + + /// Register error handler for specified status code + pub fn handler(mut self, status: StatusCode, handler: F) -> Self + where + F: Fn(&HttpRequest, HttpResponse) -> Result + 'static, + { + self.handlers.insert(status, Box::new(handler)); + self + } +} + +impl Middleware for ErrorHandlers { + fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { + if let Some(handler) = self.handlers.get(&resp.status()) { + handler(req, resp) + } else { + Ok(Response::Done(resp)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use error::{Error, ErrorInternalServerError}; + use http::header::CONTENT_TYPE; + use http::StatusCode; + use httpmessage::HttpMessage; + use middleware::Started; + use test::{self, TestRequest}; + + fn render_500(_: &HttpRequest, resp: HttpResponse) -> Result { + let mut builder = resp.into_builder(); + builder.header(CONTENT_TYPE, "0001"); + Ok(Response::Done(builder.into())) + } + + #[test] + fn test_handler() { + let mw = + ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); + + let mut req = TestRequest::default().finish(); + let resp = HttpResponse::InternalServerError().finish(); + let resp = match mw.response(&mut req, resp) { + Ok(Response::Done(resp)) => resp, + _ => panic!(), + }; + assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); + + let resp = HttpResponse::Ok().finish(); + let resp = match mw.response(&mut req, resp) { + Ok(Response::Done(resp)) => resp, + _ => panic!(), + }; + assert!(!resp.headers().contains_key(CONTENT_TYPE)); + } + + struct MiddlewareOne; + + impl Middleware for MiddlewareOne { + fn start(&self, _: &HttpRequest) -> Result { + Err(ErrorInternalServerError("middleware error")) + } + } + + #[test] + fn test_middleware_start_error() { + let mut srv = test::TestServer::new(move |app| { + app.middleware( + ErrorHandlers::new() + .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500), + ).middleware(MiddlewareOne) + .handler(|_| HttpResponse::Ok()) + }); + + let request = srv.get().finish().unwrap(); + let response = srv.execute(request.send()).unwrap(); + assert_eq!(response.headers().get(CONTENT_TYPE).unwrap(), "0001"); + } +} diff --git a/identity.rs b/identity.rs new file mode 100644 index 00000000..a664ba1f --- /dev/null +++ b/identity.rs @@ -0,0 +1,399 @@ +//! Request identity service for Actix applications. +//! +//! [**IdentityService**](struct.IdentityService.html) middleware can be +//! used with different policies types to store identity information. +//! +//! By 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::middleware::identity::RequestIdentity; +//! use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; +//! use actix_web::*; +//! +//! 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, SameSite}; +use futures::future::{err as FutErr, ok as FutOk, FutureResult}; +use futures::Future; +use time::Duration; + +use error::{Error, Result}; +use http::header::{self, HeaderValue}; +use httprequest::HttpRequest; +use httpresponse::HttpResponse; +use middleware::{Middleware, Response, Started}; + +/// The helper trait to obtain your identity from a request. +/// +/// ```rust +/// use actix_web::middleware::identity::RequestIdentity; +/// use actix_web::*; +/// +/// 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; + + /// Remember identity. + fn remember(&self, identity: String); + + /// This method is used to 'forget' the current identity on subsequent + /// requests. + fn forget(&self); +} + +impl RequestIdentity for HttpRequest { + fn identity(&self) -> Option { + if let Some(id) = self.extensions().get::() { + return id.0.identity().map(|s| s.to_owned()); + } + None + } + + fn remember(&self, identity: String) { + if let Some(id) = self.extensions_mut().get_mut::() { + return id.0.as_mut().remember(identity); + } + } + + fn forget(&self) { + if let Some(id) = self.extensions_mut().get_mut::() { + return id.0.forget(); + } + } +} + +/// An identity +pub trait Identity: 'static { + /// 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, key: String); + + /// This method is used to 'forget' the current identity on subsequent + /// requests. + fn forget(&mut self); + + /// Write session to storage backend. + fn write(&mut self, resp: HttpResponse) -> Result; +} + +/// Identity policy definition. +pub trait IdentityPolicy: Sized + 'static { + /// The associated identity + type Identity: Identity; + + /// The return type of the middleware + type Future: Future; + + /// Parse the session from request and load data from a service identity. + fn from_request(&self, request: &HttpRequest) -> Self::Future; +} + +/// Request identity middleware +/// +/// ```rust +/// # extern crate actix_web; +/// use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService}; +/// use actix_web::App; +/// +/// 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); + +impl> Middleware for IdentityService { + fn start(&self, req: &HttpRequest) -> Result { + let req = req.clone(); + let fut = self.backend.from_request(&req).then(move |res| match res { + Ok(id) => { + req.extensions_mut().insert(IdentityBox(Box::new(id))); + FutOk(None) + } + Err(err) => FutErr(err), + }); + Ok(Started::Future(Box::new(fut))) + } + + fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { + if let Some(ref mut id) = req.extensions_mut().get_mut::() { + id.0.as_mut().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, + same_site: 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, + same_site: 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); + } + + if let Some(same_site) = self.same_site { + cookie.set_same_site(same_site); + } + + 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: &HttpRequest) -> Option { + if let Ok(cookies) = req.cookies() { + for cookie in cookies.iter() { + 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::middleware::identity::{CookieIdentityPolicy, IdentityService}; +/// use actix_web::App; +/// +/// 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 + } + + /// Sets the `same_site` field in the session cookie being built. + pub fn same_site(mut self, same_site: SameSite) -> Self { + Rc::get_mut(&mut self.0).unwrap().same_site = Some(same_site); + self + } +} + +impl IdentityPolicy for CookieIdentityPolicy { + type Identity = CookieIdentity; + type Future = FutureResult; + + fn from_request(&self, req: &HttpRequest) -> Self::Future { + let identity = self.0.load(req); + FutOk(CookieIdentity { + identity, + changed: false, + inner: Rc::clone(&self.0), + }) + } +}