1use duration_string::DurationString;
15use std::{collections::HashMap, time::Duration};
16
17use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19
20use crate::component::configuration::Configuration;
21use crate::conn_type::ConnType;
22use crate::errors::ConfigError;
23#[cfg(not(target_arch = "wasm32"))]
24use crate::grpc::compression::CompressionType;
25#[cfg(not(target_arch = "wasm32"))]
26use crate::grpc::proxy::ProxyConfig;
27#[cfg(not(target_arch = "wasm32"))]
28use crate::tls::client::TlsClientConfig as TLSSetting;
29#[cfg(not(target_arch = "wasm32"))]
30use crate::tls::errors::ConfigError as TlsConfigError;
31use crate::transport::{TransportProtocol, validate_endpoint_scheme};
32use crate::websocket::client::WebSocketClientChannel;
33
34cfg_if::cfg_if! {
38 if #[cfg(not(target_arch = "wasm32"))] {
39 use display_error_chain::ErrorChainExt;
40 use slim_auth::metadata::MetadataMap;
41 use tonic::codegen::{Body, Bytes, StdError};
42
43 use crate::auth::basic::Config as BasicAuthenticationConfig;
44 use crate::auth::jwt::Config as JwtAuthenticationConfig;
45 use crate::auth::oidc::Config as OidcAuthConfig;
46 #[cfg(not(target_family = "windows"))]
47 use crate::auth::spire::SpireConfig as SpireAuthConfig;
48 use crate::auth::static_jwt::Config as BearerAuthenticationConfig;
49 use crate::backoff::exponential::Config as ExponentialBackoff;
50 use crate::backoff::fixedinterval::Config as FixedIntervalBackoff;
51 use crate::backoff::Strategy;
52 }
53}
54
55pub enum TransportChannel<G> {
62 Grpc(G),
63 Websocket(WebSocketClientChannel),
64}
65
66#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, JsonSchema)]
71pub struct KeepaliveConfig {
72 #[serde(default = "default_tcp_keepalive")]
74 #[schemars(with = "String")]
75 pub tcp_keepalive: DurationString,
76
77 #[serde(default = "default_http2_keepalive")]
79 #[schemars(with = "String")]
80 pub http2_keepalive: DurationString,
81
82 #[serde(default = "default_timeout")]
84 #[schemars(with = "String")]
85 pub timeout: DurationString,
86
87 #[serde(default = "default_keep_alive_while_idle")]
89 pub keep_alive_while_idle: bool,
90}
91
92impl Default for KeepaliveConfig {
94 fn default() -> Self {
95 KeepaliveConfig {
96 tcp_keepalive: default_tcp_keepalive(),
97 http2_keepalive: default_http2_keepalive(),
98 timeout: default_timeout(),
99 keep_alive_while_idle: default_keep_alive_while_idle(),
100 }
101 }
102}
103
104fn default_tcp_keepalive() -> DurationString {
105 Duration::from_secs(60).into()
106}
107
108fn default_http2_keepalive() -> DurationString {
109 Duration::from_secs(60).into()
110}
111
112fn default_timeout() -> DurationString {
113 Duration::from_secs(10).into()
114}
115
116fn default_keep_alive_while_idle() -> bool {
117 false
118}
119
120cfg_if::cfg_if! {
121 if #[cfg(not(target_arch = "wasm32"))] {
122 #[derive(Debug, Serialize, Default, Deserialize, Clone, PartialEq, JsonSchema)]
124 #[serde(rename_all = "snake_case", tag = "type")]
125 pub enum AuthenticationConfig {
126 Basic(BasicAuthenticationConfig),
128 StaticJwt(BearerAuthenticationConfig),
130 Jwt(JwtAuthenticationConfig),
132 Oidc(OidcAuthConfig),
134 #[cfg(not(target_family = "windows"))]
136 Spire(SpireAuthConfig),
137 #[default]
139 None,
140 }
141
142 #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, JsonSchema)]
144 #[serde(rename_all = "snake_case", tag = "type")]
145 pub enum BackoffConfig {
146 Exponential(ExponentialBackoff),
148 FixedInterval(FixedIntervalBackoff),
150 }
151
152 impl BackoffConfig {
153 pub fn new_exponential(
155 base: u64,
156 factor: u64,
157 max_delay: Duration,
158 max_attempts: usize,
159 jitter: bool,
160 ) -> Self {
161 BackoffConfig::Exponential(ExponentialBackoff::new(
162 base,
163 factor,
164 max_delay,
165 max_attempts,
166 jitter,
167 ))
168 }
169
170 pub fn new_fixed_interval(interval: Duration, max_attempts: usize) -> Self {
172 BackoffConfig::FixedInterval(FixedIntervalBackoff::new(interval, max_attempts))
173 }
174 }
175
176 impl Default for BackoffConfig {
177 fn default() -> Self {
178 BackoffConfig::Exponential(ExponentialBackoff::default())
179 }
180 }
181
182 impl Strategy for BackoffConfig {
183 fn get_strategy(&self) -> Box<dyn Iterator<Item = Duration> + Send> {
184 match self {
185 BackoffConfig::Exponential(b) => b.get_strategy(),
186 BackoffConfig::FixedInterval(b) => b.get_strategy(),
187 }
188 }
189 }
190 }
191}
192
193#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, JsonSchema)]
200pub struct ClientConfig {
201 pub endpoint: String,
207
208 pub origin: Option<String>,
210
211 pub server_name: Option<String>,
214
215 #[cfg(not(target_arch = "wasm32"))]
217 pub compression: Option<CompressionType>,
218
219 pub rate_limit: Option<String>,
221
222 #[cfg(not(target_arch = "wasm32"))]
224 #[serde(default, rename = "tls")]
225 pub tls_setting: TLSSetting,
226
227 pub keepalive: Option<KeepaliveConfig>,
229
230 #[cfg(not(target_arch = "wasm32"))]
232 #[serde(default)]
233 pub proxy: ProxyConfig,
234
235 #[serde(default = "default_connect_timeout")]
237 #[schemars(with = "String")]
238 pub connect_timeout: DurationString,
239
240 #[serde(default = "default_request_timeout")]
242 #[schemars(with = "String")]
243 pub request_timeout: DurationString,
244
245 pub buffer_size: Option<usize>,
247
248 #[serde(default)]
250 pub headers: HashMap<String, String>,
251
252 #[cfg(not(target_arch = "wasm32"))]
254 #[serde(default)]
255 pub auth: AuthenticationConfig,
256
257 #[cfg(not(target_arch = "wasm32"))]
259 #[serde(default = "default_backoff")]
260 pub backoff: BackoffConfig,
261
262 #[cfg(not(target_arch = "wasm32"))]
264 pub metadata: Option<MetadataMap>,
265
266 #[serde(default = "default_link_id")]
269 pub link_id: String,
270
271 #[serde(default = "default_require_header_mac")]
273 pub require_header_mac: bool,
274
275 #[serde(default)]
279 pub connection_type: ConnType,
280}
281
282impl Default for ClientConfig {
284 fn default() -> Self {
285 ClientConfig {
286 endpoint: String::new(),
287 origin: None,
288 server_name: None,
289 #[cfg(not(target_arch = "wasm32"))]
290 compression: None,
291 rate_limit: None,
292 #[cfg(not(target_arch = "wasm32"))]
293 tls_setting: TLSSetting::default(),
294 keepalive: None,
295 #[cfg(not(target_arch = "wasm32"))]
296 proxy: ProxyConfig::default(),
297 connect_timeout: default_connect_timeout(),
298 request_timeout: default_request_timeout(),
299 buffer_size: None,
300 headers: HashMap::new(),
301 #[cfg(not(target_arch = "wasm32"))]
302 auth: AuthenticationConfig::None,
303 #[cfg(not(target_arch = "wasm32"))]
304 backoff: default_backoff(),
305 #[cfg(not(target_arch = "wasm32"))]
306 metadata: None,
307 link_id: default_link_id(),
308 require_header_mac: true,
309 connection_type: ConnType::default(),
310 }
311 }
312}
313
314fn default_link_id() -> String {
315 uuid::Uuid::new_v4().to_string()
316}
317
318fn default_require_header_mac() -> bool {
319 true
320}
321
322#[cfg(not(target_arch = "wasm32"))]
323fn default_backoff() -> BackoffConfig {
324 BackoffConfig::new_fixed_interval(Duration::from_secs(2), usize::MAX)
325}
326
327fn default_connect_timeout() -> DurationString {
328 Duration::from_secs(0).into()
329}
330
331fn default_request_timeout() -> DurationString {
332 Duration::from_secs(0).into()
333}
334
335#[cfg(not(target_arch = "wasm32"))]
340#[path = "client_display_native.rs"]
341mod client_display;
342#[cfg(target_arch = "wasm32")]
343#[path = "client_display_wasm.rs"]
344mod client_display;
345
346impl Configuration for ClientConfig {
347 type Error = ConfigError;
348
349 fn validate(&self) -> Result<(), Self::Error> {
350 if self.endpoint.is_empty() {
351 return Err(ConfigError::MissingEndpoint);
352 }
353
354 #[cfg(not(target_arch = "wasm32"))]
356 self.tls_setting.validate()?;
357 validate_endpoint_scheme(&self.endpoint)?;
358
359 Ok(())
360 }
361}
362
363impl ClientConfig {
368 pub fn with_endpoint(endpoint: &str) -> Self {
372 Self {
373 endpoint: endpoint.to_string(),
374 ..Self::default()
375 }
376 }
377
378 pub fn with_origin(self, origin: &str) -> Self {
379 Self {
380 origin: Some(origin.to_string()),
381 ..self
382 }
383 }
384
385 pub fn with_server_name(self, server_name: &str) -> Self {
386 Self {
387 server_name: Some(server_name.to_string()),
388 ..self
389 }
390 }
391
392 pub fn with_rate_limit(self, rate_limit: &str) -> Self {
393 Self {
394 rate_limit: Some(rate_limit.to_string()),
395 ..self
396 }
397 }
398
399 pub fn with_keepalive(self, keepalive: KeepaliveConfig) -> Self {
400 Self {
401 keepalive: Some(keepalive),
402 ..self
403 }
404 }
405
406 pub fn with_connect_timeout(self, connect_timeout: Duration) -> Self {
407 Self {
408 connect_timeout: connect_timeout.into(),
409 ..self
410 }
411 }
412
413 pub fn with_request_timeout(self, request_timeout: Duration) -> Self {
414 Self {
415 request_timeout: request_timeout.into(),
416 ..self
417 }
418 }
419
420 pub fn with_buffer_size(self, buffer_size: usize) -> Self {
421 Self {
422 buffer_size: Some(buffer_size),
423 ..self
424 }
425 }
426
427 pub fn with_headers(self, headers: HashMap<String, String>) -> Self {
428 Self { headers, ..self }
429 }
430
431 pub fn with_connection_type(self, connection_type: ConnType) -> Self {
432 Self {
433 connection_type,
434 ..self
435 }
436 }
437
438 pub fn resolved_transport(&self) -> TransportProtocol {
441 TransportProtocol::from_endpoint(&self.endpoint)
442 }
443}
444
445#[cfg(not(target_arch = "wasm32"))]
450impl ClientConfig {
451 pub fn merge_server_requirements(
452 &mut self,
453 server: &ServerConnectionConfig,
454 ) -> Result<(), TlsConfigError> {
455 self.endpoint = server.endpoint.clone();
456 self.tls_setting.insecure = !server.tls_required;
457
458 match &server.auth_method {
459 RequiredAuthMethod::None => {}
460 #[cfg(not(target_family = "windows"))]
461 RequiredAuthMethod::Spire { trust_domain } => {
462 use crate::auth::spire::SpireConfig;
463 use crate::tls::common::{CaSource, TlsSource};
464
465 let trust_domains = trust_domain
466 .as_ref()
467 .map(|td| vec![td.clone()])
468 .unwrap_or_default();
469
470 if server.tls_required {
471 let socket_path = match &self.auth {
473 AuthenticationConfig::Spire(auth_cfg) => auth_cfg.socket_path.clone(),
474 _ => None,
475 };
476 self.tls_setting.config.source = TlsSource::Spire {
477 config: SpireConfig {
478 socket_path: socket_path.clone(),
479 ..Default::default()
480 },
481 };
482 self.tls_setting.config.ca_source = CaSource::Spire {
483 config: SpireConfig {
484 socket_path,
485 trust_domains,
486 ..Default::default()
487 },
488 };
489 } else {
490 return Err(TlsConfigError::Spire(
491 "TLS needs to be enabled to use Spire".to_string(),
492 ));
493 }
494 }
495 RequiredAuthMethod::Basic => {}
496 RequiredAuthMethod::Jwt => {}
497 RequiredAuthMethod::Oidc => {}
498 }
499 if let Some(ms) = server.timeout {
500 self.connect_timeout = Duration::from_millis(ms as u64).into();
501 }
502 if let Some(ms) = server.backoff {
503 self.backoff =
504 BackoffConfig::new_fixed_interval(Duration::from_millis(ms as u64), usize::MAX);
505 }
506 if let Some(ka) = &server.keepalive {
507 self.keepalive = Some(ka.clone());
508 }
509 Ok(())
510 }
511
512 pub fn with_compression(self, compression: CompressionType) -> Self {
513 Self {
514 compression: Some(compression),
515 ..self
516 }
517 }
518
519 pub fn with_tls_setting(self, tls_setting: TLSSetting) -> Self {
520 Self {
521 tls_setting,
522 ..self
523 }
524 }
525
526 pub fn with_proxy(self, proxy: ProxyConfig) -> Self {
527 Self { proxy, ..self }
528 }
529
530 pub fn with_auth(self, auth: AuthenticationConfig) -> Self {
531 Self { auth, ..self }
532 }
533
534 pub fn with_backoff(self, backoff: BackoffConfig) -> Self {
535 Self { backoff, ..self }
536 }
537
538 pub fn with_metadata(self, metadata: MetadataMap) -> Self {
539 Self {
540 metadata: Some(metadata),
541 ..self
542 }
543 }
544
545 pub(crate) async fn retry_connect<T, F, Fut>(&self, attempt: F) -> Result<T, ConfigError>
553 where
554 F: FnMut() -> Fut,
555 Fut: std::future::Future<Output = Result<T, ConfigError>>,
556 {
557 use crate::backoff::Strategy;
558 use tokio_retry::RetryIf;
559
560 let strategy = self.backoff.get_strategy();
561 RetryIf::start(strategy, attempt, |e: &ConfigError| {
562 let retry = e.is_retryable_connect_error();
563 if retry {
564 tracing::warn!(error = %e.chain(), "transient connect error, retrying");
565 } else {
566 tracing::error!(error = %e.chain(), "non-retryable connect error");
567 }
568 retry
569 })
570 .await
571 }
572
573 pub async fn to_channel(
580 &self,
581 ) -> Result<
582 TransportChannel<
583 impl tonic::client::GrpcService<
584 tonic::body::Body,
585 Error: Into<StdError> + Send,
586 ResponseBody: Body<Data = Bytes, Error: Into<StdError> + std::marker::Send>
587 + Send
588 + 'static,
589 Future: Send,
590 >
591 + Send
592 + Clone
593 + 'static
594 + use<>,
595 >,
596 ConfigError,
597 > {
598 match self.resolved_transport() {
599 TransportProtocol::Grpc => Ok(TransportChannel::Grpc(self.to_grpc_channel().await?)),
600 TransportProtocol::Websocket => Ok(TransportChannel::Websocket(
601 self.to_websocket_channel().await?,
602 )),
603 }
604 }
605}
606
607#[cfg(target_arch = "wasm32")]
610impl ClientConfig {
611 pub async fn to_channel(
616 &self,
617 ) -> Result<TransportChannel<std::convert::Infallible>, ConfigError> {
618 match self.resolved_transport() {
619 TransportProtocol::Websocket => Ok(TransportChannel::Websocket(
620 self.to_websocket_channel().await?,
621 )),
622 TransportProtocol::Grpc => Err(ConfigError::WebSocketClientUnsupportedTransport),
623 }
624 }
625}
626
627#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema)]
628#[serde(rename_all = "snake_case")]
629pub enum RequiredAuthMethod {
630 #[default]
631 None,
632 Basic,
633 Jwt,
634 Oidc,
635 #[cfg(not(target_family = "windows"))]
636 Spire {
637 trust_domain: Option<String>,
638 },
639}
640
641#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, JsonSchema)]
642pub struct ServerConnectionConfig {
643 pub endpoint: String,
644 pub tls_required: bool,
645 pub auth_method: RequiredAuthMethod,
646 pub timeout: Option<u32>,
647 pub backoff: Option<u32>,
648 pub keepalive: Option<KeepaliveConfig>,
649}
650
651impl ServerConnectionConfig {
652 #[cfg(not(target_arch = "wasm32"))]
653 pub fn from_client_config(client: &ClientConfig) -> Self {
654 let tls_required = !client.tls_setting.insecure;
655 let auth_method = match &client.auth {
656 AuthenticationConfig::None => RequiredAuthMethod::None,
657 AuthenticationConfig::Basic(_) => RequiredAuthMethod::Basic,
658 AuthenticationConfig::StaticJwt(_) | AuthenticationConfig::Jwt(_) => {
659 RequiredAuthMethod::Jwt
660 }
661 AuthenticationConfig::Oidc(_) => RequiredAuthMethod::Oidc,
662 #[cfg(not(target_family = "windows"))]
663 AuthenticationConfig::Spire(cfg) => RequiredAuthMethod::Spire {
664 trust_domain: cfg.trust_domains.first().cloned(),
665 },
666 };
667 Self {
668 endpoint: client.endpoint.clone(),
669 tls_required,
670 auth_method,
671 timeout: None,
672 backoff: None,
673 keepalive: None,
674 }
675 }
676
677 #[cfg(target_arch = "wasm32")]
678 pub fn from_client_config(client: &ClientConfig) -> Self {
679 Self {
680 endpoint: client.endpoint.clone(),
681 tls_required: false,
682 auth_method: RequiredAuthMethod::None,
683 timeout: None,
684 backoff: None,
685 keepalive: None,
686 }
687 }
688}
689
690#[cfg(test)]
691mod metadata_tests {
692 use super::*;
693
694 #[test]
695 fn client_config_with_metadata_roundtrip_json() {
696 let mut md = MetadataMap::default();
697 md.insert("feature", "alpha");
698 md.insert("level", 2u64);
699
700 let cfg = ClientConfig::with_endpoint("http://localhost:1234").with_metadata(md.clone());
701 let s = serde_json::to_string(&cfg).expect("serialize");
702 let deser: ClientConfig = serde_json::from_str(&s).expect("deserialize");
703 assert_eq!(deser.metadata, Some(md));
704 }
705}
706
707#[cfg(test)]
708mod tests {
709 use super::*;
710
711 #[test]
712 fn test_default_keepalive_config() {
713 let keepalive = KeepaliveConfig::default();
714 assert_eq!(keepalive.tcp_keepalive, Duration::from_secs(60));
715 assert_eq!(keepalive.http2_keepalive, Duration::from_secs(60));
716 assert_eq!(keepalive.timeout, Duration::from_secs(10));
717 assert!(!keepalive.keep_alive_while_idle);
718 }
719
720 #[test]
721 fn test_default_client_config() {
722 let client = ClientConfig::default();
723 assert_eq!(client.endpoint, String::new());
724 assert_eq!(client.resolved_transport(), TransportProtocol::Grpc);
725 assert_eq!(client.origin, None);
726 assert_eq!(client.compression, None);
727 assert_eq!(client.rate_limit, None);
728 assert_eq!(client.tls_setting, TLSSetting::default());
729 assert_eq!(client.keepalive, None);
730 assert_eq!(client.connect_timeout, Duration::from_secs(0));
731 assert_eq!(client.request_timeout, Duration::from_secs(0));
732 assert_eq!(client.buffer_size, None);
733 assert_eq!(client.headers, HashMap::new());
734 assert_eq!(client.auth, AuthenticationConfig::None);
735 }
736
737 #[test]
738 fn test_transport_inferred_from_endpoint_scheme() {
739 assert_eq!(
740 ClientConfig::with_endpoint("ws://localhost:46357").resolved_transport(),
741 TransportProtocol::Websocket
742 );
743 assert_eq!(
744 ClientConfig::with_endpoint("wss://localhost:46357").resolved_transport(),
745 TransportProtocol::Websocket
746 );
747 assert_eq!(
748 ClientConfig::with_endpoint("http://localhost:46357").resolved_transport(),
749 TransportProtocol::Grpc
750 );
751 assert_eq!(
752 ClientConfig::with_endpoint("https://localhost:46357").resolved_transport(),
753 TransportProtocol::Grpc
754 );
755 assert_eq!(
756 ClientConfig::with_endpoint("127.0.0.1:46357").resolved_transport(),
757 TransportProtocol::Grpc
758 );
759
760 let invalid = ClientConfig::with_endpoint("ftp://localhost:46357");
762 assert!(matches!(
763 invalid.validate(),
764 Err(ConfigError::InvalidEndpointScheme)
765 ));
766 }
767
768 #[test]
769 fn test_client_config_with_proxy() {
770 let test_password: String = format!("test-{}-{}", std::process::id(), line!()); let proxy =
772 ProxyConfig::new("http://proxy.example.com:8080").with_auth("user", &test_password);
773 let client = ClientConfig::with_endpoint("http://localhost:8080").with_proxy(proxy.clone());
774 assert_eq!(client.proxy, proxy);
775 }
776
777 #[test]
778 fn test_connect_and_request_timeout_valid_durations_deserialize() {
779 let json = r#"{
780 "endpoint": "http://localhost:1234",
781 "connect_timeout": "1m30s",
782 "request_timeout": "250ms"
783 }"#;
784
785 let cfg: ClientConfig = serde_json::from_str(json).expect("deserialization should succeed");
786 assert_eq!(cfg.connect_timeout, Duration::from_secs(90));
787 assert_eq!(cfg.request_timeout, Duration::from_millis(250));
788
789 let json = r#"{
791 "endpoint": "http://localhost:1234",
792 "connect_timeout": "1h2m3s4ms",
793 "request_timeout": "1500ms"
794 }"#;
795 let cfg: ClientConfig =
796 serde_json::from_str(json).expect("complex duration should deserialize");
797 assert_eq!(
798 cfg.connect_timeout,
799 Duration::from_secs(3723) + Duration::from_millis(4)
800 );
801 assert_eq!(cfg.request_timeout, Duration::from_millis(1500));
802 }
803
804 #[test]
805 fn test_invalid_duration_strings_fail_deserialize() {
806 let invalids = [
807 r#"{ "endpoint": "http://localhost:1234", "connect_timeout": "abc" }"#,
808 r#"{ "endpoint": "http://localhost:1234", "request_timeout": "10x" }"#,
809 r#"{ "endpoint": "http://localhost:1234", "request_timeout": "--5s" }"#,
810 ];
811 for js in invalids {
812 let res: Result<ClientConfig, _> = serde_json::from_str(js);
813 assert!(res.is_err(), "expected error for json: {}", js);
814 }
815 }
816
817 #[test]
818 fn test_keepalive_config_duration_parsing() {
819 let json = r#"{
820 "endpoint": "http://localhost:1234",
821 "keepalive": {
822 "tcp_keepalive": "30s",
823 "http2_keepalive": "45s",
824 "timeout": "5s",
825 "keep_alive_while_idle": true
826 }
827 }"#;
828 let cfg: ClientConfig = serde_json::from_str(json).expect("keepalive should deserialize");
829 let ka = cfg.keepalive.expect("keepalive should be present");
830 assert_eq!(ka.tcp_keepalive, Duration::from_secs(30));
831 assert_eq!(ka.http2_keepalive, Duration::from_secs(45));
832 assert_eq!(ka.timeout, Duration::from_secs(5));
833 assert!(ka.keep_alive_while_idle);
834
835 let invalid_json = r#"{
837 "endpoint": "http://localhost:1234",
838 "keepalive": { "tcp_keepalive": "zz", "http2_keepalive": "10s", "timeout": "5s", "keep_alive_while_idle": false }
839 }"#;
840 let res: Result<ClientConfig, _> = serde_json::from_str(invalid_json);
841 assert!(res.is_err(), "invalid tcp_keepalive should fail");
842 }
843
844 #[test]
845 fn test_client_config_roundtrip_duration_serialization() {
846 let mut cfg = ClientConfig::with_endpoint("http://localhost:9999")
847 .with_connect_timeout(Duration::from_secs(90))
848 .with_request_timeout(Duration::from_millis(750));
849
850 cfg.keepalive = Some(KeepaliveConfig {
851 tcp_keepalive: Duration::from_secs(11).into(),
852 http2_keepalive: Duration::from_secs(22).into(),
853 timeout: Duration::from_secs(3).into(),
854 keep_alive_while_idle: true,
855 });
856
857 let serialized = serde_json::to_string(&cfg).expect("serialize");
858 let deserialized: ClientConfig = serde_json::from_str(&serialized).expect("deserialize");
859
860 assert_eq!(deserialized.connect_timeout, Duration::from_secs(90));
861 assert_eq!(deserialized.request_timeout, Duration::from_millis(750));
862 let ka = deserialized.keepalive.expect("keepalive present");
863 assert_eq!(ka.tcp_keepalive, Duration::from_secs(11));
864 assert_eq!(ka.http2_keepalive, Duration::from_secs(22));
865 assert_eq!(ka.timeout, Duration::from_secs(3));
866 assert!(ka.keep_alive_while_idle);
867 }
868
869 #[test]
870 fn test_validate_accepts_any_link_id() {
871 let mut config = ClientConfig::with_endpoint("http://localhost:1234");
872 config.link_id = "my-custom-link-id".to_string();
873 assert!(config.validate().is_ok());
874 }
875
876 #[test]
877 fn test_merge_server_requirements_none_auth() {
878 let mut client = ClientConfig::with_endpoint("http://old:1234")
879 .with_auth(AuthenticationConfig::Basic(Default::default()));
880 let server = ServerConnectionConfig {
881 endpoint: "http://new:5678".to_string(),
882 tls_required: false,
883 auth_method: RequiredAuthMethod::None,
884 ..Default::default()
885 };
886 client.merge_server_requirements(&server).unwrap();
887 assert_eq!(client.endpoint, "http://new:5678");
888 assert!(client.tls_setting.insecure);
889 assert_eq!(client.auth, AuthenticationConfig::Basic(Default::default()));
893 }
894
895 #[test]
896 fn test_merge_server_requirements_basic_auth_preserved() {
897 let basic = AuthenticationConfig::Basic(Default::default());
898 let mut client = ClientConfig::with_endpoint("http://old:1234").with_auth(basic.clone());
899 let server = ServerConnectionConfig {
900 endpoint: "http://new:5678".to_string(),
901 tls_required: true,
902 auth_method: RequiredAuthMethod::Basic,
903 ..Default::default()
904 };
905 client.merge_server_requirements(&server).unwrap();
906 assert_eq!(client.endpoint, "http://new:5678");
907 assert!(!client.tls_setting.insecure);
908 assert_eq!(client.auth, basic);
909 }
910
911 #[test]
912 fn test_merge_server_requirements_jwt_auth_preserved() {
913 let jwt = AuthenticationConfig::StaticJwt(Default::default());
914 let mut client = ClientConfig::with_endpoint("http://old:1234").with_auth(jwt.clone());
915 let server = ServerConnectionConfig {
916 endpoint: "http://new:9999".to_string(),
917 tls_required: false,
918 auth_method: RequiredAuthMethod::Jwt,
919 ..Default::default()
920 };
921 client.merge_server_requirements(&server).unwrap();
922 assert_eq!(client.endpoint, "http://new:9999");
923 assert_eq!(client.auth, jwt);
924 }
925
926 #[cfg(not(target_family = "windows"))]
927 #[test]
928 fn test_merge_server_requirements_spire_with_tls_sets_sources() {
929 use crate::tls::common::{CaSource, TlsSource};
930
931 let mut client = ClientConfig::with_endpoint("http://old:1234");
932 let server = ServerConnectionConfig {
933 endpoint: "http://new:5678".to_string(),
934 tls_required: true,
935 auth_method: RequiredAuthMethod::Spire {
936 trust_domain: Some("example.org".to_string()),
937 },
938 ..Default::default()
939 };
940 client.merge_server_requirements(&server).unwrap();
941 assert_eq!(client.endpoint, "http://new:5678");
942 assert!(!client.tls_setting.insecure);
943 assert!(matches!(
944 client.tls_setting.config.source,
945 TlsSource::Spire { .. }
946 ));
947 assert!(matches!(
948 client.tls_setting.config.ca_source,
949 CaSource::Spire { .. }
950 ));
951 }
952
953 #[cfg(not(target_family = "windows"))]
954 #[test]
955 fn test_merge_server_requirements_spire_auth_socket_path_propagates_to_tls() {
956 use crate::auth::spire::SpireConfig;
957 use crate::tls::common::{CaSource, TlsSource};
958
959 let mut client = ClientConfig::with_endpoint("https://127.0.0.1:46201").with_auth(
960 AuthenticationConfig::Spire(
961 SpireConfig::new()
962 .with_socket_path("/run/spire/agent.sock")
963 .with_trust_domain("example.org"),
964 ),
965 );
966 let server = ServerConnectionConfig {
967 endpoint: "https://127.0.0.1:46201".to_string(),
968 tls_required: true,
969 auth_method: RequiredAuthMethod::Spire {
970 trust_domain: Some("example.org".to_string()),
971 },
972 ..Default::default()
973 };
974 client.merge_server_requirements(&server).unwrap();
975 if let TlsSource::Spire { config } = &client.tls_setting.config.source {
976 assert_eq!(config.socket_path.as_deref(), Some("/run/spire/agent.sock"));
977 } else {
978 panic!("expected TlsSource::Spire");
979 }
980 if let CaSource::Spire { config } = &client.tls_setting.config.ca_source {
981 assert_eq!(config.socket_path.as_deref(), Some("/run/spire/agent.sock"));
982 assert_eq!(config.trust_domains, vec!["example.org"]);
983 } else {
984 panic!("expected CaSource::Spire");
985 }
986 if let AuthenticationConfig::Spire(auth) = &client.auth {
987 assert_eq!(auth.socket_path.as_deref(), Some("/run/spire/agent.sock"));
988 } else {
989 panic!("expected AuthenticationConfig::Spire");
990 }
991 }
992
993 #[cfg(not(target_family = "windows"))]
994 #[test]
995 fn test_merge_server_requirements_spire_ignores_tls_socket_path() {
996 use crate::auth::spire::SpireConfig;
997 use crate::tls::common::{CaSource, TlsSource};
998
999 let mut client = ClientConfig::with_endpoint("http://old:1234");
1000 client.tls_setting.config.source = TlsSource::Spire {
1001 config: SpireConfig {
1002 socket_path: Some("/run/spire.sock".to_string()),
1003 ..Default::default()
1004 },
1005 };
1006 let server = ServerConnectionConfig {
1007 endpoint: "http://new:5678".to_string(),
1008 tls_required: true,
1009 auth_method: RequiredAuthMethod::Spire {
1010 trust_domain: Some("example.org".to_string()),
1011 },
1012 ..Default::default()
1013 };
1014 client.merge_server_requirements(&server).unwrap();
1015 if let TlsSource::Spire { config } = &client.tls_setting.config.source {
1016 assert_eq!(config.socket_path, None);
1017 } else {
1018 panic!("expected TlsSource::Spire");
1019 }
1020 if let CaSource::Spire { config } = &client.tls_setting.config.ca_source {
1021 assert_eq!(config.socket_path, None);
1022 assert_eq!(config.trust_domains, vec!["example.org"]);
1023 } else {
1024 panic!("expected CaSource::Spire");
1025 }
1026 }
1027
1028 #[cfg(not(target_family = "windows"))]
1029 #[test]
1030 fn test_merge_server_requirements_spire_no_trust_domain() {
1031 use crate::tls::common::CaSource;
1032
1033 let mut client = ClientConfig::with_endpoint("http://old:1234");
1034 let server = ServerConnectionConfig {
1035 endpoint: "http://new:5678".to_string(),
1036 tls_required: true,
1037 auth_method: RequiredAuthMethod::Spire { trust_domain: None },
1038 ..Default::default()
1039 };
1040 client.merge_server_requirements(&server).unwrap();
1041 if let CaSource::Spire { config } = &client.tls_setting.config.ca_source {
1042 assert!(config.trust_domains.is_empty());
1043 } else {
1044 panic!("expected CaSource::Spire");
1045 }
1046 }
1047
1048 #[cfg(not(target_family = "windows"))]
1049 #[test]
1050 fn test_merge_server_requirements_spire_without_tls_errors() {
1051 let mut client = ClientConfig::with_endpoint("http://old:1234");
1052 let server = ServerConnectionConfig {
1053 endpoint: "http://new:5678".to_string(),
1054 tls_required: false,
1055 auth_method: RequiredAuthMethod::Spire {
1056 trust_domain: Some("example.org".to_string()),
1057 },
1058 ..Default::default()
1059 };
1060 assert!(client.merge_server_requirements(&server).is_err());
1061 }
1062
1063 #[test]
1064 fn merge_server_requirements_cp_backoff_overrides_local() {
1065 let local_backoff =
1066 BackoffConfig::new_exponential(100, 2, Duration::from_secs(30), 10, false);
1067 let mut client =
1068 ClientConfig::with_endpoint("http://host:8080").with_backoff(local_backoff);
1069 let server = ServerConnectionConfig {
1070 endpoint: "http://host:8080".to_string(),
1071 backoff: Some(5000),
1072 ..Default::default()
1073 };
1074 client.merge_server_requirements(&server).unwrap();
1075 assert_eq!(
1076 client.backoff,
1077 BackoffConfig::new_fixed_interval(Duration::from_millis(5000), usize::MAX)
1078 );
1079 }
1080
1081 #[test]
1082 fn merge_server_requirements_preserves_local_backoff_when_cp_none() {
1083 let local_backoff = BackoffConfig::new_fixed_interval(Duration::from_secs(2), usize::MAX);
1084 let mut client =
1085 ClientConfig::with_endpoint("http://host:8080").with_backoff(local_backoff.clone());
1086 let server = ServerConnectionConfig {
1087 endpoint: "http://host:8080".to_string(),
1088 backoff: None,
1089 ..Default::default()
1090 };
1091 client.merge_server_requirements(&server).unwrap();
1092 assert_eq!(client.backoff, local_backoff);
1093 }
1094
1095 #[test]
1096 fn merge_server_requirements_cp_timeout_overrides_local() {
1097 let mut client = ClientConfig::with_endpoint("http://host:8080")
1098 .with_connect_timeout(Duration::from_secs(10));
1099 let server = ServerConnectionConfig {
1100 endpoint: "http://host:8080".to_string(),
1101 timeout: Some(3000),
1102 ..Default::default()
1103 };
1104 client.merge_server_requirements(&server).unwrap();
1105 assert_eq!(client.connect_timeout, Duration::from_millis(3000));
1106 }
1107
1108 #[test]
1109 fn merge_server_requirements_preserves_local_timeout_when_cp_none() {
1110 let mut client = ClientConfig::with_endpoint("http://host:8080")
1111 .with_connect_timeout(Duration::from_secs(10));
1112 let server = ServerConnectionConfig {
1113 endpoint: "http://host:8080".to_string(),
1114 timeout: None,
1115 ..Default::default()
1116 };
1117 client.merge_server_requirements(&server).unwrap();
1118 assert_eq!(client.connect_timeout, Duration::from_secs(10));
1119 }
1120
1121 #[test]
1122 fn merge_server_requirements_cp_keepalive_overrides_local() {
1123 let local_ka = KeepaliveConfig {
1124 tcp_keepalive: Duration::from_secs(30).into(),
1125 http2_keepalive: Duration::from_secs(30).into(),
1126 timeout: Duration::from_secs(5).into(),
1127 keep_alive_while_idle: false,
1128 };
1129 let cp_ka = KeepaliveConfig {
1130 tcp_keepalive: Duration::from_secs(60).into(),
1131 http2_keepalive: Duration::from_secs(60).into(),
1132 timeout: Duration::from_secs(10).into(),
1133 keep_alive_while_idle: true,
1134 };
1135 let mut client = ClientConfig::with_endpoint("http://host:8080").with_keepalive(local_ka);
1136 let server = ServerConnectionConfig {
1137 endpoint: "http://host:8080".to_string(),
1138 keepalive: Some(cp_ka.clone()),
1139 ..Default::default()
1140 };
1141 client.merge_server_requirements(&server).unwrap();
1142 assert_eq!(client.keepalive, Some(cp_ka));
1143 }
1144
1145 #[test]
1146 fn merge_server_requirements_preserves_local_keepalive_when_cp_none() {
1147 let local_ka = KeepaliveConfig {
1148 tcp_keepalive: Duration::from_secs(30).into(),
1149 http2_keepalive: Duration::from_secs(30).into(),
1150 timeout: Duration::from_secs(5).into(),
1151 keep_alive_while_idle: true,
1152 };
1153 let mut client =
1154 ClientConfig::with_endpoint("http://host:8080").with_keepalive(local_ka.clone());
1155 let server = ServerConnectionConfig {
1156 endpoint: "http://host:8080".to_string(),
1157 keepalive: None,
1158 ..Default::default()
1159 };
1160 client.merge_server_requirements(&server).unwrap();
1161 assert_eq!(client.keepalive, Some(local_ka));
1162 }
1163
1164 #[test]
1165 fn test_from_client_config_none_auth() {
1166 let client = ClientConfig::with_endpoint("http://host:1234")
1167 .with_tls_setting(TLSSetting::insecure());
1168 let server = ServerConnectionConfig::from_client_config(&client);
1169 assert_eq!(server.endpoint, "http://host:1234");
1170 assert!(!server.tls_required);
1171 assert_eq!(server.auth_method, RequiredAuthMethod::None);
1172 assert!(server.timeout.is_none());
1173 assert!(server.backoff.is_none());
1174 }
1175
1176 #[test]
1177 fn test_from_client_config_basic_auth() {
1178 let client = ClientConfig::with_endpoint("http://host:1234")
1179 .with_auth(AuthenticationConfig::Basic(Default::default()));
1180 let server = ServerConnectionConfig::from_client_config(&client);
1181 assert_eq!(server.auth_method, RequiredAuthMethod::Basic);
1182 }
1183
1184 #[test]
1185 fn test_from_client_config_jwt_auth() {
1186 let client = ClientConfig::with_endpoint("http://host:1234")
1187 .with_auth(AuthenticationConfig::StaticJwt(Default::default()));
1188 let server = ServerConnectionConfig::from_client_config(&client);
1189 assert_eq!(server.auth_method, RequiredAuthMethod::Jwt);
1190 }
1191
1192 #[cfg(not(target_family = "windows"))]
1193 #[test]
1194 fn test_from_client_config_spire_auth() {
1195 use crate::auth::spire::SpireConfig;
1196 let spire = SpireConfig {
1197 trust_domains: vec!["example.org".to_string()],
1198 ..Default::default()
1199 };
1200 let client = ClientConfig::with_endpoint("https://host:1234")
1201 .with_auth(AuthenticationConfig::Spire(spire));
1202 let server = ServerConnectionConfig::from_client_config(&client);
1203 assert_eq!(
1204 server.auth_method,
1205 RequiredAuthMethod::Spire {
1206 trust_domain: Some("example.org".to_string())
1207 }
1208 );
1209 assert!(server.tls_required);
1210 }
1211}