Merge branch 'master' into advanced_route_macro

This commit is contained in:
Rob Ede 2022-07-01 12:41:39 +01:00 committed by GitHub
commit 83a98712e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 584 additions and 60 deletions

View File

@ -2,12 +2,21 @@
## Unreleased - 2022-xx-xx ## Unreleased - 2022-xx-xx
### Fixed ### Fixed
- Websocket parser no longer throws endless overflow errors after receiving an oversized frame. [#2790] - Fix parsing ambiguity in Transfer-Encoding and Content-Length headers for HTTP/1.0 requests. [#2794]
[#2794]: https://github.com/actix/actix-web/pull/2794
## 3.2.0 - 2022-06-30
### Changed ### Changed
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
### Fixed
- Websocket parser no longer throws endless overflow errors after receiving an oversized frame. [#2790]
- Retain previously set Vary headers when using compression encoder. [#2798]
[#2790]: https://github.com/actix/actix-web/pull/2790 [#2790]: https://github.com/actix/actix-web/pull/2790
[#2798]: https://github.com/actix/actix-web/pull/2798
## 3.1.0 - 2022-06-11 ## 3.1.0 - 2022-06-11

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.1.0" version = "3.2.0"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
@ -108,10 +108,10 @@ env_logger = "0.9"
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
memchr = "2.4" memchr = "2.4"
once_cell = "1.9" once_cell = "1.9"
rcgen = "0.8" rcgen = "0.9"
regex = "1.3" regex = "1.3"
rustversion = "1" rustversion = "1"
rustls-pemfile = "0.2" rustls-pemfile = "1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
static_assertions = "1" static_assertions = "1"

View File

@ -3,11 +3,11 @@
> HTTP primitives for the Actix ecosystem. > HTTP primitives for the Actix ecosystem.
[![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.1.0)](https://docs.rs/actix-http/3.1.0) [![Documentation](https://docs.rs/actix-http/badge.svg?version=3.2.0)](https://docs.rs/actix-http/3.2.0)
![Version](https://img.shields.io/badge/rustc-1.57+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.57+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-http/3.1.0/status.svg)](https://deps.rs/crate/actix-http/3.1.0) [![dependency status](https://deps.rs/crate/actix-http/3.2.0/status.svg)](https://deps.rs/crate/actix-http/3.2.0)
[![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -481,6 +481,7 @@ mod tests {
assert_poll_next_none!(pl); assert_poll_next_none!(pl);
} }
#[allow(clippy::let_unit_value)]
#[actix_rt::test] #[actix_rt::test]
async fn test_unit() { async fn test_unit() {
let pl = (); let pl = ();

View File

@ -257,7 +257,7 @@ fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
head.headers_mut() head.headers_mut()
.insert(header::CONTENT_ENCODING, encoding.to_header_value()); .insert(header::CONTENT_ENCODING, encoding.to_header_value());
head.headers_mut() head.headers_mut()
.insert(header::VARY, HeaderValue::from_static("accept-encoding")); .append(header::VARY, HeaderValue::from_static("accept-encoding"));
head.no_chunking(false); head.no_chunking(false);
} }

View File

@ -46,6 +46,23 @@ pub(crate) enum PayloadLength {
None, None,
} }
impl PayloadLength {
/// Returns true if variant is `None`.
fn is_none(&self) -> bool {
matches!(self, Self::None)
}
/// Returns true if variant is represents zero-length (not none) payload.
fn is_zero(&self) -> bool {
matches!(
self,
PayloadLength::Payload(PayloadType::Payload(PayloadDecoder {
kind: Kind::Length(0)
}))
)
}
}
pub(crate) trait MessageType: Sized { pub(crate) trait MessageType: Sized {
fn set_connection_type(&mut self, conn_type: Option<ConnectionType>); fn set_connection_type(&mut self, conn_type: Option<ConnectionType>);
@ -59,6 +76,7 @@ pub(crate) trait MessageType: Sized {
&mut self, &mut self,
slice: &Bytes, slice: &Bytes,
raw_headers: &[HeaderIndex], raw_headers: &[HeaderIndex],
version: Version,
) -> Result<PayloadLength, ParseError> { ) -> Result<PayloadLength, ParseError> {
let mut ka = None; let mut ka = None;
let mut has_upgrade_websocket = false; let mut has_upgrade_websocket = false;
@ -87,21 +105,23 @@ pub(crate) trait MessageType: Sized {
return Err(ParseError::Header); return Err(ParseError::Header);
} }
header::CONTENT_LENGTH => match value.to_str() { header::CONTENT_LENGTH => match value.to_str().map(str::trim) {
Ok(s) if s.trim().starts_with('+') => { Ok(val) if val.starts_with('+') => {
debug!("illegal Content-Length: {:?}", s); debug!("illegal Content-Length: {:?}", val);
return Err(ParseError::Header); return Err(ParseError::Header);
} }
Ok(s) => {
if let Ok(len) = s.parse::<u64>() { Ok(val) => {
if len != 0 { if let Ok(len) = val.parse::<u64>() {
// accept 0 lengths here and remove them in `decode` after all
// headers have been processed to prevent request smuggling issues
content_length = Some(len); content_length = Some(len);
}
} else { } else {
debug!("illegal Content-Length: {:?}", s); debug!("illegal Content-Length: {:?}", val);
return Err(ParseError::Header); return Err(ParseError::Header);
} }
} }
Err(_) => { Err(_) => {
debug!("illegal Content-Length: {:?}", value); debug!("illegal Content-Length: {:?}", value);
return Err(ParseError::Header); return Err(ParseError::Header);
@ -114,22 +134,23 @@ pub(crate) trait MessageType: Sized {
return Err(ParseError::Header); return Err(ParseError::Header);
} }
header::TRANSFER_ENCODING => { header::TRANSFER_ENCODING if version == Version::HTTP_11 => {
seen_te = true; seen_te = true;
if let Ok(s) = value.to_str().map(str::trim) { if let Ok(val) = value.to_str().map(str::trim) {
if s.eq_ignore_ascii_case("chunked") { if val.eq_ignore_ascii_case("chunked") {
chunked = true; chunked = true;
} else if s.eq_ignore_ascii_case("identity") { } else if val.eq_ignore_ascii_case("identity") {
// allow silently since multiple TE headers are already checked // allow silently since multiple TE headers are already checked
} else { } else {
debug!("illegal Transfer-Encoding: {:?}", s); debug!("illegal Transfer-Encoding: {:?}", val);
return Err(ParseError::Header); return Err(ParseError::Header);
} }
} else { } else {
return Err(ParseError::Header); return Err(ParseError::Header);
} }
} }
// connection keep-alive state // connection keep-alive state
header::CONNECTION => { header::CONNECTION => {
ka = if let Ok(conn) = value.to_str().map(str::trim) { ka = if let Ok(conn) = value.to_str().map(str::trim) {
@ -146,6 +167,7 @@ pub(crate) trait MessageType: Sized {
None None
}; };
} }
header::UPGRADE => { header::UPGRADE => {
if let Ok(val) = value.to_str().map(str::trim) { if let Ok(val) = value.to_str().map(str::trim) {
if val.eq_ignore_ascii_case("websocket") { if val.eq_ignore_ascii_case("websocket") {
@ -153,19 +175,23 @@ pub(crate) trait MessageType: Sized {
} }
} }
} }
header::EXPECT => { header::EXPECT => {
let bytes = value.as_bytes(); let bytes = value.as_bytes();
if bytes.len() >= 4 && &bytes[0..4] == b"100-" { if bytes.len() >= 4 && &bytes[0..4] == b"100-" {
expect = true; expect = true;
} }
} }
_ => {} _ => {}
} }
headers.append(name, value); headers.append(name, value);
} }
} }
self.set_connection_type(ka); self.set_connection_type(ka);
if expect { if expect {
self.set_expect() self.set_expect()
} }
@ -249,7 +275,22 @@ impl MessageType for Request {
let mut msg = Request::new(); let mut msg = Request::new();
// convert headers // convert headers
let length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?; let mut length =
msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?;
// disallow HTTP/1.0 POST requests that do not contain a Content-Length headers
// see https://datatracker.ietf.org/doc/html/rfc1945#section-7.2.2
if ver == Version::HTTP_10 && method == Method::POST && length.is_none() {
debug!("no Content-Length specified for HTTP/1.0 POST request");
return Err(ParseError::Header);
}
// Remove CL value if 0 now that all headers and HTTP/1.0 special cases are processed.
// Protects against some request smuggling attacks.
// See https://github.com/actix/actix-web/issues/2767.
if length.is_zero() {
length = PayloadLength::None;
}
// payload decoder // payload decoder
let decoder = match length { let decoder = match length {
@ -337,7 +378,15 @@ impl MessageType for ResponseHead {
msg.version = ver; msg.version = ver;
// convert headers // convert headers
let length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?; let mut length =
msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?;
// Remove CL value if 0 now that all headers and HTTP/1.0 special cases are processed.
// Protects against some request smuggling attacks.
// See https://github.com/actix/actix-web/issues/2767.
if length.is_zero() {
length = PayloadLength::None;
}
// message payload // message payload
let decoder = if let PayloadLength::Payload(pl) = length { let decoder = if let PayloadLength::Payload(pl) = length {
@ -606,14 +655,100 @@ mod tests {
} }
#[test] #[test]
fn test_parse_post() { fn parse_h09_reject() {
let mut buf = BytesMut::from("POST /test2 HTTP/1.0\r\n\r\n"); let mut buf = BytesMut::from(
"GET /test1 HTTP/0.9\r\n\
\r\n",
);
let mut reader = MessageDecoder::<Request>::default();
reader.decode(&mut buf).unwrap_err();
let mut buf = BytesMut::from(
"POST /test2 HTTP/0.9\r\n\
Content-Length: 3\r\n\
\r\n
abc",
);
let mut reader = MessageDecoder::<Request>::default();
reader.decode(&mut buf).unwrap_err();
}
#[test]
fn parse_h10_get() {
let mut buf = BytesMut::from(
"GET /test1 HTTP/1.0\r\n\
\r\n",
);
let mut reader = MessageDecoder::<Request>::default();
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
assert_eq!(req.version(), Version::HTTP_10);
assert_eq!(*req.method(), Method::GET);
assert_eq!(req.path(), "/test1");
let mut buf = BytesMut::from(
"GET /test2 HTTP/1.0\r\n\
Content-Length: 0\r\n\
\r\n",
);
let mut reader = MessageDecoder::<Request>::default();
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
assert_eq!(req.version(), Version::HTTP_10);
assert_eq!(*req.method(), Method::GET);
assert_eq!(req.path(), "/test2");
let mut buf = BytesMut::from(
"GET /test3 HTTP/1.0\r\n\
Content-Length: 3\r\n\
\r\n
abc",
);
let mut reader = MessageDecoder::<Request>::default();
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
assert_eq!(req.version(), Version::HTTP_10);
assert_eq!(*req.method(), Method::GET);
assert_eq!(req.path(), "/test3");
}
#[test]
fn parse_h10_post() {
let mut buf = BytesMut::from(
"POST /test1 HTTP/1.0\r\n\
Content-Length: 3\r\n\
\r\n\
abc",
);
let mut reader = MessageDecoder::<Request>::default();
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
assert_eq!(req.version(), Version::HTTP_10);
assert_eq!(*req.method(), Method::POST);
assert_eq!(req.path(), "/test1");
let mut buf = BytesMut::from(
"POST /test2 HTTP/1.0\r\n\
Content-Length: 0\r\n\
\r\n",
);
let mut reader = MessageDecoder::<Request>::default(); let mut reader = MessageDecoder::<Request>::default();
let (req, _) = reader.decode(&mut buf).unwrap().unwrap(); let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
assert_eq!(req.version(), Version::HTTP_10); assert_eq!(req.version(), Version::HTTP_10);
assert_eq!(*req.method(), Method::POST); assert_eq!(*req.method(), Method::POST);
assert_eq!(req.path(), "/test2"); assert_eq!(req.path(), "/test2");
let mut buf = BytesMut::from(
"POST /test3 HTTP/1.0\r\n\
\r\n",
);
let mut reader = MessageDecoder::<Request>::default();
let err = reader.decode(&mut buf).unwrap_err();
assert!(err.to_string().contains("Header"))
} }
#[test] #[test]
@ -981,6 +1116,17 @@ mod tests {
); );
expect_parse_err!(&mut buf); expect_parse_err!(&mut buf);
let mut buf = BytesMut::from(
"GET / HTTP/1.1\r\n\
Host: example.com\r\n\
Content-Length: 0\r\n\
Content-Length: 2\r\n\
\r\n\
ab",
);
expect_parse_err!(&mut buf);
} }
#[test] #[test]
@ -996,6 +1142,40 @@ mod tests {
expect_parse_err!(&mut buf); expect_parse_err!(&mut buf);
} }
#[test]
fn hrs_te_http10() {
// in HTTP/1.0 transfer encoding is ignored and must therefore contain a CL header
let mut buf = BytesMut::from(
"POST / HTTP/1.0\r\n\
Host: example.com\r\n\
Transfer-Encoding: chunked\r\n\
\r\n\
3\r\n\
aaa\r\n\
0\r\n\
",
);
expect_parse_err!(&mut buf);
}
#[test]
fn hrs_cl_and_te_http10() {
// in HTTP/1.0 transfer encoding is simply ignored so it's fine to have both
let mut buf = BytesMut::from(
"GET / HTTP/1.0\r\n\
Host: example.com\r\n\
Content-Length: 3\r\n\
Transfer-Encoding: chunked\r\n\
\r\n\
000",
);
parse_ready!(&mut buf);
}
#[test] #[test]
fn hrs_unknown_transfer_encoding() { fn hrs_unknown_transfer_encoding() {
let mut buf = BytesMut::from( let mut buf = BytesMut::from(

View File

@ -25,6 +25,7 @@
)] )]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_cfg))]
pub use ::http::{uri, uri::Uri}; pub use ::http::{uri, uri::Uri};
pub use ::http::{Method, StatusCode, Version}; pub use ::http::{Method, StatusCode, Version};
@ -69,6 +70,8 @@ pub use self::payload::{BoxedPayloadStream, Payload, PayloadStream};
pub use self::requests::{Request, RequestHead, RequestHeadType}; pub use self::requests::{Request, RequestHead, RequestHeadType};
pub use self::responses::{Response, ResponseBuilder, ResponseHead}; pub use self::responses::{Response, ResponseBuilder, ResponseHead};
pub use self::service::HttpService; pub use self::service::HttpService;
#[cfg(any(feature = "openssl", feature = "rustls"))]
pub use self::service::TlsAcceptorConfig;
/// A major HTTP protocol version. /// A major HTTP protocol version.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]

View File

@ -237,7 +237,7 @@ mod tests {
.await; .await;
let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
let _ = stream stream
.write_all(b"GET /camel HTTP/1.1\r\nConnection: Close\r\n\r\n") .write_all(b"GET /camel HTTP/1.1\r\nConnection: Close\r\n\r\n")
.unwrap(); .unwrap();
let mut data = vec![]; let mut data = vec![];
@ -251,7 +251,7 @@ mod tests {
assert!(memmem::find(&data, b"content-length").is_none()); assert!(memmem::find(&data, b"content-length").is_none());
let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
let _ = stream stream
.write_all(b"GET /lower HTTP/1.1\r\nConnection: Close\r\n\r\n") .write_all(b"GET /lower HTTP/1.1\r\nConnection: Close\r\n\r\n")
.unwrap(); .unwrap();
let mut data = vec![]; let mut data = vec![];

View File

@ -181,6 +181,25 @@ where
} }
} }
/// Configuration options used when accepting TLS connection.
#[cfg(any(feature = "openssl", feature = "rustls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "openssl", feature = "rustls"))))]
#[derive(Debug, Default)]
pub struct TlsAcceptorConfig {
pub(crate) handshake_timeout: Option<std::time::Duration>,
}
#[cfg(any(feature = "openssl", feature = "rustls"))]
impl TlsAcceptorConfig {
/// Set TLS handshake timeout duration.
pub fn handshake_timeout(self, dur: std::time::Duration) -> Self {
Self {
handshake_timeout: Some(dur),
// ..self
}
}
}
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
mod openssl { mod openssl {
use actix_service::ServiceFactoryExt as _; use actix_service::ServiceFactoryExt as _;
@ -230,7 +249,28 @@ mod openssl {
Error = TlsError<SslError, DispatchError>, Error = TlsError<SslError, DispatchError>,
InitError = (), InitError = (),
> { > {
Acceptor::new(acceptor) self.openssl_with_config(acceptor, TlsAcceptorConfig::default())
}
/// Create OpenSSL based service with custom TLS acceptor configuration.
pub fn openssl_with_config(
self,
acceptor: SslAcceptor,
tls_acceptor_config: TlsAcceptorConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<SslError, DispatchError>,
InitError = (),
> {
let mut acceptor = Acceptor::new(acceptor);
if let Some(handshake_timeout) = tls_acceptor_config.handshake_timeout {
acceptor.set_handshake_timeout(handshake_timeout);
}
acceptor
.map_init_err(|_| { .map_init_err(|_| {
unreachable!("TLS acceptor service factory does not error on init") unreachable!("TLS acceptor service factory does not error on init")
}) })
@ -293,8 +333,23 @@ mod rustls {
{ {
/// Create Rustls based service. /// Create Rustls based service.
pub fn rustls( pub fn rustls(
self,
config: ServerConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = (),
> {
self.rustls_with_config(config, TlsAcceptorConfig::default())
}
/// Create Rustls based service with custom TLS acceptor configuration.
pub fn rustls_with_config(
self, self,
mut config: ServerConfig, mut config: ServerConfig,
tls_acceptor_config: TlsAcceptorConfig,
) -> impl ServiceFactory< ) -> impl ServiceFactory<
TcpStream, TcpStream,
Config = (), Config = (),
@ -306,7 +361,13 @@ mod rustls {
protos.extend_from_slice(&config.alpn_protocols); protos.extend_from_slice(&config.alpn_protocols);
config.alpn_protocols = protos; config.alpn_protocols = protos;
Acceptor::new(config) let mut acceptor = Acceptor::new(config);
if let Some(handshake_timeout) = tls_acceptor_config.handshake_timeout {
acceptor.set_handshake_timeout(handshake_timeout);
}
acceptor
.map_init_err(|_| { .map_init_err(|_| {
unreachable!("TLS acceptor service factory does not error on init") unreachable!("TLS acceptor service factory does not error on init")
}) })

View File

@ -2,13 +2,13 @@
extern crate tls_openssl as openssl; extern crate tls_openssl as openssl;
use std::{convert::Infallible, io}; use std::{convert::Infallible, io, time::Duration};
use actix_http::{ use actix_http::{
body::{BodyStream, BoxBody, SizedStream}, body::{BodyStream, BoxBody, SizedStream},
error::PayloadError, error::PayloadError,
header::{self, HeaderValue}, header::{self, HeaderValue},
Error, HttpService, Method, Request, Response, StatusCode, Version, Error, HttpService, Method, Request, Response, StatusCode, TlsAcceptorConfig, Version,
}; };
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_service::{fn_service, ServiceFactoryExt}; use actix_service::{fn_service, ServiceFactoryExt};
@ -89,7 +89,10 @@ async fn h2_1() -> io::Result<()> {
assert_eq!(req.version(), Version::HTTP_2); assert_eq!(req.version(), Version::HTTP_2);
ok::<_, Error>(Response::ok()) ok::<_, Error>(Response::ok())
}) })
.openssl(tls_config()) .openssl_with_config(
tls_config(),
TlsAcceptorConfig::default().handshake_timeout(Duration::from_secs(5)),
)
.map_err(|_| ()) .map_err(|_| ())
}) })
.await; .await;

View File

@ -8,13 +8,14 @@ use std::{
net::{SocketAddr, TcpStream as StdTcpStream}, net::{SocketAddr, TcpStream as StdTcpStream},
sync::Arc, sync::Arc,
task::Poll, task::Poll,
time::Duration,
}; };
use actix_http::{ use actix_http::{
body::{BodyStream, BoxBody, SizedStream}, body::{BodyStream, BoxBody, SizedStream},
error::PayloadError, error::PayloadError,
header::{self, HeaderName, HeaderValue}, header::{self, HeaderName, HeaderValue},
Error, HttpService, Method, Request, Response, StatusCode, Version, Error, HttpService, Method, Request, Response, StatusCode, TlsAcceptorConfig, Version,
}; };
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_rt::pin; use actix_rt::pin;
@ -160,7 +161,10 @@ async fn h2_1() -> io::Result<()> {
assert_eq!(req.version(), Version::HTTP_2); assert_eq!(req.version(), Version::HTTP_2);
ok::<_, Error>(Response::ok()) ok::<_, Error>(Response::ok())
}) })
.rustls(tls_config()) .rustls_with_config(
tls_config(),
TlsAcceptorConfig::default().handshake_timeout(Duration::from_secs(5)),
)
}) })
.await; .await;

View File

@ -649,7 +649,7 @@ impl ResourceDef {
/// resource.capture_match_info_fn( /// resource.capture_match_info_fn(
/// path, /// path,
/// // when env var is not set, reject when path contains "admin" /// // when env var is not set, reject when path contains "admin"
/// |res| !(!admin_allowed && res.path().contains("admin")), /// |path| !(!admin_allowed && path.as_str().contains("admin")),
/// ) /// )
/// } /// }
/// ///

View File

@ -29,6 +29,9 @@ tokio = { version = "1.13.1", features = ["sync"] }
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0-beta.13" actix-test = "0.1.0-beta.13"
awc = { version = "3", default-features = false } awc = { version = "3", default-features = false }
actix-web = { version = "4", features = ["macros"] }
mime = "0.3"
env_logger = "0.9" env_logger = "0.9"
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false }

View File

@ -14,6 +14,58 @@ use futures_core::Stream;
use tokio::sync::oneshot::Sender; use tokio::sync::oneshot::Sender;
/// Execution context for HTTP actors /// Execution context for HTTP actors
///
/// # Example
///
/// A demonstration of [server-sent events](https://developer.mozilla.org/docs/Web/API/Server-sent_events) using actors:
///
/// ```no_run
/// use std::time::Duration;
///
/// use actix::{Actor, AsyncContext};
/// use actix_web::{get, http::header, App, HttpResponse, HttpServer};
/// use actix_web_actors::HttpContext;
/// use bytes::Bytes;
///
/// struct MyActor {
/// count: usize,
/// }
///
/// impl Actor for MyActor {
/// type Context = HttpContext<Self>;
///
/// fn started(&mut self, ctx: &mut Self::Context) {
/// ctx.run_later(Duration::from_millis(100), Self::write);
/// }
/// }
///
/// impl MyActor {
/// fn write(&mut self, ctx: &mut HttpContext<Self>) {
/// self.count += 1;
/// if self.count > 3 {
/// ctx.write_eof()
/// } else {
/// ctx.write(Bytes::from(format!("event: count\ndata: {}\n\n", self.count)));
/// ctx.run_later(Duration::from_millis(100), Self::write);
/// }
/// }
/// }
///
/// #[get("/")]
/// async fn index() -> HttpResponse {
/// HttpResponse::Ok()
/// .insert_header(header::ContentType(mime::TEXT_EVENT_STREAM))
/// .streaming(HttpContext::create(MyActor { count: 0 }))
/// }
///
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// HttpServer::new(|| App::new().service(index))
/// .bind(("127.0.0.1", 8080))?
/// .run()
/// .await
/// }
/// ```
pub struct HttpContext<A> pub struct HttpContext<A>
where where
A: Actor<Context = HttpContext<A>>, A: Actor<Context = HttpContext<A>>,
@ -210,7 +262,7 @@ mod tests {
type Context = HttpContext<Self>; type Context = HttpContext<Self>;
fn started(&mut self, ctx: &mut Self::Context) { fn started(&mut self, ctx: &mut Self::Context) {
ctx.run_later(Duration::from_millis(100), |slf, ctx| slf.write(ctx)); ctx.run_later(Duration::from_millis(100), Self::write);
} }
} }
@ -221,7 +273,7 @@ mod tests {
ctx.write_eof() ctx.write_eof()
} else { } else {
ctx.write(Bytes::from(format!("LINE-{}", self.count))); ctx.write(Bytes::from(format!("LINE-{}", self.count)));
ctx.run_later(Duration::from_millis(100), |slf, ctx| slf.write(ctx)); ctx.run_later(Duration::from_millis(100), Self::write);
} }
} }
} }

View File

@ -1,4 +1,59 @@
//! Actix actors support for Actix Web. //! Actix actors support for Actix Web.
//!
//! # Examples
//!
//! ```no_run
//! use actix::{Actor, StreamHandler};
//! use actix_web::{get, web, App, Error, HttpRequest, HttpResponse, HttpServer};
//! use actix_web_actors::ws;
//!
//! /// Define Websocket actor
//! struct MyWs;
//!
//! impl Actor for MyWs {
//! type Context = ws::WebsocketContext<Self>;
//! }
//!
//! /// Handler for ws::Message message
//! impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWs {
//! fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
//! match msg {
//! Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
//! Ok(ws::Message::Text(text)) => ctx.text(text),
//! Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
//! _ => (),
//! }
//! }
//! }
//!
//! #[get("/ws")]
//! async fn index(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
//! ws::start(MyWs, &req, stream)
//! }
//!
//! #[actix_web::main]
//! async fn main() -> std::io::Result<()> {
//! HttpServer::new(|| App::new().service(index))
//! .bind(("127.0.0.1", 8080))?
//! .run()
//! .await
//! }
//! ```
//!
//! # Documentation & Community Resources
//! In addition to this API documentation, several other resources are available:
//!
//! * [Website & User Guide](https://actix.rs/)
//! * [Documentation for `actix_web`](actix_web)
//! * [Examples Repository](https://github.com/actix/examples)
//! * [Community Chat on Discord](https://discord.gg/NWpN5mmg3x)
//!
//! To get started navigating the API docs, you may consider looking at the following pages first:
//!
//! * [`ws`]: This module provides actor support for WebSockets.
//!
//! * [`HttpContext`]: This struct provides actor support for streaming HTTP responses.
//!
#![deny(rust_2018_idioms, nonstandard_style)] #![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)] #![warn(future_incompatible)]

View File

@ -1,4 +1,60 @@
//! Websocket integration. //! Websocket integration.
//!
//! # Examples
//!
//! ```no_run
//! use actix::{Actor, StreamHandler};
//! use actix_web::{get, web, App, Error, HttpRequest, HttpResponse, HttpServer};
//! use actix_web_actors::ws;
//!
//! /// Define Websocket actor
//! struct MyWs;
//!
//! impl Actor for MyWs {
//! type Context = ws::WebsocketContext<Self>;
//! }
//!
//! /// Handler for ws::Message message
//! impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWs {
//! fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
//! match msg {
//! Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
//! Ok(ws::Message::Text(text)) => ctx.text(text),
//! Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
//! _ => (),
//! }
//! }
//! }
//!
//! #[get("/ws")]
//! async fn websocket(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
//! ws::start(MyWs, &req, stream)
//! }
//!
//! const MAX_FRAME_SIZE: usize = 16_384; // 16KiB
//!
//! #[get("/custom-ws")]
//! async fn custom_websocket(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
//! // Create a Websocket session with a specific max frame size, and protocols.
//! ws::WsResponseBuilder::new(MyWs, &req, stream)
//! .frame_size(MAX_FRAME_SIZE)
//! .protocols(&["A", "B"])
//! .start()
//! }
//!
//! #[actix_web::main]
//! async fn main() -> std::io::Result<()> {
//! HttpServer::new(|| {
//! App::new()
//! .service(websocket)
//! .service(custom_websocket)
//! })
//! .bind(("127.0.0.1", 8080))?
//! .run()
//! .await
//! }
//! ```
//!
use std::{ use std::{
collections::VecDeque, collections::VecDeque,
@ -41,20 +97,51 @@ use tokio::sync::oneshot;
/// ///
/// # Examples /// # Examples
/// ///
/// Create a Websocket session response with default configuration. /// ```no_run
/// ```ignore /// # use actix::{Actor, StreamHandler};
/// WsResponseBuilder::new(WsActor, &req, stream).start() /// # use actix_web::{get, web, App, Error, HttpRequest, HttpResponse, HttpServer};
/// ``` /// # use actix_web_actors::ws;
/// #
/// # struct MyWs;
/// #
/// # impl Actor for MyWs {
/// # type Context = ws::WebsocketContext<Self>;
/// # }
/// #
/// # /// Handler for ws::Message message
/// # impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWs {
/// # fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {}
/// # }
/// #
/// #[get("/ws")]
/// async fn websocket(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
/// ws::WsResponseBuilder::new(MyWs, &req, stream).start()
/// }
/// ///
/// Create a Websocket session with a specific max frame size, [`Codec`], and protocols.
/// ```ignore
/// const MAX_FRAME_SIZE: usize = 16_384; // 16KiB /// const MAX_FRAME_SIZE: usize = 16_384; // 16KiB
/// ///
/// ws::WsResponseBuilder::new(WsActor, &req, stream) /// #[get("/custom-ws")]
/// .codec(Codec::new()) /// async fn custom_websocket(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
/// .protocols(&["A", "B"]) /// // Create a Websocket session with a specific max frame size, codec, and protocols.
/// ws::WsResponseBuilder::new(MyWs, &req, stream)
/// .codec(actix_http::ws::Codec::new())
/// // This will overwrite the codec's max frame-size
/// .frame_size(MAX_FRAME_SIZE) /// .frame_size(MAX_FRAME_SIZE)
/// .protocols(&["A", "B"])
/// .start() /// .start()
/// }
/// #
/// # #[actix_web::main]
/// # async fn main() -> std::io::Result<()> {
/// # HttpServer::new(|| {
/// # App::new()
/// # .service(websocket)
/// # .service(custom_websocket)
/// # })
/// # .bind(("127.0.0.1", 8080))?
/// # .run()
/// # .await
/// # }
/// ``` /// ```
pub struct WsResponseBuilder<'a, A, T> pub struct WsResponseBuilder<'a, A, T>
where where

View File

@ -1,12 +1,17 @@
# Changelog # Changelog
## Unreleased - 2022-xx-xx ## Unreleased - 2022-xx-xx
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
### Added ### Added
- Add `ServiceRequest::{parts, request}()` getter methods. [#2786] - Add `ServiceRequest::{parts, request}()` getter methods. [#2786]
- Add configuration options for TLS handshake timeout via `HttpServer::{rustls, openssl}_with_config` methods. [#2752]
### Changed
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
[#2752]: https://github.com/actix/actix-web/pull/2752
[#2786]: https://github.com/actix/actix-web/pull/2786 [#2786]: https://github.com/actix/actix-web/pull/2786
## 4.1.0 - 2022-06-11 ## 4.1.0 - 2022-06-11
### Added ### Added
- Add `ServiceRequest::extract()` to make it easier to use extractors when writing middlewares. [#2647] - Add `ServiceRequest::extract()` to make it easier to use extractors when writing middlewares. [#2647]

View File

@ -105,14 +105,14 @@ actix-test = { version = "0.1.0-beta.13", features = ["openssl", "rustls"] }
awc = { version = "3", features = ["openssl"] } awc = { version = "3", features = ["openssl"] }
brotli = "3.3.3" brotli = "3.3.3"
const-str = "0.3" const-str = "0.4"
criterion = { version = "0.3", features = ["html_reports"] } criterion = { version = "0.3", features = ["html_reports"] }
env_logger = "0.9" env_logger = "0.9"
flate2 = "1.0.13" flate2 = "1.0.13"
futures-util = { version = "0.3.7", default-features = false, features = ["std"] } futures-util = { version = "0.3.7", default-features = false, features = ["std"] }
rand = "0.8" rand = "0.8"
rcgen = "0.8" rcgen = "0.9"
rustls-pemfile = "0.2" rustls-pemfile = "1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
static_assertions = "1" static_assertions = "1"
tls-openssl = { package = "openssl", version = "0.10.9" } tls-openssl = { package = "openssl", version = "0.10.9" }

View File

@ -251,6 +251,8 @@ static SUPPORTED_ENCODINGS: Lazy<Vec<Encoding>> = Lazy::new(|| {
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::HashSet;
use super::*; use super::*;
use crate::{middleware::DefaultHeaders, test, web, App}; use crate::{middleware::DefaultHeaders, test, web, App};
@ -305,4 +307,27 @@ mod tests {
let bytes = test::read_body(res).await; let bytes = test::read_body(res).await;
assert_eq!(gzip_decode(bytes), DATA.as_bytes()); assert_eq!(gzip_decode(bytes), DATA.as_bytes());
} }
#[actix_rt::test]
async fn retains_previously_set_vary_header() {
let app = test::init_service({
App::new()
.wrap(Compress::default())
.default_service(web::to(move || {
HttpResponse::Ok()
.insert_header((header::VARY, "x-test"))
.finish()
}))
})
.await;
let req = test::TestRequest::default()
.insert_header((header::ACCEPT_ENCODING, "gzip"))
.to_request();
let res = test::call_service(&app, req).await;
assert_eq!(res.status(), StatusCode::OK);
let vary_headers = res.headers().get_all(header::VARY).collect::<HashSet<_>>();
assert!(vary_headers.contains(&HeaderValue::from_static("x-test")));
assert!(vary_headers.contains(&HeaderValue::from_static("accept-encoding")));
}
} }

View File

@ -18,6 +18,9 @@ use actix_tls::accept::openssl::reexports::{AlpnError, SslAcceptor, SslAcceptorB
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
use actix_tls::accept::rustls::reexports::ServerConfig as RustlsServerConfig; use actix_tls::accept::rustls::reexports::ServerConfig as RustlsServerConfig;
#[cfg(any(feature = "openssl", feature = "rustls"))]
use actix_http::TlsAcceptorConfig;
use crate::{config::AppConfig, Error}; use crate::{config::AppConfig, Error};
struct Socket { struct Socket {
@ -30,6 +33,8 @@ struct Config {
keep_alive: KeepAlive, keep_alive: KeepAlive,
client_request_timeout: Duration, client_request_timeout: Duration,
client_disconnect_timeout: Duration, client_disconnect_timeout: Duration,
#[cfg(any(feature = "openssl", feature = "rustls"))]
tls_handshake_timeout: Option<Duration>,
} }
/// An HTTP Server. /// An HTTP Server.
@ -92,6 +97,8 @@ where
keep_alive: KeepAlive::default(), keep_alive: KeepAlive::default(),
client_request_timeout: Duration::from_secs(5), client_request_timeout: Duration::from_secs(5),
client_disconnect_timeout: Duration::from_secs(1), client_disconnect_timeout: Duration::from_secs(1),
#[cfg(any(feature = "rustls", feature = "openssl"))]
tls_handshake_timeout: None,
})), })),
backlog: 1024, backlog: 1024,
sockets: Vec::new(), sockets: Vec::new(),
@ -225,6 +232,24 @@ where
self self
} }
/// Set TLS handshake timeout.
///
/// Defines a timeout for TLS handshake. If the TLS handshake does not complete
/// within this time, the connection is closed.
///
/// By default handshake timeout is set to 3000 milliseconds.
#[cfg(any(feature = "openssl", feature = "rustls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "openssl", feature = "rustls"))))]
pub fn tls_handshake_timeout(self, dur: Duration) -> Self {
self.config
.lock()
.unwrap()
.tls_handshake_timeout
.replace(dur);
self
}
#[doc(hidden)] #[doc(hidden)]
#[deprecated(since = "4.0.0", note = "Renamed to `client_disconnect_timeout`.")] #[deprecated(since = "4.0.0", note = "Renamed to `client_disconnect_timeout`.")]
pub fn client_shutdown(self, dur: u64) -> Self { pub fn client_shutdown(self, dur: u64) -> Self {
@ -376,10 +401,15 @@ where
.into_factory() .into_factory()
.map_err(|err| err.into().error_response()); .map_err(|err| err.into().error_response());
let acceptor_config = match c.tls_handshake_timeout {
Some(dur) => TlsAcceptorConfig::default().handshake_timeout(dur),
None => TlsAcceptorConfig::default(),
};
svc.finish(map_config(fac, move |_| { svc.finish(map_config(fac, move |_| {
AppConfig::new(true, host.clone(), addr) AppConfig::new(true, host.clone(), addr)
})) }))
.openssl(acceptor.clone()) .openssl_with_config(acceptor.clone(), acceptor_config)
})?; })?;
Ok(self) Ok(self)
@ -434,10 +464,15 @@ where
.into_factory() .into_factory()
.map_err(|err| err.into().error_response()); .map_err(|err| err.into().error_response());
let acceptor_config = match c.tls_handshake_timeout {
Some(dur) => TlsAcceptorConfig::default().handshake_timeout(dur),
None => TlsAcceptorConfig::default(),
};
svc.finish(map_config(fac, move |_| { svc.finish(map_config(fac, move |_| {
AppConfig::new(true, host.clone(), addr) AppConfig::new(true, host.clone(), addr)
})) }))
.rustls(config.clone()) .rustls_with_config(config.clone(), acceptor_config)
})?; })?;
Ok(self) Ok(self)

View File

@ -183,6 +183,7 @@ mod tests {
assert!(Path::<MyStruct>::from_request(&req, &mut pl).await.is_err()); assert!(Path::<MyStruct>::from_request(&req, &mut pl).await.is_err());
} }
#[allow(clippy::let_unit_value)]
#[actix_rt::test] #[actix_rt::test]
async fn test_tuple_extract() { async fn test_tuple_extract() {
let resource = ResourceDef::new("/{key}/{value}/"); let resource = ResourceDef::new("/{key}/{value}/");

View File

@ -113,7 +113,7 @@ pub struct BytesExtractFut {
body_fut: HttpMessageBody, body_fut: HttpMessageBody,
} }
impl<'a> Future for BytesExtractFut { impl Future for BytesExtractFut {
type Output = Result<Bytes, Error>; type Output = Result<Bytes, Error>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
@ -167,7 +167,7 @@ pub struct StringExtractFut {
encoding: &'static Encoding, encoding: &'static Encoding,
} }
impl<'a> Future for StringExtractFut { impl Future for StringExtractFut {
type Output = Result<String, Error>; type Output = Result<String, Error>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {

View File

@ -90,7 +90,7 @@ cookie = { version = "0.16", features = ["percent-encode"], optional = true }
tls-openssl = { package = "openssl", version = "0.10.9", optional = true } tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features = ["dangerous_configuration"] } tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features = ["dangerous_configuration"] }
trust-dns-resolver = { version = "0.20.0", optional = true } trust-dns-resolver = { version = "0.21", optional = true }
[dev-dependencies] [dev-dependencies]
actix-http = { version = "3", features = ["openssl"] } actix-http = { version = "3", features = ["openssl"] }
@ -102,13 +102,13 @@ actix-utils = "3"
actix-web = { version = "4", features = ["openssl"] } actix-web = { version = "4", features = ["openssl"] }
brotli = "3.3.3" brotli = "3.3.3"
const-str = "0.3" const-str = "0.4"
env_logger = "0.9" env_logger = "0.9"
flate2 = "1.0.13" flate2 = "1.0.13"
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false }
static_assertions = "1.1" static_assertions = "1.1"
rcgen = "0.8" rcgen = "0.9"
rustls-pemfile = "0.2" rustls-pemfile = "1"
tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] } tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] }
zstd = "0.11" zstd = "0.11"