mirror of
https://github.com/fafhrd91/actix-web
synced 2025-07-09 04:16:14 +02:00
Compare commits
9 Commits
http-test-
...
web-v1.0.0
Author | SHA1 | Date | |
---|---|---|---|
3b3dbb4f40 | |||
7300002226 | |||
5426413cb6 | |||
2bc937f6c3 | |||
60fa0d5427 | |||
f429d3319f | |||
2e19f572ee | |||
64f603b076 | |||
679d1cd513 |
@ -1,17 +1,21 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
|
## [1.0.0-beta.2] - 2019-04-24
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
* Add raw services support via `web::service()`
|
||||||
|
|
||||||
* Add helper functions for reading response body `test::read_body()`
|
* Add helper functions for reading response body `test::read_body()`
|
||||||
|
|
||||||
* Added support for `remainder match` (i.e "/path/{tail}*")
|
* Add support for `remainder match` (i.e "/path/{tail}*")
|
||||||
|
|
||||||
|
* Extend `Responder` trait, allow to override status code and headers.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
* `.to_async()` handler can return `Responder` type #792
|
* `.to_async()` handler can return `Responder` type #792
|
||||||
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
* Fix async web::Data factory handling
|
* Fix async web::Data factory handling
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-web"
|
name = "actix-web"
|
||||||
version = "1.0.0-beta.1"
|
version = "1.0.0-beta.2"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||||
description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust."
|
description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@ -71,10 +71,11 @@ actix-utils = "0.3.4"
|
|||||||
actix-router = "0.1.3"
|
actix-router = "0.1.3"
|
||||||
actix-rt = "0.2.2"
|
actix-rt = "0.2.2"
|
||||||
actix-web-codegen = "0.1.0-beta.1"
|
actix-web-codegen = "0.1.0-beta.1"
|
||||||
actix-http = { version = "0.1.2", features=["fail"] }
|
actix-http = { version = "0.1.4", features=["fail"] }
|
||||||
actix-server = "0.4.3"
|
actix-server = "0.4.3"
|
||||||
actix-server-config = "0.1.1"
|
actix-server-config = "0.1.1"
|
||||||
actix-threadpool = "0.1.0"
|
actix-threadpool = "0.1.0"
|
||||||
|
actix = { version = "0.8.1", features=["http"], optional = true }
|
||||||
awc = { version = "0.1.1", optional = true }
|
awc = { version = "0.1.1", optional = true }
|
||||||
|
|
||||||
bytes = "0.4"
|
bytes = "0.4"
|
||||||
@ -98,7 +99,7 @@ openssl = { version="0.10", optional = true }
|
|||||||
rustls = { version = "^0.15", optional = true }
|
rustls = { version = "^0.15", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-http = { version = "0.1.2", features=["ssl", "brotli", "flate2-zlib"] }
|
actix-http = { version = "0.1.4", features=["ssl", "brotli", "flate2-zlib"] }
|
||||||
actix-http-test = { version = "0.1.1", features=["ssl"] }
|
actix-http-test = { version = "0.1.1", features=["ssl"] }
|
||||||
actix-files = { version = "0.1.0-beta.1" }
|
actix-files = { version = "0.1.0-beta.1" }
|
||||||
rand = "0.6"
|
rand = "0.6"
|
||||||
|
@ -1,5 +1,16 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
|
## [0.1.4] - 2019-04-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
* Allow to render h1 request headers in `Camel-Case`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* Read until eof for http/1.0 responses #771
|
||||||
|
|
||||||
|
|
||||||
## [0.1.3] - 2019-04-23
|
## [0.1.3] - 2019-04-23
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-http"
|
name = "actix-http"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||||
description = "Actix http primitives"
|
description = "Actix http primitives"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -300,7 +300,13 @@ impl MessageType for ResponseHead {
|
|||||||
error!("MAX_BUFFER_SIZE unprocessed data reached, closing");
|
error!("MAX_BUFFER_SIZE unprocessed data reached, closing");
|
||||||
return Err(ParseError::TooLarge);
|
return Err(ParseError::TooLarge);
|
||||||
} else {
|
} else {
|
||||||
PayloadType::None
|
// for HTTP/1.0 read to eof and close connection
|
||||||
|
if msg.version == Version::HTTP_10 {
|
||||||
|
msg.set_connection_type(ConnectionType::Close);
|
||||||
|
PayloadType::Payload(PayloadDecoder::eof())
|
||||||
|
} else {
|
||||||
|
PayloadType::None
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Some((msg, decoder)))
|
Ok(Some((msg, decoder)))
|
||||||
@ -331,7 +337,7 @@ impl HeaderIndex {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
/// Http payload item
|
/// Http payload item
|
||||||
pub enum PayloadItem {
|
pub enum PayloadItem {
|
||||||
Chunk(Bytes),
|
Chunk(Bytes),
|
||||||
@ -1191,4 +1197,16 @@ mod tests {
|
|||||||
let msg = pl.decode(&mut buf).unwrap().unwrap();
|
let msg = pl.decode(&mut buf).unwrap().unwrap();
|
||||||
assert!(msg.eof());
|
assert!(msg.eof());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_response_http10_read_until_eof() {
|
||||||
|
let mut buf = BytesMut::from(&"HTTP/1.0 200 Ok\r\n\r\ntest data"[..]);
|
||||||
|
|
||||||
|
let mut reader = MessageDecoder::<ResponseHead>::default();
|
||||||
|
let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
||||||
|
let mut pl = pl.unwrap();
|
||||||
|
|
||||||
|
let chunk = pl.decode(&mut buf).unwrap().unwrap();
|
||||||
|
assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"test data")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,10 @@ pub(crate) trait MessageType: Sized {
|
|||||||
|
|
||||||
fn headers(&self) -> &HeaderMap;
|
fn headers(&self) -> &HeaderMap;
|
||||||
|
|
||||||
|
fn camel_case(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
fn chunked(&self) -> bool;
|
fn chunked(&self) -> bool;
|
||||||
|
|
||||||
fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()>;
|
fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()>;
|
||||||
@ -57,6 +61,7 @@ pub(crate) trait MessageType: Sized {
|
|||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
let chunked = self.chunked();
|
let chunked = self.chunked();
|
||||||
let mut skip_len = length != BodySize::Stream;
|
let mut skip_len = length != BodySize::Stream;
|
||||||
|
let camel_case = self.camel_case();
|
||||||
|
|
||||||
// Content length
|
// Content length
|
||||||
if let Some(status) = self.status() {
|
if let Some(status) = self.status() {
|
||||||
@ -74,18 +79,30 @@ pub(crate) trait MessageType: Sized {
|
|||||||
match length {
|
match length {
|
||||||
BodySize::Stream => {
|
BodySize::Stream => {
|
||||||
if chunked {
|
if chunked {
|
||||||
dst.put_slice(b"\r\ntransfer-encoding: chunked\r\n")
|
if camel_case {
|
||||||
|
dst.put_slice(b"\r\nTransfer-Encoding: chunked\r\n")
|
||||||
|
} else {
|
||||||
|
dst.put_slice(b"\r\nTransfer-Encoding: chunked\r\n")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
skip_len = false;
|
skip_len = false;
|
||||||
dst.put_slice(b"\r\n");
|
dst.put_slice(b"\r\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BodySize::Empty => {
|
BodySize::Empty => {
|
||||||
dst.put_slice(b"\r\ncontent-length: 0\r\n");
|
if camel_case {
|
||||||
|
dst.put_slice(b"\r\nContent-Length: 0\r\n");
|
||||||
|
} else {
|
||||||
|
dst.put_slice(b"\r\ncontent-length: 0\r\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
BodySize::Sized(len) => helpers::write_content_length(len, dst),
|
BodySize::Sized(len) => helpers::write_content_length(len, dst),
|
||||||
BodySize::Sized64(len) => {
|
BodySize::Sized64(len) => {
|
||||||
dst.put_slice(b"\r\ncontent-length: ");
|
if camel_case {
|
||||||
|
dst.put_slice(b"\r\nContent-Length: ");
|
||||||
|
} else {
|
||||||
|
dst.put_slice(b"\r\ncontent-length: ");
|
||||||
|
}
|
||||||
write!(dst.writer(), "{}\r\n", len)?;
|
write!(dst.writer(), "{}\r\n", len)?;
|
||||||
}
|
}
|
||||||
BodySize::None => dst.put_slice(b"\r\n"),
|
BodySize::None => dst.put_slice(b"\r\n"),
|
||||||
@ -95,10 +112,18 @@ pub(crate) trait MessageType: Sized {
|
|||||||
match ctype {
|
match ctype {
|
||||||
ConnectionType::Upgrade => dst.put_slice(b"connection: upgrade\r\n"),
|
ConnectionType::Upgrade => dst.put_slice(b"connection: upgrade\r\n"),
|
||||||
ConnectionType::KeepAlive if version < Version::HTTP_11 => {
|
ConnectionType::KeepAlive if version < Version::HTTP_11 => {
|
||||||
dst.put_slice(b"connection: keep-alive\r\n")
|
if camel_case {
|
||||||
|
dst.put_slice(b"Connection: keep-alive\r\n")
|
||||||
|
} else {
|
||||||
|
dst.put_slice(b"connection: keep-alive\r\n")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ConnectionType::Close if version >= Version::HTTP_11 => {
|
ConnectionType::Close if version >= Version::HTTP_11 => {
|
||||||
dst.put_slice(b"connection: close\r\n")
|
if camel_case {
|
||||||
|
dst.put_slice(b"Connection: close\r\n")
|
||||||
|
} else {
|
||||||
|
dst.put_slice(b"connection: close\r\n")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
@ -133,7 +158,12 @@ pub(crate) trait MessageType: Sized {
|
|||||||
buf = &mut *(dst.bytes_mut() as *mut _);
|
buf = &mut *(dst.bytes_mut() as *mut _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buf[pos..pos + k.len()].copy_from_slice(k);
|
// use upper Camel-Case
|
||||||
|
if camel_case {
|
||||||
|
write_camel_case(k, &mut buf[pos..pos + k.len()]);
|
||||||
|
} else {
|
||||||
|
buf[pos..pos + k.len()].copy_from_slice(k);
|
||||||
|
}
|
||||||
pos += k.len();
|
pos += k.len();
|
||||||
buf[pos..pos + 2].copy_from_slice(b": ");
|
buf[pos..pos + 2].copy_from_slice(b": ");
|
||||||
pos += 2;
|
pos += 2;
|
||||||
@ -158,7 +188,12 @@ pub(crate) trait MessageType: Sized {
|
|||||||
buf = &mut *(dst.bytes_mut() as *mut _);
|
buf = &mut *(dst.bytes_mut() as *mut _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buf[pos..pos + k.len()].copy_from_slice(k);
|
// use upper Camel-Case
|
||||||
|
if camel_case {
|
||||||
|
write_camel_case(k, &mut buf[pos..pos + k.len()]);
|
||||||
|
} else {
|
||||||
|
buf[pos..pos + k.len()].copy_from_slice(k);
|
||||||
|
}
|
||||||
pos += k.len();
|
pos += k.len();
|
||||||
buf[pos..pos + 2].copy_from_slice(b": ");
|
buf[pos..pos + 2].copy_from_slice(b": ");
|
||||||
pos += 2;
|
pos += 2;
|
||||||
@ -221,6 +256,10 @@ impl MessageType for RequestHead {
|
|||||||
self.chunked()
|
self.chunked()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn camel_case(&self) -> bool {
|
||||||
|
RequestHead::camel_case_headers(self)
|
||||||
|
}
|
||||||
|
|
||||||
fn headers(&self) -> &HeaderMap {
|
fn headers(&self) -> &HeaderMap {
|
||||||
&self.headers
|
&self.headers
|
||||||
}
|
}
|
||||||
@ -418,11 +457,41 @@ impl<'a> io::Write for Writer<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_camel_case(value: &[u8], buffer: &mut [u8]) {
|
||||||
|
let mut index = 0;
|
||||||
|
let key = value;
|
||||||
|
let mut key_iter = key.iter();
|
||||||
|
|
||||||
|
if let Some(c) = key_iter.next() {
|
||||||
|
if *c >= b'a' && *c <= b'z' {
|
||||||
|
buffer[index] = *c ^ b' ';
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(c) = key_iter.next() {
|
||||||
|
buffer[index] = *c;
|
||||||
|
index += 1;
|
||||||
|
if *c == b'-' {
|
||||||
|
if let Some(c) = key_iter.next() {
|
||||||
|
if *c >= b'a' && *c <= b'z' {
|
||||||
|
buffer[index] = *c ^ b' ';
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::http::header::{HeaderValue, CONTENT_TYPE};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_chunked_te() {
|
fn test_chunked_te() {
|
||||||
let mut bytes = BytesMut::new();
|
let mut bytes = BytesMut::new();
|
||||||
@ -436,4 +505,64 @@ mod tests {
|
|||||||
Bytes::from_static(b"4\r\ntest\r\n0\r\n\r\n")
|
Bytes::from_static(b"4\r\ntest\r\n0\r\n\r\n")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_camel_case() {
|
||||||
|
let mut bytes = BytesMut::with_capacity(2048);
|
||||||
|
let mut head = RequestHead::default();
|
||||||
|
head.set_camel_case_headers(true);
|
||||||
|
head.headers.insert(DATE, HeaderValue::from_static("date"));
|
||||||
|
head.headers
|
||||||
|
.insert(CONTENT_TYPE, HeaderValue::from_static("plain/text"));
|
||||||
|
|
||||||
|
let _ = head.encode_headers(
|
||||||
|
&mut bytes,
|
||||||
|
Version::HTTP_11,
|
||||||
|
BodySize::Empty,
|
||||||
|
ConnectionType::Close,
|
||||||
|
&ServiceConfig::default(),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
bytes.take().freeze(),
|
||||||
|
Bytes::from_static(b"\r\nContent-Length: 0\r\nConnection: close\r\nDate: date\r\nContent-Type: plain/text\r\n\r\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = head.encode_headers(
|
||||||
|
&mut bytes,
|
||||||
|
Version::HTTP_11,
|
||||||
|
BodySize::Stream,
|
||||||
|
ConnectionType::KeepAlive,
|
||||||
|
&ServiceConfig::default(),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
bytes.take().freeze(),
|
||||||
|
Bytes::from_static(b"\r\nTransfer-Encoding: chunked\r\nDate: date\r\nContent-Type: plain/text\r\n\r\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = head.encode_headers(
|
||||||
|
&mut bytes,
|
||||||
|
Version::HTTP_11,
|
||||||
|
BodySize::Sized64(100),
|
||||||
|
ConnectionType::KeepAlive,
|
||||||
|
&ServiceConfig::default(),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
bytes.take().freeze(),
|
||||||
|
Bytes::from_static(b"\r\nContent-Length: 100\r\nDate: date\r\nContent-Type: plain/text\r\n\r\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
head.headers
|
||||||
|
.append(CONTENT_TYPE, HeaderValue::from_static("xml"));
|
||||||
|
let _ = head.encode_headers(
|
||||||
|
&mut bytes,
|
||||||
|
Version::HTTP_11,
|
||||||
|
BodySize::Stream,
|
||||||
|
ConnectionType::KeepAlive,
|
||||||
|
&ServiceConfig::default(),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
bytes.take().freeze(),
|
||||||
|
Bytes::from_static(b"\r\nTransfer-Encoding: chunked\r\nDate: date\r\nContent-Type: xml\r\nContent-Type: plain/text\r\n\r\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ bitflags! {
|
|||||||
const UPGRADE = 0b0000_0100;
|
const UPGRADE = 0b0000_0100;
|
||||||
const EXPECT = 0b0000_1000;
|
const EXPECT = 0b0000_1000;
|
||||||
const NO_CHUNKING = 0b0001_0000;
|
const NO_CHUNKING = 0b0001_0000;
|
||||||
|
const CAMEL_CASE = 0b0010_0000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,6 +98,23 @@ impl RequestHead {
|
|||||||
&mut self.headers
|
&mut self.headers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Is to uppercase headers with Camel-Case.
|
||||||
|
/// Befault is `false`
|
||||||
|
#[inline]
|
||||||
|
pub fn camel_case_headers(&self) -> bool {
|
||||||
|
self.flags.contains(Flags::CAMEL_CASE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set `true` to send headers which are uppercased with Camel-Case.
|
||||||
|
#[inline]
|
||||||
|
pub fn set_camel_case_headers(&mut self, val: bool) {
|
||||||
|
if val {
|
||||||
|
self.flags.insert(Flags::CAMEL_CASE);
|
||||||
|
} else {
|
||||||
|
self.flags.remove(Flags::CAMEL_CASE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
/// Set connection type of the message
|
/// Set connection type of the message
|
||||||
pub fn set_connection_type(&mut self, ctype: ConnectionType) {
|
pub fn set_connection_type(&mut self, ctype: ConnectionType) {
|
||||||
|
@ -59,9 +59,7 @@ fn test_connection_close() {
|
|||||||
.finish(|_| ok::<_, ()>(Response::Ok().body(STR)))
|
.finish(|_| ok::<_, ()>(Response::Ok().body(STR)))
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
});
|
});
|
||||||
println!("REQ: {:?}", srv.get("/").force_close());
|
|
||||||
let response = srv.block_on(srv.get("/").force_close().send()).unwrap();
|
let response = srv.block_on(srv.get("/").force_close().send()).unwrap();
|
||||||
println!("RES: {:?}", response);
|
|
||||||
assert!(response.status().is_success());
|
assert!(response.status().is_success());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
* Allow to send headers in `Camel-Case` form.
|
||||||
|
|
||||||
|
|
||||||
## [0.1.1] - 2019-04-19
|
## [0.1.1] - 2019-04-19
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -38,7 +38,7 @@ flate2-rust = ["actix-http/flate2-rust"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
actix-codec = "0.1.2"
|
actix-codec = "0.1.2"
|
||||||
actix-service = "0.3.6"
|
actix-service = "0.3.6"
|
||||||
actix-http = "0.1.2"
|
actix-http = "0.1.4"
|
||||||
base64 = "0.10.1"
|
base64 = "0.10.1"
|
||||||
bytes = "0.4"
|
bytes = "0.4"
|
||||||
derive_more = "0.14"
|
derive_more = "0.14"
|
||||||
@ -56,8 +56,8 @@ openssl = { version="0.10", optional = true }
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "0.2.2"
|
actix-rt = "0.2.2"
|
||||||
actix-web = { version = "1.0.0-beta.1", features=["ssl"] }
|
actix-web = { version = "1.0.0-beta.1", features=["ssl"] }
|
||||||
actix-http = { version = "0.1.2", features=["ssl"] }
|
actix-http = { version = "0.1.4", features=["ssl"] }
|
||||||
actix-http-test = { version = "0.1.0", features=["ssl"] }
|
actix-http-test = { version = "0.1.1", features=["ssl"] }
|
||||||
actix-utils = "0.3.4"
|
actix-utils = "0.3.4"
|
||||||
actix-server = { version = "0.4.3", features=["ssl"] }
|
actix-server = { version = "0.4.3", features=["ssl"] }
|
||||||
brotli2 = { version="0.3.2" }
|
brotli2 = { version="0.3.2" }
|
||||||
|
@ -235,6 +235,13 @@ impl ClientRequest {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send headers in `Camel-Case` form.
|
||||||
|
#[inline]
|
||||||
|
pub fn camel_case(mut self) -> Self {
|
||||||
|
self.head.set_camel_case_headers(true);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Force close connection instead of returning it back to connections pool.
|
/// Force close connection instead of returning it back to connections pool.
|
||||||
/// This setting affect only http/1 connections.
|
/// This setting affect only http/1 connections.
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -90,6 +90,10 @@ fn test_simple() {
|
|||||||
// read response
|
// read response
|
||||||
let bytes = srv.block_on(response.body()).unwrap();
|
let bytes = srv.block_on(response.body()).unwrap();
|
||||||
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
|
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
|
||||||
|
|
||||||
|
// camel case
|
||||||
|
let response = srv.block_on(srv.post("/").camel_case().send()).unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -136,7 +136,9 @@ pub mod dev {
|
|||||||
pub use crate::config::{AppConfig, AppService};
|
pub use crate::config::{AppConfig, AppService};
|
||||||
pub use crate::info::ConnectionInfo;
|
pub use crate::info::ConnectionInfo;
|
||||||
pub use crate::rmap::ResourceMap;
|
pub use crate::rmap::ResourceMap;
|
||||||
pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse};
|
pub use crate::service::{
|
||||||
|
HttpServiceFactory, ServiceRequest, ServiceResponse, WebService,
|
||||||
|
};
|
||||||
pub use crate::types::form::UrlEncoded;
|
pub use crate::types::form::UrlEncoded;
|
||||||
pub use crate::types::json::JsonBody;
|
pub use crate::types::json::JsonBody;
|
||||||
pub use crate::types::readlines::Readlines;
|
pub use crate::types::readlines::Readlines;
|
||||||
|
@ -49,10 +49,12 @@
|
|||||||
//! ```
|
//! ```
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
use actix_service::{Service, Transform};
|
use actix_service::{Service, Transform};
|
||||||
use futures::future::{ok, Either, FutureResult};
|
use futures::future::{ok, Either, FutureResult};
|
||||||
use futures::{Future, IntoFuture, Poll};
|
use futures::{Future, IntoFuture, Poll};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use time::Duration;
|
use time::Duration;
|
||||||
|
|
||||||
use crate::cookie::{Cookie, CookieJar, Key, SameSite};
|
use crate::cookie::{Cookie, CookieJar, Key, SameSite};
|
||||||
@ -284,84 +286,149 @@ where
|
|||||||
|
|
||||||
struct CookieIdentityInner {
|
struct CookieIdentityInner {
|
||||||
key: Key,
|
key: Key,
|
||||||
|
key_v2: Key,
|
||||||
name: String,
|
name: String,
|
||||||
path: String,
|
path: String,
|
||||||
domain: Option<String>,
|
domain: Option<String>,
|
||||||
secure: bool,
|
secure: bool,
|
||||||
max_age: Option<Duration>,
|
max_age: Option<Duration>,
|
||||||
same_site: Option<SameSite>,
|
same_site: Option<SameSite>,
|
||||||
|
visit_deadline: Option<Duration>,
|
||||||
|
login_deadline: Option<Duration>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
struct CookieValue {
|
||||||
|
identity: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
login_timestamp: Option<SystemTime>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
visit_timestamp: Option<SystemTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct CookieIdentityExtention {
|
||||||
|
login_timestamp: Option<SystemTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CookieIdentityInner {
|
impl CookieIdentityInner {
|
||||||
fn new(key: &[u8]) -> CookieIdentityInner {
|
fn new(key: &[u8]) -> CookieIdentityInner {
|
||||||
|
let key_v2: Vec<u8> =
|
||||||
|
key.iter().chain([1, 0, 0, 0].iter()).map(|e| *e).collect();
|
||||||
CookieIdentityInner {
|
CookieIdentityInner {
|
||||||
key: Key::from_master(key),
|
key: Key::from_master(key),
|
||||||
|
key_v2: Key::from_master(&key_v2),
|
||||||
name: "actix-identity".to_owned(),
|
name: "actix-identity".to_owned(),
|
||||||
path: "/".to_owned(),
|
path: "/".to_owned(),
|
||||||
domain: None,
|
domain: None,
|
||||||
secure: true,
|
secure: true,
|
||||||
max_age: None,
|
max_age: None,
|
||||||
same_site: None,
|
same_site: None,
|
||||||
|
visit_deadline: None,
|
||||||
|
login_deadline: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_cookie<B>(
|
fn set_cookie<B>(
|
||||||
&self,
|
&self,
|
||||||
resp: &mut ServiceResponse<B>,
|
resp: &mut ServiceResponse<B>,
|
||||||
id: Option<String>,
|
value: Option<CookieValue>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let some = id.is_some();
|
let add_cookie = value.is_some();
|
||||||
{
|
let val = value.map(|val| {
|
||||||
let id = id.unwrap_or_else(String::new);
|
if !self.legacy_supported() {
|
||||||
let mut cookie = Cookie::new(self.name.clone(), id);
|
serde_json::to_string(&val)
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
if some {
|
|
||||||
jar.private(&self.key).add(cookie);
|
|
||||||
} else {
|
} else {
|
||||||
jar.add_original(cookie.clone());
|
Ok(val.identity)
|
||||||
jar.private(&self.key).remove(cookie);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
let mut cookie =
|
||||||
|
Cookie::new(self.name.clone(), val.unwrap_or_else(|| Ok(String::new()))?);
|
||||||
|
cookie.set_path(self.path.clone());
|
||||||
|
cookie.set_secure(self.secure);
|
||||||
|
cookie.set_http_only(true);
|
||||||
|
|
||||||
for cookie in jar.delta() {
|
if let Some(ref domain) = self.domain {
|
||||||
let val = HeaderValue::from_str(&cookie.to_string())?;
|
cookie.set_domain(domain.clone());
|
||||||
resp.headers_mut().append(header::SET_COOKIE, val);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
let key = if self.legacy_supported() {
|
||||||
|
&self.key
|
||||||
|
} else {
|
||||||
|
&self.key_v2
|
||||||
|
};
|
||||||
|
if add_cookie {
|
||||||
|
jar.private(&key).add(cookie);
|
||||||
|
} else {
|
||||||
|
jar.add_original(cookie.clone());
|
||||||
|
jar.private(&key).remove(cookie);
|
||||||
|
}
|
||||||
|
for cookie in jar.delta() {
|
||||||
|
let val = HeaderValue::from_str(&cookie.to_string())?;
|
||||||
|
resp.headers_mut().append(header::SET_COOKIE, val);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load(&self, req: &ServiceRequest) -> Option<String> {
|
fn load(&self, req: &ServiceRequest) -> Option<CookieValue> {
|
||||||
if let Ok(cookies) = req.cookies() {
|
let cookie = req.cookie(&self.name)?;
|
||||||
for cookie in cookies.iter() {
|
let mut jar = CookieJar::new();
|
||||||
if cookie.name() == self.name {
|
jar.add_original(cookie.clone());
|
||||||
let mut jar = CookieJar::new();
|
let res = if self.legacy_supported() {
|
||||||
jar.add_original(cookie.clone());
|
jar.private(&self.key).get(&self.name).map(|n| CookieValue {
|
||||||
|
identity: n.value().to_string(),
|
||||||
|
login_timestamp: None,
|
||||||
|
visit_timestamp: None,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
res.or_else(|| {
|
||||||
|
jar.private(&self.key_v2)
|
||||||
|
.get(&self.name)
|
||||||
|
.and_then(|c| self.parse(c))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
let cookie_opt = jar.private(&self.key).get(&self.name);
|
fn parse(&self, cookie: Cookie) -> Option<CookieValue> {
|
||||||
if let Some(cookie) = cookie_opt {
|
let value: CookieValue = serde_json::from_str(cookie.value()).ok()?;
|
||||||
return Some(cookie.value().into());
|
let now = SystemTime::now();
|
||||||
}
|
if let Some(visit_deadline) = self.visit_deadline {
|
||||||
}
|
if now.duration_since(value.visit_timestamp?).ok()?
|
||||||
|
> visit_deadline.to_std().ok()?
|
||||||
|
{
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
if let Some(login_deadline) = self.login_deadline {
|
||||||
|
if now.duration_since(value.login_timestamp?).ok()?
|
||||||
|
> login_deadline.to_std().ok()?
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn legacy_supported(&self) -> bool {
|
||||||
|
self.visit_deadline.is_none() && self.login_deadline.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn always_update_cookie(&self) -> bool {
|
||||||
|
self.visit_deadline.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn requires_oob_data(&self) -> bool {
|
||||||
|
self.login_deadline.is_some()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -443,6 +510,18 @@ impl CookieIdentityPolicy {
|
|||||||
Rc::get_mut(&mut self.0).unwrap().same_site = Some(same_site);
|
Rc::get_mut(&mut self.0).unwrap().same_site = Some(same_site);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Accepts only users whose cookie has been seen before the given deadline
|
||||||
|
pub fn visit_deadline(mut self, value: Duration) -> CookieIdentityPolicy {
|
||||||
|
Rc::get_mut(&mut self.0).unwrap().visit_deadline = Some(value);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accepts only users which has been authenticated before the given deadline
|
||||||
|
pub fn login_deadline(mut self, value: Duration) -> CookieIdentityPolicy {
|
||||||
|
Rc::get_mut(&mut self.0).unwrap().login_deadline = Some(value);
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IdentityPolicy for CookieIdentityPolicy {
|
impl IdentityPolicy for CookieIdentityPolicy {
|
||||||
@ -450,7 +529,19 @@ impl IdentityPolicy for CookieIdentityPolicy {
|
|||||||
type ResponseFuture = Result<(), Error>;
|
type ResponseFuture = Result<(), Error>;
|
||||||
|
|
||||||
fn from_request(&self, req: &mut ServiceRequest) -> Self::Future {
|
fn from_request(&self, req: &mut ServiceRequest) -> Self::Future {
|
||||||
Ok(self.0.load(req))
|
Ok(self.0.load(req).map(
|
||||||
|
|CookieValue {
|
||||||
|
identity,
|
||||||
|
login_timestamp,
|
||||||
|
..
|
||||||
|
}| {
|
||||||
|
if self.0.requires_oob_data() {
|
||||||
|
req.extensions_mut()
|
||||||
|
.insert(CookieIdentityExtention { login_timestamp });
|
||||||
|
}
|
||||||
|
identity
|
||||||
|
},
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_response<B>(
|
fn to_response<B>(
|
||||||
@ -459,9 +550,36 @@ impl IdentityPolicy for CookieIdentityPolicy {
|
|||||||
changed: bool,
|
changed: bool,
|
||||||
res: &mut ServiceResponse<B>,
|
res: &mut ServiceResponse<B>,
|
||||||
) -> Self::ResponseFuture {
|
) -> Self::ResponseFuture {
|
||||||
if changed {
|
let _ = if changed {
|
||||||
let _ = self.0.set_cookie(res, id);
|
let login_timestamp = SystemTime::now();
|
||||||
}
|
self.0.set_cookie(
|
||||||
|
res,
|
||||||
|
id.map(|identity| CookieValue {
|
||||||
|
identity,
|
||||||
|
login_timestamp: self.0.login_deadline.map(|_| login_timestamp),
|
||||||
|
visit_timestamp: self.0.visit_deadline.map(|_| login_timestamp),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else if self.0.always_update_cookie() && id.is_some() {
|
||||||
|
let visit_timestamp = SystemTime::now();
|
||||||
|
let mut login_timestamp = None;
|
||||||
|
if self.0.requires_oob_data() {
|
||||||
|
let CookieIdentityExtention {
|
||||||
|
login_timestamp: lt,
|
||||||
|
} = res.request().extensions_mut().remove().unwrap();
|
||||||
|
login_timestamp = lt;
|
||||||
|
}
|
||||||
|
self.0.set_cookie(
|
||||||
|
res,
|
||||||
|
Some(CookieValue {
|
||||||
|
identity: id.unwrap(),
|
||||||
|
login_timestamp,
|
||||||
|
visit_timestamp: self.0.visit_deadline.map(|_| visit_timestamp),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -473,14 +591,20 @@ mod tests {
|
|||||||
use crate::test::{self, TestRequest};
|
use crate::test::{self, TestRequest};
|
||||||
use crate::{web, App, HttpResponse};
|
use crate::{web, App, HttpResponse};
|
||||||
|
|
||||||
|
use std::borrow::Borrow;
|
||||||
|
|
||||||
|
const COOKIE_KEY_MASTER: [u8; 32] = [0; 32];
|
||||||
|
const COOKIE_NAME: &'static str = "actix_auth";
|
||||||
|
const COOKIE_LOGIN: &'static str = "test";
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_identity() {
|
fn test_identity() {
|
||||||
let mut srv = test::init_service(
|
let mut srv = test::init_service(
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(IdentityService::new(
|
.wrap(IdentityService::new(
|
||||||
CookieIdentityPolicy::new(&[0; 32])
|
CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
|
||||||
.domain("www.rust-lang.org")
|
.domain("www.rust-lang.org")
|
||||||
.name("actix_auth")
|
.name(COOKIE_NAME)
|
||||||
.path("/")
|
.path("/")
|
||||||
.secure(true),
|
.secure(true),
|
||||||
))
|
))
|
||||||
@ -492,7 +616,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.service(web::resource("/login").to(|id: Identity| {
|
.service(web::resource("/login").to(|id: Identity| {
|
||||||
id.remember("test".to_string());
|
id.remember(COOKIE_LOGIN.to_string());
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
}))
|
}))
|
||||||
.service(web::resource("/logout").to(|id: Identity| {
|
.service(web::resource("/logout").to(|id: Identity| {
|
||||||
@ -537,9 +661,9 @@ mod tests {
|
|||||||
let mut srv = test::init_service(
|
let mut srv = test::init_service(
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(IdentityService::new(
|
.wrap(IdentityService::new(
|
||||||
CookieIdentityPolicy::new(&[0; 32])
|
CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
|
||||||
.domain("www.rust-lang.org")
|
.domain("www.rust-lang.org")
|
||||||
.name("actix_auth")
|
.name(COOKIE_NAME)
|
||||||
.path("/")
|
.path("/")
|
||||||
.max_age_time(duration)
|
.max_age_time(duration)
|
||||||
.secure(true),
|
.secure(true),
|
||||||
@ -563,9 +687,9 @@ mod tests {
|
|||||||
let mut srv = test::init_service(
|
let mut srv = test::init_service(
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(IdentityService::new(
|
.wrap(IdentityService::new(
|
||||||
CookieIdentityPolicy::new(&[0; 32])
|
CookieIdentityPolicy::new(&COOKIE_KEY_MASTER)
|
||||||
.domain("www.rust-lang.org")
|
.domain("www.rust-lang.org")
|
||||||
.name("actix_auth")
|
.name(COOKIE_NAME)
|
||||||
.path("/")
|
.path("/")
|
||||||
.max_age(seconds)
|
.max_age(seconds)
|
||||||
.secure(true),
|
.secure(true),
|
||||||
@ -582,4 +706,328 @@ mod tests {
|
|||||||
let c = resp.response().cookies().next().unwrap().to_owned();
|
let c = resp.response().cookies().next().unwrap().to_owned();
|
||||||
assert_eq!(Duration::seconds(seconds as i64), c.max_age().unwrap());
|
assert_eq!(Duration::seconds(seconds as i64), c.max_age().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_identity_server<
|
||||||
|
F: Fn(CookieIdentityPolicy) -> CookieIdentityPolicy + Sync + Send + Clone + 'static,
|
||||||
|
>(
|
||||||
|
f: F,
|
||||||
|
) -> impl actix_service::Service<
|
||||||
|
Request = actix_http::Request,
|
||||||
|
Response = ServiceResponse<actix_http::body::Body>,
|
||||||
|
Error = actix_http::Error,
|
||||||
|
> {
|
||||||
|
test::init_service(
|
||||||
|
App::new()
|
||||||
|
.wrap(IdentityService::new(f(CookieIdentityPolicy::new(
|
||||||
|
&COOKIE_KEY_MASTER,
|
||||||
|
)
|
||||||
|
.secure(false)
|
||||||
|
.name(COOKIE_NAME))))
|
||||||
|
.service(web::resource("/").to(|id: Identity| {
|
||||||
|
let identity = id.identity();
|
||||||
|
if identity.is_none() {
|
||||||
|
id.remember(COOKIE_LOGIN.to_string())
|
||||||
|
}
|
||||||
|
web::Json(identity)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn legacy_login_cookie(identity: &'static str) -> Cookie<'static> {
|
||||||
|
let mut jar = CookieJar::new();
|
||||||
|
jar.private(&Key::from_master(&COOKIE_KEY_MASTER))
|
||||||
|
.add(Cookie::new(COOKIE_NAME, identity));
|
||||||
|
jar.get(COOKIE_NAME).unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn login_cookie(
|
||||||
|
identity: &'static str,
|
||||||
|
login_timestamp: Option<SystemTime>,
|
||||||
|
visit_timestamp: Option<SystemTime>,
|
||||||
|
) -> Cookie<'static> {
|
||||||
|
let mut jar = CookieJar::new();
|
||||||
|
let key: Vec<u8> = COOKIE_KEY_MASTER
|
||||||
|
.iter()
|
||||||
|
.chain([1, 0, 0, 0].iter())
|
||||||
|
.map(|e| *e)
|
||||||
|
.collect();
|
||||||
|
jar.private(&Key::from_master(&key)).add(Cookie::new(
|
||||||
|
COOKIE_NAME,
|
||||||
|
serde_json::to_string(&CookieValue {
|
||||||
|
identity: identity.to_string(),
|
||||||
|
login_timestamp,
|
||||||
|
visit_timestamp,
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
));
|
||||||
|
jar.get(COOKIE_NAME).unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_logged_in(response: &mut ServiceResponse, identity: Option<&str>) {
|
||||||
|
use bytes::BytesMut;
|
||||||
|
use futures::Stream;
|
||||||
|
let bytes =
|
||||||
|
test::block_on(response.take_body().fold(BytesMut::new(), |mut b, c| {
|
||||||
|
b.extend(c);
|
||||||
|
Ok::<_, Error>(b)
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
let resp: Option<String> = serde_json::from_slice(&bytes[..]).unwrap();
|
||||||
|
assert_eq!(resp.as_ref().map(|s| s.borrow()), identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_legacy_login_cookie(response: &mut ServiceResponse, identity: &str) {
|
||||||
|
let mut cookies = CookieJar::new();
|
||||||
|
for cookie in response.headers().get_all(header::SET_COOKIE) {
|
||||||
|
cookies.add(Cookie::parse(cookie.to_str().unwrap().to_string()).unwrap());
|
||||||
|
}
|
||||||
|
let cookie = cookies
|
||||||
|
.private(&Key::from_master(&COOKIE_KEY_MASTER))
|
||||||
|
.get(COOKIE_NAME)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(cookie.value(), identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LoginTimestampCheck {
|
||||||
|
NoTimestamp,
|
||||||
|
NewTimestamp,
|
||||||
|
OldTimestamp(SystemTime),
|
||||||
|
}
|
||||||
|
|
||||||
|
enum VisitTimeStampCheck {
|
||||||
|
NoTimestamp,
|
||||||
|
NewTimestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_login_cookie(
|
||||||
|
response: &mut ServiceResponse,
|
||||||
|
identity: &str,
|
||||||
|
login_timestamp: LoginTimestampCheck,
|
||||||
|
visit_timestamp: VisitTimeStampCheck,
|
||||||
|
) {
|
||||||
|
let mut cookies = CookieJar::new();
|
||||||
|
for cookie in response.headers().get_all(header::SET_COOKIE) {
|
||||||
|
cookies.add(Cookie::parse(cookie.to_str().unwrap().to_string()).unwrap());
|
||||||
|
}
|
||||||
|
let key: Vec<u8> = COOKIE_KEY_MASTER
|
||||||
|
.iter()
|
||||||
|
.chain([1, 0, 0, 0].iter())
|
||||||
|
.map(|e| *e)
|
||||||
|
.collect();
|
||||||
|
let cookie = cookies
|
||||||
|
.private(&Key::from_master(&key))
|
||||||
|
.get(COOKIE_NAME)
|
||||||
|
.unwrap();
|
||||||
|
let cv: CookieValue = serde_json::from_str(cookie.value()).unwrap();
|
||||||
|
assert_eq!(cv.identity, identity);
|
||||||
|
let now = SystemTime::now();
|
||||||
|
let t30sec_ago = now - Duration::seconds(30).to_std().unwrap();
|
||||||
|
match login_timestamp {
|
||||||
|
LoginTimestampCheck::NoTimestamp => assert_eq!(cv.login_timestamp, None),
|
||||||
|
LoginTimestampCheck::NewTimestamp => assert!(
|
||||||
|
t30sec_ago <= cv.login_timestamp.unwrap()
|
||||||
|
&& cv.login_timestamp.unwrap() <= now
|
||||||
|
),
|
||||||
|
LoginTimestampCheck::OldTimestamp(old_timestamp) => {
|
||||||
|
assert_eq!(cv.login_timestamp, Some(old_timestamp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match visit_timestamp {
|
||||||
|
VisitTimeStampCheck::NoTimestamp => assert_eq!(cv.visit_timestamp, None),
|
||||||
|
VisitTimeStampCheck::NewTimestamp => assert!(
|
||||||
|
t30sec_ago <= cv.visit_timestamp.unwrap()
|
||||||
|
&& cv.visit_timestamp.unwrap() <= now
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_no_login_cookie(response: &mut ServiceResponse) {
|
||||||
|
let mut cookies = CookieJar::new();
|
||||||
|
for cookie in response.headers().get_all(header::SET_COOKIE) {
|
||||||
|
cookies.add(Cookie::parse(cookie.to_str().unwrap().to_string()).unwrap());
|
||||||
|
}
|
||||||
|
assert!(cookies.get(COOKIE_NAME).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_identity_legacy_cookie_is_set() {
|
||||||
|
let mut srv = create_identity_server(|c| c);
|
||||||
|
let mut resp =
|
||||||
|
test::call_service(&mut srv, TestRequest::with_uri("/").to_request());
|
||||||
|
assert_logged_in(&mut resp, None);
|
||||||
|
assert_legacy_login_cookie(&mut resp, COOKIE_LOGIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_identity_legacy_cookie_works() {
|
||||||
|
let mut srv = create_identity_server(|c| c);
|
||||||
|
let cookie = legacy_login_cookie(COOKIE_LOGIN);
|
||||||
|
let mut resp = test::call_service(
|
||||||
|
&mut srv,
|
||||||
|
TestRequest::with_uri("/")
|
||||||
|
.cookie(cookie.clone())
|
||||||
|
.to_request(),
|
||||||
|
);
|
||||||
|
assert_logged_in(&mut resp, Some(COOKIE_LOGIN));
|
||||||
|
assert_no_login_cookie(&mut resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_identity_legacy_cookie_rejected_if_visit_timestamp_needed() {
|
||||||
|
let mut srv = create_identity_server(|c| c.visit_deadline(Duration::days(90)));
|
||||||
|
let cookie = legacy_login_cookie(COOKIE_LOGIN);
|
||||||
|
let mut resp = test::call_service(
|
||||||
|
&mut srv,
|
||||||
|
TestRequest::with_uri("/")
|
||||||
|
.cookie(cookie.clone())
|
||||||
|
.to_request(),
|
||||||
|
);
|
||||||
|
assert_logged_in(&mut resp, None);
|
||||||
|
assert_login_cookie(
|
||||||
|
&mut resp,
|
||||||
|
COOKIE_LOGIN,
|
||||||
|
LoginTimestampCheck::NoTimestamp,
|
||||||
|
VisitTimeStampCheck::NewTimestamp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_identity_legacy_cookie_rejected_if_login_timestamp_needed() {
|
||||||
|
let mut srv = create_identity_server(|c| c.login_deadline(Duration::days(90)));
|
||||||
|
let cookie = legacy_login_cookie(COOKIE_LOGIN);
|
||||||
|
let mut resp = test::call_service(
|
||||||
|
&mut srv,
|
||||||
|
TestRequest::with_uri("/")
|
||||||
|
.cookie(cookie.clone())
|
||||||
|
.to_request(),
|
||||||
|
);
|
||||||
|
assert_logged_in(&mut resp, None);
|
||||||
|
assert_login_cookie(
|
||||||
|
&mut resp,
|
||||||
|
COOKIE_LOGIN,
|
||||||
|
LoginTimestampCheck::NewTimestamp,
|
||||||
|
VisitTimeStampCheck::NoTimestamp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_identity_cookie_rejected_if_login_timestamp_needed() {
|
||||||
|
let mut srv = create_identity_server(|c| c.login_deadline(Duration::days(90)));
|
||||||
|
let cookie = login_cookie(COOKIE_LOGIN, None, Some(SystemTime::now()));
|
||||||
|
let mut resp = test::call_service(
|
||||||
|
&mut srv,
|
||||||
|
TestRequest::with_uri("/")
|
||||||
|
.cookie(cookie.clone())
|
||||||
|
.to_request(),
|
||||||
|
);
|
||||||
|
assert_logged_in(&mut resp, None);
|
||||||
|
assert_login_cookie(
|
||||||
|
&mut resp,
|
||||||
|
COOKIE_LOGIN,
|
||||||
|
LoginTimestampCheck::NewTimestamp,
|
||||||
|
VisitTimeStampCheck::NoTimestamp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_identity_cookie_rejected_if_visit_timestamp_needed() {
|
||||||
|
let mut srv = create_identity_server(|c| c.visit_deadline(Duration::days(90)));
|
||||||
|
let cookie = login_cookie(COOKIE_LOGIN, Some(SystemTime::now()), None);
|
||||||
|
let mut resp = test::call_service(
|
||||||
|
&mut srv,
|
||||||
|
TestRequest::with_uri("/")
|
||||||
|
.cookie(cookie.clone())
|
||||||
|
.to_request(),
|
||||||
|
);
|
||||||
|
assert_logged_in(&mut resp, None);
|
||||||
|
assert_login_cookie(
|
||||||
|
&mut resp,
|
||||||
|
COOKIE_LOGIN,
|
||||||
|
LoginTimestampCheck::NoTimestamp,
|
||||||
|
VisitTimeStampCheck::NewTimestamp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_identity_cookie_rejected_if_login_timestamp_too_old() {
|
||||||
|
let mut srv = create_identity_server(|c| c.login_deadline(Duration::days(90)));
|
||||||
|
let cookie = login_cookie(
|
||||||
|
COOKIE_LOGIN,
|
||||||
|
Some(SystemTime::now() - Duration::days(180).to_std().unwrap()),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let mut resp = test::call_service(
|
||||||
|
&mut srv,
|
||||||
|
TestRequest::with_uri("/")
|
||||||
|
.cookie(cookie.clone())
|
||||||
|
.to_request(),
|
||||||
|
);
|
||||||
|
assert_logged_in(&mut resp, None);
|
||||||
|
assert_login_cookie(
|
||||||
|
&mut resp,
|
||||||
|
COOKIE_LOGIN,
|
||||||
|
LoginTimestampCheck::NewTimestamp,
|
||||||
|
VisitTimeStampCheck::NoTimestamp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_identity_cookie_rejected_if_visit_timestamp_too_old() {
|
||||||
|
let mut srv = create_identity_server(|c| c.visit_deadline(Duration::days(90)));
|
||||||
|
let cookie = login_cookie(
|
||||||
|
COOKIE_LOGIN,
|
||||||
|
None,
|
||||||
|
Some(SystemTime::now() - Duration::days(180).to_std().unwrap()),
|
||||||
|
);
|
||||||
|
let mut resp = test::call_service(
|
||||||
|
&mut srv,
|
||||||
|
TestRequest::with_uri("/")
|
||||||
|
.cookie(cookie.clone())
|
||||||
|
.to_request(),
|
||||||
|
);
|
||||||
|
assert_logged_in(&mut resp, None);
|
||||||
|
assert_login_cookie(
|
||||||
|
&mut resp,
|
||||||
|
COOKIE_LOGIN,
|
||||||
|
LoginTimestampCheck::NoTimestamp,
|
||||||
|
VisitTimeStampCheck::NewTimestamp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_identity_cookie_not_updated_on_login_deadline() {
|
||||||
|
let mut srv = create_identity_server(|c| c.login_deadline(Duration::days(90)));
|
||||||
|
let cookie = login_cookie(COOKIE_LOGIN, Some(SystemTime::now()), None);
|
||||||
|
let mut resp = test::call_service(
|
||||||
|
&mut srv,
|
||||||
|
TestRequest::with_uri("/")
|
||||||
|
.cookie(cookie.clone())
|
||||||
|
.to_request(),
|
||||||
|
);
|
||||||
|
assert_logged_in(&mut resp, Some(COOKIE_LOGIN));
|
||||||
|
assert_no_login_cookie(&mut resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_identity_cookie_updated_on_visit_deadline() {
|
||||||
|
let mut srv = create_identity_server(|c| {
|
||||||
|
c.visit_deadline(Duration::days(90))
|
||||||
|
.login_deadline(Duration::days(90))
|
||||||
|
});
|
||||||
|
let timestamp = SystemTime::now() - Duration::days(1).to_std().unwrap();
|
||||||
|
let cookie = login_cookie(COOKIE_LOGIN, Some(timestamp), Some(timestamp));
|
||||||
|
let mut resp = test::call_service(
|
||||||
|
&mut srv,
|
||||||
|
TestRequest::with_uri("/")
|
||||||
|
.cookie(cookie.clone())
|
||||||
|
.to_request(),
|
||||||
|
);
|
||||||
|
assert_logged_in(&mut resp, Some(COOKIE_LOGIN));
|
||||||
|
assert_login_cookie(
|
||||||
|
&mut resp,
|
||||||
|
COOKIE_LOGIN,
|
||||||
|
LoginTimestampCheck::OldTimestamp(timestamp),
|
||||||
|
VisitTimeStampCheck::NewTimestamp,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
193
src/responder.rs
193
src/responder.rs
@ -1,8 +1,12 @@
|
|||||||
use actix_http::error::InternalError;
|
use actix_http::error::InternalError;
|
||||||
use actix_http::{http::StatusCode, Error, Response, ResponseBuilder};
|
use actix_http::http::{
|
||||||
|
header::IntoHeaderValue, Error as HttpError, HeaderMap, HeaderName, HttpTryFrom,
|
||||||
|
StatusCode,
|
||||||
|
};
|
||||||
|
use actix_http::{Error, Response, ResponseBuilder};
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use futures::future::{err, ok, Either as EitherFuture, FutureResult};
|
use futures::future::{err, ok, Either as EitherFuture, FutureResult};
|
||||||
use futures::{Future, IntoFuture, Poll};
|
use futures::{try_ready, Async, Future, IntoFuture, Poll};
|
||||||
|
|
||||||
use crate::request::HttpRequest;
|
use crate::request::HttpRequest;
|
||||||
|
|
||||||
@ -18,6 +22,51 @@ pub trait Responder {
|
|||||||
|
|
||||||
/// Convert itself to `AsyncResult` or `Error`.
|
/// Convert itself to `AsyncResult` or `Error`.
|
||||||
fn respond_to(self, req: &HttpRequest) -> Self::Future;
|
fn respond_to(self, req: &HttpRequest) -> Self::Future;
|
||||||
|
|
||||||
|
/// Override a status code for a Responder.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use actix_web::{HttpRequest, Responder, http::StatusCode};
|
||||||
|
///
|
||||||
|
/// fn index(req: HttpRequest) -> impl Responder {
|
||||||
|
/// "Welcome!".with_status(StatusCode::OK)
|
||||||
|
/// }
|
||||||
|
/// # fn main() {}
|
||||||
|
/// ```
|
||||||
|
fn with_status(self, status: StatusCode) -> CustomResponder<Self>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
CustomResponder::new(self).with_status(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add header to the Responder's response.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use actix_web::{web, HttpRequest, Responder};
|
||||||
|
/// use serde::Serialize;
|
||||||
|
///
|
||||||
|
/// #[derive(Serialize)]
|
||||||
|
/// struct MyObj {
|
||||||
|
/// name: String,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn index(req: HttpRequest) -> impl Responder {
|
||||||
|
/// web::Json(
|
||||||
|
/// MyObj{name: "Name".to_string()}
|
||||||
|
/// )
|
||||||
|
/// .with_header("x-version", "1.2.3")
|
||||||
|
/// }
|
||||||
|
/// # fn main() {}
|
||||||
|
/// ```
|
||||||
|
fn with_header<K, V>(self, key: K, value: V) -> CustomResponder<Self>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
HeaderName: HttpTryFrom<K>,
|
||||||
|
V: IntoHeaderValue,
|
||||||
|
{
|
||||||
|
CustomResponder::new(self).with_header(key, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Responder for Response {
|
impl Responder for Response {
|
||||||
@ -154,6 +203,117 @@ impl Responder for BytesMut {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Allows to override status code and headers for a responder.
|
||||||
|
pub struct CustomResponder<T> {
|
||||||
|
responder: T,
|
||||||
|
status: Option<StatusCode>,
|
||||||
|
headers: Option<HeaderMap>,
|
||||||
|
error: Option<HttpError>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Responder> CustomResponder<T> {
|
||||||
|
fn new(responder: T) -> Self {
|
||||||
|
CustomResponder {
|
||||||
|
responder,
|
||||||
|
status: None,
|
||||||
|
headers: None,
|
||||||
|
error: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Override a status code for the Responder's response.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use actix_web::{HttpRequest, Responder, http::StatusCode};
|
||||||
|
///
|
||||||
|
/// fn index(req: HttpRequest) -> impl Responder {
|
||||||
|
/// "Welcome!".with_status(StatusCode::OK)
|
||||||
|
/// }
|
||||||
|
/// # fn main() {}
|
||||||
|
/// ```
|
||||||
|
pub fn with_status(mut self, status: StatusCode) -> Self {
|
||||||
|
self.status = Some(status);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add header to the Responder's response.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use actix_web::{web, HttpRequest, Responder};
|
||||||
|
/// use serde::Serialize;
|
||||||
|
///
|
||||||
|
/// #[derive(Serialize)]
|
||||||
|
/// struct MyObj {
|
||||||
|
/// name: String,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn index(req: HttpRequest) -> impl Responder {
|
||||||
|
/// web::Json(
|
||||||
|
/// MyObj{name: "Name".to_string()}
|
||||||
|
/// )
|
||||||
|
/// .with_header("x-version", "1.2.3")
|
||||||
|
/// }
|
||||||
|
/// # fn main() {}
|
||||||
|
/// ```
|
||||||
|
pub fn with_header<K, V>(mut self, key: K, value: V) -> Self
|
||||||
|
where
|
||||||
|
HeaderName: HttpTryFrom<K>,
|
||||||
|
V: IntoHeaderValue,
|
||||||
|
{
|
||||||
|
if self.headers.is_none() {
|
||||||
|
self.headers = Some(HeaderMap::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
match HeaderName::try_from(key) {
|
||||||
|
Ok(key) => match value.try_into() {
|
||||||
|
Ok(value) => {
|
||||||
|
self.headers.as_mut().unwrap().append(key, value);
|
||||||
|
}
|
||||||
|
Err(e) => self.error = Some(e.into()),
|
||||||
|
},
|
||||||
|
Err(e) => self.error = Some(e.into()),
|
||||||
|
};
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Responder> Responder for CustomResponder<T> {
|
||||||
|
type Error = T::Error;
|
||||||
|
type Future = CustomResponderFut<T>;
|
||||||
|
|
||||||
|
fn respond_to(self, req: &HttpRequest) -> Self::Future {
|
||||||
|
CustomResponderFut {
|
||||||
|
fut: self.responder.respond_to(req).into_future(),
|
||||||
|
status: self.status,
|
||||||
|
headers: self.headers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CustomResponderFut<T: Responder> {
|
||||||
|
fut: <T::Future as IntoFuture>::Future,
|
||||||
|
status: Option<StatusCode>,
|
||||||
|
headers: Option<HeaderMap>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Responder> Future for CustomResponderFut<T> {
|
||||||
|
type Item = Response;
|
||||||
|
type Error = T::Error;
|
||||||
|
|
||||||
|
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
|
||||||
|
let mut res = try_ready!(self.fut.poll());
|
||||||
|
if let Some(status) = self.status {
|
||||||
|
*res.status_mut() = status;
|
||||||
|
}
|
||||||
|
if let Some(ref headers) = self.headers {
|
||||||
|
for (k, v) in headers {
|
||||||
|
res.headers_mut().insert(k.clone(), v.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Async::Ready(res))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Combines two different responder types into a single type
|
/// Combines two different responder types into a single type
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
@ -435,4 +595,33 @@ pub(crate) mod tests {
|
|||||||
);
|
);
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_custom_responder() {
|
||||||
|
let req = TestRequest::default().to_http_request();
|
||||||
|
let res = block_on(
|
||||||
|
"test"
|
||||||
|
.to_string()
|
||||||
|
.with_status(StatusCode::BAD_REQUEST)
|
||||||
|
.respond_to(&req),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||||
|
assert_eq!(res.body().bin_ref(), b"test");
|
||||||
|
|
||||||
|
let res = block_on(
|
||||||
|
"test"
|
||||||
|
.to_string()
|
||||||
|
.with_header("content-type", "json")
|
||||||
|
.respond_to(&req),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
|
assert_eq!(res.body().bin_ref(), b"test");
|
||||||
|
assert_eq!(
|
||||||
|
res.headers().get(CONTENT_TYPE).unwrap(),
|
||||||
|
HeaderValue::from_static("json")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
135
src/service.rs
135
src/service.rs
@ -7,11 +7,14 @@ use actix_http::{
|
|||||||
Error, Extensions, HttpMessage, Payload, PayloadStream, RequestHead, Response,
|
Error, Extensions, HttpMessage, Payload, PayloadStream, RequestHead, Response,
|
||||||
ResponseHead,
|
ResponseHead,
|
||||||
};
|
};
|
||||||
use actix_router::{Path, Resource, Url};
|
use actix_router::{Path, Resource, ResourceDef, Url};
|
||||||
|
use actix_service::{IntoNewService, NewService};
|
||||||
use futures::future::{ok, FutureResult, IntoFuture};
|
use futures::future::{ok, FutureResult, IntoFuture};
|
||||||
|
|
||||||
use crate::config::{AppConfig, AppService};
|
use crate::config::{AppConfig, AppService};
|
||||||
use crate::data::Data;
|
use crate::data::Data;
|
||||||
|
use crate::dev::insert_slash;
|
||||||
|
use crate::guard::Guard;
|
||||||
use crate::info::ConnectionInfo;
|
use crate::info::ConnectionInfo;
|
||||||
use crate::request::HttpRequest;
|
use crate::request::HttpRequest;
|
||||||
|
|
||||||
@ -380,10 +383,136 @@ impl<B: MessageBody> fmt::Debug for ServiceResponse<B> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct WebService {
|
||||||
|
rdef: String,
|
||||||
|
name: Option<String>,
|
||||||
|
guards: Vec<Box<Guard>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebService {
|
||||||
|
/// Create new `WebService` instance.
|
||||||
|
pub fn new(path: &str) -> Self {
|
||||||
|
WebService {
|
||||||
|
rdef: path.to_string(),
|
||||||
|
name: None,
|
||||||
|
guards: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set service name.
|
||||||
|
///
|
||||||
|
/// Name is used for url generation.
|
||||||
|
pub fn name(mut self, name: &str) -> Self {
|
||||||
|
self.name = Some(name.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add match guard to a web service.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use actix_web::{web, guard, dev, App, HttpResponse};
|
||||||
|
///
|
||||||
|
/// fn index(req: dev::ServiceRequest) -> dev::ServiceResponse {
|
||||||
|
/// req.into_response(HttpResponse::Ok().finish())
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn main() {
|
||||||
|
/// let app = App::new()
|
||||||
|
/// .service(
|
||||||
|
/// web::service("/app")
|
||||||
|
/// .guard(guard::Header("content-type", "text/plain"))
|
||||||
|
/// .finish(index)
|
||||||
|
/// );
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn guard<G: Guard + 'static>(mut self, guard: G) -> Self {
|
||||||
|
self.guards.push(Box::new(guard));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a service factory implementation and generate web service.
|
||||||
|
pub fn finish<T, F>(self, service: F) -> impl HttpServiceFactory
|
||||||
|
where
|
||||||
|
F: IntoNewService<T>,
|
||||||
|
T: NewService<
|
||||||
|
Request = ServiceRequest,
|
||||||
|
Response = ServiceResponse,
|
||||||
|
Error = Error,
|
||||||
|
InitError = (),
|
||||||
|
> + 'static,
|
||||||
|
{
|
||||||
|
WebServiceImpl {
|
||||||
|
srv: service.into_new_service(),
|
||||||
|
rdef: self.rdef,
|
||||||
|
name: self.name,
|
||||||
|
guards: self.guards,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WebServiceImpl<T> {
|
||||||
|
srv: T,
|
||||||
|
rdef: String,
|
||||||
|
name: Option<String>,
|
||||||
|
guards: Vec<Box<Guard>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> HttpServiceFactory for WebServiceImpl<T>
|
||||||
|
where
|
||||||
|
T: NewService<
|
||||||
|
Request = ServiceRequest,
|
||||||
|
Response = ServiceResponse,
|
||||||
|
Error = Error,
|
||||||
|
InitError = (),
|
||||||
|
> + 'static,
|
||||||
|
{
|
||||||
|
fn register(mut self, config: &mut AppService) {
|
||||||
|
let guards = if self.guards.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(std::mem::replace(&mut self.guards, Vec::new()))
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut rdef = if config.is_root() || !self.rdef.is_empty() {
|
||||||
|
ResourceDef::new(&insert_slash(&self.rdef))
|
||||||
|
} else {
|
||||||
|
ResourceDef::new(&self.rdef)
|
||||||
|
};
|
||||||
|
if let Some(ref name) = self.name {
|
||||||
|
*rdef.name_mut() = name.clone();
|
||||||
|
}
|
||||||
|
config.register_service(rdef, guards, self.srv, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::test::TestRequest;
|
use super::*;
|
||||||
use crate::HttpResponse;
|
use crate::test::{call_service, init_service, TestRequest};
|
||||||
|
use crate::{guard, http, web, App, HttpResponse};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_service() {
|
||||||
|
let mut srv = init_service(
|
||||||
|
App::new().service(web::service("/test").name("test").finish(
|
||||||
|
|req: ServiceRequest| req.into_response(HttpResponse::Ok().finish()),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
let req = TestRequest::with_uri("/test").to_request();
|
||||||
|
let resp = call_service(&mut srv, req);
|
||||||
|
assert_eq!(resp.status(), http::StatusCode::OK);
|
||||||
|
|
||||||
|
let mut srv = init_service(
|
||||||
|
App::new().service(web::service("/test").guard(guard::Get()).finish(
|
||||||
|
|req: ServiceRequest| req.into_response(HttpResponse::Ok().finish()),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
let req = TestRequest::with_uri("/test")
|
||||||
|
.method(http::Method::PUT)
|
||||||
|
.to_request();
|
||||||
|
let resp = call_service(&mut srv, req);
|
||||||
|
assert_eq!(resp.status(), http::StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fmt_debug() {
|
fn test_fmt_debug() {
|
||||||
|
23
src/web.rs
23
src/web.rs
@ -12,6 +12,7 @@ use crate::resource::Resource;
|
|||||||
use crate::responder::Responder;
|
use crate::responder::Responder;
|
||||||
use crate::route::Route;
|
use crate::route::Route;
|
||||||
use crate::scope::Scope;
|
use crate::scope::Scope;
|
||||||
|
use crate::service::WebService;
|
||||||
|
|
||||||
pub use crate::config::ServiceConfig;
|
pub use crate::config::ServiceConfig;
|
||||||
pub use crate::data::{Data, RouteData};
|
pub use crate::data::{Data, RouteData};
|
||||||
@ -274,6 +275,28 @@ where
|
|||||||
Route::new().to_async(handler)
|
Route::new().to_async(handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create raw service for a specific path.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # extern crate actix_web;
|
||||||
|
/// use actix_web::{dev, web, guard, App, HttpResponse};
|
||||||
|
///
|
||||||
|
/// fn my_service(req: dev::ServiceRequest) -> dev::ServiceResponse {
|
||||||
|
/// req.into_response(HttpResponse::Ok().finish())
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn main() {
|
||||||
|
/// let app = App::new().service(
|
||||||
|
/// web::service("/users/*")
|
||||||
|
/// .guard(guard::Header("content-type", "text/plain"))
|
||||||
|
/// .finish(my_service)
|
||||||
|
/// );
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn service(path: &str) -> WebService {
|
||||||
|
WebService::new(path)
|
||||||
|
}
|
||||||
|
|
||||||
/// Execute blocking function on a thread pool, returns future that resolves
|
/// Execute blocking function on a thread pool, returns future that resolves
|
||||||
/// to result of the function execution.
|
/// to result of the function execution.
|
||||||
pub fn block<F, I, E>(f: F) -> impl Future<Item = I, Error = BlockingError<E>>
|
pub fn block<F, I, E>(f: F) -> impl Future<Item = I, Error = BlockingError<E>>
|
||||||
|
Reference in New Issue
Block a user