ssh-client-wrapper 1.2.1 → 1.4.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/.github/workflows/run_test.yml +3 -0
- package/lib/fork.js +64 -33
- package/lib/index.js +21 -10
- package/lib/rsyncExec.js +9 -6
- package/lib/sshExec.js +108 -10
- package/lib/util.js +11 -16
- package/package.json +12 -2
- package/test/sshExec.js +24 -17
|
@@ -3,6 +3,8 @@ on:
|
|
|
3
3
|
push:
|
|
4
4
|
tags-ignore:
|
|
5
5
|
- 'v*'
|
|
6
|
+
branches:
|
|
7
|
+
- '*'
|
|
6
8
|
workflow_call:
|
|
7
9
|
concurrency:
|
|
8
10
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
@@ -18,6 +20,7 @@ jobs:
|
|
|
18
20
|
TEST_USER: testuser
|
|
19
21
|
TEST_PW: passw0rd
|
|
20
22
|
TEST_PORT: 10022
|
|
23
|
+
DEBUG: 'ssh*'
|
|
21
24
|
runs-on: ubuntu-latest
|
|
22
25
|
strategy:
|
|
23
26
|
matrix:
|
package/lib/fork.js
CHANGED
|
@@ -6,47 +6,75 @@ const { spawn } = require("node-pty");
|
|
|
6
6
|
|
|
7
7
|
const { rePwPrompt, rePhPrompt, reNewHostPrompt, buff2String } = require("./util.js");
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
|
|
9
|
+
const nodePtyOpt = {
|
|
11
10
|
shell: process.platform === "win32" ? process.env.ComSpec : process.env.SHELL,
|
|
12
11
|
windowsHide: true
|
|
13
12
|
};
|
|
14
|
-
async function fork(hostInfo, cmd, args, argOpt, outputCallback) {
|
|
15
|
-
return new Promise((resolve, reject)=>{
|
|
16
|
-
const opt = argOpt || defaultOpt;
|
|
17
13
|
|
|
14
|
+
function sshLoginCallback(output, ptyProcess, pw, ph) {
|
|
15
|
+
if (reNewHostPrompt.test(output)) {
|
|
16
|
+
debugSendPty("yes");
|
|
17
|
+
ptyProcess.write("yes\n");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (rePwPrompt.test(output)) {
|
|
21
|
+
if (typeof pw === "string") {
|
|
22
|
+
debug("use given password");
|
|
23
|
+
debugSendPty(pw);
|
|
24
|
+
ptyProcess.write(`${pw}\n`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (typeof pw === "function") {
|
|
28
|
+
debug("call password callback function");
|
|
29
|
+
pw()
|
|
30
|
+
.then((v)=>{
|
|
31
|
+
ptyProcess.write(`${v}\n`);
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (rePhPrompt.test(output)) {
|
|
37
|
+
if (typeof ph === "string") {
|
|
38
|
+
debug("use given passphrase");
|
|
39
|
+
debugSendPty(ph);
|
|
40
|
+
ptyProcess.write(`${ph}\n`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (typeof ph === "function") {
|
|
44
|
+
debug("call passphrase callback function");
|
|
45
|
+
ph()
|
|
46
|
+
.then((v)=>{
|
|
47
|
+
ptyProcess.write(`${v}\n`);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getMasterPty(hostInfo) {
|
|
54
|
+
debug("fork pty for master connection");
|
|
55
|
+
const ptyProcess = spawn(nodePtyOpt.shell, [], nodePtyOpt);
|
|
56
|
+
ptyProcess.onData(buff2String.bind(null, debugVerbose));
|
|
57
|
+
ptyProcess.onData((data)=>{
|
|
58
|
+
const output = data.toString();
|
|
59
|
+
sshLoginCallback(output, ptyProcess, hostInfo.password, hostInfo.passphrase);
|
|
60
|
+
});
|
|
61
|
+
return ptyProcess;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function fork(hostInfo, cmd, args, outputCallback) {
|
|
65
|
+
return new Promise((resolve, reject)=>{
|
|
18
66
|
debug(`cmd=${cmd}: args=${args}`);
|
|
19
|
-
const ptyProcess = spawn(cmd, args,
|
|
67
|
+
const ptyProcess = spawn(cmd, args, nodePtyOpt);
|
|
20
68
|
ptyProcess.onData(buff2String.bind(null, debugVerbose));
|
|
21
|
-
|
|
22
69
|
ptyProcess.onData((data)=>{
|
|
23
70
|
const output = data.toString();
|
|
71
|
+
sshLoginCallback(output, ptyProcess, hostInfo.password, hostInfo.passphrase);
|
|
24
72
|
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (typeof hostInfo.password === "string") {
|
|
31
|
-
debugSendPty(hostInfo.password);
|
|
32
|
-
ptyProcess.write(`${hostInfo.password}\n`);
|
|
33
|
-
} else if (typeof hostInfo.password === "function") {
|
|
34
|
-
hostInfo.password()
|
|
35
|
-
.then((pw)=>{
|
|
36
|
-
ptyProcess.write(`${pw}\n`);
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
if (rePhPrompt.test(output)) {
|
|
41
|
-
if (typeof hostInfo.passphrase === "string") {
|
|
42
|
-
debugSendPty(hostInfo.passphrase);
|
|
43
|
-
ptyProcess.write(`${hostInfo.passphrase}\n`);
|
|
44
|
-
} else if (typeof hostInfo.passphrase === "function") {
|
|
45
|
-
hostInfo.passphrase()
|
|
46
|
-
.then((pw)=>{
|
|
47
|
-
ptyProcess.write(`${pw}\n`);
|
|
48
|
-
});
|
|
49
|
-
}
|
|
73
|
+
if (/kex_exchange_identification: Connection closed by remote host/.test(output)) {
|
|
74
|
+
ptyProcess.kill();
|
|
75
|
+
const err = new Error("kex_exchange_identification");
|
|
76
|
+
err.retryable = true;
|
|
77
|
+
reject(err);
|
|
50
78
|
}
|
|
51
79
|
});
|
|
52
80
|
|
|
@@ -66,4 +94,7 @@ async function fork(hostInfo, cmd, args, argOpt, outputCallback) {
|
|
|
66
94
|
});
|
|
67
95
|
});
|
|
68
96
|
}
|
|
69
|
-
module.exports =
|
|
97
|
+
module.exports = {
|
|
98
|
+
fork,
|
|
99
|
+
getMasterPty
|
|
100
|
+
};
|
package/lib/index.js
CHANGED
|
@@ -12,24 +12,35 @@ const { isArrayOfString } = require("./util.js");
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* host info object which contain all settings for 1 remote host
|
|
16
|
+
* it will be (shallow) copied in constructor.
|
|
17
17
|
* @param {Object} hostInfo - option object
|
|
18
18
|
* @param {string} hostInfo.host - destination
|
|
19
19
|
* @param {string} hostInfo.user - login name
|
|
20
20
|
* @param {number} hostInfo.port - port number
|
|
21
|
-
* @param {string | Function} hostInfo.password - password
|
|
22
|
-
* @param {string | Function} hostInfo.passphrase - passphrase
|
|
21
|
+
* @param {string | Function} hostInfo.password - password or function which return password
|
|
22
|
+
* @param {string | Function} hostInfo.passphrase - passphrase or function which return passphrase of private key
|
|
23
23
|
* @param {string} hostInfo.keyFile - private key filename
|
|
24
24
|
* @param {boolean} hostInfo.noStrictHostkeyChecking - bypass host key checking if true
|
|
25
|
-
* @param {Integer} hostInfo.
|
|
25
|
+
* @param {Integer} hostInfo.ControlPersist=180 - how long keep master connection after last client connection has been closed (sec).
|
|
26
|
+
* @param {Integer} hostInfo.ConnectTimeout=60 - timeout used when connectiong to ths SSH server
|
|
27
|
+
* @param {Integer} hostInfo.maxRetry=3 - max number of retry
|
|
28
|
+
* @param {Integer} hostInfo.retryMinTimeout=60 - minimum duration before first retry
|
|
29
|
+
* @param {Integer} hostInfo.retryMaxTimeout=300 - maximum duration between each retry
|
|
30
|
+
* @param {string[]} hostInfo.sshOpt - additional options for ssh
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Facade class.
|
|
35
|
+
* @constructor
|
|
26
36
|
*/
|
|
27
37
|
class SshClientWrapper {
|
|
28
38
|
constructor(hostInfo) {
|
|
29
39
|
if (typeof hostInfo.host !== "string") {
|
|
30
40
|
return new Error("hostname must be specified!");
|
|
31
41
|
}
|
|
32
|
-
this.hostInfo = hostInfo;
|
|
42
|
+
this.hostInfo = { ...hostInfo };
|
|
43
|
+
this.hostInfo.masterPty = null;
|
|
33
44
|
}
|
|
34
45
|
|
|
35
46
|
/**
|
|
@@ -78,9 +89,9 @@ class SshClientWrapper {
|
|
|
78
89
|
|
|
79
90
|
/**
|
|
80
91
|
* Send file or directory and its child to server.
|
|
81
|
-
* @param {[
|
|
92
|
+
* @param {string[]} src - File or directory name which to be recieve.
|
|
82
93
|
* @param {string} dst - Destination path.
|
|
83
|
-
* @param {[
|
|
94
|
+
* @param {string[]} opt - option for rsync
|
|
84
95
|
* @returns {Promise} - Resolved with undefined when file transfer is done.
|
|
85
96
|
*/
|
|
86
97
|
async send(src, dst, opt) {
|
|
@@ -106,9 +117,9 @@ class SshClientWrapper {
|
|
|
106
117
|
|
|
107
118
|
/**
|
|
108
119
|
* Get file or directory and its child from server.
|
|
109
|
-
* @param {[
|
|
120
|
+
* @param {string[]} src - File or directory name which to be recieve.
|
|
110
121
|
* @param {string} dst - Destination path.
|
|
111
|
-
* @param {[
|
|
122
|
+
* @param {string[]} opt - option for rsync
|
|
112
123
|
* @returns {Promise} - Resolved with undefined when file transfer is done.
|
|
113
124
|
*/
|
|
114
125
|
async recv(src, dst, opt) {
|
package/lib/rsyncExec.js
CHANGED
|
@@ -4,11 +4,12 @@ const debug = require("debug")("sshClientWrapper:rsyncExec");
|
|
|
4
4
|
const debugVerbose = require("debug")("sshClientWrapper:rsyncExec_verbose");
|
|
5
5
|
const { glob } = require("glob");
|
|
6
6
|
|
|
7
|
-
const { sshExec } = require("./sshExec.js");
|
|
8
|
-
const fork = require("./fork.js");
|
|
7
|
+
const { connect, retryWrapper, sshExec } = require("./sshExec.js");
|
|
8
|
+
const { fork } = require("./fork.js");
|
|
9
9
|
|
|
10
10
|
const { sshCmd, rsyncCmd, getSshOption } = require("./util.js");
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
/**
|
|
13
14
|
* Send file or directory and its child to server.
|
|
14
15
|
* @param {Object} hostInfo - hostinfo object
|
|
@@ -19,6 +20,7 @@ const { sshCmd, rsyncCmd, getSshOption } = require("./util.js");
|
|
|
19
20
|
* this function execute rsync -avv --copy-unsafe-links src rsync://[user@]host[:port]/dst
|
|
20
21
|
*/
|
|
21
22
|
async function send(hostInfo, argSrc, dst, rsyncOpt = []) {
|
|
23
|
+
await connect(hostInfo);
|
|
22
24
|
const promises = await Promise.allSettled(argSrc.map((e)=>{
|
|
23
25
|
return glob(e);
|
|
24
26
|
}));
|
|
@@ -41,8 +43,8 @@ async function send(hostInfo, argSrc, dst, rsyncOpt = []) {
|
|
|
41
43
|
sshExec(hostInfo, `mkdir -p ${remoteDir}`, debugVerbose);
|
|
42
44
|
}
|
|
43
45
|
debug(`send ${src} to ${hostInfo.host}:${dst}`);
|
|
44
|
-
debugVerbose(`
|
|
45
|
-
return fork(hostInfo, rsyncCmd, args);
|
|
46
|
+
debugVerbose(`send: ${rsyncCmd} ${args.join(" ")}`);
|
|
47
|
+
return retryWrapper(fork.bind(null, hostInfo, rsyncCmd, args), hostInfo);
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
/**
|
|
@@ -55,6 +57,7 @@ async function send(hostInfo, argSrc, dst, rsyncOpt = []) {
|
|
|
55
57
|
* this function execute rsync -av --copy-unsafe-links rsync://[user@]host[:port]/src dst
|
|
56
58
|
*/
|
|
57
59
|
async function recv(hostInfo, argSrc, dst, rsyncOpt = []) {
|
|
60
|
+
await connect(hostInfo);
|
|
58
61
|
const src = `${hostInfo.host}:${argSrc.join(" ")}`;
|
|
59
62
|
const sshArgs = getSshOption(hostInfo, true);
|
|
60
63
|
const rshOpt = ["-e", `${sshCmd} ${sshArgs.join(" ")}`];
|
|
@@ -62,8 +65,8 @@ async function recv(hostInfo, argSrc, dst, rsyncOpt = []) {
|
|
|
62
65
|
const args = ["-avv", "--copy-unsafe-links", ...rshOpt, ...rsyncOpt, src, dst];
|
|
63
66
|
|
|
64
67
|
debug(`recv ${src} to ${dst}`);
|
|
65
|
-
debugVerbose(`
|
|
66
|
-
return fork(hostInfo, rsyncCmd, args);
|
|
68
|
+
debugVerbose(`recv: ${rsyncCmd} ${args.join(" ")}`);
|
|
69
|
+
return retryWrapper(fork.bind(null, hostInfo, rsyncCmd, args), hostInfo);
|
|
67
70
|
}
|
|
68
71
|
module.exports = {
|
|
69
72
|
send,
|
package/lib/sshExec.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const { setTimeout: setTimeoutPromise } = require("timers/promises");
|
|
3
3
|
const crypto = require("crypto");
|
|
4
|
+
const promiseRetry = require("promise-retry");
|
|
4
5
|
const debug = require("debug")("sshClientWrapper:sshExec");
|
|
6
|
+
const debugVerbose = require("debug")("sshClientWrapper:sshExec_verbose");
|
|
5
7
|
const { sshCmd, getSshOption } = require("./util.js");
|
|
6
|
-
const fork = require("./fork.js");
|
|
8
|
+
const { fork, getMasterPty } = require("./fork.js");
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Execute command on remote host.
|
|
@@ -11,12 +13,14 @@ const fork = require("./fork.js");
|
|
|
11
13
|
* @param {string} cmd - Cmdline which will be executed on remote host.
|
|
12
14
|
* @param {Function} outputCallback - Callback routine for stdout and stderr.
|
|
13
15
|
*/
|
|
14
|
-
async function sshExec(hostInfo, cmd, outputCallback
|
|
16
|
+
async function sshExec(hostInfo, cmd, outputCallback) {
|
|
17
|
+
await connect(hostInfo);
|
|
15
18
|
debug("exec", cmd, "on remote server");
|
|
16
|
-
const args = getSshOption(hostInfo);
|
|
19
|
+
const args = getSshOption(hostInfo, false);
|
|
17
20
|
args.push(cmd);
|
|
21
|
+
debugVerbose(`exec: ${sshCmd} ${args.join(" ")}`);
|
|
18
22
|
|
|
19
|
-
return fork(hostInfo, sshCmd, args,
|
|
23
|
+
return retryWrapper(fork.bind(null, hostInfo, sshCmd, args, outputCallback), hostInfo);
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
/**
|
|
@@ -24,7 +28,8 @@ async function sshExec(hostInfo, cmd, outputCallback, opt = null) {
|
|
|
24
28
|
* @param {Object} hostInfo - hostinfo object
|
|
25
29
|
* @param {Integer} timeout - timeout in secconds
|
|
26
30
|
*/
|
|
27
|
-
async function canConnect(hostInfo, timeout =
|
|
31
|
+
async function canConnect(hostInfo, timeout = 60) {
|
|
32
|
+
await connect(hostInfo);
|
|
28
33
|
debug("check connectibity");
|
|
29
34
|
const words = crypto.randomUUID();
|
|
30
35
|
const args = getSshOption(hostInfo);
|
|
@@ -32,7 +37,6 @@ async function canConnect(hostInfo, timeout = 5) {
|
|
|
32
37
|
args.push(`-oConnectTimeout=${timeout}`);
|
|
33
38
|
}
|
|
34
39
|
args.push(`echo ${words}`);
|
|
35
|
-
|
|
36
40
|
let output = "";
|
|
37
41
|
const cb = (e)=>{
|
|
38
42
|
output += e;
|
|
@@ -41,7 +45,7 @@ async function canConnect(hostInfo, timeout = 5) {
|
|
|
41
45
|
const ac = new AbortController();
|
|
42
46
|
const signal = ac.signal;
|
|
43
47
|
|
|
44
|
-
setTimeoutPromise(timeout * 1000, "
|
|
48
|
+
setTimeoutPromise(timeout * 1000, "neverUsedValue", { signal })
|
|
45
49
|
.then(()=>{
|
|
46
50
|
const err = new Error(`can not connect within ${timeout} sec`);
|
|
47
51
|
err.host = hostInfo.host;
|
|
@@ -55,7 +59,8 @@ async function canConnect(hostInfo, timeout = 5) {
|
|
|
55
59
|
}
|
|
56
60
|
});
|
|
57
61
|
|
|
58
|
-
|
|
62
|
+
debugVerbose(`canConnect: ${sshCmd} ${args.join(" ")}`);
|
|
63
|
+
const rt = await retryWrapper(fork.bind(null, hostInfo, sshCmd, args, cb), hostInfo);
|
|
59
64
|
ac.abort();
|
|
60
65
|
|
|
61
66
|
if (rt !== 0) {
|
|
@@ -70,6 +75,68 @@ async function canConnect(hostInfo, timeout = 5) {
|
|
|
70
75
|
return re.test(output);
|
|
71
76
|
}
|
|
72
77
|
|
|
78
|
+
async function existsMaster(hostInfo) {
|
|
79
|
+
const args = getSshOption(hostInfo);
|
|
80
|
+
args.push("-Ocheck");
|
|
81
|
+
|
|
82
|
+
let output = "";
|
|
83
|
+
const cb = (e)=>{
|
|
84
|
+
output += e;
|
|
85
|
+
};
|
|
86
|
+
try {
|
|
87
|
+
debug(`disconnect: ${sshCmd} ${args.join(" ")}`);
|
|
88
|
+
await fork(hostInfo, sshCmd, args, cb);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (err !== 255 || !/Control socket connect.*: No such file or directory/.test(output)) {
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return !/Control socket connect.*: No such file or directory/.test(output);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* create master ssh connection
|
|
99
|
+
*/
|
|
100
|
+
async function connect(hostInfo) {
|
|
101
|
+
if (await existsMaster(hostInfo)) {
|
|
102
|
+
debugVerbose("master connection exists");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
debug(`create master ssh session to ${hostInfo.host}`);
|
|
107
|
+
const pty = hostInfo.masterPty || getMasterPty(hostInfo);
|
|
108
|
+
hostInfo.masterPty = pty;
|
|
109
|
+
|
|
110
|
+
const words = crypto.randomUUID();
|
|
111
|
+
const re = new RegExp(`^${words}`);
|
|
112
|
+
|
|
113
|
+
const args = getSshOption(hostInfo);
|
|
114
|
+
args.push(`echo ${words}`);
|
|
115
|
+
const cmd = `${sshCmd} ${args.join(" ")}\n`;
|
|
116
|
+
pty.write(cmd);
|
|
117
|
+
const p = new Promise((resolve, reject)=>{
|
|
118
|
+
pty.onData((data)=>{
|
|
119
|
+
const output = data.toString();
|
|
120
|
+
if (re.test(output)) {
|
|
121
|
+
debug("call exit on remotehost");
|
|
122
|
+
pty.write("exit\n");
|
|
123
|
+
resolve();
|
|
124
|
+
} else if (/Permission denied \(/.test(output)) {
|
|
125
|
+
reject(new Error("Permission denied"));
|
|
126
|
+
} else if (/Could not resolve hostname/.test(output)) {
|
|
127
|
+
reject(new Error("Could not resolve hostname"));
|
|
128
|
+
} else if (/Bad port/.test(output)) {
|
|
129
|
+
reject(new Error("Bad port"));
|
|
130
|
+
} else if (/Operation|Connection timed out/.test(output)) {
|
|
131
|
+
reject(new Error("Operation timed out"));
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
pty.onExit(reject);
|
|
135
|
+
});
|
|
136
|
+
await p;
|
|
137
|
+
debug(`connected to ${hostInfo.host}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
73
140
|
/**
|
|
74
141
|
* remove master session
|
|
75
142
|
* @param {Object} hostInfo - hostinfo object
|
|
@@ -83,16 +150,47 @@ async function disconnect(hostInfo) {
|
|
|
83
150
|
output += e;
|
|
84
151
|
};
|
|
85
152
|
try {
|
|
86
|
-
|
|
153
|
+
debug(`disconnect: ${sshCmd} ${args.join(" ")}`);
|
|
154
|
+
await fork(hostInfo, sshCmd, args, cb);
|
|
87
155
|
} catch (err) {
|
|
88
156
|
if (err !== 255 || !/Control socket connect.*: No such file or directory/.test(output)) {
|
|
89
157
|
throw err;
|
|
90
158
|
}
|
|
91
159
|
}
|
|
160
|
+
|
|
161
|
+
if (hostInfo.masterPty) {
|
|
162
|
+
debug("exit masterPty");
|
|
163
|
+
hostInfo.masterPty.write("exit \n");
|
|
164
|
+
hostInfo.masterPty = null;
|
|
165
|
+
}
|
|
166
|
+
debug(`disconnected from ${hostInfo.host}`);
|
|
92
167
|
}
|
|
93
168
|
|
|
169
|
+
/**
|
|
170
|
+
* retry fork() if it throw retryable error
|
|
171
|
+
* @param {Function} func - function to be wrapped
|
|
172
|
+
*/
|
|
173
|
+
async function retryWrapper(func, hostInfo) {
|
|
174
|
+
const minTimeout = hostInfo.retryMinTimeout * 1000 || 60000;
|
|
175
|
+
const maxTimeout = hostInfo.retryMaxTimeout * 1000 || 300000;
|
|
176
|
+
const retries = hostInfo.maxRetry || 3;
|
|
177
|
+
return promiseRetry({ retries, minTimeout, maxTimeout }, (retry, number)=>{
|
|
178
|
+
return func()
|
|
179
|
+
.catch((e)=>{
|
|
180
|
+
if (e.retryable) {
|
|
181
|
+
debug(`retrying: ${number} times`);
|
|
182
|
+
disconnect(hostInfo).then(retry);
|
|
183
|
+
}
|
|
184
|
+
throw e;
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
94
190
|
module.exports = {
|
|
95
191
|
sshExec,
|
|
96
192
|
canConnect,
|
|
97
|
-
|
|
193
|
+
connect,
|
|
194
|
+
disconnect,
|
|
195
|
+
retryWrapper
|
|
98
196
|
};
|
package/lib/util.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const { EOL } = require("os");
|
|
3
|
-
|
|
4
3
|
const sshCmd = "ssh";
|
|
5
4
|
const rsyncCmd = "rsync";
|
|
6
5
|
|
|
@@ -48,26 +47,22 @@ const getSshOption = (hostInfo, withoutDestination)=>{
|
|
|
48
47
|
if (hostInfo.noStrictHostkeyChecking) {
|
|
49
48
|
args.push("-oStrictHostKeyChecking=no");
|
|
50
49
|
}
|
|
51
|
-
const
|
|
50
|
+
const ControlPersist = Number.isInteger(hostInfo.ControlPersist) && hostInfo.ControlPersist >= 0 ? hostInfo.ControlPersist : "180";
|
|
52
51
|
args.push("-oControlMaster=auto");
|
|
53
52
|
args.push("-oControlPath=~/.ssh/ssh-client-wrapper-%r@%h:%p");
|
|
54
|
-
args.push(`-oControlPersist=${
|
|
53
|
+
args.push(`-oControlPersist=${ControlPersist}`);
|
|
54
|
+
|
|
55
|
+
if (Number.isInteger(hostInfo.ConnectTimeout) && hostInfo.ConnectTimeout > 0) {
|
|
56
|
+
args.push(`-oConnectTimeout=${hostInfo.ConnectTimeout}`);
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(hostInfo.sshOpt)) {
|
|
59
|
+
args.push(...hostInfo.sshOpt.filter((e)=>{
|
|
60
|
+
return typeof e === "string";
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
55
63
|
return args;
|
|
56
64
|
};
|
|
57
65
|
|
|
58
|
-
/**
|
|
59
|
-
* sleep given time
|
|
60
|
-
* @param {number} time - duration in msec
|
|
61
|
-
*/
|
|
62
|
-
async function sleep(time) {
|
|
63
|
-
return new Promise((resolve)=>{
|
|
64
|
-
setTimeout(()=>{
|
|
65
|
-
resolve();
|
|
66
|
-
}, time);
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
66
|
const isArrayOfString = (target)=>{
|
|
72
67
|
if (!Array.isArray(target)) {
|
|
73
68
|
return false;
|
package/package.json
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ssh-client-wrapper",
|
|
3
|
-
"version": "1.2
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "wrapper library for open ssh client and rsync",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ssh",
|
|
7
|
+
"ssh2",
|
|
8
|
+
"secure",
|
|
9
|
+
"shell",
|
|
10
|
+
"client",
|
|
11
|
+
"wrapper",
|
|
12
|
+
"rsync"
|
|
13
|
+
],
|
|
5
14
|
"main": "lib/index.js",
|
|
6
15
|
"scripts": {
|
|
7
16
|
"test": "mocha --recursive test",
|
|
@@ -29,7 +38,8 @@
|
|
|
29
38
|
"dependencies": {
|
|
30
39
|
"debug": "^4.3.4",
|
|
31
40
|
"glob": "^10.2.2",
|
|
32
|
-
"node-pty": "^0.10.1"
|
|
41
|
+
"node-pty": "^0.10.1",
|
|
42
|
+
"promise-retry": "^2.0.1"
|
|
33
43
|
},
|
|
34
44
|
"devDependencies": {
|
|
35
45
|
"chai": "^4.3.7",
|
package/test/sshExec.js
CHANGED
|
@@ -16,12 +16,12 @@ const sshout = sinon.stub();
|
|
|
16
16
|
const { sshExec, canConnect, disconnect } = require("../lib/sshExec.js");
|
|
17
17
|
|
|
18
18
|
//test helpers
|
|
19
|
-
|
|
19
|
+
let hostInfo = require("./util/hostInfo.js");
|
|
20
20
|
const { nonExisting } = require("./util/testFiles.js");
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
describe("test for ssh execution", function() {
|
|
24
|
-
this.timeout(
|
|
24
|
+
this.timeout(20000);//eslint-disable-line no-invalid-this
|
|
25
25
|
beforeEach(()=>{
|
|
26
26
|
sshout.reset();
|
|
27
27
|
});
|
|
@@ -58,37 +58,44 @@ describe("test for ssh execution", function() {
|
|
|
58
58
|
});
|
|
59
59
|
});
|
|
60
60
|
describe("#canConnect", ()=>{
|
|
61
|
-
let
|
|
61
|
+
let hostInfoBak;
|
|
62
62
|
beforeEach(async ()=>{
|
|
63
|
-
hostInfo2 = { ...hostInfo };
|
|
64
63
|
await disconnect(hostInfo);
|
|
64
|
+
hostInfo.masterPty = null;
|
|
65
|
+
hostInfoBak = { ...hostInfo };
|
|
66
|
+
});
|
|
67
|
+
afterEach(()=>{
|
|
68
|
+
const masterPty = hostInfo.masterPty;
|
|
69
|
+
hostInfo = { ...hostInfoBak };
|
|
70
|
+
hostInfo.masterPty = masterPty;
|
|
65
71
|
});
|
|
66
72
|
it("should be resolved with true", async ()=>{
|
|
67
|
-
expect(await canConnect(hostInfo)).to.be.true;
|
|
73
|
+
expect(await canConnect(hostInfo, 2)).to.be.true;
|
|
68
74
|
});
|
|
69
75
|
it("should be rejected if user does not exist", async ()=>{
|
|
70
|
-
|
|
71
|
-
return expect(canConnect(
|
|
76
|
+
hostInfo.user = "xxxx";
|
|
77
|
+
return expect(canConnect(hostInfo, 2)).to.be.rejected;
|
|
72
78
|
});
|
|
73
79
|
it("should be rejected if password is wrong", async ()=>{
|
|
74
|
-
|
|
75
|
-
return expect(canConnect(
|
|
80
|
+
hostInfo.password = "xxxx";
|
|
81
|
+
return expect(canConnect(hostInfo, 2)).to.be.rejected;
|
|
76
82
|
});
|
|
77
83
|
it("should be rejected if host does not exist", async ()=>{
|
|
78
|
-
|
|
79
|
-
return expect(canConnect(
|
|
84
|
+
hostInfo.host = "foo.bar.example.com";
|
|
85
|
+
return expect(canConnect(hostInfo, 2)).to.be.rejectedWith(255);
|
|
80
86
|
});
|
|
81
87
|
it("should be rejected if host(ip address) does not exist", async ()=>{
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
hostInfo.host = "192.0.2.1";
|
|
89
|
+
hostInfo.ConnectTimeout = 8; //please note each test will be timed out in 20 seconds
|
|
90
|
+
return expect(canConnect(hostInfo, 2)).to.be.rejectedWith(255);
|
|
84
91
|
});
|
|
85
92
|
it("should be rejected if port number is out of range(-1)", async ()=>{
|
|
86
|
-
|
|
87
|
-
return expect(canConnect(
|
|
93
|
+
hostInfo.port = -1;
|
|
94
|
+
return expect(canConnect(hostInfo, 2)).to.be.rejectedWith(255);
|
|
88
95
|
});
|
|
89
96
|
it("should be rejected if port number is out of range(65536)", async ()=>{
|
|
90
|
-
|
|
91
|
-
return expect(canConnect(
|
|
97
|
+
hostInfo.port = 65536;
|
|
98
|
+
return expect(canConnect(hostInfo, 2)).to.be.rejectedWith(255);
|
|
92
99
|
});
|
|
93
100
|
});
|
|
94
101
|
});
|