mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-07-18 15:24:03 +02:00
Merge 6eff6f9295
into 2fd4e1cc72
This commit is contained in:
commit
7a43727a8b
10 changed files with 248 additions and 6 deletions
18
db/knex_migrations/2025-06-27-0001-add-rtsp.js
Normal file
18
db/knex_migrations/2025-06-27-0001-add-rtsp.js
Normal 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
23
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
|
68
server/monitor-types/rtsp.js
Normal file
68
server/monitor-types/rtsp.js
Normal 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,
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
93
test/backend-test/test-rtsp.js
Normal file
93
test/backend-test/test-rtsp.js
Normal 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");
|
||||
});
|
||||
});
|
||||
|
Loading…
Add table
Reference in a new issue