insanely long commit
This commit is contained in:
parent
7e05fed0b8
commit
59dfb8fd2f
|
@ -9,3 +9,7 @@ prisma/*.sqlite
|
|||
|
||||
# TS build
|
||||
/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
|
||||
|
||||
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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,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>;
|
||||
};
|
||||
|
||||
b: {
|
||||
m: "b";
|
||||
code: string;
|
||||
};
|
||||
|
||||
// Admin
|
||||
|
||||
color: {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,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 { 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;
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue