1use std::{fmt::Write as _, path::Path, sync::Arc};
11
12use fraiseql_core::security::{TlsConfig, TlsEnforcer, TlsVersion};
13use rustls::{ServerConfig, pki_types::CertificateDer};
14use rustls_pemfile::Item;
15use tracing::info;
16
17use crate::{
18 Result, ServerError,
19 server_config::{DatabaseTlsConfig, TlsServerConfig},
20};
21
22pub struct TlsSetup {
24 enforcer: TlsEnforcer,
26
27 config: Option<TlsServerConfig>,
29
30 db_config: Option<DatabaseTlsConfig>,
32}
33
34impl TlsSetup {
35 pub fn new(
43 tls_config: Option<TlsServerConfig>,
44 db_tls_config: Option<DatabaseTlsConfig>,
45 ) -> Result<Self> {
46 let enforcer = if let Some(ref tls) = tls_config {
48 if tls.enabled {
49 Self::create_enforcer(tls)?
50 } else {
51 TlsEnforcer::permissive()
52 }
53 } else {
54 TlsEnforcer::permissive()
55 };
56
57 Ok(Self {
58 enforcer,
59 config: tls_config,
60 db_config: db_tls_config,
61 })
62 }
63
64 fn create_enforcer(config: &TlsServerConfig) -> Result<TlsEnforcer> {
66 let min_version = match config.min_version.as_str() {
68 "1.2" => TlsVersion::V1_2,
69 "1.3" => TlsVersion::V1_3,
70 other => {
71 return Err(ServerError::ConfigError(format!(
72 "Invalid TLS minimum version: {}",
73 other
74 )));
75 },
76 };
77
78 let tls_config = TlsConfig {
80 tls_required: true,
81 mtls_required: config.require_client_cert,
82 min_version,
83 };
84
85 info!(
86 tls_enabled = true,
87 require_mtls = config.require_client_cert,
88 min_version = %min_version,
89 "TLS configuration loaded"
90 );
91
92 Ok(TlsEnforcer::from_config(tls_config))
93 }
94
95 #[must_use]
97 pub const fn enforcer(&self) -> &TlsEnforcer {
98 &self.enforcer
99 }
100
101 #[must_use]
103 pub const fn config(&self) -> &Option<TlsServerConfig> {
104 &self.config
105 }
106
107 #[must_use]
109 pub const fn db_config(&self) -> &Option<DatabaseTlsConfig> {
110 &self.db_config
111 }
112
113 #[must_use]
115 pub fn is_tls_enabled(&self) -> bool {
116 self.config.as_ref().is_some_and(|c| c.enabled)
117 }
118
119 #[must_use]
121 pub fn is_mtls_required(&self) -> bool {
122 self.config.as_ref().is_some_and(|c| c.enabled && c.require_client_cert)
123 }
124
125 #[must_use]
127 pub fn cert_path(&self) -> Option<&Path> {
128 self.config.as_ref().map(|c| c.cert_path.as_path())
129 }
130
131 #[must_use]
133 pub fn key_path(&self) -> Option<&Path> {
134 self.config.as_ref().map(|c| c.key_path.as_path())
135 }
136
137 #[must_use]
139 pub fn client_ca_path(&self) -> Option<&Path> {
140 self.config
141 .as_ref()
142 .and_then(|c| c.client_ca_path.as_ref())
143 .map(|p| p.as_path())
144 }
145
146 #[must_use]
148 pub fn postgres_ssl_mode(&self) -> &str {
149 self.db_config.as_ref().map_or("prefer", |c| c.postgres_ssl_mode.as_str())
150 }
151
152 #[must_use]
154 pub fn redis_ssl_enabled(&self) -> bool {
155 self.db_config.as_ref().is_some_and(|c| c.redis_ssl)
156 }
157
158 #[must_use]
160 pub fn clickhouse_https_enabled(&self) -> bool {
161 self.db_config.as_ref().is_some_and(|c| c.clickhouse_https)
162 }
163
164 #[must_use]
166 pub fn elasticsearch_https_enabled(&self) -> bool {
167 self.db_config.as_ref().is_some_and(|c| c.elasticsearch_https)
168 }
169
170 #[must_use]
172 pub fn verify_certificates(&self) -> bool {
173 self.db_config.as_ref().is_none_or(|c| c.verify_certificates)
174 }
175
176 #[must_use]
178 pub fn ca_bundle_path(&self) -> Option<&Path> {
179 self.db_config
180 .as_ref()
181 .and_then(|c| c.ca_bundle_path.as_ref())
182 .map(|p| p.as_path())
183 }
184
185 #[must_use]
187 pub fn apply_postgres_tls(&self, db_url: &str) -> String {
188 let mut url = db_url.to_string();
189
190 let ssl_mode = self.postgres_ssl_mode();
192 if !ssl_mode.is_empty() && ssl_mode != "prefer" {
193 if url.contains('?') {
195 let _ = write!(url, "&sslmode={ssl_mode}");
196 } else {
197 let _ = write!(url, "?sslmode={ssl_mode}");
198 }
199 }
200
201 url
202 }
203
204 #[must_use]
206 pub fn apply_redis_tls(&self, redis_url: &str) -> String {
207 if self.redis_ssl_enabled() {
208 redis_url.replace("redis://", "rediss://")
210 } else {
211 redis_url.to_string()
212 }
213 }
214
215 #[must_use]
217 pub fn apply_clickhouse_tls(&self, ch_url: &str) -> String {
218 if self.clickhouse_https_enabled() {
219 ch_url.replace("http://", "https://")
221 } else {
222 ch_url.to_string()
223 }
224 }
225
226 #[must_use]
228 pub fn apply_elasticsearch_tls(&self, es_url: &str) -> String {
229 if self.elasticsearch_https_enabled() {
230 es_url.replace("http://", "https://")
232 } else {
233 es_url.to_string()
234 }
235 }
236
237 fn load_certificates(path: &Path) -> Result<Vec<CertificateDer<'static>>> {
239 let cert_file = std::fs::File::open(path).map_err(|e| {
240 ServerError::ConfigError(format!(
241 "Failed to open certificate file {}: {}",
242 path.display(),
243 e
244 ))
245 })?;
246
247 let mut reader = std::io::BufReader::new(cert_file);
248 let mut certificates = Vec::new();
249
250 loop {
251 match rustls_pemfile::read_one(&mut reader).map_err(|e| {
252 ServerError::ConfigError(format!("Failed to parse certificate: {}", e))
253 })? {
254 Some(Item::X509Certificate(cert)) => certificates.push(cert),
255 Some(_) => {}, None => break,
257 }
258 }
259
260 if certificates.is_empty() {
261 return Err(ServerError::ConfigError(
262 "No certificates found in certificate file".to_string(),
263 ));
264 }
265
266 Ok(certificates)
267 }
268
269 fn load_private_key(path: &Path) -> Result<rustls::pki_types::PrivateKeyDer<'static>> {
271 let key_file = std::fs::File::open(path).map_err(|e| {
272 ServerError::ConfigError(format!("Failed to open key file {}: {}", path.display(), e))
273 })?;
274
275 let mut reader = std::io::BufReader::new(key_file);
276
277 loop {
278 match rustls_pemfile::read_one(&mut reader).map_err(|e| {
279 ServerError::ConfigError(format!("Failed to parse private key: {}", e))
280 })? {
281 Some(Item::Pkcs8Key(key)) => return Ok(key.into()),
282 Some(Item::Pkcs1Key(key)) => return Ok(key.into()),
283 Some(Item::Sec1Key(key)) => return Ok(key.into()),
284 Some(_) => {}, None => break,
286 }
287 }
288
289 Err(ServerError::ConfigError("No private key found in key file".to_string()))
290 }
291
292 pub fn create_rustls_config(&self) -> Result<Arc<ServerConfig>> {
300 let (cert_path, key_path) = match self.config.as_ref() {
301 Some(c) if c.enabled => (&c.cert_path, &c.key_path),
302 _ => return Err(ServerError::ConfigError("TLS not enabled".to_string())),
303 };
304
305 info!(
306 cert_path = %cert_path.display(),
307 key_path = %key_path.display(),
308 "Loading TLS certificates"
309 );
310
311 let certs = Self::load_certificates(cert_path)?;
312 let key = Self::load_private_key(key_path)?;
313
314 let server_config = ServerConfig::builder()
315 .with_no_client_auth()
316 .with_single_cert(certs, key)
317 .map_err(|e| ServerError::ConfigError(format!("Failed to build TLS config: {}", e)))?;
318
319 Ok(Arc::new(server_config))
320 }
321}