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)]
11pub struct Certificate {
12 pub domain: String,
14 pub cert_pem: Vec<u8>,
16 pub key_pem: Vec<u8>,
18 pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
20}
21
22pub struct CertificateStore {
24 pool: PgPool,
26 cache_dir: PathBuf,
28 cache: RwLock<std::collections::HashMap<String, Certificate>>,
30}
31
32impl CertificateStore {
33 pub async fn new(pool: PgPool, cache_dir: impl AsRef<Path>) -> ProxyResult<Self> {
35 let cache_dir = cache_dir.as_ref().to_path_buf();
36
37 tokio::fs::create_dir_all(&cache_dir).await?;
39
40 Ok(Self {
41 pool,
42 cache_dir,
43 cache: RwLock::new(std::collections::HashMap::new()),
44 })
45 }
46
47 pub async fn get(&self, domain: &str) -> Option<Certificate> {
49 {
51 let cache = self.cache.read().await;
52 if let Some(cert) = cache.get(domain) {
53 return Some(cert.clone());
54 }
55 }
56
57 if let Ok(cert) = self.load_from_file(domain).await {
59 let mut cache = self.cache.write().await;
60 cache.insert(domain.to_string(), cert.clone());
61 return Some(cert);
62 }
63
64 if let Ok(Some(cert)) = self.load_from_database(domain).await {
66 let _ = self.save_to_file(&cert).await;
68 let mut cache = self.cache.write().await;
69 cache.insert(domain.to_string(), cert.clone());
70 return Some(cert);
71 }
72
73 None
74 }
75
76 pub async fn save(&self, cert: Certificate) -> ProxyResult<()> {
78 self.save_to_database(&cert).await?;
80
81 self.save_to_file(&cert).await?;
83
84 let mut cache = self.cache.write().await;
86 cache.insert(cert.domain.clone(), cert);
87
88 Ok(())
89 }
90
91 pub async fn remove(&self, domain: &str) -> ProxyResult<()> {
93 sqlx::query("DELETE FROM proxy_certificates WHERE domain = $1")
95 .bind(domain)
96 .execute(&self.pool)
97 .await?;
98
99 let cert_path = self.cache_dir.join(format!("{}.crt", domain));
101 let key_path = self.cache_dir.join(format!("{}.key", domain));
102 let _ = tokio::fs::remove_file(cert_path).await;
103 let _ = tokio::fs::remove_file(key_path).await;
104
105 let mut cache = self.cache.write().await;
107 cache.remove(domain);
108
109 info!("Removed certificate for domain: {}", domain);
110 Ok(())
111 }
112
113 pub async fn list_domains(&self) -> ProxyResult<Vec<String>> {
115 let rows: Vec<(String,)> = sqlx::query_as("SELECT domain FROM proxy_certificates")
116 .fetch_all(&self.pool)
117 .await?;
118
119 Ok(rows.into_iter().map(|(d,)| d).collect())
120 }
121
122 async fn load_from_file(&self, domain: &str) -> ProxyResult<Certificate> {
123 let cert_path = self.cache_dir.join(format!("{}.crt", domain));
124 let key_path = self.cache_dir.join(format!("{}.key", domain));
125
126 let cert_pem = tokio::fs::read(&cert_path).await?;
127 let key_pem = tokio::fs::read(&key_path).await?;
128
129 debug!("Loaded certificate from file: {}", domain);
130
131 Ok(Certificate {
132 domain: domain.to_string(),
133 cert_pem,
134 key_pem,
135 expires_at: None, })
137 }
138
139 async fn save_to_file(&self, cert: &Certificate) -> ProxyResult<()> {
140 let cert_path = self.cache_dir.join(format!("{}.crt", cert.domain));
141 let key_path = self.cache_dir.join(format!("{}.key", cert.domain));
142
143 tokio::fs::write(&cert_path, &cert.cert_pem).await?;
144 tokio::fs::write(&key_path, &cert.key_pem).await?;
145
146 debug!("Saved certificate to file: {}", cert.domain);
147 Ok(())
148 }
149
150 async fn load_from_database(&self, domain: &str) -> ProxyResult<Option<Certificate>> {
151 let row: Option<(String, Vec<u8>, Vec<u8>, Option<chrono::DateTime<chrono::Utc>>)> =
152 sqlx::query_as(
153 "SELECT domain, cert_pem, key_pem, expires_at FROM proxy_certificates WHERE domain = $1",
154 )
155 .bind(domain)
156 .fetch_optional(&self.pool)
157 .await?;
158
159 Ok(
160 row.map(|(domain, cert_pem, key_pem, expires_at)| Certificate {
161 domain,
162 cert_pem,
163 key_pem,
164 expires_at,
165 }),
166 )
167 }
168
169 async fn save_to_database(&self, cert: &Certificate) -> ProxyResult<()> {
170 sqlx::query(
171 r#"
172 INSERT INTO proxy_certificates (domain, cert_pem, key_pem, expires_at, updated_at)
173 VALUES ($1, $2, $3, $4, NOW())
174 ON CONFLICT (domain) DO UPDATE SET
175 cert_pem = EXCLUDED.cert_pem,
176 key_pem = EXCLUDED.key_pem,
177 expires_at = EXCLUDED.expires_at,
178 updated_at = NOW()
179 "#,
180 )
181 .bind(&cert.domain)
182 .bind(&cert.cert_pem)
183 .bind(&cert.key_pem)
184 .bind(cert.expires_at)
185 .execute(&self.pool)
186 .await?;
187
188 info!("Saved certificate to database: {}", cert.domain);
189 Ok(())
190 }
191}