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)]
146pub struct TlsReloadRegistry {
147 handlers: Mutex<Vec<Arc<dyn TlsReloadHandler>>>,
148}
149
150impl TlsReloadRegistry {
151 pub fn global() -> &'static TlsReloadRegistry {
153 static INSTANCE: std::sync::OnceLock<TlsReloadRegistry> = std::sync::OnceLock::new();
154 INSTANCE.get_or_init(TlsReloadRegistry::default)
155 }
156
157 pub fn register(&self, handler: Arc<dyn TlsReloadHandler>) {
159 let mut guard = self
160 .handlers
161 .lock()
162 .expect("TlsReloadRegistry lock poisoned"); guard.push(handler);
164 }
165
166 pub fn find(&self, scheme: &str, host: &str, port: u16) -> Option<Arc<dyn TlsReloadHandler>> {
168 let guard = self
169 .handlers
170 .lock()
171 .expect("TlsReloadRegistry lock poisoned"); guard
173 .iter()
174 .find(|h| h.matches(scheme, host, port))
175 .cloned()
176 }
177
178 pub fn unregister(&self, scheme: &str, host: &str, port: u16) {
180 let mut guard = self
181 .handlers
182 .lock()
183 .expect("TlsReloadRegistry lock poisoned"); guard.retain(|h| !h.matches(scheme, host, port));
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 fn write_pem(pem: &str, name: &str) -> PathBuf {
194 crate::test_support::tls::write_pem_tmp(name, pem)
195 }
196
197 #[test]
198 fn build_server_config_valid_cert() {
199 let (ca_pem, cert_pem, key_pem) = crate::test_support::tls::gen_server_cert();
200 let _ca = write_pem(&ca_pem, "ca.pem");
201 let cert = write_pem(&cert_pem, "cert.pem");
202 let key = write_pem(&key_pem, "key.pem");
203
204 let source = ServerTlsSource {
205 cert_path: cert,
206 key_path: key,
207 client_ca_path: None,
208 };
209
210 let config = source.build_server_config().expect("valid cert+key");
211 assert!(config.alpn_protocols.is_empty(), "ALPN should be empty");
212 }
213
214 #[test]
215 fn build_server_config_mtls() {
216 let (ca_pem, cert_pem, key_pem) = crate::test_support::tls::gen_server_cert();
217 let ca = write_pem(&ca_pem, "mtls-ca.pem");
218 let cert = write_pem(&cert_pem, "cert.pem");
219 let key = write_pem(&key_pem, "key.pem");
220
221 let source = ServerTlsSource {
222 cert_path: cert,
223 key_path: key,
224 client_ca_path: Some(ca),
225 };
226
227 let config = source.build_server_config().expect("mTLS should work");
228 assert!(config.alpn_protocols.is_empty());
229 }
234
235 #[test]
236 fn build_server_config_mtls_bad_ca_rejected() {
237 let (_, cert_pem, key_pem) = crate::test_support::tls::gen_server_cert();
238 let cert = write_pem(&cert_pem, "cert.pem");
239 let key = write_pem(&key_pem, "key.pem");
240
241 let bad_ca = write_pem("not-a-valid-ca-certificate\n", "bad-ca.pem");
243
244 let source = ServerTlsSource {
245 cert_path: cert,
246 key_path: key,
247 client_ca_path: Some(bad_ca),
248 };
249
250 let err = source.build_server_config().unwrap_err();
251 assert!(
252 matches!(&err, CamelError::EndpointCreationFailed(_)),
253 "expected EndpointCreationFailed, got {err:?}"
254 );
255 }
256
257 #[test]
258 fn build_server_config_missing_file() {
259 let source = ServerTlsSource {
260 cert_path: PathBuf::from("/nonexistent/cert.pem"),
261 key_path: PathBuf::from("/nonexistent/key.pem"),
262 client_ca_path: None,
263 };
264
265 let err = source.build_server_config().unwrap_err();
266 assert!(
267 matches!(&err, CamelError::EndpointCreationFailed(_)),
268 "expected EndpointCreationFailed, got {err:?}"
269 );
270 }
271
272 #[test]
273 fn build_server_config_malformed_pem() {
274 let cert = write_pem("garbage-content\n", "bad-cert.pem");
275 let key = write_pem("also-garbage\n", "bad-key.pem");
276
277 let source = ServerTlsSource {
278 cert_path: cert,
279 key_path: key,
280 client_ca_path: None,
281 };
282
283 let err = source.build_server_config().unwrap_err();
284 assert!(
285 matches!(&err, CamelError::EndpointCreationFailed(_)),
286 "expected EndpointCreationFailed, got {err:?}"
287 );
288 }
289}
290
291#[cfg(test)]
292mod registry_tests {
293 use super::*;
294
295 struct FakeHandler {
296 scheme: String,
297 host: String,
298 port: u16,
299 }
300
301 #[async_trait::async_trait]
302 impl TlsReloadHandler for FakeHandler {
303 fn matches(&self, scheme: &str, host: &str, port: u16) -> bool {
304 self.scheme == scheme && self.host == host && self.port == port
305 }
306 async fn reload(&self) -> Result<(), CamelError> {
307 Ok(())
308 }
309 }
310
311 #[test]
312 fn registry_find_returns_matching_handler() {
313 let reg = TlsReloadRegistry::default();
314 reg.register(Arc::new(FakeHandler {
315 scheme: "grpcs".into(),
316 host: "0.0.0.0".into(),
317 port: 9090,
318 }));
319 assert!(reg.find("grpcs", "0.0.0.0", 9090).is_some());
320 assert!(reg.find("https", "0.0.0.0", 9090).is_none());
321 }
322
323 #[test]
324 fn registry_find_returns_none_when_empty() {
325 let reg = TlsReloadRegistry::default();
326 assert!(reg.find("grpcs", "0.0.0.0", 9090).is_none());
327 }
328
329 #[test]
330 fn registry_unregister_removes_handler() {
331 let reg = TlsReloadRegistry::default();
332 reg.register(Arc::new(FakeHandler {
333 scheme: "grpcs".into(),
334 host: "0.0.0.0".into(),
335 port: 9090,
336 }));
337 assert!(reg.find("grpcs", "0.0.0.0", 9090).is_some());
338 reg.unregister("grpcs", "0.0.0.0", 9090);
339 assert!(reg.find("grpcs", "0.0.0.0", 9090).is_none());
340 }
341}