From 71960104eb8b4433051f5c6d9eb7360188da8492 Mon Sep 17 00:00:00 2001 From: Hri7566 Date: Wed, 10 Jul 2024 17:02:42 -0400 Subject: [PATCH] Add rate limits for every message, fix lobby regex, implement tags into database, reword comments, add channel flags --- config/channels.yml | 16 +-- config/ratelimits.yml | 53 ++++++- prisma/schema.prisma | 1 + src/channel/Channel.ts | 142 +++++++++++++++---- src/channel/config.ts | 10 +- src/channel/forceLoad.ts | 41 ++++-- src/data/user.ts | 15 +- src/index.ts | 4 +- src/util/Logger.ts | 0 src/util/types.d.ts | 51 ++++--- src/ws/Socket.ts | 20 ++- src/ws/events/admin/handlers/ch_flag.ts | 20 +++ src/ws/events/admin/handlers/forceload.ts | 12 ++ src/ws/events/admin/handlers/tag.ts | 29 ++++ src/ws/events/user/handlers/+ls.ts | 4 + src/ws/events/user/handlers/-ls.ts | 4 + src/ws/events/user/handlers/admin_message.ts | 8 +- src/ws/events/user/handlers/bye.ts | 3 + src/ws/events/user/handlers/chown.ts | 3 + src/ws/events/user/handlers/chset.ts | 3 + src/ws/events/user/handlers/devices.ts | 3 + src/ws/events/user/handlers/hi.ts | 7 +- src/ws/events/user/handlers/m.ts | 6 +- src/ws/events/user/handlers/n.ts | 3 + src/ws/events/user/handlers/t.ts | 6 +- src/ws/ratelimit/config.ts | 73 +++++++++- src/ws/ratelimit/limits/admin.ts | 21 ++- src/ws/ratelimit/limits/crown.ts | 22 ++- src/ws/ratelimit/limits/user.ts | 22 ++- 29 files changed, 506 insertions(+), 96 deletions(-) mode change 100755 => 100644 src/util/Logger.ts create mode 100644 src/ws/events/admin/handlers/ch_flag.ts create mode 100644 src/ws/events/admin/handlers/forceload.ts create mode 100644 src/ws/events/admin/handlers/tag.ts diff --git a/config/channels.yml b/config/channels.yml index e147a71..27875e6 100644 --- a/config/channels.yml +++ b/config/channels.yml @@ -1,7 +1,6 @@ forceLoad: - lobby - test/awkward - lobbySettings: lobby: true chat: true @@ -9,17 +8,18 @@ lobbySettings: visible: true color: "#73b3cc" color2: "#273546" - defaultSettings: chat: true crownsolo: false color: "#3b5054" color2: "#001014" visible: true - lobbyRegexes: - - "^lobby[1-9]?[1-9]?$" - - "^test/.+$" - -lobbyBackdoor: "lolwutsecretlobbybackdoor" -fullChannel: "test/awkward" + - ^lobby[0-9][0-9]$ + - ^lobby[0-9]$ + - ^lobby$ + - ^test/.+$ +lobbyBackdoor: lolwutsecretlobbybackdoor +fullChannel: test/awkward +sendLimit: false +sendTags: false diff --git a/config/ratelimits.yml b/config/ratelimits.yml index 2063bf8..42076cb 100644 --- a/config/ratelimits.yml +++ b/config/ratelimits.yml @@ -3,28 +3,73 @@ user: a: 1500 m: 50 ch: 1000 - kickban: 250 + kickban: 125 + t: 7.8125 + +ls: 16.666666666666668 + -ls: 16.666666666666668 + chown: 2000 + hi: 50 + bye: 50 + devices: 50 + admin_message: 50 + admin message: 50 chains: userset: interval: 1800000 num: 1000 + chset: + interval: 1800000 + num: 1024 + n: + interval: 1000 + num: 512 crown: normal: a: 600 m: 50 ch: 1000 - kickban: 250 + kickban: 125 + t: 7.8125 + +ls: 16.666666666666668 + -ls: 16.666666666666668 + chown: 2000 + hi: 50 + bye: 50 + devices: 50 + admin_message: 50 + admin message: 50 chains: userset: interval: 1800000 num: 1000 + chset: + interval: 1800000 + num: 1024 + n: + interval: 1000 + num: 512 admin: normal: a: 120 m: 16.666666666666668 ch: 100 - kickban: 31.25 + kickban: 16.666666666666668 + t: 3.90625 + +ls: 16.666666666666668 + -ls: 16.666666666666668 + chown: 500 + hi: 50 + bye: 50 + devices: 50 + admin_message: 16.666666666666668 + admin message: 16.666666666666668 chains: userset: - interval: 1800000 + interval: 500 num: 1000 + chset: + interval: 1800000 + num: 1024 + n: + interval: 50 + num: 512 diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c04ee0a..327ed72 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -16,6 +16,7 @@ model User { name String @default("Anonymous") color String @default("#ffffff") flags String @default("{}") // JSON flags object + tag String // JSON tag } model ChatHistory { diff --git a/src/channel/Channel.ts b/src/channel/Channel.ts index 93b079d..afcf00d 100644 --- a/src/channel/Channel.ts +++ b/src/channel/Channel.ts @@ -7,7 +7,8 @@ import { Participant, ServerEvents, IChannelInfo, - Notification + Notification, + UserFlags } from "../util/types"; import type { Socket } from "../ws/Socket"; import { validateChannelSettings } from "./settings"; @@ -17,7 +18,7 @@ import { ChannelList } from "./ChannelList"; import { config } from "./config"; import { saveChatHistory, getChatHistory } from "../data/history"; import { mixin } from "../util/helpers"; -import { NoteQuota } from "../ws/ratelimit/NoteQuota"; +import { readUser } from "../data/user"; interface CachedKickban { userId: string; @@ -25,9 +26,17 @@ interface CachedKickban { endTime: number; } +interface ExtraPartData { + uuids: string[]; + flags: Partial; +} + +type ExtraPart = Participant & ExtraPartData; + export class Channel extends EventEmitter { private settings: Partial; - private ppl = new Array(); + private ppl = new Array(); + public chatHistory = new Array(); private async loadChatHistory() { @@ -39,6 +48,8 @@ export class Channel extends EventEmitter { public crown?: Crown; + private flags: Record = {}; + constructor( private _id: string, set?: Partial, @@ -55,6 +66,8 @@ export class Channel extends EventEmitter { // Validate settings in set // Set the verified settings + this.logger.debug("lobby me?", this.isLobby()); + if (!this.isLobby()) { if (set) { //this.logger.debug("Passed settings:", set); @@ -185,6 +198,15 @@ export class Channel extends EventEmitter { return false; } + /** + * Determine whether this channel is a lobby with the name "lobby" in it + */ + public isTrueLobby() { + if (this.getID().match("^lobby[0-9][0-9]$") && this.getID().match("^lobby[1-9]$")) return true; + + return false; + } + /** * Change this channel's settings * @param set Channel settings @@ -265,7 +287,7 @@ export class Channel extends EventEmitter { * @param socket Socket that is joining * @returns undefined */ - public join(socket: Socket): void { + public join(socket: Socket, force: boolean = false): void { //! /!\ Players are forced to join the same channel on two different tabs! //? TODO Should this be a bug or a feature? //this.logger.debug("join triggered"); @@ -274,17 +296,28 @@ export class Channel extends EventEmitter { const part = socket.getParticipant() as Participant; let hasChangedChannel = false; - let oldChannelID = socket.currentChannelID; - // Is user banned? - if (this.isBanned(part._id)) { - // Send user to ban channel instead - // TODO Send notification for ban - const chs = ChannelList.getList(); - for (const ch of chs) { - const chid = ch.getID(); - if (chid == config.fullChannel) { - return socket.setChannel(chid) + if (!force) { + // Is user banned? + if (this.isBanned(part._id)) { + // Send user to ban channel instead + // TODO Send notification for ban + const chs = ChannelList.getList(); + for (const ch of chs) { + const chid = ch.getID(); + if (chid == config.fullChannel) { + return socket.setChannel(chid) + } + } + } + + // Is the channel full? + if (this.isFull()) { + // Is this a genuine lobby (not a test/ room)? + if (this.isTrueLobby()) { + const nextID = this.getNextIncrementationFromID(); + this.logger.debug("New ID:", nextID); + return socket.setChannel(nextID, undefined, true) } } } @@ -311,7 +344,8 @@ export class Channel extends EventEmitter { color: part.color, id: part.id, tag: part.tag, - uuids: [socket.getUUID()] + uuids: [socket.getUUID()], + flags: socket.getUserFlags() || {} }); } else { if (socket.currentChannelID !== config.fullChannel) { @@ -331,7 +365,7 @@ export class Channel extends EventEmitter { if (hasChangedChannel) { // Were they in a channel before? if (socket.currentChannelID) { - // Find the channel they were in + // Find the other channel they were in const ch = ChannelList.getList().find( ch => ch._id == socket.currentChannelID ); @@ -340,7 +374,7 @@ export class Channel extends EventEmitter { if (ch) ch.leave(socket); } - // Change the thing we checked to point to us now + // You belong here now socket.currentChannelID = this.getID(); } @@ -443,9 +477,9 @@ export class Channel extends EventEmitter { public isFull() { // TODO Use limit setting - // if (this.isLobby() && this.ppl.length >= 20) { - // return true; - // } + if (this.isTrueLobby() && this.ppl.length >= 20) { + return true; + } return false; } @@ -472,13 +506,20 @@ export class Channel extends EventEmitter { * @returns List of people */ public getParticipantList() { - return this.ppl.map(p => ({ - id: p.id, - _id: p._id, - name: p.name, - color: p.color, - tag: p.tag - })); + const ppl = []; + + for (const p of this.ppl) { + if (p.flags.vanish) continue; + ppl.push({ + _id: p._id, + name: p.name, + color: p.color, + id: p.id, + tag: config.sendTags ? p.tag : undefined + }); + } + + return ppl; } public getParticipantListUnsanitized() { @@ -626,7 +667,7 @@ export class Channel extends EventEmitter { } /** - * Drop the crown (remove from user) + * Drop the crown (reset timer, and, if applicable, remove from user's head) */ public dropCrown() { if (this.crown) { @@ -780,6 +821,11 @@ export class Channel extends EventEmitter { } } + /** + * Check if a user is banned here right now + * @param _id User ID + * @returns True if the user is banned, otherwise false + **/ public isBanned(_id: string) { const now = Date.now(); @@ -799,6 +845,9 @@ export class Channel extends EventEmitter { return false; } + /** + * Clear the chat and chat history + **/ public async clearChat() { this.chatHistory = []; await saveChatHistory(this.getID(), this.chatHistory); @@ -826,6 +875,11 @@ export class Channel extends EventEmitter { }]); } + /** + * Send a message in chat + * @param msg Chat message event to send + * @param p Participant who is "sending the message" + **/ public async sendChat(msg: ServerEvents["a"], p: Participant) { if (!msg.message) return; @@ -848,6 +902,38 @@ export class Channel extends EventEmitter { this.chatHistory.push(outgoing); await saveChatHistory(this.getID(), this.chatHistory); } + + /** + * Set a flag on this channel + * @param key Flag ID + * @param val Value of which the flag will be set to + **/ + public setFlag(key: string, val: any) { + this.flags[key] = val; + } + + /** + * Get a flag on this channel + * @param key Flag ID + * @returns Value of flag + */ + public getFlag(key: string) { + return this.flags[key]; + } + + + /** + * Get the name of this channel where the number at the end is one higher than this one, given it ends with a number + **/ + public getNextIncrementationFromID() { + try { + const id = this.getID(); + const num = parseInt((id.match(/\d+$/) as string[])[0]); + return `${id.substring(0, id.length - num.toString().length)}${num + 1}`; + } catch (err) { + return config.fullChannel; + } + } } export default Channel; diff --git a/src/channel/config.ts b/src/channel/config.ts index 94cc61e..55f8e4a 100644 --- a/src/channel/config.ts +++ b/src/channel/config.ts @@ -8,6 +8,8 @@ interface ChannelConfig { lobbyRegexes: string[]; lobbyBackdoor: string; fullChannel: string; + sendLimit: boolean; + sendTags: boolean; } export const config = loadConfig("config/channels.yml", { @@ -27,8 +29,10 @@ export const config = loadConfig("config/channels.yml", { color2: "#001014", visible: true }, - // Here's a terrifying fact: Brandon used parseInt to check lobby names in the OG server code - lobbyRegexes: ["^lobby[0-9][0-9]$", "^lobby[1-9]$", "^test/.+$"], + // Here's a terrifying fact: Brandon used parseInt to check lobby names + lobbyRegexes: ["^lobby[0-9][0-9]$", "^lobby[0-9]$", "^lobby$", "^test/.+$"], lobbyBackdoor: "lolwutsecretlobbybackdoor", - fullChannel: "test/awkward" + fullChannel: "test/awkward", + sendLimit: false, + sendTags: false, }); diff --git a/src/channel/forceLoad.ts b/src/channel/forceLoad.ts index 9c545e6..6dcf667 100644 --- a/src/channel/forceLoad.ts +++ b/src/channel/forceLoad.ts @@ -1,16 +1,41 @@ import { Channel } from "./Channel"; import { config } from "./config"; - -// This shit was moved here to try to fix the unit tests segfaulting but it didn't work +import { Logger } from "../util/Logger"; +import { ChannelList } from "./ChannelList"; // Channel forceloader (cringe) -let hasFullChannel = false; -for (const id of config.forceLoad) { - new Channel(id, undefined, undefined, undefined, true); - if (id == config.fullChannel) hasFullChannel = true; +const logger = new Logger("Channel Forceloader"); + +export function forceloadChannel(id: string) { + try { + logger.info("Forceloading", id); + new Channel(id, undefined, undefined, undefined, true); + return true; + } catch (err) { + return false; + } } -if (!hasFullChannel) { - new Channel(config.fullChannel, undefined, undefined, undefined, true); +export function unforceloadChannel(id: string) { + const ch = ChannelList.getList().find(ch => ch.getID() == id); + if (!ch) return false; + + logger.info("Unloading", id); + ch.destroy(); + + return true; +} + +export function loadDefaultForcedChannels() { + let hasFullChannel = false; + + for (const id of config.forceLoad) { + forceloadChannel(id); + if (id == config.fullChannel) hasFullChannel = true; + } + + if (!hasFullChannel) { + new Channel(config.fullChannel, undefined, undefined, undefined, true); + } } diff --git a/src/data/user.ts b/src/data/user.ts index b2bc60a..707aa18 100644 --- a/src/data/user.ts +++ b/src/data/user.ts @@ -1,20 +1,27 @@ import { User } from "@prisma/client"; import { prisma } from "./prisma"; -import { UserFlags } from "../util/types"; +import { Tag, UserFlags } from "../util/types"; export async function createUser( _id: string, name?: string, color?: string, - flags?: UserFlags + flags?: UserFlags, + tag?: Tag ) { return await prisma.user.create({ - data: { id: _id, name, color, flags: JSON.stringify(flags) } + data: { + id: _id, + name, + color, + flags: JSON.stringify(flags) || "", + tag: JSON.stringify(tag) || "" + } }); } export async function getUsers() { - return await { + return { users: await prisma.user.findMany(), count: await prisma.user.count() } diff --git a/src/index.ts b/src/index.ts index 41533d2..93a8eb4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,10 +6,12 @@ // If you don't load the server first, bun will literally segfault import "./ws/server"; -import "./channel/forceLoad"; +import { loadDefaultForcedChannels } from "./channel/forceLoad"; import { Logger } from "./util/Logger"; const logger = new Logger("Main"); +logger.info("Loading default channels..."); +loadDefaultForcedChannels(); import "./util/readline"; diff --git a/src/util/Logger.ts b/src/util/Logger.ts old mode 100755 new mode 100644 diff --git a/src/util/types.d.ts b/src/util/types.d.ts index 5fba8b7..0f64f3f 100644 --- a/src/util/types.d.ts +++ b/src/util/types.d.ts @@ -1,18 +1,5 @@ -/** - * Typedefs - */ - import { Socket } from "../ws/Socket"; -/** - * Halfway through this file, the same types have appeared again - * - * I am not a decent enough person to go looking down there, so - * good luck when you get there. Somehow I forgot what is even - * in this file, so don't come and ask me why something isn't - * defined correctly here. - */ - declare type Omit = Pick>; declare type UserFlags = Partial<{ @@ -27,9 +14,17 @@ declare type UserFlags = Partial<{ cansetcrowns: number; // new + "no note quota": number; "no note rate limit": number; "no cursor rate limit": number; "no userset rate limit": number; + mod: number; + admin: number; + vanish: number; +}>; + +type ChannelFlags = Partial<{ + limit: number; }>; declare interface Tag { @@ -110,7 +105,6 @@ declare interface Crown { endPos: Vector2; } -// Events copied from Hri7566/mppclone-client typedefs declare interface ServerEvents { a: { m: "a"; @@ -238,19 +232,40 @@ declare interface ServerEvents { remove?: true; }; + tag: { + m: "tag"; + _id: string; + tag: { + text: string; + color: string; + }; + } + clear_chat: { - m: "clear_chat" + m: "clear_chat"; }; notification: { - m: "notification" + m: "notification"; targetChannel?: string; targetUser?: string; } & Notification; restart: { - m: "restart" - } + m: "restart"; + }; + + forceload: { + m: "forceload"; + _id: string; + }; + + ch_flag: { + m: "ch_flag"; + _id?: string; + key: string; + value: any; + }; } declare interface ClientEvents { diff --git a/src/ws/Socket.ts b/src/ws/Socket.ts index d14e4c0..d3bc75a 100644 --- a/src/ws/Socket.ts +++ b/src/ws/Socket.ts @@ -123,7 +123,7 @@ export class Socket extends EventEmitter { return this.id; } - public setChannel(_id: string, set?: Partial) { + public setChannel(_id: string, set?: Partial, force: boolean = false) { if (this.isDestroyed()) return; if (this.currentChannelID === _id) { logger.debug("Guy in channel was already in"); @@ -155,7 +155,7 @@ export class Socket extends EventEmitter { this ); - channel.join(this); + channel.join(this, force); } } @@ -443,17 +443,25 @@ export class Socket extends EventEmitter { ch.playNotes(msg, this); } + private isSubscribedToChannelList = false; + public subscribeToChannelList() { + if (this.isSubscribedToChannelList) return; + ChannelList.subscribe(this.id); const firstList = ChannelList.getPublicList().map(v => v.getInfo(this._id) ); this.sendChannelList(firstList); + + this.isSubscribedToChannelList = true; } public unsubscribeFromChannelList() { + if (!this.isSubscribedToChannelList) return; ChannelList.unsubscribe(this.id); + this.isSubscribedToChannelList = false; } public sendChannelList(list: IChannelInfo[], complete: boolean = true) { @@ -476,7 +484,6 @@ export class Socket extends EventEmitter { const channel = this.getCurrentChannel(); const part = this.getParticipant(); - // this looks cool if (!channel) return false; if (!channel.crown) return false; if (!channel.crown.userId) return false; @@ -514,6 +521,13 @@ export class Socket extends EventEmitter { html: notif.html }]); } + + public setTag(text: string, color: string) { + const user = this.getUser(); + if (!user) return; + user.tag = JSON.stringify({ text, color }); + updateUser(this.getUserID(), user); + } } export const socketsBySocketID = new Map(); diff --git a/src/ws/events/admin/handlers/ch_flag.ts b/src/ws/events/admin/handlers/ch_flag.ts new file mode 100644 index 0000000..ad4ed51 --- /dev/null +++ b/src/ws/events/admin/handlers/ch_flag.ts @@ -0,0 +1,20 @@ +import { ChannelList } from "../../../../channel/ChannelList"; +import { ServerEventListener } from "../../../../util/types"; + +export const ch_flag: ServerEventListener<"ch_flag"> = { + id: "ch_flag", + callback: async (msg, socket) => { + // Edit channel flag + let chid = msg._id; + + if (typeof chid !== "string") { + const ch = socket.getCurrentChannel(); + if (!ch) return; + + chid = ch.getID(); + } + + const ch = ChannelList.getList().find(c => c.getID() == chid); + if (!ch) return; + } +}; diff --git a/src/ws/events/admin/handlers/forceload.ts b/src/ws/events/admin/handlers/forceload.ts new file mode 100644 index 0000000..562743f --- /dev/null +++ b/src/ws/events/admin/handlers/forceload.ts @@ -0,0 +1,12 @@ +import { forceloadChannel } from "../../../../channel/forceLoad"; +import { ServerEventListener } from "../../../../util/types"; + +export const forceload: ServerEventListener<"forceload"> = { + id: "forceload", + callback: async (msg, socket) => { + // Forceload channel + if (typeof msg._id !== "string") return; + + forceloadChannel(msg._id); + } +}; diff --git a/src/ws/events/admin/handlers/tag.ts b/src/ws/events/admin/handlers/tag.ts new file mode 100644 index 0000000..9bfdcf7 --- /dev/null +++ b/src/ws/events/admin/handlers/tag.ts @@ -0,0 +1,29 @@ +import { readUser, updateUser } from "../../../../data/user"; +import { ServerEventListener } from "../../../../util/types"; +import { findSocketsByUserID } from "../../../Socket"; + +export const tag: ServerEventListener<"tag"> = { + id: "tag", + callback: async (msg, socket) => { + // Change someone else's tag + const id = msg._id; + const tag = msg.tag; + + if (typeof id !== "string") return; + if (typeof tag !== "object") return; + if (typeof tag["text"] !== "string") return; + if (typeof tag.color !== "string") return; + if (!tag.color.match(/^#[0-9a-f]{6}$/i)) return; + + const user = await readUser(msg._id); + if (!user) return; + + user.tag = JSON.stringify(tag); + await updateUser(id, user); + + const toUpdate = findSocketsByUserID(id); + toUpdate.forEach(s => { + s.setTag(msg.tag.text, msg.tag.color); + }); + } +}; diff --git a/src/ws/events/user/handlers/+ls.ts b/src/ws/events/user/handlers/+ls.ts index 41addeb..e3b22cf 100644 --- a/src/ws/events/user/handlers/+ls.ts +++ b/src/ws/events/user/handlers/+ls.ts @@ -12,6 +12,10 @@ export const plus_ls: ServerEventListener<"+ls"> = { // and when I see their cursor disappear I'll know // precisely where they went to follow them and to // annoy them in chat when I see them again. + if (socket.rateLimits) { + if (!socket.rateLimits.normal["+ls"].attempt()) return; + } + socket.subscribeToChannelList(); } }; diff --git a/src/ws/events/user/handlers/-ls.ts b/src/ws/events/user/handlers/-ls.ts index 00b66ad..d9cbb33 100644 --- a/src/ws/events/user/handlers/-ls.ts +++ b/src/ws/events/user/handlers/-ls.ts @@ -4,6 +4,10 @@ export const minus_ls: ServerEventListener<"-ls"> = { id: "-ls", callback: (msg, socket) => { // Stop giving us the latest server forecast + if (socket.rateLimits) { + if (!socket.rateLimits.normal["-ls"].attempt()) return; + } + socket.unsubscribeFromChannelList(); } }; diff --git a/src/ws/events/user/handlers/admin_message.ts b/src/ws/events/user/handlers/admin_message.ts index 72a2165..89a60ed 100644 --- a/src/ws/events/user/handlers/admin_message.ts +++ b/src/ws/events/user/handlers/admin_message.ts @@ -1,13 +1,13 @@ -import { Logger } from "../../../../util/Logger"; import env from "../../../../util/env"; import { ServerEventListener } from "../../../../util/types"; -import { config } from "../../../usersConfig"; - -const logger = new Logger("Admin Message Handler"); export const admin_message: ServerEventListener<"admin message"> = { id: "admin message", callback: (msg, socket) => { + // Administrator control message + if (socket.rateLimits) + if (!socket.rateLimits.normal["admin message"].attempt()) return; + if (typeof msg.password !== "string") return; if (msg.password !== env.ADMIN_PASS) return; diff --git a/src/ws/events/user/handlers/bye.ts b/src/ws/events/user/handlers/bye.ts index 6f2ea12..425b999 100644 --- a/src/ws/events/user/handlers/bye.ts +++ b/src/ws/events/user/handlers/bye.ts @@ -4,6 +4,9 @@ export const bye: ServerEventListener<"bye"> = { id: "bye", callback: (msg, socket) => { // Leave server + if (socket.rateLimits) + if (!socket.rateLimits.normal.bye.attempt()) return; + socket.destroy(); } }; diff --git a/src/ws/events/user/handlers/chown.ts b/src/ws/events/user/handlers/chown.ts index 54cf4da..476e98e 100644 --- a/src/ws/events/user/handlers/chown.ts +++ b/src/ws/events/user/handlers/chown.ts @@ -7,6 +7,9 @@ export const chown: ServerEventListener<"chown"> = { id: "chown", callback: (msg, socket) => { // Change channel ownership + if (socket.rateLimits) + if (!socket.rateLimits.normal["chown"].attempt()) return; + const ch = socket.getCurrentChannel(); if (!ch) return; diff --git a/src/ws/events/user/handlers/chset.ts b/src/ws/events/user/handlers/chset.ts index da8be3e..6c6094a 100644 --- a/src/ws/events/user/handlers/chset.ts +++ b/src/ws/events/user/handlers/chset.ts @@ -4,6 +4,9 @@ export const chset: ServerEventListener<"chset"> = { id: "chset", callback: (msg, socket) => { // Change channel settings + if (socket.rateLimits) + if (!socket.rateLimits.chains.chset.attempt()) return; + if (typeof msg.set == "undefined") return; const ch = socket.getCurrentChannel(); diff --git a/src/ws/events/user/handlers/devices.ts b/src/ws/events/user/handlers/devices.ts index 68ce49e..a4cba4f 100644 --- a/src/ws/events/user/handlers/devices.ts +++ b/src/ws/events/user/handlers/devices.ts @@ -4,6 +4,9 @@ export const devices: ServerEventListener<"devices"> = { id: "devices", callback: (msg, socket) => { // List of MIDI Devices (or software ports, because nobody can tell the difference) + if (socket.rateLimits) + if (!socket.rateLimits.normal.devices.attempt()) return; + if (socket.gateway.hasSentDevices) return; socket.gateway.hasSentDevices = true; } diff --git a/src/ws/events/user/handlers/hi.ts b/src/ws/events/user/handlers/hi.ts index 43dcd31..c22a3b6 100644 --- a/src/ws/events/user/handlers/hi.ts +++ b/src/ws/events/user/handlers/hi.ts @@ -5,9 +5,10 @@ export const hi: ServerEventListener<"hi"> = { callback: (msg, socket) => { // Handshake message // TODO Hi message tokens - // I'm not actually sure if I'm up for doing tokens, - // but if someone wants to submit a pull request, I - // look forward to watching you do all the work + + if (socket.rateLimits) + if (!socket.rateLimits.normal.hi.attempt()) return; + if (socket.gateway.hasProcessedHi) return; let part = socket.getParticipant(); diff --git a/src/ws/events/user/handlers/m.ts b/src/ws/events/user/handlers/m.ts index fbfa419..33c92bb 100644 --- a/src/ws/events/user/handlers/m.ts +++ b/src/ws/events/user/handlers/m.ts @@ -4,8 +4,10 @@ export const m: ServerEventListener<"m"> = { id: "m", callback: (msg, socket) => { // Cursor movement - if (!socket.rateLimits) return; - if (!socket.rateLimits.normal.m.attempt()) return; + if (socket.rateLimits) { + if (!socket.rateLimits.normal.m.attempt()) return; + } + if (!msg.x || !msg.y) return; let x = msg.x; diff --git a/src/ws/events/user/handlers/n.ts b/src/ws/events/user/handlers/n.ts index f4b14e8..2776bf2 100644 --- a/src/ws/events/user/handlers/n.ts +++ b/src/ws/events/user/handlers/n.ts @@ -5,6 +5,9 @@ export const n: ServerEventListener<"n"> = { id: "n", callback: (msg, socket) => { // Piano note + if (socket.rateLimits) + if (!socket.rateLimits.chains.n.attempt()) return; + if (!Array.isArray(msg.n)) return; if (typeof msg.t !== "number") return; diff --git a/src/ws/events/user/handlers/t.ts b/src/ws/events/user/handlers/t.ts index 6f5a7e5..df030e0 100644 --- a/src/ws/events/user/handlers/t.ts +++ b/src/ws/events/user/handlers/t.ts @@ -4,11 +4,15 @@ export const t: ServerEventListener<"t"> = { id: "t", callback: (msg, socket) => { // Ping + + if (socket.rateLimits) + if (!socket.rateLimits.normal.t.attempt()) return + if (msg.e) { if (typeof msg.e !== "number") return; } - // Pong! + // Pong socket.sendArray([ { m: "t", diff --git a/src/ws/ratelimit/config.ts b/src/ws/ratelimit/config.ts index 1ab1cf3..14eb745 100644 --- a/src/ws/ratelimit/config.ts +++ b/src/ws/ratelimit/config.ts @@ -7,14 +7,26 @@ export interface RateLimitConfigList< RLC = { num: number; interval: number } > { normal: { - m: RL; a: RL; + m: RL; ch: RL; kickban: RL; + t: RL; + "+ls": RL; + "-ls": RL; + chown: RL; + + // weird limits + hi: RL; + bye: RL; + devices: RL; + "admin message": RL; }; chains: { userset: RLC; + chset: RLC; + n: RLC; // not to be confused with NoteQuota }; } @@ -37,12 +49,29 @@ export const config = loadConfig("config/ratelimits.yml", { a: 6000 / 4, m: 1000 / 20, ch: 1000 / 1, - kickban: 1000 / 4 + kickban: 1000 / 8, + t: 1000 / 128, + "+ls": 1000 / 60, + "-ls": 1000 / 60, + chown: 2000, + + hi: 1000 / 20, + bye: 1000 / 20, + devices: 1000 / 20, + "admin message": 1000 / 20 }, chains: { userset: { interval: 1000 * 60 * 30, num: 1000 + }, + chset: { + interval: 1000 * 60 * 30, + num: 1024 + }, + n: { + interval: 1000, + num: 512 } } }, @@ -51,12 +80,29 @@ export const config = loadConfig("config/ratelimits.yml", { a: 6000 / 10, m: 1000 / 20, ch: 1000 / 1, - kickban: 1000 / 4 + kickban: 1000 / 8, + t: 1000 / 128, + "+ls": 1000 / 60, + "-ls": 1000 / 60, + chown: 2000, + + hi: 1000 / 20, + bye: 1000 / 20, + devices: 1000 / 20, + "admin message": 1000 / 20 }, chains: { userset: { interval: 1000 * 60 * 30, num: 1000 + }, + chset: { + interval: 1000 * 60 * 30, + num: 1024 + }, + n: { + interval: 1000, + num: 512 } } }, @@ -65,12 +111,29 @@ export const config = loadConfig("config/ratelimits.yml", { a: 6000 / 50, m: 1000 / 60, ch: 1000 / 10, - kickban: 1000 / 32 + kickban: 1000 / 60, + t: 1000 / 256, + "+ls": 1000 / 60, + "-ls": 1000 / 60, + chown: 500, + + hi: 1000 / 20, + bye: 1000 / 20, + devices: 1000 / 20, + "admin message": 1000 / 60 }, chains: { userset: { - interval: 1000 * 60 * 30, + interval: 500, num: 1000 + }, + chset: { + interval: 1000 * 60 * 30, + num: 1024 + }, + n: { + interval: 50, + num: 512 } } } diff --git a/src/ws/ratelimit/limits/admin.ts b/src/ws/ratelimit/limits/admin.ts index 817d205..395576c 100644 --- a/src/ws/ratelimit/limits/admin.ts +++ b/src/ws/ratelimit/limits/admin.ts @@ -7,13 +7,32 @@ export const adminLimits: RateLimitConstructorList = { a: () => new RateLimit(config.admin.normal.a), m: () => new RateLimit(config.admin.normal.m), ch: () => new RateLimit(config.admin.normal.ch), - kickban: () => new RateLimit(config.admin.normal.kickban) + kickban: () => new RateLimit(config.crown.normal.kickban), + t: () => new RateLimit(config.crown.normal.t), + "+ls": () => new RateLimit(config.crown.normal["+ls"]), + "-ls": () => new RateLimit(config.crown.normal["-ls"]), + chown: () => new RateLimit(config.crown.normal.chown), + + hi: () => new RateLimit(config.crown.normal.hi), + bye: () => new RateLimit(config.crown.normal.bye), + devices: () => new RateLimit(config.crown.normal.devices), + "admin message": () => new RateLimit(config.crown.normal["admin message"]) }, chains: { userset: () => new RateLimitChain( config.admin.chains.userset.interval, config.admin.chains.userset.num + ), + chset: () => + new RateLimitChain( + config.crown.chains.chset.interval, + config.crown.chains.userset.num + ), + n: () => + new RateLimitChain( + config.crown.chains.n.interval, + config.crown.chains.userset.num ) } }; diff --git a/src/ws/ratelimit/limits/crown.ts b/src/ws/ratelimit/limits/crown.ts index 977fd75..480a1e4 100644 --- a/src/ws/ratelimit/limits/crown.ts +++ b/src/ws/ratelimit/limits/crown.ts @@ -6,13 +6,33 @@ export const crownLimits: RateLimitConstructorList = { normal: { a: () => new RateLimit(config.crown.normal.a), m: () => new RateLimit(config.crown.normal.m), - ch: () => new RateLimit(config.crown.normal.ch) + ch: () => new RateLimit(config.crown.normal.ch), + kickban: () => new RateLimit(config.crown.normal.kickban), + t: () => new RateLimit(config.crown.normal.t), + "+ls": () => new RateLimit(config.crown.normal["+ls"]), + "-ls": () => new RateLimit(config.crown.normal["-ls"]), + chown: () => new RateLimit(config.crown.normal.chown), + + hi: () => new RateLimit(config.crown.normal.hi), + bye: () => new RateLimit(config.crown.normal.bye), + devices: () => new RateLimit(config.crown.normal.devices), + "admin message": () => new RateLimit(config.crown.normal["admin message"]) }, chains: { userset: () => new RateLimitChain( config.crown.chains.userset.interval, config.crown.chains.userset.num + ), + chset: () => + new RateLimitChain( + config.crown.chains.chset.interval, + config.crown.chains.userset.num + ), + n: () => + new RateLimitChain( + config.crown.chains.n.interval, + config.crown.chains.userset.num ) } }; diff --git a/src/ws/ratelimit/limits/user.ts b/src/ws/ratelimit/limits/user.ts index 27c0210..3b53c98 100644 --- a/src/ws/ratelimit/limits/user.ts +++ b/src/ws/ratelimit/limits/user.ts @@ -2,19 +2,37 @@ import { RateLimit } from "../RateLimit"; import { RateLimitChain } from "../RateLimitChain"; import { RateLimitConstructorList, config } from "../config"; -// All this does is instantiate rate limits from the config export const userLimits: RateLimitConstructorList = { normal: { a: () => new RateLimit(config.user.normal.a), m: () => new RateLimit(config.user.normal.m), ch: () => new RateLimit(config.user.normal.ch), - kickban: () => new RateLimit(config.user.normal.kickban) + kickban: () => new RateLimit(config.user.normal.kickban), + t: () => new RateLimit(config.user.normal.t), + "+ls": () => new RateLimit(config.user.normal["+ls"]), + "-ls": () => new RateLimit(config.user.normal["-ls"]), + chown: () => new RateLimit(config.user.normal.chown), + + hi: () => new RateLimit(config.user.normal.hi), + bye: () => new RateLimit(config.user.normal.bye), + devices: () => new RateLimit(config.user.normal.devices), + "admin message": () => new RateLimit(config.user.normal["admin message"]) }, chains: { userset: () => new RateLimitChain( config.user.chains.userset.interval, config.user.chains.userset.num + ), + chset: () => + new RateLimitChain( + config.user.chains.chset.interval, + config.user.chains.userset.num + ), + n: () => + new RateLimitChain( + config.user.chains.n.interval, + config.user.chains.userset.num ) } };