mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-02 20:49:48 +08:00
🔧 fix(connection-uri): 修复URI解析成功后异常配置落盘导致应用崩溃
- 收紧 ConnectionModal 的 URI 解析校验(长度、主机数量、主机格式、端口范围、超时上限) - 为 URI 回填增加异常兜底,避免解析阶段触发前端崩溃 - 在 store persist 的 migrate/merge 增加连接配置净化,启动时自动隔离坏数据 - 补充 ConnectionConfig 的 driver/dsn/timeout 类型并同步需求追踪文档
This commit is contained in:
@@ -7,6 +7,9 @@ import { MongoMemberInfo, SavedConnection } from '../types';
|
||||
|
||||
const { Meta } = Card;
|
||||
const { Text } = Typography;
|
||||
const MAX_URI_LENGTH = 4096;
|
||||
const MAX_URI_HOSTS = 32;
|
||||
const MAX_TIMEOUT_SECONDS = 3600;
|
||||
|
||||
const getDefaultPortByType = (type: string) => {
|
||||
switch (type) {
|
||||
@@ -60,7 +63,7 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal
|
||||
const parsedPort = Number(portText);
|
||||
return {
|
||||
host: host || 'localhost',
|
||||
port: Number.isFinite(parsedPort) && parsedPort > 0 ? parsedPort : defaultPort,
|
||||
port: Number.isFinite(parsedPort) && parsedPort > 0 && parsedPort <= 65535 ? parsedPort : defaultPort,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -73,7 +76,7 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal
|
||||
const parsedPort = Number(portText);
|
||||
return {
|
||||
host: host || 'localhost',
|
||||
port: Number.isFinite(parsedPort) && parsedPort > 0 ? parsedPort : defaultPort,
|
||||
port: Number.isFinite(parsedPort) && parsedPort > 0 && parsedPort <= 65535 ? parsedPort : defaultPort,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -105,6 +108,15 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal
|
||||
return result;
|
||||
};
|
||||
|
||||
const isValidUriHostEntry = (entry: string): boolean => {
|
||||
const text = String(entry || '').trim();
|
||||
if (!text) return false;
|
||||
if (text.length > 255) return false;
|
||||
// 拒绝明显的 DSN 片段或路径/空白,避免把非 URI 主机段误判为合法地址。
|
||||
if (/[()\\/\s]/.test(text)) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const normalizeMongoSrvHostList = (rawList: unknown, defaultPort: number): string[] => {
|
||||
const list = Array.isArray(rawList) ? rawList : [];
|
||||
const seen = new Set<string>();
|
||||
@@ -138,6 +150,10 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal
|
||||
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) {
|
||||
@@ -187,6 +203,9 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal
|
||||
if (!trimmedUri) {
|
||||
return null;
|
||||
}
|
||||
if (trimmedUri.length > MAX_URI_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (type === 'mysql' || type === 'mariadb' || type === 'sphinx') {
|
||||
const mysqlDefaultPort = getDefaultPortByType(type);
|
||||
@@ -194,18 +213,30 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal
|
||||
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 hostList = normalizeAddressList(parsed.hosts, mysqlDefaultPort);
|
||||
if (!hostList.length) {
|
||||
return null;
|
||||
}
|
||||
const primary = parseHostPort(hostList[0] || `localhost:${mysqlDefaultPort}`, mysqlDefaultPort);
|
||||
const timeoutValue = Number(parsed.params.get('timeout'));
|
||||
const topology = String(parsed.params.get('topology') || '').toLowerCase();
|
||||
return {
|
||||
host: primary?.host || 'localhost',
|
||||
port: primary?.port || mysqlDefaultPort,
|
||||
user: parsed.username,
|
||||
password: parsed.password,
|
||||
database: parsed.database || '',
|
||||
mysqlTopology: hostList.length > 1 || parsed.params.get('topology') === 'replica' ? 'replica' : 'single',
|
||||
mysqlTopology: hostList.length > 1 || topology === 'replica' ? 'replica' : 'single',
|
||||
mysqlReplicaHosts: hostList.slice(1),
|
||||
timeout: Number.isFinite(timeoutValue) && timeoutValue > 0 ? timeoutValue : undefined,
|
||||
timeout: Number.isFinite(timeoutValue) && timeoutValue > 0
|
||||
? Math.min(3600, Math.trunc(timeoutValue))
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -214,10 +245,19 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal
|
||||
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 isSrv = trimmedUri.toLowerCase().startsWith('mongodb+srv://');
|
||||
const hostList = isSrv
|
||||
? normalizeMongoSrvHostList(parsed.hosts, 27017)
|
||||
: normalizeAddressList(parsed.hosts, 27017);
|
||||
if (!hostList.length) {
|
||||
return null;
|
||||
}
|
||||
const primary = isSrv
|
||||
? { host: hostList[0] || 'localhost', port: 27017 }
|
||||
: parseHostPort(hostList[0] || 'localhost:27017', 27017);
|
||||
@@ -235,7 +275,9 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal
|
||||
mongoAuthSource: parsed.params.get('authSource') || '',
|
||||
mongoReadPreference: parsed.params.get('readPreference') || 'primary',
|
||||
mongoAuthMechanism: parsed.params.get('authMechanism') || '',
|
||||
timeout: Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.ceil(timeoutMs / 1000) : undefined,
|
||||
timeout: Number.isFinite(timeoutMs) && timeoutMs > 0
|
||||
? Math.min(MAX_TIMEOUT_SECONDS, Math.ceil(timeoutMs / 1000))
|
||||
: undefined,
|
||||
savePassword: true,
|
||||
};
|
||||
}
|
||||
@@ -357,22 +399,26 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal
|
||||
};
|
||||
|
||||
const handleParseURI = () => {
|
||||
const uriText = String(form.getFieldValue('uri') || '').trim();
|
||||
const type = String(form.getFieldValue('type') || dbType).trim().toLowerCase();
|
||||
if (!uriText) {
|
||||
message.warning('请先输入 URI');
|
||||
return;
|
||||
try {
|
||||
const uriText = String(form.getFieldValue('uri') || '').trim();
|
||||
const type = String(form.getFieldValue('type') || dbType).trim().toLowerCase();
|
||||
if (!uriText) {
|
||||
message.warning('请先输入 URI');
|
||||
return;
|
||||
}
|
||||
const parsedValues = parseUriToValues(uriText, type);
|
||||
if (!parsedValues) {
|
||||
message.error('当前 URI 与数据源类型不匹配,或 URI 格式不支持');
|
||||
return;
|
||||
}
|
||||
form.setFieldsValue({ ...parsedValues, uri: uriText });
|
||||
if (testResult) {
|
||||
setTestResult(null);
|
||||
}
|
||||
message.success('已根据 URI 回填连接参数');
|
||||
} catch {
|
||||
message.error('URI 解析失败,请检查格式后重试');
|
||||
}
|
||||
const parsedValues = parseUriToValues(uriText, type);
|
||||
if (!parsedValues) {
|
||||
message.error('当前 URI 与数据源类型不匹配,或 URI 格式不支持');
|
||||
return;
|
||||
}
|
||||
form.setFieldsValue({ ...parsedValues, uri: uriText });
|
||||
if (testResult) {
|
||||
setTestResult(null);
|
||||
}
|
||||
message.success('已根据 URI 回填连接参数');
|
||||
};
|
||||
|
||||
const handleCopyURI = async () => {
|
||||
|
||||
Reference in New Issue
Block a user