aboutsummaryrefslogtreecommitdiffstats
path: root/server/src/serverLib/MainServer.js
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/serverLib/MainServer.js')
-rw-r--r--server/src/serverLib/MainServer.js376
1 files changed, 224 insertions, 152 deletions
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;