diff options
Diffstat (limited to '')
43 files changed, 1209 insertions, 974 deletions
diff --git a/server/main.js b/server/main.js index 70042e8..b01d09d 100644 --- a/server/main.js +++ b/server/main.js @@ -1,15 +1,12 @@ /** * HackChat main server entry point - * - * Version: v2.0.0 - * Developer: Marzavec ( https://github.com/marzavec ) - * License: WTFPL ( http://www.wtfpl.net/txt/copying/ ) - * + * @author Marzavec ( https://github.com/marzavec ) + * @version v2.0.0 + * @license WTFPL ( http://www.wtfpl.net/txt/copying/ ) */ -'use strict'; - // import and initialize the core application -const CoreApp = require('./src/serverLib/CoreApp'); +import { CoreApp } from './src/serverLib/CoreApp'; + const coreApp = new CoreApp(); coreApp.init(); diff --git a/server/package-lock.json b/server/package-lock.json index 27205bc..26fd58b 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -18,9 +18,9 @@ "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" }, "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" }, "balanced-match": { "version": "1.0.0", @@ -104,6 +104,11 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, + "esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" + }, "eyes": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", @@ -350,11 +355,11 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", - "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.0.tgz", + "integrity": "sha512-+SqNqFbwTm/0DC18KYzIsMTnEWpLwJsiasW/O17la4iDRRIO9uaHbvKiAS3AHgTiuuWerK/brj4O6MYZkei9xg==", "requires": { - "async-limiter": "~1.0.0" + "async-limiter": "^1.0.0" } } } diff --git a/server/package.json b/server/package.json index 51a149d..3eb733c 100644 --- a/server/package.json +++ b/server/package.json @@ -12,8 +12,8 @@ "npm": ">= 5.7.1" }, "scripts": { - "start": "node main.js", - "config": "node src/scripts/configure.js" + "start": "node -r esm main.js", + "config": "node -r esm src/scripts/configure.js" }, "author": "Marzavec", "license": "WTFPL", @@ -22,9 +22,10 @@ "common-tags": "^1.8.0", "dateformat": "^3.0.3", "didyoumean2": "^2.0.2", + "esm": "^3.2.25", "fs-extra": "^7.0.1", "prompt": "^1.0.0", "readdir-recursive": "0.0.4", - "ws": "^6.1.4" + "ws": "^7.2.0" } } diff --git a/server/src/commands/admin/addmod.js b/server/src/commands/admin/addmod.js index 6853a42..26cec40 100644 --- a/server/src/commands/admin/addmod.js +++ b/server/src/commands/admin/addmod.js @@ -3,26 +3,26 @@ */ // module main -exports.run = async (core, server, socket, data) => { +export async function run(core, server, socket, data) { // increase rate limit chance and ignore if not admin - if (socket.uType != 'admin') { - return server.police.frisk(socket.remoteAddress, 20); + if (socket.uType !== 'admin') { + return server.police.frisk(socket.address, 20); } // add new trip to config core.config.mods.push({ trip: data.trip }); // find targets current connections - let newMod = server.findSockets({ trip: data.trip }); + const newMod = server.findSockets({ trip: data.trip }); if (newMod.length !== 0) { - for (let i = 0, l = newMod.length; i < l; i++) { + for (let i = 0, l = newMod.length; i < l; i += 1) { // upgrade privilages newMod[i].uType = 'mod'; // inform new mod server.send({ cmd: 'info', - text: 'You are now a mod.' + text: 'You are now a mod.', }, newMod[i]); } } @@ -30,21 +30,22 @@ exports.run = async (core, server, socket, data) => { // return success message server.reply({ cmd: 'info', - text: `Added mod trip: ${data.trip}, remember to run 'saveconfig' to make it permanent` + text: `Added mod trip: ${data.trip}, remember to run 'saveconfig' to make it permanent`, }, socket); // notify all mods server.broadcast({ cmd: 'info', - text: `Added mod: ${data.trip}` + text: `Added mod: ${data.trip}`, }, { uType: 'mod' }); -}; -// module meta -exports.requiredData = ['trip']; -exports.info = { + return true; +} + +export const requiredData = ['trip']; +export const info = { name: 'addmod', description: 'Adds target trip to the config as a mod and upgrades the socket type', usage: ` - API: { cmd: 'addmod', trip: '<target trip>' }` + API: { cmd: 'addmod', trip: '<target trip>' }`, }; diff --git a/server/src/commands/admin/listusers.js b/server/src/commands/admin/listusers.js index 5ff350f..0b0199f 100644 --- a/server/src/commands/admin/listusers.js +++ b/server/src/commands/admin/listusers.js @@ -3,46 +3,47 @@ */ // module main -exports.run = async (core, server, socket, data) => { +export async function run(core, server, socket) { // increase rate limit chance and ignore if not admin - if (socket.uType != 'admin') { - return server.police.frisk(socket.remoteAddress, 20); + if (socket.uType !== 'admin') { + return server.police.frisk(socket.address, 20); } // find all users currently in a channel - let currentUsers = server.findSockets({ - channel: (channel) => true + const currentUsers = server.findSockets({ + channel: (channel) => true, }); // compile channel and user list - let channels = {}; - for (let i = 0, j = currentUsers.length; i < j; i++) { + const channels = {}; + for (let i = 0, j = currentUsers.length; i < j; i += 1) { if (typeof channels[currentUsers[i].channel] === 'undefined') { channels[currentUsers[i].channel] = []; } - + channels[currentUsers[i].channel].push( - `[${currentUsers[i].trip||'null'}]${currentUsers[i].nick}` + `[${currentUsers[i].trip || 'null'}]${currentUsers[i].nick}`, ); } // build output - let lines = []; - for (let channel in channels) { - lines.push(`?${channel} ${channels[channel].join(", ")}`); + const lines = []; + for (const channel in channels) { + lines.push(`?${channel} ${channels[channel].join(', ')}`); } // send reply server.reply({ cmd: 'info', - text: lines.join("\n") + text: lines.join('\n'), }, socket); -}; -// module meta -exports.info = { + return true; +} + +export const info = { name: 'listusers', description: 'Outputs all current channels and sockets in those channels', usage: ` - API: { cmd: 'listusers' }` + API: { cmd: 'listusers' }`, }; diff --git a/server/src/commands/admin/reload.js b/server/src/commands/admin/reload.js index 588d1c5..206e2ca 100644 --- a/server/src/commands/admin/reload.js +++ b/server/src/commands/admin/reload.js @@ -3,10 +3,10 @@ */ // module main -exports.run = async (core, server, socket, data) => { +export async function run(core, server, socket, data) { // increase rate limit chance and ignore if not admin - if (socket.uType != 'admin') { - return server.police.frisk(socket.remoteAddress, 20); + if (socket.uType !== 'admin') { + return server.police.frisk(socket.address, 20); } // do command reload and store results @@ -17,7 +17,7 @@ exports.run = async (core, server, socket, data) => { server.loadHooks(); // build reply based on reload results - if (loadResult == '') { + if (loadResult === '') { loadResult = `Reloaded ${core.commands.commands.length} commands, 0 errors`; } else { loadResult = `Reloaded ${core.commands.commands.length} commands, error(s): @@ -31,20 +31,21 @@ exports.run = async (core, server, socket, data) => { // reply with results server.reply({ cmd: 'info', - text: loadResult + text: loadResult, }, socket); // notify mods of reload #transparency server.broadcast({ cmd: 'info', - text: loadResult + text: loadResult, }, { uType: 'mod' }); -}; -// module meta -exports.info = { + return true; +} + +export const info = { name: 'reload', description: '(Re)loads any new commands into memory, outputs errors if any', usage: ` - API: { cmd: 'reload', reason: '<optional reason append>' }` + API: { cmd: 'reload', reason: '<optional reason append>' }`, }; diff --git a/server/src/commands/admin/removemod.js b/server/src/commands/admin/removemod.js index a2d862c..9190dd6 100644 --- a/server/src/commands/admin/removemod.js +++ b/server/src/commands/admin/removemod.js @@ -3,26 +3,26 @@ */ // module main -exports.run = async (core, server, socket, data) => { +export async function run(core, server, socket, data) { // increase rate limit chance and ignore if not admin - if (socket.uType != 'admin') { - return server.police.frisk(socket.remoteAddress, 20); + if (socket.uType !== 'admin') { + return server.police.frisk(socket.address, 20); } // remove trip from config - core.config.mods = core.config.mods.filter(mod => mod.trip !== data.trip); + core.config.mods = core.config.mods.filter((mod) => mod.trip !== data.trip); // find targets current connections - let targetMod = server.findSockets({ trip: data.trip }); + const targetMod = server.findSockets({ trip: data.trip }); if (targetMod.length !== 0) { - for (let i = 0, l = targetMod.length; i < l; i++) { + for (let i = 0, l = targetMod.length; i < l; i += 1) { // downgrade privilages targetMod[i].uType = 'user'; // inform ex-mod server.send({ cmd: 'info', - text: 'You are now a user.' + text: 'You are now a user.', }, targetMod[i]); } } @@ -32,21 +32,22 @@ exports.run = async (core, server, socket, data) => { cmd: 'info', text: `Removed mod trip: ${ data.trip - }, remember to run 'saveconfig' to make it permanent` + }, remember to run 'saveconfig' to make it permanent`, }, socket); // notify all mods server.broadcast({ cmd: 'info', - text: `Removed mod: ${data.trip}` + text: `Removed mod: ${data.trip}`, }, { uType: 'mod' }); -}; -// module meta -exports.requiredData = ['trip']; -exports.info = { + return true; +} + +export const requiredData = ['trip']; +export const info = { name: 'removemod', description: 'Removes target trip from the config as a mod and downgrades the socket type', usage: ` - API: { cmd: 'removemod', trip: '<target trip>' }` + API: { cmd: 'removemod', trip: '<target trip>' }`, }; diff --git a/server/src/commands/admin/saveconfig.js b/server/src/commands/admin/saveconfig.js index 65fff0e..6c713b4 100644 --- a/server/src/commands/admin/saveconfig.js +++ b/server/src/commands/admin/saveconfig.js @@ -3,37 +3,38 @@ */ // module main -exports.run = async (core, server, socket, data) => { +export async function run(core, server, socket) { // increase rate limit chance and ignore if not admin - if (socket.uType != 'admin') { - return server.police.frisk(socket.remoteAddress, 20); + if (socket.uType !== 'admin') { + return server.police.frisk(socket.address, 20); } // attempt save, notify of failure if (!core.configManager.save()) { return server.reply({ cmd: 'warn', - text: 'Failed to save config, check logs.' - }, client); + text: 'Failed to save config, check logs.', + }, socket); } // return success message server.reply({ cmd: 'info', - text: 'Config saved!' + text: 'Config saved!', }, socket); // notify mods #transparency server.broadcast({ cmd: 'info', - text: 'Config saved!' + text: 'Config saved!', }, { uType: 'mod' }); -}; -// module meta -exports.info = { + return true; +} + +export const info = { name: 'saveconfig', description: 'Writes the current config to disk', usage: ` - API: { cmd: 'saveconfig' }` + API: { cmd: 'saveconfig' }`, }; diff --git a/server/src/commands/admin/shout.js b/server/src/commands/admin/shout.js index 821a22a..73b0734 100644 --- a/server/src/commands/admin/shout.js +++ b/server/src/commands/admin/shout.js @@ -3,24 +3,25 @@ */ // module main -exports.run = async (core, server, socket, data) => { +export async function run(core, server, socket, data) { // increase rate limit chance and ignore if not admin - if (socket.uType != 'admin') { - return server.police.frisk(socket.remoteAddress, 20); + if (socket.uType !== 'admin') { + return server.police.frisk(socket.address, 20); } // send text to all channels server.broadcast({ cmd: 'info', - text: `Server Notice: ${data.text}` + text: `Server Notice: ${data.text}`, }, {}); -}; -// module meta -exports.requiredData = ['text']; -exports.info = { + return true; +} + +export const requiredData = ['text']; +export const info = { name: 'shout', description: 'Displays passed text to every client connected', usage: ` - API: { cmd: 'shout', text: '<shout text>' }` + API: { cmd: 'shout', text: '<shout text>' }`, }; diff --git a/server/src/commands/core/changenick.js b/server/src/commands/core/changenick.js index cb6d1d0..632da83 100644 --- a/server/src/commands/core/changenick.js +++ b/server/src/commands/core/changenick.js @@ -6,43 +6,43 @@ const verifyNickname = (nick) => /^[a-zA-Z0-9_]{1,24}$/.test(nick); // module main -exports.run = async (core, server, socket, data) => { - if (server.police.frisk(socket.remoteAddress, 6)) { +export async function run(core, server, socket, data) { + if (server.police.frisk(socket.address, 6)) { return server.reply({ cmd: 'warn', - text: 'You are changing nicknames too fast. Wait a moment before trying again.' + text: 'You are changing nicknames too fast. Wait a moment before trying again.', }, socket); } // verify user data is string if (typeof data.nick !== 'string') { - return; + return true; } // make sure requested nickname meets standards - let newNick = data.nick.trim(); + const newNick = data.nick.trim(); if (!verifyNickname(newNick)) { return server.reply({ cmd: 'warn', - text: 'Nickname must consist of up to 24 letters, numbers, and underscores' + text: 'Nickname must consist of up to 24 letters, numbers, and underscores', }, socket); } // prevent admin impersonation // TODO: prevent mod impersonation - if (newNick.toLowerCase() == core.config.adminName.toLowerCase()) { - server.police.frisk(socket.remoteAddress, 4); + if (newNick.toLowerCase() === core.config.adminName.toLowerCase()) { + server.police.frisk(socket.address, 4); return server.reply({ cmd: 'warn', - text: 'You are not the admin, liar!' + text: 'You are not the admin, liar!', }, socket); } // find any sockets that have the same nickname - let userExists = server.findSockets({ + const userExists = server.findSockets({ channel: socket.channel, - nick: (targetNick) => targetNick.toLowerCase() === newNick.toLowerCase() + nick: (targetNick) => targetNick.toLowerCase() === newNick.toLowerCase(), }); // return error if found @@ -50,82 +50,83 @@ exports.run = async (core, server, socket, data) => { // That nickname is already in that channel return server.reply({ cmd: 'warn', - text: 'Nickname taken' + text: 'Nickname taken', }, socket); } // build join and leave notices // TODO: this is a legacy client holdover, name changes in the future will // have thieir own event - let leaveNotice = { + const leaveNotice = { cmd: 'onlineRemove', - nick: socket.nick + nick: socket.nick, }; - let joinNotice = { + const joinNotice = { cmd: 'onlineAdd', nick: newNick, trip: socket.trip || 'null', - hash: socket.hash + hash: socket.hash, }; // broadcast remove event and join event with new name, this is to support legacy clients and bots - server.broadcast( leaveNotice, { channel: socket.channel }); - server.broadcast( joinNotice, { channel: socket.channel }); + server.broadcast(leaveNotice, { channel: socket.channel }); + server.broadcast(joinNotice, { channel: socket.channel }); // notify channel that the user has changed their name - server.broadcast( { + server.broadcast({ cmd: 'info', - text: `${socket.nick} is now ${newNick}` + text: `${socket.nick} is now ${newNick}`, }, { channel: socket.channel }); // commit change to nickname socket.nick = newNick; -}; + + return true; +} // module hook functions -exports.initHooks = (server) => { +export function initHooks(server) { server.registerHook('in', 'chat', this.nickCheck, 29); -}; +} // hooks chat commands checking for /nick -exports.nickCheck = (core, server, socket, payload) => { +export function nickCheck(core, server, socket, payload) { if (typeof payload.text !== 'string') { return false; } if (payload.text.startsWith('/nick')) { - let input = payload.text.split(' '); + const input = payload.text.split(' '); // If there is no nickname target parameter if (input[1] === undefined) { server.reply({ cmd: 'warn', - text: 'Refer to `/help nick` for instructions on how to use this command.' + text: 'Refer to `/help nick` for instructions on how to use this command.', }, socket); return false; } - let newNick = input[1].replace(/@/g, ''); + const newNick = input[1].replace(/@/g, ''); this.run(core, server, socket, { cmd: 'changenick', - nick: newNick + nick: newNick, }); return false; } return payload; -}; +} -// module meta -exports.requiredData = ['nick']; -exports.info = { +export const requiredData = ['nick']; +export const info = { name: 'changenick', description: 'This will change your current connections nickname', usage: ` API: { cmd: 'changenick', nick: '<new nickname>' } - Text: /nick <new nickname>` + Text: /nick <new nickname>`, }; diff --git a/server/src/commands/core/chat.js b/server/src/commands/core/chat.js index 7b7e79e..8d0098b 100644 --- a/server/src/commands/core/chat.js +++ b/server/src/commands/core/chat.js @@ -9,43 +9,45 @@ const parseText = (text) => { return false; } + let sanitizedText = text; + // strip newlines from beginning and end - text = text.replace(/^\s*\n|^\s+$|\n\s*$/g, ''); + sanitizedText = sanitizedText.replace(/^\s*\n|^\s+$|\n\s*$/g, ''); // replace 3+ newlines with just 2 newlines - text = text.replace(/\n{3,}/g, "\n\n"); + sanitizedText = sanitizedText.replace(/\n{3,}/g, '\n\n'); - return text; + return sanitizedText; }; // module main -exports.run = async (core, server, socket, data) => { +export async function run(core, server, socket, data) { // check user input - let text = parseText(data.text); + const text = parseText(data.text); if (!text) { // lets not send objects or empty text, yea? - return server.police.frisk(socket.remoteAddress, 13); + return server.police.frisk(socket.address, 13); } // check for spam - let score = text.length / 83 / 4; - if (server.police.frisk(socket.remoteAddress, score)) { + const score = text.length / 83 / 4; + if (server.police.frisk(socket.address, score)) { return server.reply({ cmd: 'warn', - text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.' + text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.', }, socket); } // build chat payload - let payload = { + const payload = { cmd: 'chat', nick: socket.nick, - text: text + text, }; - if (socket.uType == 'admin') { + if (socket.uType === 'admin') { payload.admin = true; - } else if (socket.uType == 'mod') { + } else if (socket.uType === 'mod') { payload.mod = true; } @@ -54,20 +56,22 @@ exports.run = async (core, server, socket, data) => { } // broadcast to channel peers - server.broadcast( payload, { channel: socket.channel}); + server.broadcast(payload, { channel: socket.channel }); // stats are fun core.stats.increment('messages-sent'); -}; + + return true; +} // module hook functions -exports.initHooks = (server) => { +export function initHooks(server) { server.registerHook('in', 'chat', this.commandCheckIn, 20); server.registerHook('in', 'chat', this.finalCmdCheck, 254); -}; +} // checks for miscellaneous '/' based commands -exports.commandCheckIn = (core, server, socket, payload) => { +export function commandCheckIn(core, server, socket, payload) { if (typeof payload.text !== 'string') { return false; } @@ -75,16 +79,16 @@ exports.commandCheckIn = (core, server, socket, payload) => { if (payload.text.startsWith('/myhash')) { server.reply({ cmd: 'info', - text: `Your hash: ${socket.hash}` + text: `Your hash: ${socket.hash}`, }, socket); return false; } return payload; -}; +} -exports.finalCmdCheck = (core, server, socket, payload) => { +export function finalCmdCheck(core, server, socket, payload) { if (typeof payload.text !== 'string') { return false; } @@ -97,26 +101,23 @@ exports.finalCmdCheck = (core, server, socket, payload) => { payload.text = payload.text.substr(1); return payload; - } else { - server.reply({ - cmd: 'warn', - text: `Unknown command: ${payload.text}` - }, socket); - - return false; } - return payload; -}; + server.reply({ + cmd: 'warn', + text: `Unknown command: ${payload.text}`, + }, socket); + + return false; +} -// module meta -exports.requiredData = ['text']; -exports.info = { +export const requiredData = ['text']; +export const info = { name: 'chat', description: 'Broadcasts passed `text` field to the calling users channel', usage: ` API: { cmd: 'chat', text: '<text to send>' } Text: Uuuuhm. Just kind type in that little box at the bottom and hit enter.\n Bonus super secret hidden commands: - /myhash` + /myhash`, }; diff --git a/server/src/commands/core/emote.js b/server/src/commands/core/emote.js index b0203cc..f244d74 100644 --- a/server/src/commands/core/emote.js +++ b/server/src/commands/core/emote.js @@ -9,91 +9,94 @@ const parseText = (text) => { return false; } + let sanitizedText = text; + // strip newlines from beginning and end - text = text.replace(/^\s*\n|^\s+$|\n\s*$/g, ''); + sanitizedText = sanitizedText.replace(/^\s*\n|^\s+$|\n\s*$/g, ''); // replace 3+ newlines with just 2 newlines - text = text.replace(/\n{3,}/g, "\n\n"); + sanitizedText = sanitizedText.replace(/\n{3,}/g, '\n\n'); - return text; + return sanitizedText; }; // module main -exports.run = async (core, server, socket, payload) => { +export async function run(core, server, socket, payload) { // check user input - let text = parseText(payload.text); + const text = parseText(payload.text); if (!text) { // lets not send objects or empty text, yea? - return server.police.frisk(socket.remoteAddress, 8); + return server.police.frisk(socket.address, 8); } // check for spam - let score = text.length / 83 / 4; - if (server.police.frisk(socket.remoteAddress, score)) { + const score = text.length / 83 / 4; + if (server.police.frisk(socket.address, score)) { return server.reply({ cmd: 'warn', - text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.' + text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.', }, socket); } - let newPayload = { + const newPayload = { cmd: 'info', type: 'emote', nick: socket.nick, - text: `@${socket.nick} ${text}` + text: `@${socket.nick} ${text}`, }; if (socket.trip) { newPayload.trip = socket.trip; } // broadcast to channel peers - server.broadcast( newPayload, { channel: socket.channel}); -}; + server.broadcast(newPayload, { channel: socket.channel }); + + return true; +} // module hook functions -exports.initHooks = (server) => { +export function initHooks(server) { server.registerHook('in', 'chat', this.emoteCheck, 30); -}; +} // hooks chat commands checking for /me -exports.emoteCheck = (core, server, socket, payload) => { +export function emoteCheck(core, server, socket, payload) { if (typeof payload.text !== 'string') { return false; } if (payload.text.startsWith('/me ')) { - let input = payload.text.split(' '); + const input = payload.text.split(' '); // If there is no emote target parameter if (input[1] === undefined) { server.reply({ cmd: 'warn', - text: 'Refer to `/help emote` for instructions on how to use this command.' + text: 'Refer to `/help emote` for instructions on how to use this command.', }, socket); return false; } input.splice(0, 1); - let actionText = input.join(' '); + const actionText = input.join(' '); this.run(core, server, socket, { cmd: 'emote', - text: actionText + text: actionText, }); return false; } return payload; -}; +} -// module meta -exports.requiredData = ['text']; -exports.info = { +export const requiredData = ['text']; +export const info = { name: 'emote', description: 'Typical emote / action text', usage: ` API: { cmd: 'emote', text: '<emote/action text>' } - Text: /me <emote/action text>` + Text: /me <emote/action text>`, }; diff --git a/server/src/commands/core/help.js b/server/src/commands/core/help.js index 8eccdb6..25f8844 100644 --- a/server/src/commands/core/help.js +++ b/server/src/commands/core/help.js @@ -3,21 +3,21 @@ */ // module support functions -const stripIndents = require('common-tags').stripIndents; +const { stripIndents } = require('common-tags'); // module main -exports.run = async (core, server, socket, payload) => { +export async function run(core, server, socket, payload) { // check for spam - if (server.police.frisk(socket.remoteAddress, 2)) { + if (server.police.frisk(socket.address, 2)) { return server.reply({ cmd: 'warn', - text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.' + text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.', }, socket); } // verify user input if (typeof payload.command !== 'undefined' && typeof payload.command !== 'string') { - return; + return true; } let reply = ''; @@ -27,21 +27,21 @@ exports.run = async (core, server, socket, payload) => { API: {cmd: 'help', command: '<command name>'}`; reply += '\n\n-------------------------------------\n\n'; - let categories = core.commands.categoriesList.sort(); - for (let i = 0, j = categories.length; i < j; i++) { - reply += `${categories[i].replace('../src/commands/', '').replace(/^\w/, c => c.toUpperCase())} Commands:\n`; - let catCommands = core.commands.all(categories[i]).sort((a, b) => a.info.name.localeCompare(b.info.name)); - reply += ` ${catCommands.map(c => `${c.info.name}`).join(', ')}\n\n`; + const categories = core.commands.categoriesList.sort(); + for (let i = 0, j = categories.length; i < j; i += 1) { + reply += `${categories[i].replace('../src/commands/', '').replace(/^\w/, (c) => c.toUpperCase())} Commands:\n`; + const catCommands = core.commands.all(categories[i]).sort((a, b) => a.info.name.localeCompare(b.info.name)); + reply += ` ${catCommands.map((c) => `${c.info.name}`).join(', ')}\n\n`; } } else { - let command = core.commands.get(payload.command); + const command = core.commands.get(payload.command); if (typeof command === 'undefined') { reply = 'Unknown command'; } else { reply = stripIndents`Name: ${command.info.name} Aliases: ${typeof command.info.aliases !== 'undefined' ? command.info.aliases.join(', ') : 'None'} - Category: ${command.info.category.replace('../src/commands/', '').replace(/^\w/, c => c.toUpperCase())} + Category: ${command.info.category.replace('../src/commands/', '').replace(/^\w/, (c) => c.toUpperCase())} Required Parameters: ${command.requiredData || 'None'}\n Description: ${command.info.description || '¯\_(ツ)_/¯'}\n Usage: ${command.info.usage || command.info.name}`; @@ -51,40 +51,41 @@ exports.run = async (core, server, socket, payload) => { // output reply server.reply({ cmd: 'info', - text: reply + text: reply, }, socket); -}; + + return true; +} // module hook functions -exports.initHooks = (server) => { +export function initHooks(server) { server.registerHook('in', 'chat', this.helpCheck, 28); -}; +} // hooks chat commands checking for /whisper -exports.helpCheck = (core, server, socket, payload) => { +export function helpCheck(core, server, socket, payload) { if (typeof payload.text !== 'string') { return false; } if (payload.text.startsWith('/help')) { - let input = payload.text.substr(1).split(' ', 2); + const input = payload.text.substr(1).split(' ', 2); this.run(core, server, socket, { cmd: input[0], - command: input[1] + command: input[1], }); return false; } return payload; -}; +} -// module meta -exports.info = { +export const info = { name: 'help', description: 'Outputs information about the servers current protocol', usage: ` API: { cmd: 'help', command: '<optional command name>' } - Text: /help <optional command name>` + Text: /help <optional command name>`, }; diff --git a/server/src/commands/core/invite.js b/server/src/commands/core/invite.js index 70393b1..1811e8b 100644 --- a/server/src/commands/core/invite.js +++ b/server/src/commands/core/invite.js @@ -6,47 +6,47 @@ const verifyNickname = (nick) => /^[a-zA-Z0-9_]{1,24}$/.test(nick); // module main -exports.run = async (core, server, socket, data) => { +export async function run(core, server, socket, data) { // check for spam - if (server.police.frisk(socket.remoteAddress, 2)) { + if (server.police.frisk(socket.address, 2)) { return server.reply({ cmd: 'warn', - text: 'You are sending invites too fast. Wait a moment before trying again.' + text: 'You are sending invites too fast. Wait a moment before trying again.', }, socket); } // verify user input if (typeof data.nick !== 'string' || !verifyNickname(data.nick)) { - return; + return true; } // why would you invite yourself? - if (data.nick == socket.nick) { - return; + if (data.nick === socket.nick) { + return true; } // generate common channel - let channel = Math.random().toString(36).substr(2, 8); + const channel = Math.random().toString(36).substr(2, 8); // build and send invite - let payload = { + const payload = { cmd: 'info', type: 'invite', from: socket.nick, invite: channel, - text: `${socket.nick} invited you to ?${channel}` + text: `${socket.nick} invited you to ?${channel}`, }; - let inviteSent = server.broadcast( payload, { + const inviteSent = server.broadcast(payload, { channel: socket.channel, - nick: data.nick + nick: data.nick, }); // server indicates the user was not found if (!inviteSent) { return server.reply({ cmd: 'warn', - text: 'Could not find user in channel' + text: 'Could not find user in channel', }, socket); } @@ -55,18 +55,19 @@ exports.run = async (core, server, socket, data) => { cmd: 'info', type: 'invite', invite: channel, - text: `You invited ${data.nick} to ?${channel}` + text: `You invited ${data.nick} to ?${channel}`, }, socket); // stats are fun core.stats.increment('invites-sent'); -}; -// module meta -exports.requiredData = ['nick']; -exports.info = { + return true; +} + +export const requiredData = ['nick']; +export const info = { name: 'invite', description: 'Generates a unique (more or less) room name and passes it to two clients', usage: ` - API: { cmd: 'invite', nick: '<target nickname>' }` + API: { cmd: 'invite', nick: '<target nickname>' }`, }; diff --git a/server/src/commands/core/join.js b/server/src/commands/core/join.js index 965a8bc..644470e 100644 --- a/server/src/commands/core/join.js +++ b/server/src/commands/core/join.js @@ -6,7 +6,7 @@ const crypto = require('crypto'); const hash = (password) => { - let sha = crypto.createHash('sha256'); + const sha = crypto.createHash('sha256'); sha.update(password); return sha.digest('base64').substr(0, 6); }; @@ -15,15 +15,15 @@ const verifyNickname = (nick) => /^[a-zA-Z0-9_]{1,24}$/.test(nick); // exposed "login" function to allow hooks to verify user join events // returns object containing user info or string if error -exports.parseNickname = (core, data) => { - let userInfo = { +export function parseNickname(core, data) { + const userInfo = { nick: '', uType: 'user', trip: null, }; // seperate nick from password - let nickArray = data.nick.split('#', 2); + const nickArray = data.nick.split('#', 2); userInfo.nick = nickArray[0].trim(); if (!verifyNickname(userInfo.nick)) { @@ -31,90 +31,92 @@ exports.parseNickname = (core, data) => { return 'Nickname must consist of up to 24 letters, numbers, and underscores'; } - let password = nickArray[1]; + const password = nickArray[1]; if (hash(password + core.config.tripSalt) === core.config.adminTrip) { userInfo.uType = 'admin'; userInfo.trip = 'Admin'; - } else if (userInfo.nick.toLowerCase() == core.config.adminName.toLowerCase()) { // they've got the main-admin name while not being an admin + } else if (userInfo.nick.toLowerCase() === core.config.adminName.toLowerCase()) { + // they've got the main-admin name while not being an admin return 'You are not the admin, liar!'; } else if (password) { userInfo.trip = hash(password + core.config.tripSalt); } // TODO: disallow moderator impersonation - for (let mod of core.config.mods) { + // for (const mod of core.config.mods) { + core.config.mods.forEach((mod) => { if (userInfo.trip === mod.trip) { userInfo.uType = 'mod'; } - } + }); return userInfo; -}; +} // module main -exports.run = async (core, server, socket, data) => { +export async function run(core, server, socket, data) { // check for spam - if (server.police.frisk(socket.remoteAddress, 3)) { + if (server.police.frisk(socket.address, 3)) { return server.reply({ cmd: 'warn', - text: 'You are joining channels too fast. Wait a moment and try again.' + text: 'You are joining channels too fast. Wait a moment and try again.', }, socket); } // calling socket already in a channel if (typeof socket.channel !== 'undefined') { - return; + return true; } // check user input if (typeof data.channel !== 'string' || typeof data.nick !== 'string') { - return; + return true; } - let channel = data.channel.trim(); + const channel = data.channel.trim(); if (!channel) { // must join a non-blank channel - return; + return true; } - let userInfo = this.parseNickname(core, data); + const userInfo = this.parseNickname(core, data); if (typeof userInfo === 'string') { return server.reply({ cmd: 'warn', - text: userInfo + text: userInfo, }, socket); } // check if the nickname already exists in the channel - let userExists = server.findSockets({ + const userExists = server.findSockets({ channel: data.channel, - nick: (targetNick) => targetNick.toLowerCase() === userInfo.nick.toLowerCase() + nick: (targetNick) => targetNick.toLowerCase() === userInfo.nick.toLowerCase(), }); if (userExists.length > 0) { // that nickname is already in that channel return server.reply({ cmd: 'warn', - text: 'Nickname taken' + text: 'Nickname taken', }, socket); } userInfo.userHash = server.getSocketHash(socket); // prepare to notify channel peers - let newPeerList = server.findSockets({ channel: data.channel }); - let nicks = []; + const newPeerList = server.findSockets({ channel: data.channel }); + const nicks = []; - let joinAnnouncement = { + const joinAnnouncement = { cmd: 'onlineAdd', nick: userInfo.nick, trip: userInfo.trip || 'null', - hash: userInfo.userHash + hash: userInfo.userHash, }; // send join announcement and prep online set - for (let i = 0, l = newPeerList.length; i < l; i++) { + for (let i = 0, l = newPeerList.length; i < l; i += 1) { server.reply(joinAnnouncement, newPeerList[i]); nicks.push(newPeerList[i].nick); } @@ -131,18 +133,19 @@ exports.run = async (core, server, socket, data) => { // reply with channel peer list server.reply({ cmd: 'onlineSet', - nicks: nicks + nicks, }, socket); // stats are fun core.stats.increment('users-joined'); -}; -// module meta -exports.requiredData = ['channel', 'nick']; -exports.info = { + return true; +} + +export const requiredData = ['channel', 'nick']; +export const info = { name: 'join', description: 'Place calling socket into target channel with target nick & broadcast event to channel', usage: ` - API: { cmd: 'join', nick: '<your nickname>', channel: '<target channel>' }` + API: { cmd: 'join', nick: '<your nickname>', channel: '<target channel>' }`, }; diff --git a/server/src/commands/core/morestats.js b/server/src/commands/core/morestats.js index a71729c..3cbf8f3 100644 --- a/server/src/commands/core/morestats.js +++ b/server/src/commands/core/morestats.js @@ -3,37 +3,38 @@ */ // module support functions -const stripIndents = require('common-tags').stripIndents; +const { stripIndents } = require('common-tags'); const formatTime = (time) => { let seconds = time[0] + time[1] / 1e9; let minutes = Math.floor(seconds / 60); - seconds = seconds % 60; + seconds %= 60; let hours = Math.floor(minutes / 60); - minutes = minutes % 60; + minutes %= 60; - let days = Math.floor(hours / 24); - hours = hours % 24; + const days = Math.floor(hours / 24); + hours %= 24; return `${days.toFixed(0)}d ${hours.toFixed(0)}h ${minutes.toFixed(0)}m ${seconds.toFixed(0)}s`; }; // module main -exports.run = async (core, server, socket, data) => { +export async function run(core, server, socket) { // gather connection and channel count let ips = {}; let channels = {}; - for (let client of server.clients) { + // for (const client of server.clients) { + this.clients.forEach((client) => { if (client.channel) { channels[client.channel] = true; - ips[client.remoteAddress] = true; + ips[client.address] = true; } - } + }); - let uniqueClientCount = Object.keys(ips).length; - let uniqueChannels = Object.keys(channels).length; + const uniqueClientCount = Object.keys(ips).length; + const uniqueChannels = Object.keys(channels).length; ips = null; channels = null; @@ -49,40 +50,39 @@ exports.run = async (core, server, socket, data) => { users-banned: ${(core.stats.get('users-banned') || 0)} users-kicked: ${(core.stats.get('users-kicked') || 0)} stats-requested: ${(core.stats.get('stats-requested') || 0)} - server-uptime: ${formatTime(process.hrtime(core.stats.get('start-time')))}` + server-uptime: ${formatTime(process.hrtime(core.stats.get('start-time')))}`, }, socket); // stats are fun core.stats.increment('stats-requested'); -}; +} // module hook functions -exports.initHooks = (server) => { +export function initHooks(server) { server.registerHook('in', 'chat', this.statsCheck, 26); -}; +} // hooks chat commands checking for /stats -exports.statsCheck = (core, server, socket, payload) => { +export function statsCheck(core, server, socket, payload) { if (typeof payload.text !== 'string') { return false; } if (payload.text.startsWith('/stats')) { this.run(core, server, socket, { - cmd: 'morestats' + cmd: 'morestats', }); return false; } return payload; -}; +} -// module meta -exports.info = { +export const info = { name: 'morestats', description: 'Sends back current server stats to the calling client', usage: ` API: { cmd: 'morestats' } - Text: /stats` + Text: /stats`, }; diff --git a/server/src/commands/core/move.js b/server/src/commands/core/move.js index 8e97a06..7eda88c 100644 --- a/server/src/commands/core/move.js +++ b/server/src/commands/core/move.js @@ -3,67 +3,67 @@ */ // module main -exports.run = async (core, server, socket, data) => { +export async function run(core, server, socket, data) { // check for spam - if (server.police.frisk(socket.remoteAddress, 6)) { + if (server.police.frisk(socket.address, 6)) { return server.reply({ cmd: 'warn', - text: 'You are changing channels too fast. Wait a moment before trying again.' + text: 'You are changing channels too fast. Wait a moment before trying again.', }, socket); } // check user input if (typeof data.channel !== 'string') { - return; + return true; } if (data.channel === socket.channel) { // they are trying to rejoin the channel - return; + return true; } // check that the nickname isn't already in target channel const currentNick = socket.nick.toLowerCase(); - let userExists = server.findSockets({ + const userExists = server.findSockets({ channel: data.channel, - nick: (targetNick) => targetNick.toLowerCase() === currentNick + nick: (targetNick) => targetNick.toLowerCase() === currentNick, }); if (userExists.length > 0) { // That nickname is already in that channel - return; + return true; } // broadcast leave notice to peers - let peerList = server.findSockets({ channel: socket.channel }); + const peerList = server.findSockets({ channel: socket.channel }); if (peerList.length > 1) { - for (let i = 0, l = peerList.length; i < l; i++) { + for (let i = 0, l = peerList.length; i < l; i += 1) { server.reply({ cmd: 'onlineRemove', - nick: peerList[i].nick + nick: peerList[i].nick, }, socket); - if (socket.nick !== peerList[i].nick){ + if (socket.nick !== peerList[i].nick) { server.reply({ cmd: 'onlineRemove', - nick: socket.nick + nick: socket.nick, }, peerList[i]); } } } // broadcast join notice to new peers - let newPeerList = server.findSockets({ channel: data.channel }); - let moveAnnouncement = { + const newPeerList = server.findSockets({ channel: data.channel }); + const moveAnnouncement = { cmd: 'onlineAdd', nick: socket.nick, trip: socket.trip || 'null', - hash: socket.hash + hash: socket.hash, }; - let nicks = []; + const nicks = []; - for (let i = 0, l = newPeerList.length; i < l; i++) { + for (let i = 0, l = newPeerList.length; i < l; i += 1) { server.reply(moveAnnouncement, newPeerList[i]); nicks.push(newPeerList[i].nick); } @@ -73,18 +73,19 @@ exports.run = async (core, server, socket, data) => { // reply with new user list server.reply({ cmd: 'onlineSet', - nicks: nicks + nicks, }, socket); // commit change socket.channel = data.channel; -}; -// module meta -exports.requiredData = ['channel']; -exports.info = { + return true; +} + +export const requiredData = ['channel']; +export const info = { name: 'move', description: 'This will change your current channel to the new one provided', usage: ` - API: { cmd: 'move', channel: '<target channel>' }` + API: { cmd: 'move', channel: '<target channel>' }`, }; diff --git a/server/src/commands/core/ping.js b/server/src/commands/core/ping.js index 1e710e5..7d8623d 100644 --- a/server/src/commands/core/ping.js +++ b/server/src/commands/core/ping.js @@ -3,12 +3,11 @@ */ // module main -exports.run = async (core, server, socket, data) => { - return; -}; +export async function run() { + +} -// module meta -exports.info = { +export const info = { name: 'ping', - description: 'This module is only in place to supress error notices legacy sources may get' + description: 'This module is only in place to supress error notices legacy sources may get', }; diff --git a/server/src/commands/core/stats.js b/server/src/commands/core/stats.js index ff4b1ef..8badd5b 100644 --- a/server/src/commands/core/stats.js +++ b/server/src/commands/core/stats.js @@ -3,19 +3,20 @@ */ // module main -exports.run = async (core, server, socket, data) => { +export async function run(core, server, socket) { // gather connection and channel count let ips = {}; let channels = {}; - for (let client of server.clients) { + // for (const client of server.clients) { + this.clients.forEach((client) => { if (client.channel) { channels[client.channel] = true; - ips[client.remoteAddress] = true; + ips[client.address] = true; } - } + }); - let uniqueClientCount = Object.keys(ips).length; - let uniqueChannels = Object.keys(channels).length; + const uniqueClientCount = Object.keys(ips).length; + const uniqueChannels = Object.keys(channels).length; ips = null; channels = null; @@ -23,17 +24,16 @@ exports.run = async (core, server, socket, data) => { // dispatch info server.reply({ cmd: 'info', - text: `${uniqueClientCount} unique IPs in ${uniqueChannels} channels` + text: `${uniqueClientCount} unique IPs in ${uniqueChannels} channels`, }, socket); // stats are fun core.stats.increment('stats-requested'); -}; +} -// module meta -exports.info = { +export const info = { name: 'stats', description: 'Sends back legacy server stats to the calling client', usage: ` - API: { cmd: 'stats' }` + API: { cmd: 'stats' }`, }; diff --git a/server/src/commands/core/whisper.js b/server/src/commands/core/whisper.js index 109889d..1cfa157 100644 --- a/server/src/commands/core/whisper.js +++ b/server/src/commands/core/whisper.js @@ -11,36 +11,38 @@ const parseText = (text) => { return false; } + let sanitizedText = text; + // strip newlines from beginning and end - text = text.replace(/^\s*\n|^\s+$|\n\s*$/g, ''); + sanitizedText = sanitizedText.replace(/^\s*\n|^\s+$|\n\s*$/g, ''); // replace 3+ newlines with just 2 newlines - text = text.replace(/\n{3,}/g, "\n\n"); + sanitizedText = sanitizedText.replace(/\n{3,}/g, '\n\n'); - return text; + return sanitizedText; }; // module main -exports.run = async (core, server, socket, payload) => { +export async function run(core, server, socket, payload) { // check user input - let text = parseText(payload.text); + const text = parseText(payload.text); if (!text) { // lets not send objects or empty text, yea? - return server.police.frisk(socket.remoteAddress, 13); + return server.police.frisk(socket.address, 13); } // check for spam - let score = text.length / 83 / 4; - if (server.police.frisk(socket.remoteAddress, score)) { + const score = text.length / 83 / 4; + if (server.police.frisk(socket.address, score)) { return server.reply({ cmd: 'warn', - text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.' + text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.', }, socket); } - let targetNick = payload.nick; + const targetNick = payload.nick; if (!verifyNickname(targetNick)) { - return; + return true; } // find target user @@ -49,18 +51,18 @@ exports.run = async (core, server, socket, payload) => { if (targetClient.length === 0) { return server.reply({ cmd: 'warn', - text: 'Could not find user in channel' + text: 'Could not find user in channel', }, socket); } - targetClient = targetClient[0]; + [targetClient] = targetClient; server.reply({ cmd: 'info', type: 'whisper', from: socket.nick, trip: socket.trip || 'null', - text: `${socket.nick} whispered: ${text}` + text: `${socket.nick} whispered: ${text}`, }, targetClient); targetClient.whisperReply = socket.nick; @@ -68,42 +70,44 @@ exports.run = async (core, server, socket, payload) => { server.reply({ cmd: 'info', type: 'whisper', - text: `You whispered to @${targetNick}: ${text}` + text: `You whispered to @${targetNick}: ${text}`, }, socket); -}; + + return true; +} // module hook functions -exports.initHooks = (server) => { +export function initHooks(server) { server.registerHook('in', 'chat', this.whisperCheck, 20); -}; +} // hooks chat commands checking for /whisper -exports.whisperCheck = (core, server, socket, payload) => { +export function whisperCheck(core, server, socket, payload) { if (typeof payload.text !== 'string') { return false; } if (payload.text.startsWith('/whisper')) { - let input = payload.text.split(' '); + const input = payload.text.split(' '); // If there is no nickname target parameter if (input[1] === undefined) { server.reply({ cmd: 'warn', - text: 'Refer to `/help whisper` for instructions on how to use this command.' + text: 'Refer to `/help whisper` for instructions on how to use this command.', }, socket); return false; } - let target = input[1].replace(/@/g, ''); + const target = input[1].replace(/@/g, ''); input.splice(0, 2); - let whisperText = input.join(' '); + const whisperText = input.join(' '); this.run(core, server, socket, { cmd: 'whisper', nick: target, - text: whisperText + text: whisperText, }); return false; @@ -113,35 +117,34 @@ exports.whisperCheck = (core, server, socket, payload) => { if (typeof socket.whisperReply === 'undefined') { server.reply({ cmd: 'warn', - text: 'Cannot reply to nobody' + text: 'Cannot reply to nobody', }, socket); return false; } - let input = payload.text.split(' '); + const input = payload.text.split(' '); input.splice(0, 1); - let whisperText = input.join(' '); + const whisperText = input.join(' '); this.run(core, server, socket, { cmd: 'whisper', nick: socket.whisperReply, - text: whisperText + text: whisperText, }); return false; } return payload; -}; +} -// module meta -exports.requiredData = ['nick', 'text']; -exports.info = { +export const requiredData = ['nick', 'text']; +export const info = { name: 'whisper', description: 'Display text on targets screen that only they can see', usage: ` API: { cmd: 'whisper', nick: '<target name>', text: '<text to whisper>' } Text: /whisper <target name> <text to whisper> - Alt Text: /r <text to whisper, this will auto reply to the last person who whispered to you>` + Alt Text: /r <text to whisper, this will auto reply to the last person who whispered to you>`, }; diff --git a/server/src/commands/internal/disconnect.js b/server/src/commands/internal/disconnect.js index 520f8cb..07a125e 100644 --- a/server/src/commands/internal/disconnect.js +++ b/server/src/commands/internal/disconnect.js @@ -4,28 +4,29 @@ */ // module main -exports.run = async (core, server, socket, data) => { +export async function run(core, server, socket, data) { if (data.cmdKey !== server.cmdKey) { // internal command attempt by client, increase rate limit chance and ignore - return server.police.frisk(socket.remoteAddress, 20); + return server.police.frisk(socket.address, 20); } // send leave notice to client peers if (socket.channel) { server.broadcast({ cmd: 'onlineRemove', - nick: socket.nick + nick: socket.nick, }, { channel: socket.channel }); } // commit close just in case socket.terminate(); -}; -// module meta -exports.requiredData = ['cmdKey']; -exports.info = { + return true; +} + +export const requiredData = ['cmdKey']; +export const info = { name: 'disconnect', usage: 'Internal Use Only', - description: 'Internally used to relay `onlineRemove` event to clients' + description: 'Internally used to relay `onlineRemove` event to clients', }; diff --git a/server/src/commands/internal/legacylayer.js b/server/src/commands/internal/legacylayer.js index 50f5fd7..c0daa13 100644 --- a/server/src/commands/internal/legacylayer.js +++ b/server/src/commands/internal/legacylayer.js @@ -3,18 +3,19 @@ */ // module main -exports.run = async (core, server, socket, data) => { - return; -}; +export async function run(core, server, socket, data) { + /** + * @todo + */ +} // module hook functions -exports.initHooks = (server) => { +export function initHooks(server) { // module is only a placeholder - //server.registerHook('out', '', this.); -}; + // server.registerHook('out', '', this.); +} -// module meta -exports.info = { +export const info = { name: 'legacylayer', - description: 'This module adjusts outgoing data, making it compatible with legacy clients' + description: 'This module adjusts outgoing data, making it compatible with legacy clients', }; diff --git a/server/src/commands/internal/socketreply.js b/server/src/commands/internal/socketreply.js index 5dadaf6..1ba8df2 100644 --- a/server/src/commands/internal/socketreply.js +++ b/server/src/commands/internal/socketreply.js @@ -3,20 +3,21 @@ */ // module main -exports.run = async (core, server, socket, data) => { +export async function run(core, server, socket, data) { if (data.cmdKey !== server.cmdKey) { // internal command attempt by client, increase rate limit chance and ignore - return server.police.frisk(socket.remoteAddress, 20); + return server.police.frisk(socket.address, 20); } // send warning to target socket server.reply({ cmd: 'warn', text: data.text }, socket); -}; -// module meta -exports.requiredData = ['cmdKey', 'text']; -exports.info = { + return true; +} + +export const requiredData = ['cmdKey', 'text']; +export const info = { name: 'socketreply', usage: 'Internal Use Only', - description: 'Internally used to relay warnings to clients' + description: 'Internally used to relay warnings to clients', }; diff --git a/server/src/commands/mod/ban.js b/server/src/commands/mod/ban.js index 9c8eb4f..dd5f01e 100644 --- a/server/src/commands/mod/ban.js +++ b/server/src/commands/mod/ban.js @@ -3,53 +3,53 @@ */ // module main -exports.run = async (core, server, socket, data) => { +export async function run(core, server, socket, data) { // increase rate limit chance and ignore if not admin or mod if (socket.uType === 'user') { - return server.police.frisk(socket.remoteAddress, 10); + return server.police.frisk(socket.address, 10); } // check user input if (typeof data.nick !== 'string') { - return; + return true; } // find target user - let targetNick = data.nick; + const targetNick = data.nick; let badClient = server.findSockets({ channel: socket.channel, nick: targetNick }); if (badClient.length === 0) { return server.reply({ cmd: 'warn', - text: 'Could not find user in channel' + text: 'Could not find user in channel', }, socket); } - badClient = badClient[0]; + [badClient] = badClient; // i guess banning mods or admins isn't the best idea? if (badClient.uType !== 'user') { return server.reply({ cmd: 'warn', - text: 'Cannot ban other mods, how rude' + text: 'Cannot ban other mods, how rude', }, socket); } // commit arrest record - server.police.arrest(badClient.remoteAddress, badClient.hash); + server.police.arrest(badClient.address, badClient.hash); console.log(`${socket.nick} [${socket.trip}] banned ${targetNick} in ${socket.channel}`); // notify normal users server.broadcast({ cmd: 'info', - text: `Banned ${targetNick}` + text: `Banned ${targetNick}`, }, { channel: socket.channel, uType: 'user' }); // notify mods server.broadcast({ cmd: 'info', - text: `${socket.nick} banned ${targetNick} in ${socket.channel}, userhash: ${badClient.hash}` + text: `${socket.nick} banned ${targetNick} in ${socket.channel}, userhash: ${badClient.hash}`, }, { uType: 'mod' }); // force connection closed @@ -57,13 +57,14 @@ exports.run = async (core, server, socket, data) => { // stats are fun core.stats.increment('users-banned'); -}; -// module meta -exports.requiredData = ['nick']; -exports.info = { + return true; +} + +export const requiredData = ['nick']; +export const info = { name: 'ban', description: 'Disconnects the target nickname in the same channel as calling socket & adds to ratelimiter', usage: ` - API: { cmd: 'ban', nick: '<target nickname>' }` + API: { cmd: 'ban', nick: '<target nickname>' }`, }; diff --git a/server/src/commands/mod/dumb.js b/server/src/commands/mod/dumb.js index d5e8fee..644bd4f 100644 --- a/server/src/commands/mod/dumb.js +++ b/server/src/commands/mod/dumb.js @@ -4,22 +4,22 @@ */ // module constructor -exports.init = (core) => { +export function init(core) { if (typeof core.muzzledHashes === 'undefined') { core.muzzledHashes = {}; } -}; +} // module main -exports.run = async (core, server, socket, data) => { +export async function run(core, server, socket, data) { // increase rate limit chance and ignore if not admin or mod if (socket.uType === 'user') { - return server.police.frisk(socket.remoteAddress, 10); + return server.police.frisk(socket.address, 10); } // check user input if (typeof data.nick !== 'string') { - return; + return true; } // find target user @@ -28,56 +28,58 @@ exports.run = async (core, server, socket, data) => { if (badClient.length === 0) { return server.reply({ cmd: 'warn', - text: 'Could not find user in channel' + text: 'Could not find user in channel', }, socket); } - badClient = badClient[0]; + [badClient] = badClient; // likely dont need this, muting mods and admins is fine if (badClient.uType !== 'user') { return server.reply({ cmd: 'warn', - text: 'This trick wont work on mods and admin' + text: 'This trick wont work on mods and admin', }, socket); } // store hash in mute list - let record = core.muzzledHashes[badClient.hash] = { - dumb: true - } + const record = core.muzzledHashes[badClient.hash] = { + dumb: true, + }; // store allies if needed - if(data.allies && Array.isArray(data.allies)){ - record.allies = data.allies; + if (data.allies && Array.isArray(data.allies)) { + record.allies = data.allies; } // notify mods server.broadcast({ cmd: 'info', - text: `${socket.nick} muzzled ${data.nick} in ${socket.channel}, userhash: ${badClient.hash}` + text: `${socket.nick} muzzled ${data.nick} in ${socket.channel}, userhash: ${badClient.hash}`, }, { uType: 'mod' }); -}; + + return true; +} // module hook functions -exports.initHooks = (server) => { +export function initHooks(server) { server.registerHook('in', 'chat', this.chatCheck, 25); server.registerHook('in', 'invite', this.inviteCheck, 25); // TODO: add whisper hook, need hook priorities todo finished first -}; +} // hook incoming chat commands, shadow-prevent chat if they are muzzled -exports.chatCheck = (core, server, socket, payload) => { +export function chatCheck(core, server, socket, payload) { if (typeof payload.text !== 'string') { return false; } - if(core.muzzledHashes[socket.hash]){ + if (core.muzzledHashes[socket.hash]) { // build fake chat payload - mutedPayload = { + const mutedPayload = { cmd: 'chat', nick: socket.nick, - text: payload.text + text: payload.text, }; if (socket.trip) { @@ -85,50 +87,59 @@ exports.chatCheck = (core, server, socket, payload) => { } // broadcast to any duplicate connections in channel - server.broadcast( mutedPayload, { channel: socket.channel, hash: socket.hash }); + server.broadcast(mutedPayload, { channel: socket.channel, hash: socket.hash }); // broadcast to allies, if any - if(core.muzzledHashes[socket.hash].allies){ - server.broadcast( mutedPayload, { channel: socket.channel, nick: core.muzzledHashes[socket.hash].allies }); + if (core.muzzledHashes[socket.hash].allies) { + server.broadcast( + mutedPayload, + { + channel: socket.channel, + nick: core.muzzledHashes[socket.hash].allies, + }, + ); } - // blanket "spam" protection, may expose the ratelimiting lines from `chat` and use that, TODO: one day #lazydev - server.police.frisk(socket.remoteAddress, 9); + /** + * Blanket "spam" protection. + * May expose the ratelimiting lines from `chat` and use that + * @todo one day #lazydev + */ + server.police.frisk(socket.address, 9); return false; } return payload; -}; +} // shadow-prevent all invites from muzzled users -exports.inviteCheck = (core, server, socket, payload) => { +export function inviteCheck(core, server, socket, payload) { if (typeof payload.nick !== 'string') { return false; } - if(core.muzzledHashes[socket.hash]){ + if (core.muzzledHashes[socket.hash]) { // generate common channel - let channel = Math.random().toString(36).substr(2, 8); + const channel = Math.random().toString(36).substr(2, 8); // send fake reply server.reply({ cmd: 'info', - text: `You invited ${payload.nick} to ?${channel}` + text: `You invited ${payload.nick} to ?${channel}`, }, socket); return false; } return payload; -}; +} -// module meta -exports.requiredData = ['nick']; -exports.info = { +export const requiredData = ['nick']; +export const info = { name: 'dumb', description: 'Globally shadow mute a connection. Optional allies array will see muted messages.', usage: ` - API: { cmd: 'dumb', nick: '<target nick>', allies: ['<optional nick array>', ...] }` + API: { cmd: 'dumb', nick: '<target nick>', allies: ['<optional nick array>', ...] }`, }; -exports.info.aliases = ['muzzle', 'mute']; +info.aliases = ['muzzle', 'mute']; diff --git a/server/src/commands/mod/kick.js b/server/src/commands/mod/kick.js index f3bc7ca..cb01d5c 100644 --- a/server/src/commands/mod/kick.js +++ b/server/src/commands/mod/kick.js @@ -3,37 +3,37 @@ */ // module main -exports.run = async (core, server, socket, data) => { +export async function run(core, server, socket, data) { // increase rate limit chance and ignore if not admin or mod if (socket.uType === 'user') { - return server.police.frisk(socket.remoteAddress, 10); + return server.police.frisk(socket.address, 10); } // check user input if (typeof data.nick !== 'string') { if (typeof data.nick !== 'object' && !Array.isArray(data.nick)) { - return; + return true; } } // find target user(s) - let badClients = server.findSockets({ channel: socket.channel, nick: data.nick }); + const badClients = server.findSockets({ channel: socket.channel, nick: data.nick }); if (badClients.length === 0) { return server.reply({ cmd: 'warn', - text: 'Could not find user(s) in channel' + text: 'Could not find user(s) in channel', }, socket); } // check if found targets are kickable, commit kick let newChannel = ''; - let kicked = []; - for (let i = 0, j = badClients.length; i < j; i++) { + const kicked = []; + for (let i = 0, j = badClients.length; i < j; i += 1) { if (badClients[i].uType !== 'user') { server.reply({ cmd: 'warn', - text: 'Cannot kick other mods, how rude' + text: 'Cannot kick other mods, how rude', }, socket); } else { newChannel = Math.random().toString(36).substr(2, 8); @@ -42,7 +42,7 @@ exports.run = async (core, server, socket, data) => { // inform mods with where they were sent server.broadcast({ cmd: 'info', - text: `${badClients[i].nick} was banished to ?${newChannel}` + text: `${badClients[i].nick} was banished to ?${newChannel}`, }, { channel: socket.channel, uType: 'mod' }); kicked.push(badClients[i].nick); @@ -51,32 +51,33 @@ exports.run = async (core, server, socket, data) => { } if (kicked.length === 0) { - return; + return true; } // broadcast client leave event - for (let i = 0, j = kicked.length; i < j; i++) { + for (let i = 0, j = kicked.length; i < j; i += 1) { server.broadcast({ cmd: 'onlineRemove', - nick: kicked[i] + nick: kicked[i], }, { channel: socket.channel }); } // publicly broadcast kick event server.broadcast({ cmd: 'info', - text: `Kicked ${kicked.join(', ')}` + text: `Kicked ${kicked.join(', ')}`, }, { channel: socket.channel, uType: 'user' }); // stats are fun core.stats.increment('users-kicked', kicked.length); -}; -// module meta -exports.requiredData = ['nick']; -exports.info = { + return true; +} + +export const requiredData = ['nick']; +export const info = { name: 'kick', description: 'Silently forces target client(s) into another channel. `nick` may be string or array of strings', usage: ` - API: { cmd: 'kick', nick: '<target nick>' }` + API: { cmd: 'kick', nick: '<target nick>' }`, }; diff --git a/server/src/commands/mod/moveuser.js b/server/src/commands/mod/moveuser.js index c7fc4bf..b55c207 100644 --- a/server/src/commands/mod/moveuser.js +++ b/server/src/commands/mod/moveuser.js @@ -3,79 +3,79 @@ */ // module main -exports.run = async (core, server, socket, data) => { +export async function run(core, server, socket, data) { // increase rate limit chance and ignore if not admin or mod if (socket.uType === 'user') { - return server.police.frisk(socket.remoteAddress, 10); + return server.police.frisk(socket.address, 10); } // check user input if (typeof data.nick !== 'string' || typeof data.channel !== 'string') { - return; + return true; } if (data.channel === socket.channel) { // moving them into the same channel? y u do this? - return; + return true; } - let badClients = server.findSockets({ channel: socket.channel, nick: data.nick }); + const badClients = server.findSockets({ channel: socket.channel, nick: data.nick }); if (badClients.length === 0) { return server.reply({ cmd: 'warn', - text: 'Could not find user in channel' + text: 'Could not find user in channel', }, socket); } - let badClient = badClients[0]; + const badClient = badClients[0]; if (badClient.uType !== 'user') { return server.reply({ cmd: 'warn', - text: 'Cannot move other mods, how rude' + text: 'Cannot move other mods, how rude', }, socket); } const currentNick = badClient.nick.toLowerCase(); - let userExists = server.findSockets({ + const userExists = server.findSockets({ channel: data.channel, - nick: (targetNick) => targetNick.toLowerCase() === currentNick + nick: (targetNick) => targetNick.toLowerCase() === currentNick, }); if (userExists.length > 0) { // That nickname is already in that channel - return; + return true; } - let peerList = server.findSockets({ channel: socket.channel }); + const peerList = server.findSockets({ channel: socket.channel }); if (peerList.length > 1) { - for (let i = 0, l = peerList.length; i < l; i++) { + for (let i = 0, l = peerList.length; i < l; i += 1) { server.reply({ cmd: 'onlineRemove', - nick: peerList[i].nick + nick: peerList[i].nick, }, badClient); - if (badClient.nick !== peerList[i].nick){ + if (badClient.nick !== peerList[i].nick) { server.reply({ cmd: 'onlineRemove', - nick: badClient.nick + nick: badClient.nick, }, peerList[i]); } } } - let newPeerList = server.findSockets({ channel: data.channel }); - let moveAnnouncement = { + const newPeerList = server.findSockets({ channel: data.channel }); + const moveAnnouncement = { cmd: 'onlineAdd', nick: badClient.nick, trip: badClient.trip || 'null', - hash: server.getSocketHash(badClient) + hash: server.getSocketHash(badClient), }; - let nicks = []; + const nicks = []; - for (let i = 0, l = newPeerList.length; i < l; i++) { + for (let i = 0, l = newPeerList.length; i < l; i += 1) { server.reply(moveAnnouncement, newPeerList[i]); nicks.push(newPeerList[i].nick); } @@ -84,22 +84,23 @@ exports.run = async (core, server, socket, data) => { server.reply({ cmd: 'onlineSet', - nicks: nicks + nicks, }, badClient); badClient.channel = data.channel; - server.broadcast( { + server.broadcast({ cmd: 'info', - text: `${badClient.nick} was moved into ?${data.channel}` + text: `${badClient.nick} was moved into ?${data.channel}`, }, { channel: data.channel }); -}; -// module meta -exports.requiredData = ['nick', 'channel']; -exports.info = { + return true; +} + +export const requiredData = ['nick', 'channel']; +export const info = { name: 'moveuser', description: 'This will move the target user nick into another channel', usage: ` - API: { cmd: 'moveuser', nick: '<target nick>', channel: '<new channel>' }` + API: { cmd: 'moveuser', nick: '<target nick>', channel: '<new channel>' }`, }; diff --git a/server/src/commands/mod/speak.js b/server/src/commands/mod/speak.js index 23fc4de..5514545 100644 --- a/server/src/commands/mod/speak.js +++ b/server/src/commands/mod/speak.js @@ -3,32 +3,32 @@ * Author: simple */ - // module constructor - exports.init = (core) => { - if (typeof core.muzzledHashes === 'undefined') { - core.muzzledHashes = {}; - } - }; +// module constructor +export function init(core) { + if (typeof core.muzzledHashes === 'undefined') { + core.muzzledHashes = {}; + } +} // module main -exports.run = async (core, server, socket, data) => { +export async function run(core, server, socket, data) { // increase rate limit chance and ignore if not admin or mod if (socket.uType === 'user') { - return server.police.frisk(socket.remoteAddress, 10); + return server.police.frisk(socket.address, 10); } // check user input if (typeof data.ip !== 'string' && typeof data.hash !== 'string') { return server.reply({ cmd: 'warn', - text: "hash:'targethash' or ip:'1.2.3.4' is required" + text: "hash:'targethash' or ip:'1.2.3.4' is required", }, socket); } // find target & remove mute status let target; if (typeof data.ip === 'string') { - target = getSocketHash(data.ip); + target = server.getSocketHash(data.ip); } else { target = data.hash; } @@ -38,15 +38,16 @@ exports.run = async (core, server, socket, data) => { // notify mods server.broadcast({ cmd: 'info', - text: `${socket.nick} unmuzzled : ${target}` + text: `${socket.nick} unmuzzled : ${target}`, }, { uType: 'mod' }); -}; -// module meta -exports.info = { + return true; +} + +export const info = { name: 'speak', description: 'Pardon a dumb user to be able to speak again', usage: ` - API: { cmd: 'speak', ip/hash: '<target ip or hash' }` + API: { cmd: 'speak', ip/hash: '<target ip or hash' }`, }; -exports.info.aliases = ['unmuzzle', 'unmute']; +info.aliases = ['unmuzzle', 'unmute']; diff --git a/server/src/commands/mod/unban.js b/server/src/commands/mod/unban.js index 6744d9d..0d1e469 100644 --- a/server/src/commands/mod/unban.js +++ b/server/src/commands/mod/unban.js @@ -3,22 +3,23 @@ */ // module main -exports.run = async (core, server, socket, data) => { +export async function run(core, server, socket, data) { // increase rate limit chance and ignore if not admin or mod if (socket.uType === 'user') { - return server.police.frisk(socket.remoteAddress, 10); + return server.police.frisk(socket.address, 10); } // check user input if (typeof data.ip !== 'string' && typeof data.hash !== 'string') { return server.reply({ cmd: 'warn', - text: "hash:'targethash' or ip:'1.2.3.4' is required" + text: "hash:'targethash' or ip:'1.2.3.4' is required", }, socket); } // find target - let mode, target; + let mode; let + target; if (typeof data.ip === 'string') { mode = 'ip'; target = data.ip; @@ -39,23 +40,24 @@ exports.run = async (core, server, socket, data) => { // reply with success server.reply({ cmd: 'info', - text: `Unbanned ${target}` + text: `Unbanned ${target}`, }, socket); // notify mods server.broadcast({ cmd: 'info', - text: `${socket.nick} unbanned: ${target}` + text: `${socket.nick} unbanned: ${target}`, }, { uType: 'mod' }); // stats are fun core.stats.decrement('users-banned'); -}; -// module meta -exports.info = { + return true; +} + +export const info = { name: 'unban', description: 'Removes target ip from the ratelimiter', usage: ` - API: { cmd: 'unban', ip/hash: '<target ip or hash>' }` + API: { cmd: 'unban', ip/hash: '<target ip or hash>' }`, }; diff --git a/server/src/commands/mod/unbanall.js b/server/src/commands/mod/unbanall.js index c285b80..49eeee5 100644 --- a/server/src/commands/mod/unbanall.js +++ b/server/src/commands/mod/unbanall.js @@ -3,34 +3,35 @@ */ // module main -exports.run = async (core, server, socket, data) => { +export async function run(core, server, socket) { // increase rate limit chance and ignore if not admin or mod if (socket.uType === 'user') { - return server.police.frisk(socket.remoteAddress, 10); + return server.police.frisk(socket.address, 10); } // remove arrest records - server.police.records = {}; + server.police.clear(); console.log(`${socket.nick} [${socket.trip}] unbanned all`); // reply with success server.reply({ cmd: 'info', - text: `Unbanned all ip addresses` + text: 'Unbanned all ip addresses', }, socket); // notify mods server.broadcast({ cmd: 'info', - text: `${socket.nick} unbanned all ip addresses` + text: `${socket.nick} unbanned all ip addresses`, }, { uType: 'mod' }); -}; -// module meta -exports.info = { + return true; +} + +export const info = { name: 'unbanall', description: 'Clears all banned ip addresses', usage: ` - API: { cmd: 'unbanall' }` + API: { cmd: 'unbanall' }`, }; diff --git a/server/src/scripts/configLib/SetupWizard.js b/server/src/scripts/configLib/SetupWizard.js index bd6ef24..ac2f3a0 100644 --- a/server/src/scripts/configLib/SetupWizard.js +++ b/server/src/scripts/configLib/SetupWizard.js @@ -1,40 +1,36 @@ +import { + start as _start, + get, +} from 'prompt'; + /** * Server setup wizard, quick server setup and all that jazz. . . - * - * Version: v2.0.0 - * Developer: Marzavec ( https://github.com/marzavec ) - * License: WTFPL ( http://www.wtfpl.net/txt/copying/ ) - * + * @author Marzavec ( https://github.com/marzavec ) + * @version v2.0.0 + * @license WTFPL ( http://www.wtfpl.net/txt/copying/ ) */ - -const fse = require('fs-extra'); -const prompt = require('prompt'); -const path = require('path'); - class SetupWizard { /** * Create a `SetupWizard` instance for initializing the server's config.json - * * @param {Object} serverConfig reference to the server config class */ - constructor (serverConfig) { + constructor(serverConfig) { this.serverConfig = serverConfig; } /** * Roll a d20 and begin the wizarding process - * */ - async start () { + async start() { // load the current config to use as defaults, if available - let currentConfig = await this.serverConfig.load() || {}; + const currentConfig = await this.serverConfig.load() || {}; // auto generate the salt if not currrently created - currentConfig.tripSalt = currentConfig.tripSalt || - [...Array(Math.floor(Math.random()*1024)+1024)].map(i=>(~~(Math.random()*36)).toString(36)).join(''); + currentConfig.tripSalt = currentConfig.tripSalt + || [...Array(Math.floor(Math.random() * 1024) + 1024)].map(() => (~~(Math.random() * 36)).toString(36)).join(''); // load the setup questions & set their defaults - let questions = require('../setupSchema/Questions.js'); + const questions = require('../setupSchema/Questions.js'); questions.properties = this.setQuestionDefaults(questions.properties, currentConfig); // force password re-entry @@ -45,19 +41,18 @@ class SetupWizard { require('../setupSchema/Banner.js'); // let's start playing 20 questions - prompt.start(); - prompt.get(questions, (err, result) => this.finalize(err, result)); + _start(); + get(questions, (err, result) => this.finalize(err, result)); } /** * Compares the currently loaded config with the stock questions, adds a default * and required option to the question - * * @param {Object} questions the set of questions from /setupSchema * @param {Object} currentConfig the current server options */ - setQuestionDefaults (questions, currentConfig) { - Object.keys(questions).forEach(qName => { + setQuestionDefaults(questions, currentConfig) { + Object.keys(questions).forEach((qName) => { if (typeof currentConfig[qName] !== 'undefined') { questions[qName].default = currentConfig[qName]; questions[qName].required = false; @@ -76,7 +71,7 @@ class SetupWizard { * @param {Object} err any errors generated by Prompt * @param {Object} result the answers / new config setup */ - async finalize (err, result) { + async finalize(err, result) { // output errors and die if needed if (err) { console.error(err); @@ -87,7 +82,7 @@ class SetupWizard { if (typeof result.mods === 'undefined') { result.mods = []; } - + // If we should log errors with the err stack when they occur. // See: CommandManager.js if (typeof result.logErrDetailed === 'undefined') { @@ -110,4 +105,4 @@ class SetupWizard { } } -module.exports = SetupWizard; +export default SetupWizard; diff --git a/server/src/scripts/configure.js b/server/src/scripts/configure.js index d7f2bf2..d96e0df 100644 --- a/server/src/scripts/configure.js +++ b/server/src/scripts/configure.js @@ -1,21 +1,17 @@ /** * Server configuration script, to (re)configure server options - * - * Version: v2.0.0 - * Developer: Marzavec ( https://github.com/marzavec ) - * License: WTFPL ( http://www.wtfpl.net/txt/copying/ ) - * + * @author Marzavec ( https://github.com/marzavec ) + * @version v2.0.0 + * @license WTFPL ( http://www.wtfpl.net/txt/copying/ ) */ -'use strict'; - // import required classes -const path = require('path'); -const ConfigManager = require('../serverLib/ConfigManager'); -const SetupWizard = require('./configLib/SetupWizard'); +import { join } from 'path'; +import ConfigManager from '../serverLib/ConfigManager'; +import SetupWizard from './configLib/SetupWizard'; // import and initialize configManager & dependencies -const serverConfig = new ConfigManager(path.join(__dirname, '../..')); +const serverConfig = new ConfigManager(join(__dirname, '../..')); const setup = new SetupWizard(serverConfig); setup.start(); diff --git a/server/src/scripts/setupSchema/Banner.js b/server/src/scripts/setupSchema/Banner.js index 823f0fe..f13fde7 100644 --- a/server/src/scripts/setupSchema/Banner.js +++ b/server/src/scripts/setupSchema/Banner.js @@ -5,8 +5,8 @@ * */ -const stripIndents = require('common-tags').stripIndents; -const chalk = require('chalk'); +import { stripIndents } from 'common-tags'; +import chalk from 'chalk'; // gotta have that sexy console console.log(stripIndents` @@ -20,9 +20,9 @@ console.log(stripIndents` ${chalk.white('Note:')} ${chalk.green('npm/yarn run config')} will re-run this utility. You will now be asked for the following: - - ${chalk.magenta(' Salt')}, the salt for username trip - - ${chalk.magenta('Admin Name')}, the initial admin username - - ${chalk.magenta('Admin Pass')}, the initial admin password - - ${chalk.magenta(' Port')}, the port for the websocket + - ${chalk.magenta(' Salt')}, the salt for username trip + - ${chalk.magenta('Admin Name')}, the initial admin username + - ${chalk.magenta('Admin Pass')}, the initial admin password + - ${chalk.magenta(' Port')}, the port for the websocket \u200b `); diff --git a/server/src/scripts/setupSchema/Questions.js b/server/src/scripts/setupSchema/Questions.js index f84d32f..532ba67 100644 --- a/server/src/scripts/setupSchema/Questions.js +++ b/server/src/scripts/setupSchema/Questions.js @@ -15,10 +15,10 @@ const Questions = { type: 'string', hidden: true, replace: '*', - before: value => { + before: (value) => { salt = value; return value; - } + }, }, adminName: { @@ -26,7 +26,7 @@ const Questions = { pattern: /^"?[a-zA-Z0-9_]+"?$/, type: 'string', message: 'Nicks can only contain letters, numbers and underscores', - before: value => value.replace(/"/g, '') + before: (value) => value.replace(/"/g, ''), }, adminTrip: { @@ -35,21 +35,21 @@ const Questions = { replace: '*', description: 'Admin Password', message: 'You must enter or re-enter a password', - before: value => { + before: (value) => { const crypto = require('crypto'); - let sha = crypto.createHash('sha256'); + const sha = crypto.createHash('sha256'); sha.update(value + salt); return sha.digest('base64').substr(0, 6); - } + }, }, websocketPort: { type: 'integer', message: 'The port may only be a number!', description: 'Websocket Port', - default: '6060' - } - } -} + default: '6060', + }, + }, +}; module.exports = Questions; diff --git a/server/src/serverLib/CommandManager.js b/server/src/serverLib/CommandManager.js index 0c4f0aa..e715c6b 100644 --- a/server/src/serverLib/CommandManager.js +++ b/server/src/serverLib/CommandManager.js @@ -1,47 +1,71 @@ -/** - * Commands / protocol manager- loads, validates and handles command execution - * - * Version: v2.0.0 - * Developer: Marzavec ( https://github.com/marzavec ) - * License: WTFPL ( http://www.wtfpl.net/txt/copying/ ) - * - */ - -const path = require('path'); -const didYouMean = require('didyoumean2').default; +import { + basename, + join, + sep, + dirname, + relative, +} from 'path'; +import didYouMean from 'didyoumean2'; // default command modules path const CmdDir = 'src/commands'; +/** + * Commands / protocol manager- loads, validates and handles command execution + * @property {Array} commands - Array of currently loaded command modules + * @property {Array} categories - Array of command modules categories + * @author Marzavec ( https://github.com/marzavec ) + * @version v2.0.0 + * @license WTFPL ( http://www.wtfpl.net/txt/copying/ ) + */ class CommandManager { /** * Create a `CommandManager` instance for handling commands/protocol * * @param {Object} core Reference to the global core object */ - constructor (core) { + constructor(core) { + /** + * Stored reference to the core + * @type {CoreApp} + */ this.core = core; + + /** + * Command module storage + * @type {Array} + */ this.commands = []; + + /** + * Command module category names (based off directory or module meta) + * @type {Array} + */ this.categories = []; - if (!this.core.config.hasOwnProperty('logErrDetailed')) { + + /** + * Full path to config.json file + * @type {String} + */ + if (typeof this.core.config.logErrDetailed === 'undefined') { this.core.config.logErrDetailed = false; } } /** * (Re)initializes name spaces for commands and starts load routine - * + * @public * @return {String} Module errors or empty if none */ - loadCommands () { + loadCommands() { this.commands = []; this.categories = []; const commandImports = this.core.dynamicImports.getImport(CmdDir); let cmdErrors = ''; - Object.keys(commandImports).forEach(file => { - let command = commandImports[file]; - let name = path.basename(file); + Object.keys(commandImports).forEach((file) => { + const command = commandImports[file]; + const name = basename(file); cmdErrors += this.validateAndLoad(command, file, name); }); @@ -50,29 +74,28 @@ class CommandManager { /** * Checks the module after having been `require()`ed in and reports errors - * * @param {Object} command reference to the newly loaded object * @param {String} file file path to the module * @param {String} name command (`cmd`) name - * + * @private * @return {String} Module errors or empty if none */ - validateAndLoad (command, file, name) { - let error = this.validateCommand(command); + validateAndLoad(command, file, name) { + const error = this.validateCommand(command); if (error) { - let errText = `Failed to load '${name}': ${error}`; + const errText = `Failed to load command module '${name}': ${error}`; console.log(errText); return errText; } if (!command.category) { - let base = path.join(this.core.dynamicImports.base, 'commands'); + const base = join(this.core.dynamicImports.base, 'commands'); let category = 'Uncategorized'; - if (file.indexOf(path.sep) > -1) { - category = path.dirname(path.relative(base, file)) - .replace(new RegExp(path.sep.replace('\\', '\\\\'), 'g'), '/'); + if (file.indexOf(sep) > -1) { + category = dirname(relative(base, file)) + .replace(new RegExp(sep.replace('\\', '\\\\'), 'g'), '/'); } command.info.category = category; @@ -86,7 +109,7 @@ class CommandManager { try { command.init(this.core); } catch (err) { - let errText = `Failed to initialize '${name}': ${err}`; + const errText = `Failed to initialize '${name}': ${err}`; console.log(errText); return errText; } @@ -99,121 +122,109 @@ class CommandManager { /** * Checks the module after having been `require()`ed in and reports errors - * * @param {Object} object reference to the newly loaded object - * + * @private * @return {String} Module errors or null if none */ - validateCommand (object) { - if (typeof object !== 'object') - return 'command setup is invalid'; - - if (typeof object.run !== 'function') - return 'run function is missing'; - - if (typeof object.info !== 'object') - return 'info object is missing'; - - if (typeof object.info.name !== 'string') - return 'info object is missing a valid name field'; + validateCommand(object) { + if (typeof object !== 'object') { return 'command setup is invalid'; } + if (typeof object.run !== 'function') { return 'run function is missing'; } + if (typeof object.info !== 'object') { return 'info object is missing'; } + if (typeof object.info.name !== 'string') { return 'info object is missing a valid name field'; } return null; } /** * Pulls all command names from a passed `category` - * * @param {String} category [Optional] filter return results by this category - * + * @public * @return {Array} Array of command modules matching the category */ - all (category) { + all(category) { return !category ? this.commands : this.commands.filter( - c => c.info.category.toLowerCase() === category.toLowerCase() - ); + (c) => c.info.category.toLowerCase() === category.toLowerCase(), + ); } /** - * Pulls all category names - * - * @return {Array} Array of sub directories under CmdDir + * All category names + * @public + * @readonly + * @return {Array} Array of command category names */ - get categoriesList () { + get categoriesList() { return this.categories; } /** - * Pulls command by name or alia(s) - * + * Pulls command by name or alias * @param {String} name name or alias of command - * + * @public * @return {Object} Target command module object */ - get (name) { + get(name) { return this.findBy('name', name) || this.commands.find( - command => command.info.aliases instanceof Array && - command.info.aliases.indexOf(name) > -1 + (command) => command.info.aliases instanceof Array + && command.info.aliases.indexOf(name) > -1, ); } /** * Pulls command by arbitrary search of the `module.info` attribute - * * @param {String} key name or alias of command * @param {String} value name or alias of command - * + * @public * @return {Object} Target command module object */ - findBy (key, value) { - return this.commands.find(c => c.info[key] === value); + findBy(key, value) { + return this.commands.find((c) => c.info[key] === value); } /** * Runs `initHooks` function on any modules that utilize the event - * + * @private * @param {Object} server main server object */ - initCommandHooks (server) { - this.commands.filter(c => typeof c.initHooks !== 'undefined').forEach( - c => c.initHooks(server) - ); + initCommandHooks(server) { + this.commands.filter((c) => typeof c.initHooks !== 'undefined').forEach( + (c) => c.initHooks(server), + ); } /** * Finds and executes the requested command, or fails with semi-intelligent error - * * @param {Object} server main server reference * @param {Object} socket calling socket reference * @param {Object} data command structure passed by socket (client) - * + * @public * @return {*} Arbitrary module return data */ - handleCommand (server, socket, data) { + handleCommand(server, socket, data) { // Try to find command first - let command = this.get(data.cmd); + const command = this.get(data.cmd); if (command) { return this.execute(command, server, socket, data); - } else { - // Then fail with helpful (sorta) message - return this.handleFail(server, socket, data); } + + // Then fail with helpful (sorta) message + return this.handleFail(server, socket, data); } /** * Requested command failure handler, attempts to find command and reports back - * * @param {Object} server main server reference * @param {Object} socket calling socket reference * @param {Object} data command structure passed by socket (client) - * + * @private * @return {*} Arbitrary module return data */ - handleFail (server, socket, data) { - const maybe = didYouMean(data.cmd, this.all().map(c => c.info.name), { + handleFail(server, socket, data) { + const maybe = didYouMean(data.cmd, this.all().map((c) => c.info.name), { threshold: 5, - thresholdType: 'edit-distance' + thresholdType: 'edit-distance', }); if (maybe) { @@ -221,7 +232,7 @@ class CommandManager { return this.handleCommand(server, socket, { cmd: 'socketreply', cmdKey: server.cmdKey, - text: `Command not found, did you mean: \`${maybe}\`?` + text: `Command not found, did you mean: \`${maybe}\`?`, }); } @@ -229,39 +240,37 @@ class CommandManager { return this.handleCommand(server, socket, { cmd: 'socketreply', cmdKey: server.cmdKey, - text: 'Unknown command' + text: 'Unknown command', }); } /** * Attempt to execute the requested command, fail if err or bad params - * * @param {Object} command target command module * @param {Object} server main server reference * @param {Object} socket calling socket reference * @param {Object} data command structure passed by socket (client) - * + * @private * @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++) { - if (typeof data[command.requiredData[i]] === 'undefined') - missing.push(command.requiredData[i]); + const missing = []; + for (let i = 0, len = command.requiredData.length; i < len; i += 1) { + if (typeof data[command.requiredData[i]] === 'undefined') { missing.push(command.requiredData[i]); } } if (missing.length > 0) { console.log(`Failed to execute '${ - command.info.name - }': missing required ${missing.join(', ')}\n\n`); + 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` + command.info.name + }': missing required ${missing.join(', ')}\n\n`, }); return null; @@ -271,20 +280,20 @@ class CommandManager { try { return await command.run(this.core, server, socket, data); } catch (err) { - let errText = `Failed to execute '${command.info.name}': `; - + const errText = `Failed to execute '${command.info.name}': `; + // If we have more detail enabled, then we get the trace // if it isn't, or the property doesn't exist, then we'll get only the message if (this.core.config.logErrDetailed === true) { console.log(errText + err.stack); } else { - console.log(errText + err.toString()) + console.log(errText + err.toString()); } this.handleCommand(server, socket, { cmd: 'socketreply', cmdKey: server.cmdKey, - text: errText + err.toString() + text: errText + err.toString(), }); return null; @@ -292,4 +301,4 @@ class CommandManager { } } -module.exports = CommandManager; +export default CommandManager; diff --git a/server/src/serverLib/ConfigManager.js b/server/src/serverLib/ConfigManager.js index e29a0e7..bb414be 100644 --- a/server/src/serverLib/ConfigManager.js +++ b/server/src/serverLib/ConfigManager.js @@ -1,38 +1,47 @@ +import dateFormat from 'dateformat'; +import { + existsSync, + ensureFileSync, + readJsonSync, + copySync, + writeJSONSync, + removeSync, +} from 'fs-extra'; +import { resolve } from 'path'; + /** * Server configuration manager, handling loading, creation, parsing and saving * of the main config.json file - * - * Version: v2.0.0 - * Developer: Marzavec ( https://github.com/marzavec ) - * License: WTFPL ( http://www.wtfpl.net/txt/copying/ ) - * + * @property {String} base - Base path that all imports are required in from + * @author Marzavec ( https://github.com/marzavec ) + * @version v2.0.0 + * @license WTFPL ( http://www.wtfpl.net/txt/copying/ ) */ -const dateFormat = require('dateformat'); -const fse = require('fs-extra'); -const path = require('path'); - class ConfigManager { /** * Create a `ConfigManager` instance for managing application settings - * * @param {String} basePath executing directory name; __dirname */ - constructor (basePath = __dirname) { - this.configPath = path.resolve(basePath, 'config/config.json'); + constructor(basePath = __dirname) { + /** + * Full path to config.json file + * @type {String} + */ + this.configPath = resolve(basePath, 'config/config.json'); - if (!fse.existsSync(this.configPath)){ - fse.ensureFileSync(this.configPath); + if (!existsSync(this.configPath)) { + ensureFileSync(this.configPath); } } /** - * Loads config.json (main server config) into mem - * - * @return {Object || Boolean} False if the config.json could not be loaded + * Loads config.json (main server config) into memory + * @public + * @return {(JSON|Boolean)} False if the config.json could not be loaded */ - async load () { + async load() { try { - this.config = fse.readJsonSync(this.configPath); + this.config = readJsonSync(this.configPath); } catch (e) { return false; } @@ -42,12 +51,12 @@ class ConfigManager { /** * Creates backup of current config into configPath - * + * @private * @return {String} Backed up config.json path */ - async backup () { + backup() { const backupPath = `${this.configPath}.${dateFormat('dd-mm-yy-HH-MM-ss')}.bak`; - fse.copySync(this.configPath, backupPath); + copySync(this.configPath, backupPath); return backupPath; } @@ -55,18 +64,18 @@ class ConfigManager { /** * First makes a backup of the current `config.json`, then writes current config * to disk - * + * @public * @return {Boolean} False on failure */ - async save () { - const backupPath = await this.backup(); + save() { + const backupPath = this.backup(); try { - fse.writeJSONSync(this.configPath, this.config, { - // Indent with two spaces + writeJSONSync(this.configPath, this.config, { + // Indent with two spaces spaces: 2, }); - fse.removeSync(backupPath); + removeSync(backupPath); return true; } catch (err) { @@ -78,18 +87,17 @@ class ConfigManager { /** * Updates current config[`key`] with `value` then writes changes to disk - * * @param {*} key arbitrary configuration key * @param {*} value new value to change `key` to - * + * @public * @return {Boolean} False on failure */ - async set (key, value) { + set(key, value) { const realKey = `${key}`; this.config[realKey] = value; - return await this.save(); + return this.save(); } } -module.exports = ConfigManager; +export default ConfigManager; diff --git a/server/src/serverLib/CoreApp.js b/server/src/serverLib/CoreApp.js index 6bab090..8c2225d 100644 --- a/server/src/serverLib/CoreApp.js +++ b/server/src/serverLib/CoreApp.js @@ -1,30 +1,32 @@ -/** - * The core / global reference object - * - * Version: v2.0.0 - * Developer: Marzavec ( https://github.com/marzavec ) - * License: WTFPL ( http://www.wtfpl.net/txt/copying/ ) - * - */ - -const path = require('path'); -const { +import { join } from 'path'; +import { CommandManager, ConfigManager, ImportsManager, MainServer, - StatsManager -} = require('./'); + StatsManager, +} from '.'; +/** + * The core app builds all required classes and maintains a central + * reference point across the app + * @property {ConfigManager} configManager - Provides loading and saving of the server config + * @property {Object} config - The current json config object + * @property {ImportsManager} dynamicImports - Dynamic require interface allowing hot reloading + * @property {CommandManager} commands - Manages and executes command modules + * @property {StatsManager} stats - Stores and adjusts arbritary stat data + * @property {MainServer} server - Main websocket server reference + * @author Marzavec ( https://github.com/marzavec ) + * @version v2.0.0 + * @license WTFPL ( http://www.wtfpl.net/txt/copying/ ) + */ class CoreApp { /** - * Create the main core instance. - */ - constructor () { - - } - - async init () { + * Load config then initialize children + * @public + * @return {void} + */ + async init() { await this.buildConfigManager(); this.buildImportManager(); @@ -33,8 +35,14 @@ class CoreApp { this.buildMainServer(); } - async buildConfigManager () { - this.configManager = new ConfigManager(path.join(__dirname, '../..')); + /** + * Creates a new instance of the ConfigManager, loads and checks + * the server config + * @private + * @return {void} + */ + async buildConfigManager() { + this.configManager = new ConfigManager(join(__dirname, '../..')); this.config = await this.configManager.load(); if (this.config === false) { @@ -43,23 +51,43 @@ class CoreApp { } } - buildImportManager () { - this.dynamicImports = new ImportsManager(path.join(__dirname, '../..')); + /** + * Creates a new instance of the ImportsManager + * @private + * @return {void} + */ + buildImportManager() { + this.dynamicImports = new ImportsManager(join(__dirname, '../..')); } - buildCommandsManager () { + /** + * Creates a new instance of the CommandManager and loads the command modules + * @private + * @return {void} + */ + buildCommandsManager() { this.commands = new CommandManager(this); this.commands.loadCommands(); } - buildStatsManager () { + /** + * Creates a new instance of the StatsManager and sets the server start time + * @private + * @return {void} + */ + buildStatsManager() { this.stats = new StatsManager(this); this.stats.set('start-time', process.hrtime()); } - buildMainServer () { + /** + * Creates a new instance of the MainServer + * @private + * @return {void} + */ + buildMainServer() { this.server = new MainServer(this); } } -module.exports = CoreApp; +export { CoreApp }; diff --git a/server/src/serverLib/ImportsManager.js b/server/src/serverLib/ImportsManager.js index 8db7b68..ac1fc4c 100644 --- a/server/src/serverLib/ImportsManager.js +++ b/server/src/serverLib/ImportsManager.js @@ -1,69 +1,77 @@ +import { + resolve, + basename as _basename, + relative, +} from 'path'; +import RecursiveRead from 'readdir-recursive'; + /** * Import managment base, used to load commands/protocol and configuration objects - * - * Version: v2.0.0 - * Developer: Marzavec ( https://github.com/marzavec ) - * License: WTFPL ( http://www.wtfpl.net/txt/copying/ ) - * + * @property {String} base - Base path that all imports are required in from + * @author Marzavec ( https://github.com/marzavec ) + * @version v2.0.0 + * @license WTFPL ( http://www.wtfpl.net/txt/copying/ ) */ - -const read = require('readdir-recursive'); -const path = require('path'); - class ImportsManager { /** - * Create a `ImportsManager` instance for (re)loading classes and config - * + * Create an `ImportsManager` instance for (re)loading classes and config * @param {String} basePath executing directory name; default __dirname */ - constructor (basePath) { + constructor(basePath) { + /** + * Stored reference to the base directory path + * @type {String} + */ this.basePath = basePath; + + /** + * Data holder for imported modules + * @type {Object} + */ this.imports = {}; } /** * Pull base path that all imports are required in from - * + * @public * @type {String} readonly */ - get base () { + get base() { 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 basePath. - * + * @param {String} dirName The name of the dir to load, relative to the basePath + * @private * @return {String} Load errors or empty if none */ - loadDir (dirName) { - const dir = path.resolve(this.basePath, dirName); + loadDir(dirName) { + const dir = resolve(this.basePath, dirName); let errorText = ''; try { - read.fileSync(dir).forEach(file => { - const basename = path.basename(file); + RecursiveRead.fileSync(dir).forEach((file) => { + const basename = _basename(file); if (basename.startsWith('_') || !basename.endsWith('.js')) return; let imported; try { imported = require(file); + + if (!this.imports[dirName]) { + this.imports[dirName] = {}; + } + + this.imports[dirName][file] = imported; } catch (e) { - let err = `Unable to load modules from ${dirName} (${path.relative(dir, file)})\n${e}`; + const err = `Unable to load modules from ${dirName} (${relative(dir, file)})\n${e}`; errorText += err; console.error(err); - return errorText; - } - - if (!this.imports[dirName]) { - this.imports[dirName] = {}; } - - this.imports[dirName][file] = imported; }); } catch (e) { - let err = `Unable to load modules from ${dirName}\n${e}`; + const err = `Unable to load modules from ${dirName}\n${e}`; errorText += err; console.error(err); return errorText; @@ -75,15 +83,13 @@ 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 {Array} dirName The name of the dir to load, relative to the _base path. - * + * @public * @return {String} Load errors or empty if none */ - reloadDirCache () { + reloadDirCache() { let errorText = ''; - Object.keys(this.imports).forEach(dir => { + Object.keys(this.imports).forEach((dir) => { Object.keys(this.imports[dir]).forEach((mod) => { delete require.cache[require.resolve(mod)]; }); @@ -97,20 +103,19 @@ class ImportsManager { /** * Pull reference to imported modules that were imported from dirName, or * load required directory if not found - * * @param {String} dirName The name of the dir to load, relative to the _base path. - * + * @public * @return {Object} Object containing command module paths and structs */ - getImport (dirName) { - let imported = this.imports[dirName]; + getImport(dirName) { + const imported = this.imports[dirName]; if (!imported) { this.loadDir(dirName); } - return Object.assign({}, this.imports[dirName]); + return { ...this.imports[dirName] }; } } -module.exports = ImportsManager; +export default ImportsManager; 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; diff --git a/server/src/serverLib/RateLimiter.js b/server/src/serverLib/RateLimiter.js index 43cf077..3f9bc27 100644 --- a/server/src/serverLib/RateLimiter.js +++ b/server/src/serverLib/RateLimiter.js @@ -1,39 +1,60 @@ +import { RateLimits } from '../utility/Constants'; + /** * 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/ ) - * + * @property {Object} data - The current stats data + * @author Marzavec ( https://github.com/marzavec ) + * @author Andrew Belt ( https://github.com/AndrewBelt ) + * @version v2.0.0 + * @license WTFPL ( http://www.wtfpl.net/txt/copying/ ) */ - class RateLimiter { /** - * Create a ratelimiter instance. - */ - constructor () { + * Create a ratelimiter instance + */ + constructor() { + /** + * Data holder rate limit records + * @type {Object} + */ this.records = {}; - this.halflife = 30 * 1000; // milliseconds - this.threshold = 25; + + /** + * Time in milliseconds to decrement ratelimit weight + * @type {Number} + */ + this.halflife = RateLimits.halflife; + + /** + * Weight until ratelimited + * @type {Number} + */ + this.threshold = RateLimits.threshold; + + /** + * Stores the associated connection fingerprint with record id + * @type {Array} + */ this.hashes = []; } /** * Finds current score by `id` - * * @param {String} id target id / address - * + * @private * @return {Object} Object containing the record meta */ - search (id) { + search(id) { let record = this.records[id]; if (!record) { - record = this.records[id] = { + this.records[id] = { time: Date.now(), - score: 0 - } + score: 0, + }; + + record = this.records[id]; } return record; @@ -41,20 +62,22 @@ class RateLimiter { /** * Adjusts the current ratelimit score by `deltaScore` - * * @param {String} id target id / address * @param {Number} deltaScore amount to adjust current score by - * + * @example + * // Penalize by 1 and store if connection is ratelimited or not + * let isLimited = police.frisk(socket.address, 1); + * @public * @return {Boolean} True if record threshold has been exceeded */ - frisk (id, deltaScore) { - let record = this.search(id); + frisk(id, deltaScore) { + const record = this.search(id); if (record.arrested) { return true; } - record.score *= Math.pow(2, -(Date.now() - record.time ) / this.halflife); + record.score *= 2 ** -(Date.now() - record.time) / this.halflife; record.score += deltaScore; record.time = Date.now(); @@ -67,11 +90,16 @@ class RateLimiter { /** * Statically set server to no longer accept traffic from `id` - * * @param {String} id target id / address + * @example + * // Usage within a command module: + * let badClient = server.findSockets({ channel: socket.channel, nick: targetNick }); + * server.police.arrest(badClient[0].address, badClient[0].hash); + * @public + * @return {void} */ - arrest (id, hash) { - let record = this.search(id); + arrest(id, hash) { + const record = this.search(id); record.arrested = true; this.hashes[hash] = id; @@ -79,17 +107,32 @@ class RateLimiter { /** * Remove statically assigned limit from `id` - * * @param {String} id target id / address + * @example + * // Usage within a command module: + * server.police.pardon('targetHashOrIP'); + * @public + * @return {void} */ - pardon (id) { - if (typeof this.hashes[id] !== 'undefined') { - id = this.hashes[id]; + pardon(id) { + let targetId = id; + if (typeof this.hashes[targetId] !== 'undefined') { + targetId = this.hashes[targetId]; } - let record = this.search(id); + const record = this.search(targetId); record.arrested = false; } + + /** + * Clear all records + * @public + * @return {void} + */ + clear() { + this.records = {}; + this.hashes = []; + } } -module.exports = RateLimiter; +export default RateLimiter; diff --git a/server/src/serverLib/StatsManager.js b/server/src/serverLib/StatsManager.js index e472504..b6e4f97 100644 --- a/server/src/serverLib/StatsManager.js +++ b/server/src/serverLib/StatsManager.js @@ -1,60 +1,79 @@ /** * Simple generic stats collection script for events occurances (etc) - * - * Version: v2.0.0 - * Developer: Marzavec ( https://github.com/marzavec ) - * License: WTFPL ( http://www.wtfpl.net/txt/copying/ ) - * + * @property {Object} data - The current stats data + * @author Marzavec ( https://github.com/marzavec ) + * @version v2.0.0 + * @license WTFPL ( http://www.wtfpl.net/txt/copying/ ) */ - class StatsManager { /** - * Create a stats instance. - * + * Create a stats instance */ - constructor () { + constructor() { + /** + * Data holder for the stats class + * @type {Object} + */ this.data = {}; } /** * Retrieve value of arbitrary `key` reference - * * @param {String} key Reference to the arbitrary store name - * + * @example + * // Find previously set `start-time` + * stats.get('start-time'); + * @public * @return {*} Data referenced by `key` */ - get (key) { + get(key) { return this.data[key]; } /** * Set value of arbitrary `key` reference - * * @param {String} key Reference to the arbitrary store name * @param {Number} value New value for `key` + * @example + * // Set `start-time` + * stats.set('start-time', process.hrtime()); + * @public + * @return {void} */ - set (key, value) { + set(key, value) { this.data[key] = value; } /** * Increase value of arbitrary `key` reference, by 1 or `amount` - * * @param {String} key Reference to the arbitrary store name - * @param {Number} amount Value to increase `key` by, or 1 if omitted + * @param {?Number} [amount=1] Value to increase `key` by, or 1 if omitted + * @example + * // Increment by `amount` + * stats.increment('users', 6); + * // Increment by 1 + * stats.increment('users'); + * @public + * @return {void} */ - increment (key, amount) { - this.set(key, (this.get(key) || 0) + (amount || 1)); + increment(key, amount = 1) { + this.set(key, (this.get(key) || 0) + amount); } /** * Reduce value of arbitrary `key` reference, by 1 or `amount` - * * @param {String} key Reference to the arbitrary store name - * @param {Number} amount Value to decrease `key` by, or 1 if omitted + * @param {?Number} [amount=1] Value to decrease `key` by, or 1 if omitted + * @example + * // Decrement by `amount` + * stats.decrement('users', 6); + * // Decrement by 1 + * stats.decrement('users'); + * @public + * @return {void} */ - decrement (key, amount) { - this.set(key, (this.get(key) || 0) - (amount || 1)); + decrement(key, amount = 1) { + this.set(key, (this.get(key) || 0) - amount); } } diff --git a/server/src/serverLib/index.js b/server/src/serverLib/index.js index 4583de6..a820588 100644 --- a/server/src/serverLib/index.js +++ b/server/src/serverLib/index.js @@ -1,8 +1,6 @@ -module.exports = { - CommandManager: require('./CommandManager'), - ConfigManager: require('./ConfigManager'), - ImportsManager: require('./ImportsManager'), - MainServer: require('./MainServer'), - RateLimiter: require('./RateLimiter'), - StatsManager: require('./StatsManager') -}; +export const CommandManager = require('./CommandManager').default; +export const ConfigManager = require('./ConfigManager').default; +export const ImportsManager = require('./ImportsManager').default; +export const MainServer = require('./MainServer').default; +export const RateLimiter = require('./RateLimiter').default; +export const StatsManager = require('./StatsManager'); diff --git a/server/src/utility/Constants.js b/server/src/utility/Constants.js new file mode 100644 index 0000000..6836421 --- /dev/null +++ b/server/src/utility/Constants.js @@ -0,0 +1,19 @@ +/** + * Rate limit options + * @typedef {Object} RateLimits + * @property {number} halflife Time in milliseconds to decrement ratelimit weight + * @property {number} threshold Weight until ratelimited + */ +exports.RateLimits = { + halflife: 30 * 1000, + threshold: 25, +}; + +/** + * Websocket server options + * @typedef {Object} ServerConst + * @property {number} PulseSpeed Time in milliseconds to ping each client + */ +exports.ServerConst = { + PulseSpeed: 16 * 1000, +}; |