Merge branch 'master' into advisory-fix-1

This commit is contained in:
Louis Lam
2025-06-02 17:48:11 +08:00
65 changed files with 2612 additions and 1157 deletions

View File

@@ -736,7 +736,7 @@ class Database {
if (Database.dbConfig.type === "sqlite") {
return "DATETIME('now', ? || ' hours')";
} else {
return "DATE_ADD(NOW(), INTERVAL ? HOUR)";
return "DATE_ADD(UTC_TIMESTAMP(), INTERVAL ? HOUR)";
}
}

View File

@@ -33,7 +33,7 @@ class Group extends BeanModel {
*/
async getMonitorList() {
return R.convertToBeans("monitor", await R.getAll(`
SELECT monitor.*, monitor_group.send_url FROM monitor, monitor_group
SELECT monitor.*, monitor_group.send_url, monitor_group.custom_url FROM monitor, monitor_group
WHERE monitor.id = monitor_group.monitor_id
AND group_id = ?
ORDER BY monitor_group.weight

View File

@@ -2,7 +2,11 @@ const dayjs = require("dayjs");
const axios = require("axios");
const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
SQL_DATETIME_FORMAT, evaluateJsonQuery
SQL_DATETIME_FORMAT, evaluateJsonQuery,
PING_PACKET_SIZE_MIN, PING_PACKET_SIZE_MAX, PING_PACKET_SIZE_DEFAULT,
PING_GLOBAL_TIMEOUT_MIN, PING_GLOBAL_TIMEOUT_MAX, PING_GLOBAL_TIMEOUT_DEFAULT,
PING_COUNT_MIN, PING_COUNT_MAX, PING_COUNT_DEFAULT,
PING_PER_REQUEST_TIMEOUT_MIN, PING_PER_REQUEST_TIMEOUT_MAX, PING_PER_REQUEST_TIMEOUT_DEFAULT
} = require("../../src/util");
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
@@ -53,7 +57,7 @@ class Monitor extends BeanModel {
};
if (this.sendUrl) {
obj.url = this.url;
obj.url = this.customUrl ?? this.url;
}
if (showTags) {
@@ -153,8 +157,14 @@ class Monitor extends BeanModel {
snmpOid: this.snmpOid,
jsonPathOperator: this.jsonPathOperator,
snmpVersion: this.snmpVersion,
smtpSecurity: this.smtpSecurity,
rabbitmqNodes: JSON.parse(this.rabbitmqNodes),
conditions: JSON.parse(this.conditions),
// ping advanced options
ping_numeric: this.isPingNumeric(),
ping_count: this.ping_count,
ping_per_request_timeout: this.ping_per_request_timeout,
};
if (includeSensitiveData) {
@@ -247,6 +257,14 @@ class Monitor extends BeanModel {
return Boolean(this.expiryNotification);
}
/**
* Check if ping should use numeric output only
* @returns {boolean} True if IP addresses will be output instead of symbolic hostnames
*/
isPingNumeric() {
return Boolean(this.ping_numeric);
}
/**
* Parse to boolean
* @returns {boolean} Should TLS errors be ignored?
@@ -584,7 +602,7 @@ class Monitor extends BeanModel {
bean.status = UP;
} else if (this.type === "ping") {
bean.ping = await ping(this.hostname, this.packetSize);
bean.ping = await ping(this.hostname, this.ping_count, "", this.ping_numeric, this.packetSize, this.timeout, this.ping_per_request_timeout);
bean.msg = "";
bean.status = UP;
} else if (this.type === "push") { // Type: Push
@@ -656,7 +674,7 @@ class Monitor extends BeanModel {
bean.msg = res.data.response.servers[0].name;
try {
bean.ping = await ping(this.hostname, this.packetSize);
bean.ping = await ping(this.hostname, PING_COUNT_DEFAULT, "", true, this.packetSize, PING_GLOBAL_TIMEOUT_DEFAULT, PING_PER_REQUEST_TIMEOUT_DEFAULT);
} catch (_) { }
} else {
throw new Error("Server not found on Steam");
@@ -1294,7 +1312,8 @@ class Monitor extends BeanModel {
try {
const heartbeatJSON = bean.toJSON();
const monitorData = [{ id: monitor.id,
active: monitor.active
active: monitor.active,
name: monitor.name
}];
const preloadData = await Monitor.preparePreloadData(monitorData);
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
@@ -1467,6 +1486,31 @@ class Monitor extends BeanModel {
if (this.interval < MIN_INTERVAL_SECOND) {
throw new Error(`Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`);
}
if (this.type === "ping") {
// ping parameters validation
if (this.packetSize && (this.packetSize < PING_PACKET_SIZE_MIN || this.packetSize > PING_PACKET_SIZE_MAX)) {
throw new Error(`Packet size must be between ${PING_PACKET_SIZE_MIN} and ${PING_PACKET_SIZE_MAX} (default: ${PING_PACKET_SIZE_DEFAULT})`);
}
if (this.ping_per_request_timeout && (this.ping_per_request_timeout < PING_PER_REQUEST_TIMEOUT_MIN || this.ping_per_request_timeout > PING_PER_REQUEST_TIMEOUT_MAX)) {
throw new Error(`Per-ping timeout must be between ${PING_PER_REQUEST_TIMEOUT_MIN} and ${PING_PER_REQUEST_TIMEOUT_MAX} seconds (default: ${PING_PER_REQUEST_TIMEOUT_DEFAULT})`);
}
if (this.ping_count && (this.ping_count < PING_COUNT_MIN || this.ping_count > PING_COUNT_MAX)) {
throw new Error(`Echo requests count must be between ${PING_COUNT_MIN} and ${PING_COUNT_MAX} (default: ${PING_COUNT_DEFAULT})`);
}
if (this.timeout) {
const pingGlobalTimeout = Math.round(Number(this.timeout));
if (pingGlobalTimeout < this.ping_per_request_timeout || pingGlobalTimeout < PING_GLOBAL_TIMEOUT_MIN || pingGlobalTimeout > PING_GLOBAL_TIMEOUT_MAX) {
throw new Error(`Timeout must be between ${PING_GLOBAL_TIMEOUT_MIN} and ${PING_GLOBAL_TIMEOUT_MAX} seconds (default: ${PING_GLOBAL_TIMEOUT_DEFAULT})`);
}
this.timeout = pingGlobalTimeout;
}
}
}
/**

View File

@@ -0,0 +1,35 @@
const { MonitorType } = require("./monitor-type");
const { UP } = require("../../src/util");
const nodemailer = require("nodemailer");
class SMTPMonitorType extends MonitorType {
name = "smtp";
/**
* @inheritdoc
*/
async check(monitor, heartbeat, _server) {
let options = {
port: monitor.port || 25,
host: monitor.hostname,
secure: monitor.smtpSecurity === "secure", // use SMTPS (not STARTTLS)
ignoreTLS: monitor.smtpSecurity === "nostarttls", // don't use STARTTLS even if it's available
requireTLS: monitor.smtpSecurity === "starttls", // use STARTTLS or fail
};
let transporter = nodemailer.createTransport(options);
try {
await transporter.verify();
heartbeat.status = UP;
heartbeat.msg = "SMTP connection verifies successfully";
} catch (e) {
throw new Error(`SMTP connection doesn't verify: ${e}`);
} finally {
transporter.close();
}
}
}
module.exports = {
SMTPMonitorType,
};

View File

@@ -46,10 +46,10 @@ class Discord extends NotificationProvider {
name: "Service Name",
value: monitorJSON["name"],
},
{
...(!notification.disableUrl ? [{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: this.extractAddress(monitorJSON),
},
}] : []),
{
name: `Time (${heartbeatJSON["timezone"]})`,
value: heartbeatJSON["localDateTime"],
@@ -83,10 +83,10 @@ class Discord extends NotificationProvider {
name: "Service Name",
value: monitorJSON["name"],
},
{
...(!notification.disableUrl ? [{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: this.extractAddress(monitorJSON),
},
}] : []),
{
name: `Time (${heartbeatJSON["timezone"]})`,
value: heartbeatJSON["localDateTime"],

View File

@@ -73,13 +73,13 @@ class FlashDuty extends NotificationProvider {
}
const options = {
method: "POST",
url: "https://api.flashcat.cloud/event/push/alert/standard?integration_key=" + notification.flashdutyIntegrationKey,
url: notification.flashdutyIntegrationKey.startsWith("http") ? notification.flashdutyIntegrationKey : "https://api.flashcat.cloud/event/push/alert/standard?integration_key=" + notification.flashdutyIntegrationKey,
headers: { "Content-Type": "application/json" },
data: {
description: `[${title}] [${monitorInfo.name}] ${body}`,
title,
event_status: eventStatus || "Info",
alert_key: String(monitorInfo.id) || Math.random().toString(36).substring(7),
alert_key: monitorInfo.id ? String(monitorInfo.id) : Math.random().toString(36).substring(7),
labels,
}
};

View File

@@ -79,13 +79,11 @@ class Mattermost extends NotificationProvider {
fallback:
"Your " +
monitorJSON.pathName +
monitorJSON.name +
" service went " +
statusText,
color: color,
title:
monitorJSON.pathName +
monitorJSON.name +
" service went " +
statusText,
title_link: monitorJSON.url,

View File

@@ -0,0 +1,53 @@
const { getMonitorRelativeURL, UP } = require("../../src/util");
const { setting } = require("../util-server");
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Notifery extends NotificationProvider {
name = "notifery";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const url = "https://api.notifery.com/event";
let data = {
title: notification.notiferyTitle || "Uptime Kuma Alert",
message: msg,
};
if (notification.notiferyGroup) {
data.group = notification.notiferyGroup;
}
// Link to the monitor
const baseURL = await setting("primaryBaseURL");
if (baseURL && monitorJSON) {
data.message += `\n\nMonitor: ${baseURL}${getMonitorRelativeURL(monitorJSON.id)}`;
}
if (heartbeatJSON) {
data.code = heartbeatJSON.status === UP ? 0 : 1;
if (heartbeatJSON.ping) {
data.duration = heartbeatJSON.ping;
}
}
try {
const headers = {
"Content-Type": "application/json",
"x-api-key": notification.notiferyApiKey,
};
await axios.post(url, data, { headers });
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Notifery;

View File

@@ -0,0 +1,73 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class OneChat extends NotificationProvider {
name = "OneChat";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const url = "https://chat-api.one.th/message/api/v1/push_message";
try {
const config = {
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + notification.accessToken,
},
};
if (heartbeatJSON == null) {
const testMessage = {
to: notification.recieverId,
bot_id: notification.botId,
type: "text",
message: "Test Successful!",
};
await axios.post(url, testMessage, config);
} else if (heartbeatJSON["status"] === DOWN) {
const downMessage = {
to: notification.recieverId,
bot_id: notification.botId,
type: "text",
message:
`UptimeKuma Alert:
[🔴 Down]
Name: ${monitorJSON["name"]}
${heartbeatJSON["msg"]}
Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
};
await axios.post(url, downMessage, config);
} else if (heartbeatJSON["status"] === UP) {
const upMessage = {
to: notification.recieverId,
bot_id: notification.botId,
type: "text",
message:
`UptimeKuma Alert:
[🟢 Up]
Name: ${monitorJSON["name"]}
${heartbeatJSON["msg"]}
Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
};
await axios.post(url, upMessage, config);
}
return okMsg;
} catch (error) {
// Handle errors and throw a descriptive message
if (error.response) {
const errorMessage =
error.response.data?.message ||
"Unknown API error occurred.";
throw new Error(`OneChat API Error: ${errorMessage}`);
} else {
this.throwGeneralAxiosError(error);
}
}
}
}
module.exports = OneChat;

View File

@@ -0,0 +1,48 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { UP } = require("../../src/util");
class Pumble extends NotificationProvider {
name = "pumble";
/**
* @inheritDoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
if (heartbeatJSON === null && monitorJSON === null) {
let data = {
"attachments": [
{
"title": "Uptime Kuma Alert",
"text": msg,
"color": "#5BDD8B"
}
]
};
await axios.post(notification.webhookURL, data);
return okMsg;
}
let data = {
"attachments": [
{
"title": `${monitorJSON["name"]} is ${heartbeatJSON["status"] === UP ? "up" : "down"}`,
"text": heartbeatJSON["msg"],
"color": (heartbeatJSON["status"] === UP ? "#5BDD8B" : "#DC3645"),
}
]
};
await axios.post(notification.webhookURL, data);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Pumble;

View File

@@ -1,5 +1,6 @@
const { getMonitorRelativeURL } = require("../../src/util");
const { setting } = require("../util-server");
const { UP } = require("../../src/util");
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
@@ -43,15 +44,20 @@ class Pushover extends NotificationProvider {
if (heartbeatJSON == null) {
await axios.post(url, data);
return okMsg;
} else {
data.message += `\n<b>Time (${heartbeatJSON["timezone"]})</b>:${heartbeatJSON["localDateTime"]}`;
await axios.post(url, data);
return okMsg;
}
if (heartbeatJSON.status === UP && notification.pushoversounds_up) {
// default = DOWN => DOWN-sound is also played for non-UP/DOWN notiifcations
data.sound = notification.pushoversounds_up;
}
data.message += `\n<b>Time (${heartbeatJSON["timezone"]})</b>: ${heartbeatJSON["localDateTime"]}`;
await axios.post(url, data);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}

View File

@@ -145,6 +145,7 @@ class Slack extends NotificationProvider {
const title = "Uptime Kuma Alert";
let data = {
"text": msg,
"channel": notification.slackchannel,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,

View File

@@ -0,0 +1,40 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class SMSPlanet extends NotificationProvider {
name = "SMSPlanet";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const url = "https://api2.smsplanet.pl/sms";
try {
let config = {
headers: {
"Authorization": "Bearer " + notification.smsplanetApiToken,
"content-type": "multipart/form-data"
}
};
let data = {
"from": notification.smsplanetSenderName,
"to": notification.smsplanetPhoneNumbers,
"msg": msg.replace(/🔴/, "❌")
};
let response = await axios.post(url, data, config);
if (!response.data?.messageId) {
throw new Error(response.data?.errorMsg ?? "SMSPlanet server did not respond with the expected result");
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = SMSPlanet;

View File

@@ -11,59 +11,127 @@ class SMSEagle extends NotificationProvider {
const okMsg = "Sent Successfully.";
try {
let config = {
headers: {
"Content-Type": "application/json",
if (notification.smseagleApiType === "smseagle-apiv1") { // according to https://www.smseagle.eu/apiv1/
let config = {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
}
};
let sendMethod;
let recipientType;
let duration;
let voiceId;
if (notification.smseagleRecipientType === "smseagle-contact") {
recipientType = "contactname";
sendMethod = "/send_tocontact";
} else if (notification.smseagleRecipientType === "smseagle-group") {
recipientType = "groupname";
sendMethod = "/send_togroup";
} else if (notification.smseagleRecipientType === "smseagle-to") {
recipientType = "to";
sendMethod = "/send_sms";
if (notification.smseagleMsgType !== "smseagle-sms") {
duration = notification.smseagleDuration ?? 10;
if (notification.smseagleMsgType === "smseagle-ring") {
sendMethod = "/ring_call";
} else if (notification.smseagleMsgType === "smseagle-tts") {
sendMethod = "/tts_call";
} else if (notification.smseagleMsgType === "smseagle-tts-advanced") {
sendMethod = "/tts_adv_call";
voiceId = notification.smseagleTtsModel ? notification.smseagleTtsModel : 1;
}
}
}
};
let postData;
let sendMethod;
let recipientType;
const url = new URL(notification.smseagleUrl + "/http_api" + sendMethod);
let encoding = (notification.smseagleEncoding) ? "1" : "0";
let priority = (notification.smseaglePriority) ? notification.smseaglePriority : "0";
if (notification.smseagleRecipientType === "smseagle-contact") {
recipientType = "contactname";
sendMethod = "sms.send_tocontact";
}
if (notification.smseagleRecipientType === "smseagle-group") {
recipientType = "groupname";
sendMethod = "sms.send_togroup";
}
if (notification.smseagleRecipientType === "smseagle-to") {
recipientType = "to";
sendMethod = "sms.send_sms";
}
let params = {
access_token: notification.smseagleToken,
[recipientType]: notification.smseagleRecipient,
message: msg,
responsetype: "extended",
unicode: encoding,
highpriority: priority
};
postData = {
method: sendMethod,
params: params
};
let resp = await axios.post(notification.smseagleUrl + "/jsonrpc/sms", postData, config);
if ((JSON.stringify(resp.data)).indexOf("message_id") === -1) {
let error = "";
if (resp.data.result && resp.data.result.error_text) {
error = `SMSEagle API returned error: ${JSON.stringify(resp.data.result.error_text)}`;
url.searchParams.append("access_token", notification.smseagleToken);
url.searchParams.append(recipientType, notification.smseagleRecipient);
if (!notification.smseagleRecipientType || notification.smseagleRecipientType === "smseagle-sms") {
url.searchParams.append("unicode", (notification.smseagleEncoding) ? "1" : "0");
url.searchParams.append("highpriority", notification.smseaglePriority ?? "0");
} else {
error = "SMSEagle API returned an unexpected response";
url.searchParams.append("duration", duration);
}
if (notification.smseagleRecipientType !== "smseagle-ring") {
url.searchParams.append("message", msg);
}
if (voiceId) {
url.searchParams.append("voice_id", voiceId);
}
throw new Error(error);
}
return okMsg;
let resp = await axios.get(url.toString(), config);
if (resp.data.indexOf("OK") === -1) {
let error = `SMSEagle API returned error: ${resp.data}`;
throw new Error(error);
}
return okMsg;
} else if (notification.smseagleApiType === "smseagle-apiv2") { // according to https://www.smseagle.eu/docs/apiv2/
let config = {
headers: {
"access-token": notification.smseagleToken,
"Content-Type": "application/json",
}
};
let encoding = (notification.smseagleEncoding) ? "unicode" : "standard";
let priority = (notification.smseaglePriority) ?? 0;
let postData = {
text: msg,
encoding: encoding,
priority: priority
};
if (notification.smseagleRecipientContact) {
postData["contacts"] = notification.smseagleRecipientContact.split(",").map(Number);
}
if (notification.smseagleRecipientGroup) {
postData["groups"] = notification.smseagleRecipientGroup.split(",").map(Number);
}
if (notification.smseagleRecipientTo) {
postData["to"] = notification.smseagleRecipientTo.split(",");
}
let endpoint = "/messages/sms";
if (notification.smseagleMsgType !== "smseagle-sms") {
postData["duration"] = notification.smseagleDuration ?? 10;
if (notification.smseagleMsgType === "smseagle-ring") {
endpoint = "/calls/ring";
} else if (notification.smseagleMsgType === "smseagle-tts") {
endpoint = "/calls/tts";
} else if (notification.smseagleMsgType === "smseagle-tts-advanced") {
endpoint = "/calls/tts_advanced";
postData["voice_id"] = notification.smseagleTtsModel ?? 1;
}
}
let resp = await axios.post(notification.smseagleUrl + "/api/v2" + endpoint, postData, config);
const queuedCount = resp.data.filter(x => x.status === "queued").length;
const unqueuedCount = resp.data.length - queuedCount;
if (resp.status !== 200 || queuedCount === 0) {
if (!resp.data.length) {
throw new Error("SMSEagle API returned an empty response");
}
throw new Error(`SMSEagle API returned error: ${JSON.stringify(resp.data)}`);
}
if (unqueuedCount) {
return `Sent ${queuedCount}/${resp.data.length} Messages Successfully.`;
}
return okMsg;
}
} catch (error) {
this.throwGeneralAxiosError(error);
}

View File

@@ -42,6 +42,7 @@ class SMTP extends NotificationProvider {
// default values in case the user does not want to template
let subject = msg;
let body = msg;
let useHTMLBody = false;
if (heartbeatJSON) {
body = `${msg}\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`;
}
@@ -50,11 +51,11 @@ class SMTP extends NotificationProvider {
// cannot end with whitespace as this often raises spam scores
const customSubject = notification.customSubject?.trim() || "";
const customBody = notification.customBody?.trim() || "";
if (customSubject !== "") {
subject = await this.renderTemplate(customSubject, msg, monitorJSON, heartbeatJSON);
}
if (customBody !== "") {
useHTMLBody = notification.htmlBody || false;
body = await this.renderTemplate(customBody, msg, monitorJSON, heartbeatJSON);
}
}
@@ -67,7 +68,8 @@ class SMTP extends NotificationProvider {
bcc: notification.smtpBCC,
to: notification.smtpTo,
subject: subject,
text: body,
// If the email body is custom, and the user wants it, set the email body as HTML
[useHTMLBody ? "html" : "text"]: body
});
return okMsg;

View File

@@ -0,0 +1,37 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class SpugPush extends NotificationProvider {
name = "SpugPush";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
let formData = {
title: "Uptime Kuma Message",
content: msg
};
if (heartbeatJSON) {
if (heartbeatJSON["status"] === UP) {
formData.title = `UptimeKuma 「${monitorJSON["name"]}」 is Up`;
formData.content = `[✅ Up] ${heartbeatJSON["msg"]}`;
} else if (heartbeatJSON["status"] === DOWN) {
formData.title = `UptimeKuma 「${monitorJSON["name"]}」 is Down`;
formData.content = `[🔴 Down] ${heartbeatJSON["msg"]}`;
}
}
const apiUrl = `https://push.spug.cc/send/${notification.templateKey}`;
await axios.post(apiUrl, formData);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = SpugPush;

View File

@@ -13,6 +13,7 @@ const DingDing = require("./notification-providers/dingding");
const Discord = require("./notification-providers/discord");
const Elks = require("./notification-providers/46elks");
const Feishu = require("./notification-providers/feishu");
const Notifery = require("./notification-providers/notifery");
const FreeMobile = require("./notification-providers/freemobile");
const GoogleChat = require("./notification-providers/google-chat");
const Gorush = require("./notification-providers/gorush");
@@ -30,9 +31,11 @@ const Mattermost = require("./notification-providers/mattermost");
const Nostr = require("./notification-providers/nostr");
const Ntfy = require("./notification-providers/ntfy");
const Octopush = require("./notification-providers/octopush");
const OneChat = require("./notification-providers/onechat");
const OneBot = require("./notification-providers/onebot");
const Opsgenie = require("./notification-providers/opsgenie");
const PagerDuty = require("./notification-providers/pagerduty");
const Pumble = require("./notification-providers/pumble");
const FlashDuty = require("./notification-providers/flashduty");
const PagerTree = require("./notification-providers/pagertree");
const PromoSMS = require("./notification-providers/promosms");
@@ -72,6 +75,8 @@ const Onesender = require("./notification-providers/onesender");
const Wpush = require("./notification-providers/wpush");
const SendGrid = require("./notification-providers/send-grid");
const YZJ = require("./notification-providers/yzj");
const SMSPlanet = require("./notification-providers/sms-planet");
const SpugPush = require("./notification-providers/spugpush");
class Notification {
@@ -119,6 +124,7 @@ class Notification {
new Nostr(),
new Ntfy(),
new Octopush(),
new OneChat(),
new OneBot(),
new Onesender(),
new Opsgenie(),
@@ -126,6 +132,7 @@ class Notification {
new FlashDuty(),
new PagerTree(),
new PromoSMS(),
new Pumble(),
new Pushbullet(),
new PushDeer(),
new Pushover(),
@@ -160,7 +167,10 @@ class Notification {
new Cellsynt(),
new Wpush(),
new SendGrid(),
new YZJ()
new YZJ(),
new SMSPlanet(),
new SpugPush(),
new Notifery(),
];
for (let item of list) {
if (! item.name) {

View File

@@ -50,7 +50,7 @@ router.all("/api/push/:pushToken", async (request, response) => {
let msg = request.query.msg || "OK";
let ping = parseFloat(request.query.ping) || null;
let statusString = request.query.status || "up";
let status = (statusString === "up") ? UP : DOWN;
const statusFromParam = (statusString === "up") ? UP : DOWN;
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [
pushToken
@@ -80,7 +80,7 @@ router.all("/api/push/:pushToken", async (request, response) => {
msg = "Monitor under maintenance";
bean.status = MAINTENANCE;
} else {
determineStatus(status, previousHeartbeat, monitor.maxretries, monitor.isUpsideDown(), bean);
determineStatus(statusFromParam, previousHeartbeat, monitor.maxretries, monitor.isUpsideDown(), bean);
}
// Calculate uptime
@@ -92,21 +92,21 @@ router.all("/api/push/:pushToken", async (request, response) => {
log.debug("router", "PreviousStatus: " + previousHeartbeat?.status);
log.debug("router", "Current Status: " + bean.status);
bean.important = Monitor.isImportantBeat(isFirstBeat, previousHeartbeat?.status, status);
bean.important = Monitor.isImportantBeat(isFirstBeat, previousHeartbeat?.status, bean.status);
if (Monitor.isImportantForNotification(isFirstBeat, previousHeartbeat?.status, status)) {
if (Monitor.isImportantForNotification(isFirstBeat, previousHeartbeat?.status, bean.status)) {
// Reset down count
bean.downCount = 0;
log.debug("monitor", `[${this.name}] sendNotification`);
log.debug("monitor", `[${monitor.name}] sendNotification`);
await Monitor.sendNotification(isFirstBeat, monitor, bean);
} else {
if (bean.status === DOWN && this.resendInterval > 0) {
if (bean.status === DOWN && monitor.resendInterval > 0) {
++bean.downCount;
if (bean.downCount >= this.resendInterval) {
if (bean.downCount >= monitor.resendInterval) {
// Send notification again, because we are still DOWN
log.debug("monitor", `[${this.name}] sendNotification again: Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
await Monitor.sendNotification(isFirstBeat, this, bean);
log.debug("monitor", `[${monitor.name}] sendNotification again: Down Count: ${bean.downCount} | Resend Interval: ${monitor.resendInterval}`);
await Monitor.sendNotification(isFirstBeat, monitor, bean);
// Reset down count
bean.downCount = 0;

View File

@@ -866,6 +866,7 @@ let needSetup = false;
monitor.kafkaProducerAllowAutoTopicCreation;
bean.gamedigGivenPortOnly = monitor.gamedigGivenPortOnly;
bean.remote_browser = monitor.remote_browser;
bean.smtpSecurity = monitor.smtpSecurity;
bean.snmpVersion = monitor.snmpVersion;
bean.snmpOid = monitor.snmpOid;
bean.jsonPathOperator = monitor.jsonPathOperator;
@@ -875,6 +876,11 @@ let needSetup = false;
bean.rabbitmqPassword = monitor.rabbitmqPassword;
bean.conditions = JSON.stringify(monitor.conditions);
// ping advanced options
bean.ping_numeric = monitor.ping_numeric;
bean.ping_count = monitor.ping_count;
bean.ping_per_request_timeout = monitor.ping_per_request_timeout;
bean.validate();
await R.store(bean);

View File

@@ -211,6 +211,10 @@ module.exports.statusPageSocketHandler = (socket) => {
relationBean.send_url = monitor.sendUrl;
}
if (monitor.url !== undefined) {
relationBean.custom_url = monitor.url;
}
await R.store(relationBean);
}

View File

@@ -113,6 +113,7 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType();
UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
UptimeKumaServer.monitorTypeList["smtp"] = new SMTPMonitorType();
UptimeKumaServer.monitorTypeList["group"] = new GroupMonitorType();
UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType();
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
@@ -552,6 +553,7 @@ const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor
const { TailscalePing } = require("./monitor-types/tailscale-ping");
const { DnsMonitorType } = require("./monitor-types/dns");
const { MqttMonitorType } = require("./monitor-types/mqtt");
const { SMTPMonitorType } = require("./monitor-types/smtp");
const { GroupMonitorType } = require("./monitor-types/group");
const { SNMPMonitorType } = require("./monitor-types/snmp");
const { MongodbMonitorType } = require("./monitor-types/mongodb");

View File

@@ -1,7 +1,11 @@
const tcpp = require("tcp-ping");
const ping = require("@louislam/ping");
const { R } = require("redbean-node");
const { log, genSecret, badgeConstants } = require("../src/util");
const {
log, genSecret, badgeConstants,
PING_PACKET_SIZE_DEFAULT, PING_GLOBAL_TIMEOUT_DEFAULT,
PING_COUNT_DEFAULT, PING_PER_REQUEST_TIMEOUT_DEFAULT
} = require("../src/util");
const passwordHash = require("./password-hash");
const { Resolver } = require("dns");
const iconv = require("iconv-lite");
@@ -118,20 +122,33 @@ exports.tcping = function (hostname, port) {
/**
* Ping the specified machine
* @param {string} hostname Hostname / address of machine
* @param {number} size Size of packet to send
* @param {string} destAddr Hostname / IP address of machine to ping
* @param {number} count Number of packets to send before stopping
* @param {string} sourceAddr Source address for sending/receiving echo requests
* @param {boolean} numeric If true, IP addresses will be output instead of symbolic hostnames
* @param {number} size Size (in bytes) of echo request to send
* @param {number} deadline Maximum time in seconds before ping stops, regardless of packets sent
* @param {number} timeout Maximum time in seconds to wait for each response
* @returns {Promise<number>} Time for ping in ms rounded to nearest integer
*/
exports.ping = async (hostname, size = 56) => {
exports.ping = async (
destAddr,
count = PING_COUNT_DEFAULT,
sourceAddr = "",
numeric = true,
size = PING_PACKET_SIZE_DEFAULT,
deadline = PING_GLOBAL_TIMEOUT_DEFAULT,
timeout = PING_PER_REQUEST_TIMEOUT_DEFAULT,
) => {
try {
return await exports.pingAsync(hostname, false, size);
return await exports.pingAsync(destAddr, false, count, sourceAddr, numeric, size, deadline, timeout);
} catch (e) {
// If the host cannot be resolved, try again with ipv6
log.debug("ping", "IPv6 error message: " + e.message);
// As node-ping does not report a specific error for this, try again if it is an empty message with ipv6 no matter what.
if (!e.message) {
return await exports.pingAsync(hostname, true, size);
return await exports.pingAsync(destAddr, true, count, sourceAddr, numeric, size, deadline, timeout);
} else {
throw e;
}
@@ -140,18 +157,35 @@ exports.ping = async (hostname, size = 56) => {
/**
* Ping the specified machine
* @param {string} hostname Hostname / address of machine to ping
* @param {string} destAddr Hostname / IP address of machine to ping
* @param {boolean} ipv6 Should IPv6 be used?
* @param {number} size Size of ping packet to send
* @param {number} count Number of packets to send before stopping
* @param {string} sourceAddr Source address for sending/receiving echo requests
* @param {boolean} numeric If true, IP addresses will be output instead of symbolic hostnames
* @param {number} size Size (in bytes) of echo request to send
* @param {number} deadline Maximum time in seconds before ping stops, regardless of packets sent
* @param {number} timeout Maximum time in seconds to wait for each response
* @returns {Promise<number>} Time for ping in ms rounded to nearest integer
*/
exports.pingAsync = function (hostname, ipv6 = false, size = 56) {
exports.pingAsync = function (
destAddr,
ipv6 = false,
count = PING_COUNT_DEFAULT,
sourceAddr = "",
numeric = true,
size = PING_PACKET_SIZE_DEFAULT,
deadline = PING_GLOBAL_TIMEOUT_DEFAULT,
timeout = PING_PER_REQUEST_TIMEOUT_DEFAULT,
) {
return new Promise((resolve, reject) => {
ping.promise.probe(hostname, {
ping.promise.probe(destAddr, {
v6: ipv6,
min_reply: 1,
deadline: 10,
min_reply: count,
sourceAddr: sourceAddr,
numeric: numeric,
packetSize: size,
deadline: deadline,
timeout: timeout
}).then((res) => {
// If ping failed, it will set field to unknown
if (res.alive) {