ssh-client-wrapper 1.3.0 → 1.5.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/.github/workflows/run_test.yml +3 -0
- package/lib/fork.js +58 -37
- package/lib/index.js +19 -9
- package/lib/rsyncExec.js +25 -8
- package/lib/sshExec.js +82 -9
- package/lib/util.js +7 -15
- package/package.json +1 -1
- package/test/sshExec.js +23 -16
|
@@ -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,52 +6,70 @@ const { spawn } = require("node-pty");
|
|
|
6
6
|
|
|
7
7
|
const { rePwPrompt, rePhPrompt, reNewHostPrompt, buff2String } = require("./util.js");
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const nodePtyOpt = {
|
|
10
10
|
shell: process.platform === "win32" ? process.env.ComSpec : process.env.SHELL,
|
|
11
11
|
windowsHide: true
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
}
|
|
17
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 (reNewHostPrompt.test(output)) {
|
|
26
|
-
debugSendPty("yes");
|
|
27
|
-
ptyProcess.write("yes\n");
|
|
28
|
-
}
|
|
29
|
-
if (rePwPrompt.test(output)) {
|
|
30
|
-
if (typeof hostInfo.password === "string") {
|
|
31
|
-
debug("use given password");
|
|
32
|
-
debugSendPty(hostInfo.password);
|
|
33
|
-
ptyProcess.write(`${hostInfo.password}\n`);
|
|
34
|
-
} else if (typeof hostInfo.password === "function") {
|
|
35
|
-
debug("call password callback function");
|
|
36
|
-
hostInfo.password()
|
|
37
|
-
.then((pw)=>{
|
|
38
|
-
ptyProcess.write(`${pw}\n`);
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
if (rePhPrompt.test(output)) {
|
|
43
|
-
if (typeof hostInfo.passphrase === "string") {
|
|
44
|
-
debug("use given passphrase");
|
|
45
|
-
debugSendPty(hostInfo.passphrase);
|
|
46
|
-
ptyProcess.write(`${hostInfo.passphrase}\n`);
|
|
47
|
-
} else if (typeof hostInfo.passphrase === "function") {
|
|
48
|
-
debug("call passphrase callback function");
|
|
49
|
-
hostInfo.passphrase()
|
|
50
|
-
.then((pw)=>{
|
|
51
|
-
ptyProcess.write(`${pw}\n`);
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
73
|
if (/kex_exchange_identification: Connection closed by remote host/.test(output)) {
|
|
56
74
|
ptyProcess.kill();
|
|
57
75
|
const err = new Error("kex_exchange_identification");
|
|
@@ -76,4 +94,7 @@ async function fork(hostInfo, cmd, args, argOpt, outputCallback) {
|
|
|
76
94
|
});
|
|
77
95
|
});
|
|
78
96
|
}
|
|
79
|
-
module.exports =
|
|
97
|
+
module.exports = {
|
|
98
|
+
fork,
|
|
99
|
+
getMasterPty
|
|
100
|
+
};
|
package/lib/index.js
CHANGED
|
@@ -12,25 +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
25
|
* @param {Integer} hostInfo.ControlPersist=180 - how long keep master connection after last client connection has been closed (sec).
|
|
26
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
|
|
27
36
|
*/
|
|
28
37
|
class SshClientWrapper {
|
|
29
38
|
constructor(hostInfo) {
|
|
30
39
|
if (typeof hostInfo.host !== "string") {
|
|
31
40
|
return new Error("hostname must be specified!");
|
|
32
41
|
}
|
|
33
|
-
this.hostInfo = hostInfo;
|
|
42
|
+
this.hostInfo = { ...hostInfo };
|
|
43
|
+
this.hostInfo.masterPty = null;
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
/**
|
|
@@ -79,9 +89,9 @@ class SshClientWrapper {
|
|
|
79
89
|
|
|
80
90
|
/**
|
|
81
91
|
* Send file or directory and its child to server.
|
|
82
|
-
* @param {[
|
|
92
|
+
* @param {string[]} src - File or directory name which to be recieve.
|
|
83
93
|
* @param {string} dst - Destination path.
|
|
84
|
-
* @param {[
|
|
94
|
+
* @param {string[]} opt - option for rsync
|
|
85
95
|
* @returns {Promise} - Resolved with undefined when file transfer is done.
|
|
86
96
|
*/
|
|
87
97
|
async send(src, dst, opt) {
|
|
@@ -107,9 +117,9 @@ class SshClientWrapper {
|
|
|
107
117
|
|
|
108
118
|
/**
|
|
109
119
|
* Get file or directory and its child from server.
|
|
110
|
-
* @param {[
|
|
120
|
+
* @param {string[]} src - File or directory name which to be recieve.
|
|
111
121
|
* @param {string} dst - Destination path.
|
|
112
|
-
* @param {[
|
|
122
|
+
* @param {string[]} opt - option for rsync
|
|
113
123
|
* @returns {Promise} - Resolved with undefined when file transfer is done.
|
|
114
124
|
*/
|
|
115
125
|
async recv(src, dst, opt) {
|
package/lib/rsyncExec.js
CHANGED
|
@@ -4,8 +4,8 @@ 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 { retryWrapper, 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
|
|
|
@@ -20,15 +20,31 @@ const { sshCmd, rsyncCmd, getSshOption } = require("./util.js");
|
|
|
20
20
|
* this function execute rsync -avv --copy-unsafe-links src rsync://[user@]host[:port]/dst
|
|
21
21
|
*/
|
|
22
22
|
async function send(hostInfo, argSrc, dst, rsyncOpt = []) {
|
|
23
|
+
await connect(hostInfo);
|
|
23
24
|
const promises = await Promise.allSettled(argSrc.map((e)=>{
|
|
24
25
|
return glob(e);
|
|
25
26
|
}));
|
|
26
|
-
const src = promises
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
const src = promises
|
|
28
|
+
.map((e, i)=>{
|
|
29
|
+
if (e.status !== "fulfilled") {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
if (argSrc[i].endsWith("/")) {
|
|
33
|
+
return `${e.value}/`;
|
|
34
|
+
}
|
|
35
|
+
return e.value;
|
|
36
|
+
})
|
|
37
|
+
.filter((e)=>{
|
|
38
|
+
return e !== null;
|
|
39
|
+
})
|
|
40
|
+
.flatMap((e)=>{
|
|
41
|
+
return e;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (src.length === 0) {
|
|
45
|
+
debug("src file not found");
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
32
48
|
|
|
33
49
|
const sshArgs = getSshOption(hostInfo, true);
|
|
34
50
|
const rshOpt = ["-e", `${sshCmd} ${sshArgs.join(" ")}`];
|
|
@@ -56,6 +72,7 @@ async function send(hostInfo, argSrc, dst, rsyncOpt = []) {
|
|
|
56
72
|
* this function execute rsync -av --copy-unsafe-links rsync://[user@]host[:port]/src dst
|
|
57
73
|
*/
|
|
58
74
|
async function recv(hostInfo, argSrc, dst, rsyncOpt = []) {
|
|
75
|
+
await connect(hostInfo);
|
|
59
76
|
const src = `${hostInfo.host}:${argSrc.join(" ")}`;
|
|
60
77
|
const sshArgs = getSshOption(hostInfo, true);
|
|
61
78
|
const rshOpt = ["-e", `${sshCmd} ${sshArgs.join(" ")}`];
|
package/lib/sshExec.js
CHANGED
|
@@ -5,7 +5,7 @@ const promiseRetry = require("promise-retry");
|
|
|
5
5
|
const debug = require("debug")("sshClientWrapper:sshExec");
|
|
6
6
|
const debugVerbose = require("debug")("sshClientWrapper:sshExec_verbose");
|
|
7
7
|
const { sshCmd, getSshOption } = require("./util.js");
|
|
8
|
-
const fork = require("./fork.js");
|
|
8
|
+
const { fork, getMasterPty } = require("./fork.js");
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Execute command on remote host.
|
|
@@ -13,13 +13,14 @@ const fork = require("./fork.js");
|
|
|
13
13
|
* @param {string} cmd - Cmdline which will be executed on remote host.
|
|
14
14
|
* @param {Function} outputCallback - Callback routine for stdout and stderr.
|
|
15
15
|
*/
|
|
16
|
-
async function sshExec(hostInfo, cmd, outputCallback
|
|
16
|
+
async function sshExec(hostInfo, cmd, outputCallback) {
|
|
17
|
+
await connect(hostInfo);
|
|
17
18
|
debug("exec", cmd, "on remote server");
|
|
18
|
-
const args = getSshOption(hostInfo);
|
|
19
|
+
const args = getSshOption(hostInfo, false);
|
|
19
20
|
args.push(cmd);
|
|
20
21
|
debugVerbose(`exec: ${sshCmd} ${args.join(" ")}`);
|
|
21
22
|
|
|
22
|
-
return retryWrapper(fork.bind(null, hostInfo, sshCmd, args,
|
|
23
|
+
return retryWrapper(fork.bind(null, hostInfo, sshCmd, args, outputCallback), hostInfo);
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -27,7 +28,8 @@ async function sshExec(hostInfo, cmd, outputCallback, opt = null) {
|
|
|
27
28
|
* @param {Object} hostInfo - hostinfo object
|
|
28
29
|
* @param {Integer} timeout - timeout in secconds
|
|
29
30
|
*/
|
|
30
|
-
async function canConnect(hostInfo, timeout) {
|
|
31
|
+
async function canConnect(hostInfo, timeout = 60) {
|
|
32
|
+
await connect(hostInfo);
|
|
31
33
|
debug("check connectibity");
|
|
32
34
|
const words = crypto.randomUUID();
|
|
33
35
|
const args = getSshOption(hostInfo);
|
|
@@ -35,7 +37,6 @@ async function canConnect(hostInfo, timeout) {
|
|
|
35
37
|
args.push(`-oConnectTimeout=${timeout}`);
|
|
36
38
|
}
|
|
37
39
|
args.push(`echo ${words}`);
|
|
38
|
-
|
|
39
40
|
let output = "";
|
|
40
41
|
const cb = (e)=>{
|
|
41
42
|
output += e;
|
|
@@ -59,7 +60,7 @@ async function canConnect(hostInfo, timeout) {
|
|
|
59
60
|
});
|
|
60
61
|
|
|
61
62
|
debugVerbose(`canConnect: ${sshCmd} ${args.join(" ")}`);
|
|
62
|
-
const rt = await retryWrapper(fork.bind(null, hostInfo, sshCmd, args,
|
|
63
|
+
const rt = await retryWrapper(fork.bind(null, hostInfo, sshCmd, args, cb), hostInfo);
|
|
63
64
|
ac.abort();
|
|
64
65
|
|
|
65
66
|
if (rt !== 0) {
|
|
@@ -74,6 +75,68 @@ async function canConnect(hostInfo, timeout) {
|
|
|
74
75
|
return re.test(output);
|
|
75
76
|
}
|
|
76
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
|
+
|
|
77
140
|
/**
|
|
78
141
|
* remove master session
|
|
79
142
|
* @param {Object} hostInfo - hostinfo object
|
|
@@ -88,12 +151,18 @@ async function disconnect(hostInfo) {
|
|
|
88
151
|
};
|
|
89
152
|
try {
|
|
90
153
|
debug(`disconnect: ${sshCmd} ${args.join(" ")}`);
|
|
91
|
-
await fork(hostInfo, sshCmd, args,
|
|
154
|
+
await fork(hostInfo, sshCmd, args, cb);
|
|
92
155
|
} catch (err) {
|
|
93
156
|
if (err !== 255 || !/Control socket connect.*: No such file or directory/.test(output)) {
|
|
94
157
|
throw err;
|
|
95
158
|
}
|
|
96
159
|
}
|
|
160
|
+
|
|
161
|
+
if (hostInfo.masterPty) {
|
|
162
|
+
debug("exit masterPty");
|
|
163
|
+
hostInfo.masterPty.write("exit \n");
|
|
164
|
+
hostInfo.masterPty = null;
|
|
165
|
+
}
|
|
97
166
|
debug(`disconnected from ${hostInfo.host}`);
|
|
98
167
|
}
|
|
99
168
|
|
|
@@ -102,7 +171,10 @@ async function disconnect(hostInfo) {
|
|
|
102
171
|
* @param {Function} func - function to be wrapped
|
|
103
172
|
*/
|
|
104
173
|
async function retryWrapper(func, hostInfo) {
|
|
105
|
-
|
|
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)=>{
|
|
106
178
|
return func()
|
|
107
179
|
.catch((e)=>{
|
|
108
180
|
if (e.retryable) {
|
|
@@ -118,6 +190,7 @@ async function retryWrapper(func, hostInfo) {
|
|
|
118
190
|
module.exports = {
|
|
119
191
|
sshExec,
|
|
120
192
|
canConnect,
|
|
193
|
+
connect,
|
|
121
194
|
disconnect,
|
|
122
195
|
retryWrapper
|
|
123
196
|
};
|
package/lib/util.js
CHANGED
|
@@ -52,25 +52,17 @@ const getSshOption = (hostInfo, withoutDestination)=>{
|
|
|
52
52
|
args.push("-oControlPath=~/.ssh/ssh-client-wrapper-%r@%h:%p");
|
|
53
53
|
args.push(`-oControlPersist=${ControlPersist}`);
|
|
54
54
|
|
|
55
|
-
if (Number.isInteger(hostInfo.
|
|
56
|
-
args.push(`-oConnectTimeout=${hostInfo.
|
|
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
|
+
}));
|
|
57
62
|
}
|
|
58
63
|
return args;
|
|
59
64
|
};
|
|
60
65
|
|
|
61
|
-
/**
|
|
62
|
-
* sleep given time
|
|
63
|
-
* @param {number} time - duration in msec
|
|
64
|
-
*/
|
|
65
|
-
async function sleep(time) {
|
|
66
|
-
return new Promise((resolve)=>{
|
|
67
|
-
setTimeout(()=>{
|
|
68
|
-
resolve();
|
|
69
|
-
}, time);
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
66
|
const isArrayOfString = (target)=>{
|
|
75
67
|
if (!Array.isArray(target)) {
|
|
76
68
|
return false;
|
package/package.json
CHANGED
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
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
|
});
|