diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index dfe0770..0000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto diff --git a/.gitignore b/.gitignore deleted file mode 100644 index a6c57f5..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.json diff --git a/Quotas.js b/Quotas.js new file mode 100644 index 0000000..7edc660 --- /dev/null +++ b/Quotas.js @@ -0,0 +1,34 @@ +module.exports = Object.seal({ + chat: { + lobby: { + amount: 4, + time: 4000 + }, + normal: { + amount: 4, + time: 4000 + }, + insane: { + amount: 10, + time: 4000 + } + }, + chown: { + amount: 10, + time: 5000 + }, + userset: { + amount: 30, + time: 30 * 60000 + }, + room: { + time: 500 + }, + cursor: { + time: 16 + }, + kickban: { + amount: 2, + time: 1000 + } +}) \ No newline at end of file diff --git a/banned.json b/banned.json new file mode 100644 index 0000000..1610ea1 --- /dev/null +++ b/banned.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/config.js b/config.js new file mode 100644 index 0000000..0497b8a --- /dev/null +++ b/config.js @@ -0,0 +1,10 @@ +module.exports = Object.seal({ + port: "8080", + motd: "You agree to read this message.", + _id_PrivateKey: "boppity", + defaultUsername: "Anonymous", + defaultRoomColor: "#3b5054", + defaultLobbyColor: "#19b4b9", + defaultLobbyColor2: "#801014", + adminpass: "adminpass" +}) diff --git a/index.js b/index.js index 62db0b2..3f047d8 100644 --- a/index.js +++ b/index.js @@ -16,7 +16,7 @@ global.isObj = function(a){ } let Server = require("./src/Server.js"); -let config = require('./src/db/config.json'); +let config = require('./config'); global.SERVER = new Server(config); let console = process.platform == 'win32' ? new AsyncConsole("", input => { try { diff --git a/package.json b/package.json index 895bc77..85f2681 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,10 @@ "homepage": "https://github.com/BopItFreak/mpp-server#readme", "dependencies": { "asyncconsole": "^1.3.9", - "events": "^3.0.0", - "keccak": "^2.0.0", + "events": "^3.1.0", + "keccak": "^2.1.0", "node-json-color-stringify": "^1.1.0", - "ws": "^7.1.2" + "ws": "^7.2.3" }, "devDependencies": {} } diff --git a/src/Client.js b/src/Client.js index c762e68..ba10f85 100644 --- a/src/Client.js +++ b/src/Client.js @@ -1,4 +1,8 @@ const Room = require("./Room.js"); +const Quota = require ("./Quota.js"); +const quotas = require('../Quotas'); +const RateLimit = require('./RateLimit.js').RateLimit; +const RateLimitChain = require('./RateLimit.js').RateLimitChain; require('node-json-color-stringify'); class Client extends EventEmitter { constructor(ws, req, server) { @@ -9,6 +13,10 @@ class Client extends EventEmitter { this.server = server; this.participantId; this.channel; + this.staticQuotas = { + room: new RateLimit(quotas.room.time) + }; + this.quotas = {}; this.ws = ws; this.req = req; this.ip = (req.connection.remoteAddress).replace("::ffff:", ""); @@ -62,6 +70,24 @@ class Client extends EventEmitter { this.ws.send(JSON.stringify(arr)); } } + initParticipantQuotas() { + this.quotas = { + //"chat": new Quota(Quota.PARAMS_A_NORMAL), + chat: { + lobby: new RateLimitChain(quotas.chat.lobby.amount, quotas.chat.lobby.time), + normal: new RateLimitChain(quotas.chat.normal.amount, quotas.chat.normal.time), + insane: new RateLimitChain(quotas.chat.insane.amount, quotas.chat.insane.time) + }, + cursor: new RateLimit(quotas.cursor.time), + chown: new RateLimitChain(quotas.chown.amount, quotas.chown.time), + userset: new RateLimitChain(quotas.userset.amount, quotas.userset.time), + kickban: new RateLimitChain(quotas.kickban.amount, quotas.kickban.time), + note: new Quota(Quota.PARAMS_LOBBY), + chset: new Quota(Quota.PARAMS_USED_A_LOT), + "+ls": new Quota(Quota.PARAMS_USED_A_LOT), + "-ls": new Quota(Quota.PARAMS_USED_A_LOT) + } + } destroy() { this.ws.close(); if (this.channel) { @@ -102,4 +128,4 @@ class Client extends EventEmitter { }); } } -module.exports = Client; \ No newline at end of file +module.exports = Client; diff --git a/src/Message.js b/src/Message.js index b8b79d9..36d9fb9 100644 --- a/src/Message.js +++ b/src/Message.js @@ -1,3 +1,4 @@ +const Quota = require('./Quota'); const User = require("./User.js"); const Room = require("./Room.js"); module.exports = (cl) => { @@ -26,10 +27,28 @@ module.exports = (cl) => { if (!msg.hasOwnProperty("set") || !msg.set) msg.set = {}; if (msg.hasOwnProperty("_id") && typeof msg._id == "string") { if (msg._id.length > 512) return; + if (!cl.staticQuotas.room.attempt()) return; cl.setChannel(msg._id, msg.set); + let param; + if (cl.channel.isLobby(cl.channel._id)) { + param = Quota.N_PARAMS_LOBBY; + param.m = "nq"; + cl.sendArray([param]) + } else { + if (!(cl.user._id == cl.channel.crown.userId)) { + param = Quota.N_PARAMS_NORMAL; + param.m = "nq"; + cl.sendArray([param]) + } else { + param = Quota.N_PARAMS_RIDICULOUS; + param.m = "nq"; + cl.sendArray([param]) + } + } } }) - cl.on("m", msg => { + cl.on("m", (msg, admin) => { + if (!cl.quotas.cursor.attempt() && !admin) return; if (!(cl.channel && cl.participantId)) return; if (!msg.hasOwnProperty("x")) msg.x = null; if (!msg.hasOwnProperty("y")) msg.y = null; @@ -38,7 +57,8 @@ module.exports = (cl) => { cl.channel.emit("m", cl, msg.x, msg.y) }) - cl.on("chown", msg => { + cl.on("chown", (msg, admin) => { + if (!cl.quotas.chown.attempt() && !admin) return; if (!(cl.channel && cl.participantId)) return; //console.log((Date.now() - cl.channel.crown.time)) //console.log(!(cl.channel.crown.userId != cl.user._id), !((Date.now() - cl.channel.crown.time) > 15000)); @@ -47,9 +67,17 @@ module.exports = (cl) => { // console.log(cl.channel.crown) if (cl.user._id == cl.channel.crown.userId || cl.channel.crowndropped) cl.channel.chown(msg.id); + if (msg.id == cl.user.id) { + param = Quota.N_PARAMS_RIDICULOUS; + param.m = "nq"; + cl.sendArray([param]) + } } else { if (cl.user._id == cl.channel.crown.userId || cl.channel.crowndropped) cl.channel.chown(); + param = Quota.N_PARAMS_NORMAL; + param.m = "nq"; + cl.sendArray([param]) } }) cl.on("chset", msg => { @@ -59,10 +87,19 @@ module.exports = (cl) => { cl.channel.settings = msg.set; cl.channel.updateCh(); }) - cl.on("a", msg => { + cl.on("a", (msg, admin) => { if (!(cl.channel && cl.participantId)) return; if (!msg.hasOwnProperty('message')) return; if (cl.channel.settings.chat) { + if (cl.channel.isLobby(cl.channel._id)) { + if (!cl.quotas.chat.lobby.attempt() && !admin) return; + } else { + if (!(cl.user._id == cl.channel.crown.userId)) { + if (!cl.quotas.chat.normal.attempt() && !admin) return; + } else { + if (!cl.quotas.chat.insane.attempt() && !admin) return; + } + } cl.channel.emit('a', cl, msg); } }) @@ -104,6 +141,7 @@ module.exports = (cl) => { if (!msg.hasOwnProperty("set") || !msg.set) msg.set = {}; if (msg.set.hasOwnProperty('name') && typeof msg.set.name == "string") { if (msg.set.name.length > 40) return; + if (!cl.quotas.name.attempt()) return; cl.user.name = msg.set.name; let user = new User(cl); user.getUserData().then((usr) => { @@ -124,6 +162,7 @@ module.exports = (cl) => { if (!(cl.channel && cl.participantId)) return; if (!(cl.user._id == cl.channel.crown.userId)) return; if (msg.hasOwnProperty('_id') && typeof msg._id == "string") { + if (!cl.quotas.kickban.attempt() && !admin) return; let _id = msg._id; let ms = msg.ms || 0; cl.channel.kickban(_id, ms); @@ -153,7 +192,7 @@ module.exports = (cl) => { let dbentry = user.userdb.get(uSr._id); if (!dbentry) return; dbentry.color = msg.color; - //user.updatedb(); + user.updatedb(); cl.server.rooms.forEach((room) => { room.updateParticipant(usr.participantId, { color: msg.color @@ -165,4 +204,4 @@ module.exports = (cl) => { }) -} \ No newline at end of file +} diff --git a/src/Quota.js b/src/Quota.js index 16bb717..da24303 100644 --- a/src/Quota.js +++ b/src/Quota.js @@ -1,30 +1,96 @@ +//Adaptation of https://gist.github.com/brandon-lockaby/7339587 into modern javascript. +/* +class RateLimit { + constructor(interval_ms) { + this._interval_ms = interval_ms || 0; // (0 means no limit) + this._after = 0; + } + attempt(time) { + var time = time || Date.now(); + if(time < this._after) return false; + this._after = time + this._interval_ms; + return true; + }; + + interval(interval_ms) { + this._after += interval_ms - this._interval_ms; + this._interval_ms = interval_ms; + }; +} + +class RateLimitChain(num, interval_ms) { + constructor(num, interval_ms) { + this.setNumAndInterval(num, interval_ms); + } + + attempt(time) { + var time = time || Date.now(); + for(var i = 0; i < this._chain.length; i++) { + if(this._chain[i].attempt(time)) return true; + } + return false; + }; + + setNumAndInterval(num, interval_ms) { + this._chain = []; + for(var i = 0; i < num; i++) { + this._chain.push(new RateLimit(interval_ms)); + } + }; +}*/ + class Quota { - constructor(cb) { + constructor(params, cb) { this.cb = cb; - this.setParams(); + this.setParams(params); this.resetPoints(); + this.interval; }; - static NQ_PARAMS_LOBBY = { + static N_PARAMS_LOBBY = { allowance: 200, - max: 600 + max: 600, + interval: 2000 }; - static NQ_PARAMS_NORMAL = { + static N_PARAMS_NORMAL = { allowance: 400, - max: 1200 + max: 1200, + interval: 2000 }; - static NQ_PARAMS_RIDICULOUS = { + static N_PARAMS_RIDICULOUS = { allowance: 600, - max: 1800 + max: 1800, + interval: 2000 }; - static NQ_PARAMS_OFFLINE = { + static PARAMS_OFFLINE = { allowance: 8000, max: 24000, - maxHistLen: 3 + maxHistLen: 3, + interval: 2000 }; - static CH_PARAMS = { - allowance: 8000, - max: 24000, - maxHistLen: 3 + static PARAMS_A_NORMAL = { + allowance: 4, + max: 4, + interval: 6000 + }; + static PARAMS_A_CROWNED = { + allowance:10, + max:10, + interval: 2000 + } + static PARAMS_CH = { + allowance: 1, + max: 2, + interval: 1000 + } + static PARAMS_USED_A_LOT = { + allowance:1, + max:1, + interval: 2000 + } + static PARAMS_M = { + allowance:15000, + max:500000, + interval: 2000 } getParams() { return { @@ -35,10 +101,14 @@ class Quota { }; }; setParams(params) { - params = params || NoteQuota.PARAMS_OFFLINE; - var allowance = params.allowance || this.allowance || NoteQuota.PARAMS_OFFLINE.allowance; - var max = params.max || this.max || NoteQuota.PARAMS_OFFLINE.max; - var maxHistLen = params.maxHistLen || this.maxHistLen || NoteQuota.PARAMS_OFFLINE.maxHistLen; + params = params || Quota.PARAMS_OFFLINE; + var allowance = params.allowance || this.allowance || Quota.PARAMS_OFFLINE.allowance; + var max = params.max || this.max || Quota.PARAMS_OFFLINE.max; + var maxHistLen = params.maxHistLen || this.maxHistLen || Quota.PARAMS_OFFLINE.maxHistLen; + let interval = params.interval || 0 + this.inverval = setInterval(() => { + this.tick(); + }, params.interval) if (allowance !== this.allowance || max !== this.max || maxHistLen !== this.maxHistLen) { this.allowance = allowance; this.max = max; @@ -84,4 +154,5 @@ class Quota { } }; } -module.exports = Quota; \ No newline at end of file + +module.exports = Quota \ No newline at end of file diff --git a/src/Ratelimit.js b/src/Ratelimit.js new file mode 100644 index 0000000..8d27edc --- /dev/null +++ b/src/Ratelimit.js @@ -0,0 +1,40 @@ + +var RateLimit = function(interval_ms) { + this._interval_ms = interval_ms || 0; // (0 means no limit) + this._after = 0; +}; + +RateLimit.prototype.attempt = function(time) { + var time = time || Date.now(); + if(time < this._after) return false; + this._after = time + this._interval_ms; + return true; +}; + +RateLimit.prototype.setInterval = function(interval_ms) { + this._after += interval_ms - this._interval_ms; + this._interval_ms = interval_ms; +}; + +var RateLimitChain = function(num, interval_ms) { + this.setNumAndInterval(num, interval_ms); +}; + +RateLimitChain.prototype.attempt = function(time) { + var time = time || Date.now(); + for(var i = 0; i < this._chain.length; i++) { + if(this._chain[i].attempt(time)) return true; + } + return false; +}; + +RateLimitChain.prototype.setNumAndInterval = function(num, interval_ms) { + this._chain = []; + for(var i = 0; i < num; i++) { + this._chain.push(new RateLimit(interval_ms)); + } +}; + +var exports = typeof module !== "undefined" ? module.exports : this; +exports.RateLimit = RateLimit; +exports.RateLimitChain = RateLimitChain; \ No newline at end of file diff --git a/src/Room.js b/src/Room.js index f6f6520..6d2a2b8 100644 --- a/src/Room.js +++ b/src/Room.js @@ -2,7 +2,7 @@ //room class //room deleter //databases in Map - +const Quota = require("./Quota.js"); class Room extends EventEmitter { constructor(server, _id, settings) { super(); @@ -11,7 +11,9 @@ class Room extends EventEmitter { this.server = server; this.crown = null; this.crowndropped = false; - this.settings = this.verifySet(this._id,{set:settings}); + this.settings = this.verifySet(this._id, { + set: settings + }); this.chatmsgs = []; this.ppl = new Map(); this.connections = []; @@ -25,7 +27,9 @@ class Room extends EventEmitter { let participantId = createKeccakHash('keccak256').update((Math.random().toString() + cl.ip)).digest('hex').substr(0, 24); cl.user.id = participantId; cl.participantId = participantId; + cl.initParticipantQuotas(); if (((this.connections.length == 0 && Array.from(this.ppl.values()).length == 0) && !this.isLobby(this._id)) || this.crown && (this.crown.userId == cl.user._id)) { //user that created the room, give them the crown. + //cl.quotas.a.setParams(Quota.PARAMS_A_CROWNED); this.crown = { participantId: cl.participantId, userId: cl.user._id, @@ -40,8 +44,11 @@ class Room extends EventEmitter { } } this.crowndropped = false; + } else { + //cl.quotas.a.setParams(Quota.PARAMS_A_NORMAL); } this.ppl.set(participantId, cl); + this.connections.push(cl); this.sendArray([{ color: this.ppl.get(cl.participantId).user.color, @@ -60,6 +67,7 @@ class Room extends EventEmitter { } else { cl.user.id = otheruser.participantId; cl.participantId = otheruser.participantId; + cl.quotas = otheruser.quotas; this.connections.push(cl); cl.sendArray([{ m: "c", @@ -169,12 +177,12 @@ class Room extends EventEmitter { } return data; } - verifyColor(strColor){ + verifyColor(strColor) { var test2 = /^#[0-9A-F]{6}$/i.test(strColor); - if(test2 == true){ - return strColor; - } else{ - return false; + if (test2 == true) { + return strColor; + } else { + return false; } } isLobby(_id) { @@ -182,16 +190,16 @@ class Room extends EventEmitter { let lobbynum = _id.split("lobby")[1]; if (_id == "lobby") { return true; - } + } if (!(parseInt(lobbynum).toString() == lobbynum)) return false; - for (let i in lobbynum) { - if (parseInt(lobbynum[i]) >= 0) { - if (parseInt(i) + 1 == lobbynum.length) return true; - - } else { - return false; - } + for (let i in lobbynum) { + if (parseInt(lobbynum[i]) >= 0) { + if (parseInt(i) + 1 == lobbynum.length) return true; + + } else { + return false; } + } } else if (_id.startsWith("test/")) { if (_id == "test/") { return false; diff --git a/src/Server.js b/src/Server.js index 8d29a44..afd7ae2 100644 --- a/src/Server.js +++ b/src/Server.js @@ -1,4 +1,5 @@ -const Client = require("./Client.js") +const Client = require("./Client.js"); +const banned = require('../banned.json'); class Server extends EventEmitter { constructor(config) { super(); @@ -6,8 +7,9 @@ class Server extends EventEmitter { this.wss = new WebSocket.Server({ port: config.port, backlog: 100, - verifyClient: function (info, done) { - done(true) + verifyClient: (info) => { + if (banned.includes((info.req.connection.remoteAddress).replace("::ffff:", ""))) return false; + return true; } }); this.connectionid = 0; diff --git a/src/User.js b/src/User.js index bd10b83..3dae730 100644 --- a/src/User.js +++ b/src/User.js @@ -50,4 +50,4 @@ class User { return [...strMap.entries()].reduce((obj, [key, value]) => (obj[key] = value, obj), {}); } } -module.exports = User; \ No newline at end of file +module.exports = User; diff --git a/src/db/textfile.txt b/src/db/textfile.txt deleted file mode 100644 index e69de29..0000000 diff --git a/src/db/users.json b/src/db/users.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/src/db/users.json @@ -0,0 +1 @@ +{}