@judaharagao/opencode-ssh 1.1.0 → 1.2.1
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 +27 -5
- package/dist/config/schema.d.ts +16 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +12 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/ssh/config.d.ts +15 -0
- package/dist/ssh/config.d.ts.map +1 -1
- package/dist/ssh/config.js +39 -2
- package/dist/ssh/config.js.map +1 -1
- package/dist/ssh/connection.d.ts +25 -6
- package/dist/ssh/connection.d.ts.map +1 -1
- package/dist/ssh/connection.js +108 -46
- package/dist/ssh/connection.js.map +1 -1
- package/dist/ssh/known_hosts.d.ts +25 -0
- package/dist/ssh/known_hosts.d.ts.map +1 -0
- package/dist/ssh/known_hosts.js +160 -0
- package/dist/ssh/known_hosts.js.map +1 -0
- package/dist/ssh/proxy.d.ts +30 -0
- package/dist/ssh/proxy.d.ts.map +1 -0
- package/dist/ssh/proxy.js +133 -0
- package/dist/ssh/proxy.js.map +1 -0
- package/dist/ssh/ratelimit.d.ts +32 -0
- package/dist/ssh/ratelimit.d.ts.map +1 -0
- package/dist/ssh/ratelimit.js +63 -0
- package/dist/ssh/ratelimit.js.map +1 -0
- package/dist/tools.d.ts +7 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +118 -36
- package/dist/tools.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -12,6 +12,11 @@ SSH access plugin for OpenCode — secure remote command execution with command
|
|
|
12
12
|
- 📤 **File Transfer** — Upload and download files via SCP/SFTP
|
|
13
13
|
- 🎯 **Command Safety Checker** — Preview command safety before execution
|
|
14
14
|
- ⚙️ **Configurable Policies** — Add custom blocklist/allowlist patterns
|
|
15
|
+
- 🛤️ **Proxy Support** — `ProxyJump` and `ProxyCommand` from your ssh config
|
|
16
|
+
- 🛡️ **Host Key Verification** — optional `strict_host_key` MITM protection via `known_hosts`
|
|
17
|
+
- ⏳ **Rate Limiting** — per-host command rate limit and cooldown
|
|
18
|
+
- 🔁 **Auto-Reconnect** — dropped sessions reconnect automatically on the next command
|
|
19
|
+
- 🔐 **Credential Hygiene** — passwords are cleared from memory after a successful handshake
|
|
15
20
|
|
|
16
21
|
## Installation
|
|
17
22
|
|
|
@@ -48,15 +53,21 @@ Or with configuration:
|
|
|
48
53
|
| `audit_enabled` | boolean | `true` | Enable audit logging |
|
|
49
54
|
| `blocklist_extra` | string[] | `[]` | Additional regex patterns to block |
|
|
50
55
|
| `allowlist` | string[] | `[]` | Additional allowed patterns (restricted mode) |
|
|
51
|
-
| `ssh_config_path` | string | `~/.ssh/config` | Path to the SSH config file used to resolve host aliases, defaults, and auto-connect targets |
|
|
52
|
-
| `auto_connect` | boolean | `false` | Connect automatically at startup to every `Host` entry in the ssh config that has a `HostName` |
|
|
56
|
+
| `ssh_config_path` | string | `~/.ssh/config` | Path to the SSH config file used to resolve host aliases, defaults, proxies, and auto-connect targets |
|
|
57
|
+
| `auto_connect` | boolean | `false` | Connect automatically at startup to every `Host` entry in the ssh config that has a `HostName` (with retry/backoff) |
|
|
58
|
+
| `auto_reconnect` | boolean | `true` | Reconnect a dropped session automatically before the next `ssh.exec`/`ssh.upload`/`ssh.download` (key-authenticated sessions) |
|
|
59
|
+
| `strict_host_key` | boolean | `false` | Reject connections whose host key is not an exact match in `~/.ssh/known_hosts` (MITM protection) |
|
|
60
|
+
| `rate_limit_per_minute` | number | `120` | Max commands allowed per host per minute |
|
|
61
|
+
| `cooldown_seconds` | number | `0` | Minimum delay (seconds) between commands on the same host |
|
|
53
62
|
|
|
54
63
|
## SSH Config Integration
|
|
55
64
|
|
|
56
65
|
The plugin can reuse your existing `~/.ssh/config` (or a custom path via `ssh_config_path`):
|
|
57
66
|
|
|
58
67
|
- **Host resolution** — `ssh.connect(host="myalias")` resolves the alias to its `HostName`, and automatically applies the `User`, `Port` and `IdentityFile` from the config. `username`, `port` and `auth_method`/`key_path` become optional when configured.
|
|
59
|
-
- **
|
|
68
|
+
- **ProxyJump / ProxyCommand** — `ProxyJump` and `ProxyCommand` directives in the ssh config are honored, so connections route through bastion/jump hosts transparently.
|
|
69
|
+
- **Auto-connect** — with `"auto_connect": true`, the plugin connects at startup to every `Host` entry that has a `HostName`, using the configured key (or `~/.ssh/id_rsa`). Unreachable hosts are retried with backoff and skipped without blocking startup.
|
|
70
|
+
- **Missing file alert** — if `ssh_config_path` points to a file that does not exist, the plugin warns on connect instead of silently ignoring the setting.
|
|
60
71
|
|
|
61
72
|
Example ssh config:
|
|
62
73
|
|
|
@@ -66,9 +77,13 @@ Host prod-web
|
|
|
66
77
|
User deploy
|
|
67
78
|
Port 2222
|
|
68
79
|
IdentityFile ~/.ssh/prod_key
|
|
80
|
+
|
|
81
|
+
Host prod-db
|
|
82
|
+
HostName 10.0.0.50
|
|
83
|
+
ProxyJump jumpuser@bastion:2200
|
|
69
84
|
```
|
|
70
85
|
|
|
71
|
-
With `ssh.connect(host="prod-web")` the plugin connects to `deploy@10.0.0.10:2222` using `~/.ssh/prod_key`.
|
|
86
|
+
With `ssh.connect(host="prod-web")` the plugin connects to `deploy@10.0.0.10:2222` using `~/.ssh/prod_key`. `prod-db` is reached tunneled through `bastion`.
|
|
72
87
|
|
|
73
88
|
## Security Modes
|
|
74
89
|
|
|
@@ -157,6 +172,12 @@ ssh.audit_log(session_id="prod-server", command_filter="docker")
|
|
|
157
172
|
|
|
158
173
|
## Security
|
|
159
174
|
|
|
175
|
+
### Host Key Verification
|
|
176
|
+
With `"strict_host_key": true` the plugin verifies the remote server's host key against `~/.ssh/known_hosts` before completing the handshake:
|
|
177
|
+
- a **matching** key → connection allowed;
|
|
178
|
+
- a **changed** key (possible MITM) → connection refused;
|
|
179
|
+
- an **unknown** host → connection refused (add the host key to `known_hosts` first, e.g. via `ssh-keyscan host >> ~/.ssh/known_hosts`).
|
|
180
|
+
|
|
160
181
|
### Destructive Commands (100% Blocked)
|
|
161
182
|
These commands are **permanently blocked** with no exceptions:
|
|
162
183
|
|
|
@@ -183,7 +204,8 @@ These commands require your explicit approval each time:
|
|
|
183
204
|
### Credential Safety
|
|
184
205
|
- Passwords and SSH keys are **never** stored in logs or output
|
|
185
206
|
- Session info only contains host, username, and port
|
|
186
|
-
-
|
|
207
|
+
- Passwords are cleared from memory right after a successful handshake
|
|
208
|
+
- For this reason password-authenticated sessions do **not** auto-reconnect (re-issue `ssh.connect`); key-authenticated sessions reconnect automatically from the on-disk key
|
|
187
209
|
- All sessions are destroyed when the plugin is disposed
|
|
188
210
|
|
|
189
211
|
## Audit Log
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -27,6 +27,14 @@ export declare const SshPluginConfig: z.ZodObject<{
|
|
|
27
27
|
ssh_config_path: z.ZodOptional<z.ZodString>;
|
|
28
28
|
/** Auto-connect on plugin boot (requires saved session) */
|
|
29
29
|
auto_connect: z.ZodDefault<z.ZodBoolean>;
|
|
30
|
+
/** Reconnect a dropped session automatically on the next command */
|
|
31
|
+
auto_reconnect: z.ZodDefault<z.ZodBoolean>;
|
|
32
|
+
/** Reject hosts whose key is not in known_hosts (MITM protection) */
|
|
33
|
+
strict_host_key: z.ZodDefault<z.ZodBoolean>;
|
|
34
|
+
/** Max commands allowed per host per minute (rate limiting) */
|
|
35
|
+
rate_limit_per_minute: z.ZodDefault<z.ZodNumber>;
|
|
36
|
+
/** Minimum delay in seconds between commands on the same host (cooldown) */
|
|
37
|
+
cooldown_seconds: z.ZodDefault<z.ZodNumber>;
|
|
30
38
|
}, "strict", z.ZodTypeAny, {
|
|
31
39
|
mode: "full" | "restricted" | "read_only";
|
|
32
40
|
max_sessions: number;
|
|
@@ -35,6 +43,10 @@ export declare const SshPluginConfig: z.ZodObject<{
|
|
|
35
43
|
blocklist_extra: string[];
|
|
36
44
|
allowlist: string[];
|
|
37
45
|
auto_connect: boolean;
|
|
46
|
+
auto_reconnect: boolean;
|
|
47
|
+
strict_host_key: boolean;
|
|
48
|
+
rate_limit_per_minute: number;
|
|
49
|
+
cooldown_seconds: number;
|
|
38
50
|
ssh_config_path?: string | undefined;
|
|
39
51
|
}, {
|
|
40
52
|
mode?: "full" | "restricted" | "read_only" | undefined;
|
|
@@ -45,6 +57,10 @@ export declare const SshPluginConfig: z.ZodObject<{
|
|
|
45
57
|
allowlist?: string[] | undefined;
|
|
46
58
|
ssh_config_path?: string | undefined;
|
|
47
59
|
auto_connect?: boolean | undefined;
|
|
60
|
+
auto_reconnect?: boolean | undefined;
|
|
61
|
+
strict_host_key?: boolean | undefined;
|
|
62
|
+
rate_limit_per_minute?: number | undefined;
|
|
63
|
+
cooldown_seconds?: number | undefined;
|
|
48
64
|
}>;
|
|
49
65
|
export type SshPluginConfigType = z.infer<typeof SshPluginConfig>;
|
|
50
66
|
export declare const DEFAULT_CONFIG: SshPluginConfigType;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB;;;;;GAKG;AACH,eAAO,MAAM,YAAY,gDAA8C,CAAA;AACvE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAA;AAEvD;;GAEG;AACH,eAAO,MAAM,eAAe;IAExB,0CAA0C;;IAG1C,sCAAsC;;IAGtC,yCAAyC;;IAGzC,sCAAsC;;IAGtC,2DAA2D;;IAG3D,qEAAqE;;IAGrE,8CAA8C;;IAG9C,2DAA2D
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB;;;;;GAKG;AACH,eAAO,MAAM,YAAY,gDAA8C,CAAA;AACvE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAA;AAEvD;;GAEG;AACH,eAAO,MAAM,eAAe;IAExB,0CAA0C;;IAG1C,sCAAsC;;IAGtC,yCAAyC;;IAGzC,sCAAsC;;IAGtC,2DAA2D;;IAG3D,qEAAqE;;IAGrE,8CAA8C;;IAG9C,2DAA2D;;IAG3D,oEAAoE;;IAGpE,qEAAqE;;IAGrE,+DAA+D;;IAG/D,4EAA4E;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAGrE,CAAA;AAEX,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAA;AAEjE,eAAO,MAAM,cAAc,EAAE,mBAY5B,CAAA"}
|
package/dist/config/schema.js
CHANGED
|
@@ -27,6 +27,14 @@ export const SshPluginConfig = z
|
|
|
27
27
|
ssh_config_path: z.string().optional(),
|
|
28
28
|
/** Auto-connect on plugin boot (requires saved session) */
|
|
29
29
|
auto_connect: z.boolean().default(false),
|
|
30
|
+
/** Reconnect a dropped session automatically on the next command */
|
|
31
|
+
auto_reconnect: z.boolean().default(true),
|
|
32
|
+
/** Reject hosts whose key is not in known_hosts (MITM protection) */
|
|
33
|
+
strict_host_key: z.boolean().default(false),
|
|
34
|
+
/** Max commands allowed per host per minute (rate limiting) */
|
|
35
|
+
rate_limit_per_minute: z.number().int().min(1).max(10000).default(120),
|
|
36
|
+
/** Minimum delay in seconds between commands on the same host (cooldown) */
|
|
37
|
+
cooldown_seconds: z.number().int().min(0).max(3600).default(0),
|
|
30
38
|
})
|
|
31
39
|
.strict();
|
|
32
40
|
export const DEFAULT_CONFIG = {
|
|
@@ -37,5 +45,9 @@ export const DEFAULT_CONFIG = {
|
|
|
37
45
|
blocklist_extra: [],
|
|
38
46
|
allowlist: [],
|
|
39
47
|
auto_connect: false,
|
|
48
|
+
auto_reconnect: true,
|
|
49
|
+
strict_host_key: false,
|
|
50
|
+
rate_limit_per_minute: 120,
|
|
51
|
+
cooldown_seconds: 0,
|
|
40
52
|
};
|
|
41
53
|
//# sourceMappingURL=schema.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,YAAY,EAAE,WAAW,CAAC,CAAC,CAAA;AAGvE;;GAEG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC;KAC7B,MAAM,CAAC;IACN,0CAA0C;IAC1C,IAAI,EAAE,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC;IAElC,sCAAsC;IACtC,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAExD,yCAAyC;IACzC,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAE7D,sCAAsC;IACtC,aAAa,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IAExC,2DAA2D;IAC3D,eAAe,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAEhD,qEAAqE;IACrE,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAE1C,8CAA8C;IAC9C,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAEtC,2DAA2D;IAC3D,YAAY,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,YAAY,EAAE,WAAW,CAAC,CAAC,CAAA;AAGvE;;GAEG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC;KAC7B,MAAM,CAAC;IACN,0CAA0C;IAC1C,IAAI,EAAE,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC;IAElC,sCAAsC;IACtC,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAExD,yCAAyC;IACzC,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAE7D,sCAAsC;IACtC,aAAa,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IAExC,2DAA2D;IAC3D,eAAe,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAEhD,qEAAqE;IACrE,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAE1C,8CAA8C;IAC9C,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAEtC,2DAA2D;IAC3D,YAAY,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IAExC,oEAAoE;IACpE,cAAc,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IAEzC,qEAAqE;IACrE,eAAe,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IAE3C,+DAA+D;IAC/D,qBAAqB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC;IAEtE,4EAA4E;IAC5E,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;CAC/D,CAAC;KACD,MAAM,EAAE,CAAA;AAIX,MAAM,CAAC,MAAM,cAAc,GAAwB;IACjD,IAAI,EAAE,MAAM;IACZ,YAAY,EAAE,CAAC;IACf,eAAe,EAAE,EAAE;IACnB,aAAa,EAAE,IAAI;IACnB,eAAe,EAAE,EAAE;IACnB,SAAS,EAAE,EAAE;IACb,YAAY,EAAE,KAAK;IACnB,cAAc,EAAE,IAAI;IACpB,eAAe,EAAE,KAAK;IACtB,qBAAqB,EAAE,GAAG;IAC1B,gBAAgB,EAAE,CAAC;CACpB,CAAA"}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;;;;;AAiCjD,wBAGC"}
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,12 @@ const SshPlugin = async (_ctx, options) => {
|
|
|
5
5
|
// ── Resolve configuration ──
|
|
6
6
|
const config = resolveConfig(options);
|
|
7
7
|
// ── Create tools and hooks with resolved config ──
|
|
8
|
-
const tools = createSshTools(config.mode, config.max_sessions, config.default_timeout, config.blocklist_extra, config.ssh_config_path, config.auto_connect
|
|
8
|
+
const tools = createSshTools(config.mode, config.max_sessions, config.default_timeout, config.blocklist_extra, config.ssh_config_path, config.auto_connect, {
|
|
9
|
+
strictHostKey: config.strict_host_key,
|
|
10
|
+
rateLimitPerMinute: config.rate_limit_per_minute,
|
|
11
|
+
cooldownSeconds: config.cooldown_seconds,
|
|
12
|
+
autoReconnect: config.auto_reconnect,
|
|
13
|
+
});
|
|
9
14
|
const hooks = createSshHooks(config.mode, config.blocklist_extra);
|
|
10
15
|
return {
|
|
11
16
|
tool: tools,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAGpD,MAAM,SAAS,GAAW,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;IAChD,8BAA8B;IAC9B,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAGpD,MAAM,SAAS,GAAW,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;IAChD,8BAA8B;IAC9B,MAAM,MAAM,GAAwB,aAAa,CAAC,OAAkC,CAAC,CAAA;IAErF,oDAAoD;IACpD,MAAM,KAAK,GAAG,cAAc,CAC1B,MAAM,CAAC,IAAI,EACX,MAAM,CAAC,YAAY,EACnB,MAAM,CAAC,eAAe,EACtB,MAAM,CAAC,eAAe,EACtB,MAAM,CAAC,eAAe,EACtB,MAAM,CAAC,YAAY,EACnB;QACE,aAAa,EAAE,MAAM,CAAC,eAAe;QACrC,kBAAkB,EAAE,MAAM,CAAC,qBAAqB;QAChD,eAAe,EAAE,MAAM,CAAC,gBAAgB;QACxC,aAAa,EAAE,MAAM,CAAC,cAAc;KACrC,CACF,CAAA;IACD,MAAM,KAAK,GAAG,cAAc,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,eAAe,CAAC,CAAA;IAEjE,OAAO;QACL,IAAI,EAAE,KAAK;QACX,GAAG,KAAK;KACT,CAAA;AACH,CAAC,CAAA;AAED,eAAe;IACb,EAAE,EAAE,cAAc;IAClB,MAAM,EAAE,SAAS;CAClB,CAAA"}
|
package/dist/ssh/config.d.ts
CHANGED
|
@@ -9,10 +9,16 @@ export interface SshConfigEntry {
|
|
|
9
9
|
port?: number;
|
|
10
10
|
/** Identity file (IdentityFile). */
|
|
11
11
|
identityFile?: string;
|
|
12
|
+
/** ProxyJump: comma-separated list of user@host:port hops. */
|
|
13
|
+
proxyJump?: string;
|
|
14
|
+
/** ProxyCommand: external command to open the tunnel. */
|
|
15
|
+
proxyCommand?: string;
|
|
12
16
|
}
|
|
13
17
|
export interface LoadedSshConfig {
|
|
14
18
|
entries: SshConfigEntry[];
|
|
15
19
|
path: string;
|
|
20
|
+
/** Whether the configured ssh config file exists on disk. */
|
|
21
|
+
exists: boolean;
|
|
16
22
|
}
|
|
17
23
|
/** Expand a leading `~` to the current user's home directory. */
|
|
18
24
|
export declare function expandHomePath(p: string): string;
|
|
@@ -31,6 +37,15 @@ export declare function parseSshConfig(content: string): SshConfigEntry[];
|
|
|
31
37
|
export declare function findHostConfig(entries: SshConfigEntry[], host: string): SshConfigEntry | undefined;
|
|
32
38
|
/** Load and parse the ssh config file. Empty entry list if it does not exist. */
|
|
33
39
|
export declare function loadSshConfig(configuredPath?: string): LoadedSshConfig;
|
|
40
|
+
/**
|
|
41
|
+
* Parse a ProxyJump value ("user@host:port,user2@host2").
|
|
42
|
+
*/
|
|
43
|
+
export declare function parseProxyJump(value: string, defaultUser?: string, defaultKeyPath?: string): Array<{
|
|
44
|
+
host: string;
|
|
45
|
+
port: number;
|
|
46
|
+
username?: string;
|
|
47
|
+
keyPath?: string;
|
|
48
|
+
}>;
|
|
34
49
|
/**
|
|
35
50
|
* Concrete machines suitable for auto-connecting: `Host` entries that
|
|
36
51
|
* resolve to a real `HostName`.
|
package/dist/ssh/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/ssh/config.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mBAAmB;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,mBAAmB;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,oCAAoC;IACpC,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB,IAAI,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/ssh/config.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mBAAmB;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,mBAAmB;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,oCAAoC;IACpC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,yDAAyD;IACzD,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,6DAA6D;IAC7D,MAAM,EAAE,OAAO,CAAA;CAChB;AAED,iEAAiE;AACjE,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAKhD;AAED,6EAA6E;AAC7E,wBAAgB,oBAAoB,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,CAMpE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CA0DhE;AAaD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,cAAc,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAelG;AAED,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,eAAe,CAItE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,WAAW,CAAC,EAAE,MAAM,EACpB,cAAc,CAAC,EAAE,MAAM,GACtB,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA2B5E;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,cAAc,CAAC,EAAE,MAAM,GACtB,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAiBjG"}
|
package/dist/ssh/config.js
CHANGED
|
@@ -68,6 +68,12 @@ export function parseSshConfig(content) {
|
|
|
68
68
|
case "identityfile":
|
|
69
69
|
current.identityFile = value;
|
|
70
70
|
break;
|
|
71
|
+
case "proxyjump":
|
|
72
|
+
current.proxyJump = value;
|
|
73
|
+
break;
|
|
74
|
+
case "proxycommand":
|
|
75
|
+
current.proxyCommand = value;
|
|
76
|
+
break;
|
|
71
77
|
}
|
|
72
78
|
}
|
|
73
79
|
return entries;
|
|
@@ -111,8 +117,39 @@ export function findHostConfig(entries, host) {
|
|
|
111
117
|
export function loadSshConfig(configuredPath) {
|
|
112
118
|
const path = resolveSshConfigPath(configuredPath);
|
|
113
119
|
if (!existsSync(path))
|
|
114
|
-
return { entries: [], path };
|
|
115
|
-
return { entries: parseSshConfig(readFileSync(path, "utf-8")), path };
|
|
120
|
+
return { entries: [], path, exists: false };
|
|
121
|
+
return { entries: parseSshConfig(readFileSync(path, "utf-8")), path, exists: true };
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Parse a ProxyJump value ("user@host:port,user2@host2").
|
|
125
|
+
*/
|
|
126
|
+
export function parseProxyJump(value, defaultUser, defaultKeyPath) {
|
|
127
|
+
return value
|
|
128
|
+
.split(",")
|
|
129
|
+
.map((hop) => hop.trim())
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
.map((hop) => {
|
|
132
|
+
let user;
|
|
133
|
+
let rest = hop;
|
|
134
|
+
if (hop.includes("@")) {
|
|
135
|
+
const at = hop.lastIndexOf("@");
|
|
136
|
+
user = hop.slice(0, at);
|
|
137
|
+
rest = hop.slice(at + 1);
|
|
138
|
+
}
|
|
139
|
+
let port = 22;
|
|
140
|
+
let host = rest;
|
|
141
|
+
const colon = rest.match(/^(.*):(\d+)$/);
|
|
142
|
+
if (colon) {
|
|
143
|
+
host = colon[1];
|
|
144
|
+
port = Number(colon[2]);
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
host,
|
|
148
|
+
port,
|
|
149
|
+
username: user || defaultUser,
|
|
150
|
+
keyPath: defaultKeyPath,
|
|
151
|
+
};
|
|
152
|
+
});
|
|
116
153
|
}
|
|
117
154
|
/**
|
|
118
155
|
* Concrete machines suitable for auto-connecting: `Host` entries that
|
package/dist/ssh/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/ssh/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,IAAI,CAAA;AAC7C,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/ssh/config.ts"],"names":[],"mappings":"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"}
|
package/dist/ssh/connection.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ProxySpec } from "./proxy.js";
|
|
1
2
|
export interface SshConnectionConfig {
|
|
2
3
|
host: string;
|
|
3
4
|
port: number;
|
|
@@ -11,6 +12,16 @@ export interface SshConnectionConfig {
|
|
|
11
12
|
timeout?: number;
|
|
12
13
|
/** Connection alias for display */
|
|
13
14
|
alias?: string;
|
|
15
|
+
/** Reject hosts whose key is not (an identical match) in known_hosts. */
|
|
16
|
+
strictHostKey?: boolean;
|
|
17
|
+
/** Custom path to a known_hosts file. */
|
|
18
|
+
knownHostsPath?: string;
|
|
19
|
+
/** SSH config default path, used to locate known_hosts next to it. */
|
|
20
|
+
sshConfigPath?: string;
|
|
21
|
+
/** ProxyJump / ProxyCommand support. */
|
|
22
|
+
proxy?: ProxySpec;
|
|
23
|
+
/** Automatically reconnect a dropped session on the next exec/sftp. */
|
|
24
|
+
autoReconnect?: boolean;
|
|
14
25
|
}
|
|
15
26
|
export interface SshConnectionInfo {
|
|
16
27
|
id: string;
|
|
@@ -26,21 +37,28 @@ export declare class SshConnection {
|
|
|
26
37
|
private _connected;
|
|
27
38
|
private _connectedAt?;
|
|
28
39
|
private _lastActivity?;
|
|
29
|
-
private
|
|
30
|
-
private
|
|
40
|
+
private _maxAttempts;
|
|
41
|
+
private _attempts;
|
|
42
|
+
private _proxyCleanup?;
|
|
43
|
+
private _hostKeyVerdict?;
|
|
31
44
|
constructor(id: string, config: SshConnectionConfig);
|
|
32
45
|
get id(): string;
|
|
33
46
|
get config(): SshConnectionConfig;
|
|
34
47
|
get connected(): boolean;
|
|
35
48
|
get connectedAt(): Date | undefined;
|
|
36
49
|
get lastActivity(): Date | undefined;
|
|
50
|
+
/** Result of the last known_hosts check (undefined when disabled). */
|
|
51
|
+
get hostKeyVerdict(): "ok" | "changed" | "unknown" | undefined;
|
|
37
52
|
/**
|
|
38
|
-
* Connect to the SSH server.
|
|
53
|
+
* Connect to the SSH server, with bounded retries.
|
|
39
54
|
*/
|
|
40
55
|
connect(): Promise<void>;
|
|
56
|
+
private connectWithBackoff;
|
|
57
|
+
private buildConnectConfig;
|
|
58
|
+
private openConnection;
|
|
41
59
|
/**
|
|
42
|
-
* Execute a command on the remote server.
|
|
43
|
-
*
|
|
60
|
+
* Execute a command on the remote server. Automatically reconnects a
|
|
61
|
+
* dropped session before running.
|
|
44
62
|
*/
|
|
45
63
|
exec(command: string, options?: {
|
|
46
64
|
cwd?: string;
|
|
@@ -59,7 +77,8 @@ export declare class SshConnection {
|
|
|
59
77
|
timeout?: number;
|
|
60
78
|
}): NodeJS.ReadableStream | null;
|
|
61
79
|
/**
|
|
62
|
-
* Get an SFTP session for file transfers.
|
|
80
|
+
* Get an SFTP session for file transfers. Automatically reconnects a
|
|
81
|
+
* dropped session before returning.
|
|
63
82
|
*/
|
|
64
83
|
sftp(): Promise<any>;
|
|
65
84
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/ssh/connection.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,UAAU,GAAG,KAAK,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,mCAAmC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/ssh/connection.ts"],"names":[],"mappings":"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"}
|
package/dist/ssh/connection.js
CHANGED
|
@@ -2,6 +2,9 @@ import { Client } from "ssh2";
|
|
|
2
2
|
import { readFileSync, existsSync } from "fs";
|
|
3
3
|
import { resolve as pathResolve } from "path";
|
|
4
4
|
import { escapeShellArg } from "./sanitizer.js";
|
|
5
|
+
import { verifyHostKey } from "./known_hosts.js";
|
|
6
|
+
import { prepareProxy } from "./proxy.js";
|
|
7
|
+
const DEFAULT_KNOWN_HOSTS = () => pathResolve(process.env.HOME || "", ".ssh", "known_hosts");
|
|
5
8
|
export class SshConnection {
|
|
6
9
|
client;
|
|
7
10
|
_config;
|
|
@@ -9,12 +12,15 @@ export class SshConnection {
|
|
|
9
12
|
_connected = false;
|
|
10
13
|
_connectedAt;
|
|
11
14
|
_lastActivity;
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
_maxAttempts = 3;
|
|
16
|
+
_attempts = 0;
|
|
17
|
+
_proxyCleanup;
|
|
18
|
+
_hostKeyVerdict;
|
|
14
19
|
constructor(id, config) {
|
|
15
20
|
this._id = id;
|
|
16
21
|
this._config = config;
|
|
17
|
-
this.
|
|
22
|
+
this._config.strictHostKey = this._config.strictHostKey ?? false;
|
|
23
|
+
this._config.autoReconnect = this._config.autoReconnect ?? true;
|
|
18
24
|
}
|
|
19
25
|
get id() {
|
|
20
26
|
return this._id;
|
|
@@ -31,74 +37,124 @@ export class SshConnection {
|
|
|
31
37
|
get lastActivity() {
|
|
32
38
|
return this._lastActivity;
|
|
33
39
|
}
|
|
40
|
+
/** Result of the last known_hosts check (undefined when disabled). */
|
|
41
|
+
get hostKeyVerdict() {
|
|
42
|
+
return this._hostKeyVerdict;
|
|
43
|
+
}
|
|
34
44
|
/**
|
|
35
|
-
* Connect to the SSH server.
|
|
45
|
+
* Connect to the SSH server, with bounded retries.
|
|
36
46
|
*/
|
|
37
47
|
async connect() {
|
|
38
48
|
if (!this._config.username) {
|
|
39
49
|
throw new Error("A username is required to connect (provide one or set User in the ssh config).");
|
|
40
50
|
}
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
this._attempts = 0;
|
|
52
|
+
await this.connectWithBackoff();
|
|
53
|
+
// Credentials are only needed for the handshake; drop them from memory
|
|
54
|
+
// once the connection is established so they are not retained.
|
|
55
|
+
this._config.password = undefined;
|
|
56
|
+
}
|
|
57
|
+
async connectWithBackoff() {
|
|
58
|
+
try {
|
|
59
|
+
const cfg = await this.buildConnectConfig();
|
|
60
|
+
await this.openConnection(cfg);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
this._attempts++;
|
|
64
|
+
if (this._attempts < this._maxAttempts) {
|
|
65
|
+
await new Promise((r) => setTimeout(r, 1000 * this._attempts));
|
|
66
|
+
return this.connectWithBackoff();
|
|
67
|
+
}
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async buildConnectConfig() {
|
|
72
|
+
const connectConfig = {
|
|
73
|
+
host: this._config.host,
|
|
74
|
+
port: this._config.port,
|
|
75
|
+
username: this._config.username,
|
|
76
|
+
readyTimeout: this._config.timeout || 10000,
|
|
77
|
+
keepaliveInterval: 10000,
|
|
78
|
+
keepaliveCountMax: 3,
|
|
79
|
+
};
|
|
80
|
+
// ── Host key verification (MITM protection) ──
|
|
81
|
+
if (this._config.strictHostKey) {
|
|
82
|
+
const knownHostsPath = this._config.knownHostsPath || DEFAULT_KNOWN_HOSTS();
|
|
83
|
+
connectConfig.hostVerifier = (key) => {
|
|
84
|
+
const verdict = verifyHostKey(this._config.host, key, knownHostsPath, this._config.port);
|
|
85
|
+
this._hostKeyVerdict = verdict;
|
|
86
|
+
// A changed key is always rejected. An unknown key is only accepted
|
|
87
|
+
// in non-strict mode; here strict mode requires it to be present.
|
|
88
|
+
return verdict === "ok";
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// ── Proxy tunnel (ProxyJump / ProxyCommand) ──
|
|
92
|
+
if (this._config.proxy) {
|
|
93
|
+
const prepared = await prepareProxy(this._config.proxy, {
|
|
43
94
|
host: this._config.host,
|
|
44
95
|
port: this._config.port,
|
|
45
96
|
username: this._config.username,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
97
|
+
keyPath: this._config.keyPath,
|
|
98
|
+
passphrase: this._config.passphrase,
|
|
99
|
+
});
|
|
100
|
+
this._proxyCleanup = prepared.cleanup;
|
|
101
|
+
connectConfig.sock = prepared.sock;
|
|
102
|
+
}
|
|
103
|
+
// ── Authentication ──
|
|
104
|
+
if (this._config.authMethod === "password") {
|
|
105
|
+
if (this._config.password) {
|
|
52
106
|
connectConfig.password = this._config.password;
|
|
53
107
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
connectConfig.passphrase = this._config.passphrase;
|
|
66
|
-
}
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
const keyPath = this._config.keyPath
|
|
111
|
+
? pathResolve(this._config.keyPath.replace("~", process.env.HOME || ""))
|
|
112
|
+
: pathResolve(process.env.HOME || "", ".ssh", "id_rsa");
|
|
113
|
+
if (!existsSync(keyPath)) {
|
|
114
|
+
throw new Error(`SSH key not found: ${keyPath}`);
|
|
115
|
+
}
|
|
116
|
+
connectConfig.privateKey = readFileSync(keyPath, "utf-8");
|
|
117
|
+
if (this._config.passphrase) {
|
|
118
|
+
connectConfig.passphrase = this._config.passphrase;
|
|
67
119
|
}
|
|
68
|
-
|
|
120
|
+
}
|
|
121
|
+
return connectConfig;
|
|
122
|
+
}
|
|
123
|
+
async openConnection(cfg) {
|
|
124
|
+
// A fresh Client per attempt so a failed handshake never leaves the
|
|
125
|
+
// previous transport in an unusable state.
|
|
126
|
+
const client = new Client();
|
|
127
|
+
this.client = client;
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
client.on("ready", () => {
|
|
69
130
|
this._connected = true;
|
|
70
131
|
this._connectedAt = new Date();
|
|
71
132
|
this._lastActivity = new Date();
|
|
72
|
-
this._reconnectAttempts = 0;
|
|
73
133
|
resolve();
|
|
74
134
|
});
|
|
75
|
-
|
|
135
|
+
client.on("error", (err) => {
|
|
76
136
|
this._connected = false;
|
|
77
|
-
if (this._reconnectAttempts < this._maxReconnectAttempts) {
|
|
78
|
-
this._reconnectAttempts++;
|
|
79
|
-
// Attempt reconnect after delay
|
|
80
|
-
setTimeout(() => {
|
|
81
|
-
this.connect().catch(() => { });
|
|
82
|
-
}, 1000 * this._reconnectAttempts);
|
|
83
|
-
}
|
|
84
137
|
reject(err);
|
|
85
138
|
});
|
|
86
|
-
|
|
139
|
+
client.on("close", () => {
|
|
87
140
|
this._connected = false;
|
|
88
141
|
});
|
|
89
|
-
|
|
142
|
+
client.on("end", () => {
|
|
90
143
|
this._connected = false;
|
|
91
144
|
});
|
|
92
|
-
|
|
145
|
+
client.connect(cfg);
|
|
93
146
|
});
|
|
94
147
|
}
|
|
95
148
|
/**
|
|
96
|
-
* Execute a command on the remote server.
|
|
97
|
-
*
|
|
149
|
+
* Execute a command on the remote server. Automatically reconnects a
|
|
150
|
+
* dropped session before running.
|
|
98
151
|
*/
|
|
99
152
|
async exec(command, options) {
|
|
100
153
|
if (!this._connected) {
|
|
101
|
-
|
|
154
|
+
if (this._config.autoReconnect === false) {
|
|
155
|
+
throw new Error("SSH connection is not active");
|
|
156
|
+
}
|
|
157
|
+
await this.connect();
|
|
102
158
|
}
|
|
103
159
|
this._lastActivity = new Date();
|
|
104
160
|
const wrappedCommand = options?.cwd
|
|
@@ -115,7 +171,6 @@ export class SshConnection {
|
|
|
115
171
|
let stdout = "";
|
|
116
172
|
let stderr = "";
|
|
117
173
|
let killed = false;
|
|
118
|
-
// Set timeout
|
|
119
174
|
if (timeoutMs > 0) {
|
|
120
175
|
timeoutId = setTimeout(() => {
|
|
121
176
|
killed = true;
|
|
@@ -165,12 +220,17 @@ export class SshConnection {
|
|
|
165
220
|
return null; // Placeholder - actual streaming handled in executor.ts
|
|
166
221
|
}
|
|
167
222
|
/**
|
|
168
|
-
* Get an SFTP session for file transfers.
|
|
223
|
+
* Get an SFTP session for file transfers. Automatically reconnects a
|
|
224
|
+
* dropped session before returning.
|
|
169
225
|
*/
|
|
170
226
|
async sftp() {
|
|
171
227
|
if (!this._connected) {
|
|
172
|
-
|
|
228
|
+
if (this._config.autoReconnect === false) {
|
|
229
|
+
throw new Error("SSH connection is not active");
|
|
230
|
+
}
|
|
231
|
+
await this.connect();
|
|
173
232
|
}
|
|
233
|
+
this._lastActivity = new Date();
|
|
174
234
|
return new Promise((resolve, reject) => {
|
|
175
235
|
this.client.sftp((err, sftp) => {
|
|
176
236
|
if (err) {
|
|
@@ -185,10 +245,12 @@ export class SshConnection {
|
|
|
185
245
|
* Disconnect from the SSH server.
|
|
186
246
|
*/
|
|
187
247
|
disconnect() {
|
|
188
|
-
if (this.
|
|
248
|
+
if (this.client) {
|
|
189
249
|
this.client.end();
|
|
190
|
-
this._connected = false;
|
|
191
250
|
}
|
|
251
|
+
this._connected = false;
|
|
252
|
+
this._proxyCleanup?.();
|
|
253
|
+
this._proxyCleanup = undefined;
|
|
192
254
|
}
|
|
193
255
|
/**
|
|
194
256
|
* Get connection info for display.
|