Merge remote-tracking branch 'origin/master' into skaempfe#2593

# Conflicts:
#	server/model/monitor.js
#	src/pages/Details.vue
This commit is contained in:
Louis Lam 2023-05-26 21:32:58 +08:00
commit 0b8dddba24
247 changed files with 34964 additions and 32467 deletions

76
server/model/api_key.js Normal file
View file

@ -0,0 +1,76 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const { R } = require("redbean-node");
const dayjs = require("dayjs");
class APIKey extends BeanModel {
/**
* Get the current status of this API key
* @returns {string} active, inactive or expired
*/
getStatus() {
let current = dayjs();
let expiry = dayjs(this.expires);
if (expiry.diff(current) < 0) {
return "expired";
}
return this.active ? "active" : "inactive";
}
/**
* Returns an object that ready to parse to JSON
* @returns {Object}
*/
toJSON() {
return {
id: this.id,
key: this.key,
name: this.name,
userID: this.user_id,
createdDate: this.created_date,
active: this.active,
expires: this.expires,
status: this.getStatus(),
};
}
/**
* Returns an object that ready to parse to JSON with sensitive fields
* removed
* @returns {Object}
*/
toPublicJSON() {
return {
id: this.id,
name: this.name,
userID: this.user_id,
createdDate: this.created_date,
active: this.active,
expires: this.expires,
status: this.getStatus(),
};
}
/**
* Create a new API Key and store it in the database
* @param {Object} key Object sent by client
* @param {int} userID ID of socket user
* @returns {Promise<bean>}
*/
static async save(key, userID) {
let bean;
bean = R.dispense("api_key");
bean.key = key.key;
bean.name = key.name;
bean.user_id = userID;
bean.active = key.active;
bean.expires = key.expires;
await R.store(bean);
return bean;
}
}
module.exports = APIKey;

View file

@ -1,8 +1,10 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const { parseTimeObject, parseTimeFromTimeObject, utcToLocal, localToUTC, log } = require("../../src/util");
const { timeObjectToUTC, timeObjectToLocal } = require("../util-server");
const { parseTimeObject, parseTimeFromTimeObject, log } = require("../../src/util");
const { R } = require("redbean-node");
const dayjs = require("dayjs");
const Cron = require("croner");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const apicache = require("../modules/apicache");
class Maintenance extends BeanModel {
@ -15,16 +17,19 @@ class Maintenance extends BeanModel {
let dateRange = [];
if (this.start_date) {
dateRange.push(utcToLocal(this.start_date));
if (this.end_date) {
dateRange.push(utcToLocal(this.end_date));
}
dateRange.push(this.start_date);
} else {
dateRange.push(null);
}
if (this.end_date) {
dateRange.push(this.end_date);
}
let timeRange = [];
let startTime = timeObjectToLocal(parseTimeObject(this.start_time));
let startTime = parseTimeObject(this.start_time);
timeRange.push(startTime);
let endTime = timeObjectToLocal(parseTimeObject(this.end_time));
let endTime = parseTimeObject(this.end_time);
timeRange.push(endTime);
let obj = {
@ -39,12 +44,44 @@ class Maintenance extends BeanModel {
weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [],
timeslotList: [],
cron: this.cron,
duration: this.duration,
durationMinutes: parseInt(this.duration / 60),
timezone: await this.getTimezone(), // Only valid timezone
timezoneOption: this.timezone, // Mainly for dropdown menu, because there is a option "SAME_AS_SERVER"
timezoneOffset: await this.getTimezoneOffset(),
status: await this.getStatus(),
};
const timeslotList = await this.getTimeslotList();
if (this.strategy === "manual") {
// Do nothing, no timeslots
} else if (this.strategy === "single") {
obj.timeslotList.push({
startDate: this.start_date,
endDate: this.end_date,
});
} else {
// Should be cron or recurring here
if (this.beanMeta.job) {
let runningTimeslot = this.getRunningTimeslot();
for (let timeslot of timeslotList) {
obj.timeslotList.push(await timeslot.toPublicJSON());
if (runningTimeslot) {
obj.timeslotList.push(runningTimeslot);
}
let nextRunDate = this.beanMeta.job.nextRun();
if (nextRunDate) {
let startDateDayjs = dayjs(nextRunDate);
let startDate = startDateDayjs.toISOString();
let endDate = startDateDayjs.add(this.duration, "second").toISOString();
obj.timeslotList.push({
startDate,
endDate,
});
}
}
}
if (!Array.isArray(obj.weekdays)) {
@ -55,54 +92,9 @@ class Maintenance extends BeanModel {
obj.daysOfMonth = [];
}
// Maintenance Status
if (!obj.active) {
obj.status = "inactive";
} else if (obj.strategy === "manual") {
obj.status = "under-maintenance";
} else if (obj.timeslotList.length > 0) {
let currentTimestamp = dayjs().unix();
for (let timeslot of obj.timeslotList) {
if (dayjs.utc(timeslot.startDate).unix() <= currentTimestamp && dayjs.utc(timeslot.endDate).unix() >= currentTimestamp) {
log.debug("timeslot", "Timeslot ID: " + timeslot.id);
log.debug("timeslot", "currentTimestamp:" + currentTimestamp);
log.debug("timeslot", "timeslot.start_date:" + dayjs.utc(timeslot.startDate).unix());
log.debug("timeslot", "timeslot.end_date:" + dayjs.utc(timeslot.endDate).unix());
obj.status = "under-maintenance";
break;
}
}
if (!obj.status) {
obj.status = "scheduled";
}
} else if (obj.timeslotList.length === 0) {
obj.status = "ended";
} else {
obj.status = "unknown";
}
return obj;
}
/**
* Only get future or current timeslots only
* @returns {Promise<[]>}
*/
async getTimeslotList() {
return R.convertToBeans("maintenance_timeslot", await R.getAll(`
SELECT maintenance_timeslot.*
FROM maintenance_timeslot, maintenance
WHERE maintenance_timeslot.maintenance_id = maintenance.id
AND maintenance.id = ?
AND ${Maintenance.getActiveAndFutureMaintenanceSQLCondition()}
`, [
this.id
]));
}
/**
* Return an object that ready to parse to JSON
* @param {string} timezone If not specified, the timeRange will be in UTC
@ -112,6 +104,11 @@ class Maintenance extends BeanModel {
return this.toPublicJSON(timezone);
}
/**
* Get a list of weekdays that the maintenance is active for
* Monday=1, Tuesday=2 etc.
* @returns {number[]} Array of active weekdays
*/
getDayOfWeekList() {
log.debug("timeslot", "List: " + this.weekdays);
return JSON.parse(this.weekdays).sort(function (a, b) {
@ -119,25 +116,21 @@ class Maintenance extends BeanModel {
});
}
/**
* Get a list of days in month that maintenance is active for
* @returns {number[]|string[]} Array of active days in month
*/
getDayOfMonthList() {
return JSON.parse(this.days_of_month).sort(function (a, b) {
return a - b;
});
}
getStartDateTime() {
let startOfTheDay = dayjs.utc(this.start_date).format("HH:mm");
log.debug("timeslot", "startOfTheDay: " + startOfTheDay);
// Start Time
let startTimeSecond = dayjs.utc(this.start_time, "HH:mm").diff(dayjs.utc(startOfTheDay, "HH:mm"), "second");
log.debug("timeslot", "startTime: " + startTimeSecond);
// Bake StartDate + StartTime = Start DateTime
return dayjs.utc(this.start_date).add(startTimeSecond, "second");
}
getDuration() {
/**
* Get the duration of maintenance in seconds
* @returns {number} Duration of maintenance
*/
calcDuration() {
let duration = dayjs.utc(this.end_time, "HH:mm").diff(dayjs.utc(this.start_time, "HH:mm"), "second");
// Add 24hours if it is across day
if (duration < 0) {
@ -146,71 +139,276 @@ class Maintenance extends BeanModel {
return duration;
}
static jsonToBean(bean, obj) {
/**
* Convert data from socket to bean
* @param {Bean} bean Bean to fill in
* @param {Object} obj Data to fill bean with
* @returns {Bean} Filled bean
*/
static async jsonToBean(bean, obj) {
if (obj.id) {
bean.id = obj.id;
}
// Apply timezone offset to timeRange, as it cannot apply automatically.
if (obj.timeRange[0]) {
timeObjectToUTC(obj.timeRange[0]);
if (obj.timeRange[1]) {
timeObjectToUTC(obj.timeRange[1]);
}
}
bean.title = obj.title;
bean.description = obj.description;
bean.strategy = obj.strategy;
bean.interval_day = obj.intervalDay;
bean.timezone = obj.timezoneOption;
bean.active = obj.active;
if (obj.dateRange[0]) {
bean.start_date = localToUTC(obj.dateRange[0]);
if (obj.dateRange[1]) {
bean.end_date = localToUTC(obj.dateRange[1]);
}
bean.start_date = obj.dateRange[0];
} else {
bean.start_date = null;
}
bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
if (obj.dateRange[1]) {
bean.end_date = obj.dateRange[1];
} else {
bean.end_date = null;
}
bean.weekdays = JSON.stringify(obj.weekdays);
bean.days_of_month = JSON.stringify(obj.daysOfMonth);
if (bean.strategy === "cron") {
bean.duration = obj.durationMinutes * 60;
bean.cron = obj.cron;
this.validateCron(bean.cron);
}
if (bean.strategy.startsWith("recurring-")) {
bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
bean.weekdays = JSON.stringify(obj.weekdays);
bean.days_of_month = JSON.stringify(obj.daysOfMonth);
await bean.generateCron();
this.validateCron(bean.cron);
}
return bean;
}
/**
* SQL conditions for active maintenance
* @returns {string}
* Throw error if cron is invalid
* @param cron
* @returns {Promise<void>}
*/
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)
)
`;
static async validateCron(cron) {
let job = new Cron(cron, () => {});
job.stop();
}
/**
* SQL conditions for active and future maintenance
* @returns {string}
* Run the cron
*/
static getActiveAndFutureMaintenanceSQLCondition() {
return `
(
((maintenance_timeslot.end_date >= DATETIME('now')
AND maintenance.active = 1)
OR
(maintenance.strategy = 'manual' AND active = 1))
)
`;
async run(throwError = false) {
if (this.beanMeta.job) {
log.debug("maintenance", "Maintenance is already running, stop it first. id: " + this.id);
this.stop();
}
log.debug("maintenance", "Run maintenance id: " + this.id);
// 1.21.2 migration
if (!this.cron) {
await this.generateCron();
if (!this.timezone) {
this.timezone = "UTC";
}
if (this.cron) {
await R.store(this);
}
}
if (this.strategy === "manual") {
// Do nothing, because it is controlled by the user
} else if (this.strategy === "single") {
this.beanMeta.job = new Cron(this.start_date, { timezone: await this.getTimezone() }, () => {
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
apicache.clear();
});
} else if (this.cron != null) {
// Here should be cron or recurring
try {
this.beanMeta.status = "scheduled";
let startEvent = (customDuration = 0) => {
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
this.beanMeta.status = "under-maintenance";
clearTimeout(this.beanMeta.durationTimeout);
// Check if duration is still in the window. If not, use the duration from the current time to the end of the window
let duration;
if (customDuration > 0) {
duration = customDuration;
} else if (this.end_date) {
let d = dayjs(this.end_date).diff(dayjs(), "second");
if (d < this.duration) {
duration = d * 1000;
}
} else {
duration = this.duration * 1000;
}
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
this.beanMeta.durationTimeout = setTimeout(() => {
// End of maintenance for this timeslot
this.beanMeta.status = "scheduled";
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
}, duration);
};
// Create Cron
this.beanMeta.job = new Cron(this.cron, {
timezone: await this.getTimezone(),
}, startEvent);
// Continue if the maintenance is still in the window
let runningTimeslot = this.getRunningTimeslot();
let current = dayjs();
if (runningTimeslot) {
let duration = dayjs(runningTimeslot.endDate).diff(current, "second") * 1000;
log.debug("maintenance", "Maintenance id: " + this.id + " Remaining duration: " + duration + "ms");
startEvent(duration);
}
} catch (e) {
log.error("maintenance", "Error in maintenance id: " + this.id);
log.error("maintenance", "Cron: " + this.cron);
log.error("maintenance", e);
if (throwError) {
throw e;
}
}
} else {
log.error("maintenance", "Maintenance id: " + this.id + " has no cron");
}
}
getRunningTimeslot() {
let start = dayjs(this.beanMeta.job.nextRun(dayjs().add(-this.duration, "second").toDate()));
let end = start.add(this.duration, "second");
let current = dayjs();
if (current.isAfter(start) && current.isBefore(end)) {
return {
startDate: start.toISOString(),
endDate: end.toISOString(),
};
} else {
return null;
}
}
stop() {
if (this.beanMeta.job) {
this.beanMeta.job.stop();
delete this.beanMeta.job;
}
}
async isUnderMaintenance() {
return (await this.getStatus()) === "under-maintenance";
}
async getTimezone() {
if (!this.timezone || this.timezone === "SAME_AS_SERVER") {
return await UptimeKumaServer.getInstance().getTimezone();
}
return this.timezone;
}
async getTimezoneOffset() {
return dayjs.tz(dayjs(), await this.getTimezone()).format("Z");
}
async getStatus() {
if (!this.active) {
return "inactive";
}
if (this.strategy === "manual") {
return "under-maintenance";
}
// Check if the maintenance is started
if (this.start_date && dayjs().isBefore(dayjs.tz(this.start_date, await this.getTimezone()))) {
return "scheduled";
}
// Check if the maintenance is ended
if (this.end_date && dayjs().isAfter(dayjs.tz(this.end_date, await this.getTimezone()))) {
return "ended";
}
if (this.strategy === "single") {
return "under-maintenance";
}
if (!this.beanMeta.status) {
return "unknown";
}
return this.beanMeta.status;
}
/**
* Generate Cron for recurring maintenance
* @returns {Promise<void>}
*/
async generateCron() {
log.info("maintenance", "Generate cron for maintenance id: " + this.id);
if (this.strategy === "cron") {
// Do nothing for cron
} else if (!this.strategy.startsWith("recurring-")) {
this.cron = "";
} else if (this.strategy === "recurring-interval") {
let array = this.start_time.split(":");
let hour = parseInt(array[0]);
let minute = parseInt(array[1]);
this.cron = minute + " " + hour + " */" + this.interval_day + " * *";
this.duration = this.calcDuration();
log.debug("maintenance", "Cron: " + this.cron);
log.debug("maintenance", "Duration: " + this.duration);
} else if (this.strategy === "recurring-weekday") {
let list = this.getDayOfWeekList();
let array = this.start_time.split(":");
let hour = parseInt(array[0]);
let minute = parseInt(array[1]);
this.cron = minute + " " + hour + " * * " + list.join(",");
this.duration = this.calcDuration();
} else if (this.strategy === "recurring-day-of-month") {
let list = this.getDayOfMonthList();
let array = this.start_time.split(":");
let hour = parseInt(array[0]);
let minute = parseInt(array[1]);
let dayList = [];
for (let day of list) {
if (typeof day === "string" && day.startsWith("lastDay")) {
if (day === "lastDay1") {
dayList.push("L");
}
// Unfortunately, lastDay2-4 is not supported by cron
} else {
dayList.push(day);
}
}
// Remove duplicate
dayList = [ ...new Set(dayList) ];
this.cron = minute + " " + hour + " " + dayList.join(",") + " * *";
this.duration = this.calcDuration();
}
}
}

View file

@ -1,189 +0,0 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const { R } = require("redbean-node");
const dayjs = require("dayjs");
const { log, utcToLocal, SQL_DATETIME_FORMAT_WITHOUT_SECOND, localToUTC } = require("../../src/util");
const { UptimeKumaServer } = require("../uptime-kuma-server");
class MaintenanceTimeslot extends BeanModel {
async toPublicJSON() {
const serverTimezoneOffset = UptimeKumaServer.getInstance().getTimezoneOffset();
const obj = {
id: this.id,
startDate: this.start_date,
endDate: this.end_date,
startDateServerTimezone: utcToLocal(this.start_date, SQL_DATETIME_FORMAT_WITHOUT_SECOND),
endDateServerTimezone: utcToLocal(this.end_date, SQL_DATETIME_FORMAT_WITHOUT_SECOND),
serverTimezoneOffset,
};
return obj;
}
async toJSON() {
return await this.toPublicJSON();
}
/**
* @param {Maintenance} maintenance
* @param {dayjs} minDate (For recurring type only) Generate a next timeslot from this date.
* @param {boolean} removeExist Remove existing timeslot before create
* @returns {Promise<MaintenanceTimeslot>}
*/
static async generateTimeslot(maintenance, minDate = null, removeExist = false) {
if (removeExist) {
await R.exec("DELETE FROM maintenance_timeslot WHERE maintenance_id = ? ", [
maintenance.id
]);
}
if (maintenance.strategy === "manual") {
log.debug("maintenance", "No need to generate timeslot for manual type");
} else if (maintenance.strategy === "single") {
let bean = R.dispense("maintenance_timeslot");
bean.maintenance_id = maintenance.id;
bean.start_date = maintenance.start_date;
bean.end_date = maintenance.end_date;
bean.generated_next = true;
return await R.store(bean);
} else if (maintenance.strategy === "recurring-interval") {
// Prevent dead loop, in case interval_day is not set
if (!maintenance.interval_day || maintenance.interval_day <= 0) {
maintenance.interval_day = 1;
}
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
return startDateTime.add(maintenance.interval_day, "day");
}, () => {
return true;
});
} else if (maintenance.strategy === "recurring-weekday") {
let dayOfWeekList = maintenance.getDayOfWeekList();
log.debug("timeslot", dayOfWeekList);
if (dayOfWeekList.length <= 0) {
log.debug("timeslot", "No weekdays selected?");
return null;
}
const isValid = (startDateTime) => {
log.debug("timeslot", "nextDateTime: " + startDateTime);
let day = startDateTime.local().day();
log.debug("timeslot", "nextDateTime.day(): " + day);
return dayOfWeekList.includes(day);
};
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
while (true) {
startDateTime = startDateTime.add(1, "day");
if (isValid(startDateTime)) {
return startDateTime;
}
}
}, isValid);
} else if (maintenance.strategy === "recurring-day-of-month") {
let dayOfMonthList = maintenance.getDayOfMonthList();
if (dayOfMonthList.length <= 0) {
log.debug("timeslot", "No day selected?");
return null;
}
const isValid = (startDateTime) => {
let day = parseInt(startDateTime.local().format("D"));
log.debug("timeslot", "day: " + day);
// Check 1-31
if (dayOfMonthList.includes(day)) {
return startDateTime;
}
// Check "lastDay1","lastDay2"...
let daysInMonth = startDateTime.daysInMonth();
let lastDayList = [];
// Small first, e.g. 28 > 29 > 30 > 31
for (let i = 4; i >= 1; i--) {
if (dayOfMonthList.includes("lastDay" + i)) {
lastDayList.push(daysInMonth - i + 1);
}
}
log.debug("timeslot", lastDayList);
return lastDayList.includes(day);
};
return await this.handleRecurringType(maintenance, minDate, (startDateTime) => {
while (true) {
startDateTime = startDateTime.add(1, "day");
if (isValid(startDateTime)) {
return startDateTime;
}
}
}, isValid);
} else {
throw new Error("Unknown maintenance strategy");
}
}
/**
* Generate a next timeslot for all recurring types
* @param maintenance
* @param minDate
* @param {function} nextDayCallback The logic how to get the next possible day
* @param {function} isValidCallback Check the day whether is matched the current strategy
* @returns {Promise<null|MaintenanceTimeslot>}
*/
static async handleRecurringType(maintenance, minDate, nextDayCallback, isValidCallback) {
let bean = R.dispense("maintenance_timeslot");
let duration = maintenance.getDuration();
let startDateTime = maintenance.getStartDateTime();
let endDateTime;
// Keep generating from the first possible date, until it is ok
while (true) {
log.debug("timeslot", "startDateTime: " + startDateTime.format());
// Handling out of effective date range
if (startDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
log.debug("timeslot", "Out of effective date range");
return null;
}
endDateTime = startDateTime.add(duration, "second");
// If endDateTime is out of effective date range, use the end datetime from effective date range
if (endDateTime.diff(dayjs.utc(maintenance.end_date)) > 0) {
endDateTime = dayjs.utc(maintenance.end_date);
}
// If minDate is set, the endDateTime must be bigger than it.
// And the endDateTime must be bigger current time
// Is valid under current recurring strategy
if (
(!minDate || endDateTime.diff(minDate) > 0) &&
endDateTime.diff(dayjs()) > 0 &&
isValidCallback(startDateTime)
) {
break;
}
startDateTime = nextDayCallback(startDateTime);
}
bean.maintenance_id = maintenance.id;
bean.start_date = localToUTC(startDateTime);
bean.end_date = localToUTC(endDateTime);
bean.generated_next = false;
return await R.store(bean);
}
}
module.exports = MaintenanceTimeslot;

View file

@ -2,8 +2,12 @@ const https = require("https");
const dayjs = require("dayjs");
const axios = require("axios");
const { Prometheus } = require("../prometheus");
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 } = require("../util-server");
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
SQL_DATETIME_FORMAT
} = 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");
@ -14,8 +18,8 @@ const apicache = require("../modules/apicache");
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");
const Gamedig = require("gamedig");
/**
* status:
@ -36,7 +40,6 @@ class Monitor extends BeanModel {
id: this.id,
name: this.name,
sendUrl: this.sendUrl,
maintenance: await Monitor.isUnderMaintenance(this.id),
};
if (this.sendUrl) {
@ -70,6 +73,7 @@ class Monitor extends BeanModel {
let data = {
id: this.id,
name: this.name,
description: this.description,
url: this.url,
method: this.method,
hostname: this.hostname,
@ -85,6 +89,7 @@ class Monitor extends BeanModel {
expiryNotification: this.isEnabledExpiryNotification(),
ignoreTls: this.getIgnoreTls(),
upsideDown: this.isUpsideDown(),
packetSize: this.packetSize,
maxredirects: this.maxredirects,
accepted_statuscodes: this.getAcceptedStatuscodes(),
dns_resolve_type: this.dns_resolve_type,
@ -107,6 +112,8 @@ class Monitor extends BeanModel {
grpcEnableTls: this.getGrpcEnableTls(),
radiusCalledStationId: this.radiusCalledStationId,
radiusCallingStationId: this.radiusCallingStationId,
game: this.game,
httpBodyEncoding: this.httpBodyEncoding
};
if (includeSensitiveData) {
@ -127,6 +134,9 @@ class Monitor extends BeanModel {
mqttPassword: this.mqttPassword,
authWorkstation: this.authWorkstation,
authDomain: this.authDomain,
tlsCa: this.tlsCa,
tlsCert: this.tlsCert,
tlsKey: this.tlsKey,
};
}
@ -139,7 +149,7 @@ class Monitor extends BeanModel {
* @returns {Promise<LooseObject<any>[]>}
*/
async getTags() {
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [ this.id ]);
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ? ORDER BY tag.name", [ this.id ]);
}
/**
@ -199,7 +209,7 @@ class Monitor extends BeanModel {
let previousBeat = null;
let retries = 0;
let prometheus = new Prometheus(this);
this.prometheus = new Prometheus(this);
const beat = async () => {
@ -268,17 +278,34 @@ class Monitor extends BeanModel {
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
let contentType = null;
let bodyValue = null;
if (this.body && (typeof this.body === "string" && this.body.trim().length > 0)) {
if (!this.httpBodyEncoding || this.httpBodyEncoding === "json") {
try {
bodyValue = JSON.parse(this.body);
contentType = "application/json";
} catch (e) {
throw new Error("Your JSON body is invalid. " + e.message);
}
} else if (this.httpBodyEncoding === "xml") {
bodyValue = this.body;
contentType = "text/xml; charset=utf-8";
}
}
// Axios Options
const options = {
url: this.url,
method: (this.method || "get").toLowerCase(),
...(this.body ? { data: JSON.parse(this.body) } : {}),
timeout: this.interval * 1000 * 0.8,
headers: {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"User-Agent": "Uptime-Kuma/" + version,
...(this.headers ? JSON.parse(this.headers) : {}),
...(contentType ? { "Content-Type": contentType } : {}),
...(basicAuthHeader),
...(this.headers ? JSON.parse(this.headers) : {})
},
maxRedirects: this.maxredirects,
validateStatus: (status) => {
@ -286,6 +313,10 @@ class Monitor extends BeanModel {
},
};
if (bodyValue) {
options.data = bodyValue;
}
if (this.proxy_id) {
const proxy = await R.load("proxy", this.proxy_id);
@ -304,6 +335,18 @@ class Monitor extends BeanModel {
options.httpsAgent = new https.Agent(httpsAgentOptions);
}
if (this.auth_method === "mtls") {
if (this.tlsCert !== null && this.tlsCert !== "") {
options.httpsAgent.options.cert = Buffer.from(this.tlsCert);
}
if (this.tlsCa !== null && this.tlsCa !== "") {
options.httpsAgent.options.ca = Buffer.from(this.tlsCa);
}
if (this.tlsKey !== null && this.tlsKey !== "") {
options.httpsAgent.options.key = Buffer.from(this.tlsKey);
}
}
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
log.debug("monitor", `[${this.name}] Axios Request`);
@ -357,7 +400,7 @@ class Monitor extends BeanModel {
bean.msg += ", keyword is found";
bean.status = UP;
} else {
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ");
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ").trim();
if (data.length > 50) {
data = data.substring(0, 47) + "...";
}
@ -372,7 +415,7 @@ class Monitor extends BeanModel {
bean.status = UP;
} else if (this.type === "ping") {
bean.ping = await ping(this.hostname);
bean.ping = await ping(this.hostname, this.packetSize);
bean.msg = "";
bean.status = UP;
} else if (this.type === "dns") {
@ -482,25 +525,44 @@ class Monitor extends BeanModel {
bean.msg = res.data.response.servers[0].name;
try {
bean.ping = await ping(this.hostname);
bean.ping = await ping(this.hostname, this.packetSize);
} catch (_) { }
} else {
throw new Error("Server not found on Steam");
}
} else if (this.type === "gamedig") {
try {
const state = await Gamedig.query({
type: this.game,
host: this.hostname,
port: this.port,
givenPortOnly: true,
});
bean.msg = state.name;
bean.status = UP;
bean.ping = state.ping;
} catch (e) {
throw new Error(e.message);
}
} else if (this.type === "docker") {
log.debug(`[${this.name}] Prepare Options for Axios`);
log.debug("monitor", `[${this.name}] Prepare Options for Axios`);
const dockerHost = await R.load("docker_host", this.docker_host);
const options = {
url: `/containers/${this.docker_container}/json`,
timeout: this.interval * 1000 * 0.8,
headers: {
"Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version,
},
httpsAgent: new https.Agent({
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: ! this.getIgnoreTls(),
rejectUnauthorized: !this.getIgnoreTls(),
}),
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
maxCachedSessions: 0,
}),
};
@ -510,7 +572,7 @@ class Monitor extends BeanModel {
options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
}
log.debug(`[${this.name}] Axios Request`);
log.debug("monitor", `[${this.name}] Axios Request`);
let res = await axios.request(options);
if (res.data.State.Running) {
bean.status = UP;
@ -576,11 +638,18 @@ class Monitor extends BeanModel {
} else if (this.type === "mysql") {
let startTime = dayjs().valueOf();
await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "mongodb") {
let startTime = dayjs().valueOf();
await mongodbPing(this.databaseConnectionString);
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "radius") {
let startTime = dayjs().valueOf();
@ -617,9 +686,23 @@ 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 if (this.type in UptimeKumaServer.monitorTypeList) {
let startTime = dayjs().valueOf();
const monitorType = UptimeKumaServer.monitorTypeList[this.type];
await monitorType.check(this, bean);
if (!bean.ping) {
bean.ping = dayjs().valueOf() - startTime;
}
} else {
bean.msg = "Unknown Monitor Type";
bean.status = PENDING;
throw new Error("Unknown Monitor Type");
}
if (this.isUpsideDown()) {
@ -709,7 +792,7 @@ class Monitor extends BeanModel {
await R.store(bean);
log.debug("monitor", `[${this.name}] prometheus.update`);
prometheus.update(bean, tlsInfo);
this.prometheus?.update(bean, tlsInfo);
previousBeat = bean;
@ -748,6 +831,13 @@ class Monitor extends BeanModel {
}
}
/**
* Make a request using axios
* @param {Object} options Options for Axios
* @param {boolean} finalCall Should this be the final call i.e
* don't retry on faliure
* @returns {Object} Axios response
*/
async makeAxiosRequest(options, finalCall = false) {
try {
let res;
@ -760,7 +850,6 @@ class Monitor extends BeanModel {
domain: this.authDomain,
workstation: this.authWorkstation ? this.authWorkstation : undefined
});
} else {
res = await axios.request(options);
}
@ -787,15 +876,15 @@ class Monitor extends BeanModel {
clearTimeout(this.heartbeatInterval);
this.isStop = true;
this.prometheus().remove();
this.prometheus?.remove();
}
/**
* Get a new prometheus instance
* @returns {Prometheus}
* Get prometheus instance
* @returns {Prometheus|undefined}
*/
prometheus() {
return new Prometheus(this);
getPrometheus() {
return this.prometheus;
}
/**
@ -1089,12 +1178,18 @@ class Monitor extends BeanModel {
for (let notification of notificationList) {
try {
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
const heartbeatJSON = bean.toJSON();
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
if (!heartbeatJSON["msg"]) {
heartbeatJSON["msg"] = "N/A";
}
// Also provide the time in server timezone
heartbeatJSON["timezone"] = await UptimeKumaServer.getInstance().getTimezone();
heartbeatJSON["timezoneOffset"] = UptimeKumaServer.getInstance().getTimezoneOffset();
heartbeatJSON["localDateTime"] = dayjs.utc(heartbeatJSON["time"]).tz(heartbeatJSON["timezone"]).format(SQL_DATETIME_FORMAT);
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), heartbeatJSON);
} catch (e) {
log.error("monitor", "Cannot send notification to " + notification.name);
@ -1167,11 +1262,11 @@ class Monitor extends BeanModel {
*/
async sendCertNotificationByTargetDays(certCN, certType, daysRemaining, targetDays, notificationList) {
let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days = ?", [
"certificate",
this.id,
targetDays,
]);
let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days <= ?", [
"certificate",
this.id,
targetDays,
]);
// Sent already, no need to send again
if (row) {
@ -1209,7 +1304,7 @@ class Monitor extends BeanModel {
*/
static async getPreviousHeartbeat(monitorID) {
return await R.getRow(`
SELECT status, time FROM heartbeat
SELECT ping, status, time FROM heartbeat
WHERE id = (select MAX(id) from heartbeat where monitor_id = ?)
`, [
monitorID
@ -1222,20 +1317,22 @@ class Monitor extends BeanModel {
* @returns {Promise<boolean>}
*/
static async isUnderMaintenance(monitorID) {
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
const maintenance = await R.getRow(`
SELECT COUNT(*) AS count
FROM monitor_maintenance mm
JOIN maintenance
ON mm.maintenance_id = maintenance.id
AND mm.monitor_id = ?
LEFT JOIN maintenance_timeslot
ON maintenance_timeslot.maintenance_id = maintenance.id
WHERE ${activeCondition}
LIMIT 1`, [ monitorID ]);
return maintenance.count !== 0;
const maintenanceIDList = await R.getCol(`
SELECT maintenance_id FROM monitor_maintenance
WHERE monitor_id = ?
`, [ monitorID ]);
for (const maintenanceID of maintenanceIDList) {
const maintenance = await UptimeKumaServer.getInstance().getMaintenance(maintenanceID);
if (maintenance && await maintenance.isUnderMaintenance()) {
return true;
}
}
return false;
}
/** Make sure monitor interval is between bounds */
validate() {
if (this.interval > MAX_INTERVAL_SECOND) {
throw new Error(`Interval cannot be more than ${MAX_INTERVAL_SECOND} seconds`);

View file

@ -3,7 +3,7 @@ const { R } = require("redbean-node");
const cheerio = require("cheerio");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const jsesc = require("jsesc");
const Maintenance = require("./maintenance");
const googleAnalytics = require("../google-analytics");
class StatusPage extends BeanModel {
@ -53,9 +53,17 @@ class StatusPage extends BeanModel {
const head = $("head");
if (statusPage.googleAnalyticsTagId) {
let escapedGoogleAnalyticsScript = googleAnalytics.getGoogleAnalyticsScript(statusPage.googleAnalyticsTagId);
head.append($(escapedGoogleAnalyticsScript));
}
// OG Meta Tags
head.append(`<meta property="og:title" content="${statusPage.title}" />`);
head.append(`<meta property="og:description" content="${description155}" />`);
let ogTitle = $("<meta property=\"og:title\" content=\"\" />").attr("content", statusPage.title);
head.append(ogTitle);
let ogDescription = $("<meta property=\"og:description\" content=\"\" />").attr("content", description155);
head.append(ogDescription);
// Preload data
// Add jsesc, fix https://github.com/louislam/uptime-kuma/issues/2186
@ -225,6 +233,7 @@ class StatusPage extends BeanModel {
customCSS: this.custom_css,
footerText: this.footer_text,
showPoweredBy: !!this.show_powered_by,
googleAnalyticsId: this.google_analytics_tag_id,
};
}
@ -245,6 +254,7 @@ class StatusPage extends BeanModel {
customCSS: this.custom_css,
footerText: this.footer_text,
showPoweredBy: !!this.show_powered_by,
googleAnalyticsId: this.google_analytics_tag_id,
};
}
@ -279,21 +289,17 @@ class StatusPage extends BeanModel {
try {
const publicMaintenanceList = [];
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(`
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 ]));
let maintenanceIDList = await R.getCol(`
SELECT DISTINCT maintenance_id
FROM maintenance_status_page
WHERE status_page_id = ?
`, [ statusPageId ]);
for (const bean of maintenanceBeanList) {
publicMaintenanceList.push(await bean.toPublicJSON());
for (const maintenanceID of maintenanceIDList) {
let maintenance = UptimeKumaServer.getInstance().getMaintenance(maintenanceID);
if (maintenance && await maintenance.isUnderMaintenance()) {
publicMaintenanceList.push(await maintenance.toPublicJSON());
}
}
return publicMaintenanceList;