diff --git a/.travis.yml b/.travis.yml index 84057bb8..4745203d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,6 +37,7 @@ script: cd async_db && cargo check && cd .. cd async_ex1 && cargo check && cd .. cd basics && cargo check && cd .. + cd complex-middleware && cargo check && cd .. cd cookie-auth && cargo check && cd .. cd cookie-session && cargo check && cd .. cd diesel && cargo check && cd .. diff --git a/Cargo.lock b/Cargo.lock index c6c7bee4..42e1faf1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -366,6 +366,18 @@ dependencies = [ "bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "complex-middleware" +version = "0.1.0" +dependencies = [ + "actix 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "actix-web 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "cookie 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.23 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "cookie" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index c78478ca..4fe74db9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "async_db", "async_ex1", "basics", + "complex-middleware", "cookie-auth", "cookie-session", "diesel", diff --git a/complex-middleware/Cargo.toml b/complex-middleware/Cargo.toml new file mode 100644 index 00000000..0c192984 --- /dev/null +++ b/complex-middleware/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "complex-middleware" +version = "0.1.0" +authors = ["Nikolay Kim "] +workspace = "../" + +[dependencies] +actix = "0.7" +actix-web = "0.7" + +cookie = { version="0.10", features=["percent-encode", "secure"] } +futures = "0.1" +time = "0.1" +env_logger = "0.5" diff --git a/complex-middleware/src/auth.rs b/complex-middleware/src/auth.rs new file mode 100644 index 00000000..56ec54e4 --- /dev/null +++ b/complex-middleware/src/auth.rs @@ -0,0 +1,274 @@ +#![allow(dead_code)] +use std::rc::Rc; + +use cookie::{Cookie, CookieJar, Key}; +use futures::future::{err as FutErr, ok as FutOk, FutureResult}; +use futures::Future; +use time::Duration; + +use actix_web::http::header::{self, HeaderValue}; +use actix_web::middleware::{Middleware, Response, Started}; +use actix_web::{Error, HttpRequest, HttpResponse, Result}; + +/// Trait provides identity service for the request. +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 { + 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; +} + +/// Middleware that implements identity service +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: &HttpRequest) -> Result { + let mut req = req.clone(); + + let fut = self + .backend + .from_request(&mut 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(mut id) = req.extensions_mut().remove::() { + id.0.write(resp) + } else { + Ok(Response::Done(resp)) + } + } +} + +/// 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.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. +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/complex-middleware/src/main.rs b/complex-middleware/src/main.rs new file mode 100644 index 00000000..f7a0c09c --- /dev/null +++ b/complex-middleware/src/main.rs @@ -0,0 +1,49 @@ +extern crate actix; +extern crate actix_web; +extern crate cookie; +extern crate env_logger; +extern crate futures; +extern crate time; + +use actix_web::{middleware, server, App, HttpRequest, HttpResponse}; + +mod auth; +use auth::{CookieIdentityPolicy, IdentityService, RequestIdentity}; + +fn index(req: &HttpRequest) -> String { + format!("Hello {}", req.identity().unwrap_or("Anonymous".to_owned())) +} + +fn login(req: &HttpRequest) -> HttpResponse { + req.remember("user1".to_owned()); + HttpResponse::Found().header("location", "/").finish() +} + +fn logout(req: &HttpRequest) -> HttpResponse { + req.forget(); + HttpResponse::Found().header("location", "/").finish() +} + +fn main() { + ::std::env::set_var("RUST_LOG", "actix_web=info"); + env_logger::init(); + let sys = actix::System::new("cookie-auth"); + + server::new(|| { + App::new() + .middleware(middleware::Logger::default()) + .middleware(IdentityService::new( + CookieIdentityPolicy::new(&[0; 32]) + .name("auth-example") + .secure(false), + )) + .resource("/login", |r| r.f(login)) + .resource("/logout", |r| r.f(logout)) + .resource("/", |r| r.f(index)) + }).bind("127.0.0.1:8080") + .unwrap() + .start(); + + println!("Started http server: 127.0.0.1:8080"); + let _ = sys.run(); +}