1use crate::error::ProxyResult;
4use sqlx::PgPool;
5use std::path::{Path, PathBuf};
6use tokio::sync::RwLock;
7use tracing::{debug, info};
8
9#[derive(Clone)]
14pub struct Certificate {
15 pub domain: String,
17 pub cert_pem: Vec<u8>,
19 pub key_pem: Vec<u8>,
21 pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
23}
24
25impl std::fmt::Debug for Certificate {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 f.debug_struct("Certificate")
28 .field("domain", &self.domain)
29 .field("expires_at", &self.expires_at)
30 .field("cert_pem", &format_args!("<{} bytes>", self.cert_pem.len()))
31 .field("key_pem", &format_args!("<redacted>"))
32 .finish()
33 }
34}
35
36pub struct CertificateStore {
38 pool: PgPool,
40 cache_dir: PathBuf,
42 cache: RwLock<std::collections::HashMap<String, Certificate>>,
44}
45
46impl CertificateStore {
47 pub async fn new(pool: PgPool, cache_dir: impl AsRef<Path>) -> ProxyResult<Self> {
49 let cache_dir = cache_dir.as_ref().to_path_buf();
50
51 tokio::fs::create_dir_all(&cache_dir).await?;
53
54 Ok(Self {
55 pool,
56 cache_dir,
57 cache: RwLock::new(std::collections::HashMap::new()),
58 })
59 }
60
61 pub async fn get(&self, domain: &str) -> Option<Certificate> {
63 {
65 let cache = self.cache.read().await;
66 if let Some(cert) = cache.get(domain) {
67 return Some(cert.clone());
68 }
69 }
70
71 if let Ok(cert) = self.load_from_file(domain).await {
73 let mut cache = self.cache.write().await;
74 cache.insert(domain.to_string(), cert.clone());
75 return Some(cert);
76 }
77
78 if let Ok(Some(cert)) = self.load_from_database(domain).await {
80 let _ = self.save_to_file(&cert).await;
82 let mut cache = self.cache.write().await;
83 cache.insert(domain.to_string(), cert.clone());
84 return Some(cert);
85 }
86
87 None
88 }
89
90 pub async fn save(&self, cert: Certificate) -> ProxyResult<()> {
92 self.save_to_database(&cert).await?;
94
95 self.save_to_file(&cert).await?;
97
98 let mut cache = self.cache.write().await;
100 cache.insert(cert.domain.clone(), cert);
101
102 Ok(())
103 }
104
105 pub async fn remove(&self, domain: &str) -> ProxyResult<()> {
107 sqlx::query("DELETE FROM proxy_certificates WHERE domain = $1")
109 .bind(domain)
110 .execute(&self.pool)
111 .await?;
112
113 let cert_path = self.cache_dir.join(format!("{}.crt", domain));
115 let key_path = self.cache_dir.join(format!("{}.key", domain));
116 let _ = tokio::fs::remove_file(cert_path).await;
117 let _ = tokio::fs::remove_file(key_path).await;
118
119 let mut cache = self.cache.write().await;
121 cache.remove(domain);
122
123 info!("Removed certificate for domain: {}", domain);
124 Ok(())
125 }
126
127 pub async fn list_domains(&self) -> ProxyResult<Vec<String>> {
129 let rows: Vec<(String,)> = sqlx::query_as("SELECT domain FROM proxy_certificates")
130 .fetch_all(&self.pool)
131 .await?;
132
133 Ok(rows.into_iter().map(|(d,)| d).collect())
134 }
135
136 async fn load_from_file(&self, domain: &str) -> ProxyResult<Certificate> {
137 let cert_path = self.cache_dir.join(format!("{}.crt", domain));
138 let key_path = self.cache_dir.join(format!("{}.key", domain));
139
140 let cert_pem = tokio::fs::read(&cert_path).await?;
141 let key_pem = tokio::fs::read(&key_path).await?;
142
143 debug!("Loaded certificate from file: {}", domain);
144
145 Ok(Certificate {
146 domain: domain.to_string(),
147 cert_pem,
148 key_pem,
149 expires_at: None, })
151 }
152
153 async fn save_to_file(&self, cert: &Certificate) -> ProxyResult<()> {
154 let cert_path = self.cache_dir.join(format!("{}.crt", cert.domain));
155 let key_path = self.cache_dir.join(format!("{}.key", cert.domain));
156
157 tokio::fs::write(&cert_path, &cert.cert_pem).await?;
158 tokio::fs::write(&key_path, &cert.key_pem).await?;
159
160 debug!("Saved certificate to file: {}", cert.domain);
161 Ok(())
162 }
163
164 pub async fn load_all(&self) -> ProxyResult<Vec<Certificate>> {
175 let rows: Vec<(
176 String,
177 Vec<u8>,
178 Vec<u8>,
179 Option<chrono::DateTime<chrono::Utc>>,
180 )> = sqlx::query_as("SELECT domain, cert_pem, key_pem, expires_at FROM proxy_certificates")
181 .fetch_all(&self.pool)
182 .await?;
183
184 let certificates: Vec<Certificate> = rows
185 .into_iter()
186 .map(|(domain, cert_pem, key_pem, expires_at)| Certificate {
187 domain,
188 cert_pem,
189 key_pem,
190 expires_at,
191 })
192 .collect();
193
194 let mut cache = self.cache.write().await;
195 *cache = certificates
196 .iter()
197 .map(|c| (c.domain.clone(), c.clone()))
198 .collect();
199
200 debug!(
201 "Loaded {} certificate(s) from the database",
202 certificates.len()
203 );
204 Ok(certificates)
205 }
206
207 async fn load_from_database(&self, domain: &str) -> ProxyResult<Option<Certificate>> {
208 let row: Option<(String, Vec<u8>, Vec<u8>, Option<chrono::DateTime<chrono::Utc>>)> =
209 sqlx::query_as(
210 "SELECT domain, cert_pem, key_pem, expires_at FROM proxy_certificates WHERE domain = $1",
211 )
212 .bind(domain)
213 .fetch_optional(&self.pool)
214 .await?;
215
216 Ok(
217 row.map(|(domain, cert_pem, key_pem, expires_at)| Certificate {
218 domain,
219 cert_pem,
220 key_pem,
221 expires_at,
222 }),
223 )
224 }
225
226 async fn save_to_database(&self, cert: &Certificate) -> ProxyResult<()> {
227 sqlx::query(
228 r#"
229 INSERT INTO proxy_certificates (domain, cert_pem, key_pem, expires_at, updated_at)
230 VALUES ($1, $2, $3, $4, NOW())
231 ON CONFLICT (domain) DO UPDATE SET
232 cert_pem = EXCLUDED.cert_pem,
233 key_pem = EXCLUDED.key_pem,
234 expires_at = EXCLUDED.expires_at,
235 updated_at = NOW()
236 "#,
237 )
238 .bind(&cert.domain)
239 .bind(&cert.cert_pem)
240 .bind(&cert.key_pem)
241 .bind(cert.expires_at)
242 .execute(&self.pool)
243 .await?;
244
245 info!("Saved certificate to database: {}", cert.domain);
246 Ok(())
247 }
248}