1
1
mirror of https://github.com/Fabio286/antares.git synced 2025-06-05 21:59:22 +02:00

feat: SSH Tunnel functionality (#81)

* added ssh-tunnel-functionality for mysql-connections

* remove autoformat-stuff

* added identity for using ssh-key

* added identity to mysqlclient to use sshkey

* removed debug console.log

* added ssh-tunnel-functionality for postgresqlclient

* changed naming to sshKey for sshKey-input

* refactoring code

* fix lint

* set dbConfig.ssl to null initially
This commit is contained in:
Christian Ratz
2021-07-05 09:30:52 +02:00
committed by GitHub
parent 0db5ebd7bf
commit 1801bef019
13 changed files with 308 additions and 23 deletions

View File

@ -102,6 +102,7 @@
"source-map-support": "^0.5.16", "source-map-support": "^0.5.16",
"spectre.css": "^0.5.9", "spectre.css": "^0.5.9",
"sql-formatter": "^4.0.2", "sql-formatter": "^4.0.2",
"ssh2-promise": "^0.1.7",
"v-mask": "^2.2.4", "v-mask": "^2.2.4",
"vue-i18n": "^8.24.4", "vue-i18n": "^8.24.4",
"vuedraggable": "^2.24.3", "vuedraggable": "^2.24.3",

View File

@ -24,13 +24,23 @@ export default connections => {
}; };
} }
const connection = ClientsFactory.getConnection({ if (conn.ssh) {
params.ssh = {
host: conn.sshHost,
username: conn.sshUser,
password: conn.sshPass,
port: conn.sshPort ? conn.sshPort : 22,
identity: conn.sshKey
};
}
try {
const connection = await ClientsFactory.getConnection({
client: conn.client, client: conn.client,
params params
}); });
try {
await connection.connect(); await connection.connect();
await connection.select('1+1').run(); await connection.select('1+1').run();
connection.destroy(); connection.destroy();
@ -66,6 +76,16 @@ export default connections => {
}; };
} }
if (conn.ssh) {
params.ssh = {
host: conn.sshHost,
username: conn.sshUser,
password: conn.sshPass,
port: conn.sshPort ? conn.sshPort : 22,
identity: conn.sshKey
};
}
try { try {
const connection = ClientsFactory.getConnection({ const connection = ClientsFactory.getConnection({
client: conn.client, client: conn.client,

View File

@ -12,6 +12,10 @@ export class ClientsFactory {
* @param {String} args.params.host * @param {String} args.params.host
* @param {Number} args.params.port * @param {Number} args.params.port
* @param {String} args.params.password * @param {String} args.params.password
* @param {String} args.params.ssh.host
* @param {String} args.params.ssh.username
* @param {String} args.params.ssh.password
* @param {Number} args.params.ssh.port
* @param {Number=} args.poolSize * @param {Number=} args.poolSize
* @returns Database Connection * @returns Database Connection
* @memberof ClientsFactory * @memberof ClientsFactory

View File

@ -2,6 +2,7 @@
import mysql from 'mysql2/promise'; import mysql from 'mysql2/promise';
import { AntaresCore } from '../AntaresCore'; import { AntaresCore } from '../AntaresCore';
import dataTypes from 'common/data-types/mysql'; import dataTypes from 'common/data-types/mysql';
import * as SSH2Promise from 'ssh2-promise';
export class MySQLClient extends AntaresCore { export class MySQLClient extends AntaresCore {
constructor (args) { constructor (args) {
@ -104,11 +105,32 @@ export class MySQLClient extends AntaresCore {
async connect () { async connect () {
delete this._params.application_name; delete this._params.application_name;
if (!this._poolSize) const dbConfig = {
this._connection = await mysql.createConnection(this._params); host: this._params.host,
port: this._params.port,
user: this._params.user,
password: this._params.password,
ssl: null
};
if (this._params.database?.length) dbConfig.database = this._params.database;
if (this._params.ssl) dbConfig.ssl = { ...this._params.ssl };
if (this._params.ssh) {
this._ssh = new SSH2Promise({ ...this._params.ssh });
this._tunnel = await this._ssh.addTunnel({
remoteAddr: this._params.host,
remotePort: this._params.port
});
dbConfig.port = this._tunnel.localPort;
}
if (!this._poolSize) this._connection = await mysql.createConnection(dbConfig);
else { else {
this._connection = mysql.createPool({ this._connection = mysql.createPool({
...this._params, ...dbConfig,
connectionLimit: this._poolSize, connectionLimit: this._poolSize,
typeCast: (field, next) => { typeCast: (field, next) => {
if (field.type === 'DATETIME') if (field.type === 'DATETIME')
@ -125,6 +147,7 @@ export class MySQLClient extends AntaresCore {
*/ */
destroy () { destroy () {
this._connection.end(); this._connection.end();
if (this._ssh) this._ssh.close();
} }
/** /**

View File

@ -3,6 +3,7 @@ import { Pool, Client, types } from 'pg';
import { parse } from 'pgsql-ast-parser'; import { parse } from 'pgsql-ast-parser';
import { AntaresCore } from '../AntaresCore'; import { AntaresCore } from '../AntaresCore';
import dataTypes from 'common/data-types/postgresql'; import dataTypes from 'common/data-types/postgresql';
import * as SSH2Promise from 'ssh2-promise';
function pgToString (value) { function pgToString (value) {
return value.toString(); return value.toString();
@ -51,13 +52,35 @@ export class PostgreSQLClient extends AntaresCore {
* @memberof PostgreSQLClient * @memberof PostgreSQLClient
*/ */
async connect () { async connect () {
const dbConfig = {
host: this._params.host,
port: this._params.port,
user: this._params.user,
password: this._params.password,
ssl: null
};
if (this._params.database?.length) dbConfig.database = this._params.database;
if (this._params.ssl) dbConfig.ssl = { ...this._params.ssl };
if (this._params.ssh) {
this._ssh = new SSH2Promise({ ...this._params.ssh });
this._tunnel = await this._ssh.addTunnel({
remoteAddr: this._params.host,
remotePort: this._params.port
});
dbConfig.port = this._tunnel.localPort;
}
if (!this._poolSize) { if (!this._poolSize) {
const client = new Client(this._params); const client = new Client(dbConfig);
await client.connect(); await client.connect();
this._connection = client; this._connection = client;
} }
else { else {
const pool = new Pool({ ...this._params, max: this._poolSize }); const pool = new Pool({ ...dbConfig, max: this._poolSize });
this._connection = pool; this._connection = pool;
} }
} }
@ -67,6 +90,7 @@ export class PostgreSQLClient extends AntaresCore {
*/ */
destroy () { destroy () {
this._connection.end(); this._connection.end();
if (this._ssh) this._ssh.close();
} }
/** /**

View File

@ -28,6 +28,13 @@
> >
<a class="tab-link">{{ $t('word.ssl') }}</a> <a class="tab-link">{{ $t('word.ssl') }}</a>
</li> </li>
<li
class="tab-item"
:class="{'active': selectedTab === 'ssh'}"
@click="selectTab('ssh')"
>
<a class="c-hand">{{ $t('word.sshTunnel') }}</a>
</li>
</ul> </ul>
</div> </div>
<div v-if="selectedTab === 'general'" class="panel-body py-0"> <div v-if="selectedTab === 'general'" class="panel-body py-0">
@ -208,7 +215,6 @@
/> />
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-4 col-sm-12"> <div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.ciphers') }}</label> <label class="form-label">{{ $t('word.ciphers') }}</label>
@ -231,6 +237,95 @@
:status="toast.status" :status="toast.status"
/> />
</div> </div>
<div v-if="selectedTab === 'ssh'" class="panel-body py-0">
<div class="container">
<form class="form-horizontal">
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">
{{ $t('message.enableSsh') }}
</label>
</div>
<div class="col-8 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsh">
<input type="checkbox" :checked="localConnection.ssh">
<i class="form-icon" />
</label>
</div>
</div>
<fieldset class="m-0" :disabled="isTesting || !localConnection.ssh">
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="localConnection.sshHost"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="localConnection.sshUser"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="localConnection.sshPass"
class="form-input"
type="password"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="localConnection.sshPort"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.privateKey') }}</label>
</div>
<div class="col-8 col-sm-12">
<BaseUploadInput
:value="localConnection.sshKey"
:message="$t('word.browse')"
@clear="pathClear('sshKey')"
@change="pathSelection($event, 'sshKey')"
/>
</div>
</div>
</fieldset>
</form>
</div>
<BaseToast
class="mb-2"
:message="toast.message"
:status="toast.status"
/>
</div>
<div class="modal-footer text-light"> <div class="modal-footer text-light">
<button <button
class="btn btn-gray mr-2" class="btn btn-gray mr-2"
@ -369,6 +464,9 @@ export default {
toggleSsl () { toggleSsl () {
this.localConnection.ssl = !this.localConnection.ssl; this.localConnection.ssl = !this.localConnection.ssl;
}, },
toggleSsh () {
this.localConnection.ssh = !this.localConnection.ssh;
},
pathSelection (event, name) { pathSelection (event, name) {
const { files } = event.target; const { files } = event.target;
if (!files.length) return; if (!files.length) return;

View File

@ -29,6 +29,13 @@
> >
<a class="tab-link">{{ $t('word.ssl') }}</a> <a class="tab-link">{{ $t('word.ssl') }}</a>
</li> </li>
<li
class="tab-item"
:class="{'active': selectedTab === 'ssh'}"
@click="selectTab('ssh')"
>
<a class="c-hand">{{ $t('word.sshTunnel') }}</a>
</li>
</ul> </ul>
</div> </div>
<div v-if="selectedTab === 'general'" class="panel-body py-0"> <div v-if="selectedTab === 'general'" class="panel-body py-0">
@ -213,7 +220,6 @@
/> />
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-4 col-sm-12"> <div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.ciphers') }}</label> <label class="form-label">{{ $t('word.ciphers') }}</label>
@ -236,6 +242,95 @@
:status="toast.status" :status="toast.status"
/> />
</div> </div>
<div v-if="selectedTab === 'ssh'" class="panel-body py-0">
<div class="container">
<form class="form-horizontal">
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">
{{ $t('message.enableSsh') }}
</label>
</div>
<div class="col-8 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsh">
<input type="checkbox" :checked="connection.ssh">
<i class="form-icon" />
</label>
</div>
</div>
<fieldset class="m-0" :disabled="isTesting || !connection.ssh">
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.hostName') }}/IP</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.sshHost"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.sshUser"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.password') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.sshPass"
class="form-input"
type="password"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.port') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.sshPort"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.privateKey') }}</label>
</div>
<div class="col-8 col-sm-12">
<BaseUploadInput
:value="connection.sshKey"
:message="$t('word.browse')"
@clear="pathClear('sshKey')"
@change="pathSelection($event, 'sshKey')"
/>
</div>
</div>
</fieldset>
</form>
</div>
<BaseToast
class="mb-2"
:message="toast.message"
:status="toast.status"
/>
</div>
</div> </div>
</div> </div>
<div class="modal-footer text-light"> <div class="modal-footer text-light">
@ -294,8 +389,13 @@ export default {
cert: '', cert: '',
key: '', key: '',
ca: '', ca: '',
ciphers: '' ciphers: '',
ssh: false,
sshHost: '',
sshUser: '',
sshPass: '',
sshKey: '',
sshPort: 22
}, },
toast: { toast: {
status: '', status: '',
@ -393,6 +493,9 @@ export default {
toggleSsl () { toggleSsl () {
this.connection.ssl = !this.connection.ssl; this.connection.ssl = !this.connection.ssl;
}, },
toggleSsh () {
this.connection.ssh = !this.connection.ssh;
},
pathSelection (event, name) { pathSelection (event, name) {
const { files } = event.target; const { files } = event.target;
if (!files.length) return; if (!files.length) return;

View File

@ -104,7 +104,8 @@ module.exports = {
database: 'Datenbank', database: 'Datenbank',
scratchpad: 'Scratchpad', scratchpad: 'Scratchpad',
array: 'Array', array: 'Array',
format: 'Formatierung' format: 'Formatierung',
sshTunnel: 'SSH Tunnel'
}, },
message: { message: {
appWelcome: 'Willkommen im Antares SQL Client!', appWelcome: 'Willkommen im Antares SQL Client!',
@ -210,7 +211,8 @@ module.exports = {
deleteSchema: 'Schema löschen', deleteSchema: 'Schema löschen',
markdownSupported: 'Unterstützt Markdown', markdownSupported: 'Unterstützt Markdown',
plantATree: 'Pflanze einen Baum', plantATree: 'Pflanze einen Baum',
dataTabPageSize: 'Einträge pro Tab / Seite' dataTabPageSize: 'Einträge pro Tab / Seite',
enableSsh: 'Aktiviere SSH'
}, },
faker: { faker: {
address: 'Adresse', address: 'Adresse',

View File

@ -106,6 +106,7 @@ module.exports = {
array: 'Array', array: 'Array',
changelog: 'Changelog', changelog: 'Changelog',
format: 'Format', format: 'Format',
sshTunnel: 'SSH tunnel',
structure: 'Structure', structure: 'Structure',
small: 'Small', small: 'Small',
medium: 'Medium', medium: 'Medium',
@ -219,6 +220,7 @@ module.exports = {
markdownSupported: 'Markdown supported', markdownSupported: 'Markdown supported',
plantATree: 'Plant a Tree', plantATree: 'Plant a Tree',
dataTabPageSize: 'DATA tab page size', dataTabPageSize: 'DATA tab page size',
enableSsh: 'Enable SSH',
pageNumber: 'Page number', pageNumber: 'Page number',
duplicateTable: 'Duplicate table' duplicateTable: 'Duplicate table'
}, },

View File

@ -93,7 +93,8 @@ module.exports = {
ciphers: 'Chiffrement', ciphers: 'Chiffrement',
upload: 'Charger', upload: 'Charger',
browse: 'Parcourir', browse: 'Parcourir',
faker: 'Faker' faker: 'Faker',
sshTunnel: 'SSH tunnel'
}, },
message: { message: {
appWelcome: 'Bienvenu sur le client SQL Antares!', appWelcome: 'Bienvenu sur le client SQL Antares!',
@ -183,7 +184,8 @@ module.exports = {
preserveOnCompletion: 'Préserver à l\'achèvement', preserveOnCompletion: 'Préserver à l\'achèvement',
enableSsl: 'Activer le SSL', enableSsl: 'Activer le SSL',
manualValue: 'Valeur manuelle', manualValue: 'Valeur manuelle',
tableFiller: 'Remplisseur de table' tableFiller: 'Remplisseur de table',
enableSsh: 'Activer le SSH'
}, },
faker: { faker: {
address: 'Adresse', address: 'Adresse',

View File

@ -105,7 +105,8 @@ module.exports = {
scratchpad: 'Blocco appunti', scratchpad: 'Blocco appunti',
array: 'Array', array: 'Array',
changelog: 'Changelog', changelog: 'Changelog',
format: 'Formatta' format: 'Formatta',
sshTunnel: 'SSH tunnel'
}, },
message: { message: {
appWelcome: 'Benvenuto in Antares SQL Client!', appWelcome: 'Benvenuto in Antares SQL Client!',
@ -210,7 +211,8 @@ module.exports = {
editSchema: 'Modifica schema', editSchema: 'Modifica schema',
deleteSchema: 'Elimina schema', deleteSchema: 'Elimina schema',
markdownSupported: 'Markdown supportato', markdownSupported: 'Markdown supportato',
plantATree: 'Pianta un albero' plantATree: 'Pianta un albero',
enableSsh: 'Abilita SSH'
}, },
faker: { faker: {
address: 'Indirizzo', address: 'Indirizzo',

View File

@ -105,7 +105,8 @@ module.exports = {
scratchpad: 'Rascunho', scratchpad: 'Rascunho',
array: 'Array', array: 'Array',
changelog: 'Logs de alteração', changelog: 'Logs de alteração',
format: 'Formato' format: 'Formato',
sshTunnel: 'SSH túnel'
}, },
message: { message: {
appWelcome: 'Bem vindo ao Antares SQL Client!', appWelcome: 'Bem vindo ao Antares SQL Client!',
@ -210,7 +211,8 @@ module.exports = {
editSchema: 'Editar schema', editSchema: 'Editar schema',
deleteSchema: 'Apagar schema', deleteSchema: 'Apagar schema',
markdownSupported: 'Markdown suportado', markdownSupported: 'Markdown suportado',
plantATree: 'Plante uma árvore' plantATree: 'Plante uma árvore',
enableSsh: 'Habilitar SSH'
}, },
faker: { faker: {
address: 'Endereço', address: 'Endereço',

View File

@ -215,8 +215,10 @@
} }
.bg-checkered { .bg-checkered {
background-image: linear-gradient(to right, rgba(192, 192, 192, 0.75), rgba(192, 192, 192, 0.75)), background-image:
linear-gradient(to right, black 50%, white 50%), linear-gradient(to bottom, black 50%, white 50%); linear-gradient(to right, rgba(192, 192, 192, 0.75), rgba(192, 192, 192, 0.75)),
linear-gradient(to right, black 50%, white 50%),
linear-gradient(to bottom, black 50%, white 50%);
background-blend-mode: normal, difference, normal; background-blend-mode: normal, difference, normal;
background-size: 2em 2em; background-size: 2em 2em;
} }