From 0080f592c0ebfea179ed02a4ee437e9c7ec0854e Mon Sep 17 00:00:00 2001 From: Sebastian Tobie Date: Wed, 18 Jun 2025 21:47:24 +0200 Subject: [PATCH] current work version --- .cargo/config.toml | 25 ++++++- .editorconfig | 3 + .pre-commit-config.yaml | 8 +++ Cargo.lock | 47 +++++-------- Cargo.toml | 22 ++++--- Makefile | 10 --- polyfill-glibc-renames.txt | 1 - src/consts.rs | 9 ++- src/main.rs | 46 ++++++------- src/process/mod.rs | 60 ++++++++--------- src/types/config.rs | 11 ++-- src/types/dns/dnsupdate.rs | 29 ++++---- src/types/dns/mod.rs | 33 +++++++--- src/types/dns/pdns.rs | 132 ++++++++++++++++++++++++------------- src/types/mod.rs | 9 +-- src/types/structs.rs | 47 +++++++++++-- src/types/traits.rs | 29 ++++---- src/utils.rs | 122 ++++++++++++++++++---------------- 18 files changed, 375 insertions(+), 268 deletions(-) delete mode 100644 Makefile delete mode 100644 polyfill-glibc-renames.txt diff --git a/.cargo/config.toml b/.cargo/config.toml index 58044c6..b7013c7 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -4,5 +4,26 @@ RUST_LOG="TRACE" SSL_CERT_DIR="/etc/ca-certificates/extracted/cadir/" SSL_CERT_FILE="/etc/ca-certificates/extracted/tls-ca-bundle.pem" -[target] -[target.'cfg(debug_assertions)'] +[profile.dev] +opt-level = 3 +debug = true +strip = "none" +debug-assertions = true +overflow-checks = true +lto = false +panic = "abort" +incremental = true +codegen-units = 16 +rpath = false + +[profile.release] +opt-level = 3 +debug = false +strip = "debuginfo" +debug-assertions = false +overflow-checks = true +lto = true +panic = 'abort' +incremental = true +codegen-units = 16 +rpath = false diff --git a/.editorconfig b/.editorconfig index f93bf89..774d984 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,3 +10,6 @@ end_of_line = lf charset = utf-8 trim_trailing_whitespace = false insert_final_newline = false + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3aed9c9..d773812 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -52,3 +52,11 @@ repos: types: - rust pass_filenames: false + - id: tomlq + name: toml Format + description: Formats Toml files + entry: echo tomlq . -ti + language: system + types: + - toml + exclude: '\.lock$' diff --git a/Cargo.lock b/Cargo.lock index f5a3eef..e338342 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -190,17 +190,6 @@ dependencies = [ "syn", ] -[[package]] -name = "async-scoped" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4042078ea593edffc452eef14e99fdb2b120caa4ad9618bcdeabc4a023b98740" -dependencies = [ - "futures", - "pin-project", - "tokio", -] - [[package]] name = "async-signal" version = "0.2.11" @@ -244,9 +233,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" @@ -1096,9 +1085,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.173" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libsystemd" @@ -1529,16 +1518,15 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "racme" version = "0.1.0" dependencies = [ "acme2-eab", - "async-scoped", "caps", "clap", "data-encoding", @@ -1722,9 +1710,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.27" +version = "0.23.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" dependencies = [ "once_cell", "ring", @@ -1953,12 +1941,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "smallvec" @@ -2266,9 +2251,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", @@ -2724,18 +2709,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index d5f1f2e..6d42c6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,9 +12,18 @@ unstable = ["capabilities"] derive-new = "0.7.0" env_logger = "0.11" lazy_static = "1.5" -schemars = { version = "0.9.0", default-features = false, features = ["derive", "std", "preserve_order"] } -serde_json = { version = "1.0.140", default-features = false, features = ["std"] } toml = "0.8" + +[dependencies.schemars] +version = "0.9.0" +default-features = false +features = ["derive", "std", "preserve_order"] + +[dependencies.serde_json] +version = "1.0.140" +default-features = false +features = ["std"] + [dependencies.caps] version = "0.5.5" default-features = false @@ -23,7 +32,7 @@ optional = true [dependencies.libc] version = "0.2" default-features = false -features = ["const-extern-fn", "std", "extra_traits"] +features = ["const-extern-fn", "std"] [dependencies.data-encoding] version = "2.9" @@ -52,11 +61,6 @@ version = "0.1" default-features = false features = ["fs"] -[dependencies.async-scoped] -version = "0.9" -default-features = false -features = ["use-tokio"] - [dependencies.libsystemd] version = "0.7" default-features = false @@ -82,7 +86,7 @@ features = ["derive"] [dependencies.tokio] version = "1.45" default-features = false -features = ["rt", "sync", "time", "net"] +features = ["rt", "sync", "time", "net", "macros"] [dependencies.reqwest] version = "0.12" diff --git a/Makefile b/Makefile deleted file mode 100644 index df5204f..0000000 --- a/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -POLYFILL=polyfill-glibc -TARGET_GLIBC=2.36 -SOURCEFILES=$(wildcard src/**/*.rs) - -target/release/racme: Cargo.toml Cargo.lock $(SOURCEFILES) - cargo build --release - -racme: target/release/racme - $(POLYFILL) --target-glibc=$(TARGET_GLIBC) --rename-dynamic-symbols=polyfill-glibc-renames.txt --output=racme $^ - strip racme diff --git a/polyfill-glibc-renames.txt b/polyfill-glibc-renames.txt deleted file mode 100644 index eb7f088..0000000 --- a/polyfill-glibc-renames.txt +++ /dev/null @@ -1 +0,0 @@ -pidfd_getpid@GLIBC_2.39 pidfd_getpid diff --git a/src/consts.rs b/src/consts.rs index 3d85b86..87529cc 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -8,13 +8,16 @@ use lazy_static::lazy_static; lazy_static! { pub static ref FILE_MODE: OpenOptions = OpenOptions::new().create(false).read(true).write(false).truncate(false).to_owned(); pub static ref FILE_MODE_WRITE: OpenOptions = OpenOptions::new().create_new(true).write(true).to_owned(); + pub static ref FILE_MODE_OVERWRITE: OpenOptions = OpenOptions::new().create_new(false).create(true).truncate(true).write(true).to_owned(); +} +lazy_static! { pub static ref LETS_ENCRYPT: String = String::from("letsencrypt"); pub static ref LETS_ENCRYPT_STAGING: String = String::from("letsencrypt-staging"); } -pub fn with_mode_write(mode: u32) -> OpenOptions { - FILE_MODE.clone().mode(mode).to_owned() -} +pub const MODE_SECRETS: u32 = 0o600; +pub const MODE_PRIVATE: u32 = 0o640; +pub const MODE_PUBLIC: u32 = 0o644; pub const POOL_SIZE: usize = 1; diff --git a/src/main.rs b/src/main.rs index 7eedb9c..03581fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,8 +12,6 @@ pub(crate) mod process; pub(crate) mod types; pub(crate) mod utils; -#[cfg(feature = "capabilities")] -use crate::utils::check_permissions; use crate::{ consts::*, prelude::*, @@ -30,9 +28,9 @@ use crate::{ SubCommand, }, }, + utils::check_permissions, }; use acme2_eab::Directory; -use async_scoped::TokioScope; use clap::Parser; use env_logger::init as log_init; use libsystemd::daemon; @@ -112,7 +110,7 @@ async fn load_privkey(path: PathBuf) -> Result, Error> { async fn racme(flags: Arguments) -> Result<(), Error> { let client = default_client()?; - let dns_manager = Manager::new(); + let mut dns_manager = Manager::new(); let systemd_access = daemon::booted(); let mainconfig = { let file = match FILE_MODE.open(flags.config).await { @@ -123,6 +121,9 @@ async fn racme(flags: Arguments) -> Result<(), Error> { }; GeneralConfig::from_file(file).await }; + for (zone, builder) in mainconfig.dns.iter() { + dns_manager.add_builder(zone.clone(), builder.clone()).await; + } trace!("Parsed Config: {mainconfig:?}"); let files = { let rd = match read_dir(mainconfig.sites_path.clone()).await { @@ -171,27 +172,23 @@ async fn racme(flags: Arguments) -> Result<(), Error> { } let challengepath = mainconfig.http_challenge_path.and_then(|path| PathBuf::from_str(path.as_str()).ok()); - unsafe { - TokioScope::scope_and_collect(|scope| { - for site in siteconfigs { - if let Some(account) = accounts.get(&site.ca) { - scope.spawn(process::site(ProcessorArgs::new( - site, - Arc::clone(account), - &reload_services, - &restart_services, - certs.clone(), - challengepath.clone(), - dns_manager.clone(), - client.clone(), - ))); - } else { - error!("Could not process site {} because of previous errors", site.name) - } - } - }) + for site in siteconfigs { + if let Some(account) = accounts.get(&site.ca) { + process::site(ProcessorArgs::new( + site, + account.clone(), + &reload_services, + &restart_services, + certs.clone(), + challengepath.clone(), + dns_manager.clone(), + client.clone(), + )) + .await; + } else { + error!("Could not process site {} because of previous errors", site.name) + } } - .await; if systemd_access { process::services(restart_services.into_inner(), reload_services.into_inner()).await; @@ -242,7 +239,6 @@ async fn schema_generator() -> Result<(), Error> { fn main() { log_init(); let args = Arguments::parse(); - #[cfg(feature = "capabilities")] if args.subcommands.is_none() && !check_permissions() { error!( "This program needs the capability to change the ownership and the permissions of files. this can be done via adding the capability via `capsh --caps=\"cap_chown+ep cap_fowner+ep\" --shell=racme -- racme.toml`, systemd service setting AmbientCapabilities or running as root(not recommended)" diff --git a/src/process/mod.rs b/src/process/mod.rs index 7fdf6f2..6b3e502 100644 --- a/src/process/mod.rs +++ b/src/process/mod.rs @@ -1,27 +1,29 @@ -#[cfg(feature = "capabilities")] -use std::os::{ - fd::AsFd, - unix::fs::fchown, -}; use std::{ collections::{ HashMap, HashSet, }, fs::Permissions, - os::unix::fs::PermissionsExt as _, + os::{ + fd::AsFd, + unix::fs::{ + PermissionsExt as _, + fchown, + }, + }, path::PathBuf, sync::Arc, }; -#[cfg(feature = "capabilities")] -use crate::utils::get_uid_gid; use crate::{ consts::{ ATTEMPTS, FILE_MODE, + FILE_MODE_OVERWRITE, + MODE_PRIVATE, + MODE_PUBLIC, + MODE_SECRETS, WAIT_TIME, - with_mode_write, }, load_privkey, prelude::*, @@ -38,6 +40,7 @@ use crate::{ }, utils::{ gen_key, + get_uid_gid, prefix_emails, }, }; @@ -54,7 +57,6 @@ use acme2_eab::{ OrderBuilder, OrderStatus, }; -use async_scoped::TokioScope; use log::*; use openssl::{ hash::MessageDigest, @@ -82,6 +84,7 @@ use tokio::{ AsyncReadExt, AsyncWriteExt, }, + join, }; use zbus_systemd::systemd1; @@ -146,7 +149,7 @@ pub async fn accounts( } } else { info!("creating new key for the account {}", name.clone()); - accountkeyfile = match with_mode_write(0o600).open(accountkey).await { + accountkeyfile = match FILE_MODE_OVERWRITE.clone().mode(MODE_SECRETS).open(accountkey).await { Ok(file) => Some(file), Err(error) => { error!("Failed to open the file for the accountkey: {error}"); @@ -218,7 +221,6 @@ pub async fn site(args: ProcessorArgs<'_>) { }; cert_renew = true; } - #[cfg(feature = "capabilities")] let (uid, gid) = get_uid_gid(args.owner(), args.group()); let mut private_key; // Private key block @@ -245,7 +247,7 @@ pub async fn site(args: ProcessorArgs<'_>) { } if write_pkey { let pkey = private_key.private_key_to_pem_pkcs8().unwrap(); - let mut file = match_error!(with_mode_write(0o640).open(private_key_file.clone()).await=>Err(error)->"Failed to write new private key: {error}"); + let mut file = match_error!(FILE_MODE_OVERWRITE.clone().mode(MODE_PRIVATE).open(private_key_file.clone()).await=>Err(error)->"Failed to write new private key: {error}"); #[cfg(feature = "capabilities")] if let Err(error) = fchown(file.as_fd(), uid, gid) { error!("Failed to change owner of the new privatekey: {error}"); @@ -289,13 +291,11 @@ pub async fn site(args: ProcessorArgs<'_>) { builder.set_identifiers(args.san().iter().map(|s| s.to_owned().into()).collect::>()); order = match_error!(builder.build().await=>Err(error)-> "Failed order the certificate: {error}"); let authorizations = match_error!(order.authorizations().await=>Err(error)-> "Failed to get the authorizations: {error}"); - let (_, result) = tokio::join! { - unsafe { - TokioScope::scope_and_collect(|scope|{ + let (_, result) = join! { + async { for authorization in authorizations { - scope.spawn(auth(authorization, args.challenge_dir(), args.dns_manager())); + auth(authorization, args.challenge_dir(), args.dns_manager()).await; } - }) }, order.wait_ready(WAIT_TIME, ATTEMPTS), }; @@ -338,17 +338,13 @@ pub async fn site(args: ProcessorArgs<'_>) { Ok(Some(certs)) => certs, }; debug!("Received {} certificates.", certs.len()); - let mut pubkey_file = - match_error!(with_mode_write(0o644).open(pubkey_filename).await=>Err(error)-> "Failed to open the file for the publickey: {error}"); + let mut pubkey_file = match_error!(FILE_MODE_OVERWRITE.clone().mode(MODE_PUBLIC).open(pubkey_filename).await=>Err(error)-> "Failed to open the file for the publickey: {error}"); match_error!(pubkey_file.write_all(&certs[0].to_pem().unwrap()).await=>Err(error)-> "Failed to write the publickey: {error}"); - #[cfg(feature = "capabilities")] if let Err(error) = fchown(pubkey_file.as_fd(), uid, gid) { error!("Failed to change owner of the new publickey: {error}"); return; } - let mut fullchain = - match_error!(with_mode_write(0o644).open(directory.join("fullchain.pem")).await=>Err(error)-> "failed to open the fullchain.pem: {error}"); - #[cfg(feature = "capabilities")] + let mut fullchain = match_error!(FILE_MODE_OVERWRITE.clone().mode(MODE_PUBLIC).open(directory.join("fullchain.pem")).await=>Err(error)-> "failed to open the fullchain.pem: {error}"); if let Err(error) = fchown(fullchain.as_fd(), uid, gid) { error!("Failed to change owner of the new file with the complete chain: {error}"); return; @@ -356,8 +352,8 @@ pub async fn site(args: ProcessorArgs<'_>) { for cert in certs.clone() { let _ = fullchain.write_all(&cert.to_pem().unwrap()).await; } - let mut bundle = match_error!(with_mode_write(0o640).open(directory.join("bundle.pem")).await=>Err(error)-> "failed to open the bundle.pem: {error}"); - #[cfg(feature = "capabilities")] + let mut bundle = match_error!(FILE_MODE_OVERWRITE.clone().mode(MODE_PRIVATE).open(directory.join("bundle.pem")).await=>Err(error)-> "failed to open the bundle.pem: {error}"); + if let Err(error) = fchown(bundle.as_fd(), uid, gid) { error!("Failed to change owner of the new bundle: {error}"); return; @@ -377,7 +373,7 @@ pub async fn site(args: ProcessorArgs<'_>) { } } -async fn dns_auth(mut dns_challenge: Challenge, manager: Manager) -> types::Result<()> { +async fn dns_auth(mut dns_challenge: Challenge, manager: Manager, domain: String) -> types::Result<()> { let token = match dns_challenge.token { Some(ref token) => token.clone(), None => { @@ -389,13 +385,13 @@ async fn dns_auth(mut dns_challenge: Challenge, manager: Manager) -> types::Resu Ok(None) => return Error::err("Failed to get key authoriration: No Authorization returned"), Err(error) => return Error::err(format!("Failed to get key_authorization: {error}")), }; - if let Some(_guard) = manager.set_record(token.clone(), value).await { + if let Some(mut guard) = manager.set_record(format!("_acme-challenge.{domain}"), value).await { dns_challenge = match dns_challenge.validate().await { Ok(challenge) => challenge, Err(error) => return Error::err(format!("Failed to send the request for validation: {error}")), }; let challenge_result = dns_challenge.wait_done(WAIT_TIME, ATTEMPTS).await; - drop(_guard); + guard.unset_token().await; match challenge_result { Ok(_) => { info!("Challenge: {token} was correctly validated"); @@ -410,8 +406,9 @@ async fn dns_auth(mut dns_challenge: Challenge, manager: Manager) -> types::Resu pub async fn auth(auth: Authorization, challenge_dir: Option, manager: Manager) { + trace!("processing Authorization: {}:{}", auth.identifier.r#type, auth.identifier.value.clone()); if let Some(dns_challenge) = auth.get_challenge("dns-01") { - match dns_auth(dns_challenge, manager).await { + match dns_auth(dns_challenge, manager, auth.identifier.value.clone()).await { Ok(()) => return, Err(error) => { error!("Failed to authenticate via DNS: {error}"); @@ -426,8 +423,7 @@ pub async fn auth(auth: Authorization, challenge_dir: Option, manager: if let Some(directory) = challenge_dir { match_error!(create_dir_all(directory.clone()).await=>Err(error)-> "Failed to ensure the directory exists: {error}"); let filename = directory.join(challenge.token.clone().unwrap()); - let mut challengefile = - match_error!(with_mode_write(0o644).open(filename.clone()).await=>Err(error)-> "Failed to open the file for the http-challenge: {error}"); + let mut challengefile = match_error!(FILE_MODE_OVERWRITE.clone().mode(MODE_PUBLIC).open(filename.clone()).await=>Err(error)-> "Failed to open the file for the http-challenge: {error}"); match_error!(challengefile.set_permissions(Permissions::from_mode(0o644)).await=>Err(error)-> "Failed to give the file the nessesary permissions: {error}"); match_error!( challengefile.write_all(challenge.key_authorization().unwrap().unwrap().as_bytes()).await=>Err(error)-> diff --git a/src/types/config.rs b/src/types/config.rs index ae09ee4..7b9238e 100644 --- a/src/types/config.rs +++ b/src/types/config.rs @@ -33,10 +33,15 @@ pub struct GeneralConfig { pub sites_path: String, #[serde(default = "GeneralConfig::default_challenge")] pub http_challenge_path: Option, + + /// This contains the domains(Keys) and the DNS-Servers(values) that are responsible for it. #[serde(default = "GeneralConfig::default_dns")] pub dns: HashMap, #[serde(default = "GeneralConfig::default_certificates")] pub certificates_path: String, + + /// The Key of this table describe an nickname for an CA. + /// Letsencrypt Prod and Staging are builtin configured, so they doesn't have to be configured. #[serde(default = "GeneralConfig::default_cas")] pub ca: HashMap, } @@ -166,11 +171,9 @@ pub struct SiteConfig { /// Owner of the Certificate and private key #[serde(default)] - #[cfg(feature = "capabilities")] - pub owner: String, + pub owner: Option, /// Group of the Certificate and private key #[serde(default)] - #[cfg(feature = "capabilities")] - pub group: String, + pub group: Option, } diff --git a/src/types/dns/dnsupdate.rs b/src/types/dns/dnsupdate.rs index 5701c4f..8ff48f8 100644 --- a/src/types/dns/dnsupdate.rs +++ b/src/types/dns/dnsupdate.rs @@ -4,9 +4,10 @@ use serde::Deserialize; use crate::{ macros::DefDer, - types::traits::{ - DnsHandler, - DnsToken, + types::{ + dns::Dns, + structs::DnsToken, + traits::DnsHandler, }, }; @@ -15,22 +16,26 @@ use crate::{ #[serde(deny_unknown_fields)] pub struct DNSUpdateClientOptions {} +impl DNSUpdateClientOptions { + pub fn build(self, _zone: String) -> Dns { + Dns::DNSUpdate(DnsUpdateHandler {}) + } +} + #[macro_rules_derive(DefDer)] -pub(super) struct DnsUpdateHandler {} +pub struct DnsUpdateHandler {} impl DnsHandler for DnsUpdateHandler { - async fn set_record(&self, _domain: String, _content: String) -> crate::types::BoxedResult { - Ok(Box::pin(DnsUpdateToken {})) + async fn set_record(&self, _domain: String, _content: String) -> crate::types::Result { + Ok(DnsToken::new_dns_update()) } } #[macro_rules_derive(DefDer)] -struct DnsUpdateToken {} +pub struct DnsUpdateToken {} -impl Drop for DnsUpdateToken { - fn drop(&mut self) { - todo!() - } +impl DnsUpdateToken { + pub async fn remove(&mut self) {} } -impl DnsToken for DnsUpdateToken {} + unsafe impl Send for DnsUpdateToken {} diff --git a/src/types/dns/mod.rs b/src/types/dns/mod.rs index 2782891..1ac283d 100644 --- a/src/types/dns/mod.rs +++ b/src/types/dns/mod.rs @@ -14,11 +14,11 @@ use crate::{ PdnsHandler, }, }, - structs::Error, - traits::{ - DnsHandler, + structs::{ DnsToken, + Error, }, + traits::DnsHandler, }, }; use log::*; @@ -27,7 +27,6 @@ use schemars::JsonSchema; use serde::Deserialize; use std::{ collections::HashMap, - pin::Pin, sync::Arc, }; use tokio::sync::Mutex; @@ -42,7 +41,7 @@ impl Manager { }))) } - pub async fn set_record(&self, domain: String, value: String) -> Option>> { + pub async fn set_record(&self, domain: String, value: String) -> Option { let mut tld = domain.clone(); if !tld.ends_with('.') { tld.push('.'); @@ -50,8 +49,8 @@ impl Manager { let (mut best_match_domain, mut best_match_length) = ("", 0); let guard = self.0.lock().await; for domain in guard.servers.keys() { - if domain.ends_with(&tld) { - let matched = domain.rmatches(&tld).last().unwrap(); + trace!("Checking {domain} < {tld}"); + if let Some(matched) = tld.rmatches(domain).last() { if matched.len() > best_match_length { best_match_domain = matched; best_match_length = matched.len(); @@ -59,6 +58,7 @@ impl Manager { } } if best_match_length == 0 { + trace!("No matching Domain Found: {domain} TXT {value}"); return None; } let handler = guard.servers.get(best_match_domain).unwrap(); @@ -70,6 +70,21 @@ impl Manager { }, } } + + pub async fn add_builder(&mut self, zone: String, builder: DnsBuilder) { + let mut fixed_zone = zone.clone(); + if !fixed_zone.ends_with('.') { + fixed_zone.push('.'); + } + self.0.lock().await.servers.insert( + fixed_zone, + match builder { + DnsBuilder::PowerDNS(pdns_client_options) => pdns_client_options.build(zone), + DnsBuilder::DNSUpdate(dnsupdate_client_options) => dnsupdate_client_options.build(zone), + DnsBuilder::None => Dns::None, + }, + ); + } } unsafe impl Send for Manager {} @@ -92,14 +107,14 @@ pub enum DnsBuilder { } #[macro_rules_derive(DefDer)] -enum Dns { +pub enum Dns { PowerDNS(PdnsHandler), DNSUpdate(DnsUpdateHandler), None, } impl Dns { - pub async fn set_record(&self, domain: String, content: String) -> crate::types::BoxedResult { + pub async fn set_record(&self, domain: String, content: String) -> crate::types::Result { match self { Dns::PowerDNS(pdns_handler) => pdns_handler.set_record(domain, content).await, Dns::DNSUpdate(dns_update_handler) => dns_update_handler.set_record(domain, content).await, diff --git a/src/types/dns/pdns.rs b/src/types/dns/pdns.rs index 1e15633..b70ef43 100644 --- a/src/types/dns/pdns.rs +++ b/src/types/dns/pdns.rs @@ -1,9 +1,6 @@ -use std::{ - pin::Pin, - time::{ - SystemTime, - UNIX_EPOCH, - }, +use std::time::{ + SystemTime, + UNIX_EPOCH, }; use derive_new::new; @@ -12,22 +9,24 @@ use macro_rules_attribute::macro_rules_derive; use reqwest::{ RequestBuilder, StatusCode, + Url, }; use schemars::JsonSchema; use serde::{ Deserialize, Serialize, }; -use tokio::runtime::Handle; use crate::{ + default_client, macros::DefDer, types::{ - structs::Error, - traits::{ - DnsHandler, + dns::Dns, + structs::{ DnsToken, + Error, }, + traits::DnsHandler, }, }; @@ -48,8 +47,20 @@ impl PdnsClientOptions { } } +impl PdnsClientOptions { + pub fn build(self, zone: String) -> Dns { + Dns::PowerDNS(PdnsHandler { + client: default_client().unwrap(), + server: self.server, + api_key: self.api_key, + server_id: self.server_id, + zone, + }) + } +} + #[macro_rules_derive(DefDer)] -pub(super) struct PdnsHandler { +pub struct PdnsHandler { client: reqwest::Client, server: String, api_key: String, @@ -57,24 +68,49 @@ pub(super) struct PdnsHandler { zone: String, } +fn fix_url(baseurl: Url, path: String) -> Result { + let mut copy = baseurl.clone(); + copy.set_scheme("https")?; + copy.set_port(None)?; + copy.set_path(&path); + copy.set_query(None); + copy.set_fragment(None); + copy.set_username("")?; + copy.set_password(None)?; + trace!("Resulting URL: {copy}"); + Ok(copy) +} + + impl DnsHandler for PdnsHandler { - async fn set_record(&self, domain: String, content: String) -> crate::types::BoxedResult { - let base_request = self - .client - .patch(format!("https://{}/api/v1/servers/{}/zones/{}", self.server, self.server_id, self.zone)) - .header("Content-Type", "application/json") - .header("X-API-Key", self.api_key.clone()); + async fn set_record(&self, mut domain: String, content: String) -> crate::types::Result { + trace!("Original URL: {}", self.server); + let baseurl = match reqwest::Url::parse(&self.server) { + Ok(url) => { + match fix_url(url, format!("/api/v1/servers/{}/zones/{}", self.server_id, self.zone)) { + Ok(url) => url, + Err(_) => return Error::err("Failed to parse the Server URL of the DNS Server"), + } + }, + Err(error) => return Error::err(format!("Failed to parse the server url: {error}")), + }; + let base_request = self.client.patch(baseurl).header("Content-Type", "application/json").header("X-API-Key", self.api_key.clone()); + + if !domain.ends_with('.') { + domain.push('.'); + } match base_request .try_clone() .unwrap() .json(&RecordUpdate::new(vec![ RRSet::new( - domain, + domain.clone(), "TXT", ChangeType::Replace { - records: vec![Record::new(content)], + records: vec![Record::new(format!("\"{content}\""))], comments: vec![Comment::new("ACME entry", "")], + ttl: 0, }, ), ])) @@ -83,49 +119,53 @@ impl DnsHandler for PdnsHandler { { Ok(_resp) if _resp.status() == StatusCode::NO_CONTENT => {}, Ok(resp) => { - let err = match resp.json::().await { - Ok(error) => error.error, - Err(error) => format!("{error}"), + let response_text = match resp.text().await { + Ok(resp) => resp, + Err(error) => return Error::err(format!("Failed to get the errormessage: {error}")), + }; + let err = match serde_json::from_slice::(response_text.clone().as_bytes()) { + Ok(error) => { + trace!("The Server Error has {} suberrors", error.errors.len()); + for suberror in error.errors { + trace!("SubError: {suberror}") + } + error.error + }, + Err(error) => { + trace!("http Response: {response_text:?}"); + format!("(Parsing error) {error}") + }, }; return Error::err(format!("Failed to set the record: {err}")); }, Err(error) => return Error::err(format!("Failed to send the request to update the record: {error}")), } - Ok(Box::pin(PdnsToken { - builder: base_request.try_clone().unwrap().json(&0), - })) + Ok(DnsToken::new_pdns(base_request.try_clone().unwrap().json(&RecordUpdate::new(vec![RRSet::new(domain, "TXT", ChangeType::Delete)])))) } } /// Token that deletes the Record when its no longer needed -#[derive(Debug)] -pub(super) struct PdnsToken { - builder: RequestBuilder, +#[derive(Debug, new)] +pub struct PdnsToken { + pub(crate) builder: RequestBuilder, } -impl PdnsToken { - fn new(builder: RequestBuilder) -> Pin> { - Box::pin(Self { - builder, - }) +impl Clone for PdnsToken { + fn clone(&self) -> Self { + Self { + builder: self.builder.try_clone().unwrap(), + } } } -impl Drop for PdnsToken { - fn drop(&mut self) { - let handle = match Handle::try_current() { - Ok(handle) => handle, - Err(error) => { - error!("Failed to aquire the handle from tokio. Cleanup of the Token Failed: {error}"); - return; - }, - }; - match handle.block_on(self.builder.try_clone().unwrap().send()) { +impl PdnsToken { + pub async fn remove(&mut self) { + match self.builder.try_clone().unwrap().send().await { Ok(response) => { if response.status() != StatusCode::NO_CONTENT { let status = response.status(); - match handle.block_on(response.json::()) { + match response.json::().await { Ok(error) => error!("Failed to delete the Record({status}): {error}"), Err(error) => error!("Failed to parse the Error Response({status}): {error}"), } @@ -137,7 +177,6 @@ impl Drop for PdnsToken { } unsafe impl Send for PdnsToken {} -impl DnsToken for PdnsToken {} #[macro_rules_derive(DefDer)] #[derive(Deserialize)] @@ -183,6 +222,7 @@ enum ChangeType { Replace { records: Vec, comments: Vec, + ttl: u16, }, Delete, } @@ -201,5 +241,5 @@ struct RRSet { #[macro_rules_derive(DefDer)] #[derive(Serialize, new)] struct RecordUpdate { - rrset: Vec, + rrsets: Vec, } diff --git a/src/types/mod.rs b/src/types/mod.rs index 90023f0..6dc0f08 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,10 +1,6 @@ -use std::{ - collections::HashSet, - pin::Pin, -}; +use std::collections::HashSet; use tokio::sync::Mutex; -use crate::types::traits::DnsToken; pub mod config; pub mod cryptography; @@ -20,6 +16,3 @@ pub type VString = Vec; pub type SafeSet = Mutex>; pub type Result = std::result::Result; - -#[allow(type_alias_bounds)] -pub type BoxedResult = std::result::Result>, structs::Error>; diff --git a/src/types/structs.rs b/src/types/structs.rs index 7aa953a..341dc05 100644 --- a/src/types/structs.rs +++ b/src/types/structs.rs @@ -16,7 +16,10 @@ use clap::{ }; use derive_new::new; use macro_rules_attribute::macro_rules_derive; -use reqwest::Client; +use reqwest::{ + Client, + RequestBuilder, +}; use tokio::sync::MutexGuard; use crate::{ @@ -28,7 +31,11 @@ use crate::{ Algorithm, Strength, }, - dns::Manager, + dns::{ + Manager, + dnsupdate::DnsUpdateToken, + pdns::PdnsToken, + }, }, }; @@ -86,11 +93,9 @@ impl<'a: 'b, 'b> ProcessorArgs<'a> { attr_function!(pub challenge_dir => Option); - #[cfg(feature = "capabilities")] - attr_function!(pub owner site => String); + attr_function!(pub owner site => Option); - #[cfg(feature = "capabilities")] - attr_function!(pub group site => String); + attr_function!(pub group site => Option); attr_function!(pub client => Client); @@ -155,3 +160,33 @@ impl Error { Err(Self::new(message.into())) } } + + +#[macro_rules_derive(DefDer)] +pub enum DnsToken { + None, + Pdns(Box), + DnsUpdate(DnsUpdateToken), +} + +impl DnsToken { + pub fn new_pdns(builder: RequestBuilder) -> Self { + Self::Pdns(Box::new(PdnsToken::new(builder))) + } + + pub fn new_dns_update() -> Self { + Self::DnsUpdate(DnsUpdateToken {}) + } + + pub fn new_none() -> Self { + Self::None + } + + pub async fn unset_token(&mut self) { + match self { + DnsToken::None => {}, + DnsToken::Pdns(pdns_token) => pdns_token.remove().await, + DnsToken::DnsUpdate(dns_update_token) => dns_update_token.remove().await, + } + } +} diff --git a/src/types/traits.rs b/src/types/traits.rs index 9502022..15aacba 100644 --- a/src/types/traits.rs +++ b/src/types/traits.rs @@ -1,23 +1,25 @@ use std::collections::HashSet; -use crate::consts::{ - BRAINPOOL_MIDDLE, - BRAINPOOL_STRONG, - BRAINPOOL_WEAK, - SECP_MIDDLE, - SECP_STRONG, - SECP_WEAK, -}; - use crate::{ + consts::{ + BRAINPOOL_MIDDLE, + BRAINPOOL_STRONG, + BRAINPOOL_WEAK, + SECP_MIDDLE, + SECP_STRONG, + SECP_WEAK, + }, types, types::{ cryptography::{ Algorithm, Strength, }, - structs::San, + structs::{ + DnsToken, + San, + }, }, }; use log::*; @@ -100,7 +102,7 @@ pub trait MatchX509 { impl MatchX509 for X509 { fn days_left(&self, days: u32) -> bool { let future_date = Asn1Time::days_from_now(days).unwrap(); - self.not_after().compare(&future_date).is_ok_and(|order| order.is_le()) + self.not_after().compare(&future_date).is_ok_and(|order| order.is_gt()) } fn match_san>(&self, names: T) -> bool { @@ -120,9 +122,6 @@ impl MatchX509 for X509 { } } -#[allow(drop_bounds)] -pub trait DnsToken: std::fmt::Debug + Send + Drop {} - pub trait DnsHandler: std::fmt::Debug + Send { - async fn set_record(&self, domain: String, content: String) -> types::BoxedResult; + async fn set_record(&self, domain: String, content: String) -> types::Result; } diff --git a/src/utils.rs b/src/utils.rs index ed1c346..1008f0d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -109,75 +109,87 @@ pub fn string_to_cn(name: String) -> X509Name { builder.build() } -#[cfg(feature = "capabilities")] pub fn check_permissions() -> bool { - let caps = has_cap(None, CAPABILITY_SET, caps::Capability::CAP_CHOWN).unwrap_or(false) && - has_cap(None, CAPABILITY_SET, caps::Capability::CAP_FOWNER).unwrap_or(false); - if cfg!(debug_assertions) { - for set in [ - CapSet::Ambient, - CapSet::Bounding, - CapSet::Effective, - CapSet::Inheritable, - CapSet::Permitted, - ] { - match read(None, set) { - Ok(current) => { - trace!("Current {set:?} Set: {current:?}") - }, - Err(error) => { - trace!("Failed to get the current {set:?} Set: {error}") - }, + #[allow(unused_mut)] + let mut perms: u8 = 0; + #[cfg(feature = "capabilities")] + { + if has_cap(None, CAPABILITY_SET, caps::Capability::CAP_CHOWN).unwrap_or(false) && + has_cap(None, CAPABILITY_SET, caps::Capability::CAP_FOWNER).unwrap_or(false) + { + perms |= 1 << 0 + } + if cfg!(debug_assertions) { + for set in [ + CapSet::Ambient, + CapSet::Bounding, + CapSet::Effective, + CapSet::Inheritable, + CapSet::Permitted, + ] { + match read(None, set) { + Ok(current) => { + trace!("Current {set:?} Set: {current:?}") + }, + Err(error) => { + trace!("Failed to get the current {set:?} Set: {error}") + }, + } } } } - caps + { + if unsafe { libc::geteuid() } == 0 { + perms |= 1 << 1; + } + } + perms > 0 } -pub fn get_uid_gid(username: String, group: String) -> (Option, Option) { - let uid = if username.contains('\0') { - error!("Invalid Username"); - None +fn clean_name(name: Option) -> Option { + if let Some(name) = name { + if name.contains('\0') { + error!("Invalid name: {name}"); + return None; + } + match CString::new(name) { + Ok(cname) => Some(cname), + Err(error) => { + error!("Failed to convert the Username to an uid: {error}"); + None + }, + } } else { + None + } +} + +pub fn get_uid_gid(username: Option, group: Option) -> (Option, Option) { + let uid = if let Some(username) = clean_name(username) { unsafe { - match CString::new(username) { - Ok(cstr) => { - let passwd = libc::getpwnam(cstr.as_ptr()); - if !passwd.is_null() { - Some(passwd.read().pw_uid) - } else { - warn!("Failed to get user: User does not exist"); - None - } - }, - Err(error) => { - error!("Failed to convert the Username to an uid: {error}"); - None - }, + let passwd = libc::getpwnam(username.as_ptr()); + if !passwd.is_null() { + Some(passwd.read().pw_uid) + } else { + warn!("Failed to get user: User does not exist"); + None } } + } else { + None }; - let gid = if group.contains('\0') { - error!("Invalid Group name"); - None - } else { + let gid = if let Some(group) = clean_name(group) { unsafe { - match CString::new(group) { - Ok(cstr) => { - let passwd = libc::getgrnam(cstr.as_ptr()); - if !passwd.is_null() { - Some(passwd.read().gr_gid) - } else { - warn!("Failed to get user: User does not exist"); - None - } - }, - Err(error) => { - error!("Failed to convert the Group name to an uid: {error}"); - None - }, + let group = libc::getgrnam(group.as_ptr()); + if !group.is_null() { + Some(group.read().gr_gid) + } else { + warn!("Failed to get group: Group does not exist"); + None } } + } else { + None }; (uid, gid) }