From 03850d20e6467ab7d91d7a4f32b6e022234cae03 Mon Sep 17 00:00:00 2001 From: Rodolphe Breard Date: Thu, 9 May 2019 15:13:36 +0200 Subject: [PATCH] Add environment variables to hook templates --- CHANGELOG.md | 1 + acmed/src/certificate.rs | 4 ++- acmed/src/hooks.rs | 54 ++++++++++++++++++++++++++++++++-------- acmed/src/storage.rs | 2 ++ man/en/acmed.toml.5 | 48 +++++++++++++++++++++-------------- 5 files changed, 79 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2b3a7f..721d0ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - 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. - tacd is now able to listen on a unix socket. diff --git a/acmed/src/certificate.rs b/acmed/src/certificate.rs index c0cf333..f6b6fc9 100644 --- a/acmed/src/certificate.rs +++ b/acmed/src/certificate.rs @@ -5,7 +5,7 @@ use crate::storage::{certificate_files_exists, get_certificate}; use acme_common::error::Error; use log::{debug, trace}; use openssl::x509::X509; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::fmt; use time::{strptime, Duration}; @@ -203,6 +203,7 @@ impl Certificate { file_name: file_name.to_string(), proof: proof.to_string(), is_clean_hook: false, + env: HashMap::new(), }; let hook_type = match challenge { Challenge::Http01 => (HookType::ChallengeHttp01, HookType::ChallengeHttp01Clean), @@ -235,6 +236,7 @@ impl Certificate { algorithm: self.algo.to_string(), status: status.to_string(), is_success, + env: HashMap::new(), }; hooks::call(&hook_data, &self.hooks, HookType::PostOperation)?; Ok(()) diff --git a/acmed/src/hooks.rs b/acmed/src/hooks.rs index cb906e2..d8456cd 100644 --- a/acmed/src/hooks.rs +++ b/acmed/src/hooks.rs @@ -3,37 +3,63 @@ use acme_common::error::Error; use handlebars::Handlebars; use log::debug; use serde::Serialize; -use std::fmt; +use std::collections::HashMap; use std::fs::File; use std::io::prelude::*; use std::path::PathBuf; use std::process::{Command, Stdio}; +use std::{env, fmt}; -#[derive(Serialize)] +pub trait HookEnvData { + fn set_env(&mut self); +} + +macro_rules! imple_hook_data_env { + ($t: ty) => { + impl HookEnvData for $t { + fn set_env(&mut self) { + for (key, value) in env::vars() { + self.env.insert(key, value); + } + } + } + }; +} + +#[derive(Clone, Serialize)] pub struct PostOperationHookData { pub domains: Vec, pub algorithm: String, pub status: String, pub is_success: bool, + pub env: HashMap, } -#[derive(Serialize)] +imple_hook_data_env!(PostOperationHookData); + +#[derive(Clone, Serialize)] pub struct ChallengeHookData { pub domain: String, pub challenge: String, pub file_name: String, pub proof: String, pub is_clean_hook: bool, + pub env: HashMap, } -#[derive(Serialize)] +imple_hook_data_env!(ChallengeHookData); + +#[derive(Clone, Serialize)] pub struct FileStorageHookData { // TODO: add the current operation (create/edit) pub file_name: String, pub file_directory: String, pub file_path: PathBuf, + pub env: HashMap, } +imple_hook_data_env!(FileStorageHookData); + #[derive(Clone, Debug)] pub struct Hook { pub name: String, @@ -64,14 +90,19 @@ macro_rules! get_hook_output { }}; } -fn call_single(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(); let reg = Handlebars::new(); let mut v = vec![]; let args = match &hook.args { Some(lst) => { for fmt in lst.iter() { - let s = reg.render_template(fmt, data)?; + let s = reg.render_template(fmt, &data)?; v.push(s); } v.as_slice() @@ -82,15 +113,15 @@ fn call_single(data: &T, hook: &Hook) -> Result<(), Error> { debug!("Hook {}: args: {:?}", hook.name, args); let mut cmd = Command::new(&hook.cmd) .args(args) - .stdout(get_hook_output!(&hook.stdout, reg, data)) - .stderr(get_hook_output!(&hook.stderr, reg, data)) + .stdout(get_hook_output!(&hook.stdout, reg, &data)) + .stderr(get_hook_output!(&hook.stderr, reg, &data)) .stdin(match &hook.stdin { Some(_) => Stdio::piped(), None => Stdio::null(), }) .spawn()?; if hook.stdin.is_some() { - let data_in = reg.render_template(&hook.stdin.to_owned().unwrap(), data)?; + let data_in = reg.render_template(&hook.stdin.to_owned().unwrap(), &data)?; debug!("Hook {}: stdin: {}", hook.name, data_in); let stdin = cmd.stdin.as_mut().ok_or("stdin not found")?; stdin.write_all(data_in.as_bytes())?; @@ -104,7 +135,10 @@ fn call_single(data: &T, hook: &Hook) -> Result<(), Error> { Ok(()) } -pub fn call(data: &T, hooks: &[Hook], hook_type: HookType) -> Result<(), Error> { +pub fn call(data: &T, hooks: &[Hook], hook_type: HookType) -> Result<(), Error> +where + T: Clone + HookEnvData + Serialize, +{ for hook in hooks.iter().filter(|h| h.hook_type.contains(&hook_type)) { call_single(data, &hook)?; } diff --git a/acmed/src/storage.rs b/acmed/src/storage.rs index 6c8e054..51e4c08 100644 --- a/acmed/src/storage.rs +++ b/acmed/src/storage.rs @@ -6,6 +6,7 @@ use acme_common::error::Error; use log::trace; use openssl::pkey::{PKey, Private, Public}; use openssl::x509::X509; +use std::collections::HashMap; use std::fmt; use std::fs::{File, OpenOptions}; use std::io::{Read, Write}; @@ -138,6 +139,7 @@ fn write_file(cert: &Certificate, file_type: FileType, data: &[u8]) -> Result<() file_name, file_directory, file_path: path.to_path_buf(), + env: HashMap::new(), }; let is_new = !path.is_file(); diff --git a/man/en/acmed.toml.5 b/man/en/acmed.toml.5 index 98bfefc..2a65546 100644 --- a/man/en/acmed.toml.5 +++ b/man/en/acmed.toml.5 @@ -211,21 +211,23 @@ Invoked when the ownership of a domain must be proved using the .Em http-01 challenge. The available template variables are: .Bl -tag -compact -.It Cm domain Ar string -The domain name whom ownership is currently being validated. .It Cm challenge Ar string The name of the challenge type .Aq http-01 . Mostly used in hooks with multiple types. +.It Cm domain Ar string +The domain name whom ownership is currently being validated. +.It Cm env Ar array +Array containing all the environment variables. .It Cm file_name Ar string Name of the file containing the proof. This is not a full path and does not include the .Ql .well-known/acme-challenge/ prefix. +.It Cm is_clean_hook Ar bool +False .It Cm proof Ar string The content of the proof that must be written to .Em file_name . -.It Cm is_clean_hook Ar bool -False .El .It Ic challenge-http-01-clean Invoked once a domain ownership has been proven using the @@ -241,20 +243,22 @@ Invoked when the ownership of a domain must be proved using the .Em dns-01 challenge. The available template variables are: .Bl -tag -compact -.It Cm domain Ar string -The domain name whom ownership is currently being validated. .It Cm challenge Ar string The name of the challenge type .Aq dns-01 . Mostly used in hooks with multiple types. +.It Cm domain Ar string +The domain name whom ownership is currently being validated. +.It Cm env Ar array +Array containing all the environment variables. +.It Cm is_clean_hook Ar bool +False .It Cm proof Ar string The content of the proof that must be written to a .Ql TXT entry of the DNS zone for the .Ql _acme-challenge subdomain. -.It Cm is_clean_hook Ar bool -False .El .It Ic challenge-dns-01-clean Invoked once a domain ownership has been proven using the @@ -270,12 +274,16 @@ Invoked when the ownership of a domain must be proved using the .Em tls-alpn-01 challenge. The available template variables are: .Bl -tag -compact -.It Cm domain Ar string -The domain name whom ownership is currently being validated. .It Cm challenge Ar string The name of the challenge type .Aq tls-alpn-01 . Mostly used in hooks with multiple types. +.It Cm domain Ar string +The domain name whom ownership is currently being validated. +.It Cm env Ar array +Array containing all the environment variables. +.It Cm is_clean_hook Ar bool +False .It Cm proof Ar string Plain-text representation of the .Em acmeIdentifier @@ -285,8 +293,6 @@ ALPN extension value. .Xr acmed 8 will not generate the certificate itself since it can be done using .Xr tacd 8 . -.It Cm is_clean_hook Ar bool -False .El .It Ic challenge-tls-alpn-01-clean Invoked once a domain ownership has been proven using the @@ -304,10 +310,12 @@ a non-existent file .Em created . The available template variables are: .Bl -tag -compact -.It Cm file_name Ar string -Name of the impacted file. +.It Cm env Ar array +Array containing all the environment variables. .It Cm file_directory Ar string Name of the directory where the impacted file is located. +.It Cm file_name Ar string +Name of the impacted file. .It Cm file_path Ar string Full path to the impacted file. .El @@ -338,14 +346,16 @@ type. .It Ic post-operation Invoked at the end of the certificate request process. The available template variables are: .Bl -tag -compact -.It Cm domains Ar string -Array containing the domain names included in the requested certificate. .It Cm algorithm Ar string Name of the algorithm used in the certificate. -.It Cm status Ar string -Human-readable status. If the certificate request failed, it contains the error description. +.It Cm domains Ar string +Array containing the domain names included in the requested certificate. +.It Cm env Ar array +Array containing all the environment variables. .It Cm is_success Ar boolean True if the certificate request is successful. +.It Cm status Ar string +Human-readable status. If the certificate request failed, it contains the error description. .El .El .Sh FILES @@ -460,7 +470,7 @@ args = [ ] stdin = """Subject: Certificate renewal {{#if is_success}}succeeded{{else}}failed{{/if}} for {{domains.[0]}} -The following certificate has {{#unless is_success}}*not* {{/if}}been renewed. +The following certificate has {{#unless is_success}}*not* {{/unless}}been renewed. domains: {{#each domains}}{{#if @index}}, {{/if}}{{this}}{{/each}} algorithm: {{algorithm}} status: {{status}}"""