fix(rabbitmq): parse connection addresses without URL

Closes #5280
This commit is contained in:
t8y2
2026-08-20 13:37:34 +08:00
parent f3cc8b7a01
commit fc67a323c4
11 changed files with 116 additions and 29 deletions
@@ -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<string, unknown> = {
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;
}
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
@@ -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");
});
});
@@ -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 {