Merge branch 'master' into websocket_test

This commit is contained in:
Louis Lam
2025-11-27 20:50:07 +08:00
committed by GitHub
265 changed files with 16477 additions and 4296 deletions
+3 -3
View File
@@ -18,15 +18,15 @@ exports.login = async function (username, password) {
return null;
}
let user = await R.findOne("user", " username = ? AND active = 1 ", [
username,
let user = await R.findOne("user", "TRIM(username) = ? AND active = 1 ", [
username.trim(),
]);
if (user && passwordHash.verify(password, user.password)) {
// Upgrade the hash to bcrypt
if (passwordHash.needRehash(user.password)) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
passwordHash.generate(password),
await passwordHash.generate(password),
user.id,
]);
}
+12 -10
View File
@@ -1,4 +1,5 @@
const fs = require("fs");
const fsAsync = fs.promises;
const { R } = require("redbean-node");
const { setSetting, setting } = require("./util-server");
const { log, sleep } = require("../src/util");
@@ -11,6 +12,7 @@ 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");
const SqlString = require("sqlstring");
/**
* Database & App Data Folder
@@ -18,7 +20,7 @@ const KumaColumnCompiler = require("./utils/knex/lib/dialects/mysql2/schema/mysq
class Database {
/**
* Boostrap database for SQLite
* Bootstrap database for SQLite
* @type {string}
*/
static templatePath = "./db/kuma.db";
@@ -255,10 +257,6 @@ class Database {
}
};
} 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,
@@ -266,7 +264,11 @@ class Database {
password: dbConfig.password,
});
await connection.execute("CREATE DATABASE IF NOT EXISTS " + dbConfig.dbName + " CHARACTER SET utf8mb4");
// Set to true, so for example "uptime.kuma", becomes `uptime.kuma`, not `uptime`.`kuma`
// Doc: https://github.com/mysqljs/sqlstring?tab=readme-ov-file#escaping-query-identifiers
const escapedDBName = SqlString.escapeId(dbConfig.dbName, true);
await connection.execute("CREATE DATABASE IF NOT EXISTS " + escapedDBName + " CHARACTER SET utf8mb4");
connection.end();
config = {
@@ -707,12 +709,12 @@ class Database {
/**
* Get the size of the database (SQLite only)
* @returns {number} Size of database
* @returns {Promise<number>} Size of database
*/
static getSize() {
static async getSize() {
if (Database.dbConfig.type === "sqlite") {
log.debug("db", "Database.getSize()");
let stats = fs.statSync(Database.sqlitePath);
let stats = await fsAsync.stat(Database.sqlitePath);
log.debug("db", stats);
return stats.size;
}
@@ -736,7 +738,7 @@ class Database {
if (Database.dbConfig.type === "sqlite") {
return "DATETIME('now', ? || ' hours')";
} else {
return "DATE_ADD(NOW(), INTERVAL ? HOUR)";
return "DATE_ADD(UTC_TIMESTAMP(), INTERVAL ? HOUR)";
}
}
+9 -9
View File
@@ -1,10 +1,10 @@
const axios = require("axios");
const { R } = require("redbean-node");
const https = require("https");
const fs = require("fs");
const fsAsync = require("fs").promises;
const path = require("path");
const Database = require("./database");
const { axiosAbortSignal } = require("./util-server");
const { axiosAbortSignal, fsExists } = require("./util-server");
class DockerHost {
@@ -81,7 +81,7 @@ class DockerHost {
options.socketPath = dockerHost.dockerDaemon;
} else if (dockerHost.dockerType === "tcp") {
options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon);
options.httpsAgent = new https.Agent(DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL));
options.httpsAgent = new https.Agent(await DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL));
}
try {
@@ -141,9 +141,9 @@ class DockerHost {
* 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://
* @returns {object} HTTP agent options
* @returns {Promise<object>} HTTP agent options
*/
static getHttpsAgentOptions(dockerType, url) {
static async getHttpsAgentOptions(dockerType, url) {
let baseOptions = {
maxCachedSessions: 0,
rejectUnauthorized: true
@@ -156,10 +156,10 @@ class DockerHost {
let certPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameCert);
let keyPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameKey);
if (dockerType === "tcp" && fs.existsSync(caPath) && fs.existsSync(certPath) && fs.existsSync(keyPath)) {
let ca = fs.readFileSync(caPath);
let key = fs.readFileSync(keyPath);
let cert = fs.readFileSync(certPath);
if (dockerType === "tcp" && await fsExists(caPath) && await fsExists(certPath) && await fsExists(keyPath)) {
let ca = await fsAsync.readFile(caPath);
let key = await fsAsync.readFile(keyPath);
let cert = await fsAsync.readFile(certPath);
certOptions = {
ca,
key,
+1 -1
View File
@@ -33,7 +33,7 @@ class Group extends BeanModel {
*/
async getMonitorList() {
return R.convertToBeans("monitor", await R.getAll(`
SELECT monitor.*, monitor_group.send_url FROM monitor, monitor_group
SELECT monitor.*, monitor_group.send_url, monitor_group.custom_url FROM monitor, monitor_group
WHERE monitor.id = monitor_group.monitor_id
AND group_id = ?
ORDER BY monitor_group.weight
+49 -8
View File
@@ -158,12 +158,22 @@ class Maintenance extends BeanModel {
bean.active = obj.active;
if (obj.dateRange[0]) {
const parsedDate = new Date(obj.dateRange[0]);
if (isNaN(parsedDate.getTime()) || parsedDate.getFullYear() > 9999) {
throw new Error("Invalid start date");
}
bean.start_date = obj.dateRange[0];
} else {
bean.start_date = null;
}
if (obj.dateRange[1]) {
const parsedDate = new Date(obj.dateRange[1]);
if (isNaN(parsedDate.getTime()) || parsedDate.getFullYear() > 9999) {
throw new Error("Invalid end date");
}
bean.end_date = obj.dateRange[1];
} else {
bean.end_date = null;
@@ -192,7 +202,7 @@ class Maintenance extends BeanModel {
* @returns {void}
*/
static validateCron(cron) {
let job = new Cron(cron, () => {});
let job = new Cron(cron, () => { });
job.stop();
}
@@ -229,11 +239,13 @@ class Maintenance extends BeanModel {
apicache.clear();
});
} else if (this.cron != null) {
let current = dayjs();
// Here should be cron or recurring
try {
this.beanMeta.status = "scheduled";
let startEvent = (customDuration = 0) => {
let startEvent = async (customDuration = 0) => {
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
this.beanMeta.status = "under-maintenance";
@@ -248,6 +260,10 @@ class Maintenance extends BeanModel {
this.beanMeta.status = "scheduled";
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
}, duration);
// Set last start date to current time
this.last_start_date = current.toISOString();
await R.store(this);
};
// Create Cron
@@ -256,11 +272,34 @@ class Maintenance extends BeanModel {
const startDate = dayjs(this.startDate);
const [ hour, minute ] = this.startTime.split(":");
const startDateTime = startDate.hour(hour).minute(minute);
// Fix #6118, since the startDateTime is optional, it will throw error if the date is null when using toISOString()
let startAt = undefined;
try {
startAt = startDateTime.toISOString();
} catch (_) {}
this.beanMeta.job = new Cron(this.cron, {
timezone: await this.getTimezone(),
interval: this.interval_day * 24 * 60 * 60,
startAt: startDateTime.toISOString(),
}, startEvent);
startAt,
}, () => {
if (!this.lastStartDate || this.interval_day === 1) {
return startEvent();
}
// If last start date is set, it means the maintenance has been started before
let lastStartDate = dayjs(this.lastStartDate)
.subtract(1.1, "hour"); // Subtract 1.1 hour to avoid issues with timezone differences
// Check if the interval is enough
if (current.diff(lastStartDate, "day") < this.interval_day) {
log.debug("maintenance", "Maintenance id: " + this.id + " is still in the window, skipping start event");
return;
}
log.debug("maintenance", "Maintenance id: " + this.id + " is not in the window, starting event");
return startEvent();
});
} else {
this.beanMeta.job = new Cron(this.cron, {
timezone: await this.getTimezone(),
@@ -269,7 +308,6 @@ class Maintenance extends BeanModel {
// Continue if the maintenance is still in the window
let runningTimeslot = this.getRunningTimeslot();
let current = dayjs();
if (runningTimeslot) {
let duration = dayjs(runningTimeslot.endDate).diff(current, "second") * 1000;
@@ -413,8 +451,11 @@ class Maintenance extends BeanModel {
} else if (!this.strategy.startsWith("recurring-")) {
this.cron = "";
} else if (this.strategy === "recurring-interval") {
// For intervals, the pattern is calculated in the run function as the interval-option is set
this.cron = "* * * * *";
// For intervals, the pattern is used to check if the execution should be started
let array = this.start_time.split(":");
let hour = parseInt(array[0]);
let minute = parseInt(array[1]);
this.cron = `${minute} ${hour} * * *`;
this.duration = this.calcDuration();
log.debug("maintenance", "Cron: " + this.cron);
log.debug("maintenance", "Duration: " + this.duration);
+131 -58
View File
@@ -2,10 +2,14 @@ const dayjs = require("dayjs");
const axios = require("axios");
const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
SQL_DATETIME_FORMAT, evaluateJsonQuery
SQL_DATETIME_FORMAT, evaluateJsonQuery,
PING_PACKET_SIZE_MIN, PING_PACKET_SIZE_MAX, PING_PACKET_SIZE_DEFAULT,
PING_GLOBAL_TIMEOUT_MIN, PING_GLOBAL_TIMEOUT_MAX, PING_GLOBAL_TIMEOUT_DEFAULT,
PING_COUNT_MIN, PING_COUNT_MAX, PING_COUNT_DEFAULT,
PING_PER_REQUEST_TIMEOUT_MIN, PING_PER_REQUEST_TIMEOUT_MAX, PING_PER_REQUEST_TIMEOUT_DEFAULT
} = require("../../src/util");
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
const { ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
} = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
@@ -53,7 +57,7 @@ class Monitor extends BeanModel {
};
if (this.sendUrl) {
obj.url = this.url;
obj.url = this.customUrl ?? this.url;
}
if (showTags) {
@@ -155,8 +159,15 @@ class Monitor extends BeanModel {
snmpOid: this.snmpOid,
jsonPathOperator: this.jsonPathOperator,
snmpVersion: this.snmpVersion,
smtpSecurity: this.smtpSecurity,
rabbitmqNodes: JSON.parse(this.rabbitmqNodes),
conditions: JSON.parse(this.conditions),
ipFamily: this.ipFamily,
// ping advanced options
ping_numeric: this.isPingNumeric(),
ping_count: this.ping_count,
ping_per_request_timeout: this.ping_per_request_timeout,
};
if (includeSensitiveData) {
@@ -172,6 +183,7 @@ class Monitor extends BeanModel {
oauth_client_secret: this.oauth_client_secret,
oauth_token_url: this.oauth_token_url,
oauth_scopes: this.oauth_scopes,
oauth_audience: this.oauth_audience,
oauth_auth_method: this.oauth_auth_method,
pushToken: this.pushToken,
databaseConnectionString: this.databaseConnectionString,
@@ -180,6 +192,7 @@ class Monitor extends BeanModel {
radiusSecret: this.radiusSecret,
mqttUsername: this.mqttUsername,
mqttPassword: this.mqttPassword,
mqttWebsocketPath: this.mqttWebsocketPath,
authWorkstation: this.authWorkstation,
authDomain: this.authDomain,
tlsCa: this.tlsCa,
@@ -249,6 +262,14 @@ class Monitor extends BeanModel {
return Boolean(this.expiryNotification);
}
/**
* Check if ping should use numeric output only
* @returns {boolean} True if IP addresses will be output instead of symbolic hostnames
*/
isPingNumeric() {
return Boolean(this.ping_numeric);
}
/**
* Parse to boolean
* @returns {boolean} Should TLS errors be ignored?
@@ -338,7 +359,7 @@ class Monitor extends BeanModel {
let previousBeat = null;
let retries = 0;
this.prometheus = new Prometheus(this);
this.prometheus = new Prometheus(this, await this.getTags());
const beat = async () => {
@@ -390,39 +411,6 @@ class Monitor extends BeanModel {
if (await Monitor.isUnderMaintenance(this.id)) {
bean.msg = "Monitor under maintenance";
bean.status = MAINTENANCE;
} else if (this.type === "group") {
const children = await Monitor.getChildren(this.id);
if (children.length > 0) {
bean.status = UP;
bean.msg = "All children up and running";
for (const child of children) {
if (!child.active) {
// Ignore inactive childs
continue;
}
const lastBeat = await Monitor.getPreviousHeartbeat(child.id);
// Only change state if the monitor is in worse conditions then the ones before
// lastBeat.status could be null
if (!lastBeat) {
bean.status = PENDING;
} else if (bean.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) {
bean.status = lastBeat.status;
} else if (bean.status === PENDING && lastBeat.status === DOWN) {
bean.status = lastBeat.status;
}
}
if (bean.status !== UP) {
bean.msg = "Child inaccessible";
}
} else {
// Set status pending if group is empty
bean.status = PENDING;
bean.msg = "Group empty";
}
} else if (this.type === "http" || this.type === "keyword" || this.type === "json-query") {
// Do not do any queries/high loading things before the "bean.ping"
let startTime = dayjs().valueOf();
@@ -451,10 +439,26 @@ class Monitor extends BeanModel {
}
}
let agentFamily = undefined;
if (this.ipFamily === "ipv4") {
agentFamily = 4;
}
if (this.ipFamily === "ipv6") {
agentFamily = 6;
}
const httpsAgentOptions = {
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(),
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
autoSelectFamily: true,
...(agentFamily ? { family: agentFamily } : {})
};
const httpAgentOptions = {
maxCachedSessions: 0,
autoSelectFamily: true,
...(agentFamily ? { family: agentFamily } : {})
};
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
@@ -516,6 +520,7 @@ class Monitor extends BeanModel {
if (proxy && proxy.active) {
const { httpAgent, httpsAgent } = Proxy.createAgents(proxy, {
httpsAgentOptions: httpsAgentOptions,
httpAgentOptions: httpAgentOptions,
});
options.proxy = false;
@@ -524,6 +529,10 @@ class Monitor extends BeanModel {
}
}
if (!options.httpAgent) {
options.httpAgent = new http.Agent(httpAgentOptions);
}
if (!options.httpsAgent) {
let jar = new CookieJar();
let httpsCookieAgentOptions = {
@@ -579,7 +588,8 @@ class Monitor extends BeanModel {
}
}
if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID === this.id) {
// eslint-disable-next-line eqeqeq
if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID == this.id) {
log.info("monitor", res.data);
}
@@ -621,13 +631,8 @@ class Monitor extends BeanModel {
}
} else if (this.type === "port") {
bean.ping = await tcping(this.hostname, this.port);
bean.msg = "";
bean.status = UP;
} else if (this.type === "ping") {
bean.ping = await ping(this.hostname, this.packetSize);
bean.ping = await ping(this.hostname, this.ping_count, "", this.ping_numeric, this.packetSize, this.timeout, this.ping_per_request_timeout);
bean.msg = "";
bean.status = UP;
} else if (this.type === "push") { // Type: Push
@@ -699,7 +704,7 @@ class Monitor extends BeanModel {
bean.msg = res.data.response.servers[0].name;
try {
bean.ping = await ping(this.hostname, this.packetSize);
bean.ping = await ping(this.hostname, PING_COUNT_DEFAULT, "", true, this.packetSize, PING_GLOBAL_TIMEOUT_DEFAULT, PING_PER_REQUEST_TIMEOUT_DEFAULT);
} catch (_) { }
} else {
throw new Error("Server not found on Steam");
@@ -749,7 +754,7 @@ class Monitor extends BeanModel {
} else if (dockerHost._dockerType === "tcp") {
options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
options.httpsAgent = new https.Agent(
DockerHost.getHttpsAgentOptions(dockerHost._dockerType, options.baseURL)
await DockerHost.getHttpsAgentOptions(dockerHost._dockerType, options.baseURL)
);
}
@@ -851,13 +856,6 @@ class Monitor extends BeanModel {
bean.msg = resp.code;
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "redis") {
let startTime = dayjs().valueOf();
bean.msg = await redisPingAsync(this.databaseConnectionString, !this.ignoreTls);
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type in UptimeKumaServer.monitorTypeList) {
let startTime = dayjs().valueOf();
const monitorType = UptimeKumaServer.monitorTypeList[this.type];
@@ -1316,7 +1314,7 @@ class Monitor extends BeanModel {
/**
* Send a notification about a monitor
* @param {boolean} isFirstBeat Is this beat the first of this monitor?
* @param {Monitor} monitor The monitor to send a notificaton about
* @param {Monitor} monitor The monitor to send a notification about
* @param {Bean} bean Status information about monitor
* @returns {void}
*/
@@ -1337,7 +1335,8 @@ class Monitor extends BeanModel {
try {
const heartbeatJSON = bean.toJSON();
const monitorData = [{ id: monitor.id,
active: monitor.active
active: monitor.active,
name: monitor.name
}];
const preloadData = await Monitor.preparePreloadData(monitorData);
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
@@ -1510,6 +1509,31 @@ class Monitor extends BeanModel {
if (this.interval < MIN_INTERVAL_SECOND) {
throw new Error(`Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`);
}
if (this.type === "ping") {
// ping parameters validation
if (this.packetSize && (this.packetSize < PING_PACKET_SIZE_MIN || this.packetSize > PING_PACKET_SIZE_MAX)) {
throw new Error(`Packet size must be between ${PING_PACKET_SIZE_MIN} and ${PING_PACKET_SIZE_MAX} (default: ${PING_PACKET_SIZE_DEFAULT})`);
}
if (this.ping_per_request_timeout && (this.ping_per_request_timeout < PING_PER_REQUEST_TIMEOUT_MIN || this.ping_per_request_timeout > PING_PER_REQUEST_TIMEOUT_MAX)) {
throw new Error(`Per-ping timeout must be between ${PING_PER_REQUEST_TIMEOUT_MIN} and ${PING_PER_REQUEST_TIMEOUT_MAX} seconds (default: ${PING_PER_REQUEST_TIMEOUT_DEFAULT})`);
}
if (this.ping_count && (this.ping_count < PING_COUNT_MIN || this.ping_count > PING_COUNT_MAX)) {
throw new Error(`Echo requests count must be between ${PING_COUNT_MIN} and ${PING_COUNT_MAX} (default: ${PING_COUNT_DEFAULT})`);
}
if (this.timeout) {
const pingGlobalTimeout = Math.round(Number(this.timeout));
if (pingGlobalTimeout < this.ping_per_request_timeout || pingGlobalTimeout < PING_GLOBAL_TIMEOUT_MIN || pingGlobalTimeout > PING_GLOBAL_TIMEOUT_MAX) {
throw new Error(`Timeout must be between ${PING_GLOBAL_TIMEOUT_MIN} and ${PING_GLOBAL_TIMEOUT_MAX} seconds (default: ${PING_GLOBAL_TIMEOUT_DEFAULT})`);
}
this.timeout = pingGlobalTimeout;
}
}
}
/**
@@ -1635,7 +1659,7 @@ class Monitor extends BeanModel {
/**
* Gets all Children of the monitor
* @param {number} monitorID ID of monitor to get
* @returns {Promise<LooseObject<any>>} Children
* @returns {Promise<LooseObject<any>[]>} Children
*/
static async getChildren(monitorID) {
return await R.getAll(`
@@ -1701,6 +1725,55 @@ class Monitor extends BeanModel {
]);
}
/**
* Delete a monitor from the system
* @param {number} monitorID ID of the monitor to delete
* @param {number} userID ID of the user who owns the monitor
* @returns {Promise<void>}
*/
static async deleteMonitor(monitorID, userID) {
const server = UptimeKumaServer.getInstance();
// Stop the monitor if it's running
if (monitorID in server.monitorList) {
await server.monitorList[monitorID].stop();
delete server.monitorList[monitorID];
}
// Delete from database
await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [
monitorID,
userID,
]);
}
/**
* Recursively delete a monitor and all its descendants
* @param {number} monitorID ID of the monitor to delete
* @param {number} userID ID of the user who owns the monitor
* @returns {Promise<void>}
*/
static async deleteMonitorRecursively(monitorID, userID) {
// Check if this monitor is a group
const monitor = await R.findOne("monitor", " id = ? AND user_id = ? ", [
monitorID,
userID,
]);
if (monitor && monitor.type === "group") {
// Get all children and delete them recursively
const children = await Monitor.getChildren(monitorID);
if (children && children.length > 0) {
for (const child of children) {
await Monitor.deleteMonitorRecursively(child.id, userID);
}
}
}
// Delete the monitor itself
await Monitor.deleteMonitor(monitorID, userID);
}
/**
* Checks recursive if parent (ancestors) are active
* @param {number} monitorID ID of the monitor to get
@@ -1723,7 +1796,7 @@ class Monitor extends BeanModel {
*/
async makeOidcTokenClientCredentialsRequest() {
log.debug("monitor", `[${this.name}] The oauth access-token undefined or expired. Requesting a new token`);
const oAuthAccessToken = await getOidcTokenClientCredentials(this.oauth_token_url, this.oauth_client_id, this.oauth_client_secret, this.oauth_scopes, this.oauth_auth_method);
const oAuthAccessToken = await getOidcTokenClientCredentials(this.oauth_token_url, this.oauth_client_id, this.oauth_client_secret, this.oauth_scopes, this.oauth_audience, this.oauth_auth_method);
if (this.oauthAccessToken?.expires_at) {
log.debug("monitor", `[${this.name}] Obtained oauth access-token. Expires at ${new Date(this.oauthAccessToken?.expires_at * 1000)}`);
} else {
+4 -2
View File
@@ -120,8 +120,8 @@ class StatusPage extends BeanModel {
const head = $("head");
if (statusPage.googleAnalyticsTagId) {
let escapedGoogleAnalyticsScript = googleAnalytics.getGoogleAnalyticsScript(statusPage.googleAnalyticsTagId);
if (statusPage.google_analytics_tag_id) {
let escapedGoogleAnalyticsScript = googleAnalytics.getGoogleAnalyticsScript(statusPage.google_analytics_tag_id);
head.append($(escapedGoogleAnalyticsScript));
}
@@ -409,6 +409,7 @@ class StatusPage extends BeanModel {
showPoweredBy: !!this.show_powered_by,
googleAnalyticsId: this.google_analytics_tag_id,
showCertificateExpiry: !!this.show_certificate_expiry,
showOnlyLastHeartbeat: !!this.show_only_last_heartbeat
};
}
@@ -432,6 +433,7 @@ class StatusPage extends BeanModel {
showPoweredBy: !!this.show_powered_by,
googleAnalyticsId: this.google_analytics_tag_id,
showCertificateExpiry: !!this.show_certificate_expiry,
showOnlyLastHeartbeat: !!this.show_only_last_heartbeat
};
}
+2 -2
View File
@@ -14,7 +14,7 @@ class User extends BeanModel {
*/
static async resetPassword(userID, newPassword) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
passwordHash.generate(newPassword),
await passwordHash.generate(newPassword),
userID
]);
}
@@ -25,7 +25,7 @@ class User extends BeanModel {
* @returns {Promise<void>}
*/
async resetPassword(newPassword) {
const hashedPassword = passwordHash.generate(newPassword);
const hashedPassword = await passwordHash.generate(newPassword);
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
hashedPassword,
+1 -1
View File
@@ -82,7 +82,7 @@ function createNTLMv2Response(type2message, username, ntlmhash, nonce, targetNam
//reserved
buf.writeUInt32LE(0, 20);
//timestamp
//TODO: we are loosing precision here since js is not able to handle those large integers
//TODO: we are losing 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);
+5 -2
View File
@@ -89,6 +89,9 @@ function NtlmClient(credentials, AxiosConfig) {
switch (_b.label) {
case 0:
error = err.response;
// The header may look like this: `Negotiate, NTLM, Basic realm="itsahiddenrealm.example.net"`Add commentMore actions
// so extract the 'NTLM' part first
const ntlmheader = error.headers['www-authenticate'].split(',').find(_ => _.match(/ *NTLM/))?.trim() || '';
if (!(error && error.status === 401
&& error.headers['www-authenticate']
&& error.headers['www-authenticate'].includes('NTLM'))) return [3 /*break*/, 3];
@@ -96,12 +99,12 @@ function NtlmClient(credentials, AxiosConfig) {
// 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) {
if (ntlmheader.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]);
t2Msg = ntlm.decodeType2Message((ntlmheader.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;
+81
View File
@@ -0,0 +1,81 @@
const { UP, PENDING, DOWN } = require("../../src/util");
const { MonitorType } = require("./monitor-type");
const Monitor = require("../model/monitor");
class GroupMonitorType extends MonitorType {
name = "group";
/**
* @inheritdoc
*/
async check(monitor, heartbeat, _server) {
const children = await Monitor.getChildren(monitor.id);
if (children.length === 0) {
// Set status pending if group is empty
heartbeat.status = PENDING;
heartbeat.msg = "Group empty";
return;
}
let worstStatus = UP;
const downChildren = [];
const pendingChildren = [];
for (const child of children) {
if (!child.active) {
// Ignore inactive (=paused) children
continue;
}
const label = child.name || `#${child.id}`;
const lastBeat = await Monitor.getPreviousHeartbeat(child.id);
if (!lastBeat) {
if (worstStatus === UP) {
worstStatus = PENDING;
}
pendingChildren.push(label);
continue;
}
if (lastBeat.status === DOWN) {
worstStatus = DOWN;
downChildren.push(label);
} else if (lastBeat.status === PENDING) {
if (worstStatus !== DOWN) {
worstStatus = PENDING;
}
pendingChildren.push(label);
}
}
if (worstStatus === UP) {
heartbeat.status = UP;
heartbeat.msg = "All children up and running";
return;
}
if (worstStatus === PENDING) {
heartbeat.status = PENDING;
heartbeat.msg = `Pending child monitors: ${pendingChildren.join(", ")}`;
return;
}
heartbeat.status = DOWN;
let message = `Child monitors down: ${downChildren.join(", ")}`;
if (pendingChildren.length > 0) {
message += `; pending: ${pendingChildren.join(", ")}`;
}
// Throw to leverage the generic retry handling and notification flow
throw new Error(message);
}
}
module.exports = {
GroupMonitorType,
};
+36
View File
@@ -0,0 +1,36 @@
const { MonitorType } = require("./monitor-type");
const { UP, DOWN, PENDING } = require("../../src/util");
class ManualMonitorType extends MonitorType {
name = "Manual";
type = "manual";
description = "A monitor that allows manual control of the status";
supportsConditions = false;
conditionVariables = [];
/**
* @inheritdoc
*/
async check(monitor, heartbeat) {
if (monitor.manual_status !== null) {
heartbeat.status = monitor.manual_status;
switch (monitor.manual_status) {
case UP:
heartbeat.msg = "Up";
break;
case DOWN:
heartbeat.msg = "Down";
break;
default:
heartbeat.msg = "Pending";
}
} else {
heartbeat.status = PENDING;
heartbeat.msg = "Manual monitoring - No status set";
}
}
}
module.exports = {
ManualMonitorType
};
+17 -10
View File
@@ -10,11 +10,12 @@ class MqttMonitorType extends MonitorType {
* @inheritdoc
*/
async check(monitor, heartbeat, server) {
const receivedMessage = await this.mqttAsync(monitor.hostname, monitor.mqttTopic, {
const [ messageTopic, receivedMessage ] = await this.mqttAsync(monitor.hostname, monitor.mqttTopic, {
port: monitor.port,
username: monitor.mqttUsername,
password: monitor.mqttPassword,
interval: monitor.interval,
websocketPath: monitor.mqttWebsocketPath,
});
if (monitor.mqttCheckType == null || monitor.mqttCheckType === "") {
@@ -24,7 +25,7 @@ class MqttMonitorType extends MonitorType {
if (monitor.mqttCheckType === "keyword") {
if (receivedMessage != null && receivedMessage.includes(monitor.mqttSuccessMessage)) {
heartbeat.msg = `Topic: ${monitor.mqttTopic}; Message: ${receivedMessage}`;
heartbeat.msg = `Topic: ${messageTopic}; Message: ${receivedMessage}`;
heartbeat.status = UP;
} else {
throw Error(`Message Mismatch - Topic: ${monitor.mqttTopic}; Message: ${receivedMessage}`);
@@ -52,12 +53,12 @@ class MqttMonitorType extends MonitorType {
* @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)
* password, websocketPath 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;
const { port, username, password, websocketPath, interval = 20 } = options;
// Adds MQTT protocol to the hostname if not already present
if (!/^(?:http|mqtt|ws)s?:\/\//.test(hostname)) {
@@ -70,7 +71,15 @@ class MqttMonitorType extends MonitorType {
reject(new Error("Timeout, Message not received"));
}, interval * 1000 * 0.8);
const mqttUrl = `${hostname}:${port}`;
// Construct the URL based on protocol
let mqttUrl = `${hostname}:${port}`;
if (hostname.startsWith("ws://") || hostname.startsWith("wss://")) {
if (websocketPath && !websocketPath.startsWith("/")) {
mqttUrl = `${hostname}:${port}/${websocketPath || ""}`;
} else {
mqttUrl = `${hostname}:${port}${websocketPath || ""}`;
}
}
log.debug("mqtt", `MQTT connecting to ${mqttUrl}`);
@@ -101,11 +110,9 @@ class MqttMonitorType extends MonitorType {
});
client.on("message", (messageTopic, message) => {
if (messageTopic === topic) {
client.end();
clearTimeout(timeoutID);
resolve(message.toString("utf8"));
}
client.end();
clearTimeout(timeoutID);
resolve([ messageTopic, message.toString("utf8") ]);
});
});
@@ -2,13 +2,13 @@ const { MonitorType } = require("./monitor-type");
const { chromium } = require("playwright-core");
const { UP, log } = require("../../src/util");
const { Settings } = require("../settings");
const commandExistsSync = require("command-exists").sync;
const childProcess = require("child_process");
const path = require("path");
const Database = require("../database");
const jwt = require("jsonwebtoken");
const config = require("../config");
const { RemoteBrowser } = require("../remote-browser");
const { commandExists } = require("../util-server");
/**
* Cached instance of a browser
@@ -122,7 +122,7 @@ async function prepareChromeExecutable(executablePath) {
executablePath = "/usr/bin/chromium";
// Install chromium in container via apt install
if ( !commandExistsSync(executablePath)) {
if (! await commandExists(executablePath)) {
await new Promise((resolve, reject) => {
log.info("Chromium", "Installing Chromium...");
let child = childProcess.exec("apt update && apt --yes --no-install-recommends install chromium fonts-indic fonts-noto fonts-noto-cjk");
@@ -146,7 +146,7 @@ async function prepareChromeExecutable(executablePath) {
}
} else {
executablePath = findChrome(allowedList);
executablePath = await findChrome(allowedList);
}
} else {
// User specified a path
@@ -160,20 +160,20 @@ async function prepareChromeExecutable(executablePath) {
/**
* Find the chrome executable
* @param {any[]} executables Executables to search through
* @returns {any} Executable
* @throws Could not find executable
* @param {string[]} executables Executables to search through
* @returns {Promise<string>} Executable
* @throws {Error} Could not find executable
*/
function findChrome(executables) {
async function findChrome(executables) {
// Use the last working executable, so we don't have to search for it again
if (lastAutoDetectChromeExecutable) {
if (commandExistsSync(lastAutoDetectChromeExecutable)) {
if (await commandExists(lastAutoDetectChromeExecutable)) {
return lastAutoDetectChromeExecutable;
}
}
for (let executable of executables) {
if (commandExistsSync(executable)) {
if (await commandExists(executable)) {
lastAutoDetectChromeExecutable = executable;
return executable;
}
+57
View File
@@ -0,0 +1,57 @@
const { MonitorType } = require("./monitor-type");
const { UP } = require("../../src/util");
const redis = require("redis");
class RedisMonitorType extends MonitorType {
name = "redis";
/**
* @inheritdoc
*/
async check(monitor, heartbeat, _server) {
heartbeat.msg = await this.redisPingAsync(monitor.databaseConnectionString, !monitor.ignoreTls);
heartbeat.status = UP;
}
/**
* Redis server ping
* @param {string} dsn The redis connection string
* @param {boolean} rejectUnauthorized If false, allows unverified server certificates.
* @returns {Promise<any>} Response from redis server
*/
redisPingAsync(dsn, rejectUnauthorized) {
return new Promise((resolve, reject) => {
const client = redis.createClient({
url: dsn,
socket: {
rejectUnauthorized
}
});
client.on("error", (err) => {
if (client.isOpen) {
client.disconnect();
}
reject(err);
});
client.connect().then(() => {
if (!client.isOpen) {
client.emit("error", new Error("connection isn't open"));
}
client.ping().then((res, err) => {
if (client.isOpen) {
client.disconnect();
}
if (err) {
reject(err);
} else {
resolve(res);
}
}).catch(error => reject(error));
});
});
}
}
module.exports = {
RedisMonitorType,
};
+35
View File
@@ -0,0 +1,35 @@
const { MonitorType } = require("./monitor-type");
const { UP } = require("../../src/util");
const nodemailer = require("nodemailer");
class SMTPMonitorType extends MonitorType {
name = "smtp";
/**
* @inheritdoc
*/
async check(monitor, heartbeat, _server) {
let options = {
port: monitor.port || 25,
host: monitor.hostname,
secure: monitor.smtpSecurity === "secure", // use SMTPS (not STARTTLS)
ignoreTLS: monitor.smtpSecurity === "nostarttls", // don't use STARTTLS even if it's available
requireTLS: monitor.smtpSecurity === "starttls", // use STARTTLS or fail
};
let transporter = nodemailer.createTransport(options);
try {
await transporter.verify();
heartbeat.status = UP;
heartbeat.msg = "SMTP connection verifies successfully";
} catch (e) {
throw new Error(`SMTP connection doesn't verify: ${e}`);
} finally {
transporter.close();
}
}
}
module.exports = {
SMTPMonitorType,
};
+1 -1
View File
@@ -31,7 +31,7 @@ class TailscalePing extends MonitorType {
timeout: timeout,
encoding: "utf8",
});
if (res.stderr && res.stderr.toString()) {
if (res.stderr && res.stderr.toString() && res.code !== 0) {
throw new Error(`Error in output: ${res.stderr.toString()}`);
}
if (res.stdout && res.stdout.toString()) {
+158
View File
@@ -0,0 +1,158 @@
const { MonitorType } = require("./monitor-type");
const { UP, DOWN, PING_GLOBAL_TIMEOUT_DEFAULT: TIMEOUT, log } = require("../../src/util");
const { checkCertificate } = require("../util-server");
const tls = require("tls");
const net = require("net");
const tcpp = require("tcp-ping");
/**
* Send TCP request to specified hostname and port
* @param {string} hostname Hostname / address of machine
* @param {number} port TCP port to test
* @returns {Promise<number>} Maximum time in ms rounded to nearest integer
*/
const tcping = (hostname, port) => {
return new Promise((resolve, reject) => {
tcpp.ping(
{
address: hostname,
port: port,
attempts: 1,
},
(err, data) => {
if (err) {
reject(err);
}
if (data.results.length >= 1 && data.results[0].err) {
reject(data.results[0].err);
}
resolve(Math.round(data.max));
}
);
});
};
class TCPMonitorType extends MonitorType {
name = "port";
/**
* @inheritdoc
*/
async check(monitor, heartbeat, _server) {
try {
const resp = await tcping(monitor.hostname, monitor.port);
heartbeat.ping = resp;
heartbeat.msg = `${resp} ms`;
heartbeat.status = UP;
} catch {
heartbeat.status = DOWN;
heartbeat.msg = "Connection failed";
return;
}
let socket_;
const preTLS = () =>
new Promise((resolve, reject) => {
let timeout;
socket_ = net.connect(monitor.port, monitor.hostname);
const onTimeout = () => {
log.debug(this.name, `[${monitor.name}] Pre-TLS connection timed out`);
reject("Connection timed out");
};
socket_.on("connect", () => {
log.debug(this.name, `[${monitor.name}] Pre-TLS connection: ${JSON.stringify(socket_)}`);
});
socket_.on("data", data => {
const response = data.toString();
const response_ = response.toLowerCase();
log.debug(this.name, `[${monitor.name}] Pre-TLS response: ${response}`);
switch (true) {
case response_.includes("start tls") || response_.includes("begin tls"):
timeout && clearTimeout(timeout);
resolve({ socket: socket_ });
break;
case response.startsWith("* OK") || response.match(/CAPABILITY.+STARTTLS/):
socket_.write("a001 STARTTLS\r\n");
break;
case response.startsWith("220") || response.includes("ESMTP"):
socket_.write(`EHLO ${monitor.hostname}\r\n`);
break;
case response.includes("250-STARTTLS"):
socket_.write("STARTTLS\r\n");
break;
default:
reject(`Unexpected response: ${response}`);
}
});
socket_.on("error", error => {
log.debug(this.name, `[${monitor.name}] ${error.toString()}`);
reject(error);
});
socket_.setTimeout(1000 * TIMEOUT, onTimeout);
timeout = setTimeout(onTimeout, 1000 * TIMEOUT);
});
const reuseSocket = monitor.smtpSecurity === "starttls" ? await preTLS() : {};
if ([ "secure", "starttls" ].includes(monitor.smtpSecurity) && monitor.isEnabledExpiryNotification()) {
let socket = null;
try {
const options = {
host: monitor.hostname,
port: monitor.port,
servername: monitor.hostname,
...reuseSocket,
};
const tlsInfoObject = await new Promise((resolve, reject) => {
socket = tls.connect(options);
socket.on("secureConnect", () => {
try {
const info = checkCertificate(socket);
resolve(info);
} catch (error) {
reject(error);
}
});
socket.on("error", error => {
reject(error);
});
socket.setTimeout(1000 * TIMEOUT, () => {
reject(new Error("Connection timed out"));
});
});
await monitor.handleTlsInfo(tlsInfoObject);
if (!tlsInfoObject.valid) {
heartbeat.status = DOWN;
heartbeat.msg = "Certificate is invalid";
}
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
heartbeat.status = DOWN;
heartbeat.msg = `TLS Connection failed: ${message}`;
} finally {
if (socket && !socket.destroyed) {
socket.end();
}
}
}
if (socket_ && !socket_.destroyed) {
socket_.end();
}
}
}
module.exports = {
TCPMonitorType,
};
+3 -1
View File
@@ -17,12 +17,14 @@ class Elks extends NotificationProvider {
data.append("to", notification.elksToNumber );
data.append("message", msg);
const config = {
let config = {
headers: {
"Authorization": "Basic " + Buffer.from(`${notification.elksUsername}:${notification.elksAuthToken}`).toString("base64")
}
};
config = this.getAxiosConfigWithProxy(config);
await axios.post(url, data, config);
return okMsg;
+47
View File
@@ -0,0 +1,47 @@
const NotificationProvider = require("./notification-provider");
const { UP } = require("../../src/util");
const webpush = require("web-push");
const { setting } = require("../util-server");
class Webpush extends NotificationProvider {
name = "Webpush";
/**
* @inheritDoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
const publicVapidKey = await setting("webpushPublicVapidKey");
const privateVapidKey = await setting("webpushPrivateVapidKey");
webpush.setVapidDetails("https://github.com/louislam/uptime-kuma", publicVapidKey, privateVapidKey);
if (heartbeatJSON === null && monitorJSON === null) {
// Test message
const data = JSON.stringify({
title: "TEST",
body: `Test Alert - ${msg}`
});
await webpush.sendNotification(notification.subscription, data);
return okMsg;
}
const data = JSON.stringify({
title: heartbeatJSON["status"] === UP ? "Monitor Up" : "Monitor DOWN",
body: heartbeatJSON["status"] === UP ? `${heartbeatJSON["name"]} is DOWN` : `${heartbeatJSON["name"]} is UP`
});
await webpush.sendNotification(notification.subscription, data);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Webpush;
+2
View File
@@ -30,6 +30,8 @@ class Alerta extends NotificationProvider {
type: "exceptionAlert",
};
config = this.getAxiosConfigWithProxy(config);
if (heartbeatJSON == null) {
let postData = Object.assign({
event: "msg",
+3 -1
View File
@@ -41,7 +41,9 @@ class AlertNow extends NotificationProvider {
"event_id": eventId,
};
await axios.post(notification.alertNowWebhookURL, data);
let config = this.getAxiosConfigWithProxy({});
await axios.post(notification.alertNowWebhookURL, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
@@ -72,6 +72,8 @@ class AliyunSMS extends NotificationProvider {
data: qs.stringify(params),
};
config = this.getAxiosConfigWithProxy(config);
let result = await axios(config);
if (result.data.Message === "OK") {
return true;
+34
View File
@@ -0,0 +1,34 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Bale extends NotificationProvider {
name = "bale";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const url = "https://tapi.bale.ai";
try {
await axios.post(
`${url}/bot${notification.baleBotToken}/sendMessage`,
{
chat_id: notification.baleChatID,
text: msg
},
{
headers: {
"content-type": "application/json",
},
}
);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Bale;
+4 -3
View File
@@ -96,20 +96,21 @@ class Bark extends NotificationProvider {
*/
async postNotification(notification, title, subtitle, endpoint) {
let result;
let config = this.getAxiosConfigWithProxy({});
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}`);
result = await axios.get(`${endpoint}/${title}/${subtitle}${params}`, config);
} else {
result = await axios.post(`${endpoint}/push`, {
result = await axios.post(endpoint, {
title,
body: subtitle,
icon: barkNotificationAvatar,
sound: notification.barkSound || "telegraph", // default sound is telegraph
group: notification.barkGroup || "UptimeKuma", // default group is UptimeKuma
});
}, config);
}
this.checkResult(result);
if (result.statusText != null) {
+2 -1
View File
@@ -19,7 +19,8 @@ class Bitrix24 extends NotificationProvider {
"ATTACH[BLOCKS][0][MESSAGE]": msg
};
await axios.get(`${notification.bitrix24WebhookURL}/im.notify.system.add.json`, { params });
let config = this.getAxiosConfigWithProxy({ params });
await axios.get(`${notification.bitrix24WebhookURL}/im.notify.system.add.json`, config);
return okMsg;
} catch (error) {
+63
View File
@@ -0,0 +1,63 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Brevo extends NotificationProvider {
name = "Brevo";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
let config = {
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"api-key": notification.brevoApiKey,
},
};
config = this.getAxiosConfigWithProxy(config);
let to = [{ email: notification.brevoToEmail }];
let data = {
sender: {
email: notification.brevoFromEmail.trim(),
name: notification.brevoFromName || "Uptime Kuma"
},
to: to,
subject: notification.brevoSubject || "Notification from Your Uptime Kuma",
htmlContent: `<html><head></head><body><p>${msg.replace(/\n/g, "<br>")}</p></body></html>`
};
if (notification.brevoCcEmail) {
data.cc = notification.brevoCcEmail
.split(",")
.map((email) => ({ email: email.trim() }));
}
if (notification.brevoBccEmail) {
data.bcc = notification.brevoBccEmail
.split(",")
.map((email) => ({ email: email.trim() }));
}
let result = await axios.post(
"https://api.brevo.com/v3/smtp/email",
data,
config
);
if (result.status === 201) {
return okMsg;
} else {
throw new Error(`Unexpected status code: ${result.status}`);
}
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Brevo;
+2 -1
View File
@@ -12,7 +12,8 @@ class CallMeBot extends NotificationProvider {
try {
const url = new URL(notification.callMeBotEndpoint);
url.searchParams.set("text", msg);
await axios.get(url.toString());
let config = this.getAxiosConfigWithProxy({});
await axios.get(url.toString(), config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
+2 -1
View File
@@ -22,7 +22,8 @@ class Cellsynt extends NotificationProvider {
}
};
try {
const resp = await axios.post("https://se-1.cellsynt.net/sms.php", null, data);
let config = this.getAxiosConfigWithProxy(data);
const resp = await axios.post("https://se-1.cellsynt.net/sms.php", null, config);
if (resp.data == null ) {
throw new Error("Could not connect to Cellsynt, please try again.");
} else if (resp.data.includes("Error:")) {
@@ -29,6 +29,7 @@ class ClickSendSMS extends NotificationProvider {
}
]
};
config = this.getAxiosConfigWithProxy(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 + ".";
+17 -5
View File
@@ -11,17 +11,23 @@ class DingDing extends NotificationProvider {
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const mentionAll = notification.mentioning === "everyone";
const mobileList = notification.mentioning === "specify-mobiles" ? notification.mobileList : [];
const userList = notification.mentioning === "specify-users" ? notification.userList : [];
const finalList = [ ...mobileList || [], ...userList || [] ];
const mentionStr = finalList.length > 0 ? "\n" : "" + finalList.map(item => `@${item}`).join(" ");
try {
if (heartbeatJSON != null) {
let params = {
msgtype: "markdown",
markdown: {
title: `[${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]}`,
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n> ${heartbeatJSON["msg"]}\n> Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n> ${heartbeatJSON["msg"]}\n> Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}${mentionStr}`,
},
"at": {
"isAtAll": notification.mentioning === "everyone"
at: {
isAtAll: mentionAll,
atUserIds: userList,
atMobiles: mobileList
}
};
if (await this.sendToDingDing(notification, params)) {
@@ -31,7 +37,12 @@ class DingDing extends NotificationProvider {
let params = {
msgtype: "text",
text: {
content: msg
content: `${msg}${mentionStr}`
},
at: {
isAtAll: mentionAll,
atUserIds: userList,
atMobiles: mobileList
}
};
if (await this.sendToDingDing(notification, params)) {
@@ -60,6 +71,7 @@ class DingDing extends NotificationProvider {
url: `${notification.webHookUrl}&timestamp=${timestamp}&sign=${encodeURIComponent(this.sign(timestamp, notification.secretKey))}`,
data: JSON.stringify(params),
};
config = this.getAxiosConfigWithProxy(config);
let result = await axios(config);
if (result.data.errmsg === "ok") {
+27 -9
View File
@@ -12,24 +12,36 @@ class Discord extends NotificationProvider {
const okMsg = "Sent Successfully.";
try {
let config = this.getAxiosConfigWithProxy({});
const discordDisplayName = notification.discordUsername || "Uptime Kuma";
const webhookUrl = new URL(notification.discordWebhookUrl);
if (notification.discordChannelType === "postToThread") {
webhookUrl.searchParams.append("thread_id", notification.threadId);
}
// Check if the webhook has an avatar
let webhookHasAvatar = true;
try {
const webhookInfo = await axios.get(webhookUrl.toString(), config);
webhookHasAvatar = !!webhookInfo.data.avatar;
} catch (e) {
// If we can't verify, we assume he has an avatar to avoid forcing the default avatar
webhookHasAvatar = true;
}
// If heartbeatJSON is null, assume we're testing.
if (heartbeatJSON == null) {
let discordtestdata = {
username: discordDisplayName,
content: msg,
};
if (!webhookHasAvatar) {
discordtestdata.avatar_url = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
}
if (notification.discordChannelType === "createNewForumPost") {
discordtestdata.thread_name = notification.postName;
}
await axios.post(webhookUrl.toString(), discordtestdata);
await axios.post(webhookUrl.toString(), discordtestdata, config);
return okMsg;
}
@@ -46,10 +58,10 @@ class Discord extends NotificationProvider {
name: "Service Name",
value: monitorJSON["name"],
},
{
...(!notification.disableUrl ? [{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: this.extractAddress(monitorJSON),
},
}] : []),
{
name: `Time (${heartbeatJSON["timezone"]})`,
value: heartbeatJSON["localDateTime"],
@@ -61,6 +73,9 @@ class Discord extends NotificationProvider {
],
}],
};
if (!webhookHasAvatar) {
discorddowndata.avatar_url = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
}
if (notification.discordChannelType === "createNewForumPost") {
discorddowndata.thread_name = notification.postName;
}
@@ -68,7 +83,7 @@ class Discord extends NotificationProvider {
discorddowndata.content = notification.discordPrefixMessage;
}
await axios.post(webhookUrl.toString(), discorddowndata);
await axios.post(webhookUrl.toString(), discorddowndata, config);
return okMsg;
} else if (heartbeatJSON["status"] === UP) {
@@ -83,10 +98,10 @@ class Discord extends NotificationProvider {
name: "Service Name",
value: monitorJSON["name"],
},
{
...(!notification.disableUrl ? [{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: this.extractAddress(monitorJSON),
},
}] : []),
{
name: `Time (${heartbeatJSON["timezone"]})`,
value: heartbeatJSON["localDateTime"],
@@ -98,6 +113,9 @@ class Discord extends NotificationProvider {
],
}],
};
if (!webhookHasAvatar) {
discordupdata.avatar_url = "https://github.com/louislam/uptime-kuma/raw/master/public/icon.png";
}
if (notification.discordChannelType === "createNewForumPost") {
discordupdata.thread_name = notification.postName;
@@ -107,7 +125,7 @@ class Discord extends NotificationProvider {
discordupdata.content = notification.discordPrefixMessage;
}
await axios.post(webhookUrl.toString(), discordupdata);
await axios.post(webhookUrl.toString(), discordupdata, config);
return okMsg;
}
} catch (error) {
@@ -0,0 +1,40 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Evolution extends NotificationProvider {
name = "evolution";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
let config = {
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"apikey": notification.evolutionAuthToken,
}
};
config = this.getAxiosConfigWithProxy(config);
let data = {
"number": notification.evolutionRecipient,
"text": msg,
};
let url = (notification.evolutionApiUrl || "https://evolapicloud.com/").replace(/([^/])\/+$/, "$1") + "/message/sendText/" + encodeURIComponent(notification.evolutionInstanceName);
await axios.post(url, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Evolution;
+4 -3
View File
@@ -12,6 +12,7 @@ class Feishu extends NotificationProvider {
const okMsg = "Sent Successfully.";
try {
let config = this.getAxiosConfigWithProxy({});
if (heartbeatJSON == null) {
let testdata = {
msg_type: "text",
@@ -19,7 +20,7 @@ class Feishu extends NotificationProvider {
text: msg,
},
};
await axios.post(notification.feishuWebHookUrl, testdata);
await axios.post(notification.feishuWebHookUrl, testdata, config);
return okMsg;
}
@@ -49,7 +50,7 @@ class Feishu extends NotificationProvider {
]
}
};
await axios.post(notification.feishuWebHookUrl, downdata);
await axios.post(notification.feishuWebHookUrl, downdata, config);
return okMsg;
}
@@ -79,7 +80,7 @@ class Feishu extends NotificationProvider {
]
}
};
await axios.post(notification.feishuWebHookUrl, updata);
await axios.post(notification.feishuWebHookUrl, updata, config);
return okMsg;
}
} catch (error) {
+3 -3
View File
@@ -38,7 +38,7 @@ class FlashDuty extends NotificationProvider {
}
/**
* Generate a monitor url from the monitors infomation
* Generate a monitor url from the monitors information
* @param {object} monitorInfo Monitor details
* @returns {string|undefined} Monitor URL
*/
@@ -73,13 +73,13 @@ class FlashDuty extends NotificationProvider {
}
const options = {
method: "POST",
url: "https://api.flashcat.cloud/event/push/alert/standard?integration_key=" + notification.flashdutyIntegrationKey,
url: notification.flashdutyIntegrationKey.startsWith("http") ? notification.flashdutyIntegrationKey : "https://api.flashcat.cloud/event/push/alert/standard?integration_key=" + notification.flashdutyIntegrationKey,
headers: { "Content-Type": "application/json" },
data: {
description: `[${title}] [${monitorInfo.name}] ${body}`,
title,
event_status: eventStatus || "Info",
alert_key: String(monitorInfo.id) || Math.random().toString(36).substring(7),
alert_key: monitorInfo.id ? String(monitorInfo.id) : Math.random().toString(36).substring(7),
labels,
}
};
+2 -1
View File
@@ -11,10 +11,11 @@ class FreeMobile extends NotificationProvider {
const okMsg = "Sent Successfully.";
try {
let config = this.getAxiosConfigWithProxy({});
await axios.post(`https://smsapi.free-mobile.fr/sendmsg?msg=${encodeURIComponent(msg.replace("🔴", "⛔️"))}`, {
"user": notification.freemobileUser,
"pass": notification.freemobilePass,
});
}, config);
return okMsg;
+1
View File
@@ -24,6 +24,7 @@ class GoAlert extends NotificationProvider {
let config = {
headers: headers
};
config = this.getAxiosConfigWithProxy(config);
await axios.post(`${notification.goAlertBaseURL}/api/v2/generic/incoming?token=${notification.goAlertToken}`, data, config);
return okMsg;
} catch (error) {
+37 -2
View File
@@ -12,8 +12,44 @@ class GoogleChat extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
// If Google Chat Webhook rate limit is reached, retry to configured max retries defaults to 3, delay between 60-180 seconds
const post = async (url, data, config) => {
let retries = notification.googleChatMaxRetries || 1; // Default to 1 retries
retries = (retries > 10) ? 10 : retries; // Enforce maximum retries in backend
while (retries > 0) {
try {
await axios.post(url, data, config);
return;
} catch (error) {
if (error.response && error.response.status === 429) {
retries--;
if (retries === 0) {
throw error;
}
const delay = 60000 + Math.random() * 120000;
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
};
try {
let config = this.getAxiosConfigWithProxy({});
// Google Chat message formatting: https://developers.google.com/chat/api/guides/message-formats/basic
if (notification.googleChatUseTemplate && notification.googleChatTemplate) {
// Send message using template
const renderedText = await this.renderTemplate(
notification.googleChatTemplate,
msg,
monitorJSON,
heartbeatJSON
);
const data = { "text": renderedText };
await post(notification.googleChatWebhookURL, data, config);
return okMsg;
}
let chatHeader = {
title: "Uptime Kuma Alert",
@@ -83,12 +119,11 @@ class GoogleChat extends NotificationProvider {
],
};
await axios.post(notification.googleChatWebhookURL, data);
await post(notification.googleChatWebhookURL, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
+1 -2
View File
@@ -31,8 +31,7 @@ class Gorush extends NotificationProvider {
}
]
};
let config = {};
let config = this.getAxiosConfigWithProxy({});
await axios.post(`${notification.gorushServerURL}/api/push`, data, config);
return okMsg;
} catch (error) {
+2 -1
View File
@@ -11,6 +11,7 @@ class Gotify extends NotificationProvider {
const okMsg = "Sent Successfully.";
try {
let config = this.getAxiosConfigWithProxy({});
if (notification.gotifyserverurl && notification.gotifyserverurl.endsWith("/")) {
notification.gotifyserverurl = notification.gotifyserverurl.slice(0, -1);
}
@@ -18,7 +19,7 @@ class Gotify extends NotificationProvider {
"message": msg,
"priority": notification.gotifyPriority || 8,
"title": "Uptime-Kuma",
});
}, config);
return okMsg;
@@ -16,13 +16,14 @@ class GrafanaOncall extends NotificationProvider {
}
try {
let config = this.getAxiosConfigWithProxy({});
if (heartbeatJSON === null) {
let grafanaupdata = {
title: "General notification",
message: msg,
state: "alerting",
};
await axios.post(notification.GrafanaOncallURL, grafanaupdata);
await axios.post(notification.GrafanaOncallURL, grafanaupdata, config);
return okMsg;
} else if (heartbeatJSON["status"] === DOWN) {
let grafanadowndata = {
@@ -30,7 +31,7 @@ class GrafanaOncall extends NotificationProvider {
message: heartbeatJSON["msg"],
state: "alerting",
};
await axios.post(notification.GrafanaOncallURL, grafanadowndata);
await axios.post(notification.GrafanaOncallURL, grafanadowndata, config);
return okMsg;
} else if (heartbeatJSON["status"] === UP) {
let grafanaupdata = {
@@ -38,7 +39,7 @@ class GrafanaOncall extends NotificationProvider {
message: heartbeatJSON["msg"],
state: "ok",
};
await axios.post(notification.GrafanaOncallURL, grafanaupdata);
await axios.post(notification.GrafanaOncallURL, grafanaupdata, config);
return okMsg;
}
} catch (error) {
@@ -14,6 +14,7 @@ class GtxMessaging extends NotificationProvider {
const text = msg.replaceAll("🔴 ", "").replaceAll("✅ ", "");
try {
let config = this.getAxiosConfigWithProxy({});
const data = new URLSearchParams();
data.append("from", notification.gtxMessagingFrom.trim());
data.append("to", notification.gtxMessagingTo.trim());
@@ -21,7 +22,7 @@ class GtxMessaging extends NotificationProvider {
const url = `https://rest.gtx-messaging.net/smsc/sendsms/${notification.gtxMessagingApiKey}/json`;
await axios.post(url, data);
await axios.post(url, data, config);
return okMsg;
} catch (error) {
+2 -1
View File
@@ -18,7 +18,7 @@ class HeiiOnCall extends NotificationProvider {
payload["url"] = baseURL + getMonitorRelativeURL(monitorJSON.id);
}
const config = {
let config = {
headers: {
Accept: "application/json",
"Content-Type": "application/json",
@@ -28,6 +28,7 @@ class HeiiOnCall extends NotificationProvider {
const heiiUrl = `https://heiioncall.com/triggers/${notification.heiiOnCallTriggerId}/`;
// docs https://heiioncall.com/docs#manual-triggers
try {
config = this.getAxiosConfigWithProxy(config);
if (!heartbeatJSON) {
// Testing or general notification like certificate expiry
payload["msg"] = msg;
@@ -15,6 +15,13 @@ class HomeAssistant extends NotificationProvider {
const notificationService = notification?.notificationService || defaultNotificationService;
try {
let config = {
headers: {
Authorization: `Bearer ${notification.longLivedAccessToken}`,
"Content-Type": "application/json",
},
};
config = this.getAxiosConfigWithProxy(config);
await axios.post(
`${notification.homeAssistantUrl.trim().replace(/\/*$/, "")}/api/services/notify/${notificationService}`,
{
@@ -26,14 +33,7 @@ class HomeAssistant extends NotificationProvider {
channel: "Uptime Kuma",
icon_url: "https://github.com/louislam/uptime-kuma/blob/master/public/icon.png?raw=true",
} }),
},
{
headers: {
Authorization: `Bearer ${notification.longLivedAccessToken}`,
"Content-Type": "application/json",
},
}
);
}, config);
return okMsg;
} catch (error) {
+2
View File
@@ -31,6 +31,8 @@ class Keep extends NotificationProvider {
let webhookURL = url + "/alerts/event/uptimekuma";
config = this.getAxiosConfigWithProxy(config);
await axios.post(webhookURL, data, config);
return okMsg;
} catch (error) {
+1
View File
@@ -22,6 +22,7 @@ class Kook extends NotificationProvider {
},
};
try {
config = this.getAxiosConfigWithProxy(config);
await axios.post(url, data, config);
return okMsg;
+1
View File
@@ -19,6 +19,7 @@ class Line extends NotificationProvider {
"Authorization": "Bearer " + notification.lineChannelAccessToken
}
};
config = this.getAxiosConfigWithProxy(config);
if (heartbeatJSON == null) {
let testMessage = {
"to": notification.lineUserID,
@@ -20,6 +20,7 @@ class LineNotify extends NotificationProvider {
"Authorization": "Bearer " + notification.lineNotifyAccessToken
}
};
config = this.getAxiosConfigWithProxy(config);
if (heartbeatJSON == null) {
let testMessage = {
"message": msg,
+4 -3
View File
@@ -13,13 +13,14 @@ class LunaSea extends NotificationProvider {
const url = "https://notify.lunasea.app/v1";
try {
let config = this.getAxiosConfigWithProxy({});
const target = this.getTarget(notification);
if (heartbeatJSON == null) {
let testdata = {
"title": "Uptime Kuma Alert",
"body": msg,
};
await axios.post(`${url}/custom/${target}`, testdata);
await axios.post(`${url}/custom/${target}`, testdata, config);
return okMsg;
}
@@ -30,7 +31,7 @@ class LunaSea extends NotificationProvider {
heartbeatJSON["msg"] +
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
};
await axios.post(`${url}/custom/${target}`, downdata);
await axios.post(`${url}/custom/${target}`, downdata, config);
return okMsg;
}
@@ -41,7 +42,7 @@ class LunaSea extends NotificationProvider {
heartbeatJSON["msg"] +
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
};
await axios.post(`${url}/custom/${target}`, updata);
await axios.post(`${url}/custom/${target}`, updata, config);
return okMsg;
}
+1
View File
@@ -37,6 +37,7 @@ class Matrix extends NotificationProvider {
"body": msg
};
config = this.getAxiosConfigWithProxy(config);
await axios.put(`${notification.homeserverUrl}/_matrix/client/r0/rooms/${roomId}/send/m.room.message/${randomString}`, data, config);
return okMsg;
} catch (error) {
+3 -4
View File
@@ -12,6 +12,7 @@ class Mattermost extends NotificationProvider {
const okMsg = "Sent Successfully.";
try {
let config = this.getAxiosConfigWithProxy({});
const mattermostUserName = notification.mattermostusername || "Uptime Kuma";
// If heartbeatJSON is null, assume non monitoring notification (Certificate warning) or testing.
if (heartbeatJSON == null) {
@@ -19,7 +20,7 @@ class Mattermost extends NotificationProvider {
username: mattermostUserName,
text: msg,
};
await axios.post(notification.mattermostWebhookUrl, mattermostTestData);
await axios.post(notification.mattermostWebhookUrl, mattermostTestData, config);
return okMsg;
}
@@ -79,13 +80,11 @@ class Mattermost extends NotificationProvider {
fallback:
"Your " +
monitorJSON.pathName +
monitorJSON.name +
" service went " +
statusText,
color: color,
title:
monitorJSON.pathName +
monitorJSON.name +
" service went " +
statusText,
title_link: monitorJSON.url,
@@ -100,7 +99,7 @@ class Mattermost extends NotificationProvider {
},
],
};
await axios.post(notification.mattermostWebhookUrl, mattermostdata);
await axios.post(notification.mattermostWebhookUrl, mattermostdata, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
@@ -0,0 +1,66 @@
const { UP, DOWN } = require("../../src/util");
const Crypto = require("crypto");
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class NextcloudTalk extends NotificationProvider {
name = "nextcloudtalk";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
// See documentation at https://nextcloud-talk.readthedocs.io/en/latest/bots/#sending-a-chat-message
const okMsg = "Sent Successfully.";
// Create a random string
const talkRandom = encodeURIComponent(
Crypto
.randomBytes(64)
.toString("hex")
.slice(0, 64)
);
// Create the signature over random and message
const talkSignature = Crypto
.createHmac("sha256", Buffer.from(notification.botSecret, "utf8"))
.update(Buffer.from(`${talkRandom}${msg}`, "utf8"))
.digest("hex");
let silentUp = (heartbeatJSON?.status === UP && notification.sendSilentUp);
let silentDown = (heartbeatJSON?.status === DOWN && notification.sendSilentDown);
let silent = (silentUp || silentDown);
let url = `${notification.host}/ocs/v2.php/apps/spreed/api/v1/bot/${notification.conversationToken}/message`;
let config = this.getAxiosConfigWithProxy({});
const data = {
message: msg,
silent
};
const options = {
...config,
headers: {
"X-Nextcloud-Talk-Bot-Random": talkRandom,
"X-Nextcloud-Talk-Bot-Signature": talkSignature,
"OCS-APIRequest": true,
}
};
try {
let result = await axios.post(url, data, options);
if (result?.status === 201) {
return okMsg;
}
throw new Error("Nextcloud Talk Error " + (result?.status ?? "Unknown"));
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = NextcloudTalk;
+54
View File
@@ -0,0 +1,54 @@
const { getMonitorRelativeURL, UP } = require("../../src/util");
const { setting } = require("../util-server");
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class Notifery extends NotificationProvider {
name = "notifery";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const url = "https://api.notifery.com/event";
let data = {
title: notification.notiferyTitle || "Uptime Kuma Alert",
message: msg,
};
if (notification.notiferyGroup) {
data.group = notification.notiferyGroup;
}
// Link to the monitor
const baseURL = await setting("primaryBaseURL");
if (baseURL && monitorJSON) {
data.message += `\n\nMonitor: ${baseURL}${getMonitorRelativeURL(monitorJSON.id)}`;
}
if (heartbeatJSON) {
data.code = heartbeatJSON.status === UP ? 0 : 1;
if (heartbeatJSON.ping) {
data.duration = heartbeatJSON.ping;
}
}
try {
const headers = {
"Content-Type": "application/json",
"x-api-key": notification.notiferyApiKey,
};
let config = this.getAxiosConfigWithProxy({ headers });
await axios.post(url, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Notifery;
@@ -1,5 +1,7 @@
const { Liquid } = require("liquidjs");
const { DOWN } = require("../../src/util");
const { HttpProxyAgent } = require("http-proxy-agent");
const { HttpsProxyAgent } = require("https-proxy-agent");
class NotificationProvider {
@@ -61,7 +63,11 @@ class NotificationProvider {
* @returns {Promise<string>} rendered template
*/
async renderTemplate(template, msg, monitorJSON, heartbeatJSON) {
const engine = new Liquid();
const engine = new Liquid({
root: "./no-such-directory-uptime-kuma",
relativeReference: false,
dynamicPartials: false,
});
const parsedTpl = engine.parse(template);
// Let's start with dummy values to simplify code
@@ -115,6 +121,30 @@ class NotificationProvider {
throw new Error(msg);
}
/**
* Returns axios config with proxy agent if proxy env is set.
* @param {object} axiosConfig - Axios config containing params
* @returns {object} Axios config
*/
getAxiosConfigWithProxy(axiosConfig = {}) {
const proxyEnv = process.env.notification_proxy || process.env.NOTIFICATION_PROXY;
if (proxyEnv) {
const proxyUrl = new URL(proxyEnv);
if (proxyUrl.protocol === "http:") {
axiosConfig.httpAgent = new HttpProxyAgent(proxyEnv);
axiosConfig.httpsAgent = new HttpsProxyAgent(proxyEnv);
} else if (proxyUrl.protocol === "https:") {
const agent = new HttpsProxyAgent(proxyEnv);
axiosConfig.httpAgent = agent;
axiosConfig.httpsAgent = agent;
}
axiosConfig.proxy = false;
}
return axiosConfig;
}
}
module.exports = NotificationProvider;
+6 -4
View File
@@ -22,6 +22,8 @@ class Ntfy extends NotificationProvider {
"Authorization": "Bearer " + notification.ntfyaccesstoken,
};
}
let config = { headers };
config = this.getAxiosConfigWithProxy(config);
// If heartbeatJSON is null, assume non monitoring notification (Certificate warning) or testing.
if (heartbeatJSON == null) {
let ntfyTestData = {
@@ -31,7 +33,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, config);
return okMsg;
}
let tags = [];
@@ -41,8 +43,8 @@ class Ntfy extends NotificationProvider {
if (heartbeatJSON.status === DOWN) {
tags = [ "red_circle" ];
status = "Down";
// if priority is not 5, increase priority for down alerts
priority = priority === 5 ? priority : priority + 1;
// defaults to max(priority + 1, 5)
priority = notification.ntfyPriorityDown || (priority === 5 ? priority : priority + 1);
} else if (heartbeatJSON["status"] === UP) {
tags = [ "green_circle" ];
status = "Up";
@@ -70,7 +72,7 @@ class Ntfy extends NotificationProvider {
data.icon = notification.ntfyIcon;
}
await axios.post(notification.ntfyserverurl, data, { headers: headers });
await axios.post(notification.ntfyserverurl, data, config);
return okMsg;
@@ -22,6 +22,7 @@ class Octopush extends NotificationProvider {
"cache-control": "no-cache"
}
};
config = this.getAxiosConfigWithProxy(config);
let data = {
"recipients": [
{
@@ -53,6 +54,7 @@ class Octopush extends NotificationProvider {
},
params: data
};
config = this.getAxiosConfigWithProxy(config);
// V1 API returns 200 even on error so we must check
// response data
+1
View File
@@ -25,6 +25,7 @@ class OneBot extends NotificationProvider {
"Authorization": "Bearer " + notification.accessToken,
}
};
config = this.getAxiosConfigWithProxy(config);
let pushText = "UptimeKuma Alert: " + msg;
let data = {
"auto_escape": true,
+74
View File
@@ -0,0 +1,74 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class OneChat extends NotificationProvider {
name = "OneChat";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const url = "https://chat-api.one.th/message/api/v1/push_message";
try {
let config = {
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + notification.accessToken,
},
};
config = this.getAxiosConfigWithProxy(config);
if (heartbeatJSON == null) {
const testMessage = {
to: notification.recieverId,
bot_id: notification.botId,
type: "text",
message: "Test Successful!",
};
await axios.post(url, testMessage, config);
} else if (heartbeatJSON["status"] === DOWN) {
const downMessage = {
to: notification.recieverId,
bot_id: notification.botId,
type: "text",
message:
`UptimeKuma Alert:
[🔴 Down]
Name: ${monitorJSON["name"]}
${heartbeatJSON["msg"]}
Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
};
await axios.post(url, downMessage, config);
} else if (heartbeatJSON["status"] === UP) {
const upMessage = {
to: notification.recieverId,
bot_id: notification.botId,
type: "text",
message:
`UptimeKuma Alert:
[🟢 Up]
Name: ${monitorJSON["name"]}
${heartbeatJSON["msg"]}
Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
};
await axios.post(url, upMessage, config);
}
return okMsg;
} catch (error) {
// Handle errors and throw a descriptive message
if (error.response) {
const errorMessage =
error.response.data?.message ||
"Unknown API error occurred.";
throw new Error(`OneChat API Error: ${errorMessage}`);
} else {
this.throwGeneralAxiosError(error);
}
}
}
}
module.exports = OneChat;
@@ -33,6 +33,7 @@ class Onesender extends NotificationProvider {
"Authorization": "Bearer " + notification.onesenderToken,
}
};
config = this.getAxiosConfigWithProxy(config);
await axios.post(notification.onesenderURL, data, config);
return okMsg;
@@ -80,6 +80,7 @@ class Opsgenie extends NotificationProvider {
"Authorization": `GenieKey ${notification.opsgenieApiKey}`,
}
};
config = this.getAxiosConfigWithProxy(config);
let res = await axios.post(url, data, config);
if (res.status == null) {
+8 -7
View File
@@ -23,9 +23,7 @@ class PagerDuty extends NotificationProvider {
if (heartbeatJSON.status === UP) {
const title = "Uptime Kuma Monitor ✅ Up";
const eventAction = notification.pagerdutyAutoResolve || null;
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, eventAction);
return this.postNotification(notification, title, heartbeatJSON.msg, monitorJSON, "resolve");
}
if (heartbeatJSON.status === DOWN) {
@@ -63,10 +61,6 @@ class PagerDuty extends NotificationProvider {
*/
async postNotification(notification, title, body, monitorInfo, eventAction = "trigger") {
if (eventAction == null) {
return "No action required";
}
let monitorUrl;
if (monitorInfo.type === "port") {
monitorUrl = monitorInfo.hostname;
@@ -79,6 +73,13 @@ class PagerDuty extends NotificationProvider {
monitorUrl = monitorInfo.url;
}
if (eventAction === "resolve") {
if (notification.pagerdutyAutoResolve === "0") {
return "no action required";
}
eventAction = notification.pagerdutyAutoResolve;
}
const options = {
method: "POST",
url: notification.pagerdutyIntegrationUrl,
+2 -1
View File
@@ -15,7 +15,7 @@ class PromoSMS extends NotificationProvider {
notification.promosmsAllowLongSMS = false;
}
//TODO: Add option for enabling special characters. It will decrese message max length from 160 to 70 chars.
//TODO: Add option for enabling special characters. It will decrease message max length from 160 to 70 chars.
//Lets remove non ascii char
let cleanMsg = msg.replace(/[^\x00-\x7F]/g, "");
@@ -27,6 +27,7 @@ class PromoSMS extends NotificationProvider {
"Accept": "text/json",
}
};
config = this.getAxiosConfigWithProxy(config);
let data = {
"recipients": [ notification.promosmsPhoneNumber ],
//Trim message to maximum length of 1 SMS or 4 if we allowed long messages
+49
View File
@@ -0,0 +1,49 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { UP } = require("../../src/util");
class Pumble extends NotificationProvider {
name = "pumble";
/**
* @inheritDoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
try {
let config = this.getAxiosConfigWithProxy({});
if (heartbeatJSON === null && monitorJSON === null) {
let data = {
"attachments": [
{
"title": "Uptime Kuma Alert",
"text": msg,
"color": "#5BDD8B"
}
]
};
await axios.post(notification.webhookURL, data, config);
return okMsg;
}
let data = {
"attachments": [
{
"title": `${monitorJSON["name"]} is ${heartbeatJSON["status"] === UP ? "up" : "down"}`,
"text": heartbeatJSON["msg"],
"color": (heartbeatJSON["status"] === UP ? "#5BDD8B" : "#DC3645"),
}
]
};
await axios.post(notification.webhookURL, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = Pumble;
@@ -20,6 +20,7 @@ class Pushbullet extends NotificationProvider {
"Content-Type": "application/json"
}
};
config = this.getAxiosConfigWithProxy(config);
if (heartbeatJSON == null) {
let data = {
"type": "note",
+3 -2
View File
@@ -11,7 +11,7 @@ class PushDeer extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const serverUrl = notification.pushdeerServer || "https://api2.pushdeer.com";
// capture group below is nessesary to prevent an ReDOS-attack
// capture group below is necessary to prevent an ReDOS-attack
const url = `${serverUrl.trim().replace(/([^/])\/+$/, "$1")}/message/push`;
let valid = msg != null && monitorJSON != null && heartbeatJSON != null;
@@ -33,7 +33,8 @@ class PushDeer extends NotificationProvider {
};
try {
let res = await axios.post(url, data);
let config = this.getAxiosConfigWithProxy({});
let res = await axios.post(url, data, config);
if ("error" in res.data) {
let error = res.data.error;
+13 -6
View File
@@ -1,5 +1,6 @@
const { getMonitorRelativeURL } = require("../../src/util");
const { setting } = require("../util-server");
const { UP } = require("../../src/util");
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
@@ -40,18 +41,24 @@ class Pushover extends NotificationProvider {
}
try {
let config = this.getAxiosConfigWithProxy({});
if (heartbeatJSON == null) {
await axios.post(url, data);
return okMsg;
} else {
data.message += `\n<b>Time (${heartbeatJSON["timezone"]})</b>:${heartbeatJSON["localDateTime"]}`;
await axios.post(url, data);
await axios.post(url, data, config);
return okMsg;
}
if (heartbeatJSON.status === UP && notification.pushoversounds_up) {
// default = DOWN => DOWN-sound is also played for non-UP/DOWN notiifcations
data.sound = notification.pushoversounds_up;
}
data.message += `\n<b>Time (${heartbeatJSON["timezone"]})</b>: ${heartbeatJSON["localDateTime"]}`;
await axios.post(url, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
+2 -1
View File
@@ -17,11 +17,12 @@ class PushPlus extends NotificationProvider {
const okMsg = "Sent Successfully.";
const url = "https://www.pushplus.plus/send";
try {
const config = {
let config = {
headers: {
"Content-Type": "application/json",
},
};
config = this.getAxiosConfigWithProxy(config);
const params = {
"token": notification.pushPlusSendKey,
"title": this.checkStatus(heartbeatJSON, monitorJSON),
+2 -1
View File
@@ -11,6 +11,7 @@ class Pushy extends NotificationProvider {
const okMsg = "Sent Successfully.";
try {
let config = this.getAxiosConfigWithProxy({});
await axios.post(`https://api.pushy.me/push?api_key=${notification.pushyAPIKey}`, {
"to": notification.pushyToken,
"data": {
@@ -21,7 +22,7 @@ class Pushy extends NotificationProvider {
"badge": 1,
"sound": "ping.aiff"
}
});
}, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
+3 -2
View File
@@ -14,6 +14,7 @@ class RocketChat extends NotificationProvider {
const okMsg = "Sent Successfully.";
try {
let config = this.getAxiosConfigWithProxy({});
if (heartbeatJSON == null) {
let data = {
"text": msg,
@@ -21,7 +22,7 @@ class RocketChat extends NotificationProvider {
"username": notification.rocketusername,
"icon_emoji": notification.rocketiconemo,
};
await axios.post(notification.rocketwebhookURL, data);
await axios.post(notification.rocketwebhookURL, data, config);
return okMsg;
}
@@ -55,7 +56,7 @@ class RocketChat extends NotificationProvider {
data.attachments[0].title_link = baseURL + getMonitorRelativeURL(monitorJSON.id);
}
await axios.post(notification.rocketwebhookURL, data);
await axios.post(notification.rocketwebhookURL, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
+1 -1
View File
@@ -17,7 +17,7 @@ class SendGrid extends NotificationProvider {
Authorization: `Bearer ${notification.sendgridApiKey}`,
},
};
config = this.getAxiosConfigWithProxy(config);
let personalizations = {
to: [{ email: notification.sendgridToEmail }],
};
+2 -1
View File
@@ -18,10 +18,11 @@ class ServerChan extends NotificationProvider {
: `https://sctapi.ftqq.com/${notification.serverChanSendKey}.send`;
try {
let config = this.getAxiosConfigWithProxy({});
await axios.post(url, {
"title": this.checkStatus(heartbeatJSON, monitorJSON),
"desp": msg,
});
}, config);
return okMsg;
@@ -17,6 +17,7 @@ class SerwerSMS extends NotificationProvider {
"Content-Type": "application/json",
}
};
config = this.getAxiosConfigWithProxy(config);
let data = {
"username": notification.serwersmsUsername,
"password": notification.serwersmsPassword,
+3 -2
View File
@@ -12,12 +12,12 @@ class SevenIO extends NotificationProvider {
const okMsg = "Sent Successfully.";
const data = {
to: notification.sevenioTo,
to: notification.sevenioReceiver,
from: notification.sevenioSender || "Uptime Kuma",
text: msg,
};
const config = {
let config = {
baseURL: "https://gateway.seven.io/api/",
headers: {
"Content-Type": "application/json",
@@ -26,6 +26,7 @@ class SevenIO extends NotificationProvider {
};
try {
config = this.getAxiosConfigWithProxy(config);
// testing or certificate expiry notification
if (heartbeatJSON == null) {
await axios.post("sms", data, config);
+1 -1
View File
@@ -17,7 +17,7 @@ class Signal extends NotificationProvider {
"recipients": notification.signalRecipients.replace(/\s/g, "").split(","),
};
let config = {};
config = this.getAxiosConfigWithProxy(config);
await axios.post(notification.signalURL, data, config);
return okMsg;
} catch (error) {
+2 -1
View File
@@ -21,11 +21,12 @@ class SIGNL4 extends NotificationProvider {
monitorUrl: this.extractAddress(monitorJSON),
};
const config = {
let config = {
headers: {
"Content-Type": "application/json"
}
};
config = this.getAxiosConfigWithProxy(config);
if (heartbeatJSON == null) {
// Test alert
+6 -3
View File
@@ -2,6 +2,7 @@ const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { setSettings, setting } = require("../util-server");
const { getMonitorRelativeURL, UP, log } = require("../../src/util");
const isUrl = require("is-url");
class Slack extends NotificationProvider {
name = "slack";
@@ -49,7 +50,7 @@ class Slack extends NotificationProvider {
}
const address = this.extractAddress(monitorJSON);
if (address) {
if (isUrl(address)) {
try {
actions.push({
"type": "button",
@@ -130,6 +131,7 @@ class Slack extends NotificationProvider {
}
try {
let config = this.getAxiosConfigWithProxy({});
if (heartbeatJSON == null) {
let data = {
"text": msg,
@@ -137,7 +139,7 @@ class Slack extends NotificationProvider {
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
};
await axios.post(notification.slackwebhookURL, data);
await axios.post(notification.slackwebhookURL, data, config);
return okMsg;
}
@@ -145,6 +147,7 @@ class Slack extends NotificationProvider {
const title = "Uptime Kuma Alert";
let data = {
"text": msg,
"channel": notification.slackchannel,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
@@ -166,7 +169,7 @@ class Slack extends NotificationProvider {
await Slack.deprecateURL(notification.slackbutton);
}
await axios.post(notification.slackwebhookURL, data);
await axios.post(notification.slackwebhookURL, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
@@ -0,0 +1,41 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class SMSPlanet extends NotificationProvider {
name = "SMSPlanet";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const url = "https://api2.smsplanet.pl/sms";
try {
let config = {
headers: {
"Authorization": "Bearer " + notification.smsplanetApiToken,
"content-type": "multipart/form-data"
}
};
config = this.getAxiosConfigWithProxy(config);
let data = {
"from": notification.smsplanetSenderName,
"to": notification.smsplanetPhoneNumbers,
"msg": msg.replace(/🔴/, "❌")
};
let response = await axios.post(url, data, config);
if (!response.data?.messageId) {
throw new Error(response.data?.errorMsg ?? "SMSPlanet server did not respond with the expected result");
}
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = SMSPlanet;
+1
View File
@@ -18,6 +18,7 @@ class SMSC extends NotificationProvider {
"Accept": "text/json",
}
};
config = this.getAxiosConfigWithProxy(config);
let getArray = [
"fmt=3",
+117 -47
View File
@@ -11,59 +11,129 @@ class SMSEagle extends NotificationProvider {
const okMsg = "Sent Successfully.";
try {
let config = {
headers: {
"Content-Type": "application/json",
if (notification.smseagleApiType === "smseagle-apiv1") { // according to https://www.smseagle.eu/apiv1/
let config = {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
}
};
config = this.getAxiosConfigWithProxy(config);
let sendMethod;
let recipientType;
let duration;
let voiceId;
if (notification.smseagleRecipientType === "smseagle-contact") {
recipientType = "contactname";
sendMethod = "/send_tocontact";
} else if (notification.smseagleRecipientType === "smseagle-group") {
recipientType = "groupname";
sendMethod = "/send_togroup";
} else if (notification.smseagleRecipientType === "smseagle-to") {
recipientType = "to";
sendMethod = "/send_sms";
if (notification.smseagleMsgType !== "smseagle-sms") {
duration = notification.smseagleDuration ?? 10;
if (notification.smseagleMsgType === "smseagle-ring") {
sendMethod = "/ring_call";
} else if (notification.smseagleMsgType === "smseagle-tts") {
sendMethod = "/tts_call";
} else if (notification.smseagleMsgType === "smseagle-tts-advanced") {
sendMethod = "/tts_adv_call";
voiceId = notification.smseagleTtsModel ? notification.smseagleTtsModel : 1;
}
}
}
};
let postData;
let sendMethod;
let recipientType;
const url = new URL(notification.smseagleUrl + "/http_api" + sendMethod);
let encoding = (notification.smseagleEncoding) ? "1" : "0";
let priority = (notification.smseaglePriority) ? notification.smseaglePriority : "0";
if (notification.smseagleRecipientType === "smseagle-contact") {
recipientType = "contactname";
sendMethod = "sms.send_tocontact";
}
if (notification.smseagleRecipientType === "smseagle-group") {
recipientType = "groupname";
sendMethod = "sms.send_togroup";
}
if (notification.smseagleRecipientType === "smseagle-to") {
recipientType = "to";
sendMethod = "sms.send_sms";
}
let params = {
access_token: notification.smseagleToken,
[recipientType]: notification.smseagleRecipient,
message: msg,
responsetype: "extended",
unicode: encoding,
highpriority: priority
};
postData = {
method: sendMethod,
params: params
};
let resp = await axios.post(notification.smseagleUrl + "/jsonrpc/sms", postData, config);
if ((JSON.stringify(resp.data)).indexOf("message_id") === -1) {
let error = "";
if (resp.data.result && resp.data.result.error_text) {
error = `SMSEagle API returned error: ${JSON.stringify(resp.data.result.error_text)}`;
url.searchParams.append("access_token", notification.smseagleToken);
url.searchParams.append(recipientType, notification.smseagleRecipient);
if (!notification.smseagleRecipientType || notification.smseagleRecipientType === "smseagle-sms") {
url.searchParams.append("unicode", (notification.smseagleEncoding) ? "1" : "0");
url.searchParams.append("highpriority", notification.smseaglePriority ?? "0");
} else {
error = "SMSEagle API returned an unexpected response";
url.searchParams.append("duration", duration);
}
if (notification.smseagleRecipientType !== "smseagle-ring") {
url.searchParams.append("message", msg);
}
if (voiceId) {
url.searchParams.append("voice_id", voiceId);
}
throw new Error(error);
}
return okMsg;
let resp = await axios.get(url.toString(), config);
if (resp.data.indexOf("OK") === -1) {
let error = `SMSEagle API returned error: ${resp.data}`;
throw new Error(error);
}
return okMsg;
} else if (notification.smseagleApiType === "smseagle-apiv2") { // according to https://www.smseagle.eu/docs/apiv2/
let config = {
headers: {
"access-token": notification.smseagleToken,
"Content-Type": "application/json",
}
};
config = this.getAxiosConfigWithProxy(config);
let encoding = (notification.smseagleEncoding) ? "unicode" : "standard";
let priority = (notification.smseaglePriority) ?? 0;
let postData = {
text: msg,
encoding: encoding,
priority: priority
};
if (notification.smseagleRecipientContact) {
postData["contacts"] = notification.smseagleRecipientContact.split(",").map(Number);
}
if (notification.smseagleRecipientGroup) {
postData["groups"] = notification.smseagleRecipientGroup.split(",").map(Number);
}
if (notification.smseagleRecipientTo) {
postData["to"] = notification.smseagleRecipientTo.split(",");
}
let endpoint = "/messages/sms";
if (notification.smseagleMsgType !== "smseagle-sms") {
postData["duration"] = notification.smseagleDuration ?? 10;
if (notification.smseagleMsgType === "smseagle-ring") {
endpoint = "/calls/ring";
} else if (notification.smseagleMsgType === "smseagle-tts") {
endpoint = "/calls/tts";
} else if (notification.smseagleMsgType === "smseagle-tts-advanced") {
endpoint = "/calls/tts_advanced";
postData["voice_id"] = notification.smseagleTtsModel ?? 1;
}
}
let resp = await axios.post(notification.smseagleUrl + "/api/v2" + endpoint, postData, config);
const queuedCount = resp.data.filter(x => x.status === "queued").length;
const unqueuedCount = resp.data.length - queuedCount;
if (resp.status !== 200 || queuedCount === 0) {
if (!resp.data.length) {
throw new Error("SMSEagle API returned an empty response");
}
throw new Error(`SMSEagle API returned error: ${JSON.stringify(resp.data)}`);
}
if (unqueuedCount) {
return `Sent ${queuedCount}/${resp.data.length} Messages Successfully.`;
}
return okMsg;
}
} catch (error) {
this.throwGeneralAxiosError(error);
}
+74
View File
@@ -0,0 +1,74 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
class SMSIR extends NotificationProvider {
name = "smsir";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const okMsg = "Sent Successfully.";
const url = "https://api.sms.ir/v1/send/verify";
try {
let config = {
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"X-API-Key": notification.smsirApiKey
}
};
config = this.getAxiosConfigWithProxy(config);
const formattedMobiles = notification.smsirNumber
.split(",")
.map(mobile => {
if (mobile.length === 11 && mobile.startsWith("09") && String(parseInt(mobile)) === mobile.substring(1)) {
// 09xxxxxxxxx Format
return mobile.substring(1);
}
return mobile;
});
const MAX_MESSAGE_LENGTH = 20; // This is a limitation placed by SMSIR
// Shorten By removing spaces, keeping context is better than cutting off the text
// If that does not work, truncate. Still better than not receiving an SMS
if (msg.length > MAX_MESSAGE_LENGTH) {
msg = msg.replace(/\s/g, "");
}
if (msg.length > MAX_MESSAGE_LENGTH) {
msg = msg.substring(0, MAX_MESSAGE_LENGTH - 1 - "...".length) + "...";
}
// Run multiple network requests at once
const requestPromises = formattedMobiles
.map(mobile => {
axios.post(
url,
{
mobile: mobile,
templateId: parseInt(notification.smsirTemplate),
parameters: [
{
name: "uptkumaalert",
value: msg
}
]
},
config
);
});
await Promise.all(requestPromises);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = SMSIR;
+2 -1
View File
@@ -18,7 +18,8 @@ class SMSManager extends NotificationProvider {
number: notification.numbers,
gateway: notification.messageType,
};
await axios.get(`${url}?apikey=${data.apikey}&message=${data.message}&number=${data.number}&gateway=${data.messageType}`);
let config = this.getAxiosConfigWithProxy({});
await axios.get(`${url}?apikey=${data.apikey}&message=${data.message}&number=${data.number}&gateway=${data.messageType}`, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
@@ -29,6 +29,7 @@ class SMSPartner extends NotificationProvider {
"Accept": "application/json",
}
};
config = this.getAxiosConfigWithProxy(config);
let resp = await axios.post(url, data, config);
+4 -2
View File
@@ -42,6 +42,7 @@ class SMTP extends NotificationProvider {
// default values in case the user does not want to template
let subject = msg;
let body = msg;
let useHTMLBody = false;
if (heartbeatJSON) {
body = `${msg}\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`;
}
@@ -50,11 +51,11 @@ class SMTP extends NotificationProvider {
// cannot end with whitespace as this often raises spam scores
const customSubject = notification.customSubject?.trim() || "";
const customBody = notification.customBody?.trim() || "";
if (customSubject !== "") {
subject = await this.renderTemplate(customSubject, msg, monitorJSON, heartbeatJSON);
}
if (customBody !== "") {
useHTMLBody = notification.htmlBody || false;
body = await this.renderTemplate(customBody, msg, monitorJSON, heartbeatJSON);
}
}
@@ -67,7 +68,8 @@ class SMTP extends NotificationProvider {
bcc: notification.smtpBCC,
to: notification.smtpTo,
subject: subject,
text: body,
// If the email body is custom, and the user wants it, set the email body as HTML
[useHTMLBody ? "html" : "text"]: body
});
return okMsg;
+38
View File
@@ -0,0 +1,38 @@
const NotificationProvider = require("./notification-provider");
const axios = require("axios");
const { DOWN, UP } = require("../../src/util");
class SpugPush extends NotificationProvider {
name = "SpugPush";
/**
* @inheritdoc
*/
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
let okMsg = "Sent Successfully.";
try {
let formData = {
title: "Uptime Kuma Message",
content: msg
};
if (heartbeatJSON) {
if (heartbeatJSON["status"] === UP) {
formData.title = `UptimeKuma 「${monitorJSON["name"]}」 is Up`;
formData.content = `[✅ Up] ${heartbeatJSON["msg"]}`;
} else if (heartbeatJSON["status"] === DOWN) {
formData.title = `UptimeKuma 「${monitorJSON["name"]}」 is Down`;
formData.content = `[🔴 Down] ${heartbeatJSON["msg"]}`;
}
}
const apiUrl = `https://push.spug.cc/send/${notification.templateKey}`;
let config = this.getAxiosConfigWithProxy({});
await axios.post(apiUrl, formData, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
}
}
}
module.exports = SpugPush;
@@ -45,6 +45,7 @@ class Squadcast extends NotificationProvider {
}
});
}
config = this.getAxiosConfigWithProxy(config);
await axios.post(notification.squadcastWebhookURL, data, config);
return okMsg;
+2 -1
View File
@@ -31,8 +31,9 @@ class Stackfield extends NotificationProvider {
const data = {
"Title": textMsg,
};
let config = this.getAxiosConfigWithProxy({});
await axios.post(notification.stackfieldwebhookURL, data);
await axios.post(notification.stackfieldwebhookURL, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
+2 -1
View File
@@ -185,7 +185,8 @@ class Teams extends NotificationProvider {
* @returns {Promise<void>}
*/
_sendNotification = async (webhookUrl, payload) => {
await axios.post(webhookUrl, payload);
let config = this.getAxiosConfigWithProxy({});
await axios.post(webhookUrl, payload, config);
};
/**
@@ -25,7 +25,8 @@ class TechulusPush extends NotificationProvider {
}
try {
await axios.post(`https://push.techulus.com/api/v1/notify/${notification.pushAPIKey}`, data);
let config = this.getAxiosConfigWithProxy({});
await axios.post(`https://push.techulus.com/api/v1/notify/${notification.pushAPIKey}`, data, config);
return okMsg;
} catch (error) {
this.throwGeneralAxiosError(error);
+4 -3
View File
@@ -17,6 +17,7 @@ class Telegram extends NotificationProvider {
text: msg,
disable_notification: notification.telegramSendSilently ?? false,
protect_content: notification.telegramProtectContent ?? false,
link_preview_options: { is_disabled: true },
};
if (notification.telegramMessageThreadID) {
params.message_thread_id = notification.telegramMessageThreadID;
@@ -30,9 +31,9 @@ class Telegram extends NotificationProvider {
}
}
await axios.get(`${url}/bot${notification.telegramBotToken}/sendMessage`, {
params: params,
});
let config = this.getAxiosConfigWithProxy();
await axios.post(`${url}/bot${notification.telegramBotToken}/sendMessage`, params, config);
return okMsg;
} catch (error) {
+2 -1
View File
@@ -10,12 +10,13 @@ class Threema extends NotificationProvider {
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
const url = "https://msgapi.threema.ch/send_simple";
const config = {
let config = {
headers: {
"Accept": "*/*",
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
}
};
config = this.getAxiosConfigWithProxy(config);
const data = {
from: notification.threemaSenderIdentity,
+4
View File
@@ -19,11 +19,15 @@ class Twilio extends NotificationProvider {
"Authorization": "Basic " + Buffer.from(apiKey + ":" + notification.twilioAuthToken).toString("base64"),
}
};
config = this.getAxiosConfigWithProxy(config);
let data = new URLSearchParams();
data.append("To", notification.twilioToNumber);
data.append("From", notification.twilioFromNumber);
data.append("Body", msg);
if (notification.twilioMessagingServiceSID) {
data.append("MessagingServiceSid", notification.twilioMessagingServiceSID);
}
await axios.post(`https://api.twilio.com/2010-04-01/Accounts/${(notification.twilioAccountSID)}/Messages.json`, data, config);
+2 -1
View File
@@ -11,13 +11,14 @@ class WAHA extends NotificationProvider {
const okMsg = "Sent Successfully.";
try {
const config = {
let config = {
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"X-Api-Key": notification.wahaApiKey,
}
};
config = this.getAxiosConfigWithProxy(config);
let data = {
"session": notification.wahaSession,
+24 -3
View File
@@ -12,6 +12,8 @@ class Webhook extends NotificationProvider {
const okMsg = "Sent Successfully.";
try {
const httpMethod = notification.httpMethod.toLowerCase() || "post";
let data = {
heartbeat: heartbeatJSON,
monitor: monitorJSON,
@@ -21,7 +23,19 @@ class Webhook extends NotificationProvider {
headers: {}
};
if (notification.webhookContentType === "form-data") {
if (httpMethod === "get") {
config.params = {
msg: msg
};
if (heartbeatJSON) {
config.params.heartbeat = JSON.stringify(heartbeatJSON);
}
if (monitorJSON) {
config.params.monitor = JSON.stringify(monitorJSON);
}
} else if (notification.webhookContentType === "form-data") {
const formData = new FormData();
formData.append("data", JSON.stringify(data));
config.headers = formData.getHeaders();
@@ -37,11 +51,18 @@ class Webhook extends NotificationProvider {
...JSON.parse(notification.webhookAdditionalHeaders)
};
} catch (err) {
throw "Additional Headers is not a valid JSON";
throw new Error("Additional Headers is not a valid JSON");
}
}
await axios.post(notification.webhookURL, data, config);
config = this.getAxiosConfigWithProxy(config);
if (httpMethod === "get") {
await axios.get(notification.webhookURL, config);
} else {
await axios.post(notification.webhookURL, data, config);
}
return okMsg;
} catch (error) {
+1
View File
@@ -17,6 +17,7 @@ class WeCom extends NotificationProvider {
"Content-Type": "application/json"
}
};
config = this.getAxiosConfigWithProxy(config);
let body = this.composeMessage(heartbeatJSON, msg);
await axios.post(`https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${notification.weComBotKey}`, body, config);
return okMsg;
+2 -1
View File
@@ -11,13 +11,14 @@ class Whapi extends NotificationProvider {
const okMsg = "Sent Successfully.";
try {
const config = {
let config = {
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": "Bearer " + notification.whapiAuthToken,
}
};
config = this.getAxiosConfigWithProxy(config);
let data = {
"to": notification.whapiRecipient,
+2 -1
View File
@@ -18,7 +18,8 @@ class WPush extends NotificationProvider {
"apikey": notification.wpushAPIkey,
"channel": notification.wpushChannel
};
const result = await axios.post("https://api.wpush.cn/api/v1/send", context);
let config = this.getAxiosConfigWithProxy({});
const result = await axios.post("https://api.wpush.cn/api/v1/send", context, config);
if (result.data.code !== 0) {
throw result.data.message;
}
+2 -1
View File
@@ -16,7 +16,7 @@ class YZJ extends NotificationProvider {
msg = `${this.statusToString(heartbeatJSON["status"])} ${monitorJSON["name"]} \n> ${heartbeatJSON["msg"]}\n> Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`;
}
const config = {
let config = {
headers: {
"Content-Type": "application/json",
},
@@ -26,6 +26,7 @@ class YZJ extends NotificationProvider {
};
// yzjtype=0 => general robot
const url = `${notification.yzjWebHookUrl}?yzjtype=0&yzjtoken=${notification.yzjToken}`;
config = this.getAxiosConfigWithProxy(config);
const result = await axios.post(url, params, config);
if (!result.data?.success) {
+2 -1
View File
@@ -27,7 +27,8 @@ class ZohoCliq extends NotificationProvider {
* @returns {Promise<void>}
*/
_sendNotification = async (webhookUrl, payload) => {
await axios.post(webhookUrl, { text: payload.join("\n") });
let config = this.getAxiosConfigWithProxy({});
await axios.post(webhookUrl, { text: payload.join("\n") }, config);
};
/**
+49 -20
View File
@@ -4,6 +4,7 @@ const Alerta = require("./notification-providers/alerta");
const AlertNow = require("./notification-providers/alertnow");
const AliyunSms = require("./notification-providers/aliyun-sms");
const Apprise = require("./notification-providers/apprise");
const Bale = require("./notification-providers/bale");
const Bark = require("./notification-providers/bark");
const Bitrix24 = require("./notification-providers/bitrix24");
const ClickSendSMS = require("./notification-providers/clicksendsms");
@@ -13,6 +14,7 @@ const DingDing = require("./notification-providers/dingding");
const Discord = require("./notification-providers/discord");
const Elks = require("./notification-providers/46elks");
const Feishu = require("./notification-providers/feishu");
const Notifery = require("./notification-providers/notifery");
const FreeMobile = require("./notification-providers/freemobile");
const GoogleChat = require("./notification-providers/google-chat");
const Gorush = require("./notification-providers/gorush");
@@ -27,12 +29,15 @@ const LineNotify = require("./notification-providers/linenotify");
const LunaSea = require("./notification-providers/lunasea");
const Matrix = require("./notification-providers/matrix");
const Mattermost = require("./notification-providers/mattermost");
const NextcloudTalk = require("./notification-providers/nextcloudtalk");
const Nostr = require("./notification-providers/nostr");
const Ntfy = require("./notification-providers/ntfy");
const Octopush = require("./notification-providers/octopush");
const OneChat = require("./notification-providers/onechat");
const OneBot = require("./notification-providers/onebot");
const Opsgenie = require("./notification-providers/opsgenie");
const PagerDuty = require("./notification-providers/pagerduty");
const Pumble = require("./notification-providers/pumble");
const FlashDuty = require("./notification-providers/flashduty");
const PagerTree = require("./notification-providers/pagertree");
const PromoSMS = require("./notification-providers/promosms");
@@ -66,15 +71,21 @@ const ZohoCliq = require("./notification-providers/zoho-cliq");
const SevenIO = require("./notification-providers/sevenio");
const Whapi = require("./notification-providers/whapi");
const WAHA = require("./notification-providers/waha");
const Evolution = require("./notification-providers/evolution");
const GtxMessaging = require("./notification-providers/gtx-messaging");
const Cellsynt = require("./notification-providers/cellsynt");
const Onesender = require("./notification-providers/onesender");
const Wpush = require("./notification-providers/wpush");
const SendGrid = require("./notification-providers/send-grid");
const Brevo = require("./notification-providers/brevo");
const YZJ = require("./notification-providers/yzj");
const SMSPlanet = require("./notification-providers/sms-planet");
const SpugPush = require("./notification-providers/spugpush");
const SMSIR = require("./notification-providers/smsir");
const { commandExists } = require("./util-server");
const Webpush = require("./notification-providers/Webpush");
class Notification {
providerList = {};
/**
@@ -93,6 +104,7 @@ class Notification {
new AlertNow(),
new AliyunSms(),
new Apprise(),
new Bale(),
new Bark(),
new Bitrix24(),
new ClickSendSMS(),
@@ -116,9 +128,11 @@ class Notification {
new LunaSea(),
new Matrix(),
new Mattermost(),
new NextcloudTalk(),
new Nostr(),
new Ntfy(),
new Octopush(),
new OneChat(),
new OneBot(),
new Onesender(),
new Opsgenie(),
@@ -126,6 +140,7 @@ class Notification {
new FlashDuty(),
new PagerTree(),
new PromoSMS(),
new Pumble(),
new Pushbullet(),
new PushDeer(),
new Pushover(),
@@ -156,14 +171,21 @@ class Notification {
new SevenIO(),
new Whapi(),
new WAHA(),
new Evolution(),
new GtxMessaging(),
new Cellsynt(),
new Wpush(),
new Brevo(),
new YZJ(),
new SMSPlanet(),
new SpugPush(),
new Notifery(),
new SMSIR(),
new SendGrid(),
new YZJ()
new Webpush(),
];
for (let item of list) {
if (! item.name) {
if (!item.name) {
throw new Error("Notification provider without name");
}
@@ -183,9 +205,19 @@ class Notification {
* @returns {Promise<string>} Successful msg
* @throws Error with fail msg
*/
static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
static async send(
notification,
msg,
monitorJSON = null,
heartbeatJSON = null
) {
if (this.providerList[notification.type]) {
return this.providerList[notification.type].send(notification, msg, monitorJSON, heartbeatJSON);
return this.providerList[notification.type].send(
notification,
msg,
monitorJSON,
heartbeatJSON
);
} else {
throw new Error("Notification type is not supported");
}
@@ -207,10 +239,9 @@ class Notification {
userID,
]);
if (! bean) {
if (!bean) {
throw new Error("notification not found");
}
} else {
bean = R.dispense("notification");
}
@@ -240,7 +271,7 @@ class Notification {
userID,
]);
if (! bean) {
if (!bean) {
throw new Error("notification not found");
}
@@ -249,14 +280,11 @@ class Notification {
/**
* Check if apprise exists
* @returns {boolean} Does the command apprise exist?
* @returns {Promise<boolean>} Does the command apprise exist?
*/
static checkApprise() {
let commandExistsSync = require("command-exists").sync;
let exists = commandExistsSync("apprise");
return exists;
static async checkApprise() {
return await commandExists("apprise");
}
}
/**
@@ -267,16 +295,17 @@ class Notification {
*/
async function applyNotificationEveryMonitor(notificationID, userID) {
let monitors = await R.getAll("SELECT id FROM monitor WHERE user_id = ?", [
userID
userID,
]);
for (let i = 0; i < monitors.length; i++) {
let checkNotification = await R.findOne("monitor_notification", " monitor_id = ? AND notification_id = ? ", [
monitors[i].id,
notificationID,
]);
let checkNotification = await R.findOne(
"monitor_notification",
" monitor_id = ? AND notification_id = ? ",
[ monitors[i].id, notificationID ]
);
if (! checkNotification) {
if (!checkNotification) {
let relation = R.dispense("monitor_notification");
relation.monitor_id = monitors[i].id;
relation.notification_id = notificationID;
+2 -2
View File
@@ -5,10 +5,10 @@ const saltRounds = 10;
/**
* Hash a password
* @param {string} password Password to hash
* @returns {string} Hash
* @returns {Promise<string>} Hash
*/
exports.generate = function (password) {
return bcrypt.hashSync(password, saltRounds);
return bcrypt.hash(password, saltRounds);
};
/**

Some files were not shown because too many files have changed in this diff Show More