ssh-client-wrapper 2.0.1 → 2.1.1

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/.dockerignore ADDED
@@ -0,0 +1 @@
1
+ node_modules
@@ -4,6 +4,8 @@ on:
4
4
  tags:
5
5
  - 'v*'
6
6
  jobs:
7
+ test:
8
+ uses: ./.github/workflows/run_test.yml
7
9
  coverage:
8
10
  needs:
9
11
  - test
package/Dockerfile ADDED
@@ -0,0 +1,10 @@
1
+ # syntax=docker/dockerfile:1
2
+ #build WHEEL client code
3
+ FROM --platform=linux/amd64 node:hydrogen-bookworm-slim
4
+ #FROM --platform=linux/amd64 node:hydrogen-bullseye-slim
5
+ #FROM --platform=linux/amd64 node:hydrogen-buster-slim
6
+
7
+ WORKDIR /work
8
+ RUN apt-get update && apt -y install ssh rsync bzip2 python3 g++ build-essential
9
+ COPY ./ ssh-client-wrapper
10
+ RUN cd ssh-client-wrapper && npm install
@@ -0,0 +1,17 @@
1
+ version: "3.8"
2
+ services:
3
+ test-bed:
4
+ image: naoso5/test-sshd
5
+ ports:
6
+ - 4000:22
7
+ container_name: testbed
8
+ tester:
9
+ build:
10
+ context: .
11
+ dockerfile: Dockerfile
12
+ environment:
13
+ TEST_USER: testuser
14
+ TEST_PW: passw0rd
15
+ TEST_HOST: testbed
16
+ working_dir: /work/ssh-client-wrapper
17
+ command: bash -c "mkdir /root/.ssh 2>/dev/null; npm run test"
package/lib/fork.js CHANGED
@@ -75,21 +75,26 @@ async function fork(hostInfo, cmd, args, timeout, outputCallback) {
75
75
  if (timeout > 0) {
76
76
  ac = watchDogTimer(timeout, cmd, ()=>{
77
77
  ptyProcess.kill();
78
- reject(new Error(`watchdog timer expired ${timeout}`));
78
+ const err = new Error(`watchdog timer expired ${timeout}`);
79
+ err.code = "TIMEEXPIRE";
80
+ reject(err);
79
81
  });
80
82
  }
81
83
  ptyProcess.onData(buff2String.bind(null, debugVerbose));
82
- ptyProcess.onData((data)=>{
83
- const output = data.toString();
84
- sshLoginCallback(output, ptyProcess, hostInfo.password, hostInfo.passphrase);
85
84
 
86
- if (/kex_exchange_identification: Connection closed by remote host/.test(output)) {
87
- ptyProcess.kill();
88
- const err = new Error("kex_exchange_identification");
89
- err.retryable = true;
90
- reject(err);
91
- }
92
- });
85
+ if (hostInfo !== null) {
86
+ ptyProcess.onData((data)=>{
87
+ const output = data.toString();
88
+ sshLoginCallback(output, ptyProcess, hostInfo.password, hostInfo.passphrase);
89
+
90
+ if (/kex_exchange_identification: Connection closed by remote host/.test(output)) {
91
+ ptyProcess.kill();
92
+ const err = new Error("kex_exchange_identification");
93
+ err.retryable = true;
94
+ reject(err);
95
+ }
96
+ });
97
+ }
93
98
 
94
99
  if (typeof outputCallback === "function") {
95
100
  ptyProcess.onData(buff2String.bind(null, outputCallback));
package/lib/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  "use strict";
2
+ const fs = require("fs");
3
+ const path = require("path");
2
4
  const debug = require("debug")("sshClientWrapper:interface");
3
5
  const debugVerbose = require("debug")("sshClientWrapper:verbose:interface");
4
6
  const { sshExec, canConnect, disconnect, ls } = require("./sshExec.js");
@@ -46,6 +48,17 @@ class SshClientWrapper {
46
48
  sanityCheck(hostInfo);
47
49
  this.hostInfo = { ...hostInfo };
48
50
  this.hostInfo.masterPty = null;
51
+ this.hostInfo.rsyncVersion = null;
52
+
53
+ if (process.env.HOME) {
54
+ try {
55
+ fs.mkdirSync(path.resolve(process.env.HOME, ".ssh"));
56
+ } catch (e) {
57
+ if (e.code !== "EEXIST") {
58
+ throw e;
59
+ }
60
+ }
61
+ }
49
62
  }
50
63
 
51
64
  /**
package/lib/rsyncExec.js CHANGED
@@ -9,6 +9,19 @@ const { fork } = require("./fork.js");
9
9
 
10
10
  const { sshCmd, rsyncCmd, getSshOption } = require("./util.js");
11
11
 
12
+ async function checkRsyncVersion(timeout) {
13
+ const rt = {};
14
+ await fork(null, rsyncCmd, ["--version"], timeout, (output)=>{
15
+ const result = /^rsync *version *(\d+)\.(\d+)\.(\d+).*$/m.exec(output);
16
+
17
+ if (result) {
18
+ rt.major = Number(result[1]);
19
+ rt.minor = Number(result[2]);
20
+ rt.patch = Number(result[3]);
21
+ }
22
+ });
23
+ return rt;
24
+ }
12
25
 
13
26
  /**
14
27
  * Send file or directory and its child to server.
@@ -69,6 +82,21 @@ async function send(hostInfo, argSrc, dst, rsyncOpt, timeout) {
69
82
  return retryWrapper(fork.bind(null, hostInfo, rsyncCmd, args, timeout), hostInfo);
70
83
  }
71
84
 
85
+ /**
86
+ * format src array to src string for recv
87
+ * @param {Object} hostInfo - hostinfo object
88
+ * @returns {boolean} - rsync command version is higher than 3.2.4 or not
89
+ */
90
+ async function isNewRsync(hostInfo) {
91
+ if (!hostInfo.rsyncVersion ||
92
+ !Number.isInteger(hostInfo.rsyncVersion.major) ||
93
+ !Number.isInteger(hostInfo.rsyncVersion.minor) ||
94
+ !Number.isInteger(hostInfo.rsyncVersion.patch)) {
95
+ hostInfo.rsyncVersion = await checkRsyncVersion(3);
96
+ }
97
+ return hostInfo.rsyncVersion.major >= 3 && hostInfo.rsyncVersion.minor >= 2 && hostInfo.rsyncVersion.patch >= 4;
98
+ }
99
+
72
100
  /**
73
101
  * Send file or directory and its child to server.
74
102
  * @param {Object} hostInfo - hostinfo object
@@ -84,14 +112,17 @@ async function recv(hostInfo, argSrc, dst, rsyncOpt, timeout) {
84
112
  const src = `${hostInfo.host}:${argSrc.join(" ")}`;
85
113
  const sshArgs = getSshOption(hostInfo, true);
86
114
  const rshOpt = ["-e", `${sshCmd} ${sshArgs.join(" ")}`];
87
-
88
115
  const args = ["-avv", "--copy-unsafe-links", ...rshOpt, ...rsyncOpt, src, dst];
116
+ if (await isNewRsync(hostInfo)) {
117
+ args.unshift("--old-args");
118
+ }
89
119
 
90
120
  debug(`recv ${src} to ${dst}`);
91
121
  debugVerbose(`recv: ${rsyncCmd} ${args.join(" ")}`);
92
122
  return retryWrapper(fork.bind(null, hostInfo, rsyncCmd, args, timeout), hostInfo);
93
123
  }
94
124
  module.exports = {
125
+ checkRsyncVersion,
95
126
  send,
96
127
  recv
97
128
  };
package/lib/sshExec.js CHANGED
@@ -64,7 +64,7 @@ async function canConnect(hostInfo, timeout = 60) {
64
64
  * @param {Object} hostInfo - hostinfo object
65
65
  * @returns {boolean} - true if master session exists
66
66
  */
67
- async function existsMaster(hostInfo) {
67
+ async function existsMaster(hostInfo, argTimeout) {
68
68
  debugVerbose("existsMaster called");
69
69
  const args = getSshOption(hostInfo);
70
70
  args.push("-Ocheck");
@@ -74,20 +74,22 @@ async function existsMaster(hostInfo) {
74
74
  output += e;
75
75
  };
76
76
 
77
- const rt = true; //if timeout occurred during ssh -Ocheck, it will turn to false
78
- const timeout = hostInfo.ConnectTimeout || 60;
77
+ const timeout = argTimeout || hostInfo.ConnectTimeout || 60;
79
78
  debug(`check master session: ${sshCmd} ${args.join(" ")}`);
80
79
 
81
80
  try {
82
81
  await fork(hostInfo, sshCmd, args, timeout, cb);
83
82
  } catch (err) {
83
+ if (err.code === "TIMEEXPIRE") {
84
+ return false;
85
+ }
84
86
  if (err.rt !== 255) {
85
87
  debug("check master session failed", err);
86
88
  throw err;
87
89
  }
88
90
  }
89
91
 
90
- return rt && !/Control socket connect.*: No such file or directory/.test(output);
92
+ return !/Control socket connect.*: No such file or directory/.test(output);
91
93
  }
92
94
 
93
95
  /**
@@ -95,7 +97,7 @@ async function existsMaster(hostInfo) {
95
97
  * @param {Object} hostInfo - hostinfo object
96
98
  */
97
99
  async function connect(hostInfo, timeout = 60) {
98
- if (await existsMaster(hostInfo)) {
100
+ if (await existsMaster(hostInfo, timeout)) {
99
101
  debugVerbose("master connection exists");
100
102
  return;
101
103
  }
@@ -105,20 +107,22 @@ async function connect(hostInfo, timeout = 60) {
105
107
 
106
108
  debug(`create master ssh session to ${hostInfo.host}`);
107
109
 
108
- const ac = watchDogTimer(timeout, "make connection", ()=>{
109
- sendPty(hostInfo.masterPty, "\n", debugSendMasterPty, 137);
110
- sendPty(hostInfo.masterPty, "exit\n", debugSendMasterPty, 138);
111
-
112
- const err = new Error(`can not connect within ${timeout} sec`);
113
- err.host = hostInfo.host;
114
- err.user = hostInfo.user;
115
- err.port = hostInfo.port;
116
- throw err;
117
- });
118
-
119
110
  const words = crypto.randomUUID();
120
111
  const re = new RegExp(`^${words}`);
112
+
113
+ let ac = null;
121
114
  const p = new Promise((resolve, reject)=>{
115
+ ac = watchDogTimer(timeout, "make connection", ()=>{
116
+ sendPty(hostInfo.masterPty, "\n", debugSendMasterPty, 137);
117
+ sendPty(hostInfo.masterPty, "exit\n", debugSendMasterPty, 138);
118
+
119
+ const err = new Error(`can not connect within ${timeout} sec`);
120
+ err.host = hostInfo.host;
121
+ err.user = hostInfo.user;
122
+ err.port = hostInfo.port;
123
+ reject(err);
124
+ });
125
+
122
126
  let done = false;
123
127
  hostInfo.masterPty.onData((data)=>{
124
128
  if (done) {
@@ -157,7 +161,9 @@ async function connect(hostInfo, timeout = 60) {
157
161
  await p;
158
162
  debug(`connected to ${hostInfo.host}`);
159
163
  } finally {
160
- ac.abort();
164
+ if (ac !== null) {
165
+ ac.abort();
166
+ }
161
167
  }
162
168
  }
163
169
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ssh-client-wrapper",
3
- "version": "2.0.1",
3
+ "version": "2.1.1",
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.3",
38
+ "glob": "^10.3.4",
39
39
  "node-pty": "^0.10.1",
40
40
  "nyc": "^15.1.0",
41
41
  "promise-retry": "^2.0.1",
package/test/interface.js CHANGED
@@ -56,7 +56,7 @@ describe("test for interface", ()=>{
56
56
  describe("test for public method", ()=>{
57
57
  let ssh;
58
58
  const hostInfoOrg = { host: "hoge" };
59
- const hostInfo = { ...hostInfoOrg, masterPty: null };
59
+ const hostInfo = { ...hostInfoOrg, masterPty: null, rsyncVersion: null };
60
60
  beforeEach(()=>{
61
61
  ssh = new SshClientWrapper(hostInfoOrg);
62
62
  sshExec.reset();
package/test/rsyncExec.js CHANGED
@@ -1,6 +1,18 @@
1
1
  "use strict";
2
2
  const fs = require("fs-extra");
3
3
  const path = require("path");
4
+ const util = require("util");
5
+ const exec = util.promisify(require("node:child_process").exec);
6
+
7
+ /**
8
+ * to enable debug log dynamically
9
+ * (1) enable folloing line
10
+ * const debug = require("debug");
11
+ * (2) insert this line into the beginning of it()
12
+ * debug.enable("ssh*");
13
+ * (3) insert this line into the end of it()
14
+ * debug.disable();
15
+ **/
4
16
 
5
17
  process.on("unhandledRejection", console.dir); //eslint-disable-line no-console
6
18
  Error.traceLimit = 100000;
@@ -12,7 +24,7 @@ chai.use(require("chai-fs"));
12
24
  chai.use(require("chai-as-promised"));
13
25
 
14
26
  //testee
15
- const { send, recv } = require("../lib/rsyncExec.js");
27
+ const { checkRsyncVersion, send, recv } = require("../lib/rsyncExec.js");
16
28
 
17
29
  //test helpers
18
30
  const { sshExec, disconnect } = require("../lib/sshExec.js");
@@ -42,6 +54,14 @@ const formatLsOutput = (array)=>{
42
54
 
43
55
 
44
56
  //actual test start from here
57
+ describe("test checkRsyncVersion", async ()=>{
58
+ const { major, minor, patch } = await checkRsyncVersion(1);
59
+ const versionString = `${major}.${minor}.${patch}`;
60
+ console.log(`==== test with rsync version ${versionString} ====`); //eslint-disable-line no-console
61
+ const { stdout } = await exec("rsync --version |grep 'version'");
62
+ expect(stdout).to.match(new RegExp(versionString));
63
+ });
64
+
45
65
  describe("test rsync exec", function() {
46
66
  this.timeout(10000);//eslint-disable-line no-invalid-this
47
67
  const rt = [];
@@ -370,6 +390,14 @@ describe("test rsync exec", function() {
370
390
  expect(rt2).to.have.members(["puyo", "poyo"]);
371
391
  });
372
392
  });
393
+ describe("degradation check", ()=>{
394
+ it("should recieve multiple files from remote machine", async ()=>{
395
+ await recv(hostInfo, [path.posix.join(remoteRoot, "foo"),
396
+ path.posix.join(remoteRoot, "bar"), path.posix.join(remoteRoot, "baz")], localEmptyDir, [], 0);
397
+ const rt2 = await fs.readdir(localEmptyDir);
398
+ expect(rt2).to.have.members(["foo", "bar", "baz"]);
399
+ });
400
+ });
373
401
  describe("error case", ()=>{
374
402
  it("should not send directory to existing file path", ()=>{
375
403
  return expect(recv(hostInfo, [remoteRoot], localFiles[0], [], 0)).to.be.rejected;
package/test/sshExec.js CHANGED
@@ -76,7 +76,7 @@ describe("test for ssh execution", function() {
76
76
  await disconnect(hostInfo2);
77
77
  });
78
78
  it("should be resolved with true", async ()=>{
79
- return expect(await canConnect(hostInfo2, 2)).to.be.true;
79
+ return expect(await canConnect(hostInfo2, 5)).to.be.true;
80
80
  });
81
81
  it("should be rejected if user does not exist", async ()=>{
82
82
  hostInfo2.user = "xxxx";