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>