diff --git a/.env.template b/.env.template old mode 100644 new mode 100755 diff --git a/.eslintrc.js b/.eslintrc.js old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 6df9596..443bf36 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,7 @@ prisma/*.sqlite # TS build /out + +# JWT token keypair +mppkey +mppkey.pub diff --git a/.gitmodules b/.gitmodules old mode 100644 new mode 100755 diff --git a/.prettierrc b/.prettierrc old mode 100644 new mode 100755 diff --git a/.vscode/settings.json b/.vscode/settings.json old mode 100644 new mode 100755 diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 3675558..c29d336 --- a/README.md +++ b/README.md @@ -73,6 +73,16 @@ Don't expect these instructions to stay the same. They might not even be up to d 1. Clone the repo and setup Git submodules +This step is subject to change, due to the necessity of testing different frontends, where the frontend may or may not be a git submodule. +This will probably be updated in the near future. Expect a step asking to download the frontend manually. +If you are forking this repository, you can just setup a new submodule for the frontend. +The frontend files go in the `public` folder. + +I am also considering using handlebars or something similar for templating, where the frontend will require completely different code. +The reason behind this decision is that I would like different things to change on the frontend based on the server's config files, +such as enabling the color changing option in the userset modal menu, or sending separate code to server admins/mods/webmasters. + + ``` $ git clone https://git.hri7566.info/Hri7566/mpp-server-dev2 $ cd mpp-server-dev2 diff --git a/bun.lockb b/bun.lockb index 3da2746..9b18427 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json old mode 100644 new mode 100755 index eba0420..ed02b8d --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "date-holidays": "^3.21.5", "events": "^3.3.0", "fancy-text-converter": "^1.0.9", + "jsonwebtoken": "^9.0.2", "keccak": "^2.1.0", "nunjucks": "^3.2.4", "unique-names-generator": "^4.7.1", @@ -22,6 +23,7 @@ }, "devDependencies": { "@types/bun": "latest", + "@types/jsonwebtoken": "^9.0.6", "@types/node": "^20.5.9", "@types/nunjucks": "^3.2.6", "@typescript-eslint/eslint-plugin": "^6.19.1", diff --git a/prisma/schema.prisma b/prisma/schema.prisma old mode 100644 new mode 100755 index 327ed72..ba36a14 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -17,9 +17,16 @@ model User { color String @default("#ffffff") flags String @default("{}") // JSON flags object tag String // JSON tag + tokens String @default("[]") // JSON tokens } model ChatHistory { id String @id @unique @map("_id") messages String @default("[]") // JSON messages } + +model Token { + userId String @id @relation(fields: [userId], references: [id]) + token String + createdAt DateTime @default(now()) +} diff --git a/src/channel/Channel.ts b/src/channel/Channel.ts old mode 100644 new mode 100755 index b5b4cff..4d54a7e --- a/src/channel/Channel.ts +++ b/src/channel/Channel.ts @@ -152,56 +152,61 @@ export class Channel extends EventEmitter { ]; this.on("a", async (msg: ServerEvents["a"], socket: Socket) => { - if (typeof msg.message !== "string") return; - - const userFlags = socket.getUserFlags(); - - if (userFlags) { - if (userFlags.cant_chat) return; - } - - if (!this.settings.chat) return; - - if (msg.message.length > 512) return; - - for (const word of BANNED_WORDS) { - if (msg.message.toLowerCase().split(" ").join("").includes(word.toLowerCase())) { - return; - } - } - - // Sanitize chat message - // Regex originally written by chacha - msg.message = msg.message - .replace(/\p{C}+/gu, "") - .replace(/(\p{Mc}{5})\p{Mc}+/gu, "$1") - .trim(); - - let outgoing: ClientEvents["a"] = { - m: "a", - a: msg.message, - t: Date.now(), - p: socket.getParticipant() as Participant - }; - - this.sendArray([outgoing]); - this.chatHistory.push(outgoing); - await saveChatHistory(this.getID(), this.chatHistory); - try { + if (typeof msg.message !== "string") return; + + const userFlags = socket.getUserFlags(); + + if (userFlags) { + if (userFlags.cant_chat) return; + } + + if (!this.settings.chat) return; + + if (msg.message.length > 512) return; + + for (const word of BANNED_WORDS) { + if (msg.message.toLowerCase().split(" ").join("").includes(word.toLowerCase())) { + return; + } + } + + // Sanitize chat message + // Regex originally written by chacha for Brandon's server + // Used with permission + msg.message = msg.message + .replace(/\p{C}+/gu, "") + .replace(/(\p{Mc}{5})\p{Mc}+/gu, "$1") + .trim(); + + let outgoing: ClientEvents["a"] = { + m: "a", + a: msg.message, + t: Date.now(), + p: socket.getParticipant() as Participant + }; + + this.sendArray([outgoing]); + this.chatHistory.push(outgoing); + await saveChatHistory(this.getID(), this.chatHistory); + if (msg.message.startsWith("/")) { this.emit("command", msg, socket); } } catch (err) { this.logger.error(err); + this.logger.warn("Error whilst processing a chat message from user " + socket.getUserID()); } }); this.on("command", (msg, socket) => { const args = msg.message.split(" "); const cmd = args[0].substring(1); + const ownsChannel = this.hasUser(socket.getUserID()); if (cmd == "help") { + } else if (cmd == "") { + } }); @@ -313,7 +318,8 @@ export class Channel extends EventEmitter { * Set this channel's ID (channel name) **/ public setID(_id: string) { - // probably causes jank + // probably causes jank, but people can just reload their page or whatever + // not sure what to do about the URL situation this._id = _id; this.emit("update", this); } diff --git a/src/channel/ChannelList.ts b/src/channel/ChannelList.ts old mode 100644 new mode 100755 diff --git a/src/channel/Crown.ts b/src/channel/Crown.ts old mode 100644 new mode 100755 index 157aef1..fe0b3c8 --- a/src/channel/Crown.ts +++ b/src/channel/Crown.ts @@ -18,6 +18,8 @@ export class Crown { }; public canBeSetBy(socket: Socket) { + // This code is based on Brandon's crown code + // can claim, drop, or give if... const flags = socket.getUserFlags(); diff --git a/src/channel/config.ts b/src/channel/config.ts old mode 100644 new mode 100755 diff --git a/src/channel/forceLoad.ts b/src/channel/forceLoad.ts old mode 100644 new mode 100755 diff --git a/src/channel/index.ts b/src/channel/index.ts old mode 100644 new mode 100755 diff --git a/src/channel/settings.ts b/src/channel/settings.ts old mode 100644 new mode 100755 diff --git a/src/data/history.ts b/src/data/history.ts old mode 100644 new mode 100755 diff --git a/src/data/prisma.ts b/src/data/prisma.ts old mode 100644 new mode 100755 diff --git a/src/data/user.ts b/src/data/user.ts old mode 100644 new mode 100755 diff --git a/src/index.ts b/src/index.ts old mode 100644 new mode 100755 diff --git a/src/util/Logger.ts b/src/util/Logger.ts old mode 100644 new mode 100755 diff --git a/src/util/config.ts b/src/util/config.ts old mode 100644 new mode 100755 diff --git a/src/util/env.ts b/src/util/env.ts old mode 100644 new mode 100755 diff --git a/src/util/helpers.ts b/src/util/helpers.ts old mode 100644 new mode 100755 diff --git a/src/util/id.ts b/src/util/id.ts old mode 100644 new mode 100755 diff --git a/src/util/motd.ts b/src/util/motd.ts old mode 100644 new mode 100755 diff --git a/src/util/readline/Command.ts b/src/util/readline/Command.ts old mode 100644 new mode 100755 diff --git a/src/util/readline/commands.ts b/src/util/readline/commands.ts old mode 100644 new mode 100755 diff --git a/src/util/readline/index.ts b/src/util/readline/index.ts old mode 100644 new mode 100755 diff --git a/src/util/readline/logger.ts b/src/util/readline/logger.ts old mode 100644 new mode 100755 diff --git a/src/util/token.ts b/src/util/token.ts new file mode 100644 index 0000000..b3fdcb7 --- /dev/null +++ b/src/util/token.ts @@ -0,0 +1,53 @@ +import { config } from "../ws/usersConfig"; +import jsonwebtoken from "jsonwebtoken"; +import env from "./env"; +import { readFileSync } from "fs"; +import { Logger } from "./Logger"; + +let privkey: string; + +if (config.tokenAuth == "jwt") { + privkey = readFileSync("./mppkey").toString(); +} + +const logger = new Logger("TokenGen"); + +export function generateToken(id: string): Promise | undefined { + if (config.tokenAuth == "jwt") { + if (!privkey) throw new Error("Private key not found"); + + logger.info("Generating JWT token for user " + id + "..."); + + return new Promise((resolve, reject) => { + jsonwebtoken.sign({ id }, privkey, { algorithm: "RS256" }, (err, token) => { + if (err || !token) { + logger.warn("Token generation failed for user " + id); + reject(err); + } + + logger.info("Token generation finished for user " + id); + resolve(token); + }); + }); + } else if (config.tokenAuth == "uuid") { + logger.info("Generating UUID token for user " + id + "..."); + + return new Promise((resolve, reject) => { + let token: string | undefined; + + try { + const uuid = crypto.randomUUID(); + token = `${id}.${uuid}`; + } catch (err) { + logger.warn("Token generation failed for user " + id); + reject(err); + } + + if (!token) reject(new Error("Token generation failed for user " + id)); + + logger.info("Token generation finished for user " + id); + + if (token) resolve(token); + }); + } else return undefined; +} diff --git a/src/util/types.d.ts b/src/util/types.d.ts old mode 100644 new mode 100755 index 889ba3d..833d8e6 --- a/src/util/types.d.ts +++ b/src/util/types.d.ts @@ -210,6 +210,11 @@ declare interface ServerEvents { msg: ServerEvents; }; + b: { + m: "b"; + code: string; + }; + // Admin color: { diff --git a/src/ws/Gateway.ts b/src/ws/Gateway.ts old mode 100644 new mode 100755 index b15e103..afb373f --- a/src/ws/Gateway.ts +++ b/src/ws/Gateway.ts @@ -12,7 +12,64 @@ * and IP address instead sometime in the future. */ export class Gateway { - public hasProcessedHi: boolean = false; - public hasSentDevices: boolean = false; - public lastPing: number = Date.now(); + // Whether we have correctly processed this socket's hi message + public hasProcessedHi = false; + + // Whether they have sent the MIDI devices message + public hasSentDevices = false; + + // Whether they have sent a token + public hasSentToken = false; + + // Whether their token is valid + public isTokenValid = false; + + // Their user agent, if sent + public userAgent = ""; + + // Whether they have moved their cursor + public hasCursorMoved = false; + + // Whether they sent a cursor message that contained numbers instead of stringified numbers + public isCursorNotString = false; + + // The last time they sent a ping message + public lastPing = Date.now(); + + // Whether they have joined any channel + public hasJoinedAnyChannel = false; + + // Whether they have joined the lobby + public hasJoinedLobby = false; + + // 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; + + // Various chat message flags + public hasSentChatMessage = false; + public hasSentChatMessageWithCapitalLettersOnly = false; + public hasSentChatMessageWithInvisibleCharacters = false; + public hasSentChatMessageWithEmoji = false; + + // Whehter or not the user has played the piano in this session + public hasPlayedPianoBefore = false; + + // Whether the user has sent a channel list subscription request, a.k.a. opened the channel list + public hasOpenedChannelList = false; + + // 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; + + // Whether they sent an admin message that was invalid (wrong password, etc) + public hasSentInvalidAdminMessage = false; + + // Whether or not they have passed the b message + public hasCompletedBrowserChallenge = false; } diff --git a/src/ws/Socket.ts b/src/ws/Socket.ts old mode 100644 new mode 100755 index 5e7ec57..4d2ad41 --- a/src/ws/Socket.ts +++ b/src/ws/Socket.ts @@ -516,6 +516,19 @@ export class Socket extends EventEmitter { // TODO Permissions let isAdmin = false; let ch = this.getCurrentChannel(); + let hasNoteRateLimitBypass = false; + + try { + const flags = this.getUserFlags(); + + if (flags) { + if (flags["no note rate limit"]) { + hasNoteRateLimitBypass = true; + } + } + } catch (err) { + logger.warn("Unable to get user flags while processing rate limits"); + } if (isAdmin) { this.setRateLimits(adminLimits); @@ -705,6 +718,10 @@ export class Socket extends EventEmitter { updateUser(this.getUserID(), user); } + /** + * Execute code in this socket's context (danger warning) + * @param str JavaScript expression to execute + **/ public eval(str: string) { try { const output = eval(str); diff --git a/src/ws/events.inc.ts b/src/ws/events.inc.ts old mode 100644 new mode 100755 diff --git a/src/ws/events.ts b/src/ws/events.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/admin/handlers/admin_chat.ts b/src/ws/events/admin/handlers/admin_chat.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/admin/handlers/ch_flag.ts b/src/ws/events/admin/handlers/ch_flag.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/admin/handlers/clear_chat.ts b/src/ws/events/admin/handlers/clear_chat.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/admin/handlers/color.ts b/src/ws/events/admin/handlers/color.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/admin/handlers/eval.ts b/src/ws/events/admin/handlers/eval.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/admin/handlers/forceload.ts b/src/ws/events/admin/handlers/forceload.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/admin/handlers/move.ts b/src/ws/events/admin/handlers/move.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/admin/handlers/name.ts b/src/ws/events/admin/handlers/name.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/admin/handlers/notification.ts b/src/ws/events/admin/handlers/notification.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/admin/handlers/rename_channel.ts b/src/ws/events/admin/handlers/rename_channel.ts old mode 100644 new mode 100755 index bba7af0..fbc9c63 --- a/src/ws/events/admin/handlers/rename_channel.ts +++ b/src/ws/events/admin/handlers/rename_channel.ts @@ -33,8 +33,10 @@ export const rename_channel: ServerEventListener<"rename_channel"> = { // Not found, so it's safe to take up that ID ch.setID(msg._id); } else { - // Found, avoid jank by magically disappearing - ch.destroy(); + if (ch.getID() !== msg._id) { + // Found and different, avoid jank by magically disappearing + ch.destroy(); + } } for (const sock of socketsBySocketID.values()) { diff --git a/src/ws/events/admin/handlers/restart.ts b/src/ws/events/admin/handlers/restart.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/admin/handlers/tag.ts b/src/ws/events/admin/handlers/tag.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/admin/handlers/user_flag.ts b/src/ws/events/admin/handlers/user_flag.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/admin/index.ts b/src/ws/events/admin/index.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/user/handlers/+ls.ts b/src/ws/events/user/handlers/+ls.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/user/handlers/-ls.ts b/src/ws/events/user/handlers/-ls.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/user/handlers/a.ts b/src/ws/events/user/handlers/a.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/user/handlers/admin_message.ts b/src/ws/events/user/handlers/admin_message.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/user/handlers/b.ts b/src/ws/events/user/handlers/b.ts new file mode 100755 index 0000000..9af7d7c --- /dev/null +++ b/src/ws/events/user/handlers/b.ts @@ -0,0 +1,8 @@ +import { ServerEventListener } from "../../../../util/types"; + +export const hi: ServerEventListener<"b"> = { + id: "b", + callback: (msg, socket) => { + // Antibot message + } +}; diff --git a/src/ws/events/user/handlers/bye.ts b/src/ws/events/user/handlers/bye.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/user/handlers/ch.ts b/src/ws/events/user/handlers/ch.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/user/handlers/chown.ts b/src/ws/events/user/handlers/chown.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/user/handlers/chset.ts b/src/ws/events/user/handlers/chset.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/user/handlers/devices.ts b/src/ws/events/user/handlers/devices.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/user/handlers/hi.ts b/src/ws/events/user/handlers/hi.ts old mode 100644 new mode 100755 index c22a3b6..846a62a --- a/src/ws/events/user/handlers/hi.ts +++ b/src/ws/events/user/handlers/hi.ts @@ -1,10 +1,30 @@ +import { generateToken } from "../../../../util/token"; import { ServerEventListener } from "../../../../util/types"; +import { config } from "../../../usersConfig"; export const hi: ServerEventListener<"hi"> = { id: "hi", - callback: (msg, socket) => { + callback: async (msg, socket) => { // Handshake message - // TODO Hi message tokens + + let generatedToken: string | undefined; + + if (config.tokenAuth !== "none") { + if (socket.gateway.hasCompletedBrowserChallenge) { + if (msg.token) { + // Check if they have passed the browser challenge + // Send the token to the authenticator + // TODO + } else { + // Generate a token + generatedToken = await generateToken(socket.getUserID()) as string | undefined; + if (!generatedToken) return; + } + } else { + // TODO Ban the user for logging in without the browser + // TODO config for this + } + } if (socket.rateLimits) if (!socket.rateLimits.normal.hi.attempt()) return; diff --git a/src/ws/events/user/handlers/kickban.ts b/src/ws/events/user/handlers/kickban.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/user/handlers/m.ts b/src/ws/events/user/handlers/m.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/user/handlers/n.ts b/src/ws/events/user/handlers/n.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/user/handlers/t.ts b/src/ws/events/user/handlers/t.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/user/handlers/unban.ts b/src/ws/events/user/handlers/unban.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/user/handlers/userset.ts b/src/ws/events/user/handlers/userset.ts old mode 100644 new mode 100755 diff --git a/src/ws/events/user/index.ts b/src/ws/events/user/index.ts old mode 100644 new mode 100755 diff --git a/src/ws/message.ts b/src/ws/message.ts old mode 100644 new mode 100755 diff --git a/src/ws/ratelimit/NoteQuota.ts b/src/ws/ratelimit/NoteQuota.ts old mode 100644 new mode 100755 diff --git a/src/ws/ratelimit/RateLimit.ts b/src/ws/ratelimit/RateLimit.ts old mode 100644 new mode 100755 diff --git a/src/ws/ratelimit/RateLimitChain.ts b/src/ws/ratelimit/RateLimitChain.ts old mode 100644 new mode 100755 diff --git a/src/ws/ratelimit/config.ts b/src/ws/ratelimit/config.ts old mode 100644 new mode 100755 diff --git a/src/ws/ratelimit/limits/admin.ts b/src/ws/ratelimit/limits/admin.ts old mode 100644 new mode 100755 diff --git a/src/ws/ratelimit/limits/crown.ts b/src/ws/ratelimit/limits/crown.ts old mode 100644 new mode 100755 diff --git a/src/ws/ratelimit/limits/user.ts b/src/ws/ratelimit/limits/user.ts old mode 100644 new mode 100755 diff --git a/src/ws/server.ts b/src/ws/server.ts old mode 100644 new mode 100755 diff --git a/src/ws/usersConfig.ts b/src/ws/usersConfig.ts old mode 100644 new mode 100755 index d514422..d902cca --- a/src/ws/usersConfig.ts +++ b/src/ws/usersConfig.ts @@ -8,6 +8,7 @@ export interface UsersConfig { enableCustomNoteData: boolean; adminParticipant: Participant; enableAdminEval: boolean; + tokenAuth: "jwt" | "uuid" | "none"; } export const usersConfigPath = "config/users.yml"; @@ -25,7 +26,8 @@ export const defaultUsersConfig: UsersConfig = { color: "#fff", id: "0" }, - enableAdminEval: false + enableAdminEval: false, + tokenAuth: "none" }; // Importing this elsewhere causes bun to segfault diff --git a/tsconfig.json b/tsconfig.json old mode 100644 new mode 100755