diff --git a/packages/api/src/proc/sshForwardProcess.js b/packages/api/src/proc/sshForwardProcess.js new file mode 100644 index 000000000..4fcd5e4cc --- /dev/null +++ b/packages/api/src/proc/sshForwardProcess.js @@ -0,0 +1,68 @@ +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'); + +async function getSshConnection(connection) { + const sshConfig = { + endHost: connection.sshHost || '', + endPort: connection.sshPort || 22, + bastionHost: connection.sshBastionHost || '', + agentForward: connection.sshMode == 'agent', + passphrase: connection.sshMode == 'keyFile' ? connection.sshKeyfilePassword : undefined, + username: connection.sshLogin, + password: connection.sshMode == 'userPassword' ? connection.sshPassword : undefined, + agentSocket: connection.sshMode == 'agent' ? platformInfo.sshAuthSock : undefined, + privateKey: + connection.sshMode == 'keyFile' && connection.sshKeyfile ? await fs.readFile(connection.sshKeyfile) : undefined, + skipAutoPrivateKey: true, + noReadline: true, + }; + + const sshConn = new SSHConnection(sshConfig); + return sshConn; +} + +async function handleStart({ connection, tunnelConfig }) { + try { + const sshConn = await getSshConnection(connection); + await sshConn.forward(tunnelConfig); + + process.send({ + msgtype: 'connected', + connection, + tunnelConfig, + }); + } catch (err) { + process.send({ + msgtype: 'error', + connection, + tunnelConfig, + errorMessage: err.message, + }); + } +} + +const messageHandlers = { + connect: handleStart, +}; + +async function handleMessage({ msgtype, ...other }) { + const handler = messageHandlers[msgtype]; + await handler(other); +} + +function start() { + childProcessChecker(); + process.on('message', async message => { + if (handleProcessCommunication(message)) return; + try { + await handleMessage(message); + } catch (e) { + console.error('sshForwardProcess - unhandled error', e); + } + }); +} + +module.exports = { start }; diff --git a/packages/api/src/utility/sshTunnel.js b/packages/api/src/utility/sshTunnel.js index a330821dd..b582d3983 100644 --- a/packages/api/src/utility/sshTunnel.js +++ b/packages/api/src/utility/sshTunnel.js @@ -1,13 +1,12 @@ -const { SSHConnection } = require('node-ssh-forward'); -const fs = require('fs-extra'); const portfinder = require('portfinder'); const stableStringify = require('json-stable-stringify'); const _ = require('lodash'); -const platformInfo = require('./platformInfo'); const AsyncLock = require('async-lock'); const lock = new AsyncLock(); +const { fork } = require('child_process'); +const processArgs = require('../utility/processArgs'); -const sshConnectionCache = {}; +// const sshConnectionCache = {}; const sshTunnelCache = {}; const CONNECTION_FIELDS = [ @@ -22,37 +21,41 @@ const CONNECTION_FIELDS = [ ]; const TUNNEL_FIELDS = [...CONNECTION_FIELDS, 'server', 'port']; -async function getSshConnection(connection) { - const connectionCacheKey = stableStringify(_.pick(connection, CONNECTION_FIELDS)); - if (sshConnectionCache[connectionCacheKey]) return sshConnectionCache[connectionCacheKey]; +function callForwardProcess(connection, tunnelConfig) { + let subprocess = fork(global['API_PACKAGE'] || process.argv[1], [ + '--is-forked-api', + '--start-process', + 'sshForwardProcess', + ...processArgs.getPassArgs(), + ]); - const sshConfig = { - endHost: connection.sshHost || '', - endPort: connection.sshPort || 22, - bastionHost: connection.sshBastionHost || '', - agentForward: connection.sshMode == 'agent', - passphrase: connection.sshMode == 'keyFile' ? connection.sshKeyfilePassword : undefined, - username: connection.sshLogin, - password: connection.sshMode == 'userPassword' ? connection.sshPassword : undefined, - agentSocket: connection.sshMode == 'agent' ? platformInfo.sshAuthSock : undefined, - privateKey: - connection.sshMode == 'keyFile' && connection.sshKeyfile ? await fs.readFile(connection.sshKeyfile) : undefined, - skipAutoPrivateKey: true, - noReadline: true, - }; - - const sshConn = new SSHConnection(sshConfig); - sshConnectionCache[connectionCacheKey] = sshConn; - return sshConn; + subprocess.send({ + msgtype: 'connect', + connection, + tunnelConfig, + }); + return new Promise((resolve, reject) => { + subprocess.on('message', resp => { + // @ts-ignore + const { msgtype, errorMessage } = resp; + if (msgtype == 'connected') { + resolve(resp); + } + if (msgtype == 'error') { + reject(errorMessage); + } + }); + subprocess.on('exit', code => { + console.log('SSH forward process exited'); + }); + }); } async function getSshTunnel(connection) { const tunnelCacheKey = stableStringify(_.pick(connection, TUNNEL_FIELDS)); return await lock.acquire(tunnelCacheKey, async () => { - const sshConn = await getSshConnection(connection); if (sshTunnelCache[tunnelCacheKey]) return sshTunnelCache[tunnelCacheKey]; - const localPort = await portfinder.getPortPromise({ port: 10000, stopPort: 60000 }); // workaround for `getPortPromise` not releasing the port quickly enough await new Promise(resolve => setTimeout(resolve, 500)); @@ -66,7 +69,8 @@ async function getSshTunnel(connection) { `Creating SSH tunnel to ${connection.sshHost}-${connection.server}:${connection.port}, using local port ${localPort}` ); - const tunnel = await sshConn.forward(tunnelConfig); + await callForwardProcess(connection, tunnelConfig); + console.log( `Created SSH tunnel to ${connection.sshHost}-${connection.server}:${connection.port}, using local port ${localPort}` );