105 lines
3.3 KiB
TypeScript
105 lines
3.3 KiB
TypeScript
import { Rcon } from "rcon-client";
|
|
import { sanitizeRconOutput } from "./rcon-validation";
|
|
|
|
const TIMEOUT_MS = 5_000;
|
|
const CLEANUP_TIMEOUT_MS = 1_000;
|
|
const MAX_ACTIVE_CONNECTIONS = 8;
|
|
const activeConnections = new Set<string>();
|
|
|
|
type Connection = { id?: string; host: string; port: number; password: string };
|
|
type FailureReason = "busy" | "timeout" | "unavailable";
|
|
|
|
export interface RconTransport {
|
|
connect(): Promise<unknown>;
|
|
send(command: string): Promise<string>;
|
|
end(): Promise<unknown>;
|
|
destroy?(): void;
|
|
}
|
|
|
|
type TransportFactory = (connection: Connection) => RconTransport;
|
|
|
|
class RconDeadlineError extends Error {}
|
|
|
|
async function deadline<T>(operation: Promise<T>, timeout: () => void, timeoutMs = TIMEOUT_MS) {
|
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
try {
|
|
return await Promise.race([
|
|
operation,
|
|
new Promise<never>((_, reject) => {
|
|
timer = setTimeout(() => {
|
|
timeout();
|
|
reject(new RconDeadlineError("RCON operation timed out"));
|
|
}, timeoutMs);
|
|
}),
|
|
]);
|
|
} finally {
|
|
if (timer) clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
function defaultTransport(connection: Connection): RconTransport {
|
|
const client = new Rcon({
|
|
host: connection.host,
|
|
port: connection.port,
|
|
password: connection.password,
|
|
timeout: TIMEOUT_MS,
|
|
maxPending: 1,
|
|
});
|
|
return {
|
|
connect: () => client.connect(),
|
|
send: (command) => client.send(command),
|
|
end: async () => {
|
|
if (!client.socket) return;
|
|
if (client.socket.connecting || !client.socket.writable) {
|
|
client.socket.destroy();
|
|
return;
|
|
}
|
|
await client.end();
|
|
},
|
|
destroy: () => client.socket?.destroy(),
|
|
};
|
|
}
|
|
|
|
function failure(error: unknown): { ok: false; reason: FailureReason } {
|
|
return { ok: false, reason: error instanceof RconDeadlineError ? "timeout" : "unavailable" };
|
|
}
|
|
|
|
async function withTransport<T>(
|
|
connection: Connection,
|
|
operation: (transport: RconTransport) => Promise<T>,
|
|
factory: TransportFactory,
|
|
): Promise<T | { ok: false; reason: FailureReason }> {
|
|
const key = connection.id ?? `${connection.host}:${connection.port}`;
|
|
if (activeConnections.has(key) || activeConnections.size >= MAX_ACTIVE_CONNECTIONS) {
|
|
return { ok: false, reason: "busy" };
|
|
}
|
|
activeConnections.add(key);
|
|
let transport: RconTransport | null = null;
|
|
try {
|
|
transport = factory(connection);
|
|
return await deadline(operation(transport), () => transport?.destroy?.());
|
|
} catch (error) {
|
|
return failure(error);
|
|
} finally {
|
|
if (transport) {
|
|
await deadline(transport.end(), () => transport?.destroy?.(), CLEANUP_TIMEOUT_MS).catch(() => undefined);
|
|
}
|
|
activeConnections.delete(key);
|
|
}
|
|
}
|
|
|
|
export async function testRconConnection(connection: Connection, factory: TransportFactory = defaultTransport) {
|
|
return withTransport(connection, async (transport) => {
|
|
await transport.connect();
|
|
return { ok: true as const };
|
|
}, factory);
|
|
}
|
|
|
|
export async function executeRcon(connection: Connection, command: string, factory: TransportFactory = defaultTransport) {
|
|
return withTransport(connection, async (transport) => {
|
|
await transport.connect();
|
|
const response = await transport.send(command);
|
|
return { ok: true as const, response: sanitizeRconOutput(response) };
|
|
}, factory);
|
|
}
|