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.
 
 
 
 

359 lines
12 KiB

use acme_lib::{Directory, DirectoryUrl};
use crate::config::{self, Hook};
use crate::errors::Error;
use crate::storage::Storage;
use handlebars::Handlebars;
use log::{debug, info, warn};
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;
#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
pub enum Format {
Der,
Pem,
}
impl fmt::Display for Format {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
Format::Der => "der",
Format::Pem => "pem",
};
write!(f, "{}", s)
}
}
#[derive(Clone, Debug)]
pub enum Challenge {
Http01,
Dns01,
}
impl Challenge {
pub fn from_str(s: &str) -> Result<Self, Error> {
match s.to_lowercase().as_str() {
"http-01" => Ok(Challenge::Http01),
"dns-01" => Ok(Challenge::Dns01),
_ => Err(Error::new(&format!("{}: unknown challenge.", s))),
}
}
}
impl fmt::Display for Challenge {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
Challenge::Http01 => "http-01",
Challenge::Dns01 => "dns-01",
};
write!(f, "{}", s)
}
}
#[derive(Clone, Debug)]
pub enum Algorithm {
Rsa2048,
Rsa4096,
EcdsaP256,
EcdsaP384,
}
impl Algorithm {
pub fn from_str(s: &str) -> Result<Self, Error> {
match s.to_lowercase().as_str() {
"rsa2048" => Ok(Algorithm::Rsa2048),
"rsa4096" => Ok(Algorithm::Rsa4096),
"ecdsa_p256" => Ok(Algorithm::EcdsaP256),
"ecdsa_p384" => Ok(Algorithm::EcdsaP384),
_ => Err(Error::new(&format!("{}: unknown algorithm.", s))),
}
}
}
impl fmt::Display for Algorithm {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
Algorithm::Rsa2048 => "rsa2048",
Algorithm::Rsa4096 => "rsa4096",
Algorithm::EcdsaP256 => "ecdsa-p256",
Algorithm::EcdsaP384 => "ecdsa-p384",
};
write!(f, "{}", s)
}
}
#[derive(Serialize)]
struct HookData {
// Common
domains: Vec<String>,
algorithm: String,
challenge: String,
status: String,
// Challenge hooks
current_domain: String,
token: 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().unwrap();
stdin.write_all(data_in.as_bytes()).unwrap();
}
Ok(())
}
}
#[derive(Debug)]
struct Certificate {
domains: Vec<String>,
algo: Algorithm,
storage: Storage,
email: String,
remote_url: String,
challenge: Challenge,
challenge_hooks: Vec<Hook>,
post_operation_hooks: Vec<Hook>,
}
impl Certificate {
fn should_renew(&self) -> bool {
let domain = self.domains.first().unwrap();
let raw_cert = match self.storage.get_certificate(&Format::Der) {
Ok(c) => match c {
Some(d) => d,
None => {
debug!(
"{} certificate for {} is empty or does not exists",
self.algo, domain
);
return true;
}
},
Err(e) => {
warn!("{}", e);
return true;
}
};
match parse_x509_der(&raw_cert) {
Ok((_, cert)) => {
// TODO: allow a custom duration (using time-parse ?)
let renewal_time =
cert.tbs_certificate.validity.not_after - time::Duration::weeks(3);
debug!(
"{} certificate for {}: not after: {}",
self.algo,
domain,
cert.tbs_certificate.validity.not_after.asctime()
);
debug!(
"{} certificate for {}: renew on: {}",
self.algo,
domain,
renewal_time.asctime()
);
time::now_utc() > renewal_time
}
Err(_) => true,
}
}
fn call_challenge_hooks(&self, token: &str, proof: &str, domain: &str) -> Result<(), Error> {
let hook_data = HookData {
domains: self.domains.to_owned(),
algorithm: self.algo.to_string(),
challenge: self.challenge.to_string(),
status: format!("Validation pending for {}", domain),
current_domain: domain.to_string(),
token: token.to_string(),
proof: proof.to_string(),
};
for hook in self.challenge_hooks.iter() {
hook_data.call(&hook)?;
}
Ok(())
}
fn call_post_operation_hooks(&self, status: &str) -> Result<(), Error> {
let hook_data = HookData {
domains: self.domains.to_owned(),
algorithm: self.algo.to_string(),
challenge: self.challenge.to_string(),
status: status.to_string(),
current_domain: "".to_string(),
token: "".to_string(),
proof: "".to_string(),
};
for hook in self.post_operation_hooks.iter() {
hook_data.call(&hook)?;
}
Ok(())
}
fn renew(&mut self) -> Result<(), Error> {
// TODO: do it in a separated thread since it may take a while
let (name, alt_names_str) = self.domains.split_first().unwrap();
let mut alt_names = vec![];
for n in alt_names_str.iter() {
alt_names.push(n.as_str());
}
info!("Renewing the {} certificate for {}", self.algo, name);
let url = DirectoryUrl::Other(&self.remote_url);
let dir = Directory::from_url(self.storage.to_owned(), url)?;
let acc = dir.account(&self.email)?;
let mut ord_new = acc.new_order(name, &alt_names)?;
let ord_csr = loop {
if let Some(ord_csr) = ord_new.confirm_validations() {
break ord_csr;
}
let auths = ord_new.authorizations()?;
for auth in auths.iter() {
match self.challenge {
Challenge::Http01 => {
let chall = auth.http_challenge();
let token = chall.http_token();
let proof = chall.http_proof();
self.call_challenge_hooks(&token, &proof, auth.domain_name())?;
chall.validate(crate::DEFAULT_POOL_TIME)?;
}
Challenge::Dns01 => {
let chall = auth.dns_challenge();
let proof = chall.dns_proof();
self.call_challenge_hooks("", &proof, auth.domain_name())?;
chall.validate(crate::DEFAULT_POOL_TIME)?;
}
};
}
ord_new.refresh()?;
};
// TODO: allow PK reuse
let (pkey_pri, pkey_pub) = match self.algo {
Algorithm::Rsa2048 => acme_lib::create_rsa_key(2048),
Algorithm::Rsa4096 => acme_lib::create_rsa_key(4096),
Algorithm::EcdsaP256 => acme_lib::create_p256_key(),
Algorithm::EcdsaP384 => acme_lib::create_p384_key(),
};
let ord_cert = ord_csr.finalize_pkey(pkey_pri, pkey_pub, crate::DEFAULT_POOL_TIME)?;
ord_cert.download_and_save_cert()?;
Ok(())
}
}
pub struct Acmed {
certs: Vec<Certificate>,
}
impl Acmed {
pub fn new(config_file: &str) -> Result<Self, Error> {
let cnf = config::from_file(config_file)?;
let mut certs = Vec::new();
for crt in cnf.certificate.iter() {
let cert = Certificate {
domains: crt.domains.to_owned(),
algo: crt.get_algorithm()?,
storage: Storage {
account_directory: cnf.get_account_dir(),
account_name: crt.email.to_owned(),
crt_directory: crt.get_crt_dir(&cnf),
crt_name: crt.get_crt_name(),
crt_name_format: crt.get_crt_name_format(),
formats: crt.get_formats()?,
algo: crt.get_algorithm()?,
cert_file_mode: cnf.get_cert_file_mode(),
cert_file_owner: cnf.get_cert_file_user(),
cert_file_group: cnf.get_cert_file_group(),
pk_file_mode: cnf.get_pk_file_mode(),
pk_file_owner: cnf.get_pk_file_user(),
pk_file_group: cnf.get_pk_file_group(),
},
email: crt.email.to_owned(),
remote_url: crt.get_remote_url(&cnf)?,
challenge: crt.get_challenge()?,
challenge_hooks: crt.get_challenge_hooks(&cnf)?,
post_operation_hooks: crt.get_post_operation_hook(&cnf)?,
};
certs.push(cert);
}
Ok(Acmed { certs })
}
pub fn run(&mut self) {
loop {
for crt in self.certs.iter_mut() {
debug!("{:?}", crt);
if crt.should_renew() {
// TODO: keep track of (not yet implemented) threads and wait for them to end.
let status = match crt.renew() {
Ok(_) => "Success.".to_string(),
Err(e) => {
let msg = format!(
"Unable to renew the {} certificate for {}: {}",
crt.algo,
crt.domains.first().unwrap(),
e
);
warn!("{}", msg);
format!("Failed: {}", msg)
}
};
match crt.call_post_operation_hooks(&status) {
Ok(_) => {}
Err(e) => {
let msg = format!(
"{} certificate for {}: post-operation hook error: {}",
crt.algo,
crt.domains.first().unwrap(),
e
);
warn!("{}", msg);
}
};
}
}
thread::sleep(Duration::from_secs(crate::DEFAULT_SLEEP_TIME));
}
}
}