diff --git a/Cargo.toml b/Cargo.toml index f473ac554..2f69c7ef1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ path = "src/lib.rs" [workspace] members = [ ".", + "session", "staticfiles", ] diff --git a/session/Cargo.toml b/session/Cargo.toml new file mode 100644 index 000000000..3bbeb4f8c --- /dev/null +++ b/session/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "actix-session" +version = "0.1.0" +authors = ["Nikolay Kim "] +description = "Session for actix web framework." +readme = "README.md" +keywords = ["http", "web", "framework", "async", "futures"] +homepage = "https://actix.rs" +repository = "https://github.com/actix/actix-web.git" +documentation = "https://docs.rs/actix-web/" +license = "MIT/Apache-2.0" +exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] +workspace = ".." +edition = "2018" + +[lib] +name = "actix_session" +path = "src/lib.rs" + +[features] +default = ["cookie-session"] + +# sessions feature, session require "ring" crate and c compiler +cookie-session = ["cookie/secure"] + +[dependencies] +actix-web = { path=".." } +actix-codec = "0.1.0" + +#actix-service = "0.3.2" +#actix-utils = "0.3.1" +actix-service = { git = "https://github.com/actix/actix-net.git" } +actix-utils = { git = "https://github.com/actix/actix-net.git" } + +actix-http = { git = "https://github.com/actix/actix-http.git" } +actix-router = { git = "https://github.com/actix/actix-net.git" } +actix-server = { git = "https://github.com/actix/actix-net.git" } + +bytes = "0.4" +cookie = { version="0.11", features=["percent-encode"], optional=true } +derive_more = "0.14" +encoding = "0.2" +futures = "0.1" +hashbrown = "0.1.8" +log = "0.4" +serde = "1.0" +serde_json = "1.0" +time = "0.1" + +[dev-dependencies] +actix-rt = "0.1.0" diff --git a/session/src/cookie.rs b/session/src/cookie.rs new file mode 100644 index 000000000..9cde02e0c --- /dev/null +++ b/session/src/cookie.rs @@ -0,0 +1,360 @@ +//! Cookie session. +//! +//! [**CookieSession**](struct.CookieSession.html) +//! uses cookies as session storage. `CookieSession` creates sessions +//! which are limited to storing fewer than 4000 bytes of data, as the payload +//! must fit into a single cookie. An internal server error is generated if a +//! session contains more than 4000 bytes. +//! +//! A cookie may have a security policy of *signed* or *private*. Each has +//! a respective `CookieSession` constructor. +//! +//! A *signed* cookie may be viewed but not modified by the client. A *private* +//! cookie may neither be viewed nor modified by the client. +//! +//! The constructors take a key as an argument. This is the private key +//! for cookie session - when this value is changed, all session data is lost. + +use std::collections::HashMap; +use std::rc::Rc; + +use actix_service::{Service, Transform}; +use actix_web::http::{header::SET_COOKIE, HeaderValue}; +use actix_web::{Error, HttpMessage, ResponseError, ServiceRequest, ServiceResponse}; +use cookie::{Cookie, CookieJar, Key, SameSite}; +use derive_more::{Display, From}; +use futures::future::{ok, Future, FutureResult}; +use futures::Poll; +use serde_json::error::Error as JsonError; +use time::Duration; + +use crate::Session; + +/// Errors that can occur during handling cookie session +#[derive(Debug, From, Display)] +pub enum CookieSessionError { + /// Size of the serialized session is greater than 4000 bytes. + #[display(fmt = "Size of the serialized session is greater than 4000 bytes.")] + Overflow, + /// Fail to serialize session. + #[display(fmt = "Fail to serialize session")] + Serialize(JsonError), +} + +impl ResponseError for CookieSessionError {} + +enum CookieSecurity { + Signed, + Private, +} + +struct CookieSessionInner { + key: Key, + security: CookieSecurity, + name: String, + path: String, + domain: Option, + secure: bool, + http_only: bool, + max_age: Option, + same_site: Option, +} + +impl CookieSessionInner { + fn new(key: &[u8], security: CookieSecurity) -> CookieSessionInner { + CookieSessionInner { + security, + key: Key::from_master(key), + name: "actix-session".to_owned(), + path: "/".to_owned(), + domain: None, + secure: true, + http_only: true, + max_age: None, + same_site: None, + } + } + + fn set_cookie( + &self, + res: &mut ServiceResponse, + state: impl Iterator, + ) -> Result<(), Error> { + let state: HashMap = state.collect(); + 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()); + } + + 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(); + + match self.security { + CookieSecurity::Signed => jar.signed(&self.key).add(cookie), + CookieSecurity::Private => jar.private(&self.key).add(cookie), + } + + for cookie in jar.delta() { + let val = HeaderValue::from_str(&cookie.encoded().to_string())?; + res.headers_mut().append(SET_COOKIE, val); + } + + Ok(()) + } + + fn load

(&self, req: &ServiceRequest

) -> HashMap { + 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 = match self.security { + CookieSecurity::Signed => jar.signed(&self.key).get(&self.name), + CookieSecurity::Private => { + jar.private(&self.key).get(&self.name) + } + }; + if let Some(cookie) = cookie_opt { + if let Ok(val) = serde_json::from_str(cookie.value()) { + return val; + } + } + } + } + } + HashMap::new() + } +} + +/// Use cookies for session storage. +/// +/// `CookieSession` creates sessions which are limited to storing +/// fewer than 4000 bytes of data (as the payload must fit into a single +/// cookie). An Internal Server Error is generated if the session contains more +/// than 4000 bytes. +/// +/// A cookie may have a security policy of *signed* or *private*. Each has a +/// respective `CookieSessionBackend` constructor. +/// +/// A *signed* cookie is stored on the client as plaintext alongside +/// a signature such that the cookie may be viewed but not modified by the +/// client. +/// +/// A *private* cookie is stored on the client as encrypted text +/// such that it may neither be viewed nor modified by the client. +/// +/// The constructors take a key as an argument. +/// This is the private key for cookie session - when this value is changed, +/// all session data is lost. The constructors will panic if the key is less +/// than 32 bytes in length. +/// +/// The backend relies on `cookie` crate to create and read cookies. +/// By default all cookies are percent encoded, but certain symbols may +/// cause troubles when reading cookie, if they are not properly percent encoded. +/// +/// # Example +/// +/// ```rust +/// use actix_session::CookieSession; +/// use actix_web::{App, HttpResponse, HttpServer}; +/// +/// fn main() { +/// let app = App::new().middleware( +/// CookieSession::signed(&[0; 32]) +/// .domain("www.rust-lang.org") +/// .name("actix_session") +/// .path("/") +/// .secure(true)) +/// .resource("/", |r| r.to(|| HttpResponse::Ok())); +/// } +/// ``` +pub struct CookieSession(Rc); + +impl CookieSession { + /// Construct new *signed* `CookieSessionBackend` instance. + /// + /// Panics if key length is less than 32 bytes. + pub fn signed(key: &[u8]) -> CookieSession { + CookieSession(Rc::new(CookieSessionInner::new( + key, + CookieSecurity::Signed, + ))) + } + + /// Construct new *private* `CookieSessionBackend` instance. + /// + /// Panics if key length is less than 32 bytes. + pub fn private(key: &[u8]) -> CookieSession { + CookieSession(Rc::new(CookieSessionInner::new( + key, + CookieSecurity::Private, + ))) + } + + /// Sets the `path` field in the session cookie being built. + pub fn path>(mut self, value: S) -> CookieSession { + 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) -> CookieSession { + 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) -> CookieSession { + 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) -> CookieSession { + Rc::get_mut(&mut self.0).unwrap().secure = value; + self + } + + /// Sets the `http_only` field in the session cookie being built. + pub fn http_only(mut self, value: bool) -> CookieSession { + Rc::get_mut(&mut self.0).unwrap().http_only = value; + self + } + + /// Sets the `same_site` field in the session cookie being built. + pub fn same_site(mut self, value: SameSite) -> CookieSession { + Rc::get_mut(&mut self.0).unwrap().same_site = Some(value); + self + } + + /// Sets the `max-age` field in the session cookie being built. + pub fn max_age(mut self, value: Duration) -> CookieSession { + Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); + self + } +} + +impl Transform> for CookieSession +where + S: Service, Response = ServiceResponse>, + S::Future: 'static, + S::Error: 'static, +{ + type Response = ServiceResponse; + type Error = S::Error; + type InitError = (); + type Transform = CookieSessionMiddleware; + type Future = FutureResult; + + fn new_transform(&self, service: S) -> Self::Future { + ok(CookieSessionMiddleware { + service, + inner: self.0.clone(), + }) + } +} + +/// Cookie session middleware +pub struct CookieSessionMiddleware { + service: S, + inner: Rc, +} + +impl Service> for CookieSessionMiddleware +where + S: Service, Response = ServiceResponse>, + S::Future: 'static, + S::Error: 'static, +{ + type Response = ServiceResponse; + type Error = S::Error; + type Future = Box>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + //self.service.poll_ready().map_err(|e| e.into()) + self.service.poll_ready() + } + + fn call(&mut self, mut req: ServiceRequest

) -> Self::Future { + let inner = self.inner.clone(); + let state = self.inner.load(&req); + Session::set_session(state.into_iter(), &mut req); + + Box::new(self.service.call(req).map(move |mut res| { + if let Some(state) = Session::get_changes(&mut res) { + res.checked_expr(|res| inner.set_cookie(res, state)) + } else { + res + } + })) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use actix_web::{test, App}; + + #[test] + fn cookie_session() { + let mut app = test::init_service( + App::new() + .middleware(CookieSession::signed(&[0; 32]).secure(false)) + .resource("/", |r| { + r.to(|ses: Session| { + let _ = ses.set("counter", 100); + "test" + }) + }), + ); + + let request = test::TestRequest::get().to_request(); + let response = test::block_on(app.call(request)).unwrap(); + assert!(response + .cookies() + .find(|c| c.name() == "actix-session") + .is_some()); + } + + #[test] + fn cookie_session_extractor() { + let mut app = test::init_service( + App::new() + .middleware(CookieSession::signed(&[0; 32]).secure(false)) + .resource("/", |r| { + r.to(|ses: Session| { + let _ = ses.set("counter", 100); + "test" + }) + }), + ); + + let request = test::TestRequest::get().to_request(); + let response = test::block_on(app.call(request)).unwrap(); + assert!(response + .cookies() + .find(|c| c.name() == "actix-session") + .is_some()); + } +} diff --git a/session/src/lib.rs b/session/src/lib.rs index 0271a13f8..f57e11f2f 100644 --- a/session/src/lib.rs +++ b/session/src/lib.rs @@ -1,121 +1,61 @@ //! User sessions. //! -//! Actix provides a general solution for session management. The -//! [**SessionStorage**](struct.SessionStorage.html) -//! middleware can be used with different backend types to store session -//! data in different backends. +//! Actix provides a general solution for session management. Session +//! middlewares could provide different implementations which could +//! be accessed via general session api. //! //! By default, only cookie session backend is implemented. Other //! backend implementations can be added. //! -//! [**CookieSessionBackend**](struct.CookieSessionBackend.html) -//! uses 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. An internal server error is generated if a -//! session contains more than 4000 bytes. -//! -//! A cookie may have a security policy of *signed* or *private*. Each has -//! a respective `CookieSessionBackend` constructor. -//! -//! A *signed* cookie may be viewed but not modified by the client. A *private* -//! cookie may neither be viewed nor modified by the client. -//! -//! The constructors take a key as an argument. This is the private key -//! for cookie session - when this value is changed, all session data is lost. -//! -//! In general, you create a `SessionStorage` middleware and initialize it -//! with specific backend implementation, such as a `CookieSessionBackend`. -//! To access session data, -//! [*HttpRequest::session()*](trait.RequestSession.html#tymethod.session) -//! must be used. This method returns a -//! [*Session*](struct.Session.html) object, which allows us to get or set -//! session data. +//! In general, you insert a *session* middleware and initialize it +//! , such as a `CookieSessionBackend`. To access session data, +//! [*Session*](struct.Session.html) extractor must be used. Session +//! extractor allows us to get or set session data. //! //! ```rust -//! # extern crate actix_web; -//! # extern crate actix; -//! use actix_web::{server, App, HttpRequest, Result}; -//! use actix_web::middleware::session::{RequestSession, SessionStorage, CookieSessionBackend}; +//! use actix_web::{App, HttpServer, HttpResponse, Error}; +//! use actix_session::{Session, CookieSession}; //! -//! fn index(req: HttpRequest) -> Result<&'static str> { +//! fn index(session: Session) -> Result<&'static str, Error> { //! // access session data -//! if let Some(count) = req.session().get::("counter")? { +//! if let Some(count) = session.get::("counter")? { //! println!("SESSION value: {}", count); -//! req.session().set("counter", count+1)?; +//! session.set("counter", count+1)?; //! } else { -//! req.session().set("counter", 1)?; +//! session.set("counter", 1)?; //! } //! //! Ok("Welcome!") //! } //! -//! fn main() { -//! actix::System::run(|| { -//! server::new( -//! || App::new().middleware( -//! SessionStorage::new( // <- create session middleware -//! CookieSessionBackend::signed(&[0; 32]) // <- create signed cookie session backend +//! fn main() -> std::io::Result<()> { +//! let sys = actix_rt::System::new("example"); // <- create Actix runtime +//! +//! HttpServer::new( +//! || App::new().middleware( +//! CookieSession::signed(&[0; 32]) // <- create cookie based session middleware //! .secure(false) -//! ))) -//! .bind("127.0.0.1:59880").unwrap() -//! .start(); -//! # actix::System::current().stop(); -//! }); +//! ) +//! .resource("/", |r| r.to(|| HttpResponse::Ok()))) +//! .bind("127.0.0.1:59880")? +//! .start(); +//! # actix_rt::System::current().stop(); +//! sys.run(); +//! Ok(()) //! } //! ``` use std::cell::RefCell; -use std::collections::HashMap; -use std::marker::PhantomData; use std::rc::Rc; -use std::sync::Arc; -use cookie::{Cookie, CookieJar, Key, SameSite}; -use futures::future::{err as FutErr, ok as FutOk, FutureResult}; -use futures::Future; -use http::header::{self, HeaderValue}; +use actix_web::{Error, FromRequest, HttpMessage}; +use actix_web::{ServiceFromRequest, ServiceRequest, ServiceResponse}; +use hashbrown::HashMap; use serde::de::DeserializeOwned; use serde::Serialize; use serde_json; -use serde_json::error::Error as JsonError; -use time::Duration; -use error::{Error, ResponseError, Result}; -use handler::FromRequest; -use httprequest::HttpRequest; -use httpresponse::HttpResponse; -use middleware::{Middleware, Response, Started}; - -/// The helper trait to obtain your session data from a request. -/// -/// ```rust -/// use actix_web::middleware::session::RequestSession; -/// use actix_web::*; -/// -/// fn index(mut req: HttpRequest) -> Result<&'static str> { -/// // access session data -/// if let Some(count) = req.session().get::("counter")? { -/// req.session().set("counter", count + 1)?; -/// } else { -/// req.session().set("counter", 1)?; -/// } -/// -/// Ok("Welcome!") -/// } -/// # fn main() {} -/// ``` -pub trait RequestSession { - /// Get the session from the request - fn session(&self) -> Session; -} - -impl RequestSession for HttpRequest { - fn session(&self) -> Session { - if let Some(s_impl) = self.extensions().get::>() { - return Session(SessionInner::Session(Arc::clone(&s_impl))); - } - Session(SessionInner::None) - } -} +mod cookie; +pub use crate::cookie::CookieSession; /// The high-level interface you use to modify session data. /// @@ -124,80 +64,9 @@ impl RequestSession for HttpRequest { /// method. `RequestSession` trait is implemented for `HttpRequest`. /// /// ```rust -/// use actix_web::middleware::session::RequestSession; +/// use actix_session::Session; /// use actix_web::*; /// -/// fn index(mut req: HttpRequest) -> Result<&'static str> { -/// // access session data -/// if let Some(count) = req.session().get::("counter")? { -/// req.session().set("counter", count + 1)?; -/// } else { -/// req.session().set("counter", 1)?; -/// } -/// -/// Ok("Welcome!") -/// } -/// # fn main() {} -/// ``` -pub struct Session(SessionInner); - -enum SessionInner { - Session(Arc), - None, -} - -impl Session { - /// Get a `value` from the session. - pub fn get(&self, key: &str) -> Result> { - match self.0 { - SessionInner::Session(ref sess) => { - if let Some(s) = sess.as_ref().0.borrow().get(key) { - Ok(Some(serde_json::from_str(s)?)) - } else { - Ok(None) - } - } - SessionInner::None => Ok(None), - } - } - - /// Set a `value` from the session. - pub fn set(&self, key: &str, value: T) -> Result<()> { - match self.0 { - SessionInner::Session(ref sess) => { - sess.as_ref() - .0 - .borrow_mut() - .set(key, serde_json::to_string(&value)?); - Ok(()) - } - SessionInner::None => Ok(()), - } - } - - /// Remove value from the session. - pub fn remove(&self, key: &str) { - match self.0 { - SessionInner::Session(ref sess) => sess.as_ref().0.borrow_mut().remove(key), - SessionInner::None => (), - } - } - - /// Clear the session. - pub fn clear(&self) { - match self.0 { - SessionInner::Session(ref sess) => sess.as_ref().0.borrow_mut().clear(), - SessionInner::None => (), - } - } -} - -/// Extractor implementation for Session type. -/// -/// ```rust -/// # use actix_web::*; -/// use actix_web::middleware::session::Session; -/// /// fn index(session: Session) -> Result<&'static str> { /// // access session data /// if let Some(count) = session.get::("counter")? { @@ -210,409 +79,108 @@ impl Session { /// } /// # fn main() {} /// ``` -impl FromRequest for Session { - type Config = (); - type Result = Session; +pub struct Session(Rc>); - #[inline] - fn from_request(req: &HttpRequest, _: &Self::Config) -> Self::Result { - req.session() - } +#[derive(Default)] +struct SessionInner { + state: HashMap, + changed: bool, } -struct SessionImplCell(RefCell>); - -/// Session storage middleware -/// -/// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::session::{CookieSessionBackend, SessionStorage}; -/// use actix_web::App; -/// -/// fn main() { -/// let app = App::new().middleware(SessionStorage::new( -/// // <- create session middleware -/// CookieSessionBackend::signed(&[0; 32]) // <- create cookie session backend -/// .secure(false), -/// )); -/// } -/// ``` -pub struct SessionStorage(T, PhantomData); - -impl> SessionStorage { - /// Create session storage - pub fn new(backend: T) -> SessionStorage { - SessionStorage(backend, PhantomData) - } -} - -impl> Middleware for SessionStorage { - fn start(&self, req: &HttpRequest) -> Result { - let mut req = req.clone(); - - let fut = self.0.from_request(&mut req).then(move |res| match res { - Ok(sess) => { - req.extensions_mut() - .insert(Arc::new(SessionImplCell(RefCell::new(Box::new(sess))))); - FutOk(None) - } - Err(err) => FutErr(err), - }); - Ok(Started::Future(Box::new(fut))) - } - - fn response(&self, req: &HttpRequest, resp: HttpResponse) -> Result { - if let Some(s_box) = req.extensions().get::>() { - s_box.0.borrow_mut().write(resp) +impl Session { + /// Get a `value` from the session. + pub fn get(&self, key: &str) -> Result, Error> { + if let Some(s) = self.0.borrow().state.get(key) { + Ok(Some(serde_json::from_str(s)?)) } else { - Ok(Response::Done(resp)) + Ok(None) } } -} -/// A simple key-value storage interface that is internally used by `Session`. -pub trait SessionImpl: 'static { - /// Get session value by key - fn get(&self, key: &str) -> Option<&str>; + /// Set a `value` from the session. + pub fn set(&self, key: &str, value: T) -> Result<(), Error> { + let mut inner = self.0.borrow_mut(); + inner.changed = true; + inner + .state + .insert(key.to_owned(), serde_json::to_string(&value)?); + Ok(()) + } - /// Set session value - fn set(&mut self, key: &str, value: String); + /// Remove value from the session. + pub fn remove(&self, key: &str) { + let mut inner = self.0.borrow_mut(); + inner.changed = true; + inner.state.remove(key); + } - /// Remove specific key from session - fn remove(&mut self, key: &str); + /// Clear the session. + pub fn clear(&self) { + let mut inner = self.0.borrow_mut(); + inner.changed = true; + inner.state.clear() + } - /// Remove all values from session - fn clear(&mut self); + pub fn set_session

( + data: impl Iterator, + req: &mut ServiceRequest

, + ) { + let session = Session::get_session(req); + let mut inner = session.0.borrow_mut(); + inner.state.extend(data); + } - /// Write session to storage backend. - fn write(&self, resp: HttpResponse) -> Result; -} - -/// Session's storage backend trait definition. -pub trait SessionBackend: Sized + 'static { - /// Session item - type Session: SessionImpl; - /// Future that reads session - type ReadFuture: Future; - - /// Parse the session from request and load data from a storage backend. - fn from_request(&self, request: &mut HttpRequest) -> Self::ReadFuture; -} - -/// Session that uses signed cookies as session storage -pub struct CookieSession { - changed: bool, - state: HashMap, - inner: Rc, -} - -/// Errors that can occur 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 ResponseError for CookieSessionError {} - -impl SessionImpl for CookieSession { - fn get(&self, key: &str) -> Option<&str> { - if let Some(s) = self.state.get(key) { - Some(s) + pub fn get_changes( + res: &mut ServiceResponse, + ) -> Option> { + if let Some(s_impl) = res + .request() + .extensions() + .get::>>() + { + let state = + std::mem::replace(&mut s_impl.borrow_mut().state, HashMap::new()); + Some(state.into_iter()) } 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) -> Result { - if self.changed { - let _ = self.inner.set_cookie(&mut resp, &self.state); + fn get_session(req: R) -> Session { + if let Some(s_impl) = req.extensions().get::>>() { + return Session(Rc::clone(&s_impl)); } - Ok(Response::Done(resp)) + let inner = Rc::new(RefCell::new(SessionInner::default())); + req.extensions_mut().insert(inner.clone()); + Session(inner) } } -enum CookieSecurity { - Signed, - Private, -} - -struct CookieSessionInner { - key: Key, - security: CookieSecurity, - name: String, - path: String, - domain: Option, - secure: bool, - http_only: bool, - max_age: Option, - same_site: Option, -} - -impl CookieSessionInner { - fn new(key: &[u8], security: CookieSecurity) -> CookieSessionInner { - CookieSessionInner { - security, - key: Key::from_master(key), - name: "actix-session".to_owned(), - path: "/".to_owned(), - domain: None, - secure: true, - http_only: true, - max_age: None, - same_site: None, - } - } - - 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()); - } - - 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(); - - match self.security { - CookieSecurity::Signed => jar.signed(&self.key).add(cookie), - CookieSecurity::Private => jar.private(&self.key).add(cookie), - } - - for cookie in jar.delta() { - let val = HeaderValue::from_str(&cookie.encoded().to_string())?; - resp.headers_mut().append(header::SET_COOKIE, val); - } - - Ok(()) - } - - fn load(&self, req: &mut HttpRequest) -> HashMap { - 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 = match self.security { - CookieSecurity::Signed => jar.signed(&self.key).get(&self.name), - CookieSecurity::Private => { - jar.private(&self.key).get(&self.name) - } - }; - if let Some(cookie) = cookie_opt { - if let Ok(val) = serde_json::from_str(cookie.value()) { - return val; - } - } - } - } - } - HashMap::new() - } -} - -/// Use cookies for 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). An Internal Server Error is generated if the session contains more -/// than 4000 bytes. -/// -/// A cookie may have a security policy of *signed* or *private*. Each has a -/// respective `CookieSessionBackend` constructor. -/// -/// A *signed* cookie is stored on the client as plaintext alongside -/// a signature such that the cookie may be viewed but not modified by the -/// client. -/// -/// A *private* cookie is stored on the client as encrypted text -/// such that it may neither be viewed nor modified by the client. -/// -/// The constructors take a key as an argument. -/// This is the private key for cookie session - when this value is changed, -/// all session data is lost. The constructors will panic if the key is less -/// than 32 bytes in length. -/// -/// The backend relies on `cookie` crate to create and read cookies. -/// By default all cookies are percent encoded, but certain symbols may -/// cause troubles when reading cookie, if they are not properly percent encoded. -/// -/// # Example +/// Extractor implementation for Session type. /// /// ```rust -/// # extern crate actix_web; -/// use actix_web::middleware::session::CookieSessionBackend; +/// # use actix_web::*; +/// use actix_session::Session; /// -/// # fn main() { -/// let backend: CookieSessionBackend = CookieSessionBackend::signed(&[0; 32]) -/// .domain("www.rust-lang.org") -/// .name("actix_session") -/// .path("/") -/// .secure(true); -/// # } +/// fn index(session: Session) -> Result<&'static str> { +/// // access session data +/// if let Some(count) = session.get::("counter")? { +/// session.set("counter", count + 1)?; +/// } else { +/// session.set("counter", 1)?; +/// } +/// +/// Ok("Welcome!") +/// } +/// # fn main() {} /// ``` -pub struct CookieSessionBackend(Rc); +impl

FromRequest

for Session { + type Error = Error; + type Future = Result; + type Config = (); -impl CookieSessionBackend { - /// Construct new *signed* `CookieSessionBackend` instance. - /// - /// Panics if key length is less than 32 bytes. - pub fn signed(key: &[u8]) -> CookieSessionBackend { - CookieSessionBackend(Rc::new(CookieSessionInner::new( - key, - CookieSecurity::Signed, - ))) - } - - /// Construct new *private* `CookieSessionBackend` instance. - /// - /// Panics if key length is less than 32 bytes. - pub fn private(key: &[u8]) -> CookieSessionBackend { - CookieSessionBackend(Rc::new(CookieSessionInner::new( - key, - CookieSecurity::Private, - ))) - } - - /// Sets the `path` field in the session cookie being built. - pub fn path>(mut self, value: S) -> CookieSessionBackend { - 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) -> CookieSessionBackend { - 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) -> CookieSessionBackend { - 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) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().secure = value; - self - } - - /// Sets the `http_only` field in the session cookie being built. - pub fn http_only(mut self, value: bool) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().http_only = value; - self - } - - /// Sets the `same_site` field in the session cookie being built. - pub fn same_site(mut self, value: SameSite) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().same_site = Some(value); - self - } - - /// Sets the `max-age` field in the session cookie being built. - pub fn max_age(mut self, value: Duration) -> CookieSessionBackend { - Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); - self - } -} - -impl SessionBackend for CookieSessionBackend { - type Session = CookieSession; - type ReadFuture = FutureResult; - - fn from_request(&self, req: &mut HttpRequest) -> Self::ReadFuture { - let state = self.0.load(req); - FutOk(CookieSession { - changed: false, - inner: Rc::clone(&self.0), - state, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use application::App; - use test; - - #[test] - fn cookie_session() { - let mut srv = test::TestServer::with_factory(|| { - App::new() - .middleware(SessionStorage::new( - CookieSessionBackend::signed(&[0; 32]).secure(false), - )).resource("/", |r| { - r.f(|req| { - let _ = req.session().set("counter", 100); - "test" - }) - }) - }); - - let request = srv.get().uri(srv.url("/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.cookie("actix-session").is_some()); - } - - #[test] - fn cookie_session_extractor() { - let mut srv = test::TestServer::with_factory(|| { - App::new() - .middleware(SessionStorage::new( - CookieSessionBackend::signed(&[0; 32]).secure(false), - )).resource("/", |r| { - r.with(|ses: Session| { - let _ = ses.set("counter", 100); - "test" - }) - }) - }); - - let request = srv.get().uri(srv.url("/")).finish().unwrap(); - let response = srv.execute(request.send()).unwrap(); - assert!(response.cookie("actix-session").is_some()); + #[inline] + fn from_request(req: &mut ServiceFromRequest

) -> Self::Future { + Ok(Session::get_session(req)) } } diff --git a/src/request.rs b/src/request.rs index d90627f52..75daf59d8 100644 --- a/src/request.rs +++ b/src/request.rs @@ -64,12 +64,6 @@ impl HttpRequest { self.head().uri.path() } - #[inline] - /// Returns Request's headers. - pub fn headers(&self) -> &HeaderMap { - &self.head().headers - } - /// The query string in the URL. /// /// E.g., id=10 @@ -93,18 +87,6 @@ impl HttpRequest { &self.path } - /// Request extensions - #[inline] - pub fn extensions(&self) -> Ref { - self.head.extensions() - } - - /// Mutable reference to a the request's extensions - #[inline] - pub fn extensions_mut(&self) -> RefMut { - self.head.extensions_mut() - } - /// Application extensions #[inline] pub fn app_extensions(&self) -> &Extensions { @@ -130,8 +112,26 @@ impl HttpMessage for HttpRequest { type Stream = (); #[inline] + /// Returns Request's headers. fn headers(&self) -> &HeaderMap { - self.headers() + &self.head().headers + } + + #[inline] + fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.head.headers + } + + /// Request extensions + #[inline] + fn extensions(&self) -> Ref { + self.head.extensions() + } + + /// Mutable reference to a the request's extensions + #[inline] + fn extensions_mut(&self) -> RefMut { + self.head.extensions_mut() } #[inline] diff --git a/src/service.rs b/src/service.rs index 50b2924ad..0da664396 100644 --- a/src/service.rs +++ b/src/service.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use std::cell::{Ref, RefMut}; use std::rc::Rc; -use actix_http::body::{Body, MessageBody, ResponseBody}; +use actix_http::body::{Body, ResponseBody}; use actix_http::http::{HeaderMap, Method, Uri, Version}; use actix_http::{ Error, Extensions, HttpMessage, Payload, PayloadStream, Request, RequestHead, @@ -84,12 +84,6 @@ impl

ServiceRequest

{ self.head().uri.path() } - #[inline] - /// Returns Request's headers. - pub fn headers(&self) -> &HeaderMap { - &self.head().headers - } - /// The query string in the URL. /// /// E.g., id=10 @@ -118,18 +112,6 @@ impl

ServiceRequest

{ &mut self.req.path } - /// Request extensions - #[inline] - pub fn extensions(&self) -> Ref { - self.req.head.extensions() - } - - /// Mutable reference to a the request's extensions - #[inline] - pub fn extensions_mut(&self) -> RefMut { - self.req.head.extensions_mut() - } - /// Application extensions #[inline] pub fn app_extensions(&self) -> &Extensions { @@ -147,8 +129,27 @@ impl

HttpMessage for ServiceRequest

{ type Stream = P; #[inline] + /// Returns Request's headers. fn headers(&self) -> &HeaderMap { - self.req.headers() + &self.head().headers + } + + #[inline] + /// Mutable reference to the request's headers. + fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.head_mut().headers + } + + /// Request extensions + #[inline] + fn extensions(&self) -> Ref { + self.req.head.extensions() + } + + /// Mutable reference to a the request's extensions + #[inline] + fn extensions_mut(&self) -> RefMut { + self.req.head.extensions_mut() } #[inline] @@ -229,6 +230,23 @@ impl

HttpMessage for ServiceFromRequest

{ self.req.headers() } + #[inline] + fn headers_mut(&mut self) -> &mut HeaderMap { + self.req.headers_mut() + } + + /// Request extensions + #[inline] + fn extensions(&self) -> Ref { + self.req.head.extensions() + } + + /// Mutable reference to a the request's extensions + #[inline] + fn extensions_mut(&self) -> RefMut { + self.req.head.extensions_mut() + } + #[inline] fn take_payload(&mut self) -> Payload { std::mem::replace(&mut self.payload, Payload::None) @@ -275,11 +293,26 @@ impl ServiceResponse { pub fn headers_mut(&mut self) -> &mut HeaderMap { self.response.headers_mut() } + + /// Execute closure and in case of error convert it to response. + pub fn checked_expr(mut self, f: F) -> Self + where + F: FnOnce(&mut Self) -> Result<(), E>, + E: Into, + { + match f(&mut self) { + Ok(_) => self, + Err(err) => { + let res: Response = err.into().into(); + ServiceResponse::new(self.request, res.into_body()) + } + } + } } -impl ServiceResponse { +impl ServiceResponse { /// Set a new body - pub fn map_body(self, f: F) -> ServiceResponse + pub fn map_body(self, f: F) -> ServiceResponse where F: FnOnce(&mut ResponseHead, ResponseBody) -> ResponseBody, { @@ -292,7 +325,7 @@ impl ServiceResponse { } } -impl std::ops::Deref for ServiceResponse { +impl std::ops::Deref for ServiceResponse { type Target = Response; fn deref(&self) -> &Response { @@ -300,19 +333,19 @@ impl std::ops::Deref for ServiceResponse { } } -impl std::ops::DerefMut for ServiceResponse { +impl std::ops::DerefMut for ServiceResponse { fn deref_mut(&mut self) -> &mut Response { self.response_mut() } } -impl Into> for ServiceResponse { +impl Into> for ServiceResponse { fn into(self) -> Response { self.response } } -impl IntoFuture for ServiceResponse { +impl IntoFuture for ServiceResponse { type Item = ServiceResponse; type Error = Error; type Future = FutureResult, Error>; diff --git a/src/test.rs b/src/test.rs index 7ceedacc4..8b6667a4d 100644 --- a/src/test.rs +++ b/src/test.rs @@ -8,11 +8,12 @@ use actix_http::test::TestRequest as HttpTestRequest; use actix_http::{Extensions, PayloadStream, Request}; use actix_router::{Path, Url}; use actix_rt::Runtime; +use actix_service::{IntoNewService, NewService, Service}; use bytes::Bytes; use futures::Future; use crate::request::HttpRequest; -use crate::service::{ServiceFromRequest, ServiceRequest}; +use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse}; thread_local! { static RT: RefCell = { @@ -37,6 +38,34 @@ where RT.with(move |rt| rt.borrow_mut().block_on(f)) } +/// This method accepts application builder instance, and constructs +/// service. +/// +/// ```rust +/// use actix_http::http::{test, App, HttpResponse}; +/// +/// fn main() { +/// let app = test::init_service( +/// App::new() +/// .resource("/test", |r| r.to(|| HttpResponse::Ok())) +/// ) +/// +/// let req = TestRequest::with_uri("/test").to_request(); +/// let resp = block_on(srv.call(req)).unwrap(); +/// assert_eq!(resp.status(), StatusCode::OK); +/// } +/// ``` +pub fn init_service( + app: R, +) -> impl Service, Error = E> +where + R: IntoNewService, + S: NewService, Error = E>, + S::InitError: std::fmt::Debug, +{ + block_on(app.into_new_service().new_service(&())).unwrap() +} + /// Test `Request` builder. /// /// For unit testing, actix provides a request builder type and a simple handler runner. TestRequest implements a builder-like pattern. @@ -112,6 +141,22 @@ impl TestRequest { } } + /// Create TestRequest and set method to `Method::GET` + pub fn get() -> TestRequest { + TestRequest { + req: HttpTestRequest::default().method(Method::GET).take(), + extensions: Extensions::new(), + } + } + + /// Create TestRequest and set method to `Method::POST` + pub fn post() -> TestRequest { + TestRequest { + req: HttpTestRequest::default().method(Method::POST).take(), + extensions: Extensions::new(), + } + } + /// Set HTTP version of this request pub fn version(mut self, ver: Version) -> Self { self.req.version(ver);