mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-03-12 20:34:46 +00:00
Merge 2ad0d7805a
into b45dc6787d
This commit is contained in:
commit
3aa490f6c5
9 changed files with 218 additions and 3 deletions
|
@ -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
|
||||
|
|
13
db/knex_migrations/2025-02-15-2312-add-wstest.js
Normal file
13
db/knex_migrations/2025-02-15-2312-add-wstest.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
// Add websocket ignore headers
|
||||
exports.up = function (knex) {
|
||||
return knex.schema
|
||||
.alterTable("monitor", function (table) {
|
||||
table.boolean("ws_ignore_headers").notNullable().defaultTo(false);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.alterTable("monitor", function (table) {
|
||||
table.dropColumn("ws_ignore_headers");
|
||||
});
|
||||
};
|
|
@ -96,6 +96,7 @@ class Monitor extends BeanModel {
|
|||
parent: this.parent,
|
||||
childrenIDs: preloadData.childrenIDs.get(this.id) || [],
|
||||
url: this.url,
|
||||
wsIgnoreHeaders: this.getWsIgnoreHeaders(),
|
||||
method: this.method,
|
||||
hostname: this.hostname,
|
||||
port: this.port,
|
||||
|
@ -255,6 +256,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?
|
||||
|
|
50
server/monitor-types/websocket-upgrade.js
Normal file
50
server/monitor-types/websocket-upgrade.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
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) => {
|
||||
const ws = new WebSocket(monitor.url);
|
||||
|
||||
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,
|
||||
};
|
|
@ -790,6 +790,7 @@ let needSetup = false;
|
|||
bean.parent = monitor.parent;
|
||||
bean.type = monitor.type;
|
||||
bean.url = monitor.url;
|
||||
bean.wsIgnoreHeaders = monitor.wsIgnoreHeaders;
|
||||
bean.method = monitor.method;
|
||||
bean.body = monitor.body;
|
||||
bean.headers = monitor.headers;
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -86,6 +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": "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",
|
||||
"Max. Redirects": "Max. Redirects",
|
||||
|
|
|
@ -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,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">
|
||||
<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 -->
|
||||
|
@ -621,6 +625,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 +1088,7 @@ const monitorDefaults = {
|
|||
name: "",
|
||||
parent: null,
|
||||
url: "https://",
|
||||
wsIgnoreHeaders: false,
|
||||
method: "GET",
|
||||
interval: 60,
|
||||
retryInterval: 60,
|
||||
|
@ -1422,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
|
||||
|
|
120
test/backend-test/test-websocket.js
Normal file
120
test/backend-test/test-websocket.js
Normal 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);
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue