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.'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 = []
[dependencies]
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.caps]
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
#![allow(dead_code)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::identity_op)]
#![allow(refining_impl_trait)]
#![allow(clippy::collapsible_if)]
#![allow(dead_code)]
pub(crate) mod consts;
pub(crate) mod macros;
@ -18,11 +20,15 @@ use crate::{
GeneralConfig,
SiteConfig,
},
dns::Manager,
structs::{
Arguments,
Error,
ProcessorArgs,
SubCommand,
},
},
utils::check_permissions,
};
use acme2_eab::Directory;
use async_scoped::TokioScope;
@ -38,6 +44,16 @@ use openssl::{
},
};
use reqwest::tls::Version;
use schemars::{
SchemaGenerator,
consts::meta_schemas::DRAFT07,
generate::SchemaSettings,
};
use serde::Serialize;
use serde_json::ser::{
Formatter,
PrettyFormatter,
};
use std::{
collections::{
HashMap,
@ -57,15 +73,16 @@ use tokio::{
create_dir_all,
read_dir,
},
io::AsyncReadExt,
io::{
AsyncReadExt,
AsyncWriteExt,
},
sync::Mutex,
};
use tokio_stream::{
StreamExt,
wrappers::ReadDirStream,
};
use types::structs::Error;
use utils::check_permissions;
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> {
let client = default_client()?;
let dns_manager = Manager::new();
let systemd_access = daemon::booted();
let mainconfig = {
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 dnsserver = None;
unsafe {
TokioScope::scope_and_collect(|scope| {
@ -164,7 +181,8 @@ async fn racme(flags: Arguments) -> Result<(), Error> {
&restart_services,
certs.clone(),
challengepath.clone(),
dnsserver.clone(),
dns_manager.clone(),
client.clone(),
)));
} else {
error!("Could not process site {} because of previous errors", site.name)
@ -180,9 +198,50 @@ async fn racme(flags: Arguments) -> Result<(), Error> {
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() {
log_init();
if !check_permissions() {
let args = Arguments::parse();
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)"
);
@ -195,7 +254,10 @@ fn main() {
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));
if let Err(error) = result {
error!("{error}");

Datei anzeigen

@ -25,12 +25,12 @@ use crate::{
load_privkey,
prelude::*,
types::{
config::{
CA,
Dns,
},
self,
config::CA,
cryptography::Algorithm,
dns::Manager,
structs::{
Error,
ProcessorArgs,
San,
},
@ -45,6 +45,7 @@ use acme2_eab::{
Account,
AccountBuilder,
Authorization,
Challenge,
ChallengeStatus,
Csr,
Directory,
@ -290,7 +291,7 @@ pub async fn site(args: ProcessorArgs<'_>) {
unsafe {
TokioScope::scope_and_collect(|scope|{
for authorization in authorizations {
scope.spawn(auth(authorization, args.challenge_dir(), args.dnsserver()));
scope.spawn(auth(authorization, args.challenge_dir(), args.dns_manager()));
}
})
},
@ -371,13 +372,49 @@ pub async fn site(args: ProcessorArgs<'_>) {
}
}
pub async fn auth(auth: Authorization, challenge_dir: Option<PathBuf>, dnsserver: Option<Dns>) {
if let Some(_dnschallenge) = auth.get_challenge("dns-01") {
if let Some(_dnsserver) = dnsserver {
async fn dns_auth(mut dns_challenge: Challenge, manager: Manager) -> types::Result<()> {
let token = match dns_challenge.token {
Some(ref token) => token.clone(),
None => {
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 let Some(mut challenge) = auth.get_challenge("http-01") {
trace!("CA has an http-challenge");

Datei anzeigen

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

Datei anzeigen

@ -1,11 +1,14 @@
use crate::{
consts::RsaStrength,
prelude::*,
};
use macro_rules_attribute::macro_rules_derive;
use schemars::JsonSchema;
use serde::Deserialize;
use crate::prelude::*;
#[macro_rules_derive(DefDer)]
#[derive(Copy, Deserialize, Default)]
#[derive(Copy, Deserialize, Default, JsonSchema)]
pub enum Algorithm {
Rsa,
Brainpool,
@ -16,7 +19,8 @@ pub enum Algorithm {
#[macro_rules_derive(DefDer)]
#[derive(Copy, Deserialize, Default)]
#[derive(Copy, Deserialize, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub enum Strength {
Weak,
Middle,
@ -26,6 +30,10 @@ pub enum Strength {
impl Strength {
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 openssl::x509::GeneralName;
use super::{
use crate::types::{
config::GeneralConfig,
dns::pdns::PdnsError,
structs::{
Error,
San,
@ -78,6 +79,30 @@ impl From<San> for Identifier {
impl Display for Error {
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 crate::types::traits::DnsToken;
pub mod config;
pub mod cryptography;
pub mod dns;
mod foreign_impl;
pub mod structs;
pub mod traits;
@ -12,3 +18,8 @@ pub type VString = Vec<String>;
/// Alias for an Safe Hashset
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 clap::Parser;
use clap::{
Parser,
Subcommand,
};
use derive_new::new;
use macro_rules_attribute::macro_rules_derive;
use reqwest::Client;
use tokio::sync::MutexGuard;
use crate::{
@ -23,28 +28,39 @@ use crate::{
Algorithm,
Strength,
},
dns::Manager,
},
};
use crate::types::config::Dns;
#[macro_rules_derive(DefDer)]
#[derive(Parser)]
pub struct Arguments {
pub config: String,
#[command(subcommand)]
pub subcommands: Option<SubCommand>,
}
#[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> {
site: SiteConfig,
account: Arc<Account>,
reload_services: &'a SafeSet<String>,
restart_services: &'a SafeSet<String>,
certificate_dir: PathBuf,
#[new(value = "7")]
refresh_time: u32,
challenge_dir: Option<PathBuf>,
dnsserver: Option<Dns>,
dns_manager: Manager,
client: Client,
}
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 dnsserver => Option<Dns>);
attr_function!(pub owner site => String);
attr_function!(pub group site => String);
pub fn new(
site: SiteConfig,
account: Arc<Account>,
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,
}
}
attr_function!(pub client => Client);
attr_function!(pub dns_manager => Manager);
pub fn account(&self) -> Arc<Account> {
Arc::clone(&self.account)
@ -133,26 +130,26 @@ pub enum San {
}
#[macro_rules_derive(DefDer)]
pub struct Error(pub(super) String);
#[derive(derive_new::new)]
pub struct Error {
pub(super) message: String,
}
impl Error {
#[inline]
#[allow(unused)]
pub fn from_display<T: Display>(input: T) -> Error {
Error::new(format!("{input}"))
}
#[inline]
#[allow(unused)]
pub fn from_debug<T: Debug>(input: T) -> Error {
Error::new(format!("{input:?}"))
}
#[inline]
pub fn err<T>(message: String) -> Result<T, Self> {
Err(Self::new(message))
}
#[inline]
pub fn new(message: String) -> Self {
Self(message)
pub fn err<T, M: Into<String>>(message: M) -> Result<T, Self> {
Err(Self::new(message.into()))
}
}

Datei anzeigen

@ -8,6 +8,18 @@ use crate::consts::{
SECP_STRONG,
SECP_WEAK,
};
use crate::{
types,
types::{
cryptography::{
Algorithm,
Strength,
},
structs::San,
},
};
use log::*;
use openssl::{
asn1::Asn1Time,
@ -24,14 +36,6 @@ use tokio::{
io::AsyncReadExt,
};
use super::{
cryptography::{
Algorithm,
Strength,
},
structs::San,
};
pub trait FromFile: Default + DeserializeOwned {
async fn from_file(file: File) -> Self;
}
@ -115,3 +119,10 @@ impl MatchX509 for X509 {
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::{
CapSet,
has_cap,
read,
};
use log::*;
use openssl::{
@ -37,6 +38,8 @@ use openssl::{
},
};
const CAPABILITY_SET: CapSet = CapSet::Permitted;
pub fn prefix_emails(input: Vec<String>) -> Vec<String> {
let mut output = Vec::with_capacity(input.len());
for mut addr in input {
@ -105,8 +108,27 @@ pub fn string_to_cn(name: String) -> X509Name {
}
pub fn check_permissions() -> bool {
has_cap(None, CapSet::Ambient, caps::Capability::CAP_CHOWN).unwrap_or(false) &&
has_cap(None, CapSet::Ambient, caps::Capability::CAP_FOWNER).unwrap_or(false)
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}")
},
}
}
}
caps
}
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)
}
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");
}
}