feat(sqlserver): support legacy TLS compatibility

This commit is contained in:
zipg authored and GitHub committed 2026-07-09 22:45:54 +08:00
1 parent 63ef26b25a
commit 67edb94b1d
30 files changed
+738 -117

No files matched your search

@@ -42,6 +42,7 @@ import { appendConnectionErrorHints } from "@/lib/connection/connectionErrorHint
import { normalizeKafkaBootstrapServers } from "@/lib/connection/kafkaBootstrapServers";
import { detectMqUiAuthKind, isMqAuthKindAllowedForSystem, type MqUiAuthKind } from "@/lib/connection/mqAuth";
import { driverInstallProgressPercent, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi";
import { isSqlServerLegacyCompatibilityMode, requiresSqlServerLegacyCompatibilityComponent, setSqlServerLegacyCompatibilityMode, SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY } from "@/lib/connection/sqlServerLegacyCompatibility";
import { ArrowLeft, ArrowDown, ArrowUp, CheckSquare, ChevronRight, CircleHelp, Copy, ExternalLink, FilePlus2, FolderOpen, GripVertical, Grid3X3, KeyRound, Link2, List, ListFilter, Loader2, Pencil, Pipette, Plus, Search, ShieldCheck, Square, Trash2 } from "@lucide/vue";
import { buildDraftVisibleDatabasesConnectionId, connectionCanChooseVisibleDatabases, initialVisibleDatabaseSelection, visibleDatabaseSelectionIsStale } from "@/lib/connection/connectionVisibleDatabases";
import { canSaveVisibleDatabaseSelection, connectionUsesVisibleSchemaFilter, filterDatabaseNamesForVisiblePicker, isSystemDatabaseName, normalizeVisibleDatabaseSelection, buildDraftVisibleSchemasConnectionId, normalizeVisibleSchemaSelection } from "@/lib/database/visibleDatabases";
@@ -1070,6 +1071,10 @@ function formatInstallSize(bytes: number): string {
}
async function ensureRequiredAgentDriverInstalled(config: ConnectionConfig): Promise<void> {
if (requiresSqlServerLegacyCompatibilityComponent(config)) {
await installSqlServerLegacyCompatibilityComponentIfNeeded();
}
const driverKey = agentDriverInstallKey(config.db_type, config.driver_profile);
if (!driverKey) return;
@@ -1094,29 +1099,42 @@ async function ensureRequiredAgentDriverInstalled(config: ConnectionConfig): Pro
}
}
function isSqlServerLegacyUnencryptedMode(params: string | undefined): boolean {
const normalized = (params || "").trim().replace(/^\?/, "").replace(/;/g, "&");
if (!normalized) return false;
const parsed = new URLSearchParams(normalized);
for (const [key, value] of parsed.entries()) {
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;
}
async function installSqlServerLegacyCompatibilityComponentIfNeeded(): Promise<boolean> {
if (await api.isAgentInstalled(SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY)) return true;
const label = t("connection.sqlServerLegacyCompatibilityComponent");
testResult.value = { ok: true, message: t("connection.sqlServerLegacyCompatibilityComponentInstalling") };
beginAgentDriverInstall(SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY, label);
try {
await api.installAgent(SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY);
await refreshLocalAgentDrivers();
finishAgentDriverInstall();
} catch (error) {
testResult.value = { ok: false, message: errorMessage(error) };
failAgentDriverInstall(error);
throw error;
}
return false;
return true;
}
function setSqlServerLegacyUnencryptedMode(params: string | undefined, enabled: boolean): string {
const normalized = (params || "").trim().replace(/^\?/, "").replace(/;/g, "&");
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);
async function onSqlServerLegacyCompatibilityModeChange(event: Event) {
if (form.value.db_type !== "sqlserver") return;
const input = event.target instanceof HTMLInputElement ? event.target : null;
const enabled = input?.checked === true;
if (!enabled) {
form.value.url_params = setSqlServerLegacyCompatibilityMode(form.value.url_params, false);
return;
}
if (input) input.checked = false;
try {
await installSqlServerLegacyCompatibilityComponentIfNeeded();
form.value.url_params = setSqlServerLegacyCompatibilityMode(form.value.url_params, true);
testResult.value = { ok: true, message: t("connection.sqlServerLegacyCompatibilityModeEnabled") };
} catch {
form.value.url_params = setSqlServerLegacyCompatibilityMode(form.value.url_params, false);
if (input) input.checked = false;
}
if (enabled) parsed.set("sqlserverEncryption", "disabled");
return parsed.toString();
}
function isSqlServerTlsHandshakeFailure(message: string): boolean {
@@ -1917,13 +1935,7 @@ const agentInstallProgressLabel = computed(() => {
return `${label} ${formatInstallSize(progress.downloaded ?? 0)} / ${formatInstallSize(progress.total)} (${agentInstallPercent.value ?? 0}%)`;
});
const canCloseAgentInstallDialog = computed(() => !agentInstallRunning.value || !!agentInstallError.value);
const sqlServerLegacyUnencryptedModeEnabled = computed({
get: () => form.value.db_type === "sqlserver" && isSqlServerLegacyUnencryptedMode(form.value.url_params),
set: (enabled: boolean) => {
if (form.value.db_type !== "sqlserver") return;
form.value.url_params = setSqlServerLegacyUnencryptedMode(form.value.url_params, enabled);
},
});
const sqlServerLegacyCompatibilityModeEnabled = computed(() => form.value.db_type === "sqlserver" && isSqlServerLegacyCompatibilityMode(form.value.url_params));
const shouldUseWideConnectionDialog = computed(() => dialogStep.value === "config" && (canChooseVisibleDatabases.value || (canChooseVisibleSchemas.value && !visibleFilterUsesSchemas.value)));
const connectionDialogContentClass = computed(() => {
if (dialogStep.value === "select") return "sm:max-w-[760px]";
@@ -2019,7 +2031,7 @@ async function testConnection() {
const message = config ? connectionErrorWithDriverUpdateHint(config, rawMessage) : rawMessage;
const fallbackMessage = config ? await tryNacosDockerConsoleFallback(config, message, runId) : null;
if (runId !== testRunId) return;
const shouldShowSqlServerLegacyMode = !fallbackMessage && config?.db_type === "sqlserver" && !isSqlServerLegacyUnencryptedMode(config.url_params) && isSqlServerTlsHandshakeFailure(message);
const shouldShowSqlServerLegacyMode = !fallbackMessage && config?.db_type === "sqlserver" && !isSqlServerLegacyCompatibilityMode(config.url_params) && isSqlServerTlsHandshakeFailure(message);
if (shouldShowSqlServerLegacyMode) {
configTab.value = "advanced";
}
@@ -5027,14 +5039,14 @@ function openExternalUrl(url: string) {
</label>
</div>
<div v-show="form.db_type === 'sqlserver'" class="grid grid-cols-4 items-start gap-4">
<Label :class="connectionLabelSmallClass">{{ t("connection.sqlServerLegacyUnencryptedMode") }}</Label>
<Label :class="connectionLabelSmallClass">{{ t("connection.sqlServerLegacyCompatibilityMode") }}</Label>
<div class="col-span-3 flex flex-col gap-1">
<label class="flex h-5 cursor-pointer items-center gap-2">
<input type="checkbox" v-model="sqlServerLegacyUnencryptedModeEnabled" class="mr-0" />
<span class="text-xs text-foreground">{{ t("connection.sqlServerLegacyUnencryptedModeEnable") }}</span>
<input type="checkbox" :checked="sqlServerLegacyCompatibilityModeEnabled" :disabled="agentInstallRunning" class="mr-0" @change="onSqlServerLegacyCompatibilityModeChange" />
<span class="text-xs text-foreground">{{ t("connection.sqlServerLegacyCompatibilityModeEnable") }}</span>
</label>
<p class="m-0 whitespace-pre-line text-xs leading-5 text-muted-foreground">
{{ t("connection.sqlServerLegacyUnencryptedModeHint") }}
{{ t("connection.sqlServerLegacyCompatibilityModeHint") }}
</p>
</div>
</div>
+7 -4
View File
@@ -323,10 +323,13 @@ export default {
test: "Test",
testing: "Testing...",
copyTestResult: "Copy test result",
sqlServerLegacyUnencryptedMode: "Legacy compatibility mode",
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 encrypted transport that the embedded driver cannot negotiate.\nUse it only on trusted networks, VPNs, or SSH tunnels",
sqlServerLegacyCompatibilityMode: "Legacy compatibility mode",
sqlServerLegacyCompatibilityModeEnable: "Use SQL Server legacy compatibility connection",
sqlServerLegacyCompatibilityModeEnabled: "SQL Server legacy compatibility mode is enabled.",
sqlServerLegacyCompatibilityModeHint:
"Applies to unencrypted, login-only encrypted, or TLS 1.0 legacy compatibility scenarios. If the compatibility component is not installed, DBX installs it before enabling this mode.\nSome native SQL Server features may be unavailable when fallback uses the compatibility component.\nUse it only on trusted networks, VPNs, or SSH tunnels",
sqlServerLegacyCompatibilityComponent: "SQL Server legacy compatibility component",
sqlServerLegacyCompatibilityComponentInstalling: "Installing SQL Server legacy compatibility component...",
saveAndConnect: "Save & Connect",
save: "Save",
editTitle: "Edit Connection",
+7 -4
View File
@@ -305,10 +305,13 @@ export default withEnglishFallback({
test: "Probar",
testing: "Probando...",
copyTestResult: "Copiar resultado de prueba",
sqlServerLegacyUnencryptedMode: "Modo de compatibilidad heredado",
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 transporte cifrado que el controlador integrado no puede negociar.\nÚsalo solo en redes de confianza, VPN o túneles SSH",
sqlServerLegacyCompatibilityMode: "Modo de compatibilidad heredado",
sqlServerLegacyCompatibilityModeEnable: "Usar conexión de compatibilidad heredada de SQL Server",
sqlServerLegacyCompatibilityModeEnabled: "El modo de compatibilidad heredado de SQL Server está activado.",
sqlServerLegacyCompatibilityModeHint:
"Aplica a escenarios sin cifrado, con cifrado solo en el login o con compatibilidad heredada TLS 1.0. Si el componente de compatibilidad no está instalado, DBX lo instala antes de activar este modo.\nAl usar el componente de compatibilidad como fallback, algunas funciones nativas de SQL Server pueden no estar disponibles.\nÚsalo solo en redes de confianza, VPN o túneles SSH",
sqlServerLegacyCompatibilityComponent: "Componente de compatibilidad heredada de SQL Server",
sqlServerLegacyCompatibilityComponentInstalling: "Instalando componente de compatibilidad heredada de SQL Server...",
saveAndConnect: "Guardar y conectar",
save: "Guardar",
editTitle: "Editar conexión",
+7 -4
View File
@@ -304,10 +304,13 @@ export default withEnglishFallback({
test: "Test",
testing: "Verifica in corso...",
copyTestResult: "Copia risultato test",
sqlServerLegacyUnencryptedMode: "Modalità compatibilità legacy",
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 un trasporto crittografato che il driver integrato non può negoziare.\nUsala solo su reti attendibili, VPN o tunnel SSH",
sqlServerLegacyCompatibilityMode: "Modalità compatibilità legacy",
sqlServerLegacyCompatibilityModeEnable: "Usa connessione di compatibilità legacy SQL Server",
sqlServerLegacyCompatibilityModeEnabled: "La modalità compatibilità legacy di SQL Server è abilitata.",
sqlServerLegacyCompatibilityModeHint:
"Si applica a scenari non crittografati, con crittografia solo al login o con compatibilità legacy TLS 1.0. Se il componente di compatibilità non è installato, DBX lo installa prima di abilitare questa modalità.\nQuando il fallback usa il componente di compatibilità, alcune funzionalità native di SQL Server potrebbero non essere disponibili.\nUsala solo su reti attendibili, VPN o tunnel SSH",
sqlServerLegacyCompatibilityComponent: "Componente di compatibilità legacy SQL Server",
sqlServerLegacyCompatibilityComponentInstalling: "Installazione componente di compatibilità legacy SQL Server...",
saveAndConnect: "Salva & Connetti",
save: "Salva",
editTitle: "Modifica Connessione",
+7 -4
View File
@@ -298,10 +298,13 @@ export default withEnglishFallback({
test: "テスト",
testing: "テスト中...",
copyTestResult: "テスト結果をコピー",
sqlServerLegacyUnencryptedMode: "レガシー互換モード",
sqlServerLegacyUnencryptedModeEnable: "SQL Server レガシー非暗号化接続を使用",
sqlServerLegacyUnencryptedModeEnabled: "SQL Server レガシー互換モードが有効です。",
sqlServerLegacyUnencryptedModeHint: "非暗号化またはログイン時のみ暗号化する場合に適用されます。サーバーが内蔵ドライバーでネゴシエートできない暗号化通信を要求する場合は失敗します。\n信頼できるネットワーク、VPN、SSH トンネルでのみ使用してください",
sqlServerLegacyCompatibilityMode: "レガシー互換モード",
sqlServerLegacyCompatibilityModeEnable: "SQL Server レガシー互換接続を使用",
sqlServerLegacyCompatibilityModeEnabled: "SQL Server レガシー互換モードが有効です。",
sqlServerLegacyCompatibilityModeHint:
"非暗号化、ログイン時のみ暗号化、または TLS 1.0 レガシー互換シナリオに適用されます。互換コンポーネントが未インストールの場合、DBX はこのモードを有効にする前にインストールします。\n互換コンポーネントへフォールバックした場合、一部の SQL Server ネイティブ機能を利用できないことがあります。\n信頼できるネットワーク、VPN、SSH トンネルでのみ使用してください",
sqlServerLegacyCompatibilityComponent: "SQL Server レガシー互換コンポーネント",
sqlServerLegacyCompatibilityComponentInstalling: "SQL Server レガシー互換コンポーネントをインストールしています...",
saveAndConnect: "保存して接続",
save: "保存",
editTitle: "接続を編集",
+7 -4
View File
@@ -305,10 +305,13 @@ export default withEnglishFallback({
test: "Testar",
testing: "Testando...",
copyTestResult: "Copiar resultado do teste",
sqlServerLegacyUnencryptedMode: "Modo de compatibilidade legado",
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 transporte criptografado que o driver integrado não consegue negociar.\nUse apenas em redes confiáveis, VPNs ou túneis SSH",
sqlServerLegacyCompatibilityMode: "Modo de compatibilidade legado",
sqlServerLegacyCompatibilityModeEnable: "Usar conexão de compatibilidade legada do SQL Server",
sqlServerLegacyCompatibilityModeEnabled: "O modo de compatibilidade legado do SQL Server está ativado.",
sqlServerLegacyCompatibilityModeHint:
"Aplica-se a cenários sem criptografia, com criptografia apenas no login ou compatibilidade legada TLS 1.0. Se o componente de compatibilidade não estiver instalado, o DBX o instala antes de ativar este modo.\nQuando o fallback usa o componente de compatibilidade, alguns recursos nativos do SQL Server podem ficar indisponíveis.\nUse apenas em redes confiáveis, VPNs ou túneis SSH",
sqlServerLegacyCompatibilityComponent: "Componente de compatibilidade legada do SQL Server",
sqlServerLegacyCompatibilityComponentInstalling: "Instalando componente de compatibilidade legada do SQL Server...",
saveAndConnect: "Salvar e Conectar",
save: "Salvar",
editTitle: "Editar Conexão",
+6 -4
View File
@@ -325,10 +325,12 @@ export default withEnglishFallback({
test: "测试",
testing: "测试中...",
copyTestResult: "复制测试结果",
sqlServerLegacyUnencryptedMode: "旧版兼容模式",
sqlServerLegacyUnencryptedModeEnable: "使用 SQL Server 旧版非加密连接",
sqlServerLegacyUnencryptedModeEnabled: "已启用 SQL Server 旧版兼容模式。",
sqlServerLegacyUnencryptedModeHint: "适用于非加密或仅登录阶段加密的场景。若服务端强制使用内置驱动无法协商的加密传输,此模式仍会失败\n请仅在可信内网、VPN 或 SSH 隧道中使用",
sqlServerLegacyCompatibilityMode: "旧版兼容模式",
sqlServerLegacyCompatibilityModeEnable: "使用 SQL Server 旧版兼容连接",
sqlServerLegacyCompatibilityModeEnabled: "已启用 SQL Server 旧版兼容模式。",
sqlServerLegacyCompatibilityModeHint: "适用于非加密、仅登录阶段加密或 TLS 1.0 旧版兼容场景。若兼容组件未安装,DBX 会先安装再启用该模式\n若回退到兼容组件,部分 SQL Server 原生能力可能不可用\n请仅在可信内网、VPN 或 SSH 隧道中使用",
sqlServerLegacyCompatibilityComponent: "SQL Server 旧版兼容组件",
sqlServerLegacyCompatibilityComponentInstalling: "正在安装 SQL Server 旧版兼容组件...",
saveAndConnect: "保存并连接",
save: "保存",
editTitle: "编辑连接",
+6 -4
View File
@@ -305,10 +305,12 @@ export default withEnglishFallback({
test: "測試",
testing: "測試中……",
copyTestResult: "複製測試結果",
sqlServerLegacyUnencryptedMode: "舊版相容模式",
sqlServerLegacyUnencryptedModeEnable: "使用 SQL Server 舊版非加密連線",
sqlServerLegacyUnencryptedModeEnabled: "已啟用 SQL Server 舊版相容模式。",
sqlServerLegacyUnencryptedModeHint: "適用於非加密或僅登入階段加密的場景。若伺服器強制使用內建驅動無法協商的加密傳輸,此模式仍會失敗\n請僅在可信內網、VPN 或 SSH 隧道中使用",
sqlServerLegacyCompatibilityMode: "舊版相容模式",
sqlServerLegacyCompatibilityModeEnable: "使用 SQL Server 舊版相容連線",
sqlServerLegacyCompatibilityModeEnabled: "已啟用 SQL Server 舊版相容模式。",
sqlServerLegacyCompatibilityModeHint: "適用於非加密、僅登入階段加密或 TLS 1.0 舊版相容場景。若相容元件未安裝,DBX 會先安裝再啟用此模式\n若回退到相容元件,部分 SQL Server 原生能力可能無法使用\n請僅在可信內網、VPN 或 SSH 隧道中使用",
sqlServerLegacyCompatibilityComponent: "SQL Server 舊版相容元件",
sqlServerLegacyCompatibilityComponentInstalling: "正在安裝 SQL Server 舊版相容元件...",
saveAndConnect: "儲存並連線",
save: "儲存",
editTitle: "編輯連線",
@@ -0,0 +1,50 @@
import { describe, expect, it } from "vitest";
import { isSqlServerLegacyCompatibilityMode, requiresSqlServerLegacyCompatibilityComponent, setSqlServerLegacyCompatibilityMode } from "@/lib/connection/sqlServerLegacyCompatibility";
import type { ConnectionConfig } from "@/types/database";
function connectionConfig(urlParams?: string): ConnectionConfig {
return {
id: "sqlserver",
name: "SQL Server",
db_type: "sqlserver",
driver_profile: "sqlserver",
driver_label: "SQL Server",
host: "127.0.0.1",
port: 1433,
username: "sa",
password: "secret",
database: "master",
url_params: urlParams,
ssl: false,
ssh_enabled: false,
read_only: false,
one_time: false,
transport_layers: [],
agent_java_options: [],
};
}
describe("SQL Server legacy compatibility", () => {
it("treats existing disabled encryption params as legacy compatibility opt-in", () => {
expect(isSqlServerLegacyCompatibilityMode("sqlserverEncryption=disabled")).toBe(true);
expect(isSqlServerLegacyCompatibilityMode("applicationName=dbx;encrypt=false")).toBe(true);
expect(isSqlServerLegacyCompatibilityMode("?Encrypt=0&applicationName=dbx")).toBe(true);
expect(isSqlServerLegacyCompatibilityMode("encrypt=true")).toBe(false);
});
it("updates URL params without keeping conflicting encryption params", () => {
expect(setSqlServerLegacyCompatibilityMode("applicationName=dbx;encrypt=true", true)).toBe("applicationName=dbx&sqlserverEncryption=disabled");
expect(setSqlServerLegacyCompatibilityMode("applicationName=dbx;sqlserverEncryption=disabled", false)).toBe("applicationName=dbx");
});
it("requires the hidden component only for SQL Server legacy compatibility connections", () => {
expect(requiresSqlServerLegacyCompatibilityComponent(connectionConfig("sqlserverEncryption=disabled"))).toBe(true);
expect(requiresSqlServerLegacyCompatibilityComponent(connectionConfig("encrypt=true"))).toBe(false);
expect(
requiresSqlServerLegacyCompatibilityComponent({
...connectionConfig("sqlserverEncryption=disabled"),
db_type: "mysql",
}),
).toBe(false);
});
});
+1
View File
@@ -75,6 +75,7 @@ export const installJdbcPluginLocal = forward("installJdbcPluginLocal");
export const uninstallJdbcPlugin = forward("uninstallJdbcPlugin");
export const listInstalledAgentsLocal = forward("listInstalledAgentsLocal");
export const listInstalledAgents = forward("listInstalledAgents");
export const isAgentInstalled = forward("isAgentInstalled");
export const getDriverStoreUsage = forward("getDriverStoreUsage");
export const clearDriverDownloadCache = forward("clearDriverDownloadCache");
export const getDriverRuntimeSummary = forward("getDriverRuntimeSummary");
+4
View File
@@ -337,6 +337,10 @@ export async function listInstalledAgents(): Promise<AgentDriverInfo[]> {
return get("/api/agents/installed");
}
export async function isAgentInstalled(dbType: string): Promise<boolean> {
return get(`/api/agents/installed/${encodeURIComponent(dbType)}`);
}
export async function getDriverStoreUsage(): Promise<DriverStoreUsage> {
return get("/api/agents/storage-usage");
}
+4
View File
@@ -1122,6 +1122,10 @@ export async function listInstalledAgents(): Promise<AgentDriverInfo[]> {
return invoke("list_installed_agents");
}
export async function isAgentInstalled(dbType: string): Promise<boolean> {
return invoke("is_agent_installed", { dbType });
}
export async function getDriverStoreUsage(): Promise<DriverStoreUsage> {
return invoke("get_driver_store_usage");
}
@@ -0,0 +1,34 @@
import type { ConnectionConfig } from "@/types/database";
export const SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY = "sqlserver-legacy";
const SQLSERVER_LEGACY_DISABLED_VALUES = new Set(["disabled", "disable", "false", "0", "off", "no"]);
export function isSqlServerLegacyCompatibilityMode(params: string | undefined): boolean {
const normalized = (params || "").trim().replace(/^\?/, "").replace(/;/g, "&");
if (!normalized) return false;
const parsed = new URLSearchParams(normalized);
for (const [key, value] of parsed.entries()) {
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 (SQLSERVER_LEGACY_DISABLED_VALUES.has(value.trim().toLowerCase())) return true;
}
}
return false;
}
export function setSqlServerLegacyCompatibilityMode(params: string | undefined, enabled: boolean): string {
const normalized = (params || "").trim().replace(/^\?/, "").replace(/;/g, "&");
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();
}
export function requiresSqlServerLegacyCompatibilityComponent(config: ConnectionConfig): boolean {
return config.db_type === "sqlserver" && isSqlServerLegacyCompatibilityMode(config.url_params);
}
@@ -0,0 +1,63 @@
import { createPinia, setActivePinia } from "pinia";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ConnectionConfig } from "@/types/database";
function installLocalStorage() {
const data = new Map<string, string>();
vi.stubGlobal("localStorage", {
getItem: vi.fn((key: string) => data.get(key) ?? null),
setItem: vi.fn((key: string, value: string) => data.set(key, value)),
removeItem: vi.fn((key: string) => data.delete(key)),
});
}
function sqlServerLegacyConnection(): ConnectionConfig {
return {
id: "sqlserver-1",
name: "SQL Server",
db_type: "sqlserver",
driver_profile: "sqlserver",
driver_label: "SQL Server",
host: "127.0.0.1",
port: 1433,
username: "sa",
password: "secret",
database: "master",
url_params: "sqlserverEncryption=disabled",
ssl: false,
ssh_enabled: false,
read_only: false,
one_time: false,
transport_layers: [],
agent_java_options: [],
};
}
describe("connectionStore SQL Server legacy compatibility", () => {
beforeEach(() => {
vi.resetModules();
vi.unstubAllGlobals();
installLocalStorage();
setActivePinia(createPinia());
});
it("installs the legacy compatibility component before connecting saved legacy configs", async () => {
const connectDb = vi.fn().mockResolvedValue("sqlserver-1");
const installAgent = vi.fn().mockResolvedValue(undefined);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
connectDb,
installAgent,
isAgentInstalled: vi.fn().mockResolvedValue(false),
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
await store.connect(sqlServerLegacyConnection());
expect(installAgent).toHaveBeenCalledWith("sqlserver-legacy");
expect(connectDb).toHaveBeenCalledTimes(1);
expect(installAgent.mock.invocationCallOrder[0]).toBeLessThan(connectDb.mock.invocationCallOrder[0]);
});
});
@@ -31,6 +31,7 @@ import { collapseExpandedTreeNodes } from "@/lib/sidebar/sidebarTreeCollapse";
import { findDatabaseTreeNode } from "@/lib/sidebar/treeRefreshTarget";
import { shouldMarkDisconnected } from "@/lib/connection/connectionHealth";
import { connectionAttemptOriginalErrorMessage, connectionAttemptTimeoutMessage, connectionAttemptTimeoutMs } from "@/lib/connection/connectionAttemptTimeout";
import { requiresSqlServerLegacyCompatibilityComponent, SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY } from "@/lib/connection/sqlServerLegacyCompatibility";
import { connectionUsesVisibleSchemaFilter, filterDatabaseNamesForConnection, filterSchemaNamesForConnection, filterVisibleDatabaseNames, normalizeVisibleDatabaseSelection } from "@/lib/database/visibleDatabases";
import {
buildObjectGroupPlaceholderNodes,
@@ -1625,6 +1626,12 @@ export const useConnectionStore = defineStore("connection", () => {
rebuildTreeNodes();
}
async function ensureSqlServerLegacyCompatibilityComponentInstalled(config: ConnectionConfig) {
if (!requiresSqlServerLegacyCompatibilityComponent(config)) return;
if (await api.isAgentInstalled(SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY)) return;
await api.installAgent(SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY);
}
async function setDefaultDatabase(connectionId: string, database: string) {
const config = getConfig(connectionId);
if (!config || config.database === database) return;
@@ -1771,6 +1778,7 @@ export const useConnectionStore = defineStore("connection", () => {
const localAttempt = beginLocalConnectionAttempt(config.id);
try {
await beforeConnectHandler?.(config);
await ensureSqlServerLegacyCompatibilityComponentInstalled(config);
ensureLocalConnectionAttemptActive(config.id, localAttempt);
const id = await withConnectionAttemptTimeout(api.connectDb(config, localAttempt), config);
await ensureLocalConnectionAttemptActiveAfterConnectResult(config.id, localAttempt, id);