From 55c15f5bbf96518738bb440cea84c25551296bec Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sun, 27 Aug 2023 00:07:11 +0100 Subject: [PATCH] minimum viable rustls v0.21 support (#3112) --- .github/workflows/ci.yml | 1 + Cargo.toml | 5 ++ actix-http/CHANGES.md | 2 + actix-http/Cargo.toml | 29 ++++++--- actix-http/examples/tls_rustls.rs | 73 +++++++++++++++++++++ actix-http/examples/ws.rs | 6 +- actix-http/src/h1/service.rs | 69 ++++++++++++++++++-- actix-http/src/h2/service.rs | 57 +++++++++++++++- actix-http/src/lib.rs | 2 +- actix-http/src/service.rs | 104 ++++++++++++++++++++++++++++-- actix-http/tests/test_rustls.rs | 45 +++++++------ actix-web/Cargo.toml | 3 +- awc/Cargo.toml | 2 +- 13 files changed, 350 insertions(+), 48 deletions(-) create mode 100644 actix-http/examples/tls_rustls.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f12202017..fdd6f0f9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,7 @@ jobs: if: matrix.version.name != 'stable' run: | cargo update -p=clap --precise=4.3.24 + cargo update -p=clap_lex --precise=0.5.0 - name: check minimal run: cargo ci-check-min diff --git a/Cargo.toml b/Cargo.toml index 65e3c6ae8..58fd96935 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,11 @@ members = [ "awc", ] +[workspace.package] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.68" + [profile.dev] # Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much. debug = 0 diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 0dedd2c74..7078adb7e 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -4,6 +4,8 @@ ### Added +- Add `rustls-0_20` crate feature. +- Add `{H1Service, H2Service, HttpService}::rustls_021()` and `HttpService::rustls_021_with_config()` service constructors. - Add `body::to_body_limit()` function. - Add `body::BodyLimitExceeded` error type. diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 6909b785f..61fba4bce 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -15,8 +15,9 @@ categories = [ "web-programming::http-server", "web-programming::websocket", ] -license = "MIT OR Apache-2.0" -edition = "2021" +license.workspace = true +edition.workspace = true +rust-version.workspace = true [package.metadata.docs.rs] # features that docs.rs will build with @@ -43,15 +44,21 @@ ws = [ # TLS via OpenSSL openssl = ["actix-tls/accept", "actix-tls/openssl"] -# TLS via Rustls -rustls = ["actix-tls/accept", "actix-tls/rustls"] +# TLS via Rustls v0.20 +rustls = ["rustls-0_20"] + +# TLS via Rustls v0.20 +rustls-0_20 = ["actix-tls/accept", "actix-tls/rustls-0_20"] + +# TLS via Rustls v0.21 +rustls-0_21 = ["actix-tls/accept", "actix-tls/rustls-0_21"] # Compression codecs compress-brotli = ["__compress", "brotli"] compress-gzip = ["__compress", "flate2"] compress-zstd = ["__compress", "zstd"] -# Internal (PRIVATE!) features used to aid testing and cheking feature status. +# Internal (PRIVATE!) features used to aid testing and checking feature status. # Don't rely on these whatsoever. They are semver-exempt and may disappear at anytime. __compress = [] @@ -91,7 +98,7 @@ rand = { version = "0.8", optional = true } sha1 = { version = "0.10", optional = true } # openssl/rustls -actix-tls = { version = "3", default-features = false, optional = true } +actix-tls = { version = "3.1", default-features = false, optional = true } # compress-* brotli = { version = "3.3.3", optional = true } @@ -101,7 +108,7 @@ zstd = { version = "0.12", optional = true } [dev-dependencies] actix-http-test = { version = "3", features = ["openssl"] } actix-server = "2" -actix-tls = { version = "3", features = ["openssl"] } +actix-tls = { version = "3.1", features = ["openssl"] } actix-web = "4" async-stream = "0.3" @@ -118,12 +125,16 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" static_assertions = "1" tls-openssl = { package = "openssl", version = "0.10.55" } -tls-rustls = { package = "rustls", version = "0.20" } +tls-rustls_021 = { package = "rustls", version = "0.21" } tokio = { version = "1.24.2", features = ["net", "rt", "macros"] } [[example]] name = "ws" -required-features = ["ws", "rustls"] +required-features = ["ws", "rustls-0_21"] + +[[example]] +name = "tls_rustls" +required-features = ["http2", "rustls-0_21"] [[bench]] name = "response-body-compression" diff --git a/actix-http/examples/tls_rustls.rs b/actix-http/examples/tls_rustls.rs new file mode 100644 index 000000000..fbb55c6a4 --- /dev/null +++ b/actix-http/examples/tls_rustls.rs @@ -0,0 +1,73 @@ +//! Demonstrates TLS configuration (via Rustls) for HTTP/1.1 and HTTP/2 connections. +//! +//! Test using cURL: +//! +//! ```console +//! $ curl --insecure https://127.0.0.1:8443 +//! Hello World! +//! Protocol: HTTP/2.0 +//! +//! $ curl --insecure --http1.1 https://127.0.0.1:8443 +//! Hello World! +//! Protocol: HTTP/1.1 +//! ``` + +extern crate tls_rustls_021 as rustls; + +use std::io; + +use actix_http::{Error, HttpService, Request, Response}; +use actix_utils::future::ok; + +#[actix_rt::main] +async fn main() -> io::Result<()> { + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + tracing::info!("starting HTTP server at https://127.0.0.1:8443"); + + actix_server::Server::build() + .bind("echo", ("127.0.0.1", 8443), || { + HttpService::build() + .finish(|req: Request| { + let body = format!( + "Hello World!\n\ + Protocol: {:?}", + req.head().version + ); + ok::<_, Error>(Response::ok().set_body(body)) + }) + .rustls_021(rustls_config()) + })? + .run() + .await +} + +fn rustls_config() -> rustls::ServerConfig { + 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 cert_file = &mut io::BufReader::new(cert_file.as_bytes()); + let key_file = &mut io::BufReader::new(key_file.as_bytes()); + + let cert_chain = rustls_pemfile::certs(cert_file) + .unwrap() + .into_iter() + .map(rustls::Certificate) + .collect(); + let mut keys = rustls_pemfile::pkcs8_private_keys(key_file).unwrap(); + + let mut config = rustls::ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(cert_chain, rustls::PrivateKey(keys.remove(0))) + .unwrap(); + + const H1_ALPN: &[u8] = b"http/1.1"; + const H2_ALPN: &[u8] = b"h2"; + + config.alpn_protocols.push(H2_ALPN.to_vec()); + config.alpn_protocols.push(H1_ALPN.to_vec()); + + config +} diff --git a/actix-http/examples/ws.rs b/actix-http/examples/ws.rs index 6af6d5095..241175ae2 100644 --- a/actix-http/examples/ws.rs +++ b/actix-http/examples/ws.rs @@ -1,7 +1,7 @@ //! Sets up a WebSocket server over TCP and TLS. //! Sends a heartbeat message every 4 seconds but does not respond to any incoming frames. -extern crate tls_rustls as rustls; +extern crate tls_rustls_021 as rustls; use std::{ io, @@ -28,7 +28,9 @@ async fn main() -> io::Result<()> { HttpService::build().h1(handler).tcp() })? .bind("tls", ("127.0.0.1", 8443), || { - HttpService::build().finish(handler).rustls(tls_config()) + HttpService::build() + .finish(handler) + .rustls_021(tls_config()) })? .run() .await diff --git a/actix-http/src/h1/service.rs b/actix-http/src/h1/service.rs index fbda7138e..3b27e3db5 100644 --- a/actix-http/src/h1/service.rs +++ b/actix-http/src/h1/service.rs @@ -152,13 +152,13 @@ mod openssl { } } -#[cfg(feature = "rustls")] -mod rustls { +#[cfg(feature = "rustls-0_20")] +mod rustls_020 { 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, }; @@ -188,7 +188,7 @@ mod rustls { 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, @@ -213,6 +213,67 @@ mod rustls { } } +#[cfg(feature = "rustls-0_21")] +mod rustls_021 { + use std::io; + + use actix_service::ServiceFactoryExt as _; + use actix_tls::accept::{ + rustls_0_21::{reexports::ServerConfig, Acceptor, TlsStream}, + TlsError, + }; + + use super::*; + + impl H1Service, S, B, X, U> + where + S: ServiceFactory, + S::Future: 'static, + S::Error: Into>, + S::InitError: fmt::Debug, + S::Response: Into>, + + B: MessageBody, + + X: ServiceFactory, + X::Future: 'static, + X::Error: Into>, + X::InitError: fmt::Debug, + + U: ServiceFactory< + (Request, Framed, Codec>), + Config = (), + Response = (), + >, + U::Future: 'static, + U::Error: fmt::Display + Into>, + U::InitError: fmt::Debug, + { + /// Create Rustls v0.21 based service. + pub fn rustls_021( + self, + config: ServerConfig, + ) -> impl ServiceFactory< + TcpStream, + Config = (), + Response = (), + Error = TlsError, + InitError = (), + > { + Acceptor::new(config) + .map_init_err(|_| { + unreachable!("TLS acceptor service factory does not error on init") + }) + .map_err(TlsError::into_service_error) + .map(|io: TlsStream| { + let peer_addr = io.get_ref().0.peer_addr().ok(); + (io, peer_addr) + }) + .and_then(self.map_err(TlsError::Service)) + } + } +} + impl H1Service where S: ServiceFactory, diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index 3f742135a..0ae7ea682 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -140,8 +140,8 @@ mod openssl { } } -#[cfg(feature = "rustls")] -mod rustls { +#[cfg(feature = "rustls-0_20")] +mod rustls_020 { use std::io; use actix_service::ServiceFactoryExt as _; @@ -162,7 +162,7 @@ mod rustls { B: MessageBody + 'static, { - /// Create Rustls based service. + /// Create Rustls v0.20 based service. pub fn rustls( self, mut config: ServerConfig, @@ -191,6 +191,57 @@ mod rustls { } } +#[cfg(feature = "rustls-0_21")] +mod rustls_021 { + use std::io; + + use actix_service::ServiceFactoryExt as _; + use actix_tls::accept::{ + rustls_0_21::{reexports::ServerConfig, Acceptor, TlsStream}, + TlsError, + }; + + use super::*; + + impl H2Service, S, B> + where + S: ServiceFactory, + S::Future: 'static, + S::Error: Into> + 'static, + S::Response: Into> + 'static, + >::Future: 'static, + + B: MessageBody + 'static, + { + /// Create Rustls v0.21 based service. + pub fn rustls_021( + self, + mut config: ServerConfig, + ) -> impl ServiceFactory< + TcpStream, + Config = (), + Response = (), + Error = TlsError, + InitError = S::InitError, + > { + let mut protos = vec![b"h2".to_vec()]; + protos.extend_from_slice(&config.alpn_protocols); + config.alpn_protocols = protos; + + Acceptor::new(config) + .map_init_err(|_| { + unreachable!("TLS acceptor service factory does not error on init") + }) + .map_err(TlsError::into_service_error) + .map(|io: TlsStream| { + let peer_addr = io.get_ref().0.peer_addr().ok(); + (io, peer_addr) + }) + .and_then(self.map_err(TlsError::Service)) + } + } +} + impl ServiceFactory<(T, Option)> for H2Service where T: AsyncRead + AsyncWrite + Unpin + 'static, diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index 8b755f2f4..382295fbc 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -58,7 +58,7 @@ pub mod ws; #[allow(deprecated)] pub use self::payload::PayloadStream; -#[cfg(any(feature = "openssl", feature = "rustls"))] +#[cfg(any(feature = "openssl", feature = "rustls-0_20", feature = "rustls-0_21"))] pub use self::service::TlsAcceptorConfig; pub use self::{ builder::HttpServiceBuilder, diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index e118d8361..bb0fda8c4 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -241,13 +241,13 @@ where } /// Configuration options used when accepting TLS connection. -#[cfg(any(feature = "openssl", feature = "rustls"))] +#[cfg(any(feature = "openssl", feature = "rustls-0_20", feature = "rustls-0_21"))] #[derive(Debug, Default)] pub struct TlsAcceptorConfig { pub(crate) handshake_timeout: Option, } -#[cfg(any(feature = "openssl", feature = "rustls"))] +#[cfg(any(feature = "openssl", feature = "rustls", feature = "rustls-0_21"))] impl TlsAcceptorConfig { /// Set TLS handshake timeout duration. pub fn handshake_timeout(self, dur: std::time::Duration) -> Self { @@ -352,8 +352,8 @@ mod openssl { } } -#[cfg(feature = "rustls")] -mod rustls { +#[cfg(feature = "rustls-0_20")] +mod rustls_020 { use std::io; use actix_service::ServiceFactoryExt as _; @@ -448,6 +448,102 @@ mod rustls { } } +#[cfg(feature = "rustls-0_21")] +mod rustls_021 { + use std::io; + + use actix_service::ServiceFactoryExt as _; + use actix_tls::accept::{ + rustls_0_21::{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 based service. + pub fn rustls_021( + self, + config: ServerConfig, + ) -> impl ServiceFactory< + TcpStream, + Config = (), + Response = (), + Error = TlsError, + InitError = (), + > { + self.rustls_021_with_config(config, TlsAcceptorConfig::default()) + } + + /// Create Rustls based service with custom TLS acceptor configuration. + pub fn rustls_021_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/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index 3d9a39cbd..c94e579e5 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -1,7 +1,6 @@ -#![cfg(feature = "rustls")] -#![allow(clippy::uninlined_format_args)] +#![cfg(feature = "rustls-0_21")] -extern crate tls_rustls as rustls; +extern crate tls_rustls_021 as rustls; use std::{ convert::Infallible, @@ -21,7 +20,7 @@ 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::webpki_roots_cert_store; +use actix_tls::connect::rustls_0_21::webpki_roots_cert_store; use actix_utils::future::{err, ok, poll_fn}; use bytes::{Bytes, BytesMut}; use derive_more::{Display, Error}; @@ -110,7 +109,7 @@ async fn h1() -> io::Result<()> { let srv = test_server(move || { HttpService::build() .h1(|_| ok::<_, Error>(Response::ok())) - .rustls(tls_config()) + .rustls_021(tls_config()) }) .await; @@ -124,7 +123,7 @@ async fn h2() -> io::Result<()> { let srv = test_server(move || { HttpService::build() .h2(|_| ok::<_, Error>(Response::ok())) - .rustls(tls_config()) + .rustls_021(tls_config()) }) .await; @@ -142,7 +141,7 @@ async fn h1_1() -> io::Result<()> { assert_eq!(req.version(), Version::HTTP_11); ok::<_, Error>(Response::ok()) }) - .rustls(tls_config()) + .rustls_021(tls_config()) }) .await; @@ -160,7 +159,7 @@ async fn h2_1() -> io::Result<()> { assert_eq!(req.version(), Version::HTTP_2); ok::<_, Error>(Response::ok()) }) - .rustls_with_config( + .rustls_021_with_config( tls_config(), TlsAcceptorConfig::default().handshake_timeout(Duration::from_secs(5)), ) @@ -181,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(tls_config()) + .rustls_021(tls_config()) }) .await; @@ -207,7 +206,7 @@ async fn h2_content_length() { ]; ok::<_, Infallible>(Response::new(statuses[indx])) }) - .rustls(tls_config()) + .rustls_021(tls_config()) }) .await; @@ -279,7 +278,7 @@ async fn h2_headers() { } ok::<_, Infallible>(config.body(data.clone())) }) - .rustls(tls_config()) + .rustls_021(tls_config()) }) .await; @@ -318,7 +317,7 @@ async fn h2_body2() { let mut srv = test_server(move || { HttpService::build() .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) - .rustls(tls_config()) + .rustls_021(tls_config()) }) .await; @@ -335,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(tls_config()) + .rustls_021(tls_config()) }) .await; @@ -361,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(tls_config()) + .rustls_021(tls_config()) }) .await; @@ -386,7 +385,7 @@ async fn h2_head_binary2() { let srv = test_server(move || { HttpService::build() .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) - .rustls(tls_config()) + .rustls_021(tls_config()) }) .await; @@ -412,7 +411,7 @@ async fn h2_body_length() { Response::ok().set_body(SizedStream::new(STR.len() as u64, body)), ) }) - .rustls(tls_config()) + .rustls_021(tls_config()) }) .await; @@ -436,7 +435,7 @@ async fn h2_body_chunked_explicit() { .body(BodyStream::new(body)), ) }) - .rustls(tls_config()) + .rustls_021(tls_config()) }) .await; @@ -465,7 +464,7 @@ async fn h2_response_http_error_handling() { ) })) })) - .rustls(tls_config()) + .rustls_021(tls_config()) }) .await; @@ -495,7 +494,7 @@ async fn h2_service_error() { let mut srv = test_server(move || { HttpService::build() .h2(|_| err::, _>(BadRequest)) - .rustls(tls_config()) + .rustls_021(tls_config()) }) .await; @@ -512,7 +511,7 @@ async fn h1_service_error() { let mut srv = test_server(move || { HttpService::build() .h1(|_| err::, _>(BadRequest)) - .rustls(tls_config()) + .rustls_021(tls_config()) }) .await; @@ -535,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(config) + .rustls_021(config) }) .await; @@ -557,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(config) + .rustls_021(config) }) .await; @@ -583,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(config) + .rustls_021(config) }) .await; diff --git a/actix-web/Cargo.toml b/actix-web/Cargo.toml index 4322fb871..7dbaa2a29 100644 --- a/actix-web/Cargo.toml +++ b/actix-web/Cargo.toml @@ -16,7 +16,8 @@ categories = [ homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" license = "MIT OR Apache-2.0" -edition = "2021" +edition.workspace = true +rust-version.workspace = true [package.metadata.docs.rs] # features that docs.rs will build with diff --git a/awc/Cargo.toml b/awc/Cargo.toml index daec84ab9..2a09a52c4 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -45,7 +45,7 @@ cookies = ["cookie"] # trust-dns as dns resolver trust-dns = ["trust-dns-resolver"] -# Internal (PRIVATE!) features used to aid testing and cheking feature status. +# Internal (PRIVATE!) features used to aid testing and checking feature status. # Don't rely on these whatsoever. They may disappear at anytime. __compress = []