1
0
Fork 0
mirror of https://github.com/actix/actix-web.git synced 2024-12-17 21:56:38 +00:00

cookie session implementation

This commit is contained in:
Nikolay Kim 2017-11-26 17:30:35 -08:00
parent 53ce186294
commit 32483735ba
14 changed files with 247 additions and 70 deletions

View file

@ -1,4 +1,6 @@
#![allow(unused_variables)] #![allow(unused_variables)]
#![cfg_attr(feature="cargo-clippy", allow(needless_pass_by_value))]
extern crate actix; extern crate actix;
extern crate actix_web; extern crate actix_web;
extern crate env_logger; extern crate env_logger;
@ -6,17 +8,27 @@ extern crate futures;
use actix_web::*; use actix_web::*;
use actix_web::error::Result; use actix_web::error::Result;
use actix_web::middlewares::RequestSession;
use futures::stream::{once, Once}; use futures::stream::{once, Once};
/// somple handle /// somple handle
fn index(req: &mut HttpRequest, mut _payload: Payload, state: &()) -> HttpResponse { fn index(req: &mut HttpRequest, mut _payload: Payload, state: &()) -> Result<HttpResponse> {
println!("{:?}", req); println!("{:?}", req);
if let Ok(ch) = _payload.readany() { if let Ok(ch) = _payload.readany() {
if let futures::Async::Ready(Some(d)) = ch { if let futures::Async::Ready(Some(d)) = ch {
println!("{}", String::from_utf8_lossy(d.0.as_ref())); println!("{}", String::from_utf8_lossy(d.0.as_ref()));
} }
} }
httpcodes::HTTPOk.into()
// session
if let Some(count) = req.session().get::<i32>("counter")? {
println!("SESSION value: {}", count);
req.session().set("counter", count+1)?;
} else {
req.session().set("counter", 1)?;
}
Ok(httpcodes::HTTPOk.into())
} }
/// somple handle /// somple handle
@ -51,6 +63,12 @@ fn main() {
Application::default("/") Application::default("/")
// enable logger // enable logger
.middleware(middlewares::Logger::default()) .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 // register simple handle r, handle all methods
.handler("/index.html", index) .handler("/index.html", index)
// with path parameters // with path parameters

View file

@ -72,7 +72,7 @@ fn main() {
let sys = actix::System::new("ws-example"); let sys = actix::System::new("ws-example");
HttpServer::new( HttpServer::new(
Application::builder("/", AppState{counter: Cell::new(0)}) Application::build("/", AppState{counter: Cell::new(0)})
// enable logger // enable logger
.middleware(middlewares::Logger::default()) .middleware(middlewares::Logger::default())
// websocket route // websocket route

View file

@ -1,5 +1,4 @@
use std::rc::Rc; use std::rc::Rc;
use std::string::ToString;
use std::collections::HashMap; use std::collections::HashMap;
use task::Task; use task::Task;
@ -58,11 +57,11 @@ impl<S: 'static> HttpHandler for Application<S> {
impl Application<()> { impl Application<()> {
/// Create default `ApplicationBuilder` with no state /// Create default `ApplicationBuilder` with no state
pub fn default<T: ToString>(prefix: T) -> ApplicationBuilder<()> { pub fn default<T: Into<String>>(prefix: T) -> ApplicationBuilder<()> {
ApplicationBuilder { ApplicationBuilder {
parts: Some(ApplicationBuilderParts { parts: Some(ApplicationBuilderParts {
state: (), state: (),
prefix: prefix.to_string(), prefix: prefix.into(),
default: Resource::default_not_found(), default: Resource::default_not_found(),
handlers: HashMap::new(), handlers: HashMap::new(),
resources: HashMap::new(), resources: HashMap::new(),
@ -77,11 +76,11 @@ impl<S> Application<S> where S: 'static {
/// Create application builder with specific state. State is shared with all /// Create application builder with specific state. State is shared with all
/// routes within same application and could be /// routes within same application and could be
/// accessed with `HttpContext::state()` method. /// accessed with `HttpContext::state()` method.
pub fn builder<T: ToString>(prefix: T, state: S) -> ApplicationBuilder<S> { pub fn build<T: Into<String>>(prefix: T, state: S) -> ApplicationBuilder<S> {
ApplicationBuilder { ApplicationBuilder {
parts: Some(ApplicationBuilderParts { parts: Some(ApplicationBuilderParts {
state: state, state: state,
prefix: prefix.to_string(), prefix: prefix.into(),
default: Resource::default_not_found(), default: Resource::default_not_found(),
handlers: HashMap::new(), handlers: HashMap::new(),
resources: HashMap::new(), resources: HashMap::new(),
@ -100,7 +99,7 @@ struct ApplicationBuilderParts<S> {
middlewares: Vec<Box<Middleware>>, middlewares: Vec<Box<Middleware>>,
} }
/// Application builder /// Structure that follows the builder pattern for building `Application` structs.
pub struct ApplicationBuilder<S=()> { pub struct ApplicationBuilder<S=()> {
parts: Option<ApplicationBuilderParts<S>>, parts: Option<ApplicationBuilderParts<S>>,
} }
@ -158,14 +157,14 @@ impl<S> ApplicationBuilder<S> where S: 'static {
/// .finish(); /// .finish();
/// } /// }
/// ``` /// ```
pub fn resource<F, P: ToString>(&mut self, path: P, f: F) -> &mut Self pub fn resource<F, P: Into<String>>(&mut self, path: P, f: F) -> &mut Self
where F: FnOnce(&mut Resource<S>) + 'static where F: FnOnce(&mut Resource<S>) + 'static
{ {
{ {
let parts = self.parts.as_mut().expect("Use after finish"); let parts = self.parts.as_mut().expect("Use after finish");
// add resource // add resource
let path = path.to_string(); let path = path.into();
if !parts.resources.contains_key(&path) { if !parts.resources.contains_key(&path) {
check_pattern(&path); check_pattern(&path);
parts.resources.insert(path.clone(), Resource::default()); parts.resources.insert(path.clone(), Resource::default());
@ -208,21 +207,21 @@ impl<S> ApplicationBuilder<S> where S: 'static {
pub fn handler<P, F, R>(&mut self, path: P, handler: F) -> &mut Self pub fn handler<P, F, R>(&mut self, path: P, handler: F) -> &mut Self
where F: Fn(&mut HttpRequest, Payload, &S) -> R + 'static, where F: Fn(&mut HttpRequest, Payload, &S) -> R + 'static,
R: Into<HttpResponse> + 'static, R: Into<HttpResponse> + 'static,
P: ToString, P: Into<String>,
{ {
self.parts.as_mut().expect("Use after finish") 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 self
} }
/// Add path handler /// Add path handler
pub fn route_handler<H, P>(&mut self, path: P, h: H) -> &mut Self pub fn route_handler<H, P>(&mut self, path: P, h: H) -> &mut Self
where H: RouteHandler<S> + 'static, P: ToString where H: RouteHandler<S> + 'static, P: Into<String>
{ {
{ {
// add resource // add resource
let parts = self.parts.as_mut().expect("Use after finish"); 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) { if parts.handlers.contains_key(&path) {
panic!("Handler already registered: {:?}", path); panic!("Handler already registered: {:?}", path);
} }

View file

@ -69,7 +69,7 @@ impl<'a> From<&'a str> for ContentEncoding {
pub(crate) enum PayloadType { pub(crate) enum PayloadType {
Sender(PayloadSender), Sender(PayloadSender),
Encoding(EncodedPayload), Encoding(Box<EncodedPayload>),
} }
impl PayloadType { impl PayloadType {
@ -89,7 +89,7 @@ impl PayloadType {
match enc { match enc {
ContentEncoding::Auto | ContentEncoding::Identity => ContentEncoding::Auto | ContentEncoding::Identity =>
PayloadType::Sender(sender), PayloadType::Sender(sender),
_ => PayloadType::Encoding(EncodedPayload::new(sender, enc)), _ => PayloadType::Encoding(Box::new(EncodedPayload::new(sender, enc))),
} }
} }
} }

View file

@ -29,7 +29,7 @@ use httpcodes::{HTTPBadRequest, HTTPMethodNotAllowed, HTTPExpectationFailed};
/// is otherwise a direct mapping to `Result`. /// is otherwise a direct mapping to `Result`.
pub type Result<T> = result::Result<T, Error>; pub type Result<T> = result::Result<T, Error>;
/// Actix web error /// General purpose actix web error
#[derive(Debug)] #[derive(Debug)]
pub struct Error { pub struct Error {
cause: Box<ErrorResponse>, cause: Box<ErrorResponse>,

View file

@ -51,7 +51,6 @@ pub(crate) struct Http1<T: AsyncWrite + 'static, H: 'static> {
struct Entry { struct Entry {
task: Pipeline, task: Pipeline,
//req: UnsafeCell<HttpRequest>,
eof: bool, eof: bool,
error: bool, error: bool,
finished: bool, finished: bool,
@ -105,7 +104,6 @@ impl<T, H> Http1<T, H>
return Err(()) return Err(())
} }
// this is anoying
match item.task.poll_io(&mut self.stream) { match item.task.poll_io(&mut self.stream) {
Ok(Async::Ready(ready)) => { Ok(Async::Ready(ready)) => {
not_ready = false; not_ready = false;

View file

@ -82,7 +82,6 @@ impl<T, H> Http2<T, H>
item.poll_payload(); item.poll_payload();
if !item.eof { if !item.eof {
//let req = unsafe {item.req.get().as_mut().unwrap()};
match item.task.poll_io(&mut item.stream) { match item.task.poll_io(&mut item.stream) {
Ok(Async::Ready(ready)) => { Ok(Async::Ready(ready)) => {
item.eof = true; item.eof = true;

View file

@ -141,7 +141,7 @@ impl HttpRequest {
} }
/// Load cookies /// Load cookies
pub fn load_cookies(&mut self) -> Result<&Vec<Cookie>, CookieParseError> pub fn load_cookies(&mut self) -> Result<&Vec<Cookie<'static>>, CookieParseError>
{ {
if !self.cookies_loaded { if !self.cookies_loaded {
self.cookies_loaded = true; self.cookies_loaded = true;

View file

@ -8,8 +8,8 @@ use httpresponse::HttpResponse;
mod logger; mod logger;
mod session; mod session;
pub use self::logger::Logger; pub use self::logger::Logger;
pub use self::session::{RequestSession, Session, SessionImpl, pub use self::session::{RequestSession, Session, SessionImpl, SessionBackend, SessionStorage,
SessionBackend, SessionStorage, CookieSessionBackend}; CookieSessionError, CookieSessionBackend, CookieSessionBackendBuilder};
/// Middleware start result /// Middleware start result
pub enum Started { pub enum Started {

View file

@ -3,7 +3,10 @@
use std::any::Any; use std::any::Any;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use std::collections::HashMap;
use serde_json; use serde_json;
use serde_json::error::Error as JsonError;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use http::header::{self, HeaderValue}; use http::header::{self, HeaderValue};
use cookie::{CookieJar, Cookie, Key}; use cookie::{CookieJar, Cookie, Key};
@ -157,63 +160,160 @@ impl SessionImpl for DummySessionImpl {
/// Session that uses signed cookies as session storage /// Session that uses signed cookies as session storage
pub struct CookieSession { pub struct CookieSession {
jar: CookieJar, changed: bool,
key: Rc<Key>, state: HashMap<String, String>,
inner: Rc<CookieSessionInner>,
} }
/// 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 { impl SessionImpl for CookieSession {
fn get(&self, key: &str) -> Option<&str> { fn get(&self, key: &str) -> Option<&str> {
unimplemented!() if let Some(s) = self.state.get(key) {
} Some(s)
} else {
fn set(&mut self, key: &str, value: String) { None
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);
} }
} }
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 { fn write(&self, mut resp: HttpResponse) -> Response {
for cookie in self.jar.delta() { if self.changed {
match HeaderValue::from_str(&cookie.to_string()) { let _ = self.inner.set_cookie(&mut resp, &self.state);
Err(err) => return Response::Err(err.into()),
Ok(val) => resp.headers.append(header::SET_COOKIE, val),
};
} }
Response::Done(resp) Response::Done(resp)
} }
} }
struct CookieSessionInner {
key: Key,
name: String,
path: String,
domain: Option<String>,
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<String, String>) -> 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<String, String> {
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. /// 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`. /// 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. /// 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). /// 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. /// Constructor panics if key length is less than 32 bytes.
pub struct CookieSessionBackend { pub struct CookieSessionBackend(Rc<CookieSessionInner>);
key: Rc<Key>,
}
impl CookieSessionBackend { impl CookieSessionBackend {
/// Construct new `CookieSessionBackend` instance. /// Construct new `CookieSessionBackend` instance.
/// ///
/// Panics if key length is less than 32 bytes. /// Panics if key length is less than 32 bytes.
pub fn new(key: &[u8]) -> Self { pub fn new(key: &[u8]) -> CookieSessionBackend {
CookieSessionBackend { CookieSessionBackend(
key: Rc::new(Key::from_master(key)), 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<CookieSession, Error>; type ReadFuture = FutureResult<CookieSession, Error>;
fn from_request(&self, req: &mut HttpRequest) -> Self::ReadFuture { 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<S: Into<String>>(mut self, value: S) -> CookieSessionBackendBuilder {
self.0.path = value.into();
self
}
/// Sets the `domain` field in the session cookie being built.
pub fn domain<S: Into<String>>(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))
} }
} }

View file

@ -55,9 +55,7 @@ impl Pipeline {
st.disconnected(), st.disconnected(),
PipelineState::Handle(ref mut st) => PipelineState::Handle(ref mut st) =>
st.task.disconnected(), st.task.disconnected(),
PipelineState::Task(ref mut st) => PipelineState::Task(ref mut st) | PipelineState::Error(ref mut st) =>
st.0.disconnected(),
PipelineState::Error(ref mut st) =>
st.0.disconnected(), st.0.disconnected(),
_ =>(), _ =>(),
} }

View file

@ -1,5 +1,4 @@
use std::rc::Rc; use std::rc::Rc;
use std::string::ToString;
use std::collections::HashMap; use std::collections::HashMap;
use regex::{Regex, RegexSet, Captures}; use regex::{Regex, RegexSet, Captures};
@ -25,7 +24,7 @@ impl<T> Default for RouteRecognizer<T> {
impl<T> RouteRecognizer<T> { impl<T> RouteRecognizer<T> {
pub fn new<P: ToString, U>(prefix: P, routes: U) -> Self pub fn new<P: Into<String>, U>(prefix: P, routes: U) -> Self
where U: IntoIterator<Item=(String, T)> where U: IntoIterator<Item=(String, T)>
{ {
let mut paths = Vec::new(); let mut paths = Vec::new();
@ -38,7 +37,7 @@ impl<T> RouteRecognizer<T> {
let regset = RegexSet::new(&paths); let regset = RegexSet::new(&paths);
RouteRecognizer { RouteRecognizer {
prefix: prefix.to_string().len() - 1, prefix: prefix.into().len() - 1,
patterns: regset.unwrap(), patterns: regset.unwrap(),
routes: handlers, routes: handlers,
} }
@ -56,8 +55,8 @@ impl<T> RouteRecognizer<T> {
self.routes = handlers; self.routes = handlers;
} }
pub fn set_prefix<P: ToString>(&mut self, prefix: P) { pub fn set_prefix<P: Into<String>>(&mut self, prefix: P) {
let p = prefix.to_string(); let p = prefix.into();
if p.ends_with('/') { if p.ends_with('/') {
self.prefix = p.len() - 1; self.prefix = p.len() - 1;
} else { } else {
@ -105,7 +104,7 @@ impl Pattern {
None => return None, 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 { impl Params {
pub(crate) fn new(names: Rc<HashMap<String, usize>>, pub(crate) fn new(names: Rc<HashMap<String, usize>>,
text: &str, text: &str,
captures: Captures) -> Self captures: &Captures) -> Self
{ {
Params { Params {
names, names,

View file

@ -1,5 +1,4 @@
use std::rc::Rc; use std::rc::Rc;
use std::convert::From;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::collections::HashMap; use std::collections::HashMap;
@ -60,8 +59,8 @@ impl<S> Resource<S> where S: 'static {
} }
/// Set resource name /// Set resource name
pub fn set_name<T: ToString>(&mut self, name: T) { pub fn set_name<T: Into<String>>(&mut self, name: T) {
self.name = name.to_string(); self.name = name.into();
} }
/// Register handler for specified method. /// Register handler for specified method.
@ -136,7 +135,6 @@ impl<S: 'static> RouteHandler<S> for Resource<S> {
} }
} }
#[cfg_attr(feature="cargo-clippy", allow(large_enum_variant))] #[cfg_attr(feature="cargo-clippy", allow(large_enum_variant))]
enum ReplyItem<A> where A: Actor + Route { enum ReplyItem<A> where A: Actor + Route {
Message(HttpResponse), Message(HttpResponse),

View file

@ -72,7 +72,7 @@ impl StaticFiles {
} }
} }
fn index(&self, relpath: &str, filename: PathBuf) -> Result<HttpResponse, io::Error> { fn index(&self, relpath: &str, filename: &PathBuf) -> Result<HttpResponse, io::Error> {
let index_of = format!("Index of {}/{}", self.prefix, relpath); let index_of = format!("Index of {}/{}", self.prefix, relpath);
let mut body = String::new(); let mut body = String::new();
@ -169,7 +169,7 @@ impl<S: 'static> RouteHandler<S> for StaticFiles {
}; };
if filename.is_dir() { if filename.is_dir() {
match self.index(&filepath[idx..], filename) { match self.index(&filepath[idx..], &filename) {
Ok(resp) => Task::reply(resp), Ok(resp) => Task::reply(resp),
Err(err) => match err.kind() { Err(err) => match err.kind() {
io::ErrorKind::NotFound => Task::reply(HTTPNotFound), io::ErrorKind::NotFound => Task::reply(HTTPNotFound),