From fc628e3bea73cc36076268a77c02ff0cc5cdf613 Mon Sep 17 00:00:00 2001
From: Your Name <you@example.com>
Date: Mon, 10 Feb 2025 14:30:13 -0800
Subject: [PATCH 01/14] add websocket test

---
 server/monitor-types/websocket.js | 69 +++++++++++++++++++++++++++++++
 server/uptime-kuma-server.js      |  2 +
 src/pages/EditMonitor.vue         |  6 ++-
 3 files changed, 76 insertions(+), 1 deletion(-)
 create mode 100644 server/monitor-types/websocket.js

diff --git a/server/monitor-types/websocket.js b/server/monitor-types/websocket.js
new file mode 100644
index 000000000..47ac1df2a
--- /dev/null
+++ b/server/monitor-types/websocket.js
@@ -0,0 +1,69 @@
+const { MonitorType } = require("./monitor-type");
+const { UP } = require("../../src/util");
+const childProcessAsync = require("promisify-child-process");
+
+class websocket extends MonitorType {
+    name = "websocket";
+
+    /**
+     * @inheritdoc
+     */
+    async check(monitor, heartbeat, _server) {
+        //let status_code = await this.attemptUpgrade(monitor.url);
+        let statusCode = await this.curlTest(monitor.url);
+        this.updateStatus(heartbeat, statusCode);
+    }
+
+    /**
+     * Attempts to upgrade HTTP/HTTPs connection to Websocket. Use curl to send websocket headers to server and returns response code. Close the connection after 1 second and wrap command in bash to return exit code 0 instead of 28.
+     * @param {string} url Full URL of Websocket server
+     * @returns {string} HTTP response code
+     */
+    async curlTest(url) {
+        let res = await childProcessAsync.spawn("bash", [ "-c", "curl -s -o /dev/null -w '%{http_code}' --http1.1 -N --max-time 1 -H 'Upgrade: websocket' -H 'Sec-WebSocket-Key: test' -H 'Sec-WebSocket-Version: 13' " + url + " || true" ], {
+            timeout: 5000,
+            encoding: "utf8",
+        });
+        return res.stdout.toString();
+    }
+
+    /**
+     * Checks if status code is 101 and sets status
+     * @param {object} heartbeat The heartbeat object to update.
+     * @param {string} statusCode Status code from curl
+     * @returns {void}
+     */
+    updateStatus(heartbeat, statusCode) {
+        if (statusCode === "101") {
+            heartbeat.status = UP;
+        }
+        heartbeat.msg = statusCode;
+    }
+
+    // Attempt at using websocket library. Abandoned this idea because of the lack of control of headers. Certain websocket servers don't return the Sec-WebSocket-Accept, which causes websocket to error out.
+    // async attemptUpgrade(hostname) {
+    //     return new Promise((resolve) => {
+    //         const ws = new WebSocket('wss://' + hostname);
+
+    //         ws.addEventListener("open", (event) => {
+    //             ws.close();
+    //         });
+
+    //         ws.onerror = (error) => {
+    //             console.log(error.message);
+    //         };
+
+    //         ws.onclose = (event) => {
+    //             if (event.code === 1005) {
+    //                 resolve(true);
+    //             } else {
+    //                 resolve(false);
+    //             }
+    //         };
+    //     })
+    // }
+}
+
+module.exports = {
+    websocket,
+};
diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js
index 062f098d7..dcf803270 100644
--- a/server/uptime-kuma-server.js
+++ b/server/uptime-kuma-server.js
@@ -111,6 +111,7 @@ class UptimeKumaServer {
         // Set Monitor Types
         UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
         UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
+        UptimeKumaServer.monitorTypeList["websocket"] = new websocket();
         UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType();
         UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
         UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
@@ -549,6 +550,7 @@ module.exports = {
 // Must be at the end to avoid circular dependencies
 const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor-type");
 const { TailscalePing } = require("./monitor-types/tailscale-ping");
+const { websocket } = require("./monitor-types/websocket");
 const { DnsMonitorType } = require("./monitor-types/dns");
 const { MqttMonitorType } = require("./monitor-types/mqtt");
 const { SNMPMonitorType } = require("./monitor-types/snmp");
diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue
index a83f91cab..b17b6be24 100644
--- a/src/pages/EditMonitor.vue
+++ b/src/pages/EditMonitor.vue
@@ -46,6 +46,10 @@
                                         <option value="real-browser">
                                             HTTP(s) - Browser Engine (Chrome/Chromium) (Beta)
                                         </option>
+
+                                        <option value="websocket">
+                                            Websocket Upgrade
+                                        </option>
                                     </optgroup>
 
                                     <optgroup :label="$t('Passive Monitor Type')">
@@ -113,7 +117,7 @@
                             </div>
 
                             <!-- URL -->
-                            <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'real-browser' " class="my-3">
+                            <div v-if="monitor.type === 'websocket' || monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'real-browser' " class="my-3">
                                 <label for="url" class="form-label">{{ $t("URL") }}</label>
                                 <input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required data-testid="url-input">
                             </div>

From 587699d7b3a523c977a977d8cdadc737df65189d Mon Sep 17 00:00:00 2001
From: Your Name <you@example.com>
Date: Mon, 17 Feb 2025 17:04:58 -0800
Subject: [PATCH 02/14] Add Websocket Test v2

---
 README.md                                     |  2 +-
 .../2025-02-15-2312-add-wstest.js             | 15 ++++
 server/model/monitor.js                       | 10 +++
 server/monitor-types/websocket.js             | 64 +++++++++-------
 server/server.js                              |  2 +
 src/lang/en.json                              |  1 +
 src/pages/EditMonitor.vue                     | 24 +++++-
 test/backend-test/test-websocket.js           | 76 +++++++++++++++++++
 8 files changed, 163 insertions(+), 31 deletions(-)
 create mode 100644 db/knex_migrations/2025-02-15-2312-add-wstest.js
 create mode 100644 test/backend-test/test-websocket.js

diff --git a/README.md b/README.md
index 34e34020f..d0b1ac17e 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ It is a temporary live demo, all data will be deleted after 10 minutes. Sponsore
 
 ## ⭐ Features
 
-- Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / HTTP(s) Json Query / Ping / DNS Record / Push / Steam Game Server / Docker Containers
+- Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / HTTP(s) Json Query / Websocket / Ping / DNS Record / Push / Steam Game Server / Docker Containers
 - Fancy, Reactive, Fast UI/UX
 - Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications)
 - 20-second intervals
diff --git a/db/knex_migrations/2025-02-15-2312-add-wstest.js b/db/knex_migrations/2025-02-15-2312-add-wstest.js
new file mode 100644
index 000000000..966cff13c
--- /dev/null
+++ b/db/knex_migrations/2025-02-15-2312-add-wstest.js
@@ -0,0 +1,15 @@
+// Add websocket URL
+exports.up = function (knex) {
+    return knex.schema
+        .alterTable("monitor", function (table) {
+            table.text("wsurl");
+            table.boolean("ws_ignore_headers").notNullable().defaultTo(false);
+        });
+};
+
+exports.down = function (knex) {
+    return knex.schema.alterTable("monitor", function (table) {
+        table.dropColumn("wsurl");
+        table.dropColumn("ws_ignore_headers");
+    });
+};
diff --git a/server/model/monitor.js b/server/model/monitor.js
index 3ad8cfafc..77498fa3e 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -96,6 +96,8 @@ class Monitor extends BeanModel {
             parent: this.parent,
             childrenIDs: preloadData.childrenIDs.get(this.id) || [],
             url: this.url,
+            wsurl: this.wsurl,
+            wsIgnoreHeaders: this.getWsIgnoreHeaders(),
             method: this.method,
             hostname: this.hostname,
             port: this.port,
@@ -255,6 +257,14 @@ class Monitor extends BeanModel {
         return Boolean(this.ignoreTls);
     }
 
+    /**
+     * Parse to boolean
+     * @returns {boolean} Should WS headers be ignored?
+     */
+    getWsIgnoreHeaders() {
+        return Boolean(this.wsIgnoreHeaders);
+    }
+
     /**
      * Parse to boolean
      * @returns {boolean} Is the monitor in upside down mode?
diff --git a/server/monitor-types/websocket.js b/server/monitor-types/websocket.js
index 47ac1df2a..3e84b5b83 100644
--- a/server/monitor-types/websocket.js
+++ b/server/monitor-types/websocket.js
@@ -1,5 +1,6 @@
 const { MonitorType } = require("./monitor-type");
-const { UP } = require("../../src/util");
+const WebSocket = require("ws");
+const { UP, DOWN } = require("../../src/util");
 const childProcessAsync = require("promisify-child-process");
 
 class websocket extends MonitorType {
@@ -9,8 +10,8 @@ class websocket extends MonitorType {
      * @inheritdoc
      */
     async check(monitor, heartbeat, _server) {
-        //let status_code = await this.attemptUpgrade(monitor.url);
-        let statusCode = await this.curlTest(monitor.url);
+        let statusCode = await this.attemptUpgrade(monitor);
+        //let statusCode = await this.curlTest(monitor.url);
         this.updateStatus(heartbeat, statusCode);
     }
 
@@ -28,40 +29,45 @@ class websocket extends MonitorType {
     }
 
     /**
-     * Checks if status code is 101 and sets status
+     * Checks if status code is 1000(Normal Closure) and sets status and message
      * @param {object} heartbeat The heartbeat object to update.
-     * @param {string} statusCode Status code from curl
+     * @param {[ string, int ]} status Array containing a status message and response code
      * @returns {void}
      */
-    updateStatus(heartbeat, statusCode) {
-        if (statusCode === "101") {
-            heartbeat.status = UP;
-        }
-        heartbeat.msg = statusCode;
+    updateStatus(heartbeat, [ message, code ]) {
+        heartbeat.status = code === 1000 ? UP : DOWN;
+        heartbeat.msg = message;
     }
 
-    // Attempt at using websocket library. Abandoned this idea because of the lack of control of headers. Certain websocket servers don't return the Sec-WebSocket-Accept, which causes websocket to error out.
-    // async attemptUpgrade(hostname) {
-    //     return new Promise((resolve) => {
-    //         const ws = new WebSocket('wss://' + hostname);
+    /**
+     * Uses the builtin Websocket API to establish a connection to target server
+     * @param {object} monitor The monitor object for input parameters.
+     * @returns {[ string, int ]} Array containing a status message and response code
+     */
+    async attemptUpgrade(monitor) {
+        return new Promise((resolve) => {
+            const ws = new WebSocket(monitor.wsurl);
 
-    //         ws.addEventListener("open", (event) => {
-    //             ws.close();
-    //         });
+            ws.addEventListener("open", (event) => {
+                ws.close(1000);
+            });
 
-    //         ws.onerror = (error) => {
-    //             console.log(error.message);
-    //         };
+            ws.onerror = (error) => {
+                // console.log(error.message);
+                // Give user the choice to ignore Sec-WebSocket-Accept header
+                if (monitor.wsIgnoreHeaders && error.message === "Invalid Sec-WebSocket-Accept header") {
+                    resolve([ "101 - OK", 1000 ]);
+                }
+                resolve([ error.message, error.code ]);
+            };
 
-    //         ws.onclose = (event) => {
-    //             if (event.code === 1005) {
-    //                 resolve(true);
-    //             } else {
-    //                 resolve(false);
-    //             }
-    //         };
-    //     })
-    // }
+            ws.onclose = (event) => {
+                // console.log(event.message);
+                // console.log(event.code);
+                resolve([ "101 - OK", event.code ]);
+            };
+        });
+    }
 }
 
 module.exports = {
diff --git a/server/server.js b/server/server.js
index ec5ad49f6..0e19b6e62 100644
--- a/server/server.js
+++ b/server/server.js
@@ -790,6 +790,8 @@ let needSetup = false;
                 bean.parent = monitor.parent;
                 bean.type = monitor.type;
                 bean.url = monitor.url;
+                bean.wsurl = monitor.wsurl;
+                bean.wsIgnoreHeaders = monitor.wsIgnoreHeaders;
                 bean.method = monitor.method;
                 bean.body = monitor.body;
                 bean.headers = monitor.headers;
diff --git a/src/lang/en.json b/src/lang/en.json
index e215f1031..81adc0736 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -86,6 +86,7 @@
     "ignoreTLSError": "Ignore TLS/SSL errors for HTTPS websites",
     "ignoreTLSErrorGeneral": "Ignore TLS/SSL error for connection",
     "upsideDownModeDescription": "Flip the status upside down. If the service is reachable, it is DOWN.",
+    "wsIgnoreHeadersDescription": "Test non compliant Websocket servers that don't respond with correct headers.",
     "maxRedirectDescription": "Maximum number of redirects to follow. Set to 0 to disable redirects.",
     "Upside Down Mode": "Upside Down Mode",
     "Max. Redirects": "Max. Redirects",
diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue
index b17b6be24..679eac298 100644
--- a/src/pages/EditMonitor.vue
+++ b/src/pages/EditMonitor.vue
@@ -117,11 +117,17 @@
                             </div>
 
                             <!-- URL -->
-                            <div v-if="monitor.type === 'websocket' || monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'real-browser' " class="my-3">
+                            <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'real-browser' " class="my-3">
                                 <label for="url" class="form-label">{{ $t("URL") }}</label>
                                 <input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required data-testid="url-input">
                             </div>
 
+                            <!-- Websocket -->
+                            <div v-if="monitor.type === 'websocket'" class="my-3">
+                                <label for="wsurl" class="form-label">{{ $t("URL") }}</label>
+                                <input id="wsurl" v-model="monitor.wsurl" type="wsurl" class="form-control" pattern="wss?://.+" required data-testid="url-input">
+                            </div>
+
                             <!-- gRPC URL -->
                             <div v-if="monitor.type === 'grpc-keyword' " class="my-3">
                                 <label for="grpc-url" class="form-label">{{ $t("URL") }}</label>
@@ -625,6 +631,16 @@
                                 </div>
                             </div>
 
+                            <div v-if="monitor.type === 'websocket' " class="my-3 form-check">
+                                <input id="ws-ignore-headers" v-model="monitor.wsIgnoreHeaders" class="form-check-input" type="checkbox">
+                                <label class="form-check-label" for="ws-ignore-headers">
+                                    {{ $t("Ignore Server Headers") }}
+                                </label>
+                                <div class="form-text">
+                                    {{ $t("wsIgnoreHeadersDescription") }}
+                                </div>
+                            </div>
+
                             <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'redis' " class="my-3 form-check">
                                 <input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
                                 <label class="form-check-label" for="ignore-tls">
@@ -1078,6 +1094,8 @@ const monitorDefaults = {
     name: "",
     parent: null,
     url: "https://",
+    wsurl: "wss://",
+    wsIgnoreHeaders: false,
     method: "GET",
     interval: 60,
     retryInterval: 60,
@@ -1731,6 +1749,10 @@ message HealthCheckResponse {
                 this.monitor.url = this.monitor.url.trim();
             }
 
+            if (this.monitor.wsurl) {
+                this.monitor.wsurl = this.monitor.wsurl.trim();
+            }
+
             let createdNewParent = false;
 
             if (this.draftGroupName && this.monitor.parent === -1) {
diff --git a/test/backend-test/test-websocket.js b/test/backend-test/test-websocket.js
new file mode 100644
index 000000000..363406428
--- /dev/null
+++ b/test/backend-test/test-websocket.js
@@ -0,0 +1,76 @@
+const test = require("node:test");
+const assert = require("node:assert");
+const { websocket } = require("../../server/monitor-types/websocket");
+const { UP, DOWN, PENDING } = require("../../src/util");
+
+test("Test Websocket TLS", async () => {
+    const websocketMonitor = new websocket();
+
+    const monitor = {
+        wsurl: "wss://echo.websocket.org",
+        wsIgnoreHeaders: false,
+    };
+
+    const heartbeat = {
+        msg: "",
+        status: PENDING,
+    };
+
+    await websocketMonitor.check(monitor, heartbeat, {});
+    assert.strictEqual(heartbeat.status, UP);
+    assert.strictEqual(heartbeat.msg, "101 - OK");
+});
+
+test("Test Websocket non TLS", async () => {
+    const websocketMonitor = new websocket();
+
+    const monitor = {
+        wsurl: "ws://ws.ifelse.io",
+        wsIgnoreHeaders: false,
+    };
+
+    const heartbeat = {
+        msg: "",
+        status: PENDING,
+    };
+
+    await websocketMonitor.check(monitor, heartbeat, {});
+    assert.strictEqual(heartbeat.status, UP);
+    assert.strictEqual(heartbeat.msg, "101 - OK");
+});
+
+test("Test a non compliant WS server without ignore", async () => {
+    const websocketMonitor = new websocket();
+
+    const monitor = {
+        wsurl: "wss://c.img-cdn.net/yE4s7KehTFyj/",
+        wsIgnoreHeaders: false,
+    };
+
+    const heartbeat = {
+        msg: "",
+        status: PENDING,
+    };
+
+    await websocketMonitor.check(monitor, heartbeat, {});
+    assert.strictEqual(heartbeat.status, DOWN);
+    assert.strictEqual(heartbeat.msg, "Invalid Sec-WebSocket-Accept header");
+});
+
+test("Test a non compliant WS server with ignore", async () => {
+    const websocketMonitor = new websocket();
+
+    const monitor = {
+        wsurl: "wss://c.img-cdn.net/yE4s7KehTFyj/",
+        wsIgnoreHeaders: true,
+    };
+
+    const heartbeat = {
+        msg: "",
+        status: PENDING,
+    };
+
+    await websocketMonitor.check(monitor, heartbeat, {});
+    assert.strictEqual(heartbeat.status, UP);
+    assert.strictEqual(heartbeat.msg, "101 - OK");
+});

From dcb07a5e2e96934dc5717676f3ae11dfbe8e8863 Mon Sep 17 00:00:00 2001
From: Your Name <you@example.com>
Date: Mon, 17 Feb 2025 18:01:33 -0800
Subject: [PATCH 03/14] update tests

---
 test/backend-test/test-websocket.js | 157 ++++++++++++++++------------
 1 file changed, 89 insertions(+), 68 deletions(-)

diff --git a/test/backend-test/test-websocket.js b/test/backend-test/test-websocket.js
index 363406428..d698a6184 100644
--- a/test/backend-test/test-websocket.js
+++ b/test/backend-test/test-websocket.js
@@ -1,76 +1,97 @@
-const test = require("node:test");
+const { describe, test } = require("node:test");
 const assert = require("node:assert");
 const { websocket } = require("../../server/monitor-types/websocket");
 const { UP, DOWN, PENDING } = require("../../src/util");
 
-test("Test Websocket TLS", async () => {
-    const websocketMonitor = new websocket();
+describe("Websocket Test", {
+}, () => {
+    test("Non Websocket Server", async () => {
+        const websocketMonitor = new websocket();
 
-    const monitor = {
-        wsurl: "wss://echo.websocket.org",
-        wsIgnoreHeaders: false,
-    };
+        const monitor = {
+            wsurl: "wss://example.org",
+            wsIgnoreHeaders: false,
+        };
 
-    const heartbeat = {
-        msg: "",
-        status: PENDING,
-    };
+        const heartbeat = {
+            msg: "",
+            status: PENDING,
+        };
 
-    await websocketMonitor.check(monitor, heartbeat, {});
-    assert.strictEqual(heartbeat.status, UP);
-    assert.strictEqual(heartbeat.msg, "101 - OK");
-});
-
-test("Test Websocket non TLS", async () => {
-    const websocketMonitor = new websocket();
-
-    const monitor = {
-        wsurl: "ws://ws.ifelse.io",
-        wsIgnoreHeaders: false,
-    };
-
-    const heartbeat = {
-        msg: "",
-        status: PENDING,
-    };
-
-    await websocketMonitor.check(monitor, heartbeat, {});
-    assert.strictEqual(heartbeat.status, UP);
-    assert.strictEqual(heartbeat.msg, "101 - OK");
-});
-
-test("Test a non compliant WS server without ignore", async () => {
-    const websocketMonitor = new websocket();
-
-    const monitor = {
-        wsurl: "wss://c.img-cdn.net/yE4s7KehTFyj/",
-        wsIgnoreHeaders: false,
-    };
-
-    const heartbeat = {
-        msg: "",
-        status: PENDING,
-    };
-
-    await websocketMonitor.check(monitor, heartbeat, {});
-    assert.strictEqual(heartbeat.status, DOWN);
-    assert.strictEqual(heartbeat.msg, "Invalid Sec-WebSocket-Accept header");
-});
-
-test("Test a non compliant WS server with ignore", async () => {
-    const websocketMonitor = new websocket();
-
-    const monitor = {
-        wsurl: "wss://c.img-cdn.net/yE4s7KehTFyj/",
-        wsIgnoreHeaders: true,
-    };
-
-    const heartbeat = {
-        msg: "",
-        status: PENDING,
-    };
-
-    await websocketMonitor.check(monitor, heartbeat, {});
-    assert.strictEqual(heartbeat.status, UP);
-    assert.strictEqual(heartbeat.msg, "101 - OK");
+        await websocketMonitor.check(monitor, heartbeat, {});
+        assert.strictEqual(heartbeat.status, DOWN);
+        assert.strictEqual(heartbeat.msg, "Unexpected server response: 200");
+    });
+
+    test("Secure Websocket", async () => {
+        const websocketMonitor = new websocket();
+
+        const monitor = {
+            wsurl: "wss://echo.websocket.org",
+            wsIgnoreHeaders: false,
+        };
+
+        const heartbeat = {
+            msg: "",
+            status: PENDING,
+        };
+
+        await websocketMonitor.check(monitor, heartbeat, {});
+        assert.strictEqual(heartbeat.status, UP);
+        assert.strictEqual(heartbeat.msg, "101 - OK");
+    });
+
+    test("Insecure Websocket", async () => {
+        const websocketMonitor = new websocket();
+
+        const monitor = {
+            wsurl: "ws://ws.ifelse.io",
+            wsIgnoreHeaders: false,
+        };
+
+        const heartbeat = {
+            msg: "",
+            status: PENDING,
+        };
+
+        await websocketMonitor.check(monitor, heartbeat, {});
+        assert.strictEqual(heartbeat.status, UP);
+        assert.strictEqual(heartbeat.msg, "101 - OK");
+    });
+
+    test("Test a non compliant WS server without ignore", async () => {
+        const websocketMonitor = new websocket();
+
+        const monitor = {
+            wsurl: "wss://c.img-cdn.net/yE4s7KehTFyj/",
+            wsIgnoreHeaders: false,
+        };
+
+        const heartbeat = {
+            msg: "",
+            status: PENDING,
+        };
+
+        await websocketMonitor.check(monitor, heartbeat, {});
+        assert.strictEqual(heartbeat.status, DOWN);
+        assert.strictEqual(heartbeat.msg, "Invalid Sec-WebSocket-Accept header");
+    });
+
+    test("Test a non compliant WS server with ignore", async () => {
+        const websocketMonitor = new websocket();
+
+        const monitor = {
+            wsurl: "wss://c.img-cdn.net/yE4s7KehTFyj/",
+            wsIgnoreHeaders: true,
+        };
+
+        const heartbeat = {
+            msg: "",
+            status: PENDING,
+        };
+
+        await websocketMonitor.check(monitor, heartbeat, {});
+        assert.strictEqual(heartbeat.status, UP);
+        assert.strictEqual(heartbeat.msg, "101 - OK");
+    });
 });

From 3a61b2f6ab151773158957a683bf3e6914d74729 Mon Sep 17 00:00:00 2001
From: Your Name <you@example.com>
Date: Mon, 17 Feb 2025 19:06:10 -0800
Subject: [PATCH 04/14] macos skip insecure test

---
 test/backend-test/test-websocket.js | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/test/backend-test/test-websocket.js b/test/backend-test/test-websocket.js
index d698a6184..c3f3ffb0e 100644
--- a/test/backend-test/test-websocket.js
+++ b/test/backend-test/test-websocket.js
@@ -5,7 +5,7 @@ const { UP, DOWN, PENDING } = require("../../src/util");
 
 describe("Websocket Test", {
 }, () => {
-    test("Non Websocket Server", async () => {
+    test("Non Websocket Server", {}, async () => {
         const websocketMonitor = new websocket();
 
         const monitor = {
@@ -41,7 +41,9 @@ describe("Websocket Test", {
         assert.strictEqual(heartbeat.msg, "101 - OK");
     });
 
-    test("Insecure Websocket", async () => {
+    test("Insecure Websocket", {
+        skip: !!process.env.CI && process.platform === "darwin",
+    }, async () => {
         const websocketMonitor = new websocket();
 
         const monitor = {

From b0fb6ab5683e11343df707dc1cb1ee882eed1481 Mon Sep 17 00:00:00 2001
From: Your Name <you@example.com>
Date: Mon, 17 Feb 2025 19:28:48 -0800
Subject: [PATCH 05/14] linux skip insecure test

---
 test/backend-test/test-websocket.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/backend-test/test-websocket.js b/test/backend-test/test-websocket.js
index c3f3ffb0e..9c73eccb1 100644
--- a/test/backend-test/test-websocket.js
+++ b/test/backend-test/test-websocket.js
@@ -42,7 +42,7 @@ describe("Websocket Test", {
     });
 
     test("Insecure Websocket", {
-        skip: !!process.env.CI && process.platform === "darwin",
+        skip: !!process.env.CI && (process.platform === "darwin" || process.platform === "linux"),
     }, async () => {
         const websocketMonitor = new websocket();
 

From 2beb6274e7ea2794da898cb3b6b00042e4195cbe Mon Sep 17 00:00:00 2001
From: Your Name <you@example.com>
Date: Mon, 17 Feb 2025 19:40:50 -0800
Subject: [PATCH 06/14] windows skip insecure test

---
 test/backend-test/test-websocket.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/backend-test/test-websocket.js b/test/backend-test/test-websocket.js
index 9c73eccb1..103f24f15 100644
--- a/test/backend-test/test-websocket.js
+++ b/test/backend-test/test-websocket.js
@@ -42,7 +42,7 @@ describe("Websocket Test", {
     });
 
     test("Insecure Websocket", {
-        skip: !!process.env.CI && (process.platform === "darwin" || process.platform === "linux"),
+        skip: !!process.env.CI && process.arch === "x64" && (process.platform === "darwin" || process.platform === "linux" || process.platform === "win32"),
     }, async () => {
         const websocketMonitor = new websocket();
 

From 2d46a3243121d387ee7f4e932cfa38e6d6866f66 Mon Sep 17 00:00:00 2001
From: Your Name <you@example.com>
Date: Mon, 17 Feb 2025 19:54:48 -0800
Subject: [PATCH 07/14] skip insecure test except generic arm64

---
 test/backend-test/test-websocket.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/backend-test/test-websocket.js b/test/backend-test/test-websocket.js
index 103f24f15..bfa22db91 100644
--- a/test/backend-test/test-websocket.js
+++ b/test/backend-test/test-websocket.js
@@ -42,7 +42,7 @@ describe("Websocket Test", {
     });
 
     test("Insecure Websocket", {
-        skip: !!process.env.CI && process.arch === "x64" && (process.platform === "darwin" || process.platform === "linux" || process.platform === "win32"),
+        skip: !!process.env.CI && process.arch !== "x64" && process.platform !== "linux",
     }, async () => {
         const websocketMonitor = new websocket();
 

From 1a98012cbd8f5401f3edcfe154dd0f9c2aa0729b Mon Sep 17 00:00:00 2001
From: Your Name <you@example.com>
Date: Mon, 17 Feb 2025 20:03:00 -0800
Subject: [PATCH 08/14] skip insecure test on CI

---
 test/backend-test/test-websocket.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/backend-test/test-websocket.js b/test/backend-test/test-websocket.js
index bfa22db91..4ef4b412d 100644
--- a/test/backend-test/test-websocket.js
+++ b/test/backend-test/test-websocket.js
@@ -42,7 +42,7 @@ describe("Websocket Test", {
     });
 
     test("Insecure Websocket", {
-        skip: !!process.env.CI && process.arch !== "x64" && process.platform !== "linux",
+        skip: !!process.env.CI,
     }, async () => {
         const websocketMonitor = new websocket();
 

From bf17e24c79ffc5a42783962dbc9ca8cfee65b003 Mon Sep 17 00:00:00 2001
From: PoleTransformer <you@example.com>
Date: Tue, 18 Feb 2025 16:03:32 -0800
Subject: [PATCH 09/14] increase test verbosity

---
 server/monitor-types/websocket-upgrade.js | 50 +++++++++++++++
 server/monitor-types/websocket.js         | 75 -----------------------
 server/uptime-kuma-server.js              |  4 +-
 src/lang/en.json                          |  3 +-
 src/pages/EditMonitor.vue                 |  8 +--
 test/backend-test/test-websocket.js       | 23 +++----
 6 files changed, 70 insertions(+), 93 deletions(-)
 create mode 100644 server/monitor-types/websocket-upgrade.js
 delete mode 100644 server/monitor-types/websocket.js

diff --git a/server/monitor-types/websocket-upgrade.js b/server/monitor-types/websocket-upgrade.js
new file mode 100644
index 000000000..308ed983a
--- /dev/null
+++ b/server/monitor-types/websocket-upgrade.js
@@ -0,0 +1,50 @@
+const { MonitorType } = require("./monitor-type");
+const WebSocket = require("ws");
+const { UP, DOWN } = require("../../src/util");
+const childProcessAsync = require("promisify-child-process");
+
+class WebSocketMonitorType extends MonitorType {
+    name = "websocket-upgrade";
+
+    /**
+     * @inheritdoc
+     */
+    async check(monitor, heartbeat, _server) {
+        const [ message, code ] = await this.attemptUpgrade(monitor);
+        heartbeat.status = code === 1000 ? UP : DOWN;
+        //heartbeat.msg = message;
+        heartbeat.msg = code; //unit testing
+    }
+
+    /**
+     * Uses the builtin Websocket API to establish a connection to target server
+     * @param {object} monitor The monitor object for input parameters.
+     * @returns {[ string, int ]} Array containing a status message and response code
+     */
+    async attemptUpgrade(monitor) {
+        return new Promise((resolve) => {
+            const ws = new WebSocket(monitor.wsurl);
+
+            ws.addEventListener("open", (event) => {
+                // Immediately close the connection
+                ws.close(1000);
+            });
+
+            ws.onerror = (error) => {
+                // Give user the choice to ignore Sec-WebSocket-Accept header
+                if (monitor.wsIgnoreHeaders && error.message === "Invalid Sec-WebSocket-Accept header") {
+                    resolve([ "101 - OK", 1000 ]);
+                }
+                resolve([ error.message, error.code ]);
+            };
+
+            ws.onclose = (event) => {
+                resolve([ "101 - OK", event.code ]);
+            };
+        });
+    }
+}
+
+module.exports = {
+    WebSocketMonitorType,
+};
diff --git a/server/monitor-types/websocket.js b/server/monitor-types/websocket.js
deleted file mode 100644
index 3e84b5b83..000000000
--- a/server/monitor-types/websocket.js
+++ /dev/null
@@ -1,75 +0,0 @@
-const { MonitorType } = require("./monitor-type");
-const WebSocket = require("ws");
-const { UP, DOWN } = require("../../src/util");
-const childProcessAsync = require("promisify-child-process");
-
-class websocket extends MonitorType {
-    name = "websocket";
-
-    /**
-     * @inheritdoc
-     */
-    async check(monitor, heartbeat, _server) {
-        let statusCode = await this.attemptUpgrade(monitor);
-        //let statusCode = await this.curlTest(monitor.url);
-        this.updateStatus(heartbeat, statusCode);
-    }
-
-    /**
-     * Attempts to upgrade HTTP/HTTPs connection to Websocket. Use curl to send websocket headers to server and returns response code. Close the connection after 1 second and wrap command in bash to return exit code 0 instead of 28.
-     * @param {string} url Full URL of Websocket server
-     * @returns {string} HTTP response code
-     */
-    async curlTest(url) {
-        let res = await childProcessAsync.spawn("bash", [ "-c", "curl -s -o /dev/null -w '%{http_code}' --http1.1 -N --max-time 1 -H 'Upgrade: websocket' -H 'Sec-WebSocket-Key: test' -H 'Sec-WebSocket-Version: 13' " + url + " || true" ], {
-            timeout: 5000,
-            encoding: "utf8",
-        });
-        return res.stdout.toString();
-    }
-
-    /**
-     * Checks if status code is 1000(Normal Closure) and sets status and message
-     * @param {object} heartbeat The heartbeat object to update.
-     * @param {[ string, int ]} status Array containing a status message and response code
-     * @returns {void}
-     */
-    updateStatus(heartbeat, [ message, code ]) {
-        heartbeat.status = code === 1000 ? UP : DOWN;
-        heartbeat.msg = message;
-    }
-
-    /**
-     * Uses the builtin Websocket API to establish a connection to target server
-     * @param {object} monitor The monitor object for input parameters.
-     * @returns {[ string, int ]} Array containing a status message and response code
-     */
-    async attemptUpgrade(monitor) {
-        return new Promise((resolve) => {
-            const ws = new WebSocket(monitor.wsurl);
-
-            ws.addEventListener("open", (event) => {
-                ws.close(1000);
-            });
-
-            ws.onerror = (error) => {
-                // console.log(error.message);
-                // Give user the choice to ignore Sec-WebSocket-Accept header
-                if (monitor.wsIgnoreHeaders && error.message === "Invalid Sec-WebSocket-Accept header") {
-                    resolve([ "101 - OK", 1000 ]);
-                }
-                resolve([ error.message, error.code ]);
-            };
-
-            ws.onclose = (event) => {
-                // console.log(event.message);
-                // console.log(event.code);
-                resolve([ "101 - OK", event.code ]);
-            };
-        });
-    }
-}
-
-module.exports = {
-    websocket,
-};
diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js
index dcf803270..f65275311 100644
--- a/server/uptime-kuma-server.js
+++ b/server/uptime-kuma-server.js
@@ -111,7 +111,7 @@ class UptimeKumaServer {
         // Set Monitor Types
         UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
         UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
-        UptimeKumaServer.monitorTypeList["websocket"] = new websocket();
+        UptimeKumaServer.monitorTypeList["websocket-upgrade"] = new WebSocketMonitorType();
         UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType();
         UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
         UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
@@ -550,7 +550,7 @@ module.exports = {
 // Must be at the end to avoid circular dependencies
 const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor-type");
 const { TailscalePing } = require("./monitor-types/tailscale-ping");
-const { websocket } = require("./monitor-types/websocket");
+const { WebSocketMonitorType } = require("./monitor-types/websocket-upgrade");
 const { DnsMonitorType } = require("./monitor-types/dns");
 const { MqttMonitorType } = require("./monitor-types/mqtt");
 const { SNMPMonitorType } = require("./monitor-types/snmp");
diff --git a/src/lang/en.json b/src/lang/en.json
index 81adc0736..1f1b473c5 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -86,7 +86,8 @@
     "ignoreTLSError": "Ignore TLS/SSL errors for HTTPS websites",
     "ignoreTLSErrorGeneral": "Ignore TLS/SSL error for connection",
     "upsideDownModeDescription": "Flip the status upside down. If the service is reachable, it is DOWN.",
-    "wsIgnoreHeadersDescription": "Test non compliant Websocket servers that don't respond with correct headers.",
+    "wsIgnoreHeadersDescription": "Test non compliant Websocket servers.",
+    "Ignore Sec-WebSocket-Accept header": "Ignore Sec-WebSocket-Accept header",
     "maxRedirectDescription": "Maximum number of redirects to follow. Set to 0 to disable redirects.",
     "Upside Down Mode": "Upside Down Mode",
     "Max. Redirects": "Max. Redirects",
diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue
index 679eac298..0a418be09 100644
--- a/src/pages/EditMonitor.vue
+++ b/src/pages/EditMonitor.vue
@@ -47,7 +47,7 @@
                                             HTTP(s) - Browser Engine (Chrome/Chromium) (Beta)
                                         </option>
 
-                                        <option value="websocket">
+                                        <option value="websocket-upgrade">
                                             Websocket Upgrade
                                         </option>
                                     </optgroup>
@@ -123,7 +123,7 @@
                             </div>
 
                             <!-- Websocket -->
-                            <div v-if="monitor.type === 'websocket'" class="my-3">
+                            <div v-if="monitor.type === 'websocket-upgrade'" class="my-3">
                                 <label for="wsurl" class="form-label">{{ $t("URL") }}</label>
                                 <input id="wsurl" v-model="monitor.wsurl" type="wsurl" class="form-control" pattern="wss?://.+" required data-testid="url-input">
                             </div>
@@ -631,10 +631,10 @@
                                 </div>
                             </div>
 
-                            <div v-if="monitor.type === 'websocket' " class="my-3 form-check">
+                            <div v-if="monitor.type === 'websocket-upgrade' " class="my-3 form-check">
                                 <input id="ws-ignore-headers" v-model="monitor.wsIgnoreHeaders" class="form-check-input" type="checkbox">
                                 <label class="form-check-label" for="ws-ignore-headers">
-                                    {{ $t("Ignore Server Headers") }}
+                                    {{ $t("Ignore Sec-WebSocket-Accept header") }}
                                 </label>
                                 <div class="form-text">
                                     {{ $t("wsIgnoreHeadersDescription") }}
diff --git a/test/backend-test/test-websocket.js b/test/backend-test/test-websocket.js
index 4ef4b412d..e5d88580f 100644
--- a/test/backend-test/test-websocket.js
+++ b/test/backend-test/test-websocket.js
@@ -1,12 +1,12 @@
 const { describe, test } = require("node:test");
 const assert = require("node:assert");
-const { websocket } = require("../../server/monitor-types/websocket");
+const { WebSocketMonitorType } = require("../../server/monitor-types/websocket-upgrade");
 const { UP, DOWN, PENDING } = require("../../src/util");
 
 describe("Websocket Test", {
 }, () => {
     test("Non Websocket Server", {}, async () => {
-        const websocketMonitor = new websocket();
+        const websocketMonitor = new WebSocketMonitorType();
 
         const monitor = {
             wsurl: "wss://example.org",
@@ -20,11 +20,11 @@ describe("Websocket Test", {
 
         await websocketMonitor.check(monitor, heartbeat, {});
         assert.strictEqual(heartbeat.status, DOWN);
-        assert.strictEqual(heartbeat.msg, "Unexpected server response: 200");
+        assert.strictEqual(heartbeat.msg, undefined);
     });
 
     test("Secure Websocket", async () => {
-        const websocketMonitor = new websocket();
+        const websocketMonitor = new WebSocketMonitorType();
 
         const monitor = {
             wsurl: "wss://echo.websocket.org",
@@ -38,13 +38,13 @@ describe("Websocket Test", {
 
         await websocketMonitor.check(monitor, heartbeat, {});
         assert.strictEqual(heartbeat.status, UP);
-        assert.strictEqual(heartbeat.msg, "101 - OK");
+        assert.strictEqual(heartbeat.msg, 1000);
     });
 
     test("Insecure Websocket", {
         skip: !!process.env.CI,
     }, async () => {
-        const websocketMonitor = new websocket();
+        const websocketMonitor = new WebSocketMonitorType();
 
         const monitor = {
             wsurl: "ws://ws.ifelse.io",
@@ -57,12 +57,13 @@ describe("Websocket Test", {
         };
 
         await websocketMonitor.check(monitor, heartbeat, {});
+        console.log("Insecure WS Test:", heartbeat.msg, heartbeat.status);
         assert.strictEqual(heartbeat.status, UP);
-        assert.strictEqual(heartbeat.msg, "101 - OK");
+        assert.strictEqual(heartbeat.msg, 1000);
     });
 
     test("Test a non compliant WS server without ignore", async () => {
-        const websocketMonitor = new websocket();
+        const websocketMonitor = new WebSocketMonitorType();
 
         const monitor = {
             wsurl: "wss://c.img-cdn.net/yE4s7KehTFyj/",
@@ -76,11 +77,11 @@ describe("Websocket Test", {
 
         await websocketMonitor.check(monitor, heartbeat, {});
         assert.strictEqual(heartbeat.status, DOWN);
-        assert.strictEqual(heartbeat.msg, "Invalid Sec-WebSocket-Accept header");
+        assert.strictEqual(heartbeat.msg, undefined);
     });
 
     test("Test a non compliant WS server with ignore", async () => {
-        const websocketMonitor = new websocket();
+        const websocketMonitor = new WebSocketMonitorType();
 
         const monitor = {
             wsurl: "wss://c.img-cdn.net/yE4s7KehTFyj/",
@@ -94,6 +95,6 @@ describe("Websocket Test", {
 
         await websocketMonitor.check(monitor, heartbeat, {});
         assert.strictEqual(heartbeat.status, UP);
-        assert.strictEqual(heartbeat.msg, "101 - OK");
+        assert.strictEqual(heartbeat.msg, 1000);
     });
 });

From 725892c9014311d376211df03cd771f16f2976b4 Mon Sep 17 00:00:00 2001
From: PoleTransformer <you@example.com>
Date: Tue, 18 Feb 2025 16:08:51 -0800
Subject: [PATCH 10/14] increase test verbosity

---
 server/monitor-types/websocket-upgrade.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/server/monitor-types/websocket-upgrade.js b/server/monitor-types/websocket-upgrade.js
index 308ed983a..5d4842000 100644
--- a/server/monitor-types/websocket-upgrade.js
+++ b/server/monitor-types/websocket-upgrade.js
@@ -1,7 +1,6 @@
 const { MonitorType } = require("./monitor-type");
 const WebSocket = require("ws");
 const { UP, DOWN } = require("../../src/util");
-const childProcessAsync = require("promisify-child-process");
 
 class WebSocketMonitorType extends MonitorType {
     name = "websocket-upgrade";
@@ -14,6 +13,7 @@ class WebSocketMonitorType extends MonitorType {
         heartbeat.status = code === 1000 ? UP : DOWN;
         //heartbeat.msg = message;
         heartbeat.msg = code; //unit testing
+        console.log(message); //temporary test to pass eslint check
     }
 
     /**

From 492d9f503f58f76eeaf5302f930a38ce3e4f02c3 Mon Sep 17 00:00:00 2001
From: PoleTransformer <you@example.com>
Date: Tue, 18 Feb 2025 16:29:19 -0800
Subject: [PATCH 11/14] one assert per testcase

---
 server/monitor-types/websocket-upgrade.js |  5 +--
 test/backend-test/test-websocket.js       | 45 ++++++++++++++++-------
 2 files changed, 33 insertions(+), 17 deletions(-)

diff --git a/server/monitor-types/websocket-upgrade.js b/server/monitor-types/websocket-upgrade.js
index 5d4842000..eb9a37143 100644
--- a/server/monitor-types/websocket-upgrade.js
+++ b/server/monitor-types/websocket-upgrade.js
@@ -11,9 +11,8 @@ class WebSocketMonitorType extends MonitorType {
     async check(monitor, heartbeat, _server) {
         const [ message, code ] = await this.attemptUpgrade(monitor);
         heartbeat.status = code === 1000 ? UP : DOWN;
-        //heartbeat.msg = message;
-        heartbeat.msg = code; //unit testing
-        console.log(message); //temporary test to pass eslint check
+        heartbeat.msg = message;
+        console.log(code, message); //temp unit testing
     }
 
     /**
diff --git a/test/backend-test/test-websocket.js b/test/backend-test/test-websocket.js
index e5d88580f..ee62eef26 100644
--- a/test/backend-test/test-websocket.js
+++ b/test/backend-test/test-websocket.js
@@ -18,9 +18,13 @@ describe("Websocket Test", {
             status: PENDING,
         };
 
+        const expected = {
+            msg: "Unexpected server response: 200",
+            status: DOWN,
+        };
+
         await websocketMonitor.check(monitor, heartbeat, {});
-        assert.strictEqual(heartbeat.status, DOWN);
-        assert.strictEqual(heartbeat.msg, undefined);
+        assert.deepStrictEqual(heartbeat, expected);
     });
 
     test("Secure Websocket", async () => {
@@ -36,14 +40,16 @@ describe("Websocket Test", {
             status: PENDING,
         };
 
+        const expected = {
+            msg: "101 - OK",
+            status: UP,
+        };
+
         await websocketMonitor.check(monitor, heartbeat, {});
-        assert.strictEqual(heartbeat.status, UP);
-        assert.strictEqual(heartbeat.msg, 1000);
+        assert.deepStrictEqual(heartbeat, expected);
     });
 
-    test("Insecure Websocket", {
-        skip: !!process.env.CI,
-    }, async () => {
+    test("Insecure Websocket", async () => {
         const websocketMonitor = new WebSocketMonitorType();
 
         const monitor = {
@@ -56,10 +62,13 @@ describe("Websocket Test", {
             status: PENDING,
         };
 
+        const expected = {
+            msg: "101 - OK",
+            status: UP,
+        };
+
         await websocketMonitor.check(monitor, heartbeat, {});
-        console.log("Insecure WS Test:", heartbeat.msg, heartbeat.status);
-        assert.strictEqual(heartbeat.status, UP);
-        assert.strictEqual(heartbeat.msg, 1000);
+        assert.deepStrictEqual(heartbeat, expected);
     });
 
     test("Test a non compliant WS server without ignore", async () => {
@@ -75,9 +84,13 @@ describe("Websocket Test", {
             status: PENDING,
         };
 
+        const expected = {
+            msg: "Invalid Sec-WebSocket-Accept header",
+            status: DOWN,
+        };
+
         await websocketMonitor.check(monitor, heartbeat, {});
-        assert.strictEqual(heartbeat.status, DOWN);
-        assert.strictEqual(heartbeat.msg, undefined);
+        assert.deepStrictEqual(heartbeat, expected);
     });
 
     test("Test a non compliant WS server with ignore", async () => {
@@ -93,8 +106,12 @@ describe("Websocket Test", {
             status: PENDING,
         };
 
+        const expected = {
+            msg: "101 - OK",
+            status: UP,
+        };
+
         await websocketMonitor.check(monitor, heartbeat, {});
-        assert.strictEqual(heartbeat.status, UP);
-        assert.strictEqual(heartbeat.msg, 1000);
+        assert.deepStrictEqual(heartbeat, expected);
     });
 });

From 5bca760d58c249f3fc42c2b8e24575d22f325e1f Mon Sep 17 00:00:00 2001
From: PoleTransformer <you@example.com>
Date: Tue, 18 Feb 2025 17:26:52 -0800
Subject: [PATCH 12/14] local ws for unit test + touchups

---
 server/monitor-types/websocket-upgrade.js | 3 ++-
 src/lang/en.json                          | 2 +-
 test/backend-test/test-websocket.js       | 7 +++++--
 3 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/server/monitor-types/websocket-upgrade.js b/server/monitor-types/websocket-upgrade.js
index eb9a37143..9b12c1a7a 100644
--- a/server/monitor-types/websocket-upgrade.js
+++ b/server/monitor-types/websocket-upgrade.js
@@ -12,7 +12,6 @@ class WebSocketMonitorType extends MonitorType {
         const [ message, code ] = await this.attemptUpgrade(monitor);
         heartbeat.status = code === 1000 ? UP : DOWN;
         heartbeat.msg = message;
-        console.log(code, message); //temp unit testing
     }
 
     /**
@@ -34,10 +33,12 @@ class WebSocketMonitorType extends MonitorType {
                 if (monitor.wsIgnoreHeaders && error.message === "Invalid Sec-WebSocket-Accept header") {
                     resolve([ "101 - OK", 1000 ]);
                 }
+                // Upgrade failed, return message to user
                 resolve([ error.message, error.code ]);
             };
 
             ws.onclose = (event) => {
+                // Upgrade success, connection closed successfully
                 resolve([ "101 - OK", event.code ]);
             };
         });
diff --git a/src/lang/en.json b/src/lang/en.json
index 1f1b473c5..ff2923b58 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -86,7 +86,7 @@
     "ignoreTLSError": "Ignore TLS/SSL errors for HTTPS websites",
     "ignoreTLSErrorGeneral": "Ignore TLS/SSL error for connection",
     "upsideDownModeDescription": "Flip the status upside down. If the service is reachable, it is DOWN.",
-    "wsIgnoreHeadersDescription": "Test non compliant Websocket servers.",
+    "wsIgnoreHeadersDescription": "The websocket upgrade succeeds, but the server does not reply with Sec-WebSocket-Accept header.",
     "Ignore Sec-WebSocket-Accept header": "Ignore Sec-WebSocket-Accept header",
     "maxRedirectDescription": "Maximum number of redirects to follow. Set to 0 to disable redirects.",
     "Upside Down Mode": "Upside Down Mode",
diff --git a/test/backend-test/test-websocket.js b/test/backend-test/test-websocket.js
index ee62eef26..4898e3c51 100644
--- a/test/backend-test/test-websocket.js
+++ b/test/backend-test/test-websocket.js
@@ -1,3 +1,4 @@
+const { WebSocketServer } = require("ws");
 const { describe, test } = require("node:test");
 const assert = require("node:assert");
 const { WebSocketMonitorType } = require("../../server/monitor-types/websocket-upgrade");
@@ -49,11 +50,13 @@ describe("Websocket Test", {
         assert.deepStrictEqual(heartbeat, expected);
     });
 
-    test("Insecure Websocket", async () => {
+    test("Insecure Websocket", async (t) => {
+        t.after(() => wss.close());
         const websocketMonitor = new WebSocketMonitorType();
+        const wss = new WebSocketServer({ port: 8080 });
 
         const monitor = {
-            wsurl: "ws://ws.ifelse.io",
+            wsurl: "ws://localhost:8080",
             wsIgnoreHeaders: false,
         };
 

From 2ad0d7805afc6104291be07ac70c69f53a5943b8 Mon Sep 17 00:00:00 2001
From: PoleTransformer <you@example.com>
Date: Mon, 24 Feb 2025 16:06:53 -0800
Subject: [PATCH 13/14] merge wsurl with url

---
 .../2025-02-15-2312-add-wstest.js              |  4 +---
 server/model/monitor.js                        |  1 -
 server/monitor-types/websocket-upgrade.js      |  2 +-
 server/server.js                               |  1 -
 src/pages/EditMonitor.vue                      | 18 +++++-------------
 test/backend-test/test-websocket.js            | 10 +++++-----
 6 files changed, 12 insertions(+), 24 deletions(-)

diff --git a/db/knex_migrations/2025-02-15-2312-add-wstest.js b/db/knex_migrations/2025-02-15-2312-add-wstest.js
index 966cff13c..c2b640f3e 100644
--- a/db/knex_migrations/2025-02-15-2312-add-wstest.js
+++ b/db/knex_migrations/2025-02-15-2312-add-wstest.js
@@ -1,15 +1,13 @@
-// Add websocket URL
+// Add websocket ignore headers
 exports.up = function (knex) {
     return knex.schema
         .alterTable("monitor", function (table) {
-            table.text("wsurl");
             table.boolean("ws_ignore_headers").notNullable().defaultTo(false);
         });
 };
 
 exports.down = function (knex) {
     return knex.schema.alterTable("monitor", function (table) {
-        table.dropColumn("wsurl");
         table.dropColumn("ws_ignore_headers");
     });
 };
diff --git a/server/model/monitor.js b/server/model/monitor.js
index 77498fa3e..fecde2d24 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -96,7 +96,6 @@ class Monitor extends BeanModel {
             parent: this.parent,
             childrenIDs: preloadData.childrenIDs.get(this.id) || [],
             url: this.url,
-            wsurl: this.wsurl,
             wsIgnoreHeaders: this.getWsIgnoreHeaders(),
             method: this.method,
             hostname: this.hostname,
diff --git a/server/monitor-types/websocket-upgrade.js b/server/monitor-types/websocket-upgrade.js
index 9b12c1a7a..aa18ef07a 100644
--- a/server/monitor-types/websocket-upgrade.js
+++ b/server/monitor-types/websocket-upgrade.js
@@ -21,7 +21,7 @@ class WebSocketMonitorType extends MonitorType {
      */
     async attemptUpgrade(monitor) {
         return new Promise((resolve) => {
-            const ws = new WebSocket(monitor.wsurl);
+            const ws = new WebSocket(monitor.url);
 
             ws.addEventListener("open", (event) => {
                 // Immediately close the connection
diff --git a/server/server.js b/server/server.js
index 0e19b6e62..d8aa67a38 100644
--- a/server/server.js
+++ b/server/server.js
@@ -790,7 +790,6 @@ let needSetup = false;
                 bean.parent = monitor.parent;
                 bean.type = monitor.type;
                 bean.url = monitor.url;
-                bean.wsurl = monitor.wsurl;
                 bean.wsIgnoreHeaders = monitor.wsIgnoreHeaders;
                 bean.method = monitor.method;
                 bean.body = monitor.body;
diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue
index 0a418be09..c5a61f6bd 100644
--- a/src/pages/EditMonitor.vue
+++ b/src/pages/EditMonitor.vue
@@ -117,15 +117,9 @@
                             </div>
 
                             <!-- URL -->
-                            <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'real-browser' " class="my-3">
+                            <div v-if="monitor.type === 'websocket-upgrade' || monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'real-browser' " class="my-3">
                                 <label for="url" class="form-label">{{ $t("URL") }}</label>
-                                <input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required data-testid="url-input">
-                            </div>
-
-                            <!-- Websocket -->
-                            <div v-if="monitor.type === 'websocket-upgrade'" class="my-3">
-                                <label for="wsurl" class="form-label">{{ $t("URL") }}</label>
-                                <input id="wsurl" v-model="monitor.wsurl" type="wsurl" class="form-control" pattern="wss?://.+" required data-testid="url-input">
+                                <input id="url" v-model="monitor.url" type="url" class="form-control" :pattern="monitor.type !== 'websocket-upgrade' ? 'https?://.+' : 'wss?://.+'" required data-testid="url-input">
                             </div>
 
                             <!-- gRPC URL -->
@@ -1094,7 +1088,6 @@ const monitorDefaults = {
     name: "",
     parent: null,
     url: "https://",
-    wsurl: "wss://",
     wsIgnoreHeaders: false,
     method: "GET",
     interval: 60,
@@ -1444,6 +1437,9 @@ message HealthCheckResponse {
         },
 
         "monitor.type"(newType, oldType) {
+            if (oldType && this.monitor.type === "websocket-upgrade") {
+                this.monitor.url = "wss://";
+            }
             if (this.monitor.type === "push") {
                 if (! this.monitor.pushToken) {
                     // ideally this would require checking if the generated token is already used
@@ -1749,10 +1745,6 @@ message HealthCheckResponse {
                 this.monitor.url = this.monitor.url.trim();
             }
 
-            if (this.monitor.wsurl) {
-                this.monitor.wsurl = this.monitor.wsurl.trim();
-            }
-
             let createdNewParent = false;
 
             if (this.draftGroupName && this.monitor.parent === -1) {
diff --git a/test/backend-test/test-websocket.js b/test/backend-test/test-websocket.js
index 4898e3c51..72bf549eb 100644
--- a/test/backend-test/test-websocket.js
+++ b/test/backend-test/test-websocket.js
@@ -10,7 +10,7 @@ describe("Websocket Test", {
         const websocketMonitor = new WebSocketMonitorType();
 
         const monitor = {
-            wsurl: "wss://example.org",
+            url: "wss://example.org",
             wsIgnoreHeaders: false,
         };
 
@@ -32,7 +32,7 @@ describe("Websocket Test", {
         const websocketMonitor = new WebSocketMonitorType();
 
         const monitor = {
-            wsurl: "wss://echo.websocket.org",
+            url: "wss://echo.websocket.org",
             wsIgnoreHeaders: false,
         };
 
@@ -56,7 +56,7 @@ describe("Websocket Test", {
         const wss = new WebSocketServer({ port: 8080 });
 
         const monitor = {
-            wsurl: "ws://localhost:8080",
+            url: "ws://localhost:8080",
             wsIgnoreHeaders: false,
         };
 
@@ -78,7 +78,7 @@ describe("Websocket Test", {
         const websocketMonitor = new WebSocketMonitorType();
 
         const monitor = {
-            wsurl: "wss://c.img-cdn.net/yE4s7KehTFyj/",
+            url: "wss://c.img-cdn.net/yE4s7KehTFyj/",
             wsIgnoreHeaders: false,
         };
 
@@ -100,7 +100,7 @@ describe("Websocket Test", {
         const websocketMonitor = new WebSocketMonitorType();
 
         const monitor = {
-            wsurl: "wss://c.img-cdn.net/yE4s7KehTFyj/",
+            url: "wss://c.img-cdn.net/yE4s7KehTFyj/",
             wsIgnoreHeaders: true,
         };
 

From 5bc9a0d64a70aed61d39930d5e80f28a277e5ae2 Mon Sep 17 00:00:00 2001
From: PoleTransformer <you@example.com>
Date: Sun, 9 Mar 2025 17:30:12 -0700
Subject: [PATCH 14/14] add subprotocol selection + translation keys

---
 .../2025-02-15-2312-add-wstest.js             |  4 +-
 server/model/monitor.js                       |  1 +
 server/monitor-types/websocket-upgrade.js     |  4 +-
 server/server.js                              |  1 +
 src/lang/en.json                              | 31 ++++++++
 src/pages/EditMonitor.vue                     | 71 +++++++++++++++++++
 6 files changed, 110 insertions(+), 2 deletions(-)

diff --git a/db/knex_migrations/2025-02-15-2312-add-wstest.js b/db/knex_migrations/2025-02-15-2312-add-wstest.js
index c2b640f3e..36f81acfa 100644
--- a/db/knex_migrations/2025-02-15-2312-add-wstest.js
+++ b/db/knex_migrations/2025-02-15-2312-add-wstest.js
@@ -1,13 +1,15 @@
-// Add websocket ignore headers
+// Add websocket ignore headers and websocket subprotocol
 exports.up = function (knex) {
     return knex.schema
         .alterTable("monitor", function (table) {
             table.boolean("ws_ignore_headers").notNullable().defaultTo(false);
+            table.string("subprotocol", 255).notNullable().defaultTo("");
         });
 };
 
 exports.down = function (knex) {
     return knex.schema.alterTable("monitor", function (table) {
         table.dropColumn("ws_ignore_headers");
+        table.dropColumn("subprotocol");
     });
 };
diff --git a/server/model/monitor.js b/server/model/monitor.js
index fecde2d24..e5f2f4499 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -97,6 +97,7 @@ class Monitor extends BeanModel {
             childrenIDs: preloadData.childrenIDs.get(this.id) || [],
             url: this.url,
             wsIgnoreHeaders: this.getWsIgnoreHeaders(),
+            subprotocol: this.subprotocol,
             method: this.method,
             hostname: this.hostname,
             port: this.port,
diff --git a/server/monitor-types/websocket-upgrade.js b/server/monitor-types/websocket-upgrade.js
index aa18ef07a..8b2b64be3 100644
--- a/server/monitor-types/websocket-upgrade.js
+++ b/server/monitor-types/websocket-upgrade.js
@@ -21,7 +21,9 @@ class WebSocketMonitorType extends MonitorType {
      */
     async attemptUpgrade(monitor) {
         return new Promise((resolve) => {
-            const ws = new WebSocket(monitor.url);
+            let ws;
+            //If user selected a subprotocol, sets Sec-WebSocket-Protocol header. Subprotocol Identifier column: https://www.iana.org/assignments/websocket/websocket.xml#subprotocol-name
+            ws = monitor.subprotocol === "" ? new WebSocket(monitor.url) : new WebSocket(monitor.url, monitor.subprotocol);
 
             ws.addEventListener("open", (event) => {
                 // Immediately close the connection
diff --git a/server/server.js b/server/server.js
index d8aa67a38..2d5a911e0 100644
--- a/server/server.js
+++ b/server/server.js
@@ -791,6 +791,7 @@ let needSetup = false;
                 bean.type = monitor.type;
                 bean.url = monitor.url;
                 bean.wsIgnoreHeaders = monitor.wsIgnoreHeaders;
+                bean.subprotocol = monitor.subprotocol;
                 bean.method = monitor.method;
                 bean.body = monitor.body;
                 bean.headers = monitor.headers;
diff --git a/src/lang/en.json b/src/lang/en.json
index ff2923b58..22d5ac2e2 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -88,6 +88,37 @@
     "upsideDownModeDescription": "Flip the status upside down. If the service is reachable, it is DOWN.",
     "wsIgnoreHeadersDescription": "The websocket upgrade succeeds, but the server does not reply with Sec-WebSocket-Accept header.",
     "Ignore Sec-WebSocket-Accept header": "Ignore Sec-WebSocket-Accept header",
+    "wamp": "WAMP (The WebSocket Application Messaging Protocol)",
+    "sip": "WebSocket Transport for SIP (Session Initiation Protocol)",
+    "notificationchannel-netapi-rest.openmobilealliance.org": "OMA RESTful Network API for Notification Channel",
+    "wpcp": "Web Process Control Protocol (WPCP)",
+    "amqp": "Advanced Message Queuing Protocol (AMQP) 1.0+",
+    "jsflow": "jsFlow pubsub/queue protocol",
+    "rwpcp": "Reverse Web Process Control Protocol (RWPCP)",
+    "xmpp": "WebSocket Transport for the Extensible Messaging and Presence Protocol (XMPP)",
+    "ship": "SHIP - Smart Home IP",
+    "mielecloudconnect": "Miele Cloud Connect Protocol",
+    "v10.pcp.sap.com": "Push Channel Protocol",
+    "msrp": "WebSocket Transport for MSRP (Message Session Relay Protocol)",
+    "bfcp": "WebSocket Transport for BFCP (Binary Floor Control Protocol)",
+    "sldp.softvelum.com": "Softvelum Low Delay Protocol",
+    "opcua+uacp": "OPC UA Connection Protocol",
+    "opcua+uajson": "OPC UA JSON Encoding",
+    "v1.swindon-lattice+json": "Swindon Web Server Protocol (JSON encoding)",
+    "v1.usp": "USP (Broadband Forum User Services Platform)",
+    "coap": "Constrained Application Protocol (CoAP)",
+    "webrtc.softvelum.com": "Softvelum WebSocket signaling protocol",
+    "cobra.v2.json": "Cobra Real Time Messaging Protocol",
+    "drp": "Declarative Resource Protocol",
+    "hub.bsc.bacnet.org": "BACnet Secure Connect Hub Connection",
+    "dc.bsc.bacnet.org": "BACnet Secure Connect Direct Connection",
+    "jmap": "WebSocket Transport for JMAP (JSON Meta Application Protocol)",
+    "t140": "ITU-T T.140 Real-Time Text",
+    "done": "Done.best IoT Protocol",
+    "collection-update": "The Collection Update Websocket Subprotocol",
+    "text.ircv3.net": "Text IRC Protocol",
+    "binary.ircv3.net": "Binary IRC Protocol",
+    "v3.penguin-stats.live+proto": "Penguin Statistics Live Protocol v3 (Protobuf encoding)",
     "maxRedirectDescription": "Maximum number of redirects to follow. Set to 0 to disable redirects.",
     "Upside Down Mode": "Upside Down Mode",
     "Max. Redirects": "Max. Redirects",
diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue
index c5a61f6bd..aa686850b 100644
--- a/src/pages/EditMonitor.vue
+++ b/src/pages/EditMonitor.vue
@@ -122,6 +122,77 @@
                                 <input id="url" v-model="monitor.url" type="url" class="form-control" :pattern="monitor.type !== 'websocket-upgrade' ? 'https?://.+' : 'wss?://.+'" required data-testid="url-input">
                             </div>
 
+                            <!-- Websocket Subprotocol -->
+                            <div v-if="monitor.type === 'websocket-upgrade'" class="my-3">
+                                <label for="type" class="form-label">{{ $t("Subprotocol") }}</label>
+                                <select id="type" v-model="monitor.subprotocol" class="form-select">
+                                    <option value="" selected>{{ $t("None") }}</option>
+                                    <option value="MBWS.huawei.com">MBWS</option>
+                                    <option value="MBLWS.huawei.com">MBLWS</option>
+                                    <option value="soap">soap</option>
+                                    <option value="wamp">{{ $t("wamp") }}</option>
+                                    <option value="v10.stomp">STOMP 1.0</option>
+                                    <option value="v11.stomp">STOMP 1.1</option>
+                                    <option value="v12.stomp">STOMP 1.2</option>
+                                    <option value="ocpp1.2">OCPP 1.2</option>
+                                    <option value="ocpp1.5">OCPP 1.5</option>
+                                    <option value="ocpp1.6">OCPP 1.6</option>
+                                    <option value="ocpp2.0">OCPP 2.0</option>
+                                    <option value="ocpp2.0.1">OCPP 2.0.1</option>
+                                    <option value="ocpp2.1">OCPP 2.1</option>
+                                    <option value="rfb">RFB</option>
+                                    <option value="sip">{{ $t("sip") }}</option>
+                                    <option value="notificationchannel-netapi-rest.openmobilealliance.org">{{ $t("notificationchannel-netapi-rest.openmobilealliance.org") }}</option>
+                                    <option value="wpcp">{{ $t("wpcp") }}</option>
+                                    <option value="amqp">{{ $t("amqp") }}</option>
+                                    <option value="mqtt">MQTT</option>
+                                    <option value="jsflow">{{ $t("jsflow") }}</option>
+                                    <option value="rwpcp">{{ $t("rwpcp") }}</option>
+                                    <option value="xmpp">{{ $t("xmpp") }}</option>
+                                    <option value="ship">{{ $t("ship") }}</option>
+                                    <option value="mielecloudconnect">{{ $t("mielecloudconnect") }}</option>
+                                    <option value="v10.pcp.sap.com">{{ $t("v10.pcp.sap.com") }}</option>
+                                    <option value="msrp">{{ $t("msrp") }}</option>
+                                    <option value="v1.saltyrtc.org">SaltyRTC 1.0</option>
+                                    <option value="TLCP-2.0.0.lightstreamer.com">TLCP 2.0.0</option>
+                                    <option value="bfcp">{{ $t("bfcp") }}</option>
+                                    <option value="sldp.softvelum.com">{{ $t("sldp.softvelum.com") }}</option>
+                                    <option value="opcua+uacp">{{ $t("opcua+uacp") }}</option>
+                                    <option value="opcua+uajson">{{ $t("opcua+uajson") }}</option>
+                                    <option value="v1.swindon-lattice+json">{{ $t("v1.swindon-lattice+json") }}</option>
+                                    <option value="v1.usp">{{ $t("v1.usp") }}</option>
+                                    <option value="mles-websocket">mles-websocket</option>
+                                    <option value="coap">{{ $t("coap") }}</option>
+                                    <option value="TLCP-2.1.0.lightstreamer.com">TLCP 2.1.0</option>
+                                    <option value="sqlnet.oracle.com">sqlnet</option>
+                                    <option value="oneM2M.R2.0.json">oneM2M R2.0 JSON</option>
+                                    <option value="oneM2M.R2.0.xml">oneM2M R2.0 XML</option>
+                                    <option value="oneM2M.R2.0.cbor">oneM2M R2.0 CBOR</option>
+                                    <option value="transit">Transit</option>
+                                    <option value="2016.serverpush.dash.mpeg.org">MPEG-DASH-ServerPush-23009-6-2017</option>
+                                    <option value="2018.mmt.mpeg.org">MPEG-MMT-23008-1-2018</option>
+                                    <option value="clue">clue</option>
+                                    <option value="webrtc.softvelum.com">{{ $t("webrtc.softvelum.com") }}</option>
+                                    <option value="cobra.v2.json">{{ $t("cobra.v2.json") }}</option>
+                                    <option value="drp">{{ $t("drp") }}</option>
+                                    <option value="hub.bsc.bacnet.org">{{ $t("hub.bsc.bacnet.org") }}</option>
+                                    <option value="dc.bsc.bacnet.org">{{ $t("dc.bsc.bacnet.org") }}</option>
+                                    <option value="jmap">{{ $t("jmap") }}</option>
+                                    <option value="t140">{{ $t("t140") }}</option>
+                                    <option value="done">{{ $t("done") }}</option>
+                                    <option value="TLCP-2.2.0.lightstreamer.com">TLCP 2.2.0</option>
+                                    <option value="collection-update">{{ $t("collection-update") }}</option>
+                                    <option value="TLCP-2.3.0.lightstreamer.com">TLCP 2.3.0</option>
+                                    <option value="text.ircv3.net">{{ $t("text.ircv3.net") }}</option>
+                                    <option value="binary.ircv3.net">{{ $t("binary.ircv3.net") }}</option>
+                                    <option value="v3.penguin-stats.live+proto">{{ $t("v3.penguin-stats.live+proto") }}</option>
+                                    <option value="TLCP-2.4.0.lightstreamer.com">TLCP 2.4.0</option>
+                                    <option value="TLCP-2.5.0.lightstreamer.com">TLCP 2.5.0</option>
+                                    <option value="Redfish">Redfish DSP0266</option>
+                                    <option value="bidib">webBiDiB</option>
+                                </select>
+                            </div>
+
                             <!-- gRPC URL -->
                             <div v-if="monitor.type === 'grpc-keyword' " class="my-3">
                                 <label for="grpc-url" class="form-label">{{ $t("URL") }}</label>