Add Websocket path to mqtt monitor for WebSocket connection (#6009)
Some checks failed
Auto Test / armv7-simple-test (18, ARMv7) (push) Has been cancelled
Auto Test / armv7-simple-test (20, ARMv7) (push) Has been cancelled
Auto Test / check-linters (push) Has been cancelled
Auto Test / e2e-test (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
Merge Conflict Labeler / Labeling (push) Has been cancelled
validate / json-yaml-validate (push) Has been cancelled
validate / validate (push) Has been cancelled
Auto Test / auto-test (18, ARM64) (push) Has been cancelled
Auto Test / auto-test (18, macos-latest) (push) Has been cancelled
Auto Test / auto-test (18, ubuntu-latest) (push) Has been cancelled
Auto Test / auto-test (18, windows-latest) (push) Has been cancelled
Auto Test / auto-test (20, ARM64) (push) Has been cancelled
Auto Test / auto-test (20, macos-latest) (push) Has been cancelled
Auto Test / auto-test (20, ubuntu-latest) (push) Has been cancelled
Auto Test / auto-test (20, windows-latest) (push) Has been cancelled

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lupaulus <20111917+lupaulus@users.noreply.github.com>
This commit is contained in:
Paulus Lucas 2025-07-24 22:04:43 +02:00 committed by GitHub
parent 2fd4e1cc72
commit 2a6d9b4acd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 68 additions and 3 deletions

View file

@ -0,0 +1,15 @@
exports.up = function (knex) {
// Add new column monitor.mqtt_websocket_path
return knex.schema
.alterTable("monitor", function (table) {
table.string("mqtt_websocket_path", 255).nullable();
});
};
exports.down = function (knex) {
// Drop column monitor.mqtt_websocket_path
return knex.schema
.alterTable("monitor", function (table) {
table.dropColumn("mqtt_websocket_path");
});
};

View file

@ -190,6 +190,7 @@ class Monitor extends BeanModel {
radiusSecret: this.radiusSecret,
mqttUsername: this.mqttUsername,
mqttPassword: this.mqttPassword,
mqttWebsocketPath: this.mqttWebsocketPath,
authWorkstation: this.authWorkstation,
authDomain: this.authDomain,
tlsCa: this.tlsCa,

View file

@ -15,6 +15,7 @@ class MqttMonitorType extends MonitorType {
username: monitor.mqttUsername,
password: monitor.mqttPassword,
interval: monitor.interval,
websocketPath: monitor.mqttWebsocketPath,
});
if (monitor.mqttCheckType == null || monitor.mqttCheckType === "") {
@ -52,12 +53,12 @@ class MqttMonitorType extends MonitorType {
* @param {string} hostname Hostname / address of machine to test
* @param {string} topic MQTT topic
* @param {object} options MQTT options. Contains port, username,
* password and interval (interval defaults to 20)
* password, websocketPath and interval (interval defaults to 20)
* @returns {Promise<string>} Received MQTT message
*/
mqttAsync(hostname, topic, options = {}) {
return new Promise((resolve, reject) => {
const { port, username, password, interval = 20 } = options;
const { port, username, password, websocketPath, interval = 20 } = options;
// Adds MQTT protocol to the hostname if not already present
if (!/^(?:http|mqtt|ws)s?:\/\//.test(hostname)) {
@ -70,7 +71,15 @@ class MqttMonitorType extends MonitorType {
reject(new Error("Timeout, Message not received"));
}, interval * 1000 * 0.8);
const mqttUrl = `${hostname}:${port}`;
// Construct the URL based on protocol
let mqttUrl = `${hostname}:${port}`;
if (hostname.startsWith("ws://") || hostname.startsWith("wss://")) {
if (websocketPath && !websocketPath.startsWith("/")) {
mqttUrl = `${hostname}:${port}/${websocketPath || ""}`;
} else {
mqttUrl = `${hostname}:${port}${websocketPath || ""}`;
}
}
log.debug("mqtt", `MQTT connecting to ${mqttUrl}`);

View file

@ -837,6 +837,7 @@ let needSetup = false;
bean.mqttTopic = monitor.mqttTopic;
bean.mqttSuccessMessage = monitor.mqttSuccessMessage;
bean.mqttCheckType = monitor.mqttCheckType;
bean.mqttWebsocketPath = monitor.mqttWebsocketPath;
bean.databaseConnectionString = monitor.databaseConnectionString;
bean.databaseQuery = monitor.databaseQuery;
bean.authMethod = monitor.authMethod;

View file

@ -71,6 +71,7 @@
"locally configured mail transfer agent": "locally configured mail transfer agent",
"Either enter the hostname of the server you want to connect to or localhost if you intend to use a locally configured mail transfer agent": "Either enter the hostname of the server you want to connect to or {localhost} if you intend to use a {local_mta}",
"Port": "Port",
"Path": "Path",
"Heartbeat Interval": "Heartbeat Interval",
"Request Timeout": "Request Timeout",
"timeoutAfter": "Timeout after {0} seconds",
@ -266,6 +267,10 @@
"Current User": "Current User",
"topic": "Topic",
"topicExplanation": "MQTT topic to monitor",
"mqttWebSocketPath": "MQTT WebSocket Path",
"mqttWebsocketPathExplanation": "WebSocket path for MQTT over WebSocket connections (e.g., /mqtt)",
"mqttWebsocketPathInvalid": "Please use a valid WebSocket Path format",
"mqttHostnameTip": "Please use this format {hostnameFormat}",
"successKeyword": "Success Keyword",
"successKeywordExplanation": "MQTT Keyword that will be considered as success",
"recent": "Recent",

View file

@ -311,6 +311,13 @@
required
data-testid="hostname-input"
>
<div v-if="monitor.type === 'mqtt'" class="form-text">
<i18n-t tag="p" keypath="mqttHostnameTip">
<template #hostnameFormat>
<code>[mqtt,ws,wss]://hostname</code>
</template>
</i18n-t>
</div>
</div>
<!-- Port -->
@ -483,6 +490,21 @@
</div>
</div>
<div class="my-3">
<label for="mqttWebsocketPath" class="form-label">{{ $t("mqttWebSocketPath") }}</label>
<input
v-if="/wss?:\/\/.+/.test(monitor.hostname)"
id="mqttWebsocketPath"
v-model="monitor.mqttWebsocketPath"
type="text"
class="form-control"
>
<input v-else type="text" class="form-control" disabled>
<div class="form-text">
{{ $t("mqttWebsocketPathExplanation") }}
</div>
</div>
<div class="my-3">
<label for="mqttCheckType" class="form-label">MQTT {{ $t("Check Type") }}</label>
<select id="mqttCheckType" v-model="monitor.mqttCheckType" class="form-select" required>
@ -1181,6 +1203,7 @@ const monitorDefaults = {
mqttUsername: "",
mqttPassword: "",
mqttTopic: "",
mqttWebsocketPath: "",
mqttSuccessMessage: "",
mqttCheckType: "keyword",
authMethod: null,
@ -1845,6 +1868,16 @@ message HealthCheckResponse {
return false;
}
}
// Validate MQTT WebSocket Path pattern if present
if (this.monitor.type === "mqtt" && this.monitor.mqttWebsocketPath) {
const pattern = /^\/[A-Za-z0-9-_&()*+]*$/;
if (!pattern.test(this.monitor.mqttWebsocketPath)) {
toast.error(this.$t("mqttWebsocketPathInvalid"));
return false;
}
}
return true;
},

View file

@ -23,6 +23,7 @@ async function testMqtt(mqttSuccessMessage, mqttCheckType, receivedMessage) {
port: connectionString.split(":")[2],
mqttUsername: null,
mqttPassword: null,
mqttWebsocketPath: null, // for WebSocket connections
interval: 20, // controls the timeout
mqttSuccessMessage: mqttSuccessMessage, // for keywords
expectedValue: mqttSuccessMessage, // for json-query