1use std::fmt;
2use std::ops::Deref;
3use std::path::PathBuf;
4use thiserror::Error;
5use tokio::task::JoinHandle;
6use tokio_util::sync::CancellationToken;
7
8use crate::spec::{BridgeSpec, CXF_BRIDGE, JMS_BRIDGE, XML_BRIDGE};
9
10#[derive(Clone)]
17pub struct Redacted<T>(T);
18
19impl<T> Redacted<T> {
20 pub fn new(value: T) -> Self {
21 Self(value)
22 }
23
24 pub fn into_inner(self) -> T {
25 self.0
26 }
27}
28
29impl<T> fmt::Debug for Redacted<T> {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 write!(f, "[REDACTED]")
32 }
33}
34
35impl<T> fmt::Display for Redacted<T> {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 write!(f, "[REDACTED]")
38 }
39}
40
41impl<T> Deref for Redacted<T> {
42 type Target = T;
43
44 fn deref(&self) -> &Self::Target {
45 &self.0
46 }
47}
48
49#[derive(Debug, Error)]
50pub enum BridgeError {
51 #[error("IO error: {0}")]
52 Io(#[from] std::io::Error),
53 #[error("Bridge timed out: {0}")]
54 Timeout(String),
55 #[error("Bridge stdout closed before ready message")]
56 StdoutClosed,
57 #[error("Bridge ready message malformed: {0}")]
58 BadReadyMessage(String),
59 #[error("Download failed: {0}")]
60 Download(String),
61 #[error("Checksum mismatch: expected {expected}, got {actual}")]
62 ChecksumMismatch { expected: String, actual: String },
63 #[error("URL not allowed: {0}")]
64 UrlNotAllowed(String),
65 #[error("Transport error: {0}")]
66 Transport(String),
67 #[error("Config error: {0}")]
68 Config(String),
69}
70
71#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
72#[serde(rename_all = "lowercase")]
73pub enum BrokerType {
74 #[serde(alias = "active_mq")]
75 ActiveMq,
76 Artemis,
77 Generic,
78}
79
80impl BrokerType {
81 pub fn as_env_str(&self) -> &'static str {
82 match self {
83 BrokerType::ActiveMq => "activemq",
84 BrokerType::Artemis => "artemis",
85 BrokerType::Generic => "generic",
86 }
87 }
88}
89
90impl std::str::FromStr for BrokerType {
91 type Err = BridgeError;
92
93 fn from_str(s: &str) -> Result<Self, Self::Err> {
94 match s.to_lowercase().as_str() {
95 "activemq" => Ok(BrokerType::ActiveMq),
96 "artemis" => Ok(BrokerType::Artemis),
97 "generic" => Ok(BrokerType::Generic),
98 other => Err(BridgeError::Config(format!("unknown broker type: {other}"))), }
100 }
101}
102
103#[derive(Debug)]
106pub struct CxfProfileEnvVars {
107 pub name: String,
108 pub wsdl_path: String,
109 pub service_name: String,
110 pub port_name: String,
111 pub address: Option<String>,
112 pub keystore_path: Option<String>,
113 pub keystore_password: Option<Redacted<String>>,
114 pub truststore_path: Option<String>,
115 pub truststore_password: Option<Redacted<String>>,
116 pub sig_username: Option<String>,
117 pub sig_password: Option<Redacted<String>>,
118 pub enc_username: Option<String>,
119 pub security_actions_out: Option<String>,
120 pub security_actions_in: Option<String>,
121 pub signature_algorithm: Option<String>,
122 pub signature_digest_algorithm: Option<String>,
123 pub signature_c14n_algorithm: Option<String>,
124 pub signature_parts: Option<String>,
125}
126
127impl CxfProfileEnvVars {
128 pub fn to_env_vars(&self) -> Vec<(String, String)> {
129 let prefix = format!("CXF_PROFILE_{}_", self.name.to_uppercase());
130 let mut vars = vec![
131 (format!("{}WSDL_PATH", prefix), self.wsdl_path.clone()),
132 (format!("{}SERVICE_NAME", prefix), self.service_name.clone()),
133 (format!("{}PORT_NAME", prefix), self.port_name.clone()),
134 ];
135
136 if let Some(ref v) = self.address {
137 vars.push((format!("{}ADDRESS", prefix), v.clone()));
138 }
139 if let Some(ref v) = self.keystore_path {
140 vars.push((format!("{}KEYSTORE_PATH", prefix), v.clone()));
141 }
142 if let Some(ref v) = self.keystore_password {
143 vars.push((format!("{}KEYSTORE_PASSWORD", prefix), (**v).clone()));
144 }
145 if let Some(ref v) = self.truststore_path {
146 vars.push((format!("{}TRUSTSTORE_PATH", prefix), v.clone()));
147 }
148 if let Some(ref v) = self.truststore_password {
149 vars.push((format!("{}TRUSTSTORE_PASSWORD", prefix), (**v).clone()));
150 }
151 if let Some(ref v) = self.sig_username {
152 vars.push((format!("{}SIG_USERNAME", prefix), v.clone()));
153 }
154 if let Some(ref v) = self.sig_password {
155 vars.push((format!("{}SIG_PASSWORD", prefix), (**v).clone()));
156 }
157 if let Some(ref v) = self.enc_username {
158 vars.push((format!("{}ENC_USERNAME", prefix), v.clone()));
159 }
160 if let Some(ref v) = self.security_actions_out {
161 vars.push((format!("{}SECURITY_ACTIONS_OUT", prefix), v.clone()));
162 }
163 if let Some(ref v) = self.security_actions_in {
164 vars.push((format!("{}SECURITY_ACTIONS_IN", prefix), v.clone()));
165 }
166 if let Some(ref v) = self.signature_algorithm {
167 vars.push((format!("{}SIGNATURE_ALGORITHM", prefix), v.clone()));
168 }
169 if let Some(ref v) = self.signature_digest_algorithm {
170 vars.push((format!("{}SIGNATURE_DIGEST_ALGORITHM", prefix), v.clone()));
171 }
172 if let Some(ref v) = self.signature_c14n_algorithm {
173 vars.push((format!("{}SIGNATURE_C14N_ALGORITHM", prefix), v.clone()));
174 }
175 if let Some(ref v) = self.signature_parts {
176 vars.push((format!("{}SIGNATURE_PARTS", prefix), v.clone()));
177 }
178
179 vars
180 }
181}
182
183#[derive(Debug)]
186pub struct BridgeProcessConfig {
187 pub spec: &'static BridgeSpec,
188 pub binary_path: PathBuf,
189 pub broker_url: String,
190 pub broker_type: BrokerType,
191 pub username: Option<String>,
192 pub password: Option<Redacted<String>>,
193 pub start_timeout_ms: u64,
194 pub env_vars: Vec<(String, String)>,
195}
196
197impl BridgeProcessConfig {
198 pub fn jms(
200 binary_path: PathBuf,
201 broker_url: String,
202 broker_type: BrokerType,
203 username: Option<String>,
204 password: Option<Redacted<String>>,
205 start_timeout_ms: u64,
206 ) -> Self {
207 let mut env_vars = vec![
208 ("BRIDGE_BROKER_URL".to_string(), broker_url.clone()),
209 (
210 "BRIDGE_BROKER_TYPE".to_string(),
211 broker_type.as_env_str().to_string(),
212 ),
213 ];
214 if let Some(u) = &username {
215 env_vars.push(("BRIDGE_USERNAME".to_string(), u.clone()));
216 }
217 if let Some(p) = &password {
218 env_vars.push(("BRIDGE_PASSWORD".to_string(), (**p).clone()));
219 }
220 Self {
221 spec: &JMS_BRIDGE,
222 binary_path,
223 broker_url,
224 broker_type,
225 username,
226 password,
227 start_timeout_ms,
228 env_vars,
229 }
230 }
231
232 pub fn xml(binary_path: PathBuf, start_timeout_ms: u64) -> Self {
234 Self {
235 spec: &XML_BRIDGE,
236 binary_path,
237 broker_url: String::new(),
238 broker_type: BrokerType::Generic,
239 username: None,
240 password: None,
241 start_timeout_ms,
242 env_vars: vec![],
243 }
244 }
245
246 pub fn cxf_profiles(
249 binary_path: PathBuf,
250 profiles: &[CxfProfileEnvVars],
251 start_timeout_ms: u64,
252 ) -> Self {
253 let profile_names: Vec<String> = profiles.iter().map(|p| p.name.clone()).collect();
254 let mut env_vars = vec![("CXF_PROFILES".to_string(), profile_names.join(","))];
255
256 for profile in profiles {
257 env_vars.extend(profile.to_env_vars());
258 }
259
260 Self {
261 spec: &CXF_BRIDGE,
262 binary_path,
263 broker_url: String::new(),
264 broker_type: BrokerType::Generic,
265 username: None,
266 password: None,
267 start_timeout_ms,
268 env_vars,
269 }
270 }
271
272 pub fn validate(&self) -> Result<(), String> {
273 if self.start_timeout_ms == 0 {
274 return Err("start_timeout_ms must be > 0".to_string());
275 }
276 Ok(())
277 }
278}
279
280async fn drain_stdout<R: tokio::io::AsyncBufRead + Unpin>(mut reader: R, token: CancellationToken) {
285 use tokio::io::AsyncBufReadExt;
286 use tokio::time::{Duration, Instant};
287
288 const MAX_LINE_BYTES: usize = 64 * 1024;
289 const LOG_INTERVAL: Duration = Duration::from_secs(1);
290 const LOG_BUDGET: u32 = 100;
291
292 let mut line_buf: Vec<u8> = Vec::new();
293 let mut oversized = false;
294 let mut interval_start = Instant::now();
295 let mut logged: u32 = 0;
296 let mut dropped: u32 = 0;
297
298 loop {
299 tokio::select! {
300 biased;
301 _ = token.cancelled() => break,
302 res = reader.fill_buf() => {
303 let chunk_len = match res {
304 Ok(chunk) => {
305 if chunk.is_empty() {
306 break; }
308 for &b in chunk {
309 if b == b'\n' {
310 if oversized {
311 if logged < LOG_BUDGET {
313 tracing::warn!(
314 "bridge stdout: oversized line (>{MAX_LINE_BYTES} bytes), truncated"
315 );
316 logged += 1;
317 } else {
318 dropped += 1;
319 }
320 } else if logged < LOG_BUDGET {
321 tracing::debug!(
323 target: "camel_bridge::child",
324 "{}",
325 String::from_utf8_lossy(&line_buf)
326 );
327 logged += 1;
328 } else {
329 dropped += 1;
330 }
331 line_buf.clear();
332 oversized = false;
333 } else if !oversized {
334 if line_buf.len() < MAX_LINE_BYTES {
335 line_buf.push(b);
336 } else {
337 oversized = true;
338 }
339 }
340 }
341 chunk.len()
342 }
343 Err(_) => break,
344 };
345 reader.consume(chunk_len);
346 if interval_start.elapsed() >= LOG_INTERVAL {
347 if dropped > 0 {
348 tracing::debug!(
350 "bridge stdout: dropped {dropped} lines in last interval"
351 );
352 }
353 interval_start = Instant::now();
354 logged = 0;
355 dropped = 0;
356 }
357 }
358 }
359 }
360}
361
362pub struct BridgeProcess {
363 child: tokio::process::Child,
364 grpc_port: u16,
365 tls: crate::tls::BridgeTlsMaterial,
366 token: CancellationToken,
367 handle: Option<JoinHandle<()>>,
368}
369
370impl BridgeProcess {
371 pub fn grpc_port(&self) -> u16 {
372 self.grpc_port
373 }
374
375 pub async fn connect(&self) -> Result<tonic::transport::Channel, BridgeError> {
377 crate::channel::connect_channel(self.grpc_port, &self.tls).await
378 }
379
380 pub async fn start_and_connect(
382 config: &BridgeProcessConfig,
383 ) -> Result<(Self, tonic::transport::Channel), BridgeError> {
384 let process = Self::start(config).await?;
385 let channel = process.connect().await?;
386 Ok((process, channel))
387 }
388
389 pub async fn start(config: &BridgeProcessConfig) -> Result<Self, BridgeError> {
397 use tokio::io::AsyncBufReadExt;
398 use tokio::process::Command;
399 use tokio::time::{Duration, timeout};
400
401 config.validate().map_err(BridgeError::Config)?;
402
403 let tls = crate::tls::BridgeTlsMaterial::generate()?;
404
405 let free_port = {
407 let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
408 listener.local_addr()?.port()
409 };
410
411 let stderr_stdio: std::process::Stdio =
413 if let Ok(log_dir) = std::env::var("CAMEL_BRIDGE_LOG_STDERR") {
414 let log_filename = config
415 .spec
416 .log_file_template
417 .replace("{pid}", &std::process::id().to_string());
418 let log_path = if log_dir.is_empty() {
419 format!("/tmp/{log_filename}")
420 } else {
421 format!("{log_dir}/{log_filename}")
422 };
423 match std::fs::File::create(&log_path) {
424 Ok(f) => {
425 eprintln!("[camel-bridge] stderr → {}", log_path);
426 f.into()
427 }
428 Err(e) => {
429 eprintln!(
430 "[camel-bridge] failed to create log file {}: {}",
431 log_path, e
432 );
433 std::process::Stdio::inherit()
434 }
435 }
436 } else {
437 std::process::Stdio::inherit()
438 };
439
440 let mut command = Command::new(&config.binary_path);
441 command
442 .env("QUARKUS_HTTP_SSL_PORT", free_port.to_string())
443 .env(
444 "QUARKUS_TLS_BRIDGE_KEY_STORE_PEM_0_CERT",
445 &tls.server_pem_path,
446 )
447 .env(
448 "QUARKUS_TLS_BRIDGE_KEY_STORE_PEM_0_KEY",
449 &tls.server_key_path,
450 )
451 .env("QUARKUS_TLS_BRIDGE_TRUST_STORE_PEM_CERTS", &tls.ca_pem_path)
452 .stdout(std::process::Stdio::piped())
453 .stderr(stderr_stdio);
454
455 for (key, value) in &config.env_vars {
457 command.env(key, value);
458 }
459
460 let mut child = command.spawn()?;
461
462 let stdout = child.stdout.take().ok_or(BridgeError::StdoutClosed)?;
463 let mut reader = tokio::io::BufReader::new(stdout);
464
465 let port = timeout(Duration::from_millis(config.start_timeout_ms), async {
469 let mut buf_acc: Vec<u8> = Vec::new();
470 const READY_MAX_LINE: usize = 64 * 1024;
471 while let Ok(chunk) = reader.fill_buf().await {
472 if chunk.is_empty() {
473 break; }
475 let newline_pos = chunk.iter().position(|&b| b == b'\n');
476 let take = match newline_pos {
477 Some(i) => i + 1,
478 None => chunk.len(),
479 };
480 if buf_acc.len() + take <= READY_MAX_LINE {
481 buf_acc.extend_from_slice(&chunk[..take]);
482 }
483 reader.consume(take);
484 if newline_pos.is_some() {
485 let line = String::from_utf8_lossy(&buf_acc);
486 let line_trimmed = line.trim_end_matches('\n');
487 if let Ok(v) = serde_json::from_str::<serde_json::Value>(line_trimmed)
488 && v.get("status").and_then(|s| s.as_str()) == Some("ready")
489 {
490 if let Some(p) = v.get("port").and_then(|p| p.as_u64()) {
491 return Ok(p as u16);
492 }
493 tracing::error!("bridge ready message malformed: {line_trimmed}");
495 return Err(BridgeError::BadReadyMessage(line_trimmed.to_string()));
496 }
497 buf_acc.clear();
498 }
499 }
500 tracing::error!("bridge stdout closed before ready message");
502 Err(BridgeError::StdoutClosed)
503 })
504 .await
505 .map_err(|_| {
506 let msg = format!(
507 "{} failed to start: health check timeout after {}ms",
508 config.spec.name, config.start_timeout_ms
509 );
510 tracing::error!("{msg}");
512 BridgeError::Timeout(msg)
513 })??;
514
515 let token = CancellationToken::new();
520 let child_token = token.clone();
521 let handle = tokio::spawn(async move {
522 drain_stdout(reader, child_token).await;
523 });
524
525 Ok(BridgeProcess {
526 child,
527 grpc_port: port,
528 tls,
529 token,
530 handle: Some(handle),
531 })
532 }
533
534 pub async fn stop(mut self) -> Result<(), BridgeError> {
536 use tokio::time::{Duration, sleep};
537
538 self.token.cancel();
539
540 if let Some(handle) = self.handle.take() {
541 let join_result = tokio::time::timeout(Duration::from_secs(5), handle).await;
542 if join_result.is_err() {
543 tracing::warn!("bridge stdout drain task did not exit after cancellation");
544 }
545 }
546
547 #[cfg(unix)]
549 {
550 let pid = self.child.id().unwrap_or(0);
551 if pid > 0 {
552 unsafe {
554 libc::kill(pid as i32, libc::SIGTERM);
555 }
556 }
557 }
558
559 #[cfg(not(unix))]
561 let _ = self.child.start_kill();
562
563 tokio::select! {
565 result = self.child.wait() => {
566 result?;
567 }
568 _ = sleep(Duration::from_secs(5)) => {
569 let _ = self.child.start_kill();
570 self.child.wait().await?;
571 }
572 }
573 Ok(())
574 }
575}
576
577impl Drop for BridgeProcess {
578 fn drop(&mut self) {
579 self.token.cancel();
580 let _ = self.child.start_kill();
582 }
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588
589 #[test]
590 fn broker_type_from_str_activemq() {
591 assert_eq!(
592 "activemq".parse::<BrokerType>().unwrap(),
593 BrokerType::ActiveMq
594 );
595 assert_eq!(
596 "ACTIVEMQ".parse::<BrokerType>().unwrap(),
597 BrokerType::ActiveMq
598 );
599 }
600
601 #[test]
602 fn broker_type_from_str_artemis() {
603 assert_eq!(
604 "artemis".parse::<BrokerType>().unwrap(),
605 BrokerType::Artemis
606 );
607 }
608
609 #[test]
610 fn broker_type_from_str_generic() {
611 assert_eq!(
612 "generic".parse::<BrokerType>().unwrap(),
613 BrokerType::Generic
614 );
615 }
616
617 #[test]
618 fn broker_type_from_str_unknown_returns_err() {
619 assert!("ibmmq".parse::<BrokerType>().is_err());
620 assert!("UnknownBroker".parse::<BrokerType>().is_err());
621 }
622
623 #[test]
624 fn broker_type_env_str() {
625 assert_eq!(BrokerType::ActiveMq.as_env_str(), "activemq");
626 assert_eq!(BrokerType::Artemis.as_env_str(), "artemis");
627 assert_eq!(BrokerType::Generic.as_env_str(), "generic");
628 }
629
630 #[test]
631 fn jms_constructor_uses_jms_spec() {
632 let cfg = BridgeProcessConfig::jms(
633 PathBuf::from("/tmp/jms-bridge"),
634 "tcp://localhost:61616".to_string(),
635 BrokerType::ActiveMq,
636 Some("user".to_string()),
637 Some(Redacted::new("pass".to_string())),
638 1000,
639 );
640 assert_eq!(cfg.spec.name, "jms-bridge");
641 }
642
643 #[test]
644 fn xml_constructor_uses_xml_spec() {
645 let cfg = BridgeProcessConfig::xml(PathBuf::from("/tmp/xml-bridge"), 1000);
646 assert_eq!(cfg.spec.name, "xml-bridge");
647 }
648
649 #[test]
650 fn cxf_profiles_generates_cxf_profiles_env_var() {
651 let profiles = vec![
652 CxfProfileEnvVars {
653 name: "baleares".to_string(),
654 wsdl_path: "/a.wsdl".to_string(),
655 service_name: "Svc".to_string(),
656 port_name: "Port".to_string(),
657 address: None,
658 keystore_path: None,
659 keystore_password: None,
660 truststore_path: None,
661 truststore_password: None,
662 sig_username: None,
663 sig_password: None,
664 enc_username: None,
665 security_actions_out: None,
666 security_actions_in: None,
667 signature_algorithm: None,
668 signature_digest_algorithm: None,
669 signature_c14n_algorithm: None,
670 signature_parts: None,
671 },
672 CxfProfileEnvVars {
673 name: "extremadura".to_string(),
674 wsdl_path: "/b.wsdl".to_string(),
675 service_name: "Svc2".to_string(),
676 port_name: "Port2".to_string(),
677 address: Some("http://host:9090/ws".to_string()),
678 keystore_path: Some("/b.jks".to_string()),
679 keystore_password: Some(Redacted::new("pass".to_string())),
680 truststore_path: None,
681 truststore_password: None,
682 sig_username: Some("cert".to_string()),
683 sig_password: Some(Redacted::new("sig_pass".to_string())),
684 enc_username: None,
685 security_actions_out: Some("Timestamp Signature".to_string()),
686 security_actions_in: Some("Timestamp Signature".to_string()),
687 signature_algorithm: None,
688 signature_digest_algorithm: None,
689 signature_c14n_algorithm: None,
690 signature_parts: None,
691 },
692 ];
693
694 let cfg =
695 BridgeProcessConfig::cxf_profiles(PathBuf::from("/tmp/cxf-bridge"), &profiles, 15_000);
696
697 assert_eq!(cfg.spec.name, "cxf-bridge");
698 assert!(cfg.broker_url.is_empty());
699 assert_eq!(cfg.broker_type, BrokerType::Generic);
700 assert!(cfg.username.is_none());
701 assert!(cfg.password.is_none());
702
703 let profiles_var = cfg
705 .env_vars
706 .iter()
707 .find(|(k, _)| k == "CXF_PROFILES")
708 .expect("CXF_PROFILES env var must exist");
709 assert_eq!(profiles_var.1, "baleares,extremadura");
710
711 assert!(
713 cfg.env_vars
714 .iter()
715 .any(|(k, v)| k == "CXF_PROFILE_BALEARES_WSDL_PATH" && v == "/a.wsdl")
716 );
717 assert!(
718 cfg.env_vars
719 .iter()
720 .any(|(k, v)| k == "CXF_PROFILE_BALEARES_SERVICE_NAME" && v == "Svc")
721 );
722 assert!(
723 cfg.env_vars
724 .iter()
725 .any(|(k, v)| k == "CXF_PROFILE_BALEARES_PORT_NAME" && v == "Port")
726 );
727 assert!(
728 !cfg.env_vars
729 .iter()
730 .any(|(k, _)| k == "CXF_PROFILE_BALEARES_ADDRESS")
731 );
732
733 assert!(
735 cfg.env_vars
736 .iter()
737 .any(|(k, v)| k == "CXF_PROFILE_EXTREMADURA_WSDL_PATH" && v == "/b.wsdl")
738 );
739 assert!(
740 cfg.env_vars
741 .iter()
742 .any(|(k, v)| k == "CXF_PROFILE_EXTREMADURA_ADDRESS" && v == "http://host:9090/ws")
743 );
744 assert!(
745 cfg.env_vars
746 .iter()
747 .any(|(k, v)| k == "CXF_PROFILE_EXTREMADURA_KEYSTORE_PATH" && v == "/b.jks")
748 );
749 assert!(
750 cfg.env_vars
751 .iter()
752 .any(|(k, v)| k == "CXF_PROFILE_EXTREMADURA_KEYSTORE_PASSWORD" && v == "pass")
753 );
754 assert!(
755 cfg.env_vars
756 .iter()
757 .any(|(k, v)| k == "CXF_PROFILE_EXTREMADURA_SIG_USERNAME" && v == "cert")
758 );
759 assert!(
760 cfg.env_vars
761 .iter()
762 .any(|(k, v)| k == "CXF_PROFILE_EXTREMADURA_SIG_PASSWORD" && v == "sig_pass")
763 );
764 assert!(
765 cfg.env_vars
766 .iter()
767 .any(|(k, v)| k == "CXF_PROFILE_EXTREMADURA_SECURITY_ACTIONS_OUT"
768 && v == "Timestamp Signature")
769 );
770 }
771
772 #[test]
773 fn cxf_profiles_single_profile_no_security() {
774 let profiles = vec![CxfProfileEnvVars {
775 name: "test".to_string(),
776 wsdl_path: "service.wsdl".to_string(),
777 service_name: "{http://example.com}Service".to_string(),
778 port_name: "{http://example.com}Port".to_string(),
779 address: None,
780 keystore_path: None,
781 keystore_password: None,
782 truststore_path: None,
783 truststore_password: None,
784 sig_username: None,
785 sig_password: None,
786 enc_username: None,
787 security_actions_out: None,
788 security_actions_in: None,
789 signature_algorithm: None,
790 signature_digest_algorithm: None,
791 signature_c14n_algorithm: None,
792 signature_parts: None,
793 }];
794
795 let cfg =
796 BridgeProcessConfig::cxf_profiles(PathBuf::from("/tmp/cxf-bridge"), &profiles, 15_000);
797
798 assert_eq!(cfg.spec.name, "cxf-bridge");
799 assert_eq!(cfg.env_vars.len(), 4);
801 assert_eq!(cfg.env_vars[0].0, "CXF_PROFILES");
802 assert_eq!(cfg.env_vars[0].1, "test");
803 assert_eq!(cfg.env_vars[1].0, "CXF_PROFILE_TEST_WSDL_PATH");
804 assert_eq!(cfg.env_vars[1].1, "service.wsdl");
805 assert_eq!(cfg.env_vars[2].0, "CXF_PROFILE_TEST_SERVICE_NAME");
806 assert_eq!(cfg.env_vars[2].1, "{http://example.com}Service");
807 assert_eq!(cfg.env_vars[3].0, "CXF_PROFILE_TEST_PORT_NAME");
808 assert_eq!(cfg.env_vars[3].1, "{http://example.com}Port");
809 }
810
811 #[test]
812 fn profile_env_vars_to_env_vars_includes_all_fields() {
813 let vars = CxfProfileEnvVars {
814 name: "full".to_string(),
815 wsdl_path: "/wsdl".to_string(),
816 service_name: "Svc".to_string(),
817 port_name: "Port".to_string(),
818 address: Some("http://host:8080".to_string()),
819 keystore_path: Some("/ks.jks".to_string()),
820 keystore_password: Some(Redacted::new("ks_pass".to_string())),
821 truststore_path: Some("/ts.jks".to_string()),
822 truststore_password: Some(Redacted::new("ts_pass".to_string())),
823 sig_username: Some("user".to_string()),
824 sig_password: Some(Redacted::new("sig_pass".to_string())),
825 enc_username: None,
826 security_actions_out: Some("Timestamp Signature".to_string()),
827 security_actions_in: Some("Timestamp".to_string()),
828 signature_algorithm: None,
829 signature_digest_algorithm: None,
830 signature_c14n_algorithm: None,
831 signature_parts: None,
832 };
833
834 let env = vars.to_env_vars();
835 assert_eq!(env.len(), 12);
837
838 let keys: Vec<&str> = env.iter().map(|(k, _)| k.as_str()).collect();
839 assert!(keys.contains(&"CXF_PROFILE_FULL_WSDL_PATH"));
840 assert!(keys.contains(&"CXF_PROFILE_FULL_SERVICE_NAME"));
841 assert!(keys.contains(&"CXF_PROFILE_FULL_PORT_NAME"));
842 assert!(keys.contains(&"CXF_PROFILE_FULL_ADDRESS"));
843 assert!(keys.contains(&"CXF_PROFILE_FULL_KEYSTORE_PATH"));
844 assert!(keys.contains(&"CXF_PROFILE_FULL_KEYSTORE_PASSWORD"));
845 assert!(keys.contains(&"CXF_PROFILE_FULL_TRUSTSTORE_PATH"));
846 assert!(keys.contains(&"CXF_PROFILE_FULL_TRUSTSTORE_PASSWORD"));
847 assert!(keys.contains(&"CXF_PROFILE_FULL_SIG_USERNAME"));
848 assert!(keys.contains(&"CXF_PROFILE_FULL_SIG_PASSWORD"));
849 assert!(keys.contains(&"CXF_PROFILE_FULL_SECURITY_ACTIONS_OUT"));
850 assert!(keys.contains(&"CXF_PROFILE_FULL_SECURITY_ACTIONS_IN"));
851 }
852
853 #[test]
854 fn test_start_timeout_zero_rejected() {
855 let config = BridgeProcessConfig::jms(
856 PathBuf::from("/usr/bin/echo"),
857 "tcp://localhost:61616".to_string(),
858 BrokerType::ActiveMq,
859 None,
860 None,
861 0,
862 );
863 let result = config.validate();
864 assert!(result.is_err());
865 }
866
867 #[test]
868 fn test_bridge_rejects_zero_start_timeout() {
869 let config = BridgeProcessConfig::jms(
870 PathBuf::from("/usr/bin/echo"),
871 "tcp://localhost:61616".to_string(),
872 BrokerType::ActiveMq,
873 None,
874 None,
875 0,
876 );
877 assert!(config.validate().is_err());
878 }
879
880 #[tokio::test]
881 async fn test_bridge_stop_completes() {
882 use tokio::process::Command;
883 use tokio::time::{Duration, timeout};
884
885 let child = Command::new("sh")
886 .arg("-c")
887 .arg("trap '' TERM; while true; do echo tick; sleep 1; done")
888 .stdout(std::process::Stdio::null())
889 .spawn()
890 .expect("must spawn test child process");
891
892 let bridge = BridgeProcess {
893 child,
894 grpc_port: 0,
895 tls: crate::tls::BridgeTlsMaterial::generate().expect("test tls"),
896 token: CancellationToken::new(),
897 handle: None,
898 };
899
900 let result = timeout(Duration::from_secs(10), bridge.stop()).await;
901 assert!(result.is_ok(), "stop() must complete within 10s");
902 }
903
904 #[test]
907 fn redacted_debug_displays_redacted() {
908 let r = Redacted::new("secret_password".to_string());
909 assert_eq!(format!("{r:?}"), "[REDACTED]");
910 }
911
912 #[test]
913 fn redacted_display_displays_redacted() {
914 let r = Redacted::new("secret_password".to_string());
915 assert_eq!(format!("{r}"), "[REDACTED]");
916 }
917
918 #[test]
919 fn redacted_deref_gives_inner_value() {
920 let r = Redacted::new("secret".to_string());
921 assert_eq!(&*r, "secret");
922 }
923
924 #[test]
925 fn redacted_into_inner_returns_value() {
926 let r = Redacted::new("secret".to_string());
927 assert_eq!(r.into_inner(), "secret");
928 }
929
930 #[test]
931 fn redacted_clone_works() {
932 let r = Redacted::new("secret".to_string());
933 let c = r.clone();
934 assert_eq!(&*c, "secret");
935 assert_eq!(format!("{c:?}"), "[REDACTED]");
936 }
937
938 #[test]
939 fn bridge_process_config_debug_redacts_password() {
940 let cfg = BridgeProcessConfig::jms(
941 PathBuf::from("/tmp/jms-bridge"),
942 "tcp://localhost:61616".to_string(),
943 BrokerType::ActiveMq,
944 Some("user".to_string()),
945 Some(Redacted::new("super_secret".to_string())),
946 1000,
947 );
948 let password_debug = format!("{:?}", cfg.password); assert!(
953 !password_debug.contains("super_secret"),
954 "Password field must not leak in Debug: {password_debug}"
955 );
956 assert_eq!(
957 password_debug, "Some([REDACTED])",
958 "Password field must show [REDACTED]: {password_debug}"
959 );
960 }
961
962 #[test]
963 fn cxf_profile_debug_redacts_passwords() {
964 let profile = CxfProfileEnvVars {
965 name: "test".to_string(),
966 wsdl_path: "/a.wsdl".to_string(),
967 service_name: "Svc".to_string(),
968 port_name: "Port".to_string(),
969 address: None,
970 keystore_path: None,
971 keystore_password: Some(Redacted::new("ks_secret_val".to_string())),
972 truststore_path: None,
973 truststore_password: Some(Redacted::new("ts_secret_val".to_string())),
974 sig_username: None,
975 sig_password: Some(Redacted::new("sig_secret_val".to_string())),
976 enc_username: None,
977 security_actions_out: None,
978 security_actions_in: None,
979 signature_algorithm: None,
980 signature_digest_algorithm: None,
981 signature_c14n_algorithm: None,
982 signature_parts: None,
983 };
984 let debug_output = format!("{profile:?}");
985 assert!(
986 !debug_output.contains("ks_secret_val")
987 && !debug_output.contains("ts_secret_val")
988 && !debug_output.contains("sig_secret_val"),
989 "Debug must not contain passwords: {debug_output}"
990 );
991 }
992
993 async fn spawn_child_with_stdout(script: &str) -> tokio::process::Child {
997 use tokio::process::Command;
998
999 Command::new("sh")
1000 .arg("-c")
1001 .arg(script)
1002 .stdout(std::process::Stdio::piped())
1003 .spawn()
1004 .expect("must spawn test child")
1005 }
1006
1007 #[tokio::test]
1008 async fn drain_chatty_child_no_deadlock() {
1009 use tokio::io::AsyncBufReadExt;
1010
1011 let mut child =
1013 spawn_child_with_stdout("for i in $(seq 1 500); do echo \"line $i\"; done").await;
1014
1015 let stdout = child.stdout.take().expect("stdout piped");
1016 let reader = tokio::io::BufReader::new(stdout);
1017 let token = CancellationToken::new();
1018
1019 let handle = tokio::spawn(drain_stdout(reader, token.clone()));
1021
1022 let status = child.wait().await.expect("wait for child");
1024 assert!(status.success(), "child should exit successfully");
1025
1026 let result = tokio::time::timeout(std::time::Duration::from_secs(2), handle).await;
1028 assert!(result.is_ok(), "drain task should complete within 2s");
1029 }
1030
1031 #[tokio::test]
1032 async fn drain_oversized_line_bounded() {
1033 use tokio::io::AsyncBufReadExt;
1034
1035 let oversized_bytes = 200 * 1024; let mut child = spawn_child_with_stdout(&format!(
1045 "head -c {oversized_bytes} /dev/zero | tr '\\0' 'A'; echo"
1046 ))
1047 .await;
1048
1049 let stdout = child.stdout.take().expect("stdout piped");
1050 let reader = tokio::io::BufReader::new(stdout);
1051 let token = CancellationToken::new();
1052
1053 let handle = tokio::spawn(drain_stdout(reader, token.clone()));
1054
1055 let status = child.wait().await.expect("wait for child");
1056 assert!(status.success(), "child should exit successfully");
1057
1058 let result = tokio::time::timeout(std::time::Duration::from_millis(500), handle).await;
1062 assert!(result.is_ok(), "drain task should complete within 500ms");
1063 }
1064
1065 #[tokio::test]
1066 async fn drain_cancellation_exits_promptly() {
1067 use tokio::io::AsyncBufReadExt;
1068
1069 let mut child = spawn_child_with_stdout("while true; do echo tick; sleep 1; done").await;
1071
1072 let stdout = child.stdout.take().expect("stdout piped");
1073 let reader = tokio::io::BufReader::new(stdout);
1074 let token = CancellationToken::new();
1075
1076 let handle = tokio::spawn(drain_stdout(reader, token.clone()));
1078
1079 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1081
1082 token.cancel();
1084 let result = tokio::time::timeout(std::time::Duration::from_millis(500), handle).await;
1085 assert!(
1086 result.is_ok(),
1087 "drain task should exit promptly after cancellation"
1088 );
1089
1090 let _ = child.kill().await;
1092 }
1093
1094 #[tokio::test]
1102 async fn drain_handoff_preserves_trailing_bytes() {
1103 use tokio::io::AsyncBufReadExt;
1104
1105 let mut child = spawn_child_with_stdout(
1107 r#"echo '{"status":"ready","port":12345}'; echo "trailing-1"; echo "trailing-2"; echo "trailing-3""#,
1108 )
1109 .await;
1110
1111 let stdout = child.stdout.take().expect("stdout piped");
1112 let mut reader = tokio::io::BufReader::new(stdout);
1113
1114 let mut buf_acc: Vec<u8> = Vec::new();
1116 const READY_MAX_LINE: usize = 64 * 1024;
1117 let mut port_found = None;
1118 while let Ok(chunk) = reader.fill_buf().await {
1119 if chunk.is_empty() {
1120 break; }
1122 let newline_pos = chunk.iter().position(|&b| b == b'\n');
1123 let take = match newline_pos {
1124 Some(i) => i + 1,
1125 None => chunk.len(),
1126 };
1127 if buf_acc.len() + take <= READY_MAX_LINE {
1128 buf_acc.extend_from_slice(&chunk[..take]);
1129 }
1130 reader.consume(take);
1131 if newline_pos.is_some() {
1132 let line = String::from_utf8_lossy(&buf_acc);
1133 let line_trimmed = line.trim_end_matches('\n');
1134 if let Ok(v) = serde_json::from_str::<serde_json::Value>(line_trimmed)
1135 && v.get("status").and_then(|s| s.as_str()) == Some("ready")
1136 {
1137 if let Some(p) = v.get("port").and_then(|p| p.as_u64()) {
1138 port_found = Some(p as u16);
1139 break;
1140 }
1141 }
1142 buf_acc.clear();
1143 }
1144 }
1145 assert_eq!(port_found, Some(12345), "ready message must be parsed");
1146
1147 let token = CancellationToken::new();
1149 let handle = tokio::spawn(drain_stdout(reader, token.clone()));
1150
1151 let status = child.wait().await.expect("wait for child");
1153 assert!(status.success(), "child should exit successfully");
1154
1155 let result = tokio::time::timeout(std::time::Duration::from_secs(2), handle).await;
1157 assert!(
1158 result.is_ok(),
1159 "drain task should complete — trailing bytes preserved through handoff"
1160 );
1161 }
1162}