use std::{
fs::{create_dir_all, File},
io::{Read, Write},
thread::sleep,
time::Duration,
};
use async_trait::async_trait;
use instant_acme::{AccountCredentials, ChallengeType, LetsEncrypt, Order};
use pingora::{server::ShutdownWatch, services::background::BackgroundService};
use rcgen::KeyPair;
use serde::{Deserialize, Serialize};
use tracing::{info, instrument};
use crate::StorageArc;
#[derive(Serialize, Deserialize)]
struct HostCertificate {
host: String,
cert: String,
key: String,
expires_at: i64,
}
#[derive(Serialize, Deserialize)]
struct HostChallenge {
host: String,
url: String,
token: String,
key_auth: String,
}
#[derive(Serialize, Deserialize)]
struct HostOrder {
host: String,
url: String,
}
#[derive(Debug)]
pub struct HttpLetsencrypt {
challenge_type: ChallengeType,
url: String,
contact: String,
hosts: Vec<String>,
cert_store: StorageArc,
}
impl HttpLetsencrypt {
pub fn new(hosts: &[String], contact: &str, cert_store: StorageArc) -> Self {
HttpLetsencrypt {
challenge_type: ChallengeType::Http01,
url: LetsEncrypt::Staging.url().to_string(),
contact: contact.to_string(),
hosts: hosts.to_vec(),
cert_store,
}
}
async fn create_account(
&self,
) -> Result<(instant_acme::Account, instant_acme::AccountCredentials), ()> {
let mut existing_credentials: String = String::new();
let file = File::open(format!("{}/credentials.json", self.account_path()));
if let Ok(mut file) = file {
file.read_to_string(&mut existing_credentials).unwrap();
let credentials =
serde_json::from_str::<AccountCredentials>(&existing_credentials).unwrap();
let long_lived =
serde_json::from_str::<AccountCredentials>(&existing_credentials).unwrap();
let account = instant_acme::Account::from_credentials(credentials)
.await
.map_err(|acc| format!("Failed to fetch account {}", acc))
.unwrap();
println!("Fetched existing account");
return Ok((account, long_lived));
}
let new_account = instant_acme::NewAccount {
contact: &[&format!("mailto:{}", self.contact)],
terms_of_service_agreed: true,
only_return_existing: false,
};
let account = instant_acme::Account::create(&new_account, &self.url, None)
.await
.map_err(|acc| format!("Failed to create account {}", acc));
match account {
Ok((account, credentials)) => {
let file =
File::create(format!("{}/credentials.json", self.account_path())).unwrap();
serde_json::to_writer(file, &credentials).unwrap();
Ok((account, credentials))
}
Err(e) => {
println!("Failed to created account: {:?}", e);
Err(())
}
}
}
async fn create_order(&self, excluded_hosts: Vec<String>) -> Result<instant_acme::Order, ()> {
let (account, _credentials) = self.create_account().await.unwrap();
let mut identifiers = Vec::with_capacity(self.hosts.len());
for host in self.hosts.iter() {
if excluded_hosts.contains(host) {
continue;
}
let identifier = instant_acme::Identifier::Dns(host.to_owned());
identifiers.push(identifier);
}
if identifiers.is_empty() {
return Err(());
}
let order = account
.new_order(&instant_acme::NewOrder {
identifiers: &identifiers,
})
.await
.map_err(|order| format!("Failed to create order {}", order))
.unwrap();
Ok(order)
}
async fn create_challenges_from_order(&self, excluded_hosts: Vec<String>) -> Result<Order, ()> {
println!("Creating challenges from order");
let order = self.create_order(excluded_hosts).await;
if order.is_err() {
println!("Order error {:?}", order.err().unwrap());
return Err(());
}
let mut order_result = order.unwrap();
let authorizations = order_result.authorizations().await.unwrap();
for authz in &authorizations {
match authz.status {
instant_acme::AuthorizationStatus::Pending => continue,
instant_acme::AuthorizationStatus::Valid => {}
_ => return Err(()),
}
println!("Creating challenges for {:?}", authz.identifier);
let instant_acme::Identifier::Dns(identifier) = &authz.identifier;
let challenge = authz
.challenges
.iter()
.find(|cha| cha.r#type == self.challenge_type)
.ok_or(format!("No {:?} challenge found", self.challenge_type))
.unwrap();
let key_auth = order_result.key_authorization(challenge);
let mut lkd = self.cert_store.lock().await;
lkd.add_order(
identifier.clone(),
challenge.token.clone(),
challenge.url.clone(),
key_auth,
);
}
Ok(order_result)
}
fn challenges_path(&self) -> &str {
"./data/challenges"
}
fn certificates_path(&self) -> &str {
"./data/certificates"
}
fn account_path(&self) -> &str {
"./data/account"
}
fn orders_path(&self) -> &str {
"./data/orders"
}
}
#[async_trait]
impl BackgroundService for HttpLetsencrypt {
#[instrument]
async fn start(&self, _shutdown: ShutdownWatch) -> () {
info!(service = "LetsEncrypt", "Background service started");
create_dir_all(self.challenges_path()).unwrap();
create_dir_all(self.certificates_path()).unwrap();
create_dir_all(self.account_path()).unwrap();
create_dir_all(self.orders_path()).unwrap();
let mut excluded_hosts = Vec::new();
for host in self.hosts.iter() {
let file = std::fs::File::open(format!("./data/challenges/{}/meta.csv", host));
if file.is_ok() {
info!("Already found {} in the list of challenges", host);
excluded_hosts.push(host.clone());
}
}
if excluded_hosts.len() == self.hosts.len() {
info!("All hosts have a challenge file");
return;
}
let order = self
.create_challenges_from_order(excluded_hosts.clone())
.await;
if order.is_err() {
info!("No order to check");
return;
}
let mut order = order.unwrap();
let mut file = File::create(format!("{}/meta.txt", self.orders_path())).unwrap();
let contents = format!("{:?}", order.url());
file.write_all(contents.as_bytes()).unwrap();
file.flush().unwrap();
let lkd = self.cert_store.lock().await;
if lkd.get_orders().is_empty() {
info!("No orders to check");
return;
}
for (key, value) in lkd.get_orders().iter() {
let (token, url, key_auth) = value;
create_dir_all(format!("{}/{}", self.challenges_path(), key)).unwrap();
let mut file = File::create(format!("./data/challenges/{}/meta.csv", key)).unwrap();
let contents = format!("{};{};{}", url, key_auth.as_str(), token);
file.write_all(contents.as_bytes()).unwrap();
file.flush().unwrap();
println!("Setting challenge ready for {}", key);
order.set_challenge_ready(url).await.unwrap();
}
let max_retries = 10;
let mut current_retry = 0;
let mut retry_delay = 1;
while order.state().status != instant_acme::OrderStatus::Ready {
if current_retry >= max_retries {
info!("Max retries reached");
return;
}
info!(
"Waiting for order to be ready, attempt #{}...",
current_retry
);
sleep(Duration::from_secs(retry_delay));
order.refresh().await.unwrap();
current_retry += 1;
retry_delay *= 2;
}
let non_excluded_hosts = self
.hosts
.iter()
.filter(|&host| !excluded_hosts.contains(host))
.cloned()
.collect::<Vec<String>>();
info!("Generating certificates for {:?}", non_excluded_hosts);
let kp = KeyPair::generate().unwrap();
let mut cert_params = rcgen::CertificateParams::new(non_excluded_hosts.clone()).unwrap();
cert_params.distinguished_name = rcgen::DistinguishedName::new();
let csr = cert_params.serialize_request(&kp).unwrap();
let status = order.finalize(csr.der()).await;
if status.is_err() {
println!("Failed to finalize order: {:?}", status.err().unwrap());
return;
}
let cert_chain = loop {
match order.certificate().await {
Ok(Some(cert)) => {
info!("Cert ready");
break cert;
}
Ok(None) => {
info!("Cert not ready yet, waiting 5 seconds...");
sleep(Duration::from_secs(5));
}
Err(e) => {
info!("Error downloading cert: {:?}", e);
return;
}
}
};
for host in non_excluded_hosts.iter() {
create_dir_all(format!("{}/{}", self.certificates_path(), host)).unwrap();
let mut crt_file =
File::create(format!("{}/{}/cert.pem", self.certificates_path(), host)).unwrap();
let mut key_file =
File::create(format!("{}/{}/key.pem", self.certificates_path(), host)).unwrap();
crt_file.write_all(cert_chain.as_bytes()).unwrap();
key_file.write_all(kp.serialize_pem().as_bytes()).unwrap();
crt_file.flush().unwrap();
key_file.flush().unwrap();
info!("Certificate written to disk for {}", host);
}
return;
}
}