mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-10-04 22:49:21 +08:00
Merge branch 'master' into async-fs
This commit is contained in:
@@ -130,7 +130,7 @@ function userAuthorizer(username, password, callback) {
|
||||
* @param {express.Request} req Express request object
|
||||
* @param {express.Response} res Express response object
|
||||
* @param {express.NextFunction} next Next handler in chain
|
||||
* @returns {void}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
exports.basicAuth = async function (req, res, next) {
|
||||
const middleware = basicAuth({
|
||||
@@ -153,7 +153,7 @@ exports.basicAuth = async function (req, res, next) {
|
||||
* @param {express.Request} req Express request object
|
||||
* @param {express.Response} res Express response object
|
||||
* @param {express.NextFunction} next Next handler in chain
|
||||
* @returns {void}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
exports.apiAuth = async function (req, res, next) {
|
||||
if (!await Settings.get("disableAuth")) {
|
||||
|
@@ -1,88 +0,0 @@
|
||||
const https = require("https");
|
||||
const http = require("http");
|
||||
const CacheableLookup = require("cacheable-lookup");
|
||||
const { Settings } = require("./settings");
|
||||
const { log } = require("../src/util");
|
||||
|
||||
class CacheableDnsHttpAgent {
|
||||
|
||||
static cacheable = new CacheableLookup();
|
||||
|
||||
static httpAgentList = {};
|
||||
static httpsAgentList = {};
|
||||
|
||||
static enable = false;
|
||||
|
||||
/**
|
||||
* Register/Disable cacheable to global agents
|
||||
* @returns {void}
|
||||
*/
|
||||
static async update() {
|
||||
log.debug("CacheableDnsHttpAgent", "update");
|
||||
let isEnable = await Settings.get("dnsCache");
|
||||
|
||||
if (isEnable !== this.enable) {
|
||||
log.debug("CacheableDnsHttpAgent", "value changed");
|
||||
|
||||
if (isEnable) {
|
||||
log.debug("CacheableDnsHttpAgent", "enable");
|
||||
this.cacheable.install(http.globalAgent);
|
||||
this.cacheable.install(https.globalAgent);
|
||||
} else {
|
||||
log.debug("CacheableDnsHttpAgent", "disable");
|
||||
this.cacheable.uninstall(http.globalAgent);
|
||||
this.cacheable.uninstall(https.globalAgent);
|
||||
}
|
||||
}
|
||||
|
||||
this.enable = isEnable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach cacheable to HTTP agent
|
||||
* @param {http.Agent} agent Agent to install
|
||||
* @returns {void}
|
||||
*/
|
||||
static install(agent) {
|
||||
this.cacheable.install(agent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {https.AgentOptions} agentOptions Options to pass to HTTPS agent
|
||||
* @returns {https.Agent} The new HTTPS agent
|
||||
*/
|
||||
static getHttpsAgent(agentOptions) {
|
||||
if (!this.enable) {
|
||||
return new https.Agent(agentOptions);
|
||||
}
|
||||
|
||||
let key = JSON.stringify(agentOptions);
|
||||
if (!(key in this.httpsAgentList)) {
|
||||
this.httpsAgentList[key] = new https.Agent(agentOptions);
|
||||
this.cacheable.install(this.httpsAgentList[key]);
|
||||
}
|
||||
return this.httpsAgentList[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {http.AgentOptions} agentOptions Options to pass to the HTTP agent
|
||||
* @returns {https.Agents} The new HTTP agent
|
||||
*/
|
||||
static getHttpAgent(agentOptions) {
|
||||
if (!this.enable) {
|
||||
return new http.Agent(agentOptions);
|
||||
}
|
||||
|
||||
let key = JSON.stringify(agentOptions);
|
||||
if (!(key in this.httpAgentList)) {
|
||||
this.httpAgentList[key] = new http.Agent(agentOptions);
|
||||
this.cacheable.install(this.httpAgentList[key]);
|
||||
}
|
||||
return this.httpAgentList[key];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CacheableDnsHttpAgent,
|
||||
};
|
@@ -8,6 +8,7 @@ const server = UptimeKumaServer.getInstance();
|
||||
const io = server.io;
|
||||
const { setting } = require("./util-server");
|
||||
const checkVersion = require("./check-version");
|
||||
const Database = require("./database");
|
||||
|
||||
/**
|
||||
* Send list of notification providers to client
|
||||
@@ -144,17 +145,20 @@ async function sendInfo(socket, hideVersion = false) {
|
||||
let version;
|
||||
let latestVersion;
|
||||
let isContainer;
|
||||
let dbType;
|
||||
|
||||
if (!hideVersion) {
|
||||
version = checkVersion.version;
|
||||
latestVersion = checkVersion.latestVersion;
|
||||
isContainer = (process.env.UPTIME_KUMA_IS_CONTAINER === "1");
|
||||
dbType = Database.dbConfig.type;
|
||||
}
|
||||
|
||||
socket.emit("info", {
|
||||
version,
|
||||
latestVersion,
|
||||
isContainer,
|
||||
dbType,
|
||||
primaryBaseURL: await setting("primaryBaseURL"),
|
||||
serverTimezone: await server.getTimezone(),
|
||||
serverTimezoneOffset: server.getTimezoneOffset(),
|
||||
|
@@ -1,29 +1,46 @@
|
||||
const isFreeBSD = /^freebsd/.test(process.platform);
|
||||
|
||||
// Interop with browser
|
||||
const args = (typeof process !== "undefined") ? require("args-parser")(process.argv) : {};
|
||||
const demoMode = args["demo"] || false;
|
||||
|
||||
const badgeConstants = {
|
||||
naColor: "#999",
|
||||
defaultUpColor: "#66c20a",
|
||||
defaultWarnColor: "#eed202",
|
||||
defaultDownColor: "#c2290a",
|
||||
defaultPendingColor: "#f8a306",
|
||||
defaultMaintenanceColor: "#1747f5",
|
||||
defaultPingColor: "blue", // as defined by badge-maker / shields.io
|
||||
defaultStyle: "flat",
|
||||
defaultPingValueSuffix: "ms",
|
||||
defaultPingLabelSuffix: "h",
|
||||
defaultUptimeValueSuffix: "%",
|
||||
defaultUptimeLabelSuffix: "h",
|
||||
defaultCertExpValueSuffix: " days",
|
||||
defaultCertExpLabelSuffix: "h",
|
||||
// Values Come From Default Notification Times
|
||||
defaultCertExpireWarnDays: "14",
|
||||
defaultCertExpireDownDays: "7"
|
||||
};
|
||||
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
|
||||
// Dual-stack support for (::)
|
||||
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
|
||||
let hostEnv = isFreeBSD ? null : process.env.HOST;
|
||||
const hostname = args.host || process.env.UPTIME_KUMA_HOST || hostEnv;
|
||||
|
||||
const port = [ args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001 ]
|
||||
.map(portValue => parseInt(portValue))
|
||||
.find(portValue => !isNaN(portValue));
|
||||
|
||||
const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
|
||||
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
|
||||
const sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_SSL_KEY_PASSPHRASE || process.env.SSL_KEY_PASSPHRASE || undefined;
|
||||
|
||||
const isSSL = sslKey && sslCert;
|
||||
|
||||
/**
|
||||
* Get the local WebSocket URL
|
||||
* @returns {string} The local WebSocket URL
|
||||
*/
|
||||
function getLocalWebSocketURL() {
|
||||
const protocol = isSSL ? "wss" : "ws";
|
||||
const host = hostname || "localhost";
|
||||
return `${protocol}://${host}:${port}`;
|
||||
}
|
||||
|
||||
const localWebSocketURL = getLocalWebSocketURL();
|
||||
|
||||
const demoMode = args["demo"] || false;
|
||||
|
||||
module.exports = {
|
||||
args,
|
||||
hostname,
|
||||
port,
|
||||
sslKey,
|
||||
sslCert,
|
||||
sslKeyPassphrase,
|
||||
isSSL,
|
||||
localWebSocketURL,
|
||||
demoMode,
|
||||
badgeConstants,
|
||||
};
|
||||
|
@@ -105,7 +105,8 @@ class Database {
|
||||
"patch-add-gamedig-given-port.sql": true,
|
||||
"patch-notification-config.sql": true,
|
||||
"patch-fix-kafka-producer-booleans.sql": true,
|
||||
"patch-timeout.sql": true, // The last file so far converted to a knex migration file
|
||||
"patch-timeout.sql": true,
|
||||
"patch-monitor-tls-info-add-fk.sql": true, // The last file so far converted to a knex migration file
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -208,9 +209,9 @@ class Database {
|
||||
let config = {};
|
||||
|
||||
let mariadbPoolConfig = {
|
||||
afterCreate: function (conn, done) {
|
||||
|
||||
}
|
||||
min: 0,
|
||||
max: 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
};
|
||||
|
||||
log.info("db", `Database Type: ${dbConfig.type}`);
|
||||
@@ -222,11 +223,8 @@ class Database {
|
||||
fs.copyFileSync(Database.templatePath, Database.sqlitePath);
|
||||
}
|
||||
|
||||
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
|
||||
Dialect.prototype._driver = () => require("@louislam/sqlite3");
|
||||
|
||||
config = {
|
||||
client: Dialect,
|
||||
client: "sqlite3",
|
||||
connection: {
|
||||
filename: Database.sqlitePath,
|
||||
acquireConnectionTimeout: acquireConnectionTimeout,
|
||||
@@ -378,7 +376,7 @@ class Database {
|
||||
|
||||
/**
|
||||
* Patch the database
|
||||
* @returns {void}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async patch() {
|
||||
// Still need to keep this for old versions of Uptime Kuma
|
||||
|
@@ -65,7 +65,7 @@ class DockerHost {
|
||||
/**
|
||||
* Fetches the amount of containers on the Docker host
|
||||
* @param {object} dockerHost Docker host to check for
|
||||
* @returns {number} Total amount of containers on the host
|
||||
* @returns {Promise<number>} Total amount of containers on the host
|
||||
*/
|
||||
static async testDockerHost(dockerHost) {
|
||||
const options = {
|
||||
|
@@ -9,7 +9,7 @@ class Group extends BeanModel {
|
||||
* @param {boolean} showTags Should the JSON include monitor tags
|
||||
* @param {boolean} certExpiry Should JSON include info about
|
||||
* certificate expiry?
|
||||
* @returns {object} Object ready to parse
|
||||
* @returns {Promise<object>} Object ready to parse
|
||||
*/
|
||||
async toPublicJSON(showTags = false, certExpiry = false) {
|
||||
let monitorBeanList = await this.getMonitorList();
|
||||
@@ -29,7 +29,7 @@ class Group extends BeanModel {
|
||||
|
||||
/**
|
||||
* Get all monitors
|
||||
* @returns {Bean[]} List of monitors
|
||||
* @returns {Promise<Bean[]>} List of monitors
|
||||
*/
|
||||
async getMonitorList() {
|
||||
return R.convertToBeans("monitor", await R.getAll(`
|
||||
|
@@ -11,7 +11,7 @@ class Maintenance extends BeanModel {
|
||||
/**
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* Only show necessary data to public
|
||||
* @returns {object} Object ready to parse
|
||||
* @returns {Promise<object>} Object ready to parse
|
||||
*/
|
||||
async toPublicJSON() {
|
||||
|
||||
@@ -98,7 +98,7 @@ class Maintenance extends BeanModel {
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @param {string} timezone If not specified, the timeRange will be in UTC
|
||||
* @returns {object} Object ready to parse
|
||||
* @returns {Promise<object>} Object ready to parse
|
||||
*/
|
||||
async toJSON(timezone = null) {
|
||||
return this.toPublicJSON(timezone);
|
||||
@@ -143,7 +143,7 @@ class Maintenance extends BeanModel {
|
||||
* Convert data from socket to bean
|
||||
* @param {Bean} bean Bean to fill in
|
||||
* @param {object} obj Data to fill bean with
|
||||
* @returns {Bean} Filled bean
|
||||
* @returns {Promise<Bean>} Filled bean
|
||||
*/
|
||||
static async jsonToBean(bean, obj) {
|
||||
if (obj.id) {
|
||||
@@ -189,9 +189,9 @@ class Maintenance extends BeanModel {
|
||||
/**
|
||||
* Throw error if cron is invalid
|
||||
* @param {string|Date} cron Pattern or date
|
||||
* @returns {Promise<void>}
|
||||
* @returns {void}
|
||||
*/
|
||||
static async validateCron(cron) {
|
||||
static validateCron(cron) {
|
||||
let job = new Cron(cron, () => {});
|
||||
job.stop();
|
||||
}
|
||||
@@ -324,7 +324,7 @@ class Maintenance extends BeanModel {
|
||||
|
||||
/**
|
||||
* Is this maintenance currently active
|
||||
* @returns {boolean} The maintenance is active?
|
||||
* @returns {Promise<boolean>} The maintenance is active?
|
||||
*/
|
||||
async isUnderMaintenance() {
|
||||
return (await this.getStatus()) === "under-maintenance";
|
||||
@@ -332,7 +332,7 @@ class Maintenance extends BeanModel {
|
||||
|
||||
/**
|
||||
* Get the timezone of the maintenance
|
||||
* @returns {string} timezone
|
||||
* @returns {Promise<string>} timezone
|
||||
*/
|
||||
async getTimezone() {
|
||||
if (!this.timezone || this.timezone === "SAME_AS_SERVER") {
|
||||
@@ -343,7 +343,7 @@ class Maintenance extends BeanModel {
|
||||
|
||||
/**
|
||||
* Get offset for timezone
|
||||
* @returns {string} offset
|
||||
* @returns {Promise<string>} offset
|
||||
*/
|
||||
async getTimezoneOffset() {
|
||||
return dayjs.tz(dayjs(), await this.getTimezone()).format("Z");
|
||||
@@ -351,7 +351,7 @@ class Maintenance extends BeanModel {
|
||||
|
||||
/**
|
||||
* Get the current status of the maintenance
|
||||
* @returns {string} Current status
|
||||
* @returns {Promise<string>} Current status
|
||||
*/
|
||||
async getStatus() {
|
||||
if (!this.active) {
|
||||
|
@@ -5,7 +5,7 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MI
|
||||
SQL_DATETIME_FORMAT
|
||||
} = require("../../src/util");
|
||||
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
|
||||
redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
|
||||
redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
|
||||
} = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
@@ -15,7 +15,6 @@ const { demoMode } = require("../config");
|
||||
const version = require("../../package.json").version;
|
||||
const apicache = require("../modules/apicache");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
|
||||
const { DockerHost } = require("../docker");
|
||||
const Gamedig = require("gamedig");
|
||||
const jsonata = require("jsonata");
|
||||
@@ -24,6 +23,8 @@ const crypto = require("crypto");
|
||||
const { UptimeCalculator } = require("../uptime-calculator");
|
||||
const { CookieJar } = require("tough-cookie");
|
||||
const { HttpsCookieAgent } = require("http-cookie-agent/http");
|
||||
const https = require("https");
|
||||
const http = require("http");
|
||||
|
||||
const rootCertificates = rootCertificatesFingerprints();
|
||||
|
||||
@@ -42,7 +43,7 @@ class Monitor extends BeanModel {
|
||||
* @param {boolean} showTags Include tags in JSON
|
||||
* @param {boolean} certExpiry Include certificate expiry info in
|
||||
* JSON
|
||||
* @returns {object} Object ready to parse
|
||||
* @returns {Promise<object>} Object ready to parse
|
||||
*/
|
||||
async toPublicJSON(showTags = false, certExpiry = false) {
|
||||
let obj = {
|
||||
@@ -73,7 +74,7 @@ class Monitor extends BeanModel {
|
||||
* Return an object that ready to parse to JSON
|
||||
* @param {boolean} includeSensitiveData Include sensitive data in
|
||||
* JSON
|
||||
* @returns {object} Object ready to parse
|
||||
* @returns {Promise<object>} Object ready to parse
|
||||
*/
|
||||
async toJSON(includeSensitiveData = true) {
|
||||
|
||||
@@ -95,11 +96,15 @@ class Monitor extends BeanModel {
|
||||
screenshot = "/screenshots/" + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + ".png";
|
||||
}
|
||||
|
||||
const path = await this.getPath();
|
||||
const pathName = path.join(" / ");
|
||||
|
||||
let data = {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
pathName: await this.getPathName(),
|
||||
path,
|
||||
pathName,
|
||||
parent: this.parent,
|
||||
childrenIDs: await Monitor.getAllChildrenIDs(this.id),
|
||||
url: this.url,
|
||||
@@ -240,12 +245,12 @@ class Monitor extends BeanModel {
|
||||
/**
|
||||
* Encode user and password to Base64 encoding
|
||||
* for HTTP "basic" auth, as per RFC-7617
|
||||
* @param {string} user Username to encode
|
||||
* @param {string} pass Password to encode
|
||||
* @returns {string} Encoded username:password
|
||||
* @param {string|null} user - The username (nullable if not changed by a user)
|
||||
* @param {string|null} pass - The password (nullable if not changed by a user)
|
||||
* @returns {string} Encoded Base64 string
|
||||
*/
|
||||
encodeBase64(user, pass) {
|
||||
return Buffer.from(user + ":" + pass).toString("base64");
|
||||
return Buffer.from(`${user || ""}:${pass || ""}`).toString("base64");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -323,9 +328,9 @@ class Monitor extends BeanModel {
|
||||
/**
|
||||
* Start monitor
|
||||
* @param {Server} io Socket server instance
|
||||
* @returns {void}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
start(io) {
|
||||
async start(io) {
|
||||
let previousBeat = null;
|
||||
let retries = 0;
|
||||
|
||||
@@ -432,9 +437,7 @@ class Monitor extends BeanModel {
|
||||
if (this.auth_method === "oauth2-cc") {
|
||||
try {
|
||||
if (this.oauthAccessToken === undefined || new Date(this.oauthAccessToken.expires_at * 1000) <= new Date()) {
|
||||
log.debug("monitor", `[${this.name}] The oauth access-token undefined or expired. Requesting a new one`);
|
||||
this.oauthAccessToken = await getOidcTokenClientCredentials(this.oauth_token_url, this.oauth_client_id, this.oauth_client_secret, this.oauth_scopes, this.oauth_auth_method);
|
||||
log.debug("monitor", `[${this.name}] Obtained oauth access-token. Expires at ${new Date(this.oauthAccessToken.expires_at * 1000)}`);
|
||||
this.oauthAccessToken = await this.makeOidcTokenClientCredentialsRequest();
|
||||
}
|
||||
oauth2AuthHeader = {
|
||||
"Authorization": this.oauthAccessToken.token_type + " " + this.oauthAccessToken.access_token,
|
||||
@@ -530,6 +533,18 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
let tlsInfo = {};
|
||||
// Store tlsInfo when secureConnect event is emitted
|
||||
// The keylog event listener is a workaround to access the tlsSocket
|
||||
options.httpsAgent.once("keylog", async (line, tlsSocket) => {
|
||||
tlsSocket.once("secureConnect", async () => {
|
||||
tlsInfo = checkCertificate(tlsSocket);
|
||||
tlsInfo.valid = tlsSocket.authorized || false;
|
||||
|
||||
await this.handleTlsInfo(tlsInfo);
|
||||
});
|
||||
});
|
||||
|
||||
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
|
||||
log.debug("monitor", `[${this.name}] Axios Request`);
|
||||
|
||||
@@ -539,31 +554,19 @@ class Monitor extends BeanModel {
|
||||
bean.msg = `${res.status} - ${res.statusText}`;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
// Check certificate if https is used
|
||||
let certInfoStartTime = dayjs().valueOf();
|
||||
if (this.getUrl()?.protocol === "https:") {
|
||||
log.debug("monitor", `[${this.name}] Check cert`);
|
||||
try {
|
||||
let tlsInfoObject = checkCertificate(res);
|
||||
tlsInfo = await this.updateTlsInfo(tlsInfoObject);
|
||||
// fallback for if kelog event is not emitted, but we may still have tlsInfo,
|
||||
// e.g. if the connection is made through a proxy
|
||||
if (this.getUrl()?.protocol === "https:" && tlsInfo.valid === undefined) {
|
||||
const tlsSocket = res.request.res.socket;
|
||||
|
||||
if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) {
|
||||
log.debug("monitor", `[${this.name}] call checkCertExpiryNotifications`);
|
||||
await this.checkCertExpiryNotifications(tlsInfoObject);
|
||||
}
|
||||
if (tlsSocket) {
|
||||
tlsInfo = checkCertificate(tlsSocket);
|
||||
tlsInfo.valid = tlsSocket.authorized || false;
|
||||
|
||||
} catch (e) {
|
||||
if (e.message !== "No TLS certificate in response") {
|
||||
log.error("monitor", "Caught error");
|
||||
log.error("monitor", e.message);
|
||||
}
|
||||
await this.handleTlsInfo(tlsInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.TIMELOGGER === "1") {
|
||||
log.debug("monitor", "Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
|
||||
}
|
||||
|
||||
if (process.env.UPTIME_KUMA_LOG_RESPONSE_BODY_MONITOR_ID === this.id) {
|
||||
log.info("monitor", res.data);
|
||||
}
|
||||
@@ -596,8 +599,12 @@ class Monitor extends BeanModel {
|
||||
let data = res.data;
|
||||
|
||||
// convert data to object
|
||||
if (typeof data === "string") {
|
||||
data = JSON.parse(data);
|
||||
if (typeof data === "string" && res.headers["content-type"] !== "application/json") {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch (_) {
|
||||
// Failed to parse as JSON, just process it as a string
|
||||
}
|
||||
}
|
||||
|
||||
let expression = jsonata(this.jsonPath);
|
||||
@@ -667,12 +674,12 @@ class Monitor extends BeanModel {
|
||||
headers: {
|
||||
"Accept": "*/*",
|
||||
},
|
||||
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
|
||||
httpsAgent: new https.Agent({
|
||||
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,
|
||||
}),
|
||||
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
|
||||
httpAgent: new http.Agent({
|
||||
maxCachedSessions: 0,
|
||||
}),
|
||||
maxRedirects: this.maxredirects,
|
||||
@@ -719,12 +726,12 @@ class Monitor extends BeanModel {
|
||||
headers: {
|
||||
"Accept": "*/*",
|
||||
},
|
||||
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
|
||||
httpsAgent: new https.Agent({
|
||||
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,
|
||||
}),
|
||||
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
|
||||
httpAgent: new http.Agent({
|
||||
maxCachedSessions: 0,
|
||||
}),
|
||||
};
|
||||
@@ -739,7 +746,7 @@ class Monitor extends BeanModel {
|
||||
options.socketPath = dockerHost._dockerDaemon;
|
||||
} else if (dockerHost._dockerType === "tcp") {
|
||||
options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
|
||||
options.httpsAgent = CacheableDnsHttpAgent.getHttpsAgent(
|
||||
options.httpsAgent = new https.Agent(
|
||||
await DockerHost.getHttpsAgentOptions(dockerHost._dockerType, options.baseURL)
|
||||
);
|
||||
}
|
||||
@@ -815,15 +822,6 @@ class Monitor extends BeanModel {
|
||||
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1", mysqlPassword);
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
} else if (this.type === "mongodb") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
await mongodbPing(this.databaseConnectionString);
|
||||
|
||||
bean.msg = "";
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
} else if (this.type === "radius") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
@@ -854,7 +852,7 @@ class Monitor extends BeanModel {
|
||||
} else if (this.type === "redis") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
bean.msg = await redisPingAsync(this.databaseConnectionString);
|
||||
bean.msg = await redisPingAsync(this.databaseConnectionString, !this.ignoreTls);
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
@@ -944,7 +942,7 @@ class Monitor extends BeanModel {
|
||||
log.debug("monitor", `[${this.name}] apicache clear`);
|
||||
apicache.clear();
|
||||
|
||||
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
||||
await UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
||||
|
||||
} else {
|
||||
bean.important = false;
|
||||
@@ -1064,27 +1062,44 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
|
||||
/**
|
||||
* Make a single attempt to obtain an new access token in the event that
|
||||
* the recent api request failed for authentication purposes
|
||||
*/
|
||||
if (this.auth_method === "oauth2-cc" && error.response.status === 401 && !finalCall) {
|
||||
this.oauthAccessToken = await this.makeOidcTokenClientCredentialsRequest();
|
||||
let oauth2AuthHeader = {
|
||||
"Authorization": this.oauthAccessToken.token_type + " " + this.oauthAccessToken.access_token,
|
||||
};
|
||||
options.headers = { ...(options.headers),
|
||||
...(oauth2AuthHeader)
|
||||
};
|
||||
|
||||
return this.makeAxiosRequest(options, true);
|
||||
}
|
||||
|
||||
// Fix #2253
|
||||
// Read more: https://stackoverflow.com/questions/1759956/curl-error-18-transfer-closed-with-outstanding-read-data-remaining
|
||||
if (!finalCall && typeof e.message === "string" && e.message.includes("maxContentLength size of -1 exceeded")) {
|
||||
if (!finalCall && typeof error.message === "string" && error.message.includes("maxContentLength size of -1 exceeded")) {
|
||||
log.debug("monitor", "makeAxiosRequest with gzip");
|
||||
options.headers["Accept-Encoding"] = "gzip, deflate";
|
||||
return this.makeAxiosRequest(options, true);
|
||||
} else {
|
||||
if (typeof e.message === "string" && e.message.includes("maxContentLength size of -1 exceeded")) {
|
||||
e.message = "response timeout: incomplete response within a interval";
|
||||
if (typeof error.message === "string" && error.message.includes("maxContentLength size of -1 exceeded")) {
|
||||
error.message = "response timeout: incomplete response within a interval";
|
||||
}
|
||||
throw e;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop monitor
|
||||
* @returns {void}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
stop() {
|
||||
async stop() {
|
||||
clearTimeout(this.heartbeatInterval);
|
||||
this.isStop = true;
|
||||
|
||||
@@ -1357,7 +1372,7 @@ class Monitor extends BeanModel {
|
||||
let notifyDays = await setting("tlsExpiryNotifyDays");
|
||||
if (notifyDays == null || !Array.isArray(notifyDays)) {
|
||||
// Reset Default
|
||||
setSetting("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general");
|
||||
await setSetting("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general");
|
||||
notifyDays = [ 7, 14, 21 ];
|
||||
}
|
||||
|
||||
@@ -1511,11 +1526,11 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets Full Path-Name (Groups and Name)
|
||||
* @returns {Promise<string>} Full path name of this monitor
|
||||
* Gets the full path
|
||||
* @returns {Promise<string[]>} Full path (includes groups and the name) of the monitor
|
||||
*/
|
||||
async getPathName() {
|
||||
let path = this.name;
|
||||
async getPath() {
|
||||
const path = [ this.name ];
|
||||
|
||||
if (this.parent === null) {
|
||||
return path;
|
||||
@@ -1523,7 +1538,7 @@ class Monitor extends BeanModel {
|
||||
|
||||
let parent = await Monitor.getParent(this.id);
|
||||
while (parent !== null) {
|
||||
path = `${parent.name} / ${path}`;
|
||||
path.unshift(parent.name);
|
||||
parent = await Monitor.getParent(parent.id);
|
||||
}
|
||||
|
||||
@@ -1553,7 +1568,7 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlinks all children of the the group monitor
|
||||
* Unlinks all children of the group monitor
|
||||
* @param {number} groupID ID of group to remove children of
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@@ -1578,6 +1593,37 @@ class Monitor extends BeanModel {
|
||||
const parentActive = await Monitor.isParentActive(parent.id);
|
||||
return parent.active && parentActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a new Oidc Token
|
||||
* @returns {Promise<object>} OAuthProvider client
|
||||
*/
|
||||
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);
|
||||
if (this.oauthAccessToken?.expires_at) {
|
||||
log.debug("monitor", `[${this.name}] Obtained oauth access-token. Expires at ${new Date(this.oauthAccessToken?.expires_at * 1000)}`);
|
||||
} else {
|
||||
log.debug("monitor", `[${this.name}] Obtained oauth access-token. Time until expiry was not provided`);
|
||||
}
|
||||
|
||||
return oAuthAccessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store TLS certificate information and check for expiry
|
||||
* @param {object} tlsInfo Information about the TLS connection
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async handleTlsInfo(tlsInfo) {
|
||||
await this.updateTlsInfo(tlsInfo);
|
||||
this.prometheus?.update(null, tlsInfo);
|
||||
|
||||
if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) {
|
||||
log.debug("monitor", `[${this.name}] call checkCertExpiryNotifications`);
|
||||
await this.checkCertExpiryNotifications(tlsInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Monitor;
|
||||
|
@@ -18,9 +18,15 @@ class StatusPage extends BeanModel {
|
||||
* @param {Response} response Response object
|
||||
* @param {string} indexHTML HTML to render
|
||||
* @param {string} slug Status page slug
|
||||
* @returns {void}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async handleStatusPageResponse(response, indexHTML, slug) {
|
||||
// Handle url with trailing slash (http://localhost:3001/status/)
|
||||
// The slug comes from the route "/status/:slug". If the slug is empty, express converts it to "index.html"
|
||||
if (slug === "index.html") {
|
||||
slug = "default";
|
||||
}
|
||||
|
||||
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||
slug
|
||||
]);
|
||||
@@ -36,7 +42,7 @@ class StatusPage extends BeanModel {
|
||||
* SSR for status pages
|
||||
* @param {string} indexHTML HTML page to render
|
||||
* @param {StatusPage} statusPage Status page populate HTML with
|
||||
* @returns {void}
|
||||
* @returns {Promise<string>} the rendered html
|
||||
*/
|
||||
static async renderHTML(indexHTML, statusPage) {
|
||||
const $ = cheerio.load(indexHTML);
|
||||
@@ -232,6 +238,7 @@ class StatusPage extends BeanModel {
|
||||
description: this.description,
|
||||
icon: this.getIcon(),
|
||||
theme: this.theme,
|
||||
autoRefreshInterval: this.autoRefreshInterval,
|
||||
published: !!this.published,
|
||||
showTags: !!this.show_tags,
|
||||
domainNameList: this.getDomainNameList(),
|
||||
@@ -254,6 +261,7 @@ class StatusPage extends BeanModel {
|
||||
title: this.title,
|
||||
description: this.description,
|
||||
icon: this.getIcon(),
|
||||
autoRefreshInterval: this.autoRefreshInterval,
|
||||
theme: this.theme,
|
||||
published: !!this.published,
|
||||
showTags: !!this.show_tags,
|
||||
|
@@ -25,8 +25,14 @@ class User extends BeanModel {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async resetPassword(newPassword) {
|
||||
await User.resetPassword(this.id, newPassword);
|
||||
this.password = newPassword;
|
||||
const hashedPassword = passwordHash.generate(newPassword);
|
||||
|
||||
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
|
||||
hashedPassword,
|
||||
this.id
|
||||
]);
|
||||
|
||||
this.password = hashedPassword;
|
||||
}
|
||||
|
||||
/**
|
||||
|
65
server/monitor-types/mongodb.js
Normal file
65
server/monitor-types/mongodb.js
Normal file
@@ -0,0 +1,65 @@
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const { UP } = require("../../src/util");
|
||||
const { MongoClient } = require("mongodb");
|
||||
const jsonata = require("jsonata");
|
||||
|
||||
class MongodbMonitorType extends MonitorType {
|
||||
|
||||
name = "mongodb";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat, _server) {
|
||||
let command = { "ping": 1 };
|
||||
if (monitor.databaseQuery) {
|
||||
command = JSON.parse(monitor.databaseQuery);
|
||||
}
|
||||
|
||||
let result = await this.runMongodbCommand(monitor.databaseConnectionString, command);
|
||||
|
||||
if (result["ok"] !== 1) {
|
||||
throw new Error("MongoDB command failed");
|
||||
} else {
|
||||
heartbeat.msg = "Command executed successfully";
|
||||
}
|
||||
|
||||
if (monitor.jsonPath) {
|
||||
let expression = jsonata(monitor.jsonPath);
|
||||
result = await expression.evaluate(result);
|
||||
if (result) {
|
||||
heartbeat.msg = "Command executed successfully and the jsonata expression produces a result.";
|
||||
} else {
|
||||
throw new Error("Queried value not found.");
|
||||
}
|
||||
}
|
||||
|
||||
if (monitor.expectedValue) {
|
||||
if (result.toString() === monitor.expectedValue) {
|
||||
heartbeat.msg = "Command executed successfully and expected value was found";
|
||||
} else {
|
||||
throw new Error("Query executed, but value is not equal to expected value, value was: [" + JSON.stringify(result) + "]");
|
||||
}
|
||||
}
|
||||
|
||||
heartbeat.status = UP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to and run MongoDB command on a MongoDB database
|
||||
* @param {string} connectionString The database connection string
|
||||
* @param {object} command MongoDB command to run on the database
|
||||
* @returns {Promise<(string[] | object[] | object)>} Response from
|
||||
* server
|
||||
*/
|
||||
async runMongodbCommand(connectionString, command) {
|
||||
let client = await MongoClient.connect(connectionString);
|
||||
let result = await client.db().command(command);
|
||||
await client.close();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MongodbMonitorType,
|
||||
};
|
@@ -1,5 +1,4 @@
|
||||
class MonitorType {
|
||||
|
||||
name = undefined;
|
||||
|
||||
/**
|
||||
|
@@ -81,7 +81,8 @@ class MqttMonitorType extends MonitorType {
|
||||
|
||||
let client = mqtt.connect(mqttUrl, {
|
||||
username,
|
||||
password
|
||||
password,
|
||||
clientId: "uptime-kuma_" + Math.random().toString(16).substr(2, 8)
|
||||
});
|
||||
|
||||
client.on("connect", () => {
|
||||
|
@@ -10,6 +10,10 @@ const jwt = require("jsonwebtoken");
|
||||
const config = require("../config");
|
||||
const { RemoteBrowser } = require("../remote-browser");
|
||||
|
||||
/**
|
||||
* Cached instance of a browser
|
||||
* @type {import ("playwright-core").Browser}
|
||||
*/
|
||||
let browser = null;
|
||||
|
||||
let allowedList = [];
|
||||
@@ -71,10 +75,12 @@ async function isAllowedChromeExecutable(executablePath) {
|
||||
/**
|
||||
* Get the current instance of the browser. If there isn't one, create
|
||||
* it.
|
||||
* @returns {Promise<Browser>} The browser
|
||||
* @returns {Promise<import ("playwright-core").Browser>} The browser
|
||||
*/
|
||||
async function getBrowser() {
|
||||
if (!browser) {
|
||||
if (browser && browser.isConnected()) {
|
||||
return browser;
|
||||
} else {
|
||||
let executablePath = await Settings.get("chromeExecutable");
|
||||
|
||||
executablePath = await prepareChromeExecutable(executablePath);
|
||||
@@ -83,8 +89,9 @@ async function getBrowser() {
|
||||
//headless: false,
|
||||
executablePath,
|
||||
});
|
||||
|
||||
return browser;
|
||||
}
|
||||
return browser;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,7 +103,7 @@ async function getBrowser() {
|
||||
async function getRemoteBrowser(remoteBrowserID, userId) {
|
||||
let remoteBrowser = await RemoteBrowser.get(remoteBrowserID, userId);
|
||||
log.debug("MONITOR", `Using remote browser: ${remoteBrowser.name} (${remoteBrowser.id})`);
|
||||
browser = chromium.connect(remoteBrowser.url);
|
||||
browser = await chromium.connect(remoteBrowser.url);
|
||||
return browser;
|
||||
}
|
||||
|
||||
|
@@ -16,7 +16,7 @@ class TailscalePing extends MonitorType {
|
||||
* @param {object} monitor The monitor object associated with the check.
|
||||
* @param {object} heartbeat The heartbeat object to update.
|
||||
* @returns {Promise<void>}
|
||||
* @throws Will throw an error if checking Tailscale ping encounters any error
|
||||
* @throws Error if checking Tailscale ping encounters any error
|
||||
*/
|
||||
async check(monitor, heartbeat) {
|
||||
try {
|
||||
@@ -38,7 +38,8 @@ class TailscalePing extends MonitorType {
|
||||
async runTailscalePing(hostname, interval) {
|
||||
let timeout = interval * 1000 * 0.8;
|
||||
let res = await childProcessAsync.spawn("tailscale", [ "ping", "--c", "1", hostname ], {
|
||||
timeout: timeout
|
||||
timeout: timeout,
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (res.stderr && res.stderr.toString()) {
|
||||
throw new Error(`Error in output: ${res.stderr.toString()}`);
|
||||
|
@@ -3,17 +3,15 @@ const { DOWN, UP } = require("../../src/util");
|
||||
const axios = require("axios");
|
||||
|
||||
class Alerta extends NotificationProvider {
|
||||
|
||||
name = "alerta";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let alertaUrl = `${notification.alertaApiEndpoint}`;
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json;charset=UTF-8",
|
||||
@@ -40,7 +38,7 @@ class Alerta extends NotificationProvider {
|
||||
resource: "Message",
|
||||
}, data);
|
||||
|
||||
await axios.post(alertaUrl, postData, config);
|
||||
await axios.post(notification.alertaApiEndpoint, postData, config);
|
||||
} else {
|
||||
let datadup = Object.assign( {
|
||||
correlate: [ "service_up", "service_down" ],
|
||||
@@ -52,11 +50,11 @@ class Alerta extends NotificationProvider {
|
||||
if (heartbeatJSON["status"] === DOWN) {
|
||||
datadup.severity = notification.alertaAlertState; // critical
|
||||
datadup.text = "Service " + monitorJSON["type"] + " is down.";
|
||||
await axios.post(alertaUrl, datadup, config);
|
||||
await axios.post(notification.alertaApiEndpoint, datadup, config);
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
datadup.severity = notification.alertaRecoverState; // cleaned
|
||||
datadup.text = "Service " + monitorJSON["type"] + " is up.";
|
||||
await axios.post(alertaUrl, datadup, config);
|
||||
await axios.post(notification.alertaApiEndpoint, datadup, config);
|
||||
}
|
||||
}
|
||||
return okMsg;
|
||||
|
@@ -4,14 +4,14 @@ const { setting } = require("../util-server");
|
||||
const { getMonitorRelativeURL, UP, DOWN } = require("../../src/util");
|
||||
|
||||
class AlertNow extends NotificationProvider {
|
||||
|
||||
name = "AlertNow";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let textMsg = "";
|
||||
let status = "open";
|
||||
|
@@ -11,7 +11,7 @@ class AliyunSMS extends NotificationProvider {
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
if (heartbeatJSON != null) {
|
||||
@@ -44,7 +44,7 @@ class AliyunSMS extends NotificationProvider {
|
||||
* Send the SMS notification
|
||||
* @param {BeanModel} notification Notification details
|
||||
* @param {string} msgbody Message template
|
||||
* @returns {boolean} True if successful else false
|
||||
* @returns {Promise<boolean>} True if successful else false
|
||||
*/
|
||||
async sendSms(notification, msgbody) {
|
||||
let params = {
|
||||
|
@@ -2,26 +2,29 @@ const NotificationProvider = require("./notification-provider");
|
||||
const childProcessAsync = require("promisify-child-process");
|
||||
|
||||
class Apprise extends NotificationProvider {
|
||||
|
||||
name = "apprise";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
const args = [ "-vv", "-b", msg, notification.appriseURL ];
|
||||
if (notification.title) {
|
||||
args.push("-t");
|
||||
args.push(notification.title);
|
||||
}
|
||||
const s = await childProcessAsync.spawn("apprise", args);
|
||||
const s = await childProcessAsync.spawn("apprise", args, {
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
const output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
||||
|
||||
if (output) {
|
||||
|
||||
if (! output.includes("ERROR")) {
|
||||
return "Sent Successfully";
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
throw new Error(output);
|
||||
|
@@ -46,29 +46,29 @@ class Bark extends NotificationProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add additional parameter for Bark v1 endpoints
|
||||
* Add additional parameter for Bark v1 endpoints.
|
||||
* Leads to better on device styles (iOS 15 optimized)
|
||||
* @param {BeanModel} notification Notification to send
|
||||
* @param {string} postUrl URL to append parameters to
|
||||
* @returns {string} Additional URL parameters
|
||||
*/
|
||||
appendAdditionalParameters(notification, postUrl) {
|
||||
additionalParameters(notification) {
|
||||
// set icon to uptime kuma icon, 11kb should be fine
|
||||
postUrl += "?icon=" + barkNotificationAvatar;
|
||||
let params = "?icon=" + barkNotificationAvatar;
|
||||
// grouping all our notifications
|
||||
if (notification.barkGroup != null) {
|
||||
postUrl += "&group=" + notification.barkGroup;
|
||||
params += "&group=" + notification.barkGroup;
|
||||
} else {
|
||||
// default name
|
||||
postUrl += "&group=" + "UptimeKuma";
|
||||
params += "&group=" + "UptimeKuma";
|
||||
}
|
||||
// picked a sound, this should follow system's mute status when arrival
|
||||
if (notification.barkSound != null) {
|
||||
postUrl += "&sound=" + notification.barkSound;
|
||||
params += "&sound=" + notification.barkSound;
|
||||
} else {
|
||||
// default sound
|
||||
postUrl += "&sound=" + "telegraph";
|
||||
params += "&sound=" + "telegraph";
|
||||
}
|
||||
return postUrl;
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,7 +92,7 @@ class Bark extends NotificationProvider {
|
||||
* @param {string} title Message title
|
||||
* @param {string} subtitle Message
|
||||
* @param {string} endpoint Endpoint to send request to
|
||||
* @returns {string} Success message
|
||||
* @returns {Promise<string>} Success message
|
||||
*/
|
||||
async postNotification(notification, title, subtitle, endpoint) {
|
||||
let result;
|
||||
@@ -100,9 +100,8 @@ class Bark extends NotificationProvider {
|
||||
// url encode title and subtitle
|
||||
title = encodeURIComponent(title);
|
||||
subtitle = encodeURIComponent(subtitle);
|
||||
let postUrl = endpoint + "/" + title + "/" + subtitle;
|
||||
postUrl = this.appendAdditionalParameters(notification, postUrl);
|
||||
result = await axios.get(postUrl);
|
||||
const params = this.additionalParameters(notification);
|
||||
result = await axios.get(`${endpoint}/${title}/${subtitle}${params}`);
|
||||
} else {
|
||||
result = await axios.post(`${endpoint}/push`, {
|
||||
title,
|
||||
|
31
server/notification-providers/bitrix24.js
Normal file
31
server/notification-providers/bitrix24.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { UP } = require("../../src/util");
|
||||
|
||||
class Bitrix24 extends NotificationProvider {
|
||||
name = "Bitrix24";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
const params = {
|
||||
user_id: notification.bitrix24UserID,
|
||||
message: "[B]Uptime Kuma[/B]",
|
||||
"ATTACH[COLOR]": (heartbeatJSON ?? {})["status"] === UP ? "#b73419" : "#67b518",
|
||||
"ATTACH[BLOCKS][0][MESSAGE]": msg
|
||||
};
|
||||
|
||||
await axios.get(`${notification.bitrix24WebhookURL}/im.notify.system.add.json`, { params });
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Bitrix24;
|
23
server/notification-providers/call-me-bot.js
Normal file
23
server/notification-providers/call-me-bot.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class CallMeBot extends NotificationProvider {
|
||||
name = "CallMeBot";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
try {
|
||||
const url = new URL(notification.callMeBotEndpoint);
|
||||
url.searchParams.set("text", msg);
|
||||
await axios.get(url.toString());
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CallMeBot;
|
39
server/notification-providers/cellsynt.js
Normal file
39
server/notification-providers/cellsynt.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Cellsynt extends NotificationProvider {
|
||||
name = "Cellsynt";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
const data = {
|
||||
// docs at https://www.cellsynt.com/en/sms/api-integration
|
||||
params: {
|
||||
"username": notification.cellsyntLogin,
|
||||
"password": notification.cellsyntPassword,
|
||||
"destination": notification.cellsyntDestination,
|
||||
"text": msg.replace(/[^\x00-\x7F]/g, ""),
|
||||
"originatortype": notification.cellsyntOriginatortype,
|
||||
"originator": notification.cellsyntOriginator,
|
||||
"allowconcat": notification.cellsyntAllowLongSMS ? 6 : 1
|
||||
}
|
||||
};
|
||||
try {
|
||||
const resp = await axios.post("https://se-1.cellsynt.net/sms.php", null, data);
|
||||
if (resp.data == null ) {
|
||||
throw new Error("Could not connect to Cellsynt, please try again.");
|
||||
} else if (resp.data.includes("Error:")) {
|
||||
resp.data = resp.data.replaceAll("Error:", "");
|
||||
throw new Error(resp.data);
|
||||
}
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Cellsynt;
|
@@ -2,14 +2,15 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class ClickSendSMS extends NotificationProvider {
|
||||
|
||||
name = "clicksendsms";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://rest.clicksend.com/v3/sms/send";
|
||||
|
||||
try {
|
||||
let config = {
|
||||
headers: {
|
||||
@@ -28,7 +29,7 @@ class ClickSendSMS extends NotificationProvider {
|
||||
}
|
||||
]
|
||||
};
|
||||
let resp = await axios.post("https://rest.clicksend.com/v3/sms/send", data, config);
|
||||
let resp = await axios.post(url, data, config);
|
||||
if (resp.data.data.messages[0].status !== "SUCCESS") {
|
||||
let error = "Something gone wrong. Api returned " + resp.data.data.messages[0].status + ".";
|
||||
this.throwGeneralAxiosError(error);
|
||||
|
@@ -10,7 +10,7 @@ class DingDing extends NotificationProvider {
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
if (heartbeatJSON != null) {
|
||||
@@ -19,9 +19,12 @@ class DingDing extends NotificationProvider {
|
||||
markdown: {
|
||||
title: `[${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]}`,
|
||||
text: `## [${this.statusToString(heartbeatJSON["status"])}] ${monitorJSON["name"]} \n> ${heartbeatJSON["msg"]}\n> Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
||||
},
|
||||
"at": {
|
||||
"isAtAll": notification.mentioning === "everyone"
|
||||
}
|
||||
};
|
||||
if (this.sendToDingDing(notification, params)) {
|
||||
if (await this.sendToDingDing(notification, params)) {
|
||||
return okMsg;
|
||||
}
|
||||
} else {
|
||||
@@ -31,7 +34,7 @@ class DingDing extends NotificationProvider {
|
||||
content: msg
|
||||
}
|
||||
};
|
||||
if (this.sendToDingDing(notification, params)) {
|
||||
if (await this.sendToDingDing(notification, params)) {
|
||||
return okMsg;
|
||||
}
|
||||
}
|
||||
@@ -44,7 +47,7 @@ class DingDing extends NotificationProvider {
|
||||
* Send message to DingDing
|
||||
* @param {BeanModel} notification Notification to send
|
||||
* @param {object} params Parameters of message
|
||||
* @returns {boolean} True if successful else false
|
||||
* @returns {Promise<boolean>} True if successful else false
|
||||
*/
|
||||
async sendToDingDing(notification, params) {
|
||||
let timestamp = Date.now();
|
||||
@@ -62,7 +65,7 @@ class DingDing extends NotificationProvider {
|
||||
if (result.data.errmsg === "ok") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
throw new Error(result.data.errmsg);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -3,17 +3,20 @@ const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class Discord extends NotificationProvider {
|
||||
|
||||
name = "discord";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
const discordDisplayName = notification.discordUsername || "Uptime Kuma";
|
||||
const webhookUrl = new URL(notification.discordWebhookUrl);
|
||||
if (notification.discordChannelType === "postToThread") {
|
||||
webhookUrl.searchParams.append("thread_id", notification.threadId);
|
||||
}
|
||||
|
||||
// If heartbeatJSON is null, assume we're testing.
|
||||
if (heartbeatJSON == null) {
|
||||
@@ -21,7 +24,12 @@ class Discord extends NotificationProvider {
|
||||
username: discordDisplayName,
|
||||
content: msg,
|
||||
};
|
||||
await axios.post(notification.discordWebhookUrl, discordtestdata);
|
||||
|
||||
if (notification.discordChannelType === "createNewForumPost") {
|
||||
discordtestdata.thread_name = notification.postName;
|
||||
}
|
||||
|
||||
await axios.post(webhookUrl.toString(), discordtestdata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
@@ -73,12 +81,14 @@ class Discord extends NotificationProvider {
|
||||
],
|
||||
}],
|
||||
};
|
||||
|
||||
if (notification.discordChannelType === "createNewForumPost") {
|
||||
discorddowndata.thread_name = notification.postName;
|
||||
}
|
||||
if (notification.discordPrefixMessage) {
|
||||
discorddowndata.content = notification.discordPrefixMessage;
|
||||
}
|
||||
|
||||
await axios.post(notification.discordWebhookUrl, discorddowndata);
|
||||
await axios.post(webhookUrl.toString(), discorddowndata);
|
||||
return okMsg;
|
||||
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
@@ -109,11 +119,15 @@ class Discord extends NotificationProvider {
|
||||
}],
|
||||
};
|
||||
|
||||
if (notification.discordChannelType === "createNewForumPost") {
|
||||
discordupdata.thread_name = notification.postName;
|
||||
}
|
||||
|
||||
if (notification.discordPrefixMessage) {
|
||||
discordupdata.content = notification.discordPrefixMessage;
|
||||
}
|
||||
|
||||
await axios.post(notification.discordWebhookUrl, discordupdata);
|
||||
await axios.post(webhookUrl.toString(), discordupdata);
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
|
@@ -9,8 +9,7 @@ class Feishu extends NotificationProvider {
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
let feishuWebHookUrl = notification.feishuWebHookUrl;
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
@@ -20,7 +19,7 @@ class Feishu extends NotificationProvider {
|
||||
text: msg,
|
||||
},
|
||||
};
|
||||
await axios.post(feishuWebHookUrl, testdata);
|
||||
await axios.post(notification.feishuWebHookUrl, testdata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
@@ -46,7 +45,7 @@ class Feishu extends NotificationProvider {
|
||||
},
|
||||
},
|
||||
};
|
||||
await axios.post(feishuWebHookUrl, downdata);
|
||||
await axios.post(notification.feishuWebHookUrl, downdata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
@@ -72,7 +71,7 @@ class Feishu extends NotificationProvider {
|
||||
},
|
||||
},
|
||||
};
|
||||
await axios.post(feishuWebHookUrl, updata);
|
||||
await axios.post(notification.feishuWebHookUrl, updata);
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
|
@@ -62,6 +62,15 @@ class FlashDuty extends NotificationProvider {
|
||||
* @returns {string} Success message
|
||||
*/
|
||||
async postNotification(notification, title, body, monitorInfo, eventStatus) {
|
||||
let labels = {
|
||||
resource: this.genMonitorUrl(monitorInfo),
|
||||
check: monitorInfo.name,
|
||||
};
|
||||
if (monitorInfo.tags && monitorInfo.tags.length > 0) {
|
||||
for (let tag of monitorInfo.tags) {
|
||||
labels[tag.name] = tag.value;
|
||||
}
|
||||
}
|
||||
const options = {
|
||||
method: "POST",
|
||||
url: "https://api.flashcat.cloud/event/push/alert/standard?integration_key=" + notification.flashdutyIntegrationKey,
|
||||
@@ -71,9 +80,7 @@ class FlashDuty extends NotificationProvider {
|
||||
title,
|
||||
event_status: eventStatus || "Info",
|
||||
alert_key: String(monitorInfo.id) || Math.random().toString(36).substring(7),
|
||||
labels: monitorInfo?.tags?.reduce((acc, item) => ({ ...acc,
|
||||
[item.name]: item.value
|
||||
}), { resource: this.genMonitorUrl(monitorInfo) }),
|
||||
labels,
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -2,14 +2,14 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class FreeMobile extends NotificationProvider {
|
||||
|
||||
name = "FreeMobile";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
await axios.post(`https://smsapi.free-mobile.fr/sendmsg?msg=${encodeURIComponent(msg.replace("🔴", "⛔️"))}`, {
|
||||
"user": notification.freemobileUser,
|
||||
|
@@ -3,21 +3,20 @@ const axios = require("axios");
|
||||
const { UP } = require("../../src/util");
|
||||
|
||||
class GoAlert extends NotificationProvider {
|
||||
|
||||
name = "GoAlert";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let closeAction = "close";
|
||||
let data = {
|
||||
summary: msg,
|
||||
};
|
||||
if (heartbeatJSON != null && heartbeatJSON["status"] === UP) {
|
||||
data["action"] = closeAction;
|
||||
data["action"] = "close";
|
||||
}
|
||||
let headers = {
|
||||
"Content-Type": "multipart/form-data",
|
||||
@@ -27,7 +26,6 @@ class GoAlert extends NotificationProvider {
|
||||
};
|
||||
await axios.post(`${notification.goAlertBaseURL}/api/v2/generic/incoming?token=${notification.goAlertToken}`, data, config);
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
let msg = (error.response.data) ? error.response.data : "Error without response";
|
||||
throw new Error(msg);
|
||||
|
@@ -1,41 +1,85 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { setting } = require("../util-server");
|
||||
const { getMonitorRelativeURL } = require("../../src/util");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
const { getMonitorRelativeURL, UP } = require("../../src/util");
|
||||
|
||||
class GoogleChat extends NotificationProvider {
|
||||
|
||||
name = "GoogleChat";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
// Google Chat message formatting: https://developers.google.com/chat/api/guides/message-formats/basic
|
||||
|
||||
let textMsg = "";
|
||||
if (heartbeatJSON && heartbeatJSON.status === UP) {
|
||||
textMsg = "✅ Application is back online\n";
|
||||
} else if (heartbeatJSON && heartbeatJSON.status === DOWN) {
|
||||
textMsg = "🔴 Application went down\n";
|
||||
let chatHeader = {
|
||||
title: "Uptime Kuma Alert",
|
||||
};
|
||||
|
||||
if (monitorJSON && heartbeatJSON) {
|
||||
chatHeader["title"] =
|
||||
heartbeatJSON["status"] === UP
|
||||
? `✅ ${monitorJSON["name"]} is back online`
|
||||
: `🔴 ${monitorJSON["name"]} went down`;
|
||||
}
|
||||
|
||||
if (monitorJSON && monitorJSON.name) {
|
||||
textMsg += `*${monitorJSON.name}*\n`;
|
||||
// always show msg
|
||||
let sectionWidgets = [
|
||||
{
|
||||
textParagraph: {
|
||||
text: `<b>Message:</b>\n${msg}`,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// add time if available
|
||||
if (heartbeatJSON) {
|
||||
sectionWidgets.push({
|
||||
textParagraph: {
|
||||
text: `<b>Time (${heartbeatJSON["timezone"]}):</b>\n${heartbeatJSON["localDateTime"]}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
textMsg += `${msg}`;
|
||||
|
||||
// add button for monitor link if available
|
||||
const baseURL = await setting("primaryBaseURL");
|
||||
if (baseURL && monitorJSON) {
|
||||
textMsg += `\n${baseURL + getMonitorRelativeURL(monitorJSON.id)}`;
|
||||
if (baseURL) {
|
||||
const urlPath = monitorJSON ? getMonitorRelativeURL(monitorJSON.id) : "/";
|
||||
sectionWidgets.push({
|
||||
buttonList: {
|
||||
buttons: [
|
||||
{
|
||||
text: "Visit Uptime Kuma",
|
||||
onClick: {
|
||||
openLink: {
|
||||
url: baseURL + urlPath,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const data = {
|
||||
"text": textMsg,
|
||||
let chatSections = [
|
||||
{
|
||||
widgets: sectionWidgets,
|
||||
},
|
||||
];
|
||||
|
||||
// construct json data
|
||||
let data = {
|
||||
cardsV2: [
|
||||
{
|
||||
card: {
|
||||
header: chatHeader,
|
||||
sections: chatSections,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await axios.post(notification.googleChatWebhookURL, data);
|
||||
|
@@ -2,14 +2,13 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Gorush extends NotificationProvider {
|
||||
|
||||
name = "gorush";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
let platformMapping = {
|
||||
"ios": 1,
|
||||
|
@@ -2,14 +2,14 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Gotify extends NotificationProvider {
|
||||
|
||||
name = "gotify";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
if (notification.gotifyserverurl && notification.gotifyserverurl.endsWith("/")) {
|
||||
notification.gotifyserverurl = notification.gotifyserverurl.slice(0, -1);
|
||||
|
@@ -3,19 +3,18 @@ const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class GrafanaOncall extends NotificationProvider {
|
||||
|
||||
name = "GrafanaOncall";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
if (!notification.GrafanaOncallURL) {
|
||||
throw new Error("GrafanaOncallURL cannot be empty");
|
||||
}
|
||||
|
||||
let okMsg = "Sent Successfully.";
|
||||
try {
|
||||
if (heartbeatJSON === null) {
|
||||
let grafanaupdata = {
|
||||
@@ -23,10 +22,7 @@ class GrafanaOncall extends NotificationProvider {
|
||||
message: msg,
|
||||
state: "alerting",
|
||||
};
|
||||
await axios.post(
|
||||
notification.GrafanaOncallURL,
|
||||
grafanaupdata
|
||||
);
|
||||
await axios.post(notification.GrafanaOncallURL, grafanaupdata);
|
||||
return okMsg;
|
||||
} else if (heartbeatJSON["status"] === DOWN) {
|
||||
let grafanadowndata = {
|
||||
@@ -34,10 +30,7 @@ class GrafanaOncall extends NotificationProvider {
|
||||
message: heartbeatJSON["msg"],
|
||||
state: "alerting",
|
||||
};
|
||||
await axios.post(
|
||||
notification.GrafanaOncallURL,
|
||||
grafanadowndata
|
||||
);
|
||||
await axios.post(notification.GrafanaOncallURL, grafanadowndata);
|
||||
return okMsg;
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
let grafanaupdata = {
|
||||
@@ -45,10 +38,7 @@ class GrafanaOncall extends NotificationProvider {
|
||||
message: heartbeatJSON["msg"],
|
||||
state: "ok",
|
||||
};
|
||||
await axios.post(
|
||||
notification.GrafanaOncallURL,
|
||||
grafanaupdata
|
||||
);
|
||||
await axios.post(notification.GrafanaOncallURL, grafanaupdata);
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
|
33
server/notification-providers/gtx-messaging.js
Normal file
33
server/notification-providers/gtx-messaging.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class GtxMessaging extends NotificationProvider {
|
||||
name = "gtxmessaging";
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
// The UP/DOWN symbols will be replaced with `???` by gtx-messaging
|
||||
const text = msg.replaceAll("🔴 ", "").replaceAll("✅ ", "");
|
||||
|
||||
try {
|
||||
const data = new URLSearchParams();
|
||||
data.append("from", notification.gtxMessagingFrom.trim());
|
||||
data.append("to", notification.gtxMessagingTo.trim());
|
||||
data.append("text", text);
|
||||
|
||||
const url = `https://rest.gtx-messaging.net/smsc/sendsms/${notification.gtxMessagingApiKey}/json`;
|
||||
|
||||
await axios.post(url, data);
|
||||
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GtxMessaging;
|
52
server/notification-providers/heii-oncall.js
Normal file
52
server/notification-providers/heii-oncall.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const { UP, DOWN, getMonitorRelativeURL } = require("../../src/util");
|
||||
const { setting } = require("../util-server");
|
||||
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
class HeiiOnCall extends NotificationProvider {
|
||||
name = "HeiiOnCall";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
const payload = heartbeatJSON || {};
|
||||
|
||||
const baseURL = await setting("primaryBaseURL");
|
||||
if (baseURL && monitorJSON) {
|
||||
payload["url"] = baseURL + getMonitorRelativeURL(monitorJSON.id);
|
||||
}
|
||||
|
||||
const config = {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + notification.heiiOnCallApiKey,
|
||||
},
|
||||
};
|
||||
const heiiUrl = `https://heiioncall.com/triggers/${notification.heiiOnCallTriggerId}/`;
|
||||
// docs https://heiioncall.com/docs#manual-triggers
|
||||
try {
|
||||
if (!heartbeatJSON) {
|
||||
// Testing or general notification like certificate expiry
|
||||
payload["msg"] = msg;
|
||||
await axios.post(heiiUrl + "alert", payload, config);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
if (heartbeatJSON.status === DOWN) {
|
||||
await axios.post(heiiUrl + "alert", payload, config);
|
||||
return okMsg;
|
||||
}
|
||||
if (heartbeatJSON.status === UP) {
|
||||
await axios.post(heiiUrl + "resolve", payload, config);
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HeiiOnCall;
|
@@ -9,7 +9,9 @@ class HomeAssistant extends NotificationProvider {
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, message, monitor = null, heartbeat = null) {
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
const notificationService = notification?.notificationService || defaultNotificationService;
|
||||
|
||||
try {
|
||||
@@ -17,10 +19,12 @@ class HomeAssistant extends NotificationProvider {
|
||||
`${notification.homeAssistantUrl.trim().replace(/\/*$/, "")}/api/services/notify/${notificationService}`,
|
||||
{
|
||||
title: "Uptime Kuma",
|
||||
message,
|
||||
message: msg,
|
||||
...(notificationService !== "persistent_notification" && { data: {
|
||||
name: monitor?.name,
|
||||
status: heartbeat?.status,
|
||||
name: monitorJSON?.name,
|
||||
status: heartbeatJSON?.status,
|
||||
channel: "Uptime Kuma",
|
||||
icon_url: "https://github.com/louislam/uptime-kuma/blob/master/public/icon.png?raw=true",
|
||||
} }),
|
||||
},
|
||||
{
|
||||
@@ -31,7 +35,7 @@ class HomeAssistant extends NotificationProvider {
|
||||
}
|
||||
);
|
||||
|
||||
return "Sent Successfully.";
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
42
server/notification-providers/keep.js
Normal file
42
server/notification-providers/keep.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Keep extends NotificationProvider {
|
||||
name = "Keep";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let data = {
|
||||
heartbeat: heartbeatJSON,
|
||||
monitor: monitorJSON,
|
||||
msg,
|
||||
};
|
||||
let config = {
|
||||
headers: {
|
||||
"x-api-key": notification.webhookAPIKey,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
let url = notification.webhookURL;
|
||||
|
||||
if (url.endsWith("/")) {
|
||||
url = url.slice(0, -1);
|
||||
}
|
||||
|
||||
let webhookURL = url + "/alerts/event/uptimekuma";
|
||||
|
||||
await axios.post(webhookURL, data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Keep;
|
@@ -2,15 +2,15 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Kook extends NotificationProvider {
|
||||
|
||||
name = "Kook";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
let url = "https://www.kookapp.cn/api/v3/message/create";
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://www.kookapp.cn/api/v3/message/create";
|
||||
|
||||
let data = {
|
||||
target_id: notification.kookGuildID,
|
||||
content: msg,
|
||||
|
@@ -3,16 +3,16 @@ const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class Line extends NotificationProvider {
|
||||
|
||||
name = "line";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://api.line.me/v2/bot/message/push";
|
||||
|
||||
try {
|
||||
let lineAPIUrl = "https://api.line.me/v2/bot/message/push";
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -29,7 +29,7 @@ class Line extends NotificationProvider {
|
||||
}
|
||||
]
|
||||
};
|
||||
await axios.post(lineAPIUrl, testMessage, config);
|
||||
await axios.post(url, testMessage, config);
|
||||
} else if (heartbeatJSON["status"] === DOWN) {
|
||||
let downMessage = {
|
||||
"to": notification.lineUserID,
|
||||
@@ -43,7 +43,7 @@ class Line extends NotificationProvider {
|
||||
}
|
||||
]
|
||||
};
|
||||
await axios.post(lineAPIUrl, downMessage, config);
|
||||
await axios.post(url, downMessage, config);
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
let upMessage = {
|
||||
"to": notification.lineUserID,
|
||||
@@ -57,7 +57,7 @@ class Line extends NotificationProvider {
|
||||
}
|
||||
]
|
||||
};
|
||||
await axios.post(lineAPIUrl, upMessage, config);
|
||||
await axios.post(url, upMessage, config);
|
||||
}
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
|
@@ -4,16 +4,16 @@ const qs = require("qs");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class LineNotify extends NotificationProvider {
|
||||
|
||||
name = "LineNotify";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://notify-api.line.me/api/notify";
|
||||
|
||||
try {
|
||||
let lineAPIUrl = "https://notify-api.line.me/api/notify";
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
@@ -24,7 +24,7 @@ class LineNotify extends NotificationProvider {
|
||||
let testMessage = {
|
||||
"message": msg,
|
||||
};
|
||||
await axios.post(lineAPIUrl, qs.stringify(testMessage), config);
|
||||
await axios.post(url, qs.stringify(testMessage), config);
|
||||
} else if (heartbeatJSON["status"] === DOWN) {
|
||||
let downMessage = {
|
||||
"message": "\n[🔴 Down]\n" +
|
||||
@@ -32,7 +32,7 @@ class LineNotify extends NotificationProvider {
|
||||
heartbeatJSON["msg"] + "\n" +
|
||||
`Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
};
|
||||
await axios.post(lineAPIUrl, qs.stringify(downMessage), config);
|
||||
await axios.post(url, qs.stringify(downMessage), config);
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
let upMessage = {
|
||||
"message": "\n[✅ Up]\n" +
|
||||
@@ -40,7 +40,7 @@ class LineNotify extends NotificationProvider {
|
||||
heartbeatJSON["msg"] + "\n" +
|
||||
`Time (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
};
|
||||
await axios.post(lineAPIUrl, qs.stringify(upMessage), config);
|
||||
await axios.post(url, qs.stringify(upMessage), config);
|
||||
}
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
|
@@ -3,28 +3,23 @@ const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class LunaSea extends NotificationProvider {
|
||||
|
||||
name = "lunasea";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
let lunaseaurl = "";
|
||||
if (notification.lunaseaTarget === "user") {
|
||||
lunaseaurl = "https://notify.lunasea.app/v1/custom/user/" + notification.lunaseaUserID;
|
||||
} else {
|
||||
lunaseaurl = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice;
|
||||
}
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://notify.lunasea.app/v1";
|
||||
|
||||
try {
|
||||
const target = this.getTarget(notification);
|
||||
if (heartbeatJSON == null) {
|
||||
let testdata = {
|
||||
"title": "Uptime Kuma Alert",
|
||||
"body": msg,
|
||||
};
|
||||
await axios.post(lunaseaurl, testdata);
|
||||
await axios.post(`${url}/custom/${target}`, testdata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
@@ -35,7 +30,7 @@ class LunaSea extends NotificationProvider {
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
};
|
||||
await axios.post(lunaseaurl, downdata);
|
||||
await axios.post(`${url}/custom/${target}`, downdata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
@@ -46,13 +41,25 @@ class LunaSea extends NotificationProvider {
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||
};
|
||||
await axios.post(lunaseaurl, updata);
|
||||
await axios.post(`${url}/custom/${target}`, updata);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the lunasea target to send the notification to
|
||||
* @param {BeanModel} notification Notification details
|
||||
* @returns {string} The target to send the notification to
|
||||
*/
|
||||
getTarget(notification) {
|
||||
if (notification.lunaseaTarget === "user") {
|
||||
return "user/" + notification.lunaseaUserID;
|
||||
}
|
||||
return "device/" + notification.lunaseaDevice;
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -10,7 +10,7 @@ class Matrix extends NotificationProvider {
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
const size = 20;
|
||||
const randomString = encodeURIComponent(
|
||||
|
@@ -3,14 +3,14 @@ const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class Mattermost extends NotificationProvider {
|
||||
|
||||
name = "mattermost";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
const mattermostUserName = notification.mattermostusername || "Uptime Kuma";
|
||||
// If heartbeatJSON is null, assume non monitoring notification (Certificate warning) or testing.
|
||||
@@ -98,10 +98,7 @@ class Mattermost extends NotificationProvider {
|
||||
},
|
||||
],
|
||||
};
|
||||
await axios.post(
|
||||
notification.mattermostWebhookUrl,
|
||||
mattermostdata
|
||||
);
|
||||
await axios.post(notification.mattermostWebhookUrl, mattermostdata);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
|
@@ -107,7 +107,7 @@ class Nostr extends NotificationProvider {
|
||||
/**
|
||||
* Get public keys for recipients
|
||||
* @param {string} recipients Newline delimited list of recipients
|
||||
* @returns {nip19.DecodeResult[]} Public keys
|
||||
* @returns {Promise<nip19.DecodeResult[]>} Public keys
|
||||
*/
|
||||
async getPublicKeys(recipients) {
|
||||
const recipientsList = recipients.split("\n");
|
||||
|
@@ -3,14 +3,14 @@ const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class Ntfy extends NotificationProvider {
|
||||
|
||||
name = "ntfy";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let headers = {};
|
||||
if (notification.ntfyAuthenticationMethod === "usernamePassword") {
|
||||
@@ -31,7 +31,7 @@ class Ntfy extends NotificationProvider {
|
||||
"priority": notification.ntfyPriority,
|
||||
"tags": [ "test_tube" ],
|
||||
};
|
||||
await axios.post(`${notification.ntfyserverurl}`, ntfyTestData, { headers: headers });
|
||||
await axios.post(notification.ntfyserverurl, ntfyTestData, { headers: headers });
|
||||
return okMsg;
|
||||
}
|
||||
let tags = [];
|
||||
@@ -54,20 +54,23 @@ class Ntfy extends NotificationProvider {
|
||||
"priority": priority,
|
||||
"title": monitorJSON.name + " " + status + " [Uptime-Kuma]",
|
||||
"tags": tags,
|
||||
"actions": [
|
||||
};
|
||||
|
||||
if (monitorJSON.url && monitorJSON.url !== "https://") {
|
||||
data.actions = [
|
||||
{
|
||||
"action": "view",
|
||||
"label": "Open " + monitorJSON.name,
|
||||
"url": monitorJSON.url,
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (notification.ntfyIcon) {
|
||||
data.icon = notification.ntfyIcon;
|
||||
}
|
||||
|
||||
await axios.post(`${notification.ntfyserverurl}`, data, { headers: headers });
|
||||
await axios.post(notification.ntfyserverurl, data, { headers: headers });
|
||||
|
||||
return okMsg;
|
||||
|
||||
|
@@ -2,14 +2,15 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Octopush extends NotificationProvider {
|
||||
|
||||
name = "octopush";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
const urlV2 = "https://api.octopush.com/v1/public/sms-campaign/send";
|
||||
const urlV1 = "https://www.octopush-dm.com/api/sms/json";
|
||||
|
||||
try {
|
||||
// Default - V2
|
||||
@@ -33,7 +34,7 @@ class Octopush extends NotificationProvider {
|
||||
"purpose": "alert",
|
||||
"sender": notification.octopushSenderName
|
||||
};
|
||||
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config);
|
||||
await axios.post(urlV2, data, config);
|
||||
} else if (notification.octopushVersion === "1") {
|
||||
let data = {
|
||||
"user_login": notification.octopushDMLogin,
|
||||
@@ -55,7 +56,7 @@ class Octopush extends NotificationProvider {
|
||||
|
||||
// V1 API returns 200 even on error so we must check
|
||||
// response data
|
||||
let response = await axios.post("https://www.octopush-dm.com/api/sms/json", {}, config);
|
||||
let response = await axios.post(urlV1, {}, config);
|
||||
if ("error_code" in response.data) {
|
||||
if (response.data.error_code !== "000") {
|
||||
this.throwGeneralAxiosError(`Octopush error ${JSON.stringify(response.data)}`);
|
||||
|
@@ -2,23 +2,23 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class OneBot extends NotificationProvider {
|
||||
|
||||
name = "OneBot";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let httpAddr = notification.httpAddr;
|
||||
if (!httpAddr.startsWith("http")) {
|
||||
httpAddr = "http://" + httpAddr;
|
||||
let url = notification.httpAddr;
|
||||
if (!url.startsWith("http")) {
|
||||
url = "http://" + url;
|
||||
}
|
||||
if (!httpAddr.endsWith("/")) {
|
||||
httpAddr += "/";
|
||||
if (!url.endsWith("/")) {
|
||||
url += "/";
|
||||
}
|
||||
let onebotAPIUrl = httpAddr + "send_msg";
|
||||
url += "send_msg";
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -37,7 +37,7 @@ class OneBot extends NotificationProvider {
|
||||
data["message_type"] = "private";
|
||||
data["user_id"] = notification.recieverId;
|
||||
}
|
||||
await axios.post(onebotAPIUrl, data, config);
|
||||
await axios.post(url, data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
|
@@ -4,10 +4,9 @@ const { UP, DOWN } = require("../../src/util");
|
||||
|
||||
const opsgenieAlertsUrlEU = "https://api.eu.opsgenie.com/v2/alerts";
|
||||
const opsgenieAlertsUrlUS = "https://api.opsgenie.com/v2/alerts";
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
class Opsgenie extends NotificationProvider {
|
||||
|
||||
name = "Opsgenie";
|
||||
|
||||
/**
|
||||
|
@@ -2,14 +2,14 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class PromoSMS extends NotificationProvider {
|
||||
|
||||
name = "promosms";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://promosms.com/api/rest/v3_2/sms";
|
||||
|
||||
if (notification.promosmsAllowLongSMS === undefined) {
|
||||
notification.promosmsAllowLongSMS = false;
|
||||
@@ -36,7 +36,7 @@ class PromoSMS extends NotificationProvider {
|
||||
"sender": notification.promosmsSenderName
|
||||
};
|
||||
|
||||
let resp = await axios.post("https://promosms.com/api/rest/v3_2/sms", data, config);
|
||||
let resp = await axios.post(url, data, config);
|
||||
|
||||
if (resp.data.response.status !== 0) {
|
||||
let error = "Something gone wrong. Api returned " + resp.data.response.status + ".";
|
||||
|
@@ -4,17 +4,16 @@ const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class Pushbullet extends NotificationProvider {
|
||||
|
||||
name = "pushbullet";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://api.pushbullet.com/v2/pushes";
|
||||
|
||||
try {
|
||||
let pushbulletUrl = "https://api.pushbullet.com/v2/pushes";
|
||||
let config = {
|
||||
headers: {
|
||||
"Access-Token": notification.pushbulletAccessToken,
|
||||
@@ -27,7 +26,7 @@ class Pushbullet extends NotificationProvider {
|
||||
"title": "Uptime Kuma Alert",
|
||||
"body": msg,
|
||||
};
|
||||
await axios.post(pushbulletUrl, data, config);
|
||||
await axios.post(url, data, config);
|
||||
} else if (heartbeatJSON["status"] === DOWN) {
|
||||
let downData = {
|
||||
"type": "note",
|
||||
@@ -36,7 +35,7 @@ class Pushbullet extends NotificationProvider {
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
||||
};
|
||||
await axios.post(pushbulletUrl, downData, config);
|
||||
await axios.post(url, downData, config);
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
let upData = {
|
||||
"type": "note",
|
||||
@@ -45,7 +44,7 @@ class Pushbullet extends NotificationProvider {
|
||||
heartbeatJSON["msg"] +
|
||||
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
||||
};
|
||||
await axios.post(pushbulletUrl, upData, config);
|
||||
await axios.post(url, upData, config);
|
||||
}
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
|
@@ -3,17 +3,15 @@ const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class PushDeer extends NotificationProvider {
|
||||
|
||||
name = "PushDeer";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
let endpoint = "/message/push";
|
||||
let serverUrl = notification.pushdeerServer || "https://api2.pushdeer.com";
|
||||
let pushdeerlink = `${serverUrl.trim().replace(/\/*$/, "")}${endpoint}`;
|
||||
const okMsg = "Sent Successfully.";
|
||||
const serverUrl = notification.pushdeerServer || "https://api2.pushdeer.com";
|
||||
const url = `${serverUrl.trim().replace(/\/*$/, "")}/message/push`;
|
||||
|
||||
let valid = msg != null && monitorJSON != null && heartbeatJSON != null;
|
||||
|
||||
@@ -34,7 +32,7 @@ class PushDeer extends NotificationProvider {
|
||||
};
|
||||
|
||||
try {
|
||||
let res = await axios.post(pushdeerlink, data);
|
||||
let res = await axios.post(url, data);
|
||||
|
||||
if ("error" in res.data) {
|
||||
let error = res.data.error;
|
||||
|
@@ -2,15 +2,14 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Pushover extends NotificationProvider {
|
||||
|
||||
name = "pushover";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
let pushoverlink = "https://api.pushover.net/1/messages.json";
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://api.pushover.net/1/messages.json";
|
||||
|
||||
let data = {
|
||||
"message": msg,
|
||||
@@ -33,11 +32,11 @@ class Pushover extends NotificationProvider {
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
await axios.post(pushoverlink, data);
|
||||
await axios.post(url, data);
|
||||
return okMsg;
|
||||
} else {
|
||||
data.message += `\n<b>Time (${heartbeatJSON["timezone"]})</b>:${heartbeatJSON["localDateTime"]}`;
|
||||
await axios.post(pushoverlink, data);
|
||||
await axios.post(url, data);
|
||||
return okMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
|
@@ -2,14 +2,13 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Pushy extends NotificationProvider {
|
||||
|
||||
name = "pushy";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
await axios.post(`https://api.pushy.me/push?api_key=${notification.pushyAPIKey}`, {
|
||||
|
@@ -5,14 +5,14 @@ const { setting } = require("../util-server");
|
||||
const { getMonitorRelativeURL, DOWN } = require("../../src/util");
|
||||
|
||||
class RocketChat extends NotificationProvider {
|
||||
|
||||
name = "rocket.chat";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
let data = {
|
||||
|
@@ -3,14 +3,14 @@ const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class ServerChan extends NotificationProvider {
|
||||
|
||||
name = "ServerChan";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
await axios.post(`https://sctapi.ftqq.com/${notification.serverChanSendKey}.send`, {
|
||||
"title": this.checkStatus(heartbeatJSON, monitorJSON),
|
||||
|
@@ -2,14 +2,14 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class SerwerSMS extends NotificationProvider {
|
||||
|
||||
name = "serwersms";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://api2.serwersms.pl/messages/send_sms";
|
||||
|
||||
try {
|
||||
let config = {
|
||||
@@ -25,7 +25,7 @@ class SerwerSMS extends NotificationProvider {
|
||||
"sender": notification.serwersmsSenderName,
|
||||
};
|
||||
|
||||
let resp = await axios.post("https://api2.serwersms.pl/messages/send_sms", data, config);
|
||||
let resp = await axios.post(url, data, config);
|
||||
|
||||
if (!resp.data.success) {
|
||||
if (resp.data.error) {
|
||||
|
78
server/notification-providers/sevenio.js
Normal file
78
server/notification-providers/sevenio.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class SevenIO extends NotificationProvider {
|
||||
name = "SevenIO";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
const data = {
|
||||
to: notification.sevenioTo,
|
||||
from: notification.sevenioSender || "Uptime Kuma",
|
||||
text: msg,
|
||||
};
|
||||
|
||||
const config = {
|
||||
baseURL: "https://gateway.seven.io/api/",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": notification.sevenioApiKey,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
// testing or certificate expiry notification
|
||||
if (heartbeatJSON == null) {
|
||||
await axios.post("sms", data, config);
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
let address = "";
|
||||
|
||||
switch (monitorJSON["type"]) {
|
||||
case "ping":
|
||||
address = monitorJSON["hostname"];
|
||||
break;
|
||||
case "port":
|
||||
case "dns":
|
||||
case "gamedig":
|
||||
case "steam":
|
||||
address = monitorJSON["hostname"];
|
||||
if (monitorJSON["port"]) {
|
||||
address += ":" + monitorJSON["port"];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (![ "https://", "http://", "" ].includes(monitorJSON["url"])) {
|
||||
address = monitorJSON["url"];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (address !== "") {
|
||||
address = `(${address}) `;
|
||||
}
|
||||
|
||||
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
||||
if (heartbeatJSON["status"] === DOWN) {
|
||||
data.text = `Your service ${monitorJSON["name"]} ${address}went down at ${heartbeatJSON["localDateTime"]} ` +
|
||||
`(${heartbeatJSON["timezone"]}). Error: ${heartbeatJSON["msg"]}`;
|
||||
} else if (heartbeatJSON["status"] === UP) {
|
||||
data.text = `Your service ${monitorJSON["name"]} ${address}went back up at ${heartbeatJSON["localDateTime"]} ` +
|
||||
`(${heartbeatJSON["timezone"]}).`;
|
||||
}
|
||||
await axios.post("sms", data, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = SevenIO;
|
@@ -2,14 +2,13 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Signal extends NotificationProvider {
|
||||
|
||||
name = "signal";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let data = {
|
||||
|
@@ -4,7 +4,6 @@ const { setSettings, setting } = require("../util-server");
|
||||
const { getMonitorRelativeURL, UP } = require("../../src/util");
|
||||
|
||||
class Slack extends NotificationProvider {
|
||||
|
||||
name = "slack";
|
||||
|
||||
/**
|
||||
@@ -27,11 +26,98 @@ class Slack extends NotificationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the actions available in the slack message
|
||||
* @param {string} baseURL Uptime Kuma base URL
|
||||
* @param {object} monitorJSON The monitor config
|
||||
* @returns {Array} The relevant action objects
|
||||
*/
|
||||
static buildActions(baseURL, monitorJSON) {
|
||||
const actions = [];
|
||||
|
||||
if (baseURL) {
|
||||
actions.push({
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Visit Uptime Kuma",
|
||||
},
|
||||
"value": "Uptime-Kuma",
|
||||
"url": baseURL + getMonitorRelativeURL(monitorJSON.id),
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
if (monitorJSON.url) {
|
||||
actions.push({
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Visit site",
|
||||
},
|
||||
"value": "Site",
|
||||
"url": monitorJSON.url,
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the different blocks the Slack message consists of.
|
||||
* @param {string} baseURL Uptime Kuma base URL
|
||||
* @param {object} monitorJSON The monitor object
|
||||
* @param {object} heartbeatJSON The heartbeat object
|
||||
* @param {string} title The message title
|
||||
* @param {string} msg The message body
|
||||
* @returns {Array<object>} The rich content blocks for the Slack message
|
||||
*/
|
||||
static buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg) {
|
||||
|
||||
//create an array to dynamically add blocks
|
||||
const blocks = [];
|
||||
|
||||
// the header block
|
||||
blocks.push({
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": title,
|
||||
},
|
||||
});
|
||||
|
||||
// the body block, containing the details
|
||||
blocks.push({
|
||||
"type": "section",
|
||||
"fields": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Message*\n" + msg,
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": `*Time (${heartbeatJSON["timezone"]})*\n${heartbeatJSON["localDateTime"]}`,
|
||||
}
|
||||
],
|
||||
});
|
||||
|
||||
const actions = this.buildActions(baseURL, monitorJSON);
|
||||
if (actions.length > 0) {
|
||||
//the actions block, containing buttons
|
||||
blocks.push({
|
||||
"type": "actions",
|
||||
"elements": actions,
|
||||
});
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
if (notification.slackchannelnotify) {
|
||||
msg += " <!channel>";
|
||||
@@ -49,35 +135,18 @@ class Slack extends NotificationProvider {
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
const textMsg = "Uptime Kuma Alert";
|
||||
const baseURL = await setting("primaryBaseURL");
|
||||
|
||||
const title = "Uptime Kuma Alert";
|
||||
let data = {
|
||||
"text": `${textMsg}\n${msg}`,
|
||||
"text": `${title}\n${msg}`,
|
||||
"channel": notification.slackchannel,
|
||||
"username": notification.slackusername,
|
||||
"icon_emoji": notification.slackiconemo,
|
||||
"attachments": [
|
||||
{
|
||||
"color": (heartbeatJSON["status"] === UP) ? "#2eb886" : "#e01e5a",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": textMsg,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Message*\n" + msg,
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": `*Time (${heartbeatJSON["timezone"]})*\n${heartbeatJSON["localDateTime"]}`,
|
||||
}],
|
||||
}
|
||||
],
|
||||
"blocks": Slack.buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg),
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -86,26 +155,6 @@ class Slack extends NotificationProvider {
|
||||
await Slack.deprecateURL(notification.slackbutton);
|
||||
}
|
||||
|
||||
const baseURL = await setting("primaryBaseURL");
|
||||
|
||||
// Button
|
||||
if (baseURL) {
|
||||
data.attachments.forEach(element => {
|
||||
element.blocks.push({
|
||||
"type": "actions",
|
||||
"elements": [{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Visit Uptime Kuma",
|
||||
},
|
||||
"value": "Uptime-Kuma",
|
||||
"url": baseURL + getMonitorRelativeURL(monitorJSON.id),
|
||||
}],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await axios.post(notification.slackwebhookURL, data);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
|
@@ -8,7 +8,9 @@ class SMSC extends NotificationProvider {
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://smsc.kz/sys/send.php?";
|
||||
|
||||
try {
|
||||
let config = {
|
||||
headers: {
|
||||
@@ -29,7 +31,7 @@ class SMSC extends NotificationProvider {
|
||||
getArray.push("sender=" + notification.smscSenderName);
|
||||
}
|
||||
|
||||
let resp = await axios.get("https://smsc.kz/sys/send.php?" + getArray.join("&"), config);
|
||||
let resp = await axios.get(url + getArray.join("&"), config);
|
||||
if (resp.data.id === undefined) {
|
||||
let error = `Something gone wrong. Api returned code ${resp.data.error_code}: ${resp.data.error}`;
|
||||
this.throwGeneralAxiosError(error);
|
||||
|
@@ -2,14 +2,13 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class SMSEagle extends NotificationProvider {
|
||||
|
||||
name = "SMSEagle";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let config = {
|
||||
|
@@ -2,23 +2,24 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class SMSManager extends NotificationProvider {
|
||||
|
||||
name = "SMSManager";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://http-api.smsmanager.cz/Send";
|
||||
|
||||
try {
|
||||
let data = {
|
||||
apikey: notification.smsmanagerApiKey,
|
||||
endpoint: "https://http-api.smsmanager.cz/Send",
|
||||
message: msg.replace(/[^\x00-\x7F]/g, ""),
|
||||
to: notification.numbers,
|
||||
messageType: notification.messageType,
|
||||
number: notification.numbers,
|
||||
gateway: notification.messageType,
|
||||
};
|
||||
await axios.get(`${data.endpoint}?apikey=${data.apikey}&message=${data.message}&number=${data.to}&gateway=${data.messageType}`);
|
||||
return "SMS sent sucessfully.";
|
||||
await axios.get(`${url}?apikey=${data.apikey}&message=${data.message}&number=${data.number}&gateway=${data.messageType}`);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
|
46
server/notification-providers/smspartner.js
Normal file
46
server/notification-providers/smspartner.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class SMSPartner extends NotificationProvider {
|
||||
name = "SMSPartner";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://api.smspartner.fr/v1/send";
|
||||
|
||||
try {
|
||||
// smspartner does not support non ascii characters and only a maximum 639 characters
|
||||
let cleanMsg = msg.replace(/[^\x00-\x7F]/g, "").substring(0, 639);
|
||||
|
||||
let data = {
|
||||
"apiKey": notification.smspartnerApikey,
|
||||
"sender": notification.smspartnerSenderName.substring(0, 11),
|
||||
"phoneNumbers": notification.smspartnerPhoneNumber,
|
||||
"message": cleanMsg,
|
||||
};
|
||||
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"cache-control": "no-cache",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
};
|
||||
|
||||
let resp = await axios.post(url, data, config);
|
||||
|
||||
if (resp.data.success !== true) {
|
||||
throw Error(`Api returned ${resp.data.response.status}.`);
|
||||
}
|
||||
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SMSPartner;
|
@@ -4,13 +4,13 @@ const { DOWN } = require("../../src/util");
|
||||
const { Liquid } = require("liquidjs");
|
||||
|
||||
class SMTP extends NotificationProvider {
|
||||
|
||||
name = "smtp";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
const config = {
|
||||
host: notification.smtpHost,
|
||||
@@ -76,7 +76,7 @@ class SMTP extends NotificationProvider {
|
||||
text: body,
|
||||
});
|
||||
|
||||
return "Sent Successfully.";
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -3,14 +3,13 @@ const axios = require("axios");
|
||||
const { DOWN } = require("../../src/util");
|
||||
|
||||
class Squadcast extends NotificationProvider {
|
||||
|
||||
name = "squadcast";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
|
||||
|
@@ -4,14 +4,14 @@ const { setting } = require("../util-server");
|
||||
const { getMonitorRelativeURL } = require("../../src/util");
|
||||
|
||||
class Stackfield extends NotificationProvider {
|
||||
|
||||
name = "stackfield";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
// Stackfield message formatting: https://www.stackfield.com/help/formatting-messages-2001
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
const { setting } = require("../util-server");
|
||||
const { DOWN, UP, getMonitorRelativeURL } = require("../../src/util");
|
||||
|
||||
class Teams extends NotificationProvider {
|
||||
name = "teams";
|
||||
@@ -9,89 +10,172 @@ class Teams extends NotificationProvider {
|
||||
* Generate the message to send
|
||||
* @param {const} status The status constant
|
||||
* @param {string} monitorName Name of monitor
|
||||
* @param {boolean} withStatusSymbol If the status should be prepended as symbol
|
||||
* @returns {string} Status message
|
||||
*/
|
||||
_statusMessageFactory = (status, monitorName) => {
|
||||
_statusMessageFactory = (status, monitorName, withStatusSymbol) => {
|
||||
if (status === DOWN) {
|
||||
return `🔴 Application [${monitorName}] went down`;
|
||||
return (withStatusSymbol ? "🔴 " : "") + `[${monitorName}] went down`;
|
||||
} else if (status === UP) {
|
||||
return `✅ Application [${monitorName}] is back online`;
|
||||
return (withStatusSymbol ? "✅ " : "") + `[${monitorName}] is back online`;
|
||||
}
|
||||
return "Notification";
|
||||
};
|
||||
|
||||
/**
|
||||
* Select theme color to use based on status
|
||||
* Select the style to use based on status
|
||||
* @param {const} status The status constant
|
||||
* @returns {string} Selected color in hex RGB format
|
||||
* @returns {string} Selected style for adaptive cards
|
||||
*/
|
||||
_getThemeColor = (status) => {
|
||||
_getStyle = (status) => {
|
||||
if (status === DOWN) {
|
||||
return "ff0000";
|
||||
return "attention";
|
||||
}
|
||||
if (status === UP) {
|
||||
return "00e804";
|
||||
return "good";
|
||||
}
|
||||
return "008cff";
|
||||
return "emphasis";
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate payload for notification
|
||||
* @param {object} args Method arguments
|
||||
* @param {const} args.status The status of the monitor
|
||||
* @param {string} args.monitorMessage Message to send
|
||||
* @param {string} args.monitorName Name of monitor affected
|
||||
* @param {string} args.monitorUrl URL of monitor affected
|
||||
* @param {object} args.heartbeatJSON Heartbeat details
|
||||
* @param {string} args.monitorName Name of the monitor affected
|
||||
* @param {string} args.monitorUrl URL of the monitor affected
|
||||
* @param {string} args.dashboardUrl URL of the dashboard affected
|
||||
* @returns {object} Notification payload
|
||||
*/
|
||||
_notificationPayloadFactory = ({
|
||||
status,
|
||||
monitorMessage,
|
||||
heartbeatJSON,
|
||||
monitorName,
|
||||
monitorUrl,
|
||||
dashboardUrl,
|
||||
}) => {
|
||||
const notificationMessage = this._statusMessageFactory(
|
||||
status,
|
||||
monitorName
|
||||
);
|
||||
|
||||
const status = heartbeatJSON?.status;
|
||||
const facts = [];
|
||||
const actions = [];
|
||||
|
||||
if (dashboardUrl) {
|
||||
actions.push({
|
||||
"type": "Action.OpenUrl",
|
||||
"title": "Visit Uptime Kuma",
|
||||
"url": dashboardUrl
|
||||
});
|
||||
}
|
||||
|
||||
if (heartbeatJSON?.msg) {
|
||||
facts.push({
|
||||
title: "Description",
|
||||
value: heartbeatJSON.msg,
|
||||
});
|
||||
}
|
||||
|
||||
if (monitorName) {
|
||||
facts.push({
|
||||
name: "Monitor",
|
||||
title: "Monitor",
|
||||
value: monitorName,
|
||||
});
|
||||
}
|
||||
|
||||
if (monitorUrl && monitorUrl !== "https://") {
|
||||
facts.push({
|
||||
name: "URL",
|
||||
value: monitorUrl,
|
||||
title: "URL",
|
||||
// format URL as markdown syntax, to be clickable
|
||||
value: `[${monitorUrl}](${monitorUrl})`,
|
||||
});
|
||||
actions.push({
|
||||
"type": "Action.OpenUrl",
|
||||
"title": "Visit Monitor URL",
|
||||
"url": monitorUrl
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
"@context": "https://schema.org/extensions",
|
||||
"@type": "MessageCard",
|
||||
themeColor: this._getThemeColor(status),
|
||||
summary: notificationMessage,
|
||||
sections: [
|
||||
if (heartbeatJSON?.localDateTime) {
|
||||
facts.push({
|
||||
title: "Time",
|
||||
value: heartbeatJSON.localDateTime + (heartbeatJSON.timezone ? ` (${heartbeatJSON.timezone})` : ""),
|
||||
});
|
||||
}
|
||||
|
||||
const payload = {
|
||||
"type": "message",
|
||||
// message with status prefix as notification text
|
||||
"summary": this._statusMessageFactory(status, monitorName, true),
|
||||
"attachments": [
|
||||
{
|
||||
activityImage:
|
||||
"https://raw.githubusercontent.com/louislam/uptime-kuma/master/public/icon.png",
|
||||
activityTitle: "**Uptime Kuma**",
|
||||
},
|
||||
{
|
||||
activityTitle: notificationMessage,
|
||||
},
|
||||
{
|
||||
activityTitle: "**Description**",
|
||||
text: monitorMessage,
|
||||
facts,
|
||||
},
|
||||
],
|
||||
"contentType": "application/vnd.microsoft.card.adaptive",
|
||||
"contentUrl": "",
|
||||
"content": {
|
||||
"type": "AdaptiveCard",
|
||||
"body": [
|
||||
{
|
||||
"type": "Container",
|
||||
"verticalContentAlignment": "Center",
|
||||
"items": [
|
||||
{
|
||||
"type": "ColumnSet",
|
||||
"style": this._getStyle(status),
|
||||
"columns": [
|
||||
{
|
||||
"type": "Column",
|
||||
"width": "auto",
|
||||
"verticalContentAlignment": "Center",
|
||||
"items": [
|
||||
{
|
||||
"type": "Image",
|
||||
"width": "32px",
|
||||
"style": "Person",
|
||||
"url": "https://raw.githubusercontent.com/louislam/uptime-kuma/master/public/icon.png",
|
||||
"altText": "Uptime Kuma Logo"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Column",
|
||||
"width": "stretch",
|
||||
"items": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"size": "Medium",
|
||||
"weight": "Bolder",
|
||||
"text": `**${this._statusMessageFactory(status, monitorName, false)}**`,
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"size": "Small",
|
||||
"weight": "Default",
|
||||
"text": "Uptime Kuma Alert",
|
||||
"isSubtle": true,
|
||||
"spacing": "None"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "FactSet",
|
||||
"separator": false,
|
||||
"facts": facts
|
||||
}
|
||||
],
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"version": "1.5"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (actions) {
|
||||
payload.attachments[0].content.body.push({
|
||||
"type": "ActionSet",
|
||||
"actions": actions,
|
||||
});
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -112,7 +196,9 @@ class Teams extends NotificationProvider {
|
||||
*/
|
||||
_handleGeneralNotification = (webhookUrl, msg) => {
|
||||
const payload = this._notificationPayloadFactory({
|
||||
monitorMessage: msg
|
||||
heartbeatJSON: {
|
||||
msg: msg
|
||||
}
|
||||
});
|
||||
|
||||
return this._sendNotification(webhookUrl, payload);
|
||||
@@ -122,7 +208,7 @@ class Teams extends NotificationProvider {
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
@@ -130,26 +216,32 @@ class Teams extends NotificationProvider {
|
||||
return okMsg;
|
||||
}
|
||||
|
||||
let url;
|
||||
let monitorUrl;
|
||||
|
||||
switch (monitorJSON["type"]) {
|
||||
case "http":
|
||||
case "keywork":
|
||||
url = monitorJSON["url"];
|
||||
monitorUrl = monitorJSON["url"];
|
||||
break;
|
||||
case "docker":
|
||||
url = monitorJSON["docker_host"];
|
||||
monitorUrl = monitorJSON["docker_host"];
|
||||
break;
|
||||
default:
|
||||
url = monitorJSON["hostname"];
|
||||
monitorUrl = monitorJSON["hostname"];
|
||||
break;
|
||||
}
|
||||
|
||||
const baseURL = await setting("primaryBaseURL");
|
||||
let dashboardUrl;
|
||||
if (baseURL) {
|
||||
dashboardUrl = baseURL + getMonitorRelativeURL(monitorJSON.id);
|
||||
}
|
||||
|
||||
const payload = this._notificationPayloadFactory({
|
||||
monitorMessage: heartbeatJSON.msg,
|
||||
heartbeatJSON: heartbeatJSON,
|
||||
monitorName: monitorJSON.name,
|
||||
monitorUrl: url,
|
||||
status: heartbeatJSON.status,
|
||||
monitorUrl: monitorUrl,
|
||||
dashboardUrl: dashboardUrl,
|
||||
});
|
||||
|
||||
await this._sendNotification(notification.webhookUrl, payload);
|
||||
|
@@ -2,14 +2,13 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class TechulusPush extends NotificationProvider {
|
||||
|
||||
name = "PushByTechulus";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
await axios.post(`https://push.techulus.com/api/v1/notify/${notification.pushAPIKey}`, {
|
||||
|
@@ -2,14 +2,14 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Telegram extends NotificationProvider {
|
||||
|
||||
name = "telegram";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = "https://api.telegram.org";
|
||||
|
||||
try {
|
||||
let params = {
|
||||
@@ -22,7 +22,7 @@ class Telegram extends NotificationProvider {
|
||||
params.message_thread_id = notification.telegramMessageThreadID;
|
||||
}
|
||||
|
||||
await axios.get(`https://api.telegram.org/bot${notification.telegramBotToken}/sendMessage`, {
|
||||
await axios.get(`${url}/bot${notification.telegramBotToken}/sendMessage`, {
|
||||
params: params,
|
||||
});
|
||||
return okMsg;
|
||||
|
@@ -2,26 +2,21 @@ const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Twilio extends NotificationProvider {
|
||||
|
||||
name = "twilio";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
let okMsg = "Sent Successfully.";
|
||||
|
||||
let accountSID = notification.twilioAccountSID;
|
||||
let apiKey = notification.twilioApiKey ? notification.twilioApiKey : accountSID;
|
||||
let authToken = notification.twilioAuthToken;
|
||||
let apiKey = notification.twilioApiKey ? notification.twilioApiKey : notification.twilioAccountSID;
|
||||
|
||||
try {
|
||||
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
|
||||
"Authorization": "Basic " + Buffer.from(apiKey + ":" + authToken).toString("base64"),
|
||||
"Authorization": "Basic " + Buffer.from(apiKey + ":" + notification.twilioAuthToken).toString("base64"),
|
||||
}
|
||||
};
|
||||
|
||||
@@ -30,9 +25,7 @@ class Twilio extends NotificationProvider {
|
||||
data.append("From", notification.twilioFromNumber);
|
||||
data.append("Body", msg);
|
||||
|
||||
let url = "https://api.twilio.com/2010-04-01/Accounts/" + accountSID + "/Messages.json";
|
||||
|
||||
await axios.post(url, data, config);
|
||||
await axios.post(`https://api.twilio.com/2010-04-01/Accounts/${(notification.twilioAccountSID)}/Messages.json`, data, config);
|
||||
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
|
@@ -4,14 +4,13 @@ const FormData = require("form-data");
|
||||
const { Liquid } = require("liquidjs");
|
||||
|
||||
class Webhook extends NotificationProvider {
|
||||
|
||||
name = "webhook";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let data = {
|
||||
|
@@ -3,24 +3,22 @@ const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class WeCom extends NotificationProvider {
|
||||
|
||||
name = "WeCom";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
let WeComUrl = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=" + notification.weComBotKey;
|
||||
let config = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
};
|
||||
let body = this.composeMessage(heartbeatJSON, msg);
|
||||
await axios.post(WeComUrl, body, config);
|
||||
await axios.post(`https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${notification.weComBotKey}`, body, config);
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
|
39
server/notification-providers/whapi.js
Normal file
39
server/notification-providers/whapi.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
|
||||
class Whapi extends NotificationProvider {
|
||||
name = "whapi";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
const config = {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + notification.whapiAuthToken,
|
||||
}
|
||||
};
|
||||
|
||||
let data = {
|
||||
"to": notification.whapiRecipient,
|
||||
"body": msg,
|
||||
};
|
||||
|
||||
let url = (notification.whapiApiUrl || "https://gate.whapi.cloud/").replace(/\/+$/, "") + "/messages/text";
|
||||
|
||||
await axios.post(url, data, config);
|
||||
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Whapi;
|
@@ -3,7 +3,6 @@ const axios = require("axios");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
|
||||
class ZohoCliq extends NotificationProvider {
|
||||
|
||||
name = "ZohoCliq";
|
||||
|
||||
/**
|
||||
@@ -80,7 +79,7 @@ class ZohoCliq extends NotificationProvider {
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
let okMsg = "Sent Successfully.";
|
||||
const okMsg = "Sent Successfully.";
|
||||
|
||||
try {
|
||||
if (heartbeatJSON == null) {
|
||||
|
@@ -5,7 +5,9 @@ const AlertNow = require("./notification-providers/alertnow");
|
||||
const AliyunSms = require("./notification-providers/aliyun-sms");
|
||||
const Apprise = require("./notification-providers/apprise");
|
||||
const Bark = require("./notification-providers/bark");
|
||||
const Bitrix24 = require("./notification-providers/bitrix24");
|
||||
const ClickSendSMS = require("./notification-providers/clicksendsms");
|
||||
const CallMeBot = require("./notification-providers/call-me-bot");
|
||||
const SMSC = require("./notification-providers/smsc");
|
||||
const DingDing = require("./notification-providers/dingding");
|
||||
const Discord = require("./notification-providers/discord");
|
||||
@@ -16,6 +18,8 @@ const Gorush = require("./notification-providers/gorush");
|
||||
const Gotify = require("./notification-providers/gotify");
|
||||
const GrafanaOncall = require("./notification-providers/grafana-oncall");
|
||||
const HomeAssistant = require("./notification-providers/home-assistant");
|
||||
const HeiiOnCall = require("./notification-providers/heii-oncall");
|
||||
const Keep = require("./notification-providers/keep");
|
||||
const Kook = require("./notification-providers/kook");
|
||||
const Line = require("./notification-providers/line");
|
||||
const LineNotify = require("./notification-providers/linenotify");
|
||||
@@ -39,6 +43,7 @@ const RocketChat = require("./notification-providers/rocket-chat");
|
||||
const SerwerSMS = require("./notification-providers/serwersms");
|
||||
const Signal = require("./notification-providers/signal");
|
||||
const Slack = require("./notification-providers/slack");
|
||||
const SMSPartner = require("./notification-providers/smspartner");
|
||||
const SMSEagle = require("./notification-providers/smseagle");
|
||||
const SMTP = require("./notification-providers/smtp");
|
||||
const Squadcast = require("./notification-providers/squadcast");
|
||||
@@ -54,6 +59,10 @@ const GoAlert = require("./notification-providers/goalert");
|
||||
const SMSManager = require("./notification-providers/smsmanager");
|
||||
const ServerChan = require("./notification-providers/serverchan");
|
||||
const ZohoCliq = require("./notification-providers/zoho-cliq");
|
||||
const SevenIO = require("./notification-providers/sevenio");
|
||||
const Whapi = require("./notification-providers/whapi");
|
||||
const GtxMessaging = require("./notification-providers/gtx-messaging");
|
||||
const Cellsynt = require("./notification-providers/cellsynt");
|
||||
|
||||
class Notification {
|
||||
|
||||
@@ -76,7 +85,9 @@ class Notification {
|
||||
new AliyunSms(),
|
||||
new Apprise(),
|
||||
new Bark(),
|
||||
new Bitrix24(),
|
||||
new ClickSendSMS(),
|
||||
new CallMeBot(),
|
||||
new SMSC(),
|
||||
new DingDing(),
|
||||
new Discord(),
|
||||
@@ -87,6 +98,8 @@ class Notification {
|
||||
new Gotify(),
|
||||
new GrafanaOncall(),
|
||||
new HomeAssistant(),
|
||||
new HeiiOnCall(),
|
||||
new Keep(),
|
||||
new Kook(),
|
||||
new Line(),
|
||||
new LineNotify(),
|
||||
@@ -111,6 +124,7 @@ class Notification {
|
||||
new SerwerSMS(),
|
||||
new Signal(),
|
||||
new SMSManager(),
|
||||
new SMSPartner(),
|
||||
new Slack(),
|
||||
new SMSEagle(),
|
||||
new SMTP(),
|
||||
@@ -124,7 +138,11 @@ class Notification {
|
||||
new Webhook(),
|
||||
new WeCom(),
|
||||
new GoAlert(),
|
||||
new ZohoCliq()
|
||||
new ZohoCliq(),
|
||||
new SevenIO(),
|
||||
new Whapi(),
|
||||
new GtxMessaging(),
|
||||
new Cellsynt(),
|
||||
];
|
||||
for (let item of list) {
|
||||
if (! item.name) {
|
||||
|
@@ -80,23 +80,25 @@ class Prometheus {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
monitorStatus.set(this.monitorLabelValues, heartbeat.status);
|
||||
} catch (e) {
|
||||
log.error("prometheus", "Caught error");
|
||||
log.error("prometheus", e);
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof heartbeat.ping === "number") {
|
||||
monitorResponseTime.set(this.monitorLabelValues, heartbeat.ping);
|
||||
} else {
|
||||
// Is it good?
|
||||
monitorResponseTime.set(this.monitorLabelValues, -1);
|
||||
if (heartbeat) {
|
||||
try {
|
||||
monitorStatus.set(this.monitorLabelValues, heartbeat.status);
|
||||
} catch (e) {
|
||||
log.error("prometheus", "Caught error");
|
||||
log.error("prometheus", e);
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof heartbeat.ping === "number") {
|
||||
monitorResponseTime.set(this.monitorLabelValues, heartbeat.ping);
|
||||
} else {
|
||||
// Is it good?
|
||||
monitorResponseTime.set(this.monitorLabelValues, -1);
|
||||
}
|
||||
} catch (e) {
|
||||
log.error("prometheus", "Caught error");
|
||||
log.error("prometheus", e);
|
||||
}
|
||||
} catch (e) {
|
||||
log.error("prometheus", "Caught error");
|
||||
log.error("prometheus", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -11,11 +11,10 @@ const { R } = require("redbean-node");
|
||||
const apicache = require("../modules/apicache");
|
||||
const Monitor = require("../model/monitor");
|
||||
const dayjs = require("dayjs");
|
||||
const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log } = require("../../src/util");
|
||||
const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log, badgeConstants } = require("../../src/util");
|
||||
const StatusPage = require("../model/status_page");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const { makeBadge } = require("badge-maker");
|
||||
const { badgeConstants } = require("../config");
|
||||
const { Prometheus } = require("../prometheus");
|
||||
const Database = require("../database");
|
||||
const { UptimeCalculator } = require("../uptime-calculator");
|
||||
|
@@ -4,7 +4,7 @@ const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const StatusPage = require("../model/status_page");
|
||||
const { allowDevAllOrigin, sendHttpError } = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const { badgeConstants } = require("../config");
|
||||
const { badgeConstants } = require("../../src/util");
|
||||
const { makeBadge } = require("badge-maker");
|
||||
const { UptimeCalculator } = require("../uptime-calculator");
|
||||
|
||||
@@ -40,16 +40,12 @@ router.get("/api/status-page/:slug", cache("5 minutes"), async (request, respons
|
||||
]);
|
||||
|
||||
if (!statusPage) {
|
||||
sendHttpError(response, "Status Page Not Found");
|
||||
return null;
|
||||
}
|
||||
|
||||
let statusPageData = await StatusPage.getStatusPageData(statusPage);
|
||||
|
||||
if (!statusPageData) {
|
||||
sendHttpError(response, "Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Response
|
||||
response.json(statusPageData);
|
||||
|
||||
|
@@ -38,6 +38,7 @@ if (!semver.satisfies(nodeVersion, requiredNodeVersions)) {
|
||||
|
||||
const args = require("args-parser")(process.argv);
|
||||
const { sleep, log, getRandomInt, genSecret, isDev } = require("../src/util");
|
||||
const config = require("./config");
|
||||
|
||||
log.debug("server", "Arguments");
|
||||
log.debug("server", args);
|
||||
@@ -46,9 +47,17 @@ if (! process.env.NODE_ENV) {
|
||||
process.env.NODE_ENV = "production";
|
||||
}
|
||||
|
||||
if (!process.env.UPTIME_KUMA_WS_ORIGIN_CHECK) {
|
||||
process.env.UPTIME_KUMA_WS_ORIGIN_CHECK = "cors-like";
|
||||
}
|
||||
|
||||
log.info("server", "Env: " + process.env.NODE_ENV);
|
||||
log.debug("server", "Inside Container: " + (process.env.UPTIME_KUMA_IS_CONTAINER === "1"));
|
||||
|
||||
if (process.env.UPTIME_KUMA_WS_ORIGIN_CHECK === "bypass") {
|
||||
log.warn("server", "WebSocket Origin Check: " + process.env.UPTIME_KUMA_WS_ORIGIN_CHECK);
|
||||
}
|
||||
|
||||
const checkVersion = require("./check-version");
|
||||
log.info("server", "Uptime Kuma Version: " + checkVersion.version);
|
||||
|
||||
@@ -72,8 +81,7 @@ const notp = require("notp");
|
||||
const base32 = require("thirty-two");
|
||||
|
||||
const { UptimeKumaServer } = require("./uptime-kuma-server");
|
||||
|
||||
const server = UptimeKumaServer.getInstance(args);
|
||||
const server = UptimeKumaServer.getInstance();
|
||||
const io = module.exports.io = server.io;
|
||||
const app = server.app;
|
||||
|
||||
@@ -82,7 +90,7 @@ const Monitor = require("./model/monitor");
|
||||
const User = require("./model/user");
|
||||
|
||||
log.debug("server", "Importing Settings");
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, FBSD, doubleCheckPassword, startE2eTests, shake256, SHAKE256_LENGTH, allowDevAllOrigin,
|
||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, doubleCheckPassword, shake256, SHAKE256_LENGTH, allowDevAllOrigin,
|
||||
} = require("./util-server");
|
||||
|
||||
log.debug("server", "Importing Notification");
|
||||
@@ -100,19 +108,13 @@ const { apiAuth } = require("./auth");
|
||||
const { login } = require("./auth");
|
||||
const passwordHash = require("./password-hash");
|
||||
|
||||
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
|
||||
// Dual-stack support for (::)
|
||||
// Also read HOST if not FreeBSD, as HOST is a system environment variable in FreeBSD
|
||||
let hostEnv = FBSD ? null : process.env.HOST;
|
||||
let hostname = args.host || process.env.UPTIME_KUMA_HOST || hostEnv;
|
||||
const hostname = config.hostname;
|
||||
|
||||
if (hostname) {
|
||||
log.info("server", "Custom hostname: " + hostname);
|
||||
}
|
||||
|
||||
const port = [ args.port, process.env.UPTIME_KUMA_PORT, process.env.PORT, 3001 ]
|
||||
.map(portValue => parseInt(portValue))
|
||||
.find(portValue => !isNaN(portValue));
|
||||
const port = config.port;
|
||||
|
||||
const disableFrameSameOrigin = !!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false;
|
||||
const cloudflaredToken = args["cloudflared-token"] || process.env.UPTIME_KUMA_CLOUDFLARED_TOKEN || undefined;
|
||||
@@ -128,7 +130,6 @@ const twoFAVerifyOptions = {
|
||||
* @type {boolean}
|
||||
*/
|
||||
const testMode = !!args["test"] || false;
|
||||
const e2eTestMode = !!args["e2e"] || false;
|
||||
|
||||
// Must be after io instantiation
|
||||
const { sendNotificationList, sendHeartbeatList, sendInfo, sendProxyList, sendDockerHostList, sendAPIKeyList, sendRemoteBrowserList } = require("./client");
|
||||
@@ -144,11 +145,11 @@ const { maintenanceSocketHandler } = require("./socket-handlers/maintenance-sock
|
||||
const { apiKeySocketHandler } = require("./socket-handlers/api-key-socket-handler");
|
||||
const { generalSocketHandler } = require("./socket-handlers/general-socket-handler");
|
||||
const { Settings } = require("./settings");
|
||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||
const apicache = require("./modules/apicache");
|
||||
const { resetChrome } = require("./monitor-types/real-browser-monitor-type");
|
||||
const { EmbeddedMariaDB } = require("./embedded-mariadb");
|
||||
const { SetupDatabase } = require("./setup-database");
|
||||
const { chartSocketHandler } = require("./socket-handlers/chart-socket-handler");
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
@@ -294,7 +295,7 @@ let needSetup = false;
|
||||
log.debug("server", "Adding socket handler");
|
||||
io.on("connection", async (socket) => {
|
||||
|
||||
sendInfo(socket, true);
|
||||
await sendInfo(socket, true);
|
||||
|
||||
if (needSetup) {
|
||||
log.info("server", "Redirect to setup page");
|
||||
@@ -326,7 +327,7 @@ let needSetup = false;
|
||||
}
|
||||
|
||||
log.debug("auth", "afterLogin");
|
||||
afterLogin(socket, user);
|
||||
await afterLogin(socket, user);
|
||||
log.debug("auth", "afterLogin ok");
|
||||
|
||||
log.info("auth", `Successfully logged in user ${decoded.username}. IP=${clientIP}`);
|
||||
@@ -382,7 +383,7 @@ let needSetup = false;
|
||||
|
||||
if (user) {
|
||||
if (user.twofa_status === 0) {
|
||||
afterLogin(socket, user);
|
||||
await afterLogin(socket, user);
|
||||
|
||||
log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
|
||||
|
||||
@@ -405,7 +406,7 @@ let needSetup = false;
|
||||
let verify = notp.totp.verify(data.token, user.twofa_secret, twoFAVerifyOptions);
|
||||
|
||||
if (user.twofa_last_token !== data.token && verify) {
|
||||
afterLogin(socket, user);
|
||||
await afterLogin(socket, user);
|
||||
|
||||
await R.exec("UPDATE `user` SET twofa_last_token = ? WHERE id = ? ", [
|
||||
data.token,
|
||||
@@ -986,7 +987,7 @@ let needSetup = false;
|
||||
log.info("manage", `Delete Monitor: ${monitorID} User ID: ${socket.userID}`);
|
||||
|
||||
if (monitorID in server.monitorList) {
|
||||
server.monitorList[monitorID].stop();
|
||||
await server.monitorList[monitorID].stop();
|
||||
delete server.monitorList[monitorID];
|
||||
}
|
||||
|
||||
@@ -1266,8 +1267,11 @@ let needSetup = false;
|
||||
let user = await doubleCheckPassword(socket, password.currentPassword);
|
||||
await user.resetPassword(password.newPassword);
|
||||
|
||||
server.disconnectAllSocketClients(user.id, socket.id);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
token: User.createJWT(user, server.jwtSecret),
|
||||
msg: "successAuthChangePassword",
|
||||
msgi18n: true,
|
||||
});
|
||||
@@ -1316,14 +1320,18 @@ let needSetup = false;
|
||||
await doubleCheckPassword(socket, currentPassword);
|
||||
}
|
||||
|
||||
// Log out all clients if enabling auth
|
||||
// GHSA-23q2-5gf8-gjpp
|
||||
if (currentDisabledAuth && !data.disableAuth) {
|
||||
server.disconnectAllSocketClients(socket.userID, socket.id);
|
||||
}
|
||||
|
||||
const previousChromeExecutable = await Settings.get("chromeExecutable");
|
||||
const previousNSCDStatus = await Settings.get("nscd");
|
||||
|
||||
await setSettings("general", data);
|
||||
server.entryPage = data.entryPage;
|
||||
|
||||
await CacheableDnsHttpAgent.update();
|
||||
|
||||
// Also need to apply timezone globally
|
||||
if (data.serverTimezone) {
|
||||
await server.setTimezone(data.serverTimezone);
|
||||
@@ -1350,8 +1358,8 @@ let needSetup = false;
|
||||
msgi18n: true,
|
||||
});
|
||||
|
||||
sendInfo(socket);
|
||||
server.sendMaintenanceList(socket);
|
||||
await sendInfo(socket);
|
||||
await server.sendMaintenanceList(socket);
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
@@ -1490,6 +1498,14 @@ let needSetup = false;
|
||||
log.info("manage", `Clear Statistics User ID: ${socket.userID}`);
|
||||
|
||||
await R.exec("DELETE FROM heartbeat");
|
||||
await R.exec("DELETE FROM stat_daily");
|
||||
await R.exec("DELETE FROM stat_hourly");
|
||||
await R.exec("DELETE FROM stat_minutely");
|
||||
|
||||
// Restart all monitors to reset the stats
|
||||
for (let monitorID in server.monitorList) {
|
||||
await restartMonitor(socket.userID, monitorID);
|
||||
}
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
@@ -1513,6 +1529,7 @@ let needSetup = false;
|
||||
apiKeySocketHandler(socket);
|
||||
remoteBrowserSocketHandler(socket);
|
||||
generalSocketHandler(socket, server);
|
||||
chartSocketHandler(socket);
|
||||
|
||||
log.debug("server", "added all socket handlers");
|
||||
|
||||
@@ -1523,9 +1540,10 @@ let needSetup = false;
|
||||
log.debug("auth", "check auto login");
|
||||
if (await setting("disableAuth")) {
|
||||
log.info("auth", "Disabled Auth: auto login to admin");
|
||||
afterLogin(socket, await R.findOne("user"));
|
||||
await afterLogin(socket, await R.findOne("user"));
|
||||
socket.emit("autoLogin");
|
||||
} else {
|
||||
socket.emit("loginRequired");
|
||||
log.debug("auth", "need auth");
|
||||
}
|
||||
|
||||
@@ -1539,7 +1557,7 @@ let needSetup = false;
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
server.start();
|
||||
await server.start();
|
||||
|
||||
server.httpServer.listen(port, hostname, () => {
|
||||
if (hostname) {
|
||||
@@ -1549,10 +1567,6 @@ let needSetup = false;
|
||||
}
|
||||
startMonitors();
|
||||
checkVersion.startInterval();
|
||||
|
||||
if (e2eTestMode) {
|
||||
startE2eTests();
|
||||
}
|
||||
});
|
||||
|
||||
await initBackgroundJobs();
|
||||
@@ -1614,15 +1628,15 @@ async function afterLogin(socket, user) {
|
||||
socket.join(user.id);
|
||||
|
||||
let monitorList = await server.sendMonitorList(socket);
|
||||
sendInfo(socket);
|
||||
server.sendMaintenanceList(socket);
|
||||
sendNotificationList(socket);
|
||||
sendProxyList(socket);
|
||||
sendDockerHostList(socket);
|
||||
sendAPIKeyList(socket);
|
||||
sendRemoteBrowserList(socket);
|
||||
|
||||
await sleep(500);
|
||||
await Promise.allSettled([
|
||||
sendInfo(socket),
|
||||
server.sendMaintenanceList(socket),
|
||||
sendNotificationList(socket),
|
||||
sendProxyList(socket),
|
||||
sendDockerHostList(socket),
|
||||
sendAPIKeyList(socket),
|
||||
sendRemoteBrowserList(socket),
|
||||
]);
|
||||
|
||||
await StatusPage.sendStatusPageList(io, socket);
|
||||
|
||||
@@ -1698,11 +1712,11 @@ async function startMonitor(userID, monitorID) {
|
||||
]);
|
||||
|
||||
if (monitor.id in server.monitorList) {
|
||||
server.monitorList[monitor.id].stop();
|
||||
await server.monitorList[monitor.id].stop();
|
||||
}
|
||||
|
||||
server.monitorList[monitor.id] = monitor;
|
||||
monitor.start(io);
|
||||
await monitor.start(io);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1732,7 +1746,7 @@ async function pauseMonitor(userID, monitorID) {
|
||||
]);
|
||||
|
||||
if (monitorID in server.monitorList) {
|
||||
server.monitorList[monitorID].stop();
|
||||
await server.monitorList[monitorID].stop();
|
||||
server.monitorList[monitorID].active = 0;
|
||||
}
|
||||
}
|
||||
@@ -1749,7 +1763,7 @@ async function startMonitors() {
|
||||
}
|
||||
|
||||
for (let monitor of list) {
|
||||
monitor.start(io);
|
||||
await monitor.start(io);
|
||||
// Give some delays, so all monitors won't make request at the same moment when just start the server.
|
||||
await sleep(getRandomInt(300, 1000));
|
||||
}
|
||||
@@ -1770,7 +1784,7 @@ async function shutdownFunction(signal) {
|
||||
log.info("server", "Stopping all monitors");
|
||||
for (let id in server.monitorList) {
|
||||
let monitor = server.monitorList[id];
|
||||
monitor.stop();
|
||||
await monitor.stop();
|
||||
}
|
||||
await sleep(2000);
|
||||
await Database.close();
|
||||
|
@@ -74,7 +74,7 @@ class SetupDatabase {
|
||||
dbConfig.type = process.env.UPTIME_KUMA_DB_TYPE;
|
||||
dbConfig.hostname = process.env.UPTIME_KUMA_DB_HOSTNAME;
|
||||
dbConfig.port = process.env.UPTIME_KUMA_DB_PORT;
|
||||
dbConfig.database = process.env.UPTIME_KUMA_DB_NAME;
|
||||
dbConfig.dbName = process.env.UPTIME_KUMA_DB_NAME;
|
||||
dbConfig.username = process.env.UPTIME_KUMA_DB_USERNAME;
|
||||
dbConfig.password = process.env.UPTIME_KUMA_DB_PASSWORD;
|
||||
Database.writeDBConfig(dbConfig);
|
||||
|
38
server/socket-handlers/chart-socket-handler.js
Normal file
38
server/socket-handlers/chart-socket-handler.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const { checkLogin } = require("../util-server");
|
||||
const { UptimeCalculator } = require("../uptime-calculator");
|
||||
const { log } = require("../../src/util");
|
||||
|
||||
module.exports.chartSocketHandler = (socket) => {
|
||||
socket.on("getMonitorChartData", async (monitorID, period, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
log.debug("monitor", `Get Monitor Chart Data: ${monitorID} User ID: ${socket.userID}`);
|
||||
|
||||
if (period == null) {
|
||||
throw new Error("Invalid period.");
|
||||
}
|
||||
|
||||
let uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitorID);
|
||||
|
||||
let data;
|
||||
if (period <= 24) {
|
||||
data = uptimeCalculator.getDataArray(period * 60, "minute");
|
||||
} else if (period <= 720) {
|
||||
data = uptimeCalculator.getDataArray(period, "hour");
|
||||
} else {
|
||||
data = uptimeCalculator.getDataArray(period / 24, "day");
|
||||
}
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
data,
|
||||
});
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
@@ -27,7 +27,7 @@ module.exports = (socket) => {
|
||||
socket.on("shrinkDatabase", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
Database.shrink();
|
||||
await Database.shrink();
|
||||
callback({
|
||||
ok: true,
|
||||
});
|
||||
|
@@ -109,4 +109,14 @@ module.exports.generalSocketHandler = (socket, server) => {
|
||||
msg: "Not found",
|
||||
});
|
||||
});
|
||||
|
||||
// Disconnect all other socket clients of the user
|
||||
socket.on("disconnectOtherSocketClients", async () => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
server.disconnectAllSocketClients(socket.userID, socket.id);
|
||||
} catch (e) {
|
||||
log.warn("disconnectAllSocketClients", e.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -148,13 +148,14 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
config.logo = `/upload/${filename}?t=` + Date.now();
|
||||
|
||||
} else {
|
||||
config.icon = imgDataUrl;
|
||||
config.logo = imgDataUrl;
|
||||
}
|
||||
|
||||
statusPage.slug = config.slug;
|
||||
statusPage.title = config.title;
|
||||
statusPage.description = config.description;
|
||||
statusPage.icon = config.logo;
|
||||
statusPage.autoRefreshInterval = config.autoRefreshInterval,
|
||||
statusPage.theme = config.theme;
|
||||
//statusPage.published = ;
|
||||
//statusPage.search_engine_index = ;
|
||||
@@ -280,6 +281,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||
statusPage.title = title;
|
||||
statusPage.theme = "auto";
|
||||
statusPage.icon = "";
|
||||
statusPage.autoRefreshInterval = 300;
|
||||
await R.store(statusPage);
|
||||
|
||||
callback({
|
||||
|
@@ -34,16 +34,25 @@ class UptimeCalculator {
|
||||
*/
|
||||
minutelyUptimeDataList = new LimitQueue(24 * 60);
|
||||
|
||||
/**
|
||||
* Recent 30-day uptime, each item is a 1-hour interval
|
||||
* Key: {number} DivisionKey
|
||||
* @type {LimitQueue<number,string>}
|
||||
*/
|
||||
hourlyUptimeDataList = new LimitQueue(30 * 24);
|
||||
|
||||
/**
|
||||
* Daily uptime data,
|
||||
* Key: {number} DailyKey
|
||||
*/
|
||||
dailyUptimeDataList = new LimitQueue(365);
|
||||
|
||||
lastDailyUptimeData = null;
|
||||
lastUptimeData = null;
|
||||
lastHourlyUptimeData = null;
|
||||
lastDailyUptimeData = null;
|
||||
|
||||
lastDailyStatBean = null;
|
||||
lastHourlyStatBean = null;
|
||||
lastMinutelyStatBean = null;
|
||||
|
||||
/**
|
||||
@@ -53,6 +62,10 @@ class UptimeCalculator {
|
||||
* @returns {Promise<UptimeCalculator>} UptimeCalculator
|
||||
*/
|
||||
static async getUptimeCalculator(monitorID) {
|
||||
if (!monitorID) {
|
||||
throw new Error("Monitor ID is required");
|
||||
}
|
||||
|
||||
if (!UptimeCalculator.list[monitorID]) {
|
||||
UptimeCalculator.list[monitorID] = new UptimeCalculator();
|
||||
await UptimeCalculator.list[monitorID].init(monitorID);
|
||||
@@ -103,27 +116,73 @@ class UptimeCalculator {
|
||||
]);
|
||||
|
||||
for (let bean of minutelyStatBeans) {
|
||||
let key = bean.timestamp;
|
||||
this.minutelyUptimeDataList.push(key, {
|
||||
let data = {
|
||||
up: bean.up,
|
||||
down: bean.down,
|
||||
avgPing: bean.ping,
|
||||
});
|
||||
minPing: bean.pingMin,
|
||||
maxPing: bean.pingMax,
|
||||
};
|
||||
|
||||
if (bean.extras != null) {
|
||||
data = {
|
||||
...data,
|
||||
...JSON.parse(bean.extras),
|
||||
};
|
||||
}
|
||||
|
||||
let key = bean.timestamp;
|
||||
this.minutelyUptimeDataList.push(key, data);
|
||||
}
|
||||
|
||||
// Load hourly data from database (recent 30 days only)
|
||||
let hourlyStatBeans = await R.find("stat_hourly", " monitor_id = ? AND timestamp > ? ORDER BY timestamp", [
|
||||
monitorID,
|
||||
this.getHourlyKey(now.subtract(30, "day")),
|
||||
]);
|
||||
|
||||
for (let bean of hourlyStatBeans) {
|
||||
let data = {
|
||||
up: bean.up,
|
||||
down: bean.down,
|
||||
avgPing: bean.ping,
|
||||
minPing: bean.pingMin,
|
||||
maxPing: bean.pingMax,
|
||||
};
|
||||
|
||||
if (bean.extras != null) {
|
||||
data = {
|
||||
...data,
|
||||
...JSON.parse(bean.extras),
|
||||
};
|
||||
}
|
||||
|
||||
this.hourlyUptimeDataList.push(bean.timestamp, data);
|
||||
}
|
||||
|
||||
// Load daily data from database (recent 365 days only)
|
||||
let dailyStatBeans = await R.find("stat_daily", " monitor_id = ? AND timestamp > ? ORDER BY timestamp", [
|
||||
monitorID,
|
||||
this.getDailyKey(now.subtract(365, "day").unix()),
|
||||
this.getDailyKey(now.subtract(365, "day")),
|
||||
]);
|
||||
|
||||
for (let bean of dailyStatBeans) {
|
||||
let key = bean.timestamp;
|
||||
this.dailyUptimeDataList.push(key, {
|
||||
let data = {
|
||||
up: bean.up,
|
||||
down: bean.down,
|
||||
avgPing: bean.ping,
|
||||
});
|
||||
minPing: bean.pingMin,
|
||||
maxPing: bean.pingMax,
|
||||
};
|
||||
|
||||
if (bean.extras != null) {
|
||||
data = {
|
||||
...data,
|
||||
...JSON.parse(bean.extras),
|
||||
};
|
||||
}
|
||||
|
||||
this.dailyUptimeDataList.push(bean.timestamp, data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,11 +195,6 @@ class UptimeCalculator {
|
||||
async update(status, ping = 0) {
|
||||
let date = this.getCurrentDate();
|
||||
|
||||
// Don't count MAINTENANCE into uptime
|
||||
if (status === MAINTENANCE) {
|
||||
return date;
|
||||
}
|
||||
|
||||
let flatStatus = this.flatStatus(status);
|
||||
|
||||
if (flatStatus === DOWN && ping > 0) {
|
||||
@@ -148,13 +202,21 @@ class UptimeCalculator {
|
||||
}
|
||||
|
||||
let divisionKey = this.getMinutelyKey(date);
|
||||
let dailyKey = this.getDailyKey(divisionKey);
|
||||
let hourlyKey = this.getHourlyKey(date);
|
||||
let dailyKey = this.getDailyKey(date);
|
||||
|
||||
let minutelyData = this.minutelyUptimeDataList[divisionKey];
|
||||
let hourlyData = this.hourlyUptimeDataList[hourlyKey];
|
||||
let dailyData = this.dailyUptimeDataList[dailyKey];
|
||||
|
||||
if (flatStatus === UP) {
|
||||
if (status === MAINTENANCE) {
|
||||
minutelyData.maintenance = minutelyData.maintenance ? minutelyData.maintenance + 1 : 1;
|
||||
hourlyData.maintenance = hourlyData.maintenance ? hourlyData.maintenance + 1 : 1;
|
||||
dailyData.maintenance = dailyData.maintenance ? dailyData.maintenance + 1 : 1;
|
||||
|
||||
} else if (flatStatus === UP) {
|
||||
minutelyData.up += 1;
|
||||
hourlyData.up += 1;
|
||||
dailyData.up += 1;
|
||||
|
||||
// Only UP status can update the ping
|
||||
@@ -163,32 +225,57 @@ class UptimeCalculator {
|
||||
// The first beat of the minute, the ping is the current ping
|
||||
if (minutelyData.up === 1) {
|
||||
minutelyData.avgPing = ping;
|
||||
minutelyData.minPing = ping;
|
||||
minutelyData.maxPing = ping;
|
||||
} else {
|
||||
minutelyData.avgPing = (minutelyData.avgPing * (minutelyData.up - 1) + ping) / minutelyData.up;
|
||||
minutelyData.minPing = Math.min(minutelyData.minPing, ping);
|
||||
minutelyData.maxPing = Math.max(minutelyData.maxPing, ping);
|
||||
}
|
||||
|
||||
// Add avg ping
|
||||
// The first beat of the hour, the ping is the current ping
|
||||
if (hourlyData.up === 1) {
|
||||
hourlyData.avgPing = ping;
|
||||
hourlyData.minPing = ping;
|
||||
hourlyData.maxPing = ping;
|
||||
} else {
|
||||
hourlyData.avgPing = (hourlyData.avgPing * (hourlyData.up - 1) + ping) / hourlyData.up;
|
||||
hourlyData.minPing = Math.min(hourlyData.minPing, ping);
|
||||
hourlyData.maxPing = Math.max(hourlyData.maxPing, ping);
|
||||
}
|
||||
|
||||
// Add avg ping (daily)
|
||||
// The first beat of the day, the ping is the current ping
|
||||
if (minutelyData.up === 1) {
|
||||
if (dailyData.up === 1) {
|
||||
dailyData.avgPing = ping;
|
||||
dailyData.minPing = ping;
|
||||
dailyData.maxPing = ping;
|
||||
} else {
|
||||
dailyData.avgPing = (dailyData.avgPing * (dailyData.up - 1) + ping) / dailyData.up;
|
||||
dailyData.minPing = Math.min(dailyData.minPing, ping);
|
||||
dailyData.maxPing = Math.max(dailyData.maxPing, ping);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
} else if (flatStatus === DOWN) {
|
||||
minutelyData.down += 1;
|
||||
hourlyData.down += 1;
|
||||
dailyData.down += 1;
|
||||
}
|
||||
|
||||
if (dailyData !== this.lastDailyUptimeData) {
|
||||
this.lastDailyUptimeData = dailyData;
|
||||
}
|
||||
|
||||
if (minutelyData !== this.lastUptimeData) {
|
||||
this.lastUptimeData = minutelyData;
|
||||
}
|
||||
|
||||
if (hourlyData !== this.lastHourlyUptimeData) {
|
||||
this.lastHourlyUptimeData = hourlyData;
|
||||
}
|
||||
|
||||
if (dailyData !== this.lastDailyUptimeData) {
|
||||
this.lastDailyUptimeData = dailyData;
|
||||
}
|
||||
|
||||
// Don't store data in test mode
|
||||
if (process.env.TEST_BACKEND) {
|
||||
log.debug("uptime-calc", "Skip storing data in test mode");
|
||||
@@ -199,12 +286,45 @@ class UptimeCalculator {
|
||||
dailyStatBean.up = dailyData.up;
|
||||
dailyStatBean.down = dailyData.down;
|
||||
dailyStatBean.ping = dailyData.avgPing;
|
||||
dailyStatBean.pingMin = dailyData.minPing;
|
||||
dailyStatBean.pingMax = dailyData.maxPing;
|
||||
{
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = dailyData;
|
||||
if (Object.keys(extras).length > 0) {
|
||||
dailyStatBean.extras = JSON.stringify(extras);
|
||||
}
|
||||
}
|
||||
await R.store(dailyStatBean);
|
||||
|
||||
let hourlyStatBean = await this.getHourlyStatBean(hourlyKey);
|
||||
hourlyStatBean.up = hourlyData.up;
|
||||
hourlyStatBean.down = hourlyData.down;
|
||||
hourlyStatBean.ping = hourlyData.avgPing;
|
||||
hourlyStatBean.pingMin = hourlyData.minPing;
|
||||
hourlyStatBean.pingMax = hourlyData.maxPing;
|
||||
{
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = hourlyData;
|
||||
if (Object.keys(extras).length > 0) {
|
||||
hourlyStatBean.extras = JSON.stringify(extras);
|
||||
}
|
||||
}
|
||||
await R.store(hourlyStatBean);
|
||||
|
||||
let minutelyStatBean = await this.getMinutelyStatBean(divisionKey);
|
||||
minutelyStatBean.up = minutelyData.up;
|
||||
minutelyStatBean.down = minutelyData.down;
|
||||
minutelyStatBean.ping = minutelyData.avgPing;
|
||||
minutelyStatBean.pingMin = minutelyData.minPing;
|
||||
minutelyStatBean.pingMax = minutelyData.maxPing;
|
||||
{
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = minutelyData;
|
||||
if (Object.keys(extras).length > 0) {
|
||||
minutelyStatBean.extras = JSON.stringify(extras);
|
||||
}
|
||||
}
|
||||
await R.store(minutelyStatBean);
|
||||
|
||||
// Remove the old data
|
||||
@@ -214,6 +334,11 @@ class UptimeCalculator {
|
||||
this.getMinutelyKey(date.subtract(24, "hour")),
|
||||
]);
|
||||
|
||||
await R.exec("DELETE FROM stat_hourly WHERE monitor_id = ? AND timestamp < ?", [
|
||||
this.monitorID,
|
||||
this.getHourlyKey(date.subtract(30, "day")),
|
||||
]);
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
@@ -242,6 +367,31 @@ class UptimeCalculator {
|
||||
return this.lastDailyStatBean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hourly stat bean
|
||||
* @param {number} timestamp milliseconds
|
||||
* @returns {Promise<import("redbean-node").Bean>} stat_hourly bean
|
||||
*/
|
||||
async getHourlyStatBean(timestamp) {
|
||||
if (this.lastHourlyStatBean && this.lastHourlyStatBean.timestamp === timestamp) {
|
||||
return this.lastHourlyStatBean;
|
||||
}
|
||||
|
||||
let bean = await R.findOne("stat_hourly", " monitor_id = ? AND timestamp = ?", [
|
||||
this.monitorID,
|
||||
timestamp,
|
||||
]);
|
||||
|
||||
if (!bean) {
|
||||
bean = R.dispense("stat_hourly");
|
||||
bean.monitor_id = this.monitorID;
|
||||
bean.timestamp = timestamp;
|
||||
}
|
||||
|
||||
this.lastHourlyStatBean = bean;
|
||||
return this.lastHourlyStatBean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the minutely stat bean
|
||||
* @param {number} timestamp milliseconds
|
||||
@@ -268,11 +418,12 @@ class UptimeCalculator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert timestamp to minutely key
|
||||
* @param {dayjs.Dayjs} date The heartbeat date
|
||||
* @returns {number} Timestamp
|
||||
*/
|
||||
getMinutelyKey(date) {
|
||||
// Convert the current date to the nearest minute (e.g. 2021-01-01 12:34:56 -> 2021-01-01 12:34:00)
|
||||
// Truncate value to minutes (e.g. 2021-01-01 12:34:56 -> 2021-01-01 12:34:00)
|
||||
date = date.startOf("minute");
|
||||
|
||||
// Convert to timestamp in second
|
||||
@@ -283,6 +434,33 @@ class UptimeCalculator {
|
||||
up: 0,
|
||||
down: 0,
|
||||
avgPing: 0,
|
||||
minPing: 0,
|
||||
maxPing: 0,
|
||||
});
|
||||
}
|
||||
|
||||
return divisionKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert timestamp to hourly key
|
||||
* @param {dayjs.Dayjs} date The heartbeat date
|
||||
* @returns {number} Timestamp
|
||||
*/
|
||||
getHourlyKey(date) {
|
||||
// Truncate value to hours (e.g. 2021-01-01 12:34:56 -> 2021-01-01 12:00:00)
|
||||
date = date.startOf("hour");
|
||||
|
||||
// Convert to timestamp in second
|
||||
let divisionKey = date.unix();
|
||||
|
||||
if (! (divisionKey in this.hourlyUptimeDataList)) {
|
||||
this.hourlyUptimeDataList.push(divisionKey, {
|
||||
up: 0,
|
||||
down: 0,
|
||||
avgPing: 0,
|
||||
minPing: 0,
|
||||
maxPing: 0,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -291,13 +469,11 @@ class UptimeCalculator {
|
||||
|
||||
/**
|
||||
* Convert timestamp to daily key
|
||||
* @param {number} timestamp Timestamp
|
||||
* @param {dayjs.Dayjs} date The heartbeat date
|
||||
* @returns {number} Timestamp
|
||||
*/
|
||||
getDailyKey(timestamp) {
|
||||
let date = dayjs.unix(timestamp);
|
||||
|
||||
// Convert the date to the nearest day (e.g. 2021-01-01 12:34:56 -> 2021-01-01 00:00:00)
|
||||
getDailyKey(date) {
|
||||
// Truncate value to start of day (e.g. 2021-01-01 12:34:56 -> 2021-01-01 00:00:00)
|
||||
// Considering if the user keep changing could affect the calculation, so use UTC time to avoid this problem.
|
||||
date = date.utc().startOf("day");
|
||||
let dailyKey = date.unix();
|
||||
@@ -307,22 +483,44 @@ class UptimeCalculator {
|
||||
up: 0,
|
||||
down: 0,
|
||||
avgPing: 0,
|
||||
minPing: 0,
|
||||
maxPing: 0,
|
||||
});
|
||||
}
|
||||
|
||||
return dailyKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert timestamp to key
|
||||
* @param {dayjs.Dayjs} datetime Datetime
|
||||
* @param {"day" | "hour" | "minute"} type the type of data which is expected to be returned
|
||||
* @returns {number} Timestamp
|
||||
* @throws {Error} If the type is invalid
|
||||
*/
|
||||
getKey(datetime, type) {
|
||||
switch (type) {
|
||||
case "day":
|
||||
return this.getDailyKey(datetime);
|
||||
case "hour":
|
||||
return this.getHourlyKey(datetime);
|
||||
case "minute":
|
||||
return this.getMinutelyKey(datetime);
|
||||
default:
|
||||
throw new Error("Invalid type");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flat status to UP or DOWN
|
||||
* @param {number} status the status which schould be turned into a flat status
|
||||
* @param {number} status the status which should be turned into a flat status
|
||||
* @returns {UP|DOWN|PENDING} The flat status
|
||||
* @throws {Error} Invalid status
|
||||
*/
|
||||
flatStatus(status) {
|
||||
switch (status) {
|
||||
case UP:
|
||||
// case MAINTENANCE:
|
||||
case MAINTENANCE:
|
||||
return UP;
|
||||
case DOWN:
|
||||
case PENDING:
|
||||
@@ -333,21 +531,21 @@ class UptimeCalculator {
|
||||
|
||||
/**
|
||||
* @param {number} num the number of data points which are expected to be returned
|
||||
* @param {"day" | "minute"} type the type of data which is expected to be returned
|
||||
* @param {"day" | "hour" | "minute"} type the type of data which is expected to be returned
|
||||
* @returns {UptimeDataResult} UptimeDataResult
|
||||
* @throws {Error} The maximum number of minutes greater than 1440
|
||||
*/
|
||||
getData(num, type = "day") {
|
||||
let key;
|
||||
|
||||
if (type === "day") {
|
||||
key = this.getDailyKey(this.getCurrentDate().unix());
|
||||
} else {
|
||||
if (num > 24 * 60) {
|
||||
throw new Error("The maximum number of minutes is 1440");
|
||||
}
|
||||
key = this.getMinutelyKey(this.getCurrentDate());
|
||||
if (type === "hour" && num > 24 * 30) {
|
||||
throw new Error("The maximum number of hours is 720");
|
||||
}
|
||||
if (type === "minute" && num > 24 * 60) {
|
||||
throw new Error("The maximum number of minutes is 1440");
|
||||
}
|
||||
|
||||
// Get the current time period key based on the type
|
||||
let key = this.getKey(this.getCurrentDate(), type);
|
||||
|
||||
let total = {
|
||||
up: 0,
|
||||
@@ -357,20 +555,37 @@ class UptimeCalculator {
|
||||
let totalPing = 0;
|
||||
let endTimestamp;
|
||||
|
||||
if (type === "day") {
|
||||
endTimestamp = key - 86400 * (num - 1);
|
||||
} else {
|
||||
endTimestamp = key - 60 * (num - 1);
|
||||
// Get the eariest timestamp of the required period based on the type
|
||||
switch (type) {
|
||||
case "day":
|
||||
endTimestamp = key - 86400 * (num - 1);
|
||||
break;
|
||||
case "hour":
|
||||
endTimestamp = key - 3600 * (num - 1);
|
||||
break;
|
||||
case "minute":
|
||||
endTimestamp = key - 60 * (num - 1);
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid type");
|
||||
}
|
||||
|
||||
// Sum up all data in the specified time range
|
||||
while (key >= endTimestamp) {
|
||||
let data;
|
||||
|
||||
if (type === "day") {
|
||||
data = this.dailyUptimeDataList[key];
|
||||
} else {
|
||||
data = this.minutelyUptimeDataList[key];
|
||||
switch (type) {
|
||||
case "day":
|
||||
data = this.dailyUptimeDataList[key];
|
||||
break;
|
||||
case "hour":
|
||||
data = this.hourlyUptimeDataList[key];
|
||||
break;
|
||||
case "minute":
|
||||
data = this.minutelyUptimeDataList[key];
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid type");
|
||||
}
|
||||
|
||||
if (data) {
|
||||
@@ -379,27 +594,53 @@ class UptimeCalculator {
|
||||
totalPing += data.avgPing * data.up;
|
||||
}
|
||||
|
||||
// Previous day
|
||||
if (type === "day") {
|
||||
key -= 86400;
|
||||
} else {
|
||||
key -= 60;
|
||||
// Set key to the previous time period
|
||||
switch (type) {
|
||||
case "day":
|
||||
key -= 86400;
|
||||
break;
|
||||
case "hour":
|
||||
key -= 3600;
|
||||
break;
|
||||
case "minute":
|
||||
key -= 60;
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid type");
|
||||
}
|
||||
}
|
||||
|
||||
let uptimeData = new UptimeDataResult();
|
||||
|
||||
// If there is no data in the previous time ranges, use the last data?
|
||||
if (total.up === 0 && total.down === 0) {
|
||||
if (type === "day" && this.lastDailyUptimeData) {
|
||||
total = this.lastDailyUptimeData;
|
||||
totalPing = total.avgPing * total.up;
|
||||
} else if (type === "minute" && this.lastUptimeData) {
|
||||
total = this.lastUptimeData;
|
||||
totalPing = total.avgPing * total.up;
|
||||
} else {
|
||||
uptimeData.uptime = 0;
|
||||
uptimeData.avgPing = null;
|
||||
return uptimeData;
|
||||
switch (type) {
|
||||
case "day":
|
||||
if (this.lastDailyUptimeData) {
|
||||
total = this.lastDailyUptimeData;
|
||||
totalPing = total.avgPing * total.up;
|
||||
} else {
|
||||
return uptimeData;
|
||||
}
|
||||
break;
|
||||
case "hour":
|
||||
if (this.lastHourlyUptimeData) {
|
||||
total = this.lastHourlyUptimeData;
|
||||
totalPing = total.avgPing * total.up;
|
||||
} else {
|
||||
return uptimeData;
|
||||
}
|
||||
break;
|
||||
case "minute":
|
||||
if (this.lastUptimeData) {
|
||||
total = this.lastUptimeData;
|
||||
totalPing = total.avgPing * total.up;
|
||||
} else {
|
||||
return uptimeData;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid type");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,11 +652,94 @@ class UptimeCalculator {
|
||||
avgPing = totalPing / total.up;
|
||||
}
|
||||
|
||||
uptimeData.uptime = total.up / (total.up + total.down);
|
||||
if (total.up + total.down === 0) {
|
||||
uptimeData.uptime = 0;
|
||||
} else {
|
||||
uptimeData.uptime = total.up / (total.up + total.down);
|
||||
}
|
||||
uptimeData.avgPing = avgPing;
|
||||
return uptimeData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data in form of an array
|
||||
* @param {number} num the number of data points which are expected to be returned
|
||||
* @param {"day" | "hour" | "minute"} type the type of data which is expected to be returned
|
||||
* @returns {Array<object>} uptime data
|
||||
* @throws {Error} The maximum number of minutes greater than 1440
|
||||
*/
|
||||
getDataArray(num, type = "day") {
|
||||
if (type === "hour" && num > 24 * 30) {
|
||||
throw new Error("The maximum number of hours is 720");
|
||||
}
|
||||
if (type === "minute" && num > 24 * 60) {
|
||||
throw new Error("The maximum number of minutes is 1440");
|
||||
}
|
||||
|
||||
// Get the current time period key based on the type
|
||||
let key = this.getKey(this.getCurrentDate(), type);
|
||||
|
||||
let result = [];
|
||||
|
||||
let endTimestamp;
|
||||
|
||||
// Get the eariest timestamp of the required period based on the type
|
||||
switch (type) {
|
||||
case "day":
|
||||
endTimestamp = key - 86400 * (num - 1);
|
||||
break;
|
||||
case "hour":
|
||||
endTimestamp = key - 3600 * (num - 1);
|
||||
break;
|
||||
case "minute":
|
||||
endTimestamp = key - 60 * (num - 1);
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid type");
|
||||
}
|
||||
|
||||
// Get datapoints in the specified time range
|
||||
while (key >= endTimestamp) {
|
||||
let data;
|
||||
|
||||
switch (type) {
|
||||
case "day":
|
||||
data = this.dailyUptimeDataList[key];
|
||||
break;
|
||||
case "hour":
|
||||
data = this.hourlyUptimeDataList[key];
|
||||
break;
|
||||
case "minute":
|
||||
data = this.minutelyUptimeDataList[key];
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid type");
|
||||
}
|
||||
|
||||
if (data) {
|
||||
data.timestamp = key;
|
||||
result.push(data);
|
||||
}
|
||||
|
||||
// Set key to the previous time period
|
||||
switch (type) {
|
||||
case "day":
|
||||
key -= 86400;
|
||||
break;
|
||||
case "hour":
|
||||
key -= 3600;
|
||||
break;
|
||||
case "minute":
|
||||
key -= 60;
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid type");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the uptime data by duration
|
||||
* @param {'24h'|'30d'|'1y'} duration Only accept 24h, 30d, 1y
|
||||
@@ -446,7 +770,7 @@ class UptimeCalculator {
|
||||
* @returns {UptimeDataResult} UptimeDataResult
|
||||
*/
|
||||
get7Day() {
|
||||
return this.getData(7);
|
||||
return this.getData(168, "hour");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -464,7 +788,7 @@ class UptimeCalculator {
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {dayjs.Dayjs} Current date
|
||||
* @returns {dayjs.Dayjs} Current datetime in UTC
|
||||
*/
|
||||
getCurrentDate() {
|
||||
return dayjs.utc();
|
||||
@@ -476,12 +800,12 @@ class UptimeDataResult {
|
||||
/**
|
||||
* @type {number} Uptime
|
||||
*/
|
||||
uptime;
|
||||
uptime = 0;
|
||||
|
||||
/**
|
||||
* @type {number} Average ping
|
||||
*/
|
||||
avgPing;
|
||||
avgPing = null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
@@ -4,15 +4,15 @@ const fs = require("fs");
|
||||
const http = require("http");
|
||||
const { Server } = require("socket.io");
|
||||
const { R } = require("redbean-node");
|
||||
const { log } = require("../src/util");
|
||||
const { log, isDev } = require("../src/util");
|
||||
const Database = require("./database");
|
||||
const util = require("util");
|
||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||
const { Settings } = require("./settings");
|
||||
const dayjs = require("dayjs");
|
||||
const childProcessAsync = require("promisify-child-process");
|
||||
const path = require("path");
|
||||
const axios = require("axios");
|
||||
const { isSSL, sslKey, sslCert, sslKeyPassphrase } = require("./config");
|
||||
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
|
||||
|
||||
/**
|
||||
@@ -65,25 +65,19 @@ class UptimeKumaServer {
|
||||
/**
|
||||
* Get the current instance of the server if it exists, otherwise
|
||||
* create a new instance.
|
||||
* @param {object} args Arguments to pass to instance constructor
|
||||
* @returns {UptimeKumaServer} Server instance
|
||||
*/
|
||||
static getInstance(args) {
|
||||
static getInstance() {
|
||||
if (UptimeKumaServer.instance == null) {
|
||||
UptimeKumaServer.instance = new UptimeKumaServer(args);
|
||||
UptimeKumaServer.instance = new UptimeKumaServer();
|
||||
}
|
||||
return UptimeKumaServer.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} args Arguments to initialise server with
|
||||
*
|
||||
*/
|
||||
constructor(args) {
|
||||
// SSL
|
||||
const sslKey = args["ssl-key"] || process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
|
||||
const sslCert = args["ssl-cert"] || process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
|
||||
const sslKeyPassphrase = args["ssl-key-passphrase"] || process.env.UPTIME_KUMA_SSL_KEY_PASSPHRASE || process.env.SSL_KEY_PASSPHRASE || undefined;
|
||||
|
||||
constructor() {
|
||||
// Set axios default user-agent to Uptime-Kuma/version
|
||||
axios.defaults.headers.common["User-Agent"] = this.getUserAgent();
|
||||
|
||||
@@ -92,7 +86,7 @@ class UptimeKumaServer {
|
||||
|
||||
log.info("server", "Creating express and socket.io instance");
|
||||
this.app = express();
|
||||
if (sslKey && sslCert) {
|
||||
if (isSSL) {
|
||||
log.info("server", "Server Type: HTTPS");
|
||||
this.httpServer = https.createServer({
|
||||
key: fs.readFileSync(sslKey),
|
||||
@@ -119,8 +113,68 @@ class UptimeKumaServer {
|
||||
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
|
||||
UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType();
|
||||
UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
|
||||
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
|
||||
|
||||
this.io = new Server(this.httpServer);
|
||||
// Allow all CORS origins (polling) in development
|
||||
let cors = undefined;
|
||||
if (isDev) {
|
||||
cors = {
|
||||
origin: "*",
|
||||
};
|
||||
}
|
||||
|
||||
this.io = new Server(this.httpServer, {
|
||||
cors,
|
||||
allowRequest: async (req, callback) => {
|
||||
let transport;
|
||||
// It should be always true, but just in case, because this property is not documented
|
||||
if (req._query) {
|
||||
transport = req._query.transport;
|
||||
} else {
|
||||
log.error("socket", "Ops!!! Cannot get transport type, assume that it is polling");
|
||||
transport = "polling";
|
||||
}
|
||||
|
||||
const clientIP = await this.getClientIPwithProxy(req.connection.remoteAddress, req.headers);
|
||||
log.info("socket", `New ${transport} connection, IP = ${clientIP}`);
|
||||
|
||||
// The following check is only for websocket connections, polling connections are already protected by CORS
|
||||
if (transport === "polling") {
|
||||
callback(null, true);
|
||||
} else if (transport === "websocket") {
|
||||
const bypass = process.env.UPTIME_KUMA_WS_ORIGIN_CHECK === "bypass";
|
||||
if (bypass) {
|
||||
log.info("auth", "WebSocket origin check is bypassed");
|
||||
callback(null, true);
|
||||
} else if (!req.headers.origin) {
|
||||
log.info("auth", "WebSocket with no origin is allowed");
|
||||
callback(null, true);
|
||||
} else {
|
||||
let host = req.headers.host;
|
||||
let origin = req.headers.origin;
|
||||
|
||||
try {
|
||||
let originURL = new URL(origin);
|
||||
let xForwardedFor;
|
||||
if (await Settings.get("trustProxy")) {
|
||||
xForwardedFor = req.headers["x-forwarded-for"];
|
||||
}
|
||||
|
||||
if (host !== originURL.host && xForwardedFor !== originURL.host) {
|
||||
callback(null, false);
|
||||
log.error("auth", `Origin (${origin}) does not match host (${host}), IP: ${clientIP}`);
|
||||
} else {
|
||||
callback(null, true);
|
||||
}
|
||||
} catch (e) {
|
||||
// Invalid origin url, probably not from browser
|
||||
callback(null, false);
|
||||
log.error("auth", `Invalid origin url (${origin}), IP: ${clientIP}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,8 +185,6 @@ class UptimeKumaServer {
|
||||
// Static
|
||||
this.app.use("/screenshots", express.static(Database.screenshotDir));
|
||||
|
||||
await CacheableDnsHttpAgent.update();
|
||||
|
||||
process.env.TZ = await this.getTimezone();
|
||||
dayjs.tz.setDefault(process.env.TZ);
|
||||
log.debug("DEBUG", "Timezone: " + process.env.TZ);
|
||||
@@ -144,7 +196,7 @@ class UptimeKumaServer {
|
||||
/**
|
||||
* Send list of monitors to client
|
||||
* @param {Socket} socket Socket to send list on
|
||||
* @returns {object} List of monitors
|
||||
* @returns {Promise<object>} List of monitors
|
||||
*/
|
||||
async sendMonitorList(socket) {
|
||||
let list = await this.getMonitorJSONList(socket.userID);
|
||||
@@ -176,7 +228,7 @@ class UptimeKumaServer {
|
||||
/**
|
||||
* Send maintenance list to client
|
||||
* @param {Socket} socket Socket.io instance to send to
|
||||
* @returns {object} Maintenance list
|
||||
* @returns {Promise<object>} Maintenance list
|
||||
*/
|
||||
async sendMaintenanceList(socket) {
|
||||
return await this.sendMaintenanceListByUserID(socket.userID);
|
||||
@@ -185,7 +237,7 @@ class UptimeKumaServer {
|
||||
/**
|
||||
* Send list of maintenances to user
|
||||
* @param {number} userID User to send list to
|
||||
* @returns {object} Maintenance list
|
||||
* @returns {Promise<object>} Maintenance list
|
||||
*/
|
||||
async sendMaintenanceListByUserID(userID) {
|
||||
let list = await this.getMaintenanceJSONList(userID);
|
||||
@@ -264,20 +316,27 @@ class UptimeKumaServer {
|
||||
/**
|
||||
* Get the IP of the client connected to the socket
|
||||
* @param {Socket} socket Socket to query
|
||||
* @returns {string} IP of client
|
||||
* @returns {Promise<string>} IP of client
|
||||
*/
|
||||
async getClientIP(socket) {
|
||||
let clientIP = socket.client.conn.remoteAddress;
|
||||
getClientIP(socket) {
|
||||
return this.getClientIPwithProxy(socket.client.conn.remoteAddress, socket.client.conn.request.headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} clientIP Raw client IP
|
||||
* @param {IncomingHttpHeaders} headers HTTP headers
|
||||
* @returns {Promise<string>} Client IP with proxy (if trusted)
|
||||
*/
|
||||
async getClientIPwithProxy(clientIP, headers) {
|
||||
if (clientIP === undefined) {
|
||||
clientIP = "";
|
||||
}
|
||||
|
||||
if (await Settings.get("trustProxy")) {
|
||||
const forwardedFor = socket.client.conn.request.headers["x-forwarded-for"];
|
||||
const forwardedFor = headers["x-forwarded-for"];
|
||||
|
||||
return (typeof forwardedFor === "string" ? forwardedFor.split(",")[0].trim() : null)
|
||||
|| socket.client.conn.request.headers["x-real-ip"]
|
||||
|| headers["x-real-ip"]
|
||||
|| clientIP.replace(/^::ffff:/, "");
|
||||
} else {
|
||||
return clientIP.replace(/^::ffff:/, "");
|
||||
@@ -427,6 +486,26 @@ class UptimeKumaServer {
|
||||
getUserAgent() {
|
||||
return "Uptime-Kuma/" + require("../package.json").version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force connected sockets of a user to refresh and disconnect.
|
||||
* Used for resetting password.
|
||||
* @param {string} userID User ID
|
||||
* @param {string?} currentSocketID Current socket ID
|
||||
* @returns {void}
|
||||
*/
|
||||
disconnectAllSocketClients(userID, currentSocketID = undefined) {
|
||||
for (const socket of this.io.sockets.sockets.values()) {
|
||||
if (socket.userID === userID && socket.id !== currentSocketID) {
|
||||
try {
|
||||
socket.emit("refresh");
|
||||
socket.disconnect();
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@@ -438,3 +517,4 @@ const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor
|
||||
const { TailscalePing } = require("./monitor-types/tailscale-ping");
|
||||
const { DnsMonitorType } = require("./monitor-types/dns");
|
||||
const { MqttMonitorType } = require("./monitor-types/mqtt");
|
||||
const { MongodbMonitorType } = require("./monitor-types/mongodb");
|
||||
|
@@ -1,19 +1,16 @@
|
||||
const tcpp = require("tcp-ping");
|
||||
const ping = require("@louislam/ping");
|
||||
const { R } = require("redbean-node");
|
||||
const { log, genSecret } = require("../src/util");
|
||||
const { log, genSecret, badgeConstants } = require("../src/util");
|
||||
const passwordHash = require("./password-hash");
|
||||
const { Resolver } = require("dns");
|
||||
const childProcess = require("child_process");
|
||||
const iconv = require("iconv-lite");
|
||||
const chardet = require("chardet");
|
||||
const chroma = require("chroma-js");
|
||||
const { badgeConstants } = require("./config");
|
||||
const mssql = require("mssql");
|
||||
const { Client } = require("pg");
|
||||
const postgresConParse = require("pg-connection-string").parse;
|
||||
const mysql = require("mysql2");
|
||||
const { MongoClient } = require("mongodb");
|
||||
const { NtlmClient } = require("axios-ntlm");
|
||||
const { Settings } = require("./settings");
|
||||
const grpc = require("@grpc/grpc-js");
|
||||
@@ -131,7 +128,7 @@ exports.ping = async (hostname, size = 56) => {
|
||||
return await exports.pingAsync(hostname, false, size);
|
||||
} catch (e) {
|
||||
// If the host cannot be resolved, try again with ipv6
|
||||
console.debug("ping", "IPv6 error message: " + e.message);
|
||||
log.debug("ping", "IPv6 error message: " + e.message);
|
||||
|
||||
// As node-ping does not report a specific error for this, try again if it is an empty message with ipv6 no matter what.
|
||||
if (!e.message) {
|
||||
@@ -440,24 +437,6 @@ exports.mysqlQuery = function (connectionString, query, password = undefined) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Connect to and ping a MongoDB database
|
||||
* @param {string} connectionString The database connection string
|
||||
* @returns {Promise<(string[] | object[] | object)>} Response from
|
||||
* server
|
||||
*/
|
||||
exports.mongodbPing = async function (connectionString) {
|
||||
let client = await MongoClient.connect(connectionString);
|
||||
let dbPing = await client.db().command({ ping: 1 });
|
||||
await client.close();
|
||||
|
||||
if (dbPing["ok"] === 1) {
|
||||
return "UP";
|
||||
} else {
|
||||
throw Error("failed");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Query radius server
|
||||
* @param {string} hostname Hostname of radius server
|
||||
@@ -508,12 +487,16 @@ exports.radius = function (
|
||||
/**
|
||||
* Redis server ping
|
||||
* @param {string} dsn The redis connection string
|
||||
* @returns {Promise<any>} Response from redis server
|
||||
* @param {boolean} rejectUnauthorized If false, allows unverified server certificates.
|
||||
* @returns {Promise<any>} Response from server
|
||||
*/
|
||||
exports.redisPingAsync = function (dsn) {
|
||||
exports.redisPingAsync = function (dsn, rejectUnauthorized) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = redis.createClient({
|
||||
url: dsn
|
||||
url: dsn,
|
||||
socket: {
|
||||
rejectUnauthorized
|
||||
}
|
||||
});
|
||||
client.on("error", (err) => {
|
||||
if (client.isOpen) {
|
||||
@@ -656,21 +639,27 @@ const parseCertificateInfo = function (info) {
|
||||
|
||||
/**
|
||||
* Check if certificate is valid
|
||||
* @param {object} res Response object from axios
|
||||
* @param {tls.TLSSocket} socket TLSSocket, which may or may not be connected
|
||||
* @returns {object} Object containing certificate information
|
||||
* @throws No socket was found to check certificate for
|
||||
*/
|
||||
exports.checkCertificate = function (res) {
|
||||
if (!res.request.res.socket) {
|
||||
throw new Error("No socket found");
|
||||
exports.checkCertificate = function (socket) {
|
||||
let certInfoStartTime = dayjs().valueOf();
|
||||
|
||||
// Return null if there is no socket
|
||||
if (socket === undefined || socket == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const info = res.request.res.socket.getPeerCertificate(true);
|
||||
const valid = res.request.res.socket.authorized || false;
|
||||
const info = socket.getPeerCertificate(true);
|
||||
const valid = socket.authorized || false;
|
||||
|
||||
log.debug("cert", "Parsing Certificate Info");
|
||||
const parsedInfo = parseCertificateInfo(info);
|
||||
|
||||
if (process.env.TIMELOGGER === "1") {
|
||||
log.debug("monitor", "Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms");
|
||||
}
|
||||
|
||||
return {
|
||||
valid: valid,
|
||||
certInfo: parsedInfo
|
||||
@@ -799,29 +788,6 @@ exports.doubleCheckPassword = async (socket, currentPassword) => {
|
||||
return user;
|
||||
};
|
||||
|
||||
/**
|
||||
* Start end-to-end tests
|
||||
* @returns {void}
|
||||
*/
|
||||
exports.startE2eTests = async () => {
|
||||
console.log("Starting unit test...");
|
||||
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
||||
const child = childProcess.spawn(npm, [ "run", "cy:run" ]);
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
console.log(data.toString());
|
||||
});
|
||||
|
||||
child.stderr.on("data", (data) => {
|
||||
console.log(data.toString());
|
||||
});
|
||||
|
||||
child.on("close", function (code) {
|
||||
console.log("Jest exit code: " + code);
|
||||
process.exit(code);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert unknown string to UTF8
|
||||
* @param {Uint8Array} body Buffer
|
||||
|
Reference in New Issue
Block a user