diff --git a/CHANGELOG.md b/CHANGELOG.md index 9edf2e9c..32d29f73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,13 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). -## [0.13.2] - 2025-07-27 +## [0.13.2] - 2025-07-28 If you are upgrading from v0.11.x or v0.12.x, this version includes **breaking changes** to the message queue and MTA configuration. Please read the [UPGRADING.md](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING.md) file for more information on how to upgrade from previous versions. ## Added - ACME: DeSEC cloud DNS provider support (contributed by @Tyr3al). +- ACME: OVH cloud DNS provider support (contributed by @srachner). - CalDAV Scheduling: Catalan language support (contributed by @jolupa) (#1873). - MTA: Allow to send e-mails as group, while member of that group (#485). - OIDC: Allow local access tokens to be used with third-party OIDC backends (#1311 stalwartlabs/webadmin#52). diff --git a/Cargo.lock b/Cargo.lock index 9a42d8c7..64b9d107 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2051,15 +2051,16 @@ dependencies = [ [[package]] name = "dns-update" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dbb7b41e755c6d9f9a62d3861e84fd33860b796d93aea6cfc8ef1068f4bff6" +checksum = "42788b21a1231c646c46508c406db9bf628342a781c24888bf60e1a29396deb2" dependencies = [ "hickory-client", "reqwest 0.12.15", "serde", "serde_json", "serde_urlencoded", + "sha1", "tokio", ] diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 0aef201c..7ce6eadc 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -19,7 +19,7 @@ mail-builder = { version = "0.4" } mail-auth = { version = "0.7.1" } mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] } smtp-proto = { version = "0.1", features = ["rkyv"] } -dns-update = { version = "0.1.4" } +dns-update = { version = "0.1.5" } calcard = { version = "0.1.3", features = ["rkyv"] } ahash = { version = "0.8.2", features = ["serde"] } parking_lot = "0.12.1" diff --git a/crates/common/src/config/server/tls.rs b/crates/common/src/config/server/tls.rs index 9bf10f35..603cf889 100644 --- a/crates/common/src/config/server/tls.rs +++ b/crates/common/src/config/server/tls.rs @@ -186,6 +186,10 @@ impl AcmeProviders { #[allow(clippy::unnecessary_to_owned)] fn build_dns_updater(config: &mut Config, acme_id: &str) -> Option { + let timeout = config + .property_or_default(("acme", acme_id, "timeout"), "30s") + .unwrap_or_else(|| Duration::from_secs(30)); + match config.value_require(("acme", acme_id, "provider"))? { "rfc2136-tsig" => { let algorithm: TsigAlgorithm = config @@ -231,67 +235,79 @@ fn build_dns_updater(config: &mut Config, acme_id: &str) -> Option { }) .ok() } - "cloudflare" => { - let timeout = config - .property_or_default(("acme", acme_id, "timeout"), "30s") - .unwrap_or_else(|| Duration::from_secs(30)); - - DnsUpdater::new_cloudflare( - config - .value_require(("acme", acme_id, "secret"))? - .trim() - .to_string(), - config.value(("acme", acme_id, "user")).map(|s| s.trim()), - timeout.into(), + "cloudflare" => DnsUpdater::new_cloudflare( + config + .value_require(("acme", acme_id, "secret"))? + .trim() + .to_string(), + config.value(("acme", acme_id, "user")).map(|s| s.trim()), + timeout.into(), + ) + .map_err(|err| { + config.new_build_error( + ("acme", acme_id, "provider"), + format!("Failed to create Cloudflare DNS updater: {err}"), ) - .map_err(|err| { - config.new_build_error( - ("acme", acme_id, "provider"), - format!("Failed to create Cloudflare DNS updater: {err}"), - ) - }) - .ok() - } - "digitalocean" => { - let timeout = config - .property_or_default(("acme", acme_id, "timeout"), "30s") - .unwrap_or_else(|| Duration::from_secs(30)); - - DnsUpdater::new_digitalocean( - config - .value_require(("acme", acme_id, "secret"))? - .trim() - .to_string(), - timeout.into(), + }) + .ok(), + "digitalocean" => DnsUpdater::new_digitalocean( + config + .value_require(("acme", acme_id, "secret"))? + .trim() + .to_string(), + timeout.into(), + ) + .map_err(|err| { + config.new_build_error( + ("acme", acme_id, "provider"), + format!("Failed to create DigitalOcean DNS updater: {err}"), ) - .map_err(|err| { - config.new_build_error( - ("acme", acme_id, "provider"), - format!("Failed to create DigitalOcean DNS updater: {err}"), - ) - }) - .ok() - } - "desec" => { - let timeout = config - .property_or_default(("acme", acme_id, "timeout"), "30s") - .unwrap_or_else(|| Duration::from_secs(30)); - - DnsUpdater::new_desec( - config - .value_require(("acme", acme_id, "secret"))? - .trim() - .to_string(), - timeout.into(), + }) + .ok(), + "desec" => DnsUpdater::new_desec( + config + .value_require(("acme", acme_id, "secret"))? + .trim() + .to_string(), + timeout.into(), + ) + .map_err(|err| { + config.new_build_error( + ("acme", acme_id, "provider"), + format!("Failed to create Desec DNS updater: {err}"), ) - .map_err(|err| { - config.new_build_error( - ("acme", acme_id, "provider"), - format!("Failed to create Desec DNS updater: {err}"), - ) - }) - .ok() - } + }) + .ok(), + "ovh" => DnsUpdater::new_ovh( + config + .value_require(("acme", acme_id, "key")) + .map(|s| s.trim())? + .to_string(), + config + .value_require(("acme", acme_id, "secret"))? + .trim() + .to_string(), + config + .value_require(("acme", acme_id, "consumer-key"))? + .trim() + .to_string(), + config + .value_require(("acme", acme_id, "ovh-endpoint"))? + .parse() + .map_err(|_| { + config + .new_parse_error(("acme", acme_id, "ovh-endpoint"), "Invalid OVH endpoint") + }) + .ok()?, + timeout.into(), + ) + .map_err(|err| { + config.new_build_error( + ("acme", acme_id, "provider"), + format!("Failed to create Desec DNS updater: {err}"), + ) + }) + .ok(), _ => { config.new_parse_error(("acme", acme_id, "provider"), "Unsupported provider"); None diff --git a/crates/common/src/listener/acme/order.rs b/crates/common/src/listener/acme/order.rs index 074f9e06..6def86a4 100644 --- a/crates/common/src/listener/acme/order.rs +++ b/crates/common/src/listener/acme/order.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, TimeZone, Utc}; use compact_str::CompactString; -use dns_update::DnsRecord; +use dns_update::{DnsRecord, DnsRecordType}; use futures::future::try_join_all; use rcgen::{CertificateParams, DistinguishedName, PKCS_ECDSA_P256_SHA256}; use rustls::crypto::ring::sign::any_ecdsa_type; @@ -252,7 +252,7 @@ impl Server { .to_string(); // First try deleting the record - if let Err(err) = updater.delete(&name, &origin).await { + if let Err(err) = updater.delete(&name, &origin, DnsRecordType::TXT).await { // Errors are expected if the record does not exist trc::event!( Acme(AcmeEvent::DnsRecordDeletionFailed),