1use std::path::PathBuf;
10use std::sync::{Arc, Mutex};
11
12use async_trait::async_trait;
13use camel_api::CamelError;
14use rustls::ServerConfig;
15
16#[derive(Debug, Clone)]
21pub struct ServerTlsSource {
22 pub cert_path: PathBuf,
24 pub key_path: PathBuf,
26 pub client_ca_path: Option<PathBuf>,
29}
30
31impl ServerTlsSource {
32 pub fn build_server_config(&self) -> Result<ServerConfig, CamelError> {
40 let cert_pem = std::fs::read(&self.cert_path).map_err(|e| {
42 CamelError::EndpointCreationFailed(format!(
43 "TLS cert file '{}': {e}",
44 self.cert_path.display()
45 ))
46 })?;
47 let key_pem = std::fs::read(&self.key_path).map_err(|e| {
48 CamelError::EndpointCreationFailed(format!(
49 "TLS key file '{}': {e}",
50 self.key_path.display()
51 ))
52 })?;
53
54 let certs: Vec<_> = rustls_pemfile::certs(&mut cert_pem.as_slice())
56 .collect::<Result<Vec<_>, _>>()
57 .map_err(|e| {
58 CamelError::EndpointCreationFailed(format!("TLS cert parse error: {e}"))
59 })?;
60
61 let key = rustls_pemfile::private_key(&mut key_pem.as_slice())
63 .map_err(|e| CamelError::EndpointCreationFailed(format!("TLS key parse error: {e}")))?
64 .ok_or_else(|| {
65 CamelError::EndpointCreationFailed("TLS: no private key found in key PEM".into())
66 })?;
67
68 let provider = Arc::new(rustls::crypto::ring::default_provider());
71
72 let builder = rustls::ServerConfig::builder_with_provider(Arc::clone(&provider))
74 .with_safe_default_protocol_versions()
75 .map_err(|e| {
76 CamelError::EndpointCreationFailed(format!("rustls protocol versions: {e}"))
77 })?;
78
79 let config = match &self.client_ca_path {
81 Some(ca_path) => {
82 let ca_pem = std::fs::read(ca_path).map_err(|e| {
83 CamelError::EndpointCreationFailed(format!(
84 "TLS client CA file '{}': {e}",
85 ca_path.display()
86 ))
87 })?;
88 let ca_certs: Vec<_> = rustls_pemfile::certs(&mut ca_pem.as_slice())
89 .collect::<Result<Vec<_>, _>>()
90 .map_err(|e| {
91 CamelError::EndpointCreationFailed(format!("invalid client CA PEM: {e}"))
92 })?;
93 let mut roots = rustls::RootCertStore::empty();
94 for cert in ca_certs {
95 roots.add(cert).map_err(|e| {
96 CamelError::EndpointCreationFailed(format!("client CA root add: {e}"))
97 })?;
98 }
99 let verifier = rustls::server::WebPkiClientVerifier::builder_with_provider(
100 Arc::new(roots),
101 Arc::clone(&provider),
102 )
103 .build()
104 .map_err(|e| {
105 CamelError::EndpointCreationFailed(format!("mTLS client cert verifier: {e}"))
106 })?;
107 builder
108 .with_client_cert_verifier(verifier)
109 .with_single_cert(certs, key)
110 .map_err(|e| {
111 CamelError::EndpointCreationFailed(format!("rustls ServerConfig: {e}"))
112 })?
113 }
114 None => builder
115 .with_no_client_auth()
116 .with_single_cert(certs, key)
117 .map_err(|e| {
118 CamelError::EndpointCreationFailed(format!("rustls ServerConfig: {e}"))
119 })?,
120 };
121
122 Ok(config)
123 }
124}
125
126#[async_trait]
133pub trait TlsReloadHandler: Send + Sync {
134 fn matches(&self, scheme: &str, host: &str, port: u16) -> bool;
136 async fn reload(&self) -> Result<(), CamelError>;
138}
139
140#[derive(Default)]
152pub struct TlsReloadRegistry {
153 handlers: Mutex<Vec<Arc<dyn TlsReloadHandler>>>,
154}
155
156impl TlsReloadRegistry {
157 pub fn global() -> &'static TlsReloadRegistry {
159 Self::backing().as_ref()
160 }
161
162 pub fn global_arc() -> Arc<TlsReloadRegistry> {
166 Self::backing().clone()
167 }
168
169 fn backing() -> &'static Arc<TlsReloadRegistry> {
174 static BACKING: std::sync::OnceLock<Arc<TlsReloadRegistry>> = std::sync::OnceLock::new();
175 BACKING.get_or_init(|| Arc::new(TlsReloadRegistry::default()))
176 }
177
178 pub fn register(&self, handler: Arc<dyn TlsReloadHandler>) {
180 let mut guard = self
181 .handlers
182 .lock()
183 .expect("TlsReloadRegistry lock poisoned"); guard.push(handler);
185 }
186
187 pub fn find(&self, scheme: &str, host: &str, port: u16) -> Option<Arc<dyn TlsReloadHandler>> {
189 let guard = self
190 .handlers
191 .lock()
192 .expect("TlsReloadRegistry lock poisoned"); guard
194 .iter()
195 .find(|h| h.matches(scheme, host, port))
196 .cloned()
197 }
198
199 pub fn unregister(&self, scheme: &str, host: &str, port: u16) {
201 let mut guard = self
202 .handlers
203 .lock()
204 .expect("TlsReloadRegistry lock poisoned"); guard.retain(|h| !h.matches(scheme, host, port));
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 fn write_pem(pem: &str, name: &str) -> PathBuf {
215 crate::test_support::tls::write_pem_tmp(name, pem)
216 }
217
218 #[test]
219 fn build_server_config_valid_cert() {
220 let (ca_pem, cert_pem, key_pem) = crate::test_support::tls::gen_server_cert();
221 let _ca = write_pem(&ca_pem, "ca.pem");
222 let cert = write_pem(&cert_pem, "cert.pem");
223 let key = write_pem(&key_pem, "key.pem");
224
225 let source = ServerTlsSource {
226 cert_path: cert,
227 key_path: key,
228 client_ca_path: None,
229 };
230
231 let config = source.build_server_config().expect("valid cert+key");
232 assert!(config.alpn_protocols.is_empty(), "ALPN should be empty");
233 }
234
235 #[test]
236 fn build_server_config_mtls() {
237 let (ca_pem, cert_pem, key_pem) = crate::test_support::tls::gen_server_cert();
238 let ca = write_pem(&ca_pem, "mtls-ca.pem");
239 let cert = write_pem(&cert_pem, "cert.pem");
240 let key = write_pem(&key_pem, "key.pem");
241
242 let source = ServerTlsSource {
243 cert_path: cert,
244 key_path: key,
245 client_ca_path: Some(ca),
246 };
247
248 let config = source.build_server_config().expect("mTLS should work");
249 assert!(config.alpn_protocols.is_empty());
250 }
255
256 #[test]
257 fn build_server_config_mtls_bad_ca_rejected() {
258 let (_, cert_pem, key_pem) = crate::test_support::tls::gen_server_cert();
259 let cert = write_pem(&cert_pem, "cert.pem");
260 let key = write_pem(&key_pem, "key.pem");
261
262 let bad_ca = write_pem("not-a-valid-ca-certificate\n", "bad-ca.pem");
264
265 let source = ServerTlsSource {
266 cert_path: cert,
267 key_path: key,
268 client_ca_path: Some(bad_ca),
269 };
270
271 let err = source.build_server_config().unwrap_err();
272 assert!(
273 matches!(&err, CamelError::EndpointCreationFailed(_)),
274 "expected EndpointCreationFailed, got {err:?}"
275 );
276 }
277
278 #[test]
279 fn build_server_config_missing_file() {
280 let source = ServerTlsSource {
281 cert_path: PathBuf::from("/nonexistent/cert.pem"),
282 key_path: PathBuf::from("/nonexistent/key.pem"),
283 client_ca_path: None,
284 };
285
286 let err = source.build_server_config().unwrap_err();
287 assert!(
288 matches!(&err, CamelError::EndpointCreationFailed(_)),
289 "expected EndpointCreationFailed, got {err:?}"
290 );
291 }
292
293 #[test]
294 fn build_server_config_malformed_pem() {
295 let cert = write_pem("garbage-content\n", "bad-cert.pem");
296 let key = write_pem("also-garbage\n", "bad-key.pem");
297
298 let source = ServerTlsSource {
299 cert_path: cert,
300 key_path: key,
301 client_ca_path: None,
302 };
303
304 let err = source.build_server_config().unwrap_err();
305 assert!(
306 matches!(&err, CamelError::EndpointCreationFailed(_)),
307 "expected EndpointCreationFailed, got {err:?}"
308 );
309 }
310}
311
312#[cfg(test)]
313mod registry_tests {
314 use super::*;
315
316 struct FakeHandler {
317 scheme: String,
318 host: String,
319 port: u16,
320 }
321
322 #[async_trait::async_trait]
323 impl TlsReloadHandler for FakeHandler {
324 fn matches(&self, scheme: &str, host: &str, port: u16) -> bool {
325 self.scheme == scheme && self.host == host && self.port == port
326 }
327 async fn reload(&self) -> Result<(), CamelError> {
328 Ok(())
329 }
330 }
331
332 #[test]
333 fn registry_find_returns_matching_handler() {
334 let reg = TlsReloadRegistry::default();
335 reg.register(Arc::new(FakeHandler {
336 scheme: "grpcs".into(),
337 host: "0.0.0.0".into(),
338 port: 9090,
339 }));
340 assert!(reg.find("grpcs", "0.0.0.0", 9090).is_some());
341 assert!(reg.find("https", "0.0.0.0", 9090).is_none());
342 }
343
344 #[test]
345 fn registry_find_returns_none_when_empty() {
346 let reg = TlsReloadRegistry::default();
347 assert!(reg.find("grpcs", "0.0.0.0", 9090).is_none());
348 }
349
350 #[test]
351 fn registry_unregister_removes_handler() {
352 let reg = TlsReloadRegistry::default();
353 reg.register(Arc::new(FakeHandler {
354 scheme: "grpcs".into(),
355 host: "0.0.0.0".into(),
356 port: 9090,
357 }));
358 assert!(reg.find("grpcs", "0.0.0.0", 9090).is_some());
359 reg.unregister("grpcs", "0.0.0.0", 9090);
360 assert!(reg.find("grpcs", "0.0.0.0", 9090).is_none());
361 }
362
363 #[test]
364 fn global_and_global_arc_share_instance() {
365 const SCHEME: &str = "tlsseam-shared";
366 const HOST: &str = "shared.invalid";
367 const PORT: u16 = 64401;
368
369 let arc = TlsReloadRegistry::global_arc();
370 assert!(std::ptr::eq(TlsReloadRegistry::global(), arc.as_ref()));
371
372 arc.register(Arc::new(FakeHandler {
373 scheme: SCHEME.into(),
374 host: HOST.into(),
375 port: PORT,
376 }));
377 assert!(
378 TlsReloadRegistry::global()
379 .find(SCHEME, HOST, PORT)
380 .is_some(),
381 "registration via global_arc must be visible through global()"
382 );
383 TlsReloadRegistry::global().unregister(SCHEME, HOST, PORT);
384 assert!(
385 TlsReloadRegistry::global()
386 .find(SCHEME, HOST, PORT)
387 .is_none(),
388 "cleanup must leave no residue for sibling tests"
389 );
390 }
391
392 #[test]
393 fn default_instance_is_isolated_from_global() {
394 const SCHEME: &str = "tlsseam-isolated";
395 const HOST: &str = "isolated.invalid";
396 const PORT: u16 = 64402;
397
398 let global = TlsReloadRegistry::global();
399 global.register(Arc::new(FakeHandler {
400 scheme: SCHEME.into(),
401 host: HOST.into(),
402 port: PORT,
403 }));
404 let fresh = TlsReloadRegistry::default();
405 assert!(
406 fresh.find(SCHEME, HOST, PORT).is_none(),
407 "Default instance must not see global registrations"
408 );
409 global.unregister(SCHEME, HOST, PORT);
411 assert!(global.find(SCHEME, HOST, PORT).is_none());
412 }
413}