From 73e5c5ccfe408e58eb4bb927c71a77e0fceb22c4 Mon Sep 17 00:00:00 2001 From: neelkamath Date: Fri, 11 May 2018 20:46:12 +0530 Subject: Remove forgettable, unused, occasionally harmful (w.r.t. ES6) directive --- server/src/core/rateLimiter.js | 4 +--- server/src/core/server.js | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) (limited to 'server/src/core') diff --git a/server/src/core/rateLimiter.js b/server/src/core/rateLimiter.js index 0d94ef2..0c2a384 100644 --- a/server/src/core/rateLimiter.js +++ b/server/src/core/rateLimiter.js @@ -8,15 +8,13 @@ * */ -'use strict'; - class Police { /** * Create a ratelimiter instance. */ constructor () { this._records = {}; - this._halflife = 30000; // ms + this._halflife = 30 * 1000; // milliseconds this._threshold = 25; this._hashes = []; } diff --git a/server/src/core/server.js b/server/src/core/server.js index 9bea738..855aeba 100644 --- a/server/src/core/server.js +++ b/server/src/core/server.js @@ -7,8 +7,6 @@ * */ -'use strict'; - const wsServer = require('ws').Server; const socketReady = require('ws').OPEN; const crypto = require('crypto'); -- cgit v1.2.1 From 2bb5ced363b692a5696b176bc317fe525c0c05df Mon Sep 17 00:00:00 2001 From: Neel Kamath Date: Sun, 13 May 2018 16:09:55 +0530 Subject: Flatten --- server/src/core/rateLimiter.js | 103 --------------- server/src/core/server.js | 282 ----------------------------------------- 2 files changed, 385 deletions(-) delete mode 100644 server/src/core/rateLimiter.js delete mode 100644 server/src/core/server.js (limited to 'server/src/core') diff --git a/server/src/core/rateLimiter.js b/server/src/core/rateLimiter.js deleted file mode 100644 index 0c2a384..0000000 --- a/server/src/core/rateLimiter.js +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Tracks frequency of occurances based on `id` (remote address), then allows or - * denies command execution based on comparison with `threshold` - * - * Version: v2.0.0 - * Developer: Marzavec ( https://github.com/marzavec ) - * License: WTFPL ( http://www.wtfpl.net/txt/copying/ ) - * - */ - -class Police { - /** - * Create a ratelimiter instance. - */ - constructor () { - 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 - */ - search (id) { - let record = this._records[id]; - - if (!record) { - record = this._records[id] = { - time: Date.now(), - score: 0 - } - } - - return record; - } - - /** - * Adjusts the current ratelimit score by `deltaScore` - * - * @param {String} id target id / address - * @param {Number} deltaScore amount to adjust current score by - * @public - * - * @memberof Police - */ - frisk (id, deltaScore) { - let record = this.search(id); - - if (record.arrested) { - return true; - } - - record.score *= Math.pow(2, -(Date.now() - record.time ) / this._halflife); - record.score += deltaScore; - record.time = Date.now(); - - if (record.score >= this._threshold) { - return true; - } - - return false; - } - - /** - * 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; - } - - /** - * 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]; - } - - let record = this.search(id); - record.arrested = false; - } -} - -module.exports = Police; diff --git a/server/src/core/server.js b/server/src/core/server.js deleted file mode 100644 index 855aeba..0000000 --- a/server/src/core/server.js +++ /dev/null @@ -1,282 +0,0 @@ -/** - * 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/ ) - * - */ - -const wsServer = require('ws').Server; -const socketReady = require('ws').OPEN; -const crypto = require('crypto'); -const ipSalt = (Math.random().toString(36).substring(2, 16) + Math.random().toString(36).substring(2, (Math.random() * 16))).repeat(16); -const Police = require('./rateLimiter'); -const pulseSpeed = 16000; // ping all clients every X ms - -class server extends wsServer { - /** - * Create a HackChat server instance. - * - * @param {Object} core Reference to the global core object - */ - constructor (core) { - super({ port: core.config.websocketPort }); - - this._core = core; - this._police = new Police(); - this._cmdBlacklist = {}; - this._heartBeat = setInterval(((data) => { - this.beatHeart(); - }).bind(this), pulseSpeed); - - this.on('error', (err) => { - this.handleError('server', err); - }); - - this.on('connection', (socket, request) => { - this.newConnection(socket, request); - }); - } - - /** - * Send empty `ping` frame to each client - * - */ - beatHeart () { - let targetSockets = this.findSockets({}); - - if (targetSockets.length === 0) { - return; - } - - for (let i = 0, l = targetSockets.length; i < l; i++) { - try { - if (targetSockets[i].readyState === socketReady) { - targetSockets[i].ping(); - } - } catch (e) { } - } - } - - /** - * Bind listeners for the new socket created on connection to this class - * - * @param {Object} socket New socket object - * @param {Object} request Initial headers of the new connection - */ - newConnection (socket, request) { - socket.remoteAddress = request.headers['x-forwarded-for'] || request.connection.remoteAddress; - - socket.on('message', ((data) => { - this.handleData(socket, data); - }).bind(this)); - - socket.on('close', (() => { - this.handleClose(socket); - }).bind(this)); - - socket.on('error', ((err) => { - this.handleError(socket, err); - }).bind(this)); - } - - /** - * Handle incoming messages from clients, parse and check command, then hand-off - * - * @param {Object} socket Calling socket object - * @param {String} data Message sent from client - */ - handleData (socket, data) { - // Don't penalize yet, but check whether IP is rate-limited - if (this._police.frisk(socket.remoteAddress, 0)) { - this.reply({ cmd: 'warn', text: "Your IP is being rate-limited or blocked." }, socket); - - return; - } - - // Penalize here, but don't do anything about it - this._police.frisk(socket.remoteAddress, 1); - - // ignore ridiculously large packets - if (data.length > 65536) { - return; - } - - // Start sent data verification - var args = null; - try { - args = JSON.parse(data); - } catch (e) { - // Client sent malformed json, gtfo - socket.close(); - } - - if (args === null) { - return; - } - - if (typeof args.cmd === 'undefined' || args.cmd == 'ping') { - return; - } - - if (typeof args.cmd !== 'string') { - return; - } - - if (typeof socket.channel === 'undefined' && args.cmd !== 'join') { - return; - } - - if (typeof this._cmdBlacklist[args.cmd] === 'function') { - return; - } - - // Finished verification, pass to command modules - this._core.commands.handleCommand(this, socket, args); - } - - /** - * Handle socket close from clients - * - * @param {Object} socket Closing socket object - */ - handleClose (socket) { - this._core.commands.handleCommand(this, socket, { cmd: 'disconnect' }); - } - - /** - * "Handle" server or socket errors - * - * @param {Object||String} socket Calling socket object, or 'server' - * @param {String} err The sad stuff - */ - handleError (socket, err) { - console.log(`Server error: ${err}`); - } - - /** - * Send data payload to specific socket/client - * - * @param {Object} data Object to convert to json for transmission - * @param {Object} socket The target client - */ - send (data, socket) { - // Add timestamp to command - data.time = Date.now(); - - try { - if (socket.readyState === socketReady) { - socket.send(JSON.stringify(data)); - } - } catch (e) { } - } - - /** - * Overload function for `this.send()` - * - * @param {Object} data Object to convert to json for transmission - * @param {Object} socket The target client - */ - reply (data, socket) { - this.send(data, socket); - } - - /** - * Finds sockets/clients that meet the filter requirements, then passes the data to them - * - * @param {Object} data Object to convert to json for transmission - * @param {Object} filter see `this.findSockets()` - */ - broadcast (data, filter) { - let targetSockets = this.findSockets(filter); - - if (targetSockets.length === 0) { - return false; - } - - for (let i = 0, l = targetSockets.length; i < l; i++) { - this.send(data, targetSockets[i]); - } - - return true; - } - - /** - * 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') - */ - findSockets (filter) { - let filterAttribs = Object.keys(filter); - let reqCount = filterAttribs.length; - let curMatch; - let matches = []; - for ( let socket of this.clients ) { - curMatch = 0; - - for (let i = 0; i < reqCount; i++) { - if (typeof socket[filterAttribs[i]] !== 'undefined') { - 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++; - } - } - break; - } - - case 'function': { - if (filter[filterAttribs[i]](socket[filterAttribs[i]])) { - curMatch++; - } - break; - } - - default: { - if (socket[filterAttribs[i]] === filter[filterAttribs[i]]) { - curMatch++; - } - break; - } - } - } - } - - if (curMatch === reqCount) { - matches.push(socket); - } - } - - return matches; - } - - /** - * Encrypts 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 - */ - getSocketHash (target) { - let sha = crypto.createHash('sha256'); - - if (typeof target === 'string') { - sha.update(target + ipSalt); - } else { - sha.update(target.remoteAddress + ipSalt); - } - - return sha.digest('base64').substr(0, 15); - } -} - -module.exports = server; -- cgit v1.2.1 From 949404cd1aad8492ae0338130f16054adfa38ab7 Mon Sep 17 00:00:00 2001 From: Neel Kamath Date: Sun, 13 May 2018 16:37:56 +0530 Subject: Prevent fucking shit up --- server/src/core/rateLimiter.js | 103 +++++++++++++++ server/src/core/server.js | 282 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 385 insertions(+) create mode 100644 server/src/core/rateLimiter.js create mode 100644 server/src/core/server.js (limited to 'server/src/core') diff --git a/server/src/core/rateLimiter.js b/server/src/core/rateLimiter.js new file mode 100644 index 0000000..0c2a384 --- /dev/null +++ b/server/src/core/rateLimiter.js @@ -0,0 +1,103 @@ +/** + * Tracks frequency of occurances based on `id` (remote address), then allows or + * denies command execution based on comparison with `threshold` + * + * Version: v2.0.0 + * Developer: Marzavec ( https://github.com/marzavec ) + * License: WTFPL ( http://www.wtfpl.net/txt/copying/ ) + * + */ + +class Police { + /** + * Create a ratelimiter instance. + */ + constructor () { + 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 + */ + search (id) { + let record = this._records[id]; + + if (!record) { + record = this._records[id] = { + time: Date.now(), + score: 0 + } + } + + return record; + } + + /** + * Adjusts the current ratelimit score by `deltaScore` + * + * @param {String} id target id / address + * @param {Number} deltaScore amount to adjust current score by + * @public + * + * @memberof Police + */ + frisk (id, deltaScore) { + let record = this.search(id); + + if (record.arrested) { + return true; + } + + record.score *= Math.pow(2, -(Date.now() - record.time ) / this._halflife); + record.score += deltaScore; + record.time = Date.now(); + + if (record.score >= this._threshold) { + return true; + } + + return false; + } + + /** + * 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; + } + + /** + * 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]; + } + + let record = this.search(id); + record.arrested = false; + } +} + +module.exports = Police; diff --git a/server/src/core/server.js b/server/src/core/server.js new file mode 100644 index 0000000..855aeba --- /dev/null +++ b/server/src/core/server.js @@ -0,0 +1,282 @@ +/** + * 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/ ) + * + */ + +const wsServer = require('ws').Server; +const socketReady = require('ws').OPEN; +const crypto = require('crypto'); +const ipSalt = (Math.random().toString(36).substring(2, 16) + Math.random().toString(36).substring(2, (Math.random() * 16))).repeat(16); +const Police = require('./rateLimiter'); +const pulseSpeed = 16000; // ping all clients every X ms + +class server extends wsServer { + /** + * Create a HackChat server instance. + * + * @param {Object} core Reference to the global core object + */ + constructor (core) { + super({ port: core.config.websocketPort }); + + this._core = core; + this._police = new Police(); + this._cmdBlacklist = {}; + this._heartBeat = setInterval(((data) => { + this.beatHeart(); + }).bind(this), pulseSpeed); + + this.on('error', (err) => { + this.handleError('server', err); + }); + + this.on('connection', (socket, request) => { + this.newConnection(socket, request); + }); + } + + /** + * Send empty `ping` frame to each client + * + */ + beatHeart () { + let targetSockets = this.findSockets({}); + + if (targetSockets.length === 0) { + return; + } + + for (let i = 0, l = targetSockets.length; i < l; i++) { + try { + if (targetSockets[i].readyState === socketReady) { + targetSockets[i].ping(); + } + } catch (e) { } + } + } + + /** + * Bind listeners for the new socket created on connection to this class + * + * @param {Object} socket New socket object + * @param {Object} request Initial headers of the new connection + */ + newConnection (socket, request) { + socket.remoteAddress = request.headers['x-forwarded-for'] || request.connection.remoteAddress; + + socket.on('message', ((data) => { + this.handleData(socket, data); + }).bind(this)); + + socket.on('close', (() => { + this.handleClose(socket); + }).bind(this)); + + socket.on('error', ((err) => { + this.handleError(socket, err); + }).bind(this)); + } + + /** + * Handle incoming messages from clients, parse and check command, then hand-off + * + * @param {Object} socket Calling socket object + * @param {String} data Message sent from client + */ + handleData (socket, data) { + // Don't penalize yet, but check whether IP is rate-limited + if (this._police.frisk(socket.remoteAddress, 0)) { + this.reply({ cmd: 'warn', text: "Your IP is being rate-limited or blocked." }, socket); + + return; + } + + // Penalize here, but don't do anything about it + this._police.frisk(socket.remoteAddress, 1); + + // ignore ridiculously large packets + if (data.length > 65536) { + return; + } + + // Start sent data verification + var args = null; + try { + args = JSON.parse(data); + } catch (e) { + // Client sent malformed json, gtfo + socket.close(); + } + + if (args === null) { + return; + } + + if (typeof args.cmd === 'undefined' || args.cmd == 'ping') { + return; + } + + if (typeof args.cmd !== 'string') { + return; + } + + if (typeof socket.channel === 'undefined' && args.cmd !== 'join') { + return; + } + + if (typeof this._cmdBlacklist[args.cmd] === 'function') { + return; + } + + // Finished verification, pass to command modules + this._core.commands.handleCommand(this, socket, args); + } + + /** + * Handle socket close from clients + * + * @param {Object} socket Closing socket object + */ + handleClose (socket) { + this._core.commands.handleCommand(this, socket, { cmd: 'disconnect' }); + } + + /** + * "Handle" server or socket errors + * + * @param {Object||String} socket Calling socket object, or 'server' + * @param {String} err The sad stuff + */ + handleError (socket, err) { + console.log(`Server error: ${err}`); + } + + /** + * Send data payload to specific socket/client + * + * @param {Object} data Object to convert to json for transmission + * @param {Object} socket The target client + */ + send (data, socket) { + // Add timestamp to command + data.time = Date.now(); + + try { + if (socket.readyState === socketReady) { + socket.send(JSON.stringify(data)); + } + } catch (e) { } + } + + /** + * Overload function for `this.send()` + * + * @param {Object} data Object to convert to json for transmission + * @param {Object} socket The target client + */ + reply (data, socket) { + this.send(data, socket); + } + + /** + * Finds sockets/clients that meet the filter requirements, then passes the data to them + * + * @param {Object} data Object to convert to json for transmission + * @param {Object} filter see `this.findSockets()` + */ + broadcast (data, filter) { + let targetSockets = this.findSockets(filter); + + if (targetSockets.length === 0) { + return false; + } + + for (let i = 0, l = targetSockets.length; i < l; i++) { + this.send(data, targetSockets[i]); + } + + return true; + } + + /** + * 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') + */ + findSockets (filter) { + let filterAttribs = Object.keys(filter); + let reqCount = filterAttribs.length; + let curMatch; + let matches = []; + for ( let socket of this.clients ) { + curMatch = 0; + + for (let i = 0; i < reqCount; i++) { + if (typeof socket[filterAttribs[i]] !== 'undefined') { + 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++; + } + } + break; + } + + case 'function': { + if (filter[filterAttribs[i]](socket[filterAttribs[i]])) { + curMatch++; + } + break; + } + + default: { + if (socket[filterAttribs[i]] === filter[filterAttribs[i]]) { + curMatch++; + } + break; + } + } + } + } + + if (curMatch === reqCount) { + matches.push(socket); + } + } + + return matches; + } + + /** + * Encrypts 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 + */ + getSocketHash (target) { + let sha = crypto.createHash('sha256'); + + if (typeof target === 'string') { + sha.update(target + ipSalt); + } else { + sha.update(target.remoteAddress + ipSalt); + } + + return sha.digest('base64').substr(0, 15); + } +} + +module.exports = server; -- cgit v1.2.1