This commit is contained in:
PoleTransformer 2025-03-26 16:31:48 +00:00 committed by GitHub
commit f1d1cf1e2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 326 additions and 3 deletions

View file

@ -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

View file

@ -0,0 +1,15 @@
// 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");
});
};

View file

@ -96,6 +96,8 @@ class Monitor extends BeanModel {
parent: this.parent,
childrenIDs: preloadData.childrenIDs.get(this.id) || [],
url: this.url,
wsIgnoreHeaders: this.getWsIgnoreHeaders(),
subprotocol: this.subprotocol,
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?

View file

@ -0,0 +1,52 @@
const { MonitorType } = require("./monitor-type");
const WebSocket = require("ws");
const { UP, DOWN } = require("../../src/util");
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;
}
/**
* 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) => {
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
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 ]);
}
// Upgrade failed, return message to user
resolve([ error.message, error.code ]);
};
ws.onclose = (event) => {
// Upgrade success, connection closed successfully
resolve([ "101 - OK", event.code ]);
};
});
}
}
module.exports = {
WebSocketMonitorType,
};

View file

@ -790,6 +790,8 @@ let needSetup = false;
bean.parent = monitor.parent;
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;

View file

@ -111,6 +111,7 @@ class UptimeKumaServer {
// Set Monitor Types
UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
UptimeKumaServer.monitorTypeList["websocket-upgrade"] = new WebSocketMonitorType();
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 { WebSocketMonitorType } = require("./monitor-types/websocket-upgrade");
const { DnsMonitorType } = require("./monitor-types/dns");
const { MqttMonitorType } = require("./monitor-types/mqtt");
const { SNMPMonitorType } = require("./monitor-types/snmp");

View file

@ -86,6 +86,39 @@
"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": "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",

View file

@ -46,6 +46,10 @@
<option value="real-browser">
HTTP(s) - Browser Engine (Chrome/Chromium) (Beta)
</option>
<option value="websocket-upgrade">
Websocket Upgrade
</option>
</optgroup>
<optgroup :label="$t('Passive Monitor Type')">
@ -113,9 +117,80 @@
</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">
<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 -->
@ -621,6 +696,16 @@
</div>
</div>
<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 Sec-WebSocket-Accept header") }}
</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">
@ -1074,6 +1159,7 @@ const monitorDefaults = {
name: "",
parent: null,
url: "https://",
wsIgnoreHeaders: false,
method: "GET",
interval: 60,
retryInterval: 60,
@ -1422,6 +1508,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

View file

@ -0,0 +1,120 @@
const { WebSocketServer } = require("ws");
const { describe, test } = require("node:test");
const assert = require("node:assert");
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 WebSocketMonitorType();
const monitor = {
url: "wss://example.org",
wsIgnoreHeaders: false,
};
const heartbeat = {
msg: "",
status: PENDING,
};
const expected = {
msg: "Unexpected server response: 200",
status: DOWN,
};
await websocketMonitor.check(monitor, heartbeat, {});
assert.deepStrictEqual(heartbeat, expected);
});
test("Secure Websocket", async () => {
const websocketMonitor = new WebSocketMonitorType();
const monitor = {
url: "wss://echo.websocket.org",
wsIgnoreHeaders: false,
};
const heartbeat = {
msg: "",
status: PENDING,
};
const expected = {
msg: "101 - OK",
status: UP,
};
await websocketMonitor.check(monitor, heartbeat, {});
assert.deepStrictEqual(heartbeat, expected);
});
test("Insecure Websocket", async (t) => {
t.after(() => wss.close());
const websocketMonitor = new WebSocketMonitorType();
const wss = new WebSocketServer({ port: 8080 });
const monitor = {
url: "ws://localhost:8080",
wsIgnoreHeaders: false,
};
const heartbeat = {
msg: "",
status: PENDING,
};
const expected = {
msg: "101 - OK",
status: UP,
};
await websocketMonitor.check(monitor, heartbeat, {});
assert.deepStrictEqual(heartbeat, expected);
});
test("Test a non compliant WS server without ignore", async () => {
const websocketMonitor = new WebSocketMonitorType();
const monitor = {
url: "wss://c.img-cdn.net/yE4s7KehTFyj/",
wsIgnoreHeaders: false,
};
const heartbeat = {
msg: "",
status: PENDING,
};
const expected = {
msg: "Invalid Sec-WebSocket-Accept header",
status: DOWN,
};
await websocketMonitor.check(monitor, heartbeat, {});
assert.deepStrictEqual(heartbeat, expected);
});
test("Test a non compliant WS server with ignore", async () => {
const websocketMonitor = new WebSocketMonitorType();
const monitor = {
url: "wss://c.img-cdn.net/yE4s7KehTFyj/",
wsIgnoreHeaders: true,
};
const heartbeat = {
msg: "",
status: PENDING,
};
const expected = {
msg: "101 - OK",
status: UP,
};
await websocketMonitor.check(monitor, heartbeat, {});
assert.deepStrictEqual(heartbeat, expected);
});
});