ssh-client-wrapper 2.11.0 → 2.12.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/lib/sshExec.js CHANGED
@@ -3,20 +3,25 @@ const crypto = require("crypto");
3
3
  const { setTimeout: setTimeoutPromise } = require("timers/promises");
4
4
  const debug = require("debug")("sshClientWrapper:debug:sshExec");
5
5
  const debugVerbose = require("debug")("sshClientWrapper:verbose:sshExec");
6
- const debugSendMasterPty = require("debug")("sshClientWrapperInsecure:sshExec_masterPty");
6
+ const debugSendMasterPty = require("debug")("sshClientWrapper:insecure:sshExec:masterPty");
7
7
  const { sendPty, sshCmd, getSshOption, sanityCheck, watchDogTimer } = require("./util.js");
8
8
  const { fork, createMasterPty, sshLoginCallback } = require("./fork.js");
9
9
 
10
+ /**
11
+ * @typedef Integer {number} - integer number
12
+ */
13
+
10
14
  /**
11
15
  * Execute command on remote host.
12
- * @param {Object} hostInfo - hostinfo object
16
+ * @param {object} hostInfo - hostinfo object
13
17
  * @param {string} cmd - Cmdline which will be executed on remote host.
14
18
  * @param {number} timeout - timeout (in sec.)
19
+ * @param {Function} outputCallback - Callback routine for stdout and stderr.
15
20
  * @param {string} rcfile - rcfile path which will be sourced before executing actual command
16
21
  * @param {string} prependCmd - command string which will be executed before executing actual command
17
- * @param {Function} outputCallback - Callback routine for stdout and stderr.
22
+ * @returns {Integer} - return value of cmd
18
23
  */
19
- async function sshExec (hostInfo, cmd, timeout, outputCallback, rcfile, prependCmd) {
24
+ async function sshExec(hostInfo, cmd, timeout, outputCallback, rcfile, prependCmd) {
20
25
  debug("sshExec called");
21
26
  await connect(hostInfo);
22
27
  debug("exec", cmd, "on remote server");
@@ -44,11 +49,42 @@ async function sshExec (hostInfo, cmd, timeout, outputCallback, rcfile, prependC
44
49
  }
45
50
  if (e.rt === 126) {
46
51
  debug(`${cmd} got Permission deny error but ignored`);
47
- return e.rt;
48
52
  }
49
53
  if (e.rt === 127) {
50
54
  debug(`${cmd} not found but ignored`);
51
- return e.rt;
55
+ }
56
+ return e.rt;
57
+ }
58
+ }
59
+
60
+ async function expect(hostInfo, cmd, expects, timeout) {
61
+ debug("expect called");
62
+ await connect(hostInfo);
63
+ debugVerbose("exec", cmd, "on remote server with", expects);
64
+ const args = getSshOption(hostInfo, false);
65
+ const compiledExpects = expects.map((e)=>{
66
+ return {
67
+ reExpect: new RegExp(e.expect),
68
+ send: e.send,
69
+ keep: e.keep
70
+ };
71
+ });
72
+ const reExpect = hostInfo.prompt ? new RegExp(hostInfo.prompt) : /[$#>] /;
73
+ compiledExpects.unshift({ reExpect, send: cmd });
74
+ compiledExpects.push({ reExpect, send: "exit" });
75
+
76
+ try {
77
+ const rt = await fork(hostInfo, sshCmd, args, timeout, null, [], compiledExpects);
78
+ return rt;
79
+ } catch (e) {
80
+ if (typeof e.rt === "undefined") {
81
+ throw e;
82
+ }
83
+ if (e.rt === 126) {
84
+ debug(`${cmd} got Permission deny error but ignored`);
85
+ }
86
+ if (e.rt === 127) {
87
+ debug(`${cmd} not found but ignored`);
52
88
  }
53
89
  return e.rt;
54
90
  }
@@ -56,32 +92,32 @@ async function sshExec (hostInfo, cmd, timeout, outputCallback, rcfile, prependC
56
92
 
57
93
  /**
58
94
  * Check if you can connect to sepecified server
59
- * @param {Object} hostInfo - hostinfo object
95
+ * @param {object} hostInfo - hostinfo object
60
96
  * @param {Integer} timeout - timeout in secconds
61
97
  */
62
- async function canConnect (hostInfo, timeout = 60) {
98
+ async function canConnect(hostInfo, timeout) {
63
99
  debug("canConnect called");
64
100
  sanityCheck(hostInfo);
65
101
 
66
102
  await connect(hostInfo, timeout);
67
103
  await disconnect(hostInfo);
68
- // if connection fail, Error is throwed.
69
- // so, if we reach here, connection must be succeeded
104
+ //if connection fail, Error is throwed.
105
+ //so, if we reach here, connection must be succeeded
70
106
  return true;
71
107
  }
72
108
 
73
109
  /**
74
110
  * check if master session exists or not
75
- * @param {Object} hostInfo - hostinfo object
111
+ * @param {object} hostInfo - hostinfo object
76
112
  * @returns {boolean} - true if master session exists
77
113
  */
78
- async function existsMaster (hostInfo, argTimeout) {
114
+ async function existsMaster(hostInfo, argTimeout) {
79
115
  debugVerbose("existsMaster called");
80
116
  const args = getSshOption(hostInfo);
81
117
  args.push("-Ocheck");
82
118
 
83
119
  let output = "";
84
- const cb = (e) => {
120
+ const cb = (e)=>{
85
121
  output += e;
86
122
  };
87
123
 
@@ -105,9 +141,9 @@ async function existsMaster (hostInfo, argTimeout) {
105
141
 
106
142
  /**
107
143
  * create master ssh connection
108
- * @param {Object} hostInfo - hostinfo object
144
+ * @param {object} hostInfo - hostinfo object
109
145
  */
110
- async function connect (hostInfo, timeout = 60) {
146
+ async function connect(hostInfo, timeout = 60) {
111
147
  debug("connect called");
112
148
 
113
149
  if (await existsMaster(hostInfo, timeout)) {
@@ -123,8 +159,9 @@ async function connect (hostInfo, timeout = 60) {
123
159
  const re = new RegExp(`^${words}`);
124
160
 
125
161
  let ac = null;
126
- const p = new Promise((resolve, reject) => {
127
- ac = watchDogTimer(timeout, "make connection", () => {
162
+ let output = null;
163
+ const p = new Promise((resolve, reject)=>{
164
+ ac = watchDogTimer(timeout, "make connection", ()=>{
128
165
  sendPty(hostInfo.masterPty, "\n", debugSendMasterPty, 137);
129
166
  sendPty(hostInfo.masterPty, "exit\n", debugSendMasterPty, 138);
130
167
 
@@ -132,16 +169,17 @@ async function connect (hostInfo, timeout = 60) {
132
169
  err.host = hostInfo.host;
133
170
  err.user = hostInfo.user;
134
171
  err.port = hostInfo.port;
172
+ err.output = output;
135
173
  reject(err);
136
174
  });
137
175
 
138
176
  let done = false;
139
- hostInfo.masterPty.onData((data) => {
177
+ hostInfo.masterPty.onData((data)=>{
140
178
  if (done) {
141
- // safeguard
179
+ //safeguard
142
180
  return;
143
181
  }
144
- const output = data.toString();
182
+ output = data.toString();
145
183
  debugVerbose(output);
146
184
 
147
185
  if (re.test(output)) {
@@ -160,12 +198,12 @@ async function connect (hostInfo, timeout = 60) {
160
198
  reject(new Error("Control socket creation failed"));
161
199
  }
162
200
  });
163
- hostInfo.masterPty.onData((data) => {
201
+ hostInfo.masterPty.onData((data)=>{
164
202
  const output = data.toString();
165
203
  sshLoginCallback(output, hostInfo.masterPty, hostInfo.password, hostInfo.passphrase, debugSendMasterPty)
166
204
  .catch(reject);
167
205
  });
168
- hostInfo.masterPty.onExit((e) => {
206
+ hostInfo.masterPty.onExit((e)=>{
169
207
  debug("master pty exit", e);
170
208
  resolve(e);
171
209
  });
@@ -188,9 +226,9 @@ async function connect (hostInfo, timeout = 60) {
188
226
 
189
227
  /**
190
228
  * remove master session
191
- * @param {Object} hostInfo - hostinfo object
229
+ * @param {object} hostInfo - hostinfo object
192
230
  */
193
- async function disconnect (hostInfo) {
231
+ async function disconnect(hostInfo) {
194
232
  debug("disconnect called");
195
233
 
196
234
  if (await existsMaster(hostInfo)) {
@@ -198,7 +236,7 @@ async function disconnect (hostInfo) {
198
236
  args.push("-Oexit");
199
237
 
200
238
  let output = "";
201
- const cb = (e) => {
239
+ const cb = (e)=>{
202
240
  output += e;
203
241
  };
204
242
  try {
@@ -225,50 +263,22 @@ async function disconnect (hostInfo) {
225
263
  }
226
264
  }
227
265
 
228
- /**
229
- * execute ls command and return output
230
- * @param {Object} hostInfo - hostinfo object
231
- * @param {string} target - file or directory path to watch
232
- * @param {string[]} lsOpt - optional arguments for ls
233
- * @param {number} timeout - timeout (in sec.)
234
- * @returns {string[]} - output from ls
235
- */
236
- async function ls (hostInfo, target, lsOpt, timeout) {
237
- debug("ls called");
238
- const outputArray = [];
239
- try {
240
- await sshExec(hostInfo, `ls ${lsOpt.join(" ")} ${target}`, timeout, (data) => {
241
- outputArray.push(data);
242
- });
243
- } catch (e) {
244
- // non-zero retrun should be allowed
245
- if (typeof e.rt === "undefined" || e.rt < 0) {
246
- throw e;
247
- }
248
- }
249
- return outputArray.length === 0
250
- ? []
251
- : outputArray
252
- .join("\n")
253
- .replace(/\n+/g, "\n")
254
- .replace(/\n$/, "")
255
- .split("\n");
256
- }
257
-
258
266
  /**
259
267
  * retry fork() if it throw retryable error
260
268
  * @param {Function} func - function to be wrapped
261
- * @param {Object} hostInfo - hostinfo object
269
+ * @param {object} hostInfo - hostinfo object
262
270
  * @param {Integer} argMaxRetry - max retry number
271
+ * @param {Integer} argDuration - time to wait before retring
272
+ * @returns {Integer} - return value of func
263
273
  */
264
- async function retryWrapper (func, hostInfo, argMaxRetry, argDuration) {
274
+ async function retryWrapper(func, hostInfo, argMaxRetry, argDuration) {
265
275
  const maxRetry = argMaxRetry || hostInfo.maxRetry || 3;
266
276
  const duration = argDuration || hostInfo.retryDuration || 1000;
267
277
  if (maxRetry === 1) {
268
278
  return func();
269
279
  }
270
280
  let retryCount = 0;
271
- while (true) {
281
+ for (;;) {
272
282
  let rt;
273
283
  try {
274
284
  rt = await func();
@@ -297,6 +307,6 @@ module.exports = {
297
307
  canConnect,
298
308
  connect,
299
309
  disconnect,
300
- ls,
310
+ expect,
301
311
  retryWrapper
302
312
  };
package/lib/util.js CHANGED
@@ -26,7 +26,7 @@ const ajv = new Ajv({
26
26
  });
27
27
  require("ajv-keywords")(ajv, "transform");
28
28
 
29
- // never validate password and passphrase value to avoid security insident
29
+ //never validate password and passphrase value to avoid security insident
30
30
  const hostInfoSchema = {
31
31
  type: "object",
32
32
  properties: {
@@ -42,7 +42,11 @@ const hostInfoSchema = {
42
42
  retryDuration: { type: "number", minimum: 0, default: 1000 },
43
43
  rcfile: { type: "string", pattern: "\\S+", transform: ["trim"] },
44
44
  prependCmd: { type: "string", pattern: "\\S+", transform: ["trim"] },
45
- sshOpt: { type: "array", minItems: 1, items: { type: "string", pattern: "\\S+", transform: ["trim"] } }
45
+ sshOpt: {
46
+ type: "array",
47
+ minItems: 1,
48
+ items: { type: "string", pattern: "\\S+", transform: ["trim"] }
49
+ }
46
50
  },
47
51
  required: ["host"]
48
52
  };
@@ -66,20 +70,20 @@ const validate = ajv.compile(hostInfoSchema);
66
70
  * @param {Function} func - function
67
71
  * @param {data} data - binary data which can be converted to string
68
72
  */
69
- const buff2String = (func, data) => {
73
+ const buff2String = (func, data)=>{
70
74
  func(data.toString().replace(/\r\n/g, EOL));
71
75
  };
72
76
 
73
77
  /**
74
78
  * check if given hostInfo object is OK
75
- * @param { Object } hostInfo - host information object defined in index.js
79
+ * @param {object} hostInfo - host information object defined in index.js
76
80
  */
77
- const sanityCheck = (hostInfo) => {
81
+ const sanityCheck = (hostInfo)=>{
78
82
  debug("sanityCheck called", hostInfo);
79
83
 
80
- // keep password, passphrase, and masterPty before validate.
81
- // these properties can have a iellegal value as JSON data
82
- // so we keep them before ajv validation and put them back
84
+ //keep password, passphrase, and masterPty before validate.
85
+ //these properties can have a iellegal value as JSON data
86
+ //so we keep them before ajv validation and put them back
83
87
 
84
88
  const { password, passphrase, masterPty } = hostInfo;
85
89
  validate(hostInfo);
@@ -102,7 +106,7 @@ const sanityCheck = (hostInfo) => {
102
106
  }
103
107
  if (prop.startsWith("sshOpt")) {
104
108
  debug("remove empty member of sshOpt");
105
- hostInfo.sshOpt = hostInfo.sshOpt.filter((opt) => {
109
+ hostInfo.sshOpt = hostInfo.sshOpt.filter((opt)=>{
106
110
  return opt !== "";
107
111
  });
108
112
  continue;
@@ -134,7 +138,7 @@ const sanityCheck = (hostInfo) => {
134
138
  return hostInfo;
135
139
  };
136
140
 
137
- function getControlPersistDir (hostInfo) {
141
+ function getControlPersistDir(hostInfo) {
138
142
  for (const candidate of [hostInfo.ControlPersistDir, process.env.SSH_CONTROL_PERSIST_DIR]) {
139
143
  if (typeof candidate === "string") {
140
144
  const stat = fs.statSync(candidate, { throwIfNoEntry: false });
@@ -148,10 +152,10 @@ function getControlPersistDir (hostInfo) {
148
152
 
149
153
  /**
150
154
  * return ssh option with some hard corded options
151
- * @param { Object} hostInfo - host information object defined in index.js
155
+ * @param {object} hostInfo - host information object defined in index.js
152
156
  * @param {boolean} withoutDestination - return except for hostname
153
157
  */
154
- const getSshOption = (hostInfo, withoutDestination) => {
158
+ const getSshOption = (hostInfo, withoutDestination)=>{
155
159
  debug("getSshOption called");
156
160
  const args = [];
157
161
 
@@ -205,7 +209,7 @@ const getSshOption = (hostInfo, withoutDestination) => {
205
209
  args.push(`-oConnectTimeout=${hostInfo.ConnectTimeout}`);
206
210
  }
207
211
  if (Array.isArray(hostInfo.sshOpt)) {
208
- args.push(...hostInfo.sshOpt.filter((e) => {
212
+ args.push(...hostInfo.sshOpt.filter((e)=>{
209
213
  return typeof e === "string";
210
214
  }));
211
215
  }
@@ -213,24 +217,24 @@ const getSshOption = (hostInfo, withoutDestination) => {
213
217
  return args;
214
218
  };
215
219
 
216
- const isArrayOfString = (target) => {
220
+ const isArrayOfString = (target)=>{
217
221
  if (!Array.isArray(target)) {
218
222
  return false;
219
223
  }
220
- return !target.some((e) => {
221
- // target has non string member
224
+ return !target.some((e)=>{
225
+ //target has non string member
222
226
  return typeof e !== "string";
223
227
  });
224
228
  };
225
229
 
226
230
  /**
227
231
  * log and send message to pty
228
- * @param {Object} pty - pty object which message will be send to
232
+ * @param {object} pty - pty object which message will be send to
229
233
  * @param {string} message - message
230
234
  * @param {Function} logger - logging function
231
235
  * @param {string} label - where this function is called
232
236
  */
233
- const sendPty = async (pty, message, logger, label) => {
237
+ const sendPty = async (pty, message, logger, label)=>{
234
238
  logger(`${label}: ${message}`);
235
239
  pty.write(message);
236
240
  };
@@ -242,15 +246,15 @@ const sendPty = async (pty, message, logger, label) => {
242
246
  * @param {Function} cb - call back function which will be called after timeout
243
247
  * @returns {AbortController} - ac to stop watch dog timer
244
248
  */
245
- const watchDogTimer = (timeout, label, cb) => {
249
+ const watchDogTimer = (timeout, label, cb)=>{
246
250
  const ac = new AbortController();
247
251
  const signal = ac.signal;
248
252
  setTimeoutPromise(timeout * 1000, "neverUsedValue", { signal })
249
- .then(() => {
253
+ .then(()=>{
250
254
  debug(`can not finish ${label} within ${timeout} sec`);
251
255
  return typeof cb === "function" ? cb() : true;
252
256
  })
253
- .catch((err) => {
257
+ .catch((err)=>{
254
258
  if (err.name === "AbortError") {
255
259
  debug(`timeout was aborted because ${label} has finished or canceled`);
256
260
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ssh-client-wrapper",
3
- "version": "2.11.0",
3
+ "version": "2.12.4",
4
4
  "description": "wrapper library for open ssh client and rsync",
5
5
  "keywords": [
6
6
  "ssh",
@@ -14,7 +14,7 @@
14
14
  "main": "lib/index.js",
15
15
  "scripts": {
16
16
  "mocha": "mocha --recursive test",
17
- "lint": "eslint --fix lib test",
17
+ "lint": "eslint --fix ./",
18
18
  "test": "nyc --reporter=html --reporter=text mocha",
19
19
  "coverage": "nyc report --reporter=text-lcov > coverage-report.lcov",
20
20
  "postversion": "git push && git push origin $(git describe --abbrev=0 --tags)",
@@ -34,24 +34,22 @@
34
34
  "*.js": "eslint --fix"
35
35
  },
36
36
  "dependencies": {
37
- "ajv": "^8.13.0",
37
+ "ajv": "^8.17.1",
38
38
  "ajv-keywords": "^5.1.0",
39
- "debug": "^4.3.4",
40
- "glob": "^10.3.12",
39
+ "debug": "^4.4.0",
40
+ "eslint-plugin-chai-friendly": "^1.0.1",
41
+ "glob": "^10.4.5",
41
42
  "node-pty": "^0.10.1",
42
43
  "promise-retry": "^2.0.1"
43
44
  },
44
45
  "devDependencies": {
46
+ "@eslint/js": "^9.19.0",
47
+ "@stylistic/eslint-plugin": "^3.0.1",
45
48
  "chai": "^4.3.7",
46
49
  "chai-as-promised": "^7.1.1",
47
50
  "chai-fs": "^2.0.0",
48
- "eslint": "^8.38.0",
49
- "eslint-config-standard": "^17.1.0",
50
- "eslint-plugin-chai-expect": "^3.0.0",
51
- "eslint-plugin-chai-friendly": "^0.7.4",
52
- "eslint-plugin-import": "^2.29.1",
53
- "eslint-plugin-n": "^16.6.2",
54
- "eslint-plugin-promise": "^6.1.1",
51
+ "eslint-plugin-jsdoc": "^50.6.3",
52
+ "eslint-plugin-node": "^11.1.0",
55
53
  "fs-extra": "^11.1.1",
56
54
  "husky": "^8.0.0",
57
55
  "lint-staged": "^13.2.1",
@@ -1,22 +1,22 @@
1
1
  "use strict";
2
- process.on("unhandledRejection", console.dir); // eslint-disable-line no-console
2
+ process.on("unhandledRejection", console.dir); //eslint-disable-line no-console
3
3
 
4
- // setup test framework
4
+ //setup test framework
5
5
  const chai = require("chai");
6
6
  const { expect } = require("chai");
7
7
  chai.use(require("chai-as-promised"));
8
8
 
9
- // testee
9
+ //testee
10
10
  const { getSshOption } = require("../lib/util.js");
11
11
 
12
- describe("test for getSshOption", () => {
12
+ describe("test for getSshOption", ()=>{
13
13
  const defaultValues = {
14
14
  host: "testHostName",
15
15
  ControlPersist: 180,
16
16
  maxRetry: 3,
17
17
  retryDuration: 1000
18
18
  };
19
- it("should return minimum ssh option array", () => {
19
+ it("should return minimum ssh option array", ()=>{
20
20
  const sshOpts = getSshOption(defaultValues);
21
21
  expect(sshOpts).to.be.an("array");
22
22
  expect(sshOpts).to.have.lengthOf(4);
@@ -25,7 +25,7 @@ describe("test for getSshOption", () => {
25
25
  expect(sshOpts[2]).to.equal("-oControlPath=~/.ssh/ssh-client-wrapper-%r@%h:%p");
26
26
  expect(sshOpts[3]).to.equal("-oControlPersist=180");
27
27
  });
28
- it("should change ControlPath by ControlPersistDir", () => {
28
+ it("should change ControlPath by ControlPersistDir", ()=>{
29
29
  const hostInfo = { ...defaultValues };
30
30
  hostInfo.ControlPersistDir = "/tmp";
31
31
  const sshOpts = getSshOption(hostInfo);
@@ -36,7 +36,7 @@ describe("test for getSshOption", () => {
36
36
  expect(sshOpts[2]).to.equal("-oControlPath=/tmp/ssh-client-wrapper-%r@%h:%p");
37
37
  expect(sshOpts[3]).to.equal("-oControlPersist=180");
38
38
  });
39
- it("should change ControlPath by Environment variable", () => {
39
+ it("should change ControlPath by Environment variable", ()=>{
40
40
  process.env.SSH_CONTROL_PERSIST_DIR = "/tmp";
41
41
  const sshOpts = getSshOption(defaultValues);
42
42
  expect(sshOpts).to.be.an("array");