UNPKG

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.

225 lines (215 loc) 7.57 kB
'use strict'; var EventEmitter = require('events'); var dgram = require('dgram'); var CRC32 = require('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; } } } module.exports = RCON;