mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 06:00:39 +00:00
fix(mysql): preserve JDBC TLS parameter semantics
This commit is contained in:
Generated
+1
-1
@@ -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
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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モード",
|
||||
|
||||
@@ -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 모드",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 模式",
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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"));
|
||||
|
||||
+9
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user