rconts/index.ts
Your Name e6c5c14f32 hi
2025-02-28 22:23:19 +02:00

152 lines
No EOL
4.8 KiB
TypeScript

import { EventEmitter } from "tseep";
import { PacketReader, PacketWriter } from "./types";
import type { Socket } from "bun";
type Events = {
authenicated: () => void;
connected: () => void;
disconnected: () => void;
attemptingReauthenication: () => void;
response: (message: string, id: number) => void;
};
/**
* Simple RCON library with a custom client made specifically for Bun. Uses Bun's sockets for communication.
* Used in sophie's projects such as autowhitelist, see_leaderboard_bot and slopsite.
*/
export class Rcon extends EventEmitter<Events> {
private host: string;
private port: number;
private password?: string;
private reconnectionTime = 1000;
private reconnectionTimeout?: Timer;
private doNotReconnect = false;
private socket?: Socket<undefined>;
private responsePromises = new Map<number, (value: string | PromiseLike<string>) => void>();
/**
* @param host Host URL. Supports example.com:9000 as well as example.com
* @param port Can be undefined if host is formatted as "host:port".
*/
constructor(options: {
host: string,
port?: number;
password?: string;
}) {
super();
if (options.host.includes(":")) {
let split = options.host.split(":");
if (!split[1]) {
throw new Error("Port is undefined in host string")
}
if (isNaN(+split[1])) {
throw new Error("Port is invalid in host string")
}
this.host = split[0];
this.port = +split[1];
} else {
if (!options.port) {
throw new Error("Port is required if no port in host");
}
this.host = options.host;
this.port = options.port;
}
this.password = options.password;
}
private async onData(data: Uint8Array) {
const packet = new PacketReader(data).read(true);
if (packet.packetName == "SERVERDATA_AUTH_RESPONSE") {
this.emit("authenicated")
} else if (packet.packetName == "SERVERDATA_RESPONSE_VALUE") {
if (this.responsePromises.has(packet.id)) {
const promise = this.responsePromises.get(packet.id)!;
promise(packet.body);
}
}
}
public authenicate(password: string) {
if (!this.socket) {
throw new Error("Attempted to use .authenicate while not connected!")
}
this.socket.write(new PacketWriter({
packetName: "SERVERDATA_AUTH",
id: 0,
type: 3,
body: password
}).build())
}
public crash() {
this.socket?.write(new Uint8Array([0x00, 0x69, 0x42, 0x21]))
}
public close() {
this.doNotReconnect = true;
this.socket?.end();
}
public async exec(command: string): Promise<string> {
if (!this.socket) {
throw new Error("Attempted to use .exec while not connected!")
}
const id = Math.floor(Math.random() * 9999);
const promise = new Promise<string>(async (res, rej) => {
this.responsePromises.set(id, res);
})
this.socket?.write(new PacketWriter({
packetName: "SERVERDATA_EXECCOMMAND",
id: id,
type: 2,
body: command
}).build())
return promise;
}
public async connect() {
await Bun.connect({
hostname: this.host,
port: this.port,
socket: {
data: (socket, data) => {
this.onData(data);
},
open: (socket) => {
if (this.reconnectionTimeout) {
clearInterval(this.reconnectionTimeout);
this.reconnectionTimeout = undefined;
}
this.socket = socket;
this.emit("connected");
if (this.password) {
this.authenicate(this.password);
}
},
close: (socket) => {
this.socket = undefined; // obv, we disconnected so
if (!this.doNotReconnect) {
console.log(`[RCON] Reconnecting in ${this.reconnectionTime}ms.`)
this.reconnectionTimeout = setTimeout(() => {
this.reconnectionTimeout = undefined;
this.connect();
}, this.reconnectionTime)
}
this.emit("disconnected")
},
drain(socket) { },
error: (socket, error) => {
console.log(`[RCON] Error experienced.`, error)
}
},
});
}
}