dbgate-api
Version:
Allows run DbGate data-manipulation scripts.
359 lines (327 loc) • 11 kB
JavaScript
const crypto = require('crypto');
const connections = require('./connections');
const socket = require('../utility/socket');
const { fork } = require('child_process');
const _ = require('lodash');
const AsyncLock = require('async-lock');
const { handleProcessCommunication } = require('../utility/processComm');
const lock = new AsyncLock();
const config = require('./config');
const processArgs = require('../utility/processArgs');
const {
testConnectionPermission,
loadPermissionsFromRequest,
hasPermission,
loadDatabasePermissionsFromRequest,
getDatabasePermissionRole,
} = require('../utility/hasPermission');
const { MissingCredentialsError } = require('../utility/exceptions');
const pipeForkLogs = require('../utility/pipeForkLogs');
const { getLogger, extractErrorLogData } = require('dbgate-tools');
const { sendToAuditLog } = require('../utility/auditlog');
const logger = getLogger('serverConnection');
module.exports = {
opened: [],
closed: {},
lastPinged: {},
requests: {},
handle_databases(conid, { databases }) {
const existing = this.opened.find(x => x.conid == conid);
if (!existing) return;
existing.databases = databases;
socket.emitChanged(`database-list-changed`, { conid });
},
handle_version(conid, { version }) {
const existing = this.opened.find(x => x.conid == conid);
if (!existing) return;
existing.version = version;
socket.emitChanged(`server-version-changed`, { conid });
},
handle_status(conid, { status }) {
const existing = this.opened.find(x => x.conid == conid);
if (!existing) return;
existing.status = status;
socket.emitChanged(`server-status-changed`);
},
handle_ping() { },
handle_response(conid, { msgid, ...response }) {
const [resolve, reject] = this.requests[msgid];
resolve(response);
delete this.requests[msgid];
},
async ensureOpened(conid) {
const res = await lock.acquire(conid, async () => {
const existing = this.opened.find(x => x.conid == conid);
if (existing) return existing;
const connection = await connections.getCore({ conid });
if (!connection) {
throw new Error(`serverConnections: Connection with conid="${conid}" not found`);
}
if (connection.singleDatabase) {
return null;
}
if (connection.passwordMode == 'askPassword' || connection.passwordMode == 'askUser') {
throw new MissingCredentialsError({ conid, passwordMode: connection.passwordMode });
}
if (connection.useRedirectDbLogin) {
throw new MissingCredentialsError({ conid, redirectToDbLogin: true });
}
const subprocess = fork(
global['API_PACKAGE'] || process.argv[1],
[
'--is-forked-api',
'--start-process',
'serverConnectionProcess',
...processArgs.getPassArgs(),
// ...process.argv.slice(3),
],
{
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
}
);
pipeForkLogs(subprocess);
const newOpened = {
conid,
subprocess,
databases: [],
connection,
status: {
name: 'pending',
},
disconnected: false,
};
this.opened.push(newOpened);
delete this.closed[conid];
socket.emitChanged(`server-status-changed`);
subprocess.on('message', message => {
// @ts-ignore
const { msgtype } = message;
if (handleProcessCommunication(message, subprocess)) return;
if (newOpened.disconnected) return;
this[`handle_${msgtype}`](conid, message);
});
subprocess.on('exit', () => {
if (newOpened.disconnected) return;
this.close(conid, false);
});
subprocess.on('error', err => {
logger.error(extractErrorLogData(err), 'DBGM-00119 Error in server connection subprocess');
if (newOpened.disconnected) return;
this.close(conid, false);
});
subprocess.send({ msgtype: 'connect', ...connection, globalSettings: await config.getSettings() });
return newOpened;
});
return res;
},
close(conid, kill = true) {
const existing = this.opened.find(x => x.conid == conid);
if (existing) {
existing.disconnected = true;
if (kill) {
try {
existing.subprocess.kill();
} catch (err) {
logger.error(extractErrorLogData(err), 'DBGM-00120 Error killing subprocess');
}
}
this.opened = this.opened.filter(x => x.conid != conid);
this.closed[conid] = {
...existing.status,
name: 'error',
};
socket.emitChanged(`server-status-changed`);
}
},
disconnect_meta: true,
async disconnect({ conid }, req) {
await testConnectionPermission(conid, req);
await this.close(conid, true);
return { status: 'ok' };
},
listDatabases_meta: true,
async listDatabases({ conid }, req) {
if (!conid) return [];
if (conid == '__model') return [];
const loadedPermissions = await loadPermissionsFromRequest(req);
await testConnectionPermission(conid, req, loadedPermissions);
const opened = await this.ensureOpened(conid);
sendToAuditLog(req, {
category: 'serverop',
component: 'ServerConnectionsController',
action: 'listDatabases',
event: 'databases.list',
severity: 'info',
conid,
sessionParam: `${conid}`,
sessionGroup: 'listDatabases',
message: `Loaded databases for connection`,
});
if (!hasPermission(`all-databases`, loadedPermissions)) {
// filter databases by permissions
const databasePermissions = await loadDatabasePermissionsFromRequest(req);
const res = [];
for (const db of opened?.databases ?? []) {
const databasePermissionRole = getDatabasePermissionRole(db.id, db.name, databasePermissions);
if (databasePermissionRole != 'deny') {
res.push({
...db,
databasePermissionRole,
});
}
}
return res;
}
return opened?.databases ?? [];
},
version_meta: true,
async version({ conid }, req) {
await testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid);
return opened?.version ?? null;
},
serverStatus_meta: true,
async serverStatus() {
return {
...this.closed,
..._.mapValues(_.keyBy(this.opened, 'conid'), 'status'),
};
},
ping_meta: true,
async ping({ conidArray, strmid }) {
await Promise.all(
_.uniq(conidArray).map(async conid => {
const last = this.lastPinged[conid];
if (last && new Date().getTime() - last < 30 * 1000) {
return Promise.resolve();
}
this.lastPinged[conid] = new Date().getTime();
const opened = await this.ensureOpened(conid);
if (!opened) {
return Promise.resolve();
}
try {
opened.subprocess.send({ msgtype: 'ping' });
} catch (err) {
logger.error(extractErrorLogData(err), 'DBGM-00121 Error pinging server connection');
this.close(conid);
}
})
);
socket.setStreamIdFilter(strmid, { conid: [...(conidArray ?? []), '__model'] });
return { status: 'ok' };
},
refresh_meta: true,
async refresh({ conid, keepOpen }, req) {
await testConnectionPermission(conid, req);
if (!keepOpen) this.close(conid);
await this.ensureOpened(conid);
return { status: 'ok' };
},
async sendDatabaseOp({ conid, msgtype, name }, req) {
await testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid);
if (!opened) {
return null;
}
if (opened.connection.isReadOnly) return false;
const res = await this.sendRequest(opened, { msgtype, name });
if (res.errorMessage) {
console.error(res.errorMessage);
return {
apiErrorMessage: res.errorMessage,
};
}
return res.result || null;
},
createDatabase_meta: true,
async createDatabase({ conid, name }, req) {
return this.sendDatabaseOp({ conid, msgtype: 'createDatabase', name }, req);
},
dropDatabase_meta: true,
async dropDatabase({ conid, name }, req) {
return this.sendDatabaseOp({ conid, msgtype: 'dropDatabase', name }, req);
},
sendRequest(conn, message) {
const msgid = crypto.randomUUID();
const promise = new Promise((resolve, reject) => {
this.requests[msgid] = [resolve, reject];
try {
conn.subprocess.send({ msgid, ...message });
} catch (err) {
logger.error(extractErrorLogData(err), 'DBGM-00122 Error sending request');
this.close(conn.conid);
}
});
return promise;
},
async loadDataCore(msgtype, { conid, ...args }, req) {
await testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid);
if (!opened) {
return null;
}
const res = await this.sendRequest(opened, { msgtype, ...args });
if (res.errorMessage) {
console.error(res.errorMessage);
return {
errorMessage: res.errorMessage,
};
}
return res.result || null;
},
serverSummary_meta: true,
async serverSummary({ conid }, req) {
await testConnectionPermission(conid, req);
logger.info({ conid }, 'DBGM-00260 Processing server summary');
return this.loadDataCore('serverSummary', { conid });
},
listDatabaseProcesses_meta: true,
async listDatabaseProcesses(ctx, req) {
const { conid } = ctx;
// logger.info({ conid }, 'DBGM-00261 Listing processes of database server');
testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid);
if (!opened) {
return null;
}
if (opened.connection.isReadOnly) return false;
return this.sendRequest(opened, { msgtype: 'listDatabaseProcesses' });
},
killDatabaseProcess_meta: true,
async killDatabaseProcess(ctx, req) {
const { conid, pid } = ctx;
testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid);
if (!opened) {
return null;
}
if (opened.connection.isReadOnly) return false;
return this.sendRequest(opened, { msgtype: 'killDatabaseProcess', pid });
},
summaryCommand_meta: true,
async summaryCommand({ conid, command, row }, req) {
await testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid);
if (!opened) {
return null;
}
if (opened.connection.isReadOnly) return false;
return this.loadDataCore('summaryCommand', { conid, command, row });
},
getOpenedConnectionReport() {
return this.opened.map(con => ({
status: con.status,
versionText: con.version?.versionText,
databaseCount: con.databases.length,
connection: _.pick(con.connection, [
'engine',
'useSshTunnel',
'authType',
'trustServerCertificate',
'useSsl',
'sshMode',
]),
}));
},
};