#![cfg_attr(feature = "_sync-sender", doc = include_str!("ingress/mod.md"))]
#![cfg_attr(
not(feature = "_sync-sender"),
doc = "Shared data types used by the egress reader. Enable a `sync-sender-*` \
feature to expose the sender APIs and their full module documentation."
)]
#[cfg(feature = "_sender-qwp-ws")]
pub(crate) use self::conf::QwpWsManagedSlotExclusion;
pub use self::ndarr::{ArrayElement, NdArrayView};
pub use self::timestamp::*;
use crate::error::Result;
#[cfg(feature = "_sync-sender")]
use crate::error::{self, fmt};
#[cfg(feature = "_sync-sender")]
use crate::ingress::conf::ConfigSetting;
#[cfg(feature = "_sync-sender")]
use core::time::Duration;
#[cfg(feature = "_sync-sender")]
use std::collections::HashMap;
#[cfg(feature = "_sender-qwp-ws")]
use std::collections::HashSet;
#[cfg(feature = "_sync-sender")]
use std::fmt::Write;
use std::fmt::{Debug, Display, Formatter};
#[cfg(feature = "_sync-sender")]
use std::ops::Deref;
#[cfg(feature = "_sender-qwp-ws")]
use std::path::Path;
#[cfg(feature = "_sync-sender")]
use std::path::PathBuf;
#[cfg(feature = "_sync-sender")]
use std::str::FromStr;
#[cfg(feature = "_sync-sender")]
mod tls;
#[cfg(all(feature = "_sender-tcp", feature = "aws-lc-crypto"))]
use aws_lc_rs::signature::{ECDSA_P256_SHA256_FIXED_SIGNING, EcdsaKeyPair};
#[cfg(all(feature = "_sender-tcp", feature = "ring-crypto"))]
use ring::{
rand::SystemRandom,
signature::{ECDSA_P256_SHA256_FIXED_SIGNING, EcdsaKeyPair},
};
#[cfg(feature = "_sync-sender")]
mod conf;
#[cfg(feature = "_sender-qwp-ws")]
pub mod conn_events;
#[cfg(feature = "_sender-qwp-ws")]
pub use conn_events::{
ConnectionEvent, ConnectionEventDispatcher, ConnectionEventKind, ConnectionListener,
};
#[cfg(feature = "_sender-qwp-ws")]
pub(crate) mod rejection_events;
pub(crate) mod ndarr;
mod timestamp;
mod buffer;
pub use buffer::*;
#[cfg(feature = "_sync-sender")]
pub(crate) mod sender;
#[cfg(feature = "_sender-qwp-ws")]
pub(crate) use sender::QwpWsRoleReject;
#[cfg(feature = "polars-ingress")]
pub(crate) use sender::ReconnectPolicy;
#[cfg(feature = "sync-sender-qwp-ws")]
pub(crate) use sender::ReconnectReason;
#[cfg(feature = "_sync-sender")]
pub use sender::*;
#[cfg(feature = "sync-sender-qwp-ws")]
pub(crate) use sender::{reconnect_backoff_step, reconnect_error_is_terminal};
mod decimal;
pub use decimal::DecimalView;
#[cfg(feature = "sync-sender-qwp-ws")]
pub mod column_sender;
#[cfg(feature = "sync-sender-qwp-ws")]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum AckLevel {
#[default]
Ok,
Durable,
}
#[cfg(feature = "sync-sender-qwp-ws")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TimestampUnit {
Micros,
Nanos,
}
#[cfg(feature = "polars-ingress")]
pub mod polars;
const MAX_NAME_LEN_DEFAULT: usize = 127;
pub const MAX_ARRAY_DIMS: usize = 32;
pub 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;
pub(crate) const ARRAY_BINARY_FORMAT_TYPE: u8 = 14;
pub(crate) const DOUBLE_BINARY_FORMAT_TYPE: u8 = 16;
pub const DECIMAL_BINARY_FORMAT_TYPE: u8 = 23;
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
pub enum ProtocolVersion {
V1 = 1,
V2 = 2,
V3 = 3,
}
#[cfg(feature = "_sender-http")]
const SUPPORTED_PROTOCOL_VERSIONS: [ProtocolVersion; 3] = [
ProtocolVersion::V3,
ProtocolVersion::V2,
ProtocolVersion::V1,
];
impl Display for ProtocolVersion {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ProtocolVersion::V1 => write!(f, "v1"),
ProtocolVersion::V2 => write!(f, "v2"),
ProtocolVersion::V3 => write!(f, "v3"),
}
}
}
#[cfg(feature = "_sender-tcp")]
fn map_io_to_socket_err(prefix: &str, io_err: std::io::Error) -> error::Error {
fmt!(SocketError, "{}{}", prefix, io_err)
}
#[derive(PartialEq, Debug, Clone, Copy)]
pub enum CertificateAuthority {
#[cfg(feature = "tls-webpki-certs")]
WebpkiRoots,
#[cfg(feature = "tls-native-certs")]
OsRoots,
#[cfg(all(feature = "tls-webpki-certs", feature = "tls-native-certs"))]
WebpkiAndOsRoots,
PemFile,
}
#[cfg(feature = "_sync-sender")]
pub struct Port(String);
#[cfg(feature = "_sync-sender")]
impl From<String> for Port {
fn from(s: String) -> Self {
Port(s)
}
}
#[cfg(feature = "_sync-sender")]
impl From<&str> for Port {
fn from(s: &str) -> Self {
Port(s.to_owned())
}
}
#[cfg(feature = "_sync-sender")]
impl From<u16> for Port {
fn from(p: u16) -> Self {
Port(p.to_string())
}
}
#[cfg(feature = "_sync-sender")]
fn validate_auto_flush_params(params: &HashMap<String, String>) -> Result<()> {
if let Some(auto_flush) = params.get("auto_flush")
&& auto_flush.as_str() != "off"
{
return Err(error::fmt!(
ConfigError,
"Invalid auto_flush value '{auto_flush}'. This client does not \
support auto-flush, so the only accepted value is 'off'"
));
}
for ¶m in ["auto_flush_rows", "auto_flush_bytes", "auto_flush_interval"].iter() {
if params.contains_key(param) {
return Err(error::fmt!(
ConfigError,
"Invalid configuration parameter {:?}. This client does not support auto-flush",
param
));
}
}
Ok(())
}
#[derive(PartialEq, Debug, Clone, Copy)]
#[non_exhaustive]
#[cfg(feature = "_sync-sender")]
pub enum Protocol {
#[cfg(feature = "_sender-tcp")]
Tcp,
#[cfg(feature = "_sender-tcp")]
Tcps,
#[cfg(feature = "_sender-http")]
Http,
#[cfg(feature = "_sender-http")]
Https,
#[cfg(feature = "_sender-qwp-udp")]
Udp,
#[cfg(feature = "_sender-qwp-ws")]
Ws,
#[cfg(feature = "_sender-qwp-ws")]
Wss,
}
#[cfg(feature = "_sync-sender")]
impl Display for Protocol {
fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.write_str(self.schema())
}
}
#[cfg(feature = "_sync-sender")]
impl Protocol {
fn default_port(&self) -> &str {
match *self {
#[cfg(feature = "_sender-tcp")]
Protocol::Tcp | Protocol::Tcps => "9009",
#[cfg(feature = "_sender-http")]
Protocol::Http | Protocol::Https => "9000",
#[cfg(feature = "_sender-qwp-udp")]
Protocol::Udp => "9007",
#[cfg(feature = "_sender-qwp-ws")]
Protocol::Ws | Protocol::Wss => "9000",
}
}
fn tls_enabled(&self) -> bool {
match *self {
#[cfg(feature = "_sender-tcp")]
Protocol::Tcp => false,
#[cfg(feature = "_sender-tcp")]
Protocol::Tcps => true,
#[cfg(feature = "_sender-http")]
Protocol::Http => false,
#[cfg(feature = "_sender-http")]
Protocol::Https => true,
#[cfg(feature = "_sender-qwp-udp")]
Protocol::Udp => false,
#[cfg(feature = "_sender-qwp-ws")]
Protocol::Ws => false,
#[cfg(feature = "_sender-qwp-ws")]
Protocol::Wss => true,
}
}
#[cfg(feature = "_sender-tcp")]
fn is_tcpx(&self) -> bool {
match self {
Protocol::Tcp | Protocol::Tcps => true,
#[cfg(feature = "_sender-http")]
Protocol::Http | Protocol::Https => false,
#[cfg(feature = "_sender-qwp-udp")]
Protocol::Udp => false,
#[cfg(feature = "_sender-qwp-ws")]
Protocol::Ws | Protocol::Wss => false,
}
}
#[cfg(feature = "_sender-http")]
fn is_httpx(&self) -> bool {
match self {
#[cfg(feature = "_sender-tcp")]
Protocol::Tcp | Protocol::Tcps => false,
Protocol::Http | Protocol::Https => true,
#[cfg(feature = "_sender-qwp-udp")]
Protocol::Udp => false,
#[cfg(feature = "_sender-qwp-ws")]
Protocol::Ws | Protocol::Wss => false,
}
}
#[cfg(feature = "_sender-qwp-udp")]
fn is_qwp_udp(&self) -> bool {
matches!(self, Protocol::Udp)
}
#[cfg(feature = "_sender-qwp-ws")]
fn is_qwp_ws(&self) -> bool {
matches!(self, Protocol::Ws | Protocol::Wss)
}
#[cfg(any(feature = "_sender-http", feature = "_sender-qwp-ws"))]
fn accepts_http_auth(&self) -> bool {
let mut accepts = false;
#[cfg(feature = "_sender-http")]
if self.is_httpx() {
accepts = true;
}
#[cfg(feature = "_sender-qwp-ws")]
if self.is_qwp_ws() {
accepts = true;
}
accepts
}
fn schema(&self) -> &str {
match *self {
#[cfg(feature = "_sender-tcp")]
Protocol::Tcp => "tcp",
#[cfg(feature = "_sender-tcp")]
Protocol::Tcps => "tcps",
#[cfg(feature = "_sender-http")]
Protocol::Http => "http",
#[cfg(feature = "_sender-http")]
Protocol::Https => "https",
#[cfg(feature = "_sender-qwp-udp")]
Protocol::Udp => "udp",
#[cfg(feature = "_sender-qwp-ws")]
Protocol::Ws => "ws",
#[cfg(feature = "_sender-qwp-ws")]
Protocol::Wss => "wss",
}
}
fn from_schema(schema: &str) -> Result<Self> {
#[cfg(feature = "_sender-tcp")]
if schema.eq_ignore_ascii_case("tcp") {
return Ok(Protocol::Tcp);
}
#[cfg(feature = "_sender-tcp")]
if schema.eq_ignore_ascii_case("tcps") {
return Ok(Protocol::Tcps);
}
#[cfg(feature = "_sender-http")]
if schema.eq_ignore_ascii_case("http") {
return Ok(Protocol::Http);
}
#[cfg(feature = "_sender-http")]
if schema.eq_ignore_ascii_case("https") {
return Ok(Protocol::Https);
}
#[cfg(feature = "_sender-qwp-udp")]
if schema.eq_ignore_ascii_case("udp") {
return Ok(Protocol::Udp);
}
#[cfg(feature = "_sender-qwp-udp")]
if schema.eq_ignore_ascii_case("udps") {
return Err(error::fmt!(ConfigError, "TLS is not supported for UDP."));
}
#[cfg(feature = "_sender-qwp-ws")]
if schema.eq_ignore_ascii_case("ws") {
return Ok(Protocol::Ws);
}
#[cfg(feature = "_sender-qwp-ws")]
if schema.eq_ignore_ascii_case("wss") {
return Ok(Protocol::Wss);
}
Err(error::fmt!(ConfigError, "Unsupported protocol: {}", schema))
}
}
#[cfg(any(feature = "_sender-qwp-ws", feature = "_egress"))]
pub(crate) struct QwpWsAddrScan {
pub(crate) addr_values: Vec<String>,
pub(crate) sanitized_conf: String,
}
#[cfg(feature = "sync-sender-qwp-ws")]
pub(crate) struct QwpWsConnector {
host: String,
port: String,
endpoints: std::sync::Arc<[conf::QwpWsEndpoint]>,
use_tls: bool,
tls_settings: Option<tls::TlsSettings>,
qwp_ws: conf::QwpWsConfig,
auth_header: Option<String>,
max_buf_size: usize,
}
#[cfg(feature = "sync-sender-qwp-ws")]
impl QwpWsConnector {
pub(crate) fn endpoint_count(&self) -> usize {
self.endpoints.len()
}
pub(crate) fn endpoint(&self, idx: usize) -> Option<&conf::QwpWsEndpoint> {
self.endpoints.get(idx)
}
pub(crate) fn max_buf_size(&self) -> usize {
self.max_buf_size
}
pub(crate) fn request_durable_ack(&self) -> bool {
*self.qwp_ws.request_durable_ack
}
pub(crate) fn sender_id(&self) -> &str {
self.qwp_ws.sender_id.as_str()
}
pub(crate) fn sf_dir(&self) -> Option<&Path> {
self.qwp_ws.sf_dir.as_deref()
}
pub(crate) fn request_timeout(&self) -> Duration {
*self.qwp_ws.request_timeout
}
pub(crate) fn close_flush_timeout(&self) -> Duration {
*self.qwp_ws.close_flush_timeout
}
#[cfg(feature = "sync-sender-qwp-ws")]
#[cfg_attr(
not(any(
feature = "polars-ingress",
feature = "polars-egress",
feature = "ffi-support"
)),
allow(dead_code)
)]
pub(crate) fn reconnect_policy(&self) -> sender::ReconnectPolicy {
sender::ReconnectPolicy::bounded(
*self.qwp_ws.reconnect_max_duration,
*self.qwp_ws.reconnect_initial_backoff,
*self.qwp_ws.reconnect_max_backoff,
)
}
pub(crate) fn connect_round_pooled(
&self,
health: &std::sync::Mutex<sender::qwp_ws::QwpWsHostHealthTracker>,
events: Option<&conn_events::ConnectionEventSource>,
) -> Result<RawQwpWsRoundStream> {
self.connect_round_with(sender::qwp_ws::LockedQwpWsHealth::new(health), events)
}
fn connect_round_with<A: sender::qwp_ws::QwpWsHealthAccess>(
&self,
health: A,
events: Option<&conn_events::ConnectionEventSource>,
) -> Result<RawQwpWsRoundStream> {
let mut previous_idx = None;
let connected = sender::qwp_ws::connect_qwp_ws_endpoint_round(
&self.endpoints,
health,
&mut previous_idx,
None,
self.use_tls,
self.tls_settings.clone(),
sender::qwp_ws::QwpWsConnectKind::Foreground,
&self.qwp_ws,
self.auth_header.as_deref(),
events,
None,
)?;
let max_buf_size = if connected.server_max_batch_size > 0 {
self.max_buf_size.min(connected.server_max_batch_size)
} else {
self.max_buf_size
};
let raw = RawQwpWsRoundStream {
endpoint_idx: connected.endpoint_idx,
stream: connected.stream,
leftover: connected.leftover,
max_buf_size,
request_timeout: *self.qwp_ws.request_timeout,
durable_ack_opt_in: *self.qwp_ws.request_durable_ack,
};
if let Some(events) = events
&& let Some(endpoint) = self.endpoints.get(raw.endpoint_idx)
{
events.connect_succeeded(&endpoint.host, &endpoint.port);
}
Ok(raw)
}
pub(crate) fn connect_sfa_background_with_pool_slot(
&self,
sender_id: Option<&str>,
managed_exclusions: &[conf::QwpWsManagedSlotExclusion],
extra_orphan_slots: &[PathBuf],
conn_events: std::sync::Arc<conn_events::ConnectionEventSource>,
rejection_sink: std::sync::Arc<rejection_events::RejectionEventSource>,
force_async_initial_connect: bool,
) -> Result<sender::qwp_ws::SyncQwpWsHandlerState> {
let mut qwp_ws = self.qwp_ws.clone();
if force_async_initial_connect {
qwp_ws.force_async_initial_connect();
}
qwp_ws.conn_events = Some(conn_events);
qwp_ws.rejection_sink = Some(rejection_sink);
configure_qwp_ws_pool_slot(
&mut qwp_ws,
sender_id,
managed_exclusions,
extra_orphan_slots,
)?;
sender::qwp_ws::connect_qwp_ws_background_state(
self.host.as_str(),
self.port.as_str(),
self.use_tls,
self.tls_settings.clone(),
&qwp_ws,
self.auth_header.clone(),
)
}
}
#[cfg(feature = "sync-sender-qwp-ws")]
fn configure_qwp_ws_pool_slot(
qwp_ws: &mut conf::QwpWsConfig,
sender_id: Option<&str>,
managed_exclusions: &[conf::QwpWsManagedSlotExclusion],
extra_orphan_slots: &[PathBuf],
) -> Result<()> {
if let Some(sender_id) = sender_id {
if !conf::is_valid_qwp_ws_sender_id(sender_id) {
return Err(error::fmt!(
ConfigError,
"invalid pool-managed sender_id [value={sender_id}, allowed-chars=[A-Za-z0-9_-]]"
));
}
qwp_ws.sender_id = ConfigSetting::new_specified(sender_id.to_owned());
}
qwp_ws.orphan_exclude_managed_slots = managed_exclusions.to_vec();
qwp_ws.orphan_extra_slots = extra_orphan_slots.to_vec();
qwp_ws.pool_managed_slot = sender_id.is_some();
Ok(())
}
#[cfg(feature = "sync-sender-qwp-ws")]
pub(crate) struct RawQwpWsRoundStream {
pub(crate) endpoint_idx: usize,
pub(crate) stream: sender::qwp_ws::WsStream,
pub(crate) leftover: Vec<u8>,
pub(crate) max_buf_size: usize,
pub(crate) request_timeout: Duration,
pub(crate) durable_ack_opt_in: bool,
}
#[cfg(any(feature = "_sender-qwp-ws", feature = "_egress"))]
pub(crate) fn scan_qwp_ws_addr_params(conf: &str) -> Result<Option<QwpWsAddrScan>> {
let Some((service, params)) = conf.split_once("::") else {
return Ok(None);
};
if !service.eq_ignore_ascii_case("ws") && !service.eq_ignore_ascii_case("wss") {
return Ok(None);
}
let mut addr_values = Vec::new();
let mut sanitized_conf = String::with_capacity(conf.len());
sanitized_conf.push_str(service);
sanitized_conf.push_str("::");
let params_offset = service.len() + 2;
let mut pos = 0usize;
while pos < params.len() {
let param_start = pos;
let Some(eq_rel) = params[pos..].find('=') else {
return Ok(None);
};
let key_start = pos;
let key_end = pos + eq_rel;
let key = ¶ms[key_start..key_end];
pos = key_end + 1;
let mut value = String::new();
while pos < params.len() {
let rest = ¶ms[pos..];
let mut chars = rest.char_indices();
let (_, ch) = chars.next().expect("pos is within params");
if ch == ';' {
let next_pos = pos + ch.len_utf8();
if params[next_pos..].starts_with(';') {
value.push(';');
pos = next_pos + 1;
continue;
}
pos = next_pos;
break;
}
value.push(ch);
pos += ch.len_utf8();
}
let param_end = pos;
if key.eq_ignore_ascii_case("addr") {
if addr_values.is_empty() {
sanitized_conf
.push_str(&conf[params_offset + param_start..params_offset + param_end]);
}
addr_values.push(value);
} else {
sanitized_conf.push_str(&conf[params_offset + param_start..params_offset + param_end]);
}
}
Ok(Some(QwpWsAddrScan {
addr_values,
sanitized_conf,
}))
}
#[cfg(feature = "_sender-qwp-ws")]
fn parse_qwp_ws_endpoints(
addr_values: &[String],
default_port: &str,
) -> Result<Vec<conf::QwpWsEndpoint>> {
let mut endpoints = Vec::new();
let mut seen = HashSet::new();
for addr in addr_values {
for raw_entry in addr.split(',') {
let entry = raw_entry.trim();
if entry.is_empty() {
return Err(error::fmt!(
ConfigError,
"invalid QWP/WebSocket addr list: empty entry"
));
}
let (host, port) = if let Some(rest) = entry.strip_prefix('[') {
let (host, after) = rest.split_once(']').ok_or_else(|| {
error::fmt!(
ConfigError,
"invalid QWP/WebSocket addr entry {:?}: missing ']'",
entry
)
})?;
let port = match after.strip_prefix(':') {
Some(port) => port.trim(),
None if after.is_empty() => default_port,
None => {
return Err(error::fmt!(
ConfigError,
"invalid QWP/WebSocket addr entry {:?}: \
expected ':port' after ']'",
entry
));
}
};
(host.trim(), port)
} else if entry.matches(':').count() > 1 {
return Err(error::fmt!(
ConfigError,
"invalid QWP/WebSocket addr entry {:?}: bracket IPv6 \
addresses, e.g. [::1]:9000",
entry
));
} else {
match entry.split_once(':') {
Some((host, port)) => (host.trim(), port.trim()),
None => (entry, default_port),
}
};
if host.is_empty() {
return Err(error::fmt!(
ConfigError,
"invalid QWP/WebSocket addr entry {:?}: empty host",
entry
));
}
if port.is_empty() {
return Err(error::fmt!(
ConfigError,
"invalid QWP/WebSocket addr entry {:?}: empty port",
entry
));
}
let parsed_port = port.parse::<u16>().map_err(|_| {
error::fmt!(
ConfigError,
"invalid QWP/WebSocket addr entry {:?}: invalid port {:?}",
entry,
port
)
})?;
if parsed_port == 0 {
return Err(error::fmt!(
ConfigError,
"invalid QWP/WebSocket addr entry {:?}: invalid port {:?}",
entry,
port
));
}
let normalized_port = parsed_port.to_string();
let key = (host.to_string(), normalized_port.clone());
if !seen.insert(key.clone()) {
return Err(error::fmt!(
ConfigError,
"duplicate QWP/WebSocket addr endpoint {}:{}",
host,
normalized_port
));
}
endpoints.push(conf::QwpWsEndpoint::new(key.0, key.1));
}
}
if endpoints.is_empty() {
return Err(error::fmt!(
ConfigError,
"Missing \"addr\" parameter in config string"
));
}
Ok(endpoints)
}
#[derive(Debug, Clone)]
#[cfg(feature = "_sync-sender")]
pub struct SenderBuilder {
protocol: Protocol,
host: ConfigSetting<String>,
port: ConfigSetting<String>,
net_interface: ConfigSetting<Option<String>>,
init_buf_size: ConfigSetting<usize>,
max_buf_size: ConfigSetting<usize>,
max_name_len: ConfigSetting<usize>,
auth_timeout: ConfigSetting<Duration>,
username: ConfigSetting<Option<String>>,
password: ConfigSetting<Option<String>>,
token: ConfigSetting<Option<String>>,
#[cfg(feature = "_sender-tcp")]
token_x: ConfigSetting<Option<String>>,
#[cfg(feature = "_sender-tcp")]
token_y: ConfigSetting<Option<String>>,
protocol_version: ConfigSetting<Option<ProtocolVersion>>,
#[cfg(feature = "insecure-skip-verify")]
tls_verify: ConfigSetting<bool>,
tls_ca: ConfigSetting<CertificateAuthority>,
tls_roots: ConfigSetting<Option<PathBuf>>,
#[cfg(feature = "_sender-qwp-ws")]
tls_roots_password: ConfigSetting<Option<String>>,
#[cfg(feature = "_sender-http")]
http: Option<conf::HttpConfig>,
#[cfg(feature = "_sender-qwp-udp")]
qwp_udp: Option<conf::QwpUdpConfig>,
#[cfg(feature = "_sender-qwp-ws")]
qwp_ws: Option<conf::QwpWsConfig>,
#[cfg(feature = "_sender-qwp-ws")]
qwp_ws_error_handler: QwpWsErrorHandler,
}
#[cfg(feature = "_sync-sender")]
impl SenderBuilder {
pub fn from_conf<T: AsRef<str>>(conf: T) -> Result<Self> {
let conf = conf.as_ref();
#[cfg(feature = "_sender-qwp-ws")]
let qwp_ws_addr_scan = scan_qwp_ws_addr_params(conf)?;
#[cfg(feature = "_sender-qwp-ws")]
let conf_to_parse = qwp_ws_addr_scan
.as_ref()
.map(|scan| scan.sanitized_conf.as_str())
.unwrap_or(conf);
#[cfg(not(feature = "_sender-qwp-ws"))]
let conf_to_parse = conf;
let conf = questdb_confstr::parse_conf_str(conf_to_parse)
.map_err(|e| error::fmt!(ConfigError, "Config parse error: {}", e))?;
let service = conf.service();
let params = conf.params();
let protocol = Protocol::from_schema(service)?;
#[cfg(feature = "_sender-qwp-ws")]
let conf_is_qwp_ws = protocol.is_qwp_ws();
let Some(addr) = params.get("addr") else {
return Err(error::fmt!(
ConfigError,
"Missing \"addr\" parameter in config string"
));
};
#[cfg(feature = "_sender-qwp-ws")]
let qwp_ws_endpoints = if protocol.is_qwp_ws() {
let addr_values = qwp_ws_addr_scan
.as_ref()
.map(|scan| scan.addr_values.as_slice())
.unwrap_or_else(|| std::slice::from_ref(addr));
Some(parse_qwp_ws_endpoints(
addr_values,
protocol.default_port(),
)?)
} else {
None
};
let (host, port) = {
#[cfg(feature = "_sender-qwp-ws")]
if let Some(endpoints) = qwp_ws_endpoints.as_ref() {
let first = endpoints.first().ok_or_else(|| {
error::fmt!(ConfigError, "Missing \"addr\" parameter in config string")
})?;
(first.host.as_str(), first.port.as_str())
} else {
match addr.split_once(':') {
Some((h, p)) => (h, p),
None => (addr.as_str(), protocol.default_port()),
}
}
#[cfg(not(feature = "_sender-qwp-ws"))]
{
match addr.split_once(':') {
Some((h, p)) => (h, p),
None => (addr.as_str(), protocol.default_port()),
}
}
};
let mut builder = SenderBuilder::new(protocol, host, port);
#[cfg(feature = "_sender-qwp-ws")]
if let Some(endpoints) = qwp_ws_endpoints {
builder = builder.qwp_ws_endpoints(endpoints)?;
}
validate_auto_flush_params(params)?;
#[cfg(feature = "_sender-qwp-ws")]
const QWP_WS_PORTABLE_CONFIG_KEYS: &[&str] = &[
"addr",
"auth",
"auto_flush",
"auto_flush_bytes",
"auto_flush_interval",
"auto_flush_rows",
"buffer_pool_size",
"client_id",
"compression",
"compression_level",
"failover",
"failover_backoff_initial_ms",
"failover_backoff_max_ms",
"failover_max_attempts",
"failover_max_duration_ms",
"max_batch_rows",
"max_version",
"on_internal_error",
"on_parse_error",
"on_schema_error",
"on_security_error",
"on_server_error",
"on_write_error",
"path",
"acquire_timeout_ms",
"idle_timeout_ms",
"lazy_connect",
"pool_reap",
"query_pool_max",
"query_pool_min",
"sender_pool_max",
"sender_pool_min",
"target",
"zone",
];
for (key, val) in params.iter().map(|(k, v)| (k.as_str(), v.as_str())) {
builder = match key {
"username" => builder.username(val)?,
"password" => builder.password(val)?,
"token" => builder.token(val)?,
"token_x" => builder.token_x(val)?,
"token_y" => builder.token_y(val)?,
"bind_interface" => builder.bind_interface(val)?,
#[cfg(feature = "_sender-qwp-udp")]
"max_datagram_size" => builder.max_datagram_size(parse_conf_value(key, val)?)?,
#[cfg(feature = "_sender-qwp-udp")]
"multicast_ttl" => builder.multicast_ttl(parse_conf_value(key, val)?)?,
#[cfg(feature = "_sender-qwp-ws")]
"qwp_ws_progress" => builder.qwp_ws_progress(parse_qwp_ws_progress_value(val)?)?,
#[cfg(feature = "_sender-qwp-ws")]
"sf_dir" => builder.store_and_forward_dir(PathBuf::from(val))?,
#[cfg(feature = "_sender-qwp-ws")]
"sender_id" => builder.sender_id(val)?,
#[cfg(feature = "_sender-qwp-ws")]
"sf_max_segment_bytes" => {
builder.store_and_forward_max_bytes(parse_size_conf_value(key, val)?)?
}
#[cfg(feature = "_sender-qwp-ws")]
"sf_max_total_bytes" => {
builder.store_and_forward_max_total_bytes(parse_size_conf_value(key, val)?)?
}
#[cfg(feature = "_sender-qwp-ws")]
"sf_durability" => {
builder.store_and_forward_durability(parse_sf_durability_value(val)?)?
}
#[cfg(feature = "_sender-qwp-ws")]
"sf_sync_interval_millis" => builder.store_and_forward_sync_interval_millis(val)?,
#[cfg(feature = "_sender-qwp-ws")]
"sf_append_deadline_millis" => builder.store_and_forward_append_deadline(
Duration::from_millis(parse_conf_value(key, val)?),
)?,
#[cfg(feature = "_sender-qwp-ws")]
"reconnect_max_duration_millis" => builder
.reconnect_max_duration(Duration::from_millis(parse_conf_value(key, val)?))?,
#[cfg(feature = "_sender-qwp-ws")]
"reconnect_initial_backoff_millis" => builder.reconnect_initial_backoff(
Duration::from_millis(parse_conf_value(key, val)?),
)?,
#[cfg(feature = "_sender-qwp-ws")]
"reconnect_max_backoff_millis" => builder
.reconnect_max_backoff(Duration::from_millis(parse_conf_value(key, val)?))?,
#[cfg(feature = "_sender-qwp-ws")]
"initial_connect_retry" => {
builder.qwp_ws_initial_connect_mode(parse_initial_connect_retry_value(val)?)?
}
#[cfg(feature = "_sender-qwp-ws")]
"auth_timeout_ms" => builder.qwp_ws_auth_timeout_millis(val)?,
#[cfg(feature = "_sender-qwp-ws")]
"connect_timeout" => builder.qwp_ws_connect_timeout_millis(val)?,
#[cfg(feature = "_sender-qwp-ws")]
"close_flush_timeout_millis" => builder.close_flush_timeout_millis(val)?,
#[cfg(feature = "_sender-qwp-ws")]
"request_durable_ack" => builder.request_durable_ack(val)?,
#[cfg(feature = "_sender-qwp-ws")]
"durable_ack_keepalive_interval_millis" => {
builder.durable_ack_keepalive_interval_millis(val)?
}
#[cfg(feature = "_sender-qwp-ws")]
"drain_orphans" => builder.drain_orphans(val)?,
#[cfg(feature = "_sender-qwp-ws")]
"max_background_drainers" => builder.max_background_drainers(val)?,
#[cfg(feature = "_sender-qwp-ws")]
"error_inbox_capacity" => builder.error_inbox_capacity(val)?,
#[cfg(feature = "_sender-qwp-ws")]
"max_frame_rejections" => {
builder.max_frame_rejections(parse_conf_value(key, val)?)?
}
#[cfg(feature = "_sender-qwp-ws")]
"poison_min_escalation_window_millis" => builder.poison_min_escalation_window(
Duration::from_millis(parse_conf_value(key, val)?),
)?,
"protocol_version" => match val {
"1" => builder.protocol_version(ProtocolVersion::V1)?,
"2" => builder.protocol_version(ProtocolVersion::V2)?,
"3" => builder.protocol_version(ProtocolVersion::V3)?,
"auto" => builder,
invalid => {
return Err(error::fmt!(
ConfigError,
"invalid \"protocol_version\" [value={invalid}, allowed-values=[auto, 1, 2, 3]]"
));
}
},
"max_name_len" => builder.max_name_len(parse_conf_value(key, val)?)?,
"init_buf_size" => builder.init_buf_size(parse_conf_value(key, val)?)?,
"max_buf_size" => builder.max_buf_size(parse_conf_value(key, val)?)?,
"auth_timeout" => {
builder.auth_timeout(Duration::from_millis(parse_conf_value(key, val)?))?
}
"tls_verify" => {
let verify = match val {
"on" => true,
"unsafe_off" => false,
_ => {
return Err(fmt!(
ConfigError,
r##"Config parameter "tls_verify" must be either "on" or "unsafe_off".'"##,
));
}
};
#[cfg(not(feature = "insecure-skip-verify"))]
{
if !verify {
return Err(fmt!(
ConfigError,
r##"The "insecure-skip-verify" feature is not enabled, so "tls_verify=unsafe_off" is not supported"##,
));
}
builder
}
#[cfg(feature = "insecure-skip-verify")]
builder.tls_verify(verify)?
}
"tls_ca" => {
#[allow(unreachable_code, unused_variables)]
{
let ca = match val {
#[cfg(feature = "tls-webpki-certs")]
"webpki_roots" => CertificateAuthority::WebpkiRoots,
#[cfg(not(feature = "tls-webpki-certs"))]
"webpki_roots" => {
return Err(error::fmt!(
ConfigError,
"Config parameter \"tls_ca=webpki_roots\" requires the \"tls-webpki-certs\" feature"
));
}
#[cfg(feature = "tls-native-certs")]
"os_roots" => CertificateAuthority::OsRoots,
#[cfg(not(feature = "tls-native-certs"))]
"os_roots" => {
return Err(error::fmt!(
ConfigError,
"Config parameter \"tls_ca=os_roots\" requires the \"tls-native-certs\" feature"
));
}
#[cfg(all(feature = "tls-webpki-certs", feature = "tls-native-certs"))]
"webpki_and_os_roots" => CertificateAuthority::WebpkiAndOsRoots,
#[cfg(not(all(
feature = "tls-webpki-certs",
feature = "tls-native-certs"
)))]
"webpki_and_os_roots" => {
return Err(error::fmt!(
ConfigError,
"Config parameter \"tls_ca=webpki_and_os_roots\" requires both the \"tls-webpki-certs\" and \"tls-native-certs\" features"
));
}
_ => {
return Err(error::fmt!(
ConfigError,
"Invalid value {val:?} for \"tls_ca\""
));
}
};
builder.tls_ca(ca)?
}
}
"tls_roots" => {
let path = PathBuf::from_str(val).map_err(|e| {
error::fmt!(
ConfigError,
"Invalid path {:?} for \"tls_roots\": {}",
val,
e
)
})?;
builder.tls_roots(path)?
}
"tls_roots_password" => {
#[cfg(feature = "_sender-qwp-ws")]
{
builder.tls_roots_password(val.to_string())?
}
#[cfg(not(feature = "_sender-qwp-ws"))]
{
return Err(error::fmt!(
ConfigError,
"\"tls_roots_password\" is only supported for QWP/WebSocket \
(ws / wss). ILP/TCP and ILP/HTTP transports read \
unencrypted PEM via rustls."
));
}
}
#[cfg(feature = "sync-sender-http")]
"request_min_throughput" => {
builder.request_min_throughput(parse_conf_value(key, val)?)?
}
#[cfg(feature = "sync-sender-http")]
"request_timeout" => {
builder.request_timeout(Duration::from_millis(parse_conf_value(key, val)?))?
}
#[cfg(feature = "sync-sender-http")]
"retry_timeout" => {
builder.retry_timeout(Duration::from_millis(parse_conf_value(key, val)?))?
}
#[cfg(feature = "sync-sender-http")]
"retry_max_backoff_millis" => {
builder.retry_max_backoff(Duration::from_millis(parse_conf_value(key, val)?))?
}
#[cfg(feature = "_sender-qwp-ws")]
other if conf_is_qwp_ws && !QWP_WS_PORTABLE_CONFIG_KEYS.contains(&other) => {
return Err(error::fmt!(ConfigError, "Unknown config key \"{}\"", other));
}
_ => builder,
};
}
Ok(builder)
}
pub fn from_env() -> Result<Self> {
let conf = std::env::var("QDB_CLIENT_CONF").map_err(|_| {
error::fmt!(ConfigError, "Environment variable QDB_CLIENT_CONF not set.")
})?;
Self::from_conf(conf)
}
pub fn new<H: Into<String>, P: Into<Port>>(protocol: Protocol, host: H, port: P) -> Self {
let host = host.into();
let port: Port = port.into();
let port = port.0;
#[cfg(feature = "tls-webpki-certs")]
let tls_ca = CertificateAuthority::WebpkiRoots;
#[cfg(all(not(feature = "tls-webpki-certs"), feature = "tls-native-certs"))]
let tls_ca = CertificateAuthority::OsRoots;
#[cfg(not(any(feature = "tls-webpki-certs", feature = "tls-native-certs")))]
let tls_ca = CertificateAuthority::PemFile;
Self {
protocol,
host: ConfigSetting::new_specified(host),
port: ConfigSetting::new_specified(port),
net_interface: ConfigSetting::new_default(None),
init_buf_size: ConfigSetting::new_default(64 * 1024),
max_buf_size: ConfigSetting::new_default(100 * 1024 * 1024),
max_name_len: ConfigSetting::new_default(MAX_NAME_LEN_DEFAULT),
auth_timeout: ConfigSetting::new_default(Duration::from_secs(15)),
username: ConfigSetting::new_default(None),
password: ConfigSetting::new_default(None),
token: ConfigSetting::new_default(None),
#[cfg(feature = "_sender-tcp")]
token_x: ConfigSetting::new_default(None),
#[cfg(feature = "_sender-tcp")]
token_y: ConfigSetting::new_default(None),
protocol_version: ConfigSetting::new_default(None),
#[cfg(feature = "insecure-skip-verify")]
tls_verify: ConfigSetting::new_default(true),
tls_ca: ConfigSetting::new_default(tls_ca),
tls_roots: ConfigSetting::new_default(None),
#[cfg(feature = "_sender-qwp-ws")]
tls_roots_password: ConfigSetting::new_default(None),
#[cfg(feature = "sync-sender-http")]
http: if protocol.is_httpx() {
Some(conf::HttpConfig::default())
} else {
None
},
#[cfg(feature = "_sender-qwp-udp")]
qwp_udp: if protocol.is_qwp_udp() {
Some(conf::QwpUdpConfig::default())
} else {
None
},
#[cfg(feature = "_sender-qwp-ws")]
qwp_ws: if protocol.is_qwp_ws() {
Some(conf::QwpWsConfig::default())
} else {
None
},
#[cfg(feature = "_sender-qwp-ws")]
qwp_ws_error_handler: QwpWsErrorHandler::log_default(),
}
}
#[cfg(feature = "_sender-qwp-ws")]
pub fn qwp_ws_error_handler<F>(mut self, handler: F) -> Result<Self>
where
F: Fn(&QwpWsSenderError) + Send + Sync + 'static,
{
self.qwp_ws_error_handler = QwpWsErrorHandler::new(handler);
Ok(self)
}
pub fn bind_interface<I: Into<String>>(self, addr: I) -> Result<Self> {
#[cfg(any(feature = "_sender-tcp", feature = "_sender-qwp-udp"))]
{
let mut builder = self;
builder.ensure_supports_bind_interface("bind_interface")?;
builder
.net_interface
.set_specified("bind_interface", Some(validate_value(addr.into())?))?;
Ok(builder)
}
#[cfg(not(any(feature = "_sender-tcp", feature = "_sender-qwp-udp")))]
{
let _ = addr;
Err(error::fmt!(
ConfigError,
"The \"bind_interface\" setting can only be used with the TCP protocol."
))
}
}
pub fn username(mut self, username: &str) -> Result<Self> {
#[cfg(feature = "_sender-qwp-udp")]
self.reject_if_qwp_udp("username")?;
self.username
.set_specified("username", Some(validate_value(username.to_string())?))?;
Ok(self)
}
pub fn password(mut self, password: &str) -> Result<Self> {
#[cfg(feature = "_sender-qwp-udp")]
self.reject_if_qwp_udp("password")?;
self.password
.set_specified("password", Some(validate_value(password.to_string())?))?;
Ok(self)
}
pub fn token(mut self, token: &str) -> Result<Self> {
#[cfg(feature = "_sender-qwp-udp")]
self.reject_if_qwp_udp("token")?;
self.token
.set_specified("token", Some(validate_value(token.to_string())?))?;
Ok(self)
}
pub fn token_x(self, token_x: &str) -> Result<Self> {
#[cfg(feature = "_sender-qwp-udp")]
self.reject_if_qwp_udp("token_x")?;
#[cfg(feature = "_sender-tcp")]
{
let mut builder = self;
builder
.token_x
.set_specified("token_x", Some(validate_value(token_x.to_string())?))?;
Ok(builder)
}
#[cfg(not(feature = "_sender-tcp"))]
{
let _ = token_x;
Err(error::fmt!(
ConfigError,
"cannot specify \"token_x\": ECDSA authentication is only available with ILP/TCP and not available with ILP/HTTP."
))
}
}
pub fn token_y(self, token_y: &str) -> Result<Self> {
#[cfg(feature = "_sender-qwp-udp")]
self.reject_if_qwp_udp("token_y")?;
#[cfg(feature = "_sender-tcp")]
{
let mut builder = self;
builder
.token_y
.set_specified("token_y", Some(validate_value(token_y.to_string())?))?;
Ok(builder)
}
#[cfg(not(feature = "_sender-tcp"))]
{
let _ = token_y;
Err(error::fmt!(
ConfigError,
"cannot specify \"token_y\": ECDSA authentication is only available with ILP/TCP and not available with ILP/HTTP."
))
}
}
pub fn protocol_version(mut self, protocol_version: ProtocolVersion) -> Result<Self> {
#[cfg(feature = "_sender-qwp-udp")]
self.reject_if_qwp_udp("protocol_version")?;
self.protocol_version
.set_specified("protocol_version", Some(protocol_version))?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-udp")]
pub fn max_datagram_size(mut self, value: usize) -> Result<Self> {
if value == 0 {
return Err(error::fmt!(
ConfigError,
"\"max_datagram_size\" must be greater than 0."
));
}
if value > 65507 {
return Err(error::fmt!(
ConfigError,
"\"max_datagram_size\" must not exceed 65507 (UDP/IPv4 limit)."
));
}
let Some(qwp_udp) = &mut self.qwp_udp else {
return Err(error::fmt!(
ConfigError,
"The \"max_datagram_size\" setting is only supported for QWP/UDP."
));
};
qwp_udp
.max_datagram_size
.set_specified("max_datagram_size", value)?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-udp")]
pub fn multicast_ttl(mut self, value: u32) -> Result<Self> {
if value > 255 {
return Err(error::fmt!(
ConfigError,
"\"multicast_ttl\" must be between 0 and 255."
));
}
let Some(qwp_udp) = &mut self.qwp_udp else {
return Err(error::fmt!(
ConfigError,
"The \"multicast_ttl\" setting is only supported for QWP/UDP."
));
};
qwp_udp
.multicast_ttl
.set_specified("multicast_ttl", value)?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
pub fn connection_listener(
mut self,
listener: crate::ingress::ConnectionListener,
inbox_capacity: usize,
) -> Result<Self> {
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"connection_listener\" setting is only supported for QWP/WebSocket."
));
};
if qwp_ws.conn_events.is_some() {
return Err(error::fmt!(
ConfigError,
"A connection listener is already registered on this builder."
));
}
qwp_ws.conn_events = Some(std::sync::Arc::new(
conn_events::ConnectionEventSource::new(listener, inbox_capacity),
));
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
pub fn qwp_ws_progress(mut self, progress: QwpWsProgress) -> Result<Self> {
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"qwp_ws_progress\" setting is only supported for QWP/WebSocket."
));
};
qwp_ws.progress.set_specified("qwp_ws_progress", progress)?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
fn store_and_forward_dir(mut self, dir: PathBuf) -> Result<Self> {
if dir.as_os_str().is_empty() {
return Err(error::fmt!(ConfigError, "\"sf_dir\" cannot be empty."));
}
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"sf_dir\" setting is only supported for QWP/WebSocket."
));
};
qwp_ws.sf_dir.set_specified("sf_dir", Some(dir))?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
fn sender_id(mut self, sender_id: &str) -> Result<Self> {
let sender_id = validate_value(sender_id)?;
if !conf::is_valid_qwp_ws_sender_id(sender_id) {
return Err(error::fmt!(
ConfigError,
"invalid sender_id [value={sender_id}, allowed-chars=[A-Za-z0-9_-]]"
));
}
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"sender_id\" setting is only supported for QWP/WebSocket."
));
};
qwp_ws
.sender_id
.set_specified("sender_id", sender_id.to_owned())?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
fn store_and_forward_max_bytes(mut self, value: u64) -> Result<Self> {
if value == 0 {
return Err(error::fmt!(
ConfigError,
"\"sf_max_segment_bytes\" must be greater than 0."
));
}
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"sf_max_segment_bytes\" setting is only supported for QWP/WebSocket."
));
};
qwp_ws
.sf_max_segment_bytes
.set_specified("sf_max_segment_bytes", value)?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
fn store_and_forward_max_total_bytes(mut self, value: u64) -> Result<Self> {
if value == 0 {
return Err(error::fmt!(
ConfigError,
"\"sf_max_total_bytes\" must be greater than 0."
));
}
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"sf_max_total_bytes\" setting is only supported for QWP/WebSocket."
));
};
qwp_ws
.sf_max_total_bytes
.set_specified("sf_max_total_bytes", Some(value))?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
fn store_and_forward_durability(mut self, durability: conf::SfDurability) -> Result<Self> {
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"sf_durability\" setting is only supported for QWP/WebSocket."
));
};
qwp_ws
.sf_durability
.set_specified("sf_durability", durability)?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
fn store_and_forward_sync_interval_millis(mut self, value: &str) -> Result<Self> {
const MAX_MILLIS: i64 = i64::MAX / 1_000_000;
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"sf_sync_interval_millis\" setting is only supported for QWP/WebSocket."
));
};
let millis: i64 = parse_conf_value("sf_sync_interval_millis", value)?;
if millis <= 0 {
return Err(error::fmt!(
ConfigError,
"\"sf_sync_interval_millis\" must be greater than 0."
));
}
if millis > MAX_MILLIS {
return Err(error::fmt!(
ConfigError,
"\"sf_sync_interval_millis\" must be at most {MAX_MILLIS}."
));
}
qwp_ws.sf_sync_interval.set_specified(
"sf_sync_interval_millis",
Some(Duration::from_millis(millis as u64)),
)?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
fn store_and_forward_append_deadline(mut self, value: Duration) -> Result<Self> {
if value.is_zero() {
return Err(error::fmt!(
ConfigError,
"\"sf_append_deadline_millis\" must be greater than 0."
));
}
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"sf_append_deadline_millis\" setting is only supported for QWP/WebSocket."
));
};
qwp_ws
.sf_append_deadline
.set_specified("sf_append_deadline_millis", value)?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
pub fn reconnect_max_duration(mut self, value: Duration) -> Result<Self> {
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"reconnect_max_duration_millis\" setting is only supported for QWP/WebSocket."
));
};
qwp_ws
.reconnect_max_duration
.set_specified("reconnect_max_duration_millis", value)?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
pub fn max_frame_rejections(mut self, value: usize) -> Result<Self> {
if value == 0 {
return Err(error::fmt!(
ConfigError,
"\"max_frame_rejections\" must be greater than 0."
));
}
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"max_frame_rejections\" setting is only supported for QWP/WebSocket."
));
};
qwp_ws
.max_frame_rejections
.set_specified("max_frame_rejections", value)?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
pub fn poison_min_escalation_window(mut self, value: Duration) -> Result<Self> {
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"poison_min_escalation_window_millis\" setting is only supported for QWP/WebSocket."
));
};
qwp_ws
.poison_min_escalation_window
.set_specified("poison_min_escalation_window_millis", value)?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
pub fn reconnect_initial_backoff(mut self, value: Duration) -> Result<Self> {
if value.is_zero() {
return Err(error::fmt!(
ConfigError,
"\"reconnect_initial_backoff_millis\" must be greater than 0."
));
}
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"reconnect_initial_backoff_millis\" setting is only supported for QWP/WebSocket."
));
};
qwp_ws
.reconnect_initial_backoff
.set_specified("reconnect_initial_backoff_millis", value)?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
pub fn reconnect_max_backoff(mut self, value: Duration) -> Result<Self> {
if value.is_zero() {
return Err(error::fmt!(
ConfigError,
"\"reconnect_max_backoff_millis\" must be greater than 0."
));
}
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"reconnect_max_backoff_millis\" setting is only supported for QWP/WebSocket."
));
};
qwp_ws
.reconnect_max_backoff
.set_specified("reconnect_max_backoff_millis", value)?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
fn qwp_ws_endpoints(mut self, endpoints: Vec<conf::QwpWsEndpoint>) -> Result<Self> {
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"QWP/WebSocket endpoint lists are only supported for QWP/WebSocket."
));
};
qwp_ws.endpoints.set_specified("addr", endpoints)?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
fn qwp_ws_initial_connect_mode(mut self, mode: conf::QwpWsInitialConnectMode) -> Result<Self> {
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"initial_connect_retry\" setting is only supported for QWP/WebSocket."
));
};
qwp_ws
.initial_connect_retry
.set_specified("initial_connect_retry", mode)?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
pub fn initial_connect_retry(mut self, value: bool) -> Result<Self> {
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"initial_connect_retry\" setting is only supported for QWP/WebSocket."
));
};
qwp_ws.initial_connect_retry.set_specified(
"initial_connect_retry",
if value {
conf::QwpWsInitialConnectMode::Sync
} else {
conf::QwpWsInitialConnectMode::Off
},
)?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
fn qwp_ws_auth_timeout_millis(mut self, value: &str) -> Result<Self> {
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"auth_timeout_ms\" setting is only supported for QWP/WebSocket."
));
};
let millis: i64 = parse_conf_value("auth_timeout_ms", value)?;
if millis <= 0 {
return Err(error::fmt!(
ConfigError,
"auth_timeout_ms must be > 0: {}",
millis
));
}
qwp_ws
.auth_timeout
.set_specified("auth_timeout_ms", Duration::from_millis(millis as u64))?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
fn qwp_ws_connect_timeout_millis(mut self, value: &str) -> Result<Self> {
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"connect_timeout\" setting is only supported for QWP/WebSocket."
));
};
let millis: i64 = parse_conf_value("connect_timeout", value)?;
if millis <= 0 {
return Err(error::fmt!(
ConfigError,
"connect_timeout must be > 0: {}",
millis
));
}
qwp_ws.connect_timeout.set_specified(
"connect_timeout",
Some(Duration::from_millis(millis as u64)),
)?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
fn close_flush_timeout_millis(mut self, value: &str) -> Result<Self> {
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"close_flush_timeout_millis\" setting is only supported for QWP/WebSocket."
));
};
let millis: i64 = parse_conf_value("close_flush_timeout_millis", value)?;
let timeout = if millis <= 0 {
Duration::ZERO
} else {
Duration::from_millis(millis as u64)
};
qwp_ws
.close_flush_timeout
.set_specified("close_flush_timeout_millis", timeout)?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
fn request_durable_ack(mut self, value: &str) -> Result<Self> {
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"request_durable_ack\" setting is only supported for QWP/WebSocket."
));
};
if value.eq_ignore_ascii_case("off") {
qwp_ws
.request_durable_ack
.set_specified("request_durable_ack", false)?;
return Ok(self);
}
if value.eq_ignore_ascii_case("on") {
qwp_ws
.request_durable_ack
.set_specified("request_durable_ack", true)?;
return Ok(self);
}
Err(error::fmt!(
ConfigError,
"invalid request_durable_ack [value={value}, allowed-values=[on, off]]"
))
}
#[cfg(feature = "_sender-qwp-ws")]
fn drain_orphans(mut self, value: &str) -> Result<Self> {
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"drain_orphans\" setting is only supported for QWP/WebSocket."
));
};
if value.eq_ignore_ascii_case("off") || value.eq_ignore_ascii_case("false") {
qwp_ws.drain_orphans.set_specified("drain_orphans", false)?;
return Ok(self);
}
if value.eq_ignore_ascii_case("on") || value.eq_ignore_ascii_case("true") {
qwp_ws.drain_orphans.set_specified("drain_orphans", true)?;
return Ok(self);
}
Err(error::fmt!(
ConfigError,
"invalid drain_orphans [value={value}, allowed-values=[on, off, true, false]]"
))
}
#[cfg(feature = "_sender-qwp-ws")]
fn durable_ack_keepalive_interval_millis(mut self, value: &str) -> Result<Self> {
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"durable_ack_keepalive_interval_millis\" setting is only supported for QWP/WebSocket."
));
};
let millis: i64 = parse_conf_value("durable_ack_keepalive_interval_millis", value)?;
let interval = if millis <= 0 {
Duration::ZERO
} else {
Duration::from_millis(millis as u64)
};
qwp_ws
.durable_ack_keepalive_interval
.set_specified("durable_ack_keepalive_interval_millis", interval)?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
fn max_background_drainers(mut self, value: &str) -> Result<Self> {
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"max_background_drainers\" setting is only supported for QWP/WebSocket."
));
};
let value: i32 = parse_conf_value("max_background_drainers", value)?;
if value < 0 {
return Err(error::fmt!(
ConfigError,
"max_background_drainers must be >= 0: {value}"
));
}
qwp_ws
.max_background_drainers
.set_specified("max_background_drainers", value as usize)?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-ws")]
fn error_inbox_capacity(mut self, value: &str) -> Result<Self> {
let Some(qwp_ws) = &mut self.qwp_ws else {
return Err(error::fmt!(
ConfigError,
"The \"error_inbox_capacity\" setting is only supported for QWP/WebSocket."
));
};
let value: usize = parse_conf_value("error_inbox_capacity", value)?;
if value < conf::QWP_WS_MIN_ERROR_INBOX_CAPACITY {
return Err(error::fmt!(
ConfigError,
"error_inbox_capacity must be >= {}: {value}",
conf::QWP_WS_MIN_ERROR_INBOX_CAPACITY
));
}
qwp_ws
.error_inbox_capacity
.set_specified("error_inbox_capacity", value)?;
Ok(self)
}
pub fn auth_timeout(mut self, value: Duration) -> Result<Self> {
#[cfg(feature = "_sender-qwp-udp")]
self.reject_if_qwp_udp("auth_timeout")?;
#[cfg(feature = "_sender-qwp-ws")]
if let Some(qwp_ws) = &mut self.qwp_ws {
if value.is_zero() {
return Err(error::fmt!(
ConfigError,
"\"auth_timeout\" must be greater than 0."
));
}
qwp_ws.auth_timeout.set_specified("auth_timeout", value)?;
return Ok(self);
}
self.auth_timeout.set_specified("auth_timeout", value)?;
Ok(self)
}
#[cfg(feature = "_sender-qwp-udp")]
fn reject_if_qwp_udp(&self, setting: &str) -> Result<()> {
if self.protocol.is_qwp_udp() {
return Err(error::fmt!(
ConfigError,
"The \"{setting}\" setting is not supported for QWP/UDP."
));
}
Ok(())
}
pub fn ensure_tls_enabled(&self, property: &str) -> Result<()> {
if !self.protocol.tls_enabled() {
return Err(error::fmt!(
ConfigError,
"Cannot set {property:?}: TLS is not supported for protocol {}",
self.protocol
));
}
Ok(())
}
#[cfg(feature = "insecure-skip-verify")]
pub fn tls_verify(mut self, verify: bool) -> Result<Self> {
self.ensure_tls_enabled("tls_verify")?;
self.tls_verify.set_specified("tls_verify", verify)?;
Ok(self)
}
pub fn tls_ca(mut self, ca: CertificateAuthority) -> Result<Self> {
self.ensure_tls_enabled("tls_ca")?;
self.tls_ca.set_specified("tls_ca", ca)?;
Ok(self)
}
pub fn tls_roots<P: Into<PathBuf>>(self, path: P) -> Result<Self> {
let mut builder = self.tls_ca(CertificateAuthority::PemFile)?;
let path = path.into();
let _file = std::fs::File::open(&path).map_err(|io_err| {
error::fmt!(
ConfigError,
"Could not open root certificate file from path {:?}: {}",
path,
io_err
)
})?;
builder.tls_roots.set_specified("tls_roots", Some(path))?;
Ok(builder)
}
#[cfg(feature = "_sender-qwp-ws")]
pub fn tls_roots_password<S: Into<String>>(mut self, password: S) -> Result<Self> {
if !self.protocol.is_qwp_ws() {
return Err(error::fmt!(
ConfigError,
"\"tls_roots_password\" is only supported for QWP/WebSocket \
(ws / wss). ILP/TCP and ILP/HTTP transports read \
unencrypted PEM via rustls."
));
}
self.ensure_tls_enabled("tls_roots_password")?;
self.tls_roots_password
.set_specified("tls_roots_password", Some(password.into()))?;
Ok(self)
}
pub fn init_buf_size(mut self, value: usize) -> Result<Self> {
let min = 1024;
if value < min {
return Err(error::fmt!(
ConfigError,
"\"init_buf_size\" must be at least {min} bytes."
));
}
self.init_buf_size.set_specified("init_buf_size", value)?;
Ok(self)
}
pub fn max_buf_size(mut self, value: usize) -> Result<Self> {
let min = 1024;
if value < min {
return Err(error::fmt!(
ConfigError,
"max_buf_size\" must be at least {min} bytes."
));
}
self.max_buf_size.set_specified("max_buf_size", value)?;
Ok(self)
}
pub fn max_name_len(mut self, value: usize) -> Result<Self> {
if value < 16 {
return Err(error::fmt!(
ConfigError,
"max_name_len must be at least 16 bytes."
));
}
self.max_name_len.set_specified("max_name_len", value)?;
Ok(self)
}
#[cfg(feature = "sync-sender-qwp-ws")]
pub(crate) fn configured_max_name_len(&self) -> usize {
*self.max_name_len
}
#[cfg(feature = "sync-sender-http")]
pub fn retry_timeout(mut self, value: Duration) -> Result<Self> {
if let Some(http) = &mut self.http {
http.retry_timeout.set_specified("retry_timeout", value)?;
} else {
return Err(error::fmt!(
ConfigError,
"retry_timeout is supported only in ILP over HTTP."
));
}
Ok(self)
}
#[cfg(feature = "sync-sender-http")]
pub fn retry_max_backoff(mut self, value: Duration) -> Result<Self> {
if value < Duration::from_millis(10) {
return Err(error::fmt!(
ConfigError,
"\"retry_max_backoff_millis\" must be at least 10."
));
}
if let Some(http) = &mut self.http {
http.retry_max_backoff
.set_specified("retry_max_backoff_millis", value)?;
} else {
return Err(error::fmt!(
ConfigError,
"retry_max_backoff_millis is supported only in ILP over HTTP."
));
}
Ok(self)
}
#[cfg(feature = "sync-sender-http")]
pub fn request_min_throughput(mut self, value: u64) -> Result<Self> {
if let Some(http) = &mut self.http {
http.request_min_throughput
.set_specified("request_min_throughput", value)?;
} else {
return Err(error::fmt!(
ConfigError,
"\"request_min_throughput\" is supported only in ILP over HTTP."
));
}
Ok(self)
}
#[cfg(feature = "sync-sender-http")]
pub fn request_timeout(mut self, value: Duration) -> Result<Self> {
if let Some(http) = &mut self.http {
if value.is_zero() {
return Err(error::fmt!(
ConfigError,
"\"request_timeout\" must be greater than 0."
));
}
http.request_timeout
.set_specified("request_timeout", value)?;
} else {
return Err(error::fmt!(
ConfigError,
"\"request_timeout\" is supported only in ILP over HTTP."
));
}
Ok(self)
}
#[cfg(feature = "sync-sender-http")]
#[doc(hidden)]
pub fn user_agent(mut self, value: &str) -> Result<Self> {
let value = validate_value(value)?;
if let Some(http) = &mut self.http {
http.user_agent = value.to_string();
}
Ok(self)
}
fn build_auth(&self) -> Result<Option<conf::AuthParams>> {
match (
self.protocol,
self.username.deref(),
self.password.deref(),
self.token.deref(),
#[cfg(feature = "_sender-tcp")]
self.token_x.deref(),
#[cfg(not(feature = "_sender-tcp"))]
None::<String>,
#[cfg(feature = "_sender-tcp")]
self.token_y.deref(),
#[cfg(not(feature = "_sender-tcp"))]
None::<String>,
) {
(_, None, None, None, None, None) => Ok(None),
#[cfg(feature = "_sender-tcp")]
(protocol, Some(username), None, Some(token), Some(token_x), Some(token_y))
if protocol.is_tcpx() =>
{
Ok(Some(conf::AuthParams::Ecdsa(conf::EcdsaAuthParams {
key_id: username.to_string(),
priv_key: token.to_string(),
pub_key_x: token_x.to_string(),
pub_key_y: token_y.to_string(),
})))
}
#[cfg(feature = "_sender-tcp")]
(protocol, Some(_username), Some(_password), None, None, None)
if protocol.is_tcpx() =>
{
Err(error::fmt!(
ConfigError,
r##"The "basic_auth" setting can only be used with the ILP/HTTP protocol."##,
))
}
#[cfg(feature = "_sender-tcp")]
(protocol, None, None, Some(_token), None, None) if protocol.is_tcpx() => {
Err(error::fmt!(
ConfigError,
"Token authentication only be used with the ILP/HTTP protocol."
))
}
#[cfg(feature = "_sender-tcp")]
(protocol, _username, None, _token, _token_x, _token_y) if protocol.is_tcpx() => {
Err(error::fmt!(
ConfigError,
r##"Incomplete ECDSA authentication parameters. Specify either all or none of: "username", "token", "token_x", "token_y"."##,
))
}
#[cfg(any(feature = "_sender-http", feature = "_sender-qwp-ws"))]
(protocol, Some(username), Some(password), None, None, None)
if protocol.accepts_http_auth() =>
{
Ok(Some(conf::AuthParams::Basic(conf::BasicAuthParams {
username: username.to_string(),
password: password.to_string(),
})))
}
#[cfg(any(feature = "_sender-http", feature = "_sender-qwp-ws"))]
(protocol, Some(_username), None, None, None, None) if protocol.accepts_http_auth() => {
Err(error::fmt!(
ConfigError,
r##"Basic authentication parameter "username" is present, but "password" is missing."##,
))
}
#[cfg(any(feature = "_sender-http", feature = "_sender-qwp-ws"))]
(protocol, None, Some(_password), None, None, None) if protocol.accepts_http_auth() => {
Err(error::fmt!(
ConfigError,
r##"Basic authentication parameter "password" is present, but "username" is missing."##,
))
}
#[cfg(any(feature = "_sender-http", feature = "_sender-qwp-ws"))]
(protocol, None, None, Some(token), None, None) if protocol.accepts_http_auth() => {
Ok(Some(conf::AuthParams::Token(conf::TokenAuthParams {
token: token.to_string(),
})))
}
#[cfg(feature = "_sender-http")]
(protocol, Some(_username), None, Some(_token), Some(_token_x), Some(_token_y))
if protocol.is_httpx() =>
{
Err(error::fmt!(
ConfigError,
"ECDSA authentication is only available with ILP/TCP and not available with ILP/HTTP."
))
}
#[cfg(feature = "_sender-http")]
(protocol, _username, _password, _token, None, None) if protocol.is_httpx() => {
Err(error::fmt!(
ConfigError,
r##"Inconsistent HTTP authentication parameters. Specify either "username" and "password", or just "token"."##,
))
}
_ => Err(error::fmt!(
ConfigError,
r##"Incomplete authentication parameters. Check "username", "password", "token", "token_x" and "token_y" parameters are set correctly."##,
)),
}
}
#[cfg(feature = "_sync-sender")]
pub fn build(&self) -> Result<Sender> {
if self.init_buf_size.is_specified() && *self.init_buf_size > *self.max_buf_size {
return Err(error::fmt!(
ConfigError,
"init_buf_size ({}) cannot exceed max_buf_size ({})",
*self.init_buf_size,
*self.max_buf_size
));
}
let mut descr = format!("Sender[host={:?},port={:?},", self.host, self.port);
if self.protocol.tls_enabled() {
write!(descr, "tls=enabled,").unwrap();
} else {
write!(descr, "tls=disabled,").unwrap();
}
#[cfg(feature = "insecure-skip-verify")]
let tls_verify = *self.tls_verify;
#[cfg(feature = "_sender-qwp-ws")]
let tls_roots_password = self.tls_roots_password.deref().as_deref();
#[cfg(not(feature = "_sender-qwp-ws"))]
let tls_roots_password: Option<&str> = None;
if tls_roots_password.is_some() && self.tls_roots.deref().is_none() {
return Err(error::fmt!(
ConfigError,
"\"tls_roots_password\" requires \"tls_roots\" \
(the password unlocks the keystore at that path)"
));
}
#[allow(unused_variables)]
let tls_settings = tls::TlsSettings::build(
self.protocol.tls_enabled(),
#[cfg(feature = "insecure-skip-verify")]
tls_verify,
*self.tls_ca,
self.tls_roots.deref().as_deref(),
tls_roots_password,
)?;
let auth = self.build_auth()?;
let handler = match self.protocol {
#[cfg(feature = "sync-sender-tcp")]
Protocol::Tcp | Protocol::Tcps => connect_tcp(
self.host.as_str(),
self.port.as_str(),
self.net_interface.deref().as_deref(),
*self.auth_timeout,
tls_settings,
&auth,
)?,
#[cfg(feature = "sync-sender-http")]
Protocol::Http | Protocol::Https => {
use ureq::unversioned::transport::Connector;
use ureq::unversioned::transport::TcpConnector;
if self.net_interface.is_some() {
return Err(error::fmt!(
InvalidApiCall,
"net_interface is not supported for ILP over HTTP."
));
}
let http_config = self.http.as_ref().unwrap();
let user_agent = http_config.user_agent.as_str();
let connector = TcpConnector::default();
let agent_builder = ureq::Agent::config_builder()
.user_agent(user_agent)
.no_delay(true);
let tls_config = match tls_settings {
Some(tls_settings) => Some(tls::configure_tls(tls_settings)?),
None => None,
};
let connector = connector.chain(TlsConnector::new(tls_config));
let auth = match auth {
Some(conf::AuthParams::Basic(ref auth)) => Some(auth.to_header_string()),
Some(conf::AuthParams::Token(ref auth)) => Some(auth.to_header_string()?),
#[cfg(feature = "sync-sender-tcp")]
Some(conf::AuthParams::Ecdsa(_)) => {
return Err(fmt!(
AuthError,
"ECDSA authentication is not supported for ILP over HTTP. \
Please use basic or token authentication instead."
));
}
None => None,
};
let agent_builder = agent_builder
.timeout_connect(Some(*http_config.request_timeout.deref()))
.http_status_as_error(false);
let agent = ureq::Agent::with_parts(
agent_builder.build(),
connector,
ureq::unversioned::resolver::DefaultResolver::default(),
);
let proto = self.protocol.schema();
let url = format!(
"{}://{}:{}/write",
proto,
self.host.deref(),
self.port.deref()
);
SyncProtocolHandler::SyncHttp(SyncHttpHandlerState {
agent,
url,
auth,
config: self.http.as_ref().unwrap().clone(),
})
}
#[cfg(feature = "sync-sender-qwp-udp")]
Protocol::Udp => {
let Some(qwp_udp) = self.qwp_udp.as_ref() else {
return Err(error::fmt!(
ConfigError,
"QWP/UDP configuration is missing."
));
};
connect_qwp_udp(
self.host.as_str(),
self.port.as_str(),
self.net_interface.deref().as_deref(),
qwp_udp,
)?
}
#[cfg(feature = "sync-sender-qwp-ws")]
Protocol::Ws | Protocol::Wss => {
if self.net_interface.is_some() {
return Err(error::fmt!(
InvalidApiCall,
"net_interface is not supported for QWP over WebSocket."
));
}
let Some(qwp_ws) = self.qwp_ws.as_ref() else {
return Err(error::fmt!(
ConfigError,
"QWP/WebSocket configuration is missing."
));
};
let actual_initial_connect_retry = qwp_ws.resolve_initial_connect_retry();
let mut qwp_ws = qwp_ws.clone();
qwp_ws.initial_connect_retry =
ConfigSetting::Specified(actual_initial_connect_retry);
let qwp_ws = &qwp_ws;
reject_unsupported_qwp_ws_sf_config(qwp_ws)?;
let basic_auth = qwp_ws_auth_header(&auth)?;
if *qwp_ws.progress == QwpWsProgress::Manual {
if *qwp_ws.initial_connect_retry == conf::QwpWsInitialConnectMode::Async {
return Err(error::fmt!(
ConfigError,
"initial_connect_retry=async requires QWP/WebSocket background progress; use qwp_ws_progress=background or initial_connect_retry=sync"
));
}
SyncProtocolHandler::ManualQwpWs(Box::new(open_manual_qwp_ws(
self.host.as_str(),
self.port.as_str(),
matches!(self.protocol, Protocol::Wss),
tls_settings,
qwp_ws,
basic_auth,
)?))
} else {
connect_qwp_ws(
self.host.as_str(),
self.port.as_str(),
matches!(self.protocol, Protocol::Wss),
tls_settings,
qwp_ws,
basic_auth,
)?
}
}
};
#[allow(unused_mut)]
let mut max_name_len = *self.max_name_len;
let protocol_version = match self.protocol_version.deref() {
Some(v) => *v,
None => match self.protocol {
#[cfg(feature = "sync-sender-tcp")]
Protocol::Tcp | Protocol::Tcps => ProtocolVersion::V1,
#[cfg(feature = "sync-sender-http")]
Protocol::Http | Protocol::Https => {
#[allow(irrefutable_let_patterns)]
if let SyncProtocolHandler::SyncHttp(http_state) = &handler {
let settings_url = &format!(
"{}://{}:{}/settings",
self.protocol.schema(),
self.host.deref(),
self.port.deref()
);
let (protocol_versions, server_max_name_len) =
read_server_settings(http_state, settings_url, max_name_len)?;
max_name_len = server_max_name_len;
SUPPORTED_PROTOCOL_VERSIONS
.iter()
.find(|version| protocol_versions.contains(version))
.copied()
.ok_or_else(|| {
fmt!(
ProtocolVersionError,
"Server does not support any of the client protocol versions: {:?}",
SUPPORTED_PROTOCOL_VERSIONS
)
})?
} else {
unreachable!("HTTP handler should be used for HTTP protocol");
}
}
#[cfg(feature = "sync-sender-qwp-udp")]
Protocol::Udp => ProtocolVersion::V1,
#[cfg(feature = "sync-sender-qwp-ws")]
Protocol::Ws | Protocol::Wss => ProtocolVersion::V1,
},
};
if auth.is_some() {
descr.push_str("auth=on]");
} else {
descr.push_str("auth=off]");
}
let effective_init_buf_size = (*self.init_buf_size).min(*self.max_buf_size);
let sender = Sender::new(
descr,
handler,
effective_init_buf_size,
*self.max_buf_size,
self.protocol,
protocol_version,
max_name_len,
#[cfg(feature = "_sender-qwp-ws")]
self.qwp_ws_error_handler.clone(),
#[cfg(feature = "_sender-qwp-ws")]
self.qwp_ws
.as_ref()
.and_then(|qwp_ws| qwp_ws.conn_events.clone()),
);
Ok(sender)
}
#[cfg(feature = "sync-sender-qwp-ws")]
fn resolve_qwp_ws_ingredients(
&self,
) -> Result<(
bool,
Option<tls::TlsSettings>,
conf::QwpWsConfig,
Option<String>,
)> {
if self.init_buf_size.is_specified() && *self.init_buf_size > *self.max_buf_size {
return Err(error::fmt!(
ConfigError,
"init_buf_size ({}) cannot exceed max_buf_size ({})",
*self.init_buf_size,
*self.max_buf_size
));
}
if !matches!(self.protocol, Protocol::Ws | Protocol::Wss) {
return Err(error::fmt!(
ConfigError,
"Column-sender requires a QWP/WebSocket connect string \
(got protocol {:?})",
self.protocol
));
}
if self.net_interface.is_some() {
return Err(error::fmt!(
InvalidApiCall,
"net_interface is not supported for QWP over WebSocket."
));
}
let Some(qwp_ws) = self.qwp_ws.as_ref() else {
return Err(error::fmt!(
ConfigError,
"QWP/WebSocket configuration is missing."
));
};
#[cfg(feature = "insecure-skip-verify")]
let tls_verify = *self.tls_verify;
let tls_roots_password = self.tls_roots_password.deref().as_deref();
if tls_roots_password.is_some() && self.tls_roots.deref().is_none() {
return Err(error::fmt!(
ConfigError,
"\"tls_roots_password\" requires \"tls_roots\" \
(the password unlocks the keystore at that path)"
));
}
let tls_settings = tls::TlsSettings::build(
self.protocol.tls_enabled(),
#[cfg(feature = "insecure-skip-verify")]
tls_verify,
*self.tls_ca,
self.tls_roots.deref().as_deref(),
tls_roots_password,
)?;
let auth = self.build_auth()?;
let auth_header = qwp_ws_auth_header(&auth)?;
let qwp_ws = qwp_ws.clone();
reject_unsupported_qwp_ws_sf_config(&qwp_ws)?;
if *qwp_ws.progress == QwpWsProgress::Manual
&& *qwp_ws.initial_connect_retry == conf::QwpWsInitialConnectMode::Async
{
return Err(error::fmt!(
ConfigError,
"initial_connect_retry=async requires QWP/WebSocket background progress; use qwp_ws_progress=background or initial_connect_retry=sync"
));
}
let use_tls = matches!(self.protocol, Protocol::Wss);
Ok((use_tls, tls_settings, qwp_ws, auth_header))
}
#[cfg(feature = "sync-sender-qwp-ws")]
pub(crate) fn force_async_initial_connect(&mut self) {
if let Some(qwp_ws) = self.qwp_ws.as_mut() {
qwp_ws.force_async_initial_connect();
}
}
#[cfg(feature = "sync-sender-qwp-ws")]
pub(crate) fn build_qwp_ws_connector(&self) -> Result<QwpWsConnector> {
let (use_tls, tls_settings, qwp_ws, auth_header) = self.resolve_qwp_ws_ingredients()?;
let endpoints = sender::qwp_ws::qwp_ws_configured_endpoints(
self.host.as_str(),
self.port.as_str(),
&qwp_ws,
);
Ok(QwpWsConnector {
host: self.host.to_string(),
port: self.port.to_string(),
endpoints,
use_tls,
tls_settings,
qwp_ws,
auth_header,
max_buf_size: *self.max_buf_size,
})
}
#[cfg(any(feature = "_sender-tcp", feature = "_sender-qwp-udp"))]
fn ensure_supports_bind_interface(&self, param_name: &str) -> Result<()> {
#[cfg(feature = "_sender-tcp")]
if self.protocol.is_tcpx() {
return Ok(());
}
#[cfg(feature = "_sender-qwp-udp")]
if self.protocol.is_qwp_udp() {
return Ok(());
}
#[cfg(feature = "_sender-qwp-udp")]
let supported = "TCP or QWP/UDP";
#[cfg(not(feature = "_sender-qwp-udp"))]
let supported = "TCP";
Err(fmt!(
ConfigError,
"The {param_name:?} setting can only be used with the {supported} protocol."
))
}
}
#[cfg(feature = "_sync-sender")]
fn validate_value<T: AsRef<str>>(value: T) -> Result<T> {
let str_ref = value.as_ref();
for (p, c) in str_ref.chars().enumerate() {
if matches!(c, '\u{0}'..='\u{1f}' | '\u{7f}'..='\u{9f}') {
return Err(error::fmt!(
ConfigError,
"Invalid character {c:?} at position {p}"
));
}
}
Ok(value)
}
#[cfg(feature = "_sync-sender")]
fn parse_conf_value<T>(param_name: &str, str_value: &str) -> Result<T>
where
T: FromStr,
T::Err: std::fmt::Debug,
{
str_value.parse().map_err(|e| {
fmt!(
ConfigError,
"Could not parse {param_name:?} to number: {e:?}"
)
})
}
#[cfg(feature = "_sender-qwp-ws")]
pub(crate) fn initial_connect_retry_value_is_blocking(str_value: &str) -> bool {
matches!(
parse_initial_connect_retry_value(str_value),
Ok(mode) if mode != conf::QwpWsInitialConnectMode::Async
)
}
#[cfg(feature = "_sender-qwp-ws")]
fn parse_initial_connect_retry_value(str_value: &str) -> Result<conf::QwpWsInitialConnectMode> {
if str_value.eq_ignore_ascii_case("on") || str_value.eq_ignore_ascii_case("true") {
return Ok(conf::QwpWsInitialConnectMode::Sync);
}
if str_value.eq_ignore_ascii_case("sync") {
return Ok(conf::QwpWsInitialConnectMode::Sync);
}
if str_value.eq_ignore_ascii_case("off") || str_value.eq_ignore_ascii_case("false") {
return Ok(conf::QwpWsInitialConnectMode::Off);
}
if str_value.eq_ignore_ascii_case("async") {
return Ok(conf::QwpWsInitialConnectMode::Async);
}
Err(error::fmt!(
ConfigError,
"invalid initial_connect_retry [value={str_value}, allowed-values=[on, off, true, false, sync, async]]"
))
}
#[cfg(feature = "_sender-qwp-ws")]
fn parse_size_conf_value(param_name: &str, str_value: &str) -> Result<u64> {
let mut end = str_value.len();
if end == 0 {
return Err(error::fmt!(
ConfigError,
"invalid {param_name} [value={str_value}]"
));
}
let bytes = str_value.as_bytes();
if matches!(bytes[end - 1], b'b' | b'B') {
end -= 1;
}
let multiplier = if end > 0 {
match bytes[end - 1] {
b'k' | b'K' => {
end -= 1;
1024
}
b'm' | b'M' => {
end -= 1;
1024 * 1024
}
b'g' | b'G' => {
end -= 1;
1024 * 1024 * 1024
}
b't' | b'T' => {
end -= 1;
1024_u64 * 1024 * 1024 * 1024
}
_ => 1,
}
} else {
1
};
if end == 0 {
return Err(error::fmt!(
ConfigError,
"invalid {param_name} [value={str_value}]"
));
}
let digits = &str_value[..end];
let value = digits
.parse::<u64>()
.map_err(|_| error::fmt!(ConfigError, "invalid {param_name} [value={str_value}]"))?;
value.checked_mul(multiplier).ok_or_else(|| {
error::fmt!(
ConfigError,
"{param_name} overflows u64 [value={str_value}]"
)
})
}
#[cfg(feature = "_sender-qwp-ws")]
fn parse_sf_durability_value(str_value: &str) -> Result<conf::SfDurability> {
if str_value.eq_ignore_ascii_case("memory") {
return Ok(conf::SfDurability::Memory);
}
if str_value.eq_ignore_ascii_case("periodic") {
return Ok(conf::SfDurability::Periodic);
}
if str_value.eq_ignore_ascii_case("flush") {
return Ok(conf::SfDurability::Flush);
}
if str_value.eq_ignore_ascii_case("append") {
return Ok(conf::SfDurability::Append);
}
Err(error::fmt!(
ConfigError,
"invalid sf_durability [value={str_value}, allowed-values=[memory, periodic, flush, append]]"
))
}
#[cfg(feature = "_sender-qwp-ws")]
fn parse_qwp_ws_progress_value(str_value: &str) -> Result<QwpWsProgress> {
if str_value.eq_ignore_ascii_case("background") {
return Ok(QwpWsProgress::Background);
}
if str_value.eq_ignore_ascii_case("manual") {
return Ok(QwpWsProgress::Manual);
}
Err(error::fmt!(
ConfigError,
"invalid qwp_ws_progress [value={str_value}, allowed-values=[background, manual]]"
))
}
#[cfg(feature = "_sender-qwp-ws")]
fn reject_unsupported_qwp_ws_sf_config(qwp_ws: &conf::QwpWsConfig) -> Result<()> {
if matches!(
*qwp_ws.sf_durability,
conf::SfDurability::Flush | conf::SfDurability::Append
) {
let durability = qwp_ws.sf_durability.as_conf_value();
return Err(error::fmt!(
ConfigError,
"sf_durability={durability} is not yet supported (use sf_durability=memory or periodic)"
));
}
if *qwp_ws.sf_durability == conf::SfDurability::Periodic && qwp_ws.sf_dir.is_none() {
return Err(error::fmt!(
ConfigError,
"sf_durability=periodic requires sf_dir"
));
}
if qwp_ws.sf_sync_interval.is_specified()
&& *qwp_ws.sf_durability != conf::SfDurability::Periodic
{
return Err(error::fmt!(
ConfigError,
"sf_sync_interval_millis requires sf_durability=periodic"
));
}
Ok(())
}
#[cfg(feature = "sync-sender-qwp-ws")]
fn qwp_ws_auth_header(auth: &Option<conf::AuthParams>) -> Result<Option<String>> {
match auth {
Some(conf::AuthParams::Basic(b)) => Ok(Some(b.to_header_string())),
Some(conf::AuthParams::Token(t)) => Ok(Some(t.to_header_string()?)),
#[cfg(feature = "_sender-tcp")]
Some(conf::AuthParams::Ecdsa(_)) => Err(error::fmt!(
AuthError,
"ECDSA authentication is not supported for QWP/WebSocket. \
Use basic or token authentication instead."
)),
None => Ok(None),
}
}
#[cfg(feature = "_sender-tcp")]
fn b64_decode(descr: &'static str, buf: &str) -> Result<Vec<u8>> {
use base64ct::{Base64UrlUnpadded, Encoding};
Base64UrlUnpadded::decode_vec(buf).map_err(|b64_err| {
fmt!(
AuthError,
"Misconfigured ILP authentication keys. Could not decode {}: {}. \
Hint: Check the keys for a possible typo.",
descr,
b64_err
)
})
}
#[cfg(feature = "_sender-tcp")]
fn parse_public_key(pub_key_x: &str, pub_key_y: &str) -> Result<Vec<u8>> {
let mut pub_key_x = b64_decode("public key x", pub_key_x)?;
let mut pub_key_y = b64_decode("public key y", pub_key_y)?;
let mut encoded = Vec::new();
encoded.push(4u8); let pub_key_x_ken = pub_key_x.len();
if pub_key_x_ken > 32 {
return Err(fmt!(
AuthError,
"Misconfigured ILP authentication keys. Public key x is too long. \
Hint: Check the keys for a possible typo."
));
}
let pub_key_y_len = pub_key_y.len();
if pub_key_y_len > 32 {
return Err(fmt!(
AuthError,
"Misconfigured ILP authentication keys. Public key y is too long. \
Hint: Check the keys for a possible typo."
));
}
encoded.resize((32 - pub_key_x_ken) + 1, 0u8);
encoded.append(&mut pub_key_x);
encoded.resize((32 - pub_key_y_len) + 1 + 32, 0u8);
encoded.append(&mut pub_key_y);
Ok(encoded)
}
#[cfg(feature = "_sender-tcp")]
fn parse_key_pair(auth: &conf::EcdsaAuthParams) -> Result<EcdsaKeyPair> {
let private_key = b64_decode("private authentication key", auth.priv_key.as_str())?;
let public_key = parse_public_key(auth.pub_key_x.as_str(), auth.pub_key_y.as_str())?;
#[cfg(feature = "aws-lc-crypto")]
let res = EcdsaKeyPair::from_private_key_and_public_key(
&ECDSA_P256_SHA256_FIXED_SIGNING,
&private_key[..],
&public_key[..],
);
#[cfg(feature = "ring-crypto")]
let res = {
let system_random = SystemRandom::new();
EcdsaKeyPair::from_private_key_and_public_key(
&ECDSA_P256_SHA256_FIXED_SIGNING,
&private_key[..],
&public_key[..],
&system_random,
)
};
res.map_err(|key_rejected| {
fmt!(
AuthError,
"Misconfigured ILP authentication keys: {}. Hint: Check the keys for a possible typo.",
key_rejected
)
})
}
struct DebugBytes<'a>(pub &'a [u8]);
impl Debug for DebugBytes<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "b\"")?;
for &byte in self.0 {
match byte {
0x20..=0x21 | 0x23..=0x5B | 0x5D..=0x7E => {
write!(f, "{}", byte as char)?;
}
b'\n' => write!(f, "\\n")?,
b'\r' => write!(f, "\\r")?,
b'\t' => write!(f, "\\t")?,
b'\\' => write!(f, "\\\\")?,
b'"' => write!(f, "\\\"")?,
b'\0' => write!(f, "\\0")?,
_ => write!(f, "\\x{byte:02x}")?,
}
}
write!(f, "\"")
}
}
#[cfg(all(test, feature = "_sync-sender"))]
mod tests;