current work version

Dieser Commit ist enthalten in:
Sebastian Tobie 2025-06-18 21:47:24 +02:00
Ursprung a828ccfffc
Commit 0080f592c0
18 geänderte Dateien mit 375 neuen und 268 gelöschten Zeilen

Datei anzeigen

@ -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

Datei anzeigen

@ -10,3 +10,6 @@ end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
[*.{yml,yaml}]
indent_size = 2

Datei anzeigen

@ -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$'

47
Cargo.lock generiert
Datei anzeigen

@ -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",

Datei anzeigen

@ -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"

Datei anzeigen

@ -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

Datei anzeigen

@ -1 +0,0 @@
pidfd_getpid@GLIBC_2.39 pidfd_getpid

Datei anzeigen

@ -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;

Datei anzeigen

@ -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<PKey<Private>, 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(
process::site(ProcessorArgs::new(
site,
Arc::clone(account),
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)"

Datei anzeigen

@ -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::<Vec<Identifier>>());
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<PathBuf>, 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<PathBuf>, 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)->

Datei anzeigen

@ -33,10 +33,15 @@ pub struct GeneralConfig {
pub sites_path: String,
#[serde(default = "GeneralConfig::default_challenge")]
pub http_challenge_path: Option<String>,
/// This contains the domains(Keys) and the DNS-Servers(values) that are responsible for it.
#[serde(default = "GeneralConfig::default_dns")]
pub dns: HashMap<String, DnsBuilder>,
#[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<String, CA>,
}
@ -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<String>,
/// Group of the Certificate and private key
#[serde(default)]
#[cfg(feature = "capabilities")]
pub group: String,
pub group: Option<String>,
}

Datei anzeigen

@ -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<dyn DnsToken> {
Ok(Box::pin(DnsUpdateToken {}))
async fn set_record(&self, _domain: String, _content: String) -> crate::types::Result<DnsToken> {
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 {}

Datei anzeigen

@ -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<Pin<Box<dyn DnsToken + '_>>> {
pub async fn set_record(&self, domain: String, value: String) -> Option<DnsToken> {
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<dyn DnsToken> {
pub async fn set_record(&self, domain: String, content: String) -> crate::types::Result<DnsToken> {
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,

Datei anzeigen

@ -1,9 +1,6 @@
use std::{
pin::Pin,
time::{
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<Url, ()> {
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<dyn DnsToken> {
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<DnsToken> {
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::<PdnsError>().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::<PdnsError>(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<Self>> {
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::<PdnsError>()) {
match response.json::<PdnsError>().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<Record>,
comments: Vec<Comment>,
ttl: u16,
},
Delete,
}
@ -201,5 +241,5 @@ struct RRSet {
#[macro_rules_derive(DefDer)]
#[derive(Serialize, new)]
struct RecordUpdate {
rrset: Vec<RRSet>,
rrsets: Vec<RRSet>,
}

Datei anzeigen

@ -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<String>;
pub type SafeSet<T> = Mutex<HashSet<T>>;
pub type Result<T> = std::result::Result<T, structs::Error>;
#[allow(type_alias_bounds)]
pub type BoxedResult<T: DnsToken> = std::result::Result<Pin<Box<T>>, structs::Error>;

Datei anzeigen

@ -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<PathBuf>);
#[cfg(feature = "capabilities")]
attr_function!(pub owner site => String);
attr_function!(pub owner site => Option<String>);
#[cfg(feature = "capabilities")]
attr_function!(pub group site => String);
attr_function!(pub group site => Option<String>);
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<PdnsToken>),
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,
}
}
}

Datei anzeigen

@ -1,23 +1,25 @@
use std::collections::HashSet;
use crate::consts::{
use crate::{
consts::{
BRAINPOOL_MIDDLE,
BRAINPOOL_STRONG,
BRAINPOOL_WEAK,
SECP_MIDDLE,
SECP_STRONG,
SECP_WEAK,
};
use crate::{
},
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<T: IntoIterator<Item = 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<dyn DnsToken>;
async fn set_record(&self, domain: String, content: String) -> types::Result<DnsToken>;
}

Datei anzeigen

@ -109,10 +109,16 @@ 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);
#[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,
@ -131,53 +137,59 @@ pub fn check_permissions() -> bool {
}
}
}
caps
}
{
if unsafe { libc::geteuid() } == 0 {
perms |= 1 << 1;
}
}
perms > 0
}
pub fn get_uid_gid(username: String, group: String) -> (Option<u32>, Option<u32>) {
let uid = if username.contains('\0') {
error!("Invalid Username");
fn clean_name(name: Option<String>) -> Option<CString> {
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<String>, group: Option<String>) -> (Option<u32>, Option<u32>) {
let uid = if let Some(username) = clean_name(username) {
unsafe {
match CString::new(username) {
Ok(cstr) => {
let passwd = libc::getpwnam(cstr.as_ptr());
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
}
},
Err(error) => {
error!("Failed to convert the Username to an uid: {error}");
}
} 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)
let group = libc::getgrnam(group.as_ptr());
if !group.is_null() {
Some(group.read().gr_gid)
} else {
warn!("Failed to get user: User does not exist");
warn!("Failed to get group: Group does not exist");
None
}
},
Err(error) => {
error!("Failed to convert the Group name to an uid: {error}");
}
} else {
None
},
}
}
};
(uid, gid)
}