Skip to content
Merged
15 changes: 15 additions & 0 deletions db/knex_migrations/2025-07-17-0000-mqtt-websocket-path.js
Original file line number Diff line number Diff line change
@@ -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");
});
};
1 change: 1 addition & 0 deletions server/model/monitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 12 additions & 3 deletions server/monitor-types/mqtt.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 === "") {
Expand Down Expand Up @@ -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)) {
Expand All @@ -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}`);

Expand Down
1 change: 1 addition & 0 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link
Collaborator

@CommanderStorm CommanderStorm Jul 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not used anymore

Suggested change
"Path": "Path",

"Heartbeat Interval": "Heartbeat Interval",
"Request Timeout": "Request Timeout",
"timeoutAfter": "Timeout after {0} seconds",
Expand Down Expand Up @@ -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",
Expand Down
33 changes: 33 additions & 0 deletions src/pages/EditMonitor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 -->
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -1181,6 +1203,7 @@ const monitorDefaults = {
mqttUsername: "",
mqttPassword: "",
mqttTopic: "",
mqttWebsocketPath: "",
mqttSuccessMessage: "",
mqttCheckType: "keyword",
authMethod: null,
Expand Down Expand Up @@ -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;
},

Expand Down
1 change: 1 addition & 0 deletions test/backend-test/test-mqtt.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading