diff --git a/README.md b/README.md index e8775be..d5285da 100755 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Brandon's server originally used MongoDB for storing user data, but there are to ## TODO - Implement both UUID-based and JWT-based token auth + - Add `openssl genrsa -out mppkey 2048` to the instructions - Redo all of the validations with Zod - This probably means making Zod schemas for every single message type - Also user and channel data diff --git a/bun.lockb b/bun.lockb index 9b18427..050587b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/config/users.yml b/config/users.yml index 9ce6078..31ec330 100755 --- a/config/users.yml +++ b/config/users.yml @@ -23,8 +23,15 @@ adminParticipant: # Allows admins to evaluate code through the "eval" message. # This is a security risk, so only enable this if you trust your admins. -enableAdminEval: false +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". tokenAuth: none + +# The browser challenge scheme. Valid values 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 diff --git a/package.json b/package.json index ed02b8d..8ce072f 100755 --- a/package.json +++ b/package.json @@ -9,27 +9,27 @@ "dependencies": { "@prisma/client": "5.7.0", "@t3-oss/env-core": "^0.6.1", - "bun-types": "^1.0.1", + "bun-types": "^1.1.20", "commander": "^11.1.0", - "date-holidays": "^3.21.5", + "date-holidays": "^3.23.12", "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", - "yaml": "^2.3.2", - "zod": "^3.22.2" + "yaml": "^2.4.5", + "zod": "^3.23.8" }, "devDependencies": { "@types/bun": "latest", "@types/jsonwebtoken": "^9.0.6", - "@types/node": "^20.5.9", + "@types/node": "^20.14.12", "@types/nunjucks": "^3.2.6", - "@typescript-eslint/eslint-plugin": "^6.19.1", - "@typescript-eslint/parser": "^6.19.1", - "eslint": "^8.56.0", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint": "^8.57.0", "prisma": "5.7.0", - "typescript": "^5.2.2" + "typescript": "^5.5.4" } } \ No newline at end of file diff --git a/src/channel/Channel.ts b/src/channel/Channel.ts index 4d54a7e..e33ddc6 100755 --- a/src/channel/Channel.ts +++ b/src/channel/Channel.ts @@ -20,6 +20,7 @@ import { config as usersConfig } from "../ws/usersConfig"; import { saveChatHistory, getChatHistory } from "../data/history"; import { mixin, darken } from "../util/helpers"; import { User } from "@prisma/client"; +import { heapStats } from "bun:jsc"; interface CachedKickban { userId: string; @@ -112,6 +113,12 @@ export class Channel extends EventEmitter { this.settings.owner_id = owner_id; this.logger.info("Created"); + + if (this.getID() == "test/mem") { + setInterval(() => { + this.printMemoryInChat(); + }, 1000); + } } private alreadyBound = false; @@ -205,8 +212,8 @@ export class Channel extends EventEmitter { const ownsChannel = this.hasUser(socket.getUserID()); if (cmd == "help") { - } else if (cmd == "") { - + } else if (cmd == "mem") { + this.printMemoryInChat(); } }); @@ -1139,6 +1146,11 @@ export class Channel extends EventEmitter { } } } + + public printMemoryInChat() { + const mem = heapStats(); + this.sendChatAdmin(`Size: ${(mem.heapSize / 1000 / 1000).toFixed(2)}M / Capacity: ${(mem.heapCapacity / 1000 / 1000).toFixed(2)}M`); + } } export default Channel; diff --git a/src/util/token.ts b/src/util/token.ts index b3fdcb7..bc93dcb 100644 --- a/src/util/token.ts +++ b/src/util/token.ts @@ -49,5 +49,17 @@ export function generateToken(id: string): Promise | undefin if (token) resolve(token); }); - } else return undefined; + } else return new Promise(() => undefined); +} + +export function verifyToken(token: string) { + return jsonwebtoken.verify(token, privkey, { algorithms: ["RS256"] }); +} + +export async function decryptJWT(token: string) { + if (config.tokenAuth != "jwt") return undefined; + + if (!privkey) throw new Error("Cannot decrypt JWT without private key loaded"); + + return jsonwebtoken.decode(token); } diff --git a/src/ws/events/user/handlers/hi.ts b/src/ws/events/user/handlers/hi.ts index 846a62a..e37db73 100755 --- a/src/ws/events/user/handlers/hi.ts +++ b/src/ws/events/user/handlers/hi.ts @@ -1,7 +1,10 @@ +import { Logger } from "../../../../util/Logger"; import { generateToken } from "../../../../util/token"; import { ServerEventListener } from "../../../../util/types"; import { config } from "../../../usersConfig"; +const logger = new Logger("Hi handler"); + export const hi: ServerEventListener<"hi"> = { id: "hi", callback: async (msg, socket) => { @@ -9,21 +12,25 @@ export const hi: ServerEventListener<"hi"> = { let generatedToken: string | undefined; + // Is the browser challenge enabled and has the user completed it? + if (config.browserChallenge !== "none" && !socket.gateway.hasCompletedBrowserChallenge) return; + + // Is token auth enabled? 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; - } + logger.debug("token auth is enabled"); + + // Is the browser challenge enabled and has the user completed it? + if (msg.token) { + // Check if they have passed the browser challenge + // Send the token to the authenticator + // TODO } else { - // TODO Ban the user for logging in without the browser - // TODO config for this + // Generate a token + generatedToken = await generateToken(socket.getUserID()); + if (!generatedToken) return; } + + logger.debug("token:", generatedToken); } if (socket.rateLimits) @@ -51,7 +58,8 @@ export const hi: ServerEventListener<"hi"> = { _id: part._id, color: part.color, name: part.name - } + }, + token: generatedToken } ]); diff --git a/src/ws/server.ts b/src/ws/server.ts index f021a84..402dde1 100755 --- a/src/ws/server.ts +++ b/src/ws/server.ts @@ -91,7 +91,7 @@ export const app = Bun.serve<{ ip: string }>({ // logger.debug("Connection at " + socket.getIP()); // Let's put it in the dinner bucket. - socketsBySocketID.set(socket.socketID, socket); + socketsBySocketID.set((socket.socketID as any), socket); }, message: (ws, message) => { diff --git a/src/ws/usersConfig.ts b/src/ws/usersConfig.ts index d902cca..dd64545 100755 --- a/src/ws/usersConfig.ts +++ b/src/ws/usersConfig.ts @@ -9,6 +9,7 @@ export interface UsersConfig { adminParticipant: Participant; enableAdminEval: boolean; tokenAuth: "jwt" | "uuid" | "none"; + browserChallenge: "none" | "obf" | "basic"; } export const usersConfigPath = "config/users.yml"; @@ -27,7 +28,8 @@ export const defaultUsersConfig: UsersConfig = { id: "0" }, enableAdminEval: false, - tokenAuth: "none" + tokenAuth: "none", + browserChallenge: "none" }; // Importing this elsewhere causes bun to segfault