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 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
- return !/Control socket connect.*: No such file or directory/.test(output);
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
- reject(new Error("Permission denied"));
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
- if (await existsMaster(hostInfo)) {
269
- const args = getSshOption(hostInfo);
270
- args.push("-Oexit");
282
+ try {
283
+ if (await existsMaster(hostInfo)) {
284
+ const args = getSshOption(hostInfo);
285
+ args.push("-Oexit");
271
286
 
272
- let output = "";
273
- const cb = (e)=>{
274
- output += e;
275
- };
276
- try {
277
- debug(`disconnect: ${sshCmd} ${args.join(" ")}`);
278
- await fork(hostInfo, sshCmd, args, 0, cb);
279
- } catch (err) {
280
- if (err.rt === 255) {
281
- return;
282
- }
283
- if (/Control socket connect.*: No such file or directory/.test(output)) {
284
- return;
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
- throw err;
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ssh-client-wrapper",
3
- "version": "3.1.2",
3
+ "version": "3.1.4",
4
4
  "description": "wrapper library for open ssh client and rsync",
5
5
  "keywords": [
6
6
  "ssh",