152 lines
No EOL
4.8 KiB
TypeScript
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)
|
|
}
|
|
},
|
|
});
|
|
}
|
|
} |