diff options
Diffstat (limited to '')
-rw-r--r-- | server/src/serverLib/CommandManager.js | 95 | ||||
-rw-r--r-- | server/src/serverLib/ConfigManager.js | 15 | ||||
-rw-r--r-- | server/src/serverLib/CoreApp.js | 1 | ||||
-rw-r--r-- | server/src/serverLib/ImportsManager.js | 63 | ||||
-rw-r--r-- | server/src/serverLib/MainServer.js | 138 | ||||
-rw-r--r-- | server/src/serverLib/RateLimiter.js | 34 | ||||
-rw-r--r-- | server/src/serverLib/StatsManager.js | 2 |
7 files changed, 203 insertions, 145 deletions
diff --git a/server/src/serverLib/CommandManager.js b/server/src/serverLib/CommandManager.js index 71c8884..a99d0a9 100644 --- a/server/src/serverLib/CommandManager.js +++ b/server/src/serverLib/CommandManager.js @@ -10,6 +10,9 @@ const path = require('path'); const didYouMean = require('didyoumean2').default; +// default command modules path +const CmdDir = 'src/commands'; + class CommandManager { /** * Create a `CommandManager` instance for handling commands/protocol @@ -18,26 +21,25 @@ class CommandManager { */ constructor (core) { this.core = core; - this._commands = []; - this._categories = []; + this.commands = []; + this.categories = []; } /** * (Re)initializes name spaces for commands and starts load routine * + * @return {String} Module errors or empty if none */ loadCommands () { - this._commands = []; - this._categories = []; - - const core = this.core; + this.commands = []; + this.categories = []; - const commandImports = core.dynamicImports.getImport('src/commands'); + const commandImports = this.core.dynamicImports.getImport(CmdDir); let cmdErrors = ''; Object.keys(commandImports).forEach(file => { let command = commandImports[file]; let name = path.basename(file); - cmdErrors += this._validateAndLoad(command, file, name); + cmdErrors += this.validateAndLoad(command, file, name); }); return cmdErrors; @@ -49,9 +51,11 @@ class CommandManager { * @param {Object} command reference to the newly loaded object * @param {String} file file path to the module * @param {String} name command (`cmd`) name + * + * @return {String} Module errors or empty if none */ - _validateAndLoad (command, file, name) { - let error = this._validateCommand(command); + validateAndLoad (command, file, name) { + let error = this.validateCommand(command); if (error) { let errText = `Failed to load '${name}': ${error}`; @@ -70,8 +74,8 @@ class CommandManager { command.info.category = category; - if (this._categories.indexOf(category) === -1) { - this._categories.push(category); + if (this.categories.indexOf(category) === -1) { + this.categories.push(category); } } @@ -85,7 +89,7 @@ class CommandManager { } } - this._commands.push(command); + this.commands.push(command); return ''; } @@ -94,8 +98,10 @@ class CommandManager { * Checks the module after having been `require()`ed in and reports errors * * @param {Object} object reference to the newly loaded object + * + * @return {String} Module errors or null if none */ - _validateCommand (object) { + validateCommand (object) { if (typeof object !== 'object') return 'command setup is invalid'; @@ -115,27 +121,37 @@ class CommandManager { * Pulls all command names from a passed `category` * * @param {String} category [Optional] filter return results by this category + * + * @return {Array} Array of command modules matching the category */ all (category) { - return !category ? this._commands : this._commands.filter(c => c.info.category.toLowerCase() === category.toLowerCase()); + return !category ? this.commands : this.commands.filter( + c => c.info.category.toLowerCase() === category.toLowerCase() + ); } /** * Pulls all category names * + * @return {Array} Array of sub directories under CmdDir */ - categories () { - return this._categories; + get categoriesList () { + return this.categories; } /** * Pulls command by name or alia(s) * * @param {String} name name or alias of command + * + * @return {Object} Target command module object */ get (name) { return this.findBy('name', name) - || this._commands.find(command => command.info.aliases instanceof Array && command.info.aliases.indexOf(name) > -1); + || this.commands.find( + command => command.info.aliases instanceof Array && + command.info.aliases.indexOf(name) > -1 + ); } /** @@ -143,9 +159,11 @@ class CommandManager { * * @param {String} key name or alias of command * @param {String} value name or alias of command + * + * @return {Object} Target command module object */ findBy (key, value) { - return this._commands.find(c => c.info[key] === value); + return this.commands.find(c => c.info[key] === value); } /** @@ -154,7 +172,9 @@ class CommandManager { * @param {Object} server main server object */ initCommandHooks (server) { - this._commands.filter(c => typeof c.initHooks !== 'undefined').forEach(c => c.initHooks(server)); + this.commands.filter(c => typeof c.initHooks !== 'undefined').forEach( + c => c.initHooks(server) + ); } /** @@ -163,6 +183,8 @@ class CommandManager { * @param {Object} server main server reference * @param {Object} socket calling socket reference * @param {Object} data command structure passed by socket (client) + * + * @return {*} Arbitrary module return data */ handleCommand (server, socket, data) { // Try to find command first @@ -172,7 +194,7 @@ class CommandManager { return this.execute(command, server, socket, data); } else { // Then fail with helpful (sorta) message - return this._handleFail(server, socket, data); + return this.handleFail(server, socket, data); } } @@ -182,8 +204,10 @@ class CommandManager { * @param {Object} server main server reference * @param {Object} socket calling socket reference * @param {Object} data command structure passed by socket (client) + * + * @return {*} Arbitrary module return data */ - _handleFail(server, socket, data) { + handleFail (server, socket, data) { const maybe = didYouMean(data.cmd, this.all().map(c => c.info.name), { threshold: 5, thresholdType: 'edit-distance' @@ -193,13 +217,17 @@ class CommandManager { // Found a suggestion, pass it on to their dyslexic self return this.handleCommand(server, socket, { cmd: 'socketreply', - cmdKey: server._cmdKey, + cmdKey: server.cmdKey, text: `Command not found, did you mean: \`${maybe}\`?` }); } - // Request so mangled that I don't even, silently fail - return; + // Request so mangled that I don't even. . . + return this.handleCommand(server, socket, { + cmd: 'socketreply', + cmdKey: server.cmdKey, + text: 'Unknown command' + }); } /** @@ -209,8 +237,10 @@ class CommandManager { * @param {Object} server main server reference * @param {Object} socket calling socket reference * @param {Object} data command structure passed by socket (client) + * + * @return {*} Arbitrary module return data */ - async execute(command, server, socket, data) { + async execute (command, server, socket, data) { if (typeof command.requiredData !== 'undefined') { let missing = []; for (let i = 0, len = command.requiredData.length; i < len; i++) { @@ -219,11 +249,16 @@ class CommandManager { } if (missing.length > 0) { - console.log(`Failed to execute '${command.info.name}': missing required ${missing.join(', ')}\n\n`); + console.log(`Failed to execute '${ + command.info.name + }': missing required ${missing.join(', ')}\n\n`); + this.handleCommand(server, socket, { cmd: 'socketreply', - cmdKey: server._cmdKey, - text: `Failed to execute '${command.info.name}': missing required ${missing.join(', ')}\n\n` + cmdKey: server.cmdKey, + text: `Failed to execute '${ + command.info.name + }': missing required ${missing.join(', ')}\n\n` }); return null; @@ -238,7 +273,7 @@ class CommandManager { this.handleCommand(server, socket, { cmd: 'socketreply', - cmdKey: server._cmdKey, + cmdKey: server.cmdKey, text: errText }); diff --git a/server/src/serverLib/ConfigManager.js b/server/src/serverLib/ConfigManager.js index ebec050..36af1ec 100644 --- a/server/src/serverLib/ConfigManager.js +++ b/server/src/serverLib/ConfigManager.js @@ -15,10 +15,10 @@ class ConfigManager { /** * Create a `ConfigManager` instance for managing application settings * - * @param {String} base executing directory name; __dirname + * @param {String} basePath executing directory name; __dirname */ - constructor (base = __dirname) { - this.configPath = path.resolve(base, 'config/config.json'); + constructor (basePath = __dirname) { + this.configPath = path.resolve(basePath, 'config/config.json'); if (!fse.existsSync(this.configPath)){ fse.ensureFileSync(this.configPath); @@ -26,10 +26,9 @@ class ConfigManager { } /** - * (Re)builds the config.json (main server config), or loads the config into mem - * if rebuilding, process will exit- this is to allow a process manager to take over + * Loads config.json (main server config) into mem * - * @param {Boolean} reconfiguring set to true by `scripts/configure.js`, will exit if true + * @return {Object || Boolean} False if the config.json could not be loaded */ async load () { try { @@ -44,6 +43,7 @@ class ConfigManager { /** * Creates backup of current config into configPath * + * @return {String} Backed up config.json path */ async backup () { const backupPath = `${this.configPath}.${dateFormat('dd-mm-yy-HH-MM-ss')}.bak`; @@ -56,6 +56,7 @@ class ConfigManager { * First makes a backup of the current `config.json`, then writes current config * to disk * + * @return {Boolean} False on failure */ async save () { const backupPath = await this.backup(); @@ -77,6 +78,8 @@ class ConfigManager { * * @param {*} key arbitrary configuration key * @param {*} value new value to change `key` to + * + * @return {Boolean} False on failure */ async set (key, value) { const realKey = `${key}`; diff --git a/server/src/serverLib/CoreApp.js b/server/src/serverLib/CoreApp.js index 012ae44..6bab090 100644 --- a/server/src/serverLib/CoreApp.js +++ b/server/src/serverLib/CoreApp.js @@ -45,7 +45,6 @@ class CoreApp { buildImportManager () { this.dynamicImports = new ImportsManager(path.join(__dirname, '../..')); - this.dynamicImports.init(); } buildCommandsManager () { diff --git a/server/src/serverLib/ImportsManager.js b/server/src/serverLib/ImportsManager.js index a049d5c..8db7b68 100644 --- a/server/src/serverLib/ImportsManager.js +++ b/server/src/serverLib/ImportsManager.js @@ -14,12 +14,11 @@ class ImportsManager { /** * Create a `ImportsManager` instance for (re)loading classes and config * - * @param {String} base executing directory name; __dirname + * @param {String} basePath executing directory name; default __dirname */ - constructor (base) { - this._base = base; - - this._imports = {}; + constructor (basePath) { + this.basePath = basePath; + this.imports = {}; } /** @@ -28,29 +27,18 @@ class ImportsManager { * @type {String} readonly */ get base () { - return this._base; - } - - /** - * Initialize this class and start loading target directories - * - */ - init () { - let errorText = ''; - ImportsManager.load_dirs.forEach(dir => { - errorText += this.loadDir(dir); - }); - - return errorText; + return this.basePath; } /** * Gather all js files from target directory, then verify and load * - * @param {String} dirName The name of the dir to load, relative to the _base path. + * @param {String} dirName The name of the dir to load, relative to the basePath. + * + * @return {String} Load errors or empty if none */ loadDir (dirName) { - const dir = path.resolve(this._base, dirName); + const dir = path.resolve(this.basePath, dirName); let errorText = ''; try { @@ -68,11 +56,11 @@ class ImportsManager { return errorText; } - if (!this._imports[dirName]) { - this._imports[dirName] = {}; + if (!this.imports[dirName]) { + this.imports[dirName] = {}; } - this._imports[dirName][file] = imported; + this.imports[dirName][file] = imported; }); } catch (e) { let err = `Unable to load modules from ${dirName}\n${e}`; @@ -88,14 +76,22 @@ class ImportsManager { * Unlink references to each loaded module, pray to google that gc knows it's job, * then reinitialize this class to start the reload * - * @param {String} dirName The name of the dir to load, relative to the _base path. + * @param {Array} dirName The name of the dir to load, relative to the _base path. + * + * @return {String} Load errors or empty if none */ - reloadDirCache (dirName) { - Object.keys(this._imports[dirName]).forEach((mod) => { - delete require.cache[require.resolve(mod)]; + reloadDirCache () { + let errorText = ''; + + Object.keys(this.imports).forEach(dir => { + Object.keys(this.imports[dir]).forEach((mod) => { + delete require.cache[require.resolve(mod)]; + }); + + errorText += this.loadDir(dir); }); - return this.init(); + return errorText; } /** @@ -103,19 +99,18 @@ class ImportsManager { * load required directory if not found * * @param {String} dirName The name of the dir to load, relative to the _base path. + * + * @return {Object} Object containing command module paths and structs */ getImport (dirName) { - let imported = this._imports[dirName]; + let imported = this.imports[dirName]; if (!imported) { this.loadDir(dirName); } - return Object.assign({}, this._imports[dirName]); + return Object.assign({}, this.imports[dirName]); } } -// automagically loaded directorys on instantiation -ImportsManager.load_dirs = ['src/commands']; - module.exports = ImportsManager; diff --git a/server/src/serverLib/MainServer.js b/server/src/serverLib/MainServer.js index 628d8ab..c7a4bbe 100644 --- a/server/src/serverLib/MainServer.js +++ b/server/src/serverLib/MainServer.js @@ -7,15 +7,15 @@ * */ -const wsServer = require('ws').Server; -const socketReady = require('ws').OPEN; -const crypto = require('crypto'); -const ipSalt = [...Array(Math.floor(Math.random()*128)+128)].map(i=>(~~(Math.random()*36)).toString(36)).join(''); -const internalCmdKey = [...Array(Math.floor(Math.random()*128)+128)].map(i=>(~~(Math.random()*36)).toString(36)).join(''); +const WsServer = require('ws').Server; +const SocketReady = require('ws').OPEN; +const Crypto = require('crypto'); const RateLimiter = require('./RateLimiter'); -const pulseSpeed = 16000; // ping all clients every X ms +const PulseSpeed = 16000; // ping all clients every X ms +const IpSalt = [...Array(Math.floor(Math.random()*128)+128)].map(i=>(~~(Math.random()*36)).toString(36)).join(''); +const InternalCmdKey = [...Array(Math.floor(Math.random()*128)+128)].map(i=>(~~(Math.random()*36)).toString(36)).join(''); -class MainServer extends wsServer { +class MainServer extends WsServer { /** * Create a HackChat server instance. * @@ -24,13 +24,32 @@ class MainServer extends wsServer { constructor (core) { super({ port: core.config.websocketPort }); - this._core = core; - this._hooks = {}; - this._police = new RateLimiter(); - this._cmdBlacklist = {}; - this._cmdKey = internalCmdKey; + this.core = core; + this.hooks = {}; + this.police = new RateLimiter(); + this.cmdBlacklist = {}; - this._heartBeat = setInterval(() => this.beatHeart(), pulseSpeed); + this.setupServer(); + this.loadHooks(); + } + + /** + * Internal command key getter. Used to verify that internal only commands + * originate internally and not from a connected client. + * TODO: update to a structure that cannot be passed through json + * + * @type {String} readonly + */ + get cmdKey () { + return InternalCmdKey; + } + + /** + * Create ping interval and setup server event listeners + * + */ + setupServer () { + this.heartBeat = setInterval(() => this.beatHeart(), PulseSpeed); this.on('error', (err) => { this.handleError('server', err); @@ -39,8 +58,6 @@ class MainServer extends wsServer { this.on('connection', (socket, request) => { this.newConnection(socket, request); }); - - this.loadHooks(); } /** @@ -56,7 +73,7 @@ class MainServer extends wsServer { for (let i = 0, l = targetSockets.length; i < l; i++) { try { - if (targetSockets[i].readyState === socketReady) { + if (targetSockets[i].readyState === SocketReady) { targetSockets[i].ping(); } } catch (e) { } @@ -93,18 +110,18 @@ class MainServer extends wsServer { */ handleData (socket, data) { // Don't penalize yet, but check whether IP is rate-limited - if (this._police.frisk(socket.remoteAddress, 0)) { - this._core.commands.handleCommand(this, socket, { + if (this.police.frisk(socket.remoteAddress, 0)) { + this.core.commands.handleCommand(this, socket, { cmd: 'socketreply', - cmdKey: this._cmdKey, - text: 'Your IP is being rate-limited or blocked.' + cmdKey: this.cmdKey, + text: 'You are being rate-limited or blocked.' }); return; } // Penalize here, but don't do anything about it - this._police.frisk(socket.remoteAddress, 1); + this.police.frisk(socket.remoteAddress, 1); // Ignore ridiculously large packets if (data.length > 65536) { @@ -112,7 +129,7 @@ class MainServer extends wsServer { } // Start sent data verification - var payload = null; + let payload = null; try { payload = JSON.parse(data); } catch (e) { @@ -124,6 +141,11 @@ class MainServer extends wsServer { return; } + // TODO: make this more flexible + /* + * Issue #1: hard coded `cmd` check + * Issue #2: hard coded `cmd` value checks + */ if (typeof payload.cmd === 'undefined') { return; } @@ -136,18 +158,19 @@ class MainServer extends wsServer { return; } - if (typeof this._cmdBlacklist[payload.cmd] === 'function') { + if (typeof this.cmdBlacklist[payload.cmd] === 'function') { return; } + // End TODO // // Execute `in` (incoming data) hooks and process results payload = this.executeHooks('in', socket, payload); if (typeof payload === 'string') { // A hook malfunctioned, reply with error - this._core.commands.handleCommand(this, socket, { + this.core.commands.handleCommand(this, socket, { cmd: 'socketreply', - cmdKey: this._cmdKey, + cmdKey: this.cmdKey, text: payload }); @@ -158,7 +181,7 @@ class MainServer extends wsServer { } // Finished verification & hooks, pass to command modules - this._core.commands.handleCommand(this, socket, payload); + this.core.commands.handleCommand(this, socket, payload); } /** @@ -167,9 +190,9 @@ class MainServer extends wsServer { * @param {Object} socket Closing socket object */ handleClose (socket) { - this._core.commands.handleCommand(this, socket, { + this.core.commands.handleCommand(this, socket, { cmd: 'disconnect', - cmdKey: this._cmdKey + cmdKey: this.cmdKey }); } @@ -198,9 +221,9 @@ class MainServer extends wsServer { if (typeof payload === 'string') { // A hook malfunctioned, reply with error - this._core.commands.handleCommand(this, socket, { + this.core.commands.handleCommand(this, socket, { cmd: 'socketreply', - cmdKey: this._cmdKey, + cmdKey: this.cmdKey, text: payload }); @@ -211,7 +234,7 @@ class MainServer extends wsServer { } try { - if (socket.readyState === socketReady) { + if (socket.readyState === SocketReady) { socket.send(JSON.stringify(payload)); } } catch (e) { } @@ -232,6 +255,8 @@ class MainServer extends wsServer { * * @param {Object} payload Object to convert to json for transmission * @param {Object} filter see `this.findSockets()` + * + * @return {Boolean} False if no clients matched the filter, true if data sent */ broadcast (payload, filter) { let targetSockets = this.findSockets(filter); @@ -255,6 +280,8 @@ class MainServer extends wsServer { * = {} // matches all * = { channel: 'programming' } // matches any socket where (`socket.channel` === 'programming') * = { channel: 'programming', nick: 'Marzavec' } // matches any socket where (`socket.channel` === 'programming' && `socket.nick` === 'Marzavec') + * + * @return {Array} Clients who matched the filter requirements */ findSockets (filter) { let filterAttribs = Object.keys(filter); @@ -310,14 +337,16 @@ class MainServer extends wsServer { * encodes and shortens the output, returns that value * * @param {Object||String} target Either the target socket or ip as string + * + * @return {String} Hashed client connection string */ getSocketHash (target) { - let sha = crypto.createHash('sha256'); + let sha = Crypto.createHash('sha256'); if (typeof target === 'string') { - sha.update(target + ipSalt); + sha.update(target + IpSalt); } else { - sha.update(target.remoteAddress + ipSalt); + sha.update(target.remoteAddress + IpSalt); } return sha.digest('base64').substr(0, 15); @@ -332,26 +361,26 @@ class MainServer extends wsServer { // clear current hooks (if any) this.clearHooks(); // notify each module to register their hooks (if any) - this._core.commands.initCommandHooks(this); + this.core.commands.initCommandHooks(this); - if (typeof this._hooks['in'] !== 'undefined') { + if (typeof this.hooks['in'] !== 'undefined') { // start sorting, with incoming first - let curHooks = [ ...this._hooks['in'].keys() ]; + let curHooks = [ ...this.hooks['in'].keys() ]; let hookObj = []; for (let i = 0, j = curHooks.length; i < j; i++) { - hookObj = this._hooks['in'].get(curHooks[i]); + hookObj = this.hooks['in'].get(curHooks[i]); hookObj.sort( (h1, h2) => h1.priority - h2.priority ); - this._hooks['in'].set(hookObj); + this.hooks['in'].set(hookObj); } } - if (typeof this._hooks['out'] !== 'undefined') { + if (typeof this.hooks['out'] !== 'undefined') { // then outgoing - curHooks = [ ...this._hooks['out'].keys() ]; + curHooks = [ ...this.hooks['out'].keys() ]; for (let i = 0, j = curHooks.length; i < j; i++) { - hookObj = this._hooks['out'].get(curHooks[i]); + hookObj = this.hooks['out'].get(curHooks[i]); hookObj.sort( (h1, h2) => h1.priority - h2.priority ); - this._hooks['out'].set(hookObj); + this.hooks['out'].set(hookObj); } } } @@ -371,15 +400,15 @@ class MainServer extends wsServer { priority = 25; } - if (typeof this._hooks[type] === 'undefined') { - this._hooks[type] = new Map(); + if (typeof this.hooks[type] === 'undefined') { + this.hooks[type] = new Map(); } - if (!this._hooks[type].has(command)) { - this._hooks[type].set(command, []); + if (!this.hooks[type].has(command)) { + this.hooks[type].set(command, []); } - this._hooks[type].get(command).push({ + this.hooks[type].get(command).push({ run: hookFunction, priority: priority }); @@ -395,17 +424,19 @@ class MainServer extends wsServer { * @param {String} type The type of event, typically `in` (incoming) or `out` (outgoing) * @param {Object} socket Either the target client or the client triggering the hook (depending on `type`) * @param {Object} payload Either incoming data from client or outgoing data (depending on `type`) + * + * @return {Object || Boolean} */ executeHooks (type, socket, payload) { let command = payload.cmd; - if (typeof this._hooks[type] !== 'undefined') { - if (this._hooks[type].has(command)) { - let hooks = this._hooks[type].get(command); + if (typeof this.hooks[type] !== 'undefined') { + if (this.hooks[type].has(command)) { + let hooks = this.hooks[type].get(command); for (let i = 0, j = hooks.length; i < j; i++) { try { - payload = hooks[i].run(this._core, this, socket, payload); + payload = hooks[i].run(this.core, this, socket, payload); } catch (err) { let errText = `Hook failure, '${type}', '${command}': ${err}`; console.log(errText); @@ -425,9 +456,10 @@ class MainServer extends wsServer { /** * Wipe server hooks to make ready for module reload calls + * */ clearHooks () { - this._hooks = {}; + this.hooks = {}; } } diff --git a/server/src/serverLib/RateLimiter.js b/server/src/serverLib/RateLimiter.js index 87a1f3a..43cf077 100644 --- a/server/src/serverLib/RateLimiter.js +++ b/server/src/serverLib/RateLimiter.js @@ -13,25 +13,24 @@ class RateLimiter { * Create a ratelimiter instance. */ constructor () { - this._records = {}; - this._halflife = 30 * 1000; // milliseconds - this._threshold = 25; - this._hashes = []; + this.records = {}; + this.halflife = 30 * 1000; // milliseconds + this.threshold = 25; + this.hashes = []; } /** * Finds current score by `id` * * @param {String} id target id / address - * @public * - * @memberof Police + * @return {Object} Object containing the record meta */ search (id) { - let record = this._records[id]; + let record = this.records[id]; if (!record) { - record = this._records[id] = { + record = this.records[id] = { time: Date.now(), score: 0 } @@ -45,9 +44,8 @@ class RateLimiter { * * @param {String} id target id / address * @param {Number} deltaScore amount to adjust current score by - * @public * - * @memberof Police + * @return {Boolean} True if record threshold has been exceeded */ frisk (id, deltaScore) { let record = this.search(id); @@ -56,11 +54,11 @@ class RateLimiter { return true; } - record.score *= Math.pow(2, -(Date.now() - record.time ) / this._halflife); + record.score *= Math.pow(2, -(Date.now() - record.time ) / this.halflife); record.score += deltaScore; record.time = Date.now(); - if (record.score >= this._threshold) { + if (record.score >= this.threshold) { return true; } @@ -71,28 +69,22 @@ class RateLimiter { * Statically set server to no longer accept traffic from `id` * * @param {String} id target id / address - * @public - * - * @memberof Police */ arrest (id, hash) { let record = this.search(id); record.arrested = true; - this._hashes[hash] = id; + this.hashes[hash] = id; } /** * Remove statically assigned limit from `id` * * @param {String} id target id / address - * @public - * - * @memberof Police */ pardon (id) { - if (typeof this._hashes[id] !== 'undefined') { - id = this._hashes[id]; + if (typeof this.hashes[id] !== 'undefined') { + id = this.hashes[id]; } let record = this.search(id); diff --git a/server/src/serverLib/StatsManager.js b/server/src/serverLib/StatsManager.js index 4ec7ddf..e472504 100644 --- a/server/src/serverLib/StatsManager.js +++ b/server/src/serverLib/StatsManager.js @@ -20,6 +20,8 @@ class StatsManager { * Retrieve value of arbitrary `key` reference * * @param {String} key Reference to the arbitrary store name + * + * @return {*} Data referenced by `key` */ get (key) { return this.data[key]; |