ssh tunnel - alternative modes

This commit is contained in:
Jan Prochazka 2021-02-13 12:13:10 +01:00
parent 114dc0b543
commit 1076fb8391
12 changed files with 185 additions and 18 deletions

View File

@ -37,6 +37,15 @@
"github" "github"
] ]
}, },
"snap": {
"publish": [
"github",
"snapStore"
],
"environment": {
"ELECTRON_SNAP": "true"
}
},
"win": { "win": {
"target": [ "target": [
"nsis" "nsis"

View File

@ -1,4 +1,5 @@
const currentVersion = require('../currentVersion'); const currentVersion = require('../currentVersion');
const platformInfo = require('../utility/platformInfo');
module.exports = { module.exports = {
get_meta: 'get', get_meta: 'get',
@ -31,4 +32,10 @@ module.exports = {
...currentVersion, ...currentVersion,
}; };
}, },
platformInfo_meta: 'get',
async platformInfo() {
return platformInfo;
},
}; };

View File

@ -24,6 +24,7 @@ const requirePluginsTemplate = plugins =>
const scriptTemplate = script => ` const scriptTemplate = script => `
const dbgateApi = require(process.env.DBGATE_API); const dbgateApi = require(process.env.DBGATE_API);
dbgateApi.registerProcessCommunication();
${requirePluginsTemplate(extractPlugins(script))} ${requirePluginsTemplate(extractPlugins(script))}
require=null; require=null;
async function run() { async function run() {
@ -36,6 +37,7 @@ dbgateApi.runScript(run);
const loaderScriptTemplate = (functionName, props, runid) => ` const loaderScriptTemplate = (functionName, props, runid) => `
const dbgateApi = require(process.env.DBGATE_API); const dbgateApi = require(process.env.DBGATE_API);
dbgateApi.registerProcessCommunication();
${requirePluginsTemplate(extractShellApiPlugins(functionName, props))} ${requirePluginsTemplate(extractShellApiPlugins(functionName, props))}
require=null; require=null;
async function run() { async function run() {

View File

@ -17,6 +17,7 @@ const requirePlugin = require('./requirePlugin');
const download = require('./download'); const download = require('./download');
const executeQuery = require('./executeQuery'); const executeQuery = require('./executeQuery');
const loadFile = require('./loadFile'); const loadFile = require('./loadFile');
const registerProcessCommunication = require('./registerProcessCommunication');
const dbgateApi = { const dbgateApi = {
queryReader, queryReader,
@ -37,6 +38,7 @@ const dbgateApi = {
registerPlugins, registerPlugins,
executeQuery, executeQuery,
loadFile, loadFile,
registerProcessCommunication,
}; };
requirePlugin.initializeDbgateApi(dbgateApi); requirePlugin.initializeDbgateApi(dbgateApi);

View File

@ -0,0 +1,9 @@
const { handleProcessCommunication } = require('../utility/processComm');
async function registerProcessCommunication() {
process.on('message', async message => {
handleProcessCommunication(message);
});
}
module.exports = registerProcessCommunication;

View File

@ -42,31 +42,45 @@ function getEncryptor() {
return _encryptor; return _encryptor;
} }
function encryptConnection(connection) { function encryptPasswordField(connection, field) {
if ( if (
connection && connection &&
connection.password && connection[field] &&
!connection.password.startsWith('crypt:') && !connection[field].startsWith('crypt:') &&
connection.passwordMode != 'saveRaw' connection.passwordMode != 'saveRaw'
) { ) {
return { return {
...connection, ...connection,
password: 'crypt:' + getEncryptor().encrypt(connection.password), [field]: 'crypt:' + getEncryptor().encrypt(connection[field]),
}; };
} }
return connection; return connection;
} }
function decryptConnection(connection) { function decryptPasswordField(connection, field) {
if (connection && connection.password && connection.password.startsWith('crypt:')) { if (connection && connection[field] && connection[field].startsWith('crypt:')) {
return { return {
...connection, ...connection,
password: getEncryptor().decrypt(connection.password.substring('crypt:'.length)), [field]: getEncryptor().decrypt(connection[field].substring('crypt:'.length)),
}; };
} }
return connection; return connection;
} }
function encryptConnection(connection) {
connection = encryptPasswordField(connection, 'password');
connection = encryptPasswordField(connection, 'sshPassword');
connection = encryptPasswordField(connection, 'sshKeyFilePassword');
return connection;
}
function decryptConnection(connection) {
connection = decryptPasswordField(connection, 'password');
connection = decryptPasswordField(connection, 'sshPassword');
connection = decryptPasswordField(connection, 'sshKeyFilePassword');
return connection;
}
module.exports = { module.exports = {
loadEncryptionKey, loadEncryptionKey,
encryptConnection, encryptConnection,

View File

@ -0,0 +1,27 @@
const fs = require('fs');
const os = require('os');
const path = require('path');
const p = process;
const platform = p.env.OS_OVERRIDE ? p.env.OS_OVERRIDE : p.platform;
const isWindows = platform === 'win32';
const isMac = platform === 'darwin';
const isLinux = platform === 'linux';
const isDocker = fs.existsSync('/home/dbgate-docker/build');
const platformInfo = {
isWindows,
isMac,
isLinux,
isDocker,
isSnap: p.env.ELECTRON_SNAP,
isPortable: isWindows && p.env.PORTABLE_EXECUTABLE_DIR,
isAppImage: p.env.DESKTOPINTEGRATION === 'AppImageLauncher',
sshAuthSock: p.env.SSH_AUTH_SOCK,
environment: process.env.NODE_ENV,
platform,
runningInWebpack: !!p.env.WEBPACK_DEV_SERVER_URL,
defaultKeyFile: path.join(os.homedir(), '.ssh/id_rsa'),
};
module.exports = platformInfo;

View File

@ -1,7 +1,9 @@
const { SSHConnection } = require('node-ssh-forward'); const { SSHConnection } = require('node-ssh-forward');
const fs = require('fs-extra');
const portfinder = require('portfinder'); const portfinder = require('portfinder');
const stableStringify = require('json-stable-stringify'); const stableStringify = require('json-stable-stringify');
const _ = require('lodash'); const _ = require('lodash');
const platformInfo = require('./platformInfo');
const sshConnectionCache = {}; const sshConnectionCache = {};
const sshTunnelCache = {}; const sshTunnelCache = {};
@ -16,11 +18,14 @@ async function getSshConnection(connection) {
const sshConfig = { const sshConfig = {
endHost: connection.sshHost || '', endHost: connection.sshHost || '',
endPort: connection.sshPort || 22, endPort: connection.sshPort || 22,
bastionHost: '', bastionHost: connection.sshBastionHost || '',
agentForward: false, agentForward: connection.sshMode == 'agent',
passphrase: undefined, passphrase: connection.sshMode == 'keyFile' ? connection.sshKeyFilePassword : undefined,
username: connection.sshLogin, username: connection.sshLogin,
password: connection.sshPassword, 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, skipAutoPrivateKey: true,
noReadline: true, noReadline: true,
}; };

View File

@ -31,7 +31,7 @@ async function getConnection(extensions, storageType, conid, database) {
const driver = findEngineDriver(conn, extensions); const driver = findEngineDriver(conn, extensions);
return [ return [
{ {
..._.pick(conn, ['server', 'engine', 'user', 'password', 'port', 'authType']), ..._.omit(conn, ['_id', 'displayName']),
database, database,
}, },
driver, driver,

View File

@ -8,6 +8,7 @@ import {
FormSubmit, FormSubmit,
FormPasswordField, FormPasswordField,
FormCheckboxField, FormCheckboxField,
FormElectronFileSelector,
} from '../utility/forms'; } from '../utility/forms';
import ModalHeader from './ModalHeader'; import ModalHeader from './ModalHeader';
import ModalFooter from './ModalFooter'; import ModalFooter from './ModalFooter';
@ -17,6 +18,8 @@ import LoadingInfo from '../widgets/LoadingInfo';
import { FontIcon } from '../icons'; import { FontIcon } from '../icons';
import { FormProvider, useForm } from '../utility/FormProvider'; import { FormProvider, useForm } from '../utility/FormProvider';
import { TabControl, TabPage } from '../widgets/TabControl'; import { TabControl, TabPage } from '../widgets/TabControl';
import { usePlatformInfo } from '../utility/metadataLoaders';
import getElectron from '../utility/getElectron';
// import FormikForm from '../utility/FormikForm'; // import FormikForm from '../utility/FormikForm';
function DriverFields({ extensions }) { function DriverFields({ extensions }) {
@ -69,15 +72,55 @@ function DriverFields({ extensions }) {
} }
function SshTunnelFields() { function SshTunnelFields() {
const { values } = useForm(); const { values, setFieldValue } = useForm();
const { useSshTunnel } = values; const { useSshTunnel, sshMode, sshKeyfile } = values;
const platformInfo = usePlatformInfo();
const electron = getElectron();
React.useEffect(() => {
if (useSshTunnel && !sshMode) {
setFieldValue('sshMode', 'userPassword');
}
if (useSshTunnel && sshMode == 'keyFile' && !sshKeyfile) {
setFieldValue('sshKeyfile', platformInfo.defaultKeyFile);
}
}, [useSshTunnel, sshMode]);
return ( return (
<> <>
<FormCheckboxField label="Use SSH tunnel" name="useSshTunnel" /> <FormCheckboxField label="Use SSH tunnel" name="useSshTunnel" />
<FormTextField label="SSH Host" name="sshHost" disabled={!useSshTunnel} /> <FormTextField label="Host" name="sshHost" disabled={!useSshTunnel} />
<FormTextField label="SSH Port" name="sshPort" disabled={!useSshTunnel} /> <FormTextField label="Port" name="sshPort" disabled={!useSshTunnel} />
<FormTextField label="SSH Login" name="sshLogin" disabled={!useSshTunnel} /> <FormTextField label="Bastion host (Jump host)" name="sshBastionHost" disabled={!useSshTunnel} />
<FormPasswordField label="SSH Password" name="sshPassword" disabled={!useSshTunnel} />
<FormSelectField label="SSH Authentication" name="sshMode" disabled={!useSshTunnel}>
<option value="userPassword">Username &amp; password</option>
<option value="agent">SSH agent</option>
{!!electron && <option value="keyFile">Key file</option>}
</FormSelectField>
<FormTextField label="Login" name="sshLogin" disabled={!useSshTunnel} />
{sshMode == 'userPassword' && <FormPasswordField label="Password" name="sshPassword" disabled={!useSshTunnel} />}
{useSshTunnel &&
sshMode == 'agent' &&
(platformInfo.sshAuthSock ? (
<div>
<FontIcon icon="img ok" /> SSH Agent found
</div>
) : (
<div>
<FontIcon icon="img error" /> SSH Agent not found
</div>
))}
{sshMode == 'keyFile' && (
<FormElectronFileSelector label="Private key file" name="sshKeyfile" disabled={!useSshTunnel} />
)}
{sshMode == 'keyFile' && (
<FormPasswordField label="Key file passphrase" name="sshKeyfilePassword" disabled={!useSshTunnel} />
)}
</> </>
); );
} }

View File

@ -15,6 +15,13 @@ import axios from './axios';
import useTheme from '../theme/useTheme'; import useTheme from '../theme/useTheme';
import { useForm, useFormFieldTemplate } from './FormProvider'; import { useForm, useFormFieldTemplate } from './FormProvider';
import { FontIcon } from '../icons'; import { FontIcon } from '../icons';
import getElectron from './getElectron';
import InlineButton from '../widgets/InlineButton';
import styled from 'styled-components';
const FlexContainer = styled.div`
display: flex;
`;
export function FormFieldTemplate({ label, children, type }) { export function FormFieldTemplate({ label, children, type }) {
const FieldTemplate = useFormFieldTemplate(); const FieldTemplate = useFormFieldTemplate();
@ -321,3 +328,32 @@ export function FormArchiveFolderSelect({ name, additionalFolders = [], ...other
/> />
); );
} }
export function FormElectronFileSelectorRaw({ name }) {
const { values, setFieldValue } = useForm();
const handleBrowse = () => {
const electron = getElectron();
if (!electron) return;
const filePaths = electron.remote.dialog.showOpenDialogSync(electron.remote.getCurrentWindow(), {
defaultPath: values[name],
properties: ['showHiddenFiles'],
});
const filePath = filePaths && filePaths[0];
if (filePath) setFieldValue(name, filePath);
};
return (
<FlexContainer>
<TextField value={values[name]} onClick={handleBrowse} readOnly />
<InlineButton onClick={handleBrowse}>Browse</InlineButton>
</FlexContainer>
);
}
export function FormElectronFileSelector({ label, name, ...other }) {
const FieldTemplate = useFormFieldTemplate();
return (
<FieldTemplate label={label} type="select">
<FormElectronFileSelectorRaw name={name} {...other} />
</FieldTemplate>
);
}

View File

@ -46,6 +46,12 @@ const configLoader = () => ({
reloadTrigger: 'config-changed', reloadTrigger: 'config-changed',
}); });
const platformInfoLoader = () => ({
url: 'config/platform-info',
params: {},
reloadTrigger: 'platform-info-changed',
});
const favoritesLoader = () => ({ const favoritesLoader = () => ({
url: 'files/favorites', url: 'files/favorites',
params: {}, params: {},
@ -253,6 +259,13 @@ export function useConfig() {
return useCore(configLoader, {}) || {}; return useCore(configLoader, {}) || {};
} }
export function getPlatformInfo() {
return getCore(platformInfoLoader, {}) || {};
}
export function usePlatformInfo() {
return useCore(platformInfoLoader, {}) || {};
}
export function getArchiveFiles(args) { export function getArchiveFiles(args) {
return getCore(archiveFilesLoader, args); return getCore(archiveFilesLoader, args);
} }