mirror of
https://github.com/actix/actix-extras.git
synced 2025-06-26 10:27:42 +02:00
rename module
This commit is contained in:
128
src/middleware/defaultheaders.rs
Normal file
128
src/middleware/defaultheaders.rs
Normal file
@ -0,0 +1,128 @@
|
||||
//! Default response headers
|
||||
use http::{HeaderMap, HttpTryFrom};
|
||||
use http::header::{HeaderName, HeaderValue, CONTENT_TYPE};
|
||||
|
||||
use httprequest::HttpRequest;
|
||||
use httpresponse::HttpResponse;
|
||||
use middleware::{Response, Middleware};
|
||||
|
||||
/// `Middleware` for setting default response headers.
|
||||
///
|
||||
/// This middleware does not set header if response headers already contains it.
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// use actix_web::*;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let app = Application::new()
|
||||
/// .middleware(
|
||||
/// middleware::DefaultHeaders::build()
|
||||
/// .header("X-Version", "0.2")
|
||||
/// .finish())
|
||||
/// .resource("/test", |r| {
|
||||
/// r.method(Method::GET).f(|_| httpcodes::HTTPOk);
|
||||
/// r.method(Method::HEAD).f(|_| httpcodes::HTTPMethodNotAllowed);
|
||||
/// })
|
||||
/// .finish();
|
||||
/// }
|
||||
/// ```
|
||||
pub struct DefaultHeaders{
|
||||
ct: bool,
|
||||
headers: HeaderMap,
|
||||
}
|
||||
|
||||
impl DefaultHeaders {
|
||||
pub fn build() -> DefaultHeadersBuilder {
|
||||
DefaultHeadersBuilder{ct: false, headers: Some(HeaderMap::new())}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Middleware<S> for DefaultHeaders {
|
||||
|
||||
fn response(&self, _: &mut HttpRequest<S>, mut resp: HttpResponse) -> Response {
|
||||
for (key, value) in self.headers.iter() {
|
||||
if !resp.headers().contains_key(key) {
|
||||
resp.headers_mut().insert(key, value.clone());
|
||||
}
|
||||
}
|
||||
// default content-type
|
||||
if self.ct && !resp.headers().contains_key(CONTENT_TYPE) {
|
||||
resp.headers_mut().insert(
|
||||
CONTENT_TYPE, HeaderValue::from_static("application/octet-stream"));
|
||||
}
|
||||
Response::Done(resp)
|
||||
}
|
||||
}
|
||||
|
||||
/// Structure that follows the builder pattern for building `DefaultHeaders` middleware.
|
||||
#[derive(Debug)]
|
||||
pub struct DefaultHeadersBuilder {
|
||||
ct: bool,
|
||||
headers: Option<HeaderMap>,
|
||||
}
|
||||
|
||||
impl DefaultHeadersBuilder {
|
||||
|
||||
/// Set a header.
|
||||
#[inline]
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(match_wild_err_arm))]
|
||||
pub fn header<K, V>(&mut self, key: K, value: V) -> &mut Self
|
||||
where HeaderName: HttpTryFrom<K>,
|
||||
HeaderValue: HttpTryFrom<V>
|
||||
{
|
||||
if let Some(ref mut headers) = self.headers {
|
||||
match HeaderName::try_from(key) {
|
||||
Ok(key) => {
|
||||
match HeaderValue::try_from(value) {
|
||||
Ok(value) => { headers.append(key, value); }
|
||||
Err(_) => panic!("Can not create header value"),
|
||||
}
|
||||
},
|
||||
Err(_) => panic!("Can not create header name"),
|
||||
};
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set *CONTENT-TYPE* header if response does not contain this header.
|
||||
pub fn content_type(&mut self) -> &mut Self {
|
||||
self.ct = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Finishes building and returns the built `DefaultHeaders` middleware.
|
||||
pub fn finish(&mut self) -> DefaultHeaders {
|
||||
let headers = self.headers.take().expect("cannot reuse middleware builder");
|
||||
DefaultHeaders{ ct: self.ct, headers: headers }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use http::header::CONTENT_TYPE;
|
||||
|
||||
#[test]
|
||||
fn test_default_headers() {
|
||||
let mw = DefaultHeaders::build()
|
||||
.header(CONTENT_TYPE, "0001")
|
||||
.finish();
|
||||
|
||||
let mut req = HttpRequest::default();
|
||||
|
||||
let resp = HttpResponse::Ok().finish().unwrap();
|
||||
let resp = match mw.response(&mut req, resp) {
|
||||
Response::Done(resp) => resp,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001");
|
||||
|
||||
let resp = HttpResponse::Ok().header(CONTENT_TYPE, "0002").finish().unwrap();
|
||||
let resp = match mw.response(&mut req, resp) {
|
||||
Response::Done(resp) => resp,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0002");
|
||||
}
|
||||
}
|
365
src/middleware/logger.rs
Normal file
365
src/middleware/logger.rs
Normal file
@ -0,0 +1,365 @@
|
||||
//! Request logging middleware
|
||||
use std::env;
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use libc;
|
||||
use time;
|
||||
use regex::Regex;
|
||||
|
||||
use httprequest::HttpRequest;
|
||||
use httpresponse::HttpResponse;
|
||||
use middleware::{Middleware, Started, Finished};
|
||||
|
||||
/// `Middleware` for logging request and response info to the terminal.
|
||||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// Create `Logger` middleware with the specified `format`.
|
||||
/// Default `Logger` could be created with `default` method, it uses the default format:
|
||||
///
|
||||
/// ```ignore
|
||||
/// %a %t "%r" %s %b "%{Referrer}i" "%{User-Agent}i" %T
|
||||
/// ```
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// use actix_web::Application;
|
||||
/// use actix_web::middleware::Logger;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let app = Application::new()
|
||||
/// .middleware(Logger::default())
|
||||
/// .middleware(Logger::new("%a %{User-Agent}i"))
|
||||
/// .finish();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Format
|
||||
///
|
||||
/// `%%` The percent sign
|
||||
///
|
||||
/// `%a` Remote IP-address (IP-address of proxy if using reverse proxy)
|
||||
///
|
||||
/// `%t` Time when the request was started to process
|
||||
///
|
||||
/// `%P` The process ID of the child that serviced the request
|
||||
///
|
||||
/// `%r` First line of request
|
||||
///
|
||||
/// `%s` Response status code
|
||||
///
|
||||
/// `%b` Size of response in bytes, including HTTP headers
|
||||
///
|
||||
/// `%T` Time taken to serve the request, in seconds with floating fraction in .06f format
|
||||
///
|
||||
/// `%D` Time taken to serve the request, in milliseconds
|
||||
///
|
||||
/// `%{FOO}i` request.headers['FOO']
|
||||
///
|
||||
/// `%{FOO}o` response.headers['FOO']
|
||||
///
|
||||
/// `%{FOO}e` os.environ['FOO']
|
||||
///
|
||||
pub struct Logger {
|
||||
format: Format,
|
||||
}
|
||||
|
||||
impl Logger {
|
||||
/// Create `Logger` middleware with the specified `format`.
|
||||
pub fn new(format: &str) -> Logger {
|
||||
Logger { format: Format::new(format) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Logger {
|
||||
/// Create `Logger` middleware with format:
|
||||
///
|
||||
/// ```ignore
|
||||
/// %a %t "%r" %s %b "%{Referrer}i" "%{User-Agent}i" %T
|
||||
/// ```
|
||||
fn default() -> Logger {
|
||||
Logger { format: Format::default() }
|
||||
}
|
||||
}
|
||||
|
||||
struct StartTime(time::Tm);
|
||||
|
||||
impl Logger {
|
||||
|
||||
fn log<S>(&self, req: &mut HttpRequest<S>, resp: &HttpResponse) {
|
||||
let entry_time = req.extensions().get::<StartTime>().unwrap().0;
|
||||
|
||||
let render = |fmt: &mut Formatter| {
|
||||
for unit in &self.format.0 {
|
||||
unit.render(fmt, req, resp, entry_time)?;
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
info!("{}", FormatDisplay(&render));
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Middleware<S> for Logger {
|
||||
|
||||
fn start(&self, req: &mut HttpRequest<S>) -> Started {
|
||||
req.extensions().insert(StartTime(time::now()));
|
||||
Started::Done
|
||||
}
|
||||
|
||||
fn finish(&self, req: &mut HttpRequest<S>, resp: &HttpResponse) -> Finished {
|
||||
self.log(req, resp);
|
||||
Finished::Done
|
||||
}
|
||||
}
|
||||
|
||||
/// A formatting style for the `Logger`, consisting of multiple
|
||||
/// `FormatText`s concatenated into one line.
|
||||
#[derive(Clone)]
|
||||
#[doc(hidden)]
|
||||
struct Format(Vec<FormatText>);
|
||||
|
||||
impl Default for Format {
|
||||
/// Return the default formatting style for the `Logger`:
|
||||
fn default() -> Format {
|
||||
Format::new(r#"%a %t "%r" %s %b "%{Referrer}i" "%{User-Agent}i" %T"#)
|
||||
}
|
||||
}
|
||||
|
||||
impl Format {
|
||||
/// Create a `Format` from a format string.
|
||||
///
|
||||
/// Returns `None` if the format string syntax is incorrect.
|
||||
pub fn new(s: &str) -> Format {
|
||||
trace!("Access log format: {}", s);
|
||||
let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([ioe])|[atPrsbTD]?)").unwrap();
|
||||
|
||||
let mut idx = 0;
|
||||
let mut results = Vec::new();
|
||||
for cap in fmt.captures_iter(s) {
|
||||
let m = cap.get(0).unwrap();
|
||||
let pos = m.start();
|
||||
if idx != pos {
|
||||
results.push(FormatText::Str(s[idx..pos].to_owned()));
|
||||
}
|
||||
idx = m.end();
|
||||
|
||||
if let Some(key) = cap.get(2) {
|
||||
results.push(
|
||||
match cap.get(3).unwrap().as_str() {
|
||||
"i" => FormatText::RequestHeader(key.as_str().to_owned()),
|
||||
"o" => FormatText::ResponseHeader(key.as_str().to_owned()),
|
||||
"e" => FormatText::EnvironHeader(key.as_str().to_owned()),
|
||||
_ => unreachable!(),
|
||||
})
|
||||
} else {
|
||||
let m = cap.get(1).unwrap();
|
||||
results.push(
|
||||
match m.as_str() {
|
||||
"%" => FormatText::Percent,
|
||||
"a" => FormatText::RemoteAddr,
|
||||
"t" => FormatText::RequestTime,
|
||||
"P" => FormatText::Pid,
|
||||
"r" => FormatText::RequestLine,
|
||||
"s" => FormatText::ResponseStatus,
|
||||
"b" => FormatText::ResponseSize,
|
||||
"T" => FormatText::Time,
|
||||
"D" => FormatText::TimeMillis,
|
||||
_ => FormatText::Str(m.as_str().to_owned()),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
if idx != s.len() {
|
||||
results.push(FormatText::Str(s[idx..].to_owned()));
|
||||
}
|
||||
|
||||
Format(results)
|
||||
}
|
||||
}
|
||||
|
||||
/// A string of text to be logged. This is either one of the data
|
||||
/// fields supported by the `Logger`, or a custom `String`.
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum FormatText {
|
||||
Str(String),
|
||||
Pid,
|
||||
Percent,
|
||||
RequestLine,
|
||||
RequestTime,
|
||||
ResponseStatus,
|
||||
ResponseSize,
|
||||
Time,
|
||||
TimeMillis,
|
||||
RemoteAddr,
|
||||
RequestHeader(String),
|
||||
ResponseHeader(String),
|
||||
EnvironHeader(String),
|
||||
}
|
||||
|
||||
impl FormatText {
|
||||
|
||||
fn render<S>(&self, fmt: &mut Formatter,
|
||||
req: &HttpRequest<S>,
|
||||
resp: &HttpResponse,
|
||||
entry_time: time::Tm) -> Result<(), fmt::Error>
|
||||
{
|
||||
match *self {
|
||||
FormatText::Str(ref string) => fmt.write_str(string),
|
||||
FormatText::Percent => "%".fmt(fmt),
|
||||
FormatText::RequestLine => {
|
||||
if req.query_string().is_empty() {
|
||||
fmt.write_fmt(format_args!(
|
||||
"{} {} {:?}",
|
||||
req.method(), req.path(), req.version()))
|
||||
} else {
|
||||
fmt.write_fmt(format_args!(
|
||||
"{} {}?{} {:?}",
|
||||
req.method(), req.path(), req.query_string(), req.version()))
|
||||
}
|
||||
},
|
||||
FormatText::ResponseStatus => resp.status().as_u16().fmt(fmt),
|
||||
FormatText::ResponseSize => resp.response_size().fmt(fmt),
|
||||
FormatText::Pid => unsafe{libc::getpid().fmt(fmt)},
|
||||
FormatText::Time => {
|
||||
let response_time = time::now() - entry_time;
|
||||
let response_time = (response_time.num_seconds() * 1000) as f64 +
|
||||
(response_time.num_nanoseconds().unwrap_or(0) as f64) / 1000000000.0;
|
||||
|
||||
fmt.write_fmt(format_args!("{:.6}", response_time))
|
||||
},
|
||||
FormatText::TimeMillis => {
|
||||
let response_time = time::now() - entry_time;
|
||||
let response_time_ms = (response_time.num_seconds() * 1000) as f64 +
|
||||
(response_time.num_nanoseconds().unwrap_or(0) as f64) / 1000000.0;
|
||||
|
||||
fmt.write_fmt(format_args!("{:.6}", response_time_ms))
|
||||
},
|
||||
FormatText::RemoteAddr => {
|
||||
if let Some(remote) = req.connection_info().remote() {
|
||||
return remote.fmt(fmt);
|
||||
} else {
|
||||
"-".fmt(fmt)
|
||||
}
|
||||
}
|
||||
FormatText::RequestTime => {
|
||||
entry_time.strftime("[%d/%b/%Y:%H:%M:%S %z]")
|
||||
.unwrap()
|
||||
.fmt(fmt)
|
||||
}
|
||||
FormatText::RequestHeader(ref name) => {
|
||||
let s = if let Some(val) = req.headers().get(name) {
|
||||
if let Ok(s) = val.to_str() { s } else { "-" }
|
||||
} else {
|
||||
"-"
|
||||
};
|
||||
fmt.write_fmt(format_args!("{}", s))
|
||||
}
|
||||
FormatText::ResponseHeader(ref name) => {
|
||||
let s = if let Some(val) = resp.headers().get(name) {
|
||||
if let Ok(s) = val.to_str() { s } else { "-" }
|
||||
} else {
|
||||
"-"
|
||||
};
|
||||
fmt.write_fmt(format_args!("{}", s))
|
||||
}
|
||||
FormatText::EnvironHeader(ref name) => {
|
||||
if let Ok(val) = env::var(name) {
|
||||
fmt.write_fmt(format_args!("{}", val))
|
||||
} else {
|
||||
"-".fmt(fmt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct FormatDisplay<'a>(
|
||||
&'a Fn(&mut Formatter) -> Result<(), fmt::Error>);
|
||||
|
||||
impl<'a> fmt::Display for FormatDisplay<'a> {
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
(self.0)(fmt)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use Body;
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
use time;
|
||||
use http::{Method, Version, StatusCode, Uri};
|
||||
use http::header::{self, HeaderMap};
|
||||
|
||||
#[test]
|
||||
fn test_logger() {
|
||||
let logger = Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test");
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(header::USER_AGENT, header::HeaderValue::from_static("ACTIX-WEB"));
|
||||
let mut req = HttpRequest::new(
|
||||
Method::GET, Uri::from_str("/").unwrap(), Version::HTTP_11, headers, None);
|
||||
let resp = HttpResponse::build(StatusCode::OK)
|
||||
.header("X-Test", "ttt")
|
||||
.force_close().body(Body::Empty).unwrap();
|
||||
|
||||
match logger.start(&mut req) {
|
||||
Started::Done => (),
|
||||
_ => panic!(),
|
||||
};
|
||||
match logger.finish(&mut req, &resp) {
|
||||
Finished::Done => (),
|
||||
_ => panic!(),
|
||||
}
|
||||
let entry_time = time::now();
|
||||
let render = |fmt: &mut Formatter| {
|
||||
for unit in logger.format.0.iter() {
|
||||
unit.render(fmt, &req, &resp, entry_time)?;
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
let s = format!("{}", FormatDisplay(&render));
|
||||
assert!(s.contains("ACTIX-WEB ttt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_format() {
|
||||
let format = Format::default();
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(header::USER_AGENT, header::HeaderValue::from_static("ACTIX-WEB"));
|
||||
let req = HttpRequest::new(
|
||||
Method::GET, Uri::from_str("/").unwrap(), Version::HTTP_11, headers, None);
|
||||
let resp = HttpResponse::build(StatusCode::OK)
|
||||
.force_close().body(Body::Empty).unwrap();
|
||||
let entry_time = time::now();
|
||||
|
||||
let render = |fmt: &mut Formatter| {
|
||||
for unit in format.0.iter() {
|
||||
unit.render(fmt, &req, &resp, entry_time)?;
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
let s = format!("{}", FormatDisplay(&render));
|
||||
assert!(s.contains("GET / HTTP/1.1"));
|
||||
assert!(s.contains("200 0"));
|
||||
assert!(s.contains("ACTIX-WEB"));
|
||||
|
||||
let req = HttpRequest::new(
|
||||
Method::GET, Uri::from_str("/?test").unwrap(),
|
||||
Version::HTTP_11, HeaderMap::new(), None);
|
||||
let resp = HttpResponse::build(StatusCode::OK)
|
||||
.force_close().body(Body::Empty).unwrap();
|
||||
let entry_time = time::now();
|
||||
|
||||
let render = |fmt: &mut Formatter| {
|
||||
for unit in format.0.iter() {
|
||||
unit.render(fmt, &req, &resp, entry_time)?;
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
let s = format!("{}", FormatDisplay(&render));
|
||||
assert!(s.contains("GET /?test HTTP/1.1"));
|
||||
}
|
||||
}
|
67
src/middleware/mod.rs
Normal file
67
src/middleware/mod.rs
Normal file
@ -0,0 +1,67 @@
|
||||
//! Middlewares
|
||||
use futures::Future;
|
||||
|
||||
use error::Error;
|
||||
use httprequest::HttpRequest;
|
||||
use httpresponse::HttpResponse;
|
||||
|
||||
mod logger;
|
||||
mod session;
|
||||
mod defaultheaders;
|
||||
pub use self::logger::Logger;
|
||||
pub use self::defaultheaders::{DefaultHeaders, DefaultHeadersBuilder};
|
||||
pub use self::session::{RequestSession, Session, SessionImpl, SessionBackend, SessionStorage,
|
||||
CookieSessionError, CookieSessionBackend, CookieSessionBackendBuilder};
|
||||
|
||||
/// Middleware start result
|
||||
pub enum Started {
|
||||
/// Execution completed
|
||||
Done,
|
||||
/// Moddleware error
|
||||
Err(Error),
|
||||
/// New http response got generated. If middleware generates response
|
||||
/// handler execution halts.
|
||||
Response(HttpResponse),
|
||||
/// Execution completed, runs future to completion.
|
||||
Future(Box<Future<Item=Option<HttpResponse>, Error=Error>>),
|
||||
}
|
||||
|
||||
/// Middleware execution result
|
||||
pub enum Response {
|
||||
/// Moddleware error
|
||||
Err(Error),
|
||||
/// New http response got generated
|
||||
Done(HttpResponse),
|
||||
/// Result is a future that resolves to a new http response
|
||||
Future(Box<Future<Item=HttpResponse, Error=Error>>),
|
||||
}
|
||||
|
||||
/// Middleware finish result
|
||||
pub enum Finished {
|
||||
/// Execution completed
|
||||
Done,
|
||||
/// Execution completed, but run future to completion
|
||||
Future(Box<Future<Item=(), Error=Error>>),
|
||||
}
|
||||
|
||||
/// Middleware definition
|
||||
#[allow(unused_variables)]
|
||||
pub trait Middleware<S> {
|
||||
|
||||
/// Method is called when request is ready. It may return
|
||||
/// future, which should resolve before next middleware get called.
|
||||
fn start(&self, req: &mut HttpRequest<S>) -> Started {
|
||||
Started::Done
|
||||
}
|
||||
|
||||
/// Method is called when handler returns response,
|
||||
/// but before sending http message to peer.
|
||||
fn response(&self, req: &mut HttpRequest<S>, resp: HttpResponse) -> Response {
|
||||
Response::Done(resp)
|
||||
}
|
||||
|
||||
/// Method is called after body stream get sent to peer.
|
||||
fn finish(&self, req: &mut HttpRequest<S>, resp: &HttpResponse) -> Finished {
|
||||
Finished::Done
|
||||
}
|
||||
}
|
439
src/middleware/session.rs
Normal file
439
src/middleware/session.rs
Normal file
@ -0,0 +1,439 @@
|
||||
#![allow(dead_code, unused_imports, unused_variables)]
|
||||
|
||||
use std::any::Any;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::marker::PhantomData;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde_json;
|
||||
use serde_json::error::Error as JsonError;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use http::header::{self, HeaderValue};
|
||||
use cookie::{CookieJar, Cookie, Key};
|
||||
use futures::Future;
|
||||
use futures::future::{FutureResult, ok as FutOk, err as FutErr};
|
||||
|
||||
use error::{Result, Error, ResponseError};
|
||||
use httprequest::HttpRequest;
|
||||
use httpresponse::HttpResponse;
|
||||
use middleware::{Middleware, Started, Response};
|
||||
|
||||
/// The helper trait to obtain your session data from a request.
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::*;
|
||||
/// use actix_web::middleware::RequestSession;
|
||||
///
|
||||
/// 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 {
|
||||
fn session(&mut self) -> Session;
|
||||
}
|
||||
|
||||
impl RequestSession for HttpRequest {
|
||||
|
||||
fn session(&mut self) -> Session {
|
||||
if let Some(s_impl) = self.extensions().get_mut::<Arc<SessionImplBox>>() {
|
||||
if let Some(s) = Arc::get_mut(s_impl) {
|
||||
return Session(s.0.as_mut())
|
||||
}
|
||||
}
|
||||
//Session(&mut DUMMY)
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
/// The high-level interface you use to modify session data.
|
||||
///
|
||||
/// Session object could be obtained with
|
||||
/// [`RequestSession::session`](trait.RequestSession.html#tymethod.session)
|
||||
/// method. `RequestSession` trait is implemented for `HttpRequest`.
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::*;
|
||||
/// use actix_web::middleware::RequestSession;
|
||||
///
|
||||
/// 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<'a>(&'a mut SessionImpl);
|
||||
|
||||
impl<'a> Session<'a> {
|
||||
|
||||
/// Get a `value` from the session.
|
||||
pub fn get<T: Deserialize<'a>>(&'a self, key: &str) -> Result<Option<T>> {
|
||||
if let Some(s) = self.0.get(key) {
|
||||
Ok(Some(serde_json::from_str(s)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a `value` from the session.
|
||||
pub fn set<T: Serialize>(&'a mut self, key: &str, value: T) -> Result<()> {
|
||||
self.0.set(key, serde_json::to_string(&value)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove value from the session.
|
||||
pub fn remove(&'a mut self, key: &str) {
|
||||
self.0.remove(key)
|
||||
}
|
||||
|
||||
/// Clear the session.
|
||||
pub fn clear(&'a mut self) {
|
||||
self.0.clear()
|
||||
}
|
||||
}
|
||||
|
||||
struct SessionImplBox(Box<SessionImpl>);
|
||||
|
||||
#[doc(hidden)]
|
||||
unsafe impl Send for SessionImplBox {}
|
||||
#[doc(hidden)]
|
||||
unsafe impl Sync for SessionImplBox {}
|
||||
|
||||
/// Session storage middleware
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix;
|
||||
/// # extern crate actix_web;
|
||||
/// # use actix_web::middleware::{SessionStorage, CookieSessionBackend};
|
||||
/// use actix_web::*;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let app = Application::new().middleware(
|
||||
/// SessionStorage::new( // <- create session middleware
|
||||
/// CookieSessionBackend::build(&[0; 32]) // <- create cookie session backend
|
||||
/// .secure(false)
|
||||
/// .finish())
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
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: &mut HttpRequest<S>) -> Started {
|
||||
let mut req = req.clone();
|
||||
|
||||
let fut = self.0.from_request(&mut req)
|
||||
.then(move |res| {
|
||||
match res {
|
||||
Ok(sess) => {
|
||||
req.extensions().insert(Arc::new(SessionImplBox(Box::new(sess))));
|
||||
FutOk(None)
|
||||
},
|
||||
Err(err) => FutErr(err)
|
||||
}
|
||||
});
|
||||
Started::Future(Box::new(fut))
|
||||
}
|
||||
|
||||
fn response(&self, req: &mut HttpRequest<S>, resp: HttpResponse) -> Response {
|
||||
if let Some(s_box) = req.extensions().remove::<Arc<SessionImplBox>>() {
|
||||
s_box.0.write(resp)
|
||||
} else {
|
||||
Response::Done(resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple key-value storage interface that is internally used by `Session`.
|
||||
#[doc(hidden)]
|
||||
pub trait SessionImpl: 'static {
|
||||
|
||||
fn get(&self, key: &str) -> Option<&str>;
|
||||
|
||||
fn set(&mut self, key: &str, value: String);
|
||||
|
||||
fn remove(&mut self, key: &str);
|
||||
|
||||
fn clear(&mut self);
|
||||
|
||||
/// Write session to storage backend.
|
||||
fn write(&self, resp: HttpResponse) -> Response;
|
||||
}
|
||||
|
||||
/// Session's storage backend trait definition.
|
||||
#[doc(hidden)]
|
||||
pub trait SessionBackend<S>: Sized + 'static {
|
||||
type Session: SessionImpl;
|
||||
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;
|
||||
}
|
||||
|
||||
/// Dummy session impl, does not do anything
|
||||
struct DummySessionImpl;
|
||||
|
||||
static DUMMY: DummySessionImpl = DummySessionImpl;
|
||||
|
||||
impl SessionImpl for DummySessionImpl {
|
||||
|
||||
fn get(&self, key: &str) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
fn set(&mut self, key: &str, value: String) {}
|
||||
fn remove(&mut self, key: &str) {}
|
||||
fn clear(&mut self) {}
|
||||
fn write(&self, resp: HttpResponse) -> Response {
|
||||
Response::Done(resp)
|
||||
}
|
||||
}
|
||||
|
||||
/// Session that uses signed cookies as session storage
|
||||
pub struct CookieSession {
|
||||
changed: bool,
|
||||
state: HashMap<String, String>,
|
||||
inner: Rc<CookieSessionInner>,
|
||||
}
|
||||
|
||||
/// Errors that can occure during handling cookie session
|
||||
#[derive(Fail, Debug)]
|
||||
pub enum CookieSessionError {
|
||||
/// Size of the serialized session is greater than 4000 bytes.
|
||||
#[fail(display="Size of the serialized session is greater than 4000 bytes.")]
|
||||
Overflow,
|
||||
/// Fail to serialize session.
|
||||
#[fail(display="Fail to serialize session")]
|
||||
Serialize(JsonError),
|
||||
}
|
||||
|
||||
impl ResponseError for CookieSessionError {}
|
||||
|
||||
impl SessionImpl for CookieSession {
|
||||
|
||||
fn get(&self, key: &str) -> Option<&str> {
|
||||
if let Some(s) = self.state.get(key) {
|
||||
Some(s)
|
||||
} 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) -> Response {
|
||||
if self.changed {
|
||||
let _ = self.inner.set_cookie(&mut resp, &self.state);
|
||||
}
|
||||
Response::Done(resp)
|
||||
}
|
||||
}
|
||||
|
||||
struct CookieSessionInner {
|
||||
key: Key,
|
||||
name: String,
|
||||
path: String,
|
||||
domain: Option<String>,
|
||||
secure: bool,
|
||||
}
|
||||
|
||||
impl CookieSessionInner {
|
||||
|
||||
fn new(key: &[u8]) -> CookieSessionInner {
|
||||
CookieSessionInner {
|
||||
key: Key::from_master(key),
|
||||
name: "actix_session".to_owned(),
|
||||
path: "/".to_owned(),
|
||||
domain: None,
|
||||
secure: true }
|
||||
}
|
||||
|
||||
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(true);
|
||||
|
||||
if let Some(ref domain) = self.domain {
|
||||
cookie.set_domain(domain.clone());
|
||||
}
|
||||
|
||||
let mut jar = CookieJar::new();
|
||||
jar.signed(&self.key).add(cookie);
|
||||
|
||||
for cookie in jar.delta() {
|
||||
let val = HeaderValue::from_str(&cookie.to_string())?;
|
||||
resp.headers_mut().append(header::SET_COOKIE, val);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load<S>(&self, req: &mut HttpRequest<S>) -> HashMap<String, String> {
|
||||
if let Ok(cookies) = req.cookies() {
|
||||
for cookie in cookies {
|
||||
if cookie.name() == self.name {
|
||||
let mut jar = CookieJar::new();
|
||||
jar.add_original(cookie.clone());
|
||||
if let Some(cookie) = jar.signed(&self.key).get(&self.name) {
|
||||
if let Ok(val) = serde_json::from_str(cookie.value()) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
HashMap::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Use signed cookies as session storage.
|
||||
///
|
||||
/// `CookieSessionBackend` creates sessions which are limited to storing
|
||||
/// fewer than 4000 bytes of data (as the payload must fit into a single cookie).
|
||||
/// Internal server error get generated if session contains more than 4000 bytes.
|
||||
///
|
||||
/// You need to pass a random value to the constructor of `CookieSessionBackend`.
|
||||
/// This is private key for cookie session, When this value is changed, all session data is lost.
|
||||
///
|
||||
/// Note that whatever you write into your session is visible by the user (but not modifiable).
|
||||
///
|
||||
/// Constructor panics if key length is less than 32 bytes.
|
||||
pub struct CookieSessionBackend(Rc<CookieSessionInner>);
|
||||
|
||||
impl CookieSessionBackend {
|
||||
|
||||
/// Construct new `CookieSessionBackend` instance.
|
||||
///
|
||||
/// Panics if key length is less than 32 bytes.
|
||||
pub fn new(key: &[u8]) -> CookieSessionBackend {
|
||||
CookieSessionBackend(
|
||||
Rc::new(CookieSessionInner::new(key)))
|
||||
}
|
||||
|
||||
/// Creates a new `CookieSessionBackendBuilder` instance from the given key.
|
||||
///
|
||||
/// Panics if key length is less than 32 bytes.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::middleware::CookieSessionBackend;
|
||||
///
|
||||
/// let backend = CookieSessionBackend::build(&[0; 32]).finish();
|
||||
/// ```
|
||||
pub fn build(key: &[u8]) -> CookieSessionBackendBuilder {
|
||||
CookieSessionBackendBuilder::new(key)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
state: state,
|
||||
inner: Rc::clone(&self.0),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Structure that follows the builder pattern for building `CookieSessionBackend` structs.
|
||||
///
|
||||
/// To construct a backend:
|
||||
///
|
||||
/// 1. Call [`CookieSessionBackend::build`](struct.CookieSessionBackend.html#method.build) to start building.
|
||||
/// 2. Use any of the builder methods to set fields in the backend.
|
||||
/// 3. Call [finish](#method.finish) to retrieve the constructed backend.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
///
|
||||
/// use actix_web::middleware::CookieSessionBackend;
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let backend: CookieSessionBackend = CookieSessionBackend::build(&[0; 32])
|
||||
/// .domain("www.rust-lang.org")
|
||||
/// .path("/")
|
||||
/// .secure(true)
|
||||
/// .finish();
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct CookieSessionBackendBuilder(CookieSessionInner);
|
||||
|
||||
impl CookieSessionBackendBuilder {
|
||||
pub fn new(key: &[u8]) -> CookieSessionBackendBuilder {
|
||||
CookieSessionBackendBuilder(
|
||||
CookieSessionInner::new(key))
|
||||
}
|
||||
|
||||
/// Sets the `path` field in the session cookie being built.
|
||||
pub fn path<S: Into<String>>(mut self, value: S) -> CookieSessionBackendBuilder {
|
||||
self.0.path = value.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `domain` field in the session cookie being built.
|
||||
pub fn domain<S: Into<String>>(mut self, value: S) -> CookieSessionBackendBuilder {
|
||||
self.0.domain = Some(value.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the `secure` field in the session cookie being built.
|
||||
pub fn secure(mut self, value: bool) -> CookieSessionBackendBuilder {
|
||||
self.0.secure = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Finishes building and returns the built `CookieSessionBackend`.
|
||||
pub fn finish(self) -> CookieSessionBackend {
|
||||
CookieSessionBackend(Rc::new(self.0))
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user