dbgate-api 6.3.0 → 6.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dbgate-api",
3
3
  "main": "src/index.js",
4
- "version": "6.3.0",
4
+ "version": "6.3.3",
5
5
  "homepage": "https://dbgate.org/",
6
6
  "repository": {
7
7
  "type": "git",
@@ -29,10 +29,10 @@
29
29
  "compare-versions": "^3.6.0",
30
30
  "cors": "^2.8.5",
31
31
  "cross-env": "^6.0.3",
32
- "dbgate-datalib": "^6.3.0",
32
+ "dbgate-datalib": "^6.3.3",
33
33
  "dbgate-query-splitter": "^4.11.3",
34
- "dbgate-sqltree": "^6.3.0",
35
- "dbgate-tools": "^6.3.0",
34
+ "dbgate-sqltree": "^6.3.3",
35
+ "dbgate-tools": "^6.3.3",
36
36
  "debug": "^4.3.4",
37
37
  "diff": "^5.0.0",
38
38
  "diff2html": "^3.4.13",
@@ -83,7 +83,7 @@
83
83
  "devDependencies": {
84
84
  "@types/fs-extra": "^9.0.11",
85
85
  "@types/lodash": "^4.14.149",
86
- "dbgate-types": "^6.3.0",
86
+ "dbgate-types": "^6.3.3",
87
87
  "env-cmd": "^10.1.0",
88
88
  "jsdoc-to-markdown": "^9.0.5",
89
89
  "node-loader": "^1.0.2",
@@ -12,6 +12,7 @@ const {
12
12
  getAuthProviderById,
13
13
  } = require('../auth/authProvider');
14
14
  const storage = require('./storage');
15
+ const { decryptPasswordString } = require('../utility/crypting');
15
16
 
16
17
  const logger = getLogger('auth');
17
18
 
@@ -44,6 +45,7 @@ function authMiddleware(req, res, next) {
44
45
  '/connections/dblogin-auth',
45
46
  '/connections/dblogin-auth-token',
46
47
  '/health',
48
+ '/__health',
47
49
  ];
48
50
 
49
51
  // console.log('********************* getAuthProvider()', getAuthProvider());
@@ -95,7 +97,7 @@ module.exports = {
95
97
  let adminPassword = process.env.ADMIN_PASSWORD;
96
98
  if (!adminPassword) {
97
99
  const adminConfig = await storage.readConfig({ group: 'admin' });
98
- adminPassword = adminConfig?.adminPassword;
100
+ adminPassword = decryptPasswordString(adminConfig?.adminPassword);
99
101
  }
100
102
  if (adminPassword && adminPassword == password) {
101
103
  return {
@@ -102,12 +102,21 @@ function getPortalCollections() {
102
102
  trustServerCertificate: process.env[`SSL_TRUST_CERTIFICATE_${id}`],
103
103
  }));
104
104
 
105
+ for(const conn of connections) {
106
+ for(const prop in process.env) {
107
+ if (prop.startsWith(`CONNECTION_${conn._id}_`)) {
108
+ const name = prop.substring(`CONNECTION_${conn._id}_`.length);
109
+ conn[name] = process.env[prop];
110
+ }
111
+ }
112
+ }
113
+
105
114
  logger.info({ connections: connections.map(pickSafeConnectionInfo) }, 'Using connections from ENV variables');
106
115
  const noengine = connections.filter(x => !x.engine);
107
116
  if (noengine.length > 0) {
108
117
  logger.warn(
109
118
  { connections: noengine.map(x => x._id) },
110
- 'Invalid CONNECTIONS configutation, missing ENGINE for connection ID'
119
+ 'Invalid CONNECTIONS configuration, missing ENGINE for connection ID'
111
120
  );
112
121
  }
113
122
  return connections;
@@ -37,6 +37,8 @@ const loadModelTransform = require('../utility/loadModelTransform');
37
37
  const exportDbModelSql = require('../utility/exportDbModelSql');
38
38
  const axios = require('axios');
39
39
  const { callTextToSqlApi, callCompleteOnCursorApi, callRefactorSqlQueryApi } = require('../utility/authProxy');
40
+ const { decryptConnection } = require('../utility/crypting');
41
+ const { getSshTunnel } = require('../utility/sshTunnel');
40
42
 
41
43
  const logger = getLogger('databaseConnections');
42
44
 
@@ -140,6 +142,11 @@ module.exports = {
140
142
  if (newOpened.disconnected) return;
141
143
  this.close(conid, database, false);
142
144
  });
145
+ subprocess.on('error', err => {
146
+ logger.error(extractErrorLogData(err), 'Error in database connection subprocess');
147
+ if (newOpened.disconnected) return;
148
+ this.close(conid, database, false);
149
+ });
143
150
 
144
151
  subprocess.send({
145
152
  msgtype: 'connect',
@@ -619,9 +626,26 @@ module.exports = {
619
626
  command,
620
627
  { conid, database, outputFile, inputFile, options, selectedTables, skippedTables, argsFormat }
621
628
  ) {
622
- const connection = await connections.getCore({ conid });
629
+ const sourceConnection = await connections.getCore({ conid });
630
+ const connection = {
631
+ ...decryptConnection(sourceConnection),
632
+ };
623
633
  const driver = requireEngineDriver(connection);
624
634
 
635
+ if (!connection.port && driver.defaultPort) {
636
+ connection.port = driver.defaultPort.toString();
637
+ }
638
+
639
+ if (connection.useSshTunnel) {
640
+ const tunnel = await getSshTunnel(connection);
641
+ if (tunnel.state == 'error') {
642
+ throw new Error(tunnel.message);
643
+ }
644
+
645
+ connection.server = tunnel.localHost;
646
+ connection.port = tunnel.localPort;
647
+ }
648
+
625
649
  const settingsValue = await config.getSettings();
626
650
 
627
651
  const externalTools = {};
@@ -195,8 +195,8 @@ module.exports = {
195
195
  },
196
196
 
197
197
  exportDiagram_meta: true,
198
- async exportDiagram({ filePath, html, css, themeType, themeClassName }) {
199
- await fs.writeFile(filePath, getDiagramExport(html, css, themeType, themeClassName));
198
+ async exportDiagram({ filePath, html, css, themeType, themeClassName, watermark }) {
199
+ await fs.writeFile(filePath, getDiagramExport(html, css, themeType, themeClassName, watermark));
200
200
  return true;
201
201
  },
202
202
 
@@ -98,6 +98,11 @@ module.exports = {
98
98
  if (newOpened.disconnected) return;
99
99
  this.close(conid, false);
100
100
  });
101
+ subprocess.on('error', err => {
102
+ logger.error(extractErrorLogData(err), 'Error in server connection subprocess');
103
+ if (newOpened.disconnected) return;
104
+ this.close(conid, false);
105
+ });
101
106
  subprocess.send({ msgtype: 'connect', ...connection, globalSettings: await config.getSettings() });
102
107
  return newOpened;
103
108
  });
@@ -1,5 +1,5 @@
1
1
 
2
2
  module.exports = {
3
- version: '6.3.0',
4
- buildTime: '2025-03-18T11:15:00.683Z'
3
+ version: '6.3.3',
4
+ buildTime: '2025-04-09T10:29:36.096Z'
5
5
  };
package/src/main.js CHANGED
@@ -38,7 +38,7 @@ const { getLogger } = require('dbgate-tools');
38
38
  const { getDefaultAuthProvider } = require('./auth/authProvider');
39
39
  const startCloudUpgradeTimer = require('./utility/cloudUpgrade');
40
40
  const { isProApp } = require('./utility/checkLicense');
41
- const getHealthStatus = require('./utility/healthStatus');
41
+ const { getHealthStatus, getHealthStatusSprinx } = require('./utility/healthStatus');
42
42
 
43
43
  const logger = getLogger('main');
44
44
 
@@ -124,6 +124,12 @@ function start() {
124
124
  res.end(JSON.stringify(health, null, 2));
125
125
  });
126
126
 
127
+ app.get(getExpressPath('/__health'), async function (req, res) {
128
+ res.setHeader('Content-Type', 'application/json');
129
+ const health = await getHealthStatusSprinx();
130
+ res.end(JSON.stringify(health, null, 2));
131
+ });
132
+
127
133
  app.use(bodyParser.json({ limit: '50mb' }));
128
134
 
129
135
  app.use(
@@ -38,6 +38,8 @@ function start() {
38
38
  detail: formatErrorDetail(e, connection),
39
39
  });
40
40
  }
41
+
42
+ process.exit(0);
41
43
  });
42
44
  }
43
45
 
@@ -60,6 +60,10 @@ class DatastoreProxy {
60
60
  // if (this.disconnected) return;
61
61
  this.subprocess = null;
62
62
  });
63
+ this.subprocess.on('error', err => {
64
+ logger.error(extractErrorLogData(err), 'Error in data store subprocess');
65
+ this.subprocess = null;
66
+ });
63
67
  this.subprocess.send({ msgtype: 'open', file: this.file });
64
68
  }
65
69
  return this.subprocess;
@@ -96,7 +96,9 @@ async function connectUtility(driver, storedConnection, connectionMode, addition
96
96
  ...decryptConnection(connectionLoaded),
97
97
  };
98
98
 
99
- if (!connection.port && driver.defaultPort) connection.port = driver.defaultPort.toString();
99
+ if (!connection.port && driver.defaultPort) {
100
+ connection.port = driver.defaultPort.toString();
101
+ }
100
102
 
101
103
  if (connection.useSshTunnel) {
102
104
  const tunnel = await getSshTunnelProxy(connection);
@@ -5,12 +5,16 @@ const path = require('path');
5
5
  const _ = require('lodash');
6
6
 
7
7
  const { datadir } = require('./directories');
8
+ const { encryptionKeyArg } = require('./processArgs');
8
9
 
9
10
  const defaultEncryptionKey = 'mQAUaXhavRGJDxDTXSCg7Ej0xMmGCrx6OKA07DIMBiDcYYkvkaXjTAzPUEHEHEf9';
10
11
 
11
12
  let _encryptionKey = null;
12
13
 
13
14
  function loadEncryptionKey() {
15
+ if (encryptionKeyArg) {
16
+ return encryptionKeyArg;
17
+ }
14
18
  if (_encryptionKey) {
15
19
  return _encryptionKey;
16
20
  }
@@ -33,6 +37,26 @@ function loadEncryptionKey() {
33
37
  return _encryptionKey;
34
38
  }
35
39
 
40
+ async function loadEncryptionKeyFromExternal(storedValue, setStoredValue) {
41
+ const encryptor = simpleEncryptor.createEncryptor(defaultEncryptionKey);
42
+
43
+ if (!storedValue) {
44
+ const generatedKey = crypto.randomBytes(32);
45
+ const newKey = generatedKey.toString('hex');
46
+ const result = {
47
+ encryptionKey: newKey,
48
+ };
49
+ await setStoredValue(encryptor.encrypt(result));
50
+
51
+ setEncryptionKey(newKey);
52
+
53
+ return;
54
+ }
55
+
56
+ const data = encryptor.decrypt(storedValue);
57
+ setEncryptionKey(data['encryptionKey']);
58
+ }
59
+
36
60
  let _encryptor = null;
37
61
 
38
62
  function getEncryptor() {
@@ -43,35 +67,46 @@ function getEncryptor() {
43
67
  return _encryptor;
44
68
  }
45
69
 
46
- function encryptPasswordField(connection, field) {
47
- if (
48
- connection &&
49
- connection[field] &&
50
- !connection[field].startsWith('crypt:') &&
51
- connection.passwordMode != 'saveRaw'
52
- ) {
70
+ function encryptPasswordString(password) {
71
+ if (password && !password.startsWith('crypt:')) {
72
+ return 'crypt:' + getEncryptor().encrypt(password);
73
+ }
74
+ return password;
75
+ }
76
+
77
+ function decryptPasswordString(password) {
78
+ if (password && password.startsWith('crypt:')) {
79
+ return getEncryptor().decrypt(password.substring('crypt:'.length));
80
+ }
81
+ return password;
82
+ }
83
+
84
+ function encryptObjectPasswordField(obj, field) {
85
+ if (obj && obj[field] && !obj[field].startsWith('crypt:')) {
53
86
  return {
54
- ...connection,
55
- [field]: 'crypt:' + getEncryptor().encrypt(connection[field]),
87
+ ...obj,
88
+ [field]: 'crypt:' + getEncryptor().encrypt(obj[field]),
56
89
  };
57
90
  }
58
- return connection;
91
+ return obj;
59
92
  }
60
93
 
61
- function decryptPasswordField(connection, field) {
62
- if (connection && connection[field] && connection[field].startsWith('crypt:')) {
94
+ function decryptObjectPasswordField(obj, field) {
95
+ if (obj && obj[field] && obj[field].startsWith('crypt:')) {
63
96
  return {
64
- ...connection,
65
- [field]: getEncryptor().decrypt(connection[field].substring('crypt:'.length)),
97
+ ...obj,
98
+ [field]: getEncryptor().decrypt(obj[field].substring('crypt:'.length)),
66
99
  };
67
100
  }
68
- return connection;
101
+ return obj;
69
102
  }
70
103
 
71
104
  function encryptConnection(connection) {
72
- connection = encryptPasswordField(connection, 'password');
73
- connection = encryptPasswordField(connection, 'sshPassword');
74
- connection = encryptPasswordField(connection, 'sshKeyfilePassword');
105
+ if (connection.passwordMode != 'saveRaw') {
106
+ connection = encryptObjectPasswordField(connection, 'password');
107
+ connection = encryptObjectPasswordField(connection, 'sshPassword');
108
+ connection = encryptObjectPasswordField(connection, 'sshKeyfilePassword');
109
+ }
75
110
  return connection;
76
111
  }
77
112
 
@@ -81,12 +116,24 @@ function maskConnection(connection) {
81
116
  }
82
117
 
83
118
  function decryptConnection(connection) {
84
- connection = decryptPasswordField(connection, 'password');
85
- connection = decryptPasswordField(connection, 'sshPassword');
86
- connection = decryptPasswordField(connection, 'sshKeyfilePassword');
119
+ connection = decryptObjectPasswordField(connection, 'password');
120
+ connection = decryptObjectPasswordField(connection, 'sshPassword');
121
+ connection = decryptObjectPasswordField(connection, 'sshKeyfilePassword');
87
122
  return connection;
88
123
  }
89
124
 
125
+ function encryptUser(user) {
126
+ if (user.encryptPassword) {
127
+ user = encryptObjectPasswordField(user, 'password');
128
+ }
129
+ return user;
130
+ }
131
+
132
+ function decryptUser(user) {
133
+ user = decryptObjectPasswordField(user, 'password');
134
+ return user;
135
+ }
136
+
90
137
  function pickSafeConnectionInfo(connection) {
91
138
  if (process.env.LOG_CONNECTION_SENSITIVE_VALUES) {
92
139
  return connection;
@@ -99,10 +146,27 @@ function pickSafeConnectionInfo(connection) {
99
146
  });
100
147
  }
101
148
 
149
+ function setEncryptionKey(encryptionKey) {
150
+ _encryptionKey = encryptionKey;
151
+ _encryptor = null;
152
+ global.ENCRYPTION_KEY = encryptionKey;
153
+ }
154
+
155
+ function getEncryptionKey() {
156
+ return _encryptionKey;
157
+ }
158
+
102
159
  module.exports = {
103
160
  loadEncryptionKey,
104
161
  encryptConnection,
162
+ encryptUser,
163
+ decryptUser,
105
164
  decryptConnection,
106
165
  maskConnection,
107
166
  pickSafeConnectionInfo,
167
+ loadEncryptionKeyFromExternal,
168
+ getEncryptionKey,
169
+ setEncryptionKey,
170
+ encryptPasswordString,
171
+ decryptPasswordString,
108
172
  };
@@ -1,4 +1,11 @@
1
- const getDiagramExport = (html, css, themeType, themeClassName) => {
1
+ const getDiagramExport = (html, css, themeType, themeClassName, watermark) => {
2
+ const watermarkHtml = watermark
3
+ ? `
4
+ <div style="position: fixed; bottom: 0; right: 0; padding: 5px; font-size: 12px; color: var(--theme-font-2); background-color: var(--theme-bg-2); border-top-left-radius: 5px; border: 1px solid var(--theme-border);">
5
+ ${watermark}
6
+ </div>
7
+ `
8
+ : '';
2
9
  return `<html>
3
10
  <meta charset='utf-8'>
4
11
 
@@ -13,10 +20,44 @@ const getDiagramExport = (html, css, themeType, themeClassName) => {
13
20
  </style>
14
21
 
15
22
  <link rel="stylesheet" href='https://cdn.jsdelivr.net/npm/@mdi/font@6.5.95/css/materialdesignicons.css' />
23
+
24
+ <script>
25
+ let lastX = null;
26
+ let lastY = null;
27
+
28
+ const handleMoveDown = e => {
29
+ lastX = e.clientX;
30
+ lastY = e.clientY;
31
+ document.addEventListener('mousemove', handleMoveMove, true);
32
+ document.addEventListener('mouseup', handleMoveEnd, true);
33
+ };
34
+
35
+ const handleMoveMove = e => {
36
+ e.preventDefault();
37
+
38
+ document.body.scrollLeft -= e.clientX - lastX;
39
+ document.body.scrollTop -= e.clientY - lastY;
40
+
41
+ lastX = e.clientX;
42
+ lastY = e.clientY;
43
+ };
44
+ const handleMoveEnd = e => {
45
+ e.preventDefault();
46
+ e.stopPropagation();
47
+
48
+ lastX = null;
49
+ lastY = null;
50
+ document.removeEventListener('mousemove', handleMoveMove, true);
51
+ document.removeEventListener('mouseup', handleMoveEnd, true);
52
+ };
53
+
54
+ document.addEventListener('mousedown', handleMoveDown);
55
+ </script>
16
56
  </head>
17
57
 
18
- <body class='${themeType == 'dark' ? 'theme-type-dark' : 'theme-type-light'} ${themeClassName}'>
58
+ <body class='${themeType == 'dark' ? 'theme-type-dark' : 'theme-type-light'} ${themeClassName}' style='user-select:none; cursor:pointer'>
19
59
  ${html}
60
+ ${watermarkHtml}
20
61
  </body>
21
62
 
22
63
  </html>`;
@@ -22,6 +22,8 @@ const getMapExport = (geoJson) => {
22
22
  })
23
23
  .addTo(map);
24
24
 
25
+ leaflet.control.scale().addTo(map);
26
+
25
27
  const geoJsonObj = leaflet
26
28
  .geoJSON(${JSON.stringify(geoJson)}, {
27
29
  style: function () {
@@ -24,4 +24,15 @@ async function getHealthStatus() {
24
24
  };
25
25
  }
26
26
 
27
- module.exports = getHealthStatus;
27
+ async function getHealthStatusSprinx() {
28
+ return {
29
+ overallStatus: 'OK',
30
+ timeStamp: new Date().toISOString(),
31
+ timeStampUnix: Math.floor(Date.now() / 1000),
32
+ };
33
+ }
34
+
35
+ module.exports = {
36
+ getHealthStatus,
37
+ getHealthStatusSprinx,
38
+ };
@@ -17,6 +17,7 @@ const processDisplayName = getNamedArg('--process-display-name');
17
17
  const listenApi = process.argv.includes('--listen-api');
18
18
  const listenApiChild = process.argv.includes('--listen-api-child') || listenApi;
19
19
  const runE2eTests = process.argv.includes('--run-e2e-tests');
20
+ const encryptionKeyArg = getNamedArg('--encryption-key');
20
21
 
21
22
  function getPassArgs() {
22
23
  const res = [];
@@ -31,6 +32,9 @@ function getPassArgs() {
31
32
  if (runE2eTests) {
32
33
  res.push('--run-e2e-tests');
33
34
  }
35
+ if (global['ENCRYPTION_KEY']) {
36
+ res.push('--encryption-key', global['ENCRYPTION_KEY']);
37
+ }
34
38
  return res;
35
39
  }
36
40
 
@@ -45,4 +49,5 @@ module.exports = {
45
49
  listenApiChild,
46
50
  processDisplayName,
47
51
  runE2eTests,
52
+ encryptionKeyArg,
48
53
  };
@@ -57,10 +57,21 @@ function callForwardProcess(connection, tunnelConfig, tunnelCacheKey) {
57
57
  }
58
58
  });
59
59
  subprocess.on('exit', code => {
60
- logger.info('SSH forward process exited');
60
+ logger.info(`SSH forward process exited with code ${code}`);
61
61
  delete sshTunnelCache[tunnelCacheKey];
62
62
  if (!promiseHandled) {
63
- reject(new Error('SSH forward process exited, try to change "Local host address for SSH connections" in Settings/Connections'));
63
+ reject(
64
+ new Error(
65
+ 'SSH forward process exited, try to change "Local host address for SSH connections" in Settings/Connections'
66
+ )
67
+ );
68
+ }
69
+ });
70
+ subprocess.on('error', error => {
71
+ logger.error(extractErrorLogData(error), 'SSH forward process error');
72
+ delete sshTunnelCache[tunnelCacheKey];
73
+ if (!promiseHandled) {
74
+ reject(error);
64
75
  }
65
76
  });
66
77
  });