mirror of
https://github.com/fafhrd91/actix-web
synced 2024-11-27 17:52:56 +01:00
add session and cookie session backend
This commit is contained in:
parent
6457996cf1
commit
143ef87b66
@ -27,6 +27,7 @@ path = "src/lib.rs"
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
".",
|
".",
|
||||||
|
"session",
|
||||||
"staticfiles",
|
"staticfiles",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
51
session/Cargo.toml
Normal file
51
session/Cargo.toml
Normal 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
360
session/src/cookie.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
@ -1,121 +1,61 @@
|
|||||||
//! User sessions.
|
//! User sessions.
|
||||||
//!
|
//!
|
||||||
//! Actix provides a general solution for session management. The
|
//! Actix provides a general solution for session management. Session
|
||||||
//! [**SessionStorage**](struct.SessionStorage.html)
|
//! middlewares could provide different implementations which could
|
||||||
//! middleware can be used with different backend types to store session
|
//! be accessed via general session api.
|
||||||
//! data in different backends.
|
|
||||||
//!
|
//!
|
||||||
//! By default, only cookie session backend is implemented. Other
|
//! By default, only cookie session backend is implemented. Other
|
||||||
//! backend implementations can be added.
|
//! backend implementations can be added.
|
||||||
//!
|
//!
|
||||||
//! [**CookieSessionBackend**](struct.CookieSessionBackend.html)
|
//! In general, you insert a *session* middleware and initialize it
|
||||||
//! uses cookies as session storage. `CookieSessionBackend` creates sessions
|
//! , such as a `CookieSessionBackend`. To access session data,
|
||||||
//! which are limited to storing fewer than 4000 bytes of data, as the payload
|
//! [*Session*](struct.Session.html) extractor must be used. Session
|
||||||
//! must fit into a single cookie. An internal server error is generated if a
|
//! extractor allows us to get or set session data.
|
||||||
//! 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.
|
|
||||||
//!
|
//!
|
||||||
//! ```rust
|
//! ```rust
|
||||||
//! # extern crate actix_web;
|
//! use actix_web::{App, HttpServer, HttpResponse, Error};
|
||||||
//! # extern crate actix;
|
//! use actix_session::{Session, CookieSession};
|
||||||
//! use actix_web::{server, App, HttpRequest, Result};
|
|
||||||
//! use actix_web::middleware::session::{RequestSession, SessionStorage, CookieSessionBackend};
|
|
||||||
//!
|
//!
|
||||||
//! fn index(req: HttpRequest) -> Result<&'static str> {
|
//! fn index(session: Session) -> Result<&'static str, Error> {
|
||||||
//! // access session data
|
//! // access session data
|
||||||
//! if let Some(count) = req.session().get::<i32>("counter")? {
|
//! if let Some(count) = session.get::<i32>("counter")? {
|
||||||
//! println!("SESSION value: {}", count);
|
//! println!("SESSION value: {}", count);
|
||||||
//! req.session().set("counter", count+1)?;
|
//! session.set("counter", count+1)?;
|
||||||
//! } else {
|
//! } else {
|
||||||
//! req.session().set("counter", 1)?;
|
//! session.set("counter", 1)?;
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! Ok("Welcome!")
|
//! Ok("Welcome!")
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! fn main() {
|
//! fn main() -> std::io::Result<()> {
|
||||||
//! actix::System::run(|| {
|
//! let sys = actix_rt::System::new("example"); // <- create Actix runtime
|
||||||
//! server::new(
|
//!
|
||||||
|
//! HttpServer::new(
|
||||||
//! || App::new().middleware(
|
//! || App::new().middleware(
|
||||||
//! SessionStorage::new( // <- create session middleware
|
//! CookieSession::signed(&[0; 32]) // <- create cookie based session middleware
|
||||||
//! CookieSessionBackend::signed(&[0; 32]) // <- create signed cookie session backend
|
|
||||||
//! .secure(false)
|
//! .secure(false)
|
||||||
//! )))
|
//! )
|
||||||
//! .bind("127.0.0.1:59880").unwrap()
|
//! .resource("/", |r| r.to(|| HttpResponse::Ok())))
|
||||||
|
//! .bind("127.0.0.1:59880")?
|
||||||
//! .start();
|
//! .start();
|
||||||
//! # actix::System::current().stop();
|
//! # actix_rt::System::current().stop();
|
||||||
//! });
|
//! sys.run();
|
||||||
|
//! Ok(())
|
||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::marker::PhantomData;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use cookie::{Cookie, CookieJar, Key, SameSite};
|
use actix_web::{Error, FromRequest, HttpMessage};
|
||||||
use futures::future::{err as FutErr, ok as FutOk, FutureResult};
|
use actix_web::{ServiceFromRequest, ServiceRequest, ServiceResponse};
|
||||||
use futures::Future;
|
use hashbrown::HashMap;
|
||||||
use http::header::{self, HeaderValue};
|
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use serde_json::error::Error as JsonError;
|
|
||||||
use time::Duration;
|
|
||||||
|
|
||||||
use error::{Error, ResponseError, Result};
|
mod cookie;
|
||||||
use handler::FromRequest;
|
pub use crate::cookie::CookieSession;
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The high-level interface you use to modify session data.
|
/// 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`.
|
/// method. `RequestSession` trait is implemented for `HttpRequest`.
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use actix_web::middleware::session::RequestSession;
|
/// use actix_session::Session;
|
||||||
/// use actix_web::*;
|
/// 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> {
|
/// fn index(session: Session) -> Result<&'static str> {
|
||||||
/// // access session data
|
/// // access session data
|
||||||
/// if let Some(count) = session.get::<i32>("counter")? {
|
/// if let Some(count) = session.get::<i32>("counter")? {
|
||||||
@ -210,409 +79,108 @@ impl Session {
|
|||||||
/// }
|
/// }
|
||||||
/// # fn main() {}
|
/// # fn main() {}
|
||||||
/// ```
|
/// ```
|
||||||
impl<S> FromRequest<S> for Session {
|
pub struct Session(Rc<RefCell<SessionInner>>);
|
||||||
type Config = ();
|
|
||||||
type Result = Session;
|
|
||||||
|
|
||||||
#[inline]
|
#[derive(Default)]
|
||||||
fn from_request(req: &HttpRequest<S>, _: &Self::Config) -> Self::Result {
|
struct SessionInner {
|
||||||
req.session()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
} else {
|
|
||||||
Ok(Response::Done(resp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 session value
|
|
||||||
fn set(&mut self, key: &str, value: String);
|
|
||||||
|
|
||||||
/// Remove specific key from session
|
|
||||||
fn remove(&mut self, key: &str);
|
|
||||||
|
|
||||||
/// Remove all values from session
|
|
||||||
fn clear(&mut self);
|
|
||||||
|
|
||||||
/// 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>,
|
state: HashMap<String, String>,
|
||||||
inner: Rc<CookieSessionInner>,
|
changed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Errors that can occur during handling cookie session
|
impl Session {
|
||||||
#[derive(Fail, Debug)]
|
/// Get a `value` from the session.
|
||||||
pub enum CookieSessionError {
|
pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, Error> {
|
||||||
/// Size of the serialized session is greater than 4000 bytes.
|
if let Some(s) = self.0.borrow().state.get(key) {
|
||||||
#[fail(display = "Size of the serialized session is greater than 4000 bytes.")]
|
Ok(Some(serde_json::from_str(s)?))
|
||||||
Overflow,
|
} else {
|
||||||
/// Fail to serialize session.
|
Ok(None)
|
||||||
#[fail(display = "Fail to serialize session")]
|
}
|
||||||
Serialize(JsonError),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseError for CookieSessionError {}
|
/// 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(())
|
||||||
|
}
|
||||||
|
|
||||||
impl SessionImpl for CookieSession {
|
/// Remove value from the session.
|
||||||
fn get(&self, key: &str) -> Option<&str> {
|
pub fn remove(&self, key: &str) {
|
||||||
if let Some(s) = self.state.get(key) {
|
let mut inner = self.0.borrow_mut();
|
||||||
Some(s)
|
inner.changed = true;
|
||||||
|
inner.state.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the session.
|
||||||
|
pub fn clear(&self) {
|
||||||
|
let mut inner = self.0.borrow_mut();
|
||||||
|
inner.changed = true;
|
||||||
|
inner.state.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set(&mut self, key: &str, value: String) {
|
fn get_session<R: HttpMessage>(req: R) -> Session {
|
||||||
self.changed = true;
|
if let Some(s_impl) = req.extensions().get::<Rc<RefCell<SessionInner>>>() {
|
||||||
self.state.insert(key.to_owned(), value);
|
return Session(Rc::clone(&s_impl));
|
||||||
}
|
}
|
||||||
|
let inner = Rc::new(RefCell::new(SessionInner::default()));
|
||||||
fn remove(&mut self, key: &str) {
|
req.extensions_mut().insert(inner.clone());
|
||||||
self.changed = true;
|
Session(inner)
|
||||||
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);
|
|
||||||
}
|
|
||||||
Ok(Response::Done(resp))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CookieSecurity {
|
/// Extractor implementation for Session type.
|
||||||
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
|
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # extern crate actix_web;
|
/// # use actix_web::*;
|
||||||
/// use actix_web::middleware::session::CookieSessionBackend;
|
/// use actix_session::Session;
|
||||||
///
|
///
|
||||||
/// # fn main() {
|
/// fn index(session: Session) -> Result<&'static str> {
|
||||||
/// let backend: CookieSessionBackend = CookieSessionBackend::signed(&[0; 32])
|
/// // access session data
|
||||||
/// .domain("www.rust-lang.org")
|
/// if let Some(count) = session.get::<i32>("counter")? {
|
||||||
/// .name("actix_session")
|
/// session.set("counter", count + 1)?;
|
||||||
/// .path("/")
|
/// } else {
|
||||||
/// .secure(true);
|
/// 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 {
|
#[inline]
|
||||||
/// Construct new *signed* `CookieSessionBackend` instance.
|
fn from_request(req: &mut ServiceFromRequest<P>) -> Self::Future {
|
||||||
///
|
Ok(Session::get_session(req))
|
||||||
/// 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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,12 +64,6 @@ impl HttpRequest {
|
|||||||
self.head().uri.path()
|
self.head().uri.path()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
/// Returns Request's headers.
|
|
||||||
pub fn headers(&self) -> &HeaderMap {
|
|
||||||
&self.head().headers
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The query string in the URL.
|
/// The query string in the URL.
|
||||||
///
|
///
|
||||||
/// E.g., id=10
|
/// E.g., id=10
|
||||||
@ -93,18 +87,6 @@ impl HttpRequest {
|
|||||||
&self.path
|
&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
|
/// Application extensions
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn app_extensions(&self) -> &Extensions {
|
pub fn app_extensions(&self) -> &Extensions {
|
||||||
@ -130,8 +112,26 @@ impl HttpMessage for HttpRequest {
|
|||||||
type Stream = ();
|
type Stream = ();
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
/// Returns Request's headers.
|
||||||
fn headers(&self) -> &HeaderMap {
|
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]
|
#[inline]
|
||||||
|
@ -2,7 +2,7 @@ use std::borrow::Cow;
|
|||||||
use std::cell::{Ref, RefMut};
|
use std::cell::{Ref, RefMut};
|
||||||
use std::rc::Rc;
|
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::http::{HeaderMap, Method, Uri, Version};
|
||||||
use actix_http::{
|
use actix_http::{
|
||||||
Error, Extensions, HttpMessage, Payload, PayloadStream, Request, RequestHead,
|
Error, Extensions, HttpMessage, Payload, PayloadStream, Request, RequestHead,
|
||||||
@ -84,12 +84,6 @@ impl<P> ServiceRequest<P> {
|
|||||||
self.head().uri.path()
|
self.head().uri.path()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
/// Returns Request's headers.
|
|
||||||
pub fn headers(&self) -> &HeaderMap {
|
|
||||||
&self.head().headers
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The query string in the URL.
|
/// The query string in the URL.
|
||||||
///
|
///
|
||||||
/// E.g., id=10
|
/// E.g., id=10
|
||||||
@ -118,18 +112,6 @@ impl<P> ServiceRequest<P> {
|
|||||||
&mut self.req.path
|
&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
|
/// Application extensions
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn app_extensions(&self) -> &Extensions {
|
pub fn app_extensions(&self) -> &Extensions {
|
||||||
@ -147,8 +129,27 @@ impl<P> HttpMessage for ServiceRequest<P> {
|
|||||||
type Stream = P;
|
type Stream = P;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
/// Returns Request's headers.
|
||||||
fn headers(&self) -> &HeaderMap {
|
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]
|
#[inline]
|
||||||
@ -229,6 +230,23 @@ impl<P> HttpMessage for ServiceFromRequest<P> {
|
|||||||
self.req.headers()
|
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]
|
#[inline]
|
||||||
fn take_payload(&mut self) -> Payload<Self::Stream> {
|
fn take_payload(&mut self) -> Payload<Self::Stream> {
|
||||||
std::mem::replace(&mut self.payload, Payload::None)
|
std::mem::replace(&mut self.payload, Payload::None)
|
||||||
@ -275,11 +293,26 @@ impl<B> ServiceResponse<B> {
|
|||||||
pub fn headers_mut(&mut self) -> &mut HeaderMap {
|
pub fn headers_mut(&mut self) -> &mut HeaderMap {
|
||||||
self.response.headers_mut()
|
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
|
/// 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
|
where
|
||||||
F: FnOnce(&mut ResponseHead, ResponseBody<B>) -> ResponseBody<B2>,
|
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>;
|
type Target = Response<B>;
|
||||||
|
|
||||||
fn deref(&self) -> &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> {
|
fn deref_mut(&mut self) -> &mut Response<B> {
|
||||||
self.response_mut()
|
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> {
|
fn into(self) -> Response<B> {
|
||||||
self.response
|
self.response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<B: MessageBody> IntoFuture for ServiceResponse<B> {
|
impl<B> IntoFuture for ServiceResponse<B> {
|
||||||
type Item = ServiceResponse<B>;
|
type Item = ServiceResponse<B>;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Future = FutureResult<ServiceResponse<B>, Error>;
|
type Future = FutureResult<ServiceResponse<B>, Error>;
|
||||||
|
47
src/test.rs
47
src/test.rs
@ -8,11 +8,12 @@ use actix_http::test::TestRequest as HttpTestRequest;
|
|||||||
use actix_http::{Extensions, PayloadStream, Request};
|
use actix_http::{Extensions, PayloadStream, Request};
|
||||||
use actix_router::{Path, Url};
|
use actix_router::{Path, Url};
|
||||||
use actix_rt::Runtime;
|
use actix_rt::Runtime;
|
||||||
|
use actix_service::{IntoNewService, NewService, Service};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures::Future;
|
use futures::Future;
|
||||||
|
|
||||||
use crate::request::HttpRequest;
|
use crate::request::HttpRequest;
|
||||||
use crate::service::{ServiceFromRequest, ServiceRequest};
|
use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse};
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static RT: RefCell<Runtime> = {
|
static RT: RefCell<Runtime> = {
|
||||||
@ -37,6 +38,34 @@ where
|
|||||||
RT.with(move |rt| rt.borrow_mut().block_on(f))
|
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.
|
/// Test `Request` builder.
|
||||||
///
|
///
|
||||||
/// For unit testing, actix provides a request builder type and a simple handler runner. TestRequest implements a builder-like pattern.
|
/// 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
|
/// Set HTTP version of this request
|
||||||
pub fn version(mut self, ver: Version) -> Self {
|
pub fn version(mut self, ver: Version) -> Self {
|
||||||
self.req.version(ver);
|
self.req.version(ver);
|
||||||
|
Loading…
Reference in New Issue
Block a user