diff --git a/CHANGELOG.md b/CHANGELOG.md index 686bd10..624d0e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - ACMEd now displays a warning when the server indicates an error in an order or an authorization. - A configuration file can now include several other files. - Hooks have access to environment variables. -- In the configuration, certificates can define environment variables for the hooks. +- In the configuration, certificates and domains can define environment variables for the hooks. - tacd is now able to listen on a unix socket. diff --git a/acmed/src/acme_proto.rs b/acmed/src/acme_proto.rs index be0d866..d989b26 100644 --- a/acmed/src/acme_proto.rs +++ b/acmed/src/acme_proto.rs @@ -70,7 +70,7 @@ pub fn request_certificate(cert: &Certificate, root_certs: &[String]) -> Result< let domains = cert .domains .iter() - .map(|d| d.0.to_owned()) + .map(|d| d.dns.to_owned()) .collect::>(); let mut hook_datas = vec![]; diff --git a/acmed/src/acme_proto/certificate.rs b/acmed/src/acme_proto/certificate.rs index debd6f4..197354f 100644 --- a/acmed/src/acme_proto/certificate.rs +++ b/acmed/src/acme_proto/certificate.rs @@ -46,8 +46,8 @@ pub fn generate_csr( builder.set_pubkey(pub_key)?; let ctx = builder.x509v3_context(None); let mut san = SubjectAlternativeName::new(); - for name in cert.domains.iter().map(|d| d.0.to_owned()) { - san.dns(&name); + for c in cert.domains.iter() { + san.dns(&c.dns); } let san = san.build(&ctx)?; let mut ext_stack = Stack::new()?; diff --git a/acmed/src/certificate.rs b/acmed/src/certificate.rs index 620ed1e..7074bd9 100644 --- a/acmed/src/certificate.rs +++ b/acmed/src/certificate.rs @@ -1,6 +1,6 @@ use crate::acme_proto::Challenge; -use crate::config::{Account, HookType}; -use crate::hooks::{self, ChallengeHookData, Hook, PostOperationHookData}; +use crate::config::{Account, Domain, HookType}; +use crate::hooks::{self, ChallengeHookData, Hook, HookEnvData, PostOperationHookData}; use crate::storage::{certificate_files_exists, get_certificate}; use acme_common::error::Error; use log::{debug, trace}; @@ -72,7 +72,7 @@ impl fmt::Display for Algorithm { #[derive(Debug)] pub struct Certificate { pub account: Account, - pub domains: Vec<(String, Challenge)>, + pub domains: Vec, pub algo: Algorithm, pub kp_reuse: bool, pub remote_url: String, @@ -102,7 +102,7 @@ impl fmt::Display for Certificate { let domains = self .domains .iter() - .map(|d| format!("{} ({})", d.0, d.1)) + .map(|d| format!("{} ({})", d.dns, d.challenge)) .collect::>() .join(", "); write!( @@ -125,9 +125,10 @@ Hooks: {hooks}", impl Certificate { pub fn get_domain_challenge(&self, domain_name: &str) -> Result { let domain_name = domain_name.to_string(); - for (domain, challenge) in self.domains.iter() { - if *domain == domain_name { - return Ok((*challenge).to_owned()); + for d in self.domains.iter() { + if d.dns == domain_name { + let c = Challenge::from_str(&d.challenge)?; + return Ok(c); } } let msg = format!("{}: domain name not found", domain_name); @@ -156,7 +157,7 @@ impl Certificate { let req_names = self .domains .iter() - .map(|v| v.0.to_owned()) + .map(|v| v.dns.to_owned()) .collect::>(); let has_miss = req_names.difference(&cert_names).count() != 0; if has_miss { @@ -198,7 +199,7 @@ impl Certificate { domain: &str, ) -> Result<(ChallengeHookData, HookType), Error> { let challenge = self.get_domain_challenge(domain)?; - let hook_data = ChallengeHookData { + let mut hook_data = ChallengeHookData { challenge: challenge.to_string(), domain: domain.to_string(), file_name: file_name.to_string(), @@ -206,6 +207,10 @@ impl Certificate { is_clean_hook: false, env: HashMap::new(), }; + hook_data.set_env(&self.env); + for d in self.domains.iter().filter(|d| d.dns == domain) { + hook_data.set_env(&d.env); + } let hook_type = match challenge { Challenge::Http01 => (HookType::ChallengeHttp01, HookType::ChallengeHttp01Clean), Challenge::Dns01 => (HookType::ChallengeDns01, HookType::ChallengeDns01Clean), @@ -230,15 +235,16 @@ impl Certificate { let domains = self .domains .iter() - .map(|d| format!("{} ({})", d.0, d.1)) + .map(|d| format!("{} ({})", d.dns, d.challenge)) .collect::>(); - let hook_data = PostOperationHookData { + let mut hook_data = PostOperationHookData { domains, algorithm: self.algo.to_string(), status: status.to_string(), is_success, env: HashMap::new(), }; + hook_data.set_env(&self.env); hooks::call(self, &hook_data, HookType::PostOperation)?; Ok(()) } diff --git a/acmed/src/config.rs b/acmed/src/config.rs index 1d00aac..20b1e8c 100644 --- a/acmed/src/config.rs +++ b/acmed/src/config.rs @@ -298,6 +298,8 @@ impl Certificate { pub struct Domain { pub challenge: String, pub dns: String, + #[serde(default)] + pub env: HashMap, } impl fmt::Display for Domain { diff --git a/acmed/src/hooks.rs b/acmed/src/hooks.rs index 3343992..576a3b4 100644 --- a/acmed/src/hooks.rs +++ b/acmed/src/hooks.rs @@ -4,6 +4,7 @@ use acme_common::error::Error; use handlebars::Handlebars; use log::debug; use serde::Serialize; +use std::collections::hash_map::Iter; use std::collections::HashMap; use std::fs::File; use std::io::prelude::*; @@ -12,7 +13,8 @@ use std::process::{Command, Stdio}; use std::{env, fmt}; pub trait HookEnvData { - fn set_env(&mut self, cert: &Certificate); + fn set_env(&mut self, env: &HashMap); + fn get_env(&self) -> Iter; } fn deref(t: (&F, &G)) -> (F, G) @@ -26,11 +28,15 @@ where macro_rules! imple_hook_data_env { ($t: ty) => { impl HookEnvData for $t { - fn set_env(&mut self, cert: &Certificate) { - for (key, value) in env::vars().chain(cert.env.iter().map(deref)) { + fn set_env(&mut self, env: &HashMap) { + for (key, value) in env::vars().chain(env.iter().map(deref)) { self.env.insert(key, value); } } + + fn get_env(&self) -> Iter { + self.env.iter() + } } }; } @@ -99,13 +105,11 @@ macro_rules! get_hook_output { }}; } -fn call_single(cert: &Certificate, data: &T, hook: &Hook) -> Result<(), Error> +fn call_single(data: &T, hook: &Hook) -> Result<(), Error> where T: Clone + HookEnvData + Serialize, { debug!("Calling hook: {}", hook.name); - let mut data = (*data).clone(); - data.set_env(cert); let reg = Handlebars::new(); let mut v = vec![]; let args = match &hook.args { @@ -121,7 +125,7 @@ where debug!("Hook {}: cmd: {}", hook.name, hook.cmd); debug!("Hook {}: args: {:?}", hook.name, args); let mut cmd = Command::new(&hook.cmd) - .envs(cert.env.iter()) + .envs(data.get_env()) .args(args) .stdout(get_hook_output!(&hook.stdout, reg, &data)) .stderr(get_hook_output!(&hook.stderr, reg, &data)) @@ -154,7 +158,7 @@ where .iter() .filter(|h| h.hook_type.contains(&hook_type)) { - call_single(cert, data, &hook)?; + call_single(data, &hook)?; } Ok(()) } diff --git a/acmed/src/main_event_loop.rs b/acmed/src/main_event_loop.rs index ba5f885..f362256 100644 --- a/acmed/src/main_event_loop.rs +++ b/acmed/src/main_event_loop.rs @@ -1,5 +1,4 @@ use crate::acme_proto::request_certificate; -use crate::acme_proto::Challenge; use crate::certificate::Certificate; use crate::config; use acme_common::error::Error; @@ -20,11 +19,7 @@ impl MainEventLoop { for crt in cnf.certificate.iter() { let cert = Certificate { account: crt.get_account(&cnf)?, - domains: crt - .domains - .iter() - .map(|d| (d.dns.to_owned(), Challenge::from_str(&d.challenge).unwrap())) - .collect(), + domains: crt.domains.to_owned(), algo: crt.get_algorithm()?, kp_reuse: crt.get_kp_reuse(), remote_url: crt.get_remote_url(&cnf)?, @@ -65,7 +60,7 @@ impl MainEventLoop { let msg = format!( "Unable to renew the {} certificate for {}: {}", crt.algo, - crt.domains.first().unwrap().0, + &crt.domains.first().unwrap().dns, e ); warn!("{}", msg); @@ -78,7 +73,7 @@ impl MainEventLoop { let msg = format!( "{} certificate for {}: post-operation hook error: {}", crt.algo, - crt.domains.first().unwrap().0, + &crt.domains.first().unwrap().dns, e ); warn!("{}", msg); diff --git a/acmed/src/storage.rs b/acmed/src/storage.rs index 75b297f..daa910b 100644 --- a/acmed/src/storage.rs +++ b/acmed/src/storage.rs @@ -1,6 +1,6 @@ use crate::certificate::Certificate; use crate::config::HookType; -use crate::hooks::{self, FileStorageHookData}; +use crate::hooks::{self, FileStorageHookData, HookEnvData}; use acme_common::b64_encode; use acme_common::error::Error; use log::trace; @@ -135,12 +135,13 @@ fn set_owner(cert: &Certificate, path: &PathBuf, file_type: FileType) -> Result< fn write_file(cert: &Certificate, file_type: FileType, data: &[u8]) -> Result<(), Error> { let (file_directory, file_name, path) = get_file_full_path(cert, file_type.clone())?; - let hook_data = FileStorageHookData { + let mut hook_data = FileStorageHookData { file_name, file_directory, file_path: path.to_path_buf(), env: HashMap::new(), }; + hook_data.set_env(&cert.env); let is_new = !path.is_file(); if is_new { diff --git a/man/en/acmed.toml.5 b/man/en/acmed.toml.5 index 3f428a9..d329df4 100644 --- a/man/en/acmed.toml.5 +++ b/man/en/acmed.toml.5 @@ -146,9 +146,7 @@ Table of environment variables that will be accessible from hooks. .It Ic domains Ar array Array of tables listing the domains that should be included in the certificate along with the challenge to use for each one. .Bl -tag -.It Ic dns -The domain name. -.It Ic challenge +.It Ic challenge Ar string The name of the challenge to use to prove the domain's ownership. Possible values are: .Bl -dash -compact .It @@ -158,6 +156,10 @@ dns-01 .It tls-alpn-01 .El +.It Ic dns Ar string +The domain name. +.It Ic env Ar table +Table of environment variables that will be accessible from hooks. .El .It Ic algorithm Ar string Name of the asymetric cryptography algorithm used to generate the certificate's key pair. Possible values are :