1#![cfg_attr(feature = "_sync-sender", doc = include_str!("ingress/mod.md"))]
26#![cfg_attr(
27 not(feature = "_sync-sender"),
28 doc = "Shared data types used by the egress reader. Enable a `sync-sender-*` \
29 feature to expose the sender APIs and their full module documentation."
30)]
31
32#[cfg(feature = "_sender-qwp-ws")]
33pub(crate) use self::conf::QwpWsManagedSlotExclusion;
34pub use self::ndarr::{ArrayElement, NdArrayView};
35pub use self::timestamp::*;
36use crate::error::Result;
37#[cfg(feature = "_sync-sender")]
38use crate::error::{self, fmt};
39#[cfg(feature = "_sync-sender")]
40use crate::ingress::conf::ConfigSetting;
41#[cfg(feature = "_sync-sender")]
42use core::time::Duration;
43#[cfg(feature = "_sync-sender")]
44use std::collections::HashMap;
45#[cfg(feature = "_sender-qwp-ws")]
46use std::collections::HashSet;
47#[cfg(feature = "_sync-sender")]
48use std::fmt::Write;
49use std::fmt::{Debug, Display, Formatter};
50
51#[cfg(feature = "_sync-sender")]
52use std::ops::Deref;
53#[cfg(feature = "_sender-qwp-ws")]
54use std::path::Path;
55#[cfg(feature = "_sync-sender")]
56use std::path::PathBuf;
57#[cfg(feature = "_sync-sender")]
58use std::str::FromStr;
59
60#[cfg(feature = "_sync-sender")]
61mod tls;
62
63#[cfg(all(feature = "_sender-tcp", feature = "aws-lc-crypto"))]
64use aws_lc_rs::signature::{ECDSA_P256_SHA256_FIXED_SIGNING, EcdsaKeyPair};
65
66#[cfg(all(feature = "_sender-tcp", feature = "ring-crypto"))]
67use ring::{
68 rand::SystemRandom,
69 signature::{ECDSA_P256_SHA256_FIXED_SIGNING, EcdsaKeyPair},
70};
71
72#[cfg(feature = "_sync-sender")]
73mod conf;
74
75#[cfg(feature = "_sender-qwp-ws")]
76pub mod conn_events;
77#[cfg(feature = "_sender-qwp-ws")]
78pub use conn_events::{
79 ConnectionEvent, ConnectionEventDispatcher, ConnectionEventKind, ConnectionListener,
80};
81
82#[cfg(feature = "_sender-qwp-ws")]
83pub(crate) mod rejection_events;
84
85pub(crate) mod ndarr;
86
87mod timestamp;
88
89mod buffer;
90pub use buffer::*;
91
92#[cfg(feature = "_sync-sender")]
93pub(crate) mod sender;
94#[cfg(feature = "_sender-qwp-ws")]
95pub(crate) use sender::QwpWsRoleReject;
96#[cfg(feature = "polars-ingress")]
97pub(crate) use sender::ReconnectPolicy;
98#[cfg(feature = "sync-sender-qwp-ws")]
99pub(crate) use sender::ReconnectReason;
100#[cfg(feature = "_sync-sender")]
101pub use sender::*;
102#[cfg(feature = "sync-sender-qwp-ws")]
103pub(crate) use sender::{reconnect_backoff_step, reconnect_error_is_terminal};
104
105mod decimal;
106pub use decimal::DecimalView;
107
108#[cfg(feature = "sync-sender-qwp-ws")]
109pub mod column_sender;
110
111#[cfg(feature = "sync-sender-qwp-ws")]
114#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
115#[non_exhaustive]
116pub enum AckLevel {
117 #[default]
119 Ok,
120 Durable,
123}
124
125#[cfg(feature = "sync-sender-qwp-ws")]
131#[derive(Clone, Copy, Debug, PartialEq, Eq)]
132pub enum TimestampUnit {
133 Micros,
135 Nanos,
137}
138
139#[cfg(feature = "polars-ingress")]
140pub mod polars;
141
142const MAX_NAME_LEN_DEFAULT: usize = 127;
143
144pub const MAX_ARRAY_DIMS: usize = 32;
146pub const MAX_ARRAY_BUFFER_SIZE: usize = 512 * 1024 * 1024; pub const MAX_ARRAY_DIM_LEN: usize = 0x0FFF_FFFF; pub const MAX_NDARRAY_LEAF_ELEMS: usize = 1 << 24;
155
156pub(crate) const ARRAY_BINARY_FORMAT_TYPE: u8 = 14;
157pub(crate) const DOUBLE_BINARY_FORMAT_TYPE: u8 = 16;
158pub const DECIMAL_BINARY_FORMAT_TYPE: u8 = 23;
159
160#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
166pub enum ProtocolVersion {
167 V1 = 1,
169
170 V2 = 2,
172
173 V3 = 3,
175}
176
177#[cfg(feature = "_sender-http")]
179const SUPPORTED_PROTOCOL_VERSIONS: [ProtocolVersion; 3] = [
180 ProtocolVersion::V3,
181 ProtocolVersion::V2,
182 ProtocolVersion::V1,
183];
184
185impl Display for ProtocolVersion {
186 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
187 match self {
188 ProtocolVersion::V1 => write!(f, "v1"),
189 ProtocolVersion::V2 => write!(f, "v2"),
190 ProtocolVersion::V3 => write!(f, "v3"),
191 }
192 }
193}
194
195#[cfg(feature = "_sender-tcp")]
196fn map_io_to_socket_err(prefix: &str, io_err: std::io::Error) -> error::Error {
197 fmt!(SocketError, "{}{}", prefix, io_err)
198}
199
200#[derive(PartialEq, Debug, Clone, Copy)]
203pub enum CertificateAuthority {
204 #[cfg(feature = "tls-webpki-certs")]
207 WebpkiRoots,
208
209 #[cfg(feature = "tls-native-certs")]
211 OsRoots,
212
213 #[cfg(all(feature = "tls-webpki-certs", feature = "tls-native-certs"))]
215 WebpkiAndOsRoots,
216
217 PemFile,
219}
220
221#[cfg(feature = "_sync-sender")]
241pub struct Port(String);
242
243#[cfg(feature = "_sync-sender")]
244impl From<String> for Port {
245 fn from(s: String) -> Self {
246 Port(s)
247 }
248}
249
250#[cfg(feature = "_sync-sender")]
251impl From<&str> for Port {
252 fn from(s: &str) -> Self {
253 Port(s.to_owned())
254 }
255}
256
257#[cfg(feature = "_sync-sender")]
258impl From<u16> for Port {
259 fn from(p: u16) -> Self {
260 Port(p.to_string())
261 }
262}
263
264#[cfg(feature = "_sync-sender")]
265fn validate_auto_flush_params(params: &HashMap<String, String>) -> Result<()> {
266 if let Some(auto_flush) = params.get("auto_flush")
267 && auto_flush.as_str() != "off"
268 {
269 return Err(error::fmt!(
270 ConfigError,
271 "Invalid auto_flush value '{auto_flush}'. This client does not \
272 support auto-flush, so the only accepted value is 'off'"
273 ));
274 }
275
276 for ¶m in ["auto_flush_rows", "auto_flush_bytes", "auto_flush_interval"].iter() {
277 if params.contains_key(param) {
278 return Err(error::fmt!(
279 ConfigError,
280 "Invalid configuration parameter {:?}. This client does not support auto-flush",
281 param
282 ));
283 }
284 }
285 Ok(())
286}
287
288#[derive(PartialEq, Debug, Clone, Copy)]
294#[non_exhaustive]
295#[cfg(feature = "_sync-sender")]
296pub enum Protocol {
297 #[cfg(feature = "_sender-tcp")]
298 Tcp,
300
301 #[cfg(feature = "_sender-tcp")]
302 Tcps,
304
305 #[cfg(feature = "_sender-http")]
306 Http,
309
310 #[cfg(feature = "_sender-http")]
311 Https,
313
314 #[cfg(feature = "_sender-qwp-udp")]
315 Udp,
317
318 #[cfg(feature = "_sender-qwp-ws")]
319 Ws,
321
322 #[cfg(feature = "_sender-qwp-ws")]
323 Wss,
325}
326
327#[cfg(feature = "_sync-sender")]
328impl Display for Protocol {
329 fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
330 f.write_str(self.schema())
331 }
332}
333
334#[cfg(feature = "_sync-sender")]
335impl Protocol {
336 fn default_port(&self) -> &str {
337 match *self {
338 #[cfg(feature = "_sender-tcp")]
339 Protocol::Tcp | Protocol::Tcps => "9009",
340 #[cfg(feature = "_sender-http")]
341 Protocol::Http | Protocol::Https => "9000",
342 #[cfg(feature = "_sender-qwp-udp")]
343 Protocol::Udp => "9007",
344 #[cfg(feature = "_sender-qwp-ws")]
345 Protocol::Ws | Protocol::Wss => "9000",
346 }
347 }
348
349 fn tls_enabled(&self) -> bool {
350 match *self {
351 #[cfg(feature = "_sender-tcp")]
352 Protocol::Tcp => false,
353 #[cfg(feature = "_sender-tcp")]
354 Protocol::Tcps => true,
355 #[cfg(feature = "_sender-http")]
356 Protocol::Http => false,
357 #[cfg(feature = "_sender-http")]
358 Protocol::Https => true,
359 #[cfg(feature = "_sender-qwp-udp")]
360 Protocol::Udp => false,
361 #[cfg(feature = "_sender-qwp-ws")]
362 Protocol::Ws => false,
363 #[cfg(feature = "_sender-qwp-ws")]
364 Protocol::Wss => true,
365 }
366 }
367
368 #[cfg(feature = "_sender-tcp")]
369 fn is_tcpx(&self) -> bool {
370 match self {
371 Protocol::Tcp | Protocol::Tcps => true,
372 #[cfg(feature = "_sender-http")]
373 Protocol::Http | Protocol::Https => false,
374 #[cfg(feature = "_sender-qwp-udp")]
375 Protocol::Udp => false,
376 #[cfg(feature = "_sender-qwp-ws")]
377 Protocol::Ws | Protocol::Wss => false,
378 }
379 }
380
381 #[cfg(feature = "_sender-http")]
382 fn is_httpx(&self) -> bool {
383 match self {
384 #[cfg(feature = "_sender-tcp")]
385 Protocol::Tcp | Protocol::Tcps => false,
386 Protocol::Http | Protocol::Https => true,
387 #[cfg(feature = "_sender-qwp-udp")]
388 Protocol::Udp => false,
389 #[cfg(feature = "_sender-qwp-ws")]
390 Protocol::Ws | Protocol::Wss => false,
391 }
392 }
393
394 #[cfg(feature = "_sender-qwp-udp")]
395 fn is_qwp_udp(&self) -> bool {
396 matches!(self, Protocol::Udp)
397 }
398
399 #[cfg(feature = "_sender-qwp-ws")]
400 fn is_qwp_ws(&self) -> bool {
401 matches!(self, Protocol::Ws | Protocol::Wss)
402 }
403
404 #[cfg(any(feature = "_sender-http", feature = "_sender-qwp-ws"))]
407 fn accepts_http_auth(&self) -> bool {
408 let mut accepts = false;
409 #[cfg(feature = "_sender-http")]
410 if self.is_httpx() {
411 accepts = true;
412 }
413 #[cfg(feature = "_sender-qwp-ws")]
414 if self.is_qwp_ws() {
415 accepts = true;
416 }
417 accepts
418 }
419
420 fn schema(&self) -> &str {
421 match *self {
422 #[cfg(feature = "_sender-tcp")]
423 Protocol::Tcp => "tcp",
424 #[cfg(feature = "_sender-tcp")]
425 Protocol::Tcps => "tcps",
426 #[cfg(feature = "_sender-http")]
427 Protocol::Http => "http",
428 #[cfg(feature = "_sender-http")]
429 Protocol::Https => "https",
430 #[cfg(feature = "_sender-qwp-udp")]
431 Protocol::Udp => "udp",
432 #[cfg(feature = "_sender-qwp-ws")]
433 Protocol::Ws => "ws",
434 #[cfg(feature = "_sender-qwp-ws")]
435 Protocol::Wss => "wss",
436 }
437 }
438
439 fn from_schema(schema: &str) -> Result<Self> {
440 #[cfg(feature = "_sender-tcp")]
441 if schema.eq_ignore_ascii_case("tcp") {
442 return Ok(Protocol::Tcp);
443 }
444 #[cfg(feature = "_sender-tcp")]
445 if schema.eq_ignore_ascii_case("tcps") {
446 return Ok(Protocol::Tcps);
447 }
448 #[cfg(feature = "_sender-http")]
449 if schema.eq_ignore_ascii_case("http") {
450 return Ok(Protocol::Http);
451 }
452 #[cfg(feature = "_sender-http")]
453 if schema.eq_ignore_ascii_case("https") {
454 return Ok(Protocol::Https);
455 }
456 #[cfg(feature = "_sender-qwp-udp")]
457 if schema.eq_ignore_ascii_case("udp") {
458 return Ok(Protocol::Udp);
459 }
460 #[cfg(feature = "_sender-qwp-udp")]
461 if schema.eq_ignore_ascii_case("udps") {
462 return Err(error::fmt!(ConfigError, "TLS is not supported for UDP."));
463 }
464 #[cfg(feature = "_sender-qwp-ws")]
465 if schema.eq_ignore_ascii_case("ws") {
466 return Ok(Protocol::Ws);
467 }
468 #[cfg(feature = "_sender-qwp-ws")]
469 if schema.eq_ignore_ascii_case("wss") {
470 return Ok(Protocol::Wss);
471 }
472 Err(error::fmt!(ConfigError, "Unsupported protocol: {}", schema))
473 }
474}
475
476#[cfg(any(feature = "_sender-qwp-ws", feature = "_egress"))]
477pub(crate) struct QwpWsAddrScan {
478 pub(crate) addr_values: Vec<String>,
479 pub(crate) sanitized_conf: String,
480}
481
482#[cfg(feature = "sync-sender-qwp-ws")]
490pub(crate) struct QwpWsConnector {
491 host: String,
492 port: String,
493 endpoints: std::sync::Arc<[conf::QwpWsEndpoint]>,
494 use_tls: bool,
495 tls_settings: Option<tls::TlsSettings>,
496 qwp_ws: conf::QwpWsConfig,
497 auth_header: Option<String>,
498 max_buf_size: usize,
499}
500
501#[cfg(feature = "sync-sender-qwp-ws")]
502impl QwpWsConnector {
503 pub(crate) fn endpoint_count(&self) -> usize {
506 self.endpoints.len()
507 }
508
509 pub(crate) fn endpoint(&self, idx: usize) -> Option<&conf::QwpWsEndpoint> {
511 self.endpoints.get(idx)
512 }
513
514 pub(crate) fn max_buf_size(&self) -> usize {
515 self.max_buf_size
516 }
517
518 pub(crate) fn request_durable_ack(&self) -> bool {
519 *self.qwp_ws.request_durable_ack
520 }
521
522 pub(crate) fn sender_id(&self) -> &str {
523 self.qwp_ws.sender_id.as_str()
524 }
525
526 pub(crate) fn sf_dir(&self) -> Option<&Path> {
527 self.qwp_ws.sf_dir.as_deref()
528 }
529
530 pub(crate) fn request_timeout(&self) -> Duration {
536 *self.qwp_ws.request_timeout
537 }
538
539 pub(crate) fn close_flush_timeout(&self) -> Duration {
544 *self.qwp_ws.close_flush_timeout
545 }
546
547 #[cfg(feature = "sync-sender-qwp-ws")]
553 #[cfg_attr(
554 not(any(
555 feature = "polars-ingress",
556 feature = "polars-egress",
557 feature = "ffi-support"
558 )),
559 allow(dead_code)
560 )]
561 pub(crate) fn reconnect_policy(&self) -> sender::ReconnectPolicy {
562 sender::ReconnectPolicy::bounded(
563 *self.qwp_ws.reconnect_max_duration,
564 *self.qwp_ws.reconnect_initial_backoff,
565 *self.qwp_ws.reconnect_max_backoff,
566 )
567 }
568
569 pub(crate) fn connect_round_pooled(
577 &self,
578 health: &std::sync::Mutex<sender::qwp_ws::QwpWsHostHealthTracker>,
579 events: Option<&conn_events::ConnectionEventSource>,
580 ) -> Result<RawQwpWsRoundStream> {
581 self.connect_round_with(sender::qwp_ws::LockedQwpWsHealth::new(health), events)
582 }
583
584 fn connect_round_with<A: sender::qwp_ws::QwpWsHealthAccess>(
585 &self,
586 health: A,
587 events: Option<&conn_events::ConnectionEventSource>,
588 ) -> Result<RawQwpWsRoundStream> {
589 let mut previous_idx = None;
590 let connected = sender::qwp_ws::connect_qwp_ws_endpoint_round(
591 &self.endpoints,
592 health,
593 &mut previous_idx,
594 None,
595 self.use_tls,
596 self.tls_settings.clone(),
597 sender::qwp_ws::QwpWsConnectKind::Foreground,
598 &self.qwp_ws,
599 self.auth_header.as_deref(),
600 events,
601 None,
602 )?;
603 let max_buf_size = if connected.server_max_batch_size > 0 {
608 self.max_buf_size.min(connected.server_max_batch_size)
609 } else {
610 self.max_buf_size
611 };
612 let raw = RawQwpWsRoundStream {
613 endpoint_idx: connected.endpoint_idx,
614 stream: connected.stream,
615 leftover: connected.leftover,
616 max_buf_size,
617 request_timeout: *self.qwp_ws.request_timeout,
618 durable_ack_opt_in: *self.qwp_ws.request_durable_ack,
619 };
620 if let Some(events) = events
621 && let Some(endpoint) = self.endpoints.get(raw.endpoint_idx)
622 {
623 events.connect_succeeded(&endpoint.host, &endpoint.port);
626 }
627 Ok(raw)
628 }
629
630 pub(crate) fn connect_sfa_background_with_pool_slot(
631 &self,
632 sender_id: Option<&str>,
633 managed_exclusions: &[conf::QwpWsManagedSlotExclusion],
634 extra_orphan_slots: &[PathBuf],
635 conn_events: std::sync::Arc<conn_events::ConnectionEventSource>,
636 rejection_sink: std::sync::Arc<rejection_events::RejectionEventSource>,
637 force_async_initial_connect: bool,
638 ) -> Result<sender::qwp_ws::SyncQwpWsHandlerState> {
639 let mut qwp_ws = self.qwp_ws.clone();
640 if force_async_initial_connect {
644 qwp_ws.force_async_initial_connect();
645 }
646 qwp_ws.conn_events = Some(conn_events);
651 qwp_ws.rejection_sink = Some(rejection_sink);
652 configure_qwp_ws_pool_slot(
653 &mut qwp_ws,
654 sender_id,
655 managed_exclusions,
656 extra_orphan_slots,
657 )?;
658 sender::qwp_ws::connect_qwp_ws_background_state(
659 self.host.as_str(),
660 self.port.as_str(),
661 self.use_tls,
662 self.tls_settings.clone(),
663 &qwp_ws,
664 self.auth_header.clone(),
665 )
666 }
667}
668
669#[cfg(feature = "sync-sender-qwp-ws")]
670fn configure_qwp_ws_pool_slot(
671 qwp_ws: &mut conf::QwpWsConfig,
672 sender_id: Option<&str>,
673 managed_exclusions: &[conf::QwpWsManagedSlotExclusion],
674 extra_orphan_slots: &[PathBuf],
675) -> Result<()> {
676 if let Some(sender_id) = sender_id {
677 if !conf::is_valid_qwp_ws_sender_id(sender_id) {
678 return Err(error::fmt!(
679 ConfigError,
680 "invalid pool-managed sender_id [value={sender_id}, allowed-chars=[A-Za-z0-9_-]]"
681 ));
682 }
683 qwp_ws.sender_id = ConfigSetting::new_specified(sender_id.to_owned());
684 }
685 qwp_ws.orphan_exclude_managed_slots = managed_exclusions.to_vec();
686 qwp_ws.orphan_extra_slots = extra_orphan_slots.to_vec();
687 qwp_ws.pool_managed_slot = sender_id.is_some();
688 Ok(())
689}
690
691#[cfg(feature = "sync-sender-qwp-ws")]
695pub(crate) struct RawQwpWsRoundStream {
696 pub(crate) endpoint_idx: usize,
697 pub(crate) stream: sender::qwp_ws::WsStream,
698 pub(crate) leftover: Vec<u8>,
699 pub(crate) max_buf_size: usize,
700 pub(crate) request_timeout: Duration,
701 pub(crate) durable_ack_opt_in: bool,
702}
703
704#[cfg(any(feature = "_sender-qwp-ws", feature = "_egress"))]
713pub(crate) fn scan_qwp_ws_addr_params(conf: &str) -> Result<Option<QwpWsAddrScan>> {
714 let Some((service, params)) = conf.split_once("::") else {
715 return Ok(None);
716 };
717 if !service.eq_ignore_ascii_case("ws") && !service.eq_ignore_ascii_case("wss") {
718 return Ok(None);
719 }
720
721 let mut addr_values = Vec::new();
722 let mut sanitized_conf = String::with_capacity(conf.len());
723 sanitized_conf.push_str(service);
724 sanitized_conf.push_str("::");
725
726 let params_offset = service.len() + 2;
727 let mut pos = 0usize;
728 while pos < params.len() {
729 let param_start = pos;
730 let Some(eq_rel) = params[pos..].find('=') else {
731 return Ok(None);
732 };
733 let key_start = pos;
734 let key_end = pos + eq_rel;
735 let key = ¶ms[key_start..key_end];
736 pos = key_end + 1;
737
738 let mut value = String::new();
739 while pos < params.len() {
740 let rest = ¶ms[pos..];
741 let mut chars = rest.char_indices();
742 let (_, ch) = chars.next().expect("pos is within params");
743 if ch == ';' {
744 let next_pos = pos + ch.len_utf8();
745 if params[next_pos..].starts_with(';') {
746 value.push(';');
747 pos = next_pos + 1;
748 continue;
749 }
750 pos = next_pos;
751 break;
752 }
753 value.push(ch);
754 pos += ch.len_utf8();
755 }
756
757 let param_end = pos;
758 if key.eq_ignore_ascii_case("addr") {
759 if addr_values.is_empty() {
760 sanitized_conf
761 .push_str(&conf[params_offset + param_start..params_offset + param_end]);
762 }
763 addr_values.push(value);
764 } else {
765 sanitized_conf.push_str(&conf[params_offset + param_start..params_offset + param_end]);
766 }
767 }
768
769 Ok(Some(QwpWsAddrScan {
770 addr_values,
771 sanitized_conf,
772 }))
773}
774
775#[cfg(feature = "_sender-qwp-ws")]
776fn parse_qwp_ws_endpoints(
777 addr_values: &[String],
778 default_port: &str,
779) -> Result<Vec<conf::QwpWsEndpoint>> {
780 let mut endpoints = Vec::new();
781 let mut seen = HashSet::new();
782 for addr in addr_values {
783 for raw_entry in addr.split(',') {
784 let entry = raw_entry.trim();
785 if entry.is_empty() {
786 return Err(error::fmt!(
787 ConfigError,
788 "invalid QWP/WebSocket addr list: empty entry"
789 ));
790 }
791 let (host, port) = if let Some(rest) = entry.strip_prefix('[') {
792 let (host, after) = rest.split_once(']').ok_or_else(|| {
793 error::fmt!(
794 ConfigError,
795 "invalid QWP/WebSocket addr entry {:?}: missing ']'",
796 entry
797 )
798 })?;
799 let port = match after.strip_prefix(':') {
800 Some(port) => port.trim(),
801 None if after.is_empty() => default_port,
802 None => {
803 return Err(error::fmt!(
804 ConfigError,
805 "invalid QWP/WebSocket addr entry {:?}: \
806 expected ':port' after ']'",
807 entry
808 ));
809 }
810 };
811 (host.trim(), port)
812 } else if entry.matches(':').count() > 1 {
813 return Err(error::fmt!(
814 ConfigError,
815 "invalid QWP/WebSocket addr entry {:?}: bracket IPv6 \
816 addresses, e.g. [::1]:9000",
817 entry
818 ));
819 } else {
820 match entry.split_once(':') {
821 Some((host, port)) => (host.trim(), port.trim()),
822 None => (entry, default_port),
823 }
824 };
825 if host.is_empty() {
826 return Err(error::fmt!(
827 ConfigError,
828 "invalid QWP/WebSocket addr entry {:?}: empty host",
829 entry
830 ));
831 }
832 if port.is_empty() {
833 return Err(error::fmt!(
834 ConfigError,
835 "invalid QWP/WebSocket addr entry {:?}: empty port",
836 entry
837 ));
838 }
839 let parsed_port = port.parse::<u16>().map_err(|_| {
840 error::fmt!(
841 ConfigError,
842 "invalid QWP/WebSocket addr entry {:?}: invalid port {:?}",
843 entry,
844 port
845 )
846 })?;
847 if parsed_port == 0 {
848 return Err(error::fmt!(
849 ConfigError,
850 "invalid QWP/WebSocket addr entry {:?}: invalid port {:?}",
851 entry,
852 port
853 ));
854 }
855 let normalized_port = parsed_port.to_string();
856 let key = (host.to_string(), normalized_port.clone());
857 if !seen.insert(key.clone()) {
858 return Err(error::fmt!(
859 ConfigError,
860 "duplicate QWP/WebSocket addr endpoint {}:{}",
861 host,
862 normalized_port
863 ));
864 }
865 endpoints.push(conf::QwpWsEndpoint::new(key.0, key.1));
866 }
867 }
868 if endpoints.is_empty() {
869 return Err(error::fmt!(
870 ConfigError,
871 "Missing \"addr\" parameter in config string"
872 ));
873 }
874 Ok(endpoints)
875}
876
877#[derive(Debug, Clone)]
923#[cfg(feature = "_sync-sender")]
924pub struct SenderBuilder {
925 protocol: Protocol,
926 host: ConfigSetting<String>,
927 port: ConfigSetting<String>,
928 net_interface: ConfigSetting<Option<String>>,
929 init_buf_size: ConfigSetting<usize>,
930 max_buf_size: ConfigSetting<usize>,
931 max_name_len: ConfigSetting<usize>,
932 auth_timeout: ConfigSetting<Duration>,
933 username: ConfigSetting<Option<String>>,
934 password: ConfigSetting<Option<String>>,
935 token: ConfigSetting<Option<String>>,
936
937 #[cfg(feature = "_sender-tcp")]
938 token_x: ConfigSetting<Option<String>>,
939
940 #[cfg(feature = "_sender-tcp")]
941 token_y: ConfigSetting<Option<String>>,
942
943 protocol_version: ConfigSetting<Option<ProtocolVersion>>,
944
945 #[cfg(feature = "insecure-skip-verify")]
946 tls_verify: ConfigSetting<bool>,
947
948 tls_ca: ConfigSetting<CertificateAuthority>,
949 tls_roots: ConfigSetting<Option<PathBuf>>,
950
951 #[cfg(feature = "_sender-qwp-ws")]
955 tls_roots_password: ConfigSetting<Option<String>>,
956
957 #[cfg(feature = "_sender-http")]
958 http: Option<conf::HttpConfig>,
959
960 #[cfg(feature = "_sender-qwp-udp")]
961 qwp_udp: Option<conf::QwpUdpConfig>,
962
963 #[cfg(feature = "_sender-qwp-ws")]
964 qwp_ws: Option<conf::QwpWsConfig>,
965
966 #[cfg(feature = "_sender-qwp-ws")]
967 qwp_ws_error_handler: QwpWsErrorHandler,
968}
969
970#[cfg(feature = "_sync-sender")]
971impl SenderBuilder {
972 pub fn from_conf<T: AsRef<str>>(conf: T) -> Result<Self> {
1016 let conf = conf.as_ref();
1017 #[cfg(feature = "_sender-qwp-ws")]
1018 let qwp_ws_addr_scan = scan_qwp_ws_addr_params(conf)?;
1019 #[cfg(feature = "_sender-qwp-ws")]
1020 let conf_to_parse = qwp_ws_addr_scan
1021 .as_ref()
1022 .map(|scan| scan.sanitized_conf.as_str())
1023 .unwrap_or(conf);
1024 #[cfg(not(feature = "_sender-qwp-ws"))]
1025 let conf_to_parse = conf;
1026
1027 let conf = questdb_confstr::parse_conf_str(conf_to_parse)
1028 .map_err(|e| error::fmt!(ConfigError, "Config parse error: {}", e))?;
1029 let service = conf.service();
1030 let params = conf.params();
1031
1032 let protocol = Protocol::from_schema(service)?;
1033 #[cfg(feature = "_sender-qwp-ws")]
1034 let conf_is_qwp_ws = protocol.is_qwp_ws();
1035
1036 let Some(addr) = params.get("addr") else {
1037 return Err(error::fmt!(
1038 ConfigError,
1039 "Missing \"addr\" parameter in config string"
1040 ));
1041 };
1042 #[cfg(feature = "_sender-qwp-ws")]
1043 let qwp_ws_endpoints = if protocol.is_qwp_ws() {
1044 let addr_values = qwp_ws_addr_scan
1045 .as_ref()
1046 .map(|scan| scan.addr_values.as_slice())
1047 .unwrap_or_else(|| std::slice::from_ref(addr));
1048 Some(parse_qwp_ws_endpoints(
1049 addr_values,
1050 protocol.default_port(),
1051 )?)
1052 } else {
1053 None
1054 };
1055 let (host, port) = {
1056 #[cfg(feature = "_sender-qwp-ws")]
1057 if let Some(endpoints) = qwp_ws_endpoints.as_ref() {
1058 let first = endpoints.first().ok_or_else(|| {
1059 error::fmt!(ConfigError, "Missing \"addr\" parameter in config string")
1060 })?;
1061 (first.host.as_str(), first.port.as_str())
1062 } else {
1063 match addr.split_once(':') {
1064 Some((h, p)) => (h, p),
1065 None => (addr.as_str(), protocol.default_port()),
1066 }
1067 }
1068
1069 #[cfg(not(feature = "_sender-qwp-ws"))]
1070 {
1071 match addr.split_once(':') {
1072 Some((h, p)) => (h, p),
1073 None => (addr.as_str(), protocol.default_port()),
1074 }
1075 }
1076 };
1077 let mut builder = SenderBuilder::new(protocol, host, port);
1078 #[cfg(feature = "_sender-qwp-ws")]
1079 if let Some(endpoints) = qwp_ws_endpoints {
1080 builder = builder.qwp_ws_endpoints(endpoints)?;
1081 }
1082
1083 validate_auto_flush_params(params)?;
1084
1085 #[cfg(feature = "_sender-qwp-ws")]
1096 const QWP_WS_PORTABLE_CONFIG_KEYS: &[&str] = &[
1097 "addr",
1098 "auth",
1099 "auto_flush",
1100 "auto_flush_bytes",
1101 "auto_flush_interval",
1102 "auto_flush_rows",
1103 "buffer_pool_size",
1104 "client_id",
1105 "compression",
1106 "compression_level",
1107 "failover",
1108 "failover_backoff_initial_ms",
1109 "failover_backoff_max_ms",
1110 "failover_max_attempts",
1111 "failover_max_duration_ms",
1112 "max_batch_rows",
1113 "max_version",
1114 "on_internal_error",
1115 "on_parse_error",
1116 "on_schema_error",
1117 "on_security_error",
1118 "on_server_error",
1119 "on_write_error",
1120 "path",
1121 "acquire_timeout_ms",
1122 "idle_timeout_ms",
1123 "lazy_connect",
1124 "pool_reap",
1125 "query_pool_max",
1126 "query_pool_min",
1127 "sender_pool_max",
1128 "sender_pool_min",
1129 "target",
1130 "zone",
1131 ];
1132
1133 for (key, val) in params.iter().map(|(k, v)| (k.as_str(), v.as_str())) {
1134 builder = match key {
1135 "username" => builder.username(val)?,
1136 "password" => builder.password(val)?,
1137 "token" => builder.token(val)?,
1138 "token_x" => builder.token_x(val)?,
1139 "token_y" => builder.token_y(val)?,
1140 "bind_interface" => builder.bind_interface(val)?,
1141 #[cfg(feature = "_sender-qwp-udp")]
1142 "max_datagram_size" => builder.max_datagram_size(parse_conf_value(key, val)?)?,
1143 #[cfg(feature = "_sender-qwp-udp")]
1144 "multicast_ttl" => builder.multicast_ttl(parse_conf_value(key, val)?)?,
1145 #[cfg(feature = "_sender-qwp-ws")]
1146 "qwp_ws_progress" => builder.qwp_ws_progress(parse_qwp_ws_progress_value(val)?)?,
1147 #[cfg(feature = "_sender-qwp-ws")]
1148 "sf_dir" => builder.store_and_forward_dir(PathBuf::from(val))?,
1149 #[cfg(feature = "_sender-qwp-ws")]
1150 "sender_id" => builder.sender_id(val)?,
1151 #[cfg(feature = "_sender-qwp-ws")]
1152 "sf_max_segment_bytes" => {
1153 builder.store_and_forward_max_bytes(parse_size_conf_value(key, val)?)?
1154 }
1155 #[cfg(feature = "_sender-qwp-ws")]
1156 "sf_max_total_bytes" => {
1157 builder.store_and_forward_max_total_bytes(parse_size_conf_value(key, val)?)?
1158 }
1159 #[cfg(feature = "_sender-qwp-ws")]
1160 "sf_durability" => {
1161 builder.store_and_forward_durability(parse_sf_durability_value(val)?)?
1162 }
1163 #[cfg(feature = "_sender-qwp-ws")]
1164 "sf_sync_interval_millis" => builder.store_and_forward_sync_interval_millis(val)?,
1165 #[cfg(feature = "_sender-qwp-ws")]
1166 "sf_append_deadline_millis" => builder.store_and_forward_append_deadline(
1167 Duration::from_millis(parse_conf_value(key, val)?),
1168 )?,
1169 #[cfg(feature = "_sender-qwp-ws")]
1170 "reconnect_max_duration_millis" => builder
1171 .reconnect_max_duration(Duration::from_millis(parse_conf_value(key, val)?))?,
1172 #[cfg(feature = "_sender-qwp-ws")]
1173 "reconnect_initial_backoff_millis" => builder.reconnect_initial_backoff(
1174 Duration::from_millis(parse_conf_value(key, val)?),
1175 )?,
1176 #[cfg(feature = "_sender-qwp-ws")]
1177 "reconnect_max_backoff_millis" => builder
1178 .reconnect_max_backoff(Duration::from_millis(parse_conf_value(key, val)?))?,
1179 #[cfg(feature = "_sender-qwp-ws")]
1180 "initial_connect_retry" => {
1181 builder.qwp_ws_initial_connect_mode(parse_initial_connect_retry_value(val)?)?
1182 }
1183 #[cfg(feature = "_sender-qwp-ws")]
1184 "auth_timeout_ms" => builder.qwp_ws_auth_timeout_millis(val)?,
1185 #[cfg(feature = "_sender-qwp-ws")]
1186 "connect_timeout" => builder.qwp_ws_connect_timeout_millis(val)?,
1187 #[cfg(feature = "_sender-qwp-ws")]
1188 "close_flush_timeout_millis" => builder.close_flush_timeout_millis(val)?,
1189 #[cfg(feature = "_sender-qwp-ws")]
1190 "request_durable_ack" => builder.request_durable_ack(val)?,
1191 #[cfg(feature = "_sender-qwp-ws")]
1192 "durable_ack_keepalive_interval_millis" => {
1193 builder.durable_ack_keepalive_interval_millis(val)?
1194 }
1195 #[cfg(feature = "_sender-qwp-ws")]
1196 "drain_orphans" => builder.drain_orphans(val)?,
1197 #[cfg(feature = "_sender-qwp-ws")]
1198 "max_background_drainers" => builder.max_background_drainers(val)?,
1199 #[cfg(feature = "_sender-qwp-ws")]
1200 "error_inbox_capacity" => builder.error_inbox_capacity(val)?,
1201 #[cfg(feature = "_sender-qwp-ws")]
1202 "max_frame_rejections" => {
1203 builder.max_frame_rejections(parse_conf_value(key, val)?)?
1204 }
1205 #[cfg(feature = "_sender-qwp-ws")]
1206 "poison_min_escalation_window_millis" => builder.poison_min_escalation_window(
1207 Duration::from_millis(parse_conf_value(key, val)?),
1208 )?,
1209 "protocol_version" => match val {
1210 "1" => builder.protocol_version(ProtocolVersion::V1)?,
1211 "2" => builder.protocol_version(ProtocolVersion::V2)?,
1212 "3" => builder.protocol_version(ProtocolVersion::V3)?,
1213 "auto" => builder,
1214 invalid => {
1215 return Err(error::fmt!(
1216 ConfigError,
1217 "invalid \"protocol_version\" [value={invalid}, allowed-values=[auto, 1, 2, 3]]"
1218 ));
1219 }
1220 },
1221 "max_name_len" => builder.max_name_len(parse_conf_value(key, val)?)?,
1222
1223 "init_buf_size" => builder.init_buf_size(parse_conf_value(key, val)?)?,
1224
1225 "max_buf_size" => builder.max_buf_size(parse_conf_value(key, val)?)?,
1226
1227 "auth_timeout" => {
1228 builder.auth_timeout(Duration::from_millis(parse_conf_value(key, val)?))?
1229 }
1230
1231 "tls_verify" => {
1232 let verify = match val {
1233 "on" => true,
1234 "unsafe_off" => false,
1235 _ => {
1236 return Err(fmt!(
1237 ConfigError,
1238 r##"Config parameter "tls_verify" must be either "on" or "unsafe_off".'"##,
1239 ));
1240 }
1241 };
1242
1243 #[cfg(not(feature = "insecure-skip-verify"))]
1244 {
1245 if !verify {
1246 return Err(fmt!(
1247 ConfigError,
1248 r##"The "insecure-skip-verify" feature is not enabled, so "tls_verify=unsafe_off" is not supported"##,
1249 ));
1250 }
1251 builder
1252 }
1253
1254 #[cfg(feature = "insecure-skip-verify")]
1255 builder.tls_verify(verify)?
1256 }
1257
1258 "tls_ca" => {
1259 #[allow(unreachable_code, unused_variables)]
1260 {
1261 let ca = match val {
1262 #[cfg(feature = "tls-webpki-certs")]
1263 "webpki_roots" => CertificateAuthority::WebpkiRoots,
1264
1265 #[cfg(not(feature = "tls-webpki-certs"))]
1266 "webpki_roots" => {
1267 return Err(error::fmt!(
1268 ConfigError,
1269 "Config parameter \"tls_ca=webpki_roots\" requires the \"tls-webpki-certs\" feature"
1270 ));
1271 }
1272
1273 #[cfg(feature = "tls-native-certs")]
1274 "os_roots" => CertificateAuthority::OsRoots,
1275
1276 #[cfg(not(feature = "tls-native-certs"))]
1277 "os_roots" => {
1278 return Err(error::fmt!(
1279 ConfigError,
1280 "Config parameter \"tls_ca=os_roots\" requires the \"tls-native-certs\" feature"
1281 ));
1282 }
1283
1284 #[cfg(all(feature = "tls-webpki-certs", feature = "tls-native-certs"))]
1285 "webpki_and_os_roots" => CertificateAuthority::WebpkiAndOsRoots,
1286
1287 #[cfg(not(all(
1288 feature = "tls-webpki-certs",
1289 feature = "tls-native-certs"
1290 )))]
1291 "webpki_and_os_roots" => {
1292 return Err(error::fmt!(
1293 ConfigError,
1294 "Config parameter \"tls_ca=webpki_and_os_roots\" requires both the \"tls-webpki-certs\" and \"tls-native-certs\" features"
1295 ));
1296 }
1297
1298 _ => {
1299 return Err(error::fmt!(
1300 ConfigError,
1301 "Invalid value {val:?} for \"tls_ca\""
1302 ));
1303 }
1304 };
1305 builder.tls_ca(ca)?
1306 }
1307 }
1308
1309 "tls_roots" => {
1310 let path = PathBuf::from_str(val).map_err(|e| {
1311 error::fmt!(
1312 ConfigError,
1313 "Invalid path {:?} for \"tls_roots\": {}",
1314 val,
1315 e
1316 )
1317 })?;
1318 builder.tls_roots(path)?
1319 }
1320
1321 "tls_roots_password" => {
1322 #[cfg(feature = "_sender-qwp-ws")]
1323 {
1324 builder.tls_roots_password(val.to_string())?
1325 }
1326 #[cfg(not(feature = "_sender-qwp-ws"))]
1327 {
1328 return Err(error::fmt!(
1329 ConfigError,
1330 "\"tls_roots_password\" is only supported for QWP/WebSocket \
1331 (ws / wss). ILP/TCP and ILP/HTTP transports read \
1332 unencrypted PEM via rustls."
1333 ));
1334 }
1335 }
1336
1337 #[cfg(feature = "sync-sender-http")]
1338 "request_min_throughput" => {
1339 builder.request_min_throughput(parse_conf_value(key, val)?)?
1340 }
1341
1342 #[cfg(feature = "sync-sender-http")]
1343 "request_timeout" => {
1344 builder.request_timeout(Duration::from_millis(parse_conf_value(key, val)?))?
1345 }
1346
1347 #[cfg(feature = "sync-sender-http")]
1348 "retry_timeout" => {
1349 builder.retry_timeout(Duration::from_millis(parse_conf_value(key, val)?))?
1350 }
1351 #[cfg(feature = "sync-sender-http")]
1352 "retry_max_backoff_millis" => {
1353 builder.retry_max_backoff(Duration::from_millis(parse_conf_value(key, val)?))?
1354 }
1355
1356 #[cfg(feature = "_sender-qwp-ws")]
1362 other if conf_is_qwp_ws && !QWP_WS_PORTABLE_CONFIG_KEYS.contains(&other) => {
1363 return Err(error::fmt!(ConfigError, "Unknown config key \"{}\"", other));
1364 }
1365 _ => builder,
1366 };
1367 }
1368
1369 Ok(builder)
1370 }
1371
1372 pub fn from_env() -> Result<Self> {
1377 let conf = std::env::var("QDB_CLIENT_CONF").map_err(|_| {
1378 error::fmt!(ConfigError, "Environment variable QDB_CLIENT_CONF not set.")
1379 })?;
1380 Self::from_conf(conf)
1381 }
1382
1383 pub fn new<H: Into<String>, P: Into<Port>>(protocol: Protocol, host: H, port: P) -> Self {
1403 let host = host.into();
1404 let port: Port = port.into();
1405 let port = port.0;
1406
1407 #[cfg(feature = "tls-webpki-certs")]
1408 let tls_ca = CertificateAuthority::WebpkiRoots;
1409
1410 #[cfg(all(not(feature = "tls-webpki-certs"), feature = "tls-native-certs"))]
1411 let tls_ca = CertificateAuthority::OsRoots;
1412
1413 #[cfg(not(any(feature = "tls-webpki-certs", feature = "tls-native-certs")))]
1414 let tls_ca = CertificateAuthority::PemFile;
1415
1416 Self {
1417 protocol,
1418 host: ConfigSetting::new_specified(host),
1419 port: ConfigSetting::new_specified(port),
1420 net_interface: ConfigSetting::new_default(None),
1421 init_buf_size: ConfigSetting::new_default(64 * 1024),
1422 max_buf_size: ConfigSetting::new_default(100 * 1024 * 1024),
1423 max_name_len: ConfigSetting::new_default(MAX_NAME_LEN_DEFAULT),
1424 auth_timeout: ConfigSetting::new_default(Duration::from_secs(15)),
1425 username: ConfigSetting::new_default(None),
1426 password: ConfigSetting::new_default(None),
1427 token: ConfigSetting::new_default(None),
1428
1429 #[cfg(feature = "_sender-tcp")]
1430 token_x: ConfigSetting::new_default(None),
1431
1432 #[cfg(feature = "_sender-tcp")]
1433 token_y: ConfigSetting::new_default(None),
1434
1435 protocol_version: ConfigSetting::new_default(None),
1436
1437 #[cfg(feature = "insecure-skip-verify")]
1438 tls_verify: ConfigSetting::new_default(true),
1439
1440 tls_ca: ConfigSetting::new_default(tls_ca),
1441 tls_roots: ConfigSetting::new_default(None),
1442
1443 #[cfg(feature = "_sender-qwp-ws")]
1444 tls_roots_password: ConfigSetting::new_default(None),
1445
1446 #[cfg(feature = "sync-sender-http")]
1447 http: if protocol.is_httpx() {
1448 Some(conf::HttpConfig::default())
1449 } else {
1450 None
1451 },
1452
1453 #[cfg(feature = "_sender-qwp-udp")]
1454 qwp_udp: if protocol.is_qwp_udp() {
1455 Some(conf::QwpUdpConfig::default())
1456 } else {
1457 None
1458 },
1459
1460 #[cfg(feature = "_sender-qwp-ws")]
1461 qwp_ws: if protocol.is_qwp_ws() {
1462 Some(conf::QwpWsConfig::default())
1463 } else {
1464 None
1465 },
1466
1467 #[cfg(feature = "_sender-qwp-ws")]
1468 qwp_ws_error_handler: QwpWsErrorHandler::log_default(),
1469 }
1470 }
1471
1472 #[cfg(feature = "_sender-qwp-ws")]
1478 pub fn qwp_ws_error_handler<F>(mut self, handler: F) -> Result<Self>
1479 where
1480 F: Fn(&QwpWsSenderError) + Send + Sync + 'static,
1481 {
1482 self.qwp_ws_error_handler = QwpWsErrorHandler::new(handler);
1483 Ok(self)
1484 }
1485
1486 pub fn bind_interface<I: Into<String>>(self, addr: I) -> Result<Self> {
1492 #[cfg(any(feature = "_sender-tcp", feature = "_sender-qwp-udp"))]
1493 {
1494 let mut builder = self;
1495 builder.ensure_supports_bind_interface("bind_interface")?;
1496 builder
1497 .net_interface
1498 .set_specified("bind_interface", Some(validate_value(addr.into())?))?;
1499 Ok(builder)
1500 }
1501
1502 #[cfg(not(any(feature = "_sender-tcp", feature = "_sender-qwp-udp")))]
1503 {
1504 let _ = addr;
1505 Err(error::fmt!(
1506 ConfigError,
1507 "The \"bind_interface\" setting can only be used with the TCP protocol."
1508 ))
1509 }
1510 }
1511
1512 pub fn username(mut self, username: &str) -> Result<Self> {
1521 #[cfg(feature = "_sender-qwp-udp")]
1522 self.reject_if_qwp_udp("username")?;
1523 self.username
1524 .set_specified("username", Some(validate_value(username.to_string())?))?;
1525 Ok(self)
1526 }
1527
1528 pub fn password(mut self, password: &str) -> Result<Self> {
1531 #[cfg(feature = "_sender-qwp-udp")]
1532 self.reject_if_qwp_udp("password")?;
1533 self.password
1534 .set_specified("password", Some(validate_value(password.to_string())?))?;
1535 Ok(self)
1536 }
1537
1538 pub fn token(mut self, token: &str) -> Result<Self> {
1542 #[cfg(feature = "_sender-qwp-udp")]
1543 self.reject_if_qwp_udp("token")?;
1544 self.token
1545 .set_specified("token", Some(validate_value(token.to_string())?))?;
1546 Ok(self)
1547 }
1548
1549 pub fn token_x(self, token_x: &str) -> Result<Self> {
1551 #[cfg(feature = "_sender-qwp-udp")]
1552 self.reject_if_qwp_udp("token_x")?;
1553 #[cfg(feature = "_sender-tcp")]
1554 {
1555 let mut builder = self;
1556 builder
1557 .token_x
1558 .set_specified("token_x", Some(validate_value(token_x.to_string())?))?;
1559 Ok(builder)
1560 }
1561
1562 #[cfg(not(feature = "_sender-tcp"))]
1563 {
1564 let _ = token_x;
1565 Err(error::fmt!(
1566 ConfigError,
1567 "cannot specify \"token_x\": ECDSA authentication is only available with ILP/TCP and not available with ILP/HTTP."
1568 ))
1569 }
1570 }
1571
1572 pub fn token_y(self, token_y: &str) -> Result<Self> {
1574 #[cfg(feature = "_sender-qwp-udp")]
1575 self.reject_if_qwp_udp("token_y")?;
1576 #[cfg(feature = "_sender-tcp")]
1577 {
1578 let mut builder = self;
1579 builder
1580 .token_y
1581 .set_specified("token_y", Some(validate_value(token_y.to_string())?))?;
1582 Ok(builder)
1583 }
1584
1585 #[cfg(not(feature = "_sender-tcp"))]
1586 {
1587 let _ = token_y;
1588 Err(error::fmt!(
1589 ConfigError,
1590 "cannot specify \"token_y\": ECDSA authentication is only available with ILP/TCP and not available with ILP/HTTP."
1591 ))
1592 }
1593 }
1594
1595 pub fn protocol_version(mut self, protocol_version: ProtocolVersion) -> Result<Self> {
1605 #[cfg(feature = "_sender-qwp-udp")]
1606 self.reject_if_qwp_udp("protocol_version")?;
1607 self.protocol_version
1608 .set_specified("protocol_version", Some(protocol_version))?;
1609 Ok(self)
1610 }
1611
1612 #[cfg(feature = "_sender-qwp-udp")]
1613 pub fn max_datagram_size(mut self, value: usize) -> Result<Self> {
1624 if value == 0 {
1625 return Err(error::fmt!(
1626 ConfigError,
1627 "\"max_datagram_size\" must be greater than 0."
1628 ));
1629 }
1630 if value > 65507 {
1631 return Err(error::fmt!(
1632 ConfigError,
1633 "\"max_datagram_size\" must not exceed 65507 (UDP/IPv4 limit)."
1634 ));
1635 }
1636 let Some(qwp_udp) = &mut self.qwp_udp else {
1637 return Err(error::fmt!(
1638 ConfigError,
1639 "The \"max_datagram_size\" setting is only supported for QWP/UDP."
1640 ));
1641 };
1642 qwp_udp
1643 .max_datagram_size
1644 .set_specified("max_datagram_size", value)?;
1645 Ok(self)
1646 }
1647
1648 #[cfg(feature = "_sender-qwp-udp")]
1649 pub fn multicast_ttl(mut self, value: u32) -> Result<Self> {
1654 if value > 255 {
1655 return Err(error::fmt!(
1656 ConfigError,
1657 "\"multicast_ttl\" must be between 0 and 255."
1658 ));
1659 }
1660 let Some(qwp_udp) = &mut self.qwp_udp else {
1661 return Err(error::fmt!(
1662 ConfigError,
1663 "The \"multicast_ttl\" setting is only supported for QWP/UDP."
1664 ));
1665 };
1666 qwp_udp
1667 .multicast_ttl
1668 .set_specified("multicast_ttl", value)?;
1669 Ok(self)
1670 }
1671
1672 #[cfg(feature = "_sender-qwp-ws")]
1673 pub fn connection_listener(
1682 mut self,
1683 listener: crate::ingress::ConnectionListener,
1684 inbox_capacity: usize,
1685 ) -> Result<Self> {
1686 let Some(qwp_ws) = &mut self.qwp_ws else {
1687 return Err(error::fmt!(
1688 ConfigError,
1689 "The \"connection_listener\" setting is only supported for QWP/WebSocket."
1690 ));
1691 };
1692 if qwp_ws.conn_events.is_some() {
1693 return Err(error::fmt!(
1694 ConfigError,
1695 "A connection listener is already registered on this builder."
1696 ));
1697 }
1698 qwp_ws.conn_events = Some(std::sync::Arc::new(
1699 conn_events::ConnectionEventSource::new(listener, inbox_capacity),
1700 ));
1701 Ok(self)
1702 }
1703
1704 #[cfg(feature = "_sender-qwp-ws")]
1705 pub fn qwp_ws_progress(mut self, progress: QwpWsProgress) -> Result<Self> {
1709 let Some(qwp_ws) = &mut self.qwp_ws else {
1710 return Err(error::fmt!(
1711 ConfigError,
1712 "The \"qwp_ws_progress\" setting is only supported for QWP/WebSocket."
1713 ));
1714 };
1715 qwp_ws.progress.set_specified("qwp_ws_progress", progress)?;
1716 Ok(self)
1717 }
1718
1719 #[cfg(feature = "_sender-qwp-ws")]
1720 fn store_and_forward_dir(mut self, dir: PathBuf) -> Result<Self> {
1721 if dir.as_os_str().is_empty() {
1722 return Err(error::fmt!(ConfigError, "\"sf_dir\" cannot be empty."));
1723 }
1724 let Some(qwp_ws) = &mut self.qwp_ws else {
1725 return Err(error::fmt!(
1726 ConfigError,
1727 "The \"sf_dir\" setting is only supported for QWP/WebSocket."
1728 ));
1729 };
1730 qwp_ws.sf_dir.set_specified("sf_dir", Some(dir))?;
1731 Ok(self)
1732 }
1733
1734 #[cfg(feature = "_sender-qwp-ws")]
1735 fn sender_id(mut self, sender_id: &str) -> Result<Self> {
1736 let sender_id = validate_value(sender_id)?;
1737 if !conf::is_valid_qwp_ws_sender_id(sender_id) {
1738 return Err(error::fmt!(
1739 ConfigError,
1740 "invalid sender_id [value={sender_id}, allowed-chars=[A-Za-z0-9_-]]"
1741 ));
1742 }
1743 let Some(qwp_ws) = &mut self.qwp_ws else {
1744 return Err(error::fmt!(
1745 ConfigError,
1746 "The \"sender_id\" setting is only supported for QWP/WebSocket."
1747 ));
1748 };
1749 qwp_ws
1750 .sender_id
1751 .set_specified("sender_id", sender_id.to_owned())?;
1752 Ok(self)
1753 }
1754
1755 #[cfg(feature = "_sender-qwp-ws")]
1756 fn store_and_forward_max_bytes(mut self, value: u64) -> Result<Self> {
1757 if value == 0 {
1758 return Err(error::fmt!(
1759 ConfigError,
1760 "\"sf_max_segment_bytes\" must be greater than 0."
1761 ));
1762 }
1763 let Some(qwp_ws) = &mut self.qwp_ws else {
1764 return Err(error::fmt!(
1765 ConfigError,
1766 "The \"sf_max_segment_bytes\" setting is only supported for QWP/WebSocket."
1767 ));
1768 };
1769 qwp_ws
1770 .sf_max_segment_bytes
1771 .set_specified("sf_max_segment_bytes", value)?;
1772 Ok(self)
1773 }
1774
1775 #[cfg(feature = "_sender-qwp-ws")]
1776 fn store_and_forward_max_total_bytes(mut self, value: u64) -> Result<Self> {
1777 if value == 0 {
1778 return Err(error::fmt!(
1779 ConfigError,
1780 "\"sf_max_total_bytes\" must be greater than 0."
1781 ));
1782 }
1783 let Some(qwp_ws) = &mut self.qwp_ws else {
1784 return Err(error::fmt!(
1785 ConfigError,
1786 "The \"sf_max_total_bytes\" setting is only supported for QWP/WebSocket."
1787 ));
1788 };
1789 qwp_ws
1790 .sf_max_total_bytes
1791 .set_specified("sf_max_total_bytes", Some(value))?;
1792 Ok(self)
1793 }
1794
1795 #[cfg(feature = "_sender-qwp-ws")]
1796 fn store_and_forward_durability(mut self, durability: conf::SfDurability) -> Result<Self> {
1797 let Some(qwp_ws) = &mut self.qwp_ws else {
1798 return Err(error::fmt!(
1799 ConfigError,
1800 "The \"sf_durability\" setting is only supported for QWP/WebSocket."
1801 ));
1802 };
1803 qwp_ws
1804 .sf_durability
1805 .set_specified("sf_durability", durability)?;
1806 Ok(self)
1807 }
1808
1809 #[cfg(feature = "_sender-qwp-ws")]
1810 fn store_and_forward_sync_interval_millis(mut self, value: &str) -> Result<Self> {
1811 const MAX_MILLIS: i64 = i64::MAX / 1_000_000;
1812
1813 let Some(qwp_ws) = &mut self.qwp_ws else {
1814 return Err(error::fmt!(
1815 ConfigError,
1816 "The \"sf_sync_interval_millis\" setting is only supported for QWP/WebSocket."
1817 ));
1818 };
1819 let millis: i64 = parse_conf_value("sf_sync_interval_millis", value)?;
1820 if millis <= 0 {
1821 return Err(error::fmt!(
1822 ConfigError,
1823 "\"sf_sync_interval_millis\" must be greater than 0."
1824 ));
1825 }
1826 if millis > MAX_MILLIS {
1827 return Err(error::fmt!(
1828 ConfigError,
1829 "\"sf_sync_interval_millis\" must be at most {MAX_MILLIS}."
1830 ));
1831 }
1832 qwp_ws.sf_sync_interval.set_specified(
1833 "sf_sync_interval_millis",
1834 Some(Duration::from_millis(millis as u64)),
1835 )?;
1836 Ok(self)
1837 }
1838
1839 #[cfg(feature = "_sender-qwp-ws")]
1840 fn store_and_forward_append_deadline(mut self, value: Duration) -> Result<Self> {
1841 if value.is_zero() {
1842 return Err(error::fmt!(
1843 ConfigError,
1844 "\"sf_append_deadline_millis\" must be greater than 0."
1845 ));
1846 }
1847 let Some(qwp_ws) = &mut self.qwp_ws else {
1848 return Err(error::fmt!(
1849 ConfigError,
1850 "The \"sf_append_deadline_millis\" setting is only supported for QWP/WebSocket."
1851 ));
1852 };
1853 qwp_ws
1854 .sf_append_deadline
1855 .set_specified("sf_append_deadline_millis", value)?;
1856 Ok(self)
1857 }
1858
1859 #[cfg(feature = "_sender-qwp-ws")]
1860 pub fn reconnect_max_duration(mut self, value: Duration) -> Result<Self> {
1862 let Some(qwp_ws) = &mut self.qwp_ws else {
1863 return Err(error::fmt!(
1864 ConfigError,
1865 "The \"reconnect_max_duration_millis\" setting is only supported for QWP/WebSocket."
1866 ));
1867 };
1868 qwp_ws
1869 .reconnect_max_duration
1870 .set_specified("reconnect_max_duration_millis", value)?;
1871 Ok(self)
1872 }
1873
1874 #[cfg(feature = "_sender-qwp-ws")]
1875 pub fn max_frame_rejections(mut self, value: usize) -> Result<Self> {
1879 if value == 0 {
1880 return Err(error::fmt!(
1881 ConfigError,
1882 "\"max_frame_rejections\" must be greater than 0."
1883 ));
1884 }
1885 let Some(qwp_ws) = &mut self.qwp_ws else {
1886 return Err(error::fmt!(
1887 ConfigError,
1888 "The \"max_frame_rejections\" setting is only supported for QWP/WebSocket."
1889 ));
1890 };
1891 qwp_ws
1892 .max_frame_rejections
1893 .set_specified("max_frame_rejections", value)?;
1894 Ok(self)
1895 }
1896
1897 #[cfg(feature = "_sender-qwp-ws")]
1898 pub fn poison_min_escalation_window(mut self, value: Duration) -> Result<Self> {
1902 let Some(qwp_ws) = &mut self.qwp_ws else {
1903 return Err(error::fmt!(
1904 ConfigError,
1905 "The \"poison_min_escalation_window_millis\" setting is only supported for QWP/WebSocket."
1906 ));
1907 };
1908 qwp_ws
1909 .poison_min_escalation_window
1910 .set_specified("poison_min_escalation_window_millis", value)?;
1911 Ok(self)
1912 }
1913
1914 #[cfg(feature = "_sender-qwp-ws")]
1915 pub fn reconnect_initial_backoff(mut self, value: Duration) -> Result<Self> {
1917 if value.is_zero() {
1918 return Err(error::fmt!(
1919 ConfigError,
1920 "\"reconnect_initial_backoff_millis\" must be greater than 0."
1921 ));
1922 }
1923 let Some(qwp_ws) = &mut self.qwp_ws else {
1924 return Err(error::fmt!(
1925 ConfigError,
1926 "The \"reconnect_initial_backoff_millis\" setting is only supported for QWP/WebSocket."
1927 ));
1928 };
1929 qwp_ws
1930 .reconnect_initial_backoff
1931 .set_specified("reconnect_initial_backoff_millis", value)?;
1932 Ok(self)
1933 }
1934
1935 #[cfg(feature = "_sender-qwp-ws")]
1936 pub fn reconnect_max_backoff(mut self, value: Duration) -> Result<Self> {
1939 if value.is_zero() {
1940 return Err(error::fmt!(
1941 ConfigError,
1942 "\"reconnect_max_backoff_millis\" must be greater than 0."
1943 ));
1944 }
1945 let Some(qwp_ws) = &mut self.qwp_ws else {
1946 return Err(error::fmt!(
1947 ConfigError,
1948 "The \"reconnect_max_backoff_millis\" setting is only supported for QWP/WebSocket."
1949 ));
1950 };
1951 qwp_ws
1952 .reconnect_max_backoff
1953 .set_specified("reconnect_max_backoff_millis", value)?;
1954 Ok(self)
1955 }
1956
1957 #[cfg(feature = "_sender-qwp-ws")]
1958 fn qwp_ws_endpoints(mut self, endpoints: Vec<conf::QwpWsEndpoint>) -> Result<Self> {
1959 let Some(qwp_ws) = &mut self.qwp_ws else {
1960 return Err(error::fmt!(
1961 ConfigError,
1962 "QWP/WebSocket endpoint lists are only supported for QWP/WebSocket."
1963 ));
1964 };
1965 qwp_ws.endpoints.set_specified("addr", endpoints)?;
1966 Ok(self)
1967 }
1968
1969 #[cfg(feature = "_sender-qwp-ws")]
1970 fn qwp_ws_initial_connect_mode(mut self, mode: conf::QwpWsInitialConnectMode) -> Result<Self> {
1971 let Some(qwp_ws) = &mut self.qwp_ws else {
1972 return Err(error::fmt!(
1973 ConfigError,
1974 "The \"initial_connect_retry\" setting is only supported for QWP/WebSocket."
1975 ));
1976 };
1977 qwp_ws
1978 .initial_connect_retry
1979 .set_specified("initial_connect_retry", mode)?;
1980 Ok(self)
1981 }
1982
1983 #[cfg(feature = "_sender-qwp-ws")]
1984 pub fn initial_connect_retry(mut self, value: bool) -> Result<Self> {
1993 let Some(qwp_ws) = &mut self.qwp_ws else {
1994 return Err(error::fmt!(
1995 ConfigError,
1996 "The \"initial_connect_retry\" setting is only supported for QWP/WebSocket."
1997 ));
1998 };
1999 qwp_ws.initial_connect_retry.set_specified(
2000 "initial_connect_retry",
2001 if value {
2002 conf::QwpWsInitialConnectMode::Sync
2003 } else {
2004 conf::QwpWsInitialConnectMode::Off
2005 },
2006 )?;
2007 Ok(self)
2008 }
2009
2010 #[cfg(feature = "_sender-qwp-ws")]
2011 fn qwp_ws_auth_timeout_millis(mut self, value: &str) -> Result<Self> {
2012 let Some(qwp_ws) = &mut self.qwp_ws else {
2013 return Err(error::fmt!(
2014 ConfigError,
2015 "The \"auth_timeout_ms\" setting is only supported for QWP/WebSocket."
2016 ));
2017 };
2018 let millis: i64 = parse_conf_value("auth_timeout_ms", value)?;
2019 if millis <= 0 {
2020 return Err(error::fmt!(
2021 ConfigError,
2022 "auth_timeout_ms must be > 0: {}",
2023 millis
2024 ));
2025 }
2026 qwp_ws
2027 .auth_timeout
2028 .set_specified("auth_timeout_ms", Duration::from_millis(millis as u64))?;
2029 Ok(self)
2030 }
2031
2032 #[cfg(feature = "_sender-qwp-ws")]
2033 fn qwp_ws_connect_timeout_millis(mut self, value: &str) -> Result<Self> {
2034 let Some(qwp_ws) = &mut self.qwp_ws else {
2035 return Err(error::fmt!(
2036 ConfigError,
2037 "The \"connect_timeout\" setting is only supported for QWP/WebSocket."
2038 ));
2039 };
2040 let millis: i64 = parse_conf_value("connect_timeout", value)?;
2041 if millis <= 0 {
2042 return Err(error::fmt!(
2043 ConfigError,
2044 "connect_timeout must be > 0: {}",
2045 millis
2046 ));
2047 }
2048 qwp_ws.connect_timeout.set_specified(
2049 "connect_timeout",
2050 Some(Duration::from_millis(millis as u64)),
2051 )?;
2052 Ok(self)
2053 }
2054
2055 #[cfg(feature = "_sender-qwp-ws")]
2056 fn close_flush_timeout_millis(mut self, value: &str) -> Result<Self> {
2057 let Some(qwp_ws) = &mut self.qwp_ws else {
2058 return Err(error::fmt!(
2059 ConfigError,
2060 "The \"close_flush_timeout_millis\" setting is only supported for QWP/WebSocket."
2061 ));
2062 };
2063 let millis: i64 = parse_conf_value("close_flush_timeout_millis", value)?;
2064 let timeout = if millis <= 0 {
2065 Duration::ZERO
2066 } else {
2067 Duration::from_millis(millis as u64)
2068 };
2069 qwp_ws
2070 .close_flush_timeout
2071 .set_specified("close_flush_timeout_millis", timeout)?;
2072 Ok(self)
2073 }
2074
2075 #[cfg(feature = "_sender-qwp-ws")]
2076 fn request_durable_ack(mut self, value: &str) -> Result<Self> {
2077 let Some(qwp_ws) = &mut self.qwp_ws else {
2078 return Err(error::fmt!(
2079 ConfigError,
2080 "The \"request_durable_ack\" setting is only supported for QWP/WebSocket."
2081 ));
2082 };
2083 if value.eq_ignore_ascii_case("off") {
2084 qwp_ws
2085 .request_durable_ack
2086 .set_specified("request_durable_ack", false)?;
2087 return Ok(self);
2088 }
2089 if value.eq_ignore_ascii_case("on") {
2090 qwp_ws
2091 .request_durable_ack
2092 .set_specified("request_durable_ack", true)?;
2093 return Ok(self);
2094 }
2095
2096 Err(error::fmt!(
2097 ConfigError,
2098 "invalid request_durable_ack [value={value}, allowed-values=[on, off]]"
2099 ))
2100 }
2101
2102 #[cfg(feature = "_sender-qwp-ws")]
2103 fn drain_orphans(mut self, value: &str) -> Result<Self> {
2104 let Some(qwp_ws) = &mut self.qwp_ws else {
2105 return Err(error::fmt!(
2106 ConfigError,
2107 "The \"drain_orphans\" setting is only supported for QWP/WebSocket."
2108 ));
2109 };
2110 if value.eq_ignore_ascii_case("off") || value.eq_ignore_ascii_case("false") {
2111 qwp_ws.drain_orphans.set_specified("drain_orphans", false)?;
2112 return Ok(self);
2113 }
2114 if value.eq_ignore_ascii_case("on") || value.eq_ignore_ascii_case("true") {
2115 qwp_ws.drain_orphans.set_specified("drain_orphans", true)?;
2116 return Ok(self);
2117 }
2118
2119 Err(error::fmt!(
2120 ConfigError,
2121 "invalid drain_orphans [value={value}, allowed-values=[on, off, true, false]]"
2122 ))
2123 }
2124
2125 #[cfg(feature = "_sender-qwp-ws")]
2126 fn durable_ack_keepalive_interval_millis(mut self, value: &str) -> Result<Self> {
2127 let Some(qwp_ws) = &mut self.qwp_ws else {
2128 return Err(error::fmt!(
2129 ConfigError,
2130 "The \"durable_ack_keepalive_interval_millis\" setting is only supported for QWP/WebSocket."
2131 ));
2132 };
2133 let millis: i64 = parse_conf_value("durable_ack_keepalive_interval_millis", value)?;
2134 let interval = if millis <= 0 {
2135 Duration::ZERO
2136 } else {
2137 Duration::from_millis(millis as u64)
2138 };
2139 qwp_ws
2140 .durable_ack_keepalive_interval
2141 .set_specified("durable_ack_keepalive_interval_millis", interval)?;
2142 Ok(self)
2143 }
2144
2145 #[cfg(feature = "_sender-qwp-ws")]
2146 fn max_background_drainers(mut self, value: &str) -> Result<Self> {
2147 let Some(qwp_ws) = &mut self.qwp_ws else {
2148 return Err(error::fmt!(
2149 ConfigError,
2150 "The \"max_background_drainers\" setting is only supported for QWP/WebSocket."
2151 ));
2152 };
2153 let value: i32 = parse_conf_value("max_background_drainers", value)?;
2154 if value < 0 {
2155 return Err(error::fmt!(
2156 ConfigError,
2157 "max_background_drainers must be >= 0: {value}"
2158 ));
2159 }
2160 qwp_ws
2161 .max_background_drainers
2162 .set_specified("max_background_drainers", value as usize)?;
2163 Ok(self)
2164 }
2165
2166 #[cfg(feature = "_sender-qwp-ws")]
2167 fn error_inbox_capacity(mut self, value: &str) -> Result<Self> {
2168 let Some(qwp_ws) = &mut self.qwp_ws else {
2169 return Err(error::fmt!(
2170 ConfigError,
2171 "The \"error_inbox_capacity\" setting is only supported for QWP/WebSocket."
2172 ));
2173 };
2174 let value: usize = parse_conf_value("error_inbox_capacity", value)?;
2175 if value < conf::QWP_WS_MIN_ERROR_INBOX_CAPACITY {
2176 return Err(error::fmt!(
2177 ConfigError,
2178 "error_inbox_capacity must be >= {}: {value}",
2179 conf::QWP_WS_MIN_ERROR_INBOX_CAPACITY
2180 ));
2181 }
2182 qwp_ws
2183 .error_inbox_capacity
2184 .set_specified("error_inbox_capacity", value)?;
2185 Ok(self)
2186 }
2187
2188 pub fn auth_timeout(mut self, value: Duration) -> Result<Self> {
2192 #[cfg(feature = "_sender-qwp-udp")]
2193 self.reject_if_qwp_udp("auth_timeout")?;
2194 #[cfg(feature = "_sender-qwp-ws")]
2195 if let Some(qwp_ws) = &mut self.qwp_ws {
2196 if value.is_zero() {
2197 return Err(error::fmt!(
2198 ConfigError,
2199 "\"auth_timeout\" must be greater than 0."
2200 ));
2201 }
2202 qwp_ws.auth_timeout.set_specified("auth_timeout", value)?;
2203 return Ok(self);
2204 }
2205 self.auth_timeout.set_specified("auth_timeout", value)?;
2206 Ok(self)
2207 }
2208
2209 #[cfg(feature = "_sender-qwp-udp")]
2210 fn reject_if_qwp_udp(&self, setting: &str) -> Result<()> {
2211 if self.protocol.is_qwp_udp() {
2212 return Err(error::fmt!(
2213 ConfigError,
2214 "The \"{setting}\" setting is not supported for QWP/UDP."
2215 ));
2216 }
2217 Ok(())
2218 }
2219
2220 pub fn ensure_tls_enabled(&self, property: &str) -> Result<()> {
2222 if !self.protocol.tls_enabled() {
2223 return Err(error::fmt!(
2224 ConfigError,
2225 "Cannot set {property:?}: TLS is not supported for protocol {}",
2226 self.protocol
2227 ));
2228 }
2229 Ok(())
2230 }
2231
2232 #[cfg(feature = "insecure-skip-verify")]
2238 pub fn tls_verify(mut self, verify: bool) -> Result<Self> {
2239 self.ensure_tls_enabled("tls_verify")?;
2240 self.tls_verify.set_specified("tls_verify", verify)?;
2241 Ok(self)
2242 }
2243
2244 pub fn tls_ca(mut self, ca: CertificateAuthority) -> Result<Self> {
2247 self.ensure_tls_enabled("tls_ca")?;
2248 self.tls_ca.set_specified("tls_ca", ca)?;
2249 Ok(self)
2250 }
2251
2252 pub fn tls_roots<P: Into<PathBuf>>(self, path: P) -> Result<Self> {
2263 let mut builder = self.tls_ca(CertificateAuthority::PemFile)?;
2264 let path = path.into();
2265 let _file = std::fs::File::open(&path).map_err(|io_err| {
2267 error::fmt!(
2268 ConfigError,
2269 "Could not open root certificate file from path {:?}: {}",
2270 path,
2271 io_err
2272 )
2273 })?;
2274 builder.tls_roots.set_specified("tls_roots", Some(path))?;
2275 Ok(builder)
2276 }
2277
2278 #[cfg(feature = "_sender-qwp-ws")]
2288 pub fn tls_roots_password<S: Into<String>>(mut self, password: S) -> Result<Self> {
2289 if !self.protocol.is_qwp_ws() {
2290 return Err(error::fmt!(
2291 ConfigError,
2292 "\"tls_roots_password\" is only supported for QWP/WebSocket \
2293 (ws / wss). ILP/TCP and ILP/HTTP transports read \
2294 unencrypted PEM via rustls."
2295 ));
2296 }
2297 self.ensure_tls_enabled("tls_roots_password")?;
2298 self.tls_roots_password
2299 .set_specified("tls_roots_password", Some(password.into()))?;
2300 Ok(self)
2301 }
2302
2303 pub fn init_buf_size(mut self, value: usize) -> Result<Self> {
2315 let min = 1024;
2316 if value < min {
2317 return Err(error::fmt!(
2318 ConfigError,
2319 "\"init_buf_size\" must be at least {min} bytes."
2320 ));
2321 }
2322 self.init_buf_size.set_specified("init_buf_size", value)?;
2323 Ok(self)
2324 }
2325
2326 pub fn max_buf_size(mut self, value: usize) -> Result<Self> {
2333 let min = 1024;
2334 if value < min {
2335 return Err(error::fmt!(
2336 ConfigError,
2337 "max_buf_size\" must be at least {min} bytes."
2338 ));
2339 }
2340 self.max_buf_size.set_specified("max_buf_size", value)?;
2341 Ok(self)
2342 }
2343
2344 pub fn max_name_len(mut self, value: usize) -> Result<Self> {
2350 if value < 16 {
2351 return Err(error::fmt!(
2352 ConfigError,
2353 "max_name_len must be at least 16 bytes."
2354 ));
2355 }
2356 self.max_name_len.set_specified("max_name_len", value)?;
2357 Ok(self)
2358 }
2359
2360 #[cfg(feature = "sync-sender-qwp-ws")]
2362 pub(crate) fn configured_max_name_len(&self) -> usize {
2363 *self.max_name_len
2364 }
2365
2366 #[cfg(feature = "sync-sender-http")]
2367 pub fn retry_timeout(mut self, value: Duration) -> Result<Self> {
2370 if let Some(http) = &mut self.http {
2371 http.retry_timeout.set_specified("retry_timeout", value)?;
2372 } else {
2373 return Err(error::fmt!(
2374 ConfigError,
2375 "retry_timeout is supported only in ILP over HTTP."
2376 ));
2377 }
2378 Ok(self)
2379 }
2380
2381 #[cfg(feature = "sync-sender-http")]
2382 pub fn retry_max_backoff(mut self, value: Duration) -> Result<Self> {
2393 if value < Duration::from_millis(10) {
2394 return Err(error::fmt!(
2395 ConfigError,
2396 "\"retry_max_backoff_millis\" must be at least 10."
2397 ));
2398 }
2399 if let Some(http) = &mut self.http {
2400 http.retry_max_backoff
2401 .set_specified("retry_max_backoff_millis", value)?;
2402 } else {
2403 return Err(error::fmt!(
2404 ConfigError,
2405 "retry_max_backoff_millis is supported only in ILP over HTTP."
2406 ));
2407 }
2408 Ok(self)
2409 }
2410
2411 #[cfg(feature = "sync-sender-http")]
2412 pub fn request_min_throughput(mut self, value: u64) -> Result<Self> {
2422 if let Some(http) = &mut self.http {
2423 http.request_min_throughput
2424 .set_specified("request_min_throughput", value)?;
2425 } else {
2426 return Err(error::fmt!(
2427 ConfigError,
2428 "\"request_min_throughput\" is supported only in ILP over HTTP."
2429 ));
2430 }
2431 Ok(self)
2432 }
2433
2434 #[cfg(feature = "sync-sender-http")]
2435 pub fn request_timeout(mut self, value: Duration) -> Result<Self> {
2440 if let Some(http) = &mut self.http {
2441 if value.is_zero() {
2442 return Err(error::fmt!(
2443 ConfigError,
2444 "\"request_timeout\" must be greater than 0."
2445 ));
2446 }
2447 http.request_timeout
2448 .set_specified("request_timeout", value)?;
2449 } else {
2450 return Err(error::fmt!(
2451 ConfigError,
2452 "\"request_timeout\" is supported only in ILP over HTTP."
2453 ));
2454 }
2455 Ok(self)
2456 }
2457
2458 #[cfg(feature = "sync-sender-http")]
2459 #[doc(hidden)]
2463 pub fn user_agent(mut self, value: &str) -> Result<Self> {
2464 let value = validate_value(value)?;
2465 if let Some(http) = &mut self.http {
2466 http.user_agent = value.to_string();
2467 }
2468 Ok(self)
2469 }
2470
2471 fn build_auth(&self) -> Result<Option<conf::AuthParams>> {
2472 match (
2473 self.protocol,
2474 self.username.deref(),
2475 self.password.deref(),
2476 self.token.deref(),
2477 #[cfg(feature = "_sender-tcp")]
2478 self.token_x.deref(),
2479 #[cfg(not(feature = "_sender-tcp"))]
2480 None::<String>,
2481 #[cfg(feature = "_sender-tcp")]
2482 self.token_y.deref(),
2483 #[cfg(not(feature = "_sender-tcp"))]
2484 None::<String>,
2485 ) {
2486 (_, None, None, None, None, None) => Ok(None),
2487
2488 #[cfg(feature = "_sender-tcp")]
2489 (protocol, Some(username), None, Some(token), Some(token_x), Some(token_y))
2490 if protocol.is_tcpx() =>
2491 {
2492 Ok(Some(conf::AuthParams::Ecdsa(conf::EcdsaAuthParams {
2493 key_id: username.to_string(),
2494 priv_key: token.to_string(),
2495 pub_key_x: token_x.to_string(),
2496 pub_key_y: token_y.to_string(),
2497 })))
2498 }
2499
2500 #[cfg(feature = "_sender-tcp")]
2501 (protocol, Some(_username), Some(_password), None, None, None)
2502 if protocol.is_tcpx() =>
2503 {
2504 Err(error::fmt!(
2505 ConfigError,
2506 r##"The "basic_auth" setting can only be used with the ILP/HTTP protocol."##,
2507 ))
2508 }
2509
2510 #[cfg(feature = "_sender-tcp")]
2511 (protocol, None, None, Some(_token), None, None) if protocol.is_tcpx() => {
2512 Err(error::fmt!(
2513 ConfigError,
2514 "Token authentication only be used with the ILP/HTTP protocol."
2515 ))
2516 }
2517
2518 #[cfg(feature = "_sender-tcp")]
2519 (protocol, _username, None, _token, _token_x, _token_y) if protocol.is_tcpx() => {
2520 Err(error::fmt!(
2521 ConfigError,
2522 r##"Incomplete ECDSA authentication parameters. Specify either all or none of: "username", "token", "token_x", "token_y"."##,
2523 ))
2524 }
2525 #[cfg(any(feature = "_sender-http", feature = "_sender-qwp-ws"))]
2526 (protocol, Some(username), Some(password), None, None, None)
2527 if protocol.accepts_http_auth() =>
2528 {
2529 Ok(Some(conf::AuthParams::Basic(conf::BasicAuthParams {
2530 username: username.to_string(),
2531 password: password.to_string(),
2532 })))
2533 }
2534 #[cfg(any(feature = "_sender-http", feature = "_sender-qwp-ws"))]
2535 (protocol, Some(_username), None, None, None, None) if protocol.accepts_http_auth() => {
2536 Err(error::fmt!(
2537 ConfigError,
2538 r##"Basic authentication parameter "username" is present, but "password" is missing."##,
2539 ))
2540 }
2541 #[cfg(any(feature = "_sender-http", feature = "_sender-qwp-ws"))]
2542 (protocol, None, Some(_password), None, None, None) if protocol.accepts_http_auth() => {
2543 Err(error::fmt!(
2544 ConfigError,
2545 r##"Basic authentication parameter "password" is present, but "username" is missing."##,
2546 ))
2547 }
2548 #[cfg(any(feature = "_sender-http", feature = "_sender-qwp-ws"))]
2549 (protocol, None, None, Some(token), None, None) if protocol.accepts_http_auth() => {
2550 Ok(Some(conf::AuthParams::Token(conf::TokenAuthParams {
2551 token: token.to_string(),
2552 })))
2553 }
2554 #[cfg(feature = "_sender-http")]
2555 (protocol, Some(_username), None, Some(_token), Some(_token_x), Some(_token_y))
2556 if protocol.is_httpx() =>
2557 {
2558 Err(error::fmt!(
2559 ConfigError,
2560 "ECDSA authentication is only available with ILP/TCP and not available with ILP/HTTP."
2561 ))
2562 }
2563 #[cfg(feature = "_sender-http")]
2564 (protocol, _username, _password, _token, None, None) if protocol.is_httpx() => {
2565 Err(error::fmt!(
2566 ConfigError,
2567 r##"Inconsistent HTTP authentication parameters. Specify either "username" and "password", or just "token"."##,
2568 ))
2569 }
2570 _ => Err(error::fmt!(
2571 ConfigError,
2572 r##"Incomplete authentication parameters. Check "username", "password", "token", "token_x" and "token_y" parameters are set correctly."##,
2573 )),
2574 }
2575 }
2576
2577 #[cfg(feature = "_sync-sender")]
2578 pub fn build(&self) -> Result<Sender> {
2585 if self.init_buf_size.is_specified() && *self.init_buf_size > *self.max_buf_size {
2590 return Err(error::fmt!(
2591 ConfigError,
2592 "init_buf_size ({}) cannot exceed max_buf_size ({})",
2593 *self.init_buf_size,
2594 *self.max_buf_size
2595 ));
2596 }
2597
2598 let mut descr = format!("Sender[host={:?},port={:?},", self.host, self.port);
2599
2600 if self.protocol.tls_enabled() {
2601 write!(descr, "tls=enabled,").unwrap();
2602 } else {
2603 write!(descr, "tls=disabled,").unwrap();
2604 }
2605
2606 #[cfg(feature = "insecure-skip-verify")]
2607 let tls_verify = *self.tls_verify;
2608
2609 #[cfg(feature = "_sender-qwp-ws")]
2610 let tls_roots_password = self.tls_roots_password.deref().as_deref();
2611 #[cfg(not(feature = "_sender-qwp-ws"))]
2612 let tls_roots_password: Option<&str> = None;
2613
2614 if tls_roots_password.is_some() && self.tls_roots.deref().is_none() {
2619 return Err(error::fmt!(
2620 ConfigError,
2621 "\"tls_roots_password\" requires \"tls_roots\" \
2622 (the password unlocks the keystore at that path)"
2623 ));
2624 }
2625
2626 #[allow(unused_variables)]
2627 let tls_settings = tls::TlsSettings::build(
2628 self.protocol.tls_enabled(),
2629 #[cfg(feature = "insecure-skip-verify")]
2630 tls_verify,
2631 *self.tls_ca,
2632 self.tls_roots.deref().as_deref(),
2633 tls_roots_password,
2634 )?;
2635
2636 let auth = self.build_auth()?;
2637
2638 let handler = match self.protocol {
2639 #[cfg(feature = "sync-sender-tcp")]
2640 Protocol::Tcp | Protocol::Tcps => connect_tcp(
2641 self.host.as_str(),
2642 self.port.as_str(),
2643 self.net_interface.deref().as_deref(),
2644 *self.auth_timeout,
2645 tls_settings,
2646 &auth,
2647 )?,
2648 #[cfg(feature = "sync-sender-http")]
2649 Protocol::Http | Protocol::Https => {
2650 use ureq::unversioned::transport::Connector;
2651 use ureq::unversioned::transport::TcpConnector;
2652 if self.net_interface.is_some() {
2653 return Err(error::fmt!(
2655 InvalidApiCall,
2656 "net_interface is not supported for ILP over HTTP."
2657 ));
2658 }
2659
2660 let http_config = self.http.as_ref().unwrap();
2661 let user_agent = http_config.user_agent.as_str();
2662 let connector = TcpConnector::default();
2663
2664 let agent_builder = ureq::Agent::config_builder()
2665 .user_agent(user_agent)
2666 .no_delay(true);
2667
2668 let tls_config = match tls_settings {
2669 Some(tls_settings) => Some(tls::configure_tls(tls_settings)?),
2670 None => None,
2671 };
2672
2673 let connector = connector.chain(TlsConnector::new(tls_config));
2674
2675 let auth = match auth {
2676 Some(conf::AuthParams::Basic(ref auth)) => Some(auth.to_header_string()),
2677 Some(conf::AuthParams::Token(ref auth)) => Some(auth.to_header_string()?),
2678
2679 #[cfg(feature = "sync-sender-tcp")]
2680 Some(conf::AuthParams::Ecdsa(_)) => {
2681 return Err(fmt!(
2682 AuthError,
2683 "ECDSA authentication is not supported for ILP over HTTP. \
2684 Please use basic or token authentication instead."
2685 ));
2686 }
2687 None => None,
2688 };
2689 let agent_builder = agent_builder
2690 .timeout_connect(Some(*http_config.request_timeout.deref()))
2691 .http_status_as_error(false);
2692 let agent = ureq::Agent::with_parts(
2693 agent_builder.build(),
2694 connector,
2695 ureq::unversioned::resolver::DefaultResolver::default(),
2696 );
2697 let proto = self.protocol.schema();
2698 let url = format!(
2699 "{}://{}:{}/write",
2700 proto,
2701 self.host.deref(),
2702 self.port.deref()
2703 );
2704 SyncProtocolHandler::SyncHttp(SyncHttpHandlerState {
2705 agent,
2706 url,
2707 auth,
2708 config: self.http.as_ref().unwrap().clone(),
2709 })
2710 }
2711 #[cfg(feature = "sync-sender-qwp-udp")]
2712 Protocol::Udp => {
2713 let Some(qwp_udp) = self.qwp_udp.as_ref() else {
2714 return Err(error::fmt!(
2715 ConfigError,
2716 "QWP/UDP configuration is missing."
2717 ));
2718 };
2719 connect_qwp_udp(
2720 self.host.as_str(),
2721 self.port.as_str(),
2722 self.net_interface.deref().as_deref(),
2723 qwp_udp,
2724 )?
2725 }
2726 #[cfg(feature = "sync-sender-qwp-ws")]
2727 Protocol::Ws | Protocol::Wss => {
2728 if self.net_interface.is_some() {
2729 return Err(error::fmt!(
2730 InvalidApiCall,
2731 "net_interface is not supported for QWP over WebSocket."
2732 ));
2733 }
2734 let Some(qwp_ws) = self.qwp_ws.as_ref() else {
2735 return Err(error::fmt!(
2736 ConfigError,
2737 "QWP/WebSocket configuration is missing."
2738 ));
2739 };
2740 let actual_initial_connect_retry = qwp_ws.resolve_initial_connect_retry();
2745 let mut qwp_ws = qwp_ws.clone();
2746 qwp_ws.initial_connect_retry =
2747 ConfigSetting::Specified(actual_initial_connect_retry);
2748 let qwp_ws = &qwp_ws;
2749 reject_unsupported_qwp_ws_sf_config(qwp_ws)?;
2750 let basic_auth = qwp_ws_auth_header(&auth)?;
2751 if *qwp_ws.progress == QwpWsProgress::Manual {
2752 if *qwp_ws.initial_connect_retry == conf::QwpWsInitialConnectMode::Async {
2753 return Err(error::fmt!(
2754 ConfigError,
2755 "initial_connect_retry=async requires QWP/WebSocket background progress; use qwp_ws_progress=background or initial_connect_retry=sync"
2756 ));
2757 }
2758 SyncProtocolHandler::ManualQwpWs(Box::new(open_manual_qwp_ws(
2759 self.host.as_str(),
2760 self.port.as_str(),
2761 matches!(self.protocol, Protocol::Wss),
2762 tls_settings,
2763 qwp_ws,
2764 basic_auth,
2765 )?))
2766 } else {
2767 connect_qwp_ws(
2768 self.host.as_str(),
2769 self.port.as_str(),
2770 matches!(self.protocol, Protocol::Wss),
2771 tls_settings,
2772 qwp_ws,
2773 basic_auth,
2774 )?
2775 }
2776 }
2777 };
2778
2779 #[allow(unused_mut)]
2780 let mut max_name_len = *self.max_name_len;
2781
2782 let protocol_version = match self.protocol_version.deref() {
2783 Some(v) => *v,
2784 None => match self.protocol {
2785 #[cfg(feature = "sync-sender-tcp")]
2786 Protocol::Tcp | Protocol::Tcps => ProtocolVersion::V1,
2787 #[cfg(feature = "sync-sender-http")]
2788 Protocol::Http | Protocol::Https => {
2789 #[allow(irrefutable_let_patterns)]
2790 if let SyncProtocolHandler::SyncHttp(http_state) = &handler {
2791 let settings_url = &format!(
2792 "{}://{}:{}/settings",
2793 self.protocol.schema(),
2794 self.host.deref(),
2795 self.port.deref()
2796 );
2797 let (protocol_versions, server_max_name_len) =
2798 read_server_settings(http_state, settings_url, max_name_len)?;
2799 max_name_len = server_max_name_len;
2800 SUPPORTED_PROTOCOL_VERSIONS
2801 .iter()
2802 .find(|version| protocol_versions.contains(version))
2803 .copied()
2804 .ok_or_else(|| {
2805 fmt!(
2806 ProtocolVersionError,
2807 "Server does not support any of the client protocol versions: {:?}",
2808 SUPPORTED_PROTOCOL_VERSIONS
2809 )
2810 })?
2811 } else {
2812 unreachable!("HTTP handler should be used for HTTP protocol");
2813 }
2814 }
2815 #[cfg(feature = "sync-sender-qwp-udp")]
2816 Protocol::Udp => ProtocolVersion::V1,
2817 #[cfg(feature = "sync-sender-qwp-ws")]
2818 Protocol::Ws | Protocol::Wss => ProtocolVersion::V1,
2819 },
2820 };
2821
2822 if auth.is_some() {
2823 descr.push_str("auth=on]");
2824 } else {
2825 descr.push_str("auth=off]");
2826 }
2827
2828 let effective_init_buf_size = (*self.init_buf_size).min(*self.max_buf_size);
2832
2833 let sender = Sender::new(
2834 descr,
2835 handler,
2836 effective_init_buf_size,
2837 *self.max_buf_size,
2838 self.protocol,
2839 protocol_version,
2840 max_name_len,
2841 #[cfg(feature = "_sender-qwp-ws")]
2842 self.qwp_ws_error_handler.clone(),
2843 #[cfg(feature = "_sender-qwp-ws")]
2844 self.qwp_ws
2845 .as_ref()
2846 .and_then(|qwp_ws| qwp_ws.conn_events.clone()),
2847 );
2848
2849 Ok(sender)
2850 }
2851
2852 #[cfg(feature = "sync-sender-qwp-ws")]
2857 fn resolve_qwp_ws_ingredients(
2858 &self,
2859 ) -> Result<(
2860 bool,
2861 Option<tls::TlsSettings>,
2862 conf::QwpWsConfig,
2863 Option<String>,
2864 )> {
2865 if self.init_buf_size.is_specified() && *self.init_buf_size > *self.max_buf_size {
2866 return Err(error::fmt!(
2867 ConfigError,
2868 "init_buf_size ({}) cannot exceed max_buf_size ({})",
2869 *self.init_buf_size,
2870 *self.max_buf_size
2871 ));
2872 }
2873
2874 if !matches!(self.protocol, Protocol::Ws | Protocol::Wss) {
2875 return Err(error::fmt!(
2876 ConfigError,
2877 "Column-sender requires a QWP/WebSocket connect string \
2878 (got protocol {:?})",
2879 self.protocol
2880 ));
2881 }
2882 if self.net_interface.is_some() {
2883 return Err(error::fmt!(
2884 InvalidApiCall,
2885 "net_interface is not supported for QWP over WebSocket."
2886 ));
2887 }
2888 let Some(qwp_ws) = self.qwp_ws.as_ref() else {
2889 return Err(error::fmt!(
2890 ConfigError,
2891 "QWP/WebSocket configuration is missing."
2892 ));
2893 };
2894
2895 #[cfg(feature = "insecure-skip-verify")]
2896 let tls_verify = *self.tls_verify;
2897 let tls_roots_password = self.tls_roots_password.deref().as_deref();
2898
2899 if tls_roots_password.is_some() && self.tls_roots.deref().is_none() {
2900 return Err(error::fmt!(
2901 ConfigError,
2902 "\"tls_roots_password\" requires \"tls_roots\" \
2903 (the password unlocks the keystore at that path)"
2904 ));
2905 }
2906
2907 let tls_settings = tls::TlsSettings::build(
2908 self.protocol.tls_enabled(),
2909 #[cfg(feature = "insecure-skip-verify")]
2910 tls_verify,
2911 *self.tls_ca,
2912 self.tls_roots.deref().as_deref(),
2913 tls_roots_password,
2914 )?;
2915
2916 let auth = self.build_auth()?;
2917 let auth_header = qwp_ws_auth_header(&auth)?;
2918 let qwp_ws = qwp_ws.clone();
2919 reject_unsupported_qwp_ws_sf_config(&qwp_ws)?;
2920 if *qwp_ws.progress == QwpWsProgress::Manual
2921 && *qwp_ws.initial_connect_retry == conf::QwpWsInitialConnectMode::Async
2922 {
2923 return Err(error::fmt!(
2924 ConfigError,
2925 "initial_connect_retry=async requires QWP/WebSocket background progress; use qwp_ws_progress=background or initial_connect_retry=sync"
2926 ));
2927 }
2928
2929 let use_tls = matches!(self.protocol, Protocol::Wss);
2930 Ok((use_tls, tls_settings, qwp_ws, auth_header))
2931 }
2932
2933 #[cfg(feature = "sync-sender-qwp-ws")]
2935 pub(crate) fn force_async_initial_connect(&mut self) {
2936 if let Some(qwp_ws) = self.qwp_ws.as_mut() {
2937 qwp_ws.force_async_initial_connect();
2938 }
2939 }
2940
2941 #[cfg(feature = "sync-sender-qwp-ws")]
2949 pub(crate) fn build_qwp_ws_connector(&self) -> Result<QwpWsConnector> {
2950 let (use_tls, tls_settings, qwp_ws, auth_header) = self.resolve_qwp_ws_ingredients()?;
2951 let endpoints = sender::qwp_ws::qwp_ws_configured_endpoints(
2952 self.host.as_str(),
2953 self.port.as_str(),
2954 &qwp_ws,
2955 );
2956 Ok(QwpWsConnector {
2957 host: self.host.to_string(),
2958 port: self.port.to_string(),
2959 endpoints,
2960 use_tls,
2961 tls_settings,
2962 qwp_ws,
2963 auth_header,
2964 max_buf_size: *self.max_buf_size,
2965 })
2966 }
2967
2968 #[cfg(any(feature = "_sender-tcp", feature = "_sender-qwp-udp"))]
2969 fn ensure_supports_bind_interface(&self, param_name: &str) -> Result<()> {
2970 #[cfg(feature = "_sender-tcp")]
2971 if self.protocol.is_tcpx() {
2972 return Ok(());
2973 }
2974
2975 #[cfg(feature = "_sender-qwp-udp")]
2976 if self.protocol.is_qwp_udp() {
2977 return Ok(());
2978 }
2979
2980 #[cfg(feature = "_sender-qwp-udp")]
2981 let supported = "TCP or QWP/UDP";
2982 #[cfg(not(feature = "_sender-qwp-udp"))]
2983 let supported = "TCP";
2984
2985 Err(fmt!(
2986 ConfigError,
2987 "The {param_name:?} setting can only be used with the {supported} protocol."
2988 ))
2989 }
2990}
2991
2992#[cfg(feature = "_sync-sender")]
2995fn validate_value<T: AsRef<str>>(value: T) -> Result<T> {
2996 let str_ref = value.as_ref();
2997 for (p, c) in str_ref.chars().enumerate() {
2998 if matches!(c, '\u{0}'..='\u{1f}' | '\u{7f}'..='\u{9f}') {
2999 return Err(error::fmt!(
3000 ConfigError,
3001 "Invalid character {c:?} at position {p}"
3002 ));
3003 }
3004 }
3005 Ok(value)
3006}
3007
3008#[cfg(feature = "_sync-sender")]
3009fn parse_conf_value<T>(param_name: &str, str_value: &str) -> Result<T>
3010where
3011 T: FromStr,
3012 T::Err: std::fmt::Debug,
3013{
3014 str_value.parse().map_err(|e| {
3015 fmt!(
3016 ConfigError,
3017 "Could not parse {param_name:?} to number: {e:?}"
3018 )
3019 })
3020}
3021
3022#[cfg(feature = "_sender-qwp-ws")]
3027pub(crate) fn initial_connect_retry_value_is_blocking(str_value: &str) -> bool {
3028 matches!(
3029 parse_initial_connect_retry_value(str_value),
3030 Ok(mode) if mode != conf::QwpWsInitialConnectMode::Async
3031 )
3032}
3033
3034#[cfg(feature = "_sender-qwp-ws")]
3035fn parse_initial_connect_retry_value(str_value: &str) -> Result<conf::QwpWsInitialConnectMode> {
3036 if str_value.eq_ignore_ascii_case("on") || str_value.eq_ignore_ascii_case("true") {
3037 return Ok(conf::QwpWsInitialConnectMode::Sync);
3038 }
3039 if str_value.eq_ignore_ascii_case("sync") {
3040 return Ok(conf::QwpWsInitialConnectMode::Sync);
3041 }
3042 if str_value.eq_ignore_ascii_case("off") || str_value.eq_ignore_ascii_case("false") {
3043 return Ok(conf::QwpWsInitialConnectMode::Off);
3044 }
3045 if str_value.eq_ignore_ascii_case("async") {
3046 return Ok(conf::QwpWsInitialConnectMode::Async);
3047 }
3048 Err(error::fmt!(
3049 ConfigError,
3050 "invalid initial_connect_retry [value={str_value}, allowed-values=[on, off, true, false, sync, async]]"
3051 ))
3052}
3053
3054#[cfg(feature = "_sender-qwp-ws")]
3055fn parse_size_conf_value(param_name: &str, str_value: &str) -> Result<u64> {
3056 let mut end = str_value.len();
3057 if end == 0 {
3058 return Err(error::fmt!(
3059 ConfigError,
3060 "invalid {param_name} [value={str_value}]"
3061 ));
3062 }
3063
3064 let bytes = str_value.as_bytes();
3065 if matches!(bytes[end - 1], b'b' | b'B') {
3066 end -= 1;
3067 }
3068
3069 let multiplier = if end > 0 {
3070 match bytes[end - 1] {
3071 b'k' | b'K' => {
3072 end -= 1;
3073 1024
3074 }
3075 b'm' | b'M' => {
3076 end -= 1;
3077 1024 * 1024
3078 }
3079 b'g' | b'G' => {
3080 end -= 1;
3081 1024 * 1024 * 1024
3082 }
3083 b't' | b'T' => {
3084 end -= 1;
3085 1024_u64 * 1024 * 1024 * 1024
3086 }
3087 _ => 1,
3088 }
3089 } else {
3090 1
3091 };
3092
3093 if end == 0 {
3094 return Err(error::fmt!(
3095 ConfigError,
3096 "invalid {param_name} [value={str_value}]"
3097 ));
3098 }
3099
3100 let digits = &str_value[..end];
3101 let value = digits
3102 .parse::<u64>()
3103 .map_err(|_| error::fmt!(ConfigError, "invalid {param_name} [value={str_value}]"))?;
3104 value.checked_mul(multiplier).ok_or_else(|| {
3105 error::fmt!(
3106 ConfigError,
3107 "{param_name} overflows u64 [value={str_value}]"
3108 )
3109 })
3110}
3111
3112#[cfg(feature = "_sender-qwp-ws")]
3113fn parse_sf_durability_value(str_value: &str) -> Result<conf::SfDurability> {
3114 if str_value.eq_ignore_ascii_case("memory") {
3115 return Ok(conf::SfDurability::Memory);
3116 }
3117 if str_value.eq_ignore_ascii_case("periodic") {
3118 return Ok(conf::SfDurability::Periodic);
3119 }
3120 if str_value.eq_ignore_ascii_case("flush") {
3121 return Ok(conf::SfDurability::Flush);
3122 }
3123 if str_value.eq_ignore_ascii_case("append") {
3124 return Ok(conf::SfDurability::Append);
3125 }
3126 Err(error::fmt!(
3127 ConfigError,
3128 "invalid sf_durability [value={str_value}, allowed-values=[memory, periodic, flush, append]]"
3129 ))
3130}
3131
3132#[cfg(feature = "_sender-qwp-ws")]
3133fn parse_qwp_ws_progress_value(str_value: &str) -> Result<QwpWsProgress> {
3134 if str_value.eq_ignore_ascii_case("background") {
3135 return Ok(QwpWsProgress::Background);
3136 }
3137 if str_value.eq_ignore_ascii_case("manual") {
3138 return Ok(QwpWsProgress::Manual);
3139 }
3140 Err(error::fmt!(
3141 ConfigError,
3142 "invalid qwp_ws_progress [value={str_value}, allowed-values=[background, manual]]"
3143 ))
3144}
3145
3146#[cfg(feature = "_sender-qwp-ws")]
3147fn reject_unsupported_qwp_ws_sf_config(qwp_ws: &conf::QwpWsConfig) -> Result<()> {
3148 if matches!(
3149 *qwp_ws.sf_durability,
3150 conf::SfDurability::Flush | conf::SfDurability::Append
3151 ) {
3152 let durability = qwp_ws.sf_durability.as_conf_value();
3153 return Err(error::fmt!(
3154 ConfigError,
3155 "sf_durability={durability} is not yet supported (use sf_durability=memory or periodic)"
3156 ));
3157 }
3158 if *qwp_ws.sf_durability == conf::SfDurability::Periodic && qwp_ws.sf_dir.is_none() {
3159 return Err(error::fmt!(
3160 ConfigError,
3161 "sf_durability=periodic requires sf_dir"
3162 ));
3163 }
3164 if qwp_ws.sf_sync_interval.is_specified()
3165 && *qwp_ws.sf_durability != conf::SfDurability::Periodic
3166 {
3167 return Err(error::fmt!(
3168 ConfigError,
3169 "sf_sync_interval_millis requires sf_durability=periodic"
3170 ));
3171 }
3172
3173 Ok(())
3174}
3175
3176#[cfg(feature = "sync-sender-qwp-ws")]
3177fn qwp_ws_auth_header(auth: &Option<conf::AuthParams>) -> Result<Option<String>> {
3178 match auth {
3179 Some(conf::AuthParams::Basic(b)) => Ok(Some(b.to_header_string())),
3180 Some(conf::AuthParams::Token(t)) => Ok(Some(t.to_header_string()?)),
3181 #[cfg(feature = "_sender-tcp")]
3182 Some(conf::AuthParams::Ecdsa(_)) => Err(error::fmt!(
3183 AuthError,
3184 "ECDSA authentication is not supported for QWP/WebSocket. \
3185 Use basic or token authentication instead."
3186 )),
3187 None => Ok(None),
3188 }
3189}
3190
3191#[cfg(feature = "_sender-tcp")]
3192fn b64_decode(descr: &'static str, buf: &str) -> Result<Vec<u8>> {
3193 use base64ct::{Base64UrlUnpadded, Encoding};
3194 Base64UrlUnpadded::decode_vec(buf).map_err(|b64_err| {
3195 fmt!(
3196 AuthError,
3197 "Misconfigured ILP authentication keys. Could not decode {}: {}. \
3198 Hint: Check the keys for a possible typo.",
3199 descr,
3200 b64_err
3201 )
3202 })
3203}
3204
3205#[cfg(feature = "_sender-tcp")]
3206fn parse_public_key(pub_key_x: &str, pub_key_y: &str) -> Result<Vec<u8>> {
3207 let mut pub_key_x = b64_decode("public key x", pub_key_x)?;
3208 let mut pub_key_y = b64_decode("public key y", pub_key_y)?;
3209
3210 let mut encoded = Vec::new();
3212 encoded.push(4u8); let pub_key_x_ken = pub_key_x.len();
3214 if pub_key_x_ken > 32 {
3215 return Err(fmt!(
3216 AuthError,
3217 "Misconfigured ILP authentication keys. Public key x is too long. \
3218 Hint: Check the keys for a possible typo."
3219 ));
3220 }
3221 let pub_key_y_len = pub_key_y.len();
3222 if pub_key_y_len > 32 {
3223 return Err(fmt!(
3224 AuthError,
3225 "Misconfigured ILP authentication keys. Public key y is too long. \
3226 Hint: Check the keys for a possible typo."
3227 ));
3228 }
3229 encoded.resize((32 - pub_key_x_ken) + 1, 0u8);
3230 encoded.append(&mut pub_key_x);
3231 encoded.resize((32 - pub_key_y_len) + 1 + 32, 0u8);
3232 encoded.append(&mut pub_key_y);
3233 Ok(encoded)
3234}
3235
3236#[cfg(feature = "_sender-tcp")]
3237fn parse_key_pair(auth: &conf::EcdsaAuthParams) -> Result<EcdsaKeyPair> {
3238 let private_key = b64_decode("private authentication key", auth.priv_key.as_str())?;
3239 let public_key = parse_public_key(auth.pub_key_x.as_str(), auth.pub_key_y.as_str())?;
3240
3241 #[cfg(feature = "aws-lc-crypto")]
3242 let res = EcdsaKeyPair::from_private_key_and_public_key(
3243 &ECDSA_P256_SHA256_FIXED_SIGNING,
3244 &private_key[..],
3245 &public_key[..],
3246 );
3247
3248 #[cfg(feature = "ring-crypto")]
3249 let res = {
3250 let system_random = SystemRandom::new();
3251 EcdsaKeyPair::from_private_key_and_public_key(
3252 &ECDSA_P256_SHA256_FIXED_SIGNING,
3253 &private_key[..],
3254 &public_key[..],
3255 &system_random,
3256 )
3257 };
3258
3259 res.map_err(|key_rejected| {
3260 fmt!(
3261 AuthError,
3262 "Misconfigured ILP authentication keys: {}. Hint: Check the keys for a possible typo.",
3263 key_rejected
3264 )
3265 })
3266}
3267
3268struct DebugBytes<'a>(pub &'a [u8]);
3269
3270impl Debug for DebugBytes<'_> {
3271 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
3272 write!(f, "b\"")?;
3273
3274 for &byte in self.0 {
3275 match byte {
3276 0x20..=0x21 | 0x23..=0x5B | 0x5D..=0x7E => {
3278 write!(f, "{}", byte as char)?;
3279 }
3280 b'\n' => write!(f, "\\n")?,
3282 b'\r' => write!(f, "\\r")?,
3283 b'\t' => write!(f, "\\t")?,
3284 b'\\' => write!(f, "\\\\")?,
3285 b'"' => write!(f, "\\\"")?,
3286 b'\0' => write!(f, "\\0")?,
3287 _ => write!(f, "\\x{byte:02x}")?,
3289 }
3290 }
3291
3292 write!(f, "\"")
3293 }
3294}
3295
3296#[cfg(all(test, feature = "_sync-sender"))]
3297mod tests;