fix(mysql): preserve JDBC TLS parameter semantics

This commit is contained in:
zipg
2026-08-21 19:57:22 +08:00
committed by GitHub
parent 8671a2fb2a
commit 7d5cae6486
17 changed files with 428 additions and 23 deletions
Generated
+1 -1
View File
@@ -5053,7 +5053,7 @@ dependencies = [
[[package]]
name = "mysql_async"
version = "0.37.0"
source = "git+https://github.com/t8y2/mysql_async.git?rev=b615bb0b10b5975182a4aa069cabc3d09021f6ab#b615bb0b10b5975182a4aa069cabc3d09021f6ab"
source = "git+https://github.com/zipg/mysql_async.git?rev=4a9e60a62f5a48e35ef9ccc9fc8277d5012b19dd#4a9e60a62f5a48e35ef9ccc9fc8277d5012b19dd"
dependencies = [
"bytes",
"crossbeam-queue",
+3 -3
View File
@@ -27,9 +27,9 @@ wry = { path = "vendor/wry" }
tokio-postgres = { git = "https://github.com/t8y2/tokio-postgres-gaussdb.git", rev = "ac8ad476de59093d33ab9f294531c956e0c6a724" }
postgres-types = { git = "https://github.com/t8y2/tokio-postgres-gaussdb.git", rev = "ac8ad476de59093d33ab9f294531c956e0c6a724" }
postgres-protocol = { git = "https://github.com/t8y2/tokio-postgres-gaussdb.git", rev = "ac8ad476de59093d33ab9f294531c956e0c6a724" }
# Temporary compatibility patch for MySQL accounts using the deprecated
# sha256_password auth plugin. Remove after upstream support lands.
mysql_async = { git = "https://github.com/t8y2/mysql_async.git", rev = "b615bb0b10b5975182a4aa069cabc3d09021f6ab" }
# Temporary compatibility patches for deprecated sha256_password accounts and
# rustls dangerous-mode connections to legacy certificates. Remove after upstream support lands.
mysql_async = { git = "https://github.com/zipg/mysql_async.git", rev = "4a9e60a62f5a48e35ef9ccc9fc8277d5012b19dd" }
# Preserve readable SQL Server result sets when a row contains an unpaired
# UTF-16 surrogate. Remove after the behavior is available in upstream tiberius.
tiberius = { path = "vendor/tiberius" }
@@ -4529,6 +4529,15 @@ function mysqlTlsModeFromParams(params: string | undefined, ssl: boolean | undef
return "verify_identity";
}
const jdbcUseSsl = getUrlParam(params, "useSSL").trim().toLowerCase();
const jdbcRequireSsl = getUrlParam(params, "requireSSL").trim().toLowerCase();
const jdbcVerifyServerCertificate = getUrlParam(params, "verifyServerCertificate").trim().toLowerCase();
const isTrue = (value: string) => ["true", "1", "yes", "on"].includes(value);
if (isTrue(jdbcVerifyServerCertificate) && (isTrue(jdbcUseSsl) || isTrue(jdbcRequireSsl))) return "verify_ca";
if (isTrue(jdbcRequireSsl)) return "required";
if (["false", "0", "no", "off"].includes(jdbcUseSsl)) return "disabled";
if (isTrue(jdbcUseSsl)) return "preferred";
if (!ssl && getUrlParam(params, "require_ssl").toLowerCase() !== "true") return "disabled";
if (getUrlParam(params, "verify_identity").toLowerCase() === "true") return "verify_identity";
if (getUrlParam(params, "verify_ca").toLowerCase() === "true") return "verify_ca";
@@ -4536,7 +4545,7 @@ function mysqlTlsModeFromParams(params: string | undefined, ssl: boolean | undef
}
function applyMysqlTlsMode(params: string | undefined, mode: string): string {
let next = deleteUrlParams(params, ["ssl-mode", "sslmode", "require_ssl", "verify_ca", "verify_identity"]);
let next = deleteUrlParams(params, ["ssl-mode", "sslmode", "sslMode", "require_ssl", "verify_ca", "verify_identity", "useSSL", "requireSSL", "verifyServerCertificate"]);
if (mode === "disabled") {
return setUrlParam(next, "ssl-mode", "disabled");
}
+1
View File
@@ -574,6 +574,7 @@ export default {
mysqlClientKeyPlaceholder: "/path/to/client.key",
mysqlClientCertHint: "Client certificate and private key must be provided together when MySQL requires mTLS.",
mysqlTlsConnectionFailureHint: "If the MySQL server does not require TLS, edit the connection, set TLS Mode to Disabled, and try again.",
mysqlUnsupportedCertVersionHint: "Strict verification does not support this server certificate version. Replace it with an X.509 v3 certificate, or use Required mode only on a trusted network.",
mysqlClientCertBrowse: "Choose client certificate",
mysqlClientKeyBrowse: "Choose client private key",
postgresSslMode: "TLS Mode",
+1
View File
@@ -549,6 +549,7 @@ export default withEnglishFallback({
mysqlClientKeyPlaceholder: "/ruta/a/client.key",
mysqlClientCertHint: "El certificado y la clave privada deben indicarse juntos si MySQL requiere mTLS.",
mysqlTlsConnectionFailureHint: "Si el servidor MySQL no requiere TLS, edita la conexión, establece el Modo TLS en Deshabilitado e inténtalo de nuevo.",
mysqlUnsupportedCertVersionHint: "La verificación estricta no admite esta versión del certificado del servidor. Sustitúyelo por un certificado X.509 v3 o usa el modo Requerido solo en una red de confianza.",
mysqlClientCertBrowse: "Elegir certificado cliente",
mysqlClientKeyBrowse: "Elegir clave privada cliente",
postgresSslMode: "Modo TLS",
+1
View File
@@ -548,6 +548,7 @@ export default withEnglishFallback({
mysqlClientKeyPlaceholder: "/percorso/per/client.key",
mysqlClientCertHint: "Il certificato client e la chiave privata devono essere forniti insieme quando MySQL richiede mTLS.",
mysqlTlsConnectionFailureHint: "Se il server MySQL non richiede TLS, modifica la connessione, imposta la modalità TLS su Disabilitata e riprova.",
mysqlUnsupportedCertVersionHint: "La verifica rigorosa non supporta questa versione del certificato del server. Sostituiscilo con un certificato X.509 v3 oppure usa la modalità Obbligatoria solo su una rete attendibile.",
mysqlClientCertBrowse: "Scegli certificato client",
mysqlClientKeyBrowse: "Scegli chiave privata client",
postgresSslMode: "Modalità TLS",
+1
View File
@@ -548,6 +548,7 @@ export default withEnglishFallback({
mysqlClientKeyPlaceholder: "/path/to/client.key",
mysqlClientCertHint: "MySQLがmTLSを要求する場合、クライアント証明書と秘密鍵の両方を指定する必要があります。",
mysqlTlsConnectionFailureHint: "MySQLサーバーがTLSを要求しない場合は、接続を編集し、TLSモードを無効にしてから再試行してください。",
mysqlUnsupportedCertVersionHint: "厳密な検証では、このサーバー証明書のバージョンはサポートされていません。X.509 v3証明書に置き換えるか、信頼できるネットワークでのみ「必須」モードを使用してください。",
mysqlClientCertBrowse: "クライアント証明書を選択",
mysqlClientKeyBrowse: "クライアント秘密鍵を選択",
postgresSslMode: "TLSモード",
+1
View File
@@ -568,6 +568,7 @@ export default withEnglishFallback({
mysqlClientKeyPlaceholder: "/path/to/client.key",
mysqlClientCertHint: "MySQL이 mTLS를 요구할 때 클라이언트 인증서와 개인 키를 함께 제공해야 합니다.",
mysqlTlsConnectionFailureHint: "MySQL 서버가 TLS를 요구하지 않으면 연결을 편집하여 TLS 모드를 비활성화로 설정하고 다시 시도하세요.",
mysqlUnsupportedCertVersionHint: "엄격한 검증은 이 서버 인증서 버전을 지원하지 않습니다. X.509 v3 인증서로 교체하거나 신뢰할 수 있는 네트워크에서만 필수 모드를 사용하세요.",
mysqlClientCertBrowse: "클라이언트 인증서 선택",
mysqlClientKeyBrowse: "클라이언트 개인 키 선택",
postgresSslMode: "TLS 모드",
+1
View File
@@ -549,6 +549,7 @@ export default withEnglishFallback({
mysqlClientKeyPlaceholder: "/caminho/para/client.key",
mysqlClientCertHint: "O certificado e a chave privada do cliente devem ser fornecidos juntos quando o MySQL exige mTLS.",
mysqlTlsConnectionFailureHint: "Se o servidor MySQL não exigir TLS, edite a conexão, defina o Modo TLS como Desabilitado e tente novamente.",
mysqlUnsupportedCertVersionHint: "A verificação estrita não oferece suporte a esta versão do certificado do servidor. Substitua-o por um certificado X.509 v3 ou use o modo Obrigatório apenas em uma rede confiável.",
mysqlClientCertBrowse: "Escolher certificado do cliente",
mysqlClientKeyBrowse: "Escolher chave privada do cliente",
postgresSslMode: "Modo TLS",
+1
View File
@@ -499,6 +499,7 @@ export default withEnglishFallback({
mysqlClientKeyPlaceholder: "/path/to/client.key",
mysqlClientCertHint: "MySQL 要求 mTLS 时,客户端证书和私钥必须一起填写。",
mysqlTlsConnectionFailureHint: "如果 MySQL 服务端不要求 TLS,请编辑连接,在 TLS 模式中选择“禁用”后重试。",
mysqlUnsupportedCertVersionHint: "服务端证书版本不受严格校验支持。请更换为 X.509 v3 证书,或仅在可信网络中改用“必需”模式。",
mysqlClientCertBrowse: "选择客户端证书",
mysqlClientKeyBrowse: "选择客户端私钥",
postgresSslMode: "TLS 模式",
+1
View File
@@ -549,6 +549,7 @@ export default withEnglishFallback({
mysqlClientKeyPlaceholder: "/path/to/client.key",
mysqlClientCertHint: "MySQL 要求 mTLS 時,用戶端憑證和私鑰必須一起填寫。",
mysqlTlsConnectionFailureHint: "如果 MySQL 伺服器不要求 TLS,請編輯連線,在 TLS 模式中選擇「停用」後重試。",
mysqlUnsupportedCertVersionHint: "伺服器憑證版本不受嚴格驗證支援。請更換為 X.509 v3 憑證,或僅在可信任網路中改用「必要」模式。",
mysqlClientCertBrowse: "選擇用戶端憑證",
mysqlClientKeyBrowse: "選擇用戶端私鑰",
postgresSslMode: "TLS 模式",
@@ -33,6 +33,7 @@ function jdbcConfig(): ConnectionConfig {
const t = (key: string) => {
if (key === "connection.mysqlTlsConnectionFailureHint") return "Set TLS Mode to Disabled.";
if (key === "connection.mysqlUnsupportedCertVersionHint") return "Replace the certificate or use Required mode.";
if (key === "connection.mysqlMissingPasswordHint") return "No database password was sent.";
if (key === "connection.jdbcMissingRuntimeDependencyHint") return "Install from Maven or import every dependency JAR.";
return key;
@@ -53,6 +54,52 @@ describe("appendConnectionErrorHints", () => {
expect(message).toContain("Set TLS Mode to Disabled.");
});
it("recognizes Connector/J TLS aliases", () => {
const message = appendConnectionErrorHints(mysqlConfig("useSSL=true&requireSSL=true&verifyServerCertificate=true"), "MySQL connection failed: TLS handshake failed", t);
expect(message).toContain("Set TLS Mode to Disabled.");
});
it("treats verifyServerCertificate as verified TLS unless useSSL is disabled", () => {
const verified = appendConnectionErrorHints(mysqlConfig("verifyServerCertificate=true"), "MySQL connection failed: TLS handshake failed", t);
const disabled = appendConnectionErrorHints(mysqlConfig("useSSL=false&requireSSL=true&verifyServerCertificate=true"), "MySQL connection failed: TLS handshake failed", t);
expect(verified).toContain("Set TLS Mode to Disabled.");
expect(disabled).toBe("MySQL connection failed: TLS handshake failed");
});
it("uses the last duplicate Connector/J TLS parameter", () => {
const enabled = appendConnectionErrorHints(mysqlConfig("useSSL=false&useSSL=true"), "MySQL connection failed: TLS handshake failed", t);
const disabled = appendConnectionErrorHints(mysqlConfig("useSSL=true&useSSL=false"), "MySQL connection failed: TLS handshake failed", t);
expect(enabled).toContain("Set TLS Mode to Disabled.");
expect(disabled).toBe("MySQL connection failed: TLS handshake failed");
});
it("prefers native require_ssl over Connector/J TLS aliases", () => {
const disabled = appendConnectionErrorHints(mysqlConfig("require_ssl=false&verifyServerCertificate=true"), "MySQL connection failed: TLS handshake failed", t);
const required = appendConnectionErrorHints(mysqlConfig("require_ssl=true&useSSL=false"), "MySQL connection failed: TLS handshake failed", t);
expect(disabled).toBe("MySQL connection failed: TLS handshake failed");
expect(required).toContain("Set TLS Mode to Disabled.");
});
it("uses the last duplicate native require_ssl value", () => {
const required = appendConnectionErrorHints(mysqlConfig("require_ssl=false&require_ssl=true"), "MySQL connection failed: TLS handshake failed", t);
const disabled = appendConnectionErrorHints(mysqlConfig("require_ssl=true&require_ssl=false"), "MySQL connection failed: TLS handshake failed", t);
expect(required).toContain("Set TLS Mode to Disabled.");
expect(disabled).toBe("MySQL connection failed: TLS handshake failed");
});
it("uses a certificate-specific hint for unsupported certificate versions", () => {
const error = "MySQL connection failed: invalid peer certificate: Other(OtherError(UnsupportedCertVersion))";
const message = appendConnectionErrorHints(mysqlConfig("sslMode=VERIFY_CA"), error, t);
expect(message).toContain("Replace the certificate or use Required mode.");
expect(message).not.toContain("Set TLS Mode to Disabled.");
});
it("does not add the TLS hint when MySQL TLS is disabled", () => {
const message = appendConnectionErrorHints(mysqlConfig("ssl-mode=disabled"), "MySQL connection failed: TLS handshake failed", t);
@@ -12,10 +12,28 @@ function normalizeUrlParamKey(key: string): string {
function urlParamValue(params: URLSearchParams, key: string): string {
const normalizedKey = normalizeUrlParamKey(key);
let result = "";
for (const [paramKey, value] of params.entries()) {
if (normalizeUrlParamKey(paramKey) === normalizedKey) return value;
if (normalizeUrlParamKey(paramKey) === normalizedKey) result = value;
}
return "";
return result;
}
function jdbcUrlParamValue(params: URLSearchParams, key: string): string {
const normalizedKey = key.trim().toLowerCase();
let result = "";
for (const [paramKey, value] of params.entries()) {
if (paramKey.trim().toLowerCase() === normalizedKey) result = value;
}
return result;
}
function hasJdbcUrlParam(params: URLSearchParams, key: string): boolean {
const normalizedKey = key.trim().toLowerCase();
for (const paramKey of params.keys()) {
if (paramKey.trim().toLowerCase() === normalizedKey) return true;
}
return false;
}
function mysqlTlsMode(config: ConnectionConfig): string {
@@ -25,7 +43,19 @@ function mysqlTlsMode(config: ConnectionConfig): string {
if (["disabled", "disable"].includes(mode)) return "disabled";
if (["preferred", "prefer"].includes(mode)) return "preferred";
if (["required", "require", "verify_ca", "verify_identity"].includes(mode)) return mode;
if (config.ssl || ["true", "1", "yes", "on"].includes(urlParamValue(parsed, "require_ssl").trim().toLowerCase())) return "required";
const isTrue = (value: string) => ["true", "1", "yes", "on"].includes(value);
const isFalse = (value: string) => ["false", "0", "no", "off"].includes(value);
const nativeRequireSsl = jdbcUrlParamValue(parsed, "require_ssl").trim().toLowerCase();
if (isFalse(nativeRequireSsl)) return "disabled";
if (isTrue(nativeRequireSsl)) return "required";
const jdbcUseSsl = jdbcUrlParamValue(parsed, "useSSL").trim().toLowerCase();
const jdbcRequireSsl = jdbcUrlParamValue(parsed, "requireSSL").trim().toLowerCase();
const jdbcVerifyServerCertificate = jdbcUrlParamValue(parsed, "verifyServerCertificate").trim().toLowerCase();
if (isFalse(jdbcUseSsl)) return "disabled";
if (isTrue(jdbcVerifyServerCertificate)) return "verify_ca";
if (isTrue(jdbcRequireSsl)) return "required";
if (hasJdbcUrlParam(parsed, "useSSL") || hasJdbcUrlParam(parsed, "requireSSL") || hasJdbcUrlParam(parsed, "verifyServerCertificate")) return "preferred";
if (config.ssl) return "required";
return "disabled";
}
@@ -66,6 +96,9 @@ export function appendConnectionErrorHints(config: ConnectionConfig | undefined,
}
if (mysqlTlsMode(config) === "disabled") return message;
if (!isMysqlTlsLikeFailure(message)) return message;
if (/UnsupportedCertVersion/i.test(message)) {
return appendHint(message, t("connection.mysqlUnsupportedCertVersionHint"));
}
const hint = t("connection.mysqlTlsConnectionFailureHint");
return appendHint(message, hint);
}
@@ -257,6 +257,18 @@ function queryParamValue(params: string, key: string): string | undefined {
return undefined;
}
function queryParamLastValue(params: string, key: string): string | undefined {
let result: string | undefined;
for (const part of params.split(/[&;]/)) {
if (!part) continue;
const [rawKey, ...rest] = part.split("=");
if (decodeUrlPart(rawKey).toLowerCase() === key.toLowerCase()) {
result = decodeUrlPart(rest.join("=")).trim();
}
}
return result;
}
function extractHiveStructuredParams(params: string): { username?: string; password?: string; ssl: boolean; urlParams: string } {
let username: string | undefined;
let password: string | undefined;
@@ -334,10 +346,16 @@ function urlParamsRequireTls(dbType: DatabaseType, params: string): boolean {
}
if (dbType === "mysql") {
const requireSsl = queryParamValue(params, "require_ssl")?.toLowerCase();
const requireSsl = queryParamLastValue(params, "require_ssl")?.toLowerCase();
if (requireSsl === "true" || requireSsl === "1" || requireSsl === "yes") return true;
const sslMode = (queryParamValue(params, "ssl-mode") || queryParamValue(params, "sslmode") || "").toLowerCase().replace("-", "_");
return sslMode === "required" || sslMode === "require" || sslMode === "verify_ca" || sslMode === "verify_identity";
const sslMode = (queryParamLastValue(params, "ssl-mode") || queryParamLastValue(params, "sslmode") || "").toLowerCase().replace("-", "_");
if (sslMode === "required" || sslMode === "require" || sslMode === "verify_ca" || sslMode === "verify_identity") return true;
if (requireSsl !== undefined || sslMode) return false;
const jdbcUseSsl = (queryParamLastValue(params, "useSSL") || "").toLowerCase();
const jdbcRequireSsl = (queryParamLastValue(params, "requireSSL") || "").toLowerCase();
const jdbcVerifyServerCertificate = (queryParamLastValue(params, "verifyServerCertificate") || "").toLowerCase();
if (["false", "0", "no", "off"].includes(jdbcUseSsl)) return false;
return ["true", "1", "yes", "on"].includes(jdbcRequireSsl) || ["true", "1", "yes", "on"].includes(jdbcVerifyServerCertificate);
}
if (dbType === "postgres" || dbType === "redshift" || dbType === "kwdb") {
+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"));
+9
View File
@@ -162,6 +162,15 @@ test("removes only the connection name from URL params", () => {
test("parses mysql TLS URL params into the SSL switch state", () => {
assert.equal(parseConnectionUrl("mysql://root@tidb.example.com:4000/test?ssl-mode=required").ssl, true);
assert.equal(parseConnectionUrl("mysql://root@tidb.example.com:4000/test?require_ssl=true").ssl, true);
assert.equal(parseConnectionUrl("jdbc:mysql://db.example.com/test?useSSL=true&requireSSL=true&verifyServerCertificate=true").ssl, true);
assert.equal(parseConnectionUrl("jdbc:mysql://db.example.com/test?verifyServerCertificate=true").ssl, true);
assert.equal(parseConnectionUrl("jdbc:mysql://db.example.com/test?useSSL=false").ssl, false);
assert.equal(parseConnectionUrl("jdbc:mysql://db.example.com/test?useSSL=false&requireSSL=true&verifyServerCertificate=true").ssl, false);
});
test("uses the last duplicate MySQL JDBC TLS parameter", () => {
assert.equal(parseConnectionUrl("jdbc:mysql://db.example.com/test?useSSL=false&useSSL=true&requireSSL=true").ssl, true);
assert.equal(parseConnectionUrl("jdbc:mysql://db.example.com/test?useSSL=true&useSSL=false&requireSSL=true").ssl, false);
});
test("parses TiDB Cloud MySQL URLs as TLS connections", () => {