From 195817832d9a38f7f19d4a67eb70c77f35a997af Mon Sep 17 00:00:00 2001
From: c43721 <rlahman.game@gmail.com>
Date: Tue, 12 Nov 2024 21:30:28 -0600
Subject: [PATCH] feat: add timeout to requests

---
 .gitignore   |  3 ++-
 cli.ts       |  1 +
 deno.json    |  4 ++--
 deno.lock    | 41 -----------------------------------------
 src/rcon.ts  | 24 ++++++++++++++----------
 src/types.ts |  5 +++++
 6 files changed, 24 insertions(+), 54 deletions(-)
 delete mode 100644 deno.lock

diff --git a/.gitignore b/.gitignore
index 600d2d3..346a7da 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
-.vscode
\ No newline at end of file
+.vscode
+deno.lock
\ No newline at end of file
diff --git a/cli.ts b/cli.ts
index 1b36c86..b6d7176 100644
--- a/cli.ts
+++ b/cli.ts
@@ -14,6 +14,7 @@ if (!args.password || !args.ip || !args.command) {
   using rcon = new Rcon({
     host: args.ip,
     port,
+    timeout: 5_000,
   });
 
   const didAuthenticate = await rcon.authenticate(args.password!);
diff --git a/deno.json b/deno.json
index f97c2c0..886797a 100644
--- a/deno.json
+++ b/deno.json
@@ -3,9 +3,9 @@
   "imports": {
     "@std/bytes": "jsr:@std/bytes@^1.0.2",
     "@std/cli": "jsr:@std/cli@^1.0.6",
-    "@std/io": "jsr:@std/io@^0.225.0"
+    "@std/async": "jsr:@std/async@^1.0.8"
   },
-  "version": "0.0.6",
+  "version": "0.0.7",
   "exports": "./mod.ts",
   "publish": {
     "include": ["README.md", "mod.ts", "src/"]
diff --git a/deno.lock b/deno.lock
deleted file mode 100644
index 633548e..0000000
--- a/deno.lock
+++ /dev/null
@@ -1,41 +0,0 @@
-{
-  "version": "4",
-  "specifiers": {
-    "jsr:@std/bytes@^1.0.2": "1.0.2",
-    "jsr:@std/cli@^1.0.6": "1.0.6",
-    "jsr:@std/io@0.225": "0.225.0",
-    "npm:@types/node@*": "18.16.19"
-  },
-  "jsr": {
-    "@std/bytes@1.0.2": {
-      "integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57"
-    },
-    "@std/cli@1.0.6": {
-      "integrity": "d22d8b38c66c666d7ad1f2a66c5b122da1704f985d3c47f01129f05abb6c5d3d"
-    },
-    "@std/io@0.225.0": {
-      "integrity": "c1db7c5e5a231629b32d64b9a53139445b2ca640d828c26bf23e1c55f8c079b3",
-      "dependencies": [
-        "jsr:@std/bytes"
-      ]
-    }
-  },
-  "npm": {
-    "@types/node@18.16.19": {
-      "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA=="
-    }
-  },
-  "remote": {
-    "https://deno.land/std@0.77.0/encoding/base64.ts": "b1d8f99b778981548457ec74bc6273ad785ffd6f61b2233bd5b30925345b565d",
-    "https://deno.land/std@0.77.0/encoding/hex.ts": "07a03ba41c96060a4ed4ba272e50b9e23f3c5b3839f4b069cdebc24d57434386",
-    "https://deno.land/std@0.77.0/node/_utils.ts": "3c3096695a3c6f926fb0d3e60f4bc534c5a28be3d6073e0eb6cd49654a675d68",
-    "https://deno.land/std@0.77.0/node/buffer.ts": "fa828a387ae0e044871a0337b66b044b47897dfb1bd64126b7204a0625fc8b30"
-  },
-  "workspace": {
-    "dependencies": [
-      "jsr:@std/bytes@^1.0.2",
-      "jsr:@std/cli@^1.0.6",
-      "jsr:@std/io@0.225"
-    ]
-  }
-}
diff --git a/src/rcon.ts b/src/rcon.ts
index af03b0a..dd26043 100644
--- a/src/rcon.ts
+++ b/src/rcon.ts
@@ -2,6 +2,7 @@ import { protocol } from "./protocol.ts";
 import { concat } from "@std/bytes";
 import { createConnection, type Socket } from "node:net";
 import { encode, decode } from "./packet.ts";
+import { abortable } from "@std/async";
 import {
   NotAuthenticatedException,
   NotConnectedException,
@@ -34,6 +35,8 @@ import type { RconOptions } from "./types.ts";
 export class Rcon {
   #host: string;
   #port: number;
+  #timeout: number;
+
   #connection?: Socket;
   #connected = false;
   #authenticated = false;
@@ -44,10 +47,11 @@ export class Rcon {
    * @param {RconOptions} options The connection options
    */
   constructor(options: RconOptions) {
-    const { host, port = 27015 } = options;
+    const { host, port = 27015, timeout = 30_000 } = options;
 
     this.#host = host;
     this.#port = port;
+    this.#timeout = timeout;
   }
 
   /**
@@ -74,7 +78,7 @@ export class Rcon {
   /**
    * Authenticates the connection
    * @param password The RCON password
-   * 
+   *
    * @returns {Promise<boolean>} The result of the authentication
    */
   public async authenticate(password: string): Promise<boolean> {
@@ -82,10 +86,9 @@ export class Rcon {
       this.#connect();
     }
 
-    const response = await this.#send(
-      protocol.SERVERDATA_AUTH,
-      protocol.ID_AUTH,
-      password
+    const response = await abortable(
+      this.#send(protocol.SERVERDATA_AUTH, protocol.ID_AUTH, password),
+      AbortSignal.timeout(this.#timeout)
     );
 
     if (response === "true") {
@@ -100,7 +103,7 @@ export class Rcon {
   /**
    * Executes a command on the server
    * @param command The command to execute
-   * 
+   *
    * @returns {Promise<string>} The result of the execution
    */
   public async execute(command: string): Promise<string> {
@@ -114,7 +117,10 @@ export class Rcon {
 
     const packetId = Math.floor(Math.random() * (256 - 1) + 1);
 
-    return await this.#send(protocol.SERVERDATA_EXECCOMMAND, packetId, command);
+    return await abortable(
+      this.#send(protocol.SERVERDATA_EXECCOMMAND, packetId, command),
+      AbortSignal.timeout(this.#timeout)
+    );
   }
 
   /**
@@ -185,7 +191,6 @@ export class Rcon {
         (decodedPacket.type === protocol.SERVERDATA_RESPONSE_VALUE ||
           decodedPacket.id === protocol.ID_TERM)
       ) {
-        // concat the response- even if it's not a multipacket response
         if (decodedPacket.id != protocol.ID_TERM) {
           potentialMultiPacketResponse = concat([
             potentialMultiPacketResponse,
@@ -204,7 +209,6 @@ export class Rcon {
 
           this.#connection!.write(encodedTerminationPacket);
         } else if (decodedPacket.size <= 3700) {
-          // no need to check for ID_TERM here, since this packet will always be < 3700
           return new TextDecoder().decode(potentialMultiPacketResponse);
         }
       }
diff --git a/src/types.ts b/src/types.ts
index 9a3bc80..a5b52d6 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -11,4 +11,9 @@ export interface RconOptions {
    * (Optional- Default "27017") The port to connect to
    */
   port?: number;
+
+  /**
+   * (Optional- Default "30000") The timeout in milliseconds before a request is aborted
+   */
+  timeout?: number;
 }