battleye-node
Version:
A TS/JS library for communicating with BattlEye’s RCon (Remote Console) protocol. Easily send commands, receive server responses, and automate server management for games protected by BattlEye anti-cheat.
223 lines (214 loc) • 7.55 kB
JavaScript
import EventEmitter from 'events';
import dgram from 'dgram';
import CRC32 from 'crc-32';
class UDPSocket {
constructor(config) {
this.isSocketOpen = false;
this.config = config;
this.socket = dgram.createSocket(this.config.connectionType || "udp4");
this.isSocketOpen = true;
}
send(buffer) {
this.socket.send(buffer, 0, buffer.length, this.config.port, this.config.address);
}
open() {
this.socket = dgram.createSocket(this.config.connectionType || "udp4");
this.isSocketOpen = true;
}
close() {
this.socket.close();
this.isSocketOpen = false;
}
}
const bufferHeader = (subsequent) => {
const crc = CRC32.buf(subsequent);
const crcBuffer = Buffer.alloc(4);
crcBuffer.writeInt32LE(crc, 0);
return Buffer.concat([Buffer.from([0x42]), Buffer.from([0x45]), crcBuffer]);
};
function bufferLogin(pass) {
const buf = Buffer.concat([Buffer.from([0xff, 0x00]), Buffer.from(pass, "ascii")]);
const header = bufferHeader(buf);
const packet = Buffer.concat([header, buf]);
return packet;
}
const bufferAck = (sequence) => {
const buf = Buffer.from([0xff, 0x02, sequence]);
const header = bufferHeader(buf);
const packet = Buffer.concat([header, buf]);
return packet;
};
const bufferKeepAlive = (sequence) => {
const buf = Buffer.from([0xff, 0x01, sequence]);
const header = bufferHeader(buf);
const packet = Buffer.concat([header, buf]);
return packet;
};
const bufferCommand = (command, sequence) => {
const buf = Buffer.concat([Buffer.from([0xff, 0x01, sequence]), Buffer.from(command, "ascii")]);
const header = bufferHeader(buf);
const packet = Buffer.concat([header, buf]);
return packet;
};
class RCON extends EventEmitter {
constructor(config) {
super();
this.connected = false;
this.packageSend = false;
this.sequence = -1;
this.loginAck = false;
this.keepAliveInterval = null;
this.loginConnectionInterval = null;
this.sequenceQueue = new Set();
if (!config.address || !config.port || !config.password) {
throw new Error("Address, port, and password are required");
}
this.config = {
...config,
connectionTimeout: Math.max(config.connectionTimeout ?? 50000, 50000),
connectionInterval: Math.max(config.connectionInterval ?? 5000, 5000),
keepAliveInterval: config.keepAliveInterval ?? 10000,
};
this.udp = new UDPSocket(config);
}
login() {
if (this.connected) {
this.printMessage("Already connected", "error");
return;
}
if (!this.udp?.isSocketOpen) {
this.udp?.open();
}
if (!this.connected && !this.packageSend) {
this.udp?.socket.on("message", (msg) => this.onMessage(msg));
this.packageSend = true;
this.udp?.send(bufferLogin(this.config.password));
this.connectAttemping();
}
}
logout() {
if (this.udp?.isSocketOpen) {
this.udp.close();
}
this.reset();
}
commandSend(command) {
if (this.loginConnectionInterval) {
return;
}
if (!this.connected || !this.udp?.socket) {
this.printMessage("Not connected", "error", true);
return;
}
if (this.sequenceQueue.size >= 5) {
this.printMessage("The server is not responding", "error", true);
return;
}
this.sequence = this.sequence >= 255 ? 0 : this.sequence + 1;
this.udp?.send(bufferCommand(command, this.sequence));
}
get isRconConnected() {
return this.connected;
}
onLogin() {
this.connected = true;
this.emit("onConnect", true);
if (this.loginConnectionInterval) {
clearInterval(this.loginConnectionInterval);
this.loginConnectionInterval = null;
}
if (this.keepAliveInterval) {
clearInterval(this.keepAliveInterval);
this.keepAliveInterval = null;
}
this.keepAliveInterval = setInterval(() => this.sendKeepAlive(), this.config.keepAliveInterval);
}
onACK(sequence, msg) {
if (!this.connected) {
this.printMessage("Not connected", "error");
return;
}
this.udp?.send(bufferAck(sequence));
if (msg) {
this.printMessage(msg);
}
}
sendKeepAlive() {
this.sequence = this.sequence >= 255 ? 0 : this.sequence + 1;
if (this.sequenceQueue.size >= 2) {
this.printMessage("The server is not responding. Trying to reconnect...", "error");
this.reset();
this.connectAttemping();
return;
}
this.sequenceQueue.add(this.sequence);
this.udp?.send(bufferKeepAlive(this.sequence));
}
connectAttemping() {
let attemps = Math.floor(this.config.connectionTimeout / 5000);
let attemptCount = 0;
this.loginConnectionInterval = setInterval(() => {
if (attemptCount >= attemps && this.loginConnectionInterval) {
this.printMessage("The server is not responding", "error", true);
clearInterval(this.loginConnectionInterval);
this.loginConnectionInterval = null;
return;
}
this.udp?.send(bufferLogin(this.config.password));
attemptCount++;
this.printMessage("Trying to connect...", "error");
}, this.config.connectionInterval);
}
reset() {
if (this.keepAliveInterval) {
clearInterval(this.keepAliveInterval);
this.keepAliveInterval = null;
}
if (this.loginConnectionInterval) {
clearInterval(this.loginConnectionInterval);
this.loginConnectionInterval = null;
}
this.connected = false;
this.packageSend = false;
this.loginAck = false;
this.sequence = -1;
this.sequenceQueue.clear();
}
printMessage(msg, type = "message", isExit = false) {
if (type === "error" && isExit) {
this.emit("onConnect", false);
this.logout();
}
this.emit(type, msg);
}
onMessage(msg) {
if (msg.toString("utf8", 0, 2) !== "BE")
return;
const payload = msg.subarray(7, msg.length);
const code = payload.readUInt8(0);
switch (code) {
case 0:
if (!this.loginAck) {
this.loginAck = true;
payload.readUInt8(1) === 1
? this.onLogin()
: this.printMessage("Authentication failed. Disconnecting...", "error", true);
}
break;
case 1:
let payloadSequence = payload.readUInt8(1);
if (this.sequenceQueue.has(payloadSequence)) {
this.sequenceQueue.delete(payloadSequence);
}
let message = payload.toString("utf-8", 2, payload.length);
if (message) {
this.printMessage(message);
}
break;
case 2:
this.onACK(payload.readUInt8(1), payload.toString("utf-8", 2, payload.length));
break;
}
}
}
export { RCON as default };