Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions db/knex_migrations/2025-02-15-2312-add-wstest.js
Original file line number Diff line number Diff line change
@@ -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");
});
};
10 changes: 10 additions & 0 deletions server/model/monitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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?
Expand Down
52 changes: 52 additions & 0 deletions server/monitor-types/websocket-upgrade.js
Original file line number Diff line number Diff line change
@@ -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,
};
2 changes: 2 additions & 0 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions server/uptime-kuma-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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");
Expand Down
33 changes: 33 additions & 0 deletions src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
93 changes: 91 additions & 2 deletions src/pages/EditMonitor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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')">
Expand Down Expand Up @@ -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 -->
Expand Down Expand Up @@ -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">
Expand Down Expand Up @@ -1074,6 +1159,7 @@ const monitorDefaults = {
name: "",
parent: null,
url: "https://",
wsIgnoreHeaders: false,
method: "GET",
interval: 60,
retryInterval: 60,
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading