diff --git a/package-lock.json b/package-lock.json
index 296362633..31eeb4c6e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,6 +19,7 @@
"axios": "~0.21.4",
"bcryptjs": "~2.4.3",
"bootstrap": "~5.1.1",
+ "chardet": "^1.3.0",
"chart.js": "~3.5.1",
"chartjs-adapter-dayjs": "~1.0.0",
"command-exists": "~1.2.9",
@@ -28,6 +29,7 @@
"express-basic-auth": "~1.2.0",
"form-data": "~4.0.0",
"http-graceful-shutdown": "~3.1.4",
+ "iconv-lite": "^0.6.3",
"jsonwebtoken": "~8.5.1",
"nodemailer": "~6.6.5",
"notp": "~2.0.3",
@@ -64,7 +66,7 @@
"@vitejs/plugin-legacy": "~1.6.1",
"@vitejs/plugin-vue": "~1.9.2",
"@vue/compiler-sfc": "~3.2.19",
- "babel-plugin-rewire": "^1.2.0",
+ "babel-plugin-rewire": "~1.2.0",
"core-js": "~3.18.1",
"cross-env": "~7.0.3",
"dns2": "~2.0.1",
@@ -3551,6 +3553,17 @@
"ms": "2.0.0"
}
},
+ "node_modules/body-parser/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/body-parser/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -3813,6 +3826,11 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/chardet": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-1.3.0.tgz",
+ "integrity": "sha512-cyTQGGptIjIT+CMGT5J/0l9c6Fb+565GCFjjeUTKxUO7w3oR+FcNCMEKTn5xtVKaLFmladN7QF68IiQsv5Fbdw=="
+ },
"node_modules/chart.js": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.1.tgz",
@@ -6303,11 +6321,11 @@
}
},
"node_modules/iconv-lite": {
- "version": "0.4.24",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dependencies": {
- "safer-buffer": ">= 2.1.2 < 3"
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
@@ -9527,6 +9545,17 @@
"node": ">= 0.8"
}
},
+ "node_modules/raw-body/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@@ -11978,6 +12007,18 @@
"iconv-lite": "0.4.24"
}
},
+ "node_modules/whatwg-encoding/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/whatwg-mimetype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz",
@@ -14905,6 +14946,14 @@
"ms": "2.0.0"
}
},
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -15090,6 +15139,11 @@
"integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==",
"dev": true
},
+ "chardet": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-1.3.0.tgz",
+ "integrity": "sha512-cyTQGGptIjIT+CMGT5J/0l9c6Fb+565GCFjjeUTKxUO7w3oR+FcNCMEKTn5xtVKaLFmladN7QF68IiQsv5Fbdw=="
+ },
"chart.js": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.1.tgz",
@@ -16967,11 +17021,11 @@
"dev": true
},
"iconv-lite": {
- "version": "0.4.24",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"requires": {
- "safer-buffer": ">= 2.1.2 < 3"
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
}
},
"ieee754": {
@@ -19393,6 +19447,16 @@
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
+ },
+ "dependencies": {
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ }
}
},
"react-is": {
@@ -21251,6 +21315,17 @@
"dev": true,
"requires": {
"iconv-lite": "0.4.24"
+ },
+ "dependencies": {
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ }
}
},
"whatwg-mimetype": {
diff --git a/package.json b/package.json
index 03112518a..d117e7f2b 100644
--- a/package.json
+++ b/package.json
@@ -60,9 +60,9 @@
"@popperjs/core": "~2.10.2",
"args-parser": "~1.3.0",
"axios": "~0.21.4",
- "babel-plugin-rewire": "~1.2.0",
"bcryptjs": "~2.4.3",
"bootstrap": "~5.1.1",
+ "chardet": "^1.3.0",
"chart.js": "~3.5.1",
"chartjs-adapter-dayjs": "~1.0.0",
"command-exists": "~1.2.9",
@@ -72,6 +72,7 @@
"express-basic-auth": "~1.2.0",
"form-data": "~4.0.0",
"http-graceful-shutdown": "~3.1.4",
+ "iconv-lite": "^0.6.3",
"jsonwebtoken": "~8.5.1",
"nodemailer": "~6.6.5",
"notp": "~2.0.3",
@@ -108,6 +109,7 @@
"@vitejs/plugin-legacy": "~1.6.1",
"@vitejs/plugin-vue": "~1.9.2",
"@vue/compiler-sfc": "~3.2.19",
+ "babel-plugin-rewire": "~1.2.0",
"core-js": "~3.18.1",
"cross-env": "~7.0.3",
"dns2": "~2.0.1",
diff --git a/server/model/monitor.js b/server/model/monitor.js
index 161815277..9fe6ffd43 100644
--- a/server/model/monitor.js
+++ b/server/model/monitor.js
@@ -292,54 +292,13 @@ class Monitor extends BeanModel {
let beatInterval = this.interval;
- // * ? -> ANY STATUS = important [isFirstBeat]
- // UP -> PENDING = not important
- // * UP -> DOWN = important
- // UP -> UP = not important
- // PENDING -> PENDING = not important
- // * PENDING -> DOWN = important
- // PENDING -> UP = not important
- // DOWN -> PENDING = this case not exists
- // DOWN -> DOWN = not important
- // * DOWN -> UP = important
- let isImportant = isFirstBeat ||
- (previousBeat.status === UP && bean.status === DOWN) ||
- (previousBeat.status === DOWN && bean.status === UP) ||
- (previousBeat.status === PENDING && bean.status === DOWN);
+ let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat.status, bean.status);
// Mark as important if status changed, ignore pending pings,
// Don't notify if disrupted changes to up
if (isImportant) {
bean.important = true;
-
- // Send only if the first beat is DOWN
- if (!isFirstBeat || bean.status === DOWN) {
- let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
- this.id,
- ]);
-
- let text;
- if (bean.status === UP) {
- text = "✅ Up";
- } else {
- text = "🔴 Down";
- }
-
- let msg = `[${this.name}] [${text}] ${bean.msg}`;
-
- for (let notification of notificationList) {
- try {
- await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON());
- } catch (e) {
- console.error("Cannot send notification to " + notification.name);
- console.log(e);
- }
- }
-
- // Clear Status Page Cache
- apicache.clear();
- }
-
+ await Monitor.sendNotification(isFirstBeat, this, bean);
} else {
bean.important = false;
}
@@ -546,6 +505,53 @@ class Monitor extends BeanModel {
io.to(userID).emit("uptime", monitorID, duration, uptime);
}
+ static isImportantBeat(isFirstBeat, previousBeatStatus, currentBeatStatus) {
+ // * ? -> ANY STATUS = important [isFirstBeat]
+ // UP -> PENDING = not important
+ // * UP -> DOWN = important
+ // UP -> UP = not important
+ // PENDING -> PENDING = not important
+ // * PENDING -> DOWN = important
+ // PENDING -> UP = not important
+ // DOWN -> PENDING = this case not exists
+ // DOWN -> DOWN = not important
+ // * DOWN -> UP = important
+ let isImportant = isFirstBeat ||
+ (previousBeatStatus === UP && currentBeatStatus === DOWN) ||
+ (previousBeatStatus === DOWN && currentBeatStatus === UP) ||
+ (previousBeatStatus === PENDING && currentBeatStatus === DOWN);
+ return isImportant;
+ }
+
+ static async sendNotification(isFirstBeat, monitor, bean) {
+ if (!isFirstBeat || bean.status === DOWN) {
+ let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
+ monitor.id,
+ ]);
+
+ let text;
+ if (bean.status === UP) {
+ text = "✅ Up";
+ } else {
+ text = "🔴 Down";
+ }
+
+ let msg = `[${monitor.name}] [${text}] ${bean.msg}`;
+
+ for (let notification of notificationList) {
+ try {
+ await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(), bean.toJSON());
+ } catch (e) {
+ console.error("Cannot send notification to " + notification.name);
+ console.log(e);
+ }
+ }
+
+ // Clear Status Page Cache
+ apicache.clear();
+ }
+ }
+
}
module.exports = Monitor;
diff --git a/server/notification-providers/aliyun-sms.js b/server/notification-providers/aliyun-sms.js
new file mode 100644
index 000000000..6a2063200
--- /dev/null
+++ b/server/notification-providers/aliyun-sms.js
@@ -0,0 +1,108 @@
+const NotificationProvider = require("./notification-provider");
+const { DOWN, UP } = require("../../src/util");
+const { default: axios } = require("axios");
+const Crypto = require("crypto");
+const qs = require("qs");
+
+class AliyunSMS extends NotificationProvider {
+ name = "AliyunSMS";
+
+ async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
+ let okMsg = "Sent Successfully.";
+
+ try {
+ if (heartbeatJSON != null) {
+ let msgBody = JSON.stringify({
+ name: monitorJSON["name"],
+ time: heartbeatJSON["time"],
+ status: this.statusToString(heartbeatJSON["status"]),
+ msg: heartbeatJSON["msg"],
+ });
+ if (this.sendSms(notification, msgBody)) {
+ return okMsg;
+ }
+ } else {
+ let msgBody = JSON.stringify({
+ name: "",
+ time: "",
+ status: "",
+ msg: msg,
+ });
+ if (this.sendSms(notification, msgBody)) {
+ return okMsg;
+ }
+ }
+ } catch (error) {
+ this.throwGeneralAxiosError(error);
+ }
+ }
+
+ async sendSms(notification, msgbody) {
+ let params = {
+ PhoneNumbers: notification.phonenumber,
+ TemplateCode: notification.templateCode,
+ SignName: notification.signName,
+ TemplateParam: msgbody,
+ AccessKeyId: notification.accessKeyId,
+ Format: "JSON",
+ SignatureMethod: "HMAC-SHA1",
+ SignatureVersion: "1.0",
+ SignatureNonce: Math.random().toString(),
+ Timestamp: new Date().toISOString(),
+ Action: "SendSms",
+ Version: "2017-05-25",
+ };
+
+ params.Signature = this.sign(params, notification.secretAccessKey);
+ let config = {
+ method: "POST",
+ url: "http://dysmsapi.aliyuncs.com/",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ data: qs.stringify(params),
+ };
+
+ let result = await axios(config);
+ if (result.data.Message == "OK") {
+ return true;
+ }
+ return false;
+ }
+
+ /** Aliyun request sign */
+ sign(param, AccessKeySecret) {
+ let param2 = {};
+ let data = [];
+
+ let oa = Object.keys(param).sort();
+
+ for (let i = 0; i < oa.length; i++) {
+ let key = oa[i];
+ param2[key] = param[key];
+ }
+
+ for (let key in param2) {
+ data.push(`${encodeURIComponent(key)}=${encodeURIComponent(param2[key])}`);
+ }
+
+ let StringToSign = `POST&${encodeURIComponent("/")}&${encodeURIComponent(data.join("&"))}`;
+ return Crypto
+ .createHmac("sha1", `${AccessKeySecret}&`)
+ .update(Buffer.from(StringToSign))
+ .digest("base64");
+ }
+
+ statusToString(status) {
+ switch (status) {
+ case DOWN:
+ return "DOWN";
+ case UP:
+ return "UP";
+ default:
+ return status;
+ }
+ }
+}
+
+module.exports = AliyunSMS;
diff --git a/server/notification-providers/dingding.js b/server/notification-providers/dingding.js
new file mode 100644
index 000000000..f099192d8
--- /dev/null
+++ b/server/notification-providers/dingding.js
@@ -0,0 +1,79 @@
+const NotificationProvider = require("./notification-provider");
+const { DOWN, UP } = require("../../src/util");
+const { default: axios } = require("axios");
+const Crypto = require("crypto");
+
+class DingDing extends NotificationProvider {
+ name = "DingDing";
+
+ async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
+ let okMsg = "Sent Successfully.";
+
+ try {
+ if (heartbeatJSON != null) {
+ let params = {
+ msgtype: "markdown",
+ markdown: {
+ title: monitorJSON["name"],
+ text: `## [${this.statusToString(heartbeatJSON["status"])}] \n > ${heartbeatJSON["msg"]} \n > Time(UTC):${heartbeatJSON["time"]}`,
+ }
+ };
+ if (this.sendToDingDing(notification, params)) {
+ return okMsg;
+ }
+ } else {
+ let params = {
+ msgtype: "text",
+ text: {
+ content: msg
+ }
+ };
+ if (this.sendToDingDing(notification, params)) {
+ return okMsg;
+ }
+ }
+ } catch (error) {
+ this.throwGeneralAxiosError(error);
+ }
+ }
+
+ async sendToDingDing(notification, params) {
+ let timestamp = Date.now();
+
+ let config = {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ url: `${notification.webHookUrl}×tamp=${timestamp}&sign=${encodeURIComponent(this.sign(timestamp, notification.secretKey))}`,
+ data: JSON.stringify(params),
+ };
+
+ let result = await axios(config);
+ if (result.data.errmsg == "ok") {
+ return true;
+ }
+ return false;
+ }
+
+ /** DingDing sign */
+ sign(timestamp, secretKey) {
+ return Crypto
+ .createHmac("sha256", Buffer.from(secretKey, "utf8"))
+ .update(Buffer.from(`${timestamp}\n${secretKey}`, "utf8"))
+ .digest("base64");
+ }
+
+ statusToString(status) {
+ switch (status) {
+ case DOWN:
+ return "DOWN";
+ case UP:
+ return "UP";
+ default:
+ return status;
+ }
+ }
+}
+
+module.exports = DingDing;
diff --git a/server/notification-providers/slack.js b/server/notification-providers/slack.js
index 5132ba977..b4dad6fe3 100644
--- a/server/notification-providers/slack.js
+++ b/server/notification-providers/slack.js
@@ -39,8 +39,9 @@ class Slack extends NotificationProvider {
}
const time = heartbeatJSON["time"];
+ const textMsg = "Uptime Kuma Alert";
let data = {
- "text": "Uptime Kuma Alert",
+ "text": monitorJSON ? textMsg + `: ${monitorJSON.name}` : textMsg,
"channel": notification.slackchannel,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
diff --git a/server/notification-providers/smtp.js b/server/notification-providers/smtp.js
index ecb583eb7..60068eb77 100644
--- a/server/notification-providers/smtp.js
+++ b/server/notification-providers/smtp.js
@@ -1,5 +1,6 @@
const nodemailer = require("nodemailer");
const NotificationProvider = require("./notification-provider");
+const { DOWN, UP } = require("../../src/util");
class SMTP extends NotificationProvider {
@@ -20,6 +21,56 @@ class SMTP extends NotificationProvider {
pass: notification.smtpPassword,
};
}
+ // Lets start with default subject and empty string for custom one
+ let subject = msg;
+
+ // Change the subject if:
+ // - The msg ends with "Testing" or
+ // - Actual Up/Down Notification
+ if ((monitorJSON && heartbeatJSON) || msg.endsWith("Testing")) {
+ let customSubject = "";
+
+ // Our subject cannot end with whitespace it's often raise spam score
+ // Once I got "Cannot read property 'trim' of undefined", better be safe than sorry
+ if (notification.customSubject) {
+ customSubject = notification.customSubject.trim();
+ }
+
+ // If custom subject is not empty, change subject for notification
+ if (customSubject !== "") {
+
+ // Replace "MACROS" with corresponding variable
+ let replaceName = new RegExp("{{NAME}}", "g");
+ let replaceHostnameOrURL = new RegExp("{{HOSTNAME_OR_URL}}", "g");
+ let replaceStatus = new RegExp("{{STATUS}}", "g");
+
+ // Lets start with dummy values to simplify code
+ let monitorName = "Test";
+ let monitorHostnameOrURL = "testing.hostname";
+ let serviceStatus = "⚠️ Test";
+
+ if (monitorJSON !== null) {
+ monitorName = monitorJSON["name"];
+
+ if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword") {
+ monitorHostnameOrURL = monitorJSON["url"];
+ } else {
+ monitorHostnameOrURL = monitorJSON["hostname"];
+ }
+ }
+
+ if (heartbeatJSON !== null) {
+ serviceStatus = (heartbeatJSON["status"] === DOWN) ? "🔴 Down" : "✅ Up";
+ }
+
+ // Break replace to one by line for better readability
+ customSubject = customSubject.replace(replaceStatus, serviceStatus);
+ customSubject = customSubject.replace(replaceName, monitorName);
+ customSubject = customSubject.replace(replaceHostnameOrURL, monitorHostnameOrURL);
+
+ subject = customSubject;
+ }
+ }
let transporter = nodemailer.createTransport(config);
@@ -34,7 +85,7 @@ class SMTP extends NotificationProvider {
cc: notification.smtpCC,
bcc: notification.smtpBCC,
to: notification.smtpTo,
- subject: msg,
+ subject: subject,
text: bodyTextContent,
tls: {
rejectUnauthorized: notification.smtpIgnoreTLSError || false,
diff --git a/server/notification.js b/server/notification.js
index 41a0063c3..658216f91 100644
--- a/server/notification.js
+++ b/server/notification.js
@@ -19,6 +19,8 @@ const Teams = require("./notification-providers/teams");
const Telegram = require("./notification-providers/telegram");
const Webhook = require("./notification-providers/webhook");
const Feishu = require("./notification-providers/feishu");
+const AliyunSms = require("./notification-providers/aliyun-sms");
+const DingDing = require("./notification-providers/dingding");
class Notification {
@@ -31,6 +33,8 @@ class Notification {
const list = [
new Apprise(),
+ new AliyunSms(),
+ new DingDing(),
new Discord(),
new Teams(),
new Gotify(),
diff --git a/server/ping-lite.js b/server/ping-lite.js
index 0af0e9706..b2d6405ad 100644
--- a/server/ping-lite.js
+++ b/server/ping-lite.js
@@ -4,10 +4,7 @@ const net = require("net");
const spawn = require("child_process").spawn;
const events = require("events");
const fs = require("fs");
-const WIN = /^win/.test(process.platform);
-const LIN = /^linux/.test(process.platform);
-const MAC = /^darwin/.test(process.platform);
-const FBSD = /^freebsd/.test(process.platform);
+const util = require("./util-server");
module.exports = Ping;
@@ -23,12 +20,12 @@ function Ping(host, options) {
const timeout = 10;
- if (WIN) {
+ if (util.WIN) {
this._bin = "c:/windows/system32/ping.exe";
this._args = (options.args) ? options.args : [ "-n", "1", "-w", timeout * 1000, host ];
this._regmatch = /[><=]([0-9.]+?)ms/;
- } else if (LIN) {
+ } else if (util.LIN) {
this._bin = "/bin/ping";
const defaultArgs = [ "-n", "-w", timeout, "-c", "1", host ];
@@ -40,7 +37,7 @@ function Ping(host, options) {
this._args = (options.args) ? options.args : defaultArgs;
this._regmatch = /=([0-9.]+?) ms/;
- } else if (MAC) {
+ } else if (util.MAC) {
if (net.isIPv6(host) || options.ipv6) {
this._bin = "/sbin/ping6";
@@ -51,7 +48,7 @@ function Ping(host, options) {
this._args = (options.args) ? options.args : [ "-n", "-t", timeout, "-c", "1", host ];
this._regmatch = /=([0-9.]+?) ms/;
- } else if (FBSD) {
+ } else if (util.FBSD) {
this._bin = "/sbin/ping";
const defaultArgs = [ "-n", "-t", timeout, "-c", "1", host ];
@@ -101,6 +98,9 @@ Ping.prototype.send = function (callback) {
});
this._ping.stdout.on("data", function (data) { // log stdout
+ if (util.WIN) {
+ data = convertOutput(data);
+ }
this._stdout = (this._stdout || "") + data;
});
@@ -112,6 +112,9 @@ Ping.prototype.send = function (callback) {
});
this._ping.stderr.on("data", function (data) { // log stderr
+ if (util.WIN) {
+ data = convertOutput(data);
+ }
this._stderr = (this._stderr || "") + data;
});
@@ -157,3 +160,19 @@ Ping.prototype.start = function (callback) {
Ping.prototype.stop = function () {
clearInterval(this._i);
};
+
+/**
+ * Try to convert to UTF-8 for Windows, as the ping's output on Windows is not UTF-8 and could be in other languages
+ * Thank @pemassi
+ * https://github.com/louislam/uptime-kuma/issues/570#issuecomment-941984094
+ * @param data
+ * @returns {string}
+ */
+function convertOutput(data) {
+ if (util.WIN) {
+ if (data) {
+ return util.convertToUTF8(data);
+ }
+ }
+ return data;
+}
diff --git a/server/routers/api-router.js b/server/routers/api-router.js
index 0da1fd705..fbe8136e5 100644
--- a/server/routers/api-router.js
+++ b/server/routers/api-router.js
@@ -5,7 +5,7 @@ const server = require("../server");
const apicache = require("../modules/apicache");
const Monitor = require("../model/monitor");
const dayjs = require("dayjs");
-const { UP } = require("../../src/util");
+const { UP, flipStatus, debug } = require("../../src/util");
let router = express.Router();
let cache = apicache.middleware;
@@ -18,9 +18,10 @@ router.get("/api/entry-page", async (_, response) => {
router.get("/api/push/:pushToken", async (request, response) => {
try {
+
let pushToken = request.params.pushToken;
let msg = request.query.msg || "OK";
- let ping = request.query.ping;
+ let ping = request.query.ping || null;
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [
pushToken
@@ -30,12 +31,40 @@ router.get("/api/push/:pushToken", async (request, response) => {
throw new Error("Monitor not found or not active.");
}
+ const previousHeartbeat = await R.getRow(`
+ SELECT status, time FROM heartbeat
+ WHERE id = (select MAX(id) from heartbeat where monitor_id = ?)
+ `, [
+ monitor.id
+ ]);
+
+ let status = UP;
+ if (monitor.isUpsideDown()) {
+ status = flipStatus(status);
+ }
+
+ let isFirstBeat = true;
+ let previousStatus = status;
+ let duration = 0;
+
let bean = R.dispense("heartbeat");
- bean.monitor_id = monitor.id;
bean.time = R.isoDateTime(dayjs.utc());
- bean.status = UP;
+
+ if (previousHeartbeat) {
+ isFirstBeat = false;
+ previousStatus = previousHeartbeat.status;
+ duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
+ }
+
+ debug("PreviousStatus: " + previousStatus);
+ debug("Current Status: " + status);
+
+ bean.important = Monitor.isImportantBeat(isFirstBeat, previousStatus, status);
+ bean.monitor_id = monitor.id;
+ bean.status = status;
bean.msg = msg;
bean.ping = ping;
+ bean.duration = duration;
await R.store(bean);
@@ -45,6 +74,11 @@ router.get("/api/push/:pushToken", async (request, response) => {
response.json({
ok: true,
});
+
+ if (bean.important) {
+ await Monitor.sendNotification(isFirstBeat, monitor, bean);
+ }
+
} catch (e) {
response.json({
ok: false,
diff --git a/server/server.js b/server/server.js
index 67095ff53..376e7bb3f 100644
--- a/server/server.js
+++ b/server/server.js
@@ -43,7 +43,7 @@ console.log("Importing this project modules");
debug("Importing Monitor");
const Monitor = require("./model/monitor");
debug("Importing Settings");
-const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest } = require("./util-server");
+const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD } = require("./util-server");
debug("Importing Notification");
const { Notification } = require("./notification");
@@ -61,12 +61,22 @@ console.info("Version: " + checkVersion.version);
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
// Dual-stack support for (::)
-const hostname = process.env.HOST || args.host;
-const port = parseInt(process.env.PORT || args.port || 3001);
+let hostname = process.env.UPTIME_KUMA_HOST || args.host;
+
+// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
+if (!hostname && !FBSD) {
+ hostname = process.env.HOST;
+}
+
+if (hostname) {
+ console.log("Custom hostname: " + hostname);
+}
+
+const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || args.port || 3001);
// SSL
-const sslKey = process.env.SSL_KEY || args["ssl-key"] || undefined;
-const sslCert = process.env.SSL_CERT || args["ssl-cert"] || undefined;
+const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || args["ssl-key"] || undefined;
+const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || args["ssl-cert"] || undefined;
/**
* Run unit test after the server is ready
diff --git a/server/util-server.js b/server/util-server.js
index 5620d674b..a65af74a4 100644
--- a/server/util-server.js
+++ b/server/util-server.js
@@ -6,6 +6,14 @@ const passwordHash = require("./password-hash");
const dayjs = require("dayjs");
const { Resolver } = require("dns");
const child_process = require("child_process");
+const iconv = require("iconv-lite");
+const chardet = require("chardet");
+
+// From ping-lite
+exports.WIN = /^win/.test(process.platform);
+exports.LIN = /^linux/.test(process.platform);
+exports.MAC = /^darwin/.test(process.platform);
+exports.FBSD = /^freebsd/.test(process.platform);
/**
* Init or reset JWT secret
@@ -312,3 +320,14 @@ exports.startUnitTest = async () => {
process.exit(code);
});
};
+
+/**
+ * @param body : Buffer
+ * @returns {string}
+ */
+exports.convertToUTF8 = (body) => {
+ const guessEncoding = chardet.detect(body);
+ debug("Guess Encoding: " + guessEncoding);
+ const str = iconv.decode(body, guessEncoding);
+ return str.toString();
+};
diff --git a/src/components/notifications/AliyunSms.vue b/src/components/notifications/AliyunSms.vue
new file mode 100644
index 000000000..2c25a3a9c
--- /dev/null
+++ b/src/components/notifications/AliyunSms.vue
@@ -0,0 +1,25 @@
+
+ Sms template must contain parameters: For safety, must use secret key
${name} ${time} ${status} ${msg}
*{{ $t("Required") }}