mirror of
https://github.com/dbgate/dbgate
synced 2024-11-07 20:26:23 +00:00
ssh tunnel - alternative modes
This commit is contained in:
parent
114dc0b543
commit
1076fb8391
@ -37,6 +37,15 @@
|
|||||||
"github"
|
"github"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"snap": {
|
||||||
|
"publish": [
|
||||||
|
"github",
|
||||||
|
"snapStore"
|
||||||
|
],
|
||||||
|
"environment": {
|
||||||
|
"ELECTRON_SNAP": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
"win": {
|
"win": {
|
||||||
"target": [
|
"target": [
|
||||||
"nsis"
|
"nsis"
|
||||||
|
@ -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;
|
||||||
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@ -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() {
|
||||||
|
@ -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);
|
||||||
|
9
packages/api/src/shell/registerProcessCommunication.js
Normal file
9
packages/api/src/shell/registerProcessCommunication.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
const { handleProcessCommunication } = require('../utility/processComm');
|
||||||
|
|
||||||
|
async function registerProcessCommunication() {
|
||||||
|
process.on('message', async message => {
|
||||||
|
handleProcessCommunication(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerProcessCommunication;
|
@ -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,
|
||||||
|
27
packages/api/src/utility/platformInfo.js
Normal file
27
packages/api/src/utility/platformInfo.js
Normal 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;
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
@ -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 & 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} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user