dbgate-api
Version:
Allows run DbGate data-manipulation scripts.
130 lines (118 loc) • 3.99 kB
JavaScript
const portfinder = require('portfinder');
const stableStringify = require('json-stable-stringify');
const _ = require('lodash');
const AsyncLock = require('async-lock');
const lock = new AsyncLock();
const { fork } = require('child_process');
const processArgs = require('../utility/processArgs');
const { getLogger, extractErrorLogData } = require('dbgate-tools');
const pipeForkLogs = require('./pipeForkLogs');
const logger = getLogger('sshTunnel');
const sshTunnelCache = {};
const CONNECTION_FIELDS = [
'sshHost',
'sshPort',
'sshLogin',
'sshPassword',
'sshMode',
'sshKeyfile',
'sshBastionHost',
'sshKeyfilePassword',
];
const TUNNEL_FIELDS = [...CONNECTION_FIELDS, 'server', 'port'];
function callForwardProcess(connection, tunnelConfig, tunnelCacheKey) {
let subprocess = fork(
global['API_PACKAGE'] || process.argv[1],
['--is-forked-api', '--start-process', 'sshForwardProcess', ...processArgs.getPassArgs()],
{
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
}
);
pipeForkLogs(subprocess);
try {
subprocess.send({
msgtype: 'connect',
connection,
tunnelConfig,
});
} catch (err) {
logger.error(extractErrorLogData(err), 'DBGM-00174 Error connecting SSH');
}
return new Promise((resolve, reject) => {
let promiseHandled = false;
subprocess.on('message', resp => {
// @ts-ignore
const { msgtype, errorMessage } = resp;
if (msgtype == 'connected') {
resolve(subprocess);
promiseHandled = true;
}
if (msgtype == 'error') {
reject(new Error(errorMessage));
promiseHandled = true;
}
});
subprocess.on('exit', code => {
logger.info(`DBGM-00090 SSH forward process exited with code ${code}`);
delete sshTunnelCache[tunnelCacheKey];
if (!promiseHandled) {
reject(
new Error(
'DBGM-00091 SSH forward process exited, try to change "Local host address for SSH connections" in Settings/Connections'
)
);
}
});
subprocess.on('error', error => {
logger.error(extractErrorLogData(error), 'DBGM-00092 SSH forward process error');
delete sshTunnelCache[tunnelCacheKey];
if (!promiseHandled) {
reject(error);
}
});
});
}
async function getSshTunnel(connection) {
const config = require('../controllers/config');
const tunnelCacheKey = stableStringify(_.pick(connection, TUNNEL_FIELDS));
const globalSettings = await config.getSettings();
return await lock.acquire(tunnelCacheKey, async () => {
if (sshTunnelCache[tunnelCacheKey]) return sshTunnelCache[tunnelCacheKey];
const localPort = await portfinder.getPortPromise({ port: 10000, stopPort: 60000 });
const localHost = globalSettings?.['connection.sshBindHost'] || '127.0.0.1';
// workaround for `getPortPromise` not releasing the port quickly enough
await new Promise(resolve => setTimeout(resolve, 500));
const tunnelConfig = {
fromPort: localPort,
fromHost: localHost,
toPort: connection.port,
toHost: connection.server,
};
try {
logger.info(
`DBGM-00093 Creating SSH tunnel to ${connection.sshHost}-${connection.server}:${connection.port}, using local port ${localPort}`
);
const subprocess = await callForwardProcess(connection, tunnelConfig, tunnelCacheKey);
logger.info(
`DBGM-00094 Created SSH tunnel to ${connection.sshHost}-${connection.server}:${connection.port}, using local port ${localPort}`
);
sshTunnelCache[tunnelCacheKey] = {
state: 'ok',
localPort,
localHost,
subprocess,
};
return sshTunnelCache[tunnelCacheKey];
} catch (err) {
logger.error(extractErrorLogData(err), 'DBGM-00095 Error creating SSH tunnel:');
// error is not cached
return {
state: 'error',
message: err.message,
};
}
});
}
module.exports = {
getSshTunnel,
};