ssh-client-wrapper 1.8.1 → 1.8.3
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/lib/fork.js +23 -17
- package/lib/index.js +8 -4
- package/lib/rsyncExec.js +2 -2
- package/lib/sshExec.js +27 -21
- package/lib/util.js +30 -3
- package/package.json +2 -2
package/lib/fork.js
CHANGED
|
@@ -1,34 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
const debug = require("debug")("sshClientWrapper:fork");
|
|
3
|
-
const debugVerbose = require("debug")("sshClientWrapper:
|
|
4
|
-
const
|
|
2
|
+
const debug = require("debug")("sshClientWrapper:debug:fork");
|
|
3
|
+
const debugVerbose = require("debug")("sshClientWrapper:verbose:fork");
|
|
4
|
+
const debugMasterVerbose = require("debug")("sshClientWrapper:insecure:fork_masterPty");
|
|
5
|
+
const debugSendPty = require("debug")("sshClientWrapper:insecure:fork_sendpty");
|
|
5
6
|
const { spawn } = require("node-pty");
|
|
6
7
|
|
|
7
|
-
const { rePwPrompt, rePhPrompt, reNewHostPrompt, buff2String } = require("./util.js");
|
|
8
|
+
const { rePwPrompt, rePhPrompt, reNewHostPrompt, buff2String, sendPty } = require("./util.js");
|
|
8
9
|
|
|
9
10
|
const nodePtyOpt = {
|
|
10
11
|
shell: process.platform === "win32" ? process.env.ComSpec : process.env.SHELL,
|
|
11
12
|
windowsHide: true
|
|
12
13
|
};
|
|
13
14
|
|
|
14
|
-
function sshLoginCallback(output, ptyProcess, pw, ph) {
|
|
15
|
+
function sshLoginCallback(output, ptyProcess, pw, ph, logger = debugSendPty) {
|
|
15
16
|
if (reNewHostPrompt.test(output)) {
|
|
16
|
-
|
|
17
|
-
ptyProcess.write("yes\n");
|
|
17
|
+
sendPty(ptyProcess, "yes\n", logger, 17);
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
20
|
if (rePwPrompt.test(output)) {
|
|
21
21
|
if (typeof pw === "string") {
|
|
22
22
|
debug("use given password");
|
|
23
|
-
|
|
24
|
-
ptyProcess.write(`${pw}\n`);
|
|
23
|
+
sendPty(ptyProcess, `${pw}\n`, logger, 23);
|
|
25
24
|
return;
|
|
26
25
|
}
|
|
27
26
|
if (typeof pw === "function") {
|
|
28
27
|
debug("call password callback function");
|
|
29
28
|
pw()
|
|
30
29
|
.then((v)=>{
|
|
31
|
-
ptyProcess
|
|
30
|
+
sendPty(ptyProcess, `${v}\n`, logger, 30);
|
|
32
31
|
});
|
|
33
32
|
return;
|
|
34
33
|
}
|
|
@@ -36,29 +35,36 @@ function sshLoginCallback(output, ptyProcess, pw, ph) {
|
|
|
36
35
|
if (rePhPrompt.test(output)) {
|
|
37
36
|
if (typeof ph === "string") {
|
|
38
37
|
debug("use given passphrase");
|
|
39
|
-
|
|
40
|
-
ptyProcess.write(`${ph}\n`);
|
|
38
|
+
sendPty(ptyProcess, `${ph}\n`, logger, 38);
|
|
41
39
|
return;
|
|
42
40
|
}
|
|
43
41
|
if (typeof ph === "function") {
|
|
44
42
|
debug("call passphrase callback function");
|
|
45
43
|
ph()
|
|
46
44
|
.then((v)=>{
|
|
47
|
-
ptyProcess
|
|
45
|
+
sendPty(ptyProcess, `${v}\n`, logger, 45);
|
|
48
46
|
});
|
|
49
47
|
}
|
|
50
48
|
}
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
|
|
51
|
+
/**
|
|
52
|
+
* create pty for master ssh session and store to hostInfo.masterPty
|
|
53
|
+
*/
|
|
54
|
+
function getMasterPty(hostInfo, logger) {
|
|
54
55
|
debug("fork pty for master connection");
|
|
56
|
+
|
|
57
|
+
if (hostInfo.masterPty) {
|
|
58
|
+
debug(`kill existing master pty for ${hostInfo.host}`);
|
|
59
|
+
hostInfo.masterPty.kill();
|
|
60
|
+
}
|
|
55
61
|
const ptyProcess = spawn(nodePtyOpt.shell, [], nodePtyOpt);
|
|
56
|
-
ptyProcess.onData(buff2String.bind(null,
|
|
62
|
+
ptyProcess.onData(buff2String.bind(null, debugMasterVerbose));
|
|
57
63
|
ptyProcess.onData((data)=>{
|
|
58
64
|
const output = data.toString();
|
|
59
|
-
sshLoginCallback(output, ptyProcess, hostInfo.password, hostInfo.passphrase);
|
|
65
|
+
sshLoginCallback(output, ptyProcess, hostInfo.password, hostInfo.passphrase, logger);
|
|
60
66
|
});
|
|
61
|
-
|
|
67
|
+
hostInfo.masterPty = ptyProcess;
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
async function fork(hostInfo, cmd, args, outputCallback) {
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
const
|
|
3
|
-
const
|
|
2
|
+
const debug = require("debug")("sshClientWrapper:interface");
|
|
3
|
+
const debugVerbose = require("debug")("sshClientWrapper:verbose:interface");
|
|
4
4
|
const { sshExec, canConnect, disconnect, ls } = require("./sshExec.js");
|
|
5
5
|
const { send, recv } = require("./rsyncExec.js");
|
|
6
6
|
const { isArrayOfString, sanityCheck } = require("./util.js");
|
|
@@ -36,6 +36,8 @@ const { isArrayOfString, sanityCheck } = require("./util.js");
|
|
|
36
36
|
*/
|
|
37
37
|
class SshClientWrapper {
|
|
38
38
|
constructor(hostInfo) {
|
|
39
|
+
debug("constructor called for", hostInfo.host);
|
|
40
|
+
debugVerbose("hostInfo=", hostInfo);
|
|
39
41
|
sanityCheck(hostInfo);
|
|
40
42
|
this.hostInfo = { ...hostInfo };
|
|
41
43
|
this.hostInfo.masterPty = null;
|
|
@@ -66,6 +68,7 @@ class SshClientWrapper {
|
|
|
66
68
|
* @returns {string[]} - output from ls
|
|
67
69
|
*/
|
|
68
70
|
async ls(target, lsOpt = []) {
|
|
71
|
+
debug("ls called", target, lsOpt);
|
|
69
72
|
return ls(this.hostInfo, target, lsOpt);
|
|
70
73
|
}
|
|
71
74
|
|
|
@@ -158,7 +161,8 @@ class SshClientWrapper {
|
|
|
158
161
|
* @returns {Promise} - Resolved with true on success, otherwise rejected with Error.
|
|
159
162
|
*/
|
|
160
163
|
async canConnect(timeout = 60) {
|
|
161
|
-
debug("canConnect called timeout=", timeout);
|
|
164
|
+
debug("canConnect called with timeout=", timeout);
|
|
165
|
+
debugVerbose("hostInfo=", this.hostInfo);
|
|
162
166
|
|
|
163
167
|
if (!Number.isInteger(timeout) || timeout <= 0) {
|
|
164
168
|
return new Error("timeout must be positive integer");
|
|
@@ -170,7 +174,7 @@ class SshClientWrapper {
|
|
|
170
174
|
* Disconnect master session
|
|
171
175
|
*/
|
|
172
176
|
async disconnect() {
|
|
173
|
-
debug(
|
|
177
|
+
debug(`disconnect from ${this.hostInfo.host} called`);
|
|
174
178
|
disconnect(this.hostInfo);
|
|
175
179
|
}
|
|
176
180
|
}
|
package/lib/rsyncExec.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const path = require("path");
|
|
3
|
-
const debug = require("debug")("sshClientWrapper:rsyncExec");
|
|
4
|
-
const debugVerbose = require("debug")("sshClientWrapper:
|
|
3
|
+
const debug = require("debug")("sshClientWrapper:debug:rsyncExec");
|
|
4
|
+
const debugVerbose = require("debug")("sshClientWrapper:verbose:rsyncExec");
|
|
5
5
|
const { glob } = require("glob");
|
|
6
6
|
|
|
7
7
|
const { connect, retryWrapper, sshExec } = require("./sshExec.js");
|
package/lib/sshExec.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
const { setTimeout: setTimeoutPromise } = require("timers/promises");
|
|
3
3
|
const crypto = require("crypto");
|
|
4
4
|
const promiseRetry = require("promise-retry");
|
|
5
|
-
const debug = require("debug")("sshClientWrapper:sshExec");
|
|
6
|
-
const debugVerbose = require("debug")("sshClientWrapper:
|
|
7
|
-
const
|
|
5
|
+
const debug = require("debug")("sshClientWrapper:debug:sshExec");
|
|
6
|
+
const debugVerbose = require("debug")("sshClientWrapper:verbose:sshExec");
|
|
7
|
+
const debugSendMasterPty = require("debug")("sshClientWrapper:insecure:sshExec_masterPty");
|
|
8
|
+
const { sendPty, sshCmd, getSshOption, sanityCheck } = require("./util.js");
|
|
8
9
|
const { fork, getMasterPty } = require("./fork.js");
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -64,6 +65,7 @@ async function canConnect(hostInfo, timeout = 60) {
|
|
|
64
65
|
* @returns {boolean} - true if master session exists
|
|
65
66
|
*/
|
|
66
67
|
async function existsMaster(hostInfo) {
|
|
68
|
+
debugVerbose("existsMaster called");
|
|
67
69
|
const ac = new AbortController();
|
|
68
70
|
const signal = ac.signal;
|
|
69
71
|
|
|
@@ -80,6 +82,8 @@ async function existsMaster(hostInfo) {
|
|
|
80
82
|
const timeout = hostInfo.ConnectTimeout || 60;
|
|
81
83
|
setTimeoutPromise(timeout * 1000, "neverUsedValue", { signal })
|
|
82
84
|
.then(()=>{
|
|
85
|
+
debug(`can not finish master session check within ${timeout} sec`);
|
|
86
|
+
|
|
83
87
|
if (p.pty) {
|
|
84
88
|
p.pty.kill();
|
|
85
89
|
}
|
|
@@ -93,17 +97,17 @@ async function existsMaster(hostInfo) {
|
|
|
93
97
|
}
|
|
94
98
|
});
|
|
95
99
|
|
|
96
|
-
|
|
97
100
|
try {
|
|
98
101
|
debug(`check master session: ${sshCmd} ${args.join(" ")}`);
|
|
99
102
|
p = fork(hostInfo, sshCmd, args, cb);
|
|
100
103
|
await p;
|
|
101
|
-
ac.abort();
|
|
102
104
|
} catch (err) {
|
|
103
105
|
if (err.rt !== 255) {
|
|
104
106
|
debug("check master session failed", err);
|
|
105
107
|
throw err;
|
|
106
108
|
}
|
|
109
|
+
} finally {
|
|
110
|
+
ac.abort();
|
|
107
111
|
}
|
|
108
112
|
|
|
109
113
|
return rt && !/Control socket connect.*: No such file or directory/.test(output);
|
|
@@ -118,30 +122,30 @@ async function connect(hostInfo, timeout = 60) {
|
|
|
118
122
|
debugVerbose("master connection exists");
|
|
119
123
|
return;
|
|
120
124
|
}
|
|
121
|
-
debug(`create master ssh session to ${hostInfo.host}`);
|
|
122
125
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
hostInfo.masterPty = getMasterPty(hostInfo);
|
|
126
|
-
} else {
|
|
127
|
-
debug(`master pty for ${hostInfo.host} exists`);
|
|
128
|
-
}
|
|
126
|
+
debug(`create master pty for ${hostInfo.host}`);
|
|
127
|
+
getMasterPty(hostInfo, debugSendMasterPty);
|
|
129
128
|
|
|
129
|
+
debug(`create master ssh session to ${hostInfo.host}`);
|
|
130
130
|
const ac = new AbortController();
|
|
131
131
|
const signal = ac.signal;
|
|
132
132
|
setTimeoutPromise(timeout * 1000, "neverUsedValue", { signal })
|
|
133
133
|
.then(()=>{
|
|
134
|
+
debug(`can not connect within ${timeout} sec`);
|
|
135
|
+
sendPty(hostInfo.masterPty, "\n", debugSendMasterPty, 137);
|
|
136
|
+
sendPty(hostInfo.masterPty, "exit\n", debugSendMasterPty, 138);
|
|
137
|
+
|
|
134
138
|
const err = new Error(`can not connect within ${timeout} sec`);
|
|
135
139
|
err.host = hostInfo.host;
|
|
136
140
|
err.user = hostInfo.user;
|
|
137
141
|
err.port = hostInfo.port;
|
|
138
|
-
hostInfo.masterPty.write("\n");
|
|
139
|
-
hostInfo.masterPty.write("exit\n");
|
|
140
142
|
throw err;
|
|
141
143
|
})
|
|
142
144
|
.catch((err)=>{
|
|
143
145
|
if (err.name === "AbortError") {
|
|
144
146
|
debug("timeout was aborted because connection check has done");
|
|
147
|
+
} else {
|
|
148
|
+
throw err;
|
|
145
149
|
}
|
|
146
150
|
});
|
|
147
151
|
|
|
@@ -159,7 +163,6 @@ async function connect(hostInfo, timeout = 60) {
|
|
|
159
163
|
|
|
160
164
|
if (re.test(output)) {
|
|
161
165
|
debug("test done. call exit on remotehost");
|
|
162
|
-
hostInfo.masterPty.write("exit\n");
|
|
163
166
|
done = true;
|
|
164
167
|
resolve();
|
|
165
168
|
} else if (/Permission denied \(/.test(output)) {
|
|
@@ -181,11 +184,14 @@ async function connect(hostInfo, timeout = 60) {
|
|
|
181
184
|
const args = getSshOption(hostInfo);
|
|
182
185
|
args.push(`echo ${words}`);
|
|
183
186
|
const cmd = `${sshCmd} ${args.join(" ")}\n`;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
sendPty(hostInfo.masterPty, cmd, debugSendMasterPty, 190);
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
await p;
|
|
191
|
+
debug(`connected to ${hostInfo.host}`);
|
|
192
|
+
} finally {
|
|
193
|
+
ac.abort();
|
|
194
|
+
}
|
|
189
195
|
}
|
|
190
196
|
|
|
191
197
|
/**
|
|
@@ -220,7 +226,7 @@ async function disconnect(hostInfo) {
|
|
|
220
226
|
|
|
221
227
|
if (hostInfo.masterPty) {
|
|
222
228
|
debug("exit masterPty");
|
|
223
|
-
hostInfo.masterPty
|
|
229
|
+
sendPty(hostInfo.masterPty, "exit \n", debugSendMasterPty, 228);
|
|
224
230
|
hostInfo.masterPty = null;
|
|
225
231
|
}
|
|
226
232
|
}
|
package/lib/util.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const { EOL } = require("os");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const debug = require("debug")("sshClientWrapper:debug:util");
|
|
5
|
+
|
|
3
6
|
const sshCmd = "ssh";
|
|
4
7
|
const rsyncCmd = "rsync";
|
|
5
8
|
|
|
@@ -62,8 +65,20 @@ const getSshOption = (hostInfo, withoutDestination)=>{
|
|
|
62
65
|
args.push(hostInfo.port);
|
|
63
66
|
}
|
|
64
67
|
if (typeof hostInfo.keyFile === "string") {
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
try {
|
|
69
|
+
const stats = fs.statSync(hostInfo.keyFile);
|
|
70
|
+
if (stats.isFile()) {
|
|
71
|
+
args.push("-i");
|
|
72
|
+
args.push(hostInfo.keyFile);
|
|
73
|
+
} else {
|
|
74
|
+
debug(`specified keyFile(${hostInfo.keyFile}) is not file. so it is ignored`);
|
|
75
|
+
}
|
|
76
|
+
} catch (e) {
|
|
77
|
+
if (e.code !== "ENOENT") {
|
|
78
|
+
throw e;
|
|
79
|
+
}
|
|
80
|
+
debug(`specified keyFile(${hostInfo.keyFile}) not found and ignored`);
|
|
81
|
+
}
|
|
67
82
|
}
|
|
68
83
|
|
|
69
84
|
if (hostInfo.noStrictHostkeyChecking) {
|
|
@@ -95,6 +110,17 @@ const isArrayOfString = (target)=>{
|
|
|
95
110
|
});
|
|
96
111
|
};
|
|
97
112
|
|
|
113
|
+
/**
|
|
114
|
+
* log and send message to pty
|
|
115
|
+
* @param {Object} pty - pty object which message will be send to
|
|
116
|
+
* @param {string} message - message
|
|
117
|
+
* @param {Function} logger - logging function
|
|
118
|
+
* @param {string} label - where this function is called
|
|
119
|
+
*/
|
|
120
|
+
const sendPty = async (pty, message, logger, label)=>{
|
|
121
|
+
logger(`${label}: ${message}`);
|
|
122
|
+
pty.write(message);
|
|
123
|
+
};
|
|
98
124
|
|
|
99
125
|
module.exports = {
|
|
100
126
|
sshCmd,
|
|
@@ -105,5 +131,6 @@ module.exports = {
|
|
|
105
131
|
buff2String,
|
|
106
132
|
getSshOption,
|
|
107
133
|
sanityCheck,
|
|
108
|
-
isArrayOfString
|
|
134
|
+
isArrayOfString,
|
|
135
|
+
sendPty
|
|
109
136
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ssh-client-wrapper",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.3",
|
|
4
4
|
"description": "wrapper library for open ssh client and rsync",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ssh",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"debug": "^4.3.4",
|
|
38
|
-
"glob": "^10.3.
|
|
38
|
+
"glob": "^10.3.3",
|
|
39
39
|
"node-pty": "^0.10.1",
|
|
40
40
|
"nyc": "^15.1.0",
|
|
41
41
|
"promise-retry": "^2.0.1"
|