fix(mysql): preserve JDBC TLS parameter semantics
This commit is contained in:
17 files changed
+428
-23
No files matched your search
@@ -18,7 +18,10 @@ use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::models::connection::{ConnectionConfig, DatabaseConnectionInfo, DatabaseType};
|
||||
use crate::models::connection::{
|
||||
is_mysql_jdbc_tls_param, mysql_jdbc_tls_mode, ConnectionConfig, DatabaseConnectionInfo, DatabaseType,
|
||||
MysqlJdbcTlsMode,
|
||||
};
|
||||
use crate::schema::{table_name_filter_matches, TableNameFilter};
|
||||
use crate::sql::{starts_with_executable_sql_keyword, starts_with_executable_sql_keyword_for_database};
|
||||
use crate::types::{
|
||||
@@ -1914,7 +1917,14 @@ fn mysql_url_requires_ssl(url: &str) -> bool {
|
||||
let Some((_, query)) = url.split_once('?') else {
|
||||
return false;
|
||||
};
|
||||
query.split('&').any(|segment| {
|
||||
let query = query.split('#').next().unwrap_or(query);
|
||||
let has_native_tls_param = query.split('&').any(|segment| {
|
||||
let key = segment.split_once('=').map(|(key, _)| key).unwrap_or(segment).trim();
|
||||
key.eq_ignore_ascii_case("ssl-mode")
|
||||
|| key.eq_ignore_ascii_case("sslmode")
|
||||
|| key.eq_ignore_ascii_case("require_ssl")
|
||||
});
|
||||
let native_requires_ssl = query.split('&').any(|segment| {
|
||||
let Some((key, value)) = segment.split_once('=') else {
|
||||
return false;
|
||||
};
|
||||
@@ -1928,7 +1938,13 @@ fn mysql_url_requires_ssl(url: &str) -> bool {
|
||||
value.to_ascii_lowercase().replace('-', "_").as_str(),
|
||||
"required" | "require" | "verify_ca" | "verify_identity"
|
||||
))
|
||||
})
|
||||
});
|
||||
native_requires_ssl
|
||||
|| (!has_native_tls_param
|
||||
&& matches!(
|
||||
mysql_jdbc_tls_mode(Some(query)),
|
||||
Some(MysqlJdbcTlsMode::Required | MysqlJdbcTlsMode::VerifyCa)
|
||||
))
|
||||
}
|
||||
|
||||
fn mysql_url_attempts_ssl(url: &str) -> bool {
|
||||
@@ -1939,7 +1955,14 @@ fn mysql_url_attempts_ssl(url: &str) -> bool {
|
||||
let Some((_, query)) = url.split_once('?') else {
|
||||
return false;
|
||||
};
|
||||
query.split('&').any(|segment| {
|
||||
let query = query.split('#').next().unwrap_or(query);
|
||||
let has_native_tls_param = query.split('&').any(|segment| {
|
||||
let key = segment.split_once('=').map(|(key, _)| key).unwrap_or(segment).trim();
|
||||
key.eq_ignore_ascii_case("ssl-mode")
|
||||
|| key.eq_ignore_ascii_case("sslmode")
|
||||
|| key.eq_ignore_ascii_case("require_ssl")
|
||||
});
|
||||
let native_attempts_ssl = query.split('&').any(|segment| {
|
||||
let Some((key, value)) = segment.split_once('=') else {
|
||||
return false;
|
||||
};
|
||||
@@ -1947,7 +1970,13 @@ fn mysql_url_attempts_ssl(url: &str) -> bool {
|
||||
let value = value.trim();
|
||||
(key.eq_ignore_ascii_case("ssl-mode") || key.eq_ignore_ascii_case("sslmode"))
|
||||
&& matches!(value.to_ascii_lowercase().replace('-', "_").as_str(), "preferred" | "prefer")
|
||||
})
|
||||
});
|
||||
native_attempts_ssl
|
||||
|| (!has_native_tls_param
|
||||
&& matches!(
|
||||
mysql_jdbc_tls_mode(Some(query)),
|
||||
Some(MysqlJdbcTlsMode::Preferred | MysqlJdbcTlsMode::Required | MysqlJdbcTlsMode::VerifyCa)
|
||||
))
|
||||
}
|
||||
|
||||
fn mysql_url_verifies_identity(url: &str) -> bool {
|
||||
@@ -2069,6 +2098,13 @@ fn mysql_async_url(url: &str) -> Cow<'_, str> {
|
||||
let mut changed = false;
|
||||
let mut has_catalog = false;
|
||||
let mut enable_cleartext_plugin = false;
|
||||
let has_native_tls_param = query.split('&').any(|segment| {
|
||||
let key = segment.split_once('=').map(|(key, _)| key).unwrap_or(segment).trim();
|
||||
key.eq_ignore_ascii_case("ssl-mode")
|
||||
|| key.eq_ignore_ascii_case("sslmode")
|
||||
|| key.eq_ignore_ascii_case("require_ssl")
|
||||
});
|
||||
let jdbc_tls_mode = mysql_jdbc_tls_mode(Some(query));
|
||||
for segment in query.split('&') {
|
||||
let segment = segment.trim();
|
||||
if segment.is_empty() {
|
||||
@@ -2088,6 +2124,10 @@ fn mysql_async_url(url: &str) -> Cow<'_, str> {
|
||||
enable_cleartext_plugin |= mysql_url_param_value_is_true(value);
|
||||
continue;
|
||||
}
|
||||
if is_mysql_jdbc_tls_param(key) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
if is_dbx_handled_mysql_url_param(key) {
|
||||
changed = true;
|
||||
continue;
|
||||
@@ -2121,6 +2161,22 @@ fn mysql_async_url(url: &str) -> Cow<'_, str> {
|
||||
}
|
||||
filtered.push(segment.to_string());
|
||||
}
|
||||
if !has_native_tls_param {
|
||||
match jdbc_tls_mode {
|
||||
Some(MysqlJdbcTlsMode::Disabled) => filtered.push("require_ssl=false".to_string()),
|
||||
Some(MysqlJdbcTlsMode::Preferred | MysqlJdbcTlsMode::Required) => {
|
||||
filtered.push("require_ssl=true".to_string());
|
||||
filtered.push("verify_ca=false".to_string());
|
||||
filtered.push("verify_identity=false".to_string());
|
||||
}
|
||||
Some(MysqlJdbcTlsMode::VerifyCa) => {
|
||||
filtered.push("require_ssl=true".to_string());
|
||||
filtered.push("verify_ca=true".to_string());
|
||||
filtered.push("verify_identity=false".to_string());
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
if enable_cleartext_plugin {
|
||||
filtered.push("enable_cleartext_plugin=true".to_string());
|
||||
}
|
||||
@@ -7619,9 +7675,58 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_async_url_strips_jdbc_params() {
|
||||
fn mysql_async_url_translates_connector_j_tls_params() {
|
||||
let url = "mysql://host:3306/db?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true";
|
||||
assert_eq!(mysql_async_url(url).as_ref(), "mysql://host:3306/db");
|
||||
assert_eq!(
|
||||
mysql_async_url(url).as_ref(),
|
||||
"mysql://host:3306/db?require_ssl=true&verify_ca=false&verify_identity=false"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_async_url_translates_connector_j_required_verified_tls_params() {
|
||||
let url = "mysql://host:3306/db?useSSL=true&requireSSL=true&verifyServerCertificate=true";
|
||||
assert_eq!(
|
||||
mysql_async_url(url).as_ref(),
|
||||
"mysql://host:3306/db?require_ssl=true&verify_ca=true&verify_identity=false"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_async_url_preserves_connector_j_tls_truth_table() {
|
||||
assert_eq!(
|
||||
mysql_async_url("mysql://host:3306/db?verifyServerCertificate=true").as_ref(),
|
||||
"mysql://host:3306/db?require_ssl=true&verify_ca=true&verify_identity=false"
|
||||
);
|
||||
assert_eq!(
|
||||
mysql_async_url("mysql://host:3306/db?useSSL=false&requireSSL=true&verifyServerCertificate=true").as_ref(),
|
||||
"mysql://host:3306/db?require_ssl=false"
|
||||
);
|
||||
assert_eq!(
|
||||
mysql_async_url("mysql://host:3306/db?requireSSL=false").as_ref(),
|
||||
"mysql://host:3306/db?require_ssl=true&verify_ca=false&verify_identity=false"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_async_url_uses_last_connector_j_tls_param_value() {
|
||||
assert_eq!(
|
||||
mysql_async_url("mysql://host:3306/db?useSSL=false&useSSL=true").as_ref(),
|
||||
"mysql://host:3306/db?require_ssl=true&verify_ca=false&verify_identity=false"
|
||||
);
|
||||
assert_eq!(
|
||||
mysql_async_url("mysql://host:3306/db?useSSL=true&useSSL=false&verifyServerCertificate=true").as_ref(),
|
||||
"mysql://host:3306/db?require_ssl=false"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_async_url_prefers_native_tls_mode_over_connector_j_aliases() {
|
||||
let url = "mysql://host:3306/db?sslMode=REQUIRED&useSSL=false&requireSSL=false";
|
||||
assert_eq!(
|
||||
mysql_async_url(url).as_ref(),
|
||||
"mysql://host:3306/db?require_ssl=true&verify_ca=false&verify_identity=false"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -7630,6 +7735,19 @@ mod tests {
|
||||
assert_eq!(mysql_async_url(url).as_ref(), "mysql://host:3306/db?require_ssl=true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connector_j_preferred_tls_falls_back_but_required_tls_does_not() {
|
||||
assert_eq!(
|
||||
ssl_fallback_url("mysql://host:3306/db?useSSL=true&characterEncoding=utf8"),
|
||||
Some("mysql://host:3306/db?useSSL=true&characterEncoding=utf8&ssl-mode=disabled".to_string())
|
||||
);
|
||||
assert_eq!(ssl_fallback_url("mysql://host:3306/db?useSSL=true&requireSSL=true"), None);
|
||||
assert_eq!(ssl_fallback_url("mysql://host:3306/db?verifyServerCertificate=true"), None);
|
||||
let disabled = "mysql://host:3306/db?useSSL=false&requireSSL=true&verifyServerCertificate=true";
|
||||
assert!(!mysql_url_requires_ssl(disabled));
|
||||
assert!(!mysql_url_attempts_ssl(disabled));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_async_url_accepts_reported_doris_jdbc_params() {
|
||||
let url = "mysql://host:9030/db?useLocalSessionState=true&rewriteBatchedStatements=true&prepStmtCacheSqlLimit=2048&prepStmtCacheSize=250&sessionVariables=query_timeout%3D60";
|
||||
|
||||
@@ -1738,7 +1738,11 @@ fn mysql_tls_file_param_is(key: &str, target: &str) -> bool {
|
||||
}
|
||||
|
||||
fn mysql_url_params_tls_disabled(params: Option<&str>) -> bool {
|
||||
params.unwrap_or("").trim().trim_start_matches('?').split('&').any(|part| {
|
||||
let params = params.unwrap_or("").trim().trim_start_matches('?');
|
||||
let has_native_tls_param = params.split('&').any(|part| {
|
||||
url_param_key_is(part, "ssl-mode") || url_param_key_is(part, "sslmode") || url_param_key_is(part, "require_ssl")
|
||||
});
|
||||
let native_tls_disabled = params.split('&').any(|part| {
|
||||
let part = part.trim();
|
||||
if part.is_empty() {
|
||||
return false;
|
||||
@@ -1751,11 +1755,17 @@ fn mysql_url_params_tls_disabled(params: Option<&str>) -> bool {
|
||||
(key.eq_ignore_ascii_case("require_ssl") && value.eq_ignore_ascii_case("false"))
|
||||
|| ((key.eq_ignore_ascii_case("ssl-mode") || key.eq_ignore_ascii_case("sslmode"))
|
||||
&& matches!(value.to_ascii_lowercase().replace('-', "_").as_str(), "disabled" | "disable"))
|
||||
})
|
||||
});
|
||||
native_tls_disabled
|
||||
|| (!has_native_tls_param && mysql_jdbc_tls_mode(Some(params)) == Some(MysqlJdbcTlsMode::Disabled))
|
||||
}
|
||||
|
||||
fn mysql_url_params_require_tls(params: Option<&str>) -> bool {
|
||||
params.unwrap_or("").trim().trim_start_matches('?').split('&').any(|part| {
|
||||
let params = params.unwrap_or("").trim().trim_start_matches('?');
|
||||
let has_native_tls_param = params.split('&').any(|part| {
|
||||
url_param_key_is(part, "ssl-mode") || url_param_key_is(part, "sslmode") || url_param_key_is(part, "require_ssl")
|
||||
});
|
||||
let native_requires_tls = params.split('&').any(|part| {
|
||||
let part = part.trim();
|
||||
if part.is_empty() {
|
||||
return false;
|
||||
@@ -1773,7 +1783,71 @@ fn mysql_url_params_require_tls(params: Option<&str>) -> bool {
|
||||
value.to_ascii_lowercase().replace('-', "_").as_str(),
|
||||
"required" | "require" | "verify_ca" | "verify_identity"
|
||||
))
|
||||
})
|
||||
});
|
||||
native_requires_tls
|
||||
|| (!has_native_tls_param
|
||||
&& matches!(
|
||||
mysql_jdbc_tls_mode(Some(params)),
|
||||
Some(MysqlJdbcTlsMode::Required | MysqlJdbcTlsMode::VerifyCa)
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum MysqlJdbcTlsMode {
|
||||
Disabled,
|
||||
Preferred,
|
||||
Required,
|
||||
VerifyCa,
|
||||
}
|
||||
|
||||
pub(crate) fn is_mysql_jdbc_tls_param(key: &str) -> bool {
|
||||
matches!(
|
||||
percent_decode_str(key).decode_utf8_lossy().trim().to_ascii_lowercase().as_str(),
|
||||
"usessl" | "requiressl" | "verifyservercertificate"
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn mysql_jdbc_tls_mode(params: Option<&str>) -> Option<MysqlJdbcTlsMode> {
|
||||
let mut has_jdbc_tls_param = false;
|
||||
let mut use_ssl = None;
|
||||
let mut require_ssl = None;
|
||||
let mut verify_server_certificate = None;
|
||||
|
||||
for part in params.unwrap_or("").trim().trim_start_matches('?').split('&') {
|
||||
let Some((raw_key, raw_value)) = part.split_once('=') else {
|
||||
continue;
|
||||
};
|
||||
let key = percent_decode_str(raw_key).decode_utf8_lossy().trim().to_ascii_lowercase();
|
||||
let value = percent_decode_str(raw_value).decode_utf8_lossy();
|
||||
let parsed_value = mysql_url_param_value_is_true(&value);
|
||||
match key.as_str() {
|
||||
"usessl" => {
|
||||
has_jdbc_tls_param = true;
|
||||
use_ssl = Some(parsed_value);
|
||||
}
|
||||
"requiressl" => {
|
||||
has_jdbc_tls_param = true;
|
||||
require_ssl = Some(parsed_value);
|
||||
}
|
||||
"verifyservercertificate" => {
|
||||
has_jdbc_tls_param = true;
|
||||
verify_server_certificate = Some(parsed_value);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if !has_jdbc_tls_param {
|
||||
None
|
||||
} else if use_ssl == Some(false) {
|
||||
Some(MysqlJdbcTlsMode::Disabled)
|
||||
} else if verify_server_certificate == Some(true) {
|
||||
Some(MysqlJdbcTlsMode::VerifyCa)
|
||||
} else if require_ssl == Some(true) {
|
||||
Some(MysqlJdbcTlsMode::Required)
|
||||
} else {
|
||||
Some(MysqlJdbcTlsMode::Preferred)
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_bare_mysql_url_params(value: &str) -> String {
|
||||
@@ -1804,6 +1878,10 @@ fn mysql_url_param_value_is_true(value: &str) -> bool {
|
||||
fn normalize_mysql_url_params(value: &str, force_tls: bool, accept_invalid_certs: bool) -> String {
|
||||
let value = value.trim_start_matches('?');
|
||||
let mut parts: Vec<String> = value.split('&').filter(|part| !part.is_empty()).map(str::to_string).collect();
|
||||
let jdbc_tls_mode = mysql_jdbc_tls_mode(Some(value));
|
||||
let has_native_tls_param = parts.iter().any(|part| {
|
||||
url_param_key_is(part, "ssl-mode") || url_param_key_is(part, "sslmode") || url_param_key_is(part, "require_ssl")
|
||||
});
|
||||
let enable_cleartext_plugin = parts.iter().any(|part| {
|
||||
let Some((key, value)) = part.split_once('=') else {
|
||||
return false;
|
||||
@@ -1815,9 +1893,27 @@ fn normalize_mysql_url_params(value: &str, force_tls: bool, accept_invalid_certs
|
||||
let Some((key, _)) = part.split_once('=') else {
|
||||
return true;
|
||||
};
|
||||
!is_mysql_cleartext_password_param(key.trim())
|
||||
!is_mysql_cleartext_password_param(key.trim()) && !is_mysql_jdbc_tls_param(key.trim())
|
||||
});
|
||||
|
||||
if !has_native_tls_param {
|
||||
match jdbc_tls_mode {
|
||||
Some(MysqlJdbcTlsMode::Disabled) => parts.push("ssl-mode=disabled".to_string()),
|
||||
Some(MysqlJdbcTlsMode::Preferred) => parts.push("ssl-mode=preferred".to_string()),
|
||||
Some(MysqlJdbcTlsMode::Required) => {
|
||||
parts.push("require_ssl=true".to_string());
|
||||
parts.push("verify_ca=false".to_string());
|
||||
parts.push("verify_identity=false".to_string());
|
||||
}
|
||||
Some(MysqlJdbcTlsMode::VerifyCa) => {
|
||||
parts.push("require_ssl=true".to_string());
|
||||
parts.push("verify_ca=true".to_string());
|
||||
parts.push("verify_identity=false".to_string());
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
if force_tls {
|
||||
parts.retain(|part| {
|
||||
!url_param_key_is(part, "ssl-mode")
|
||||
@@ -3467,6 +3563,73 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_connector_j_tls_params_are_normalized() {
|
||||
let mut config = mysql_config("root", "secret", Some("test"));
|
||||
config.url_params = Some("useSSL=true&requireSSL=true&verifyServerCertificate=true".to_string());
|
||||
|
||||
assert_eq!(
|
||||
config.connection_url(),
|
||||
"mysql://root:secret@10.1.2.3:2883/test?require_ssl=true&verify_ca=true&verify_identity=false&charset=utf8mb4"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_connector_j_preferred_and_disabled_tls_modes_are_preserved() {
|
||||
let mut config = mysql_config("root", "secret", Some("test"));
|
||||
config.url_params = Some("useSSL=true".to_string());
|
||||
assert_eq!(
|
||||
config.connection_url(),
|
||||
"mysql://root:secret@10.1.2.3:2883/test?ssl-mode=preferred&charset=utf8mb4"
|
||||
);
|
||||
|
||||
config.url_params = Some("useSSL=false".to_string());
|
||||
assert_eq!(config.connection_url(), "mysql://root:secret@10.1.2.3:2883/test?ssl-mode=disabled&charset=utf8mb4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_connector_j_tls_truth_table_preserves_legacy_precedence() {
|
||||
let mut config = mysql_config("root", "secret", Some("test"));
|
||||
config.url_params = Some("verifyServerCertificate=true".to_string());
|
||||
assert_eq!(
|
||||
config.connection_url(),
|
||||
"mysql://root:secret@10.1.2.3:2883/test?require_ssl=true&verify_ca=true&verify_identity=false&charset=utf8mb4"
|
||||
);
|
||||
|
||||
config.url_params = Some("useSSL=false&requireSSL=true&verifyServerCertificate=true".to_string());
|
||||
assert_eq!(config.connection_url(), "mysql://root:secret@10.1.2.3:2883/test?ssl-mode=disabled&charset=utf8mb4");
|
||||
|
||||
config.url_params = Some("requireSSL=false".to_string());
|
||||
assert_eq!(
|
||||
config.connection_url(),
|
||||
"mysql://root:secret@10.1.2.3:2883/test?ssl-mode=preferred&charset=utf8mb4"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_connector_j_duplicate_tls_params_use_the_last_value() {
|
||||
let mut config = mysql_config("root", "secret", Some("test"));
|
||||
config.url_params = Some("useSSL=false&useSSL=true".to_string());
|
||||
assert_eq!(
|
||||
config.connection_url(),
|
||||
"mysql://root:secret@10.1.2.3:2883/test?ssl-mode=preferred&charset=utf8mb4"
|
||||
);
|
||||
|
||||
config.url_params = Some("useSSL=true&useSSL=false&verifyServerCertificate=true".to_string());
|
||||
assert_eq!(config.connection_url(), "mysql://root:secret@10.1.2.3:2883/test?ssl-mode=disabled&charset=utf8mb4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_native_tls_mode_takes_precedence_over_connector_j_aliases() {
|
||||
let mut config = mysql_config("root", "secret", Some("test"));
|
||||
config.url_params = Some("sslMode=REQUIRED&useSSL=false".to_string());
|
||||
|
||||
assert_eq!(
|
||||
config.connection_url(),
|
||||
"mysql://root:secret@10.1.2.3:2883/test?require_ssl=true&verify_ca=false&verify_identity=false&charset=utf8mb4"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_tls_switch_uses_ca_cert_for_ca_validation() {
|
||||
let mut config = mysql_config("root", "secret", Some("test"));
|
||||
|
||||
Reference in new issue
Block a user