diff --git a/packages/api/package.json b/packages/api/package.json index b86efe77f..b672f80b2 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/packages/api/src/proc/connectProcess.js b/packages/api/src/proc/connectProcess.js index dcbe0466b..570899443 100644 --- a/packages/api/src/proc/connectProcess.js +++ b/packages/api/src/proc/connectProcess.js @@ -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) { diff --git a/packages/api/src/proc/databaseConnectionProcess.js b/packages/api/src/proc/databaseConnectionProcess.js index d2a0be517..1719a266d 100644 --- a/packages/api/src/proc/databaseConnectionProcess.js +++ b/packages/api/src/proc/databaseConnectionProcess.js @@ -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(); diff --git a/packages/api/src/proc/serverConnectionProcess.js b/packages/api/src/proc/serverConnectionProcess.js index f530b5877..2c7fed0e7 100644 --- a/packages/api/src/proc/serverConnectionProcess.js +++ b/packages/api/src/proc/serverConnectionProcess.js @@ -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(); diff --git a/packages/api/src/proc/sessionProcess.js b/packages/api/src/proc/sessionProcess.js index 62a6ab572..b3dcce4b6 100644 --- a/packages/api/src/proc/sessionProcess.js +++ b/packages/api/src/proc/sessionProcess.js @@ -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(); } diff --git a/packages/api/src/shell/executeQuery.js b/packages/api/src/shell/executeQuery.js index 72a85d545..e38fd54aa 100644 --- a/packages/api/src/shell/executeQuery.js +++ b/packages/api/src/shell/executeQuery.js @@ -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)) { diff --git a/packages/api/src/shell/queryReader.js b/packages/api/src/shell/queryReader.js index ce96f40f8..aee708cf4 100644 --- a/packages/api/src/shell/queryReader.js +++ b/packages/api/src/shell/queryReader.js @@ -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); } diff --git a/packages/api/src/shell/tableReader.js b/packages/api/src/shell/tableReader.js index 7fdca1414..a468185e0 100644 --- a/packages/api/src/shell/tableReader.js +++ b/packages/api/src/shell/tableReader.js @@ -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 }; diff --git a/packages/api/src/shell/tableWriter.js b/packages/api/src/shell/tableWriter.js index 63c393e8f..f4eff0e40 100644 --- a/packages/api/src/shell/tableWriter.js +++ b/packages/api/src/shell/tableWriter.js @@ -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); } diff --git a/packages/api/src/utility/connectUtility.js b/packages/api/src/utility/connectUtility.js new file mode 100644 index 000000000..33c4e7766 --- /dev/null +++ b/packages/api/src/utility/connectUtility.js @@ -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; diff --git a/packages/web/src/modals/AboutModal.js b/packages/web/src/modals/AboutModal.js index 6be1ed8ca..152e346be 100644 --- a/packages/web/src/modals/AboutModal.js +++ b/packages/web/src/modals/AboutModal.js @@ -70,7 +70,7 @@ export default function AboutModal({ modalState }) { dbgate.org - + github diff --git a/packages/web/src/modals/ConnectionModal.js b/packages/web/src/modals/ConnectionModal.js index 96c79db3a..f189e6330 100644 --- a/packages/web/src/modals/ConnectionModal.js +++ b/packages/web/src/modals/ConnectionModal.js @@ -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 ( + <> + + + + + + + ); +} + 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 }) {connection ? 'Edit connection' : 'Add connection'} - - - - {extensions.drivers.map(driver => ( - - ))} - {/* + + + + + + {extensions.drivers.map(driver => ( + + ))} + {/* */} - - - - {!isTesting && sqlConnectResult && sqlConnectResult.msgtype == 'connected' && ( -
- Connected: {sqlConnectResult.version} -
- )} - {!isTesting && sqlConnectResult && sqlConnectResult.msgtype == 'error' && ( -
- Connect failed: {sqlConnectResult.error} -
- )} - {isTesting && } +
+ + + {!isTesting && sqlConnectResult && sqlConnectResult.msgtype == 'connected' && ( +
+ Connected: {sqlConnectResult.version} +
+ )} + {!isTesting && sqlConnectResult && sqlConnectResult.msgtype == 'error' && ( +
+ Connect failed: {sqlConnectResult.error} +
+ )} + {isTesting && } + + + + +
diff --git a/packages/web/src/modals/ModalContent.js b/packages/web/src/modals/ModalContent.js index ae1a854eb..7411b429d 100644 --- a/packages/web/src/modals/ModalContent.js +++ b/packages/web/src/modals/ModalContent.js @@ -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 {children}; + return ( + + {children} + + ); } diff --git a/packages/web/src/widgets/TabControl.js b/packages/web/src/widgets/TabControl.js index 7097ea536..8b933ab1f 100644 --- a/packages/web/src/widgets/TabControl.js +++ b/packages/web/src/widgets/TabControl.js @@ -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 {childrenArray[index] && childrenArray[index].props.children} diff --git a/yarn.lock b/yarn.lock index 687c33408..552e42cd8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2128,7 +2128,7 @@ asn1.js@^4.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" -asn1@~0.2.3: +asn1@~0.2.0, asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== @@ -2488,7 +2488,7 @@ batch@0.6.1: resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= -bcrypt-pbkdf@^1.0.0: +bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= @@ -7759,6 +7759,13 @@ mkdirp@0.5.1, mkdirp@0.x, mkdirp@^0.5.1, mkdirp@~0.5.1: dependencies: minimist "0.0.8" +mkdirp@^0.5.5: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + mkdirp@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" @@ -8012,6 +8019,14 @@ node-releases@^1.1.47: dependencies: semver "^6.3.0" +node-ssh-forward@^0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/node-ssh-forward/-/node-ssh-forward-0.7.2.tgz#a5103ba9ae0e044156b0568a5084304e7d5b4a2a" + integrity sha512-dQGhwT9emJJ0PymZGXdwHue18oc+bnAeqSGUCi4+YufGLaTSfA4Ft04T97WGkVxmBUySy/O6lvKIfpJM0a8lgA== + dependencies: + debug "^4.1.1" + ssh2 "^0.8.9" + nodemon@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.2.tgz#9c7efeaaf9b8259295a97e5d4585ba8f0cbe50b0" @@ -8751,6 +8766,15 @@ portfinder@^1.0.25: debug "^3.1.1" mkdirp "^0.5.1" +portfinder@^1.0.28: + version "1.0.28" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" + integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA== + dependencies: + async "^2.6.2" + debug "^3.1.1" + mkdirp "^0.5.5" + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -11028,6 +11052,22 @@ sql-formatter@^2.3.3: dependencies: lodash "^4.16.0" +ssh2-streams@~0.4.10: + version "0.4.10" + resolved "https://registry.yarnpkg.com/ssh2-streams/-/ssh2-streams-0.4.10.tgz#48ef7e8a0e39d8f2921c30521d56dacb31d23a34" + integrity sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ== + dependencies: + asn1 "~0.2.0" + bcrypt-pbkdf "^1.0.2" + streamsearch "~0.1.2" + +ssh2@^0.8.9: + version "0.8.9" + resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-0.8.9.tgz#54da3a6c4ba3daf0d8477a538a481326091815f3" + integrity sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw== + dependencies: + ssh2-streams "~0.4.10" + sshpk@^1.7.0: version "1.16.1" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" @@ -11118,7 +11158,7 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== -streamsearch@0.1.2: +streamsearch@0.1.2, streamsearch@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=