mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-08-10 03:05:45 +08:00
Merge branch 'master' into 1.23.X-to-2
# Conflicts: # package-lock.json # package.json
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 {Promise<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 {Promise<void>}
|
||||
*/
|
||||
exports.apiAuth = async function (req, res, next) {
|
||||
if (!await Settings.get("disableAuth")) {
|
||||
|
@@ -1,86 +0,0 @@
|
||||
const https = require("https");
|
||||
const http = require("http");
|
||||
const CacheableLookup = require("cacheable-lookup");
|
||||
const { Settings } = require("./settings");
|
||||
const { log } = require("../src/util");
|
||||
|
||||
class CacheableDnsHttpAgent {
|
||||
|
||||
static cacheable = new CacheableLookup();
|
||||
|
||||
static httpAgentList = {};
|
||||
static httpsAgentList = {};
|
||||
|
||||
static enable = false;
|
||||
|
||||
/**
|
||||
* Register/Disable cacheable to global agents
|
||||
*/
|
||||
static async update() {
|
||||
log.debug("CacheableDnsHttpAgent", "update");
|
||||
let isEnable = await Settings.get("dnsCache");
|
||||
|
||||
if (isEnable !== this.enable) {
|
||||
log.debug("CacheableDnsHttpAgent", "value changed");
|
||||
|
||||
if (isEnable) {
|
||||
log.debug("CacheableDnsHttpAgent", "enable");
|
||||
this.cacheable.install(http.globalAgent);
|
||||
this.cacheable.install(https.globalAgent);
|
||||
} else {
|
||||
log.debug("CacheableDnsHttpAgent", "disable");
|
||||
this.cacheable.uninstall(http.globalAgent);
|
||||
this.cacheable.uninstall(https.globalAgent);
|
||||
}
|
||||
}
|
||||
|
||||
this.enable = isEnable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach cacheable to HTTP agent
|
||||
* @param {http.Agent} agent Agent to install
|
||||
*/
|
||||
static install(agent) {
|
||||
this.cacheable.install(agent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @var {https.AgentOptions} agentOptions
|
||||
* @return {https.Agent}
|
||||
*/
|
||||
static getHttpsAgent(agentOptions) {
|
||||
if (!this.enable) {
|
||||
return new https.Agent(agentOptions);
|
||||
}
|
||||
|
||||
let key = JSON.stringify(agentOptions);
|
||||
if (!(key in this.httpsAgentList)) {
|
||||
this.httpsAgentList[key] = new https.Agent(agentOptions);
|
||||
this.cacheable.install(this.httpsAgentList[key]);
|
||||
}
|
||||
return this.httpsAgentList[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* @var {http.AgentOptions} agentOptions
|
||||
* @return {https.Agents}
|
||||
*/
|
||||
static getHttpAgent(agentOptions) {
|
||||
if (!this.enable) {
|
||||
return new http.Agent(agentOptions);
|
||||
}
|
||||
|
||||
let key = JSON.stringify(agentOptions);
|
||||
if (!(key in this.httpAgentList)) {
|
||||
this.httpAgentList[key] = new http.Agent(agentOptions);
|
||||
this.cacheable.install(this.httpAgentList[key]);
|
||||
}
|
||||
return this.httpAgentList[key];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CacheableDnsHttpAgent,
|
||||
};
|
@@ -8,11 +8,12 @@ const server = UptimeKumaServer.getInstance();
|
||||
const io = server.io;
|
||||
const { setting } = require("./util-server");
|
||||
const checkVersion = require("./check-version");
|
||||
const Database = require("./database");
|
||||
|
||||
/**
|
||||
* 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 +41,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 +62,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 +97,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,24 +138,27 @@ 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) {
|
||||
let version;
|
||||
let latestVersion;
|
||||
let isContainer;
|
||||
let dbType;
|
||||
|
||||
if (!hideVersion) {
|
||||
version = checkVersion.version;
|
||||
latestVersion = checkVersion.latestVersion;
|
||||
isContainer = (process.env.UPTIME_KUMA_IS_CONTAINER === "1");
|
||||
dbType = Database.dbConfig.type;
|
||||
}
|
||||
|
||||
socket.emit("info", {
|
||||
version,
|
||||
latestVersion,
|
||||
isContainer,
|
||||
dbType,
|
||||
primaryBaseURL: await setting("primaryBaseURL"),
|
||||
serverTimezone: await server.getTimezone(),
|
||||
serverTimezoneOffset: server.getTimezoneOffset(),
|
||||
@@ -168,7 +168,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();
|
||||
@@ -189,6 +189,56 @@ async function sendDockerHostList(socket) {
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send list of docker hosts to client
|
||||
* @param {Socket} socket Socket.io socket instance
|
||||
* @returns {Promise<Bean[]>} List of docker hosts
|
||||
*/
|
||||
async function sendRemoteBrowserList(socket) {
|
||||
const timeLogger = new TimeLogger();
|
||||
|
||||
let result = [];
|
||||
let list = await R.find("remote_browser", " user_id = ? ", [
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
for (let bean of list) {
|
||||
result.push(bean.toJSON());
|
||||
}
|
||||
|
||||
io.to(socket.userID).emit("remoteBrowserList", result);
|
||||
|
||||
timeLogger.print("Send Remote Browser List");
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send list of monitor types to client
|
||||
* @param {Socket} socket Socket.io socket instance
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function sendMonitorTypeList(socket) {
|
||||
const result = Object.entries(UptimeKumaServer.monitorTypeList).map(([ key, type ]) => {
|
||||
return [ key, {
|
||||
supportsConditions: type.supportsConditions,
|
||||
conditionVariables: type.conditionVariables.map(v => {
|
||||
return {
|
||||
id: v.id,
|
||||
operators: v.operators.map(o => {
|
||||
return {
|
||||
id: o.id,
|
||||
caption: o.caption,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
}];
|
||||
});
|
||||
|
||||
io.to(socket.userID).emit("monitorTypeList", Object.fromEntries(result));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendNotificationList,
|
||||
sendImportantHeartbeatList,
|
||||
@@ -196,5 +246,7 @@ module.exports = {
|
||||
sendProxyList,
|
||||
sendAPIKeyList,
|
||||
sendInfo,
|
||||
sendDockerHostList
|
||||
sendDockerHostList,
|
||||
sendRemoteBrowserList,
|
||||
sendMonitorTypeList,
|
||||
};
|
||||
|
@@ -19,6 +19,10 @@ const sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_S
|
||||
|
||||
const isSSL = sslKey && sslCert;
|
||||
|
||||
/**
|
||||
* Get the local WebSocket URL
|
||||
* @returns {string} The local WebSocket URL
|
||||
*/
|
||||
function getLocalWebSocketURL() {
|
||||
const protocol = isSSL ? "wss" : "ws";
|
||||
const host = hostname || "localhost";
|
||||
|
@@ -4,28 +4,53 @@ 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");
|
||||
const { Settings } = require("./settings");
|
||||
const { UptimeCalculator } = require("./uptime-calculator");
|
||||
const dayjs = require("dayjs");
|
||||
const { SimpleMigrationServer } = require("./utils/simple-migration-server");
|
||||
const KumaColumnCompiler = require("./utils/knex/lib/dialects/mysql2/schema/mysql2-columncompiler");
|
||||
|
||||
/**
|
||||
* 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 +59,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,
|
||||
@@ -84,7 +111,7 @@ class Database {
|
||||
"patch-notification-config.sql": true,
|
||||
"patch-fix-kafka-producer-booleans.sql": true,
|
||||
"patch-timeout.sql": true,
|
||||
"patch-monitor-tls-info-add-fk.sql": true,
|
||||
"patch-monitor-tls-info-add-fk.sql": true, // The last file so far converted to a knex migration file
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -95,15 +122,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 });
|
||||
}
|
||||
@@ -125,39 +157,172 @@ 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) {
|
||||
// Patch "mysql2" knex client
|
||||
// Workaround: Tried extending the ColumnCompiler class, but it didn't work for unknown reasons, so I override the function via prototype
|
||||
const { getDialectByNameOrAlias } = require("knex/lib/dialects");
|
||||
const mysql2 = getDialectByNameOrAlias("mysql2");
|
||||
mysql2.prototype.columnCompiler = function () {
|
||||
return new KumaColumnCompiler(this, ...arguments);
|
||||
};
|
||||
|
||||
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 = {};
|
||||
|
||||
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,
|
||||
let mariadbPoolConfig = {
|
||||
min: 0,
|
||||
max: 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
};
|
||||
|
||||
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: "Z",
|
||||
typeCast: function (field, next) {
|
||||
if (field.type === "DATETIME") {
|
||||
// Do not perform timezone conversion
|
||||
return field.string();
|
||||
}
|
||||
return next();
|
||||
},
|
||||
},
|
||||
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: embeddedMariaDB.username,
|
||||
database: "kuma",
|
||||
timezone: "Z",
|
||||
typeCast: function (field, next) {
|
||||
if (field.type === "DATETIME") {
|
||||
// Do not perform timezone conversion
|
||||
return field.string();
|
||||
}
|
||||
return next();
|
||||
},
|
||||
},
|
||||
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);
|
||||
|
||||
@@ -172,6 +337,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
|
||||
@@ -189,35 +367,110 @@ 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 */
|
||||
static async patch() {
|
||||
/**
|
||||
* 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
|
||||
* @param {number} port Start the migration server for aggregate tables on this port if provided
|
||||
* @param {string} hostname Start the migration server for aggregate tables on this hostname if provided
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async patch(port = undefined, hostname = undefined) {
|
||||
// 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 {
|
||||
// Disable foreign key check for SQLite
|
||||
// Known issue of knex: https://github.com/drizzle-team/drizzle-orm/issues/1813
|
||||
if (Database.dbConfig.type === "sqlite") {
|
||||
await R.exec("PRAGMA foreign_keys = OFF");
|
||||
}
|
||||
|
||||
await R.knex.migrate.latest({
|
||||
directory: Database.knexMigrationsPath,
|
||||
});
|
||||
|
||||
// Enable foreign key check for SQLite
|
||||
if (Database.dbConfig.type === "sqlite") {
|
||||
await R.exec("PRAGMA foreign_keys = ON");
|
||||
}
|
||||
|
||||
await this.migrateAggregateTable(port, hostname);
|
||||
|
||||
} 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}`);
|
||||
@@ -234,18 +487,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) {
|
||||
@@ -278,6 +532,7 @@ class Database {
|
||||
}
|
||||
|
||||
/**
|
||||
* SQlite only
|
||||
* Migrate status page value in setting to "status_page" table
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@@ -349,8 +604,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) {
|
||||
@@ -374,7 +629,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");
|
||||
|
||||
@@ -385,7 +640,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) {
|
||||
@@ -417,14 +672,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>}
|
||||
@@ -438,7 +685,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;
|
||||
@@ -451,17 +700,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -469,8 +724,189 @@ 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)";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate the old data in the heartbeat table to the new format (stat_daily, stat_hourly, stat_minutely)
|
||||
* It should be run once while upgrading V1 to V2
|
||||
*
|
||||
* Normally, it should be in transaction, but UptimeCalculator wasn't designed to be in transaction before that.
|
||||
* I don't want to heavily modify the UptimeCalculator, so it is not in transaction.
|
||||
* Run `npm run reset-migrate-aggregate-table-state` to reset, in case the migration is interrupted.
|
||||
* @param {number} port Start the migration server on this port if provided
|
||||
* @param {string} hostname Start the migration server on this hostname if provided
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async migrateAggregateTable(port, hostname = undefined) {
|
||||
log.debug("db", "Enter Migrate Aggregate Table function");
|
||||
|
||||
// Add a setting for 2.0.0-dev users to skip this migration
|
||||
if (process.env.SET_MIGRATE_AGGREGATE_TABLE_TO_TRUE === "1") {
|
||||
log.warn("db", "SET_MIGRATE_AGGREGATE_TABLE_TO_TRUE is set to 1, skipping aggregate table migration forever (for 2.0.0-dev users)");
|
||||
await Settings.set("migrateAggregateTableState", "migrated");
|
||||
}
|
||||
|
||||
let migrateState = await Settings.get("migrateAggregateTableState");
|
||||
|
||||
// Skip if already migrated
|
||||
// If it is migrating, it possibly means the migration was interrupted, or the migration is in progress
|
||||
if (migrateState === "migrated") {
|
||||
log.debug("db", "Migrated aggregate table already, skip");
|
||||
return;
|
||||
} else if (migrateState === "migrating") {
|
||||
log.warn("db", "Aggregate table migration is already in progress, or it was interrupted");
|
||||
throw new Error("Aggregate table migration is already in progress");
|
||||
}
|
||||
|
||||
/**
|
||||
* Start migration server for displaying the migration status
|
||||
* @type {SimpleMigrationServer}
|
||||
*/
|
||||
let migrationServer;
|
||||
let msg;
|
||||
|
||||
if (port) {
|
||||
migrationServer = new SimpleMigrationServer();
|
||||
await migrationServer.start(port, hostname);
|
||||
}
|
||||
|
||||
log.info("db", "Migrating Aggregate Table");
|
||||
|
||||
log.info("db", "Getting list of unique monitors");
|
||||
|
||||
// Get a list of unique monitors from the heartbeat table, using raw sql
|
||||
let monitors = await R.getAll(`
|
||||
SELECT DISTINCT monitor_id
|
||||
FROM heartbeat
|
||||
ORDER BY monitor_id ASC
|
||||
`);
|
||||
|
||||
// Stop if stat_* tables are not empty
|
||||
for (let table of [ "stat_minutely", "stat_hourly", "stat_daily" ]) {
|
||||
let countResult = await R.getRow(`SELECT COUNT(*) AS count FROM ${table}`);
|
||||
let count = countResult.count;
|
||||
if (count > 0) {
|
||||
log.warn("db", `Aggregate table ${table} is not empty, migration will not be started (Maybe you were using 2.0.0-dev?)`);
|
||||
await migrationServer?.stop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await Settings.set("migrateAggregateTableState", "migrating");
|
||||
|
||||
let progressPercent = 0;
|
||||
let part = 100 / monitors.length;
|
||||
let i = 1;
|
||||
for (let monitor of monitors) {
|
||||
// Get a list of unique dates from the heartbeat table, using raw sql
|
||||
let dates = await R.getAll(`
|
||||
SELECT DISTINCT DATE(time) AS date
|
||||
FROM heartbeat
|
||||
WHERE monitor_id = ?
|
||||
ORDER BY date ASC
|
||||
`, [
|
||||
monitor.monitor_id
|
||||
]);
|
||||
|
||||
for (let date of dates) {
|
||||
// New Uptime Calculator
|
||||
let calculator = new UptimeCalculator();
|
||||
calculator.monitorID = monitor.monitor_id;
|
||||
calculator.setMigrationMode(true);
|
||||
|
||||
// Get all the heartbeats for this monitor and date
|
||||
let heartbeats = await R.getAll(`
|
||||
SELECT status, ping, time
|
||||
FROM heartbeat
|
||||
WHERE monitor_id = ?
|
||||
AND DATE(time) = ?
|
||||
ORDER BY time ASC
|
||||
`, [ monitor.monitor_id, date.date ]);
|
||||
|
||||
if (heartbeats.length > 0) {
|
||||
msg = `[DON'T STOP] Migrating monitor data ${monitor.monitor_id} - ${date.date} [${progressPercent.toFixed(2)}%][${i}/${monitors.length}]`;
|
||||
log.info("db", msg);
|
||||
migrationServer?.update(msg);
|
||||
}
|
||||
|
||||
for (let heartbeat of heartbeats) {
|
||||
await calculator.update(heartbeat.status, parseFloat(heartbeat.ping), dayjs(heartbeat.time));
|
||||
}
|
||||
|
||||
progressPercent += (Math.round(part / dates.length * 100) / 100);
|
||||
|
||||
// Lazy to fix the floating point issue, it is acceptable since it is just a progress bar
|
||||
if (progressPercent > 100) {
|
||||
progressPercent = 100;
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
msg = "Clearing non-important heartbeats";
|
||||
log.info("db", msg);
|
||||
migrationServer?.update(msg);
|
||||
|
||||
await Database.clearHeartbeatData(true);
|
||||
await Settings.set("migrateAggregateTableState", "migrated");
|
||||
await migrationServer?.stop();
|
||||
|
||||
if (monitors.length > 0) {
|
||||
log.info("db", "Aggregate Table Migration Completed");
|
||||
} else {
|
||||
log.info("db", "No data to migrate");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all non-important heartbeats from heartbeat table, keep last 24-hour or {KEEP_LAST_ROWS} rows for each monitor
|
||||
* @param {boolean} detailedLog Log detailed information
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async clearHeartbeatData(detailedLog = false) {
|
||||
let monitors = await R.getAll("SELECT id FROM monitor");
|
||||
const sqlHourOffset = Database.sqlHourOffset();
|
||||
|
||||
for (let monitor of monitors) {
|
||||
if (detailedLog) {
|
||||
log.info("db", "Deleting non-important heartbeats for monitor " + monitor.id);
|
||||
}
|
||||
await R.exec(`
|
||||
DELETE FROM heartbeat
|
||||
WHERE monitor_id = ?
|
||||
AND important = 0
|
||||
AND time < ${sqlHourOffset}
|
||||
AND id NOT IN (
|
||||
SELECT id
|
||||
FROM heartbeat
|
||||
WHERE monitor_id = ?
|
||||
ORDER BY time DESC
|
||||
LIMIT ?
|
||||
)
|
||||
`, [
|
||||
monitor.id,
|
||||
-24,
|
||||
monitor.id,
|
||||
100,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Database;
|
||||
|
@@ -1,10 +1,10 @@
|
||||
const axios = require("axios");
|
||||
const { R } = require("redbean-node");
|
||||
const version = require("../package.json").version;
|
||||
const https = require("https");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const Database = require("./database");
|
||||
const { axiosAbortSignal } = require("./util-server");
|
||||
|
||||
class DockerHost {
|
||||
|
||||
@@ -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,16 +64,17 @@ class DockerHost {
|
||||
|
||||
/**
|
||||
* Fetches the amount of containers on the Docker host
|
||||
* @param {Object} dockerHost Docker host to check for
|
||||
* @returns {number} Total amount of containers on the host
|
||||
* @param {object} dockerHost Docker host to check for
|
||||
* @returns {Promise<number>} Total amount of containers on the host
|
||||
*/
|
||||
static async testDockerHost(dockerHost) {
|
||||
const options = {
|
||||
url: "/containers/json?all=true",
|
||||
timeout: 5000,
|
||||
headers: {
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "Uptime-Kuma/" + version
|
||||
},
|
||||
signal: axiosAbortSignal(6000),
|
||||
};
|
||||
|
||||
if (dockerHost.dockerType === "socket") {
|
||||
@@ -83,31 +84,40 @@ class DockerHost {
|
||||
options.httpsAgent = new https.Agent(DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL));
|
||||
}
|
||||
|
||||
let res = await axios.request(options);
|
||||
try {
|
||||
let res = await axios.request(options);
|
||||
|
||||
if (Array.isArray(res.data)) {
|
||||
if (Array.isArray(res.data)) {
|
||||
|
||||
if (res.data.length > 1) {
|
||||
if (res.data.length > 1) {
|
||||
|
||||
if ("ImageID" in res.data[0]) {
|
||||
return res.data.length;
|
||||
} else {
|
||||
throw new Error("Invalid Docker response, is it Docker really a daemon?");
|
||||
}
|
||||
|
||||
if ("ImageID" in res.data[0]) {
|
||||
return res.data.length;
|
||||
} else {
|
||||
throw new Error("Invalid Docker response, is it Docker really a daemon?");
|
||||
return res.data.length;
|
||||
}
|
||||
|
||||
} else {
|
||||
return res.data.length;
|
||||
throw new Error("Invalid Docker response, is it Docker really a daemon?");
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.code === "ECONNABORTED" || e.name === "CanceledError") {
|
||||
throw new Error("Connection to Docker daemon timed out.");
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error("Invalid Docker response, is it Docker really a daemon?");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 +139,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,
|
||||
|
219
server/embedded-mariadb.js
Normal file
219
server/embedded-mariadb.js
Normal file
@@ -0,0 +1,219 @@
|
||||
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";
|
||||
|
||||
socketPath = this.runDir + "/mariadb.sock";
|
||||
|
||||
/**
|
||||
* The username to connect to the MariaDB
|
||||
* @type {string}
|
||||
*/
|
||||
username = null;
|
||||
|
||||
/**
|
||||
* @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
|
||||
* @throws {Error} If the current user is not "node" or "root"
|
||||
* @returns {Promise<void>|void} A promise that resolves when the MariaDB is started or void if it is already started
|
||||
*/
|
||||
start() {
|
||||
// Check if the current user is "node" or "root"
|
||||
this.username = require("os").userInfo().username;
|
||||
if (this.username !== "node" && this.username !== "root") {
|
||||
throw new Error("Embedded Mariadb supports only 'node' or 'root' user, but the current user is: " + this.username);
|
||||
}
|
||||
|
||||
this.initDB();
|
||||
|
||||
this.startChildProcess();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let interval = setInterval(() => {
|
||||
if (this.started) {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
} else {
|
||||
log.info("mariadb", "Waiting for Embedded MariaDB to start...");
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the child process
|
||||
* @returns {void}
|
||||
*/
|
||||
startChildProcess() {
|
||||
if (this.childProcess) {
|
||||
log.info("mariadb", "Already started");
|
||||
return;
|
||||
}
|
||||
|
||||
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`,
|
||||
// Don't add the following option, the mariadb will not report message to the console, which affects initDBAfterStarted()
|
||||
// "--log-error=" + `${this.mariadbDataDir}/mariadb-error.log`,
|
||||
]);
|
||||
|
||||
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.error("mariadb", "Try to restart Embedded MariaDB as it is not stopped by user");
|
||||
this.startChildProcess();
|
||||
}
|
||||
});
|
||||
|
||||
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.info("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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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("mariadb-install-db", [
|
||||
"--user=node",
|
||||
"--auth-root-socket-user=node",
|
||||
"--datadir=" + this.mariadbDataDir,
|
||||
"--auth-root-authentication-method=socket",
|
||||
]);
|
||||
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
// Check the owner of the mariadb directory, and change it if necessary
|
||||
let stat = fs.statSync(this.mariadbDataDir);
|
||||
if (stat.uid !== 1000 || stat.gid !== 1000) {
|
||||
fs.chownSync(this.mariadbDataDir, 1000, 1000);
|
||||
}
|
||||
|
||||
// Check the permission of the mariadb directory, and change it if it is not 755
|
||||
if (stat.mode !== 0o755) {
|
||||
fs.chmodSync(this.mariadbDataDir, 0o755);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(this.runDir)) {
|
||||
log.info("mariadb", `Embedded MariaDB: ${this.runDir} is not found, create one now.`);
|
||||
fs.mkdirSync(this.runDir, {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
|
||||
stat = fs.statSync(this.runDir);
|
||||
if (stat.uid !== 1000 || stat.gid !== 1000) {
|
||||
fs.chownSync(this.runDir, 1000, 1000);
|
||||
}
|
||||
if (stat.mode !== 0o755) {
|
||||
fs.chmodSync(this.runDir, 0o755);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the "kuma" database in mariadb if it does not exist
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async initDBAfterStarted() {
|
||||
const connection = mysql.createConnection({
|
||||
socketPath: this.socketPath,
|
||||
user: this.username,
|
||||
});
|
||||
|
||||
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,
|
||||
};
|
@@ -4,8 +4,8 @@ const { escape } = require("html-escaper");
|
||||
/**
|
||||
* 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 escapedTagIdJS = 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,20 +1,22 @@
|
||||
const { R } = require("redbean-node");
|
||||
const { log } = require("../../src/util");
|
||||
const { setSetting, setting } = require("../util-server");
|
||||
const Database = require("../database");
|
||||
const { Settings } = require("../settings");
|
||||
const dayjs = require("dayjs");
|
||||
|
||||
const DEFAULT_KEEP_PERIOD = 180;
|
||||
const DEFAULT_KEEP_PERIOD = 365;
|
||||
|
||||
/**
|
||||
* Clears old data from the heartbeat table of the database.
|
||||
* @return {Promise<void>} A promise that resolves when the data has been cleared.
|
||||
* Clears old data from the heartbeat table and the stat_daily of the database.
|
||||
* @returns {Promise<void>} A promise that resolves when the data has been cleared.
|
||||
*/
|
||||
|
||||
const clearOldData = async () => {
|
||||
let period = await setting("keepDataPeriodDays");
|
||||
await Database.clearHeartbeatData();
|
||||
let period = await Settings.get("keepDataPeriodDays");
|
||||
|
||||
// Set Default Period
|
||||
if (period == null) {
|
||||
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
|
||||
await Settings.set("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
|
||||
period = DEFAULT_KEEP_PERIOD;
|
||||
}
|
||||
|
||||
@@ -24,27 +26,38 @@ const clearOldData = async () => {
|
||||
parsedPeriod = parseInt(period);
|
||||
} catch (_) {
|
||||
log.warn("clearOldData", "Failed to parse setting, resetting to default..");
|
||||
await setSetting("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
|
||||
await Settings.set("keepDataPeriodDays", DEFAULT_KEEP_PERIOD, "general");
|
||||
parsedPeriod = DEFAULT_KEEP_PERIOD;
|
||||
}
|
||||
|
||||
if (parsedPeriod < 1) {
|
||||
log.info("clearOldData", `Data deletion has been disabled as period is less than 1. Period is ${parsedPeriod} days.`);
|
||||
} else {
|
||||
|
||||
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 ]
|
||||
);
|
||||
// Heartbeat
|
||||
await R.exec("DELETE FROM heartbeat WHERE time < " + sqlHourOffset, [
|
||||
parsedPeriod * -24,
|
||||
]);
|
||||
|
||||
await R.exec("PRAGMA optimize;");
|
||||
let timestamp = dayjs().subtract(parsedPeriod, "day").utc().startOf("day").unix();
|
||||
|
||||
// stat_daily
|
||||
await R.exec("DELETE FROM stat_daily WHERE timestamp < ? ", [
|
||||
timestamp,
|
||||
]);
|
||||
|
||||
if (Database.dbConfig.type === "sqlite") {
|
||||
await R.exec("PRAGMA optimize;");
|
||||
}
|
||||
} catch (e) {
|
||||
log.error("clearOldData", `Failed to clear old data: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("clearOldData", "Data cleared.");
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
@@ -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 {Promise<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 {Promise<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,17 +25,18 @@ class Heartbeat extends BeanModel {
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @returns {Object}
|
||||
* @returns {object} Object ready to parse
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
monitorID: this.monitor_id,
|
||||
status: this.status,
|
||||
time: this.time,
|
||||
msg: this.msg,
|
||||
ping: this.ping,
|
||||
important: this.important,
|
||||
duration: this.duration,
|
||||
monitorID: this._monitorId,
|
||||
status: this._status,
|
||||
time: this._time,
|
||||
msg: this._msg,
|
||||
ping: this._ping,
|
||||
important: this._important,
|
||||
duration: this._duration,
|
||||
retries: this._retries,
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -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 {Promise<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 {Promise<object>} Object ready to parse
|
||||
*/
|
||||
async toJSON(timezone = null) {
|
||||
return this.toPublicJSON(timezone);
|
||||
@@ -142,8 +142,8 @@ 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
|
||||
* @returns {Bean} Filled bean
|
||||
* @param {object} obj Data to fill bean with
|
||||
* @returns {Promise<Bean>} Filled bean
|
||||
*/
|
||||
static async jsonToBean(bean, obj) {
|
||||
if (obj.id) {
|
||||
@@ -188,16 +188,18 @@ class Maintenance extends BeanModel {
|
||||
|
||||
/**
|
||||
* Throw error if cron is invalid
|
||||
* @param cron
|
||||
* @returns {Promise<void>}
|
||||
* @param {string|Date} cron Pattern or date
|
||||
* @returns {void}
|
||||
*/
|
||||
static async validateCron(cron) {
|
||||
static validateCron(cron) {
|
||||
let job = new Cron(cron, () => {});
|
||||
job.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the cron
|
||||
* @param {boolean} throwError Should an error be thrown on failure
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async run(throwError = false) {
|
||||
if (this.beanMeta.job) {
|
||||
@@ -237,19 +239,7 @@ class Maintenance extends BeanModel {
|
||||
this.beanMeta.status = "under-maintenance";
|
||||
clearTimeout(this.beanMeta.durationTimeout);
|
||||
|
||||
// Check if duration is still in the window. If not, use the duration from the current time to the end of the window
|
||||
let duration;
|
||||
|
||||
if (customDuration > 0) {
|
||||
duration = customDuration;
|
||||
} else if (this.end_date) {
|
||||
let d = dayjs(this.end_date).diff(dayjs(), "second");
|
||||
if (d < this.duration) {
|
||||
duration = d * 1000;
|
||||
}
|
||||
} else {
|
||||
duration = this.duration * 1000;
|
||||
}
|
||||
let duration = this.inferDuration(customDuration);
|
||||
|
||||
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
||||
|
||||
@@ -261,9 +251,21 @@ class Maintenance extends BeanModel {
|
||||
};
|
||||
|
||||
// Create Cron
|
||||
this.beanMeta.job = new Cron(this.cron, {
|
||||
timezone: await this.getTimezone(),
|
||||
}, startEvent);
|
||||
if (this.strategy === "recurring-interval") {
|
||||
// For recurring-interval, Croner needs to have interval and startAt
|
||||
const startDate = dayjs(this.startDate);
|
||||
const [ hour, minute ] = this.startTime.split(":");
|
||||
const startDateTime = startDate.hour(hour).minute(minute);
|
||||
this.beanMeta.job = new Cron(this.cron, {
|
||||
timezone: await this.getTimezone(),
|
||||
interval: this.interval_day * 24 * 60 * 60,
|
||||
startAt: startDateTime.toISOString(),
|
||||
}, startEvent);
|
||||
} else {
|
||||
this.beanMeta.job = new Cron(this.cron, {
|
||||
timezone: await this.getTimezone(),
|
||||
}, startEvent);
|
||||
}
|
||||
|
||||
// Continue if the maintenance is still in the window
|
||||
let runningTimeslot = this.getRunningTimeslot();
|
||||
@@ -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,28 @@ class Maintenance extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the maintenance duration
|
||||
* @param {number} customDuration - The custom duration in milliseconds.
|
||||
* @returns {number} The inferred duration in milliseconds.
|
||||
*/
|
||||
inferDuration(customDuration) {
|
||||
// Check if duration is still in the window. If not, use the duration from the current time to the end of the window
|
||||
if (customDuration > 0) {
|
||||
return customDuration;
|
||||
} else if (this.end_date) {
|
||||
let d = dayjs(this.end_date).diff(dayjs(), "second");
|
||||
if (d < this.duration) {
|
||||
return d * 1000;
|
||||
}
|
||||
}
|
||||
return this.duration * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the maintenance
|
||||
* @returns {void}
|
||||
*/
|
||||
stop() {
|
||||
if (this.beanMeta.job) {
|
||||
this.beanMeta.job.stop();
|
||||
@@ -312,10 +340,18 @@ class Maintenance extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this maintenance currently active
|
||||
* @returns {Promise<boolean>} The maintenance is active?
|
||||
*/
|
||||
async isUnderMaintenance() {
|
||||
return (await this.getStatus()) === "under-maintenance";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timezone of the maintenance
|
||||
* @returns {Promise<string>} timezone
|
||||
*/
|
||||
async getTimezone() {
|
||||
if (!this.timezone || this.timezone === "SAME_AS_SERVER") {
|
||||
return await UptimeKumaServer.getInstance().getTimezone();
|
||||
@@ -323,10 +359,18 @@ class Maintenance extends BeanModel {
|
||||
return this.timezone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get offset for timezone
|
||||
* @returns {Promise<string>} offset
|
||||
*/
|
||||
async getTimezoneOffset() {
|
||||
return dayjs.tz(dayjs(), await this.getTimezone()).format("Z");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current status of the maintenance
|
||||
* @returns {Promise<string>} Current status
|
||||
*/
|
||||
async getStatus() {
|
||||
if (!this.active) {
|
||||
return "inactive";
|
||||
@@ -369,10 +413,8 @@ class Maintenance extends BeanModel {
|
||||
} else if (!this.strategy.startsWith("recurring-")) {
|
||||
this.cron = "";
|
||||
} else if (this.strategy === "recurring-interval") {
|
||||
let array = this.start_time.split(":");
|
||||
let hour = parseInt(array[0]);
|
||||
let minute = parseInt(array[1]);
|
||||
this.cron = minute + " " + hour + " */" + this.interval_day + " * *";
|
||||
// For intervals, the pattern is calculated in the run function as the interval-option is set
|
||||
this.cron = "* * * * *";
|
||||
this.duration = this.calcDuration();
|
||||
log.debug("maintenance", "Cron: " + this.cron);
|
||||
log.debug("maintenance", "Duration: " + this.duration);
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
|
17
server/model/remote_browser.js
Normal file
17
server/model/remote_browser.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
|
||||
class RemoteBrowser extends BeanModel {
|
||||
/**
|
||||
* Returns an object that ready to parse to JSON
|
||||
* @returns {object} Object ready to parse
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
url: this.url,
|
||||
name: this.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RemoteBrowser;
|
@@ -4,6 +4,11 @@ const cheerio = require("cheerio");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const jsesc = require("jsesc");
|
||||
const googleAnalytics = require("../google-analytics");
|
||||
const { marked } = require("marked");
|
||||
const { Feed } = require("feed");
|
||||
const config = require("../config");
|
||||
|
||||
const { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, STATUS_PAGE_PARTIAL_DOWN, UP, MAINTENANCE, DOWN } = require("../../src/util");
|
||||
|
||||
class StatusPage extends BeanModel {
|
||||
|
||||
@@ -14,12 +19,37 @@ class StatusPage extends BeanModel {
|
||||
static domainMappingList = { };
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Response} response
|
||||
* @param {string} indexHTML
|
||||
* @param {string} slug
|
||||
* Handle responses to RSS pages
|
||||
* @param {Response} response Response object
|
||||
* @param {string} slug Status page slug
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async handleStatusPageRSSResponse(response, slug) {
|
||||
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||
slug
|
||||
]);
|
||||
|
||||
if (statusPage) {
|
||||
response.send(await StatusPage.renderRSS(statusPage, slug));
|
||||
} else {
|
||||
response.status(404).send(UptimeKumaServer.getInstance().indexHTML);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle responses to status page
|
||||
* @param {Response} response Response object
|
||||
* @param {string} indexHTML HTML to render
|
||||
* @param {string} slug Status page slug
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async handleStatusPageResponse(response, indexHTML, slug) {
|
||||
// Handle url with trailing slash (http://localhost:3001/status/)
|
||||
// The slug comes from the route "/status/:slug". If the slug is empty, express converts it to "index.html"
|
||||
if (slug === "index.html") {
|
||||
slug = "default";
|
||||
}
|
||||
|
||||
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||
slug
|
||||
]);
|
||||
@@ -31,14 +61,51 @@ class StatusPage extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SSR for RSS feed
|
||||
* @param {statusPage} statusPage object
|
||||
* @param {slug} slug from router
|
||||
* @returns {Promise<string>} the rendered html
|
||||
*/
|
||||
static async renderRSS(statusPage, slug) {
|
||||
const { heartbeats, statusDescription } = await StatusPage.getRSSPageData(statusPage);
|
||||
|
||||
let proto = config.isSSL ? "https" : "http";
|
||||
let host = `${proto}://${config.hostname || "localhost"}:${config.port}/status/${slug}`;
|
||||
|
||||
const feed = new Feed({
|
||||
title: "uptime kuma rss feed",
|
||||
description: `current status: ${statusDescription}`,
|
||||
link: host,
|
||||
language: "en", // optional, used only in RSS 2.0, possible values: http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
|
||||
updated: new Date(), // optional, default = today
|
||||
});
|
||||
|
||||
heartbeats.forEach(heartbeat => {
|
||||
feed.addItem({
|
||||
title: `${heartbeat.name} is down`,
|
||||
description: `${heartbeat.name} has been down since ${heartbeat.time}`,
|
||||
id: heartbeat.monitorID,
|
||||
date: new Date(heartbeat.time),
|
||||
});
|
||||
});
|
||||
|
||||
return feed.rss2();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {Promise<string>} the rendered html
|
||||
*/
|
||||
static async renderHTML(indexHTML, statusPage) {
|
||||
const $ = cheerio.load(indexHTML);
|
||||
const description155 = statusPage.description?.substring(0, 155) ?? "";
|
||||
|
||||
const description155 = marked(statusPage.description ?? "")
|
||||
.replace(/<[^>]+>/gm, "")
|
||||
.trim()
|
||||
.substring(0, 155);
|
||||
|
||||
$("title").text(statusPage.title);
|
||||
$("meta[name=description]").attr("content", description155);
|
||||
@@ -85,9 +152,113 @@ class StatusPage extends BeanModel {
|
||||
return $.root().html();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {heartbeats} heartbeats from getRSSPageData
|
||||
* @returns {number} status_page constant from util.ts
|
||||
*/
|
||||
static overallStatus(heartbeats) {
|
||||
if (heartbeats.length === 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
let status = STATUS_PAGE_ALL_UP;
|
||||
let hasUp = false;
|
||||
|
||||
for (let beat of heartbeats) {
|
||||
if (beat.status === MAINTENANCE) {
|
||||
return STATUS_PAGE_MAINTENANCE;
|
||||
} else if (beat.status === UP) {
|
||||
hasUp = true;
|
||||
} else {
|
||||
status = STATUS_PAGE_PARTIAL_DOWN;
|
||||
}
|
||||
}
|
||||
|
||||
if (! hasUp) {
|
||||
status = STATUS_PAGE_ALL_DOWN;
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} status from overallStatus
|
||||
* @returns {string} description
|
||||
*/
|
||||
static getStatusDescription(status) {
|
||||
if (status === -1) {
|
||||
return "No Services";
|
||||
}
|
||||
|
||||
if (status === STATUS_PAGE_ALL_UP) {
|
||||
return "All Systems Operational";
|
||||
}
|
||||
|
||||
if (status === STATUS_PAGE_PARTIAL_DOWN) {
|
||||
return "Partially Degraded Service";
|
||||
}
|
||||
|
||||
if (status === STATUS_PAGE_ALL_DOWN) {
|
||||
return "Degraded Service";
|
||||
}
|
||||
|
||||
// TODO: show the real maintenance information: title, description, time
|
||||
if (status === MAINTENANCE) {
|
||||
return "Under maintenance";
|
||||
}
|
||||
|
||||
return "?";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all data required for RSS
|
||||
* @param {StatusPage} statusPage Status page to get data for
|
||||
* @returns {object} Status page data
|
||||
*/
|
||||
static async getRSSPageData(statusPage) {
|
||||
// get all heartbeats that correspond to this statusPage
|
||||
const config = await statusPage.toPublicJSON();
|
||||
|
||||
// Public Group List
|
||||
const showTags = !!statusPage.show_tags;
|
||||
|
||||
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
|
||||
statusPage.id
|
||||
]);
|
||||
|
||||
let heartbeats = [];
|
||||
|
||||
for (let groupBean of list) {
|
||||
let monitorGroup = await groupBean.toPublicJSON(showTags, config?.showCertificateExpiry);
|
||||
for (const monitor of monitorGroup.monitorList) {
|
||||
const heartbeat = await R.findOne("heartbeat", "monitor_id = ? ORDER BY time DESC", [ monitor.id ]);
|
||||
if (heartbeat) {
|
||||
heartbeats.push({
|
||||
...monitor,
|
||||
status: heartbeat.status,
|
||||
time: heartbeat.time
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// calculate RSS feed description
|
||||
let status = StatusPage.overallStatus(heartbeats);
|
||||
let statusDescription = StatusPage.getStatusDescription(status);
|
||||
|
||||
// keep only DOWN heartbeats in the RSS feed
|
||||
heartbeats = heartbeats.filter(heartbeat => heartbeat.status === DOWN);
|
||||
|
||||
return {
|
||||
heartbeats,
|
||||
statusDescription
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 +313,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 +330,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 +374,7 @@ class StatusPage extends BeanModel {
|
||||
|
||||
/**
|
||||
* Get list of domain names
|
||||
* @returns {Object[]}
|
||||
* @returns {object[]} List of status page domains
|
||||
*/
|
||||
getDomainNameList() {
|
||||
let domainList = [];
|
||||
@@ -219,7 +390,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 {
|
||||
@@ -229,6 +400,7 @@ class StatusPage extends BeanModel {
|
||||
description: this.description,
|
||||
icon: this.getIcon(),
|
||||
theme: this.theme,
|
||||
autoRefreshInterval: this.autoRefreshInterval,
|
||||
published: !!this.published,
|
||||
showTags: !!this.show_tags,
|
||||
domainNameList: this.getDomainNameList(),
|
||||
@@ -243,7 +415,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 {
|
||||
@@ -251,6 +423,7 @@ class StatusPage extends BeanModel {
|
||||
title: this.title,
|
||||
description: this.description,
|
||||
icon: this.getIcon(),
|
||||
autoRefreshInterval: this.autoRefreshInterval,
|
||||
theme: this.theme,
|
||||
published: !!this.published,
|
||||
showTags: !!this.show_tags,
|
||||
@@ -264,7 +437,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 +448,7 @@ class StatusPage extends BeanModel {
|
||||
|
||||
/**
|
||||
* Get path to the icon for the page
|
||||
* @returns {string}
|
||||
* @returns {string} Path
|
||||
*/
|
||||
getIcon() {
|
||||
if (!this.icon) {
|
||||
@@ -287,7 +461,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,19 +21,25 @@ class User extends BeanModel {
|
||||
|
||||
/**
|
||||
* Reset this users password
|
||||
* @param {string} newPassword
|
||||
* @param {string} newPassword Users new password
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async resetPassword(newPassword) {
|
||||
await User.resetPassword(this.id, newPassword);
|
||||
this.password = newPassword;
|
||||
const hashedPassword = passwordHash.generate(newPassword);
|
||||
|
||||
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
|
||||
hashedPassword,
|
||||
this.id
|
||||
]);
|
||||
|
||||
this.password = hashedPassword;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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({
|
||||
|
21
server/modules/axios-ntlm/LICENSE
Normal file
21
server/modules/axios-ntlm/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 CatButtes
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
77
server/modules/axios-ntlm/lib/flags.js
Normal file
77
server/modules/axios-ntlm/lib/flags.js
Normal file
@@ -0,0 +1,77 @@
|
||||
'use strict';
|
||||
// Original file https://raw.githubusercontent.com/elasticio/node-ntlm-client/master/lib/flags.js
|
||||
module.exports.NTLMFLAG_NEGOTIATE_UNICODE = 1 << 0;
|
||||
/* Indicates that Unicode strings are supported for use in security buffer
|
||||
data. */
|
||||
module.exports.NTLMFLAG_NEGOTIATE_OEM = 1 << 1;
|
||||
/* Indicates that OEM strings are supported for use in security buffer data. */
|
||||
module.exports.NTLMFLAG_REQUEST_TARGET = 1 << 2;
|
||||
/* Requests that the server's authentication realm be included in the Type 2
|
||||
message. */
|
||||
/* unknown (1<<3) */
|
||||
module.exports.NTLMFLAG_NEGOTIATE_SIGN = 1 << 4;
|
||||
/* Specifies that authenticated communication between the client and server
|
||||
should carry a digital signature (message integrity). */
|
||||
module.exports.NTLMFLAG_NEGOTIATE_SEAL = 1 << 5;
|
||||
/* Specifies that authenticated communication between the client and server
|
||||
should be encrypted (message confidentiality). */
|
||||
module.exports.NTLMFLAG_NEGOTIATE_DATAGRAM_STYLE = 1 << 6;
|
||||
/* Indicates that datagram authentication is being used. */
|
||||
module.exports.NTLMFLAG_NEGOTIATE_LM_KEY = 1 << 7;
|
||||
/* Indicates that the LAN Manager session key should be used for signing and
|
||||
sealing authenticated communications. */
|
||||
module.exports.NTLMFLAG_NEGOTIATE_NETWARE = 1 << 8;
|
||||
/* unknown purpose */
|
||||
module.exports.NTLMFLAG_NEGOTIATE_NTLM_KEY = 1 << 9;
|
||||
/* Indicates that NTLM authentication is being used. */
|
||||
/* unknown (1<<10) */
|
||||
module.exports.NTLMFLAG_NEGOTIATE_ANONYMOUS = 1 << 11;
|
||||
/* Sent by the client in the Type 3 message to indicate that an anonymous
|
||||
context has been established. This also affects the response fields. */
|
||||
module.exports.NTLMFLAG_NEGOTIATE_DOMAIN_SUPPLIED = 1 << 12;
|
||||
/* Sent by the client in the Type 1 message to indicate that a desired
|
||||
authentication realm is included in the message. */
|
||||
module.exports.NTLMFLAG_NEGOTIATE_WORKSTATION_SUPPLIED = 1 << 13;
|
||||
/* Sent by the client in the Type 1 message to indicate that the client
|
||||
workstation's name is included in the message. */
|
||||
module.exports.NTLMFLAG_NEGOTIATE_LOCAL_CALL = 1 << 14;
|
||||
/* Sent by the server to indicate that the server and client are on the same
|
||||
machine. Implies that the client may use a pre-established local security
|
||||
context rather than responding to the challenge. */
|
||||
module.exports.NTLMFLAG_NEGOTIATE_ALWAYS_SIGN = 1 << 15;
|
||||
/* Indicates that authenticated communication between the client and server
|
||||
should be signed with a "dummy" signature. */
|
||||
module.exports.NTLMFLAG_TARGET_TYPE_DOMAIN = 1 << 16;
|
||||
/* Sent by the server in the Type 2 message to indicate that the target
|
||||
authentication realm is a domain. */
|
||||
module.exports.NTLMFLAG_TARGET_TYPE_SERVER = 1 << 17;
|
||||
/* Sent by the server in the Type 2 message to indicate that the target
|
||||
authentication realm is a server. */
|
||||
module.exports.NTLMFLAG_TARGET_TYPE_SHARE = 1 << 18;
|
||||
/* Sent by the server in the Type 2 message to indicate that the target
|
||||
authentication realm is a share. Presumably, this is for share-level
|
||||
authentication. Usage is unclear. */
|
||||
module.exports.NTLMFLAG_NEGOTIATE_NTLM2_KEY = 1 << 19;
|
||||
/* Indicates that the NTLM2 signing and sealing scheme should be used for
|
||||
protecting authenticated communications. */
|
||||
module.exports.NTLMFLAG_REQUEST_INIT_RESPONSE = 1 << 20;
|
||||
/* unknown purpose */
|
||||
module.exports.NTLMFLAG_REQUEST_ACCEPT_RESPONSE = 1 << 21;
|
||||
/* unknown purpose */
|
||||
module.exports.NTLMFLAG_REQUEST_NONNT_SESSION_KEY = 1 << 22;
|
||||
/* unknown purpose */
|
||||
module.exports.NTLMFLAG_NEGOTIATE_TARGET_INFO = 1 << 23;
|
||||
/* Sent by the server in the Type 2 message to indicate that it is including a
|
||||
Target Information block in the message. */
|
||||
/* unknown (1<24) */
|
||||
/* unknown (1<25) */
|
||||
/* unknown (1<26) */
|
||||
/* unknown (1<27) */
|
||||
/* unknown (1<28) */
|
||||
module.exports.NTLMFLAG_NEGOTIATE_128 = 1 << 29;
|
||||
/* Indicates that 128-bit encryption is supported. */
|
||||
module.exports.NTLMFLAG_NEGOTIATE_KEY_EXCHANGE = 1 << 30;
|
||||
/* Indicates that the client will provide an encrypted master key in
|
||||
the "Session Key" field of the Type 3 message. */
|
||||
module.exports.NTLMFLAG_NEGOTIATE_56 = 1 << 31;
|
||||
//# sourceMappingURL=flags.js.map
|
122
server/modules/axios-ntlm/lib/hash.js
Normal file
122
server/modules/axios-ntlm/lib/hash.js
Normal file
@@ -0,0 +1,122 @@
|
||||
'use strict';
|
||||
// Original source at https://github.com/elasticio/node-ntlm-client/blob/master/lib/hash.js
|
||||
var crypto = require('crypto');
|
||||
function createLMResponse(challenge, lmhash) {
|
||||
var buf = new Buffer.alloc(24), pwBuffer = new Buffer.alloc(21).fill(0);
|
||||
lmhash.copy(pwBuffer);
|
||||
calculateDES(pwBuffer.slice(0, 7), challenge).copy(buf);
|
||||
calculateDES(pwBuffer.slice(7, 14), challenge).copy(buf, 8);
|
||||
calculateDES(pwBuffer.slice(14), challenge).copy(buf, 16);
|
||||
return buf;
|
||||
}
|
||||
function createLMHash(password) {
|
||||
var buf = new Buffer.alloc(16), pwBuffer = new Buffer.alloc(14), magicKey = new Buffer.from('KGS!@#$%', 'ascii');
|
||||
if (password.length > 14) {
|
||||
buf.fill(0);
|
||||
return buf;
|
||||
}
|
||||
pwBuffer.fill(0);
|
||||
pwBuffer.write(password.toUpperCase(), 0, 'ascii');
|
||||
return Buffer.concat([
|
||||
calculateDES(pwBuffer.slice(0, 7), magicKey),
|
||||
calculateDES(pwBuffer.slice(7), magicKey)
|
||||
]);
|
||||
}
|
||||
function calculateDES(key, message) {
|
||||
var desKey = new Buffer.alloc(8);
|
||||
desKey[0] = key[0] & 0xFE;
|
||||
desKey[1] = ((key[0] << 7) & 0xFF) | (key[1] >> 1);
|
||||
desKey[2] = ((key[1] << 6) & 0xFF) | (key[2] >> 2);
|
||||
desKey[3] = ((key[2] << 5) & 0xFF) | (key[3] >> 3);
|
||||
desKey[4] = ((key[3] << 4) & 0xFF) | (key[4] >> 4);
|
||||
desKey[5] = ((key[4] << 3) & 0xFF) | (key[5] >> 5);
|
||||
desKey[6] = ((key[5] << 2) & 0xFF) | (key[6] >> 6);
|
||||
desKey[7] = (key[6] << 1) & 0xFF;
|
||||
for (var i = 0; i < 8; i++) {
|
||||
var parity = 0;
|
||||
for (var j = 1; j < 8; j++) {
|
||||
parity += (desKey[i] >> j) % 2;
|
||||
}
|
||||
desKey[i] |= (parity % 2) === 0 ? 1 : 0;
|
||||
}
|
||||
var des = crypto.createCipheriv('DES-ECB', desKey, '');
|
||||
return des.update(message);
|
||||
}
|
||||
function createNTLMResponse(challenge, ntlmhash) {
|
||||
var buf = new Buffer.alloc(24), ntlmBuffer = new Buffer.alloc(21).fill(0);
|
||||
ntlmhash.copy(ntlmBuffer);
|
||||
calculateDES(ntlmBuffer.slice(0, 7), challenge).copy(buf);
|
||||
calculateDES(ntlmBuffer.slice(7, 14), challenge).copy(buf, 8);
|
||||
calculateDES(ntlmBuffer.slice(14), challenge).copy(buf, 16);
|
||||
return buf;
|
||||
}
|
||||
function createNTLMHash(password) {
|
||||
var md4sum = crypto.createHash('md4');
|
||||
md4sum.update(new Buffer.from(password, 'ucs2'));
|
||||
return md4sum.digest();
|
||||
}
|
||||
function createNTLMv2Hash(ntlmhash, username, authTargetName) {
|
||||
var hmac = crypto.createHmac('md5', ntlmhash);
|
||||
hmac.update(new Buffer.from(username.toUpperCase() + authTargetName, 'ucs2'));
|
||||
return hmac.digest();
|
||||
}
|
||||
function createLMv2Response(type2message, username, ntlmhash, nonce, targetName) {
|
||||
var buf = new Buffer.alloc(24), ntlm2hash = createNTLMv2Hash(ntlmhash, username, targetName), hmac = crypto.createHmac('md5', ntlm2hash);
|
||||
//server challenge
|
||||
type2message.challenge.copy(buf, 8);
|
||||
//client nonce
|
||||
buf.write(nonce || createPseudoRandomValue(16), 16, 'hex');
|
||||
//create hash
|
||||
hmac.update(buf.slice(8));
|
||||
var hashedBuffer = hmac.digest();
|
||||
hashedBuffer.copy(buf);
|
||||
return buf;
|
||||
}
|
||||
function createNTLMv2Response(type2message, username, ntlmhash, nonce, targetName) {
|
||||
var buf = new Buffer.alloc(48 + type2message.targetInfo.buffer.length), ntlm2hash = createNTLMv2Hash(ntlmhash, username, targetName), hmac = crypto.createHmac('md5', ntlm2hash);
|
||||
//the first 8 bytes are spare to store the hashed value before the blob
|
||||
//server challenge
|
||||
type2message.challenge.copy(buf, 8);
|
||||
//blob signature
|
||||
buf.writeUInt32BE(0x01010000, 16);
|
||||
//reserved
|
||||
buf.writeUInt32LE(0, 20);
|
||||
//timestamp
|
||||
//TODO: we are loosing precision here since js is not able to handle those large integers
|
||||
// maybe think about a different solution here
|
||||
// 11644473600000 = diff between 1970 and 1601
|
||||
var timestamp = ((Date.now() + 11644473600000) * 10000).toString(16);
|
||||
var timestampLow = Number('0x' + timestamp.substring(Math.max(0, timestamp.length - 8)));
|
||||
var timestampHigh = Number('0x' + timestamp.substring(0, Math.max(0, timestamp.length - 8)));
|
||||
buf.writeUInt32LE(timestampLow, 24, false);
|
||||
buf.writeUInt32LE(timestampHigh, 28, false);
|
||||
//random client nonce
|
||||
buf.write(nonce || createPseudoRandomValue(16), 32, 'hex');
|
||||
//zero
|
||||
buf.writeUInt32LE(0, 40);
|
||||
//complete target information block from type 2 message
|
||||
type2message.targetInfo.buffer.copy(buf, 44);
|
||||
//zero
|
||||
buf.writeUInt32LE(0, 44 + type2message.targetInfo.buffer.length);
|
||||
hmac.update(buf.slice(8));
|
||||
var hashedBuffer = hmac.digest();
|
||||
hashedBuffer.copy(buf);
|
||||
return buf;
|
||||
}
|
||||
function createPseudoRandomValue(length) {
|
||||
var str = '';
|
||||
while (str.length < length) {
|
||||
str += Math.floor(Math.random() * 16).toString(16);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
module.exports = {
|
||||
createLMHash: createLMHash,
|
||||
createNTLMHash: createNTLMHash,
|
||||
createLMResponse: createLMResponse,
|
||||
createNTLMResponse: createNTLMResponse,
|
||||
createLMv2Response: createLMv2Response,
|
||||
createNTLMv2Response: createNTLMv2Response,
|
||||
createPseudoRandomValue: createPseudoRandomValue
|
||||
};
|
||||
//# sourceMappingURL=hash.js.map
|
220
server/modules/axios-ntlm/lib/ntlm.js
Normal file
220
server/modules/axios-ntlm/lib/ntlm.js
Normal file
@@ -0,0 +1,220 @@
|
||||
'use strict';
|
||||
// Original file https://raw.githubusercontent.com/elasticio/node-ntlm-client/master/lib/ntlm.js
|
||||
var os = require('os'), flags = require('./flags'), hash = require('./hash');
|
||||
var NTLMSIGNATURE = "NTLMSSP\0";
|
||||
function createType1Message(workstation, target) {
|
||||
var dataPos = 32, pos = 0, buf = new Buffer.alloc(1024);
|
||||
workstation = workstation === undefined ? os.hostname() : workstation;
|
||||
target = target === undefined ? '' : target;
|
||||
//signature
|
||||
buf.write(NTLMSIGNATURE, pos, NTLMSIGNATURE.length, 'ascii');
|
||||
pos += NTLMSIGNATURE.length;
|
||||
//message type
|
||||
buf.writeUInt32LE(1, pos);
|
||||
pos += 4;
|
||||
//flags
|
||||
buf.writeUInt32LE(flags.NTLMFLAG_NEGOTIATE_OEM |
|
||||
flags.NTLMFLAG_REQUEST_TARGET |
|
||||
flags.NTLMFLAG_NEGOTIATE_NTLM_KEY |
|
||||
flags.NTLMFLAG_NEGOTIATE_NTLM2_KEY |
|
||||
flags.NTLMFLAG_NEGOTIATE_ALWAYS_SIGN, pos);
|
||||
pos += 4;
|
||||
//domain security buffer
|
||||
buf.writeUInt16LE(target.length, pos);
|
||||
pos += 2;
|
||||
buf.writeUInt16LE(target.length, pos);
|
||||
pos += 2;
|
||||
buf.writeUInt32LE(target.length === 0 ? 0 : dataPos, pos);
|
||||
pos += 4;
|
||||
if (target.length > 0) {
|
||||
dataPos += buf.write(target, dataPos, 'ascii');
|
||||
}
|
||||
//workstation security buffer
|
||||
buf.writeUInt16LE(workstation.length, pos);
|
||||
pos += 2;
|
||||
buf.writeUInt16LE(workstation.length, pos);
|
||||
pos += 2;
|
||||
buf.writeUInt32LE(workstation.length === 0 ? 0 : dataPos, pos);
|
||||
pos += 4;
|
||||
if (workstation.length > 0) {
|
||||
dataPos += buf.write(workstation, dataPos, 'ascii');
|
||||
}
|
||||
return 'NTLM ' + buf.toString('base64', 0, dataPos);
|
||||
}
|
||||
function decodeType2Message(str) {
|
||||
if (str === undefined) {
|
||||
throw new Error('Invalid argument');
|
||||
}
|
||||
//convenience
|
||||
if (Object.prototype.toString.call(str) !== '[object String]') {
|
||||
if (str.hasOwnProperty('headers') && str.headers.hasOwnProperty('www-authenticate')) {
|
||||
str = str.headers['www-authenticate'];
|
||||
}
|
||||
else {
|
||||
throw new Error('Invalid argument');
|
||||
}
|
||||
}
|
||||
var ntlmMatch = /^NTLM ([^,\s]+)/.exec(str);
|
||||
if (ntlmMatch) {
|
||||
str = ntlmMatch[1];
|
||||
}
|
||||
var buf = new Buffer.from(str, 'base64'), obj = {};
|
||||
//check signature
|
||||
if (buf.toString('ascii', 0, NTLMSIGNATURE.length) !== NTLMSIGNATURE) {
|
||||
throw new Error('Invalid message signature: ' + str);
|
||||
}
|
||||
//check message type
|
||||
if (buf.readUInt32LE(NTLMSIGNATURE.length) !== 2) {
|
||||
throw new Error('Invalid message type (no type 2)');
|
||||
}
|
||||
//read flags
|
||||
obj.flags = buf.readUInt32LE(20);
|
||||
obj.encoding = (obj.flags & flags.NTLMFLAG_NEGOTIATE_OEM) ? 'ascii' : 'ucs2';
|
||||
obj.version = (obj.flags & flags.NTLMFLAG_NEGOTIATE_NTLM2_KEY) ? 2 : 1;
|
||||
obj.challenge = buf.slice(24, 32);
|
||||
//read target name
|
||||
obj.targetName = (function () {
|
||||
var length = buf.readUInt16LE(12);
|
||||
//skipping allocated space
|
||||
var offset = buf.readUInt32LE(16);
|
||||
if (length === 0) {
|
||||
return '';
|
||||
}
|
||||
if ((offset + length) > buf.length || offset < 32) {
|
||||
throw new Error('Bad type 2 message');
|
||||
}
|
||||
return buf.toString(obj.encoding, offset, offset + length);
|
||||
})();
|
||||
//read target info
|
||||
if (obj.flags & flags.NTLMFLAG_NEGOTIATE_TARGET_INFO) {
|
||||
obj.targetInfo = (function () {
|
||||
var info = {};
|
||||
var length = buf.readUInt16LE(40);
|
||||
//skipping allocated space
|
||||
var offset = buf.readUInt32LE(44);
|
||||
var targetInfoBuffer = new Buffer.alloc(length);
|
||||
buf.copy(targetInfoBuffer, 0, offset, offset + length);
|
||||
if (length === 0) {
|
||||
return info;
|
||||
}
|
||||
if ((offset + length) > buf.length || offset < 32) {
|
||||
throw new Error('Bad type 2 message');
|
||||
}
|
||||
var pos = offset;
|
||||
while (pos < (offset + length)) {
|
||||
var blockType = buf.readUInt16LE(pos);
|
||||
pos += 2;
|
||||
var blockLength = buf.readUInt16LE(pos);
|
||||
pos += 2;
|
||||
if (blockType === 0) {
|
||||
//reached the terminator subblock
|
||||
break;
|
||||
}
|
||||
var blockTypeStr = void 0;
|
||||
switch (blockType) {
|
||||
case 1:
|
||||
blockTypeStr = 'SERVER';
|
||||
break;
|
||||
case 2:
|
||||
blockTypeStr = 'DOMAIN';
|
||||
break;
|
||||
case 3:
|
||||
blockTypeStr = 'FQDN';
|
||||
break;
|
||||
case 4:
|
||||
blockTypeStr = 'DNS';
|
||||
break;
|
||||
case 5:
|
||||
blockTypeStr = 'PARENT_DNS';
|
||||
break;
|
||||
default:
|
||||
blockTypeStr = '';
|
||||
break;
|
||||
}
|
||||
if (blockTypeStr) {
|
||||
info[blockTypeStr] = buf.toString('ucs2', pos, pos + blockLength);
|
||||
}
|
||||
pos += blockLength;
|
||||
}
|
||||
return {
|
||||
parsed: info,
|
||||
buffer: targetInfoBuffer
|
||||
};
|
||||
})();
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
function createType3Message(type2Message, username, password, workstation, target) {
|
||||
var dataPos = 52, buf = new Buffer.alloc(1024);
|
||||
if (workstation === undefined) {
|
||||
workstation = os.hostname();
|
||||
}
|
||||
if (target === undefined) {
|
||||
target = type2Message.targetName;
|
||||
}
|
||||
//signature
|
||||
buf.write(NTLMSIGNATURE, 0, NTLMSIGNATURE.length, 'ascii');
|
||||
//message type
|
||||
buf.writeUInt32LE(3, 8);
|
||||
if (type2Message.version === 2) {
|
||||
dataPos = 64;
|
||||
var ntlmHash = hash.createNTLMHash(password), nonce = hash.createPseudoRandomValue(16), lmv2 = hash.createLMv2Response(type2Message, username, ntlmHash, nonce, target), ntlmv2 = hash.createNTLMv2Response(type2Message, username, ntlmHash, nonce, target);
|
||||
//lmv2 security buffer
|
||||
buf.writeUInt16LE(lmv2.length, 12);
|
||||
buf.writeUInt16LE(lmv2.length, 14);
|
||||
buf.writeUInt32LE(dataPos, 16);
|
||||
lmv2.copy(buf, dataPos);
|
||||
dataPos += lmv2.length;
|
||||
//ntlmv2 security buffer
|
||||
buf.writeUInt16LE(ntlmv2.length, 20);
|
||||
buf.writeUInt16LE(ntlmv2.length, 22);
|
||||
buf.writeUInt32LE(dataPos, 24);
|
||||
ntlmv2.copy(buf, dataPos);
|
||||
dataPos += ntlmv2.length;
|
||||
}
|
||||
else {
|
||||
var lmHash = hash.createLMHash(password), ntlmHash = hash.createNTLMHash(password), lm = hash.createLMResponse(type2Message.challenge, lmHash), ntlm = hash.createNTLMResponse(type2Message.challenge, ntlmHash);
|
||||
//lm security buffer
|
||||
buf.writeUInt16LE(lm.length, 12);
|
||||
buf.writeUInt16LE(lm.length, 14);
|
||||
buf.writeUInt32LE(dataPos, 16);
|
||||
lm.copy(buf, dataPos);
|
||||
dataPos += lm.length;
|
||||
//ntlm security buffer
|
||||
buf.writeUInt16LE(ntlm.length, 20);
|
||||
buf.writeUInt16LE(ntlm.length, 22);
|
||||
buf.writeUInt32LE(dataPos, 24);
|
||||
ntlm.copy(buf, dataPos);
|
||||
dataPos += ntlm.length;
|
||||
}
|
||||
//target name security buffer
|
||||
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? target.length : target.length * 2, 28);
|
||||
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? target.length : target.length * 2, 30);
|
||||
buf.writeUInt32LE(dataPos, 32);
|
||||
dataPos += buf.write(target, dataPos, type2Message.encoding);
|
||||
//user name security buffer
|
||||
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? username.length : username.length * 2, 36);
|
||||
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? username.length : username.length * 2, 38);
|
||||
buf.writeUInt32LE(dataPos, 40);
|
||||
dataPos += buf.write(username, dataPos, type2Message.encoding);
|
||||
//workstation name security buffer
|
||||
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? workstation.length : workstation.length * 2, 44);
|
||||
buf.writeUInt16LE(type2Message.encoding === 'ascii' ? workstation.length : workstation.length * 2, 46);
|
||||
buf.writeUInt32LE(dataPos, 48);
|
||||
dataPos += buf.write(workstation, dataPos, type2Message.encoding);
|
||||
if (type2Message.version === 2) {
|
||||
//session key security buffer
|
||||
buf.writeUInt16LE(0, 52);
|
||||
buf.writeUInt16LE(0, 54);
|
||||
buf.writeUInt32LE(0, 56);
|
||||
//flags
|
||||
buf.writeUInt32LE(type2Message.flags, 60);
|
||||
}
|
||||
return 'NTLM ' + buf.toString('base64', 0, dataPos);
|
||||
}
|
||||
module.exports = {
|
||||
createType1Message: createType1Message,
|
||||
decodeType2Message: decodeType2Message,
|
||||
createType3Message: createType3Message
|
||||
};
|
||||
//# sourceMappingURL=ntlm.js.map
|
127
server/modules/axios-ntlm/lib/ntlmClient.js
Normal file
127
server/modules/axios-ntlm/lib/ntlmClient.js
Normal file
@@ -0,0 +1,127 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.NtlmClient = void 0;
|
||||
var axios_1 = __importDefault(require("axios"));
|
||||
var ntlm = __importStar(require("./ntlm"));
|
||||
var https = __importStar(require("https"));
|
||||
var http = __importStar(require("http"));
|
||||
var dev_null_1 = __importDefault(require("dev-null"));
|
||||
/**
|
||||
* @param credentials An NtlmCredentials object containing the username and password
|
||||
* @param AxiosConfig The Axios config for the instance you wish to create
|
||||
*
|
||||
* @returns This function returns an axios instance configured to use the provided credentials
|
||||
*/
|
||||
function NtlmClient(credentials, AxiosConfig) {
|
||||
var _this = this;
|
||||
var config = AxiosConfig !== null && AxiosConfig !== void 0 ? AxiosConfig : {};
|
||||
if (!config.httpAgent) {
|
||||
config.httpAgent = new http.Agent({ keepAlive: true });
|
||||
}
|
||||
if (!config.httpsAgent) {
|
||||
config.httpsAgent = new https.Agent({ keepAlive: true });
|
||||
}
|
||||
var client = axios_1.default.create(config);
|
||||
client.interceptors.response.use(function (response) {
|
||||
return response;
|
||||
}, function (err) { return __awaiter(_this, void 0, void 0, function () {
|
||||
var error, t1Msg, t2Msg, t3Msg, stream_1;
|
||||
var _a;
|
||||
return __generator(this, function (_b) {
|
||||
switch (_b.label) {
|
||||
case 0:
|
||||
error = err.response;
|
||||
if (!(error && error.status === 401
|
||||
&& error.headers['www-authenticate']
|
||||
&& error.headers['www-authenticate'].includes('NTLM'))) return [3 /*break*/, 3];
|
||||
// This length check is a hack because SharePoint is awkward and will
|
||||
// include the Negotiate option when responding with the T2 message
|
||||
// There is nore we could do to ensure we are processing correctly,
|
||||
// but this is the easiest option for now
|
||||
if (error.headers['www-authenticate'].length < 50) {
|
||||
t1Msg = ntlm.createType1Message(credentials.workstation, credentials.domain);
|
||||
error.config.headers["Authorization"] = t1Msg;
|
||||
}
|
||||
else {
|
||||
t2Msg = ntlm.decodeType2Message((error.headers['www-authenticate'].match(/^NTLM\s+(.+?)(,|\s+|$)/) || [])[1]);
|
||||
t3Msg = ntlm.createType3Message(t2Msg, credentials.username, credentials.password, credentials.workstation, credentials.domain);
|
||||
error.config.headers["X-retry"] = "false";
|
||||
error.config.headers["Authorization"] = t3Msg;
|
||||
}
|
||||
if (!(error.config.responseType === "stream")) return [3 /*break*/, 2];
|
||||
stream_1 = (_a = err.response) === null || _a === void 0 ? void 0 : _a.data;
|
||||
if (!(stream_1 && !stream_1.readableEnded)) return [3 /*break*/, 2];
|
||||
return [4 /*yield*/, new Promise(function (resolve) {
|
||||
stream_1.pipe((0, dev_null_1.default)());
|
||||
stream_1.once('close', resolve);
|
||||
})];
|
||||
case 1:
|
||||
_b.sent();
|
||||
_b.label = 2;
|
||||
case 2: return [2 /*return*/, client(error.config)];
|
||||
case 3: throw err;
|
||||
}
|
||||
});
|
||||
}); });
|
||||
return client;
|
||||
}
|
||||
exports.NtlmClient = NtlmClient;
|
||||
//# sourceMappingURL=ntlmClient.js.map
|
71
server/monitor-conditions/evaluator.js
Normal file
71
server/monitor-conditions/evaluator.js
Normal file
@@ -0,0 +1,71 @@
|
||||
const { ConditionExpressionGroup, ConditionExpression, LOGICAL } = require("./expression");
|
||||
const { operatorMap } = require("./operators");
|
||||
|
||||
/**
|
||||
* @param {ConditionExpression} expression Expression to evaluate
|
||||
* @param {object} context Context to evaluate against; These are values for variables in the expression
|
||||
* @returns {boolean} Whether the expression evaluates true or false
|
||||
* @throws {Error}
|
||||
*/
|
||||
function evaluateExpression(expression, context) {
|
||||
/**
|
||||
* @type {import("./operators").ConditionOperator|null}
|
||||
*/
|
||||
const operator = operatorMap.get(expression.operator) || null;
|
||||
if (operator === null) {
|
||||
throw new Error("Unexpected expression operator ID '" + expression.operator + "'. Expected one of [" + operatorMap.keys().join(",") + "]");
|
||||
}
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(context, expression.variable)) {
|
||||
throw new Error("Variable missing in context: " + expression.variable);
|
||||
}
|
||||
|
||||
return operator.test(context[expression.variable], expression.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ConditionExpressionGroup} group Group of expressions to evaluate
|
||||
* @param {object} context Context to evaluate against; These are values for variables in the expression
|
||||
* @returns {boolean} Whether the group evaluates true or false
|
||||
* @throws {Error}
|
||||
*/
|
||||
function evaluateExpressionGroup(group, context) {
|
||||
if (!group.children.length) {
|
||||
throw new Error("ConditionExpressionGroup must contain at least one child.");
|
||||
}
|
||||
|
||||
let result = null;
|
||||
|
||||
for (const child of group.children) {
|
||||
let childResult;
|
||||
|
||||
if (child instanceof ConditionExpression) {
|
||||
childResult = evaluateExpression(child, context);
|
||||
} else if (child instanceof ConditionExpressionGroup) {
|
||||
childResult = evaluateExpressionGroup(child, context);
|
||||
} else {
|
||||
throw new Error("Invalid child type in ConditionExpressionGroup. Expected ConditionExpression or ConditionExpressionGroup");
|
||||
}
|
||||
|
||||
if (result === null) {
|
||||
result = childResult; // Initialize result with the first child's result
|
||||
} else if (child.andOr === LOGICAL.OR) {
|
||||
result = result || childResult;
|
||||
} else if (child.andOr === LOGICAL.AND) {
|
||||
result = result && childResult;
|
||||
} else {
|
||||
throw new Error("Invalid logical operator in child of ConditionExpressionGroup. Expected 'and' or 'or'. Got '" + group.andOr + "'");
|
||||
}
|
||||
}
|
||||
|
||||
if (result === null) {
|
||||
throw new Error("ConditionExpressionGroup did not result in a boolean.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
evaluateExpression,
|
||||
evaluateExpressionGroup,
|
||||
};
|
111
server/monitor-conditions/expression.js
Normal file
111
server/monitor-conditions/expression.js
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
const LOGICAL = {
|
||||
AND: "and",
|
||||
OR: "or",
|
||||
};
|
||||
|
||||
/**
|
||||
* Recursively processes an array of raw condition objects and populates the given parent group with
|
||||
* corresponding ConditionExpression or ConditionExpressionGroup instances.
|
||||
* @param {Array} conditions Array of raw condition objects, where each object represents either a group or an expression.
|
||||
* @param {ConditionExpressionGroup} parentGroup The parent group to which the instantiated ConditionExpression or ConditionExpressionGroup objects will be added.
|
||||
* @returns {void}
|
||||
*/
|
||||
function processMonitorConditions(conditions, parentGroup) {
|
||||
conditions.forEach(condition => {
|
||||
const andOr = condition.andOr === LOGICAL.OR ? LOGICAL.OR : LOGICAL.AND;
|
||||
|
||||
if (condition.type === "group") {
|
||||
const group = new ConditionExpressionGroup([], andOr);
|
||||
|
||||
// Recursively process the group's children
|
||||
processMonitorConditions(condition.children, group);
|
||||
|
||||
parentGroup.children.push(group);
|
||||
} else if (condition.type === "expression") {
|
||||
const expression = new ConditionExpression(condition.variable, condition.operator, condition.value, andOr);
|
||||
parentGroup.children.push(expression);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class ConditionExpressionGroup {
|
||||
/**
|
||||
* @type {ConditionExpressionGroup[]|ConditionExpression[]} Groups and/or expressions to test
|
||||
*/
|
||||
children = [];
|
||||
|
||||
/**
|
||||
* @type {LOGICAL} Connects group result with previous group/expression results
|
||||
*/
|
||||
andOr;
|
||||
|
||||
/**
|
||||
* @param {ConditionExpressionGroup[]|ConditionExpression[]} children Groups and/or expressions to test
|
||||
* @param {LOGICAL} andOr Connects group result with previous group/expression results
|
||||
*/
|
||||
constructor(children = [], andOr = LOGICAL.AND) {
|
||||
this.children = children;
|
||||
this.andOr = andOr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Monitor} monitor Monitor instance
|
||||
* @returns {ConditionExpressionGroup|null} A ConditionExpressionGroup with the Monitor's conditions
|
||||
*/
|
||||
static fromMonitor(monitor) {
|
||||
const conditions = JSON.parse(monitor.conditions);
|
||||
if (conditions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const root = new ConditionExpressionGroup();
|
||||
processMonitorConditions(conditions, root);
|
||||
|
||||
return root;
|
||||
}
|
||||
}
|
||||
|
||||
class ConditionExpression {
|
||||
/**
|
||||
* @type {string} ID of variable
|
||||
*/
|
||||
variable;
|
||||
|
||||
/**
|
||||
* @type {string} ID of operator
|
||||
*/
|
||||
operator;
|
||||
|
||||
/**
|
||||
* @type {string} Value to test with the operator
|
||||
*/
|
||||
value;
|
||||
|
||||
/**
|
||||
* @type {LOGICAL} Connects expression result with previous group/expression results
|
||||
*/
|
||||
andOr;
|
||||
|
||||
/**
|
||||
* @param {string} variable ID of variable to test against
|
||||
* @param {string} operator ID of operator to test the variable with
|
||||
* @param {string} value Value to test with the operator
|
||||
* @param {LOGICAL} andOr Connects expression result with previous group/expression results
|
||||
*/
|
||||
constructor(variable, operator, value, andOr = LOGICAL.AND) {
|
||||
this.variable = variable;
|
||||
this.operator = operator;
|
||||
this.value = value;
|
||||
this.andOr = andOr;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
LOGICAL,
|
||||
ConditionExpressionGroup,
|
||||
ConditionExpression,
|
||||
};
|
318
server/monitor-conditions/operators.js
Normal file
318
server/monitor-conditions/operators.js
Normal file
@@ -0,0 +1,318 @@
|
||||
class ConditionOperator {
|
||||
id = undefined;
|
||||
caption = undefined;
|
||||
|
||||
/**
|
||||
* @type {mixed} variable
|
||||
* @type {mixed} value
|
||||
*/
|
||||
test(variable, value) {
|
||||
throw new Error("You need to override test()");
|
||||
}
|
||||
}
|
||||
|
||||
const OP_STR_EQUALS = "equals";
|
||||
|
||||
const OP_STR_NOT_EQUALS = "not_equals";
|
||||
|
||||
const OP_CONTAINS = "contains";
|
||||
|
||||
const OP_NOT_CONTAINS = "not_contains";
|
||||
|
||||
const OP_STARTS_WITH = "starts_with";
|
||||
|
||||
const OP_NOT_STARTS_WITH = "not_starts_with";
|
||||
|
||||
const OP_ENDS_WITH = "ends_with";
|
||||
|
||||
const OP_NOT_ENDS_WITH = "not_ends_with";
|
||||
|
||||
const OP_NUM_EQUALS = "num_equals";
|
||||
|
||||
const OP_NUM_NOT_EQUALS = "num_not_equals";
|
||||
|
||||
const OP_LT = "lt";
|
||||
|
||||
const OP_GT = "gt";
|
||||
|
||||
const OP_LTE = "lte";
|
||||
|
||||
const OP_GTE = "gte";
|
||||
|
||||
/**
|
||||
* Asserts a variable is equal to a value.
|
||||
*/
|
||||
class StringEqualsOperator extends ConditionOperator {
|
||||
id = OP_STR_EQUALS;
|
||||
caption = "equals";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return variable === value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a variable is not equal to a value.
|
||||
*/
|
||||
class StringNotEqualsOperator extends ConditionOperator {
|
||||
id = OP_STR_NOT_EQUALS;
|
||||
caption = "not equals";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return variable !== value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a variable contains a value.
|
||||
* Handles both Array and String variable types.
|
||||
*/
|
||||
class ContainsOperator extends ConditionOperator {
|
||||
id = OP_CONTAINS;
|
||||
caption = "contains";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
if (Array.isArray(variable)) {
|
||||
return variable.includes(value);
|
||||
}
|
||||
|
||||
return variable.indexOf(value) !== -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a variable does not contain a value.
|
||||
* Handles both Array and String variable types.
|
||||
*/
|
||||
class NotContainsOperator extends ConditionOperator {
|
||||
id = OP_NOT_CONTAINS;
|
||||
caption = "not contains";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
if (Array.isArray(variable)) {
|
||||
return !variable.includes(value);
|
||||
}
|
||||
|
||||
return variable.indexOf(value) === -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a variable starts with a value.
|
||||
*/
|
||||
class StartsWithOperator extends ConditionOperator {
|
||||
id = OP_STARTS_WITH;
|
||||
caption = "starts with";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return variable.startsWith(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a variable does not start with a value.
|
||||
*/
|
||||
class NotStartsWithOperator extends ConditionOperator {
|
||||
id = OP_NOT_STARTS_WITH;
|
||||
caption = "not starts with";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return !variable.startsWith(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a variable ends with a value.
|
||||
*/
|
||||
class EndsWithOperator extends ConditionOperator {
|
||||
id = OP_ENDS_WITH;
|
||||
caption = "ends with";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return variable.endsWith(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a variable does not end with a value.
|
||||
*/
|
||||
class NotEndsWithOperator extends ConditionOperator {
|
||||
id = OP_NOT_ENDS_WITH;
|
||||
caption = "not ends with";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return !variable.endsWith(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a numeric variable is equal to a value.
|
||||
*/
|
||||
class NumberEqualsOperator extends ConditionOperator {
|
||||
id = OP_NUM_EQUALS;
|
||||
caption = "equals";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return variable === Number(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a numeric variable is not equal to a value.
|
||||
*/
|
||||
class NumberNotEqualsOperator extends ConditionOperator {
|
||||
id = OP_NUM_NOT_EQUALS;
|
||||
caption = "not equals";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return variable !== Number(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a variable is less than a value.
|
||||
*/
|
||||
class LessThanOperator extends ConditionOperator {
|
||||
id = OP_LT;
|
||||
caption = "less than";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return variable < Number(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a variable is greater than a value.
|
||||
*/
|
||||
class GreaterThanOperator extends ConditionOperator {
|
||||
id = OP_GT;
|
||||
caption = "greater than";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return variable > Number(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a variable is less than or equal to a value.
|
||||
*/
|
||||
class LessThanOrEqualToOperator extends ConditionOperator {
|
||||
id = OP_LTE;
|
||||
caption = "less than or equal to";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return variable <= Number(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a variable is greater than or equal to a value.
|
||||
*/
|
||||
class GreaterThanOrEqualToOperator extends ConditionOperator {
|
||||
id = OP_GTE;
|
||||
caption = "greater than or equal to";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
test(variable, value) {
|
||||
return variable >= Number(value);
|
||||
}
|
||||
}
|
||||
|
||||
const operatorMap = new Map([
|
||||
[ OP_STR_EQUALS, new StringEqualsOperator ],
|
||||
[ OP_STR_NOT_EQUALS, new StringNotEqualsOperator ],
|
||||
[ OP_CONTAINS, new ContainsOperator ],
|
||||
[ OP_NOT_CONTAINS, new NotContainsOperator ],
|
||||
[ OP_STARTS_WITH, new StartsWithOperator ],
|
||||
[ OP_NOT_STARTS_WITH, new NotStartsWithOperator ],
|
||||
[ OP_ENDS_WITH, new EndsWithOperator ],
|
||||
[ OP_NOT_ENDS_WITH, new NotEndsWithOperator ],
|
||||
[ OP_NUM_EQUALS, new NumberEqualsOperator ],
|
||||
[ OP_NUM_NOT_EQUALS, new NumberNotEqualsOperator ],
|
||||
[ OP_LT, new LessThanOperator ],
|
||||
[ OP_GT, new GreaterThanOperator ],
|
||||
[ OP_LTE, new LessThanOrEqualToOperator ],
|
||||
[ OP_GTE, new GreaterThanOrEqualToOperator ],
|
||||
]);
|
||||
|
||||
const defaultStringOperators = [
|
||||
operatorMap.get(OP_STR_EQUALS),
|
||||
operatorMap.get(OP_STR_NOT_EQUALS),
|
||||
operatorMap.get(OP_CONTAINS),
|
||||
operatorMap.get(OP_NOT_CONTAINS),
|
||||
operatorMap.get(OP_STARTS_WITH),
|
||||
operatorMap.get(OP_NOT_STARTS_WITH),
|
||||
operatorMap.get(OP_ENDS_WITH),
|
||||
operatorMap.get(OP_NOT_ENDS_WITH)
|
||||
];
|
||||
|
||||
const defaultNumberOperators = [
|
||||
operatorMap.get(OP_NUM_EQUALS),
|
||||
operatorMap.get(OP_NUM_NOT_EQUALS),
|
||||
operatorMap.get(OP_LT),
|
||||
operatorMap.get(OP_GT),
|
||||
operatorMap.get(OP_LTE),
|
||||
operatorMap.get(OP_GTE)
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
OP_STR_EQUALS,
|
||||
OP_STR_NOT_EQUALS,
|
||||
OP_CONTAINS,
|
||||
OP_NOT_CONTAINS,
|
||||
OP_STARTS_WITH,
|
||||
OP_NOT_STARTS_WITH,
|
||||
OP_ENDS_WITH,
|
||||
OP_NOT_ENDS_WITH,
|
||||
OP_NUM_EQUALS,
|
||||
OP_NUM_NOT_EQUALS,
|
||||
OP_LT,
|
||||
OP_GT,
|
||||
OP_LTE,
|
||||
OP_GTE,
|
||||
operatorMap,
|
||||
defaultStringOperators,
|
||||
defaultNumberOperators,
|
||||
ConditionOperator,
|
||||
};
|
31
server/monitor-conditions/variables.js
Normal file
31
server/monitor-conditions/variables.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Represents a variable used in a condition and the set of operators that can be applied to this variable.
|
||||
*
|
||||
* A `ConditionVariable` holds the ID of the variable and a list of operators that define how this variable can be evaluated
|
||||
* in conditions. For example, if the variable is a request body or a specific field in a request, the operators can include
|
||||
* operations such as equality checks, comparisons, or other custom evaluations.
|
||||
*/
|
||||
class ConditionVariable {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
id;
|
||||
|
||||
/**
|
||||
* @type {import("./operators").ConditionOperator[]}
|
||||
*/
|
||||
operators = {};
|
||||
|
||||
/**
|
||||
* @param {string} id ID of variable
|
||||
* @param {import("./operators").ConditionOperator[]} operators Operators the condition supports
|
||||
*/
|
||||
constructor(id, operators = []) {
|
||||
this.id = id;
|
||||
this.operators = operators;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ConditionVariable,
|
||||
};
|
85
server/monitor-types/dns.js
Normal file
85
server/monitor-types/dns.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const { UP, DOWN } = require("../../src/util");
|
||||
const dayjs = require("dayjs");
|
||||
const { dnsResolve } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const { ConditionVariable } = require("../monitor-conditions/variables");
|
||||
const { defaultStringOperators } = require("../monitor-conditions/operators");
|
||||
const { ConditionExpressionGroup } = require("../monitor-conditions/expression");
|
||||
const { evaluateExpressionGroup } = require("../monitor-conditions/evaluator");
|
||||
|
||||
class DnsMonitorType extends MonitorType {
|
||||
name = "dns";
|
||||
|
||||
supportsConditions = true;
|
||||
|
||||
conditionVariables = [
|
||||
new ConditionVariable("record", defaultStringOperators ),
|
||||
];
|
||||
|
||||
/**
|
||||
* @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;
|
||||
|
||||
const conditions = ConditionExpressionGroup.fromMonitor(monitor);
|
||||
let conditionsResult = true;
|
||||
const handleConditions = (data) => conditions ? evaluateExpressionGroup(conditions, data) : true;
|
||||
|
||||
switch (monitor.dns_resolve_type) {
|
||||
case "A":
|
||||
case "AAAA":
|
||||
case "TXT":
|
||||
case "PTR":
|
||||
dnsMessage = `Records: ${dnsRes.join(" | ")}`;
|
||||
conditionsResult = dnsRes.some(record => handleConditions({ record }));
|
||||
break;
|
||||
|
||||
case "CNAME":
|
||||
dnsMessage = dnsRes[0];
|
||||
conditionsResult = handleConditions({ record: dnsRes[0] });
|
||||
break;
|
||||
|
||||
case "CAA":
|
||||
dnsMessage = dnsRes[0].issue;
|
||||
conditionsResult = handleConditions({ record: dnsRes[0].issue });
|
||||
break;
|
||||
|
||||
case "MX":
|
||||
dnsMessage = dnsRes.map(record => `Hostname: ${record.exchange} - Priority: ${record.priority}`).join(" | ");
|
||||
conditionsResult = dnsRes.some(record => handleConditions({ record: record.exchange }));
|
||||
break;
|
||||
|
||||
case "NS":
|
||||
dnsMessage = `Servers: ${dnsRes.join(" | ")}`;
|
||||
conditionsResult = dnsRes.some(record => handleConditions({ record }));
|
||||
break;
|
||||
|
||||
case "SOA":
|
||||
dnsMessage = `NS-Name: ${dnsRes.nsname} | Hostmaster: ${dnsRes.hostmaster} | Serial: ${dnsRes.serial} | Refresh: ${dnsRes.refresh} | Retry: ${dnsRes.retry} | Expire: ${dnsRes.expire} | MinTTL: ${dnsRes.minttl}`;
|
||||
conditionsResult = handleConditions({ record: dnsRes.nsname });
|
||||
break;
|
||||
|
||||
case "SRV":
|
||||
dnsMessage = dnsRes.map(record => `Name: ${record.name} | Port: ${record.port} | Priority: ${record.priority} | Weight: ${record.weight}`).join(" | ");
|
||||
conditionsResult = dnsRes.some(record => handleConditions({ record: record.name }));
|
||||
break;
|
||||
}
|
||||
|
||||
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 = conditionsResult ? UP : DOWN;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DnsMonitorType,
|
||||
};
|
63
server/monitor-types/mongodb.js
Normal file
63
server/monitor-types/mongodb.js
Normal file
@@ -0,0 +1,63 @@
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const { UP } = require("../../src/util");
|
||||
const { MongoClient } = require("mongodb");
|
||||
const jsonata = require("jsonata");
|
||||
|
||||
class MongodbMonitorType extends MonitorType {
|
||||
name = "mongodb";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat, _server) {
|
||||
let command = { "ping": 1 };
|
||||
if (monitor.databaseQuery) {
|
||||
command = JSON.parse(monitor.databaseQuery);
|
||||
}
|
||||
|
||||
let result = await this.runMongodbCommand(monitor.databaseConnectionString, command);
|
||||
|
||||
if (result["ok"] !== 1) {
|
||||
throw new Error("MongoDB command failed");
|
||||
} else {
|
||||
heartbeat.msg = "Command executed successfully";
|
||||
}
|
||||
|
||||
if (monitor.jsonPath) {
|
||||
let expression = jsonata(monitor.jsonPath);
|
||||
result = await expression.evaluate(result);
|
||||
if (result) {
|
||||
heartbeat.msg = "Command executed successfully and the jsonata expression produces a result.";
|
||||
} else {
|
||||
throw new Error("Queried value not found.");
|
||||
}
|
||||
}
|
||||
|
||||
if (monitor.expectedValue) {
|
||||
if (result.toString() === monitor.expectedValue) {
|
||||
heartbeat.msg = "Command executed successfully and expected value was found";
|
||||
} else {
|
||||
throw new Error("Query executed, but value is not equal to expected value, value was: [" + JSON.stringify(result) + "]");
|
||||
}
|
||||
}
|
||||
|
||||
heartbeat.status = UP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to and run MongoDB command on a MongoDB database
|
||||
* @param {string} connectionString The database connection string
|
||||
* @param {object} command MongoDB command to run on the database
|
||||
* @returns {Promise<(string[] | object[] | object)>} Response from server
|
||||
*/
|
||||
async runMongodbCommand(connectionString, command) {
|
||||
let client = await MongoClient.connect(connectionString);
|
||||
let result = await client.db().command(command);
|
||||
await client.close();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MongodbMonitorType,
|
||||
};
|
@@ -1,18 +1,29 @@
|
||||
class MonitorType {
|
||||
|
||||
name = undefined;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Monitor} monitor
|
||||
* @param {Heartbeat} heartbeat
|
||||
* @param {UptimeKumaServer} server
|
||||
* Whether or not this type supports monitor conditions. Controls UI visibility in monitor form.
|
||||
* @type {boolean}
|
||||
*/
|
||||
supportsConditions = false;
|
||||
|
||||
/**
|
||||
* Variables supported by this type. e.g. an HTTP type could have a "response_code" variable to test against.
|
||||
* This property controls the choices displayed in the monitor edit form.
|
||||
* @type {import("../monitor-conditions/variables").ConditionVariable[]}
|
||||
*/
|
||||
conditionVariables = [];
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
throw new Error("You need to override check()");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
117
server/monitor-types/mqtt.js
Normal file
117
server/monitor-types/mqtt.js
Normal file
@@ -0,0 +1,117 @@
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const { log, UP } = require("../../src/util");
|
||||
const mqtt = require("mqtt");
|
||||
const jsonata = require("jsonata");
|
||||
|
||||
class MqttMonitorType extends MonitorType {
|
||||
name = "mqtt";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat, server) {
|
||||
const receivedMessage = await this.mqttAsync(monitor.hostname, monitor.mqttTopic, {
|
||||
port: monitor.port,
|
||||
username: monitor.mqttUsername,
|
||||
password: monitor.mqttPassword,
|
||||
interval: monitor.interval,
|
||||
});
|
||||
|
||||
if (monitor.mqttCheckType == null || monitor.mqttCheckType === "") {
|
||||
// use old default
|
||||
monitor.mqttCheckType = "keyword";
|
||||
}
|
||||
|
||||
if (monitor.mqttCheckType === "keyword") {
|
||||
if (receivedMessage != null && receivedMessage.includes(monitor.mqttSuccessMessage)) {
|
||||
heartbeat.msg = `Topic: ${monitor.mqttTopic}; Message: ${receivedMessage}`;
|
||||
heartbeat.status = UP;
|
||||
} else {
|
||||
throw Error(`Message Mismatch - Topic: ${monitor.mqttTopic}; Message: ${receivedMessage}`);
|
||||
}
|
||||
} else if (monitor.mqttCheckType === "json-query") {
|
||||
const parsedMessage = JSON.parse(receivedMessage);
|
||||
|
||||
let expression = jsonata(monitor.jsonPath);
|
||||
|
||||
let result = await expression.evaluate(parsedMessage);
|
||||
|
||||
if (result?.toString() === monitor.expectedValue) {
|
||||
heartbeat.msg = "Message received, expected value is found";
|
||||
heartbeat.status = UP;
|
||||
} else {
|
||||
throw new Error("Message received but value is not equal to expected value, value was: [" + result + "]");
|
||||
}
|
||||
} else {
|
||||
throw Error("Unknown MQTT Check Type");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to MQTT Broker, subscribe to topic and receive message as String
|
||||
* @param {string} hostname Hostname / address of machine to test
|
||||
* @param {string} topic MQTT topic
|
||||
* @param {object} options MQTT options. Contains port, username,
|
||||
* password and interval (interval defaults to 20)
|
||||
* @returns {Promise<string>} Received MQTT message
|
||||
*/
|
||||
mqttAsync(hostname, topic, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { port, username, password, interval = 20 } = options;
|
||||
|
||||
// Adds MQTT protocol to the hostname if not already present
|
||||
if (!/^(?:http|mqtt|ws)s?:\/\//.test(hostname)) {
|
||||
hostname = "mqtt://" + hostname;
|
||||
}
|
||||
|
||||
const timeoutID = setTimeout(() => {
|
||||
log.debug("mqtt", "MQTT timeout triggered");
|
||||
client.end();
|
||||
reject(new Error("Timeout, Message not received"));
|
||||
}, interval * 1000 * 0.8);
|
||||
|
||||
const mqttUrl = `${hostname}:${port}`;
|
||||
|
||||
log.debug("mqtt", `MQTT connecting to ${mqttUrl}`);
|
||||
|
||||
let client = mqtt.connect(mqttUrl, {
|
||||
username,
|
||||
password,
|
||||
clientId: "uptime-kuma_" + Math.random().toString(16).substr(2, 8)
|
||||
});
|
||||
|
||||
client.on("connect", () => {
|
||||
log.debug("mqtt", "MQTT connected");
|
||||
|
||||
try {
|
||||
client.subscribe(topic, () => {
|
||||
log.debug("mqtt", "MQTT subscribed to topic");
|
||||
});
|
||||
} catch (e) {
|
||||
client.end();
|
||||
clearTimeout(timeoutID);
|
||||
reject(new Error("Cannot subscribe topic"));
|
||||
}
|
||||
});
|
||||
|
||||
client.on("error", (error) => {
|
||||
client.end();
|
||||
clearTimeout(timeoutID);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
client.on("message", (messageTopic, message) => {
|
||||
if (messageTopic === topic) {
|
||||
client.end();
|
||||
clearTimeout(timeoutID);
|
||||
resolve(message.toString("utf8"));
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MqttMonitorType,
|
||||
};
|
67
server/monitor-types/rabbitmq.js
Normal file
67
server/monitor-types/rabbitmq.js
Normal file
@@ -0,0 +1,67 @@
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const { log, UP, DOWN } = require("../../src/util");
|
||||
const { axiosAbortSignal } = require("../util-server");
|
||||
const axios = require("axios");
|
||||
|
||||
class RabbitMqMonitorType extends MonitorType {
|
||||
name = "rabbitmq";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat, server) {
|
||||
let baseUrls = [];
|
||||
try {
|
||||
baseUrls = JSON.parse(monitor.rabbitmqNodes);
|
||||
} catch (error) {
|
||||
throw new Error("Invalid RabbitMQ Nodes");
|
||||
}
|
||||
|
||||
heartbeat.status = DOWN;
|
||||
for (let baseUrl of baseUrls) {
|
||||
try {
|
||||
// Without a trailing slash, path in baseUrl will be removed. https://example.com/api -> https://example.com
|
||||
if ( !baseUrl.endsWith("/") ) {
|
||||
baseUrl += "/";
|
||||
}
|
||||
const options = {
|
||||
// Do not start with slash, it will strip the trailing slash from baseUrl
|
||||
url: new URL("api/health/checks/alarms/", baseUrl).href,
|
||||
method: "get",
|
||||
timeout: monitor.timeout * 1000,
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Authorization": "Basic " + Buffer.from(`${monitor.rabbitmqUsername || ""}:${monitor.rabbitmqPassword || ""}`).toString("base64"),
|
||||
},
|
||||
signal: axiosAbortSignal((monitor.timeout + 10) * 1000),
|
||||
// Capture reason for 503 status
|
||||
validateStatus: (status) => status === 200 || status === 503,
|
||||
};
|
||||
log.debug("monitor", `[${monitor.name}] Axios Request: ${JSON.stringify(options)}`);
|
||||
const res = await axios.request(options);
|
||||
log.debug("monitor", `[${monitor.name}] Axios Response: status=${res.status} body=${JSON.stringify(res.data)}`);
|
||||
if (res.status === 200) {
|
||||
heartbeat.status = UP;
|
||||
heartbeat.msg = "OK";
|
||||
break;
|
||||
} else if (res.status === 503) {
|
||||
heartbeat.msg = res.data.reason;
|
||||
} else {
|
||||
heartbeat.msg = `${res.status} - ${res.statusText}`;
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isCancel(error)) {
|
||||
heartbeat.msg = "Request timed out";
|
||||
log.debug("monitor", `[${monitor.name}] Request timed out`);
|
||||
} else {
|
||||
log.debug("monitor", `[${monitor.name}] Axios Error: ${JSON.stringify(error.message)}`);
|
||||
heartbeat.msg = error.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RabbitMqMonitorType,
|
||||
};
|
@@ -8,6 +8,7 @@ const path = require("path");
|
||||
const Database = require("../database");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const config = require("../config");
|
||||
const { RemoteBrowser } = require("../remote-browser");
|
||||
|
||||
/**
|
||||
* Cached instance of a browser
|
||||
@@ -28,6 +29,9 @@ if (process.platform === "win32") {
|
||||
allowedList.push(process.env.PROGRAMFILES + "\\Chromium\\Application\\chrome.exe");
|
||||
allowedList.push(process.env["ProgramFiles(x86)"] + "\\Chromium\\Application\\chrome.exe");
|
||||
|
||||
// Allow MS Edge
|
||||
allowedList.push(process.env["ProgramFiles(x86)"] + "\\Microsoft\\Edge\\Application\\msedge.exe");
|
||||
|
||||
// For Loop A to Z
|
||||
for (let i = 65; i <= 90; i++) {
|
||||
let drive = String.fromCharCode(i);
|
||||
@@ -47,15 +51,17 @@ if (process.platform === "win32") {
|
||||
"/snap/bin/chromium", // Ubuntu
|
||||
];
|
||||
} else if (process.platform === "darwin") {
|
||||
// TODO: Generated by GitHub Copilot, but not sure if it's correct
|
||||
allowedList = [
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
||||
];
|
||||
}
|
||||
|
||||
log.debug("chrome", allowedList);
|
||||
|
||||
/**
|
||||
* 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") {
|
||||
@@ -88,6 +94,24 @@ async function getBrowser() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current instance of the browser. If there isn't one, create it
|
||||
* @param {integer} remoteBrowserID Path to executable
|
||||
* @param {integer} userId User ID
|
||||
* @returns {Promise<Browser>} The browser
|
||||
*/
|
||||
async function getRemoteBrowser(remoteBrowserID, userId) {
|
||||
let remoteBrowser = await RemoteBrowser.get(remoteBrowserID, userId);
|
||||
log.debug("MONITOR", `Using remote browser: ${remoteBrowser.name} (${remoteBrowser.id})`);
|
||||
browser = await chromium.connect(remoteBrowser.url);
|
||||
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") {
|
||||
@@ -134,6 +158,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) {
|
||||
@@ -151,6 +181,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();
|
||||
@@ -160,8 +194,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 {
|
||||
@@ -179,17 +213,30 @@ async function testChrome(executablePath) {
|
||||
throw new Error(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// test remote browser
|
||||
/**
|
||||
* TODO: connect remote browser? https://playwright.dev/docs/api/class-browsertype#browser-type-connect
|
||||
*
|
||||
* @param {string} remoteBrowserURL Remote Browser URL
|
||||
* @returns {Promise<boolean>} Returns if connection worked
|
||||
*/
|
||||
async function testRemoteBrowser(remoteBrowserURL) {
|
||||
try {
|
||||
const browser = await chromium.connect(remoteBrowserURL);
|
||||
browser.version();
|
||||
await browser.close();
|
||||
return true;
|
||||
} catch (e) {
|
||||
throw new Error(e.message);
|
||||
}
|
||||
}
|
||||
class RealBrowserMonitorType extends MonitorType {
|
||||
|
||||
name = "real-browser";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat, server) {
|
||||
const browser = await getBrowser();
|
||||
const browser = monitor.remote_browser ? await getRemoteBrowser(monitor.remote_browser, monitor.user_id) : await getBrowser();
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
@@ -230,4 +277,5 @@ module.exports = {
|
||||
RealBrowserMonitorType,
|
||||
testChrome,
|
||||
resetChrome,
|
||||
testRemoteBrowser,
|
||||
};
|
||||
|
63
server/monitor-types/snmp.js
Normal file
63
server/monitor-types/snmp.js
Normal file
@@ -0,0 +1,63 @@
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const { UP, log, evaluateJsonQuery } = require("../../src/util");
|
||||
const snmp = require("net-snmp");
|
||||
|
||||
class SNMPMonitorType extends MonitorType {
|
||||
name = "snmp";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat, _server) {
|
||||
let session;
|
||||
try {
|
||||
const sessionOptions = {
|
||||
port: monitor.port || "161",
|
||||
retries: monitor.maxretries,
|
||||
timeout: monitor.timeout * 1000,
|
||||
version: snmp.Version[monitor.snmpVersion],
|
||||
};
|
||||
session = snmp.createSession(monitor.hostname, monitor.radiusPassword, sessionOptions);
|
||||
|
||||
// Handle errors during session creation
|
||||
session.on("error", (error) => {
|
||||
throw new Error(`Error creating SNMP session: ${error.message}`);
|
||||
});
|
||||
|
||||
const varbinds = await new Promise((resolve, reject) => {
|
||||
session.get([ monitor.snmpOid ], (error, varbinds) => {
|
||||
error ? reject(error) : resolve(varbinds);
|
||||
});
|
||||
});
|
||||
log.debug("monitor", `SNMP: Received varbinds (Type: ${snmp.ObjectType[varbinds[0].type]} Value: ${varbinds[0].value})`);
|
||||
|
||||
if (varbinds.length === 0) {
|
||||
throw new Error(`No varbinds returned from SNMP session (OID: ${monitor.snmpOid})`);
|
||||
}
|
||||
|
||||
if (varbinds[0].type === snmp.ObjectType.NoSuchInstance) {
|
||||
throw new Error(`The SNMP query returned that no instance exists for OID ${monitor.snmpOid}`);
|
||||
}
|
||||
|
||||
// We restrict querying to one OID per monitor, therefore `varbinds[0]` will always contain the value we're interested in.
|
||||
const value = varbinds[0].value;
|
||||
|
||||
const { status, response } = await evaluateJsonQuery(value, monitor.jsonPath, monitor.jsonPathOperator, monitor.expectedValue);
|
||||
|
||||
if (status) {
|
||||
heartbeat.status = UP;
|
||||
heartbeat.msg = `JSON query passes (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`;
|
||||
} else {
|
||||
throw new Error(`JSON query does not pass (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`);
|
||||
}
|
||||
} finally {
|
||||
if (session) {
|
||||
session.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SNMPMonitorType,
|
||||
};
|
@@ -2,23 +2,13 @@ const { MonitorType } = require("./monitor-type");
|
||||
const { UP } = require("../../src/util");
|
||||
const childProcessAsync = require("promisify-child-process");
|
||||
|
||||
/**
|
||||
* A TailscalePing class extends the MonitorType.
|
||||
* It runs Tailscale ping to monitor the status of a specific node.
|
||||
*/
|
||||
class TailscalePing extends MonitorType {
|
||||
|
||||
name = "tailscale-ping";
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @throws Will throw an error if checking Tailscale ping encounters any error
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat) {
|
||||
async check(monitor, heartbeat, _server) {
|
||||
try {
|
||||
let tailscaleOutput = await this.runTailscalePing(monitor.hostname, monitor.interval);
|
||||
this.parseTailscaleOutput(tailscaleOutput, heartbeat);
|
||||
@@ -30,10 +20,9 @@ class TailscalePing extends MonitorType {
|
||||
|
||||
/**
|
||||
* Runs the Tailscale ping command to the given URL.
|
||||
*
|
||||
* @param {string} hostname - The hostname to ping.
|
||||
* @param {number} interval
|
||||
* @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) {
|
||||
@@ -54,9 +43,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) {
|
||||
|
35
server/notification-providers/46elks.js
Normal file
35
server/notification-providers/46elks.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Elks extends NotificationProvider {
|
||||
name = "Elks";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://api.46elks.com/a1/sms";
|
||||
|
||||
try {
|
||||
let data = new URLSearchParams();
|
||||
data.append("from", notification.elksFromNumber);
|
||||
data.append("to", notification.elksToNumber );
|
||||
data.append("message", msg);
|
||||
|
||||
const config = {
|
||||
headers: {
|
||||
"Authorization": "Basic " + Buffer.from(`${notification.elksUsername}:${notification.elksAuthToken}`).toString("base64")
|
||||
}
|
||||
};
|
||||
|
||||
await axios.post(url, data, config);
|
||||
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Elks;
|
@@ -3,14 +3,15 @@ const { DOWN, UP } = require("../../src/util");
|
||||
const axios = require("axios");
|
||||
|
||||
class Alerta extends NotificationProvider {
|
||||
|
||||
name = "alerta";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let alertaUrl = `${notification.alertaApiEndpoint}`;
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json;charset=UTF-8",
|
||||
@@ -37,7 +38,7 @@ class Alerta extends NotificationProvider {
|
||||
resource: "Message",
|
||||
}, data);
|
||||
|
||||
await axios.post(alertaUrl, postData, config);
|
||||
await axios.post(notification.alertaApiEndpoint, postData, config);
|
||||
} else {
|
||||
let datadup = Object.assign( {
|
||||
correlate: [ "service_up", "service_down" ],
|
||||
@@ -49,11 +50,11 @@ class Alerta extends NotificationProvider {
|
||||
if (heartbeatJSON["status"] === DOWN) {
|
||||
datadup.severity = notification.alertaAlertState; // critical
|
||||
datadup.text = "Service " + monitorJSON["type"] + " is down.";
|
||||
await axios.post(alertaUrl, datadup, config);
|
||||
await axios.post(notification.alertaApiEndpoint, datadup, config);
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
datadup.severity = notification.alertaRecoverState; // cleaned
|
||||
datadup.text = "Service " + monitorJSON["type"] + " is up.";
|
||||
await axios.post(alertaUrl, datadup, config);
|
||||
await axios.post(notification.alertaApiEndpoint, datadup, config);
|
||||
}
|
||||
}
|
||||
return okMsg;
|
||||
|
@@ -4,11 +4,14 @@ const { setting } = require("../util-server");
|
||||
const { getMonitorRelativeURL, UP, DOWN } = require("../../src/util");
|
||||
|
||||
class AlertNow extends NotificationProvider {
|
||||
|
||||
name = "AlertNow";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let textMsg = "";
|
||||
let status = "open";
|
||||
|
@@ -7,8 +7,11 @@ const qs = require("qs");
|
||||
class AliyunSMS extends NotificationProvider {
|
||||
name = "AliyunSMS";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
if (heartbeatJSON != null) {
|
||||
@@ -41,7 +44,7 @@ class AliyunSMS extends NotificationProvider {
|
||||
* Send the SMS notification
|
||||
* @param {BeanModel} notification Notification details
|
||||
* @param {string} msgbody Message template
|
||||
* @returns {boolean} True if successful else false
|
||||
* @returns {Promise<boolean>} True if successful else false
|
||||
*/
|
||||
async sendSms(notification, msgbody) {
|
||||
let params = {
|
||||
@@ -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) {
|
||||
|
@@ -2,10 +2,14 @@ const NotificationProvider = require("./notification-provider");
|
||||
const childProcessAsync = require("promisify-child-process");
|
||||
|
||||
class Apprise extends NotificationProvider {
|
||||
|
||||
name = "apprise";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
const args = [ "-vv", "-b", msg, notification.appriseURL ];
|
||||
if (notification.title) {
|
||||
args.push("-t");
|
||||
@@ -20,7 +24,7 @@ class Apprise extends NotificationProvider {
|
||||
if (output) {
|
||||
|
||||
if (! output.includes("ERROR")) {
|
||||
return "Sent Successfully";
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
throw new Error(output);
|
||||
|
@@ -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,34 +46,35 @@ class Bark extends NotificationProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add additional parameter for better on device styles (iOS 15
|
||||
* optimized)
|
||||
* @param {string} postUrl URL to append parameters to
|
||||
* @returns {string}
|
||||
* Add additional parameter for Bark v1 endpoints.
|
||||
* Leads to better on device styles (iOS 15 optimized)
|
||||
* @param {BeanModel} notification Notification to send
|
||||
* @returns {string} Additional URL parameters
|
||||
*/
|
||||
appendAdditionalParameters(notification, postUrl) {
|
||||
additionalParameters(notification) {
|
||||
// set icon to uptime kuma icon, 11kb should be fine
|
||||
postUrl += "?icon=" + barkNotificationAvatar;
|
||||
let params = "?icon=" + barkNotificationAvatar;
|
||||
// grouping all our notifications
|
||||
if (notification.barkGroup != null) {
|
||||
postUrl += "&group=" + notification.barkGroup;
|
||||
params += "&group=" + notification.barkGroup;
|
||||
} else {
|
||||
// default name
|
||||
postUrl += "&group=" + "UptimeKuma";
|
||||
params += "&group=" + "UptimeKuma";
|
||||
}
|
||||
// picked a sound, this should follow system's mute status when arrival
|
||||
if (notification.barkSound != null) {
|
||||
postUrl += "&sound=" + notification.barkSound;
|
||||
params += "&sound=" + notification.barkSound;
|
||||
} else {
|
||||
// default sound
|
||||
postUrl += "&sound=" + "telegraph";
|
||||
params += "&sound=" + "telegraph";
|
||||
}
|
||||
return postUrl;
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,29 @@ 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 {Promise<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);
|
||||
const params = this.additionalParameters(notification);
|
||||
result = await axios.get(`${endpoint}/${title}/${subtitle}${params}`);
|
||||
} 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;
|
||||
|
31
server/notification-providers/bitrix24.js
Normal file
31
server/notification-providers/bitrix24.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { UP } = require("../../src/util");
|
||||
|
||||
class Bitrix24 extends NotificationProvider {
|
||||
name = "Bitrix24";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
const params = {
|
||||
user_id: notification.bitrix24UserID,
|
||||
message: "[B]Uptime Kuma[/B]",
|
||||
"ATTACH[COLOR]": (heartbeatJSON ?? {})["status"] === UP ? "#b73419" : "#67b518",
|
||||
"ATTACH[BLOCKS][0][MESSAGE]": msg
|
||||
};
|
||||
|
||||
await axios.get(`${notification.bitrix24WebhookURL}/im.notify.system.add.json`, { params });
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Bitrix24;
|
23
server/notification-providers/call-me-bot.js
Normal file
23
server/notification-providers/call-me-bot.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class CallMeBot extends NotificationProvider {
|
||||
name = "CallMeBot";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
try {
|
||||
const url = new URL(notification.callMeBotEndpoint);
|
||||
url.searchParams.set("text", msg);
|
||||
await axios.get(url.toString());
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CallMeBot;
|
39
server/notification-providers/cellsynt.js
Normal file
39
server/notification-providers/cellsynt.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Cellsynt extends NotificationProvider {
|
||||
name = "Cellsynt";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
const data = {
|
||||
// docs at https://www.cellsynt.com/en/sms/api-integration
|
||||
params: {
|
||||
"username": notification.cellsyntLogin,
|
||||
"password": notification.cellsyntPassword,
|
||||
"destination": notification.cellsyntDestination,
|
||||
"text": msg.replace(/[^\x00-\x7F]/g, ""),
|
||||
"originatortype": notification.cellsyntOriginatortype,
|
||||
"originator": notification.cellsyntOriginator,
|
||||
"allowconcat": notification.cellsyntAllowLongSMS ? 6 : 1
|
||||
}
|
||||
};
|
||||
try {
|
||||
const resp = await axios.post("https://se-1.cellsynt.net/sms.php", null, data);
|
||||
if (resp.data == null ) {
|
||||
throw new Error("Could not connect to Cellsynt, please try again.");
|
||||
} else if (resp.data.includes("Error:")) {
|
||||
resp.data = resp.data.replaceAll("Error:", "");
|
||||
throw new Error(resp.data);
|
||||
}
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Cellsynt;
|
@@ -2,11 +2,15 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class ClickSendSMS extends NotificationProvider {
|
||||
|
||||
name = "clicksendsms";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://rest.clicksend.com/v3/sms/send";
|
||||
|
||||
try {
|
||||
let config = {
|
||||
headers: {
|
||||
@@ -25,7 +29,7 @@ class ClickSendSMS extends NotificationProvider {
|
||||
}
|
||||
]
|
||||
};
|
||||
let resp = await axios.post("https://rest.clicksend.com/v3/sms/send", data, config);
|
||||
let resp = await axios.post(url, data, config);
|
||||
if (resp.data.data.messages[0].status !== "SUCCESS") {
|
||||
let error = "Something gone wrong. Api returned " + resp.data.data.messages[0].status + ".";
|
||||
this.throwGeneralAxiosError(error);
|
||||
|
@@ -6,8 +6,11 @@ const Crypto = require("crypto");
|
||||
class DingDing extends NotificationProvider {
|
||||
name = "DingDing";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
if (heartbeatJSON != null) {
|
||||
@@ -16,6 +19,9 @@ class DingDing extends NotificationProvider {
|
||||
markdown: {
|
||||
title: `[${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]}`,
|
||||
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n> ${heartbeatJSON["msg"]}\n> Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
||||
},
|
||||
"at": {
|
||||
"isAtAll": notification.mentioning === "everyone"
|
||||
}
|
||||
};
|
||||
if (await this.sendToDingDing(notification, params)) {
|
||||
@@ -39,9 +45,9 @@ class DingDing extends NotificationProvider {
|
||||
|
||||
/**
|
||||
* Send message to DingDing
|
||||
* @param {BeanModel} notification
|
||||
* @param {Object} params Parameters of message
|
||||
* @returns {boolean} True if successful else false
|
||||
* @param {BeanModel} notification Notification to send
|
||||
* @param {object} params Parameters of message
|
||||
* @returns {Promise<boolean>} True if successful else false
|
||||
*/
|
||||
async sendToDingDing(notification, params) {
|
||||
let timestamp = Date.now();
|
||||
@@ -66,7 +72,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,10 +84,9 @@ 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
|
||||
switch (status) {
|
||||
case DOWN:
|
||||
return "DOWN";
|
||||
|
@@ -3,14 +3,20 @@ const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class Discord extends NotificationProvider {
|
||||
|
||||
name = "discord";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
const discordDisplayName = notification.discordUsername || "Uptime Kuma";
|
||||
const webhookUrl = new URL(notification.discordWebhookUrl);
|
||||
if (notification.discordChannelType === "postToThread") {
|
||||
webhookUrl.searchParams.append("thread_id", notification.threadId);
|
||||
}
|
||||
|
||||
// If heartbeatJSON is null, assume we're testing.
|
||||
if (heartbeatJSON == null) {
|
||||
@@ -18,30 +24,15 @@ class Discord extends NotificationProvider {
|
||||
username: discordDisplayName,
|
||||
content: msg,
|
||||
};
|
||||
await axios.post(notification.discordWebhookUrl, discordtestdata);
|
||||
|
||||
if (notification.discordChannelType === "createNewForumPost") {
|
||||
discordtestdata.thread_name = notification.postName;
|
||||
}
|
||||
|
||||
await axios.post(webhookUrl.toString(), discordtestdata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
let address;
|
||||
|
||||
switch (monitorJSON["type"]) {
|
||||
case "ping":
|
||||
address = monitorJSON["hostname"];
|
||||
break;
|
||||
case "port":
|
||||
case "dns":
|
||||
case "gamedig":
|
||||
case "steam":
|
||||
address = monitorJSON["hostname"];
|
||||
if (monitorJSON["port"]) {
|
||||
address += ":" + monitorJSON["port"];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
address = monitorJSON["url"];
|
||||
break;
|
||||
}
|
||||
|
||||
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
||||
if (heartbeatJSON["status"] === DOWN) {
|
||||
let discorddowndata = {
|
||||
@@ -57,7 +48,7 @@ class Discord extends NotificationProvider {
|
||||
},
|
||||
{
|
||||
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
||||
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
|
||||
value: this.extractAddress(monitorJSON),
|
||||
},
|
||||
{
|
||||
name: `Time (${heartbeatJSON["timezone"]})`,
|
||||
@@ -70,12 +61,14 @@ class Discord extends NotificationProvider {
|
||||
],
|
||||
}],
|
||||
};
|
||||
|
||||
if (notification.discordChannelType === "createNewForumPost") {
|
||||
discorddowndata.thread_name = notification.postName;
|
||||
}
|
||||
if (notification.discordPrefixMessage) {
|
||||
discorddowndata.content = notification.discordPrefixMessage;
|
||||
}
|
||||
|
||||
await axios.post(notification.discordWebhookUrl, discorddowndata);
|
||||
await axios.post(webhookUrl.toString(), discorddowndata);
|
||||
return okMsg;
|
||||
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
@@ -92,7 +85,7 @@ class Discord extends NotificationProvider {
|
||||
},
|
||||
{
|
||||
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
|
||||
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
|
||||
value: this.extractAddress(monitorJSON),
|
||||
},
|
||||
{
|
||||
name: `Time (${heartbeatJSON["timezone"]})`,
|
||||
@@ -106,11 +99,15 @@ class Discord extends NotificationProvider {
|
||||
}],
|
||||
};
|
||||
|
||||
if (notification.discordChannelType === "createNewForumPost") {
|
||||
discordupdata.thread_name = notification.postName;
|
||||
}
|
||||
|
||||
if (notification.discordPrefixMessage) {
|
||||
discordupdata.content = notification.discordPrefixMessage;
|
||||
}
|
||||
|
||||
await axios.post(notification.discordWebhookUrl, discordupdata);
|
||||
await axios.post(webhookUrl.toString(), discordupdata);
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
|
@@ -5,9 +5,11 @@ 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;
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
@@ -17,59 +19,67 @@ class Feishu extends NotificationProvider {
|
||||
text: msg,
|
||||
},
|
||||
};
|
||||
await axios.post(feishuWebHookUrl, testdata);
|
||||
await axios.post(notification.feishuWebHookUrl, testdata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
if (heartbeatJSON["status"] === DOWN) {
|
||||
let downdata = {
|
||||
msg_type: "post",
|
||||
content: {
|
||||
post: {
|
||||
zh_cn: {
|
||||
title: "UptimeKuma Alert: [Down] " + monitorJSON["name"],
|
||||
content: [
|
||||
[
|
||||
{
|
||||
tag: "text",
|
||||
text:
|
||||
"[Down] " +
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
msg_type: "interactive",
|
||||
card: {
|
||||
config: {
|
||||
update_multi: false,
|
||||
wide_screen_mode: true,
|
||||
},
|
||||
},
|
||||
header: {
|
||||
title: {
|
||||
tag: "plain_text",
|
||||
content: "UptimeKuma Alert: [Down] " + monitorJSON["name"],
|
||||
},
|
||||
template: "red",
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
tag: "div",
|
||||
text: {
|
||||
tag: "lark_md",
|
||||
content: getContent(heartbeatJSON),
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
await axios.post(feishuWebHookUrl, downdata);
|
||||
await axios.post(notification.feishuWebHookUrl, downdata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
if (heartbeatJSON["status"] === UP) {
|
||||
let updata = {
|
||||
msg_type: "post",
|
||||
content: {
|
||||
post: {
|
||||
zh_cn: {
|
||||
title: "UptimeKuma Alert: [Up] " + monitorJSON["name"],
|
||||
content: [
|
||||
[
|
||||
{
|
||||
tag: "text",
|
||||
text:
|
||||
"[Up] " +
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
msg_type: "interactive",
|
||||
card: {
|
||||
config: {
|
||||
update_multi: false,
|
||||
wide_screen_mode: true,
|
||||
},
|
||||
},
|
||||
header: {
|
||||
title: {
|
||||
tag: "plain_text",
|
||||
content: "UptimeKuma Alert: [UP] " + monitorJSON["name"],
|
||||
},
|
||||
template: "green",
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
tag: "div",
|
||||
text: {
|
||||
tag: "lark_md",
|
||||
content: getContent(heartbeatJSON),
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
};
|
||||
await axios.post(feishuWebHookUrl, updata);
|
||||
await axios.post(notification.feishuWebHookUrl, updata);
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -78,4 +88,17 @@ class Feishu extends NotificationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content
|
||||
* @param {?object} heartbeatJSON Heartbeat details (For Up/Down only)
|
||||
* @returns {string} Return Successful Message
|
||||
*/
|
||||
function getContent(heartbeatJSON) {
|
||||
return [
|
||||
"**Message**: " + heartbeatJSON["msg"],
|
||||
"**Ping**: " + (heartbeatJSON["ping"] == null ? "N/A" : heartbeatJSON["ping"] + " ms"),
|
||||
`**Time (${heartbeatJSON["timezone"]})**: ${heartbeatJSON["localDateTime"]}`
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
module.exports = Feishu;
|
||||
|
@@ -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,11 +57,20 @@ 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) {
|
||||
let labels = {
|
||||
resource: this.genMonitorUrl(monitorInfo),
|
||||
check: monitorInfo.name,
|
||||
};
|
||||
if (monitorInfo.tags && monitorInfo.tags.length > 0) {
|
||||
for (let tag of monitorInfo.tags) {
|
||||
labels[tag.name] = tag.value;
|
||||
}
|
||||
}
|
||||
const options = {
|
||||
method: "POST",
|
||||
url: "https://api.flashcat.cloud/event/push/alert/standard?integration_key=" + notification.flashdutyIntegrationKey,
|
||||
@@ -68,9 +80,7 @@ class FlashDuty extends NotificationProvider {
|
||||
title,
|
||||
event_status: eventStatus || "Info",
|
||||
alert_key: String(monitorInfo.id) || Math.random().toString(36).substring(7),
|
||||
labels: monitorInfo?.tags?.reduce((acc, item) => ({ ...acc,
|
||||
[item.name]: item.value
|
||||
}), { resource: this.genMonitorUrl(monitorInfo) }),
|
||||
labels,
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -2,11 +2,14 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class FreeMobile extends NotificationProvider {
|
||||
|
||||
name = "FreeMobile";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
await axios.post(`https://smsapi.free-mobile.fr/sendmsg?msg=${encodeURIComponent(msg.replace("🔴", "⛔️"))}`, {
|
||||
"user": notification.freemobileUser,
|
||||
|
@@ -3,18 +3,20 @@ const axios = require("axios");
|
||||
const { UP } = require("../../src/util");
|
||||
|
||||
class GoAlert extends NotificationProvider {
|
||||
|
||||
name = "GoAlert";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let closeAction = "close";
|
||||
let data = {
|
||||
summary: msg,
|
||||
};
|
||||
if (heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
||||
data["action"] = closeAction;
|
||||
data["action"] = "close";
|
||||
}
|
||||
let headers = {
|
||||
"Content-Type": "multipart/form-data",
|
||||
@@ -24,7 +26,6 @@ class GoAlert extends NotificationProvider {
|
||||
};
|
||||
await axios.post(`${notification.goAlertBaseURL}/api/v2/generic/incoming?token=${notification.goAlertToken}`, data, config);
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
let msg = (error.response.data) ? error.response.data : "Error without response";
|
||||
throw new Error(msg);
|
||||
|
@@ -1,38 +1,85 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { setting } = require("../util-server");
|
||||
const { getMonitorRelativeURL } = require("../../src/util");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
const { getMonitorRelativeURL, UP } = require("../../src/util");
|
||||
|
||||
class GoogleChat extends NotificationProvider {
|
||||
|
||||
name = "GoogleChat";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
// Google Chat message formatting: https://developers.google.com/chat/api/guides/message-formats/basic
|
||||
|
||||
let textMsg = "";
|
||||
if (heartbeatJSON && heartbeatJSON.status === UP) {
|
||||
textMsg = "✅ Application is back online\n";
|
||||
} else if (heartbeatJSON && heartbeatJSON.status === DOWN) {
|
||||
textMsg = "🔴 Application went down\n";
|
||||
let chatHeader = {
|
||||
title: "Uptime Kuma Alert",
|
||||
};
|
||||
|
||||
if (monitorJSON && heartbeatJSON) {
|
||||
chatHeader["title"] =
|
||||
heartbeatJSON["status"] === UP
|
||||
? `✅ ${monitorJSON["name"]} is back online`
|
||||
: `🔴 ${monitorJSON["name"]} went down`;
|
||||
}
|
||||
|
||||
if (monitorJSON && monitorJSON.name) {
|
||||
textMsg += `*${monitorJSON.name}*\n`;
|
||||
// always show msg
|
||||
let sectionWidgets = [
|
||||
{
|
||||
textParagraph: {
|
||||
text: `<b>Message:</b>\n${msg}`,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// add time if available
|
||||
if (heartbeatJSON) {
|
||||
sectionWidgets.push({
|
||||
textParagraph: {
|
||||
text: `<b>Time (${heartbeatJSON["timezone"]}):</b>\n${heartbeatJSON["localDateTime"]}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
textMsg += `${msg}`;
|
||||
|
||||
// add button for monitor link if available
|
||||
const baseURL = await setting("primaryBaseURL");
|
||||
if (baseURL && monitorJSON) {
|
||||
textMsg += `\n${baseURL + getMonitorRelativeURL(monitorJSON.id)}`;
|
||||
if (baseURL) {
|
||||
const urlPath = monitorJSON ? getMonitorRelativeURL(monitorJSON.id) : "/";
|
||||
sectionWidgets.push({
|
||||
buttonList: {
|
||||
buttons: [
|
||||
{
|
||||
text: "Visit Uptime Kuma",
|
||||
onClick: {
|
||||
openLink: {
|
||||
url: baseURL + urlPath,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const data = {
|
||||
"text": textMsg,
|
||||
let chatSections = [
|
||||
{
|
||||
widgets: sectionWidgets,
|
||||
},
|
||||
];
|
||||
|
||||
// construct json data
|
||||
let data = {
|
||||
cardsV2: [
|
||||
{
|
||||
card: {
|
||||
header: chatHeader,
|
||||
sections: chatSections,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await axios.post(notification.googleChatWebhookURL, data);
|
||||
|
@@ -2,11 +2,13 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Gorush extends NotificationProvider {
|
||||
|
||||
name = "gorush";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
let platformMapping = {
|
||||
"ios": 1,
|
||||
|
@@ -2,11 +2,14 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Gotify extends NotificationProvider {
|
||||
|
||||
name = "gotify";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
if (notification.gotifyserverurl && notification.gotifyserverurl.endsWith("/")) {
|
||||
notification.gotifyserverurl = notification.gotifyserverurl.slice(0, -1);
|
||||
|
51
server/notification-providers/grafana-oncall.js
Normal file
51
server/notification-providers/grafana-oncall.js
Normal file
@@ -0,0 +1,51 @@
|
||||
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) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
if (!notification.GrafanaOncallURL) {
|
||||
throw new Error("GrafanaOncallURL cannot be empty");
|
||||
}
|
||||
|
||||
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;
|
33
server/notification-providers/gtx-messaging.js
Normal file
33
server/notification-providers/gtx-messaging.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class GtxMessaging extends NotificationProvider {
|
||||
name = "gtxmessaging";
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
// The UP/DOWN symbols will be replaced with `???` by gtx-messaging
|
||||
const text = msg.replaceAll("🔴 ", "").replaceAll("✅ ", "");
|
||||
|
||||
try {
|
||||
const data = new URLSearchParams();
|
||||
data.append("from", notification.gtxMessagingFrom.trim());
|
||||
data.append("to", notification.gtxMessagingTo.trim());
|
||||
data.append("text", text);
|
||||
|
||||
const url = `https://rest.gtx-messaging.net/smsc/sendsms/${notification.gtxMessagingApiKey}/json`;
|
||||
|
||||
await axios.post(url, data);
|
||||
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GtxMessaging;
|
52
server/notification-providers/heii-oncall.js
Normal file
52
server/notification-providers/heii-oncall.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
|
||||
const { setting } = require("../util-server");
|
||||
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
class HeiiOnCall extends NotificationProvider {
|
||||
name = "HeiiOnCall";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
const payload = heartbeatJSON || {};
|
||||
|
||||
const baseURL = await setting("primaryBaseURL");
|
||||
if (baseURL && monitorJSON) {
|
||||
payload["url"] = baseURL + getMonitorRelativeURL(monitorJSON.id);
|
||||
}
|
||||
|
||||
const config = {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + notification.heiiOnCallApiKey,
|
||||
},
|
||||
};
|
||||
const heiiUrl = `https://heiioncall.com/triggers/${notification.heiiOnCallTriggerId}/`;
|
||||
// docs https://heiioncall.com/docs#manual-triggers
|
||||
try {
|
||||
if (!heartbeatJSON) {
|
||||
// Testing or general notification like certificate expiry
|
||||
payload["msg"] = msg;
|
||||
await axios.post(heiiUrl + "alert", payload, config);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
if (heartbeatJSON.status === DOWN) {
|
||||
await axios.post(heiiUrl + "alert", payload, config);
|
||||
return okMsg;
|
||||
}
|
||||
if (heartbeatJSON.status === UP) {
|
||||
await axios.post(heiiUrl + "resolve", payload, config);
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HeiiOnCall;
|
@@ -6,7 +6,12 @@ const defaultNotificationService = "notify";
|
||||
class HomeAssistant extends NotificationProvider {
|
||||
name = "HomeAssistant";
|
||||
|
||||
async send(notification, message, monitor = null, heartbeat = null) {
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
const notificationService = notification?.notificationService || defaultNotificationService;
|
||||
|
||||
try {
|
||||
@@ -14,10 +19,12 @@ class HomeAssistant extends NotificationProvider {
|
||||
`${notification.homeAssistantUrl.trim().replace(/\/*$/, "")}/api/services/notify/${notificationService}`,
|
||||
{
|
||||
title: "Uptime Kuma",
|
||||
message,
|
||||
message: msg,
|
||||
...(notificationService !== "persistent_notification" && { data: {
|
||||
name: monitor?.name,
|
||||
status: heartbeat?.status,
|
||||
name: monitorJSON?.name,
|
||||
status: heartbeatJSON?.status,
|
||||
channel: "Uptime Kuma",
|
||||
icon_url: "https://github.com/louislam/uptime-kuma/blob/master/public/icon.png?raw=true",
|
||||
} }),
|
||||
},
|
||||
{
|
||||
@@ -28,7 +35,7 @@ class HomeAssistant extends NotificationProvider {
|
||||
}
|
||||
);
|
||||
|
||||
return "Sent Successfully.";
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
42
server/notification-providers/keep.js
Normal file
42
server/notification-providers/keep.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Keep extends NotificationProvider {
|
||||
name = "Keep";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let data = {
|
||||
heartbeat: heartbeatJSON,
|
||||
monitor: monitorJSON,
|
||||
msg,
|
||||
};
|
||||
let config = {
|
||||
headers: {
|
||||
"x-api-key": notification.webhookAPIKey,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
let url = notification.webhookURL;
|
||||
|
||||
if (url.endsWith("/")) {
|
||||
url = url.slice(0, -1);
|
||||
}
|
||||
|
||||
let webhookURL = url + "/alerts/event/uptimekuma";
|
||||
|
||||
await axios.post(webhookURL, data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Keep;
|
@@ -2,12 +2,15 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
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";
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://www.kookapp.cn/api/v3/message/create";
|
||||
|
||||
let data = {
|
||||
target_id: notification.kookGuildID,
|
||||
content: msg,
|
||||
|
@@ -3,13 +3,16 @@ const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class Line extends NotificationProvider {
|
||||
|
||||
name = "line";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://api.line.me/v2/bot/message/push";
|
||||
|
||||
try {
|
||||
let lineAPIUrl = "https://api.line.me/v2/bot/message/push";
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -26,7 +29,7 @@ class Line extends NotificationProvider {
|
||||
}
|
||||
]
|
||||
};
|
||||
await axios.post(lineAPIUrl, testMessage, config);
|
||||
await axios.post(url, testMessage, config);
|
||||
} else if (heartbeatJSON["status"] === DOWN) {
|
||||
let downMessage = {
|
||||
"to": notification.lineUserID,
|
||||
@@ -40,7 +43,7 @@ class Line extends NotificationProvider {
|
||||
}
|
||||
]
|
||||
};
|
||||
await axios.post(lineAPIUrl, downMessage, config);
|
||||
await axios.post(url, downMessage, config);
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
let upMessage = {
|
||||
"to": notification.lineUserID,
|
||||
@@ -54,7 +57,7 @@ class Line extends NotificationProvider {
|
||||
}
|
||||
]
|
||||
};
|
||||
await axios.post(lineAPIUrl, upMessage, config);
|
||||
await axios.post(url, upMessage, config);
|
||||
}
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
|
@@ -4,13 +4,16 @@ const qs = require("qs");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class LineNotify extends NotificationProvider {
|
||||
|
||||
name = "LineNotify";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://notify-api.line.me/api/notify";
|
||||
|
||||
try {
|
||||
let lineAPIUrl = "https://notify-api.line.me/api/notify";
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
@@ -21,7 +24,7 @@ class LineNotify extends NotificationProvider {
|
||||
let testMessage = {
|
||||
"message": msg,
|
||||
};
|
||||
await axios.post(lineAPIUrl, qs.stringify(testMessage), config);
|
||||
await axios.post(url, qs.stringify(testMessage), config);
|
||||
} else if (heartbeatJSON["status"] === DOWN) {
|
||||
let downMessage = {
|
||||
"message": "\n[🔴 Down]\n" +
|
||||
@@ -29,7 +32,7 @@ class LineNotify extends NotificationProvider {
|
||||
heartbeatJSON["msg"] + "\n" +
|
||||
`Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
};
|
||||
await axios.post(lineAPIUrl, qs.stringify(downMessage), config);
|
||||
await axios.post(url, qs.stringify(downMessage), config);
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
let upMessage = {
|
||||
"message": "\n[✅ Up]\n" +
|
||||
@@ -37,7 +40,7 @@ class LineNotify extends NotificationProvider {
|
||||
heartbeatJSON["msg"] + "\n" +
|
||||
`Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
};
|
||||
await axios.post(lineAPIUrl, qs.stringify(upMessage), config);
|
||||
await axios.post(url, qs.stringify(upMessage), config);
|
||||
}
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
|
@@ -3,25 +3,23 @@ const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class LunaSea extends NotificationProvider {
|
||||
|
||||
name = "lunasea";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
let lunaseaurl = "";
|
||||
if (notification.lunaseaTarget === "user") {
|
||||
lunaseaurl = "https://notify.lunasea.app/v1/custom/user/" + notification.lunaseaUserID;
|
||||
} else {
|
||||
lunaseaurl = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice;
|
||||
}
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://notify.lunasea.app/v1";
|
||||
|
||||
try {
|
||||
const target = this.getTarget(notification);
|
||||
if (heartbeatJSON == null) {
|
||||
let testdata = {
|
||||
"title": "Uptime Kuma Alert",
|
||||
"body": msg,
|
||||
};
|
||||
await axios.post(lunaseaurl, testdata);
|
||||
await axios.post(`${url}/custom/${target}`, testdata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
@@ -32,7 +30,7 @@ class LunaSea extends NotificationProvider {
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
};
|
||||
await axios.post(lunaseaurl, downdata);
|
||||
await axios.post(`${url}/custom/${target}`, downdata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
@@ -43,13 +41,25 @@ class LunaSea extends NotificationProvider {
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
};
|
||||
await axios.post(lunaseaurl, updata);
|
||||
await axios.post(`${url}/custom/${target}`, updata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the lunasea target to send the notification to
|
||||
* @param {BeanModel} notification Notification details
|
||||
* @returns {string} The target to send the notification to
|
||||
*/
|
||||
getTarget(notification) {
|
||||
if (notification.lunaseaTarget === "user") {
|
||||
return "user/" + notification.lunaseaUserID;
|
||||
}
|
||||
return "device/" + notification.lunaseaDevice;
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -6,8 +6,11 @@ const { log } = require("../../src/util");
|
||||
class Matrix extends NotificationProvider {
|
||||
name = "matrix";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
const size = 20;
|
||||
const randomString = encodeURIComponent(
|
||||
|
@@ -3,11 +3,14 @@ const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class Mattermost extends NotificationProvider {
|
||||
|
||||
name = "mattermost";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
const mattermostUserName = notification.mattermostusername || "Uptime Kuma";
|
||||
// If heartbeatJSON is null, assume non monitoring notification (Certificate warning) or testing.
|
||||
@@ -75,12 +78,12 @@ class Mattermost extends NotificationProvider {
|
||||
{
|
||||
fallback:
|
||||
"Your " +
|
||||
monitorJSON.name +
|
||||
monitorJSON.pathName +
|
||||
" service went " +
|
||||
statusText,
|
||||
color: color,
|
||||
title:
|
||||
monitorJSON.name +
|
||||
monitorJSON.pathName +
|
||||
" service went " +
|
||||
statusText,
|
||||
title_link: monitorJSON.url,
|
||||
@@ -95,10 +98,7 @@ class Mattermost extends NotificationProvider {
|
||||
},
|
||||
],
|
||||
};
|
||||
await axios.post(
|
||||
notification.mattermostWebhookUrl,
|
||||
mattermostdata
|
||||
);
|
||||
await axios.post(notification.mattermostWebhookUrl, mattermostdata);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
|
@@ -1,4 +1,3 @@
|
||||
const { log } = require("../../src/util");
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const {
|
||||
relayInit,
|
||||
@@ -12,16 +11,7 @@ const {
|
||||
// polyfills for node versions
|
||||
const semver = require("semver");
|
||||
const nodeVersion = process.version;
|
||||
if (semver.lt(nodeVersion, "16.0.0")) {
|
||||
log.warn("monitor", "Node <= 16 is unsupported for nostr, sorry :(");
|
||||
} else if (semver.lt(nodeVersion, "18.0.0")) {
|
||||
// polyfills for node 16
|
||||
global.crypto = require("crypto");
|
||||
global.WebSocket = require("isomorphic-ws");
|
||||
if (typeof crypto !== "undefined" && !crypto.subtle && crypto.webcrypto) {
|
||||
crypto.subtle = crypto.webcrypto.subtle;
|
||||
}
|
||||
} else if (semver.lt(nodeVersion, "20.0.0")) {
|
||||
if (semver.lt(nodeVersion, "20.0.0")) {
|
||||
// polyfills for node 18
|
||||
global.crypto = require("crypto");
|
||||
global.WebSocket = require("isomorphic-ws");
|
||||
@@ -33,6 +23,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 +79,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 +94,11 @@ class Nostr extends NotificationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public keys for recipients
|
||||
* @param {string} recipients Newline delimited list of recipients
|
||||
* @returns {Promise<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
|
||||
*/
|
||||
@@ -19,9 +19,40 @@ class NotificationProvider {
|
||||
throw new Error("Have to override Notification.send(...)");
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the address from a monitor JSON object based on its type.
|
||||
* @param {?object} monitorJSON Monitor details (For Up/Down only)
|
||||
* @returns {string} The extracted address based on the monitor type.
|
||||
*/
|
||||
extractAddress(monitorJSON) {
|
||||
if (!monitorJSON) {
|
||||
return "";
|
||||
}
|
||||
switch (monitorJSON["type"]) {
|
||||
case "push":
|
||||
return "Heartbeat";
|
||||
case "ping":
|
||||
return monitorJSON["hostname"];
|
||||
case "port":
|
||||
case "dns":
|
||||
case "gamedig":
|
||||
case "steam":
|
||||
if (monitorJSON["port"]) {
|
||||
return monitorJSON["hostname"] + ":" + monitorJSON["port"];
|
||||
}
|
||||
return monitorJSON["hostname"];
|
||||
default:
|
||||
if (![ "https://", "http://", "" ].includes(monitorJSON["url"])) {
|
||||
return monitorJSON["url"];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an error
|
||||
* @param {any} error The error to throw
|
||||
* @returns {void}
|
||||
* @throws {any} The error specified
|
||||
*/
|
||||
throwGeneralAxiosError(error) {
|
||||
|
@@ -3,11 +3,14 @@ const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class Ntfy extends NotificationProvider {
|
||||
|
||||
name = "ntfy";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let headers = {};
|
||||
if (notification.ntfyAuthenticationMethod === "usernamePassword") {
|
||||
@@ -28,7 +31,7 @@ class Ntfy extends NotificationProvider {
|
||||
"priority": notification.ntfyPriority,
|
||||
"tags": [ "test_tube" ],
|
||||
};
|
||||
await axios.post(`${notification.ntfyserverurl}`, ntfyTestData, { headers: headers });
|
||||
await axios.post(notification.ntfyserverurl, ntfyTestData, { headers: headers });
|
||||
return okMsg;
|
||||
}
|
||||
let tags = [];
|
||||
@@ -51,20 +54,23 @@ class Ntfy extends NotificationProvider {
|
||||
"priority": priority,
|
||||
"title": monitorJSON.name + " " + status + " [Uptime-Kuma]",
|
||||
"tags": tags,
|
||||
"actions": [
|
||||
};
|
||||
|
||||
if (monitorJSON.url && monitorJSON.url !== "https://") {
|
||||
data.actions = [
|
||||
{
|
||||
"action": "view",
|
||||
"label": "Open " + monitorJSON.name,
|
||||
"url": monitorJSON.url,
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (notification.ntfyIcon) {
|
||||
data.icon = notification.ntfyIcon;
|
||||
}
|
||||
|
||||
await axios.post(`${notification.ntfyserverurl}`, data, { headers: headers });
|
||||
await axios.post(notification.ntfyserverurl, data, { headers: headers });
|
||||
|
||||
return okMsg;
|
||||
|
||||
|
@@ -2,11 +2,15 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Octopush extends NotificationProvider {
|
||||
|
||||
name = "octopush";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
const urlV2 = "https://api.octopush.com/v1/public/sms-campaign/send";
|
||||
const urlV1 = "https://www.octopush-dm.com/api/sms/json";
|
||||
|
||||
try {
|
||||
// Default - V2
|
||||
@@ -30,7 +34,7 @@ class Octopush extends NotificationProvider {
|
||||
"purpose": "alert",
|
||||
"sender": notification.octopushSenderName
|
||||
};
|
||||
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config);
|
||||
await axios.post(urlV2, data, config);
|
||||
} else if (notification.octopushVersion === "1") {
|
||||
let data = {
|
||||
"user_login": notification.octopushDMLogin,
|
||||
@@ -52,7 +56,7 @@ class Octopush extends NotificationProvider {
|
||||
|
||||
// V1 API returns 200 even on error so we must check
|
||||
// response data
|
||||
let response = await axios.post("https://www.octopush-dm.com/api/sms/json", {}, config);
|
||||
let response = await axios.post(urlV1, {}, config);
|
||||
if ("error_code" in response.data) {
|
||||
if (response.data.error_code !== "000") {
|
||||
this.throwGeneralAxiosError(`Octopush error ${JSON.stringify(response.data)}`);
|
||||
|
@@ -2,20 +2,23 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class OneBot extends NotificationProvider {
|
||||
|
||||
name = "OneBot";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let httpAddr = notification.httpAddr;
|
||||
if (!httpAddr.startsWith("http")) {
|
||||
httpAddr = "http://" + httpAddr;
|
||||
let url = notification.httpAddr;
|
||||
if (!url.startsWith("http")) {
|
||||
url = "http://" + url;
|
||||
}
|
||||
if (!httpAddr.endsWith("/")) {
|
||||
httpAddr += "/";
|
||||
if (!url.endsWith("/")) {
|
||||
url += "/";
|
||||
}
|
||||
let onebotAPIUrl = httpAddr + "send_msg";
|
||||
url += "send_msg";
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -34,7 +37,7 @@ class OneBot extends NotificationProvider {
|
||||
data["message_type"] = "private";
|
||||
data["user_id"] = notification.recieverId;
|
||||
}
|
||||
await axios.post(onebotAPIUrl, data, config);
|
||||
await axios.post(url, data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
|
47
server/notification-providers/onesender.js
Normal file
47
server/notification-providers/onesender.js
Normal file
@@ -0,0 +1,47 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Onesender extends NotificationProvider {
|
||||
name = "Onesender";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let data = {
|
||||
heartbeat: heartbeatJSON,
|
||||
monitor: monitorJSON,
|
||||
msg,
|
||||
to: notification.onesenderReceiver,
|
||||
type: "text",
|
||||
recipient_type: "individual",
|
||||
text: {
|
||||
body: msg
|
||||
}
|
||||
};
|
||||
if (notification.onesenderTypeReceiver === "private") {
|
||||
data.to = notification.onesenderReceiver + "@s.whatsapp.net";
|
||||
} else {
|
||||
data.recipient_type = "group";
|
||||
data.to = notification.onesenderReceiver + "@g.us";
|
||||
}
|
||||
let config = {
|
||||
headers: {
|
||||
"Authorization": "Bearer " + notification.onesenderToken,
|
||||
}
|
||||
};
|
||||
await axios.post(notification.onesenderURL, data, config);
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Onesender;
|
@@ -4,10 +4,9 @@ const { UP, DOWN } = require("../../src/util");
|
||||
|
||||
const opsgenieAlertsUrlEU = "https://api.eu.opsgenie.com/v2/alerts";
|
||||
const opsgenieAlertsUrlUS = "https://api.opsgenie.com/v2/alerts";
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
class Opsgenie extends NotificationProvider {
|
||||
|
||||
name = "Opsgenie";
|
||||
|
||||
/**
|
||||
@@ -68,11 +67,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") {
|
||||
|
||||
|
@@ -2,11 +2,14 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class PromoSMS extends NotificationProvider {
|
||||
|
||||
name = "promosms";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://promosms.com/api/rest/v3_2/sms";
|
||||
|
||||
if (notification.promosmsAllowLongSMS === undefined) {
|
||||
notification.promosmsAllowLongSMS = false;
|
||||
@@ -33,7 +36,7 @@ class PromoSMS extends NotificationProvider {
|
||||
"sender": notification.promosmsSenderName
|
||||
};
|
||||
|
||||
let resp = await axios.post("https://promosms.com/api/rest/v3_2/sms", data, config);
|
||||
let resp = await axios.post(url, data, config);
|
||||
|
||||
if (resp.data.response.status !== 0) {
|
||||
let error = "Something gone wrong. Api returned " + resp.data.response.status + ".";
|
||||
|
@@ -4,14 +4,16 @@ const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class Pushbullet extends NotificationProvider {
|
||||
|
||||
name = "pushbullet";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://api.pushbullet.com/v2/pushes";
|
||||
|
||||
try {
|
||||
let pushbulletUrl = "https://api.pushbullet.com/v2/pushes";
|
||||
let config = {
|
||||
headers: {
|
||||
"Access-Token": notification.pushbulletAccessToken,
|
||||
@@ -24,7 +26,7 @@ class Pushbullet extends NotificationProvider {
|
||||
"title": "Uptime Kuma Alert",
|
||||
"body": msg,
|
||||
};
|
||||
await axios.post(pushbulletUrl, data, config);
|
||||
await axios.post(url, data, config);
|
||||
} else if (heartbeatJSON["status"] === DOWN) {
|
||||
let downData = {
|
||||
"type": "note",
|
||||
@@ -33,7 +35,7 @@ class Pushbullet extends NotificationProvider {
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
||||
};
|
||||
await axios.post(pushbulletUrl, downData, config);
|
||||
await axios.post(url, downData, config);
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
let upData = {
|
||||
"type": "note",
|
||||
@@ -42,7 +44,7 @@ class Pushbullet extends NotificationProvider {
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
||||
};
|
||||
await axios.post(pushbulletUrl, upData, config);
|
||||
await axios.post(url, upData, config);
|
||||
}
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
|
@@ -3,14 +3,15 @@ const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class PushDeer extends NotificationProvider {
|
||||
|
||||
name = "PushDeer";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
let endpoint = "/message/push";
|
||||
let serverUrl = notification.pushdeerServer || "https://api2.pushdeer.com";
|
||||
let pushdeerlink = `${serverUrl.trim().replace(/\/*$/, "")}${endpoint}`;
|
||||
const okMsg = "Sent Successfully.";
|
||||
const serverUrl = notification.pushdeerServer || "https://api2.pushdeer.com";
|
||||
const url = `${serverUrl.trim().replace(/\/*$/, "")}/message/push`;
|
||||
|
||||
let valid = msg != null && monitorJSON != null && heartbeatJSON != null;
|
||||
|
||||
@@ -31,7 +32,7 @@ class PushDeer extends NotificationProvider {
|
||||
};
|
||||
|
||||
try {
|
||||
let res = await axios.post(pushdeerlink, data);
|
||||
let res = await axios.post(url, data);
|
||||
|
||||
if ("error" in res.data) {
|
||||
let error = res.data.error;
|
||||
|
@@ -1,13 +1,18 @@
|
||||
const { getMonitorRelativeURL } = require("../../src/util");
|
||||
const { setting } = require("../util-server");
|
||||
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
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";
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://api.pushover.net/1/messages.json";
|
||||
|
||||
let data = {
|
||||
"message": msg,
|
||||
@@ -21,6 +26,12 @@ class Pushover extends NotificationProvider {
|
||||
"html": 1,
|
||||
};
|
||||
|
||||
const baseURL = await setting("primaryBaseURL");
|
||||
if (baseURL && monitorJSON) {
|
||||
data["url"] = baseURL + getMonitorRelativeURL(monitorJSON.id);
|
||||
data["url_title"] = "Link to Monitor";
|
||||
}
|
||||
|
||||
if (notification.pushoverdevice) {
|
||||
data.device = notification.pushoverdevice;
|
||||
}
|
||||
@@ -30,11 +41,11 @@ class Pushover extends NotificationProvider {
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
await axios.post(pushoverlink, data);
|
||||
await axios.post(url, data);
|
||||
return okMsg;
|
||||
} else {
|
||||
data.message += `\n<b>Time (${heartbeatJSON["timezone"]})</b>:${heartbeatJSON["localDateTime"]}`;
|
||||
await axios.post(pushoverlink, data);
|
||||
await axios.post(url, data);
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
|
@@ -2,11 +2,13 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Pushy extends NotificationProvider {
|
||||
|
||||
name = "pushy";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
await axios.post(`https://api.pushy.me/push?api_key=${notification.pushyAPIKey}`, {
|
||||
|
@@ -5,11 +5,14 @@ const { setting } = require("../util-server");
|
||||
const { getMonitorRelativeURL, DOWN } = require("../../src/util");
|
||||
|
||||
class RocketChat extends NotificationProvider {
|
||||
|
||||
name = "rocket.chat";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {
|
||||
|
65
server/notification-providers/send-grid.js
Normal file
65
server/notification-providers/send-grid.js
Normal file
@@ -0,0 +1,65 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class SendGrid extends NotificationProvider {
|
||||
name = "SendGrid";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${notification.sendgridApiKey}`,
|
||||
},
|
||||
};
|
||||
|
||||
let personalizations = {
|
||||
to: [{ email: notification.sendgridToEmail }],
|
||||
};
|
||||
|
||||
// Add CC recipients if provided
|
||||
if (notification.sendgridCcEmail) {
|
||||
personalizations.cc = notification.sendgridCcEmail
|
||||
.split(",")
|
||||
.map((email) => ({ email: email.trim() }));
|
||||
}
|
||||
|
||||
// Add BCC recipients if provided
|
||||
if (notification.sendgridBccEmail) {
|
||||
personalizations.bcc = notification.sendgridBccEmail
|
||||
.split(",")
|
||||
.map((email) => ({ email: email.trim() }));
|
||||
}
|
||||
|
||||
let data = {
|
||||
personalizations: [ personalizations ],
|
||||
from: { email: notification.sendgridFromEmail.trim() },
|
||||
subject:
|
||||
notification.sendgridSubject ||
|
||||
"Notification from Your Uptime Kuma",
|
||||
content: [
|
||||
{
|
||||
type: "text/plain",
|
||||
value: msg,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await axios.post(
|
||||
"https://api.sendgrid.com/v3/mail/send",
|
||||
data,
|
||||
config
|
||||
);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SendGrid;
|
@@ -3,13 +3,22 @@ const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class ServerChan extends NotificationProvider {
|
||||
|
||||
name = "ServerChan";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
// serverchan3 requires sending via ft07.com
|
||||
const matchResult = String(notification.serverChanSendKey).match(/^sctp(\d+)t/i);
|
||||
const url = matchResult && matchResult[1]
|
||||
? `https://${matchResult[1]}.push.ft07.com/send/${notification.serverChanSendKey}.send`
|
||||
: `https://sctapi.ftqq.com/${notification.serverChanSendKey}.send`;
|
||||
|
||||
try {
|
||||
await axios.post(`https://sctapi.ftqq.com/${notification.serverChanSendKey}.send`, {
|
||||
await axios.post(url, {
|
||||
"title": this.checkStatus(heartbeatJSON, monitorJSON),
|
||||
"desp": msg,
|
||||
});
|
||||
@@ -23,8 +32,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) {
|
||||
|
@@ -2,11 +2,14 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class SerwerSMS extends NotificationProvider {
|
||||
|
||||
name = "serwersms";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://api2.serwersms.pl/messages/send_sms";
|
||||
|
||||
try {
|
||||
let config = {
|
||||
@@ -22,7 +25,7 @@ class SerwerSMS extends NotificationProvider {
|
||||
"sender": notification.serwersmsSenderName,
|
||||
};
|
||||
|
||||
let resp = await axios.post("https://api2.serwersms.pl/messages/send_sms", data, config);
|
||||
let resp = await axios.post(url, data, config);
|
||||
|
||||
if (!resp.data.success) {
|
||||
if (resp.data.error) {
|
||||
|
57
server/notification-providers/sevenio.js
Normal file
57
server/notification-providers/sevenio.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class SevenIO extends NotificationProvider {
|
||||
name = "SevenIO";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
const data = {
|
||||
to: notification.sevenioTo,
|
||||
from: notification.sevenioSender || "Uptime Kuma",
|
||||
text: msg,
|
||||
};
|
||||
|
||||
const config = {
|
||||
baseURL: "https://gateway.seven.io/api/",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": notification.sevenioApiKey,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
// testing or certificate expiry notification
|
||||
if (heartbeatJSON == null) {
|
||||
await axios.post("sms", data, config);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
let address = this.extractAddress(monitorJSON);
|
||||
if (address !== "") {
|
||||
address = `(${address}) `;
|
||||
}
|
||||
|
||||
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
||||
if (heartbeatJSON["status"] === DOWN) {
|
||||
data.text = `Your service ${monitorJSON["name"]} ${address}went down at ${heartbeatJSON["localDateTime"]} ` +
|
||||
`(${heartbeatJSON["timezone"]}). Error: ${heartbeatJSON["msg"]}`;
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
data.text = `Your service ${monitorJSON["name"]} ${address}went back up at ${heartbeatJSON["localDateTime"]} ` +
|
||||
`(${heartbeatJSON["timezone"]}).`;
|
||||
}
|
||||
await axios.post("sms", data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = SevenIO;
|
@@ -2,11 +2,13 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Signal extends NotificationProvider {
|
||||
|
||||
name = "signal";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let data = {
|
||||
|
52
server/notification-providers/signl4.js
Normal file
52
server/notification-providers/signl4.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { UP, DOWN } = require("../../src/util");
|
||||
|
||||
class SIGNL4 extends NotificationProvider {
|
||||
name = "SIGNL4";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let data = {
|
||||
heartbeat: heartbeatJSON,
|
||||
monitor: monitorJSON,
|
||||
msg,
|
||||
// Source system
|
||||
"X-S4-SourceSystem": "UptimeKuma",
|
||||
monitorUrl: this.extractAddress(monitorJSON),
|
||||
};
|
||||
|
||||
const config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
if (heartbeatJSON == null) {
|
||||
// Test alert
|
||||
data.title = "Uptime Kuma Alert";
|
||||
data.message = msg;
|
||||
} else if (heartbeatJSON.status === UP) {
|
||||
data.title = "Uptime Kuma Monitor ✅ Up";
|
||||
data["X-S4-ExternalID"] = "UptimeKuma-" + monitorJSON.monitorID;
|
||||
data["X-S4-Status"] = "resolved";
|
||||
} else if (heartbeatJSON.status === DOWN) {
|
||||
data.title = "Uptime Kuma Monitor 🔴 Down";
|
||||
data["X-S4-ExternalID"] = "UptimeKuma-" + monitorJSON.monitorID;
|
||||
data["X-S4-Status"] = "new";
|
||||
}
|
||||
|
||||
await axios.post(notification.webhookURL, data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SIGNL4;
|
@@ -1,16 +1,17 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { setSettings, setting } = require("../util-server");
|
||||
const { getMonitorRelativeURL, UP } = require("../../src/util");
|
||||
const { getMonitorRelativeURL, UP, log } = require("../../src/util");
|
||||
|
||||
class Slack extends NotificationProvider {
|
||||
|
||||
name = "slack";
|
||||
|
||||
/**
|
||||
* 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,8 +26,104 @@ class Slack extends NotificationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the actions available in the slack message
|
||||
* @param {string} baseURL Uptime Kuma base URL
|
||||
* @param {object} monitorJSON The monitor config
|
||||
* @returns {Array} The relevant action objects
|
||||
*/
|
||||
buildActions(baseURL, monitorJSON) {
|
||||
const actions = [];
|
||||
|
||||
if (baseURL) {
|
||||
actions.push({
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Visit Uptime Kuma",
|
||||
},
|
||||
"value": "Uptime-Kuma",
|
||||
"url": baseURL + getMonitorRelativeURL(monitorJSON.id),
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
const address = this.extractAddress(monitorJSON);
|
||||
if (address) {
|
||||
try {
|
||||
actions.push({
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Visit site",
|
||||
},
|
||||
"value": "Site",
|
||||
"url": new URL(address),
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
log.debug("slack", `Failed to parse address ${address} as URL`);
|
||||
}
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the different blocks the Slack message consists of.
|
||||
* @param {string} baseURL Uptime Kuma base URL
|
||||
* @param {object} monitorJSON The monitor object
|
||||
* @param {object} heartbeatJSON The heartbeat object
|
||||
* @param {string} title The message title
|
||||
* @param {string} msg The message body
|
||||
* @returns {Array<object>} The rich content blocks for the Slack message
|
||||
*/
|
||||
buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg) {
|
||||
|
||||
//create an array to dynamically add blocks
|
||||
const blocks = [];
|
||||
|
||||
// the header block
|
||||
blocks.push({
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": title,
|
||||
},
|
||||
});
|
||||
|
||||
// the body block, containing the details
|
||||
blocks.push({
|
||||
"type": "section",
|
||||
"fields": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Message*\n" + msg,
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": `*Time (${heartbeatJSON["timezone"]})*\n${heartbeatJSON["localDateTime"]}`,
|
||||
}
|
||||
],
|
||||
});
|
||||
|
||||
const actions = this.buildActions(baseURL, monitorJSON);
|
||||
if (actions.length > 0) {
|
||||
//the actions block, containing buttons
|
||||
blocks.push({
|
||||
"type": "actions",
|
||||
"elements": actions,
|
||||
});
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
if (notification.slackchannelnotify) {
|
||||
msg += " <!channel>";
|
||||
@@ -44,63 +141,31 @@ class Slack extends NotificationProvider {
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const textMsg = "Uptime Kuma Alert";
|
||||
const baseURL = await setting("primaryBaseURL");
|
||||
|
||||
const title = "Uptime Kuma Alert";
|
||||
let data = {
|
||||
"text": `${textMsg}\n${msg}`,
|
||||
"channel": notification.slackchannel,
|
||||
"username": notification.slackusername,
|
||||
"icon_emoji": notification.slackiconemo,
|
||||
"attachments": [
|
||||
"attachments": [],
|
||||
};
|
||||
|
||||
if (notification.slackrichmessage) {
|
||||
data.attachments.push(
|
||||
{
|
||||
"color": (heartbeatJSON["status"] === UP) ? "#2eb886" : "#e01e5a",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": textMsg,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Message*\n" + msg,
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": `*Time (${heartbeatJSON["timezone"]})*\n${heartbeatJSON["localDateTime"]}`,
|
||||
}],
|
||||
}
|
||||
],
|
||||
"blocks": this.buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg),
|
||||
}
|
||||
]
|
||||
};
|
||||
);
|
||||
} else {
|
||||
data.text = `${title}\n${msg}`;
|
||||
}
|
||||
|
||||
if (notification.slackbutton) {
|
||||
await Slack.deprecateURL(notification.slackbutton);
|
||||
}
|
||||
|
||||
const baseURL = await setting("primaryBaseURL");
|
||||
|
||||
// Button
|
||||
if (baseURL) {
|
||||
data.attachments.forEach(element => {
|
||||
element.blocks.push({
|
||||
"type": "actions",
|
||||
"elements": [{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Visit Uptime Kuma",
|
||||
},
|
||||
"value": "Uptime-Kuma",
|
||||
"url": baseURL + getMonitorRelativeURL(monitorJSON.id),
|
||||
}],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await axios.post(notification.slackwebhookURL, data);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
|
@@ -4,8 +4,13 @@ const axios = require("axios");
|
||||
class SMSC extends NotificationProvider {
|
||||
name = "smsc";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://smsc.kz/sys/send.php?";
|
||||
|
||||
try {
|
||||
let config = {
|
||||
headers: {
|
||||
@@ -26,7 +31,7 @@ class SMSC extends NotificationProvider {
|
||||
getArray.push("sender=" + notification.smscSenderName);
|
||||
}
|
||||
|
||||
let resp = await axios.get("https://smsc.kz/sys/send.php?" + getArray.join("&"), config);
|
||||
let resp = await axios.get(url + getArray.join("&"), config);
|
||||
if (resp.data.id === undefined) {
|
||||
let error = `Something gone wrong. Api returned code ${resp.data.error_code}: ${resp.data.error}`;
|
||||
this.throwGeneralAxiosError(error);
|
||||
|
@@ -2,11 +2,13 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class SMSEagle extends NotificationProvider {
|
||||
|
||||
name = "SMSEagle";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let config = {
|
||||
|
@@ -2,20 +2,24 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class SMSManager extends NotificationProvider {
|
||||
|
||||
name = "SMSManager";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://http-api.smsmanager.cz/Send";
|
||||
|
||||
try {
|
||||
let data = {
|
||||
apikey: notification.smsmanagerApiKey,
|
||||
endpoint: "https://http-api.smsmanager.cz/Send",
|
||||
message: msg.replace(/[^\x00-\x7F]/g, ""),
|
||||
to: notification.numbers,
|
||||
messageType: notification.messageType,
|
||||
number: notification.numbers,
|
||||
gateway: notification.messageType,
|
||||
};
|
||||
await axios.get(`${data.endpoint}?apikey=${data.apikey}&message=${data.message}&number=${data.to}&gateway=${data.messageType}`);
|
||||
return "SMS sent sucessfully.";
|
||||
await axios.get(`${url}?apikey=${data.apikey}&message=${data.message}&number=${data.number}&gateway=${data.messageType}`);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
46
server/notification-providers/smspartner.js
Normal file
46
server/notification-providers/smspartner.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class SMSPartner extends NotificationProvider {
|
||||
name = "SMSPartner";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://api.smspartner.fr/v1/send";
|
||||
|
||||
try {
|
||||
// smspartner does not support non ascii characters and only a maximum 639 characters
|
||||
let cleanMsg = msg.replace(/[^\x00-\x7F]/g, "").substring(0, 639);
|
||||
|
||||
let data = {
|
||||
"apiKey": notification.smspartnerApikey,
|
||||
"sender": notification.smspartnerSenderName.substring(0, 11),
|
||||
"phoneNumbers": notification.smspartnerPhoneNumber,
|
||||
"message": cleanMsg,
|
||||
};
|
||||
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"cache-control": "no-cache",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
};
|
||||
|
||||
let resp = await axios.post(url, data, config);
|
||||
|
||||
if (resp.data.success !== true) {
|
||||
throw Error(`Api returned ${resp.data.response.status}.`);
|
||||
}
|
||||
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SMSPartner;
|
@@ -1,12 +1,16 @@
|
||||
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 okMsg = "Sent Successfully.";
|
||||
|
||||
const config = {
|
||||
host: notification.smtpHost,
|
||||
@@ -36,75 +40,80 @@ 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.";
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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"];
|
||||
monitorHostnameOrURL = this.extractAddress(monitorJSON);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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") {
|
||||
|
||||
|
@@ -3,11 +3,13 @@ const axios = require("axios");
|
||||
const { DOWN } = require("../../src/util");
|
||||
|
||||
class Squadcast extends NotificationProvider {
|
||||
|
||||
name = "squadcast";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
|
||||
@@ -32,25 +34,7 @@ class Squadcast extends NotificationProvider {
|
||||
data.status = "resolve";
|
||||
}
|
||||
|
||||
let address;
|
||||
switch (monitorJSON["type"]) {
|
||||
case "ping":
|
||||
address = monitorJSON["hostname"];
|
||||
break;
|
||||
case "port":
|
||||
case "dns":
|
||||
case "steam":
|
||||
address = monitorJSON["hostname"];
|
||||
if (monitorJSON["port"]) {
|
||||
address += ":" + monitorJSON["port"];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
address = monitorJSON["url"];
|
||||
break;
|
||||
}
|
||||
|
||||
data.tags["AlertAddress"] = address;
|
||||
data.tags["AlertAddress"] = this.extractAddress(monitorJSON);
|
||||
|
||||
monitorJSON["tags"].forEach(tag => {
|
||||
data.tags[tag["name"]] = {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user