import { createReader, createWriter, Endian } from "bufferstuff"; import { WebSocketServer } from "ws"; import Fastify from "fastify"; import FastifyView from "@fastify/view"; import EJS from "ejs"; import Config from "./objects/Config"; import FunkyArray from "./objects/FunkyArray"; import RemoteUser from "./objects/RemoteUser"; import { MessageType } from "./enums/MessageType"; import Database from "./objects/Database"; import { Console } from "hsconsole"; Console.customHeader(`MultiProbe server started at ${new Date()}`); const users = new FunkyArray(); new Database(Config.database.address, Config.database.port, Config.database.username, Config.database.password, Config.database.name); // Web stuff const fastify = Fastify({ logger: false }); fastify.register(FastifyView, { engine: { ejs: EJS } }) fastify.get("/", async (req, res) => { return res.view("templates/index.ejs", { }); }); fastify.get("/account", async (req, res) => { return "TODO"; }); fastify.get("/account/login", async (req, res) => { return res.view("templates/account/login.ejs", { }); }); fastify.get("/account/register", async (req, res) => { return res.view("templates/account/register.ejs", { }); }); fastify.setNotFoundHandler(async (req, res) => { return res.view("templates/404.ejs", { }); }); // Websocket stuff const websocketServer = new WebSocketServer({ port: Config.ports.ws }, () => { Console.printInfo(`WebsocketServer listening at ws://localhost:${Config.ports.ws}`); fastify.listen({ port: Config.ports.http }, (err, address) => { if (err) { Console.printError(`Error occured while spinning up fastify:\n${err}`); process.exit(1); } Console.printInfo(`Fastify listening at ${address.replace("[::1]", "localhost")}`); Console.printInfo("MultiProbe is ready to go!"); }); }); function sendToAllButSelf(user:RemoteUser, data:Buffer) { users.forEach(otherUser => { if (otherUser.id !== user.id && otherUser.currentURL === user.currentURL) { otherUser.send(data); } }); } function sendToAll(user:RemoteUser, data:Buffer) { users.forEach(otherUser => { if (otherUser.currentURL === user.currentURL) { otherUser.send(data); } }); } websocketServer.on("connection", (socket) => { const myUUID = crypto.randomUUID(); let user:RemoteUser; function closeOrError() { if (users.has(myUUID)) { users.remove(myUUID); const userLeftPacket = createWriter(Endian.LE, 5).writeByte(MessageType.ClientLeft).writeUInt(user.id).toBuffer(); users.forEach(otherUser => otherUser.send(userLeftPacket)); } } socket.on("close", closeOrError); socket.on("error", closeOrError); socket.on("message", async (data) => { const reader = createReader(Endian.LE, data as Buffer); // There is absolutely no reason we should ever get // more than 50 bytes legit. if (reader.length > 0 && reader.length < 50) { switch (reader.readUByte()) { case MessageType.ClientDetails: if (user !== undefined) { return; } const username = reader.readShortString(); const rawURL = reader.readString(); let page = rawURL.toLowerCase().replace(".htm", "").replace(".html", ""); if (page === "index") { page = ""; } let lengthOfUsernames = 0; const usersOnPage = new Array(); await users.forEach(otherUser => { if (otherUser.currentURL === page) { usersOnPage.push(otherUser); lengthOfUsernames += otherUser.username.length + 1; // + 1 for length byte } }); const usersToSend = createWriter(Endian.LE, 3 + (usersOnPage.length * 12) + lengthOfUsernames).writeByte(MessageType.Clients).writeUShort(usersOnPage.length); for (const otherUser of usersOnPage) { usersToSend.writeUInt(otherUser.id).writeShortString(otherUser.username).writeFloat(otherUser.cursorX).writeInt(otherUser.cursorY); } user = users.set(myUUID, new RemoteUser(socket, username, page, rawURL)); sendToAllButSelf(user, createWriter(Endian.LE, 6 + username.length).writeByte(MessageType.ClientJoined).writeUInt(user.id).writeShortString(username).toBuffer()); user.send(usersToSend.toBuffer()); break; case MessageType.CursorPos: { if (user === undefined) { return; } user.cursorX = reader.readFloat(); user.cursorY = reader.readInt(); sendToAllButSelf(user, createWriter(Endian.LE, 13).writeByte(MessageType.CursorPos).writeUInt(user.id).writeFloat(user.cursorX).writeInt(user.cursorY).toBuffer()); break; } case MessageType.Ping: { if (user === undefined) { return; } if ((performance.now() - user.lastPingReset) >= 1000) { user.allowedPings = 10; user.lastPingReset = performance.now(); } if (user.allowedPings > 0) { user.allowedPings--; const cursorX = reader.readFloat(); const cursorY = reader.readInt(); const packet = createWriter(Endian.LE, 9).writeByte(MessageType.Ping).writeFloat(cursorX).writeInt(cursorY).toBuffer(); sendToAll(user, packet); } break; } } } }); }); let isShuttingDown = false; function shutdown() { if (isShuttingDown) { return; } isShuttingDown = true; Console.printInfo("Shutting down..."); websocketServer.close(async () => { await fastify.close(); Console.cleanup(); console.log("Goodbye!"); }); } process.on("SIGQUIT", shutdown); process.on("SIGINT", shutdown); process.on("SIGUSR2", shutdown);