@hyzyn/dsh-tty 0.2.1 → 0.9.0
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 -0
- package/README.md +233 -43
- package/client.js +121 -24
- package/lib/index.d.ts +35 -4
- package/lib/index.js +1692 -94
- package/lib/index.js.map +1 -1
- package/lib/known-hosts.d.ts +6 -0
- package/lib/known-hosts.js +129 -0
- package/lib/known-hosts.js.map +1 -0
- package/lib/sftp.d.ts +110 -0
- package/lib/sftp.js +449 -0
- package/lib/sftp.js.map +1 -0
- package/lib/shell-integration.d.ts +11 -0
- package/lib/shell-integration.js +184 -0
- package/lib/shell-integration.js.map +1 -0
- package/lib/ssh-config.d.ts +15 -0
- package/lib/ssh-config.js +74 -0
- package/lib/ssh-config.js.map +1 -0
- package/lib/ssh.d.ts +40 -0
- package/lib/ssh.js +54 -14
- package/lib/ssh.js.map +1 -1
- package/lib/tunnels.d.ts +65 -0
- package/lib/tunnels.js +327 -0
- package/lib/tunnels.js.map +1 -0
- package/package.json +13 -11
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hyzyn/dsh-tty — shell 集成注入(OSC 133 命令边界标记 + OSC 7 cwd 上报)。
|
|
3
|
+
*
|
|
4
|
+
* 动机:tty_capture 只能读原始尾部,agent 拿不到「上一条命令」的边界与退出码;
|
|
5
|
+
* tty_list 的 cwd 是 spawn 时的快照,用户 cd 之后失真。借 shell 钩子在输出流里
|
|
6
|
+
* 打标记即可两全,不需要动 DSH 的 spawnTerminal 协议。
|
|
7
|
+
*
|
|
8
|
+
* 注入方式(对用户透明,不要求改 rc):
|
|
9
|
+
* - zsh:进程级懒创建临时目录写四个桩文件(.zshenv/.zprofile/.zshrc/.zlogin,
|
|
10
|
+
* 各自先 source 用户 ZDOTDIR/HOME 下的原文件,.zshrc 追加钩子),spawn 时经
|
|
11
|
+
* `-c` 包装层 `export ZDOTDIR=<桩目录>` 生效 —— VS Code 同款成熟方案;
|
|
12
|
+
* - bash:写桩 rc(先 source ~/.bashrc 再挂钩子),`exec bash --rcfile <桩>`;
|
|
13
|
+
* B 标记按版本二选一:bash ≥ 4.4 走 PS0;bash < 4.4(macOS 自带 3.2)无
|
|
14
|
+
* PS0,用**自卸式 DEBUG trap** 兜底——首次触发即发 B 并卸载(避免复合命令
|
|
15
|
+
* 内部逐条触发),再由 PROMPT_COMMAND 尾段在每轮 prompt 前重新武装;
|
|
16
|
+
* PROMPT_COMMAND 挂钩兼容字符串与数组(bash 5.1+)两种形态;
|
|
17
|
+
* - 其他 shell:返回 null,集成静默关闭(tty_capture{last} 会明确报错)。
|
|
18
|
+
*
|
|
19
|
+
* 标记语义(iTerm2/kitty/VS Code 同一约定):
|
|
20
|
+
* OSC 133;A —— prompt 开始;OSC 133;B —— 命令开始(preexec/PS0);
|
|
21
|
+
* OSC 133;D;<exit> —— 命令结束带退出码;OSC 7;file://<host><path> —— cwd 上报。
|
|
22
|
+
*/
|
|
23
|
+
import { mkdtempSync, writeFileSync } from 'node:fs';
|
|
24
|
+
import { homedir, tmpdir } from 'node:os';
|
|
25
|
+
import { join, basename } from 'node:path';
|
|
26
|
+
/** 桩文件目录(进程级懒创建,zsh/bash 共用进程生命周期)。 */
|
|
27
|
+
const stubDirs = new Map();
|
|
28
|
+
function ensureZshStubDir() {
|
|
29
|
+
const cached = stubDirs.get('zsh');
|
|
30
|
+
if (cached !== undefined)
|
|
31
|
+
return cached;
|
|
32
|
+
try {
|
|
33
|
+
const dir = mkdtempSync(join(tmpdir(), 'dsh-tty-zsh-'));
|
|
34
|
+
// 桩只做「source 用户原文件 + 追加钩子」;DSH_TTY_ORIG_ZDOTDIR 在包装层里
|
|
35
|
+
// 固化为用户原 ZDOTDIR(无则 $HOME),保证用户 rc 链路不变
|
|
36
|
+
for (const name of ['.zshenv', '.zprofile', '.zlogin']) {
|
|
37
|
+
writeFileSync(join(dir, name), [
|
|
38
|
+
'# dsh-tty shell integration stub (chain to user file)',
|
|
39
|
+
'[ -f "$DSH_TTY_ORIG_ZDOTDIR/' + name + '" ] && source "$DSH_TTY_ORIG_ZDOTDIR/' + name + '"',
|
|
40
|
+
'',
|
|
41
|
+
].join('\n'));
|
|
42
|
+
}
|
|
43
|
+
writeFileSync(join(dir, '.zshrc'), [
|
|
44
|
+
'# dsh-tty shell integration stub (chain to user file, then add hooks)',
|
|
45
|
+
'[ -f "$DSH_TTY_ORIG_ZDOTDIR/.zshrc" ] && source "$DSH_TTY_ORIG_ZDOTDIR/.zshrc"',
|
|
46
|
+
'',
|
|
47
|
+
'# >>> dsh-tty shell integration >>>',
|
|
48
|
+
'if [ -z "$DSH_TTY_HOOKS_LOADED" ]; then',
|
|
49
|
+
' export DSH_TTY_HOOKS_LOADED=1',
|
|
50
|
+
' typeset -g __DSH_TTY_IN_CMD=0',
|
|
51
|
+
' __dsh_tty_precmd() {',
|
|
52
|
+
' local ec=$?',
|
|
53
|
+
' if (( __DSH_TTY_IN_CMD )); then printf \'\\e]133;D;%s\\a\' "$ec"; __DSH_TTY_IN_CMD=0; fi',
|
|
54
|
+
' printf \'\\e]133;A\\a\'',
|
|
55
|
+
' printf \'\\e]7;file://%s%s\\a\' "${HOST}" "${PWD}"',
|
|
56
|
+
' }',
|
|
57
|
+
' __dsh_tty_preexec() {',
|
|
58
|
+
' __DSH_TTY_IN_CMD=1',
|
|
59
|
+
' printf \'\\e]133;B\\a\'',
|
|
60
|
+
' }',
|
|
61
|
+
' precmd_functions+=(__dsh_tty_precmd)',
|
|
62
|
+
' preexec_functions+=(__dsh_tty_preexec)',
|
|
63
|
+
'fi',
|
|
64
|
+
'# <<< dsh-tty shell integration <<<',
|
|
65
|
+
'',
|
|
66
|
+
].join('\n'));
|
|
67
|
+
stubDirs.set('zsh', dir);
|
|
68
|
+
return dir;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function ensureBashStubRc() {
|
|
75
|
+
const cached = stubDirs.get('bash');
|
|
76
|
+
if (cached !== undefined)
|
|
77
|
+
return cached;
|
|
78
|
+
try {
|
|
79
|
+
const dir = mkdtempSync(join(tmpdir(), 'dsh-tty-bash-'));
|
|
80
|
+
const rc = join(dir, 'bashrc');
|
|
81
|
+
writeFileSync(rc, [
|
|
82
|
+
'# dsh-tty shell integration stub (chain to user file, then add hooks)',
|
|
83
|
+
'[ -f "$HOME/.bashrc" ] && source "$HOME/.bashrc"',
|
|
84
|
+
'',
|
|
85
|
+
'# >>> dsh-tty shell integration >>>',
|
|
86
|
+
'if [ -z "$DSH_TTY_HOOKS_LOADED" ]; then',
|
|
87
|
+
' export DSH_TTY_HOOKS_LOADED=1',
|
|
88
|
+
' __DSH_TTY_IN_CMD=0',
|
|
89
|
+
' __dsh_tty_precmd() {',
|
|
90
|
+
' local ec=$?',
|
|
91
|
+
' if [ "$__DSH_TTY_IN_CMD" = "1" ]; then printf \'\\e]133;D;%s\\a\' "$ec"; __DSH_TTY_IN_CMD=0; fi',
|
|
92
|
+
' printf \'\\e]133;A\\a\'',
|
|
93
|
+
' printf \'\\e]7;file://%s%s\\a\' "${HOSTNAME}" "${PWD}"',
|
|
94
|
+
' }',
|
|
95
|
+
' # —— B 标记(命令开始):按 bash 版本二选一 ——',
|
|
96
|
+
' __dsh_tty_major=${BASH_VERSION%%.*}',
|
|
97
|
+
' __dsh_tty_minor=${BASH_VERSION#*.}; __dsh_tty_minor=${__dsh_tty_minor%%.*}',
|
|
98
|
+
' if [ "$__dsh_tty_major" -gt 4 ] || { [ "$__dsh_tty_major" -eq 4 ] && [ "$__dsh_tty_minor" -ge 4 ]; }; then',
|
|
99
|
+
' # bash ≥ 4.4:PS0 在每个提示符后、命令输出前展开',
|
|
100
|
+
' case "${PS0:-}" in',
|
|
101
|
+
' *\'133;B\'*) ;;',
|
|
102
|
+
' *) PS0=\'\\e]133;B\\a\'"$PS0" ;;',
|
|
103
|
+
' esac',
|
|
104
|
+
' # PROMPT_COMMAND 挂钩:兼容字符串与数组(bash 5.1+)两种形态',
|
|
105
|
+
' case "$(declare -p PROMPT_COMMAND 2>/dev/null)" in',
|
|
106
|
+
' "declare -a"*)',
|
|
107
|
+
' case "${PROMPT_COMMAND[*]}" in',
|
|
108
|
+
' *__dsh_tty_precmd*) ;;',
|
|
109
|
+
' *) PROMPT_COMMAND+=("__dsh_tty_precmd") ;;',
|
|
110
|
+
' esac ;;',
|
|
111
|
+
' *)',
|
|
112
|
+
' case "${PROMPT_COMMAND:-}" in',
|
|
113
|
+
' *__dsh_tty_precmd*) ;;',
|
|
114
|
+
' *) PROMPT_COMMAND="__dsh_tty_precmd${PROMPT_COMMAND:+;$PROMPT_COMMAND}" ;;',
|
|
115
|
+
' esac ;;',
|
|
116
|
+
' esac',
|
|
117
|
+
' else',
|
|
118
|
+
' # bash < 4.4(macOS 自带 3.2):无 PS0,用 DEBUG trap 兜底。实测两个 3.2 怪癖:',
|
|
119
|
+
' # 1) handler 内 trap - DEBUG 卸载不生效(trap 保持武装);',
|
|
120
|
+
' # 2) PROMPT_COMMAND 机制自身(precmd/rearm 段)也会触发 DEBUG fire。',
|
|
121
|
+
' # 因此按「永久武装 + handler 过滤」设计:$BASH_COMMAND 含桩内函数名的',
|
|
122
|
+
' # fire 直接忽略(否则幻影 B 会重启捕获区间,把用户输出切出 last 区域);',
|
|
123
|
+
' # PROMPT_COMMAND 尾段每轮重新武装(bash 4.0~4.3 上 trap - DEBUG 生效时',
|
|
124
|
+
' # 走正常卸载/重装轮转)。副作用:循环体等内部命令会多发 B,仅影响',
|
|
125
|
+
' # capture{last} 的截取起点,D/退出码与 tty_expect 不受影响。',
|
|
126
|
+
' __dsh_tty_preexec() {',
|
|
127
|
+
' case "$BASH_COMMAND" in',
|
|
128
|
+
' ""|*__dsh_tty_precmd*|*__dsh_tty_rearm*) return ;;',
|
|
129
|
+
' esac',
|
|
130
|
+
' __DSH_TTY_IN_CMD=1',
|
|
131
|
+
' printf \'\\e]133;B\\a\'',
|
|
132
|
+
' trap - DEBUG',
|
|
133
|
+
' }',
|
|
134
|
+
' __dsh_tty_rearm() { trap \'__dsh_tty_preexec\' DEBUG; }',
|
|
135
|
+
' # 不在 rc 加载时武装:bash 启动横幅("default interactive shell" 等)',
|
|
136
|
+
' # 会触发一次无主 fire,产生幻影 B/D;首次 PROMPT_COMMAND 尾段武装即可',
|
|
137
|
+
' case "${PROMPT_COMMAND:-}" in',
|
|
138
|
+
' *__dsh_tty_precmd*) ;;',
|
|
139
|
+
' *) PROMPT_COMMAND="__dsh_tty_precmd${PROMPT_COMMAND:+;$PROMPT_COMMAND}; __dsh_tty_rearm" ;;',
|
|
140
|
+
' esac',
|
|
141
|
+
' fi',
|
|
142
|
+
'fi',
|
|
143
|
+
'# <<< dsh-tty shell integration <<<',
|
|
144
|
+
'',
|
|
145
|
+
].join('\n'));
|
|
146
|
+
stubDirs.set('bash', rc);
|
|
147
|
+
return rc;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* 组装 shell 启动计划:在原有 TERM/COLORTERM 包装层之上,按 shell basename
|
|
155
|
+
* 注入集成(zsh → ZDOTDIR 桩;bash → --rcfile 桩)。integration=false 或不
|
|
156
|
+
* 支持的 shell 返回最简包装层。
|
|
157
|
+
*/
|
|
158
|
+
export function buildShellSpawn(shell, term, colorTerm, integration) {
|
|
159
|
+
const pre = `export TERM='${term}'; export COLORTERM='${colorTerm}';`;
|
|
160
|
+
const exec = `exec "${shell}"`;
|
|
161
|
+
if (integration) {
|
|
162
|
+
const kind = basename(shell);
|
|
163
|
+
if (kind === 'zsh') {
|
|
164
|
+
const stubDir = ensureZshStubDir();
|
|
165
|
+
if (stubDir !== undefined) {
|
|
166
|
+
return {
|
|
167
|
+
argv: [shell, '-c', `${pre} export DSH_TTY_ORIG_ZDOTDIR="\${ZDOTDIR:-${homedir()}}"; export ZDOTDIR='${stubDir}'; ${exec}`],
|
|
168
|
+
env: {},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else if (kind === 'bash') {
|
|
173
|
+
const stubRc = ensureBashStubRc();
|
|
174
|
+
if (stubRc !== undefined) {
|
|
175
|
+
return {
|
|
176
|
+
argv: [shell, '-c', `${pre} ${exec} --rcfile '${stubRc}'`],
|
|
177
|
+
env: {},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { argv: [shell, '-c', `${pre} ${exec}`], env: {} };
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=shell-integration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell-integration.js","sourceRoot":"","sources":["../src/shell-integration.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hyzyn/dsh-tty — ~/.ssh/config 迷你解析器(连接簿导入候选)。
|
|
3
|
+
*
|
|
4
|
+
* 宽容优先:目标是把常见配置安全搬进连接簿,而不是完整实现 OpenSSH 语法——
|
|
5
|
+
* - 键大小写不敏感,`key value` 与 `key=value` 都收;
|
|
6
|
+
* - `Host` 多模式时只收「全具体」块(任一模式含 * ? ! 或首字符为空格否定
|
|
7
|
+
* 即整块跳过),块名取第一个模式;
|
|
8
|
+
* - 只映射 HostName / User / Port / IdentityFile;Include 不展开(跳过),
|
|
9
|
+
* 其余选项(ProxyJump、ServerAliveInterval 等)原样忽略;
|
|
10
|
+
* - 没有 User 的块无法构成连接簿条目(username 必填),跳过;
|
|
11
|
+
* - IdentityFile 取第一个 → auth=key + keyPath,否则 auth=agent;
|
|
12
|
+
* - 单文件最多产出 100 条,超出丢弃(防异常巨型文件)。
|
|
13
|
+
*/
|
|
14
|
+
import type { SshHostEntry } from './ssh.js';
|
|
15
|
+
export declare function parseSshConfig(text: string): SshHostEntry[];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export function parseSshConfig(text) {
|
|
2
|
+
const entries = [];
|
|
3
|
+
/** 当前 Host 块:模式列表 + 选项表(键已小写)。 */
|
|
4
|
+
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
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
33
|
+
let line = rawLine.trim();
|
|
34
|
+
if (line === '' || line.startsWith('#'))
|
|
35
|
+
continue;
|
|
36
|
+
// 行内注释(非引号内的第一个 ' #'):宽容处理为直接截断
|
|
37
|
+
const hash = line.indexOf(' #');
|
|
38
|
+
if (hash !== -1)
|
|
39
|
+
line = line.slice(0, hash).trim();
|
|
40
|
+
if (line === '')
|
|
41
|
+
continue;
|
|
42
|
+
let key;
|
|
43
|
+
let rest;
|
|
44
|
+
const eq = line.indexOf('=');
|
|
45
|
+
if (eq > 0 && !/\s/.test(line.slice(0, eq))) {
|
|
46
|
+
key = line.slice(0, eq).trim();
|
|
47
|
+
rest = line.slice(eq + 1).trim();
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const match = line.match(/^(\S+)\s+(.*)$/);
|
|
51
|
+
if (match === null)
|
|
52
|
+
continue;
|
|
53
|
+
key = match[1];
|
|
54
|
+
rest = match[2].replace(/^=\s*/, ''); // 宽容「key = value」的空格等号写法
|
|
55
|
+
}
|
|
56
|
+
const keyLower = key.toLowerCase();
|
|
57
|
+
if (keyLower === 'host') {
|
|
58
|
+
flush();
|
|
59
|
+
// 「Host = name」的孤立等号当作分隔符宽容丢弃
|
|
60
|
+
block = { patterns: rest.split(/\s+/).filter((p) => p !== '' && p !== '='), options: new Map() };
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (block === null)
|
|
64
|
+
continue;
|
|
65
|
+
if (keyLower === 'include')
|
|
66
|
+
continue; // 不展开,避免读入用户无法预期的文件
|
|
67
|
+
if (block.options.has(keyLower))
|
|
68
|
+
continue; // 首个生效(OpenSSH 语义)
|
|
69
|
+
block.options.set(keyLower, rest.replace(/^"+|"+$/g, ''));
|
|
70
|
+
}
|
|
71
|
+
flush();
|
|
72
|
+
return entries;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=ssh-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssh-config.js","sourceRoot":"","sources":["../src/ssh-config.ts"],"names":[],"mappings":"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"}
|
package/lib/ssh.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ConnectConfig } from 'ssh2';
|
|
1
2
|
import { PassThrough } from 'node:stream';
|
|
2
3
|
export interface TermExit {
|
|
3
4
|
exitCode: number | null;
|
|
@@ -17,6 +18,13 @@ export interface TermHandle {
|
|
|
17
18
|
/** terminate 失败后的最后手段(本地 PTY:对顶层 shell 直接 SIGKILL)。 */
|
|
18
19
|
forceKill?(): void;
|
|
19
20
|
}
|
|
21
|
+
/** TOFU 主机指纹记录。 */
|
|
22
|
+
export interface HostKeyRecord {
|
|
23
|
+
host: string;
|
|
24
|
+
port: number;
|
|
25
|
+
/** hostVerifier 收到的原样 sha256 十六进制指纹。 */
|
|
26
|
+
fingerprint: string;
|
|
27
|
+
}
|
|
20
28
|
/** 内联 SSH 连接规格(ws 帧或连接簿条目共用)。 */
|
|
21
29
|
export interface SshSpec {
|
|
22
30
|
host: string;
|
|
@@ -26,6 +34,8 @@ export interface SshSpec {
|
|
|
26
34
|
keyPath?: string;
|
|
27
35
|
passphrase?: string;
|
|
28
36
|
password?: string;
|
|
37
|
+
/** OpenSSH agent forwarding:远程可用本地 ssh-agent 的钥匙(git clone 等)。 */
|
|
38
|
+
agentForward?: boolean;
|
|
29
39
|
}
|
|
30
40
|
/** 连接簿条目(带名字,存 settings)。 */
|
|
31
41
|
export interface SshHostEntry extends SshSpec {
|
|
@@ -39,9 +49,39 @@ export interface SshSpawnOptions {
|
|
|
39
49
|
info(msg: string): void;
|
|
40
50
|
warn(msg: string): void;
|
|
41
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* known_hosts TOFU 钉扎存储:首次连接 record() 记录指纹,之后 get() 校验。
|
|
54
|
+
* 缺省时退化为 accept-and-log(仅记录指纹,无条件放行)。
|
|
55
|
+
*/
|
|
56
|
+
hostKeyStore?: HostKeyStore;
|
|
42
57
|
}
|
|
58
|
+
/** 主机指纹钉扎存储(宿主半体实现为 LiveConfig + settings 持久化)。 */
|
|
59
|
+
export interface HostKeyStore {
|
|
60
|
+
/** 已记录的指纹(hostVerifier 收到的原样十六进制串);未记录返回 undefined。 */
|
|
61
|
+
get(host: string, port: number): string | undefined;
|
|
62
|
+
/** 首次连接握手时记录指纹。 */
|
|
63
|
+
record(host: string, port: number, fingerprint: string): void;
|
|
64
|
+
}
|
|
65
|
+
declare function expandHome(path: string): string;
|
|
66
|
+
/** 供 ~/.ssh/config 导入路由使用(~ 与 ~/ 前缀展开 home)。 */
|
|
67
|
+
export { expandHome };
|
|
43
68
|
/** 展示用目标串:user@host(非默认端口时带 :port)。 */
|
|
44
69
|
export declare function sshTarget(spec: SshSpec): string;
|
|
70
|
+
/** 构造连接配置(认证三态 + keepalive + hostHash);隧道管理器与 spawnSsh 共用。 */
|
|
71
|
+
export declare function buildConnectConfig(spec: SshSpec): ConnectConfig;
|
|
72
|
+
/** TOFU 主机指纹策略(hostVerifier 接线);返回的 mismatchMessage() 供连接错误路径取人类可读拒绝原因。 */
|
|
73
|
+
export declare function applyHostKeyPolicy(options: {
|
|
74
|
+
connectConfig: ConnectConfig;
|
|
75
|
+
spec: SshSpec;
|
|
76
|
+
store?: HostKeyStore;
|
|
77
|
+
logger?: {
|
|
78
|
+
info(msg: string): void;
|
|
79
|
+
warn(msg: string): void;
|
|
80
|
+
};
|
|
81
|
+
target: string;
|
|
82
|
+
}): {
|
|
83
|
+
mismatchMessage(): string | null;
|
|
84
|
+
};
|
|
45
85
|
/**
|
|
46
86
|
* 建立 SSH 连接并打开交互 shell channel,返回 TermHandle。
|
|
47
87
|
* 失败(连接超时/认证被拒/host 不可达)时 reject 带人类可读信息。
|
package/lib/ssh.js
CHANGED
|
@@ -12,12 +12,11 @@
|
|
|
12
12
|
* password / passphrase 支持 `env:VAR` 前缀从进程环境变量取值(配合
|
|
13
13
|
* dsh-env-manager 插件托管密钥,避免明文写入 settings 文件)。
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* 仅作为缓解;known_hosts 钉扎留作后续项。
|
|
15
|
+
* 主机密钥策略:known_hosts TOFU(trust-on-first-use)钉扎——hostVerifier 里
|
|
16
|
+
* 首次连接记录 sha256 指纹(经 HostKeyStore 持久化),之后每次连接校验:
|
|
17
|
+
* 指纹一致放行;指纹变更拒绝连接(防中间人冒充),用户确认安全后可在
|
|
18
|
+
* 设置卡片删除该主机记录重连。未提供 hostKeyStore 时退化为 accept-and-log
|
|
19
|
+
* (旧行为,测试路径用)。
|
|
21
20
|
*/
|
|
22
21
|
import { Client } from 'ssh2';
|
|
23
22
|
import { readFileSync } from 'node:fs';
|
|
@@ -43,6 +42,8 @@ function expandHome(path) {
|
|
|
43
42
|
return join(homedir(), path.slice(2));
|
|
44
43
|
return path;
|
|
45
44
|
}
|
|
45
|
+
/** 供 ~/.ssh/config 导入路由使用(~ 与 ~/ 前缀展开 home)。 */
|
|
46
|
+
export { expandHome };
|
|
46
47
|
/** 展示用目标串:user@host(非默认端口时带 :port)。 */
|
|
47
48
|
export function sshTarget(spec) {
|
|
48
49
|
const port = spec.port ?? 22;
|
|
@@ -51,7 +52,8 @@ export function sshTarget(spec) {
|
|
|
51
52
|
/* ------------------------------------------------------------------ *
|
|
52
53
|
* 连接与 channel 包装
|
|
53
54
|
* ------------------------------------------------------------------ */
|
|
54
|
-
|
|
55
|
+
/** 构造连接配置(认证三态 + keepalive + hostHash);隧道管理器与 spawnSsh 共用。 */
|
|
56
|
+
export function buildConnectConfig(spec) {
|
|
55
57
|
const auth = spec.auth ?? 'agent';
|
|
56
58
|
const base = {
|
|
57
59
|
host: spec.host,
|
|
@@ -60,7 +62,8 @@ function buildConnectConfig(spec) {
|
|
|
60
62
|
readyTimeout: 20000,
|
|
61
63
|
keepaliveInterval: 10000,
|
|
62
64
|
keepaliveCountMax: 3,
|
|
63
|
-
//
|
|
65
|
+
// hostVerifier 依赖 hostHash 计算指纹;放行与否由 spawnSsh 里的
|
|
66
|
+
// TOFU 策略(HostKeyStore)决定,见文件头策略说明
|
|
64
67
|
hostHash: 'sha256',
|
|
65
68
|
};
|
|
66
69
|
if (auth === 'agent') {
|
|
@@ -83,8 +86,38 @@ function buildConnectConfig(spec) {
|
|
|
83
86
|
// 大量服务端(如部分路由器/堡垒机)只开 keyboard-interactive
|
|
84
87
|
base.tryKeyboard = true;
|
|
85
88
|
}
|
|
89
|
+
// agent forwarding 需要 agent 通道:key/password 认证时也把 SSH_AUTH_SOCK
|
|
90
|
+
// 挂上(只用于转发,不参与认证)。@types/ssh2 的 ConnectConfig.agentForward
|
|
91
|
+
// 在缺 agent 时会直接 throw,故这里仅在 SOCK 存在时设置
|
|
92
|
+
if (spec.agentForward === true && process.env.SSH_AUTH_SOCK !== undefined && process.env.SSH_AUTH_SOCK !== '') {
|
|
93
|
+
base.agent = base.agent ?? process.env.SSH_AUTH_SOCK;
|
|
94
|
+
}
|
|
86
95
|
return base;
|
|
87
96
|
}
|
|
97
|
+
/** TOFU 主机指纹策略(hostVerifier 接线);返回的 mismatchMessage() 供连接错误路径取人类可读拒绝原因。 */
|
|
98
|
+
export function applyHostKeyPolicy(options) {
|
|
99
|
+
const { connectConfig, spec, store, logger, target } = options;
|
|
100
|
+
const port = spec.port ?? 22;
|
|
101
|
+
let hostKeyMismatch = null;
|
|
102
|
+
connectConfig.hostVerifier = (hash) => {
|
|
103
|
+
const known = store?.get(spec.host, port);
|
|
104
|
+
if (known === undefined) {
|
|
105
|
+
store?.record(spec.host, port, hash);
|
|
106
|
+
logger?.info(`[dsh-tty] ssh ${target} 首次连接,已记录 host key 指纹 sha256:${hash}(TOFU)`);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
if (known === hash) {
|
|
110
|
+
logger?.info(`[dsh-tty] ssh ${target} host key 指纹匹配(sha256:${hash})`);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
hostKeyMismatch =
|
|
114
|
+
`SSH 主机密钥指纹变更:${target} 已记录 sha256:${known},本次为 sha256:${hash}。` +
|
|
115
|
+
'可能是主机重装或换钥匙,也可能是中间人(MITM)冒充;确认安全后,到 设置 → 插件 → 终端面板 → SSH 主机密钥记录 删除该主机再重连。';
|
|
116
|
+
logger?.warn(`[dsh-tty] ${hostKeyMismatch}`);
|
|
117
|
+
return false;
|
|
118
|
+
};
|
|
119
|
+
return { mismatchMessage: () => hostKeyMismatch };
|
|
120
|
+
}
|
|
88
121
|
/**
|
|
89
122
|
* 建立 SSH 连接并打开交互 shell channel,返回 TermHandle。
|
|
90
123
|
* 失败(连接超时/认证被拒/host 不可达)时 reject 带人类可读信息。
|
|
@@ -92,6 +125,10 @@ function buildConnectConfig(spec) {
|
|
|
92
125
|
export async function spawnSsh(spec, options) {
|
|
93
126
|
const target = sshTarget(spec);
|
|
94
127
|
const logger = options.logger;
|
|
128
|
+
// agent forwarding 预检:缺 SSH_AUTH_SOCK 时 ssh2 只会静默不转发,这里显式报错
|
|
129
|
+
if (spec.agentForward === true && (process.env.SSH_AUTH_SOCK === undefined || process.env.SSH_AUTH_SOCK === '')) {
|
|
130
|
+
throw new Error('agent forwarding 需要 SSH_AUTH_SOCK(本机未运行 ssh-agent 或变量未设置)');
|
|
131
|
+
}
|
|
95
132
|
const conn = new Client();
|
|
96
133
|
const output = new PassThrough();
|
|
97
134
|
let exitCode = null;
|
|
@@ -121,14 +158,13 @@ export async function spawnSsh(spec, options) {
|
|
|
121
158
|
};
|
|
122
159
|
// 认证配置可能抛错(keyPath 读不到 / env 变量缺失)——先构造再连
|
|
123
160
|
const connectConfig = buildConnectConfig(spec);
|
|
124
|
-
|
|
125
|
-
logger?.info(`[dsh-tty] ssh ${target} host key sha256:${hash}(accept-and-log 放行)`);
|
|
126
|
-
return true;
|
|
127
|
-
};
|
|
161
|
+
const policy = applyHostKeyPolicy({ connectConfig, spec, store: options.hostKeyStore, logger, target });
|
|
128
162
|
const channel = await new Promise((resolve, reject) => {
|
|
129
163
|
let settled = false;
|
|
130
164
|
conn.on('ready', () => {
|
|
131
|
-
|
|
165
|
+
// agentForward 走 per-channel 请求(@types/ssh2 的 ShellOptions 未声明,
|
|
166
|
+
// 运行时支持;仅在本机 agent 存在时生效,见 buildConnectConfig)
|
|
167
|
+
conn.shell({ term: options.term, cols: options.cols, rows: options.rows, agentForward: spec.agentForward === true }, (error, ch) => {
|
|
132
168
|
settled = true;
|
|
133
169
|
if (error !== undefined && error !== null) {
|
|
134
170
|
conn.end();
|
|
@@ -141,7 +177,11 @@ export async function spawnSsh(spec, options) {
|
|
|
141
177
|
conn.on('error', (error) => {
|
|
142
178
|
if (!settled) {
|
|
143
179
|
settled = true;
|
|
144
|
-
|
|
180
|
+
const mismatch = policy.mismatchMessage();
|
|
181
|
+
if (mismatch !== null)
|
|
182
|
+
reject(new Error(mismatch));
|
|
183
|
+
else
|
|
184
|
+
reject(new Error(`SSH 连接失败(${target}): ${error.message}`));
|
|
145
185
|
}
|
|
146
186
|
else {
|
|
147
187
|
logger?.warn(`[dsh-tty] ssh ${target} 连接错误: ${error.message}`);
|
package/lib/ssh.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssh.js","sourceRoot":"","sources":["../src/ssh.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"ssh.js","sourceRoot":"","sources":["../src/ssh.ts"],"names":[],"mappings":"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"}
|
package/lib/tunnels.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { HostKeyStore, SshHostEntry } from './ssh.js';
|
|
2
|
+
/** 隧道规格(settings 存储;bookName 引用连接簿条目提供主机与认证)。 */
|
|
3
|
+
export interface TunnelSpec {
|
|
4
|
+
name: string;
|
|
5
|
+
bookName: string;
|
|
6
|
+
/** local = -L(本地监听 → 服务端侧拨号);remote = -R(服务端监听 → 本地拨号) */
|
|
7
|
+
direction: 'local' | 'remote';
|
|
8
|
+
/** local:本地监听端口 */
|
|
9
|
+
localPort?: number;
|
|
10
|
+
/** local:服务端侧拨号目标主机;remote:服务端监听地址(缺省 127.0.0.1) */
|
|
11
|
+
remoteHost?: string;
|
|
12
|
+
/** local:服务端侧目标端口;remote:服务端监听端口 */
|
|
13
|
+
remotePort?: number;
|
|
14
|
+
/** remote:本地拨号目标主机(缺省 127.0.0.1) */
|
|
15
|
+
localTargetHost?: string;
|
|
16
|
+
/** remote:本地拨号目标端口 */
|
|
17
|
+
localTargetPort?: number;
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
}
|
|
20
|
+
export type TunnelState = 'connecting' | 'active' | 'error' | 'stopped';
|
|
21
|
+
export interface TunnelStatus {
|
|
22
|
+
name: string;
|
|
23
|
+
bookName: string;
|
|
24
|
+
direction: 'local' | 'remote';
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
state: TunnelState;
|
|
27
|
+
error: string | null;
|
|
28
|
+
/** 规则的人类可读形式:`本机:5432 → db:5432` / `远程:8080 → 本机:3000` */
|
|
29
|
+
rule: string;
|
|
30
|
+
/** 当前活跃连接数 */
|
|
31
|
+
connections: number;
|
|
32
|
+
totalConnections: number;
|
|
33
|
+
/** 最近一次 forwardOut/转发失败原因(隧道本身 active 但目标拨号失败时可见) */
|
|
34
|
+
lastForwardError: string | null;
|
|
35
|
+
}
|
|
36
|
+
export interface TunnelLogger {
|
|
37
|
+
info(msg: string): void;
|
|
38
|
+
warn(msg: string): void;
|
|
39
|
+
}
|
|
40
|
+
export declare class TunnelManager {
|
|
41
|
+
private readonly logger;
|
|
42
|
+
private readonly store;
|
|
43
|
+
/** 按名字解析连接簿条目(实时读取,重连自动用最新凭证) */
|
|
44
|
+
private readonly resolveBook;
|
|
45
|
+
private readonly tunnels;
|
|
46
|
+
constructor(logger: TunnelLogger, store: HostKeyStore,
|
|
47
|
+
/** 按名字解析连接簿条目(实时读取,重连自动用最新凭证) */
|
|
48
|
+
resolveBook: (bookName: string) => SshHostEntry | undefined);
|
|
49
|
+
/** 按配置对齐运行态:新增/删除/规格变更重建,启停切换资源。幂等。 */
|
|
50
|
+
reconcile(specs: TunnelSpec[]): void;
|
|
51
|
+
list(): TunnelStatus[];
|
|
52
|
+
disposeAll(): void;
|
|
53
|
+
/** ------------------------------------------------------------------ */
|
|
54
|
+
private startTunnel;
|
|
55
|
+
private stopTunnel;
|
|
56
|
+
private connectTunnel;
|
|
57
|
+
/** remote 方向:让服务端监听端口(重连后必须重新调用,断线即失效)。 */
|
|
58
|
+
private bindRemoteListen;
|
|
59
|
+
private onRemoteConnection;
|
|
60
|
+
private onLocalConnection;
|
|
61
|
+
/** 失败且不再自动重试(需要人工介入:如远程端口被占、本地监听端口非法/无权限)。 */
|
|
62
|
+
private failTunnel;
|
|
63
|
+
/** 失败后按指数退避重连(1s→15s 封顶);重连期间保持 error 态供 UI 展示原因。 */
|
|
64
|
+
private scheduleRetry;
|
|
65
|
+
}
|