rcon-ts-valve
Version:
Improved RCON client in typescript
266 lines (265 loc) • 11.1 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Rcon = exports.Defaults = exports.RconError = exports.ExtendableError = void 0;
const net = require("net");
const buffer_1 = require("buffer");
class ExtendableError extends Error {
constructor(message = '', innerException) {
super(message);
this.innerException = innerException;
this.message = message;
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
exports.ExtendableError = ExtendableError;
class RconError extends ExtendableError {
constructor(message, innerException) {
super(message, innerException);
Object.freeze(this);
}
}
exports.RconError = RconError;
var Defaults;
(function (Defaults) {
Defaults.PORT = 25575;
Defaults.TIMEOUT = 5000;
})(Defaults || (exports.Defaults = Defaults = {}));
Object.freeze(Defaults);
class Rcon {
get errors() {
return this._errors.slice();
}
get state() {
return this._state;
}
constructor(config) {
this.enableConsoleLogging = false;
this._authPacketId = NaN;
this._state = 0 /* State.Disconnected */;
this._lastRequestId = 0xF4240;
this._callbacks = new Map();
this._errors = [];
this._sessionCount = 0;
this._tempResponse = '';
let host = config.host;
this.host = host = host && host.trim();
if (!host)
throw new TypeError('"host" argument cannot be empty');
this.port = config.port || Defaults.PORT;
const password = config.password;
if (!password || !password.trim())
throw new TypeError('"password" argument cannot be empty');
this.password = password;
this.timeout = config.timeout || Defaults.TIMEOUT;
}
connect() {
const _ = this;
let p = _._connector;
if (!p)
_._connector = p = new Promise((resolve, reject) => {
_._state = 0.5 /* State.Connecting */;
if (_.enableConsoleLogging)
console.log(this.toString(), "Connecting...");
const s = _._socket = net.createConnection(_.port, _.host);
function cleanup(message, error) {
if (error)
_._errors.push(error);
s.removeAllListeners();
if (_._socket == s)
_._socket = undefined;
if (_._connector == p)
_._connector = undefined;
if (message) {
if (_.enableConsoleLogging)
console.error(_.toString(), message);
if (message)
return new RconError(message, error);
}
}
// Look for connection failure...
s.once('error', error => {
_._state = -1 /* State.Refused */;
reject(cleanup("Connection refused.", error)); // ** First point of failure.
});
// Look for successful connection...
s.once('connect', () => {
s.removeAllListeners('error');
_._state = 1 /* State.Connected */;
if (_.enableConsoleLogging)
console.log(_.toString(), "Connected. Authorizing ...");
s.on('data', data => _._handleResponse(data));
s.on('error', error => {
_._errors.push(error);
if (_.enableConsoleLogging)
console.error(_.toString(), error);
});
_._send(_.password, 3 /* PacketType.AUTH */).then(() => {
_._state = 2 /* State.Authorized */;
if (_.enableConsoleLogging)
console.log(_.toString(), "Authorized.");
resolve(_);
}).catch(error => {
_._state = -2 /* State.Unauthorized */;
reject(cleanup("Authorization failed.", error)); // ** Second point of failure.
});
});
s.once('end', () => {
if (_.enableConsoleLogging)
console.warn(this.toString(), "Disconnected.");
_._state = 0 /* State.Disconnected */;
cleanup();
});
});
return p;
}
session(context) {
return __awaiter(this, void 0, void 0, function* () {
const sessionId = ++this._sessionCount;
let rcon;
try {
rcon = yield this.connect();
return yield context(rcon, sessionId);
}
finally {
this._sessionCount--;
if (!this._sessionCount && rcon)
rcon.disconnect();
}
});
}
toString() {
return `RCON: ${this.host}:${this.port}`;
}
disconnect() {
const s = this._socket;
this._callbacks.clear();
if (s)
s.end();
this._socket = undefined;
this._connector = undefined;
}
_handleResponse(data) {
const len = data.readInt32LE(0);
if (!len)
throw new RconError('Received empty response package');
let id = data.readInt32LE(4);
const type = data.readInt32LE(8);
const callbacks = this._callbacks;
const authId = this._authPacketId;
let payload = data.toString('utf8', 12, len + 2);
if (payload.charAt(payload.length - 1) === '\n')
payload = payload.substring(0, payload.length - 1);
// console.log("Received\n", "type:", type ,", response:", payload, ", current id: ", id, ", authId: ", authId);
// console.log("callback: ", callbacks);
if (id === -1 && !isNaN(authId) && type === 2 /* PacketType.RESPONSE_AUTH */) {
if (callbacks.has(authId)) {
id = authId;
this._authPacketId = NaN;
callbacks.get(authId)(null, new RconError('Authentication failed.'));
}
}
if (callbacks.has(id) && type === 2 /* PacketType.RESPONSE_AUTH */) {
callbacks.get(id)("");
}
if (type === 0 /* PacketType.RESPONSE */) {
if (callbacks.has(id + 1)) {
this._tempResponse = this._tempResponse + payload;
}
else if (!callbacks.has(id + 1) && this._tempResponse.length > 0 && payload.length === 0) {
callbacks.get(id - 1)(this._tempResponse);
this._tempResponse = '';
callbacks.get(id)("");
}
}
// if (callbacks.has(id)) {
// let str = data.toString('utf8', 12, len + 2);
// if (str.charAt(str.length - 1) === '\n')
// str = str.substring(0, str.length - 1);
// callbacks.get(id)!(str);
// }
// callbacks.delete(id); // Possibly superfluous but best to be sure.
}
send(data) {
return __awaiter(this, void 0, void 0, function* () {
if (!this._connector || this._state <= 0)
throw new RconError('Instance is not connected.');
yield this._connector;
// console.log("exist callback: ", this._callbacks);
const response = this._send(data, 2 /* PacketType.COMMAND */);
this._send('', 0 /* PacketType.RESPONSE */);
return yield response;
});
}
sequentialSend(data) {
return __awaiter(this, void 0, void 0, function* () {
if (!this._connector || this._state <= 0)
throw new RconError('Instance is not connected.');
yield this._connector;
let responseList = [];
for (let i = 0; i < data.length; i++) {
const response = this._send(data[i], 2 /* PacketType.COMMAND */);
this._send('', 0 /* PacketType.RESPONSE */);
responseList.push(yield response);
}
return responseList;
});
}
_send(data, cmd) {
return __awaiter(this, void 0, void 0, function* () {
const s = this._socket;
if (!s || this._state <= 0)
throw new RconError('Instance was disconnected.');
const length = buffer_1.Buffer.byteLength(data);
const id = ++this._lastRequestId;
if (cmd === 3 /* PacketType.AUTH */)
this._authPacketId = id;
// console.log("Send command:", data, "current id: ", this._lastRequestId);
const buf = buffer_1.Buffer.allocUnsafe(length + 14);
buf.writeInt32LE(length + 10, 0);
buf.writeInt32LE(id, 4); // Not sure how this is used or needed.
buf.writeInt32LE(cmd, 8);
buf.write(data, 12);
buf.fill(0x00, length + 12);
yield s.write(buf, 'binary');
return yield new Promise((resolve, reject) => {
const cleanup = () => {
clearTimeout(timeout);
s.removeListener('end', onEnded);
this._callbacks.delete(id);
if (cmd === 3 /* PacketType.AUTH */)
this._authPacketId = NaN;
};
const timeout = setTimeout(() => {
cleanup();
reject(new RconError('Request timed out'));
}, this.timeout);
const onEnded = () => {
cleanup();
reject(new RconError('Disconnected before response.'));
};
s.once('end', onEnded);
this._callbacks.set(id, (data, err) => {
cleanup();
if (err)
reject(err);
if (data == null)
reject(new RconError("No data returned."));
else
resolve(data);
});
});
});
}
}
exports.Rcon = Rcon;
exports.default = Rcon;