mirror of https://github.com/Ale32bit/Soqet.git
377 lines
11 KiB
JavaScript
377 lines
11 KiB
JavaScript
/*
|
|
Soqet Server by Ale32bit
|
|
|
|
MIT License
|
|
|
|
*/
|
|
|
|
const WebSocket = require("ws");
|
|
const net = require("net");
|
|
const crypto = require("crypto");
|
|
const config = require("./config.json");
|
|
|
|
const channels = {};
|
|
const clients = [];
|
|
const WILDCARD = '*';
|
|
|
|
// ---- FUNCTIONS ----
|
|
|
|
function sha256(str) { // Hash a string using SHA256
|
|
return crypto.createHash("sha256").update(str).digest("hex");
|
|
}
|
|
|
|
function random(len = 16, prefix = "g") { // Generate a random secure string in hex
|
|
return prefix + crypto.randomBytes(len).toString("hex");
|
|
}
|
|
|
|
function randomToken() { // Same with random but in base64 and non-alphanumerical chars removed -- Currently not used
|
|
return crypto.randomBytes(42).toString("base64").replace(/[^a-zA-Z0-9 -]/g, "")
|
|
}
|
|
|
|
function incRandom(min, max) { // random number inclusive
|
|
min = Math.ceil(min);
|
|
max = Math.floor(max);
|
|
return Math.floor(Math.random() * (max - min + 1)) + min
|
|
}
|
|
|
|
function hexToBase36(input) { // hexadecimal number to base 36
|
|
const byte = 48 + Math.floor(input / 7);
|
|
return String.fromCharCode(byte + 39 > 122 ? 101 : byte > 57 ? byte + 39 : byte);
|
|
}
|
|
|
|
function createID(token) { // From krist-utils, generate an ID from the token, like an HASH
|
|
token = sha256(token);
|
|
let prefix = "a";
|
|
let len = 31;
|
|
let hash = sha256(sha256(token));
|
|
let chars = [];
|
|
|
|
for (let i = 0; i <= len; i++) {
|
|
chars[i] = hash.substring(0, 2);
|
|
hash = sha256(sha256(hash));
|
|
}
|
|
|
|
for (let i = 0; i <= len;) {
|
|
const index = parseInt(hash.substring(2 * i, 2 + (2 * i)), 16) % (len + 1);
|
|
|
|
if (!chars[index]) {
|
|
hash = sha256(hash);
|
|
} else {
|
|
prefix += hexToBase36(parseInt(chars[index], 16));
|
|
chars[index] = undefined;
|
|
i++;
|
|
}
|
|
}
|
|
|
|
return prefix;
|
|
}
|
|
|
|
function disconnect(sID) { // remove a client UUID from all channels.
|
|
for (let chn in channels) {
|
|
let ch = channels[chn];
|
|
for (let i = 0; i < ch.length; i++) {
|
|
if (ch[i] === sID) {
|
|
let index = ch.indexOf(sID);
|
|
ch.splice(index, 1)
|
|
}
|
|
}
|
|
if (ch.length === 0) {
|
|
delete channels[chn]
|
|
}
|
|
}
|
|
}
|
|
|
|
const server = new WebSocket.Server({ // create the websocket server, port is from config.json
|
|
port: config.port,
|
|
});
|
|
|
|
const netServer = net.createServer();
|
|
|
|
function getClient(sID) { // just a for loop to get the ws client from the session ID
|
|
for (let item of clients) {
|
|
if (item.sID === sID) {
|
|
return item;
|
|
}
|
|
}
|
|
}
|
|
|
|
function transmit(channel, message, meta, ignore = null) { // transmit a message to the channel. WILDCARD channel is read-only
|
|
try {
|
|
if (channel === WILDCARD) { // prevents from sending a message directly to WILDCARD channel
|
|
return;
|
|
}
|
|
|
|
if(channels[channel]) {
|
|
channels[channel].forEach(sID => {
|
|
if (ignore === sID) return;
|
|
|
|
let ws = getClient(sID);
|
|
if (ws) {
|
|
ws.send(JSON.stringify({
|
|
type: "message",
|
|
channel: channel,
|
|
message: message,
|
|
meta: meta,
|
|
}))
|
|
}
|
|
});
|
|
}
|
|
|
|
if (channels[WILDCARD]) { // send message to WILDCARD channel
|
|
channels[WILDCARD].forEach(sID => {
|
|
let ws = getClient(sID);
|
|
if (ws) {
|
|
ws.send(JSON.stringify({
|
|
type: "message",
|
|
channel: WILDCARD,
|
|
message: message,
|
|
meta: meta,
|
|
}))
|
|
}
|
|
})
|
|
}
|
|
} catch (e) { // this code kept crashing, no longer happens
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
function onMessage(ws) {
|
|
return function(message) {
|
|
let data;
|
|
try {
|
|
data = JSON.parse(message);
|
|
} catch (e) {
|
|
return ws.send(JSON.stringify({
|
|
ok: false,
|
|
error: "Invalid data format",
|
|
uuid: ws.uuid,
|
|
}));
|
|
}
|
|
|
|
data.id = Number.parseInt(data.id) || 1; // if request ID is invalid or nonexistend, define it as 1
|
|
|
|
if (!data.type) { // requests require type field
|
|
return ws.send(JSON.stringify({
|
|
ok: false,
|
|
error: "Invalid request",
|
|
uuid: ws.uuid,
|
|
id: data.id,
|
|
}))
|
|
}
|
|
|
|
switch (data.type) {
|
|
case "send": // Send a message to a channel
|
|
if (!data.channel) {
|
|
return ws.send(JSON.stringify({
|
|
ok: false,
|
|
error: "Missing channel field",
|
|
uuid: ws.uuid,
|
|
id: data.id,
|
|
}))
|
|
}
|
|
|
|
// create the message meta
|
|
|
|
let meta = typeof data.meta === "object" && !Array.isArray(data.meta) ? data.meta : {};
|
|
|
|
meta.uuid = ws.uuid; // sender uuid
|
|
meta.time = Date.now(); // time of sending
|
|
meta.channel = data.channel; // channel
|
|
meta.guest = !ws.auth; // if not authenticated
|
|
|
|
|
|
transmit(data.channel, data.message, meta, ws.sID); // proceed to send the message
|
|
|
|
ws.send(JSON.stringify({
|
|
ok: true,
|
|
id: data.id,
|
|
uuid: ws.uuid,
|
|
}));
|
|
break;
|
|
case "open": // Open channel
|
|
if (!data.channel) {
|
|
return ws.send(JSON.stringify({
|
|
ok: false,
|
|
error: "Missing channel field",
|
|
uuid: ws.uuid,
|
|
id: data.id,
|
|
}))
|
|
}
|
|
|
|
// limit of channel name:
|
|
// Must be either a string long max 256 chars or a number
|
|
|
|
if ((typeof data.channel === "string" && data.channel.length <= 256) || typeof data.channel === "number") {
|
|
|
|
if (!channels[data.channel]) channels[data.channel] = [];
|
|
channels[data.channel].push(ws.sID);
|
|
|
|
ws.send(JSON.stringify({
|
|
ok: true,
|
|
id: data.id,
|
|
uuid: ws.uuid,
|
|
}));
|
|
} else {
|
|
return ws.send(JSON.stringify({
|
|
ok: false,
|
|
error: "Invalid channel field",
|
|
uuid: ws.uuid,
|
|
id: data.id,
|
|
}))
|
|
}
|
|
|
|
break;
|
|
case "close": // close a channel
|
|
if (!data.channel) {
|
|
return ws.send(JSON.stringify({
|
|
ok: false,
|
|
error: "Missing channel field",
|
|
uuid: ws.uuid,
|
|
id: data.id,
|
|
}))
|
|
}
|
|
|
|
if (channels[data.channel]) { // remove uuid from the channel
|
|
let index = channels[data.channel].indexOf(ws.sID);
|
|
if (index >= 0) {
|
|
channels[data.channel].splice(index, 1);
|
|
}
|
|
}
|
|
|
|
ws.send(JSON.stringify({
|
|
ok: true,
|
|
id: data.id,
|
|
uuid: ws.uuid,
|
|
}));
|
|
|
|
break;
|
|
case "ping": // allow clients to ping the server if they want to
|
|
ws.send(JSON.stringify({
|
|
ok: true,
|
|
id: data.id,
|
|
uuid: ws.uuid,
|
|
}));
|
|
break;
|
|
case "auth": // authentication
|
|
if (!data.token) {
|
|
return ws.send(JSON.stringify({
|
|
ok: false,
|
|
error: "Missing token field",
|
|
uuid: ws.uuid,
|
|
id: data.id,
|
|
}))
|
|
}
|
|
|
|
let authid = createID(data.token); // create the UUID from the token
|
|
|
|
let olduuid = ws.uuid;
|
|
ws.uuid = authid; // set new UUID
|
|
ws.auth = true; // set as authenticated
|
|
|
|
console.log(`AUTH: ${olduuid} is now ${ws.uuid}`);
|
|
|
|
return ws.send(JSON.stringify({
|
|
ok: true,
|
|
uuid: ws.uuid,
|
|
id: data.id,
|
|
}));
|
|
|
|
break;
|
|
default: // if no request type is found send this as error
|
|
ws.send(JSON.stringify({
|
|
ok: false,
|
|
error: "Invalid request",
|
|
uuid: ws.uuid,
|
|
id: data.id,
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
|
|
server.on("connection", ws => { // Listen to clients connecting to the websocket server
|
|
|
|
ws.uuid = random(); // assign a random UUID as guest
|
|
ws.sID = random(undefined, "S"); // Session ID
|
|
ws.auth = false; // not authenticated by default
|
|
ws.type = "websocket";
|
|
|
|
clients.push(ws);
|
|
|
|
console.log("Connect:", ws.uuid, ws.sID);
|
|
|
|
let pingInterval = setInterval(function () { // Send a ping to the client every 10 seconds to keep the connection alive
|
|
ws.send(JSON.stringify({
|
|
type: "ping",
|
|
uuid: ws.uuid,
|
|
ping: Date.now(),
|
|
}))
|
|
}, 10000);
|
|
|
|
ws.send(JSON.stringify({ // A friendly message upon connection
|
|
type: "motd",
|
|
motd: "Welcome to the Soqet network",
|
|
uuid: ws.uuid,
|
|
}));
|
|
|
|
ws.on("message", onMessage(ws));
|
|
|
|
ws.on("close", (code, reason) => { // WS Client disconnects
|
|
console.log("Close:", ws.uuid, ws.sID, `(${code} ${reason})`);
|
|
clearInterval(pingInterval); // Clear Ping interval
|
|
|
|
// remove uuid from all channels
|
|
disconnect(ws.sID);
|
|
});
|
|
|
|
ws.on("error", (err) => {
|
|
console.error(ws.uuid, ws.sID, err) // it can happen
|
|
});
|
|
});
|
|
|
|
netServer.on("connection", socket => {
|
|
socket.send = function(data) {
|
|
return socket.write(data)
|
|
}
|
|
|
|
socket.uuid = random();
|
|
socket.sID = random(undefined, "S");
|
|
socket.auth = false;
|
|
socket.type = "socket"
|
|
|
|
clients.push(socket)
|
|
|
|
console.log("TCP Connect:", socket.uuid, socket.sID);
|
|
|
|
let pingInterval = setInterval(function () { // Send a ping to the client every 10 seconds to keep the connection alive
|
|
socket.send(JSON.stringify({
|
|
type: "ping",
|
|
uuid: socket.uuid,
|
|
ping: Date.now(),
|
|
}))
|
|
}, 10000);
|
|
|
|
socket.send(JSON.stringify({ // A friendly message upon connection
|
|
type: "motd",
|
|
motd: "Welcome to the Soqet network",
|
|
uuid: socket.uuid,
|
|
}));
|
|
|
|
socket.on("data", onMessage(socket));
|
|
|
|
socket.on("close", (code, reason) => { // WS Client disconnects
|
|
console.log("Close:", socket.uuid, socket.sID, `(${code} ${reason})`);
|
|
clearInterval(pingInterval); // Clear Ping interval
|
|
|
|
// remove uuid from all channels
|
|
disconnect(socket.sID);
|
|
});
|
|
|
|
socket.on("error", (err) => {
|
|
console.error(socket.uuid, socket.sID, err) // it can happen
|
|
});
|
|
})
|
|
|
|
netServer.listen(config.tcp_port);
|
|
|
|
// hopefully this service will help cc communities
|