use crate::error::ProxyResult;
use sqlx::PgPool;
use std::path::{Path, PathBuf};
use tokio::sync::RwLock;
use tracing::{debug, info};
#[derive(Clone)]
pub struct Certificate {
pub domain: String,
pub cert_pem: Vec<u8>,
pub key_pem: Vec<u8>,
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
}
pub struct CertificateStore {
pool: PgPool,
cache_dir: PathBuf,
cache: RwLock<std::collections::HashMap<String, Certificate>>,
}
impl CertificateStore {
pub async fn new(pool: PgPool, cache_dir: impl AsRef<Path>) -> ProxyResult<Self> {
let cache_dir = cache_dir.as_ref().to_path_buf();
tokio::fs::create_dir_all(&cache_dir).await?;
Ok(Self {
pool,
cache_dir,
cache: RwLock::new(std::collections::HashMap::new()),
})
}
pub async fn get(&self, domain: &str) -> Option<Certificate> {
{
let cache = self.cache.read().await;
if let Some(cert) = cache.get(domain) {
return Some(cert.clone());
}
}
if let Ok(cert) = self.load_from_file(domain).await {
let mut cache = self.cache.write().await;
cache.insert(domain.to_string(), cert.clone());
return Some(cert);
}
if let Ok(Some(cert)) = self.load_from_database(domain).await {
let _ = self.save_to_file(&cert).await;
let mut cache = self.cache.write().await;
cache.insert(domain.to_string(), cert.clone());
return Some(cert);
}
None
}
pub async fn save(&self, cert: Certificate) -> ProxyResult<()> {
self.save_to_database(&cert).await?;
self.save_to_file(&cert).await?;
let mut cache = self.cache.write().await;
cache.insert(cert.domain.clone(), cert);
Ok(())
}
pub async fn remove(&self, domain: &str) -> ProxyResult<()> {
sqlx::query("DELETE FROM proxy_certificates WHERE domain = $1")
.bind(domain)
.execute(&self.pool)
.await?;
let cert_path = self.cache_dir.join(format!("{}.crt", domain));
let key_path = self.cache_dir.join(format!("{}.key", domain));
let _ = tokio::fs::remove_file(cert_path).await;
let _ = tokio::fs::remove_file(key_path).await;
let mut cache = self.cache.write().await;
cache.remove(domain);
info!("Removed certificate for domain: {}", domain);
Ok(())
}
pub async fn list_domains(&self) -> ProxyResult<Vec<String>> {
let rows: Vec<(String,)> = sqlx::query_as("SELECT domain FROM proxy_certificates")
.fetch_all(&self.pool)
.await?;
Ok(rows.into_iter().map(|(d,)| d).collect())
}
async fn load_from_file(&self, domain: &str) -> ProxyResult<Certificate> {
let cert_path = self.cache_dir.join(format!("{}.crt", domain));
let key_path = self.cache_dir.join(format!("{}.key", domain));
let cert_pem = tokio::fs::read(&cert_path).await?;
let key_pem = tokio::fs::read(&key_path).await?;
debug!("Loaded certificate from file: {}", domain);
Ok(Certificate {
domain: domain.to_string(),
cert_pem,
key_pem,
expires_at: None, })
}
async fn save_to_file(&self, cert: &Certificate) -> ProxyResult<()> {
let cert_path = self.cache_dir.join(format!("{}.crt", cert.domain));
let key_path = self.cache_dir.join(format!("{}.key", cert.domain));
tokio::fs::write(&cert_path, &cert.cert_pem).await?;
tokio::fs::write(&key_path, &cert.key_pem).await?;
debug!("Saved certificate to file: {}", cert.domain);
Ok(())
}
async fn load_from_database(&self, domain: &str) -> ProxyResult<Option<Certificate>> {
let row: Option<(String, Vec<u8>, Vec<u8>, Option<chrono::DateTime<chrono::Utc>>)> =
sqlx::query_as(
"SELECT domain, cert_pem, key_pem, expires_at FROM proxy_certificates WHERE domain = $1",
)
.bind(domain)
.fetch_optional(&self.pool)
.await?;
Ok(
row.map(|(domain, cert_pem, key_pem, expires_at)| Certificate {
domain,
cert_pem,
key_pem,
expires_at,
}),
)
}
async fn save_to_database(&self, cert: &Certificate) -> ProxyResult<()> {
sqlx::query(
r#"
INSERT INTO proxy_certificates (domain, cert_pem, key_pem, expires_at, updated_at)
VALUES ($1, $2, $3, $4, NOW())
ON CONFLICT (domain) DO UPDATE SET
cert_pem = EXCLUDED.cert_pem,
key_pem = EXCLUDED.key_pem,
expires_at = EXCLUDED.expires_at,
updated_at = NOW()
"#,
)
.bind(&cert.domain)
.bind(&cert.cert_pem)
.bind(&cert.key_pem)
.bind(cert.expires_at)
.execute(&self.pool)
.await?;
info!("Saved certificate to database: {}", cert.domain);
Ok(())
}
}