From 2688d24d4963550ff3efee363194446a3c75cc59 Mon Sep 17 00:00:00 2001 From: Noa <130567821+noagbmn@users.noreply.github.com> Date: Mon, 1 Jul 2024 15:35:40 +0500 Subject: [PATCH] feat(tls): add support for https in actix web (#5089) Co-authored-by: noagbmn <> --- Cargo.lock | 64 +++++++++++++++++++++++++-- config/config.example.toml | 9 ++++ config/deployments/env_specific.toml | 8 ++++ crates/router/Cargo.toml | 5 ++- crates/router/src/configs/defaults.rs | 2 + crates/router/src/configs/settings.rs | 15 +++++++ crates/router/src/lib.rs | 64 ++++++++++++++++++++++++--- 7 files changed, 158 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33a587ca0c..557504d98b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,6 +43,7 @@ dependencies = [ "actix-codec", "actix-rt", "actix-service", + "actix-tls", "actix-utils", "ahash 0.8.11", "base64 0.21.7", @@ -187,8 +188,10 @@ dependencies = [ "http 1.1.0", "impl-more", "pin-project-lite", + "rustls-pki-types", "tokio 1.37.0", "tokio-rustls 0.23.4", + "tokio-rustls 0.25.0", "tokio-util", "tracing", "webpki-roots 0.22.6", @@ -217,6 +220,7 @@ dependencies = [ "actix-rt", "actix-server", "actix-service", + "actix-tls", "actix-utils", "actix-web-codegen", "ahash 0.8.11", @@ -5800,7 +5804,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls 0.21.10", - "rustls-pemfile", + "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", @@ -5990,6 +5994,8 @@ dependencies = [ "roxmltree", "rust_decimal", "rustc-hash", + "rustls 0.22.4", + "rustls-pemfile 2.1.2", "scheduler", "serde", "serde_json", @@ -6219,10 +6225,24 @@ checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", "ring 0.17.8", - "rustls-webpki", + "rustls-webpki 0.101.7", "sct", ] +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring 0.17.8", + "rustls-pki-types", + "rustls-webpki 0.102.3", + "subtle", + "zeroize", +] + [[package]] name = "rustls-native-certs" version = "0.6.3" @@ -6230,7 +6250,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ "openssl-probe", - "rustls-pemfile", + "rustls-pemfile 1.0.4", "schannel", "security-framework", ] @@ -6244,6 +6264,22 @@ dependencies = [ "base64 0.21.7", ] +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64 0.22.0", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" + [[package]] name = "rustls-webpki" version = "0.101.7" @@ -6254,6 +6290,17 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "rustls-webpki" +version = "0.102.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" +dependencies = [ + "ring 0.17.8", + "rustls-pki-types", + "untrusted 0.9.0", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -7625,6 +7672,17 @@ dependencies = [ "tokio 1.37.0", ] +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio 1.37.0", +] + [[package]] name = "tokio-stream" version = "0.1.15" diff --git a/config/config.example.toml b/config/config.example.toml index 10f8ba7a5e..1c3d9ebd98 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -11,6 +11,15 @@ host = "127.0.0.1" shutdown_timeout = 30 # HTTP Request body limit. Defaults to 32kB request_body_limit = 32_768 + +# HTTPS Server Configuration +# Self-signed Private Key and Certificate can be generated with mkcert for local development +[server.tls] +port = 8081 +host = "127.0.0.1" +private_key = "/path/to/private_key.pem" +certificate = "/path/to/certificate.pem" + # Proxy server configuration for connecting to payment gateways. # Don't define the fields if a Proxy isn't needed. Empty strings will cause failure. [proxy] diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml index 68df3d28e2..27567ec6cc 100644 --- a/config/deployments/env_specific.toml +++ b/config/deployments/env_specific.toml @@ -265,6 +265,14 @@ shutdown_timeout = 30 # HTTP Request body limit. Defaults to 32kB request_body_limit = 32_768 +# HTTPS Server Configuration +# Self-signed Private Key and Certificate can be generated with mkcert for local development +[server.tls] +port = 8081 +host = "127.0.0.1" +private_key = "/path/to/private_key.pem" +certificate = "/path/to/certificate.pem" + [secrets_management] secrets_manager = "aws_kms" # Secrets manager client to be used diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index dd14c8d896..e4bfb4a454 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -9,7 +9,8 @@ readme = "README.md" license.workspace = true [features] -default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "payout_retry", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm"] +default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "payout_retry", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm", "tls"] +tls = ["actix-web/rustls-0_22"] email = ["external_services/email", "scheduler/email", "olap"] frm = ["api_models/frm", "hyperswitch_domain_models/frm"] stripe = ["dep:serde_qs"] @@ -77,6 +78,8 @@ ring = "0.17.8" roxmltree = "0.19.0" rust_decimal = { version = "1.35.0", features = ["serde-with-float", "serde-with-str"] } rustc-hash = "1.1.0" +rustls = "0.22" +rustls-pemfile = "2" serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.115" serde_path_to_error = "0.1.16" diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 95b585d344..4b8863547c 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -12,6 +12,8 @@ impl Default for super::settings::Server { host: "localhost".into(), request_body_limit: 16 * 1024, // POST request body is limited to 16KiB shutdown_timeout: 30, + #[cfg(feature = "tls")] + tls: None, } } } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 2bbb50d681..2dc0afdd11 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -547,6 +547,8 @@ pub struct Server { pub host: String, pub request_body_limit: usize, pub shutdown_timeout: u64, + #[cfg(feature = "tls")] + pub tls: Option, } #[derive(Debug, Deserialize, Clone)] @@ -826,6 +828,19 @@ pub struct PayPalOnboarding { pub enabled: bool, } +#[cfg(feature = "tls")] +#[derive(Debug, Deserialize, Clone)] +pub struct ServerTls { + /// Port to host the TLS secure server on + pub port: u16, + /// Use a different host (optional) (defaults to the host provided in [`Server`] config) + pub host: Option, + /// private key file path associated with TLS (path to the private key file (`pem` format)) + pub private_key: PathBuf, + /// certificate file associated with TLS (path to the certificate file (`pem` format)) + pub certificate: PathBuf, +} + fn deserialize_hashset_inner(value: impl AsRef) -> Result, String> where T: Eq + std::str::FromStr + std::hash::Hash, diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index f0d332514d..b653c31ec6 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -200,11 +200,65 @@ pub async fn start_server(conf: settings::Settings) -> Applicatio ); let state = Box::pin(AppState::new(conf, tx, api_client)).await; let request_body_limit = server.request_body_limit; - let server = actix_web::HttpServer::new(move || mk_app(state.clone(), request_body_limit)) - .bind((server.host.as_str(), server.port))? - .workers(server.workers) - .shutdown_timeout(server.shutdown_timeout) - .run(); + + let server_builder = + actix_web::HttpServer::new(move || mk_app(state.clone(), request_body_limit)) + .bind((server.host.as_str(), server.port))? + .workers(server.workers) + .shutdown_timeout(server.shutdown_timeout); + + #[cfg(feature = "tls")] + let server = match server.tls { + None => server_builder.run(), + Some(tls_conf) => { + let cert_file = + &mut std::io::BufReader::new(std::fs::File::open(tls_conf.certificate).map_err( + |err| errors::ApplicationError::InvalidConfigurationValueError(err.to_string()), + )?); + let key_file = + &mut std::io::BufReader::new(std::fs::File::open(tls_conf.private_key).map_err( + |err| errors::ApplicationError::InvalidConfigurationValueError(err.to_string()), + )?); + + let cert_chain = rustls_pemfile::certs(cert_file) + .collect::, _>>() + .map_err(|err| { + errors::ApplicationError::InvalidConfigurationValueError(err.to_string()) + })?; + + let mut keys = rustls_pemfile::pkcs8_private_keys(key_file) + .map(|key| key.map(rustls::pki_types::PrivateKeyDer::Pkcs8)) + .collect::, _>>() + .map_err(|err| { + errors::ApplicationError::InvalidConfigurationValueError(err.to_string()) + })?; + + // exit if no keys could be parsed + if keys.is_empty() { + return Err(errors::ApplicationError::InvalidConfigurationValueError( + "Could not locate PKCS8 private keys.".into(), + )); + } + + let config_builder = rustls::ServerConfig::builder().with_no_client_auth(); + let config = config_builder + .with_single_cert(cert_chain, keys.remove(0)) + .map_err(|err| { + errors::ApplicationError::InvalidConfigurationValueError(err.to_string()) + })?; + + server_builder + .bind_rustls_0_22( + (tls_conf.host.unwrap_or(server.host).as_str(), tls_conf.port), + config, + )? + .run() + } + }; + + #[cfg(not(feature = "tls"))] + let server = server_builder.run(); + let _task_handle = tokio::spawn(receiver_for_error(rx, server.handle()).in_current_span()); Ok(server) }