From f28e65ab8035682372edbe1c11d9ca2581e0a2e6 Mon Sep 17 00:00:00 2001 From: marzavec Date: Wed, 6 Nov 2019 23:35:23 -0800 Subject: Syntax update and formatting tweaks --- server/src/serverLib/MainServer.js | 376 ++++++++++++++++++++++--------------- 1 file changed, 224 insertions(+), 152 deletions(-) (limited to 'server/src/serverLib/MainServer.js') diff --git a/server/src/serverLib/MainServer.js b/server/src/serverLib/MainServer.js index 8ef5129..35a0d3e 100644 --- a/server/src/serverLib/MainServer.js +++ b/server/src/serverLib/MainServer.js @@ -1,58 +1,96 @@ +import { + Server as WsServer, + OPEN as SocketReady, +} from 'ws'; +import { createHash } from 'crypto'; +import RateLimiter from './RateLimiter'; + +import { ServerConst } from '../utility/Constants'; + /** * Main websocket server handling communications and connection events - * - * Version: v2.0.0 - * Developer: Marzavec ( https://github.com/marzavec ) - * License: WTFPL ( http://www.wtfpl.net/txt/copying/ ) - * + * @property {RateLimiter} police - Main rate limit handler + * @property {String} cmdKey - Internal use command key + * @author Marzavec ( https://github.com/marzavec ) + * @version v2.0.0 + * @license WTFPL ( http://www.wtfpl.net/txt/copying/ ) */ - -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 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 { /** - * Create a HackChat server instance. - * - * @param {Object} core Reference to the global core object - */ - constructor (core) { + * Create a HackChat server instance + * @param {CoreApp} core Reference to the global core object + */ + constructor(core) { super({ port: core.config.websocketPort }); + /** + * Stored reference to the core + * @type {CoreApp} + */ this.core = core; + + /** + * Command key used to verify internal commands + * @type {String} + */ + this.internalCmdKey = [...Array(Math.floor(Math.random() * 128) + 128)].map(() => (~~(Math.random() * 36)).toString(36)).join(''); + + /** + * Salt used to hash a clients ip + * @type {String} + */ + this.ipSalt = [...Array(Math.floor(Math.random() * 128) + 128)].map(() => (~~(Math.random() * 36)).toString(36)).join(''); + + /** + * Data store for command hooks + * @type {Object} + */ this.hooks = {}; + + /** + * Main rate limit tracker + * @type {RateLimiter} + */ this.police = new RateLimiter(); + + /** + * Black listed command names + * @type {Object} + */ this.cmdBlacklist = {}; + /** + * Stored info about the last server error + * @type {ErrorEvent} + */ + this.lastErr = null; + 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 + * originate internally and not from a connected client + * @todo Update to a structure that cannot be passed through json + * @type {String} + * @public + * @readonly */ - get cmdKey () { - return InternalCmdKey; + get cmdKey() { + return this.internalCmdKey; } /** * Create ping interval and setup server event listeners - * + * @private + * @return {void} */ - setupServer () { - this.heartBeat = setInterval(() => this.beatHeart(), PulseSpeed); + setupServer() { + this.heartBeat = setInterval(() => this.beatHeart(), ServerConst.PulseSpeed); this.on('error', (err) => { - this.handleError('server', err); + this.handleError(err); }); this.on('connection', (socket, request) => { @@ -62,66 +100,71 @@ class MainServer extends WsServer { /** * Send empty `ping` frame to each client - * + * @private + * @return {void} */ - beatHeart () { - let targetSockets = this.findSockets({}); + beatHeart() { + const targetSockets = this.findSockets({}); if (targetSockets.length === 0) { return; } - for (let i = 0, l = targetSockets.length; i < l; i++) { + for (let i = 0, l = targetSockets.length; i < l; i += 1) { try { if (targetSockets[i].readyState === SocketReady) { targetSockets[i].ping(); } - } catch (e) { } + } catch (e) { /* yolo */ } } } /** * Bind listeners for the new socket created on connection to this class - * - * @param {Object} socket New socket object + * @param {ws#WebSocket} socket New socket object * @param {Object} request Initial headers of the new connection + * @private + * @return {void} */ - newConnection (socket, request) { - socket.remoteAddress = request.headers['x-forwarded-for'] || request.connection.remoteAddress; + newConnection(socket, request) { + const newSocket = socket; - socket.on('message', (data) => { + newSocket.address = request.headers['x-forwarded-for'] || request.connection.remoteAddress; + + newSocket.on('message', (data) => { this.handleData(socket, data); }); - socket.on('close', () => { + newSocket.on('close', () => { this.handleClose(socket); }); - socket.on('error', (err) => { - this.handleError(socket, err); + newSocket.on('error', (err) => { + this.handleError(err); }); } /** * Handle incoming messages from clients, parse and check command, then hand-off - * - * @param {Object} socket Calling socket object + * @param {ws#WebSocket} socket Calling socket object * @param {String} data Message sent from client + * @private + * @return {void} */ - handleData (socket, data) { + handleData(socket, data) { // Don't penalize yet, but check whether IP is rate-limited - if (this.police.frisk(socket.remoteAddress, 0)) { + if (this.police.frisk(socket.address, 0)) { this.core.commands.handleCommand(this, socket, { cmd: 'socketreply', cmdKey: this.cmdKey, - text: 'You are being rate-limited or blocked.' + 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.address, 1); // Ignore ridiculously large packets if (data.length > 65536) { @@ -141,11 +184,11 @@ class MainServer extends WsServer { return; } - // TODO: make this more flexible - /* - * Issue #1: hard coded `cmd` check - * Issue #2: hard coded `cmd` value checks - */ + /** + * @todo make the following more flexible + * Issue #1: hard coded `cmd` check + * Issue #2: hard coded `cmd` value checks + */ if (typeof payload.cmd === 'undefined') { return; } @@ -161,7 +204,7 @@ class MainServer extends WsServer { if (typeof this.cmdBlacklist[payload.cmd] === 'function') { return; } - // End TODO // + // End @todo // // Execute `in` (incoming data) hooks and process results payload = this.executeHooks('in', socket, payload); @@ -171,11 +214,11 @@ class MainServer extends WsServer { this.core.commands.handleCommand(this, socket, { cmd: 'socketreply', cmdKey: this.cmdKey, - text: payload + text: payload, }); return; - } else if (payload === false) { + } if (payload === false) { // A hook requested this data be dropped return; } @@ -185,87 +228,107 @@ class MainServer extends WsServer { } /** - * Handle socket close from clients - * - * @param {Object} socket Closing socket object + * Pass socket close event to disconnection command module + * @param {ws#WebSocket} socket Closing socket object + * @private + * @return {void} */ - handleClose (socket) { + handleClose(socket) { this.core.commands.handleCommand(this, socket, { cmd: 'disconnect', - cmdKey: this.cmdKey + cmdKey: this.cmdKey, }); } /** * "Handle" server or socket errors - * - * @param {Object||String} socket Calling socket object, or 'server' - * @param {String} err The sad stuff + * @param {ErrorEvent} err The sad stuff + * @private + * @return {void} */ - handleError (socket, err) { + handleError(err) { + this.lastErr = err; console.log(`Server error: ${err}`); } /** * Send data payload to specific socket/client - * * @param {Object} payload Object to convert to json for transmission - * @param {Object} socket The target client + * @param {ws#WebSocket} socket The target client + * @example + * server.send({ + * cmd: 'info', + * text: 'Only targetSocket will see this' + * }, targetSocket); + * @public + * @return {void} */ - send (payload, socket) { + send(payload, socket) { + let outgoingPayload = payload; + // Add timestamp to command - payload.time = Date.now(); + outgoingPayload.time = Date.now(); // Execute `in` (incoming data) hooks and process results - payload = this.executeHooks('out', socket, payload); + outgoingPayload = this.executeHooks('out', socket, outgoingPayload); - if (typeof payload === 'string') { + if (typeof outgoingPayload === 'string') { // A hook malfunctioned, reply with error this.core.commands.handleCommand(this, socket, { cmd: 'socketreply', cmdKey: this.cmdKey, - text: payload + text: outgoingPayload, }); return; - } else if (payload === false) { + } if (outgoingPayload === false) { // A hook requested this data be dropped return; } try { if (socket.readyState === SocketReady) { - socket.send(JSON.stringify(payload)); + socket.send(JSON.stringify(outgoingPayload)); } - } catch (e) { } + } catch (e) { /* yolo */ } } /** * Overload function for `this.send()` - * * @param {Object} payload Object to convert to json for transmission - * @param {Object} socket The target client + * @param {ws#WebSocket} socket The target client + * @example + * server.reply({ + * cmd: 'info', + * text: 'Only targetSocket will see this' + * }, targetSocket); + * @public + * @return {void} */ - reply (payload, socket) { + reply(payload, socket) { this.send(payload, socket); } /** * Finds sockets/clients that meet the filter requirements, then passes the data to them - * * @param {Object} payload Object to convert to json for transmission * @param {Object} filter see `this.findSockets()` - * + * @example + * server.broadcast({ + * cmd: 'info', + * text: 'Everyone in "programming" will see this' + * }, { channel: 'programming' }); + * @public * @return {Boolean} False if no clients matched the filter, true if data sent */ - broadcast (payload, filter) { - let targetSockets = this.findSockets(filter); + broadcast(payload, filter) { + const targetSockets = this.findSockets(filter); if (targetSockets.length === 0) { return false; } - for (let i = 0, l = targetSockets.length; i < l; i++) { + for (let i = 0, l = targetSockets.length; i < l; i += 1) { this.send(payload, targetSockets[i]); } @@ -274,51 +337,54 @@ class MainServer extends WsServer { /** * Finds sockets/clients that meet the filter requirements, returns result as array - * * @param {Object} data Object to convert to json for transmission * @param {Object} filter The socket must of equal or greater attribs matching `filter` - * = {} // 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') - * + * @example + * // match all sockets: + * `filter` = {} + * // match any socket where socket.channel === 'programming' + * `filter` = { channel: 'programming' } + * // match any socket where + * // socket.channel === 'programming' && socket.nick === 'Marzavec' + * `filter` = { channel: 'programming', nick: 'Marzavec' } + * @public * @return {Array} Clients who matched the filter requirements */ - findSockets (filter) { - let filterAttribs = Object.keys(filter); - let reqCount = filterAttribs.length; + findSockets(filter) { + const filterAttribs = Object.keys(filter); + const reqCount = filterAttribs.length; let curMatch; - let matches = []; - for ( let socket of this.clients ) { + const matches = []; + this.clients.forEach((socket) => { + // for (const socket of this.clients) { curMatch = 0; - for (let i = 0; i < reqCount; i++) { + for (let i = 0; i < reqCount; i += 1) { if (typeof socket[filterAttribs[i]] !== 'undefined') { - switch(typeof filter[filterAttribs[i]]) { + switch (typeof filter[filterAttribs[i]]) { case 'object': { if (Array.isArray(filter[filterAttribs[i]])) { if (filter[filterAttribs[i]].indexOf(socket[filterAttribs[i]]) !== -1) { - curMatch++; - } - } else { - if (socket[filterAttribs[i]] === filter[filterAttribs[i]]) { - curMatch++; + curMatch += 1; } + } else if (socket[filterAttribs[i]] === filter[filterAttribs[i]]) { + curMatch += 1; } - break; + break; } case 'function': { if (filter[filterAttribs[i]](socket[filterAttribs[i]])) { - curMatch++; + curMatch += 1; } - break; + break; } default: { if (socket[filterAttribs[i]] === filter[filterAttribs[i]]) { - curMatch++; + curMatch += 1; } - break; + break; } } } @@ -327,7 +393,7 @@ class MainServer extends WsServer { if (curMatch === reqCount) { matches.push(socket); } - } + }); return matches; } @@ -335,18 +401,20 @@ class MainServer extends WsServer { /** * Hashes target socket's remote address using non-static variable length salt * encodes and shortens the output, returns that value - * - * @param {Object||String} target Either the target socket or ip as string - * + * @param {(ws#WebSocket|String)} target Either the target socket or ip as string + * @example + * let userHash = server.getSocketHash('1.2.3.4'); + * let userHash = server.getSocketHash(client); + * @public * @return {String} Hashed client connection string */ - getSocketHash (target) { - let sha = Crypto.createHash('sha256'); + getSocketHash(target) { + const sha = createHash('sha256'); if (typeof target === 'string') { - sha.update(target + IpSalt); + sha.update(target + this.ipSalt); } else { - sha.update(target.remoteAddress + IpSalt); + sha.update(target.address + this.ipSalt); } return sha.digest('base64').substr(0, 15); @@ -355,9 +423,10 @@ class MainServer extends WsServer { /** * (Re)loads all command module hooks, then sorts their order of operation by * priority, ascending (0 being highest priority) - * + * @public + * @return {void} */ - loadHooks () { + loadHooks() { // clear current hooks (if any) this.clearHooks(); // notify each module to register their hooks (if any) @@ -366,23 +435,23 @@ class MainServer extends WsServer { let curHooks = []; let hookObj = []; - if (typeof this.hooks['in'] !== 'undefined') { + if (typeof this.hooks.in !== 'undefined') { // start sorting, with incoming first - curHooks = [ ...this.hooks['in'].keys() ]; - for (let i = 0, j = curHooks.length; i < j; i++) { - hookObj = this.hooks['in'].get(curHooks[i]); - hookObj.sort( (h1, h2) => h1.priority - h2.priority ); - this.hooks['in'].set(hookObj); + curHooks = [...this.hooks.in.keys()]; + for (let i = 0, j = curHooks.length; i < j; i += 1) { + hookObj = this.hooks.in.get(curHooks[i]); + hookObj.sort((h1, h2) => h1.priority - h2.priority); + this.hooks.in.set(hookObj); } } - if (typeof this.hooks['out'] !== 'undefined') { + if (typeof this.hooks.out !== 'undefined') { // then outgoing - curHooks = [ ...this.hooks['out'].keys() ]; - for (let i = 0, j = curHooks.length; i < j; i++) { - hookObj = this.hooks['out'].get(curHooks[i]); - hookObj.sort( (h1, h2) => h1.priority - h2.priority ); - this.hooks['out'].set(hookObj); + curHooks = [...this.hooks.out.keys()]; + for (let i = 0, j = curHooks.length; i < j; i += 1) { + hookObj = this.hooks.out.get(curHooks[i]); + hookObj.sort((h1, h2) => h1.priority - h2.priority); + this.hooks.out.set(hookObj); } } } @@ -391,17 +460,19 @@ class MainServer extends WsServer { * Adds a target function to an array of hooks. Hooks are executed either before * processing user input (`in`) or before sending data back to the client (`out`) * and allows a module to modify each payload before moving forward - * * @param {String} type The type of event, typically `in` (incoming) or `out` (outgoing) * @param {String} command Should match the desired `cmd` attrib of the payload - * @param {Function} hookFunction Target function to execute, should accept `server`, `socket` and `payload` as parameters - * @param {Number} priority Execution priority, hooks with priority 1 will be executed before hooks with priority 200 for example + * @param {Function} hookFunction Target function to execute, should accept + * `server`, `socket` and `payload` as parameters + * @param {Number} priority Execution priority, hooks with priority 1 will be executed before + * hooks with priority 200 for example + * @example + * // Create hook to add "and stuff" to every chat line + * server.registerHook('in', 'chat', (server, socket, payload) => payload.text += ' and stuff'); + * @public + * @return {void} */ - registerHook (type, command, hookFunction, priority) { - if (typeof priority === 'undefined') { - priority = 25; - } - + registerHook(type, command, hookFunction, priority = 25) { if (typeof this.hooks[type] === 'undefined') { this.hooks[type] = new Map(); } @@ -412,7 +483,7 @@ class MainServer extends WsServer { this.hooks[type].get(command).push({ run: hookFunction, - priority: priority + priority, }); } @@ -422,25 +493,25 @@ class MainServer extends WsServer { * A payload (modified or not) that will continue through the data flow * A boolean false to indicate halting the data through flow * A string which indicates an error occured in executing the hook - * * @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} + * @param {ws#WebSocket} socket Either target client or client (depends on `type`) + * @param {Object} payload Either incoming data from client or outgoing data (depends on `type`) + * @private + * @return {Object|Boolean} */ - executeHooks (type, socket, payload) { - let command = payload.cmd; + executeHooks(type, socket, payload) { + const command = payload.cmd; + let newPayload = payload; if (typeof this.hooks[type] !== 'undefined') { if (this.hooks[type].has(command)) { - let hooks = this.hooks[type].get(command); + const hooks = this.hooks[type].get(command); - for (let i = 0, j = hooks.length; i < j; i++) { + for (let i = 0, j = hooks.length; i < j; i += 1) { try { - payload = hooks[i].run(this.core, this, socket, payload); + newPayload = hooks[i].run(this.core, this, socket, newPayload); } catch (err) { - let errText = `Hook failure, '${type}', '${command}': `; + const errText = `Hook failure, '${type}', '${command}': `; if (this.core.config.logErrDetailed === true) { console.log(errText + err.stack); } else { @@ -450,23 +521,24 @@ class MainServer extends WsServer { } // A hook function may choose to return false to prevent all further processing - if (payload === false) { + if (newPayload === false) { return false; } } } } - return payload; + return newPayload; } /** * Wipe server hooks to make ready for module reload calls - * + * @public + * @return {void} */ - clearHooks () { + clearHooks() { this.hooks = {}; } } -module.exports = MainServer; +export default MainServer; -- cgit v1.2.1