ssh-client-wrapper 2.2.1 → 2.2.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/Dockerfile CHANGED
@@ -1,5 +1,4 @@
1
1
  # syntax=docker/dockerfile:1
2
- #build WHEEL client code
3
2
  FROM --platform=linux/amd64 node:hydrogen-bookworm-slim
4
3
  #FROM --platform=linux/amd64 node:hydrogen-bullseye-slim
5
4
  #FROM --platform=linux/amd64 node:hydrogen-buster-slim
package/README.md CHANGED
@@ -4,3 +4,9 @@
4
4
  [![Maintainability](https://api.codeclimate.com/v1/badges/8f3c0ea00e755ae31081/maintainability)](https://codeclimate.com/github/so5/ssh-client-wrapper/maintainability)
5
5
  # ssh-client-wrapper
6
6
  open ssh client wrapper library for nodejs
7
+
8
+
9
+ # how to test on local machine
10
+ ```
11
+ docker-compose up --build
12
+ ```
package/lib/util.js CHANGED
@@ -11,6 +11,53 @@ const rePwPrompt = /password:/;
11
11
  const rePhPrompt = /Enter passphrase for key/;
12
12
  const reNewHostPrompt = /Are you sure you want to continue connecting/;
13
13
 
14
+ const Ajv = require("ajv");
15
+
16
+ const ajv = new Ajv({
17
+ allErrors: true,
18
+ removeAdditional: "all",
19
+ coerceTypes: true,
20
+ logger: {
21
+ log: debug,
22
+ warn: debug,
23
+ error: debug
24
+ }
25
+ });
26
+ require("ajv-keywords")(ajv, "transform");
27
+
28
+ //never validate password and passphrase value to avoid security insident
29
+ const hostInfoSchema = {
30
+ type: "object",
31
+ properties: {
32
+ host: { type: "string", pattern: "\\S+", transform: ["trim"] },
33
+ user: { type: "string", pattern: "\\S+", transform: ["trim"] },
34
+ port: { type: "number", minimum: 0, maximum: 65535 },
35
+ keyFile: { type: "string", pattern: "\\S+", transform: ["trim"] },
36
+ noStrictHostkeyChecking: { type: "boolean" },
37
+ ControlPersist: { type: "number", minimum: 0 },
38
+ ConnectTimeout: { type: "number", minimum: 0 },
39
+ maxRetry: { type: "number", minimum: 0 },
40
+ retryMinTimeout: { type: "number", minimum: 0 },
41
+ retryMaxTimeout: { type: "number", minimum: 0 },
42
+ sshOpt: { type: "array", minItems: 1, items: { type: "string", pattern: "\\S+", transform: ["trim"] } }
43
+ },
44
+ required: ["host"],
45
+ additionalProperties: false
46
+ };
47
+
48
+ const stringOptions = [
49
+ "user",
50
+ "keyFile"
51
+ ];
52
+ const numberOptions = [
53
+ "ControlPersist",
54
+ "ConnectTimeout",
55
+ "maxRetry",
56
+ "retryMinTimeout",
57
+ "retryMaxTimeout"
58
+ ];
59
+
60
+ const validate = ajv.compile(hostInfoSchema);
14
61
 
15
62
  /**
16
63
  * bridge function to functions which need string
@@ -23,20 +70,62 @@ const buff2String = (func, data)=>{
23
70
 
24
71
  /**
25
72
  * check if given hostInfo object is OK
26
- * @param { Object} hostInfo - host information object defined in index.js
73
+ * @param { Object } hostInfo - host information object defined in index.js
27
74
  */
28
75
  const sanityCheck = (hostInfo)=>{
29
- debug("sanityCheck called");
76
+ debug("sanityCheck called", hostInfo);
77
+ //keep password, passphrase, and masterPty
78
+ const { password, passphrase, masterPty } = hostInfo;
79
+ validate(hostInfo);
30
80
 
31
- if (typeof hostInfo.host !== "string" || hostInfo.host === "") {
32
- throw new Error("host must be non-empty string");
33
- }
34
- if (Object.prototype.hasOwnProperty.call(hostInfo, "port") && (typeof hostInfo.port === "string" || Number.isInteger(hostInfo.port))) {
35
- const port = Number(hostInfo.port);
36
- if (port < 0 || port > 65535) {
37
- throw new Error("port number must be in the range of 0 to 65535");
81
+ if (validate !== null && Array.isArray(validate.errors)) {
82
+ for (const e of validate.errors) {
83
+ debug("validation error:", e);
84
+ const prop = e.instancePath.replace(/^\//, "");
85
+ if (e.keyword === "required" && e.params.missingProperty === "host") {
86
+ throw new Error("host is required");
87
+ }
88
+ if (prop === "host") {
89
+ if (hostInfo.host === "") {
90
+ throw new Error("empty host is not allowed");
91
+ }
92
+ const err = new Error("invalid host specified");
93
+ err.hostInfo = hostInfo;
94
+ err.validationError = e;
95
+ throw err;
96
+ }
97
+ if (prop.startsWith("sshOpt")) {
98
+ debug("remove empty member of sshOpt");
99
+ hostInfo.sshOpt = hostInfo.sshOpt.filter((opt)=>{
100
+ return opt !== "";
101
+ });
102
+ continue;
103
+ } else if (stringOptions.includes(prop) && hostInfo[prop] === "") {
104
+ debug("remove empty string option", prop, hostInfo[prop]);
105
+ delete hostInfo[prop];
106
+ continue;
107
+ } else if (numberOptions.includes(prop) && ["minimum", "maximum"].includes(e.keyword)) {
108
+ debug("remove out of range number option", prop, hostInfo[prop]);
109
+ delete hostInfo[prop];
110
+ continue;
111
+ } else {
112
+ const err = new Error(`invalid ${prop} specified ${hostInfo[prop]}`);
113
+ err.hostInfo = hostInfo;
114
+ err.validationError = e;
115
+ throw err;
116
+ }
38
117
  }
39
118
  }
119
+ if (["string", "function"].includes(typeof password)) {
120
+ hostInfo.password = password;
121
+ }
122
+ if (["string", "function"].includes(typeof passphrase)) {
123
+ hostInfo.password = password;
124
+ }
125
+ if (masterPty === null || typeof masterPty === "function") {
126
+ hostInfo.masterPty = masterPty;
127
+ }
128
+ return hostInfo;
40
129
  };
41
130
 
42
131
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ssh-client-wrapper",
3
- "version": "2.2.1",
3
+ "version": "2.2.3",
4
4
  "description": "wrapper library for open ssh client and rsync",
5
5
  "keywords": [
6
6
  "ssh",
@@ -34,12 +34,12 @@
34
34
  "*.js": "eslint --fix"
35
35
  },
36
36
  "dependencies": {
37
+ "ajv": "^8.12.0",
38
+ "ajv-keywords": "^5.1.0",
37
39
  "debug": "^4.3.4",
38
40
  "glob": "^10.3.10",
39
41
  "node-pty": "^0.10.1",
40
- "nyc": "^15.1.0",
41
- "promise-retry": "^2.0.1",
42
- "rewire": "^7.0.0"
42
+ "promise-retry": "^2.0.1"
43
43
  },
44
44
  "devDependencies": {
45
45
  "chai": "^4.3.7",
@@ -56,6 +56,8 @@
56
56
  "lint-staged": "^13.2.1",
57
57
  "mocha": "^10.2.0",
58
58
  "sinon": "^15.0.3",
59
+ "rewire": "^7.0.0",
60
+ "nyc": "^15.1.0",
59
61
  "sinon-chai": "^3.7.0"
60
62
  }
61
63
  }
package/test/interface.js CHANGED
@@ -33,15 +33,11 @@ describe("test for interface", ()=>{
33
33
  expect(()=>{
34
34
  //eslint-disable-next-line no-new
35
35
  new SshClientWrapper({});
36
- }).to.throw("host must be non-empty string");
37
- expect(()=>{
38
- //eslint-disable-next-line no-new
39
- new SshClientWrapper({ host: 1 });
40
- }).to.throw("host must be non-empty string");
36
+ }).to.throw("host is required");
41
37
  expect(()=>{
42
38
  //eslint-disable-next-line no-new
43
39
  new SshClientWrapper({ host: "" });
44
- }).to.throw("host must be non-empty string");
40
+ }).to.throw("empty host is not allowed");
45
41
  });
46
42
  it("should successfully instanciate only with host prop", ()=>{
47
43
  expect(new SshClientWrapper({ host: "hoge" })).to.have.property("exec");
package/test/rsyncExec.js CHANGED
@@ -28,7 +28,7 @@ const { checkRsyncVersion, send, recv } = require("../lib/rsyncExec.js");
28
28
 
29
29
  //test helpers
30
30
  const { sshExec, disconnect } = require("../lib/sshExec.js");
31
- const hostInfo = require("./util/hostInfo.js");
31
+ const hostInfo = require("./testUtil/hostInfo.js");
32
32
  const {
33
33
  clearLocalTestFiles,
34
34
  clearRemoteTestFiles,
@@ -40,7 +40,7 @@ const {
40
40
  remoteRoot,
41
41
  remoteEmptyDir,
42
42
  remoteFiles
43
- } = require("./util/testFiles");
43
+ } = require("./testUtil/testFiles");
44
44
 
45
45
  const formatLsOutput = (array)=>{
46
46
  const rt = [];
package/test/sshExec.js CHANGED
@@ -17,8 +17,8 @@ const sshout = sinon.stub();
17
17
  const { sshExec, canConnect, disconnect, ls } = require("../lib/sshExec.js");
18
18
 
19
19
  //test helpers
20
- const hostInfo = require("./util/hostInfo.js");
21
- const { clearRemoteTestFiles, clearLocalTestFiles, createRemoteFiles, nonExisting, remoteRoot, remoteEmptyDir } = require("./util/testFiles.js");
20
+ const hostInfo = require("./testUtil/hostInfo.js");
21
+ const { clearRemoteTestFiles, clearLocalTestFiles, createRemoteFiles, nonExisting, remoteRoot, remoteEmptyDir } = require("./testUtil/testFiles.js");
22
22
 
23
23
  describe("test for ssh execution", function() {
24
24
  this.timeout(65000);//eslint-disable-line no-invalid-this
@@ -97,11 +97,11 @@ describe("test for ssh execution", function() {
97
97
  });
98
98
  it("should be rejected if port number is out of range(-1)", async ()=>{
99
99
  hostInfo2.port = -1;
100
- return expect(canConnect(hostInfo2, 2)).to.be.rejectedWith(255);
100
+ return expect(canConnect(hostInfo2, 2)).to.be.rejectedWith("invalid port specified -1");
101
101
  });
102
102
  it("should be rejected if port number is out of range(65536)", async ()=>{
103
103
  hostInfo2.port = 65536;
104
- return expect(canConnect(hostInfo2, 2)).to.be.rejectedWith(255);
104
+ return expect(canConnect(hostInfo2, 2)).to.be.rejectedWith("invalid port specified 65536");
105
105
  });
106
106
  });
107
107
  describe("#ls", ()=>{
@@ -2,7 +2,7 @@
2
2
  module.exports = {
3
3
  host: process.env.TEST_HOST || "localhost",
4
4
  user: process.env.TEST_USER || null,
5
- port: process.env.TEST_PORT || null,
5
+ port: process.env.TEST_PORT || 22,
6
6
  password: process.env.TEST_PW,
7
7
  passphrase: process.env.TEST_PH,
8
8
  keyFile: process.env.TEST_PRIVATE_KEY,
package/test/util.js ADDED
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ process.on("unhandledRejection", console.dir); //eslint-disable-line no-console
3
+
4
+ //setup test framework
5
+ const chai = require("chai");
6
+ const { expect } = require("chai");
7
+ chai.use(require("chai-as-promised"));
8
+
9
+
10
+ //testee
11
+ const { sanityCheck } = require("../lib/util.js");
12
+
13
+ describe("test for sanityCheck", ()=>{
14
+ const host = "testHostName";
15
+ it("should throw error for empty host", ()=>{
16
+ expect(sanityCheck.bind(null, { host: " ", user: " ", port: 22 })).to.throw(/empty host is not allowed/);
17
+ });
18
+ it("should throw error if host is not in argument object", ()=>{
19
+ expect(sanityCheck.bind(null, { user: " ", port: 22 })).to.throw(/host is required/);
20
+ });
21
+ it("should just remove empty string props", ()=>{
22
+ expect(sanityCheck({ host, user: " ", port: 22 })).to.deep.equal({ host, port: 22 });
23
+ });
24
+ it("should remove empty string member in sshOpt", ()=>{
25
+ expect(sanityCheck({ host, sshOpt: ["foo", " ", "bar"], user: "user", port: 33 })).to.deep.equal({ host, sshOpt: ["foo", "bar"], user: "user", port: 33 });
26
+ });
27
+ it("should just chage type if string value specified for number", ()=>{
28
+ expect(sanityCheck({
29
+ host,
30
+ port: "11",
31
+ ControlPersist: "22",
32
+ ConnectTimeout: "33",
33
+ maxRetry: "44",
34
+ retryMinTimeout: "55",
35
+ retryMaxTimeout: "66"
36
+ })).to.deep.equal({
37
+ host,
38
+ port: 11,
39
+ ControlPersist: 22,
40
+ ConnectTimeout: 33,
41
+ maxRetry: 44,
42
+ retryMinTimeout: 55,
43
+ retryMaxTimeout: 66
44
+ });
45
+ });
46
+ it("should remove out of range members", ()=>{
47
+ expect(sanityCheck({
48
+ host,
49
+ ControlPersist: "-1",
50
+ ConnectTimeout: "-1",
51
+ maxRetry: "-1",
52
+ retryMinTimeout: "-1",
53
+ retryMaxTimeout: "-1"
54
+ })).to.deep.equal({
55
+ host
56
+ });
57
+ });
58
+ it("should just chage type if string value specified for boolean member", ()=>{
59
+ expect(sanityCheck({
60
+ host,
61
+ noStrictHostkeyChecking: "true"
62
+ })).to.deep.equal({
63
+ host,
64
+ noStrictHostkeyChecking: true
65
+ });
66
+ });
67
+ it("should just chage type if number value specified for boolean member", ()=>{
68
+ expect(sanityCheck({
69
+ host,
70
+ noStrictHostkeyChecking: 0
71
+ })).to.deep.equal({
72
+ host,
73
+ noStrictHostkeyChecking: false
74
+ });
75
+ });
76
+ });
File without changes