2024-04-18 23:18:49 +01:00
import { createReader , createWriter , Endian } from "bufferstuff" ;
import { WebSocketServer } from "ws" ;
2024-04-22 16:05:42 +01:00
import Fastify from "fastify" ;
2024-04-23 00:58:07 +01:00
import FastifyFormBody from "@fastify/formbody" ;
import FastifyCookie from "@fastify/cookie" ;
2024-04-22 16:05:42 +01:00
import FastifyView from "@fastify/view" ;
import EJS from "ejs" ;
2024-04-18 23:18:49 +01:00
import Config from "./objects/Config" ;
import FunkyArray from "./objects/FunkyArray" ;
2024-04-21 15:35:47 +01:00
import RemoteUser from "./objects/RemoteUser" ;
2024-04-18 23:18:49 +01:00
import { MessageType } from "./enums/MessageType" ;
2024-04-21 15:35:47 +01:00
import Database from "./objects/Database" ;
2024-04-22 02:01:14 +01:00
import { Console } from "hsconsole" ;
2024-04-23 00:58:07 +01:00
import UserService from "./services/UserService" ;
import UsernameData from "./interfaces/UsernameData" ;
import { randomBytes } from "crypto" ;
import SessionUser from "./objects/SessionUser" ;
import PasswordUtility from "./utilities/PasswordUtility" ;
2024-04-23 17:01:25 +01:00
import CreateEditPartyData from "./interfaces/CreateEditPartyData" ;
2024-04-24 00:05:57 +01:00
import JoinPartyData from "./interfaces/JoinPartyData" ;
import UserParty from "./objects/UserParty" ;
import IdData from "./interfaces/IdData" ;
2024-04-22 02:01:14 +01:00
Console . customHeader ( ` MultiProbe server started at ${ new Date ( ) } ` ) ;
2024-04-18 23:18:49 +01:00
2024-04-21 15:35:47 +01:00
const users = new FunkyArray < string , RemoteUser > ( ) ;
new Database ( Config . database . address , Config . database . port , Config . database . username , Config . database . password , Config . database . name ) ;
2024-04-18 23:18:49 +01:00
2024-04-22 17:02:31 +01:00
// Web stuff
2024-04-23 00:58:07 +01:00
const sessions = new FunkyArray < string , SessionUser > ( ) ;
const sessionExpiryInterval = setInterval ( ( ) = > {
const currentTime = Date . now ( ) ;
for ( const key of sessions . keys ) {
const session = sessions . get ( key ) ;
if ( ! session || ( session && currentTime >= session . validityPeriod . getTime ( ) ) ) {
sessions . remove ( key ) ;
}
}
} , 3600000 ) ;
2024-04-22 16:05:42 +01:00
const fastify = Fastify ( {
logger : false
} ) ;
fastify . register ( FastifyView , {
engine : {
ejs : EJS
}
2024-04-23 00:58:07 +01:00
} ) ;
fastify . register ( FastifyFormBody ) ;
fastify . register ( FastifyCookie , {
secret : Config.session.secret ,
parseOptions : {
path : "/" ,
secure : true
}
} ) ;
2024-04-23 17:01:25 +01:00
fastify . setNotFoundHandler ( async ( req , res ) = > {
return res . status ( 404 ) . view ( "templates/404.ejs" , { } ) ;
} ) ;
2024-04-23 00:58:07 +01:00
function validateSession ( cookies : { [ cookieName : string ] : string | undefined } ) {
if ( "MP_SESSION" in cookies && typeof ( cookies [ "MP_SESSION" ] ) === "string" ) {
const key = FastifyCookie . unsign ( cookies [ "MP_SESSION" ] , Config . session . secret ) ;
if ( key . valid && sessions . has ( key . value ? ? "badkey" ) ) {
return sessions . get ( key . value ? ? "badkey" ) ;
}
}
return undefined ;
}
2024-04-22 16:05:42 +01:00
2024-04-23 17:01:25 +01:00
// Get Methods
2024-04-22 16:05:42 +01:00
fastify . get ( "/" , async ( req , res ) = > {
2024-04-23 00:58:07 +01:00
let session :SessionUser | undefined ;
if ( session = validateSession ( req . cookies ) ) {
const user = await UserService . GetUser ( session . userId ) ;
2024-04-23 17:01:25 +01:00
const parties = await UserService . GetUserParties ( session . userId ) ;
2024-04-24 00:05:57 +01:00
const activeUserParty = await UserService . GetActiveParty ( session . userId ) ;
2024-04-23 00:58:07 +01:00
if ( user ) {
2024-04-24 00:05:57 +01:00
return res . view ( "templates/home.ejs" , { user , parties , activeUserParty } ) ;
2024-04-23 00:58:07 +01:00
}
return res . view ( "templates/index.ejs" , { } ) ;
}
2024-04-22 16:05:42 +01:00
return res . view ( "templates/index.ejs" , { } ) ;
} ) ;
2024-04-22 17:02:31 +01:00
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" , { } ) ;
} ) ;
2024-04-24 00:05:57 +01:00
fastify . get ( "/account/logout" , async ( req , res ) = > {
res . clearCookie ( "MP_SESSION" ) ;
return res . redirect ( 302 , "/" ) ;
} ) ;
2024-04-23 17:01:25 +01:00
fastify . get ( "/party/create" , async ( req , res ) = > {
let session :SessionUser | undefined ;
if ( ! ( session = validateSession ( req . cookies ) ) ) {
return res . redirect ( 302 , "/" ) ;
}
2024-04-24 00:05:57 +01:00
return res . view ( "templates/party/createedit.ejs" , { session } ) ;
2024-04-23 17:01:25 +01:00
} ) ;
fastify . get ( "/party/join" , async ( req , res ) = > {
let session :SessionUser | undefined ;
if ( ! ( session = validateSession ( req . cookies ) ) ) {
return res . redirect ( 302 , "/" ) ;
}
2024-04-24 00:05:57 +01:00
return res . view ( "templates/party/join.ejs" , { session } ) ;
} ) ;
fastify . get ( "/party/setactive" , async ( req , res ) = > {
let session :SessionUser | undefined ;
if ( ! ( session = validateSession ( req . cookies ) ) ) {
return res . redirect ( 302 , "/" ) ;
}
const data = req . query as IdData ;
const numericId = parseInt ( data . id ? ? "-1" ) ;
if ( typeof ( data . id ) !== "string" || isNaN ( numericId ) ) {
return res . redirect ( 302 , "/" ) ;
}
await UserService . SetActiveParty ( session . userId , numericId ) ;
return res . redirect ( 302 , "/" ) ;
} ) ;
fastify . get ( "/party/deactivate" , async ( req , res ) = > {
let session :SessionUser | undefined ;
if ( ! ( session = validateSession ( req . cookies ) ) ) {
return res . redirect ( 302 , "/" ) ;
}
await UserService . DeactivateCurrentParty ( session . userId ) ;
return res . redirect ( 302 , "/" ) ;
2024-04-23 00:58:07 +01:00
} ) ;
// Post Methods
fastify . post ( "/account/register" , async ( req , res ) = > {
const data = req . body as UsernameData ;
if ( typeof ( data . username ) !== "string" || typeof ( data . password ) !== "string" || data . username . length > 32 || data . password . length < 8 ) {
return res . view ( "templates/account/register.ejs" , { } ) ;
}
const username = data . username . replaceAll ( "<" , "<" ) . replaceAll ( ">" , ">" ) ;
await UserService . CreateUser ( 0 , username , data . password ) ;
const user = await UserService . GetUserByUsername ( username ) ;
if ( ! user ) {
return res . view ( "templates/account/register.ejs" , { } ) ;
}
const validPeriod = new Date ( ) ;
validPeriod . setTime ( validPeriod . getTime ( ) + Config . session . validity ) ;
const key = randomBytes ( Config . session . length ) . toString ( "hex" ) ;
sessions . set ( key , new SessionUser ( user . Id , validPeriod ) ) ;
res . setCookie ( "MP_SESSION" , key , {
path : "/" ,
signed : true
} ) ;
return res . redirect ( 302 , "/" ) ;
} ) ;
fastify . post ( "/account/login" , async ( req , res ) = > {
const data = req . body as UsernameData ;
if ( typeof ( data . username ) !== "string" || typeof ( data . password ) !== "string" || data . username . length > 32 || data . password . length < 8 ) {
return res . view ( "templates/account/login.ejs" , { } ) ;
}
const user = await UserService . GetUserByUsername ( data . username ) ;
if ( ! user ) {
return res . view ( "templates/account/login.ejs" , { } ) ;
}
if ( await PasswordUtility . ValidatePassword ( user . PasswordHash , user . PasswordSalt , data . password ) ) {
const validPeriod = new Date ( ) ;
validPeriod . setTime ( validPeriod . getTime ( ) + Config . session . validity ) ;
const key = randomBytes ( Config . session . length ) . toString ( "hex" ) ;
sessions . set ( key , new SessionUser ( user . Id , validPeriod ) ) ;
res . setCookie ( "MP_SESSION" , key , {
path : "/" ,
signed : true
} ) ;
return res . redirect ( 302 , "/" ) ;
}
return res . view ( "templates/account/login.ejs" , { } ) ;
2024-04-22 17:02:31 +01:00
} ) ;
2024-04-23 17:01:25 +01:00
fastify . post ( "/party/create" , async ( req , res ) = > {
try {
let session :SessionUser | undefined ;
if ( ! ( session = validateSession ( req . cookies ) ) ) {
return res . redirect ( 302 , "/" ) ;
}
const data = req . body as CreateEditPartyData ;
if ( typeof ( data . partyName ) !== "string" || typeof ( data . partyRef ) !== "string" || data . partyName . length === 0 || data . partyRef . length === 0 ) {
2024-04-24 00:05:57 +01:00
return res . view ( "templates/party/createedit.ejs" , { session , partyName : data.partyName ? ? "" , partyRef : data.partyRef ? ? "" } ) ;
2024-04-23 17:01:25 +01:00
}
const party = await UserService . GetPartyByPartyRef ( data . partyRef )
if ( party != null ) {
2024-04-24 00:05:57 +01:00
return res . view ( "templates/party/createedit.ejs" , { session , partyName : data.partyName ? ? "" , partyRef : data.partyRef ? ? "" , error : "A group with that Party ID already exists" } ) ;
2024-04-23 17:01:25 +01:00
}
await UserService . CreateParty ( session . userId , data . partyName , data . partyRef ) ;
return res . redirect ( 302 , "/" ) ;
} catch ( e ) {
console . error ( e ) ;
}
} ) ;
2024-04-24 00:05:57 +01:00
fastify . post ( "/party/join" , async ( req , res ) = > {
try {
let session :SessionUser | undefined ;
if ( ! ( session = validateSession ( req . cookies ) ) ) {
return res . redirect ( 302 , "/" ) ;
}
const data = req . body as JoinPartyData ;
if ( typeof ( data . partyRef ) !== "string" || data . partyRef . length === 0 ) {
return res . view ( "templates/party/join.ejs" , { partyRef : data.partyRef ? ? "" } ) ;
}
const party = await UserService . GetPartyByPartyRef ( data . partyRef ) ;
if ( party == null ) {
return res . view ( "templates/party/join.ejs" , { session , partyRef : data.partyRef ? ? "" , error : "That Join Code / Party ID is invalid." } ) ;
}
const userPartyExisting = await UserService . GetUserPartyForUser ( session . userId , party . Id ) ;
if ( userPartyExisting != null ) {
return res . view ( "templates/party/join.ejs" , { session , partyRef : data.partyRef ? ? "" , error : "You are already in this group." } ) ;
}
await UserService . AddUserToParty ( session . userId , party . Id ) ;
return res . redirect ( 302 , "/" ) ;
} catch ( e ) {
console . error ( e ) ;
}
} ) ;
2024-04-22 17:02:31 +01:00
// Websocket stuff
2024-04-22 16:05:42 +01:00
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!" ) ;
} ) ;
} ) ;
2024-04-18 23:18:49 +01:00
2024-04-21 15:35:47 +01:00
function sendToAllButSelf ( user :RemoteUser , data :Buffer ) {
2024-04-18 23:18:49 +01:00
users . forEach ( otherUser = > {
if ( otherUser . id !== user . id && otherUser . currentURL === user . currentURL ) {
otherUser . send ( data ) ;
}
} ) ;
}
2024-04-21 15:35:47 +01:00
function sendToAll ( user :RemoteUser , data :Buffer ) {
2024-04-19 00:18:12 +01:00
users . forEach ( otherUser = > {
if ( otherUser . currentURL === user . currentURL ) {
otherUser . send ( data ) ;
}
} ) ;
}
2024-04-22 16:05:42 +01:00
websocketServer . on ( "connection" , ( socket ) = > {
2024-04-18 23:18:49 +01:00
const myUUID = crypto . randomUUID ( ) ;
2024-04-21 15:35:47 +01:00
let user :RemoteUser ;
2024-04-18 23:18:49 +01:00
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 ( ) ;
2024-04-20 17:20:13 +01:00
const rawURL = reader . readString ( ) ;
let page = rawURL . toLowerCase ( ) . replace ( ".htm" , "" ) . replace ( ".html" , "" ) ;
2024-04-18 23:18:49 +01:00
if ( page === "index" ) {
page = "" ;
}
let lengthOfUsernames = 0 ;
2024-04-21 15:35:47 +01:00
const usersOnPage = new Array < RemoteUser > ( ) ;
2024-04-18 23:18:49 +01:00
await users . forEach ( otherUser = > {
2024-04-19 09:44:26 +01:00
if ( otherUser . currentURL === page ) {
usersOnPage . push ( otherUser ) ;
lengthOfUsernames += otherUser . username . length + 1 ; // + 1 for length byte
}
2024-04-18 23:18:49 +01:00
} ) ;
2024-04-19 09:44:26 +01:00
const usersToSend = createWriter ( Endian . LE , 3 + ( usersOnPage . length * 12 ) + lengthOfUsernames ) . writeByte ( MessageType . Clients ) . writeUShort ( usersOnPage . length ) ;
for ( const otherUser of usersOnPage ) {
2024-04-19 00:18:12 +01:00
usersToSend . writeUInt ( otherUser . id ) . writeShortString ( otherUser . username ) . writeFloat ( otherUser . cursorX ) . writeInt ( otherUser . cursorY ) ;
2024-04-19 09:44:26 +01:00
}
2024-04-21 15:35:47 +01:00
user = users . set ( myUUID , new RemoteUser ( socket , username , page , rawURL ) ) ;
2024-04-18 23:18:49 +01:00
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 ;
}
2024-04-19 00:18:12 +01:00
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 ( ) ) ;
2024-04-18 23:18:49 +01:00
break ;
}
case MessageType . Ping :
{
2024-04-20 17:20:13 +01:00
if ( user === undefined ) {
return ;
}
2024-04-22 16:05:42 +01:00
if ( ( performance . now ( ) - user . lastPingReset ) >= 1000 ) {
2024-04-20 17:20:13 +01:00
user . allowedPings = 10 ;
2024-04-22 16:05:42 +01:00
user . lastPingReset = performance . now ( ) ;
2024-04-20 17:20:13 +01:00
}
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 ) ;
}
2024-04-18 23:18:49 +01:00
break ;
}
}
}
} ) ;
2024-04-22 16:05:42 +01:00
} ) ;
let isShuttingDown = false ;
function shutdown() {
if ( isShuttingDown ) {
return ;
}
isShuttingDown = true ;
Console . printInfo ( "Shutting down..." ) ;
websocketServer . close ( async ( ) = > {
await fastify . close ( ) ;
2024-04-23 00:58:07 +01:00
clearInterval ( sessionExpiryInterval ) ;
2024-04-22 16:05:42 +01:00
Console . cleanup ( ) ;
console . log ( "Goodbye!" ) ;
} ) ;
}
process . on ( "SIGQUIT" , shutdown ) ;
process . on ( "SIGINT" , shutdown ) ;
2024-04-23 17:01:25 +01:00
//process.on("SIGUSR2", shutdown);