Merge remote-tracking branch 'davidrepos/eos-integration'

# Conflicts:
#	squad-server/index.js
#	squad-server/log-parser/index.js
#	squad-server/log-parser/player-disconnected.js
#	squad-server/log-parser/player-possess.js
#	squad-server/rcon.js
This commit is contained in:
Skillet 2023-12-12 16:35:54 -05:00
commit cb11120d7c
29 changed files with 1039 additions and 460 deletions

View File

@ -329,7 +329,7 @@ Interested in creating your own plugin? [See more here](./squad-server/plugins/r
<h6>Description</h6>
<p>Message SquadJS will send to players warning them they will be kicked</p>
<h6>Default</h6>
<pre><code>Join a squad, you are are unassigned and will be kicked</code></pre></li>
<pre><code>Join a squad, you are unassigned and will be kicked</code></pre></li>
<li><h4>kickMessage</h4>
<h6>Description</h6>
<p>Message to send to players when they are kicked</p>
@ -952,6 +952,18 @@ Grafana:
<pre><code>300000</code></pre></li></ul>
</details>
<details>
<summary>PersistentEOSIDtoSteamID</summary>
<h2>PersistentEOSIDtoSteamID</h2>
<p>Stores into a DB every association of SteamID-EOSID</p>
<h3>Options</h3>
<ul><li><h4>database (Required)</h4>
<h6>Description</h6>
<p>The Sequelize connector.</p>
<h6>Default</h6>
<pre><code>sqlite</code></pre></li></ul>
</details>
<details>
<summary>SeedingMode</summary>
<h2>SeedingMode</h2>

View File

@ -7,6 +7,9 @@
"queryPort": 27165,
"rconPort": 21114,
"rconPassword": "password",
"rconPassThrough": true,
"rconPassThroughPort": 8124,
"dumpRconResponsesToFile": false,
"logReaderMode": "tail",
"logDir": "C:/path/to/squad/log/folder",
"ftp": {
@ -45,7 +48,7 @@
{
"plugin": "AutoKickUnassigned",
"enabled": true,
"warningMessage": "Join a squad, you are are unassigned and will be kicked",
"warningMessage": "Join a squad, you are unassigned and will be kicked",
"kickMessage": "Unassigned - automatically removed",
"frequencyOfWarnings": 30,
"unassignedTimer": 360,
@ -236,6 +239,11 @@
"broadcasts": [],
"interval": 300000
},
{
"plugin": "PersistentEOSIDtoSteamID",
"enabled": true,
"database": "sqlite"
},
{
"plugin": "SeedingMode",
"enabled": true,

View File

@ -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;

View File

@ -1,367 +1,613 @@
import EventEmitter from 'events';
import net from 'net';
import util from 'util';
/* eslint-disable */
import { EventEmitter } from 'node:events';
import net from 'node:net';
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 fs from 'fs';
import path from 'path';
const RCON_LOG_FILEPATH = 'RCON_RECEIVED_MESSAGES.log';
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.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.dumpRconResponsesToFile = options.dumpRconResponsesToFile || false;
this.rconClients = {};
for (let i = 1; i <= this.passThroughMaxClients; i++) this.rconClients[`${i}`] = null;
this.ptServer = null;
this.incomingData = Buffer.from([]);
this.incomingResponse = [];
this.steamIndex = { '76561198799344716': '00026e21ce3d43c792613bdbb6dec1ba' }; // example dtata
this.eosIndex = { '00026e21ce3d43c792613bdbb6dec1ba': '76561198799344716' }; // example dtata
this.responseCallbackQueue = [];
this.rotateLogFile(RCON_LOG_FILEPATH);
}
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
)}`
);
}
}
}
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;
}
}
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);
}
return packets;
}
decodePacket(packet) {
return {
size: packet.readInt32LE(0),
id: packet.readInt32LE(4),
type: packet.readInt32LE(8),
body: packet.toString('utf8', 12, packet.byteLength - 2)
};
}
processChatPacket(decodedPacket) {}
onClose(hadError) {
this.connected = false;
Logger.verbose('RCON', 1, `Socket closed ${hadError ? 'without' : 'with'} an error.`);
if (this.autoReconnect) {
Logger.verbose('RCON', 1, `Sleeping ${this.autoReconnectDelay}ms before reconnecting.`);
setTimeout(this.connect, this.autoReconnectDelay);
}
}
onError(err) {
Logger.verbose('RCON', 1, `Socket had error:`, err);
this.emit('RCON_ERROR', err);
}
connect() {
processChatPacket(decodedPacket) {
console.log(decodedPacket.body);
} //
async 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;
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}`);
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);
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}`);
});
}
disconnect() {
async 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
clearTimeout(this.connectionRetry);
this.removeAllListeners('server');
this.removeAllListeners('auth');
this.autoReconnect = false;
// clear the timeout just in case the socket closed and then we DCed
clearTimeout(this.autoReconnectTimeout);
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]);
}
execute(command) {
return this.write(SERVERDATA_EXECCOMMAND, command);
}
write(type, body) {
return new Promise((resolve, reject) => {
if (!this.connected) {
reject(new Error('Not connected.'));
return;
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++;
}
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;
}
const onError = (err) => {
Logger.verbose('RCON', 1, 'Error occurred. Wiping response action queue.', err);
this.responseCallbackQueue = [];
reject(err);
};
// 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) {
}).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) {
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',
4,
`Sending empty packet: ${this.bufToHexString(encodedEmptyPacket)}`
3,
`Processing decoded packet: Size: ${packet.size}, ID: ${packet.id}, Type: ${packet.type}, Body: ${packet.body}`
);
this.client.write(encodedEmptyPacket);
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');
}
}
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);
}
});
}
encodePacket(type, id, body, encoding = 'utf8') {
const size = Buffer.byteLength(body) + 14;
const buf = Buffer.alloc(size);
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);
return buf;
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();
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;
}
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();
}
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;
this.removeAllListeners();
clearTimeout(this.connectionRetry);
if (this.autoReconnect) {
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.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`);
}
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")
}
}
}
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;
}
}
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);//////////////////////////////////////////////////////////////////////////////////////////
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(' ');
}
decodedPacketToString(decodedPacket) {
return util.inspect(decodedPacket, { breakLength: Infinity });
}
async warn(steamID, message) {
await this.execute(`AdminWarn "${steamID}" ${message}`);
this.execute(`AdminWarn "${steamID}" ${message}`);
}
async kick(steamID, reason) {
await this.execute(`AdminKick "${steamID}" ${reason}`);
this.execute(`AdminKick "${steamID}" ${reason}`);
}
async forceTeamChange(steamID) {
this.execute(`AdminForceTeamChange "${steamID}"`);
}
async forceTeamChange(steamID) {
await this.execute(`AdminForceTeamChange "${steamID}"`);
addIds(steamId, eosId) {
this.steamIndex[steamId] = eosId; // { "76561198799344716": "00026e21ce3d43c792613bdbb6dec1ba" };
this.eosIndex[eosId] = steamId;
}
removeIds(eosId) {
// clean up ids on leave
}
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;
}
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');
}
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
);
}
}
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);
}
}
}
const defs = [
//strict matching to avoid 'name as steamId errors'
{
regex:
/^ID: [0-9]+ \| Online IDs: EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\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: (?<Id>[0-9]+) \| Online IDs: EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d{17}) \| Since Disconnect: (?<SinceDc>.+) \| Name: (?<name>.+)$/,
rep: (line, steamId, eosId) => {
return line.replace(
/Online IDs: EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d{17})/,
`SteamID: ${steamId}`
);
}
},
{
regex:
/^ID: (?<sqdId>[0-9]+) \| Name: (?<sqdName>.+) \| Size: (?<sqdSize>[0-9]) \| Locked: (?<locked>True|False) \| Creator Name: (?<creatorName>.+) \| Creator Online IDs: EOS: (?<eosID>[\d\w]{32}) steam: (?<steamId>\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 (?<id>[0-9]+). \[Online IDs= EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d{17})] (.+)/,
rep: (line, steamId, eosId) => {
return line.replace(
/Online IDs= EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d{17})/,
`steamid=${steamId}`
);
}
},
{
regex: /^Could not find player (?<eosId>[0-9a-f]{32})/,
rep: (line, steamId, eosId) => {
return line.replace(`Could not find player ${eosId}`, `Could not find player ${steamId}`);
}
},
{
regex:
/^\[Chat(All|Team|Squad|Admin)] \[Online IDs:EOS: (?<eosId>[\d\w]{32}) steam: (?<steamId>\d{17})] (?<name>.+) : (?<msg>.+)/,
rep: (line, steamId, eosId) => {
return line.replace(/Online IDs:EOS: [\d\w]{32} steam: \d{17}/, `SteamID:${steamId}`);
}
},
{
regex:
/^(?<name>.+) \(Online IDs: EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d+)\) has created Squad (?<squadNum>[0-9]+) \(Squad Name: (?<squadName>.+)\) on (?<teamName>.+)/,
rep: (line, steamId, eosId) => {
return line.replace(
/Online IDs: EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d+)/,
`Steam ID: ${steamId}`
);
}
},
{
regex:
/^Kicked player (?<Id>[0-9]+). \[Online IDs= EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d{17})] (?<name>.+)/,
rep: (line, steamId, eosId) => {
return line.replace(
/Online IDs= EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d{17})/,
`steamid=${steamId}`
);
}
},
{
regex: /^ERROR: Unable to find player with name or id \((?<eosId>[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: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>)\d{17}] (?<name>.+) 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}`);
}
}
];
//////////////////////////////////////////////////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 (?<level>.+), layer is (?<layer>.+)/);
// 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();

View File

@ -1,6 +1,6 @@
{
"name": "SquadJS",
"version": "3.8.1",
"version": "3.8.2",
"repository": "https://github.com/Team-Silver-Sphere/SquadJS.git",
"author": "Thomas Smyth <https://github.com/Thomas-Smyth>",
"license": "BSL-1.0",

View File

@ -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;
@ -74,11 +73,10 @@ export default class SquadServer extends EventEmitter {
this.admins = await fetchAdminLists(this.options.adminLists);
await this.rcon.connect();
await this.updateLayerList();
await this.logParser.watch();
await this.updateSquadList();
await this.updatePlayerList();
await this.updatePlayerList(this);
await this.updateLayerInformation();
await this.updateA2SInformation();
@ -97,7 +95,10 @@ 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,
dumpRconResponsesToFile: this.options.dumpRconResponsesToFile,
passThroughPort: this.options.rconPassThroughPort,
passThrough: this.options.rconPassThrough
});
this.rcon.on('CHAT_MESSAGE', async (data) => {
@ -229,55 +230,74 @@ export default class SquadServer extends EventEmitter {
});
this.logParser.on('PLAYER_CONNECTED', async (data) => {
data.player = await this.getPlayerBySteamID(data.steamID);
if (data.player) {
data.player.suffix = data.playerSuffix;
} else {
data.player = {
steamID: data.steamID,
name: data.playerSuffix
};
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.eosID);
if (data.player) data.player.suffix = data.playerSuffix;
else {
data.player = {
steamID: data.steamID,
name: data.playerSuffix,
eosID: data.eosID
}
}
delete data.steamID;
delete data.playerSuffix;
this.emit('PLAYER_CONNECTED', data);
});
this.logParser.on('PLAYER_DISCONNECTED', async (data) => {
Logger.verbose('PlayerBugFix', 1, `player ${data.steamID} disconnect with playerinfo: ${JSON.stringify(Array.from(this.playerinfo.entries()))}`);
data.player = await this.getPlayerBySteamID(data.steamID);
if (!data.player) {
Logger.verbose('PlayerBugFix', 1, `Bug detected, using playerinfo data for ${data.steamID}`);
data.player = this.playerinfo.get(data.steamID);
}
if (!data.player) {
Logger.verbose('PlayerBugFix', 1, `Bug detected, FAILED, falling back for ${data.steamID}`);
data.player = {
steamID: data.steamID
};
}
this.playerinfo.delete(data.steamID);
Logger.verbose('PlayerBugFix', 1, `player ${data.playerEOSID} disconnect with playerinfo: ${JSON.stringify(Array.from(this.playerinfo.entries()))}`);
data.player = await this.getPlayerByEOSID(data.playerEOSID);
if(!data.player){
Logger.verbose('PlayerBugFix', 1, `Bug detected, using playerinfo data for ${data.steamID}`);
data.player = this.playerinfo.get(data.playerEOSID);
}
if (!data.player){
Logger.verbose('PlayerBugFix', 1, `Bug detected, FAILED, falling back for ${data.steamID}`);
data.player = {
steamID: data.steamID,
eosID: data.playerEOSID
};
}
this.playerinfo.delete(data.playerEOSID);
this.emit('PLAYER_DISCONNECTED', data);
});
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.victim && data.attacker)
if (!data.attacker.playercontroller) 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;
}
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);
@ -291,9 +311,8 @@ export default class SquadServer extends EventEmitter {
});
this.logParser.on('PLAYER_DIED', async (data) => {
// console.log(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);
@ -308,9 +327,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;
@ -320,7 +339,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;
if (data.player) data.player.characterClassname = data.characterClassname;
@ -330,7 +349,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;
@ -369,6 +388,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() {
@ -407,13 +443,13 @@ export default class SquadServer extends EventEmitter {
try {
const oldPlayerInfo = new Map();
for (const player of this.players) {
oldPlayerInfo.set(player.steamID, player);
oldPlayerInfo.set(player.eosID, player);
}
const players = [];
for (const player of await this.rcon.getListPlayers()) {
for (const player of await this.rcon.getListPlayers(this)){
players.push({
...oldPlayerInfo.get(player.steamID),
...oldPlayerInfo.get(player.eosID),
...player,
playercont: this.logParser.eventStore.players[player.steamID]
? this.logParser.eventStore.players[player.steamID].controller
@ -539,39 +575,47 @@ export default class SquadServer extends EventEmitter {
Logger.verbose('SquadServer', 1, `Updating A2S information...`);
try {
const serverlayer = this.currentLayer;
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
// });
// console.log(data);
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,
serverName: data.name,
raw: data,
serverName: data.ServerName_s,
maxPlayers: parseInt(data.maxplayers),
publicSlots: parseInt(data.raw.rules.NUMPUBCONN),
reserveSlots: parseInt(data.raw.rules.NUMPRIVCONN),
maxPlayers: parseInt(data.MaxPlayers),
publicQueueLimit: 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),
playerCount: 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,
currentLayer: data.map
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
};
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;
@ -594,6 +638,7 @@ 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);
}
@ -735,6 +780,10 @@ export default class SquadServer extends EventEmitter {
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);
}

View File

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

View File

@ -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] });
}
};

View File

@ -0,0 +1,21 @@
export default {
regex:
/^\[([0-9.:-]+)]\[([ 0-9]+)]LogEOS: Verbose: \[LogEOSConnect] FConnectClient::CacheExternalAccountInfo - ProductUserId: (?<eosId>[0-9a-f]{32}), AccountType: (\d), AccountId: (?<steamId>[0-9]{17}), DisplayName: <Redacted>/,
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);
}
};

View File

@ -1,6 +1,6 @@
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],
@ -13,7 +13,7 @@ export default {
2nd Step in player connected path
*/
logParser.eventStore['client-login'] = logParser.eventStore.clients[args[3]];
logParser.eventStore.joinRequests[data.chainID].connection = data.connection;
delete logParser.eventStore.clients[args[3]];
logParser.emit('CLIENT_LOGIN', data);
}

View File

@ -16,10 +16,16 @@ 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 BadPlayerMovement from './BadPlayerMovement.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) {
@ -44,10 +50,17 @@ export default class SquadLogParser extends LogParser {
RoundTickets,
RoundWinner,
ServerTickRate,
ClientConnected,
AddingClientConnection,
ClientLogin,
PendingConnectionDestroyed,
BadPlayerMovement
BadPlayerMovement,
PendingConnectionDestroyed,
ClientExternalAccountInfo,
SendingAuthResult,
LoginRequest,
JoinRequest,
PlayerJoinSucceeded,
CheckPermissionResolveEosid
];
}
}

View File

@ -0,0 +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]
};
logParser.eventStore.joinRequests[data.chainID] = data;
// console.log(logParser.eventStore.loginRequests[ data.chainID ])
logParser.emit('CLIENT_JOIN_REQUEST', data);
}
};

View File

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

View File

@ -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],

View File

@ -1,27 +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],
steamID: logParser.eventStore['client-login'], // player connected
controller: logParser.eventStore['player-controller'] // playercontroller connected
chainID: +args[2],
ip: args[4],
eosID: args[5],
steamID: args[6]
};
delete logParser.eventStore['client-login'];
delete logParser.eventStore['player-controller'];
const joinRequestData = logParser.eventStore.joinRequests[+args[2]];
data.connection = joinRequestData.connection;
data.playerSuffix = joinRequestData.suffix;
// 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] = {
steamID: data.steamID,
suffix: data.playerSuffix,
controller: data.controller
};
}
};

View File

@ -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;

View File

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

View File

@ -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-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: ([\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],
steamID: args[3],
playerController: args[4]
ip: args[3],
playerController: args[4],
playerEOSID: args[5]
};
logParser.eventStore.disconnected[data.steamID] = true;
logParser.emit('PLAYER_DISCONNECTED', data);
}
};

View File

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

View File

@ -1,15 +1,16 @@
export default {
regex:
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQPlayerController::)?OnPossess\(\): PC=(.+) Pawn=(([A-z0-9_]+)_C_[0-9]+)/,
/^\[([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],
characterClassname: args[4],
possessClassname: args[5],
pawn: args[6]
playerEOSID: args[4],
playerSteamID: args[5],
possessClassname: args[6],
pawn: args[5]
};
logParser.eventStore.session[args[3]] = args[2];

View File

@ -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);

View File

@ -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]];

View File

@ -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;

View File

@ -5,12 +5,11 @@ export default {
const data = {
raw: args[0],
time: args[1],
chainID: args[2],
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);
}
};

View File

@ -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'];
}
};

View File

@ -17,7 +17,7 @@ export default class AutoKickUnassigned extends BasePlugin {
warningMessage: {
required: false,
description: 'Message SquadJS will send to players warning them they will be kicked',
default: 'Join a squad, you are are unassigned and will be kicked'
default: 'Join a squad, you are unassigned and will be kicked'
},
kickMessage: {
required: false,

View File

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

View File

@ -4,7 +4,7 @@ import Rcon from 'core/rcon';
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)] \[Online IDs:EOS: ([0-9a-f]{32}) steam: (\d{17})\] (.+?) : (.*)/
);
if (matchChat) {
Logger.verbose('SquadRcon', 2, `Matched chat message: ${decodedPacket.body}`);
@ -12,9 +12,10 @@ export default class SquadRcon extends Rcon {
this.emit('CHAT_MESSAGE', {
raw: decodedPacket.body,
chat: matchChat[1],
steamID: matchChat[2],
name: matchChat[3],
message: matchChat[4],
eosID: matchChat[2],
steamID: matchChat[3],
name: matchChat[4],
message: matchChat[5],
time: new Date()
});
@ -22,14 +23,14 @@ export default class SquadRcon extends Rcon {
}
const matchPossessedAdminCam = decodedPacket.body.match(
/\[SteamID:([0-9]{17})] (.+?) 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()
});
@ -37,14 +38,14 @@ export default class SquadRcon extends Rcon {
}
const matchUnpossessedAdminCam = decodedPacket.body.match(
/\[SteamID:([0-9]{17})] (.+?) 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: matchUnpossessedAdminCam[2],
name: matchUnpossessedAdminCam[3],
time: new Date()
});
@ -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]+)\. \[Online IDs= EOS: ([0-9a-f]{32}) steam: (\d{17})] (.*)/
);
if (matchKick) {
Logger.verbose('SquadRcon', 2, `Matched kick message: ${decodedPacket.body}`);
@ -76,8 +77,8 @@ export default class SquadRcon extends Rcon {
this.emit('PLAYER_KICKED', {
raw: decodedPacket.body,
playerID: matchKick[1],
steamID: matchKick[2],
name: matchKick[3],
steamID: matchKick[3],
name: matchKick[4],
time: new Date()
});
@ -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 (.+)/
/(.+) \(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}`);
@ -93,10 +94,10 @@ export default class SquadRcon extends Rcon {
this.emit('SQUAD_CREATED', {
time: new Date(),
playerName: matchSqCreated[1],
playerSteamID: matchSqCreated[2],
squadID: matchSqCreated[3],
squadName: matchSqCreated[4],
teamName: matchSqCreated[5]
playerSteamID: matchSqCreated[3],
squadID: matchSqCreated[4],
squadName: matchSqCreated[5],
teamName: matchSqCreated[6]
});
return;
@ -134,27 +135,31 @@ 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;
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: (.+)/
/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;
server.rcon.addIds(match[3], match[2]);
players.push({
playerID: match[1],
steamID: match[2],
name: match[3],
teamID: match[4],
squadID: match[5] !== 'N/A' ? match[5] : null,
isSquadLeader: match[6] === 'True',
rconRole: match[7]
EOSID: match[2],
steamID: match[3],
name: match[4],
teamID: match[5],
squadID: match[6] !== 'N/A' ? match[5] : null,
isLeader: match[7] === 'True',
isSquadLeader: match[7] === 'True',
role: match[8],
rconRole: match[8]
});
}
@ -168,11 +173,11 @@ export default class SquadRcon extends Rcon {
let teamName;
let teamID;
if(!responseSquad || responseSquad.length < 1) return squads;
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 Online IDs: EOS: ([0-9a-f]{32}) steam: (\d{17})/
);
const matchSide = line.match(/Team ID: (1|2) \((.+)\)/);
if (matchSide) {
@ -186,7 +191,7 @@ export default class SquadRcon extends Rcon {
size: match[3],
locked: match[4],
creatorName: match[5],
creatorSteamID: match[6],
creatorSteamID: match[7],
teamID: teamID,
teamName: teamName
});

View File

@ -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": {