use std::collections::{HashMap, HashSet};
use std::sync::{Arc, RwLock};
use std::time::Duration;
use anyhow::Result;
use chrono::Utc;
use log::*;
use tokio::sync::watch;
use tokio::task::block_in_place;
use acme_micro::create_p384_key;
use acme_micro::{Directory, DirectoryUrl};
use rustls::sign::CertifiedKey;
use crate::cert::{Cert, CertSer};
use crate::consul::Consul;
use crate::proxy_config::ProxyConfig;
pub struct CertStore {
consul: Consul,
certs: RwLock<HashMap<String, Arc<Cert>>>,
}
impl CertStore {
pub fn new(consul: Consul) -> Arc<Self> {
Arc::new(Self {
consul,
certs: RwLock::new(HashMap::new()),
})
}
pub async fn watch_proxy_config(
self: Arc<Self>,
mut rx_proxy_config: watch::Receiver<Arc<ProxyConfig>>,
) {
while rx_proxy_config.changed().await.is_ok() {
let mut domains: HashSet<String> = HashSet::new();
let proxy_config: Arc<ProxyConfig> = rx_proxy_config.borrow().clone();
for ent in proxy_config.entries.iter() {
domains.insert(ent.host.clone());
}
info!("Ensuring we have certs for domains: {:#?}", domains);
for dom in domains.iter() {
if let Err(e) = self.get_cert(dom).await {
warn!("Error get_cert {}: {}", dom, e);
}
}
}
}
pub async fn get_cert(self: &Arc<Self>, domain: &str) -> Result<Arc<Cert>> {
// First, try locally.
{
let certs = self.certs.read().unwrap();
if let Some(cert) = certs.get(domain) {
if !cert.is_old() {
return Ok(cert.clone());
}
}
}
// Second, try from Consul.
if let Some(consul_cert) = self
.consul
.kv_get_json::<CertSer>(&format!("certs/{}", domain))
.await?
{
if let Ok(cert) = Cert::new(consul_cert) {
let cert = Arc::new(cert);
if !cert.is_old() {
self.certs
.write()
.unwrap()
.insert(domain.to_string(), cert.clone());
return Ok(cert);
}
}
}
// Third, ask from Let's Encrypt
self.renew_cert(domain).await
}
pub async fn renew_cert(self: &Arc<Self>, domain: &str) -> Result<Arc<Cert>> {
info!("Renewing certificate for {}", domain);
let dir = Directory::from_url(DirectoryUrl::LetsEncrypt)?;
let contact = vec!["mailto:alex@adnab.me".to_string()];
let acc =
if let Some(acc_privkey) = self.consul.kv_get("letsencrypt_account_key.pem").await? {
info!("Using existing Let's encrypt account");
dir.load_account(std::str::from_utf8(&acc_privkey)?, contact)?
} else {
info!("Creating new Let's encrypt account");
let acc = block_in_place(|| dir.register_account(contact.clone()))?;
self.consul
.kv_put(
"letsencrypt_account_key.pem",
acc.acme_private_key_pem()?.into_bytes().into(),
)
.await?;
acc
};
let mut ord_new = acc.new_order(domain, &[])?;
let ord_csr = loop {
if let Some(ord_csr) = ord_new.confirm_validations() {
break ord_csr;
}
let auths = ord_new.authorizations()?;
info!("Creating challenge and storing in Consul");
let chall = auths[0].http_challenge().unwrap();
let chall_key = format!("challenge/{}", chall.http_token());
self.consul
.kv_put(&chall_key, chall.http_proof()?.into())
.await?;
info!("Validating challenge");
block_in_place(|| chall.validate(Duration::from_millis(5000)))?;
info!("Deleting challenge");
self.consul.kv_delete(&chall_key).await?;
block_in_place(|| ord_new.refresh())?;
};
let pkey_pri = create_p384_key()?;
let ord_cert =
block_in_place(|| ord_csr.finalize_pkey(pkey_pri, Duration::from_millis(5000)))?;
let cert = block_in_place(|| ord_cert.download_cert())?;
info!("Keys and certificate obtained");
let key_pem = cert.private_key().to_string();
let cert_pem = cert.certificate().to_string();
let certser = CertSer {
hostname: domain.to_string(),
date: Utc::today().naive_utc(),
valid_days: cert.valid_days_left()?,
key_pem,
cert_pem,
};
self.consul
.kv_put_json(&format!("certs/{}", domain), &certser)
.await?;
let cert = Arc::new(Cert::new(certser)?);
self.certs
.write()
.unwrap()
.insert(domain.to_string(), cert.clone());
info!("Cert successfully renewed: {}", domain);
Ok(cert)
}
}
pub struct StoreResolver(pub Arc<CertStore>);
impl rustls::server::ResolvesServerCert for StoreResolver {
fn resolve(&self, client_hello: rustls::server::ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
let domain = client_hello.server_name()?;
let cert = futures::executor::block_on(self.0.get_cert(domain)).ok()?;
Some(cert.certkey.clone())
}
}