insanely long commit

This commit is contained in:
Hri7566 2024-07-23 05:55:13 -04:00
parent 7e05fed0b8
commit 59dfb8fd2f
81 changed files with 240 additions and 45 deletions

0
.env.template Normal file → Executable file
View File

0
.eslintrc.js Normal file → Executable file
View File

4
.gitignore vendored Normal file → Executable file
View File

@ -9,3 +9,7 @@ prisma/*.sqlite
# TS build
/out
# JWT token keypair
mppkey
mppkey.pub

0
.gitmodules vendored Normal file → Executable file
View File

0
.prettierrc Normal file → Executable file
View File

0
.vscode/settings.json vendored Normal file → Executable file
View File

0
LICENSE Normal file → Executable file
View File

10
README.md Normal file → Executable file
View File

@ -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

BIN
bun.lockb

Binary file not shown.

2
package.json Normal file → Executable file
View File

@ -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",

7
prisma/schema.prisma Normal file → Executable file
View File

@ -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())
}

80
src/channel/Channel.ts Normal file → Executable file
View File

@ -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);
}

0
src/channel/ChannelList.ts Normal file → Executable file
View File

2
src/channel/Crown.ts Normal file → Executable file
View File

@ -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();

0
src/channel/config.ts Normal file → Executable file
View File

0
src/channel/forceLoad.ts Normal file → Executable file
View File

0
src/channel/index.ts Normal file → Executable file
View File

0
src/channel/settings.ts Normal file → Executable file
View File

0
src/data/history.ts Normal file → Executable file
View File

0
src/data/prisma.ts Normal file → Executable file
View File

0
src/data/user.ts Normal file → Executable file
View File

0
src/index.ts Normal file → Executable file
View File

0
src/util/Logger.ts Normal file → Executable file
View File

0
src/util/config.ts Normal file → Executable file
View File

0
src/util/env.ts Normal file → Executable file
View File

0
src/util/helpers.ts Normal file → Executable file
View File

0
src/util/id.ts Normal file → Executable file
View File

0
src/util/motd.ts Normal file → Executable file
View File

0
src/util/readline/Command.ts Normal file → Executable file
View File

0
src/util/readline/commands.ts Normal file → Executable file
View File

0
src/util/readline/index.ts Normal file → Executable file
View File

0
src/util/readline/logger.ts Normal file → Executable file
View File

53
src/util/token.ts Normal file
View File

@ -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<string | undefined> | 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;
}

5
src/util/types.d.ts vendored Normal file → Executable file
View File

@ -210,6 +210,11 @@ declare interface ServerEvents {
msg: ServerEvents<keyof ServerEvents>;
};
b: {
m: "b";
code: string;
};
// Admin
color: {

63
src/ws/Gateway.ts Normal file → Executable file
View File

@ -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;
}

17
src/ws/Socket.ts Normal file → Executable file
View File

@ -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);

0
src/ws/events.inc.ts Normal file → Executable file
View File

0
src/ws/events.ts Normal file → Executable file
View File

0
src/ws/events/admin/handlers/admin_chat.ts Normal file → Executable file
View File

0
src/ws/events/admin/handlers/ch_flag.ts Normal file → Executable file
View File

0
src/ws/events/admin/handlers/clear_chat.ts Normal file → Executable file
View File

0
src/ws/events/admin/handlers/color.ts Normal file → Executable file
View File

0
src/ws/events/admin/handlers/eval.ts Normal file → Executable file
View File

0
src/ws/events/admin/handlers/forceload.ts Normal file → Executable file
View File

0
src/ws/events/admin/handlers/move.ts Normal file → Executable file
View File

0
src/ws/events/admin/handlers/name.ts Normal file → Executable file
View File

0
src/ws/events/admin/handlers/notification.ts Normal file → Executable file
View File

6
src/ws/events/admin/handlers/rename_channel.ts Normal file → Executable file
View File

@ -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()) {

0
src/ws/events/admin/handlers/restart.ts Normal file → Executable file
View File

0
src/ws/events/admin/handlers/tag.ts Normal file → Executable file
View File

0
src/ws/events/admin/handlers/user_flag.ts Normal file → Executable file
View File

0
src/ws/events/admin/index.ts Normal file → Executable file
View File

0
src/ws/events/user/handlers/+ls.ts Normal file → Executable file
View File

0
src/ws/events/user/handlers/-ls.ts Normal file → Executable file
View File

0
src/ws/events/user/handlers/a.ts Normal file → Executable file
View File

0
src/ws/events/user/handlers/admin_message.ts Normal file → Executable file
View File

View File

@ -0,0 +1,8 @@
import { ServerEventListener } from "../../../../util/types";
export const hi: ServerEventListener<"b"> = {
id: "b",
callback: (msg, socket) => {
// Antibot message
}
};

0
src/ws/events/user/handlers/bye.ts Normal file → Executable file
View File

0
src/ws/events/user/handlers/ch.ts Normal file → Executable file
View File

0
src/ws/events/user/handlers/chown.ts Normal file → Executable file
View File

0
src/ws/events/user/handlers/chset.ts Normal file → Executable file
View File

0
src/ws/events/user/handlers/devices.ts Normal file → Executable file
View File

24
src/ws/events/user/handlers/hi.ts Normal file → Executable file
View File

@ -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;

0
src/ws/events/user/handlers/kickban.ts Normal file → Executable file
View File

0
src/ws/events/user/handlers/m.ts Normal file → Executable file
View File

0
src/ws/events/user/handlers/n.ts Normal file → Executable file
View File

0
src/ws/events/user/handlers/t.ts Normal file → Executable file
View File

0
src/ws/events/user/handlers/unban.ts Normal file → Executable file
View File

0
src/ws/events/user/handlers/userset.ts Normal file → Executable file
View File

0
src/ws/events/user/index.ts Normal file → Executable file
View File

0
src/ws/message.ts Normal file → Executable file
View File

0
src/ws/ratelimit/NoteQuota.ts Normal file → Executable file
View File

0
src/ws/ratelimit/RateLimit.ts Normal file → Executable file
View File

0
src/ws/ratelimit/RateLimitChain.ts Normal file → Executable file
View File

0
src/ws/ratelimit/config.ts Normal file → Executable file
View File

0
src/ws/ratelimit/limits/admin.ts Normal file → Executable file
View File

0
src/ws/ratelimit/limits/crown.ts Normal file → Executable file
View File

0
src/ws/ratelimit/limits/user.ts Normal file → Executable file
View File

0
src/ws/server.ts Normal file → Executable file
View File

4
src/ws/usersConfig.ts Normal file → Executable file
View File

@ -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

0
tsconfig.json Normal file → Executable file
View File