From 31b1dc5aa844cad3e0e1bd8323801f8c750d25ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Fernandes?= <38193239+0rangeFox@users.noreply.github.com> Date: Sat, 3 Aug 2024 09:59:13 +0100 Subject: [PATCH] feature(settings): add TLS (#380) * Complete the missing TLS feature. * Make the `cfg` attributes more clear. * Format the project issued by command `cargo +nightly fmt`. * Small changes on cargo file. * Update CHANGES.md. * Add documentation for `Tls::get_ssl_acceptor_builder()` and remove unused imports. * Add the `cfg` macro with required feature on `TLS` tests. * Update actix-settings/src/settings/tls.rs Co-authored-by: Rob Ede * Copy the workflow steps related to OpenSSL for windows from [actix-web workflow](https://github.com/actix/actix-web/blob/a7375b687658790122c72e27e476affdd7bc1cb6/.github/workflows/ci.yml#L38-L45). * ci: install openssl 1.1.1 * Replaced `apply_settings` with `try_apply_settings` for a better error handling. * Updated the example. * Add `OpenSSL` error. * Restrict `OpenSSL` error only for `tls` feature. * Rename feature `tls` to `openssl`. * Add doc feature `broken_intra_doc_links` to `get_ssl_acceptor_builder` function. --------- Co-authored-by: Rob Ede --- .github/workflows/ci-post-merge.yml | 9 +++++ .github/workflows/ci.yml | 9 +++++ actix-settings/CHANGES.md | 7 ++++ actix-settings/Cargo.toml | 5 ++- actix-settings/README.md | 4 -- actix-settings/examples/actix.rs | 2 +- actix-settings/src/error.rs | 17 ++++++++ actix-settings/src/lib.rs | 62 ++++++++++++++++++++--------- actix-settings/src/settings/mod.rs | 6 ++- actix-settings/src/settings/tls.rs | 45 ++++++++++++++++++++- 10 files changed, 140 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci-post-merge.yml b/.github/workflows/ci-post-merge.yml index 917990059..0ce620d4d 100644 --- a/.github/workflows/ci-post-merge.yml +++ b/.github/workflows/ci-post-merge.yml @@ -71,6 +71,15 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install OpenSSL + if: matrix.target.os == 'windows-latest' + shell: bash + run: | + set -e + choco install openssl --version=1.1.1.2100 -y --no-progress + echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' >> $GITHUB_ENV + echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV + - name: Install Rust (nightly) uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c883d4497..8106f0812 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,6 +92,15 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install OpenSSL + if: matrix.target.os == 'windows-latest' + shell: bash + run: | + set -e + choco install openssl --version=1.1.1.2100 -y --no-progress + echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' >> $GITHUB_ENV + echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV + - name: Install Rust (${{ matrix.version.name }}) uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 with: diff --git a/actix-settings/CHANGES.md b/actix-settings/CHANGES.md index c3a34362e..3093de83c 100644 --- a/actix-settings/CHANGES.md +++ b/actix-settings/CHANGES.md @@ -2,7 +2,14 @@ ## Unreleased +- Add new feature named `openssl` for TLS settings using `OpenSSL` dependency. [#380] +- Add new function `settings::tls::Tls::get_ssl_acceptor_builder()` to build `openssl::ssl::SslAcceptorBuilder`. [#380] +- Implement TLS logic for `ApplySettings::try_apply_settings()`. [#380] +- Add `openssl` dependency; - Minimum supported Rust version (MSRV) is now 1.75. +- `ApplySettings::apply_settings()` is deprecated; `ApplySettings::try_apply_settings()` should be preferred. [#380] + +[#380]: https://github.com/actix/actix-extras/pull/380 ## 0.7.1 diff --git a/actix-settings/Cargo.toml b/actix-settings/Cargo.toml index 6920d698a..09f48f2c9 100644 --- a/actix-settings/Cargo.toml +++ b/actix-settings/Cargo.toml @@ -14,7 +14,9 @@ rust-version.workspace = true [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] -all-features = true + +[features] +openssl = ["dep:openssl", "actix-web/openssl"] [dependencies] actix-http = "3" @@ -22,6 +24,7 @@ actix-service = "2" actix-web = { version = "4", default-features = false } derive_more = "0.99.7" once_cell = "1.13" +openssl = { version = "0.10", features = ["v110"], optional = true } regex = "1.5" serde = { version = "1", features = ["derive"] } toml = "0.8" diff --git a/actix-settings/README.md b/actix-settings/README.md index 659871edc..58393babd 100644 --- a/actix-settings/README.md +++ b/actix-settings/README.md @@ -23,10 +23,6 @@ There is a way to extend the available settings. This can be used to combine the Have a look at [the usage example][usage] to see how. -## WIP - -Configuration options for TLS set up are not yet implemented. - ## Special Thanks This crate was made possible by support from Accept B.V and [@jjpe]. diff --git a/actix-settings/examples/actix.rs b/actix-settings/examples/actix.rs index 51e96df33..d9140fcfe 100644 --- a/actix-settings/examples/actix.rs +++ b/actix-settings/examples/actix.rs @@ -57,7 +57,7 @@ async fn main() -> std::io::Result<()> { } }) // apply the `Settings` to Actix Web's `HttpServer` - .apply_settings(&settings) + .try_apply_settings(&settings)? .run() .await } diff --git a/actix-settings/src/error.rs b/actix-settings/src/error.rs index 7803dca43..c60fe08bd 100644 --- a/actix-settings/src/error.rs +++ b/actix-settings/src/error.rs @@ -1,6 +1,8 @@ use std::{env::VarError, io, num::ParseIntError, path::PathBuf, str::ParseBoolError}; use derive_more::{Display, Error}; +#[cfg(feature = "openssl")] +use openssl::error::ErrorStack as OpenSSLError; use toml::de::Error as TomlError; /// Errors that can be returned from methods in this crate. @@ -29,6 +31,11 @@ pub enum Error { #[display(fmt = "")] IoError(io::Error), + /// OpenSSL Error. + #[cfg(feature = "openssl")] + #[display(fmt = "OpenSSL error: {_0}")] + OpenSSLError(OpenSSLError), + /// Value is not a boolean. #[display(fmt = "Failed to parse boolean: {_0}")] ParseBoolError(ParseBoolError), @@ -64,6 +71,13 @@ impl From for Error { } } +#[cfg(feature = "openssl")] +impl From for Error { + fn from(err: OpenSSLError) -> Self { + Self::OpenSSLError(err) + } +} + impl From for Error { fn from(err: ParseBoolError) -> Self { Self::ParseBoolError(err) @@ -101,6 +115,9 @@ impl From for io::Error { Error::IoError(io_error) => io_error, + #[cfg(feature = "openssl")] + Error::OpenSSLError(ossl_error) => io::Error::new(io::ErrorKind::Other, ossl_error), + Error::ParseBoolError(_) => { io::Error::new(io::ErrorKind::InvalidInput, err.to_string()) } diff --git a/actix-settings/src/lib.rs b/actix-settings/src/lib.rs index d8dd9498a..a527dfe51 100644 --- a/actix-settings/src/lib.rs +++ b/actix-settings/src/lib.rs @@ -54,7 +54,7 @@ //! } //! }) //! // apply the `Settings` to Actix Web's `HttpServer` -//! .apply_settings(&settings) +//! .try_apply_settings(&settings)? //! .run() //! .await //! } @@ -89,12 +89,14 @@ mod error; mod parse; mod settings; +#[cfg(feature = "openssl")] +pub use self::settings::Tls; pub use self::{ error::Error, parse::Parse, settings::{ ActixSettings, Address, Backlog, KeepAlive, MaxConnectionRate, MaxConnections, Mode, - NumWorkers, Timeout, Tls, + NumWorkers, Timeout, }, }; @@ -239,10 +241,13 @@ where } /// Extension trait for applying parsed settings to the server object. -pub trait ApplySettings { +pub trait ApplySettings: Sized { /// Apply some settings object value to `self`. - #[must_use] + #[deprecated = "Prefer `try_apply_settings`."] fn apply_settings(self, settings: &S) -> Self; + + /// Apply some settings object value to `self`. + fn try_apply_settings(self, settings: &S) -> AsResult; } impl ApplySettings for HttpServer @@ -256,17 +261,27 @@ where S::Future: 'static, B: MessageBody + 'static, { - fn apply_settings(mut self, settings: &ActixSettings) -> Self { - if settings.tls.enabled { - // for Address { host, port } in &settings.actix.hosts { - // self = self.bind(format!("{}:{}", host, port)) - // .unwrap(/*TODO*/); - // } - unimplemented!("[ApplySettings] TLS support has not been implemented yet."); - } else { - for Address { host, port } in &settings.hosts { - self = self.bind(format!("{host}:{port}")) - .unwrap(/*TODO*/); + fn apply_settings(self, settings: &ActixSettings) -> Self { + self.try_apply_settings(settings).unwrap() + } + + fn try_apply_settings(mut self, settings: &ActixSettings) -> AsResult { + for Address { host, port } in &settings.hosts { + #[cfg(feature = "openssl")] + { + if settings.tls.enabled { + self = self.bind_openssl( + format!("{}:{}", host, port), + settings.tls.get_ssl_acceptor_builder()?, + )?; + } else { + self = self.bind(format!("{host}:{port}"))?; + } + } + + #[cfg(not(feature = "openssl"))] + { + self = self.bind(format!("{host}:{port}"))?; } } @@ -319,7 +334,7 @@ where Timeout::Seconds(n) => self.shutdown_timeout(n as u64), }; - self + Ok(self) } } @@ -336,7 +351,11 @@ where A: de::DeserializeOwned, { fn apply_settings(self, settings: &BasicSettings) -> Self { - self.apply_settings(&settings.actix) + self.try_apply_settings(&settings.actix).unwrap() + } + + fn try_apply_settings(self, settings: &BasicSettings) -> AsResult { + self.try_apply_settings(&settings.actix) } } @@ -349,7 +368,8 @@ mod tests { #[test] fn apply_settings() { let settings = Settings::parse_toml("Server.toml").unwrap(); - let _ = HttpServer::new(App::new).apply_settings(&settings); + let server = HttpServer::new(App::new).try_apply_settings(&settings); + assert!(server.is_ok()); } #[test] @@ -662,6 +682,7 @@ mod tests { assert_eq!(settings.actix.shutdown_timeout, Timeout::Seconds(42)); } + #[cfg(feature = "openssl")] #[test] fn override_field_tls_enabled() { let mut settings = Settings::from_default_template(); @@ -670,6 +691,7 @@ mod tests { assert!(settings.actix.tls.enabled); } + #[cfg(feature = "openssl")] #[test] fn override_field_with_env_var_tls_enabled() { let mut settings = Settings::from_default_template(); @@ -683,6 +705,7 @@ mod tests { assert!(settings.actix.tls.enabled); } + #[cfg(feature = "openssl")] #[test] fn override_field_tls_certificate() { let mut settings = Settings::from_default_template(); @@ -701,6 +724,7 @@ mod tests { ); } + #[cfg(feature = "openssl")] #[test] fn override_field_with_env_var_tls_certificate() { let mut settings = Settings::from_default_template(); @@ -723,6 +747,7 @@ mod tests { ); } + #[cfg(feature = "openssl")] #[test] fn override_field_tls_private_key() { let mut settings = Settings::from_default_template(); @@ -741,6 +766,7 @@ mod tests { ); } + #[cfg(feature = "openssl")] #[test] fn override_field_with_env_var_tls_private_key() { let mut settings = Settings::from_default_template(); diff --git a/actix-settings/src/settings/mod.rs b/actix-settings/src/settings/mod.rs index 514b01fa3..acd316296 100644 --- a/actix-settings/src/settings/mod.rs +++ b/actix-settings/src/settings/mod.rs @@ -8,12 +8,15 @@ mod max_connections; mod mode; mod num_workers; mod timeout; +#[cfg(feature = "openssl")] mod tls; +#[cfg(feature = "openssl")] +pub use self::tls::Tls; pub use self::{ address::Address, backlog::Backlog, keep_alive::KeepAlive, max_connection_rate::MaxConnectionRate, max_connections::MaxConnections, mode::Mode, - num_workers::NumWorkers, timeout::Timeout, tls::Tls, + num_workers::NumWorkers, timeout::Timeout, }; /// Settings types for Actix Web. @@ -57,5 +60,6 @@ pub struct ActixSettings { pub shutdown_timeout: Timeout, /// TLS (HTTPS) configuration. + #[cfg(feature = "openssl")] pub tls: Tls, } diff --git a/actix-settings/src/settings/tls.rs b/actix-settings/src/settings/tls.rs index fd0a63eb3..c39b16f8f 100644 --- a/actix-settings/src/settings/tls.rs +++ b/actix-settings/src/settings/tls.rs @@ -1,13 +1,16 @@ use std::path::PathBuf; +use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod}; use serde::Deserialize; +use crate::AsResult; + /// TLS (HTTPS) configuration. #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)] #[serde(rename_all = "kebab-case")] #[doc(alias = "ssl", alias = "https")] pub struct Tls { - /// Tru if accepting TLS connections should be enabled. + /// True if accepting TLS connections should be enabled. pub enabled: bool, /// Path to certificate `.pem` file. @@ -16,3 +19,43 @@ pub struct Tls { /// Path to private key `.pem` file. pub private_key: PathBuf, } + +impl Tls { + /// Generates an [`SslAcceptorBuilder`] with its settings. It is often used for the following method + /// [`actix_web::server::HttpServer::bind_openssl`]. + /// + /// # Example + /// ```no_run + /// use actix_settings::{ApplySettings, Settings}; + /// use actix_web::{get, App, HttpServer, Responder}; + /// + /// #[get("/")] + /// async fn index() -> impl Responder { + /// "Hello." + /// } + /// + /// #[actix_web::main] + /// async fn main() -> std::io::Result<()> { + /// let settings = Settings::from_default_template(); + /// + /// HttpServer::new(|| { + /// App::new() + /// .service(index) + /// }) + /// .try_apply_settings(&settings)? + /// .bind(("127.0.0.1", 8080))? + /// .bind_openssl(("127.0.0.1", 8081), settings.actix.tls.get_ssl_acceptor_builder()?)? + /// .run() + /// .await + /// } + /// ``` + #[allow(rustdoc::broken_intra_doc_links)] + pub fn get_ssl_acceptor_builder(&self) -> AsResult { + let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls())?; + builder.set_certificate_chain_file(&self.certificate)?; + builder.set_private_key_file(&self.private_key, SslFiletype::PEM)?; + builder.check_private_key()?; + + Ok(builder) + } +}