Browse Source

Add hooks before and after a file is created or edited

It is considered a good practice to archive old certificates and private
keys instead of simply dropping them away. Because ACMEd should not
impose a way of doing things to system administrators, hooks are the way
to go.
pull/5/head
Rodolphe Breard 6 years ago
parent
commit
f4f339b8c2
  1. 1
      CHANGELOG.md
  2. 1
      README.md
  3. 62
      acmed/src/acmed.rs
  4. 59
      acmed/src/config.rs
  5. 57
      acmed/src/hooks.rs
  6. 1
      acmed/src/main.rs
  7. 59
      acmed/src/storage.rs

1
CHANGELOG.md

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- The `kp_reuse` flag allow to reuse a key pair instead of creating a new one at each renewal.
- It is now possible to define hook groups that can reference either hooks or other hook groups.
- Hooks can be defined when before and after a file is created or edited (`file_pre_create_hooks`, `file_post_create_hooks`, `file_pre_edit_hooks` and `file_post_edit_hooks`).
### Changed
- `post_operation_hook` has been renamed `post_operation_hooks`.

1
README.md

@ -13,6 +13,7 @@ The Automatic Certificate Management Environment (ACME), is an internet standard
- HTTP-01 and DNS-01 challenges
- RSA 2048, RSA 4096, ECDSA P-256 and ECDSA P-384 certificates
- Fully customizable challenge validation action
- Fully customizable archiving method (yes, you can use git or anything else)
- Run as a deamon: no need to set-up timers, crontab or other time-triggered process
- Nice and simple configuration file

62
acmed/src/acmed.rs

@ -1,15 +1,12 @@
use acme_lib::{Directory, DirectoryUrl};
use crate::config::{self, Hook};
use crate::errors::Error;
use crate::hooks;
use crate::storage::Storage;
use handlebars::Handlebars;
use log::{debug, info, warn};
use openssl;
use serde::Serialize;
use std::{fmt, thread};
use std::fs::File;
use std::io::Write;
use std::process::{Command, Stdio};
use std::time::Duration;
use x509_parser::parse_x509_der;
@ -100,51 +97,6 @@ struct HookData {
proof: String,
}
macro_rules! get_hook_output {
($out: expr, $reg: ident, $data: expr) => {{
match $out {
Some(path) => {
let path = $reg.render_template(path, $data)?;
let file = File::create(path)?;
Stdio::from(file)
}
None => Stdio::null(),
}
}};
}
impl HookData {
pub fn call(&self, hook: &Hook) -> Result<(), Error> {
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, &self)?;
v.push(s);
}
v.as_slice()
}
None => &[],
};
let mut cmd = Command::new(&hook.cmd)
.args(args)
.stdout(get_hook_output!(&hook.stdout, reg, &self))
.stderr(get_hook_output!(&hook.stderr, reg, &self))
.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(), &self)?;
let stdin = cmd.stdin.as_mut().ok_or("stdin not found")?;
stdin.write_all(data_in.as_bytes())?;
}
Ok(())
}
}
#[derive(Debug)]
struct Certificate {
domains: Vec<String>,
@ -210,9 +162,7 @@ impl Certificate {
token: token.to_string(),
proof: proof.to_string(),
};
for hook in self.challenge_hooks.iter() {
hook_data.call(&hook)?;
}
hooks::call_multiple(&hook_data, &self.challenge_hooks)?;
Ok(())
}
@ -226,9 +176,7 @@ impl Certificate {
token: "".to_string(),
proof: "".to_string(),
};
for hook in self.post_operation_hooks.iter() {
hook_data.call(&hook)?;
}
hooks::call_multiple(&hook_data, &self.post_operation_hooks)?;
Ok(())
}
@ -326,6 +274,10 @@ impl Acmed {
pk_file_mode: cnf.get_pk_file_mode(),
pk_file_owner: cnf.get_pk_file_user(),
pk_file_group: cnf.get_pk_file_group(),
file_pre_create_hooks: crt.get_file_pre_create_hooks(&cnf)?,
file_post_create_hooks: crt.get_file_post_create_hooks(&cnf)?,
file_pre_edit_hooks: crt.get_file_pre_edit_hooks(&cnf)?,
file_post_edit_hooks: crt.get_file_post_edit_hooks(&cnf)?,
},
email: crt.email.to_owned(),
remote_url: crt.get_remote_url(&cnf)?,

59
acmed/src/config.rs

@ -135,14 +135,18 @@ pub struct Certificate {
pub endpoint: String,
pub domains: Vec<String>,
pub challenge: String,
pub challenge_hooks: Vec<String>,
pub post_operation_hooks: Option<Vec<String>>,
pub algorithm: Option<String>,
pub kp_reuse: Option<bool>,
pub directory: Option<String>,
pub name: Option<String>,
pub name_format: Option<String>,
pub formats: Option<Vec<String>>,
pub challenge_hooks: Vec<String>,
pub post_operation_hooks: Option<Vec<String>>,
pub file_pre_create_hooks: Option<Vec<String>>,
pub file_post_create_hooks: Option<Vec<String>>,
pub file_pre_edit_hooks: Option<Vec<String>>,
pub file_post_edit_hooks: Option<Vec<String>>,
}
impl Certificate {
@ -223,25 +227,52 @@ impl Certificate {
}
pub fn get_challenge_hooks(&self, cnf: &Config) -> Result<Vec<Hook>, Error> {
let mut res = vec![];
for name in self.challenge_hooks.iter() {
let mut h = cnf.get_hook(&name)?;
res.append(&mut h);
}
Ok(res)
get_hooks(&self.challenge_hooks, cnf)
}
pub fn get_post_operation_hooks(&self, cnf: &Config) -> Result<Vec<Hook>, Error> {
let mut res = vec![];
match &self.post_operation_hooks {
Some(po_hooks) => for name in po_hooks.iter() {
Some(hooks) => get_hooks(hooks, cnf),
None => Ok(vec![]),
}
}
pub fn get_file_pre_create_hooks(&self, cnf: &Config) -> Result<Vec<Hook>, Error> {
match &self.file_pre_create_hooks {
Some(hooks) => get_hooks(hooks, cnf),
None => Ok(vec![]),
}
}
pub fn get_file_post_create_hooks(&self, cnf: &Config) -> Result<Vec<Hook>, Error> {
match &self.file_post_create_hooks {
Some(hooks) => get_hooks(hooks, cnf),
None => Ok(vec![]),
}
}
pub fn get_file_pre_edit_hooks(&self, cnf: &Config) -> Result<Vec<Hook>, Error> {
match &self.file_pre_edit_hooks {
Some(hooks) => get_hooks(hooks, cnf),
None => Ok(vec![]),
}
}
pub fn get_file_post_edit_hooks(&self, cnf: &Config) -> Result<Vec<Hook>, Error> {
match &self.file_post_edit_hooks {
Some(hooks) => get_hooks(hooks, cnf),
None => Ok(vec![]),
}
}
}
fn get_hooks(lst: &Vec<String>, cnf: &Config) -> Result<Vec<Hook>, Error> {
let mut res = vec![];
for name in lst.iter() {
let mut h = cnf.get_hook(&name)?;
res.append(&mut h);
},
None => {}
};
Ok(res)
}
Ok(res)
}
fn create_dir(path: &str) -> Result<(), Error> {

57
acmed/src/hooks.rs

@ -0,0 +1,57 @@
use crate::config::Hook;
use crate::errors::Error;
use handlebars::Handlebars;
use serde::Serialize;
use std::fs::File;
use std::io::prelude::*;
use std::process::{Command, Stdio};
macro_rules! get_hook_output {
($out: expr, $reg: ident, $data: expr) => {{
match $out {
Some(path) => {
let path = $reg.render_template(path, $data)?;
let file = File::create(path)?;
Stdio::from(file)
}
None => Stdio::null(),
}
}};
}
pub fn call_multiple<T: Serialize>(data: &T, hooks: &Vec<Hook>) -> Result<(), Error> {
for hook in hooks.iter() {
call(data, &hook)?;
}
Ok(())
}
pub fn call<T: Serialize>(data: &T, hook: &Hook) -> Result<(), Error> {
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)?;
v.push(s);
}
v.as_slice()
}
None => &[],
};
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))
.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 stdin = cmd.stdin.as_mut().ok_or("stdin not found")?;
stdin.write_all(data_in.as_bytes())?;
}
Ok(())
}

1
acmed/src/main.rs

@ -6,6 +6,7 @@ mod acmed;
mod config;
mod encoding;
mod errors;
mod hooks;
mod storage;
pub const DEFAULT_CONFIG_FILE: &str = "/etc/acmed/acmed.toml";

59
acmed/src/storage.rs

@ -1,8 +1,12 @@
use acme_lib::Error;
use acme_lib::persist::{Persist, PersistKey, PersistKind};
use crate::acmed::{Algorithm, Format};
use crate::config::Hook;
use crate::errors;
use crate::encoding::convert;
use crate::hooks;
use log::debug;
use serde::Serialize;
use std::fs::{File, OpenOptions};
use std::io::prelude::*;
use std::path::PathBuf;
@ -28,6 +32,13 @@ macro_rules! get_file_name {
}};
}
#[derive(Serialize)]
struct FileData {
file_name: String,
file_directory: String,
file_path: PathBuf,
}
#[derive(Clone, Debug)]
pub struct Storage {
pub account_directory: String,
@ -43,6 +54,10 @@ pub struct Storage {
pub pk_file_mode: u32,
pub pk_file_owner: Option<String>,
pub pk_file_group: Option<String>,
pub file_pre_create_hooks: Vec<Hook>,
pub file_post_create_hooks: Vec<Hook>,
pub file_pre_edit_hooks: Vec<Hook>,
pub file_post_edit_hooks: Vec<Hook>,
}
impl Storage {
@ -90,7 +105,7 @@ impl Storage {
}
}
fn get_file_path(&self, kind: PersistKind, fmt: &Format) -> PathBuf {
fn get_file_path(&self, kind: PersistKind, fmt: &Format) -> FileData {
let base_path = match kind {
PersistKind::Certificate => &self.crt_directory,
PersistKind::PrivateKey => &self.crt_directory,
@ -104,8 +119,12 @@ impl Storage {
}
};
let mut path = PathBuf::from(base_path);
path.push(file_name);
path
path.push(&file_name);
FileData {
file_directory: base_path.to_string(),
file_name: file_name,
file_path: path,
}
}
pub fn get_certificate(&self, fmt: &Format) -> Result<Option<Vec<u8>>, Error> {
@ -122,12 +141,12 @@ impl Storage {
} else {
self.formats.first().unwrap()
};
let path = self.get_file_path(kind, src_fmt);
debug!("Reading file {:?}", path);
if !path.exists() {
let file_data = self.get_file_path(kind, src_fmt);
debug!("Reading file {:?}", file_data.file_path);
if !file_data.file_path.exists() {
return Ok(None);
}
let mut file = File::open(&path)?;
let mut file = File::open(&file_data.file_path)?;
let mut contents = vec![];
file.read_to_end(&mut contents)?;
if contents.is_empty() {
@ -145,15 +164,21 @@ impl Storage {
impl Persist for Storage {
fn put(&self, key: &PersistKey, value: &[u8]) -> Result<(), Error> {
for fmt in self.formats.iter() {
let path = self.get_file_path(key.kind, &fmt);
debug!("Writing file {:?}", path);
let file_data = self.get_file_path(key.kind, &fmt);
debug!("Writing file {:?}", file_data.file_path);
let file_exists = file_data.file_path.exists();
if file_exists {
hooks::call_multiple(&file_data, &self.file_pre_edit_hooks).map_err(to_acme_err)?;
} else {
hooks::call_multiple(&file_data, &self.file_pre_create_hooks).map_err(to_acme_err)?;
}
{
let mut f = if cfg!(unix) {
let mut options = OpenOptions::new();
options.mode(self.get_file_mode(key.kind));
options.write(true).create(true).open(&path)?
options.write(true).create(true).open(&file_data.file_path)?
} else {
File::create(&path)?
File::create(&file_data.file_path)?
};
match fmt {
Format::Der => {
@ -165,7 +190,13 @@ impl Persist for Storage {
f.sync_all()?;
}
if cfg!(unix) {
self.set_owner(&path, key.kind)?;
self.set_owner(&file_data.file_path, key.kind)?;
}
if file_exists {
hooks::call_multiple(&file_data, &self.file_post_edit_hooks).map_err(to_acme_err)?;
} else {
hooks::call_multiple(&file_data, &self.file_post_create_hooks)
.map_err(to_acme_err)?;
}
}
Ok(())
@ -175,3 +206,7 @@ impl Persist for Storage {
self.get_file(key.kind, &Format::Pem)
}
}
fn to_acme_err(e: errors::Error) -> Error {
Error::Other(e.message)
}
Loading…
Cancel
Save