ssh-client-wrapper 3.1.2 → 3.1.4
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 +10 -1
- package/lib/sshExec.js +43 -28
- package/lib/util.js +19 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Creates a new SSH client instance.
|
|
|
56
56
|
* `host` (string): The hostname or IP address of the server.
|
|
57
57
|
* `user` (string): The username for authentication.
|
|
58
58
|
* `port` (number, optional): The port number. Defaults to 22.
|
|
59
|
-
* `password` (string | Function, optional): The password for authentication, or a function that returns the password.
|
|
59
|
+
* `password` (string | Function, optional): The password for authentication, or a function that returns the password. See [Wrong password](#wrong-password).
|
|
60
60
|
* `keyFile` (string, optional): The path to the private key file for key-based authentication.
|
|
61
61
|
* `passphrase` (string | Function, optional): The passphrase for the private key, or a function that returns it.
|
|
62
62
|
* `noStrictHostKeyChecking` (boolean, optional): If `true`, bypasses strict host key checking.
|
|
@@ -190,6 +190,15 @@ Checks if a connection to the remote host can be established.
|
|
|
190
190
|
|
|
191
191
|
* Returns: `Promise<boolean>` - Resolves with `true` on success.
|
|
192
192
|
|
|
193
|
+
### Wrong password
|
|
194
|
+
|
|
195
|
+
If the login is refused, the promise is rejected with an `Error` whose `message` is `Permission denied` and whose `code` is `AUTH_FAILED`. `host`, `user` and `port` tell which login failed.
|
|
196
|
+
|
|
197
|
+
* `password` / `passphrase` given as a **string**: ssh is asked to try only once (`-oNumberOfPasswordPrompts=1`), because sending the same wrong string again can not succeed (and may lock the account). The promise is rejected at once.
|
|
198
|
+
* `password` / `passphrase` given as a **function**: the function is called each time ssh asks (up to three times, the ssh default), so that it can ask the user again and return another value. If none of them is accepted the promise is rejected as above.
|
|
199
|
+
|
|
200
|
+
Whether to ask the user again after an `AUTH_FAILED` is up to the application.
|
|
201
|
+
|
|
193
202
|
### `.disconnect()`
|
|
194
203
|
Closes the master SSH connection to the remote host.
|
|
195
204
|
|
package/lib/sshExec.js
CHANGED
|
@@ -138,7 +138,8 @@ async function existsMaster(hostInfo, argTimeout) {
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
//"Connection refused" means that the socket is left but nobody listens (the master was killed, or it is exiting)
|
|
142
|
+
return !/Control socket connect.*: (No such file or directory|Connection refused)/.test(output);
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
/**
|
|
@@ -205,7 +206,12 @@ export async function connect(hostInfo, timeout = 60) {
|
|
|
205
206
|
done = true;
|
|
206
207
|
resolve();
|
|
207
208
|
} else if (/Permission denied \(/.test(output)) {
|
|
208
|
-
|
|
209
|
+
const err = new Error("Permission denied");
|
|
210
|
+
err.code = "AUTH_FAILED";
|
|
211
|
+
err.host = hostInfo.host;
|
|
212
|
+
err.user = hostInfo.user;
|
|
213
|
+
err.port = hostInfo.port;
|
|
214
|
+
reject(err);
|
|
209
215
|
} else if (/Could not resolve hostname/.test(output)) {
|
|
210
216
|
reject(new Error("Could not resolve hostname"));
|
|
211
217
|
} else if (/Bad port/.test(output)) {
|
|
@@ -251,6 +257,14 @@ export async function connect(hostInfo, timeout = 60) {
|
|
|
251
257
|
try {
|
|
252
258
|
await p;
|
|
253
259
|
debug(`connected to ${hostInfo.host}`);
|
|
260
|
+
} catch (e) {
|
|
261
|
+
//the pty of a failed connection must not be left behind. It keeps the process alive.
|
|
262
|
+
debug(`failed to connect to ${hostInfo.host}, kill master pty`);
|
|
263
|
+
if (hostInfo.masterPty) {
|
|
264
|
+
hostInfo.masterPty.kill();
|
|
265
|
+
hostInfo.masterPty = null;
|
|
266
|
+
}
|
|
267
|
+
throw e;
|
|
254
268
|
} finally {
|
|
255
269
|
if (ac !== null) {
|
|
256
270
|
ac.abort();
|
|
@@ -265,35 +279,36 @@ export async function connect(hostInfo, timeout = 60) {
|
|
|
265
279
|
export async function disconnect(hostInfo) {
|
|
266
280
|
debug("disconnect called");
|
|
267
281
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
282
|
+
try {
|
|
283
|
+
if (await existsMaster(hostInfo)) {
|
|
284
|
+
const args = getSshOption(hostInfo);
|
|
285
|
+
args.push("-Oexit");
|
|
271
286
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
287
|
+
let output = "";
|
|
288
|
+
const cb = (e)=>{
|
|
289
|
+
output += e;
|
|
290
|
+
};
|
|
291
|
+
try {
|
|
292
|
+
debug(`disconnect: ${sshCmd} ${args.join(" ")}`);
|
|
293
|
+
await fork(hostInfo, sshCmd, args, 0, cb);
|
|
294
|
+
debug(`disconnected from ${hostInfo.host}`);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
//the master is already gone
|
|
297
|
+
const gone = err.rt === 255 || /Control socket connect.*: No such file or directory/.test(output);
|
|
298
|
+
if (!gone) {
|
|
299
|
+
throw err;
|
|
300
|
+
}
|
|
285
301
|
}
|
|
286
|
-
|
|
302
|
+
} else {
|
|
303
|
+
debug(`disconnect: master session to ${hostInfo.host} does not exist`);
|
|
304
|
+
}
|
|
305
|
+
} finally {
|
|
306
|
+
//the pty must be released even if the master is already gone. It keeps the process alive.
|
|
307
|
+
if (hostInfo.masterPty) {
|
|
308
|
+
debug("exit masterPty");
|
|
309
|
+
sendPty(hostInfo.masterPty, "exit \n", debugSendMasterPty, 228);
|
|
310
|
+
hostInfo.masterPty = null;
|
|
287
311
|
}
|
|
288
|
-
debug(`disconnected from ${hostInfo.host}`);
|
|
289
|
-
} else {
|
|
290
|
-
debug(`disconnect: master session to ${hostInfo.host} does not exist`);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (hostInfo.masterPty) {
|
|
294
|
-
debug("exit masterPty");
|
|
295
|
-
sendPty(hostInfo.masterPty, "exit \n", debugSendMasterPty, 228);
|
|
296
|
-
hostInfo.masterPty = null;
|
|
297
312
|
}
|
|
298
313
|
}
|
|
299
314
|
|
package/lib/util.js
CHANGED
|
@@ -155,6 +155,20 @@ export const sanityCheck = (hostInfo)=>{
|
|
|
155
155
|
return hostInfo;
|
|
156
156
|
};
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* check if the password and the passphrase can not change between the prompts of ssh
|
|
160
|
+
* @param {object} hostInfo - host information object defined in index.js
|
|
161
|
+
* @returns {boolean} - true if at least one of them is a string and none of them is a function
|
|
162
|
+
*/
|
|
163
|
+
function hasFixedSecret(hostInfo) {
|
|
164
|
+
const secrets = [hostInfo.password, hostInfo.passphrase];
|
|
165
|
+
return secrets.some((e)=>{
|
|
166
|
+
return typeof e === "string";
|
|
167
|
+
}) && !secrets.some((e)=>{
|
|
168
|
+
return typeof e === "function";
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
158
172
|
function getControlPersistDir(hostInfo) {
|
|
159
173
|
for (const candidate of [hostInfo.ControlPersistDir, process.env.SSH_CONTROL_PERSIST_DIR]) {
|
|
160
174
|
if (typeof candidate === "string") {
|
|
@@ -237,6 +251,11 @@ export const getSshOption = (hostInfo, withoutDestination)=>{
|
|
|
237
251
|
if (Number.isInteger(hostInfo.ConnectTimeout) && hostInfo.ConnectTimeout > 0) {
|
|
238
252
|
args.push(`-oConnectTimeout=${hostInfo.ConnectTimeout}`);
|
|
239
253
|
}
|
|
254
|
+
//ssh asks three times by default. If the secret is a fixed string, asking again with the same string is
|
|
255
|
+
//of no use. A function can give another secret each time, so it keeps the default.
|
|
256
|
+
if (hasFixedSecret(hostInfo)) {
|
|
257
|
+
args.push("-oNumberOfPasswordPrompts=1");
|
|
258
|
+
}
|
|
240
259
|
if (Array.isArray(hostInfo.sshOpt)) {
|
|
241
260
|
args.push(...hostInfo.sshOpt.filter((e)=>{
|
|
242
261
|
return typeof e === "string";
|