UNPKG

@awearsolutions/redis-commander

Version:

Redis web-based management tool written in node.js

296 lines (277 loc) 8.92 kB
var sf = require('sf'); var ejs = require('ejs'); var fs = require('fs'); var path = require('path'); var Redis = require('ioredis'); var express = require('express'); var browserify = require('browserify-middleware'); var myUtils = require('./util'); var methodOverride = require('method-override'); var bodyParser = require('body-parser'); var partials = require('express-partials'); var jwt = require('jsonwebtoken'); var crypto = require('crypto'); var bcrypt; try { bcrypt = require('bcrypt'); } catch (e) { bcrypt = require('bcryptjs'); } function equalStrings(a, b) { if (!crypto.timeingSafeEqual) { return a === b; } var bufA = Buffer.from(`${a}`); var bufB = Buffer.from(`${b}`); // Funny way to force buffers to have same length return crypto.timingSafeEqual( Buffer.concat([a, b]), Buffer.concat([b, a]) ); } var usedTokens = new Set(); function jwtSign(jwtSecret, data) { return new Promise((resolve, reject) => jwt.sign(data, jwtSecret, { "issuer": "Redis Commander", "subject": "Session Token", "expiresIn": 60 }, (err, token) => (err ? reject(err) : resolve(token)))); } function jwtVerify(jwtSecret, token) { return new Promise(resolve => { jwt.verify(token, jwtSecret, { "issuer": "Redis Commander", "subject": "Session Token" }, (err, decodedToken) => { if (err) { return resolve(false); } if (decodedToken.singleUse) { if (usedTokens.has(token)) { console.log("Single-Usage token already used"); return resolve(false); } usedTokens.add(token); if (decodedToken.exp) { setTimeout(() => { usedTokens.delete(token); }, ((decodedToken.exp * 1 + 10) * 1e3) - (new Date() * 1)) } } return resolve(true); }); }) } // process.chdir( path.join(__dirname, '..') ); // fix the cwd var viewsPath = path.join(__dirname, '../web/views'); var staticPath = path.join(__dirname, '../web/static'); var redisConnections = []; module.exports = function (httpServerOptions, _redisConnections, nosave, rootPattern, defaultJwtSecret) { redisConnections = _redisConnections; var jwtSecret = defaultJwtSecret || crypto.randomBytes(20).toString('base64'); var app = express(); app.use(partials()); app.use(function(req, res, next) { res.locals.sf = sf; res.locals.getFlashes = function() { if (req.query.error === 'login') { return { "error": ["Invalid Login"] }; } return {}; }; next(); }); app.redisConnections = redisConnections; app.getConfig = myUtils.getConfig; if (!nosave) { app.saveConfig = myUtils.saveConfig; } else { app.saveConfig = function (config, callback) { callback(null) }; } app.login = login; app.logout = logout; app.layoutFilename = path.join(__dirname, '../web/views/layout.ejs'); app.rootPattern = rootPattern; app.set('views', viewsPath); app.set('view engine', 'ejs'); app.use('/bootstrap', express.static(path.join(staticPath, '/bootstrap'))); app.use('/clippy-jquery', express.static(path.join(staticPath, '/clippy-jquery'))); app.use('/css', express.static(path.join(staticPath, '/css'))); app.use('/favicon.png', express.static(path.join(staticPath, '/favicon.png'))); app.use('/images', express.static(path.join(staticPath, '/images'))); app.use('/json-tree', express.static(path.join(staticPath, '/json-tree'))); app.use('/jstree', express.static(path.join(staticPath, '/jstree'))); app.use('/scripts', express.static(path.join(staticPath, '/scripts'))); app.use('/templates', express.static(path.join(staticPath, '/templates'))); var browserifyCallback = browserify(['cmdparser','readline-browserify']); // WTF I don't know how to use app.use(app.router) so order will be maintained app.use('/browserify.js', function(req, res, next) { if ((req.method !== 'GET') || (req.path !== '/')) { return next(); } return browserifyCallback(req, res, next); }); app.use(bodyParser.urlencoded({ extended: false })) app.use(bodyParser.json()) app.use(methodOverride()); app.use(express.query()); app.use('/', function(req, res, next) { if ((req.method !== 'GET') || (req.path !== '/')) { return next(); } res.render('home/home.ejs', { title: 'Home', layout: req.app.layoutFilename }); }); app.use('/signin', function(req, res, next) { if ((req.method !== 'POST') || (req.path !== '/')) { return next(); } return Promise.resolve() .then(() => { if (!httpServerOptions.username || !(httpServerOptions.passwordHash || httpServerOptions.password)) { // username is not defined or password is not defined return true; } if (req.body && (req.body.username || req.body.password)) { // signin with username and password if (req.body.username !== httpServerOptions.username) { return false; } if (httpServerOptions.passwordHash) { return bcrypt.compare(`${req.body.password}`, httpServerOptions.passwordHash) } return equalStrings(`${req.body.password}`, httpServerOptions.password); } var authorization = (req.get('Authorization') || '').split(/\s+/); if (/^Bearer$/i.test(authorization[0])) { return new jwtVerify(jwtSecret, authorization[1] || ''); } return false; }) .then(success => { if (!success) { return res.json({ "ok": false }); } return Promise.all([jwtSign(jwtSecret, {}), jwtSign(jwtSecret, { "singleUse": true })]) .then(([bearerToken, queryToken]) => res.json({ "ok": true, "bearerToken": bearerToken, "queryToken": queryToken })); }); }); app.use(function(req, res, next) { if (!httpServerOptions.username || !(httpServerOptions.passwordHash || httpServerOptions.password)) { return next(); } var token; if (req.body && req.body.redisCommanderQueryToken) { token = req.body.redisCommanderQueryToken; } else if (req.query.redisCommanderQueryToken) { token = req.query.redisCommanderQueryToken; } else { var authorization = `${req.get('Authorization') || ''}`.split(/\s+/); if (/^Bearer$/i.test(authorization[0])) { token = `${authorization[1] || ''}`; } } if (!token) { res.statusCode = 401; return res.end('Unauthorized - Missing Token'); } return jwtVerify(jwtSecret, token) .then(success => { if (!success) { res.statusCode = 401; return res.end('Unauthorized - Token Invalid or Expired'); } return next(); }); }); app.use(app.router); require('./routes')(app); return app; }; function logout (hostname, port, db, callback) { var notRemoved = true; redisConnections.forEach(function (instance, index) { if (notRemoved && instance.options.host == hostname && instance.options.port == port && instance.options.db == db) { notRemoved = false; var connectionToClose = redisConnections.splice(index, 1); connectionToClose[0].quit(); } }); if (notRemoved) { return callback(new Error("Could not remove ", hostname, port, ".")); } else { return callback(null); } } function login (label, hostname, port, password, dbIndex, callback) { function onceCallback(err) { if (!callback) { return; } var callbackCopy = callback; callback = null; callbackCopy(err); } console.log('connecting... ', hostname, port); var client = new Redis({ port: port, host: hostname, family: 4, dbIndex: dbIndex, password: password }); client.label = label; var isPushed = false; client.on("error", function (err) { console.error("Redis error", err.stack); if (!isPushed) { console.error("Quiting Redis"); client.quit(); client.disconnect(); } onceCallback(err); }); client.on("end", function () { console.log("Connection closed. Attempting to Reconnect..."); }); if (password) { return client.auth(password, function (err) { if (err) { console.error("Could not authenticate", err.stack); return onceCallback(err); } client.on("connect", selectDatabase); }); } else { return client.on("connect", selectDatabase); } function selectDatabase () { try { dbIndex = parseInt(dbIndex || 0); } catch (e) { return onceCallback(e); } return client.select(dbIndex, function (err) { if (err) { console.log("could not select database", err.stack); return onceCallback(err) } console.log("Using Redis DB #" + dbIndex); redisConnections.push(client); isPushed = true; return onceCallback(); }); } }