aboutsummaryrefslogtreecommitdiffstats
path: root/server/src/core
diff options
context:
space:
mode:
authorNeel Kamath <neelkamath@protonmail.com>2018-05-13 12:39:55 +0200
committerNeel Kamath <neelkamath@protonmail.com>2018-05-13 12:39:55 +0200
commit2bb5ced363b692a5696b176bc317fe525c0c05df (patch)
tree1532d9456c5f2b25ac05f7cec620a3af890eff83 /server/src/core
parentRe-add module documentation (diff)
downloadhackchat-2bb5ced363b692a5696b176bc317fe525c0c05df.tar.gz
hackchat-2bb5ced363b692a5696b176bc317fe525c0c05df.zip
Flatten
Diffstat (limited to 'server/src/core')
-rw-r--r--server/src/core/rateLimiter.js103
-rw-r--r--server/src/core/server.js282
2 files changed, 0 insertions, 385 deletions
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;