diff --git a/Cargo.lock b/Cargo.lock index 2a928c26f..b62456a58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1879,6 +1879,7 @@ dependencies = [ name = "dbx-core" version = "0.1.0" dependencies = [ + "aes 0.8.4", "aes-gcm 0.10.3", "anyhow", "argon2", @@ -1886,6 +1887,7 @@ dependencies = [ "base64 0.22.1", "bytes", "calamine", + "cbc 0.1.2", "chrono", "csv", "deadpool-postgres", diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index ab1e77bb4..39317fc73 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -16,7 +16,7 @@ import { Switch } from "@/components/ui/switch"; import type { ConnectionConfig, ConnectionTestResult, DatabaseConnectionInfo, DatabaseType, HttpTunnelConfig, IdentifierCase, JdbcDriverInfo, JdbcLocalBundleInfo, JdbcMavenBundleInfo, ProxyTunnelConfig, SshConfigHostEntry, SshTunnelConfig, TransportLayerConfig } from "@/types/database"; import type { InfluxDbExternalConfig, InfluxDbVersion } from "@/types/influxdb"; import type { MqAdminConfig, MqAuth, MqSystemKind } from "@/types/mq"; -import type { NacosAdminConfig, NacosAuthConfig } from "@/types/nacos"; +import type { NacosAdminConfig, NacosAuthConfig, NacosImplementation, NacosRNacosConsoleAuth, NacosVersionMode } from "@/types/nacos"; import { CONNECTION_ATTEMPT_CANCELLED_MESSAGE, useConnectionStore } from "@/stores/connectionStore"; import { useTunnelProfileStore } from "@/stores/tunnelProfileStore"; import { detachTunnelProfileLayer, tunnelProfileReferenceLayer, tunnelProfileSummary } from "@/lib/connection/tunnelProfiles"; @@ -53,6 +53,7 @@ import { normalizeRabbitmqAddresses } from "@/lib/connection/rabbitmqAddresses"; import { detectMqUiAuthKind, isMqAuthKindAllowedForSystem, type MqUiAuthKind } from "@/lib/connection/mqAuth"; import { driverInstallProgressChannel, driverInstallProgressPercent, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi"; import { requiresSqlServerLegacyCompatibilityComponent, setSqlServerLegacyCompatibilityConfig, sqlServerUsesLegacyCompatibility, SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY } from "@/lib/connection/sqlServerLegacyCompatibility"; +import { normalizeNacosEndpoint } from "@/lib/nacos/nacosAdmin"; import { ArrowLeft, ArrowDown, @@ -114,9 +115,6 @@ const DREMIO_ARROW_FLIGHT_SQL_JDBC_URL = "jdbc:arrow-flight-sql://127.0.0.1:3201 const DREMIO_ARROW_FLIGHT_SQL_JDBC_DRIVER_CLASS = "org.apache.arrow.driver.jdbc.ArrowFlightJdbcDriver"; const DREMIO_LEGACY_JDBC_URL = "jdbc:dremio:direct=127.0.0.1:31010"; const DREMIO_LEGACY_JDBC_DRIVER_CLASS = "com.dremio.jdbc.Driver"; -const NACOS_DEFAULT_CONSOLE_URL = "http://127.0.0.1:8085"; -const NACOS_LEGACY_SERVER_PORT = "8848"; -const NACOS_DOCKER_CONSOLE_PORT = "8085"; const DEFAULT_SSH_USER = "root"; type LegacyTransportFields = { @@ -586,14 +584,77 @@ const mqKafkaSaslMechanismOptions = [ { value: "SCRAM-SHA-256", label: "SCRAM-SHA-256" }, { value: "SCRAM-SHA-512", label: "SCRAM-SHA-512" }, ]; -const nacosServerAddr = ref(NACOS_DEFAULT_CONSOLE_URL); +const nacosImplementation = ref("nacos"); +const nacosVersionMode = ref("auto"); +const nacosServerAddr = ref(""); const nacosNamespace = ref(""); const nacosContextPath = ref(""); +const nacosContextPathCustomized = ref(false); +const nacosRNacosConsoleAddr = ref(""); +const nacosHistoryEnabled = ref(false); +const nacosConsoleAuthKind = ref("inherit"); +const nacosConsoleUsername = ref(""); +const nacosConsolePassword = ref(""); const nacosAuthKind = ref("none"); const nacosUsername = ref("nacos"); const nacosPassword = ref(""); const nacosTlsSkipVerify = ref(false); const nacosPageSize = ref(20); +const nacosPrimaryAddressLabel = computed(() => { + if (nacosImplementation.value === "rnacos") return t("connection.nacosPrimaryAddressRNacos"); + if (nacosVersionMode.value === "v2") return t("connection.nacosPrimaryAddressV2"); + if (nacosVersionMode.value === "v3") return t("connection.nacosPrimaryAddressV3"); + return t("connection.nacosPrimaryAddressAuto"); +}); +const nacosPrimaryAddressPlaceholder = computed(() => { + if (nacosImplementation.value === "rnacos" || nacosVersionMode.value === "v2") return "http://127.0.0.1:8848/nacos"; + return "http://127.0.0.1:8080"; +}); +const nacosNormalizedPreview = computed(() => { + if (!nacosServerAddr.value.trim()) return ""; + try { + const normalized = normalizeNacosEndpoint(nacosServerAddr.value, { + implementation: nacosImplementation.value, + versionMode: nacosVersionMode.value, + contextPath: nacosContextPathCustomized.value ? nacosContextPath.value : undefined, + }); + return `${normalized.serverAddr}${normalized.contextPath || ""}`; + } catch { + return ""; + } +}); +const nacosEffectiveContextPath = computed(() => { + if (!nacosServerAddr.value.trim()) { + return nacosContextPathCustomized.value ? nacosContextPath.value.trim() || "/" : nacosImplementation.value === "rnacos" || nacosVersionMode.value === "v2" ? "/nacos" : "/"; + } + try { + const normalized = normalizeNacosEndpoint(nacosServerAddr.value, { + implementation: nacosImplementation.value, + versionMode: nacosVersionMode.value, + contextPath: nacosContextPathCustomized.value ? nacosContextPath.value : undefined, + }); + return normalized.contextPath || "/"; + } catch { + return nacosContextPathCustomized.value ? nacosContextPath.value.trim() || "/" : "/"; + } +}); +const nacosContextPathInput = computed({ + get: () => (nacosContextPathCustomized.value ? nacosContextPath.value : nacosEffectiveContextPath.value), + set: (value: string) => { + nacosContextPathCustomized.value = true; + nacosContextPath.value = value; + }, +}); + +function resetNacosContextPathCustomization() { + nacosContextPathCustomized.value = false; + nacosContextPath.value = ""; +} + +watch(nacosImplementation, (implementation) => { + if (implementation === "rnacos") nacosVersionMode.value = "auto"; + if (implementation !== "rnacos") nacosHistoryEnabled.value = false; +}); const colorOptions = [ { value: "", class: "bg-transparent border-dashed", labelKey: "connection.colorNone" }, @@ -1002,9 +1063,18 @@ watch(mqAuthKind, (kind) => { }); function resetNacosFields(config?: Partial) { - nacosServerAddr.value = config?.serverAddr?.trim() || NACOS_DEFAULT_CONSOLE_URL; + nacosImplementation.value = config?.implementation || (config?.rnacosConsoleAddr ? "rnacos" : "nacos"); + nacosVersionMode.value = config?.versionMode || "auto"; + nacosServerAddr.value = config?.serverAddr?.trim() || ""; nacosNamespace.value = config?.namespace || ""; nacosContextPath.value = config?.contextPath || ""; + nacosContextPathCustomized.value = !!config?.contextPath; + nacosRNacosConsoleAddr.value = config?.rnacosConsoleAddr?.trim() || ""; + nacosHistoryEnabled.value = config?.rnacosHistoryEnabled ?? !!config?.rnacosConsoleAddr; + const consoleAuth = config?.rnacosConsoleAuth || { kind: "inherit" }; + nacosConsoleAuthKind.value = consoleAuth.kind; + nacosConsoleUsername.value = consoleAuth.kind === "usernamePassword" ? consoleAuth.username : ""; + nacosConsolePassword.value = consoleAuth.kind === "usernamePassword" ? consoleAuth.password : ""; nacosTlsSkipVerify.value = !!config?.tlsSkipVerify; nacosPageSize.value = Number(config?.pageSize) > 0 ? Number(config?.pageSize) : 20; const auth = (config?.auth || { kind: "none" }) as NacosAuthConfig; @@ -1200,10 +1270,38 @@ function buildNacosAuth(): NacosAuthConfig { } function buildNacosAdminConfig(): NacosAdminConfig { + const primaryAddress = requireMqField(nacosServerAddr.value, t("connection.nacosConsoleUrlRequired")); + const normalized = normalizeNacosEndpoint(primaryAddress, { + implementation: nacosImplementation.value, + versionMode: nacosVersionMode.value, + contextPath: nacosContextPathCustomized.value ? nacosContextPath.value : undefined, + }); + if (nacosImplementation.value === "rnacos" && normalized.warnings.length) { + throw new Error(t("connection.nacosRNacosOpenApiRequired")); + } + let rnacosConsoleAuth: NacosRNacosConsoleAuth | undefined; + if (nacosImplementation.value === "rnacos" && nacosHistoryEnabled.value) { + if (!nacosRNacosConsoleAddr.value.trim()) throw new Error(t("connection.nacosRNacosConsoleUrlRequired")); + if (nacosConsoleAuthKind.value === "inherit") { + if (nacosAuthKind.value !== "usernamePassword") throw new Error(t("connection.nacosConsoleAuthSeparateRequired")); + rnacosConsoleAuth = { kind: "inherit" }; + } else { + rnacosConsoleAuth = { + kind: "usernamePassword", + username: requireMqField(nacosConsoleUsername.value, t("connection.nacosConsoleUsernameRequired")), + password: nacosConsolePassword.value, + }; + } + } return { - serverAddr: requireMqField(nacosServerAddr.value, t("connection.nacosConsoleUrlRequired")), + implementation: nacosImplementation.value, + versionMode: nacosImplementation.value === "nacos" ? nacosVersionMode.value : undefined, + serverAddr: normalized.serverAddr, namespace: nacosNamespace.value.trim() || undefined, - contextPath: nacosContextPath.value.trim(), + contextPath: normalized.contextPath || undefined, + rnacosConsoleAddr: nacosImplementation.value === "rnacos" && nacosHistoryEnabled.value ? nacosRNacosConsoleAddr.value.trim() || undefined : undefined, + rnacosHistoryEnabled: nacosImplementation.value === "rnacos" ? nacosHistoryEnabled.value : undefined, + rnacosConsoleAuth, auth: buildNacosAuth(), tlsSkipVerify: nacosTlsSkipVerify.value || undefined, pageSize: Number(nacosPageSize.value) > 0 ? Number(nacosPageSize.value) : 20, @@ -1219,10 +1317,9 @@ function dockerNacosConsoleFallbackUrl(serverAddr: string): string | null { } const port = parsed.port || (parsed.protocol === "https:" ? "443" : "80"); const host = parsed.hostname.toLowerCase(); - if (port !== NACOS_LEGACY_SERVER_PORT || !["127.0.0.1", "localhost", "::1"].includes(host)) { - return null; - } - parsed.port = NACOS_DOCKER_CONSOLE_PORT; + if (port !== "8848" || !["127.0.0.1", "localhost", "::1"].includes(host)) return null; + + parsed.port = "8085"; return parsed.toString().replace(/\/$/, ""); } @@ -1232,6 +1329,7 @@ function isNacosAdminEndpointNotFound(message: string): boolean { async function tryNacosDockerConsoleFallback(config: ConnectionConfig, originalError: string, runId: number): Promise { if (config.db_type !== "nacos" || !isNacosAdminEndpointNotFound(originalError)) return null; + const fallbackUrl = dockerNacosConsoleFallbackUrl(nacosServerAddr.value); if (!fallbackUrl || fallbackUrl === nacosServerAddr.value.trim()) return null; @@ -1404,6 +1502,11 @@ async function setSqlServerDriverMode(mode: "auto" | "legacy") { } } +function isSqlServerTlsHandshakeFailure(message: string): boolean { + const text = message.toLowerCase(); + return text.includes("sql server") && text.includes("tls") && (text.includes("handshake") || text.includes("eof") || text.includes("performing i/o")); +} + function clearTestedConnectionInfo() { testedConfigFingerprint.value = ""; testedConfigId.value = ""; @@ -2661,6 +2764,10 @@ async function testConnection() { void persistSuccessfulConnectionTest(fallback.result, fallback.config, submittedSourceName, runId); clearEditedConnectionErrorAfterSuccessfulTest(); } else { + const shouldShowSqlServerLegacyMode = config?.db_type === "sqlserver" && !sqlServerUsesLegacyCompatibility(config) && isSqlServerTlsHandshakeFailure(message); + if (shouldShowSqlServerLegacyMode) { + configTab.value = "advanced"; + } clearTestedConnectionInfo(); testResult.value = { ok: false, message }; showConnectionError(message); @@ -4456,7 +4563,7 @@ function openExternalUrl(url: string) {
-
+
@@ -5018,24 +5125,92 @@ function openExternalUrl(url: string) {
- +