Merge branch 'master' into 1384

This commit is contained in:
Omid Rad 2020-05-17 09:40:17 +02:00 committed by GitHub
commit c99a17f913
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 134 additions and 3460 deletions

View File

@ -2,11 +2,21 @@
## [Unreleased]
### Added
* Add option to create `Data<T>` from `Arc<T>` [#1509]
### Changed
* Resources and Scopes can now access non-overridden data types set on App (or containing scopes) when setting their own data. [#1486]
* Fix audit issue logging by default peer address [#1485]
* Bump minimum supported Rust version to 1.40
[#1485]: https://github.com/actix/actix-web/pull/1485
[#1509]: https://github.com/actix/actix-web/pull/1509
## [3.0.0-alpha.2] - 2020-05-08
### Changed
@ -21,6 +31,7 @@
[#1452]: https://github.com/actix/actix-web/pull/1452
[#1486]: https://github.com/actix/actix-web/pull/1486
## [3.0.0-alpha.1] - 2020-03-11
### Added

View File

@ -30,11 +30,8 @@ members = [
".",
"awc",
"actix-http",
# "actix-cors",
"actix-files",
"actix-framed",
# "actix-session",
# "actix-identity",
"actix-multipart",
"actix-web-actors",
"actix-web-codegen",

View File

@ -1,15 +0,0 @@
# Changes
## [0.2.0] - 2019-12-20
* Release
## [0.2.0-alpha.3] - 2019-12-07
* Migrate to actix-web 2.0.0
* Bump `derive_more` crate version to 0.99.0
## [0.1.0] - 2019-06-15
* Move cors middleware to separate crate

View File

@ -1,26 +0,0 @@
[package]
name = "actix-cors"
version = "0.2.0"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Cross-origin resource sharing (CORS) for Actix applications."
readme = "README.md"
keywords = ["web", "framework"]
homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web.git"
documentation = "https://docs.rs/actix-cors/"
license = "MIT/Apache-2.0"
edition = "2018"
workspace = ".."
[lib]
name = "actix_cors"
path = "src/lib.rs"
[dependencies]
actix-web = "2.0.0-rc"
actix-service = "1.0.1"
derive_more = "0.99.2"
futures = "0.3.1"
[dev-dependencies]
actix-rt = "1.0.0"

View File

@ -1 +0,0 @@
../LICENSE-APACHE

View File

@ -1 +0,0 @@
../LICENSE-MIT

File diff suppressed because it is too large Load Diff

View File

@ -189,7 +189,7 @@ impl MessageBody for Body {
if len == 0 {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(mem::replace(bin, Bytes::new()))))
Poll::Ready(Some(Ok(mem::take(bin))))
}
}
Body::Message(ref mut body) => Pin::new(body.as_mut()).poll_next(cx),
@ -307,7 +307,7 @@ impl MessageBody for Bytes {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(mem::replace(self.get_mut(), Bytes::new()))))
Poll::Ready(Some(Ok(mem::take(self.get_mut()))))
}
}
}
@ -324,9 +324,7 @@ impl MessageBody for BytesMut {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(
mem::replace(self.get_mut(), BytesMut::new()).freeze()
)))
Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze())))
}
}
}
@ -344,7 +342,7 @@ impl MessageBody for &'static str {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(Bytes::from_static(
mem::replace(self.get_mut(), "").as_ref(),
mem::take(self.get_mut()).as_ref(),
))))
}
}
@ -362,10 +360,7 @@ impl MessageBody for Vec<u8> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(Bytes::from(mem::replace(
self.get_mut(),
Vec::new(),
)))))
Poll::Ready(Some(Ok(Bytes::from(mem::take(self.get_mut())))))
}
}
}
@ -383,7 +378,7 @@ impl MessageBody for String {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(Bytes::from(
mem::replace(self.get_mut(), String::new()).into_bytes(),
mem::take(self.get_mut()).into_bytes(),
))))
}
}

View File

@ -1,5 +1,5 @@
use std::collections::HashSet;
use std::mem::replace;
use std::mem;
use time::{Duration, OffsetDateTime};
@ -273,7 +273,7 @@ impl CookieJar {
)]
pub fn clear(&mut self) {
self.delta_cookies.clear();
for delta in replace(&mut self.original_cookies, HashSet::new()) {
for delta in mem::take(&mut self.original_cookies) {
self.remove(delta.cookie);
}
}

View File

@ -106,7 +106,7 @@ impl<B: MessageBody> MessageBody for EncoderBody<B> {
if b.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(std::mem::replace(b, Bytes::new()))))
Poll::Ready(Some(Ok(std::mem::take(b))))
}
}
EncoderBody::Stream(b) => b.poll_next(cx),

View File

@ -1,6 +1,3 @@
// Because MSRV is 1.39.0.
#![allow(clippy::mem_replace_with_default)]
use std::collections::VecDeque;
use std::future::Future;
use std::pin::Pin;
@ -795,13 +792,10 @@ where
let inner_p = inner.as_mut().project();
let mut parts = FramedParts::with_read_buf(
inner_p.io.take().unwrap(),
std::mem::replace(inner_p.codec, Codec::default()),
std::mem::replace(inner_p.read_buf, BytesMut::default()),
);
parts.write_buf = std::mem::replace(
inner_p.write_buf,
BytesMut::default(),
std::mem::take(inner_p.codec),
std::mem::take(inner_p.read_buf),
);
parts.write_buf = std::mem::take(inner_p.write_buf);
let framed = Framed::from_parts(parts);
let upgrade =
inner_p.upgrade.take().unwrap().call((req, framed));

View File

@ -1,17 +0,0 @@
# Changes
## [Unreleased] - 2020-xx-xx
* Update the `time` dependency to 0.2.5
## [0.2.1] - 2020-01-10
* Fix panic with already borrowed: BorrowMutError #1263
## [0.2.0] - 2019-12-20
* Use actix-web 2.0
## [0.1.0] - 2019-06-xx
* Move identity middleware to separate crate

View File

@ -1,29 +0,0 @@
[package]
name = "actix-identity"
version = "0.2.1"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Identity service 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-identity/"
license = "MIT/Apache-2.0"
edition = "2018"
[lib]
name = "actix_identity"
path = "src/lib.rs"
[dependencies]
actix-web = { version = "2.0.0", default-features = false, features = ["secure-cookies"] }
actix-service = "1.0.5"
futures = "0.3.1"
serde = "1.0"
serde_json = "1.0"
time = { version = "0.2.5", default-features = false, features = ["std"] }
[dev-dependencies]
actix-rt = "1.0.0"
actix-http = "1.0.1"
bytes = "0.5.4"

View File

@ -1 +0,0 @@
../LICENSE-APACHE

View File

@ -1 +0,0 @@
../LICENSE-MIT

File diff suppressed because it is too large Load Diff

View File

@ -1,73 +0,0 @@
# Changes
## [Unreleased] - 2020-01-xx
* Update the `time` dependency to 0.2.5
* [#1292](https://github.com/actix/actix-web/pull/1292) Long lasting auto-prolonged session
## [0.3.0] - 2019-12-20
* Release
## [0.3.0-alpha.4] - 2019-12-xx
* Allow access to sessions also from not mutable references to the request
## [0.3.0-alpha.3] - 2019-12-xx
* Add access to the session from RequestHead for use of session from guard methods
* Migrate to `std::future`
* Migrate to `actix-web` 2.0
## [0.2.0] - 2019-07-08
* Enhanced ``actix-session`` to facilitate state changes. Use ``Session.renew()``
at successful login to cycle a session (new key/cookie but keeps state).
Use ``Session.purge()`` at logout to invalid a session cookie (and remove
from redis cache, if applicable).
## [0.1.1] - 2019-06-03
* Fix optional cookie session support
## [0.1.0] - 2019-05-18
* Use actix-web 1.0.0-rc
## [0.1.0-beta.4] - 2019-05-12
* Use actix-web 1.0.0-beta.4
## [0.1.0-beta.2] - 2019-04-28
* Add helper trait `UserSession` which allows to get session for ServiceRequest and HttpRequest
## [0.1.0-beta.1] - 2019-04-20
* Update actix-web to beta.1
* `CookieSession::max_age()` accepts value in seconds
## [0.1.0-alpha.6] - 2019-04-14
* Update actix-web alpha.6
## [0.1.0-alpha.4] - 2019-04-08
* Update actix-web
## [0.1.0-alpha.3] - 2019-04-02
* Update actix-web
## [0.1.0-alpha.2] - 2019-03-29
* Update actix-web
* Use new feature name for secure cookies
## [0.1.0-alpha.1] - 2019-03-28
* Initial impl

View File

@ -1,35 +0,0 @@
[package]
name = "actix-session"
version = "0.3.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-session/"
license = "MIT/Apache-2.0"
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 = ["actix-web/secure-cookies"]
[dependencies]
actix-web = { version = "2.0.0" }
actix-service = "1.0.5"
bytes = "0.5.4"
derive_more = "0.99.2"
futures = "0.3.1"
serde = "1.0"
serde_json = "1.0"
time = { version = "0.2.5", default-features = false, features = ["std"] }
[dev-dependencies]
actix-rt = "1.0.0"

View File

@ -1 +0,0 @@
../LICENSE-APACHE

View File

@ -1 +0,0 @@
../LICENSE-MIT

View File

@ -1,545 +0,0 @@
//! 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 std::task::{Context, Poll};
use actix_service::{Service, Transform};
use actix_web::cookie::{Cookie, CookieJar, Key, SameSite};
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::http::{header::SET_COOKIE, HeaderValue};
use actix_web::{Error, HttpMessage, ResponseError};
use derive_more::{Display, From};
use futures::future::{ok, FutureExt, LocalBoxFuture, Ready};
use serde_json::error::Error as JsonError;
use time::{Duration, OffsetDateTime};
use crate::{Session, SessionStatus};
/// 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>,
expires_in: 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,
expires_in: 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(expires_in) = self.expires_in {
cookie.set_expires(OffsetDateTime::now() + expires_in);
}
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(())
}
/// invalidates session cookie
fn remove_cookie<B>(&self, res: &mut ServiceResponse<B>) -> Result<(), Error> {
let mut cookie = Cookie::named(self.name.clone());
cookie.set_value("");
cookie.set_max_age(Duration::zero());
cookie.set_expires(OffsetDateTime::now() - Duration::days(365));
let val = HeaderValue::from_str(&cookie.to_string())?;
res.headers_mut().append(SET_COOKIE, val);
Ok(())
}
fn load(&self, req: &ServiceRequest) -> (bool, 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 (false, val);
}
}
}
}
}
(true, 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::{web, App, HttpResponse, HttpServer};
///
/// fn main() {
/// let app = App::new().wrap(
/// CookieSession::signed(&[0; 32])
/// .domain("www.rust-lang.org")
/// .name("actix_session")
/// .path("/")
/// .secure(true))
/// .service(web::resource("/").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(self, seconds: i64) -> CookieSession {
self.max_age_time(Duration::seconds(seconds))
}
/// Sets the `max-age` field in the session cookie being built.
pub fn max_age_time(mut self, value: time::Duration) -> CookieSession {
Rc::get_mut(&mut self.0).unwrap().max_age = Some(value);
self
}
/// Sets the `expires` field in the session cookie being built.
pub fn expires_in(self, seconds: i64) -> CookieSession {
self.expires_in_time(Duration::seconds(seconds))
}
/// Sets the `expires` field in the session cookie being built.
pub fn expires_in_time(mut self, value: Duration) -> CookieSession {
Rc::get_mut(&mut self.0).unwrap().expires_in = Some(value);
self
}
}
impl<S, B: 'static> Transform<S> for CookieSession
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>>,
S::Future: 'static,
S::Error: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = S::Error;
type InitError = ();
type Transform = CookieSessionMiddleware<S>;
type Future = Ready<Result<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, B: 'static> Service for CookieSessionMiddleware<S>
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>>,
S::Future: 'static,
S::Error: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = S::Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
/// On first request, a new session cookie is returned in response, regardless
/// of whether any session state is set. With subsequent requests, if the
/// session state changes, then set-cookie is returned in response. As
/// a user logs out, call session.purge() to set SessionStatus accordingly
/// and this will trigger removal of the session cookie in the response.
fn call(&mut self, mut req: ServiceRequest) -> Self::Future {
let inner = self.inner.clone();
let (is_new, state) = self.inner.load(&req);
let prolong_expiration = self.inner.expires_in.is_some();
Session::set_session(state.into_iter(), &mut req);
let fut = self.service.call(req);
async move {
fut.await.map(|mut res| {
match Session::get_changes(&mut res) {
(SessionStatus::Changed, Some(state))
| (SessionStatus::Renewed, Some(state)) => {
res.checked_expr(|res| inner.set_cookie(res, state))
}
(SessionStatus::Unchanged, Some(state)) if prolong_expiration => {
res.checked_expr(|res| inner.set_cookie(res, state))
}
(SessionStatus::Unchanged, _) =>
// set a new session cookie upon first request (new client)
{
if is_new {
let state: HashMap<String, String> = HashMap::new();
res.checked_expr(|res| {
inner.set_cookie(res, state.into_iter())
})
} else {
res
}
}
(SessionStatus::Purged, _) => {
let _ = inner.remove_cookie(&mut res);
res
}
_ => res,
}
})
}
.boxed_local()
}
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::{test, web, App};
use bytes::Bytes;
#[actix_rt::test]
async fn cookie_session() {
let mut app = test::init_service(
App::new()
.wrap(CookieSession::signed(&[0; 32]).secure(false))
.service(web::resource("/").to(|ses: Session| {
async move {
let _ = ses.set("counter", 100);
"test"
}
})),
)
.await;
let request = test::TestRequest::get().to_request();
let response = app.call(request).await.unwrap();
assert!(response
.response()
.cookies()
.find(|c| c.name() == "actix-session")
.is_some());
}
#[actix_rt::test]
async fn private_cookie() {
let mut app = test::init_service(
App::new()
.wrap(CookieSession::private(&[0; 32]).secure(false))
.service(web::resource("/").to(|ses: Session| {
async move {
let _ = ses.set("counter", 100);
"test"
}
})),
)
.await;
let request = test::TestRequest::get().to_request();
let response = app.call(request).await.unwrap();
assert!(response
.response()
.cookies()
.find(|c| c.name() == "actix-session")
.is_some());
}
#[actix_rt::test]
async fn cookie_session_extractor() {
let mut app = test::init_service(
App::new()
.wrap(CookieSession::signed(&[0; 32]).secure(false))
.service(web::resource("/").to(|ses: Session| {
async move {
let _ = ses.set("counter", 100);
"test"
}
})),
)
.await;
let request = test::TestRequest::get().to_request();
let response = app.call(request).await.unwrap();
assert!(response
.response()
.cookies()
.find(|c| c.name() == "actix-session")
.is_some());
}
#[actix_rt::test]
async fn basics() {
let mut app = test::init_service(
App::new()
.wrap(
CookieSession::signed(&[0; 32])
.path("/test/")
.name("actix-test")
.domain("localhost")
.http_only(true)
.same_site(SameSite::Lax)
.max_age(100),
)
.service(web::resource("/").to(|ses: Session| {
async move {
let _ = ses.set("counter", 100);
"test"
}
}))
.service(web::resource("/test/").to(|ses: Session| {
async move {
let val: usize = ses.get("counter").unwrap().unwrap();
format!("counter: {}", val)
}
})),
)
.await;
let request = test::TestRequest::get().to_request();
let response = app.call(request).await.unwrap();
let cookie = response
.response()
.cookies()
.find(|c| c.name() == "actix-test")
.unwrap()
.clone();
assert_eq!(cookie.path().unwrap(), "/test/");
let request = test::TestRequest::with_uri("/test/")
.cookie(cookie)
.to_request();
let body = test::read_response(&mut app, request).await;
assert_eq!(body, Bytes::from_static(b"counter: 100"));
}
#[actix_rt::test]
async fn prolong_expiration() {
let mut app = test::init_service(
App::new()
.wrap(CookieSession::signed(&[0; 32]).secure(false).expires_in(60))
.service(web::resource("/").to(|ses: Session| {
async move {
let _ = ses.set("counter", 100);
"test"
}
}))
.service(
web::resource("/test/")
.to(|| async move { "no-changes-in-session" }),
),
)
.await;
let request = test::TestRequest::get().to_request();
let response = app.call(request).await.unwrap();
let expires_1 = response
.response()
.cookies()
.find(|c| c.name() == "actix-session")
.expect("Cookie is set")
.expires()
.expect("Expiration is set");
actix_rt::time::delay_for(std::time::Duration::from_secs(1)).await;
let request = test::TestRequest::with_uri("/test/").to_request();
let response = app.call(request).await.unwrap();
let expires_2 = response
.response()
.cookies()
.find(|c| c.name() == "actix-session")
.expect("Cookie is set")
.expires()
.expect("Expiration is set");
assert!(expires_2 - expires_1 >= Duration::seconds(1));
}
}

View File

@ -1,322 +0,0 @@
//! User sessions.
//!
//! 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.
//!
//! 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,no_run
//! use actix_web::{web, App, HttpServer, HttpResponse, Error};
//! use actix_session::{Session, CookieSession};
//!
//! fn index(session: Session) -> Result<&'static str, Error> {
//! // access session data
//! if let Some(count) = session.get::<i32>("counter")? {
//! println!("SESSION value: {}", count);
//! session.set("counter", count+1)?;
//! } else {
//! session.set("counter", 1)?;
//! }
//!
//! Ok("Welcome!")
//! }
//!
//! #[actix_rt::main]
//! async fn main() -> std::io::Result<()> {
//! HttpServer::new(
//! || App::new().wrap(
//! CookieSession::signed(&[0; 32]) // <- create cookie based session middleware
//! .secure(false)
//! )
//! .service(web::resource("/").to(|| HttpResponse::Ok())))
//! .bind("127.0.0.1:59880")?
//! .run()
//! .await
//! }
//! ```
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use actix_web::dev::{
Extensions, Payload, RequestHead, ServiceRequest, ServiceResponse,
};
use actix_web::{Error, FromRequest, HttpMessage, HttpRequest};
use futures::future::{ok, Ready};
use serde::de::DeserializeOwned;
use serde::Serialize;
use serde_json;
#[cfg(feature = "cookie-session")]
mod cookie;
#[cfg(feature = "cookie-session")]
pub use crate::cookie::CookieSession;
/// 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_session::Session;
/// use actix_web::*;
///
/// 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 Session(Rc<RefCell<SessionInner>>);
/// Helper trait that allows to get session
pub trait UserSession {
fn get_session(&self) -> Session;
}
impl UserSession for HttpRequest {
fn get_session(&self) -> Session {
Session::get_session(&mut *self.extensions_mut())
}
}
impl UserSession for ServiceRequest {
fn get_session(&self) -> Session {
Session::get_session(&mut *self.extensions_mut())
}
}
impl UserSession for RequestHead {
fn get_session(&self) -> Session {
Session::get_session(&mut *self.extensions_mut())
}
}
#[derive(PartialEq, Clone, Debug)]
pub enum SessionStatus {
Changed,
Purged,
Renewed,
Unchanged,
}
impl Default for SessionStatus {
fn default() -> SessionStatus {
SessionStatus::Unchanged
}
}
#[derive(Default)]
struct SessionInner {
state: HashMap<String, String>,
pub status: SessionStatus,
}
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(None)
}
}
/// 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();
if inner.status != SessionStatus::Purged {
inner.status = SessionStatus::Changed;
inner
.state
.insert(key.to_owned(), serde_json::to_string(&value)?);
}
Ok(())
}
/// Remove value from the session.
pub fn remove(&self, key: &str) {
let mut inner = self.0.borrow_mut();
if inner.status != SessionStatus::Purged {
inner.status = SessionStatus::Changed;
inner.state.remove(key);
}
}
/// Clear the session.
pub fn clear(&self) {
let mut inner = self.0.borrow_mut();
if inner.status != SessionStatus::Purged {
inner.status = SessionStatus::Changed;
inner.state.clear()
}
}
/// Removes session, both client and server side.
pub fn purge(&self) {
let mut inner = self.0.borrow_mut();
inner.status = SessionStatus::Purged;
inner.state.clear();
}
/// Renews the session key, assigning existing session state to new key.
pub fn renew(&self) {
let mut inner = self.0.borrow_mut();
if inner.status != SessionStatus::Purged {
inner.status = SessionStatus::Renewed;
}
}
pub fn set_session(
data: impl Iterator<Item = (String, String)>,
req: &mut ServiceRequest,
) {
let session = Session::get_session(&mut *req.extensions_mut());
let mut inner = session.0.borrow_mut();
inner.state.extend(data);
}
pub fn get_changes<B>(
res: &mut ServiceResponse<B>,
) -> (
SessionStatus,
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());
(s_impl.borrow().status.clone(), Some(state.into_iter()))
} else {
(SessionStatus::Unchanged, None)
}
}
fn get_session(extensions: &mut Extensions) -> Session {
if let Some(s_impl) = extensions.get::<Rc<RefCell<SessionInner>>>() {
return Session(Rc::clone(&s_impl));
}
let inner = Rc::new(RefCell::new(SessionInner::default()));
extensions.insert(inner.clone());
Session(inner)
}
}
/// Extractor implementation for Session type.
///
/// ```rust
/// # use actix_web::*;
/// use actix_session::Session;
///
/// 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() {}
/// ```
impl FromRequest for Session {
type Error = Error;
type Future = Ready<Result<Session, Error>>;
type Config = ();
#[inline]
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ok(Session::get_session(&mut *req.extensions_mut()))
}
}
#[cfg(test)]
mod tests {
use actix_web::{test, HttpResponse};
use super::*;
#[test]
fn session() {
let mut req = test::TestRequest::default().to_srv_request();
Session::set_session(
vec![("key".to_string(), "\"value\"".to_string())].into_iter(),
&mut req,
);
let session = Session::get_session(&mut *req.extensions_mut());
let res = session.get::<String>("key").unwrap();
assert_eq!(res, Some("value".to_string()));
session.set("key2", "value2".to_string()).unwrap();
session.remove("key");
let mut res = req.into_response(HttpResponse::Ok().finish());
let (_status, state) = Session::get_changes(&mut res);
let changes: Vec<_> = state.unwrap().collect();
assert_eq!(changes, [("key2".to_string(), "\"value2\"".to_string())]);
}
#[test]
fn get_session() {
let mut req = test::TestRequest::default().to_srv_request();
Session::set_session(
vec![("key".to_string(), "\"value\"".to_string())].into_iter(),
&mut req,
);
let session = req.get_session();
let res = session.get::<String>("key").unwrap();
assert_eq!(res, Some("value".to_string()));
}
#[test]
fn get_session_from_request_head() {
let mut req = test::TestRequest::default().to_srv_request();
Session::set_session(
vec![("key".to_string(), "\"value\"".to_string())].into_iter(),
&mut req,
);
let session = req.head_mut().get_session();
let res = session.get::<String>("key").unwrap();
assert_eq!(res, Some("value".to_string()));
}
#[test]
fn purge_session() {
let req = test::TestRequest::default().to_srv_request();
let session = Session::get_session(&mut *req.extensions_mut());
assert_eq!(session.0.borrow().status, SessionStatus::Unchanged);
session.purge();
assert_eq!(session.0.borrow().status, SessionStatus::Purged);
}
#[test]
fn renew_session() {
let req = test::TestRequest::default().to_srv_request();
let session = Session::get_session(&mut *req.extensions_mut());
assert_eq!(session.0.borrow().status, SessionStatus::Unchanged);
session.renew();
assert_eq!(session.0.borrow().status, SessionStatus::Renewed);
}
}

View File

@ -12,7 +12,7 @@ use actix_service::{fn_service, Service, ServiceFactory};
use futures::future::{join_all, ok, FutureExt, LocalBoxFuture};
use crate::config::{AppConfig, AppService};
use crate::data::{FnDataFactory, DataFactory};
use crate::data::{DataFactory, FnDataFactory};
use crate::error::Error;
use crate::guard::Guard;
use crate::request::{HttpRequest, HttpRequestPool};
@ -76,7 +76,7 @@ where
let mut config = AppService::new(config, default.clone(), self.data.clone());
// register services
std::mem::replace(&mut *self.services.borrow_mut(), Vec::new())
std::mem::take(&mut *self.services.borrow_mut())
.into_iter()
.for_each(|mut srv| srv.register(&mut config));
@ -99,7 +99,7 @@ where
});
// external resources
for mut rdef in std::mem::replace(&mut *self.external.borrow_mut(), Vec::new()) {
for mut rdef in std::mem::take(&mut *self.external.borrow_mut()) {
rmap.add(&mut rdef, None);
}

View File

@ -103,6 +103,12 @@ impl<T> Clone for Data<T> {
}
}
impl<T> From<Arc<T>> for Data<T> {
fn from(arc: Arc<T>) -> Self {
Data(arc)
}
}
impl<T: 'static> FromRequest for Data<T> {
type Config = ();
type Error = Error;
@ -281,4 +287,11 @@ mod tests {
assert_eq!(num.load(Ordering::SeqCst), 0);
}
#[actix_rt::test]
async fn test_data_from_arc() {
let data_new = Data::new(String::from("test-123"));
let data_from_arc = Data::from(Arc::new(String::from("test-123")));
assert_eq!(data_new.0, data_from_arc.0)
}
}

View File

@ -12,8 +12,8 @@ const X_FORWARDED_PROTO: &[u8] = b"x-forwarded-proto";
pub struct ConnectionInfo {
scheme: String,
host: String,
remote: Option<String>,
peer: Option<String>,
realip_remote_addr: Option<String>,
remote_addr: Option<String>,
}
impl ConnectionInfo {
@ -29,8 +29,7 @@ impl ConnectionInfo {
fn new(req: &RequestHead, cfg: &AppConfig) -> ConnectionInfo {
let mut host = None;
let mut scheme = None;
let mut remote = None;
let mut peer = None;
let mut realip_remote_addr = None;
// load forwarded header
for hdr in req.headers.get_all(&header::FORWARDED) {
@ -42,8 +41,8 @@ impl ConnectionInfo {
if let Some(val) = items.next() {
match &name.to_lowercase() as &str {
"for" => {
if remote.is_none() {
remote = Some(val.trim());
if realip_remote_addr.is_none() {
realip_remote_addr = Some(val.trim());
}
}
"proto" => {
@ -106,27 +105,25 @@ impl ConnectionInfo {
}
}
// remote addr
if remote.is_none() {
// get remote_addraddr from socketaddr
let remote_addr = req.peer_addr.map(|addr| format!("{}", addr));
if realip_remote_addr.is_none() {
if let Some(h) = req
.headers
.get(&HeaderName::from_lowercase(X_FORWARDED_FOR).unwrap())
{
if let Ok(h) = h.to_str() {
remote = h.split(',').next().map(|v| v.trim());
realip_remote_addr = h.split(',').next().map(|v| v.trim());
}
}
if remote.is_none() {
// get peeraddr from socketaddr
peer = req.peer_addr.map(|addr| format!("{}", addr));
}
}
ConnectionInfo {
peer,
remote_addr,
scheme: scheme.unwrap_or("http").to_owned(),
host: host.unwrap_or("localhost").to_owned(),
remote: remote.map(|s| s.to_owned()),
realip_remote_addr: realip_remote_addr.map(|s| s.to_owned()),
}
}
@ -155,13 +152,23 @@ impl ConnectionInfo {
&self.host
}
/// Remote socket addr of client initiated HTTP request.
/// remote_addr address of the request.
///
/// Get remote_addr address from socket address
pub fn remote_addr(&self) -> Option<&str> {
if let Some(ref remote_addr) = self.remote_addr {
Some(remote_addr)
} else {
None
}
}
/// Real ip remote addr of client initiated HTTP request.
///
/// The addr is resolved through the following headers, in this order:
///
/// - Forwarded
/// - X-Forwarded-For
/// - peer name of opened socket
/// - remote_addr name of opened socket
///
/// # Security
/// Do not use this function for security purposes, unless you can ensure the Forwarded and
@ -169,11 +176,11 @@ impl ConnectionInfo {
/// address explicitly, use
/// [`HttpRequest::peer_addr()`](../web/struct.HttpRequest.html#method.peer_addr) instead.
#[inline]
pub fn remote(&self) -> Option<&str> {
if let Some(ref r) = self.remote {
pub fn realip_remote_addr(&self) -> Option<&str> {
if let Some(ref r) = self.realip_remote_addr {
Some(r)
} else if let Some(ref peer) = self.peer {
Some(peer)
} else if let Some(ref remote_addr) = self.remote_addr {
Some(remote_addr)
} else {
None
}
@ -202,7 +209,7 @@ mod tests {
let info = req.connection_info();
assert_eq!(info.scheme(), "https");
assert_eq!(info.host(), "rust-lang.org");
assert_eq!(info.remote(), Some("192.0.2.60"));
assert_eq!(info.realip_remote_addr(), Some("192.0.2.60"));
let req = TestRequest::default()
.header(header::HOST, "rust-lang.org")
@ -211,20 +218,20 @@ mod tests {
let info = req.connection_info();
assert_eq!(info.scheme(), "http");
assert_eq!(info.host(), "rust-lang.org");
assert_eq!(info.remote(), None);
assert_eq!(info.realip_remote_addr(), None);
let req = TestRequest::default()
.header(X_FORWARDED_FOR, "192.0.2.60")
.to_http_request();
let info = req.connection_info();
assert_eq!(info.remote(), Some("192.0.2.60"));
assert_eq!(info.realip_remote_addr(), Some("192.0.2.60"));
let req = TestRequest::default()
.header(X_FORWARDED_HOST, "192.0.2.60")
.to_http_request();
let info = req.connection_info();
assert_eq!(info.host(), "192.0.2.60");
assert_eq!(info.remote(), None);
assert_eq!(info.realip_remote_addr(), None);
let req = TestRequest::default()
.header(X_FORWARDED_PROTO, "https")

View File

@ -72,12 +72,21 @@ use crate::HttpResponse;
///
/// `%U` Request URL
///
/// `%{r}a` Real IP remote address **\***
///
/// `%{FOO}i` request.headers['FOO']
///
/// `%{FOO}o` response.headers['FOO']
///
/// `%{FOO}e` os.environ['FOO']
///
/// # Security
/// **\*** It is calculated using
/// [`ConnectionInfo::realip_remote_addr()`](../dev/struct.ConnectionInfo.html#method.realip_remote_addr)
///
/// If you use this value ensure that all requests come from trusted hosts, since it is trivial
/// for the remote client to simulate been another client.
///
pub struct Logger(Rc<Inner>);
struct Inner {
@ -301,7 +310,7 @@ impl Format {
/// Returns `None` if the format string syntax is incorrect.
pub fn new(s: &str) -> Format {
log::trace!("Access log format: {}", s);
let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([ioe])|[atPrUsbTD]?)").unwrap();
let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([aioe])|[atPrUsbTD]?)").unwrap();
let mut idx = 0;
let mut results = Vec::new();
@ -315,6 +324,11 @@ impl Format {
if let Some(key) = cap.get(2) {
results.push(match cap.get(3).unwrap().as_str() {
"a" => if key.as_str() == "r" {
FormatText::RealIPRemoteAddr
} else {
unreachable!()
},
"i" => FormatText::RequestHeader(
HeaderName::try_from(key.as_str()).unwrap(),
),
@ -362,6 +376,7 @@ pub enum FormatText {
Time,
TimeMillis,
RemoteAddr,
RealIPRemoteAddr,
UrlPath,
RequestHeader(HeaderName),
ResponseHeader(HeaderName),
@ -458,7 +473,15 @@ impl FormatText {
*self = FormatText::Str(s.to_string());
}
FormatText::RemoteAddr => {
let s = if let Some(remote) = req.connection_info().remote() {
let s = if let Some(ref peer) = req.connection_info().remote_addr() {
FormatText::Str(peer.to_string())
} else {
FormatText::Str("-".to_string())
};
*self = s;
}
FormatText::RealIPRemoteAddr => {
let s = if let Some(remote) = req.connection_info().realip_remote_addr() {
FormatText::Str(remote.to_string())
} else {
FormatText::Str("-".to_string())
@ -549,6 +572,7 @@ mod tests {
header::USER_AGENT,
header::HeaderValue::from_static("ACTIX-WEB"),
)
.peer_addr("127.0.0.1:8081".parse().unwrap())
.to_srv_request();
let now = OffsetDateTime::now_utc();
@ -570,6 +594,7 @@ mod tests {
};
let s = format!("{}", FormatDisplay(&render));
assert!(s.contains("GET / HTTP/1.1"));
assert!(s.contains("127.0.0.1"));
assert!(s.contains("200 1024"));
assert!(s.contains("ACTIX-WEB"));
}
@ -598,4 +623,36 @@ mod tests {
let s = format!("{}", FormatDisplay(&render));
assert!(s.contains(&format!("{}", now.format("%Y-%m-%dT%H:%M:%S"))));
}
#[actix_rt::test]
async fn test_remote_addr_format() {
let mut format = Format::new("%{r}a");
let req = TestRequest::with_header(
header::FORWARDED,
header::HeaderValue::from_static("for=192.0.2.60;proto=http;by=203.0.113.43"),
)
.to_srv_request();
let now = OffsetDateTime::now_utc();
for unit in &mut format.0 {
unit.render_request(now, &req);
}
let resp = HttpResponse::build(StatusCode::OK).force_close().finish();
for unit in &mut format.0 {
unit.render_response(&resp);
}
let entry_time = OffsetDateTime::now_utc();
let render = |fmt: &mut Formatter<'_>| {
for unit in &format.0 {
unit.render(fmt, 1024, entry_time)?;
}
Ok(())
};
let s = format!("{}", FormatDisplay(&render));
println!("{}", s);
assert!(s.contains("192.0.2.60"));
}
}

View File

@ -379,7 +379,7 @@ where
let guards = if self.guards.is_empty() {
None
} else {
Some(std::mem::replace(&mut self.guards, Vec::new()))
Some(std::mem::take(&mut self.guards))
};
let mut rdef = if config.is_root() || !self.rdef.is_empty() {
ResourceDef::new(insert_slash(self.rdef.clone()))

View File

@ -56,7 +56,7 @@ impl Route {
}
pub(crate) fn take_guards(&mut self) -> Vec<Box<dyn Guard>> {
std::mem::replace(Rc::get_mut(&mut self.guards).unwrap(), Vec::new())
std::mem::take(Rc::get_mut(&mut self.guards).unwrap())
}
}

View File

@ -429,7 +429,7 @@ where
let mut rmap = ResourceMap::new(ResourceDef::root_prefix(&self.rdef));
// external resources
for mut rdef in std::mem::replace(&mut self.external, Vec::new()) {
for mut rdef in std::mem::take(&mut self.external) {
rmap.add(&mut rdef, None);
}

View File

@ -515,7 +515,7 @@ where
let guards = if self.guards.is_empty() {
None
} else {
Some(std::mem::replace(&mut self.guards, Vec::new()))
Some(std::mem::take(&mut self.guards))
};
let mut rdef = if config.is_root() || !self.rdef.is_empty() {