mirror of https://github.com/breard-r/acmed.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
213 lines
5.0 KiB
213 lines
5.0 KiB
pub use crate::config::HookType;
|
|
use crate::logs::HasLogger;
|
|
use crate::template::render_template;
|
|
use acme_common::error::Error;
|
|
use serde::Serialize;
|
|
use std::collections::hash_map::Iter;
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::fs::File;
|
|
use std::io::prelude::*;
|
|
use std::io::BufReader;
|
|
use std::path::PathBuf;
|
|
use std::process::{Command, Stdio};
|
|
use std::{env, fmt};
|
|
|
|
pub trait HookEnvData {
|
|
fn set_env(&mut self, env: &HashMap<String, String>);
|
|
fn get_env(&self) -> Iter<String, String>;
|
|
}
|
|
|
|
fn deref<F, G>(t: (&F, &G)) -> (F, G)
|
|
where
|
|
F: Clone,
|
|
G: Clone,
|
|
{
|
|
((*(t.0)).to_owned(), (*(t.1)).to_owned())
|
|
}
|
|
|
|
macro_rules! imple_hook_data_env {
|
|
($t: ty) => {
|
|
impl HookEnvData for $t {
|
|
fn set_env(&mut self, env: &HashMap<String, String>) {
|
|
for (key, value) in env::vars().chain(env.iter().map(deref)) {
|
|
self.env.insert(key, value);
|
|
}
|
|
}
|
|
|
|
fn get_env(&self) -> Iter<String, String> {
|
|
self.env.iter()
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
#[derive(Clone, Serialize)]
|
|
pub struct PostOperationHookData {
|
|
pub identifiers: Vec<String>,
|
|
pub key_type: String,
|
|
pub status: String,
|
|
pub is_success: bool,
|
|
pub env: HashMap<String, String>,
|
|
}
|
|
|
|
imple_hook_data_env!(PostOperationHookData);
|
|
|
|
#[derive(Clone, Serialize)]
|
|
pub struct ChallengeHookData {
|
|
pub identifier: String,
|
|
pub identifier_tls_alpn: String,
|
|
pub challenge: String,
|
|
pub file_name: String,
|
|
pub proof: String,
|
|
pub is_clean_hook: bool,
|
|
pub env: HashMap<String, String>,
|
|
}
|
|
|
|
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<String, String>,
|
|
}
|
|
|
|
imple_hook_data_env!(FileStorageHookData);
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum HookStdin {
|
|
File(String),
|
|
Str(String),
|
|
None,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct Hook {
|
|
pub name: String,
|
|
pub hook_type: HashSet<HookType>,
|
|
pub cmd: String,
|
|
pub args: Option<Vec<String>>,
|
|
pub stdin: HookStdin,
|
|
pub stdout: Option<String>,
|
|
pub stderr: Option<String>,
|
|
pub allow_failure: bool,
|
|
}
|
|
|
|
impl fmt::Display for Hook {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
write!(f, "{}", self.name)
|
|
}
|
|
}
|
|
|
|
macro_rules! get_hook_output {
|
|
($logger: expr, $out: expr, $data: expr, $hook_name: expr, $out_name: expr) => {{
|
|
match $out {
|
|
Some(path) => {
|
|
let path = render_template(path, $data)?;
|
|
$logger.trace(&format!(
|
|
"hook \"{}\": {}: {}",
|
|
$hook_name, $out_name, &path
|
|
));
|
|
let file = File::create(&path)?;
|
|
Stdio::from(file)
|
|
}
|
|
None => Stdio::null(),
|
|
}
|
|
}};
|
|
}
|
|
|
|
fn call_single<L, T>(logger: &L, data: &T, hook: &Hook) -> Result<(), Error>
|
|
where
|
|
L: HasLogger,
|
|
T: Clone + HookEnvData + Serialize,
|
|
{
|
|
logger.debug(&format!("calling hook \"{}\"", hook.name));
|
|
let mut v = vec![];
|
|
let args = match &hook.args {
|
|
Some(lst) => {
|
|
for fmt in lst.iter() {
|
|
let s = render_template(fmt, &data)?;
|
|
v.push(s);
|
|
}
|
|
v.as_slice()
|
|
}
|
|
None => &[],
|
|
};
|
|
logger.trace(&format!("hook \"{}\": cmd: {}", hook.name, hook.cmd));
|
|
logger.trace(&format!("hook \"{}\": args: {:?}", hook.name, args));
|
|
let mut cmd = Command::new(&hook.cmd)
|
|
.envs(data.get_env())
|
|
.args(args)
|
|
.stdout(get_hook_output!(
|
|
logger,
|
|
&hook.stdout,
|
|
&data,
|
|
&hook.name,
|
|
"stdout"
|
|
))
|
|
.stderr(get_hook_output!(
|
|
logger,
|
|
&hook.stderr,
|
|
&data,
|
|
&hook.name,
|
|
"stderr"
|
|
))
|
|
.stdin(match &hook.stdin {
|
|
HookStdin::Str(_) | HookStdin::File(_) => Stdio::piped(),
|
|
HookStdin::None => Stdio::null(),
|
|
})
|
|
.spawn()?;
|
|
match &hook.stdin {
|
|
HookStdin::Str(s) => {
|
|
let data_in = render_template(s, &data)?;
|
|
logger.trace(&format!(
|
|
"hook \"{}\": string stdin: {}",
|
|
hook.name, &data_in
|
|
));
|
|
let stdin = cmd.stdin.as_mut().ok_or("stdin not found")?;
|
|
stdin.write_all(data_in.as_bytes())?;
|
|
}
|
|
HookStdin::File(f) => {
|
|
let file_name = render_template(f, &data)?;
|
|
logger.trace(&format!(
|
|
"hook \"{}\": file stdin: {}",
|
|
hook.name, &file_name
|
|
));
|
|
let stdin = cmd.stdin.as_mut().ok_or("stdin not found")?;
|
|
let file = File::open(&file_name).map_err(|e| Error::from(e).prefix(&file_name))?;
|
|
let buf_reader = BufReader::new(file);
|
|
for line in buf_reader.lines() {
|
|
let line = format!("{}\n", line?);
|
|
stdin.write_all(line.as_bytes())?;
|
|
}
|
|
}
|
|
HookStdin::None => {}
|
|
}
|
|
// TODO: add a timeout
|
|
let status = cmd.wait()?;
|
|
if !status.success() && !hook.allow_failure {
|
|
let msg = match status.code() {
|
|
Some(code) => format!("unrecoverable failure: code {}", code).into(),
|
|
None => "unrecoverable failure".into(),
|
|
};
|
|
return Err(msg);
|
|
}
|
|
match status.code() {
|
|
Some(code) => logger.debug(&format!("hook \"{}\": exited: code {}", hook.name, code)),
|
|
None => logger.debug(&format!("hook \"{}\": exited", hook.name)),
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
pub fn call<L, T>(logger: &L, hooks: &[Hook], data: &T, hook_type: HookType) -> Result<(), Error>
|
|
where
|
|
L: HasLogger,
|
|
T: Clone + HookEnvData + Serialize,
|
|
{
|
|
for hook in hooks.iter().filter(|h| h.hook_type.contains(&hook_type)) {
|
|
call_single(logger, data, hook).map_err(|e| e.prefix(&hook.name))?;
|
|
}
|
|
Ok(())
|
|
}
|