ssh-client-wrapper 2.10.2 → 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,22 +3,40 @@ 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.)
15
19
  * @param {Function} outputCallback - Callback routine for stdout and stderr.
20
+ * @param {string} rcfile - rcfile path which will be sourced before executing actual command
21
+ * @param {string} prependCmd - command string which will be executed before executing actual command
22
+ * @returns {Integer} - return value of cmd
16
23
  */
17
- async function sshExec (hostInfo, cmd, timeout, outputCallback) {
24
+ async function sshExec(hostInfo, cmd, timeout, outputCallback, rcfile, prependCmd) {
18
25
  debug("sshExec called");
19
26
  await connect(hostInfo);
20
27
  debug("exec", cmd, "on remote server");
21
28
  const args = getSshOption(hostInfo, false);
29
+ if (rcfile) {
30
+ args.push(`. ${rcfile};`);
31
+ } else if (hostInfo.rcfile) {
32
+ args.push(`. ${hostInfo.rcfile};`);
33
+ }
34
+ if (prependCmd) {
35
+ args.push(`${prependCmd};`);
36
+ } else if (hostInfo.prependCmd) {
37
+ args.push(`${hostInfo.prependCmd};`);
38
+ }
39
+
22
40
  args.push(cmd);
23
41
  debugVerbose(`exec: ${sshCmd} ${args.join(" ")}`);
24
42
 
@@ -31,11 +49,42 @@ async function sshExec (hostInfo, cmd, timeout, outputCallback) {
31
49
  }
32
50
  if (e.rt === 126) {
33
51
  debug(`${cmd} got Permission deny error but ignored`);
34
- return e.rt;
35
52
  }
36
53
  if (e.rt === 127) {
37
54
  debug(`${cmd} not found but ignored`);
38
- 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`);
39
88
  }
40
89
  return e.rt;
41
90
  }
@@ -43,32 +92,32 @@ async function sshExec (hostInfo, cmd, timeout, outputCallback) {
43
92
 
44
93
  /**
45
94
  * Check if you can connect to sepecified server
46
- * @param {Object} hostInfo - hostinfo object
95
+ * @param {object} hostInfo - hostinfo object
47
96
  * @param {Integer} timeout - timeout in secconds
48
97
  */
49
- async function canConnect (hostInfo, timeout = 60) {
98
+ async function canConnect(hostInfo, timeout) {
50
99
  debug("canConnect called");
51
100
  sanityCheck(hostInfo);
52
101
 
53
102
  await connect(hostInfo, timeout);
54
103
  await disconnect(hostInfo);
55
- // if connection fail, Error is throwed.
56
- // 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
57
106
  return true;
58
107
  }
59
108
 
60
109
  /**
61
110
  * check if master session exists or not
62
- * @param {Object} hostInfo - hostinfo object
111
+ * @param {object} hostInfo - hostinfo object
63
112
  * @returns {boolean} - true if master session exists
64
113
  */
65
- async function existsMaster (hostInfo, argTimeout) {
114
+ async function existsMaster(hostInfo, argTimeout) {
66
115
  debugVerbose("existsMaster called");
67
116
  const args = getSshOption(hostInfo);
68
117
  args.push("-Ocheck");
69
118
 
70
119
  let output = "";
71
- const cb = (e) => {
120
+ const cb = (e)=>{
72
121
  output += e;
73
122
  };
74
123
 
@@ -92,9 +141,9 @@ async function existsMaster (hostInfo, argTimeout) {
92
141
 
93
142
  /**
94
143
  * create master ssh connection
95
- * @param {Object} hostInfo - hostinfo object
144
+ * @param {object} hostInfo - hostinfo object
96
145
  */
97
- async function connect (hostInfo, timeout = 60) {
146
+ async function connect(hostInfo, timeout = 60) {
98
147
  debug("connect called");
99
148
 
100
149
  if (await existsMaster(hostInfo, timeout)) {
@@ -110,8 +159,9 @@ async function connect (hostInfo, timeout = 60) {
110
159
  const re = new RegExp(`^${words}`);
111
160
 
112
161
  let ac = null;
113
- const p = new Promise((resolve, reject) => {
114
- ac = watchDogTimer(timeout, "make connection", () => {
162
+ let output = null;
163
+ const p = new Promise((resolve, reject)=>{
164
+ ac = watchDogTimer(timeout, "make connection", ()=>{
115
165
  sendPty(hostInfo.masterPty, "\n", debugSendMasterPty, 137);
116
166
  sendPty(hostInfo.masterPty, "exit\n", debugSendMasterPty, 138);
117
167
 
@@ -119,16 +169,17 @@ async function connect (hostInfo, timeout = 60) {
119
169
  err.host = hostInfo.host;
120
170
  err.user = hostInfo.user;
121
171
  err.port = hostInfo.port;
172
+ err.output = output;
122
173
  reject(err);
123
174
  });
124
175
 
125
176
  let done = false;
126
- hostInfo.masterPty.onData((data) => {
177
+ hostInfo.masterPty.onData((data)=>{
127
178
  if (done) {
128
- // safeguard
179
+ //safeguard
129
180
  return;
130
181
  }
131
- const output = data.toString();
182
+ output = data.toString();
132
183
  debugVerbose(output);
133
184
 
134
185
  if (re.test(output)) {
@@ -147,12 +198,12 @@ async function connect (hostInfo, timeout = 60) {
147
198
  reject(new Error("Control socket creation failed"));
148
199
  }
149
200
  });
150
- hostInfo.masterPty.onData((data) => {
201
+ hostInfo.masterPty.onData((data)=>{
151
202
  const output = data.toString();
152
203
  sshLoginCallback(output, hostInfo.masterPty, hostInfo.password, hostInfo.passphrase, debugSendMasterPty)
153
204
  .catch(reject);
154
205
  });
155
- hostInfo.masterPty.onExit((e) => {
206
+ hostInfo.masterPty.onExit((e)=>{
156
207
  debug("master pty exit", e);
157
208
  resolve(e);
158
209
  });
@@ -175,9 +226,9 @@ async function connect (hostInfo, timeout = 60) {
175
226
 
176
227
  /**
177
228
  * remove master session
178
- * @param {Object} hostInfo - hostinfo object
229
+ * @param {object} hostInfo - hostinfo object
179
230
  */
180
- async function disconnect (hostInfo) {
231
+ async function disconnect(hostInfo) {
181
232
  debug("disconnect called");
182
233
 
183
234
  if (await existsMaster(hostInfo)) {
@@ -185,7 +236,7 @@ async function disconnect (hostInfo) {
185
236
  args.push("-Oexit");
186
237
 
187
238
  let output = "";
188
- const cb = (e) => {
239
+ const cb = (e)=>{
189
240
  output += e;
190
241
  };
191
242
  try {
@@ -212,50 +263,22 @@ async function disconnect (hostInfo) {
212
263
  }
213
264
  }
214
265
 
215
- /**
216
- * execute ls command and return output
217
- * @param {Object} hostInfo - hostinfo object
218
- * @param {string} target - file or directory path to watch
219
- * @param {string[]} lsOpt - optional arguments for ls
220
- * @param {number} timeout - timeout (in sec.)
221
- * @returns {string[]} - output from ls
222
- */
223
- async function ls (hostInfo, target, lsOpt, timeout) {
224
- debug("ls called");
225
- const outputArray = [];
226
- try {
227
- await sshExec(hostInfo, `ls ${lsOpt.join(" ")} ${target}`, timeout, (data) => {
228
- outputArray.push(data);
229
- });
230
- } catch (e) {
231
- // non-zero retrun should be allowed
232
- if (typeof e.rt === "undefined" || e.rt < 0) {
233
- throw e;
234
- }
235
- }
236
- return outputArray.length === 0
237
- ? []
238
- : outputArray
239
- .join("\n")
240
- .replace(/\n+/g, "\n")
241
- .replace(/\n$/, "")
242
- .split("\n");
243
- }
244
-
245
266
  /**
246
267
  * retry fork() if it throw retryable error
247
268
  * @param {Function} func - function to be wrapped
248
- * @param {Object} hostInfo - hostinfo object
269
+ * @param {object} hostInfo - hostinfo object
249
270
  * @param {Integer} argMaxRetry - max retry number
271
+ * @param {Integer} argDuration - time to wait before retring
272
+ * @returns {Integer} - return value of func
250
273
  */
251
- async function retryWrapper (func, hostInfo, argMaxRetry, argDuration) {
274
+ async function retryWrapper(func, hostInfo, argMaxRetry, argDuration) {
252
275
  const maxRetry = argMaxRetry || hostInfo.maxRetry || 3;
253
276
  const duration = argDuration || hostInfo.retryDuration || 1000;
254
277
  if (maxRetry === 1) {
255
278
  return func();
256
279
  }
257
280
  let retryCount = 0;
258
- while (true) {
281
+ for (;;) {
259
282
  let rt;
260
283
  try {
261
284
  rt = await func();
@@ -284,6 +307,6 @@ module.exports = {
284
307
  canConnect,
285
308
  connect,
286
309
  disconnect,
287
- ls,
310
+ expect,
288
311
  retryWrapper
289
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: {
@@ -40,7 +40,13 @@ const hostInfoSchema = {
40
40
  ControlPersistDir: { type: "string", pattern: "\\S+", transform: ["trim"] },
41
41
  maxRetry: { type: "number", minimum: 0, default: 3 },
42
42
  retryDuration: { type: "number", minimum: 0, default: 1000 },
43
- sshOpt: { type: "array", minItems: 1, items: { type: "string", pattern: "\\S+", transform: ["trim"] } }
43
+ rcfile: { type: "string", pattern: "\\S+", transform: ["trim"] },
44
+ prependCmd: { type: "string", pattern: "\\S+", transform: ["trim"] },
45
+ sshOpt: {
46
+ type: "array",
47
+ minItems: 1,
48
+ items: { type: "string", pattern: "\\S+", transform: ["trim"] }
49
+ }
44
50
  },
45
51
  required: ["host"]
46
52
  };
@@ -64,20 +70,20 @@ const validate = ajv.compile(hostInfoSchema);
64
70
  * @param {Function} func - function
65
71
  * @param {data} data - binary data which can be converted to string
66
72
  */
67
- const buff2String = (func, data) => {
73
+ const buff2String = (func, data)=>{
68
74
  func(data.toString().replace(/\r\n/g, EOL));
69
75
  };
70
76
 
71
77
  /**
72
78
  * check if given hostInfo object is OK
73
- * @param { Object } hostInfo - host information object defined in index.js
79
+ * @param {object} hostInfo - host information object defined in index.js
74
80
  */
75
- const sanityCheck = (hostInfo) => {
81
+ const sanityCheck = (hostInfo)=>{
76
82
  debug("sanityCheck called", hostInfo);
77
83
 
78
- // keep password, passphrase, and masterPty before validate.
79
- // these properties can have a iellegal value as JSON data
80
- // 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
81
87
 
82
88
  const { password, passphrase, masterPty } = hostInfo;
83
89
  validate(hostInfo);
@@ -100,7 +106,7 @@ const sanityCheck = (hostInfo) => {
100
106
  }
101
107
  if (prop.startsWith("sshOpt")) {
102
108
  debug("remove empty member of sshOpt");
103
- hostInfo.sshOpt = hostInfo.sshOpt.filter((opt) => {
109
+ hostInfo.sshOpt = hostInfo.sshOpt.filter((opt)=>{
104
110
  return opt !== "";
105
111
  });
106
112
  continue;
@@ -132,7 +138,7 @@ const sanityCheck = (hostInfo) => {
132
138
  return hostInfo;
133
139
  };
134
140
 
135
- function getControlPersistDir (hostInfo) {
141
+ function getControlPersistDir(hostInfo) {
136
142
  for (const candidate of [hostInfo.ControlPersistDir, process.env.SSH_CONTROL_PERSIST_DIR]) {
137
143
  if (typeof candidate === "string") {
138
144
  const stat = fs.statSync(candidate, { throwIfNoEntry: false });
@@ -146,10 +152,10 @@ function getControlPersistDir (hostInfo) {
146
152
 
147
153
  /**
148
154
  * return ssh option with some hard corded options
149
- * @param { Object} hostInfo - host information object defined in index.js
155
+ * @param {object} hostInfo - host information object defined in index.js
150
156
  * @param {boolean} withoutDestination - return except for hostname
151
157
  */
152
- const getSshOption = (hostInfo, withoutDestination) => {
158
+ const getSshOption = (hostInfo, withoutDestination)=>{
153
159
  debug("getSshOption called");
154
160
  const args = [];
155
161
 
@@ -203,31 +209,32 @@ const getSshOption = (hostInfo, withoutDestination) => {
203
209
  args.push(`-oConnectTimeout=${hostInfo.ConnectTimeout}`);
204
210
  }
205
211
  if (Array.isArray(hostInfo.sshOpt)) {
206
- args.push(...hostInfo.sshOpt.filter((e) => {
212
+ args.push(...hostInfo.sshOpt.filter((e)=>{
207
213
  return typeof e === "string";
208
214
  }));
209
215
  }
216
+
210
217
  return args;
211
218
  };
212
219
 
213
- const isArrayOfString = (target) => {
220
+ const isArrayOfString = (target)=>{
214
221
  if (!Array.isArray(target)) {
215
222
  return false;
216
223
  }
217
- return !target.some((e) => {
218
- // target has non string member
224
+ return !target.some((e)=>{
225
+ //target has non string member
219
226
  return typeof e !== "string";
220
227
  });
221
228
  };
222
229
 
223
230
  /**
224
231
  * log and send message to pty
225
- * @param {Object} pty - pty object which message will be send to
232
+ * @param {object} pty - pty object which message will be send to
226
233
  * @param {string} message - message
227
234
  * @param {Function} logger - logging function
228
235
  * @param {string} label - where this function is called
229
236
  */
230
- const sendPty = async (pty, message, logger, label) => {
237
+ const sendPty = async (pty, message, logger, label)=>{
231
238
  logger(`${label}: ${message}`);
232
239
  pty.write(message);
233
240
  };
@@ -239,15 +246,15 @@ const sendPty = async (pty, message, logger, label) => {
239
246
  * @param {Function} cb - call back function which will be called after timeout
240
247
  * @returns {AbortController} - ac to stop watch dog timer
241
248
  */
242
- const watchDogTimer = (timeout, label, cb) => {
249
+ const watchDogTimer = (timeout, label, cb)=>{
243
250
  const ac = new AbortController();
244
251
  const signal = ac.signal;
245
252
  setTimeoutPromise(timeout * 1000, "neverUsedValue", { signal })
246
- .then(() => {
253
+ .then(()=>{
247
254
  debug(`can not finish ${label} within ${timeout} sec`);
248
255
  return typeof cb === "function" ? cb() : true;
249
256
  })
250
- .catch((err) => {
257
+ .catch((err)=>{
251
258
  if (err.name === "AbortError") {
252
259
  debug(`timeout was aborted because ${label} has finished or canceled`);
253
260
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ssh-client-wrapper",
3
- "version": "2.10.2",
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",
package/run_test_all.sh CHANGED
@@ -1,21 +1,11 @@
1
1
  #!/bin/bash
2
-
3
2
  function cleanup(){
4
- docker compose rm --stop --force testbed
3
+ docker compose down
5
4
  }
6
-
7
5
  trap cleanup EXIT
8
6
  set -e -o pipefail
9
7
 
10
- docker compose up testbed -d
11
- export TEST_HOST=localhost
12
- export TEST_USER=testuser
13
- export TEST_PW=passw0rd
14
- export TEST_PORT=4100
15
- ssh-keygen -R '[localhost]:4100'
16
-
17
- npm run test
18
- echo local test success
8
+ ./run_test_local.sh keep
19
9
 
20
10
  docker compose run --build --rm tester
21
11
  echo test on latest ubuntu success
@@ -25,3 +15,4 @@ echo test on bullseye success
25
15
 
26
16
  docker compose run --build --rm tester-buster
27
17
  echo test on buster success
18
+ docker compose down
@@ -0,0 +1,13 @@
1
+ #!/bin/bash
2
+ docker compose up testbed -d
3
+ export TEST_HOST=localhost
4
+ export TEST_USER=testuser
5
+ export TEST_PW=passw0rd
6
+ export TEST_PORT=4100
7
+ ssh-keygen -R '[localhost]:4100'
8
+
9
+ npm run test
10
+ echo local test success
11
+ if [ x${1} != xkeep ];then
12
+ docker compose down
13
+ fi
@@ -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");