LinkCloudDev/server/v2.js

299 lines
10 KiB
JavaScript
Raw Normal View History

2024-08-02 22:55:55 +00:00
import Net from "net";
import "dotenv/config";
import { setup as dbSetup, runPrepQuery, numRows } from "./Utility/db.js";
import { Err, Log, Debug, Warn } from "./Utility/loggerUtility.js";
import crypto from "crypto";
import { botSetup, client } from "./Utility/discordClient.js";
import { event } from "./Utility/eventHandler.js";
import fsConfig from "./config.json" assert { type: "json" };
import { loadModelFromDB, createNewLS } from "./Utility/lsModel.js";
import { listen } from "./webserver.js";
import { getUserFromJwt, setUserSocket, loadUsersFromDB } from "./Utility/userModel.js";
import { getLSModel } from "./Utility/lsModel.js";
import { link, write } from "fs";
const token = fsConfig.token;
Log("Connecting to MySQL DB...");
dbSetup(fsConfig.db);
event.on("DATABASE_CONNECTED", async () => {
Log("Ready!");
await loadUsersFromDB();
await loadModelFromDB();
botSetup();
listen();
});
event.on("MYSQL_FAILED_TO_CONNECT", () => {
Err(
"Failed to launch LinkCloud. Unable to connect to MySQL database. Please check database connection parameters and try again."
);
process.exit();
});
const server = Net.createServer();
const discordPacketBuffer = [];
const lsPacketBuffer = []
const sockets = [];
const discordWebhookQueue = []
const checkTime = (theirTime) => {
const myTime = Math.floor(Date.now() / 1000);
return Number(theirTime - myTime);
};
const hashPacketData = (packet) => {
let base = packet.type;
base += packet.metaData.gameTime;
base += packet.metaData.server;
base += packet.payload.name.replace(/[\n\r]/g, '').trim();
base += packet.payload.message.replace(/[\n\r]/g, '').trim();
base += packet.metaData.platform;
if (packet.payload?.linkshellname) {
base += packet.payload?.linkshellname;
} else if (packet.payload?.area) {
base += packet.payload?.area;
}
return crypto.createHash("md5").update(base).digest("hex");
};
const addToDiscordBuffer = (packet) => {
for (const idx in discordPacketBuffer) {
if (discordPacketBuffer[idx].hash == packet.hash) {
//Debug(`${packet.hash} exists, skipping...`);
return false;
}
}
//Debug(`New hash created ${packet.hash}`)
discordPacketBuffer.push(packet);
if(discordPacketBuffer.length > 1000) {
discordPacketBuffer.shift()
}
};
server.on("connection", (sock) => {
//Debug("CONNECTED: " + sock.remoteAddress + ":" + sock.remotePort);
sock.on("data", async (data) => {
const response = {
error: false,
};
try {
const packets = data.toString().split("\n");
for (const p in packets) {
if (packets[p]) {
const packet = JSON.parse(packets[p]);
const differential = checkTime(packet?.metaData?.clientTime);
let isLsMessage = false;
if (differential <= process.env.MAX_CLOCK_SYNC_MISMATCH_SECONDS) {
response.type = packet?.type?.toUpperCase();
response.packetId = packet?.packetId;
switch (response.type) {
case "HANDSHAKE":
const authed = AuthenticateSocket(packet, sock);
if(!authed) {
response.error = true;
response.errorMsg = "AUTH_FAIL";
response.errorDetails = `You shall not pass!`;
}
response.payload = authed ? "ACCEPTED" : "REJECTED";
response.disconnect = true
Log(`[${sock.remoteAddress}] connection ${response.payload}.`);
break;
case "HEARTBEAT":
response.payload = "PONG";
break;
case "LINKSHELL_MESSAGE":
console.log(packet)
packet.hash = hashPacketData(packet);
ProcessLSMessage(packet, sock)
break;
case "LINKSHELL_UPDATE":
console.log(packet)
ProcessLSUpdate(packet, sock)
break;
case "SHOUT":
packet.hash = hashPacketData(packet);
addToDiscordBuffer(packet);
break;
case "OTHER":
break;
case "ADD_LINKSHELL":
ProcessAddLinkshell(packet, sock);
break;
default:
response.error = true;
response.errorMsg = "UNKNOWN_PACKET_TYPE";
response.errorDetails = `UNKNOWN PACKET TYPE`;
return;
}
} else {
response.error = true;
response.errorMsg = "CLOCK_OUT_OF_SYNC";
response.errorDetails = `This system and the servers clocks are out of sync by ${String(
differential
)} second(s).`;
}
if (sock) writeToClientSocket(sock,JSON.stringify(response) + "\n\r");
if (response.error) {
console.log(data)
console.log(packet)
sock.resetAndDestroy();
}
}
}
//Debug(`TO ${sock.remoteAddress} ${JSON.stringify(response)}`)
} catch (ex) {
Err("Unexpected packet format, unable to parse.");
console.log(ex);
console.log(data.toString());
}
});
sock.on("timeout", (data) => {
removeSocketFromPool(sock)
Debug("TIMEOUT: " + sock.remoteAddress + " " + sock.remotePort);
})
sock.on("close", (data) => {
removeSocketFromPool(sock)
Debug("CLOSED: " + sock.remoteAddress + " " + sock.remotePort);
});
writeToClientSocket(sock,"CHALLENGE\n");
sockets.push(sock);
});
const removeSocketFromPool = (sock) => {
const index = sockets.findIndex(function (o) {
return o.remoteAddress === sock.remoteAddress && o.remotePort === sock.remotePort;
});
if (index !== -1) sockets.splice(index, 1);
}
server.listen(process.env.TCP_LISTEN_PORT, () => {
Log(`Server Bound to ${process.env.BIND_ADDRESS} on port ${process.env.TCP_LISTEN_PORT}.`);
});
const writeToClientSocket = (socket, data, retry = 0) => {
if(!socket?.destroyed && socket?.readyState === 'open') {
socket.write(data, (err) => {
if(err) {
Debug(`Failed to write to socket`)
console.log(err)
}
})
} else {
if(socket?.destroyed) {
Debug(`The socket is dead and cant be written to`)
} else {
if(retry < 5) {
Debug(`Retrying to send packet ${data}`)
setTimeout(() => {writeToClientSocket(socket, data, retry + 1)}, 1000)
}
}
}
}
const ProcessLSMessage = (packet, socket) => {
for (const idx in lsPacketBuffer) {
if (lsPacketBuffer[idx].hash == packet.hash) {
//Debug(`${packet.hash} exists, skipping...`);
return false;
}
}
//Debug(`New hash created ${packet.hash}`)
lsPacketBuffer.push(packet);
if(lsPacketBuffer.length > 1000) {
lsPacketBuffer.shift()
}
const linkshell = ProcessLSUpdate(packet, socket)
if(!linkshell.channels) return false;
const re = /^(\[.+\] .+)/
if(!packet.payload.message.match(re)){
linkshell.webhookQueue.push({
linkshell,
packet
})
}
}
const ProcessLSUpdate = (packet, socket) => {
const linkshell = getLSModel(packet.payload?.linkshellname, packet.metaData.platform, packet.metaData.server);
if(linkshell) {
linkshell.socket = socket
Log(`Linkshell "${packet.payload.linkshellname}" Registered By ${packet.metaData.character}`)
}
return linkshell
}
const sendMessageToClient = (socket, message, error = false) => {
const packet = {};
packet.type = "SYSTEM_MESSAGE";
packet.payload = {};
packet.payload.isError = error;
packet.payload.message = message;
if (socket) writeToClientSocket(socket,JSON.stringify(packet) + "\n\r");
};
const sendLSMessage = (socket, message, from, ls) => {
const packet = {};
packet.type = "LS_ECHO";
packet.payload = {};
packet.payload.from = from.trim();
packet.payload.message = message.trim();
packet.payload.linkshell = ls
if (socket) writeToClientSocket(socket,JSON.stringify(packet) + "\n\r");
}
const AuthenticateSocket = (packet, socket) => {
const authId = packet.payload.authId;
const user = getUserFromJwt(authId);
if (user) {
setUserSocket(socket, user.userId)
return true;
} else {
return false;
}
};
const ProcessAddLinkshell = (packet, socket) => {
const linkId = packet.payload.linkId.replace("\r", "").replace("\n", "");
const serverId = packet.metaData.server;
const lsName = packet.payload.lsName;
const result = runPrepQuery("SELECT * FROM pendinglinks WHERE linkId = ? LIMIT 0,1", [linkId], async (r, f, e) => {
if (numRows(r)) {
const ffxiver = r[0].ffxiver;
const userId = r[0].userId;
const discordUser = client.users.cache.get(userId);
console.log({ linkId, serverId, lsName, ffxiver, userId });
const newLs = await createNewLS(lsName, serverId, ffxiver, userId).catch((e) => {
console.log(e);
sendMessageToClient(socket, "An error occured. Contact Support. [0x10]", true);
});
if (newLs) {
discordUser.send(
`# Success!\nYour Linkshell ${lsName} has been added to LinkCloud!\n\n## What's Next?\n- Set up the chat echo channel in your discord server using the \`/lccreateecho\` command in the channel you want to use for the chat echo.\n- Encourage LS members to use the \`/lcjoin\` command to get started streaming data to LinkCloud. The more streamers you have, the more reliable the echo will be.`
);
Log(`New Linkshell "${lsName}" has been created!`);
sendMessageToClient(
socket,
`Linkshell ${lsName} added successfully to LinkCloud. Check discord for further instruction.`
);
} else {
Err(`Failed to create Linkshell "${lsName}".`);
sendMessageToClient(
socket,
"An error occured. This is most likely because this Linkshell already exists.",
true
);
console.log(e);
discordUser.send(
`# Uh-oh!\nYour Linkshell ${lsName} seems to already exist in our database.\n\n## Need Help?\nYou can reach out to support via discord.\nhttps://discord.gg/n5VYHSQbhA`
);
}
} else {
sendMessageToClient(socket, "An error occured. The supplied token is not valid.", true);
}
});
};
event.on('NEW_DISCORD_ECHO_RECEIVED', message => {
const linkshell = getLSModel(message.lsName, message.platform, message.server)
if(linkshell.socket) {
sendLSMessage(linkshell.socket, message.message, message.from, message.lsName)
}
})