Merge remote-tracking branch 'origin/master' into mongodb-ping

# Conflicts:
#	server/model/monitor.js
#	server/util-server.js
#	src/pages/EditMonitor.vue
This commit is contained in:
Louis Lam
2023-01-15 01:13:11 +08:00
92 changed files with 6445 additions and 2124 deletions

View File

@@ -1,6 +1,8 @@
const https = require("https");
const http = require("http");
const CacheableLookup = require("cacheable-lookup");
const { Settings } = require("./settings");
const { log } = require("../src/util");
class CacheableDnsHttpAgent {
@@ -9,12 +11,30 @@ class CacheableDnsHttpAgent {
static httpAgentList = {};
static httpsAgentList = {};
static enable = false;
/**
* Register cacheable to global agents
* Register/Disable cacheable to global agents
*/
static registerGlobalAgent() {
this.cacheable.install(http.globalAgent);
this.cacheable.install(https.globalAgent);
static async update() {
log.debug("CacheableDnsHttpAgent", "update");
let isEnable = await Settings.get("dnsCache");
if (isEnable !== this.enable) {
log.debug("CacheableDnsHttpAgent", "value changed");
if (isEnable) {
log.debug("CacheableDnsHttpAgent", "enable");
this.cacheable.install(http.globalAgent);
this.cacheable.install(https.globalAgent);
} else {
log.debug("CacheableDnsHttpAgent", "disable");
this.cacheable.uninstall(http.globalAgent);
this.cacheable.uninstall(https.globalAgent);
}
}
this.enable = isEnable;
}
static install(agent) {
@@ -26,6 +46,10 @@ class CacheableDnsHttpAgent {
* @return {https.Agent}
*/
static getHttpsAgent(agentOptions) {
if (!this.enable) {
return new https.Agent(agentOptions);
}
let key = JSON.stringify(agentOptions);
if (!(key in this.httpsAgentList)) {
this.httpsAgentList[key] = new https.Agent(agentOptions);
@@ -39,6 +63,10 @@ class CacheableDnsHttpAgent {
* @return {https.Agents}
*/
static getHttpAgent(agentOptions) {
if (!this.enable) {
return new http.Agent(agentOptions);
}
let key = JSON.stringify(agentOptions);
if (!(key in this.httpAgentList)) {
this.httpAgentList[key] = new http.Agent(agentOptions);

View File

@@ -25,7 +25,7 @@ exports.startInterval = () => {
let checkBeta = await setting("checkBeta");
if (checkBeta && res.data.beta) {
if (compareVersions.compare(res.data.beta, res.data.beta, ">")) {
if (compareVersions.compare(res.data.beta, res.data.slow, ">")) {
exports.latestVersion = res.data.beta;
return;
}

View File

@@ -152,9 +152,6 @@ class Database {
await R.exec("PRAGMA cache_size = -12000");
await R.exec("PRAGMA auto_vacuum = FULL");
// Avoid error "SQLITE_BUSY: database is locked" by allowing SQLITE to wait up to 5 seconds to do a write
await R.exec("PRAGMA busy_timeout = 5000");
// This ensures that an operating system crash or power failure will not corrupt the database.
// FULL synchronous is very safe, but it is also slower.
// Read more: https://sqlite.org/pragma.html#pragma_synchronous

View File

@@ -25,15 +25,20 @@ const DEFAULT_KEEP_PERIOD = 180;
parsedPeriod = DEFAULT_KEEP_PERIOD;
}
log(`Clearing Data older than ${parsedPeriod} days...`);
if (parsedPeriod < 1) {
log(`Data deletion has been disabled as period is less than 1. Period is ${parsedPeriod} days.`);
} else {
try {
await R.exec(
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
[ parsedPeriod ]
);
} catch (e) {
log(`Failed to clear old data: ${e.message}`);
log(`Clearing Data older than ${parsedPeriod} days...`);
try {
await R.exec(
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
[ parsedPeriod ]
);
} catch (e) {
log(`Failed to clear old data: ${e.message}`);
}
}
exit();

View File

@@ -188,13 +188,13 @@ class Maintenance extends BeanModel {
*/
static getActiveMaintenanceSQLCondition() {
return `
(maintenance_timeslot.start_date <= DATETIME('now')
AND maintenance_timeslot.end_date >= DATETIME('now')
AND maintenance.active = 1)
OR
(maintenance.strategy = 'manual' AND active = 1)
(
(maintenance_timeslot.start_date <= DATETIME('now')
AND maintenance_timeslot.end_date >= DATETIME('now')
AND maintenance.active = 1)
OR
(maintenance.strategy = 'manual' AND active = 1)
)
`;
}
@@ -204,10 +204,12 @@ class Maintenance extends BeanModel {
*/
static getActiveAndFutureMaintenanceSQLCondition() {
return `
((maintenance_timeslot.end_date >= DATETIME('now')
AND maintenance.active = 1)
OR
(maintenance.strategy = 'manual' AND active = 1))
(
((maintenance_timeslot.end_date >= DATETIME('now')
AND maintenance.active = 1)
OR
(maintenance.strategy = 'manual' AND active = 1))
)
`;
}
}

View File

@@ -2,8 +2,10 @@ const https = require("https");
const dayjs = require("dayjs");
const axios = require("axios");
const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mongodbPing, mqttAsync, setSetting, httpNtlm, radius, grpcQuery } = require("../util-server");
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
redisPingAsync, mongodbPing,
} = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification");
@@ -15,6 +17,7 @@ const { UptimeKumaServer } = require("../uptime-kuma-server");
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
const { DockerHost } = require("../docker");
const Maintenance = require("./maintenance");
const { UptimeCacheList } = require("../uptime-cache-list");
/**
* status:
@@ -89,32 +92,23 @@ class Monitor extends BeanModel {
dns_resolve_type: this.dns_resolve_type,
dns_resolve_server: this.dns_resolve_server,
dns_last_result: this.dns_last_result,
pushToken: this.pushToken,
docker_container: this.docker_container,
docker_host: this.docker_host,
proxyId: this.proxy_id,
notificationIDList,
tags: tags,
maintenance: await Monitor.isUnderMaintenance(this.id),
mqttUsername: this.mqttUsername,
mqttPassword: this.mqttPassword,
mqttTopic: this.mqttTopic,
mqttSuccessMessage: this.mqttSuccessMessage,
databaseConnectionString: this.databaseConnectionString,
databaseQuery: this.databaseQuery,
authMethod: this.authMethod,
authWorkstation: this.authWorkstation,
authDomain: this.authDomain,
grpcUrl: this.grpcUrl,
grpcProtobuf: this.grpcProtobuf,
grpcMethod: this.grpcMethod,
grpcServiceName: this.grpcServiceName,
grpcEnableTls: this.getGrpcEnableTls(),
radiusUsername: this.radiusUsername,
radiusPassword: this.radiusPassword,
radiusCalledStationId: this.radiusCalledStationId,
radiusCallingStationId: this.radiusCallingStationId,
radiusSecret: this.radiusSecret,
};
if (includeSensitiveData) {
@@ -127,9 +121,18 @@ class Monitor extends BeanModel {
basic_auth_user: this.basic_auth_user,
basic_auth_pass: this.basic_auth_pass,
pushToken: this.pushToken,
databaseConnectionString: this.databaseConnectionString,
radiusUsername: this.radiusUsername,
radiusPassword: this.radiusPassword,
radiusSecret: this.radiusSecret,
mqttUsername: this.mqttUsername,
mqttPassword: this.mqttPassword,
authWorkstation: this.authWorkstation,
authDomain: this.authDomain,
};
}
data.includeSensitiveData = includeSensitiveData;
return data;
}
@@ -267,6 +270,7 @@ class Monitor extends BeanModel {
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
// Axios Options
const options = {
url: this.url,
method: (this.method || "get").toLowerCase(),
@@ -305,20 +309,8 @@ class Monitor extends BeanModel {
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
log.debug("monitor", `[${this.name}] Axios Request`);
let res;
if (this.auth_method === "ntlm") {
options.httpsAgent.keepAlive = true;
res = await httpNtlm(options, {
username: this.basic_auth_user,
password: this.basic_auth_pass,
domain: this.authDomain,
workstation: this.authWorkstation ? this.authWorkstation : undefined
});
} else {
res = await axios.request(options);
}
// Make Request
let res = await this.makeAxiosRequest(options);
bean.msg = `${res.status} - ${res.statusText}`;
bean.ping = dayjs().valueOf() - startTime;
@@ -524,7 +516,9 @@ class Monitor extends BeanModel {
let res = await axios.request(options);
if (res.data.State.Running) {
bean.status = UP;
bean.msg = "";
bean.msg = res.data.State.Status;
} else {
throw Error("Container State is " + res.data.State.Status);
}
} else if (this.type === "mqtt") {
bean.msg = await mqttAsync(this.hostname, this.mqttTopic, this.mqttSuccessMessage, {
@@ -558,7 +552,7 @@ class Monitor extends BeanModel {
log.debug("monitor:", `gRPC response: ${JSON.stringify(response)}`);
let responseData = response.data;
if (responseData.length > 50) {
responseData = response.substring(0, 47) + "...";
responseData = responseData.toString().substring(0, 47) + "...";
}
if (response.code !== 1) {
bean.status = DOWN;
@@ -578,6 +572,14 @@ class Monitor extends BeanModel {
await postgresQuery(this.databaseConnectionString, this.databaseQuery);
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "mysql") {
let startTime = dayjs().valueOf();
await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
@@ -589,6 +591,7 @@ class Monitor extends BeanModel {
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "radius") {
let startTime = dayjs().valueOf();
@@ -625,6 +628,12 @@ class Monitor extends BeanModel {
}
}
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "redis") {
let startTime = dayjs().valueOf();
bean.msg = await redisPingAsync(this.databaseConnectionString);
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else {
bean.msg = "Unknown Monitor Type";
bean.status = PENDING;
@@ -709,6 +718,7 @@ class Monitor extends BeanModel {
}
log.debug("monitor", `[${this.name}] Send to socket`);
UptimeCacheList.clearCache(this.id);
io.to(this.user_id).emit("heartbeat", bean.toJSON());
Monitor.sendStats(io, this.id, this.user_id);
@@ -755,6 +765,40 @@ class Monitor extends BeanModel {
}
}
async makeAxiosRequest(options, finalCall = false) {
try {
let res;
if (this.auth_method === "ntlm") {
options.httpsAgent.keepAlive = true;
res = await httpNtlm(options, {
username: this.basic_auth_user,
password: this.basic_auth_pass,
domain: this.authDomain,
workstation: this.authWorkstation ? this.authWorkstation : undefined
});
} else {
res = await axios.request(options);
}
return res;
} catch (e) {
// Fix #2253
// Read more: https://stackoverflow.com/questions/1759956/curl-error-18-transfer-closed-with-outstanding-read-data-remaining
if (!finalCall && typeof e.message === "string" && e.message.includes("maxContentLength size of -1 exceeded")) {
log.debug("monitor", "makeAxiosRequest with gzip");
options.headers["Accept-Encoding"] = "gzip, deflate";
return this.makeAxiosRequest(options, true);
} else {
if (typeof e.message === "string" && e.message.includes("maxContentLength size of -1 exceeded")) {
e.message = "response timeout: incomplete response within a interval";
}
throw e;
}
}
}
/** Stop monitor */
stop() {
clearTimeout(this.heartbeatInterval);
@@ -893,7 +937,15 @@ class Monitor extends BeanModel {
* @param {number} duration Hours
* @param {number} monitorID ID of monitor to calculate
*/
static async calcUptime(duration, monitorID) {
static async calcUptime(duration, monitorID, forceNoCache = false) {
if (!forceNoCache) {
let cachedUptime = UptimeCacheList.getUptime(monitorID, duration);
if (cachedUptime != null) {
return cachedUptime;
}
}
const timeLogger = new TimeLogger();
const startTime = R.isoDateTime(dayjs.utc().subtract(duration, "hour"));
@@ -952,6 +1004,9 @@ class Monitor extends BeanModel {
}
}
// Cache
UptimeCacheList.addUptime(monitorID, duration, uptime);
return uptime;
}
@@ -1051,7 +1106,13 @@ class Monitor extends BeanModel {
for (let notification of notificationList) {
try {
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), bean.toJSON());
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
const heartbeatJSON = bean.toJSON();
if (!heartbeatJSON["msg"]) {
heartbeatJSON["msg"] = "N/A";
}
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), heartbeatJSON);
} catch (e) {
log.error("monitor", "Cannot send notification to " + notification.name);
log.error("monitor", e);
@@ -1184,6 +1245,15 @@ class Monitor extends BeanModel {
LIMIT 1`, [ monitorID ]);
return maintenance.count !== 0;
}
validate() {
if (this.interval > MAX_INTERVAL_SECOND) {
throw new Error(`Interval cannot be more than ${MAX_INTERVAL_SECOND} seconds`);
}
if (this.interval < MIN_INTERVAL_SECOND) {
throw new Error(`Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`);
}
}
}
module.exports = Monitor;

View File

@@ -281,12 +281,14 @@ class StatusPage extends BeanModel {
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(`
SELECT maintenance.*
FROM maintenance, maintenance_status_page msp, maintenance_timeslot
WHERE msp.maintenance_id = maintenance.id
AND maintenance_timeslot.maintenance_id = maintenance.id
AND msp.status_page_id = ?
AND ${activeCondition}
SELECT DISTINCT maintenance.*
FROM maintenance
JOIN maintenance_status_page
ON maintenance_status_page.maintenance_id = maintenance.id
AND maintenance_status_page.status_page_id = ?
LEFT JOIN maintenance_timeslot
ON maintenance_timeslot.maintenance_id = maintenance.id
WHERE ${activeCondition}
ORDER BY maintenance.end_date
`, [ statusPageId ]));

View File

@@ -0,0 +1,20 @@
import { PluginFunc, ConfigType } from 'dayjs'
declare const plugin: PluginFunc
export = plugin
declare module 'dayjs' {
interface Dayjs {
tz(timezone?: string, keepLocalTime?: boolean): Dayjs
offsetName(type?: 'short' | 'long'): string | undefined
}
interface DayjsTimezone {
(date: ConfigType, timezone?: string): Dayjs
(date: ConfigType, format: string, timezone?: string): Dayjs
guess(): string
setDefault(timezone?: string): void
}
const tz: DayjsTimezone
}

View File

@@ -0,0 +1,115 @@
/**
* Copy from node_modules/dayjs/plugin/timezone.js
* Try to fix https://github.com/louislam/uptime-kuma/issues/2318
* Source: https://github.com/iamkun/dayjs/tree/dev/src/plugin/utc
* License: MIT
*/
!function (t, e) {
// eslint-disable-next-line no-undef
typeof exports == "object" && typeof module != "undefined" ? module.exports = e() : typeof define == "function" && define.amd ? define(e) : (t = typeof globalThis != "undefined" ? globalThis : t || self).dayjs_plugin_timezone = e();
}(this, (function () {
"use strict";
let t = {
year: 0,
month: 1,
day: 2,
hour: 3,
minute: 4,
second: 5
};
let e = {};
return function (n, i, o) {
let r;
let a = function (t, n, i) {
void 0 === i && (i = {});
let o = new Date(t);
let r = function (t, n) {
void 0 === n && (n = {});
let i = n.timeZoneName || "short";
let o = t + "|" + i;
let r = e[o];
return r || (r = new Intl.DateTimeFormat("en-US", {
hour12: !1,
timeZone: t,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
timeZoneName: i
}), e[o] = r), r;
}(n, i);
return r.formatToParts(o);
};
let u = function (e, n) {
let i = a(e, n);
let r = [];
let u = 0;
for (; u < i.length; u += 1) {
let f = i[u];
let s = f.type;
let m = f.value;
let c = t[s];
c >= 0 && (r[c] = parseInt(m, 10));
}
let d = r[3];
let l = d === 24 ? 0 : d;
let v = r[0] + "-" + r[1] + "-" + r[2] + " " + l + ":" + r[4] + ":" + r[5] + ":000";
let h = +e;
return (o.utc(v).valueOf() - (h -= h % 1e3)) / 6e4;
};
let f = i.prototype;
f.tz = function (t, e) {
void 0 === t && (t = r);
let n = this.utcOffset();
let i = this.toDate();
let a = i.toLocaleString("en-US", { timeZone: t }).replace("\u202f", " ");
let u = Math.round((i - new Date(a)) / 1e3 / 60);
let f = o(a).$set("millisecond", this.$ms).utcOffset(15 * -Math.round(i.getTimezoneOffset() / 15) - u, !0);
if (e) {
let s = f.utcOffset();
f = f.add(n - s, "minute");
}
return f.$x.$timezone = t, f;
}, f.offsetName = function (t) {
let e = this.$x.$timezone || o.tz.guess();
let n = a(this.valueOf(), e, { timeZoneName: t }).find((function (t) {
return t.type.toLowerCase() === "timezonename";
}));
return n && n.value;
};
let s = f.startOf;
f.startOf = function (t, e) {
if (!this.$x || !this.$x.$timezone) {
return s.call(this, t, e);
}
let n = o(this.format("YYYY-MM-DD HH:mm:ss:SSS"));
return s.call(n, t, e).tz(this.$x.$timezone, !0);
}, o.tz = function (t, e, n) {
let i = n && e;
let a = n || e || r;
let f = u(+o(), a);
if (typeof t != "string") {
return o(t).tz(a);
}
let s = function (t, e, n) {
let i = t - 60 * e * 1e3;
let o = u(i, n);
if (e === o) {
return [ i, e ];
}
let r = u(i -= 60 * (o - e) * 1e3, n);
return o === r ? [ i, o ] : [ t - 60 * Math.min(o, r) * 1e3, Math.max(o, r) ];
}(o.utc(t, i).valueOf(), f, a);
let m = s[0];
let c = s[1];
let d = o(m).utcOffset(c);
return d.$x.$timezone = a, d;
}, o.tz.guess = function () {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
}, o.tz.setDefault = function (t) {
r = t;
};
};
}));

View File

@@ -64,7 +64,7 @@ class Discord extends NotificationProvider {
},
{
name: "Error",
value: heartbeatJSON["msg"],
value: heartbeatJSON["msg"] == null ? "N/A" : heartbeatJSON["msg"],
},
],
}],
@@ -91,7 +91,7 @@ class Discord extends NotificationProvider {
},
{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: monitorJSON["type"] === "push" ? "Heartbeat" : address.startsWith("http") ? "[Visit Service](" + address + ")" : address,
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
},
{
name: "Time (UTC)",

View File

@@ -0,0 +1,31 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Kook extends NotificationProvider {
name = "Kook";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
let url = "https://www.kookapp.cn/api/v3/message/create";
let data = {
target_id: notification.kookGuildID,
content: msg,
};
let config = {
headers: {
"Authorization": "Bot " + notification.kookBotToken,
"Content-Type": "application/json",
},
};
try {
await axios.post(url, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Kook;

View File

@@ -10,7 +10,7 @@ class Pushover extends NotificationProvider {
let pushoverlink = "https://api.pushover.net/1/messages.json";
let data = {
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg,
"message": "<b>Message</b>:" + msg,
"user": notification.pushoveruserkey,
"token": notification.pushoverapptoken,
"sound": notification.pushoversounds,

View File

@@ -1,7 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { setSettings, setting } = require("../util-server");
const { getMonitorRelativeURL } = require("../../src/util");
const { getMonitorRelativeURL, UP } = require("../../src/util");
class Slack extends NotificationProvider {
@@ -46,24 +46,31 @@ class Slack extends NotificationProvider {
"channel": notification.slackchannel,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
"blocks": [{
"type": "header",
"text": {
"type": "plain_text",
"text": "Uptime Kuma Alert",
},
},
{
"type": "section",
"fields": [{
"type": "mrkdwn",
"text": "*Message*\n" + msg,
},
"attachments": [
{
"type": "mrkdwn",
"text": "*Time (UTC)*\n" + time,
}],
}],
"color": (heartbeatJSON["status"] === UP) ? "#2eb886" : "#e01e5a",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Uptime Kuma Alert",
},
},
{
"type": "section",
"fields": [{
"type": "mrkdwn",
"text": "*Message*\n" + msg,
},
{
"type": "mrkdwn",
"text": "*Time (UTC)*\n" + time,
}],
}
],
}
]
};
if (notification.slackbutton) {
@@ -74,17 +81,19 @@ class Slack extends NotificationProvider {
// Button
if (baseURL) {
data.blocks.push({
"type": "actions",
"elements": [{
"type": "button",
"text": {
"type": "plain_text",
"text": "Visit Uptime Kuma",
},
"value": "Uptime-Kuma",
"url": baseURL + getMonitorRelativeURL(monitorJSON.id),
}],
data.attachments.forEach(element => {
element.blocks.push({
"type": "actions",
"elements": [{
"type": "button",
"text": {
"type": "plain_text",
"text": "Visit Uptime Kuma",
},
"value": "Uptime-Kuma",
"url": baseURL + getMonitorRelativeURL(monitorJSON.id),
}],
});
});
}

View File

@@ -16,20 +16,29 @@ class Webhook extends NotificationProvider {
msg,
};
let finalData;
let config = {};
let config = {
headers: {}
};
if (notification.webhookContentType === "form-data") {
finalData = new FormData();
finalData.append("data", JSON.stringify(data));
config = {
headers: finalData.getHeaders(),
};
config.headers = finalData.getHeaders();
} else {
finalData = data;
}
if (notification.webhookAdditionalHeaders) {
try {
config.headers = {
...config.headers,
...JSON.parse(notification.webhookAdditionalHeaders)
};
} catch (err) {
throw "Additional Headers is not a valid JSON";
}
}
await axios.post(notification.webhookURL, finalData, config);
return okMsg;

View File

@@ -0,0 +1,116 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class ZohoCliq extends NotificationProvider {
name = "ZohoCliq";
/**
* Generate the message to send
* @param {const} status The status constant
* @param {string} monitorName Name of monitor
* @returns {string}
*/
_statusMessageFactory = (status, monitorName) => {
if (status === DOWN) {
return `🔴 Application [${monitorName}] went down\n`;
} else if (status === UP) {
return `✅ Application [${monitorName}] is back online\n`;
}
return "Notification\n";
};
/**
* Send the notification
* @param {string} webhookUrl URL to send the request to
* @param {Array} payload Payload generated by _notificationPayloadFactory
*/
_sendNotification = async (webhookUrl, payload) => {
await axios.post(webhookUrl, { text: payload.join("\n") });
};
/**
* Generate payload for notification
* @param {const} status The status of the monitor
* @param {string} monitorMessage Message to send
* @param {string} monitorName Name of monitor affected
* @param {string} monitorUrl URL of monitor affected
* @returns {Array}
*/
_notificationPayloadFactory = ({
status,
monitorMessage,
monitorName,
monitorUrl,
}) => {
const payload = [];
payload.push("### Uptime Kuma\n");
payload.push(this._statusMessageFactory(status, monitorName));
payload.push(`*Description:* ${monitorMessage}`);
if (monitorName) {
payload.push(`*Monitor:* ${monitorName}`);
}
if (monitorUrl && monitorUrl !== "https://") {
payload.push(`*URL:* [${monitorUrl}](${monitorUrl})`);
}
return payload;
};
/**
* Send a general notification
* @param {string} webhookUrl URL to send request to
* @param {string} msg Message to send
* @returns {Promise<void>}
*/
_handleGeneralNotification = (webhookUrl, msg) => {
const payload = this._notificationPayloadFactory({
monitorMessage: msg
});
return this._sendNotification(webhookUrl, payload);
};
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
if (heartbeatJSON == null) {
await this._handleGeneralNotification(notification.webhookUrl, msg);
return okMsg;
}
let url;
switch (monitorJSON["type"]) {
case "http":
case "keywork":
url = monitorJSON["url"];
break;
case "docker":
url = monitorJSON["docker_host"];
break;
default:
url = monitorJSON["hostname"];
break;
}
const payload = this._notificationPayloadFactory({
monitorMessage: heartbeatJSON.msg,
monitorName: monitorJSON.name,
monitorUrl: url,
status: heartbeatJSON.status
});
await this._sendNotification(notification.webhookUrl, payload);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = ZohoCliq;

View File

@@ -14,6 +14,7 @@ const GoogleChat = require("./notification-providers/google-chat");
const Gorush = require("./notification-providers/gorush");
const Gotify = require("./notification-providers/gotify");
const HomeAssistant = require("./notification-providers/home-assistant");
const Kook = require("./notification-providers/kook");
const Line = require("./notification-providers/line");
const LineNotify = require("./notification-providers/linenotify");
const LunaSea = require("./notification-providers/lunasea");
@@ -44,6 +45,7 @@ const WeCom = require("./notification-providers/wecom");
const GoAlert = require("./notification-providers/goalert");
const SMSManager = require("./notification-providers/smsmanager");
const ServerChan = require("./notification-providers/serverchan");
const ZohoCliq = require("./notification-providers/zoho-cliq");
class Notification {
@@ -70,6 +72,7 @@ class Notification {
new Gorush(),
new Gotify(),
new HomeAssistant(),
new Kook(),
new Line(),
new LineNotify(),
new LunaSea(),
@@ -100,6 +103,7 @@ class Notification {
new Webhook(),
new WeCom(),
new GoAlert(),
new ZohoCliq()
];
for (let item of list) {

View File

@@ -1,199 +0,0 @@
// https://github.com/ben-bradley/ping-lite/blob/master/ping-lite.js
// Fixed on Windows
const net = require("net");
const spawn = require("child_process").spawn;
const events = require("events");
const fs = require("fs");
const util = require("./util-server");
module.exports = Ping;
/**
* Constructor for ping class
* @param {string} host Host to ping
* @param {object} [options] Options for the ping command
* @param {array|string} [options.args] - Arguments to pass to the ping command
*/
function Ping(host, options) {
if (!host) {
throw new Error("You must specify a host to ping!");
}
this._host = host;
this._options = options = (options || {});
events.EventEmitter.call(this);
const timeout = 10;
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 (util.LIN) {
this._bin = "/bin/ping";
const defaultArgs = [ "-n", "-w", timeout, "-c", "1", host ];
if (net.isIPv6(host) || options.ipv6) {
defaultArgs.unshift("-6");
}
this._args = (options.args) ? options.args : defaultArgs;
this._regmatch = /=([0-9.]+?) ms/;
} else if (util.MAC) {
if (net.isIPv6(host) || options.ipv6) {
this._bin = "/sbin/ping6";
} else {
this._bin = "/sbin/ping";
}
this._args = (options.args) ? options.args : [ "-n", "-t", timeout, "-c", "1", host ];
this._regmatch = /=([0-9.]+?) ms/;
} else if (util.BSD) {
this._bin = "/sbin/ping";
const defaultArgs = [ "-n", "-t", timeout, "-c", "1", host ];
if (net.isIPv6(host) || options.ipv6) {
defaultArgs.unshift("-6");
}
this._args = (options.args) ? options.args : defaultArgs;
this._regmatch = /=([0-9.]+?) ms/;
} else {
throw new Error("Could not detect your ping binary.");
}
if (!fs.existsSync(this._bin)) {
throw new Error("Could not detect " + this._bin + " on your system");
}
this._i = 0;
return this;
}
Ping.prototype.__proto__ = events.EventEmitter.prototype;
/**
* Callback for send
* @callback pingCB
* @param {any} err Any error encountered
* @param {number} ms Ping time in ms
*/
/**
* Send a ping
* @param {pingCB} callback Callback to call with results
*/
Ping.prototype.send = function (callback) {
let self = this;
callback = callback || function (err, ms) {
if (err) {
return self.emit("error", err);
}
return self.emit("result", ms);
};
let _ended;
let _exited;
let _errored;
this._ping = spawn(this._bin, this._args, { windowsHide: true }); // spawn the binary
this._ping.on("error", function (err) { // handle binary errors
_errored = true;
callback(err);
});
this._ping.stdout.on("data", function (data) { // log stdout
if (util.WIN) {
data = convertOutput(data);
}
this._stdout = (this._stdout || "") + data;
});
this._ping.stdout.on("end", function () {
_ended = true;
if (_exited && !_errored) {
onEnd.call(self._ping);
}
});
this._ping.stderr.on("data", function (data) { // log stderr
if (util.WIN) {
data = convertOutput(data);
}
this._stderr = (this._stderr || "") + data;
});
this._ping.on("exit", function (code) { // handle complete
_exited = true;
if (_ended && !_errored) {
onEnd.call(self._ping);
}
});
/**
* @param {Function} callback
*
* Generated by Trelent
*/
function onEnd() {
let stdout = this.stdout._stdout;
let stderr = this.stderr._stderr;
let ms;
if (stderr) {
return callback(new Error(stderr));
}
if (!stdout) {
return callback(new Error("No stdout detected"));
}
ms = stdout.match(self._regmatch); // parse out the ##ms response
ms = (ms && ms[1]) ? Number(ms[1]) : ms;
callback(null, ms, stdout);
}
};
/**
* Ping every interval
* @param {pingCB} callback Callback to call with results
*/
Ping.prototype.start = function (callback) {
let self = this;
this._i = setInterval(function () {
self.send(callback);
}, (self._options.interval || 5000));
self.send(callback);
};
/** Stop sending pings */
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 {any} data
* @returns {string}
*/
function convertOutput(data) {
if (util.WIN) {
if (data) {
return util.convertToUTF8(data);
}
}
return data;
}

View File

@@ -7,7 +7,7 @@ const { UptimeKumaServer } = require("./uptime-kuma-server");
class Proxy {
static SUPPORTED_PROXY_PROTOCOLS = [ "http", "https", "socks", "socks5", "socks4" ];
static SUPPORTED_PROXY_PROTOCOLS = [ "http", "https", "socks", "socks5", "socks5h", "socks4" ];
/**
* Saves and updates given proxy entity
@@ -126,6 +126,7 @@ class Proxy {
break;
case "socks":
case "socks5":
case "socks5h":
case "socks4":
agent = new SocksProxyAgent({
...httpAgentOptions,

View File

@@ -8,7 +8,7 @@ console.log("Welcome to Uptime Kuma");
// As the log function need to use dayjs, it should be very top
const dayjs = require("dayjs");
dayjs.extend(require("dayjs/plugin/utc"));
dayjs.extend(require("dayjs/plugin/timezone"));
dayjs.extend(require("./modules/dayjs/plugin/timezone"));
dayjs.extend(require("dayjs/plugin/customParseFormat"));
// Check Node.js Version
@@ -135,7 +135,9 @@ const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudfl
const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler");
const { dockerSocketHandler } = require("./socket-handlers/docker-socket-handler");
const { maintenanceSocketHandler } = require("./socket-handlers/maintenance-socket-handler");
const { generalSocketHandler } = require("./socket-handlers/general-socket-handler");
const { Settings } = require("./settings");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
app.use(express.json());
@@ -203,6 +205,7 @@ let needSetup = false;
if (isDev) {
app.post("/test-webhook", async (request, response) => {
log.debug("test", request.headers);
log.debug("test", request.body);
response.send("OK");
});
@@ -631,6 +634,9 @@ let needSetup = false;
bean.import(monitor);
bean.user_id = socket.userID;
bean.validate();
await R.store(bean);
await updateMonitorNotification(bean.id, notificationIDList);
@@ -708,6 +714,7 @@ let needSetup = false;
bean.authDomain = monitor.authDomain;
bean.grpcUrl = monitor.grpcUrl;
bean.grpcProtobuf = monitor.grpcProtobuf;
bean.grpcServiceName = monitor.grpcServiceName;
bean.grpcMethod = monitor.grpcMethod;
bean.grpcBody = monitor.grpcBody;
bean.grpcMetadata = monitor.grpcMetadata;
@@ -718,6 +725,8 @@ let needSetup = false;
bean.radiusCallingStationId = monitor.radiusCallingStationId;
bean.radiusSecret = monitor.radiusSecret;
bean.validate();
await R.store(bean);
await updateMonitorNotification(bean.id, monitor.notificationIDList);
@@ -932,13 +941,21 @@ let needSetup = false;
try {
checkLogin(socket);
let bean = await R.findOne("monitor", " id = ? ", [ tag.id ]);
let bean = await R.findOne("tag", " id = ? ", [ tag.id ]);
if (bean == null) {
callback({
ok: false,
msg: "Tag not found",
});
return;
}
bean.name = tag.name;
bean.color = tag.color;
await R.store(bean);
callback({
ok: true,
msg: "Saved",
tag: await bean.toJSON(),
});
@@ -1108,6 +1125,8 @@ let needSetup = false;
await setSettings("general", data);
server.entryPage = data.entryPage;
await CacheableDnsHttpAgent.update();
// Also need to apply timezone globally
if (data.serverTimezone) {
await server.setTimezone(data.serverTimezone);
@@ -1479,6 +1498,7 @@ let needSetup = false;
proxySocketHandler(socket);
dockerSocketHandler(socket);
maintenanceSocketHandler(socket);
generalSocketHandler(socket, server);
log.debug("server", "added all socket handlers");
@@ -1601,6 +1621,13 @@ async function afterLogin(socket, user) {
for (let monitorID in monitorList) {
await Monitor.sendStats(io, monitorID, user.id);
}
// Set server timezone from client browser if not set
// It should be run once only
if (! await Settings.get("initServerTimezone")) {
log.debug("server", "emit initServerTimezone");
socket.emit("initServerTimezone");
}
}
/**
@@ -1739,6 +1766,7 @@ async function shutdownFunction(signal) {
stopBackgroundJobs();
await cloudflaredStop();
Settings.stopCacheCleaner();
}
/** Final function called before application exits */

View File

@@ -158,6 +158,13 @@ class Settings {
delete Settings.cacheList[key];
}
}
static stopCacheCleaner() {
if (Settings.cacheCleaner) {
clearInterval(Settings.cacheCleaner);
Settings.cacheCleaner = null;
}
}
}
module.exports = {

View File

@@ -1,6 +1,7 @@
const { checkLogin, setSetting, setting, doubleCheckPassword } = require("../util-server");
const { CloudflaredTunnel } = require("node-cloudflared-tunnel");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { log } = require("../../src/util");
const io = UptimeKumaServer.getInstance().io;
const prefix = "cloudflared_";
@@ -107,7 +108,7 @@ module.exports.autoStart = async (token) => {
/** Stop cloudflared */
module.exports.stop = async () => {
console.log("Stop cloudflared");
log.info("cloudflared", "Stop cloudflared");
if (cloudflared) {
cloudflared.stop();
}

View File

@@ -0,0 +1,20 @@
const { log } = require("../../src/util");
const { Settings } = require("../settings");
const { sendInfo } = require("../client");
const { checkLogin } = require("../util-server");
module.exports.generalSocketHandler = (socket, server) => {
socket.on("initServerTimezone", async (timezone) => {
try {
checkLogin(socket);
log.debug("generalSocketHandler", "Timezone: " + timezone);
await Settings.set("initServerTimezone", true);
await server.setTimezone(timezone);
await sendInfo(socket);
} catch (e) {
log.warn("initServerTimezone", e.message);
}
});
};

View File

@@ -244,6 +244,8 @@ module.exports.maintenanceSocketHandler = (socket) => {
socket.userID,
]);
apicache.clear();
callback({
ok: true,
msg: "Deleted Successfully.",
@@ -269,6 +271,8 @@ module.exports.maintenanceSocketHandler = (socket) => {
maintenanceID,
]);
apicache.clear();
callback({
ok: true,
msg: "Paused Successfully.",
@@ -294,6 +298,8 @@ module.exports.maintenanceSocketHandler = (socket) => {
maintenanceID,
]);
apicache.clear();
callback({
ok: true,
msg: "Resume Successfully",

View File

@@ -0,0 +1,39 @@
const { log } = require("../src/util");
class UptimeCacheList {
/**
* list[monitorID][duration]
*/
static list = {};
/**
*
* @param monitorID
* @param duration
* @return number
*/
static getUptime(monitorID, duration) {
if (UptimeCacheList.list[monitorID] && UptimeCacheList.list[monitorID][duration]) {
log.debug("UptimeCacheList", "getUptime: " + monitorID + " " + duration);
return UptimeCacheList.list[monitorID][duration];
} else {
return null;
}
}
static addUptime(monitorID, duration, uptime) {
log.debug("UptimeCacheList", "addUptime: " + monitorID + " " + duration);
if (!UptimeCacheList.list[monitorID]) {
UptimeCacheList.list[monitorID] = {};
}
UptimeCacheList.list[monitorID][duration] = uptime;
}
static clearCache(monitorID) {
log.debug("UptimeCacheList", "clearCache: " + monitorID);
delete UptimeCacheList.list[monitorID];
}
}
module.exports = {
UptimeCacheList,
};

View File

@@ -83,12 +83,12 @@ class UptimeKumaServer {
}
}
CacheableDnsHttpAgent.registerGlobalAgent();
this.io = new Server(this.httpServer);
}
async initAfterDatabaseReady() {
await CacheableDnsHttpAgent.update();
process.env.TZ = await this.getTimezone();
dayjs.tz.setDefault(process.env.TZ);
log.debug("DEBUG", "Timezone: " + process.env.TZ);

View File

@@ -1,5 +1,5 @@
const tcpp = require("tcp-ping");
const Ping = require("./ping-lite");
const ping = require("@louislam/ping");
const { R } = require("redbean-node");
const { log, genSecret } = require("../src/util");
const passwordHash = require("./password-hash");
@@ -13,12 +13,14 @@ const { badgeConstants } = require("./config");
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");
const protojs = require("protobufjs");
const radiusClient = require("node-radius-client");
const redis = require("redis");
const {
dictionaries: {
rfc2865: { file, attributes },
@@ -26,12 +28,7 @@ const {
} = require("node-radius-utils");
const dayjs = require("dayjs");
// 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);
exports.BSD = /bsd$/.test(process.platform);
const isWindows = process.platform === /^win/.test(process.platform);
/**
* Init or reset JWT secret
@@ -105,18 +102,23 @@ exports.ping = async (hostname) => {
*/
exports.pingAsync = function (hostname, ipv6 = false) {
return new Promise((resolve, reject) => {
const ping = new Ping(hostname, {
ipv6
});
ping.send(function (err, ms, stdout) {
if (err) {
reject(err);
} else if (ms === null) {
reject(new Error(stdout));
ping.promise.probe(hostname, {
v6: ipv6,
min_reply: 1,
timeout: 10,
}).then((res) => {
// If ping failed, it will set field to unknown
if (res.alive) {
resolve(res.time);
} else {
resolve(Math.round(ms));
if (isWindows) {
reject(new Error(exports.convertToUTF8(res.output)));
} else {
reject(new Error(res.output));
}
}
}).catch((err) => {
reject(err);
});
});
};
@@ -248,19 +250,19 @@ exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) {
* @param {string} query The query to validate the database with
* @returns {Promise<(string[]|Object[]|Object)>}
*/
exports.mssqlQuery = function (connectionString, query) {
return new Promise((resolve, reject) => {
mssql.connect(connectionString).then(pool => {
return pool.request()
.query(query);
}).then(result => {
resolve(result);
}).catch(err => {
reject(err);
}).finally(() => {
mssql.close();
});
});
exports.mssqlQuery = async function (connectionString, query) {
let pool;
try {
pool = new mssql.ConnectionPool(connectionString);
await pool.connect();
await pool.request().query(query);
pool.close();
} catch (e) {
if (pool) {
pool.close();
}
throw e;
}
};
/**
@@ -295,6 +297,28 @@ exports.postgresQuery = function (connectionString, query) {
});
};
/**
* Run a query on MySQL/MariaDB
* @param {string} connectionString The database connection string
* @param {string} query The query to validate the database with
* @returns {Promise<(string[]|Object[]|Object)>}
*/
exports.mysqlQuery = function (connectionString, query) {
return new Promise((resolve, reject) => {
const connection = mysql.createConnection(connectionString);
connection.promise().query(query)
.then(res => {
resolve(res);
})
.catch(err => {
reject(err);
})
.finally(() => {
connection.end();
});
});
};
/**
* Connect to and Ping a MongoDB database
* @param {string} connectionString The database connection string
@@ -356,6 +380,30 @@ exports.radius = function (
});
};
/**
* Redis server ping
* @param {string} dsn The redis connection string
*/
exports.redisPingAsync = function (dsn) {
return new Promise((resolve, reject) => {
const client = redis.createClient({
url: dsn,
});
client.on("error", (err) => {
reject(err);
});
client.connect().then(() => {
client.ping().then((res, err) => {
if (err) {
reject(err);
} else {
resolve(res);
}
});
});
});
};
/**
* Retrieve value of setting based on key
* @param {string} key Key of setting to retrieve
@@ -472,6 +520,10 @@ const parseCertificateInfo = function (info) {
* @returns {Object} Object containing certificate information
*/
exports.checkCertificate = function (res) {
if (!res.request.res.socket) {
throw new Error("No socket found");
}
const info = res.request.res.socket.getPeerCertificate(true);
const valid = res.request.res.socket.authorized || false;
@@ -776,22 +828,31 @@ module.exports.grpcQuery = async (options) => {
cb);
}, false, false);
return new Promise((resolve, _) => {
return grpcService[`${grpcMethod}`](JSON.parse(grpcBody), function (err, response) {
const responseData = JSON.stringify(response);
if (err) {
return resolve({
code: err.code,
errorMessage: err.details,
data: ""
});
} else {
log.debug("monitor:", `gRPC response: ${response}`);
return resolve({
code: 1,
errorMessage: "",
data: responseData
});
}
});
try {
return grpcService[`${grpcMethod}`](JSON.parse(grpcBody), function (err, response) {
const responseData = JSON.stringify(response);
if (err) {
return resolve({
code: err.code,
errorMessage: err.details,
data: ""
});
} else {
log.debug("monitor:", `gRPC response: ${JSON.stringify(response)}`);
return resolve({
code: 1,
errorMessage: "",
data: responseData
});
}
});
} catch (err) {
return resolve({
code: -1,
errorMessage: `Error ${err}. Please review your gRPC configuration option. The service name must not include package name value, and the method name must follow camelCase format`,
data: ""
});
}
});
};