mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-08-09 09:29:31 +08:00
Merge branch 'master' into 1.23.X-merge-to-2.X.X
# Conflicts: # docker/debian-base.dockerfile # package-lock.json # server/database.js # server/model/monitor.js # server/uptime-kuma-server.js # server/util-server.js
This commit is contained in:
@@ -9,9 +9,9 @@ const dayjs = require("dayjs");
|
||||
|
||||
/**
|
||||
* Login to web app
|
||||
* @param {string} username
|
||||
* @param {string} password
|
||||
* @returns {Promise<(Bean|null)>}
|
||||
* @param {string} username Username to login with
|
||||
* @param {string} password Password to login with
|
||||
* @returns {Promise<(Bean|null)>} User or null if login failed
|
||||
*/
|
||||
exports.login = async function (username, password) {
|
||||
if (typeof username !== "string" || typeof password !== "string") {
|
||||
@@ -39,6 +39,7 @@ exports.login = async function (username, password) {
|
||||
/**
|
||||
* Validate a provided API key
|
||||
* @param {string} key API key to verify
|
||||
* @returns {boolean} API is ok?
|
||||
*/
|
||||
async function verifyAPIKey(key) {
|
||||
if (typeof key !== "string") {
|
||||
@@ -73,9 +74,10 @@ async function verifyAPIKey(key) {
|
||||
|
||||
/**
|
||||
* Custom authorizer for express-basic-auth
|
||||
* @param {string} username
|
||||
* @param {string} password
|
||||
* @param {authCallback} callback
|
||||
* @param {string} username Username to login with
|
||||
* @param {string} password Password to login with
|
||||
* @param {authCallback} callback Callback to handle login result
|
||||
* @returns {void}
|
||||
*/
|
||||
function apiAuthorizer(username, password, callback) {
|
||||
// API Rate Limit
|
||||
@@ -99,9 +101,10 @@ function apiAuthorizer(username, password, callback) {
|
||||
|
||||
/**
|
||||
* Custom authorizer for express-basic-auth
|
||||
* @param {string} username
|
||||
* @param {string} password
|
||||
* @param {authCallback} callback
|
||||
* @param {string} username Username to login with
|
||||
* @param {string} password Password to login with
|
||||
* @param {authCallback} callback Callback to handle login result
|
||||
* @returns {void}
|
||||
*/
|
||||
function userAuthorizer(username, password, callback) {
|
||||
// Login Rate Limit
|
||||
@@ -126,7 +129,8 @@ function userAuthorizer(username, password, callback) {
|
||||
* Use basic auth if auth is not disabled
|
||||
* @param {express.Request} req Express request object
|
||||
* @param {express.Response} res Express response object
|
||||
* @param {express.NextFunction} next
|
||||
* @param {express.NextFunction} next Next handler in chain
|
||||
* @returns {void}
|
||||
*/
|
||||
exports.basicAuth = async function (req, res, next) {
|
||||
const middleware = basicAuth({
|
||||
@@ -148,7 +152,8 @@ exports.basicAuth = async function (req, res, next) {
|
||||
* Use use API Key if API keys enabled, else use basic auth
|
||||
* @param {express.Request} req Express request object
|
||||
* @param {express.Response} res Express response object
|
||||
* @param {express.NextFunction} next
|
||||
* @param {express.NextFunction} next Next handler in chain
|
||||
* @returns {void}
|
||||
*/
|
||||
exports.apiAuth = async function (req, res, next) {
|
||||
if (!await Settings.get("disableAuth")) {
|
||||
|
@@ -15,6 +15,7 @@ class CacheableDnsHttpAgent {
|
||||
|
||||
/**
|
||||
* Register/Disable cacheable to global agents
|
||||
* @returns {void}
|
||||
*/
|
||||
static async update() {
|
||||
log.debug("CacheableDnsHttpAgent", "update");
|
||||
@@ -40,14 +41,15 @@ class CacheableDnsHttpAgent {
|
||||
/**
|
||||
* Attach cacheable to HTTP agent
|
||||
* @param {http.Agent} agent Agent to install
|
||||
* @returns {void}
|
||||
*/
|
||||
static install(agent) {
|
||||
this.cacheable.install(agent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @var {https.AgentOptions} agentOptions
|
||||
* @return {https.Agent}
|
||||
* @param {https.AgentOptions} agentOptions Options to pass to HTTPS agent
|
||||
* @returns {https.Agent} The new HTTPS agent
|
||||
*/
|
||||
static getHttpsAgent(agentOptions) {
|
||||
if (!this.enable) {
|
||||
@@ -63,8 +65,8 @@ class CacheableDnsHttpAgent {
|
||||
}
|
||||
|
||||
/**
|
||||
* @var {http.AgentOptions} agentOptions
|
||||
* @return {https.Agents}
|
||||
* @param {http.AgentOptions} agentOptions Options to pass to the HTTP agent
|
||||
* @returns {https.Agents} The new HTTP agent
|
||||
*/
|
||||
static getHttpAgent(agentOptions) {
|
||||
if (!this.enable) {
|
||||
|
@@ -12,7 +12,7 @@ const checkVersion = require("./check-version");
|
||||
/**
|
||||
* Send list of notification providers to client
|
||||
* @param {Socket} socket Socket.io socket instance
|
||||
* @returns {Promise<Bean[]>}
|
||||
* @returns {Promise<Bean[]>} List of notifications
|
||||
*/
|
||||
async function sendNotificationList(socket) {
|
||||
const timeLogger = new TimeLogger();
|
||||
@@ -40,13 +40,11 @@ async function sendNotificationList(socket) {
|
||||
* Send Heartbeat History list to socket
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @param {number} monitorID ID of monitor to send heartbeat history
|
||||
* @param {boolean} [toUser=false] True = send to all browsers with the same user id, False = send to the current browser only
|
||||
* @param {boolean} [overwrite=false] Overwrite client-side's heartbeat list
|
||||
* @param {boolean} toUser True = send to all browsers with the same user id, False = send to the current browser only
|
||||
* @param {boolean} overwrite Overwrite client-side's heartbeat list
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
|
||||
const timeLogger = new TimeLogger();
|
||||
|
||||
let list = await R.getAll(`
|
||||
SELECT * FROM heartbeat
|
||||
WHERE monitor_id = ?
|
||||
@@ -63,16 +61,14 @@ async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite =
|
||||
} else {
|
||||
socket.emit("heartbeatList", monitorID, result, overwrite);
|
||||
}
|
||||
|
||||
timeLogger.print(`[Monitor: ${monitorID}] sendHeartbeatList`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Important Heart beat list (aka event list)
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @param {number} monitorID ID of monitor to send heartbeat history
|
||||
* @param {boolean} [toUser=false] True = send to all browsers with the same user id, False = send to the current browser only
|
||||
* @param {boolean} [overwrite=false] Overwrite client-side's heartbeat list
|
||||
* @param {boolean} toUser True = send to all browsers with the same user id, False = send to the current browser only
|
||||
* @param {boolean} overwrite Overwrite client-side's heartbeat list
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function sendImportantHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
|
||||
@@ -100,7 +96,7 @@ async function sendImportantHeartbeatList(socket, monitorID, toUser = false, ove
|
||||
/**
|
||||
* Emit proxy list to client
|
||||
* @param {Socket} socket Socket.io socket instance
|
||||
* @return {Promise<Bean[]>}
|
||||
* @returns {Promise<Bean[]>} List of proxies
|
||||
*/
|
||||
async function sendProxyList(socket) {
|
||||
const timeLogger = new TimeLogger();
|
||||
@@ -141,7 +137,7 @@ async function sendAPIKeyList(socket) {
|
||||
/**
|
||||
* Emits the version information to the client.
|
||||
* @param {Socket} socket Socket.io socket instance
|
||||
* @param {boolean} hideVersion
|
||||
* @param {boolean} hideVersion Should we hide the version information in the response?
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function sendInfo(socket, hideVersion = false) {
|
||||
@@ -168,7 +164,7 @@ async function sendInfo(socket, hideVersion = false) {
|
||||
/**
|
||||
* Send list of docker hosts to client
|
||||
* @param {Socket} socket Socket.io socket instance
|
||||
* @returns {Promise<Bean[]>}
|
||||
* @returns {Promise<Bean[]>} List of docker hosts
|
||||
*/
|
||||
async function sendDockerHostList(socket) {
|
||||
const timeLogger = new TimeLogger();
|
||||
|
@@ -4,28 +4,48 @@ const { setSetting, setting } = require("./util-server");
|
||||
const { log, sleep } = require("../src/util");
|
||||
const knex = require("knex");
|
||||
const path = require("path");
|
||||
const { EmbeddedMariaDB } = require("./embedded-mariadb");
|
||||
const mysql = require("mysql2/promise");
|
||||
|
||||
/**
|
||||
* Database & App Data Folder
|
||||
*/
|
||||
class Database {
|
||||
|
||||
/**
|
||||
* Boostrap database for SQLite
|
||||
* @type {string}
|
||||
*/
|
||||
static templatePath = "./db/kuma.db";
|
||||
|
||||
/**
|
||||
* Data Dir (Default: ./data)
|
||||
* @type {string}
|
||||
*/
|
||||
static dataDir;
|
||||
|
||||
/**
|
||||
* User Upload Dir (Default: ./data/upload)
|
||||
* @type {string}
|
||||
*/
|
||||
static uploadDir;
|
||||
|
||||
/**
|
||||
* Chrome Screenshot Dir (Default: ./data/screenshots)
|
||||
* @type {string}
|
||||
*/
|
||||
static screenshotDir;
|
||||
|
||||
static path;
|
||||
/**
|
||||
* SQLite file path (Default: ./data/kuma.db)
|
||||
* @type {string}
|
||||
*/
|
||||
static sqlitePath;
|
||||
|
||||
/**
|
||||
* For storing Docker TLS certs (Default: ./data/docker-tls)
|
||||
* @type {string}
|
||||
*/
|
||||
static dockerTLSDir;
|
||||
|
||||
/**
|
||||
@@ -34,11 +54,13 @@ class Database {
|
||||
static patched = false;
|
||||
|
||||
/**
|
||||
* SQLite only
|
||||
* Add patch filename in key
|
||||
* Values:
|
||||
* true: Add it regardless of order
|
||||
* false: Do nothing
|
||||
* { parents: []}: Need parents before add it
|
||||
* @deprecated
|
||||
*/
|
||||
static patchList = {
|
||||
"patch-setting-value-type.sql": true,
|
||||
@@ -81,7 +103,7 @@ class Database {
|
||||
"patch-monitor-oauth-cc.sql": true,
|
||||
"patch-add-timeout-monitor.sql": true,
|
||||
"patch-add-gamedig-given-port.sql": true,
|
||||
"patch-notification-config.sql": true,
|
||||
"patch-notification-config.sql": true, // The last file so far converted to a knex migration file
|
||||
"patch-fix-kafka-producer-booleans.sql": true,
|
||||
};
|
||||
|
||||
@@ -93,15 +115,20 @@ class Database {
|
||||
|
||||
static noReject = true;
|
||||
|
||||
static dbConfig = {};
|
||||
|
||||
static knexMigrationsPath = "./db/knex_migrations";
|
||||
|
||||
/**
|
||||
* Initialize the database
|
||||
* @param {Object} args Arguments to initialize DB with
|
||||
* Initialize the data directory
|
||||
* @param {object} args Arguments to initialize DB with
|
||||
* @returns {void}
|
||||
*/
|
||||
static init(args) {
|
||||
static initDataDir(args) {
|
||||
// Data Directory (must be end with "/")
|
||||
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
|
||||
|
||||
Database.path = path.join(Database.dataDir, "kuma.db");
|
||||
Database.sqlitePath = path.join(Database.dataDir, "kuma.db");
|
||||
if (! fs.existsSync(Database.dataDir)) {
|
||||
fs.mkdirSync(Database.dataDir, { recursive: true });
|
||||
}
|
||||
@@ -123,39 +150,149 @@ class Database {
|
||||
fs.mkdirSync(Database.dockerTLSDir, { recursive: true });
|
||||
}
|
||||
|
||||
log.info("db", `Data Dir: ${Database.dataDir}`);
|
||||
log.info("server", `Data Dir: ${Database.dataDir}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the database config
|
||||
* @throws {Error} If the config is invalid
|
||||
* @typedef {string|undefined} envString
|
||||
* @returns {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} Database config
|
||||
*/
|
||||
static readDBConfig() {
|
||||
let dbConfig;
|
||||
|
||||
let dbConfigString = fs.readFileSync(path.join(Database.dataDir, "db-config.json")).toString("utf-8");
|
||||
dbConfig = JSON.parse(dbConfigString);
|
||||
|
||||
if (typeof dbConfig !== "object") {
|
||||
throw new Error("Invalid db-config.json, it must be an object");
|
||||
}
|
||||
|
||||
if (typeof dbConfig.type !== "string") {
|
||||
throw new Error("Invalid db-config.json, type must be a string");
|
||||
}
|
||||
return dbConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {string|undefined} envString
|
||||
* @param {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} dbConfig the database configuration that should be written
|
||||
* @returns {void}
|
||||
*/
|
||||
static writeDBConfig(dbConfig) {
|
||||
fs.writeFileSync(path.join(Database.dataDir, "db-config.json"), JSON.stringify(dbConfig, null, 4));
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the database
|
||||
* @param {boolean} [testMode=false] Should the connection be
|
||||
* started in test mode?
|
||||
* @param {boolean} [autoloadModels=true] Should models be
|
||||
* automatically loaded?
|
||||
* @param {boolean} [noLog=false] Should logs not be output?
|
||||
* @param {boolean} testMode Should the connection be started in test mode?
|
||||
* @param {boolean} autoloadModels Should models be automatically loaded?
|
||||
* @param {boolean} noLog Should logs not be output?
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async connect(testMode = false, autoloadModels = true, noLog = false) {
|
||||
const acquireConnectionTimeout = 120 * 1000;
|
||||
let dbConfig;
|
||||
try {
|
||||
dbConfig = this.readDBConfig();
|
||||
Database.dbConfig = dbConfig;
|
||||
} catch (err) {
|
||||
log.warn("db", err.message);
|
||||
dbConfig = {
|
||||
type: "sqlite",
|
||||
};
|
||||
}
|
||||
|
||||
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
|
||||
Dialect.prototype._driver = () => require("@louislam/sqlite3");
|
||||
let config = {};
|
||||
|
||||
let mariadbPoolConfig = {
|
||||
afterCreate: function (conn, done) {
|
||||
|
||||
const knexInstance = knex({
|
||||
client: Dialect,
|
||||
connection: {
|
||||
filename: Database.path,
|
||||
acquireConnectionTimeout: acquireConnectionTimeout,
|
||||
},
|
||||
useNullAsDefault: true,
|
||||
pool: {
|
||||
min: 1,
|
||||
max: 1,
|
||||
idleTimeoutMillis: 120 * 1000,
|
||||
propagateCreateError: false,
|
||||
acquireTimeoutMillis: acquireConnectionTimeout,
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
log.info("db", `Database Type: ${dbConfig.type}`);
|
||||
|
||||
if (dbConfig.type === "sqlite") {
|
||||
|
||||
if (! fs.existsSync(Database.sqlitePath)) {
|
||||
log.info("server", "Copying Database");
|
||||
fs.copyFileSync(Database.templatePath, Database.sqlitePath);
|
||||
}
|
||||
|
||||
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
|
||||
Dialect.prototype._driver = () => require("@louislam/sqlite3");
|
||||
|
||||
config = {
|
||||
client: Dialect,
|
||||
connection: {
|
||||
filename: Database.sqlitePath,
|
||||
acquireConnectionTimeout: acquireConnectionTimeout,
|
||||
},
|
||||
useNullAsDefault: true,
|
||||
pool: {
|
||||
min: 1,
|
||||
max: 1,
|
||||
idleTimeoutMillis: 120 * 1000,
|
||||
propagateCreateError: false,
|
||||
acquireTimeoutMillis: acquireConnectionTimeout,
|
||||
}
|
||||
};
|
||||
} else if (dbConfig.type === "mariadb") {
|
||||
if (!/^\w+$/.test(dbConfig.dbName)) {
|
||||
throw Error("Invalid database name. A database name can only consist of letters, numbers and underscores");
|
||||
}
|
||||
|
||||
const connection = await mysql.createConnection({
|
||||
host: dbConfig.hostname,
|
||||
port: dbConfig.port,
|
||||
user: dbConfig.username,
|
||||
password: dbConfig.password,
|
||||
});
|
||||
|
||||
await connection.execute("CREATE DATABASE IF NOT EXISTS " + dbConfig.dbName + " CHARACTER SET utf8mb4");
|
||||
connection.end();
|
||||
|
||||
config = {
|
||||
client: "mysql2",
|
||||
connection: {
|
||||
host: dbConfig.hostname,
|
||||
port: dbConfig.port,
|
||||
user: dbConfig.username,
|
||||
password: dbConfig.password,
|
||||
database: dbConfig.dbName,
|
||||
timezone: "+00:00",
|
||||
},
|
||||
pool: mariadbPoolConfig,
|
||||
};
|
||||
} else if (dbConfig.type === "embedded-mariadb") {
|
||||
let embeddedMariaDB = EmbeddedMariaDB.getInstance();
|
||||
await embeddedMariaDB.start();
|
||||
log.info("mariadb", "Embedded MariaDB started");
|
||||
config = {
|
||||
client: "mysql2",
|
||||
connection: {
|
||||
socketPath: embeddedMariaDB.socketPath,
|
||||
user: "node",
|
||||
database: "kuma",
|
||||
},
|
||||
pool: mariadbPoolConfig,
|
||||
};
|
||||
} else {
|
||||
throw new Error("Unknown Database type: " + dbConfig.type);
|
||||
}
|
||||
|
||||
// Set to utf8mb4 for MariaDB
|
||||
if (dbConfig.type.endsWith("mariadb")) {
|
||||
config.pool = {
|
||||
afterCreate(conn, done) {
|
||||
conn.query("SET CHARACTER SET utf8mb4;", (err) => done(err, conn));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const knexInstance = knex(config);
|
||||
|
||||
R.setup(knexInstance);
|
||||
|
||||
@@ -170,6 +307,19 @@ class Database {
|
||||
await R.autoloadModels("./server/model");
|
||||
}
|
||||
|
||||
if (dbConfig.type === "sqlite") {
|
||||
await this.initSQLite(testMode, noLog);
|
||||
} else if (dbConfig.type.endsWith("mariadb")) {
|
||||
await this.initMariaDB();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@param {boolean} testMode Should the connection be started in test mode?
|
||||
@param {boolean} noLog Should logs not be output?
|
||||
@returns {Promise<void>}
|
||||
*/
|
||||
static async initSQLite(testMode, noLog) {
|
||||
await R.exec("PRAGMA foreign_keys = ON");
|
||||
if (testMode) {
|
||||
// Change to MEMORY
|
||||
@@ -187,35 +337,94 @@ class Database {
|
||||
await R.exec("PRAGMA synchronous = NORMAL");
|
||||
|
||||
if (!noLog) {
|
||||
log.info("db", "SQLite config:");
|
||||
log.info("db", await R.getAll("PRAGMA journal_mode"));
|
||||
log.info("db", await R.getAll("PRAGMA cache_size"));
|
||||
log.info("db", "SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
|
||||
log.debug("db", "SQLite config:");
|
||||
log.debug("db", await R.getAll("PRAGMA journal_mode"));
|
||||
log.debug("db", await R.getAll("PRAGMA cache_size"));
|
||||
log.debug("db", "SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
|
||||
}
|
||||
}
|
||||
|
||||
/** Patch the database */
|
||||
/**
|
||||
* Initialize MariaDB
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async initMariaDB() {
|
||||
log.debug("db", "Checking if MariaDB database exists...");
|
||||
|
||||
let hasTable = await R.hasTable("docker_host");
|
||||
if (!hasTable) {
|
||||
const { createTables } = require("../db/knex_init_db");
|
||||
await createTables();
|
||||
} else {
|
||||
log.debug("db", "MariaDB database already exists");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch the database
|
||||
* @returns {void}
|
||||
*/
|
||||
static async patch() {
|
||||
// Still need to keep this for old versions of Uptime Kuma
|
||||
if (Database.dbConfig.type === "sqlite") {
|
||||
await this.patchSqlite();
|
||||
}
|
||||
|
||||
// Using knex migrations
|
||||
// https://knexjs.org/guide/migrations.html
|
||||
// https://gist.github.com/NigelEarle/70db130cc040cc2868555b29a0278261
|
||||
try {
|
||||
await R.knex.migrate.latest({
|
||||
directory: Database.knexMigrationsPath,
|
||||
});
|
||||
} catch (e) {
|
||||
// Allow missing patch files for downgrade or testing pr.
|
||||
if (e.message.includes("the following files are missing:")) {
|
||||
log.warn("db", e.message);
|
||||
log.warn("db", "Database migration failed, you may be downgrading Uptime Kuma.");
|
||||
} else {
|
||||
log.error("db", "Database migration failed");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async rollbackLatestPatch() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch the database for SQLite
|
||||
* @returns {Promise<void>}
|
||||
* @deprecated
|
||||
*/
|
||||
static async patchSqlite() {
|
||||
let version = parseInt(await setting("database_version"));
|
||||
|
||||
if (! version) {
|
||||
version = 0;
|
||||
}
|
||||
|
||||
log.info("db", "Your database version: " + version);
|
||||
log.info("db", "Latest database version: " + this.latestVersion);
|
||||
if (version !== this.latestVersion) {
|
||||
log.info("db", "Your database version: " + version);
|
||||
log.info("db", "Latest database version: " + this.latestVersion);
|
||||
}
|
||||
|
||||
if (version === this.latestVersion) {
|
||||
log.info("db", "Database patch not needed");
|
||||
log.debug("db", "Database patch not needed");
|
||||
} else if (version > this.latestVersion) {
|
||||
log.info("db", "Warning: Database version is newer than expected");
|
||||
log.warn("db", "Warning: Database version is newer than expected");
|
||||
} else {
|
||||
log.info("db", "Database patch is needed");
|
||||
|
||||
// Try catch anything here
|
||||
try {
|
||||
for (let i = version + 1; i <= this.latestVersion; i++) {
|
||||
const sqlFile = `./db/patch${i}.sql`;
|
||||
const sqlFile = `./db/old_migrations/patch${i}.sql`;
|
||||
log.info("db", `Patching ${sqlFile}`);
|
||||
await Database.importSQLFile(sqlFile);
|
||||
log.info("db", `Patched ${sqlFile}`);
|
||||
@@ -232,18 +441,19 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
await this.patch2();
|
||||
await this.patchSqlite2();
|
||||
await this.migrateNewStatusPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch DB using new process
|
||||
* Call it from patch() only
|
||||
* @deprecated
|
||||
* @private
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async patch2() {
|
||||
log.info("db", "Database Patch 2.0 Process");
|
||||
static async patchSqlite2() {
|
||||
log.debug("db", "Database Patch 2.0 Process");
|
||||
let databasePatchedFiles = await setting("databasePatchedFiles");
|
||||
|
||||
if (! databasePatchedFiles) {
|
||||
@@ -276,6 +486,7 @@ class Database {
|
||||
}
|
||||
|
||||
/**
|
||||
* SQlite only
|
||||
* Migrate status page value in setting to "status_page" table
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@@ -347,8 +558,8 @@ class Database {
|
||||
* Patch database using new patching process
|
||||
* Used it patch2() only
|
||||
* @private
|
||||
* @param sqlFilename
|
||||
* @param databasePatchedFiles
|
||||
* @param {string} sqlFilename Name of SQL file to load
|
||||
* @param {object} databasePatchedFiles Patch status of database files
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async patch2Recursion(sqlFilename, databasePatchedFiles) {
|
||||
@@ -372,7 +583,7 @@ class Database {
|
||||
|
||||
log.info("db", sqlFilename + " is patching");
|
||||
this.patched = true;
|
||||
await this.importSQLFile("./db/" + sqlFilename);
|
||||
await this.importSQLFile("./db/old_migrations/" + sqlFilename);
|
||||
databasePatchedFiles[sqlFilename] = true;
|
||||
log.info("db", sqlFilename + " was patched successfully");
|
||||
|
||||
@@ -383,7 +594,7 @@ class Database {
|
||||
|
||||
/**
|
||||
* Load an SQL file and execute it
|
||||
* @param filename Filename of SQL file to import
|
||||
* @param {string} filename Filename of SQL file to import
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async importSQLFile(filename) {
|
||||
@@ -415,14 +626,6 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aquire a direct connection to database
|
||||
* @returns {any}
|
||||
*/
|
||||
static getBetterSQLite3Database() {
|
||||
return R.knex.client.acquireConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Special handle, because tarn.js throw a promise reject that cannot be caught
|
||||
* @returns {Promise<void>}
|
||||
@@ -436,7 +639,9 @@ class Database {
|
||||
log.info("db", "Closing the database");
|
||||
|
||||
// Flush WAL to main database
|
||||
await R.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
||||
if (Database.dbConfig.type === "sqlite") {
|
||||
await R.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
||||
}
|
||||
|
||||
while (true) {
|
||||
Database.noReject = true;
|
||||
@@ -449,17 +654,23 @@ class Database {
|
||||
log.info("db", "Waiting to close the database");
|
||||
}
|
||||
}
|
||||
log.info("db", "SQLite closed");
|
||||
log.info("db", "Database closed");
|
||||
|
||||
process.removeListener("unhandledRejection", listener);
|
||||
}
|
||||
|
||||
/** Get the size of the database */
|
||||
/**
|
||||
* Get the size of the database (SQLite only)
|
||||
* @returns {number} Size of database
|
||||
*/
|
||||
static getSize() {
|
||||
log.debug("db", "Database.getSize()");
|
||||
let stats = fs.statSync(Database.path);
|
||||
log.debug("db", stats);
|
||||
return stats.size;
|
||||
if (Database.dbConfig.type === "sqlite") {
|
||||
log.debug("db", "Database.getSize()");
|
||||
let stats = fs.statSync(Database.sqlitePath);
|
||||
log.debug("db", stats);
|
||||
return stats.size;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -467,8 +678,22 @@ class Database {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async shrink() {
|
||||
await R.exec("VACUUM");
|
||||
if (Database.dbConfig.type === "sqlite") {
|
||||
await R.exec("VACUUM");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} Get the SQL for the current time plus a number of hours
|
||||
*/
|
||||
static sqlHourOffset() {
|
||||
if (Database.dbConfig.type === "sqlite") {
|
||||
return "DATETIME('now', ? || ' hours')";
|
||||
} else {
|
||||
return "DATE_ADD(NOW(), INTERVAL ? HOUR)";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Database;
|
||||
|
@@ -14,10 +14,10 @@ class DockerHost {
|
||||
|
||||
/**
|
||||
* Save a docker host
|
||||
* @param {Object} dockerHost Docker host to save
|
||||
* @param {object} dockerHost Docker host to save
|
||||
* @param {?number} dockerHostID ID of the docker host to update
|
||||
* @param {number} userID ID of the user who adds the docker host
|
||||
* @returns {Promise<Bean>}
|
||||
* @returns {Promise<Bean>} Updated docker host
|
||||
*/
|
||||
static async save(dockerHost, dockerHostID, userID) {
|
||||
let bean;
|
||||
@@ -64,7 +64,7 @@ class DockerHost {
|
||||
|
||||
/**
|
||||
* Fetches the amount of containers on the Docker host
|
||||
* @param {Object} dockerHost Docker host to check for
|
||||
* @param {object} dockerHost Docker host to check for
|
||||
* @returns {number} Total amount of containers on the host
|
||||
*/
|
||||
static async testDockerHost(dockerHost) {
|
||||
@@ -72,7 +72,6 @@ class DockerHost {
|
||||
url: "/containers/json?all=true",
|
||||
headers: {
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "Uptime-Kuma/" + version
|
||||
},
|
||||
};
|
||||
|
||||
@@ -108,6 +107,8 @@ class DockerHost {
|
||||
/**
|
||||
* Since axios 0.27.X, it does not accept `tcp://` protocol.
|
||||
* Change it to `http://` on the fly in order to fix it. (https://github.com/louislam/uptime-kuma/issues/2165)
|
||||
* @param {any} url URL to fix
|
||||
* @returns {any} URL with tcp:// replaced by http://
|
||||
*/
|
||||
static patchDockerURL(url) {
|
||||
if (typeof url === "string") {
|
||||
@@ -129,11 +130,10 @@ class DockerHost {
|
||||
* 'data/docker-tls/example.com/' would be searched for certificate files),
|
||||
* then 'ca.pem', 'key.pem' and 'cert.pem' files are included in the agent options.
|
||||
* File names can also be overridden via 'DOCKER_TLS_FILE_NAME_(CA|KEY|CERT)'.
|
||||
*
|
||||
* @param {String} dockerType i.e. "tcp" or "socket"
|
||||
* @param {String} url The docker host URL rewritten to https://
|
||||
* @return {Object}
|
||||
* */
|
||||
* @param {string} dockerType i.e. "tcp" or "socket"
|
||||
* @param {string} url The docker host URL rewritten to https://
|
||||
* @returns {object} HTTP agent options
|
||||
*/
|
||||
static getHttpsAgentOptions(dockerType, url) {
|
||||
let baseOptions = {
|
||||
maxCachedSessions: 0,
|
||||
|
176
server/embedded-mariadb.js
Normal file
176
server/embedded-mariadb.js
Normal file
@@ -0,0 +1,176 @@
|
||||
const { log } = require("../src/util");
|
||||
const childProcess = require("child_process");
|
||||
const fs = require("fs");
|
||||
const mysql = require("mysql2");
|
||||
|
||||
/**
|
||||
* It is only used inside the docker container
|
||||
*/
|
||||
class EmbeddedMariaDB {
|
||||
|
||||
static instance = null;
|
||||
|
||||
exec = "mariadbd";
|
||||
|
||||
mariadbDataDir = "/app/data/mariadb";
|
||||
|
||||
runDir = "/app/data/run/mariadb";
|
||||
|
||||
socketPath = this.runDir + "/mysqld.sock";
|
||||
|
||||
/**
|
||||
* @type {ChildProcessWithoutNullStreams}
|
||||
* @private
|
||||
*/
|
||||
childProcess = null;
|
||||
running = false;
|
||||
|
||||
started = false;
|
||||
|
||||
/**
|
||||
* @returns {EmbeddedMariaDB} The singleton instance
|
||||
*/
|
||||
static getInstance() {
|
||||
if (!EmbeddedMariaDB.instance) {
|
||||
EmbeddedMariaDB.instance = new EmbeddedMariaDB();
|
||||
}
|
||||
return EmbeddedMariaDB.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} If the singleton instance is created
|
||||
*/
|
||||
static hasInstance() {
|
||||
return !!EmbeddedMariaDB.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the embedded MariaDB
|
||||
* @returns {Promise<void>|void} A promise that resolves when the MariaDB is started or void if it is already started
|
||||
*/
|
||||
start() {
|
||||
if (this.childProcess) {
|
||||
log.info("mariadb", "Already started");
|
||||
return;
|
||||
}
|
||||
|
||||
this.initDB();
|
||||
|
||||
this.running = true;
|
||||
log.info("mariadb", "Starting Embedded MariaDB");
|
||||
this.childProcess = childProcess.spawn(this.exec, [
|
||||
"--user=node",
|
||||
"--datadir=" + this.mariadbDataDir,
|
||||
`--socket=${this.socketPath}`,
|
||||
`--pid-file=${this.runDir}/mysqld.pid`,
|
||||
]);
|
||||
|
||||
this.childProcess.on("close", (code) => {
|
||||
this.running = false;
|
||||
this.childProcess = null;
|
||||
this.started = false;
|
||||
log.info("mariadb", "Stopped Embedded MariaDB: " + code);
|
||||
|
||||
if (code !== 0) {
|
||||
log.info("mariadb", "Try to restart Embedded MariaDB as it is not stopped by user");
|
||||
this.start();
|
||||
}
|
||||
});
|
||||
|
||||
this.childProcess.on("error", (err) => {
|
||||
if (err.code === "ENOENT") {
|
||||
log.error("mariadb", `Embedded MariaDB: ${this.exec} is not found`);
|
||||
} else {
|
||||
log.error("mariadb", err);
|
||||
}
|
||||
});
|
||||
|
||||
let handler = (data) => {
|
||||
log.debug("mariadb", data.toString("utf-8"));
|
||||
if (data.toString("utf-8").includes("ready for connections")) {
|
||||
this.initDBAfterStarted();
|
||||
}
|
||||
};
|
||||
|
||||
this.childProcess.stdout.on("data", handler);
|
||||
this.childProcess.stderr.on("data", handler);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let interval = setInterval(() => {
|
||||
if (this.started) {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
} else {
|
||||
log.info("mariadb", "Waiting for Embedded MariaDB to start...");
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all the child processes
|
||||
* @returns {void}
|
||||
*/
|
||||
stop() {
|
||||
if (this.childProcess) {
|
||||
this.childProcess.kill("SIGINT");
|
||||
this.childProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install MariaDB if it is not installed and make sure the `runDir` directory exists
|
||||
* @returns {void}
|
||||
*/
|
||||
initDB() {
|
||||
if (!fs.existsSync(this.mariadbDataDir)) {
|
||||
log.info("mariadb", `Embedded MariaDB: ${this.mariadbDataDir} is not found, create one now.`);
|
||||
fs.mkdirSync(this.mariadbDataDir, {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
let result = childProcess.spawnSync("mysql_install_db", [
|
||||
"--user=node",
|
||||
"--ldata=" + this.mariadbDataDir,
|
||||
]);
|
||||
|
||||
if (result.status !== 0) {
|
||||
let error = result.stderr.toString("utf-8");
|
||||
log.error("mariadb", error);
|
||||
return;
|
||||
} else {
|
||||
log.info("mariadb", "Embedded MariaDB: mysql_install_db done:" + result.stdout.toString("utf-8"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(this.runDir)) {
|
||||
log.info("mariadb", `Embedded MariaDB: ${this.runDir} is not found, create one now.`);
|
||||
fs.mkdirSync(this.runDir, {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the "kuma" database in mariadb if it does not exist
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async initDBAfterStarted() {
|
||||
const connection = mysql.createConnection({
|
||||
socketPath: this.socketPath,
|
||||
user: "node",
|
||||
});
|
||||
|
||||
let result = await connection.execute("CREATE DATABASE IF NOT EXISTS `kuma`");
|
||||
log.debug("mariadb", "CREATE DATABASE: " + JSON.stringify(result));
|
||||
|
||||
log.info("mariadb", "Embedded MariaDB is ready for connections");
|
||||
this.started = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
EmbeddedMariaDB,
|
||||
};
|
@@ -3,8 +3,8 @@ const jsesc = require("jsesc");
|
||||
/**
|
||||
* Returns a string that represents the javascript that is required to insert the Google Analytics scripts
|
||||
* into a webpage.
|
||||
* @param tagId Google UA/G/AW/DC Property ID to use with the Google Analytics script.
|
||||
* @returns {string}
|
||||
* @param {string} tagId Google UA/G/AW/DC Property ID to use with the Google Analytics script.
|
||||
* @returns {string} HTML script tags to inject into page
|
||||
*/
|
||||
function getGoogleAnalyticsScript(tagId) {
|
||||
let escapedTagId = jsesc(tagId, { isScriptContext: true });
|
||||
|
@@ -10,7 +10,7 @@ let ImageDataURI = (() => {
|
||||
/**
|
||||
* Decode the data:image/ URI
|
||||
* @param {string} dataURI data:image/ URI to decode
|
||||
* @returns {?Object} An object with properties "imageType" and "dataBase64".
|
||||
* @returns {?object} An object with properties "imageType" and "dataBase64".
|
||||
* The former is the image type, e.g., "png", and the latter is a base64
|
||||
* encoded string of the image's binary data. If it fails to parse, returns
|
||||
* null instead of an object.
|
||||
@@ -52,8 +52,8 @@ let ImageDataURI = (() => {
|
||||
/**
|
||||
* Write data URI to file
|
||||
* @param {string} dataURI data:image/ URI
|
||||
* @param {string} [filePath] Path to write file to
|
||||
* @returns {Promise<string>}
|
||||
* @param {string} filePath Path to write file to
|
||||
* @returns {Promise<string|void>} Write file error
|
||||
*/
|
||||
function outputFile(dataURI, filePath) {
|
||||
filePath = filePath || "./";
|
||||
|
@@ -39,7 +39,10 @@ const initBackgroundJobs = async function () {
|
||||
|
||||
};
|
||||
|
||||
/** Stop all background jobs if running */
|
||||
/**
|
||||
* Stop all background jobs if running
|
||||
* @returns {void}
|
||||
*/
|
||||
const stopBackgroundJobs = function () {
|
||||
for (const job of jobs) {
|
||||
if (job.croner) {
|
||||
|
@@ -1,12 +1,13 @@
|
||||
const { R } = require("redbean-node");
|
||||
const { log } = require("../../src/util");
|
||||
const { setSetting, setting } = require("../util-server");
|
||||
const Database = require("../database");
|
||||
|
||||
const DEFAULT_KEEP_PERIOD = 180;
|
||||
|
||||
/**
|
||||
* Clears old data from the heartbeat table of the database.
|
||||
* @return {Promise<void>} A promise that resolves when the data has been cleared.
|
||||
* @returns {Promise<void>} A promise that resolves when the data has been cleared.
|
||||
*/
|
||||
|
||||
const clearOldData = async () => {
|
||||
@@ -34,13 +35,17 @@ const clearOldData = async () => {
|
||||
|
||||
log.debug("clearOldData", `Clearing Data older than ${parsedPeriod} days...`);
|
||||
|
||||
const sqlHourOffset = Database.sqlHourOffset();
|
||||
|
||||
try {
|
||||
await R.exec(
|
||||
"DELETE FROM heartbeat WHERE time < DATETIME('now', '-' || ? || ' days') ",
|
||||
[ parsedPeriod ]
|
||||
"DELETE FROM heartbeat WHERE time < " + sqlHourOffset,
|
||||
[ parsedPeriod * -24 ]
|
||||
);
|
||||
|
||||
await R.exec("PRAGMA optimize;");
|
||||
if (Database.dbConfig.type === "sqlite") {
|
||||
await R.exec("PRAGMA optimize;");
|
||||
}
|
||||
} catch (e) {
|
||||
log.error("clearOldData", `Failed to clear old data: ${e.message}`);
|
||||
}
|
||||
|
@@ -1,13 +1,19 @@
|
||||
const { R } = require("redbean-node");
|
||||
const { log } = require("../../src/util");
|
||||
const Database = require("../database");
|
||||
|
||||
/**
|
||||
* Run incremental_vacuum and checkpoint the WAL.
|
||||
* @return {Promise<void>} A promise that resolves when the process is finished.
|
||||
* @returns {Promise<void>} A promise that resolves when the process is finished.
|
||||
*/
|
||||
|
||||
const incrementalVacuum = async () => {
|
||||
try {
|
||||
if (Database.dbConfig.type !== "sqlite") {
|
||||
log.debug("incrementalVacuum", "Skipping incremental_vacuum, not using SQLite.");
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("incrementalVacuum", "Running incremental_vacuum and wal_checkpoint(PASSIVE)...");
|
||||
await R.exec("PRAGMA incremental_vacuum(200)");
|
||||
await R.exec("PRAGMA wal_checkpoint(PASSIVE)");
|
||||
|
@@ -19,7 +19,7 @@ class APIKey extends BeanModel {
|
||||
|
||||
/**
|
||||
* Returns an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
* @returns {object} Object ready to parse
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
@@ -37,7 +37,7 @@ class APIKey extends BeanModel {
|
||||
/**
|
||||
* Returns an object that ready to parse to JSON with sensitive fields
|
||||
* removed
|
||||
* @returns {Object}
|
||||
* @returns {object} Object ready to parse
|
||||
*/
|
||||
toPublicJSON() {
|
||||
return {
|
||||
@@ -53,9 +53,9 @@ class APIKey extends BeanModel {
|
||||
|
||||
/**
|
||||
* Create a new API Key and store it in the database
|
||||
* @param {Object} key Object sent by client
|
||||
* @param {object} key Object sent by client
|
||||
* @param {int} userID ID of socket user
|
||||
* @returns {Promise<bean>}
|
||||
* @returns {Promise<bean>} API key
|
||||
*/
|
||||
static async save(key, userID) {
|
||||
let bean;
|
||||
|
@@ -3,7 +3,7 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
class DockerHost extends BeanModel {
|
||||
/**
|
||||
* Returns an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
* @returns {object} Object ready to parse
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
|
@@ -4,10 +4,12 @@ const { R } = require("redbean-node");
|
||||
class Group extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
* @param {boolean} [showTags=false] Should the JSON include monitor tags
|
||||
* @returns {Object}
|
||||
* Return an object that ready to parse to JSON for public Only show
|
||||
* necessary data to public
|
||||
* @param {boolean} showTags Should the JSON include monitor tags
|
||||
* @param {boolean} certExpiry Should JSON include info about
|
||||
* certificate expiry?
|
||||
* @returns {object} Object ready to parse
|
||||
*/
|
||||
async toPublicJSON(showTags = false, certExpiry = false) {
|
||||
let monitorBeanList = await this.getMonitorList();
|
||||
@@ -27,7 +29,7 @@ class Group extends BeanModel {
|
||||
|
||||
/**
|
||||
* Get all monitors
|
||||
* @returns {Bean[]}
|
||||
* @returns {Bean[]} List of monitors
|
||||
*/
|
||||
async getMonitorList() {
|
||||
return R.convertToBeans("monitor", await R.getAll(`
|
||||
|
@@ -12,7 +12,7 @@ class Heartbeat extends BeanModel {
|
||||
/**
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
* @returns {Object}
|
||||
* @returns {object} Object ready to parse
|
||||
*/
|
||||
toPublicJSON() {
|
||||
return {
|
||||
@@ -25,7 +25,7 @@ class Heartbeat extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
* @returns {object} Object ready to parse
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
|
@@ -5,7 +5,7 @@ class Incident extends BeanModel {
|
||||
/**
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
* @returns {Object}
|
||||
* @returns {object} Object ready to parse
|
||||
*/
|
||||
toPublicJSON() {
|
||||
return {
|
||||
|
@@ -11,7 +11,7 @@ class Maintenance extends BeanModel {
|
||||
/**
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
* @returns {Object}
|
||||
* @returns {object} Object ready to parse
|
||||
*/
|
||||
async toPublicJSON() {
|
||||
|
||||
@@ -98,7 +98,7 @@ class Maintenance extends BeanModel {
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @param {string} timezone If not specified, the timeRange will be in UTC
|
||||
* @returns {Object}
|
||||
* @returns {object} Object ready to parse
|
||||
*/
|
||||
async toJSON(timezone = null) {
|
||||
return this.toPublicJSON(timezone);
|
||||
@@ -142,7 +142,7 @@ class Maintenance extends BeanModel {
|
||||
/**
|
||||
* Convert data from socket to bean
|
||||
* @param {Bean} bean Bean to fill in
|
||||
* @param {Object} obj Data to fill bean with
|
||||
* @param {object} obj Data to fill bean with
|
||||
* @returns {Bean} Filled bean
|
||||
*/
|
||||
static async jsonToBean(bean, obj) {
|
||||
@@ -188,7 +188,7 @@ class Maintenance extends BeanModel {
|
||||
|
||||
/**
|
||||
* Throw error if cron is invalid
|
||||
* @param cron
|
||||
* @param {string|Date} cron Pattern or date
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async validateCron(cron) {
|
||||
@@ -198,6 +198,8 @@ class Maintenance extends BeanModel {
|
||||
|
||||
/**
|
||||
* Run the cron
|
||||
* @param {boolean} throwError Should an error be thrown on failure
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async run(throwError = false) {
|
||||
if (this.beanMeta.job) {
|
||||
@@ -290,6 +292,10 @@ class Maintenance extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timeslots where maintenance is running
|
||||
* @returns {object|null} Maintenance time slot
|
||||
*/
|
||||
getRunningTimeslot() {
|
||||
let start = dayjs(this.beanMeta.job.nextRun(dayjs().add(-this.duration, "second").toDate()));
|
||||
let end = start.add(this.duration, "second");
|
||||
@@ -305,6 +311,10 @@ class Maintenance extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the maintenance
|
||||
* @returns {void}
|
||||
*/
|
||||
stop() {
|
||||
if (this.beanMeta.job) {
|
||||
this.beanMeta.job.stop();
|
||||
@@ -312,10 +322,18 @@ class Maintenance extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this maintenance currently active
|
||||
* @returns {boolean} The maintenance is active?
|
||||
*/
|
||||
async isUnderMaintenance() {
|
||||
return (await this.getStatus()) === "under-maintenance";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timezone of the maintenance
|
||||
* @returns {string} timezone
|
||||
*/
|
||||
async getTimezone() {
|
||||
if (!this.timezone || this.timezone === "SAME_AS_SERVER") {
|
||||
return await UptimeKumaServer.getInstance().getTimezone();
|
||||
@@ -323,10 +341,18 @@ class Maintenance extends BeanModel {
|
||||
return this.timezone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get offset for timezone
|
||||
* @returns {string} offset
|
||||
*/
|
||||
async getTimezoneOffset() {
|
||||
return dayjs.tz(dayjs(), await this.getTimezone()).format("Z");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current status of the maintenance
|
||||
* @returns {string} Current status
|
||||
*/
|
||||
async getStatus() {
|
||||
if (!this.active) {
|
||||
return "inactive";
|
||||
|
@@ -2,10 +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, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
|
||||
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
|
||||
SQL_DATETIME_FORMAT, isDev, sleep, getRandomInt
|
||||
} = require("../../src/util");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
|
||||
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
|
||||
redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
|
||||
} = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
@@ -18,10 +18,10 @@ const apicache = require("../modules/apicache");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
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");
|
||||
const { UptimeCalculator } = require("../uptime-calculator");
|
||||
|
||||
const rootCertificates = rootCertificatesFingerprints();
|
||||
|
||||
@@ -35,9 +35,12 @@ const rootCertificates = rootCertificatesFingerprints();
|
||||
class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
* @returns {Object}
|
||||
* Return an object that ready to parse to JSON for public Only show
|
||||
* necessary data to public
|
||||
* @param {boolean} showTags Include tags in JSON
|
||||
* @param {boolean} certExpiry Include certificate expiry info in
|
||||
* JSON
|
||||
* @returns {object} Object ready to parse
|
||||
*/
|
||||
async toPublicJSON(showTags = false, certExpiry = false) {
|
||||
let obj = {
|
||||
@@ -66,7 +69,9 @@ class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
* @param {boolean} includeSensitiveData Include sensitive data in
|
||||
* JSON
|
||||
* @returns {object} Object ready to parse
|
||||
*/
|
||||
async toJSON(includeSensitiveData = true) {
|
||||
|
||||
@@ -184,9 +189,9 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the monitor is active based on itself and its parents
|
||||
* @returns {Promise<Boolean>}
|
||||
*/
|
||||
* Checks if the monitor is active based on itself and its parents
|
||||
* @returns {Promise<boolean>} Is the monitor active?
|
||||
*/
|
||||
async isActive() {
|
||||
const parentActive = await Monitor.isParentActive(this.id);
|
||||
|
||||
@@ -195,7 +200,8 @@ class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Get all tags applied to this monitor
|
||||
* @returns {Promise<LooseObject<any>[]>}
|
||||
* @returns {Promise<LooseObject<any>[]>} List of tags on the
|
||||
* monitor
|
||||
*/
|
||||
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 = ? ORDER BY tag.name", [ this.id ]);
|
||||
@@ -204,7 +210,8 @@ class Monitor extends BeanModel {
|
||||
/**
|
||||
* Gets certificate expiry for this monitor
|
||||
* @param {number} monitorID ID of monitor to send
|
||||
* @returns {Promise<LooseObject<any>>}
|
||||
* @returns {Promise<LooseObject<any>>} Certificate expiry info for
|
||||
* monitor
|
||||
*/
|
||||
async getCertExpiry(monitorID) {
|
||||
let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||
@@ -229,7 +236,9 @@ class Monitor extends BeanModel {
|
||||
/**
|
||||
* Encode user and password to Base64 encoding
|
||||
* for HTTP "basic" auth, as per RFC-7617
|
||||
* @returns {string}
|
||||
* @param {string} user Username to encode
|
||||
* @param {string} pass Password to encode
|
||||
* @returns {string} Encoded username:password
|
||||
*/
|
||||
encodeBase64(user, pass) {
|
||||
return Buffer.from(user + ":" + pass).toString("base64");
|
||||
@@ -237,7 +246,7 @@ class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Is the TLS expiry notification enabled?
|
||||
* @returns {boolean}
|
||||
* @returns {boolean} Enabled?
|
||||
*/
|
||||
isEnabledExpiryNotification() {
|
||||
return Boolean(this.expiryNotification);
|
||||
@@ -245,7 +254,7 @@ class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Parse to boolean
|
||||
* @returns {boolean}
|
||||
* @returns {boolean} Should TLS errors be ignored?
|
||||
*/
|
||||
getIgnoreTls() {
|
||||
return Boolean(this.ignoreTls);
|
||||
@@ -253,7 +262,7 @@ class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Parse to boolean
|
||||
* @returns {boolean}
|
||||
* @returns {boolean} Is the monitor in upside down mode?
|
||||
*/
|
||||
isUpsideDown() {
|
||||
return Boolean(this.upsideDown);
|
||||
@@ -261,7 +270,7 @@ class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Parse to boolean
|
||||
* @returns {boolean}
|
||||
* @returns {boolean} Invert keyword match?
|
||||
*/
|
||||
isInvertKeyword() {
|
||||
return Boolean(this.invertKeyword);
|
||||
@@ -269,7 +278,7 @@ class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Parse to boolean
|
||||
* @returns {boolean}
|
||||
* @returns {boolean} Enable TLS for gRPC?
|
||||
*/
|
||||
getGrpcEnableTls() {
|
||||
return Boolean(this.grpcEnableTls);
|
||||
@@ -277,12 +286,16 @@ class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Get accepted status codes
|
||||
* @returns {Object}
|
||||
* @returns {object} Accepted status codes
|
||||
*/
|
||||
getAcceptedStatuscodes() {
|
||||
return JSON.parse(this.accepted_statuscodes_json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get if game dig should only use the port which was provided
|
||||
* @returns {boolean} gamedig should only use the provided port
|
||||
*/
|
||||
getGameDigGivenPortOnly() {
|
||||
return Boolean(this.gamedigGivenPortOnly);
|
||||
}
|
||||
@@ -306,6 +319,7 @@ class Monitor extends BeanModel {
|
||||
/**
|
||||
* Start monitor
|
||||
* @param {Server} io Socket server instance
|
||||
* @returns {void}
|
||||
*/
|
||||
start(io) {
|
||||
let previousBeat = null;
|
||||
@@ -360,13 +374,6 @@ class Monitor extends BeanModel {
|
||||
bean.status = flipStatus(bean.status);
|
||||
}
|
||||
|
||||
// Duration
|
||||
if (!isFirstBeat) {
|
||||
bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), "second");
|
||||
} else {
|
||||
bean.duration = 0;
|
||||
}
|
||||
|
||||
// Runtime patch timeout if it is 0
|
||||
// See https://github.com/louislam/uptime-kuma/pull/3961#issuecomment-1804149144
|
||||
if (this.timeout <= 0) {
|
||||
@@ -458,6 +465,9 @@ class Monitor extends BeanModel {
|
||||
} catch (e) {
|
||||
throw new Error("Your JSON body is invalid. " + e.message);
|
||||
}
|
||||
} else if (this.httpBodyEncoding === "form") {
|
||||
bodyValue = this.body;
|
||||
contentType = "application/x-www-form-urlencoded";
|
||||
} else if (this.httpBodyEncoding === "xml") {
|
||||
bodyValue = this.body;
|
||||
contentType = "text/xml; charset=utf-8";
|
||||
@@ -471,7 +481,6 @@ class Monitor extends BeanModel {
|
||||
timeout: this.timeout * 1000,
|
||||
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,
|
||||
...(contentType ? { "Content-Type": contentType } : {}),
|
||||
...(basicAuthHeader),
|
||||
...(oauth2AuthHeader),
|
||||
@@ -609,46 +618,6 @@ class Monitor extends BeanModel {
|
||||
bean.ping = await ping(this.hostname, this.packetSize);
|
||||
bean.msg = "";
|
||||
bean.status = UP;
|
||||
} else if (this.type === "dns") {
|
||||
let startTime = dayjs().valueOf();
|
||||
let dnsMessage = "";
|
||||
|
||||
let dnsRes = await dnsResolve(this.hostname, this.dns_resolve_server, this.port, this.dns_resolve_type);
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
if (this.dns_resolve_type === "A" || this.dns_resolve_type === "AAAA" || this.dns_resolve_type === "TXT" || this.dns_resolve_type === "PTR") {
|
||||
dnsMessage += "Records: ";
|
||||
dnsMessage += dnsRes.join(" | ");
|
||||
} else if (this.dns_resolve_type === "CNAME") {
|
||||
dnsMessage += dnsRes[0];
|
||||
} else if (this.dns_resolve_type === "CAA") {
|
||||
dnsMessage += dnsRes[0].issue;
|
||||
} else if (this.dns_resolve_type === "MX") {
|
||||
dnsRes.forEach(record => {
|
||||
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
|
||||
});
|
||||
dnsMessage = dnsMessage.slice(0, -2);
|
||||
} else if (this.dns_resolve_type === "NS") {
|
||||
dnsMessage += "Servers: ";
|
||||
dnsMessage += dnsRes.join(" | ");
|
||||
} else if (this.dns_resolve_type === "SOA") {
|
||||
dnsMessage += `NS-Name: ${dnsRes.nsname} | Hostmaster: ${dnsRes.hostmaster} | Serial: ${dnsRes.serial} | Refresh: ${dnsRes.refresh} | Retry: ${dnsRes.retry} | Expire: ${dnsRes.expire} | MinTTL: ${dnsRes.minttl}`;
|
||||
} else if (this.dns_resolve_type === "SRV") {
|
||||
dnsRes.forEach(record => {
|
||||
dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `;
|
||||
});
|
||||
dnsMessage = dnsMessage.slice(0, -2);
|
||||
}
|
||||
|
||||
if (this.dnsLastResult !== dnsMessage && dnsMessage !== undefined) {
|
||||
R.exec("UPDATE `monitor` SET dns_last_result = ? WHERE id = ? ", [
|
||||
dnsMessage,
|
||||
this.id
|
||||
]);
|
||||
}
|
||||
|
||||
bean.msg = dnsMessage;
|
||||
bean.status = UP;
|
||||
} else if (this.type === "push") { // Type: Push
|
||||
log.debug("monitor", `[${this.name}] Checking monitor at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
|
||||
const bufferTime = 1000; // 1s buffer to accommodate clock differences
|
||||
@@ -692,7 +661,6 @@ class Monitor extends BeanModel {
|
||||
timeout: this.timeout * 1000,
|
||||
headers: {
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "Uptime-Kuma/" + version,
|
||||
},
|
||||
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
|
||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||
@@ -744,7 +712,6 @@ class Monitor extends BeanModel {
|
||||
timeout: this.interval * 1000 * 0.8,
|
||||
headers: {
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "Uptime-Kuma/" + version,
|
||||
},
|
||||
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
|
||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||
@@ -1000,11 +967,17 @@ class Monitor extends BeanModel {
|
||||
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
|
||||
}
|
||||
|
||||
// Calculate uptime
|
||||
let uptimeCalculator = await UptimeCalculator.getUptimeCalculator(this.id);
|
||||
let endTimeDayjs = await uptimeCalculator.update(bean.status, parseFloat(bean.ping));
|
||||
bean.end_time = R.isoDateTimeMillis(endTimeDayjs);
|
||||
|
||||
// Send to frontend
|
||||
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);
|
||||
|
||||
// Store to database
|
||||
log.debug("monitor", `[${this.name}] Store`);
|
||||
await R.store(bean);
|
||||
|
||||
@@ -1015,7 +988,15 @@ class Monitor extends BeanModel {
|
||||
|
||||
if (! this.isStop) {
|
||||
log.debug("monitor", `[${this.name}] SetTimeout for next check.`);
|
||||
this.heartbeatInterval = setTimeout(safeBeat, beatInterval * 1000);
|
||||
|
||||
let intervalRemainingMs = Math.max(
|
||||
1,
|
||||
beatInterval * 1000 - dayjs().diff(dayjs.utc(bean.time))
|
||||
);
|
||||
|
||||
log.debug("monitor", `[${this.name}] Next heartbeat in: ${intervalRemainingMs}ms`);
|
||||
|
||||
this.heartbeatInterval = setTimeout(safeBeat, intervalRemainingMs);
|
||||
this.lastScheduleBeatTime = dayjs();
|
||||
} else {
|
||||
log.info("monitor", `[${this.name}] isStop = true, no next check.`);
|
||||
@@ -1023,7 +1004,10 @@ class Monitor extends BeanModel {
|
||||
|
||||
};
|
||||
|
||||
/** Get a heartbeat and handle errors */
|
||||
/**
|
||||
* Get a heartbeat and handle errors7
|
||||
* @returns {void}
|
||||
*/
|
||||
const safeBeat = async () => {
|
||||
try {
|
||||
this.lastStartBeatTime = dayjs();
|
||||
@@ -1056,10 +1040,10 @@ class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Make a request using axios
|
||||
* @param {Object} options Options for 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
|
||||
* don't retry on failure
|
||||
* @returns {object} Axios response
|
||||
*/
|
||||
async makeAxiosRequest(options, finalCall = false) {
|
||||
try {
|
||||
@@ -1094,7 +1078,10 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
/** Stop monitor */
|
||||
/**
|
||||
* Stop monitor
|
||||
* @returns {void}
|
||||
*/
|
||||
stop() {
|
||||
clearTimeout(this.heartbeatInterval);
|
||||
this.isStop = true;
|
||||
@@ -1104,7 +1091,7 @@ class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Get prometheus instance
|
||||
* @returns {Prometheus|undefined}
|
||||
* @returns {Prometheus|undefined} Current prometheus instance
|
||||
*/
|
||||
getPrometheus() {
|
||||
return this.prometheus;
|
||||
@@ -1114,7 +1101,7 @@ class Monitor extends BeanModel {
|
||||
* Helper Method:
|
||||
* returns URL object for further usage
|
||||
* returns null if url is invalid
|
||||
* @returns {(null|URL)}
|
||||
* @returns {(null|URL)} Monitor URL
|
||||
*/
|
||||
getUrl() {
|
||||
try {
|
||||
@@ -1126,7 +1113,7 @@ class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Example: http: or https:
|
||||
* @returns {(null|string)}
|
||||
* @returns {(null|string)} URL's protocol
|
||||
*/
|
||||
getURLProtocol() {
|
||||
const url = this.getUrl();
|
||||
@@ -1139,8 +1126,8 @@ class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Store TLS info to database
|
||||
* @param checkCertificateResult
|
||||
* @returns {Promise<Object>}
|
||||
* @param {object} checkCertificateResult Certificate to update
|
||||
* @returns {Promise<object>} Updated certificate
|
||||
*/
|
||||
async updateTlsInfo(checkCertificateResult) {
|
||||
let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||
@@ -1187,47 +1174,41 @@ class Monitor extends BeanModel {
|
||||
* @param {Server} io Socket server instance
|
||||
* @param {number} monitorID ID of monitor to send
|
||||
* @param {number} userID ID of user to send to
|
||||
* @returns {void}
|
||||
*/
|
||||
static async sendStats(io, monitorID, userID) {
|
||||
const hasClients = getTotalClientInRoom(io, userID) > 0;
|
||||
let uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitorID);
|
||||
|
||||
if (hasClients) {
|
||||
await Monitor.sendAvgPing(24, io, monitorID, userID);
|
||||
await Monitor.sendUptime(24, io, monitorID, userID);
|
||||
await Monitor.sendUptime(24 * 30, io, monitorID, userID);
|
||||
// Send 24 hour average ping
|
||||
let data24h = await uptimeCalculator.get24Hour();
|
||||
io.to(userID).emit("avgPing", monitorID, (data24h.avgPing) ? Number(data24h.avgPing.toFixed(2)) : null);
|
||||
|
||||
// Send 24 hour uptime
|
||||
io.to(userID).emit("uptime", monitorID, 24, data24h.uptime);
|
||||
|
||||
// Send 30 day uptime
|
||||
let data30d = await uptimeCalculator.get30Day();
|
||||
io.to(userID).emit("uptime", monitorID, 720, data30d.uptime);
|
||||
|
||||
// Send 1-year uptime
|
||||
let data1y = await uptimeCalculator.get1Year();
|
||||
io.to(userID).emit("uptime", monitorID, "1y", data1y.uptime);
|
||||
|
||||
// Send Cert Info
|
||||
await Monitor.sendCertInfo(io, monitorID, userID);
|
||||
} else {
|
||||
log.debug("monitor", "No clients in the room, no need to send stats");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the average ping to user
|
||||
* @param {number} duration Hours
|
||||
*/
|
||||
static async sendAvgPing(duration, io, monitorID, userID) {
|
||||
const timeLogger = new TimeLogger();
|
||||
|
||||
let avgPing = parseInt(await R.getCell(`
|
||||
SELECT AVG(ping)
|
||||
FROM heartbeat
|
||||
WHERE time > DATETIME('now', ? || ' hours')
|
||||
AND ping IS NOT NULL
|
||||
AND monitor_id = ? `, [
|
||||
-duration,
|
||||
monitorID,
|
||||
]));
|
||||
|
||||
timeLogger.print(`[Monitor: ${monitorID}] avgPing`);
|
||||
|
||||
io.to(userID).emit("avgPing", monitorID, avgPing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send certificate information to client
|
||||
* @param {Server} io Socket server instance
|
||||
* @param {number} monitorID ID of monitor to send
|
||||
* @param {number} userID ID of user to send to
|
||||
* @returns {void}
|
||||
*/
|
||||
static async sendCertInfo(io, monitorID, userID) {
|
||||
let tlsInfo = await R.findOne("monitor_tls_info", "monitor_id = ?", [
|
||||
@@ -1238,98 +1219,6 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uptime with calculation
|
||||
* Calculation based on:
|
||||
* https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
|
||||
* @param {number} duration Hours
|
||||
* @param {number} monitorID ID of monitor to calculate
|
||||
*/
|
||||
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"));
|
||||
|
||||
// Handle if heartbeat duration longer than the target duration
|
||||
// e.g. If the last beat's duration is bigger that the 24hrs window, it will use the duration between the (beat time - window margin) (THEN case in SQL)
|
||||
let result = await R.getRow(`
|
||||
SELECT
|
||||
-- SUM all duration, also trim off the beat out of time window
|
||||
SUM(
|
||||
CASE
|
||||
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
|
||||
THEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400
|
||||
ELSE duration
|
||||
END
|
||||
) AS total_duration,
|
||||
|
||||
-- SUM all uptime duration, also trim off the beat out of time window
|
||||
SUM(
|
||||
CASE
|
||||
WHEN (status = 1 OR status = 3)
|
||||
THEN
|
||||
CASE
|
||||
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
|
||||
THEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400
|
||||
ELSE duration
|
||||
END
|
||||
END
|
||||
) AS uptime_duration
|
||||
FROM heartbeat
|
||||
WHERE time > ?
|
||||
AND monitor_id = ?
|
||||
`, [
|
||||
startTime, startTime, startTime, startTime, startTime,
|
||||
monitorID,
|
||||
]);
|
||||
|
||||
timeLogger.print(`[Monitor: ${monitorID}][${duration}] sendUptime`);
|
||||
|
||||
let totalDuration = result.total_duration;
|
||||
let uptimeDuration = result.uptime_duration;
|
||||
let uptime = 0;
|
||||
|
||||
if (totalDuration > 0) {
|
||||
uptime = uptimeDuration / totalDuration;
|
||||
if (uptime < 0) {
|
||||
uptime = 0;
|
||||
}
|
||||
|
||||
} else {
|
||||
// Handle new monitor with only one beat, because the beat's duration = 0
|
||||
let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [ monitorID ]));
|
||||
|
||||
if (status === UP) {
|
||||
uptime = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache
|
||||
UptimeCacheList.addUptime(monitorID, duration, uptime);
|
||||
|
||||
return uptime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Uptime
|
||||
* @param {number} duration Hours
|
||||
* @param {Server} io Socket server instance
|
||||
* @param {number} monitorID ID of monitor to send
|
||||
* @param {number} userID ID of user to send to
|
||||
*/
|
||||
static async sendUptime(duration, io, monitorID, userID) {
|
||||
const uptime = await this.calcUptime(duration, monitorID);
|
||||
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Has status of monitor changed since last beat?
|
||||
* @param {boolean} isFirstBeat Is this the first beat of this monitor?
|
||||
@@ -1398,6 +1287,7 @@ class Monitor extends BeanModel {
|
||||
* @param {boolean} isFirstBeat Is this beat the first of this monitor?
|
||||
* @param {Monitor} monitor The monitor to send a notificaton about
|
||||
* @param {Bean} bean Status information about monitor
|
||||
* @returns {void}
|
||||
*/
|
||||
static async sendNotification(isFirstBeat, monitor, bean) {
|
||||
if (!isFirstBeat || bean.status === DOWN) {
|
||||
@@ -1438,7 +1328,7 @@ class Monitor extends BeanModel {
|
||||
/**
|
||||
* Get list of notification providers for a given monitor
|
||||
* @param {Monitor} monitor Monitor to get notification providers for
|
||||
* @returns {Promise<LooseObject<any>[]>}
|
||||
* @returns {Promise<LooseObject<any>[]>} List of notifications
|
||||
*/
|
||||
static async getNotificationList(monitor) {
|
||||
let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [
|
||||
@@ -1449,7 +1339,8 @@ class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* checks certificate chain for expiring certificates
|
||||
* @param {Object} tlsInfoObject Information about certificate
|
||||
* @param {object} tlsInfoObject Information about certificate
|
||||
* @returns {void}
|
||||
*/
|
||||
async checkCertExpiryNotifications(tlsInfoObject) {
|
||||
if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) {
|
||||
@@ -1539,7 +1430,7 @@ class Monitor extends BeanModel {
|
||||
/**
|
||||
* Get the status of the previous heartbeat
|
||||
* @param {number} monitorID ID of monitor to check
|
||||
* @returns {Promise<LooseObject<any>>}
|
||||
* @returns {Promise<LooseObject<any>>} Previous heartbeat
|
||||
*/
|
||||
static async getPreviousHeartbeat(monitorID) {
|
||||
return await R.getRow(`
|
||||
@@ -1553,7 +1444,7 @@ class Monitor extends BeanModel {
|
||||
/**
|
||||
* Check if monitor is under maintenance
|
||||
* @param {number} monitorID ID of monitor to check
|
||||
* @returns {Promise<boolean>}
|
||||
* @returns {Promise<boolean>} Is the monitor under maintenance
|
||||
*/
|
||||
static async isUnderMaintenance(monitorID) {
|
||||
const maintenanceIDList = await R.getCol(`
|
||||
@@ -1576,7 +1467,11 @@ class Monitor extends BeanModel {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Make sure monitor interval is between bounds */
|
||||
/**
|
||||
* Make sure monitor interval is between bounds
|
||||
* @returns {void}
|
||||
* @throws Interval is outside of range
|
||||
*/
|
||||
validate() {
|
||||
if (this.interval > MAX_INTERVAL_SECOND) {
|
||||
throw new Error(`Interval cannot be more than ${MAX_INTERVAL_SECOND} seconds`);
|
||||
@@ -1589,7 +1484,7 @@ class Monitor extends BeanModel {
|
||||
/**
|
||||
* Gets Parent of the monitor
|
||||
* @param {number} monitorID ID of monitor to get
|
||||
* @returns {Promise<LooseObject<any>>}
|
||||
* @returns {Promise<LooseObject<any>>} Parent
|
||||
*/
|
||||
static async getParent(monitorID) {
|
||||
return await R.getRow(`
|
||||
@@ -1605,7 +1500,7 @@ class Monitor extends BeanModel {
|
||||
/**
|
||||
* Gets all Children of the monitor
|
||||
* @param {number} monitorID ID of monitor to get
|
||||
* @returns {Promise<LooseObject<any>>}
|
||||
* @returns {Promise<LooseObject<any>>} Children
|
||||
*/
|
||||
static async getChildren(monitorID) {
|
||||
return await R.getAll(`
|
||||
@@ -1618,7 +1513,7 @@ class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Gets Full Path-Name (Groups and Name)
|
||||
* @returns {Promise<String>}
|
||||
* @returns {Promise<string>} Full path name of this monitor
|
||||
*/
|
||||
async getPathName() {
|
||||
let path = this.name;
|
||||
@@ -1638,8 +1533,8 @@ class Monitor extends BeanModel {
|
||||
|
||||
/**
|
||||
* Gets recursive all child ids
|
||||
* @param {number} monitorID ID of the monitor to get
|
||||
* @returns {Promise<Array>}
|
||||
* @param {number} monitorID ID of the monitor to get
|
||||
* @returns {Promise<Array>} IDs of all children
|
||||
*/
|
||||
static async getAllChildrenIDs(monitorID) {
|
||||
const childs = await Monitor.getChildren(monitorID);
|
||||
@@ -1670,10 +1565,10 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks recursive if parent (ancestors) are active
|
||||
* @param {number} monitorID ID of the monitor to get
|
||||
* @returns {Promise<Boolean>}
|
||||
*/
|
||||
* Checks recursive if parent (ancestors) are active
|
||||
* @param {number} monitorID ID of the monitor to get
|
||||
* @returns {Promise<boolean>} Is the parent monitor active?
|
||||
*/
|
||||
static async isParentActive(monitorID) {
|
||||
const parent = await Monitor.getParent(monitorID);
|
||||
|
||||
|
@@ -3,7 +3,7 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
class Proxy extends BeanModel {
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
* @returns {object} Object ready to parse
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
|
@@ -14,10 +14,11 @@ class StatusPage extends BeanModel {
|
||||
static domainMappingList = { };
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Response} response
|
||||
* @param {string} indexHTML
|
||||
* @param {string} slug
|
||||
* Handle responses to status page
|
||||
* @param {Response} response Response object
|
||||
* @param {string} indexHTML HTML to render
|
||||
* @param {string} slug Status page slug
|
||||
* @returns {void}
|
||||
*/
|
||||
static async handleStatusPageResponse(response, indexHTML, slug) {
|
||||
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||
@@ -33,8 +34,9 @@ class StatusPage extends BeanModel {
|
||||
|
||||
/**
|
||||
* SSR for status pages
|
||||
* @param {string} indexHTML
|
||||
* @param {StatusPage} statusPage
|
||||
* @param {string} indexHTML HTML page to render
|
||||
* @param {StatusPage} statusPage Status page populate HTML with
|
||||
* @returns {void}
|
||||
*/
|
||||
static async renderHTML(indexHTML, statusPage) {
|
||||
const $ = cheerio.load(indexHTML);
|
||||
@@ -87,7 +89,8 @@ class StatusPage extends BeanModel {
|
||||
|
||||
/**
|
||||
* Get all status page data in one call
|
||||
* @param {StatusPage} statusPage
|
||||
* @param {StatusPage} statusPage Status page to get data for
|
||||
* @returns {object} Status page data
|
||||
*/
|
||||
static async getStatusPageData(statusPage) {
|
||||
const config = await statusPage.toPublicJSON();
|
||||
@@ -142,7 +145,7 @@ class StatusPage extends BeanModel {
|
||||
* Send status page list to client
|
||||
* @param {Server} io io Socket server instance
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @returns {Promise<Bean[]>}
|
||||
* @returns {Promise<Bean[]>} Status page list
|
||||
*/
|
||||
static async sendStatusPageList(io, socket) {
|
||||
let result = {};
|
||||
@@ -159,7 +162,7 @@ class StatusPage extends BeanModel {
|
||||
|
||||
/**
|
||||
* Update list of domain names
|
||||
* @param {string[]} domainNameList
|
||||
* @param {string[]} domainNameList List of status page domains
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async updateDomainNameList(domainNameList) {
|
||||
@@ -203,7 +206,7 @@ class StatusPage extends BeanModel {
|
||||
|
||||
/**
|
||||
* Get list of domain names
|
||||
* @returns {Object[]}
|
||||
* @returns {object[]} List of status page domains
|
||||
*/
|
||||
getDomainNameList() {
|
||||
let domainList = [];
|
||||
@@ -219,7 +222,7 @@ class StatusPage extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
* @returns {object} Object ready to parse
|
||||
*/
|
||||
async toJSON() {
|
||||
return {
|
||||
@@ -243,7 +246,7 @@ class StatusPage extends BeanModel {
|
||||
/**
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
* @returns {Object}
|
||||
* @returns {object} Object ready to parse
|
||||
*/
|
||||
async toPublicJSON() {
|
||||
return {
|
||||
@@ -264,7 +267,8 @@ class StatusPage extends BeanModel {
|
||||
|
||||
/**
|
||||
* Convert slug to status page ID
|
||||
* @param {string} slug
|
||||
* @param {string} slug Status page slug
|
||||
* @returns {Promise<number>} ID of status page
|
||||
*/
|
||||
static async slugToID(slug) {
|
||||
return await R.getCell("SELECT id FROM status_page WHERE slug = ? ", [
|
||||
@@ -274,7 +278,7 @@ class StatusPage extends BeanModel {
|
||||
|
||||
/**
|
||||
* Get path to the icon for the page
|
||||
* @returns {string}
|
||||
* @returns {string} Path
|
||||
*/
|
||||
getIcon() {
|
||||
if (!this.icon) {
|
||||
@@ -287,7 +291,7 @@ class StatusPage extends BeanModel {
|
||||
/**
|
||||
* Get list of maintenances
|
||||
* @param {number} statusPageId ID of status page to get maintenance for
|
||||
* @returns {Object} Object representing maintenances sanitized for public
|
||||
* @returns {object} Object representing maintenances sanitized for public
|
||||
*/
|
||||
static async getMaintenanceList(statusPageId) {
|
||||
try {
|
||||
|
@@ -4,7 +4,7 @@ class Tag extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
* @returns {object} Object ready to parse
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
|
@@ -9,7 +9,7 @@ class User extends BeanModel {
|
||||
* Reset user password
|
||||
* Fix #1510, as in the context reset-password.js, there is no auto model mapping. Call this static function instead.
|
||||
* @param {number} userID ID of user to update
|
||||
* @param {string} newPassword
|
||||
* @param {string} newPassword Users new password
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async resetPassword(userID, newPassword) {
|
||||
@@ -21,7 +21,7 @@ class User extends BeanModel {
|
||||
|
||||
/**
|
||||
* Reset this users password
|
||||
* @param {string} newPassword
|
||||
* @param {string} newPassword Users new password
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async resetPassword(newPassword) {
|
||||
@@ -31,9 +31,9 @@ class User extends BeanModel {
|
||||
|
||||
/**
|
||||
* Create a new JWT for a user
|
||||
* @param {User} user
|
||||
* @param {string} jwtSecret
|
||||
* @return {string}
|
||||
* @param {User} user The User to create a JsonWebToken for
|
||||
* @param {string} jwtSecret The key used to sign the JsonWebToken
|
||||
* @returns {string} the JsonWebToken as a string
|
||||
*/
|
||||
static createJWT(user, jwtSecret) {
|
||||
return jwt.sign({
|
||||
|
56
server/monitor-types/dns.js
Normal file
56
server/monitor-types/dns.js
Normal file
@@ -0,0 +1,56 @@
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const { UP } = require("../../src/util");
|
||||
const dayjs = require("dayjs");
|
||||
const { dnsResolve } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
|
||||
class DnsMonitorType extends MonitorType {
|
||||
|
||||
name = "dns";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat, _server) {
|
||||
let startTime = dayjs().valueOf();
|
||||
let dnsMessage = "";
|
||||
|
||||
let dnsRes = await dnsResolve(monitor.hostname, monitor.dns_resolve_server, monitor.port, monitor.dns_resolve_type);
|
||||
heartbeat.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
if (monitor.dns_resolve_type === "A" || monitor.dns_resolve_type === "AAAA" || monitor.dns_resolve_type === "TXT" || monitor.dns_resolve_type === "PTR") {
|
||||
dnsMessage += "Records: ";
|
||||
dnsMessage += dnsRes.join(" | ");
|
||||
} else if (monitor.dns_resolve_type === "CNAME" || monitor.dns_resolve_type === "PTR") {
|
||||
dnsMessage += dnsRes[0];
|
||||
} else if (monitor.dns_resolve_type === "CAA") {
|
||||
dnsMessage += dnsRes[0].issue;
|
||||
} else if (monitor.dns_resolve_type === "MX") {
|
||||
dnsRes.forEach(record => {
|
||||
dnsMessage += `Hostname: ${record.exchange} - Priority: ${record.priority} | `;
|
||||
});
|
||||
dnsMessage = dnsMessage.slice(0, -2);
|
||||
} else if (monitor.dns_resolve_type === "NS") {
|
||||
dnsMessage += "Servers: ";
|
||||
dnsMessage += dnsRes.join(" | ");
|
||||
} else if (monitor.dns_resolve_type === "SOA") {
|
||||
dnsMessage += `NS-Name: ${dnsRes.nsname} | Hostmaster: ${dnsRes.hostmaster} | Serial: ${dnsRes.serial} | Refresh: ${dnsRes.refresh} | Retry: ${dnsRes.retry} | Expire: ${dnsRes.expire} | MinTTL: ${dnsRes.minttl}`;
|
||||
} else if (monitor.dns_resolve_type === "SRV") {
|
||||
dnsRes.forEach(record => {
|
||||
dnsMessage += `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight} | `;
|
||||
});
|
||||
dnsMessage = dnsMessage.slice(0, -2);
|
||||
}
|
||||
|
||||
if (monitor.dns_last_result !== dnsMessage && dnsMessage !== undefined) {
|
||||
await R.exec("UPDATE `monitor` SET dns_last_result = ? WHERE id = ? ", [ dnsMessage, monitor.id ]);
|
||||
}
|
||||
|
||||
heartbeat.msg = dnsMessage;
|
||||
heartbeat.status = UP;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DnsMonitorType,
|
||||
};
|
@@ -3,10 +3,10 @@ class MonitorType {
|
||||
name = undefined;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Monitor} monitor
|
||||
* @param {Heartbeat} heartbeat
|
||||
* @param {UptimeKumaServer} server
|
||||
* Run the monitoring check on the given monitor
|
||||
* @param {Monitor} monitor Monitor to check
|
||||
* @param {Heartbeat} heartbeat Monitor heartbeat to update
|
||||
* @param {UptimeKumaServer} server Uptime Kuma server
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async check(monitor, heartbeat, server) {
|
||||
|
@@ -49,8 +49,11 @@ if (process.platform === "win32") {
|
||||
];
|
||||
}
|
||||
|
||||
log.debug("chrome", allowedList);
|
||||
|
||||
/**
|
||||
* Is the executable path allowed?
|
||||
* @param {string} executablePath Path to executable
|
||||
* @returns {Promise<boolean>} The executable is allowed?
|
||||
*/
|
||||
async function isAllowedChromeExecutable(executablePath) {
|
||||
console.log(config.args);
|
||||
if (config.args["allow-all-chrome-exec"] || process.env.UPTIME_KUMA_ALLOW_ALL_CHROME_EXEC === "1") {
|
||||
@@ -61,6 +64,11 @@ async function isAllowedChromeExecutable(executablePath) {
|
||||
return allowedList.includes(executablePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current instance of the browser. If there isn't one, create
|
||||
* it.
|
||||
* @returns {Promise<Browser>} The browser
|
||||
*/
|
||||
async function getBrowser() {
|
||||
if (!browser) {
|
||||
let executablePath = await Settings.get("chromeExecutable");
|
||||
@@ -75,6 +83,11 @@ async function getBrowser() {
|
||||
return browser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the chrome executable path
|
||||
* @param {string} executablePath Path to chrome executable
|
||||
* @returns {Promise<string>} Executable path
|
||||
*/
|
||||
async function prepareChromeExecutable(executablePath) {
|
||||
// Special code for using the playwright_chromium
|
||||
if (typeof executablePath === "string" && executablePath.toLocaleLowerCase() === "#playwright_chromium") {
|
||||
@@ -121,6 +134,12 @@ async function prepareChromeExecutable(executablePath) {
|
||||
return executablePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the chrome executable
|
||||
* @param {any[]} executables Executables to search through
|
||||
* @returns {any} Executable
|
||||
* @throws Could not find executable
|
||||
*/
|
||||
function findChrome(executables) {
|
||||
// Use the last working executable, so we don't have to search for it again
|
||||
if (lastAutoDetectChromeExecutable) {
|
||||
@@ -138,6 +157,10 @@ function findChrome(executables) {
|
||||
throw new Error("Chromium not found, please specify Chromium executable path in the settings page.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset chrome
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function resetChrome() {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
@@ -147,8 +170,8 @@ async function resetChrome() {
|
||||
|
||||
/**
|
||||
* Test if the chrome executable is valid and return the version
|
||||
* @param executablePath
|
||||
* @returns {Promise<string>}
|
||||
* @param {string} executablePath Path to executable
|
||||
* @returns {Promise<string>} Chrome version
|
||||
*/
|
||||
async function testChrome(executablePath) {
|
||||
try {
|
||||
@@ -175,6 +198,9 @@ class RealBrowserMonitorType extends MonitorType {
|
||||
|
||||
name = "real-browser";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat, server) {
|
||||
const browser = await getBrowser();
|
||||
const context = await browser.newContext();
|
||||
|
@@ -13,9 +13,9 @@ class TailscalePing extends MonitorType {
|
||||
/**
|
||||
* Checks the ping status of the URL associated with the monitor.
|
||||
* It then parses the Tailscale ping command output to update the heatrbeat.
|
||||
*
|
||||
* @param {Object} monitor - The monitor object associated with the check.
|
||||
* @param {Object} heartbeat - The heartbeat object to update.
|
||||
* @param {object} monitor The monitor object associated with the check.
|
||||
* @param {object} heartbeat The heartbeat object to update.
|
||||
* @returns {Promise<void>}
|
||||
* @throws Will throw an error if checking Tailscale ping encounters any error
|
||||
*/
|
||||
async check(monitor, heartbeat) {
|
||||
@@ -31,9 +31,9 @@ class TailscalePing extends MonitorType {
|
||||
|
||||
/**
|
||||
* Runs the Tailscale ping command to the given URL.
|
||||
*
|
||||
* @param {string} hostname - The hostname to ping.
|
||||
* @returns {Promise<string>} - A Promise that resolves to the output of the Tailscale ping command
|
||||
* @param {string} hostname The hostname to ping.
|
||||
* @param {number} interval Interval to send ping
|
||||
* @returns {Promise<string>} A Promise that resolves to the output of the Tailscale ping command
|
||||
* @throws Will throw an error if the command execution encounters any error.
|
||||
*/
|
||||
async runTailscalePing(hostname, interval) {
|
||||
@@ -61,9 +61,9 @@ class TailscalePing extends MonitorType {
|
||||
|
||||
/**
|
||||
* Parses the output of the Tailscale ping command to update the heartbeat.
|
||||
*
|
||||
* @param {string} tailscaleOutput - The output of the Tailscale ping command.
|
||||
* @param {Object} heartbeat - The heartbeat object to update.
|
||||
* @param {string} tailscaleOutput The output of the Tailscale ping command.
|
||||
* @param {object} heartbeat The heartbeat object to update.
|
||||
* @returns {void}
|
||||
* @throws Will throw an eror if the output contains any unexpected string.
|
||||
*/
|
||||
parseTailscaleOutput(tailscaleOutput, heartbeat) {
|
||||
|
@@ -6,6 +6,9 @@ class Alerta extends NotificationProvider {
|
||||
|
||||
name = "alerta";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
|
@@ -7,6 +7,9 @@ class AlertNow extends NotificationProvider {
|
||||
|
||||
name = "AlertNow";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
|
@@ -7,6 +7,9 @@ const qs = require("qs");
|
||||
class AliyunSMS extends NotificationProvider {
|
||||
name = "AliyunSMS";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
@@ -79,9 +82,9 @@ class AliyunSMS extends NotificationProvider {
|
||||
|
||||
/**
|
||||
* Aliyun request sign
|
||||
* @param {Object} param Parameters object to sign
|
||||
* @param {object} param Parameters object to sign
|
||||
* @param {string} AccessKeySecret Secret key to sign parameters with
|
||||
* @returns {string}
|
||||
* @returns {string} Base64 encoded request
|
||||
*/
|
||||
sign(param, AccessKeySecret) {
|
||||
let param2 = {};
|
||||
@@ -123,7 +126,7 @@ class AliyunSMS extends NotificationProvider {
|
||||
/**
|
||||
* Convert status constant to string
|
||||
* @param {const} status The status constant
|
||||
* @returns {string}
|
||||
* @returns {string} Status
|
||||
*/
|
||||
statusToString(status) {
|
||||
switch (status) {
|
||||
|
@@ -5,6 +5,9 @@ class Apprise extends NotificationProvider {
|
||||
|
||||
name = "apprise";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const args = [ "-vv", "-b", msg, notification.appriseURL ];
|
||||
if (notification.title) {
|
||||
|
@@ -18,6 +18,9 @@ const successMessage = "Successes!";
|
||||
class Bark extends NotificationProvider {
|
||||
name = "Bark";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let barkEndpoint = notification.barkEndpoint;
|
||||
|
||||
@@ -43,10 +46,10 @@ class Bark extends NotificationProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add additional parameter for better on device styles (iOS 15
|
||||
* optimized)
|
||||
* Add additional parameter for Bark v1 endpoints
|
||||
* @param {BeanModel} notification Notification to send
|
||||
* @param {string} postUrl URL to append parameters to
|
||||
* @returns {string}
|
||||
* @returns {string} Additional URL parameters
|
||||
*/
|
||||
appendAdditionalParameters(notification, postUrl) {
|
||||
// set icon to uptime kuma icon, 11kb should be fine
|
||||
@@ -70,7 +73,8 @@ class Bark extends NotificationProvider {
|
||||
|
||||
/**
|
||||
* Check if result is successful
|
||||
* @param {Object} result Axios response object
|
||||
* @param {object} result Axios response object
|
||||
* @returns {void}
|
||||
* @throws {Error} The status code is not in range 2xx
|
||||
*/
|
||||
checkResult(result) {
|
||||
@@ -84,18 +88,30 @@ class Bark extends NotificationProvider {
|
||||
|
||||
/**
|
||||
* Send the message
|
||||
* @param {BeanModel} notification Notification to send
|
||||
* @param {string} title Message title
|
||||
* @param {string} subtitle Message
|
||||
* @param {string} endpoint Endpoint to send request to
|
||||
* @returns {string}
|
||||
* @returns {string} Success message
|
||||
*/
|
||||
async postNotification(notification, title, subtitle, endpoint) {
|
||||
// url encode title and subtitle
|
||||
title = encodeURIComponent(title);
|
||||
subtitle = encodeURIComponent(subtitle);
|
||||
let postUrl = endpoint + "/" + title + "/" + subtitle;
|
||||
postUrl = this.appendAdditionalParameters(notification, postUrl);
|
||||
let result = await axios.get(postUrl);
|
||||
let result;
|
||||
if (notification.apiVersion === "v1" || notification.apiVersion == null) {
|
||||
// url encode title and subtitle
|
||||
title = encodeURIComponent(title);
|
||||
subtitle = encodeURIComponent(subtitle);
|
||||
let postUrl = endpoint + "/" + title + "/" + subtitle;
|
||||
postUrl = this.appendAdditionalParameters(notification, postUrl);
|
||||
result = await axios.get(postUrl);
|
||||
} else {
|
||||
result = await axios.post(`${endpoint}/push`, {
|
||||
title,
|
||||
body: subtitle,
|
||||
icon: barkNotificationAvatar,
|
||||
sound: notification.barkSound || "telegraph", // default sound is telegraph
|
||||
group: notification.barkGroup || "UptimeKuma", // default group is UptimeKuma
|
||||
});
|
||||
}
|
||||
this.checkResult(result);
|
||||
if (result.statusText != null) {
|
||||
return "Bark notification succeed: " + result.statusText;
|
||||
|
@@ -5,6 +5,9 @@ class ClickSendSMS extends NotificationProvider {
|
||||
|
||||
name = "clicksendsms";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
|
@@ -6,6 +6,9 @@ const Crypto = require("crypto");
|
||||
class DingDing extends NotificationProvider {
|
||||
name = "DingDing";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
@@ -39,8 +42,8 @@ class DingDing extends NotificationProvider {
|
||||
|
||||
/**
|
||||
* Send message to DingDing
|
||||
* @param {BeanModel} notification
|
||||
* @param {Object} params Parameters of message
|
||||
* @param {BeanModel} notification Notification to send
|
||||
* @param {object} params Parameters of message
|
||||
* @returns {boolean} True if successful else false
|
||||
*/
|
||||
async sendToDingDing(notification, params) {
|
||||
@@ -66,7 +69,7 @@ class DingDing extends NotificationProvider {
|
||||
* DingDing sign
|
||||
* @param {Date} timestamp Timestamp of message
|
||||
* @param {string} secretKey Secret key to sign data with
|
||||
* @returns {string}
|
||||
* @returns {string} Base64 encoded signature
|
||||
*/
|
||||
sign(timestamp, secretKey) {
|
||||
return Crypto
|
||||
@@ -78,7 +81,7 @@ class DingDing extends NotificationProvider {
|
||||
/**
|
||||
* Convert status constant to string
|
||||
* @param {const} status The status constant
|
||||
* @returns {string}
|
||||
* @returns {string} Status
|
||||
*/
|
||||
statusToString(status) {
|
||||
// TODO: Move to notification-provider.js to avoid repetition in classes
|
||||
|
@@ -6,6 +6,9 @@ class Discord extends NotificationProvider {
|
||||
|
||||
name = "discord";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
|
@@ -5,6 +5,9 @@ const { DOWN, UP } = require("../../src/util");
|
||||
class Feishu extends NotificationProvider {
|
||||
name = "Feishu";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
let feishuWebHookUrl = notification.feishuWebHookUrl;
|
||||
|
@@ -7,6 +7,9 @@ const successMessage = "Sent Successfully.";
|
||||
class FlashDuty extends NotificationProvider {
|
||||
name = "FlashDuty";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
@@ -33,12 +36,12 @@ class FlashDuty extends NotificationProvider {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a monitor url from the monitors infomation
|
||||
* @param {Object} monitorInfo Monitor details
|
||||
* @returns {string|undefined}
|
||||
* @param {object} monitorInfo Monitor details
|
||||
* @returns {string|undefined} Monitor URL
|
||||
*/
|
||||
|
||||
genMonitorUrl(monitorInfo) {
|
||||
if (monitorInfo.type === "port" && monitorInfo.port) {
|
||||
return monitorInfo.hostname + ":" + monitorInfo.port;
|
||||
@@ -54,9 +57,9 @@ class FlashDuty extends NotificationProvider {
|
||||
* @param {BeanModel} notification Message title
|
||||
* @param {string} title Message
|
||||
* @param {string} body Message
|
||||
* @param {Object} monitorInfo Monitor details
|
||||
* @param {object} monitorInfo Monitor details
|
||||
* @param {string} eventStatus Monitor status (Info, Warning, Critical, Ok)
|
||||
* @returns {string}
|
||||
* @returns {string} Success message
|
||||
*/
|
||||
async postNotification(notification, title, body, monitorInfo, eventStatus) {
|
||||
const options = {
|
||||
|
@@ -5,6 +5,9 @@ class FreeMobile extends NotificationProvider {
|
||||
|
||||
name = "FreeMobile";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
|
@@ -6,6 +6,9 @@ class GoAlert extends NotificationProvider {
|
||||
|
||||
name = "GoAlert";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
|
@@ -8,6 +8,9 @@ class GoogleChat extends NotificationProvider {
|
||||
|
||||
name = "GoogleChat";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
|
@@ -5,6 +5,9 @@ class Gorush extends NotificationProvider {
|
||||
|
||||
name = "gorush";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
|
@@ -5,6 +5,9 @@ class Gotify extends NotificationProvider {
|
||||
|
||||
name = "gotify";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
|
61
server/notification-providers/grafana-oncall.js
Normal file
61
server/notification-providers/grafana-oncall.js
Normal file
@@ -0,0 +1,61 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class GrafanaOncall extends NotificationProvider {
|
||||
|
||||
name = "GrafanaOncall";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
|
||||
if (!notification.GrafanaOncallURL) {
|
||||
throw new Error("GrafanaOncallURL cannot be empty");
|
||||
}
|
||||
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
if (heartbeatJSON === null) {
|
||||
let grafanaupdata = {
|
||||
title: "General notification",
|
||||
message: msg,
|
||||
state: "alerting",
|
||||
};
|
||||
await axios.post(
|
||||
notification.GrafanaOncallURL,
|
||||
grafanaupdata
|
||||
);
|
||||
return okMsg;
|
||||
} else if (heartbeatJSON["status"] === DOWN) {
|
||||
let grafanadowndata = {
|
||||
title: monitorJSON["name"] + " is down",
|
||||
message: heartbeatJSON["msg"],
|
||||
state: "alerting",
|
||||
};
|
||||
await axios.post(
|
||||
notification.GrafanaOncallURL,
|
||||
grafanadowndata
|
||||
);
|
||||
return okMsg;
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
let grafanaupdata = {
|
||||
title: monitorJSON["name"] + " is up",
|
||||
message: heartbeatJSON["msg"],
|
||||
state: "ok",
|
||||
};
|
||||
await axios.post(
|
||||
notification.GrafanaOncallURL,
|
||||
grafanaupdata
|
||||
);
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GrafanaOncall;
|
@@ -6,6 +6,9 @@ const defaultNotificationService = "notify";
|
||||
class HomeAssistant extends NotificationProvider {
|
||||
name = "HomeAssistant";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, message, monitor = null, heartbeat = null) {
|
||||
const notificationService = notification?.notificationService || defaultNotificationService;
|
||||
|
||||
|
@@ -5,6 +5,9 @@ class Kook extends NotificationProvider {
|
||||
|
||||
name = "Kook";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
let url = "https://www.kookapp.cn/api/v3/message/create";
|
||||
|
@@ -6,6 +6,9 @@ class Line extends NotificationProvider {
|
||||
|
||||
name = "line";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
|
@@ -7,6 +7,9 @@ class LineNotify extends NotificationProvider {
|
||||
|
||||
name = "LineNotify";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
|
@@ -6,6 +6,9 @@ class LunaSea extends NotificationProvider {
|
||||
|
||||
name = "lunasea";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
let lunaseaurl = "";
|
||||
|
@@ -6,6 +6,9 @@ const { log } = require("../../src/util");
|
||||
class Matrix extends NotificationProvider {
|
||||
name = "matrix";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
|
@@ -6,6 +6,9 @@ class Mattermost extends NotificationProvider {
|
||||
|
||||
name = "mattermost";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
|
@@ -33,6 +33,9 @@ if (semver.lt(nodeVersion, "16.0.0")) {
|
||||
class Nostr extends NotificationProvider {
|
||||
name = "nostr";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
// All DMs should have same timestamp
|
||||
const createdAt = Math.floor(Date.now() / 1000);
|
||||
@@ -86,6 +89,11 @@ class Nostr extends NotificationProvider {
|
||||
return `${successfulRelays}/${relays.length} relays connected.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the private key for the sender
|
||||
* @param {string} sender Sender to retrieve key for
|
||||
* @returns {nip19.DecodeResult} Private key
|
||||
*/
|
||||
async getPrivateKey(sender) {
|
||||
try {
|
||||
const senderDecodeResult = await nip19.decode(sender);
|
||||
@@ -96,6 +104,11 @@ class Nostr extends NotificationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public keys for recipients
|
||||
* @param {string} recipients Newline delimited list of recipients
|
||||
* @returns {nip19.DecodeResult[]} Public keys
|
||||
*/
|
||||
async getPublicKeys(recipients) {
|
||||
const recipientsList = recipients.split("\n");
|
||||
const publicKeys = [];
|
||||
|
@@ -2,16 +2,16 @@ class NotificationProvider {
|
||||
|
||||
/**
|
||||
* Notification Provider Name
|
||||
* @type string
|
||||
* @type {string}
|
||||
*/
|
||||
name = undefined;
|
||||
|
||||
/**
|
||||
* Send a notification
|
||||
* @param {BeanModel} notification
|
||||
* @param {BeanModel} notification Notification to send
|
||||
* @param {string} msg General Message
|
||||
* @param {?Object} monitorJSON Monitor details (For Up/Down only)
|
||||
* @param {?Object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||
* @param {?object} monitorJSON Monitor details (For Up/Down only)
|
||||
* @param {?object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||
* @returns {Promise<string>} Return Successful Message
|
||||
* @throws Error with fail msg
|
||||
*/
|
||||
@@ -22,6 +22,7 @@ class NotificationProvider {
|
||||
/**
|
||||
* Throws an error
|
||||
* @param {any} error The error to throw
|
||||
* @returns {void}
|
||||
* @throws {any} The error specified
|
||||
*/
|
||||
throwGeneralAxiosError(error) {
|
||||
|
@@ -6,6 +6,9 @@ class Ntfy extends NotificationProvider {
|
||||
|
||||
name = "ntfy";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
|
@@ -5,6 +5,9 @@ class Octopush extends NotificationProvider {
|
||||
|
||||
name = "octopush";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
|
@@ -5,6 +5,9 @@ class OneBot extends NotificationProvider {
|
||||
|
||||
name = "OneBot";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
|
@@ -68,11 +68,11 @@ class Opsgenie extends NotificationProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {BeanModel} notification
|
||||
* Make POST request to Opsgenie
|
||||
* @param {BeanModel} notification Notification to send
|
||||
* @param {string} url Request url
|
||||
* @param {Object} data Request body
|
||||
* @returns {Promise<string>}
|
||||
* @param {object} data Request body
|
||||
* @returns {Promise<string>} Success message
|
||||
*/
|
||||
async post(notification, url, data) {
|
||||
let config = {
|
||||
|
@@ -39,7 +39,8 @@ class PagerDuty extends NotificationProvider {
|
||||
|
||||
/**
|
||||
* Check if result is successful, result code should be in range 2xx
|
||||
* @param {Object} result Axios response object
|
||||
* @param {object} result Axios response object
|
||||
* @returns {void}
|
||||
* @throws {Error} The status code is not in range 2xx
|
||||
*/
|
||||
checkResult(result) {
|
||||
@@ -56,9 +57,9 @@ class PagerDuty extends NotificationProvider {
|
||||
* @param {BeanModel} notification Message title
|
||||
* @param {string} title Message title
|
||||
* @param {string} body Message
|
||||
* @param {Object} monitorInfo Monitor details (For Up/Down only)
|
||||
* @param {object} monitorInfo Monitor details (For Up/Down only)
|
||||
* @param {?string} eventAction Action event for PagerDuty (trigger, acknowledge, resolve)
|
||||
* @returns {string}
|
||||
* @returns {Promise<string>} Success message
|
||||
*/
|
||||
async postNotification(notification, title, body, monitorInfo, eventAction = "trigger") {
|
||||
|
||||
|
@@ -32,7 +32,8 @@ class PagerTree extends NotificationProvider {
|
||||
|
||||
/**
|
||||
* Check if result is successful, result code should be in range 2xx
|
||||
* @param {Object} result Axios response object
|
||||
* @param {object} result Axios response object
|
||||
* @returns {void}
|
||||
* @throws {Error} The status code is not in range 2xx
|
||||
*/
|
||||
checkResult(result) {
|
||||
@@ -48,9 +49,10 @@ class PagerTree extends NotificationProvider {
|
||||
* Send the message
|
||||
* @param {BeanModel} notification Message title
|
||||
* @param {string} title Message title
|
||||
* @param {Object} monitorJSON Monitor details (For Up/Down only)
|
||||
* @param {object} monitorJSON Monitor details (For Up/Down only)
|
||||
* @param {object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||
* @param {?string} eventAction Action event for PagerTree (create, resolve)
|
||||
* @returns {string}
|
||||
* @returns {Promise<string>} Success state
|
||||
*/
|
||||
async postNotification(notification, title, monitorJSON, heartbeatJSON, eventAction = "create") {
|
||||
|
||||
|
@@ -5,6 +5,9 @@ class PromoSMS extends NotificationProvider {
|
||||
|
||||
name = "promosms";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
|
@@ -7,6 +7,9 @@ class Pushbullet extends NotificationProvider {
|
||||
|
||||
name = "pushbullet";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
|
@@ -6,6 +6,9 @@ class PushDeer extends NotificationProvider {
|
||||
|
||||
name = "PushDeer";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
let endpoint = "/message/push";
|
||||
|
@@ -5,6 +5,9 @@ class Pushover extends NotificationProvider {
|
||||
|
||||
name = "pushover";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
let pushoverlink = "https://api.pushover.net/1/messages.json";
|
||||
|
@@ -5,6 +5,9 @@ class Pushy extends NotificationProvider {
|
||||
|
||||
name = "pushy";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
|
@@ -8,6 +8,9 @@ class RocketChat extends NotificationProvider {
|
||||
|
||||
name = "rocket.chat";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
|
@@ -6,6 +6,9 @@ class ServerChan extends NotificationProvider {
|
||||
|
||||
name = "ServerChan";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
@@ -23,8 +26,8 @@ class ServerChan extends NotificationProvider {
|
||||
|
||||
/**
|
||||
* Get the formatted title for message
|
||||
* @param {?Object} monitorJSON Monitor details (For Up/Down only)
|
||||
* @param {?Object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||
* @param {?object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||
* @param {?object} monitorJSON Monitor details (For Up/Down only)
|
||||
* @returns {string} Formatted title
|
||||
*/
|
||||
checkStatus(heartbeatJSON, monitorJSON) {
|
||||
|
@@ -5,6 +5,9 @@ class SerwerSMS extends NotificationProvider {
|
||||
|
||||
name = "serwersms";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
|
@@ -5,6 +5,9 @@ class Signal extends NotificationProvider {
|
||||
|
||||
name = "signal";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
|
@@ -10,7 +10,9 @@ class Slack extends NotificationProvider {
|
||||
/**
|
||||
* Deprecated property notification.slackbutton
|
||||
* Set it as primary base url if this is not yet set.
|
||||
* @deprecated
|
||||
* @param {string} url The primary base URL to use
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async deprecateURL(url) {
|
||||
let currentPrimaryBaseURL = await setting("primaryBaseURL");
|
||||
@@ -25,6 +27,9 @@ class Slack extends NotificationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
|
@@ -4,6 +4,9 @@ const axios = require("axios");
|
||||
class SMSC extends NotificationProvider {
|
||||
name = "smsc";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
|
@@ -5,6 +5,9 @@ class SMSEagle extends NotificationProvider {
|
||||
|
||||
name = "SMSEagle";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
|
@@ -5,6 +5,9 @@ class SMSManager extends NotificationProvider {
|
||||
|
||||
name = "SMSManager";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
try {
|
||||
let data = {
|
||||
|
@@ -1,11 +1,15 @@
|
||||
const nodemailer = require("nodemailer");
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const { DOWN } = require("../../src/util");
|
||||
const { Liquid } = require("liquidjs");
|
||||
|
||||
class SMTP extends NotificationProvider {
|
||||
|
||||
name = "smtp";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
|
||||
const config = {
|
||||
@@ -36,76 +40,86 @@ class SMTP extends NotificationProvider {
|
||||
pass: notification.smtpPassword,
|
||||
};
|
||||
}
|
||||
// Lets start with default subject and empty string for custom one
|
||||
|
||||
// default values in case the user does not want to template
|
||||
let subject = msg;
|
||||
|
||||
// Change the subject if:
|
||||
// - The msg ends with "Testing" or
|
||||
// - Actual Up/Down Notification
|
||||
if ((monitorJSON && heartbeatJSON) || msg.endsWith("Testing")) {
|
||||
let customSubject = "";
|
||||
|
||||
// Our subject cannot end with whitespace it's often raise spam score
|
||||
// Once I got "Cannot read property 'trim' of undefined", better be safe than sorry
|
||||
if (notification.customSubject) {
|
||||
customSubject = notification.customSubject.trim();
|
||||
}
|
||||
|
||||
// If custom subject is not empty, change subject for notification
|
||||
if (customSubject !== "") {
|
||||
|
||||
// Replace "MACROS" with corresponding variable
|
||||
let replaceName = new RegExp("{{NAME}}", "g");
|
||||
let replaceHostnameOrURL = new RegExp("{{HOSTNAME_OR_URL}}", "g");
|
||||
let replaceStatus = new RegExp("{{STATUS}}", "g");
|
||||
|
||||
// Lets start with dummy values to simplify code
|
||||
let monitorName = "Test";
|
||||
let monitorHostnameOrURL = "testing.hostname";
|
||||
let serviceStatus = "⚠️ Test";
|
||||
|
||||
if (monitorJSON !== null) {
|
||||
monitorName = monitorJSON["name"];
|
||||
|
||||
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword" || monitorJSON["type"] === "json-query") {
|
||||
monitorHostnameOrURL = monitorJSON["url"];
|
||||
} else {
|
||||
monitorHostnameOrURL = monitorJSON["hostname"];
|
||||
}
|
||||
}
|
||||
|
||||
if (heartbeatJSON !== null) {
|
||||
serviceStatus = (heartbeatJSON["status"] === DOWN) ? "🔴 Down" : "✅ Up";
|
||||
}
|
||||
|
||||
// Break replace to one by line for better readability
|
||||
customSubject = customSubject.replace(replaceStatus, serviceStatus);
|
||||
customSubject = customSubject.replace(replaceName, monitorName);
|
||||
customSubject = customSubject.replace(replaceHostnameOrURL, monitorHostnameOrURL);
|
||||
|
||||
subject = customSubject;
|
||||
}
|
||||
}
|
||||
|
||||
let transporter = nodemailer.createTransport(config);
|
||||
|
||||
let bodyTextContent = msg;
|
||||
let body = msg;
|
||||
if (heartbeatJSON) {
|
||||
bodyTextContent = `${msg}\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`;
|
||||
body = `${msg}\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`;
|
||||
}
|
||||
// subject and body are templated
|
||||
if ((monitorJSON && heartbeatJSON) || msg.endsWith("Testing")) {
|
||||
// cannot end with whitespace as this often raises spam scores
|
||||
const customSubject = notification.customSubject?.trim() || "";
|
||||
const customBody = notification.customBody?.trim() || "";
|
||||
|
||||
const context = this.generateContext(msg, monitorJSON, heartbeatJSON);
|
||||
const engine = new Liquid();
|
||||
if (customSubject !== "") {
|
||||
const tpl = engine.parse(customSubject);
|
||||
subject = await engine.render(tpl, context);
|
||||
}
|
||||
if (customBody !== "") {
|
||||
const tpl = engine.parse(customBody);
|
||||
body = await engine.render(tpl, context);
|
||||
}
|
||||
}
|
||||
|
||||
// send mail with defined transport object
|
||||
let transporter = nodemailer.createTransport(config);
|
||||
await transporter.sendMail({
|
||||
from: notification.smtpFrom,
|
||||
cc: notification.smtpCC,
|
||||
bcc: notification.smtpBCC,
|
||||
to: notification.smtpTo,
|
||||
subject: subject,
|
||||
text: bodyTextContent,
|
||||
text: body,
|
||||
});
|
||||
|
||||
return "Sent Successfully.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate context for LiquidJS
|
||||
* @param {string} msg the message that will be included in the context
|
||||
* @param {?object} monitorJSON Monitor details (For Up/Down/Cert-Expiry only)
|
||||
* @param {?object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||
* @returns {{STATUS: string, status: string, HOSTNAME_OR_URL: string, hostnameOrUrl: string, NAME: string, name: string, monitorJSON: ?object, heartbeatJSON: ?object, msg: string}} the context
|
||||
*/
|
||||
generateContext(msg, monitorJSON, heartbeatJSON) {
|
||||
// Let's start with dummy values to simplify code
|
||||
let monitorName = "Monitor Name not available";
|
||||
let monitorHostnameOrURL = "testing.hostname";
|
||||
|
||||
if (monitorJSON !== null) {
|
||||
monitorName = monitorJSON["name"];
|
||||
|
||||
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword" || monitorJSON["type"] === "json-query") {
|
||||
monitorHostnameOrURL = monitorJSON["url"];
|
||||
} else {
|
||||
monitorHostnameOrURL = monitorJSON["hostname"];
|
||||
}
|
||||
}
|
||||
|
||||
let serviceStatus = "⚠️ Test";
|
||||
if (heartbeatJSON !== null) {
|
||||
serviceStatus = (heartbeatJSON["status"] === DOWN) ? "🔴 Down" : "✅ Up";
|
||||
}
|
||||
return {
|
||||
// for v1 compatibility, to be removed in v3
|
||||
"STATUS": serviceStatus,
|
||||
"NAME": monitorName,
|
||||
"HOSTNAME_OR_URL": monitorHostnameOrURL,
|
||||
|
||||
// variables which are officially supported
|
||||
"status": serviceStatus,
|
||||
"name": monitorName,
|
||||
"hostnameOrURL": monitorHostnameOrURL,
|
||||
monitorJSON,
|
||||
heartbeatJSON,
|
||||
msg,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SMTP;
|
||||
|
@@ -37,7 +37,8 @@ class Splunk extends NotificationProvider {
|
||||
|
||||
/**
|
||||
* Check if result is successful, result code should be in range 2xx
|
||||
* @param {Object} result Axios response object
|
||||
* @param {object} result Axios response object
|
||||
* @returns {void}
|
||||
* @throws {Error} The status code is not in range 2xx
|
||||
*/
|
||||
checkResult(result) {
|
||||
@@ -54,9 +55,9 @@ class Splunk extends NotificationProvider {
|
||||
* @param {BeanModel} notification Message title
|
||||
* @param {string} title Message title
|
||||
* @param {string} body Message
|
||||
* @param {Object} monitorInfo Monitor details (For Up/Down only)
|
||||
* @param {object} monitorInfo Monitor details (For Up/Down only)
|
||||
* @param {?string} eventAction Action event for PagerDuty (trigger, acknowledge, resolve)
|
||||
* @returns {string}
|
||||
* @returns {Promise<string>} Success state
|
||||
*/
|
||||
async postNotification(notification, title, body, monitorInfo, eventAction = "trigger") {
|
||||
|
||||
|
@@ -6,6 +6,9 @@ class Squadcast extends NotificationProvider {
|
||||
|
||||
name = "squadcast";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
|
@@ -7,6 +7,9 @@ class Stackfield extends NotificationProvider {
|
||||
|
||||
name = "stackfield";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
|
@@ -9,7 +9,7 @@ class Teams extends NotificationProvider {
|
||||
* Generate the message to send
|
||||
* @param {const} status The status constant
|
||||
* @param {string} monitorName Name of monitor
|
||||
* @returns {string}
|
||||
* @returns {string} Status message
|
||||
*/
|
||||
_statusMessageFactory = (status, monitorName) => {
|
||||
if (status === DOWN) {
|
||||
@@ -37,11 +37,12 @@ class Teams extends NotificationProvider {
|
||||
|
||||
/**
|
||||
* 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 {Object}
|
||||
* @param {object} args Method arguments
|
||||
* @param {const} args.status The status of the monitor
|
||||
* @param {string} args.monitorMessage Message to send
|
||||
* @param {string} args.monitorName Name of monitor affected
|
||||
* @param {string} args.monitorUrl URL of monitor affected
|
||||
* @returns {object} Notification payload
|
||||
*/
|
||||
_notificationPayloadFactory = ({
|
||||
status,
|
||||
@@ -96,7 +97,8 @@ class Teams extends NotificationProvider {
|
||||
/**
|
||||
* Send the notification
|
||||
* @param {string} webhookUrl URL to send the request to
|
||||
* @param {Object} payload Payload generated by _notificationPayloadFactory
|
||||
* @param {object} payload Payload generated by _notificationPayloadFactory
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
_sendNotification = async (webhookUrl, payload) => {
|
||||
await axios.post(webhookUrl, payload);
|
||||
@@ -116,6 +118,9 @@ class Teams extends NotificationProvider {
|
||||
return this._sendNotification(webhookUrl, payload);
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
|
@@ -5,6 +5,9 @@ class TechulusPush extends NotificationProvider {
|
||||
|
||||
name = "PushByTechulus";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
|
@@ -5,6 +5,9 @@ class Telegram extends NotificationProvider {
|
||||
|
||||
name = "telegram";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
@@ -25,11 +28,7 @@ class Telegram extends NotificationProvider {
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data && error.response.data.description) {
|
||||
throw new Error(error.response.data.description);
|
||||
} else {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,9 @@ class Twilio extends NotificationProvider {
|
||||
|
||||
name = "twilio";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
@@ -7,6 +7,9 @@ class Webhook extends NotificationProvider {
|
||||
|
||||
name = "webhook";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
|
@@ -6,6 +6,9 @@ class WeCom extends NotificationProvider {
|
||||
|
||||
name = "WeCom";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
@@ -26,9 +29,9 @@ class WeCom extends NotificationProvider {
|
||||
|
||||
/**
|
||||
* Generate the message to send
|
||||
* @param {Object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||
* @param {object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||
* @param {string} msg General message
|
||||
* @returns {Object}
|
||||
* @returns {object} Message
|
||||
*/
|
||||
composeMessage(heartbeatJSON, msg) {
|
||||
let title;
|
||||
|
@@ -10,7 +10,7 @@ class ZohoCliq extends NotificationProvider {
|
||||
* Generate the message to send
|
||||
* @param {const} status The status constant
|
||||
* @param {string} monitorName Name of monitor
|
||||
* @returns {string}
|
||||
* @returns {string} Status message
|
||||
*/
|
||||
_statusMessageFactory = (status, monitorName) => {
|
||||
if (status === DOWN) {
|
||||
@@ -25,6 +25,7 @@ class ZohoCliq extends NotificationProvider {
|
||||
* Send the notification
|
||||
* @param {string} webhookUrl URL to send the request to
|
||||
* @param {Array} payload Payload generated by _notificationPayloadFactory
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
_sendNotification = async (webhookUrl, payload) => {
|
||||
await axios.post(webhookUrl, { text: payload.join("\n") });
|
||||
@@ -32,11 +33,12 @@ class ZohoCliq extends NotificationProvider {
|
||||
|
||||
/**
|
||||
* 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}
|
||||
* @param {object} args Method arguments
|
||||
* @param {const} args.status The status of the monitor
|
||||
* @param {string} args.monitorMessage Message to send
|
||||
* @param {string} args.monitorName Name of monitor affected
|
||||
* @param {string} args.monitorUrl URL of monitor affected
|
||||
* @returns {Array} Notification payload
|
||||
*/
|
||||
_notificationPayloadFactory = ({
|
||||
status,
|
||||
@@ -74,6 +76,9 @@ class ZohoCliq extends NotificationProvider {
|
||||
return this._sendNotification(webhookUrl, payload);
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
|
@@ -14,6 +14,7 @@ const FreeMobile = require("./notification-providers/freemobile");
|
||||
const GoogleChat = require("./notification-providers/google-chat");
|
||||
const Gorush = require("./notification-providers/gorush");
|
||||
const Gotify = require("./notification-providers/gotify");
|
||||
const GrafanaOncall = require("./notification-providers/grafana-oncall");
|
||||
const HomeAssistant = require("./notification-providers/home-assistant");
|
||||
const Kook = require("./notification-providers/kook");
|
||||
const Line = require("./notification-providers/line");
|
||||
@@ -58,9 +59,14 @@ class Notification {
|
||||
|
||||
providerList = {};
|
||||
|
||||
/** Initialize the notification providers */
|
||||
/**
|
||||
* Initialize the notification providers
|
||||
* @returns {void}
|
||||
* @throws Notification provider does not have a name
|
||||
* @throws Duplicate notification providers in list
|
||||
*/
|
||||
static init() {
|
||||
log.info("notification", "Prepare Notification Providers");
|
||||
log.debug("notification", "Prepare Notification Providers");
|
||||
|
||||
this.providerList = {};
|
||||
|
||||
@@ -79,6 +85,7 @@ class Notification {
|
||||
new GoogleChat(),
|
||||
new Gorush(),
|
||||
new Gotify(),
|
||||
new GrafanaOncall(),
|
||||
new HomeAssistant(),
|
||||
new Kook(),
|
||||
new Line(),
|
||||
@@ -133,10 +140,10 @@ class Notification {
|
||||
|
||||
/**
|
||||
* Send a notification
|
||||
* @param {BeanModel} notification
|
||||
* @param {BeanModel} notification Notification to send
|
||||
* @param {string} msg General Message
|
||||
* @param {Object} monitorJSON Monitor details (For Up/Down only)
|
||||
* @param {Object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||
* @param {object} monitorJSON Monitor details (For Up/Down only)
|
||||
* @param {object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||
* @returns {Promise<string>} Successful msg
|
||||
* @throws Error with fail msg
|
||||
*/
|
||||
@@ -150,10 +157,10 @@ class Notification {
|
||||
|
||||
/**
|
||||
* Save a notification
|
||||
* @param {Object} notification Notification to save
|
||||
* @param {object} notification Notification to save
|
||||
* @param {?number} notificationID ID of notification to update
|
||||
* @param {number} userID ID of user who adds notification
|
||||
* @returns {Promise<Bean>}
|
||||
* @returns {Promise<Bean>} Notification that was saved
|
||||
*/
|
||||
static async save(notification, notificationID, userID) {
|
||||
let bean;
|
||||
|
@@ -4,8 +4,8 @@ const saltRounds = 10;
|
||||
|
||||
/**
|
||||
* Hash a password
|
||||
* @param {string} password
|
||||
* @returns {string}
|
||||
* @param {string} password Password to hash
|
||||
* @returns {string} Hash
|
||||
*/
|
||||
exports.generate = function (password) {
|
||||
return bcrypt.hashSync(password, saltRounds);
|
||||
@@ -13,8 +13,8 @@ exports.generate = function (password) {
|
||||
|
||||
/**
|
||||
* Verify a password against a hash
|
||||
* @param {string} password
|
||||
* @param {string} hash
|
||||
* @param {string} password Password to verify
|
||||
* @param {string} hash Hash to verify against
|
||||
* @returns {boolean} Does the password match the hash?
|
||||
*/
|
||||
exports.verify = function (password, hash) {
|
||||
@@ -27,8 +27,8 @@ exports.verify = function (password, hash) {
|
||||
|
||||
/**
|
||||
* Is the hash a SHA1 hash
|
||||
* @param {string} hash
|
||||
* @returns {boolean}
|
||||
* @param {string} hash Hash to check
|
||||
* @returns {boolean} Is SHA1 hash?
|
||||
*/
|
||||
function isSHA1(hash) {
|
||||
return (typeof hash === "string" && hash.startsWith("sha1"));
|
||||
@@ -36,7 +36,8 @@ function isSHA1(hash) {
|
||||
|
||||
/**
|
||||
* Does the hash need to be rehashed?
|
||||
* @returns {boolean}
|
||||
* @param {string} hash Hash to check
|
||||
* @returns {boolean} Needs to be rehashed?
|
||||
*/
|
||||
exports.needRehash = function (hash) {
|
||||
return isSHA1(hash);
|
||||
|
@@ -36,7 +36,7 @@ class Prometheus {
|
||||
monitorLabelValues = {};
|
||||
|
||||
/**
|
||||
* @param {Object} monitor Monitor object to monitor
|
||||
* @param {object} monitor Monitor object to monitor
|
||||
*/
|
||||
constructor(monitor) {
|
||||
this.monitorLabelValues = {
|
||||
@@ -50,8 +50,9 @@ class Prometheus {
|
||||
|
||||
/**
|
||||
* Update the metrics page
|
||||
* @param {Object} heartbeat Heartbeat details
|
||||
* @param {Object} tlsInfo TLS details
|
||||
* @param {object} heartbeat Heartbeat details
|
||||
* @param {object} tlsInfo TLS details
|
||||
* @returns {void}
|
||||
*/
|
||||
update(heartbeat, tlsInfo) {
|
||||
|
||||
@@ -99,7 +100,10 @@ class Prometheus {
|
||||
}
|
||||
}
|
||||
|
||||
/** Remove monitor from prometheus */
|
||||
/**
|
||||
* Remove monitor from prometheus
|
||||
* @returns {void}
|
||||
*/
|
||||
remove() {
|
||||
try {
|
||||
monitorCertDaysRemaining.remove(this.monitorLabelValues);
|
||||
|
@@ -11,11 +11,10 @@ class Proxy {
|
||||
|
||||
/**
|
||||
* Saves and updates given proxy entity
|
||||
*
|
||||
* @param proxy
|
||||
* @param proxyID
|
||||
* @param userID
|
||||
* @return {Promise<Bean>}
|
||||
* @param {object} proxy Proxy to store
|
||||
* @param {number} proxyID ID of proxy to update
|
||||
* @param {number} userID ID of user the proxy belongs to
|
||||
* @returns {Promise<Bean>} Updated proxy
|
||||
*/
|
||||
static async save(proxy, proxyID, userID) {
|
||||
let bean;
|
||||
@@ -65,10 +64,9 @@ class Proxy {
|
||||
|
||||
/**
|
||||
* Deletes proxy with given id and removes it from monitors
|
||||
*
|
||||
* @param proxyID
|
||||
* @param userID
|
||||
* @return {Promise<void>}
|
||||
* @param {number} proxyID ID of proxy to delete
|
||||
* @param {number} userID ID of proxy owner
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async delete(proxyID, userID) {
|
||||
const bean = await R.findOne("proxy", " id = ? AND user_id = ? ", [ proxyID, userID ]);
|
||||
@@ -86,10 +84,10 @@ class Proxy {
|
||||
|
||||
/**
|
||||
* Create HTTP and HTTPS agents related with given proxy bean object
|
||||
*
|
||||
* @param proxy proxy bean object
|
||||
* @param options http and https agent options
|
||||
* @return {{httpAgent: Agent, httpsAgent: Agent}}
|
||||
* @param {object} proxy proxy bean object
|
||||
* @param {object} options http and https agent options
|
||||
* @returns {{httpAgent: Agent, httpsAgent: Agent}} New HTTP and HTTPS agents
|
||||
* @throws Proxy protocol is unsupported
|
||||
*/
|
||||
static createAgents(proxy, options) {
|
||||
const { httpAgentOptions, httpsAgentOptions } = options || {};
|
||||
@@ -171,10 +169,9 @@ class Proxy {
|
||||
|
||||
/**
|
||||
* Applies given proxy id to monitors
|
||||
*
|
||||
* @param proxyID
|
||||
* @param userID
|
||||
* @return {Promise<void>}
|
||||
* @param {number} proxyID ID of proxy to apply
|
||||
* @param {number} userID ID of proxy owner
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function applyProxyEveryMonitor(proxyID, userID) {
|
||||
// Find all monitors with id and proxy id
|
||||
|
@@ -3,7 +3,7 @@ const { log } = require("../src/util");
|
||||
|
||||
class KumaRateLimiter {
|
||||
/**
|
||||
* @param {Object} config Rate limiter configuration object
|
||||
* @param {object} config Rate limiter configuration object
|
||||
*/
|
||||
constructor(config) {
|
||||
this.errorMessage = config.errorMessage;
|
||||
@@ -13,14 +13,14 @@ class KumaRateLimiter {
|
||||
/**
|
||||
* Callback for pass
|
||||
* @callback passCB
|
||||
* @param {Object} err Too many requests
|
||||
* @param {object} err Too many requests
|
||||
*/
|
||||
|
||||
/**
|
||||
* Should the request be passed through
|
||||
* @param {passCB} callback
|
||||
* @param {number} [num=1] Number of tokens to remove
|
||||
* @returns {Promise<boolean>}
|
||||
* @param {passCB} callback Callback function to call with decision
|
||||
* @param {number} num Number of tokens to remove
|
||||
* @returns {Promise<boolean>} Should the request be allowed?
|
||||
*/
|
||||
async pass(callback, num = 1) {
|
||||
const remainingRequests = await this.removeTokens(num);
|
||||
@@ -39,8 +39,8 @@ class KumaRateLimiter {
|
||||
|
||||
/**
|
||||
* Remove a given number of tokens
|
||||
* @param {number} [num=1] Number of tokens to remove
|
||||
* @returns {Promise<number>}
|
||||
* @param {number} num Number of tokens to remove
|
||||
* @returns {Promise<number>} Number of remaining tokens
|
||||
*/
|
||||
async removeTokens(num = 1) {
|
||||
return await this.rateLimiter.removeTokens(num);
|
||||
|
@@ -14,10 +14,11 @@ const dayjs = require("dayjs");
|
||||
const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log } = require("../../src/util");
|
||||
const StatusPage = require("../model/status_page");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const { UptimeCacheList } = require("../uptime-cache-list");
|
||||
const { makeBadge } = require("badge-maker");
|
||||
const { badgeConstants } = require("../config");
|
||||
const { Prometheus } = require("../prometheus");
|
||||
const Database = require("../database");
|
||||
const { UptimeCalculator } = require("../uptime-calculator");
|
||||
|
||||
let router = express.Router();
|
||||
|
||||
@@ -99,7 +100,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||
await R.store(bean);
|
||||
|
||||
io.to(monitor.user_id).emit("heartbeat", bean.toJSON());
|
||||
UptimeCacheList.clearCache(monitor.id);
|
||||
|
||||
Monitor.sendStats(io, monitor.id, monitor.user_id);
|
||||
new Prometheus(monitor).update(bean, undefined);
|
||||
|
||||
@@ -216,9 +217,13 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques
|
||||
try {
|
||||
const requestedMonitorId = parseInt(request.params.id, 10);
|
||||
// if no duration is given, set value to 24 (h)
|
||||
const requestedDuration = request.params.duration !== undefined ? parseInt(request.params.duration, 10) : 24;
|
||||
let requestedDuration = request.params.duration !== undefined ? request.params.duration : "24h";
|
||||
const overrideValue = value && parseFloat(value);
|
||||
|
||||
if (requestedDuration === "24") {
|
||||
requestedDuration = "24h";
|
||||
}
|
||||
|
||||
let publicMonitor = await R.getRow(`
|
||||
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||
WHERE monitor_group.group_id = \`group\`.id
|
||||
@@ -235,10 +240,8 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques
|
||||
badgeValues.message = "N/A";
|
||||
badgeValues.color = badgeConstants.naColor;
|
||||
} else {
|
||||
const uptime = overrideValue ?? await Monitor.calcUptime(
|
||||
requestedDuration,
|
||||
requestedMonitorId
|
||||
);
|
||||
const uptimeCalculator = await UptimeCalculator.getUptimeCalculator(requestedMonitorId);
|
||||
const uptime = overrideValue ?? uptimeCalculator.getDataByDuration(requestedDuration).uptime;
|
||||
|
||||
// limit the displayed uptime percentage to four (two, when displayed as percent) decimal digits
|
||||
const cleanUptime = (uptime * 100).toPrecision(4);
|
||||
@@ -284,19 +287,17 @@ router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request,
|
||||
const requestedMonitorId = parseInt(request.params.id, 10);
|
||||
|
||||
// Default duration is 24 (h) if not defined in queryParam, limited to 720h (30d)
|
||||
const requestedDuration = Math.min(request.params.duration ? parseInt(request.params.duration, 10) : 24, 720);
|
||||
let requestedDuration = request.params.duration !== undefined ? request.params.duration : "24h";
|
||||
const overrideValue = value && parseFloat(value);
|
||||
|
||||
const publicAvgPing = parseInt(await R.getCell(`
|
||||
SELECT AVG(ping) FROM monitor_group, \`group\`, heartbeat
|
||||
WHERE monitor_group.group_id = \`group\`.id
|
||||
AND heartbeat.time > DATETIME('now', ? || ' hours')
|
||||
AND heartbeat.ping IS NOT NULL
|
||||
AND public = 1
|
||||
AND heartbeat.monitor_id = ?
|
||||
`,
|
||||
[ -requestedDuration, requestedMonitorId ]
|
||||
));
|
||||
if (requestedDuration === "24") {
|
||||
requestedDuration = "24h";
|
||||
}
|
||||
|
||||
// Check if monitor is public
|
||||
|
||||
const uptimeCalculator = await UptimeCalculator.getUptimeCalculator(requestedMonitorId);
|
||||
const publicAvgPing = uptimeCalculator.getDataByDuration(requestedDuration).avgPing;
|
||||
|
||||
const badgeValues = { style };
|
||||
|
||||
@@ -353,10 +354,12 @@ router.get("/api/badge/:id/avg-response/:duration?", cache("5 minutes"), async (
|
||||
);
|
||||
const overrideValue = value && parseFloat(value);
|
||||
|
||||
const sqlHourOffset = Database.sqlHourOffset();
|
||||
|
||||
const publicAvgPing = parseInt(await R.getCell(`
|
||||
SELECT AVG(ping) FROM monitor_group, \`group\`, heartbeat
|
||||
WHERE monitor_group.group_id = \`group\`.id
|
||||
AND heartbeat.time > DATETIME('now', ? || ' hours')
|
||||
AND heartbeat.time > ${sqlHourOffset}
|
||||
AND heartbeat.ping IS NOT NULL
|
||||
AND public = 1
|
||||
AND heartbeat.monitor_id = ?
|
||||
|
@@ -4,9 +4,9 @@ const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
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");
|
||||
const { UptimeCalculator } = require("../uptime-calculator");
|
||||
|
||||
let router = express.Router();
|
||||
|
||||
@@ -92,8 +92,8 @@ router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (reques
|
||||
list = R.convertToBeans("heartbeat", list);
|
||||
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());
|
||||
|
||||
const type = 24;
|
||||
uptimeList[`${monitorID}_${type}`] = await Monitor.calcUptime(type, monitorID);
|
||||
const uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitorID);
|
||||
uptimeList[`${monitorID}_24`] = uptimeCalculator.get24Hour().uptime;
|
||||
}
|
||||
|
||||
response.json({
|
||||
|
494
server/server.js
494
server/server.js
@@ -38,9 +38,7 @@ if (!semver.satisfies(nodeVersion, requiredNodeVersions)) {
|
||||
|
||||
const args = require("args-parser")(process.argv);
|
||||
const { sleep, log, getRandomInt, genSecret, isDev } = require("../src/util");
|
||||
const config = require("./config");
|
||||
|
||||
log.info("server", "Welcome to Uptime Kuma");
|
||||
log.debug("server", "Arguments");
|
||||
log.debug("server", args);
|
||||
|
||||
@@ -48,13 +46,13 @@ if (! process.env.NODE_ENV) {
|
||||
process.env.NODE_ENV = "production";
|
||||
}
|
||||
|
||||
log.info("server", "Node Env: " + process.env.NODE_ENV);
|
||||
log.info("server", "Inside Container: " + (process.env.UPTIME_KUMA_IS_CONTAINER === "1"));
|
||||
log.info("server", "Env: " + process.env.NODE_ENV);
|
||||
log.debug("server", "Inside Container: " + (process.env.UPTIME_KUMA_IS_CONTAINER === "1"));
|
||||
|
||||
log.info("server", "Importing Node libraries");
|
||||
const fs = require("fs");
|
||||
const checkVersion = require("./check-version");
|
||||
log.info("server", "Uptime Kuma Version: " + checkVersion.version);
|
||||
|
||||
log.info("server", "Importing 3rd-party libraries");
|
||||
log.info("server", "Loading modules");
|
||||
|
||||
log.debug("server", "Importing express");
|
||||
const express = require("express");
|
||||
@@ -67,8 +65,6 @@ log.debug("server", "Importing http-graceful-shutdown");
|
||||
const gracefulShutdown = require("http-graceful-shutdown");
|
||||
log.debug("server", "Importing prometheus-api-metrics");
|
||||
const prometheusAPIMetrics = require("prometheus-api-metrics");
|
||||
log.debug("server", "Importing compare-versions");
|
||||
const compareVersions = require("compare-versions");
|
||||
const { passwordStrength } = require("check-password-strength");
|
||||
|
||||
log.debug("server", "Importing 2FA Modules");
|
||||
@@ -76,26 +72,23 @@ const notp = require("notp");
|
||||
const base32 = require("thirty-two");
|
||||
|
||||
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
||||
|
||||
const server = UptimeKumaServer.getInstance(args);
|
||||
const io = module.exports.io = server.io;
|
||||
const app = server.app;
|
||||
|
||||
log.info("server", "Importing this project modules");
|
||||
log.debug("server", "Importing Monitor");
|
||||
const Monitor = require("./model/monitor");
|
||||
const User = require("./model/user");
|
||||
|
||||
log.debug("server", "Importing Settings");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword, startE2eTests, shake256, SHAKE256_LENGTH
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, FBSD, doubleCheckPassword, startE2eTests, shake256, SHAKE256_LENGTH, allowDevAllOrigin,
|
||||
} = require("./util-server");
|
||||
|
||||
log.debug("server", "Importing Notification");
|
||||
const { Notification } = require("./notification");
|
||||
Notification.init();
|
||||
|
||||
log.debug("server", "Importing Proxy");
|
||||
const { Proxy } = require("./proxy");
|
||||
|
||||
log.debug("server", "Importing Database");
|
||||
const Database = require("./database");
|
||||
|
||||
@@ -107,9 +100,6 @@ const { apiAuth } = require("./auth");
|
||||
const { login } = require("./auth");
|
||||
const passwordHash = require("./password-hash");
|
||||
|
||||
const checkVersion = require("./check-version");
|
||||
log.info("server", "Version: " + checkVersion.version);
|
||||
|
||||
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
|
||||
// Dual-stack support for (::)
|
||||
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
|
||||
@@ -140,12 +130,8 @@ const twoFAVerifyOptions = {
|
||||
const testMode = !!args["test"] || false;
|
||||
const e2eTestMode = !!args["e2e"] || false;
|
||||
|
||||
if (config.demoMode) {
|
||||
log.info("server", "==== Demo Mode ====");
|
||||
}
|
||||
|
||||
// Must be after io instantiation
|
||||
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList } = require("./client");
|
||||
const { sendNotificationList, sendHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList } = require("./client");
|
||||
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
||||
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
||||
const TwoFA = require("./2fa");
|
||||
@@ -160,6 +146,8 @@ const { Settings } = require("./settings");
|
||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||
const apicache = require("./modules/apicache");
|
||||
const { resetChrome } = require("./monitor-types/real-browser-monitor-type");
|
||||
const { EmbeddedMariaDB } = require("./embedded-mariadb");
|
||||
const { SetupDatabase } = require("./setup-database");
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
@@ -179,13 +167,30 @@ app.use(function (req, res, next) {
|
||||
let needSetup = false;
|
||||
|
||||
(async () => {
|
||||
Database.init(args);
|
||||
await initDatabase(testMode);
|
||||
// Create a data directory
|
||||
Database.initDataDir(args);
|
||||
|
||||
// Check if is chosen a database type
|
||||
let setupDatabase = new SetupDatabase(args, server);
|
||||
if (setupDatabase.isNeedSetup()) {
|
||||
// Hold here and start a special setup page until user choose a database type
|
||||
await setupDatabase.start(hostname, port);
|
||||
}
|
||||
|
||||
// Connect to database
|
||||
try {
|
||||
await initDatabase(testMode);
|
||||
} catch (e) {
|
||||
log.error("server", "Failed to prepare your database: " + e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Database should be ready now
|
||||
await server.initAfterDatabaseReady();
|
||||
server.entryPage = await Settings.get("entryPage");
|
||||
await StatusPage.loadDomainMappingList();
|
||||
|
||||
log.info("server", "Adding route");
|
||||
log.debug("server", "Adding route");
|
||||
|
||||
// ***************************
|
||||
// Normal Router here
|
||||
@@ -218,6 +223,14 @@ let needSetup = false;
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/setup-database-info", (request, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
response.json({
|
||||
runningSetup: false,
|
||||
needSetup: false,
|
||||
});
|
||||
});
|
||||
|
||||
if (isDev) {
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.post("/test-webhook", async (request, response) => {
|
||||
@@ -225,6 +238,12 @@ let needSetup = false;
|
||||
log.debug("test", request.body);
|
||||
response.send("OK");
|
||||
});
|
||||
|
||||
app.post("/test-x-www-form-urlencoded", async (request, response) => {
|
||||
log.debug("test", request.headers);
|
||||
log.debug("test", request.body);
|
||||
response.send("OK");
|
||||
});
|
||||
}
|
||||
|
||||
// Robots.txt
|
||||
@@ -271,7 +290,7 @@ let needSetup = false;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("server", "Adding socket handler");
|
||||
log.debug("server", "Adding socket handler");
|
||||
io.on("connection", async (socket) => {
|
||||
|
||||
sendInfo(socket, true);
|
||||
@@ -320,7 +339,8 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "The user is inactive or deleted.",
|
||||
msg: "authUserInactiveOrDeleted",
|
||||
msgi18n: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -330,7 +350,8 @@ let needSetup = false;
|
||||
}
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Invalid token.",
|
||||
msg: "authInvalidToken",
|
||||
msgi18n: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -351,7 +372,7 @@ let needSetup = false;
|
||||
}
|
||||
|
||||
// Login Rate Limit
|
||||
if (! await loginRateLimiter.pass(callback)) {
|
||||
if (!await loginRateLimiter.pass(callback)) {
|
||||
log.info("auth", `Too many failed requests for user ${data.username}. IP=${clientIP}`);
|
||||
return;
|
||||
}
|
||||
@@ -402,7 +423,8 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Invalid Token!",
|
||||
msg: "authInvalidToken",
|
||||
msgi18n: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -412,7 +434,8 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Incorrect username or password.",
|
||||
msg: "authIncorrectCreds",
|
||||
msgi18n: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -420,7 +443,7 @@ let needSetup = false;
|
||||
|
||||
socket.on("logout", async (callback) => {
|
||||
// Rate Limit
|
||||
if (! await loginRateLimiter.pass(callback)) {
|
||||
if (!await loginRateLimiter.pass(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -434,7 +457,7 @@ let needSetup = false;
|
||||
|
||||
socket.on("prepare2FA", async (currentPassword, callback) => {
|
||||
try {
|
||||
if (! await twoFaRateLimiter.pass(callback)) {
|
||||
if (!await twoFaRateLimiter.pass(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -468,7 +491,8 @@ let needSetup = false;
|
||||
} else {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "2FA is already enabled.",
|
||||
msg: "2faAlreadyEnabled",
|
||||
msgi18n: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -483,7 +507,7 @@ let needSetup = false;
|
||||
const clientIP = await server.getClientIP(socket);
|
||||
|
||||
try {
|
||||
if (! await twoFaRateLimiter.pass(callback)) {
|
||||
if (!await twoFaRateLimiter.pass(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -498,7 +522,8 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "2FA Enabled.",
|
||||
msg: "2faEnabled",
|
||||
msgi18n: true,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -515,7 +540,7 @@ let needSetup = false;
|
||||
const clientIP = await server.getClientIP(socket);
|
||||
|
||||
try {
|
||||
if (! await twoFaRateLimiter.pass(callback)) {
|
||||
if (!await twoFaRateLimiter.pass(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -527,7 +552,8 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "2FA Disabled.",
|
||||
msg: "2faDisabled",
|
||||
msgi18n: true,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -559,7 +585,8 @@ let needSetup = false;
|
||||
} else {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Invalid Token.",
|
||||
msg: "authInvalidToken",
|
||||
msgi18n: true,
|
||||
valid: false,
|
||||
});
|
||||
}
|
||||
@@ -609,7 +636,7 @@ let needSetup = false;
|
||||
throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length.");
|
||||
}
|
||||
|
||||
if ((await R.count("user")) !== 0) {
|
||||
if ((await R.knex("user").count("id as count").first()).count !== 0) {
|
||||
throw new Error("Uptime Kuma has been initialized. If you want to run setup again, please delete the database.");
|
||||
}
|
||||
|
||||
@@ -622,7 +649,8 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Added Successfully.",
|
||||
msg: "successAdded",
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
@@ -675,7 +703,8 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Added Successfully.",
|
||||
msg: "successAdded",
|
||||
msgi18n: true,
|
||||
monitorID: bean.id,
|
||||
});
|
||||
|
||||
@@ -746,6 +775,11 @@ let needSetup = false;
|
||||
bean.game = monitor.game;
|
||||
bean.maxretries = monitor.maxretries;
|
||||
bean.port = parseInt(monitor.port);
|
||||
|
||||
if (isNaN(bean.port)) {
|
||||
bean.port = null;
|
||||
}
|
||||
|
||||
bean.keyword = monitor.keyword;
|
||||
bean.invertKeyword = monitor.invertKeyword;
|
||||
bean.ignoreTls = monitor.ignoreTls;
|
||||
@@ -813,6 +847,7 @@ let needSetup = false;
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Saved.",
|
||||
msgi18n: true,
|
||||
monitorID: bean.id,
|
||||
});
|
||||
|
||||
@@ -875,14 +910,17 @@ let needSetup = false;
|
||||
throw new Error("Invalid period.");
|
||||
}
|
||||
|
||||
const sqlHourOffset = Database.sqlHourOffset();
|
||||
|
||||
let list = await R.getAll(`
|
||||
SELECT * FROM heartbeat
|
||||
WHERE monitor_id = ? AND
|
||||
time > DATETIME('now', '-' || ? || ' hours')
|
||||
SELECT *
|
||||
FROM heartbeat
|
||||
WHERE monitor_id = ?
|
||||
AND time > ${sqlHourOffset}
|
||||
ORDER BY time ASC
|
||||
`, [
|
||||
monitorID,
|
||||
period,
|
||||
-period,
|
||||
]);
|
||||
|
||||
callback({
|
||||
@@ -906,7 +944,8 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Resumed Successfully.",
|
||||
msg: "successResumed",
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
@@ -925,7 +964,8 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Paused Successfully.",
|
||||
msg: "successPaused",
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
@@ -963,12 +1003,11 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted Successfully.",
|
||||
msg: "successDeleted",
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
await server.sendMonitorList(socket);
|
||||
// Clear heartbeat list on client
|
||||
await sendImportantHeartbeatList(socket, monitorID, true, true);
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
@@ -1027,7 +1066,8 @@ let needSetup = false;
|
||||
if (bean == null) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Tag not found",
|
||||
msg: "tagNotFound",
|
||||
msgi18n: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -1037,7 +1077,8 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Saved",
|
||||
msg: "Saved.",
|
||||
msgi18n: true,
|
||||
tag: await bean.toJSON(),
|
||||
});
|
||||
|
||||
@@ -1057,7 +1098,8 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted Successfully.",
|
||||
msg: "successDeleted",
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
@@ -1080,7 +1122,8 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Added Successfully.",
|
||||
msg: "successAdded",
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
@@ -1103,7 +1146,8 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Edited Successfully.",
|
||||
msg: "successEdited",
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
@@ -1126,7 +1170,8 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted Successfully.",
|
||||
msg: "successDeleted",
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
@@ -1137,11 +1182,77 @@ let needSetup = false;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("monitorImportantHeartbeatListCount", async (monitorID, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
let count;
|
||||
if (monitorID == null) {
|
||||
count = await R.count("heartbeat", "important = 1");
|
||||
} else {
|
||||
count = await R.count("heartbeat", "monitor_id = ? AND important = 1", [
|
||||
monitorID,
|
||||
]);
|
||||
}
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
count: count,
|
||||
});
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("monitorImportantHeartbeatListPaged", async (monitorID, offset, count, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
let list;
|
||||
if (monitorID == null) {
|
||||
list = await R.find("heartbeat", `
|
||||
important = 1
|
||||
ORDER BY time DESC
|
||||
LIMIT ?
|
||||
OFFSET ?
|
||||
`, [
|
||||
count,
|
||||
offset,
|
||||
]);
|
||||
} else {
|
||||
list = await R.find("heartbeat", `
|
||||
monitor_id = ?
|
||||
AND important = 1
|
||||
ORDER BY time DESC
|
||||
LIMIT ?
|
||||
OFFSET ?
|
||||
`, [
|
||||
monitorID,
|
||||
count,
|
||||
offset,
|
||||
]);
|
||||
}
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
data: list,
|
||||
});
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("changePassword", async (password, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
if (! password.newPassword) {
|
||||
if (!password.newPassword) {
|
||||
throw new Error("Invalid new password");
|
||||
}
|
||||
|
||||
@@ -1154,7 +1265,8 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Password has been updated successfully.",
|
||||
msg: "successAuthChangePassword",
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
@@ -1231,7 +1343,8 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Saved"
|
||||
msg: "Saved.",
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
sendInfo(socket);
|
||||
@@ -1255,7 +1368,8 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Saved",
|
||||
msg: "Saved.",
|
||||
msgi18n: true,
|
||||
id: notificationBean.id,
|
||||
});
|
||||
|
||||
@@ -1276,7 +1390,8 @@ let needSetup = false;
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted",
|
||||
msg: "successDeleted",
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
@@ -1317,211 +1432,6 @@ let needSetup = false;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("uploadBackup", async (uploadedJSON, importHandle, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
let backupData = JSON.parse(uploadedJSON);
|
||||
|
||||
log.info("manage", `Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`);
|
||||
|
||||
let notificationListData = backupData.notificationList;
|
||||
let proxyListData = backupData.proxyList;
|
||||
let monitorListData = backupData.monitorList;
|
||||
|
||||
let version17x = compareVersions.compare(backupData.version, "1.7.0", ">=");
|
||||
|
||||
// If the import option is "overwrite" it'll clear most of the tables, except "settings" and "user"
|
||||
if (importHandle === "overwrite") {
|
||||
// Stops every monitor first, so it doesn't execute any heartbeat while importing
|
||||
for (let id in server.monitorList) {
|
||||
let monitor = server.monitorList[id];
|
||||
await monitor.stop();
|
||||
}
|
||||
await R.exec("DELETE FROM heartbeat");
|
||||
await R.exec("DELETE FROM monitor_notification");
|
||||
await R.exec("DELETE FROM monitor_tls_info");
|
||||
await R.exec("DELETE FROM notification");
|
||||
await R.exec("DELETE FROM monitor_tag");
|
||||
await R.exec("DELETE FROM tag");
|
||||
await R.exec("DELETE FROM monitor");
|
||||
await R.exec("DELETE FROM proxy");
|
||||
}
|
||||
|
||||
// Only starts importing if the backup file contains at least one notification
|
||||
if (notificationListData.length >= 1) {
|
||||
// Get every existing notification name and puts them in one simple string
|
||||
let notificationNameList = await R.getAll("SELECT name FROM notification");
|
||||
let notificationNameListString = JSON.stringify(notificationNameList);
|
||||
|
||||
for (let i = 0; i < notificationListData.length; i++) {
|
||||
// Only starts importing the notification if the import option is "overwrite", "keep" or "skip" but the notification doesn't exists
|
||||
if ((importHandle === "skip" && notificationNameListString.includes(notificationListData[i].name) === false) || importHandle === "keep" || importHandle === "overwrite") {
|
||||
|
||||
let notification = JSON.parse(notificationListData[i].config);
|
||||
await Notification.save(notification, null, socket.userID);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only starts importing if the backup file contains at least one proxy
|
||||
if (proxyListData && proxyListData.length >= 1) {
|
||||
const proxies = await R.findAll("proxy");
|
||||
|
||||
// Loop over proxy list and save proxies
|
||||
for (const proxy of proxyListData) {
|
||||
const exists = proxies.find(item => item.id === proxy.id);
|
||||
|
||||
// Do not process when proxy already exists in import handle is skip and keep
|
||||
if ([ "skip", "keep" ].includes(importHandle) && !exists) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save proxy as new entry if exists update exists one
|
||||
await Proxy.save(proxy, exists ? proxy.id : undefined, proxy.userId);
|
||||
}
|
||||
}
|
||||
|
||||
// Only starts importing if the backup file contains at least one monitor
|
||||
if (monitorListData.length >= 1) {
|
||||
// Get every existing monitor name and puts them in one simple string
|
||||
let monitorNameList = await R.getAll("SELECT name FROM monitor");
|
||||
let monitorNameListString = JSON.stringify(monitorNameList);
|
||||
|
||||
for (let i = 0; i < monitorListData.length; i++) {
|
||||
// Only starts importing the monitor if the import option is "overwrite", "keep" or "skip" but the notification doesn't exists
|
||||
if ((importHandle === "skip" && monitorNameListString.includes(monitorListData[i].name) === false) || importHandle === "keep" || importHandle === "overwrite") {
|
||||
|
||||
// Define in here every new variable for monitors which where implemented after the first version of the Import/Export function (1.6.0)
|
||||
// --- Start ---
|
||||
|
||||
// Define default values
|
||||
let retryInterval = 0;
|
||||
let timeout = monitorListData[i].timeout || (monitorListData[i].interval * 0.8); // fallback to old value
|
||||
|
||||
/*
|
||||
Only replace the default value with the backup file data for the specific version, where it appears the first time
|
||||
More information about that where "let version" will be defined
|
||||
*/
|
||||
if (version17x) {
|
||||
retryInterval = monitorListData[i].retryInterval;
|
||||
}
|
||||
|
||||
// --- End ---
|
||||
|
||||
let monitor = {
|
||||
// Define the new variable from earlier here
|
||||
name: monitorListData[i].name,
|
||||
description: monitorListData[i].description,
|
||||
type: monitorListData[i].type,
|
||||
url: monitorListData[i].url,
|
||||
method: monitorListData[i].method || "GET",
|
||||
body: monitorListData[i].body,
|
||||
headers: monitorListData[i].headers,
|
||||
authMethod: monitorListData[i].authMethod,
|
||||
basic_auth_user: monitorListData[i].basic_auth_user,
|
||||
basic_auth_pass: monitorListData[i].basic_auth_pass,
|
||||
authWorkstation: monitorListData[i].authWorkstation,
|
||||
authDomain: monitorListData[i].authDomain,
|
||||
timeout,
|
||||
interval: monitorListData[i].interval,
|
||||
retryInterval: retryInterval,
|
||||
resendInterval: monitorListData[i].resendInterval || 0,
|
||||
hostname: monitorListData[i].hostname,
|
||||
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: monitorListData[i].notificationIDList,
|
||||
proxy_id: monitorListData[i].proxy_id || null,
|
||||
};
|
||||
|
||||
if (monitorListData[i].pushToken) {
|
||||
monitor.pushToken = monitorListData[i].pushToken;
|
||||
}
|
||||
|
||||
let bean = R.dispense("monitor");
|
||||
|
||||
let notificationIDList = monitor.notificationIDList;
|
||||
delete monitor.notificationIDList;
|
||||
|
||||
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
|
||||
delete monitor.accepted_statuscodes;
|
||||
|
||||
bean.import(monitor);
|
||||
bean.user_id = socket.userID;
|
||||
await R.store(bean);
|
||||
|
||||
// Only for backup files with the version 1.7.0 or higher, since there was the tag feature implemented
|
||||
if (version17x) {
|
||||
// Only import if the specific monitor has tags assigned
|
||||
for (const oldTag of monitorListData[i].tags) {
|
||||
|
||||
// Check if tag already exists and get data ->
|
||||
let tag = await R.findOne("tag", " name = ?", [
|
||||
oldTag.name,
|
||||
]);
|
||||
|
||||
let tagId;
|
||||
if (! tag) {
|
||||
// -> If it doesn't exist, create new tag from backup file
|
||||
let beanTag = R.dispense("tag");
|
||||
beanTag.name = oldTag.name;
|
||||
beanTag.color = oldTag.color;
|
||||
await R.store(beanTag);
|
||||
|
||||
tagId = beanTag.id;
|
||||
} else {
|
||||
// -> If it already exist, set tagId to value from database
|
||||
tagId = tag.id;
|
||||
}
|
||||
|
||||
// Assign the new created tag to the monitor
|
||||
await R.exec("INSERT INTO monitor_tag (tag_id, monitor_id, value) VALUES (?, ?, ?)", [
|
||||
tagId,
|
||||
bean.id,
|
||||
oldTag.value,
|
||||
]);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
await updateMonitorNotification(bean.id, notificationIDList);
|
||||
|
||||
// If monitor was active start it immediately, otherwise pause it
|
||||
if (monitorListData[i].active === 1) {
|
||||
await startMonitor(socket.userID, bean.id);
|
||||
} else {
|
||||
await pauseMonitor(socket.userID, bean.id);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
await sendNotificationList(socket);
|
||||
await server.sendMonitorList(socket);
|
||||
}
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Backup successfully restored.",
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("clearEvents", async (monitorID, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
@@ -1534,8 +1444,6 @@ let needSetup = false;
|
||||
monitorID,
|
||||
]);
|
||||
|
||||
await sendImportantHeartbeatList(socket, monitorID, true, true);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
});
|
||||
@@ -1619,11 +1527,12 @@ let needSetup = false;
|
||||
|
||||
});
|
||||
|
||||
log.info("server", "Init the server");
|
||||
log.debug("server", "Init the server");
|
||||
|
||||
server.httpServer.once("error", async (err) => {
|
||||
console.error("Cannot listen: " + err.message);
|
||||
log.error("server", "Cannot listen: " + err.message);
|
||||
await shutdownFunction();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
server.start();
|
||||
@@ -1637,10 +1546,6 @@ let needSetup = false;
|
||||
startMonitors();
|
||||
checkVersion.startInterval();
|
||||
|
||||
if (testMode) {
|
||||
startUnitTest();
|
||||
}
|
||||
|
||||
if (e2eTestMode) {
|
||||
startE2eTests();
|
||||
}
|
||||
@@ -1677,8 +1582,8 @@ async function updateMonitorNotification(monitorID, notificationIDList) {
|
||||
|
||||
/**
|
||||
* Check if a given user owns a specific monitor
|
||||
* @param {number} userID
|
||||
* @param {number} monitorID
|
||||
* @param {number} userID ID of user to check
|
||||
* @param {number} monitorID ID of monitor to check
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} The specified user does not own the monitor
|
||||
*/
|
||||
@@ -1697,7 +1602,7 @@ async function checkOwner(userID, monitorID) {
|
||||
* Function called after user login
|
||||
* This function is used to send the heartbeat list of a monitor.
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @param {Object} user User object
|
||||
* @param {object} user User object
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function afterLogin(socket, user) {
|
||||
@@ -1720,10 +1625,6 @@ async function afterLogin(socket, user) {
|
||||
await sendHeartbeatList(socket, monitorID);
|
||||
}
|
||||
|
||||
for (let monitorID in monitorList) {
|
||||
await sendImportantHeartbeatList(socket, monitorID);
|
||||
}
|
||||
|
||||
for (let monitorID in monitorList) {
|
||||
await Monitor.sendStats(io, monitorID, user.id);
|
||||
}
|
||||
@@ -1738,19 +1639,14 @@ async function afterLogin(socket, user) {
|
||||
|
||||
/**
|
||||
* Initialize the database
|
||||
* @param {boolean} [testMode=false] Should the connection be
|
||||
* @param {boolean} testMode Should the connection be
|
||||
* started in test mode?
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function initDatabase(testMode = false) {
|
||||
if (! fs.existsSync(Database.path)) {
|
||||
log.info("server", "Copying Database");
|
||||
fs.copyFileSync(Database.templatePath, Database.path);
|
||||
}
|
||||
|
||||
log.info("server", "Connecting to the Database");
|
||||
log.debug("server", "Connecting to the database");
|
||||
await Database.connect(testMode);
|
||||
log.info("server", "Connected");
|
||||
log.info("server", "Connected to the database");
|
||||
|
||||
// Patch the database
|
||||
await Database.patch();
|
||||
@@ -1764,11 +1660,11 @@ async function initDatabase(testMode = false) {
|
||||
jwtSecretBean = await initJWTSecret();
|
||||
log.info("server", "Stored JWT secret into database");
|
||||
} else {
|
||||
log.info("server", "Load JWT secret from database.");
|
||||
log.debug("server", "Load JWT secret from database.");
|
||||
}
|
||||
|
||||
// If there is no record in user table, it is a new Uptime Kuma instance, need to setup
|
||||
if ((await R.count("user")) === 0) {
|
||||
if ((await R.knex("user").count("id as count").first()).count === 0) {
|
||||
log.info("server", "No user, need setup");
|
||||
needSetup = true;
|
||||
}
|
||||
@@ -1835,7 +1731,10 @@ async function pauseMonitor(userID, monitorID) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Resume active monitors */
|
||||
/**
|
||||
* Resume active monitors
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function startMonitors() {
|
||||
let list = await R.find("monitor", " active = 1 ");
|
||||
|
||||
@@ -1870,12 +1769,19 @@ async function shutdownFunction(signal) {
|
||||
await sleep(2000);
|
||||
await Database.close();
|
||||
|
||||
if (EmbeddedMariaDB.hasInstance()) {
|
||||
EmbeddedMariaDB.getInstance().stop();
|
||||
}
|
||||
|
||||
stopBackgroundJobs();
|
||||
await cloudflaredStop();
|
||||
Settings.stopCacheCleaner();
|
||||
}
|
||||
|
||||
/** Final function called before application exits */
|
||||
/**
|
||||
* Final function called before application exits
|
||||
* @returns {void}
|
||||
*/
|
||||
function finalFunction() {
|
||||
log.info("server", "Graceful shutdown successful!");
|
||||
}
|
||||
|
@@ -96,7 +96,7 @@ class Settings {
|
||||
/**
|
||||
* Get settings based on type
|
||||
* @param {string} type The type of setting
|
||||
* @returns {Promise<Bean>}
|
||||
* @returns {Promise<Bean>} Settings
|
||||
*/
|
||||
static async getSettings(type) {
|
||||
let list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
|
||||
@@ -119,7 +119,7 @@ class Settings {
|
||||
/**
|
||||
* Set settings based on type
|
||||
* @param {string} type Type of settings to set
|
||||
* @param {Object} data Values of settings
|
||||
* @param {object} data Values of settings
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async setSettings(type, data) {
|
||||
@@ -150,8 +150,9 @@ class Settings {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string[]} keyList
|
||||
* Delete selected keys from settings cache
|
||||
* @param {string[]} keyList Keys to remove
|
||||
* @returns {void}
|
||||
*/
|
||||
static deleteCache(keyList) {
|
||||
for (let key of keyList) {
|
||||
@@ -159,6 +160,10 @@ class Settings {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the cache cleaner if running
|
||||
* @returns {void}
|
||||
*/
|
||||
static stopCacheCleaner() {
|
||||
if (Settings.cacheCleaner) {
|
||||
clearInterval(Settings.cacheCleaner);
|
||||
|
271
server/setup-database.js
Normal file
271
server/setup-database.js
Normal file
@@ -0,0 +1,271 @@
|
||||
const express = require("express");
|
||||
const { log } = require("../src/util");
|
||||
const expressStaticGzip = require("express-static-gzip");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const Database = require("./database");
|
||||
const { allowDevAllOrigin } = require("./util-server");
|
||||
const mysql = require("mysql2/promise");
|
||||
|
||||
/**
|
||||
* A standalone express app that is used to setup a database
|
||||
* It is used when db-config.json and kuma.db are not found or invalid
|
||||
* Once it is configured, it will shut down and start the main server
|
||||
*/
|
||||
class SetupDatabase {
|
||||
/**
|
||||
* Show Setup Page
|
||||
* @type {boolean}
|
||||
*/
|
||||
needSetup = true;
|
||||
/**
|
||||
* If the server has finished the setup
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
runningSetup = false;
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @type {UptimeKumaServer}
|
||||
* @private
|
||||
*/
|
||||
server;
|
||||
|
||||
/**
|
||||
* @param {object} args The arguments passed from the command line
|
||||
* @param {UptimeKumaServer} server the main server instance
|
||||
*/
|
||||
constructor(args, server) {
|
||||
this.server = server;
|
||||
|
||||
// Priority: env > db-config.json
|
||||
// If env is provided, write it to db-config.json
|
||||
// If db-config.json is found, check if it is valid
|
||||
// If db-config.json is not found or invalid, check if kuma.db is found
|
||||
// If kuma.db is not found, show setup page
|
||||
|
||||
let dbConfig;
|
||||
|
||||
try {
|
||||
dbConfig = Database.readDBConfig();
|
||||
log.debug("setup-database", "db-config.json is found and is valid");
|
||||
this.needSetup = false;
|
||||
|
||||
} catch (e) {
|
||||
log.info("setup-database", "db-config.json is not found or invalid: " + e.message);
|
||||
|
||||
// Check if kuma.db is found (1.X.X users), generate db-config.json
|
||||
if (fs.existsSync(path.join(Database.dataDir, "kuma.db"))) {
|
||||
this.needSetup = false;
|
||||
|
||||
log.info("setup-database", "kuma.db is found, generate db-config.json");
|
||||
Database.writeDBConfig({
|
||||
type: "sqlite",
|
||||
});
|
||||
} else {
|
||||
this.needSetup = true;
|
||||
}
|
||||
dbConfig = {};
|
||||
}
|
||||
|
||||
if (process.env.UPTIME_KUMA_DB_TYPE) {
|
||||
this.needSetup = false;
|
||||
log.info("setup-database", "UPTIME_KUMA_DB_TYPE is provided by env, try to override db-config.json");
|
||||
dbConfig.type = process.env.UPTIME_KUMA_DB_TYPE;
|
||||
dbConfig.hostname = process.env.UPTIME_KUMA_DB_HOSTNAME;
|
||||
dbConfig.port = process.env.UPTIME_KUMA_DB_PORT;
|
||||
dbConfig.database = process.env.UPTIME_KUMA_DB_NAME;
|
||||
dbConfig.username = process.env.UPTIME_KUMA_DB_USERNAME;
|
||||
dbConfig.password = process.env.UPTIME_KUMA_DB_PASSWORD;
|
||||
Database.writeDBConfig(dbConfig);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Show Setup Page
|
||||
* @returns {boolean} true if the setup page should be shown
|
||||
*/
|
||||
isNeedSetup() {
|
||||
return this.needSetup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the embedded MariaDB is enabled
|
||||
* @returns {boolean} true if the embedded MariaDB is enabled
|
||||
*/
|
||||
isEnabledEmbeddedMariaDB() {
|
||||
return process.env.UPTIME_KUMA_ENABLE_EMBEDDED_MARIADB === "1";
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the setup-database server
|
||||
* @param {string} hostname where the server is listening
|
||||
* @param {number} port where the server is listening
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
start(hostname, port) {
|
||||
return new Promise((resolve) => {
|
||||
const app = express();
|
||||
let tempServer;
|
||||
app.use(express.json());
|
||||
|
||||
// Disable Keep Alive, otherwise the server will not shutdown, as the client will keep the connection alive
|
||||
app.use(function (req, res, next) {
|
||||
res.setHeader("Connection", "close");
|
||||
next();
|
||||
});
|
||||
|
||||
app.get("/", async (request, response) => {
|
||||
response.redirect("/setup-database");
|
||||
});
|
||||
|
||||
app.get("/api/entry-page", async (request, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
response.json({
|
||||
type: "setup-database",
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/setup-database-info", (request, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
console.log("Request /setup-database-info");
|
||||
response.json({
|
||||
runningSetup: this.runningSetup,
|
||||
needSetup: this.needSetup,
|
||||
isEnabledEmbeddedMariaDB: this.isEnabledEmbeddedMariaDB(),
|
||||
});
|
||||
});
|
||||
|
||||
app.post("/setup-database", async (request, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
|
||||
if (this.runningSetup) {
|
||||
response.status(400).json("Setup is already running");
|
||||
return;
|
||||
}
|
||||
|
||||
this.runningSetup = true;
|
||||
|
||||
let dbConfig = request.body.dbConfig;
|
||||
|
||||
let supportedDBTypes = [ "mariadb", "sqlite" ];
|
||||
|
||||
if (this.isEnabledEmbeddedMariaDB()) {
|
||||
supportedDBTypes.push("embedded-mariadb");
|
||||
}
|
||||
|
||||
// Validate input
|
||||
if (typeof dbConfig !== "object") {
|
||||
response.status(400).json("Invalid dbConfig");
|
||||
this.runningSetup = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dbConfig.type) {
|
||||
response.status(400).json("Database Type is required");
|
||||
this.runningSetup = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!supportedDBTypes.includes(dbConfig.type)) {
|
||||
response.status(400).json("Unsupported Database Type");
|
||||
this.runningSetup = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// External MariaDB
|
||||
if (dbConfig.type === "mariadb") {
|
||||
if (!dbConfig.hostname) {
|
||||
response.status(400).json("Hostname is required");
|
||||
this.runningSetup = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dbConfig.port) {
|
||||
response.status(400).json("Port is required");
|
||||
this.runningSetup = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dbConfig.dbName) {
|
||||
response.status(400).json("Database name is required");
|
||||
this.runningSetup = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dbConfig.username) {
|
||||
response.status(400).json("Username is required");
|
||||
this.runningSetup = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dbConfig.password) {
|
||||
response.status(400).json("Password is required");
|
||||
this.runningSetup = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Test connection
|
||||
try {
|
||||
const connection = await mysql.createConnection({
|
||||
host: dbConfig.hostname,
|
||||
port: dbConfig.port,
|
||||
user: dbConfig.username,
|
||||
password: dbConfig.password,
|
||||
});
|
||||
await connection.execute("SELECT 1");
|
||||
connection.end();
|
||||
} catch (e) {
|
||||
response.status(400).json("Cannot connect to the database: " + e.message);
|
||||
this.runningSetup = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Write db-config.json
|
||||
Database.writeDBConfig(dbConfig);
|
||||
|
||||
response.json({
|
||||
ok: true,
|
||||
});
|
||||
|
||||
// Shutdown down this express and start the main server
|
||||
log.info("setup-database", "Database is configured, close the setup-database server and start the main server now.");
|
||||
if (tempServer) {
|
||||
tempServer.close(() => {
|
||||
log.info("setup-database", "The setup-database server is closed");
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
app.use("/", expressStaticGzip("dist", {
|
||||
enableBrotli: true,
|
||||
}));
|
||||
|
||||
app.get("*", async (_request, response) => {
|
||||
response.send(this.server.indexHTML);
|
||||
});
|
||||
|
||||
app.options("*", async (_request, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
response.end();
|
||||
});
|
||||
|
||||
tempServer = app.listen(port, hostname, () => {
|
||||
log.info("setup-database", `Starting Setup Database on ${port}`);
|
||||
let domain = (hostname) ? hostname : "localhost";
|
||||
log.info("setup-database", `Open http://${domain}:${port} in your browser`);
|
||||
log.info("setup-database", "Waiting for user action...");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SetupDatabase,
|
||||
};
|
@@ -9,8 +9,9 @@ const { Settings } = require("../settings");
|
||||
const { sendAPIKeyList } = require("../client");
|
||||
|
||||
/**
|
||||
* Handlers for Maintenance
|
||||
* Handlers for API keys
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @returns {void}
|
||||
*/
|
||||
module.exports.apiKeySocketHandler = (socket) => {
|
||||
// Add a new api key
|
||||
@@ -37,7 +38,8 @@ module.exports.apiKeySocketHandler = (socket) => {
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Added Successfully.",
|
||||
msg: "successAdded",
|
||||
msgi18n: true,
|
||||
key: formattedKey,
|
||||
keyID: bean.id,
|
||||
});
|
||||
@@ -81,7 +83,8 @@ module.exports.apiKeySocketHandler = (socket) => {
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted Successfully.",
|
||||
msg: "successDeleted",
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
await sendAPIKeyList(socket);
|
||||
@@ -108,7 +111,8 @@ module.exports.apiKeySocketHandler = (socket) => {
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Disabled Successfully.",
|
||||
msg: "successDisabled",
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
await sendAPIKeyList(socket);
|
||||
@@ -135,7 +139,8 @@ module.exports.apiKeySocketHandler = (socket) => {
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Enabled Successfully",
|
||||
msg: "successEnabled",
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
await sendAPIKeyList(socket);
|
||||
|
@@ -11,6 +11,7 @@ const cloudflared = new CloudflaredTunnel();
|
||||
* Change running state
|
||||
* @param {string} running Is it running?
|
||||
* @param {string} message Message to pass
|
||||
* @returns {void}
|
||||
*/
|
||||
cloudflared.change = (running, message) => {
|
||||
io.to("cloudflared").emit(prefix + "running", running);
|
||||
@@ -19,7 +20,8 @@ cloudflared.change = (running, message) => {
|
||||
|
||||
/**
|
||||
* Emit an error message
|
||||
* @param {string} errorMessage
|
||||
* @param {string} errorMessage Error message to send
|
||||
* @returns {void}
|
||||
*/
|
||||
cloudflared.error = (errorMessage) => {
|
||||
io.to("cloudflared").emit(prefix + "errorMessage", errorMessage);
|
||||
@@ -28,6 +30,7 @@ cloudflared.error = (errorMessage) => {
|
||||
/**
|
||||
* Handler for cloudflared
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @returns {void}
|
||||
*/
|
||||
module.exports.cloudflaredSocketHandler = (socket) => {
|
||||
|
||||
@@ -89,6 +92,7 @@ module.exports.cloudflaredSocketHandler = (socket) => {
|
||||
/**
|
||||
* Automatically start cloudflared
|
||||
* @param {string} token Cloudflared tunnel token
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
module.exports.autoStart = async (token) => {
|
||||
if (!token) {
|
||||
@@ -106,7 +110,10 @@ module.exports.autoStart = async (token) => {
|
||||
}
|
||||
};
|
||||
|
||||
/** Stop cloudflared */
|
||||
/**
|
||||
* Stop cloudflared
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
module.exports.stop = async () => {
|
||||
log.info("cloudflared", "Stop cloudflared");
|
||||
if (cloudflared) {
|
||||
|
@@ -4,6 +4,7 @@ const Database = require("../database");
|
||||
/**
|
||||
* Handlers for database
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @returns {void}
|
||||
*/
|
||||
module.exports = (socket) => {
|
||||
|
||||
|
@@ -6,6 +6,7 @@ const { log } = require("../../src/util");
|
||||
/**
|
||||
* Handlers for docker hosts
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @returns {void}
|
||||
*/
|
||||
module.exports.dockerSocketHandler = (socket) => {
|
||||
socket.on("addDockerHost", async (dockerHost, dockerHostID, callback) => {
|
||||
@@ -17,7 +18,8 @@ module.exports.dockerSocketHandler = (socket) => {
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Saved",
|
||||
msg: "Saved.",
|
||||
msgi18n: true,
|
||||
id: dockerHostBean.id,
|
||||
});
|
||||
|
||||
@@ -38,7 +40,8 @@ module.exports.dockerSocketHandler = (socket) => {
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted",
|
||||
msg: "successDeleted",
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
|
@@ -4,13 +4,15 @@ const { sendInfo } = require("../client");
|
||||
const { checkLogin } = require("../util-server");
|
||||
const GameResolver = require("gamedig/lib/GameResolver");
|
||||
const { testChrome } = require("../monitor-types/real-browser-monitor-type");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
let gameResolver = new GameResolver();
|
||||
let gameList = null;
|
||||
|
||||
/**
|
||||
* Get a game list via GameDig
|
||||
* @returns {Object[]} list of games supported by GameDig
|
||||
* @returns {object[]} list of games supported by GameDig
|
||||
*/
|
||||
function getGameList() {
|
||||
if (gameList == null) {
|
||||
@@ -53,7 +55,11 @@ module.exports.generalSocketHandler = (socket, server) => {
|
||||
testChrome(executable).then((version) => {
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Found Chromium/Chrome. Version: " + version,
|
||||
msg: {
|
||||
key: "foundChromiumVersion",
|
||||
values: [ version ],
|
||||
},
|
||||
msgi18n: true,
|
||||
});
|
||||
}).catch((e) => {
|
||||
callback({
|
||||
@@ -62,4 +68,29 @@ module.exports.generalSocketHandler = (socket, server) => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("getPushExample", (language, callback) => {
|
||||
|
||||
try {
|
||||
let dir = path.join("./extra/push-examples", language);
|
||||
let files = fs.readdirSync(dir);
|
||||
|
||||
for (let file of files) {
|
||||
if (file.startsWith("index.")) {
|
||||
callback({
|
||||
ok: true,
|
||||
code: fs.readFileSync(path.join(dir, file), "utf8"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
|
||||
callback({
|
||||
ok: false,
|
||||
msg: "Not found",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@@ -9,6 +9,7 @@ const server = UptimeKumaServer.getInstance();
|
||||
/**
|
||||
* Handlers for Maintenance
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @returns {void}
|
||||
*/
|
||||
module.exports.maintenanceSocketHandler = (socket) => {
|
||||
// Add a new maintenance
|
||||
@@ -29,7 +30,8 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Added Successfully.",
|
||||
msg: "successAdded",
|
||||
msgi18n: true,
|
||||
maintenanceID,
|
||||
});
|
||||
|
||||
@@ -60,6 +62,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Saved.",
|
||||
msgi18n: true,
|
||||
maintenanceID: bean.id,
|
||||
});
|
||||
|
||||
@@ -95,7 +98,8 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Added Successfully.",
|
||||
msg: "successAdded",
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
@@ -129,7 +133,8 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Added Successfully.",
|
||||
msg: "successAdded",
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
@@ -248,7 +253,8 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted Successfully.",
|
||||
msg: "successDeleted",
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
await server.sendMaintenanceList(socket);
|
||||
@@ -281,7 +287,8 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Paused Successfully.",
|
||||
msg: "successPaused",
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
await server.sendMaintenanceList(socket);
|
||||
@@ -314,7 +321,8 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Resume Successfully",
|
||||
msg: "successResumed",
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
await server.sendMaintenanceList(socket);
|
||||
|
@@ -7,6 +7,7 @@ const server = UptimeKumaServer.getInstance();
|
||||
/**
|
||||
* Handlers for proxy
|
||||
* @param {Socket} socket Socket.io instance
|
||||
* @returns {void}
|
||||
*/
|
||||
module.exports.proxySocketHandler = (socket) => {
|
||||
socket.on("addProxy", async (proxy, proxyID, callback) => {
|
||||
@@ -23,7 +24,8 @@ module.exports.proxySocketHandler = (socket) => {
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Saved",
|
||||
msg: "Saved.",
|
||||
msgi18n: true,
|
||||
id: proxyBean.id,
|
||||
});
|
||||
|
||||
@@ -45,7 +47,8 @@ module.exports.proxySocketHandler = (socket) => {
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted",
|
||||
msg: "successDeleted",
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
|
@@ -11,6 +11,7 @@ const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
/**
|
||||
* Socket handlers for status page
|
||||
* @param {Socket} socket Socket.io instance to add listeners on
|
||||
* @returns {void}
|
||||
*/
|
||||
module.exports.statusPageSocketHandler = (socket) => {
|
||||
|
||||
@@ -283,7 +284,8 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "OK!"
|
||||
msg: "successAdded",
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
@@ -350,6 +352,8 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
* Check slug a-z, 0-9, - only
|
||||
* Regex from: https://stackoverflow.com/questions/22454258/js-regex-string-validation-for-slug
|
||||
* @param {string} slug Slug to test
|
||||
* @returns {void}
|
||||
* @throws Slug is not valid
|
||||
*/
|
||||
function checkSlug(slug) {
|
||||
if (typeof slug !== "string") {
|
||||
|
@@ -1,49 +0,0 @@
|
||||
const { log } = require("../src/util");
|
||||
class UptimeCacheList {
|
||||
/**
|
||||
* list[monitorID][duration]
|
||||
*/
|
||||
static list = {};
|
||||
|
||||
/**
|
||||
* Get the uptime for a specific period
|
||||
* @param {number} monitorID
|
||||
* @param {number} 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add uptime for specified monitor
|
||||
* @param {number} monitorID
|
||||
* @param {number} duration
|
||||
* @param {number} uptime Uptime to add
|
||||
*/
|
||||
static addUptime(monitorID, duration, uptime) {
|
||||
log.debug("UptimeCacheList", "addUptime: " + monitorID + " " + duration);
|
||||
if (!UptimeCacheList.list[monitorID]) {
|
||||
UptimeCacheList.list[monitorID] = {};
|
||||
}
|
||||
UptimeCacheList.list[monitorID][duration] = uptime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache for specified monitor
|
||||
* @param {number} monitorID
|
||||
*/
|
||||
static clearCache(monitorID) {
|
||||
log.debug("UptimeCacheList", "clearCache: " + monitorID);
|
||||
delete UptimeCacheList.list[monitorID];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
UptimeCacheList,
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user