working-rcon
Version:
A working rcon library
213 lines (166 loc) • 5.53 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.TimeoutError = exports.AuthenticationError = exports.UnexpectedPacketError = exports.RconError = void 0;
var _net = require("net");
var _promiseTimeout = require("promise-timeout");
var _packet = require("./packet");
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
class RconError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
}
}
exports.RconError = RconError;
class UnexpectedPacketError extends RconError {
constructor(packet, expectedType) {
super(`Got unexpected packet ${JSON.stringify(packet)}. Expected ${expectedType}.`);
this._packet = packet;
this._expectedType = expectedType;
}
}
exports.UnexpectedPacketError = UnexpectedPacketError;
class AuthenticationError extends RconError {
constructor() {
super('Authentication failed!');
}
}
exports.AuthenticationError = AuthenticationError;
class TimeoutError extends RconError {
constructor() {
super('Request timed out.');
}
}
exports.TimeoutError = TimeoutError;
const newPacket = size => ({
size,
buffer: Buffer.alloc(size == null ? 4 : size),
offset: 0
});
class RconClient {
constructor(socket, timeout) {
_defineProperty(this, "_onReceiveData", data => {
let currentOffset = 0;
while (currentOffset < data.length) {
const packet = this._pendingPacket != null ? this._pendingPacket : newPacket((0, _packet.peekSize)(data, currentOffset));
const packetEnd = currentOffset + packet.buffer.length - packet.offset;
const copyUntil = Math.min(packetEnd, data.length);
data.copy(packet.buffer, packet.offset, currentOffset, copyUntil);
if (packetEnd > data.length) {
this._pendingPacket = { ...packet,
offset: packet.offset + copyUntil - currentOffset
};
} else {
this._pendingPacket = null;
if (packet.size == null) {
this._onReceiveData(packet.buffer);
} else {
this._onReceivePacket(packet.buffer);
}
}
currentOffset = copyUntil;
}
});
_defineProperty(this, "_onSocketError", err => {
throw err;
});
this._callbacks = new Map();
this._socket = socket;
this._timeout = timeout;
this._currentId = 0;
this._pendingPacket = null;
this._socket.on('data', this._onReceiveData);
}
async authenticate(password) {
const id = this._uniqueId;
this._write({
id,
type: _packet.Types.SERVERDATA_AUTH,
body: password
});
await Promise.race([this._receive(id), this._receive(-1)]); // We cannot provide the rest of the implementation of the protocol
// according to Valve's Wiki page, since some servers do not respond
// with authentication results at all (e.g. CS:GO: it just accepts faulty
// passwords but doesn't execute the commands sent).
// Taking the pragmatic approach, we instead opt to require passwords to be correct.
// The code below implements the rest of the protocol as described in the wiki page.
// if (reply.type === Types.SERVERDATA_RESPONSE_VALUE) {
// reply = await Promise.race([this._receive(id), this._receive(-1)])
// }
// if (reply.type !== Types.SERVERDATA_AUTH_RESPONSE) {
// throw new UnexpectedPacketError(packet, 'SERVERDATA_AUTH_RESPONSE')
// }
// if (reply.id === -1) {
// throw new AuthenticationError()
// }
}
async command(cmd) {
const result = await this._query({
type: _packet.Types.SERVERDATA_EXECCOMMAND,
body: cmd
});
if (result.type !== _packet.Types.SERVERDATA_RESPONSE_VALUE) {
throw new UnexpectedPacketError(packet, 'SERVERDATA_RESPONSE_VALUE');
}
return result.body;
}
disconnect() {
return new Promise(resolve => {
this._socket.end(null, null, resolve);
});
}
_query(packet) {
const id = this._uniqueId;
this._write({
id,
...packet
});
return this._receive(id);
}
_write(packet) {
this._socket.write((0, _packet.encode)(packet));
}
async _receive(conversationId) {
try {
return await (0, _promiseTimeout.timeout)(new Promise(resolve => {
this._callbacks.set(conversationId, resolve);
}), this._timeout);
} catch (err) {
if (err instanceof _promiseTimeout.TimeoutError) {
this._callbacks.delete(conversationId);
throw new TimeoutError();
} else {
throw err;
}
}
}
_onReceivePacket(buf) {
const packet = (0, _packet.decode)(buf);
const callback = this._callbacks.get(packet.id); // If the callback doesn't exist it may be a query that timed out, ignore.
if (callback != null) {
this._callbacks.delete(packet.id);
callback(packet);
}
}
get _uniqueId() {
return this._currentId++;
}
}
exports.connect = (host, port, password, timeout = 1000) => new Promise((resolve, reject) => {
const socket = (0, _net.createConnection)({
host,
port
}, async () => {
try {
const client = new RconClient(socket, timeout);
await client.authenticate(password);
resolve(client);
} catch (err) {
socket.destroy();
reject(err);
}
});
socket.once('error', reject);
});