tsrcon-client
Version:
A TypeScript RCON client for communicating with a RCON Server.
171 lines (170 loc) • 5.31 kB
JavaScript
// src/index.ts
import { Socket } from "net";
import { randomInt } from "crypto";
var RCONClient = class {
constructor(host, port, password, opts) {
this.host = host;
this.port = port;
this.password = password;
this.buffer = Buffer.alloc(0);
this.isConnected = false;
this.reconnectAttempts = 0;
this.authenticated = false;
this.callbacks = /* @__PURE__ */ new Map();
this.requestQueue = [];
this.processingQueue = false;
this.lastRequestTime = 0;
this.socket = new Socket();
this.options = {
timeout: opts?.timeout ?? 3e3,
retries: opts?.retries ?? 1,
reconnect: opts?.reconnect ?? true,
reconnectDelay: opts?.reconnectDelay ?? 2e3,
maxReconnectAttempts: opts?.maxReconnectAttempts ?? 5,
minDelayBetweenRequests: opts?.minDelayBetweenRequests ?? 0
};
this.socket.on("data", this.handleData.bind(this));
this.socket.on("error", (err) => {
for (const cb of this.callbacks.values()) cb.reject(err);
this.callbacks.clear();
});
this.socket.on("close", () => {
this.isConnected = false;
this.authenticated = false;
if (this.options.reconnect) {
this.tryReconnect();
}
});
}
async connect() {
return new Promise((resolve, reject) => {
this.socket.connect(this.port, this.host, async () => {
try {
await this.authenticate();
this.isConnected = true;
this.reconnectAttempts = 0;
resolve();
} catch (e) {
reject(e);
}
});
});
}
disconnect() {
this.socket.end();
this.socket.destroy();
this.isConnected = false;
this.authenticated = false;
}
async tryReconnect() {
if (this.reconnectAttempts >= this.options.maxReconnectAttempts) return;
this.reconnectAttempts++;
setTimeout(async () => {
try {
await this.connect();
console.log("RCON: Reconnected successfully");
} catch (err) {
console.error("RCON: Reconnect failed:", err);
this.tryReconnect();
}
}, this.options.reconnectDelay);
}
handleData(data) {
this.buffer = Buffer.concat([this.buffer, data]);
while (this.buffer.length >= 4) {
const packetLength = this.buffer.readInt32LE(0);
if (this.buffer.length < packetLength + 4) return;
const packet = this.buffer.subarray(4, 4 + packetLength);
const requestId = packet.readInt32LE(0);
const type = packet.readInt32LE(4);
const body = packet.toString("utf8", 8, packet.length - 2);
this.buffer = this.buffer.subarray(4 + packetLength);
const cb = this.callbacks.get(requestId);
if (!cb) continue;
if (type === 2 /* AUTH_RESPONSE */ && requestId === -1) {
cb.reject(new Error("Authentication failed"));
this.callbacks.delete(requestId);
continue;
}
cb.buffer.push(body);
clearTimeout(cb.timeout);
cb.timeout = setTimeout(() => {
cb.resolve(cb.buffer.join(""));
this.callbacks.delete(requestId);
}, 10);
}
}
sendPacket(type, body) {
const requestId = randomInt(1, 2147483647);
const payload = Buffer.from(body + "\0", "utf8");
const size = 4 + 4 + payload.length + 1;
const buf = Buffer.alloc(4 + size);
buf.writeInt32LE(size, 0);
buf.writeInt32LE(requestId, 4);
buf.writeInt32LE(type, 8);
payload.copy(buf, 12);
buf.writeUInt8(0, 12 + payload.length);
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.callbacks.delete(requestId);
reject(new Error("RCON request timed out"));
}, this.options.timeout);
this.callbacks.set(requestId, {
resolve,
reject,
buffer: [],
timeout
});
this.socket.write(buf);
});
}
async authenticate() {
for (let attempt = 1; attempt <= this.options.retries + 1; attempt++) {
try {
await this.sendPacket(3 /* AUTH */, this.password);
this.authenticated = true;
return;
} catch (err) {
if (attempt > this.options.retries) throw err;
}
}
}
async sendCommand(command) {
return new Promise((resolve, reject) => {
const task = async () => {
const now = Date.now();
const delay = Math.max(this.options.minDelayBetweenRequests - (now - this.lastRequestTime), 0);
if (delay > 0) await new Promise((r) => setTimeout(r, delay));
try {
if (!this.authenticated) throw new Error("Not authenticated");
const result = await this.sendPacket(2 /* COMMAND */, command);
this.lastRequestTime = Date.now();
resolve(result);
} catch (err) {
reject(err);
}
this.processingQueue = false;
this.processQueue();
};
this.requestQueue.push(task);
this.processQueue();
});
}
async processQueue() {
if (this.processingQueue || this.requestQueue.length === 0) return;
this.processingQueue = true;
const next = this.requestQueue.shift();
if (next) await next();
}
async ping() {
try {
const result = await this.sendCommand("list");
return !!result;
} catch {
return false;
}
}
};
export {
RCONClient
};