messenger.js/server.js

238 lines
10 KiB
JavaScript
Raw Normal View History

const config = {"__proto__": null, port: 19180};
var uidcounter = 0;
Object.freeze(config); // Config should not be modified after initialization!
const ChannelStorage = new Map();
const UserAttachedChannelStorage = new Map(); //uid=>channel resolve
const UserAttachedNameStorage = new Map(); // uid=>username resolve
const ReverseNameToUidStorage = new Map(); // username=>uid resolve
/** @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;
if(position > instruction.length) {
return;
}
sections.push(instruction.slice(length + 1, position)
.replace(/'/g, "'")
.replace(/"/g, '"')
.replace(///g, '/')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/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, '&amp;' )
.replace(/>/g, '&gt;' )
.replace(/</g, '&lt;' )
.replace(/\//g, '&#x2F' )
.replace(/"/g, '&quot;')
.replace(/'/g, "&#x27;")
;
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": {
var nickname = (args.length > 1) ? args[1] : `guest${Math.floor(Math.random() * 99999)}`;
if(nickname.length < 3 || nickname.length > 32) { // if it's too short or long
nickname = `guest${Math.floor(Math.random() * 99999)}`;
}
// 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)}`;
}
}
if(UserAttachedNameStorage.has(webSocket.getUserData().uid)) {
console.log(`user "${UserAttachedNameStorage.get(webSocket.getUserData().uid)}" (${webSocket.getUserData().uid}) renamed to "${nickname}"`);
ReverseNameToUidStorage.delete(UserAttachedNameStorage.get(webSocket.getUserData().uid));
}
UserAttachedNameStorage.set(webSocket.getUserData().uid, nickname);
ReverseNameToUidStorage.set(nickname, webSocket.getUserData().uid);
webSocket.send(EncodeCommandString(["rename", 0, "", nickname]), true);
break;
}
case "connect": {
if(args.length <= 2) {
webSocket.send(EncodeCommandString(["error", "need more arguments", 768]), true);
break;
}
let getChannelId = args[1];
if(server.numSubscribers(`channels/${getChannelId}`) >= 2) {
// this channel is full
webSocket.send(EncodeCommandString(["error", "channel has too many members", 513]), true);
break;
}
if(UserAttachedNameStorage.has(webSocket.getUserData().uid) === false) {
// user doesn't have a username
webSocket.send(EncodeCommandString(["error", "you need a nickname", 769]), true);
break;
}
if(ChannelStorage.has(getChannelId) === false) {
// make the channel right then and there
console.log(`creating #${getChannelId} (fourCC: ${args[2]}) for (${webSocket.getUserData().uid})`);
ChannelStorage.set(getChannelId, {"owner": webSocket.getUserData().uid, "fourCC": args[2]});
};
if(ChannelStorage.get(getChannelId).fourCC !== args[2]) {
// if the fourCC is not the same as the channel, error as such
webSocket.send(EncodeCommandString(["error", `Activity is incompatible! (You: ${args[2]}, Remote: ${ChannelStorage.get(getChannelId).fourCC}).`, 256]), true);
break;
}
if(webSocket.isSubscribed(`channels/${getChannelId}`) === true) {
// already in this channel
webSocket.send(EncodeCommandString(["error", "already in this channel", 517]), true);
break;
};
if(UserAttachedChannelStorage.has(webSocket.getUserData().uid) === true) {
// already in a channel that isn't the one we're trying to join
webSocket.send(EncodeCommandString(["error", "already in another channel", 797]), true);
break;
}
console.log(`user "${UserAttachedNameStorage.get(webSocket.getUserData().uid)}" (${webSocket.getUserData().uid}) joined #${getChannelId}`);
webSocket.send(EncodeCommandString(["ready", server.numSubscribers(`channels/${getChannelId}`)]), true);
webSocket.subscribe(`channels/${getChannelId}`);
UserAttachedChannelStorage.set(webSocket.getUserData().uid, getChannelId);
// do we keep adduser?
if(ChannelStorage.get(getChannelId).owner !== webSocket.getUserData().uid) {
// send other user's presense if we don't own the channel (if we're not owner)
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;
}
case "disconnect": {
if(UserAttachedChannelStorage.has(webSocket.getUserData().uid) === true) {
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
);
// if channel is empty, delete it
if(server.numSubscribers(`channels/${UserAttachedChannelStorage.get(webSocket.getUserData().uid)}`) === 0) {
console.log(`deleting #${UserAttachedChannelStorage.get(webSocket.getUserData().uid)} since (${webSocket.getUserData().uid}) left, leaving it with no members`);
ChannelStorage.delete(UserAttachedChannelStorage.get(webSocket.getUserData().uid));
}
ReverseNameToUidStorage.delete(UserAttachedNameStorage.get(webSocket.getUserData().uid));
UserAttachedNameStorage.delete(webSocket.getUserData().uid);
UserAttachedChannelStorage.delete(webSocket.getUserData().uid);
// user is disconnecting next frame!
}
return;
}
case "sync": {
// client is alive... TODO check if this is too ahead/behind
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);
webSocket.ping("pulse");
}
//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()));
webSocket.subscribe("system");
webSocket.ping("welcome");
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()}`)}
});
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
});