1
0
Fork 0
mirror of https://github.com/actix/actix-web.git synced 2024-06-02 13:29:24 +00:00

add session and cookie session backend

This commit is contained in:
Nikolay Kim 2019-03-05 18:47:18 -08:00
parent 6457996cf1
commit 143ef87b66
7 changed files with 646 additions and 588 deletions

View file

@ -27,6 +27,7 @@ path = "src/lib.rs"
[workspace]
members = [
".",
"session",
"staticfiles",
]

51
session/Cargo.toml Normal file
View file

@ -0,0 +1,51 @@
[package]
name = "actix-session"
version = "0.1.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
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"

360
session/src/cookie.rs Normal file
View file

@ -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<String>,
secure: bool,
http_only: bool,
max_age: Option<Duration>,
same_site: Option<SameSite>,
}
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<B>(
&self,
res: &mut ServiceResponse<B>,
state: impl Iterator<Item = (String, String)>,
) -> Result<(), Error> {
let state: HashMap<String, String> = 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<P>(&self, req: &ServiceRequest<P>) -> HashMap<String, String> {
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<CookieSessionInner>);
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<S: Into<String>>(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<S: Into<String>>(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<S: Into<String>>(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<S, P, B: 'static> Transform<S, ServiceRequest<P>> for CookieSession
where
S: Service<ServiceRequest<P>, Response = ServiceResponse<B>>,
S::Future: 'static,
S::Error: 'static,
{
type Response = ServiceResponse<B>;
type Error = S::Error;
type InitError = ();
type Transform = CookieSessionMiddleware<S>;
type Future = FutureResult<Self::Transform, Self::InitError>;
fn new_transform(&self, service: S) -> Self::Future {
ok(CookieSessionMiddleware {
service,
inner: self.0.clone(),
})
}
}
/// Cookie session middleware
pub struct CookieSessionMiddleware<S> {
service: S,
inner: Rc<CookieSessionInner>,
}
impl<S, P, B: 'static> Service<ServiceRequest<P>> for CookieSessionMiddleware<S>
where
S: Service<ServiceRequest<P>, Response = ServiceResponse<B>>,
S::Future: 'static,
S::Error: 'static,
{
type Response = ServiceResponse<B>;
type Error = S::Error;
type Future = Box<Future<Item = Self::Response, Error = Self::Error>>;
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<P>) -> 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());
}
}

View file

@ -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::<i32>("counter")? {
//! if let Some(count) = session.get::<i32>("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::<i32>("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<S> RequestSession for HttpRequest<S> {
fn session(&self) -> Session {
if let Some(s_impl) = self.extensions().get::<Arc<SessionImplCell>>() {
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<S> RequestSession for HttpRequest<S> {
/// 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::<i32>("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<SessionImplCell>),
None,
}
impl Session {
/// Get a `value` from the session.
pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
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<T: Serialize>(&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::<i32>("counter")? {
@ -210,409 +79,108 @@ impl Session {
/// }
/// # fn main() {}
/// ```
impl<S> FromRequest<S> for Session {
type Config = ();
type Result = Session;
pub struct Session(Rc<RefCell<SessionInner>>);
#[inline]
fn from_request(req: &HttpRequest<S>, _: &Self::Config) -> Self::Result {
req.session()
}
#[derive(Default)]
struct SessionInner {
state: HashMap<String, String>,
changed: bool,
}
struct SessionImplCell(RefCell<Box<SessionImpl>>);
/// 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, S>(T, PhantomData<S>);
impl<S, T: SessionBackend<S>> SessionStorage<T, S> {
/// Create session storage
pub fn new(backend: T) -> SessionStorage<T, S> {
SessionStorage(backend, PhantomData)
}
}
impl<S: 'static, T: SessionBackend<S>> Middleware<S> for SessionStorage<T, S> {
fn start(&self, req: &HttpRequest<S>) -> Result<Started> {
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<S>, resp: HttpResponse) -> Result<Response> {
if let Some(s_box) = req.extensions().get::<Arc<SessionImplCell>>() {
s_box.0.borrow_mut().write(resp)
impl Session {
/// Get a `value` from the session.
pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, 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<T: Serialize>(&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<P>(
data: impl Iterator<Item = (String, String)>,
req: &mut ServiceRequest<P>,
) {
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<Response>;
}
/// Session's storage backend trait definition.
pub trait SessionBackend<S>: Sized + 'static {
/// Session item
type Session: SessionImpl;
/// Future that reads session
type ReadFuture: Future<Item = Self::Session, Error = Error>;
/// Parse the session from request and load data from a storage backend.
fn from_request(&self, request: &mut HttpRequest<S>) -> Self::ReadFuture;
}
/// Session that uses signed cookies as session storage
pub struct CookieSession {
changed: bool,
state: HashMap<String, String>,
inner: Rc<CookieSessionInner>,
}
/// 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<B>(
res: &mut ServiceResponse<B>,
) -> Option<impl Iterator<Item = (String, String)>> {
if let Some(s_impl) = res
.request()
.extensions()
.get::<Rc<RefCell<SessionInner>>>()
{
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<Response> {
if self.changed {
let _ = self.inner.set_cookie(&mut resp, &self.state);
fn get_session<R: HttpMessage>(req: R) -> Session {
if let Some(s_impl) = req.extensions().get::<Rc<RefCell<SessionInner>>>() {
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<String>,
secure: bool,
http_only: bool,
max_age: Option<Duration>,
same_site: Option<SameSite>,
}
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<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());
}
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<S>(&self, req: &mut HttpRequest<S>) -> HashMap<String, String> {
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::<i32>("counter")? {
/// session.set("counter", count + 1)?;
/// } else {
/// session.set("counter", 1)?;
/// }
///
/// Ok("Welcome!")
/// }
/// # fn main() {}
/// ```
pub struct CookieSessionBackend(Rc<CookieSessionInner>);
impl<P> FromRequest<P> for Session {
type Error = Error;
type Future = Result<Session, Error>;
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<S: Into<String>>(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<S: Into<String>>(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<S: Into<String>>(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<S> SessionBackend<S> for CookieSessionBackend {
type Session = CookieSession;
type ReadFuture = FutureResult<CookieSession, Error>;
fn from_request(&self, req: &mut HttpRequest<S>) -> 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<P>) -> Self::Future {
Ok(Session::get_session(req))
}
}

View file

@ -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<Extensions> {
self.head.extensions()
}
/// Mutable reference to a the request's extensions
#[inline]
pub fn extensions_mut(&self) -> RefMut<Extensions> {
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<Extensions> {
self.head.extensions()
}
/// Mutable reference to a the request's extensions
#[inline]
fn extensions_mut(&self) -> RefMut<Extensions> {
self.head.extensions_mut()
}
#[inline]

View file

@ -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<P> ServiceRequest<P> {
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<P> ServiceRequest<P> {
&mut self.req.path
}
/// Request extensions
#[inline]
pub fn extensions(&self) -> Ref<Extensions> {
self.req.head.extensions()
}
/// Mutable reference to a the request's extensions
#[inline]
pub fn extensions_mut(&self) -> RefMut<Extensions> {
self.req.head.extensions_mut()
}
/// Application extensions
#[inline]
pub fn app_extensions(&self) -> &Extensions {
@ -147,8 +129,27 @@ impl<P> HttpMessage for ServiceRequest<P> {
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<Extensions> {
self.req.head.extensions()
}
/// Mutable reference to a the request's extensions
#[inline]
fn extensions_mut(&self) -> RefMut<Extensions> {
self.req.head.extensions_mut()
}
#[inline]
@ -229,6 +230,23 @@ impl<P> HttpMessage for ServiceFromRequest<P> {
self.req.headers()
}
#[inline]
fn headers_mut(&mut self) -> &mut HeaderMap {
self.req.headers_mut()
}
/// Request extensions
#[inline]
fn extensions(&self) -> Ref<Extensions> {
self.req.head.extensions()
}
/// Mutable reference to a the request's extensions
#[inline]
fn extensions_mut(&self) -> RefMut<Extensions> {
self.req.head.extensions_mut()
}
#[inline]
fn take_payload(&mut self) -> Payload<Self::Stream> {
std::mem::replace(&mut self.payload, Payload::None)
@ -275,11 +293,26 @@ impl<B> ServiceResponse<B> {
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<F, E>(mut self, f: F) -> Self
where
F: FnOnce(&mut Self) -> Result<(), E>,
E: Into<Error>,
{
match f(&mut self) {
Ok(_) => self,
Err(err) => {
let res: Response = err.into().into();
ServiceResponse::new(self.request, res.into_body())
}
}
}
}
impl<B: MessageBody> ServiceResponse<B> {
impl<B> ServiceResponse<B> {
/// Set a new body
pub fn map_body<F, B2: MessageBody>(self, f: F) -> ServiceResponse<B2>
pub fn map_body<F, B2>(self, f: F) -> ServiceResponse<B2>
where
F: FnOnce(&mut ResponseHead, ResponseBody<B>) -> ResponseBody<B2>,
{
@ -292,7 +325,7 @@ impl<B: MessageBody> ServiceResponse<B> {
}
}
impl<B: MessageBody> std::ops::Deref for ServiceResponse<B> {
impl<B> std::ops::Deref for ServiceResponse<B> {
type Target = Response<B>;
fn deref(&self) -> &Response<B> {
@ -300,19 +333,19 @@ impl<B: MessageBody> std::ops::Deref for ServiceResponse<B> {
}
}
impl<B: MessageBody> std::ops::DerefMut for ServiceResponse<B> {
impl<B> std::ops::DerefMut for ServiceResponse<B> {
fn deref_mut(&mut self) -> &mut Response<B> {
self.response_mut()
}
}
impl<B: MessageBody> Into<Response<B>> for ServiceResponse<B> {
impl<B> Into<Response<B>> for ServiceResponse<B> {
fn into(self) -> Response<B> {
self.response
}
}
impl<B: MessageBody> IntoFuture for ServiceResponse<B> {
impl<B> IntoFuture for ServiceResponse<B> {
type Item = ServiceResponse<B>;
type Error = Error;
type Future = FutureResult<ServiceResponse<B>, Error>;

View file

@ -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<Runtime> = {
@ -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<R, S, B, E>(
app: R,
) -> impl Service<Request, Response = ServiceResponse<B>, Error = E>
where
R: IntoNewService<S, Request, ()>,
S: NewService<Request, Response = ServiceResponse<B>, 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);