fix(mysql): preserve JDBC TLS parameter semantics

This commit is contained in:
zipg authored and GitHub committed 2026-08-21 19:57:22 +08:00
1 parent 8671a2fb2a
commit 7d5cae6486
17 files changed
+428 -23

No files matched your search

+125 -7
View File
@@ -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";
+168 -5
View File
@@ -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"));