Current version with an rudimentary dns update

Dieser Commit ist enthalten in:
Sebastian Tobie 2025-06-14 22:40:03 +02:00
Ursprung b4d11f0abe
Commit 4cb07326a7
18 geänderte Dateien mit 1375 neuen und 498 gelöschten Zeilen

Datei anzeigen

@ -6,4 +6,3 @@ SSL_CERT_FILE="/etc/ca-certificates/extracted/tls-ca-bundle.pem"
[target] [target]
[target.'cfg(debug_assertions)'] [target.'cfg(debug_assertions)']
runner = "strace -e trace=open,openat -P /etc/*"

12
.editorconfig Normale Datei
Datei anzeigen

@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

765
Cargo.lock generiert

Datei-Diff unterdrückt, da er zu groß ist Diff laden

Datei anzeigen

@ -8,8 +8,11 @@ resolver = "3"
unstable = [] unstable = []
[dependencies] [dependencies]
derive-new = "0.7.0"
env_logger = "0.11" env_logger = "0.11"
lazy_static = "1.5" 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" toml = "0.8"
[dependencies.caps] [dependencies.caps]
version = "0.5.5" version = "0.5.5"

137
schema-general.json Normale Datei
Datei anzeigen

@ -0,0 +1,137 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "GeneralConfig",
"type": "object",
"properties": {
"accounts_path": {
"type": "string",
"default": "accounts"
},
"sites_path": {
"type": "string",
"default": "sites"
},
"http_challenge_path": {
"type": [
"string",
"null"
],
"default": null
},
"dns": {
"type": "object",
"additionalProperties": {
"$ref": "#/$defs/DnsBuilder"
}
},
"certificates_path": {
"type": "string",
"default": "certificates"
},
"ca": {
"type": "object",
"additionalProperties": {
"$ref": "#/$defs/CA"
}
}
},
"additionalProperties": false,
"$defs": {
"DnsBuilder": {
"oneOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "powerdns"
},
"api_key": {
"type": "string"
},
"server": {
"type": "string"
},
"server_id": {
"type": "string",
"default": "localhost"
}
},
"additionalProperties": false,
"required": [
"type",
"api_key",
"server"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "dnsupdate"
}
},
"additionalProperties": false,
"required": [
"type"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "none"
}
},
"required": [
"type"
],
"additionalProperties": false
}
]
},
"CA": {
"type": "object",
"properties": {
"directory": {
"description": "Url for the directory",
"type": "uri"
},
"email_addresses": {
"description": "Email addresses for the CA to contact the user",
"type": [
"array",
"null"
],
"items": {
"type": "email"
}
},
"eab_token": {
"type": "string"
},
"eab_key": {
"type": "string"
},
"renew_before": {
"description": "Amount of days the certificate is renewed before the Certificate is outdated\n TODO: give to processor",
"type": "integer",
"format": "uint32",
"minimum": 1,
"maximum": 90,
"default": 7
},
"tos_accepted": {
"type": "boolean",
"default": false
}
},
"additionalProperties": false,
"required": [
"directory"
]
}
}
}

197
schema-site.json Normale Datei
Datei anzeigen

@ -0,0 +1,197 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "SiteConfig",
"type": "object",
"properties": {
"ca": {
"description": "The Configured Certificate Authority",
"type": "string"
},
"domains": {
"description": "The Domains this site is responsible for",
"type": "array",
"items": {
"type": "string"
}
},
"addresses": {
"description": "IPAddresses for the Certificate",
"type": "array",
"items": {
"type": "string",
"format": "ip"
},
"default": []
},
"emails": {
"description": "EmailAdresses that this Certificate is valid for",
"type": "array",
"items": {
"type": "email"
},
"default": []
},
"reload_services": {
"description": "The systemd services are reloaded",
"type": "array",
"items": {
"type": "string"
},
"default": []
},
"restart_services": {
"description": "The Systemd-Services have to be restarted to get the new certificates",
"type": "array",
"items": {
"type": "string"
},
"default": []
},
"trigger_commands": {
"description": "Commands that have to be run after the certificates have been issued if they don't have an systemd service",
"type": "array",
"items": {
"type": "string"
},
"default": []
},
"algorithm": {
"description": "The Algorithm for the Private Key",
"$ref": "#/$defs/Algorithm"
},
"strength": {
"description": "The Strength of the Private key.",
"$ref": "#/$defs/Strength"
},
"owner": {
"description": "Owner of the Certificate and private key",
"type": "string",
"default": ""
},
"group": {
"description": "Group of the Certificate and private key",
"type": "string",
"default": ""
}
},
"additionalProperties": false,
"required": [
"ca",
"domains"
],
"$defs": {
"DnsBuilder": {
"oneOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "powerdns"
},
"api_key": {
"type": "string"
},
"server": {
"type": "string"
},
"server_id": {
"type": "string",
"default": "localhost"
}
},
"additionalProperties": false,
"required": [
"type",
"api_key",
"server"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "dnsupdate"
}
},
"additionalProperties": false,
"required": [
"type"
]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "none"
}
},
"required": [
"type"
],
"additionalProperties": false
}
]
},
"CA": {
"type": "object",
"properties": {
"directory": {
"description": "Url for the directory",
"type": "uri"
},
"email_addresses": {
"description": "Email addresses for the CA to contact the user",
"type": [
"array",
"null"
],
"items": {
"type": "email"
}
},
"eab_token": {
"type": "string"
},
"eab_key": {
"type": "string"
},
"renew_before": {
"description": "Amount of days the certificate is renewed before the Certificate is outdated\n TODO: give to processor",
"type": "integer",
"format": "uint32",
"minimum": 1,
"maximum": 90,
"default": 7
},
"tos_accepted": {
"type": "boolean",
"default": false
}
},
"additionalProperties": false,
"required": [
"directory"
]
},
"Algorithm": {
"type": "string",
"enum": [
"Rsa",
"Brainpool",
"Secp",
"ED25519"
]
},
"Strength": {
"type": "string",
"enum": [
"Weak",
"Middle",
"Strong"
]
}
}
}

Datei anzeigen

@ -1,7 +1,9 @@
//! Acme client that supports multiple CAs and configs for sites that can be seperate from the mainconfig //! Acme client that supports multiple CAs and configs for sites that can be seperate from the mainconfig
#![allow(dead_code)]
#![allow(clippy::clone_on_copy)] #![allow(clippy::clone_on_copy)]
#![allow(clippy::identity_op)] #![allow(clippy::identity_op)]
#![allow(refining_impl_trait)]
#![allow(clippy::collapsible_if)]
#![allow(dead_code)]
pub(crate) mod consts; pub(crate) mod consts;
pub(crate) mod macros; pub(crate) mod macros;
@ -18,11 +20,15 @@ use crate::{
GeneralConfig, GeneralConfig,
SiteConfig, SiteConfig,
}, },
dns::Manager,
structs::{ structs::{
Arguments, Arguments,
Error,
ProcessorArgs, ProcessorArgs,
SubCommand,
}, },
}, },
utils::check_permissions,
}; };
use acme2_eab::Directory; use acme2_eab::Directory;
use async_scoped::TokioScope; use async_scoped::TokioScope;
@ -38,6 +44,16 @@ use openssl::{
}, },
}; };
use reqwest::tls::Version; use reqwest::tls::Version;
use schemars::{
SchemaGenerator,
consts::meta_schemas::DRAFT07,
generate::SchemaSettings,
};
use serde::Serialize;
use serde_json::ser::{
Formatter,
PrettyFormatter,
};
use std::{ use std::{
collections::{ collections::{
HashMap, HashMap,
@ -57,15 +73,16 @@ use tokio::{
create_dir_all, create_dir_all,
read_dir, read_dir,
}, },
io::AsyncReadExt, io::{
AsyncReadExt,
AsyncWriteExt,
},
sync::Mutex, sync::Mutex,
}; };
use tokio_stream::{ use tokio_stream::{
StreamExt, StreamExt,
wrappers::ReadDirStream, wrappers::ReadDirStream,
}; };
use types::structs::Error;
use utils::check_permissions;
fn default_client() -> Result<reqwest::Client, Error> { fn default_client() -> Result<reqwest::Client, Error> {
@ -94,6 +111,7 @@ async fn load_privkey(path: PathBuf) -> Result<PKey<Private>, Error> {
async fn racme(flags: Arguments) -> Result<(), Error> { async fn racme(flags: Arguments) -> Result<(), Error> {
let client = default_client()?; let client = default_client()?;
let dns_manager = Manager::new();
let systemd_access = daemon::booted(); let systemd_access = daemon::booted();
let mainconfig = { let mainconfig = {
let file = match FILE_MODE.open(flags.config).await { let file = match FILE_MODE.open(flags.config).await {
@ -151,7 +169,6 @@ async fn racme(flags: Arguments) -> Result<(), Error> {
} }
} }
let challengepath = mainconfig.http_challenge_path.and_then(|path| PathBuf::from_str(path.as_str()).ok()); let challengepath = mainconfig.http_challenge_path.and_then(|path| PathBuf::from_str(path.as_str()).ok());
let dnsserver = None;
unsafe { unsafe {
TokioScope::scope_and_collect(|scope| { TokioScope::scope_and_collect(|scope| {
@ -164,7 +181,8 @@ async fn racme(flags: Arguments) -> Result<(), Error> {
&restart_services, &restart_services,
certs.clone(), certs.clone(),
challengepath.clone(), challengepath.clone(),
dnsserver.clone(), dns_manager.clone(),
client.clone(),
))); )));
} else { } else {
error!("Could not process site {} because of previous errors", site.name) error!("Could not process site {} because of previous errors", site.name)
@ -180,9 +198,50 @@ async fn racme(flags: Arguments) -> Result<(), Error> {
Ok(()) Ok(())
} }
fn serialize_with_formatter<T: Serialize, F: Formatter>(value: &T, formatter: F) -> Result<String, Error> {
let mut store = Vec::with_capacity(2 ^ 10);
let mut serializer = serde_json::ser::Serializer::with_formatter(&mut store, formatter);
match value.serialize(&mut serializer) {
Ok(_) => {},
Err(error) => return Error::err(format!("Failed to Serialize the schema: {error}")),
}
Ok(unsafe { String::from_utf8_unchecked(store) })
}
async fn schema_generator() -> Result<(), Error> {
let formatter = PrettyFormatter::with_indent(&[b' '; 4]);
let mut schema_settings = SchemaSettings::default();
schema_settings.meta_schema = Some(DRAFT07.into());
let mut generator = SchemaGenerator::new(schema_settings);
let general_schema = serialize_with_formatter(&generator.root_schema_for::<GeneralConfig>(), formatter.clone())?;
match FILE_MODE_WRITE.clone().create_new(false).open("schema-general.json").await {
Ok(mut file) => {
match file.write(general_schema.as_bytes()).await {
Ok(_) => {},
Err(error) => return Error::err(format!("{error}")),
}
},
Err(error) => return Err(Error::from_display(error)),
};
let site_schema = serialize_with_formatter(&generator.root_schema_for::<SiteConfig>(), formatter.clone())?;
match FILE_MODE_WRITE.clone().create_new(false).open("schema-site.json").await {
Ok(mut file) => {
match file.write(site_schema.as_bytes()).await {
Ok(_) => {},
Err(error) => return Error::err(format!("{error}")),
}
},
Err(error) => return Err(Error::from_display(error)),
};
Ok(())
}
fn main() { fn main() {
log_init(); log_init();
if !check_permissions() { let args = Arguments::parse();
if args.subcommands.is_none() && !check_permissions() {
error!( 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)" "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)"
); );
@ -195,7 +254,10 @@ fn main() {
exit(2) exit(2)
}, },
}; };
let result = runtime.block_on(racme(Arguments::parse())); let result = match args.subcommands {
None => runtime.block_on(racme(args)),
Some(SubCommand::Schema) => runtime.block_on(schema_generator()),
};
runtime.shutdown_timeout(Duration::from_secs(1)); runtime.shutdown_timeout(Duration::from_secs(1));
if let Err(error) = result { if let Err(error) = result {
error!("{error}"); error!("{error}");

Datei anzeigen

@ -25,12 +25,12 @@ use crate::{
load_privkey, load_privkey,
prelude::*, prelude::*,
types::{ types::{
config::{ self,
CA, config::CA,
Dns,
},
cryptography::Algorithm, cryptography::Algorithm,
dns::Manager,
structs::{ structs::{
Error,
ProcessorArgs, ProcessorArgs,
San, San,
}, },
@ -45,6 +45,7 @@ use acme2_eab::{
Account, Account,
AccountBuilder, AccountBuilder,
Authorization, Authorization,
Challenge,
ChallengeStatus, ChallengeStatus,
Csr, Csr,
Directory, Directory,
@ -290,7 +291,7 @@ pub async fn site(args: ProcessorArgs<'_>) {
unsafe { unsafe {
TokioScope::scope_and_collect(|scope|{ TokioScope::scope_and_collect(|scope|{
for authorization in authorizations { for authorization in authorizations {
scope.spawn(auth(authorization, args.challenge_dir(), args.dnsserver())); scope.spawn(auth(authorization, args.challenge_dir(), args.dns_manager()));
} }
}) })
}, },
@ -371,12 +372,48 @@ pub async fn site(args: ProcessorArgs<'_>) {
} }
} }
pub async fn auth(auth: Authorization, challenge_dir: Option<PathBuf>, dnsserver: Option<Dns>) { async fn dns_auth(mut dns_challenge: Challenge, manager: Manager) -> types::Result<()> {
if let Some(_dnschallenge) = auth.get_challenge("dns-01") { let token = match dns_challenge.token {
if let Some(_dnsserver) = dnsserver { Some(ref token) => token.clone(),
} else { None => {
debug!("DNS-01 is disabled") return Error::err("Failed to get valid Token");
},
};
let value = match dns_challenge.key_authorization_encoded() {
Ok(Some(ref value)) => value.clone(),
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 {
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);
match challenge_result {
Ok(_) => {
info!("Challenge: {token} was correctly validated");
Ok(())
},
Err(error) => Error::err(format!("Failed to validate the challenge: {token}: {error}")),
} }
} else {
Error::err("Failed to set record")
}
}
pub async fn auth(auth: Authorization, challenge_dir: Option<PathBuf>, manager: Manager) {
if let Some(dns_challenge) = auth.get_challenge("dns-01") {
match dns_auth(dns_challenge, manager).await {
Ok(()) => return,
Err(error) => {
error!("Failed to authenticate via DNS: {error}");
},
}
} else {
debug!("DNS-01 is disabled")
} }
if !auth.wildcard.unwrap_or(false) { if !auth.wildcard.unwrap_or(false) {
if let Some(mut challenge) = auth.get_challenge("http-01") { if let Some(mut challenge) = auth.get_challenge("http-01") {

Datei anzeigen

@ -6,6 +6,7 @@ use crate::{
Algorithm, Algorithm,
Strength, Strength,
}, },
dns::DnsBuilder,
structs::Error, structs::Error,
}, },
}; };
@ -15,6 +16,7 @@ use openssl::pkey::{
PKey, PKey,
Private, Private,
}; };
use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use std::{ use std::{
collections::HashMap, collections::HashMap,
@ -22,7 +24,8 @@ use std::{
}; };
#[macro_rules_derive(DefDer)] #[macro_rules_derive(DefDer)]
#[derive(Deserialize)] #[derive(Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct GeneralConfig { pub struct GeneralConfig {
#[serde(default = "GeneralConfig::default_accounts")] #[serde(default = "GeneralConfig::default_accounts")]
pub accounts_path: String, pub accounts_path: String,
@ -31,7 +34,7 @@ pub struct GeneralConfig {
#[serde(default = "GeneralConfig::default_challenge")] #[serde(default = "GeneralConfig::default_challenge")]
pub http_challenge_path: Option<String>, pub http_challenge_path: Option<String>,
#[serde(default = "GeneralConfig::default_dns")] #[serde(default = "GeneralConfig::default_dns")]
pub dns: Option<Dns>, pub dns: HashMap<String, DnsBuilder>,
#[serde(default = "GeneralConfig::default_certificates")] #[serde(default = "GeneralConfig::default_certificates")]
pub certificates_path: String, pub certificates_path: String,
#[serde(default = "GeneralConfig::default_cas")] #[serde(default = "GeneralConfig::default_cas")]
@ -55,8 +58,8 @@ impl GeneralConfig {
} }
#[inline] #[inline]
pub(super) fn default_dns() -> Option<Dns> { pub(super) fn default_dns() -> HashMap<String, DnsBuilder> {
None HashMap::new()
} }
#[inline] #[inline]
@ -70,12 +73,10 @@ impl GeneralConfig {
} }
} }
#[macro_rules_derive(DefDer)]
#[derive(Deserialize)]
pub struct Dns;
#[macro_rules_derive(DefDer)] #[macro_rules_derive(DefDer)]
#[derive(Deserialize)] #[derive(Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Eab { pub struct Eab {
#[serde(rename = "eab_token", alias = "id")] #[serde(rename = "eab_token", alias = "id")]
pub token: String, pub token: String,
@ -84,19 +85,22 @@ pub struct Eab {
} }
impl Eab { impl Eab {
pub fn key(&self) -> Result<PKey<Private>, Error> { pub fn key(&self) -> super::Result<PKey<Private>> {
let decoded = &match_error!(data_encoding::BASE64URL_NOPAD.decode(self.key.as_bytes())=>Err(error)-> "Failed to decode the HMAC key for the eab_key: {error}", Error::err("Failed to decode eab_key".into())); let decoded = &match_error!(data_encoding::BASE64URL_NOPAD.decode(self.key.as_bytes())=>Err(error)-> "Failed to decode the HMAC key for the eab_key: {error}", Error::err("Failed to decode eab_key"));
PKey::hmac(decoded).map_err(|error| Error::new(format!("Failed to parse the private key: {error}"))) PKey::hmac(decoded).map_err(|error| Error::new(format!("Failed to parse the private key: {error}")))
} }
} }
#[macro_rules_derive(DefDer)] #[macro_rules_derive(DefDer)]
#[derive(Deserialize)] #[derive(Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CA { pub struct CA {
/// Url for the directory /// Url for the directory
#[schemars(transform=crate::utils::schema::uri_transform)]
pub directory: String, pub directory: String,
/// Email addresses for the CA to contact the user /// Email addresses for the CA to contact the user
#[schemars(transform=crate::utils::schema::email_transform)]
pub email_addresses: Option<VString>, pub email_addresses: Option<VString>,
#[serde(flatten, default)] #[serde(flatten, default)]
@ -104,6 +108,7 @@ pub struct CA {
/// Amount of days the certificate is renewed before the Certificate is outdated /// Amount of days the certificate is renewed before the Certificate is outdated
/// TODO: give to processor /// TODO: give to processor
#[schemars(range(min = 1, max = 90))]
#[serde(default = "CA::default_renew")] #[serde(default = "CA::default_renew")]
pub renew_before: u32, pub renew_before: u32,
@ -115,18 +120,11 @@ impl CA {
fn default_renew() -> u32 { fn default_renew() -> u32 {
7 7
} }
fn default_owner() -> String {
"root".into()
}
fn default_group() -> String {
"root".into()
}
} }
#[macro_rules_derive(DefDer)] #[macro_rules_derive(DefDer)]
#[derive(Deserialize, Default)] #[derive(Deserialize, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SiteConfig { pub struct SiteConfig {
/// The Configured Certificate Authority /// The Configured Certificate Authority
pub ca: String, pub ca: String,
@ -140,6 +138,7 @@ pub struct SiteConfig {
/// EmailAdresses that this Certificate is valid for /// EmailAdresses that this Certificate is valid for
#[serde(default)] #[serde(default)]
#[schemars(transform=crate::utils::schema::email_transform)]
pub emails: VString, pub emails: VString,
/// The systemd services are reloaded /// The systemd services are reloaded

Datei anzeigen

@ -1,11 +1,14 @@
use crate::{
consts::RsaStrength,
prelude::*,
};
use macro_rules_attribute::macro_rules_derive; use macro_rules_attribute::macro_rules_derive;
use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use crate::prelude::*;
#[macro_rules_derive(DefDer)] #[macro_rules_derive(DefDer)]
#[derive(Copy, Deserialize, Default)] #[derive(Copy, Deserialize, Default, JsonSchema)]
pub enum Algorithm { pub enum Algorithm {
Rsa, Rsa,
Brainpool, Brainpool,
@ -16,7 +19,8 @@ pub enum Algorithm {
#[macro_rules_derive(DefDer)] #[macro_rules_derive(DefDer)]
#[derive(Copy, Deserialize, Default)] #[derive(Copy, Deserialize, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub enum Strength { pub enum Strength {
Weak, Weak,
Middle, Middle,
@ -26,6 +30,10 @@ pub enum Strength {
impl Strength { impl Strength {
pub fn rsabits(self) -> u32 { pub fn rsabits(self) -> u32 {
self as u32 match self {
Self::Weak => RsaStrength::Weak as u32,
Self::Middle => RsaStrength::Middle as u32,
Self::Strong => RsaStrength::Strong as u32,
}
} }
} }

36
src/types/dns/dnsupdate.rs Normale Datei
Datei anzeigen

@ -0,0 +1,36 @@
use macro_rules_attribute::macro_rules_derive;
use schemars::JsonSchema;
use serde::Deserialize;
use crate::{
macros::DefDer,
types::traits::{
DnsHandler,
DnsToken,
},
};
#[macro_rules_derive(DefDer)]
#[derive(Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct DNSUpdateClientOptions {}
#[macro_rules_derive(DefDer)]
pub(super) struct DnsUpdateHandler {}
impl DnsHandler for DnsUpdateHandler {
async fn set_record(&self, _domain: String, _content: String) -> crate::types::BoxedResult<dyn DnsToken> {
Ok(Box::pin(DnsUpdateToken {}))
}
}
#[macro_rules_derive(DefDer)]
struct DnsUpdateToken {}
impl Drop for DnsUpdateToken {
fn drop(&mut self) {
todo!()
}
}
impl DnsToken for DnsUpdateToken {}
unsafe impl Send for DnsUpdateToken {}

109
src/types/dns/mod.rs Normale Datei
Datei anzeigen

@ -0,0 +1,109 @@
pub(super) mod dnsupdate;
pub(super) mod pdns;
use crate::{
macros::DefDer,
types::{
dns::{
dnsupdate::{
DNSUpdateClientOptions,
DnsUpdateHandler,
},
pdns::{
PdnsClientOptions,
PdnsHandler,
},
},
structs::Error,
traits::{
DnsHandler,
DnsToken,
},
},
};
use log::*;
use macro_rules_attribute::macro_rules_derive;
use schemars::JsonSchema;
use serde::Deserialize;
use std::{
collections::HashMap,
pin::Pin,
sync::Arc,
};
use tokio::sync::Mutex;
#[macro_rules_derive(DefDer)]
pub struct Manager(Arc<Mutex<InnerManager>>);
impl Manager {
pub fn new() -> Self {
Self(Arc::new(Mutex::new(InnerManager {
servers: HashMap::new(),
})))
}
pub async fn set_record(&self, domain: String, value: String) -> Option<Pin<Box<dyn DnsToken + '_>>> {
let mut tld = domain.clone();
if !tld.ends_with('.') {
tld.push('.');
}
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();
if matched.len() > best_match_length {
best_match_domain = matched;
best_match_length = matched.len();
}
}
}
if best_match_length == 0 {
return None;
}
let handler = guard.servers.get(best_match_domain).unwrap();
match handler.set_record(domain, value).await {
Ok(token) => Some(token),
Err(error) => {
error!("Failed to set the DNS Record in the backend: {error}");
None
},
}
}
}
unsafe impl Send for Manager {}
#[derive(Debug)]
struct InnerManager {
servers: HashMap<String, Dns>,
}
#[macro_rules_derive(DefDer)]
#[derive(Deserialize, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum DnsBuilder {
PowerDNS(PdnsClientOptions),
DNSUpdate(DNSUpdateClientOptions),
#[default]
None,
}
#[macro_rules_derive(DefDer)]
enum Dns {
PowerDNS(PdnsHandler),
DNSUpdate(DnsUpdateHandler),
None,
}
impl Dns {
pub async fn set_record(&self, domain: String, content: String) -> crate::types::BoxedResult<dyn 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,
Dns::None => Error::err("Not Implemented"),
}
}
}

205
src/types/dns/pdns.rs Normale Datei
Datei anzeigen

@ -0,0 +1,205 @@
use std::{
pin::Pin,
time::{
SystemTime,
UNIX_EPOCH,
},
};
use derive_new::new;
use log::*;
use macro_rules_attribute::macro_rules_derive;
use reqwest::{
RequestBuilder,
StatusCode,
};
use schemars::JsonSchema;
use serde::{
Deserialize,
Serialize,
};
use tokio::runtime::Handle;
use crate::{
macros::DefDer,
types::{
structs::Error,
traits::{
DnsHandler,
DnsToken,
},
},
};
#[macro_rules_derive(DefDer)]
#[derive(Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PdnsClientOptions {
api_key: String,
server: String,
#[serde(default = "PdnsClientOptions::default_serverid")]
server_id: String,
}
impl PdnsClientOptions {
fn default_serverid() -> String {
"localhost".into()
}
}
#[macro_rules_derive(DefDer)]
pub(super) struct PdnsHandler {
client: reqwest::Client,
server: String,
api_key: String,
server_id: String,
zone: String,
}
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());
match base_request
.try_clone()
.unwrap()
.json(&RecordUpdate::new(vec![
RRSet::new(
domain,
"TXT",
ChangeType::Replace {
records: vec![Record::new(content)],
comments: vec![Comment::new("ACME entry", "")],
},
),
]))
.send()
.await
{
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}"),
};
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),
}))
}
}
/// Token that deletes the Record when its no longer needed
#[derive(Debug)]
pub(super) struct PdnsToken {
builder: RequestBuilder,
}
impl PdnsToken {
fn new(builder: RequestBuilder) -> Pin<Box<Self>> {
Box::pin(Self {
builder,
})
}
}
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()) {
Ok(response) => {
if response.status() != StatusCode::NO_CONTENT {
let status = response.status();
match handle.block_on(response.json::<PdnsError>()) {
Ok(error) => error!("Failed to delete the Record({status}): {error}"),
Err(error) => error!("Failed to parse the Error Response({status}): {error}"),
}
}
},
Err(error) => error!("Failed to delete the Record: {error}"),
};
}
}
unsafe impl Send for PdnsToken {}
impl DnsToken for PdnsToken {}
#[macro_rules_derive(DefDer)]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PdnsError {
pub error: String,
#[serde(default)]
pub errors: Vec<String>,
}
#[macro_rules_derive(DefDer)]
#[derive(Serialize, new)]
struct Comment {
#[new(into)]
content: String,
#[new(into)]
account: String,
#[new(value = "Comment::unix_epoch()")]
modified_at: u64,
}
impl Comment {
#[inline]
fn unix_epoch() -> u64 {
SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs()
}
}
#[macro_rules_derive(DefDer)]
#[derive(Serialize, new)]
struct Record {
content: String,
#[new(value = "false")]
disabled: bool,
}
#[macro_rules_derive(DefDer)]
#[derive(Serialize)]
#[serde(tag = "changetype", rename_all = "UPPERCASE")]
enum ChangeType {
Replace {
records: Vec<Record>,
comments: Vec<Comment>,
},
Delete,
}
#[macro_rules_derive(DefDer)]
#[derive(Serialize, new)]
struct RRSet {
name: String,
#[new(into)]
r#type: String,
#[serde(flatten)]
changetype: ChangeType,
}
#[macro_rules_derive(DefDer)]
#[derive(Serialize, new)]
struct RecordUpdate {
rrset: Vec<RRSet>,
}

Datei anzeigen

@ -6,8 +6,9 @@ use std::{
use acme2_eab::Identifier; use acme2_eab::Identifier;
use openssl::x509::GeneralName; use openssl::x509::GeneralName;
use super::{ use crate::types::{
config::GeneralConfig, config::GeneralConfig,
dns::pdns::PdnsError,
structs::{ structs::{
Error, Error,
San, San,
@ -78,6 +79,30 @@ impl From<San> for Identifier {
impl Display for Error { impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0) f.write_str(&self.message)
}
}
impl Display for PdnsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.error)?;
if !self.errors.is_empty() {
f.write_str("(")?;
let mut iter = self.errors.iter();
let mut error = iter.next().unwrap();
loop {
f.write_str(error)?;
match iter.next() {
Some(err) => {
error = err;
f.write_str(", ")?;
},
None => break,
}
}
f.write_str(")")?;
}
Ok(())
} }
} }

Datei anzeigen

@ -1,8 +1,14 @@
use std::collections::HashSet; use std::{
collections::HashSet,
pin::Pin,
};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::types::traits::DnsToken;
pub mod config; pub mod config;
pub mod cryptography; pub mod cryptography;
pub mod dns;
mod foreign_impl; mod foreign_impl;
pub mod structs; pub mod structs;
pub mod traits; pub mod traits;
@ -12,3 +18,8 @@ pub type VString = Vec<String>;
/// Alias for an Safe Hashset /// Alias for an Safe Hashset
pub type SafeSet<T> = Mutex<HashSet<T>>; 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

@ -10,8 +10,13 @@ use std::{
}; };
use acme2_eab::Account; use acme2_eab::Account;
use clap::Parser; use clap::{
Parser,
Subcommand,
};
use derive_new::new;
use macro_rules_attribute::macro_rules_derive; use macro_rules_attribute::macro_rules_derive;
use reqwest::Client;
use tokio::sync::MutexGuard; use tokio::sync::MutexGuard;
use crate::{ use crate::{
@ -23,28 +28,39 @@ use crate::{
Algorithm, Algorithm,
Strength, Strength,
}, },
dns::Manager,
}, },
}; };
use crate::types::config::Dns;
#[macro_rules_derive(DefDer)] #[macro_rules_derive(DefDer)]
#[derive(Parser)] #[derive(Parser)]
pub struct Arguments { pub struct Arguments {
pub config: String, pub config: String,
#[command(subcommand)]
pub subcommands: Option<SubCommand>,
} }
#[macro_rules_derive(DefDer)] #[macro_rules_derive(DefDer)]
#[derive(Subcommand)]
pub enum SubCommand {
Schema,
}
#[macro_rules_derive(DefDer)]
#[allow(clippy::too_many_arguments)]
#[derive(new)]
pub struct ProcessorArgs<'a> { pub struct ProcessorArgs<'a> {
site: SiteConfig, site: SiteConfig,
account: Arc<Account>, account: Arc<Account>,
reload_services: &'a SafeSet<String>, reload_services: &'a SafeSet<String>,
restart_services: &'a SafeSet<String>, restart_services: &'a SafeSet<String>,
certificate_dir: PathBuf, certificate_dir: PathBuf,
#[new(value = "7")]
refresh_time: u32, refresh_time: u32,
challenge_dir: Option<PathBuf>, challenge_dir: Option<PathBuf>,
dnsserver: Option<Dns>, dns_manager: Manager,
client: Client,
} }
impl<'a: 'b, 'b> ProcessorArgs<'a> { impl<'a: 'b, 'b> ProcessorArgs<'a> {
@ -70,32 +86,13 @@ impl<'a: 'b, 'b> ProcessorArgs<'a> {
attr_function!(pub challenge_dir => Option<PathBuf>); attr_function!(pub challenge_dir => Option<PathBuf>);
attr_function!(pub dnsserver => Option<Dns>);
attr_function!(pub owner site => String); attr_function!(pub owner site => String);
attr_function!(pub group site => String); attr_function!(pub group site => String);
pub fn new( attr_function!(pub client => Client);
site: SiteConfig,
account: Arc<Account>, attr_function!(pub dns_manager => Manager);
reload_services: &'a SafeSet<String>,
restart_services: &'a SafeSet<String>,
certificate_dir: PathBuf,
http_challenge_dir: Option<PathBuf>,
dnsserver: Option<Dns>,
) -> Self {
ProcessorArgs {
site,
account,
reload_services,
restart_services,
certificate_dir,
refresh_time: 7,
challenge_dir: http_challenge_dir,
dnsserver,
}
}
pub fn account(&self) -> Arc<Account> { pub fn account(&self) -> Arc<Account> {
Arc::clone(&self.account) Arc::clone(&self.account)
@ -133,26 +130,26 @@ pub enum San {
} }
#[macro_rules_derive(DefDer)] #[macro_rules_derive(DefDer)]
pub struct Error(pub(super) String); #[derive(derive_new::new)]
pub struct Error {
pub(super) message: String,
}
impl Error { impl Error {
#[inline] #[inline]
#[allow(unused)]
pub fn from_display<T: Display>(input: T) -> Error { pub fn from_display<T: Display>(input: T) -> Error {
Error::new(format!("{input}")) Error::new(format!("{input}"))
} }
#[inline] #[inline]
#[allow(unused)]
pub fn from_debug<T: Debug>(input: T) -> Error { pub fn from_debug<T: Debug>(input: T) -> Error {
Error::new(format!("{input:?}")) Error::new(format!("{input:?}"))
} }
#[inline] #[inline]
pub fn err<T>(message: String) -> Result<T, Self> { pub fn err<T, M: Into<String>>(message: M) -> Result<T, Self> {
Err(Self::new(message)) Err(Self::new(message.into()))
}
#[inline]
pub fn new(message: String) -> Self {
Self(message)
} }
} }

Datei anzeigen

@ -8,6 +8,18 @@ use crate::consts::{
SECP_STRONG, SECP_STRONG,
SECP_WEAK, SECP_WEAK,
}; };
use crate::{
types,
types::{
cryptography::{
Algorithm,
Strength,
},
structs::San,
},
};
use log::*; use log::*;
use openssl::{ use openssl::{
asn1::Asn1Time, asn1::Asn1Time,
@ -24,14 +36,6 @@ use tokio::{
io::AsyncReadExt, io::AsyncReadExt,
}; };
use super::{
cryptography::{
Algorithm,
Strength,
},
structs::San,
};
pub trait FromFile: Default + DeserializeOwned { pub trait FromFile: Default + DeserializeOwned {
async fn from_file(file: File) -> Self; async fn from_file(file: File) -> Self;
} }
@ -115,3 +119,10 @@ impl MatchX509 for X509 {
config_san.difference(&cert_san).count() == 0 config_san.difference(&cert_san).count() == 0
} }
} }
#[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>;
}

Datei anzeigen

@ -20,6 +20,7 @@ use crate::{
use caps::{ use caps::{
CapSet, CapSet,
has_cap, has_cap,
read,
}; };
use log::*; use log::*;
use openssl::{ use openssl::{
@ -37,6 +38,8 @@ use openssl::{
}, },
}; };
const CAPABILITY_SET: CapSet = CapSet::Permitted;
pub fn prefix_emails(input: Vec<String>) -> Vec<String> { pub fn prefix_emails(input: Vec<String>) -> Vec<String> {
let mut output = Vec::with_capacity(input.len()); let mut output = Vec::with_capacity(input.len());
for mut addr in input { for mut addr in input {
@ -105,8 +108,27 @@ pub fn string_to_cn(name: String) -> X509Name {
} }
pub fn check_permissions() -> bool { pub fn check_permissions() -> bool {
has_cap(None, CapSet::Ambient, caps::Capability::CAP_CHOWN).unwrap_or(false) && let caps = has_cap(None, CAPABILITY_SET, caps::Capability::CAP_CHOWN).unwrap_or(false) &&
has_cap(None, CapSet::Ambient, caps::Capability::CAP_FOWNER).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}")
},
}
}
}
caps
} }
pub fn get_uid_gid(username: String, group: String) -> (Option<u32>, Option<u32>) { pub fn get_uid_gid(username: String, group: String) -> (Option<u32>, Option<u32>) {
@ -156,3 +178,59 @@ pub fn get_uid_gid(username: String, group: String) -> (Option<u32>, Option<u32>
}; };
(uid, gid) (uid, gid)
} }
pub mod schema {
use schemars::Schema;
use serde_json::{
Map,
Value,
};
fn is_array(val: &Value) -> bool {
if let Some(typ) = val.as_str() {
typ == "array"
} else if let Some(typ) = val.as_array() {
for typvariant in typ {
if is_array(typvariant) {
return true;
}
}
false
} else {
false
}
}
pub fn type_transform(schema: &mut Schema, typ: &'static str) {
let typval = Value::String(typ.into());
if let Some(val) = schema.get("type") {
if is_array(val) {
if let Some(items) = schema.get("items") {
let mut items = items.to_owned();
items.as_object_mut().unwrap().insert("type".to_string(), typval);
schema.insert("items".to_owned(), items);
} else {
schema.insert(
"items".to_owned(),
Value::Object({
let mut items = Map::with_capacity(1);
items.insert("type".to_owned(), typval);
items
}),
);
}
return;
}
}
schema.insert("type".to_string(), typval);
}
pub fn email_transform(schema: &mut Schema) {
type_transform(schema, "email");
}
pub fn uri_transform(schema: &mut Schema) {
type_transform(schema, "uri");
}
}