dsh-remote 0.8.71 → 0.8.73
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/LICENSE +21 -21
- package/README.en.md +389 -389
- package/README.md +462 -462
- package/cordis.patch.yml +9 -9
- package/lib/audit.js +45 -0
- package/lib/binding.js +172 -172
- package/lib/client.js +4022 -3990
- package/lib/credential.js +263 -263
- package/lib/errors.js +141 -118
- package/lib/exec-job.js +235 -235
- package/lib/file-reference.js +457 -457
- package/lib/forwards.js +258 -258
- package/lib/hostkey.js +108 -108
- package/lib/http-transport.js +196 -196
- package/lib/ignore.js +202 -202
- package/lib/index.js +4550 -4548
- package/lib/instructions-dynamic.js +415 -415
- package/lib/openssh.js +879 -879
- package/lib/paths.js +252 -252
- package/lib/pool.js +963 -886
- package/lib/registry.js +296 -293
- package/lib/remote-fs.js +173 -173
- package/lib/remote-sandbox.js +183 -183
- package/lib/remote-shell-child.cjs +344 -305
- package/lib/remote-shell.js +542 -518
- package/lib/routes-fs.js +245 -245
- package/lib/routes-term.js +169 -169
- package/lib/search.js +235 -235
- package/lib/sftp-surface.js +119 -119
- package/lib/sshconfig.js +779 -705
- package/lib/sync.js +275 -275
- package/lib/tasks.js +95 -95
- package/lib/telemetry.js +213 -213
- package/lib/terminal.js +205 -205
- package/lib/update.js +472 -472
- package/lib/web-attach.js +850 -850
- package/package.json +100 -99
package/lib/errors.js
CHANGED
|
@@ -1,118 +1,141 @@
|
|
|
1
|
-
// dsh-remote — SSH/SFTP error classification → actionable hints.
|
|
2
|
-
// Pure function so it is unit-testable and reused by tools, slash commands,
|
|
3
|
-
// the test-connect route and the settings UI.
|
|
4
|
-
|
|
5
|
-
/** Classify an error thrown by the ssh pool / sftp wrapper.
|
|
6
|
-
* @returns {{ category: 'auth'|'network'|'hostkey'|'timeout'|'sftp'|'credentials'|'other', hint: string, raw: string }}
|
|
7
|
-
*/
|
|
8
|
-
export function classifyError(err, opts = {}) {
|
|
9
|
-
const msg = String((err && err.message) || err || '')
|
|
10
|
-
const { host, port } = opts
|
|
11
|
-
const where = host ? ` (${host}:${port || 22})` : ''
|
|
12
|
-
|
|
13
|
-
if (/host key/i.test(msg) && /(change|mismatch|fingerprint|verify|unknown host)/i.test(msg)) {
|
|
14
|
-
return {
|
|
15
|
-
category: 'hostkey',
|
|
16
|
-
hint: `主机指纹校验失败${where}:可能遭遇中间人攻击,或该主机重装/更换过密钥。确认无异常后执行 /remote-forget-key 重新信任。`,
|
|
17
|
-
raw: msg,
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
if (/permission denied|all configured authentication methods failed|authentication failed/i.test(msg)) {
|
|
21
|
-
return {
|
|
22
|
-
category: 'auth',
|
|
23
|
-
hint: `认证失败${where}:检查用户名/密码/私钥路径,私钥是否设了 passphrase;若公司机器要求 OTP/动态码,请确认机器配置了 keyboard-interactive 或对应密码。`,
|
|
24
|
-
raw: msg,
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
// Checked BEFORE the generic `no credentials` branch: the actionable failure
|
|
28
|
-
// there is "this process has no ssh-agent", not "you forgot to configure a
|
|
29
|
-
// credential". Telling a Windows user to "set a password or a privateKeyPath"
|
|
30
|
-
// when they already ticked 使用 SSH agent is how a config page gets a
|
|
31
|
-
// reputation for lying.
|
|
32
|
-
if (/no SSH agent available|SSH_AUTH_SOCK/i.test(msg)) {
|
|
33
|
-
return {
|
|
34
|
-
category: 'credentials',
|
|
35
|
-
hint: `SSH agent 不可用${where}:这台机器勾选了「使用 SSH agent」,但 DSH 宿主进程里没有 SSH_AUTH_SOCK(Windows 默认没有 agent 服务)。要么在启动 DSH 之前把 agent 跑起来并导出 SSH_AUTH_SOCK(或用 Pageant),要么取消勾选 agent 并给这台机器保存密码/私钥。`,
|
|
36
|
-
raw: msg,
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
if (/no credentials/i.test(msg)) {
|
|
40
|
-
return {
|
|
41
|
-
category: 'credentials',
|
|
42
|
-
hint: `缺少凭据${where}:这台机器没有可用密码、私钥或 agent。请在「远程工作区」设置里填密码(或用私钥路径 / 勾选 agent)后保存;若你确信已经填过,说明保存时密码没有真正落盘(看保存后是否弹出密钥库失败警告),重新填写并保存即可,可用「测试连接」当场验证。`,
|
|
43
|
-
raw: msg,
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
if (/ENOTFOUND|EAI_AGAIN|getaddrinfo/i.test(msg)) {
|
|
47
|
-
return {
|
|
48
|
-
category: 'network',
|
|
49
|
-
hint: `主机不可达${where}:检查 host 拼写 / IP / DNS;内网机器可能需要走跳板机(配置 proxy)或 VPN。`,
|
|
50
|
-
raw: msg,
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
// ssh2 reports a socket that dies DURING the handshake as the bare string
|
|
54
|
-
// "Connection lost before handshake" — no host, no cause, no advice. That is
|
|
55
|
-
// the message a user sees when the port is open but nothing speaks SSH on it,
|
|
56
|
-
// or when a firewall/security group cuts the connection after the TCP accept;
|
|
57
|
-
// it is indistinguishable from "wrong credentials" unless it is classified.
|
|
58
|
-
if (/connection lost before handshake|handshake failed|no matching (key exchange|host key|cipher)/i.test(msg)) {
|
|
59
|
-
return {
|
|
60
|
-
category: 'network',
|
|
61
|
-
hint: `SSH 握手未完成${where}:TCP 连上了但 SSH 握手没走完——端口上没有跑 sshd、连接被防火墙/安全组拒绝、或远端只支持不兼容的加密算法。确认 host/port 指向 sshd,必要时换端口或让运维放行。`,
|
|
62
|
-
raw: msg,
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
if (/ECONNREFUSED|connection refused/i.test(msg)) {
|
|
66
|
-
return {
|
|
67
|
-
category: 'network',
|
|
68
|
-
hint: `连接被拒绝${where}:确认端口 ${port || 22} 正确、远端 sshd 在监听;检查防火墙/安全组是否放行。`,
|
|
69
|
-
raw: msg,
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
1
|
+
// dsh-remote — SSH/SFTP error classification → actionable hints.
|
|
2
|
+
// Pure function so it is unit-testable and reused by tools, slash commands,
|
|
3
|
+
// the test-connect route and the settings UI.
|
|
4
|
+
|
|
5
|
+
/** Classify an error thrown by the ssh pool / sftp wrapper.
|
|
6
|
+
* @returns {{ category: 'auth'|'network'|'hostkey'|'timeout'|'sftp'|'credentials'|'other', hint: string, raw: string }}
|
|
7
|
+
*/
|
|
8
|
+
export function classifyError(err, opts = {}) {
|
|
9
|
+
const msg = String((err && err.message) || err || '')
|
|
10
|
+
const { host, port } = opts
|
|
11
|
+
const where = host ? ` (${host}:${port || 22})` : ''
|
|
12
|
+
|
|
13
|
+
if (/host key/i.test(msg) && /(change|mismatch|fingerprint|verify|unknown host)/i.test(msg)) {
|
|
14
|
+
return {
|
|
15
|
+
category: 'hostkey',
|
|
16
|
+
hint: `主机指纹校验失败${where}:可能遭遇中间人攻击,或该主机重装/更换过密钥。确认无异常后执行 /remote-forget-key 重新信任。`,
|
|
17
|
+
raw: msg,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (/permission denied|all configured authentication methods failed|authentication failed/i.test(msg)) {
|
|
21
|
+
return {
|
|
22
|
+
category: 'auth',
|
|
23
|
+
hint: `认证失败${where}:检查用户名/密码/私钥路径,私钥是否设了 passphrase;若公司机器要求 OTP/动态码,请确认机器配置了 keyboard-interactive 或对应密码。`,
|
|
24
|
+
raw: msg,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Checked BEFORE the generic `no credentials` branch: the actionable failure
|
|
28
|
+
// there is "this process has no ssh-agent", not "you forgot to configure a
|
|
29
|
+
// credential". Telling a Windows user to "set a password or a privateKeyPath"
|
|
30
|
+
// when they already ticked 使用 SSH agent is how a config page gets a
|
|
31
|
+
// reputation for lying.
|
|
32
|
+
if (/no SSH agent available|SSH_AUTH_SOCK/i.test(msg)) {
|
|
33
|
+
return {
|
|
34
|
+
category: 'credentials',
|
|
35
|
+
hint: `SSH agent 不可用${where}:这台机器勾选了「使用 SSH agent」,但 DSH 宿主进程里没有 SSH_AUTH_SOCK(Windows 默认没有 agent 服务)。要么在启动 DSH 之前把 agent 跑起来并导出 SSH_AUTH_SOCK(或用 Pageant),要么取消勾选 agent 并给这台机器保存密码/私钥。`,
|
|
36
|
+
raw: msg,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (/no credentials/i.test(msg)) {
|
|
40
|
+
return {
|
|
41
|
+
category: 'credentials',
|
|
42
|
+
hint: `缺少凭据${where}:这台机器没有可用密码、私钥或 agent。请在「远程工作区」设置里填密码(或用私钥路径 / 勾选 agent)后保存;若你确信已经填过,说明保存时密码没有真正落盘(看保存后是否弹出密钥库失败警告),重新填写并保存即可,可用「测试连接」当场验证。`,
|
|
43
|
+
raw: msg,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (/ENOTFOUND|EAI_AGAIN|getaddrinfo/i.test(msg)) {
|
|
47
|
+
return {
|
|
48
|
+
category: 'network',
|
|
49
|
+
hint: `主机不可达${where}:检查 host 拼写 / IP / DNS;内网机器可能需要走跳板机(配置 proxy)或 VPN。`,
|
|
50
|
+
raw: msg,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// ssh2 reports a socket that dies DURING the handshake as the bare string
|
|
54
|
+
// "Connection lost before handshake" — no host, no cause, no advice. That is
|
|
55
|
+
// the message a user sees when the port is open but nothing speaks SSH on it,
|
|
56
|
+
// or when a firewall/security group cuts the connection after the TCP accept;
|
|
57
|
+
// it is indistinguishable from "wrong credentials" unless it is classified.
|
|
58
|
+
if (/connection lost before handshake|handshake failed|no matching (key exchange|host key|cipher)/i.test(msg)) {
|
|
59
|
+
return {
|
|
60
|
+
category: 'network',
|
|
61
|
+
hint: `SSH 握手未完成${where}:TCP 连上了但 SSH 握手没走完——端口上没有跑 sshd、连接被防火墙/安全组拒绝、或远端只支持不兼容的加密算法。确认 host/port 指向 sshd,必要时换端口或让运维放行。`,
|
|
62
|
+
raw: msg,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (/ECONNREFUSED|connection refused/i.test(msg)) {
|
|
66
|
+
return {
|
|
67
|
+
category: 'network',
|
|
68
|
+
hint: `连接被拒绝${where}:确认端口 ${port || 22} 正确、远端 sshd 在监听;检查防火墙/安全组是否放行。`,
|
|
69
|
+
raw: msg,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// ECONNRESET is the same class of failure as ECONNREFUSED one step later: the
|
|
73
|
+
// peer (or a firewall/middlebox) tore the TCP connection down. Windows surfaces
|
|
74
|
+
// it where POSIX returns ECONNREFUSED (e.g. loopback under load), so leaving it
|
|
75
|
+
// unclassified made a raw OS errno the user-facing message on exactly one OS.
|
|
76
|
+
if (/ECONNRESET|connection reset/i.test(msg)) {
|
|
77
|
+
return {
|
|
78
|
+
category: 'network',
|
|
79
|
+
hint: `连接被重置${where}:对端或中间设备强行断开了 TCP——常见于防火墙/安全组拦截、端口上跑的不是 sshd、或远端刚重启。重试一次;持续出现则确认端口 ${port || 22} 确实指向 sshd 并检查防火墙。`,
|
|
80
|
+
raw: msg,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (/sftp operation timed out|sftp failed|readdir|writeFile|fastGet|fastPut/i.test(msg)) {
|
|
84
|
+
return {
|
|
85
|
+
category: 'sftp',
|
|
86
|
+
hint: `SFTP 操作失败:可能是远端磁盘/权限问题或服务器卡死(已自动超时保护)。检查目标路径权限。`,
|
|
87
|
+
raw: msg,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (/ETIMEDOUT|timed out|timeout/i.test(msg)) {
|
|
91
|
+
return {
|
|
92
|
+
category: 'timeout',
|
|
93
|
+
hint: `连接/操作超时${where}:检查端口、防火墙、路由;或调大 connectTimeoutMs / commandTimeoutMs。`,
|
|
94
|
+
raw: msg,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (/cannot read private key/i.test(msg)) {
|
|
98
|
+
return {
|
|
99
|
+
category: 'credentials',
|
|
100
|
+
hint: `无法读取私钥文件:检查 privateKeyPath 是否存在、当前用户是否有读取权限。`,
|
|
101
|
+
raw: msg,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// ssh2 throws these SYNCHRONOUSLY from Client.connect (issue #48) — the most
|
|
105
|
+
// common one is an encrypted OpenSSH key without a passphrase.
|
|
106
|
+
if (/no passphrase given|cannot parse privatekey|does not contain a \(valid\) private key/i.test(msg)) {
|
|
107
|
+
return {
|
|
108
|
+
category: 'credentials',
|
|
109
|
+
hint: `私钥解析失败${where}:私钥已加密但未提供 passphrase,或私钥格式不受支持。请为该机器填写 passphrase,或改用未加密/受支持格式(OpenSSH/PEM)的私钥。`,
|
|
110
|
+
raw: msg,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { category: 'other', hint: msg, raw: msg }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** One-line friendly message: hint (+ raw when it adds information).
|
|
117
|
+
*
|
|
118
|
+
* An UNCLASSIFIED failure still gets the host appended: "Connection lost before
|
|
119
|
+
* handshake" or a raw errno on its own leaves the user unable to tell WHICH
|
|
120
|
+
* machine failed, which is the one thing a multi-machine plugin must never hide.
|
|
121
|
+
*
|
|
122
|
+
* IDEMPOTENT on purpose: the pool already classifies connect-stage errors, and
|
|
123
|
+
* callers (rw_connect, routes) classify again at their own layer. A message
|
|
124
|
+
* starting with a known hint prefix passes through unchanged instead of
|
|
125
|
+
* growing a second "(原始错误:…)" shell. */
|
|
126
|
+
const KNOWN_HINT_PREFIXES = [
|
|
127
|
+
'主机指纹校验失败', '认证失败', 'SSH agent 不可用', '缺少凭据', '主机不可达',
|
|
128
|
+
'SSH 握手未完成', '连接被拒绝', '连接被重置', 'SFTP 操作失败', '连接/操作超时',
|
|
129
|
+
'无法读取私钥文件', '私钥解析失败',
|
|
130
|
+
]
|
|
131
|
+
export function friendlyMessage(err, opts = {}) {
|
|
132
|
+
const incoming = String((err && err.message) || err || '')
|
|
133
|
+
if (KNOWN_HINT_PREFIXES.some((p) => incoming.startsWith(p))) return incoming
|
|
134
|
+
const c = classifyError(err, opts)
|
|
135
|
+
const where = opts.host ? ` (${opts.host}:${opts.port || 22})` : ''
|
|
136
|
+
if (c.category === 'other') {
|
|
137
|
+
return where && !c.raw.includes(opts.host) ? `${c.raw}${where}` : c.raw
|
|
138
|
+
}
|
|
139
|
+
if (c.raw && c.raw !== c.hint) return `${c.hint}(原始错误:${c.raw})`
|
|
140
|
+
return c.hint
|
|
141
|
+
}
|