Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 205 additions & 2 deletions core/common/src/types/configuration/auth_config/connection_string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,213 @@ impl ConnectionStringUtils {
#[cfg(test)]
mod tests {
use super::*;
use crate::NonZeroIggyDuration;
use crate::TcpConnectionStringOptions;
use crate::{
IggyDuration, NonZeroIggyDuration, QuicClientConfig, QuicConnectionStringOptions,
TcpClientConfig, TcpConnectionStringOptions, WebSocketClientConfig,
WebSocketConnectionStringOptions,
};
use secrecy::ExposeSecret;

#[test]
fn should_parse_tcp_canonical_options_and_legacy_aliases() {
for (case, query, expected) in [
(
"canonical keys",
"reconnection_max_retries=3&reestablish_after=7s&tls_validate_certificate=true",
Some(3),
),
(
"deprecated aliases",
"reconnection_retries=3&reestablish_after=7s&tls_validate_certificate=true",
Some(3),
),
] {
let value = format!("iggy+tcp://user:secret@localhost:8090?{query}");
let config = TcpClientConfig::from(
ConnectionString::<TcpConnectionStringOptions>::new(&value)
.unwrap_or_else(|error| panic!("{case}: {error}")),
);
assert_eq!(config.reconnection.max_retries, expected, "{case}");
assert_eq!(
config.reconnection.reestablish_after,
IggyDuration::from_str("7s").unwrap(),
"{case}"
);
assert!(config.tls_validate_certificate, "{case}");
}
}

#[test]
fn should_use_tcp_defaults_without_options() {
let value = "iggy+tcp://user:secret@localhost:8090";
let config = TcpClientConfig::from(
ConnectionString::<TcpConnectionStringOptions>::new(value).unwrap(),
);
assert_eq!(config.reconnection.max_retries, None);
assert_eq!(
config.reconnection.reestablish_after,
IggyDuration::from_str("5s").unwrap()
);
assert!(config.tls_validate_certificate);
}

#[test]
fn should_parse_tcp_disabled_certificate_validation() {
let value = "iggy+tcp://user:secret@localhost:8090?tls_validate_certificate=false";
let config = TcpClientConfig::from(
ConnectionString::<TcpConnectionStringOptions>::new(value).unwrap(),
);
assert!(!config.tls_validate_certificate);
}

#[test]
fn should_parse_quic_canonical_options_and_legacy_aliases() {
for (case, query, expected) in [
(
"canonical keys",
"reconnection_max_retries=3&reestablish_after=7s&tls_validate_certificate=true",
Some(3),
),
(
"deprecated aliases",
"reconnection_max_retries=3&reconnection_reestablish_after=7s&validate_certificate=true",
Some(3),
),
] {
let value = format!("iggy+quic://user:secret@localhost:8090?{query}");
let config = QuicClientConfig::from(
ConnectionString::<QuicConnectionStringOptions>::new(&value)
.unwrap_or_else(|error| panic!("{case}: {error}")),
);
assert_eq!(config.reconnection.max_retries, expected, "{case}");
assert_eq!(
config.reconnection.reestablish_after,
IggyDuration::from_str("7s").unwrap(),
"{case}"
);
assert!(config.validate_certificate, "{case}");
}
}

#[test]
fn should_parse_websocket_canonical_options_and_legacy_aliases() {
for (case, query, expected) in [
(
"canonical keys",
"reconnection_max_retries=3&reestablish_after=7s&tls_validate_certificate=true",
Some(3),
),
(
"deprecated aliases",
"reconnection_retries=3&reestablish_after=7s&tls_validate_certificate=true",
Some(3),
),
] {
let value = format!("iggy+ws://user:secret@localhost:8090?{query}");
let config = WebSocketClientConfig::from(
ConnectionString::<WebSocketConnectionStringOptions>::new(&value)
.unwrap_or_else(|error| panic!("{case}: {error}")),
);
assert_eq!(config.reconnection.max_retries, expected, "{case}");
assert_eq!(
config.reconnection.reestablish_after,
IggyDuration::from_str("7s").unwrap(),
"{case}"
);
assert!(config.tls_validate_certificate, "{case}");
}
}

#[test]
fn should_use_last_connection_option_including_aliases() {
for (case, query, expected) in [
(
"canonical retries last",
"reconnection_retries=2&reconnection_max_retries=3",
Some(3),
),
(
"deprecated retries last",
"reconnection_max_retries=3&reconnection_retries=2",
Some(2),
),
(
"canonical unlimited retries last",
"reconnection_retries=3&reconnection_max_retries=unlimited",
None,
),
(
"deprecated unlimited retries last",
"reconnection_max_retries=3&reconnection_retries=unlimited",
None,
),
] {
assert_eq!(
TcpConnectionStringOptions::parse_options(query)
.unwrap_or_else(|error| panic!("{case}: {error}"))
.retries(),
expected,
"{case}"
);
assert_eq!(
WebSocketConnectionStringOptions::parse_options(query)
.unwrap_or_else(|error| panic!("{case}: {error}"))
.retries(),
expected,
"{case}"
);
}
for (case, query, expected) in [
(
"canonical cooldown last",
"reconnection_reestablish_after=2s&reestablish_after=3s",
"3s",
),
(
"deprecated cooldown last",
"reestablish_after=3s&reconnection_reestablish_after=2s",
"2s",
),
] {
let options = QuicConnectionStringOptions::parse_options(query)
.unwrap_or_else(|error| panic!("{case}: {error}"));
assert_eq!(
options.reconnection().reestablish_after,
IggyDuration::from_str(expected).unwrap(),
"{case}"
);
}
for (case, query, expected) in [
(
"canonical certificate validation last",
"validate_certificate=false&tls_validate_certificate=true",
true,
),
(
"deprecated certificate validation last",
"tls_validate_certificate=true&validate_certificate=false",
false,
),
] {
assert_eq!(
QuicConnectionStringOptions::parse_options(query)
.unwrap_or_else(|error| panic!("{case}: {error}"))
.validate_certificate(),
expected,
"{case}"
);
}
}

#[test]
fn should_reject_invalid_tcp_certificate_validation() {
assert_eq!(
TcpConnectionStringOptions::parse_options("tls_validate_certificate=invalid")
.unwrap_err(),
IggyError::InvalidConnectionString
);
}

#[test]
fn should_fail_without_username() {
let server_address = "127.0.0.1";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,13 @@ impl ConnectionStringOptions for QuicConnectionStringOptions {
return Err(IggyError::InvalidConnectionString);
}
},
"validate_certificate" => {
"tls_validate_certificate" | "validate_certificate" => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: the key is tls_validate_certificate now, but the quic field and builder still say validate_certificate (quic_client_config.rs:54, quic_client_config_builder.rs:147), while tcp and websocket say tls_validate_certificate. renaming breaks the api, so either note it on #4210 or leave it for a follow-up.

// TODO: Remove the deprecated `validate_certificate` alias after the compatibility release.
if option_parts[0] == "validate_certificate" {
tracing::warn!(
"Connection string option 'validate_certificate' is deprecated; use 'tls_validate_certificate'"
);
}
validate_certificate = option_parts[1] == "true";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: any value other than an exact true reads as false, so a typo like tls_validate_certificate=ture turns certificate verification off and lands on SkipServerVerification at quic_client.rs:1198. parse the value with parse::<bool>() and reject the rest, like tcp does at tcp_connection_string_options.rs:97.

also at websocket_connection_string_options.rs:191.

}
"heartbeat_interval" => {
Expand All @@ -187,7 +193,13 @@ impl ConnectionStringOptions for QuicConnectionStringOptions {
"reconnection_interval" => {
reconnection_interval = option_parts[1].to_string();
}
"reconnection_reestablish_after" => {
"reestablish_after" | "reconnection_reestablish_after" => {
// TODO: Remove the deprecated `reconnection_reestablish_after` alias after the compatibility release.
if option_parts[0] == "reconnection_reestablish_after" {
tracing::warn!(
"Connection string option 'reconnection_reestablish_after' is deprecated; use 'reestablish_after'"
);
}
reconnection_reestablish_after = option_parts[1].to_string();
}
_ => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,7 @@ impl From<ConnectionString<TcpConnectionStringOptions>> for TcpClientConfig {
tls_enabled: connection_string.options().tls_enabled(),
tls_domain: connection_string.options().tls_domain().into(),
tls_ca_file: connection_string.options().tls_ca_file().to_owned(),
// Require certificate verification, including when trusting a private CA.
tls_validate_certificate: true,
tls_validate_certificate: connection_string.options().tls_validate_certificate(),
reconnection: connection_string.options().reconnection().to_owned(),
heartbeat_interval: connection_string.options().heartbeat_interval(),
nodelay: connection_string.options().nodelay(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub struct TcpConnectionStringOptions {
tls_enabled: bool,
tls_domain: String,
tls_ca_file: Option<String>,
tls_validate_certificate: bool,
reconnection: TcpClientReconnectionConfig,
heartbeat_interval: NonZeroIggyDuration,
nodelay: bool,
Expand All @@ -44,6 +45,10 @@ impl TcpConnectionStringOptions {
&self.tls_ca_file
}

pub fn tls_validate_certificate(&self) -> bool {
self.tls_validate_certificate
}

pub fn reconnection(&self) -> &TcpClientReconnectionConfig {
&self.reconnection
}
Expand All @@ -67,6 +72,7 @@ impl ConnectionStringOptions for TcpConnectionStringOptions {
let mut tls_enabled = false;
let mut tls_domain = "".to_string();
let mut tls_ca_file = None;
let mut tls_validate_certificate = true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: this key defaults to true on tcp and false on quic and websocket (quic_connection_string_options.rs:100, websocket_connection_string_options.rs:215), so one key now means two security postures. the rustdoc covers both, but foreign/cpp/include/iggy.hpp:2067 states it for tcp only.

let mut reconnection_retries = "unlimited".to_owned();
let mut reconnection_interval = "1s".to_owned();
let mut reestablish_after = "5s".to_owned();
Expand All @@ -88,7 +94,19 @@ impl ConnectionStringOptions for TcpConnectionStringOptions {
"tls_ca_file" => {
tls_ca_file = Some(option_parts[1].to_string());
}
"tls_validate_certificate" => {
tls_validate_certificate = option_parts[1]
.parse()
.map_err(|_| IggyError::InvalidConnectionString)?;
}
"reconnection_max_retries" => {
reconnection_retries = option_parts[1].to_string();
}
// TODO: Remove the deprecated `reconnection_retries` alias after the compatibility release.
"reconnection_retries" => {
tracing::warn!(
"Connection string option 'reconnection_retries' is deprecated; use 'reconnection_max_retries'"
);
reconnection_retries = option_parts[1].to_string();
}
"reconnection_interval" => {
Expand Down Expand Up @@ -128,7 +146,7 @@ impl ConnectionStringOptions for TcpConnectionStringOptions {
let heartbeat_interval = NonZeroIggyDuration::from_str(heartbeat_interval.as_str())
.map_err(|_| IggyError::InvalidConnectionString)?;

let connection_string_options = TcpConnectionStringOptions::new(
let mut connection_string_options = TcpConnectionStringOptions::new(
tls_enabled,
tls_domain,
tls_ca_file,
Expand All @@ -137,6 +155,7 @@ impl ConnectionStringOptions for TcpConnectionStringOptions {
nodelay,
);

connection_string_options.tls_validate_certificate = tls_validate_certificate;
Ok(connection_string_options)
}
}
Expand All @@ -154,6 +173,7 @@ impl TcpConnectionStringOptions {
tls_enabled,
tls_domain,
tls_ca_file,
tls_validate_certificate: true,
reconnection,
heartbeat_interval,
nodelay,
Expand All @@ -167,6 +187,7 @@ impl Default for TcpConnectionStringOptions {
tls_enabled: false,
tls_domain: "".to_string(),
tls_ca_file: None,
tls_validate_certificate: true,
reconnection: Default::default(),
heartbeat_interval: NonZeroIggyDuration::from_str("5s").unwrap(),
nodelay: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,13 @@ impl ConnectionStringOptions for WebSocketConnectionStringOptions {
parsed_options.heartbeat_interval = NonZeroIggyDuration::from_str(parts[1])
.map_err(|_| IggyError::InvalidConnectionString)?;
}
"reconnection_retries" => {
"reconnection_max_retries" | "reconnection_retries" => {
// TODO: Remove the deprecated `reconnection_retries` alias after the compatibility release.
if parts[0] == "reconnection_retries" {
tracing::warn!(
"Connection string option 'reconnection_retries' is deprecated; use 'reconnection_max_retries'"
);
}
let retries = match parts[1] {
"unlimited" => None,
val => Some(
Expand Down
Loading
Loading