@hyzyn/dsh-tty 0.21.0 → 0.22.0-rc.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/README.en.md +113 -10
- package/README.md +117 -10
- package/client.js +150 -36
- package/lib/index.d.ts +195 -25
- package/lib/index.js +930 -149
- package/lib/index.js.map +1 -1
- package/lib/probe.d.ts +29 -1
- package/lib/probe.js +98 -19
- package/lib/probe.js.map +1 -1
- package/lib/sftp.js +37 -9
- package/lib/sftp.js.map +1 -1
- package/lib/shell-integration.js +25 -1
- package/lib/shell-integration.js.map +1 -1
- package/lib/ssh-config.d.ts +34 -2
- package/lib/ssh-config.js +150 -32
- package/lib/ssh-config.js.map +1 -1
- package/lib/ssh.d.ts +256 -2
- package/lib/ssh.js +637 -6
- package/lib/ssh.js.map +1 -1
- package/lib/tunnels.js +47 -5
- package/lib/tunnels.js.map +1 -1
- package/package.json +4 -2
- package/scripts/integration.mjs +183 -20
- package/scripts/jump-smoke.mjs +175 -0
- package/scripts/lib/proxy-bridge.mjs +30 -0
- package/scripts/preview/harness.js +74 -26
- package/scripts/probe-route-smoke.mjs +10 -1
- package/scripts/proxycommand-smoke.mjs +267 -0
- package/scripts/windows-smoke.mjs +123 -1
package/lib/ssh-config.d.ts
CHANGED
|
@@ -6,10 +6,42 @@
|
|
|
6
6
|
* - `Host` 多模式时只收「全具体」块(任一模式含 * ? ! 或首字符为空格否定
|
|
7
7
|
* 即整块跳过),块名取第一个模式;
|
|
8
8
|
* - 只映射 HostName / User / Port / IdentityFile;Include 不展开(跳过),
|
|
9
|
-
* 其余选项(
|
|
9
|
+
* 其余选项(ServerAliveInterval 等)原样忽略;
|
|
10
|
+
* - **`ProxyJump` 解析成结构化的 `jump` 一起导入**(单跳):值是 `[user@]host[:port]`,
|
|
11
|
+
* 也可以引用**同一份 config 里的另一个具体 Host**(按块名解析,块序任意);
|
|
12
|
+
* 解析不出来(别名缺失 / 别名自己还依赖跳板机)就整块跳过并把块名报回去;
|
|
13
|
+
* - **`ProxyCommand` 仍整块跳过并报数**:它是「设置字段驱动的本地任意命令执行」,
|
|
14
|
+
* 信任级与回环围栏之后的其它字段不同,要单独定闸门(见 ROADMAP 第 2 项);
|
|
15
|
+
* - `ProxyJump none` / `ProxyCommand none` 是显式直连,照常导入(OpenSSH 用它抵消
|
|
16
|
+
* 上层 `Host *` 的设置);
|
|
10
17
|
* - 没有 User 的块无法构成连接簿条目(username 必填),跳过;
|
|
11
18
|
* - IdentityFile 取第一个 → auth=key + keyPath,否则 auth=agent;
|
|
12
|
-
* - 单文件最多产出 100
|
|
19
|
+
* - 单文件最多产出 100 条,**超出部分报数**(`droppedOverflow`):静默少列是本仓
|
|
20
|
+
* 反复出现的一类缺陷(**D63**;见 docs/architecture.md § 7 的「截断要有信号」)。
|
|
13
21
|
*/
|
|
14
22
|
import type { SshHostEntry } from './ssh.js';
|
|
23
|
+
/** 单文件最多产出多少条连接簿候选(防异常巨型文件)。 */
|
|
24
|
+
export declare const MAX_IMPORT_ENTRIES = 100;
|
|
25
|
+
/** `proxy` 名单最多回报多少个块名(超出的只计数):导入提示不该被一份巨型配置撑爆。 */
|
|
26
|
+
export declare const MAX_PROXY_NAMES = 50;
|
|
27
|
+
/** `parseSshConfigDetailed` 的结果:候选 + **每一种丢弃都要有信号**。 */
|
|
28
|
+
export interface ParseSshConfigResult {
|
|
29
|
+
entries: SshHostEntry[];
|
|
30
|
+
/** 用了 `ProxyJump` 但**解析不出跳板机**(别名缺失 / 别名自己也依赖跳板机)的块名。 */
|
|
31
|
+
proxy: string[];
|
|
32
|
+
/** 上一类块的总数(即使名单被截断,这个数也是准的)。 */
|
|
33
|
+
proxyCount: number;
|
|
34
|
+
/** 用了 `ProxyCommand` 而跳过的块名(本版本不支持,信任级需单独定闸门)。 */
|
|
35
|
+
proxyCommand: string[];
|
|
36
|
+
/** 上一类块的总数。 */
|
|
37
|
+
proxyCommandCount: number;
|
|
38
|
+
/** **成功带上跳板机**的条目数(对照组:让用户看得出导入到底生效了没有)。 */
|
|
39
|
+
jumpImported: number;
|
|
40
|
+
/** 其余跳过(通配 / 否定 Host 模式、没有 User)的块数。 */
|
|
41
|
+
skippedOther: number;
|
|
42
|
+
/** 超过 MAX_IMPORT_ENTRIES 被丢弃的块数。 */
|
|
43
|
+
droppedOverflow: number;
|
|
44
|
+
}
|
|
45
|
+
export declare function parseSshConfigDetailed(text: string): ParseSshConfigResult;
|
|
46
|
+
/** 只要导入候选(老调用点与既有用例的形状);需要「跳过了什么」时用 detailed 版。 */
|
|
15
47
|
export declare function parseSshConfig(text: string): SshHostEntry[];
|
package/lib/ssh-config.js
CHANGED
|
@@ -1,34 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
/** 单文件最多产出多少条连接簿候选(防异常巨型文件)。 */
|
|
2
|
+
export const MAX_IMPORT_ENTRIES = 100;
|
|
3
|
+
/** `proxy` 名单最多回报多少个块名(超出的只计数):导入提示不该被一份巨型配置撑爆。 */
|
|
4
|
+
export const MAX_PROXY_NAMES = 50;
|
|
5
|
+
/**
|
|
6
|
+
* 这个块是不是显式要求直连(`ProxyJump none` / `ProxyCommand none`)。
|
|
7
|
+
* OpenSSH 用 `none` 抵消上层(`Host *`)的跳板机设置——这类块照常可导入。
|
|
8
|
+
*/
|
|
9
|
+
function proxyOptOut(value) {
|
|
10
|
+
return typeof value === 'string' && value.trim().toLowerCase() === 'none';
|
|
11
|
+
}
|
|
12
|
+
export function parseSshConfigDetailed(text) {
|
|
2
13
|
const entries = [];
|
|
3
|
-
|
|
14
|
+
const proxy = [];
|
|
15
|
+
const proxyCommand = [];
|
|
16
|
+
let proxyCount = 0;
|
|
17
|
+
let proxyCommandCount = 0;
|
|
18
|
+
let jumpImported = 0;
|
|
19
|
+
let skippedOther = 0;
|
|
20
|
+
let droppedOverflow = 0;
|
|
21
|
+
/*
|
|
22
|
+
* **两遍**:第一遍只把 config 切成块(逐行状态机原样保留),第二遍才产出条目。
|
|
23
|
+
* 为什么要两遍:`ProxyJump bastion` 里的 `bastion` 常常是**同一份 config 里的另一个
|
|
24
|
+
* Host**,而那个块可能写在**后面**——边切边产出就没法解析别名。
|
|
25
|
+
*/
|
|
26
|
+
const blocks = [];
|
|
4
27
|
let block = null;
|
|
5
|
-
const flush = () => {
|
|
6
|
-
if (block === null)
|
|
7
|
-
return;
|
|
8
|
-
const concrete = block.patterns.length > 0 && block.patterns.every((p) => p !== '' && !/[*?!]/.test(p));
|
|
9
|
-
if (concrete) {
|
|
10
|
-
const name = block.patterns[0];
|
|
11
|
-
const host = block.options.get('hostname') ?? name;
|
|
12
|
-
const user = block.options.get('user');
|
|
13
|
-
const portRaw = Number(block.options.get('port'));
|
|
14
|
-
const port = Number.isInteger(portRaw) && portRaw >= 1 && portRaw <= 65535 ? portRaw : 22;
|
|
15
|
-
const identityFile = block.options.get('identityfile');
|
|
16
|
-
if (typeof user === 'string' && user.trim() !== '' && entries.length < 100) {
|
|
17
|
-
entries.push({
|
|
18
|
-
name,
|
|
19
|
-
host,
|
|
20
|
-
port,
|
|
21
|
-
username: user.trim(),
|
|
22
|
-
auth: identityFile !== undefined && identityFile.trim() !== '' ? 'key' : 'agent',
|
|
23
|
-
keyPath: identityFile !== undefined ? identityFile.trim() : '',
|
|
24
|
-
passphrase: '',
|
|
25
|
-
password: '',
|
|
26
|
-
agentForward: false,
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
block = null;
|
|
31
|
-
};
|
|
32
28
|
for (const rawLine of text.split(/\r?\n/)) {
|
|
33
29
|
let line = rawLine.trim();
|
|
34
30
|
if (line === '' || line.startsWith('#'))
|
|
@@ -55,7 +51,8 @@ export function parseSshConfig(text) {
|
|
|
55
51
|
}
|
|
56
52
|
const keyLower = key.toLowerCase();
|
|
57
53
|
if (keyLower === 'host') {
|
|
58
|
-
|
|
54
|
+
if (block !== null)
|
|
55
|
+
blocks.push(block);
|
|
59
56
|
// 「Host = name」的孤立等号当作分隔符宽容丢弃
|
|
60
57
|
block = { patterns: rest.split(/\s+/).filter((p) => p !== '' && p !== '='), options: new Map() };
|
|
61
58
|
continue;
|
|
@@ -68,7 +65,128 @@ export function parseSshConfig(text) {
|
|
|
68
65
|
continue; // 首个生效(OpenSSH 语义)
|
|
69
66
|
block.options.set(keyLower, rest.replace(/^"+|"+$/g, ''));
|
|
70
67
|
}
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
if (block !== null)
|
|
69
|
+
blocks.push(block);
|
|
70
|
+
/** 一个块是不是「全具体」的 Host 模式(通配 / 否定整块不要)。 */
|
|
71
|
+
const isConcrete = (item) => item.patterns.length > 0 && item.patterns.every((pattern) => pattern !== '' && !/[*?!]/.test(pattern));
|
|
72
|
+
/** 具体块名 → 选项表(同名取第一个,与 OpenSSH「首个生效」一致):别名解析用。 */
|
|
73
|
+
const concrete = new Map();
|
|
74
|
+
for (const item of blocks) {
|
|
75
|
+
if (!isConcrete(item))
|
|
76
|
+
continue;
|
|
77
|
+
const name = item.patterns[0];
|
|
78
|
+
if (!concrete.has(name))
|
|
79
|
+
concrete.set(name, item.options);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 解析 `ProxyJump` 的值 → `SshJumpSpec`;解析不出返回 undefined(调用方会把块名报回去)。
|
|
83
|
+
*
|
|
84
|
+
* 支持 `[user@]host[:port]`(IPv6 写 `[::1]:22`)与**同文件别名**;别名的 IdentityFile
|
|
85
|
+
* **不进 jump**——v1 的跳板机凭据缺省继承目标那一跳(企业内网里最常见的就是共用一把钥匙
|
|
86
|
+
* 或同一个 agent),显式要不同的凭据时在连接条目里手填。
|
|
87
|
+
*/
|
|
88
|
+
const resolveJump = (value) => {
|
|
89
|
+
let rest = value.trim();
|
|
90
|
+
if (rest === '')
|
|
91
|
+
return undefined;
|
|
92
|
+
let username = '';
|
|
93
|
+
const at = rest.lastIndexOf('@');
|
|
94
|
+
if (at > 0) {
|
|
95
|
+
username = rest.slice(0, at).trim();
|
|
96
|
+
rest = rest.slice(at + 1).trim();
|
|
97
|
+
}
|
|
98
|
+
let host = rest;
|
|
99
|
+
let port = 22;
|
|
100
|
+
const match = /^\[([^\]]+)\](?::(\d+))?$/.exec(rest) ?? /^([^:]+):(\d+)$/.exec(rest);
|
|
101
|
+
if (match !== null) {
|
|
102
|
+
host = match[1];
|
|
103
|
+
const parsedPort = Number(match[2]);
|
|
104
|
+
if (Number.isInteger(parsedPort) && parsedPort >= 1 && parsedPort <= 65535)
|
|
105
|
+
port = parsedPort;
|
|
106
|
+
}
|
|
107
|
+
if (host === '')
|
|
108
|
+
return undefined;
|
|
109
|
+
const alias = concrete.get(host);
|
|
110
|
+
if (alias !== undefined) {
|
|
111
|
+
// 别名自己还依赖跳板机 → 不支持嵌套(单跳),按「解析不出」处理
|
|
112
|
+
if (alias.has('proxyjump') || alias.has('proxycommand'))
|
|
113
|
+
return undefined;
|
|
114
|
+
const aliasHost = alias.get('hostname') ?? host;
|
|
115
|
+
const aliasPortRaw = Number(alias.get('port'));
|
|
116
|
+
const jump = {
|
|
117
|
+
host: aliasHost,
|
|
118
|
+
port: Number.isInteger(aliasPortRaw) && aliasPortRaw >= 1 && aliasPortRaw <= 65535 ? aliasPortRaw : port,
|
|
119
|
+
};
|
|
120
|
+
const user = username !== '' ? username : alias.get('user');
|
|
121
|
+
if (typeof user === 'string' && user.trim() !== '')
|
|
122
|
+
jump.username = user.trim();
|
|
123
|
+
return jump;
|
|
124
|
+
}
|
|
125
|
+
const jump = { host, port };
|
|
126
|
+
if (username !== '')
|
|
127
|
+
jump.username = username;
|
|
128
|
+
return jump;
|
|
129
|
+
};
|
|
130
|
+
for (const item of blocks) {
|
|
131
|
+
if (!isConcrete(item)) {
|
|
132
|
+
skippedOther += 1;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const name = item.patterns[0];
|
|
136
|
+
// ProxyCommand 与 ProxyJump 同时给时按 OpenSSH 语义:ProxyJump 优先(这里先判 ProxyCommand
|
|
137
|
+
// 是为了「只配了 ProxyCommand」这种情况能被明确报数,而不是静默直连)
|
|
138
|
+
const hasJump = item.options.has('proxyjump') && !proxyOptOut(item.options.get('proxyjump'));
|
|
139
|
+
const hasCommand = item.options.has('proxycommand') && !proxyOptOut(item.options.get('proxycommand'));
|
|
140
|
+
let jump;
|
|
141
|
+
if (hasJump) {
|
|
142
|
+
jump = resolveJump(item.options.get('proxyjump') ?? '');
|
|
143
|
+
if (jump === undefined) {
|
|
144
|
+
proxyCount += 1;
|
|
145
|
+
if (proxy.length < MAX_PROXY_NAMES)
|
|
146
|
+
proxy.push(name);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else if (hasCommand) {
|
|
151
|
+
proxyCommandCount += 1;
|
|
152
|
+
if (proxyCommand.length < MAX_PROXY_NAMES)
|
|
153
|
+
proxyCommand.push(name);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const user = item.options.get('user');
|
|
157
|
+
if (typeof user !== 'string' || user.trim() === '') {
|
|
158
|
+
skippedOther += 1;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (entries.length >= MAX_IMPORT_ENTRIES) {
|
|
162
|
+
droppedOverflow += 1;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const host = item.options.get('hostname') ?? name;
|
|
166
|
+
const portRaw = Number(item.options.get('port'));
|
|
167
|
+
const port = Number.isInteger(portRaw) && portRaw >= 1 && portRaw <= 65535 ? portRaw : 22;
|
|
168
|
+
const identityFile = item.options.get('identityfile');
|
|
169
|
+
const entry = {
|
|
170
|
+
name,
|
|
171
|
+
host,
|
|
172
|
+
port,
|
|
173
|
+
username: user.trim(),
|
|
174
|
+
auth: identityFile !== undefined && identityFile.trim() !== '' ? 'key' : 'agent',
|
|
175
|
+
keyPath: identityFile !== undefined ? identityFile.trim() : '',
|
|
176
|
+
passphrase: '',
|
|
177
|
+
password: '',
|
|
178
|
+
agentForward: false,
|
|
179
|
+
};
|
|
180
|
+
if (jump !== undefined) {
|
|
181
|
+
entry.jump = jump;
|
|
182
|
+
jumpImported += 1;
|
|
183
|
+
}
|
|
184
|
+
entries.push(entry);
|
|
185
|
+
}
|
|
186
|
+
return { entries, proxy, proxyCount, proxyCommand, proxyCommandCount, jumpImported, skippedOther, droppedOverflow };
|
|
187
|
+
}
|
|
188
|
+
/** 只要导入候选(老调用点与既有用例的形状);需要「跳过了什么」时用 detailed 版。 */
|
|
189
|
+
export function parseSshConfig(text) {
|
|
190
|
+
return parseSshConfigDetailed(text).entries;
|
|
73
191
|
}
|
|
74
192
|
//# sourceMappingURL=ssh-config.js.map
|
package/lib/ssh-config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssh-config.js","sourceRoot":"","sources":["../src/ssh-config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ssh-config.js","sourceRoot":"","sources":["../src/ssh-config.ts"],"names":[],"mappings":"AAuBA,gCAAgC;AAChC,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAA;AACrC,mDAAmD;AACnD,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAA;AAqBjC;;;GAGG;AACH,SAAS,WAAW,CAAC,KAAyB;IAC5C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,MAAM,CAAA;AAC3E,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,IAAY;IACjD,MAAM,OAAO,GAAmB,EAAE,CAAA;IAClC,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,MAAM,YAAY,GAAa,EAAE,CAAA;IACjC,IAAI,UAAU,GAAG,CAAC,CAAA;IAClB,IAAI,iBAAiB,GAAG,CAAC,CAAA;IACzB,IAAI,YAAY,GAAG,CAAC,CAAA;IACpB,IAAI,YAAY,GAAG,CAAC,CAAA;IACpB,IAAI,eAAe,GAAG,CAAC,CAAA;IAEvB;;;;OAIG;IACH,MAAM,MAAM,GAAgE,EAAE,CAAA;IAC9E,IAAI,KAAK,GAAgE,IAAI,CAAA;IAE7E,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QAC1C,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAA;QACzB,IAAI,IAAI,KAAK,EAAE,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAQ;QACjD,gCAAgC;QAChC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QAC/B,IAAI,IAAI,KAAK,CAAC,CAAC;YAAE,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,EAAE,CAAA;QAClD,IAAI,IAAI,KAAK,EAAE;YAAE,SAAQ;QACzB,IAAI,GAAW,CAAA;QACf,IAAI,IAAY,CAAA;QAChB,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAC5B,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC;YAC5C,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;YAC9B,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QAClC,CAAC;aAAM,CAAC;YACN,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAA;YAC1C,IAAI,KAAK,KAAK,IAAI;gBAAE,SAAQ;YAC5B,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;YACd,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA,CAAC,yBAAyB;QAChE,CAAC;QACD,MAAM,QAAQ,GAAG,GAAG,CAAC,WAAW,EAAE,CAAA;QAClC,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;YACxB,IAAI,KAAK,KAAK,IAAI;gBAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;YACtC,8BAA8B;YAC9B,KAAK,GAAG,EAAE,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,EAAE,CAAA;YAChG,SAAQ;QACV,CAAC;QACD,IAAI,KAAK,KAAK,IAAI;YAAE,SAAQ;QAC5B,IAAI,QAAQ,KAAK,SAAS;YAAE,SAAQ,CAAC,oBAAoB;QACzD,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,SAAQ,CAAC,mBAAmB;QAC7D,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,CAAA;IAC3D,CAAC;IACD,IAAI,KAAK,KAAK,IAAI;QAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAEtC,yCAAyC;IACzC,MAAM,UAAU,GAAG,CAAC,IAA4B,EAAW,EAAE,CAC3D,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAA;IAExG,kDAAkD;IAClD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA+B,CAAA;IACvD,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;QAC1B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,SAAQ;QAC/B,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAA;QAC7B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAA;IAC3D,CAAC;IAED;;;;;;OAMG;IACH,MAAM,WAAW,GAAG,CAAC,KAAa,EAA2B,EAAE;QAC7D,IAAI,IAAI,GAAG,KAAK,CAAC,IAAI,EAAE,CAAA;QACvB,IAAI,IAAI,KAAK,EAAE;YAAE,OAAO,SAAS,CAAA;QACjC,IAAI,QAAQ,GAAG,EAAE,CAAA;QACjB,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;QAChC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;YACX,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;YACnC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QAClC,CAAC;QACD,IAAI,IAAI,GAAG,IAAI,CAAA;QACf,IAAI,IAAI,GAAG,EAAE,CAAA;QACb,MAAM,KAAK,GAAG,2BAA2B,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACpF,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;YACf,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;YACnC,IAAI,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,UAAU,IAAI,CAAC,IAAI,UAAU,IAAI,KAAK;gBAAE,IAAI,GAAG,UAAU,CAAA;QAC/F,CAAC;QACD,IAAI,IAAI,KAAK,EAAE;YAAE,OAAO,SAAS,CAAA;QACjC,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QAChC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,mCAAmC;YACnC,IAAI,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC;gBAAE,OAAO,SAAS,CAAA;YACzE,MAAM,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,IAAI,CAAA;YAC/C,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAA;YAC9C,MAAM,IAAI,GAAgB;gBACxB,IAAI,EAAE,SAAS;gBACf,IAAI,EAAE,MAAM,CAAC,SAAS,CAAC,YAAY,CAAC,IAAI,YAAY,IAAI,CAAC,IAAI,YAAY,IAAI,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI;aACzG,CAAA;YACD,MAAM,IAAI,GAAG,QAAQ,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;YAC3D,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;gBAAE,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE,CAAA;YAC/E,OAAO,IAAI,CAAA;QACb,CAAC;QACD,MAAM,IAAI,GAAgB,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;QACxC,IAAI,QAAQ,KAAK,EAAE;YAAE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QAC7C,OAAO,IAAI,CAAA;IACb,CAAC,CAAA;IAED,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;QAC1B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YACtB,YAAY,IAAI,CAAC,CAAA;YACjB,SAAQ;QACV,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAA;QAC7B,2EAA2E;QAC3E,2CAA2C;QAC3C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAA;QAC5F,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAA;QACrG,IAAI,IAA6B,CAAA;QACjC,IAAI,OAAO,EAAE,CAAC;YACZ,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAA;YACvD,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;gBACvB,UAAU,IAAI,CAAC,CAAA;gBACf,IAAI,KAAK,CAAC,MAAM,GAAG,eAAe;oBAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;gBACpD,SAAQ;YACV,CAAC;QACH,CAAC;aAAM,IAAI,UAAU,EAAE,CAAC;YACtB,iBAAiB,IAAI,CAAC,CAAA;YACtB,IAAI,YAAY,CAAC,MAAM,GAAG,eAAe;gBAAE,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YAClE,SAAQ;QACV,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;QACrC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACnD,YAAY,IAAI,CAAC,CAAA;YACjB,SAAQ;QACV,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,IAAI,kBAAkB,EAAE,CAAC;YACzC,eAAe,IAAI,CAAC,CAAA;YACpB,SAAQ;QACV,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,IAAI,CAAA;QACjD,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAA;QAChD,MAAM,IAAI,GAAG,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,OAAO,IAAI,CAAC,IAAI,OAAO,IAAI,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAA;QACzF,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;QACrD,MAAM,KAAK,GAAiB;YAC1B,IAAI;YACJ,IAAI;YACJ,IAAI;YACJ,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE;YACrB,IAAI,EAAE,YAAY,KAAK,SAAS,IAAI,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO;YAChF,OAAO,EAAE,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE;YAC9D,UAAU,EAAE,EAAE;YACd,QAAQ,EAAE,EAAE;YACZ,YAAY,EAAE,KAAK;SACpB,CAAA;QACD,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,KAAK,CAAC,IAAI,GAAG,IAAI,CAAA;YACjB,YAAY,IAAI,CAAC,CAAA;QACnB,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACrB,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,CAAA;AACrH,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,sBAAsB,CAAC,IAAI,CAAC,CAAC,OAAO,CAAA;AAC7C,CAAC"}
|
package/lib/ssh.d.ts
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @hyzyn/dsh-tty — SSH 会话封装(方案 C:ssh2 原生集成)。
|
|
3
|
+
*
|
|
4
|
+
* 不经过本地 ssh 进程 / node-pty,直接用 ssh2 建立连接并开 shell channel,
|
|
5
|
+
* 包装成与本地 PTY 完全一致的 TermHandle 形状,TtyServer 无差别调度:
|
|
6
|
+
* input/resize/kill 上行,data/exit 下行,背压、环形缓冲、agent 工具全复用。
|
|
7
|
+
*
|
|
8
|
+
* 认证优先级由 spec.auth 决定:
|
|
9
|
+
* agent —— ssh-agent(SSH_AUTH_SOCK),最推荐,凭证不落盘
|
|
10
|
+
* key —— keyPath 私钥文件(~ 可省略 home),passphrase 可选
|
|
11
|
+
* password —— 密码认证,同时挂 keyboard-interactive(很多服务端只开这个)
|
|
12
|
+
* password / passphrase 支持 `env:VAR` 前缀从进程环境变量取值(配合
|
|
13
|
+
* dsh-env-manager 插件托管密钥,避免明文写入 settings 文件)。
|
|
14
|
+
*
|
|
15
|
+
* 主机密钥策略:known_hosts TOFU(trust-on-first-use)钉扎——hostVerifier 里
|
|
16
|
+
* 首次连接记录 sha256 指纹(经 HostKeyStore 持久化),之后每次连接校验:
|
|
17
|
+
* 指纹一致放行;指纹变更拒绝连接(防中间人冒充),用户确认安全后可在
|
|
18
|
+
* 设置卡片删除该主机记录重连。未提供 hostKeyStore 时退化为 accept-and-log
|
|
19
|
+
* (旧行为,测试路径用)。
|
|
20
|
+
*/
|
|
21
|
+
import { Client } from 'ssh2';
|
|
22
|
+
import type { ClientChannel, ConnectConfig } from 'ssh2';
|
|
23
|
+
import type { ChildProcess } from 'node:child_process';
|
|
24
|
+
import { Duplex, PassThrough } from 'node:stream';
|
|
3
25
|
export interface TermExit {
|
|
4
26
|
exitCode: number | null;
|
|
5
27
|
signal: string | null;
|
|
@@ -65,6 +87,43 @@ export interface SshSpec {
|
|
|
65
87
|
password?: string;
|
|
66
88
|
/** OpenSSH agent forwarding:远程可用本地 ssh-agent 的钥匙(git clone 等)。 */
|
|
67
89
|
agentForward?: boolean;
|
|
90
|
+
/** 经跳板机连接(ProxyJump 语义,**单跳**);缺省 = 直连。 */
|
|
91
|
+
jump?: SshJumpSpec;
|
|
92
|
+
/**
|
|
93
|
+
* 代理命令(ProxyCommand 语义):本机执行的命令,它的 stdin/stdout 就是 SSH 传输。
|
|
94
|
+
*
|
|
95
|
+
* **单独一档信任级**:这是「设置字段驱动的本机任意命令执行」,与跳板机(只连一跳 TCP)
|
|
96
|
+
* 完全不同。因此:
|
|
97
|
+
* - **默认关闭**(settings `allowProxyCommand`):关着时携带它的连接一律**明确失败**,
|
|
98
|
+
* 绝不静默退回直连——直连多半也连不上,还会把「配置没生效」伪装成网络问题;
|
|
99
|
+
* - **`~/.ssh/config` 导入永不自动带入**(`parseSshConfigDetailed` 只报数);
|
|
100
|
+
* - `%h` / `%p` / `%r` / `%n` / `%%` 按 OpenSSH 同义展开(见 `expandProxyCommand`),
|
|
101
|
+
* 展开值只允许 `[A-Za-z0-9._@:\[\]-]`,否则**拒绝执行**(防连接簿字段注入 shell);
|
|
102
|
+
* - 与 `jump` 同时给时按 OpenSSH 语义 **ProxyJump 优先**,并记一条 warn。
|
|
103
|
+
*/
|
|
104
|
+
proxyCommand?: string;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 跳板机规格(ProxyJump 语义,**单跳**)。
|
|
108
|
+
*
|
|
109
|
+
* 与 `SshSpec` 同形但**不再嵌套**——不支持「跳板机的跳板机」;`username` / `auth` /
|
|
110
|
+
* `keyPath` / `passphrase` / `password` 任一缺省都会**继承目标那一跳**(企业内网里两者
|
|
111
|
+
* 通常共用同一把钥匙或同一个 agent),这也正是 v1 的界面只需要一个输入框的原因。
|
|
112
|
+
*
|
|
113
|
+
* 为什么不像 OpenSSH 那样只存 `user@host:port` 字符串:字符串装不下「与目标不同的凭据」,
|
|
114
|
+
* 而本仓所有认证都要走 `resolveSecret`(`env:VAR` 引用 / 凭据存储层)。界面与导入可以把
|
|
115
|
+
* 简写解析成这个结构。
|
|
116
|
+
*
|
|
117
|
+
* 凭据永不进日志与错误文案:展示串只由 `jumpTargetLabel()` 生成(`user@host:port`)。
|
|
118
|
+
*/
|
|
119
|
+
export interface SshJumpSpec {
|
|
120
|
+
host: string;
|
|
121
|
+
port?: number;
|
|
122
|
+
username?: string;
|
|
123
|
+
auth?: 'agent' | 'key' | 'password';
|
|
124
|
+
keyPath?: string;
|
|
125
|
+
passphrase?: string;
|
|
126
|
+
password?: string;
|
|
68
127
|
}
|
|
69
128
|
/** 连接簿条目(带名字,存 settings)。 */
|
|
70
129
|
export interface SshHostEntry extends SshSpec {
|
|
@@ -162,6 +221,11 @@ export declare function classifyError(message: string): string;
|
|
|
162
221
|
* resolveSecretVia —— 这是"存入凭据存储"的值能被连接真正用到的唯一通路。
|
|
163
222
|
*/
|
|
164
223
|
export declare function buildConnectConfig(spec: SshSpec): Promise<ConnectConfig>;
|
|
224
|
+
/** 本文件与各连接点共用的最小日志面(结构上兼容宿主 logger)。 */
|
|
225
|
+
export interface SshLogger {
|
|
226
|
+
info(msg: string): void;
|
|
227
|
+
warn(msg: string): void;
|
|
228
|
+
}
|
|
165
229
|
/** TOFU 主机指纹策略(hostVerifier 接线);返回的 mismatchMessage() 供连接错误路径取人类可读拒绝原因。 */
|
|
166
230
|
export declare function applyHostKeyPolicy(options: {
|
|
167
231
|
connectConfig: ConnectConfig;
|
|
@@ -175,6 +239,196 @@ export declare function applyHostKeyPolicy(options: {
|
|
|
175
239
|
}): {
|
|
176
240
|
mismatchMessage(): string | null;
|
|
177
241
|
};
|
|
242
|
+
/** 跳板机展示串(`user@host:port`);没有跳板机时返回空串。**凭据不进这里**。 */
|
|
243
|
+
export declare function jumpTargetLabel(spec: SshSpec): string;
|
|
244
|
+
/**
|
|
245
|
+
* 跳板机连接 + 借来的通道(目标那一跳把它当 `ConnectConfig.sock`)。
|
|
246
|
+
*
|
|
247
|
+
* 跳板机**自己的 TOFU 策略**在 `dialJump` 内部就接好了(指纹变更提示必须来自正确的那一跳,
|
|
248
|
+
* 而握手期的错误也只有那一段能拿到),所以不往外传句柄。
|
|
249
|
+
*/
|
|
250
|
+
export interface JumpDial {
|
|
251
|
+
/**
|
|
252
|
+
* 跳板机连接:**它拥有通道**,目标 client 只是借用(ssh2 的 `end()`/`destroy()` 只关
|
|
253
|
+
* 借来的通道,不关跳板机传输)——谁拨的号,谁就要在收尾时 `end()` 它。
|
|
254
|
+
*/
|
|
255
|
+
bastion: Client;
|
|
256
|
+
sock: ClientChannel;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* 拨跳板机并借一条 `forwardOut` 通道(ProxyJump 单跳)。
|
|
260
|
+
*
|
|
261
|
+
* 三件事刻意做在这里:
|
|
262
|
+
* 1. **失败一律点名跳板机**(见 `targetWithJump` 的理由);
|
|
263
|
+
* 2. **指纹策略单独一份**:TOFU 的键是 `(host, port)`,跳板机与目标撞 host:port
|
|
264
|
+
* (NAT 后的 `127.0.0.1:22` 很常见)时不能共用句柄,否则会出现假「指纹变更」;
|
|
265
|
+
* 3. **失败路径自己关连接**:抛出去之前 `end()` 掉,否则每次重试都会漏一条
|
|
266
|
+
* keepalive 一直养着的连接。
|
|
267
|
+
*/
|
|
268
|
+
export declare function dialJump(options: {
|
|
269
|
+
spec: SshSpec;
|
|
270
|
+
store?: HostKeyStore;
|
|
271
|
+
logger?: SshLogger;
|
|
272
|
+
/** 覆盖跳板机那一跳的握手超时(探针路径要短;缺省沿用 buildConnectConfig 的 20s)。 */
|
|
273
|
+
readyTimeoutMs?: number;
|
|
274
|
+
}): Promise<JumpDial>;
|
|
275
|
+
/** 代理命令长度上限:与自定义命令标签同量级(它是一条命令行,不是脚本文件)。 */
|
|
276
|
+
export declare const PROXY_COMMAND_MAX = 2000;
|
|
277
|
+
/** 关着闸门时携带代理命令的连接报什么错(导出供单测与客户端文案对照)。 */
|
|
278
|
+
export declare const PROXY_COMMAND_DISABLED: string;
|
|
279
|
+
/**
|
|
280
|
+
* 宿主**没有授权**这条能力时报什么错(与「授权了但开关关着」分开报)。
|
|
281
|
+
*
|
|
282
|
+
* 为什么要分开:两者的「下一步动作」完全不同——前者要去宿主侧设环境变量 + 重启(界面上那个
|
|
283
|
+
* 开关是点不动的),后者只是把开关打开。合成一句话会让用户对着一个点不动的开关反复点。
|
|
284
|
+
*/
|
|
285
|
+
export declare const PROXY_COMMAND_NOT_GRANTED: string;
|
|
286
|
+
/** 设置 ProxyCommand 闸门(插件 settings 就绪与每次热更新时调用)。 */
|
|
287
|
+
export declare function setProxyCommandPolicy(next: {
|
|
288
|
+
granted: boolean;
|
|
289
|
+
enabled: boolean;
|
|
290
|
+
}): void;
|
|
291
|
+
/** 当前是否真的会用代理命令(探针用它把「配了但没开」与「连不上」分开报)。 */
|
|
292
|
+
export declare function proxyCommandAllowedNow(): boolean;
|
|
293
|
+
/** 宿主侧是否授权了这条能力(探针用它选文案:未授权 vs 未启用)。 */
|
|
294
|
+
export declare function proxyCommandGrantedNow(): boolean;
|
|
295
|
+
/**
|
|
296
|
+
* 清洗一份代理命令输入(settings schema / 宽松清洗 / 内联融合共用)。
|
|
297
|
+
*
|
|
298
|
+
* 返回 `undefined` = 没配。**只做形状校验**(非空 / 单行 / 长度),命令内容本身不解释——
|
|
299
|
+
* 「能不能执行」由闸门决定,不由这里猜(含 `;` `/` `|` 都是合法的 shell 写法)。
|
|
300
|
+
*/
|
|
301
|
+
export declare function sanitizeProxyCommand(input: unknown): string | undefined;
|
|
302
|
+
/** 严格校验一份代理命令输入(HTTP POST 路径);返回错误信息或清洗结果。 */
|
|
303
|
+
export declare function validateProxyCommand(input: unknown): {
|
|
304
|
+
proxyCommand?: string;
|
|
305
|
+
error?: string;
|
|
306
|
+
};
|
|
307
|
+
/**
|
|
308
|
+
* 展开 `%h` / `%p` / `%r` / `%n` / `%%`(与 OpenSSH 同义)。
|
|
309
|
+
*
|
|
310
|
+
* `%h` 目标主机、`%p` 目标端口、`%r` 目标用户名、`%n` 目标主机(如写法里给的名字——
|
|
311
|
+
* 本包没有别名概念,与 `%h` 同值)、`%%` 字面 `%`。其余 `%X` **原样保留**(不报错):
|
|
312
|
+
* 用户可能是在命令里写 `printf '%s'`,替我们没实现的占位符而失败才是意外。
|
|
313
|
+
*
|
|
314
|
+
* 代入值必须过白名单,否则**抛错拒绝执行**(见 `PROXY_VALUE_SAFE`)。
|
|
315
|
+
*/
|
|
316
|
+
export declare function expandProxyCommand(command: string, spec: SshSpec): string;
|
|
317
|
+
/** 代理命令传输:子进程 + 它的 stdio(目标那条连接把 stdio 当 `ConnectConfig.sock`)。 */
|
|
318
|
+
export interface ProxyCommandDial {
|
|
319
|
+
/**
|
|
320
|
+
* 代理命令子进程:**它拥有传输**,目标 client 只是借用——ssh2 的 `end()`/`destroy()`
|
|
321
|
+
* 只关借来的流,不会去杀子进程。
|
|
322
|
+
*/
|
|
323
|
+
child: ChildProcess;
|
|
324
|
+
/** 目标连接用的传输(stdin/stdout 合成的 Duplex)。 */
|
|
325
|
+
sock: Duplex;
|
|
326
|
+
/** 子进程提前退出 / 出错的事实;没有则 null(错误文案里点名它,别让用户去查目标主机)。 */
|
|
327
|
+
failure(): Error | null;
|
|
328
|
+
/**
|
|
329
|
+
* 已经攒到的 stderr 摘要(`;代理命令 stderr: …` 或空串)——**错误路径的兜底**。
|
|
330
|
+
*
|
|
331
|
+
* 为什么需要它(真机验收才暴露的竞态):ssh2 一看到流断了就报错,而「子进程退出 / 传输
|
|
332
|
+
* 关闭」这两个事件比它晚 1~2ms,于是 `failure()` 那一刻还是 null,错误文案就只剩一句
|
|
333
|
+
* 「连接已关闭」——最有用的一句(命令自己说了什么)被丢掉。调用方**已经在报错**时,
|
|
334
|
+
* 手里有 stderr 就该交出去;没有 stderr 则返回空串(不制造噪音)。
|
|
335
|
+
*/
|
|
336
|
+
stderrHint(): string;
|
|
337
|
+
/** 收尾:关传输 + 杀子进程(**幂等**;所有 teardown 路径都要调,否则漏一个常驻进程)。 */
|
|
338
|
+
dispose(): void;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* 启动代理命令并把它接到目标连接上。
|
|
342
|
+
*
|
|
343
|
+
* 这一跳与 `dialJump` 的差别,逐条都有理由:
|
|
344
|
+
* 1. **先过闸门**:关着就直接抛 `PROXY_COMMAND_DISABLED`(不 spawn、不退回直连);
|
|
345
|
+
* 2. **没有「握手」可等**:传输就是子进程的 stdio,ssh2 会在它上面跑握手——所以这里
|
|
346
|
+
* spawn 成功即返回,剩下的失败由目标连接的 error/close 路径 + `failure()` 共同呈现;
|
|
347
|
+
* 3. **子进程死了要拖垮传输**:提前退出时 destroy 掉 sock,逼 ssh2 立刻报错(否则
|
|
348
|
+
* 「命令一开始就失败」会伪装成 20s 握手超时);
|
|
349
|
+
* 4. **stderr 必须排空**:不排空的话命令输出一多就把管道写满、子进程卡死(经典坑),
|
|
350
|
+
* 同时留一小段尾巴进错误文案——它是排查代理命令失败最直接的信息;
|
|
351
|
+
* 5. **命令原文不进日志与错误文案**:它可能含凭据(如 `-i /path/key`),日志只写
|
|
352
|
+
* 「已启动代理命令」与退出码。
|
|
353
|
+
*/
|
|
354
|
+
export declare function dialProxyCommand(options: {
|
|
355
|
+
spec: SshSpec;
|
|
356
|
+
logger?: SshLogger;
|
|
357
|
+
}): Promise<ProxyCommandDial>;
|
|
358
|
+
/**
|
|
359
|
+
* 清洗一份跳板机输入(settings schema / 宽松清洗 / 内联融合共用)。
|
|
360
|
+
*
|
|
361
|
+
* 返回 `undefined` = **没有可用的跳板机**(`host` 为空)——调用方据此把 `jump` 整个丢掉,
|
|
362
|
+
* 而不是留下一个 `host: ''` 的半个对象(那会让 `dialJump` 去连空主机名)。
|
|
363
|
+
* 缺省不填的字段**不写进结果**:它们要在拨号时「继承目标那一跳」(见 `jumpSpecOf`)。
|
|
364
|
+
*/
|
|
365
|
+
export declare function sanitizeJumpSpec(input: unknown): SshJumpSpec | undefined;
|
|
366
|
+
/** 严格校验一份跳板机输入(HTTP POST 路径);返回错误信息或清洗结果。 */
|
|
367
|
+
export declare function validateJumpSpec(input: unknown): {
|
|
368
|
+
jump?: SshJumpSpec;
|
|
369
|
+
error?: string;
|
|
370
|
+
};
|
|
371
|
+
/** 目标连接的传输来源(两者互斥:`jump` 优先,见 `attachSshTransport`)。 */
|
|
372
|
+
export interface SshTransport {
|
|
373
|
+
/** 需要跳板机时非 null;**调用方必须在收尾时 `end()` 它**(目标只是借用它的通道)。 */
|
|
374
|
+
bastion: Client | null;
|
|
375
|
+
/** 需要代理命令时非 null;**调用方必须在收尾时 `dispose()` 它**(目标只是借用它的 stdio)。 */
|
|
376
|
+
proxy: ProxyCommandDial | null;
|
|
377
|
+
}
|
|
378
|
+
/** 建连前准备的结果:目标 config(可能挂了跳板机通道 / 代理命令传输)+ 目标那一跳的 TOFU 策略。 */
|
|
379
|
+
export interface PreparedSshConnect {
|
|
380
|
+
connectConfig: ConnectConfig;
|
|
381
|
+
policy: {
|
|
382
|
+
mismatchMessage(): string | null;
|
|
383
|
+
};
|
|
384
|
+
/** 需要跳板机时非 null;**调用方必须在收尾时 `end()` 它**(目标只是借用它的通道)。 */
|
|
385
|
+
bastion: Client | null;
|
|
386
|
+
/** 需要代理命令时非 null;**调用方必须在收尾时 `dispose()` 它**(目标只是借用它的 stdio)。 */
|
|
387
|
+
proxy: ProxyCommandDial | null;
|
|
388
|
+
/** 展示串:带「经跳板机 X」后缀,错误文案直接用。 */
|
|
389
|
+
target: string;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* 代理命令失败的事实 → 错误文案后缀(空串 = 没失败)。
|
|
393
|
+
*
|
|
394
|
+
* 为什么要它:代理命令死了之后 ssh2 只会报一句「握手前连接中断」,那会把用户支到目标主机上
|
|
395
|
+
* 去查。调用方在错误分支拼上这句,用户才知道是该去看代理命令的 stderr。
|
|
396
|
+
*/
|
|
397
|
+
export declare function proxyFailureSuffix(proxy: ProxyCommandDial | null): string;
|
|
398
|
+
/**
|
|
399
|
+
* 四个连接点(终端 / SFTP / 隧道 / 探针)**共用**的建连前准备。
|
|
400
|
+
*
|
|
401
|
+
* 为什么要有这个函数:跳板机不是「终端的特性」——SFTP、端口转发、探针各自都在建 SSH 连接
|
|
402
|
+
* (`sftp.ts` / `tunnels.ts` / `probe.ts` 各有一处 `new Client()`)。把「构造 config →
|
|
403
|
+
* 需要时拨跳板机 / 起代理命令 → 接上传输 → 装目标 TOFU 策略」收成一处,四条路才不会各写一份
|
|
404
|
+
* (那正是这一项立项时点名的「三处各写一份必然漂」)。
|
|
405
|
+
*
|
|
406
|
+
* **调用方负责**:自己 `conn.connect(connectConfig)`、自己处理 ready/error/close,
|
|
407
|
+
* 并在收尾(成功或失败)时对 `bastion` 调 `end()`、对 `proxy` 调 `dispose()`。
|
|
408
|
+
*/
|
|
409
|
+
export declare function prepareSshConnect(options: {
|
|
410
|
+
spec: SshSpec;
|
|
411
|
+
store?: HostKeyStore;
|
|
412
|
+
logger?: SshLogger;
|
|
413
|
+
/** 覆盖两跳的握手超时(探针路径要短) */
|
|
414
|
+
readyTimeoutMs?: number;
|
|
415
|
+
}): Promise<PreparedSshConnect>;
|
|
416
|
+
/**
|
|
417
|
+
* 只做「构造目标 config(需要时拨跳板机 / 起代理命令并把传输接上)」这一半。
|
|
418
|
+
*
|
|
419
|
+
* 拆出来的唯一理由:**探针自己装 hostVerifier**(`makeHostKeyVerifier` 要收集
|
|
420
|
+
* hostkey 结论,不用 `applyHostKeyPolicy`),但它同样需要跳板机 / 代理命令。返回值里的
|
|
421
|
+
* `transport` 由调用方负责收尾。
|
|
422
|
+
*/
|
|
423
|
+
export declare function attachSshTransport(options: {
|
|
424
|
+
spec: SshSpec;
|
|
425
|
+
store?: HostKeyStore;
|
|
426
|
+
logger?: SshLogger;
|
|
427
|
+
readyTimeoutMs?: number;
|
|
428
|
+
}): Promise<{
|
|
429
|
+
connectConfig: ConnectConfig;
|
|
430
|
+
transport: SshTransport;
|
|
431
|
+
}>;
|
|
178
432
|
/**
|
|
179
433
|
* 建立 SSH 连接并打开交互 shell channel,返回 TermHandle。
|
|
180
434
|
* 失败(连接超时/认证被拒/host 不可达)时 reject 带人类可读信息。
|