@pyrokine/mcp-ssh 1.1.0 → 1.1.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.md +81 -72
- package/README_zh.md +81 -73
- package/dist/file-ops.js +36 -20
- package/dist/index.js +24 -9
- package/dist/session-manager.d.ts +62 -51
- package/dist/session-manager.js +201 -168
- package/dist/ssh-config.js +2 -2
- package/package.json +1 -1
- package/src/file-ops.ts +602 -577
- package/src/index.ts +971 -948
- package/src/session-manager.ts +986 -945
- package/src/ssh-config.ts +185 -185
- package/src/types.ts +89 -89
- package/tsconfig.json +7 -2
package/dist/session-manager.js
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
* - 自动重连
|
|
8
8
|
* - 会话持久化
|
|
9
9
|
*/
|
|
10
|
-
import
|
|
10
|
+
import xterm from '@xterm/headless';
|
|
11
11
|
import * as fs from 'fs';
|
|
12
|
+
import * as net from 'net';
|
|
12
13
|
import * as path from 'path';
|
|
13
|
-
import
|
|
14
|
+
import { Client } from 'ssh2';
|
|
14
15
|
const Terminal = xterm.Terminal;
|
|
15
|
-
import * as net from 'net';
|
|
16
16
|
export class SessionManager {
|
|
17
17
|
sessions = new Map();
|
|
18
18
|
ptySessions = new Map();
|
|
@@ -29,18 +29,6 @@ export class SessionManager {
|
|
|
29
29
|
this.persistPath = persistPath || path.join(process.env.HOME || '/tmp', '.ssh-mcp-pro', 'sessions.json');
|
|
30
30
|
this.ensurePersistDir();
|
|
31
31
|
}
|
|
32
|
-
ensurePersistDir() {
|
|
33
|
-
const dir = path.dirname(this.persistPath);
|
|
34
|
-
if (!fs.existsSync(dir)) {
|
|
35
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* 生成连接别名
|
|
40
|
-
*/
|
|
41
|
-
generateAlias(config) {
|
|
42
|
-
return config.alias || `${config.username}@${config.host}:${config.port}`;
|
|
43
|
-
}
|
|
44
32
|
/**
|
|
45
33
|
* 建立 SSH 连接
|
|
46
34
|
*/
|
|
@@ -114,7 +102,8 @@ export class SessionManager {
|
|
|
114
102
|
if (session.reconnectAttempts < this.maxReconnectAttempts) {
|
|
115
103
|
session.reconnectAttempts++;
|
|
116
104
|
setTimeout(() => {
|
|
117
|
-
this.reconnect(alias).catch(() => {
|
|
105
|
+
this.reconnect(alias).catch(() => {
|
|
106
|
+
});
|
|
118
107
|
}, 5000); // 5 秒后重连
|
|
119
108
|
}
|
|
120
109
|
}
|
|
@@ -122,25 +111,6 @@ export class SessionManager {
|
|
|
122
111
|
client.connect(connectConfig);
|
|
123
112
|
});
|
|
124
113
|
}
|
|
125
|
-
/**
|
|
126
|
-
* 通过跳板机转发连接
|
|
127
|
-
*/
|
|
128
|
-
forwardConnection(jumpClient, targetHost, targetPort) {
|
|
129
|
-
return new Promise((resolve, reject) => {
|
|
130
|
-
jumpClient.forwardOut('127.0.0.1', 0, targetHost, targetPort, (err, stream) => {
|
|
131
|
-
if (err)
|
|
132
|
-
reject(err);
|
|
133
|
-
else
|
|
134
|
-
resolve(stream);
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* 检查连接是否存活
|
|
140
|
-
*/
|
|
141
|
-
isAlive(session) {
|
|
142
|
-
return session.connected;
|
|
143
|
-
}
|
|
144
114
|
/**
|
|
145
115
|
* 重新连接
|
|
146
116
|
*/
|
|
@@ -152,7 +122,8 @@ export class SessionManager {
|
|
|
152
122
|
try {
|
|
153
123
|
session.client.end();
|
|
154
124
|
}
|
|
155
|
-
catch {
|
|
125
|
+
catch {
|
|
126
|
+
}
|
|
156
127
|
await this.connect(session.config);
|
|
157
128
|
}
|
|
158
129
|
/**
|
|
@@ -164,7 +135,8 @@ export class SessionManager {
|
|
|
164
135
|
try {
|
|
165
136
|
session.client.end();
|
|
166
137
|
}
|
|
167
|
-
catch {
|
|
138
|
+
catch {
|
|
139
|
+
}
|
|
168
140
|
this.sessions.delete(alias);
|
|
169
141
|
this.persistSessions();
|
|
170
142
|
return true;
|
|
@@ -212,12 +184,6 @@ export class SessionManager {
|
|
|
212
184
|
}
|
|
213
185
|
return result;
|
|
214
186
|
}
|
|
215
|
-
/**
|
|
216
|
-
* 转义 shell 参数(使用单引号方式)
|
|
217
|
-
*/
|
|
218
|
-
escapeShellArg(s) {
|
|
219
|
-
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
220
|
-
}
|
|
221
187
|
/**
|
|
222
188
|
* 执行命令
|
|
223
189
|
*/
|
|
@@ -274,8 +240,9 @@ export class SessionManager {
|
|
|
274
240
|
reject(new Error(`Command timed out after ${timeout}ms`));
|
|
275
241
|
}, timeout);
|
|
276
242
|
stream.on('close', (code) => {
|
|
277
|
-
if (timeoutId)
|
|
243
|
+
if (timeoutId) {
|
|
278
244
|
clearTimeout(timeoutId);
|
|
245
|
+
}
|
|
279
246
|
resolve({
|
|
280
247
|
success: code === 0,
|
|
281
248
|
stdout: stdoutTruncated ? stdout + '\n... [truncated]' : stdout,
|
|
@@ -311,28 +278,24 @@ export class SessionManager {
|
|
|
311
278
|
});
|
|
312
279
|
});
|
|
313
280
|
}
|
|
314
|
-
/**
|
|
315
|
-
* 校验用户名(只允许字母、数字、下划线、连字符)
|
|
316
|
-
*/
|
|
317
|
-
isValidUsername(username) {
|
|
318
|
-
return /^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(username);
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* 校验环境变量名(只允许字母、数字、下划线,不能以数字开头)
|
|
322
|
-
*/
|
|
323
|
-
isValidEnvKey(key) {
|
|
324
|
-
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key);
|
|
325
|
-
}
|
|
326
281
|
/**
|
|
327
282
|
* 以其他用户身份执行命令
|
|
283
|
+
* @param loadProfile 是否加载用户的 shell 配置(默认 true)。
|
|
284
|
+
* su -c 创建非交互式 shell,不会自动执行 rc 文件,
|
|
285
|
+
* 但大多数用户的环境变量设置在 rc 文件中,因此默认加载。
|
|
286
|
+
* 支持 bash(.bashrc)、zsh(.zshrc) 及其他 shell(.profile)。
|
|
328
287
|
*/
|
|
329
288
|
async execAsUser(alias, command, targetUser, options = {}) {
|
|
330
289
|
// 校验用户名防止注入
|
|
331
290
|
if (!this.isValidUsername(targetUser)) {
|
|
332
291
|
throw new Error(`Invalid username: ${targetUser}`);
|
|
333
292
|
}
|
|
334
|
-
const
|
|
335
|
-
|
|
293
|
+
const { loadProfile = true, ...execOpts } = options;
|
|
294
|
+
const wrappedCommand = loadProfile
|
|
295
|
+
? `${this.getLoadProfileCommand()}${command}`
|
|
296
|
+
: command;
|
|
297
|
+
const suCommand = `su - ${targetUser} -c ${this.escapeShellArg(wrappedCommand)}`;
|
|
298
|
+
return this.exec(alias, suCommand, execOpts);
|
|
336
299
|
}
|
|
337
300
|
/**
|
|
338
301
|
* 使用 sudo 执行命令
|
|
@@ -355,36 +318,15 @@ export class SessionManager {
|
|
|
355
318
|
const session = this.getSession(alias);
|
|
356
319
|
return new Promise((resolve, reject) => {
|
|
357
320
|
session.client.sftp((err, sftp) => {
|
|
358
|
-
if (err)
|
|
321
|
+
if (err) {
|
|
359
322
|
reject(err);
|
|
360
|
-
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
361
325
|
resolve(sftp);
|
|
326
|
+
}
|
|
362
327
|
});
|
|
363
328
|
});
|
|
364
329
|
}
|
|
365
|
-
/**
|
|
366
|
-
* 持久化会话信息
|
|
367
|
-
*/
|
|
368
|
-
persistSessions() {
|
|
369
|
-
const data = [];
|
|
370
|
-
for (const [alias, session] of this.sessions) {
|
|
371
|
-
// 不保存敏感信息(密码、密钥)
|
|
372
|
-
data.push({
|
|
373
|
-
alias,
|
|
374
|
-
host: session.config.host,
|
|
375
|
-
port: session.config.port || 22,
|
|
376
|
-
username: session.config.username,
|
|
377
|
-
connectedAt: session.connectedAt,
|
|
378
|
-
env: session.config.env,
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
try {
|
|
382
|
-
fs.writeFileSync(this.persistPath, JSON.stringify(data, null, 2));
|
|
383
|
-
}
|
|
384
|
-
catch (e) {
|
|
385
|
-
// 忽略写入错误
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
330
|
/**
|
|
389
331
|
* 加载持久化的会话信息(仅用于显示,不自动重连)
|
|
390
332
|
*/
|
|
@@ -394,16 +336,10 @@ export class SessionManager {
|
|
|
394
336
|
return JSON.parse(fs.readFileSync(this.persistPath, 'utf-8'));
|
|
395
337
|
}
|
|
396
338
|
}
|
|
397
|
-
catch {
|
|
339
|
+
catch {
|
|
340
|
+
}
|
|
398
341
|
return [];
|
|
399
342
|
}
|
|
400
|
-
// ========== PTY 会话管理 ==========
|
|
401
|
-
/**
|
|
402
|
-
* 生成 PTY 会话 ID
|
|
403
|
-
*/
|
|
404
|
-
generatePtyId() {
|
|
405
|
-
return `pty_${++this.ptyIdCounter}_${Date.now()}`;
|
|
406
|
-
}
|
|
407
343
|
/**
|
|
408
344
|
* 启动持久化 PTY 会话
|
|
409
345
|
*/
|
|
@@ -456,8 +392,9 @@ export class SessionManager {
|
|
|
456
392
|
};
|
|
457
393
|
// 监听输出数据
|
|
458
394
|
stream.on('data', (data) => {
|
|
459
|
-
if (!ptySession.active)
|
|
395
|
+
if (!ptySession.active) {
|
|
460
396
|
return;
|
|
397
|
+
}
|
|
461
398
|
const chunk = data.toString('utf-8');
|
|
462
399
|
// 写入终端仿真器(解析 ANSI 序列)
|
|
463
400
|
terminal.write(chunk);
|
|
@@ -492,24 +429,6 @@ export class SessionManager {
|
|
|
492
429
|
}
|
|
493
430
|
return ptySession.stream.write(data);
|
|
494
431
|
}
|
|
495
|
-
/**
|
|
496
|
-
* 从终端仿真器获取当前屏幕内容
|
|
497
|
-
*/
|
|
498
|
-
getScreenContent(terminal) {
|
|
499
|
-
const buffer = terminal.buffer.active;
|
|
500
|
-
const lines = [];
|
|
501
|
-
for (let i = 0; i < terminal.rows; i++) {
|
|
502
|
-
const line = buffer.getLine(i);
|
|
503
|
-
if (line) {
|
|
504
|
-
lines.push(line.translateToString(true));
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
// 移除尾部空行
|
|
508
|
-
while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
|
|
509
|
-
lines.pop();
|
|
510
|
-
}
|
|
511
|
-
return lines.join('\n');
|
|
512
|
-
}
|
|
513
432
|
/**
|
|
514
433
|
* 读取 PTY 输出
|
|
515
434
|
* @param mode 'screen' 返回当前屏幕内容,'raw' 返回原始 ANSI 流
|
|
@@ -571,11 +490,13 @@ export class SessionManager {
|
|
|
571
490
|
try {
|
|
572
491
|
ptySession.stream.close();
|
|
573
492
|
}
|
|
574
|
-
catch {
|
|
493
|
+
catch {
|
|
494
|
+
}
|
|
575
495
|
try {
|
|
576
496
|
ptySession.terminal.dispose();
|
|
577
497
|
}
|
|
578
|
-
catch {
|
|
498
|
+
catch {
|
|
499
|
+
}
|
|
579
500
|
ptySession.active = false;
|
|
580
501
|
this.ptySessions.delete(ptyId);
|
|
581
502
|
return true;
|
|
@@ -612,13 +533,6 @@ export class SessionManager {
|
|
|
612
533
|
}
|
|
613
534
|
return count;
|
|
614
535
|
}
|
|
615
|
-
// ========== 端口转发 ==========
|
|
616
|
-
/**
|
|
617
|
-
* 生成端口转发 ID
|
|
618
|
-
*/
|
|
619
|
-
generateForwardId() {
|
|
620
|
-
return `fwd_${++this.forwardIdCounter}_${Date.now()}`;
|
|
621
|
-
}
|
|
622
536
|
/**
|
|
623
537
|
* 创建本地端口转发
|
|
624
538
|
* 本地监听 localHost:localPort,转发到远程 remoteHost:remotePort
|
|
@@ -688,6 +602,174 @@ export class SessionManager {
|
|
|
688
602
|
});
|
|
689
603
|
});
|
|
690
604
|
}
|
|
605
|
+
// ========== PTY 会话管理 ==========
|
|
606
|
+
/**
|
|
607
|
+
* 关闭端口转发
|
|
608
|
+
*/
|
|
609
|
+
forwardClose(forwardId) {
|
|
610
|
+
const fwdSession = this.forwardSessions.get(forwardId);
|
|
611
|
+
if (!fwdSession) {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
fwdSession.active = false;
|
|
615
|
+
if (fwdSession.type === 'local' && fwdSession.server) {
|
|
616
|
+
try {
|
|
617
|
+
fwdSession.server.close();
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
else if (fwdSession.type === 'remote') {
|
|
623
|
+
const session = this.sessions.get(fwdSession.alias);
|
|
624
|
+
if (session) {
|
|
625
|
+
try {
|
|
626
|
+
session.client.unforwardIn(fwdSession.remoteHost, fwdSession.remotePort);
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
}
|
|
630
|
+
// 检查是否需要移除共享 dispatcher
|
|
631
|
+
this.removeTcpDispatcherIfEmpty(session, fwdSession.alias);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
this.forwardSessions.delete(forwardId);
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* 列出所有端口转发
|
|
639
|
+
*/
|
|
640
|
+
forwardList() {
|
|
641
|
+
const result = [];
|
|
642
|
+
for (const [id, fwd] of this.forwardSessions) {
|
|
643
|
+
result.push({
|
|
644
|
+
id,
|
|
645
|
+
alias: fwd.alias,
|
|
646
|
+
type: fwd.type,
|
|
647
|
+
localHost: fwd.localHost,
|
|
648
|
+
localPort: fwd.localPort,
|
|
649
|
+
remoteHost: fwd.remoteHost,
|
|
650
|
+
remotePort: fwd.remotePort,
|
|
651
|
+
createdAt: fwd.createdAt,
|
|
652
|
+
active: fwd.active,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
return result;
|
|
656
|
+
}
|
|
657
|
+
ensurePersistDir() {
|
|
658
|
+
const dir = path.dirname(this.persistPath);
|
|
659
|
+
if (!fs.existsSync(dir)) {
|
|
660
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* 生成连接别名
|
|
665
|
+
*/
|
|
666
|
+
generateAlias(config) {
|
|
667
|
+
return config.alias || `${config.username}@${config.host}:${config.port}`;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* 通过跳板机转发连接
|
|
671
|
+
*/
|
|
672
|
+
forwardConnection(jumpClient, targetHost, targetPort) {
|
|
673
|
+
return new Promise((resolve, reject) => {
|
|
674
|
+
jumpClient.forwardOut('127.0.0.1', 0, targetHost, targetPort, (err, stream) => {
|
|
675
|
+
if (err) {
|
|
676
|
+
reject(err);
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
resolve(stream);
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* 检查连接是否存活
|
|
686
|
+
*/
|
|
687
|
+
isAlive(session) {
|
|
688
|
+
return session.connected;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* 转义 shell 参数(使用单引号方式)
|
|
692
|
+
*/
|
|
693
|
+
escapeShellArg(s) {
|
|
694
|
+
return `'${s.replace(/'/g, '\'\\\'\'')}'`;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* 校验用户名(只允许字母、数字、下划线、连字符)
|
|
698
|
+
*/
|
|
699
|
+
isValidUsername(username) {
|
|
700
|
+
return /^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(username);
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* 校验环境变量名(只允许字母、数字、下划线,不能以数字开头)
|
|
704
|
+
*/
|
|
705
|
+
isValidEnvKey(key) {
|
|
706
|
+
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key);
|
|
707
|
+
}
|
|
708
|
+
// ========== 端口转发 ==========
|
|
709
|
+
/**
|
|
710
|
+
* 根据用户 shell 类型生成加载配置文件的命令
|
|
711
|
+
* bash → .bashrc, zsh → .zshrc, 其他 → .profile
|
|
712
|
+
*/
|
|
713
|
+
getLoadProfileCommand() {
|
|
714
|
+
return 'case "$(basename "$SHELL" 2>/dev/null)" in ' +
|
|
715
|
+
'bash) [ -f ~/.bashrc ] && . ~/.bashrc ;; ' +
|
|
716
|
+
'zsh) [ -f ~/.zshrc ] && . ~/.zshrc ;; ' +
|
|
717
|
+
'*) [ -f ~/.profile ] && . ~/.profile ;; ' +
|
|
718
|
+
'esac 2>/dev/null; ';
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* 持久化会话信息
|
|
722
|
+
*/
|
|
723
|
+
persistSessions() {
|
|
724
|
+
const data = [];
|
|
725
|
+
for (const [alias, session] of this.sessions) {
|
|
726
|
+
// 不保存敏感信息(密码、密钥)
|
|
727
|
+
data.push({
|
|
728
|
+
alias,
|
|
729
|
+
host: session.config.host,
|
|
730
|
+
port: session.config.port || 22,
|
|
731
|
+
username: session.config.username,
|
|
732
|
+
connectedAt: session.connectedAt,
|
|
733
|
+
env: session.config.env,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
try {
|
|
737
|
+
fs.writeFileSync(this.persistPath, JSON.stringify(data, null, 2));
|
|
738
|
+
}
|
|
739
|
+
catch (e) {
|
|
740
|
+
// 忽略写入错误
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* 生成 PTY 会话 ID
|
|
745
|
+
*/
|
|
746
|
+
generatePtyId() {
|
|
747
|
+
return `pty_${++this.ptyIdCounter}_${Date.now()}`;
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* 从终端仿真器获取当前屏幕内容
|
|
751
|
+
*/
|
|
752
|
+
getScreenContent(terminal) {
|
|
753
|
+
const buffer = terminal.buffer.active;
|
|
754
|
+
const lines = [];
|
|
755
|
+
for (let i = 0; i < terminal.rows; i++) {
|
|
756
|
+
const line = buffer.getLine(i);
|
|
757
|
+
if (line) {
|
|
758
|
+
lines.push(line.translateToString(true));
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// 移除尾部空行
|
|
762
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
|
|
763
|
+
lines.pop();
|
|
764
|
+
}
|
|
765
|
+
return lines.join('\n');
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* 生成端口转发 ID
|
|
769
|
+
*/
|
|
770
|
+
generateForwardId() {
|
|
771
|
+
return `fwd_${++this.forwardIdCounter}_${Date.now()}`;
|
|
772
|
+
}
|
|
691
773
|
/**
|
|
692
774
|
* 确保 SSH session 有共享的 tcp connection dispatcher
|
|
693
775
|
* 所有 remote forward 共用一个 dispatcher,根据 destIP/destPort 路由
|
|
@@ -738,55 +820,6 @@ export class SessionManager {
|
|
|
738
820
|
session.client.removeListener('tcp connection', session.tcpDispatcher);
|
|
739
821
|
session.tcpDispatcher = undefined;
|
|
740
822
|
}
|
|
741
|
-
/**
|
|
742
|
-
* 关闭端口转发
|
|
743
|
-
*/
|
|
744
|
-
forwardClose(forwardId) {
|
|
745
|
-
const fwdSession = this.forwardSessions.get(forwardId);
|
|
746
|
-
if (!fwdSession) {
|
|
747
|
-
return false;
|
|
748
|
-
}
|
|
749
|
-
fwdSession.active = false;
|
|
750
|
-
if (fwdSession.type === 'local' && fwdSession.server) {
|
|
751
|
-
try {
|
|
752
|
-
fwdSession.server.close();
|
|
753
|
-
}
|
|
754
|
-
catch { }
|
|
755
|
-
}
|
|
756
|
-
else if (fwdSession.type === 'remote') {
|
|
757
|
-
const session = this.sessions.get(fwdSession.alias);
|
|
758
|
-
if (session) {
|
|
759
|
-
try {
|
|
760
|
-
session.client.unforwardIn(fwdSession.remoteHost, fwdSession.remotePort);
|
|
761
|
-
}
|
|
762
|
-
catch { }
|
|
763
|
-
// 检查是否需要移除共享 dispatcher
|
|
764
|
-
this.removeTcpDispatcherIfEmpty(session, fwdSession.alias);
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
this.forwardSessions.delete(forwardId);
|
|
768
|
-
return true;
|
|
769
|
-
}
|
|
770
|
-
/**
|
|
771
|
-
* 列出所有端口转发
|
|
772
|
-
*/
|
|
773
|
-
forwardList() {
|
|
774
|
-
const result = [];
|
|
775
|
-
for (const [id, fwd] of this.forwardSessions) {
|
|
776
|
-
result.push({
|
|
777
|
-
id,
|
|
778
|
-
alias: fwd.alias,
|
|
779
|
-
type: fwd.type,
|
|
780
|
-
localHost: fwd.localHost,
|
|
781
|
-
localPort: fwd.localPort,
|
|
782
|
-
remoteHost: fwd.remoteHost,
|
|
783
|
-
remotePort: fwd.remotePort,
|
|
784
|
-
createdAt: fwd.createdAt,
|
|
785
|
-
active: fwd.active,
|
|
786
|
-
});
|
|
787
|
-
}
|
|
788
|
-
return result;
|
|
789
|
-
}
|
|
790
823
|
}
|
|
791
824
|
// 全局单例
|
|
792
825
|
export const sessionManager = new SessionManager();
|
package/dist/ssh-config.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* - ProxyJump 解析(支持 user@host:port 格式)
|
|
9
9
|
*/
|
|
10
10
|
import * as fs from 'fs';
|
|
11
|
-
import * as path from 'path';
|
|
12
11
|
import * as os from 'os';
|
|
12
|
+
import * as path from 'path';
|
|
13
13
|
/**
|
|
14
14
|
* 解析 host:port 或 [ipv6]:port
|
|
15
15
|
* 返回 { host, port },host 不含方括号
|
|
@@ -92,7 +92,7 @@ function stripInlineComment(value) {
|
|
|
92
92
|
let quoteChar = '';
|
|
93
93
|
for (let i = 0; i < value.length; i++) {
|
|
94
94
|
const ch = value[i];
|
|
95
|
-
if (!inQuote && (ch === '"' || ch ===
|
|
95
|
+
if (!inQuote && (ch === '"' || ch === '\'')) {
|
|
96
96
|
inQuote = true;
|
|
97
97
|
quoteChar = ch;
|
|
98
98
|
}
|