This commit is contained in:
Hemanth Rachapalli 2025-07-17 06:48:03 +00:00 committed by GitHub
commit 7a43727a8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 248 additions and 6 deletions

View file

@ -0,0 +1,18 @@
// Add new columns and alter 'manual_status' to smallint
// migration file: add_rtsp_fields_to_monitor.js
exports.up = function (knex) {
return knex.schema.alterTable("monitor", function (table) {
table.string("rtsp_username");
table.string("rtsp_password");
table.string("rtsp_path");
});
};
exports.down = function (knex) {
return knex.schema.alterTable("monitor", function (table) {
table.dropColumn("rtsp_username");
table.dropColumn("rtsp_password");
table.dropColumn("rtsp_path");
});
};

23
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "uptime-kuma",
"version": "2.0.0-beta.2",
"version": "2.0.0-beta.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "uptime-kuma",
"version": "2.0.0-beta.2",
"version": "2.0.0-beta.3",
"license": "MIT",
"dependencies": {
"@grpc/grpc-js": "~1.8.22",
@ -75,6 +75,7 @@
"qs": "~6.10.4",
"redbean-node": "~0.3.0",
"redis": "~4.5.1",
"rtsp-client": "^1.4.5",
"semver": "~7.5.4",
"socket.io": "~4.8.0",
"socket.io-client": "~4.8.0",
@ -14850,6 +14851,16 @@
"rtlcss": "bin/rtlcss.js"
}
},
"node_modules/rtsp-client": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/rtsp-client/-/rtsp-client-1.4.5.tgz",
"integrity": "sha512-21ZjCoGZdCPOTZOME1BzZ+OCXJIU6SQoGEAKwlbuVPU/jhAX9S8wTr+ZkBMR074xK28UfFx1KQvTV1/wECU3MA==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.15",
"www-authenticate": "^0.6.2"
}
},
"node_modules/run-applescript": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz",
@ -17852,6 +17863,14 @@
}
}
},
"node_modules/www-authenticate": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/www-authenticate/-/www-authenticate-0.6.3.tgz",
"integrity": "sha512-8VkdLBJiBh5aXlJvcVaPykwSI//OA+Sxw7g84vIyCqoqlXtLupGNhyXxbgVuZ7g5ZS+lCJ4bTtcw/gJciqEuAg==",
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/xml-js": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",

View file

@ -133,6 +133,7 @@
"qs": "~6.10.4",
"redbean-node": "~0.3.0",
"redis": "~4.5.1",
"rtsp-client": "^1.4.5",
"semver": "~7.5.4",
"socket.io": "~4.8.0",
"socket.io-client": "~4.8.0",

View file

@ -198,6 +198,10 @@ class Monitor extends BeanModel {
kafkaProducerSaslOptions: JSON.parse(this.kafkaProducerSaslOptions),
rabbitmqUsername: this.rabbitmqUsername,
rabbitmqPassword: this.rabbitmqPassword,
rtspUsername: this.rtspUsername,
rtspPassword: this.rtspPassword,
rtspPath: this.rtspPath
};
}

View file

@ -0,0 +1,68 @@
const { MonitorType } = require("./monitor-type");
const RTSPClient = require("rtsp-client");
const { log, UP, DOWN } = require("../../src/util");
class RtspMonitorType extends MonitorType {
name = "rtsp";
/**
* @inheritdoc
*/
async check(monitor, heartbeat, _server) {
const { rtspUsername, rtspPassword, hostname, port, rtspPath } = monitor;
// Construct RTSP URL
let url = `rtsp://${hostname}:${port}${rtspPath}`;
if (rtspUsername && rtspPassword !== undefined) {
url = `rtsp://${rtspUsername}:${rtspPassword}@${hostname}:${port}${rtspPath}`;
}
// Validate URL
if (!url || !url.startsWith("rtsp://")) {
heartbeat.status = DOWN;
heartbeat.msg = "Invalid RTSP URL";
return;
}
const client = new RTSPClient();
client.on("error", (err) => {
log.debug("monitor", `RTSP client emitted error: ${err.message}`);
});
try {
log.debug("monitor", `Connecting to RTSP URL: ${url}`);
await client.connect(url);
const res = await client.describe();
log.debug("monitor", `RTSP DESCRIBE response: ${JSON.stringify(res)}`);
const statusCode = res?.statusCode;
const statusMessage = res?.statusMessage || "Unknown";
if (statusCode === 200) {
heartbeat.status = UP;
heartbeat.msg = "RTSP stream is accessible";
} else if (statusCode === 503) {
heartbeat.status = DOWN;
heartbeat.msg = res.body?.reason || "Service Unavailable";
} else {
heartbeat.status = DOWN;
heartbeat.msg = `${statusCode} - ${statusMessage}`;
}
} catch (error) {
log.debug("monitor", `[${monitor.name}] RTSP check failed: ${error.message}`);
heartbeat.status = DOWN;
heartbeat.msg = `RTSP check failed: ${error.message}`;
} finally {
try {
await client.close();
} catch (closeError) {
log.debug("monitor", `Error closing RTSP client: ${closeError.message}`);
}
}
}
}
module.exports = {
RtspMonitorType,
};

View file

@ -878,6 +878,9 @@ let needSetup = false;
bean.rabbitmqPassword = monitor.rabbitmqPassword;
bean.conditions = JSON.stringify(monitor.conditions);
bean.manual_status = monitor.manual_status;
bean.rtspUsername = monitor.rtspUsername;
bean.rtspPassword = monitor.rtspPassword;
bean.rtspPath = monitor.rtspPath;
// ping advanced options
bean.ping_numeric = monitor.ping_numeric;

View file

@ -119,6 +119,7 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
UptimeKumaServer.monitorTypeList["rabbitmq"] = new RabbitMqMonitorType();
UptimeKumaServer.monitorTypeList["manual"] = new ManualMonitorType();
UptimeKumaServer.monitorTypeList["rtsp"] = new RtspMonitorType();
// Allow all CORS origins (polling) in development
let cors = undefined;
@ -560,4 +561,5 @@ const { SNMPMonitorType } = require("./monitor-types/snmp");
const { MongodbMonitorType } = require("./monitor-types/mongodb");
const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq");
const { ManualMonitorType } = require("./monitor-types/manual");
const { RtspMonitorType } = require("./monitor-types/rtsp");
const Monitor = require("./model/monitor");

View file

@ -1125,5 +1125,9 @@
"Staged Tags for Batch Add": "Staged Tags for Batch Add",
"Clear Form": "Clear Form",
"pause": "Pause",
"Manual": "Manual"
"Manual": "Manual",
"RTSP Username": "RTSP Username",
"RTSP Password": "RTSP Password",
"RTSP Path": "RTSP Path",
"Path": "Path"
}

View file

@ -97,6 +97,9 @@
<option v-if="!$root.info.isContainer" value="tailscale-ping">
Tailscale Ping
</option>
<option value="rtsp">
RTSP
</option>
</optgroup>
</select>
<i18n-t v-if="monitor.type === 'rabbitmq'" keypath="rabbitmqHelpText" tag="div" class="form-text">
@ -300,7 +303,7 @@
<!-- Hostname -->
<!-- TCP Port / Ping / DNS / Steam / MQTT / Radius / Tailscale Ping / SNMP / SMTP only -->
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'tailscale-ping' || monitor.type === 'smtp' || monitor.type === 'snmp'" class="my-3">
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'tailscale-ping' || monitor.type === 'smtp' || monitor.type === 'snmp' || monitor.type === 'rtsp'" class="my-3">
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
<input
id="hostname"
@ -315,7 +318,7 @@
<!-- Port -->
<!-- For TCP Port / Steam / MQTT / Radius Type / SNMP -->
<div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'smtp' || monitor.type === 'snmp'" class="my-3">
<div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'smtp' || monitor.type === 'snmp' || monitor.type === 'rtsp'" class="my-3">
<label for="port" class="form-label">{{ $t("Port") }}</label>
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
</div>
@ -515,6 +518,13 @@
</div>
</template>
<template v-if="monitor.type === 'rtsp'">
<div class="my-3">
<label for="rtspPath" class="form-label"> {{ $t("RTSP Path") }}</label>
<input id="rtspPath" v-model="monitor.rtspPath" :placeholder="$t('Path')" type="text" class="form-control">
</div>
</template>
<template v-if="monitor.type === 'radius'">
<div class="my-3">
<label for="radius_username" class="form-label">Radius {{ $t("Username") }}</label>
@ -1056,6 +1066,22 @@
</template>
</template>
<template v-if="monitor.type === 'rtsp'">
<h4 class="mt-5 mb-2">{{ $t("Authentication") }}</h4>
<div class="my-3">
<label for="rtspUsername" class="form-label">{{ $t("RTSP Username") }}</label>
<input
id="rtspUsername" v-model="monitor.rtspUsername" :placeholder="$t('Username')" type="text" class="form-control"
>
</div>
<div class="my-3">
<label for="rtspPassword" class="form-label">{{ $t("RTSP Password") }}</label>
<input id="rtspPassword" v-model="monitor.rtspPassword" :placeholder="$t('Password')" type="password" class="form-control">
</div>
</template>
<!-- gRPC Options -->
<template v-if="monitor.type === 'grpc-keyword' ">
<!-- Proto service enable TLS -->
@ -1198,7 +1224,11 @@ const monitorDefaults = {
rabbitmqNodes: [],
rabbitmqUsername: "",
rabbitmqPassword: "",
conditions: []
conditions: [],
rtspUsername: "",
rtspPassword: "",
rtspPath: ""
};
export default {

View file

@ -0,0 +1,93 @@
const { describe, test } = require("node:test");
const assert = require("node:assert");
const { RtspMonitorType } = require("../../server/monitor-types/rtsp");
const { UP, DOWN, PENDING } = require("../../src/util");
const RTSPClient = require("rtsp-client");
describe("RTSP Monitor", {
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64"),
}, () => {
test("RTSP stream is accessible", async () => {
const rtspMonitor = new RtspMonitorType();
const monitor = {
hostname: "localhost",
port: 8554,
rtspPath: "/teststream",
rtspUsername: "user",
rtspPassword: "pass",
name: "RTSP Test Monitor",
};
const heartbeat = {
msg: "",
status: PENDING,
};
RTSPClient.prototype.connect = async () => {};
RTSPClient.prototype.describe = async () => ({
statusCode: 200,
statusMessage: "OK",
});
RTSPClient.prototype.close = async () => {};
await rtspMonitor.check(monitor, heartbeat, {});
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "RTSP stream is accessible");
});
test("RTSP stream is not accessible", async () => {
const rtspMonitor = new RtspMonitorType();
const monitor = {
hostname: "localhost",
port: 9999,
rtspPath: "/teststream",
rtspUsername: "user",
rtspPassword: "pass",
name: "RTSP Test Monitor",
};
const heartbeat = {
msg: "",
status: PENDING,
};
RTSPClient.prototype.connect = async () => {
throw new Error("Connection refused");
};
RTSPClient.prototype.close = async () => {};
await rtspMonitor.check(monitor, heartbeat, {});
assert.strictEqual(heartbeat.status, DOWN);
assert.match(heartbeat.msg, /RTSP check failed: Connection refused/);
});
test("RTSP stream returns 503 error", async () => {
const rtspMonitor = new RtspMonitorType();
const monitor = {
hostname: "localhost",
port: 8554,
rtspPath: "/teststream",
rtspUsername: "user",
rtspPassword: "pass",
name: "RTSP Test Monitor",
};
const heartbeat = {
msg: "",
status: PENDING,
};
RTSPClient.prototype.connect = async () => {};
RTSPClient.prototype.describe = async () => ({
statusCode: 503,
statusMessage: "Service Unavailable",
body: { reason: "Server overloaded" },
});
RTSPClient.prototype.close = async () => {};
await rtspMonitor.check(monitor, heartbeat, {});
assert.strictEqual(heartbeat.status, DOWN);
assert.strictEqual(heartbeat.msg, "Server overloaded");
});
});