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. 3
      README.md
  3. 62
      acmed/src/acmed.rs
  4. 63
      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 ### Added
- The `kp_reuse` flag allow to reuse a key pair instead of creating a new one at each renewal. - 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. - 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 ### Changed
- `post_operation_hook` has been renamed `post_operation_hooks`. - `post_operation_hook` has been renamed `post_operation_hooks`.

3
README.md

@ -12,7 +12,8 @@ The Automatic Certificate Management Environment (ACME), is an internet standard
- HTTP-01 and DNS-01 challenges - HTTP-01 and DNS-01 challenges
- RSA 2048, RSA 4096, ECDSA P-256 and ECDSA P-384 certificates - RSA 2048, RSA 4096, ECDSA P-256 and ECDSA P-384 certificates
- Fully customizable challenge validation action
- 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 - Run as a deamon: no need to set-up timers, crontab or other time-triggered process
- Nice and simple configuration file - Nice and simple configuration file

62
acmed/src/acmed.rs

@ -1,15 +1,12 @@
use acme_lib::{Directory, DirectoryUrl}; use acme_lib::{Directory, DirectoryUrl};
use crate::config::{self, Hook}; use crate::config::{self, Hook};
use crate::errors::Error; use crate::errors::Error;
use crate::hooks;
use crate::storage::Storage; use crate::storage::Storage;
use handlebars::Handlebars;
use log::{debug, info, warn}; use log::{debug, info, warn};
use openssl; use openssl;
use serde::Serialize; use serde::Serialize;
use std::{fmt, thread}; use std::{fmt, thread};
use std::fs::File;
use std::io::Write;
use std::process::{Command, Stdio};
use std::time::Duration; use std::time::Duration;
use x509_parser::parse_x509_der; use x509_parser::parse_x509_der;
@ -100,51 +97,6 @@ struct HookData {
proof: String, 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)] #[derive(Debug)]
struct Certificate { struct Certificate {
domains: Vec<String>, domains: Vec<String>,
@ -210,9 +162,7 @@ impl Certificate {
token: token.to_string(), token: token.to_string(),
proof: proof.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(()) Ok(())
} }
@ -226,9 +176,7 @@ impl Certificate {
token: "".to_string(), token: "".to_string(),
proof: "".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(()) Ok(())
} }
@ -326,6 +274,10 @@ impl Acmed {
pk_file_mode: cnf.get_pk_file_mode(), pk_file_mode: cnf.get_pk_file_mode(),
pk_file_owner: cnf.get_pk_file_user(), pk_file_owner: cnf.get_pk_file_user(),
pk_file_group: cnf.get_pk_file_group(), 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(), email: crt.email.to_owned(),
remote_url: crt.get_remote_url(&cnf)?, remote_url: crt.get_remote_url(&cnf)?,

63
acmed/src/config.rs

@ -135,14 +135,18 @@ pub struct Certificate {
pub endpoint: String, pub endpoint: String,
pub domains: Vec<String>, pub domains: Vec<String>,
pub challenge: String, pub challenge: String,
pub challenge_hooks: Vec<String>,
pub post_operation_hooks: Option<Vec<String>>,
pub algorithm: Option<String>, pub algorithm: Option<String>,
pub kp_reuse: Option<bool>, pub kp_reuse: Option<bool>,
pub directory: Option<String>, pub directory: Option<String>,
pub name: Option<String>, pub name: Option<String>,
pub name_format: Option<String>, pub name_format: Option<String>,
pub formats: Option<Vec<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 { impl Certificate {
@ -223,25 +227,52 @@ impl Certificate {
} }
pub fn get_challenge_hooks(&self, cnf: &Config) -> Result<Vec<Hook>, Error> { 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> { pub fn get_post_operation_hooks(&self, cnf: &Config) -> Result<Vec<Hook>, Error> {
let mut res = vec![];
match &self.post_operation_hooks { match &self.post_operation_hooks {
Some(po_hooks) => for name in po_hooks.iter() {
let mut h = cnf.get_hook(&name)?;
res.append(&mut h);
},
None => {}
};
Ok(res)
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);
} }
Ok(res)
} }
fn create_dir(path: &str) -> Result<(), Error> { 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 config;
mod encoding; mod encoding;
mod errors; mod errors;
mod hooks;
mod storage; mod storage;
pub const DEFAULT_CONFIG_FILE: &str = "/etc/acmed/acmed.toml"; 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::Error;
use acme_lib::persist::{Persist, PersistKey, PersistKind}; use acme_lib::persist::{Persist, PersistKey, PersistKind};
use crate::acmed::{Algorithm, Format}; use crate::acmed::{Algorithm, Format};
use crate::config::Hook;
use crate::errors;
use crate::encoding::convert; use crate::encoding::convert;
use crate::hooks;
use log::debug; use log::debug;
use serde::Serialize;
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
use std::io::prelude::*; use std::io::prelude::*;
use std::path::PathBuf; 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)] #[derive(Clone, Debug)]
pub struct Storage { pub struct Storage {
pub account_directory: String, pub account_directory: String,
@ -43,6 +54,10 @@ pub struct Storage {
pub pk_file_mode: u32, pub pk_file_mode: u32,
pub pk_file_owner: Option<String>, pub pk_file_owner: Option<String>,
pub pk_file_group: 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 { 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 { let base_path = match kind {
PersistKind::Certificate => &self.crt_directory, PersistKind::Certificate => &self.crt_directory,
PersistKind::PrivateKey => &self.crt_directory, PersistKind::PrivateKey => &self.crt_directory,
@ -104,8 +119,12 @@ impl Storage {
} }
}; };
let mut path = PathBuf::from(base_path); 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> { pub fn get_certificate(&self, fmt: &Format) -> Result<Option<Vec<u8>>, Error> {
@ -122,12 +141,12 @@ impl Storage {
} else { } else {
self.formats.first().unwrap() 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); return Ok(None);
} }
let mut file = File::open(&path)?;
let mut file = File::open(&file_data.file_path)?;
let mut contents = vec![]; let mut contents = vec![];
file.read_to_end(&mut contents)?; file.read_to_end(&mut contents)?;
if contents.is_empty() { if contents.is_empty() {
@ -145,15 +164,21 @@ impl Storage {
impl Persist for Storage { impl Persist for Storage {
fn put(&self, key: &PersistKey, value: &[u8]) -> Result<(), Error> { fn put(&self, key: &PersistKey, value: &[u8]) -> Result<(), Error> {
for fmt in self.formats.iter() { 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 f = if cfg!(unix) {
let mut options = OpenOptions::new(); let mut options = OpenOptions::new();
options.mode(self.get_file_mode(key.kind)); 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 { } else {
File::create(&path)?
File::create(&file_data.file_path)?
}; };
match fmt { match fmt {
Format::Der => { Format::Der => {
@ -165,7 +190,13 @@ impl Persist for Storage {
f.sync_all()?; f.sync_all()?;
} }
if cfg!(unix) { 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(()) Ok(())
@ -175,3 +206,7 @@ impl Persist for Storage {
self.get_file(key.kind, &Format::Pem) self.get_file(key.kind, &Format::Pem)
} }
} }
fn to_acme_err(e: errors::Error) -> Error {
Error::Other(e.message)
}
Loading…
Cancel
Save