postrust_proxy/tls/
cert_store.rs1use 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 async fn load_from_database(&self, domain: &str) -> ProxyResult<Option<Certificate>> {
165 let row: Option<(String, Vec<u8>, Vec<u8>, Option<chrono::DateTime<chrono::Utc>>)> =
166 sqlx::query_as(
167 "SELECT domain, cert_pem, key_pem, expires_at FROM proxy_certificates WHERE domain = $1",
168 )
169 .bind(domain)
170 .fetch_optional(&self.pool)
171 .await?;
172
173 Ok(
174 row.map(|(domain, cert_pem, key_pem, expires_at)| Certificate {
175 domain,
176 cert_pem,
177 key_pem,
178 expires_at,
179 }),
180 )
181 }
182
183 async fn save_to_database(&self, cert: &Certificate) -> ProxyResult<()> {
184 sqlx::query(
185 r#"
186 INSERT INTO proxy_certificates (domain, cert_pem, key_pem, expires_at, updated_at)
187 VALUES ($1, $2, $3, $4, NOW())
188 ON CONFLICT (domain) DO UPDATE SET
189 cert_pem = EXCLUDED.cert_pem,
190 key_pem = EXCLUDED.key_pem,
191 expires_at = EXCLUDED.expires_at,
192 updated_at = NOW()
193 "#,
194 )
195 .bind(&cert.domain)
196 .bind(&cert.cert_pem)
197 .bind(&cert.key_pem)
198 .bind(cert.expires_at)
199 .execute(&self.pool)
200 .await?;
201
202 info!("Saved certificate to database: {}", cert.domain);
203 Ok(())
204 }
205}