dsh-db-tool 1.9.0 → 1.9.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/CHANGELOG.md +47 -0
- package/README.md +20 -8
- package/README_EN.md +20 -8
- package/client/client.js +71 -13
- package/dist/adapters/mysql/index.js +6 -2
- package/dist/adapters/sqlserver/index.js +241 -13
- package/dist/store/connections.js +10 -1
- package/package.json +5 -1
- package/skills/db-admin/SKILL.md +21 -6
- package/vendor/sqlserver-driver.cjs +1130 -990
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
* 服务层拦 execute + 适配器侧 query 首词白名单(复用 guard 的 SQL_READ_HEADS)+
|
|
25
25
|
* 拒绝多语句(防 `SELECT 1; UPDATE ...` 堆叠写)。这里是应用层防线,不是服务器级保证。
|
|
26
26
|
*/
|
|
27
|
+
import { readFileSync } from 'node:fs';
|
|
27
28
|
import { createRequire } from 'node:module';
|
|
29
|
+
import * as tls from 'node:tls';
|
|
28
30
|
import { SQL_READ_HEADS, sqlHead } from '../../guard/index.js';
|
|
29
31
|
import { assertIdent, clampLimit, clampOffset, humanize, normalizeCell } from '../sql-shared/common.js';
|
|
30
32
|
let vendorCache;
|
|
@@ -104,14 +106,178 @@ function applyTdsVersion(options, raw, source) {
|
|
|
104
106
|
}
|
|
105
107
|
options.tdsVersion = v;
|
|
106
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* 加密模式(URL ?encrypt= 与 fields.encrypt 同口径):true / false / strict 三态。
|
|
111
|
+
*
|
|
112
|
+
* strict 是 tedious 对 TDS 8.0 的支持:TLS 在登录包之前就完成握手(ALPN=tds/8.0),仅 SQL Server
|
|
113
|
+
* 2022+ 开启 Force Strict Encryption 的实例可用;老实例必然拒绝,故非法值必须 fail-closed 报清
|
|
114
|
+
* 三态,不能把拼错的 'stric' 之类静默当成 true(加密强度是安全承诺,连错不报是安全事故)。
|
|
115
|
+
*/
|
|
116
|
+
function applyEncrypt(options, raw, source) {
|
|
117
|
+
if (raw === undefined || raw === null || String(raw).trim() === '')
|
|
118
|
+
return;
|
|
119
|
+
if (typeof raw === 'boolean') {
|
|
120
|
+
options.encrypt = raw;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const s = String(raw).trim().toLowerCase();
|
|
124
|
+
if (s === 'true' || s === '1' || s === 'yes') {
|
|
125
|
+
options.encrypt = true;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (s === 'false' || s === '0' || s === 'no') {
|
|
129
|
+
options.encrypt = false;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (s === 'strict') {
|
|
133
|
+
options.encrypt = 'strict';
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
throw new Error(`sqlserver 连接${source}的 encrypt 取值非法:${JSON.stringify(raw)}(只接受 true/false/strict;strict 表示 TDS 8.0 强制加密,仅 SQL Server 2022 及以上并开启 Force Strict Encryption 的实例支持)`);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 证书校验用的服务器名(URL ?serverName= 与 fields.serverName 同口径)。
|
|
140
|
+
*
|
|
141
|
+
* 两个真实场景需要它:① 用 IP 直连但证书签发给域名;② 走端口转发/NAT 后主机名与证书不符。
|
|
142
|
+
* tedious 走 strict 时 servername 取 options.serverName(wrapWithTls),非 strict 时同样优先
|
|
143
|
+
* 用它(startTls),故一个参数覆盖两条路径。留空不注入,沿用「主机名作校验名」的默认语义。
|
|
144
|
+
*/
|
|
145
|
+
function applyServerName(options, raw) {
|
|
146
|
+
if (raw === undefined || raw === null || String(raw) === '')
|
|
147
|
+
return;
|
|
148
|
+
options.serverName = String(raw);
|
|
149
|
+
}
|
|
150
|
+
/** 取(必要时创建)cryptoCredentialsDetails,供 minVersion 与 ca 合并写入而不互相覆盖 */
|
|
151
|
+
function credentialsOf(options) {
|
|
152
|
+
const cur = options.cryptoCredentialsDetails;
|
|
153
|
+
if (cur !== undefined && cur !== null && typeof cur === 'object')
|
|
154
|
+
return cur;
|
|
155
|
+
const fresh = {};
|
|
156
|
+
options.cryptoCredentialsDetails = fresh;
|
|
157
|
+
return fresh;
|
|
158
|
+
}
|
|
159
|
+
/** PEM 证书起始标记(用于区分「直接传证书文本」与「传文件路径」) */
|
|
160
|
+
const PEM_HEADER = '-----BEGIN';
|
|
161
|
+
/**
|
|
162
|
+
* 系统信任库中的 CA(Node >=22.15 的 tls.getCACertificates('system'))。
|
|
163
|
+
*
|
|
164
|
+
* 失败或不可用时返回空数组并静默回退:拿不到系统根证书不该阻断连接(用户显式给的 caCert 仍然
|
|
165
|
+
* 生效)。之所以要主动合并——Node tls 一旦收到 ca 选项就只用这份列表校验,默认的系统信任库不再
|
|
166
|
+
* 生效;只塞用户那张 CA 会让「装了自签 CA 之后连别的实例」全部失败。
|
|
167
|
+
*/
|
|
168
|
+
function systemCa() {
|
|
169
|
+
try {
|
|
170
|
+
const get = tls.getCACertificates;
|
|
171
|
+
if (typeof get !== 'function')
|
|
172
|
+
return [];
|
|
173
|
+
const list = get('system');
|
|
174
|
+
if (!Array.isArray(list))
|
|
175
|
+
return [];
|
|
176
|
+
return list.filter((x) => typeof x === 'string' && x !== '');
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* 内置 bundle 的 CA(Node >=22.15 的 tls.getCACertificates('default'))。
|
|
184
|
+
* 取不到时返回空数组——调用方据此跳过注入,保持 Node 默认校验行为。
|
|
185
|
+
*/
|
|
186
|
+
function defaultCa() {
|
|
187
|
+
try {
|
|
188
|
+
const get = tls.getCACertificates;
|
|
189
|
+
if (typeof get !== 'function')
|
|
190
|
+
return [];
|
|
191
|
+
const list = get('default');
|
|
192
|
+
if (!Array.isArray(list))
|
|
193
|
+
return [];
|
|
194
|
+
return list.filter((x) => typeof x === 'string' && x !== '');
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* 显式受信 CA(URL ?caCert= 与 fields.caCert 同口径):值可以是 PEM 文本,也可以是 PEM 文件路径。
|
|
202
|
+
*
|
|
203
|
+
* 两条硬约束:
|
|
204
|
+
* - 路径读不出来必须 fail-closed 抛中文错。若静默跳过,用户会以为「已装 CA 但还连不上」,实际
|
|
205
|
+
* 仍按系统信任库校验——问题只会更难定位。
|
|
206
|
+
* - 写入的 ca 必须与系统信任库合并(见 systemCa 的注释),否则等于缩小信任范围。
|
|
207
|
+
*/
|
|
208
|
+
function applyCaCert(options, raw, source) {
|
|
209
|
+
if (raw === undefined || raw === null || String(raw) === '')
|
|
210
|
+
return;
|
|
211
|
+
const text = String(raw);
|
|
212
|
+
// PEM 文本原样保留(换行是 PEM 结构的一部分);只有当路径时才去掉两端空白
|
|
213
|
+
const pem = text.includes(PEM_HEADER) ? text : readCaFile(text.trim(), source);
|
|
214
|
+
const merged = systemCa().slice();
|
|
215
|
+
if (!merged.includes(pem))
|
|
216
|
+
merged.push(pem);
|
|
217
|
+
credentialsOf(options).ca = merged;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* 把系统信任库并入默认校验(未显式给 caCert 时也生效)。
|
|
221
|
+
*
|
|
222
|
+
* 为什么需要:Node 默认的 ca 是编译期内置的 bundle,**不读操作系统信任库**(只有 --use-system-ca
|
|
223
|
+
* 或显式注入才读)。因此「用户按官方推荐把自签证书导入 Windows 受信任根」这一最常见的正确做法,
|
|
224
|
+
* 在不注入的情况下对连接毫无帮助,用户会以为"导入没用"。此处在未给 caCert 时注入 system 列表,
|
|
225
|
+
* 让该做法真正生效。
|
|
226
|
+
*
|
|
227
|
+
* 合并而非替换:注入系统库后 Node 不再使用内置 bundle,只给 system 会让"连公网/别家实例"反而失败,
|
|
228
|
+
* 故 default + system 一起给。
|
|
229
|
+
*
|
|
230
|
+
* 兼容性(老环境不能因此连不上,故逐级降级而不是一刀切):
|
|
231
|
+
* - 没有 tls.getCACertificates(Node < 22.15 / 非官方构建)→ defaultCa() 与 systemCa() 都返回 [],
|
|
232
|
+
* 本函数不注入任何东西,保持 Node 原生默认行为(仍可用内置 bundle 正常连接公网证书)。
|
|
233
|
+
* - 有 API 但系统库为空(部分 Linux 容器、精简镜像)→ 只注入 bundled(等价于默认),
|
|
234
|
+
* 不会把 ca 设成空数组(那会让校验彻底失败)。
|
|
235
|
+
* - 任何异常都被 defaultCa/systemCa 内部吞掉,绝不让"取系统证书失败"打断建连。
|
|
236
|
+
*/
|
|
237
|
+
function applySystemCaDefault(options) {
|
|
238
|
+
if (options.cryptoCredentialsDetails !== undefined) {
|
|
239
|
+
const cur = options.cryptoCredentialsDetails;
|
|
240
|
+
// 已经有人写过 ca(caCert 显式给过)就不再插手,避免覆盖用户的显式选择
|
|
241
|
+
if (cur !== null && typeof cur === 'object' && cur['ca'] !== undefined)
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const bundled = defaultCa();
|
|
245
|
+
const system = systemCa();
|
|
246
|
+
// 两者都拿不到 → 不注入:保持 Node 默认(内置 bundle 依然生效)
|
|
247
|
+
if (bundled.length === 0 && system.length === 0)
|
|
248
|
+
return;
|
|
249
|
+
const merged = bundled.slice();
|
|
250
|
+
for (const pem of system)
|
|
251
|
+
if (!merged.includes(pem))
|
|
252
|
+
merged.push(pem);
|
|
253
|
+
if (merged.length === 0)
|
|
254
|
+
return;
|
|
255
|
+
credentialsOf(options).ca = merged;
|
|
256
|
+
}
|
|
257
|
+
/** 读取 PEM 文件;任何失败都转成带修复指引的中文错误(绝不静默忽略该配置) */
|
|
258
|
+
function readCaFile(path, source) {
|
|
259
|
+
let text;
|
|
260
|
+
try {
|
|
261
|
+
text = readFileSync(path, 'utf8');
|
|
262
|
+
}
|
|
263
|
+
catch (e) {
|
|
264
|
+
throw new Error(`sqlserver 连接${source}的 caCert 无法读取:${path}(${errText(e)})。请给出可读的 PEM 证书文件路径,或直接传 PEM 文本;该配置不会被忽略——忽略等于换回系统信任库校验,真正的原因会被掩盖`);
|
|
265
|
+
}
|
|
266
|
+
if (!text.includes(PEM_HEADER)) {
|
|
267
|
+
throw new Error(`sqlserver 连接${source}的 caCert 指向的文件不含 PEM 证书:${path}(应形如 -----BEGIN CERTIFICATE----- 开头;若本来就想直接传文本,请确认文本未被截断)`);
|
|
268
|
+
}
|
|
269
|
+
return text;
|
|
270
|
+
}
|
|
107
271
|
/**
|
|
108
272
|
* 最低 TLS 版本(URL ?tlsMinVersion= 与 fields.tlsMinVersion 同口径)。
|
|
109
273
|
*
|
|
110
|
-
* 只写 cryptoCredentialsDetails.minVersion
|
|
274
|
+
* 只写 cryptoCredentialsDetails.minVersion,不提供 ciphers / maxVersion 开关:
|
|
111
275
|
* - Electron 内置 Node 是 BoringSSL,OpenSSL 专有的 cipher 串(如 DEFAULT@SECLEVEL=0)会直接抛
|
|
112
276
|
* INVALID_COMMAND;而「连 2008/2008R2 只有 TLS 1.0」这个场景只需把下限从默认的 TLSv1.2 放开即可。
|
|
113
|
-
* - tedious message-io
|
|
114
|
-
*
|
|
277
|
+
* - TLS 上限在非 strict 路径由 tedious 自己封顶:message-io.js 的 startTls 会把
|
|
278
|
+
* cryptoCredentialsDetails.maxVersion 就地改写为 'TLSv1.2'(只有在白名单 TLSv1/TLSv1.1/TLSv1.2
|
|
279
|
+
* 内才原样保留),所以这里写 maxVersion=TLSv1.2 等价于「什么都不做」而不是「加固」。写更低的上限
|
|
280
|
+
* 反而会真的降级到 1.0/1.1,属于削弱安全性——上限留给 tedious 定,本函数只管下限。
|
|
115
281
|
*
|
|
116
282
|
* 取值区分大小写、不做归一化(与 Node tls 官方 API 语义一致:'tlsv1' / 'TLSv1_0' 均会被底层拒绝)。
|
|
117
283
|
* 非法值 fail-closed:静默回退到 Node 默认下限会让用户以为已下探,实际握手仍按 TLS 1.2 起步,
|
|
@@ -125,7 +291,8 @@ function applyTlsMinVersion(options, raw, source) {
|
|
|
125
291
|
if (hit === undefined) {
|
|
126
292
|
throw new Error(`sqlserver 连接${source}的 tlsMinVersion 取值非法:${JSON.stringify(raw)}(可选 ${TLS_MIN_VERSIONS.join('/')},默认不设置=沿用 Node 默认 TLSv1.2;TLSv1.3 不可选:底层 tedious 把 TLS 上限封顶在 1.2,设 1.3 会导致最小版本大于最大版本而握手失败)`);
|
|
127
293
|
}
|
|
128
|
-
|
|
294
|
+
// 与 caCert 合并进同一份 cryptoCredentialsDetails:两者都可能被用户同时设置,直接整体赋值会互相覆盖
|
|
295
|
+
credentialsOf(options).minVersion = hit;
|
|
129
296
|
}
|
|
130
297
|
function decodeCred(raw) {
|
|
131
298
|
try {
|
|
@@ -193,11 +360,14 @@ function poolConfig(conn) {
|
|
|
193
360
|
const path = u.pathname.replace(/^\//, '');
|
|
194
361
|
if (path)
|
|
195
362
|
cfg.database = decodeCred(path);
|
|
196
|
-
|
|
363
|
+
// encrypt 是三态(true/false/strict),故不走 boolParam:strict 会被布尔白名单判为非法
|
|
364
|
+
applyEncrypt(options, queryParam(u.searchParams, 'encrypt'), ' URL ');
|
|
197
365
|
options.trustServerCertificate = boolParam(u.searchParams, 'trustServerCertificate', false);
|
|
198
366
|
options.useUTC = boolParam(u.searchParams, 'useUTC', true);
|
|
199
367
|
applyTdsVersion(options, queryParam(u.searchParams, 'tdsVersion'), ' URL ');
|
|
200
368
|
applyTlsMinVersion(options, queryParam(u.searchParams, 'tlsMinVersion'), ' URL ');
|
|
369
|
+
applyServerName(options, queryParam(u.searchParams, 'serverName'));
|
|
370
|
+
applyCaCert(options, queryParam(u.searchParams, 'caCert'), ' URL ');
|
|
201
371
|
const instance = queryParam(u.searchParams, 'instanceName');
|
|
202
372
|
// instanceName 与 port 互斥:mssql 层在建连前会 delete 掉 port(tedious 对两者共存直接抛
|
|
203
373
|
// 互斥 Error),端口改由 SQL Server Browser(UDP 1434)按实例名解析
|
|
@@ -227,17 +397,24 @@ function poolConfig(conn) {
|
|
|
227
397
|
// TLS 下限都不生效(连接仍按 URL 默认值走),排障时会误以为参数没被实现。此处与 URL 分支
|
|
228
398
|
// 同口径补齐,默认值仍取自 options 初始值(encrypt=true / trustServerCertificate=false /
|
|
229
399
|
// tdsVersion=7_4 / useUTC=true)。
|
|
230
|
-
|
|
400
|
+
// encrypt 与 URL 分支同口径的三态校验(true/false/strict),缺省沿用 options 初始值 encrypt=true
|
|
401
|
+
applyEncrypt(options, f.encrypt, '配置');
|
|
231
402
|
options.trustServerCertificate = boolField(f.trustServerCertificate, 'trustServerCertificate', options.trustServerCertificate);
|
|
232
403
|
applyTdsVersion(options, f.tdsVersion, '配置');
|
|
233
404
|
applyTlsMinVersion(options, f.tlsMinVersion, '配置');
|
|
405
|
+
applyServerName(options, f.serverName);
|
|
406
|
+
applyCaCert(options, f.caCert, '配置');
|
|
234
407
|
}
|
|
235
408
|
else {
|
|
236
409
|
throw new Error('sqlserver 连接缺少 url 或 fields 配置');
|
|
237
410
|
}
|
|
238
|
-
// conn.ssl 是通用字段(mysql/pg
|
|
239
|
-
|
|
411
|
+
// conn.ssl 是通用字段(mysql/pg 适配器同样处理):语义上等价要求加密信道。
|
|
412
|
+
// 已显式选 strict(TDS 8.0)时不降级为 true——strict 是更强的加密承诺,降级会悄悄削弱安全要求。
|
|
413
|
+
if (conn.ssl && options.encrypt !== 'strict')
|
|
240
414
|
options.encrypt = true;
|
|
415
|
+
// 未显式给 caCert 时把系统信任库并入默认校验:Node 默认只信任内置 bundle,不读操作系统信任库,
|
|
416
|
+
// 因此"按官方推荐把自签证书导入 Windows 受信任根"若不注入就对连接毫无帮助(详见 applySystemCaDefault)。
|
|
417
|
+
applySystemCaDefault(options);
|
|
241
418
|
cfg.connectionTimeout = 15000;
|
|
242
419
|
cfg.requestTimeout = 30000;
|
|
243
420
|
cfg.pool = { max: 10, min: 0, idleTimeoutMillis: 30000 };
|
|
@@ -422,6 +599,34 @@ function assertReadOnlyQuery(sql, scan) {
|
|
|
422
599
|
function errText(e) {
|
|
423
600
|
return e instanceof Error ? e.message : String(e);
|
|
424
601
|
}
|
|
602
|
+
/**
|
|
603
|
+
* SQL Server 引擎错误号 → 针对性的中文诊断与出口(仅覆盖「光看英文 message 会走错方向」的那些)。
|
|
604
|
+
*
|
|
605
|
+
* 原则:错误号是服务器给的结构化事实,比 message 文本可靠;但药方必须与成因正交地给对——
|
|
606
|
+
* - 17819 只要 TDS 8.0,与证书是否受信无关,绝不能给 trustServerCertificate;
|
|
607
|
+
* - 17898/17802 是 TDS 协议版本被拒,官方协议规定服务器会自动降级,故正解是升级客户端库,
|
|
608
|
+
* 不是让用户去下调 tdsVersion(那会把问题从「连不上」变成「连上了但行为异常」);
|
|
609
|
+
* - 17835 是服务器强制加密,解法是开 encrypt,同样与证书无关;
|
|
610
|
+
* - 17836/17828/17832 是包结构错配(代理/分片/加密中间层/连错端口),不是版本问题,
|
|
611
|
+
* 给 tdsVersion 建议会把排查引向完全错误的方向;
|
|
612
|
+
* - 47072 是 Azure 的最低 TLS 版本门槛,只会由「版本太低」触发,不会由「证书不受信」触发。
|
|
613
|
+
*/
|
|
614
|
+
const EREQUEST_HINTS = {
|
|
615
|
+
17819: '服务器只接受加密连接(要求 TDS 8.0 严格加密):请在连接 URL 追加 ?encrypt=strict 后重试(strict 走 TDS 8.0,TLS 在登录包之前就完成握手,仅 SQL Server 2022 及以上且开启 Force Strict Encryption 的实例支持)',
|
|
616
|
+
17898: '客户端声明的 TDS 协议版本不被服务器接受:官方协议(MS-TDS LOGIN7)规定服务器应降级到自身最高版本,所以正确做法是升级客户端驱动/库的版本,而不是下调 tdsVersion',
|
|
617
|
+
17802: '服务器拒绝该 TDS 协议版本(中间 TDS 版本不受支持):请升级客户端驱动/库的版本;下调 tdsVersion 只会让协议降级、后续行为更不可预期',
|
|
618
|
+
17835: '服务器强制要求加密连接,而当前配置未启用加密:请在连接 URL 追加 ?encrypt=true 后重试(这是加密开关问题,与证书是否受信无关,不要用关闭证书校验的方式绕过)',
|
|
619
|
+
17836: '登录包长度与网络包声明不一致:通常是代理、端口转发、加密中间层或分片设备改写了 TDS 包,或连到了并非 SQL Server 的端口。请核对端口与链路中间设备',
|
|
620
|
+
17828: '预登录包结构非法(服务器认为客户端发来的预登录包不合法):通常是代理或加密中间层改写了包结构,请核对网络链路中的中间设备与端口',
|
|
621
|
+
17832: '服务器无法解析客户端登录包:可能是包被中间层改写、驱动版本与服务端加解密方式不匹配,或连到了非 SQL Server 端口。请核对链路中间设备',
|
|
622
|
+
47072: 'Azure SQL 要求的最低 TLS 版本未满足:请把客户端 TLS 版本提升到 1.2 及以上(不要下探 tlsMinVersion,也不要关闭证书校验——这个错误只由版本过低触发)',
|
|
623
|
+
};
|
|
624
|
+
/** 已知错误号对应的补充诊断(未收录返回 undefined,保持原有通用文案) */
|
|
625
|
+
function erequestHint(num) {
|
|
626
|
+
if (num === undefined)
|
|
627
|
+
return undefined;
|
|
628
|
+
return Object.prototype.hasOwnProperty.call(EREQUEST_HINTS, num) ? EREQUEST_HINTS[num] : undefined;
|
|
629
|
+
}
|
|
425
630
|
/**
|
|
426
631
|
* mssql 错误码 → 中文可读提示。ETIMEOUT 在 ConnectionError(建连超时)与 RequestError
|
|
427
632
|
* (语句执行超时)下语义不同,故结合 err.name 判断而非只看 code;originalError/precedingErrors
|
|
@@ -458,6 +663,8 @@ function mssqlReason(e) {
|
|
|
458
663
|
const tlsish = tlsProtoish ||
|
|
459
664
|
tlsCertish ||
|
|
460
665
|
/ERR_SSL|EPROTO|SSL[ -_]?routines|OPENSSL_internal|handshake[ _-]?(failure|client|server)|TLSV1_ALERT|SSLV3_ALERT|alert handshake failure/i.test(tlsText);
|
|
666
|
+
// 已知错误号对应的针对性诊断(未收录为 undefined):既用于 EREQUEST 文案,也用于抑制 TLS 族建议
|
|
667
|
+
const knownHint = erequestHint(num);
|
|
461
668
|
let base;
|
|
462
669
|
switch (code) {
|
|
463
670
|
case 'ELOGIN':
|
|
@@ -480,8 +687,14 @@ function mssqlReason(e) {
|
|
|
480
687
|
break;
|
|
481
688
|
// 服务器强制加密(SQL Server 2022 / Azure SQL 默认)而客户端 encrypt=false:tedious 抛英文原文,
|
|
482
689
|
// 不映射则中文界面里只剩英文,故单列一条给出可直接照做的改法。
|
|
690
|
+
// 同一个 EENCRYPT 有两种相反成因,必须分开表述(tedious 20.3.7+ 新增了前者的校验):
|
|
691
|
+
// - encrypt=true 而服务器预登录没回 ON/REQ → 服务器未协商加密,握手本会退化成明文,驱动现在直接拒绝;
|
|
692
|
+
// - encrypt=false 而服务器要求加密 → 服务器侧强制。
|
|
693
|
+
// 两者的药方正好相反(一个要关、一个要开),合并成一句必然把用户引反方向。
|
|
483
694
|
case 'EENCRYPT':
|
|
484
|
-
base =
|
|
695
|
+
base = /did not negotiate encryption/i.test(detail)
|
|
696
|
+
? `服务器未协商加密:客户端要求加密(encrypt=true)但服务器在预登录响应里没有回 ON/REQ,驱动为避免明文降级直接拒绝连接。若该实例确实不支持加密,可改用 ?encrypt=false 明文连接(内网可接受时);若要求加密,请在服务器侧启用「强制加密」并配置有效证书(${detail})`
|
|
697
|
+
: `服务器要求加密连接:当前 encrypt=false 被服务器拒绝,请改用 ?encrypt=true 后重试(${detail})`;
|
|
485
698
|
break;
|
|
486
699
|
case 'EINSTLOOKUP':
|
|
487
700
|
base = `实例名解析失败:SQL Server Browser 服务可能未运行或 UDP 1434 不可达(命名实例可改用「主机,端口」直连,如 host,1433)(${detail})`;
|
|
@@ -493,14 +706,21 @@ function mssqlReason(e) {
|
|
|
493
706
|
case 'ECANCEL':
|
|
494
707
|
base = `请求已取消(${detail})`;
|
|
495
708
|
break;
|
|
496
|
-
case 'EREQUEST':
|
|
497
|
-
base = `SQL 执行错误${num !== undefined ? `(错误号 ${num}${sev !== undefined ? `、严重级 ${sev}` : ''}${line !== undefined ? `、第 ${line} 行` : ''})` : ''}:${detail}`;
|
|
709
|
+
case 'EREQUEST': {
|
|
710
|
+
base = `SQL 执行错误${num !== undefined ? `(错误号 ${num}${sev !== undefined ? `、严重级 ${sev}` : ''}${line !== undefined ? `、第 ${line} 行` : ''})` : ''}:${knownHint === undefined ? detail : `${knownHint}。服务器原文:${detail}`}`;
|
|
498
711
|
break;
|
|
712
|
+
}
|
|
499
713
|
default:
|
|
500
714
|
base = detail;
|
|
501
715
|
}
|
|
502
716
|
const trail = [];
|
|
503
|
-
|
|
717
|
+
// 已知错误号已经给出了针对性诊断时,抑制 TLS 族建议:这类错误的服务器原文常含 certificate /
|
|
718
|
+
// encryption 字样(如 17819 的「TLS certificate is not configured to accept strict」),会被下面的
|
|
719
|
+
// 正则命中;但真因是服务端配置/协议问题,客户端装 CA、关校验一律无效,叠加只会给出互相矛盾的药方。
|
|
720
|
+
if (knownHint !== undefined) {
|
|
721
|
+
// 已由 EREQUEST_HINTS 给出单一出口,此处不再追加任何族建议
|
|
722
|
+
}
|
|
723
|
+
else if (tlsProtoish) {
|
|
504
724
|
// 顺序即优先级:服务端升级是正解(一次性、对所有客户端生效),客户端下探是权宜(仍受限于
|
|
505
725
|
// 服务端实际支持的版本)。第③条显式点名「跳过证书校验」是先发制人——用户看到 TLS 报错的第一
|
|
506
726
|
// 反应通常就是去信任证书,明说无效比沉默更省一轮排查;措辞必须是否定式,不得写成可尝试项。
|
|
@@ -509,7 +729,14 @@ function mssqlReason(e) {
|
|
|
509
729
|
trail.push('③ 注意:跳过证书校验(信任任意证书)对这类错误无效——它只影响证书校验阶段,而此处是协议版本协商失败,证书校验根本没执行');
|
|
510
730
|
}
|
|
511
731
|
else if (tlsCertish) {
|
|
512
|
-
|
|
732
|
+
// 证书族(自签/链断/主机名不符/已过期):解法有强弱两档,必须把官方推荐摆在前面。
|
|
733
|
+
// ① 装 CA(?caCert= 给 PEM 或文件路径)——校验照旧生效,只把签发者加进信任列表,这是官方推荐做法;
|
|
734
|
+
// ② strict(TDS 8.0)下 trustServerCertificate 完全失效:tedious 的 wrapWithTls 调 tls.connect
|
|
735
|
+
// 时没有传 rejectUnauthorized,证书是强制校验的。必须说清,否则用户在 strict 下改这个开关
|
|
736
|
+
// 会「改完还是同样的错」,白排查一轮。
|
|
737
|
+
trail.push('官方推荐先在客户端装 CA 而不是关校验:把签发服务器证书的根 CA(自签场景就是该证书本身)经 ?caCert= 指向 PEM 路径或直接给 PEM 文本,让证书校验照常生效;若证书签发给域名而你是用 IP/转发地址连接的,再用 ?serverName=域名 指定用于校验的名字');
|
|
738
|
+
trail.push('② 不得已才关校验:?trustServerCertificate=true(跳过证书链与主机名校验,仅加密信道不保证对端身份),或 ?encrypt=false(前提是服务器未强制加密)——SQL Server 2008/2008 R2 与使用自签证书的实例常见此情形,但这两条都会削弱安全性,仅建议内网临时使用');
|
|
739
|
+
trail.push('③ 注意:若当前用的是 ?encrypt=strict(TDS 8.0),trustServerCertificate 完全不生效(驱动 strict 路径不传 rejectUnauthorized,证书强制校验),只能装 CA 或改用 ?encrypt=true');
|
|
513
740
|
}
|
|
514
741
|
if (err.originalError !== undefined)
|
|
515
742
|
trail.push(`原始错误: ${errText(err.originalError)}`);
|
|
@@ -667,6 +894,7 @@ const PRODUCT_NAMES = {
|
|
|
667
894
|
14: '2017',
|
|
668
895
|
15: '2019',
|
|
669
896
|
16: '2022',
|
|
897
|
+
17: '2025',
|
|
670
898
|
};
|
|
671
899
|
/** 版本串展示:'Microsoft SQL Server 2019 - 15.0.2000.5'(Azure SQL 按 EngineEdition=5 单独标注) */
|
|
672
900
|
function serverInfo(version, major, engine) {
|
|
@@ -163,7 +163,7 @@ function toMeta(rec, hasPassword = false) {
|
|
|
163
163
|
if (hasPassword)
|
|
164
164
|
meta.hasPassword = true;
|
|
165
165
|
if (rec.fields) {
|
|
166
|
-
const { host, port, user, database, tlsMinVersion } = rec.fields;
|
|
166
|
+
const { host, port, user, database, tlsMinVersion, encrypt, trustServerCertificate, serverName, caCert } = rec.fields;
|
|
167
167
|
if (typeof host === 'string')
|
|
168
168
|
meta.host = host;
|
|
169
169
|
if (typeof port === 'number')
|
|
@@ -174,6 +174,15 @@ function toMeta(rec, hasPassword = false) {
|
|
|
174
174
|
meta.database = database;
|
|
175
175
|
if (typeof tlsMinVersion === 'string')
|
|
176
176
|
meta.tlsMinVersion = tlsMinVersion;
|
|
177
|
+
// encrypt 三态:布尔按 boolean 透传,'strict' 按字面量透传(两者语义不同,不能互相折叠)
|
|
178
|
+
if (typeof encrypt === 'boolean' || encrypt === 'strict')
|
|
179
|
+
meta.encrypt = encrypt;
|
|
180
|
+
if (typeof trustServerCertificate === 'boolean')
|
|
181
|
+
meta.trustServerCertificate = trustServerCertificate;
|
|
182
|
+
if (typeof serverName === 'string')
|
|
183
|
+
meta.serverName = serverName;
|
|
184
|
+
if (typeof caCert === 'string')
|
|
185
|
+
meta.caCert = caCert;
|
|
177
186
|
}
|
|
178
187
|
return meta;
|
|
179
188
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-db-tool",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.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": "Apache-2.0",
|
|
@@ -59,6 +59,9 @@
|
|
|
59
59
|
"optionalDependencies": {
|
|
60
60
|
"better-sqlite3": "^13.0.3"
|
|
61
61
|
},
|
|
62
|
+
"overrides": {
|
|
63
|
+
"tedious": "^20.3.8"
|
|
64
|
+
},
|
|
62
65
|
"devDependencies": {
|
|
63
66
|
"@codemirror/autocomplete": "^6.20.3",
|
|
64
67
|
"@codemirror/commands": "^6.11.1",
|
|
@@ -76,6 +79,7 @@
|
|
|
76
79
|
"mongodb": "^7.6.0",
|
|
77
80
|
"mssql": "12.7.4",
|
|
78
81
|
"semver": "^7.7.0",
|
|
82
|
+
"tedious": "^20.3.8",
|
|
79
83
|
"sql-formatter": "^15.9.0",
|
|
80
84
|
"stryker-cli": "^1.1.0",
|
|
81
85
|
"typescript": "^5.9.3",
|
package/skills/db-admin/SKILL.md
CHANGED
|
@@ -180,20 +180,35 @@ DatabaseManager({ action: "run_script", conn_id: "mongo1", code: '{"find":"users
|
|
|
180
180
|
|
|
181
181
|
## SQL Server 覆盖范围与边界
|
|
182
182
|
|
|
183
|
-
- 覆盖 SQL Server
|
|
184
|
-
-
|
|
185
|
-
-
|
|
186
|
-
-
|
|
183
|
+
- 覆盖 SQL Server 2005(9.0)→ 2025(17.0)与 Azure SQL Database;不支持 SQL Server 2000 及更早。方言与元数据只使用 2005+ 通用写法(元数据走 `sys.*` catalog views)。
|
|
184
|
+
- **TDS 版本自动协商,无需用户选择**:客户端固定声明 `7_4`,服务器按自身能力自动降级(2005→`7_2`、2008→`7_3_A`、2008 R2→`7_3_B`、2012+→`7_4`)。这是 [MS-TDS](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/773a62b6-ee89-4c02-9e5e-344882630aac) 的 MUST 级规定(客户端声明高于服务器时,服务器**必须**用自己最高版本)。`tdsVersion` 仅作排障覆盖项;**不要**因「老版本」就下调它——那会把「连不上」变成「连上了但行为异常」。
|
|
185
|
+
- **认证**:仅 SQL 认证(user/password);**不支持 Windows 集成认证,也不支持 Azure AD 认证**——需要这两类认证时本工具不可用。SQL Server 全版本另支持 FEDAUTH 之外的 `IsIntegratedSecurityOnly` 判定:为 1 表示实例仅允许 Windows 认证,此时应直接告知用户该实例不可用,而不是让用户反复试密码。
|
|
186
|
+
- **连接形态**:`sqlserver://user:password@host:port/db`,支持 query 参数 `encrypt` / `trustServerCertificate` / `tdsVersion` / `tlsMinVersion` / `serverName` / `caCert` / `instanceName` / `useUTC`(参数名大小写不敏感);`instanceName` 与端口互斥(命名实例改由 SQL Server Browser 解析)。**不接受** ADO.NET 键值连接串(`Server=...;Database=...`)。
|
|
187
|
+
- **默认与覆盖**:`encrypt=true` + `trustServerCertificate=false` + `tdsVersion=7_4` + `useUTC=true`(同 mssql 官方默认,也是微软官方推荐的安全档)。URL query 与分字段表单**同口径**,两处都能覆盖上述各项;非法取值直接报错,不静默回退。
|
|
187
188
|
|
|
188
189
|
### TLS 握手失败:两类根因,解法互不通用
|
|
189
190
|
|
|
190
191
|
| 报错特征 | 根因 | 处置(按优先级) |
|
|
191
192
|
|---|---|---|
|
|
192
|
-
| `self signed certificate` / `unable to verify` / `CERT_` 家族 | 证书校验阶段失败(自签、链断、主机名不符、已过期) | ① `?trustServerCertificate=true
|
|
193
|
-
| `UNSUPPORTED_PROTOCOL` / `no protocols available` / `alert protocol version`(含 `SSL routines ... UNSUPPORTED`) | 协议版本协商阶段失败:客户端与服务端无共同 TLS 版本,**证书校验根本没执行** | ① 服务端启用 TLS 1.2(2008 R2 SP3 装 KB4057113、2008 SP4 装 KB4057114,重启实例);② 暂时不能动服务端时,客户端下探 `?tlsMinVersion=TLSv1` |
|
|
193
|
+
| `self signed certificate` / `unable to verify` / `CERT_` 家族 | 证书校验阶段失败(自签、链断、主机名不符、已过期) | ① `?caCert=` 装受信 CA(PEM 路径或文本,校验照常生效,**官方推荐**);证书签给域名而在用 IP 连接时再加 `?serverName=域名`;② 不得已才 `?trustServerCertificate=true` 或 `?encrypt=false`(后者需服务器未强制加密) |
|
|
194
|
+
| `UNSUPPORTED_PROTOCOL` / `no protocols available` / `alert protocol version`(含 `SSL routines ... UNSUPPORTED`) | 协议版本协商阶段失败:客户端与服务端无共同 TLS 版本,**证书校验根本没执行** | ① 服务端启用 TLS 1.2(2008 R2 SP3 装 KB4057113、2008 SP4 装 KB4057114、2012/2014 装对应 CU/GDR 与 KB3052404,重启实例);② 暂时不能动服务端时,客户端下探 `?tlsMinVersion=TLSv1` |
|
|
194
195
|
|
|
195
196
|
**不要拿证书类参数治协议版本类**:`trustServerCertificate=true` / `encrypt=false` 只影响证书校验,对 `UNSUPPORTED_PROTOCOL` 没有任何作用,试错只会白跑一轮。两类判据互斥,报错消息里已按族给出中文根因;看到 `TCP 已连通但 TLS ...` 说明网络与端口都没问题,不要再查主机/防火墙。
|
|
196
197
|
|
|
198
|
+
**关键事实:`encrypt=false` 不能跳过 TLS 握手**。SQL Server 的登录包(Login7,含账号密码)**即使在未配置加密时也始终加密**(官方原文:"even when you don't configure encryption, the credentials (in the login packet) ... are always encrypted")。所以协议版本无交集时 `encrypt=false` 一样连不上——这是「改了加密参数却仍报同一个错」的根因。另注:连 2005 或未打补丁的 2008 时,服务端可能只有 TLS 1.0,此时必须下探 `?tlsMinVersion=TLSv1`。
|
|
199
|
+
|
|
200
|
+
### 版本与 TLS 1.2 支持
|
|
201
|
+
|
|
202
|
+
| 版本 | TLS 1.2 | 所需补丁 |
|
|
203
|
+
|---|---|---|
|
|
204
|
+
| 2005(9.0) | **不支持**(无任何补丁) | 无解,只能下探客户端 TLS 下限 + 信任证书 |
|
|
205
|
+
| 2008 SP4 / 2008 R2 SP3 | 需补丁 | KB4057114 / KB4057113 |
|
|
206
|
+
| 2012 SP4- / 2014 SP3- | 需补丁 | 对应 CU/GDR + KB3052404 |
|
|
207
|
+
| 2012 SP4 / 2014 SP2 / 2016+ | 原生支持 | — |
|
|
208
|
+
| 2022 / 2025 | 原生;TDS 8.0 下可 TLS 1.3 | — |
|
|
209
|
+
|
|
210
|
+
2016+ 实际使用哪个 TLS 版本由**操作系统的 Schannel** 决定,不由 SQL Server 决定——同一版本实例在不同机器上表现可能不同。
|
|
211
|
+
|
|
197
212
|
### `tlsMinVersion` 参数
|
|
198
213
|
|
|
199
214
|
| 取值 | 含义 |
|