Merge branch 'master' into snmp-monitor

This commit is contained in:
Frank Elsinga
2024-06-12 19:55:31 +02:00
committed by GitHub
78 changed files with 4345 additions and 981 deletions

View File

@@ -209,9 +209,9 @@ class Database {
let config = {};
let mariadbPoolConfig = {
afterCreate: function (conn, done) {
}
min: 0,
max: 10,
idleTimeoutMillis: 30000,
};
log.info("db", `Database Type: ${dbConfig.type}`);
@@ -223,11 +223,8 @@ class Database {
fs.copyFileSync(Database.templatePath, Database.sqlitePath);
}
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
Dialect.prototype._driver = () => require("@louislam/sqlite3");
config = {
client: Dialect,
client: "sqlite3",
connection: {
filename: Database.sqlitePath,
acquireConnectionTimeout: acquireConnectionTimeout,

View File

@@ -5,7 +5,7 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MI
SQL_DATETIME_FORMAT, evaluateJsonQuery
} = require("../../src/util");
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
} = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
@@ -811,15 +811,6 @@ class Monitor extends BeanModel {
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1", mysqlPassword);
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "mongodb") {
let startTime = dayjs().valueOf();
await mongodbPing(this.databaseConnectionString);
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "radius") {
let startTime = dayjs().valueOf();
@@ -850,7 +841,7 @@ class Monitor extends BeanModel {
} else if (this.type === "redis") {
let startTime = dayjs().valueOf();
bean.msg = await redisPingAsync(this.databaseConnectionString);
bean.msg = await redisPingAsync(this.databaseConnectionString, !this.ignoreTls);
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
@@ -895,7 +886,6 @@ class Monitor extends BeanModel {
retries = 0;
} catch (error) {
if (error?.name === "CanceledError") {
bean.msg = `timeout by AbortSignal (${this.timeout}s)`;
} else {
@@ -968,6 +958,7 @@ class Monitor extends BeanModel {
} else if (bean.status === MAINTENANCE) {
log.warn("monitor", `Monitor #${this.id} '${this.name}': Under Maintenance | Type: ${this.type}`);
} else {
beatInterval = this.retryInterval;
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
}

View File

@@ -238,6 +238,7 @@ class StatusPage extends BeanModel {
description: this.description,
icon: this.getIcon(),
theme: this.theme,
autoRefreshInterval: this.autoRefreshInterval,
published: !!this.published,
showTags: !!this.show_tags,
domainNameList: this.getDomainNameList(),
@@ -260,6 +261,7 @@ class StatusPage extends BeanModel {
title: this.title,
description: this.description,
icon: this.getIcon(),
autoRefreshInterval: this.autoRefreshInterval,
theme: this.theme,
published: !!this.published,
showTags: !!this.show_tags,

View File

@@ -5,7 +5,6 @@ const { dnsResolve } = require("../util-server");
const { R } = require("redbean-node");
class DnsMonitorType extends MonitorType {
name = "dns";
/**

View File

@@ -0,0 +1,63 @@
const { MonitorType } = require("./monitor-type");
const { UP } = require("../../src/util");
const { MongoClient } = require("mongodb");
const jsonata = require("jsonata");
class MongodbMonitorType extends MonitorType {
name = "mongodb";
/**
* @inheritdoc
*/
async check(monitor, heartbeat, _server) {
let command = { "ping": 1 };
if (monitor.databaseQuery) {
command = JSON.parse(monitor.databaseQuery);
}
let result = await this.runMongodbCommand(monitor.databaseConnectionString, command);
if (result["ok"] !== 1) {
throw new Error("MongoDB command failed");
} else {
heartbeat.msg = "Command executed successfully";
}
if (monitor.jsonPath) {
let expression = jsonata(monitor.jsonPath);
result = await expression.evaluate(result);
if (result) {
heartbeat.msg = "Command executed successfully and the jsonata expression produces a result.";
} else {
throw new Error("Queried value not found.");
}
}
if (monitor.expectedValue) {
if (result.toString() === monitor.expectedValue) {
heartbeat.msg = "Command executed successfully and expected value was found";
} else {
throw new Error("Query executed, but value is not equal to expected value, value was: [" + JSON.stringify(result) + "]");
}
}
heartbeat.status = UP;
}
/**
* Connect to and run MongoDB command on a MongoDB database
* @param {string} connectionString The database connection string
* @param {object} command MongoDB command to run on the database
* @returns {Promise<(string[] | object[] | object)>} Response from server
*/
async runMongodbCommand(connectionString, command) {
let client = await MongoClient.connect(connectionString);
let result = await client.db().command(command);
await client.close();
return result;
}
}
module.exports = {
MongodbMonitorType,
};

View File

@@ -11,7 +11,6 @@ class MonitorType {
async check(monitor, heartbeat, server) {
throw new Error("You need to override check()");
}
}
module.exports = {

View File

@@ -4,15 +4,10 @@ const mqtt = require("mqtt");
const jsonata = require("jsonata");
class MqttMonitorType extends MonitorType {
name = "mqtt";
/**
* Run the monitoring check on the MQTT monitor
* @param {Monitor} monitor Monitor to check
* @param {Heartbeat} heartbeat Monitor heartbeat to update
* @param {UptimeKumaServer} server Uptime Kuma server
* @returns {Promise<void>}
* @inheritdoc
*/
async check(monitor, heartbeat, server) {
const receivedMessage = await this.mqttAsync(monitor.hostname, monitor.mqttTopic, {

View File

@@ -2,23 +2,13 @@ const { MonitorType } = require("./monitor-type");
const { UP } = require("../../src/util");
const childProcessAsync = require("promisify-child-process");
/**
* A TailscalePing class extends the MonitorType.
* It runs Tailscale ping to monitor the status of a specific node.
*/
class TailscalePing extends MonitorType {
name = "tailscale-ping";
/**
* Checks the ping status of the URL associated with the monitor.
* It then parses the Tailscale ping command output to update the heatrbeat.
* @param {object} monitor The monitor object associated with the check.
* @param {object} heartbeat The heartbeat object to update.
* @returns {Promise<void>}
* @throws Error if checking Tailscale ping encounters any error
* @inheritdoc
*/
async check(monitor, heartbeat) {
async check(monitor, heartbeat, _server) {
try {
let tailscaleOutput = await this.runTailscalePing(monitor.hostname, monitor.interval);
this.parseTailscaleOutput(tailscaleOutput, heartbeat);

View File

@@ -0,0 +1,31 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { UP } = require("../../src/util");
class Bitrix24 extends NotificationProvider {
name = "Bitrix24";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
const params = {
user_id: notification.bitrix24UserID,
message: "[B]Uptime Kuma[/B]",
"ATTACH[COLOR]": (heartbeatJSON ?? {})["status"] === UP ? "#b73419" : "#67b518",
"ATTACH[BLOCKS][0][MESSAGE]": msg
};
await axios.get(`${notification.bitrix24WebhookURL}/im.notify.system.add.json`, { params });
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Bitrix24;

View File

@@ -13,6 +13,10 @@ class Discord extends NotificationProvider {
try {
const discordDisplayName = notification.discordUsername || "Uptime Kuma";
const webhookUrl = new URL(notification.discordWebhookUrl);
if (notification.discordChannelType === "postToThread") {
webhookUrl.searchParams.append("thread_id", notification.threadId);
}
// If heartbeatJSON is null, assume we're testing.
if (heartbeatJSON == null) {
@@ -20,7 +24,12 @@ class Discord extends NotificationProvider {
username: discordDisplayName,
content: msg,
};
await axios.post(notification.discordWebhookUrl, discordtestdata);
if (notification.discordChannelType === "createNewForumPost") {
discordtestdata.thread_name = notification.postName;
}
await axios.post(webhookUrl.toString(), discordtestdata);
return okMsg;
}
@@ -72,12 +81,14 @@ class Discord extends NotificationProvider {
],
}],
};
if (notification.discordChannelType === "createNewForumPost") {
discorddowndata.thread_name = notification.postName;
}
if (notification.discordPrefixMessage) {
discorddowndata.content = notification.discordPrefixMessage;
}
await axios.post(notification.discordWebhookUrl, discorddowndata);
await axios.post(webhookUrl.toString(), discorddowndata);
return okMsg;
} else if (heartbeatJSON["status"] === UP) {
@@ -108,11 +119,15 @@ class Discord extends NotificationProvider {
}],
};
if (notification.discordChannelType === "createNewForumPost") {
discordupdata.thread_name = notification.postName;
}
if (notification.discordPrefixMessage) {
discordupdata.content = notification.discordPrefixMessage;
}
await axios.post(notification.discordWebhookUrl, discordupdata);
await axios.post(webhookUrl.toString(), discordupdata);
return okMsg;
}
} catch (error) {

View File

@@ -25,25 +25,29 @@ class Feishu extends NotificationProvider {
if (heartbeatJSON["status"] === DOWN) {
let downdata = {
msg_type: "post",
content: {
post: {
zh_cn: {
title: "UptimeKuma Alert: [Down] " + monitorJSON["name"],
content: [
[
{
tag: "text",
text:
"[Down] " +
heartbeatJSON["msg"] +
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
},
],
],
},
msg_type: "interactive",
card: {
config: {
update_multi: false,
wide_screen_mode: true,
},
},
header: {
title: {
tag: "plain_text",
content: "UptimeKuma Alert: [Down] " + monitorJSON["name"],
},
template: "red",
},
elements: [
{
tag: "div",
text: {
tag: "lark_md",
content: getContent(heartbeatJSON),
},
}
]
}
};
await axios.post(notification.feishuWebHookUrl, downdata);
return okMsg;
@@ -51,25 +55,29 @@ class Feishu extends NotificationProvider {
if (heartbeatJSON["status"] === UP) {
let updata = {
msg_type: "post",
content: {
post: {
zh_cn: {
title: "UptimeKuma Alert: [Up] " + monitorJSON["name"],
content: [
[
{
tag: "text",
text:
"[Up] " +
heartbeatJSON["msg"] +
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
},
],
],
},
msg_type: "interactive",
card: {
config: {
update_multi: false,
wide_screen_mode: true,
},
},
header: {
title: {
tag: "plain_text",
content: "UptimeKuma Alert: [UP] " + monitorJSON["name"],
},
template: "green",
},
elements: [
{
tag: "div",
text: {
tag: "lark_md",
content: getContent(heartbeatJSON),
},
},
]
}
};
await axios.post(notification.feishuWebHookUrl, updata);
return okMsg;
@@ -80,4 +88,17 @@ class Feishu extends NotificationProvider {
}
}
/**
* Get content
* @param {?object} heartbeatJSON Heartbeat details (For Up/Down only)
* @returns {string} Return Successful Message
*/
function getContent(heartbeatJSON) {
return [
"**Message**: " + heartbeatJSON["msg"],
"**Ping**: " + (heartbeatJSON["ping"] == null ? "N/A" : heartbeatJSON["ping"] + " ms"),
`**Time (${heartbeatJSON["timezone"]})**: ${heartbeatJSON["localDateTime"]}`
].join("\n");
}
module.exports = Feishu;

View File

@@ -62,6 +62,15 @@ class FlashDuty extends NotificationProvider {
* @returns {string} Success message
*/
async postNotification(notification, title, body, monitorInfo, eventStatus) {
let labels = {
resource: this.genMonitorUrl(monitorInfo),
check: monitorInfo.name,
};
if (monitorInfo.tags && monitorInfo.tags.length > 0) {
for (let tag of monitorInfo.tags) {
labels[tag.name] = tag.value;
}
}
const options = {
method: "POST",
url: "https://api.flashcat.cloud/event/push/alert/standard?integration_key=" + notification.flashdutyIntegrationKey,
@@ -71,9 +80,7 @@ class FlashDuty extends NotificationProvider {
title,
event_status: eventStatus || "Info",
alert_key: String(monitorInfo.id) || Math.random().toString(36).substring(7),
labels: monitorInfo?.tags?.reduce((acc, item) => ({ ...acc,
[item.name]: item.value
}), { resource: this.genMonitorUrl(monitorInfo) }),
labels,
}
};

View File

@@ -0,0 +1,46 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class SMSPartner extends NotificationProvider {
name = "SMSPartner";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const url = "https://api.smspartner.fr/v1/send";
try {
// smspartner does not support non ascii characters and only a maximum 639 characters
let cleanMsg = msg.replace(/[^\x00-\x7F]/g, "").substring(0, 639);
let data = {
"apiKey": notification.smspartnerApikey,
"sender": notification.smspartnerSenderName.substring(0, 11),
"phoneNumbers": notification.smspartnerPhoneNumber,
"message": cleanMsg,
};
let config = {
headers: {
"Content-Type": "application/json",
"cache-control": "no-cache",
"Accept": "application/json",
}
};
let resp = await axios.post(url, data, config);
if (resp.data.success !== true) {
throw Error(`Api returned ${resp.data.response.status}.`);
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = SMSPartner;

View File

@@ -5,6 +5,7 @@ const AlertNow = require("./notification-providers/alertnow");
const AliyunSms = require("./notification-providers/aliyun-sms");
const Apprise = require("./notification-providers/apprise");
const Bark = require("./notification-providers/bark");
const Bitrix24 = require("./notification-providers/bitrix24");
const ClickSendSMS = require("./notification-providers/clicksendsms");
const CallMeBot = require("./notification-providers/call-me-bot");
const SMSC = require("./notification-providers/smsc");
@@ -42,6 +43,7 @@ const RocketChat = require("./notification-providers/rocket-chat");
const SerwerSMS = require("./notification-providers/serwersms");
const Signal = require("./notification-providers/signal");
const Slack = require("./notification-providers/slack");
const SMSPartner = require("./notification-providers/smspartner");
const SMSEagle = require("./notification-providers/smseagle");
const SMTP = require("./notification-providers/smtp");
const Squadcast = require("./notification-providers/squadcast");
@@ -83,6 +85,7 @@ class Notification {
new AliyunSms(),
new Apprise(),
new Bark(),
new Bitrix24(),
new ClickSendSMS(),
new CallMeBot(),
new SMSC(),
@@ -121,6 +124,7 @@ class Notification {
new SerwerSMS(),
new Signal(),
new SMSManager(),
new SMSPartner(),
new Slack(),
new SMSEagle(),
new SMTP(),

View File

@@ -44,9 +44,8 @@ router.get("/api/entry-page", async (request, response) => {
response.json(result);
});
router.get("/api/push/:pushToken", async (request, response) => {
router.all("/api/push/:pushToken", async (request, response) => {
try {
let pushToken = request.params.pushToken;
let msg = request.query.msg || "OK";
let ping = parseFloat(request.query.ping) || null;

View File

@@ -149,6 +149,7 @@ const apicache = require("./modules/apicache");
const { resetChrome } = require("./monitor-types/real-browser-monitor-type");
const { EmbeddedMariaDB } = require("./embedded-mariadb");
const { SetupDatabase } = require("./setup-database");
const { chartSocketHandler } = require("./socket-handlers/chart-socket-handler");
app.use(express.json());
@@ -1532,6 +1533,7 @@ let needSetup = false;
apiKeySocketHandler(socket);
remoteBrowserSocketHandler(socket);
generalSocketHandler(socket, server);
chartSocketHandler(socket);
log.debug("server", "added all socket handlers");

View File

@@ -0,0 +1,38 @@
const { checkLogin } = require("../util-server");
const { UptimeCalculator } = require("../uptime-calculator");
const { log } = require("../../src/util");
module.exports.chartSocketHandler = (socket) => {
socket.on("getMonitorChartData", async (monitorID, period, callback) => {
try {
checkLogin(socket);
log.debug("monitor", `Get Monitor Chart Data: ${monitorID} User ID: ${socket.userID}`);
if (period == null) {
throw new Error("Invalid period.");
}
let uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitorID);
let data;
if (period <= 24) {
data = uptimeCalculator.getDataArray(period * 60, "minute");
} else if (period <= 720) {
data = uptimeCalculator.getDataArray(period, "hour");
} else {
data = uptimeCalculator.getDataArray(period / 24, "day");
}
callback({
ok: true,
data,
});
} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});
};

View File

@@ -148,13 +148,14 @@ module.exports.statusPageSocketHandler = (socket) => {
config.logo = `/upload/${filename}?t=` + Date.now();
} else {
config.icon = imgDataUrl;
config.logo = imgDataUrl;
}
statusPage.slug = config.slug;
statusPage.title = config.title;
statusPage.description = config.description;
statusPage.icon = config.logo;
statusPage.autoRefreshInterval = config.autoRefreshInterval,
statusPage.theme = config.theme;
//statusPage.published = ;
//statusPage.search_engine_index = ;
@@ -280,6 +281,7 @@ module.exports.statusPageSocketHandler = (socket) => {
statusPage.title = title;
statusPage.theme = "auto";
statusPage.icon = "";
statusPage.autoRefreshInterval = 300;
await R.store(statusPage);
callback({

View File

@@ -290,7 +290,7 @@ class UptimeCalculator {
dailyStatBean.pingMax = dailyData.maxPing;
{
// eslint-disable-next-line no-unused-vars
const { up, down, avgPing, minPing, maxPing, ...extras } = dailyData;
const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = dailyData;
if (Object.keys(extras).length > 0) {
dailyStatBean.extras = JSON.stringify(extras);
}
@@ -305,7 +305,7 @@ class UptimeCalculator {
hourlyStatBean.pingMax = hourlyData.maxPing;
{
// eslint-disable-next-line no-unused-vars
const { up, down, avgPing, minPing, maxPing, ...extras } = hourlyData;
const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = hourlyData;
if (Object.keys(extras).length > 0) {
hourlyStatBean.extras = JSON.stringify(extras);
}
@@ -320,7 +320,7 @@ class UptimeCalculator {
minutelyStatBean.pingMax = minutelyData.maxPing;
{
// eslint-disable-next-line no-unused-vars
const { up, down, avgPing, minPing, maxPing, ...extras } = minutelyData;
const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = minutelyData;
if (Object.keys(extras).length > 0) {
minutelyStatBean.extras = JSON.stringify(extras);
}

View File

@@ -114,6 +114,7 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType();
UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
// Allow all CORS origins (polling) in development
let cors = undefined;
@@ -518,3 +519,4 @@ const { TailscalePing } = require("./monitor-types/tailscale-ping");
const { DnsMonitorType } = require("./monitor-types/dns");
const { MqttMonitorType } = require("./monitor-types/mqtt");
const { SNMPMonitorType } = require("./monitor-types/snmp");
const { MongodbMonitorType } = require("./monitor-types/mongodb");

View File

@@ -11,7 +11,6 @@ const mssql = require("mssql");
const { Client } = require("pg");
const postgresConParse = require("pg-connection-string").parse;
const mysql = require("mysql2");
const { MongoClient } = require("mongodb");
const { NtlmClient } = require("axios-ntlm");
const { Settings } = require("./settings");
const grpc = require("@grpc/grpc-js");
@@ -437,24 +436,6 @@ exports.mysqlQuery = function (connectionString, query, password = undefined) {
});
};
/**
* Connect to and ping a MongoDB database
* @param {string} connectionString The database connection string
* @returns {Promise<(string[] | object[] | object)>} Response from
* server
*/
exports.mongodbPing = async function (connectionString) {
let client = await MongoClient.connect(connectionString);
let dbPing = await client.db().command({ ping: 1 });
await client.close();
if (dbPing["ok"] === 1) {
return "UP";
} else {
throw Error("failed");
}
};
/**
* Query radius server
* @param {string} hostname Hostname of radius server
@@ -505,12 +486,16 @@ exports.radius = function (
/**
* Redis server ping
* @param {string} dsn The redis connection string
* @returns {Promise<any>} Response from redis server
* @param {boolean} rejectUnauthorized If false, allows unverified server certificates.
* @returns {Promise<any>} Response from server
*/
exports.redisPingAsync = function (dsn) {
exports.redisPingAsync = function (dsn, rejectUnauthorized) {
return new Promise((resolve, reject) => {
const client = redis.createClient({
url: dsn
url: dsn,
socket: {
rejectUnauthorized
}
});
client.on("error", (err) => {
if (client.isOpen) {