mirror of https://github.com/fafhrd91/actix-web
Merge pull request #58 from actix/master
actix#1507 actix#1512 actix#1496
This commit is contained in:
commit
5009ac2cae
|
@ -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",
|
||||
|
|
|
@ -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
|
|
@ -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"
|
|
@ -1 +0,0 @@
|
|||
../LICENSE-APACHE
|
|
@ -1 +0,0 @@
|
|||
../LICENSE-MIT
|
File diff suppressed because it is too large
Load Diff
|
@ -3,6 +3,7 @@
|
|||
## [Unreleased] - 2020-xx-xx
|
||||
|
||||
* Bump minimum supported Rust version to 1.40
|
||||
* Support sending Content-Length when Content-Range is specified #1384
|
||||
|
||||
## [0.2.1] - 2019-12-22
|
||||
|
||||
|
|
|
@ -952,135 +952,92 @@ mod tests {
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn test_named_file_content_range_headers() {
|
||||
let mut srv = test::init_service(
|
||||
App::new().service(Files::new("/test", ".").index_file("tests/test.binary")),
|
||||
)
|
||||
.await;
|
||||
let srv = test::start(|| {
|
||||
App::new().service(Files::new("/", "."))
|
||||
});
|
||||
|
||||
// Valid range header
|
||||
let request = TestRequest::get()
|
||||
.uri("/t%65st/tests/test.binary")
|
||||
let response = srv
|
||||
.get("/tests/test.binary")
|
||||
.header(header::RANGE, "bytes=10-20")
|
||||
.to_request();
|
||||
|
||||
let response = test::call_service(&mut srv, request).await;
|
||||
let contentrange = response
|
||||
.headers()
|
||||
.get(header::CONTENT_RANGE)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(contentrange, "bytes 10-20/100");
|
||||
let content_range = response.headers().get(header::CONTENT_RANGE).unwrap();
|
||||
assert_eq!(content_range.to_str().unwrap(), "bytes 10-20/100");
|
||||
|
||||
// Invalid range header
|
||||
let request = TestRequest::get()
|
||||
.uri("/t%65st/tests/test.binary")
|
||||
let response = srv
|
||||
.get("/tests/test.binary")
|
||||
.header(header::RANGE, "bytes=10-5")
|
||||
.to_request();
|
||||
let response = test::call_service(&mut srv, request).await;
|
||||
|
||||
let contentrange = response
|
||||
.headers()
|
||||
.get(header::CONTENT_RANGE)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(contentrange, "bytes */100");
|
||||
let content_range = response.headers().get(header::CONTENT_RANGE).unwrap();
|
||||
assert_eq!(content_range.to_str().unwrap(), "bytes */100");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_named_file_content_length_headers() {
|
||||
// use actix_web::body::{MessageBody, ResponseBody};
|
||||
|
||||
let mut srv = test::init_service(
|
||||
App::new().service(Files::new("test", ".").index_file("tests/test.binary")),
|
||||
)
|
||||
.await;
|
||||
let srv = test::start(|| {
|
||||
App::new().service(Files::new("/", "."))
|
||||
});
|
||||
|
||||
// Valid range header
|
||||
let request = TestRequest::get()
|
||||
.uri("/t%65st/tests/test.binary")
|
||||
let response = srv
|
||||
.get("/tests/test.binary")
|
||||
.header(header::RANGE, "bytes=10-20")
|
||||
.to_request();
|
||||
let _response = test::call_service(&mut srv, request).await;
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let content_length = response.headers().get(header::CONTENT_LENGTH).unwrap();
|
||||
assert_eq!(content_length.to_str().unwrap(), "11");
|
||||
|
||||
// let contentlength = response
|
||||
// .headers()
|
||||
// .get(header::CONTENT_LENGTH)
|
||||
// .unwrap()
|
||||
// .to_str()
|
||||
// .unwrap();
|
||||
// assert_eq!(contentlength, "11");
|
||||
|
||||
// Invalid range header
|
||||
let request = TestRequest::get()
|
||||
.uri("/t%65st/tests/test.binary")
|
||||
.header(header::RANGE, "bytes=10-8")
|
||||
.to_request();
|
||||
let response = test::call_service(&mut srv, request).await;
|
||||
assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
|
||||
// Valid range header, starting from 0
|
||||
let response = srv
|
||||
.get("/tests/test.binary")
|
||||
.header(header::RANGE, "bytes=0-20")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let content_length = response.headers().get(header::CONTENT_LENGTH).unwrap();
|
||||
assert_eq!(content_length.to_str().unwrap(), "21");
|
||||
|
||||
// Without range header
|
||||
let request = TestRequest::get()
|
||||
.uri("/t%65st/tests/test.binary")
|
||||
// .no_default_headers()
|
||||
.to_request();
|
||||
let _response = test::call_service(&mut srv, request).await;
|
||||
let mut response = srv.get("/tests/test.binary").send().await.unwrap();
|
||||
let content_length = response.headers().get(header::CONTENT_LENGTH).unwrap();
|
||||
assert_eq!(content_length.to_str().unwrap(), "100");
|
||||
|
||||
// let contentlength = response
|
||||
// .headers()
|
||||
// .get(header::CONTENT_LENGTH)
|
||||
// .unwrap()
|
||||
// .to_str()
|
||||
// .unwrap();
|
||||
// assert_eq!(contentlength, "100");
|
||||
// Should be no transfer-encoding
|
||||
let transfer_encoding = response.headers().get(header::TRANSFER_ENCODING);
|
||||
assert!(transfer_encoding.is_none());
|
||||
|
||||
// chunked
|
||||
let request = TestRequest::get()
|
||||
.uri("/t%65st/tests/test.binary")
|
||||
.to_request();
|
||||
let response = test::call_service(&mut srv, request).await;
|
||||
|
||||
// with enabled compression
|
||||
// {
|
||||
// let te = response
|
||||
// .headers()
|
||||
// .get(header::TRANSFER_ENCODING)
|
||||
// .unwrap()
|
||||
// .to_str()
|
||||
// .unwrap();
|
||||
// assert_eq!(te, "chunked");
|
||||
// }
|
||||
|
||||
let bytes = test::read_body(response).await;
|
||||
// Check file contents
|
||||
let bytes = response.body().await.unwrap();
|
||||
let data = Bytes::from(fs::read("tests/test.binary").unwrap());
|
||||
assert_eq!(bytes, data);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_head_content_length_headers() {
|
||||
let mut srv = test::init_service(
|
||||
App::new().service(Files::new("test", ".").index_file("tests/test.binary")),
|
||||
)
|
||||
.await;
|
||||
let srv = test::start(|| {
|
||||
App::new().service(Files::new("/", "."))
|
||||
});
|
||||
|
||||
// Valid range header
|
||||
let request = TestRequest::default()
|
||||
.method(Method::HEAD)
|
||||
.uri("/t%65st/tests/test.binary")
|
||||
.to_request();
|
||||
let _response = test::call_service(&mut srv, request).await;
|
||||
let response = srv
|
||||
.head("/tests/test.binary")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// TODO: fix check
|
||||
// let contentlength = response
|
||||
// .headers()
|
||||
// .get(header::CONTENT_LENGTH)
|
||||
// .unwrap()
|
||||
// .to_str()
|
||||
// .unwrap();
|
||||
// assert_eq!(contentlength, "100");
|
||||
let content_length = response
|
||||
.headers()
|
||||
.get(header::CONTENT_LENGTH)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(content_length, "100");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
|
|
@ -388,11 +388,12 @@ impl NamedFile {
|
|||
fut: None,
|
||||
counter: 0,
|
||||
};
|
||||
|
||||
if offset != 0 || length != self.md.len() {
|
||||
Ok(resp.status(StatusCode::PARTIAL_CONTENT).streaming(reader))
|
||||
} else {
|
||||
Ok(resp.body(SizedStream::new(length, reader)))
|
||||
resp.status(StatusCode::PARTIAL_CONTENT);
|
||||
}
|
||||
|
||||
Ok(resp.body(SizedStream::new(length, reader)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
))))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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
|
|
@ -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"
|
|
@ -1 +0,0 @@
|
|||
../LICENSE-APACHE
|
|
@ -1 +0,0 @@
|
|||
../LICENSE-MIT
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
|
@ -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"
|
|
@ -1 +0,0 @@
|
|||
../LICENSE-APACHE
|
|
@ -1 +0,0 @@
|
|||
../LICENSE-MIT
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in New Issue