From 8364f468a0c92b377586ede972e900163edd15c5 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 7 Jul 2026 22:50:43 +0800 Subject: [PATCH] fix(sqlserver): use legacy encryption for fallback --- .../connection/ConnectionDialog.vue | 14 +++++-- apps/desktop/src/i18n/locales/en.ts | 4 +- apps/desktop/src/i18n/locales/es.ts | 4 +- apps/desktop/src/i18n/locales/it.ts | 4 +- apps/desktop/src/i18n/locales/ja.ts | 4 +- apps/desktop/src/i18n/locales/pt-BR.ts | 4 +- apps/desktop/src/i18n/locales/zh-CN.ts | 4 +- apps/desktop/src/i18n/locales/zh-TW.ts | 4 +- crates/dbx-core/src/db/sqlserver.rs | 42 +++++++++++++------ 9 files changed, 54 insertions(+), 30 deletions(-) diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index df638f3d53..026d148e7d 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -1023,8 +1023,10 @@ function isSqlServerLegacyUnencryptedMode(params: string | undefined): boolean { if (!normalized) return false; const parsed = new URLSearchParams(normalized); for (const [key, value] of parsed.entries()) { - if (key.trim().toLowerCase() === "sqlserverencryption") { - return ["disabled", "disable", "false", "0", "off"].includes(value.trim().toLowerCase()); + const normalizedKey = key.trim().toLowerCase(); + if (normalizedKey === "sqlserverencryption" || normalizedKey === "encrypt") { + // Accept JDBC-style `encrypt=false` from imported SQL Server URLs as the same opt-in. + if (["disabled", "disable", "false", "0", "off", "no"].includes(value.trim().toLowerCase())) return true; } } return false; @@ -1032,7 +1034,13 @@ function isSqlServerLegacyUnencryptedMode(params: string | undefined): boolean { function setSqlServerLegacyUnencryptedMode(params: string | undefined, enabled: boolean): string { const normalized = (params || "").trim().replace(/^\?/, "").replace(/;/g, "&"); - return setUrlParam(normalized, "sqlserverEncryption", enabled ? "disabled" : ""); + const parsed = new URLSearchParams(normalized); + for (const key of Array.from(parsed.keys())) { + const normalizedKey = key.trim().toLowerCase(); + if (normalizedKey === "sqlserverencryption" || normalizedKey === "encrypt") parsed.delete(key); + } + if (enabled) parsed.set("sqlserverEncryption", "disabled"); + return parsed.toString(); } function isSqlServerTlsHandshakeFailure(message: string): boolean { diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 7027d67b91..19ff9a2a20 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -305,9 +305,9 @@ export default { testing: "Testing...", copyTestResult: "Copy test result", sqlServerLegacyUnencryptedMode: "Legacy compatibility mode", - sqlServerLegacyUnencryptedModeEnable: "Use SQL Server legacy TLS 1.0 unencrypted connection", + sqlServerLegacyUnencryptedModeEnable: "Use SQL Server legacy unencrypted connection", sqlServerLegacyUnencryptedModeEnabled: "SQL Server legacy compatibility mode is enabled.", - sqlServerLegacyUnencryptedModeHint: "Applies to unencrypted transport or login-only encryption. It will still fail if the server requires TLS 1.0 encryption.\nUse it only on trusted networks, VPNs, or SSH tunnels", + sqlServerLegacyUnencryptedModeHint: "Applies to unencrypted transport or login-only encryption. It will still fail if the server requires encrypted transport that the embedded driver cannot negotiate.\nUse it only on trusted networks, VPNs, or SSH tunnels", saveAndConnect: "Save & Connect", save: "Save", editTitle: "Edit Connection", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index d65a3e0386..ce89a3b361 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -307,9 +307,9 @@ export default withEnglishFallback({ testing: "Probando...", copyTestResult: "Copiar resultado de prueba", sqlServerLegacyUnencryptedMode: "Modo de compatibilidad heredado", - sqlServerLegacyUnencryptedModeEnable: "Usar conexión SQL Server heredada TLS 1.0 sin cifrar", + sqlServerLegacyUnencryptedModeEnable: "Usar conexión SQL Server heredada sin cifrar", sqlServerLegacyUnencryptedModeEnabled: "El modo de compatibilidad heredado de SQL Server está activado.", - sqlServerLegacyUnencryptedModeHint: "Aplica a transporte sin cifrar o cifrado solo en el login. Seguirá fallando si el servidor requiere cifrado TLS 1.0.\nÚsalo solo en redes de confianza, VPN o túneles SSH", + sqlServerLegacyUnencryptedModeHint: "Aplica a transporte sin cifrar o cifrado solo en el login. Seguirá fallando si el servidor requiere transporte cifrado que el controlador integrado no puede negociar.\nÚsalo solo en redes de confianza, VPN o túneles SSH", saveAndConnect: "Guardar y conectar", save: "Guardar", editTitle: "Editar conexión", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index a46a892410..a0884cd0b5 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -306,9 +306,9 @@ export default withEnglishFallback({ testing: "Verifica in corso...", copyTestResult: "Copia risultato test", sqlServerLegacyUnencryptedMode: "Modalità compatibilità legacy", - sqlServerLegacyUnencryptedModeEnable: "Usa connessione non crittografata legacy TLS 1.0 SQL Server", + sqlServerLegacyUnencryptedModeEnable: "Usa connessione non crittografata legacy SQL Server", sqlServerLegacyUnencryptedModeEnabled: "La modalità compatibilità legacy di SQL Server è abilitata.", - sqlServerLegacyUnencryptedModeHint: "Si applica al trasporto non crittografato o alla crittografia solo per il login. Fallirà comunque se il server richiede la crittografia TLS 1.0.\nUsala solo su reti attendibili, VPN o tunnel SSH", + sqlServerLegacyUnencryptedModeHint: "Si applica al trasporto non crittografato o alla crittografia solo per il login. Fallirà comunque se il server richiede un trasporto crittografato che il driver integrato non può negoziare.\nUsala solo su reti attendibili, VPN o tunnel SSH", saveAndConnect: "Salva & Connetti", save: "Salva", editTitle: "Modifica Connessione", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 75d5b9a859..a4303161fe 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -300,9 +300,9 @@ export default withEnglishFallback({ testing: "テスト中...", copyTestResult: "テスト結果をコピー", sqlServerLegacyUnencryptedMode: "レガシー互換モード", - sqlServerLegacyUnencryptedModeEnable: "SQL Server レガシー TLS 1.0 非暗号化接続を使用", + sqlServerLegacyUnencryptedModeEnable: "SQL Server レガシー非暗号化接続を使用", sqlServerLegacyUnencryptedModeEnabled: "SQL Server レガシー互換モードが有効です。", - sqlServerLegacyUnencryptedModeHint: "非暗号化またはログイン時のみ暗号化する場合に適用されます。サーバーが TLS 1.0 暗号化を要求する場合は失敗します。\n信頼できるネットワーク、VPN、SSH トンネルでのみ使用してください", + sqlServerLegacyUnencryptedModeHint: "非暗号化またはログイン時のみ暗号化する場合に適用されます。サーバーが内蔵ドライバーでネゴシエートできない暗号化通信を要求する場合は失敗します。\n信頼できるネットワーク、VPN、SSH トンネルでのみ使用してください", saveAndConnect: "保存して接続", save: "保存", editTitle: "接続を編集", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 91e21b8e9b..cdc0ae7702 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -307,9 +307,9 @@ export default withEnglishFallback({ testing: "Testando...", copyTestResult: "Copiar resultado do teste", sqlServerLegacyUnencryptedMode: "Modo de compatibilidade legado", - sqlServerLegacyUnencryptedModeEnable: "Usar conexão SQL Server legada TLS 1.0 não criptografada", + sqlServerLegacyUnencryptedModeEnable: "Usar conexão SQL Server legada não criptografada", sqlServerLegacyUnencryptedModeEnabled: "O modo de compatibilidade legado do SQL Server está ativado.", - sqlServerLegacyUnencryptedModeHint: "Aplica-se a transporte não criptografado ou criptografia apenas no login. Ainda falhará se o servidor exigir criptografia TLS 1.0.\nUse apenas em redes confiáveis, VPNs ou túneis SSH", + sqlServerLegacyUnencryptedModeHint: "Aplica-se a transporte não criptografado ou criptografia apenas no login. Ainda falhará se o servidor exigir transporte criptografado que o driver integrado não consegue negociar.\nUse apenas em redes confiáveis, VPNs ou túneis SSH", saveAndConnect: "Salvar e Conectar", save: "Salvar", editTitle: "Editar Conexão", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 4f6af590af..61750a9b19 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -307,9 +307,9 @@ export default withEnglishFallback({ testing: "测试中...", copyTestResult: "复制测试结果", sqlServerLegacyUnencryptedMode: "旧版兼容模式", - sqlServerLegacyUnencryptedModeEnable: "使用 SQL Server 旧版TLS 1.0非加密连接", + sqlServerLegacyUnencryptedModeEnable: "使用 SQL Server 旧版非加密连接", sqlServerLegacyUnencryptedModeEnabled: "已启用 SQL Server 旧版兼容模式。", - sqlServerLegacyUnencryptedModeHint: "适用于非加密或仅登录阶段加密的场景。若服务端强制 TLS 1.0 加密,此模式仍会失败\n请仅在可信内网、VPN 或 SSH 隧道中使用", + sqlServerLegacyUnencryptedModeHint: "适用于非加密或仅登录阶段加密的场景。若服务端强制使用内置驱动无法协商的加密传输,此模式仍会失败\n请仅在可信内网、VPN 或 SSH 隧道中使用", saveAndConnect: "保存并连接", save: "保存", editTitle: "编辑连接", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index f9dff779cb..888945ff36 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -307,9 +307,9 @@ export default withEnglishFallback({ testing: "測試中……", copyTestResult: "複製測試結果", sqlServerLegacyUnencryptedMode: "舊版相容模式", - sqlServerLegacyUnencryptedModeEnable: "使用 SQL Server 舊版 TLS 1.0 非加密連線", + sqlServerLegacyUnencryptedModeEnable: "使用 SQL Server 舊版非加密連線", sqlServerLegacyUnencryptedModeEnabled: "已啟用 SQL Server 舊版相容模式。", - sqlServerLegacyUnencryptedModeHint: "適用於非加密或僅登入階段加密的場景。若伺服器強制 TLS 1.0 加密,此模式仍會失敗\n請僅在可信內網、VPN 或 SSH 隧道中使用", + sqlServerLegacyUnencryptedModeHint: "適用於非加密或僅登入階段加密的場景。若伺服器強制使用內建驅動無法協商的加密傳輸,此模式仍會失敗\n請僅在可信內網、VPN 或 SSH 隧道中使用", saveAndConnect: "儲存並連線", save: "儲存", editTitle: "編輯連線", diff --git a/crates/dbx-core/src/db/sqlserver.rs b/crates/dbx-core/src/db/sqlserver.rs index e715f96e4e..43b669582b 100644 --- a/crates/dbx-core/src/db/sqlserver.rs +++ b/crates/dbx-core/src/db/sqlserver.rs @@ -17,6 +17,8 @@ use tokio_util::sync::CancellationToken; pub type SqlServerClient = Client>; pub const SQLSERVER_DRIVER_PANIC_ERROR_PREFIX: &str = "SQL Server driver panic:"; const SIMPLE_QUERY_MODULE_KEYWORDS: &[&str] = &["FUNCTION", "PROC", "PROCEDURE", "TRIGGER", "VIEW"]; +// Match JDBC/tiberius `encrypt=false`: encrypt only login, then drop back to raw TDS. +// `NotSupported` sends ENCRYPT_NOT_SUP and is rejected by some legacy SQL Server setups. const SQLSERVER_LEGACY_ENCRYPTION_LEVEL: tiberius::EncryptionLevel = tiberius::EncryptionLevel::Off; #[derive(Debug, PartialEq, Eq)] @@ -55,23 +57,24 @@ pub async fn connect( match try_connect(host, port, user, pass, database, tiberius::EncryptionLevel::Required, timeout).await { Ok(client) => Ok(client), Err(encrypted_error) => { - try_connect(host, port, user, pass, database, tiberius::EncryptionLevel::NotSupported, timeout) - .await - .map_err(|plain_error| { + try_connect(host, port, user, pass, database, SQLSERVER_LEGACY_ENCRYPTION_LEVEL, timeout).await.map_err( + |plain_error| { if is_sqlserver_tls_handshake_error(&encrypted_error) { format!( - "{encrypted_error}\n\nThis may be caused by an old SQL Server TLS configuration. \ - If you are connecting to SQL Server 2008/2008 R2 or another legacy instance, \ + "{encrypted_error}\n\nThis may be caused by an old SQL Server TLS/encryption configuration. \ + If you are connecting to SQL Server 2008/2008 R2/2012 or another legacy instance, \ try SQL Server legacy unencrypted mode. It behaves like encrypt=false and only helps \ when the server allows unencrypted transport or login-only encryption. It will still fail \ - if the server requires TLS 1.0 encryption. Only use this mode on trusted networks, VPNs, \ + if the server requires encrypted transport that the embedded driver cannot negotiate. \ + Only use this mode on trusted networks, VPNs, \ or SSH tunnels.\n\n\ - Automatic unencrypted fallback also failed: {plain_error}" - ) + Automatic legacy unencrypted fallback also failed: {plain_error}" + ) } else { plain_error } - }) + }, + ) } } } @@ -82,8 +85,10 @@ fn sqlserver_legacy_encryption_disabled(url_params: Option<&str>) -> bool { }; params.trim_start_matches('?').split(['&', ';']).filter_map(|pair| pair.split_once('=')).any(|(key, value)| { - key.trim().eq_ignore_ascii_case("sqlserverEncryption") - && matches!(value.trim().to_ascii_lowercase().as_str(), "disabled" | "disable" | "false" | "0" | "off") + let key = key.trim(); + let value = value.trim().to_ascii_lowercase(); + let disabled = matches!(value.as_str(), "disabled" | "disable" | "false" | "0" | "off" | "no"); + (key.eq_ignore_ascii_case("sqlserverEncryption") || key.eq_ignore_ascii_case("encrypt")) && disabled }) } @@ -1860,12 +1865,14 @@ mod tests { } #[test] - fn sqlserver_legacy_encryption_flag_is_opt_in() { + fn sqlserver_legacy_encryption_flag_accepts_dbx_and_jdbc_params() { assert!(!super::sqlserver_legacy_encryption_disabled(None)); - assert!(!super::sqlserver_legacy_encryption_disabled(Some("encrypt=false"))); + assert!(!super::sqlserver_legacy_encryption_disabled(Some("encrypt=true"))); assert!(super::sqlserver_legacy_encryption_disabled(Some("sqlserverEncryption=disabled"))); assert!(super::sqlserver_legacy_encryption_disabled(Some("applicationName=dbx;sqlserverEncryption=off"))); assert!(super::sqlserver_legacy_encryption_disabled(Some("?sqlserverEncryption=false&applicationName=dbx"))); + assert!(super::sqlserver_legacy_encryption_disabled(Some("applicationName=dbx;encrypt=false"))); + assert!(super::sqlserver_legacy_encryption_disabled(Some("?Encrypt=0&applicationName=dbx"))); } #[test] @@ -1873,6 +1880,15 @@ mod tests { assert_eq!(super::SQLSERVER_LEGACY_ENCRYPTION_LEVEL, tiberius::EncryptionLevel::Off); } + #[test] + fn sqlserver_automatic_fallback_uses_legacy_encryption_mode() { + let source = include_str!("sqlserver.rs"); + let connect = source.split("pub async fn connect").nth(1).unwrap(); + let connect = connect.split("fn sqlserver_legacy_encryption_disabled").next().unwrap(); + assert!(connect.contains("database, SQLSERVER_LEGACY_ENCRYPTION_LEVEL, timeout")); + assert!(!connect.contains("EncryptionLevel::NotSupported, timeout")); + } + #[test] fn sqlserver_tls_handshake_error_detection_matches_legacy_hint_cases() { assert!(super::is_sqlserver_tls_handshake_error(