From 32483735ba3e2c95367e0838ac7dd04918130674 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sun, 26 Nov 2017 17:30:35 -0800 Subject: [PATCH] cookie session implementation --- examples/basic.rs | 22 +++- examples/state.rs | 2 +- src/application.rs | 23 ++-- src/encoding.rs | 4 +- src/error.rs | 2 +- src/h1.rs | 2 - src/h2.rs | 1 - src/httprequest.rs | 2 +- src/middlewares/mod.rs | 4 +- src/middlewares/session.rs | 228 ++++++++++++++++++++++++++++++++----- src/pipeline.rs | 4 +- src/recognizer.rs | 13 +-- src/resource.rs | 6 +- src/staticfiles.rs | 4 +- 14 files changed, 247 insertions(+), 70 deletions(-) diff --git a/examples/basic.rs b/examples/basic.rs index bf740dc23..c83d9b329 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,4 +1,6 @@ #![allow(unused_variables)] +#![cfg_attr(feature="cargo-clippy", allow(needless_pass_by_value))] + extern crate actix; extern crate actix_web; extern crate env_logger; @@ -6,17 +8,27 @@ extern crate futures; use actix_web::*; use actix_web::error::Result; +use actix_web::middlewares::RequestSession; use futures::stream::{once, Once}; /// somple handle -fn index(req: &mut HttpRequest, mut _payload: Payload, state: &()) -> HttpResponse { +fn index(req: &mut HttpRequest, mut _payload: Payload, state: &()) -> Result { println!("{:?}", req); if let Ok(ch) = _payload.readany() { if let futures::Async::Ready(Some(d)) = ch { println!("{}", String::from_utf8_lossy(d.0.as_ref())); } } - httpcodes::HTTPOk.into() + + // session + if let Some(count) = req.session().get::("counter")? { + println!("SESSION value: {}", count); + req.session().set("counter", count+1)?; + } else { + req.session().set("counter", 1)?; + } + + Ok(httpcodes::HTTPOk.into()) } /// somple handle @@ -51,6 +63,12 @@ fn main() { Application::default("/") // enable logger .middleware(middlewares::Logger::default()) + // cookie session middleware + .middleware(middlewares::SessionStorage::new( + middlewares::CookieSessionBackend::build(&[0; 32]) + .secure(false) + .finish() + )) // register simple handle r, handle all methods .handler("/index.html", index) // with path parameters diff --git a/examples/state.rs b/examples/state.rs index 3b7bba380..7844288a1 100644 --- a/examples/state.rs +++ b/examples/state.rs @@ -72,7 +72,7 @@ fn main() { let sys = actix::System::new("ws-example"); HttpServer::new( - Application::builder("/", AppState{counter: Cell::new(0)}) + Application::build("/", AppState{counter: Cell::new(0)}) // enable logger .middleware(middlewares::Logger::default()) // websocket route diff --git a/src/application.rs b/src/application.rs index 6c64e2307..b6cf27a88 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,5 +1,4 @@ use std::rc::Rc; -use std::string::ToString; use std::collections::HashMap; use task::Task; @@ -58,11 +57,11 @@ impl HttpHandler for Application { impl Application<()> { /// Create default `ApplicationBuilder` with no state - pub fn default(prefix: T) -> ApplicationBuilder<()> { + pub fn default>(prefix: T) -> ApplicationBuilder<()> { ApplicationBuilder { parts: Some(ApplicationBuilderParts { state: (), - prefix: prefix.to_string(), + prefix: prefix.into(), default: Resource::default_not_found(), handlers: HashMap::new(), resources: HashMap::new(), @@ -77,11 +76,11 @@ impl Application where S: 'static { /// Create application builder with specific state. State is shared with all /// routes within same application and could be /// accessed with `HttpContext::state()` method. - pub fn builder(prefix: T, state: S) -> ApplicationBuilder { + pub fn build>(prefix: T, state: S) -> ApplicationBuilder { ApplicationBuilder { parts: Some(ApplicationBuilderParts { state: state, - prefix: prefix.to_string(), + prefix: prefix.into(), default: Resource::default_not_found(), handlers: HashMap::new(), resources: HashMap::new(), @@ -100,7 +99,7 @@ struct ApplicationBuilderParts { middlewares: Vec>, } -/// Application builder +/// Structure that follows the builder pattern for building `Application` structs. pub struct ApplicationBuilder { parts: Option>, } @@ -158,14 +157,14 @@ impl ApplicationBuilder where S: 'static { /// .finish(); /// } /// ``` - pub fn resource(&mut self, path: P, f: F) -> &mut Self + pub fn resource>(&mut self, path: P, f: F) -> &mut Self where F: FnOnce(&mut Resource) + 'static { { let parts = self.parts.as_mut().expect("Use after finish"); // add resource - let path = path.to_string(); + let path = path.into(); if !parts.resources.contains_key(&path) { check_pattern(&path); parts.resources.insert(path.clone(), Resource::default()); @@ -208,21 +207,21 @@ impl ApplicationBuilder where S: 'static { pub fn handler(&mut self, path: P, handler: F) -> &mut Self where F: Fn(&mut HttpRequest, Payload, &S) -> R + 'static, R: Into + 'static, - P: ToString, + P: Into, { self.parts.as_mut().expect("Use after finish") - .handlers.insert(path.to_string(), Box::new(FnHandler::new(handler))); + .handlers.insert(path.into(), Box::new(FnHandler::new(handler))); self } /// Add path handler pub fn route_handler(&mut self, path: P, h: H) -> &mut Self - where H: RouteHandler + 'static, P: ToString + where H: RouteHandler + 'static, P: Into { { // add resource let parts = self.parts.as_mut().expect("Use after finish"); - let path = path.to_string(); + let path = path.into(); if parts.handlers.contains_key(&path) { panic!("Handler already registered: {:?}", path); } diff --git a/src/encoding.rs b/src/encoding.rs index 27138ff73..7bdbaea49 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -69,7 +69,7 @@ impl<'a> From<&'a str> for ContentEncoding { pub(crate) enum PayloadType { Sender(PayloadSender), - Encoding(EncodedPayload), + Encoding(Box), } impl PayloadType { @@ -89,7 +89,7 @@ impl PayloadType { match enc { ContentEncoding::Auto | ContentEncoding::Identity => PayloadType::Sender(sender), - _ => PayloadType::Encoding(EncodedPayload::new(sender, enc)), + _ => PayloadType::Encoding(Box::new(EncodedPayload::new(sender, enc))), } } } diff --git a/src/error.rs b/src/error.rs index ea977505d..f3b1507d2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -29,7 +29,7 @@ use httpcodes::{HTTPBadRequest, HTTPMethodNotAllowed, HTTPExpectationFailed}; /// is otherwise a direct mapping to `Result`. pub type Result = result::Result; -/// Actix web error +/// General purpose actix web error #[derive(Debug)] pub struct Error { cause: Box, diff --git a/src/h1.rs b/src/h1.rs index 6fdb5ea7c..39f5e6afe 100644 --- a/src/h1.rs +++ b/src/h1.rs @@ -51,7 +51,6 @@ pub(crate) struct Http1 { struct Entry { task: Pipeline, - //req: UnsafeCell, eof: bool, error: bool, finished: bool, @@ -105,7 +104,6 @@ impl Http1 return Err(()) } - // this is anoying match item.task.poll_io(&mut self.stream) { Ok(Async::Ready(ready)) => { not_ready = false; diff --git a/src/h2.rs b/src/h2.rs index 95fd6d65c..572843b8c 100644 --- a/src/h2.rs +++ b/src/h2.rs @@ -82,7 +82,6 @@ impl Http2 item.poll_payload(); if !item.eof { - //let req = unsafe {item.req.get().as_mut().unwrap()}; match item.task.poll_io(&mut item.stream) { Ok(Async::Ready(ready)) => { item.eof = true; diff --git a/src/httprequest.rs b/src/httprequest.rs index 4f3caaa1e..bde2e34d0 100644 --- a/src/httprequest.rs +++ b/src/httprequest.rs @@ -141,7 +141,7 @@ impl HttpRequest { } /// Load cookies - pub fn load_cookies(&mut self) -> Result<&Vec, CookieParseError> + pub fn load_cookies(&mut self) -> Result<&Vec>, CookieParseError> { if !self.cookies_loaded { self.cookies_loaded = true; diff --git a/src/middlewares/mod.rs b/src/middlewares/mod.rs index 6699edab6..66913fa45 100644 --- a/src/middlewares/mod.rs +++ b/src/middlewares/mod.rs @@ -8,8 +8,8 @@ use httpresponse::HttpResponse; mod logger; mod session; pub use self::logger::Logger; -pub use self::session::{RequestSession, Session, SessionImpl, - SessionBackend, SessionStorage, CookieSessionBackend}; +pub use self::session::{RequestSession, Session, SessionImpl, SessionBackend, SessionStorage, + CookieSessionError, CookieSessionBackend, CookieSessionBackendBuilder}; /// Middleware start result pub enum Started { diff --git a/src/middlewares/session.rs b/src/middlewares/session.rs index 79f70804e..c89817f91 100644 --- a/src/middlewares/session.rs +++ b/src/middlewares/session.rs @@ -3,7 +3,10 @@ use std::any::Any; use std::rc::Rc; use std::sync::Arc; +use std::collections::HashMap; + use serde_json; +use serde_json::error::Error as JsonError; use serde::{Serialize, Deserialize}; use http::header::{self, HeaderValue}; use cookie::{CookieJar, Cookie, Key}; @@ -157,63 +160,160 @@ impl SessionImpl for DummySessionImpl { /// Session that uses signed cookies as session storage pub struct CookieSession { - jar: CookieJar, - key: Rc, + changed: bool, + state: HashMap, + inner: Rc, } +/// Errors that can occure during handling cookie session +#[derive(Fail, Debug)] +pub enum CookieSessionError { + /// Size of the serialized session is greater than 4000 bytes. + #[fail(display="Size of the serialized session is greater than 4000 bytes.")] + Overflow, + /// Fail to serialize session. + #[fail(display="Fail to serialize session")] + Serialize(JsonError), +} + +impl ErrorResponse for CookieSessionError {} + impl SessionImpl for CookieSession { fn get(&self, key: &str) -> Option<&str> { - unimplemented!() - } - - fn set(&mut self, key: &str, value: String) { - unimplemented!() - } - - fn remove(&mut self, key: &str) { - unimplemented!() - } - - fn clear(&mut self) { - let cookies: Vec<_> = self.jar.iter().cloned().collect(); - for cookie in cookies { - self.jar.remove(cookie); + if let Some(s) = self.state.get(key) { + Some(s) + } else { + None } } + fn set(&mut self, key: &str, value: String) { + self.changed = true; + self.state.insert(key.to_owned(), value); + } + + fn remove(&mut self, key: &str) { + self.changed = true; + self.state.remove(key); + } + + fn clear(&mut self) { + self.changed = true; + self.state.clear() + } + fn write(&self, mut resp: HttpResponse) -> Response { - for cookie in self.jar.delta() { - match HeaderValue::from_str(&cookie.to_string()) { - Err(err) => return Response::Err(err.into()), - Ok(val) => resp.headers.append(header::SET_COOKIE, val), - }; + if self.changed { + let _ = self.inner.set_cookie(&mut resp, &self.state); } Response::Done(resp) } } +struct CookieSessionInner { + key: Key, + name: String, + path: String, + domain: Option, + secure: bool, + http_only: bool, +} + +impl CookieSessionInner { + + fn new(key: &[u8]) -> CookieSessionInner { + CookieSessionInner { + key: Key::from_master(key), + name: "actix_session".to_owned(), + path: "/".to_owned(), + domain: None, + secure: true, + http_only: true } + } + + fn set_cookie(&self, resp: &mut HttpResponse, state: &HashMap) -> Result<()> { + let value = serde_json::to_string(&state) + .map_err(CookieSessionError::Serialize)?; + if value.len() > 4064 { + return Err(CookieSessionError::Overflow.into()) + } + + let mut cookie = Cookie::new(self.name.clone(), value); + cookie.set_path(self.path.clone()); + cookie.set_secure(self.secure); + cookie.set_http_only(self.http_only); + + if let Some(ref domain) = self.domain { + cookie.set_domain(domain.clone()); + } + + let mut jar = CookieJar::new(); + jar.signed(&self.key).add(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) -> HashMap { + if let Ok(cookies) = req.load_cookies() { + for cookie in cookies { + if cookie.name() == self.name { + let mut jar = CookieJar::new(); + jar.add_original(cookie.clone()); + if let Some(cookie) = jar.signed(&self.key).get(&self.name) { + if let Ok(val) = serde_json::from_str(cookie.value()) { + return val; + } + } + } + } + } + HashMap::new() + } +} + /// Use signed cookies as session storage. /// +/// `CookieSessionBackend` creates sessions which are limited to storing +/// fewer than 4000 bytes of data (as the payload must fit into a single cookie). +/// Internal server error get generated if session contains more than 4000 bytes. +/// /// You need to pass a random value to the constructor of `CookieSessionBackend`. /// This is private key for cookie session, When this value is changed, all session data is lost. /// /// Note that whatever you write into your session is visible by the user (but not modifiable). /// /// Constructor panics if key length is less than 32 bytes. -pub struct CookieSessionBackend { - key: Rc, -} +pub struct CookieSessionBackend(Rc); impl CookieSessionBackend { /// Construct new `CookieSessionBackend` instance. /// /// Panics if key length is less than 32 bytes. - pub fn new(key: &[u8]) -> Self { - CookieSessionBackend { - key: Rc::new(Key::from_master(key)), - } + pub fn new(key: &[u8]) -> CookieSessionBackend { + CookieSessionBackend( + Rc::new(CookieSessionInner::new(key))) + } + + /// Creates a new `CookieSessionBackendBuilder` instance from the given key. + /// + /// Panics if key length is less than 32 bytes. + /// + /// # Example + /// + /// ``` + /// use actix_web::middlewares::CookieSessionBackend; + /// + /// let backend = CookieSessionBackend::build(&[0; 32]).finish(); + /// ``` + pub fn build(key: &[u8]) -> CookieSessionBackendBuilder { + CookieSessionBackendBuilder::new(key) } } @@ -223,6 +323,74 @@ impl SessionBackend for CookieSessionBackend { type ReadFuture = FutureResult; fn from_request(&self, req: &mut HttpRequest) -> Self::ReadFuture { - unimplemented!() + let state = self.0.load(req); + FutOk( + CookieSession { + changed: false, + state: state, + inner: Rc::clone(&self.0), + }) + } +} + +/// Structure that follows the builder pattern for building `CookieSessionBackend` structs. +/// +/// To construct a backend: +/// +/// 1. Call [`CookieSessionBackend::build`](struct.CookieSessionBackend.html#method.build) to start building. +/// 2. Use any of the builder methods to set fields in the backend. +/// 3. Call [finish](#method.finish) to retrieve the constructed backend. +/// +/// # Example +/// +/// ```rust +/// # extern crate actix_web; +/// +/// use actix_web::middlewares::CookieSessionBackend; +/// +/// # fn main() { +/// let backend: CookieSessionBackend = CookieSessionBackend::build(&[0; 32]) +/// .domain("www.rust-lang.org") +/// .path("/") +/// .secure(true) +/// .http_only(true) +/// .finish(); +/// # } +/// ``` +pub struct CookieSessionBackendBuilder(CookieSessionInner); + +impl CookieSessionBackendBuilder { + pub fn new(key: &[u8]) -> CookieSessionBackendBuilder { + CookieSessionBackendBuilder( + CookieSessionInner::new(key)) + } + + /// Sets the `path` field in the session cookie being built. + pub fn path>(mut self, value: S) -> CookieSessionBackendBuilder { + self.0.path = value.into(); + self + } + + /// Sets the `domain` field in the session cookie being built. + pub fn domain>(mut self, value: S) -> CookieSessionBackendBuilder { + self.0.domain = Some(value.into()); + self + } + + /// Sets the `secure` field in the session cookie being built. + pub fn secure(mut self, value: bool) -> CookieSessionBackendBuilder { + self.0.secure = value; + self + } + + /// Sets the `http_only` field in the session cookie being built. + pub fn http_only(mut self, value: bool) -> CookieSessionBackendBuilder { + self.0.http_only = value; + self + } + + /// Finishes building and returns the built `CookieSessionBackend`. + pub fn finish(self) -> CookieSessionBackend { + CookieSessionBackend(Rc::new(self.0)) } } diff --git a/src/pipeline.rs b/src/pipeline.rs index aa9979910..d6bc5fff4 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -55,9 +55,7 @@ impl Pipeline { st.disconnected(), PipelineState::Handle(ref mut st) => st.task.disconnected(), - PipelineState::Task(ref mut st) => - st.0.disconnected(), - PipelineState::Error(ref mut st) => + PipelineState::Task(ref mut st) | PipelineState::Error(ref mut st) => st.0.disconnected(), _ =>(), } diff --git a/src/recognizer.rs b/src/recognizer.rs index 71acfd4ac..480e5a89a 100644 --- a/src/recognizer.rs +++ b/src/recognizer.rs @@ -1,5 +1,4 @@ use std::rc::Rc; -use std::string::ToString; use std::collections::HashMap; use regex::{Regex, RegexSet, Captures}; @@ -25,7 +24,7 @@ impl Default for RouteRecognizer { impl RouteRecognizer { - pub fn new(prefix: P, routes: U) -> Self + pub fn new, U>(prefix: P, routes: U) -> Self where U: IntoIterator { let mut paths = Vec::new(); @@ -38,7 +37,7 @@ impl RouteRecognizer { let regset = RegexSet::new(&paths); RouteRecognizer { - prefix: prefix.to_string().len() - 1, + prefix: prefix.into().len() - 1, patterns: regset.unwrap(), routes: handlers, } @@ -56,8 +55,8 @@ impl RouteRecognizer { self.routes = handlers; } - pub fn set_prefix(&mut self, prefix: P) { - let p = prefix.to_string(); + pub fn set_prefix>(&mut self, prefix: P) { + let p = prefix.into(); if p.ends_with('/') { self.prefix = p.len() - 1; } else { @@ -105,7 +104,7 @@ impl Pattern { None => return None, }; - Some(Params::new(Rc::clone(&self.names), text, captures)) + Some(Params::new(Rc::clone(&self.names), text, &captures)) } } @@ -176,7 +175,7 @@ pub struct Params { impl Params { pub(crate) fn new(names: Rc>, text: &str, - captures: Captures) -> Self + captures: &Captures) -> Self { Params { names, diff --git a/src/resource.rs b/src/resource.rs index 3e1341501..ceeba4707 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -1,5 +1,4 @@ use std::rc::Rc; -use std::convert::From; use std::marker::PhantomData; use std::collections::HashMap; @@ -60,8 +59,8 @@ impl Resource where S: 'static { } /// Set resource name - pub fn set_name(&mut self, name: T) { - self.name = name.to_string(); + pub fn set_name>(&mut self, name: T) { + self.name = name.into(); } /// Register handler for specified method. @@ -136,7 +135,6 @@ impl RouteHandler for Resource { } } - #[cfg_attr(feature="cargo-clippy", allow(large_enum_variant))] enum ReplyItem where A: Actor + Route { Message(HttpResponse), diff --git a/src/staticfiles.rs b/src/staticfiles.rs index b7e1a80ab..00222fed7 100644 --- a/src/staticfiles.rs +++ b/src/staticfiles.rs @@ -72,7 +72,7 @@ impl StaticFiles { } } - fn index(&self, relpath: &str, filename: PathBuf) -> Result { + fn index(&self, relpath: &str, filename: &PathBuf) -> Result { let index_of = format!("Index of {}/{}", self.prefix, relpath); let mut body = String::new(); @@ -169,7 +169,7 @@ impl RouteHandler for StaticFiles { }; if filename.is_dir() { - match self.index(&filepath[idx..], filename) { + match self.index(&filepath[idx..], &filename) { Ok(resp) => Task::reply(resp), Err(err) => match err.kind() { io::ErrorKind::NotFound => Task::reply(HTTPNotFound),