fix(dameng): support SSL connection settings

This commit is contained in:
t8y2 2026-07-26 15:31:11 +08:00
parent ca11b166f8
commit 5e7ab62066
No known key found for this signature in database
15 changed files with 308 additions and 3 deletions

View File

@ -1089,7 +1089,17 @@ public final class DamengAgent extends BaseDatabaseAgent {
private static String buildUrl(ConnectParams params) {
String database = params.getDatabase() == null ? "" : params.getDatabase().trim();
String suffix = database.isEmpty() ? "" : "/" + database;
return "jdbc:dm://" + params.getHost() + ":" + params.getPort() + suffix;
String url = "jdbc:dm://" + params.getHost() + ":" + params.getPort() + suffix;
String urlParams = params.getUrl_params() == null ? "" : params.getUrl_params().trim();
while (urlParams.startsWith("?") || urlParams.startsWith("&") || urlParams.startsWith(";")) {
urlParams = urlParams.substring(1);
}
if (urlParams.isEmpty()) {
return url;
}
// DM8 SSL options are JDBC URL query parameters; dropping them makes the driver initialize SSL with defaults.
return url + "?" + urlParams;
}
private static String formatDataType(

View File

@ -21,6 +21,25 @@ class DamengAgentUrlTest {
Assertions.assertEquals("jdbc:dm://127.0.0.1:5236/MAIN", url);
}
@Test
void appendsDmJdbcUrlParameters() throws Exception {
String url = invokeBuildUrl(new ConnectParams(
"127.0.0.1",
5236,
"",
"SYSDBA",
"pwd",
"?sslFilesPath=/Users/test/dmcert&sslkeystorePass=secret",
"",
false
));
Assertions.assertEquals(
"jdbc:dm://127.0.0.1:5236?sslFilesPath=/Users/test/dmcert&sslkeystorePass=secret",
url
);
}
private static String invokeBuildUrl(ConnectParams params) throws Exception {
Method method = DamengAgent.class.getDeclaredMethod("buildUrl", ConnectParams.class);
method.setAccessible(true);

View File

@ -39,6 +39,7 @@ import { isLocalFileTypeDb } from "@/lib/connection/connectionFile";
import { MQ_PINNED_VERSION_OPTIONS, pinnedVersionToSelection, selectionToPinnedVersion } from "@/lib/mq/mqPinnedVersionOptions";
import { mongodbAuthFailureHint, mongoUrlParam, mongoUrlParamIsTrue, normalizeMongoTlsFormState, setMongoUrlParam, setMongoUrlParamBoolean } from "@/lib/mongo/mongoConnectionOptions";
import { mysqlCleartextPasswordAuthEnabled, setMysqlCleartextPasswordAuthEnabled } from "@/lib/database/mysqlConnectionOptions";
import { applyDamengSslUrlParams, damengSslFormConfig } from "@/lib/database/damengSslOptions";
import { copyToClipboard } from "@/lib/common/clipboard";
import { configuredDatabaseProductName, connectionConfigFingerprint, databaseInfoCopyText, databaseInfoRows, normalizeDatabaseConnectionInfo, type DatabaseInfoField } from "@/lib/connection/connectionDatabaseInfo";
import { agentDriverInstallKey, appendAgentDriverUpdateHint, hasAgentDriverUpdate, showAgentDriverInstallHint, type AgentDriverInstallState, type DriverStoreFocus } from "@/lib/connection/agentDriverInstallHint";
@ -2434,7 +2435,7 @@ const sqliteExtensionPaths = computed({
form.value.url_params = setSqliteExtensionPaths(form.value.url_params, value);
},
});
const tlsCapableDatabaseTypes = new Set<DatabaseType>(["mysql", "starrocks", "postgres", "redshift", "gaussdb", "kwdb", "opengauss", "questdb", "redis", "etcd", "clickhouse", "elasticsearch", "qdrant", "milvus", "weaviate", "chromadb", "influxdb"]);
const tlsCapableDatabaseTypes = new Set<DatabaseType>(["mysql", "starrocks", "postgres", "redshift", "gaussdb", "kwdb", "opengauss", "questdb", "dameng", "redis", "etcd", "clickhouse", "elasticsearch", "qdrant", "milvus", "weaviate", "chromadb", "influxdb"]);
const supportsTlsToggle = computed(() => tlsCapableDatabaseTypes.has(form.value.db_type));
const supportsCaCertificatePath = computed(() => form.value.db_type === "clickhouse");
const supportsGenericUrlParams = computed(() => form.value.db_type !== "manticoresearch");
@ -2447,6 +2448,37 @@ const mysqlCleartextPasswordAuth = computed({
form.value.url_params = setMysqlCleartextPasswordAuthEnabled(form.value.url_params, value);
},
});
// DM8 configures SSL through JDBC URL parameters, so the TLS form and Advanced tab share one source of truth.
const tlsEnabled = computed({
get: () => !!form.value.ssl || (form.value.db_type === "dameng" && damengSslFormConfig(form.value.url_params).enabled),
set: (enabled: boolean) => {
form.value.ssl = enabled;
if (form.value.db_type === "dameng" && !enabled) {
form.value.url_params = applyDamengSslUrlParams(form.value.url_params, false, "", "", "");
}
},
});
const damengSslFilesPath = computed({
get: () => damengSslFormConfig(form.value.url_params).sslFilesPath,
set: (value: string) => {
const current = damengSslFormConfig(form.value.url_params);
form.value.url_params = applyDamengSslUrlParams(form.value.url_params, true, value, current.sslKeystorePassword, current.sslProtocol);
},
});
const damengSslKeystorePassword = computed({
get: () => damengSslFormConfig(form.value.url_params).sslKeystorePassword,
set: (value: string) => {
const current = damengSslFormConfig(form.value.url_params);
form.value.url_params = applyDamengSslUrlParams(form.value.url_params, true, current.sslFilesPath, value, current.sslProtocol);
},
});
const damengSslProtocol = computed({
get: () => damengSslFormConfig(form.value.url_params).sslProtocol,
set: (value: string) => {
const current = damengSslFormConfig(form.value.url_params);
form.value.url_params = applyDamengSslUrlParams(form.value.url_params, true, current.sslFilesPath, current.sslKeystorePassword, value);
},
});
const mysqlTlsMode = computed({
get: () => mysqlTlsModeFromParams(form.value.url_params, form.value.ssl),
set: (value: string) => {
@ -3020,6 +3052,11 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo
if (config.db_type === "manticoresearch") {
config.url_params = "";
}
if (config.db_type === "dameng") {
const damengSsl = damengSslFormConfig(config.url_params);
config.ssl = !!config.ssl || damengSsl.enabled;
config.url_params = applyDamengSslUrlParams(config.url_params, config.ssl, damengSsl.sslFilesPath, damengSsl.sslKeystorePassword, damengSsl.sslProtocol);
}
if (config.db_type === "hive") {
if (hiveAuthMode.value === "kerberos" && !hivePrincipal.value.trim()) {
throw new Error(t("connection.hiveKerberosPrincipalRequired"));
@ -4201,6 +4238,20 @@ async function browseCaCertPath() {
}
}
async function browseDamengSslFilesPath() {
if (isTauriRuntime()) {
const { open } = await import("@tauri-apps/plugin-dialog");
const selected = await open({
title: t("connection.damengSslFilesPathBrowse"),
directory: true,
multiple: false,
});
if (selected && typeof selected === "string") {
damengSslFilesPath.value = selected;
}
}
}
async function browseMysqlTlsFile(target: "cert" | "key") {
if (isTauriRuntime()) {
const { open } = await import("@tauri-apps/plugin-dialog");
@ -6044,11 +6095,46 @@ function openExternalUrl(url: string) {
<div v-if="!supportsPostgresTlsOptions && !supportsMysqlTlsOptions" class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">SSL/TLS</Label>
<label class="col-span-3 flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="form.ssl" class="mr-0" />
<input type="checkbox" v-model="tlsEnabled" class="mr-0" />
<span class="text-xs text-muted-foreground">{{ t("connection.sslEnable") }}</span>
</label>
</div>
<template v-if="form.db_type === 'dameng'">
<div class="grid grid-cols-4 items-start gap-4">
<Label :class="connectionLabelSmallPaddedClass">{{ t("connection.damengSslFilesPath") }}</Label>
<div class="col-span-3 space-y-1.5">
<div class="flex items-center gap-1">
<Input v-model="damengSslFilesPath" class="flex-1" :placeholder="t('connection.damengSslFilesPathPlaceholder')" :disabled="!tlsEnabled" />
<Tooltip v-if="isDesktop">
<TooltipTrigger as-child>
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" :disabled="!tlsEnabled" @click="browseDamengSslFilesPath">
<FolderOpen class="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t("connection.damengSslFilesPathBrowse") }}</TooltipContent>
</Tooltip>
</div>
<p class="text-[11px] leading-4 text-muted-foreground">{{ t("connection.damengSslHint") }}</p>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">{{ t("connection.damengSslKeystorePassword") }}</Label>
<PasswordInput v-model="damengSslKeystorePassword" class="col-span-3" :disabled="!tlsEnabled" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">{{ t("connection.damengSslProtocol") }}</Label>
<Input v-model="damengSslProtocol" class="col-span-3" :placeholder="t('connection.damengSslProtocolPlaceholder')" :disabled="!tlsEnabled" />
</div>
<div class="grid grid-cols-4 items-start gap-4">
<span />
<p class="col-span-3 text-[11px] leading-4 text-muted-foreground">{{ t("connection.damengSslVerificationHint") }}</p>
</div>
</template>
<div v-if="form.db_type === 'redis'" class="grid grid-cols-4 items-start gap-4">
<Label :class="connectionLabelSmallClass">{{ t("connection.redisTlsInsecure") }}</Label>
<label class="col-span-3 flex items-start gap-2 cursor-pointer">

View File

@ -250,6 +250,14 @@ export default {
caCertPath: "CA Certificate",
caCertPathPlaceholder: "Optional, e.g. ~/.yandex/RootCA.crt",
caCertPathBrowse: "Browse certificate",
damengSslFilesPath: "Certificate directory",
damengSslFilesPathPlaceholder: "/path/to/dmcert",
damengSslFilesPathBrowse: "Select certificate directory",
damengSslKeystorePassword: "Keystore password",
damengSslHint: "DM8 JDBC reads the SSL certificates from this directory and passes both fields through URL parameters.",
damengSslProtocol: "SSL protocol",
damengSslProtocolPlaceholder: "Optional, e.g. TLSv1.2",
damengSslVerificationHint: "Certificate verification is controlled by the DM8 server ENABLE_ENCRYPT setting. Mode 5 verifies the server certificate; mode 4 encrypts without certificate verification.",
redisTlsInsecure: "Skip certificate verification",
redisTlsInsecureHint: "Equivalent to redis-cli --tls --insecure, for self-signed certificates or private CAs.",
mysqlTlsMode: "TLS Mode",

View File

@ -237,6 +237,14 @@ export default withEnglishFallback({
caCertPath: "Certificado CA",
caCertPathPlaceholder: "Opcional, p. ej. ~/.yandex/RootCA.crt",
caCertPathBrowse: "Buscar certificado",
damengSslFilesPath: "Directorio de certificados",
damengSslFilesPathPlaceholder: "/path/to/dmcert",
damengSslFilesPathBrowse: "Seleccionar directorio de certificados",
damengSslKeystorePassword: "Contraseña del almacén de claves",
damengSslHint: "DM8 JDBC lee los certificados SSL de este directorio y pasa ambos valores mediante parámetros URL.",
damengSslProtocol: "Protocolo SSL",
damengSslProtocolPlaceholder: "Opcional, p. ej. TLSv1.2",
damengSslVerificationHint: "La verificación del certificado depende de ENABLE_ENCRYPT en el servidor DM8. El modo 5 verifica el certificado del servidor; el modo 4 cifra sin verificar certificados.",
redisTlsInsecure: "Omitir verificación del certificado",
redisTlsInsecureHint: "Equivale a redis-cli --tls --insecure, para certificados autofirmados o CA privadas.",
mysqlTlsMode: "Modo TLS",

View File

@ -236,6 +236,14 @@ export default withEnglishFallback({
caCertPath: "Certificato CA",
caCertPathPlaceholder: "Opzionale, es. ~/.yandex/RootCA.crt",
caCertPathBrowse: "Sfoglia certificato",
damengSslFilesPath: "Directory certificati",
damengSslFilesPathPlaceholder: "/path/to/dmcert",
damengSslFilesPathBrowse: "Seleziona directory certificati",
damengSslKeystorePassword: "Password keystore",
damengSslHint: "DM8 JDBC legge i certificati SSL da questa directory e passa entrambi i valori tramite parametri URL.",
damengSslProtocol: "Protocollo SSL",
damengSslProtocolPlaceholder: "Opzionale, es. TLSv1.2",
damengSslVerificationHint: "La verifica del certificato dipende da ENABLE_ENCRYPT sul server DM8. La modalità 5 verifica il certificato del server; la modalità 4 cifra senza verificare i certificati.",
redisTlsInsecure: "Salta verifica certificato",
redisTlsInsecureHint: "Equivalente a redis-cli --tls --insecure, per certificati auto-firmati o CA private.",
mysqlTlsMode: "Modalità TLS",

View File

@ -236,6 +236,14 @@ export default withEnglishFallback({
caCertPath: "CA証明書",
caCertPathPlaceholder: "任意、例: ~/.yandex/RootCA.crt",
caCertPathBrowse: "証明書を参照",
damengSslFilesPath: "証明書ディレクトリ",
damengSslFilesPathPlaceholder: "/path/to/dmcert",
damengSslFilesPathBrowse: "証明書ディレクトリを選択",
damengSslKeystorePassword: "キーストアパスワード",
damengSslHint: "DM8 JDBC はこのディレクトリから SSL 証明書を読み取り、両方の設定を URL パラメータとして渡します。",
damengSslProtocol: "SSL プロトコル",
damengSslProtocolPlaceholder: "任意、例: TLSv1.2",
damengSslVerificationHint: "証明書の検証方法は DM8 サーバーの ENABLE_ENCRYPT 設定で決まります。モード 5 はサーバー証明書を検証し、モード 4 は証明書を検証せず暗号化のみ行います。",
redisTlsInsecure: "証明書の検証をスキップ",
redisTlsInsecureHint: "自己署名証明書やプライベートCA用の、redis-cli --tls --insecure 相当の設定です。",
mysqlTlsMode: "TLSモード",

View File

@ -237,6 +237,14 @@ export default withEnglishFallback({
caCertPath: "Certificado CA",
caCertPathPlaceholder: "Opcional, ex.: ~/.yandex/RootCA.crt",
caCertPathBrowse: "Procurar certificado",
damengSslFilesPath: "Diretório de certificados",
damengSslFilesPathPlaceholder: "/path/to/dmcert",
damengSslFilesPathBrowse: "Selecionar diretório de certificados",
damengSslKeystorePassword: "Senha do keystore",
damengSslHint: "O DM8 JDBC lê os certificados SSL deste diretório e envia os dois valores por parâmetros de URL.",
damengSslProtocol: "Protocolo SSL",
damengSslProtocolPlaceholder: "Opcional, ex.: TLSv1.2",
damengSslVerificationHint: "A verificação do certificado depende de ENABLE_ENCRYPT no servidor DM8. O modo 5 verifica o certificado do servidor; o modo 4 criptografa sem verificar certificados.",
redisTlsInsecure: "Ignorar verificação de certificado",
redisTlsInsecureHint: "Equivalente a redis-cli --tls --insecure, para certificados autoassinados ou CAs privadas.",
mysqlTlsMode: "Modo TLS",

View File

@ -252,6 +252,14 @@ export default withEnglishFallback({
caCertPath: "CA 证书",
caCertPathPlaceholder: "可选,例如 ~/.yandex/RootCA.crt",
caCertPathBrowse: "选择证书",
damengSslFilesPath: "证书目录",
damengSslFilesPathPlaceholder: "/path/to/dmcert",
damengSslFilesPathBrowse: "选择证书目录",
damengSslKeystorePassword: "密钥库密码",
damengSslHint: "DM8 JDBC 从该目录读取 SSL 证书,并通过 URL 参数传递这两项配置。",
damengSslProtocol: "SSL 协议",
damengSslProtocolPlaceholder: "可选,例如 TLSv1.2",
damengSslVerificationHint: "证书验证方式由 DM8 服务端 ENABLE_ENCRYPT 决定:模式 5 验证服务端证书,模式 4 仅加密而不验证证书。",
redisTlsInsecure: "跳过证书验证",
redisTlsInsecureHint: "等价于 redis-cli --tls --insecure适用于自签名证书或私有 CA。",
mysqlTlsMode: "TLS 模式",

View File

@ -237,6 +237,14 @@ export default withEnglishFallback({
caCertPath: "CA 憑證",
caCertPathPlaceholder: "可選,例如 ~/.yandex/RootCA.crt",
caCertPathBrowse: "瀏覽憑證",
damengSslFilesPath: "憑證目錄",
damengSslFilesPathPlaceholder: "/path/to/dmcert",
damengSslFilesPathBrowse: "選擇憑證目錄",
damengSslKeystorePassword: "金鑰庫密碼",
damengSslHint: "DM8 JDBC 會從此目錄讀取 SSL 憑證,並透過 URL 參數傳遞這兩項設定。",
damengSslProtocol: "SSL 協定",
damengSslProtocolPlaceholder: "可選,例如 TLSv1.2",
damengSslVerificationHint: "憑證驗證方式由 DM8 伺服器端 ENABLE_ENCRYPT 決定:模式 5 驗證伺服器憑證,模式 4 僅加密而不驗證憑證。",
redisTlsInsecure: "跳過憑證驗證",
redisTlsInsecureHint: "等價於 redis-cli --tls --insecure適用於自簽名憑證或私有 CA。",
mysqlTlsMode: "TLS 模式",

View File

@ -0,0 +1,15 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const dialogSource = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
describe("Dameng SSL connection form", () => {
it("shows Dameng in the TLS tab with certificate directory and password fields", () => {
expect(dialogSource).toContain('"questdb", "dameng", "redis"');
expect(dialogSource).toContain("<template v-if=\"form.db_type === 'dameng'\">");
expect(dialogSource).toContain('v-model="damengSslFilesPath"');
expect(dialogSource).toContain('v-model="damengSslKeystorePassword"');
expect(dialogSource).toContain('v-model="damengSslProtocol"');
expect(dialogSource).toContain('t("connection.damengSslVerificationHint")');
});
});

View File

@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import { parseConnectionUrl } from "@/lib/connection/connectionUrl";
describe("Dameng connection URLs", () => {
it("parses JDBC SSL parameters and enables the TLS form", () => {
const parsed = parseConnectionUrl("jdbc:dm://dm.example.com:5236/MAIN?sslFilesPath=/Users/test/dmcert&sslkeystorePass=secret");
expect(parsed).toMatchObject({
dbType: "dameng",
driverProfile: "dm",
host: "dm.example.com",
port: 5236,
database: "MAIN",
urlParams: "sslFilesPath=/Users/test/dmcert&sslkeystorePass=secret",
ssl: true,
});
});
});

View File

@ -0,0 +1,21 @@
import { describe, expect, it } from "vitest";
import { applyDamengSslUrlParams, damengSslFormConfig } from "@/lib/database/damengSslOptions";
describe("Dameng SSL URL parameters", () => {
it("reads SSL fields without changing parameter casing requirements", () => {
expect(damengSslFormConfig("schema=APP&sslFilesPath=/Users/test/dmcert&sslkeystorePass=secret&sslProtocol=TLSv1.2")).toEqual({
enabled: true,
sslFilesPath: "/Users/test/dmcert",
sslKeystorePassword: "secret",
sslProtocol: "TLSv1.2",
});
});
it("updates SSL fields while preserving unrelated URL parameters", () => {
expect(applyDamengSslUrlParams("?schema=APP;compatMode=oracle", true, "/opt/dm/cert", "changeit", "TLSv1.2")).toBe("schema=APP&compatMode=oracle&sslFilesPath=/opt/dm/cert&sslkeystorePass=changeit&sslProtocol=TLSv1.2");
});
it("removes only managed SSL fields when SSL is disabled", () => {
expect(applyDamengSslUrlParams("schema=APP&sslFilesPath=/opt/dm/cert&sslkeystorePass=secret&sslProtocol=TLSv1.2&foo=", false, "", "", "")).toBe("schema=APP&foo=");
});
});

View File

@ -1,5 +1,6 @@
import type { ConnectionConfig, DatabaseType } from "@/types/database";
import { h2JdbcUrlHasPasswordParam, h2JdbcUrlHasUserParam, parseH2JdbcUrl } from "@/lib/database/h2Connection";
import { damengSslFormConfig } from "@/lib/database/damengSslOptions";
export interface ParsedConnectionUrl {
name?: string;
@ -226,6 +227,10 @@ function extractMysqlCredentialParams(params: string): { username?: string; pass
}
function urlParamsRequireTls(dbType: DatabaseType, params: string): boolean {
if (dbType === "dameng") {
return damengSslFormConfig(params).enabled;
}
if (dbType === "mysql") {
const requireSsl = queryParamValue(params, "require_ssl")?.toLowerCase();
if (requireSsl === "true" || requireSsl === "1" || requireSsl === "yes") return true;

View File

@ -0,0 +1,75 @@
export interface DamengSslFormConfig {
enabled: boolean;
sslFilesPath: string;
sslKeystorePassword: string;
sslProtocol: string;
}
interface DamengUrlParam {
key: string;
value: string;
raw: string;
}
const SSL_FILES_PATH_KEY = "sslfilespath";
const SSL_KEYSTORE_PASSWORD_KEY = "sslkeystorepass";
const SSL_PROTOCOL_KEY = "sslprotocol";
export function damengSslFormConfig(urlParams?: string): DamengSslFormConfig {
const params = parseDamengUrlParams(urlParams);
const sslFilesPath = getDamengUrlParam(params, SSL_FILES_PATH_KEY);
const sslKeystorePassword = getDamengUrlParam(params, SSL_KEYSTORE_PASSWORD_KEY);
const sslProtocol = getDamengUrlParam(params, SSL_PROTOCOL_KEY);
return {
enabled: params.some((param) => isManagedDamengSslParam(param.key)),
sslFilesPath,
sslKeystorePassword,
sslProtocol,
};
}
export function applyDamengSslUrlParams(urlParams: string | undefined, enabled: boolean, sslFilesPath: string, sslKeystorePassword: string, sslProtocol: string): string {
const parts = parseDamengUrlParams(urlParams)
.filter((param) => !isManagedDamengSslParam(param.key))
.map((param) => param.raw);
if (enabled) {
const normalizedFilesPath = sslFilesPath.trim();
const normalizedProtocol = sslProtocol.trim();
if (normalizedFilesPath) parts.push(`sslFilesPath=${normalizedFilesPath}`);
if (sslKeystorePassword) parts.push(`sslkeystorePass=${sslKeystorePassword}`);
if (normalizedProtocol) parts.push(`sslProtocol=${normalizedProtocol}`);
}
return parts.join("&");
}
function parseDamengUrlParams(urlParams?: string): DamengUrlParam[] {
return (urlParams || "")
.trim()
.replace(/^[?&;]+/, "")
.replace(/[?&;]+$/, "")
.split(/[&;]/)
.map((part) => part.trim())
.filter(Boolean)
.map((raw) => {
const equals = raw.indexOf("=");
if (equals < 0) return { key: raw, value: "", raw };
return {
key: raw.slice(0, equals).trim(),
value: raw.slice(equals + 1).trim(),
raw,
};
})
.filter((param) => !!param.key);
}
function getDamengUrlParam(params: DamengUrlParam[], key: string): string {
return params.find((param) => param.key.toLowerCase() === key)?.value || "";
}
function isManagedDamengSslParam(key: string): boolean {
const normalizedKey = key.trim().toLowerCase();
return normalizedKey === SSL_FILES_PATH_KEY || normalizedKey === SSL_KEYSTORE_PASSWORD_KEY || normalizedKey === SSL_PROTOCOL_KEY;
}