UNPKG

@afocommunity/eco-rcon

Version:
290 lines (289 loc) 10.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const events_1 = __importDefault(require("events")); const events_2 = __importDefault(require("events")); const net_1 = __importDefault(require("net")); const RCONParser_1 = __importDefault(require("./parser/RCONParser")); var PacketType; (function (PacketType) { PacketType[PacketType["COMMAND"] = 2] = "COMMAND"; PacketType[PacketType["AUTH"] = 3] = "AUTH"; PacketType[PacketType["RESPONSE_VALUE"] = 0] = "RESPONSE_VALUE"; PacketType[PacketType["RESPONSE_AUTH"] = 2] = "RESPONSE_AUTH"; })(PacketType || (PacketType = {})); /**! * node-rcon * Copyright(c) 2012 Justin Li <j-li.net> * MIT Licensed * Stripped Down and modified by @bombitmanbomb */ class Rcon extends events_1.default { host; port; password; outstandingData = null; hasAuthed; _tcpSocket; constructor(host, port, password) { super(); this.host = host; this.port = port; this.password = password; this.hasAuthed = false; events_2.default.EventEmitter.call(this); } _sendSocket(buf) { this._tcpSocket.write(buf.toString('binary'), 'binary'); } connect() { try { this._tcpSocket = net_1.default.createConnection(this.port, this.host); this._tcpSocket .on('data', ((data) => { this._tcpSocketOnData(data); }).bind(this)); this._tcpSocket .on('connect', (() => { this.socketOnConnect(); }).bind(this)); this._tcpSocket .on('error', ((err) => { this.emit('error', err); }).bind(this)); this._tcpSocket .on('end', (() => { this.socketOnEnd(); }).bind(this)); } catch (error) { this.emit('error', error); } } send(data, cmd, id = 0x0012d4a6) { cmd = cmd || PacketType.COMMAND; const length = Buffer.byteLength(data); const sendBuf = Buffer.alloc(length + 14); sendBuf.writeInt32LE(length + 10, 0); sendBuf.writeInt32LE(id, 4); sendBuf.writeInt32LE(cmd, 8); sendBuf.write(data, 12); sendBuf.writeInt16LE(0, length + 12); this._sendSocket(sendBuf); } disconnect() { if (this._tcpSocket) this._tcpSocket.end(); } setTimeout(timeout, callback) { if (!this._tcpSocket) return; this._tcpSocket.setTimeout(timeout, (() => { this._tcpSocket.end(); if (callback) callback(); }).bind(this)); } _tcpSocketOnData(data) { if (this.outstandingData != null) { data = Buffer.concat([this.outstandingData, data], this.outstandingData.length + data.length); this.outstandingData = null; } while (data.length >= 12) { const len = data.readInt32LE(0); // Size of entire packet, not including the 4 byte length field if (!len) return; // No valid packet header, discard entire buffer const packetLen = len + 4; if (data.length < packetLen) break; // Wait for full packet, TCP may have segmented it const bodyLen = len - 10; // Subtract size of ID, type, and two mandatory trailing null bytes if (bodyLen < 0) { data = data.subarray(packetLen); // Length is too short, discard malformed packet break; } const id = data.readInt32LE(4); const type = data.readInt32LE(8); if (id == -1) return this.emit('error', new Error('Authentication failed')); if (!this.hasAuthed && type == PacketType.RESPONSE_AUTH) { this.hasAuthed = true; this.emit('auth'); } else if (type == PacketType.RESPONSE_VALUE) { // Read just the body of the packet (truncate the last null byte) // See https://developer.valvesoftware.com/wiki/Source_RCON_Protocol for details let str = data.toString('utf8', 12, 12 + bodyLen); if (str.charAt(str.length - 1) === '\n') { // Emit the response without the newline. str = str.substring(0, str.length - 1); } const response = { size: data.readInt32LE(0), id: data.readInt32LE(4), type: data.readInt32LE(8), body: str, }; this.emit('response', response); } data = data.subarray(packetLen); } // Keep a reference to remaining data, since the buffer might be split within a packet this.outstandingData = data; } socketOnConnect() { this.emit('connect'); this.send(this.password, PacketType.AUTH); } socketOnEnd() { this.emit('end'); this.hasAuthed = false; } } /** * Primary Interface Object for ECO servers. */ class ECO { /** * Rejects if an error occurs when connecting. */ onReady; rconParser; messageID; _conn; _isAuthorized; _responsePromiseQueue; debug = false; /**Reconnect to the client */ reconnect; _events; constructor(ip, port, password, options) { Object.defineProperty(this, '_isAuthorized', { enumerable: false, value: false }); Object.defineProperty(this, 'messageID', { enumerable: false, value: 1 }); Object.defineProperty(this, '_events', { enumerable: false, value: new events_1.default }); Object.defineProperty(this, '_responsePromiseQueue', { enumerable: false, value: new Map }); Object.defineProperty(this, 'rconParser', { enumerable: false, value: new RCONParser_1.default(this) }); Object.defineProperty(this, '_onResponse', { enumerable: false, value: this._onResponse.bind(this) }); Object.defineProperty(this, '_onError', { enumerable: false, value: this._onError.bind(this) }); Object.defineProperty(this, '_onEnd', { enumerable: false, value: this.onEnd.bind(this) }); Object.defineProperty(this, '_conn', { enumerable: false, value: new Rcon(ip, port, password) }); const executor = (res, rej) => { let handled = false; this._conn.once('error', (err) => { if (handled) return; handled = true; rej(err); }); this._conn.once('auth', () => { if (handled) return; handled = true; res(null); }); this._conn.connect(); }; this._conn .on('response', this._onResponse) .on('error', this._onError) .on('end', this.onEnd); Object.defineProperty(this, 'onReady', { enumerable: false, value: new Promise(executor) }); Object.defineProperty(this, 'reconnect', { enumerable: false, value: () => { Object.defineProperty(this, 'onReady', { enumerable: false, value: new Promise(executor) }); return this.onReady; } }); this.onReady.then(() => { this._events.emit('READY'); }) .catch((err) => { this._events.emit('error', err); }); this._events.on('error', () => { //Handle Error. Hookable }); } on(event, cb) { return this._events.on(event, cb); } removeListener(event, cb) { return this._events.removeListener(event, cb); } /** * Send a Raw RCON command. */ send(cmd) { const id = this.messageID++; return new Promise((resolve, reject) => { const ob = { time: new Date(), buffer: '', handled: false, }; ob.resolve = (dat) => { ob.handled = true; resolve(dat); }; ob.reject = (dat) => { ob.handled = true; reject(dat); }; //? Timeout in the event of the final packet is Max packet length ob.timeOut = setTimeout(() => { if (ob.handled) return; ob.handled = true; ob.resolve(this._parseResponse(ob.buffer) ?? ob.buffer); }, 5000); this._responsePromiseQueue.set(id, ob); this._conn.send(cmd, 0x02, id); }); } _onResponse(response) { const size = response?.size; if (this._responsePromiseQueue.has(response.id)) { const q = this._responsePromiseQueue.get(response.id); if (size >= 4092) { //MAX PACKET SIZE, MIGHT BE SPLIT q.buffer += response.body; return; } //eslint-disable-next-line @typescript-eslint/no-explicit-any const res = this._parseResponse((q.buffer ?? '') + response.body) ?? response; if (res.success === false) { q.reject(res.data); } else { q.resolve(res.data ?? res); } this._responsePromiseQueue.delete(response.id); return; } // Non command //TODO } /** * Disconnect the client. */ disconnect() { this._conn.disconnect(); } _parseResponse(res) { const data = res?.replaceAll('\\n', '\n').replaceAll('\\r', ''); if (data == null || data?.trim?.() == '') return null; return this.rconParser.parse(data); } _onError(err) { if (this.debug) { console.error(err); } //Handle Error. Hookable } onEnd() { //TODO: onEnd } } exports.default = ECO;