diff --git a/packages/api/package.json b/packages/api/package.json index e084d3497..25cd1c693 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -28,6 +28,7 @@ "dbgate-query-splitter": "^4.9.0", "dbgate-sqltree": "^5.0.0-alpha.1", "dbgate-tools": "^5.0.0-alpha.1", + "debug": "^4.3.4", "diff": "^5.0.0", "diff2html": "^3.4.13", "eslint": "^6.8.0", @@ -45,9 +46,9 @@ "lodash": "^4.17.21", "ncp": "^2.0.0", "node-cron": "^2.0.3", - "node-ssh-forward": "^0.7.2", "portfinder": "^1.0.28", "simple-encryptor": "^4.0.0", + "ssh2": "^1.11.0", "tar": "^6.0.5", "uuid": "^3.4.0" }, diff --git a/packages/api/src/proc/sshForwardProcess.js b/packages/api/src/proc/sshForwardProcess.js index 566d5550a..75a5a85a1 100644 --- a/packages/api/src/proc/sshForwardProcess.js +++ b/packages/api/src/proc/sshForwardProcess.js @@ -1,8 +1,8 @@ const fs = require('fs-extra'); const platformInfo = require('../utility/platformInfo'); const childProcessChecker = require('../utility/childProcessChecker'); -const { SSHConnection } = require('node-ssh-forward'); const { handleProcessCommunication } = require('../utility/processComm'); +const { SSHConnection } = require('../utility/SSHConnection'); async function getSshConnection(connection) { const sshConfig = { diff --git a/packages/api/src/utility/SSHConnection.js b/packages/api/src/utility/SSHConnection.js new file mode 100644 index 000000000..1561ad633 --- /dev/null +++ b/packages/api/src/utility/SSHConnection.js @@ -0,0 +1,251 @@ +/* + * Copyright 2018 Stocard GmbH. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const { Client } = require('ssh2'); +const net = require('net'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const debug = require('debug'); + +// interface Options { +// username?: string; +// password?: string; +// privateKey?: string | Buffer; +// agentForward?: boolean; +// bastionHost?: string; +// passphrase?: string; +// endPort?: number; +// endHost: string; +// agentSocket?: string; +// skipAutoPrivateKey?: boolean; +// noReadline?: boolean; +// } + +// interface ForwardingOptions { +// fromPort: number; +// toPort: number; +// toHost?: string; +// } + +class SSHConnection { + constructor(options) { + this.options = options; + this.debug = debug('ssh'); + this.connections = []; + this.isWindows = process.platform === 'win32'; + if (!options.username) { + this.options.username = process.env['SSH_USERNAME'] || process.env['USER']; + } + if (!options.endPort) { + this.options.endPort = 22; + } + if (!options.privateKey && !options.agentForward && !options.skipAutoPrivateKey) { + const defaultFilePath = path.join(os.homedir(), '.ssh', 'id_rsa'); + if (fs.existsSync(defaultFilePath)) { + this.options.privateKey = fs.readFileSync(defaultFilePath); + } + } + } + + async shutdown() { + this.debug('Shutdown connections'); + for (const connection of this.connections) { + connection.removeAllListeners(); + connection.end(); + } + return new Promise(resolve => { + if (this.server) { + this.server.close(resolve); + } + return resolve(); + }); + } + + async tty() { + const connection = await this.establish(); + this.debug('Opening tty'); + await this.shell(connection); + } + + async executeCommand(command) { + const connection = await this.establish(); + this.debug('Executing command "%s"', command); + await this.shell(connection, command); + } + + async shell(connection, command) { + return new Promise((resolve, reject) => { + connection.shell((err, stream) => { + if (err) { + return reject(err); + } + stream + .on('close', async () => { + stream.end(); + process.stdin.unpipe(stream); + process.stdin.destroy(); + connection.end(); + await this.shutdown(); + return resolve(); + }) + .stderr.on('data', data => { + return reject(data); + }); + stream.pipe(process.stdout); + + if (command) { + stream.end(`${command}\nexit\n`); + } else { + process.stdin.pipe(stream); + } + }); + }); + } + + async establish() { + let connection; + if (this.options.bastionHost) { + connection = await this.connectViaBastion(this.options.bastionHost); + } else { + connection = await this.connect(this.options.endHost); + } + return connection; + } + + async connectViaBastion(bastionHost) { + this.debug('Connecting to bastion host "%s"', bastionHost); + const connectionToBastion = await this.connect(bastionHost); + return new Promise((resolve, reject) => { + connectionToBastion.forwardOut( + '127.0.0.1', + 22, + this.options.endHost, + this.options.endPort || 22, + async (err, stream) => { + if (err) { + return reject(err); + } + const connection = await this.connect(this.options.endHost, stream); + return resolve(connection); + } + ); + }); + } + + async connect(host, stream) { + this.debug('Connecting to "%s"', host); + const connection = new Client(); + return new Promise(async (resolve, reject) => { + const options = { + host, + port: this.options.endPort, + username: this.options.username, + password: this.options.password, + privateKey: this.options.privateKey, + }; + if (this.options.agentForward) { + options['agentForward'] = true; + + // see https://github.com/mscdex/ssh2#client for agents on Windows + // guaranteed to give the ssh agent sock if the agent is running (posix) + let agentDefault = process.env['SSH_AUTH_SOCK']; + if (this.isWindows) { + // null or undefined + if (agentDefault == null) { + agentDefault = 'pageant'; + } + } + + const agentSock = this.options.agentSocket ? this.options.agentSocket : agentDefault; + if (agentSock == null) { + throw new Error('SSH Agent Socket is not provided, or is not set in the SSH_AUTH_SOCK env variable'); + } + options['agent'] = agentSock; + } + if (stream) { + options['sock'] = stream; + } + // PPK private keys can be encrypted, but won't contain the word 'encrypted' + // in fact they always contain a `encryption` header, so we can't do a simple check + options['passphrase'] = this.options.passphrase; + const looksEncrypted = this.options.privateKey + ? this.options.privateKey.toString().toLowerCase().includes('encrypted') + : false; + if (looksEncrypted && !options['passphrase'] && !this.options.noReadline) { + // options['passphrase'] = await this.getPassphrase(); + } + connection.on('ready', () => { + this.connections.push(connection); + return resolve(connection); + }); + + connection.on('error', error => { + reject(error); + }); + try { + connection.connect(options); + } catch (error) { + reject(error); + } + }); + } + + // private async getPassphrase() { + // return new Promise(resolve => { + // const rl = readline.createInterface({ + // input: process.stdin, + // output: process.stdout, + // }); + // rl.question('Please type in the passphrase for your private key: ', answer => { + // return resolve(answer); + // }); + // }); + // } + + async forward(options) { + const connection = await this.establish(); + return new Promise((resolve, reject) => { + this.server = net + .createServer(socket => { + this.debug( + 'Forwarding connection from "localhost:%d" to "%s:%d"', + options.fromPort, + options.toHost, + options.toPort + ); + connection.forwardOut( + 'localhost', + options.fromPort, + options.toHost || 'localhost', + options.toPort, + (error, stream) => { + if (error) { + return reject(error); + } + socket.pipe(stream); + stream.pipe(socket); + } + ); + }) + .listen(options.fromPort, 'localhost', () => { + return resolve(); + }); + }); + } +} + +module.exports = { SSHConnection }; diff --git a/packages/api/src/utility/connectUtility.js b/packages/api/src/utility/connectUtility.js index 5fa33ff6e..52ef41831 100644 --- a/packages/api/src/utility/connectUtility.js +++ b/packages/api/src/utility/connectUtility.js @@ -1,8 +1,5 @@ -const { SSHConnection } = require('node-ssh-forward'); -const portfinder = require('portfinder'); const fs = require('fs-extra'); const { decryptConnection } = require('./crypting'); -const { getSshTunnel } = require('./sshTunnel'); const { getSshTunnelProxy } = require('./sshTunnelProxy'); const platformInfo = require('../utility/platformInfo'); const connections = require('../controllers/connections'); diff --git a/yarn.lock b/yarn.lock index 4f88aa70d..ef005b6ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1941,7 +1941,14 @@ asn1.js@^4.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" -asn1@~0.2.0, asn1@~0.2.3: +asn1@^0.2.4: + version "0.2.6" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + +asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== @@ -2471,6 +2478,11 @@ bufferutil@^4.0.1: dependencies: node-gyp-build "~3.7.0" +buildcheck@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/buildcheck/-/buildcheck-0.0.3.tgz#70451897a95d80f7807e68fc412eb2e7e35ff4d5" + integrity sha512-pziaA+p/wdVImfcbsZLNF32EiWyujlQLwolMqUQE8xpKNOH7KmZQaY8sXN7DGOEzPAElo9QTaeNRfGnf3iOJbA== + builtin-modules@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887" @@ -3090,6 +3102,14 @@ cors@^2.8.5: object-assign "^4" vary "^1" +cpu-features@~0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/cpu-features/-/cpu-features-0.0.4.tgz#0023475bb4f4c525869c162e4108099e35bf19d8" + integrity sha512-fKiZ/zp1mUwQbnzb9IghXtHtDoTMtNeb8oYGx6kX2SYfhnG0HNdBEBIzB9b5KlXu5DQPhfy3mInbBxFcgwAr3A== + dependencies: + buildcheck "0.0.3" + nan "^2.15.0" + crc-32@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208" @@ -3353,6 +3373,13 @@ debug@^4.3.1: dependencies: ms "2.1.2" +debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + decamelize-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" @@ -7794,6 +7821,11 @@ nan@^2.15.0: resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== +nan@^2.16.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.16.0.tgz#664f43e45460fb98faf00edca0bb0d7b8dce7916" + integrity sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -7978,14 +8010,6 @@ node-releases@^1.1.71: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.72.tgz#14802ab6b1039a79a0c7d662b610a5bbd76eacbe" integrity sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw== -node-ssh-forward@^0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/node-ssh-forward/-/node-ssh-forward-0.7.2.tgz#a5103ba9ae0e044156b0568a5084304e7d5b4a2a" - integrity sha512-dQGhwT9emJJ0PymZGXdwHue18oc+bnAeqSGUCi4+YufGLaTSfA4Ft04T97WGkVxmBUySy/O6lvKIfpJM0a8lgA== - dependencies: - debug "^4.1.1" - ssh2 "^0.8.9" - node-xml-stream-parser@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/node-xml-stream-parser/-/node-xml-stream-parser-1.0.12.tgz#2d97cc91147bbcdffa0a89cf7dfcc129db2aa789" @@ -10141,21 +10165,16 @@ ssf@~0.11.2: dependencies: frac "~1.1.2" -ssh2-streams@~0.4.10: - version "0.4.10" - resolved "https://registry.yarnpkg.com/ssh2-streams/-/ssh2-streams-0.4.10.tgz#48ef7e8a0e39d8f2921c30521d56dacb31d23a34" - integrity sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ== +ssh2@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.11.0.tgz#ce60186216971e12f6deb553dcf82322498fe2e4" + integrity sha512-nfg0wZWGSsfUe/IBJkXVll3PEZ//YH2guww+mP88gTpuSU4FtZN7zu9JoeTGOyCNx2dTDtT9fOpWwlzyj4uOOw== dependencies: - asn1 "~0.2.0" + asn1 "^0.2.4" bcrypt-pbkdf "^1.0.2" - streamsearch "~0.1.2" - -ssh2@^0.8.9: - version "0.8.9" - resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-0.8.9.tgz#54da3a6c4ba3daf0d8477a538a481326091815f3" - integrity sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw== - dependencies: - ssh2-streams "~0.4.10" + optionalDependencies: + cpu-features "~0.0.4" + nan "^2.16.0" sshpk@^1.7.0: version "1.16.1" @@ -10265,7 +10284,7 @@ stream-transform@^2.1.0: dependencies: mixme "^0.5.0" -streamsearch@0.1.2, streamsearch@~0.1.2: +streamsearch@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=