From fc67a323c4c779250d30bdee141b64efe7933998 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Thu, 20 Aug 2026 13:37:34 +0800 Subject: [PATCH] fix(rabbitmq): parse connection addresses without URL Closes #5280 --- .../connection/ConnectionDialog.vue | 20 +++--- apps/desktop/src/i18n/locales/en.ts | 2 +- apps/desktop/src/i18n/locales/es.ts | 2 +- apps/desktop/src/i18n/locales/it.ts | 2 +- apps/desktop/src/i18n/locales/ja.ts | 2 +- apps/desktop/src/i18n/locales/ko.ts | 2 +- apps/desktop/src/i18n/locales/pt-BR.ts | 2 +- apps/desktop/src/i18n/locales/zh-CN.ts | 2 +- apps/desktop/src/i18n/locales/zh-TW.ts | 2 +- .../__tests__/rabbitmqAddresses.spec.ts | 38 +++++++++- .../src/lib/connection/rabbitmqAddresses.ts | 71 ++++++++++++++++--- 11 files changed, 116 insertions(+), 29 deletions(-) diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index bedc16cb4a..15259fe001 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -65,7 +65,7 @@ import { buildMqKafkaConnectionExtra, mqKafkaConnectionTarget, resolveMqKafkaCon import { assertCompleteDatabaseCategories, databaseSelectionForCategory } from "@/lib/connection/databaseCategoryOptions"; import { loadConnectionPickerView, saveConnectionPickerView, type DbPickerView } from "@/lib/connection/connectionPickerViewPreference"; import { normalizeRocketmqNamesrvAddr } from "@/lib/connection/rocketmqNamesrv"; -import { normalizeRabbitmqAddresses } from "@/lib/connection/rabbitmqAddresses"; +import { normalizeRabbitmqAddresses, parseRabbitmqAddress } from "@/lib/connection/rabbitmqAddresses"; import { detectMqUiAuthKind, isMqAuthKindAllowedForSystem, type MqUiAuthKind } from "@/lib/connection/mqAuth"; import { driverInstallProgressChannel, driverInstallProgressPercent, isDriverInstallProgressForOperation, requestAgentInstallCancellation, resolveAgentInstallOutcome, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi"; import { requiresSqlServerLegacyCompatibilityComponent, setSqlServerLegacyCompatibilityConfig, sqlServerUsesLegacyCompatibility, SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY } from "@/lib/connection/sqlServerLegacyCompatibility"; @@ -1744,7 +1744,12 @@ function buildMqAdminConfig(): MqAdminConfig { } if (systemKind === "rabbitmq") { - const addresses = normalizeRabbitmqAddresses(mqRabbitmqAddresses.value); + let addresses: string; + try { + addresses = normalizeRabbitmqAddresses(mqRabbitmqAddresses.value); + } catch { + throw new Error(t(mqRabbitmqAddresses.value.trim() ? "connection.mqRabbitmqAddressesInvalid" : "connection.mqRabbitmqAddressesRequired")); + } const extra: Record = { addresses, virtualHost: mqRabbitmqVirtualHost.value.trim() || "/", @@ -2287,14 +2292,9 @@ function applyMqKafkaConnectionTarget(config: LegacyConnectionConfig, extra: Rec function applyMqRabbitmqAddresses(config: LegacyConnectionConfig, addresses: string) { const first = normalizeRabbitmqAddresses(addresses).split(",")[0]; if (!first) throw new Error(t("connection.mqRabbitmqAddressesRequired")); - let parsed: URL; - try { - parsed = new URL(`amqp://${first}`); - } catch { - throw new Error(t("connection.mqRabbitmqAddressesInvalid")); - } - config.host = parsed.hostname; - config.port = Number(parsed.port) || 5672; + const parsed = parseRabbitmqAddress(first); + config.host = parsed.host; + config.port = parsed.port; config.ssl = false; } diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index ccabcaf984..4ff784ee79 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -740,7 +740,7 @@ export default { mqRabbitmqAddresses: "Addresses", mqRabbitmqAddressesPlaceholder: "127.0.0.1:5672", mqRabbitmqAddressesRequired: "RabbitMQ addresses are required", - mqRabbitmqAddressesInvalid: "RabbitMQ addresses are invalid", + mqRabbitmqAddressesInvalid: "Enter RabbitMQ addresses as host:port (for example, 127.0.0.1:5672), without amqp://", mqVirtualHost: "Virtual Host", mqVirtualHostPlaceholder: "/", mqRabbitmqAdminUrl: "Management URL", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 96744c9c77..4664021105 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -938,7 +938,7 @@ export default withEnglishFallback({ mqRabbitmqAddresses: "Direcciones", mqRabbitmqAddressesPlaceholder: "127.0.0.1:5672", mqRabbitmqAddressesRequired: "Las direcciones de RabbitMQ son obligatorias", - mqRabbitmqAddressesInvalid: "Las direcciones de RabbitMQ no son válidas", + mqRabbitmqAddressesInvalid: "Introduce las direcciones de RabbitMQ como host:puerto (por ejemplo, 127.0.0.1:5672), sin amqp://", mqVirtualHost: "Host virtual", mqVirtualHostPlaceholder: "/", mqRabbitmqAdminUrl: "Management URL", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 9d70dc506c..5f8ef212b8 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -936,7 +936,7 @@ export default withEnglishFallback({ mqRabbitmqAddresses: "Indirizzi", mqRabbitmqAddressesPlaceholder: "127.0.0.1:5672", mqRabbitmqAddressesRequired: "Gli indirizzi RabbitMQ sono obbligatori", - mqRabbitmqAddressesInvalid: "Gli indirizzi RabbitMQ non sono validi", + mqRabbitmqAddressesInvalid: "Inserisci gli indirizzi RabbitMQ come host:porta (ad esempio 127.0.0.1:5672), senza amqp://", mqVirtualHost: "Host virtuale", mqVirtualHostPlaceholder: "/", mqRabbitmqAdminUrl: "Management URL", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 970732b899..19a3e20afb 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -936,7 +936,7 @@ export default withEnglishFallback({ mqRabbitmqAddresses: "アドレス", mqRabbitmqAddressesPlaceholder: "127.0.0.1:5672", mqRabbitmqAddressesRequired: "RabbitMQ アドレスは必須です", - mqRabbitmqAddressesInvalid: "RabbitMQ アドレスが無効です", + mqRabbitmqAddressesInvalid: "RabbitMQ アドレスは host:port 形式(例: 127.0.0.1:5672)で入力し、amqp:// は含めないでください", mqVirtualHost: "仮想ホスト", mqVirtualHostPlaceholder: "/", mqRabbitmqAdminUrl: "Management URL", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 9e6702140f..d88f415cad 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -730,7 +730,7 @@ export default withEnglishFallback({ mqRabbitmqAddresses: "주소", mqRabbitmqAddressesPlaceholder: "127.0.0.1:5672", mqRabbitmqAddressesRequired: "RabbitMQ 주소는 필수입니다", - mqRabbitmqAddressesInvalid: "RabbitMQ 주소가 잘못되었습니다", + mqRabbitmqAddressesInvalid: "RabbitMQ 주소를 host:port 형식(예: 127.0.0.1:5672)으로 입력하고 amqp://는 포함하지 마세요", mqVirtualHost: "가상 호스트", mqVirtualHostPlaceholder: "/", mqRabbitmqAdminUrl: "관리 URL", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 61a3cbaf21..ca886039d7 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -937,7 +937,7 @@ export default withEnglishFallback({ mqRabbitmqAddresses: "Endereços", mqRabbitmqAddressesPlaceholder: "127.0.0.1:5672", mqRabbitmqAddressesRequired: "Os endereços do RabbitMQ são obrigatórios", - mqRabbitmqAddressesInvalid: "Os endereços do RabbitMQ são inválidos", + mqRabbitmqAddressesInvalid: "Informe os endereços do RabbitMQ como host:porta (por exemplo, 127.0.0.1:5672), sem amqp://", mqVirtualHost: "Host virtual", mqVirtualHostPlaceholder: "/", mqRabbitmqAdminUrl: "Management URL", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 5db5c0572a..966fb83203 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -664,7 +664,7 @@ export default withEnglishFallback({ mqRabbitmqAddresses: "地址", mqRabbitmqAddressesPlaceholder: "127.0.0.1:5672", mqRabbitmqAddressesRequired: "RabbitMQ 地址不能为空", - mqRabbitmqAddressesInvalid: "RabbitMQ 地址无效", + mqRabbitmqAddressesInvalid: "RabbitMQ 地址无效,请填写 host:port(例如 127.0.0.1:5672),不要包含 amqp://", mqVirtualHost: "虚拟主机", mqVirtualHostPlaceholder: "/", mqRabbitmqAdminUrl: "Management URL", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 01b8b35a4b..4f95607232 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -936,7 +936,7 @@ export default withEnglishFallback({ mqRabbitmqAddresses: "位址", mqRabbitmqAddressesPlaceholder: "127.0.0.1:5672", mqRabbitmqAddressesRequired: "RabbitMQ 位址不能為空", - mqRabbitmqAddressesInvalid: "RabbitMQ 位址無效", + mqRabbitmqAddressesInvalid: "RabbitMQ 位址無效,請填寫 host:port(例如 127.0.0.1:5672),不要包含 amqp://", mqVirtualHost: "虛擬主機", mqVirtualHostPlaceholder: "/", mqRabbitmqAdminUrl: "Management URL", diff --git a/apps/desktop/src/lib/connection/__tests__/rabbitmqAddresses.spec.ts b/apps/desktop/src/lib/connection/__tests__/rabbitmqAddresses.spec.ts index d44357dfb1..429357d6fb 100644 --- a/apps/desktop/src/lib/connection/__tests__/rabbitmqAddresses.spec.ts +++ b/apps/desktop/src/lib/connection/__tests__/rabbitmqAddresses.spec.ts @@ -1,7 +1,11 @@ -import { describe, expect, it } from "vitest"; -import { normalizeRabbitmqAddresses } from "@/lib/connection/rabbitmqAddresses"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { normalizeRabbitmqAddresses, parseRabbitmqAddress } from "@/lib/connection/rabbitmqAddresses"; describe("RabbitMQ addresses", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + it("keeps comma-separated addresses", () => { expect(normalizeRabbitmqAddresses("node1:5672, node2:5672")).toBe("node1:5672,node2:5672"); }); @@ -11,12 +15,36 @@ describe("RabbitMQ addresses", () => { expect(normalizeRabbitmqAddresses("node1, node2:5673")).toBe("node1:5672,node2:5673"); }); + it("keeps IPv4 addresses with custom ports", () => { + expect(normalizeRabbitmqAddresses("192.0.2.176:51016")).toBe("192.0.2.176:51016"); + expect(parseRabbitmqAddress("192.0.2.176:51016")).toEqual({ host: "192.0.2.176", port: 51016 }); + }); + + it("parses IPv4 addresses without the browser URL implementation", () => { + vi.stubGlobal( + "URL", + class { + constructor() { + throw new Error("custom schemes are unsupported"); + } + }, + ); + expect(normalizeRabbitmqAddresses("192.0.2.176:51016")).toBe("192.0.2.176:51016"); + expect(parseRabbitmqAddress("192.0.2.176:51016")).toEqual({ host: "192.0.2.176", port: 51016 }); + }); + it("normalizes common address separators to commas", () => { expect(normalizeRabbitmqAddresses("node1:5672;node2:5672,node3:5672\nnode4:5672 node5:5672")).toBe("node1:5672,node2:5672,node3:5672,node4:5672,node5:5672"); }); it("keeps IPv6 addresses", () => { expect(normalizeRabbitmqAddresses("[::1]:5672;[2001:db8::1]:5672")).toBe("[::1]:5672,[2001:db8::1]:5672"); + expect(parseRabbitmqAddress("[::1]:5672")).toEqual({ host: "::1", port: 5672 }); + }); + + it("appends the default port to IPv6 addresses without a port", () => { + expect(normalizeRabbitmqAddresses("[::1]")).toBe("[::1]:5672"); + expect(parseRabbitmqAddress("[::1]")).toEqual({ host: "::1", port: 5672 }); }); it("rejects empty addresses", () => { @@ -30,4 +58,10 @@ describe("RabbitMQ addresses", () => { it("rejects invalid address values", () => { expect(() => normalizeRabbitmqAddresses("node1:5672/path,node2:5672")).toThrow("RabbitMQ addresses are invalid"); }); + + it("rejects invalid ports", () => { + expect(() => normalizeRabbitmqAddresses("node1:")).toThrow("RabbitMQ addresses are invalid"); + expect(() => normalizeRabbitmqAddresses("node1:0")).toThrow("RabbitMQ addresses are invalid"); + expect(() => normalizeRabbitmqAddresses("node1:65536")).toThrow("RabbitMQ addresses are invalid"); + }); }); diff --git a/apps/desktop/src/lib/connection/rabbitmqAddresses.ts b/apps/desktop/src/lib/connection/rabbitmqAddresses.ts index 819341582a..e99da5f009 100644 --- a/apps/desktop/src/lib/connection/rabbitmqAddresses.ts +++ b/apps/desktop/src/lib/connection/rabbitmqAddresses.ts @@ -1,5 +1,12 @@ const RABBITMQ_ADDRESS_SEPARATOR = /[\s,;,;]+/u; const RABBITMQ_DEFAULT_PORT = "5672"; +const RABBITMQ_PORT_PATTERN = /^\d+$/u; +const RABBITMQ_INVALID_HOST_CHARACTER = /[\s/?#@%\\\u200b-\u200d\ufeff]/u; + +export interface RabbitmqAddress { + host: string; + port: number; +} function requireRabbitmqAddresses(value: string): string { const trimmed = value.trim(); @@ -7,20 +14,66 @@ function requireRabbitmqAddresses(value: string): string { return trimmed; } -function normalizeRabbitmqAddress(address: string): string { +function invalidRabbitmqAddresses(): never { + throw new Error("RabbitMQ addresses are invalid"); +} + +function validateRabbitmqPort(port: string): void { + if (!RABBITMQ_PORT_PATTERN.test(port)) invalidRabbitmqAddresses(); + const parsed = Number(port); + if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) invalidRabbitmqAddresses(); +} + +function validateRabbitmqHostname(host: string): void { + if (!host || RABBITMQ_INVALID_HOST_CHARACTER.test(host)) invalidRabbitmqAddresses(); +} + +function validateRabbitmqIpv6(host: string): void { + try { + const parsed = new URL(`http://[${host}]`); + if (!parsed.hostname || parsed.username || parsed.password || parsed.port || parsed.search || parsed.hash || parsed.pathname !== "/") { + invalidRabbitmqAddresses(); + } + } catch { + invalidRabbitmqAddresses(); + } +} + +export function parseRabbitmqAddress(address: string): RabbitmqAddress { if (address.includes("://")) { throw new Error("RabbitMQ addresses must be host:port values without a URL scheme"); } - let parsed: URL; - try { - parsed = new URL(`amqp://${address}`); - } catch { - throw new Error("RabbitMQ addresses are invalid"); + + if (address.startsWith("[")) { + const match = address.match(/^\[([^\]]+)\](?::([^:]+))?$/u); + if (!match) invalidRabbitmqAddresses(); + const [, host, port] = match; + validateRabbitmqIpv6(host); + if (port) { + validateRabbitmqPort(port); + return { host, port: Number(port) }; + } + return { host, port: Number(RABBITMQ_DEFAULT_PORT) }; } - if (!parsed.hostname || parsed.username || parsed.password || parsed.search || parsed.hash || (parsed.pathname && parsed.pathname !== "/")) { - throw new Error("RabbitMQ addresses are invalid"); + + if (address.includes("[") || address.includes("]")) invalidRabbitmqAddresses(); + const separator = address.indexOf(":"); + if (separator !== address.lastIndexOf(":")) invalidRabbitmqAddresses(); + const host = separator >= 0 ? address.slice(0, separator) : address; + const port = separator >= 0 ? address.slice(separator + 1) : ""; + if (!host || (separator >= 0 && !port)) invalidRabbitmqAddresses(); + validateRabbitmqHostname(host); + if (port) { + validateRabbitmqPort(port); + return { host, port: Number(port) }; } - return parsed.port ? address : `${address}:${RABBITMQ_DEFAULT_PORT}`; + return { host, port: Number(RABBITMQ_DEFAULT_PORT) }; +} + +function normalizeRabbitmqAddress(address: string): string { + const parsed = parseRabbitmqAddress(address); + const hasExplicitPort = address.startsWith("[") ? address.includes("]:") : address.includes(":"); + return hasExplicitPort ? address : `${address}:${parsed.port}`; } export function normalizeRabbitmqAddresses(value: string): string {