dsh-db-tool 1.8.0 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/client.js +36 -5
- package/dist/adapters/dmdb/index.js +8 -1
- package/dist/adapters/mongodb/index.js +4 -1
- package/dist/adapters/mysql/index.js +5 -3
- package/dist/adapters/oracle/index.js +6 -2
- package/dist/adapters/redis/index.js +5 -2
- package/dist/adapters/sql-shared/common.js +15 -0
- package/dist/adapters/sql-shared/pg-like.js +14 -6
- package/dist/adapters/sqlserver/index.js +37 -9
- package/dist/store/connections.js +3 -0
- package/package.json +1 -1
package/client/client.js
CHANGED
|
@@ -105,6 +105,7 @@ window.__ModuleLoader__.load({
|
|
|
105
105
|
urlUser: "用户名(可选,自动注入 URL)",
|
|
106
106
|
urlPassword: "密码(可选,自动注入 URL;留空保持已存)",
|
|
107
107
|
fieldsHost: "主机地址",
|
|
108
|
+
fieldsHostInstance: "主机地址(命名实例填 主机\\实例名,无需端口)",
|
|
108
109
|
fieldsPort: "端口",
|
|
109
110
|
fieldsUser: "用户名",
|
|
110
111
|
fieldsPassword: "密码",
|
|
@@ -323,6 +324,7 @@ window.__ModuleLoader__.load({
|
|
|
323
324
|
urlUser: "Username (optional, injected into URL)",
|
|
324
325
|
urlPassword: "Password (optional, injected into URL; blank keeps saved)",
|
|
325
326
|
fieldsHost: "Host",
|
|
327
|
+
fieldsHostInstance: "Host (named instance: host\\instance, no port needed)",
|
|
326
328
|
fieldsPort: "Port",
|
|
327
329
|
fieldsUser: "User",
|
|
328
330
|
fieldsPassword: "Password",
|
|
@@ -1251,7 +1253,7 @@ window.__ModuleLoader__.load({
|
|
|
1251
1253
|
mongodb: { host: "127.0.0.1", port: 27017, database: "test" },
|
|
1252
1254
|
oracle: { host: "127.0.0.1", port: 1521, user: "system" },
|
|
1253
1255
|
dmdb: { host: "127.0.0.1", port: 5236, user: "SYSDBA" },
|
|
1254
|
-
sqlserver: { host: "127.0.0.1",
|
|
1256
|
+
sqlserver: { host: "127.0.0.1", user: "sa" },
|
|
1255
1257
|
};
|
|
1256
1258
|
|
|
1257
1259
|
/**
|
|
@@ -1268,6 +1270,24 @@ window.__ModuleLoader__.load({
|
|
|
1268
1270
|
return next;
|
|
1269
1271
|
}
|
|
1270
1272
|
|
|
1273
|
+
/** URL→分字段的字段反解:只取可信部分。safeUrl 仅脱敏密码段(用户名/主机/端口为真值),
|
|
1274
|
+
* 密码不可反解——留空走编辑语义,clearUrl 时由服务端迁移 URL 内嵌密码。非法 URL 返回 null。 */
|
|
1275
|
+
function urlToFieldsPatch(url) {
|
|
1276
|
+
if (!url) return null;
|
|
1277
|
+
try {
|
|
1278
|
+
const u = new URL(url);
|
|
1279
|
+
const decoded = (s) => { try { return decodeURIComponent(s); } catch { return s; } };
|
|
1280
|
+
return {
|
|
1281
|
+
host: u.hostname,
|
|
1282
|
+
port: u.port,
|
|
1283
|
+
user: decoded(u.username),
|
|
1284
|
+
database: decoded(u.pathname.replace(/^\//, "")),
|
|
1285
|
+
};
|
|
1286
|
+
} catch {
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1271
1291
|
/** 新建表单初始态:分字段模式 + 当前 kind 的官方默认值 */
|
|
1272
1292
|
const freshForm = () => withKindDefaults({ ...EMPTY_FORM, mode: "fields" });
|
|
1273
1293
|
|
|
@@ -1322,12 +1342,20 @@ window.__ModuleLoader__.load({
|
|
|
1322
1342
|
setDirty(new Set());
|
|
1323
1343
|
setDraftTest(null);
|
|
1324
1344
|
}
|
|
1325
|
-
/** 从 URL
|
|
1345
|
+
/** 从 URL 模式切回分字段:表单已有值保留;空字段先从已填 URL 反解真实值,仍空的才补官方默认值——
|
|
1346
|
+
* 直接补默认会把 URL 里的真实主机/端口悄悄换成 127.0.0.1/1433(丢端口 bug)。 */
|
|
1326
1347
|
function reenterFields() {
|
|
1327
1348
|
setForm((prev) => {
|
|
1328
1349
|
const keep = new Set(dirty);
|
|
1329
1350
|
for (const k of ["host", "port", "user", "database"]) if (prev[k]) keep.add(k);
|
|
1330
|
-
|
|
1351
|
+
const next = Object.assign({}, prev, { mode: "fields" });
|
|
1352
|
+
const fromUrl = urlToFieldsPatch(prev.url);
|
|
1353
|
+
if (fromUrl) {
|
|
1354
|
+
for (const k of ["host", "port", "user", "database"]) {
|
|
1355
|
+
if (!prev[k] && fromUrl[k]) { next[k] = fromUrl[k]; keep.add(k); }
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
return withKindDefaults(next, keep);
|
|
1331
1359
|
});
|
|
1332
1360
|
setDraftTest(null);
|
|
1333
1361
|
}
|
|
@@ -1348,7 +1376,8 @@ window.__ModuleLoader__.load({
|
|
|
1348
1376
|
if (up) body.urlPassword = up;
|
|
1349
1377
|
} else {
|
|
1350
1378
|
body.fields = { host: form.host, user: form.user, database: form.database || undefined };
|
|
1351
|
-
|
|
1379
|
+
// 端口原样发字符串:Number("abc")=NaN 会被 JSON 序列化成 null,服务端按「未提供端口」静默回退默认值,配置错误被掩盖
|
|
1380
|
+
if (form.port) body.fields.port = form.port;
|
|
1352
1381
|
if (form.password) body.fields.password = form.password;
|
|
1353
1382
|
// 从 url 方式切到分字段保存 → 服务端清除已存 url(否则连接仍走旧 url)
|
|
1354
1383
|
if (props.initial && props.initial.mode === "url") body.clearUrl = true;
|
|
@@ -1406,7 +1435,8 @@ window.__ModuleLoader__.load({
|
|
|
1406
1435
|
"div",
|
|
1407
1436
|
{ style: { display: "flex", flexDirection: "column", gap: 6 } },
|
|
1408
1437
|
React.createElement("div", { className: "dbt-row" },
|
|
1409
|
-
|
|
1438
|
+
// sqlserver:官方模型 port 可选(默认 1433)且与命名实例互斥——host 直接填 主机\实例名,端口留空
|
|
1439
|
+
React.createElement("input", { placeholder: form.kind === "sqlserver" ? t("fieldsHostInstance") : t("fieldsHost"), "aria-label": t("fieldsHost") + t("requiredMark"), "aria-required": "true", ref: firstInputRef, value: form.host, onChange: (e) => patch({ host: e.target.value }) }),
|
|
1410
1440
|
React.createElement("input", { placeholder: t("fieldsPort"), "aria-label": t("fieldsPort"), value: form.port, onChange: (e) => patch({ port: e.target.value }) }),
|
|
1411
1441
|
React.createElement("input", { placeholder: t("fieldsDatabase"), "aria-label": t("fieldsDatabase"), value: form.database, onChange: (e) => patch({ database: e.target.value }) })),
|
|
1412
1442
|
React.createElement("div", { className: "dbt-row" },
|
|
@@ -3685,6 +3715,7 @@ window.__ModuleLoader__.load({
|
|
|
3685
3715
|
sortRows, slicePage, ResultSetGrid,
|
|
3686
3716
|
classifyHead, ConsoleView,
|
|
3687
3717
|
Panel, TransferView, TransferLogView,
|
|
3718
|
+
urlToFieldsPatch,
|
|
3688
3719
|
};
|
|
3689
3720
|
return module.exports;
|
|
3690
3721
|
},
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import db from 'dmdb';
|
|
22
22
|
import { sqlHead, SQL_READ_HEADS } from '../../guard/index.js';
|
|
23
|
+
import { validPort } from '../sql-shared/common.js';
|
|
23
24
|
const ROWS_MAX = 500;
|
|
24
25
|
const CELL_TRUNC = 1000;
|
|
25
26
|
const DDL_KEYWORDS = new Set([
|
|
@@ -69,6 +70,10 @@ export function resolveDmConn(conn) {
|
|
|
69
70
|
throw new Error('DM URL 应以 dm:// 开头,如 dm://user:pass@host:5236');
|
|
70
71
|
}
|
|
71
72
|
const u = new URL(conn.url);
|
|
73
|
+
// 端口段校验:'dm://host:0' 等非法值抛中文错误,不再透传驱动报晦涩的 EADDRNOTAVAIL
|
|
74
|
+
// (connectString 仍用原 URL 原样透传,不重组)
|
|
75
|
+
if (u.port)
|
|
76
|
+
validPort(u.port, 5236, 'dmdb');
|
|
72
77
|
return {
|
|
73
78
|
connectString: conn.url,
|
|
74
79
|
user: decodeURIComponent(u.username),
|
|
@@ -77,7 +82,9 @@ export function resolveDmConn(conn) {
|
|
|
77
82
|
}
|
|
78
83
|
const f = conn.fields ?? {};
|
|
79
84
|
const host = typeof f.host === 'string' ? f.host : '';
|
|
80
|
-
|
|
85
|
+
// 前端原样发字符串端口:显式校验;历史 typeof==='number' 门把一切字符串(含合法 '14330')
|
|
86
|
+
// 静默丢弃回退 5236,且驱动侧 parseInt 对非法形态还有第二重静默回退
|
|
87
|
+
const port = validPort(f.port, 5236, 'dmdb');
|
|
81
88
|
if (host === '') {
|
|
82
89
|
throw new Error('DM 连接需要 url(dm://user:pass@host:5236)或 fields(host/port)');
|
|
83
90
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* bson 保持 dependencies(bundle 的 external),保证 BSON 类实例与外部一致。
|
|
18
18
|
*/
|
|
19
19
|
import { createRequire } from 'node:module';
|
|
20
|
+
import { validPort } from '../sql-shared/common.js';
|
|
20
21
|
let vendorCache;
|
|
21
22
|
/** 容错加载 vendor bundle:缺失时返回 undefined(调用方降级处理),成功后缓存。
|
|
22
23
|
* 失败不缓存 → vendor 构建完成后无需重启即可恢复。 */
|
|
@@ -574,7 +575,9 @@ export function reviveEjson(v) {
|
|
|
574
575
|
}
|
|
575
576
|
export function buildUri(fields, ssl) {
|
|
576
577
|
const host = typeof fields?.host === 'string' && fields.host !== '' ? fields.host : '127.0.0.1';
|
|
577
|
-
|
|
578
|
+
// 前端原样发字符串端口:显式校验;历史 typeof==='number' 门把一切字符串(含合法 '14330')
|
|
579
|
+
// 静默丢弃回退 27017,分字段模式用户所填端口实际永不生效
|
|
580
|
+
const port = validPort(fields?.port, 27017, 'mongodb');
|
|
578
581
|
const user = typeof fields?.user === 'string' ? encodeURIComponent(fields.user) : '';
|
|
579
582
|
const pass = typeof fields?.password === 'string' ? `:${encodeURIComponent(fields.password)}` : '';
|
|
580
583
|
const auth = user !== '' ? `${user}${pass}@` : '';
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* TRANSACTION READ ONLY)+ query 首词只读白名单双保险;服务层拦截 execute。
|
|
7
7
|
*/
|
|
8
8
|
import mysql from 'mysql2/promise';
|
|
9
|
-
import { assertIdent, clampLimit, clampOffset, humanize, normalizeCell, quoteIdent } from '../sql-shared/common.js';
|
|
9
|
+
import { assertIdent, clampLimit, clampOffset, humanize, normalizeCell, quoteIdent, validPort } from '../sql-shared/common.js';
|
|
10
10
|
/** ro 模式下 query 允许的读语句首词白名单 */
|
|
11
11
|
const RO_READ_PREFIX = /^(select|show|desc|describe|explain|use|help|table)\b/i;
|
|
12
12
|
/**
|
|
@@ -43,7 +43,8 @@ function connOptions(conn) {
|
|
|
43
43
|
if (conn.url) {
|
|
44
44
|
const u = new URL(conn.url);
|
|
45
45
|
base.host = u.hostname;
|
|
46
|
-
|
|
46
|
+
// 端口段校验:':0' 等非法值抛中文错误,不再被 Number()|| 静默改写 3306
|
|
47
|
+
base.port = u.port ? validPort(u.port, 3306, 'mysql') : 3306;
|
|
47
48
|
try {
|
|
48
49
|
if (u.username)
|
|
49
50
|
base.user = decodeURIComponent(u.username);
|
|
@@ -63,7 +64,8 @@ function connOptions(conn) {
|
|
|
63
64
|
else if (conn.fields) {
|
|
64
65
|
const f = conn.fields;
|
|
65
66
|
base.host = String(f.host ?? '127.0.0.1');
|
|
66
|
-
|
|
67
|
+
// 前端原样发字符串端口:显式校验,NaN/0 等不再被驱动 `|| 3306` 静默改写后连错服务
|
|
68
|
+
base.port = validPort(f.port, 3306, 'mysql');
|
|
67
69
|
base.user = String(f.user ?? f.username ?? 'root');
|
|
68
70
|
if (f.password != null)
|
|
69
71
|
base.password = String(f.password);
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import oracledb from 'oracledb';
|
|
20
20
|
import { sqlHead, SQL_READ_HEADS } from '../../guard/index.js';
|
|
21
|
+
import { validPort } from '../sql-shared/common.js';
|
|
21
22
|
// CLOB 统一按字符串返回;BLOB 统一按 Buffer 返回。均为官方全局按类型转换
|
|
22
23
|
// (fetchInfo 的键是列名而非类型名,`{ BLOB: … }` 只会匹配恰好名为 "BLOB" 的列,
|
|
23
24
|
// 按类型统一转换须用全局 fetchAsString/fetchAsBuffer——官方 connection.rst
|
|
@@ -111,12 +112,15 @@ export function resolveOracleConn(conn) {
|
|
|
111
112
|
return {
|
|
112
113
|
user,
|
|
113
114
|
password: decodeURIComponent(u.password),
|
|
114
|
-
|
|
115
|
+
// 端口段校验:':0' 等非法值抛中文错误,不再 host:0 直传驱动
|
|
116
|
+
connectString: `${u.hostname}:${u.port ? validPort(u.port, 1521, 'oracle') : 1521}/${service}`,
|
|
115
117
|
};
|
|
116
118
|
}
|
|
117
119
|
const f = conn.fields ?? {};
|
|
118
120
|
const host = typeof f.host === 'string' ? f.host : '';
|
|
119
|
-
|
|
121
|
+
// 前端原样发字符串端口:显式校验;历史 typeof==='number' 门把一切字符串(含合法 '14330')
|
|
122
|
+
// 静默丢弃回退 1521
|
|
123
|
+
const port = validPort(f.port, 1521, 'oracle');
|
|
120
124
|
const service = typeof f.service === 'string' ? f.service : typeof f.database === 'string' ? f.database : '';
|
|
121
125
|
if (host === '' || service === '') {
|
|
122
126
|
throw new Error('Oracle 连接需要 url(oracle://user:pass@host:1521/SERVICE)或 fields(host/port/service)');
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - 只读账号建议由用户在服务端配置 ACL 只读用户(见 README/文档)。
|
|
11
11
|
*/
|
|
12
12
|
import { createClient } from 'redis';
|
|
13
|
+
import { validPort } from '../sql-shared/common.js';
|
|
13
14
|
/** 危险命令:服务层拦截器在用户确认前拒绝执行(KEYS 同时被 query 白名单禁用)。对齐官方 ACL dangerous 类。 */
|
|
14
15
|
export const DANGEROUS_COMMANDS = new Set([
|
|
15
16
|
'FLUSHALL', 'FLUSHDB', 'SWAPDB', 'CONFIG', 'DEBUG', 'SAVE', 'SHUTDOWN',
|
|
@@ -111,9 +112,11 @@ function parseInfo(raw) {
|
|
|
111
112
|
return rows;
|
|
112
113
|
}
|
|
113
114
|
/** 从 fields 拼接连接 URL(url 缺省时) */
|
|
114
|
-
function buildUrl(fields, ssl) {
|
|
115
|
+
export function buildUrl(fields, ssl) {
|
|
115
116
|
const host = typeof fields?.host === 'string' && fields.host !== '' ? fields.host : '127.0.0.1';
|
|
116
|
-
|
|
117
|
+
// 前端原样发字符串端口:显式校验;历史 typeof==='number' 门把一切字符串(含合法 '14330')
|
|
118
|
+
// 静默丢弃回退 6379,属静默错连
|
|
119
|
+
const port = validPort(fields?.port, 6379, 'redis');
|
|
117
120
|
const user = typeof fields?.user === 'string' && fields.user !== '' ? encodeURIComponent(fields.user) : '';
|
|
118
121
|
const pass = typeof fields?.password === 'string' ? `:${encodeURIComponent(fields.password)}@` : '';
|
|
119
122
|
const db = typeof fields?.database === 'number' && fields.database !== 0 ? `/${fields.database}` : '';
|
|
@@ -27,6 +27,21 @@ export function clampOffset(offset) {
|
|
|
27
27
|
const n = Math.floor(Number(offset ?? 0));
|
|
28
28
|
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* 端口校验(fail-closed,sqlserver validPort 同口径,各适配器 fields/URL 两路径共用):
|
|
32
|
+
* 仅「未提供」(undefined/null/'',表单留空的官方语义)回退该库默认端口;提供了但
|
|
33
|
+
* Number() 后不是 1-65535 的整数则抛中文错误——绝不静默改写用户配置(各驱动普遍对
|
|
34
|
+
* falsy 端口 `|| 默认` 静默回退、对非整数静默截断,连错服务/端口后排障会被误导)。
|
|
35
|
+
*/
|
|
36
|
+
export function validPort(v, dflt, label) {
|
|
37
|
+
if (v === undefined || v === null || v === '')
|
|
38
|
+
return dflt;
|
|
39
|
+
const n = Number(v);
|
|
40
|
+
if (!Number.isInteger(n) || n <= 0 || n > 65535) {
|
|
41
|
+
throw new Error(`${label} 连接的端口取值非法:${JSON.stringify(v)}(须为 1-65535 的整数;留空才使用默认端口 ${dflt})`);
|
|
42
|
+
}
|
|
43
|
+
return n;
|
|
44
|
+
}
|
|
30
45
|
/** 任意单元格 → NormalizedCell(string | number | null) */
|
|
31
46
|
export function normalizeCell(v) {
|
|
32
47
|
if (v === null || v === undefined)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertIdent, clampLimit, clampOffset, humanize, normalizeCell, quoteIdent } from './common.js';
|
|
1
|
+
import { assertIdent, clampLimit, clampOffset, humanize, normalizeCell, quoteIdent, validPort } from './common.js';
|
|
2
2
|
/** 多主机 authority 逐台重组段(host[:port]) */
|
|
3
3
|
function joinHostSeg(h) {
|
|
4
4
|
return `${h.host}${h.port != null ? ':' + h.port : ''}`;
|
|
@@ -20,7 +20,9 @@ function poolConfig(conn, overrideUrl, max = 10) {
|
|
|
20
20
|
// devg-dist/gaussdb-12-0319)、默认管理用户 gaussdb;postgresql 维持社区默认 5432/postgres 不变。
|
|
21
21
|
const gb = conn.meta.kind === 'gaussdb';
|
|
22
22
|
base.host = String(f.host ?? '127.0.0.1');
|
|
23
|
-
|
|
23
|
+
// 前端原样发字符串端口:显式校验,NaN/'' 经 Number() 变 0 后会被驱动 truthy 检查
|
|
24
|
+
// 静默回退 defaults 5432(连 gaussdb 想兜底的 8000 都落不到)、小数被 parseInt 截断
|
|
25
|
+
base.port = validPort(f.port, gb ? 8000 : 5432, conn.meta.kind);
|
|
24
26
|
base.user = String(f.user ?? f.username ?? (gb ? 'gaussdb' : 'postgres'));
|
|
25
27
|
if (f.password != null)
|
|
26
28
|
base.password = String(f.password);
|
|
@@ -42,7 +44,7 @@ function poolConfig(conn, overrideUrl, max = 10) {
|
|
|
42
44
|
* authority(host:port,host:port)端口段含逗号非数字 → new URL 直接抛 Invalid URL。
|
|
43
45
|
* 故自行按文本解析 authority,不经 WHATWG 解析。
|
|
44
46
|
*/
|
|
45
|
-
export function normalizeUrl(url) {
|
|
47
|
+
export function normalizeUrl(url, label = 'postgres') {
|
|
46
48
|
const stripped = url.replace(/^jdbc:/i, '');
|
|
47
49
|
const schemeEnd = stripped.indexOf('://');
|
|
48
50
|
// 无 authority(非 URL 形态):整串原样保留,交由驱动按既有逻辑处理
|
|
@@ -60,8 +62,14 @@ export function normalizeUrl(url) {
|
|
|
60
62
|
const ci = seg.lastIndexOf(':');
|
|
61
63
|
if (ci < 0)
|
|
62
64
|
return { host: seg };
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
+
const host = seg.slice(0, ci);
|
|
66
|
+
const rawPort = seg.slice(ci + 1);
|
|
67
|
+
// 空端口段视同未提供(驱动按默认端口探测);非空则必须为 1-65535 整数——历史 parseInt
|
|
68
|
+
// 会静默截断 '1433.5'→1433、NaN 时把整段当主机名,均属静默改写端口。dflt 传 0 仅占位:
|
|
69
|
+
// rawPort 非空不会走到回退分支。
|
|
70
|
+
if (rawPort === '')
|
|
71
|
+
return { host };
|
|
72
|
+
return { host, port: validPort(rawPort, 0, label) };
|
|
65
73
|
});
|
|
66
74
|
const prefix = `${stripped.slice(0, schemeEnd)}://${userinfo}`;
|
|
67
75
|
// 单台不设 hosts(调用方只在多台时探测)
|
|
@@ -118,7 +126,7 @@ export async function pickHost(driver, base, hosts, masterOnly) {
|
|
|
118
126
|
* 返回最终驱动池配置与最终连接 URL。
|
|
119
127
|
*/
|
|
120
128
|
export async function pgLikePoolCfg(kind, driver, conn, max) {
|
|
121
|
-
const norm = conn.url ? normalizeUrl(conn.url) : null;
|
|
129
|
+
const norm = conn.url ? normalizeUrl(conn.url, kind) : null;
|
|
122
130
|
const cleanUrl = norm ? norm.prefix + (norm.hosts ?? []).map(joinHostSeg).join(',') + norm.suffix : undefined;
|
|
123
131
|
let finalUrl = cleanUrl;
|
|
124
132
|
if (norm?.hosts && norm.hosts.length > 1) {
|
|
@@ -81,9 +81,19 @@ function decodeCred(raw) {
|
|
|
81
81
|
throw new Error('sqlserver 连接 URL 的用户名或密码 percent 编码非法(如含未编码的 %),请 URL 编码后重试');
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* 端口校验(fail-closed,对齐官方语义):仅「未提供」回退默认端口(tedious 对未提供 port
|
|
86
|
+
* 默认 1433);提供了但不是 1-65535 的整数则抛中文错误——与 tedious 对非法 port 抛
|
|
87
|
+
* RangeError 的行为一致,绝不静默改写用户配置(排障会被假端口误导)。
|
|
88
|
+
*/
|
|
84
89
|
function validPort(v, dflt) {
|
|
90
|
+
if (v === undefined || v === null || v === '')
|
|
91
|
+
return dflt;
|
|
85
92
|
const n = Number(v);
|
|
86
|
-
|
|
93
|
+
if (!Number.isInteger(n) || n <= 0 || n > 65535) {
|
|
94
|
+
throw new Error(`sqlserver 连接的端口取值非法:${JSON.stringify(v)}(须为 1-65535 的整数;留空才使用默认端口 ${dflt})`);
|
|
95
|
+
}
|
|
96
|
+
return n;
|
|
87
97
|
}
|
|
88
98
|
/**
|
|
89
99
|
* URL 脱敏(错误消息可能进 audit.jsonl 与模型上下文,绝不能回显明文密码):
|
|
@@ -116,7 +126,7 @@ function poolConfig(conn) {
|
|
|
116
126
|
}
|
|
117
127
|
catch {
|
|
118
128
|
// 脱敏:错误消息会进 audit.jsonl 与模型上下文,绝不回显 userinfo 明文密码
|
|
119
|
-
throw new Error(`sqlserver 连接 URL 无法解析:${redactUrl(conn.url)}(期望形态 sqlserver://user:pass@host:1433/db?encrypt=true;请检查端口是否在 1-65535
|
|
129
|
+
throw new Error(`sqlserver 连接 URL 无法解析:${redactUrl(conn.url)}(期望形态 sqlserver://user:pass@host:1433/db?encrypt=true;请检查端口是否在 1-65535、是否有多余字符;命名实例请改用 ?instanceName=实例名 参数)`);
|
|
120
130
|
}
|
|
121
131
|
if (!u.hostname)
|
|
122
132
|
throw new Error('sqlserver 连接 URL 缺少主机名(期望形态 sqlserver://user:pass@host:1433/db)');
|
|
@@ -140,7 +150,8 @@ function poolConfig(conn) {
|
|
|
140
150
|
options.tdsVersion = tds;
|
|
141
151
|
}
|
|
142
152
|
const instance = queryParam(u.searchParams, 'instanceName');
|
|
143
|
-
//
|
|
153
|
+
// instanceName 与 port 互斥:mssql 层在建连前会 delete 掉 port(tedious 对两者共存直接抛
|
|
154
|
+
// 互斥 Error),端口改由 SQL Server Browser(UDP 1434)按实例名解析
|
|
144
155
|
if (instance !== undefined && instance !== '')
|
|
145
156
|
options.instanceName = instance;
|
|
146
157
|
// fields.database 覆盖 URL 库名:传输端跨库定位(sqlserverLocate)按 pg/mysql 惯例、
|
|
@@ -371,8 +382,15 @@ function mssqlReason(e) {
|
|
|
371
382
|
// 默认 encrypt=true + trustServerCertificate=false 与 mssql 官方一致(不改默认值),但必须:
|
|
372
383
|
// 1) 不把「TCP 已连通、TLS 校验失败」误报成网络不可达(会把用户引向错误排查方向);
|
|
373
384
|
// 2) 把「怎么连上」写清楚(URL 加 ?trustServerCertificate=true / ?encrypt=false)。
|
|
374
|
-
|
|
375
|
-
|
|
385
|
+
// tedious 建连期把底层 socket/TLS 错误用 ES cause 选项挂在 err.cause(originalError 是 mssql 事务
|
|
386
|
+
// abort 专用,建连场景恒 undefined),message 已聚合底层原文但读 cause 更稳,一并纳入特征文本。
|
|
387
|
+
const causeText = errText(err.cause);
|
|
388
|
+
const tlsText = `${detail} ${errText(err.originalError)} ${causeText}`;
|
|
389
|
+
// 特征族来源:OpenSSL/BoringSSL 原生错误串(SSL routines/OPENSSL_internal/UNSUPPORTED_PROTOCOL、
|
|
390
|
+
// handshake_*.c)、Node ERR_SSL_NO_PROTOCOLS_AVAILABLE 的 message、服务器 alert 拒绝握手/版本。
|
|
391
|
+
// 刻意不加 ECONNRESET/socket hang up/ENOTFOUND——与真实网络故障不可区分,误加会让 ECONNREFUSED
|
|
392
|
+
// 场景被套上 TLS 指引。
|
|
393
|
+
const tlsish = /self[ -]?signed|certificate|unable to verify|wrong version number|ERR_SSL|EPROTO|CERT_|UNSUPPORTED_PROTOCOL|SSL[ -_]?routines|OPENSSL_internal|handshake[ _-]?(failure|client|server)|TLSV1_ALERT|SSLV3_ALERT|alert protocol version|alert handshake failure|no protocols available/i.test(tlsText);
|
|
376
394
|
let base;
|
|
377
395
|
switch (code) {
|
|
378
396
|
case 'ELOGIN':
|
|
@@ -388,7 +406,7 @@ function mssqlReason(e) {
|
|
|
388
406
|
// 保留「网络不可达」字样(既有断言与用户检索习惯依赖),但 TLS 特征时先说明 TCP 已连通
|
|
389
407
|
base = tlsish
|
|
390
408
|
? `TCP 已连通但 TLS 握手/证书校验失败(不是网络不可达:网络不可达指主机/端口不可达或连接被拒,此处 TCP 已建立)(${detail})`
|
|
391
|
-
: `网络不可达:无法与服务器建立 TCP
|
|
409
|
+
: `网络不可达:无法与服务器建立 TCP 连接,请检查主机名、端口与网络;端口确认可达仍反复出现时,也可能是服务器在 TLS 握手阶段主动断开(如老实例仅支持 TLS 1.0)(${detail})`;
|
|
392
410
|
break;
|
|
393
411
|
// 服务器强制加密(SQL Server 2022 / Azure SQL 默认)而客户端 encrypt=false:tedious 抛英文原文,
|
|
394
412
|
// 不映射则中文界面里只剩英文,故单列一条给出可直接照做的改法。
|
|
@@ -396,7 +414,7 @@ function mssqlReason(e) {
|
|
|
396
414
|
base = `服务器要求加密连接:当前 encrypt=false 被服务器拒绝,请改用 ?encrypt=true 后重试(${detail})`;
|
|
397
415
|
break;
|
|
398
416
|
case 'EINSTLOOKUP':
|
|
399
|
-
base = `实例名解析失败:SQL Server Browser 服务可能未运行或 UDP 1434
|
|
417
|
+
base = `实例名解析失败:SQL Server Browser 服务可能未运行或 UDP 1434 不可达(命名实例可改用「主机,端口」直连,如 host,1433)(${detail})`;
|
|
400
418
|
break;
|
|
401
419
|
case 'ENOTOPEN':
|
|
402
420
|
case 'ECONNCLOSED':
|
|
@@ -417,6 +435,14 @@ function mssqlReason(e) {
|
|
|
417
435
|
}
|
|
418
436
|
if (err.originalError !== undefined)
|
|
419
437
|
trail.push(`原始错误: ${errText(err.originalError)}`);
|
|
438
|
+
else if (err.cause !== undefined &&
|
|
439
|
+
err.cause !== null &&
|
|
440
|
+
errText(err.cause) !== detail &&
|
|
441
|
+
!detail.includes(errText(err.cause))) {
|
|
442
|
+
// 建连错误(ESOCKET 等)的底层错误在 cause 且已被 tedious 拼进 message 尾段;仅当
|
|
443
|
+
// message 未包含其全文(如被截断/包装)时补一条干净的底层错误,避免重复
|
|
444
|
+
trail.push(`原始错误: ${errText(err.cause)}`);
|
|
445
|
+
}
|
|
420
446
|
if (Array.isArray(err.precedingErrors) && err.precedingErrors.length > 0) {
|
|
421
447
|
trail.push(`前序错误: ${err.precedingErrors.map(errText).join('; ')}`);
|
|
422
448
|
}
|
|
@@ -492,8 +518,10 @@ function rowsOf(res) {
|
|
|
492
518
|
function makePoolBox(Driver, cfg) {
|
|
493
519
|
const pool = new Driver.ConnectionPool(cfg);
|
|
494
520
|
// 无条件兜底:mssql 的 ConnectionPool 在 acquire 失败(空闲连接被服务器断开等)时会
|
|
495
|
-
// emit('error', err),EventEmitter 无监听器时直接抛 →
|
|
496
|
-
pool.on('error', () => {
|
|
521
|
+
// emit('error', err),EventEmitter 无监听器时直接抛 → 崩掉宿主进程。这里只留痕不中断请求。
|
|
522
|
+
pool.on('error', (e) => {
|
|
523
|
+
console.error('[sqlserver] 连接池错误(不影响后续请求,连接将按需重建):', e instanceof Error ? e.message : e);
|
|
524
|
+
});
|
|
497
525
|
let connecting;
|
|
498
526
|
const ready = () => {
|
|
499
527
|
if (connecting === undefined) {
|
|
@@ -264,6 +264,9 @@ export class ConnectionStore {
|
|
|
264
264
|
if (finalUrl !== undefined) {
|
|
265
265
|
rec.urlSafe = redactUrl(finalUrl);
|
|
266
266
|
this.secrets.set(id, { url: finalUrl });
|
|
267
|
+
// 对称清理(clearUrl 的反方向):切到 URL 方式保存即放弃分字段配置;残留 fields 会被
|
|
268
|
+
// sqlserver 等适配器的 fields.database 幽灵覆盖 URL 库名(safeUrl 显示新库、实际连旧库的静默错库)
|
|
269
|
+
delete rec.fields;
|
|
267
270
|
}
|
|
268
271
|
if (patch.clearUrl && rec.urlSafe !== undefined) {
|
|
269
272
|
delete rec.urlSafe;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-db-tool",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.2",
|
|
4
4
|
"description": "DSH community plugin: chat-operated multi-database admin tool with sidebar management, project-scoped grants, ro/rw modes and dangerous-operation confirmation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|