insanely long commit
This commit is contained in:
parent
7e05fed0b8
commit
59dfb8fd2f
|
@ -9,3 +9,7 @@ prisma/*.sqlite
|
||||||
|
|
||||||
# TS build
|
# TS build
|
||||||
/out
|
/out
|
||||||
|
|
||||||
|
# JWT token keypair
|
||||||
|
mppkey
|
||||||
|
mppkey.pub
|
||||||
|
|
|
@ -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
|
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
|
$ git clone https://git.hri7566.info/Hri7566/mpp-server-dev2
|
||||||
$ cd mpp-server-dev2
|
$ cd mpp-server-dev2
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
"date-holidays": "^3.21.5",
|
"date-holidays": "^3.21.5",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"fancy-text-converter": "^1.0.9",
|
"fancy-text-converter": "^1.0.9",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"keccak": "^2.1.0",
|
"keccak": "^2.1.0",
|
||||||
"nunjucks": "^3.2.4",
|
"nunjucks": "^3.2.4",
|
||||||
"unique-names-generator": "^4.7.1",
|
"unique-names-generator": "^4.7.1",
|
||||||
|
@ -22,6 +23,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
"@types/node": "^20.5.9",
|
"@types/node": "^20.5.9",
|
||||||
"@types/nunjucks": "^3.2.6",
|
"@types/nunjucks": "^3.2.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.19.1",
|
"@typescript-eslint/eslint-plugin": "^6.19.1",
|
||||||
|
|
|
@ -17,9 +17,16 @@ model User {
|
||||||
color String @default("#ffffff")
|
color String @default("#ffffff")
|
||||||
flags String @default("{}") // JSON flags object
|
flags String @default("{}") // JSON flags object
|
||||||
tag String // JSON tag
|
tag String // JSON tag
|
||||||
|
tokens String @default("[]") // JSON tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
model ChatHistory {
|
model ChatHistory {
|
||||||
id String @id @unique @map("_id")
|
id String @id @unique @map("_id")
|
||||||
messages String @default("[]") // JSON messages
|
messages String @default("[]") // JSON messages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Token {
|
||||||
|
userId String @id @relation(fields: [userId], references: [id])
|
||||||
|
token String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
|
@ -152,56 +152,61 @@ export class Channel extends EventEmitter {
|
||||||
];
|
];
|
||||||
|
|
||||||
this.on("a", async (msg: ServerEvents["a"], socket: Socket) => {
|
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 {
|
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("/")) {
|
if (msg.message.startsWith("/")) {
|
||||||
this.emit("command", msg, socket);
|
this.emit("command", msg, socket);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
|
this.logger.warn("Error whilst processing a chat message from user " + socket.getUserID());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.on("command", (msg, socket) => {
|
this.on("command", (msg, socket) => {
|
||||||
const args = msg.message.split(" ");
|
const args = msg.message.split(" ");
|
||||||
const cmd = args[0].substring(1);
|
const cmd = args[0].substring(1);
|
||||||
|
const ownsChannel = this.hasUser(socket.getUserID());
|
||||||
|
|
||||||
if (cmd == "help") {
|
if (cmd == "help") {
|
||||||
|
} else if (cmd == "") {
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -313,7 +318,8 @@ export class Channel extends EventEmitter {
|
||||||
* Set this channel's ID (channel name)
|
* Set this channel's ID (channel name)
|
||||||
**/
|
**/
|
||||||
public setID(_id: string) {
|
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._id = _id;
|
||||||
this.emit("update", this);
|
this.emit("update", this);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,8 @@ export class Crown {
|
||||||
};
|
};
|
||||||
|
|
||||||
public canBeSetBy(socket: Socket) {
|
public canBeSetBy(socket: Socket) {
|
||||||
|
// This code is based on Brandon's crown code
|
||||||
|
|
||||||
// can claim, drop, or give if...
|
// can claim, drop, or give if...
|
||||||
const flags = socket.getUserFlags();
|
const flags = socket.getUserFlags();
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -210,6 +210,11 @@ declare interface ServerEvents {
|
||||||
msg: ServerEvents<keyof ServerEvents>;
|
msg: ServerEvents<keyof ServerEvents>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
b: {
|
||||||
|
m: "b";
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
// Admin
|
// Admin
|
||||||
|
|
||||||
color: {
|
color: {
|
||||||
|
|
|
@ -12,7 +12,64 @@
|
||||||
* and IP address instead sometime in the future.
|
* and IP address instead sometime in the future.
|
||||||
*/
|
*/
|
||||||
export class Gateway {
|
export class Gateway {
|
||||||
public hasProcessedHi: boolean = false;
|
// Whether we have correctly processed this socket's hi message
|
||||||
public hasSentDevices: boolean = false;
|
public hasProcessedHi = false;
|
||||||
public lastPing: number = Date.now();
|
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -516,6 +516,19 @@ export class Socket extends EventEmitter {
|
||||||
// TODO Permissions
|
// TODO Permissions
|
||||||
let isAdmin = false;
|
let isAdmin = false;
|
||||||
let ch = this.getCurrentChannel();
|
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) {
|
if (isAdmin) {
|
||||||
this.setRateLimits(adminLimits);
|
this.setRateLimits(adminLimits);
|
||||||
|
@ -705,6 +718,10 @@ export class Socket extends EventEmitter {
|
||||||
updateUser(this.getUserID(), user);
|
updateUser(this.getUserID(), user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute code in this socket's context (danger warning)
|
||||||
|
* @param str JavaScript expression to execute
|
||||||
|
**/
|
||||||
public eval(str: string) {
|
public eval(str: string) {
|
||||||
try {
|
try {
|
||||||
const output = eval(str);
|
const output = eval(str);
|
||||||
|
|
|
@ -33,8 +33,10 @@ export const rename_channel: ServerEventListener<"rename_channel"> = {
|
||||||
// Not found, so it's safe to take up that ID
|
// Not found, so it's safe to take up that ID
|
||||||
ch.setID(msg._id);
|
ch.setID(msg._id);
|
||||||
} else {
|
} else {
|
||||||
// Found, avoid jank by magically disappearing
|
if (ch.getID() !== msg._id) {
|
||||||
ch.destroy();
|
// Found and different, avoid jank by magically disappearing
|
||||||
|
ch.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const sock of socketsBySocketID.values()) {
|
for (const sock of socketsBySocketID.values()) {
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { ServerEventListener } from "../../../../util/types";
|
||||||
|
|
||||||
|
export const hi: ServerEventListener<"b"> = {
|
||||||
|
id: "b",
|
||||||
|
callback: (msg, socket) => {
|
||||||
|
// Antibot message
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,10 +1,30 @@
|
||||||
|
import { generateToken } from "../../../../util/token";
|
||||||
import { ServerEventListener } from "../../../../util/types";
|
import { ServerEventListener } from "../../../../util/types";
|
||||||
|
import { config } from "../../../usersConfig";
|
||||||
|
|
||||||
export const hi: ServerEventListener<"hi"> = {
|
export const hi: ServerEventListener<"hi"> = {
|
||||||
id: "hi",
|
id: "hi",
|
||||||
callback: (msg, socket) => {
|
callback: async (msg, socket) => {
|
||||||
// Handshake message
|
// 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)
|
||||||
if (!socket.rateLimits.normal.hi.attempt()) return;
|
if (!socket.rateLimits.normal.hi.attempt()) return;
|
||||||
|
|
|
@ -8,6 +8,7 @@ export interface UsersConfig {
|
||||||
enableCustomNoteData: boolean;
|
enableCustomNoteData: boolean;
|
||||||
adminParticipant: Participant;
|
adminParticipant: Participant;
|
||||||
enableAdminEval: boolean;
|
enableAdminEval: boolean;
|
||||||
|
tokenAuth: "jwt" | "uuid" | "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usersConfigPath = "config/users.yml";
|
export const usersConfigPath = "config/users.yml";
|
||||||
|
@ -25,7 +26,8 @@ export const defaultUsersConfig: UsersConfig = {
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
id: "0"
|
id: "0"
|
||||||
},
|
},
|
||||||
enableAdminEval: false
|
enableAdminEval: false,
|
||||||
|
tokenAuth: "none"
|
||||||
};
|
};
|
||||||
|
|
||||||
// Importing this elsewhere causes bun to segfault
|
// Importing this elsewhere causes bun to segfault
|
||||||
|
|
Loading…
Reference in New Issue