From 1076fb83915057e525d458915fb9990c827132da Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sat, 13 Feb 2021 12:13:10 +0100 Subject: [PATCH] ssh tunnel - alternative modes --- app/package.json | 9 +++ packages/api/src/controllers/config.js | 7 +++ packages/api/src/controllers/runners.js | 2 + packages/api/src/shell/index.js | 2 + .../src/shell/registerProcessCommunication.js | 9 +++ packages/api/src/utility/crypting.js | 28 +++++++--- packages/api/src/utility/platformInfo.js | 27 +++++++++ packages/api/src/utility/sshTunnel.js | 13 +++-- packages/web/src/impexp/createImpExpScript.js | 2 +- packages/web/src/modals/ConnectionModal.js | 55 +++++++++++++++++-- packages/web/src/utility/forms.js | 36 ++++++++++++ packages/web/src/utility/metadataLoaders.js | 13 +++++ 12 files changed, 185 insertions(+), 18 deletions(-) create mode 100644 packages/api/src/shell/registerProcessCommunication.js create mode 100644 packages/api/src/utility/platformInfo.js diff --git a/app/package.json b/app/package.json index 559b95b5..77005ee4 100644 --- a/app/package.json +++ b/app/package.json @@ -37,6 +37,15 @@ "github" ] }, + "snap": { + "publish": [ + "github", + "snapStore" + ], + "environment": { + "ELECTRON_SNAP": "true" + } + }, "win": { "target": [ "nsis" diff --git a/packages/api/src/controllers/config.js b/packages/api/src/controllers/config.js index 7baa3320..16f9efae 100644 --- a/packages/api/src/controllers/config.js +++ b/packages/api/src/controllers/config.js @@ -1,4 +1,5 @@ const currentVersion = require('../currentVersion'); +const platformInfo = require('../utility/platformInfo'); module.exports = { get_meta: 'get', @@ -31,4 +32,10 @@ module.exports = { ...currentVersion, }; }, + + platformInfo_meta: 'get', + async platformInfo() { + return platformInfo; + }, + }; diff --git a/packages/api/src/controllers/runners.js b/packages/api/src/controllers/runners.js index bec81e62..e1c9a225 100644 --- a/packages/api/src/controllers/runners.js +++ b/packages/api/src/controllers/runners.js @@ -24,6 +24,7 @@ const requirePluginsTemplate = plugins => const scriptTemplate = script => ` const dbgateApi = require(process.env.DBGATE_API); +dbgateApi.registerProcessCommunication(); ${requirePluginsTemplate(extractPlugins(script))} require=null; async function run() { @@ -36,6 +37,7 @@ dbgateApi.runScript(run); const loaderScriptTemplate = (functionName, props, runid) => ` const dbgateApi = require(process.env.DBGATE_API); +dbgateApi.registerProcessCommunication(); ${requirePluginsTemplate(extractShellApiPlugins(functionName, props))} require=null; async function run() { diff --git a/packages/api/src/shell/index.js b/packages/api/src/shell/index.js index 403501ec..492adc6f 100644 --- a/packages/api/src/shell/index.js +++ b/packages/api/src/shell/index.js @@ -17,6 +17,7 @@ const requirePlugin = require('./requirePlugin'); const download = require('./download'); const executeQuery = require('./executeQuery'); const loadFile = require('./loadFile'); +const registerProcessCommunication = require('./registerProcessCommunication'); const dbgateApi = { queryReader, @@ -37,6 +38,7 @@ const dbgateApi = { registerPlugins, executeQuery, loadFile, + registerProcessCommunication, }; requirePlugin.initializeDbgateApi(dbgateApi); diff --git a/packages/api/src/shell/registerProcessCommunication.js b/packages/api/src/shell/registerProcessCommunication.js new file mode 100644 index 00000000..d090bd7c --- /dev/null +++ b/packages/api/src/shell/registerProcessCommunication.js @@ -0,0 +1,9 @@ +const { handleProcessCommunication } = require('../utility/processComm'); + +async function registerProcessCommunication() { + process.on('message', async message => { + handleProcessCommunication(message); + }); +} + +module.exports = registerProcessCommunication; diff --git a/packages/api/src/utility/crypting.js b/packages/api/src/utility/crypting.js index 98c26718..8fad163a 100644 --- a/packages/api/src/utility/crypting.js +++ b/packages/api/src/utility/crypting.js @@ -42,31 +42,45 @@ function getEncryptor() { return _encryptor; } -function encryptConnection(connection) { +function encryptPasswordField(connection, field) { if ( connection && - connection.password && - !connection.password.startsWith('crypt:') && + connection[field] && + !connection[field].startsWith('crypt:') && connection.passwordMode != 'saveRaw' ) { return { ...connection, - password: 'crypt:' + getEncryptor().encrypt(connection.password), + [field]: 'crypt:' + getEncryptor().encrypt(connection[field]), }; } return connection; } -function decryptConnection(connection) { - if (connection && connection.password && connection.password.startsWith('crypt:')) { +function decryptPasswordField(connection, field) { + if (connection && connection[field] && connection[field].startsWith('crypt:')) { return { ...connection, - password: getEncryptor().decrypt(connection.password.substring('crypt:'.length)), + [field]: getEncryptor().decrypt(connection[field].substring('crypt:'.length)), }; } 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 = { loadEncryptionKey, encryptConnection, diff --git a/packages/api/src/utility/platformInfo.js b/packages/api/src/utility/platformInfo.js new file mode 100644 index 00000000..22ba0fb2 --- /dev/null +++ b/packages/api/src/utility/platformInfo.js @@ -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; diff --git a/packages/api/src/utility/sshTunnel.js b/packages/api/src/utility/sshTunnel.js index c37b9e72..2744f695 100644 --- a/packages/api/src/utility/sshTunnel.js +++ b/packages/api/src/utility/sshTunnel.js @@ -1,7 +1,9 @@ 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 sshConnectionCache = {}; const sshTunnelCache = {}; @@ -16,11 +18,14 @@ async function getSshConnection(connection) { const sshConfig = { endHost: connection.sshHost || '', endPort: connection.sshPort || 22, - bastionHost: '', - agentForward: false, - passphrase: undefined, + bastionHost: connection.sshBastionHost || '', + agentForward: connection.sshMode == 'agent', + passphrase: connection.sshMode == 'keyFile' ? connection.sshKeyFilePassword : undefined, 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, noReadline: true, }; diff --git a/packages/web/src/impexp/createImpExpScript.js b/packages/web/src/impexp/createImpExpScript.js index 5b29fc17..b1ff8261 100644 --- a/packages/web/src/impexp/createImpExpScript.js +++ b/packages/web/src/impexp/createImpExpScript.js @@ -31,7 +31,7 @@ async function getConnection(extensions, storageType, conid, database) { const driver = findEngineDriver(conn, extensions); return [ { - ..._.pick(conn, ['server', 'engine', 'user', 'password', 'port', 'authType']), + ..._.omit(conn, ['_id', 'displayName']), database, }, driver, diff --git a/packages/web/src/modals/ConnectionModal.js b/packages/web/src/modals/ConnectionModal.js index f189e633..ec8bdf7c 100644 --- a/packages/web/src/modals/ConnectionModal.js +++ b/packages/web/src/modals/ConnectionModal.js @@ -8,6 +8,7 @@ import { FormSubmit, FormPasswordField, FormCheckboxField, + FormElectronFileSelector, } from '../utility/forms'; import ModalHeader from './ModalHeader'; import ModalFooter from './ModalFooter'; @@ -17,6 +18,8 @@ import LoadingInfo from '../widgets/LoadingInfo'; import { FontIcon } from '../icons'; import { FormProvider, useForm } from '../utility/FormProvider'; import { TabControl, TabPage } from '../widgets/TabControl'; +import { usePlatformInfo } from '../utility/metadataLoaders'; +import getElectron from '../utility/getElectron'; // import FormikForm from '../utility/FormikForm'; function DriverFields({ extensions }) { @@ -69,15 +72,55 @@ function DriverFields({ extensions }) { } function SshTunnelFields() { - const { values } = useForm(); - const { useSshTunnel } = values; + const { values, setFieldValue } = useForm(); + 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 ( <> - - - - + + + + + + + + {!!electron && } + + + + + {sshMode == 'userPassword' && } + {useSshTunnel && + sshMode == 'agent' && + (platformInfo.sshAuthSock ? ( +
+ SSH Agent found +
+ ) : ( +
+ SSH Agent not found +
+ ))} + + {sshMode == 'keyFile' && ( + + )} + + {sshMode == 'keyFile' && ( + + )} ); } diff --git a/packages/web/src/utility/forms.js b/packages/web/src/utility/forms.js index d61f0406..0d6ef3c4 100644 --- a/packages/web/src/utility/forms.js +++ b/packages/web/src/utility/forms.js @@ -15,6 +15,13 @@ import axios from './axios'; import useTheme from '../theme/useTheme'; import { useForm, useFormFieldTemplate } from './FormProvider'; 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 }) { 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 ( + + + Browse + + ); +} + +export function FormElectronFileSelector({ label, name, ...other }) { + const FieldTemplate = useFormFieldTemplate(); + return ( + + + + ); +} diff --git a/packages/web/src/utility/metadataLoaders.js b/packages/web/src/utility/metadataLoaders.js index 244d9c5d..946adfa0 100644 --- a/packages/web/src/utility/metadataLoaders.js +++ b/packages/web/src/utility/metadataLoaders.js @@ -46,6 +46,12 @@ const configLoader = () => ({ reloadTrigger: 'config-changed', }); +const platformInfoLoader = () => ({ + url: 'config/platform-info', + params: {}, + reloadTrigger: 'platform-info-changed', +}); + const favoritesLoader = () => ({ url: 'files/favorites', params: {}, @@ -253,6 +259,13 @@ export function useConfig() { return useCore(configLoader, {}) || {}; } +export function getPlatformInfo() { + return getCore(platformInfoLoader, {}) || {}; +} +export function usePlatformInfo() { + return useCore(platformInfoLoader, {}) || {}; +} + export function getArchiveFiles(args) { return getCore(archiveFilesLoader, args); }