UNPKG

redis-commander

Version:

Redis web-based management tool written in node.js

333 lines (311 loc) 11.4 kB
'use strict'; let myUtils = require('../util'); let config = require('config'); const connectionWrapper = require("../connections"); module.exports = function() { const express = require('express'); const router = express.Router(); const connectionWrapper = require('../connections'); router.get('/connections', getConnections); router.post('/login', postLogin); router.post('/login/detectDb', postLoginDetectDb); router.get('/config', getConfig); router.post('/logout/:connectionId', postLogout); function getConnections(req, res) { res.json({ 'ok': true, 'connections': req.app.locals.redisConnections.convertConnectionsInfoForUI() }); } function getConfig (req, res) { // do not return connections at all, that queried via /connections route... return res.send(config.get('ui')); } /** extract all connection data needed from body of request to create a new connection * object suitable to create new redis client from via utility function. * Function throws error if data are missing or non-parsable. * This function understands redis connections via socket, single ip or sentinel. * * @param {object} body body of request with connection data * @returns {{password: *, port: number, dbIndex: number, label: *}} connection object */ function extractLoginDataFromBody(body) { let newConnection = { label: body.label, port: body.port, password: body.password, dbIndex: body.dbIndex }; if (body.serverType === 'sentinel') { newConnection.sentinels = myUtils.parseRedisServerList('newConnection', body.sentinels); newConnection.sentinelName = myUtils.getRedisSentinelGroupName(body.sentinelName); switch (body.sentinelPWType) { case 'sentinel': newConnection.sentinelPassword = body.sentinelPassword; break; case 'redis': newConnection.sentinelPassword = body.password; break; } if (body.sentinelTLS === 'yes') { newConnection.sentinelTLS = true; } else if (body.sentinelTLS === 'custom') { newConnection.sentinelTLS = {}; if (body.sentinelTLSCA) { newConnection.sentinelTLS.ca = body.sentinelTLSCA.replace(/\\n/g, '\n'); } if (body.sentinelTLSPublicKey) { newConnection.sentinelTLS.cert = body.sentinelTLSPublicKey.replace(/\\n/g, '\n'); } if (body.sentinelTLSPrivateKey) { newConnection.sentinelTLS.key = body.sentinelTLSPrivateKey.replace(/\\n/g, '\n'); } if (body.sentinelTLSServerName) { newConnection.sentinelTLS.servername = body.sentinelTLSServerName; } } } else if (body.serverType === 'cluster') { newConnection.clusters = myUtils.parseRedisServerList('newConnection', body.clusters); delete newConnection.port; } else if (typeof body.hostname === 'string') { if (body.hostname.startsWith('/')) { newConnection.path = body.hostname; } else { newConnection.host = body.hostname; } } else { throw new Error('invalid or missing hostname or socket path'); } if (body.redisTLS === 'yes') { newConnection.tls = true newConnection.clusterNoTlsValidation = (typeof body.clusterNoTlsValidation !== 'undefined'); // checkbox } else if (body.redisTLS === 'custom') { newConnection.tls = {}; newConnection.clusterNoTlsValidation = (typeof body.clusterNoTlsValidation !== 'undefined'); if (body.redisTLSCA) { newConnection.tls.ca = body.redisTLSCA.replace(/\\n/g, '\n'); } if (body.redisTLSPublicKey) { newConnection.tls.cert = body.redisTLSPublicKey.replace(/\\n/g, '\n'); } if (body.redisTLSPrivateKey) { newConnection.tls.key = body.redisTLSPrivateKey.replace(/\\n/g, '\n'); } if (body.redisTLSServerName) { newConnection.tls.servername = body.sentinelTLSServerName; } } return newConnection; } function postLogin (req, res, next) { if (Number.isNaN(req.body.dbIndex)) { return res.json({ ok: false, message: 'invalid database index' }); } // first check if this connection is already known & active - do not create duplicate connections let newConnection = {}; try { newConnection = extractLoginDataFromBody(req.body); } catch (e) { return res.json({ ok: false, message: e.message }); } if (req.app.locals.redisConnections.containsConnection(newConnection)) { return res.json({ ok: true, message: 'already logged in to this server and db' }); } // now try to log in if (newConnection.sentinels) { console.log('connecting sentinel... ', newConnection.sentinelName, JSON.stringify(newConnection.sentinels)); } else if (newConnection.clusters) { console.log('connecting cluster... ', JSON.stringify(newConnection.clusters)); } else { console.log('connecting... ', newConnection.host, newConnection.port); } let client = myUtils.createRedisClient(newConnection); req.app.locals.redisConnections.setUpConnection(client, // called on connection errors (ECONNRESET, AUTH failed etc.), used to inform frontend about it function (err) { console.log('Invalid Login: ' + err); if (!res._headerSent) { return res.json({ ok: false, message: 'invalid login: ' + (err.message ? err.message : JSON.stringify(err)) }); } client.disconnect(); return; }, // called if connection was successful, add connection to our lists and configs and send back success to client function () { // add to in-memory connection list req.app.locals.redisConnections.push(client); // written config and current in-memory config may differ if (!connectionWrapper.containsConnection(config.get('connections'), newConnection)) { config.connections.push(newConnection); } req.app.saveConfig(config, function (errSave) { if (errSave) { return next(errSave); } if (!res._headerSent) { return res.json({'ok': true}) } }); }); } function postLoginDetectDb (req, res, next) { try { let newConnection = extractLoginDataFromBody(req.body); // set db to zero as this one must exist, all higher numbers are optional... newConnection.dbIndex = 0; // now try to log in and get server info to check number of keys per db if (newConnection.sentinels) { console.log('checking for dbs at sentinel... ', newConnection.sentinelName, JSON.stringify(newConnection.sentinels)); } else if (newConnection.clusters) { console.log('checking for dbs at cluster... ', JSON.stringify(newConnection.clusters)); } else { console.log('checking for dbs... ', newConnection.host, newConnection.port); } let client = myUtils.createRedisClient(newConnection); client.on('error', function (err) { disconnectClient(client); console.log('Cannot connect to redis db: ' + err.message); return res.json({ ok: false, message: `Error connecting to Redis to get all databases used: ${err.message}` }); }); client.on('ready', function () { Promise.allSettled([ client.call('info', 'keyspace'), client.call('config', 'get', 'databases') ]).then((promises) => { let dbMax = 16; let host = ''; let dbLines = [] // check which key-spaces aka dbs are holding keys right now if (promises[0].status === 'rejected') { console.log('Error calling "info" command to get all databases used.', (promises[0].reason ? promises[0].reason.message : 'unknown error')); return res.json({ ok: false, message: (promises[0].reason ? promises[0].reason.message : 'Error calling "info" command to get all databases used.') }); } else { dbLines = promises[0].value.split('\n').filter(function(line) { return line.trim().match(/^db\d+:/); }).map(function(line) { let parts = line.trim().split(':'); return { dbIndex: parts[0].substr(2), keys: parts[1] }; }); } // check number of max dbs allowed (config get databases), defaults to 16 if (promises[1].status === 'rejected') { // ignore errors, often command not allowed for security n stuff console.info('Cannot query max number of databases allowed, use default 16 instead: ', promises[1].reason.message); } else { dbMax = Array.isArray(promises[1].value) ? parseInt(promises[1].value[1]) : 16; } switch (client.options.type) { case 'socket': host = client.options.path; break; case 'sentinel': host = client.options.sentinels[0].host; break; case 'cluster': host = client.options.clusters[0].host; break; default: // standalone host = client.options.host; } res.json({ ok: true, server: `${client.options.type} ${host}`, dbs: { used: dbLines, max: dbMax } }); }).finally(() => { disconnectClient(client); }); }); } catch (e) { return res.json({ ok: false, message: e.message }); } function disconnectClient(client) { client.quit(); client.disconnect(); } } function postLogout (req, res, next) { var connectionId = req.params.connectionId; req.app.logout(connectionId, function (err) { if (err) { return next(err); } removeConnectionFromDefaults(config.get('connections'), connectionId, function (errRem, newDefaults) { if (errRem) { console.log('postLogout - removeConnectionFromDefaults', errRem); if (!res._headerSent) { return res.send('OK'); } } config.connections = newDefaults; req.app.saveConfig(config, function (errSave) { if (errSave) { return next(errSave); } if (!res._headerSent) { return res.send('OK'); } }); }); }); } function removeConnectionFromDefaults (connections, connectionId, callback) { let notRemoved = true; connections.forEach(function (connection, index) { if (notRemoved) { if (connection.connectionId === connectionId) { notRemoved = false; connections.splice(index, 1); } } }); if (notRemoved) { return callback('Could not remove ' + connectionId + ' from default connections.'); } else { return callback(null, connections); } } return router; };