use crate::{challenge::*, config::*, directory::*, error::*};
use std::path::Path;
#[derive(Debug)]
pub struct AcmeClient {
config: AcmeConfig,
directory: Directory,
#[allow(dead_code)]
http_client: reqwest::Client,
account_url: Option<String>,
}
impl AcmeClient {
pub async fn new(config: AcmeConfig) -> Result<Self> {
let http_client = reqwest::Client::builder()
.use_rustls_tls()
.build()
.map_err(|e| AcmeError::Internal(e.to_string()))?;
let directory = Self::fetch_directory(&http_client, &config.directory_url).await?;
Ok(Self {
config,
directory,
http_client,
account_url: None,
})
}
async fn fetch_directory(client: &reqwest::Client, url: &str) -> Result<Directory> {
let response = client.get(url).send().await?;
if !response.status().is_success() {
return Err(AcmeError::InvalidDirectory(format!(
"Failed to fetch directory: {}",
response.status()
)));
}
let directory = response.json().await?;
Ok(directory)
}
pub async fn register_account(&mut self) -> Result<()> {
tracing::info!("Registering ACME account");
self.account_url = Some(self.directory.new_account.clone());
Ok(())
}
pub async fn create_order(&self) -> Result<String> {
if self.account_url.is_none() {
return Err(AcmeError::InvalidAccount(
"Account not registered".to_string(),
));
}
tracing::info!(
"Creating certificate order for domains: {:?}",
self.config.domains
);
Ok(self.directory.new_order.clone())
}
pub async fn get_challenges(&self, _order_url: &str) -> Result<Vec<Http01Challenge>> {
tracing::info!("Fetching challenges");
Ok(Vec::new())
}
pub async fn notify_challenge_ready(&self, _challenge_url: &str) -> Result<()> {
tracing::info!("Notifying challenge ready");
Ok(())
}
pub async fn finalize_order(&self, _order_url: &str) -> Result<(String, String)> {
tracing::info!("Finalizing order");
Ok((String::new(), String::new()))
}
pub async fn order_certificate(&mut self) -> Result<(String, String)> {
if self.account_url.is_none() {
self.register_account().await?;
}
let order_url = self.create_order().await?;
let _challenges = self.get_challenges(&order_url).await?;
self.finalize_order(&order_url).await
}
pub async fn should_renew(&self, cert_path: impl AsRef<Path>) -> Result<bool> {
let cert_path = cert_path.as_ref();
if !cert_path.exists() {
return Ok(true);
}
Ok(false)
}
pub async fn save_certificate(
&self,
cert_pem: &str,
key_pem: &str,
) -> Result<(String, String)> {
use std::fs;
fs::create_dir_all(&self.config.cert_dir)?;
let cert_path = self.config.cert_dir.join("cert.pem");
let key_path = self.config.cert_dir.join("key.pem");
fs::write(&cert_path, cert_pem)?;
fs::write(&key_path, key_pem)?;
Ok((
cert_path.to_string_lossy().to_string(),
key_path.to_string_lossy().to_string(),
))
}
pub fn directory(&self) -> &Directory {
&self.directory
}
pub fn config(&self) -> &AcmeConfig {
&self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_fetch_directory_invalid_url() {
let client = reqwest::Client::new();
let result = AcmeClient::fetch_directory(&client, "https://invalid.example.com").await;
assert!(result.is_err());
}
#[test]
fn test_client_config_access() {
let config = AcmeConfig::lets_encrypt_staging(
vec!["admin@example.com".to_string()],
vec!["example.com".to_string()],
);
let domains = config.domains.clone();
assert_eq!(domains, vec!["example.com"]);
}
}