,
+
flags: Flags,
}
diff --git a/actix-http/src/requests/request.rs b/actix-http/src/requests/request.rs
index 1750fb2f7..6a267a7a6 100644
--- a/actix-http/src/requests/request.rs
+++ b/actix-http/src/requests/request.rs
@@ -173,7 +173,7 @@ impl Request
{
/// Peer address is the directly connected peer's socket address. If a proxy is used in front of
/// the Actix Web server, then it would be address of this proxy.
///
- /// Will only return None when called in unit tests.
+ /// Will only return None when called in unit tests unless set manually.
#[inline]
pub fn peer_addr(&self) -> Option {
self.head().peer_addr
diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs
index fb38ba636..a58be93c7 100644
--- a/actix-http/src/service.rs
+++ b/actix-http/src/service.rs
@@ -241,13 +241,25 @@ where
}
/// Configuration options used when accepting TLS connection.
-#[cfg(any(feature = "openssl", feature = "rustls-0_20", feature = "rustls-0_21"))]
+#[cfg(any(
+ feature = "openssl",
+ feature = "rustls-0_20",
+ feature = "rustls-0_21",
+ feature = "rustls-0_22",
+ feature = "rustls-0_23",
+))]
#[derive(Debug, Default)]
pub struct TlsAcceptorConfig {
pub(crate) handshake_timeout: Option,
}
-#[cfg(any(feature = "openssl", feature = "rustls-0_20", feature = "rustls-0_21"))]
+#[cfg(any(
+ feature = "openssl",
+ feature = "rustls-0_20",
+ feature = "rustls-0_21",
+ feature = "rustls-0_22",
+ feature = "rustls-0_23",
+))]
impl TlsAcceptorConfig {
/// Set TLS handshake timeout duration.
pub fn handshake_timeout(self, dur: std::time::Duration) -> Self {
@@ -353,12 +365,12 @@ mod openssl {
}
#[cfg(feature = "rustls-0_20")]
-mod rustls_020 {
+mod rustls_0_20 {
use std::io;
use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{
- rustls::{reexports::ServerConfig, Acceptor, TlsStream},
+ rustls_0_20::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError,
};
@@ -389,7 +401,7 @@ mod rustls_020 {
U::Error: fmt::Display + Into>,
U::InitError: fmt::Debug,
{
- /// Create Rustls based service.
+ /// Create Rustls v0.20 based service.
pub fn rustls(
self,
config: ServerConfig,
@@ -403,7 +415,7 @@ mod rustls_020 {
self.rustls_with_config(config, TlsAcceptorConfig::default())
}
- /// Create Rustls based service with custom TLS acceptor configuration.
+ /// Create Rustls v0.20 based service with custom TLS acceptor configuration.
pub fn rustls_with_config(
self,
mut config: ServerConfig,
@@ -449,7 +461,7 @@ mod rustls_020 {
}
#[cfg(feature = "rustls-0_21")]
-mod rustls_021 {
+mod rustls_0_21 {
use std::io;
use actix_service::ServiceFactoryExt as _;
@@ -485,7 +497,7 @@ mod rustls_021 {
U::Error: fmt::Display + Into>,
U::InitError: fmt::Debug,
{
- /// Create Rustls based service.
+ /// Create Rustls v0.21 based service.
pub fn rustls_021(
self,
config: ServerConfig,
@@ -499,7 +511,7 @@ mod rustls_021 {
self.rustls_021_with_config(config, TlsAcceptorConfig::default())
}
- /// Create Rustls based service with custom TLS acceptor configuration.
+ /// Create Rustls v0.21 based service with custom TLS acceptor configuration.
pub fn rustls_021_with_config(
self,
mut config: ServerConfig,
@@ -544,6 +556,198 @@ mod rustls_021 {
}
}
+#[cfg(feature = "rustls-0_22")]
+mod rustls_0_22 {
+ use std::io;
+
+ use actix_service::ServiceFactoryExt as _;
+ use actix_tls::accept::{
+ rustls_0_22::{reexports::ServerConfig, Acceptor, TlsStream},
+ TlsError,
+ };
+
+ use super::*;
+
+ impl HttpService, S, B, X, U>
+ where
+ S: ServiceFactory,
+ S::Future: 'static,
+ S::Error: Into> + 'static,
+ S::InitError: fmt::Debug,
+ S::Response: Into> + 'static,
+ >::Future: 'static,
+
+ B: MessageBody + 'static,
+
+ X: ServiceFactory,
+ X::Future: 'static,
+ X::Error: Into>,
+ X::InitError: fmt::Debug,
+
+ U: ServiceFactory<
+ (Request, Framed, h1::Codec>),
+ Config = (),
+ Response = (),
+ >,
+ U::Future: 'static,
+ U::Error: fmt::Display + Into>,
+ U::InitError: fmt::Debug,
+ {
+ /// Create Rustls v0.22 based service.
+ pub fn rustls_0_22(
+ self,
+ config: ServerConfig,
+ ) -> impl ServiceFactory<
+ TcpStream,
+ Config = (),
+ Response = (),
+ Error = TlsError,
+ InitError = (),
+ > {
+ self.rustls_0_22_with_config(config, TlsAcceptorConfig::default())
+ }
+
+ /// Create Rustls v0.22 based service with custom TLS acceptor configuration.
+ pub fn rustls_0_22_with_config(
+ self,
+ mut config: ServerConfig,
+ tls_acceptor_config: TlsAcceptorConfig,
+ ) -> impl ServiceFactory<
+ TcpStream,
+ Config = (),
+ Response = (),
+ Error = TlsError,
+ InitError = (),
+ > {
+ let mut protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
+ protos.extend_from_slice(&config.alpn_protocols);
+ config.alpn_protocols = protos;
+
+ 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(|_| {
+ unreachable!("TLS acceptor service factory does not error on init")
+ })
+ .map_err(TlsError::into_service_error)
+ .and_then(|io: TlsStream| async {
+ let proto = if let Some(protos) = io.get_ref().1.alpn_protocol() {
+ if protos.windows(2).any(|window| window == b"h2") {
+ Protocol::Http2
+ } else {
+ Protocol::Http1
+ }
+ } else {
+ Protocol::Http1
+ };
+ let peer_addr = io.get_ref().0.peer_addr().ok();
+ Ok((io, proto, peer_addr))
+ })
+ .and_then(self.map_err(TlsError::Service))
+ }
+ }
+}
+
+#[cfg(feature = "rustls-0_23")]
+mod rustls_0_23 {
+ use std::io;
+
+ use actix_service::ServiceFactoryExt as _;
+ use actix_tls::accept::{
+ rustls_0_23::{reexports::ServerConfig, Acceptor, TlsStream},
+ TlsError,
+ };
+
+ use super::*;
+
+ impl HttpService, S, B, X, U>
+ where
+ S: ServiceFactory,
+ S::Future: 'static,
+ S::Error: Into> + 'static,
+ S::InitError: fmt::Debug,
+ S::Response: Into> + 'static,
+ >::Future: 'static,
+
+ B: MessageBody + 'static,
+
+ X: ServiceFactory,
+ X::Future: 'static,
+ X::Error: Into>,
+ X::InitError: fmt::Debug,
+
+ U: ServiceFactory<
+ (Request, Framed, h1::Codec>),
+ Config = (),
+ Response = (),
+ >,
+ U::Future: 'static,
+ U::Error: fmt::Display + Into>,
+ U::InitError: fmt::Debug,
+ {
+ /// Create Rustls v0.23 based service.
+ pub fn rustls_0_23(
+ self,
+ config: ServerConfig,
+ ) -> impl ServiceFactory<
+ TcpStream,
+ Config = (),
+ Response = (),
+ Error = TlsError,
+ InitError = (),
+ > {
+ self.rustls_0_23_with_config(config, TlsAcceptorConfig::default())
+ }
+
+ /// Create Rustls v0.23 based service with custom TLS acceptor configuration.
+ pub fn rustls_0_23_with_config(
+ self,
+ mut config: ServerConfig,
+ tls_acceptor_config: TlsAcceptorConfig,
+ ) -> impl ServiceFactory<
+ TcpStream,
+ Config = (),
+ Response = (),
+ Error = TlsError,
+ InitError = (),
+ > {
+ let mut protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
+ protos.extend_from_slice(&config.alpn_protocols);
+ config.alpn_protocols = protos;
+
+ 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(|_| {
+ unreachable!("TLS acceptor service factory does not error on init")
+ })
+ .map_err(TlsError::into_service_error)
+ .and_then(|io: TlsStream| async {
+ let proto = if let Some(protos) = io.get_ref().1.alpn_protocol() {
+ if protos.windows(2).any(|window| window == b"h2") {
+ Protocol::Http2
+ } else {
+ Protocol::Http1
+ }
+ } else {
+ Protocol::Http1
+ };
+ let peer_addr = io.get_ref().0.peer_addr().ok();
+ Ok((io, proto, peer_addr))
+ })
+ .and_then(self.map_err(TlsError::Service))
+ }
+ }
+}
+
impl ServiceFactory<(T, Protocol, Option)>
for HttpService
where
diff --git a/actix-http/src/ws/mod.rs b/actix-http/src/ws/mod.rs
index 87f9b38f3..3ed53b70a 100644
--- a/actix-http/src/ws/mod.rs
+++ b/actix-http/src/ws/mod.rs
@@ -221,7 +221,7 @@ pub fn handshake_response(req: &RequestHead) -> ResponseBuilder {
#[cfg(test)]
mod tests {
use super::*;
- use crate::{header, test::TestRequest, Method};
+ use crate::{header, test::TestRequest};
#[test]
fn test_handshake() {
diff --git a/actix-http/src/ws/proto.rs b/actix-http/src/ws/proto.rs
index 0653c00b0..27815eaf2 100644
--- a/actix-http/src/ws/proto.rs
+++ b/actix-http/src/ws/proto.rs
@@ -1,7 +1,4 @@
-use std::{
- convert::{From, Into},
- fmt,
-};
+use std::fmt;
use base64::prelude::*;
use tracing::error;
diff --git a/actix-http/tests/test_openssl.rs b/actix-http/tests/test_openssl.rs
index cb16a4fec..4dd22b585 100644
--- a/actix-http/tests/test_openssl.rs
+++ b/actix-http/tests/test_openssl.rs
@@ -42,9 +42,11 @@ where
}
fn tls_config() -> SslAcceptor {
- let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
- let cert_file = cert.serialize_pem().unwrap();
- let key_file = cert.serialize_private_key_pem();
+ let rcgen::CertifiedKey { cert, key_pair } =
+ rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap();
+ let cert_file = cert.pem();
+ let key_file = key_pair.serialize_pem();
+
let cert = X509::from_pem(cert_file.as_bytes()).unwrap();
let key = PKey::private_key_from_pem(key_file.as_bytes()).unwrap();
diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs
index c94e579e5..3ca0d94c2 100644
--- a/actix-http/tests/test_rustls.rs
+++ b/actix-http/tests/test_rustls.rs
@@ -1,6 +1,6 @@
-#![cfg(feature = "rustls-0_21")]
+#![cfg(feature = "rustls-0_23")]
-extern crate tls_rustls_021 as rustls;
+extern crate tls_rustls_023 as rustls;
use std::{
convert::Infallible,
@@ -20,13 +20,13 @@ use actix_http::{
use actix_http_test::test_server;
use actix_rt::pin;
use actix_service::{fn_factory_with_config, fn_service};
-use actix_tls::connect::rustls_0_21::webpki_roots_cert_store;
+use actix_tls::connect::rustls_0_23::webpki_roots_cert_store;
use actix_utils::future::{err, ok, poll_fn};
use bytes::{Bytes, BytesMut};
use derive_more::{Display, Error};
use futures_core::{ready, Stream};
use futures_util::stream::once;
-use rustls::{Certificate, PrivateKey, ServerConfig as RustlsServerConfig, ServerName};
+use rustls::{pki_types::ServerName, ServerConfig as RustlsServerConfig};
use rustls_pemfile::{certs, pkcs8_private_keys};
async fn load_body(stream: S) -> Result
@@ -52,24 +52,25 @@ where
}
fn tls_config() -> RustlsServerConfig {
- let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
- let cert_file = cert.serialize_pem().unwrap();
- let key_file = cert.serialize_private_key_pem();
+ let rcgen::CertifiedKey { cert, key_pair } =
+ rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap();
+ let cert_file = cert.pem();
+ let key_file = key_pair.serialize_pem();
let cert_file = &mut BufReader::new(cert_file.as_bytes());
let key_file = &mut BufReader::new(key_file.as_bytes());
- let cert_chain = certs(cert_file)
- .unwrap()
- .into_iter()
- .map(Certificate)
- .collect();
- let mut keys = pkcs8_private_keys(key_file).unwrap();
+ let cert_chain = certs(cert_file).collect::, _>>().unwrap();
+ let mut keys = pkcs8_private_keys(key_file)
+ .collect::, _>>()
+ .unwrap();
let mut config = RustlsServerConfig::builder()
- .with_safe_defaults()
.with_no_client_auth()
- .with_single_cert(cert_chain, PrivateKey(keys.remove(0)))
+ .with_single_cert(
+ cert_chain,
+ rustls::pki_types::PrivateKeyDer::Pkcs8(keys.remove(0)),
+ )
.unwrap();
config.alpn_protocols.push(HTTP1_1_ALPN_PROTOCOL.to_vec());
@@ -83,7 +84,6 @@ pub fn get_negotiated_alpn_protocol(
client_alpn_protocol: &[u8],
) -> Option> {
let mut config = rustls::ClientConfig::builder()
- .with_safe_defaults()
.with_root_certificates(webpki_roots_cert_store())
.with_no_client_auth();
@@ -109,7 +109,7 @@ async fn h1() -> io::Result<()> {
let srv = test_server(move || {
HttpService::build()
.h1(|_| ok::<_, Error>(Response::ok()))
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -123,7 +123,7 @@ async fn h2() -> io::Result<()> {
let srv = test_server(move || {
HttpService::build()
.h2(|_| ok::<_, Error>(Response::ok()))
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -141,7 +141,7 @@ async fn h1_1() -> io::Result<()> {
assert_eq!(req.version(), Version::HTTP_11);
ok::<_, Error>(Response::ok())
})
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -159,7 +159,7 @@ async fn h2_1() -> io::Result<()> {
assert_eq!(req.version(), Version::HTTP_2);
ok::<_, Error>(Response::ok())
})
- .rustls_021_with_config(
+ .rustls_0_23_with_config(
tls_config(),
TlsAcceptorConfig::default().handshake_timeout(Duration::from_secs(5)),
)
@@ -180,7 +180,7 @@ async fn h2_body1() -> io::Result<()> {
let body = load_body(req.take_payload()).await?;
Ok::<_, Error>(Response::ok().set_body(body))
})
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -206,7 +206,7 @@ async fn h2_content_length() {
];
ok::<_, Infallible>(Response::new(statuses[indx]))
})
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -278,7 +278,7 @@ async fn h2_headers() {
}
ok::<_, Infallible>(config.body(data.clone()))
})
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -317,7 +317,7 @@ async fn h2_body2() {
let mut srv = test_server(move || {
HttpService::build()
.h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -334,7 +334,7 @@ async fn h2_head_empty() {
let mut srv = test_server(move || {
HttpService::build()
.finish(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -360,7 +360,7 @@ async fn h2_head_binary() {
let mut srv = test_server(move || {
HttpService::build()
.h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -385,7 +385,7 @@ async fn h2_head_binary2() {
let srv = test_server(move || {
HttpService::build()
.h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -411,7 +411,7 @@ async fn h2_body_length() {
Response::ok().set_body(SizedStream::new(STR.len() as u64, body)),
)
})
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -435,7 +435,7 @@ async fn h2_body_chunked_explicit() {
.body(BodyStream::new(body)),
)
})
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -464,7 +464,7 @@ async fn h2_response_http_error_handling() {
)
}))
}))
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -494,7 +494,7 @@ async fn h2_service_error() {
let mut srv = test_server(move || {
HttpService::build()
.h2(|_| err::, _>(BadRequest))
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -511,7 +511,7 @@ async fn h1_service_error() {
let mut srv = test_server(move || {
HttpService::build()
.h1(|_| err::, _>(BadRequest))
- .rustls_021(tls_config())
+ .rustls_0_23(tls_config())
})
.await;
@@ -534,7 +534,7 @@ async fn alpn_h1() -> io::Result<()> {
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build()
.h1(|_| ok::<_, Error>(Response::ok()))
- .rustls_021(config)
+ .rustls_0_23(config)
})
.await;
@@ -556,7 +556,7 @@ async fn alpn_h2() -> io::Result<()> {
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build()
.h2(|_| ok::<_, Error>(Response::ok()))
- .rustls_021(config)
+ .rustls_0_23(config)
})
.await;
@@ -582,7 +582,7 @@ async fn alpn_h2_1() -> io::Result<()> {
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build()
.finish(|_| ok::<_, Error>(Response::ok()))
- .rustls_021(config)
+ .rustls_0_23(config)
})
.await;
diff --git a/actix-multipart-derive/CHANGES.md b/actix-multipart-derive/CHANGES.md
index e36a13d04..1b44ba4b7 100644
--- a/actix-multipart-derive/CHANGES.md
+++ b/actix-multipart-derive/CHANGES.md
@@ -2,6 +2,8 @@
## Unreleased
+- Minimum supported Rust version (MSRV) is now 1.72.
+
## 0.6.1
- Update `syn` dependency to `2`.
diff --git a/actix-multipart-derive/Cargo.toml b/actix-multipart-derive/Cargo.toml
index 2f049a3fb..e978864a3 100644
--- a/actix-multipart-derive/Cargo.toml
+++ b/actix-multipart-derive/Cargo.toml
@@ -4,10 +4,11 @@ version = "0.6.1"
authors = ["Jacob Halsey "]
description = "Multipart form derive macro for Actix Web"
keywords = ["http", "web", "framework", "async", "futures"]
-homepage = "https://actix.rs"
-repository = "https://github.com/actix/actix-web"
-license = "MIT OR Apache-2.0"
-edition = "2021"
+homepage.workspace = true
+repository.workspace = true
+license.workspace = true
+edition.workspace = true
+rust-version.workspace = true
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
diff --git a/actix-multipart-derive/README.md b/actix-multipart-derive/README.md
index cd5780c56..ec0afffdd 100644
--- a/actix-multipart-derive/README.md
+++ b/actix-multipart-derive/README.md
@@ -1,17 +1,16 @@
-# actix-multipart-derive
+# `actix-multipart-derive`
> The derive macro implementation for actix-multipart-derive.
+
+
[](https://crates.io/crates/actix-multipart-derive)
[](https://docs.rs/actix-multipart-derive/0.6.1)
-
+

[](https://deps.rs/crate/actix-multipart-derive/0.6.1)
[](https://crates.io/crates/actix-multipart-derive)
[](https://discord.gg/NWpN5mmg3x)
-## Documentation & Resources
-
-- [API Documentation](https://docs.rs/actix-multipart-derive)
-- Minimum Supported Rust Version (MSRV): 1.68
+
diff --git a/actix-multipart-derive/tests/trybuild.rs b/actix-multipart-derive/tests/trybuild.rs
index 88aa619c6..6b25d78df 100644
--- a/actix-multipart-derive/tests/trybuild.rs
+++ b/actix-multipart-derive/tests/trybuild.rs
@@ -1,4 +1,4 @@
-#[rustversion::stable(1.68)] // MSRV
+#[rustversion::stable(1.72)] // MSRV
#[test]
fn compile_macros() {
let t = trybuild::TestCases::new();
diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md
index 50faf7cfa..196d2ca93 100644
--- a/actix-multipart/CHANGES.md
+++ b/actix-multipart/CHANGES.md
@@ -2,6 +2,9 @@
## Unreleased
+- Add testing utilities under new module `test`.
+- Minimum supported Rust version (MSRV) is now 1.72.
+
## 0.6.1
- Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency.
diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml
index 257d56132..6e36c3391 100644
--- a/actix-multipart/Cargo.toml
+++ b/actix-multipart/Cargo.toml
@@ -35,6 +35,7 @@ local-waker = "0.1"
log = "0.4"
memchr = "2.5"
mime = "0.3"
+rand = "0.8"
serde = "1"
serde_json = "1"
serde_plain = "1"
@@ -46,7 +47,9 @@ actix-http = "3"
actix-multipart-rfc7578 = "0.10"
actix-rt = "2.2"
actix-test = "0.1"
+actix-web = "4"
awc = "3"
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
+multer = "3"
tokio = { version = "1.24.2", features = ["sync"] }
tokio-stream = "0.1"
diff --git a/actix-multipart/README.md b/actix-multipart/README.md
index 8fe0328ab..83947b0c2 100644
--- a/actix-multipart/README.md
+++ b/actix-multipart/README.md
@@ -1,17 +1,78 @@
-# actix-multipart
+# `actix-multipart`
> Multipart form support for Actix Web.
+
+
[](https://crates.io/crates/actix-multipart)
[](https://docs.rs/actix-multipart/0.6.1)
-
+

[](https://deps.rs/crate/actix-multipart/0.6.1)
[](https://crates.io/crates/actix-multipart)
[](https://discord.gg/NWpN5mmg3x)
-## Documentation & Resources
+
-- [API Documentation](https://docs.rs/actix-multipart)
-- Minimum Supported Rust Version (MSRV): 1.68
+
+## Example
+
+Dependencies:
+
+```toml
+[dependencies]
+actix-multipart = "0.6"
+actix-web = "4.5"
+serde = { version = "1.0", features = ["derive"] }
+```
+
+Code:
+
+```rust
+use actix_web::{post, App, HttpServer, Responder};
+
+use actix_multipart::form::{json::Json as MPJson, tempfile::TempFile, MultipartForm};
+use serde::Deserialize;
+
+#[derive(Debug, Deserialize)]
+struct Metadata {
+ name: String,
+}
+
+#[derive(Debug, MultipartForm)]
+struct UploadForm {
+ #[multipart(limit = "100MB")]
+ file: TempFile,
+ json: MPJson,
+}
+
+#[post("/videos")]
+pub async fn post_video(MultipartForm(form): MultipartForm) -> impl Responder {
+ format!(
+ "Uploaded file {}, with size: {}",
+ form.json.name, form.file.size
+ )
+}
+
+#[actix_web::main]
+async fn main() -> std::io::Result<()> {
+ HttpServer::new(move || App::new().service(post_video))
+ .bind(("127.0.0.1", 8080))?
+ .run()
+ .await
+}
+```
+
+Curl request :
+```bash
+curl -v --request POST \
+ --url http://localhost:8080/videos \
+ -F 'json={"name": "Cargo.lock"};type=application/json' \
+ -F file=@./Cargo.lock
+```
+
+
+### Examples
+
+https://github.com/actix/examples/tree/master/forms/multipart
\ No newline at end of file
diff --git a/actix-multipart/src/form/json.rs b/actix-multipart/src/form/json.rs
index fb90a82b9..bb4e03bf6 100644
--- a/actix-multipart/src/form/json.rs
+++ b/actix-multipart/src/form/json.rs
@@ -131,14 +131,13 @@ impl Default for JsonConfig {
#[cfg(test)]
mod tests {
- use std::{collections::HashMap, io::Cursor};
+ use std::collections::HashMap;
- use actix_multipart_rfc7578::client::multipart;
use actix_web::{http::StatusCode, web, App, HttpResponse, Responder};
+ use bytes::Bytes;
use crate::form::{
json::{Json, JsonConfig},
- tests::send_form,
MultipartForm,
};
@@ -155,6 +154,8 @@ mod tests {
HttpResponse::Ok().finish()
}
+ const TEST_JSON: &str = r#"{"key1": "value1", "key2": "value2"}"#;
+
#[actix_rt::test]
async fn test_json_without_content_type() {
let srv = actix_test::start(|| {
@@ -163,10 +164,16 @@ mod tests {
.app_data(JsonConfig::default().validate_content_type(false))
});
- let mut form = multipart::Form::default();
- form.add_text("json", "{\"key1\": \"value1\", \"key2\": \"value2\"}");
- let response = send_form(&srv, form, "/").await;
- assert_eq!(response.status(), StatusCode::OK);
+ let (body, headers) = crate::test::create_form_data_payload_and_headers(
+ "json",
+ None,
+ None,
+ Bytes::from_static(TEST_JSON.as_bytes()),
+ );
+ let mut req = srv.post("/");
+ *req.headers_mut() = headers;
+ let res = req.send_body(body).await.unwrap();
+ assert_eq!(res.status(), StatusCode::OK);
}
#[actix_rt::test]
@@ -178,17 +185,27 @@ mod tests {
});
// Deny because wrong content type
- let bytes = Cursor::new("{\"key1\": \"value1\", \"key2\": \"value2\"}");
- let mut form = multipart::Form::default();
- form.add_reader_file_with_mime("json", bytes, "", mime::APPLICATION_OCTET_STREAM);
- let response = send_form(&srv, form, "/").await;
- assert_eq!(response.status(), StatusCode::BAD_REQUEST);
+ let (body, headers) = crate::test::create_form_data_payload_and_headers(
+ "json",
+ None,
+ Some(mime::APPLICATION_OCTET_STREAM),
+ Bytes::from_static(TEST_JSON.as_bytes()),
+ );
+ let mut req = srv.post("/");
+ *req.headers_mut() = headers;
+ let res = req.send_body(body).await.unwrap();
+ assert_eq!(res.status(), StatusCode::BAD_REQUEST);
// Allow because correct content type
- let bytes = Cursor::new("{\"key1\": \"value1\", \"key2\": \"value2\"}");
- let mut form = multipart::Form::default();
- form.add_reader_file_with_mime("json", bytes, "", mime::APPLICATION_JSON);
- let response = send_form(&srv, form, "/").await;
- assert_eq!(response.status(), StatusCode::OK);
+ let (body, headers) = crate::test::create_form_data_payload_and_headers(
+ "json",
+ None,
+ Some(mime::APPLICATION_JSON),
+ Bytes::from_static(TEST_JSON.as_bytes()),
+ );
+ let mut req = srv.post("/");
+ *req.headers_mut() = headers;
+ let res = req.send_body(body).await.unwrap();
+ assert_eq!(res.status(), StatusCode::OK);
}
}
diff --git a/actix-multipart/src/form/mod.rs b/actix-multipart/src/form/mod.rs
index 67adfd4b2..451b103fd 100644
--- a/actix-multipart/src/form/mod.rs
+++ b/actix-multipart/src/form/mod.rs
@@ -313,7 +313,8 @@ where
let entry = field_limits
.entry(field.name().to_owned())
.or_insert_with(|| T::limit(field.name()));
- limits.field_limit_remaining = entry.to_owned();
+
+ limits.field_limit_remaining.clone_from(entry);
T::handle_field(&req, field, &mut limits, &mut state).await?;
diff --git a/actix-multipart/src/lib.rs b/actix-multipart/src/lib.rs
index 495bae9c0..d19e951e6 100644
--- a/actix-multipart/src/lib.rs
+++ b/actix-multipart/src/lib.rs
@@ -1,4 +1,39 @@
//! Multipart form support for Actix Web.
+//! # Examples
+//! ```no_run
+//! use actix_web::{post, App, HttpServer, Responder};
+//!
+//! use actix_multipart::form::{json::Json as MPJson, tempfile::TempFile, MultipartForm};
+//! use serde::Deserialize;
+//!
+//! #[derive(Debug, Deserialize)]
+//! struct Metadata {
+//! name: String,
+//! }
+//!
+//! #[derive(Debug, MultipartForm)]
+//! struct UploadForm {
+//! #[multipart(limit = "100MB")]
+//! file: TempFile,
+//! json: MPJson,
+//! }
+//!
+//! #[post("/videos")]
+//! pub async fn post_video(MultipartForm(form): MultipartForm) -> impl Responder {
+//! format!(
+//! "Uploaded file {}, with size: {}",
+//! form.json.name, form.file.size
+//! )
+//! }
+//!
+//! #[actix_web::main]
+//! async fn main() -> std::io::Result<()> {
+//! HttpServer::new(move || App::new().service(post_video))
+//! .bind(("127.0.0.1", 8080))?
+//! .run()
+//! .await
+//! }
+//! ```
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
@@ -13,11 +48,14 @@ extern crate self as actix_multipart;
mod error;
mod extractor;
-mod server;
-
pub mod form;
+mod server;
+pub mod test;
pub use self::{
error::MultipartError,
server::{Field, Multipart},
+ test::{
+ create_form_data_payload_and_headers, create_form_data_payload_and_headers_with_boundary,
+ },
};
diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs
index c08031eba..d0f833318 100644
--- a/actix-multipart/src/server.rs
+++ b/actix-multipart/src/server.rs
@@ -863,13 +863,15 @@ mod tests {
test::TestRequest,
FromRequest,
};
- use bytes::Bytes;
+ use bytes::BufMut as _;
use futures_util::{future::lazy, StreamExt as _};
use tokio::sync::mpsc;
use tokio_stream::wrappers::UnboundedReceiverStream;
use super::*;
+ const BOUNDARY: &str = "abbc761f78ff4d7cb7573b5a23f96ef0";
+
#[actix_rt::test]
async fn test_boundary() {
let headers = HeaderMap::new();
@@ -965,6 +967,26 @@ mod tests {
}
fn create_simple_request_with_header() -> (Bytes, HeaderMap) {
+ let (body, headers) = crate::test::create_form_data_payload_and_headers_with_boundary(
+ BOUNDARY,
+ "file",
+ Some("fn.txt".to_owned()),
+ Some(mime::TEXT_PLAIN_UTF_8),
+ Bytes::from_static(b"data"),
+ );
+
+ let mut buf = BytesMut::with_capacity(body.len() + 14);
+
+ // add junk before form to test pre-boundary data rejection
+ buf.put("testasdadsad\r\n".as_bytes());
+
+ buf.put(body);
+
+ (buf.freeze(), headers)
+ }
+
+ // TODO: use test utility when multi-file support is introduced
+ fn create_double_request_with_header() -> (Bytes, HeaderMap) {
let bytes = Bytes::from(
"testasdadsad\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
@@ -990,7 +1012,7 @@ mod tests {
#[actix_rt::test]
async fn test_multipart_no_end_crlf() {
let (sender, payload) = create_stream();
- let (mut bytes, headers) = create_simple_request_with_header();
+ let (mut bytes, headers) = create_double_request_with_header();
let bytes_stripped = bytes.split_to(bytes.len()); // strip crlf
sender.send(Ok(bytes_stripped)).unwrap();
@@ -1017,7 +1039,7 @@ mod tests {
#[actix_rt::test]
async fn test_multipart() {
let (sender, payload) = create_stream();
- let (bytes, headers) = create_simple_request_with_header();
+ let (bytes, headers) = create_double_request_with_header();
sender.send(Ok(bytes)).unwrap();
@@ -1080,7 +1102,7 @@ mod tests {
#[actix_rt::test]
async fn test_stream() {
- let (bytes, headers) = create_simple_request_with_header();
+ let (bytes, headers) = create_double_request_with_header();
let payload = SlowStream::new(bytes);
let mut multipart = Multipart::new(&headers, payload);
@@ -1319,7 +1341,7 @@ mod tests {
#[actix_rt::test]
async fn test_drop_field_awaken_multipart() {
let (sender, payload) = create_stream();
- let (bytes, headers) = create_simple_request_with_header();
+ let (bytes, headers) = create_double_request_with_header();
sender.send(Ok(bytes)).unwrap();
drop(sender); // eof
diff --git a/actix-multipart/src/test.rs b/actix-multipart/src/test.rs
new file mode 100644
index 000000000..77d918283
--- /dev/null
+++ b/actix-multipart/src/test.rs
@@ -0,0 +1,217 @@
+use actix_web::http::header::{self, HeaderMap};
+use bytes::{BufMut as _, Bytes, BytesMut};
+use mime::Mime;
+use rand::{
+ distributions::{Alphanumeric, DistString as _},
+ thread_rng,
+};
+
+const CRLF: &[u8] = b"\r\n";
+const CRLF_CRLF: &[u8] = b"\r\n\r\n";
+const HYPHENS: &[u8] = b"--";
+const BOUNDARY_PREFIX: &str = "------------------------";
+
+/// Constructs a `multipart/form-data` payload from bytes and metadata.
+///
+/// Returned header map can be extended or merged with existing headers.
+///
+/// Multipart boundary used is a random alphanumeric string.
+///
+/// # Examples
+///
+/// ```
+/// use actix_multipart::test::create_form_data_payload_and_headers;
+/// use actix_web::test::TestRequest;
+/// use bytes::Bytes;
+/// use memchr::memmem::find;
+///
+/// let (body, headers) = create_form_data_payload_and_headers(
+/// "foo",
+/// Some("lorem.txt".to_owned()),
+/// Some(mime::TEXT_PLAIN_UTF_8),
+/// Bytes::from_static(b"Lorem ipsum."),
+/// );
+///
+/// assert!(find(&body, b"foo").is_some());
+/// assert!(find(&body, b"lorem.txt").is_some());
+/// assert!(find(&body, b"text/plain; charset=utf-8").is_some());
+/// assert!(find(&body, b"Lorem ipsum.").is_some());
+///
+/// let req = TestRequest::default();
+///
+/// // merge header map into existing test request and set multipart body
+/// let req = headers
+/// .into_iter()
+/// .fold(req, |req, hdr| req.insert_header(hdr))
+/// .set_payload(body)
+/// .to_http_request();
+///
+/// assert!(
+/// req.headers()
+/// .get("content-type")
+/// .unwrap()
+/// .to_str()
+/// .unwrap()
+/// .starts_with("multipart/form-data; boundary=\"")
+/// );
+/// ```
+pub fn create_form_data_payload_and_headers(
+ name: &str,
+ filename: Option,
+ content_type: Option,
+ file: Bytes,
+) -> (Bytes, HeaderMap) {
+ let boundary = Alphanumeric.sample_string(&mut thread_rng(), 32);
+
+ create_form_data_payload_and_headers_with_boundary(
+ &boundary,
+ name,
+ filename,
+ content_type,
+ file,
+ )
+}
+
+/// Constructs a `multipart/form-data` payload from bytes and metadata with a fixed boundary.
+///
+/// See [`create_form_data_payload_and_headers`] for more details.
+pub fn create_form_data_payload_and_headers_with_boundary(
+ boundary: &str,
+ name: &str,
+ filename: Option,
+ content_type: Option,
+ file: Bytes,
+) -> (Bytes, HeaderMap) {
+ let mut buf = BytesMut::with_capacity(file.len() + 128);
+
+ let boundary_str = [BOUNDARY_PREFIX, boundary].concat();
+ let boundary = boundary_str.as_bytes();
+
+ buf.put(HYPHENS);
+ buf.put(boundary);
+ buf.put(CRLF);
+
+ buf.put(format!("Content-Disposition: form-data; name=\"{name}\"").as_bytes());
+ if let Some(filename) = filename {
+ buf.put(format!("; filename=\"{filename}\"").as_bytes());
+ }
+ buf.put(CRLF);
+
+ if let Some(ct) = content_type {
+ buf.put(format!("Content-Type: {ct}").as_bytes());
+ buf.put(CRLF);
+ }
+
+ buf.put(format!("Content-Length: {}", file.len()).as_bytes());
+ buf.put(CRLF_CRLF);
+
+ buf.put(file);
+ buf.put(CRLF);
+
+ buf.put(HYPHENS);
+ buf.put(boundary);
+ buf.put(HYPHENS);
+ buf.put(CRLF);
+
+ let mut headers = HeaderMap::new();
+ headers.insert(
+ header::CONTENT_TYPE,
+ format!("multipart/form-data; boundary=\"{boundary_str}\"")
+ .parse()
+ .unwrap(),
+ );
+
+ (buf.freeze(), headers)
+}
+
+#[cfg(test)]
+mod tests {
+ use std::convert::Infallible;
+
+ use futures_util::stream;
+
+ use super::*;
+
+ fn find_boundary(headers: &HeaderMap) -> String {
+ headers
+ .get("content-type")
+ .unwrap()
+ .to_str()
+ .unwrap()
+ .parse::()
+ .unwrap()
+ .get_param(mime::BOUNDARY)
+ .unwrap()
+ .as_str()
+ .to_owned()
+ }
+
+ #[test]
+ fn wire_format() {
+ let (pl, headers) = create_form_data_payload_and_headers_with_boundary(
+ "qWeRtYuIoP",
+ "foo",
+ None,
+ None,
+ Bytes::from_static(b"Lorem ipsum dolor\nsit ame."),
+ );
+
+ assert_eq!(
+ find_boundary(&headers),
+ "------------------------qWeRtYuIoP",
+ );
+
+ assert_eq!(
+ std::str::from_utf8(&pl).unwrap(),
+ "--------------------------qWeRtYuIoP\r\n\
+ Content-Disposition: form-data; name=\"foo\"\r\n\
+ Content-Length: 26\r\n\
+ \r\n\
+ Lorem ipsum dolor\n\
+ sit ame.\r\n\
+ --------------------------qWeRtYuIoP--\r\n",
+ );
+
+ let (pl, _headers) = create_form_data_payload_and_headers_with_boundary(
+ "qWeRtYuIoP",
+ "foo",
+ Some("Lorem.txt".to_owned()),
+ Some(mime::TEXT_PLAIN_UTF_8),
+ Bytes::from_static(b"Lorem ipsum dolor\nsit ame."),
+ );
+
+ assert_eq!(
+ std::str::from_utf8(&pl).unwrap(),
+ "--------------------------qWeRtYuIoP\r\n\
+ Content-Disposition: form-data; name=\"foo\"; filename=\"Lorem.txt\"\r\n\
+ Content-Type: text/plain; charset=utf-8\r\n\
+ Content-Length: 26\r\n\
+ \r\n\
+ Lorem ipsum dolor\n\
+ sit ame.\r\n\
+ --------------------------qWeRtYuIoP--\r\n",
+ );
+ }
+
+ /// Test using an external library to prevent the two-wrongs-make-a-right class of errors.
+ #[actix_web::test]
+ async fn ecosystem_compat() {
+ let (pl, headers) = create_form_data_payload_and_headers(
+ "foo",
+ None,
+ None,
+ Bytes::from_static(b"Lorem ipsum dolor\nsit ame."),
+ );
+
+ let boundary = find_boundary(&headers);
+
+ let pl = stream::once(async { Ok::<_, Infallible>(pl) });
+
+ let mut form = multer::Multipart::new(pl, boundary);
+ let field = form.next_field().await.unwrap().unwrap();
+ assert_eq!(field.name().unwrap(), "foo");
+ assert_eq!(field.file_name(), None);
+ assert_eq!(field.content_type(), None);
+ assert!(field.bytes().await.unwrap().starts_with(b"Lorem"));
+ }
+}
diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md
index 31316ff47..6305b45c3 100644
--- a/actix-router/CHANGES.md
+++ b/actix-router/CHANGES.md
@@ -2,6 +2,13 @@
## Unreleased
+## 0.5.3
+
+- Add `unicode` crate feature (on-by-default) to switch between `regex` and `regex-lite` as a trade-off between full unicode support and binary size.
+- Minimum supported Rust version (MSRV) is now 1.72.
+
+## 0.5.2
+
- Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency.
## 0.5.1
diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml
index 8a404dd20..56e4bed2f 100644
--- a/actix-router/Cargo.toml
+++ b/actix-router/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "actix-router"
-version = "0.5.1"
+version = "0.5.3"
authors = [
"Nikolay Kim ",
"Ali MJ Al-Nasrawy ",
@@ -17,12 +17,16 @@ name = "actix_router"
path = "src/lib.rs"
[features]
-default = ["http"]
+default = ["http", "unicode"]
+http = ["dep:http"]
+unicode = ["dep:regex"]
[dependencies]
bytestring = ">=0.1.5, <2"
+cfg-if = "1"
http = { version = "0.2.7", optional = true }
-regex = "1.5"
+regex = { version = "1.5", optional = true }
+regex-lite = "0.1"
serde = "1"
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
@@ -35,6 +39,7 @@ percent-encoding = "2.1"
[[bench]]
name = "router"
harness = false
+required-features = ["unicode"]
[[bench]]
name = "quoter"
diff --git a/actix-router/README.md b/actix-router/README.md
index 15a449c44..12d1b0146 100644
--- a/actix-router/README.md
+++ b/actix-router/README.md
@@ -1,14 +1,18 @@
# `actix-router`
+
+
[](https://crates.io/crates/actix-router)
-[](https://docs.rs/actix-router/0.5.1)
-
+[](https://docs.rs/actix-router/0.5.3)
+

-[](https://deps.rs/crate/actix-router/0.5.1)
+[](https://deps.rs/crate/actix-router/0.5.3)
[](https://crates.io/crates/actix-router)
[](https://discord.gg/NWpN5mmg3x)
+
+
Resource path matching and router.
diff --git a/actix-router/src/de.rs b/actix-router/src/de.rs
index e8c7c658e..ce2dcf8f3 100644
--- a/actix-router/src/de.rs
+++ b/actix-router/src/de.rs
@@ -500,10 +500,10 @@ impl<'de> de::VariantAccess<'de> for UnitVariant {
#[cfg(test)]
mod tests {
- use serde::{de, Deserialize};
+ use serde::Deserialize;
use super::*;
- use crate::{path::Path, router::Router, ResourceDef};
+ use crate::{router::Router, ResourceDef};
#[derive(Deserialize)]
struct MyStruct {
diff --git a/actix-router/src/lib.rs b/actix-router/src/lib.rs
index f10093436..c4d0d2c87 100644
--- a/actix-router/src/lib.rs
+++ b/actix-router/src/lib.rs
@@ -10,6 +10,7 @@ mod de;
mod path;
mod pattern;
mod quoter;
+mod regex_set;
mod resource;
mod resource_path;
mod router;
diff --git a/actix-router/src/path.rs b/actix-router/src/path.rs
index dc4150ddc..9031ab763 100644
--- a/actix-router/src/path.rs
+++ b/actix-router/src/path.rs
@@ -3,7 +3,7 @@ use std::{
ops::{DerefMut, Index},
};
-use serde::de;
+use serde::{de, Deserialize};
use crate::{de::PathDeserializer, Resource, ResourcePath};
@@ -24,8 +24,13 @@ impl Default for PathItem {
/// If resource path contains variable patterns, `Path` stores them.
#[derive(Debug, Clone, Default)]
pub struct Path {
+ /// Full path representation.
path: T,
+
+ /// Number of characters in `path` that have been processed into `segments`.
pub(crate) skip: u16,
+
+ /// List of processed dynamic segments; name->value pairs.
pub(crate) segments: Vec<(Cow<'static, str>, PathItem)>,
}
@@ -83,8 +88,8 @@ impl Path {
/// Set new path.
#[inline]
pub fn set(&mut self, path: T) {
- self.skip = 0;
self.path = path;
+ self.skip = 0;
self.segments.clear();
}
@@ -103,7 +108,7 @@ impl Path {
pub(crate) fn add(&mut self, name: impl Into>, value: PathItem) {
match value {
- PathItem::Static(s) => self.segments.push((name.into(), PathItem::Static(s))),
+ PathItem::Static(seg) => self.segments.push((name.into(), PathItem::Static(seg))),
PathItem::Segment(begin, end) => self.segments.push((
name.into(),
PathItem::Segment(self.skip + begin, self.skip + end),
@@ -149,15 +154,11 @@ impl Path {
None
}
- /// Get matched parameter by name.
+ /// Returns matched parameter by name.
///
/// If keyed parameter is not available empty string is used as default value.
pub fn query(&self, key: &str) -> &str {
- if let Some(s) = self.get(key) {
- s
- } else {
- ""
- }
+ self.get(key).unwrap_or_default()
}
/// Return iterator to items in parameter container.
@@ -168,9 +169,13 @@ impl Path {
}
}
- /// Try to deserialize matching parameters to a specified type `U`
- pub fn load<'de, U: serde::Deserialize<'de>>(&'de self) -> Result {
- de::Deserialize::deserialize(PathDeserializer::new(self))
+ /// Deserializes matching parameters to a specified type `U`.
+ ///
+ /// # Errors
+ ///
+ /// Returns error when dynamic path segments cannot be deserialized into a `U` type.
+ pub fn load<'de, U: Deserialize<'de>>(&'de self) -> Result {
+ Deserialize::deserialize(PathDeserializer::new(self))
}
}
diff --git a/actix-router/src/regex_set.rs b/actix-router/src/regex_set.rs
new file mode 100644
index 000000000..48f38df2c
--- /dev/null
+++ b/actix-router/src/regex_set.rs
@@ -0,0 +1,66 @@
+//! Abstraction over `regex` and `regex-lite` depending on whether we have `unicode` crate feature
+//! enabled.
+
+use cfg_if::cfg_if;
+#[cfg(feature = "unicode")]
+pub(crate) use regex::{escape, Regex};
+#[cfg(not(feature = "unicode"))]
+pub(crate) use regex_lite::{escape, Regex};
+
+#[cfg(feature = "unicode")]
+#[derive(Debug, Clone)]
+pub(crate) struct RegexSet(regex::RegexSet);
+
+#[cfg(not(feature = "unicode"))]
+#[derive(Debug, Clone)]
+pub(crate) struct RegexSet(Vec);
+
+impl RegexSet {
+ /// Create a new regex set.
+ ///
+ /// # Panics
+ ///
+ /// Panics if any path patterns are malformed.
+ pub(crate) fn new(re_set: Vec) -> Self {
+ cfg_if! {
+ if #[cfg(feature = "unicode")] {
+ Self(regex::RegexSet::new(re_set).unwrap())
+ } else {
+ Self(re_set.iter().map(|re| Regex::new(re).unwrap()).collect())
+ }
+ }
+ }
+
+ /// Create a new empty regex set.
+ pub(crate) fn empty() -> Self {
+ cfg_if! {
+ if #[cfg(feature = "unicode")] {
+ Self(regex::RegexSet::empty())
+ } else {
+ Self(Vec::new())
+ }
+ }
+ }
+
+ /// Returns true if regex set matches `path`.
+ pub(crate) fn is_match(&self, path: &str) -> bool {
+ cfg_if! {
+ if #[cfg(feature = "unicode")] {
+ self.0.is_match(path)
+ } else {
+ self.0.iter().any(|re| re.is_match(path))
+ }
+ }
+ }
+
+ /// Returns index within `path` of first match.
+ pub(crate) fn first_match_idx(&self, path: &str) -> Option {
+ cfg_if! {
+ if #[cfg(feature = "unicode")] {
+ self.0.matches(path).into_iter().next()
+ } else {
+ Some(self.0.iter().enumerate().find(|(_, re)| re.is_match(path))?.0)
+ }
+ }
+ }
+}
diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs
index abd132211..3a102945b 100644
--- a/actix-router/src/resource.rs
+++ b/actix-router/src/resource.rs
@@ -5,10 +5,13 @@ use std::{
mem,
};
-use regex::{escape, Regex, RegexSet};
use tracing::error;
-use crate::{path::PathItem, IntoPatterns, Patterns, Resource, ResourcePath};
+use crate::{
+ path::PathItem,
+ regex_set::{escape, Regex, RegexSet},
+ IntoPatterns, Patterns, Resource, ResourcePath,
+};
const MAX_DYNAMIC_SEGMENTS: usize = 16;
@@ -233,7 +236,7 @@ enum PatternSegment {
Var(String),
}
-#[derive(Clone, Debug)]
+#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
enum PatternType {
/// Single constant/literal segment.
@@ -603,7 +606,7 @@ impl ResourceDef {
PatternType::Dynamic(re, _) => Some(re.captures(path)?[1].len()),
PatternType::DynamicSet(re, params) => {
- let idx = re.matches(path).into_iter().next()?;
+ let idx = re.first_match_idx(path)?;
let (ref pattern, _) = params[idx];
Some(pattern.captures(path)?[1].len())
}
@@ -706,7 +709,7 @@ impl ResourceDef {
PatternType::DynamicSet(re, params) => {
let path = path.unprocessed();
- let (pattern, names) = match re.matches(path).into_iter().next() {
+ let (pattern, names) = match re.first_match_idx(path) {
Some(idx) => ¶ms[idx],
_ => return false,
};
@@ -870,7 +873,7 @@ impl ResourceDef {
}
}
- let pattern_re_set = RegexSet::new(re_set).unwrap();
+ let pattern_re_set = RegexSet::new(re_set);
let segments = segments.unwrap_or_default();
(
diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md
index a3ca7fe10..b55a8305c 100644
--- a/actix-test/CHANGES.md
+++ b/actix-test/CHANGES.md
@@ -2,6 +2,14 @@
## Unreleased
+- Add `TestServerConfig::rustls_0_23()` method for Rustls v0.23 support behind new `rustls-0_23` crate feature.
+- Minimum supported Rust version (MSRV) is now 1.72.
+- Various types from `awc`, such as `ClientRequest` and `ClientResponse`, are now re-exported.
+
+## 0.1.3
+
+- Add `TestServerConfig::rustls_0_22()` method for Rustls v0.22 support behind new `rustls-0_22` crate feature.
+
## 0.1.2
- Add `TestServerConfig::rustls_021()` method for Rustls v0.21 support behind new `rustls-0_21` crate feature.
diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml
index 46d65abdc..dddcabec9 100644
--- a/actix-test/Cargo.toml
+++ b/actix-test/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "actix-test"
-version = "0.1.2"
+version = "0.1.3"
authors = [
"Nikolay Kim ",
"Rob Ede ",
@@ -27,19 +27,23 @@ rustls = ["rustls-0_20"]
rustls-0_20 = ["tls-rustls-0_20", "actix-http/rustls-0_20", "awc/rustls-0_20"]
# TLS via Rustls v0.21
rustls-0_21 = ["tls-rustls-0_21", "actix-http/rustls-0_21", "awc/rustls-0_21"]
+# TLS via Rustls v0.22
+rustls-0_22 = ["tls-rustls-0_22", "actix-http/rustls-0_22", "awc/rustls-0_22-webpki-roots"]
+# TLS via Rustls v0.23
+rustls-0_23 = ["tls-rustls-0_23", "actix-http/rustls-0_23", "awc/rustls-0_23-webpki-roots"]
# TLS via OpenSSL
openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
[dependencies]
actix-codec = "0.5"
-actix-http = "3"
+actix-http = "3.7"
actix-http-test = "3"
actix-rt = "2.1"
actix-service = "2"
actix-utils = "3"
-actix-web = { version = "4", default-features = false, features = ["cookies"] }
-awc = { version = "3", default-features = false, features = ["cookies"] }
+actix-web = { version = "4.6", default-features = false, features = ["cookies"] }
+awc = { version = "3.5", default-features = false, features = ["cookies"] }
futures-core = { version = "0.3.17", default-features = false, features = ["std"] }
futures-util = { version = "0.3.17", default-features = false, features = [] }
@@ -50,4 +54,6 @@ serde_urlencoded = "0.7"
tls-openssl = { package = "openssl", version = "0.10.55", optional = true }
tls-rustls-0_20 = { package = "rustls", version = "0.20", optional = true }
tls-rustls-0_21 = { package = "rustls", version = "0.21", optional = true }
+tls-rustls-0_22 = { package = "rustls", version = "0.22", optional = true }
+tls-rustls-0_23 = { package = "rustls", version = "0.23", default-features = false, optional = true }
tokio = { version = "1.24.2", features = ["sync"] }
diff --git a/actix-test/src/lib.rs b/actix-test/src/lib.rs
index e570bb266..1c3d8ff11 100644
--- a/actix-test/src/lib.rs
+++ b/actix-test/src/lib.rs
@@ -52,7 +52,7 @@ use actix_web::{
rt::{self, System},
web, Error,
};
-use awc::{error::PayloadError, Client, ClientRequest, ClientResponse, Connector};
+pub use awc::{error::PayloadError, Client, ClientRequest, ClientResponse, Connector};
use futures_core::Stream;
use tokio::sync::mpsc;
@@ -143,6 +143,10 @@ where
StreamType::Rustls020(_) => true,
#[cfg(feature = "rustls-0_21")]
StreamType::Rustls021(_) => true,
+ #[cfg(feature = "rustls-0_22")]
+ StreamType::Rustls022(_) => true,
+ #[cfg(feature = "rustls-0_23")]
+ StreamType::Rustls023(_) => true,
};
// run server in separate orphaned thread
@@ -327,6 +331,90 @@ where
.rustls_021(config.clone())
}),
},
+ #[cfg(feature = "rustls-0_22")]
+ StreamType::Rustls022(config) => match cfg.tp {
+ HttpVer::Http1 => builder.listen("test", tcp, move || {
+ let app_cfg =
+ AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
+
+ let fac = factory()
+ .into_factory()
+ .map_err(|err| err.into().error_response());
+
+ HttpService::build()
+ .client_request_timeout(timeout)
+ .h1(map_config(fac, move |_| app_cfg.clone()))
+ .rustls_0_22(config.clone())
+ }),
+ HttpVer::Http2 => builder.listen("test", tcp, move || {
+ let app_cfg =
+ AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
+
+ let fac = factory()
+ .into_factory()
+ .map_err(|err| err.into().error_response());
+
+ HttpService::build()
+ .client_request_timeout(timeout)
+ .h2(map_config(fac, move |_| app_cfg.clone()))
+ .rustls_0_22(config.clone())
+ }),
+ HttpVer::Both => builder.listen("test", tcp, move || {
+ let app_cfg =
+ AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
+
+ let fac = factory()
+ .into_factory()
+ .map_err(|err| err.into().error_response());
+
+ HttpService::build()
+ .client_request_timeout(timeout)
+ .finish(map_config(fac, move |_| app_cfg.clone()))
+ .rustls_0_22(config.clone())
+ }),
+ },
+ #[cfg(feature = "rustls-0_23")]
+ StreamType::Rustls023(config) => match cfg.tp {
+ HttpVer::Http1 => builder.listen("test", tcp, move || {
+ let app_cfg =
+ AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
+
+ let fac = factory()
+ .into_factory()
+ .map_err(|err| err.into().error_response());
+
+ HttpService::build()
+ .client_request_timeout(timeout)
+ .h1(map_config(fac, move |_| app_cfg.clone()))
+ .rustls_0_23(config.clone())
+ }),
+ HttpVer::Http2 => builder.listen("test", tcp, move || {
+ let app_cfg =
+ AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
+
+ let fac = factory()
+ .into_factory()
+ .map_err(|err| err.into().error_response());
+
+ HttpService::build()
+ .client_request_timeout(timeout)
+ .h2(map_config(fac, move |_| app_cfg.clone()))
+ .rustls_0_23(config.clone())
+ }),
+ HttpVer::Both => builder.listen("test", tcp, move || {
+ let app_cfg =
+ AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
+
+ let fac = factory()
+ .into_factory()
+ .map_err(|err| err.into().error_response());
+
+ HttpService::build()
+ .client_request_timeout(timeout)
+ .finish(map_config(fac, move |_| app_cfg.clone()))
+ .rustls_0_23(config.clone())
+ }),
+ },
}
.expect("test server could not be created");
@@ -401,6 +489,10 @@ enum StreamType {
Rustls020(tls_rustls_0_20::ServerConfig),
#[cfg(feature = "rustls-0_21")]
Rustls021(tls_rustls_0_21::ServerConfig),
+ #[cfg(feature = "rustls-0_22")]
+ Rustls022(tls_rustls_0_22::ServerConfig),
+ #[cfg(feature = "rustls-0_23")]
+ Rustls023(tls_rustls_0_23::ServerConfig),
}
/// Create default test server config.
@@ -424,7 +516,7 @@ impl Default for TestServerConfig {
}
impl TestServerConfig {
- /// Create default server configuration
+ /// Constructs default server configuration.
pub(crate) fn new() -> TestServerConfig {
TestServerConfig {
tp: HttpVer::Both,
@@ -435,40 +527,70 @@ impl TestServerConfig {
}
}
- /// Accept HTTP/1.1 only.
+ /// Accepts HTTP/1.1 only.
pub fn h1(mut self) -> Self {
self.tp = HttpVer::Http1;
self
}
- /// Accept HTTP/2 only.
+ /// Accepts HTTP/2 only.
pub fn h2(mut self) -> Self {
self.tp = HttpVer::Http2;
self
}
- /// Accept secure connections via OpenSSL.
+ /// Accepts secure connections via OpenSSL.
#[cfg(feature = "openssl")]
pub fn openssl(mut self, acceptor: openssl::ssl::SslAcceptor) -> Self {
self.stream = StreamType::Openssl(acceptor);
self
}
- /// Accept secure connections via Rustls.
+ #[doc(hidden)]
+ #[deprecated(note = "Renamed to `rustls_0_20()`.")]
#[cfg(feature = "rustls-0_20")]
pub fn rustls(mut self, config: tls_rustls_0_20::ServerConfig) -> Self {
self.stream = StreamType::Rustls020(config);
self
}
- /// Accept secure connections via Rustls.
+ /// Accepts secure connections via Rustls v0.20.
+ #[cfg(feature = "rustls-0_20")]
+ pub fn rustls_0_20(mut self, config: tls_rustls_0_20::ServerConfig) -> Self {
+ self.stream = StreamType::Rustls020(config);
+ self
+ }
+
+ #[doc(hidden)]
+ #[deprecated(note = "Renamed to `rustls_0_21()`.")]
#[cfg(feature = "rustls-0_21")]
pub fn rustls_021(mut self, config: tls_rustls_0_21::ServerConfig) -> Self {
self.stream = StreamType::Rustls021(config);
self
}
- /// Set client timeout for first request.
+ /// Accepts secure connections via Rustls v0.21.
+ #[cfg(feature = "rustls-0_21")]
+ pub fn rustls_0_21(mut self, config: tls_rustls_0_21::ServerConfig) -> Self {
+ self.stream = StreamType::Rustls021(config);
+ self
+ }
+
+ /// Accepts secure connections via Rustls v0.22.
+ #[cfg(feature = "rustls-0_22")]
+ pub fn rustls_0_22(mut self, config: tls_rustls_0_22::ServerConfig) -> Self {
+ self.stream = StreamType::Rustls022(config);
+ self
+ }
+
+ /// Accepts secure connections via Rustls v0.23.
+ #[cfg(feature = "rustls-0_23")]
+ pub fn rustls_0_23(mut self, config: tls_rustls_0_23::ServerConfig) -> Self {
+ self.stream = StreamType::Rustls023(config);
+ self
+ }
+
+ /// Sets client timeout for first request.
pub fn client_request_timeout(mut self, dur: Duration) -> Self {
self.client_request_timeout = dur;
self
diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md
index 5c516db56..9a622d8da 100644
--- a/actix-web-actors/CHANGES.md
+++ b/actix-web-actors/CHANGES.md
@@ -2,6 +2,10 @@
## Unreleased
+- Minimum supported Rust version (MSRV) is now 1.72.
+
+## 4.3.0
+
- Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency.
## 4.2.0
diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml
index c6f14554a..114ec5a87 100644
--- a/actix-web-actors/Cargo.toml
+++ b/actix-web-actors/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "actix-web-actors"
-version = "4.2.0"
+version = "4.3.0"
authors = ["Nikolay Kim "]
description = "Actix actors support for Actix Web"
keywords = ["actix", "http", "web", "framework", "async"]
@@ -32,6 +32,6 @@ actix-test = "0.1"
awc = { version = "3", default-features = false }
actix-web = { version = "4", features = ["macros"] }
-env_logger = "0.10"
+env_logger = "0.11"
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
mime = "0.3"
diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md
index b2c30b954..feb3d1b33 100644
--- a/actix-web-actors/README.md
+++ b/actix-web-actors/README.md
@@ -1,17 +1,16 @@
-# actix-web-actors
+# `actix-web-actors`
> Actix actors support for Actix Web.
+
+
[](https://crates.io/crates/actix-web-actors)
-[](https://docs.rs/actix-web-actors/4.2.0)
-
+[](https://docs.rs/actix-web-actors/4.3.0)
+

-[](https://deps.rs/crate/actix-web-actors/4.2.0)
+[](https://deps.rs/crate/actix-web-actors/4.3.0)
[](https://crates.io/crates/actix-web-actors)
[](https://discord.gg/NWpN5mmg3x)
-## Documentation & Resources
-
-- [API Documentation](https://docs.rs/actix-web-actors)
-- Minimum Supported Rust Version (MSRV): 1.68
+
diff --git a/actix-web-actors/src/context.rs b/actix-web-actors/src/context.rs
index be8fd387c..23e336459 100644
--- a/actix-web-actors/src/context.rs
+++ b/actix-web-actors/src/context.rs
@@ -248,13 +248,11 @@ where
mod tests {
use std::time::Duration;
- use actix::Actor;
use actix_web::{
http::StatusCode,
test::{call_service, init_service, read_body, TestRequest},
web, App, HttpResponse,
};
- use bytes::Bytes;
use super::*;
diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs
index 04dbf5e17..1fb903225 100644
--- a/actix-web-actors/src/ws.rs
+++ b/actix-web-actors/src/ws.rs
@@ -817,10 +817,7 @@ where
#[cfg(test)]
mod tests {
- use actix_web::{
- http::{header, Method},
- test::TestRequest,
- };
+ use actix_web::test::TestRequest;
use super::*;
diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md
index 00e36b037..875c4021e 100644
--- a/actix-web-codegen/CHANGES.md
+++ b/actix-web-codegen/CHANGES.md
@@ -2,6 +2,9 @@
## Unreleased
+- Prevent inclusion of default `actix-router` features.
+- Minimum supported Rust version (MSRV) is now 1.72.
+
## 4.2.2
- Fix regression when declaring `wrap` attribute using an expression.
diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml
index 7039ea7df..31c470694 100644
--- a/actix-web-codegen/Cargo.toml
+++ b/actix-web-codegen/Cargo.toml
@@ -2,20 +2,21 @@
name = "actix-web-codegen"
version = "4.2.2"
description = "Routing and runtime macros for Actix Web"
-homepage = "https://actix.rs"
-repository = "https://github.com/actix/actix-web"
authors = [
"Nikolay Kim ",
"Rob Ede ",
]
-license = "MIT OR Apache-2.0"
-edition = "2021"
+homepage.workspace = true
+repository.workspace = true
+license.workspace = true
+edition.workspace = true
+rust-version.workspace = true
[lib]
proc-macro = true
[dependencies]
-actix-router = "0.5"
+actix-router = { version = "0.5", default-features = false }
proc-macro2 = "1"
quote = "1"
syn = { version = "2", features = ["full", "extra-traits"] }
diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md
index e9a1f9c7e..9229f8f16 100644
--- a/actix-web-codegen/README.md
+++ b/actix-web-codegen/README.md
@@ -1,20 +1,19 @@
-# actix-web-codegen
+# `actix-web-codegen`
> Routing and runtime macros for Actix Web.
+
+
[](https://crates.io/crates/actix-web-codegen)
[](https://docs.rs/actix-web-codegen/4.2.2)
-
+

[](https://deps.rs/crate/actix-web-codegen/4.2.2)
[](https://crates.io/crates/actix-web-codegen)
[](https://discord.gg/NWpN5mmg3x)
-## Documentation & Resources
-
-- [API Documentation](https://docs.rs/actix-web-codegen)
-- Minimum Supported Rust Version (MSRV): 1.68
+
## Compile Testing
diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs
index 8e1f58a4c..88f77548b 100644
--- a/actix-web-codegen/tests/trybuild.rs
+++ b/actix-web-codegen/tests/trybuild.rs
@@ -1,4 +1,4 @@
-#[rustversion::stable(1.68)] // MSRV
+#[rustversion::stable(1.72)] // MSRV
#[test]
fn compile_macros() {
let t = trybuild::TestCases::new();
diff --git a/actix-web-codegen/tests/trybuild/route-custom-lowercase.stderr b/actix-web-codegen/tests/trybuild/route-custom-lowercase.stderr
index 88198a55d..c2a51d005 100644
--- a/actix-web-codegen/tests/trybuild/route-custom-lowercase.stderr
+++ b/actix-web-codegen/tests/trybuild/route-custom-lowercase.stderr
@@ -13,17 +13,20 @@ error[E0277]: the trait bound `fn() -> impl std::future::Future