diff --git a/.github/workflows/connectivity-tests.yaml b/.github/workflows/connectivity-tests.yaml index 70b89e7216f..fd28ebc13a7 100644 --- a/.github/workflows/connectivity-tests.yaml +++ b/.github/workflows/connectivity-tests.yaml @@ -67,7 +67,7 @@ jobs: run: | git clone https://github.com/mongodb-js/devtools-docker-test-envs.git cd devtools-docker-test-envs - git checkout v1.2.2 + git checkout v1.2.4 docker-compose -f docker/enterprise/docker-compose.yaml up -d docker-compose -f docker/ldap/docker-compose.yaml up -d docker-compose -f docker/scram/docker-compose.yaml up -d diff --git a/package-lock.json b/package-lock.json index 488357eadd8..f1356f57743 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7343,9 +7343,9 @@ } }, "node_modules/@mongodb-js/devtools-docker-test-envs": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-docker-test-envs/-/devtools-docker-test-envs-1.2.2.tgz", - "integrity": "sha512-WQhx2hyGXcTw5Fwlwzu6jLhDRKOC1C6JTS9kjRnTCTO+BxJgTSHVzyFdIeAuHueekDYsaaImfg+9QwoBxUGYKg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-docker-test-envs/-/devtools-docker-test-envs-1.2.4.tgz", + "integrity": "sha512-Ja1tUOc3nSySfs2TbYZ64pejK+5cgs+bqslL5eCcXarjV4VcvRIoYzbX958194UmAuZc6k1h/bqjW5nXGwGxBQ==", "dependencies": { "eslint-plugin-mocha": "^9.0.0", "execa": "^5.1.1", @@ -15816,8 +15816,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", - "dev": true, - "optional": true, "dependencies": { "exit": "0.1.2", "glob": "^7.1.1" @@ -23117,7 +23115,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true, "engines": { "node": ">= 0.8.0" } @@ -48905,6 +48902,44 @@ "node": ">= 6" } }, + "node_modules/socksv5": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/socksv5/-/socksv5-0.0.6.tgz", + "integrity": "sha1-EycjX/fo3iGsQ0oKV53GnD8HEGE=", + "bundleDependencies": [ + "ipv6" + ], + "dependencies": { + "ipv6": "*" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/socksv5/node_modules/ipv6": { + "version": "3.1.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "cli": "0.4.x", + "cliff": "0.1.x", + "sprintf": "0.1.x" + }, + "bin": { + "ipv6": "bin/ipv6.js", + "ipv6grep": "bin/ipv6grep.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/socksv5/node_modules/ipv6/node_modules/sprintf": { + "version": "0.1.3", + "inBundle": true, + "engines": { + "node": ">=0.2.4" + } + }, "node_modules/sort-keys": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-4.2.0.tgz", @@ -57496,7 +57531,7 @@ "make-fetch-happen": "^8.0.14", "marky": "^1.2.1", "mocha": "^8.4.0", - "mongodb": "^4.2.2", + "mongodb": "^4.3.0", "mongodb-connection-model": "^21.11.1", "mongodb-data-service": "^21.15.1", "mongodb-instance-model": "^11.17.1", @@ -65147,7 +65182,7 @@ "hadron-build": "^24.11.0", "lodash": "^4.17.21", "mocha": "*", - "mongodb": "^4.2.2", + "mongodb": "^4.3.0", "mongodb-runner": "^4.8.3", "prettier": "*", "spectron": "^15.0.0", @@ -76932,7 +76967,7 @@ "lodash": "^4.17.21", "mocha": "^5.2.0", "mocha-webpack": "^2.0.0-beta.0", - "mongodb": "^4.2.2", + "mongodb": "^4.3.0", "mongodb-connection-model": "^21.11.1", "mongodb-data-service": "^21.15.1", "mongodb-ns": "^2.2.0", @@ -85339,7 +85374,7 @@ "mini-css-extract-plugin": "^0.8.0", "mocha": "^5.2.0", "mocha-webpack": "^2.0.0-beta.0", - "mongodb": "^4.2.2", + "mongodb": "^4.3.0", "mongodb-reflux-store": "^0.0.1", "node-loader": "^0.6.0", "nyc": "^13.1.0", @@ -94107,7 +94142,7 @@ "eslint": "^7.25.0", "gen-esm-wrapper": "^1.1.0", "mocha": "^8.4.0", - "mongodb": "^4.2.2", + "mongodb": "^4.3.0", "mongodb-data-service": "^21.15.1", "nyc": "^15.1.0", "prettier": "2.3.2", @@ -94473,14 +94508,14 @@ "eslint-config-mongodb-js": "^5.0.3", "mocha": "^8.0.1", "mock-require": "^3.0.3", - "mongodb": "^4.2.1", + "mongodb": "^4.3.0", "mongodb-runner": "^4.8.3", "proxyquire": "^2.1.0", "sinon": "^9.0.2", "uuid": "^8.2.0" }, "peerDependencies": { - "mongodb": "^4.2.2" + "mongodb": "^4.3.0" } }, "packages/connection-model/node_modules/anymatch": { @@ -95247,7 +95282,7 @@ "uuid": "^8.3.2" }, "devDependencies": { - "@mongodb-js/devtools-docker-test-envs": "^1.2.2", + "@mongodb-js/devtools-docker-test-envs": "^1.2.4", "@mongodb-js/eslint-config-compass": "^0.5.0", "@mongodb-js/mocha-config-compass": "^0.7.0", "@mongodb-js/prettier-config-compass": "^0.4.0", @@ -95263,7 +95298,7 @@ "eslint": "^7.25.0", "kerberos": "^2.0.0-beta.0", "mocha": "^8.4.0", - "mongodb": "^4.2.1", + "mongodb": "^4.3.0", "mongodb-connection-model": "^21.11.1", "mongodb-runner": "^4.8.3", "nyc": "^15.0.0", @@ -95277,7 +95312,7 @@ "node": ">=14.17.5" }, "peerDependencies": { - "mongodb": "^4.2.2", + "mongodb": "^4.3.0", "mongodb-connection-model": "^21.11.1" } }, @@ -100555,7 +100590,7 @@ "eslint": "^7.25.0", "eslint-config-mongodb-js": "^5.0.3", "mocha": "^7.1.0", - "mongodb": "^4.2.2", + "mongodb": "^4.3.0", "mongodb-runner": "^4.8.3" } }, @@ -104385,6 +104420,8 @@ "version": "1.2.3", "license": "Apache-2.0", "dependencies": { + "debug": "4.3.0", + "socksv5": "0.0.6", "ssh2": "^0.8.9" }, "devDependencies": { @@ -104393,6 +104430,7 @@ "@mongodb-js/prettier-config-compass": "^0.4.0", "@mongodb-js/tsconfig-compass": "^0.4.0", "@types/chai": "^4.2.21", + "@types/debug": "^4.1.7", "@types/mocha": "^9.0.0", "@types/node-fetch": "^2.5.8", "@types/sinon-chai": "^3.2.5", @@ -104406,6 +104444,7 @@ "nyc": "^15.1.0", "prettier": "2.3.2", "sinon": "^9.2.3", + "socks": "^2.6.1", "typescript": "^4.3.5" } }, @@ -104470,6 +104509,28 @@ "fsevents": "~2.3.1" } }, + "packages/ssh-tunnel/node_modules/debug": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.0.tgz", + "integrity": "sha512-jjO6JD2rKfiZQnBoRzhRTbXjHLGLfH+UtGkWLc/UXAh/rzZMyjbgn0NcfFpqT8nd1kTtFnDiJcrIFkq4UKeJVg==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "packages/ssh-tunnel/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "packages/ssh-tunnel/node_modules/decamelize": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", @@ -104767,7 +104828,7 @@ "version": "0.1.0", "license": "SSPL", "dependencies": { - "@mongodb-js/devtools-docker-test-envs": "^1.2.2", + "@mongodb-js/devtools-docker-test-envs": "^1.2.4", "chalk": "^4.1.1", "electron": "^13.5.1", "find-up": "^5.0.0", @@ -123768,7 +123829,7 @@ "mocha": "^5.2.0", "mocha-webpack": "^2.0.0-beta.0", "moment": "^2.27.0", - "mongodb": "^4.2.2", + "mongodb": "^4.3.0", "mongodb-connection-model": "^21.11.1", "mongodb-data-service": "^21.15.1", "mongodb-ns": "^2.2.0", @@ -125474,7 +125535,7 @@ "@mongodb-js/compass-scripts": { "version": "file:scripts", "requires": { - "@mongodb-js/devtools-docker-test-envs": "^1.2.2", + "@mongodb-js/devtools-docker-test-envs": "^1.2.4", "@mongodb-js/eslint-config-compass": "^0.5.0", "@mongodb-js/prettier-config-compass": "^0.4.0", "chalk": "^4.1.1", @@ -130132,7 +130193,7 @@ "mini-css-extract-plugin": "^0.8.0", "mocha": "^5.2.0", "mocha-webpack": "^2.0.0-beta.0", - "mongodb": "^4.2.2", + "mongodb": "^4.3.0", "mongodb-reflux-store": "^0.0.1", "node-loader": "^0.6.0", "nyc": "^13.1.0", @@ -135415,7 +135476,7 @@ "eslint": "^7.25.0", "gen-esm-wrapper": "^1.1.0", "mocha": "^8.4.0", - "mongodb": "^4.2.2", + "mongodb": "^4.3.0", "mongodb-connection-string-url": "^2.4.1", "mongodb-data-service": "^21.15.1", "nyc": "^15.1.0", @@ -135952,9 +136013,9 @@ } }, "@mongodb-js/devtools-docker-test-envs": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-docker-test-envs/-/devtools-docker-test-envs-1.2.2.tgz", - "integrity": "sha512-WQhx2hyGXcTw5Fwlwzu6jLhDRKOC1C6JTS9kjRnTCTO+BxJgTSHVzyFdIeAuHueekDYsaaImfg+9QwoBxUGYKg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-docker-test-envs/-/devtools-docker-test-envs-1.2.4.tgz", + "integrity": "sha512-Ja1tUOc3nSySfs2TbYZ64pejK+5cgs+bqslL5eCcXarjV4VcvRIoYzbX958194UmAuZc6k1h/bqjW5nXGwGxBQ==", "requires": { "eslint-plugin-mocha": "^9.0.0", "execa": "^5.1.1", @@ -138881,11 +138942,13 @@ "@mongodb-js/prettier-config-compass": "^0.4.0", "@mongodb-js/tsconfig-compass": "^0.4.0", "@types/chai": "^4.2.21", + "@types/debug": "^4.1.7", "@types/mocha": "^9.0.0", "@types/node-fetch": "^2.5.8", "@types/sinon-chai": "^3.2.5", "@types/ssh2": "^0.5.46", "chai": "^4.3.4", + "debug": "4.3.0", "depcheck": "^1.4.1", "eslint": "^7.25.0", "gen-esm-wrapper": "^1.1.0", @@ -138894,6 +138957,8 @@ "nyc": "^15.1.0", "prettier": "2.3.2", "sinon": "^9.2.3", + "socks": "^2.6.1", + "socksv5": "0.0.6", "ssh2": "^0.8.9", "typescript": "^4.3.5" }, @@ -138942,6 +139007,21 @@ "readdirp": "~3.5.0" } }, + "debug": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.0.tgz", + "integrity": "sha512-jjO6JD2rKfiZQnBoRzhRTbXjHLGLfH+UtGkWLc/UXAh/rzZMyjbgn0NcfFpqT8nd1kTtFnDiJcrIFkq4UKeJVg==", + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "decamelize": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", @@ -147545,8 +147625,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", - "dev": true, - "optional": true, "requires": { "exit": "0.1.2", "glob": "^7.1.1" @@ -148067,7 +148145,7 @@ "hadron-build": "^24.11.0", "lodash": "^4.17.21", "mocha": "*", - "mongodb": "^4.2.2", + "mongodb": "^4.3.0", "mongodb-compass": "^0.0.0-dev.0", "mongodb-runner": "^4.8.3", "prettier": "*", @@ -156534,8 +156612,7 @@ "exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=" }, "exit-hook": { "version": "1.1.1", @@ -171832,7 +171909,7 @@ "make-fetch-happen": "^8.0.14", "marky": "^1.2.1", "mocha": "^8.4.0", - "mongodb": "^4.2.2", + "mongodb": "^4.3.0", "mongodb-connection-model": "^21.11.1", "mongodb-data-service": "^21.15.1", "mongodb-instance-model": "^11.17.1", @@ -172108,7 +172185,7 @@ "lodash": "^4.17.15", "mocha": "^8.0.1", "mock-require": "^3.0.3", - "mongodb": "^4.2.1", + "mongodb": "^4.3.0", "mongodb-connection-string-url": "^2.4.1", "mongodb-runner": "^4.8.3", "mongodb3": "npm:mongodb@^3.6.3", @@ -172448,7 +172525,7 @@ "version": "file:packages/data-service", "requires": { "@mongodb-js/compass-logging": "^0.6.1", - "@mongodb-js/devtools-docker-test-envs": "^1.2.2", + "@mongodb-js/devtools-docker-test-envs": "^1.2.4", "@mongodb-js/eslint-config-compass": "^0.5.0", "@mongodb-js/mocha-config-compass": "^0.7.0", "@mongodb-js/prettier-config-compass": "^0.4.0", @@ -172469,7 +172546,7 @@ "kerberos": "^2.0.0-beta.0", "lodash": "^4.17.20", "mocha": "^8.4.0", - "mongodb": "^4.2.1", + "mongodb": "^4.3.0", "mongodb-build-info": "^1.3.0", "mongodb-connection-model": "^21.11.1", "mongodb-connection-string-url": "^2.4.1", @@ -173276,7 +173353,7 @@ "eslint-config-mongodb-js": "^5.0.3", "lodash": "^4.17.15", "mocha": "^7.1.0", - "mongodb": "^4.2.2", + "mongodb": "^4.3.0", "mongodb-js-errors": "^0.5.0", "mongodb-ns": "^2.2.0", "mongodb-runner": "^4.8.3" @@ -181541,6 +181618,31 @@ "socks": "^2.3.3" } }, + "socksv5": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/socksv5/-/socksv5-0.0.6.tgz", + "integrity": "sha1-EycjX/fo3iGsQ0oKV53GnD8HEGE=", + "requires": { + "ipv6": "*" + }, + "dependencies": { + "ipv6": { + "version": "3.1.1", + "bundled": true, + "requires": { + "cli": "0.4.x", + "cliff": "0.1.x", + "sprintf": "0.1.x" + }, + "dependencies": { + "sprintf": { + "version": "0.1.3", + "bundled": true + } + } + } + } + }, "sort-keys": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-4.2.0.tgz", diff --git a/packages/compass-e2e-tests/package.json b/packages/compass-e2e-tests/package.json index 13fa908a4a7..b5058f1c93a 100644 --- a/packages/compass-e2e-tests/package.json +++ b/packages/compass-e2e-tests/package.json @@ -40,7 +40,7 @@ "glob": "^7.1.6", "hadron-build": "^24.11.0", "mocha": "*", - "mongodb": "^4.2.2", + "mongodb": "^4.3.0", "mongodb-runner": "^4.8.3", "prettier": "*", "spectron": "^15.0.0", diff --git a/packages/compass-schema/package.json b/packages/compass-schema/package.json index a8c4b827bde..96a6f136a68 100644 --- a/packages/compass-schema/package.json +++ b/packages/compass-schema/package.json @@ -103,7 +103,7 @@ "lodash": "^4.17.21", "mocha": "^5.2.0", "mocha-webpack": "^2.0.0-beta.0", - "mongodb": "^4.2.2", + "mongodb": "^4.3.0", "mongodb-connection-model": "^21.11.1", "mongodb-data-service": "^21.15.1", "mongodb-ns": "^2.2.0", diff --git a/packages/compass-shell/package.json b/packages/compass-shell/package.json index a46f6ea0e60..3603073d2ca 100644 --- a/packages/compass-shell/package.json +++ b/packages/compass-shell/package.json @@ -115,7 +115,7 @@ "mini-css-extract-plugin": "^0.8.0", "mocha": "^5.2.0", "mocha-webpack": "^2.0.0-beta.0", - "mongodb": "^4.2.2", + "mongodb": "^4.3.0", "mongodb-reflux-store": "^0.0.1", "node-loader": "^0.6.0", "nyc": "^13.1.0", diff --git a/packages/compass/package.json b/packages/compass/package.json index b284ffbf781..6bb22795c4a 100644 --- a/packages/compass/package.json +++ b/packages/compass/package.json @@ -228,7 +228,7 @@ "make-fetch-happen": "^8.0.14", "marky": "^1.2.1", "mocha": "^8.4.0", - "mongodb": "^4.2.2", + "mongodb": "^4.3.0", "mongodb-connection-model": "^21.11.1", "mongodb-data-service": "^21.15.1", "mongodb-instance-model": "^11.17.1", diff --git a/packages/connect-form/package.json b/packages/connect-form/package.json index bd237235067..91e9d711483 100644 --- a/packages/connect-form/package.json +++ b/packages/connect-form/package.json @@ -77,7 +77,7 @@ "eslint": "^7.25.0", "gen-esm-wrapper": "^1.1.0", "mocha": "^8.4.0", - "mongodb": "^4.2.2", + "mongodb": "^4.3.0", "mongodb-data-service": "^21.15.1", "nyc": "^15.1.0", "prettier": "2.3.2", diff --git a/packages/connection-model/package.json b/packages/connection-model/package.json index dae90622d01..541bdf07a94 100644 --- a/packages/connection-model/package.json +++ b/packages/connection-model/package.json @@ -33,7 +33,7 @@ "posttest-ci": "node ../../scripts/killall-mongo.js" }, "peerDependencies": { - "mongodb": "^4.2.2" + "mongodb": "^4.3.0" }, "dependencies": { "@mongodb-js/ssh-tunnel": "^1.2.3", @@ -57,7 +57,7 @@ "eslint-config-mongodb-js": "^5.0.3", "mocha": "^8.0.1", "mock-require": "^3.0.3", - "mongodb": "^4.2.1", + "mongodb": "^4.3.0", "mongodb-runner": "^4.8.3", "proxyquire": "^2.1.0", "sinon": "^9.0.2", diff --git a/packages/data-service/package.json b/packages/data-service/package.json index d642781407f..73f2f11c50f 100644 --- a/packages/data-service/package.json +++ b/packages/data-service/package.json @@ -53,7 +53,7 @@ "reformat": "npm run prettier -- --write ." }, "peerDependencies": { - "mongodb": "^4.2.2", + "mongodb": "^4.3.0", "mongodb-connection-model": "^21.11.1" }, "dependencies": { @@ -71,7 +71,7 @@ "uuid": "^8.3.2" }, "devDependencies": { - "@mongodb-js/devtools-docker-test-envs": "^1.2.2", + "@mongodb-js/devtools-docker-test-envs": "^1.2.4", "@mongodb-js/eslint-config-compass": "^0.5.0", "@mongodb-js/mocha-config-compass": "^0.7.0", "@mongodb-js/prettier-config-compass": "^0.4.0", @@ -87,7 +87,7 @@ "eslint": "^7.25.0", "kerberos": "^2.0.0-beta.0", "mocha": "^8.4.0", - "mongodb": "^4.2.1", + "mongodb": "^4.3.0", "mongodb-connection-model": "^21.11.1", "mongodb-runner": "^4.8.3", "nyc": "^15.0.0", diff --git a/packages/data-service/src/connect-mongo-client.spec.ts b/packages/data-service/src/connect-mongo-client.spec.ts index 93e7b9a7041..2c3e27110f9 100644 --- a/packages/data-service/src/connect-mongo-client.spec.ts +++ b/packages/data-service/src/connect-mongo-client.spec.ts @@ -110,31 +110,6 @@ describe('connectMongoClient', function () { }); describe('ssh tunnel failures', function () { - it('should refuse to open the tunnel with a replica set', async function () { - const connectionOptions: ConnectionOptions = { - connectionString: - 'mongodb://localhost:27018,localhost:27019,localhost:27020?serverSelectionTimeoutMS=100', - sshTunnel: { - host: 'localhost', - port: 22, - username: 'my-user', - password: 'password', - }, - }; - - const error = await connectMongoClient( - connectionOptions, - setupListeners, - tunnelLocalPort - ).catch((err) => err); - - expect(error).to.be.instanceOf(Error); - - expect(error.message).to.match( - /It is currently not possible to open an SSH tunnel to a replica set/ - ); - }); - it('should close ssh tunnel if the connection fails', async function () { const connectionOptions: ConnectionOptions = { connectionString: diff --git a/packages/data-service/src/connect-mongo-client.ts b/packages/data-service/src/connect-mongo-client.ts index b381f662b6a..946929d6514 100644 --- a/packages/data-service/src/connect-mongo-client.ts +++ b/packages/data-service/src/connect-mongo-client.ts @@ -48,18 +48,22 @@ export default async function connectMongoClient( // If connectionOptions.sshTunnel is not defined the tunnel // will also be undefined and the connectionString will be the // same as the one passed as argument. - const [tunnel, urlWithSshTunnel] = await openSshTunnel( + const [tunnel, socks5Options] = await openSshTunnel( srvResolvedUrl, connectionOptions.sshTunnel, tunnelLocalPort ); + if (socks5Options) { + Object.assign(driverOptions, socks5Options); + } + log.info(mongoLogId(1_001_000_009), 'Connect', 'Initiating connection', { - url: redactConnectionString(urlWithSshTunnel), + url: redactConnectionString(srvResolvedUrl), options: driverOptions, }); - const mongoClient = new MongoClient(urlWithSshTunnel, driverOptions); + const mongoClient = new MongoClient(srvResolvedUrl, driverOptions); if (setupListeners) { setupListeners(mongoClient); @@ -77,13 +81,13 @@ export default async function connectMongoClient( log.info(mongoLogId(1_001_000_012), 'Connect', 'Connection established', { driver: mongoClient.options?.metadata?.driver, - url: redactConnectionString(urlWithSshTunnel), + url: redactConnectionString(srvResolvedUrl), }); return [ mongoClient, tunnel, - { url: urlWithSshTunnel, options: driverOptions }, + { url: srvResolvedUrl, options: driverOptions }, ]; } catch (err: any) { log.error( diff --git a/packages/data-service/src/connect.spec.ts b/packages/data-service/src/connect.spec.ts index c858125ba87..852ced361d5 100644 --- a/packages/data-service/src/connect.spec.ts +++ b/packages/data-service/src/connect.spec.ts @@ -494,6 +494,20 @@ describe('connect', function () { { authenticatedUserRoles: [], authenticatedUsers: [] } ); }); + + it('connects with ssh (sshReplicaSetSeedlist)', async function () { + await testConnection( + envs.getConnectionOptions('sshReplicaSetSeedlist'), + { authenticatedUserRoles: [], authenticatedUsers: [] } + ); + }); + + it('connects with ssh (sshReplicaSetByReplSetName)', async function () { + await testConnection( + envs.getConnectionOptions('sshReplicaSetByReplSetName'), + { authenticatedUserRoles: [], authenticatedUsers: [] } + ); + }); }); describe('tls', function () { @@ -511,6 +525,16 @@ describe('connect', function () { }); }); + it('connects with tls (tlsServerValidationSsh)', async function () { + await testConnection( + envs.getConnectionOptions('tlsServerValidationSsh'), + { + authenticatedUserRoles: [], + authenticatedUsers: [], + } + ); + }); + it('connects with tls (tlsServerAndClientValidation)', async function () { await testConnection( envs.getConnectionOptions('tlsServerAndClientValidation'), diff --git a/packages/data-service/src/ssh-tunnel.ts b/packages/data-service/src/ssh-tunnel.ts index e58c125e274..98d82ba2c55 100644 --- a/packages/data-service/src/ssh-tunnel.ts +++ b/packages/data-service/src/ssh-tunnel.ts @@ -1,47 +1,47 @@ import { EventEmitter, once } from 'events'; import createDebug from 'debug'; import fs from 'fs'; +import crypto from 'crypto'; +import { promisify } from 'util'; import ConnectionStringUrl from 'mongodb-connection-string-url'; import SSHTunnel from '@mongodb-js/ssh-tunnel'; import createLoggerAndTelemetry from '@mongodb-js/compass-logging'; +import type { MongoClientOptions } from 'mongodb'; import { ConnectionSshOptions } from './connection-options'; import { redactSshTunnelOptions } from './redact'; const debug = createDebug('mongodb-data-service:connect'); const { log, mongoLogId } = createLoggerAndTelemetry('COMPASS-CONNECT'); +const randomBytes = promisify(crypto.randomBytes); + +type Socks5Options = Pick< + MongoClientOptions, + 'proxyHost' | 'proxyPort' | 'proxyUsername' | 'proxyPassword' +>; export async function openSshTunnel( srvResolvedConnectionString: string, sshTunnelOptions: ConnectionSshOptions | undefined, localPort: number -): Promise<[SSHTunnel | undefined, string]> { +): Promise<[SSHTunnel | undefined, Socks5Options | undefined]> { if (!sshTunnelOptions) { - return [undefined, srvResolvedConnectionString]; - } - - const connectionStringUrl = new ConnectionStringUrl( - srvResolvedConnectionString - ); - - if (connectionStringUrl.hosts.length !== 1) { - throw new Error( - 'It is currently not possible to open an SSH tunnel to a replica set' - ); + return [undefined, undefined]; } - const [dstHost, dstPort = 27017] = connectionStringUrl.hosts[0].split(':'); + const credentialsSource = await randomBytes(64); + const socks5Username = credentialsSource.slice(0, 32).toString('base64'); + const socks5Password = credentialsSource.slice(32).toString('base64'); const tunnelConstructorOptions = { readyTimeout: 20000, forwardTimeout: 20000, keepaliveInterval: 20000, - srcAddr: '127.0.0.1', // OS should figure out an ephemeral srcPort. - dstPort: +dstPort, - dstAddr: dstHost, localPort: localPort, localAddr: '127.0.0.1', + socks5Username: socks5Username, + socks5Password: socks5Password, host: sshTunnelOptions.host, port: sshTunnelOptions.port, username: sshTunnelOptions.username, @@ -73,9 +73,15 @@ export async function openSshTunnel( log.info(mongoLogId(1_001_000_007), 'SSHTunnel', 'SSH tunnel opened'); - connectionStringUrl.hosts = [`127.0.0.1:${localPort}`]; - - return [tunnel, connectionStringUrl.href]; + return [ + tunnel, + { + proxyHost: 'localhost', + proxyPort: localPort, + proxyUsername: socks5Username, + proxyPassword: socks5Password, + }, + ]; } export async function forceCloseTunnel( diff --git a/packages/index-model/package.json b/packages/index-model/package.json index 5ef52005f68..ac599ceac8e 100644 --- a/packages/index-model/package.json +++ b/packages/index-model/package.json @@ -46,7 +46,7 @@ "eslint": "^7.25.0", "eslint-config-mongodb-js": "^5.0.3", "mocha": "^7.1.0", - "mongodb": "^4.2.2", + "mongodb": "^4.3.0", "mongodb-runner": "^4.8.3" } } diff --git a/packages/ssh-tunnel/package.json b/packages/ssh-tunnel/package.json index 777fe84fd65..bc43de421bf 100644 --- a/packages/ssh-tunnel/package.json +++ b/packages/ssh-tunnel/package.json @@ -51,6 +51,7 @@ "@mongodb-js/prettier-config-compass": "^0.4.0", "@mongodb-js/tsconfig-compass": "^0.4.0", "@types/chai": "^4.2.21", + "@types/debug": "^4.1.7", "@types/mocha": "^9.0.0", "@types/node-fetch": "^2.5.8", "@types/sinon-chai": "^3.2.5", @@ -64,9 +65,12 @@ "nyc": "^15.1.0", "prettier": "2.3.2", "sinon": "^9.2.3", + "socks": "^2.6.1", "typescript": "^4.3.5" }, "dependencies": { + "debug": "4.3.0", + "socksv5": "0.0.6", "ssh2": "^0.8.9" } } diff --git a/packages/ssh-tunnel/src/index.spec.ts b/packages/ssh-tunnel/src/index.spec.ts index b6b22bde252..95892a6a8b9 100644 --- a/packages/ssh-tunnel/src/index.spec.ts +++ b/packages/ssh-tunnel/src/index.spec.ts @@ -1,11 +1,13 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ import { Server as SSHServer, ServerConfig } from 'ssh2'; -import { createServer, Server as HttpServer } from 'http'; +import { createServer, Server as HttpServer, Agent as HttpAgent } from 'http'; import { promisify } from 'util'; import { readFileSync } from 'fs'; import path from 'path'; import { AddressInfo, Socket } from 'net'; import fetch from 'node-fetch'; import { expect } from 'chai'; +import { SocksClient } from 'socks'; import SSHTunnel, { SshTunnelConfig } from './index'; @@ -86,7 +88,6 @@ async function createTestSshTunnel(config: Partial = {}) { sshTunnel = new SSHTunnel({ username: 'user', port: sshServer.address().port, - dstPort: (httpServer.address() as AddressInfo).port, localPort: 0, ...config, }); @@ -102,9 +103,54 @@ async function stopTestSshTunnel() { } } -function getLocalServerHost() { - const { localAddr, localPort } = sshTunnel.config; - return `http://${localAddr}:${localPort}`; +interface Socks5ProxyOptions { + proxyHost: string; + proxyPort: number; + proxyUsername?: string; + proxyPassword?: string; +} + +class Socks5HttpAgent extends HttpAgent { + options: Socks5ProxyOptions; + + constructor(options: Socks5ProxyOptions) { + super(); + this.options = options; + } + + createConnection(options, callback) { + void SocksClient.createConnection( + { + destination: { + host: options.host, + port: +options.port, + }, + proxy: { + host: this.options.proxyHost, + port: this.options.proxyPort, + type: 5, + userId: this.options.proxyUsername, + password: this.options.proxyPassword, + }, + command: 'connect', + }, + (err, info) => { + if (err) { + callback(err); + } else { + callback(null, info.socket); + } + } + ); + } +} + +async function httpFetchWithSocks5( + httpUrl: string, + options: Socks5ProxyOptions +): ReturnType { + const agent = new Socks5HttpAgent(options); + return await fetch(httpUrl, { agent }); } describe('SSHTunnel', function () { @@ -126,16 +172,68 @@ describe('SSHTunnel', function () { it('creates a tunnel that allows to request remote server through an ssh server', async function () { await createTestSshTunnel(); - const res = await fetch(getLocalServerHost()); + const res = await httpFetchWithSocks5( + `http://localhost:${httpServer.address().port}/`, + { + proxyHost: sshTunnel.config.localAddr, + proxyPort: sshTunnel.config.localPort, + } + ); + expect(await res.text()).to.equal('Hello from http server'); + }); + + it('creates a tunnel that passes through requests when auth matches', async function () { + await createTestSshTunnel({ + socks5Username: 'cat', + socks5Password: 'meow', + }); + + const res = await httpFetchWithSocks5( + `http://localhost:${httpServer.address().port}/`, + { + proxyHost: sshTunnel.config.localAddr, + proxyPort: sshTunnel.config.localPort, + proxyUsername: 'cat', + proxyPassword: 'meow', + } + ); expect(await res.text()).to.equal('Hello from http server'); }); + it('creates a tunnel that rejects requests when auth mismatches', async function () { + await createTestSshTunnel({ + socks5Username: 'cat', + socks5Password: 'meow', + }); + + try { + await httpFetchWithSocks5( + `http://localhost:${httpServer.address().port}/`, + { + proxyHost: sshTunnel.config.localAddr, + proxyPort: sshTunnel.config.localPort, + proxyUsername: 'cow', + proxyPassword: 'moo', + } + ); + expect.fail('missed exception'); + } catch (err) { + expect(err.message).to.match(/Socks5 Authentication failed/); + } + }); + it('closes any connections on tunnel close', async function () { await createTestSshTunnel(); try { await Promise.all([ - fetch(`${getLocalServerHost()}/wait`), + httpFetchWithSocks5( + `http://localhost:${httpServer.address().port}/wait`, + { + proxyHost: sshTunnel.config.localAddr, + proxyPort: sshTunnel.config.localPort, + } + ), (async () => { await sleep(50); await sshTunnel.close(); @@ -169,7 +267,7 @@ describe('SSHTunnel', function () { }); expect.fail('missed exception'); } catch { - expect(sshTunnel['server'].listening).to.equal(false); + expect(sshTunnel.server.address()).to.equal(null); } }); }); diff --git a/packages/ssh-tunnel/src/index.ts b/packages/ssh-tunnel/src/index.ts index dbf09a259cb..3d5072f51ab 100644 --- a/packages/ssh-tunnel/src/index.ts +++ b/packages/ssh-tunnel/src/index.ts @@ -1,36 +1,36 @@ import { promisify } from 'util'; import { EventEmitter, once } from 'events'; -import { createServer, Server, Socket } from 'net'; +import type { Socket } from 'net'; import { Client as SshClient, ClientChannel, ConnectConfig } from 'ssh2'; +import createDebug from 'debug'; -type ForwardOutConfig = { - srcAddr: string; - srcPort: number; - dstAddr: string; - dstPort: number; -}; +// The socksv5 module is not bundle-able by itself, so we get the +// subpackages directly +import socks5Server from 'socksv5/lib/server'; +import socks5AuthNone from 'socksv5/lib/auth/None'; +import socks5AuthUserPassword from 'socksv5/lib/auth/UserPassword'; + +const debug = createDebug('mongodb:ssh-tunnel'); type LocalProxyServerConfig = { localAddr: string; localPort: number; + socks5Username?: string; + socks5Password?: string; }; type ErrorWithOrigin = Error & { origin?: string }; -export type SshTunnelConfig = ConnectConfig & - ForwardOutConfig & - LocalProxyServerConfig; +export type SshTunnelConfig = ConnectConfig & LocalProxyServerConfig; function getConnectConfig(config: Partial): ConnectConfig { const { // Doing it the other way around would be too much /* eslint-disable @typescript-eslint/no-unused-vars */ - srcAddr, - srcPort, - dstAddr, - dstPort, localAddr, localPort, + socks5Password, + socks5Username, /* eslint-enable @typescript-eslint/no-unused-vars */ ...connectConfig } = config; @@ -39,27 +39,18 @@ function getConnectConfig(config: Partial): ConnectConfig { } function getSshTunnelConfig(config: Partial): SshTunnelConfig { - const connectConfig = { port: 22, ...getConnectConfig(config) }; - - return Object.assign( - {}, - { - srcPort: 0, - srcAddr: '127.0.0.1', - dstAddr: '127.0.0.1', - dstPort: connectConfig.port, - }, - { - localAddr: '127.0.0.1', - localPort: 0, - }, - config - ); + return { + localAddr: '127.0.0.1', + localPort: 0, + socks5Username: undefined, + socks5Password: undefined, + ...config, + }; } export class SshTunnel extends EventEmitter { private connections: Set = new Set(); - private server: Server; + private server: any; private rawConfig: SshTunnelConfig; private sshClient: SshClient; private serverListen: (port?: number, host?: string) => Promise; @@ -81,44 +72,73 @@ export class SshTunnel extends EventEmitter { this.forwardOut = promisify(this.sshClient.forwardOut.bind(this.sshClient)); // eslint-disable-next-line @typescript-eslint/no-misused-promises - this.server = createServer(async (socket) => { - this.connections.add(socket); - - socket.on('error', (err: ErrorWithOrigin) => { - err.origin = err.origin ?? 'connection'; - this.server.emit('error', err); - }); - - socket.once('close', () => { - this.connections.delete(socket); - }); - - try { - const { srcAddr, srcPort, dstAddr, dstPort } = this.rawConfig; - - const channel = await this.forwardOut( - srcAddr, - srcPort, - dstAddr, - dstPort - ); - - socket.pipe(channel).pipe(socket); - } catch (err) { - (err as any).origin = 'ssh-client'; - socket.destroy(err as Error); + this.server = socks5Server.createServer( + async ( + info: any, + accept: (intercept: true) => Socket, + deny: () => void + ) => { + debug('receiving socks5 forwarding request', info); + let socket: Socket | null = null; + + try { + const channel = await this.forwardOut( + info.srcAddr, + info.srcPort, + info.dstAddr, + info.dstPort + ); + debug('channel opened, accepting socks5 request', info); + + socket = accept(true); + this.connections.add(socket); + + socket.on('error', (err: ErrorWithOrigin) => { + debug('error on socksv5 socket', info, err); + err.origin = err.origin ?? 'connection'; + this.server.emit('error', err); + }); + + socket.once('close', () => { + debug('socksv5 socket closed, removing from set'); + this.connections.delete(socket as Socket); + }); + + socket.pipe(channel).pipe(socket); + } catch (err) { + debug('caught error, rejecting socks5 request', info, err); + deny(); + if (socket) { + (err as any).origin = 'ssh-client'; + socket.destroy(err as any); + } + } } - }); + ); - this.serverListen = promisify(this.server.listen.bind(this.server)); + if (!this.rawConfig.socks5Username) { + debug('skipping auth setup for this server'); + this.server.useAuth(socks5AuthNone()); + } else { + this.server.useAuth( + socks5AuthUserPassword( + (user: string, pass: string, cb: (success: boolean) => void) => { + const success = + this.rawConfig.socks5Username === user && + this.rawConfig.socks5Password === pass; + debug('validating auth parameters', success); + process.nextTick(cb, success); + } + ) + ); + } + this.serverListen = promisify(this.server.listen.bind(this.server)); this.serverClose = promisify(this.server.close.bind(this.server)); - (['close', 'connection', 'error', 'listening'] as const).forEach( - (eventName) => { - this.server.on(eventName, this.emit.bind(this, eventName)); - } - ); + for (const eventName of ['close', 'error', 'listening'] as const) { + this.server.on(eventName, this.emit.bind(this, eventName)); + } } get config(): SshTunnelConfig { @@ -135,9 +155,11 @@ export class SshTunnel extends EventEmitter { async listen(): Promise { const { localPort, localAddr } = this.rawConfig; + debug('starting to listen', { localAddr, localPort }); await this.serverListen(localPort, localAddr); try { + debug('creating SSH connection'); await Promise.race([ once(this.sshClient, 'error').then(([err]) => { throw err; @@ -148,13 +170,16 @@ export class SshTunnel extends EventEmitter { return waitForReady; })(), ]); + debug('created SSH connection'); } catch (err) { + debug('failed to establish SSH connection', err); await this.serverClose(); throw err; } } async close(): Promise { + debug('closing SSH tunnel'); const [maybeError] = await Promise.all([ // If we catch anything, just return the error instead of throwing, we // want to await on closing the connections before re-throwing server diff --git a/packages/ssh-tunnel/src/socksv5.d.ts b/packages/ssh-tunnel/src/socksv5.d.ts new file mode 100644 index 00000000000..9c9cc3abfba --- /dev/null +++ b/packages/ssh-tunnel/src/socksv5.d.ts @@ -0,0 +1,12 @@ +declare module 'socksv5/lib/server' { + const mod: any; + export = mod; +} +declare module 'socksv5/lib/auth/None' { + const mod: any; + export = mod; +} +declare module 'socksv5/lib/auth/UserPassword' { + const mod: any; + export = mod; +} diff --git a/scripts/import-test-connections.js b/scripts/import-test-connections.js index 060dd2d4602..5ab9d3443b0 100644 --- a/scripts/import-test-connections.js +++ b/scripts/import-test-connections.js @@ -201,8 +201,11 @@ if (COMPASS_TEST_SERVERLESS_URL) { 'sshPassword', 'sshIdentityKey', 'sshIdentityKeyWithPassphrase', + 'sshReplicaSetSeedlist', + 'sshReplicaSetByReplSetName', 'tlsUnvalidated', 'tlsServerValidation', + 'tlsServerValidationSsh', 'tlsServerAndClientValidation', 'tlsServerAndClientValidationKeyCrt', 'tlsX509', diff --git a/scripts/package.json b/scripts/package.json index 95606130f4b..608ac8beb11 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -34,7 +34,7 @@ "prettier": "2.3.2" }, "dependencies": { - "@mongodb-js/devtools-docker-test-envs": "^1.2.2", + "@mongodb-js/devtools-docker-test-envs": "^1.2.4", "chalk": "^4.1.1", "electron": "^13.5.1", "find-up": "^5.0.0",