Merge remote-tracking branch 'origin/master' into feat/monitor-list-improved-filtering

This commit is contained in:
Louis Lam
2023-07-16 21:04:46 +08:00
80 changed files with 4291 additions and 4034 deletions

View File

@@ -2,6 +2,7 @@ const basicAuth = require("express-basic-auth");
const passwordHash = require("./password-hash");
const { R } = require("redbean-node");
const { setting } = require("./util-server");
const { log } = require("../src/util");
const { loginRateLimiter, apiRateLimiter } = require("./rate-limiter");
const { Settings } = require("./settings");
const dayjs = require("dayjs");
@@ -81,12 +82,16 @@ function apiAuthorizer(username, password, callback) {
apiRateLimiter.pass(null, 0).then((pass) => {
if (pass) {
verifyAPIKey(password).then((valid) => {
if (!valid) {
log.warn("api-auth", "Failed API auth attempt: invalid API Key");
}
callback(null, valid);
// Only allow a set number of api requests per minute
// (currently set to 60)
apiRateLimiter.removeTokens(1);
});
} else {
log.warn("api-auth", "Failed API auth attempt: rate limit exceeded");
callback(null, false);
}
});
@@ -106,10 +111,12 @@ function userAuthorizer(username, password, callback) {
callback(null, user != null);
if (user == null) {
log.warn("basic-auth", "Failed basic auth attempt: invalid username/password");
loginRateLimiter.removeTokens(1);
}
});
} else {
log.warn("basic-auth", "Failed basic auth attempt: rate limit exceeded");
callback(null, false);
}
});

View File

@@ -1,27 +1,33 @@
const { setSetting, setting } = require("./util-server");
const axios = require("axios");
const compareVersions = require("compare-versions");
const { log } = require("../src/util");
exports.version = require("../package.json").version;
exports.latestVersion = null;
// How much time in ms to wait between update checks
const UPDATE_CHECKER_INTERVAL_MS = 1000 * 60 * 60 * 48;
const UPDATE_CHECKER_LATEST_VERSION_URL = "https://uptime.kuma.pet/version";
let interval;
/** Start 48 hour check interval */
exports.startInterval = () => {
let check = async () => {
if (await setting("checkUpdate") === false) {
return;
}
log.debug("update-checker", "Retrieving latest versions");
try {
const res = await axios.get("https://uptime.kuma.pet/version");
const res = await axios.get(UPDATE_CHECKER_LATEST_VERSION_URL);
// For debug
if (process.env.TEST_CHECK_VERSION === "1") {
res.data.slow = "1000.0.0";
}
if (await setting("checkUpdate") === false) {
return;
}
let checkBeta = await setting("checkBeta");
if (checkBeta && res.data.beta) {
@@ -35,12 +41,14 @@ exports.startInterval = () => {
exports.latestVersion = res.data.slow;
}
} catch (_) { }
} catch (_) {
log.info("update-checker", "Failed to check for new versions");
}
};
check();
interval = setInterval(check, 3600 * 1000 * 48);
interval = setInterval(check, UPDATE_CHECKER_INTERVAL_MS);
};
/**

View File

@@ -141,12 +141,21 @@ async function sendAPIKeyList(socket) {
/**
* Emits the version information to the client.
* @param {Socket} socket Socket.io socket instance
* @param {boolean} hideVersion
* @returns {Promise<void>}
*/
async function sendInfo(socket) {
async function sendInfo(socket, hideVersion = false) {
let version;
let latestVersion;
if (!hideVersion) {
version = checkVersion.version;
latestVersion = checkVersion.latestVersion;
}
socket.emit("info", {
version: checkVersion.version,
latestVersion: checkVersion.latestVersion,
version,
latestVersion,
primaryBaseURL: await setting("primaryBaseURL"),
serverTimezone: await server.getTimezone(),
serverTimezoneOffset: server.getTimezoneOffset(),

View File

@@ -1,4 +1,5 @@
const args = require("args-parser")(process.argv);
// Interop with browser
const args = (typeof process !== "undefined") ? require("args-parser")(process.argv) : {};
const demoMode = args["demo"] || false;
const badgeConstants = {

View File

@@ -3,7 +3,6 @@ const { R } = require("redbean-node");
const { setSetting, setting } = require("./util-server");
const { log, sleep } = require("../src/util");
const knex = require("knex");
const { PluginsManager } = require("./plugins-manager");
/**
* Database & App Data Folder
@@ -22,6 +21,8 @@ class Database {
*/
static uploadDir;
static screenshotDir;
static path;
/**
@@ -70,6 +71,8 @@ class Database {
"patch-monitor-tls.sql": true,
"patch-maintenance-cron.sql": true,
"patch-add-parent-monitor.sql": true,
"patch-add-invert-keyword.sql": true,
"patch-added-json-query.sql": true,
};
/**
@@ -88,12 +91,6 @@ class Database {
// Data Directory (must be end with "/")
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
// Plugin feature is working only if the dataDir = "./data";
if (Database.dataDir !== "./data/") {
log.warn("PLUGIN", "Warning: In order to enable plugin feature, you need to use the default data directory: ./data/");
PluginsManager.disable = true;
}
Database.path = Database.dataDir + "kuma.db";
if (! fs.existsSync(Database.dataDir)) {
fs.mkdirSync(Database.dataDir, { recursive: true });
@@ -105,6 +102,12 @@ class Database {
fs.mkdirSync(Database.uploadDir, { recursive: true });
}
// Create screenshot dir
Database.screenshotDir = Database.dataDir + "screenshots/";
if (! fs.existsSync(Database.screenshotDir)) {
fs.mkdirSync(Database.screenshotDir, { recursive: true });
}
log.info("db", `Data Dir: ${Database.dataDir}`);
}
@@ -161,12 +164,12 @@ class Database {
await R.exec("PRAGMA journal_mode = WAL");
}
await R.exec("PRAGMA cache_size = -12000");
await R.exec("PRAGMA auto_vacuum = FULL");
await R.exec("PRAGMA auto_vacuum = INCREMENTAL");
// 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
await R.exec("PRAGMA synchronous = FULL");
await R.exec("PRAGMA synchronous = NORMAL");
if (!noLog) {
log.info("db", "SQLite config:");

View File

@@ -1,24 +0,0 @@
const childProcess = require("child_process");
class Git {
static clone(repoURL, cwd, targetDir = ".") {
let result = childProcess.spawnSync("git", [
"clone",
repoURL,
targetDir,
], {
cwd: cwd,
});
if (result.status !== 0) {
throw new Error(result.stderr.toString("utf-8"));
} else {
return result.stdout.toString("utf-8") + result.stderr.toString("utf-8");
}
}
}
module.exports = {
Git,
};

View File

@@ -1,5 +1,6 @@
const { UptimeKumaServer } = require("./uptime-kuma-server");
const { clearOldData } = require("./jobs/clear-old-data");
const { incrementalVacuum } = require("./jobs/incremental-vacuum");
const Cron = require("croner");
const jobs = [
@@ -9,6 +10,12 @@ const jobs = [
jobFunc: clearOldData,
croner: null,
},
{
name: "incremental-vacuum",
interval: "*/5 * * * *",
jobFunc: incrementalVacuum,
croner: null,
}
];
/**

View File

@@ -39,6 +39,8 @@ const clearOldData = async () => {
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
[ parsedPeriod ]
);
await R.exec("PRAGMA optimize;");
} catch (e) {
log.error("clearOldData", `Failed to clear old data: ${e.message}`);
}

View File

@@ -0,0 +1,21 @@
const { R } = require("redbean-node");
const { log } = require("../../src/util");
/**
* Run incremental_vacuum and checkpoint the WAL.
* @return {Promise<void>} A promise that resolves when the process is finished.
*/
const incrementalVacuum = async () => {
try {
log.debug("incrementalVacuum", "Running incremental_vacuum and wal_checkpoint(PASSIVE)...");
await R.exec("PRAGMA incremental_vacuum(200)");
await R.exec("PRAGMA wal_checkpoint(PASSIVE)");
} catch (e) {
log.error("incrementalVacuum", `Failed: ${e.message}`);
}
};
module.exports = {
incrementalVacuum,
};

View File

@@ -20,6 +20,8 @@ const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
const { DockerHost } = require("../docker");
const { UptimeCacheList } = require("../uptime-cache-list");
const Gamedig = require("gamedig");
const jsonata = require("jsonata");
const jwt = require("jsonwebtoken");
/**
* status:
@@ -70,6 +72,12 @@ class Monitor extends BeanModel {
const tags = await this.getTags();
let screenshot = null;
if (this.type === "real-browser") {
screenshot = "/screenshots/" + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + ".png";
}
let data = {
id: this.id,
name: this.name,
@@ -90,6 +98,7 @@ class Monitor extends BeanModel {
retryInterval: this.retryInterval,
resendInterval: this.resendInterval,
keyword: this.keyword,
invertKeyword: this.isInvertKeyword(),
expiryNotification: this.isEnabledExpiryNotification(),
ignoreTls: this.getIgnoreTls(),
upsideDown: this.isUpsideDown(),
@@ -117,7 +126,10 @@ class Monitor extends BeanModel {
radiusCalledStationId: this.radiusCalledStationId,
radiusCallingStationId: this.radiusCallingStationId,
game: this.game,
httpBodyEncoding: this.httpBodyEncoding
httpBodyEncoding: this.httpBodyEncoding,
jsonPath: this.jsonPath,
expectedValue: this.expectedValue,
screenshot,
};
if (includeSensitiveData) {
@@ -199,6 +211,14 @@ class Monitor extends BeanModel {
return Boolean(this.upsideDown);
}
/**
* Parse to boolean
* @returns {boolean}
*/
isInvertKeyword() {
return Boolean(this.invertKeyword);
}
/**
* Parse to boolean
* @returns {boolean}
@@ -303,7 +323,7 @@ class Monitor extends BeanModel {
bean.msg = "Group empty";
}
} else if (this.type === "http" || this.type === "keyword") {
} else if (this.type === "http" || this.type === "keyword" || this.type === "json-query") {
// Do not do any queries/high loading things before the "bean.ping"
let startTime = dayjs().valueOf();
@@ -431,7 +451,7 @@ class Monitor extends BeanModel {
if (this.type === "http") {
bean.status = UP;
} else {
} else if (this.type === "keyword") {
let data = res.data;
@@ -440,17 +460,37 @@ class Monitor extends BeanModel {
data = JSON.stringify(data);
}
if (data.includes(this.keyword)) {
bean.msg += ", keyword is found";
let keywordFound = data.includes(this.keyword);
if (keywordFound === !this.isInvertKeyword()) {
bean.msg += ", keyword " + (keywordFound ? "is" : "not") + " found";
bean.status = UP;
} else {
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ").trim();
if (data.length > 50) {
data = data.substring(0, 47) + "...";
}
throw new Error(bean.msg + ", but keyword is not in [" + data + "]");
throw new Error(bean.msg + ", but keyword is " +
(keywordFound ? "present" : "not") + " in [" + data + "]");
}
} else if (this.type === "json-query") {
let data = res.data;
// convert data to object
if (typeof data === "string") {
data = JSON.parse(data);
}
let expression = jsonata(this.jsonPath);
let result = await expression.evaluate(data);
if (result.toString() === this.expectedValue) {
bean.msg += ", expected value is found";
bean.status = UP;
} else {
throw new Error(bean.msg + ", but value is not equal to expected value, value was: [" + result + "]");
}
}
} else if (this.type === "port") {
@@ -525,7 +565,7 @@ class Monitor extends BeanModel {
// No need to insert successful heartbeat for push type, so end here
retries = 0;
log.debug("monitor", `[${this.name}] timeout = ${timeout}`);
this.heartbeatInterval = setTimeout(beat, timeout);
this.heartbeatInterval = setTimeout(safeBeat, timeout);
return;
}
} else {
@@ -618,9 +658,15 @@ class Monitor extends BeanModel {
log.debug("monitor", `[${this.name}] Axios Request`);
let res = await axios.request(options);
if (res.data.State.Running) {
bean.status = UP;
bean.msg = res.data.State.Status;
if (res.data.State.Health && res.data.State.Health.Status !== "healthy") {
bean.status = PENDING;
bean.msg = res.data.State.Health.Status;
} else {
bean.status = UP;
bean.msg = res.data.State.Health ? res.data.State.Health.Status : res.data.State.Status;
}
} else {
throw Error("Container State is " + res.data.State.Status);
}
@@ -649,7 +695,6 @@ class Monitor extends BeanModel {
grpcEnableTls: this.grpcEnableTls,
grpcMethod: this.grpcMethod,
grpcBody: this.grpcBody,
keyword: this.keyword
};
const response = await grpcQuery(options);
bean.ping = dayjs().valueOf() - startTime;
@@ -662,13 +707,14 @@ class Monitor extends BeanModel {
bean.status = DOWN;
bean.msg = `Error in send gRPC ${response.code} ${response.errorMessage}`;
} else {
if (response.data.toString().includes(this.keyword)) {
let keywordFound = response.data.toString().includes(this.keyword);
if (keywordFound === !this.isInvertKeyword()) {
bean.status = UP;
bean.msg = `${responseData}, keyword [${this.keyword}] is found`;
bean.msg = `${responseData}, keyword [${this.keyword}] ${keywordFound ? "is" : "not"} found`;
} else {
log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is not in [" + ${response.data} + "]"`);
log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${response.data} + "]"`);
bean.status = DOWN;
bean.msg = `, but keyword [${this.keyword}] is not in [" + ${responseData} + "]`;
bean.msg = `, but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${responseData} + "]`;
}
}
} else if (this.type === "postgres") {
@@ -715,7 +761,8 @@ class Monitor extends BeanModel {
this.radiusCalledStationId,
this.radiusCallingStationId,
this.radiusSecret,
port
port,
this.interval * 1000 * 0.8,
);
if (resp.code) {
bean.msg = resp.code;
@@ -740,7 +787,7 @@ class Monitor extends BeanModel {
} else if (this.type in UptimeKumaServer.monitorTypeList) {
let startTime = dayjs().valueOf();
const monitorType = UptimeKumaServer.monitorTypeList[this.type];
await monitorType.check(this, bean);
await monitorType.check(this, bean, UptimeKumaServer.getInstance());
if (!bean.ping) {
bean.ping = dayjs().valueOf() - startTime;
}
@@ -1463,6 +1510,17 @@ class Monitor extends BeanModel {
return childrenIDs;
}
/**
* Unlinks all children of the the group monitor
* @param {number} groupID ID of group to remove children of
* @returns {Promise<void>}
*/
static async unlinkAllChildren(groupID) {
return await R.exec("UPDATE `monitor` SET parent = ? WHERE parent = ? ", [
null, groupID
]);
}
/**
* Checks recursive if parent (ancestors) are active
* @param {number} monitorID ID of the monitor to get

View File

@@ -6,9 +6,10 @@ class MonitorType {
*
* @param {Monitor} monitor
* @param {Heartbeat} heartbeat
* @param {UptimeKumaServer} server
* @returns {Promise<void>}
*/
async check(monitor, heartbeat) {
async check(monitor, heartbeat, server) {
throw new Error("You need to override check()");
}

View File

@@ -0,0 +1,212 @@
const { MonitorType } = require("./monitor-type");
const { chromium } = require("playwright-core");
const { UP, log } = require("../../src/util");
const { Settings } = require("../settings");
const commandExistsSync = require("command-exists").sync;
const childProcess = require("child_process");
const path = require("path");
const Database = require("../database");
const jwt = require("jsonwebtoken");
const config = require("../config");
let browser = null;
let allowedList = [];
let lastAutoDetectChromeExecutable = null;
if (process.platform === "win32") {
allowedList.push(process.env.LOCALAPPDATA + "\\Google\\Chrome\\Application\\chrome.exe");
allowedList.push(process.env.PROGRAMFILES + "\\Google\\Chrome\\Application\\chrome.exe");
allowedList.push(process.env["ProgramFiles(x86)"] + "\\Google\\Chrome\\Application\\chrome.exe");
// Allow Chromium too
allowedList.push(process.env.LOCALAPPDATA + "\\Chromium\\Application\\chrome.exe");
allowedList.push(process.env.PROGRAMFILES + "\\Chromium\\Application\\chrome.exe");
allowedList.push(process.env["ProgramFiles(x86)"] + "\\Chromium\\Application\\chrome.exe");
// For Loop A to Z
for (let i = 65; i <= 90; i++) {
let drive = String.fromCharCode(i);
allowedList.push(drive + ":\\Program Files\\Google\\Chrome\\Application\\chrome.exe");
allowedList.push(drive + ":\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe");
}
} else if (process.platform === "linux") {
allowedList = [
"chromium",
"chromium-browser",
"google-chrome",
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
"/usr/bin/google-chrome",
];
} else if (process.platform === "darwin") {
// TODO: Generated by GitHub Copilot, but not sure if it's correct
allowedList = [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
];
}
log.debug("chrome", allowedList);
async function isAllowedChromeExecutable(executablePath) {
console.log(config.args);
if (config.args["allow-all-chrome-exec"] || process.env.UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC === "1") {
return true;
}
// Check if the executablePath is in the list of allowed executables
return allowedList.includes(executablePath);
}
async function getBrowser() {
if (!browser) {
let executablePath = await Settings.get("chromeExecutable");
executablePath = await prepareChromeExecutable(executablePath);
browser = await chromium.launch({
//headless: false,
executablePath,
});
}
return browser;
}
async function prepareChromeExecutable(executablePath) {
// Special code for using the playwright_chromium
if (typeof executablePath === "string" && executablePath.toLocaleLowerCase() === "#playwright_chromium") {
// Set to undefined = use playwright_chromium
executablePath = undefined;
} else if (!executablePath) {
if (process.env.UPTIME_KUMA_IS_CONTAINER) {
executablePath = "/usr/bin/chromium";
// Install chromium in container via apt install
if ( !commandExistsSync(executablePath)) {
await new Promise((resolve, reject) => {
log.info("Chromium", "Installing Chromium...");
let child = childProcess.exec("apt update && apt --yes --no-install-recommends install chromium fonts-indic fonts-noto fonts-noto-cjk");
// On exit
child.on("exit", (code) => {
log.info("Chromium", "apt install chromium exited with code " + code);
if (code === 0) {
log.info("Chromium", "Installed Chromium");
let version = childProcess.execSync(executablePath + " --version").toString("utf8");
log.info("Chromium", "Chromium version: " + version);
resolve();
} else if (code === 100) {
reject(new Error("Installing Chromium, please wait..."));
} else {
reject(new Error("apt install chromium failed with code " + code));
}
});
});
}
} else {
executablePath = findChrome(allowedList);
}
} else {
// User specified a path
// Check if the executablePath is in the list of allowed
if (!await isAllowedChromeExecutable(executablePath)) {
throw new Error("This Chromium executable path is not allowed by default. If you are sure this is safe, please add an environment variable UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC=1 to allow it.");
}
}
return executablePath;
}
function findChrome(executables) {
// Use the last working executable, so we don't have to search for it again
if (lastAutoDetectChromeExecutable) {
if (commandExistsSync(lastAutoDetectChromeExecutable)) {
return lastAutoDetectChromeExecutable;
}
}
for (let executable of executables) {
if (commandExistsSync(executable)) {
lastAutoDetectChromeExecutable = executable;
return executable;
}
}
throw new Error("Chromium not found, please specify Chromium executable path in the settings page.");
}
async function resetChrome() {
if (browser) {
await browser.close();
browser = null;
}
}
/**
* Test if the chrome executable is valid and return the version
* @param executablePath
* @returns {Promise<string>}
*/
async function testChrome(executablePath) {
try {
executablePath = await prepareChromeExecutable(executablePath);
log.info("Chromium", "Testing Chromium executable: " + executablePath);
const browser = await chromium.launch({
executablePath,
});
const version = browser.version();
await browser.close();
return version;
} catch (e) {
throw new Error(e.message);
}
}
/**
* TODO: connect remote browser? https://playwright.dev/docs/api/class-browsertype#browser-type-connect
*
*/
class RealBrowserMonitorType extends MonitorType {
name = "real-browser";
async check(monitor, heartbeat, server) {
const browser = await getBrowser();
const context = await browser.newContext();
const page = await context.newPage();
const res = await page.goto(monitor.url, {
waitUntil: "networkidle",
timeout: monitor.interval * 1000 * 0.8,
});
let filename = jwt.sign(monitor.id, server.jwtSecret) + ".png";
await page.screenshot({
path: path.join(Database.screenshotDir, filename),
});
await context.close();
if (res.status() >= 200 && res.status() < 400) {
heartbeat.status = UP;
heartbeat.msg = res.status();
const timing = res.request().timing();
heartbeat.ping = timing.responseEnd;
} else {
throw new Error(res.status() + "");
}
}
}
module.exports = {
RealBrowserMonitorType,
testChrome,
resetChrome,
};

View File

@@ -11,7 +11,7 @@ class HomeAssistant extends NotificationProvider {
try {
await axios.post(
`${notification.homeAssistantUrl}/api/services/notify/${notificationService}`,
`${notification.homeAssistantUrl.trim().replace(/\/*$/, "")}/api/services/notify/${notificationService}`,
{
title: "Uptime Kuma",
message,

View File

@@ -27,6 +27,11 @@ class Slack extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
if (notification.slackchannelnotify) {
msg += " <!channel>";
}
try {
if (heartbeatJSON == null) {
let data = {
@@ -53,7 +58,7 @@ class Slack extends NotificationProvider {
"type": "header",
"text": {
"type": "plain_text",
"text": "Uptime Kuma Alert",
"text": textMsg,
},
},
{

View File

@@ -0,0 +1,42 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class SMSC extends NotificationProvider {
name = "smsc";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
let config = {
headers: {
"Content-Type": "application/json",
"Accept": "text/json",
}
};
let getArray = [
"fmt=3",
"translit=" + notification.smscTranslit,
"login=" + notification.smscLogin,
"psw=" + notification.smscPassword,
"phones=" + notification.smscToNumber,
"mes=" + encodeURIComponent(msg.replace(/[^\x00-\x7F]/g, "")),
];
if (notification.smscSenderName !== "") {
getArray.push("sender=" + notification.smscSenderName);
}
let resp = await axios.get("https://smsc.kz/sys/send.php?" + getArray.join("&"), config);
if (resp.data.id === undefined) {
let error = `Something gone wrong. Api returned code ${resp.data.error_code}: ${resp.data.error}`;
this.throwGeneralAxiosError(error);
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = SMSC;

View File

@@ -67,7 +67,7 @@ class SMTP extends NotificationProvider {
if (monitorJSON !== null) {
monitorName = monitorJSON["name"];
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword") {
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword" || monitorJSON["type"] === "json-query") {
monitorHostnameOrURL = monitorJSON["url"];
} else {
monitorHostnameOrURL = monitorJSON["hostname"];

View File

@@ -10,6 +10,7 @@ class Twilio extends NotificationProvider {
let okMsg = "Sent Successfully.";
let accountSID = notification.twilioAccountSID;
let apiKey = notification.twilioApiKey ? notification.twilioApiKey : accountSID;
let authToken = notification.twilioAuthToken;
try {
@@ -17,7 +18,7 @@ class Twilio extends NotificationProvider {
let config = {
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
"Authorization": "Basic " + Buffer.from(accountSID + ":" + authToken).toString("base64"),
"Authorization": "Basic " + Buffer.from(apiKey + ":" + authToken).toString("base64"),
}
};

View File

@@ -1,6 +1,7 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const FormData = require("form-data");
const { Liquid } = require("liquidjs");
class Webhook extends NotificationProvider {
@@ -15,17 +16,27 @@ class Webhook extends NotificationProvider {
monitor: monitorJSON,
msg,
};
let finalData;
let config = {
headers: {}
};
if (notification.webhookContentType === "form-data") {
finalData = new FormData();
finalData.append("data", JSON.stringify(data));
config.headers = finalData.getHeaders();
} else {
finalData = data;
const formData = new FormData();
formData.append("data", JSON.stringify(data));
config.headers = formData.getHeaders();
data = formData;
} else if (notification.webhookContentType === "custom") {
// Initialize LiquidJS and parse the custom Body Template
const engine = new Liquid();
const tpl = engine.parse(notification.webhookCustomBody);
// Insert templated values into Body
data = await engine.render(tpl,
{
msg,
heartbeatJSON,
monitorJSON
});
}
if (notification.webhookAdditionalHeaders) {
@@ -39,7 +50,7 @@ class Webhook extends NotificationProvider {
}
}
await axios.post(notification.webhookURL, finalData, config);
await axios.post(notification.webhookURL, data, config);
return okMsg;
} catch (error) {

View File

@@ -6,6 +6,7 @@ const AliyunSms = require("./notification-providers/aliyun-sms");
const Apprise = require("./notification-providers/apprise");
const Bark = require("./notification-providers/bark");
const ClickSendSMS = require("./notification-providers/clicksendsms");
const SMSC = require("./notification-providers/smsc");
const DingDing = require("./notification-providers/dingding");
const Discord = require("./notification-providers/discord");
const Feishu = require("./notification-providers/feishu");
@@ -68,6 +69,7 @@ class Notification {
new Apprise(),
new Bark(),
new ClickSendSMS(),
new SMSC(),
new DingDing(),
new Discord(),
new Feishu(),

View File

@@ -1,13 +0,0 @@
class Plugin {
async load() {
}
async unload() {
}
}
module.exports = {
Plugin,
};

View File

@@ -1,256 +0,0 @@
const fs = require("fs");
const { log } = require("../src/util");
const path = require("path");
const axios = require("axios");
const { Git } = require("./git");
const childProcess = require("child_process");
class PluginsManager {
static disable = false;
/**
* Plugin List
* @type {PluginWrapper[]}
*/
pluginList = [];
/**
* Plugins Dir
*/
pluginsDir;
server;
/**
*
* @param {UptimeKumaServer} server
*/
constructor(server) {
this.server = server;
if (!PluginsManager.disable) {
this.pluginsDir = "./data/plugins/";
if (! fs.existsSync(this.pluginsDir)) {
fs.mkdirSync(this.pluginsDir, { recursive: true });
}
log.debug("plugin", "Scanning plugin directory");
let list = fs.readdirSync(this.pluginsDir);
this.pluginList = [];
for (let item of list) {
this.loadPlugin(item);
}
} else {
log.warn("PLUGIN", "Skip scanning plugin directory");
}
}
/**
* Install a Plugin
*/
async loadPlugin(name) {
log.info("plugin", "Load " + name);
let plugin = new PluginWrapper(this.server, this.pluginsDir + name);
try {
await plugin.load();
this.pluginList.push(plugin);
} catch (e) {
log.error("plugin", "Failed to load plugin: " + this.pluginsDir + name);
log.error("plugin", "Reason: " + e.message);
}
}
/**
* Download a Plugin
* @param {string} repoURL Git repo url
* @param {string} name Directory name, also known as plugin unique name
*/
downloadPlugin(repoURL, name) {
if (fs.existsSync(this.pluginsDir + name)) {
log.info("plugin", "Plugin folder already exists? Removing...");
fs.rmSync(this.pluginsDir + name, {
recursive: true
});
}
log.info("plugin", "Installing plugin: " + name + " " + repoURL);
let result = Git.clone(repoURL, this.pluginsDir, name);
log.info("plugin", "Install result: " + result);
}
/**
* Remove a plugin
* @param {string} name
*/
async removePlugin(name) {
log.info("plugin", "Removing plugin: " + name);
for (let plugin of this.pluginList) {
if (plugin.info.name === name) {
await plugin.unload();
// Delete the plugin directory
fs.rmSync(this.pluginsDir + name, {
recursive: true
});
this.pluginList.splice(this.pluginList.indexOf(plugin), 1);
return;
}
}
log.warn("plugin", "Plugin not found: " + name);
throw new Error("Plugin not found: " + name);
}
/**
* TODO: Update a plugin
* Only available for plugins which were downloaded from the official list
* @param pluginID
*/
updatePlugin(pluginID) {
}
/**
* Get the plugin list from server + local installed plugin list
* Item will be merged if the `name` is the same.
* @returns {Promise<[]>}
*/
async fetchPluginList() {
let remotePluginList;
try {
const res = await axios.get("https://uptime.kuma.pet/c/plugins.json");
remotePluginList = res.data.pluginList;
} catch (e) {
log.error("plugin", "Failed to fetch plugin list: " + e.message);
remotePluginList = [];
}
for (let plugin of this.pluginList) {
let find = false;
// Try to merge
for (let remotePlugin of remotePluginList) {
if (remotePlugin.name === plugin.info.name) {
find = true;
remotePlugin.installed = true;
remotePlugin.name = plugin.info.name;
remotePlugin.fullName = plugin.info.fullName;
remotePlugin.description = plugin.info.description;
remotePlugin.version = plugin.info.version;
break;
}
}
// Local plugin
if (!find) {
plugin.info.local = true;
remotePluginList.push(plugin.info);
}
}
// Sort Installed first, then sort by name
return remotePluginList.sort((a, b) => {
if (a.installed === b.installed) {
if (a.fullName < b.fullName) {
return -1;
}
if (a.fullName > b.fullName) {
return 1;
}
return 0;
} else if (a.installed) {
return -1;
} else {
return 1;
}
});
}
}
class PluginWrapper {
server = undefined;
pluginDir = undefined;
/**
* Must be an `new-able` class.
* @type {function}
*/
pluginClass = undefined;
/**
*
* @type {Plugin}
*/
object = undefined;
info = {};
/**
*
* @param {UptimeKumaServer} server
* @param {string} pluginDir
*/
constructor(server, pluginDir) {
this.server = server;
this.pluginDir = pluginDir;
}
async load() {
let indexFile = this.pluginDir + "/index.js";
let packageJSON = this.pluginDir + "/package.json";
log.info("plugin", "Installing dependencies");
if (fs.existsSync(indexFile)) {
// Install dependencies
let result = childProcess.spawnSync("npm", [ "install" ], {
cwd: this.pluginDir,
env: {
...process.env,
PLAYWRIGHT_BROWSERS_PATH: "../../browsers", // Special handling for read-browser-monitor
}
});
if (result.stdout) {
log.info("plugin", "Install dependencies result: " + result.stdout.toString("utf-8"));
} else {
log.warn("plugin", "Install dependencies result: no output");
}
this.pluginClass = require(path.join(process.cwd(), indexFile));
let pluginClassType = typeof this.pluginClass;
if (pluginClassType === "function") {
this.object = new this.pluginClass(this.server);
await this.object.load();
} else {
throw new Error("Invalid plugin, it does not export a class");
}
if (fs.existsSync(packageJSON)) {
this.info = require(path.join(process.cwd(), packageJSON));
} else {
this.info.fullName = this.pluginDir;
this.info.name = "[unknown]";
this.info.version = "[unknown-version]";
}
this.info.installed = true;
log.info("plugin", `${this.info.fullName} v${this.info.version} loaded`);
}
}
async unload() {
await this.object.unload();
}
}
module.exports = {
PluginsManager,
PluginWrapper
};

View File

@@ -442,7 +442,7 @@ router.get("/api/badge/:id/cert-exp", cache("5 minutes"), async (request, respon
if (!tlsInfo.valid) {
// return a "Bad Cert" badge in naColor (grey), when cert is not valid
badgeValues.message = "Bad Cert";
badgeValues.color = badgeConstants.downColor;
badgeValues.color = downColor;
} else {
const daysRemaining = parseInt(overrideValue ?? tlsInfo.certInfo.daysRemaining);

View File

@@ -5,6 +5,8 @@ const StatusPage = require("../model/status_page");
const { allowDevAllOrigin, sendHttpError } = require("../util-server");
const { R } = require("redbean-node");
const Monitor = require("../model/monitor");
const { badgeConstants } = require("../config");
const { makeBadge } = require("badge-maker");
let router = express.Router();
@@ -139,4 +141,100 @@ router.get("/api/status-page/:slug/manifest.json", cache("1440 minutes"), async
}
});
// overall status-page status badge
router.get("/api/status-page/:slug/badge", cache("5 minutes"), async (request, response) => {
allowDevAllOrigin(response);
const slug = request.params.slug;
const statusPageID = await StatusPage.slugToID(slug);
const {
label,
upColor = badgeConstants.defaultUpColor,
downColor = badgeConstants.defaultDownColor,
partialColor = "#F6BE00",
maintenanceColor = "#808080",
style = badgeConstants.defaultStyle
} = request.query;
try {
let monitorIDList = await R.getCol(`
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
WHERE monitor_group.group_id = \`group\`.id
AND public = 1
AND \`group\`.status_page_id = ?
`, [
statusPageID
]);
let hasUp = false;
let hasDown = false;
let hasMaintenance = false;
for (let monitorID of monitorIDList) {
// retrieve the latest heartbeat
let beat = await R.getAll(`
SELECT * FROM heartbeat
WHERE monitor_id = ?
ORDER BY time DESC
LIMIT 1
`, [
monitorID,
]);
// to be sure, when corresponding monitor not found
if (beat.length === 0) {
continue;
}
// handle status of beat
if (beat[0].status === 3) {
hasMaintenance = true;
} else if (beat[0].status === 2) {
// ignored
} else if (beat[0].status === 1) {
hasUp = true;
} else {
hasDown = true;
}
}
const badgeValues = { style };
if (!hasUp && !hasDown && !hasMaintenance) {
// return a "N/A" badge in naColor (grey), if monitor is not public / not available / non exsitant
badgeValues.message = "N/A";
badgeValues.color = badgeConstants.naColor;
} else {
if (hasMaintenance) {
badgeValues.label = label ? label : "";
badgeValues.color = maintenanceColor;
badgeValues.message = "Maintenance";
} else if (hasUp && !hasDown) {
badgeValues.label = label ? label : "";
badgeValues.color = upColor;
badgeValues.message = "Up";
} else if (hasUp && hasDown) {
badgeValues.label = label ? label : "";
badgeValues.color = partialColor;
badgeValues.message = "Degraded";
} else {
badgeValues.label = label ? label : "";
badgeValues.color = downColor;
badgeValues.message = "Down";
}
}
// build the svg based on given values
const svg = makeBadge(badgeValues);
response.type("image/svg+xml");
response.send(svg);
} catch (error) {
sendHttpError(response, error.message);
}
});
module.exports = router;

View File

@@ -147,8 +147,8 @@ const { apiKeySocketHandler } = require("./socket-handlers/api-key-socket-handle
const { generalSocketHandler } = require("./socket-handlers/general-socket-handler");
const { Settings } = require("./settings");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
const { pluginsHandler } = require("./socket-handlers/plugins-handler");
const apicache = require("./modules/apicache");
const { resetChrome } = require("./monitor-types/real-browser-monitor-type");
app.use(express.json());
@@ -161,12 +161,6 @@ app.use(function (req, res, next) {
next();
});
/**
* Use for decode the auth object
* @type {null}
*/
let jwtSecret = null;
/**
* Show Setup Page
* @type {boolean}
@@ -177,7 +171,6 @@ let needSetup = false;
Database.init(args);
await initDatabase(testMode);
await server.initAfterDatabaseReady();
server.loadPlugins();
server.entryPage = await Settings.get("entryPage");
await StatusPage.loadDomainMappingList();
@@ -215,6 +208,7 @@ let needSetup = false;
});
if (isDev) {
app.use(express.urlencoded({ extended: true }));
app.post("/test-webhook", async (request, response) => {
log.debug("test", request.headers);
log.debug("test", request.body);
@@ -269,7 +263,7 @@ let needSetup = false;
log.info("server", "Adding socket handler");
io.on("connection", async (socket) => {
sendInfo(socket);
sendInfo(socket, true);
if (needSetup) {
log.info("server", "Redirect to setup page");
@@ -286,7 +280,7 @@ let needSetup = false;
log.info("auth", `Login by token. IP=${clientIP}`);
try {
let decoded = jwt.verify(token, jwtSecret);
let decoded = jwt.verify(token, server.jwtSecret);
log.info("auth", "Username from JWT: " + decoded.username);
@@ -357,7 +351,7 @@ let needSetup = false;
ok: true,
token: jwt.sign({
username: data.username,
}, jwtSecret),
}, server.jwtSecret),
});
}
@@ -387,7 +381,7 @@ let needSetup = false;
ok: true,
token: jwt.sign({
username: data.username,
}, jwtSecret),
}, server.jwtSecret),
});
} else {
@@ -676,6 +670,7 @@ let needSetup = false;
// Edit a monitor
socket.on("editMonitor", async (monitor, callback) => {
try {
let removeGroupChildren = false;
checkLogin(socket);
let bean = await R.findOne("monitor", " id = ? ", [ monitor.id ]);
@@ -684,7 +679,7 @@ let needSetup = false;
throw new Error("Permission denied.");
}
// Check if Parent is Decendant (would cause endless loop)
// Check if Parent is Descendant (would cause endless loop)
if (monitor.parent !== null) {
const childIDs = await Monitor.getAllChildrenIDs(monitor.id);
if (childIDs.includes(monitor.parent)) {
@@ -692,6 +687,11 @@ let needSetup = false;
}
}
// Remove children if monitor type has changed (from group to non-group)
if (bean.type === "group" && monitor.type !== bean.type) {
removeGroupChildren = true;
}
bean.name = monitor.name;
bean.description = monitor.description;
bean.parent = monitor.parent;
@@ -713,6 +713,7 @@ let needSetup = false;
bean.maxretries = monitor.maxretries;
bean.port = parseInt(monitor.port);
bean.keyword = monitor.keyword;
bean.invertKeyword = monitor.invertKeyword;
bean.ignoreTls = monitor.ignoreTls;
bean.expiryNotification = monitor.expiryNotification;
bean.upsideDown = monitor.upsideDown;
@@ -747,11 +748,17 @@ let needSetup = false;
bean.radiusCallingStationId = monitor.radiusCallingStationId;
bean.radiusSecret = monitor.radiusSecret;
bean.httpBodyEncoding = monitor.httpBodyEncoding;
bean.expectedValue = monitor.expectedValue;
bean.jsonPath = monitor.jsonPath;
bean.validate();
await R.store(bean);
if (removeGroupChildren) {
await Monitor.unlinkAllChildren(monitor.id);
}
await updateMonitorNotification(bean.id, monitor.notificationIDList);
if (bean.isActive()) {
@@ -897,6 +904,8 @@ let needSetup = false;
delete server.monitorList[monitorID];
}
const startTime = Date.now();
await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [
monitorID,
socket.userID,
@@ -905,6 +914,10 @@ let needSetup = false;
// Fix #2880
apicache.clear();
const endTime = Date.now();
log.info("DB", `Delete Monitor completed in : ${endTime - startTime} ms`);
callback({
ok: true,
msg: "Deleted Successfully.",
@@ -1148,6 +1161,8 @@ let needSetup = false;
await doubleCheckPassword(socket, currentPassword);
}
const previousChromeExecutable = await Settings.get("chromeExecutable");
await setSettings("general", data);
server.entryPage = data.entryPage;
@@ -1158,6 +1173,12 @@ let needSetup = false;
await server.setTimezone(data.serverTimezone);
}
// If Chrome Executable is changed, need to reset the browser
if (previousChromeExecutable !== data.chromeExecutable) {
log.info("settings", "Chrome executable is changed. Resetting Chrome...");
await resetChrome();
}
callback({
ok: true,
msg: "Saved"
@@ -1359,13 +1380,14 @@ let needSetup = false;
maxretries: monitorListData[i].maxretries,
port: monitorListData[i].port,
keyword: monitorListData[i].keyword,
invertKeyword: monitorListData[i].invertKeyword,
ignoreTls: monitorListData[i].ignoreTls,
upsideDown: monitorListData[i].upsideDown,
maxredirects: monitorListData[i].maxredirects,
accepted_statuscodes: monitorListData[i].accepted_statuscodes,
dns_resolve_type: monitorListData[i].dns_resolve_type,
dns_resolve_server: monitorListData[i].dns_resolve_server,
notificationIDList: {},
notificationIDList: monitorListData[i].notificationIDList,
proxy_id: monitorListData[i].proxy_id || null,
};
@@ -1527,7 +1549,6 @@ let needSetup = false;
maintenanceSocketHandler(socket);
apiKeySocketHandler(socket);
generalSocketHandler(socket, server);
pluginsHandler(socket, server);
log.debug("server", "added all socket handlers");
@@ -1630,6 +1651,7 @@ async function afterLogin(socket, user) {
socket.join(user.id);
let monitorList = await server.sendMonitorList(socket);
sendInfo(socket);
server.sendMaintenanceList(socket);
sendNotificationList(socket);
sendProxyList(socket);
@@ -1697,7 +1719,7 @@ async function initDatabase(testMode = false) {
needSetup = true;
}
jwtSecret = jwtSecretBean.value;
server.jwtSecret = jwtSecretBean.value;
}
/**

View File

@@ -3,6 +3,7 @@ const { Settings } = require("../settings");
const { sendInfo } = require("../client");
const { checkLogin } = require("../util-server");
const GameResolver = require("gamedig/lib/GameResolver");
const { testChrome } = require("../monitor-types/real-browser-monitor-type");
let gameResolver = new GameResolver();
let gameList = null;
@@ -47,4 +48,18 @@ module.exports.generalSocketHandler = (socket, server) => {
});
});
socket.on("testChrome", (executable, callback) => {
// Just noticed that await call could block the whole socket.io server!!! Use pure promise instead.
testChrome(executable).then((version) => {
callback({
ok: true,
msg: "Found Chromium/Chrome. Version: " + version,
});
}).catch((e) => {
callback({
ok: false,
msg: e.message,
});
});
});
};

View File

@@ -1,69 +0,0 @@
const { checkLogin } = require("../util-server");
const { PluginsManager } = require("../plugins-manager");
const { log } = require("../../src/util.js");
/**
* Handlers for plugins
* @param {Socket} socket Socket.io instance
* @param {UptimeKumaServer} server
*/
module.exports.pluginsHandler = (socket, server) => {
const pluginManager = server.getPluginManager();
// Get Plugin List
socket.on("getPluginList", async (callback) => {
try {
checkLogin(socket);
log.debug("plugin", "PluginManager.disable: " + PluginsManager.disable);
if (PluginsManager.disable) {
throw new Error("Plugin Disabled: In order to enable plugin feature, you need to use the default data directory: ./data/");
}
let pluginList = await pluginManager.fetchPluginList();
callback({
ok: true,
pluginList,
});
} catch (error) {
log.warn("plugin", "Error: " + error.message);
callback({
ok: false,
msg: error.message,
});
}
});
socket.on("installPlugin", async (repoURL, name, callback) => {
try {
checkLogin(socket);
pluginManager.downloadPlugin(repoURL, name);
await pluginManager.loadPlugin(name);
callback({
ok: true,
});
} catch (error) {
callback({
ok: false,
msg: error.message,
});
}
});
socket.on("uninstallPlugin", async (name, callback) => {
try {
checkLogin(socket);
await pluginManager.removePlugin(name);
callback({
ok: true,
});
} catch (error) {
callback({
ok: false,
msg: error.message,
});
}
});
};

View File

@@ -10,8 +10,7 @@ const util = require("util");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
const { Settings } = require("./settings");
const dayjs = require("dayjs");
const { PluginsManager } = require("./plugins-manager");
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
/**
* `module.exports` (alias: `server`) should be inside this class, in order to avoid circular dependency issue.
@@ -47,12 +46,6 @@ class UptimeKumaServer {
*/
indexHTML = "";
/**
* Plugins Manager
* @type {PluginsManager}
*/
pluginsManager = null;
/**
*
* @type {{}}
@@ -61,6 +54,12 @@ class UptimeKumaServer {
};
/**
* Use for decode the auth object
* @type {null}
*/
jwtSecret = null;
static getInstance(args) {
if (UptimeKumaServer.instance == null) {
UptimeKumaServer.instance = new UptimeKumaServer(args);
@@ -98,11 +97,17 @@ class UptimeKumaServer {
}
}
// Set Monitor Types
UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType();
this.io = new Server(this.httpServer);
}
/** Initialise app after the database has been set up */
async initAfterDatabaseReady() {
// Static
this.app.use("/screenshots", express.static(Database.screenshotDir));
await CacheableDnsHttpAgent.update();
process.env.TZ = await this.getTimezone();
@@ -244,9 +249,9 @@ class UptimeKumaServer {
return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null)
|| socket.client.conn.request.headers["x-real-ip"]
|| clientIP.replace(/^.*:/, "");
|| clientIP.replace(/^::ffff:/, "");
} else {
return clientIP.replace(/^.*:/, "");
return clientIP.replace(/^::ffff:/, "");
}
}
@@ -257,13 +262,43 @@ class UptimeKumaServer {
* @returns {Promise<string>}
*/
async getTimezone() {
// From process.env.TZ
try {
if (process.env.TZ) {
this.checkTimezone(process.env.TZ);
return process.env.TZ;
}
} catch (e) {
log.warn("timezone", e.message + " in process.env.TZ");
}
let timezone = await Settings.get("serverTimezone");
if (timezone) {
return timezone;
} else if (process.env.TZ) {
return process.env.TZ;
} else {
return dayjs.tz.guess();
// From Settings
try {
log.debug("timezone", "Using timezone from settings: " + timezone);
if (timezone) {
this.checkTimezone(timezone);
return timezone;
}
} catch (e) {
log.warn("timezone", e.message + " in settings");
}
// Guess
try {
let guess = dayjs.tz.guess();
log.debug("timezone", "Guessing timezone: " + guess);
if (guess) {
this.checkTimezone(guess);
return guess;
} else {
return "UTC";
}
} catch (e) {
// Guess failed, fall back to UTC
log.debug("timezone", "Guessed an invalid timezone. Use UTC as fallback");
return "UTC";
}
}
@@ -275,11 +310,24 @@ class UptimeKumaServer {
return dayjs().format("Z");
}
/**
* Throw an error if the timezone is invalid
* @param timezone
*/
checkTimezone(timezone) {
try {
dayjs.utc("2013-11-18 11:55").tz(timezone).format();
} catch (e) {
throw new Error("Invalid timezone:" + timezone);
}
}
/**
* Set the current server timezone and environment variables
* @param {string} timezone
*/
async setTimezone(timezone) {
this.checkTimezone(timezone);
await Settings.set("serverTimezone", timezone, "general");
process.env.TZ = timezone;
dayjs.tz.setDefault(timezone);
@@ -289,51 +337,11 @@ class UptimeKumaServer {
async stop() {
}
loadPlugins() {
this.pluginsManager = new PluginsManager(this);
}
/**
*
* @returns {PluginsManager}
*/
getPluginManager() {
return this.pluginsManager;
}
/**
*
* @param {MonitorType} monitorType
*/
addMonitorType(monitorType) {
if (monitorType instanceof MonitorType && monitorType.name) {
if (monitorType.name in UptimeKumaServer.monitorTypeList) {
log.error("", "Conflict Monitor Type name");
}
UptimeKumaServer.monitorTypeList[monitorType.name] = monitorType;
} else {
log.error("", "Invalid Monitor Type: " + monitorType.name);
}
}
/**
*
* @param {MonitorType} monitorType
*/
removeMonitorType(monitorType) {
if (UptimeKumaServer.monitorTypeList[monitorType.name] === monitorType) {
delete UptimeKumaServer.monitorTypeList[monitorType.name];
} else {
log.error("", "Remove MonitorType failed: " + monitorType.name);
}
}
}
module.exports = {
UptimeKumaServer
};
// Must be at the end
const { MonitorType } = require("./monitor-types/monitor-type");
// Must be at the end to avoid circular dependencies
const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor-type");

View File

@@ -378,6 +378,7 @@ exports.mongodbPing = async function (connectionString) {
* @param {string} callingStationId ID of calling station
* @param {string} secret Secret to use
* @param {number} [port=1812] Port to contact radius server on
* @param {number} [timeout=2500] Timeout for connection to use
* @returns {Promise<any>}
*/
exports.radius = function (
@@ -388,10 +389,12 @@ exports.radius = function (
callingStationId,
secret,
port = 1812,
timeout = 2500,
) {
const client = new radiusClient({
host: hostname,
hostPort: port,
timeout: timeout,
dictionaries: [ file ],
});
@@ -413,12 +416,18 @@ exports.radius = function (
exports.redisPingAsync = function (dsn) {
return new Promise((resolve, reject) => {
const client = redis.createClient({
url: dsn,
url: dsn
});
client.on("error", (err) => {
if (client.isOpen) {
client.disconnect();
}
reject(err);
});
client.connect().then(() => {
if (!client.isOpen) {
client.emit("error", new Error("connection isn't open"));
}
client.ping().then((res, err) => {
if (client.isOpen) {
client.disconnect();
@@ -428,7 +437,7 @@ exports.redisPingAsync = function (dsn) {
} else {
resolve(res);
}
});
}).catch(error => reject(error));
});
});
};