diff --git a/Cargo.toml b/Cargo.toml index e88f30859..f47f47606 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,12 +2,23 @@ name = "actix-redis" version = "0.1.0" authors = ["Nikolay Kim "] +description = "Redis integration for actix web framework" +license = "MIT/Apache-2.0" +readme = "README.md" +keywords = ["http", "web", "framework", "async", "actix"] +homepage = "https://github.com/actix/actix-redis" +repository = "https://github.com/actix/actix-redis.git" +documentation = "https://docs.rs/actix-redis/" +categories = ["network-programming", "asynchronous"] +exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] [lib] name = "actix_redis" path = "src/lib.rs" [dependencies] +rand = "0.3" +http = "0.1" bytes = "0.4" failure = "^0.1.1" futures = "0.1" @@ -16,5 +27,10 @@ serde_json = "1.0" tokio-io = "0.1" tokio-core = "0.1" actix = "^0.3.5" -actix-web = { path = "../actix-web/" } -redis-async = { git = "https://github.com/benashford/redis-async-rs.git" } +redis-async = "0.0" +cookie = { version="0.10", features=["percent-encode", "secure"] } + +actix-web = { git = "https://github.com/actix/actix-web.git" } + +[dev-dependencies] +env_logger = "0.4" diff --git a/README.md b/README.md new file mode 100644 index 000000000..53cbcc354 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Actix redis + +## Redis session backend + +Use redis as session storage. + +You need to pass an address of the redis server and random value to the +constructor of `RedisSessionBackend`. 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. + +```rust,ignore +# extern crate actix; +# extern crate actix_web; +# use actix_web::*; +use actix_web::middleware::SessionStorage; +use actix_redis::RedisSessionBackend; + +fn main() { + ::std::env::set_var("RUST_LOG", "actix_web=info"); + let _ = env_logger::init(); + let sys = actix::System::new("basic-example"); + + HttpServer::new( + || Application::new() + // enable logger + .middleware(middleware::Logger::default()) + // cookie session middleware + .middleware(SessionStorage::new( + RedisSessionBackend::new("127.0.0.1:6379", &[0; 32]) + .expect("Can not connect to redis server") + )) + // register simple route, handle all methods + .resource("/", |r| r.f(index))) + .bind("0.0.0.0:8080").unwrap() + .start(); + + let _ = sys.run(); +} +``` diff --git a/example/basic.rs b/examples/basic.rs similarity index 80% rename from example/basic.rs rename to examples/basic.rs index c6dbd93e4..53e6bd3b3 100644 --- a/example/basic.rs +++ b/examples/basic.rs @@ -15,11 +15,6 @@ use actix_redis::RedisSessionBackend; /// simple handler fn index(mut req: HttpRequest) -> Result { println!("{:?}", req); - if let Ok(ch) = req.payload_mut().readany().poll() { - if let futures::Async::Ready(Some(d)) = ch { - println!("{}", String::from_utf8_lossy(d.as_ref())); - } - } // session if let Some(count) = req.session().get::("counter")? { @@ -43,14 +38,14 @@ fn main() { .middleware(middleware::Logger::default()) // cookie session middleware .middleware(middleware::SessionStorage::new( - RedisSessionBackend::new("127.0.0.1:6379", Duration::from_secs(7200)) + RedisSessionBackend::new("127.0.0.1:6379", &[0; 32]) .expect("Can not connect to redis server") )) // register simple route, handle all methods .resource("/", |r| r.f(index))) .bind("0.0.0.0:8080").unwrap() + .threads(1) .start(); - println!("Starting http server: 127.0.0.1:8080"); let _ = sys.run(); } diff --git a/src/lib.rs b/src/lib.rs index 3cf9edbb8..38665b500 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,12 @@ extern crate actix; extern crate actix_web; extern crate bytes; +extern crate cookie; extern crate futures; extern crate serde; extern crate serde_json; +extern crate rand; +extern crate http; extern crate tokio_io; extern crate tokio_core; #[macro_use] @@ -14,4 +17,5 @@ extern crate failure; mod redis; mod session; +pub use redis::RedisActor; pub use session::RedisSessionBackend; diff --git a/src/redis.rs b/src/redis.rs index ba7420e92..6ecbec22a 100644 --- a/src/redis.rs +++ b/src/redis.rs @@ -3,7 +3,6 @@ use std::collections::VecDeque; use bytes::BytesMut; use futures::Future; -use futures::future::Either; use futures::unsync::oneshot; use tokio_core::net::TcpStream; use tokio_io::codec::{Decoder, Encoder}; @@ -108,7 +107,7 @@ impl Handler for RedisActor { fn handle(&mut self, msg: Command, ctx: &mut Self::Context) -> Response { let (tx, rx) = oneshot::channel(); self.queue.push_back(tx); - ctx.send(Value(msg.0)); + let _ = ctx.send(Value(msg.0)); Self::async_reply( rx.map_err(|_| io::Error::new(io::ErrorKind::Other, "").into()) diff --git a/src/session.rs b/src/session.rs index 49a4b0c48..9db88f092 100644 --- a/src/session.rs +++ b/src/session.rs @@ -1,12 +1,16 @@ use std::{io, net}; use std::rc::Rc; -use std::time::Duration; +use std::iter::FromIterator; use std::collections::HashMap; use serde_json; +use rand::{self, Rng}; use futures::Future; use futures::future::{Either, ok as FutOk, err as FutErr}; use tokio_core::net::TcpStream; +use redis_async::resp::RespValue; +use cookie::{CookieJar, Cookie, Key}; +use http::header::{self, HeaderValue}; use actix::prelude::*; use actix_web::{error, Error, HttpRequest, HttpResponse}; use actix_web::middleware::{SessionImpl, SessionBackend, Response as MiddlewareResponse}; @@ -19,6 +23,7 @@ pub struct RedisSession { changed: bool, inner: Rc, state: HashMap, + value: Option, } impl SessionImpl for RedisSession { @@ -46,27 +51,44 @@ impl SessionImpl for RedisSession { self.state.clear() } - fn write(&self, mut resp: HttpResponse) -> MiddlewareResponse { + fn write(&self, resp: HttpResponse) -> MiddlewareResponse { if self.changed { - let _ = self.inner.update(&self.state); + MiddlewareResponse::Future(self.inner.update(&self.state, resp, self.value.as_ref())) + } else { + MiddlewareResponse::Done(resp) } - MiddlewareResponse::Done(resp) } } +/// Use redis as session storage. +/// +/// You need to pass an address of the redis server and random value to the +/// constructor of `RedisSessionBackend`. 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 RedisSessionBackend(Rc); impl RedisSessionBackend { /// Create new redis session backend - pub fn new(addr: S, ttl: Duration) -> io::Result { + /// + /// * `addr` - address of the redis server + pub fn new(addr: S, key: &[u8]) -> io::Result { let h = Arbiter::handle(); let mut err = None; for addr in addr.to_socket_addrs()? { - match TcpStream::connect(&addr, &h).wait() { + match net::TcpStream::connect(&addr) { Err(e) => err = Some(e), Ok(conn) => { - let addr = RedisActor::start(conn); - return Ok(RedisSessionBackend(Rc::new(Inner{ttl: ttl, addr: addr}))); + let addr = RedisActor::start( + TcpStream::from_stream(conn, h).expect("Can not create tcp stream")); + return Ok(RedisSessionBackend( + Rc::new(Inner{key: Key::from_master(key), + ttl: "7200".to_owned(), + addr: addr, + name: "actix-session".to_owned()}))); }, } } @@ -76,6 +98,17 @@ impl RedisSessionBackend { Err(io::Error::new(io::ErrorKind::Other, "Can not connect to redis server.")) } } + + /// Set time to live in seconds for session value + pub fn ttl(mut self, ttl: u16) -> Self { + Rc::get_mut(&mut self.0).unwrap().ttl = format!("{}", ttl); + self + } + + pub fn cookie_name(mut self, name: &str) -> Self { + Rc::get_mut(&mut self.0).unwrap().name = name.to_owned(); + self + } } impl SessionBackend for RedisSessionBackend { @@ -87,17 +120,19 @@ impl SessionBackend for RedisSessionBackend { let inner = Rc::clone(&self.0); Box::new(self.0.load(req).map(move |state| { - if let Some(state) = state { + if let Some((state, value)) = state { RedisSession { changed: false, inner: inner, state: state, + value: Some(value), } } else { RedisSession { changed: false, inner: inner, state: HashMap::new(), + value: None, } } })) @@ -105,49 +140,99 @@ impl SessionBackend for RedisSessionBackend { } struct Inner { - ttl: Duration, + key: Key, + ttl: String, + name: String, addr: Address, } impl Inner { fn load(&self, req: &mut HttpRequest) - -> Box>, Error=Error>> - { + -> Box, String)>, Error=Error>> { if let Ok(cookies) = req.cookies() { for cookie in cookies { - if cookie.name() == "actix-session" { - return Box::new( - self.addr.call_fut(Command(resp_array!["GET", cookie.value()])) - .map_err(Error::from) - .and_then(|res| { - match res { - Ok(val) => { - println!("VAL {:?}", val); - Ok(Some(HashMap::new())) - }, - Err(err) => Err( - io::Error::new(io::ErrorKind::Other, "Error").into()) - } - })) + 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) { + let value = cookie.value().to_owned(); + return Box::new( + self.addr.call_fut(Command(resp_array!["GET", cookie.value()])) + .map_err(Error::from) + .and_then(move |res| { + match res { + Ok(val) => { + match val { + RespValue::Error(err) => + return Err( + error::ErrorInternalServerError(err).into()), + RespValue::SimpleString(s) => + if let Ok(val) = serde_json::from_str(&s) { + return Ok(Some((val, value))) + }, + RespValue::BulkString(s) => { + if let Ok(val) = serde_json::from_slice(&s) { + return Ok(Some((val, value))) + } + }, + _ => (), + } + Ok(None) + }, + Err(err) => Err(error::ErrorInternalServerError(err).into()) + } + })) + } else { + return Box::new(FutOk(None)) + } } } } Box::new(FutOk(None)) } - fn update(&self, state: &HashMap) -> Box> { + fn update(&self, state: &HashMap, + mut resp: HttpResponse, + value: Option<&String>) -> Box> + { + let (value, jar) = if let Some(value) = value { + (value.clone(), None) + } else { + let mut rng = rand::OsRng::new().unwrap(); + let value = String::from_iter(rng.gen_ascii_chars().take(32)); + + let mut cookie = Cookie::new(self.name.clone(), value.clone()); + cookie.set_path("/"); + cookie.set_http_only(true); + + // set cookie + let mut jar = CookieJar::new(); + jar.signed(&self.key).add(cookie); + + (value, Some(jar)) + }; + Box::new( match serde_json::to_string(state) { Err(e) => Either::A(FutErr(e.into())), Ok(body) => { Either::B( - self.addr.call_fut(Command(resp_array!["GET", "test"])) + self.addr.call_fut( + Command(resp_array!["SET", value, body,"EX", &self.ttl])) .map_err(Error::from) - .and_then(|res| { + .and_then(move |res| { match res { - Ok(val) => Ok(()), - Err(err) => Err( - error::ErrorInternalServerError(err).into()) + Ok(_) => { + if let Some(jar) = jar { + for cookie in jar.delta() { + let val = HeaderValue::from_str( + &cookie.to_string())?; + resp.headers_mut().append(header::SET_COOKIE, val); + } + } + Ok(resp) + }, + Err(err) => Err(error::ErrorInternalServerError(err).into()) } })) }