From 78fc178652d15e57f87a7bd7c1a091507edae312 Mon Sep 17 00:00:00 2001 From: Hri7566 Date: Tue, 13 Aug 2024 05:55:27 -0400 Subject: [PATCH] Create docker build config --- .dockerignore | 13 +++ Dockerfile | 34 ++++++++ README.md | 9 +- bun.lockb | Bin 74596 -> 76003 bytes config/channels.yml | 19 +++++ config/prometheus.yml | 12 +++ config/users.yml | 5 +- config/util.yml | 1 + package.json | 7 +- src/channel/Channel.ts | 36 +++++--- src/channel/config.ts | 4 +- src/index.ts | 36 +++++--- src/util/Logger.ts | 45 ++++++++-- src/util/config.ts | 2 +- src/util/id.ts | 3 + src/util/metrics.ts | 37 ++++++++ src/util/readline/index.ts | 39 +++++---- src/util/utilConfig.ts | 5 ++ src/ws/Gateway.ts | 56 ++++++------ src/ws/Socket.ts | 15 +++- src/ws/events/user/handlers/+ls.ts | 2 + src/ws/events/user/handlers/a.ts | 31 ++++++- src/ws/events/user/handlers/admin_message.ts | 11 ++- src/ws/events/user/handlers/hi.ts | 20 ++++- src/ws/events/user/handlers/m.ts | 18 +++- src/ws/events/user/handlers/n.ts | 2 + src/ws/events/user/handlers/t.ts | 2 + src/ws/events/user/handlers/userset.ts | 21 ++--- src/ws/server.ts | 85 ++++++++++++------- 29 files changed, 441 insertions(+), 129 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 config/prometheus.yml create mode 100644 config/util.yml create mode 100644 src/util/metrics.ts create mode 100644 src/util/utilConfig.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..beaace2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +node_modules +Dockerfile* +docker-compose* +.dockerignore +.git +.gitignore +README.md +LICENSE +.vscode +.env.template +.gitmodules +.prettierrc +.eslintrc.js diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..436da2d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +FROM oven/bun:latest AS base +WORKDIR /usr/src/app + +FROM base AS install +RUN mkdir -p /temp/dev +COPY package.json bun.lockb /temp/dev/ +RUN cd /temp/dev && bun install --frozen-lockfile + +RUN mkdir -p /temp/prod +COPY package.json bun.lockb /temp/prod +RUN cd /temp/prod && bun install --frozen-lockfile --production + +FROM base AS prerelease +COPY --from=install /temp/dev/node_modules node_modules +COPY . . + +ENV NODE_ENV=production +#RUN bun test +#RUN bun build + +FROM base AS release +COPY --from=install /temp/prod/node_modules node_modules +COPY --from=prerelease /usr/src/app/src/ ./src +COPY --from=prerelease /usr/src/app/package.json . +COPY --from=prerelease /usr/src/app/config ./config +COPY --from=prerelease /usr/src/app/public ./public +COPY --from=prerelease /usr/src/app/mppkey ./mppkey +COPY --from=prerelease /usr/src/app/tsconfig.json . +COPY --from=prerelease /usr/src/app/prisma ./prisma +COPY --from=prerelease /usr/src/app/.env . + +USER bun +EXPOSE 8443/tcp +ENTRYPOINT [ "bun", "." ] diff --git a/README.md b/README.md index 2f37d81..f0c0f6f 100644 --- a/README.md +++ b/README.md @@ -66,9 +66,6 @@ This has always been the future intention of this project. ## TODO -- Fully implement and test tags - - Tags are sent to clients now - - Check if tags are sent to everyone - Channel data saving - Permission groups and permissions - Probable permission groups: owner, admin, mod, trialmod, default @@ -104,6 +101,12 @@ This has always been the future intention of this project. - Check for different messages? - Check for URL? - Notifications for server-generated XSS? +- Migrate to PostgreSQL instead of SQLite + - Likely a low priority, we use prisma anyway, but it would be nice to have a server +- Implement user caching + - Skip redis due to the infamous licensing issues + - Probably use a simple in-memory cache + - Likely store with leveldb or JSON ## How to run diff --git a/bun.lockb b/bun.lockb index bb2cffcddde470500acd95e09aadd8a92e8dd8c3..a666ad28106aa49919831846e22ed4ba64952754 100755 GIT binary patch delta 13205 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 Yo7nrB-}Z)D=o1xiXSGfKd#LTd03w(CRR910 delta 12419 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 K?byYHz<&XYZ}M#b diff --git a/config/channels.yml b/config/channels.yml index 81674d0..ac2e97d 100644 --- a/config/channels.yml +++ b/config/channels.yml @@ -1,8 +1,11 @@ # Channel config file +# Which channels to keep loaded on startup forceLoad: - lobby - test/awkward + +# Default settings for lobbies lobbySettings: lobby: true chat: true @@ -10,19 +13,35 @@ lobbySettings: visible: true color: "#73b3cc" color2: "#273546" + +# Default settings for regular channels defaultSettings: chat: true crownsolo: false color: "#3b5054" color2: "#001014" visible: true + +# Regexes to match against channel names to determine whether they are lobbies or not +# This doesn't affect the `isRealLobby` function, which is used to determine "classic" lobbies lobbyRegexes: - ^lobby[0-9][0-9]$ - ^lobby[0-9]$ - ^lobby$ - ^lobbyNaN$ - ^test/.+$ + +# Backdoor channel ID for bypassing the lobby limit lobbyBackdoor: lolwutsecretlobbybackdoor + +# Channel ID for where you get sent when you join a channel that is full/you get banned/etc fullChannel: test/awkward + +# Whether to send the channel limit to the client sendLimit: false + +# Whether to give the crown to the user who had it when they rejoin chownOnRejoin: true + +# Time in milliseconds to wait before destroying an empty channel +channelDestroyTimeout: 1000 diff --git a/config/prometheus.yml b/config/prometheus.yml new file mode 100644 index 0000000..6687145 --- /dev/null +++ b/config/prometheus.yml @@ -0,0 +1,12 @@ +global: + scrape_interval: 15s + +scrape_configs: + - job_name: prometheus + scrape_interval: "5s" + static_configs: + - targets: ["localhost:9090"] + + - job_name: mpp + static_configs: + - targets: ["192.168.1.24:9100"] diff --git a/config/users.yml b/config/users.yml index 6e2012f..383fedd 100644 --- a/config/users.yml +++ b/config/users.yml @@ -37,14 +37,15 @@ enableAdminEval: true # The token validation scheme. Valid values are "none", "jwt" and "uuid". # This server will still validate existing tokens generated with other schemes if not set to "none", mimicking MPP.net's server. # This is set to "none" by default because MPP.com does not have a token system. -tokenAuth: none +tokenAuth: jwt # The browser challenge scheme. Valid options are "none", "obf" and "basic". # This is to change what is sent in the "b" message. # "none" will disable the browser challenge, # "obf" will sent an obfuscated function to the client, # and "basic" will just send a simple function that expects a boolean. -browserChallenge: none +# FIXME Note that "obf" is not implemented yet, and has undefined behavior. +browserChallenge: basic # Scheme for generating user IDs. # Valid options are "random", "sha256", "mpp" and "uuid". diff --git a/config/util.yml b/config/util.yml new file mode 100644 index 0000000..19dd417 --- /dev/null +++ b/config/util.yml @@ -0,0 +1 @@ +enableLogFiles: true diff --git a/package.json b/package.json index 8891890..6e61e92 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,10 @@ "keywords": [], "author": "Hri7566", "license": "ISC", + "scripts": { + "start": "bun run src/index.ts", + "dev": "bun run src/index.ts --watch" + }, "dependencies": { "@prisma/client": "5.17.0", "@t3-oss/env-core": "^0.6.1", @@ -17,6 +21,7 @@ "jsonwebtoken": "^9.0.2", "keccak": "^2.1.0", "nunjucks": "^3.2.4", + "prom-client": "^15.1.3", "unique-names-generator": "^4.7.1", "yaml": "^2.5.0", "zod": "^3.23.8" @@ -32,4 +37,4 @@ "prisma": "5.17.0", "typescript": "^5.5.4" } -} \ No newline at end of file +} diff --git a/src/channel/Channel.ts b/src/channel/Channel.ts index d1c5b34..045a18b 100644 --- a/src/channel/Channel.ts +++ b/src/channel/Channel.ts @@ -68,7 +68,7 @@ export class Channel extends EventEmitter { } private async save() { - this.logger.debug("Saving channel data"); + //this.logger.debug("Saving channel data"); try { const info = this.getInfo(); @@ -78,16 +78,16 @@ export class Channel extends EventEmitter { flags: JSON.stringify(this.flags) }; - this.logger.debug("Channel data to save:", data); + //this.logger.debug("Channel data to save:", data); await saveChannel(this.getID(), data); } catch (err) { - this.logger.debug("Error saving cannel:", err); + this.logger.warn("Error saving channel data:", err); } } private async load() { - this.logger.debug("Loading saved data"); + //this.logger.debug("Loading saved data"); try { const data = await getSavedChannel(this.getID()); if (data) { @@ -100,9 +100,11 @@ export class Channel extends EventEmitter { forceloadChannel(this.getID()); } - this.logger.debug("Loaded channel data:", data); + //this.logger.debug("Loaded channel data:", data); + + this.emit("update", this); } catch (err) { - this.logger.debug("Error loading channel data:", err); + this.logger.error("Error loading channel data:", err); } } } catch (err) { } @@ -125,7 +127,7 @@ export class Channel extends EventEmitter { ) { super(); - this.logger = new Logger("Channel - " + _id); + this.logger = new Logger("Channel - " + _id, "logs/channel"); this.settings = {}; // Copy default settings @@ -209,7 +211,13 @@ export class Channel extends EventEmitter { } if (this.ppl.length == 0 && !this.stays) { - this.destroy(); + if (config.channelDestroyTimeout) { + setTimeout(() => { + this.destroy(); + }, config.channelDestroyTimeout); + } else { + this.destroy(); + } } }); @@ -246,17 +254,21 @@ export class Channel extends EventEmitter { .replace(/(\p{Mc}{5})\p{Mc}+/gu, "$1") .trim(); + const part = socket.getParticipant() as Participant; + let outgoing: ClientEvents["a"] = { m: "a", a: msg.message, t: Date.now(), - p: socket.getParticipant() as Participant + p: part }; this.sendArray([outgoing]); this.chatHistory.push(outgoing); await saveChatHistory(this.getID(), this.chatHistory); + this.logger.info(`${part._id} ${part.name}: ${outgoing.a}`); + if (msg.message.startsWith("/")) { this.emit("command", msg, socket); } @@ -373,6 +385,10 @@ export class Channel extends EventEmitter { } ]); }); + + this.on("set owner id", id => { + this.setFlag("owner_id", id); + }); } /** @@ -450,7 +466,7 @@ export class Channel extends EventEmitter { // Set the verified settings for (const key of Object.keys(validatedSet)) { - this.logger.debug(`${key}: ${(validatedSet as any)[key]}`); + //this.logger.debug(`${key}: ${(validatedSet as any)[key]}`); if ((validatedSet as any)[key] === false) continue; (this.settings as any)[key] = (set as any)[key]; } diff --git a/src/channel/config.ts b/src/channel/config.ts index 8fbc82f..63563bb 100644 --- a/src/channel/config.ts +++ b/src/channel/config.ts @@ -10,6 +10,7 @@ interface ChannelConfig { fullChannel: string; sendLimit: boolean; chownOnRejoin: boolean; + channelDestroyTimeout: number; } export const config = loadConfig("config/channels.yml", { @@ -34,5 +35,6 @@ export const config = loadConfig("config/channels.yml", { lobbyBackdoor: "lolwutsecretlobbybackdoor", fullChannel: "test/awkward", sendLimit: false, - chownOnRejoin: true + chownOnRejoin: true, + channelDestroyTimeout: 1000 }); diff --git a/src/index.ts b/src/index.ts index de0060b..ee61822 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,15 @@ /** * MPP Server 2 - * for mpp.dev - * by Hri7566 + * for https://www.multiplayerpiano.dev/ + * Written by Hri7566 + * This code is licensed under the GNU General Public License v3.0. + * Please see `./LICENSE` for more information. */ +/** + * Main entry point for the server + **/ + // There are a lot of unhinged bs comments in this repo // Pay no attention to the ones that cuss you out @@ -11,15 +17,23 @@ import "./ws/server"; import { loadForcedStartupChannels } from "./channel/forceLoad"; import { Logger } from "./util/Logger"; +import { startReadline } from "./util/readline"; +import { startMetricsServer } from "./util/metrics"; -// Let's construct an entire object just for one thing to be printed -// and then keep it in memory for the entirety of runtime -const logger = new Logger("Main"); -logger.info("Forceloading startup channels..."); -loadForcedStartupChannels(); +// wrapper for some reason +export function startServer() { + // Let's construct an entire object just for one thing to be printed + // and then keep it in memory for the entirety of runtime + const logger = new Logger("Main"); + logger.info("Forceloading startup channels..."); + loadForcedStartupChannels(); -// This literally breaks editors and they stick all the imports here instead of at the top -import "./util/readline"; + // Break the console + startReadline(); -// Nevermind we use it twice -logger.info("Ready"); + // Nevermind, two things are printed + logger.info("Ready"); +} + +startServer(); +startMetricsServer(); diff --git a/src/util/Logger.ts b/src/util/Logger.ts index d07c7e5..670d286 100644 --- a/src/util/Logger.ts +++ b/src/util/Logger.ts @@ -1,8 +1,19 @@ import EventEmitter from "events"; import { padNum, unimportant } from "./helpers"; +import { join } from "path"; +import { existsSync, mkdirSync, appendFile, writeFile } from "fs"; +import { config } from "./utilConfig"; export const logEvents = new EventEmitter(); +const logFolder = "./logs"; +// https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python +const logRegex = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g; + +if (config.enableLogFiles) { + if (!existsSync(logFolder)) mkdirSync(logFolder); +} + /** * A logger that doesn't fuck with the readline prompt * timestamps are likely wrong because of js timezones @@ -14,7 +25,7 @@ export class Logger { * @param method The method from `console` to use * @param args The data to print **/ - private static log(method: string, ...args: any[]) { + private static log(method: string, logPath: string, ...args: any[]) { // Un-fuck the readline prompt process.stdout.write("\x1b[2K\r"); @@ -33,6 +44,23 @@ export class Logger { // Emit the log event for remote consoles logEvents.emit("log", method, unimportant(this.getDate()), unimportant(this.getHHMMSSMS()), args); + + if (config.enableLogFiles) { + // Write to file + (async () => { + const orig = unimportant(this.getDate()) + " " + unimportant(this.getHHMMSSMS()) + " " + args.join(" ") + "\n" + const text = orig.replace(logRegex, ""); + if (!existsSync(logPath)) { + writeFile(logPath, text, (err) => { + if (err) console.error(err); + }); + } else { + appendFile(logPath, text, (err) => { + if (err) console.error(err); + }); + } + })(); + } } /** @@ -62,14 +90,19 @@ export class Logger { return new Date().toISOString().split("T")[0]; } - constructor(public id: string) { } + public logPath: string; + + constructor(public id: string, logdir: string = logFolder) { + if (!existsSync(logdir)) mkdirSync(logdir); + this.logPath = join(logdir, `${encodeURIComponent(this.id)}.log`); + } /** * Print an info message * @param args The data to print **/ public info(...args: any[]) { - Logger.log("log", `[${this.id}]`, `\x1b[34m[info]\x1b[0m`, ...args); + Logger.log("log", this.logPath, `[${this.id}]`, `\x1b[34m[info]\x1b[0m`, ...args); } /** @@ -77,7 +110,7 @@ export class Logger { * @param args The data to print **/ public error(...args: any[]) { - Logger.log("error", `[${this.id}]`, `\x1b[31m[error]\x1b[0m`, ...args); + Logger.log("error", this.logPath, `[${this.id}]`, `\x1b[31m[error]\x1b[0m`, ...args); } /** @@ -85,7 +118,7 @@ export class Logger { * @param args The data to print **/ public warn(...args: any[]) { - Logger.log("warn", `[${this.id}]`, `\x1b[33m[warn]\x1b[0m`, ...args); + Logger.log("warn", this.logPath, `[${this.id}]`, `\x1b[33m[warn]\x1b[0m`, ...args); } /** @@ -93,6 +126,6 @@ export class Logger { * @param args The data to print **/ public debug(...args: any[]) { - Logger.log("debug", `[${this.id}]`, `\x1b[32m[debug]\x1b[0m`, ...args); + Logger.log("debug", this.logPath, `[${this.id}]`, `\x1b[32m[debug]\x1b[0m`, ...args); } } diff --git a/src/util/config.ts b/src/util/config.ts index 8ede627..8eb8fb5 100644 --- a/src/util/config.ts +++ b/src/util/config.ts @@ -1,6 +1,5 @@ import { existsSync, readFileSync, writeFileSync } from "fs"; import { parse, stringify } from "yaml"; -import { z } from "zod"; /** * This file uses the synchronous functions from the fs @@ -63,6 +62,7 @@ export function loadConfig(configPath: string, defaultConfig: T): T { return config as T; } else { // Write default config to disk and use that + //logger.warn(`Config file "${configPath}" not found, writing default config to disk`); writeConfig(configPath, defaultConfig); return defaultConfig as T; } diff --git a/src/util/id.ts b/src/util/id.ts index 714b4fc..3939ade 100644 --- a/src/util/id.ts +++ b/src/util/id.ts @@ -35,6 +35,9 @@ export function createUserID(ip: string) { .update("::ffff:" + ip + env.SALT) .digest("hex") .substring(0, 24); + } else { + // Fallback if someone typed random garbage in the config + return createID(); } } diff --git a/src/util/metrics.ts b/src/util/metrics.ts new file mode 100644 index 0000000..1171c0c --- /dev/null +++ b/src/util/metrics.ts @@ -0,0 +1,37 @@ +import client, { Registry } from "prom-client"; +import { Logger } from "./Logger"; + +const logger = new Logger("Metrics Server"); + +export function startMetricsServer() { + client.collectDefaultMetrics(); + logger.info("Starting Prometheus metrics server..."); + + const server = Bun.serve({ + port: 9100, + async fetch(req) { + const res = new Response(await client.register.metrics()); + res.headers.set("Content-Type", client.register.contentType); + return res; + } + }); + + enableMetrics(); +} + +export const metrics = { + concurrentUsers: new client.Histogram({ + name: "concurrent_users", + help: "Number of concurrent users", + }), + callbacks: [], + addCallback(callback: (...args: any[]) => void | Promise) { + (this.callbacks as ((...args: any[]) => void)[]).push(callback); + } +} + +function enableMetrics() { + setInterval(() => { + (metrics.callbacks as ((...args: any[]) => void)[]).forEach(callback => callback()); + }, 5000); +} diff --git a/src/util/readline/index.ts b/src/util/readline/index.ts index d68ca78..c57528b 100644 --- a/src/util/readline/index.ts +++ b/src/util/readline/index.ts @@ -3,23 +3,32 @@ import logger from "./logger"; import Command from "./Command"; import "./commands"; -export const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout -}); +export let rl: readline.Interface; -rl.setPrompt("mpps> "); -rl.prompt(); +// Turned into a function so the import isn't in a weird spot +export function startReadline() { + rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); -rl.on("line", async line => { - const out = await Command.handleCommand(line); - logger.info(out); + rl.setPrompt("mpps> "); rl.prompt(); -}); -rl.on("SIGINT", () => { - process.exit(); -}); + rl.on("line", async line => { + const out = await Command.handleCommand(line); + logger.info(out); + rl.prompt(); + }); -// Fucking cringe but it works -(globalThis as unknown as any).rl = rl; + rl.on("SIGINT", () => { + process.exit(); + }); + + // Fucking cringe but it works + (globalThis as unknown as any).rl = rl; +} + +export function stopReadline() { + rl.close(); +} diff --git a/src/util/utilConfig.ts b/src/util/utilConfig.ts new file mode 100644 index 0000000..85b8ec1 --- /dev/null +++ b/src/util/utilConfig.ts @@ -0,0 +1,5 @@ +import { loadConfig } from "./config"; + +export const config = loadConfig("config/util.yml", { + enableLogFiles: true +}); diff --git a/src/ws/Gateway.ts b/src/ws/Gateway.ts index afb373f..b6f945a 100644 --- a/src/ws/Gateway.ts +++ b/src/ws/Gateway.ts @@ -11,65 +11,71 @@ * or, you know, maybe I could log their user agent * and IP address instead sometime in the future. */ + +import { Logger } from "../util/Logger"; + +const logger = new Logger("Socket Gateway"); + export class Gateway { // Whether we have correctly processed this socket's hi message - public hasProcessedHi = false; + public hasProcessedHi = false; // implemented // Whether they have sent the MIDI devices message - public hasSentDevices = false; + public hasSentDevices = false; // implemented // Whether they have sent a token - public hasSentToken = false; + public hasSentToken = false; // implemented // Whether their token is valid - public isTokenValid = false; + public isTokenValid = false; // implemented // Their user agent, if sent - public userAgent = ""; + public userAgent = ""; // TODO // Whether they have moved their cursor - public hasCursorMoved = false; + public hasCursorMoved = false; // implemented // Whether they sent a cursor message that contained numbers instead of stringified numbers - public isCursorNotString = false; + public isCursorNotString = false; // implemented // The last time they sent a ping message - public lastPing = Date.now(); + public lastPing = Date.now(); // implemented // Whether they have joined any channel - public hasJoinedAnyChannel = false; + public hasJoinedAnyChannel = false; // implemented - // Whether they have joined the lobby - public hasJoinedLobby = false; + // Whether they have joined a lobby + public hasJoinedLobby = false; // implemented // Whether they have made a regular non-websocket request to the HTTP server // probably useful for checking if they are actually on the site // Maybe not useful if cloudflare is being used // In that scenario, templating wouldn't work, either - public hasConnectedToHTTPServer = false; + public hasConnectedToHTTPServer = false; // implemented // Various chat message flags - public hasSentChatMessage = false; - public hasSentChatMessageWithCapitalLettersOnly = false; - public hasSentChatMessageWithInvisibleCharacters = false; - public hasSentChatMessageWithEmoji = false; + public hasSentChatMessage = false; // implemented + public hasSentChatMessageWithCapitalLettersOnly = false; // implemented + public hasSentChatMessageWithInvisibleCharacters = false; // implemented + public hasSentChatMessageWithEmoji = false; // implemented // Whehter or not the user has played the piano in this session - public hasPlayedPianoBefore = false; + public hasPlayedPianoBefore = false; // implemented // Whether the user has sent a channel list subscription request, a.k.a. opened the channel list - public hasOpenedChannelList = false; + public hasOpenedChannelList = false; // implemented // Whether the user has changed their name/color this session (not just changed from default) - public hasChangedName = false; - public hasChangedColor = false; - - // Whether the user has sent - public hasSentCustomNoteData = false; + public hasChangedName = false; // implemented + public hasChangedColor = false; // implemented // Whether they sent an admin message that was invalid (wrong password, etc) - public hasSentInvalidAdminMessage = false; + public hasSentInvalidAdminMessage = false; // implemented // Whether or not they have passed the b message - public hasCompletedBrowserChallenge = false; + public hasCompletedBrowserChallenge = false; // implemented + + public dump() { + return JSON.stringify(this, undefined, 4); + } } diff --git a/src/ws/Socket.ts b/src/ws/Socket.ts index d3f456c..96ae5f9 100644 --- a/src/ws/Socket.ts +++ b/src/ws/Socket.ts @@ -146,7 +146,7 @@ export class Socket extends EventEmitter { // Basic function this.sendArray([{ m: "b", - code: `~return true;` + code: `~return btoa(JSON.stringify([true, navigator.userAgent]));` }]); } else if (config.browserChallenge == "obf") { // Obfuscated challenge building @@ -183,7 +183,7 @@ export class Socket extends EventEmitter { } /** - * Move this participant to a channel + * Move this socket to a channel * @param _id Target channel ID * @param set Channel settings, if the channel is instantiated * @param force Whether to make this socket join regardless of channel properties @@ -239,6 +239,17 @@ export class Socket extends EventEmitter { // Make them join the new channel channel.join(this, force); } + + // Gateway stuff + this.gateway.hasJoinedAnyChannel = true; + + const ch = this.getCurrentChannel(); + + if (ch) { + if (ch.isLobby()) { + this.gateway.hasJoinedLobby = true; + } + } } public admin = new EventEmitter(); diff --git a/src/ws/events/user/handlers/+ls.ts b/src/ws/events/user/handlers/+ls.ts index d9f47af..d595f03 100644 --- a/src/ws/events/user/handlers/+ls.ts +++ b/src/ws/events/user/handlers/+ls.ts @@ -16,6 +16,8 @@ export const plus_ls: ServerEventListener<"+ls"> = { if (!socket.rateLimits.normal["+ls"].attempt()) return; } + socket.gateway.hasOpenedChannelList = true; + socket.subscribeToChannelList(); } }; diff --git a/src/ws/events/user/handlers/a.ts b/src/ws/events/user/handlers/a.ts index e14ffdd..08c6dcb 100644 --- a/src/ws/events/user/handlers/a.ts +++ b/src/ws/events/user/handlers/a.ts @@ -1,4 +1,28 @@ -import { ServerEventListener } from "../../../../util/types"; +import { Socket } from "../../../Socket"; +import { ServerEventListener, ServerEvents } from "../../../../util/types"; + +// https://stackoverflow.com/questions/64509631/is-there-a-regex-to-match-all-unicode-emojis +const emojiRegex = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/g; + +function populateSocketChatGatewayFlags(msg: ServerEvents["a"], socket: Socket) { + socket.gateway.hasSentChatMessage = true; + + if (msg.message.toUpperCase() == msg.message) { + socket.gateway.hasSentChatMessageWithCapitalLettersOnly = true; + } + + if (msg.message.includes("\u034f") || msg.message.includes("\u200b")) { + socket.gateway.hasSentChatMessageWithInvisibleCharacters = true; + } + + if (msg.message.match(/[^\x00-\x7f]/gm)) { + socket.gateway.hasSentChatMessageWithInvisibleCharacters = true; + } + + if (msg.message.match(emojiRegex)) { + socket.gateway.hasSentChatMessageWithEmoji = true; + } +} export const a: ServerEventListener<"a"> = { id: "a", @@ -7,9 +31,14 @@ export const a: ServerEventListener<"a"> = { const flags = socket.getUserFlags(); if (!flags) return; + if (typeof msg.message !== "string") return; + // Why did I write this statement so weird if (!flags["no chat rate limit"] || flags["no chat rate limit"] == 0) if (!socket.rateLimits?.normal.a.attempt()) return; + + populateSocketChatGatewayFlags(msg, socket); + const ch = socket.getCurrentChannel(); if (!ch) return; diff --git a/src/ws/events/user/handlers/admin_message.ts b/src/ws/events/user/handlers/admin_message.ts index eb44d47..7e0c00a 100644 --- a/src/ws/events/user/handlers/admin_message.ts +++ b/src/ws/events/user/handlers/admin_message.ts @@ -9,8 +9,15 @@ export const admin_message: ServerEventListener<"admin 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; + if (typeof msg.password !== "string") { + socket.gateway.hasSentInvalidAdminMessage = true; + return; + } + + if (msg.password !== env.ADMIN_PASS) { + socket.gateway.hasSentInvalidAdminMessage = true; + return; + } // Probably shouldn't be using password auth in 2024 // Maybe I'll setup a dashboard instead some day diff --git a/src/ws/events/user/handlers/hi.ts b/src/ws/events/user/handlers/hi.ts index 56fa878..236f20e 100644 --- a/src/ws/events/user/handlers/hi.ts +++ b/src/ws/events/user/handlers/hi.ts @@ -17,10 +17,20 @@ export const hi: ServerEventListener<"hi"> = { // Browser challenge if (config.browserChallenge == "basic") { - if (typeof msg.code !== "boolean") return; + try { + if (typeof msg.code !== "string") return; + const code = atob(msg.code); + const arr = JSON.parse(code); - if (msg.code === true) { - socket.gateway.hasCompletedBrowserChallenge = true; + if (arr[0] === true) { + socket.gateway.hasCompletedBrowserChallenge = true; + + if (typeof arr[1] === "string") { + socket.gateway.userAgent = arr[1]; + } + } + } catch (err) { + logger.warn("Unable to parse basic browser challenge code:", err); } } else if (config.browserChallenge == "obf") { // TODO @@ -34,11 +44,14 @@ export const hi: ServerEventListener<"hi"> = { if (config.tokenAuth !== "none") { if (typeof msg.token !== "string") { + socket.gateway.hasSentToken = true; + // Get a saved token token = await getToken(socket.getUserID()); if (typeof token !== "string") { // Generate a new one token = await createToken(socket.getUserID(), socket.gateway); + socket.gateway.isTokenValid = true; if (typeof token !== "string") { logger.warn(`Unable to generate token for user ${socket.getUserID()}`); @@ -54,6 +67,7 @@ export const hi: ServerEventListener<"hi"> = { //return; } else { token = msg.token; + socket.gateway.isTokenValid = true; } } } diff --git a/src/ws/events/user/handlers/m.ts b/src/ws/events/user/handlers/m.ts index 5e6dc8b..6086751 100644 --- a/src/ws/events/user/handlers/m.ts +++ b/src/ws/events/user/handlers/m.ts @@ -13,11 +13,21 @@ export const m: ServerEventListener<"m"> = { let x = msg.x; let y = msg.y; - // Make it numbers - if (typeof msg.x == "string") x = parseFloat(msg.x); - if (typeof msg.y == "string") y = parseFloat(msg.y); + // Parse cursor position if it's strings + if (typeof msg.x == "string") { + x = parseFloat(msg.x); + } else { + socket.gateway.isCursorNotString = true; + } - // Move the laggy piece of shit + if (typeof msg.y == "string") { + y = parseFloat(msg.y); + } else { + socket.gateway.isCursorNotString = true; + } + + // Relocate the laggy microscopic speck socket.setCursorPos(x, y); + socket.gateway.hasCursorMoved = true; } }; diff --git a/src/ws/events/user/handlers/n.ts b/src/ws/events/user/handlers/n.ts index 66c83eb..0dcc102 100644 --- a/src/ws/events/user/handlers/n.ts +++ b/src/ws/events/user/handlers/n.ts @@ -11,6 +11,8 @@ export const n: ServerEventListener<"n"> = { if (!Array.isArray(msg.n)) return; if (typeof msg.t !== "number") return; + socket.gateway.hasPlayedPianoBefore = true; + // This should've been here months ago const channel = socket.getCurrentChannel(); if (!channel) return; diff --git a/src/ws/events/user/handlers/t.ts b/src/ws/events/user/handlers/t.ts index 92dfe20..9e9c372 100644 --- a/src/ws/events/user/handlers/t.ts +++ b/src/ws/events/user/handlers/t.ts @@ -12,6 +12,8 @@ export const t: ServerEventListener<"t"> = { if (typeof msg.e !== "number") return; } + socket.gateway.lastPing = Date.now(); + // Pong socket.sendArray([ { diff --git a/src/ws/events/user/handlers/userset.ts b/src/ws/events/user/handlers/userset.ts index 601063c..5555499 100644 --- a/src/ws/events/user/handlers/userset.ts +++ b/src/ws/events/user/handlers/userset.ts @@ -1,20 +1,21 @@ import { ServerEventListener } from "../../../../util/types"; +import { config } from "../../../usersConfig"; export const userset: ServerEventListener<"userset"> = { id: "userset", callback: async (msg, socket) => { // Change username/color if (!socket.rateLimits?.chains.userset.attempt()) return; - // You can disable color in the config because - // Brandon's/jacored's server doesn't allow color changes, - // and that's the OG server, but folks over at MPP.net - // said otherwise because they're dumb roleplayers - // or something and don't understand the unique value - // of the fishing bot and how it allows you to change colors - // without much control, giving it the feeling of value... - // Kinda reminds me of crypto. - // Also, Brandon's server had color changing on before. - if (!msg.set.name && !msg.set.color) return; + if (typeof msg.set.name !== "string" && typeof msg.set.color !== "string") return; + + if (typeof msg.set.name == "string") { + socket.gateway.hasChangedName = true; + } + + if (typeof msg.set.color == "string" && config.enableColorChanging) { + socket.gateway.hasChangedColor = true; + } + socket.userset(msg.set.name, msg.set.color); } }; diff --git a/src/ws/server.ts b/src/ws/server.ts index 66e4934..f7aefc6 100644 --- a/src/ws/server.ts +++ b/src/ws/server.ts @@ -7,9 +7,14 @@ import { Socket, socketsBySocketID } from "./Socket"; import env from "../util/env"; import { getMOTD } from "../util/motd"; import nunjucks from "nunjucks"; +import { metrics } from "../util/metrics"; const logger = new Logger("WebSocket Server"); +// ip -> timestamp +// for checking if they visited the site and are also connected to the websocket +const httpIpCache = new Map(); + /** * Get a rendered version of the index file * @returns Response with html in it @@ -39,65 +44,81 @@ export const app = Bun.serve<{ ip: string }>({ fetch: (req, server) => { const reqip = server.requestIP(req); if (!reqip) return; + const ip = req.headers.get("x-forwarded-for") || reqip.address; - if ( - server.upgrade(req, { - data: { - ip - } - }) - ) { + // Upgrade websocket connections + if (server.upgrade(req, { data: { ip } })) { return; - } else { - const url = new URL(req.url).pathname; + } - // lol - // const ip = decoder.decode(res.getRemoteAddressAsText()); - // logger.debug(`${req.getMethod()} ${url} ${ip}`); - // res.writeStatus(`200 OK`).end("HI!"); + httpIpCache.set(ip, Date.now()); + const url = new URL(req.url).pathname; - // I have no clue if this is even safe... - // wtf do I do when the user types "/../.env" in the URL? - // From my testing, nothing out of the ordinary happens... - // but just in case, if you find something wrong with URLs, - // this is the most likely culprit + // lol + // const ip = decoder.decode(res.getRemoteAddressAsText()); + // logger.debug(`${req.getMethod()} ${url} ${ip}`); + // res.writeStatus(`200 OK`).end("HI!"); - const file = path.join("./public/", url); + // I have no clue if this is even safe... + // wtf do I do when the user types "/../.env" in the URL? + // From my testing, nothing out of the ordinary happens... + // but just in case, if you find something wrong with URLs, + // this is the most likely culprit - // Time for unreadable blocks of confusion - try { - if (fs.lstatSync(file).isFile()) { - const data = Bun.file(file); + const file = path.join("./public/", url); - if (data) { - return new Response(data); - } else { - return getIndex(); - } + // Time for unreadable blocks of confusion + try { + // Is it a file? + if (fs.lstatSync(file).isFile()) { + // Read the file + const data = Bun.file(file); + + // Return the file + if (data) { + return new Response(data); } else { return getIndex(); } - } catch (err) { + } else { + // Return the index file, since it's a channel name or something return getIndex(); } + } catch (err) { + // Return the index file as a coverup of our extreme failure + return getIndex(); } }, websocket: { open: ws => { - // We got one! + // swimming in the pool const socket = new Socket(ws, createSocketID()); - // Reel 'em in... (ws as unknown as any).socket = socket; // logger.debug("Connection at " + socket.getIP()); - // Let's put it in the dinner bucket. if (socket.socketID == undefined) { socket.socketID = createSocketID(); } socketsBySocketID.set(socket.socketID, socket); + + const ip = socket.getIP(); + + if (httpIpCache.has(ip)) { + const date = httpIpCache.get(ip); + + if (date) { + if (Date.now() - date < 1000 * 60) { + // They got the page and we were connected in under a minute + socket.gateway.hasConnectedToHTTPServer = true; + } else { + // They got the page and a long time has passed + httpIpCache.delete(ip); + } + } + } }, message: (ws, message) => {