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) } })