ssh tunnel - wking POC

This commit is contained in:
Jan Prochazka
2021-02-11 11:34:54 +01:00
parent e243ecd96a
commit 728ca72cc1
15 changed files with 187 additions and 48 deletions

View File

@@ -35,16 +35,18 @@
"find-free-port": "^2.0.0",
"fs-extra": "^8.1.0",
"http": "^0.0.0",
"json-stable-stringify": "^1.0.1",
"line-reader": "^0.4.0",
"lodash": "^4.17.15",
"ncp": "^2.0.0",
"nedb-promises": "^4.0.1",
"node-cron": "^2.0.3",
"node-ssh-forward": "^0.7.2",
"portfinder": "^1.0.28",
"simple-encryptor": "^4.0.0",
"tar": "^6.0.5",
"uuid": "^3.4.0",
"socket.io": "^2.3.0",
"json-stable-stringify": "^1.0.1"
"tar": "^6.0.5",
"uuid": "^3.4.0"
},
"scripts": {
"start": "nodemon src/index.js",
@@ -66,4 +68,4 @@
"optionalDependencies": {
"msnodesqlv8": "^2.0.10"
}
}
}

View File

@@ -1,13 +1,13 @@
const childProcessChecker = require('../utility/childProcessChecker');
const requireEngineDriver = require('../utility/requireEngineDriver');
const { decryptConnection } = require('../utility/crypting');
const connectUtility = require('../utility/connectUtility');
function start() {
childProcessChecker();
process.on('message', async connection => {
try {
const driver = requireEngineDriver(connection);
const conn = await driver.connect(decryptConnection(connection));
const conn = await connectUtility(driver, connection);
const res = await driver.getVersion(conn);
process.send({ msgtype: 'connected', ...res });
} catch (e) {

View File

@@ -1,7 +1,7 @@
const stableStringify = require('json-stable-stringify');
const childProcessChecker = require('../utility/childProcessChecker');
const requireEngineDriver = require('../utility/requireEngineDriver');
const { decryptConnection } = require('../utility/crypting');
const connectUtility = require('../utility/connectUtility');
let systemConnection;
let storedConnection;
@@ -60,7 +60,7 @@ async function handleConnect({ connection, structure }) {
if (!structure) setStatusName('pending');
const driver = requireEngineDriver(storedConnection);
systemConnection = await checkedAsyncCall(driver.connect(decryptConnection(storedConnection)));
systemConnection = await checkedAsyncCall(connectUtility(driver, storedConnection));
if (structure) {
analysedStructure = structure;
handleIncrementalRefresh();

View File

@@ -2,6 +2,7 @@ const stableStringify = require('json-stable-stringify');
const childProcessChecker = require('../utility/childProcessChecker');
const requireEngineDriver = require('../utility/requireEngineDriver');
const { decryptConnection } = require('../utility/crypting');
const connectUtility = require('../utility/connectUtility');
let systemConnection;
let storedConnection;
@@ -48,7 +49,7 @@ async function handleConnect(connection) {
const driver = requireEngineDriver(storedConnection);
try {
systemConnection = await driver.connect(decryptConnection(storedConnection));
systemConnection = await connectUtility(driver, storedConnection);
handleRefresh();
setInterval(handleRefresh, 30 * 1000);
} catch (err) {
@@ -67,7 +68,7 @@ function handlePing() {
async function handleCreateDatabase({ name }) {
const driver = requireEngineDriver(storedConnection);
systemConnection = await driver.connect(decryptConnection(storedConnection));
systemConnection = await connectUtility(driver, storedConnection);
console.log(`RUNNING SCRIPT: CREATE DATABASE ${driver.dialect.quoteIdentifier(name)}`);
await driver.query(systemConnection, `CREATE DATABASE ${driver.dialect.quoteIdentifier(name)}`);
await handleRefresh();

View File

@@ -8,6 +8,7 @@ const goSplit = require('../utility/goSplit');
const { jsldir } = require('../utility/directories');
const requireEngineDriver = require('../utility/requireEngineDriver');
const { decryptConnection } = require('../utility/crypting');
const connectUtility = require('../utility/connectUtility');
let systemConnection;
let storedConnection;
@@ -131,7 +132,7 @@ async function handleConnect(connection) {
storedConnection = connection;
const driver = requireEngineDriver(storedConnection);
systemConnection = await driver.connect(decryptConnection(storedConnection));
systemConnection = await connectUtility(driver, storedConnection);
for (const [resolve] of afterConnectCallbacks) {
resolve();
}

View File

@@ -1,12 +1,13 @@
const goSplit = require('../utility/goSplit');
const requireEngineDriver = require('../utility/requireEngineDriver');
const { decryptConnection } = require('../utility/crypting');
const connectUtility = require('../utility/connectUtility');
async function executeQuery({ connection, sql }) {
console.log(`Execute query ${sql}`);
const driver = requireEngineDriver(connection);
const pool = await driver.connect(decryptConnection(connection));
const pool = await connectUtility(driver, connection);
console.log(`Connected.`);
for (const sqlItem of goSplit(sql)) {

View File

@@ -1,11 +1,12 @@
const requireEngineDriver = require('../utility/requireEngineDriver');
const { decryptConnection } = require('../utility/crypting');
const connectUtility = require('../utility/connectUtility');
async function queryReader({ connection, sql }) {
console.log(`Reading query ${sql}`);
const driver = requireEngineDriver(connection);
const pool = await driver.connect(decryptConnection(connection));
const pool = await connectUtility(driver, connection);
console.log(`Connected.`);
return await driver.readQuery(pool, sql);
}

View File

@@ -1,10 +1,11 @@
const { quoteFullName, fullNameToString } = require('dbgate-tools');
const requireEngineDriver = require('../utility/requireEngineDriver');
const { decryptConnection } = require('../utility/crypting');
const connectUtility = require('../utility/connectUtility');
async function tableReader({ connection, pureName, schemaName }) {
const driver = requireEngineDriver(connection);
const pool = await driver.connect(decryptConnection(connection));
const pool = await connectUtility(driver, connection);
console.log(`Connected.`);
const fullName = { pureName, schemaName };

View File

@@ -1,12 +1,13 @@
const { fullNameToString } = require('dbgate-tools');
const requireEngineDriver = require('../utility/requireEngineDriver');
const { decryptConnection } = require('../utility/crypting');
const connectUtility = require('../utility/connectUtility');
async function tableWriter({ connection, schemaName, pureName, ...options }) {
console.log(`Writing table ${fullNameToString({ schemaName, pureName })}`);
const driver = requireEngineDriver(connection);
const pool = await driver.connect(decryptConnection(connection));
const pool = await connectUtility(driver, connection);
console.log(`Connected.`);
return await driver.writeTable(pool, { schemaName, pureName }, options);
}

View File

@@ -0,0 +1,43 @@
const { SSHConnection } = require('node-ssh-forward');
const portfinder = require('portfinder');
const { decryptConnection } = require('./crypting');
async function connectUtility(driver, storedConnection) {
let connection = decryptConnection(storedConnection);
if (connection.useSshTunnel) {
const sshConfig = {
endHost: connection.sshHost || '',
endPort: connection.sshPort || 22,
bastionHost: '',
agentForward: false,
passphrase: undefined,
username: connection.sshLogin,
password: connection.sshPassword,
skipAutoPrivateKey: true,
noReadline: true,
};
const sshConn = new SSHConnection(sshConfig);
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));
const tunnelConfig = {
fromPort: localPort,
toPort: connection.port,
toHost: connection.server,
};
const tunnel = await sshConn.forward(tunnelConfig);
console.log(`Created SSH tunnel to ${connection.sshHost}-${connection.server}:${connection.port}, using local port ${localPort}`)
connection = {
...connection,
server: '127.0.0.1',
port: localPort,
};
}
const conn = await driver.connect(connection);
return conn;
}
module.exports = connectUtility;

View File

@@ -70,7 +70,7 @@ export default function AboutModal({ modalState }) {
<Link label="Web" href="https://dbgate.org">
dbgate.org
</Link>
<Link label="Source codes" href="https://github.com/dbshell/dbgate/">
<Link label="Source codes" href="https://github.com/dbgate/dbgate/">
github
</Link>
<Link label="Docker container" href="https://hub.docker.com/r/dbgate/dbgate">

View File

@@ -1,7 +1,14 @@
import React from 'react';
import axios from '../utility/axios';
import ModalBase from './ModalBase';
import { FormButton, FormTextField, FormSelectField, FormSubmit, FormPasswordField } from '../utility/forms';
import {
FormButton,
FormTextField,
FormSelectField,
FormSubmit,
FormPasswordField,
FormCheckboxField,
} from '../utility/forms';
import ModalHeader from './ModalHeader';
import ModalFooter from './ModalFooter';
import ModalContent from './ModalContent';
@@ -9,6 +16,7 @@ import useExtensions from '../utility/useExtensions';
import LoadingInfo from '../widgets/LoadingInfo';
import { FontIcon } from '../icons';
import { FormProvider, useForm } from '../utility/FormProvider';
import { TabControl, TabPage } from '../widgets/TabControl';
// import FormikForm from '../utility/FormikForm';
function DriverFields({ extensions }) {
@@ -60,6 +68,20 @@ function DriverFields({ extensions }) {
);
}
function SshTunnelFields() {
const { values } = useForm();
const { useSshTunnel } = values;
return (
<>
<FormCheckboxField label="Use SSH tunnel" name="useSshTunnel" />
<FormTextField label="SSH Host" name="sshHost" disabled={!useSshTunnel} />
<FormTextField label="SSH Port" name="sshPort" disabled={!useSshTunnel} />
<FormTextField label="SSH Login" name="sshLogin" disabled={!useSshTunnel} />
<FormPasswordField label="SSH Password" name="sshPassword" disabled={!useSshTunnel} />
</>
);
}
export default function ConnectionModal({ modalState, connection = undefined }) {
const [sqlConnectResult, setSqlConnectResult] = React.useState(null);
const extensions = useExtensions();
@@ -90,31 +112,38 @@ export default function ConnectionModal({ modalState, connection = undefined })
<ModalBase modalState={modalState}>
<ModalHeader modalState={modalState}>{connection ? 'Edit connection' : 'Add connection'}</ModalHeader>
<FormProvider initialValues={connection || { server: 'localhost', engine: 'mssql@dbgate-plugin-mssql' }}>
<ModalContent>
<FormSelectField label="Database engine" name="engine">
<option value="(select driver)"></option>
{extensions.drivers.map(driver => (
<option value={driver.engine} key={driver.engine}>
{driver.title}
</option>
))}
{/* <option value="mssql">Microsoft SQL Server</option>
<ModalContent noPadding>
<TabControl isInline>
<TabPage label="Main" key="main">
<FormSelectField label="Database engine" name="engine">
<option value="(select driver)"></option>
{extensions.drivers.map(driver => (
<option value={driver.engine} key={driver.engine}>
{driver.title}
</option>
))}
{/* <option value="mssql">Microsoft SQL Server</option>
<option value="mysql">MySQL</option>
<option value="postgres">Postgre SQL</option> */}
</FormSelectField>
<DriverFields extensions={extensions} />
<FormTextField label="Display name" name="displayName" />
{!isTesting && sqlConnectResult && sqlConnectResult.msgtype == 'connected' && (
<div>
Connected: <FontIcon icon="img ok" /> {sqlConnectResult.version}
</div>
)}
{!isTesting && sqlConnectResult && sqlConnectResult.msgtype == 'error' && (
<div>
Connect failed: <FontIcon icon="img error" /> {sqlConnectResult.error}
</div>
)}
{isTesting && <LoadingInfo message="Testing connection" />}
</FormSelectField>
<DriverFields extensions={extensions} />
<FormTextField label="Display name" name="displayName" />
{!isTesting && sqlConnectResult && sqlConnectResult.msgtype == 'connected' && (
<div>
Connected: <FontIcon icon="img ok" /> {sqlConnectResult.version}
</div>
)}
{!isTesting && sqlConnectResult && sqlConnectResult.msgtype == 'error' && (
<div>
Connect failed: <FontIcon icon="img error" /> {sqlConnectResult.error}
</div>
)}
{isTesting && <LoadingInfo message="Testing connection" />}
</TabPage>
<TabPage label="SSH Tunnel" key="sshTunnel">
<SshTunnelFields />
</TabPage>
</TabControl>
</ModalContent>
<ModalFooter>

View File

@@ -5,10 +5,23 @@ import useTheme from '../theme/useTheme';
const Wrapper = styled.div`
border-bottom: 1px solid ${props => props.theme.border};
border-top: 1px solid ${props => props.theme.border};
${props =>
// @ts-ignore
!props.noPadding &&
`
padding: 15px;
`}
`;
export default function ModalContent({ children }) {
export default function ModalContent({ children, noPadding = false }) {
const theme = useTheme();
return <Wrapper theme={theme}>{children}</Wrapper>;
return (
<Wrapper
theme={theme}
// @ts-ignore
noPadding={noPadding}
>
{children}
</Wrapper>
);
}

View File

@@ -28,16 +28,21 @@ const TabNameWrapper = styled.span`
// props.tabVisible ? 'visible' : 'none'};
const TabContainer = styled.div`
${props =>
// @ts-ignore
!props.isInline &&
`
position: absolute;
display: flex;
left: 0;
right: 0
top: 0;
bottom: 0;
bottom: 0;
`}
${props =>
// @ts-ignore
!props.tabVisible && `visibility: hidden;`}
!props.tabVisible && (props.isInline ? `display:none` : `visibility: hidden;`)}
`;
const TabsContainer = styled.div`
@@ -62,7 +67,7 @@ export function TabPage({ key, label, children }) {
return children;
}
export function TabControl({ children, activePageIndex = undefined, activePageLabel = undefined }) {
export function TabControl({ children, activePageIndex = undefined, activePageLabel = undefined, isInline = false }) {
const [value, setValue] = React.useState(0);
// const [mountedTabs, setMountedTabs] = React.useState({});
@@ -112,6 +117,7 @@ export function TabControl({ children, activePageIndex = undefined, activePageLa
<TabContainer
// @ts-ignore
tabVisible={tabVisible}
isInline={isInline}
key={tab.props.key}
>
{childrenArray[index] && childrenArray[index].props.children}