fix(mysql): add TLS failure hint
This commit is contained in:
parent
38f9893425
commit
95397f7152
|
|
@ -37,6 +37,7 @@ import { agentDriverInstallKey, appendAgentDriverUpdateHint, hasAgentDriverUpdat
|
|||
import { prestoSqlBuiltinDriverPaths } from "@/lib/database/prestoSqlBuiltinDriver";
|
||||
import { SQLITE_DATABASE_FILE_EXTENSIONS } from "@/lib/database/databaseFileDetection";
|
||||
import { connectionAttemptOriginalErrorMessage, connectionAttemptTimeoutMessage, connectionAttemptTimeoutMs } from "@/lib/connection/connectionAttemptTimeout";
|
||||
import { appendConnectionErrorHints } from "@/lib/connection/connectionErrorHints";
|
||||
import { driverInstallProgressPercent, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi";
|
||||
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";
|
||||
|
|
@ -918,6 +919,7 @@ function errorMessage(error: unknown): string {
|
|||
}
|
||||
|
||||
function connectionErrorWithDriverUpdateHint(config: ConnectionConfig, message: string): string {
|
||||
message = appendConnectionErrorHints(config, message, t);
|
||||
if (!hasAgentDriverUpdate(config.db_type, agentDrivers.value, config.driver_profile)) return message;
|
||||
return appendAgentDriverUpdateHint(message, t("connection.agentDriverUpdateConnectionHint"));
|
||||
}
|
||||
|
|
@ -3039,7 +3041,7 @@ async function save() {
|
|||
const message = String(e?.message || e);
|
||||
if (message.includes(CONNECTION_ATTEMPT_CANCELLED_MESSAGE)) return;
|
||||
if (config.one_time) void store.removeConnection(config.id);
|
||||
emit("connectFailed", mongodbAuthFailureHint(message));
|
||||
emit("connectFailed", appendConnectionErrorHints(config, mongodbAuthFailureHint(message), t));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -215,6 +215,7 @@ export default {
|
|||
mysqlClientCertPlaceholder: "/path/to/client.crt",
|
||||
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.",
|
||||
mysqlClientCertBrowse: "Choose client certificate",
|
||||
mysqlClientKeyBrowse: "Choose client private key",
|
||||
postgresSslMode: "TLS Mode",
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@ export default withEnglishFallback({
|
|||
mysqlClientCertPlaceholder: "/ruta/a/client.crt",
|
||||
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.",
|
||||
mysqlClientCertBrowse: "Elegir certificado cliente",
|
||||
mysqlClientKeyBrowse: "Elegir clave privada cliente",
|
||||
postgresSslMode: "Modo TLS",
|
||||
|
|
|
|||
|
|
@ -216,6 +216,7 @@ export default withEnglishFallback({
|
|||
mysqlClientCertPlaceholder: "/percorso/per/client.crt",
|
||||
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.",
|
||||
mysqlClientCertBrowse: "Scegli certificato client",
|
||||
mysqlClientKeyBrowse: "Scegli chiave privata client",
|
||||
postgresSslMode: "Modalità TLS",
|
||||
|
|
|
|||
|
|
@ -216,6 +216,7 @@ export default withEnglishFallback({
|
|||
mysqlClientCertPlaceholder: "/path/to/client.crt",
|
||||
mysqlClientKeyPlaceholder: "/path/to/client.key",
|
||||
mysqlClientCertHint: "MySQLがmTLSを要求する場合、クライアント証明書と秘密鍵の両方を指定する必要があります。",
|
||||
mysqlTlsConnectionFailureHint: "MySQLサーバーがTLSを要求しない場合は、接続を編集し、TLSモードを無効にしてから再試行してください。",
|
||||
mysqlClientCertBrowse: "クライアント証明書を選択",
|
||||
mysqlClientKeyBrowse: "クライアント秘密鍵を選択",
|
||||
postgresSslMode: "TLSモード",
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@ export default withEnglishFallback({
|
|||
mysqlClientCertPlaceholder: "/caminho/para/client.crt",
|
||||
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.",
|
||||
mysqlClientCertBrowse: "Escolher certificado do cliente",
|
||||
mysqlClientKeyBrowse: "Escolher chave privada do cliente",
|
||||
postgresSslMode: "Modo TLS",
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@ export default withEnglishFallback({
|
|||
mysqlClientCertPlaceholder: "/path/to/client.crt",
|
||||
mysqlClientKeyPlaceholder: "/path/to/client.key",
|
||||
mysqlClientCertHint: "MySQL 要求 mTLS 时,客户端证书和私钥必须一起填写。",
|
||||
mysqlTlsConnectionFailureHint: "如果 MySQL 服务端不要求 TLS,请编辑连接,在 TLS 模式中选择“禁用”后重试。",
|
||||
mysqlClientCertBrowse: "选择客户端证书",
|
||||
mysqlClientKeyBrowse: "选择客户端私钥",
|
||||
postgresSslMode: "TLS 模式",
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@ export default withEnglishFallback({
|
|||
mysqlClientCertPlaceholder: "/path/to/client.crt",
|
||||
mysqlClientKeyPlaceholder: "/path/to/client.key",
|
||||
mysqlClientCertHint: "MySQL 要求 mTLS 時,用戶端憑證和私鑰必須一起填寫。",
|
||||
mysqlTlsConnectionFailureHint: "如果 MySQL 伺服器不要求 TLS,請編輯連線,在 TLS 模式中選擇「停用」後重試。",
|
||||
mysqlClientCertBrowse: "選擇用戶端憑證",
|
||||
mysqlClientKeyBrowse: "選擇用戶端私鑰",
|
||||
postgresSslMode: "TLS 模式",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { appendConnectionErrorHints } from "@/lib/connection/connectionErrorHints";
|
||||
import type { ConnectionConfig } from "@/types/database";
|
||||
|
||||
function mysqlConfig(urlParams: string | undefined): ConnectionConfig {
|
||||
return {
|
||||
id: "mysql-test",
|
||||
name: "MySQL",
|
||||
db_type: "mysql",
|
||||
host: "127.0.0.1",
|
||||
port: 3306,
|
||||
username: "root",
|
||||
password: "",
|
||||
database: undefined,
|
||||
url_params: urlParams,
|
||||
ssl: false,
|
||||
};
|
||||
}
|
||||
|
||||
const t = (key: string) => (key === "connection.mysqlTlsConnectionFailureHint" ? "Set TLS Mode to Disabled." : key);
|
||||
|
||||
describe("appendConnectionErrorHints", () => {
|
||||
it("adds a MySQL TLS hint for non-disabled TLS failures", () => {
|
||||
const message = appendConnectionErrorHints(mysqlConfig("ssl-mode=preferred"), "MySQL connection failed: TLS handshake failed", t);
|
||||
|
||||
expect(message).toContain("TLS handshake failed");
|
||||
expect(message).toContain("Set TLS Mode to Disabled.");
|
||||
});
|
||||
|
||||
it("adds the TLS hint for camel-case MySQL sslMode params", () => {
|
||||
const message = appendConnectionErrorHints(mysqlConfig("sslMode=REQUIRED"), "MySQL connection failed: Driver error: `Client asked for SSL but server does not have this capability'", t);
|
||||
|
||||
expect(message).toContain("server does not have this capability");
|
||||
expect(message).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);
|
||||
|
||||
expect(message).toBe("MySQL connection failed: TLS handshake failed");
|
||||
});
|
||||
|
||||
it("does not add the TLS hint for non-TLS errors", () => {
|
||||
const message = appendConnectionErrorHints(mysqlConfig("ssl-mode=preferred"), "Access denied for user root", t);
|
||||
|
||||
expect(message).toBe("Access denied for user root");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import type { ConnectionConfig } from "@/types/database";
|
||||
|
||||
type Translate = (key: string) => string;
|
||||
|
||||
function normalizeUrlParams(params: string | undefined): URLSearchParams {
|
||||
return new URLSearchParams((params || "").trim().replace(/^\?/, ""));
|
||||
}
|
||||
|
||||
function normalizeUrlParamKey(key: string): string {
|
||||
return key.trim().toLowerCase().replace(/[-_]/g, "");
|
||||
}
|
||||
|
||||
function urlParamValue(params: URLSearchParams, key: string): string {
|
||||
const normalizedKey = normalizeUrlParamKey(key);
|
||||
for (const [paramKey, value] of params.entries()) {
|
||||
if (normalizeUrlParamKey(paramKey) === normalizedKey) return value;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function mysqlTlsMode(config: ConnectionConfig): string {
|
||||
const parsed = normalizeUrlParams(config.url_params);
|
||||
// MySQL clients use ssl-mode, sslmode and sslMode spellings; keep hint behavior aligned with backend parsing.
|
||||
const mode = urlParamValue(parsed, "ssl-mode").trim().toLowerCase().replace("-", "_");
|
||||
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";
|
||||
return "disabled";
|
||||
}
|
||||
|
||||
function isMysqlTlsLikeFailure(message: string): boolean {
|
||||
const text = message.toLowerCase();
|
||||
return (
|
||||
(text.includes("mysql") || text.includes("mariadb") || text.includes("tidb") || text.includes("tls") || text.includes("ssl")) &&
|
||||
(text.includes("tls") || text.includes("ssl") || text.includes("handshake") || text.includes("certificate") || text.includes("cert") || text.includes("unknown ca") || text.includes("self signed"))
|
||||
);
|
||||
}
|
||||
|
||||
export function appendConnectionErrorHints(config: ConnectionConfig | undefined, message: string, t: Translate): string {
|
||||
if (!config || config.db_type !== "mysql") return message;
|
||||
if (mysqlTlsMode(config) === "disabled") return message;
|
||||
if (!isMysqlTlsLikeFailure(message)) return message;
|
||||
const hint = t("connection.mysqlTlsConnectionFailureHint");
|
||||
return message.includes(hint) ? message : `${message}\n\n${hint}`;
|
||||
}
|
||||
|
|
@ -60,6 +60,7 @@ import { completionSchemasFromTree, completionTablesFromTree } from "@/lib/metad
|
|||
import { kvRootNodeLabel } from "@/lib/kv/kvRootPresentation";
|
||||
import { REDIS_SCAN_PAGE_SIZE_DEFAULT } from "@/lib/redis/redisKeyPattern";
|
||||
import { appendAgentDriverUpdateHint, hasAgentDriverUpdate, type AgentDriverInstallState } from "@/lib/connection/agentDriverInstallHint";
|
||||
import { appendConnectionErrorHints } from "@/lib/connection/connectionErrorHints";
|
||||
import { createMetadataLoadTrace, logMetadataLoadTrace, MetadataLoadCoordinator, type MetadataLoadTraceLogger } from "@/lib/metadata/metadataLoadCoordinator";
|
||||
import type { MetadataScopeInput } from "@/lib/metadata/metadataLoadScope";
|
||||
import { MetadataResultCache, type MetadataCacheInvalidation } from "@/lib/metadata/metadataResultCache";
|
||||
|
|
@ -522,6 +523,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
|
||||
function connectionErrorWithDriverUpdateHint(config: ConnectionConfig | undefined, message: string): string {
|
||||
if (!config) return message;
|
||||
message = appendConnectionErrorHints(config, message, i18n.global.t);
|
||||
if (!hasAgentDriverUpdate(config.db_type, agentDrivers.value, config.driver_profile)) return message;
|
||||
return appendAgentDriverUpdateHint(message, agentDriverUpdateHint());
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue