@judaharagao/opencode-ssh 1.0.0 → 1.2.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/README.md +44 -1
- 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 +32 -2
- package/dist/index.js.map +1 -1
- package/dist/ssh/config.d.ts +60 -0
- package/dist/ssh/config.d.ts.map +1 -0
- package/dist/ssh/config.js +174 -0
- package/dist/ssh/config.js.map +1 -0
- package/dist/ssh/connection.d.ts +27 -7
- package/dist/ssh/connection.d.ts.map +1 -1
- package/dist/ssh/connection.js +111 -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 +200 -55
- package/dist/tools.js.map +1 -1
- package/package.json +2 -2
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,71 +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
|
-
|
|
39
|
-
|
|
48
|
+
if (!this._config.username) {
|
|
49
|
+
throw new Error("A username is required to connect (provide one or set User in the ssh config).");
|
|
50
|
+
}
|
|
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, {
|
|
40
94
|
host: this._config.host,
|
|
41
95
|
port: this._config.port,
|
|
42
96
|
username: this._config.username,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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) {
|
|
49
106
|
connectConfig.password = this._config.password;
|
|
50
107
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
connectConfig.passphrase = this._config.passphrase;
|
|
63
|
-
}
|
|
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;
|
|
64
119
|
}
|
|
65
|
-
|
|
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", () => {
|
|
66
130
|
this._connected = true;
|
|
67
131
|
this._connectedAt = new Date();
|
|
68
132
|
this._lastActivity = new Date();
|
|
69
|
-
this._reconnectAttempts = 0;
|
|
70
133
|
resolve();
|
|
71
134
|
});
|
|
72
|
-
|
|
135
|
+
client.on("error", (err) => {
|
|
73
136
|
this._connected = false;
|
|
74
|
-
if (this._reconnectAttempts < this._maxReconnectAttempts) {
|
|
75
|
-
this._reconnectAttempts++;
|
|
76
|
-
// Attempt reconnect after delay
|
|
77
|
-
setTimeout(() => {
|
|
78
|
-
this.connect().catch(() => { });
|
|
79
|
-
}, 1000 * this._reconnectAttempts);
|
|
80
|
-
}
|
|
81
137
|
reject(err);
|
|
82
138
|
});
|
|
83
|
-
|
|
139
|
+
client.on("close", () => {
|
|
84
140
|
this._connected = false;
|
|
85
141
|
});
|
|
86
|
-
|
|
142
|
+
client.on("end", () => {
|
|
87
143
|
this._connected = false;
|
|
88
144
|
});
|
|
89
|
-
|
|
145
|
+
client.connect(cfg);
|
|
90
146
|
});
|
|
91
147
|
}
|
|
92
148
|
/**
|
|
93
|
-
* Execute a command on the remote server.
|
|
94
|
-
*
|
|
149
|
+
* Execute a command on the remote server. Automatically reconnects a
|
|
150
|
+
* dropped session before running.
|
|
95
151
|
*/
|
|
96
152
|
async exec(command, options) {
|
|
97
153
|
if (!this._connected) {
|
|
98
|
-
|
|
154
|
+
if (this._config.autoReconnect === false) {
|
|
155
|
+
throw new Error("SSH connection is not active");
|
|
156
|
+
}
|
|
157
|
+
await this.connect();
|
|
99
158
|
}
|
|
100
159
|
this._lastActivity = new Date();
|
|
101
160
|
const wrappedCommand = options?.cwd
|
|
@@ -112,7 +171,6 @@ export class SshConnection {
|
|
|
112
171
|
let stdout = "";
|
|
113
172
|
let stderr = "";
|
|
114
173
|
let killed = false;
|
|
115
|
-
// Set timeout
|
|
116
174
|
if (timeoutMs > 0) {
|
|
117
175
|
timeoutId = setTimeout(() => {
|
|
118
176
|
killed = true;
|
|
@@ -162,12 +220,17 @@ export class SshConnection {
|
|
|
162
220
|
return null; // Placeholder - actual streaming handled in executor.ts
|
|
163
221
|
}
|
|
164
222
|
/**
|
|
165
|
-
* Get an SFTP session for file transfers.
|
|
223
|
+
* Get an SFTP session for file transfers. Automatically reconnects a
|
|
224
|
+
* dropped session before returning.
|
|
166
225
|
*/
|
|
167
226
|
async sftp() {
|
|
168
227
|
if (!this._connected) {
|
|
169
|
-
|
|
228
|
+
if (this._config.autoReconnect === false) {
|
|
229
|
+
throw new Error("SSH connection is not active");
|
|
230
|
+
}
|
|
231
|
+
await this.connect();
|
|
170
232
|
}
|
|
233
|
+
this._lastActivity = new Date();
|
|
171
234
|
return new Promise((resolve, reject) => {
|
|
172
235
|
this.client.sftp((err, sftp) => {
|
|
173
236
|
if (err) {
|
|
@@ -182,10 +245,12 @@ export class SshConnection {
|
|
|
182
245
|
* Disconnect from the SSH server.
|
|
183
246
|
*/
|
|
184
247
|
disconnect() {
|
|
185
|
-
if (this.
|
|
248
|
+
if (this.client) {
|
|
186
249
|
this.client.end();
|
|
187
|
-
this._connected = false;
|
|
188
250
|
}
|
|
251
|
+
this._connected = false;
|
|
252
|
+
this._proxyCleanup?.();
|
|
253
|
+
this._proxyCleanup = undefined;
|
|
189
254
|
}
|
|
190
255
|
/**
|
|
191
256
|
* Get connection info for display.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.js","sourceRoot":"","sources":["../../src/ssh/connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAsB,MAAM,MAAM,CAAA;AACjD,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,IAAI,CAAA;AAC7C,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,MAAM,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"connection.js","sourceRoot":"","sources":["../../src/ssh/connection.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type HostKeyVerdict = "ok" | "changed" | "unknown";
|
|
2
|
+
interface KnownHostEntry {
|
|
3
|
+
/** Unix shell-style pattern for the host (may contain * / ? and be comma separated). */
|
|
4
|
+
original: string;
|
|
5
|
+
/** Base64-encoded SSH public key blob (matches what ssh2 passes to hostVerifier). */
|
|
6
|
+
key: Buffer;
|
|
7
|
+
/** True if the hostname portion is stored hashed (`|1|...|`). */
|
|
8
|
+
hashed: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** Strip `[host]:port` brackets and the default-22 port suffix for comparisons. */
|
|
11
|
+
export declare function normalizeHostName(host: string, port?: number): string;
|
|
12
|
+
/** Load and parse a known_hosts file (defaults to ~/.ssh/known_hosts). */
|
|
13
|
+
export declare function loadKnownHosts(configuredPath?: string): KnownHostEntry[];
|
|
14
|
+
/**
|
|
15
|
+
* Verify a server host key against known_hosts.
|
|
16
|
+
*
|
|
17
|
+
* @param host hostname/IP the user asked to connect to
|
|
18
|
+
* @param serverKey raw server public key blob (as delivered to ssh2 hostVerifier)
|
|
19
|
+
* @param configuredPath optional path to a custom known_hosts file
|
|
20
|
+
* @returns "ok" when a matching, identical key exists; "changed" when a matching
|
|
21
|
+
* entry holds a different key; "unknown" when no entry matches the host.
|
|
22
|
+
*/
|
|
23
|
+
export declare function verifyHostKey(host: string, serverKey: Buffer, configuredPath?: string, port?: number): HostKeyVerdict;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=known_hosts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"known_hosts.d.ts","sourceRoot":"","sources":["../../src/ssh/known_hosts.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,SAAS,GAAG,SAAS,CAAA;AAIzD,UAAU,cAAc;IACtB,wFAAwF;IACxF,QAAQ,EAAE,MAAM,CAAA;IAChB,qFAAqF;IACrF,GAAG,EAAE,MAAM,CAAA;IACX,iEAAiE;IACjE,MAAM,EAAE,OAAO,CAAA;CAChB;AAoCD,mFAAmF;AACnF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAcrE;AAuED,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,cAAc,EAAE,CAIxE;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,cAAc,CAAC,EAAE,MAAM,EACvB,IAAI,CAAC,EAAE,MAAM,GACZ,cAAc,CAShB"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* known_hosts support — verifies the remote server's host key against
|
|
3
|
+
* ~/.ssh/known_hosts so connections cannot be silently redirected (MITM).
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, existsSync } from "fs";
|
|
6
|
+
import { createHash } from "crypto";
|
|
7
|
+
import { resolve as pathResolve } from "path";
|
|
8
|
+
const DEFAULT_KNOWN_HOSTS = () => pathResolve(process.env.HOME || "", ".ssh", "known_hosts");
|
|
9
|
+
function parseKnownHosts(content) {
|
|
10
|
+
const entries = [];
|
|
11
|
+
for (const rawLine of content.split("\n")) {
|
|
12
|
+
const line = rawLine.trim();
|
|
13
|
+
if (!line || line.startsWith("#"))
|
|
14
|
+
continue;
|
|
15
|
+
// Fields: marker? hostpart(s) keytype base64key [comment...]
|
|
16
|
+
const parts = line.split(/\s+/);
|
|
17
|
+
if (parts.length < 3)
|
|
18
|
+
continue;
|
|
19
|
+
// Skip optional "marker" (@cert-authority, @revoked)
|
|
20
|
+
let idx = 0;
|
|
21
|
+
if (parts[0].startsWith("@"))
|
|
22
|
+
idx = 1;
|
|
23
|
+
const hostPart = parts[idx];
|
|
24
|
+
const keyPart = parts[idx + 2]; // parts[idx+1] is the key type
|
|
25
|
+
if (!keyPart)
|
|
26
|
+
continue;
|
|
27
|
+
let key;
|
|
28
|
+
try {
|
|
29
|
+
key = Buffer.from(keyPart, "base64");
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (!key || key.length === 0)
|
|
35
|
+
continue;
|
|
36
|
+
const hashed = hostPart.startsWith("|");
|
|
37
|
+
entries.push({ original: hostPart, key, hashed });
|
|
38
|
+
}
|
|
39
|
+
return entries;
|
|
40
|
+
}
|
|
41
|
+
/** Strip `[host]:port` brackets and the default-22 port suffix for comparisons. */
|
|
42
|
+
export function normalizeHostName(host, port) {
|
|
43
|
+
let h = host;
|
|
44
|
+
const bracket = h.match(/^\[(.+)\]/);
|
|
45
|
+
if (bracket) {
|
|
46
|
+
h = bracket[1];
|
|
47
|
+
return h;
|
|
48
|
+
}
|
|
49
|
+
// `hostname,hostname2` handled at compare time; here just strip port if present
|
|
50
|
+
const withPort = h.match(/^(.+):(\d+)$/);
|
|
51
|
+
if (withPort) {
|
|
52
|
+
const p = Number(withPort[2]);
|
|
53
|
+
if (port === undefined || p === port)
|
|
54
|
+
h = withPort[1];
|
|
55
|
+
}
|
|
56
|
+
return h;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Normalize a known_hosts pattern into a bare host string, extracting the
|
|
60
|
+
* host portion out of `[host]:port` / `host:port` forms when the stored port
|
|
61
|
+
* matches the requested one.
|
|
62
|
+
*/
|
|
63
|
+
function normalizePatternHost(pattern, port) {
|
|
64
|
+
let p = pattern;
|
|
65
|
+
const bracket = p.match(/^\[(.+)\](?::(\d+))?$/);
|
|
66
|
+
if (bracket) {
|
|
67
|
+
const pPort = bracket[2] ? Number(bracket[2]) : undefined;
|
|
68
|
+
if (port !== undefined && pPort !== undefined && pPort !== port)
|
|
69
|
+
return "";
|
|
70
|
+
return bracket[1];
|
|
71
|
+
}
|
|
72
|
+
const withPort = p.match(/^(.+):(\d+)$/);
|
|
73
|
+
if (withPort) {
|
|
74
|
+
const pPort = Number(withPort[2]);
|
|
75
|
+
if (port !== undefined && pPort !== port)
|
|
76
|
+
return "";
|
|
77
|
+
return withPort[1];
|
|
78
|
+
}
|
|
79
|
+
return p;
|
|
80
|
+
}
|
|
81
|
+
function hostPatternMatches(pattern, host, port) {
|
|
82
|
+
// known_hosts supports `|1|base64salt|base64hash`
|
|
83
|
+
if (pattern.startsWith("|")) {
|
|
84
|
+
const parts = pattern.split("|");
|
|
85
|
+
if (parts.length !== 4)
|
|
86
|
+
return false;
|
|
87
|
+
const [, magic, saltB64, hashB64] = parts;
|
|
88
|
+
if (magic !== "1")
|
|
89
|
+
return false;
|
|
90
|
+
try {
|
|
91
|
+
const salt = Buffer.from(saltB64, "base64");
|
|
92
|
+
const expected = Buffer.from(hashB64, "base64");
|
|
93
|
+
// OpenSSH stores SHA1(salt || lowercased-host)
|
|
94
|
+
const digest = createHash("sha1").update(salt).update(host.toLowerCase(), "utf8").digest();
|
|
95
|
+
return digest.equals(expected);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// `[host]:port`, `host:port` and comma-separated lists of patterns.
|
|
102
|
+
const target = host.toLowerCase();
|
|
103
|
+
for (const raw of pattern.split(",")) {
|
|
104
|
+
const part = normalizePatternHost(raw.trim(), port);
|
|
105
|
+
if (!part)
|
|
106
|
+
continue;
|
|
107
|
+
const p = part.toLowerCase();
|
|
108
|
+
if (p === target)
|
|
109
|
+
return true;
|
|
110
|
+
if (!p.includes("*") && !p.includes("?"))
|
|
111
|
+
continue;
|
|
112
|
+
let re = "";
|
|
113
|
+
for (const ch of p) {
|
|
114
|
+
if (ch === "*")
|
|
115
|
+
re += ".*";
|
|
116
|
+
else if (ch === "?")
|
|
117
|
+
re += ".";
|
|
118
|
+
else
|
|
119
|
+
re += ch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
if (new RegExp(`^${re}$`).test(target))
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
/* ignore invalid patterns */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
function entriesForHost(entries, host, port) {
|
|
132
|
+
const normalized = normalizeHostName(host, port);
|
|
133
|
+
return entries.filter((e) => hostPatternMatches(e.original, normalized, port));
|
|
134
|
+
}
|
|
135
|
+
/** Load and parse a known_hosts file (defaults to ~/.ssh/known_hosts). */
|
|
136
|
+
export function loadKnownHosts(configuredPath) {
|
|
137
|
+
const path = configuredPath || DEFAULT_KNOWN_HOSTS();
|
|
138
|
+
if (!existsSync(path))
|
|
139
|
+
return [];
|
|
140
|
+
return parseKnownHosts(readFileSync(path, "utf-8"));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Verify a server host key against known_hosts.
|
|
144
|
+
*
|
|
145
|
+
* @param host hostname/IP the user asked to connect to
|
|
146
|
+
* @param serverKey raw server public key blob (as delivered to ssh2 hostVerifier)
|
|
147
|
+
* @param configuredPath optional path to a custom known_hosts file
|
|
148
|
+
* @returns "ok" when a matching, identical key exists; "changed" when a matching
|
|
149
|
+
* entry holds a different key; "unknown" when no entry matches the host.
|
|
150
|
+
*/
|
|
151
|
+
export function verifyHostKey(host, serverKey, configuredPath, port) {
|
|
152
|
+
if (!serverKey || serverKey.length === 0)
|
|
153
|
+
return "unknown";
|
|
154
|
+
const entries = entriesForHost(loadKnownHosts(configuredPath), host, port ?? 22);
|
|
155
|
+
if (entries.length === 0)
|
|
156
|
+
return "unknown";
|
|
157
|
+
const match = entries.some((e) => e.key.equals(serverKey));
|
|
158
|
+
return match ? "ok" : "changed";
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=known_hosts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"known_hosts.js","sourceRoot":"","sources":["../../src/ssh/known_hosts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Duplex } from "stream";
|
|
2
|
+
export interface JumpHost {
|
|
3
|
+
host: string;
|
|
4
|
+
port: number;
|
|
5
|
+
username?: string;
|
|
6
|
+
keyPath?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ProxySpec {
|
|
9
|
+
jumps?: JumpHost[];
|
|
10
|
+
command?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ProxyTarget {
|
|
13
|
+
host: string;
|
|
14
|
+
port: number;
|
|
15
|
+
username?: string;
|
|
16
|
+
keyPath?: string;
|
|
17
|
+
passphrase?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface PreparedProxy {
|
|
20
|
+
/** Stream the main connection should use instead of a fresh TCP socket. */
|
|
21
|
+
sock: Duplex;
|
|
22
|
+
/** Release intermediate resources (jump clients / child processes). */
|
|
23
|
+
cleanup(): void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build a tunnel to `target` through the given proxy. The returned stream is
|
|
27
|
+
* passed as the `sock` of the target connection.
|
|
28
|
+
*/
|
|
29
|
+
export declare function prepareProxy(proxy: ProxySpec, target: ProxyTarget): Promise<PreparedProxy>;
|
|
30
|
+
//# sourceMappingURL=proxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/ssh/proxy.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAG/B,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAA;IACZ,uEAAuE;IACvE,OAAO,IAAI,IAAI,CAAA;CAChB;AAmDD;;;GAGG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC,CAgFhG"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy support for SSH connections:
|
|
3
|
+
* - ProxyJump: hop through one or more jump hosts (OpenSSH ProxyJump).
|
|
4
|
+
* - ProxyCommand: use an external command to open the tunnel
|
|
5
|
+
* (OpenSSH ProxyCommand), e.g. `ssh -W %h:%p bastion`.
|
|
6
|
+
*/
|
|
7
|
+
import { Client } from "ssh2";
|
|
8
|
+
import { spawn } from "child_process";
|
|
9
|
+
import { Duplex } from "stream";
|
|
10
|
+
import { readFileSync, existsSync } from "fs";
|
|
11
|
+
function loadPrivateKey(keyPath) {
|
|
12
|
+
if (!keyPath)
|
|
13
|
+
return undefined;
|
|
14
|
+
const path = keyPath;
|
|
15
|
+
if (!existsSync(path))
|
|
16
|
+
return undefined;
|
|
17
|
+
return readFileSync(path);
|
|
18
|
+
}
|
|
19
|
+
function connectClient(opts, sock) {
|
|
20
|
+
const privateKey = loadPrivateKey(opts.keyPath);
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const client = new Client();
|
|
23
|
+
const timer = setTimeout(() => {
|
|
24
|
+
client.destroy();
|
|
25
|
+
reject(new Error(`Proxy connection to ${opts.username}@${opts.host}:${opts.port} timed out`));
|
|
26
|
+
}, opts.timeoutMs);
|
|
27
|
+
client
|
|
28
|
+
.on("ready", () => {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
resolve(client);
|
|
31
|
+
})
|
|
32
|
+
.on("error", (err) => {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
reject(err);
|
|
35
|
+
})
|
|
36
|
+
.connect({
|
|
37
|
+
host: opts.host,
|
|
38
|
+
port: opts.port,
|
|
39
|
+
username: opts.username,
|
|
40
|
+
readyTimeout: opts.timeoutMs,
|
|
41
|
+
...(sock ? { sock } : {}),
|
|
42
|
+
...(privateKey ? { privateKey } : {}),
|
|
43
|
+
...(opts.passphrase ? { passphrase: opts.passphrase } : {}),
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function forwardOut(client, srcIP, srcPort, dstIP, dstPort) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
client.forwardOut(srcIP, srcPort, dstIP, dstPort, (err, stream) => {
|
|
50
|
+
if (err)
|
|
51
|
+
reject(err);
|
|
52
|
+
else
|
|
53
|
+
resolve(stream);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Build a tunnel to `target` through the given proxy. The returned stream is
|
|
59
|
+
* passed as the `sock` of the target connection.
|
|
60
|
+
*/
|
|
61
|
+
export async function prepareProxy(proxy, target) {
|
|
62
|
+
const timeoutMs = 10_000;
|
|
63
|
+
// ── ProxyCommand: spawn and use its stdio as the socket ──
|
|
64
|
+
if (proxy.command && proxy.command.trim()) {
|
|
65
|
+
const cmd = proxy.command
|
|
66
|
+
.replaceAll("%h", target.host)
|
|
67
|
+
.replaceAll("%p", String(target.port))
|
|
68
|
+
.replaceAll("%r", target.username || process.env.USER || "");
|
|
69
|
+
const child = spawn("sh", ["-c", cmd], { stdio: ["pipe", "pipe", "inherit"] });
|
|
70
|
+
const tunnel = new Duplex({
|
|
71
|
+
read() {
|
|
72
|
+
// Data is pushed manually from child.stdout below.
|
|
73
|
+
},
|
|
74
|
+
write(chunk, _enc, cb) {
|
|
75
|
+
child.stdin?.write(chunk, cb);
|
|
76
|
+
},
|
|
77
|
+
final(cb) {
|
|
78
|
+
child.stdin?.end(cb);
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
child.stdout?.on("data", (chunk) => tunnel.push(chunk));
|
|
82
|
+
child.stdout?.on("end", () => tunnel.push(null));
|
|
83
|
+
child.stdout?.on("error", (err) => tunnel.destroy(err));
|
|
84
|
+
child.on("error", (err) => tunnel.destroy(err));
|
|
85
|
+
return {
|
|
86
|
+
sock: tunnel,
|
|
87
|
+
cleanup: () => {
|
|
88
|
+
if (!child.killed)
|
|
89
|
+
child.kill();
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// ── ProxyJump: chain clients through hops, last hop receives the target ──
|
|
94
|
+
const jumps = proxy.jumps || [];
|
|
95
|
+
if (jumps.length === 0) {
|
|
96
|
+
throw new Error("No proxy strategy configured");
|
|
97
|
+
}
|
|
98
|
+
const clients = [];
|
|
99
|
+
let sock;
|
|
100
|
+
try {
|
|
101
|
+
for (let i = 0; i < jumps.length; i++) {
|
|
102
|
+
const hop = jumps[i];
|
|
103
|
+
const next = jumps[i + 1];
|
|
104
|
+
const destHost = next ? next.host : target.host;
|
|
105
|
+
const destPort = next ? next.port : target.port;
|
|
106
|
+
const destUser = next ? next.username || hop.username || target.username : target.username;
|
|
107
|
+
const username = hop.username || target.username || process.env.USER || "root";
|
|
108
|
+
const keyPath = hop.keyPath || target.keyPath;
|
|
109
|
+
// The next-hop client talks over the previous hop's forwarded stream
|
|
110
|
+
const client = await connectClient({ host: hop.host, port: hop.port, username, keyPath, timeoutMs }, sock);
|
|
111
|
+
clients.push(client);
|
|
112
|
+
// Open the outbound channel to the next destination
|
|
113
|
+
sock = await forwardOut(client, "127.0.0.1", 0, destHost, destPort);
|
|
114
|
+
void destUser;
|
|
115
|
+
}
|
|
116
|
+
if (!sock)
|
|
117
|
+
throw new Error("Failed to build proxy tunnel");
|
|
118
|
+
const finalSock = sock;
|
|
119
|
+
return {
|
|
120
|
+
sock: finalSock,
|
|
121
|
+
cleanup: () => {
|
|
122
|
+
for (const c of clients)
|
|
123
|
+
c.end();
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
for (const c of clients)
|
|
129
|
+
c.end();
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/ssh/proxy.ts"],"names":[],"mappings":"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"}
|