2024-03-25 10:11:34 -04:00
const config = { "__proto__" : null , port : 9091 } ;
2024-02-26 09:33:55 -05:00
var uidcounter = 0 ;
Object . freeze ( config ) ; // Config should not be modified after initialization!
const ChannelStorage = new Map ( ) ;
2024-02-27 10:23:34 -05:00
const UserAttachedChannelStorage = new Map ( ) ; //uid=>channel resolve
const UserAttachedNameStorage = new Map ( ) ; // uid=>username resolve
const ReverseNameToUidStorage = new Map ( ) ; // username=>uid resolve
2024-02-26 09:33:55 -05:00
/** @section guacamole **/
const ParseCommandString = function ( instruction ) {
if ( typeof instruction !== "string" ) {
return ;
}
let position = - 1 ;
let sections = new Array ( ) ;
for ( ; ; ) {
let length = instruction . indexOf ( '.' , position + 1 ) ;
if ( length === - 1 ) {
break ;
}
position = ( parseInt ( instruction . slice ( position + 1 , length ) ) + length ) + 1 ; // todo: not overflow
sections . push ( instruction . slice ( length + 1 , position )
. replace ( /'/g , "'" )
. replace ( /"/g , '"' )
. replace ( ///g , '/' )
. replace ( /</g , '<' )
. replace ( />/g , '>' )
. replace ( /&/g , '&' )
) ;
if ( instruction . slice ( position , position + 1 ) === ';' ) {
break ;
}
}
return sections ;
}
const EncodeCommandString = function ( sections ) {
if ( Array . isArray ( sections ) === false ) {
return ;
}
let instruction = new String ( ) ;
const argv = sections ;
for ( let argc = 0 ; argc < sections . length ; argc ++ ) {
if ( typeof sections [ argc ] !== "string" ) {
argv [ argc ] = sections [ argc ] . toString ( ) ;
}
argv [ argc ] = argv [ argc ]
. replace ( /'/g , "'" )
. replace ( /"/g , '"' )
. replace ( /\//g , '/' )
. replace ( /</g , '<' )
. replace ( />/g , '>' )
. replace ( /&/g , '&' )
;
instruction = instruction . concat ( argv [ argc ] . length . toString ( ) , "." , argv [ argc ] )
instruction += ( argc === sections . length - 1 ) ? ';' : ',' ;
}
return instruction ;
}
/** @section sv_functions **/
const MessageParser = function ( webSocket , message , isBinary ) {
const msg = new TextDecoder ( "utf-8" ) . decode ( message ) ;
if ( isBinary ) {
// TODO: command functions
try {
const args = ParseCommandString ( msg ) ;
switch ( args [ 0 ] ) {
case "rename" : {
// todo: make users be able to rename based on parameters given to rename
var nickname = ` guest ${ Math . floor ( Math . random ( ) * 99999 ) } ` ;
2024-02-27 10:23:34 -05:00
if ( nickname . length > 32 ) { // if it's too long
2024-02-26 09:33:55 -05:00
nickname = ` guest ${ Math . floor ( Math . random ( ) * 99999 ) } ` ;
}
2024-02-27 10:23:34 -05:00
// this should probably be a while or something (only do this if the name exists)
if ( ReverseNameToUidStorage . has ( nickname ) === true ) {
if ( ReverseNameToUidStorage . get ( nickname ) !== webSocket . getUserData ( ) . uid ) {
nickname = ` guest ${ Math . floor ( Math . random ( ) * 99999 ) } ` ;
}
}
2024-02-26 09:33:55 -05:00
UserAttachedNameStorage . set ( webSocket . getUserData ( ) . uid , nickname ) ;
ReverseNameToUidStorage . set ( nickname , webSocket . getUserData ( ) . uid ) ;
webSocket . send ( EncodeCommandString ( [ "rename" , 0 , "" , nickname ] ) , true ) ;
break ;
}
case "connect" : {
2024-03-23 12:25:57 -04:00
if ( args . length <= 2 ) {
2024-02-28 08:41:20 -05:00
webSocket . send ( EncodeCommandString ( [ "error" , "need more arguments" , 768 ] ) , true ) ;
2024-02-26 09:33:55 -05:00
break ;
}
let getChannelId = args [ 1 ] ;
if ( server . numSubscribers ( ` channels/ ${ getChannelId } ` ) >= 2 ) {
2024-03-25 13:00:16 -04:00
// this channel is full
2024-02-28 08:41:20 -05:00
webSocket . send ( EncodeCommandString ( [ "error" , "channel has too many members" , 513 ] ) , true ) ;
2024-02-26 09:33:55 -05:00
break ;
}
2024-02-27 10:23:34 -05:00
if ( UserAttachedNameStorage . has ( webSocket . getUserData ( ) . uid ) === false ) {
2024-02-26 09:33:55 -05:00
// user doesn't have a username
2024-02-28 08:41:20 -05:00
webSocket . send ( EncodeCommandString ( [ "error" , "you need a nickname" , 769 ] ) , true ) ;
2024-02-26 09:33:55 -05:00
break ;
}
2024-02-27 10:23:34 -05:00
if ( ChannelStorage . has ( getChannelId ) === false ) {
2024-02-26 09:33:55 -05:00
// make the channel right then and there
2024-03-25 13:00:16 -04:00
console . log ( ` creating # ${ getChannelId } (fourCC: ${ args2 } ) for ( ${ webSocket . getUserData ( ) . uid } ) ` ) ;
2024-03-23 12:25:57 -04:00
ChannelStorage . set ( getChannelId , { "owner" : webSocket . getUserData ( ) . uid , "fourCC" : args [ 2 ] } ) ;
2024-02-26 09:33:55 -05:00
} ;
2024-03-23 12:25:57 -04:00
if ( ChannelStorage . get ( getChannelId ) . fourCC !== args [ 2 ] ) {
// if the fourCC is not the same as the channel, error as such
2024-03-25 13:00:16 -04:00
webSocket . send ( EncodeCommandString ( [ "error" , ` Activity is incompatible! (You: ${ args [ 2 ] } , Remote: ${ ChannelStorage . get ( getChannelId ) . fourCC } ). ` , 256 ] ) , true ) ;
2024-03-23 12:25:57 -04:00
break ;
}
2024-02-26 09:33:55 -05:00
if ( webSocket . isSubscribed ( ` channels/ ${ getChannelId } ` ) === true ) {
// already in this channel
2024-02-28 08:41:20 -05:00
webSocket . send ( EncodeCommandString ( [ "error" , "already in this channel" , 517 ] ) , true ) ;
2024-02-26 09:33:55 -05:00
break ;
} ;
2024-02-27 10:23:34 -05:00
if ( UserAttachedChannelStorage . has ( webSocket . getUserData ( ) . uid ) === true ) {
2024-02-26 09:33:55 -05:00
// already in a channel that isn't the one we're trying to join
2024-02-28 08:41:20 -05:00
webSocket . send ( EncodeCommandString ( [ "error" , "already in another channel" , 797 ] ) , true ) ;
2024-02-26 09:33:55 -05:00
break ;
}
2024-02-27 09:14:08 -05:00
console . log ( ` user ${ UserAttachedNameStorage . get ( webSocket . getUserData ( ) . uid ) } ( ${ webSocket . getUserData ( ) . uid } ) joined # ${ getChannelId } ` ) ;
2024-02-28 08:41:20 -05:00
webSocket . send ( EncodeCommandString ( [ "ready" , server . numSubscribers ( ` channels/ ${ getChannelId } ` ) ] ) , true ) ;
2024-02-26 09:33:55 -05:00
webSocket . subscribe ( ` channels/ ${ getChannelId } ` ) ;
UserAttachedChannelStorage . set ( webSocket . getUserData ( ) . uid , getChannelId ) ;
// do we keep adduser?
if ( ChannelStorage . get ( getChannelId ) . owner !== webSocket . getUserData ( ) . uid ) {
2024-02-27 09:14:08 -05:00
// send other user's presense if we don't own the channel (if we're not owner)
2024-02-26 09:33:55 -05:00
webSocket . send ( EncodeCommandString ( [ "adduser" , 1 , UserAttachedNameStorage . get ( ChannelStorage . get ( getChannelId ) . owner ) ] ) , true ) ;
}
// send to other user (or nobody) that we joined this channel
webSocket . publish ( ` channels/ ${ getChannelId } ` , EncodeCommandString ( [ "adduser" , 1 , UserAttachedNameStorage . get ( webSocket . getUserData ( ) . uid ) ] ) , true ) ;
break ;
}
2024-02-27 09:14:08 -05:00
case "disconnect" : {
2024-02-27 10:23:34 -05:00
if ( UserAttachedChannelStorage . has ( webSocket . getUserData ( ) . uid ) === true ) {
2024-02-27 09:14:08 -05:00
console . log ( ` user ${ UserAttachedNameStorage . get ( webSocket . getUserData ( ) . uid ) } ( ${ webSocket . getUserData ( ) . uid } ) left # ${ UserAttachedChannelStorage . get ( webSocket . getUserData ( ) . uid ) } ` ) ;
webSocket . unsubscribe ( ` channels/ ${ UserAttachedChannelStorage . get ( webSocket . getUserData ( ) . uid ) } ` ) ;
webSocket . publish (
` channels/ ${ UserAttachedChannelStorage . get ( webSocket . getUserData ( ) . uid ) } ` ,
EncodeCommandString ( [ "remuser" , UserAttachedNameStorage . get ( webSocket . getUserData ( ) . uid ) ] ) ,
true
) ;
2024-03-25 13:00:16 -04:00
// if channel is empty, delete it
2024-02-27 09:14:08 -05:00
if ( server . numSubscribers ( ` channels/ ${ UserAttachedChannelStorage . get ( webSocket . getUserData ( ) . uid ) } ` ) === 0 ) {
2024-02-27 17:50:05 -05:00
console . log ( ` deleting # ${ UserAttachedChannelStorage . get ( webSocket . getUserData ( ) . uid ) } since ( ${ webSocket . getUserData ( ) . uid } ) left, leaving it with no members ` ) ;
2024-02-27 09:14:08 -05:00
ChannelStorage . delete ( UserAttachedChannelStorage . get ( webSocket . getUserData ( ) . uid ) ) ;
}
UserAttachedChannelStorage . delete ( webSocket . getUserData ( ) . uid ) ;
2024-02-28 08:41:20 -05:00
// user is disconnecting next frame!
2024-02-27 09:14:08 -05:00
}
return ;
}
2024-02-26 09:33:55 -05:00
case "sync" : {
2024-02-28 08:41:20 -05:00
// client is alive... TODO check if this is too ahead/behind
2024-02-26 09:33:55 -05:00
break ;
}
default : {
console . warn ( "unknown command" , args ) ;
}
}
} catch ( e ) {
console . log ( e ) ;
webSocket . end ( 1003 , new TextEncoder ( "utf-8" ) . encode ( "Unknown format." ) ) ;
return ;
}
return ;
} else {
// send data to channel
webSocket . publish ( ` channels/ ${ UserAttachedChannelStorage . get ( webSocket . getUserData ( ) . uid ) } ` , message , false ) ;
// ask other user if alive still
webSocket . publish ( ` channels/ ${ UserAttachedChannelStorage . get ( webSocket . getUserData ( ) . uid ) } ` , EncodeCommandString ( [ "sync" , ( new Date ) . getTime ( ) ] ) , true ) ;
2024-02-28 08:41:20 -05:00
webSocket . ping ( "pulse" ) ;
2024-02-26 09:33:55 -05:00
}
//console.log(`${new TextDecoder("utf-8").decode(webSocket.getRemoteAddressAsText())} message: ${msg}`);
}
const UserStateRequest = function ( webSocket , state , data = null ) {
switch ( state ) {
case "open" : {
//console.log("%s client hello", new TextDecoder("utf-8").decode(webSocket.getRemoteAddressAsText()));
2024-02-28 08:41:20 -05:00
webSocket . subscribe ( "system" ) ;
webSocket . ping ( "welcome" ) ;
2024-02-26 09:33:55 -05:00
break ;
}
case "close" : {
// close event gets WS disconnect code and the client's termination message (avaliable if data.code is 1000, 3xxx, or 4xxx, but only 1000 is standard)
// but we DO NOT get to access any WS methods, since they've been closed, so we don't actually know who left.
if ( data . code === 1000 ) {
console . log ( ` client exiting, reason: ${ new TextDecoder ( "utf-8" ) . decode ( data . message ) } ` ) ;
}
break ;
}
}
}
/** @section creation **/
const server = require ( "uWebSockets.js" ) . App ( { } ) ;
/** @section config **/
server . ws ( "/connect" , {
// options
"compression" : require ( "uWebSockets.js" ) . DEDICATED _COMPRESSOR _3KB , // do we need a compressor for this kind of protocol, uWS actively despises compression, so it's worth considering.
// here be events
"upgrade" : function ( rs , rq , cx ) {
rs . upgrade ( { "uid" : ( uidcounter ++ ) } , /* very important, this sets a UserData object (which can contain anything we like) to the WSConnection instance. This can only be done here. */
rq . getHeader ( "sec-websocket-key" ) ,
rq . getHeader ( "sec-websocket-protocol" ) ,
rq . getHeader ( "sec-websocket-extensions" ) ,
cx ) ;
} ,
"message" : MessageParser ,
"open" : function ( ws ) { UserStateRequest ( ws , "open" ) } ,
"close" : function ( ws , code , message ) { UserStateRequest ( ws , "close" , { code , message } ) } ,
"drain" : function ( ws ) { console . log ( ` WS going through back-pressure!: ${ ws . getBufferedAmount ( ) } ` ) }
} ) ;
2024-02-28 08:41:20 -05:00
server . listen ( config . port , function ( token ) { console . log ( token ? ` open on ${ config . port } ` : ` failed to listen to ${ config . port } ` ) } ) ;
var serverIsClosing = false ;
require ( "process" ) . on ( 'SIGINT' , function ( ) {
if ( serverIsClosing ) { return } ;
server . publish ( "system" , EncodeCommandString ( [ "error" , "server is closing" , 0 ] ) , true ) ;
serverIsClosing = true ;
require ( "process" ) . on ( 'SIGINT' , function ( ) { console . warn ( "emergency shutdown was engaged, clients may be confused!" ) ; require ( "process" ) . exit ( ) } ) ;
console . log ( "shutting down server, giving clients 5 seconds..." ) ;
setTimeout ( function ( ) { console . log ( "server stopping now" ) ; server . close ( ) ; require ( "process" ) . exit ( ) } , 5000 )
2024-03-25 10:11:34 -04:00
} ) ;