From 8b39bd720e8eaedcdec8e7398237dd2e32b53035 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Mon, 6 Nov 2023 22:50:07 +0100 Subject: [PATCH 01/53] feat: updated core/rcon.js with passThrough support and EOSID to SeamID rewriting --- config.json | 2 + core/rcon.js | 779 +++++++++++++++++++++++++----------------- squad-server/index.js | 120 ++++--- 3 files changed, 535 insertions(+), 366 deletions(-) diff --git a/config.json b/config.json index e68f5e58..5ac773e4 100644 --- a/config.json +++ b/config.json @@ -5,6 +5,8 @@ "queryPort": 27165, "rconPort": 21114, "rconPassword": "password", + "rconPassThrough": true, + "rconPassThroughPort": 8124, "logReaderMode": "tail", "logDir": "C:/path/to/squad/log/folder", "ftp": { diff --git a/core/rcon.js b/core/rcon.js index 61b20309..f299c010 100644 --- a/core/rcon.js +++ b/core/rcon.js @@ -1,367 +1,502 @@ -import EventEmitter from 'events'; -import net from 'net'; -import util from 'util'; - -import Logger from './logger.js'; - -const SERVERDATA_EXECCOMMAND = 0x02; -const SERVERDATA_RESPONSE_VALUE = 0x00; -const SERVERDATA_AUTH = 0x03; -const SERVERDATA_AUTH_RESPONSE = 0x02; -const SERVERDATA_CHAT_VALUE = 0x01; - -const MID_PACKET_ID = 0x01; -const END_PACKET_ID = 0x02; - +import { EventEmitter } from "node:events"; +import net from "node:net"; +import Logger from "./logger.js"; export default class Rcon extends EventEmitter { constructor(options = {}) { super(); - - // store config - for (const option of ['host', 'port', 'password']) - if (!(option in options)) throw new Error(`${option} must be specified.`); - + for (const option of ["host", "port", "password"]) if (!(option in options)) throw new Error(`${option} must be specified.`); this.host = options.host; this.port = options.port; this.password = options.password; - this.autoReconnectDelay = options.autoReconnectDelay || 5000; - - // bind methods - this.connect = this.connect.bind(this); // we bind this as we call it on the auto reconnect timeout - this.onData = this.onData.bind(this); - this.onClose = this.onClose.bind(this); - this.onError = this.onError.bind(this); - - // setup socket - this.client = new net.Socket(); - this.client.on('data', this.onData); - this.client.on('close', this.onClose); - this.client.on('error', this.onError); - - // constants - this.maximumPacketSize = 4096; - - // internal variables + this.client = null; + this.stream = new Buffer.alloc(0); + this.type = { auth: 0x03, command: 0x02, response: 0x00, server: 0x01 }; + this.soh = { size: 7, id: 0, type: this.type.response, body: "" }; + this.responseString = { id: 0, body: "" }; this.connected = false; this.autoReconnect = false; - this.autoReconnectTimeout = null; + this.autoReconnectDelay = options.autoReconnectDelay || 1000; + this.connectionRetry; + this.msgIdLow = 6; + this.msgIdHigh = 16; + this.specialId = 19; + this.msgId = this.msgIdLow; + this.passThrough = options.passThrough ? true : false; + this.passThroughPort = options.passThroughPort || 8124; + this.passThroughTimeOut = options.passThroughTimeOut || 60000; + this.passThroughMaxClients = 1; //options.passThroughMaxClients || 10; + this.passThroughChallenge = options.passThroughChallenge || options.password; + this.rconClients = {}; + for (let i = 1; i <= this.passThroughMaxClients; i++) this.rconClients[`${i}`] = null; + this.ptServer = null; + + this.steamIndex = { "76561198799344716": "00026e21ce3d43c792613bdbb6dec1ba" }; // example dtata + this.eosIndex = { "00026e21ce3d43c792613bdbb6dec1ba": "76561198799344716" }; // example dtata - this.incomingData = Buffer.from([]); - this.incomingResponse = []; - this.responseCallbackQueue = []; } - - onData(data) { - Logger.verbose('RCON', 4, `Got data: ${this.bufToHexString(data)}`); - - // the logic in this method simply splits data sent via the data event into packets regardless of how they're - // distributed in the event calls - const packets = this.decodeData(data); - - for (const packet of packets) { - Logger.verbose('RCON', 4, `Processing packet: ${this.bufToHexString(packet)}`); - - const decodedPacket = this.decodePacket(packet); - Logger.verbose( - 'RCON', - 3, - `Processing decoded packet: ${this.decodedPacketToString(decodedPacket)}` - ); - - switch (decodedPacket.type) { - case SERVERDATA_RESPONSE_VALUE: - case SERVERDATA_AUTH_RESPONSE: - switch (decodedPacket.id) { - case MID_PACKET_ID: - this.incomingResponse.push(decodedPacket); - break; - case END_PACKET_ID: - this.responseCallbackQueue.shift()( - this.incomingResponse.map((packet) => packet.body).join() - ); - this.incomingResponse = []; - break; - default: - Logger.verbose( - 'RCON', - 1, - `Unknown packet ID ${decodedPacket.id} in: ${this.decodedPacketToString( - decodedPacket - )}` - ); - } - break; - - case SERVERDATA_CHAT_VALUE: - this.processChatPacket(decodedPacket); - break; - - default: - Logger.verbose( - 'RCON', - 1, - `Unknown packet type ${decodedPacket.type} in: ${this.decodedPacketToString( - decodedPacket - )}` - ); + processChatPacket(decodedPacket) { + console.log(decodedPacket.body); + } // + async connect() { + return new Promise((resolve, reject) => { + if (this.client && this.connected && !this.client.destroyed) return reject(new Error("Rcon.connect() Rcon already connected.")); + this.removeAllListeners("server"); + this.removeAllListeners("auth"); + this.on("server", (pkt) => this.processChatPacket(pkt)); + this.once("auth", () => { + Logger.verbose("RCON", 1, `Connected to: ${this.host}:${this.port}`); + clearTimeout(this.connectionRetry); + this.connected = true; + if (this.passThrough) this.createServer(); + resolve(); + }); + Logger.verbose("RCON", 1, `Connecting to: ${this.host}:${this.port}`); + this.connectionRetry = setTimeout(() => this.connect(), this.autoReconnectDelay); + this.autoReconnect = true; + this.client = net + .createConnection({ port: this.port, host: this.host }, () => this.#sendAuth()) + .on("data", (data) => this.#onData(data)) + .on("end", () => this.#onClose()) + .on("error", () => this.#onNetError()); + }).catch((error) => { + Logger.verbose("RCON", 1, `Rcon.connect() ${error}`); + }); + } + async disconnect() { + return new Promise((resolve, reject) => { + Logger.verbose("RCON", 1, `Disconnecting from: ${this.host}:${this.port}`); + clearTimeout(this.connectionRetry); + this.removeAllListeners("server"); + this.removeAllListeners("auth"); + this.autoReconnect = false; + this.client.end(); + this.connected = false; + this.closeServer(); + resolve(); + }).catch((error) => { + Logger.verbose("RCON", 1, `Rcon.disconnect() ${error}`); + }); + } + async execute(body) { + return new Promise((resolve, reject) => { + if (!this.connected) return reject(new Error("Rcon not connected.")); + if (!this.client.writable) return reject(new Error("Unable to write to node:net socket")); + const string = String(body); + const length = Buffer.from(string).length; + if (length > 4154) Logger.verbose("RCON", 1, `Error occurred. Oversize, "${length}" > 4154`); + else { + const outputData = (data) => { + clearTimeout(timeOut); + resolve(data); + }; + const timedOut = () => { + console.warn("MISSED", listenerId) + this.removeListener(listenerId, outputData); + return reject(new Error(`Rcon response timed out`)); + }; + if (this.msgId > this.msgIdHigh -2) this.msgId = this.msgIdLow; + const listenerId = `response${this.msgId}`; + const timeOut = setTimeout(timedOut, 10000); + this.once(listenerId, outputData); + this.#send(string, this.msgId); + this.msgId++; } + }).catch((error) => { + Logger.verbose("RCON", 1, `Rcon.execute() ${error}`); + }); + } + #sendAuth() { + Logger.verbose("RCON", 1, `Sending Token to: ${this.host}:${this.port}`); + this.client.write(this.#encode(this.type.auth, 0, this.password).toString("binary"), "binary");//2147483647 + } + #send(body, id = 99) { + this.#write(this.type.command, id, body); + this.#write(this.type.command, id + 2); + } + #write(type, id, body) { + Logger.verbose("RCON", 2, `Writing packet with type "${type}", id "${id}" and body "${body || ""}"`); + this.client.write(this.#encode(type, id, body).toString("binary"), "binary"); + } + #encode(type, id, body = "") { + const size = Buffer.byteLength(body) + 14; + const buffer = new Buffer.alloc(size); + buffer.writeInt32LE(size - 4, 0); + buffer.writeInt32LE(id, 4); + buffer.writeInt32LE(type, 8); + buffer.write(body, 12, size - 2, "utf8"); + buffer.writeInt16LE(0, size - 2); + return buffer; + } + #onData(data) { + console.log(data) + Logger.verbose("RCON", 4, `Got data: ${this.#bufToHexString(data)}`); + this.stream = Buffer.concat([this.stream, data], this.stream.byteLength + data.byteLength); + while (this.stream.byteLength >= 7) { + const packet = this.#decode(); + if (!packet) break; + else Logger.verbose("RCON", 3, `Processing decoded packet: Size: ${packet.size}, ID: ${packet.id}, Type: ${packet.type}, Body: ${packet.body}`); + + if (packet.id > this.msgIdHigh) this.emit(`responseForward_1`, packet); + else if (packet.type === this.type.response) this.#onResponse(packet); + else if (packet.type === this.type.server) this.#onServer(packet); + else if (packet.type === this.type.command) this.emit("auth"); } } - - decodeData(data) { - this.incomingData = Buffer.concat([this.incomingData, data]); - - const packets = []; - - // we check that it's greater than 4 as if it's not then the length header is not fully present which breaks the - // rest of the code. We just need to wait for more data. - while (this.incomingData.byteLength >= 4) { - const size = this.incomingData.readInt32LE(0); - const packetSize = size + 4; - - // The packet following an empty packet will report to be 10 long (14 including the size header bytes), but in - // it should report 17 long (21 including the size header bytes). Therefore, if the packet is 10 in size - // and there's enough data for it to be a longer packet then we need to probe to check it's this broken packet. - const probeSize = 17; - const probePacketSize = 21; - - if (size === 10 && this.incomingData.byteLength >= probeSize) { - // copy the section of the incoming data of interest - const probeBuf = this.incomingData.slice(0, probePacketSize); - // decode it - const decodedProbePacket = this.decodePacket(probeBuf); - // check whether body matches - if (decodedProbePacket.body === '\x00\x00\x00\x01\x00\x00\x00') { - // it does so it's the broken packet - // remove the broken packet from the incoming data - this.incomingData = this.incomingData.slice(probePacketSize); - Logger.verbose('RCON', 4, `Ignoring some data: ${this.bufToHexString(probeBuf)}`); - continue; - } + #onServer(packet) { + this.emit("server", packet); + for (const client in this.rconClients) + if (this.rconClients[client]) { + this.emit(`serverForward_${this.rconClients[client].rconIdClient}`, packet.body); } - - if (this.incomingData.byteLength < packetSize) { - Logger.verbose('RCON', 4, `Waiting for more data...`); - break; - } - - const packet = this.incomingData.slice(0, packetSize); - packets.push(packet); - - this.incomingData = this.incomingData.slice(packetSize); + } + #decode() { + if (this.stream[0] === 0 && this.stream[1] === 1 && this.stream[2] === 0 && this.stream[3] === 0 && this.stream[4] === 0 && this.stream[5] === 0 && this.stream[6] === 0) { + this.stream = this.stream.subarray(7); + return this.soh; } - - return packets; + const bufSize = this.stream.readInt32LE(0); + if (bufSize > 4154 || bufSize < 10) return this.#badPacket(); + else if (bufSize <= this.stream.byteLength - 4 && this.stream.byteLength >= 12) { + const bufId = this.stream.readInt32LE(4); + const bufType = this.stream.readInt32LE(8); + if (this.stream[bufSize + 2] !== 0 || this.stream[bufSize + 3] !== 0 || bufId < 0 || bufType < 0 || bufType > 5) return this.#badPacket(); + else { + const response = { size: bufSize, id: bufId, type: bufType, body: this.stream.toString("utf8", 12, bufSize + 2) }; + this.stream = this.stream.subarray(bufSize + 4); + if (response.body === "" && this.stream[0] === 0 && this.stream[1] === 1 && this.stream[2] === 0 && this.stream[3] === 0 && this.stream[4] === 0 && this.stream[5] === 0 && this.stream[6] === 0) { + this.stream = this.stream.subarray(7); + response.body = ""; + } + return response; + } + } else return null; } - - decodePacket(packet) { - return { - size: packet.readInt32LE(0), - id: packet.readInt32LE(4), - type: packet.readInt32LE(8), - body: packet.toString('utf8', 12, packet.byteLength - 2) - }; + #onResponse(packet) { + if (packet.body === "") { + this.emit(`response${this.responseString.id - 2}`, this.responseString.body); + this.responseString.body = ""; + } else if (!packet.body.includes("")) { + this.responseString.body = this.responseString.body += packet.body; + this.responseString.id = packet.id; + } else this.#badPacket(); } - - processChatPacket(decodedPacket) {} - - onClose(hadError) { + #badPacket() { + Logger.verbose("RCON", 1, `Bad packet, clearing: ${this.bufToHexString(this.stream)} Pending string: ${this.responseString}`); + this.stream = Buffer.alloc(0); + this.responseString = ""; + return null; + } + #onClose() { + Logger.verbose("RCON", 1, `Socket closed`); + this.#cleanUp(); + } + #onNetError(error) { + Logger.verbose("RCON", 1, `node:net error:`, error); + this.emit("RCON_ERROR", error); + this.#cleanUp(); + } + #cleanUp() { + this.closeServer(); this.connected = false; - - Logger.verbose('RCON', 1, `Socket closed ${hadError ? 'without' : 'with'} an error.`); - + this.removeAllListeners(); + clearTimeout(this.connectionRetry); if (this.autoReconnect) { - Logger.verbose('RCON', 1, `Sleeping ${this.autoReconnectDelay}ms before reconnecting.`); - setTimeout(this.connect, this.autoReconnectDelay); + Logger.verbose("RCON", 1, `Sleeping ${this.autoReconnectDelay}ms before reconnecting`); + this.connectionRetry = setTimeout(() => this.connect(), this.autoReconnectDelay); } } - - onError(err) { - Logger.verbose('RCON', 1, `Socket had error:`, err); - this.emit('RCON_ERROR', err); + createServer() { + this.ptServer = net.createServer((client) => this.#onNewClient(client)); + this.ptServer.maxConnections = this.passThroughMaxClients; + this.ptServer.on("error", (error) => this.#onSerErr(error)); + this.ptServer.on("drop", () => Logger.verbose("RCON", 1, `Pass-through Server: Max Clients Reached (${this.passThroughMaxClients}) rejecting new connection`)); + this.ptServer.listen(this.passThroughPort, () => Logger.verbose("RCON", 1, `Pass-through Server: Listening on port ${this.passThroughPort}`)); } - - connect() { - return new Promise((resolve, reject) => { - Logger.verbose('RCON', 1, `Connecting to: ${this.host}:${this.port}`); - - const onConnect = async () => { - this.client.removeListener('error', onError); - this.connected = true; - - Logger.verbose('RCON', 1, `Connected to: ${this.host}:${this.port}`); - - try { - // connected successfully, now try auth... - await this.write(SERVERDATA_AUTH, this.password); - - // connected and authed successfully - this.autoReconnect = true; - resolve(); - } catch (err) { - reject(err); - } - }; - - const onError = (err) => { - this.client.removeListener('connect', onConnect); - - Logger.verbose('RCON', 1, `Failed to connect to: ${this.host}:${this.port}`, err); - - reject(err); - }; - - this.client.once('connect', onConnect); - this.client.once('error', onError); - - this.client.connect(this.port, this.host); - }); + closeServer() { + for (const client in this.rconClients) if (this.rconClients[client]) this.rconClients[client].end(); + if (!this.ptServer) return; + this.ptServer.close(() => this.#onServerClose()); } - - disconnect() { - return new Promise((resolve, reject) => { - Logger.verbose('RCON', 1, `Disconnecting from: ${this.host}:${this.port}`); - - const onClose = () => { - this.client.removeListener('error', onError); - - Logger.verbose('RCON', 1, `Disconnected from: ${this.host}:${this.port}`); - - resolve(); - }; - - const onError = (err) => { - this.client.removeListener('close', onClose); - - Logger.verbose('RCON', 1, `Failed to disconnect from: ${this.host}:${this.port}`, err); - - reject(err); - }; - - this.client.once('close', onClose); - this.client.once('error', onError); - - // prevent any auto reconnection happening - this.autoReconnect = false; - // clear the timeout just in case the socket closed and then we DCed - clearTimeout(this.autoReconnectTimeout); - - this.client.end(); - }); + #onServerClose() { + if (!this.ptServer) return; + this.ptServer.removeAllListeners(); + this.ptServer = null; + Logger.verbose("RCON", 1, `Pass-through Server: Closed`); } - - execute(command) { - return this.write(SERVERDATA_EXECCOMMAND, command); + #onNewClient(client) { + client.setTimeout(this.passThroughTimeOut); + client.on("end", () => this.#onClientEnd(client)); + client.on("error", () => this.#onClientEnd(client)); + client.on("timeout", () => this.#onClientTimeOut(client)); + client.on("data", (data) => this.#onClientData(client, data)); + Logger.verbose("RCON", 1, `Pass-through Server: Client connecting`); } - - write(type, body) { - return new Promise((resolve, reject) => { - if (!this.connected) { - reject(new Error('Not connected.')); - return; - } - - if (!this.client.writable) { - reject(new Error('Unable to write to socket.')); - return; - } - - const encodedPacket = this.encodePacket( - type, - type !== SERVERDATA_AUTH ? MID_PACKET_ID : END_PACKET_ID, - body - ); - - const encodedEmptyPacket = this.encodePacket(type, END_PACKET_ID, ''); - - if (this.maximumPacketSize < encodedPacket.length) { - reject(new Error('Packet too long.')); - return; + #onSerErr(error) { + this.closeServer(); + Logger.verbose("RCON", 1, `Pass-through Server: ${error}`); + } + #onClientEnd(client) { + if (!client.rconIdClient) return; + this.removeAllListeners(`serverForward_${client.rconIdClient}`); + this.removeAllListeners(`responseForward_${client.rconIdClient}`); + this.rconClients[`${client.rconIdClient}`] = null; + Logger.verbose("RCON", 1, `Pass-through Server: Client-${client.rconIdClient} Disconnected`); + } + #onClientTimeOut(client) { + client.end(); + Logger.verbose("RCON", 1, `Pass-through Server: Client-${client.rconIdClient} Timed Out`); + } + #onClientData(client, data) { + if (!client.rconStream) client.rconStream = new Buffer.alloc(0); + client.rconStream = Buffer.concat([client.rconStream, data], client.rconStream.byteLength + data.byteLength); + while (client.rconStream.byteLength >= 4) { + const packet = this.#decodeClient(client); + if (!packet) break; + if (!client.rconHasAuthed) this.#authClient(client, packet); + else { + + if (!client.rconWheel || client.rconWheel > 20) client.rconWheel = 0; + else client.rconWheel++; + + client.rconIdQueueNEW[`${client.rconWheel}`] = packet.id + + const encoded = this.#encode(packet.type, this.specialId + client.rconWheel, this.#steamToEosClient(packet.body)); //////////////////////////////////////////////// + this.client.write(encoded.toString("binary"), "binary"); + // this.client.write(this.#encode(packet.type, this.specialId * client.rconIdClient).toString("binary"), "binary") } - - const onError = (err) => { - Logger.verbose('RCON', 1, 'Error occurred. Wiping response action queue.', err); - this.responseCallbackQueue = []; - reject(err); + } + } + #decodeClient(client) { + const bufSize = client.rconStream.readInt32LE(0); + if (bufSize <= client.rconStream.byteLength - 4) { + const response = { + size: bufSize, + id: client.rconStream.readInt32LE(4), + type: client.rconStream.readInt32LE(8), + body: client.rconStream.toString("utf8", 12, bufSize + 2), }; - - // the auth packet also sends a normal response, so we add an extra empty action to ignore it - if (type === SERVERDATA_AUTH) { - Logger.verbose('RCON', 2, `Writing Auth Packet`); - Logger.verbose('RCON', 4, `Writing packet with type "${type}" and body "${body}".`); - this.responseCallbackQueue.push(() => {}); - this.responseCallbackQueue.push((decodedPacket) => { - this.client.removeListener('error', onError); - if (decodedPacket.id === -1) { - Logger.verbose('RCON', 1, 'Authentication failed.'); - reject(new Error('Authentication failed.')); - } else { - Logger.verbose('RCON', 1, 'Authentication succeeded.'); - resolve(); - } - }); - } else { - Logger.verbose('RCON', 2, `Writing packet with type "${type}" and body "${body}".`); - this.responseCallbackQueue.push((response) => { - this.client.removeListener('error', onError); - - Logger.verbose( - 'RCON', - 2, - `Returning complete response: ${response.replace(/\r\n|\r|\n/g, '\\n')}` - ); - - resolve(response); - }); - } - - this.client.once('error', onError); - - Logger.verbose('RCON', 4, `Sending packet: ${this.bufToHexString(encodedPacket)}`); - this.client.write(encodedPacket); - - if (type !== SERVERDATA_AUTH) { - Logger.verbose( - 'RCON', - 4, - `Sending empty packet: ${this.bufToHexString(encodedEmptyPacket)}` - ); - this.client.write(encodedEmptyPacket); + client.rconStream = client.rconStream.subarray(bufSize + 4); + return response; + } else return null; + } + #authClient(client, packet) { + if (packet.body !== this.passThroughChallenge) { + client.end(); + Logger.verbose("RCON", 1, `Pass-through Server: Client [Rejected] Password not matched`); + } else { + client.rconHasAuthed = true; + client.rconIdQueueNEW = {} + for (let i = 1; i <= this.passThroughMaxClients; i++) { + if (this.rconClients[`${i}`] === null) { + client.rconIdClient = i; + this.rconClients[`${i}`] = client; + break; + } } - }); + this.on(`serverForward_${client.rconIdClient}`, (body) => client.write(this.#encode(1, 0, this.#eosToSteam(body)).toString("binary"), "binary")); + this.on(`responseForward_${client.rconIdClient}`, (packet) => this.#onForward(client, packet)); + client.write(this.#encode(0, packet.id)); + client.write(this.#encode(2, packet.id)); + Logger.verbose("RCON", 1, `Pass-through Server: Client-${client.rconIdClient} Connected`); + } } + #onForward(client, packet) { + if (packet.body !== "" && packet.body !== "") { - encodePacket(type, id, body, encoding = 'utf8') { - const size = Buffer.byteLength(body) + 14; - const buf = Buffer.alloc(size); + const int = packet.id - this.specialId - buf.writeInt32LE(size - 4, 0); - buf.writeInt32LE(id, 4); - buf.writeInt32LE(type, 8); - buf.write(body, 12, size - 2, encoding); - buf.writeInt16LE(0, size - 2); + //console.log(client.rconIdQueueNEW);////////////////////////////////////////////////////////////////////////////////////////// - return buf; + client.write(this.#encode(packet.type, client.rconIdQueueNEW[int], this.#eosToSteam(packet.body)).toString("binary"), "binary"); + } else if (packet.body != "") { + const int = packet.id - this.specialId + client.write(this.#encode(0, client.rconIdQueueNEW[int]).toString("binary"), "binary"); + client.write(this.#encodeSpecial(client.rconIdQueueNEW[int]).toString("binary"), "binary"); + } + } + #encodeSpecial(id) { + const buffer = new Buffer.alloc(21); + buffer.writeInt32LE(10, 0); + buffer.writeInt32LE(id, 4); + buffer.writeInt32LE(0, 8); + buffer.writeInt32LE(1, 15); + return buffer; + } + #bufToHexString(buf) { + return buf.toString("hex").match(/../g).join(" "); + } + async warn(steamID, message) { + this.execute(`AdminWarn "${steamID}" ${message}`); + } + async kick(steamID, reason) { + this.execute(`AdminKick "${steamID}" ${reason}`); + } + async forceTeamChange(steamID) { + this.execute(`AdminForceTeamChange "${steamID}"`); } - bufToHexString(buf) { - return buf.toString('hex').match(/../g).join(' '); + addIds(steamId, eosId) { + this.steamIndex[steamId] = eosId; // { "76561198799344716": "00026e21ce3d43c792613bdbb6dec1ba" }; + this.eosIndex[eosId] = steamId; } - decodedPacketToString(decodedPacket) { - return util.inspect(decodedPacket, { breakLength: Infinity }); + removeIds(eosId) { + // clean up ids on leave } - async warn(steamID, message) { - await this.execute(`AdminWarn "${steamID}" ${message}`); + #steamToEosClient(body) { + //assume client does not send more than 1 steamId per msg + const m = body.match(/[0-9]{17}/); + if (m && m[1] in this.steamIndex) return body.replaceAll(`${m[0]}`, this.steamIndex[m[0]]); + return body; } - async kick(steamID, reason) { - await this.execute(`AdminKick "${steamID}" ${reason}`); + #eosToSteam(body) { + //split body to lines for matching (1 steamId per line) + const lines = body.split("\n"); + const nBody = []; + for (let line of lines) nBody.push(this.#matchRcon(line)); + return nBody.join("\n"); } - async forceTeamChange(steamID) { - await this.execute(`AdminForceTeamChange "${steamID}"`); + #matchRcon(line) { + console.warn(line); + for (const r of this.defs) { + const match = line.match(r.regex); + if (match && match.groups.eosId in this.eosIndex) return r.rep(line, this.eosIndex[match.groups.eosId], match.groups.eosId); + } + return line; } + + defs = [ + //strict matching to avoid 'name as steamId errors' + { + regex: /^ID: [0-9]+ \| SteamID: (?[0-9a-f]{32}) \| Name: .+ \| Team ID: (1|2|N\/A) \| Squad ID: ([0-9]+|N\/A) \| Is Leader: (True|False|N\/A) \| Role: .+$/, + rep: (line, steamId, eosId) => { + return line.replace(` SteamID: ${eosId} `, ` SteamID: ${steamId} `); + }, + }, + { + regex: /^ID: (?[0-9]+) \| SteamID: (?[0-9a-f]{32}) \| Since Disconnect: (?.+) \| Name: (?.+)$/, + rep: (line, steamId, eosId) => { + return line.replace(` SteamID: ${eosId} `, ` SteamID: ${steamId} `); + }, + }, + { + regex: /^ID: (?[0-9]+) \| Name: (?.+) \| Size: (?[0-9]) \| Locked: (?True|False) \| Creator Name: (?.+) \| Creator Steam ID: (?[0-9]{17})/, + rep: (line, steamId, eosId) => { + return line.replace(` Creator Steam ID: ${eosId}`, ` Creator Steam ID: ${steamId}`); + }, + }, + { + regex: /^Forced team change for player (?[0-9]+). \[steamid=(?[0-9a-f]{32})] (.+)/, + rep: (line, steamId, eosId) => { + return line.replace(` [steamid=${eosId}] `, ` [steamid=${steamId}] `); + }, + }, + + { + regex: /^Could not find player (?[0-9a-f]{32})/, + rep: (line, steamId, eosId) => { + return line.replace(`Could not find player ${eosId}`, `Could not find player ${steamId}`); + }, + }, + + { + regex: /^\[ChatAll] \[SteamID:(?[0-9a-f]{32})] ((?.+) : (?.+))/, + rep: (line, steamId, eosId) => { + return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); + }, + }, + { + regex: /^\[ChatTeam] \[SteamID:(?[0-9a-f]{32})] ((?.+) : (?.+))/, + rep: (line, steamId, eosId) => { + return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); + }, + }, + { + regex: /^\[ChatSquad] \[SteamID:(?[0-9a-f]{32})] ((?.+) : (?.+))/, + rep: (line, steamId, eosId) => { + return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); + }, + }, + { + regex: /^\[ChatAdmin] \[SteamID:(?[0-9a-f]{32})] ((?.+) : (?.+))/, + rep: (line, steamId, eosId) => { + return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); + }, + }, + { + regex: /^(?.+) \(Steam ID: (?[0-9a-f]{32})\) has created Squad (?[0-9]+) \(Squad Name: (?.+)\) on (?.+)/, + rep: (line, steamId, eosId) => { + return line.replace(` (Steam ID: ${eosId}) `, ` (Steam ID: ${steamId}) `); + }, + }, + { + regex: /^Kicked player (?[0-9]+). \[steamid=(?[0-9a-f]{32})] (?.+)/, + rep: (line, steamId, eosId) => { + return line.replace(` [steamid=${eosId}] `, ` [steamid=${steamId}] `); + }, + }, + + { + regex: /^ERROR: Unable to find player with name or id \((?[0-9a-f]{32})\)$/, + rep: (line, steamId, eosId) => { + return line.replace(`name or id (${eosId})`, `name or id (${steamId})`); + }, + }, + { + regex: /^\[SteamID:(?[0-9a-f]{32})] (?.+) has possessed admin camera./, + rep: (line, steamId, eosId) => { + return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); + }, + }, + { + regex: /^\[SteamID:(?[0-9a-f]{32})] (?.+) has unpossessed admin camera./, + rep: (line, steamId, eosId) => { + return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); + }, + }, + ]; } + +//////////////////////////////////////////////////ALL BELOW IS FOR STANDALONE TESTING/RUNNING +// const Logger = { +// level: 1, +// verbose(type, lvl, msg, msg1 = "") { +// if (lvl > this.level) return; +// console.log(type, lvl, msg, msg1); +// }, +// }; + +// const squadJsStyle = async () => { +// const getCurrentMap = async () => { +// const response = await rcon.execute("ShowCurrentMap"); +// const match = response.match(/^Current level is (?.+), layer is (?.+)/); +// if (!match) { +// debugger +// } +// return [match.groups.level, match.groups.layer]; +// }; + +// const rcon = new Rcon({ port: "port", host: "ip", password: "password", passThrough: true, passThroughPort: "8124", passThroughTimeOut: 30000, passThroughChallenge: "password" }); // + +// try { +// await rcon.connect(); +// } catch (e) { +// console.warn(e); +// } + +// rcon.interval = setInterval(async () => { + +// try { +// const currentMap = await getCurrentMap(); +// console.log(currentMap); +// } catch (e) { +// console.warn(e); +// } +// }, 5000); +// }; + +// squadJsStyle(); diff --git a/squad-server/index.js b/squad-server/index.js index 490f0a7d..e3506518 100644 --- a/squad-server/index.js +++ b/squad-server/index.js @@ -19,7 +19,7 @@ export default class SquadServer extends EventEmitter { constructor(options = {}) { super(); - for (const option of ['host', 'queryPort']) + for (const option of [ 'host', 'queryPort' ]) if (!(option in options)) throw new Error(`${option} must be specified.`); this.id = options.id; @@ -95,38 +95,40 @@ export default class SquadServer extends EventEmitter { host: this.options.rconHost || this.options.host, port: this.options.rconPort, password: this.options.rconPassword, - autoReconnectInterval: this.options.rconAutoReconnectInterval + autoReconnectInterval: this.options.rconAutoReconnectInterval, + passThroughPort: this.options.rconPassThroughPort, + passThrough: this.options.rconPassThrough }); this.rcon.on('CHAT_MESSAGE', async (data) => { - data.player = await this.getPlayerBySteamID(data.steamID); + data.player = await this.getPlayerByEOSID(data.steamID); this.emit('CHAT_MESSAGE', data); const command = data.message.match(/!([^ ]+) ?(.*)/); if (command) - this.emit(`CHAT_COMMAND:${command[1].toLowerCase()}`, { + this.emit(`CHAT_COMMAND:${command[ 1 ].toLowerCase()}`, { ...data, - message: command[2].trim() + message: command[ 2 ].trim() }); }); this.rcon.on('POSSESSED_ADMIN_CAMERA', async (data) => { - data.player = await this.getPlayerBySteamID(data.steamID); + data.player = await this.getPlayerByEOSID(data.steamID); - this.adminsInAdminCam[data.steamID] = data.time; + this.adminsInAdminCam[ data.steamID ] = data.time; this.emit('POSSESSED_ADMIN_CAMERA', data); }); this.rcon.on('UNPOSSESSED_ADMIN_CAMERA', async (data) => { - data.player = await this.getPlayerBySteamID(data.steamID); - if (this.adminsInAdminCam[data.steamID]) { - data.duration = data.time.getTime() - this.adminsInAdminCam[data.steamID].getTime(); + data.player = await this.getPlayerByEOSID(data.steamID); + if (this.adminsInAdminCam[ data.steamID ]) { + data.duration = data.time.getTime() - this.adminsInAdminCam[ data.steamID ].getTime(); } else { data.duration = 0; } - delete this.adminsInAdminCam[data.steamID]; + delete this.adminsInAdminCam[ data.steamID ]; this.emit('UNPOSSESSED_ADMIN_CAMERA', data); }); @@ -142,19 +144,19 @@ export default class SquadServer extends EventEmitter { }); this.rcon.on('PLAYER_KICKED', async (data) => { - data.player = await this.getPlayerBySteamID(data.steamID); + data.player = await this.getPlayerByEOSID(data.steamID); this.emit('PLAYER_KICKED', data); }); this.rcon.on('PLAYER_BANNED', async (data) => { - data.player = await this.getPlayerBySteamID(data.steamID); + data.player = await this.getPlayerByEOSID(data.steamID); this.emit('PLAYER_BANNED', data); }); this.rcon.on('SQUAD_CREATED', async (data) => { - data.player = await this.getPlayerBySteamID(data.playerSteamID, true); + data.player = await this.getPlayerByEOSID(data.playerSteamID, true); delete data.playerName; delete data.playerSteamID; @@ -207,7 +209,11 @@ export default class SquadServer extends EventEmitter { }); this.logParser.on('PLAYER_CONNECTED', async (data) => { - data.player = await this.getPlayerBySteamID(data.steamID); + Logger.verbose("SquadServer", 1, `Player connected ${data.playerSuffix} - SteamID: ${data.steamID} - EOSID: ${data.eosID}`) + + this.rcon.addIds(data.steamID, data.eosID) + + data.player = await this.getPlayerByEOSID(data.steamID); if (data.player) data.player.suffix = data.playerSuffix; delete data.steamID; @@ -217,7 +223,7 @@ export default class SquadServer extends EventEmitter { }); this.logParser.on('PLAYER_DISCONNECTED', async (data) => { - data.player = await this.getPlayerBySteamID(data.steamID); + data.player = await this.getPlayerByEOSID(data.steamID); delete data.steamID; @@ -310,6 +316,23 @@ export default class SquadServer extends EventEmitter { this.logParser.on('TICK_RATE', (data) => { this.emit('TICK_RATE', data); }); + + this.logParser.on('CLIENT_EXTERNAL_ACCOUNT_INFO', (data) => { + this.rcon.addIds(data.steamID, data.eosID) + }) + // this.logParser.on('CLIENT_CONNECTED', (data) => { + // Logger.verbose("SquadServer", 1, `Client connected. Connection: ${data.connection} - SteamID: ${data.steamID}`) + // }) + // this.logParser.on('CLIENT_LOGIN_REQUEST', (data) => { + // Logger.verbose("SquadServer", 1, `Login request. ChainID: ${data.chainID} - Suffix: ${data.suffix} - EOSID: ${data.eosID}`) + + // }) + // this.logParser.on('RESOLVED_EOS_ID', (data) => { + // Logger.verbose("SquadServer", 1, `Resolved EOSID. ChainID: ${data.chainID} - Suffix: ${data.suffix} - EOSID: ${data.eosID}`) + // }) + // this.logParser.on('ADDING_CLIENT_CONNECTION', (data) => { + // Logger.verbose("SquadServer", 1, `Adding client connection`, data) + // }) } async restartLogParser() { @@ -325,12 +348,12 @@ export default class SquadServer extends EventEmitter { } getAdminPermsBySteamID(steamID) { - return this.admins[steamID]; + return this.admins[ steamID ]; } getAdminsWithPermission(perm) { const ret = []; - for (const [steamID, perms] of Object.entries(this.admins)) { + for (const [ steamID, perms ] of Object.entries(this.admins)) { if (perm in perms) ret.push(steamID); } return ret; @@ -348,13 +371,13 @@ export default class SquadServer extends EventEmitter { try { const oldPlayerInfo = {}; for (const player of this.players) { - oldPlayerInfo[player.steamID] = player; + oldPlayerInfo[ player.steamID ] = player; } const players = []; - for (const player of await this.rcon.getListPlayers()) + for (const player of await this.rcon.getListPlayers(this)) players.push({ - ...oldPlayerInfo[player.steamID], + ...oldPlayerInfo[ player.steamID ], ...player, playercontroller: this.logParser.eventStore.players[player.steamID] ? this.logParser.eventStore.players[player.steamID].controller @@ -365,17 +388,17 @@ export default class SquadServer extends EventEmitter { this.players = players; for (const player of this.players) { - if (typeof oldPlayerInfo[player.steamID] === 'undefined') continue; - if (player.teamID !== oldPlayerInfo[player.steamID].teamID) + if (typeof oldPlayerInfo[ player.steamID ] === 'undefined') continue; + if (player.teamID !== oldPlayerInfo[ player.steamID ].teamID) this.emit('PLAYER_TEAM_CHANGE', { player: player, - oldTeamID: oldPlayerInfo[player.steamID].teamID, + oldTeamID: oldPlayerInfo[ player.steamID ].teamID, newTeamID: player.teamID }); - if (player.squadID !== oldPlayerInfo[player.steamID].squadID) + if (player.squadID !== oldPlayerInfo[ player.steamID ].squadID) this.emit('PLAYER_SQUAD_CHANGE', { player: player, - oldSquadID: oldPlayerInfo[player.steamID].squadID, + oldSquadID: oldPlayerInfo[ player.steamID ].squadID, newSquadID: player.squadID }); } @@ -447,26 +470,32 @@ export default class SquadServer extends EventEmitter { Logger.verbose('SquadServer', 1, `Updating A2S information...`); try { - const data = await Gamedig.query({ - type: 'squad', - host: this.options.host, - port: this.options.queryPort - }); + // const data = await Gamedig.query({ + // type: 'squad', + // host: this.options.host, + // port: this.options.queryPort + // }); + + const rawData = await this.rcon.execute(`ShowServerInfo`); + Logger.verbose("SquadServer", 3, `A2S raw data`, rawData) + const data = JSON.parse(rawData); + Logger.verbose("SquadServer", 2, `A2S data`, JSON.data) + // Logger.verbose("SquadServer", 1, `A2S data`, JSON.stringify(data, null, 2)) const info = { - raw: data.raw, + raw: data, serverName: data.name, - maxPlayers: parseInt(data.maxplayers), - publicSlots: parseInt(data.raw.rules.NUMPUBCONN), - reserveSlots: parseInt(data.raw.rules.NUMPRIVCONN), + maxPlayers: parseInt(data.MaxPlayers), + publicSlots: parseInt(data.PublicQueueLimit_I), + reserveSlots: parseInt(data.PlayerReserveCount_I), - a2sPlayerCount: parseInt(data.raw.rules.PlayerCount_i), - publicQueue: parseInt(data.raw.rules.PublicQueue_i), - reserveQueue: parseInt(data.raw.rules.ReservedQueue_i), + a2sPlayerCount: parseInt(data.PlayerCount_I), + publicQueue: parseInt(data.PublicQueue_I), + reserveQueue: parseInt(data.ReservedQueue_I), - matchTimeout: parseFloat(data.raw.rules.MatchTimeout_f), - gameVersion: data.raw.version + matchTimeout: parseFloat(data.MatchTimeout_d), + gameVersion: data.GameVersion_s }; this.serverName = info.serverName; @@ -500,7 +529,7 @@ export default class SquadServer extends EventEmitter { if (!forceUpdate) { matches = this.players.filter(condition); - if (matches.length === 1) return matches[0]; + if (matches.length === 1) return matches[ 0 ]; if (!retry) return null; } @@ -508,7 +537,7 @@ export default class SquadServer extends EventEmitter { await this.updatePlayerList(); matches = this.players.filter(condition); - if (matches.length === 1) return matches[0]; + if (matches.length === 1) return matches[ 0 ]; return null; } @@ -518,7 +547,7 @@ export default class SquadServer extends EventEmitter { if (!forceUpdate) { matches = this.squads.filter(condition); - if (matches.length === 1) return matches[0]; + if (matches.length === 1) return matches[ 0 ]; if (!retry) return null; } @@ -526,7 +555,7 @@ export default class SquadServer extends EventEmitter { await this.updateSquadList(); matches = this.squads.filter(condition); - if (matches.length === 1) return matches[0]; + if (matches.length === 1) return matches[ 0 ]; return null; } @@ -541,6 +570,9 @@ export default class SquadServer extends EventEmitter { async getPlayerBySteamID(steamID, forceUpdate) { return this.getPlayerByCondition((player) => player.steamID === steamID, forceUpdate); } + async getPlayerByEOSID(eosID, forceUpdate) { + return this.getPlayerByCondition((player) => player.EOSID === eosID, forceUpdate); + } async getPlayerByName(name, forceUpdate) { return this.getPlayerByCondition((player) => player.name === name, forceUpdate); From cb420866cddd369682e3847888c7b1477b37689f Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Mon, 6 Nov 2023 22:53:29 +0100 Subject: [PATCH 02/53] chore: improved eventstore default objects --- core/log-parser/index.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/log-parser/index.js b/core/log-parser/index.js index 2a91ebbc..d5047366 100644 --- a/core/log-parser/index.js +++ b/core/log-parser/index.js @@ -16,9 +16,13 @@ export default class LogParser extends EventEmitter { this.eventStore = { disconnected: {}, // holding area, cleared on map change. - players: {}, // persistent data, steamid, controller, suffix. + players: [], // persistent data, steamid, controller, suffix. + playersEOS: [], // proxies from EOSID to persistent data, steamid, controller, suffix. + connectionIdToSteamID: new Map(), session: {}, // old eventstore, nonpersistent data - clients: {} // used in the connection chain before we resolve a player. + clients: {}, // used in the connection chain before we resolve a player. + lastConnection: {}, // used to store the last client connection data to then associate a steamid + joinRequests: [] }; this.linesPerMinute = 0; From 9878e553241c9ab9380af7119f20d03a37e088b6 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Mon, 6 Nov 2023 23:02:50 +0100 Subject: [PATCH 03/53] fix: updated regular expressions for matching EOSIDs in rcon.js --- squad-server/rcon.js | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/squad-server/rcon.js b/squad-server/rcon.js index b618ba49..dcef0af6 100644 --- a/squad-server/rcon.js +++ b/squad-server/rcon.js @@ -1,10 +1,11 @@ import Logger from 'core/logger'; import Rcon from 'core/rcon'; +import PersistentEOSIDtoSteamID from './plugins/persistent-eosid-to-steamid.js'; export default class SquadRcon extends Rcon { processChatPacket(decodedPacket) { const matchChat = decodedPacket.body.match( - /\[(ChatAll|ChatTeam|ChatSquad|ChatAdmin)] \[SteamID:([0-9]{17})] (.+?) : (.*)/ + /\[(ChatAll|ChatTeam|ChatSquad|ChatAdmin)] \[SteamID:([0-9a-f]{32})] (.+?) : (.*)/ ); if (matchChat) { Logger.verbose('SquadRcon', 2, `Matched chat message: ${decodedPacket.body}`); @@ -22,7 +23,7 @@ export default class SquadRcon extends Rcon { } const matchPossessedAdminCam = decodedPacket.body.match( - /\[SteamID:([0-9]{17})] (.+?) has possessed admin camera./ + /\[SteamID:([0-9a-f]{32})] (.+?) has possessed admin camera./ ); if (matchPossessedAdminCam) { Logger.verbose('SquadRcon', 2, `Matched admin camera possessed: ${decodedPacket.body}`); @@ -37,7 +38,7 @@ export default class SquadRcon extends Rcon { } const matchUnpossessedAdminCam = decodedPacket.body.match( - /\[SteamID:([0-9]{17})] (.+?) has unpossessed admin camera./ + /\[SteamID:([0-9a-f]{32})] (.+?) has unpossessed admin camera./ ); if (matchUnpossessedAdminCam) { Logger.verbose('SquadRcon', 2, `Matched admin camera possessed: ${decodedPacket.body}`); @@ -68,7 +69,7 @@ export default class SquadRcon extends Rcon { } const matchKick = decodedPacket.body.match( - /Kicked player ([0-9]+)\. \[steamid=([0-9]{17})] (.*)/ + /Kicked player ([0-9]+)\. \[steamid=([0-9a-f]{32})] (.*)/ ); if (matchKick) { Logger.verbose('SquadRcon', 2, `Matched kick message: ${decodedPacket.body}`); @@ -85,7 +86,7 @@ export default class SquadRcon extends Rcon { } const matchSqCreated = decodedPacket.body.match( - /(.+) \(Steam ID: ([0-9]{17})\) has created Squad (\d+) \(Squad Name: (.+)\) on (.+)/ + /(.+) \(Steam ID: ([0-9a-f]{32})\) has created Squad (\d+) \(Squad Name: (.+)\) on (.+)/ ); if (matchSqCreated) { Logger.verbose('SquadRcon', 2, `Matched Squad Created: ${decodedPacket.body}`); @@ -134,22 +135,32 @@ export default class SquadRcon extends Rcon { }; } - async getListPlayers() { + async getListPlayers(server) { const response = await this.execute('ListPlayers'); const players = []; if(!response || response.length < 1) return players; - + for (const line of response.split('\n')) { const match = line.match( - /ID: ([0-9]+) \| SteamID: ([0-9]{17}) \| Name: (.+) \| Team ID: ([0-9]+) \| Squad ID: ([0-9]+|N\/A) \| Is Leader: (True|False) \| Role: ([A-Za-z0-9_]*)\b/ + /ID: ([0-9]+) \| SteamID: ([0-9a-f]{32}) \| Name: (.+) \| Team ID: ([0-9]+) \| Squad ID: ([0-9]+|N\/A) \| Is Leader: (True|False) \| Role: ([A-Za-z0-9_]*)\b/ ); if (!match) continue; + let steamID = this.eosIndex[ match[ 2 ] ] + + const persEosIdPlugin = server.plugins.find(p => p instanceof PersistentEOSIDtoSteamID) + if (persEosIdPlugin && !this.eosIndex[ match[ 2 ] ]) { + steamID = (await persEosIdPlugin.getByEOSID(match[ 2 ])).steamID + this.addIds(steamID, match[ 2 ]); + Logger.verbose("RCON", 1, `Getting SteamID for player: ${match[ 3 ]} from EOSID: ${match[ 2 ]} => ${steamID}`) + } + players.push({ playerID: match[1], - steamID: match[2], + steamID: steamID, + EOSID: match[2], name: match[3], teamID: match[4], squadID: match[5] !== 'N/A' ? match[5] : null, @@ -168,11 +179,9 @@ export default class SquadRcon extends Rcon { let teamName; let teamID; - if(!responseSquad || responseSquad.length < 1) return squads; - for (const line of responseSquad.split('\n')) { const match = line.match( - /ID: ([0-9]+) \| Name: (.+) \| Size: ([0-9]+) \| Locked: (True|False) \| Creator Name: (.+) \| Creator Steam ID: ([0-9]{17})/ + /ID: ([0-9]+) \| Name: (.+) \| Size: ([0-9]+) \| Locked: (True|False) \| Creator Name: (.+) \| Creator Steam ID: ([0-9a-f]{32})/ ); const matchSide = line.match(/Team ID: (1|2) \((.+)\)/); if (matchSide) { From 4b0ee2023f114e21c8462236023ee66afabfa33d Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Mon, 6 Nov 2023 23:06:01 +0100 Subject: [PATCH 04/53] feat: updated log-parser with matching regex and new connection sequence --- .../log-parser/adding-client-connection.js | 20 +++++++++++ .../check-permission-resolve-eosid.js | 15 ++++++++ .../client-external-account-info.js | 21 ++++++++++++ squad-server/log-parser/client-login.js | 14 ++++---- squad-server/log-parser/index.js | 17 +++++++--- squad-server/log-parser/join-request.js | 16 +++++++++ squad-server/log-parser/login-request.js | 17 ++++++++++ .../pending-connection-destroyed.js | 2 +- squad-server/log-parser/player-connected.js | 34 +++++++++++-------- .../log-parser/player-disconnected.js | 2 +- .../log-parser/playercontroller-connected.js | 11 +++--- .../log-parser/sending-auth-result.js | 21 ++++++++++++ 12 files changed, 156 insertions(+), 34 deletions(-) create mode 100644 squad-server/log-parser/adding-client-connection.js create mode 100644 squad-server/log-parser/check-permission-resolve-eosid.js create mode 100644 squad-server/log-parser/client-external-account-info.js create mode 100644 squad-server/log-parser/join-request.js create mode 100644 squad-server/log-parser/login-request.js create mode 100644 squad-server/log-parser/sending-auth-result.js diff --git a/squad-server/log-parser/adding-client-connection.js b/squad-server/log-parser/adding-client-connection.js new file mode 100644 index 00000000..b4011e94 --- /dev/null +++ b/squad-server/log-parser/adding-client-connection.js @@ -0,0 +1,20 @@ +export default { + regex: + /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: AddClientConnection: Added client connection: \[UNetConnection\] RemoteAddr: ([\d\.]+):[0-9]+, Name: (EOSIpNetConnection_[0-9]+), Driver: GameNetDriver (EOSNetDriver_[0-9]+), IsServer: YES, PC: NULL, Owner: NULL, UniqueId: INVALID/, + onMatch: (args, logParser) => { + const data = { + raw: args[ 0 ], + time: args[ 1 ], + chainID: args[ 2 ], + // steamID: args[ 3 ], + ip: args[ 3 ], + connection: args[ 4 ], + driver: args[ 5 ] + }; + /* This is Called when unreal engine adds a client connection + First Step in Adding a Player to server + */ + logParser.eventStore[ 'last-connection' ] = data; + logParser.emit('ADDING_CLIENT_CONNECTION', data); + } +}; diff --git a/squad-server/log-parser/check-permission-resolve-eosid.js b/squad-server/log-parser/check-permission-resolve-eosid.js new file mode 100644 index 00000000..853200e6 --- /dev/null +++ b/squad-server/log-parser/check-permission-resolve-eosid.js @@ -0,0 +1,15 @@ +export default { + regex: + /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadCommon: SQCommonStatics Check Permissions, UniqueId:([\da-f]+)$/, + onMatch: (args, logParser) => { + const data = { + raw: args[ 0 ], + time: args[ 1 ], + chainID: +args[ 2 ], + eosID: args[ 3 ], + }; + + logParser.eventStore.joinRequests[ data.chainID ].eosID = data.eosID; + logParser.emit('RESOLVED_EOS_ID', { ...logParser.eventStore.joinRequests[ data.chainID ] }); + } +}; diff --git a/squad-server/log-parser/client-external-account-info.js b/squad-server/log-parser/client-external-account-info.js new file mode 100644 index 00000000..693016c3 --- /dev/null +++ b/squad-server/log-parser/client-external-account-info.js @@ -0,0 +1,21 @@ +export default { + regex: + /^\[([0-9.:-]+)]\[([ 0-9]+)]LogEOS: Verbose: \[LogEOSConnect] FConnectClient::CacheExternalAccountInfo - ProductUserId: (?[0-9a-f]{32}), AccountType: (\d), AccountId: (?[0-9]{17}), DisplayName: /, + onMatch: (args, logParser) => { + const data = { + raw: args[ 0 ], + time: args[ 1 ], + chainID: args[ 2 ], + eosID: args.groups.eosId, + steamID: args.groups.steamId, + }; + + logParser.eventStore.players[ data.steamID ] = { + eosID: data.eosID, + steamID: data.steamID + }; + logParser.eventStore.playersEOS[ data.eosID ] = logParser.eventStore.players[ data.steamID ] + + logParser.emit('CLIENT_EXTERNAL_ACCOUNT_INFO', data); + } +}; diff --git a/squad-server/log-parser/client-login.js b/squad-server/log-parser/client-login.js index 20b84d72..7f2406f8 100644 --- a/squad-server/log-parser/client-login.js +++ b/squad-server/log-parser/client-login.js @@ -1,20 +1,20 @@ export default { regex: - /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: Login: NewPlayer: SteamNetConnection \/Engine\/Transient\.(SteamNetConnection_[0-9]+)/, + /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: Login: NewPlayer: EOSIpNetConnection \/Engine\/Transient\.(EOSIpNetConnection_[0-9]+)/, onMatch: (args, logParser) => { const data = { - raw: args[0], - time: args[1], - chainID: args[2], - connection: args[3] + raw: args[ 0 ], + time: args[ 1 ], + chainID: args[ 2 ], + connection: args[ 3 ] }; /* This is Called when a player begins the Login process We use this to get a SteamID into playerConnected. 2nd Step in player connected path */ - logParser.eventStore['client-login'] = logParser.eventStore.clients[args[3]]; - delete logParser.eventStore.clients[args[3]]; + logParser.eventStore.joinRequests[ data.chainID ].connection = data.connection; + delete logParser.eventStore.clients[ args[ 3 ] ]; logParser.emit('CLIENT_LOGIN', data); } }; diff --git a/squad-server/log-parser/index.js b/squad-server/log-parser/index.js index 594734c4..2223b338 100644 --- a/squad-server/log-parser/index.js +++ b/squad-server/log-parser/index.js @@ -16,10 +16,14 @@ import RoundEnded from './round-ended.js'; import RoundTickets from './round-tickets.js'; import RoundWinner from './round-winner.js'; import ServerTickRate from './server-tick-rate.js'; -import ClientConnected from './client-connected.js'; +import AddingClientConnection from './adding-client-connection.js'; import ClientLogin from './client-login.js'; import PendingConnectionDestroyed from './pending-connection-destroyed.js'; - +import clientExternalAccountInfo from './client-external-account-info.js'; +import sendingAuthResult from './sending-auth-result.js'; +import loginRequest from './login-request.js'; +import joinRequest from './join-request.js'; +import checkPermissionResolveEosid from './check-permission-resolve-eosid.js'; export default class SquadLogParser extends LogParser { constructor(options) { super('SquadGame.log', options); @@ -43,9 +47,14 @@ export default class SquadLogParser extends LogParser { RoundTickets, RoundWinner, ServerTickRate, - ClientConnected, + AddingClientConnection, ClientLogin, - PendingConnectionDestroyed + PendingConnectionDestroyed, + clientExternalAccountInfo, + sendingAuthResult, + loginRequest, + joinRequest, + checkPermissionResolveEosid, ]; } } diff --git a/squad-server/log-parser/join-request.js b/squad-server/log-parser/join-request.js new file mode 100644 index 00000000..3fdfd3fe --- /dev/null +++ b/squad-server/log-parser/join-request.js @@ -0,0 +1,16 @@ +export default { + regex: + /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: Join request: .+\?Name=(.+)\?SplitscreenCount=\d$/, + onMatch: (args, logParser) => { + const data = { + raw: args[ 0 ], + time: args[ 1 ], + chainID: +args[ 2 ], + suffix: args[ 3 ], + }; + + logParser.eventStore.joinRequests[ data.chainID ] = data; + // console.log(logParser.eventStore.loginRequests[ data.chainID ]) + logParser.emit('CLIENT_JOIN_REQUEST', data); + } +}; diff --git a/squad-server/log-parser/login-request.js b/squad-server/log-parser/login-request.js new file mode 100644 index 00000000..e1cc8226 --- /dev/null +++ b/squad-server/log-parser/login-request.js @@ -0,0 +1,17 @@ +export default { + regex: + /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: Login request: \?Name=(.+) userId: RedpointEOS:([\da-f]{32}) platform: RedpointEOS/, + onMatch: (args, logParser) => { + const data = { + raw: args[ 0 ], + time: args[ 1 ], + chainID: +args[ 2 ], + suffix: args[ 3 ], + eosID: args[ 4 ] + }; + + // logParser.eventStore.loginRequests[ data.chainID ] = data; + // console.log(logParser.eventStore.loginRequests[ data.chainID ]) + logParser.emit('CLIENT_LOGIN_REQUEST', data); + } +}; diff --git a/squad-server/log-parser/pending-connection-destroyed.js b/squad-server/log-parser/pending-connection-destroyed.js index 3d4aa1c6..5ec32398 100644 --- a/squad-server/log-parser/pending-connection-destroyed.js +++ b/squad-server/log-parser/pending-connection-destroyed.js @@ -1,6 +1,6 @@ export default { regex: - /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: UNetConnection::PendingConnectionLost\. \[UNetConnection\] RemoteAddr: ([0-9]{17}):[0-9]+, Name: (SteamNetConnection_[0-9]+), Driver: GameNetDriver (SteamNetDriver_[0-9]+), IsServer: YES, PC: NULL, Owner: NULL, UniqueId: (?:Steam:UNKNOWN \[.+\]|INVALID) bPendingDestroy=0/, + /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: UNetConnection::PendingConnectionLost\. \[UNetConnection\] RemoteAddr: ([0-9a-f]{32}):[0-9]+, Name: (SteamNetConnection_[0-9]+), Driver: GameNetDriver (SteamNetDriver_[0-9]+), IsServer: YES, PC: NULL, Owner: NULL, UniqueId: (?:Steam:UNKNOWN \[.+\]|INVALID) bPendingDestroy=0/, onMatch: (args, logParser) => { const data = { raw: args[0], diff --git a/squad-server/log-parser/player-connected.js b/squad-server/log-parser/player-connected.js index 279257d7..9e768c29 100644 --- a/squad-server/log-parser/player-connected.js +++ b/squad-server/log-parser/player-connected.js @@ -2,26 +2,30 @@ export default { regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: Join succeeded: (.+)/, onMatch: (args, logParser) => { const data = { - raw: args[0], - time: args[1], - chainID: args[2], - playerSuffix: args[3], - steamID: logParser.eventStore['client-login'], // player connected - controller: logParser.eventStore['player-controller'] // playercontroller connected + raw: args[ 0 ], + time: args[ 1 ], + chainID: +args[ 2 ], + playerSuffix: args[ 3 ] }; - delete logParser.eventStore['client-login']; - delete logParser.eventStore['player-controller']; + // console.log(`ChainID: ${data.chainID}`, logParser.eventStore.joinRequests[ data.chainID ]); + const joinRequestsData = { ...logParser.eventStore.joinRequests[ data.chainID ] }; + // console.log('loginRequestData', loginRequestData) + + data.eosID = joinRequestsData.eosID + data.controller = joinRequestsData.controller + data.steamID = `${logParser.eventStore.connectionIdToSteamID.get(joinRequestsData.connection)}` + + logParser.eventStore.connectionIdToSteamID.delete(joinRequestsData.connection) + + delete logParser.eventStore.joinRequests[ +data.chainID ]; // Handle Reconnecting players - if (logParser.eventStore.disconnected[data.steamID]) { - delete logParser.eventStore.disconnected[data.steamID]; + if (logParser.eventStore.disconnected[ data.steamID ]) { + delete logParser.eventStore.disconnected[ data.steamID ]; } logParser.emit('PLAYER_CONNECTED', data); - logParser.eventStore.players[data.steamID] = { - steamID: data.steamID, - suffix: data.playerSuffix, - controller: data.controller - }; + // logParser.eventStore.players[ data.steamID ].suffix = data.playerSuffix + // logParser.eventStore.players[ data.steamID ].controller = data.controller } }; diff --git a/squad-server/log-parser/player-disconnected.js b/squad-server/log-parser/player-disconnected.js index 09976979..9fd86e90 100644 --- a/squad-server/log-parser/player-disconnected.js +++ b/squad-server/log-parser/player-disconnected.js @@ -1,6 +1,6 @@ export default { regex: - /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: UChannel::Close: Sending CloseBunch\. ChIndex == [0-9]+\. Name: \[UChannel\] ChIndex: [0-9]+, Closing: [0-9]+ \[UNetConnection\] RemoteAddr: ([0-9]{17}):[0-9]+, Name: SteamNetConnection_[0-9]+, Driver: GameNetDriver SteamNetDriver_[0-9]+, IsServer: YES, PC: ([^ ]+PlayerController_C_[0-9]+), Owner: [^ ]+PlayerController_C_[0-9]+/, + /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: UChannel::Close: Sending CloseBunch\. ChIndex == [0-9]+\. Name: \[UChannel\] ChIndex: [0-9]+, Closing: [0-9]+ \[UNetConnection\] RemoteAddr: ([0-9a-f]{32}):[0-9]+, Name: SteamNetConnection_[0-9]+, Driver: GameNetDriver SteamNetDriver_[0-9]+, IsServer: YES, PC: ([^ ]+PlayerController_C_[0-9]+), Owner: [^ ]+PlayerController_C_[0-9]+/, onMatch: (args, logParser) => { const data = { raw: args[0], diff --git a/squad-server/log-parser/playercontroller-connected.js b/squad-server/log-parser/playercontroller-connected.js index 18a0216f..0e8040de 100644 --- a/squad-server/log-parser/playercontroller-connected.js +++ b/squad-server/log-parser/playercontroller-connected.js @@ -3,14 +3,13 @@ export default { /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: PostLogin: NewPlayer: BP_PlayerController_C .+(BP_PlayerController_C_[0-9]+)/, onMatch: (args, logParser) => { const data = { - raw: args[0], - time: args[1], - chainID: args[2], - controller: args[3] + raw: args[ 0 ], + time: args[ 1 ], + chainID: +args[ 2 ], + controller: args[ 3 ] }; - logParser.eventStore['player-controller'] = args[3]; - + logParser.eventStore.joinRequests[ data.chainID ].controller = data.controller; logParser.emit('PLAYER_CONTROLLER_CONNECTED', data); } }; diff --git a/squad-server/log-parser/sending-auth-result.js b/squad-server/log-parser/sending-auth-result.js new file mode 100644 index 00000000..ce0ca8c8 --- /dev/null +++ b/squad-server/log-parser/sending-auth-result.js @@ -0,0 +1,21 @@ +export default { + regex: + /^\[([0-9.:-]+)]\[([ 0-9]*)]LogOnline: STEAM: AUTH HANDLER: Sending auth result to user (\d{17}) with flag success\? 1/, + onMatch: (args, logParser) => { + if (!logParser.eventStore[ 'last-connection' ]) return; + + const data = { + ...logParser.eventStore[ 'last-connection' ], + steamID: args[ 3 ] + }; + /* This is Called when unreal engine adds a client connection + First Step in Adding a Player to server + */ + + logParser.eventStore.clients[ data.connection ] = data.steamID; + logParser.eventStore.connectionIdToSteamID.set(data.connection, data.steamID) + logParser.emit('CLIENT_CONNECTED', data); + + delete logParser.eventStore[ 'last-connection' ]; + } +}; From c43fec5fc1bdc728771addc862f5a3f43860cab1 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Mon, 6 Nov 2023 23:07:23 +0100 Subject: [PATCH 05/53] feat: plugin to store in a database known steamid-eosid associations --- config.json | 5 + .../plugins/persistent-eosid-to-steamid.js | 91 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 squad-server/plugins/persistent-eosid-to-steamid.js diff --git a/config.json b/config.json index 5ac773e4..b707e35e 100644 --- a/config.json +++ b/config.json @@ -42,6 +42,11 @@ "sqlite": "sqlite:database.sqlite" }, "plugins": [ + { + "plugin": "PersistentEOSIDtoSteamID", + "enabled": true, + "database": "sqlite" + }, { "plugin": "AutoKickUnassigned", "enabled": true, diff --git a/squad-server/plugins/persistent-eosid-to-steamid.js b/squad-server/plugins/persistent-eosid-to-steamid.js new file mode 100644 index 00000000..0fa05988 --- /dev/null +++ b/squad-server/plugins/persistent-eosid-to-steamid.js @@ -0,0 +1,91 @@ +import Sequelize from 'sequelize'; + +import BasePlugin from './base-plugin.js'; + +const { DataTypes } = Sequelize; + +export default class PersistentEOSIDtoSteamID extends BasePlugin { + static get description() { + return "Stores into a DB every association of SteamID-EOSID"; + } + + static get defaultEnabled() { + return false; + } + + static get optionsSpecification() { + return { + database: { + required: true, + connector: 'sequelize', + description: 'The Sequelize connector.', + default: 'sqlite' + } + }; + } + + constructor(server, options, connectors) { + super(server, options, connectors); + + this.models = {}; + + this.createModel( + 'SteamIDtoEOSID', + { + steamID: { + type: DataTypes.STRING, + primaryKey: true + }, + eosID: { + type: DataTypes.STRING, + } + }, + { + charset: 'utf8mb4', + collate: 'utf8mb4_unicode_ci' + } + ); + + this.onPlayerConnected = this.onPlayerConnected.bind(this); + } + + createModel(name, schema) { + this.models[ name ] = this.options.database.define(`EOS_${name}`, schema, { + timestamps: false, + indexes: [ + { + unique: true, + fields: [ 'eosID' ] + }, + ] + }); + } + + async prepareToMount() { + await this.models.SteamIDtoEOSID.sync(); + } + + async mount() { + this.server.on('PLAYER_CONNECTED', this.onPlayerConnected); + this.verbose(1, 'Mounted') + } + + async unmount() { + this.server.removeEventListener('PLAYER_CONNECTED', this.onPlayerConnected); + } + + async onPlayerConnected(info) { + await this.models.SteamIDtoEOSID.upsert({ + steamID: info.player.steamID, + eosID: info.eosID + }); + } + + async getByEOSID(eosID) { + return await this.models.SteamIDtoEOSID.findOne({ where: { eosID: eosID } }) + } + + async getBySteamID(steamID) { + return await this.models.SteamIDtoEOSID.findOne({ where: { steamID: steamID } }) + } +} From b4d87d7be4ded54f6de2603333c58b65f8dd557a Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Mon, 6 Nov 2023 23:10:49 +0100 Subject: [PATCH 06/53] chore: removed console.log --- core/rcon.js | 1 - 1 file changed, 1 deletion(-) diff --git a/core/rcon.js b/core/rcon.js index f299c010..9799b8ee 100644 --- a/core/rcon.js +++ b/core/rcon.js @@ -129,7 +129,6 @@ export default class Rcon extends EventEmitter { return buffer; } #onData(data) { - console.log(data) Logger.verbose("RCON", 4, `Got data: ${this.#bufToHexString(data)}`); this.stream = Buffer.concat([this.stream, data], this.stream.byteLength + data.byteLength); while (this.stream.byteLength >= 7) { From 685248df565e3d08d13dc103f48f6885593b52ea Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Sat, 18 Nov 2023 23:31:34 +0100 Subject: [PATCH 07/53] chore: rcon server messages to file + convert steamids to eosid in rcon->execute() method --- core/rcon.js | 83 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 23 deletions(-) diff --git a/core/rcon.js b/core/rcon.js index 9799b8ee..fdaa0585 100644 --- a/core/rcon.js +++ b/core/rcon.js @@ -1,10 +1,13 @@ import { EventEmitter } from "node:events"; import net from "node:net"; import Logger from "./logger.js"; +import fs from 'fs'; +import path from 'path'; +const RCON_LOG_FILEPATH = "RCON_RECEIVED_MESSAGES.log" export default class Rcon extends EventEmitter { constructor(options = {}) { super(); - for (const option of ["host", "port", "password"]) if (!(option in options)) throw new Error(`${option} must be specified.`); + for (const option of [ "host", "port", "password" ]) if (!(option in options)) throw new Error(`${option} must be specified.`); this.host = options.host; this.port = options.port; this.password = options.password; @@ -26,13 +29,15 @@ export default class Rcon extends EventEmitter { this.passThroughTimeOut = options.passThroughTimeOut || 60000; this.passThroughMaxClients = 1; //options.passThroughMaxClients || 10; this.passThroughChallenge = options.passThroughChallenge || options.password; + this.dumpRconResponsesToFile = options.dumpRconResponsesToFile || false; this.rconClients = {}; - for (let i = 1; i <= this.passThroughMaxClients; i++) this.rconClients[`${i}`] = null; + for (let i = 1; i <= this.passThroughMaxClients; i++) this.rconClients[ `${i}` ] = null; this.ptServer = null; this.steamIndex = { "76561198799344716": "00026e21ce3d43c792613bdbb6dec1ba" }; // example dtata this.eosIndex = { "00026e21ce3d43c792613bdbb6dec1ba": "76561198799344716" }; // example dtata + this.rotateLogFile(RCON_LOG_FILEPATH) } processChatPacket(decodedPacket) { @@ -79,6 +84,12 @@ export default class Rcon extends EventEmitter { }); } async execute(body) { + let steamID = body.match(/\d{17}/); + if (steamID) { + steamID = steamID[ 0 ] + body = body.replace(/\d{17}/, this.steamIndex[ steamID ]); + } + return new Promise((resolve, reject) => { if (!this.connected) return reject(new Error("Rcon not connected.")); if (!this.client.writable) return reject(new Error("Unable to write to node:net socket")); @@ -95,7 +106,7 @@ export default class Rcon extends EventEmitter { this.removeListener(listenerId, outputData); return reject(new Error(`Rcon response timed out`)); }; - if (this.msgId > this.msgIdHigh -2) this.msgId = this.msgIdLow; + if (this.msgId > this.msgIdHigh - 2) this.msgId = this.msgIdLow; const listenerId = `response${this.msgId}`; const timeOut = setTimeout(timedOut, 10000); this.once(listenerId, outputData); @@ -130,12 +141,13 @@ export default class Rcon extends EventEmitter { } #onData(data) { Logger.verbose("RCON", 4, `Got data: ${this.#bufToHexString(data)}`); - this.stream = Buffer.concat([this.stream, data], this.stream.byteLength + data.byteLength); + this.stream = Buffer.concat([ this.stream, data ], this.stream.byteLength + data.byteLength); while (this.stream.byteLength >= 7) { const packet = this.#decode(); if (!packet) break; else Logger.verbose("RCON", 3, `Processing decoded packet: Size: ${packet.size}, ID: ${packet.id}, Type: ${packet.type}, Body: ${packet.body}`); - + this.appendToFile(RCON_LOG_FILEPATH, packet.body) + if (packet.id > this.msgIdHigh) this.emit(`responseForward_1`, packet); else if (packet.type === this.type.response) this.#onResponse(packet); else if (packet.type === this.type.server) this.#onServer(packet); @@ -145,12 +157,12 @@ export default class Rcon extends EventEmitter { #onServer(packet) { this.emit("server", packet); for (const client in this.rconClients) - if (this.rconClients[client]) { - this.emit(`serverForward_${this.rconClients[client].rconIdClient}`, packet.body); + if (this.rconClients[ client ]) { + this.emit(`serverForward_${this.rconClients[ client ].rconIdClient}`, packet.body); } } #decode() { - if (this.stream[0] === 0 && this.stream[1] === 1 && this.stream[2] === 0 && this.stream[3] === 0 && this.stream[4] === 0 && this.stream[5] === 0 && this.stream[6] === 0) { + if (this.stream[ 0 ] === 0 && this.stream[ 1 ] === 1 && this.stream[ 2 ] === 0 && this.stream[ 3 ] === 0 && this.stream[ 4 ] === 0 && this.stream[ 5 ] === 0 && this.stream[ 6 ] === 0) { this.stream = this.stream.subarray(7); return this.soh; } @@ -159,11 +171,11 @@ export default class Rcon extends EventEmitter { else if (bufSize <= this.stream.byteLength - 4 && this.stream.byteLength >= 12) { const bufId = this.stream.readInt32LE(4); const bufType = this.stream.readInt32LE(8); - if (this.stream[bufSize + 2] !== 0 || this.stream[bufSize + 3] !== 0 || bufId < 0 || bufType < 0 || bufType > 5) return this.#badPacket(); + if (this.stream[ bufSize + 2 ] !== 0 || this.stream[ bufSize + 3 ] !== 0 || bufId < 0 || bufType < 0 || bufType > 5) return this.#badPacket(); else { const response = { size: bufSize, id: bufId, type: bufType, body: this.stream.toString("utf8", 12, bufSize + 2) }; this.stream = this.stream.subarray(bufSize + 4); - if (response.body === "" && this.stream[0] === 0 && this.stream[1] === 1 && this.stream[2] === 0 && this.stream[3] === 0 && this.stream[4] === 0 && this.stream[5] === 0 && this.stream[6] === 0) { + if (response.body === "" && this.stream[ 0 ] === 0 && this.stream[ 1 ] === 1 && this.stream[ 2 ] === 0 && this.stream[ 3 ] === 0 && this.stream[ 4 ] === 0 && this.stream[ 5 ] === 0 && this.stream[ 6 ] === 0) { this.stream = this.stream.subarray(7); response.body = ""; } @@ -213,7 +225,7 @@ export default class Rcon extends EventEmitter { this.ptServer.listen(this.passThroughPort, () => Logger.verbose("RCON", 1, `Pass-through Server: Listening on port ${this.passThroughPort}`)); } closeServer() { - for (const client in this.rconClients) if (this.rconClients[client]) this.rconClients[client].end(); + for (const client in this.rconClients) if (this.rconClients[ client ]) this.rconClients[ client ].end(); if (!this.ptServer) return; this.ptServer.close(() => this.#onServerClose()); } @@ -239,7 +251,7 @@ export default class Rcon extends EventEmitter { if (!client.rconIdClient) return; this.removeAllListeners(`serverForward_${client.rconIdClient}`); this.removeAllListeners(`responseForward_${client.rconIdClient}`); - this.rconClients[`${client.rconIdClient}`] = null; + this.rconClients[ `${client.rconIdClient}` ] = null; Logger.verbose("RCON", 1, `Pass-through Server: Client-${client.rconIdClient} Disconnected`); } #onClientTimeOut(client) { @@ -248,7 +260,7 @@ export default class Rcon extends EventEmitter { } #onClientData(client, data) { if (!client.rconStream) client.rconStream = new Buffer.alloc(0); - client.rconStream = Buffer.concat([client.rconStream, data], client.rconStream.byteLength + data.byteLength); + client.rconStream = Buffer.concat([ client.rconStream, data ], client.rconStream.byteLength + data.byteLength); while (client.rconStream.byteLength >= 4) { const packet = this.#decodeClient(client); if (!packet) break; @@ -258,7 +270,7 @@ export default class Rcon extends EventEmitter { if (!client.rconWheel || client.rconWheel > 20) client.rconWheel = 0; else client.rconWheel++; - client.rconIdQueueNEW[`${client.rconWheel}`] = packet.id + client.rconIdQueueNEW[ `${client.rconWheel}` ] = packet.id const encoded = this.#encode(packet.type, this.specialId + client.rconWheel, this.#steamToEosClient(packet.body)); //////////////////////////////////////////////// this.client.write(encoded.toString("binary"), "binary"); @@ -287,9 +299,9 @@ export default class Rcon extends EventEmitter { client.rconHasAuthed = true; client.rconIdQueueNEW = {} for (let i = 1; i <= this.passThroughMaxClients; i++) { - if (this.rconClients[`${i}`] === null) { + if (this.rconClients[ `${i}` ] === null) { client.rconIdClient = i; - this.rconClients[`${i}`] = client; + this.rconClients[ `${i}` ] = client; break; } } @@ -307,11 +319,11 @@ export default class Rcon extends EventEmitter { //console.log(client.rconIdQueueNEW);////////////////////////////////////////////////////////////////////////////////////////// - client.write(this.#encode(packet.type, client.rconIdQueueNEW[int], this.#eosToSteam(packet.body)).toString("binary"), "binary"); + client.write(this.#encode(packet.type, client.rconIdQueueNEW[ int ], this.#eosToSteam(packet.body)).toString("binary"), "binary"); } else if (packet.body != "") { const int = packet.id - this.specialId - client.write(this.#encode(0, client.rconIdQueueNEW[int]).toString("binary"), "binary"); - client.write(this.#encodeSpecial(client.rconIdQueueNEW[int]).toString("binary"), "binary"); + client.write(this.#encode(0, client.rconIdQueueNEW[ int ]).toString("binary"), "binary"); + client.write(this.#encodeSpecial(client.rconIdQueueNEW[ int ]).toString("binary"), "binary"); } } #encodeSpecial(id) { @@ -336,8 +348,8 @@ export default class Rcon extends EventEmitter { } addIds(steamId, eosId) { - this.steamIndex[steamId] = eosId; // { "76561198799344716": "00026e21ce3d43c792613bdbb6dec1ba" }; - this.eosIndex[eosId] = steamId; + this.steamIndex[ steamId ] = eosId; // { "76561198799344716": "00026e21ce3d43c792613bdbb6dec1ba" }; + this.eosIndex[ eosId ] = steamId; } removeIds(eosId) { @@ -347,7 +359,7 @@ export default class Rcon extends EventEmitter { #steamToEosClient(body) { //assume client does not send more than 1 steamId per msg const m = body.match(/[0-9]{17}/); - if (m && m[1] in this.steamIndex) return body.replaceAll(`${m[0]}`, this.steamIndex[m[0]]); + if (m && m[ 1 ] in this.steamIndex) return body.replaceAll(`${m[ 0 ]}`, this.steamIndex[ m[ 0 ] ]); return body; } @@ -363,11 +375,36 @@ export default class Rcon extends EventEmitter { console.warn(line); for (const r of this.defs) { const match = line.match(r.regex); - if (match && match.groups.eosId in this.eosIndex) return r.rep(line, this.eosIndex[match.groups.eosId], match.groups.eosId); + if (match && match.groups.eosId in this.eosIndex) return r.rep(line, this.eosIndex[ match.groups.eosId ], match.groups.eosId); } return line; } + appendToFile(filePath, content) { + if (!this.dumpRconResponsesToFile) return; + const dir = path.dirname(filePath); + + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + fs.appendFile(filePath, content + "\n", (err) => { + if (err) throw err; + }); + } + rotateLogFile(logFile) { + if (!this.dumpRconResponsesToFile) return; + if (fs.existsSync(logFile)) { + const ext = path.extname(logFile); + const base = path.basename(logFile, ext); + const dir = path.dirname(logFile); + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const newFile = path.join(dir, `${base}_${timestamp}${ext}`); + + fs.renameSync(logFile, newFile); + } + } + defs = [ //strict matching to avoid 'name as steamId errors' { From 522b85cdcc4aeba741c0682bd81e5aad995cbed6 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Sat, 18 Nov 2023 23:33:03 +0100 Subject: [PATCH 08/53] fix+chore: improved server information parsing + fixed server public slots count --- squad-server/index.js | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/squad-server/index.js b/squad-server/index.js index e3506518..36854639 100644 --- a/squad-server/index.js +++ b/squad-server/index.js @@ -76,7 +76,7 @@ export default class SquadServer extends EventEmitter { await this.logParser.watch(); await this.updateSquadList(); - await this.updatePlayerList(); + await this.updatePlayerList(this); await this.updateLayerInformation(); await this.updateA2SInformation(); @@ -96,12 +96,13 @@ export default class SquadServer extends EventEmitter { port: this.options.rconPort, password: this.options.rconPassword, autoReconnectInterval: this.options.rconAutoReconnectInterval, + dumpRconResponsesToFile: this.options.dumpRconResponsesToFile, passThroughPort: this.options.rconPassThroughPort, passThrough: this.options.rconPassThrough }); this.rcon.on('CHAT_MESSAGE', async (data) => { - data.player = await this.getPlayerByEOSID(data.steamID); + data.player = await this.getPlayerBySteamID(data.steamID); this.emit('CHAT_MESSAGE', data); const command = data.message.match(/!([^ ]+) ?(.*)/); @@ -113,7 +114,7 @@ export default class SquadServer extends EventEmitter { }); this.rcon.on('POSSESSED_ADMIN_CAMERA', async (data) => { - data.player = await this.getPlayerByEOSID(data.steamID); + data.player = await this.getPlayerBySteamID(data.steamID); this.adminsInAdminCam[ data.steamID ] = data.time; @@ -121,7 +122,7 @@ export default class SquadServer extends EventEmitter { }); this.rcon.on('UNPOSSESSED_ADMIN_CAMERA', async (data) => { - data.player = await this.getPlayerByEOSID(data.steamID); + data.player = await this.getPlayerBySteamID(data.steamID); if (this.adminsInAdminCam[ data.steamID ]) { data.duration = data.time.getTime() - this.adminsInAdminCam[ data.steamID ].getTime(); } else { @@ -144,19 +145,19 @@ export default class SquadServer extends EventEmitter { }); this.rcon.on('PLAYER_KICKED', async (data) => { - data.player = await this.getPlayerByEOSID(data.steamID); + data.player = await this.getPlayerBySteamID(data.steamID); this.emit('PLAYER_KICKED', data); }); this.rcon.on('PLAYER_BANNED', async (data) => { - data.player = await this.getPlayerByEOSID(data.steamID); + data.player = await this.getPlayerBySteamID(data.steamID); this.emit('PLAYER_BANNED', data); }); this.rcon.on('SQUAD_CREATED', async (data) => { - data.player = await this.getPlayerByEOSID(data.playerSteamID, true); + data.player = await this.getPlayerBySteamID(data.playerSteamID, true); delete data.playerName; delete data.playerSteamID; @@ -213,7 +214,7 @@ export default class SquadServer extends EventEmitter { this.rcon.addIds(data.steamID, data.eosID) - data.player = await this.getPlayerByEOSID(data.steamID); + data.player = await this.getPlayerBySteamID(data.steamID); if (data.player) data.player.suffix = data.playerSuffix; delete data.steamID; @@ -223,7 +224,7 @@ export default class SquadServer extends EventEmitter { }); this.logParser.on('PLAYER_DISCONNECTED', async (data) => { - data.player = await this.getPlayerByEOSID(data.steamID); + data.player = await this.getPlayerBySteamID(data.steamID); delete data.steamID; @@ -379,8 +380,8 @@ export default class SquadServer extends EventEmitter { players.push({ ...oldPlayerInfo[ player.steamID ], ...player, - playercontroller: this.logParser.eventStore.players[player.steamID] - ? this.logParser.eventStore.players[player.steamID].controller + playercontroller: this.logParser.eventStore.players[ player.steamID ] + ? this.logParser.eventStore.players[ player.steamID ].controller : null, squad: await this.getSquadByID(player.teamID, player.squadID) }); @@ -484,16 +485,22 @@ export default class SquadServer extends EventEmitter { const info = { raw: data, - serverName: data.name, + serverName: data.ServerName_s, maxPlayers: parseInt(data.MaxPlayers), - publicSlots: parseInt(data.PublicQueueLimit_I), + publicQueueLimit: parseInt(data.PublicQueueLimit_I), reserveSlots: parseInt(data.PlayerReserveCount_I), - a2sPlayerCount: parseInt(data.PlayerCount_I), + playerCount: parseInt(data.PlayerCount_I), publicQueue: parseInt(data.PublicQueue_I), reserveQueue: parseInt(data.ReservedQueue_I), + currentLayer: data.MapName_s, + nextLayer: data.NextLayer_s, + + teamOne: data.TeamOne_s.replace(new RegExp(data.MapName_s, "i"), ''), + teamTwo: data.TeamTwo_s.replace(new RegExp(data.MapName_s, "i"), ''), + matchTimeout: parseFloat(data.MatchTimeout_d), gameVersion: data.GameVersion_s }; @@ -501,10 +508,10 @@ export default class SquadServer extends EventEmitter { this.serverName = info.serverName; this.maxPlayers = info.maxPlayers; - this.publicSlots = info.publicSlots; + this.publicSlots = info.maxPlayers - info.reserveSlots; this.reserveSlots = info.reserveSlots; - this.a2sPlayerCount = info.a2sPlayerCount; + this.a2sPlayerCount = info.playerCount; this.publicQueue = info.publicQueue; this.reserveQueue = info.reserveQueue; @@ -512,6 +519,7 @@ export default class SquadServer extends EventEmitter { this.gameVersion = info.gameVersion; this.emit('UPDATED_A2S_INFORMATION', info); + this.emit('UPDATED_SERVER_INFORMATION', info); } catch (err) { Logger.verbose('SquadServer', 1, 'Failed to update A2S information.', err); } From 82dcbd49a67def9f163e8c55952b1dbbcd480b90 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Sat, 18 Nov 2023 23:34:04 +0100 Subject: [PATCH 09/53] feat: support Squad V7 rcon messages --- squad-server/rcon.js | 109 ++++++++++++++++++++----------------------- 1 file changed, 51 insertions(+), 58 deletions(-) diff --git a/squad-server/rcon.js b/squad-server/rcon.js index dcef0af6..9ad8321c 100644 --- a/squad-server/rcon.js +++ b/squad-server/rcon.js @@ -5,17 +5,18 @@ import PersistentEOSIDtoSteamID from './plugins/persistent-eosid-to-steamid.js'; export default class SquadRcon extends Rcon { processChatPacket(decodedPacket) { const matchChat = decodedPacket.body.match( - /\[(ChatAll|ChatTeam|ChatSquad|ChatAdmin)] \[SteamID:([0-9a-f]{32})] (.+?) : (.*)/ + /\[(ChatAll|ChatTeam|ChatSquad|ChatAdmin)] \[Online IDs:EOS: ([0-9a-f]{32}) steam: (\d{17})\] (.+?) : (.*)/ ); if (matchChat) { Logger.verbose('SquadRcon', 2, `Matched chat message: ${decodedPacket.body}`); this.emit('CHAT_MESSAGE', { raw: decodedPacket.body, - chat: matchChat[1], - steamID: matchChat[2], - name: matchChat[3], - message: matchChat[4], + chat: matchChat[ 1 ], + eosID: matchChat[ 2 ], + steamID: matchChat[ 3 ], + name: matchChat[ 4 ], + message: matchChat[ 5 ], time: new Date() }); @@ -23,14 +24,14 @@ export default class SquadRcon extends Rcon { } const matchPossessedAdminCam = decodedPacket.body.match( - /\[SteamID:([0-9a-f]{32})] (.+?) has possessed admin camera./ + /\[Online Ids:EOS: ([0-9a-f]{32}) steam: (\d{17})\] (.+) has possessed admin camera\./ ); if (matchPossessedAdminCam) { Logger.verbose('SquadRcon', 2, `Matched admin camera possessed: ${decodedPacket.body}`); this.emit('POSSESSED_ADMIN_CAMERA', { raw: decodedPacket.body, - steamID: matchPossessedAdminCam[1], - name: matchPossessedAdminCam[2], + steamID: matchPossessedAdminCam[ 2 ], + name: matchPossessedAdminCam[ 3 ], time: new Date() }); @@ -38,14 +39,14 @@ export default class SquadRcon extends Rcon { } const matchUnpossessedAdminCam = decodedPacket.body.match( - /\[SteamID:([0-9a-f]{32})] (.+?) has unpossessed admin camera./ + /\[Online IDs:EOS: ([0-9a-f]{32}) steam: (\d{17})\] (.+) has unpossessed admin camera\./ ); if (matchUnpossessedAdminCam) { Logger.verbose('SquadRcon', 2, `Matched admin camera possessed: ${decodedPacket.body}`); this.emit('UNPOSSESSED_ADMIN_CAMERA', { raw: decodedPacket.body, - steamID: matchUnpossessedAdminCam[1], - name: matchUnpossessedAdminCam[2], + steamID: matchPossessedAdminCam[ 2 ], + name: matchPossessedAdminCam[ 3 ], time: new Date() }); @@ -60,8 +61,8 @@ export default class SquadRcon extends Rcon { this.emit('PLAYER_WARNED', { raw: decodedPacket.body, - name: matchWarn[1], - reason: matchWarn[2], + name: matchWarn[ 1 ], + reason: matchWarn[ 2 ], time: new Date() }); @@ -69,16 +70,16 @@ export default class SquadRcon extends Rcon { } const matchKick = decodedPacket.body.match( - /Kicked player ([0-9]+)\. \[steamid=([0-9a-f]{32})] (.*)/ + /Kicked player ([0-9]+)\. \[Online IDs= EOS: ([0-9a-f]{32}) steam: (\d{17})] (.*)/ ); if (matchKick) { Logger.verbose('SquadRcon', 2, `Matched kick message: ${decodedPacket.body}`); this.emit('PLAYER_KICKED', { raw: decodedPacket.body, - playerID: matchKick[1], - steamID: matchKick[2], - name: matchKick[3], + playerID: matchKick[ 1 ], + steamID: matchKick[ 3 ], + name: matchKick[ 4 ], time: new Date() }); @@ -86,18 +87,18 @@ export default class SquadRcon extends Rcon { } const matchSqCreated = decodedPacket.body.match( - /(.+) \(Steam ID: ([0-9a-f]{32})\) has created Squad (\d+) \(Squad Name: (.+)\) on (.+)/ + /(.+) \(Online IDs: EOS: ([0-9a-f]{32}) steam: (\d{17})\) has created Squad (\d+) \(Squad Name: (.+)\) on (.+)/ ); if (matchSqCreated) { Logger.verbose('SquadRcon', 2, `Matched Squad Created: ${decodedPacket.body}`); this.emit('SQUAD_CREATED', { time: new Date(), - playerName: matchSqCreated[1], - playerSteamID: matchSqCreated[2], - squadID: matchSqCreated[3], - squadName: matchSqCreated[4], - teamName: matchSqCreated[5] + playerName: matchSqCreated[ 1 ], + playerSteamID: matchSqCreated[ 3 ], + squadID: matchSqCreated[ 4 ], + squadName: matchSqCreated[ 5 ], + teamName: matchSqCreated[ 6 ] }); return; @@ -111,10 +112,10 @@ export default class SquadRcon extends Rcon { this.emit('PLAYER_BANNED', { raw: decodedPacket.body, - playerID: matchBan[1], - steamID: matchBan[2], - name: matchBan[3], - interval: matchBan[4], + playerID: matchBan[ 1 ], + steamID: matchBan[ 2 ], + name: matchBan[ 3 ], + interval: matchBan[ 4 ], time: new Date() }); } @@ -123,15 +124,15 @@ export default class SquadRcon extends Rcon { async getCurrentMap() { const response = await this.execute('ShowCurrentMap'); const match = response.match(/^Current level is (.*), layer is (.*)/); - return { level: match[1], layer: match[2] }; + return { level: match[ 1 ], layer: match[ 2 ] }; } async getNextMap() { const response = await this.execute('ShowNextMap'); const match = response.match(/^Next level is (.*), layer is (.*)/); return { - level: match[1] !== '' ? match[1] : null, - layer: match[2] !== 'To be voted' ? match[2] : null + level: match[ 1 ] !== '' ? match[ 1 ] : null, + layer: match[ 2 ] !== 'To be voted' ? match[ 2 ] : null }; } @@ -140,32 +141,24 @@ export default class SquadRcon extends Rcon { const players = []; - if(!response || response.length < 1) return players; + if (!response || response.length < 1) return players; for (const line of response.split('\n')) { const match = line.match( - /ID: ([0-9]+) \| SteamID: ([0-9a-f]{32}) \| Name: (.+) \| Team ID: ([0-9]+) \| Squad ID: ([0-9]+|N\/A) \| Is Leader: (True|False) \| Role: ([A-Za-z0-9_]*)\b/ + /ID: ([0-9]+) \| Online IDs: EOS: ([0-9a-f]{32}) steam: (\d{17}) \| Name: (.+) \| Team ID: ([0-9]+) \| Squad ID: ([0-9]+|N\/A) \| Is Leader: (True|False) \| Role: ([A-Za-z0-9_]*)\b/ ); if (!match) continue; - let steamID = this.eosIndex[ match[ 2 ] ] - - const persEosIdPlugin = server.plugins.find(p => p instanceof PersistentEOSIDtoSteamID) - if (persEosIdPlugin && !this.eosIndex[ match[ 2 ] ]) { - steamID = (await persEosIdPlugin.getByEOSID(match[ 2 ])).steamID - this.addIds(steamID, match[ 2 ]); - Logger.verbose("RCON", 1, `Getting SteamID for player: ${match[ 3 ]} from EOSID: ${match[ 2 ]} => ${steamID}`) - } - + server.rcon.addIds(match[ 3 ], match[ 2 ]); players.push({ - playerID: match[1], - steamID: steamID, - EOSID: match[2], - name: match[3], - teamID: match[4], - squadID: match[5] !== 'N/A' ? match[5] : null, - isLeader: match[6] === 'True', - role: match[7] + playerID: match[ 1 ], + EOSID: match[ 2 ], + steamID: match[ 3 ], + name: match[ 4 ], + teamID: match[ 5 ], + squadID: match[ 6 ] !== 'N/A' ? match[ 5 ] : null, + isLeader: match[ 7 ] === 'True', + role: match[ 8 ] }); } @@ -181,21 +174,21 @@ export default class SquadRcon extends Rcon { for (const line of responseSquad.split('\n')) { const match = line.match( - /ID: ([0-9]+) \| Name: (.+) \| Size: ([0-9]+) \| Locked: (True|False) \| Creator Name: (.+) \| Creator Steam ID: ([0-9a-f]{32})/ + /ID: ([0-9]+) \| Name: (.+) \| Size: ([0-9]+) \| Locked: (True|False) \| Creator Name: (.+) \| Creator Online IDs: EOS: ([0-9a-f]{32}) steam: (\d{17})/ ); const matchSide = line.match(/Team ID: (1|2) \((.+)\)/); if (matchSide) { - teamID = matchSide[1]; - teamName = matchSide[2]; + teamID = matchSide[ 1 ]; + teamName = matchSide[ 2 ]; } if (!match) continue; squads.push({ - squadID: match[1], - squadName: match[2], - size: match[3], - locked: match[4], - creatorName: match[5], - creatorSteamID: match[6], + squadID: match[ 1 ], + squadName: match[ 2 ], + size: match[ 3 ], + locked: match[ 4 ], + creatorName: match[ 5 ], + creatorSteamID: match[ 7 ], teamID: teamID, teamName: teamName }); From 4c7f55339ae256e02d38e2b31e09dbd59d50939a Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Tue, 12 Dec 2023 02:48:36 +0100 Subject: [PATCH 10/53] fix: typo matchUnpossessedAdminCam and removed unused dependency --- README.md | 12 ++++++ config.json | 12 +++--- squad-server/rcon.js | 87 ++++++++++++++++++++++---------------------- 3 files changed, 60 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index e1b720e3..ae1cdfe9 100644 --- a/README.md +++ b/README.md @@ -810,6 +810,18 @@ Grafana:
300000
+
+ PersistentEOSIDtoSteamID +

PersistentEOSIDtoSteamID

+

Stores into a DB every association of SteamID-EOSID

+

Options

+
  • database (Required)

    +
    Description
    +

    The Sequelize connector.

    +
    Default
    +
    sqlite
+
+
SeedingMode

SeedingMode

diff --git a/config.json b/config.json index b707e35e..30ef9e27 100644 --- a/config.json +++ b/config.json @@ -5,8 +5,6 @@ "queryPort": 27165, "rconPort": 21114, "rconPassword": "password", - "rconPassThrough": true, - "rconPassThroughPort": 8124, "logReaderMode": "tail", "logDir": "C:/path/to/squad/log/folder", "ftp": { @@ -42,11 +40,6 @@ "sqlite": "sqlite:database.sqlite" }, "plugins": [ - { - "plugin": "PersistentEOSIDtoSteamID", - "enabled": true, - "database": "sqlite" - }, { "plugin": "AutoKickUnassigned", "enabled": true, @@ -224,6 +217,11 @@ "broadcasts": [], "interval": 300000 }, + { + "plugin": "PersistentEOSIDtoSteamID", + "enabled": false, + "database": "sqlite" + }, { "plugin": "SeedingMode", "enabled": true, diff --git a/squad-server/rcon.js b/squad-server/rcon.js index 9ad8321c..39530387 100644 --- a/squad-server/rcon.js +++ b/squad-server/rcon.js @@ -1,6 +1,5 @@ import Logger from 'core/logger'; import Rcon from 'core/rcon'; -import PersistentEOSIDtoSteamID from './plugins/persistent-eosid-to-steamid.js'; export default class SquadRcon extends Rcon { processChatPacket(decodedPacket) { @@ -12,11 +11,11 @@ export default class SquadRcon extends Rcon { this.emit('CHAT_MESSAGE', { raw: decodedPacket.body, - chat: matchChat[ 1 ], - eosID: matchChat[ 2 ], - steamID: matchChat[ 3 ], - name: matchChat[ 4 ], - message: matchChat[ 5 ], + chat: matchChat[1], + eosID: matchChat[2], + steamID: matchChat[3], + name: matchChat[4], + message: matchChat[5], time: new Date() }); @@ -30,8 +29,8 @@ export default class SquadRcon extends Rcon { Logger.verbose('SquadRcon', 2, `Matched admin camera possessed: ${decodedPacket.body}`); this.emit('POSSESSED_ADMIN_CAMERA', { raw: decodedPacket.body, - steamID: matchPossessedAdminCam[ 2 ], - name: matchPossessedAdminCam[ 3 ], + steamID: matchPossessedAdminCam[2], + name: matchPossessedAdminCam[3], time: new Date() }); @@ -45,8 +44,8 @@ export default class SquadRcon extends Rcon { Logger.verbose('SquadRcon', 2, `Matched admin camera possessed: ${decodedPacket.body}`); this.emit('UNPOSSESSED_ADMIN_CAMERA', { raw: decodedPacket.body, - steamID: matchPossessedAdminCam[ 2 ], - name: matchPossessedAdminCam[ 3 ], + steamID: matchUnpossessedAdminCam[2], + name: matchUnpossessedAdminCam[3], time: new Date() }); @@ -61,8 +60,8 @@ export default class SquadRcon extends Rcon { this.emit('PLAYER_WARNED', { raw: decodedPacket.body, - name: matchWarn[ 1 ], - reason: matchWarn[ 2 ], + name: matchWarn[1], + reason: matchWarn[2], time: new Date() }); @@ -77,9 +76,9 @@ export default class SquadRcon extends Rcon { this.emit('PLAYER_KICKED', { raw: decodedPacket.body, - playerID: matchKick[ 1 ], - steamID: matchKick[ 3 ], - name: matchKick[ 4 ], + playerID: matchKick[1], + steamID: matchKick[3], + name: matchKick[4], time: new Date() }); @@ -94,11 +93,11 @@ export default class SquadRcon extends Rcon { this.emit('SQUAD_CREATED', { time: new Date(), - playerName: matchSqCreated[ 1 ], - playerSteamID: matchSqCreated[ 3 ], - squadID: matchSqCreated[ 4 ], - squadName: matchSqCreated[ 5 ], - teamName: matchSqCreated[ 6 ] + playerName: matchSqCreated[1], + playerSteamID: matchSqCreated[3], + squadID: matchSqCreated[4], + squadName: matchSqCreated[5], + teamName: matchSqCreated[6] }); return; @@ -112,10 +111,10 @@ export default class SquadRcon extends Rcon { this.emit('PLAYER_BANNED', { raw: decodedPacket.body, - playerID: matchBan[ 1 ], - steamID: matchBan[ 2 ], - name: matchBan[ 3 ], - interval: matchBan[ 4 ], + playerID: matchBan[1], + steamID: matchBan[2], + name: matchBan[3], + interval: matchBan[4], time: new Date() }); } @@ -124,15 +123,15 @@ export default class SquadRcon extends Rcon { async getCurrentMap() { const response = await this.execute('ShowCurrentMap'); const match = response.match(/^Current level is (.*), layer is (.*)/); - return { level: match[ 1 ], layer: match[ 2 ] }; + return { level: match[1], layer: match[2] }; } async getNextMap() { const response = await this.execute('ShowNextMap'); const match = response.match(/^Next level is (.*), layer is (.*)/); return { - level: match[ 1 ] !== '' ? match[ 1 ] : null, - layer: match[ 2 ] !== 'To be voted' ? match[ 2 ] : null + level: match[1] !== '' ? match[1] : null, + layer: match[2] !== 'To be voted' ? match[2] : null }; } @@ -149,16 +148,16 @@ export default class SquadRcon extends Rcon { ); if (!match) continue; - server.rcon.addIds(match[ 3 ], match[ 2 ]); + server.rcon.addIds(match[3], match[2]); players.push({ - playerID: match[ 1 ], - EOSID: match[ 2 ], - steamID: match[ 3 ], - name: match[ 4 ], - teamID: match[ 5 ], - squadID: match[ 6 ] !== 'N/A' ? match[ 5 ] : null, - isLeader: match[ 7 ] === 'True', - role: match[ 8 ] + playerID: match[1], + EOSID: match[2], + steamID: match[3], + name: match[4], + teamID: match[5], + squadID: match[6] !== 'N/A' ? match[5] : null, + isLeader: match[7] === 'True', + role: match[8] }); } @@ -178,17 +177,17 @@ export default class SquadRcon extends Rcon { ); const matchSide = line.match(/Team ID: (1|2) \((.+)\)/); if (matchSide) { - teamID = matchSide[ 1 ]; - teamName = matchSide[ 2 ]; + teamID = matchSide[1]; + teamName = matchSide[2]; } if (!match) continue; squads.push({ - squadID: match[ 1 ], - squadName: match[ 2 ], - size: match[ 3 ], - locked: match[ 4 ], - creatorName: match[ 5 ], - creatorSteamID: match[ 7 ], + squadID: match[1], + squadName: match[2], + size: match[3], + locked: match[4], + creatorName: match[5], + creatorSteamID: match[7], teamID: teamID, teamName: teamName }); From f18ea1ab66c82b723065b0d78f5945710cfafcdb Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Tue, 12 Dec 2023 02:50:06 +0100 Subject: [PATCH 11/53] chore: changes to make eslint happy --- core/rcon.js | 551 +++++++++++++++++++++++++++++---------------------- 1 file changed, 311 insertions(+), 240 deletions(-) diff --git a/core/rcon.js b/core/rcon.js index fdaa0585..34cb1853 100644 --- a/core/rcon.js +++ b/core/rcon.js @@ -1,21 +1,23 @@ -import { EventEmitter } from "node:events"; -import net from "node:net"; -import Logger from "./logger.js"; +/* eslint-disable */ +import { EventEmitter } from 'node:events'; +import net from 'node:net'; +import Logger from './logger.js'; import fs from 'fs'; import path from 'path'; -const RCON_LOG_FILEPATH = "RCON_RECEIVED_MESSAGES.log" +const RCON_LOG_FILEPATH = 'RCON_RECEIVED_MESSAGES.log'; export default class Rcon extends EventEmitter { constructor(options = {}) { super(); - for (const option of [ "host", "port", "password" ]) if (!(option in options)) throw new Error(`${option} must be specified.`); + for (const option of ['host', 'port', 'password']) + if (!(option in options)) throw new Error(`${option} must be specified.`); this.host = options.host; this.port = options.port; this.password = options.password; this.client = null; this.stream = new Buffer.alloc(0); this.type = { auth: 0x03, command: 0x02, response: 0x00, server: 0x01 }; - this.soh = { size: 7, id: 0, type: this.type.response, body: "" }; - this.responseString = { id: 0, body: "" }; + this.soh = { size: 7, id: 0, type: this.type.response, body: '' }; + this.responseString = { id: 0, body: '' }; this.connected = false; this.autoReconnect = false; this.autoReconnectDelay = options.autoReconnectDelay || 1000; @@ -31,78 +33,78 @@ export default class Rcon extends EventEmitter { this.passThroughChallenge = options.passThroughChallenge || options.password; this.dumpRconResponsesToFile = options.dumpRconResponsesToFile || false; this.rconClients = {}; - for (let i = 1; i <= this.passThroughMaxClients; i++) this.rconClients[ `${i}` ] = null; + for (let i = 1; i <= this.passThroughMaxClients; i++) this.rconClients[`${i}`] = null; this.ptServer = null; - this.steamIndex = { "76561198799344716": "00026e21ce3d43c792613bdbb6dec1ba" }; // example dtata - this.eosIndex = { "00026e21ce3d43c792613bdbb6dec1ba": "76561198799344716" }; // example dtata - - this.rotateLogFile(RCON_LOG_FILEPATH) + this.steamIndex = { '76561198799344716': '00026e21ce3d43c792613bdbb6dec1ba' }; // example dtata + this.eosIndex = { '00026e21ce3d43c792613bdbb6dec1ba': '76561198799344716' }; // example dtata + this.rotateLogFile(RCON_LOG_FILEPATH); } processChatPacket(decodedPacket) { console.log(decodedPacket.body); } // async connect() { return new Promise((resolve, reject) => { - if (this.client && this.connected && !this.client.destroyed) return reject(new Error("Rcon.connect() Rcon already connected.")); - this.removeAllListeners("server"); - this.removeAllListeners("auth"); - this.on("server", (pkt) => this.processChatPacket(pkt)); - this.once("auth", () => { - Logger.verbose("RCON", 1, `Connected to: ${this.host}:${this.port}`); + if (this.client && this.connected && !this.client.destroyed) + return reject(new Error('Rcon.connect() Rcon already connected.')); + this.removeAllListeners('server'); + this.removeAllListeners('auth'); + this.on('server', (pkt) => this.processChatPacket(pkt)); + this.once('auth', () => { + Logger.verbose('RCON', 1, `Connected to: ${this.host}:${this.port}`); clearTimeout(this.connectionRetry); this.connected = true; if (this.passThrough) this.createServer(); resolve(); }); - Logger.verbose("RCON", 1, `Connecting to: ${this.host}:${this.port}`); + Logger.verbose('RCON', 1, `Connecting to: ${this.host}:${this.port}`); this.connectionRetry = setTimeout(() => this.connect(), this.autoReconnectDelay); this.autoReconnect = true; this.client = net - .createConnection({ port: this.port, host: this.host }, () => this.#sendAuth()) - .on("data", (data) => this.#onData(data)) - .on("end", () => this.#onClose()) - .on("error", () => this.#onNetError()); + .createConnection({ port: this.port, host: this.host }, () => this.sendAuth()) + .on('data', (data) => this.onData(data)) + .on('end', () => this.onClose()) + .on('error', () => this.onNetError()); }).catch((error) => { - Logger.verbose("RCON", 1, `Rcon.connect() ${error}`); + Logger.verbose('RCON', 1, `Rcon.connect() ${error}`); }); } async disconnect() { return new Promise((resolve, reject) => { - Logger.verbose("RCON", 1, `Disconnecting from: ${this.host}:${this.port}`); + Logger.verbose('RCON', 1, `Disconnecting from: ${this.host}:${this.port}`); clearTimeout(this.connectionRetry); - this.removeAllListeners("server"); - this.removeAllListeners("auth"); + this.removeAllListeners('server'); + this.removeAllListeners('auth'); this.autoReconnect = false; this.client.end(); this.connected = false; this.closeServer(); resolve(); }).catch((error) => { - Logger.verbose("RCON", 1, `Rcon.disconnect() ${error}`); + Logger.verbose('RCON', 1, `Rcon.disconnect() ${error}`); }); } async execute(body) { let steamID = body.match(/\d{17}/); if (steamID) { - steamID = steamID[ 0 ] - body = body.replace(/\d{17}/, this.steamIndex[ steamID ]); + steamID = steamID[0]; + body = body.replace(/\d{17}/, this.steamIndex[steamID]); } return new Promise((resolve, reject) => { - if (!this.connected) return reject(new Error("Rcon not connected.")); - if (!this.client.writable) return reject(new Error("Unable to write to node:net socket")); + if (!this.connected) return reject(new Error('Rcon not connected.')); + if (!this.client.writable) return reject(new Error('Unable to write to node:net socket')); const string = String(body); const length = Buffer.from(string).length; - if (length > 4154) Logger.verbose("RCON", 1, `Error occurred. Oversize, "${length}" > 4154`); + if (length > 4154) Logger.verbose('RCON', 1, `Error occurred. Oversize, "${length}" > 4154`); else { const outputData = (data) => { clearTimeout(timeOut); resolve(data); }; const timedOut = () => { - console.warn("MISSED", listenerId) + console.warn('MISSED', listenerId); this.removeListener(listenerId, outputData); return reject(new Error(`Rcon response timed out`)); }; @@ -110,223 +112,288 @@ export default class Rcon extends EventEmitter { const listenerId = `response${this.msgId}`; const timeOut = setTimeout(timedOut, 10000); this.once(listenerId, outputData); - this.#send(string, this.msgId); + this.send(string, this.msgId); this.msgId++; } }).catch((error) => { - Logger.verbose("RCON", 1, `Rcon.execute() ${error}`); + Logger.verbose('RCON', 1, `Rcon.execute() ${error}`); }); } - #sendAuth() { - Logger.verbose("RCON", 1, `Sending Token to: ${this.host}:${this.port}`); - this.client.write(this.#encode(this.type.auth, 0, this.password).toString("binary"), "binary");//2147483647 + sendAuth() { + Logger.verbose('RCON', 1, `Sending Token to: ${this.host}:${this.port}`); + this.client.write(this.encode(this.type.auth, 0, this.password).toString('binary'), 'binary'); //2147483647 } - #send(body, id = 99) { - this.#write(this.type.command, id, body); - this.#write(this.type.command, id + 2); + send(body, id = 99) { + this.write(this.type.command, id, body); + this.write(this.type.command, id + 2); } - #write(type, id, body) { - Logger.verbose("RCON", 2, `Writing packet with type "${type}", id "${id}" and body "${body || ""}"`); - this.client.write(this.#encode(type, id, body).toString("binary"), "binary"); + write(type, id, body) { + Logger.verbose( + 'RCON', + 2, + `Writing packet with type "${type}", id "${id}" and body "${body || ''}"` + ); + this.client.write(this.encode(type, id, body).toString('binary'), 'binary'); } - #encode(type, id, body = "") { + encode(type, id, body = '') { const size = Buffer.byteLength(body) + 14; const buffer = new Buffer.alloc(size); buffer.writeInt32LE(size - 4, 0); buffer.writeInt32LE(id, 4); buffer.writeInt32LE(type, 8); - buffer.write(body, 12, size - 2, "utf8"); + buffer.write(body, 12, size - 2, 'utf8'); buffer.writeInt16LE(0, size - 2); return buffer; } - #onData(data) { - Logger.verbose("RCON", 4, `Got data: ${this.#bufToHexString(data)}`); - this.stream = Buffer.concat([ this.stream, data ], this.stream.byteLength + data.byteLength); + onData(data) { + Logger.verbose('RCON', 4, `Got data: ${this.bufToHexString(data)}`); + this.stream = Buffer.concat([this.stream, data], this.stream.byteLength + data.byteLength); while (this.stream.byteLength >= 7) { - const packet = this.#decode(); + const packet = this.decode(); if (!packet) break; - else Logger.verbose("RCON", 3, `Processing decoded packet: Size: ${packet.size}, ID: ${packet.id}, Type: ${packet.type}, Body: ${packet.body}`); - this.appendToFile(RCON_LOG_FILEPATH, packet.body) + else + Logger.verbose( + 'RCON', + 3, + `Processing decoded packet: Size: ${packet.size}, ID: ${packet.id}, Type: ${packet.type}, Body: ${packet.body}` + ); + this.appendToFile(RCON_LOG_FILEPATH, packet.body); if (packet.id > this.msgIdHigh) this.emit(`responseForward_1`, packet); - else if (packet.type === this.type.response) this.#onResponse(packet); - else if (packet.type === this.type.server) this.#onServer(packet); - else if (packet.type === this.type.command) this.emit("auth"); + else if (packet.type === this.type.response) this.onResponse(packet); + else if (packet.type === this.type.server) this.onServer(packet); + else if (packet.type === this.type.command) this.emit('auth'); } } - #onServer(packet) { - this.emit("server", packet); + onServer(packet) { + this.emit('server', packet); for (const client in this.rconClients) - if (this.rconClients[ client ]) { - this.emit(`serverForward_${this.rconClients[ client ].rconIdClient}`, packet.body); + if (this.rconClients[client]) { + this.emit(`serverForward_${this.rconClients[client].rconIdClient}`, packet.body); } } - #decode() { - if (this.stream[ 0 ] === 0 && this.stream[ 1 ] === 1 && this.stream[ 2 ] === 0 && this.stream[ 3 ] === 0 && this.stream[ 4 ] === 0 && this.stream[ 5 ] === 0 && this.stream[ 6 ] === 0) { + decode() { + if ( + this.stream[0] === 0 && + this.stream[1] === 1 && + this.stream[2] === 0 && + this.stream[3] === 0 && + this.stream[4] === 0 && + this.stream[5] === 0 && + this.stream[6] === 0 + ) { this.stream = this.stream.subarray(7); return this.soh; } const bufSize = this.stream.readInt32LE(0); - if (bufSize > 4154 || bufSize < 10) return this.#badPacket(); + if (bufSize > 4154 || bufSize < 10) return this.badPacket(); else if (bufSize <= this.stream.byteLength - 4 && this.stream.byteLength >= 12) { const bufId = this.stream.readInt32LE(4); const bufType = this.stream.readInt32LE(8); - if (this.stream[ bufSize + 2 ] !== 0 || this.stream[ bufSize + 3 ] !== 0 || bufId < 0 || bufType < 0 || bufType > 5) return this.#badPacket(); + if ( + this.stream[bufSize + 2] !== 0 || + this.stream[bufSize + 3] !== 0 || + bufId < 0 || + bufType < 0 || + bufType > 5 + ) + return this.badPacket(); else { - const response = { size: bufSize, id: bufId, type: bufType, body: this.stream.toString("utf8", 12, bufSize + 2) }; + const response = { + size: bufSize, + id: bufId, + type: bufType, + body: this.stream.toString('utf8', 12, bufSize + 2) + }; this.stream = this.stream.subarray(bufSize + 4); - if (response.body === "" && this.stream[ 0 ] === 0 && this.stream[ 1 ] === 1 && this.stream[ 2 ] === 0 && this.stream[ 3 ] === 0 && this.stream[ 4 ] === 0 && this.stream[ 5 ] === 0 && this.stream[ 6 ] === 0) { + if ( + response.body === '' && + this.stream[0] === 0 && + this.stream[1] === 1 && + this.stream[2] === 0 && + this.stream[3] === 0 && + this.stream[4] === 0 && + this.stream[5] === 0 && + this.stream[6] === 0 + ) { this.stream = this.stream.subarray(7); - response.body = ""; + response.body = ''; } return response; } } else return null; } - #onResponse(packet) { - if (packet.body === "") { + onResponse(packet) { + if (packet.body === '') { this.emit(`response${this.responseString.id - 2}`, this.responseString.body); - this.responseString.body = ""; - } else if (!packet.body.includes("")) { + this.responseString.body = ''; + } else if (!packet.body.includes('')) { this.responseString.body = this.responseString.body += packet.body; this.responseString.id = packet.id; - } else this.#badPacket(); - } - #badPacket() { - Logger.verbose("RCON", 1, `Bad packet, clearing: ${this.bufToHexString(this.stream)} Pending string: ${this.responseString}`); + } else this.badPacket(); + } + badPacket() { + Logger.verbose( + 'RCON', + 1, + `Bad packet, clearing: ${this.bufToHexString(this.stream)} Pending string: ${ + this.responseString + }` + ); this.stream = Buffer.alloc(0); - this.responseString = ""; + this.responseString = ''; return null; } - #onClose() { - Logger.verbose("RCON", 1, `Socket closed`); - this.#cleanUp(); + onClose() { + Logger.verbose('RCON', 1, `Socket closed`); + this.cleanUp(); } - #onNetError(error) { - Logger.verbose("RCON", 1, `node:net error:`, error); - this.emit("RCON_ERROR", error); - this.#cleanUp(); + onNetError(error) { + Logger.verbose('RCON', 1, `node:net error:`, error); + this.emit('RCON_ERROR', error); + this.cleanUp(); } - #cleanUp() { + cleanUp() { this.closeServer(); this.connected = false; this.removeAllListeners(); clearTimeout(this.connectionRetry); if (this.autoReconnect) { - Logger.verbose("RCON", 1, `Sleeping ${this.autoReconnectDelay}ms before reconnecting`); + Logger.verbose('RCON', 1, `Sleeping ${this.autoReconnectDelay}ms before reconnecting`); this.connectionRetry = setTimeout(() => this.connect(), this.autoReconnectDelay); } } createServer() { - this.ptServer = net.createServer((client) => this.#onNewClient(client)); + this.ptServer = net.createServer((client) => this.onNewClient(client)); this.ptServer.maxConnections = this.passThroughMaxClients; - this.ptServer.on("error", (error) => this.#onSerErr(error)); - this.ptServer.on("drop", () => Logger.verbose("RCON", 1, `Pass-through Server: Max Clients Reached (${this.passThroughMaxClients}) rejecting new connection`)); - this.ptServer.listen(this.passThroughPort, () => Logger.verbose("RCON", 1, `Pass-through Server: Listening on port ${this.passThroughPort}`)); + this.ptServer.on('error', (error) => this.onSerErr(error)); + this.ptServer.on('drop', () => + Logger.verbose( + 'RCON', + 1, + `Pass-through Server: Max Clients Reached (${this.passThroughMaxClients}) rejecting new connection` + ) + ); + this.ptServer.listen(this.passThroughPort, () => + Logger.verbose('RCON', 1, `Pass-through Server: Listening on port ${this.passThroughPort}`) + ); } closeServer() { - for (const client in this.rconClients) if (this.rconClients[ client ]) this.rconClients[ client ].end(); + for (const client in this.rconClients) + if (this.rconClients[client]) this.rconClients[client].end(); if (!this.ptServer) return; - this.ptServer.close(() => this.#onServerClose()); + this.ptServer.close(() => this.onServerClose()); } - #onServerClose() { + onServerClose() { if (!this.ptServer) return; this.ptServer.removeAllListeners(); this.ptServer = null; - Logger.verbose("RCON", 1, `Pass-through Server: Closed`); + Logger.verbose('RCON', 1, `Pass-through Server: Closed`); } - #onNewClient(client) { + onNewClient(client) { client.setTimeout(this.passThroughTimeOut); - client.on("end", () => this.#onClientEnd(client)); - client.on("error", () => this.#onClientEnd(client)); - client.on("timeout", () => this.#onClientTimeOut(client)); - client.on("data", (data) => this.#onClientData(client, data)); - Logger.verbose("RCON", 1, `Pass-through Server: Client connecting`); + client.on('end', () => this.onClientEnd(client)); + client.on('error', () => this.onClientEnd(client)); + client.on('timeout', () => this.onClientTimeOut(client)); + client.on('data', (data) => this.onClientData(client, data)); + Logger.verbose('RCON', 1, `Pass-through Server: Client connecting`); } - #onSerErr(error) { + onSerErr(error) { this.closeServer(); - Logger.verbose("RCON", 1, `Pass-through Server: ${error}`); + Logger.verbose('RCON', 1, `Pass-through Server: ${error}`); } - #onClientEnd(client) { + onClientEnd(client) { if (!client.rconIdClient) return; this.removeAllListeners(`serverForward_${client.rconIdClient}`); this.removeAllListeners(`responseForward_${client.rconIdClient}`); - this.rconClients[ `${client.rconIdClient}` ] = null; - Logger.verbose("RCON", 1, `Pass-through Server: Client-${client.rconIdClient} Disconnected`); + this.rconClients[`${client.rconIdClient}`] = null; + Logger.verbose('RCON', 1, `Pass-through Server: Client-${client.rconIdClient} Disconnected`); } - #onClientTimeOut(client) { + onClientTimeOut(client) { client.end(); - Logger.verbose("RCON", 1, `Pass-through Server: Client-${client.rconIdClient} Timed Out`); + Logger.verbose('RCON', 1, `Pass-through Server: Client-${client.rconIdClient} Timed Out`); } - #onClientData(client, data) { + onClientData(client, data) { if (!client.rconStream) client.rconStream = new Buffer.alloc(0); - client.rconStream = Buffer.concat([ client.rconStream, data ], client.rconStream.byteLength + data.byteLength); + client.rconStream = Buffer.concat( + [client.rconStream, data], + client.rconStream.byteLength + data.byteLength + ); while (client.rconStream.byteLength >= 4) { - const packet = this.#decodeClient(client); + const packet = this.decodeClient(client); if (!packet) break; - if (!client.rconHasAuthed) this.#authClient(client, packet); + if (!client.rconHasAuthed) this.authClient(client, packet); else { - if (!client.rconWheel || client.rconWheel > 20) client.rconWheel = 0; else client.rconWheel++; - client.rconIdQueueNEW[ `${client.rconWheel}` ] = packet.id + client.rconIdQueueNEW[`${client.rconWheel}`] = packet.id; - const encoded = this.#encode(packet.type, this.specialId + client.rconWheel, this.#steamToEosClient(packet.body)); //////////////////////////////////////////////// - this.client.write(encoded.toString("binary"), "binary"); - // this.client.write(this.#encode(packet.type, this.specialId * client.rconIdClient).toString("binary"), "binary") + const encoded = this.encode( + packet.type, + this.specialId + client.rconWheel, + this.steamToEosClient(packet.body) + ); //////////////////////////////////////////////// + this.client.write(encoded.toString('binary'), 'binary'); + // this.client.write(this.encode(packet.type, this.specialId * client.rconIdClient).toString("binary"), "binary") } } } - #decodeClient(client) { + decodeClient(client) { const bufSize = client.rconStream.readInt32LE(0); if (bufSize <= client.rconStream.byteLength - 4) { const response = { size: bufSize, id: client.rconStream.readInt32LE(4), type: client.rconStream.readInt32LE(8), - body: client.rconStream.toString("utf8", 12, bufSize + 2), + body: client.rconStream.toString('utf8', 12, bufSize + 2) }; client.rconStream = client.rconStream.subarray(bufSize + 4); return response; } else return null; } - #authClient(client, packet) { + authClient(client, packet) { if (packet.body !== this.passThroughChallenge) { client.end(); - Logger.verbose("RCON", 1, `Pass-through Server: Client [Rejected] Password not matched`); + Logger.verbose('RCON', 1, `Pass-through Server: Client [Rejected] Password not matched`); } else { client.rconHasAuthed = true; - client.rconIdQueueNEW = {} + client.rconIdQueueNEW = {}; for (let i = 1; i <= this.passThroughMaxClients; i++) { - if (this.rconClients[ `${i}` ] === null) { + if (this.rconClients[`${i}`] === null) { client.rconIdClient = i; - this.rconClients[ `${i}` ] = client; + this.rconClients[`${i}`] = client; break; } } - this.on(`serverForward_${client.rconIdClient}`, (body) => client.write(this.#encode(1, 0, this.#eosToSteam(body)).toString("binary"), "binary")); - this.on(`responseForward_${client.rconIdClient}`, (packet) => this.#onForward(client, packet)); - client.write(this.#encode(0, packet.id)); - client.write(this.#encode(2, packet.id)); - Logger.verbose("RCON", 1, `Pass-through Server: Client-${client.rconIdClient} Connected`); + this.on(`serverForward_${client.rconIdClient}`, (body) => + client.write(this.encode(1, 0, this.eosToSteam(body)).toString('binary'), 'binary') + ); + this.on(`responseForward_${client.rconIdClient}`, (packet) => this.onForward(client, packet)); + client.write(this.encode(0, packet.id)); + client.write(this.encode(2, packet.id)); + Logger.verbose('RCON', 1, `Pass-through Server: Client-${client.rconIdClient} Connected`); } } - #onForward(client, packet) { - if (packet.body !== "" && packet.body !== "") { - - const int = packet.id - this.specialId + onForward(client, packet) { + if (packet.body !== '' && packet.body !== '') { + const int = packet.id - this.specialId; //console.log(client.rconIdQueueNEW);////////////////////////////////////////////////////////////////////////////////////////// - client.write(this.#encode(packet.type, client.rconIdQueueNEW[ int ], this.#eosToSteam(packet.body)).toString("binary"), "binary"); - } else if (packet.body != "") { - const int = packet.id - this.specialId - client.write(this.#encode(0, client.rconIdQueueNEW[ int ]).toString("binary"), "binary"); - client.write(this.#encodeSpecial(client.rconIdQueueNEW[ int ]).toString("binary"), "binary"); + client.write( + this.encode(packet.type, client.rconIdQueueNEW[int], this.eosToSteam(packet.body)).toString( + 'binary' + ), + 'binary' + ); + } else if (packet.body != '') { + const int = packet.id - this.specialId; + client.write(this.encode(0, client.rconIdQueueNEW[int]).toString('binary'), 'binary'); + client.write(this.encodeSpecial(client.rconIdQueueNEW[int]).toString('binary'), 'binary'); } } - #encodeSpecial(id) { + encodeSpecial(id) { const buffer = new Buffer.alloc(21); buffer.writeInt32LE(10, 0); buffer.writeInt32LE(id, 4); @@ -334,8 +401,8 @@ export default class Rcon extends EventEmitter { buffer.writeInt32LE(1, 15); return buffer; } - #bufToHexString(buf) { - return buf.toString("hex").match(/../g).join(" "); + bufToHexString(buf) { + return buf.toString('hex').match(/../g).join(' '); } async warn(steamID, message) { this.execute(`AdminWarn "${steamID}" ${message}`); @@ -348,34 +415,35 @@ export default class Rcon extends EventEmitter { } addIds(steamId, eosId) { - this.steamIndex[ steamId ] = eosId; // { "76561198799344716": "00026e21ce3d43c792613bdbb6dec1ba" }; - this.eosIndex[ eosId ] = steamId; + this.steamIndex[steamId] = eosId; // { "76561198799344716": "00026e21ce3d43c792613bdbb6dec1ba" }; + this.eosIndex[eosId] = steamId; } removeIds(eosId) { // clean up ids on leave } - #steamToEosClient(body) { + steamToEosClient(body) { //assume client does not send more than 1 steamId per msg const m = body.match(/[0-9]{17}/); - if (m && m[ 1 ] in this.steamIndex) return body.replaceAll(`${m[ 0 ]}`, this.steamIndex[ m[ 0 ] ]); + if (m && m[1] in this.steamIndex) return body.replaceAll(`${m[0]}`, this.steamIndex[m[0]]); return body; } - #eosToSteam(body) { + eosToSteam(body) { //split body to lines for matching (1 steamId per line) - const lines = body.split("\n"); + const lines = body.split('\n'); const nBody = []; - for (let line of lines) nBody.push(this.#matchRcon(line)); - return nBody.join("\n"); + for (let line of lines) nBody.push(this.matchRcon(line)); + return nBody.join('\n'); } - #matchRcon(line) { + matchRcon(line) { console.warn(line); - for (const r of this.defs) { + for (const r of defs) { const match = line.match(r.regex); - if (match && match.groups.eosId in this.eosIndex) return r.rep(line, this.eosIndex[ match.groups.eosId ], match.groups.eosId); + if (match && match.groups.eosId in this.eosIndex) + return r.rep(line, this.eosIndex[match.groups.eosId], match.groups.eosId); } return line; } @@ -388,7 +456,7 @@ export default class Rcon extends EventEmitter { fs.mkdirSync(dir, { recursive: true }); } - fs.appendFile(filePath, content + "\n", (err) => { + fs.appendFile(filePath, content + '\n', (err) => { if (err) throw err; }); } @@ -404,98 +472,101 @@ export default class Rcon extends EventEmitter { fs.renameSync(logFile, newFile); } } +} +const defs = [ + //strict matching to avoid 'name as steamId errors' + { + regex: + /^ID: [0-9]+ \| SteamID: (?[0-9a-f]{32}) \| Name: .+ \| Team ID: (1|2|N\/A) \| Squad ID: ([0-9]+|N\/A) \| Is Leader: (True|False|N\/A) \| Role: .+$/, + rep: (line, steamId, eosId) => { + return line.replace(` SteamID: ${eosId} `, ` SteamID: ${steamId} `); + } + }, + { + regex: + /^ID: (?[0-9]+) \| SteamID: (?[0-9a-f]{32}) \| Since Disconnect: (?.+) \| Name: (?.+)$/, + rep: (line, steamId, eosId) => { + return line.replace(` SteamID: ${eosId} `, ` SteamID: ${steamId} `); + } + }, + { + regex: + /^ID: (?[0-9]+) \| Name: (?.+) \| Size: (?[0-9]) \| Locked: (?True|False) \| Creator Name: (?.+) \| Creator Steam ID: (?[0-9]{17})/, + rep: (line, steamId, eosId) => { + return line.replace(` Creator Steam ID: ${eosId}`, ` Creator Steam ID: ${steamId}`); + } + }, + { + regex: /^Forced team change for player (?[0-9]+). \[steamid=(?[0-9a-f]{32})] (.+)/, + rep: (line, steamId, eosId) => { + return line.replace(` [steamid=${eosId}] `, ` [steamid=${steamId}] `); + } + }, - defs = [ - //strict matching to avoid 'name as steamId errors' - { - regex: /^ID: [0-9]+ \| SteamID: (?[0-9a-f]{32}) \| Name: .+ \| Team ID: (1|2|N\/A) \| Squad ID: ([0-9]+|N\/A) \| Is Leader: (True|False|N\/A) \| Role: .+$/, - rep: (line, steamId, eosId) => { - return line.replace(` SteamID: ${eosId} `, ` SteamID: ${steamId} `); - }, - }, - { - regex: /^ID: (?[0-9]+) \| SteamID: (?[0-9a-f]{32}) \| Since Disconnect: (?.+) \| Name: (?.+)$/, - rep: (line, steamId, eosId) => { - return line.replace(` SteamID: ${eosId} `, ` SteamID: ${steamId} `); - }, - }, - { - regex: /^ID: (?[0-9]+) \| Name: (?.+) \| Size: (?[0-9]) \| Locked: (?True|False) \| Creator Name: (?.+) \| Creator Steam ID: (?[0-9]{17})/, - rep: (line, steamId, eosId) => { - return line.replace(` Creator Steam ID: ${eosId}`, ` Creator Steam ID: ${steamId}`); - }, - }, - { - regex: /^Forced team change for player (?[0-9]+). \[steamid=(?[0-9a-f]{32})] (.+)/, - rep: (line, steamId, eosId) => { - return line.replace(` [steamid=${eosId}] `, ` [steamid=${steamId}] `); - }, - }, - - { - regex: /^Could not find player (?[0-9a-f]{32})/, - rep: (line, steamId, eosId) => { - return line.replace(`Could not find player ${eosId}`, `Could not find player ${steamId}`); - }, - }, + { + regex: /^Could not find player (?[0-9a-f]{32})/, + rep: (line, steamId, eosId) => { + return line.replace(`Could not find player ${eosId}`, `Could not find player ${steamId}`); + } + }, - { - regex: /^\[ChatAll] \[SteamID:(?[0-9a-f]{32})] ((?.+) : (?.+))/, - rep: (line, steamId, eosId) => { - return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); - }, - }, - { - regex: /^\[ChatTeam] \[SteamID:(?[0-9a-f]{32})] ((?.+) : (?.+))/, - rep: (line, steamId, eosId) => { - return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); - }, - }, - { - regex: /^\[ChatSquad] \[SteamID:(?[0-9a-f]{32})] ((?.+) : (?.+))/, - rep: (line, steamId, eosId) => { - return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); - }, - }, - { - regex: /^\[ChatAdmin] \[SteamID:(?[0-9a-f]{32})] ((?.+) : (?.+))/, - rep: (line, steamId, eosId) => { - return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); - }, - }, - { - regex: /^(?.+) \(Steam ID: (?[0-9a-f]{32})\) has created Squad (?[0-9]+) \(Squad Name: (?.+)\) on (?.+)/, - rep: (line, steamId, eosId) => { - return line.replace(` (Steam ID: ${eosId}) `, ` (Steam ID: ${steamId}) `); - }, - }, - { - regex: /^Kicked player (?[0-9]+). \[steamid=(?[0-9a-f]{32})] (?.+)/, - rep: (line, steamId, eosId) => { - return line.replace(` [steamid=${eosId}] `, ` [steamid=${steamId}] `); - }, - }, + { + regex: /^\[ChatAll] \[SteamID:(?[0-9a-f]{32})] ((?.+) : (?.+))/, + rep: (line, steamId, eosId) => { + return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); + } + }, + { + regex: /^\[ChatTeam] \[SteamID:(?[0-9a-f]{32})] ((?.+) : (?.+))/, + rep: (line, steamId, eosId) => { + return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); + } + }, + { + regex: /^\[ChatSquad] \[SteamID:(?[0-9a-f]{32})] ((?.+) : (?.+))/, + rep: (line, steamId, eosId) => { + return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); + } + }, + { + regex: /^\[ChatAdmin] \[SteamID:(?[0-9a-f]{32})] ((?.+) : (?.+))/, + rep: (line, steamId, eosId) => { + return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); + } + }, + { + regex: + /^(?.+) \(Steam ID: (?[0-9a-f]{32})\) has created Squad (?[0-9]+) \(Squad Name: (?.+)\) on (?.+)/, + rep: (line, steamId, eosId) => { + return line.replace(` (Steam ID: ${eosId}) `, ` (Steam ID: ${steamId}) `); + } + }, + { + regex: /^Kicked player (?[0-9]+). \[steamid=(?[0-9a-f]{32})] (?.+)/, + rep: (line, steamId, eosId) => { + return line.replace(` [steamid=${eosId}] `, ` [steamid=${steamId}] `); + } + }, - { - regex: /^ERROR: Unable to find player with name or id \((?[0-9a-f]{32})\)$/, - rep: (line, steamId, eosId) => { - return line.replace(`name or id (${eosId})`, `name or id (${steamId})`); - }, - }, - { - regex: /^\[SteamID:(?[0-9a-f]{32})] (?.+) has possessed admin camera./, - rep: (line, steamId, eosId) => { - return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); - }, - }, - { - regex: /^\[SteamID:(?[0-9a-f]{32})] (?.+) has unpossessed admin camera./, - rep: (line, steamId, eosId) => { - return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); - }, - }, - ]; -} + { + regex: /^ERROR: Unable to find player with name or id \((?[0-9a-f]{32})\)$/, + rep: (line, steamId, eosId) => { + return line.replace(`name or id (${eosId})`, `name or id (${steamId})`); + } + }, + { + regex: /^\[SteamID:(?[0-9a-f]{32})] (?.+) has possessed admin camera./, + rep: (line, steamId, eosId) => { + return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); + } + }, + { + regex: /^\[SteamID:(?[0-9a-f]{32})] (?.+) has unpossessed admin camera./, + rep: (line, steamId, eosId) => { + return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); + } + } +]; //////////////////////////////////////////////////ALL BELOW IS FOR STANDALONE TESTING/RUNNING // const Logger = { From 971c32f247d55ac83951bf510930e1cf45908be9 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Tue, 12 Dec 2023 02:52:06 +0100 Subject: [PATCH 12/53] chore: removed unused dependencies, linting --- squad-server/index.js | 95 ++++++++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 42 deletions(-) diff --git a/squad-server/index.js b/squad-server/index.js index 36854639..d33c7914 100644 --- a/squad-server/index.js +++ b/squad-server/index.js @@ -1,7 +1,6 @@ import EventEmitter from 'events'; import axios from 'axios'; -import Gamedig from 'gamedig'; import Logger from 'core/logger'; import { SQUADJS_API_DOMAIN } from 'core/constants'; @@ -19,7 +18,7 @@ export default class SquadServer extends EventEmitter { constructor(options = {}) { super(); - for (const option of [ 'host', 'queryPort' ]) + for (const option of ['host']) if (!(option in options)) throw new Error(`${option} must be specified.`); this.id = options.id; @@ -107,29 +106,29 @@ export default class SquadServer extends EventEmitter { const command = data.message.match(/!([^ ]+) ?(.*)/); if (command) - this.emit(`CHAT_COMMAND:${command[ 1 ].toLowerCase()}`, { + this.emit(`CHAT_COMMAND:${command[1].toLowerCase()}`, { ...data, - message: command[ 2 ].trim() + message: command[2].trim() }); }); this.rcon.on('POSSESSED_ADMIN_CAMERA', async (data) => { data.player = await this.getPlayerBySteamID(data.steamID); - this.adminsInAdminCam[ data.steamID ] = data.time; + this.adminsInAdminCam[data.steamID] = data.time; this.emit('POSSESSED_ADMIN_CAMERA', data); }); this.rcon.on('UNPOSSESSED_ADMIN_CAMERA', async (data) => { data.player = await this.getPlayerBySteamID(data.steamID); - if (this.adminsInAdminCam[ data.steamID ]) { - data.duration = data.time.getTime() - this.adminsInAdminCam[ data.steamID ].getTime(); + if (this.adminsInAdminCam[data.steamID]) { + data.duration = data.time.getTime() - this.adminsInAdminCam[data.steamID].getTime(); } else { data.duration = 0; } - delete this.adminsInAdminCam[ data.steamID ]; + delete this.adminsInAdminCam[data.steamID]; this.emit('UNPOSSESSED_ADMIN_CAMERA', data); }); @@ -210,11 +209,15 @@ export default class SquadServer extends EventEmitter { }); this.logParser.on('PLAYER_CONNECTED', async (data) => { - Logger.verbose("SquadServer", 1, `Player connected ${data.playerSuffix} - SteamID: ${data.steamID} - EOSID: ${data.eosID}`) + Logger.verbose( + 'SquadServer', + 1, + `Player connected ${data.playerSuffix} - SteamID: ${data.steamID} - EOSID: ${data.eosID}` + ); - this.rcon.addIds(data.steamID, data.eosID) + this.rcon.addIds(data.steamID, data.eosID); - data.player = await this.getPlayerBySteamID(data.steamID); + data.player = await this.getPlayerByEOSID(data.eosID); if (data.player) data.player.suffix = data.playerSuffix; delete data.steamID; @@ -224,7 +227,7 @@ export default class SquadServer extends EventEmitter { }); this.logParser.on('PLAYER_DISCONNECTED', async (data) => { - data.player = await this.getPlayerBySteamID(data.steamID); + data.player = await this.getPlayerByEOSID(data.playerEOSID); delete data.steamID; @@ -233,22 +236,29 @@ export default class SquadServer extends EventEmitter { this.logParser.on('PLAYER_DAMAGED', async (data) => { data.victim = await this.getPlayerByName(data.victimName); - data.attacker = await this.getPlayerByName(data.attackerName); + data.attacker = await this.getPlayerByEOSID(data.attackerEOSID); + + if (!data.attacker.playercontroller) data.attacker.playercontroller = data.attackerController; + + if (data.victim && data.attacker) { + if (!data.victim.playercontroller) data.victim.playercontroller = data.attackerController; - if (data.victim && data.attacker) data.teamkill = data.victim.teamID === data.attacker.teamID && data.victim.steamID !== data.attacker.steamID; + } delete data.victimName; delete data.attackerName; + console.log('player damage', data); + this.emit('PLAYER_DAMAGED', data); }); this.logParser.on('PLAYER_WOUNDED', async (data) => { data.victim = await this.getPlayerByName(data.victimName); - data.attacker = await this.getPlayerByName(data.attackerName); + data.attacker = await this.getPlayerByEOSID(data.attackerEOSID); if (!data.attacker) data.attacker = await this.getPlayerByController(data.attackerPlayerController); @@ -266,7 +276,7 @@ export default class SquadServer extends EventEmitter { this.logParser.on('PLAYER_DIED', async (data) => { data.victim = await this.getPlayerByName(data.victimName); - data.attacker = await this.getPlayerByName(data.attackerName); + data.attacker = await this.getPlayerByEOSID(data.attackerEOSID); if (!data.attacker) data.attacker = await this.getPlayerByController(data.attackerPlayerController); @@ -282,9 +292,9 @@ export default class SquadServer extends EventEmitter { }); this.logParser.on('PLAYER_REVIVED', async (data) => { - data.victim = await this.getPlayerByName(data.victimName); - data.attacker = await this.getPlayerByName(data.attackerName); - data.reviver = await this.getPlayerByName(data.reviverName); + data.victim = await this.getPlayerByEOSID(data.victimEOSID); + data.attacker = await this.getPlayerByEOSID(data.attackerEOSID); + data.reviver = await this.getPlayerByEOSID(data.reviverEOSID); delete data.victimName; delete data.attackerName; @@ -294,7 +304,7 @@ export default class SquadServer extends EventEmitter { }); this.logParser.on('PLAYER_POSSESS', async (data) => { - data.player = await this.getPlayerByNameSuffix(data.playerSuffix); + data.player = await this.getPlayerByEOSID(data.playerEOSID); if (data.player) data.player.possessClassname = data.possessClassname; delete data.playerSuffix; @@ -303,7 +313,7 @@ export default class SquadServer extends EventEmitter { }); this.logParser.on('PLAYER_UNPOSSESS', async (data) => { - data.player = await this.getPlayerByNameSuffix(data.playerSuffix); + data.player = await this.getPlayerByEOSID(data.playerEOSID); delete data.playerSuffix; @@ -319,8 +329,8 @@ export default class SquadServer extends EventEmitter { }); this.logParser.on('CLIENT_EXTERNAL_ACCOUNT_INFO', (data) => { - this.rcon.addIds(data.steamID, data.eosID) - }) + this.rcon.addIds(data.steamID, data.eosID); + }); // this.logParser.on('CLIENT_CONNECTED', (data) => { // Logger.verbose("SquadServer", 1, `Client connected. Connection: ${data.connection} - SteamID: ${data.steamID}`) // }) @@ -349,12 +359,12 @@ export default class SquadServer extends EventEmitter { } getAdminPermsBySteamID(steamID) { - return this.admins[ steamID ]; + return this.admins[steamID]; } getAdminsWithPermission(perm) { const ret = []; - for (const [ steamID, perms ] of Object.entries(this.admins)) { + for (const [steamID, perms] of Object.entries(this.admins)) { if (perm in perms) ret.push(steamID); } return ret; @@ -372,16 +382,16 @@ export default class SquadServer extends EventEmitter { try { const oldPlayerInfo = {}; for (const player of this.players) { - oldPlayerInfo[ player.steamID ] = player; + oldPlayerInfo[player.steamID] = player; } const players = []; for (const player of await this.rcon.getListPlayers(this)) players.push({ - ...oldPlayerInfo[ player.steamID ], + ...oldPlayerInfo[player.steamID], ...player, - playercontroller: this.logParser.eventStore.players[ player.steamID ] - ? this.logParser.eventStore.players[ player.steamID ].controller + playercontroller: this.logParser.eventStore.players[player.steamID] + ? this.logParser.eventStore.players[player.steamID].controller : null, squad: await this.getSquadByID(player.teamID, player.squadID) }); @@ -389,17 +399,17 @@ export default class SquadServer extends EventEmitter { this.players = players; for (const player of this.players) { - if (typeof oldPlayerInfo[ player.steamID ] === 'undefined') continue; - if (player.teamID !== oldPlayerInfo[ player.steamID ].teamID) + if (typeof oldPlayerInfo[player.steamID] === 'undefined') continue; + if (player.teamID !== oldPlayerInfo[player.steamID].teamID) this.emit('PLAYER_TEAM_CHANGE', { player: player, - oldTeamID: oldPlayerInfo[ player.steamID ].teamID, + oldTeamID: oldPlayerInfo[player.steamID].teamID, newTeamID: player.teamID }); - if (player.squadID !== oldPlayerInfo[ player.steamID ].squadID) + if (player.squadID !== oldPlayerInfo[player.steamID].squadID) this.emit('PLAYER_SQUAD_CHANGE', { player: player, - oldSquadID: oldPlayerInfo[ player.steamID ].squadID, + oldSquadID: oldPlayerInfo[player.steamID].squadID, newSquadID: player.squadID }); } @@ -478,9 +488,9 @@ export default class SquadServer extends EventEmitter { // }); const rawData = await this.rcon.execute(`ShowServerInfo`); - Logger.verbose("SquadServer", 3, `A2S raw data`, rawData) + Logger.verbose('SquadServer', 3, `A2S raw data`, rawData); const data = JSON.parse(rawData); - Logger.verbose("SquadServer", 2, `A2S data`, JSON.data) + Logger.verbose('SquadServer', 2, `A2S data`, JSON.data); // Logger.verbose("SquadServer", 1, `A2S data`, JSON.stringify(data, null, 2)) const info = { @@ -498,8 +508,8 @@ export default class SquadServer extends EventEmitter { currentLayer: data.MapName_s, nextLayer: data.NextLayer_s, - teamOne: data.TeamOne_s.replace(new RegExp(data.MapName_s, "i"), ''), - teamTwo: data.TeamTwo_s.replace(new RegExp(data.MapName_s, "i"), ''), + teamOne: data.TeamOne_s.replace(new RegExp(data.MapName_s, 'i'), ''), + teamTwo: data.TeamTwo_s.replace(new RegExp(data.MapName_s, 'i'), ''), matchTimeout: parseFloat(data.MatchTimeout_d), gameVersion: data.GameVersion_s @@ -537,7 +547,7 @@ export default class SquadServer extends EventEmitter { if (!forceUpdate) { matches = this.players.filter(condition); - if (matches.length === 1) return matches[ 0 ]; + if (matches.length === 1) return matches[0]; if (!retry) return null; } @@ -545,7 +555,7 @@ export default class SquadServer extends EventEmitter { await this.updatePlayerList(); matches = this.players.filter(condition); - if (matches.length === 1) return matches[ 0 ]; + if (matches.length === 1) return matches[0]; return null; } @@ -555,7 +565,7 @@ export default class SquadServer extends EventEmitter { if (!forceUpdate) { matches = this.squads.filter(condition); - if (matches.length === 1) return matches[ 0 ]; + if (matches.length === 1) return matches[0]; if (!retry) return null; } @@ -563,7 +573,7 @@ export default class SquadServer extends EventEmitter { await this.updateSquadList(); matches = this.squads.filter(condition); - if (matches.length === 1) return matches[ 0 ]; + if (matches.length === 1) return matches[0]; return null; } @@ -578,6 +588,7 @@ export default class SquadServer extends EventEmitter { async getPlayerBySteamID(steamID, forceUpdate) { return this.getPlayerByCondition((player) => player.steamID === steamID, forceUpdate); } + async getPlayerByEOSID(eosID, forceUpdate) { return this.getPlayerByCondition((player) => player.EOSID === eosID, forceUpdate); } From 2905b229a7651408114117ec612ac61c085a6a02 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Tue, 12 Dec 2023 02:54:56 +0100 Subject: [PATCH 13/53] chore: defaultEnabled: true, linting --- config.json | 2 +- .../plugins/persistent-eosid-to-steamid.js | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/config.json b/config.json index 30ef9e27..deb3b184 100644 --- a/config.json +++ b/config.json @@ -219,7 +219,7 @@ }, { "plugin": "PersistentEOSIDtoSteamID", - "enabled": false, + "enabled": true, "database": "sqlite" }, { diff --git a/squad-server/plugins/persistent-eosid-to-steamid.js b/squad-server/plugins/persistent-eosid-to-steamid.js index 0fa05988..b83458df 100644 --- a/squad-server/plugins/persistent-eosid-to-steamid.js +++ b/squad-server/plugins/persistent-eosid-to-steamid.js @@ -6,11 +6,11 @@ const { DataTypes } = Sequelize; export default class PersistentEOSIDtoSteamID extends BasePlugin { static get description() { - return "Stores into a DB every association of SteamID-EOSID"; + return 'Stores into a DB every association of SteamID-EOSID'; } static get defaultEnabled() { - return false; + return true; } static get optionsSpecification() { @@ -37,7 +37,7 @@ export default class PersistentEOSIDtoSteamID extends BasePlugin { primaryKey: true }, eosID: { - type: DataTypes.STRING, + type: DataTypes.STRING } }, { @@ -50,13 +50,13 @@ export default class PersistentEOSIDtoSteamID extends BasePlugin { } createModel(name, schema) { - this.models[ name ] = this.options.database.define(`EOS_${name}`, schema, { + this.models[name] = this.options.database.define(`EOS_${name}`, schema, { timestamps: false, indexes: [ { unique: true, - fields: [ 'eosID' ] - }, + fields: ['eosID'] + } ] }); } @@ -67,7 +67,7 @@ export default class PersistentEOSIDtoSteamID extends BasePlugin { async mount() { this.server.on('PLAYER_CONNECTED', this.onPlayerConnected); - this.verbose(1, 'Mounted') + this.verbose(1, 'Mounted'); } async unmount() { @@ -82,10 +82,10 @@ export default class PersistentEOSIDtoSteamID extends BasePlugin { } async getByEOSID(eosID) { - return await this.models.SteamIDtoEOSID.findOne({ where: { eosID: eosID } }) + return await this.models.SteamIDtoEOSID.findOne({ where: { eosID: eosID } }); } async getBySteamID(steamID) { - return await this.models.SteamIDtoEOSID.findOne({ where: { steamID: steamID } }) + return await this.models.SteamIDtoEOSID.findOne({ where: { steamID: steamID } }); } } From 978ad35b3ad8b593a92cde1aaaf92abb2041e465 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Tue, 12 Dec 2023 02:55:51 +0100 Subject: [PATCH 14/53] refactor: updated log parsers for Squad V7 --- .../log-parser/adding-client-connection.js | 16 ++++----- .../check-permission-resolve-eosid.js | 24 +++++++------- .../client-external-account-info.js | 14 ++++---- squad-server/log-parser/client-login.js | 12 +++---- squad-server/log-parser/index.js | 22 +++++++------ squad-server/log-parser/join-request.js | 25 +++++++------- squad-server/log-parser/login-request.js | 28 ++++++++-------- squad-server/log-parser/player-connected.js | 33 +++++++------------ squad-server/log-parser/player-damaged.js | 7 ++-- squad-server/log-parser/player-died.js | 8 +++-- .../log-parser/player-disconnected.js | 26 ++++++++------- .../log-parser/player-join-succeeded.js | 28 ++++++++++++++++ squad-server/log-parser/player-possess.js | 6 ++-- squad-server/log-parser/player-revived.js | 9 +++-- squad-server/log-parser/player-un-possess.js | 6 ++-- squad-server/log-parser/player-wounded.js | 6 ++-- .../log-parser/playercontroller-connected.js | 10 +++--- .../log-parser/sending-auth-result.js | 12 +++---- 18 files changed, 165 insertions(+), 127 deletions(-) create mode 100644 squad-server/log-parser/player-join-succeeded.js diff --git a/squad-server/log-parser/adding-client-connection.js b/squad-server/log-parser/adding-client-connection.js index b4011e94..968cf8b9 100644 --- a/squad-server/log-parser/adding-client-connection.js +++ b/squad-server/log-parser/adding-client-connection.js @@ -1,20 +1,20 @@ export default { regex: - /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: AddClientConnection: Added client connection: \[UNetConnection\] RemoteAddr: ([\d\.]+):[0-9]+, Name: (EOSIpNetConnection_[0-9]+), Driver: GameNetDriver (EOSNetDriver_[0-9]+), IsServer: YES, PC: NULL, Owner: NULL, UniqueId: INVALID/, + /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: AddClientConnection: Added client connection: \[UNetConnection\] RemoteAddr: ([\d.]+):[0-9]+, Name: (EOSIpNetConnection_[0-9]+), Driver: GameNetDriver (EOSNetDriver_[0-9]+), IsServer: YES, PC: NULL, Owner: NULL, UniqueId: INVALID/, onMatch: (args, logParser) => { const data = { - raw: args[ 0 ], - time: args[ 1 ], - chainID: args[ 2 ], + raw: args[0], + time: args[1], + chainID: args[2], // steamID: args[ 3 ], - ip: args[ 3 ], - connection: args[ 4 ], - driver: args[ 5 ] + ip: args[3], + connection: args[4], + driver: args[5] }; /* This is Called when unreal engine adds a client connection First Step in Adding a Player to server */ - logParser.eventStore[ 'last-connection' ] = data; + logParser.eventStore['last-connection'] = data; logParser.emit('ADDING_CLIENT_CONNECTION', data); } }; diff --git a/squad-server/log-parser/check-permission-resolve-eosid.js b/squad-server/log-parser/check-permission-resolve-eosid.js index 853200e6..6579be52 100644 --- a/squad-server/log-parser/check-permission-resolve-eosid.js +++ b/squad-server/log-parser/check-permission-resolve-eosid.js @@ -1,15 +1,15 @@ export default { - regex: - /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadCommon: SQCommonStatics Check Permissions, UniqueId:([\da-f]+)$/, - onMatch: (args, logParser) => { - const data = { - raw: args[ 0 ], - time: args[ 1 ], - chainID: +args[ 2 ], - eosID: args[ 3 ], - }; + regex: + /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadCommon: SQCommonStatics Check Permissions, UniqueId:([\da-f]+)$/, + onMatch: (args, logParser) => { + const data = { + raw: args[0], + time: args[1], + chainID: +args[2], + eosID: args[3] + }; - logParser.eventStore.joinRequests[ data.chainID ].eosID = data.eosID; - logParser.emit('RESOLVED_EOS_ID', { ...logParser.eventStore.joinRequests[ data.chainID ] }); - } + logParser.eventStore.joinRequests[data.chainID].eosID = data.eosID; + logParser.emit('RESOLVED_EOS_ID', { ...logParser.eventStore.joinRequests[data.chainID] }); + } }; diff --git a/squad-server/log-parser/client-external-account-info.js b/squad-server/log-parser/client-external-account-info.js index 693016c3..38ff55f2 100644 --- a/squad-server/log-parser/client-external-account-info.js +++ b/squad-server/log-parser/client-external-account-info.js @@ -3,19 +3,19 @@ export default { /^\[([0-9.:-]+)]\[([ 0-9]+)]LogEOS: Verbose: \[LogEOSConnect] FConnectClient::CacheExternalAccountInfo - ProductUserId: (?[0-9a-f]{32}), AccountType: (\d), AccountId: (?[0-9]{17}), DisplayName: /, onMatch: (args, logParser) => { const data = { - raw: args[ 0 ], - time: args[ 1 ], - chainID: args[ 2 ], + raw: args[0], + time: args[1], + chainID: args[2], eosID: args.groups.eosId, - steamID: args.groups.steamId, + steamID: args.groups.steamId }; - logParser.eventStore.players[ data.steamID ] = { + logParser.eventStore.players[data.steamID] = { eosID: data.eosID, steamID: data.steamID }; - logParser.eventStore.playersEOS[ data.eosID ] = logParser.eventStore.players[ data.steamID ] - + logParser.eventStore.playersEOS[data.eosID] = logParser.eventStore.players[data.steamID]; + logParser.emit('CLIENT_EXTERNAL_ACCOUNT_INFO', data); } }; diff --git a/squad-server/log-parser/client-login.js b/squad-server/log-parser/client-login.js index 7f2406f8..e98456c4 100644 --- a/squad-server/log-parser/client-login.js +++ b/squad-server/log-parser/client-login.js @@ -3,18 +3,18 @@ export default { /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: Login: NewPlayer: EOSIpNetConnection \/Engine\/Transient\.(EOSIpNetConnection_[0-9]+)/, onMatch: (args, logParser) => { const data = { - raw: args[ 0 ], - time: args[ 1 ], - chainID: args[ 2 ], - connection: args[ 3 ] + raw: args[0], + time: args[1], + chainID: args[2], + connection: args[3] }; /* This is Called when a player begins the Login process We use this to get a SteamID into playerConnected. 2nd Step in player connected path */ - logParser.eventStore.joinRequests[ data.chainID ].connection = data.connection; - delete logParser.eventStore.clients[ args[ 3 ] ]; + logParser.eventStore.joinRequests[data.chainID].connection = data.connection; + delete logParser.eventStore.clients[args[3]]; logParser.emit('CLIENT_LOGIN', data); } }; diff --git a/squad-server/log-parser/index.js b/squad-server/log-parser/index.js index 2223b338..fa9b3737 100644 --- a/squad-server/log-parser/index.js +++ b/squad-server/log-parser/index.js @@ -19,11 +19,12 @@ import ServerTickRate from './server-tick-rate.js'; import AddingClientConnection from './adding-client-connection.js'; import ClientLogin from './client-login.js'; import PendingConnectionDestroyed from './pending-connection-destroyed.js'; -import clientExternalAccountInfo from './client-external-account-info.js'; -import sendingAuthResult from './sending-auth-result.js'; -import loginRequest from './login-request.js'; -import joinRequest from './join-request.js'; -import checkPermissionResolveEosid from './check-permission-resolve-eosid.js'; +import ClientExternalAccountInfo from './client-external-account-info.js'; +import SendingAuthResult from './sending-auth-result.js'; +import LoginRequest from './login-request.js'; +import JoinRequest from './join-request.js'; +import PlayerJoinSucceeded from './player-join-succeeded.js'; +import CheckPermissionResolveEosid from './check-permission-resolve-eosid.js'; export default class SquadLogParser extends LogParser { constructor(options) { super('SquadGame.log', options); @@ -50,11 +51,12 @@ export default class SquadLogParser extends LogParser { AddingClientConnection, ClientLogin, PendingConnectionDestroyed, - clientExternalAccountInfo, - sendingAuthResult, - loginRequest, - joinRequest, - checkPermissionResolveEosid, + ClientExternalAccountInfo, + SendingAuthResult, + LoginRequest, + JoinRequest, + PlayerJoinSucceeded, + CheckPermissionResolveEosid ]; } } diff --git a/squad-server/log-parser/join-request.js b/squad-server/log-parser/join-request.js index 3fdfd3fe..5cd2e249 100644 --- a/squad-server/log-parser/join-request.js +++ b/squad-server/log-parser/join-request.js @@ -1,16 +1,15 @@ export default { - regex: - /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: Join request: .+\?Name=(.+)\?SplitscreenCount=\d$/, - onMatch: (args, logParser) => { - const data = { - raw: args[ 0 ], - time: args[ 1 ], - chainID: +args[ 2 ], - suffix: args[ 3 ], - }; + regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: Join request: .+\?Name=(.+)\?SplitscreenCount=\d$/, + onMatch: (args, logParser) => { + const data = { + raw: args[0], + time: args[1], + chainID: +args[2], + suffix: args[3] + }; - logParser.eventStore.joinRequests[ data.chainID ] = data; - // console.log(logParser.eventStore.loginRequests[ data.chainID ]) - logParser.emit('CLIENT_JOIN_REQUEST', data); - } + logParser.eventStore.joinRequests[data.chainID] = data; + // console.log(logParser.eventStore.loginRequests[ data.chainID ]) + logParser.emit('CLIENT_JOIN_REQUEST', data); + } }; diff --git a/squad-server/log-parser/login-request.js b/squad-server/log-parser/login-request.js index e1cc8226..1ea963f2 100644 --- a/squad-server/log-parser/login-request.js +++ b/squad-server/log-parser/login-request.js @@ -1,17 +1,17 @@ export default { - regex: - /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: Login request: \?Name=(.+) userId: RedpointEOS:([\da-f]{32}) platform: RedpointEOS/, - onMatch: (args, logParser) => { - const data = { - raw: args[ 0 ], - time: args[ 1 ], - chainID: +args[ 2 ], - suffix: args[ 3 ], - eosID: args[ 4 ] - }; + regex: + /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: Login request: \?Name=(.+) userId: RedpointEOS:([\da-f]{32}) platform: RedpointEOS/, + onMatch: (args, logParser) => { + const data = { + raw: args[0], + time: args[1], + chainID: +args[2], + suffix: args[3], + eosID: args[4] + }; - // logParser.eventStore.loginRequests[ data.chainID ] = data; - // console.log(logParser.eventStore.loginRequests[ data.chainID ]) - logParser.emit('CLIENT_LOGIN_REQUEST', data); - } + // logParser.eventStore.loginRequests[ data.chainID ] = data; + // console.log(logParser.eventStore.loginRequests[ data.chainID ]) + logParser.emit('CLIENT_LOGIN_REQUEST', data); + } }; diff --git a/squad-server/log-parser/player-connected.js b/squad-server/log-parser/player-connected.js index 9e768c29..662e8726 100644 --- a/squad-server/log-parser/player-connected.js +++ b/squad-server/log-parser/player-connected.js @@ -1,31 +1,20 @@ export default { - regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: Join succeeded: (.+)/, + regex: + /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: PostLogin: NewPlayer: BP_PlayerController_C .+PersistentLevel\.([^\s]+) \(IP: ([\d.]+) \| Online IDs: EOS: ([0-9a-f]{32}) steam: (\d+)\)/, onMatch: (args, logParser) => { const data = { - raw: args[ 0 ], - time: args[ 1 ], - chainID: +args[ 2 ], - playerSuffix: args[ 3 ] + raw: args[0], + time: args[1], + chainID: +args[2], + ip: args[4], + eosID: args[5], + steamID: args[6] }; - // console.log(`ChainID: ${data.chainID}`, logParser.eventStore.joinRequests[ data.chainID ]); - const joinRequestsData = { ...logParser.eventStore.joinRequests[ data.chainID ] }; - // console.log('loginRequestData', loginRequestData) + const joinRequestData = logParser.eventStore.joinRequests[+args[2]]; + data.connection = joinRequestData.connection; + data.playerSuffix = joinRequestData.suffix; - data.eosID = joinRequestsData.eosID - data.controller = joinRequestsData.controller - data.steamID = `${logParser.eventStore.connectionIdToSteamID.get(joinRequestsData.connection)}` - - logParser.eventStore.connectionIdToSteamID.delete(joinRequestsData.connection) - - delete logParser.eventStore.joinRequests[ +data.chainID ]; - - // Handle Reconnecting players - if (logParser.eventStore.disconnected[ data.steamID ]) { - delete logParser.eventStore.disconnected[ data.steamID ]; - } logParser.emit('PLAYER_CONNECTED', data); - // logParser.eventStore.players[ data.steamID ].suffix = data.playerSuffix - // logParser.eventStore.players[ data.steamID ].controller = data.controller } }; diff --git a/squad-server/log-parser/player-damaged.js b/squad-server/log-parser/player-damaged.js index b1057e2c..7d4a7775 100644 --- a/squad-server/log-parser/player-damaged.js +++ b/squad-server/log-parser/player-damaged.js @@ -1,6 +1,6 @@ export default { regex: - /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: Player:(.+) ActualDamage=([0-9.]+) from (.+) caused by ([A-z_0-9-]+)_C/, + /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: Player:(.+) ActualDamage=([0-9.]+) from (.+) \(Online IDs: EOS: ([0-9a-f]{32}) steam: (\d{17}) \| Player Controller ID: ([^ ]+)\)caused by ([A-z_0-9-]+)_C/, onMatch: (args, logParser) => { const data = { raw: args[0], @@ -9,7 +9,10 @@ export default { victimName: args[3], damage: parseFloat(args[4]), attackerName: args[5], - weapon: args[6] + attackerEOSID: args[6], + attackerSteamID: args[7], + attackerController: args[8], + weapon: args[9] }; logParser.eventStore.session[args[3]] = data; diff --git a/squad-server/log-parser/player-died.js b/squad-server/log-parser/player-died.js index c9f15201..4a9e40f4 100644 --- a/squad-server/log-parser/player-died.js +++ b/squad-server/log-parser/player-died.js @@ -1,6 +1,6 @@ export default { regex: - /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQSoldier::)?Die\(\): Player:(.+) KillingDamage=(?:-)*([0-9.]+) from ([A-z_0-9]+) caused by ([A-z_0-9-]+)_C/, + /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQSoldier::)?Die\(\): Player:(.+) KillingDamage=(?:-)*([0-9.]+) from ([A-z_0-9]+) \(Online IDs: EOS: ([\w\d]{32}) steam: (\d{17}) \| Contoller ID: ([\w\d]+)\) caused by ([A-z_0-9-]+)_C/, onMatch: (args, logParser) => { const data = { ...logParser.eventStore.session[args[3]], @@ -11,11 +11,15 @@ export default { victimName: args[3], damage: parseFloat(args[4]), attackerPlayerController: args[5], - weapon: args[6] + attackerEOSID: args[6], + attackerSteamID: args[7], + weapon: args[9] }; logParser.eventStore.session[args[3]] = data; + console.log('Die', data); + logParser.emit('PLAYER_DIED', data); } }; diff --git a/squad-server/log-parser/player-disconnected.js b/squad-server/log-parser/player-disconnected.js index 9fd86e90..8302c078 100644 --- a/squad-server/log-parser/player-disconnected.js +++ b/squad-server/log-parser/player-disconnected.js @@ -1,16 +1,18 @@ export default { regex: - /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: UChannel::Close: Sending CloseBunch\. ChIndex == [0-9]+\. Name: \[UChannel\] ChIndex: [0-9]+, Closing: [0-9]+ \[UNetConnection\] RemoteAddr: ([0-9a-f]{32}):[0-9]+, Name: SteamNetConnection_[0-9]+, Driver: GameNetDriver SteamNetDriver_[0-9]+, IsServer: YES, PC: ([^ ]+PlayerController_C_[0-9]+), Owner: [^ ]+PlayerController_C_[0-9]+/, - onMatch: (args, logParser) => { - const data = { - raw: args[0], - time: args[1], - chainID: args[2], - steamID: args[3], - playerController: args[4] - }; + /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: UChannel::Close: Sending CloseBunch\. ChIndex == [0-9]+\. Name: \[UChannel\] ChIndex: [0-9]+, Closing: [0-9]+ \[UNetConnection\] RemoteAddr: ([\d.]+):[\d]+, Name: EOSIpNetConnection_[0-9]+, Driver: GameNetDriver EOSNetDriver_[0-9]+, IsServer: YES, PC: ([^ ]+PlayerController_C_[0-9]+), Owner: [^ ]+PlayerController_C_[0-9]+, UniqueId: RedpointEOS:([\d\w]+)/, + onMatch: (args, logParser) => { + const data = { + raw: args[0], + time: args[1], + chainID: args[2], + ip: args[3], + playerController: args[4], + playerEOSID: args[5] + }; - logParser.eventStore.disconnected[data.steamID] = true; - logParser.emit('PLAYER_DISCONNECTED', data); - } + logParser.eventStore.disconnected[data.steamID] = true; + + logParser.emit('PLAYER_DISCONNECTED', data); + } }; diff --git a/squad-server/log-parser/player-join-succeeded.js b/squad-server/log-parser/player-join-succeeded.js new file mode 100644 index 00000000..cae145a2 --- /dev/null +++ b/squad-server/log-parser/player-join-succeeded.js @@ -0,0 +1,28 @@ +export default { + regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: Join succeeded: (.+)/, + onMatch: (args, logParser) => { + const data = { + raw: args[0], + time: args[1], + chainID: +args[2], + playerSuffix: args[3] + }; + + const joinRequestsData = { ...logParser.eventStore.joinRequests[data.chainID] }; + + data.eosID = joinRequestsData.eosID; + data.controller = joinRequestsData.controller; + data.steamID = `${logParser.eventStore.connectionIdToSteamID.get(joinRequestsData.connection)}`; + + logParser.eventStore.connectionIdToSteamID.delete(joinRequestsData.connection); + + delete logParser.eventStore.joinRequests[+data.chainID]; + + // Handle Reconnecting players + if (logParser.eventStore.disconnected[data.steamID]) { + delete logParser.eventStore.disconnected[data.steamID]; + } + + logParser.emit('JOIN_SUCCEEDED', data); + } +}; diff --git a/squad-server/log-parser/player-possess.js b/squad-server/log-parser/player-possess.js index 12ceb84c..f6302eb9 100644 --- a/squad-server/log-parser/player-possess.js +++ b/squad-server/log-parser/player-possess.js @@ -1,13 +1,15 @@ export default { regex: - /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQPlayerController::)?OnPossess\(\): PC=(.+) Pawn=([A-z0-9_]+)_C/, + /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQPlayerController::)?OnPossess\(\): PC=(.+) \(Online IDs: EOS: ([\w\d]{32}) steam: (\d{17})\) Pawn=([A-z0-9_]+)_C/, onMatch: (args, logParser) => { const data = { raw: args[0], time: args[1], chainID: args[2], playerSuffix: args[3], - possessClassname: args[4], + playerEOSID: args[4], + playerSteamID: args[5], + possessClassname: args[6], pawn: args[5] }; diff --git a/squad-server/log-parser/player-revived.js b/squad-server/log-parser/player-revived.js index bb412ee4..819a2d7d 100644 --- a/squad-server/log-parser/player-revived.js +++ b/squad-server/log-parser/player-revived.js @@ -1,6 +1,7 @@ export default { // the names are currently the wrong way around in these logs - regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: (.+) has revived (.+)\./, + regex: + /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: (.+) \(Online IDs: EOS: ([0-9a-f]{32}) steam: (\d{17})\) has revived (.+) \(Online IDs: EOS: ([0-9a-f]{32}) steam: (\d{17})\)\./, onMatch: (args, logParser) => { const data = { ...logParser.eventStore.session[args[3]], @@ -8,7 +9,11 @@ export default { time: args[1], chainID: args[2], reviverName: args[3], - victimName: args[4] + reviverEOSID: args[4], + reviverSteamID: args[5], + victimName: args[6], + victimEOSID: args[7], + victimSteamID: args[8] }; logParser.emit('PLAYER_REVIVED', data); diff --git a/squad-server/log-parser/player-un-possess.js b/squad-server/log-parser/player-un-possess.js index d2b9bc40..9df7e00d 100644 --- a/squad-server/log-parser/player-un-possess.js +++ b/squad-server/log-parser/player-un-possess.js @@ -1,14 +1,16 @@ export default { regex: - /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQPlayerController::)?OnUnPossess\(\): PC=(.+)/, + /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQPlayerController::)?OnUnPossess\(\): PC=(.+) \(Online IDs: EOS: ([\w\d]{32}) steam: (\d{17})\)/, onMatch: (args, logParser) => { const data = { raw: args[0], time: args[1], chainID: args[2], playerSuffix: args[3], + playerEOSID: args[4], + playerSteamID: args[5], switchPossess: - args[3] in logParser.eventStore.session && logParser.eventStore.session[args[3]] === args[2] + args[4] in logParser.eventStore.session && logParser.eventStore.session[args[4]] === args[2] }; delete logParser.eventStore.session[args[3]]; diff --git a/squad-server/log-parser/player-wounded.js b/squad-server/log-parser/player-wounded.js index 9a832dc0..ef61b26f 100644 --- a/squad-server/log-parser/player-wounded.js +++ b/squad-server/log-parser/player-wounded.js @@ -1,6 +1,6 @@ export default { regex: - /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQSoldier::)?Wound\(\): Player:(.+) KillingDamage=(?:-)*([0-9.]+) from ([A-z_0-9]+) caused by ([A-z_0-9-]+)_C/, + /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQSoldier::)?Wound\(\): Player:(.+) KillingDamage=(?:-)*([0-9.]+) from ([A-z_0-9]+) \(Online IDs: EOS: ([\w\d]{32}) steam: (\d{17}) \| Controller ID: ([\w\d]+)\) caused by ([A-z_0-9-]+)_C/, onMatch: (args, logParser) => { const data = { ...logParser.eventStore.session[args[3]], @@ -10,7 +10,9 @@ export default { victimName: args[3], damage: parseFloat(args[4]), attackerPlayerController: args[5], - weapon: args[6] + attackerEOSID: args[6], + attackerSteamID: args[7], + weapon: args[9] }; logParser.eventStore.session[args[3]] = data; diff --git a/squad-server/log-parser/playercontroller-connected.js b/squad-server/log-parser/playercontroller-connected.js index 0e8040de..68451abc 100644 --- a/squad-server/log-parser/playercontroller-connected.js +++ b/squad-server/log-parser/playercontroller-connected.js @@ -3,13 +3,13 @@ export default { /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: PostLogin: NewPlayer: BP_PlayerController_C .+(BP_PlayerController_C_[0-9]+)/, onMatch: (args, logParser) => { const data = { - raw: args[ 0 ], - time: args[ 1 ], - chainID: +args[ 2 ], - controller: args[ 3 ] + raw: args[0], + time: args[1], + chainID: +args[2], + controller: args[3] }; - logParser.eventStore.joinRequests[ data.chainID ].controller = data.controller; + logParser.eventStore.joinRequests[data.chainID].controller = data.controller; logParser.emit('PLAYER_CONTROLLER_CONNECTED', data); } }; diff --git a/squad-server/log-parser/sending-auth-result.js b/squad-server/log-parser/sending-auth-result.js index ce0ca8c8..c8f75d1a 100644 --- a/squad-server/log-parser/sending-auth-result.js +++ b/squad-server/log-parser/sending-auth-result.js @@ -2,20 +2,20 @@ export default { regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogOnline: STEAM: AUTH HANDLER: Sending auth result to user (\d{17}) with flag success\? 1/, onMatch: (args, logParser) => { - if (!logParser.eventStore[ 'last-connection' ]) return; + if (!logParser.eventStore['last-connection']) return; const data = { - ...logParser.eventStore[ 'last-connection' ], - steamID: args[ 3 ] + ...logParser.eventStore['last-connection'], + steamID: args[3] }; /* This is Called when unreal engine adds a client connection First Step in Adding a Player to server */ - logParser.eventStore.clients[ data.connection ] = data.steamID; - logParser.eventStore.connectionIdToSteamID.set(data.connection, data.steamID) + logParser.eventStore.clients[data.connection] = data.steamID; + logParser.eventStore.connectionIdToSteamID.set(data.connection, data.steamID); logParser.emit('CLIENT_CONNECTED', data); - delete logParser.eventStore[ 'last-connection' ]; + delete logParser.eventStore['last-connection']; } }; From ee09fc3d63ebac2f395a7d13222f51978644febe Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Tue, 12 Dec 2023 03:01:20 +0100 Subject: [PATCH 15/53] chore: conflict resolution --- squad-server/rcon.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/squad-server/rcon.js b/squad-server/rcon.js index 39530387..a8d467ba 100644 --- a/squad-server/rcon.js +++ b/squad-server/rcon.js @@ -171,6 +171,8 @@ export default class SquadRcon extends Rcon { let teamName; let teamID; + if (!responseSquad || responseSquad.length < 1) return squads; + for (const line of responseSquad.split('\n')) { const match = line.match( /ID: ([0-9]+) \| Name: (.+) \| Size: ([0-9]+) \| Locked: (True|False) \| Creator Name: (.+) \| Creator Online IDs: EOS: ([0-9a-f]{32}) steam: (\d{17})/ From 140bee22ea1faf0b8d32b30aa5ea336bbe49a581 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Tue, 12 Dec 2023 18:46:50 +0100 Subject: [PATCH 16/53] fix: handle Teams not loaded in server info --- squad-server/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/squad-server/index.js b/squad-server/index.js index d33c7914..31c49b0e 100644 --- a/squad-server/index.js +++ b/squad-server/index.js @@ -508,8 +508,8 @@ export default class SquadServer extends EventEmitter { currentLayer: data.MapName_s, nextLayer: data.NextLayer_s, - teamOne: data.TeamOne_s.replace(new RegExp(data.MapName_s, 'i'), ''), - teamTwo: data.TeamTwo_s.replace(new RegExp(data.MapName_s, 'i'), ''), + teamOne: data.TeamOne_s?.replace(new RegExp(data.MapName_s, 'i'), '') || '', + teamTwo: data.TeamTwo_s?.replace(new RegExp(data.MapName_s, 'i'), '') || '', matchTimeout: parseFloat(data.MatchTimeout_d), gameVersion: data.GameVersion_s From 4ebb14cffd2bc168cefd7ca76aafbd46ae8b1f3e Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Tue, 12 Dec 2023 18:49:45 +0100 Subject: [PATCH 17/53] chore: rcon lines rewriting for legacy support --- core/rcon.js | 90 +++++++++++++++++++++++++++------------------------- 1 file changed, 47 insertions(+), 43 deletions(-) diff --git a/core/rcon.js b/core/rcon.js index 34cb1853..1fe0bb07 100644 --- a/core/rcon.js +++ b/core/rcon.js @@ -439,11 +439,15 @@ export default class Rcon extends EventEmitter { } matchRcon(line) { - console.warn(line); for (const r of defs) { const match = line.match(r.regex); - if (match && match.groups.eosId in this.eosIndex) - return r.rep(line, this.eosIndex[match.groups.eosId], match.groups.eosId); + if (match && (match.groups.eosId in this.eosIndex || match.groups.steamId)) { + return r.rep( + line, + match.groups.steamId || this.eosIndex[match.groups.eosId], + match.groups.eosId + ); + } } return line; } @@ -477,29 +481,44 @@ const defs = [ //strict matching to avoid 'name as steamId errors' { regex: - /^ID: [0-9]+ \| SteamID: (?[0-9a-f]{32}) \| Name: .+ \| Team ID: (1|2|N\/A) \| Squad ID: ([0-9]+|N\/A) \| Is Leader: (True|False|N\/A) \| Role: .+$/, + /^ID: [0-9]+ \| Online IDs: EOS: (?[0-9a-f]{32}) steam: (?\d{17}) \| Name: .+ \| Team ID: (1|2|N\/A) \| Squad ID: ([0-9]+|N\/A) \| Is Leader: (True|False|N\/A) \| Role: .+$/, rep: (line, steamId, eosId) => { - return line.replace(` SteamID: ${eosId} `, ` SteamID: ${steamId} `); + return line.replace( + /\| Online IDs: EOS: [\w\d]{32} steam: \d{17} \|/, + `| SteamID: ${steamId} |` + ); } }, { regex: - /^ID: (?[0-9]+) \| SteamID: (?[0-9a-f]{32}) \| Since Disconnect: (?.+) \| Name: (?.+)$/, + /^ID: (?[0-9]+) \| Online IDs: EOS: (?[0-9a-f]{32}) steam: (?\d{17}) \| Since Disconnect: (?.+) \| Name: (?.+)$/, rep: (line, steamId, eosId) => { - return line.replace(` SteamID: ${eosId} `, ` SteamID: ${steamId} `); + return line.replace( + /Online IDs: EOS: (?[0-9a-f]{32}) steam: (?\d{17})/, + `SteamID: ${steamId}` + ); } }, { regex: - /^ID: (?[0-9]+) \| Name: (?.+) \| Size: (?[0-9]) \| Locked: (?True|False) \| Creator Name: (?.+) \| Creator Steam ID: (?[0-9]{17})/, + /^ID: (?[0-9]+) \| Name: (?.+) \| Size: (?[0-9]) \| Locked: (?True|False) \| Creator Name: (?.+) \| Creator Online IDs: EOS: (?[\d\w]{32}) steam: (?\d{17})/, rep: (line, steamId, eosId) => { - return line.replace(` Creator Steam ID: ${eosId}`, ` Creator Steam ID: ${steamId}`); + console.log(line, steamId, eosId); + const ret = line.replace( + /\| Creator Online IDs: EOS: [\w\d]{32} steam: \d{17}/, + `| Creator Steam ID: ${steamId}` + ); + return ret; } }, { - regex: /^Forced team change for player (?[0-9]+). \[steamid=(?[0-9a-f]{32})] (.+)/, + regex: + /^Forced team change for player (?[0-9]+). \[Online IDs= EOS: (?[0-9a-f]{32}) steam: (?\d{17})] (.+)/, rep: (line, steamId, eosId) => { - return line.replace(` [steamid=${eosId}] `, ` [steamid=${steamId}] `); + return line.replace( + /Online IDs= EOS: (?[0-9a-f]{32}) steam: (?\d{17})/, + `steamid=${steamId}` + ); } }, @@ -511,40 +530,30 @@ const defs = [ }, { - regex: /^\[ChatAll] \[SteamID:(?[0-9a-f]{32})] ((?.+) : (?.+))/, - rep: (line, steamId, eosId) => { - return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); - } - }, - { - regex: /^\[ChatTeam] \[SteamID:(?[0-9a-f]{32})] ((?.+) : (?.+))/, - rep: (line, steamId, eosId) => { - return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); - } - }, - { - regex: /^\[ChatSquad] \[SteamID:(?[0-9a-f]{32})] ((?.+) : (?.+))/, - rep: (line, steamId, eosId) => { - return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); - } - }, - { - regex: /^\[ChatAdmin] \[SteamID:(?[0-9a-f]{32})] ((?.+) : (?.+))/, + regex: + /^\[Chat(All|Team|Squad|Admin)] \[Online IDs:EOS: (?[\d\w]{32}) steam: (?\d{17})] (?.+) : (?.+)/, rep: (line, steamId, eosId) => { - return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); + return line.replace(/Online IDs:EOS: [\d\w]{32} steam: \d{17}/, `SteamID:${steamId}`); } }, { regex: - /^(?.+) \(Steam ID: (?[0-9a-f]{32})\) has created Squad (?[0-9]+) \(Squad Name: (?.+)\) on (?.+)/, + /^(?.+) \(Online IDs: EOS: (?[0-9a-f]{32}) steam: (?\d+)\) has created Squad (?[0-9]+) \(Squad Name: (?.+)\) on (?.+)/, rep: (line, steamId, eosId) => { - return line.replace(` (Steam ID: ${eosId}) `, ` (Steam ID: ${steamId}) `); + return line.replace( + /Online IDs: EOS: (?[0-9a-f]{32}) steam: (?\d+)/, + `Steam ID: ${steamId}` + ); } }, { - regex: /^Kicked player (?[0-9]+). \[steamid=(?[0-9a-f]{32})] (?.+)/, + regex: + /^Kicked player (?[0-9]+). \[Online IDs= EOS: (?[0-9a-f]{32}) steam: (?\d{17})] (?.+)/, rep: (line, steamId, eosId) => { - return line.replace(` [steamid=${eosId}] `, ` [steamid=${steamId}] `); + return line.replace( + /Online IDs= EOS: (?[0-9a-f]{32}) steam: (?\d{17})/, + `steamid=${steamId}` + ); } }, @@ -555,15 +564,10 @@ const defs = [ } }, { - regex: /^\[SteamID:(?[0-9a-f]{32})] (?.+) has possessed admin camera./, - rep: (line, steamId, eosId) => { - return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); - } - }, - { - regex: /^\[SteamID:(?[0-9a-f]{32})] (?.+) has unpossessed admin camera./, + regex: + /^\[Online I(d|D)s:EOS: (?[0-9a-f]{32}) steam: (?)\d{17}] (?.+) has (un)?possessed admin camera\./, rep: (line, steamId, eosId) => { - return line.replace(` [SteamID:${eosId}] `, ` [SteamID:${steamId}] `); + return line.replace(/Online I(d|D)s:EOS: [\w\d]{32} steam: \d{17}/, `SteamID:${steamId}`); } } ]; From 4637bc78d6b3955e6da8a4b958791441f472e547 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Tue, 12 Dec 2023 19:04:22 +0100 Subject: [PATCH 18/53] chore: added config options for rconPassThrough in config-template --- config.json | 3 +++ squad-server/templates/config-template.json | 3 +++ 2 files changed, 6 insertions(+) diff --git a/config.json b/config.json index d0dbd46c..47f77f43 100644 --- a/config.json +++ b/config.json @@ -5,6 +5,9 @@ "queryPort": 27165, "rconPort": 21114, "rconPassword": "password", + "rconPassThrough": true, + "rconPassThroughPort": 8124, + "dumpRconResponsesToFile": false, "logReaderMode": "tail", "logDir": "C:/path/to/squad/log/folder", "ftp": { diff --git a/squad-server/templates/config-template.json b/squad-server/templates/config-template.json index f8f6458b..be873456 100644 --- a/squad-server/templates/config-template.json +++ b/squad-server/templates/config-template.json @@ -5,6 +5,9 @@ "queryPort": 27165, "rconPort": 21114, "rconPassword": "password", + "rconPassThrough": true, + "rconPassThroughPort": 8124, + "dumpRconResponsesToFile": false, "logReaderMode": "tail", "logDir": "C:/path/to/squad/log/folder", "ftp": { From cfc300bd908c08077899cbaa0430bf744eeaaf37 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Tue, 12 Dec 2023 22:40:47 +0100 Subject: [PATCH 19/53] fix: getListPlayers case of server is null --- squad-server/rcon.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/squad-server/rcon.js b/squad-server/rcon.js index a8d467ba..9058effe 100644 --- a/squad-server/rcon.js +++ b/squad-server/rcon.js @@ -148,7 +148,8 @@ export default class SquadRcon extends Rcon { ); if (!match) continue; - server.rcon.addIds(match[3], match[2]); + if (server) server.rcon.addIds(match[3], match[2]); + players.push({ playerID: match[1], EOSID: match[2], From 677a85bdc04129111877fcbac3250e9b4bac16c3 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Wed, 13 Dec 2023 00:18:12 +0100 Subject: [PATCH 20/53] fix: handle null server or server.rcon in getListPlayers --- squad-server/rcon.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/squad-server/rcon.js b/squad-server/rcon.js index 9058effe..4ba19859 100644 --- a/squad-server/rcon.js +++ b/squad-server/rcon.js @@ -148,7 +148,7 @@ export default class SquadRcon extends Rcon { ); if (!match) continue; - if (server) server.rcon.addIds(match[3], match[2]); + if (server && server.rcon) server.rcon.addIds(match[3], match[2]); players.push({ playerID: match[1], From f00f79060825687b878f2b2679d96c0f978f2951 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Wed, 13 Dec 2023 00:18:35 +0100 Subject: [PATCH 21/53] fix: missing a2sPlayerCount property --- squad-server/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/squad-server/index.js b/squad-server/index.js index 31c49b0e..be1ac07d 100644 --- a/squad-server/index.js +++ b/squad-server/index.js @@ -502,6 +502,7 @@ export default class SquadServer extends EventEmitter { reserveSlots: parseInt(data.PlayerReserveCount_I), playerCount: parseInt(data.PlayerCount_I), + a2sPlayerCount: parseInt(data.PlayerCount_I), publicQueue: parseInt(data.PublicQueue_I), reserveQueue: parseInt(data.ReservedQueue_I), From bfe3ceb436fcc27e9e631f7fb9e609a6a73d529c Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Wed, 13 Dec 2023 14:04:20 +0100 Subject: [PATCH 22/53] chore: removed console.log --- squad-server/log-parser/player-died.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/squad-server/log-parser/player-died.js b/squad-server/log-parser/player-died.js index 4a9e40f4..98557561 100644 --- a/squad-server/log-parser/player-died.js +++ b/squad-server/log-parser/player-died.js @@ -18,8 +18,6 @@ export default { logParser.eventStore.session[args[3]] = data; - console.log('Die', data); - logParser.emit('PLAYER_DIED', data); } }; From cc32b017369587fa1c6133e5d3f6d66d04eeaead Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Wed, 13 Dec 2023 14:16:28 +0100 Subject: [PATCH 23/53] chore: removed console.log --- squad-server/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/squad-server/index.js b/squad-server/index.js index be1ac07d..c08c9f8f 100644 --- a/squad-server/index.js +++ b/squad-server/index.js @@ -251,8 +251,6 @@ export default class SquadServer extends EventEmitter { delete data.victimName; delete data.attackerName; - console.log('player damage', data); - this.emit('PLAYER_DAMAGED', data); }); From b3c2567043ecc207751cedeb6dde2663805af979 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Thu, 14 Dec 2023 02:41:09 +0100 Subject: [PATCH 24/53] chore: redundant recognition of current and next layer --- squad-server/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/squad-server/index.js b/squad-server/index.js index c08c9f8f..c6a12b9b 100644 --- a/squad-server/index.js +++ b/squad-server/index.js @@ -527,6 +527,9 @@ export default class SquadServer extends EventEmitter { this.matchTimeout = info.matchTimeout; this.gameVersion = info.gameVersion; + if (!this.currentLayer) this.currentLayer = Layers.getLayerByClassname(info.currentLayer); + if (!this.nextLayer) this.nextLayer = Layers.getLayerByClassname(info.nextLayer); + this.emit('UPDATED_A2S_INFORMATION', info); this.emit('UPDATED_SERVER_INFORMATION', info); } catch (err) { From ff61c1a502f55a22cb005a123cbca50f73756ba8 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Thu, 14 Dec 2023 02:43:04 +0100 Subject: [PATCH 25/53] fix: increased maximum buffer size --- core/rcon.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/rcon.js b/core/rcon.js index 1fe0bb07..e06b9c2b 100644 --- a/core/rcon.js +++ b/core/rcon.js @@ -186,7 +186,7 @@ export default class Rcon extends EventEmitter { return this.soh; } const bufSize = this.stream.readInt32LE(0); - if (bufSize > 4154 || bufSize < 10) return this.badPacket(); + if (bufSize > 8192 || bufSize < 10) return this.badPacket(); else if (bufSize <= this.stream.byteLength - 4 && this.stream.byteLength >= 12) { const bufId = this.stream.readInt32LE(4); const bufType = this.stream.readInt32LE(8); From 1f6e61288a1ed4c81aa6fdc5b99ebedccefb79e7 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Thu, 14 Dec 2023 03:08:14 +0100 Subject: [PATCH 26/53] fix: typos as suggested in https://github.com/Team-Silver-Sphere/SquadJS/pull/318#issuecomment-1854740474 --- core/rcon.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/rcon.js b/core/rcon.js index e06b9c2b..1cbe73a5 100644 --- a/core/rcon.js +++ b/core/rcon.js @@ -228,7 +228,7 @@ export default class Rcon extends EventEmitter { this.emit(`response${this.responseString.id - 2}`, this.responseString.body); this.responseString.body = ''; } else if (!packet.body.includes('')) { - this.responseString.body = this.responseString.body += packet.body; + this.responseString.body = this.responseString.body + packet.body; this.responseString.id = packet.id; } else this.badPacket(); } @@ -241,7 +241,7 @@ export default class Rcon extends EventEmitter { }` ); this.stream = Buffer.alloc(0); - this.responseString = ''; + this.responseString.body = ''; return null; } onClose() { From 83d3aca9e70271472c3586eb7fe32fa51f188b37 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Thu, 14 Dec 2023 22:05:23 +0100 Subject: [PATCH 27/53] chore: less restrictive matching on squad lines --- squad-server/rcon.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/squad-server/rcon.js b/squad-server/rcon.js index 4ba19859..d48c7d12 100644 --- a/squad-server/rcon.js +++ b/squad-server/rcon.js @@ -176,9 +176,9 @@ export default class SquadRcon extends Rcon { for (const line of responseSquad.split('\n')) { const match = line.match( - /ID: ([0-9]+) \| Name: (.+) \| Size: ([0-9]+) \| Locked: (True|False) \| Creator Name: (.+) \| Creator Online IDs: EOS: ([0-9a-f]{32}) steam: (\d{17})/ + /ID: (\d+) \| Name: (.+) \| Size: (\d+) \| Locked: (True|False) \| Creator Name: (.+) \| Creator Online IDs: EOS: ([\d\w]{32}) steam: (\d{17})/ ); - const matchSide = line.match(/Team ID: (1|2) \((.+)\)/); + const matchSide = line.match(/Team ID: (\d) \((.+)\)/); if (matchSide) { teamID = matchSide[1]; teamName = matchSide[2]; From f7569ab437fc41d95807589cce3716aeaf8191a6 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Thu, 14 Dec 2023 22:08:20 +0100 Subject: [PATCH 28/53] chore: added squad parameter creatorEOSID --- squad-server/rcon.js | 1 + 1 file changed, 1 insertion(+) diff --git a/squad-server/rcon.js b/squad-server/rcon.js index d48c7d12..a15948b9 100644 --- a/squad-server/rcon.js +++ b/squad-server/rcon.js @@ -190,6 +190,7 @@ export default class SquadRcon extends Rcon { size: match[3], locked: match[4], creatorName: match[5], + creatorEOSID: match[6], creatorSteamID: match[7], teamID: teamID, teamName: teamName From eaa98bb281ffa14b468c3178ad44c14240fedf1c Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Thu, 14 Dec 2023 22:11:57 +0100 Subject: [PATCH 29/53] refactor: full check on server.rcon.addIds existance --- squad-server/rcon.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/squad-server/rcon.js b/squad-server/rcon.js index a15948b9..c37b55d6 100644 --- a/squad-server/rcon.js +++ b/squad-server/rcon.js @@ -148,7 +148,7 @@ export default class SquadRcon extends Rcon { ); if (!match) continue; - if (server && server.rcon) server.rcon.addIds(match[3], match[2]); + if (server?.rcon?.addIds) server.rcon.addIds(match[3], match[2]); players.push({ playerID: match[1], From de2878d7aa36399bc3e37e5d20d18bac7c428015 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Thu, 14 Dec 2023 23:20:35 +0100 Subject: [PATCH 30/53] fix: workaround for the outdated player list data requested when a new squad is created, causing missing of the squad id parameter --- squad-server/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/squad-server/index.js b/squad-server/index.js index c6a12b9b..9babd590 100644 --- a/squad-server/index.js +++ b/squad-server/index.js @@ -157,6 +157,8 @@ export default class SquadServer extends EventEmitter { this.rcon.on('SQUAD_CREATED', async (data) => { data.player = await this.getPlayerBySteamID(data.playerSteamID, true); + data.player.squadID = data.squadID; + delete data.playerName; delete data.playerSteamID; From 98a04351b157ee64bfb62f4c7590fb8538a975e9 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Fri, 15 Dec 2023 01:27:51 +0100 Subject: [PATCH 31/53] fix: corrected startup procedure, added logging --- squad-server/index.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/squad-server/index.js b/squad-server/index.js index 9babd590..5f91a5db 100644 --- a/squad-server/index.js +++ b/squad-server/index.js @@ -72,13 +72,13 @@ export default class SquadServer extends EventEmitter { this.admins = await fetchAdminLists(this.options.adminLists); await this.rcon.connect(); - await this.logParser.watch(); - await this.updateSquadList(); await this.updatePlayerList(this); await this.updateLayerInformation(); await this.updateA2SInformation(); + await this.logParser.watch(); + Logger.verbose('SquadServer', 1, `Watching ${this.serverName}...`); await this.pingSquadJSAPI(); @@ -414,6 +414,13 @@ export default class SquadServer extends EventEmitter { }); } + if (this.a2sPlayerCount > 0 && players.length === 0) + Logger.verbose( + 'SquadServer', + 1, + `Real Player Count: ${this.a2sPlayerCount} but loaded ${players.length}` + ); + this.emit('UPDATED_PLAYER_INFORMATION'); } catch (err) { Logger.verbose('SquadServer', 1, 'Failed to update player list.', err); From d332dad8f6bdb04edbc2d77cc57001e49a97c573 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Fri, 15 Dec 2023 01:45:55 +0100 Subject: [PATCH 32/53] fix+chore: using regex groups --- squad-server/rcon.js | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/squad-server/rcon.js b/squad-server/rcon.js index c37b55d6..b22f0696 100644 --- a/squad-server/rcon.js +++ b/squad-server/rcon.js @@ -144,22 +144,17 @@ export default class SquadRcon extends Rcon { for (const line of response.split('\n')) { const match = line.match( - /ID: ([0-9]+) \| Online IDs: EOS: ([0-9a-f]{32}) steam: (\d{17}) \| Name: (.+) \| Team ID: ([0-9]+) \| Squad ID: ([0-9]+|N\/A) \| Is Leader: (True|False) \| Role: ([A-Za-z0-9_]*)\b/ + /^ID: (?\d+) \| Online IDs: EOS: (?[a-f\d]{32}) (?:steam: (?\d{17}) )?\| Name: (?.+) \| Team ID: (?\d|N\/A) \| Squad ID: (?\d+|N\/A) \| Is Leader: (?True|False) \| Role: (?.+)$/ ); if (!match) continue; if (server?.rcon?.addIds) server.rcon.addIds(match[3], match[2]); - players.push({ - playerID: match[1], - EOSID: match[2], - steamID: match[3], - name: match[4], - teamID: match[5], - squadID: match[6] !== 'N/A' ? match[5] : null, - isLeader: match[7] === 'True', - role: match[8] - }); + const data = match.groups; + data.isLeader = data.isLeader === 'True'; + data.squadID = data.squadID !== 'N/A' ? data.squadID : null; + + players.push(data); } return players; @@ -176,7 +171,7 @@ export default class SquadRcon extends Rcon { for (const line of responseSquad.split('\n')) { const match = line.match( - /ID: (\d+) \| Name: (.+) \| Size: (\d+) \| Locked: (True|False) \| Creator Name: (.+) \| Creator Online IDs: EOS: ([\d\w]{32}) steam: (\d{17})/ + /ID: (\d+) \| Name: (.+) \| Size: (\d+) \| Locked: (True|False) \| Creator Name: (.+) \| Creator Online IDs: EOS: ([a-f\d]{32}) steam: (\d{17})/ ); const matchSide = line.match(/Team ID: (\d) \((.+)\)/); if (matchSide) { From 778a505ff0d791ef25c92119d308846fc1789d58 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Sat, 16 Dec 2023 23:19:41 +0100 Subject: [PATCH 33/53] chore: added playerEOSID to SQUAD_CREATED event --- squad-server/rcon.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/squad-server/rcon.js b/squad-server/rcon.js index b22f0696..5b0a365c 100644 --- a/squad-server/rcon.js +++ b/squad-server/rcon.js @@ -86,7 +86,7 @@ export default class SquadRcon extends Rcon { } const matchSqCreated = decodedPacket.body.match( - /(.+) \(Online IDs: EOS: ([0-9a-f]{32}) steam: (\d{17})\) has created Squad (\d+) \(Squad Name: (.+)\) on (.+)/ + /(.+) \(Online IDs: EOS: ([\da-f]{32}) steam: (\d{17})\) has created Squad (\d+) \(Squad Name: (.+)\) on (.+)/ ); if (matchSqCreated) { Logger.verbose('SquadRcon', 2, `Matched Squad Created: ${decodedPacket.body}`); @@ -94,6 +94,7 @@ export default class SquadRcon extends Rcon { this.emit('SQUAD_CREATED', { time: new Date(), playerName: matchSqCreated[1], + playerEOSID: matchSqCreated[2], playerSteamID: matchSqCreated[3], squadID: matchSqCreated[4], squadName: matchSqCreated[5], From 081df656b905534d5b7e3f366b1c17d4e01eaac8 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Wed, 20 Dec 2023 23:42:51 +0100 Subject: [PATCH 34/53] refactor: use eosid in squad_created event --- squad-server/index.js | 3 ++- squad-server/rcon.js | 19 ++++--------------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/squad-server/index.js b/squad-server/index.js index 5f91a5db..0a7fbcd8 100644 --- a/squad-server/index.js +++ b/squad-server/index.js @@ -156,11 +156,12 @@ export default class SquadServer extends EventEmitter { }); this.rcon.on('SQUAD_CREATED', async (data) => { - data.player = await this.getPlayerBySteamID(data.playerSteamID, true); + data.player = await this.getPlayerByEOSID(data.playerEOSID, true); data.player.squadID = data.squadID; delete data.playerName; delete data.playerSteamID; + delete data.playerEOSID; this.emit('SQUAD_CREATED', data); }); diff --git a/squad-server/rcon.js b/squad-server/rcon.js index 5b0a365c..8f640613 100644 --- a/squad-server/rcon.js +++ b/squad-server/rcon.js @@ -86,19 +86,14 @@ export default class SquadRcon extends Rcon { } const matchSqCreated = decodedPacket.body.match( - /(.+) \(Online IDs: EOS: ([\da-f]{32}) steam: (\d{17})\) has created Squad (\d+) \(Squad Name: (.+)\) on (.+)/ + /(?.+) \(Online IDs: EOS: (?[\da-f]{32})(?: steam: (?\d{17}))?\) has created Squad (?\d+) \(Squad Name: (?.+)\) on (?.+)/ ); if (matchSqCreated) { Logger.verbose('SquadRcon', 2, `Matched Squad Created: ${decodedPacket.body}`); this.emit('SQUAD_CREATED', { time: new Date(), - playerName: matchSqCreated[1], - playerEOSID: matchSqCreated[2], - playerSteamID: matchSqCreated[3], - squadID: matchSqCreated[4], - squadName: matchSqCreated[5], - teamName: matchSqCreated[6] + ...matchSqCreated.groups }); return; @@ -172,7 +167,7 @@ export default class SquadRcon extends Rcon { for (const line of responseSquad.split('\n')) { const match = line.match( - /ID: (\d+) \| Name: (.+) \| Size: (\d+) \| Locked: (True|False) \| Creator Name: (.+) \| Creator Online IDs: EOS: ([a-f\d]{32}) steam: (\d{17})/ + /ID: (?\d+) \| Name: (?.+) \| Size: (?\d+) \| Locked: (?True|False) \| Creator Name: (?.+) \| Creator Online IDs: EOS: (?[a-f\d]{32})(?: steam: (?\d{17}))?/ ); const matchSide = line.match(/Team ID: (\d) \((.+)\)/); if (matchSide) { @@ -181,13 +176,7 @@ export default class SquadRcon extends Rcon { } if (!match) continue; squads.push({ - squadID: match[1], - squadName: match[2], - size: match[3], - locked: match[4], - creatorName: match[5], - creatorEOSID: match[6], - creatorSteamID: match[7], + ...match.groups, teamID: teamID, teamName: teamName }); From 5cd02868694269b8b8989de0b122db53e84981db Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Thu, 21 Dec 2023 20:21:37 +0100 Subject: [PATCH 35/53] fix: victim player controller and more accurate checks on data.attacker.playercontroller in index.js PLAYER_DAMAGED event --- squad-server/index.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/squad-server/index.js b/squad-server/index.js index 0a7fbcd8..85e7deb4 100644 --- a/squad-server/index.js +++ b/squad-server/index.js @@ -241,11 +241,10 @@ export default class SquadServer extends EventEmitter { data.victim = await this.getPlayerByName(data.victimName); data.attacker = await this.getPlayerByEOSID(data.attackerEOSID); - if (!data.attacker.playercontroller) data.attacker.playercontroller = data.attackerController; + if (data.attacker && !data.attacker.playercontroller && data.attackerController) + data.attacker.playercontroller = data.attackerController; if (data.victim && data.attacker) { - if (!data.victim.playercontroller) data.victim.playercontroller = data.attackerController; - data.teamkill = data.victim.teamID === data.attacker.teamID && data.victim.steamID !== data.attacker.steamID; From 21950067657302379191522064741398d734ec8e Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Tue, 26 Dec 2023 16:01:45 +0100 Subject: [PATCH 36/53] refactor: improper usage of hierarchy in squad-server/rcon.js getListPlayers() --- squad-server/rcon.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/squad-server/rcon.js b/squad-server/rcon.js index 8f640613..63676337 100644 --- a/squad-server/rcon.js +++ b/squad-server/rcon.js @@ -131,7 +131,7 @@ export default class SquadRcon extends Rcon { }; } - async getListPlayers(server) { + async getListPlayers() { const response = await this.execute('ListPlayers'); const players = []; @@ -144,7 +144,7 @@ export default class SquadRcon extends Rcon { ); if (!match) continue; - if (server?.rcon?.addIds) server.rcon.addIds(match[3], match[2]); + if (this.addIds) this.addIds(match[3], match[2]); const data = match.groups; data.isLeader = data.isLeader === 'True'; From 5a4787f38209a513ee28a39004d8c488d56a1f4e Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Tue, 26 Dec 2023 19:11:17 +0100 Subject: [PATCH 37/53] refactor: implemented core/rcon.js from PR 267 --- config.json | 3 - core/rcon.js | 918 ++++++++------------ squad-server/index.js | 5 +- squad-server/rcon.js | 2 - squad-server/templates/config-template.json | 3 - 5 files changed, 367 insertions(+), 564 deletions(-) diff --git a/config.json b/config.json index 47f77f43..d0dbd46c 100644 --- a/config.json +++ b/config.json @@ -5,9 +5,6 @@ "queryPort": 27165, "rconPort": 21114, "rconPassword": "password", - "rconPassThrough": true, - "rconPassThroughPort": 8124, - "dumpRconResponsesToFile": false, "logReaderMode": "tail", "logDir": "C:/path/to/squad/log/folder", "ftp": { diff --git a/core/rcon.js b/core/rcon.js index 1cbe73a5..af6b8997 100644 --- a/core/rcon.js +++ b/core/rcon.js @@ -1,613 +1,427 @@ -/* eslint-disable */ -import { EventEmitter } from 'node:events'; -import net from 'node:net'; +import EventEmitter from 'events'; +import net from 'net'; +import util from 'util'; + import Logger from './logger.js'; -import fs from 'fs'; -import path from 'path'; -const RCON_LOG_FILEPATH = 'RCON_RECEIVED_MESSAGES.log'; + +const SERVERDATA_EXECCOMMAND = 0x02; +const SERVERDATA_RESPONSE_VALUE = 0x00; +const SERVERDATA_AUTH = 0x03; +const SERVERDATA_AUTH_RESPONSE = 0x02; +const SERVERDATA_CHAT_VALUE = 0x01; + +const MID_PACKET_ID = 0x01; +const END_PACKET_ID = 0x02; + export default class Rcon extends EventEmitter { constructor(options = {}) { super(); + + // store config for (const option of ['host', 'port', 'password']) if (!(option in options)) throw new Error(`${option} must be specified.`); + this.host = options.host; this.port = options.port; this.password = options.password; - this.client = null; - this.stream = new Buffer.alloc(0); - this.type = { auth: 0x03, command: 0x02, response: 0x00, server: 0x01 }; - this.soh = { size: 7, id: 0, type: this.type.response, body: '' }; - this.responseString = { id: 0, body: '' }; + this.autoReconnectDelay = options.autoReconnectDelay || 5000; + + // bind methods + this.connect = this.connect.bind(this); // we bind this as we call it on the auto reconnect timeout + this.onPacket = this.onPacket.bind(this); + this.onClose = this.onClose.bind(this); + this.onError = this.onError.bind(this); + this.decodeData = this.decodeData.bind(this); + this.encodePacket = this.encodePacket.bind(this); + + // setup socket + this.client = new net.Socket(); + this.client.on('data', this.decodeData); + this.client.on('close', this.onClose); + this.client.on('error', this.onError); + + // constants + this.maximumPacketSize = 4096; + + // internal variables this.connected = false; this.autoReconnect = false; - this.autoReconnectDelay = options.autoReconnectDelay || 1000; - this.connectionRetry; - this.msgIdLow = 6; - this.msgIdHigh = 16; - this.specialId = 19; - this.msgId = this.msgIdLow; - this.passThrough = options.passThrough ? true : false; - this.passThroughPort = options.passThroughPort || 8124; - this.passThroughTimeOut = options.passThroughTimeOut || 60000; - this.passThroughMaxClients = 1; //options.passThroughMaxClients || 10; - this.passThroughChallenge = options.passThroughChallenge || options.password; - this.dumpRconResponsesToFile = options.dumpRconResponsesToFile || false; - this.rconClients = {}; - for (let i = 1; i <= this.passThroughMaxClients; i++) this.rconClients[`${i}`] = null; - this.ptServer = null; - - this.steamIndex = { '76561198799344716': '00026e21ce3d43c792613bdbb6dec1ba' }; // example dtata - this.eosIndex = { '00026e21ce3d43c792613bdbb6dec1ba': '76561198799344716' }; // example dtata - - this.rotateLogFile(RCON_LOG_FILEPATH); - } - processChatPacket(decodedPacket) { - console.log(decodedPacket.body); - } // - async connect() { - return new Promise((resolve, reject) => { - if (this.client && this.connected && !this.client.destroyed) - return reject(new Error('Rcon.connect() Rcon already connected.')); - this.removeAllListeners('server'); - this.removeAllListeners('auth'); - this.on('server', (pkt) => this.processChatPacket(pkt)); - this.once('auth', () => { - Logger.verbose('RCON', 1, `Connected to: ${this.host}:${this.port}`); - clearTimeout(this.connectionRetry); - this.connected = true; - if (this.passThrough) this.createServer(); - resolve(); - }); - Logger.verbose('RCON', 1, `Connecting to: ${this.host}:${this.port}`); - this.connectionRetry = setTimeout(() => this.connect(), this.autoReconnectDelay); - this.autoReconnect = true; - this.client = net - .createConnection({ port: this.port, host: this.host }, () => this.sendAuth()) - .on('data', (data) => this.onData(data)) - .on('end', () => this.onClose()) - .on('error', () => this.onNetError()); - }).catch((error) => { - Logger.verbose('RCON', 1, `Rcon.connect() ${error}`); - }); - } - async disconnect() { - return new Promise((resolve, reject) => { - Logger.verbose('RCON', 1, `Disconnecting from: ${this.host}:${this.port}`); - clearTimeout(this.connectionRetry); - this.removeAllListeners('server'); - this.removeAllListeners('auth'); - this.autoReconnect = false; - this.client.end(); - this.connected = false; - this.closeServer(); - resolve(); - }).catch((error) => { - Logger.verbose('RCON', 1, `Rcon.disconnect() ${error}`); - }); - } - async execute(body) { - let steamID = body.match(/\d{17}/); - if (steamID) { - steamID = steamID[0]; - body = body.replace(/\d{17}/, this.steamIndex[steamID]); - } + this.autoReconnectTimeout = null; - return new Promise((resolve, reject) => { - if (!this.connected) return reject(new Error('Rcon not connected.')); - if (!this.client.writable) return reject(new Error('Unable to write to node:net socket')); - const string = String(body); - const length = Buffer.from(string).length; - if (length > 4154) Logger.verbose('RCON', 1, `Error occurred. Oversize, "${length}" > 4154`); - else { - const outputData = (data) => { - clearTimeout(timeOut); - resolve(data); - }; - const timedOut = () => { - console.warn('MISSED', listenerId); - this.removeListener(listenerId, outputData); - return reject(new Error(`Rcon response timed out`)); - }; - if (this.msgId > this.msgIdHigh - 2) this.msgId = this.msgIdLow; - const listenerId = `response${this.msgId}`; - const timeOut = setTimeout(timedOut, 10000); - this.once(listenerId, outputData); - this.send(string, this.msgId); - this.msgId++; - } - }).catch((error) => { - Logger.verbose('RCON', 1, `Rcon.execute() ${error}`); - }); + this.incomingData = Buffer.from([]); + this.incomingResponse = []; + this.responseCallbackQueue = []; + // Used For tracking Callbacks + this.callbackIds = []; + this.count = 1; + this.loggedin = false; } - sendAuth() { - Logger.verbose('RCON', 1, `Sending Token to: ${this.host}:${this.port}`); - this.client.write(this.encode(this.type.auth, 0, this.password).toString('binary'), 'binary'); //2147483647 - } - send(body, id = 99) { - this.write(this.type.command, id, body); - this.write(this.type.command, id + 2); - } - write(type, id, body) { + + onPacket(decodedPacket) { + // the logic in this method simply splits data sent via the data event into packets regardless of how they're + // distributed in the event calls Logger.verbose( 'RCON', 2, - `Writing packet with type "${type}", id "${id}" and body "${body || ''}"` + `Processing decoded packet: ${this.decodedPacketToString(decodedPacket)}` ); - this.client.write(this.encode(type, id, body).toString('binary'), 'binary'); - } - encode(type, id, body = '') { - const size = Buffer.byteLength(body) + 14; - const buffer = new Buffer.alloc(size); - buffer.writeInt32LE(size - 4, 0); - buffer.writeInt32LE(id, 4); - buffer.writeInt32LE(type, 8); - buffer.write(body, 12, size - 2, 'utf8'); - buffer.writeInt16LE(0, size - 2); - return buffer; - } - onData(data) { - Logger.verbose('RCON', 4, `Got data: ${this.bufToHexString(data)}`); - this.stream = Buffer.concat([this.stream, data], this.stream.byteLength + data.byteLength); - while (this.stream.byteLength >= 7) { - const packet = this.decode(); - if (!packet) break; - else + + switch (decodedPacket.type) { + case SERVERDATA_RESPONSE_VALUE: + case SERVERDATA_AUTH_RESPONSE: + switch (decodedPacket.id) { + case MID_PACKET_ID: + this.incomingResponse.push(decodedPacket); + + break; + case END_PACKET_ID: + this.callbackIds = this.callbackIds.filter((p) => p.id !== decodedPacket.count); + + this.responseCallbackQueue.shift()( + this.incomingResponse.map((packet) => packet.body).join() + ); + this.incomingResponse = []; + + break; + default: + Logger.verbose( + 'RCON', + 1, + `Unknown packet ID ${decodedPacket.id} in: ${this.decodedPacketToString( + decodedPacket + )}` + ); + this.onClose('Unknown Packet'); + } + break; + + case SERVERDATA_CHAT_VALUE: + this.processChatPacket(decodedPacket); + break; + + default: Logger.verbose( 'RCON', - 3, - `Processing decoded packet: Size: ${packet.size}, ID: ${packet.id}, Type: ${packet.type}, Body: ${packet.body}` + 1, + `Unknown packet type ${decodedPacket.type} in: ${this.decodedPacketToString( + decodedPacket + )}` ); - this.appendToFile(RCON_LOG_FILEPATH, packet.body); - - if (packet.id > this.msgIdHigh) this.emit(`responseForward_1`, packet); - else if (packet.type === this.type.response) this.onResponse(packet); - else if (packet.type === this.type.server) this.onServer(packet); - else if (packet.type === this.type.command) this.emit('auth'); + this.onClose('Unknown Packet'); } } - onServer(packet) { - this.emit('server', packet); - for (const client in this.rconClients) - if (this.rconClients[client]) { - this.emit(`serverForward_${this.rconClients[client].rconIdClient}`, packet.body); + + decodeData(data) { + Logger.verbose('RCON', 4, `Got data: ${this.bufToHexString(data)}`); + + this.incomingData = Buffer.concat([this.incomingData, data]); + + while (this.incomingData.byteLength >= 4) { + const size = this.incomingData.readInt32LE(0); + const packetSize = size + 4; + + if (this.incomingData.byteLength < packetSize) { + Logger.verbose( + 'RCON', + 4, + `Waiting for more data... Have: ${this.incomingData.byteLength} Expected: ${packetSize}` + ); + break; } - } - decode() { - if ( - this.stream[0] === 0 && - this.stream[1] === 1 && - this.stream[2] === 0 && - this.stream[3] === 0 && - this.stream[4] === 0 && - this.stream[5] === 0 && - this.stream[6] === 0 - ) { - this.stream = this.stream.subarray(7); - return this.soh; - } - const bufSize = this.stream.readInt32LE(0); - if (bufSize > 8192 || bufSize < 10) return this.badPacket(); - else if (bufSize <= this.stream.byteLength - 4 && this.stream.byteLength >= 12) { - const bufId = this.stream.readInt32LE(4); - const bufType = this.stream.readInt32LE(8); + const packet = this.incomingData.slice(0, packetSize); + + Logger.verbose('RCON', 4, `Processing packet: ${this.bufToHexString(packet)}`); + const decodedPacket = this.decodePacket(packet); + + const matchCount = this.callbackIds.filter((d) => d.id === decodedPacket.count); + if ( - this.stream[bufSize + 2] !== 0 || - this.stream[bufSize + 3] !== 0 || - bufId < 0 || - bufType < 0 || - bufType > 5 - ) - return this.badPacket(); - else { - const response = { - size: bufSize, - id: bufId, - type: bufType, - body: this.stream.toString('utf8', 12, bufSize + 2) - }; - this.stream = this.stream.subarray(bufSize + 4); - if ( - response.body === '' && - this.stream[0] === 0 && - this.stream[1] === 1 && - this.stream[2] === 0 && - this.stream[3] === 0 && - this.stream[4] === 0 && - this.stream[5] === 0 && - this.stream[6] === 0 - ) { - this.stream = this.stream.subarray(7); - response.body = ''; + matchCount.length > 0 || + [SERVERDATA_AUTH_RESPONSE, SERVERDATA_CHAT_VALUE].includes(decodedPacket.type) + ) { + this.onPacket(decodedPacket); + this.incomingData = this.incomingData.slice(packetSize); + continue; + } + // The packet following an empty packet will report to be 10 long (14 including the size header bytes), but in + // it should report 17 long (21 including the size header bytes). Therefore, if the packet is 10 in size + // and there's enough data for it to be a longer packet then we need to probe to check it's this broken packet. + const probePacketSize = 21; + + if (size === 10 && this.incomingData.byteLength >= 21) { + // copy the section of the incoming data of interest + const probeBuf = this.incomingData.slice(0, probePacketSize); + // decode it + const decodedProbePacket = this.decodePacket(probeBuf); + // check whether body matches + if (decodedProbePacket.body === '\x00\x00\x00\x01\x00\x00\x00') { + // it does so it's the broken packet + // remove the broken packet from the incoming data + this.incomingData = this.incomingData.slice(probePacketSize); + Logger.verbose('RCON', 4, `Ignoring some data: ${this.bufToHexString(probeBuf)}`); + continue; } - return response; } - } else return null; + + // We should only get this far into the loop when we are done processing packets from this onData event. + break; + } } - onResponse(packet) { - if (packet.body === '') { - this.emit(`response${this.responseString.id - 2}`, this.responseString.body); - this.responseString.body = ''; - } else if (!packet.body.includes('')) { - this.responseString.body = this.responseString.body + packet.body; - this.responseString.id = packet.id; - } else this.badPacket(); + + decodePacket(packet) { + return { + size: packet.readUInt32LE(0), + id: packet.readUInt8(4), + count: packet.readUInt16LE(6), + type: packet.readUInt32LE(8), + body: packet.toString('utf8', 12, packet.byteLength - 2) + }; } - badPacket() { + + processChatPacket(decodedPacket) {} + + onClose(hadError) { + this.connected = false; + this.loggedin = false; Logger.verbose( 'RCON', 1, - `Bad packet, clearing: ${this.bufToHexString(this.stream)} Pending string: ${ - this.responseString - }` + `Socket closed ${hadError ? 'with' : 'without'} an error. ${hadError}` ); - this.stream = Buffer.alloc(0); - this.responseString.body = ''; - return null; - } - onClose() { - Logger.verbose('RCON', 1, `Socket closed`); - this.cleanUp(); - } - onNetError(error) { - Logger.verbose('RCON', 1, `node:net error:`, error); - this.emit('RCON_ERROR', error); - this.cleanUp(); - } - cleanUp() { - this.closeServer(); - this.connected = false; - this.removeAllListeners(); - clearTimeout(this.connectionRetry); + + // Cleanup all local state onClose + if (this.incomingData.length > 0) { + Logger.verbose('RCON', 2, `Clearing Buffered Data`); + this.incomingData = Buffer.from([]); + } + if (this.incomingResponse.length > 0) { + Logger.verbose('RCON', 2, `Clearing Buffered Response Data`); + this.incomingResponse = []; + } + if (this.responseCallbackQueue.length > 0) { + Logger.verbose('RCON', 2, `Clearing Pending Callbacks`); + + // Cleanup Pending Callbacks; We should maybe retry these on next connection + // However, depending on the reason we got disconnected it may be a while. + // IE, Squad server crash, Squad server shutdown for multiple minutes. + + while (this.responseCallbackQueue.length > 0) { + this.responseCallbackQueue.shift()(new Error('RCON DISCONNECTED')); + } + this.callbackIds = []; + } + if (this.autoReconnect) { - Logger.verbose('RCON', 1, `Sleeping ${this.autoReconnectDelay}ms before reconnecting`); - this.connectionRetry = setTimeout(() => this.connect(), this.autoReconnectDelay); + Logger.verbose('RCON', 1, `Sleeping ${this.autoReconnectDelay}ms before reconnecting.`); + setTimeout(this.connect, this.autoReconnectDelay); } } - createServer() { - this.ptServer = net.createServer((client) => this.onNewClient(client)); - this.ptServer.maxConnections = this.passThroughMaxClients; - this.ptServer.on('error', (error) => this.onSerErr(error)); - this.ptServer.on('drop', () => - Logger.verbose( - 'RCON', - 1, - `Pass-through Server: Max Clients Reached (${this.passThroughMaxClients}) rejecting new connection` - ) - ); - this.ptServer.listen(this.passThroughPort, () => - Logger.verbose('RCON', 1, `Pass-through Server: Listening on port ${this.passThroughPort}`) - ); - } - closeServer() { - for (const client in this.rconClients) - if (this.rconClients[client]) this.rconClients[client].end(); - if (!this.ptServer) return; - this.ptServer.close(() => this.onServerClose()); - } - onServerClose() { - if (!this.ptServer) return; - this.ptServer.removeAllListeners(); - this.ptServer = null; - Logger.verbose('RCON', 1, `Pass-through Server: Closed`); - } - onNewClient(client) { - client.setTimeout(this.passThroughTimeOut); - client.on('end', () => this.onClientEnd(client)); - client.on('error', () => this.onClientEnd(client)); - client.on('timeout', () => this.onClientTimeOut(client)); - client.on('data', (data) => this.onClientData(client, data)); - Logger.verbose('RCON', 1, `Pass-through Server: Client connecting`); + + onError(err) { + Logger.verbose('RCON', 1, `Socket had error:`, err); + this.emit('RCON_ERROR', err); } - onSerErr(error) { - this.closeServer(); - Logger.verbose('RCON', 1, `Pass-through Server: ${error}`); + + connect() { + return new Promise((resolve, reject) => { + Logger.verbose('RCON', 1, `Connecting to: ${this.host}:${this.port}`); + + const onConnect = async () => { + this.client.removeListener('error', onError); + this.connected = true; + + Logger.verbose('RCON', 1, `Connected to: ${this.host}:${this.port}`); + + try { + // connected successfully, now try auth... + await this.write(SERVERDATA_AUTH, this.password); + + // connected and authed successfully + this.autoReconnect = true; + resolve(); + } catch (err) { + reject(err); + } + }; + + const onError = (err) => { + this.client.removeListener('connect', onConnect); + + Logger.verbose('RCON', 1, `Failed to connect to: ${this.host}:${this.port}`, err); + + reject(err); + }; + + this.client.once('connect', onConnect); + this.client.once('error', onError); + + this.client.connect(this.port, this.host); + }); } - onClientEnd(client) { - if (!client.rconIdClient) return; - this.removeAllListeners(`serverForward_${client.rconIdClient}`); - this.removeAllListeners(`responseForward_${client.rconIdClient}`); - this.rconClients[`${client.rconIdClient}`] = null; - Logger.verbose('RCON', 1, `Pass-through Server: Client-${client.rconIdClient} Disconnected`); + + disconnect() { + return new Promise((resolve, reject) => { + Logger.verbose('RCON', 1, `Disconnecting from: ${this.host}:${this.port}`); + + const onClose = () => { + this.client.removeListener('error', onError); + + Logger.verbose('RCON', 1, `Disconnected from: ${this.host}:${this.port}`); + + resolve(); + }; + + const onError = (err) => { + this.client.removeListener('close', onClose); + + Logger.verbose('RCON', 1, `Failed to disconnect from: ${this.host}:${this.port}`, err); + + reject(err); + }; + + this.client.once('close', onClose); + this.client.once('error', onError); + + // prevent any auto reconnection happening + this.autoReconnect = false; + // clear the timeout just in case the socket closed and then we DCed + clearTimeout(this.autoReconnectTimeout); + + this.client.end(); + }); } - onClientTimeOut(client) { - client.end(); - Logger.verbose('RCON', 1, `Pass-through Server: Client-${client.rconIdClient} Timed Out`); + + execute(command) { + return this.write(SERVERDATA_EXECCOMMAND, command); } - onClientData(client, data) { - if (!client.rconStream) client.rconStream = new Buffer.alloc(0); - client.rconStream = Buffer.concat( - [client.rconStream, data], - client.rconStream.byteLength + data.byteLength - ); - while (client.rconStream.byteLength >= 4) { - const packet = this.decodeClient(client); - if (!packet) break; - if (!client.rconHasAuthed) this.authClient(client, packet); - else { - if (!client.rconWheel || client.rconWheel > 20) client.rconWheel = 0; - else client.rconWheel++; - - client.rconIdQueueNEW[`${client.rconWheel}`] = packet.id; - - const encoded = this.encode( - packet.type, - this.specialId + client.rconWheel, - this.steamToEosClient(packet.body) - ); //////////////////////////////////////////////// - this.client.write(encoded.toString('binary'), 'binary'); - // this.client.write(this.encode(packet.type, this.specialId * client.rconIdClient).toString("binary"), "binary") + + write(type, body) { + return new Promise((resolve, reject) => { + if (!this.connected) { + reject(new Error('Not connected.')); + return; } - } - } - decodeClient(client) { - const bufSize = client.rconStream.readInt32LE(0); - if (bufSize <= client.rconStream.byteLength - 4) { - const response = { - size: bufSize, - id: client.rconStream.readInt32LE(4), - type: client.rconStream.readInt32LE(8), - body: client.rconStream.toString('utf8', 12, bufSize + 2) - }; - client.rconStream = client.rconStream.subarray(bufSize + 4); - return response; - } else return null; - } - authClient(client, packet) { - if (packet.body !== this.passThroughChallenge) { - client.end(); - Logger.verbose('RCON', 1, `Pass-through Server: Client [Rejected] Password not matched`); - } else { - client.rconHasAuthed = true; - client.rconIdQueueNEW = {}; - for (let i = 1; i <= this.passThroughMaxClients; i++) { - if (this.rconClients[`${i}`] === null) { - client.rconIdClient = i; - this.rconClients[`${i}`] = client; - break; - } + + if (!this.client.writable) { + reject(new Error('Unable to write to socket.')); + return; + } + + if (!this.loggedin && type !== SERVERDATA_AUTH) { + reject(new Error('RCON not Logged in')); + return; } - this.on(`serverForward_${client.rconIdClient}`, (body) => - client.write(this.encode(1, 0, this.eosToSteam(body)).toString('binary'), 'binary') - ); - this.on(`responseForward_${client.rconIdClient}`, (packet) => this.onForward(client, packet)); - client.write(this.encode(0, packet.id)); - client.write(this.encode(2, packet.id)); - Logger.verbose('RCON', 1, `Pass-through Server: Client-${client.rconIdClient} Connected`); - } - } - onForward(client, packet) { - if (packet.body !== '' && packet.body !== '') { - const int = packet.id - this.specialId; - //console.log(client.rconIdQueueNEW);////////////////////////////////////////////////////////////////////////////////////////// + Logger.verbose('RCON', 2, `Writing packet with type "${type}" and body "${body}".`); - client.write( - this.encode(packet.type, client.rconIdQueueNEW[int], this.eosToSteam(packet.body)).toString( - 'binary' - ), - 'binary' + const encodedPacket = this.encodePacket( + type, + type !== SERVERDATA_AUTH ? MID_PACKET_ID : END_PACKET_ID, + body ); - } else if (packet.body != '') { - const int = packet.id - this.specialId; - client.write(this.encode(0, client.rconIdQueueNEW[int]).toString('binary'), 'binary'); - client.write(this.encodeSpecial(client.rconIdQueueNEW[int]).toString('binary'), 'binary'); - } - } - encodeSpecial(id) { - const buffer = new Buffer.alloc(21); - buffer.writeInt32LE(10, 0); - buffer.writeInt32LE(id, 4); - buffer.writeInt32LE(0, 8); - buffer.writeInt32LE(1, 15); - return buffer; - } - bufToHexString(buf) { - return buf.toString('hex').match(/../g).join(' '); - } - async warn(steamID, message) { - this.execute(`AdminWarn "${steamID}" ${message}`); - } - async kick(steamID, reason) { - this.execute(`AdminKick "${steamID}" ${reason}`); - } - async forceTeamChange(steamID) { - this.execute(`AdminForceTeamChange "${steamID}"`); - } - addIds(steamId, eosId) { - this.steamIndex[steamId] = eosId; // { "76561198799344716": "00026e21ce3d43c792613bdbb6dec1ba" }; - this.eosIndex[eosId] = steamId; - } + const encodedEmptyPacket = this.encodePacket(type, END_PACKET_ID, ''); - removeIds(eosId) { - // clean up ids on leave - } + if (this.maximumPacketSize < encodedPacket.length) { + reject(new Error('Packet too long.')); + return; + } - steamToEosClient(body) { - //assume client does not send more than 1 steamId per msg - const m = body.match(/[0-9]{17}/); - if (m && m[1] in this.steamIndex) return body.replaceAll(`${m[0]}`, this.steamIndex[m[0]]); - return body; - } + const onError = (err) => { + Logger.verbose('RCON', 1, 'Error occurred. Wiping response action queue.', err); + this.responseCallbackQueue = []; + reject(err); + }; - eosToSteam(body) { - //split body to lines for matching (1 steamId per line) - const lines = body.split('\n'); - const nBody = []; - for (let line of lines) nBody.push(this.matchRcon(line)); - return nBody.join('\n'); - } + // the auth packet also sends a normal response, so we add an extra empty action to ignore it + + if (type === SERVERDATA_AUTH) { + this.callbackIds.push({ id: this.count, cmd: body }); + this.responseCallbackQueue.push(() => {}); + this.responseCallbackQueue.push((decodedPacket) => { + this.client.removeListener('error', onError); + if (decodedPacket.id === -1) { + Logger.verbose('RCON', 1, 'Authentication failed.'); + reject(new Error('Authentication failed.')); + } else { + Logger.verbose('RCON', 1, 'Authentication succeeded.'); + this.loggedin = true; + resolve(); + } + }); + } else { + this.callbackIds.push({ id: this.count, cmd: body }); + this.responseCallbackQueue.push((response) => { + this.client.removeListener('error', onError); + + if (response instanceof Error) { + // Called from onClose() + reject(response); + } else { + Logger.verbose( + 'RCON', + 2, + `Returning complete response: ${response.replace(/\r\n|\r|\n/g, '\\n')}` + ); + + resolve(response); + } + }); + } + + this.client.once('error', onError); + + if (this.count + 1 > 65535) { + this.count = 1; + } + + Logger.verbose('RCON', 4, `Sending packet: ${this.bufToHexString(encodedPacket)}`); + this.client.write(encodedPacket); - matchRcon(line) { - for (const r of defs) { - const match = line.match(r.regex); - if (match && (match.groups.eosId in this.eosIndex || match.groups.steamId)) { - return r.rep( - line, - match.groups.steamId || this.eosIndex[match.groups.eosId], - match.groups.eosId + if (type !== SERVERDATA_AUTH) { + Logger.verbose( + 'RCON', + 4, + `Sending empty packet: ${this.bufToHexString(encodedEmptyPacket)}` ); + this.client.write(encodedEmptyPacket); + this.count++; } - } - return line; + }); } - appendToFile(filePath, content) { - if (!this.dumpRconResponsesToFile) return; - const dir = path.dirname(filePath); + encodePacket(type, id, body, encoding = 'utf8') { + const size = Buffer.byteLength(body) + 14; + const buf = Buffer.alloc(size); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } + buf.writeUInt32LE(size - 4, 0); + buf.writeUInt8(id, 4); + buf.writeUInt8(0, 5); + buf.writeUInt16LE(this.count, 6); + buf.writeUInt32LE(type, 8); + buf.write(body, 12, size - 2, encoding); + buf.writeUInt16LE(0, size - 2); - fs.appendFile(filePath, content + '\n', (err) => { - if (err) throw err; - }); + return buf; } - rotateLogFile(logFile) { - if (!this.dumpRconResponsesToFile) return; - if (fs.existsSync(logFile)) { - const ext = path.extname(logFile); - const base = path.basename(logFile, ext); - const dir = path.dirname(logFile); - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - const newFile = path.join(dir, `${base}_${timestamp}${ext}`); - - fs.renameSync(logFile, newFile); - } + + bufToHexString(buf) { + return buf.toString('hex').match(/../g).join(' '); } -} -const defs = [ - //strict matching to avoid 'name as steamId errors' - { - regex: - /^ID: [0-9]+ \| Online IDs: EOS: (?[0-9a-f]{32}) steam: (?\d{17}) \| Name: .+ \| Team ID: (1|2|N\/A) \| Squad ID: ([0-9]+|N\/A) \| Is Leader: (True|False|N\/A) \| Role: .+$/, - rep: (line, steamId, eosId) => { - return line.replace( - /\| Online IDs: EOS: [\w\d]{32} steam: \d{17} \|/, - `| SteamID: ${steamId} |` - ); - } - }, - { - regex: - /^ID: (?[0-9]+) \| Online IDs: EOS: (?[0-9a-f]{32}) steam: (?\d{17}) \| Since Disconnect: (?.+) \| Name: (?.+)$/, - rep: (line, steamId, eosId) => { - return line.replace( - /Online IDs: EOS: (?[0-9a-f]{32}) steam: (?\d{17})/, - `SteamID: ${steamId}` - ); - } - }, - { - regex: - /^ID: (?[0-9]+) \| Name: (?.+) \| Size: (?[0-9]) \| Locked: (?True|False) \| Creator Name: (?.+) \| Creator Online IDs: EOS: (?[\d\w]{32}) steam: (?\d{17})/, - rep: (line, steamId, eosId) => { - console.log(line, steamId, eosId); - const ret = line.replace( - /\| Creator Online IDs: EOS: [\w\d]{32} steam: \d{17}/, - `| Creator Steam ID: ${steamId}` - ); - return ret; - } - }, - { - regex: - /^Forced team change for player (?[0-9]+). \[Online IDs= EOS: (?[0-9a-f]{32}) steam: (?\d{17})] (.+)/, - rep: (line, steamId, eosId) => { - return line.replace( - /Online IDs= EOS: (?[0-9a-f]{32}) steam: (?\d{17})/, - `steamid=${steamId}` - ); - } - }, - { - regex: /^Could not find player (?[0-9a-f]{32})/, - rep: (line, steamId, eosId) => { - return line.replace(`Could not find player ${eosId}`, `Could not find player ${steamId}`); - } - }, + decodedPacketToString(decodedPacket) { + return util.inspect(decodedPacket, { breakLength: Infinity }); + } - { - regex: - /^\[Chat(All|Team|Squad|Admin)] \[Online IDs:EOS: (?[\d\w]{32}) steam: (?\d{17})] (?.+) : (?.+)/, - rep: (line, steamId, eosId) => { - return line.replace(/Online IDs:EOS: [\d\w]{32} steam: \d{17}/, `SteamID:${steamId}`); - } - }, - { - regex: - /^(?.+) \(Online IDs: EOS: (?[0-9a-f]{32}) steam: (?\d+)\) has created Squad (?[0-9]+) \(Squad Name: (?.+)\) on (?.+)/, - rep: (line, steamId, eosId) => { - return line.replace( - /Online IDs: EOS: (?[0-9a-f]{32}) steam: (?\d+)/, - `Steam ID: ${steamId}` - ); - } - }, - { - regex: - /^Kicked player (?[0-9]+). \[Online IDs= EOS: (?[0-9a-f]{32}) steam: (?\d{17})] (?.+)/, - rep: (line, steamId, eosId) => { - return line.replace( - /Online IDs= EOS: (?[0-9a-f]{32}) steam: (?\d{17})/, - `steamid=${steamId}` - ); - } - }, + async warn(steamID, message) { + await this.execute(`AdminWarn "${steamID}" ${message}`); + } - { - regex: /^ERROR: Unable to find player with name or id \((?[0-9a-f]{32})\)$/, - rep: (line, steamId, eosId) => { - return line.replace(`name or id (${eosId})`, `name or id (${steamId})`); - } - }, - { - regex: - /^\[Online I(d|D)s:EOS: (?[0-9a-f]{32}) steam: (?)\d{17}] (?.+) has (un)?possessed admin camera\./, - rep: (line, steamId, eosId) => { - return line.replace(/Online I(d|D)s:EOS: [\w\d]{32} steam: \d{17}/, `SteamID:${steamId}`); - } + async kick(steamID, reason) { + await this.execute(`AdminKick "${steamID}" ${reason}`); + } + + async forceTeamChange(steamID) { + await this.execute(`AdminForceTeamChange "${steamID}"`); } -]; - -//////////////////////////////////////////////////ALL BELOW IS FOR STANDALONE TESTING/RUNNING -// const Logger = { -// level: 1, -// verbose(type, lvl, msg, msg1 = "") { -// if (lvl > this.level) return; -// console.log(type, lvl, msg, msg1); -// }, -// }; - -// const squadJsStyle = async () => { -// const getCurrentMap = async () => { -// const response = await rcon.execute("ShowCurrentMap"); -// const match = response.match(/^Current level is (?.+), layer is (?.+)/); -// if (!match) { -// debugger -// } -// return [match.groups.level, match.groups.layer]; -// }; - -// const rcon = new Rcon({ port: "port", host: "ip", password: "password", passThrough: true, passThroughPort: "8124", passThroughTimeOut: 30000, passThroughChallenge: "password" }); // - -// try { -// await rcon.connect(); -// } catch (e) { -// console.warn(e); -// } - -// rcon.interval = setInterval(async () => { - -// try { -// const currentMap = await getCurrentMap(); -// console.log(currentMap); -// } catch (e) { -// console.warn(e); -// } -// }, 5000); -// }; - -// squadJsStyle(); +} diff --git a/squad-server/index.js b/squad-server/index.js index 85e7deb4..c40cfd66 100644 --- a/squad-server/index.js +++ b/squad-server/index.js @@ -94,10 +94,7 @@ export default class SquadServer extends EventEmitter { host: this.options.rconHost || this.options.host, port: this.options.rconPort, password: this.options.rconPassword, - autoReconnectInterval: this.options.rconAutoReconnectInterval, - dumpRconResponsesToFile: this.options.dumpRconResponsesToFile, - passThroughPort: this.options.rconPassThroughPort, - passThrough: this.options.rconPassThrough + autoReconnectInterval: this.options.rconAutoReconnectInterval }); this.rcon.on('CHAT_MESSAGE', async (data) => { diff --git a/squad-server/rcon.js b/squad-server/rcon.js index 63676337..0fa12405 100644 --- a/squad-server/rcon.js +++ b/squad-server/rcon.js @@ -144,8 +144,6 @@ export default class SquadRcon extends Rcon { ); if (!match) continue; - if (this.addIds) this.addIds(match[3], match[2]); - const data = match.groups; data.isLeader = data.isLeader === 'True'; data.squadID = data.squadID !== 'N/A' ? data.squadID : null; diff --git a/squad-server/templates/config-template.json b/squad-server/templates/config-template.json index be873456..f8f6458b 100644 --- a/squad-server/templates/config-template.json +++ b/squad-server/templates/config-template.json @@ -5,9 +5,6 @@ "queryPort": 27165, "rconPort": 21114, "rconPassword": "password", - "rconPassThrough": true, - "rconPassThroughPort": 8124, - "dumpRconResponsesToFile": false, "logReaderMode": "tail", "logDir": "C:/path/to/squad/log/folder", "ftp": { From 4af850c365d4fca082c76653db44a118fc1931bf Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Tue, 26 Dec 2023 19:12:34 +0100 Subject: [PATCH 38/53] fix: removed code with old core/rcon.js dependency --- squad-server/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/squad-server/index.js b/squad-server/index.js index c40cfd66..670253ec 100644 --- a/squad-server/index.js +++ b/squad-server/index.js @@ -215,8 +215,6 @@ export default class SquadServer extends EventEmitter { `Player connected ${data.playerSuffix} - SteamID: ${data.steamID} - EOSID: ${data.eosID}` ); - this.rcon.addIds(data.steamID, data.eosID); - data.player = await this.getPlayerByEOSID(data.eosID); if (data.player) data.player.suffix = data.playerSuffix; From bb01a434850dae66ae4f07b8f67102b65c797685 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Tue, 26 Dec 2023 19:26:42 +0100 Subject: [PATCH 39/53] chore: set version in package.json to 4.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 29459d4e..5fd1e1bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "SquadJS", - "version": "3.8.2", + "version": "4.0.0", "repository": "https://github.com/Team-Silver-Sphere/SquadJS.git", "author": "Thomas Smyth ", "license": "BSL-1.0", From 39fda36badb60573f01d61fa262e4422054837ea Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Tue, 26 Dec 2023 19:33:01 +0100 Subject: [PATCH 40/53] refactor: renamed player property 'EOSID' into 'eosID' --- squad-server/index.js | 2 +- squad-server/rcon.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/squad-server/index.js b/squad-server/index.js index 670253ec..aee8ad4f 100644 --- a/squad-server/index.js +++ b/squad-server/index.js @@ -596,7 +596,7 @@ export default class SquadServer extends EventEmitter { } async getPlayerByEOSID(eosID, forceUpdate) { - return this.getPlayerByCondition((player) => player.EOSID === eosID, forceUpdate); + return this.getPlayerByCondition((player) => player.eosID === eosID, forceUpdate); } async getPlayerByName(name, forceUpdate) { diff --git a/squad-server/rcon.js b/squad-server/rcon.js index 0fa12405..b100bde0 100644 --- a/squad-server/rcon.js +++ b/squad-server/rcon.js @@ -140,7 +140,7 @@ export default class SquadRcon extends Rcon { for (const line of response.split('\n')) { const match = line.match( - /^ID: (?\d+) \| Online IDs: EOS: (?[a-f\d]{32}) (?:steam: (?\d{17}) )?\| Name: (?.+) \| Team ID: (?\d|N\/A) \| Squad ID: (?\d+|N\/A) \| Is Leader: (?True|False) \| Role: (?.+)$/ + /^ID: (?\d+) \| Online IDs: EOS: (?[a-f\d]{32}) (?:steam: (?\d{17}) )?\| Name: (?.+) \| Team ID: (?\d|N\/A) \| Squad ID: (?\d+|N\/A) \| Is Leader: (?True|False) \| Role: (?.+)$/ ); if (!match) continue; From ac49c8e17014e47e36e9e5a33f47b7d6d870e5c5 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Thu, 28 Dec 2023 23:58:53 +0100 Subject: [PATCH 41/53] chore: improved server information --- squad-server/index.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/squad-server/index.js b/squad-server/index.js index aee8ad4f..273a7a54 100644 --- a/squad-server/index.js +++ b/squad-server/index.js @@ -515,6 +515,7 @@ export default class SquadServer extends EventEmitter { teamTwo: data.TeamTwo_s?.replace(new RegExp(data.MapName_s, 'i'), '') || '', matchTimeout: parseFloat(data.MatchTimeout_d), + matchStartTime: this.getMatchStartTimeByPlaytime(data.PLAYTIME_I), gameVersion: data.GameVersion_s }; @@ -525,10 +526,12 @@ export default class SquadServer extends EventEmitter { this.reserveSlots = info.reserveSlots; this.a2sPlayerCount = info.playerCount; + this.playerCount = info.playerCount; this.publicQueue = info.publicQueue; this.reserveQueue = info.reserveQueue; this.matchTimeout = info.matchTimeout; + this.matchStartTime = info.matchStartTime; this.gameVersion = info.gameVersion; if (!this.currentLayer) this.currentLayer = Layers.getLayerByClassname(info.currentLayer); @@ -663,4 +666,8 @@ export default class SquadServer extends EventEmitter { this.pingSquadJSAPITimeout = setTimeout(this.pingSquadJSAPI, this.pingSquadJSAPIInterval); } + + getMatchStartTimeByPlaytime(playtime) { + return new Date(Date.now() - +playtime * 1000); + } } From f0d4348a30caaeb5973af26a89b52ed2cdd81f83 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Fri, 29 Dec 2023 00:05:38 +0100 Subject: [PATCH 42/53] refactor: replaced "A2S" with "server" in log lines for server information --- squad-server/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/squad-server/index.js b/squad-server/index.js index 273a7a54..fb834893 100644 --- a/squad-server/index.js +++ b/squad-server/index.js @@ -540,10 +540,10 @@ export default class SquadServer extends EventEmitter { this.emit('UPDATED_A2S_INFORMATION', info); this.emit('UPDATED_SERVER_INFORMATION', info); } catch (err) { - Logger.verbose('SquadServer', 1, 'Failed to update A2S information.', err); + Logger.verbose('SquadServer', 1, 'Failed to update server information.', err); } - Logger.verbose('SquadServer', 1, `Updated A2S information.`); + Logger.verbose('SquadServer', 1, `Updated server information.`); this.updateA2SInformationTimeout = setTimeout( this.updateA2SInformation, From 4522f5122ff38cef3aabb0f8b9c4908e09a9d3d1 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Fri, 29 Dec 2023 18:04:53 +0100 Subject: [PATCH 43/53] refactor: removed A2S labels, and replaced with "server". legacy support for updateA2SInformation() --- squad-server/index.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/squad-server/index.js b/squad-server/index.js index fb834893..77a9a269 100644 --- a/squad-server/index.js +++ b/squad-server/index.js @@ -477,23 +477,20 @@ export default class SquadServer extends EventEmitter { ); } - async updateA2SInformation() { + updateA2SInformation() { + return this.updateServerInformation(); + } + + async updateServerInformation() { if (this.updateA2SInformationTimeout) clearTimeout(this.updateA2SInformationTimeout); - Logger.verbose('SquadServer', 1, `Updating A2S information...`); + Logger.verbose('SquadServer', 1, `Updating server information...`); try { - // const data = await Gamedig.query({ - // type: 'squad', - // host: this.options.host, - // port: this.options.queryPort - // }); - const rawData = await this.rcon.execute(`ShowServerInfo`); - Logger.verbose('SquadServer', 3, `A2S raw data`, rawData); + Logger.verbose('SquadServer', 3, `Server information raw data`, rawData); const data = JSON.parse(rawData); - Logger.verbose('SquadServer', 2, `A2S data`, JSON.data); - // Logger.verbose("SquadServer", 1, `A2S data`, JSON.stringify(data, null, 2)) + Logger.verbose('SquadServer', 2, `Server information data`, JSON.data); const info = { raw: data, From 04af5b9c8bc1d94f093764a3985c8b98399d37c9 Mon Sep 17 00:00:00 2001 From: Marek Date: Sat, 30 Dec 2023 06:58:49 +0000 Subject: [PATCH 44/53] Building Configs --- README.md | 572 ++++++++++++++++++++++++++-------------------------- config.json | 172 ++++++++-------- 2 files changed, 372 insertions(+), 372 deletions(-) diff --git a/README.md b/README.md index 21a6aeef..ac377c0b 100644 --- a/README.md +++ b/README.md @@ -202,50 +202,106 @@ The following is a list of plugins built into SquadJS, you can click their title Interested in creating your own plugin? [See more here](./squad-server/plugins/readme.md)
- AutoKickUnassigned -

AutoKickUnassigned

-

The AutoKickUnassigned plugin will automatically kick players that are not in a squad after a specified ammount of time.

+ SeedingMode +

SeedingMode

+

The SeedingMode plugin broadcasts seeding rule messages to players at regular intervals when the server is below a specified player count. It can also be configured to display "Live" messages when the server goes live.

Options

-
  • warningMessage

    +
    • interval

      Description
      -

      Message SquadJS will send to players warning them they will be kicked

      +

      Frequency of seeding messages in milliseconds.

      Default
      -
      Join a squad, you are unassigned and will be kicked
    • -
    • kickMessage

      +
      150000
    • +
    • seedingThreshold

      Description
      -

      Message to send to players when they are kicked

      +

      Player count required for server not to be in seeding mode.

      Default
      -
      Unassigned - automatically removed
    • -
    • frequencyOfWarnings

      +
      50
    • +
    • seedingMessage

      Description
      -

      How often in Seconds should we warn the player about being unassigned?

      +

      Seeding message to display.

      Default
      -
      30
    • -
    • unassignedTimer

      +
      Seeding Rules Active! Fight only over the middle flags! No FOB Hunting!
    • +
    • liveEnabled

      Description
      -

      How long in Seconds to wait before a unassigned player is kicked

      +

      Enable "Live" messages for when the server goes live.

      Default
      -
      360
    • -
    • playerThreshold

      +
      true
    • +
    • liveThreshold

      Description
      -

      Player count required for AutoKick to start kicking players, set to -1 to disable

      +

      Player count required for "Live" messages to not bee displayed.

      Default
      -
      93
    • -
    • roundStartDelay

      +
      52
    • +
    • liveMessage

      Description
      -

      Time delay in Seconds from start of the round before AutoKick starts kicking again

      +

      "Live" message to display.

      Default
      -
      900
    • -
    • ignoreAdmins

      +
      Live!
    • +
    • waitOnNewGames

      Description
      -

      • true: Admins will NOT be kicked
      • false: Admins WILL be kicked

      +

      Should the plugin wait to be executed on NEW_GAME event.

      Default
      -
      false
    • -
    • ignoreWhitelist

      +
      true
    • +
    • waitTimeOnNewGame

      Description
      -

      • true: Reserve slot players will NOT be kicked
      • false: Reserve slot players WILL be kicked

      +

      The time to wait before check player counts in seconds.

      Default
      -
      false
    +
    30
+
+ +
+ DiscordChat +

DiscordChat

+

The DiscordChat plugin will log in-game chat to a Discord channel.

+

Options

+
  • discordClient (Required)

    +
    Description
    +

    Discord connector name.

    +
    Default
    +
    discord
  • +
  • channelID (Required)

    +
    Description
    +

    The ID of the channel to log admin broadcasts to.

    +
    Default
    +
  • Example
    +
    667741905228136459
    +
  • chatColors

    +
    Description
    +

    The color of the embed for each chat.

    +
    Default
    +
    {}
  • Example
    +
    {
    +  "ChatAll": 16761867
    +}
    +
  • color

    +
    Description
    +

    The color of the embed.

    +
    Default
    +
    16761867
  • +
  • ignoreChats

    +
    Description
    +

    A list of chat names to ignore.

    +
    Default
    +
    [
    +  "ChatSquad"
    +]
+
+ +
+ DiscordSubsystemRestarter +

DiscordSubsystemRestarter

+

The DiscordSubSystemRestarter plugin allows you to manually restart SquadJS subsystems in case an issues arises with them.

  • !squadjs restartsubsystem rcon
  • !squadjs restartsubsystem logparser

+

Options

+
  • discordClient (Required)

    +
    Description
    +

    Discord connector name.

    +
    Default
    +
    discord
  • +
  • role (Required)

    +
    Description
    +

    ID of role required to run the sub system restart commands.

    +
    Default
    +
  • Example
    +
    667741905228136459
@@ -266,9 +322,9 @@ Interested in creating your own plugin? [See more here](./squad-server/plugins/r
- CBLInfo -

CBLInfo

-

The CBLInfo plugin alerts admins when a harmful player is detected joining their server based on data from the Community Ban List.

+ DiscordRoundWinner +

DiscordRoundWinner

+

The DiscordRoundWinner plugin will send the round winner to a Discord channel.

Options

  • discordClient (Required)

    Description
    @@ -277,15 +333,15 @@ Interested in creating your own plugin? [See more here](./squad-server/plugins/r
    discord
  • channelID (Required)

    Description
    -

    The ID of the channel to alert admins through.

    +

    The ID of the channel to log admin broadcasts to.

    Default
  • Example
    667741905228136459
    -
  • threshold

    +
  • color

    Description
    -

    Admins will be alerted when a player has this or more reputation points. For more information on reputation points, see the Community Ban List's FAQ

    +

    The color of the embed.

    Default
    -
    6
+
16761867
@@ -307,30 +363,6 @@ Interested in creating your own plugin? [See more here](./squad-server/plugins/r ]
-
- DBLog -

DBLog

-

The mysql-log plugin will log various server statistics and events to a database. This is great for server performance monitoring and/or player stat tracking. - -Grafana: -

  • Grafana is a cool way of viewing server statistics stored in the database.
  • -
  • Install Grafana.
  • -
  • Add your database as a datasource named SquadJS.
  • -
  • Import the SquadJS Dashboard to get a preconfigured MySQL only Grafana dashboard.
  • -
  • Install any missing Grafana plugins.

-

Options

-
  • database (Required)

    -
    Description
    -

    The Sequelize connector to log server information to.

    -
    Default
    -
    mysql
  • -
  • overrideServerID

    -
    Description
    -

    A overridden server ID.

    -
    Default
    -
    null
-
-
DiscordAdminBroadcast

DiscordAdminBroadcast

@@ -355,9 +387,9 @@ Grafana:
- DiscordAdminCamLogs -

DiscordAdminCamLogs

-

The DiscordAdminCamLogs plugin will log in game admin camera usage to a Discord channel.

+ DiscordRoundEnded +

DiscordRoundEnded

+

The DiscordRoundEnded plugin will send the round winner to a Discord channel.

Options

  • discordClient (Required)

    Description
    @@ -366,7 +398,7 @@ Grafana:
    discord
  • channelID (Required)

    Description
    -

    The ID of the channel to log admin camera usage to.

    +

    The ID of the channel to log round end events to.

    Default
  • Example
    667741905228136459
    @@ -377,6 +409,78 @@ Grafana:
    16761867
+
+ AutoKickUnassigned +

AutoKickUnassigned

+

The AutoKickUnassigned plugin will automatically kick players that are not in a squad after a specified ammount of time.

+

Options

+
  • warningMessage

    +
    Description
    +

    Message SquadJS will send to players warning them they will be kicked

    +
    Default
    +
    Join a squad, you are unassigned and will be kicked
  • +
  • kickMessage

    +
    Description
    +

    Message to send to players when they are kicked

    +
    Default
    +
    Unassigned - automatically removed
  • +
  • frequencyOfWarnings

    +
    Description
    +

    How often in Seconds should we warn the player about being unassigned?

    +
    Default
    +
    30
  • +
  • unassignedTimer

    +
    Description
    +

    How long in Seconds to wait before a unassigned player is kicked

    +
    Default
    +
    360
  • +
  • playerThreshold

    +
    Description
    +

    Player count required for AutoKick to start kicking players, set to -1 to disable

    +
    Default
    +
    93
  • +
  • roundStartDelay

    +
    Description
    +

    Time delay in Seconds from start of the round before AutoKick starts kicking again

    +
    Default
    +
    900
  • +
  • ignoreAdmins

    +
    Description
    +

    • true: Admins will NOT be kicked
    • false: Admins WILL be kicked

    +
    Default
    +
    false
  • +
  • ignoreWhitelist

    +
    Description
    +

    • true: Reserve slot players will NOT be kicked
    • false: Reserve slot players WILL be kicked

    +
    Default
    +
    false
+
+ +
+ SocketIOAPI +

SocketIOAPI

+

The SocketIOAPI plugin allows remote access to a SquadJS instance via Socket.IO
As a client example you can use this to connect to the socket.io server;


+      const socket = io.connect('ws://IP:PORT', {
+        auth: {
+          token: "MySecretPassword"
+        }
+      })
+    
If you need more documentation about socket.io please go ahead and read the following;
General Socket.io documentation: Socket.io Docs
Authentication and securing your websocket: Sending-credentials
How to use, install and configure a socketIO-client: Usage Guide with Examples

+

Options

+
  • websocketPort (Required)

    +
    Description
    +

    The port for the websocket.

    +
    Default
    +
  • Example
    +
    3000
    +
  • securityToken (Required)

    +
    Description
    +

    Your secret token/password for connecting.

    +
    Default
    +
  • Example
    +
    MySecretPassword
+
+
DiscordAdminRequest

DiscordAdminRequest

@@ -445,9 +549,21 @@ Grafana:
- DiscordChat -

DiscordChat

-

The DiscordChat plugin will log in-game chat to a Discord channel.

+ TeamRandomizer +

TeamRandomizer

+

The TeamRandomizer can be used to randomize teams. It's great for destroying clan stacks or for social events. It can be run by typing, by default, !randomize into in-game admin chat

+

Options

+
  • command

    +
    Description
    +

    The command used to randomize the teams.

    +
    Default
    +
    randomize
+
+ +
+ DiscordKillFeed +

DiscordKillFeed

+

The DiscordKillFeed plugin logs all wounds and related information to a Discord channel for admins to review.

Options

  • discordClient (Required)

    Description
    @@ -456,36 +572,26 @@ Grafana:
    discord
  • channelID (Required)

    Description
    -

    The ID of the channel to log admin broadcasts to.

    +

    The ID of the channel to log teamkills to.

    Default
  • Example
    667741905228136459
    -
  • chatColors

    -
    Description
    -

    The color of the embed for each chat.

    -
    Default
    -
    {}
  • Example
    -
    {
    -  "ChatAll": 16761867
    -}
  • color

    Description
    -

    The color of the embed.

    +

    The color of the embeds.

    Default
    16761867
  • -
  • ignoreChats

    +
  • disableCBL

    Description
    -

    A list of chat names to ignore.

    +

    Disable Community Ban List information.

    Default
    -
    [
    -  "ChatSquad"
    -]
+
false
- DiscordDebug -

DiscordDebug

-

The DiscordDebug plugin can be used to help debug SquadJS by dumping SquadJS events to a Discord channel.

+ DiscordAdminCamLogs +

DiscordAdminCamLogs

+

The DiscordAdminCamLogs plugin will log in game admin camera usage to a Discord channel.

Options

  • discordClient (Required)

    Description
    @@ -494,47 +600,45 @@ Grafana:
    discord
  • channelID (Required)

    Description
    -

    The ID of the channel to log events to.

    +

    The ID of the channel to log admin camera usage to.

    Default
  • Example
    667741905228136459
    -
  • events (Required)

    +
  • color

    Description
    -

    A list of events to dump.

    +

    The color of the embed.

    Default
    -
    []
  • Example
    -
    [
    -  "PLAYER_DIED"
    -]
+
16761867
- DiscordFOBHABExplosionDamage -

DiscordFOBHABExplosionDamage

-

The DiscordFOBHABExplosionDamage plugin logs damage done to FOBs and HABs by explosions to help identify engineers blowing up friendly FOBs and HABs.

+ DBLog +

DBLog

+

The mysql-log plugin will log various server statistics and events to a database. This is great for server performance monitoring and/or player stat tracking. + +Grafana: +

  • Grafana is a cool way of viewing server statistics stored in the database.
  • +
  • Install Grafana.
  • +
  • Add your database as a datasource named SquadJS.
  • +
  • Import the SquadJS Dashboard to get a preconfigured MySQL only Grafana dashboard.
  • +
  • Install any missing Grafana plugins.

Options

-
  • discordClient (Required)

    -
    Description
    -

    Discord connector name.

    -
    Default
    -
    discord
  • -
  • channelID (Required)

    +
    • database (Required)

      Description
      -

      The ID of the channel to log FOB/HAB explosion damage to.

      +

      The Sequelize connector to log server information to.

      Default
      -
    • Example
      -
      667741905228136459
      -
    • color

      +
      mysql
    • +
    • overrideServerID

      Description
      -

      The color of the embeds.

      +

      A overridden server ID.

      Default
      -
      16761867
    +
    null
- DiscordKillFeed -

DiscordKillFeed

-

The DiscordKillFeed plugin logs all wounds and related information to a Discord channel for admins to review.

+ CBLInfo +

CBLInfo

+

The CBLInfo plugin alerts admins when a harmful player is detected joining their server based on data from the Community Ban List.

Options

  • discordClient (Required)

    Description
    @@ -543,42 +647,41 @@ Grafana:
    discord
  • channelID (Required)

    Description
    -

    The ID of the channel to log teamkills to.

    +

    The ID of the channel to alert admins through.

    Default
  • Example
    667741905228136459
    -
  • color

    -
    Description
    -

    The color of the embeds.

    -
    Default
    -
    16761867
  • -
  • disableCBL

    +
  • threshold

    Description
    -

    Disable Community Ban List information.

    +

    Admins will be alerted when a player has this or more reputation points. For more information on reputation points, see the Community Ban List's FAQ

    Default
    -
    false
+
6
- DiscordPlaceholder -

DiscordPlaceholder

-

The DiscordPlaceholder plugin allows you to make your bot create placeholder messages that can be used when configuring other plugins.

+ DiscordDebug +

DiscordDebug

+

The DiscordDebug plugin can be used to help debug SquadJS by dumping SquadJS events to a Discord channel.

Options

  • discordClient (Required)

    Description

    Discord connector name.

    Default
    discord
  • -
  • command

    +
  • channelID (Required)

    Description
    -

    Command to create Discord placeholder.

    +

    The ID of the channel to log events to.

    Default
    -
    !placeholder
  • -
  • channelID (Required)

    +
  • Example
    +
    667741905228136459
    +
  • events (Required)

    Description
    -

    The bot will only answer with a placeholder on this channel

    +

    A list of events to dump.

    Default
    -
+
[]
Example
+
[
+  "PLAYER_DIED"
+]
@@ -617,9 +720,9 @@ Grafana:
- DiscordRoundWinner -

DiscordRoundWinner

-

The DiscordRoundWinner plugin will send the round winner to a Discord channel.

+ DiscordFOBHABExplosionDamage +

DiscordFOBHABExplosionDamage

+

The DiscordFOBHABExplosionDamage plugin logs damage done to FOBs and HABs by explosions to help identify engineers blowing up friendly FOBs and HABs.

Options

  • discordClient (Required)

    Description
    @@ -628,75 +731,77 @@ Grafana:
    discord
  • channelID (Required)

    Description
    -

    The ID of the channel to log admin broadcasts to.

    +

    The ID of the channel to log FOB/HAB explosion damage to.

    Default
  • Example
    667741905228136459
  • color

    Description
    -

    The color of the embed.

    +

    The color of the embeds.

    Default
    16761867
- DiscordRoundEnded -

DiscordRoundEnded

-

The DiscordRoundEnded plugin will send the round winner to a Discord channel.

+ PersistentEOSIDtoSteamID +

PersistentEOSIDtoSteamID

+

Stores into a DB every association of SteamID-EOSID

+

Options

+
  • database (Required)

    +
    Description
    +

    The Sequelize connector.

    +
    Default
    +
    sqlite
+
+ +
+ DiscordPlaceholder +

DiscordPlaceholder

+

The DiscordPlaceholder plugin allows you to make your bot create placeholder messages that can be used when configuring other plugins.

Options

  • discordClient (Required)

    Description

    Discord connector name.

    Default
    discord
  • -
  • channelID (Required)

    +
  • command

    Description
    -

    The ID of the channel to log round end events to.

    +

    Command to create Discord placeholder.

    Default
    -
  • Example
    -
    667741905228136459
    -
  • color

    +
    !placeholder
  • +
  • channelID (Required)

    Description
    -

    The color of the embed.

    +

    The bot will only answer with a placeholder on this channel

    Default
    -
    16761867
+
- DiscordServerStatus -

DiscordServerStatus

-

The DiscordServerStatus plugin can be used to get the server status in Discord.

+ DiscordTeamkill +

DiscordTeamkill

+

The DiscordTeamkill plugin logs teamkills and related information to a Discord channel for admins to review.

Options

  • discordClient (Required)

    Description

    Discord connector name.

    Default
    discord
  • -
  • messageStore (Required)

    -
    Description
    -

    Sequelize connector name.

    -
    Default
    -
    sqlite
  • -
  • command

    -
    Description
    -

    Command name to get message.

    -
    Default
    -
    !status
  • -
  • disableSubscriptions

    +
  • channelID (Required)

    Description
    -

    Whether to allow messages to be subscribed to automatic updates.

    +

    The ID of the channel to log teamkills to.

    Default
    -
    false
  • -
  • updateInterval

    +
  • Example
    +
    667741905228136459
    +
  • color

    Description
    -

    How frequently to update the time in Discord.

    +

    The color of the embeds.

    Default
    -
    60000
  • -
  • setBotStatus

    +
    16761867
  • +
  • disableCBL

    Description
    -

    Whether to update the bot's status with server information.

    +

    Disable Community Ban List information.

    Default
    -
    true
+
false
@@ -728,49 +833,40 @@ Grafana:
- DiscordSubsystemRestarter -

DiscordSubsystemRestarter

-

The DiscordSubSystemRestarter plugin allows you to manually restart SquadJS subsystems in case an issues arises with them.

  • !squadjs restartsubsystem rcon
  • !squadjs restartsubsystem logparser

+ DiscordServerStatus +

DiscordServerStatus

+

The DiscordServerStatus plugin can be used to get the server status in Discord.

Options

  • discordClient (Required)

    Description

    Discord connector name.

    Default
    discord
  • -
  • role (Required)

    +
  • messageStore (Required)

    Description
    -

    ID of role required to run the sub system restart commands.

    +

    Sequelize connector name.

    Default
    -
  • Example
    -
    667741905228136459
-
- -
- DiscordTeamkill -

DiscordTeamkill

-

The DiscordTeamkill plugin logs teamkills and related information to a Discord channel for admins to review.

-

Options

-
  • discordClient (Required)

    +
    sqlite
  • +
  • command

    Description
    -

    Discord connector name.

    +

    Command name to get message.

    Default
    -
    discord
  • -
  • channelID (Required)

    +
    !status
  • +
  • disableSubscriptions

    Description
    -

    The ID of the channel to log teamkills to.

    +

    Whether to allow messages to be subscribed to automatic updates.

    Default
    -
  • Example
    -
    667741905228136459
    -
  • color

    +
    false
  • +
  • updateInterval

    Description
    -

    The color of the embeds.

    +

    How frequently to update the time in Discord.

    Default
    -
    16761867
  • -
  • disableCBL

    +
    60000
  • +
  • setBotStatus

    Description
    -

    Disable Community Ban List information.

    +

    Whether to update the bot's status with server information.

    Default
    -
    false
+
true
@@ -810,102 +906,6 @@ Grafana:
300000
-
- PersistentEOSIDtoSteamID -

PersistentEOSIDtoSteamID

-

Stores into a DB every association of SteamID-EOSID

-

Options

-
  • database (Required)

    -
    Description
    -

    The Sequelize connector.

    -
    Default
    -
    sqlite
-
- -
- SeedingMode -

SeedingMode

-

The SeedingMode plugin broadcasts seeding rule messages to players at regular intervals when the server is below a specified player count. It can also be configured to display "Live" messages when the server goes live.

-

Options

-
  • interval

    -
    Description
    -

    Frequency of seeding messages in milliseconds.

    -
    Default
    -
    150000
  • -
  • seedingThreshold

    -
    Description
    -

    Player count required for server not to be in seeding mode.

    -
    Default
    -
    50
  • -
  • seedingMessage

    -
    Description
    -

    Seeding message to display.

    -
    Default
    -
    Seeding Rules Active! Fight only over the middle flags! No FOB Hunting!
  • -
  • liveEnabled

    -
    Description
    -

    Enable "Live" messages for when the server goes live.

    -
    Default
    -
    true
  • -
  • liveThreshold

    -
    Description
    -

    Player count required for "Live" messages to not bee displayed.

    -
    Default
    -
    52
  • -
  • liveMessage

    -
    Description
    -

    "Live" message to display.

    -
    Default
    -
    Live!
  • -
  • waitOnNewGames

    -
    Description
    -

    Should the plugin wait to be executed on NEW_GAME event.

    -
    Default
    -
    true
  • -
  • waitTimeOnNewGame

    -
    Description
    -

    The time to wait before check player counts in seconds.

    -
    Default
    -
    30
-
- -
- SocketIOAPI -

SocketIOAPI

-

The SocketIOAPI plugin allows remote access to a SquadJS instance via Socket.IO
As a client example you can use this to connect to the socket.io server;


-      const socket = io.connect('ws://IP:PORT', {
-        auth: {
-          token: "MySecretPassword"
-        }
-      })
-    
If you need more documentation about socket.io please go ahead and read the following;
General Socket.io documentation: Socket.io Docs
Authentication and securing your websocket: Sending-credentials
How to use, install and configure a socketIO-client: Usage Guide with Examples

-

Options

-
  • websocketPort (Required)

    -
    Description
    -

    The port for the websocket.

    -
    Default
    -
  • Example
    -
    3000
    -
  • securityToken (Required)

    -
    Description
    -

    Your secret token/password for connecting.

    -
    Default
    -
  • Example
    -
    MySecretPassword
-
- -
- TeamRandomizer -

TeamRandomizer

-

The TeamRandomizer can be used to randomize teams. It's great for destroying clan stacks or for social events. It can be run by typing, by default, !randomize into in-game admin chat

-

Options

-
  • command

    -
    Description
    -

    The command used to randomize the teams.

    -
    Default
    -
    randomize
-
-
## Statement on Accuracy diff --git a/config.json b/config.json index d0dbd46c..931609cb 100644 --- a/config.json +++ b/config.json @@ -41,16 +41,33 @@ }, "plugins": [ { - "plugin": "AutoKickUnassigned", + "plugin": "SeedingMode", "enabled": true, - "warningMessage": "Join a squad, you are unassigned and will be kicked", - "kickMessage": "Unassigned - automatically removed", - "frequencyOfWarnings": 30, - "unassignedTimer": 360, - "playerThreshold": 93, - "roundStartDelay": 900, - "ignoreAdmins": false, - "ignoreWhitelist": false + "interval": 150000, + "seedingThreshold": 50, + "seedingMessage": "Seeding Rules Active! Fight only over the middle flags! No FOB Hunting!", + "liveEnabled": true, + "liveThreshold": 52, + "liveMessage": "Live!", + "waitOnNewGames": true, + "waitTimeOnNewGame": 30 + }, + { + "plugin": "DiscordChat", + "enabled": true, + "discordClient": "discord", + "channelID": "", + "chatColors": {}, + "color": 16761867, + "ignoreChats": [ + "ChatSquad" + ] + }, + { + "plugin": "DiscordSubsystemRestarter", + "enabled": false, + "discordClient": "discord", + "role": "" }, { "plugin": "AutoTKWarn", @@ -59,11 +76,11 @@ "victimMessage": null }, { - "plugin": "CBLInfo", + "plugin": "DiscordRoundWinner", "enabled": true, "discordClient": "discord", "channelID": "", - "threshold": 6 + "color": 16761867 }, { "plugin": "ChatCommands", @@ -77,12 +94,6 @@ } ] }, - { - "plugin": "DBLog", - "enabled": false, - "database": "mysql", - "overrideServerID": null - }, { "plugin": "DiscordAdminBroadcast", "enabled": false, @@ -91,12 +102,30 @@ "color": 16761867 }, { - "plugin": "DiscordAdminCamLogs", - "enabled": false, + "plugin": "DiscordRoundEnded", + "enabled": true, "discordClient": "discord", "channelID": "", "color": 16761867 }, + { + "plugin": "AutoKickUnassigned", + "enabled": true, + "warningMessage": "Join a squad, you are unassigned and will be kicked", + "kickMessage": "Unassigned - automatically removed", + "frequencyOfWarnings": 30, + "unassignedTimer": 360, + "playerThreshold": 93, + "roundStartDelay": 900, + "ignoreAdmins": false, + "ignoreWhitelist": false + }, + { + "plugin": "SocketIOAPI", + "enabled": false, + "websocketPort": "", + "securityToken": "" + }, { "plugin": "DiscordAdminRequest", "enabled": true, @@ -112,44 +141,44 @@ "showInGameAdmins": true }, { - "plugin": "DiscordChat", + "plugin": "TeamRandomizer", "enabled": true, - "discordClient": "discord", - "channelID": "", - "chatColors": {}, - "color": 16761867, - "ignoreChats": [ - "ChatSquad" - ] + "command": "randomize" }, { - "plugin": "DiscordDebug", + "plugin": "DiscordKillFeed", "enabled": false, "discordClient": "discord", "channelID": "", - "events": [] + "color": 16761867, + "disableCBL": false }, { - "plugin": "DiscordFOBHABExplosionDamage", - "enabled": true, + "plugin": "DiscordAdminCamLogs", + "enabled": false, "discordClient": "discord", "channelID": "", "color": 16761867 }, { - "plugin": "DiscordKillFeed", + "plugin": "DBLog", "enabled": false, + "database": "mysql", + "overrideServerID": null + }, + { + "plugin": "CBLInfo", + "enabled": true, "discordClient": "discord", "channelID": "", - "color": 16761867, - "disableCBL": false + "threshold": 6 }, { - "plugin": "DiscordPlaceholder", - "enabled": true, + "plugin": "DiscordDebug", + "enabled": false, "discordClient": "discord", - "command": "!placeholder", - "channelID": "" + "channelID": "", + "events": [] }, { "plugin": "DiscordRcon", @@ -160,50 +189,49 @@ "prependAdminNameInBroadcast": false }, { - "plugin": "DiscordRoundWinner", + "plugin": "DiscordFOBHABExplosionDamage", "enabled": true, "discordClient": "discord", "channelID": "", "color": 16761867 }, { - "plugin": "DiscordRoundEnded", + "plugin": "PersistentEOSIDtoSteamID", "enabled": true, - "discordClient": "discord", - "channelID": "", - "color": 16761867 + "database": "sqlite" }, { - "plugin": "DiscordServerStatus", + "plugin": "DiscordPlaceholder", "enabled": true, "discordClient": "discord", - "messageStore": "sqlite", - "command": "!status", - "disableSubscriptions": false, - "updateInterval": 60000, - "setBotStatus": true + "command": "!placeholder", + "channelID": "" }, { - "plugin": "DiscordSquadCreated", - "enabled": false, + "plugin": "DiscordTeamkill", + "enabled": true, "discordClient": "discord", "channelID": "", "color": 16761867, - "useEmbed": true + "disableCBL": false }, { - "plugin": "DiscordSubsystemRestarter", + "plugin": "DiscordSquadCreated", "enabled": false, "discordClient": "discord", - "role": "" + "channelID": "", + "color": 16761867, + "useEmbed": true }, { - "plugin": "DiscordTeamkill", + "plugin": "DiscordServerStatus", "enabled": true, "discordClient": "discord", - "channelID": "", - "color": 16761867, - "disableCBL": false + "messageStore": "sqlite", + "command": "!status", + "disableSubscriptions": false, + "updateInterval": 60000, + "setBotStatus": true }, { "plugin": "FogOfWar", @@ -216,34 +244,6 @@ "enabled": false, "broadcasts": [], "interval": 300000 - }, - { - "plugin": "PersistentEOSIDtoSteamID", - "enabled": true, - "database": "sqlite" - }, - { - "plugin": "SeedingMode", - "enabled": true, - "interval": 150000, - "seedingThreshold": 50, - "seedingMessage": "Seeding Rules Active! Fight only over the middle flags! No FOB Hunting!", - "liveEnabled": true, - "liveThreshold": 52, - "liveMessage": "Live!", - "waitOnNewGames": true, - "waitTimeOnNewGame": 30 - }, - { - "plugin": "SocketIOAPI", - "enabled": false, - "websocketPort": "", - "securityToken": "" - }, - { - "plugin": "TeamRandomizer", - "enabled": true, - "command": "randomize" } ], "logger": { From 08bf6a4e6a19ea58b472340b1aeb3d0fd6b154a4 Mon Sep 17 00:00:00 2001 From: Marek Date: Sat, 30 Dec 2023 07:00:44 +0000 Subject: [PATCH 45/53] Linting --- core/rcon.js | 1 - 1 file changed, 1 deletion(-) diff --git a/core/rcon.js b/core/rcon.js index 15cda3a3..770aa800 100644 --- a/core/rcon.js +++ b/core/rcon.js @@ -351,7 +351,6 @@ export default class Rcon extends EventEmitter { } }); } else { - this.callbackIds.push({ id: this.count, cmd: body }); this.responseCallbackQueue.push((response) => { this.client.removeListener('error', onError); From 527411f808f8c52e5a7ce759032b42b6f8e14370 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Sat, 30 Dec 2023 22:59:27 +0100 Subject: [PATCH 46/53] chore: added IP logging on player connected event --- README.md | 578 +++++++++++++++++++++--------------------- config.json | 172 ++++++------- squad-server/index.js | 2 +- 3 files changed, 376 insertions(+), 376 deletions(-) diff --git a/README.md b/README.md index ac377c0b..21a6aeef 100644 --- a/README.md +++ b/README.md @@ -202,106 +202,50 @@ The following is a list of plugins built into SquadJS, you can click their title Interested in creating your own plugin? [See more here](./squad-server/plugins/readme.md)
- SeedingMode -

SeedingMode

-

The SeedingMode plugin broadcasts seeding rule messages to players at regular intervals when the server is below a specified player count. It can also be configured to display "Live" messages when the server goes live.

+ AutoKickUnassigned +

AutoKickUnassigned

+

The AutoKickUnassigned plugin will automatically kick players that are not in a squad after a specified ammount of time.

Options

-
  • interval

    -
    Description
    -

    Frequency of seeding messages in milliseconds.

    -
    Default
    -
    150000
  • -
  • seedingThreshold

    -
    Description
    -

    Player count required for server not to be in seeding mode.

    -
    Default
    -
    50
  • -
  • seedingMessage

    -
    Description
    -

    Seeding message to display.

    -
    Default
    -
    Seeding Rules Active! Fight only over the middle flags! No FOB Hunting!
  • -
  • liveEnabled

    -
    Description
    -

    Enable "Live" messages for when the server goes live.

    -
    Default
    -
    true
  • -
  • liveThreshold

    -
    Description
    -

    Player count required for "Live" messages to not bee displayed.

    -
    Default
    -
    52
  • -
  • liveMessage

    -
    Description
    -

    "Live" message to display.

    -
    Default
    -
    Live!
  • -
  • waitOnNewGames

    -
    Description
    -

    Should the plugin wait to be executed on NEW_GAME event.

    -
    Default
    -
    true
  • -
  • waitTimeOnNewGame

    +
    • warningMessage

      Description
      -

      The time to wait before check player counts in seconds.

      +

      Message SquadJS will send to players warning them they will be kicked

      Default
      -
      30
    -
- -
- DiscordChat -

DiscordChat

-

The DiscordChat plugin will log in-game chat to a Discord channel.

-

Options

-
  • discordClient (Required)

    +
    Join a squad, you are unassigned and will be kicked
  • +
  • kickMessage

    Description
    -

    Discord connector name.

    +

    Message to send to players when they are kicked

    Default
    -
    discord
  • -
  • channelID (Required)

    +
    Unassigned - automatically removed
  • +
  • frequencyOfWarnings

    Description
    -

    The ID of the channel to log admin broadcasts to.

    +

    How often in Seconds should we warn the player about being unassigned?

    Default
    -
  • Example
    -
    667741905228136459
    -
  • chatColors

    +
    30
  • +
  • unassignedTimer

    Description
    -

    The color of the embed for each chat.

    +

    How long in Seconds to wait before a unassigned player is kicked

    Default
    -
    {}
  • Example
    -
    {
    -  "ChatAll": 16761867
    -}
    -
  • color

    +
    360
  • +
  • playerThreshold

    Description
    -

    The color of the embed.

    +

    Player count required for AutoKick to start kicking players, set to -1 to disable

    Default
    -
    16761867
  • -
  • ignoreChats

    +
    93
  • +
  • roundStartDelay

    Description
    -

    A list of chat names to ignore.

    +

    Time delay in Seconds from start of the round before AutoKick starts kicking again

    Default
    -
    [
    -  "ChatSquad"
    -]
-
- -
- DiscordSubsystemRestarter -

DiscordSubsystemRestarter

-

The DiscordSubSystemRestarter plugin allows you to manually restart SquadJS subsystems in case an issues arises with them.

  • !squadjs restartsubsystem rcon
  • !squadjs restartsubsystem logparser

-

Options

-
  • discordClient (Required)

    +
    900
  • +
  • ignoreAdmins

    Description
    -

    Discord connector name.

    +

    • true: Admins will NOT be kicked
    • false: Admins WILL be kicked

    Default
    -
    discord
  • -
  • role (Required)

    +
    false
  • +
  • ignoreWhitelist

    Description
    -

    ID of role required to run the sub system restart commands.

    +

    • true: Reserve slot players will NOT be kicked
    • false: Reserve slot players WILL be kicked

    Default
    -
  • Example
    -
    667741905228136459
+
false
@@ -322,9 +266,9 @@ Interested in creating your own plugin? [See more here](./squad-server/plugins/r
- DiscordRoundWinner -

DiscordRoundWinner

-

The DiscordRoundWinner plugin will send the round winner to a Discord channel.

+ CBLInfo +

CBLInfo

+

The CBLInfo plugin alerts admins when a harmful player is detected joining their server based on data from the Community Ban List.

Options

  • discordClient (Required)

    Description
    @@ -333,15 +277,15 @@ Interested in creating your own plugin? [See more here](./squad-server/plugins/r
    discord
  • channelID (Required)

    Description
    -

    The ID of the channel to log admin broadcasts to.

    +

    The ID of the channel to alert admins through.

    Default
  • Example
    667741905228136459
    -
  • color

    +
  • threshold

    Description
    -

    The color of the embed.

    +

    Admins will be alerted when a player has this or more reputation points. For more information on reputation points, see the Community Ban List's FAQ

    Default
    -
    16761867
+
6
@@ -363,6 +307,30 @@ Interested in creating your own plugin? [See more here](./squad-server/plugins/r ]
+
+ DBLog +

DBLog

+

The mysql-log plugin will log various server statistics and events to a database. This is great for server performance monitoring and/or player stat tracking. + +Grafana: +

  • Grafana is a cool way of viewing server statistics stored in the database.
  • +
  • Install Grafana.
  • +
  • Add your database as a datasource named SquadJS.
  • +
  • Import the SquadJS Dashboard to get a preconfigured MySQL only Grafana dashboard.
  • +
  • Install any missing Grafana plugins.

+

Options

+
  • database (Required)

    +
    Description
    +

    The Sequelize connector to log server information to.

    +
    Default
    +
    mysql
  • +
  • overrideServerID

    +
    Description
    +

    A overridden server ID.

    +
    Default
    +
    null
+
+
DiscordAdminBroadcast

DiscordAdminBroadcast

@@ -387,9 +355,9 @@ Interested in creating your own plugin? [See more here](./squad-server/plugins/r
- DiscordRoundEnded -

DiscordRoundEnded

-

The DiscordRoundEnded plugin will send the round winner to a Discord channel.

+ DiscordAdminCamLogs +

DiscordAdminCamLogs

+

The DiscordAdminCamLogs plugin will log in game admin camera usage to a Discord channel.

Options

  • discordClient (Required)

    Description
    @@ -398,7 +366,7 @@ Interested in creating your own plugin? [See more here](./squad-server/plugins/r
    discord
  • channelID (Required)

    Description
    -

    The ID of the channel to log round end events to.

    +

    The ID of the channel to log admin camera usage to.

    Default
  • Example
    667741905228136459
    @@ -409,78 +377,6 @@ Interested in creating your own plugin? [See more here](./squad-server/plugins/r
    16761867
-
- AutoKickUnassigned -

AutoKickUnassigned

-

The AutoKickUnassigned plugin will automatically kick players that are not in a squad after a specified ammount of time.

-

Options

-
  • warningMessage

    -
    Description
    -

    Message SquadJS will send to players warning them they will be kicked

    -
    Default
    -
    Join a squad, you are unassigned and will be kicked
  • -
  • kickMessage

    -
    Description
    -

    Message to send to players when they are kicked

    -
    Default
    -
    Unassigned - automatically removed
  • -
  • frequencyOfWarnings

    -
    Description
    -

    How often in Seconds should we warn the player about being unassigned?

    -
    Default
    -
    30
  • -
  • unassignedTimer

    -
    Description
    -

    How long in Seconds to wait before a unassigned player is kicked

    -
    Default
    -
    360
  • -
  • playerThreshold

    -
    Description
    -

    Player count required for AutoKick to start kicking players, set to -1 to disable

    -
    Default
    -
    93
  • -
  • roundStartDelay

    -
    Description
    -

    Time delay in Seconds from start of the round before AutoKick starts kicking again

    -
    Default
    -
    900
  • -
  • ignoreAdmins

    -
    Description
    -

    • true: Admins will NOT be kicked
    • false: Admins WILL be kicked

    -
    Default
    -
    false
  • -
  • ignoreWhitelist

    -
    Description
    -

    • true: Reserve slot players will NOT be kicked
    • false: Reserve slot players WILL be kicked

    -
    Default
    -
    false
-
- -
- SocketIOAPI -

SocketIOAPI

-

The SocketIOAPI plugin allows remote access to a SquadJS instance via Socket.IO
As a client example you can use this to connect to the socket.io server;


-      const socket = io.connect('ws://IP:PORT', {
-        auth: {
-          token: "MySecretPassword"
-        }
-      })
-    
If you need more documentation about socket.io please go ahead and read the following;
General Socket.io documentation: Socket.io Docs
Authentication and securing your websocket: Sending-credentials
How to use, install and configure a socketIO-client: Usage Guide with Examples

-

Options

-
  • websocketPort (Required)

    -
    Description
    -

    The port for the websocket.

    -
    Default
    -
  • Example
    -
    3000
    -
  • securityToken (Required)

    -
    Description
    -

    Your secret token/password for connecting.

    -
    Default
    -
  • Example
    -
    MySecretPassword
-
-
DiscordAdminRequest

DiscordAdminRequest

@@ -549,21 +445,9 @@ Interested in creating your own plugin? [See more here](./squad-server/plugins/r
- TeamRandomizer -

TeamRandomizer

-

The TeamRandomizer can be used to randomize teams. It's great for destroying clan stacks or for social events. It can be run by typing, by default, !randomize into in-game admin chat

-

Options

-
  • command

    -
    Description
    -

    The command used to randomize the teams.

    -
    Default
    -
    randomize
-
- -
- DiscordKillFeed -

DiscordKillFeed

-

The DiscordKillFeed plugin logs all wounds and related information to a Discord channel for admins to review.

+ DiscordChat +

DiscordChat

+

The DiscordChat plugin will log in-game chat to a Discord channel.

Options

  • discordClient (Required)

    Description
    @@ -572,26 +456,36 @@ Interested in creating your own plugin? [See more here](./squad-server/plugins/r
    discord
  • channelID (Required)

    Description
    -

    The ID of the channel to log teamkills to.

    +

    The ID of the channel to log admin broadcasts to.

    Default
  • Example
    667741905228136459
    -
  • color

    +
  • chatColors

    Description
    -

    The color of the embeds.

    +

    The color of the embed for each chat.

    Default
    -
    16761867
  • -
  • disableCBL

    -
    Description
    -

    Disable Community Ban List information.

    +
    {}
  • Example
    +
    {
    +  "ChatAll": 16761867
    +}
    +
  • color

    +
    Description
    +

    The color of the embed.

    Default
    -
    false
+
16761867
+
  • ignoreChats

    +
    Description
    +

    A list of chat names to ignore.

    +
    Default
    +
    [
    +  "ChatSquad"
    +]
  • - DiscordAdminCamLogs -

    DiscordAdminCamLogs

    -

    The DiscordAdminCamLogs plugin will log in game admin camera usage to a Discord channel.

    + DiscordDebug +

    DiscordDebug

    +

    The DiscordDebug plugin can be used to help debug SquadJS by dumping SquadJS events to a Discord channel.

    Options

    • discordClient (Required)

      Description
      @@ -600,45 +494,47 @@ Interested in creating your own plugin? [See more here](./squad-server/plugins/r
      discord
    • channelID (Required)

      Description
      -

      The ID of the channel to log admin camera usage to.

      +

      The ID of the channel to log events to.

      Default
    • Example
      667741905228136459
      -
    • color

      +
    • events (Required)

      Description
      -

      The color of the embed.

      +

      A list of events to dump.

      Default
      -
      16761867
    +
    []
    Example
    +
    [
    +  "PLAYER_DIED"
    +]
    - DBLog -

    DBLog

    -

    The mysql-log plugin will log various server statistics and events to a database. This is great for server performance monitoring and/or player stat tracking. - -Grafana: -

    • Grafana is a cool way of viewing server statistics stored in the database.
    • -
    • Install Grafana.
    • -
    • Add your database as a datasource named SquadJS.
    • -
    • Import the SquadJS Dashboard to get a preconfigured MySQL only Grafana dashboard.
    • -
    • Install any missing Grafana plugins.

    + DiscordFOBHABExplosionDamage +

    DiscordFOBHABExplosionDamage

    +

    The DiscordFOBHABExplosionDamage plugin logs damage done to FOBs and HABs by explosions to help identify engineers blowing up friendly FOBs and HABs.

    Options

    -
    • database (Required)

      +
      • discordClient (Required)

        Description
        -

        The Sequelize connector to log server information to.

        +

        Discord connector name.

        Default
        -
        mysql
      • -
      • overrideServerID

        +
        discord
      • +
      • channelID (Required)

        Description
        -

        A overridden server ID.

        +

        The ID of the channel to log FOB/HAB explosion damage to.

        Default
        -
        null
      +
    • Example
      +
      667741905228136459
      +
    • color

      +
      Description
      +

      The color of the embeds.

      +
      Default
      +
      16761867
    - CBLInfo -

    CBLInfo

    -

    The CBLInfo plugin alerts admins when a harmful player is detected joining their server based on data from the Community Ban List.

    + DiscordKillFeed +

    DiscordKillFeed

    +

    The DiscordKillFeed plugin logs all wounds and related information to a Discord channel for admins to review.

    Options

    • discordClient (Required)

      Description
      @@ -647,41 +543,42 @@ Grafana:
      discord
    • channelID (Required)

      Description
      -

      The ID of the channel to alert admins through.

      +

      The ID of the channel to log teamkills to.

      Default
    • Example
      667741905228136459
      -
    • threshold

      +
    • color

      Description
      -

      Admins will be alerted when a player has this or more reputation points. For more information on reputation points, see the Community Ban List's FAQ

      +

      The color of the embeds.

      Default
      -
      6
    +
    16761867
    +
  • disableCBL

    +
    Description
    +

    Disable Community Ban List information.

    +
    Default
    +
    false
  • - DiscordDebug -

    DiscordDebug

    -

    The DiscordDebug plugin can be used to help debug SquadJS by dumping SquadJS events to a Discord channel.

    + DiscordPlaceholder +

    DiscordPlaceholder

    +

    The DiscordPlaceholder plugin allows you to make your bot create placeholder messages that can be used when configuring other plugins.

    Options

    • discordClient (Required)

      Description

      Discord connector name.

      Default
      discord
    • -
    • channelID (Required)

      +
    • command

      Description
      -

      The ID of the channel to log events to.

      +

      Command to create Discord placeholder.

      Default
      -
    • Example
      -
      667741905228136459
      -
    • events (Required)

      +
      !placeholder
    • +
    • channelID (Required)

      Description
      -

      A list of events to dump.

      +

      The bot will only answer with a placeholder on this channel

      Default
      -
      []
    • Example
      -
      [
      -  "PLAYER_DIED"
      -]
    +
    @@ -720,9 +617,9 @@ Grafana:
    - DiscordFOBHABExplosionDamage -

    DiscordFOBHABExplosionDamage

    -

    The DiscordFOBHABExplosionDamage plugin logs damage done to FOBs and HABs by explosions to help identify engineers blowing up friendly FOBs and HABs.

    + DiscordRoundWinner +

    DiscordRoundWinner

    +

    The DiscordRoundWinner plugin will send the round winner to a Discord channel.

    Options

    • discordClient (Required)

      Description
      @@ -731,77 +628,75 @@ Grafana:
      discord
    • channelID (Required)

      Description
      -

      The ID of the channel to log FOB/HAB explosion damage to.

      +

      The ID of the channel to log admin broadcasts to.

      Default
    • Example
      667741905228136459
    • color

      Description
      -

      The color of the embeds.

      +

      The color of the embed.

      Default
      16761867
    - PersistentEOSIDtoSteamID -

    PersistentEOSIDtoSteamID

    -

    Stores into a DB every association of SteamID-EOSID

    -

    Options

    -
    • database (Required)

      -
      Description
      -

      The Sequelize connector.

      -
      Default
      -
      sqlite
    -
    - -
    - DiscordPlaceholder -

    DiscordPlaceholder

    -

    The DiscordPlaceholder plugin allows you to make your bot create placeholder messages that can be used when configuring other plugins.

    + DiscordRoundEnded +

    DiscordRoundEnded

    +

    The DiscordRoundEnded plugin will send the round winner to a Discord channel.

    Options

    • discordClient (Required)

      Description

      Discord connector name.

      Default
      discord
    • -
    • command

      +
    • channelID (Required)

      Description
      -

      Command to create Discord placeholder.

      +

      The ID of the channel to log round end events to.

      Default
      -
      !placeholder
    • -
    • channelID (Required)

      +
    • Example
      +
      667741905228136459
      +
    • color

      Description
      -

      The bot will only answer with a placeholder on this channel

      +

      The color of the embed.

      Default
      -
    +
    16761867
    - DiscordTeamkill -

    DiscordTeamkill

    -

    The DiscordTeamkill plugin logs teamkills and related information to a Discord channel for admins to review.

    + DiscordServerStatus +

    DiscordServerStatus

    +

    The DiscordServerStatus plugin can be used to get the server status in Discord.

    Options

    • discordClient (Required)

      Description

      Discord connector name.

      Default
      discord
    • -
    • channelID (Required)

      +
    • messageStore (Required)

      Description
      -

      The ID of the channel to log teamkills to.

      +

      Sequelize connector name.

      Default
      -
    • Example
      -
      667741905228136459
      -
    • color

      +
      sqlite
    • +
    • command

      Description
      -

      The color of the embeds.

      +

      Command name to get message.

      Default
      -
      16761867
    • -
    • disableCBL

      +
      !status
    • +
    • disableSubscriptions

      Description
      -

      Disable Community Ban List information.

      +

      Whether to allow messages to be subscribed to automatic updates.

      Default
      -
      false
    +
    false
    +
  • updateInterval

    +
    Description
    +

    How frequently to update the time in Discord.

    +
    Default
    +
    60000
  • +
  • setBotStatus

    +
    Description
    +

    Whether to update the bot's status with server information.

    +
    Default
    +
    true
  • @@ -833,40 +728,49 @@ Grafana:
    - DiscordServerStatus -

    DiscordServerStatus

    -

    The DiscordServerStatus plugin can be used to get the server status in Discord.

    + DiscordSubsystemRestarter +

    DiscordSubsystemRestarter

    +

    The DiscordSubSystemRestarter plugin allows you to manually restart SquadJS subsystems in case an issues arises with them.

    • !squadjs restartsubsystem rcon
    • !squadjs restartsubsystem logparser

    Options

    • discordClient (Required)

      Description

      Discord connector name.

      Default
      discord
    • -
    • messageStore (Required)

      +
    • role (Required)

      Description
      -

      Sequelize connector name.

      +

      ID of role required to run the sub system restart commands.

      Default
      -
      sqlite
    • -
    • command

      +
    • Example
      +
      667741905228136459
    +
    + +
    + DiscordTeamkill +

    DiscordTeamkill

    +

    The DiscordTeamkill plugin logs teamkills and related information to a Discord channel for admins to review.

    +

    Options

    +
    • discordClient (Required)

      Description
      -

      Command name to get message.

      +

      Discord connector name.

      Default
      -
      !status
    • -
    • disableSubscriptions

      +
      discord
    • +
    • channelID (Required)

      Description
      -

      Whether to allow messages to be subscribed to automatic updates.

      +

      The ID of the channel to log teamkills to.

      Default
      -
      false
    • -
    • updateInterval

      +
    • Example
      +
      667741905228136459
      +
    • color

      Description
      -

      How frequently to update the time in Discord.

      +

      The color of the embeds.

      Default
      -
      60000
    • -
    • setBotStatus

      +
      16761867
    • +
    • disableCBL

      Description
      -

      Whether to update the bot's status with server information.

      +

      Disable Community Ban List information.

      Default
      -
      true
    +
    false
    @@ -906,6 +810,102 @@ Grafana:
    300000
    +
    + PersistentEOSIDtoSteamID +

    PersistentEOSIDtoSteamID

    +

    Stores into a DB every association of SteamID-EOSID

    +

    Options

    +
    • database (Required)

      +
      Description
      +

      The Sequelize connector.

      +
      Default
      +
      sqlite
    +
    + +
    + SeedingMode +

    SeedingMode

    +

    The SeedingMode plugin broadcasts seeding rule messages to players at regular intervals when the server is below a specified player count. It can also be configured to display "Live" messages when the server goes live.

    +

    Options

    +
    • interval

      +
      Description
      +

      Frequency of seeding messages in milliseconds.

      +
      Default
      +
      150000
    • +
    • seedingThreshold

      +
      Description
      +

      Player count required for server not to be in seeding mode.

      +
      Default
      +
      50
    • +
    • seedingMessage

      +
      Description
      +

      Seeding message to display.

      +
      Default
      +
      Seeding Rules Active! Fight only over the middle flags! No FOB Hunting!
    • +
    • liveEnabled

      +
      Description
      +

      Enable "Live" messages for when the server goes live.

      +
      Default
      +
      true
    • +
    • liveThreshold

      +
      Description
      +

      Player count required for "Live" messages to not bee displayed.

      +
      Default
      +
      52
    • +
    • liveMessage

      +
      Description
      +

      "Live" message to display.

      +
      Default
      +
      Live!
    • +
    • waitOnNewGames

      +
      Description
      +

      Should the plugin wait to be executed on NEW_GAME event.

      +
      Default
      +
      true
    • +
    • waitTimeOnNewGame

      +
      Description
      +

      The time to wait before check player counts in seconds.

      +
      Default
      +
      30
    +
    + +
    + SocketIOAPI +

    SocketIOAPI

    +

    The SocketIOAPI plugin allows remote access to a SquadJS instance via Socket.IO
    As a client example you can use this to connect to the socket.io server;

    
    +      const socket = io.connect('ws://IP:PORT', {
    +        auth: {
    +          token: "MySecretPassword"
    +        }
    +      })
    +    
    If you need more documentation about socket.io please go ahead and read the following;
    General Socket.io documentation: Socket.io Docs
    Authentication and securing your websocket: Sending-credentials
    How to use, install and configure a socketIO-client: Usage Guide with Examples

    +

    Options

    +
    • websocketPort (Required)

      +
      Description
      +

      The port for the websocket.

      +
      Default
      +
    • Example
      +
      3000
      +
    • securityToken (Required)

      +
      Description
      +

      Your secret token/password for connecting.

      +
      Default
      +
    • Example
      +
      MySecretPassword
    +
    + +
    + TeamRandomizer +

    TeamRandomizer

    +

    The TeamRandomizer can be used to randomize teams. It's great for destroying clan stacks or for social events. It can be run by typing, by default, !randomize into in-game admin chat

    +

    Options

    +
    • command

      +
      Description
      +

      The command used to randomize the teams.

      +
      Default
      +
      randomize
    +
    +
    ## Statement on Accuracy diff --git a/config.json b/config.json index 931609cb..d0dbd46c 100644 --- a/config.json +++ b/config.json @@ -41,33 +41,16 @@ }, "plugins": [ { - "plugin": "SeedingMode", - "enabled": true, - "interval": 150000, - "seedingThreshold": 50, - "seedingMessage": "Seeding Rules Active! Fight only over the middle flags! No FOB Hunting!", - "liveEnabled": true, - "liveThreshold": 52, - "liveMessage": "Live!", - "waitOnNewGames": true, - "waitTimeOnNewGame": 30 - }, - { - "plugin": "DiscordChat", + "plugin": "AutoKickUnassigned", "enabled": true, - "discordClient": "discord", - "channelID": "", - "chatColors": {}, - "color": 16761867, - "ignoreChats": [ - "ChatSquad" - ] - }, - { - "plugin": "DiscordSubsystemRestarter", - "enabled": false, - "discordClient": "discord", - "role": "" + "warningMessage": "Join a squad, you are unassigned and will be kicked", + "kickMessage": "Unassigned - automatically removed", + "frequencyOfWarnings": 30, + "unassignedTimer": 360, + "playerThreshold": 93, + "roundStartDelay": 900, + "ignoreAdmins": false, + "ignoreWhitelist": false }, { "plugin": "AutoTKWarn", @@ -76,11 +59,11 @@ "victimMessage": null }, { - "plugin": "DiscordRoundWinner", + "plugin": "CBLInfo", "enabled": true, "discordClient": "discord", "channelID": "", - "color": 16761867 + "threshold": 6 }, { "plugin": "ChatCommands", @@ -94,6 +77,12 @@ } ] }, + { + "plugin": "DBLog", + "enabled": false, + "database": "mysql", + "overrideServerID": null + }, { "plugin": "DiscordAdminBroadcast", "enabled": false, @@ -102,30 +91,12 @@ "color": 16761867 }, { - "plugin": "DiscordRoundEnded", - "enabled": true, + "plugin": "DiscordAdminCamLogs", + "enabled": false, "discordClient": "discord", "channelID": "", "color": 16761867 }, - { - "plugin": "AutoKickUnassigned", - "enabled": true, - "warningMessage": "Join a squad, you are unassigned and will be kicked", - "kickMessage": "Unassigned - automatically removed", - "frequencyOfWarnings": 30, - "unassignedTimer": 360, - "playerThreshold": 93, - "roundStartDelay": 900, - "ignoreAdmins": false, - "ignoreWhitelist": false - }, - { - "plugin": "SocketIOAPI", - "enabled": false, - "websocketPort": "", - "securityToken": "" - }, { "plugin": "DiscordAdminRequest", "enabled": true, @@ -141,44 +112,44 @@ "showInGameAdmins": true }, { - "plugin": "TeamRandomizer", + "plugin": "DiscordChat", "enabled": true, - "command": "randomize" - }, - { - "plugin": "DiscordKillFeed", - "enabled": false, "discordClient": "discord", "channelID": "", + "chatColors": {}, "color": 16761867, - "disableCBL": false + "ignoreChats": [ + "ChatSquad" + ] }, { - "plugin": "DiscordAdminCamLogs", + "plugin": "DiscordDebug", "enabled": false, "discordClient": "discord", "channelID": "", - "color": 16761867 - }, - { - "plugin": "DBLog", - "enabled": false, - "database": "mysql", - "overrideServerID": null + "events": [] }, { - "plugin": "CBLInfo", + "plugin": "DiscordFOBHABExplosionDamage", "enabled": true, "discordClient": "discord", "channelID": "", - "threshold": 6 + "color": 16761867 }, { - "plugin": "DiscordDebug", + "plugin": "DiscordKillFeed", "enabled": false, "discordClient": "discord", "channelID": "", - "events": [] + "color": 16761867, + "disableCBL": false + }, + { + "plugin": "DiscordPlaceholder", + "enabled": true, + "discordClient": "discord", + "command": "!placeholder", + "channelID": "" }, { "plugin": "DiscordRcon", @@ -189,31 +160,28 @@ "prependAdminNameInBroadcast": false }, { - "plugin": "DiscordFOBHABExplosionDamage", + "plugin": "DiscordRoundWinner", "enabled": true, "discordClient": "discord", "channelID": "", "color": 16761867 }, { - "plugin": "PersistentEOSIDtoSteamID", - "enabled": true, - "database": "sqlite" - }, - { - "plugin": "DiscordPlaceholder", + "plugin": "DiscordRoundEnded", "enabled": true, "discordClient": "discord", - "command": "!placeholder", - "channelID": "" + "channelID": "", + "color": 16761867 }, { - "plugin": "DiscordTeamkill", + "plugin": "DiscordServerStatus", "enabled": true, "discordClient": "discord", - "channelID": "", - "color": 16761867, - "disableCBL": false + "messageStore": "sqlite", + "command": "!status", + "disableSubscriptions": false, + "updateInterval": 60000, + "setBotStatus": true }, { "plugin": "DiscordSquadCreated", @@ -224,14 +192,18 @@ "useEmbed": true }, { - "plugin": "DiscordServerStatus", + "plugin": "DiscordSubsystemRestarter", + "enabled": false, + "discordClient": "discord", + "role": "" + }, + { + "plugin": "DiscordTeamkill", "enabled": true, "discordClient": "discord", - "messageStore": "sqlite", - "command": "!status", - "disableSubscriptions": false, - "updateInterval": 60000, - "setBotStatus": true + "channelID": "", + "color": 16761867, + "disableCBL": false }, { "plugin": "FogOfWar", @@ -244,6 +216,34 @@ "enabled": false, "broadcasts": [], "interval": 300000 + }, + { + "plugin": "PersistentEOSIDtoSteamID", + "enabled": true, + "database": "sqlite" + }, + { + "plugin": "SeedingMode", + "enabled": true, + "interval": 150000, + "seedingThreshold": 50, + "seedingMessage": "Seeding Rules Active! Fight only over the middle flags! No FOB Hunting!", + "liveEnabled": true, + "liveThreshold": 52, + "liveMessage": "Live!", + "waitOnNewGames": true, + "waitTimeOnNewGame": 30 + }, + { + "plugin": "SocketIOAPI", + "enabled": false, + "websocketPort": "", + "securityToken": "" + }, + { + "plugin": "TeamRandomizer", + "enabled": true, + "command": "randomize" } ], "logger": { diff --git a/squad-server/index.js b/squad-server/index.js index 77a9a269..16ed6611 100644 --- a/squad-server/index.js +++ b/squad-server/index.js @@ -212,7 +212,7 @@ export default class SquadServer extends EventEmitter { Logger.verbose( 'SquadServer', 1, - `Player connected ${data.playerSuffix} - SteamID: ${data.steamID} - EOSID: ${data.eosID}` + `Player connected ${data.playerSuffix} - SteamID: ${data.steamID} - EOSID: ${data.eosID} - IP: ${data.ip}` ); data.player = await this.getPlayerByEOSID(data.eosID); From 1ff648d1aa11eefd3c2f76f07aba1ae91773b70e Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Sat, 30 Dec 2023 23:35:55 +0100 Subject: [PATCH 47/53] feat: introduced model Player, data+constraints migration from SteamUser to Player --- squad-server/plugins/db-log.js | 252 ++++++++++++++++++++++++++++----- 1 file changed, 216 insertions(+), 36 deletions(-) diff --git a/squad-server/plugins/db-log.js b/squad-server/plugins/db-log.js index e3eb64af..2e681632 100644 --- a/squad-server/plugins/db-log.js +++ b/squad-server/plugins/db-log.js @@ -2,7 +2,7 @@ import Sequelize from 'sequelize'; import BasePlugin from './base-plugin.js'; -const { DataTypes } = Sequelize; +const { DataTypes, QueryTypes } = Sequelize; export default class DBLog extends BasePlugin { static get description() { @@ -146,6 +146,44 @@ export default class DBLog extends BasePlugin { } ); + this.createModel( + 'Player', + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + eosID: { + type: DataTypes.STRING, + unique: true + }, + steamID: { + type: DataTypes.STRING, + notNull: true, + unique: true + }, + lastName: { + type: DataTypes.STRING + }, + lastIP: { + type: DataTypes.STRING + } + }, + { + charset: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + indexes: [ + { + fields: ['eosID'] + }, + { + fields: ['steamID'] + } + ] + } + ); + this.createModel( 'Wound', { @@ -329,37 +367,44 @@ export default class DBLog extends BasePlugin { onDelete: 'CASCADE' }); - this.models.SteamUser.hasMany(this.models.Wound, { + this.models.Player.hasMany(this.models.Wound, { + sourceKey: 'steamID', foreignKey: { name: 'attacker' }, onDelete: 'CASCADE' }); - this.models.SteamUser.hasMany(this.models.Wound, { + this.models.Player.hasMany(this.models.Wound, { + sourceKey: 'steamID', foreignKey: { name: 'victim' }, onDelete: 'CASCADE' }); - this.models.SteamUser.hasMany(this.models.Death, { + this.models.Player.hasMany(this.models.Death, { + sourceKey: 'steamID', foreignKey: { name: 'attacker' }, onDelete: 'CASCADE' }); - this.models.SteamUser.hasMany(this.models.Death, { + this.models.Player.hasMany(this.models.Death, { + sourceKey: 'steamID', foreignKey: { name: 'victim' }, onDelete: 'CASCADE' }); - this.models.SteamUser.hasMany(this.models.Revive, { + this.models.Player.hasMany(this.models.Revive, { + sourceKey: 'steamID', foreignKey: { name: 'attacker' }, onDelete: 'CASCADE' }); - this.models.SteamUser.hasMany(this.models.Revive, { + this.models.Player.hasMany(this.models.Revive, { + sourceKey: 'steamID', foreignKey: { name: 'victim' }, onDelete: 'CASCADE' }); - this.models.SteamUser.hasMany(this.models.Revive, { + this.models.Player.hasMany(this.models.Revive, { + sourceKey: 'steamID', foreignKey: { name: 'reviver' }, onDelete: 'CASCADE' }); @@ -392,9 +437,12 @@ export default class DBLog extends BasePlugin { this.onTickRate = this.onTickRate.bind(this); this.onUpdatedA2SInformation = this.onUpdatedA2SInformation.bind(this); this.onNewGame = this.onNewGame.bind(this); + this.onPlayerConnected = this.onPlayerConnected.bind(this); this.onPlayerWounded = this.onPlayerWounded.bind(this); this.onPlayerDied = this.onPlayerDied.bind(this); this.onPlayerRevived = this.onPlayerRevived.bind(this); + this.migrateSteamUsersIntoPlayers = this.migrateSteamUsersIntoPlayers.bind(this); + this.dropAllForeignKeys = this.dropAllForeignKeys.bind(this); } createModel(name, schema) { @@ -409,12 +457,15 @@ export default class DBLog extends BasePlugin { await this.models.TickRate.sync(); await this.models.PlayerCount.sync(); await this.models.SteamUser.sync(); + await this.models.Player.sync(); await this.models.Wound.sync(); await this.models.Death.sync(); await this.models.Revive.sync(); } async mount() { + await this.migrateSteamUsersIntoPlayers(); + await this.models.Server.upsert({ id: this.options.overrideServerID || this.server.id, name: this.server.serverName @@ -427,6 +478,7 @@ export default class DBLog extends BasePlugin { this.server.on('TICK_RATE', this.onTickRate); this.server.on('UPDATED_A2S_INFORMATION', this.onUpdatedA2SInformation); this.server.on('NEW_GAME', this.onNewGame); + this.server.on('PLAYER_CONNECTED', this.onPlayerConnected); this.server.on('PLAYER_WOUNDED', this.onPlayerWounded); this.server.on('PLAYER_DIED', this.onPlayerDied); this.server.on('PLAYER_REVIVED', this.onPlayerRevived); @@ -436,6 +488,7 @@ export default class DBLog extends BasePlugin { this.server.removeEventListener('TICK_RATE', this.onTickRate); this.server.removeEventListener('UPDATED_A2S_INFORMATION', this.onTickRate); this.server.removeEventListener('NEW_GAME', this.onNewGame); + this.server.removeEventListener('PLAYER_CONNECTED', this.onPlayerConnected); this.server.removeEventListener('PLAYER_WOUNDED', this.onPlayerWounded); this.server.removeEventListener('PLAYER_DIED', this.onPlayerDied); this.server.removeEventListener('PLAYER_REVIVED', this.onPlayerRevived); @@ -479,15 +532,27 @@ export default class DBLog extends BasePlugin { async onPlayerWounded(info) { if (info.attacker) - await this.models.SteamUser.upsert({ - steamID: info.attacker.steamID, - lastName: info.attacker.name - }); + await this.models.Player.upsert( + { + eosID: info.attacker.eosID, + steamID: info.attacker.steamID, + lastName: info.attacker.name + }, + { + conflictFields: ['steamID'] + } + ); if (info.victim) - await this.models.SteamUser.upsert({ - steamID: info.victim.steamID, - lastName: info.victim.name - }); + await this.models.Player.upsert( + { + eosID: info.victim.eosID, + steamID: info.victim.steamID, + lastName: info.victim.name + }, + { + conflictFields: ['steamID'] + } + ); await this.models.Wound.create({ server: this.options.overrideServerID || this.server.id, @@ -509,15 +574,27 @@ export default class DBLog extends BasePlugin { async onPlayerDied(info) { if (info.attacker) - await this.models.SteamUser.upsert({ - steamID: info.attacker.steamID, - lastName: info.attacker.name - }); + await this.models.Player.upsert( + { + eosID: info.attacker.eosID, + steamID: info.attacker.steamID, + lastName: info.attacker.name + }, + { + conflictFields: ['steamID'] + } + ); if (info.victim) - await this.models.SteamUser.upsert({ - steamID: info.victim.steamID, - lastName: info.victim.name - }); + await this.models.Player.upsert( + { + eosID: info.victim.eosID, + steamID: info.victim.steamID, + lastName: info.victim.name + }, + { + conflictFields: ['steamID'] + } + ); await this.models.Death.create({ server: this.options.overrideServerID || this.server.id, @@ -540,20 +617,38 @@ export default class DBLog extends BasePlugin { async onPlayerRevived(info) { if (info.attacker) - await this.models.SteamUser.upsert({ - steamID: info.attacker.steamID, - lastName: info.attacker.name - }); + await this.models.Player.upsert( + { + eosID: info.attacker.eosID, + steamID: info.attacker.steamID, + lastName: info.attacker.name + }, + { + conflictFields: ['steamID'] + } + ); if (info.victim) - await this.models.SteamUser.upsert({ - steamID: info.victim.steamID, - lastName: info.victim.name - }); + await this.models.Player.upsert( + { + eosID: info.victim.eosID, + steamID: info.victim.steamID, + lastName: info.victim.name + }, + { + conflictFields: ['steamID'] + } + ); if (info.reviver) - await this.models.SteamUser.upsert({ - steamID: info.reviver.steamID, - lastName: info.reviver.name - }); + await this.models.Player.upsert( + { + eosID: info.reviver.eosID, + steamID: info.reviver.steamID, + lastName: info.reviver.name + }, + { + conflictFields: ['steamID'] + } + ); await this.models.Revive.create({ server: this.options.overrideServerID || this.server.id, @@ -577,4 +672,89 @@ export default class DBLog extends BasePlugin { reviverSquadID: info.reviver ? info.reviver.squadID : null }); } + + async onPlayerConnected(info) { + await this.models.Player.upsert( + { + eosID: info.eosID, + steamID: info.player.steamID, + lastName: info.player.name, + lastIP: info.ip + }, + { + conflictFields: ['steamID'] + } + ); + } + + async migrateSteamUsersIntoPlayers() { + try { + const steamUsersCount = await this.models.SteamUser.count(); + const playersCount = await this.models.Player.count(); + + if (steamUsersCount < playersCount) { + this.verbose( + 1, + `Skipping migration from SteamUsers to Players due to a previous successful migration.` + ); + return; + } + + await this.dropAllForeignKeys(); + + const steamUsers = (await this.models.SteamUser.findAll()).map((u) => u.dataValues); + await this.models.Player.bulkCreate(steamUsers); + + this.verbose(1, `Migration from SteamUsers to Players successful`); + } catch (error) { + this.verbose(1, `Error during Migration from SteamUsers to Players: ${error}`); + } + } + + async dropAllForeignKeys() { + this.verbose( + 1, + `Starting to drop constraints on DB: ${this.options.database.config.database} related to DBLog_SteamUsers deptecated table.` + ); + for (const modelName in this.models) { + const model = this.models[modelName]; + const tableName = model.tableName; + + try { + const result = await this.options.database.query( + `SELECT * FROM information_schema.key_column_usage WHERE referenced_table_name IS NOT NULL AND table_schema = '${this.options.database.config.database}' AND table_name = '${tableName}';`, + { type: QueryTypes.SELECT } + ); + + for (const r of result) { + if (r.REFERENCED_TABLE_NAME === 'DBLog_SteamUsers') { + this.verbose( + 1, + `Found constraint ${r.COLUMN_NAME} on table ${tableName}, referencing ${r.REFERENCED_COLUMN_NAME} on ${r.REFERENCED_TABLE_NAME}` + ); + + await this.options.database + .query(`ALTER TABLE ${tableName} DROP FOREIGN KEY ${r.CONSTRAINT_NAME}`, { + type: QueryTypes.RAW + }) + .then(() => { + this.verbose(1, `Dropped foreign key ${r.COLUMN_NAME} on table ${tableName}`); + }) + .catch((e) => { + this.verbose( + 1, + `Error dropping foreign key ${r.COLUMN_NAME} on table ${tableName}:`, + e + ); + }); + } + } + } catch (error) { + this.verbose(1, `Error dropping foreign keys for table ${tableName}:`, error); + } finally { + model.sync(); + } + } + await this.models.Player.sync(); + } } From bde69a8d7356a8329fbfcb3c5e6b1e5e4c8506b5 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Tue, 2 Jan 2024 16:16:04 +0100 Subject: [PATCH 48/53] refactor: renamed property "playerEOSID" into "eosID" in squad-server/log-parser/player-disconnected.js --- squad-server/index.js | 2 +- squad-server/log-parser/player-disconnected.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/squad-server/index.js b/squad-server/index.js index 16ed6611..840df26e 100644 --- a/squad-server/index.js +++ b/squad-server/index.js @@ -225,7 +225,7 @@ export default class SquadServer extends EventEmitter { }); this.logParser.on('PLAYER_DISCONNECTED', async (data) => { - data.player = await this.getPlayerByEOSID(data.playerEOSID); + data.player = await this.getPlayerByEOSID(data.eosID); delete data.steamID; diff --git a/squad-server/log-parser/player-disconnected.js b/squad-server/log-parser/player-disconnected.js index 8302c078..20ef0100 100644 --- a/squad-server/log-parser/player-disconnected.js +++ b/squad-server/log-parser/player-disconnected.js @@ -8,7 +8,7 @@ export default { chainID: args[2], ip: args[3], playerController: args[4], - playerEOSID: args[5] + eosID: args[5] }; logParser.eventStore.disconnected[data.steamID] = true; From ee90c318da448511fec5d75cc280548a9d305f92 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Tue, 2 Jan 2024 16:17:57 +0100 Subject: [PATCH 49/53] chore: remove unused persistent-eosid-to-steamid plugin --- README.md | 12 --- config.json | 5 - .../plugins/persistent-eosid-to-steamid.js | 91 ------------------- 3 files changed, 108 deletions(-) delete mode 100644 squad-server/plugins/persistent-eosid-to-steamid.js diff --git a/README.md b/README.md index 21a6aeef..b137e4f9 100644 --- a/README.md +++ b/README.md @@ -810,18 +810,6 @@ Grafana:
    300000
    -
    - PersistentEOSIDtoSteamID -

    PersistentEOSIDtoSteamID

    -

    Stores into a DB every association of SteamID-EOSID

    -

    Options

    -
    • database (Required)

      -
      Description
      -

      The Sequelize connector.

      -
      Default
      -
      sqlite
    -
    -
    SeedingMode

    SeedingMode

    diff --git a/config.json b/config.json index d0dbd46c..c3e82bc6 100644 --- a/config.json +++ b/config.json @@ -217,11 +217,6 @@ "broadcasts": [], "interval": 300000 }, - { - "plugin": "PersistentEOSIDtoSteamID", - "enabled": true, - "database": "sqlite" - }, { "plugin": "SeedingMode", "enabled": true, diff --git a/squad-server/plugins/persistent-eosid-to-steamid.js b/squad-server/plugins/persistent-eosid-to-steamid.js deleted file mode 100644 index b83458df..00000000 --- a/squad-server/plugins/persistent-eosid-to-steamid.js +++ /dev/null @@ -1,91 +0,0 @@ -import Sequelize from 'sequelize'; - -import BasePlugin from './base-plugin.js'; - -const { DataTypes } = Sequelize; - -export default class PersistentEOSIDtoSteamID extends BasePlugin { - static get description() { - return 'Stores into a DB every association of SteamID-EOSID'; - } - - static get defaultEnabled() { - return true; - } - - static get optionsSpecification() { - return { - database: { - required: true, - connector: 'sequelize', - description: 'The Sequelize connector.', - default: 'sqlite' - } - }; - } - - constructor(server, options, connectors) { - super(server, options, connectors); - - this.models = {}; - - this.createModel( - 'SteamIDtoEOSID', - { - steamID: { - type: DataTypes.STRING, - primaryKey: true - }, - eosID: { - type: DataTypes.STRING - } - }, - { - charset: 'utf8mb4', - collate: 'utf8mb4_unicode_ci' - } - ); - - this.onPlayerConnected = this.onPlayerConnected.bind(this); - } - - createModel(name, schema) { - this.models[name] = this.options.database.define(`EOS_${name}`, schema, { - timestamps: false, - indexes: [ - { - unique: true, - fields: ['eosID'] - } - ] - }); - } - - async prepareToMount() { - await this.models.SteamIDtoEOSID.sync(); - } - - async mount() { - this.server.on('PLAYER_CONNECTED', this.onPlayerConnected); - this.verbose(1, 'Mounted'); - } - - async unmount() { - this.server.removeEventListener('PLAYER_CONNECTED', this.onPlayerConnected); - } - - async onPlayerConnected(info) { - await this.models.SteamIDtoEOSID.upsert({ - steamID: info.player.steamID, - eosID: info.eosID - }); - } - - async getByEOSID(eosID) { - return await this.models.SteamIDtoEOSID.findOne({ where: { eosID: eosID } }); - } - - async getBySteamID(steamID) { - return await this.models.SteamIDtoEOSID.findOne({ where: { steamID: steamID } }); - } -} From 65171e526a31705e3ea7f944ccac9a230d486b6a Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Tue, 2 Jan 2024 16:49:20 +0100 Subject: [PATCH 50/53] fix: player controller tracking --- squad-server/log-parser/player-connected.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/squad-server/log-parser/player-connected.js b/squad-server/log-parser/player-connected.js index 662e8726..65682271 100644 --- a/squad-server/log-parser/player-connected.js +++ b/squad-server/log-parser/player-connected.js @@ -6,6 +6,7 @@ export default { raw: args[0], time: args[1], chainID: +args[2], + playercontroller: args[3], ip: args[4], eosID: args[5], steamID: args[6] @@ -15,6 +16,10 @@ export default { data.connection = joinRequestData.connection; data.playerSuffix = joinRequestData.suffix; + if (!logParser.eventStore.players[data.steamID]) + logParser.eventStore.players[data.steamID] = {}; + logParser.eventStore.players[data.steamID].controller = data.playercontroller; + logParser.emit('PLAYER_CONNECTED', data); } }; From dc55169e0553c87e41c77fdd1370d5c566222a1c Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Tue, 2 Jan 2024 16:51:13 +0100 Subject: [PATCH 51/53] chore: improving player controller tracking via player damaged event --- squad-server/log-parser/player-damaged.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/squad-server/log-parser/player-damaged.js b/squad-server/log-parser/player-damaged.js index 7d4a7775..9f293b61 100644 --- a/squad-server/log-parser/player-damaged.js +++ b/squad-server/log-parser/player-damaged.js @@ -17,6 +17,10 @@ export default { logParser.eventStore.session[args[3]] = data; + if (!logParser.eventStore.players[data.attackerSteamID]) + logParser.eventStore.players[data.attackerSteamID] = {}; + logParser.eventStore.players[data.attackerSteamID].controller = data.attackerController; + logParser.emit('PLAYER_DAMAGED', data); } }; From b4c42d099f230a079ebb5a4aa4a09dc022da0122 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Wed, 3 Jan 2024 13:09:20 +0100 Subject: [PATCH 52/53] chore: number parsing in player list and squad list --- squad-server/rcon.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/squad-server/rcon.js b/squad-server/rcon.js index b100bde0..29626529 100644 --- a/squad-server/rcon.js +++ b/squad-server/rcon.js @@ -145,8 +145,10 @@ export default class SquadRcon extends Rcon { if (!match) continue; const data = match.groups; + data.playerID = +data.playerID; data.isLeader = data.isLeader === 'True'; - data.squadID = data.squadID !== 'N/A' ? data.squadID : null; + data.teamID = data.teamID !== 'N/A' ? +data.teamID : null; + data.squadID = data.squadID !== 'N/A' ? +data.squadID : null; players.push(data); } @@ -169,10 +171,11 @@ export default class SquadRcon extends Rcon { ); const matchSide = line.match(/Team ID: (\d) \((.+)\)/); if (matchSide) { - teamID = matchSide[1]; + teamID = +matchSide[1]; teamName = matchSide[2]; } if (!match) continue; + match.groups.squadID = +match.groups.squadID; squads.push({ ...match.groups, teamID: teamID, From 8fcce1ae634441dce029f62d6ec5033d287230b7 Mon Sep 17 00:00:00 2001 From: Fantino Davide Date: Thu, 4 Jan 2024 19:27:53 +0100 Subject: [PATCH 53/53] fix: handle client login request for servers with password --- squad-server/log-parser/login-request.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/squad-server/log-parser/login-request.js b/squad-server/log-parser/login-request.js index 1ea963f2..905f6812 100644 --- a/squad-server/log-parser/login-request.js +++ b/squad-server/log-parser/login-request.js @@ -1,6 +1,6 @@ export default { regex: - /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: Login request: \?Name=(.+) userId: RedpointEOS:([\da-f]{32}) platform: RedpointEOS/, + /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: Login request: \?Name=(.+?)(?:\?PASSWORD=(?:.+?))? userId: RedpointEOS:([\da-f]{32}) platform: RedpointEOS/, onMatch: (args, logParser) => { const data = { raw: args[0],