Skip to main content

camel_component_api/
tls_source.rs

1//! Shared [`ServerTlsSource`] — builds a validated [`rustls::ServerConfig`] from
2//! PEM cert/key files, optionally with mTLS client CA verification.
3//!
4//! This deduplicates ~40 lines of PEM parsing that each of gRPC, HTTP, and WS
5//! components implement separately. The individual components still own ALPN
6//! configuration — `build_server_config` intentionally leaves ALPN empty so the
7//! caller can set it after construction.
8
9use std::path::PathBuf;
10use std::sync::{Arc, Mutex};
11
12use async_trait::async_trait;
13use camel_api::CamelError;
14use rustls::ServerConfig;
15
16/// File-based TLS source for rustls server-side configuration.
17///
18/// Reads PEM-encoded cert+key from the filesystem. When `client_ca_path` is
19/// set, the server requires (and verifies) a client certificate (mTLS).
20#[derive(Debug, Clone)]
21pub struct ServerTlsSource {
22    /// Path to the PEM-encoded server certificate chain.
23    pub cert_path: PathBuf,
24    /// Path to the PEM-encoded private key for the server certificate.
25    pub key_path: PathBuf,
26    /// Optional path to a PEM-encoded CA certificate for client verification.
27    /// When set, mTLS is enabled (clients must present a cert signed by this CA).
28    pub client_ca_path: Option<PathBuf>,
29}
30
31impl ServerTlsSource {
32    /// Build a [`rustls::ServerConfig`] from the PEM files.
33    ///
34    /// Returns `EndpointCreationFailed` on any I/O, PEM parse, or rustls
35    /// configuration error.
36    ///
37    /// ALPN protocols are intentionally NOT set — callers add their own
38    /// (e.g., `b"h2"` for gRPC) after construction.
39    pub fn build_server_config(&self) -> Result<ServerConfig, CamelError> {
40        // Read cert and key files as raw bytes.
41        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        // Parse PEM certs.
55        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        // Parse PEM private key.
62        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        // Explicit ring provider — rustls 0.23 requires this (the default
69        // `ServerConfig::builder()` panics at runtime without a process-default).
70        let provider = Arc::new(rustls::crypto::ring::default_provider());
71
72        // Build the protocol-version-safe builder.
73        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        // Branch: mTLS (client cert verification) or plain server auth.
80        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// ---------------------------------------------------------------------------
127// TlsReloadHandler + TlsReloadRegistry
128// ---------------------------------------------------------------------------
129
130/// Implemented by each TLS-terminating component.
131/// Registered with [`TlsReloadRegistry::global()`] when a server first spawns.
132#[async_trait]
133pub trait TlsReloadHandler: Send + Sync {
134    /// Returns true if this handler owns the (scheme, host, port).
135    fn matches(&self, scheme: &str, host: &str, port: u16) -> bool;
136    /// Re-read cert files and swap. Returns Err if cert invalid; old cert stays.
137    async fn reload(&self) -> Result<(), CamelError>;
138}
139
140/// Process-global singleton registry of TLS reload handlers.
141///
142/// Provides a singleton via [`TlsReloadRegistry::global()`]. Each TLS-terminating
143/// component registers its handler when it first spawns, and unregisters on
144/// release/eviction.
145#[derive(Default)]
146pub struct TlsReloadRegistry {
147    handlers: Mutex<Vec<Arc<dyn TlsReloadHandler>>>,
148}
149
150impl TlsReloadRegistry {
151    /// Returns a reference to the process-global singleton.
152    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    /// Register a handler so it can be found later via [`find`](Self::find).
158    pub fn register(&self, handler: Arc<dyn TlsReloadHandler>) {
159        let mut guard = self
160            .handlers
161            .lock()
162            .expect("TlsReloadRegistry lock poisoned"); // allow-unwrap
163        guard.push(handler);
164    }
165
166    /// Find the handler that matches (scheme, host, port), if any.
167    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"); // allow-unwrap
172        guard
173            .iter()
174            .find(|h| h.matches(scheme, host, port))
175            .cloned()
176    }
177
178    /// Remove handlers matching (scheme, host, port). Called on server release/eviction.
179    pub fn unregister(&self, scheme: &str, host: &str, port: u16) {
180        let mut guard = self
181            .handlers
182            .lock()
183            .expect("TlsReloadRegistry lock poisoned"); // allow-unwrap
184        guard.retain(|h| !h.matches(scheme, host, port));
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    /// Helper: write a PEM string to a temp file and return the path.
193    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        // mTLS is enabled — verifier should require client cert.
230        // We can't easily assert the verifier type, but we can verify
231        // the config uses a client-cert verifier by checking it was
232        // built without errors.
233    }
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        // Write garbage as the CA PEM.
242        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}