mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 02:19:58 +08:00
♻️ refactor(redis): 抽离 Redis 连接 URI 与拓扑装配逻辑
This commit is contained in:
@@ -63,6 +63,11 @@ import { resolveConnectionSecretDraft } from "../utils/connectionSecretDraft";
|
||||
import { getCustomConnectionDsnValidationMessage } from "../utils/customConnectionDsn";
|
||||
import { mergeParsedUriValuesForForm } from "../utils/connectionUriMerge";
|
||||
import { buildRpcConnectionConfig } from "../utils/connectionRpcConfig";
|
||||
import {
|
||||
buildRedisUriFromValues,
|
||||
parseRedisUriToFormValues,
|
||||
resolveRedisConfigDraft,
|
||||
} from "../utils/redisConnectionUri";
|
||||
import {
|
||||
CONNECTION_TYPE_GROUPS,
|
||||
getAllConnectionTypeCatalogItems,
|
||||
@@ -1484,85 +1489,7 @@ const ConnectionModal: React.FC<{
|
||||
}
|
||||
|
||||
if (type === "redis") {
|
||||
const parsed =
|
||||
parseMultiHostUri(trimmedUri, "redis") ||
|
||||
parseMultiHostUri(trimmedUri, "rediss");
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
if (!parsed.hosts.length || parsed.hosts.length > MAX_URI_HOSTS) {
|
||||
return null;
|
||||
}
|
||||
if (parsed.hosts.some((entry) => !isValidUriHostEntry(entry))) {
|
||||
return null;
|
||||
}
|
||||
const topologyParam = String(
|
||||
parsed.params.get("topology") || "",
|
||||
).toLowerCase();
|
||||
const isSentinelTopology = topologyParam === "sentinel";
|
||||
const redisNodeDefaultPort = isSentinelTopology ? 26379 : 6379;
|
||||
const hostList = normalizeAddressList(parsed.hosts, redisNodeDefaultPort);
|
||||
if (!hostList.length) {
|
||||
return null;
|
||||
}
|
||||
const primary = parseHostPort(
|
||||
hostList[0] || `localhost:${redisNodeDefaultPort}`,
|
||||
redisNodeDefaultPort,
|
||||
);
|
||||
const dbText = String(parsed.database || "")
|
||||
.trim()
|
||||
.replace(/^\//, "");
|
||||
const dbIndex = Number(dbText);
|
||||
const isRediss = trimmedUri.toLowerCase().startsWith("rediss://");
|
||||
const skipVerifyText = String(parsed.params.get("skip_verify") || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const skipVerify =
|
||||
skipVerifyText === "1" ||
|
||||
skipVerifyText === "true" ||
|
||||
skipVerifyText === "yes" ||
|
||||
skipVerifyText === "on";
|
||||
return {
|
||||
host: primary?.host || "localhost",
|
||||
port: primary?.port || redisNodeDefaultPort,
|
||||
user: parsed.username || "",
|
||||
password: parsed.password || "",
|
||||
useSSL: isRediss,
|
||||
sslMode: isRediss
|
||||
? skipVerify
|
||||
? "skip-verify"
|
||||
: "required"
|
||||
: "disable",
|
||||
...extractSSLPathValuesFromParams(parsed.params, type),
|
||||
redisTopology: isSentinelTopology
|
||||
? "sentinel"
|
||||
: hostList.length > 1 || topologyParam === "cluster"
|
||||
? "cluster"
|
||||
: "single",
|
||||
redisHosts: hostList.slice(1),
|
||||
redisSentinelMaster: isSentinelTopology
|
||||
? String(
|
||||
parsed.params.get("master") ||
|
||||
parsed.params.get("master_name") ||
|
||||
parsed.params.get("sentinel_master") ||
|
||||
"",
|
||||
).trim()
|
||||
: "",
|
||||
redisSentinelUser: isSentinelTopology
|
||||
? String(
|
||||
parsed.params.get("sentinel_user") ||
|
||||
parsed.params.get("sentinel_username") ||
|
||||
"",
|
||||
).trim()
|
||||
: "",
|
||||
redisSentinelPassword: isSentinelTopology
|
||||
? String(parsed.params.get("sentinel_password") || "")
|
||||
: "",
|
||||
redisDB:
|
||||
Number.isFinite(dbIndex) && dbIndex >= 0 && dbIndex <= 15
|
||||
? Math.trunc(dbIndex)
|
||||
: 0,
|
||||
};
|
||||
return parseRedisUriToFormValues(trimmedUri);
|
||||
}
|
||||
|
||||
if (type === "mongodb") {
|
||||
@@ -1976,62 +1903,7 @@ const ConnectionModal: React.FC<{
|
||||
}
|
||||
|
||||
if (type === "redis") {
|
||||
const redisTopology = String(values.redisTopology || "single");
|
||||
const redisNodeDefaultPort = redisTopology === "sentinel" ? 26379 : 6379;
|
||||
const primary = toAddress(host, port, redisNodeDefaultPort);
|
||||
const extraRedisHosts =
|
||||
redisTopology === "cluster" || redisTopology === "sentinel"
|
||||
? normalizeAddressList(values.redisHosts, redisNodeDefaultPort)
|
||||
: [];
|
||||
const hosts = normalizeAddressList(
|
||||
[primary, ...extraRedisHosts],
|
||||
redisNodeDefaultPort,
|
||||
);
|
||||
const params = new URLSearchParams();
|
||||
if (redisTopology === "sentinel") {
|
||||
params.set("topology", "sentinel");
|
||||
const sentinelMaster = String(values.redisSentinelMaster || "").trim();
|
||||
if (sentinelMaster) {
|
||||
params.set("master", sentinelMaster);
|
||||
}
|
||||
const sentinelUser = String(values.redisSentinelUser || "").trim();
|
||||
if (sentinelUser) {
|
||||
params.set("sentinel_user", sentinelUser);
|
||||
}
|
||||
const sentinelPassword = String(values.redisSentinelPassword || "");
|
||||
if (sentinelPassword) {
|
||||
params.set("sentinel_password", sentinelPassword);
|
||||
}
|
||||
} else if (hosts.length > 1 || redisTopology === "cluster") {
|
||||
params.set("topology", "cluster");
|
||||
}
|
||||
const redisUser = String(values.user || "").trim();
|
||||
const redisPassword = String(values.password || "");
|
||||
let redisAuth = "";
|
||||
if (redisUser || redisPassword) {
|
||||
const encodedPassword = redisPassword
|
||||
? encodeURIComponent(redisPassword)
|
||||
: "";
|
||||
redisAuth = redisUser
|
||||
? `${encodeURIComponent(redisUser)}${redisPassword ? `:${encodedPassword}` : ""}@`
|
||||
: `:${encodedPassword}@`;
|
||||
}
|
||||
const redisDB = Number.isFinite(Number(values.redisDB))
|
||||
? Math.max(0, Math.min(15, Math.trunc(Number(values.redisDB))))
|
||||
: 0;
|
||||
const dbPath = `/${redisDB}`;
|
||||
if (values.useSSL) {
|
||||
const mode = String(values.sslMode || "preferred")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (mode === "skip-verify" || mode === "preferred") {
|
||||
params.set("skip_verify", "true");
|
||||
}
|
||||
}
|
||||
appendSSLPathParamsForUri(params, type, values);
|
||||
const query = params.toString();
|
||||
const scheme = values.useSSL ? "rediss" : "redis";
|
||||
return `${scheme}://${redisAuth}${hosts.join(",")}${dbPath}${query ? `?${query}` : ""}`;
|
||||
return buildRedisUriFromValues(values);
|
||||
}
|
||||
|
||||
if (isFileDatabaseType(type)) {
|
||||
@@ -3426,37 +3298,19 @@ const ConnectionModal: React.FC<{
|
||||
}
|
||||
|
||||
if (type === "redis") {
|
||||
const redisTopology = String(mergedValues.redisTopology || "single");
|
||||
const redisNodeDefaultPort = redisTopology === "sentinel" ? 26379 : defaultPort;
|
||||
if (
|
||||
redisTopology === "sentinel" &&
|
||||
(!Number(mergedValues.port) || Number(mergedValues.port) === defaultPort)
|
||||
) {
|
||||
primaryPort = redisNodeDefaultPort;
|
||||
}
|
||||
const extraRedisNodes =
|
||||
redisTopology === "cluster" || redisTopology === "sentinel"
|
||||
? normalizeAddressList(mergedValues.redisHosts, redisNodeDefaultPort)
|
||||
: [];
|
||||
const allHosts = normalizeAddressList(
|
||||
[`${primaryHost}:${primaryPort}`, ...extraRedisNodes],
|
||||
redisNodeDefaultPort,
|
||||
const redisDraft = resolveRedisConfigDraft(
|
||||
mergedValues,
|
||||
primaryHost,
|
||||
primaryPort,
|
||||
defaultPort,
|
||||
);
|
||||
if (redisTopology === "sentinel") {
|
||||
hosts = allHosts;
|
||||
topology = "sentinel";
|
||||
redisSentinelMaster = String(mergedValues.redisSentinelMaster || "").trim();
|
||||
redisSentinelUser = String(mergedValues.redisSentinelUser || "").trim();
|
||||
redisSentinelPassword = String(mergedValues.redisSentinelPassword || "");
|
||||
} else if (redisTopology === "cluster" || allHosts.length > 1) {
|
||||
hosts = allHosts;
|
||||
topology = "cluster";
|
||||
} else {
|
||||
topology = "single";
|
||||
}
|
||||
mergedValues.redisDB = Number.isFinite(Number(mergedValues.redisDB))
|
||||
? Math.max(0, Math.min(15, Math.trunc(Number(mergedValues.redisDB))))
|
||||
: 0;
|
||||
primaryPort = redisDraft.primaryPort;
|
||||
hosts = redisDraft.hosts;
|
||||
topology = redisDraft.topology;
|
||||
redisSentinelMaster = redisDraft.redisSentinelMaster;
|
||||
redisSentinelUser = redisDraft.redisSentinelUser;
|
||||
redisSentinelPassword = redisDraft.redisSentinelPassword;
|
||||
mergedValues.redisDB = redisDraft.redisDB;
|
||||
}
|
||||
|
||||
const sshConfig = mergedValues.useSSH
|
||||
|
||||
85
frontend/src/utils/redisConnectionUri.test.ts
Normal file
85
frontend/src/utils/redisConnectionUri.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
buildRedisUriFromValues,
|
||||
parseRedisUriToFormValues,
|
||||
resolveRedisConfigDraft,
|
||||
} from './redisConnectionUri';
|
||||
|
||||
describe('redisConnectionUri', () => {
|
||||
it('parses Redis Sentinel URI into form values without dropping topology fields', () => {
|
||||
const result = parseRedisUriToFormValues(
|
||||
'rediss://default:redis%40secret@sentinel-a.local:26379,sentinel-b.local/3?topology=sentinel&master=mymaster&sentinel_user=ops&sentinel_password=s%40p&skip_verify=true&sslCAPath=C%3A%2Fcerts%2Fca.pem',
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
host: 'sentinel-a.local',
|
||||
port: 26379,
|
||||
user: 'default',
|
||||
password: 'redis@secret',
|
||||
useSSL: true,
|
||||
sslMode: 'skip-verify',
|
||||
sslCAPath: 'C:/certs/ca.pem',
|
||||
redisTopology: 'sentinel',
|
||||
redisHosts: ['sentinel-b.local:26379'],
|
||||
redisSentinelMaster: 'mymaster',
|
||||
redisSentinelUser: 'ops',
|
||||
redisSentinelPassword: 's@p',
|
||||
redisDB: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('builds Redis Sentinel URI with Sentinel credentials separated from Redis auth', () => {
|
||||
expect(buildRedisUriFromValues({
|
||||
host: 'sentinel-a.local',
|
||||
port: 26379,
|
||||
redisHosts: ['sentinel-b.local', 'sentinel-b.local:26379'],
|
||||
redisTopology: 'sentinel',
|
||||
user: 'default',
|
||||
password: 'redis secret',
|
||||
redisSentinelMaster: 'mymaster',
|
||||
redisSentinelUser: 'sentinel-user',
|
||||
redisSentinelPassword: 'sentinel secret',
|
||||
redisDB: 6,
|
||||
useSSL: true,
|
||||
sslMode: 'required',
|
||||
sslCAPath: 'C:/certs/ca.pem',
|
||||
})).toBe(
|
||||
'rediss://default:redis%20secret@sentinel-a.local:26379,sentinel-b.local:26379/6?topology=sentinel&master=mymaster&sentinel_user=sentinel-user&sentinel_password=sentinel+secret&sslCAPath=C%3A%2Fcerts%2Fca.pem',
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves Redis config draft for cluster and Sentinel save payloads', () => {
|
||||
expect(resolveRedisConfigDraft({
|
||||
redisTopology: 'cluster',
|
||||
redisHosts: ['redis-b.local', 'redis-c.local:6380'],
|
||||
redisDB: 2,
|
||||
}, 'redis-a.local', 6379, 6379)).toEqual({
|
||||
primaryPort: 6379,
|
||||
hosts: ['redis-a.local:6379', 'redis-b.local:6379', 'redis-c.local:6380'],
|
||||
topology: 'cluster',
|
||||
redisSentinelMaster: '',
|
||||
redisSentinelUser: '',
|
||||
redisSentinelPassword: '',
|
||||
redisDB: 2,
|
||||
});
|
||||
|
||||
expect(resolveRedisConfigDraft({
|
||||
redisTopology: 'sentinel',
|
||||
port: 6379,
|
||||
redisHosts: ['sentinel-b.local'],
|
||||
redisSentinelMaster: 'mymaster',
|
||||
redisSentinelUser: 'ops',
|
||||
redisSentinelPassword: 'sentinel-pass',
|
||||
redisDB: 99,
|
||||
}, 'sentinel-a.local', 6379, 6379)).toEqual({
|
||||
primaryPort: 26379,
|
||||
hosts: ['sentinel-a.local:26379', 'sentinel-b.local:26379'],
|
||||
topology: 'sentinel',
|
||||
redisSentinelMaster: 'mymaster',
|
||||
redisSentinelUser: 'ops',
|
||||
redisSentinelPassword: 'sentinel-pass',
|
||||
redisDB: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
452
frontend/src/utils/redisConnectionUri.ts
Normal file
452
frontend/src/utils/redisConnectionUri.ts
Normal file
@@ -0,0 +1,452 @@
|
||||
import type { ConnectionConfig } from '../types';
|
||||
|
||||
export type RedisTopology = Extract<
|
||||
NonNullable<ConnectionConfig['topology']>,
|
||||
'single' | 'cluster' | 'sentinel'
|
||||
>;
|
||||
|
||||
export interface RedisUriFormValues {
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
password: string;
|
||||
useSSL: boolean;
|
||||
sslMode: 'required' | 'skip-verify' | 'disable';
|
||||
sslCAPath?: string;
|
||||
sslCertPath?: string;
|
||||
sslKeyPath?: string;
|
||||
redisTopology: RedisTopology;
|
||||
redisHosts: string[];
|
||||
redisSentinelMaster: string;
|
||||
redisSentinelUser: string;
|
||||
redisSentinelPassword: string;
|
||||
redisDB: number;
|
||||
}
|
||||
|
||||
export interface RedisConfigDraft {
|
||||
primaryPort: number;
|
||||
hosts: string[];
|
||||
topology: RedisTopology;
|
||||
redisSentinelMaster: string;
|
||||
redisSentinelUser: string;
|
||||
redisSentinelPassword: string;
|
||||
redisDB: number;
|
||||
}
|
||||
|
||||
const REDIS_DEFAULT_PORT = 6379;
|
||||
const REDIS_SENTINEL_DEFAULT_PORT = 26379;
|
||||
const MAX_URI_HOSTS = 32;
|
||||
|
||||
const parseHostPort = (
|
||||
raw: string,
|
||||
defaultPort: number,
|
||||
): { host: string; port: number } | null => {
|
||||
const text = String(raw || '').trim();
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
if (text.startsWith('[')) {
|
||||
const closingBracket = text.indexOf(']');
|
||||
if (closingBracket > 0) {
|
||||
const host = text.slice(1, closingBracket).trim();
|
||||
const portText = text
|
||||
.slice(closingBracket + 1)
|
||||
.trim()
|
||||
.replace(/^:/, '');
|
||||
const parsedPort = Number(portText);
|
||||
return {
|
||||
host: host || 'localhost',
|
||||
port:
|
||||
Number.isFinite(parsedPort) && parsedPort > 0 && parsedPort <= 65535
|
||||
? parsedPort
|
||||
: defaultPort,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const colonCount = (text.match(/:/g) || []).length;
|
||||
if (colonCount === 1) {
|
||||
const splitIndex = text.lastIndexOf(':');
|
||||
const host = text.slice(0, splitIndex).trim();
|
||||
const portText = text.slice(splitIndex + 1).trim();
|
||||
const parsedPort = Number(portText);
|
||||
return {
|
||||
host: host || 'localhost',
|
||||
port:
|
||||
Number.isFinite(parsedPort) && parsedPort > 0 && parsedPort <= 65535
|
||||
? parsedPort
|
||||
: defaultPort,
|
||||
};
|
||||
}
|
||||
|
||||
return { host: text, port: defaultPort };
|
||||
};
|
||||
|
||||
const toAddress = (host: string, port: number, defaultPort: number) => {
|
||||
const safeHost = String(host || '').trim() || 'localhost';
|
||||
const safePort =
|
||||
Number.isFinite(Number(port)) && Number(port) > 0
|
||||
? Number(port)
|
||||
: defaultPort;
|
||||
return `${safeHost}:${safePort}`;
|
||||
};
|
||||
|
||||
const normalizeAddressList = (
|
||||
rawList: unknown,
|
||||
defaultPort: number,
|
||||
): string[] => {
|
||||
const list = Array.isArray(rawList) ? rawList : [];
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
list.forEach((entry) => {
|
||||
const parsed = parseHostPort(String(entry || ''), defaultPort);
|
||||
if (!parsed) {
|
||||
return;
|
||||
}
|
||||
const normalized = toAddress(parsed.host, parsed.port, defaultPort);
|
||||
if (seen.has(normalized)) {
|
||||
return;
|
||||
}
|
||||
seen.add(normalized);
|
||||
result.push(normalized);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const isValidUriHostEntry = (entry: string): boolean => {
|
||||
const text = String(entry || '').trim();
|
||||
if (!text) return false;
|
||||
if (text.length > 255) return false;
|
||||
return !/[()\\/\s]/.test(text);
|
||||
};
|
||||
|
||||
const safeDecode = (text: string) => {
|
||||
try {
|
||||
return decodeURIComponent(text);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
};
|
||||
|
||||
const parseMultiHostUri = (uriText: string, expectedScheme: string) => {
|
||||
const prefix = `${expectedScheme}://`;
|
||||
if (!uriText.toLowerCase().startsWith(prefix)) {
|
||||
return null;
|
||||
}
|
||||
let rest = uriText.slice(prefix.length);
|
||||
const hashIndex = rest.indexOf('#');
|
||||
if (hashIndex >= 0) {
|
||||
rest = rest.slice(0, hashIndex);
|
||||
}
|
||||
let queryText = '';
|
||||
const queryIndex = rest.indexOf('?');
|
||||
if (queryIndex >= 0) {
|
||||
queryText = rest.slice(queryIndex + 1);
|
||||
rest = rest.slice(0, queryIndex);
|
||||
}
|
||||
|
||||
let pathText = '';
|
||||
const slashIndex = rest.indexOf('/');
|
||||
if (slashIndex >= 0) {
|
||||
pathText = rest.slice(slashIndex + 1);
|
||||
rest = rest.slice(0, slashIndex);
|
||||
}
|
||||
|
||||
let hostText = rest;
|
||||
let username = '';
|
||||
let password = '';
|
||||
const atIndex = rest.lastIndexOf('@');
|
||||
if (atIndex >= 0) {
|
||||
const userInfo = rest.slice(0, atIndex);
|
||||
hostText = rest.slice(atIndex + 1);
|
||||
const colonIndex = userInfo.indexOf(':');
|
||||
if (colonIndex >= 0) {
|
||||
username = safeDecode(userInfo.slice(0, colonIndex));
|
||||
password = safeDecode(userInfo.slice(colonIndex + 1));
|
||||
} else {
|
||||
username = safeDecode(userInfo);
|
||||
}
|
||||
}
|
||||
|
||||
const hosts = hostText
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
username,
|
||||
password,
|
||||
hosts,
|
||||
database: safeDecode(pathText),
|
||||
params: new URLSearchParams(queryText),
|
||||
};
|
||||
};
|
||||
|
||||
const firstConnectionParamValue = (
|
||||
params: URLSearchParams,
|
||||
names: string[],
|
||||
): string => {
|
||||
for (const name of names) {
|
||||
const value = String(params.get(name) || '').trim();
|
||||
if (value) return value;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const extractRedisSSLPathValuesFromParams = (
|
||||
params: URLSearchParams,
|
||||
): Pick<RedisUriFormValues, 'sslCAPath' | 'sslCertPath' | 'sslKeyPath'> => {
|
||||
const caPath = firstConnectionParamValue(params, [
|
||||
'sslCAPath',
|
||||
'ssl_ca_path',
|
||||
'sslrootcert',
|
||||
'sslRootCert',
|
||||
'tlsCAFile',
|
||||
'caFile',
|
||||
'certificate',
|
||||
'servercertificate',
|
||||
'serverCertificate',
|
||||
]);
|
||||
const certPath = firstConnectionParamValue(params, [
|
||||
'sslCertPath',
|
||||
'ssl_cert_path',
|
||||
'SSL_CERT_PATH',
|
||||
'sslcert',
|
||||
'sslCert',
|
||||
'tlsCertificateFile',
|
||||
]);
|
||||
const keyPath = firstConnectionParamValue(params, [
|
||||
'sslKeyPath',
|
||||
'ssl_key_path',
|
||||
'SSL_KEY_PATH',
|
||||
'sslkey',
|
||||
'sslKey',
|
||||
'tlsKeyFile',
|
||||
]);
|
||||
return {
|
||||
...(caPath ? { sslCAPath: caPath } : {}),
|
||||
...(certPath ? { sslCertPath: certPath } : {}),
|
||||
...(keyPath ? { sslKeyPath: keyPath } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
const appendRedisSSLPathParamsForUri = (
|
||||
params: URLSearchParams,
|
||||
values: Record<string, any>,
|
||||
) => {
|
||||
const caPath = String(values.sslCAPath || '').trim();
|
||||
const certPath = String(values.sslCertPath || '').trim();
|
||||
const keyPath = String(values.sslKeyPath || '').trim();
|
||||
if (caPath) {
|
||||
params.set('sslCAPath', caPath);
|
||||
}
|
||||
if (certPath) {
|
||||
params.set('sslCertPath', certPath);
|
||||
}
|
||||
if (keyPath) {
|
||||
params.set('sslKeyPath', keyPath);
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeRedisDB = (value: unknown): number => {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) && parsed >= 0 && parsed <= 15
|
||||
? Math.trunc(parsed)
|
||||
: 0;
|
||||
};
|
||||
|
||||
const normalizeRedisTopology = (value: unknown): RedisTopology => {
|
||||
const text = String(value || '').trim().toLowerCase();
|
||||
if (text === 'cluster' || text === 'sentinel') {
|
||||
return text;
|
||||
}
|
||||
return 'single';
|
||||
};
|
||||
|
||||
export const parseRedisUriToFormValues = (
|
||||
uriText: string,
|
||||
): RedisUriFormValues | null => {
|
||||
const trimmedUri = String(uriText || '').trim();
|
||||
const parsed =
|
||||
parseMultiHostUri(trimmedUri, 'redis') ||
|
||||
parseMultiHostUri(trimmedUri, 'rediss');
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
if (!parsed.hosts.length || parsed.hosts.length > MAX_URI_HOSTS) {
|
||||
return null;
|
||||
}
|
||||
if (parsed.hosts.some((entry) => !isValidUriHostEntry(entry))) {
|
||||
return null;
|
||||
}
|
||||
const topologyParam = String(parsed.params.get('topology') || '').toLowerCase();
|
||||
const isSentinelTopology = topologyParam === 'sentinel';
|
||||
const redisNodeDefaultPort = isSentinelTopology
|
||||
? REDIS_SENTINEL_DEFAULT_PORT
|
||||
: REDIS_DEFAULT_PORT;
|
||||
const hostList = normalizeAddressList(parsed.hosts, redisNodeDefaultPort);
|
||||
if (!hostList.length) {
|
||||
return null;
|
||||
}
|
||||
const primary = parseHostPort(
|
||||
hostList[0] || `localhost:${redisNodeDefaultPort}`,
|
||||
redisNodeDefaultPort,
|
||||
);
|
||||
const dbText = String(parsed.database || '')
|
||||
.trim()
|
||||
.replace(/^\//, '');
|
||||
const isRediss = trimmedUri.toLowerCase().startsWith('rediss://');
|
||||
const skipVerifyText = String(parsed.params.get('skip_verify') || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const skipVerify =
|
||||
skipVerifyText === '1' ||
|
||||
skipVerifyText === 'true' ||
|
||||
skipVerifyText === 'yes' ||
|
||||
skipVerifyText === 'on';
|
||||
return {
|
||||
host: primary?.host || 'localhost',
|
||||
port: primary?.port || redisNodeDefaultPort,
|
||||
user: parsed.username || '',
|
||||
password: parsed.password || '',
|
||||
useSSL: isRediss,
|
||||
sslMode: isRediss ? (skipVerify ? 'skip-verify' : 'required') : 'disable',
|
||||
...extractRedisSSLPathValuesFromParams(parsed.params),
|
||||
redisTopology: isSentinelTopology
|
||||
? 'sentinel'
|
||||
: hostList.length > 1 || topologyParam === 'cluster'
|
||||
? 'cluster'
|
||||
: 'single',
|
||||
redisHosts: hostList.slice(1),
|
||||
redisSentinelMaster: isSentinelTopology
|
||||
? String(
|
||||
parsed.params.get('master') ||
|
||||
parsed.params.get('master_name') ||
|
||||
parsed.params.get('sentinel_master') ||
|
||||
'',
|
||||
).trim()
|
||||
: '',
|
||||
redisSentinelUser: isSentinelTopology
|
||||
? String(
|
||||
parsed.params.get('sentinel_user') ||
|
||||
parsed.params.get('sentinel_username') ||
|
||||
'',
|
||||
).trim()
|
||||
: '',
|
||||
redisSentinelPassword: isSentinelTopology
|
||||
? String(parsed.params.get('sentinel_password') || '')
|
||||
: '',
|
||||
redisDB: normalizeRedisDB(dbText),
|
||||
};
|
||||
};
|
||||
|
||||
export const buildRedisUriFromValues = (values: Record<string, any>): string => {
|
||||
const redisTopology = normalizeRedisTopology(values.redisTopology);
|
||||
const redisNodeDefaultPort =
|
||||
redisTopology === 'sentinel'
|
||||
? REDIS_SENTINEL_DEFAULT_PORT
|
||||
: REDIS_DEFAULT_PORT;
|
||||
const primary = toAddress(
|
||||
String(values.host || '').trim() || 'localhost',
|
||||
Number(values.port || redisNodeDefaultPort),
|
||||
redisNodeDefaultPort,
|
||||
);
|
||||
const extraRedisHosts =
|
||||
redisTopology === 'cluster' || redisTopology === 'sentinel'
|
||||
? normalizeAddressList(values.redisHosts, redisNodeDefaultPort)
|
||||
: [];
|
||||
const hosts = normalizeAddressList(
|
||||
[primary, ...extraRedisHosts],
|
||||
redisNodeDefaultPort,
|
||||
);
|
||||
const params = new URLSearchParams();
|
||||
if (redisTopology === 'sentinel') {
|
||||
params.set('topology', 'sentinel');
|
||||
const sentinelMaster = String(values.redisSentinelMaster || '').trim();
|
||||
if (sentinelMaster) {
|
||||
params.set('master', sentinelMaster);
|
||||
}
|
||||
const sentinelUser = String(values.redisSentinelUser || '').trim();
|
||||
if (sentinelUser) {
|
||||
params.set('sentinel_user', sentinelUser);
|
||||
}
|
||||
const sentinelPassword = String(values.redisSentinelPassword || '');
|
||||
if (sentinelPassword) {
|
||||
params.set('sentinel_password', sentinelPassword);
|
||||
}
|
||||
} else if (hosts.length > 1 || redisTopology === 'cluster') {
|
||||
params.set('topology', 'cluster');
|
||||
}
|
||||
const redisUser = String(values.user || '').trim();
|
||||
const redisPassword = String(values.password || '');
|
||||
let redisAuth = '';
|
||||
if (redisUser || redisPassword) {
|
||||
const encodedPassword = redisPassword
|
||||
? encodeURIComponent(redisPassword)
|
||||
: '';
|
||||
redisAuth = redisUser
|
||||
? `${encodeURIComponent(redisUser)}${redisPassword ? `:${encodedPassword}` : ''}@`
|
||||
: `:${encodedPassword}@`;
|
||||
}
|
||||
const redisDB = normalizeRedisDB(values.redisDB);
|
||||
if (values.useSSL) {
|
||||
const mode = String(values.sslMode || 'preferred')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (mode === 'skip-verify' || mode === 'preferred') {
|
||||
params.set('skip_verify', 'true');
|
||||
}
|
||||
}
|
||||
appendRedisSSLPathParamsForUri(params, values);
|
||||
const query = params.toString();
|
||||
const scheme = values.useSSL ? 'rediss' : 'redis';
|
||||
return `${scheme}://${redisAuth}${hosts.join(',')}/${redisDB}${query ? `?${query}` : ''}`;
|
||||
};
|
||||
|
||||
export const resolveRedisConfigDraft = (
|
||||
values: Record<string, any>,
|
||||
primaryHost: string,
|
||||
primaryPort: number,
|
||||
defaultPort: number,
|
||||
): RedisConfigDraft => {
|
||||
const redisTopology = normalizeRedisTopology(values.redisTopology);
|
||||
const redisNodeDefaultPort =
|
||||
redisTopology === 'sentinel'
|
||||
? REDIS_SENTINEL_DEFAULT_PORT
|
||||
: defaultPort;
|
||||
const normalizedPrimaryPort =
|
||||
redisTopology === 'sentinel' &&
|
||||
(!Number(values.port) || Number(values.port) === defaultPort)
|
||||
? redisNodeDefaultPort
|
||||
: primaryPort;
|
||||
const extraRedisNodes =
|
||||
redisTopology === 'cluster' || redisTopology === 'sentinel'
|
||||
? normalizeAddressList(values.redisHosts, redisNodeDefaultPort)
|
||||
: [];
|
||||
const allHosts = normalizeAddressList(
|
||||
[`${primaryHost}:${normalizedPrimaryPort}`, ...extraRedisNodes],
|
||||
redisNodeDefaultPort,
|
||||
);
|
||||
|
||||
if (redisTopology === 'sentinel') {
|
||||
return {
|
||||
primaryPort: normalizedPrimaryPort,
|
||||
hosts: allHosts,
|
||||
topology: 'sentinel',
|
||||
redisSentinelMaster: String(values.redisSentinelMaster || '').trim(),
|
||||
redisSentinelUser: String(values.redisSentinelUser || '').trim(),
|
||||
redisSentinelPassword: String(values.redisSentinelPassword || ''),
|
||||
redisDB: normalizeRedisDB(values.redisDB),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
primaryPort: normalizedPrimaryPort,
|
||||
hosts: redisTopology === 'cluster' || allHosts.length > 1 ? allHosts : [],
|
||||
topology: redisTopology === 'cluster' || allHosts.length > 1 ? 'cluster' : 'single',
|
||||
redisSentinelMaster: '',
|
||||
redisSentinelUser: '',
|
||||
redisSentinelPassword: '',
|
||||
redisDB: normalizeRedisDB(values.redisDB),
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user